diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed916da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,102 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# IPython Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# dotenv
+.env
+
+# virtualenv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+
+# Rope project settings
+.ropeproject
+
+
+#Vis related stuff
+config.py
+vis.sock
+vis.log
+app/json_save_states
+
+# Mac stuff
+.DS_Store
+
+#Other stuff
+.idea/
diff --git a/LICENSE b/LICENSE
index 9cecc1d..5604901 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,21 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- {one line to give the program's name and a brief idea of what it does.}
- Copyright (C) {year} {name of author}
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- {project} Copyright (C) {year} {fullname}
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+The MIT License
+
+Copyright © 2010-2017 three.js authors
+
+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
index 23b3309..fcd9363 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,35 @@
-# Vis
-A 3D/2D WYSIWYG Visualizer to aid CEESIM operations
+# Vis - Web Visualizer
+A 3D WYSIWYG Visualizer to aid Northrop Grumman CEESIM operations
+
+---
+
+## What Is It?
+Our program is an extension of the three.js development evironment modified to align with Northrop Grumman's Combat Magnetic Environment Simulator (CEESIM). Using this program you will be able to add, remove, and manipulate various antenna placements on a 3D aircraft model to simulate a realistic arrangment one might encounter in the field.
+
+---
+
+## How To Run
+
+### For Users
+
+If you're interested in trying out Vis yourself, please visit http://vis.ipwnage.com. From there, you'll be able to upload models, add antennas, and modify wireframe status easily.
+
+### For Developers
+
+If you're interested in contributing to Vis, or looking for deploy your own installation, setup is a breeze.
+
+1. Clone the repository: `git clone https://github.com/TheVindicators/Vis.git`
+2. Install Python2.7 for your respective operating system (https://www.python.org/download/releases/2.7/)
+3. Install virtualenv (*nix/Mac: `sudo pip install virtualenv` Windows: `pip install virtualenv` )
+4. Create a virtualenv in your Vis directory: `virtualenv venv`
+5. Activate the virtualenv (*nix/Mac: `. venv/bin/activate` Windows: `venv\Scripts\activate.bat`)
+6. Install all the requirements: `pip install -r requirements.txt`
+7. Copy `config_sample.py` and name is `config.py`
+
+That's it. To run the server, run `manage.py runserver`
+
+If you're looking to deploy this for production use, it's recommended you use the uWSGI files in the `uwsgi` folder and deploy Vis as a service.
+
+---
+
+*Team: Nitin Garg, Christian Evans, David Lohle, and Joshua Waney*
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..be7d762
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,27 @@
+from flask import Flask, render_template
+from config import config
+import os
+
+#Initialize object methods
+
+def create_app(config_name):
+ app = Flask(__name__)
+ app.config.from_object(config[config_name])
+ config[config_name].init_app
+
+ if not os.path.exists(app.config["JSON_STORE_DATA"]):
+ os.makedirs(app.config["JSON_STORE_DATA"])
+
+ #properly expose IP addresses if being reversed proxy
+# app.wsgi_app = ProxyFix(app.wsgi_app)
+
+ #Register main web interface as blueprint
+ from main import main as main_blueprint
+ app.register_blueprint(main_blueprint)
+
+ #Register REST server-client API
+ from rest import rest as rest_blueprint
+ app.register_blueprint(rest_blueprint, url_prefix='/rest')
+
+
+ return app
diff --git a/app/main/__init__.py b/app/main/__init__.py
new file mode 100644
index 0000000..c519b42
--- /dev/null
+++ b/app/main/__init__.py
@@ -0,0 +1,4 @@
+from flask import Blueprint
+main = Blueprint('main', __name__)
+
+from . import views
diff --git a/app/main/views.py b/app/main/views.py
new file mode 100644
index 0000000..a279a6c
--- /dev/null
+++ b/app/main/views.py
@@ -0,0 +1,10 @@
+from flask import render_template, current_app
+from . import main
+import os
+
+@main.route('/')
+def index():
+ states = []
+ for save_state in os.listdir(current_app.config["JSON_STORE_DATA"]):
+ states.append(save_state[:-5]) #Only return the UUID, remove .json ending
+ return render_template('editor.html', states=states)
diff --git a/app/rest/__init__.py b/app/rest/__init__.py
new file mode 100644
index 0000000..8dbf92f
--- /dev/null
+++ b/app/rest/__init__.py
@@ -0,0 +1,4 @@
+from flask import Blueprint
+rest = Blueprint('rest', __name__)
+
+from . import views
diff --git a/app/rest/views.py b/app/rest/views.py
new file mode 100644
index 0000000..152e290
--- /dev/null
+++ b/app/rest/views.py
@@ -0,0 +1,70 @@
+from flask import render_template, request, current_app
+from werkzeug.utils import secure_filename
+from . import rest
+import json, uuid, os
+
+
+#This URL (website.com/rest/debug) is used to test the website and provide debug output. It's really only a developer tool.
+@rest.route('/debug')
+def rest_debug():
+ return render_template("debug.html")
+
+
+#This URL (website.com/rest/convert_object) is used by the client when it receives an object file it doesn't understand.
+#Rather than doing the conversion client-side, we've made the conscience decision to do this server-side. It's easier in Python; plus the
+#server will be more powerful than the client for such a task.
+#The server will then send the file back to the client after file conversion is complete. Because this may take a few seconds, async
+#javascript requests is an absolute must to prevent client lockup.
+@rest.route('/convert_object', methods=["GET", "POST"])
+def convert_object():
+ return ""
+
+
+
+#This URL (website.com/rest/save_state) is used by the client to save their current state, represented as a JSON file.
+#The client is responsible for POSTing the data, the server will never prompt the client to save.
+
+#We're going to assume a 5 second timer for autosaving. The server is responsible for generating a UUID on first load, when
+#the user first selects their state (i.e. new, resume). Then, that UUID is forever used as the official representation of that state.
+#JSON schema TBD
+
+@rest.route('/save_state', methods=["GET", "POST"])
+def save_state():
+ if request.method == "POST":
+ try:
+ state = request.get_json(request.data)
+ #Check to see if the POSTed save state has a UUID. If not, then this is a new project and we need to generate a UUID for it.
+ if "uuid" not in state["project"] or state["project"]["uuid"] == "":
+ state["project"]["uuid"] = str(uuid.uuid4())
+ #Save the save state to the appropriate folder. The file is named 'UUID.json'
+ with open(current_app.config["JSON_STORE_DATA"] + secure_filename(str(state["project"]["uuid"])) + ".json", 'w+') as save_state_file:
+ save_state_file.write(json.dumps(state))
+ print "I'm saving: " + state["project"]["uuid"]
+ return state["project"]["uuid"] #Return the UUID if successful. This is used by the client to receive the UUID on the first initial save.
+ except:
+ return "FAIL" #Something went wrong. Let's be purposely dense about what went wrong.
+ return "FAIL" #How'd we get here? Someone trying to load the page?
+
+
+#This URL (website.com/rest/resume_state) is used to fetch the JSON file of the state requested by the user.
+#The user requests the UUID of the specific JSON file, which is fetched and dumped back to client.
+#JSON schema TBD
+@rest.route('/resume_state/', methods=["GET", "POST"])
+@rest.route('/resume_state/', methods=["GET", "POST"])
+def resume_state(uuid=None):
+ if uuid == None: #We're looking to see which save states we have
+ states = ""
+ try:
+ for save_state in os.listdir(current_app.config["JSON_STORE_DATA"]):
+ states += save_state[:-5] + "," #Only return the UUID, remove .json ending
+ return str(states[:-1])
+ except:
+ return "FAIL" #Something went wrong. Let's be purposely dense about what went wrong.
+ else: #We're requesting a specific UUID to resume from.
+ try:
+
+ with open(current_app.config["JSON_STORE_DATA"] + secure_filename(str(uuid))+ ".json", 'r') as save_state_file:
+ return save_state_file.read()
+ except:
+ return "FAIL"
+ return "FAIL" #How'd we get here?
diff --git a/app/static/css/bootstrap.min.css b/app/static/css/bootstrap.min.css
new file mode 100755
index 0000000..5e068b9
--- /dev/null
+++ b/app/static/css/bootstrap.min.css
@@ -0,0 +1 @@
+.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}
diff --git a/app/static/css/dark.css b/app/static/css/dark.css
new file mode 100644
index 0000000..117390e
--- /dev/null
+++ b/app/static/css/dark.css
@@ -0,0 +1,249 @@
+button {
+ color: #aaa;
+ background-color: #222;
+ border: 0px;
+ padding: 5px 8px;
+ text-transform: uppercase;
+ cursor: pointer;
+ outline: none;
+}
+
+ button:hover {
+ color: #ccc;
+ background-color: #444;
+ }
+
+ button.selected {
+ color: #fff;
+ background-color: #08f;
+ }
+
+input, textarea {
+ background-color: #222;
+ border: 1px solid transparent;
+ color: #888;
+}
+
+input.Number {
+ color: #08f!important;
+ font-size: 12px;
+ border: 0px;
+ padding: 2px;
+ cursor: col-resize;
+}
+
+select {
+ color: #aaa;
+ background-color: #222;
+ border: 0px;
+ text-transform: uppercase;
+ cursor: pointer;
+ outline: none;
+}
+
+ select:hover {
+ color: #ccc;
+ background-color: #444;
+ }
+
+/* UI */
+
+#viewport {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+}
+
+ #viewport #info {
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
+ pointer-events: none;
+ }
+
+#script {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+ opacity: 0.9;
+}
+
+#player {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+}
+
+#menubar {
+ position: absolute;
+ width: 100%;
+ height: 32px;
+ background: #111;
+ padding: 0;
+ margin: 0;
+ right: 0;
+ top: 0;
+}
+
+ #menubar .menu {
+ float: left;
+ cursor: pointer;
+ padding-right: 8px;
+ }
+
+ #menubar .menu.right {
+ float: right;
+ cursor: auto;
+ padding-right: 0;
+ text-align: right;
+ }
+
+ #menubar .menu .title {
+ display: inline-block;
+ color: #888;
+ margin: 0;
+ padding: 8px;
+ }
+
+ #menubar .menu .options {
+ position: fixed;
+ display: none;
+ padding: 5px 0;
+ background: #111;
+ width: 150px;
+ max-height: calc(100% - 80px);
+ overflow: auto;
+ }
+
+ #menubar .menu:hover .options {
+ display: block;
+ }
+
+ #menubar .menu .options hr {
+ border-color: #222;
+ }
+
+ #menubar .menu .options .option {
+ color: #888;
+ background-color: transparent;
+ padding: 5px 10px;
+ margin: 0 !important;
+ }
+
+ #menubar .menu .options .option:hover {
+ color: #fff;
+ background-color: #08f;
+ }
+
+ #menubar .menu .options .option:active {
+ background: transparent;
+ }
+
+ #menubar .menu .options .inactive {
+ color: #444;
+ background-color: transparent;
+ padding: 5px 10px;
+ margin: 0 !important;
+ }
+
+#sidebar {
+ position: absolute;
+ right: 0;
+ top: 32px;
+ bottom: 0;
+ width: 300px;
+ background-color: #111;
+ overflow: auto;
+}
+
+ #sidebar * {
+ vertical-align: middle;
+ }
+
+ #sidebar .Panel {
+ color: #888;
+ padding: 10px;
+ border-top: 1px solid #222;
+ }
+
+ #sidebar .Panel.collapsed {
+ margin-bottom: 0;
+ }
+
+ #sidebar .Panel.Material canvas {
+ border: solid 1px #5A5A5A;
+ }
+
+ #sidebar .Row {
+ min-height: 20px;
+ margin-bottom: 10px;
+ }
+
+#tabs {
+ background-color: #1b1b1b;
+ border-top: 1px solid #222;
+}
+
+ #tabs span {
+ color: #555;
+ border-right: 1px solid #222;
+ padding: 10px;
+ }
+
+ #tabs span.selected {
+ color: #888;
+ background-color: #111;
+ }
+
+#toolbar {
+ position: absolute;
+ left: 0;
+ right: 300px;
+ bottom: 0;
+ height: 32px;
+ background-color: #111;
+ color: #333;
+}
+
+ #toolbar * {
+ vertical-align: middle;
+ }
+
+ #toolbar .Panel {
+ padding: 4px;
+ color: #888;
+ }
+
+ #toolbar button {
+ margin-right: 6px;
+ }
+
+.Outliner {
+ color: #888;
+ background: #222;
+ padding: 0;
+ width: 100%;
+ height: 140px;
+ font-size: 12px;
+ cursor: default;
+ overflow: auto;
+ outline: none;
+}
+
+ .Outliner .option {
+ padding: 4px;
+ white-space: nowrap;
+ }
+
+ .Outliner .option:hover {
+ background-color: rgba(21,60,94,0.5);
+ }
+
+
+ .Outliner .option.active {
+ background-color: rgba(21,60,94,1);
+ }
diff --git a/app/static/css/light.css b/app/static/css/light.css
new file mode 100644
index 0000000..ac7b73b
--- /dev/null
+++ b/app/static/css/light.css
@@ -0,0 +1,242 @@
+button {
+ color: #555;
+ background-color: #ddd;
+ border: 0px;
+ padding: 5px 8px;
+ text-transform: uppercase;
+ cursor: pointer;
+ outline: none;
+}
+
+ button:hover {
+ background-color: #fff;
+ }
+
+ button.selected {
+ background-color: #fff;
+ }
+
+input, textarea {
+ border: 1px solid transparent;
+ color: #444;
+}
+
+input.Number {
+ color: #08f!important;
+ font-size: 12px;
+ border: 0px;
+ padding: 2px;
+ cursor: col-resize;
+}
+
+select {
+ color: #666;
+ background-color: #ddd;
+ border: 0px;
+ text-transform: uppercase;
+ cursor: pointer;
+ outline: none;
+}
+
+ select:hover {
+ background-color: #fff;
+ }
+
+/* UI */
+
+#viewport {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+}
+
+ #viewport #info {
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.25);
+ pointer-events: none;
+ }
+
+#script {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+ opacity: 0.9;
+}
+
+#player {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ right: 300px;
+ bottom: 32px;
+}
+
+#menubar {
+ position: absolute;
+ width: 100%;
+ height: 32px;
+ background: #eee;
+ padding: 0;
+ margin: 0;
+ right: 0;
+ top: 0;
+}
+
+ #menubar .menu {
+ float: left;
+ cursor: pointer;
+ padding-right: 8px;
+ }
+
+ #menubar .menu.right {
+ float: right;
+ cursor: auto;
+ padding-right: 0;
+ text-align: right;
+ }
+
+ #menubar .menu .title {
+ display: inline-block;
+ color: #888;
+ margin: 0;
+ padding: 8px;
+ }
+
+ #menubar .menu .options {
+ position: fixed;
+ display: none;
+ padding: 5px 0;
+ background: #eee;
+ width: 150px;
+ max-height: calc(100% - 80px);
+ overflow: auto;
+ }
+
+ #menubar .menu:hover .options {
+ display: block;
+ }
+
+ #menubar .menu .options hr {
+ border-color: #ddd;
+ }
+
+ #menubar .menu .options .option {
+ color: #666;
+ background-color: transparent;
+ padding: 5px 10px;
+ margin: 0 !important;
+ }
+
+ #menubar .menu .options .option:hover {
+ color: #fff;
+ background-color: #08f;
+ }
+
+ #menubar .menu .options .option:active {
+ color: #666;
+ background: transparent;
+ }
+
+ #menubar .menu .options .inactive {
+ color: #bbb;
+ background-color: transparent;
+ padding: 5px 10px;
+ margin: 0 !important;
+ }
+
+#sidebar {
+ position: absolute;
+ right: 0;
+ top: 32px;
+ bottom: 0;
+ width: 300px;
+ background: #eee;
+ overflow: auto;
+}
+
+ #sidebar * {
+ vertical-align: middle;
+ }
+
+ #sidebar .Panel {
+ color: #888;
+ padding: 10px;
+ border-top: 1px solid #ccc;
+ }
+
+ #sidebar .Panel.collapsed {
+ margin-bottom: 0;
+ }
+
+ #sidebar .Row {
+ min-height: 20px;
+ margin-bottom: 10px;
+ }
+
+#tabs {
+ background-color: #ddd;
+ border-top: 1px solid #ccc;
+}
+
+ #tabs span {
+ color: #aaa;
+ border-right: 1px solid #ccc;
+ padding: 10px;
+ }
+
+ #tabs span.selected {
+ color: #888;
+ background-color: #eee;
+ }
+
+#toolbar {
+ position: absolute;
+ left: 0;
+ right: 300px;
+ bottom: 0;
+ height: 32px;
+ background: #eee;
+ color: #333;
+}
+
+ #toolbar * {
+ vertical-align: middle;
+ }
+
+ #toolbar .Panel {
+ padding: 4px;
+ color: #888;
+ }
+
+ #toolbar button {
+ margin-right: 6px;
+ }
+
+.Outliner {
+ color: #444;
+ background-color: #fff;
+ padding: 0;
+ width: 100%;
+ height: 140px;
+ font-size: 12px;
+ cursor: default;
+ overflow: auto;
+ outline: none !important;
+}
+
+ .Outliner .option {
+ padding: 4px;
+ color: #666;
+ white-space: nowrap;
+ }
+
+ .Outliner .option:hover {
+ background-color: rgba(0,0,0,0.02);
+ }
+
+ .Outliner .option.active {
+ background-color: rgba(0,0,0,0.04);
+ }
diff --git a/app/static/css/main.css b/app/static/css/main.css
new file mode 100644
index 0000000..85cc3b8
--- /dev/null
+++ b/app/static/css/main.css
@@ -0,0 +1,163 @@
+body {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ margin: 0;
+ overflow: hidden;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid #ccc;
+}
+
+button {
+ position: relative;
+}
+
+textarea {
+ tab-size: 4;
+ white-space: pre;
+ word-wrap: normal;
+}
+
+ textarea.success {
+ border-color: #8b8 !important;
+ }
+
+ textarea.fail {
+ border-color: #f00 !important;
+ background-color: rgba(255,0,0,0.05);
+ }
+
+textarea, input { outline: none; } /* osx */
+
+.Panel {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+
+ /* No support for these yet */
+ -o-user-select: none;
+ user-select: none;
+}
+
+/* CodeMirror */
+
+.CodeMirror {
+
+ position: absolute !important;
+ top: 37px;
+ width: 100% !important;
+ height: calc(100% - 37px) !important;
+
+}
+
+ .CodeMirror .errorLine {
+
+ background: rgba(255,0,0,0.25);
+
+ }
+
+ .CodeMirror .esprima-error {
+
+ color: #f00;
+ text-align: right;
+ padding: 0 20px;
+
+ }
+
+/* outliner */
+
+#outliner .option {
+
+ border: 1px solid transparent;
+}
+
+#outliner .option.drag {
+
+ border: 1px dashed #999;
+
+}
+
+#outliner .option.dragTop {
+
+ border-top: 1px dashed #999;
+
+}
+
+#outliner .option.dragBottom {
+
+ border-bottom: 1px dashed #999;
+
+}
+
+#outliner .type {
+ position:relative;
+ top:-2px;
+ padding: 0 2px;
+ color: #ddd;
+}
+
+#outliner .type:after {
+ content: '■';
+}
+
+#outliner .Scene {
+ color: #ccccff;
+}
+
+#outliner .Object3D {
+ color: #aaaaee;
+}
+
+#outliner .Mesh {
+ color: #8888ee;
+}
+
+#outliner .Line {
+ color: #88ee88;
+}
+
+#outliner .LineSegments {
+ color: #88ee88;
+}
+
+#outliner .Points {
+ color: #ee8888;
+}
+
+/* */
+
+#outliner .PointLight {
+ color: #dddd00;
+}
+
+/* */
+
+#outliner .Geometry {
+ color: #88ff88;
+}
+
+#outliner .BoxGeometry {
+ color: #bbeebb;
+}
+
+#outliner .TorusGeometry {
+ color: #aaeeaa;
+}
+
+/* */
+
+#outliner .Material {
+ color: #ff8888;
+}
+
+#outliner .MeshPhongMaterial {
+ color: #ffaa88;
+}
+
+/* */
+
+#outliner .Script:after {
+ content: '{...}' /* ❮/❯ */
+}
diff --git a/app/static/js/AnimationClipCreator.js b/app/static/js/AnimationClipCreator.js
new file mode 100644
index 0000000..7aa39d8
--- /dev/null
+++ b/app/static/js/AnimationClipCreator.js
@@ -0,0 +1,115 @@
+/**
+ *
+ * Creator of typical test AnimationClips / KeyframeTracks
+ *
+ * @author Ben Houston / http://clara.io/
+ * @author David Sarno / http://lighthaus.us/
+ */
+
+THREE.AnimationClipCreator = function() {
+};
+
+THREE.AnimationClipCreator.CreateRotationAnimation = function( period, axis ) {
+
+ var times = [ 0, period ], values = [ 0, 360 ];
+
+ axis = axis || 'x';
+ var trackName = '.rotation[' + axis + ']';
+
+ var track = new THREE.NumberKeyframeTrack( trackName, times, values );
+
+ return new THREE.AnimationClip( null, period, [ track ] );
+
+};
+
+THREE.AnimationClipCreator.CreateScaleAxisAnimation = function( period, axis ) {
+
+ var times = [ 0, period ], values = [ 0, 1 ];
+
+ axis = axis || 'x';
+ var trackName = '.scale[' + axis + ']';
+
+ var track = new THREE.NumberKeyframeTrack( trackName, times, values );
+
+ return new THREE.AnimationClip( null, period, [ track ] );
+
+};
+
+THREE.AnimationClipCreator.CreateShakeAnimation = function( duration, shakeScale ) {
+
+ var times = [], values = [], tmp = new THREE.Vector3();
+
+ for( var i = 0; i < duration * 10; i ++ ) {
+
+ times.push( i / 10 );
+
+ tmp.set( Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0 ).
+ multiply( shakeScale ).
+ toArray( values, values.length );
+
+ }
+
+ var trackName = '.position';
+
+ var track = new THREE.VectorKeyframeTrack( trackName, times, values );
+
+ return new THREE.AnimationClip( null, duration, [ track ] );
+
+};
+
+
+THREE.AnimationClipCreator.CreatePulsationAnimation = function( duration, pulseScale ) {
+
+ var times = [], values = [], tmp = new THREE.Vector3();
+
+ for( var i = 0; i < duration * 10; i ++ ) {
+
+ times.push( i / 10 );
+
+ var scaleFactor = Math.random() * pulseScale;
+ tmp.set( scaleFactor, scaleFactor, scaleFactor ).
+ toArray( values, values.length );
+
+ }
+
+ var trackName = '.scale';
+
+ var track = new THREE.VectorKeyframeTrack( trackName, keys );
+
+ return new THREE.AnimationClip( null, duration, [ track ] );
+
+};
+
+
+THREE.AnimationClipCreator.CreateVisibilityAnimation = function( duration ) {
+
+ var times = [ 0, duration / 2, duration ], values = [ true, false, true ];
+
+ var trackName = '.visible';
+
+ var track = new THREE.BooleanKeyframeTrack( trackName, times, values );
+
+ return new THREE.AnimationClip( null, duration, [ track ] );
+
+};
+
+
+THREE.AnimationClipCreator.CreateMaterialColorAnimation = function( duration, colors, loop ) {
+
+ var times = [], values = [],
+ timeStep = duration / colors.length;
+
+ for( var i = 0; i <= colors.length; i ++ ) {
+
+ timees.push( i * timeStep );
+ values.push( colors[ i % colors.length ] );
+
+ }
+
+ var trackName = '.material[0].color';
+
+ var track = new THREE.ColorKeyframeTrack( trackName, times, values );
+
+ return new THREE.AnimationClip( null, duration, [ track ] );
+
+};
diff --git a/app/static/js/BufferGeometryUtils.js b/app/static/js/BufferGeometryUtils.js
new file mode 100644
index 0000000..18d4732
--- /dev/null
+++ b/app/static/js/BufferGeometryUtils.js
@@ -0,0 +1,187 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.BufferGeometryUtils = {
+
+ computeTangents: function ( geometry ) {
+
+ var index = geometry.index;
+ var attributes = geometry.attributes;
+
+ // based on http://www.terathon.com/code/tangent.html
+ // (per vertex tangents)
+
+ if ( index === null ||
+ attributes.position === undefined ||
+ attributes.normal === undefined ||
+ attributes.uv === undefined ) {
+
+ console.warn( 'THREE.BufferGeometry: Missing required attributes (index, position, normal or uv) in BufferGeometry.computeTangents()' );
+ return;
+
+ }
+
+ var indices = index.array;
+ var positions = attributes.position.array;
+ var normals = attributes.normal.array;
+ var uvs = attributes.uv.array;
+
+ var nVertices = positions.length / 3;
+
+ if ( attributes.tangent === undefined ) {
+
+ geometry.addAttribute( 'tangent', new THREE.BufferAttribute( new Float32Array( 4 * nVertices ), 4 ) );
+
+ }
+
+ var tangents = attributes.tangent.array;
+
+ var tan1 = [], tan2 = [];
+
+ for ( var k = 0; k < nVertices; k ++ ) {
+
+ tan1[ k ] = new THREE.Vector3();
+ tan2[ k ] = new THREE.Vector3();
+
+ }
+
+ var vA = new THREE.Vector3(),
+ vB = new THREE.Vector3(),
+ vC = new THREE.Vector3(),
+
+ uvA = new THREE.Vector2(),
+ uvB = new THREE.Vector2(),
+ uvC = new THREE.Vector2(),
+
+ sdir = new THREE.Vector3(),
+ tdir = new THREE.Vector3();
+
+ function handleTriangle( a, b, c ) {
+
+ vA.fromArray( positions, a * 3 );
+ vB.fromArray( positions, b * 3 );
+ vC.fromArray( positions, c * 3 );
+
+ uvA.fromArray( uvs, a * 2 );
+ uvB.fromArray( uvs, b * 2 );
+ uvC.fromArray( uvs, c * 2 );
+
+ var x1 = vB.x - vA.x;
+ var x2 = vC.x - vA.x;
+
+ var y1 = vB.y - vA.y;
+ var y2 = vC.y - vA.y;
+
+ var z1 = vB.z - vA.z;
+ var z2 = vC.z - vA.z;
+
+ var s1 = uvB.x - uvA.x;
+ var s2 = uvC.x - uvA.x;
+
+ var t1 = uvB.y - uvA.y;
+ var t2 = uvC.y - uvA.y;
+
+ var r = 1.0 / ( s1 * t2 - s2 * t1 );
+
+ sdir.set(
+ ( t2 * x1 - t1 * x2 ) * r,
+ ( t2 * y1 - t1 * y2 ) * r,
+ ( t2 * z1 - t1 * z2 ) * r
+ );
+
+ tdir.set(
+ ( s1 * x2 - s2 * x1 ) * r,
+ ( s1 * y2 - s2 * y1 ) * r,
+ ( s1 * z2 - s2 * z1 ) * r
+ );
+
+ tan1[ a ].add( sdir );
+ tan1[ b ].add( sdir );
+ tan1[ c ].add( sdir );
+
+ tan2[ a ].add( tdir );
+ tan2[ b ].add( tdir );
+ tan2[ c ].add( tdir );
+
+ }
+
+ var groups = geometry.groups;
+
+ if ( groups.length === 0 ) {
+
+ groups = [ {
+ start: 0,
+ count: indices.length
+ } ];
+
+ }
+
+ for ( var j = 0, jl = groups.length; j < jl; ++ j ) {
+
+ var group = groups[ j ];
+
+ var start = group.start;
+ var count = group.count;
+
+ for ( var i = start, il = start + count; i < il; i += 3 ) {
+
+ handleTriangle(
+ indices[ i + 0 ],
+ indices[ i + 1 ],
+ indices[ i + 2 ]
+ );
+
+ }
+
+ }
+
+ var tmp = new THREE.Vector3(), tmp2 = new THREE.Vector3();
+ var n = new THREE.Vector3(), n2 = new THREE.Vector3();
+ var w, t, test;
+
+ function handleVertex( v ) {
+
+ n.fromArray( normals, v * 3 );
+ n2.copy( n );
+
+ t = tan1[ v ];
+
+ // Gram-Schmidt orthogonalize
+
+ tmp.copy( t );
+ tmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();
+
+ // Calculate handedness
+
+ tmp2.crossVectors( n2, t );
+ test = tmp2.dot( tan2[ v ] );
+ w = ( test < 0.0 ) ? - 1.0 : 1.0;
+
+ tangents[ v * 4 ] = tmp.x;
+ tangents[ v * 4 + 1 ] = tmp.y;
+ tangents[ v * 4 + 2 ] = tmp.z;
+ tangents[ v * 4 + 3 ] = w;
+
+ }
+
+ for ( var j = 0, jl = groups.length; j < jl; ++ j ) {
+
+ var group = groups[ j ];
+
+ var start = group.start;
+ var count = group.count;
+
+ for ( var i = start, il = start + count; i < il; i += 3 ) {
+
+ handleVertex( indices[ i + 0 ] );
+ handleVertex( indices[ i + 1 ] );
+ handleVertex( indices[ i + 2 ] );
+
+ }
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/Car.js b/app/static/js/Car.js
new file mode 100644
index 0000000..c5f01ff
--- /dev/null
+++ b/app/static/js/Car.js
@@ -0,0 +1,407 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.Car = function () {
+
+ var scope = this;
+
+ // car geometry manual parameters
+
+ this.modelScale = 1;
+
+ this.backWheelOffset = 2;
+
+ this.autoWheelGeometry = true;
+
+ // car geometry parameters automatically set from wheel mesh
+ // - assumes wheel mesh is front left wheel in proper global
+ // position with respect to body mesh
+ // - other wheels are mirrored against car root
+ // - if necessary back wheels can be offset manually
+
+ this.wheelOffset = new THREE.Vector3();
+
+ this.wheelDiameter = 1;
+
+ // car "feel" parameters
+
+ this.MAX_SPEED = 2200;
+ this.MAX_REVERSE_SPEED = - 1500;
+
+ this.MAX_WHEEL_ROTATION = 0.6;
+
+ this.FRONT_ACCELERATION = 1250;
+ this.BACK_ACCELERATION = 1500;
+
+ this.WHEEL_ANGULAR_ACCELERATION = 1.5;
+
+ this.FRONT_DECCELERATION = 750;
+ this.WHEEL_ANGULAR_DECCELERATION = 1.0;
+
+ this.STEERING_RADIUS_RATIO = 0.0023;
+
+ this.MAX_TILT_SIDES = 0.05;
+ this.MAX_TILT_FRONTBACK = 0.015;
+
+ // internal control variables
+
+ this.speed = 0;
+ this.acceleration = 0;
+
+ this.wheelOrientation = 0;
+ this.carOrientation = 0;
+
+ // car rigging
+
+ this.root = new THREE.Object3D();
+
+ this.frontLeftWheelRoot = new THREE.Object3D();
+ this.frontRightWheelRoot = new THREE.Object3D();
+
+ this.bodyMesh = null;
+
+ this.frontLeftWheelMesh = null;
+ this.frontRightWheelMesh = null;
+
+ this.backLeftWheelMesh = null;
+ this.backRightWheelMesh = null;
+
+ this.bodyGeometry = null;
+ this.wheelGeometry = null;
+
+ this.bodyMaterials = null;
+ this.wheelMaterials = null;
+
+ // internal helper variables
+
+ this.loaded = false;
+
+ this.meshes = [];
+
+ // API
+
+ this.enableShadows = function ( enable ) {
+
+ for ( var i = 0; i < this.meshes.length; i ++ ) {
+
+ this.meshes[ i ].castShadow = enable;
+ this.meshes[ i ].receiveShadow = enable;
+
+ }
+
+ };
+
+ this.setVisible = function ( enable ) {
+
+ for ( var i = 0; i < this.meshes.length; i ++ ) {
+
+ this.meshes[ i ].visible = enable;
+ this.meshes[ i ].visible = enable;
+
+ }
+
+ };
+
+ this.loadPartsJSON = function ( bodyURL, wheelURL ) {
+
+ var loader = new THREE.JSONLoader();
+
+ loader.load( bodyURL, function( geometry, materials ) {
+
+ createBody( geometry, materials )
+
+ } );
+ loader.load( wheelURL, function( geometry, materials ) {
+
+ createWheels( geometry, materials )
+
+ } );
+
+ };
+
+ this.loadPartsBinary = function ( bodyURL, wheelURL ) {
+
+ var loader = new THREE.BinaryLoader();
+
+ loader.load( bodyURL, function( geometry, materials ) {
+
+ createBody( geometry, materials )
+
+ } );
+ loader.load( wheelURL, function( geometry, materials ) {
+
+ createWheels( geometry, materials )
+
+ } );
+
+ };
+
+ this.updateCarModel = function ( delta, controls ) {
+
+ // speed and wheels based on controls
+
+ if ( controls.moveForward ) {
+
+ this.speed = THREE.Math.clamp( this.speed + delta * this.FRONT_ACCELERATION, this.MAX_REVERSE_SPEED, this.MAX_SPEED );
+ this.acceleration = THREE.Math.clamp( this.acceleration + delta, - 1, 1 );
+
+ }
+
+ if ( controls.moveBackward ) {
+
+
+ this.speed = THREE.Math.clamp( this.speed - delta * this.BACK_ACCELERATION, this.MAX_REVERSE_SPEED, this.MAX_SPEED );
+ this.acceleration = THREE.Math.clamp( this.acceleration - delta, - 1, 1 );
+
+ }
+
+ if ( controls.moveLeft ) {
+
+ this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation + delta * this.WHEEL_ANGULAR_ACCELERATION, - this.MAX_WHEEL_ROTATION, this.MAX_WHEEL_ROTATION );
+
+ }
+
+ if ( controls.moveRight ) {
+
+ this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation - delta * this.WHEEL_ANGULAR_ACCELERATION, - this.MAX_WHEEL_ROTATION, this.MAX_WHEEL_ROTATION );
+
+ }
+
+ // speed decay
+
+ if ( ! ( controls.moveForward || controls.moveBackward ) ) {
+
+ if ( this.speed > 0 ) {
+
+ var k = exponentialEaseOut( this.speed / this.MAX_SPEED );
+
+ this.speed = THREE.Math.clamp( this.speed - k * delta * this.FRONT_DECCELERATION, 0, this.MAX_SPEED );
+ this.acceleration = THREE.Math.clamp( this.acceleration - k * delta, 0, 1 );
+
+ } else {
+
+ var k = exponentialEaseOut( this.speed / this.MAX_REVERSE_SPEED );
+
+ this.speed = THREE.Math.clamp( this.speed + k * delta * this.BACK_ACCELERATION, this.MAX_REVERSE_SPEED, 0 );
+ this.acceleration = THREE.Math.clamp( this.acceleration + k * delta, - 1, 0 );
+
+ }
+
+
+ }
+
+ // steering decay
+
+ if ( ! ( controls.moveLeft || controls.moveRight ) ) {
+
+ if ( this.wheelOrientation > 0 ) {
+
+ this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation - delta * this.WHEEL_ANGULAR_DECCELERATION, 0, this.MAX_WHEEL_ROTATION );
+
+ } else {
+
+ this.wheelOrientation = THREE.Math.clamp( this.wheelOrientation + delta * this.WHEEL_ANGULAR_DECCELERATION, - this.MAX_WHEEL_ROTATION, 0 );
+
+ }
+
+ }
+
+ // car update
+
+ var forwardDelta = this.speed * delta;
+
+ this.carOrientation += ( forwardDelta * this.STEERING_RADIUS_RATIO ) * this.wheelOrientation;
+
+ // displacement
+
+ this.root.position.x += Math.sin( this.carOrientation ) * forwardDelta;
+ this.root.position.z += Math.cos( this.carOrientation ) * forwardDelta;
+
+ // steering
+
+ this.root.rotation.y = this.carOrientation;
+
+ // tilt
+
+ if ( this.loaded ) {
+
+ this.bodyMesh.rotation.z = this.MAX_TILT_SIDES * this.wheelOrientation * ( this.speed / this.MAX_SPEED );
+ this.bodyMesh.rotation.x = - this.MAX_TILT_FRONTBACK * this.acceleration;
+
+ }
+
+ // wheels rolling
+
+ var angularSpeedRatio = 1 / ( this.modelScale * ( this.wheelDiameter / 2 ) );
+
+ var wheelDelta = forwardDelta * angularSpeedRatio;
+
+ if ( this.loaded ) {
+
+ this.frontLeftWheelMesh.rotation.x += wheelDelta;
+ this.frontRightWheelMesh.rotation.x += wheelDelta;
+ this.backLeftWheelMesh.rotation.x += wheelDelta;
+ this.backRightWheelMesh.rotation.x += wheelDelta;
+
+ }
+
+ // front wheels steering
+
+ this.frontLeftWheelRoot.rotation.y = this.wheelOrientation;
+ this.frontRightWheelRoot.rotation.y = this.wheelOrientation;
+
+ };
+
+ // internal helper methods
+
+ function createBody ( geometry, materials ) {
+
+ scope.bodyGeometry = geometry;
+ scope.bodyMaterials = materials;
+
+ createCar();
+
+ }
+
+ function createWheels ( geometry, materials ) {
+
+ scope.wheelGeometry = geometry;
+ scope.wheelMaterials = materials;
+
+ createCar();
+
+ }
+
+ function createCar () {
+
+ if ( scope.bodyGeometry && scope.wheelGeometry ) {
+
+ // compute wheel geometry parameters
+
+ if ( scope.autoWheelGeometry ) {
+
+ scope.wheelGeometry.computeBoundingBox();
+
+ var bb = scope.wheelGeometry.boundingBox;
+
+ scope.wheelOffset.addVectors( bb.min, bb.max );
+ scope.wheelOffset.multiplyScalar( 0.5 );
+
+ scope.wheelDiameter = bb.max.y - bb.min.y;
+
+ scope.wheelGeometry.center();
+
+ }
+
+ // rig the car
+
+ var s = scope.modelScale,
+ delta = new THREE.Vector3();
+
+ var bodyFaceMaterial = scope.bodyMaterials;
+ var wheelFaceMaterial = scope.wheelMaterials;
+
+ // body
+
+ scope.bodyMesh = new THREE.Mesh( scope.bodyGeometry, bodyFaceMaterial );
+ scope.bodyMesh.scale.set( s, s, s );
+
+ scope.root.add( scope.bodyMesh );
+
+ // front left wheel
+
+ delta.multiplyVectors( scope.wheelOffset, new THREE.Vector3( s, s, s ) );
+
+ scope.frontLeftWheelRoot.position.add( delta );
+
+ scope.frontLeftWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );
+ scope.frontLeftWheelMesh.scale.set( s, s, s );
+
+ scope.frontLeftWheelRoot.add( scope.frontLeftWheelMesh );
+ scope.root.add( scope.frontLeftWheelRoot );
+
+ // front right wheel
+
+ delta.multiplyVectors( scope.wheelOffset, new THREE.Vector3( - s, s, s ) );
+
+ scope.frontRightWheelRoot.position.add( delta );
+
+ scope.frontRightWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );
+
+ scope.frontRightWheelMesh.scale.set( s, s, s );
+ scope.frontRightWheelMesh.rotation.z = Math.PI;
+
+ scope.frontRightWheelRoot.add( scope.frontRightWheelMesh );
+ scope.root.add( scope.frontRightWheelRoot );
+
+ // back left wheel
+
+ delta.multiplyVectors( scope.wheelOffset, new THREE.Vector3( s, s, - s ) );
+ delta.z -= scope.backWheelOffset;
+
+ scope.backLeftWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );
+
+ scope.backLeftWheelMesh.position.add( delta );
+ scope.backLeftWheelMesh.scale.set( s, s, s );
+
+ scope.root.add( scope.backLeftWheelMesh );
+
+ // back right wheel
+
+ delta.multiplyVectors( scope.wheelOffset, new THREE.Vector3( - s, s, - s ) );
+ delta.z -= scope.backWheelOffset;
+
+ scope.backRightWheelMesh = new THREE.Mesh( scope.wheelGeometry, wheelFaceMaterial );
+
+ scope.backRightWheelMesh.position.add( delta );
+ scope.backRightWheelMesh.scale.set( s, s, s );
+ scope.backRightWheelMesh.rotation.z = Math.PI;
+
+ scope.root.add( scope.backRightWheelMesh );
+
+ // cache meshes
+
+ scope.meshes = [ scope.bodyMesh, scope.frontLeftWheelMesh, scope.frontRightWheelMesh, scope.backLeftWheelMesh, scope.backRightWheelMesh ];
+
+ // callback
+
+ scope.loaded = true;
+
+ if ( scope.callback ) {
+
+ scope.callback( scope );
+
+ }
+
+ }
+
+ }
+
+ function quadraticEaseOut( k ) {
+
+ return - k * ( k - 2 );
+
+ }
+ function cubicEaseOut( k ) {
+
+ return -- k * k * k + 1;
+
+ }
+ function circularEaseOut( k ) {
+
+ return Math.sqrt( 1 - -- k * k );
+
+ }
+ function sinusoidalEaseOut( k ) {
+
+ return Math.sin( k * Math.PI / 2 );
+
+ }
+ function exponentialEaseOut( k ) {
+
+ return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1;
+
+ }
+
+};
diff --git a/app/static/js/Cloth.js b/app/static/js/Cloth.js
new file mode 100644
index 0000000..a9f435f
--- /dev/null
+++ b/app/static/js/Cloth.js
@@ -0,0 +1,330 @@
+/*
+ * Cloth Simulation using a relaxed constraints solver
+ */
+
+// Suggested Readings
+
+// Advanced Character Physics by Thomas Jakobsen Character
+// http://freespace.virgin.net/hugo.elias/models/m_cloth.htm
+// http://en.wikipedia.org/wiki/Cloth_modeling
+// http://cg.alexandra.dk/tag/spring-mass-system/
+// Real-time Cloth Animation http://www.darwin3d.com/gamedev/articles/col0599.pdf
+
+var DAMPING = 0.03;
+var DRAG = 1 - DAMPING;
+var MASS = 0.1;
+var restDistance = 25;
+
+var xSegs = 10;
+var ySegs = 10;
+
+var clothFunction = plane( restDistance * xSegs, restDistance * ySegs );
+
+var cloth = new Cloth( xSegs, ySegs );
+
+var GRAVITY = 981 * 1.4;
+var gravity = new THREE.Vector3( 0, - GRAVITY, 0 ).multiplyScalar( MASS );
+
+
+var TIMESTEP = 18 / 1000;
+var TIMESTEP_SQ = TIMESTEP * TIMESTEP;
+
+var pins = [];
+
+
+var wind = true;
+var windStrength = 2;
+var windForce = new THREE.Vector3( 0, 0, 0 );
+
+var ballPosition = new THREE.Vector3( 0, - 45, 0 );
+var ballSize = 60; //40
+
+var tmpForce = new THREE.Vector3();
+
+var lastTime;
+
+
+function plane( width, height ) {
+
+ return function( u, v ) {
+
+ var x = ( u - 0.5 ) * width;
+ var y = ( v + 0.5 ) * height;
+ var z = 0;
+
+ return new THREE.Vector3( x, y, z );
+
+ };
+
+}
+
+function Particle( x, y, z, mass ) {
+
+ this.position = clothFunction( x, y ); // position
+ this.previous = clothFunction( x, y ); // previous
+ this.original = clothFunction( x, y );
+ this.a = new THREE.Vector3( 0, 0, 0 ); // acceleration
+ this.mass = mass;
+ this.invMass = 1 / mass;
+ this.tmp = new THREE.Vector3();
+ this.tmp2 = new THREE.Vector3();
+
+}
+
+// Force -> Acceleration
+
+Particle.prototype.addForce = function( force ) {
+
+ this.a.add(
+ this.tmp2.copy( force ).multiplyScalar( this.invMass )
+ );
+
+};
+
+
+// Performs Verlet integration
+
+Particle.prototype.integrate = function( timesq ) {
+
+ var newPos = this.tmp.subVectors( this.position, this.previous );
+ newPos.multiplyScalar( DRAG ).add( this.position );
+ newPos.add( this.a.multiplyScalar( timesq ) );
+
+ this.tmp = this.previous;
+ this.previous = this.position;
+ this.position = newPos;
+
+ this.a.set( 0, 0, 0 );
+
+};
+
+
+var diff = new THREE.Vector3();
+
+function satisfyConstraints( p1, p2, distance ) {
+
+ diff.subVectors( p2.position, p1.position );
+ var currentDist = diff.length();
+ if ( currentDist === 0 ) return; // prevents division by 0
+ var correction = diff.multiplyScalar( 1 - distance / currentDist );
+ var correctionHalf = correction.multiplyScalar( 0.5 );
+ p1.position.add( correctionHalf );
+ p2.position.sub( correctionHalf );
+
+}
+
+
+function Cloth( w, h ) {
+
+ w = w || 10;
+ h = h || 10;
+ this.w = w;
+ this.h = h;
+
+ var particles = [];
+ var constraints = [];
+
+ var u, v;
+
+ // Create particles
+ for ( v = 0; v <= h; v ++ ) {
+
+ for ( u = 0; u <= w; u ++ ) {
+
+ particles.push(
+ new Particle( u / w, v / h, 0, MASS )
+ );
+
+ }
+
+ }
+
+ // Structural
+
+ for ( v = 0; v < h; v ++ ) {
+
+ for ( u = 0; u < w; u ++ ) {
+
+ constraints.push( [
+ particles[ index( u, v ) ],
+ particles[ index( u, v + 1 ) ],
+ restDistance
+ ] );
+
+ constraints.push( [
+ particles[ index( u, v ) ],
+ particles[ index( u + 1, v ) ],
+ restDistance
+ ] );
+
+ }
+
+ }
+
+ for ( u = w, v = 0; v < h; v ++ ) {
+
+ constraints.push( [
+ particles[ index( u, v ) ],
+ particles[ index( u, v + 1 ) ],
+ restDistance
+
+ ] );
+
+ }
+
+ for ( v = h, u = 0; u < w; u ++ ) {
+
+ constraints.push( [
+ particles[ index( u, v ) ],
+ particles[ index( u + 1, v ) ],
+ restDistance
+ ] );
+
+ }
+
+
+ // While many systems use shear and bend springs,
+ // the relaxed constraints model seems to be just fine
+ // using structural springs.
+ // Shear
+ // var diagonalDist = Math.sqrt(restDistance * restDistance * 2);
+
+
+ // for (v=0;v 0
+
+ // Create vertices mark
+ var vertices = object.geometry.vertices;
+ for ( var i = 0, il = vertices.length; i < il; i++ ) {
+ vertices[ i ].mark = 0;
+ }
+
+ var userData = object.userData;
+ userData.mass = mass;
+ userData.velocity = velocity.clone();
+ userData.angularVelocity = angularVelocity.clone();
+ userData.breakable = breakable;
+
+ },
+
+ /*
+ * @param {int} maxRadialIterations Iterations for radial cuts.
+ * @param {int} maxRandomIterations Max random iterations for not-radial cuts
+ * @param {double} minSizeForRadialSubdivision Min size a debris can have to break in radial subdivision.
+ *
+ * Returns the array of pieces
+ */
+ subdivideByImpact: function( object, pointOfImpact, normal, maxRadialIterations, maxRandomIterations, minSizeForRadialSubdivision ) {
+
+ var debris = [];
+
+ var tempPlane1 = this.tempPlane1;
+ var tempPlane2 = this.tempPlane2;
+
+ this.tempVector3.addVectors( pointOfImpact, normal );
+ tempPlane1.setFromCoplanarPoints( pointOfImpact, object.position, this.tempVector3 );
+
+ var maxTotalIterations = maxRandomIterations + maxRadialIterations;
+
+ var scope = this;
+
+ function subdivideRadial( subObject, startAngle, endAngle, numIterations ) {
+
+ if ( Math.random() < numIterations * 0.05 || numIterations > maxTotalIterations ) {
+
+ debris.push( subObject );
+
+ return;
+
+ }
+
+ var angle = Math.PI;
+
+ if ( numIterations === 0 ) {
+
+ tempPlane2.normal.copy( tempPlane1.normal );
+ tempPlane2.constant = tempPlane1.constant;
+
+ }
+ else {
+
+ if ( numIterations <= maxRadialIterations ) {
+
+ angle = ( endAngle - startAngle ) * ( 0.2 + 0.6 * Math.random() ) + startAngle;
+
+ // Rotate tempPlane2 at impact point around normal axis and the angle
+ scope.tempVector3_2.copy( object.position ).sub( pointOfImpact ).applyAxisAngle( normal, angle ).add( pointOfImpact );
+ tempPlane2.setFromCoplanarPoints( pointOfImpact, scope.tempVector3, scope.tempVector3_2 );
+
+ }
+ else {
+
+ angle = ( ( 0.5 * ( numIterations & 1 ) ) + 0.2 * ( 2 - Math.random() ) ) * Math.PI;
+
+ // Rotate tempPlane2 at object position around normal axis and the angle
+ scope.tempVector3_2.copy( pointOfImpact ).sub( subObject.position ).applyAxisAngle( normal, angle ).add( subObject.position );
+ scope.tempVector3_3.copy( normal ).add( subObject.position );
+ tempPlane2.setFromCoplanarPoints( subObject.position, scope.tempVector3_3, scope.tempVector3_2 );
+
+ }
+
+ }
+
+ // Perform the cut
+ scope.cutByPlane( subObject, tempPlane2, scope.tempResultObjects );
+
+ var obj1 = scope.tempResultObjects.object1;
+ var obj2 = scope.tempResultObjects.object2;
+
+ if ( obj1 ) {
+
+ subdivideRadial( obj1, startAngle, angle, numIterations + 1 );
+
+ }
+
+ if ( obj2 ) {
+
+ subdivideRadial( obj2, angle, endAngle, numIterations + 1 );
+
+ }
+
+ }
+
+ subdivideRadial( object, 0, 2 * Math.PI, 0 );
+
+ return debris;
+
+ },
+
+ cutByPlane: function( object, plane, output ) {
+
+ // Returns breakable objects in output.object1 and output.object2 members, the resulting 2 pieces of the cut.
+ // object2 can be null if the plane doesn't cut the object.
+ // object1 can be null only in case of internal error
+ // Returned value is number of pieces, 0 for error.
+
+ var geometry = object.geometry;
+ var points = geometry.vertices;
+ var faces = geometry.faces;
+
+ var numPoints = points.length;
+
+ var points1 = [];
+ var points2 = [];
+
+ var delta = this.smallDelta;
+
+ // Reset vertices mark
+ for ( var i = 0; i < numPoints; i++ ) {
+ points[ i ].mark = 0;
+ }
+
+ // Reset segments mark
+ var numPointPairs = numPoints * numPoints;
+ for ( var i = 0; i < numPointPairs; i++ ) {
+ this.segments[ i ] = false;
+ }
+
+ // Iterate through the faces to mark edges shared by coplanar faces
+ for ( var i = 0, il = faces.length - 1; i < il; i++ ) {
+
+ var face1 = faces[ i ];
+
+ for ( var j = i + 1, jl = faces.length; j < jl; j++ ) {
+
+ var face2 = faces[ j ];
+
+ var coplanar = 1 - face1.normal.dot( face2.normal ) < delta;
+
+ if ( coplanar ) {
+
+ var a1 = face1.a;
+ var b1 = face1.b;
+ var c1 = face1.c;
+ var a2 = face2.a;
+ var b2 = face2.b;
+ var c2 = face2.c;
+
+
+ if ( a1 === a2 || a1 === b2 || a1 === c2 ) {
+ if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
+ this.segments[ a1 * numPoints + b1 ] = true;
+ this.segments[ b1 * numPoints + a1 ] = true;
+ }
+ else {
+ this.segments[ c1 * numPoints + a1 ] = true;
+ this.segments[ a1 * numPoints + c1 ] = true;
+ }
+ }
+ else if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
+ this.segments[ c1 * numPoints + b1 ] = true;
+ this.segments[ b1 * numPoints + c1 ] = true;
+ }
+
+ }
+
+ }
+
+ }
+
+ // Transform the plane to object local space
+ var localPlane = this.tempPlane1;
+ object.updateMatrix();
+ THREE.ConvexObjectBreaker.transformPlaneToLocalSpace( plane, object.matrix, localPlane );
+
+ // Iterate through the faces adding points to both pieces
+ for ( var i = 0, il = faces.length; i < il; i ++ ) {
+
+ var face = faces[ i ];
+
+ for ( var segment = 0; segment < 3; segment++ ) {
+
+ var i0 = segment === 0 ? face.a : ( segment === 1 ? face.b : face.c );
+ var i1 = segment === 0 ? face.b : ( segment === 1 ? face.c : face.a );
+
+ var segmentState = this.segments[ i0 * numPoints + i1 ];
+
+ if ( segmentState ) {
+ // The segment already has been processed in another face
+ continue;
+ }
+
+ // Mark segment as processed (also inverted segment)
+ this.segments[ i0 * numPoints + i1 ] = true;
+ this.segments[ i1 * numPoints + i0 ] = true;
+
+ var p0 = points[ i0 ];
+ var p1 = points[ i1 ];
+
+ if ( p0.mark === 0 ) {
+
+ var d = localPlane.distanceToPoint( p0 );
+
+ // mark: 1 for negative side, 2 for positive side, 3 for coplanar point
+ if ( d > delta ) {
+ p0.mark = 2;
+ points2.push( p0 );
+ }
+ else if ( d < - delta ) {
+ p0.mark = 1;
+ points1.push( p0 );
+ }
+ else {
+ p0.mark = 3;
+ points1.push( p0 );
+ var p0_2 = p0.clone();
+ p0_2.mark = 3;
+ points2.push( p0_2 );
+ }
+
+ }
+
+ if ( p1.mark === 0 ) {
+
+ var d = localPlane.distanceToPoint( p1 );
+
+ // mark: 1 for negative side, 2 for positive side, 3 for coplanar point
+ if ( d > delta ) {
+ p1.mark = 2;
+ points2.push( p1 );
+ }
+ else if ( d < - delta ) {
+ p1.mark = 1;
+ points1.push( p1 );
+ }
+ else {
+ p1.mark = 3;
+ points1.push( p1 );
+ var p1_2 = p1.clone();
+ p1_2.mark = 3;
+ points2.push( p1_2 );
+ }
+
+ }
+
+ var mark0 = p0.mark;
+ var mark1 = p1.mark;
+
+ if ( ( mark0 === 1 && mark1 === 2 ) || ( mark0 === 2 && mark1 === 1 ) ) {
+
+ // Intersection of segment with the plane
+
+ this.tempLine1.start.copy( p0 );
+ this.tempLine1.end.copy( p1 );
+ var intersection = localPlane.intersectLine( this.tempLine1 );
+ if ( intersection === undefined ) {
+ // Shouldn't happen
+ console.error( "Internal error: segment does not intersect plane." );
+ output.segmentedObject1 = null;
+ output.segmentedObject2 = null;
+ return 0;
+ }
+
+ intersection.mark = 1;
+ points1.push( intersection );
+ var intersection_2 = intersection.clone();
+ intersection_2.mark = 2;
+ points2.push( intersection_2 );
+
+ }
+
+ }
+
+ }
+
+ // Calculate debris mass (very fast and imprecise):
+ var newMass = object.userData.mass * 0.5;
+
+ // Calculate debris Center of Mass (again fast and imprecise)
+ this.tempCM1.set( 0, 0, 0 );
+ var radius1 = 0;
+ var numPoints1 = points1.length;
+ if ( numPoints1 > 0 ) {
+ for ( var i = 0; i < numPoints1; i++ ) {
+ this.tempCM1.add( points1[ i ] );
+ }
+ this.tempCM1.divideScalar( numPoints1 );
+ for ( var i = 0; i < numPoints1; i++ ) {
+ var p = points1[ i ];
+ p.sub( this.tempCM1 );
+ radius1 = Math.max( radius1, p.x, p.y, p.z );
+ }
+ this.tempCM1.add( object.position );
+ }
+
+ this.tempCM2.set( 0, 0, 0 );
+ var radius2 = 0;
+ var numPoints2 = points2.length;
+ if ( numPoints2 > 0 ) {
+ for ( var i = 0; i < numPoints2; i++ ) {
+ this.tempCM2.add( points2[ i ] );
+ }
+ this.tempCM2.divideScalar( numPoints2 );
+ for ( var i = 0; i < numPoints2; i++ ) {
+ var p = points2[ i ];
+ p.sub( this.tempCM2 );
+ radius2 = Math.max( radius2, p.x, p.y, p.z );
+ }
+ this.tempCM2.add( object.position );
+ }
+
+ var object1 = null;
+ var object2 = null;
+
+ var numObjects = 0;
+
+ if ( numPoints1 > 4 ) {
+
+ object1 = new THREE.Mesh( new THREE.ConvexGeometry( points1 ), object.material );
+ object1.position.copy( this.tempCM1 );
+ object1.quaternion.copy( object.quaternion );
+
+ this.prepareBreakableObject( object1, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius1 > this.minSizeForBreak );
+
+ numObjects++;
+
+ }
+
+ if ( numPoints2 > 4 ) {
+
+ object2 = new THREE.Mesh( new THREE.ConvexGeometry( points2 ), object.material );
+ object2.position.copy( this.tempCM2 );
+ object2.quaternion.copy( object.quaternion );
+
+ this.prepareBreakableObject( object2, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius2 > this.minSizeForBreak );
+
+ numObjects++;
+
+ }
+
+
+ output.object1 = object1;
+ output.object2 = object2;
+
+ return numObjects;
+
+ }
+
+};
+
+THREE.ConvexObjectBreaker.transformFreeVector = function( v, m ) {
+
+ // input:
+ // vector interpreted as a free vector
+ // THREE.Matrix4 orthogonal matrix (matrix without scale)
+
+ var x = v.x, y = v.y, z = v.z;
+ var e = m.elements;
+
+ v.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
+ v.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
+ v.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;
+
+ return v;
+
+};
+
+THREE.ConvexObjectBreaker.transformFreeVectorInverse = function( v, m ) {
+
+ // input:
+ // vector interpreted as a free vector
+ // THREE.Matrix4 orthogonal matrix (matrix without scale)
+
+ var x = v.x, y = v.y, z = v.z;
+ var e = m.elements;
+
+ v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z;
+ v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z;
+ v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z;
+
+ return v;
+
+};
+
+THREE.ConvexObjectBreaker.transformTiedVectorInverse = function( v, m ) {
+
+ // input:
+ // vector interpreted as a tied (ordinary) vector
+ // THREE.Matrix4 orthogonal matrix (matrix without scale)
+
+ var x = v.x, y = v.y, z = v.z;
+ var e = m.elements;
+
+ v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z - e[ 12 ];
+ v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z - e[ 13 ];
+ v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z - e[ 14 ];
+
+ return v;
+
+};
+
+THREE.ConvexObjectBreaker.transformPlaneToLocalSpace = function() {
+
+ var v1 = new THREE.Vector3();
+ var m1 = new THREE.Matrix3();
+
+ return function transformPlaneToLocalSpace( plane, m, resultPlane ) {
+
+ resultPlane.normal.copy( plane.normal );
+ resultPlane.constant = plane.constant;
+
+ var referencePoint = THREE.ConvexObjectBreaker.transformTiedVectorInverse( plane.coplanarPoint( v1 ), m );
+
+ THREE.ConvexObjectBreaker.transformFreeVectorInverse( resultPlane.normal, m );
+
+ // recalculate constant (like in setFromNormalAndCoplanarPoint)
+ resultPlane.constant = - referencePoint.dot( resultPlane.normal );
+
+
+ };
+
+}();
diff --git a/app/static/js/CurveExtras.js b/app/static/js/CurveExtras.js
new file mode 100644
index 0000000..ff7a029
--- /dev/null
+++ b/app/static/js/CurveExtras.js
@@ -0,0 +1,397 @@
+/*
+ * A bunch of parametric curves
+ * @author zz85
+ *
+ * Formulas collected from various sources
+ * http://mathworld.wolfram.com/HeartCurve.html
+ * http://mathdl.maa.org/images/upload_library/23/stemkoski/knots/page6.html
+ * http://en.wikipedia.org/wiki/Viviani%27s_curve
+ * http://mathdl.maa.org/images/upload_library/23/stemkoski/knots/page4.html
+ * http://www.mi.sanu.ac.rs/vismath/taylorapril2011/Taylor.pdf
+ * http://prideout.net/blog/?p=44
+ */
+
+( function( Curves ) {
+
+ // GrannyKnot
+
+ function GrannyKnot() {
+
+ THREE.Curve.call( this );
+
+ }
+
+ GrannyKnot.prototype = Object.create( THREE.Curve.prototype );
+ GrannyKnot.prototype.constructor = GrannyKnot;
+
+ GrannyKnot.prototype.getPoint = function( t ) {
+
+ t = 2 * Math.PI * t;
+
+ var x = - 0.22 * Math.cos( t ) - 1.28 * Math.sin( t ) - 0.44 * Math.cos( 3 * t ) - 0.78 * Math.sin( 3 * t );
+ var y = - 0.1 * Math.cos( 2 * t ) - 0.27 * Math.sin( 2 * t ) + 0.38 * Math.cos( 4 * t ) + 0.46 * Math.sin( 4 * t );
+ var z = 0.7 * Math.cos( 3 * t ) - 0.4 * Math.sin( 3 * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( 20 );
+
+ };
+
+ // HeartCurve
+
+ function HeartCurve( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 5 : s;
+
+ }
+
+ HeartCurve.prototype = Object.create( THREE.Curve.prototype );
+ HeartCurve.prototype.constructor = HeartCurve;
+
+ HeartCurve.prototype.getPoint = function( t ) {
+
+ t *= 2 * Math.PI;
+
+ var x = 16 * Math.pow( Math.sin( t ), 3 );
+ var y = 13 * Math.cos( t ) - 5 * Math.cos( 2 * t ) - 2 * Math.cos( 3 * t ) - Math.cos( 4 * t );
+ var z = 0;
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // Viviani's Curve
+
+ function VivianiCurve( radius ) {
+
+ THREE.Curve.call( this );
+
+ this.radius = radius;
+
+ }
+
+ VivianiCurve.prototype = Object.create( THREE.Curve.prototype );
+ VivianiCurve.prototype.constructor = VivianiCurve;
+
+ VivianiCurve.prototype.getPoint = function( t ) {
+
+ t = t * 4 * Math.PI; // normalized to 0..1
+ var a = this.radius / 2;
+
+ var x = a * ( 1 + Math.cos( t ) );
+ var y = a * Math.sin( t );
+ var z = 2 * a * Math.sin( t / 2 );
+
+ return new THREE.Vector3( x, y, z );
+
+ };
+
+ // KnotCurve
+
+ function KnotCurve() {
+
+ THREE.Curve.call( this );
+
+ }
+
+ KnotCurve.prototype = Object.create( THREE.Curve.prototype );
+ KnotCurve.prototype.constructor = KnotCurve;
+
+ KnotCurve.prototype.getPoint = function( t ) {
+
+ t *= 2 * Math.PI;
+
+ var R = 10;
+ var s = 50;
+
+ var x = s * Math.sin( t );
+ var y = Math.cos( t ) * ( R + s * Math.cos( t ) );
+ var z = Math.sin( t ) * ( R + s * Math.cos( t ) );
+
+ return new THREE.Vector3( x, y, z );
+
+ };
+
+ // HelixCurve
+
+ function HelixCurve() {
+
+ THREE.Curve.call( this );
+
+ }
+
+ HelixCurve.prototype = Object.create( THREE.Curve.prototype );
+ HelixCurve.prototype.constructor = HelixCurve;
+
+ HelixCurve.prototype.getPoint = function( t ) {
+
+ var a = 30; // radius
+ var b = 150; // height
+
+ var t2 = 2 * Math.PI * t * b / 30;
+
+ var x = Math.cos( t2 ) * a;
+ var y = Math.sin( t2 ) * a;
+ var z = b * t;
+
+ return new THREE.Vector3( x, y, z );
+
+ };
+
+ // TrefoilKnot
+
+ function TrefoilKnot( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 10 : s;
+
+ }
+
+ TrefoilKnot.prototype = Object.create( THREE.Curve.prototype );
+ TrefoilKnot.prototype.constructor = TrefoilKnot;
+
+ TrefoilKnot.prototype.getPoint = function( t ) {
+
+ t *= Math.PI * 2;
+
+ var x = ( 2 + Math.cos( 3 * t ) ) * Math.cos( 2 * t );
+ var y = ( 2 + Math.cos( 3 * t ) ) * Math.sin( 2 * t );
+ var z = Math.sin( 3 * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // TorusKnot
+
+ function TorusKnot( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 10 : s;
+
+ }
+
+ TorusKnot.prototype = Object.create( THREE.Curve.prototype );
+ TorusKnot.prototype.constructor = TorusKnot;
+
+ TorusKnot.prototype.getPoint = function( t ) {
+
+ var p = 3;
+ var q = 4;
+
+ t *= Math.PI * 2;
+
+ var x = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t );
+ var y = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t );
+ var z = Math.sin( q * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // CinquefoilKnot
+
+ function CinquefoilKnot( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 10 : s;
+
+ }
+
+ CinquefoilKnot.prototype = Object.create( THREE.Curve.prototype );
+ CinquefoilKnot.prototype.constructor = CinquefoilKnot;
+
+ CinquefoilKnot.prototype.getPoint = function( t ) {
+
+ var p = 2;
+ var q = 5;
+
+ t *= Math.PI * 2;
+
+ var x = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t );
+ var y = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t );
+ var z = Math.sin( q * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // TrefoilPolynomialKnot
+
+ function TrefoilPolynomialKnot( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 10 : s;
+
+ }
+
+ TrefoilPolynomialKnot.prototype = Object.create( THREE.Curve.prototype );
+ TrefoilPolynomialKnot.prototype.constructor = TrefoilPolynomialKnot;
+
+ TrefoilPolynomialKnot.prototype.getPoint = function( t ) {
+
+ t = t * 4 - 2;
+
+ var x = Math.pow( t, 3 ) - 3 * t;
+ var y = Math.pow( t, 4 ) - 4 * t * t;
+ var z = 1 / 5 * Math.pow( t, 5 ) - 2 * t;
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ var scaleTo = function( x, y, t ) {
+
+ var r = y - x;
+ return t * r + x;
+
+ };
+
+ // FigureEightPolynomialKnot
+
+ function FigureEightPolynomialKnot( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 1 : s;
+
+ }
+
+ FigureEightPolynomialKnot.prototype = Object.create( THREE.Curve.prototype );
+ FigureEightPolynomialKnot.prototype.constructor = FigureEightPolynomialKnot;
+
+ FigureEightPolynomialKnot.prototype.getPoint = function( t ) {
+
+ t = scaleTo( - 4, 4, t );
+
+ var x = 2 / 5 * t * ( t * t - 7 ) * ( t * t - 10 );
+ var y = Math.pow( t, 4 ) - 13 * t * t;
+ var z = 1 / 10 * t * ( t * t - 4 ) * ( t * t - 9 ) * ( t * t - 12 );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // DecoratedTorusKnot4a
+
+ function DecoratedTorusKnot4a( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 40 : s;
+
+ }
+
+ DecoratedTorusKnot4a.prototype = Object.create( THREE.Curve.prototype );
+ DecoratedTorusKnot4a.prototype.constructor = DecoratedTorusKnot4a;
+
+ DecoratedTorusKnot4a.prototype.getPoint = function( t ) {
+
+ t *= Math.PI * 2;
+
+ var x = Math.cos( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) );
+ var y = Math.sin( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) );
+ var z = 0.35 * Math.sin( 5 * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // DecoratedTorusKnot4b
+
+ function DecoratedTorusKnot4b( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 40 : s;
+
+ }
+
+ DecoratedTorusKnot4b.prototype = Object.create( THREE.Curve.prototype );
+ DecoratedTorusKnot4b.prototype.constructor = DecoratedTorusKnot4b;
+
+ DecoratedTorusKnot4b.prototype.getPoint = function( t ) {
+
+ var fi = t * Math.PI * 2;
+
+ var x = Math.cos( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) );
+ var y = Math.sin( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) );
+ var z = 0.2 * Math.sin( 9 * fi );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // DecoratedTorusKnot5a
+
+ function DecoratedTorusKnot5a( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 40 : s;
+
+ }
+
+ DecoratedTorusKnot5a.prototype = Object.create( THREE.Curve.prototype );
+ DecoratedTorusKnot5a.prototype.constructor = DecoratedTorusKnot5a;
+
+ DecoratedTorusKnot5a.prototype.getPoint = function( t ) {
+
+ var fi = t * Math.PI * 2;
+
+ var x = Math.cos( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) );
+ var y = Math.sin( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) );
+ var z = 0.2 * Math.sin( 20 * fi );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // DecoratedTorusKnot5c
+
+ function DecoratedTorusKnot5c( s ) {
+
+ THREE.Curve.call( this );
+
+ this.scale = ( s === undefined ) ? 40 : s;
+
+ }
+
+ DecoratedTorusKnot5c.prototype = Object.create( THREE.Curve.prototype );
+ DecoratedTorusKnot5c.prototype.constructor = DecoratedTorusKnot5c;
+
+ DecoratedTorusKnot5c.prototype.getPoint = function( t ) {
+
+ var fi = t * Math.PI * 2;
+
+ var x = Math.cos( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) );
+ var y = Math.sin( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) );
+ var z = 0.35 * Math.sin( 15 * fi );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale );
+
+ };
+
+ // export
+
+ Curves.GrannyKnot = GrannyKnot;
+ Curves.HeartCurve = HeartCurve;
+ Curves.VivianiCurve = VivianiCurve;
+ Curves.KnotCurve = KnotCurve;
+ Curves.HelixCurve = HelixCurve;
+ Curves.TrefoilKnot = TrefoilKnot;
+ Curves.TorusKnot = TorusKnot;
+ Curves.CinquefoilKnot = CinquefoilKnot;
+ Curves.TrefoilPolynomialKnot = TrefoilPolynomialKnot;
+ Curves.FigureEightPolynomialKnot = FigureEightPolynomialKnot;
+ Curves.DecoratedTorusKnot4a = DecoratedTorusKnot4a;
+ Curves.DecoratedTorusKnot4b = DecoratedTorusKnot4b;
+ Curves.DecoratedTorusKnot5a = DecoratedTorusKnot5a;
+ Curves.DecoratedTorusKnot5c = DecoratedTorusKnot5c;
+
+} ) ( THREE.Curves = THREE.Curves || {} );
diff --git a/app/static/js/Detector.js b/app/static/js/Detector.js
new file mode 100644
index 0000000..95a2724
--- /dev/null
+++ b/app/static/js/Detector.js
@@ -0,0 +1,78 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ * @author mr.doob / http://mrdoob.com/
+ */
+
+var Detector = {
+
+ canvas: !! window.CanvasRenderingContext2D,
+ webgl: ( function () {
+
+ try {
+
+ var canvas = document.createElement( 'canvas' ); return !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) );
+
+ } catch ( e ) {
+
+ return false;
+
+ }
+
+ } )(),
+ workers: !! window.Worker,
+ fileapi: window.File && window.FileReader && window.FileList && window.Blob,
+
+ getWebGLErrorMessage: function () {
+
+ var element = document.createElement( 'div' );
+ element.id = 'webgl-error-message';
+ element.style.fontFamily = 'monospace';
+ element.style.fontSize = '13px';
+ element.style.fontWeight = 'normal';
+ element.style.textAlign = 'center';
+ element.style.background = '#fff';
+ element.style.color = '#000';
+ element.style.padding = '1.5em';
+ element.style.width = '400px';
+ element.style.margin = '5em auto 0';
+
+ if ( ! this.webgl ) {
+
+ element.innerHTML = window.WebGLRenderingContext ? [
+ 'Your graphics card does not seem to support WebGL.
',
+ 'Find out how to get it here.'
+ ].join( '\n' ) : [
+ 'Your browser does not seem to support WebGL.
',
+ 'Find out how to get it here.'
+ ].join( '\n' );
+
+ }
+
+ return element;
+
+ },
+
+ addGetWebGLMessage: function ( parameters ) {
+
+ var parent, id, element;
+
+ parameters = parameters || {};
+
+ parent = parameters.parent !== undefined ? parameters.parent : document.body;
+ id = parameters.id !== undefined ? parameters.id : 'oldie';
+
+ element = Detector.getWebGLErrorMessage();
+ element.id = id;
+
+ parent.appendChild( element );
+
+ }
+
+};
+
+// browserify support
+if ( typeof module === 'object' ) {
+
+ module.exports = Detector;
+
+}
diff --git a/app/static/js/Editor.js b/app/static/js/Editor.js
new file mode 100644
index 0000000..5ed66df
--- /dev/null
+++ b/app/static/js/Editor.js
@@ -0,0 +1,719 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Editor = function () {
+
+ this.DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.1, 10000 );
+ this.DEFAULT_CAMERA.name = 'Camera';
+ this.DEFAULT_CAMERA.position.set( 20, 10, 20 );
+ this.DEFAULT_CAMERA.lookAt( new THREE.Vector3() );
+ this.project_uuid = "";
+
+ var Signal = signals.Signal;
+
+ this.signals = {
+
+ // script
+
+ editScript: new Signal(),
+
+ // player
+
+ startPlayer: new Signal(),
+ stopPlayer: new Signal(),
+
+ // actions
+
+ showModal: new Signal(),
+
+ // notifications
+
+ editorCleared: new Signal(),
+
+ savingStarted: new Signal(),
+ savingFinished: new Signal(),
+
+ themeChanged: new Signal(),
+
+ transformModeChanged: new Signal(),
+ snapChanged: new Signal(),
+ spaceChanged: new Signal(),
+ rendererChanged: new Signal(),
+
+ sceneBackgroundChanged: new Signal(),
+ sceneFogChanged: new Signal(),
+ sceneGraphChanged: new Signal(),
+
+ cameraChanged: new Signal(),
+
+ geometryChanged: new Signal(),
+
+ objectSelected: new Signal(),
+ objectFocused: new Signal(),
+
+ objectAdded: new Signal(),
+ objectChanged: new Signal(),
+ objectRemoved: new Signal(),
+
+ helperAdded: new Signal(),
+ helperRemoved: new Signal(),
+
+ materialChanged: new Signal(),
+
+ scriptAdded: new Signal(),
+ scriptChanged: new Signal(),
+ scriptRemoved: new Signal(),
+
+ windowResize: new Signal(),
+
+ showGridChanged: new Signal(),
+ refreshSidebarObject3D: new Signal(),
+ historyChanged: new Signal()
+
+ };
+
+ this.config = new Config( 'threejs-editor' );
+ this.history = new History( this );
+ this.storage = new Storage();
+ this.loader = new Loader( this );
+
+ this.camera = this.DEFAULT_CAMERA.clone();
+
+ this.scene = new THREE.Scene();
+ this.scene.name = 'Scene';
+ this.scene.background = new THREE.Color( 0xaaaaaa );
+
+ this.sceneHelpers = new THREE.Scene();
+
+ this.object = {};
+ this.geometries = {};
+ this.materials = {};
+ this.textures = {};
+ this.scripts = {};
+
+ this.selected = null;
+ this.helpers = {};
+
+};
+
+var length = 20;
+var wingspan = 15;
+var height = 5;
+var x_max = 0;
+var x_min = 0;
+var y_max = 0;
+var y_min = 0;
+var z_max = 0;
+var z_min = 0;
+var z_short = 0;
+var y_short = 0;
+
+Editor.prototype = {
+
+ setTheme: function ( value ) {
+
+ document.getElementById( 'theme' ).href = value;
+
+ this.signals.themeChanged.dispatch( value );
+
+ },
+
+ //
+
+ setScene: function ( scene ) {
+
+ this.scene.uuid = scene.uuid;
+ this.scene.name = scene.name;
+
+ if ( scene.background !== null ) this.scene.background = scene.background.clone();
+ if ( scene.fog !== null ) this.scene.fog = scene.fog.clone();
+
+ this.scene.userData = JSON.parse( JSON.stringify( scene.userData ) );
+
+ // avoid render per object
+
+ this.signals.sceneGraphChanged.active = false;
+
+ while ( scene.children.length > 0 ) {
+
+ this.addObject( scene.children[ 0 ] );
+
+ }
+
+ this.signals.sceneGraphChanged.active = true;
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ //
+
+ addObject: function ( object ) {
+
+ var scope = this;
+
+ object.traverse( function ( child ) {
+
+ if ( child.geometry !== undefined ) scope.addGeometry( child.geometry );
+ if ( child.material !== undefined ) scope.addMaterial( child.material );
+
+ scope.addHelper( child );
+
+ } );
+
+ this.scene.add( object );
+
+ this.signals.objectAdded.dispatch( object );
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ moveObject: function ( object, parent, before ) {
+
+ if ( parent === undefined ) {
+
+ parent = this.scene;
+
+ }
+
+ parent.add( object );
+
+ // sort children array
+
+ if ( before !== undefined ) {
+
+ var index = parent.children.indexOf( before );
+ parent.children.splice( index, 0, object );
+ parent.children.pop();
+
+ }
+
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ nameObject: function ( object, name ) {
+
+ object.name = name;
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ removeObject: function ( object ) {
+
+ if ( object.parent === null ) return; // avoid deleting the camera or scene
+
+ var scope = this;
+
+ object.traverse( function ( child ) {
+
+ scope.removeHelper( child );
+
+ } );
+
+ object.parent.remove( object );
+
+ this.signals.objectRemoved.dispatch( object );
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ addGeometry: function ( geometry ) {
+
+ this.geometries[ geometry.uuid ] = geometry;
+
+ },
+
+ setGeometryName: function ( geometry, name ) {
+
+ geometry.name = name;
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ addMaterial: function ( material ) {
+
+ this.materials[ material.uuid ] = material;
+
+ },
+
+ setMaterialName: function ( material, name ) {
+
+ material.name = name;
+ this.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ addTexture: function ( texture ) {
+
+ this.textures[ texture.uuid ] = texture;
+
+ },
+
+ //
+
+ addHelper: function () {
+
+ var geometry = new THREE.SphereBufferGeometry( 2, 4, 2 );
+ var material = new THREE.MeshBasicMaterial( { color: 0xff0000, visible: false } );
+
+ return function ( object ) {
+
+ var helper;
+
+ if ( object instanceof THREE.Camera ) {
+
+ helper = new THREE.CameraHelper( object, 1 );
+
+ } else if ( object instanceof THREE.PointLight ) {
+
+ helper = new THREE.PointLightHelper( object, 1 );
+
+ } else if ( object instanceof THREE.DirectionalLight ) {
+
+ helper = new THREE.DirectionalLightHelper( object, 1 );
+
+ } else if ( object instanceof THREE.SpotLight ) {
+
+ helper = new THREE.SpotLightHelper( object, 1 );
+
+ } else if ( object instanceof THREE.HemisphereLight ) {
+
+ helper = new THREE.HemisphereLightHelper( object, 1 );
+
+ } else if ( object instanceof THREE.SkinnedMesh ) {
+
+ helper = new THREE.SkeletonHelper( object );
+
+ } else {
+
+ // no helper for this object type
+ return;
+
+ }
+
+ var picker = new THREE.Mesh( geometry, material );
+ picker.name = 'picker';
+ picker.userData.object = object;
+ helper.add( picker );
+
+ this.sceneHelpers.add( helper );
+ this.helpers[ object.id ] = helper;
+
+ this.signals.helperAdded.dispatch( helper );
+
+ };
+
+ }(),
+
+ removeHelper: function ( object ) {
+
+ if ( this.helpers[ object.id ] !== undefined ) {
+
+ var helper = this.helpers[ object.id ];
+ helper.parent.remove( helper );
+
+ delete this.helpers[ object.id ];
+
+ this.signals.helperRemoved.dispatch( helper );
+
+ }
+
+ },
+
+ //
+
+ addScript: function ( object, script ) {
+
+ if ( this.scripts[ object.uuid ] === undefined ) {
+
+ this.scripts[ object.uuid ] = [];
+
+ }
+
+ this.scripts[ object.uuid ].push( script );
+
+ this.signals.scriptAdded.dispatch( script );
+
+ },
+
+ removeScript: function ( object, script ) {
+
+ if ( this.scripts[ object.uuid ] === undefined ) return;
+
+ var index = this.scripts[ object.uuid ].indexOf( script );
+
+ if ( index !== - 1 ) {
+
+ this.scripts[ object.uuid ].splice( index, 1 );
+
+ }
+
+ this.signals.scriptRemoved.dispatch( script );
+
+ },
+
+ getObjectMaterial: function ( object, slot ) {
+
+ var material = object.material;
+
+ if ( Array.isArray( material ) ) {
+
+ material = material[ slot ];
+
+ }
+
+ return material;
+
+ },
+
+ setObjectMaterial: function ( object, slot, newMaterial ) {
+
+ if ( Array.isArray( object.material ) ) {
+
+ object.material[ slot ] = newMaterial;
+
+ } else {
+
+ object.material = newMaterial;
+
+ }
+
+ },
+
+ //
+
+ select: function ( object ) {
+ if ( this.selected === object ) return;
+
+ if(this.scene == object || this.scene === object.parent){
+ var uuid = null;
+
+ if ( object !== null ) {
+
+ uuid = object.uuid;
+
+ }
+
+ this.selected = object;
+
+ this.config.setKey( 'selected', uuid );
+ this.signals.objectSelected.dispatch( object );
+
+ } else {
+ var uuid = null;
+
+ if ( object.parent !== null ) {
+
+ uuid = object.parent.uuid;
+
+ }
+
+ this.selected = object.parent;
+
+ this.config.setKey( 'selected', uuid );
+ this.signals.objectSelected.dispatch( object.parent );
+ }
+ },
+
+ selectById: function ( id ) {
+
+ if ( id === this.camera.id ) {
+
+ this.select( this.camera );
+ return;
+
+ }
+
+ this.select( this.scene.getObjectById( id, true ) );
+
+ },
+
+ selectByUuid: function ( uuid ) {
+
+ var scope = this;
+
+ this.scene.traverse( function ( child ) {
+
+ if ( child.uuid === uuid ) {
+
+ scope.select( child );
+
+ }
+
+ } );
+
+ },
+
+ deselect: function () {
+
+ this.select( null );
+
+ },
+
+ focus: function ( object ) {
+
+ this.signals.objectFocused.dispatch( object );
+
+ },
+
+ focusById: function ( id ) {
+
+ this.focus( this.scene.getObjectById( id, true ) );
+
+ },
+
+ clear: function () {
+
+ this.history.clear();
+ this.storage.clear();
+
+ this.camera.copy( this.DEFAULT_CAMERA );
+ this.scene.background.setHex( 0xaaaaaa );
+ this.scene.fog = null;
+
+ var objects = this.scene.children;
+
+ while ( objects.length > 0 ) {
+
+ this.removeObject( objects[ 0 ] );
+
+ }
+
+ this.geometries = {};
+ this.materials = {};
+ this.textures = {};
+ this.scripts = {};
+
+ this.deselect();
+
+ this.signals.editorCleared.dispatch();
+ length = 20;
+ wingspan = 15;
+ height = 5;
+ x_max = 0;
+ x_min = 0;
+ y_max = 0;
+ y_min = 0;
+ z_max = 0;
+ z_min = 0;
+
+ },
+
+ //
+
+ fromJSON: function ( json ) {
+
+ var loader = new THREE.ObjectLoader();
+
+ // backwards
+
+ if ( json.scene === undefined ) {
+
+ this.setScene( loader.parse( json ) );
+ return;
+
+ }
+
+ var camera = loader.parse( json.camera );
+ this.project_uuid = json.project.uuid;
+ this.camera.copy( camera );
+ this.camera.aspect = this.DEFAULT_CAMERA.aspect;
+ this.camera.updateProjectionMatrix();
+
+ this.history.fromJSON( json.history );
+ this.scripts = json.scripts;
+
+ this.setScene( loader.parse( json.scene ) );
+
+ },
+
+ toJSON: function () {
+
+ // scripts clean up
+
+ var scene = this.scene;
+ var scripts = this.scripts;
+
+ for ( var key in scripts ) {
+
+ var script = scripts[ key ];
+
+ if ( script.length === 0 || scene.getObjectByProperty( 'uuid', key ) === undefined ) {
+
+ delete scripts[ key ];
+
+ }
+
+ }
+
+ //
+
+ return {
+
+ metadata: {},
+ project: {
+ uuid: this.project_uuid,
+ gammaInput: this.config.getKey( 'project/renderer/gammaInput' ),
+ gammaOutput: this.config.getKey( 'project/renderer/gammaOutput' ),
+ shadows: this.config.getKey( 'project/renderer/shadows' ),
+ vr: this.config.getKey( 'project/vr' )
+ },
+ camera: this.camera.toJSON(),
+ scene: this.scene.toJSON(),
+ scripts: this.scripts,
+ history: this.history.toJSON()
+
+ };
+
+ },
+
+ objectByUuid: function ( uuid ) {
+
+ return this.scene.getObjectByProperty( 'uuid', uuid, true );
+
+ },
+
+ execute: function ( cmd, optionalName ) {
+
+ this.history.execute( cmd, optionalName );
+
+ },
+
+ undo: function () {
+
+ this.history.undo();
+
+ },
+
+ redo: function () {
+
+ this.history.redo();
+
+ },
+
+ getModelLength: function() { // return model length
+
+ return length;
+
+ },
+
+ getModelWingspan: function() { // return model wingspan
+
+ return wingspan;
+
+ },
+
+ getModelHeight: function() { // return model height (nose up)
+
+ return height;
+
+ },
+
+ setModelDimensions: function( len, wing, heigh ) { // set input model dimensions
+
+ length = len;
+ wingspan = wing;
+ height = heigh;
+
+ },
+
+ setModel: function ( geo ){
+
+ for ( var i = 0; i < geo.children.length; i++ ){ // loop through each vertex of the model
+ var type = geo.children[i].geometry;
+ if ( type.type === "BufferGeometry" ) { // check if object makeup is of type "BufferGeometry"
+ var next = type.attributes.position.array;
+ for (var j = 0; j < next.length; j = j + 3) {
+ if ( next[j] > x_max ) { // store the max and min value on each axis
+ x_max = next[j];
+ }
+ if ( next[j] < x_min ) {
+ x_min = next[j];
+ }
+ if ( next[j + 1] > y_max ) {
+ y_max = next[j + 1];
+ }
+ if ( next[j + 1] < y_min ) {
+ y_min = next[j + 1];
+ }
+ if ( next[j + 2] > z_max ) {
+ z_max = next[j + 2];
+ y_short = next[j + 1]; // for a converted zero base for z, store the associated minimum at the nose of the model
+ }
+ if ( next[j + 2] < z_min ) {
+ z_min = next[j + 2];
+ z_short = next[j + 1];
+ }
+ }
+ }
+ else if ( type.type === "Geometry" ){ // check if makeup is of type "Geometry"
+ var next = type.vertices; // perform same steps as "BufferGeometry"
+ for ( var j = 0; j < next.length; j++ ) {
+ if ( next[j].x > x_max ) {
+ x_max = next[j].x;
+ }
+ if ( next[j].x < x_min ) {
+ x_min = next[j].x;
+ }
+ if ( next[j].y > y_max ) {
+ y_max = next[j].y;
+ }
+ if ( next[j].y < y_min ) {
+ y_min = next[j].y;
+ }
+ if ( next[j].z > z_max ) {
+ z_max = next[j].z;
+ y_short = next[j].y;
+ }
+ if ( next[j].z < z_min ) {
+ z_min = next[j].z;
+ z_short = next[j].y;
+ }
+ }
+ }
+ }
+
+ if ( geo.name === "A-10 Thunderbolt II" ){ // since A-10 model generates backwards, flip z axis results
+
+ var temp = z_max;
+ z_max = ( z_min * -1 ) - .2;
+ z_min = temp * -1;
+ y_min = z_short + .4; // set y minimum
+
+ var newRotation = new THREE.Euler( 0, 180 * THREE.Math.DEG2RAD, 0 ); // rotate model accordingly
+ this.execute( new SetRotationCommand( geo, newRotation ) );
+
+ }
+ else {
+ y_min = y_short; // for all other models set y minimum
+ }
+
+ x_max = geo.scale.x * x_max; // scale each value according to the model's preset scale
+ x_min = geo.scale.x * x_min;
+ y_max = geo.scale.y * y_max;
+ y_min = geo.scale.y * y_min;
+ z_max = geo.scale.z * z_max;
+ z_min = geo.scale.z * z_min;
+
+ var scale = z_max - z_min;
+ var newPos = new THREE.Vector3( scale , scale/2, scale );
+ this.execute( new SetPositionCommand( this.camera, newPos ) );
+
+ },
+
+ getModel: function (){ // return all axis extreme values
+
+ var array = new Object();
+ array[0] = x_max;
+ array[1] = x_min;
+ array[2] = y_max;
+ array[3] = y_min;
+ array[4] = z_max;
+ array[5] = z_min;
+ return array;
+
+ }
+
+
+};
diff --git a/app/static/js/GPUComputationRenderer.js b/app/static/js/GPUComputationRenderer.js
new file mode 100644
index 0000000..3c64d7c
--- /dev/null
+++ b/app/static/js/GPUComputationRenderer.js
@@ -0,0 +1,370 @@
+/**
+ * @author yomboprime https://github.com/yomboprime
+ *
+ * GPUComputationRenderer, based on SimulationRenderer by zz85
+ *
+ * The GPUComputationRenderer uses the concept of variables. These variables are RGBA float textures that hold 4 floats
+ * for each compute element (texel)
+ *
+ * Each variable has a fragment shader that defines the computation made to obtain the variable in question.
+ * You can use as many variables you need, and make dependencies so you can use textures of other variables in the shader
+ * (the sampler uniforms are added automatically) Most of the variables will need themselves as dependency.
+ *
+ * The renderer has actually two render targets per variable, to make ping-pong. Textures from the current frame are used
+ * as inputs to render the textures of the next frame.
+ *
+ * The render targets of the variables can be used as input textures for your visualization shaders.
+ *
+ * Variable names should be valid identifiers and should not collide with THREE GLSL used identifiers.
+ * a common approach could be to use 'texture' prefixing the variable name; i.e texturePosition, textureVelocity...
+ *
+ * The size of the computation (sizeX * sizeY) is defined as 'resolution' automatically in the shader. For example:
+ * #DEFINE resolution vec2( 1024.0, 1024.0 )
+ *
+ * -------------
+ *
+ * Basic use:
+ *
+ * // Initialization...
+ *
+ * // Create computation renderer
+ * var gpuCompute = new GPUComputationRenderer( 1024, 1024, renderer );
+ *
+ * // Create initial state float textures
+ * var pos0 = gpuCompute.createTexture();
+ * var vel0 = gpuCompute.createTexture();
+ * // and fill in here the texture data...
+ *
+ * // Add texture variables
+ * var velVar = gpuCompute.addVariable( "textureVelocity", fragmentShaderVel, pos0 );
+ * var posVar = gpuCompute.addVariable( "texturePosition", fragmentShaderPos, vel0 );
+ *
+ * // Add variable dependencies
+ * gpuCompute.setVariableDependencies( velVar, [ velVar, posVar ] );
+ * gpuCompute.setVariableDependencies( posVar, [ velVar, posVar ] );
+ *
+ * // Add custom uniforms
+ * velVar.material.uniforms.time = { value: 0.0 };
+ *
+ * // Check for completeness
+ * var error = gpuCompute.init();
+ * if ( error !== null ) {
+ * console.error( error );
+ * }
+ *
+ *
+ * // In each frame...
+ *
+ * // Compute!
+ * gpuCompute.compute();
+ *
+ * // Update texture uniforms in your visualization materials with the gpu renderer output
+ * myMaterial.uniforms.myTexture.value = gpuCompute.getCurrentRenderTarget( posVar ).texture;
+ *
+ * // Do your rendering
+ * renderer.render( myScene, myCamera );
+ *
+ * -------------
+ *
+ * Also, you can use utility functions to create ShaderMaterial and perform computations (rendering between textures)
+ * Note that the shaders can have multiple input textures.
+ *
+ * var myFilter1 = gpuCompute.createShaderMaterial( myFilterFragmentShader1, { theTexture: { value: null } } );
+ * var myFilter2 = gpuCompute.createShaderMaterial( myFilterFragmentShader2, { theTexture: { value: null } } );
+ *
+ * var inputTexture = gpuCompute.createTexture();
+ *
+ * // Fill in here inputTexture...
+ *
+ * myFilter1.uniforms.theTexture.value = inputTexture;
+ *
+ * var myRenderTarget = gpuCompute.createRenderTarget();
+ * myFilter2.uniforms.theTexture.value = myRenderTarget.texture;
+ *
+ * var outputRenderTarget = gpuCompute.createRenderTarget();
+ *
+ * // Now use the output texture where you want:
+ * myMaterial.uniforms.map.value = outputRenderTarget.texture;
+ *
+ * // And compute each frame, before rendering to screen:
+ * gpuCompute.doRenderTarget( myFilter1, myRenderTarget );
+ * gpuCompute.doRenderTarget( myFilter2, outputRenderTarget );
+ *
+ *
+ *
+ * @param {int} sizeX Computation problem size is always 2d: sizeX * sizeY elements.
+ * @param {int} sizeY Computation problem size is always 2d: sizeX * sizeY elements.
+ * @param {WebGLRenderer} renderer The renderer
+ */
+
+function GPUComputationRenderer( sizeX, sizeY, renderer ) {
+
+ this.variables = [];
+
+ this.currentTextureIndex = 0;
+
+ var scene = new THREE.Scene();
+
+ var camera = new THREE.Camera();
+ camera.position.z = 1;
+
+ var passThruUniforms = {
+ texture: { value: null }
+ };
+
+ var passThruShader = createShaderMaterial( getPassThroughFragmentShader(), passThruUniforms );
+
+ var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), passThruShader );
+ scene.add( mesh );
+
+
+ this.addVariable = function( variableName, computeFragmentShader, initialValueTexture ) {
+
+ var material = this.createShaderMaterial( computeFragmentShader );
+
+ var variable = {
+ name: variableName,
+ initialValueTexture: initialValueTexture,
+ material: material,
+ dependencies: null,
+ renderTargets: [],
+ wrapS: null,
+ wrapT: null,
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter
+ };
+
+ this.variables.push( variable );
+
+ return variable;
+
+ };
+
+ this.setVariableDependencies = function( variable, dependencies ) {
+
+ variable.dependencies = dependencies;
+
+ };
+
+ this.init = function() {
+
+ if ( ! renderer.extensions.get( "OES_texture_float" ) ) {
+
+ return "No OES_texture_float support for float textures.";
+
+ }
+
+ if ( renderer.capabilities.maxVertexTextures === 0 ) {
+
+ return "No support for vertex shader textures.";
+
+ }
+
+ for ( var i = 0; i < this.variables.length; i++ ) {
+
+ var variable = this.variables[ i ];
+
+ // Creates rendertargets and initialize them with input texture
+ variable.renderTargets[ 0 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
+ variable.renderTargets[ 1 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
+ this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 0 ] );
+ this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 1 ] );
+
+ // Adds dependencies uniforms to the ShaderMaterial
+ var material = variable.material;
+ var uniforms = material.uniforms;
+ if ( variable.dependencies !== null ) {
+
+ for ( var d = 0; d < variable.dependencies.length; d++ ) {
+
+ var depVar = variable.dependencies[ d ];
+
+ if ( depVar.name !== variable.name ) {
+
+ // Checks if variable exists
+ var found = false;
+ for ( var j = 0; j < this.variables.length; j++ ) {
+
+ if ( depVar.name === this.variables[ j ].name ) {
+ found = true;
+ break;
+ }
+
+ }
+ if ( ! found ) {
+ return "Variable dependency not found. Variable=" + variable.name + ", dependency=" + depVar.name;
+ }
+
+ }
+
+ uniforms[ depVar.name ] = { value: null };
+
+ material.fragmentShader = "\nuniform sampler2D " + depVar.name + ";\n" + material.fragmentShader;
+
+ }
+ }
+ }
+
+ this.currentTextureIndex = 0;
+
+ return null;
+
+ };
+
+ this.compute = function() {
+
+ var currentTextureIndex = this.currentTextureIndex;
+ var nextTextureIndex = this.currentTextureIndex === 0 ? 1 : 0;
+
+ for ( var i = 0, il = this.variables.length; i < il; i++ ) {
+
+ var variable = this.variables[ i ];
+
+ // Sets texture dependencies uniforms
+ if ( variable.dependencies !== null ) {
+
+ var uniforms = variable.material.uniforms;
+ for ( var d = 0, dl = variable.dependencies.length; d < dl; d++ ) {
+
+ var depVar = variable.dependencies[ d ];
+
+ uniforms[ depVar.name ].value = depVar.renderTargets[ currentTextureIndex ].texture;
+
+ }
+
+ }
+
+ // Performs the computation for this variable
+ this.doRenderTarget( variable.material, variable.renderTargets[ nextTextureIndex ] );
+
+ }
+
+ this.currentTextureIndex = nextTextureIndex;
+ };
+
+ this.getCurrentRenderTarget = function( variable ) {
+
+ return variable.renderTargets[ this.currentTextureIndex ];
+
+ };
+
+ this.getAlternateRenderTarget = function( variable ) {
+
+ return variable.renderTargets[ this.currentTextureIndex === 0 ? 1 : 0 ];
+
+ };
+
+ function addResolutionDefine( materialShader ) {
+
+ materialShader.defines.resolution = 'vec2( ' + sizeX.toFixed( 1 ) + ', ' + sizeY.toFixed( 1 ) + " )";
+
+ }
+ this.addResolutionDefine = addResolutionDefine;
+
+
+ // The following functions can be used to compute things manually
+
+ function createShaderMaterial( computeFragmentShader, uniforms ) {
+
+ uniforms = uniforms || {};
+
+ var material = new THREE.ShaderMaterial( {
+ uniforms: uniforms,
+ vertexShader: getPassThroughVertexShader(),
+ fragmentShader: computeFragmentShader
+ } );
+
+ addResolutionDefine( material );
+
+ return material;
+ }
+ this.createShaderMaterial = createShaderMaterial;
+
+ this.createRenderTarget = function( sizeXTexture, sizeYTexture, wrapS, wrapT, minFilter, magFilter ) {
+
+ sizeXTexture = sizeXTexture || sizeX;
+ sizeYTexture = sizeYTexture || sizeY;
+
+ wrapS = wrapS || THREE.ClampToEdgeWrapping;
+ wrapT = wrapT || THREE.ClampToEdgeWrapping;
+
+ minFilter = minFilter || THREE.NearestFilter;
+ magFilter = magFilter || THREE.NearestFilter;
+
+ var renderTarget = new THREE.WebGLRenderTarget( sizeXTexture, sizeYTexture, {
+ wrapS: wrapS,
+ wrapT: wrapT,
+ minFilter: minFilter,
+ magFilter: magFilter,
+ format: THREE.RGBAFormat,
+ type: ( /(iPad|iPhone|iPod)/g.test( navigator.userAgent ) ) ? THREE.HalfFloatType : THREE.FloatType,
+ stencilBuffer: false
+ } );
+
+ return renderTarget;
+
+ };
+
+ this.createTexture = function( sizeXTexture, sizeYTexture ) {
+
+ sizeXTexture = sizeXTexture || sizeX;
+ sizeYTexture = sizeYTexture || sizeY;
+
+ var a = new Float32Array( sizeXTexture * sizeYTexture * 4 );
+ var texture = new THREE.DataTexture( a, sizeX, sizeY, THREE.RGBAFormat, THREE.FloatType );
+ texture.needsUpdate = true;
+
+ return texture;
+
+ };
+
+
+ this.renderTexture = function( input, output ) {
+
+ // Takes a texture, and render out in rendertarget
+ // input = Texture
+ // output = RenderTarget
+
+ passThruUniforms.texture.value = input;
+
+ this.doRenderTarget( passThruShader, output);
+
+ passThruUniforms.texture.value = null;
+
+ };
+
+ this.doRenderTarget = function( material, output ) {
+
+ mesh.material = material;
+ renderer.render( scene, camera, output );
+ mesh.material = passThruShader;
+
+ };
+
+ // Shaders
+
+ function getPassThroughVertexShader() {
+
+ return "void main() {\n" +
+ "\n" +
+ " gl_Position = vec4( position, 1.0 );\n" +
+ "\n" +
+ "}\n";
+
+ }
+
+ function getPassThroughFragmentShader() {
+
+ return "uniform sampler2D texture;\n" +
+ "\n" +
+ "void main() {\n" +
+ "\n" +
+ " vec2 uv = gl_FragCoord.xy / resolution.xy;\n" +
+ "\n" +
+ " gl_FragColor = texture2D( texture, uv );\n" +
+ "\n" +
+ "}\n";
+
+ }
+
+}
diff --git a/app/static/js/GPUParticleSystem.js b/app/static/js/GPUParticleSystem.js
new file mode 100644
index 0000000..535f085
--- /dev/null
+++ b/app/static/js/GPUParticleSystem.js
@@ -0,0 +1,501 @@
+/*
+ * GPU Particle System
+ * @author flimshaw - Charlie Hoey - http://charliehoey.com
+ *
+ * A simple to use, general purpose GPU system. Particles are spawn-and-forget with
+ * several options available, and do not require monitoring or cleanup after spawning.
+ * Because the paths of all particles are completely deterministic once spawned, the scale
+ * and direction of time is also variable.
+ *
+ * Currently uses a static wrapping perlin noise texture for turbulence, and a small png texture for
+ * particles, but adding support for a particle texture atlas or changing to a different type of turbulence
+ * would be a fairly light day's work.
+ *
+ * Shader and javascript packing code derrived from several Stack Overflow examples.
+ *
+ */
+
+THREE.GPUParticleSystem = function( options ) {
+
+ THREE.Object3D.apply( this, arguments );
+
+ options = options || {};
+
+ // parse options and use defaults
+
+ this.PARTICLE_COUNT = options.maxParticles || 1000000;
+ this.PARTICLE_CONTAINERS = options.containerCount || 1;
+
+ this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null;
+ this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null;
+
+ this.PARTICLES_PER_CONTAINER = Math.ceil( this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS );
+ this.PARTICLE_CURSOR = 0;
+ this.time = 0;
+ this.particleContainers = [];
+ this.rand = [];
+
+ // custom vertex and fragement shader
+
+ var GPUParticleShader = {
+
+ vertexShader: [
+
+ 'uniform float uTime;',
+ 'uniform float uScale;',
+ 'uniform sampler2D tNoise;',
+
+ 'attribute vec3 positionStart;',
+ 'attribute float startTime;',
+ 'attribute vec3 velocity;',
+ 'attribute float turbulence;',
+ 'attribute vec3 color;',
+ 'attribute float size;',
+ 'attribute float lifeTime;',
+
+ 'varying vec4 vColor;',
+ 'varying float lifeLeft;',
+
+ 'void main() {',
+
+ // unpack things from our attributes'
+
+ ' vColor = vec4( color, 1.0 );',
+
+ // convert our velocity back into a value we can use'
+
+ ' vec3 newPosition;',
+ ' vec3 v;',
+
+ ' float timeElapsed = uTime - startTime;',
+
+ ' lifeLeft = 1.0 - ( timeElapsed / lifeTime );',
+
+ ' gl_PointSize = ( uScale * size ) * lifeLeft;',
+
+ ' v.x = ( velocity.x - 0.5 ) * 3.0;',
+ ' v.y = ( velocity.y - 0.5 ) * 3.0;',
+ ' v.z = ( velocity.z - 0.5 ) * 3.0;',
+
+ ' newPosition = positionStart + ( v * 10.0 ) * timeElapsed;',
+
+ ' vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb;',
+ ' vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0;',
+
+ ' newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / lifeTime ) );',
+
+ ' if( v.y > 0. && v.y < .05 ) {',
+
+ ' lifeLeft = 0.0;',
+
+ ' }',
+
+ ' if( v.x < - 1.45 ) {',
+
+ ' lifeLeft = 0.0;',
+
+ ' }',
+
+ ' if( timeElapsed > 0.0 ) {',
+
+ ' gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );',
+
+ ' } else {',
+
+ ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+ ' lifeLeft = 0.0;',
+ ' gl_PointSize = 0.;',
+
+ ' }',
+
+ '}'
+
+ ].join( '\n' ),
+
+ fragmentShader: [
+
+ 'float scaleLinear( float value, vec2 valueDomain ) {',
+
+ ' return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x );',
+
+ '}',
+
+ 'float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) {',
+
+ ' return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) );',
+
+ '}',
+
+ 'varying vec4 vColor;',
+ 'varying float lifeLeft;',
+
+ 'uniform sampler2D tSprite;',
+
+ 'void main() {',
+
+ ' float alpha = 0.;',
+
+ ' if( lifeLeft > 0.995 ) {',
+
+ ' alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) );',
+
+ ' } else {',
+
+ ' alpha = lifeLeft * 0.75;',
+
+ ' }',
+
+ ' vec4 tex = texture2D( tSprite, gl_PointCoord );',
+ ' gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a );',
+
+ '}'
+
+ ].join( '\n' )
+
+ };
+
+ // preload a million random numbers
+
+ var i;
+
+ for ( i = 1e5; i > 0; i-- ) {
+
+ this.rand.push( Math.random() - 0.5 );
+
+ }
+
+ this.random = function() {
+
+ return ++ i >= this.rand.length ? this.rand[ i = 1 ] : this.rand[ i ];
+
+ };
+
+ var textureLoader = new THREE.TextureLoader();
+
+ this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE || textureLoader.load( 'textures/perlin-512.png' );
+ this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = THREE.RepeatWrapping;
+
+ this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE || textureLoader.load( 'textures/particle2.png' );
+ this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = THREE.RepeatWrapping;
+
+ this.particleShaderMat = new THREE.ShaderMaterial( {
+ transparent: true,
+ depthWrite: false,
+ uniforms: {
+ 'uTime': {
+ value: 0.0
+ },
+ 'uScale': {
+ value: 1.0
+ },
+ 'tNoise': {
+ value: this.particleNoiseTex
+ },
+ 'tSprite': {
+ value: this.particleSpriteTex
+ }
+ },
+ blending: THREE.AdditiveBlending,
+ vertexShader: GPUParticleShader.vertexShader,
+ fragmentShader: GPUParticleShader.fragmentShader
+ } );
+
+ // define defaults for all values
+
+ this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ];
+ this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ];
+
+ this.init = function() {
+
+ for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
+
+ var c = new THREE.GPUParticleContainer( this.PARTICLES_PER_CONTAINER, this );
+ this.particleContainers.push( c );
+ this.add( c );
+
+ }
+
+ };
+
+ this.spawnParticle = function( options ) {
+
+ this.PARTICLE_CURSOR ++;
+
+ if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
+
+ this.PARTICLE_CURSOR = 1;
+
+ }
+
+ var currentContainer = this.particleContainers[ Math.floor( this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER ) ];
+
+ currentContainer.spawnParticle( options );
+
+ };
+
+ this.update = function( time ) {
+
+ for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
+
+ this.particleContainers[ i ].update( time );
+
+ }
+
+ };
+
+ this.dispose = function() {
+
+ this.particleShaderMat.dispose();
+ this.particleNoiseTex.dispose();
+ this.particleSpriteTex.dispose();
+
+ for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
+
+ this.particleContainers[ i ].dispose();
+
+ }
+
+ };
+
+ this.init();
+
+};
+
+THREE.GPUParticleSystem.prototype = Object.create( THREE.Object3D.prototype );
+THREE.GPUParticleSystem.prototype.constructor = THREE.GPUParticleSystem;
+
+
+// Subclass for particle containers, allows for very large arrays to be spread out
+
+THREE.GPUParticleContainer = function( maxParticles, particleSystem ) {
+
+ THREE.Object3D.apply( this, arguments );
+
+ this.PARTICLE_COUNT = maxParticles || 100000;
+ this.PARTICLE_CURSOR = 0;
+ this.time = 0;
+ this.offset = 0;
+ this.count = 0;
+ this.DPR = window.devicePixelRatio;
+ this.GPUParticleSystem = particleSystem;
+ this.particleUpdate = false;
+
+ // geometry
+
+ this.particleShaderGeo = new THREE.BufferGeometry();
+
+ this.particleShaderGeo.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'positionStart', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'startTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'velocity', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'turbulence', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'size', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
+ this.particleShaderGeo.addAttribute( 'lifeTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
+
+ // material
+
+ this.particleShaderMat = this.GPUParticleSystem.particleShaderMat;
+
+ var position = new THREE.Vector3();
+ var velocity = new THREE.Vector3();
+ var color = new THREE.Color();
+
+ this.spawnParticle = function( options ) {
+
+ var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' );
+ var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' );
+ var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' );
+ var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' );
+ var colorAttribute = this.particleShaderGeo.getAttribute( 'color' );
+ var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' );
+ var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' );
+
+ options = options || {};
+
+ // setup reasonable default values for all arguments
+
+ position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 );
+ velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 );
+ color = options.color !== undefined ? color.set( options.color ) : color.set( 0xffffff );
+
+ var positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0;
+ var velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0;
+ var colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1;
+ var turbulence = options.turbulence !== undefined ? options.turbulence : 1;
+ var lifetime = options.lifetime !== undefined ? options.lifetime : 5;
+ var size = options.size !== undefined ? options.size : 10;
+ var sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0;
+ var smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false;
+
+ if ( this.DPR !== undefined ) size *= this.DPR;
+
+ var i = this.PARTICLE_CURSOR;
+
+ // position
+
+ positionStartAttribute.array[ i * 3 + 0 ] = position.x + ( particleSystem.random() * positionRandomness );
+ positionStartAttribute.array[ i * 3 + 1 ] = position.y + ( particleSystem.random() * positionRandomness );
+ positionStartAttribute.array[ i * 3 + 2 ] = position.z + ( particleSystem.random() * positionRandomness );
+
+ if ( smoothPosition === true ) {
+
+ positionStartAttribute.array[ i * 3 + 0 ] += - ( velocity.x * particleSystem.random() );
+ positionStartAttribute.array[ i * 3 + 1 ] += - ( velocity.y * particleSystem.random() );
+ positionStartAttribute.array[ i * 3 + 2 ] += - ( velocity.z * particleSystem.random() );
+
+ }
+
+ // velocity
+
+ var maxVel = 2;
+
+ var velX = velocity.x + particleSystem.random() * velocityRandomness;
+ var velY = velocity.y + particleSystem.random() * velocityRandomness;
+ var velZ = velocity.z + particleSystem.random() * velocityRandomness;
+
+ velX = THREE.Math.clamp( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
+ velY = THREE.Math.clamp( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
+ velZ = THREE.Math.clamp( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
+
+ velocityAttribute.array[ i * 3 + 0 ] = velX;
+ velocityAttribute.array[ i * 3 + 1 ] = velY;
+ velocityAttribute.array[ i * 3 + 2 ] = velZ;
+
+ // color
+
+ color.r = THREE.Math.clamp( color.r + particleSystem.random() * colorRandomness, 0, 1 );
+ color.g = THREE.Math.clamp( color.g + particleSystem.random() * colorRandomness, 0, 1 );
+ color.b = THREE.Math.clamp( color.b + particleSystem.random() * colorRandomness, 0, 1 );
+
+ colorAttribute.array[ i * 3 + 0 ] = color.r;
+ colorAttribute.array[ i * 3 + 1 ] = color.g;
+ colorAttribute.array[ i * 3 + 2 ] = color.b;
+
+ // turbulence, size, lifetime and starttime
+
+ turbulenceAttribute.array[ i ] = turbulence;
+ sizeAttribute.array[ i ] = size + particleSystem.random() * sizeRandomness;
+ lifeTimeAttribute.array[ i ] = lifetime;
+ startTimeAttribute.array[ i ] = this.time + particleSystem.random() * 2e-2;
+
+ // offset
+
+ if ( this.offset === 0 ) {
+
+ this.offset = this.PARTICLE_CURSOR;
+
+ }
+
+ // counter and cursor
+
+ this.count ++;
+ this.PARTICLE_CURSOR ++;
+
+ if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
+
+ this.PARTICLE_CURSOR = 0;
+
+ }
+
+ this.particleUpdate = true;
+
+ };
+
+ this.init = function() {
+
+ this.particleSystem = new THREE.Points( this.particleShaderGeo, this.particleShaderMat );
+ this.particleSystem.frustumCulled = false;
+ this.add( this.particleSystem );
+
+ };
+
+ this.update = function( time ) {
+
+ this.time = time;
+ this.particleShaderMat.uniforms.uTime.value = time;
+
+ this.geometryUpdate();
+
+ };
+
+ this.geometryUpdate = function() {
+
+ if ( this.particleUpdate === true ) {
+
+ this.particleUpdate = false;
+
+ var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' );
+ var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' );
+ var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' );
+ var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' );
+ var colorAttribute = this.particleShaderGeo.getAttribute( 'color' );
+ var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' );
+ var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' );
+
+ if ( this.offset + this.count < this.PARTICLE_COUNT ) {
+
+ positionStartAttribute.updateRange.offset = this.offset * positionStartAttribute.itemSize;
+ startTimeAttribute.updateRange.offset = this.offset * startTimeAttribute.itemSize;
+ velocityAttribute.updateRange.offset = this.offset * velocityAttribute.itemSize;
+ turbulenceAttribute.updateRange.offset = this.offset * turbulenceAttribute.itemSize;
+ colorAttribute.updateRange.offset = this.offset * colorAttribute.itemSize;
+ sizeAttribute.updateRange.offset = this.offset * sizeAttribute.itemSize;
+ lifeTimeAttribute.updateRange.offset = this.offset * lifeTimeAttribute.itemSize;
+
+ positionStartAttribute.updateRange.count = this.count * positionStartAttribute.itemSize;
+ startTimeAttribute.updateRange.count = this.count * startTimeAttribute.itemSize;
+ velocityAttribute.updateRange.count = this.count * velocityAttribute.itemSize;
+ turbulenceAttribute.updateRange.count = this.count * turbulenceAttribute.itemSize;
+ colorAttribute.updateRange.count = this.count * colorAttribute.itemSize;
+ sizeAttribute.updateRange.count = this.count * sizeAttribute.itemSize;
+ lifeTimeAttribute.updateRange.count = this.count * lifeTimeAttribute.itemSize;
+
+ } else {
+
+ positionStartAttribute.updateRange.offset = 0;
+ startTimeAttribute.updateRange.offset = 0;
+ velocityAttribute.updateRange.offset = 0;
+ turbulenceAttribute.updateRange.offset = 0;
+ colorAttribute.updateRange.offset = 0;
+ sizeAttribute.updateRange.offset = 0;
+ lifeTimeAttribute.updateRange.offset = 0;
+
+ // Use -1 to update the entire buffer, see #11476
+ positionStartAttribute.updateRange.count = -1;
+ startTimeAttribute.updateRange.count = -1;
+ velocityAttribute.updateRange.count = -1;
+ turbulenceAttribute.updateRange.count = -1;
+ colorAttribute.updateRange.count = -1;
+ sizeAttribute.updateRange.count = -1;
+ lifeTimeAttribute.updateRange.count = -1;
+
+ }
+
+ positionStartAttribute.needsUpdate = true;
+ startTimeAttribute.needsUpdate = true;
+ velocityAttribute.needsUpdate = true;
+ turbulenceAttribute.needsUpdate = true;
+ colorAttribute.needsUpdate = true;
+ sizeAttribute.needsUpdate = true;
+ lifeTimeAttribute.needsUpdate = true;
+
+ this.offset = 0;
+ this.count = 0;
+
+ }
+
+ };
+
+ this.dispose = function() {
+
+ this.particleShaderGeo.dispose();
+
+ };
+
+ this.init();
+
+};
+
+THREE.GPUParticleContainer.prototype = Object.create( THREE.Object3D.prototype );
+THREE.GPUParticleContainer.prototype.constructor = THREE.GPUParticleContainer;
diff --git a/app/static/js/Gyroscope.js b/app/static/js/Gyroscope.js
new file mode 100644
index 0000000..a235a51
--- /dev/null
+++ b/app/static/js/Gyroscope.js
@@ -0,0 +1,65 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.Gyroscope = function () {
+
+ THREE.Object3D.call( this );
+
+};
+
+THREE.Gyroscope.prototype = Object.create( THREE.Object3D.prototype );
+THREE.Gyroscope.prototype.constructor = THREE.Gyroscope;
+
+THREE.Gyroscope.prototype.updateMatrixWorld = ( function () {
+
+ var translationObject = new THREE.Vector3();
+ var quaternionObject = new THREE.Quaternion();
+ var scaleObject = new THREE.Vector3();
+
+ var translationWorld = new THREE.Vector3();
+ var quaternionWorld = new THREE.Quaternion();
+ var scaleWorld = new THREE.Vector3();
+
+ return function updateMatrixWorld( force ) {
+
+ this.matrixAutoUpdate && this.updateMatrix();
+
+ // update matrixWorld
+
+ if ( this.matrixWorldNeedsUpdate || force ) {
+
+ if ( this.parent !== null ) {
+
+ this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
+
+ this.matrixWorld.decompose( translationWorld, quaternionWorld, scaleWorld );
+ this.matrix.decompose( translationObject, quaternionObject, scaleObject );
+
+ this.matrixWorld.compose( translationWorld, quaternionObject, scaleWorld );
+
+
+ } else {
+
+ this.matrixWorld.copy( this.matrix );
+
+ }
+
+
+ this.matrixWorldNeedsUpdate = false;
+
+ force = true;
+
+ }
+
+ // update children
+
+ for ( var i = 0, l = this.children.length; i < l; i ++ ) {
+
+ this.children[ i ].updateMatrixWorld( force );
+
+ }
+
+ };
+
+}() );
diff --git a/app/static/js/History.js b/app/static/js/History.js
new file mode 100644
index 0000000..d583044
--- /dev/null
+++ b/app/static/js/History.js
@@ -0,0 +1,323 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+History = function ( editor ) {
+
+ this.editor = editor;
+ this.undos = [];
+ this.redos = [];
+ this.lastCmdTime = new Date();
+ this.idCounter = 0;
+
+ this.historyDisabled = false;
+ this.config = editor.config;
+
+ //Set editor-reference in Command
+
+ Command( editor );
+
+ // signals
+
+ var scope = this;
+
+ this.editor.signals.startPlayer.add( function () {
+
+ scope.historyDisabled = true;
+
+ } );
+
+ this.editor.signals.stopPlayer.add( function () {
+
+ scope.historyDisabled = false;
+
+ } );
+
+};
+
+History.prototype = {
+
+ execute: function ( cmd, optionalName ) {
+
+ var lastCmd = this.undos[ this.undos.length - 1 ];
+ var timeDifference = new Date().getTime() - this.lastCmdTime.getTime();
+
+ var isUpdatableCmd = lastCmd &&
+ lastCmd.updatable &&
+ cmd.updatable &&
+ lastCmd.object === cmd.object &&
+ lastCmd.type === cmd.type &&
+ lastCmd.script === cmd.script &&
+ lastCmd.attributeName === cmd.attributeName;
+
+ if ( isUpdatableCmd && cmd.type === "SetScriptValueCommand" ) {
+
+ // When the cmd.type is "SetScriptValueCommand" the timeDifference is ignored
+
+ lastCmd.update( cmd );
+ cmd = lastCmd;
+
+ } else if ( isUpdatableCmd && timeDifference < 500 ) {
+
+ lastCmd.update( cmd );
+ cmd = lastCmd;
+
+ } else {
+
+ // the command is not updatable and is added as a new part of the history
+
+ this.undos.push( cmd );
+ cmd.id = ++ this.idCounter;
+
+ }
+ cmd.name = ( optionalName !== undefined ) ? optionalName : cmd.name;
+ cmd.execute();
+ cmd.inMemory = true;
+
+ if ( this.config.getKey( 'settings/history' ) ) {
+
+ cmd.json = cmd.toJSON(); // serialize the cmd immediately after execution and append the json to the cmd
+
+ }
+ this.lastCmdTime = new Date();
+
+ // clearing all the redo-commands
+
+ this.redos = [];
+ this.editor.signals.historyChanged.dispatch( cmd );
+
+ },
+
+ undo: function () {
+
+ if ( this.historyDisabled ) {
+
+ alert( "Undo/Redo disabled while scene is playing." );
+ return;
+
+ }
+
+ var cmd = undefined;
+
+ if ( this.undos.length > 0 ) {
+
+ cmd = this.undos.pop();
+
+ if ( cmd.inMemory === false ) {
+
+ cmd.fromJSON( cmd.json );
+
+ }
+
+ }
+
+ if ( cmd !== undefined ) {
+
+ cmd.undo();
+ this.redos.push( cmd );
+ this.editor.signals.historyChanged.dispatch( cmd );
+
+ }
+
+ return cmd;
+
+ },
+
+ redo: function () {
+
+ if ( this.historyDisabled ) {
+
+ alert( "Undo/Redo disabled while scene is playing." );
+ return;
+
+ }
+
+ var cmd = undefined;
+
+ if ( this.redos.length > 0 ) {
+
+ cmd = this.redos.pop();
+
+ if ( cmd.inMemory === false ) {
+
+ cmd.fromJSON( cmd.json );
+
+ }
+
+ }
+
+ if ( cmd !== undefined ) {
+
+ cmd.execute();
+ this.undos.push( cmd );
+ this.editor.signals.historyChanged.dispatch( cmd );
+
+ }
+
+ return cmd;
+
+ },
+
+ toJSON: function () {
+
+ var history = {};
+ history.undos = [];
+ history.redos = [];
+
+ if ( ! this.config.getKey( 'settings/history' ) ) {
+
+ return history;
+
+ }
+
+ // Append Undos to History
+
+ for ( var i = 0 ; i < this.undos.length; i ++ ) {
+
+ if ( this.undos[ i ].hasOwnProperty( "json" ) ) {
+
+ history.undos.push( this.undos[ i ].json );
+
+ }
+
+ }
+
+ // Append Redos to History
+
+ for ( var i = 0 ; i < this.redos.length; i ++ ) {
+
+ if ( this.redos[ i ].hasOwnProperty( "json" ) ) {
+
+ history.redos.push( this.redos[ i ].json );
+
+ }
+
+ }
+
+ return history;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ if ( json === undefined ) return;
+
+ for ( var i = 0; i < json.undos.length; i ++ ) {
+
+ var cmdJSON = json.undos[ i ];
+ var cmd = new window[ cmdJSON.type ](); // creates a new object of type "json.type"
+ cmd.json = cmdJSON;
+ cmd.id = cmdJSON.id;
+ cmd.name = cmdJSON.name;
+ this.undos.push( cmd );
+ this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+ }
+
+ for ( var i = 0; i < json.redos.length; i ++ ) {
+
+ var cmdJSON = json.redos[ i ];
+ var cmd = new window[ cmdJSON.type ](); // creates a new object of type "json.type"
+ cmd.json = cmdJSON;
+ cmd.id = cmdJSON.id;
+ cmd.name = cmdJSON.name;
+ this.redos.push( cmd );
+ this.idCounter = ( cmdJSON.id > this.idCounter ) ? cmdJSON.id : this.idCounter; // set last used idCounter
+
+ }
+
+ // Select the last executed undo-command
+ this.editor.signals.historyChanged.dispatch( this.undos[ this.undos.length - 1 ] );
+
+ },
+
+ clear: function () {
+
+ this.undos = [];
+ this.redos = [];
+ this.idCounter = 0;
+
+ this.editor.signals.historyChanged.dispatch();
+
+ },
+
+ goToState: function ( id ) {
+
+ if ( this.historyDisabled ) {
+
+ alert( "Undo/Redo disabled while scene is playing." );
+ return;
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = false;
+ this.editor.signals.historyChanged.active = false;
+
+ var cmd = this.undos.length > 0 ? this.undos[ this.undos.length - 1 ] : undefined; // next cmd to pop
+
+ if ( cmd === undefined || id > cmd.id ) {
+
+ cmd = this.redo();
+ while ( cmd !== undefined && id > cmd.id ) {
+
+ cmd = this.redo();
+
+ }
+
+ } else {
+
+ while ( true ) {
+
+ cmd = this.undos[ this.undos.length - 1 ]; // next cmd to pop
+
+ if ( cmd === undefined || id === cmd.id ) break;
+
+ this.undo();
+
+ }
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.historyChanged.active = true;
+
+ this.editor.signals.sceneGraphChanged.dispatch();
+ this.editor.signals.historyChanged.dispatch( cmd );
+
+ },
+
+ enableSerialization: function ( id ) {
+
+ /**
+ * because there might be commands in this.undos and this.redos
+ * which have not been serialized with .toJSON() we go back
+ * to the oldest command and redo one command after the other
+ * while also calling .toJSON() on them.
+ */
+
+ this.goToState( - 1 );
+
+ this.editor.signals.sceneGraphChanged.active = false;
+ this.editor.signals.historyChanged.active = false;
+
+ var cmd = this.redo();
+ while ( cmd !== undefined ) {
+
+ if ( ! cmd.hasOwnProperty( "json" ) ) {
+
+ cmd.json = cmd.toJSON();
+
+ }
+ cmd = this.redo();
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.historyChanged.active = true;
+
+ this.goToState( id );
+
+ }
+
+};
diff --git a/app/static/js/ImprovedNoise.js b/app/static/js/ImprovedNoise.js
new file mode 100644
index 0000000..35db426
--- /dev/null
+++ b/app/static/js/ImprovedNoise.js
@@ -0,0 +1,71 @@
+// http://mrl.nyu.edu/~perlin/noise/
+
+var ImprovedNoise = function () {
+
+ var p = [ 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,
+ 23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,
+ 174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,
+ 133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,
+ 89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,
+ 202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,
+ 248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,
+ 178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,
+ 14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,
+ 93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 ];
+
+ for (var i = 0; i < 256 ; i ++) {
+
+ p[256 + i] = p[i];
+
+ }
+
+ function fade(t) {
+
+ return t * t * t * (t * (t * 6 - 15) + 10);
+
+ }
+
+ function lerp(t, a, b) {
+
+ return a + t * (b - a);
+
+ }
+
+ function grad(hash, x, y, z) {
+
+ var h = hash & 15;
+ var u = h < 8 ? x : y, v = h < 4 ? y : h == 12 || h == 14 ? x : z;
+ return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
+
+ }
+
+ return {
+
+ noise: function (x, y, z) {
+
+ var floorX = Math.floor(x), floorY = Math.floor(y), floorZ = Math.floor(z);
+
+ var X = floorX & 255, Y = floorY & 255, Z = floorZ & 255;
+
+ x -= floorX;
+ y -= floorY;
+ z -= floorZ;
+
+ var xMinus1 = x - 1, yMinus1 = y - 1, zMinus1 = z - 1;
+
+ var u = fade(x), v = fade(y), w = fade(z);
+
+ var A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z, B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z;
+
+ return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z),
+ grad(p[BA], xMinus1, y, z)),
+ lerp(u, grad(p[AB], x, yMinus1, z),
+ grad(p[BB], xMinus1, yMinus1, z))),
+ lerp(v, lerp(u, grad(p[AA + 1], x, y, zMinus1),
+ grad(p[BA + 1], xMinus1, y, z - 1)),
+ lerp(u, grad(p[AB + 1], x, yMinus1, zMinus1),
+ grad(p[BB + 1], xMinus1, yMinus1, zMinus1))));
+
+ }
+ }
+};
diff --git a/app/static/js/Loader.js b/app/static/js/Loader.js
new file mode 100644
index 0000000..326bdca
--- /dev/null
+++ b/app/static/js/Loader.js
@@ -0,0 +1,579 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Loader = function ( editor ) {
+
+ var scope = this;
+ var signals = editor.signals;
+
+ this.texturePath = '';
+
+ this.loadFile = function ( file ) {
+
+ var filename = file.name;
+ var extension = filename.split( '.' ).pop().toLowerCase();
+
+ var reader = new FileReader();
+ reader.addEventListener( 'progress', function ( event ) {
+
+ var size = '(' + Math.floor( event.total / 1000 ).format() + ' KB)';
+ var progress = Math.floor( ( event.loaded / event.total ) * 100 ) + '%';
+ console.log( 'Loading', filename, size, progress );
+
+ } );
+
+ switch ( extension ) {
+
+ case '3ds':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var loader = new THREE.TDSLoader();
+ var object = loader.parse( event.target.result );
+
+ editor.execute( new AddObjectCommand( object ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'amf':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var loader = new THREE.AMFLoader();
+ var amfobject = loader.parse( event.target.result );
+
+ editor.execute( new AddObjectCommand( amfobject ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'awd':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var loader = new THREE.AWDLoader();
+ var scene = loader.parse( event.target.result );
+
+ editor.execute( new SetSceneCommand( scene ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'babylon':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+ var json = JSON.parse( contents );
+
+ var loader = new THREE.BabylonLoader();
+ var scene = loader.parse( json );
+
+ editor.execute( new SetSceneCommand( scene ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'babylonmeshdata':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+ var json = JSON.parse( contents );
+
+ var loader = new THREE.BabylonLoader();
+
+ var geometry = loader.parseGeometry( json );
+ var material = new THREE.MeshStandardMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'ctm':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var data = new Uint8Array( event.target.result );
+
+ var stream = new CTM.Stream( data );
+ stream.offset = 0;
+
+ var loader = new THREE.CTMLoader();
+ loader.createModel( new CTM.File( stream ), function( geometry ) {
+
+ geometry.sourceType = "ctm";
+ geometry.sourceFile = file.name;
+
+ var material = new THREE.MeshStandardMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ } );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'dae':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var loader = new THREE.ColladaLoader();
+ var collada = loader.parse( contents );
+
+ collada.scene.name = filename;
+
+ editor.execute( new AddObjectCommand( collada.scene ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'fbx':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var loader = new THREE.FBXLoader();
+ var object = loader.parse( contents );
+
+ editor.execute( new AddObjectCommand( object ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'glb':
+ case 'gltf':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var loader = new THREE.GLTFLoader();
+ loader.parse( contents, '', function ( result ) {
+
+ result.scene.name = filename;
+ editor.execute( new AddObjectCommand( result.scene ) );
+
+ } );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'js':
+ case 'json':
+
+ case '3geo':
+ case '3mat':
+ case '3obj':
+ case '3scn':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ // 2.0
+
+ if ( contents.indexOf( 'postMessage' ) !== - 1 ) {
+
+ var blob = new Blob( [ contents ], { type: 'text/javascript' } );
+ var url = URL.createObjectURL( blob );
+
+ var worker = new Worker( url );
+
+ worker.onmessage = function ( event ) {
+
+ event.data.metadata = { version: 2 };
+ handleJSON( event.data, file, filename );
+
+ };
+
+ worker.postMessage( Date.now() );
+
+ return;
+
+ }
+
+ // >= 3.0
+
+ var data;
+
+ try {
+
+ data = JSON.parse( contents );
+
+ } catch ( error ) {
+
+ alert( error );
+ return;
+
+ }
+
+ handleJSON( data, file, filename );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+
+ case 'kmz':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var loader = new THREE.KMZLoader();
+ var collada = loader.parse( event.target.result );
+
+ collada.scene.name = filename;
+
+ editor.execute( new AddObjectCommand( collada.scene ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'md2':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var geometry = new THREE.MD2Loader().parse( contents );
+ var material = new THREE.MeshStandardMaterial( {
+ morphTargets: true,
+ morphNormals: true
+ } );
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.mixer = new THREE.AnimationMixer( mesh );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'obj':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var object = new THREE.OBJLoader().parse( contents );
+ object.name = filename;
+
+ editor.execute( new AddObjectCommand( object ) );
+ editor.setModel( object ); // set import as base model
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'playcanvas':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+ var json = JSON.parse( contents );
+
+ var loader = new THREE.PlayCanvasLoader();
+ var object = loader.parse( json );
+
+ editor.execute( new AddObjectCommand( object ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'ply':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var geometry = new THREE.PLYLoader().parse( contents );
+ geometry.sourceType = "ply";
+ geometry.sourceFile = file.name;
+
+ var material = new THREE.MeshStandardMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+ reader.readAsArrayBuffer( file );
+
+ break;
+
+ case 'stl':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var geometry = new THREE.STLLoader().parse( contents );
+ geometry.sourceType = "stl";
+ geometry.sourceFile = file.name;
+
+ var material = new THREE.MeshStandardMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+
+ if ( reader.readAsBinaryString !== undefined ) {
+
+ reader.readAsBinaryString( file );
+
+ } else {
+
+ reader.readAsArrayBuffer( file );
+
+ }
+
+ break;
+
+ /*
+ case 'utf8':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var geometry = new THREE.UTF8Loader().parse( contents );
+ var material = new THREE.MeshLambertMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+ reader.readAsBinaryString( file );
+
+ break;
+ */
+
+ case 'vtk':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var geometry = new THREE.VTKLoader().parse( contents );
+ geometry.sourceType = "vtk";
+ geometry.sourceFile = file.name;
+
+ var material = new THREE.MeshStandardMaterial();
+
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'wrl':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var result = new THREE.VRMLLoader().parse( contents );
+
+ editor.execute( new SetSceneCommand( result ) );
+
+ }, false );
+ reader.readAsText( file );
+
+ break;
+
+ case 'zip':
+
+ reader.addEventListener( 'load', function ( event ) {
+
+ var contents = event.target.result;
+
+ var zip = new JSZip( contents );
+
+ // BLOCKS
+
+ if ( zip.files[ 'model.obj' ] && zip.files[ 'materials.mtl' ] ) {
+
+ var materials = new THREE.MTLLoader().parse( zip.file( 'materials.mtl' ).asText() );
+ var object = new THREE.OBJLoader().setMaterials( materials ).parse( zip.file( 'model.obj' ).asText() );
+ editor.execute( new AddObjectCommand( object ) );
+
+ }
+
+ }, false );
+ reader.readAsBinaryString( file );
+
+ break;
+
+ default:
+
+ alert( 'Unsupported file format (' + extension + ').' );
+
+ break;
+
+ }
+
+ };
+
+ function handleJSON( data, file, filename ) {
+
+ if ( data.metadata === undefined ) { // 2.0
+
+ data.metadata = { type: 'Geometry' };
+
+ }
+
+ if ( data.metadata.type === undefined ) { // 3.0
+
+ data.metadata.type = 'Geometry';
+
+ }
+
+ if ( data.metadata.formatVersion !== undefined ) {
+
+ data.metadata.version = data.metadata.formatVersion;
+
+ }
+
+ switch ( data.metadata.type.toLowerCase() ) {
+
+ case 'buffergeometry':
+
+ var loader = new THREE.BufferGeometryLoader();
+ var result = loader.parse( data );
+
+ var mesh = new THREE.Mesh( result );
+
+ editor.execute( new AddObjectCommand( mesh ) );
+ editor.setModel( mesh ); // set import as base model
+
+ break;
+
+ case 'geometry':
+
+ var loader = new THREE.JSONLoader();
+ loader.setTexturePath( scope.texturePath );
+
+ var result = loader.parse( data );
+
+ var geometry = result.geometry;
+ var material;
+
+ if ( result.materials !== undefined ) {
+
+ if ( result.materials.length > 1 ) {
+
+ material = new THREE.MultiMaterial( result.materials );
+
+ } else {
+
+ material = result.materials[ 0 ];
+
+ }
+
+ } else {
+
+ material = new THREE.MeshStandardMaterial();
+
+ }
+
+ geometry.sourceType = "ascii";
+ geometry.sourceFile = file.name;
+
+ var mesh;
+
+ if ( geometry.animation && geometry.animation.hierarchy ) {
+
+ mesh = new THREE.SkinnedMesh( geometry, material );
+
+ } else {
+
+ mesh = new THREE.Mesh( geometry, material );
+
+ }
+
+ mesh.name = filename;
+
+ editor.execute( new AddObjectCommand( mesh ) );
+ editor.setModel( mesh ); // set import as base model
+
+ break;
+
+ case 'object':
+
+ var loader = new THREE.ObjectLoader();
+ loader.setTexturePath( scope.texturePath );
+
+ var result = loader.parse( data );
+
+ if ( result instanceof THREE.Scene ) {
+
+ editor.execute( new SetSceneCommand( result ) );
+
+ } else {
+
+ editor.execute( new AddObjectCommand( result ) );
+ editor.setModel( result ); // set import as base model
+
+ }
+
+ break;
+
+ case 'app':
+
+ editor.fromJSON( data );
+
+ break;
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/MD2Character.js b/app/static/js/MD2Character.js
new file mode 100644
index 0000000..b984633
--- /dev/null
+++ b/app/static/js/MD2Character.js
@@ -0,0 +1,254 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.MD2Character = function () {
+
+ var scope = this;
+
+ this.scale = 1;
+ this.animationFPS = 6;
+
+ this.root = new THREE.Object3D();
+
+ this.meshBody = null;
+ this.meshWeapon = null;
+
+ this.skinsBody = [];
+ this.skinsWeapon = [];
+
+ this.weapons = [];
+
+ this.activeAnimation = null;
+
+ this.mixer = null;
+
+ this.onLoadComplete = function () {};
+
+ this.loadCounter = 0;
+
+ this.loadParts = function ( config ) {
+
+ this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
+
+ var weaponsTextures = [];
+ for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
+ // SKINS
+
+ this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
+ this.skinsWeapon = loadTextures( config.baseUrl + "skins/", weaponsTextures );
+
+ // BODY
+
+ var loader = new THREE.MD2Loader();
+
+ loader.load( config.baseUrl + config.body, function( geo ) {
+
+ geo.computeBoundingBox();
+ scope.root.position.y = - scope.scale * geo.boundingBox.min.y;
+
+ var mesh = createPart( geo, scope.skinsBody[ 0 ] );
+ mesh.scale.set( scope.scale, scope.scale, scope.scale );
+
+ scope.root.add( mesh );
+
+ scope.meshBody = mesh;
+
+ scope.meshBody.clipOffset = 0;
+ scope.activeAnimationClipName = mesh.geometry.animations[0].name;
+
+ scope.mixer = new THREE.AnimationMixer( mesh );
+
+ checkLoadingComplete();
+
+ } );
+
+ // WEAPONS
+
+ var generateCallback = function ( index, name ) {
+
+ return function( geo ) {
+
+ var mesh = createPart( geo, scope.skinsWeapon[ index ] );
+ mesh.scale.set( scope.scale, scope.scale, scope.scale );
+ mesh.visible = false;
+
+ mesh.name = name;
+
+ scope.root.add( mesh );
+
+ scope.weapons[ index ] = mesh;
+ scope.meshWeapon = mesh;
+
+ checkLoadingComplete();
+
+ }
+
+ };
+
+ for ( var i = 0; i < config.weapons.length; i ++ ) {
+
+ loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
+
+ }
+
+ };
+
+ this.setPlaybackRate = function ( rate ) {
+
+ if( rate !== 0 ) {
+ this.mixer.timeScale = 1 / rate;
+ }
+ else {
+ this.mixer.timeScale = 0;
+ }
+
+ };
+
+ this.setWireframe = function ( wireframeEnabled ) {
+
+ if ( wireframeEnabled ) {
+
+ if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
+ if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
+
+ } else {
+
+ if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
+ if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
+
+ }
+
+ };
+
+ this.setSkin = function( index ) {
+
+ if ( this.meshBody && this.meshBody.material.wireframe === false ) {
+
+ this.meshBody.material.map = this.skinsBody[ index ];
+
+ }
+
+ };
+
+ this.setWeapon = function ( index ) {
+
+ for ( var i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
+
+ var activeWeapon = this.weapons[ index ];
+
+ if ( activeWeapon ) {
+
+ activeWeapon.visible = true;
+ this.meshWeapon = activeWeapon;
+
+ scope.syncWeaponAnimation();
+
+ }
+
+ };
+
+ this.setAnimation = function ( clipName ) {
+
+ if ( this.meshBody ) {
+
+ if( this.meshBody.activeAction ) {
+ this.meshBody.activeAction.stop();
+ this.meshBody.activeAction = null;
+ }
+
+ var action = this.mixer.clipAction( clipName, this.meshBody );
+ if( action ) {
+
+ this.meshBody.activeAction = action.play();
+
+ }
+
+ }
+
+ scope.activeClipName = clipName;
+
+ scope.syncWeaponAnimation();
+
+ };
+
+ this.syncWeaponAnimation = function() {
+
+ var clipName = scope.activeClipName;
+
+ if ( scope.meshWeapon ) {
+
+ if( this.meshWeapon.activeAction ) {
+ this.meshWeapon.activeAction.stop();
+ this.meshWeapon.activeAction = null;
+ }
+
+ var geometry = this.meshWeapon.geometry,
+ animations = geometry.animations;
+
+ var action = this.mixer.clipAction( clipName, this.meshWeapon );
+ if( action ) {
+
+ this.meshWeapon.activeAction =
+ action.syncWith( this.meshBody.activeAction ).play();
+
+ }
+
+ }
+
+ }
+
+ this.update = function ( delta ) {
+
+ if( this.mixer ) this.mixer.update( delta );
+
+ };
+
+ function loadTextures( baseUrl, textureUrls ) {
+
+ var textureLoader = new THREE.TextureLoader();
+ var textures = [];
+
+ for ( var i = 0; i < textureUrls.length; i ++ ) {
+
+ textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
+ textures[ i ].mapping = THREE.UVMapping;
+ textures[ i ].name = textureUrls[ i ];
+
+ }
+
+ return textures;
+
+ }
+
+ function createPart( geometry, skinMap ) {
+
+ var materialWireframe = new THREE.MeshLambertMaterial( { color: 0xffaa00, wireframe: true, morphTargets: true, morphNormals: true } );
+ var materialTexture = new THREE.MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap, morphTargets: true, morphNormals: true } );
+
+ //
+
+ var mesh = new THREE.Mesh( geometry, materialTexture );
+ mesh.rotation.y = - Math.PI / 2;
+
+ mesh.castShadow = true;
+ mesh.receiveShadow = true;
+
+ //
+
+ mesh.materialTexture = materialTexture;
+ mesh.materialWireframe = materialWireframe;
+
+ return mesh;
+
+ }
+
+ function checkLoadingComplete() {
+
+ scope.loadCounter -= 1;
+
+ if ( scope.loadCounter === 0 ) scope.onLoadComplete();
+
+ }
+
+};
diff --git a/app/static/js/MD2CharacterComplex.js b/app/static/js/MD2CharacterComplex.js
new file mode 100644
index 0000000..699c82b
--- /dev/null
+++ b/app/static/js/MD2CharacterComplex.js
@@ -0,0 +1,560 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.MD2CharacterComplex = function () {
+
+ var scope = this;
+
+ this.scale = 1;
+
+ // animation parameters
+
+ this.animationFPS = 6;
+ this.transitionFrames = 15;
+
+ // movement model parameters
+
+ this.maxSpeed = 275;
+ this.maxReverseSpeed = - 275;
+
+ this.frontAcceleration = 600;
+ this.backAcceleration = 600;
+
+ this.frontDecceleration = 600;
+
+ this.angularSpeed = 2.5;
+
+ // rig
+
+ this.root = new THREE.Object3D();
+
+ this.meshBody = null;
+ this.meshWeapon = null;
+
+ this.controls = null;
+
+ // skins
+
+ this.skinsBody = [];
+ this.skinsWeapon = [];
+
+ this.weapons = [];
+
+ this.currentSkin = undefined;
+
+ //
+
+ this.onLoadComplete = function () {};
+
+ // internals
+
+ this.meshes = [];
+ this.animations = {};
+
+ this.loadCounter = 0;
+
+ // internal movement control variables
+
+ this.speed = 0;
+ this.bodyOrientation = 0;
+
+ this.walkSpeed = this.maxSpeed;
+ this.crouchSpeed = this.maxSpeed * 0.5;
+
+ // internal animation parameters
+
+ this.activeAnimation = null;
+ this.oldAnimation = null;
+
+ // API
+
+ this.enableShadows = function ( enable ) {
+
+ for ( var i = 0; i < this.meshes.length; i ++ ) {
+
+ this.meshes[ i ].castShadow = enable;
+ this.meshes[ i ].receiveShadow = enable;
+
+ }
+
+ };
+
+ this.setVisible = function ( enable ) {
+
+ for ( var i = 0; i < this.meshes.length; i ++ ) {
+
+ this.meshes[ i ].visible = enable;
+ this.meshes[ i ].visible = enable;
+
+ }
+
+ };
+
+
+ this.shareParts = function ( original ) {
+
+ this.animations = original.animations;
+ this.walkSpeed = original.walkSpeed;
+ this.crouchSpeed = original.crouchSpeed;
+
+ this.skinsBody = original.skinsBody;
+ this.skinsWeapon = original.skinsWeapon;
+
+ // BODY
+
+ var mesh = createPart( original.meshBody.geometry, this.skinsBody[ 0 ] );
+ mesh.scale.set( this.scale, this.scale, this.scale );
+
+ this.root.position.y = original.root.position.y;
+ this.root.add( mesh );
+
+ this.meshBody = mesh;
+
+ this.meshes.push( mesh );
+
+ // WEAPONS
+
+ for ( var i = 0; i < original.weapons.length; i ++ ) {
+
+ var meshWeapon = createPart( original.weapons[ i ].geometry, this.skinsWeapon[ i ] );
+ meshWeapon.scale.set( this.scale, this.scale, this.scale );
+ meshWeapon.visible = false;
+
+ meshWeapon.name = original.weapons[ i ].name;
+
+ this.root.add( meshWeapon );
+
+ this.weapons[ i ] = meshWeapon;
+ this.meshWeapon = meshWeapon;
+
+ this.meshes.push( meshWeapon );
+
+ }
+
+ };
+
+ this.loadParts = function ( config ) {
+
+ this.animations = config.animations;
+ this.walkSpeed = config.walkSpeed;
+ this.crouchSpeed = config.crouchSpeed;
+
+ this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
+
+ var weaponsTextures = [];
+ for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
+
+ // SKINS
+
+ this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
+ this.skinsWeapon = loadTextures( config.baseUrl + "skins/", weaponsTextures );
+
+ // BODY
+
+ var loader = new THREE.MD2Loader();
+
+ loader.load( config.baseUrl + config.body, function( geo ) {
+
+ geo.computeBoundingBox();
+ scope.root.position.y = - scope.scale * geo.boundingBox.min.y;
+
+ var mesh = createPart( geo, scope.skinsBody[ 0 ] );
+ mesh.scale.set( scope.scale, scope.scale, scope.scale );
+
+ scope.root.add( mesh );
+
+ scope.meshBody = mesh;
+ scope.meshes.push( mesh );
+
+ checkLoadingComplete();
+
+ } );
+
+ // WEAPONS
+
+ var generateCallback = function ( index, name ) {
+
+ return function( geo ) {
+
+ var mesh = createPart( geo, scope.skinsWeapon[ index ] );
+ mesh.scale.set( scope.scale, scope.scale, scope.scale );
+ mesh.visible = false;
+
+ mesh.name = name;
+
+ scope.root.add( mesh );
+
+ scope.weapons[ index ] = mesh;
+ scope.meshWeapon = mesh;
+ scope.meshes.push( mesh );
+
+ checkLoadingComplete();
+
+ }
+
+ };
+
+ for ( var i = 0; i < config.weapons.length; i ++ ) {
+
+ loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
+
+ }
+
+ };
+
+ this.setPlaybackRate = function ( rate ) {
+
+ if ( this.meshBody ) this.meshBody.duration = this.meshBody.baseDuration / rate;
+ if ( this.meshWeapon ) this.meshWeapon.duration = this.meshWeapon.baseDuration / rate;
+
+ };
+
+ this.setWireframe = function ( wireframeEnabled ) {
+
+ if ( wireframeEnabled ) {
+
+ if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
+ if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
+
+ } else {
+
+ if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
+ if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
+
+ }
+
+ };
+
+ this.setSkin = function( index ) {
+
+ if ( this.meshBody && this.meshBody.material.wireframe === false ) {
+
+ this.meshBody.material.map = this.skinsBody[ index ];
+ this.currentSkin = index;
+
+ }
+
+ };
+
+ this.setWeapon = function ( index ) {
+
+ for ( var i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
+
+ var activeWeapon = this.weapons[ index ];
+
+ if ( activeWeapon ) {
+
+ activeWeapon.visible = true;
+ this.meshWeapon = activeWeapon;
+
+ if ( this.activeAnimation ) {
+
+ activeWeapon.playAnimation( this.activeAnimation );
+ this.meshWeapon.setAnimationTime( this.activeAnimation, this.meshBody.getAnimationTime( this.activeAnimation ) );
+
+ }
+
+ }
+
+ };
+
+ this.setAnimation = function ( animationName ) {
+
+ if ( animationName === this.activeAnimation || ! animationName ) return;
+
+ if ( this.meshBody ) {
+
+ this.meshBody.setAnimationWeight( animationName, 0 );
+ this.meshBody.playAnimation( animationName );
+
+ this.oldAnimation = this.activeAnimation;
+ this.activeAnimation = animationName;
+
+ this.blendCounter = this.transitionFrames;
+
+ }
+
+ if ( this.meshWeapon ) {
+
+ this.meshWeapon.setAnimationWeight( animationName, 0 );
+ this.meshWeapon.playAnimation( animationName );
+
+ }
+
+
+ };
+
+ this.update = function ( delta ) {
+
+ if ( this.controls ) this.updateMovementModel( delta );
+
+ if ( this.animations ) {
+
+ this.updateBehaviors( delta );
+ this.updateAnimations( delta );
+
+ }
+
+ };
+
+ this.updateAnimations = function ( delta ) {
+
+ var mix = 1;
+
+ if ( this.blendCounter > 0 ) {
+
+ mix = ( this.transitionFrames - this.blendCounter ) / this.transitionFrames;
+ this.blendCounter -= 1;
+
+ }
+
+ if ( this.meshBody ) {
+
+ this.meshBody.update( delta );
+
+ this.meshBody.setAnimationWeight( this.activeAnimation, mix );
+ this.meshBody.setAnimationWeight( this.oldAnimation, 1 - mix );
+
+ }
+
+ if ( this.meshWeapon ) {
+
+ this.meshWeapon.update( delta );
+
+ this.meshWeapon.setAnimationWeight( this.activeAnimation, mix );
+ this.meshWeapon.setAnimationWeight( this.oldAnimation, 1 - mix );
+
+ }
+
+ };
+
+ this.updateBehaviors = function ( delta ) {
+
+ var controls = this.controls;
+ var animations = this.animations;
+
+ var moveAnimation, idleAnimation;
+
+ // crouch vs stand
+
+ if ( controls.crouch ) {
+
+ moveAnimation = animations[ "crouchMove" ];
+ idleAnimation = animations[ "crouchIdle" ];
+
+ } else {
+
+ moveAnimation = animations[ "move" ];
+ idleAnimation = animations[ "idle" ];
+
+ }
+
+ // actions
+
+ if ( controls.jump ) {
+
+ moveAnimation = animations[ "jump" ];
+ idleAnimation = animations[ "jump" ];
+
+ }
+
+ if ( controls.attack ) {
+
+ if ( controls.crouch ) {
+
+ moveAnimation = animations[ "crouchAttack" ];
+ idleAnimation = animations[ "crouchAttack" ];
+
+ } else {
+
+ moveAnimation = animations[ "attack" ];
+ idleAnimation = animations[ "attack" ];
+
+ }
+
+ }
+
+ // set animations
+
+ if ( controls.moveForward || controls.moveBackward || controls.moveLeft || controls.moveRight ) {
+
+ if ( this.activeAnimation !== moveAnimation ) {
+
+ this.setAnimation( moveAnimation );
+
+ }
+
+ }
+
+
+ if ( Math.abs( this.speed ) < 0.2 * this.maxSpeed && ! ( controls.moveLeft || controls.moveRight || controls.moveForward || controls.moveBackward ) ) {
+
+ if ( this.activeAnimation !== idleAnimation ) {
+
+ this.setAnimation( idleAnimation );
+
+ }
+
+ }
+
+ // set animation direction
+
+ if ( controls.moveForward ) {
+
+ if ( this.meshBody ) {
+
+ this.meshBody.setAnimationDirectionForward( this.activeAnimation );
+ this.meshBody.setAnimationDirectionForward( this.oldAnimation );
+
+ }
+
+ if ( this.meshWeapon ) {
+
+ this.meshWeapon.setAnimationDirectionForward( this.activeAnimation );
+ this.meshWeapon.setAnimationDirectionForward( this.oldAnimation );
+
+ }
+
+ }
+
+ if ( controls.moveBackward ) {
+
+ if ( this.meshBody ) {
+
+ this.meshBody.setAnimationDirectionBackward( this.activeAnimation );
+ this.meshBody.setAnimationDirectionBackward( this.oldAnimation );
+
+ }
+
+ if ( this.meshWeapon ) {
+
+ this.meshWeapon.setAnimationDirectionBackward( this.activeAnimation );
+ this.meshWeapon.setAnimationDirectionBackward( this.oldAnimation );
+
+ }
+
+ }
+
+ };
+
+ this.updateMovementModel = function ( delta ) {
+
+ var controls = this.controls;
+
+ // speed based on controls
+
+ if ( controls.crouch ) this.maxSpeed = this.crouchSpeed;
+ else this.maxSpeed = this.walkSpeed;
+
+ this.maxReverseSpeed = - this.maxSpeed;
+
+ if ( controls.moveForward ) this.speed = THREE.Math.clamp( this.speed + delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
+ if ( controls.moveBackward ) this.speed = THREE.Math.clamp( this.speed - delta * this.backAcceleration, this.maxReverseSpeed, this.maxSpeed );
+
+ // orientation based on controls
+ // (don't just stand while turning)
+
+ var dir = 1;
+
+ if ( controls.moveLeft ) {
+
+ this.bodyOrientation += delta * this.angularSpeed;
+ this.speed = THREE.Math.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
+
+ }
+
+ if ( controls.moveRight ) {
+
+ this.bodyOrientation -= delta * this.angularSpeed;
+ this.speed = THREE.Math.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
+
+ }
+
+ // speed decay
+
+ if ( ! ( controls.moveForward || controls.moveBackward ) ) {
+
+ if ( this.speed > 0 ) {
+
+ var k = exponentialEaseOut( this.speed / this.maxSpeed );
+ this.speed = THREE.Math.clamp( this.speed - k * delta * this.frontDecceleration, 0, this.maxSpeed );
+
+ } else {
+
+ var k = exponentialEaseOut( this.speed / this.maxReverseSpeed );
+ this.speed = THREE.Math.clamp( this.speed + k * delta * this.backAcceleration, this.maxReverseSpeed, 0 );
+
+ }
+
+ }
+
+ // displacement
+
+ var forwardDelta = this.speed * delta;
+
+ this.root.position.x += Math.sin( this.bodyOrientation ) * forwardDelta;
+ this.root.position.z += Math.cos( this.bodyOrientation ) * forwardDelta;
+
+ // steering
+
+ this.root.rotation.y = this.bodyOrientation;
+
+ };
+
+ // internal helpers
+
+ function loadTextures( baseUrl, textureUrls ) {
+
+ var textureLoader = new THREE.TextureLoader();
+ var textures = [];
+
+ for ( var i = 0; i < textureUrls.length; i ++ ) {
+
+ textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
+ textures[ i ].mapping = THREE.UVMapping;
+ textures[ i ].name = textureUrls[ i ];
+
+ }
+
+ return textures;
+
+ }
+
+ function createPart( geometry, skinMap ) {
+
+ var materialWireframe = new THREE.MeshLambertMaterial( { color: 0xffaa00, wireframe: true, morphTargets: true, morphNormals: true } );
+ var materialTexture = new THREE.MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap, morphTargets: true, morphNormals: true } );
+
+ //
+
+ var mesh = new THREE.MorphBlendMesh( geometry, materialTexture );
+ mesh.rotation.y = - Math.PI / 2;
+
+ //
+
+ mesh.materialTexture = materialTexture;
+ mesh.materialWireframe = materialWireframe;
+
+ //
+
+ mesh.autoCreateAnimations( scope.animationFPS );
+
+ return mesh;
+
+ }
+
+ function checkLoadingComplete() {
+
+ scope.loadCounter -= 1;
+ if ( scope.loadCounter === 0 ) scope.onLoadComplete();
+
+ }
+
+ function exponentialEaseOut( k ) {
+
+ return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1;
+
+ }
+
+};
diff --git a/app/static/js/MarchingCubes.js b/app/static/js/MarchingCubes.js
new file mode 100644
index 0000000..32bf013
--- /dev/null
+++ b/app/static/js/MarchingCubes.js
@@ -0,0 +1,1044 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ * @author mrdoob / http://mrdoob.com
+ * Port of http://webglsamples.org/blob/blob.html
+ */
+
+THREE.MarchingCubes = function ( resolution, material, enableUvs, enableColors ) {
+
+ THREE.ImmediateRenderObject.call( this, material );
+
+ var scope = this;
+
+ // temp buffers used in polygonize
+
+ var vlist = new Float32Array( 12 * 3 );
+ var nlist = new Float32Array( 12 * 3 );
+
+ this.enableUvs = enableUvs !== undefined ? enableUvs : false;
+ this.enableColors = enableColors !== undefined ? enableColors : false;
+
+ // functions have to be object properties
+ // prototype functions kill performance
+ // (tested and it was 4x slower !!!)
+
+ this.init = function ( resolution ) {
+
+ this.resolution = resolution;
+
+ // parameters
+
+ this.isolation = 80.0;
+
+ // size of field, 32 is pushing it in Javascript :)
+
+ this.size = resolution;
+ this.size2 = this.size * this.size;
+ this.size3 = this.size2 * this.size;
+ this.halfsize = this.size / 2.0;
+
+ // deltas
+
+ this.delta = 2.0 / this.size;
+ this.yd = this.size;
+ this.zd = this.size2;
+
+ this.field = new Float32Array( this.size3 );
+ this.normal_cache = new Float32Array( this.size3 * 3 );
+
+ // immediate render mode simulator
+
+ this.maxCount = 4096; // TODO: find the fastest size for this buffer
+ this.count = 0;
+
+ this.hasPositions = false;
+ this.hasNormals = false;
+ this.hasColors = false;
+ this.hasUvs = false;
+
+ this.positionArray = new Float32Array( this.maxCount * 3 );
+ this.normalArray = new Float32Array( this.maxCount * 3 );
+
+ if ( this.enableUvs ) {
+
+ this.uvArray = new Float32Array( this.maxCount * 2 );
+
+ }
+
+ if ( this.enableColors ) {
+
+ this.colorArray = new Float32Array( this.maxCount * 3 );
+
+ }
+
+ };
+
+ ///////////////////////
+ // Polygonization
+ ///////////////////////
+
+ function lerp( a, b, t ) {
+
+ return a + ( b - a ) * t;
+
+ }
+
+ function VIntX( q, offset, isol, x, y, z, valp1, valp2 ) {
+
+ var mu = ( isol - valp1 ) / ( valp2 - valp1 ),
+ nc = scope.normal_cache;
+
+ vlist[ offset + 0 ] = x + mu * scope.delta;
+ vlist[ offset + 1 ] = y;
+ vlist[ offset + 2 ] = z;
+
+ nlist[ offset + 0 ] = lerp( nc[ q + 0 ], nc[ q + 3 ], mu );
+ nlist[ offset + 1 ] = lerp( nc[ q + 1 ], nc[ q + 4 ], mu );
+ nlist[ offset + 2 ] = lerp( nc[ q + 2 ], nc[ q + 5 ], mu );
+
+ }
+
+ function VIntY( q, offset, isol, x, y, z, valp1, valp2 ) {
+
+ var mu = ( isol - valp1 ) / ( valp2 - valp1 ),
+ nc = scope.normal_cache;
+
+ vlist[ offset + 0 ] = x;
+ vlist[ offset + 1 ] = y + mu * scope.delta;
+ vlist[ offset + 2 ] = z;
+
+ var q2 = q + scope.yd * 3;
+
+ nlist[ offset + 0 ] = lerp( nc[ q + 0 ], nc[ q2 + 0 ], mu );
+ nlist[ offset + 1 ] = lerp( nc[ q + 1 ], nc[ q2 + 1 ], mu );
+ nlist[ offset + 2 ] = lerp( nc[ q + 2 ], nc[ q2 + 2 ], mu );
+
+ }
+
+ function VIntZ( q, offset, isol, x, y, z, valp1, valp2 ) {
+
+ var mu = ( isol - valp1 ) / ( valp2 - valp1 ),
+ nc = scope.normal_cache;
+
+ vlist[ offset + 0 ] = x;
+ vlist[ offset + 1 ] = y;
+ vlist[ offset + 2 ] = z + mu * scope.delta;
+
+ var q2 = q + scope.zd * 3;
+
+ nlist[ offset + 0 ] = lerp( nc[ q + 0 ], nc[ q2 + 0 ], mu );
+ nlist[ offset + 1 ] = lerp( nc[ q + 1 ], nc[ q2 + 1 ], mu );
+ nlist[ offset + 2 ] = lerp( nc[ q + 2 ], nc[ q2 + 2 ], mu );
+
+ }
+
+ function compNorm( q ) {
+
+ var q3 = q * 3;
+
+ if ( scope.normal_cache[ q3 ] === 0.0 ) {
+
+ scope.normal_cache[ q3 + 0 ] = scope.field[ q - 1 ] - scope.field[ q + 1 ];
+ scope.normal_cache[ q3 + 1 ] = scope.field[ q - scope.yd ] - scope.field[ q + scope.yd ];
+ scope.normal_cache[ q3 + 2 ] = scope.field[ q - scope.zd ] - scope.field[ q + scope.zd ];
+
+ }
+
+ }
+
+ // Returns total number of triangles. Fills triangles.
+ // (this is where most of time is spent - it's inner work of O(n3) loop )
+
+ function polygonize( fx, fy, fz, q, isol, renderCallback ) {
+
+ // cache indices
+ var q1 = q + 1,
+ qy = q + scope.yd,
+ qz = q + scope.zd,
+ q1y = q1 + scope.yd,
+ q1z = q1 + scope.zd,
+ qyz = q + scope.yd + scope.zd,
+ q1yz = q1 + scope.yd + scope.zd;
+
+ var cubeindex = 0,
+ field0 = scope.field[ q ],
+ field1 = scope.field[ q1 ],
+ field2 = scope.field[ qy ],
+ field3 = scope.field[ q1y ],
+ field4 = scope.field[ qz ],
+ field5 = scope.field[ q1z ],
+ field6 = scope.field[ qyz ],
+ field7 = scope.field[ q1yz ];
+
+ if ( field0 < isol ) cubeindex |= 1;
+ if ( field1 < isol ) cubeindex |= 2;
+ if ( field2 < isol ) cubeindex |= 8;
+ if ( field3 < isol ) cubeindex |= 4;
+ if ( field4 < isol ) cubeindex |= 16;
+ if ( field5 < isol ) cubeindex |= 32;
+ if ( field6 < isol ) cubeindex |= 128;
+ if ( field7 < isol ) cubeindex |= 64;
+
+ // if cube is entirely in/out of the surface - bail, nothing to draw
+
+ var bits = THREE.edgeTable[ cubeindex ];
+ if ( bits === 0 ) return 0;
+
+ var d = scope.delta,
+ fx2 = fx + d,
+ fy2 = fy + d,
+ fz2 = fz + d;
+
+ // top of the cube
+
+ if ( bits & 1 ) {
+
+ compNorm( q );
+ compNorm( q1 );
+ VIntX( q * 3, 0, isol, fx, fy, fz, field0, field1 );
+
+ }
+
+ if ( bits & 2 ) {
+
+ compNorm( q1 );
+ compNorm( q1y );
+ VIntY( q1 * 3, 3, isol, fx2, fy, fz, field1, field3 );
+
+ }
+
+ if ( bits & 4 ) {
+
+ compNorm( qy );
+ compNorm( q1y );
+ VIntX( qy * 3, 6, isol, fx, fy2, fz, field2, field3 );
+
+ }
+
+ if ( bits & 8 ) {
+
+ compNorm( q );
+ compNorm( qy );
+ VIntY( q * 3, 9, isol, fx, fy, fz, field0, field2 );
+
+ }
+
+ // bottom of the cube
+
+ if ( bits & 16 ) {
+
+ compNorm( qz );
+ compNorm( q1z );
+ VIntX( qz * 3, 12, isol, fx, fy, fz2, field4, field5 );
+
+ }
+
+ if ( bits & 32 ) {
+
+ compNorm( q1z );
+ compNorm( q1yz );
+ VIntY( q1z * 3, 15, isol, fx2, fy, fz2, field5, field7 );
+
+ }
+
+ if ( bits & 64 ) {
+
+ compNorm( qyz );
+ compNorm( q1yz );
+ VIntX( qyz * 3, 18, isol, fx, fy2, fz2, field6, field7 );
+
+ }
+
+ if ( bits & 128 ) {
+
+ compNorm( qz );
+ compNorm( qyz );
+ VIntY( qz * 3, 21, isol, fx, fy, fz2, field4, field6 );
+
+ }
+
+ // vertical lines of the cube
+
+ if ( bits & 256 ) {
+
+ compNorm( q );
+ compNorm( qz );
+ VIntZ( q * 3, 24, isol, fx, fy, fz, field0, field4 );
+
+ }
+
+ if ( bits & 512 ) {
+
+ compNorm( q1 );
+ compNorm( q1z );
+ VIntZ( q1 * 3, 27, isol, fx2, fy, fz, field1, field5 );
+
+ }
+
+ if ( bits & 1024 ) {
+
+ compNorm( q1y );
+ compNorm( q1yz );
+ VIntZ( q1y * 3, 30, isol, fx2, fy2, fz, field3, field7 );
+
+ }
+
+ if ( bits & 2048 ) {
+
+ compNorm( qy );
+ compNorm( qyz );
+ VIntZ( qy * 3, 33, isol, fx, fy2, fz, field2, field6 );
+
+ }
+
+ cubeindex <<= 4; // re-purpose cubeindex into an offset into triTable
+
+ var o1, o2, o3, numtris = 0, i = 0;
+
+ // here is where triangles are created
+
+ while ( THREE.triTable[ cubeindex + i ] != - 1 ) {
+
+ o1 = cubeindex + i;
+ o2 = o1 + 1;
+ o3 = o1 + 2;
+
+ posnormtriv( vlist, nlist,
+ 3 * THREE.triTable[ o1 ],
+ 3 * THREE.triTable[ o2 ],
+ 3 * THREE.triTable[ o3 ],
+ renderCallback );
+
+ i += 3;
+ numtris ++;
+
+ }
+
+ return numtris;
+
+ }
+
+ /////////////////////////////////////
+ // Immediate render mode simulator
+ /////////////////////////////////////
+
+ function posnormtriv( pos, norm, o1, o2, o3, renderCallback ) {
+
+ var c = scope.count * 3;
+
+ // positions
+
+ scope.positionArray[ c + 0 ] = pos[ o1 ];
+ scope.positionArray[ c + 1 ] = pos[ o1 + 1 ];
+ scope.positionArray[ c + 2 ] = pos[ o1 + 2 ];
+
+ scope.positionArray[ c + 3 ] = pos[ o2 ];
+ scope.positionArray[ c + 4 ] = pos[ o2 + 1 ];
+ scope.positionArray[ c + 5 ] = pos[ o2 + 2 ];
+
+ scope.positionArray[ c + 6 ] = pos[ o3 ];
+ scope.positionArray[ c + 7 ] = pos[ o3 + 1 ];
+ scope.positionArray[ c + 8 ] = pos[ o3 + 2 ];
+
+ // normals
+
+ scope.normalArray[ c + 0 ] = norm[ o1 ];
+ scope.normalArray[ c + 1 ] = norm[ o1 + 1 ];
+ scope.normalArray[ c + 2 ] = norm[ o1 + 2 ];
+
+ scope.normalArray[ c + 3 ] = norm[ o2 ];
+ scope.normalArray[ c + 4 ] = norm[ o2 + 1 ];
+ scope.normalArray[ c + 5 ] = norm[ o2 + 2 ];
+
+ scope.normalArray[ c + 6 ] = norm[ o3 ];
+ scope.normalArray[ c + 7 ] = norm[ o3 + 1 ];
+ scope.normalArray[ c + 8 ] = norm[ o3 + 2 ];
+
+ // uvs
+
+ if ( scope.enableUvs ) {
+
+ var d = scope.count * 2;
+
+ scope.uvArray[ d + 0 ] = pos[ o1 ];
+ scope.uvArray[ d + 1 ] = pos[ o1 + 2 ];
+
+ scope.uvArray[ d + 2 ] = pos[ o2 ];
+ scope.uvArray[ d + 3 ] = pos[ o2 + 2 ];
+
+ scope.uvArray[ d + 4 ] = pos[ o3 ];
+ scope.uvArray[ d + 5 ] = pos[ o3 + 2 ];
+
+ }
+
+ // colors
+
+ if ( scope.enableColors ) {
+
+ scope.colorArray[ c + 0 ] = pos[ o1 ];
+ scope.colorArray[ c + 1 ] = pos[ o1 + 1 ];
+ scope.colorArray[ c + 2 ] = pos[ o1 + 2 ];
+
+ scope.colorArray[ c + 3 ] = pos[ o2 ];
+ scope.colorArray[ c + 4 ] = pos[ o2 + 1 ];
+ scope.colorArray[ c + 5 ] = pos[ o2 + 2 ];
+
+ scope.colorArray[ c + 6 ] = pos[ o3 ];
+ scope.colorArray[ c + 7 ] = pos[ o3 + 1 ];
+ scope.colorArray[ c + 8 ] = pos[ o3 + 2 ];
+
+ }
+
+ scope.count += 3;
+
+ if ( scope.count >= scope.maxCount - 3 ) {
+
+ scope.hasPositions = true;
+ scope.hasNormals = true;
+
+ if ( scope.enableUvs ) {
+
+ scope.hasUvs = true;
+
+ }
+
+ if ( scope.enableColors ) {
+
+ scope.hasColors = true;
+
+ }
+
+ renderCallback( scope );
+
+ }
+
+ }
+
+ this.begin = function () {
+
+ this.count = 0;
+
+ this.hasPositions = false;
+ this.hasNormals = false;
+ this.hasUvs = false;
+ this.hasColors = false;
+
+ };
+
+ this.end = function ( renderCallback ) {
+
+ if ( this.count === 0 ) return;
+
+ for ( var i = this.count * 3; i < this.positionArray.length; i ++ ) {
+
+ this.positionArray[ i ] = 0.0;
+
+ }
+
+ this.hasPositions = true;
+ this.hasNormals = true;
+
+ if ( this.enableUvs ) {
+
+ this.hasUvs = true;
+
+ }
+
+ if ( this.enableColors ) {
+
+ this.hasColors = true;
+
+ }
+
+ renderCallback( this );
+
+ };
+
+ /////////////////////////////////////
+ // Metaballs
+ /////////////////////////////////////
+
+ // Adds a reciprocal ball (nice and blobby) that, to be fast, fades to zero after
+ // a fixed distance, determined by strength and subtract.
+
+ this.addBall = function ( ballx, bally, ballz, strength, subtract ) {
+
+ var sign = Math.sign( strength );
+ strength = Math.abs( strength );
+
+ // Let's solve the equation to find the radius:
+ // 1.0 / (0.000001 + radius^2) * strength - subtract = 0
+ // strength / (radius^2) = subtract
+ // strength = subtract * radius^2
+ // radius^2 = strength / subtract
+ // radius = sqrt(strength / subtract)
+
+ var radius = this.size * Math.sqrt( strength / subtract ),
+ zs = ballz * this.size,
+ ys = bally * this.size,
+ xs = ballx * this.size;
+
+ var min_z = Math.floor( zs - radius ); if ( min_z < 1 ) min_z = 1;
+ var max_z = Math.floor( zs + radius ); if ( max_z > this.size - 1 ) max_z = this.size - 1;
+ var min_y = Math.floor( ys - radius ); if ( min_y < 1 ) min_y = 1;
+ var max_y = Math.floor( ys + radius ); if ( max_y > this.size - 1 ) max_y = this.size - 1;
+ var min_x = Math.floor( xs - radius ); if ( min_x < 1 ) min_x = 1;
+ var max_x = Math.floor( xs + radius ); if ( max_x > this.size - 1 ) max_x = this.size - 1;
+
+
+ // Don't polygonize in the outer layer because normals aren't
+ // well-defined there.
+
+ var x, y, z, y_offset, z_offset, fx, fy, fz, fz2, fy2, val;
+
+ for ( z = min_z; z < max_z; z ++ ) {
+
+ z_offset = this.size2 * z;
+ fz = z / this.size - ballz;
+ fz2 = fz * fz;
+
+ for ( y = min_y; y < max_y; y ++ ) {
+
+ y_offset = z_offset + this.size * y;
+ fy = y / this.size - bally;
+ fy2 = fy * fy;
+
+ for ( x = min_x; x < max_x; x ++ ) {
+
+ fx = x / this.size - ballx;
+ val = strength / ( 0.000001 + fx * fx + fy2 + fz2 ) - subtract;
+ if ( val > 0.0 ) this.field[ y_offset + x ] += val * sign;
+
+ }
+
+ }
+
+ }
+
+ };
+
+ this.addPlaneX = function( strength, subtract ) {
+
+ var x, y, z, xx, val, xdiv, cxy,
+
+ // cache attribute lookups
+ size = this.size,
+ yd = this.yd,
+ zd = this.zd,
+ field = this.field,
+
+ dist = size * Math.sqrt( strength / subtract );
+
+ if ( dist > size ) dist = size;
+
+ for ( x = 0; x < dist; x ++ ) {
+
+ xdiv = x / size;
+ xx = xdiv * xdiv;
+ val = strength / ( 0.0001 + xx ) - subtract;
+
+ if ( val > 0.0 ) {
+
+ for ( y = 0; y < size; y ++ ) {
+
+ cxy = x + y * yd;
+
+ for ( z = 0; z < size; z ++ ) {
+
+ field[ zd * z + cxy ] += val;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ };
+
+ this.addPlaneY = function( strength, subtract ) {
+
+ var x, y, z, yy, val, ydiv, cy, cxy,
+
+ // cache attribute lookups
+ size = this.size,
+ yd = this.yd,
+ zd = this.zd,
+ field = this.field,
+
+ dist = size * Math.sqrt( strength / subtract );
+
+ if ( dist > size ) dist = size;
+
+ for ( y = 0; y < dist; y ++ ) {
+
+ ydiv = y / size;
+ yy = ydiv * ydiv;
+ val = strength / ( 0.0001 + yy ) - subtract;
+
+ if ( val > 0.0 ) {
+
+ cy = y * yd;
+
+ for ( x = 0; x < size; x ++ ) {
+
+ cxy = cy + x;
+
+ for ( z = 0; z < size; z ++ )
+ field[ zd * z + cxy ] += val;
+
+ }
+
+ }
+
+ }
+
+ };
+
+ this.addPlaneZ = function( strength, subtract ) {
+
+ var x, y, z, zz, val, zdiv, cz, cyz,
+
+ // cache attribute lookups
+ size = this.size,
+ yd = this.yd,
+ zd = this.zd,
+ field = this.field,
+
+ dist = size * Math.sqrt( strength / subtract );
+
+ if ( dist > size ) dist = size;
+
+ for ( z = 0; z < dist; z ++ ) {
+
+ zdiv = z / size;
+ zz = zdiv * zdiv;
+ val = strength / ( 0.0001 + zz ) - subtract;
+ if ( val > 0.0 ) {
+
+ cz = zd * z;
+
+ for ( y = 0; y < size; y ++ ) {
+
+ cyz = cz + y * yd;
+
+ for ( x = 0; x < size; x ++ )
+ field[ cyz + x ] += val;
+
+ }
+
+ }
+
+ }
+
+ };
+
+ /////////////////////////////////////
+ // Updates
+ /////////////////////////////////////
+
+ this.reset = function () {
+
+ var i;
+
+ // wipe the normal cache
+
+ for ( i = 0; i < this.size3; i ++ ) {
+
+ this.normal_cache[ i * 3 ] = 0.0;
+ this.field[ i ] = 0.0;
+
+ }
+
+ };
+
+ this.render = function ( renderCallback ) {
+
+ this.begin();
+
+ // Triangulate. Yeah, this is slow.
+
+ var smin2 = this.size - 2;
+
+ for ( var z = 1; z < smin2; z ++ ) {
+
+ var z_offset = this.size2 * z;
+ var fz = ( z - this.halfsize ) / this.halfsize; //+ 1
+
+ for ( var y = 1; y < smin2; y ++ ) {
+
+ var y_offset = z_offset + this.size * y;
+ var fy = ( y - this.halfsize ) / this.halfsize; //+ 1
+
+ for ( var x = 1; x < smin2; x ++ ) {
+
+ var fx = ( x - this.halfsize ) / this.halfsize; //+ 1
+ var q = y_offset + x;
+
+ polygonize( fx, fy, fz, q, this.isolation, renderCallback );
+
+ }
+
+ }
+
+ }
+
+ this.end( renderCallback );
+
+ };
+
+ this.generateGeometry = function() {
+
+ var start = 0, geo = new THREE.Geometry();
+ var normals = [];
+
+ var geo_callback = function( object ) {
+
+ for ( var i = 0; i < object.count; i ++ ) {
+
+ var vertex = new THREE.Vector3().fromArray( object.positionArray, i * 3 );
+ var normal = new THREE.Vector3().fromArray( object.normalArray, i * 3 );
+
+ geo.vertices.push( vertex );
+ normals.push( normal );
+
+ }
+
+ var nfaces = object.count / 3;
+
+ for ( i = 0; i < nfaces; i ++ ) {
+
+ var a = ( start + i ) * 3;
+ var b = a + 1;
+ var c = a + 2;
+
+ var na = normals[ a ];
+ var nb = normals[ b ];
+ var nc = normals[ c ];
+
+ var face = new THREE.Face3( a, b, c, [ na, nb, nc ] );
+ geo.faces.push( face );
+
+ }
+
+ start += nfaces;
+ object.count = 0;
+
+ };
+
+ this.render( geo_callback );
+
+ // console.log( "generated " + geo.faces.length + " triangles" );
+
+ return geo;
+
+ };
+
+ this.init( resolution );
+
+};
+
+THREE.MarchingCubes.prototype = Object.create( THREE.ImmediateRenderObject.prototype );
+THREE.MarchingCubes.prototype.constructor = THREE.MarchingCubes;
+
+
+/////////////////////////////////////
+// Marching cubes lookup tables
+/////////////////////////////////////
+
+// These tables are straight from Paul Bourke's page:
+// http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/
+// who in turn got them from Cory Gene Bloyd.
+
+THREE.edgeTable = new Int32Array( [
+0x0, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
+0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
+0x190, 0x99, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
+0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
+0x230, 0x339, 0x33, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
+0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
+0x3a0, 0x2a9, 0x1a3, 0xaa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
+0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
+0x460, 0x569, 0x663, 0x76a, 0x66, 0x16f, 0x265, 0x36c,
+0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
+0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff, 0x3f5, 0x2fc,
+0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
+0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55, 0x15c,
+0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
+0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc,
+0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
+0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
+0xcc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
+0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
+0x15c, 0x55, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
+0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
+0x2fc, 0x3f5, 0xff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
+0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
+0x36c, 0x265, 0x16f, 0x66, 0x76a, 0x663, 0x569, 0x460,
+0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
+0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa, 0x1a3, 0x2a9, 0x3a0,
+0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
+0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33, 0x339, 0x230,
+0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
+0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99, 0x190,
+0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
+0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ] );
+
+THREE.triTable = new Int32Array( [
+- 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 1, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 8, 3, 9, 8, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, 1, 2, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 2, 10, 0, 2, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 8, 3, 2, 10, 8, 10, 9, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 11, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 11, 2, 8, 11, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 9, 0, 2, 3, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 11, 2, 1, 9, 11, 9, 8, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 10, 1, 11, 10, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 10, 1, 0, 8, 10, 8, 11, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 9, 0, 3, 11, 9, 11, 10, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 8, 10, 10, 8, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 7, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 3, 0, 7, 3, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 1, 9, 8, 4, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 1, 9, 4, 7, 1, 7, 3, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, 8, 4, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 4, 7, 3, 0, 4, 1, 2, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 2, 10, 9, 0, 2, 8, 4, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, - 1, - 1, - 1, - 1,
+8, 4, 7, 3, 11, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 4, 7, 11, 2, 4, 2, 0, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 0, 1, 8, 4, 7, 2, 3, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, - 1, - 1, - 1, - 1,
+3, 10, 1, 3, 11, 10, 7, 8, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, - 1, - 1, - 1, - 1,
+4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, - 1, - 1, - 1, - 1,
+4, 7, 11, 4, 11, 9, 9, 11, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 5, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 5, 4, 0, 8, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 5, 4, 1, 5, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 5, 4, 8, 3, 5, 3, 1, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, 9, 5, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 0, 8, 1, 2, 10, 4, 9, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 2, 10, 5, 4, 2, 4, 0, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, - 1, - 1, - 1, - 1,
+9, 5, 4, 2, 3, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 11, 2, 0, 8, 11, 4, 9, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 5, 4, 0, 1, 5, 2, 3, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, - 1, - 1, - 1, - 1,
+10, 3, 11, 10, 1, 3, 9, 5, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, - 1, - 1, - 1, - 1,
+5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, - 1, - 1, - 1, - 1,
+5, 4, 8, 5, 8, 10, 10, 8, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 7, 8, 5, 7, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 3, 0, 9, 5, 3, 5, 7, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 7, 8, 0, 1, 7, 1, 5, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 5, 3, 3, 5, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 7, 8, 9, 5, 7, 10, 1, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, - 1, - 1, - 1, - 1,
+8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, - 1, - 1, - 1, - 1,
+2, 10, 5, 2, 5, 3, 3, 5, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 9, 5, 7, 8, 9, 3, 11, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, - 1, - 1, - 1, - 1,
+2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, - 1, - 1, - 1, - 1,
+11, 2, 1, 11, 1, 7, 7, 1, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, - 1, - 1, - 1, - 1,
+5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, - 1,
+11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, - 1,
+11, 10, 5, 7, 11, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 6, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, 5, 10, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 0, 1, 5, 10, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 8, 3, 1, 9, 8, 5, 10, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 6, 5, 2, 6, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 6, 5, 1, 2, 6, 3, 0, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 6, 5, 9, 0, 6, 0, 2, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, - 1, - 1, - 1, - 1,
+2, 3, 11, 10, 6, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 0, 8, 11, 2, 0, 10, 6, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 1, 9, 2, 3, 11, 5, 10, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, - 1, - 1, - 1, - 1,
+6, 3, 11, 6, 5, 3, 5, 1, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, - 1, - 1, - 1, - 1,
+3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, - 1, - 1, - 1, - 1,
+6, 5, 9, 6, 9, 11, 11, 9, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 10, 6, 4, 7, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 3, 0, 4, 7, 3, 6, 5, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 9, 0, 5, 10, 6, 8, 4, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, - 1, - 1, - 1, - 1,
+6, 1, 2, 6, 5, 1, 4, 7, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, - 1, - 1, - 1, - 1,
+8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, - 1, - 1, - 1, - 1,
+7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, - 1,
+3, 11, 2, 7, 8, 4, 10, 6, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, - 1, - 1, - 1, - 1,
+0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, - 1, - 1, - 1, - 1,
+9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, - 1,
+8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, - 1, - 1, - 1, - 1,
+5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, - 1,
+0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, - 1,
+6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, - 1, - 1, - 1, - 1,
+10, 4, 9, 6, 4, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 10, 6, 4, 9, 10, 0, 8, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 0, 1, 10, 6, 0, 6, 4, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, - 1, - 1, - 1, - 1,
+1, 4, 9, 1, 2, 4, 2, 6, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, - 1, - 1, - 1, - 1,
+0, 2, 4, 4, 2, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 3, 2, 8, 2, 4, 4, 2, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 4, 9, 10, 6, 4, 11, 2, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, - 1, - 1, - 1, - 1,
+3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, - 1, - 1, - 1, - 1,
+6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, - 1,
+9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, - 1, - 1, - 1, - 1,
+8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, - 1,
+3, 11, 6, 3, 6, 0, 0, 6, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+6, 4, 8, 11, 6, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 10, 6, 7, 8, 10, 8, 9, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, - 1, - 1, - 1, - 1,
+10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, - 1, - 1, - 1, - 1,
+10, 6, 7, 10, 7, 1, 1, 7, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, - 1, - 1, - 1, - 1,
+2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, - 1,
+7, 8, 0, 7, 0, 6, 6, 0, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 3, 2, 6, 7, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, - 1, - 1, - 1, - 1,
+2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, - 1,
+1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, - 1,
+11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, - 1, - 1, - 1, - 1,
+8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, - 1,
+0, 9, 1, 11, 6, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, - 1, - 1, - 1, - 1,
+7, 11, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 6, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 0, 8, 11, 7, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 1, 9, 11, 7, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 1, 9, 8, 3, 1, 11, 7, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 1, 2, 6, 11, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, 3, 0, 8, 6, 11, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 9, 0, 2, 10, 9, 6, 11, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, - 1, - 1, - 1, - 1,
+7, 2, 3, 6, 2, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+7, 0, 8, 7, 6, 0, 6, 2, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 7, 6, 2, 3, 7, 0, 1, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, - 1, - 1, - 1, - 1,
+10, 7, 6, 10, 1, 7, 1, 3, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, - 1, - 1, - 1, - 1,
+0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, - 1, - 1, - 1, - 1,
+7, 6, 10, 7, 10, 8, 8, 10, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+6, 8, 4, 11, 8, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 6, 11, 3, 0, 6, 0, 4, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 6, 11, 8, 4, 6, 9, 0, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, - 1, - 1, - 1, - 1,
+6, 8, 4, 6, 11, 8, 2, 10, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, - 1, - 1, - 1, - 1,
+4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, - 1, - 1, - 1, - 1,
+10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, - 1,
+8, 2, 3, 8, 4, 2, 4, 6, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 4, 2, 4, 6, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, - 1, - 1, - 1, - 1,
+1, 9, 4, 1, 4, 2, 2, 4, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, - 1, - 1, - 1, - 1,
+10, 1, 0, 10, 0, 6, 6, 0, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, - 1,
+10, 9, 4, 6, 10, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 9, 5, 7, 6, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, 4, 9, 5, 11, 7, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 0, 1, 5, 4, 0, 7, 6, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, - 1, - 1, - 1, - 1,
+9, 5, 4, 10, 1, 2, 7, 6, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, - 1, - 1, - 1, - 1,
+7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, - 1, - 1, - 1, - 1,
+3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, - 1,
+7, 2, 3, 7, 6, 2, 5, 4, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, - 1, - 1, - 1, - 1,
+3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, - 1, - 1, - 1, - 1,
+6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, - 1,
+9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, - 1, - 1, - 1, - 1,
+1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, - 1,
+4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, - 1,
+7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, - 1, - 1, - 1, - 1,
+6, 9, 5, 6, 11, 9, 11, 8, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, - 1, - 1, - 1, - 1,
+0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, - 1, - 1, - 1, - 1,
+6, 11, 3, 6, 3, 5, 5, 3, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, - 1, - 1, - 1, - 1,
+0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, - 1,
+11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, - 1,
+6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, - 1, - 1, - 1, - 1,
+5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, - 1, - 1, - 1, - 1,
+9, 5, 6, 9, 6, 0, 0, 6, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, - 1,
+1, 5, 6, 2, 1, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, - 1,
+10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, - 1, - 1, - 1, - 1,
+0, 3, 8, 5, 6, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 5, 6, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 5, 10, 7, 5, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 5, 10, 11, 7, 5, 8, 3, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 11, 7, 5, 10, 11, 1, 9, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, - 1, - 1, - 1, - 1,
+11, 1, 2, 11, 7, 1, 7, 5, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, - 1, - 1, - 1, - 1,
+9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, - 1, - 1, - 1, - 1,
+7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, - 1,
+2, 5, 10, 2, 3, 5, 3, 7, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, - 1, - 1, - 1, - 1,
+9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, - 1, - 1, - 1, - 1,
+9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, - 1,
+1, 3, 5, 3, 7, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 7, 0, 7, 1, 1, 7, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 0, 3, 9, 3, 5, 5, 3, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 8, 7, 5, 9, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 8, 4, 5, 10, 8, 10, 11, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, - 1, - 1, - 1, - 1,
+0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, - 1, - 1, - 1, - 1,
+10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, - 1,
+2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, - 1, - 1, - 1, - 1,
+0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, - 1,
+0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, - 1,
+9, 4, 5, 2, 11, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, - 1, - 1, - 1, - 1,
+5, 10, 2, 5, 2, 4, 4, 2, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, - 1,
+5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, - 1, - 1, - 1, - 1,
+8, 4, 5, 8, 5, 3, 3, 5, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 4, 5, 1, 0, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, - 1, - 1, - 1, - 1,
+9, 4, 5, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 11, 7, 4, 9, 11, 9, 10, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, - 1, - 1, - 1, - 1,
+1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, - 1, - 1, - 1, - 1,
+3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, - 1,
+4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, - 1, - 1, - 1, - 1,
+9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, - 1,
+11, 7, 4, 11, 4, 2, 2, 4, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, - 1, - 1, - 1, - 1,
+2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, - 1, - 1, - 1, - 1,
+9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, - 1,
+3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, - 1,
+1, 10, 2, 8, 7, 4, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 9, 1, 4, 1, 7, 7, 1, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, - 1, - 1, - 1, - 1,
+4, 0, 3, 7, 4, 3, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+4, 8, 7, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 10, 8, 10, 11, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 0, 9, 3, 9, 11, 11, 9, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 1, 10, 0, 10, 8, 8, 10, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 1, 10, 11, 3, 10, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 2, 11, 1, 11, 9, 9, 11, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, - 1, - 1, - 1, - 1,
+0, 2, 11, 8, 0, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+3, 2, 11, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 3, 8, 2, 8, 10, 10, 8, 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+9, 10, 2, 0, 9, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, - 1, - 1, - 1, - 1,
+1, 10, 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+1, 3, 8, 9, 1, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 9, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+0, 3, 8, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1,
+- 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 ] );
diff --git a/app/static/js/Menubar.Add.js b/app/static/js/Menubar.Add.js
new file mode 100644
index 0000000..90e7e90
--- /dev/null
+++ b/app/static/js/Menubar.Add.js
@@ -0,0 +1,150 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Add = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'Add' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ //
+
+ var meshCount = 0;
+ var lightCount = 0;
+ var cameraCount = 0;
+
+ editor.signals.editorCleared.add( function () {
+
+ meshCount = 0;
+ lightCount = 0;
+ cameraCount = 0;
+
+ } );
+ /*
+ var light = new THREE.SpotLight( 0xffffff, 1, 0, Math.PI * 0.1, 0 );
+ light.name = 'SpotLight';
+ light.target.name = 'SpotLight Target';
+ light.position.set( 5000, 5500, 0 );
+ editor.execute( new AddObjectCommand( light ) );
+ */
+ // Input Variables
+
+ var check = false; // set a flag to check if button already pressed
+
+ var x, y, z; // initialize x, y, and z variables for inputs
+ var input_pane = new UI.Panel(); // create interface elements to display input option
+ var input_row = new UI.Row();
+ var filler = new UI.HorizontalRule();
+ var input; // initialize general input button
+
+ var text = new UI.Text("Input Coordinates"); // instruction text and spacing
+ text.setMarginLeft('18px');
+ text.setMarginRight('18px');
+ text.setPaddingBottom('12px');
+
+ var text_x = new UI.Text("X:").setMarginRight('2px').setMarginLeft('4px'); // input text, area, and spacing
+ var text_y = new UI.Text("Y:").setMarginRight('2px');
+ var text_z = new UI.Text("Z:").setMarginRight('2px');
+ var input_x = new UI.Number().setWidth( '28px' );
+ var input_y = new UI.Number().setWidth( '28px' );
+ var input_z = new UI.Number().setWidth( '28px' );
+
+ input_pane.add(text); // ready all contents to be displayed once added to the main panel
+ input_row.add(text_x);
+ input_row.add(input_x);
+ input_row.add(text_y);
+ input_row.add(input_y);
+ input_row.add(text_z);
+ input_row.add(input_z);
+ input_pane.add(input_row);
+ input_pane.add(new UI.Break());
+
+ // Antenna
+
+ var option = new UI.Row(); // basic point for antenna representation
+ option.setClass( 'option' );
+ option.setTextContent( 'Antenna' );
+ option.onClick( function () {
+
+ if (check === true){ // check if prior button state already displayed
+ options.newInput(); // if so, remove the display
+ input_pane.remove(input);
+ }
+ check = true; // set flag to true as new display will populate
+
+ input = new UI.Button(); // set input button spacing and function
+ input.setMarginLeft('42px');
+ input.setClass("input");
+ input.setTextContent("Enter");
+ input.onClick( function () {
+
+ x = input_x.getValue(); // store entered values
+ y = input_y.getValue();
+ z = input_z.getValue();
+
+ var x_nose = editor.getModel()[4]; // convert entered values to meters coordinate system
+ var x_tail = editor.getModel()[5];
+ var x_slope = ( x_nose - x_tail ) / editor.getModelLength();
+
+ var z_nose = editor.getModel()[3];
+ var z_tail = editor.getModel()[2];
+ var z_slope = ( z_nose - z_tail ) / editor.getModelHeight();
+
+ var right_wing = editor.getModel()[0];
+ var left_wing = editor.getModel()[1];
+ var y_slope = ( right_wing - left_wing ) / editor.getModelWingspan();
+
+ var x_NG = y * y_slope;
+ var y_NG = ( z * z_slope ) + z_nose;
+ var z_NG = x_nose + ( x * x_slope );
+
+ var radius = ( right_wing - left_wing ) / 180; // create sphere object according to model size
+ var widthSegments = 32;
+ var heightSegments = 16;
+ var phiStart = 0;
+ var phiLength = Math.PI * 2;
+ var thetaStart = 0;
+ var thetaLength = Math.PI;
+
+ var geometry = new THREE.SphereBufferGeometry( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength );
+ var material = new THREE.MeshBasicMaterial( {color: 0xff0000} );
+ var mesh = new THREE.Mesh( geometry, material );
+ mesh.name = 'Antenna' + ( ++ meshCount );
+
+ editor.execute( new SetPositionCommand( mesh, new THREE.Vector3( x_NG, y_NG, z_NG ) ) ); // move object to desired coordinates
+
+ editor.execute( new AddObjectCommand( mesh ) ); // add object to scene
+
+ input_pane.remove(input); // remove additional input display
+ options.newInput();
+ check = false;
+
+ });
+
+ input_pane.add(input); // add display to menubar panel
+ options.add(filler);
+ options.add(input_pane);
+
+ } );
+ options.add( option );
+
+ options.newInput = function(){ // reset all input values and displays
+ input_x.setValue(0);
+ input_y.setValue(0);
+ input_z.setValue(0);
+ options.remove(input_pane);
+ options.remove(filler);
+ };
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.Edit.js b/app/static/js/Menubar.Edit.js
new file mode 100644
index 0000000..17b7b73
--- /dev/null
+++ b/app/static/js/Menubar.Edit.js
@@ -0,0 +1,210 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Edit = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'Edit' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ // Undo
+
+ var undo = new UI.Row();
+ undo.setClass( 'option' );
+ undo.setTextContent( 'Undo (Ctrl+Z)' );
+ undo.onClick( function () {
+
+ editor.undo();
+
+ } );
+ options.add( undo );
+
+ // Redo
+
+ var redo = new UI.Row();
+ redo.setClass( 'option' );
+ redo.setTextContent( 'Redo (Ctrl+Shift+Z)' );
+ redo.onClick( function () {
+
+ editor.redo();
+
+ } );
+ options.add( redo );
+
+ // Clear History
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Clear History' );
+ option.onClick( function () {
+
+ if ( confirm( 'The Undo/Redo History will be cleared. Are you sure?' ) ) {
+
+ editor.history.clear();
+
+ }
+
+ } );
+ options.add( option );
+
+
+ editor.signals.historyChanged.add( function () {
+
+ var history = editor.history;
+
+ undo.setClass( 'option' );
+ redo.setClass( 'option' );
+
+ if ( history.undos.length == 0 ) {
+
+ undo.setClass( 'inactive' );
+
+ }
+
+ if ( history.redos.length == 0 ) {
+
+ redo.setClass( 'inactive' );
+
+ }
+
+ } );
+
+ // ---
+
+ options.add( new UI.HorizontalRule() );
+
+ // Clone
+
+ /*
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Clone' );
+ option.onClick( function () {
+
+ var object = editor.selected;
+
+ if ( object.parent === null ) return; // avoid cloning the camera or scene
+
+ object = object.clone();
+
+ editor.execute( new AddObjectCommand( object ) );
+
+ } );
+ options.add( option );
+
+ */
+
+ // Delete
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Delete (Del)' );
+ option.onClick( function () {
+
+ var object = editor.selected;
+
+ if ( confirm( 'Delete ' + object.name + '?' ) === false ) return;
+
+ var parent = object.parent;
+ if ( parent === undefined ) return; // avoid deleting the camera or scene
+
+ editor.execute( new RemoveObjectCommand( object ) );
+
+ } );
+ options.add( option );
+
+ // Minify shaders
+
+ /*
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Minify Shaders' );
+ option.onClick( function() {
+
+ var root = editor.selected || editor.scene;
+
+ var errors = [];
+ var nMaterialsChanged = 0;
+
+ var path = [];
+
+ function getPath ( object ) {
+
+ path.length = 0;
+
+ var parent = object.parent;
+ if ( parent !== undefined ) getPath( parent );
+
+ path.push( object.name || object.uuid );
+
+ return path;
+
+ }
+
+ var cmds = [];
+ root.traverse( function ( object ) {
+
+ var material = object.material;
+
+ if ( material instanceof THREE.ShaderMaterial ) {
+
+ try {
+
+ var shader = glslprep.minifyGlsl( [
+ material.vertexShader, material.fragmentShader ] );
+
+ cmds.push( new SetMaterialValueCommand( object, 'vertexShader', shader[ 0 ] ) );
+ cmds.push( new SetMaterialValueCommand( object, 'fragmentShader', shader[ 1 ] ) );
+
+ ++nMaterialsChanged;
+
+ } catch ( e ) {
+
+ var path = getPath( object ).join( "/" );
+
+ if ( e instanceof glslprep.SyntaxError )
+
+ errors.push( path + ":" +
+ e.line + ":" + e.column + ": " + e.message );
+
+ else {
+
+ errors.push( path +
+ ": Unexpected error (see console for details)." );
+
+ console.error( e.stack || e );
+
+ }
+
+ }
+
+ }
+
+ } );
+
+ if ( nMaterialsChanged > 0 ) {
+
+ editor.execute( new MultiCmdsCommand( cmds ), 'Minify Shaders' );
+
+ }
+
+ window.alert( nMaterialsChanged +
+ " material(s) were changed.\n" + errors.join( "\n" ) );
+
+ } );
+ options.add( option );
+
+ */
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.Examples.js b/app/static/js/Menubar.Examples.js
new file mode 100644
index 0000000..8fc7a3e
--- /dev/null
+++ b/app/static/js/Menubar.Examples.js
@@ -0,0 +1,62 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Examples = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'Examples' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ // Examples
+
+ var items = [
+ { title: 'Arkanoid', file: 'arkanoid.app.json' },
+ { title: 'Camera', file: 'camera.app.json' },
+ { title: 'Particles', file: 'particles.app.json' },
+ { title: 'Pong', file: 'pong.app.json' },
+ { title: 'Shaders', file: 'shaders.app.json' }
+ ];
+
+ var loader = new THREE.FileLoader();
+
+ for ( var i = 0; i < items.length; i ++ ) {
+
+ ( function ( i ) {
+
+ var item = items[ i ];
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( item.title );
+ option.onClick( function () {
+
+ if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
+
+ loader.load( 'examples/' + item.file, function ( text ) {
+
+ editor.clear();
+ editor.fromJSON( JSON.parse( text ) );
+
+ } );
+
+ }
+
+ } );
+ options.add( option );
+
+ } )( i )
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.File.js b/app/static/js/Menubar.File.js
new file mode 100644
index 0000000..804aca7
--- /dev/null
+++ b/app/static/js/Menubar.File.js
@@ -0,0 +1,396 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.File = function ( editor ) {
+
+ var NUMBER_PRECISION = 6;
+
+ function parseNumber( key, value ) {
+
+ return typeof value === 'number' ? parseFloat( value.toFixed( NUMBER_PRECISION ) ) : value;
+
+ }
+
+ //
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'File' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ // New
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'New' );
+ option.onClick( function () {
+
+ if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
+
+ editor.clear();
+ editor.storage.clear();
+ editor.project_uuid = "";
+ }
+
+ } );
+ options.add( option );
+
+// Load/Resume State
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Load' );
+ option.onClick( function () {
+
+ if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) {
+ fetchStates();
+
+ document.getElementById('myModal').style.display = "block";
+
+ }
+
+ } );
+ options.add( option );
+
+
+ //
+
+ options.add( new UI.HorizontalRule() );
+
+ // Import
+
+ var form = document.createElement( 'form' );
+ form.style.display = 'none';
+ document.body.appendChild( form );
+
+ var fileInput = document.createElement( 'input' );
+ fileInput.type = 'file';
+ fileInput.addEventListener( 'change', function ( event ) {
+
+ editor.loader.loadFile( fileInput.files[ 0 ] );
+ form.reset();
+
+ var color = 0xffffff; // create spotlight when new model imported
+ var intensity = 1;
+ var distance = 0;
+ var angle = Math.PI * 0.1;
+ var penumbra = 0;
+
+ var light = new THREE.SpotLight( color, intensity, distance, angle, penumbra );
+ light.name = 'SpotLight';
+ light.target.name = 'SpotLight Target';
+
+ light.position.set( 0, 5500, 5000 );
+
+ editor.execute( new AddObjectCommand( light ) );
+
+ } );
+ form.appendChild( fileInput );
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Import' );
+ option.onClick( function () {
+
+ fileInput.click();
+
+ } );
+ options.add( option );
+
+ //
+
+ options.add( new UI.HorizontalRule() );
+
+ /*
+ // Export Geometry
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export Geometry' );
+ option.onClick( function () {
+
+ var object = editor.selected;
+
+ if ( object === null ) {
+
+ alert( 'No object selected.' );
+ return;
+
+ }
+
+ var geometry = object.geometry;
+
+ if ( geometry === undefined ) {
+
+ alert( 'The selected object doesn\'t have geometry.' );
+ return;
+
+ }
+
+ var output = geometry.toJSON();
+
+ try {
+
+ output = JSON.stringify( output, parseNumber, '\t' );
+ output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
+ } catch ( e ) {
+
+ output = JSON.stringify( output );
+
+ }
+
+ saveString( output, 'geometry.json' );
+
+ } );
+ options.add( option );
+
+ */
+
+ // Export Object
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export Object' );
+ option.onClick( function () {
+
+ var object = editor.selected;
+
+ if ( object === null ) {
+
+ alert( 'No object selected' );
+ return;
+
+ }
+
+ var output = object.toJSON();
+
+ try {
+
+ output = JSON.stringify( output, parseNumber, '\t' );
+ output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
+ } catch ( e ) {
+
+ output = JSON.stringify( output );
+
+ }
+
+ saveString( output, 'model.json' );
+
+ } );
+ options.add( option );
+
+ // Export
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export Scene' );
+ option.onClick( function () {
+
+ var output = editor.scene.toJSON();
+
+ try {
+
+ output = JSON.stringify( output, parseNumber, '\t' );
+ output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
+ } catch ( e ) {
+
+ output = JSON.stringify( output );
+
+ }
+
+ saveString( output, 'scene.json' );
+
+ } );
+ options.add( option );
+
+ //
+
+ /*
+ options.add( new UI.HorizontalRule() );
+
+ // Export GLTF
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export GLTF' );
+ option.onClick( function () {
+
+ var exporter = new THREE.GLTFExporter();
+
+ exporter.parse( editor.scene, function ( result ) {
+
+ saveString( JSON.stringify( result, null, 2 ), 'scene.gltf' );
+
+ } );
+
+
+ } );
+ options.add( option );
+
+ // Export OBJ
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export OBJ' );
+ option.onClick( function () {
+
+ var object = editor.selected;
+
+ if ( object === null ) {
+
+ alert( 'No object selected.' );
+ return;
+
+ }
+
+ var exporter = new THREE.OBJExporter();
+
+ saveString( exporter.parse( object ), 'model.obj' );
+
+ } );
+ options.add( option );
+
+ // Export STL
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Export STL' );
+ option.onClick( function () {
+
+ var exporter = new THREE.STLExporter();
+
+ saveString( exporter.parse( editor.scene ), 'model.stl' );
+
+ } );
+ options.add( option );
+
+ //
+
+ options.add( new UI.HorizontalRule() );
+
+ // Publish
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Publish' );
+ option.onClick( function () {
+
+ var zip = new JSZip();
+
+ //
+
+ var output = editor.toJSON();
+ output.metadata.type = 'App';
+ delete output.history;
+
+ var vr = output.project.vr;
+
+ output = JSON.stringify( output, parseNumber, '\t' );
+ output = output.replace( /[\n\t]+([\d\.e\-\[\]]+)/g, '$1' );
+
+ zip.file( 'app.json', output );
+
+ //
+
+ var manager = new THREE.LoadingManager( function () {
+
+ save( zip.generate( { type: 'blob' } ), 'download.zip' );
+
+ } );
+
+ var loader = new THREE.FileLoader( manager );
+ loader.load( 'js/libs/app/index.html', function ( content ) {
+
+ var includes = [];
+
+ if ( vr ) {
+
+ includes.push( '' );
+
+ }
+
+ content = content.replace( '', includes.join( '\n\t\t' ) );
+
+ zip.file( 'index.html', content );
+
+ } );
+ loader.load( 'js/libs/app.js', function ( content ) {
+
+ zip.file( 'js/app.js', content );
+
+ } );
+ loader.load( '../build/three.min.js', function ( content ) {
+
+ zip.file( 'js/three.min.js', content );
+
+ } );
+
+ if ( vr ) {
+
+ loader.load( '../examples/js/vr/WebVR.js', function ( content ) {
+
+ zip.file( 'js/WebVR.js', content );
+
+ } );
+
+ }
+
+ } );
+ options.add( option );
+
+ /*
+ // Publish (Dropbox)
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Publish (Dropbox)' );
+ option.onClick( function () {
+
+ var parameters = {
+ files: [
+ { 'url': 'data:text/plain;base64,' + window.btoa( "Hello, World" ), 'filename': 'app/test.txt' }
+ ]
+ };
+
+ Dropbox.save( parameters );
+
+ } );
+ options.add( option );
+ */
+
+
+ //
+
+ var link = document.createElement( 'a' );
+ link.style.display = 'none';
+ document.body.appendChild( link ); // Firefox workaround, see #6594
+
+ function save( blob, filename ) {
+
+ link.href = URL.createObjectURL( blob );
+ link.download = filename || 'data.json';
+ link.click();
+
+ // URL.revokeObjectURL( url ); breaks Firefox...
+
+ }
+
+ function saveString( text, filename ) {
+
+ save( new Blob( [ text ], { type: 'text/plain' } ), filename );
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.Help.js b/app/static/js/Menubar.Help.js
new file mode 100644
index 0000000..53dca97
--- /dev/null
+++ b/app/static/js/Menubar.Help.js
@@ -0,0 +1,45 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Help = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'Help' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ // Source code
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'Source code' );
+ option.onClick( function () {
+
+ window.open( 'https://github.com/mrdoob/three.js/tree/master/editor', '_blank' )
+
+ } );
+ options.add( option );
+
+ // About
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'About' );
+ option.onClick( function () {
+
+ window.open( 'http://threejs.org', '_blank' );
+
+ } );
+ options.add( option );
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.Play.js b/app/static/js/Menubar.Play.js
new file mode 100644
index 0000000..75a3cb2
--- /dev/null
+++ b/app/static/js/Menubar.Play.js
@@ -0,0 +1,38 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Play = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var isPlaying = false;
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'Play' );
+ title.onClick( function () {
+
+ if ( isPlaying === false ) {
+
+ isPlaying = true;
+ title.setTextContent( 'Stop' );
+ signals.startPlayer.dispatch();
+
+ } else {
+
+ isPlaying = false;
+ title.setTextContent( 'Play' );
+ signals.stopPlayer.dispatch();
+
+ }
+
+ } );
+ container.add( title );
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.Status.js b/app/static/js/Menubar.Status.js
new file mode 100644
index 0000000..96ad03e
--- /dev/null
+++ b/app/static/js/Menubar.Status.js
@@ -0,0 +1,46 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.Status = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu right' );
+
+ var autosave = new UI.THREE.Boolean( editor.config.getKey( 'autosave' ), 'autosave' );
+ autosave.text.setColor( '#888' );
+ autosave.onChange( function () {
+
+ var value = this.getValue();
+
+ editor.config.setKey( 'autosave', value );
+
+ if ( value === true ) {
+
+ editor.signals.sceneGraphChanged.dispatch();
+
+ }
+
+ } );
+ container.add( autosave );
+
+ editor.signals.savingStarted.add( function () {
+
+ autosave.text.setTextDecoration( 'underline' );
+
+ } );
+
+ editor.signals.savingFinished.add( function () {
+
+ autosave.text.setTextDecoration( 'none' );
+
+ } );
+
+ var version = new UI.Text( 'prototype' );
+ version.setClass( 'title' );
+ version.setOpacity( 0.5 );
+ container.add( version );
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.View.js b/app/static/js/Menubar.View.js
new file mode 100644
index 0000000..2acaa3f
--- /dev/null
+++ b/app/static/js/Menubar.View.js
@@ -0,0 +1,41 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Menubar.View = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setClass( 'menu' );
+
+ var title = new UI.Panel();
+ title.setClass( 'title' );
+ title.setTextContent( 'View' );
+ container.add( title );
+
+ var options = new UI.Panel();
+ options.setClass( 'options' );
+ container.add( options );
+
+ // VR mode
+
+ var option = new UI.Row();
+ option.setClass( 'option' );
+ option.setTextContent( 'VR mode' );
+ option.onClick( function () {
+
+ if ( WEBVR.isAvailable() === true ) {
+
+ editor.signals.enterVR.dispatch();
+
+ } else {
+
+ alert( 'WebVR not available' );
+
+ }
+
+ } );
+ options.add( option );
+
+ return container;
+
+};
diff --git a/app/static/js/Menubar.js b/app/static/js/Menubar.js
new file mode 100644
index 0000000..63b522b
--- /dev/null
+++ b/app/static/js/Menubar.js
@@ -0,0 +1,17 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Menubar = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setId( 'menubar' );
+
+ container.add( new Menubar.File( editor ) );
+ container.add( new Menubar.Edit( editor ) );
+ container.add( new Menubar.Add( editor ) );
+ container.add( new Menubar.Status( editor ) );
+
+ return container;
+
+};
diff --git a/app/static/js/MorphAnimMesh.js b/app/static/js/MorphAnimMesh.js
new file mode 100644
index 0000000..a0d2063
--- /dev/null
+++ b/app/static/js/MorphAnimMesh.js
@@ -0,0 +1,69 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.MorphAnimMesh = function ( geometry, material ) {
+
+ THREE.Mesh.call( this, geometry, material );
+
+ this.type = 'MorphAnimMesh';
+
+ this.mixer = new THREE.AnimationMixer( this );
+ this.activeAction = null;
+};
+
+THREE.MorphAnimMesh.prototype = Object.create( THREE.Mesh.prototype );
+THREE.MorphAnimMesh.prototype.constructor = THREE.MorphAnimMesh;
+
+THREE.MorphAnimMesh.prototype.setDirectionForward = function () {
+
+ this.mixer.timeScale = 1.0;
+
+};
+
+THREE.MorphAnimMesh.prototype.setDirectionBackward = function () {
+
+ this.mixer.timeScale = -1.0;
+
+};
+
+THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
+
+ if( this.activeAction ) {
+
+ this.activeAction.stop();
+ this.activeAction = null;
+
+ }
+
+ var clip = THREE.AnimationClip.findByName( this, label );
+
+ if ( clip ) {
+
+ var action = this.mixer.clipAction( clip );
+ action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
+ this.activeAction = action.play();
+
+ } else {
+
+ throw new Error( 'THREE.MorphAnimMesh: animations[' + label + '] undefined in .playAnimation()' );
+
+ }
+
+};
+
+THREE.MorphAnimMesh.prototype.updateAnimation = function ( delta ) {
+
+ this.mixer.update( delta );
+
+};
+
+THREE.MorphAnimMesh.prototype.copy = function ( source ) {
+
+ THREE.Mesh.prototype.copy.call( this, source );
+
+ this.mixer = new THREE.AnimationMixer( this );
+
+ return this;
+
+};
diff --git a/app/static/js/MorphBlendMesh.js b/app/static/js/MorphBlendMesh.js
new file mode 100644
index 0000000..73eb6ec
--- /dev/null
+++ b/app/static/js/MorphBlendMesh.js
@@ -0,0 +1,318 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.MorphBlendMesh = function ( geometry, material ) {
+
+ THREE.Mesh.call( this, geometry, material );
+
+ this.animationsMap = {};
+ this.animationsList = [];
+
+ // prepare default animation
+ // (all frames played together in 1 second)
+
+ var numFrames = this.geometry.morphTargets.length;
+
+ var name = "__default";
+
+ var startFrame = 0;
+ var endFrame = numFrames - 1;
+
+ var fps = numFrames / 1;
+
+ this.createAnimation( name, startFrame, endFrame, fps );
+ this.setAnimationWeight( name, 1 );
+
+}
+
+THREE.MorphBlendMesh.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), {
+
+ constructor: THREE.MorphBlendMesh,
+
+ createAnimation: function ( name, start, end, fps ) {
+
+ var animation = {
+
+ start: start,
+ end: end,
+
+ length: end - start + 1,
+
+ fps: fps,
+ duration: ( end - start ) / fps,
+
+ lastFrame: 0,
+ currentFrame: 0,
+
+ active: false,
+
+ time: 0,
+ direction: 1,
+ weight: 1,
+
+ directionBackwards: false,
+ mirroredLoop: false
+
+ };
+
+ this.animationsMap[ name ] = animation;
+ this.animationsList.push( animation );
+
+ },
+
+ autoCreateAnimations: function ( fps ) {
+
+ var pattern = /([a-z]+)_?(\d+)/i;
+
+ var firstAnimation, frameRanges = {};
+
+ var geometry = this.geometry;
+
+ for ( var i = 0, il = geometry.morphTargets.length; i < il; i ++ ) {
+
+ var morph = geometry.morphTargets[ i ];
+ var chunks = morph.name.match( pattern );
+
+ if ( chunks && chunks.length > 1 ) {
+
+ var name = chunks[ 1 ];
+
+ if ( ! frameRanges[ name ] ) frameRanges[ name ] = { start: Infinity, end: - Infinity };
+
+ var range = frameRanges[ name ];
+
+ if ( i < range.start ) range.start = i;
+ if ( i > range.end ) range.end = i;
+
+ if ( ! firstAnimation ) firstAnimation = name;
+
+ }
+
+ }
+
+ for ( var name in frameRanges ) {
+
+ var range = frameRanges[ name ];
+ this.createAnimation( name, range.start, range.end, fps );
+
+ }
+
+ this.firstAnimation = firstAnimation;
+
+ },
+
+ setAnimationDirectionForward: function ( name ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.direction = 1;
+ animation.directionBackwards = false;
+
+ }
+
+ },
+
+ setAnimationDirectionBackward: function ( name ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.direction = - 1;
+ animation.directionBackwards = true;
+
+ }
+
+ },
+
+ setAnimationFPS: function ( name, fps ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.fps = fps;
+ animation.duration = ( animation.end - animation.start ) / animation.fps;
+
+ }
+
+ },
+
+ setAnimationDuration: function ( name, duration ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.duration = duration;
+ animation.fps = ( animation.end - animation.start ) / animation.duration;
+
+ }
+
+ },
+
+ setAnimationWeight: function ( name, weight ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.weight = weight;
+
+ }
+
+ },
+
+ setAnimationTime: function ( name, time ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.time = time;
+
+ }
+
+ },
+
+ getAnimationTime: function ( name ) {
+
+ var time = 0;
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ time = animation.time;
+
+ }
+
+ return time;
+
+ },
+
+ getAnimationDuration: function ( name ) {
+
+ var duration = - 1;
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ duration = animation.duration;
+
+ }
+
+ return duration;
+
+ },
+
+ playAnimation: function ( name ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.time = 0;
+ animation.active = true;
+
+ } else {
+
+ console.warn( "THREE.MorphBlendMesh: animation[" + name + "] undefined in .playAnimation()" );
+
+ }
+
+ },
+
+ stopAnimation: function ( name ) {
+
+ var animation = this.animationsMap[ name ];
+
+ if ( animation ) {
+
+ animation.active = false;
+
+ }
+
+ },
+
+ update: function ( delta ) {
+
+ for ( var i = 0, il = this.animationsList.length; i < il; i ++ ) {
+
+ var animation = this.animationsList[ i ];
+
+ if ( ! animation.active ) continue;
+
+ var frameTime = animation.duration / animation.length;
+
+ animation.time += animation.direction * delta;
+
+ if ( animation.mirroredLoop ) {
+
+ if ( animation.time > animation.duration || animation.time < 0 ) {
+
+ animation.direction *= - 1;
+
+ if ( animation.time > animation.duration ) {
+
+ animation.time = animation.duration;
+ animation.directionBackwards = true;
+
+ }
+
+ if ( animation.time < 0 ) {
+
+ animation.time = 0;
+ animation.directionBackwards = false;
+
+ }
+
+ }
+
+ } else {
+
+ animation.time = animation.time % animation.duration;
+
+ if ( animation.time < 0 ) animation.time += animation.duration;
+
+ }
+
+ var keyframe = animation.start + THREE.Math.clamp( Math.floor( animation.time / frameTime ), 0, animation.length - 1 );
+ var weight = animation.weight;
+
+ if ( keyframe !== animation.currentFrame ) {
+
+ this.morphTargetInfluences[ animation.lastFrame ] = 0;
+ this.morphTargetInfluences[ animation.currentFrame ] = 1 * weight;
+
+ this.morphTargetInfluences[ keyframe ] = 0;
+
+ animation.lastFrame = animation.currentFrame;
+ animation.currentFrame = keyframe;
+
+ }
+
+ var mix = ( animation.time % frameTime ) / frameTime;
+
+ if ( animation.directionBackwards ) mix = 1 - mix;
+
+ if ( animation.currentFrame !== animation.lastFrame ) {
+
+ this.morphTargetInfluences[ animation.currentFrame ] = mix * weight;
+ this.morphTargetInfluences[ animation.lastFrame ] = ( 1 - mix ) * weight;
+
+ } else {
+
+ this.morphTargetInfluences[ animation.currentFrame ] = weight;
+
+ }
+
+ }
+
+ }
+
+} );
diff --git a/app/static/js/Ocean.js b/app/static/js/Ocean.js
new file mode 100644
index 0000000..d30873a
--- /dev/null
+++ b/app/static/js/Ocean.js
@@ -0,0 +1,359 @@
+THREE.Ocean = function ( renderer, camera, scene, options ) {
+
+ // flag used to trigger parameter changes
+ this.changed = true;
+ this.initial = true;
+
+ // Assign required parameters as object properties
+ this.oceanCamera = new THREE.OrthographicCamera(); //camera.clone();
+ this.oceanCamera.position.z = 1;
+ this.renderer = renderer;
+ this.renderer.clearColor( 0xffffff );
+
+ this.scene = new THREE.Scene();
+
+ // Assign optional parameters as variables and object properties
+ function optionalParameter( value, defaultValue ) {
+
+ return value !== undefined ? value : defaultValue;
+
+ }
+ options = options || {};
+ this.clearColor = optionalParameter( options.CLEAR_COLOR, [ 1.0, 1.0, 1.0, 0.0 ] );
+ this.geometryOrigin = optionalParameter( options.GEOMETRY_ORIGIN, [ - 1000.0, - 1000.0 ] );
+ this.sunDirectionX = optionalParameter( options.SUN_DIRECTION[ 0 ], - 1.0 );
+ this.sunDirectionY = optionalParameter( options.SUN_DIRECTION[ 1 ], 1.0 );
+ this.sunDirectionZ = optionalParameter( options.SUN_DIRECTION[ 2 ], 1.0 );
+ this.oceanColor = optionalParameter( options.OCEAN_COLOR, new THREE.Vector3( 0.004, 0.016, 0.047 ) );
+ this.skyColor = optionalParameter( options.SKY_COLOR, new THREE.Vector3( 3.2, 9.6, 12.8 ) );
+ this.exposure = optionalParameter( options.EXPOSURE, 0.35 );
+ this.geometryResolution = optionalParameter( options.GEOMETRY_RESOLUTION, 32 );
+ this.geometrySize = optionalParameter( options.GEOMETRY_SIZE, 2000 );
+ this.resolution = optionalParameter( options.RESOLUTION, 64 );
+ this.floatSize = optionalParameter( options.SIZE_OF_FLOAT, 4 );
+ this.windX = optionalParameter( options.INITIAL_WIND[ 0 ], 10.0 );
+ this.windY = optionalParameter( options.INITIAL_WIND[ 1 ], 10.0 );
+ this.size = optionalParameter( options.INITIAL_SIZE, 250.0 );
+ this.choppiness = optionalParameter( options.INITIAL_CHOPPINESS, 1.5 );
+
+ //
+ this.matrixNeedsUpdate = false;
+
+ // Setup framebuffer pipeline
+ var renderTargetType = optionalParameter( options.USE_HALF_FLOAT, false ) ? THREE.HalfFloatType : THREE.FloatType;
+ var LinearClampParams = {
+ minFilter: THREE.LinearFilter,
+ magFilter: THREE.LinearFilter,
+ wrapS: THREE.ClampToEdgeWrapping,
+ wrapT: THREE.ClampToEdgeWrapping,
+ format: THREE.RGBAFormat,
+ stencilBuffer: false,
+ depthBuffer: false,
+ premultiplyAlpha: false,
+ type: renderTargetType
+ };
+ var NearestClampParams = {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ wrapS: THREE.ClampToEdgeWrapping,
+ wrapT: THREE.ClampToEdgeWrapping,
+ format: THREE.RGBAFormat,
+ stencilBuffer: false,
+ depthBuffer: false,
+ premultiplyAlpha: false,
+ type: renderTargetType
+ };
+ var NearestRepeatParams = {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ wrapS: THREE.RepeatWrapping,
+ wrapT: THREE.RepeatWrapping,
+ format: THREE.RGBAFormat,
+ stencilBuffer: false,
+ depthBuffer: false,
+ premultiplyAlpha: false,
+ type: renderTargetType
+ };
+ this.initialSpectrumFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestRepeatParams );
+ this.spectrumFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestClampParams );
+ this.pingPhaseFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestClampParams );
+ this.pongPhaseFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestClampParams );
+ this.pingTransformFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestClampParams );
+ this.pongTransformFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, NearestClampParams );
+ this.displacementMapFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, LinearClampParams );
+ this.normalMapFramebuffer = new THREE.WebGLRenderTarget( this.resolution, this.resolution, LinearClampParams );
+
+ // Define shaders and constant uniforms
+ ////////////////////////////////////////
+
+ // 0 - The vertex shader used in all of the simulation steps
+ var fullscreeenVertexShader = THREE.ShaderLib[ "ocean_sim_vertex" ];
+
+ // 1 - Horizontal wave vertices used for FFT
+ var oceanHorizontalShader = THREE.ShaderLib[ "ocean_subtransform" ];
+ var oceanHorizontalUniforms = THREE.UniformsUtils.clone( oceanHorizontalShader.uniforms );
+ this.materialOceanHorizontal = new THREE.ShaderMaterial( {
+ uniforms: oceanHorizontalUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: "#define HORIZONTAL \n" + oceanHorizontalShader.fragmentShader
+ } );
+ this.materialOceanHorizontal.uniforms.u_transformSize = { value: this.resolution };
+ this.materialOceanHorizontal.uniforms.u_subtransformSize = { value: null };
+ this.materialOceanHorizontal.uniforms.u_input = { value: null };
+ this.materialOceanHorizontal.depthTest = false;
+
+ // 2 - Vertical wave vertices used for FFT
+ var oceanVerticalShader = THREE.ShaderLib[ "ocean_subtransform" ];
+ var oceanVerticalUniforms = THREE.UniformsUtils.clone( oceanVerticalShader.uniforms );
+ this.materialOceanVertical = new THREE.ShaderMaterial( {
+ uniforms: oceanVerticalUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: oceanVerticalShader.fragmentShader
+ } );
+ this.materialOceanVertical.uniforms.u_transformSize = { value: this.resolution };
+ this.materialOceanVertical.uniforms.u_subtransformSize = { value: null };
+ this.materialOceanVertical.uniforms.u_input = { value: null };
+ this.materialOceanVertical.depthTest = false;
+
+ // 3 - Initial spectrum used to generate height map
+ var initialSpectrumShader = THREE.ShaderLib[ "ocean_initial_spectrum" ];
+ var initialSpectrumUniforms = THREE.UniformsUtils.clone( initialSpectrumShader.uniforms );
+ this.materialInitialSpectrum = new THREE.ShaderMaterial( {
+ uniforms: initialSpectrumUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: initialSpectrumShader.fragmentShader
+ } );
+ this.materialInitialSpectrum.uniforms.u_wind = { value: new THREE.Vector2() };
+ this.materialInitialSpectrum.uniforms.u_resolution = { value: this.resolution };
+ this.materialInitialSpectrum.depthTest = false;
+
+ // 4 - Phases used to animate heightmap
+ var phaseShader = THREE.ShaderLib[ "ocean_phase" ];
+ var phaseUniforms = THREE.UniformsUtils.clone( phaseShader.uniforms );
+ this.materialPhase = new THREE.ShaderMaterial( {
+ uniforms: phaseUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: phaseShader.fragmentShader
+ } );
+ this.materialPhase.uniforms.u_resolution = { value: this.resolution };
+ this.materialPhase.depthTest = false;
+
+ // 5 - Shader used to update spectrum
+ var spectrumShader = THREE.ShaderLib[ "ocean_spectrum" ];
+ var spectrumUniforms = THREE.UniformsUtils.clone( spectrumShader.uniforms );
+ this.materialSpectrum = new THREE.ShaderMaterial( {
+ uniforms: spectrumUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: spectrumShader.fragmentShader
+ } );
+ this.materialSpectrum.uniforms.u_initialSpectrum = { value: null };
+ this.materialSpectrum.uniforms.u_resolution = { value: this.resolution };
+ this.materialSpectrum.depthTest = false;
+
+ // 6 - Shader used to update spectrum normals
+ var normalShader = THREE.ShaderLib[ "ocean_normals" ];
+ var normalUniforms = THREE.UniformsUtils.clone( normalShader.uniforms );
+ this.materialNormal = new THREE.ShaderMaterial( {
+ uniforms: normalUniforms,
+ vertexShader: fullscreeenVertexShader.vertexShader,
+ fragmentShader: normalShader.fragmentShader
+ } );
+ this.materialNormal.uniforms.u_displacementMap = { value: null };
+ this.materialNormal.uniforms.u_resolution = { value: this.resolution };
+ this.materialNormal.depthTest = false;
+
+ // 7 - Shader used to update normals
+ var oceanShader = THREE.ShaderLib[ "ocean_main" ];
+ var oceanUniforms = THREE.UniformsUtils.clone( oceanShader.uniforms );
+ this.materialOcean = new THREE.ShaderMaterial( {
+ uniforms: oceanUniforms,
+ vertexShader: oceanShader.vertexShader,
+ fragmentShader: oceanShader.fragmentShader
+ } );
+ // this.materialOcean.wireframe = true;
+ this.materialOcean.uniforms.u_geometrySize = { value: this.resolution };
+ this.materialOcean.uniforms.u_displacementMap = { value: this.displacementMapFramebuffer.texture };
+ this.materialOcean.uniforms.u_normalMap = { value: this.normalMapFramebuffer.texture };
+ this.materialOcean.uniforms.u_oceanColor = { value: this.oceanColor };
+ this.materialOcean.uniforms.u_skyColor = { value: this.skyColor };
+ this.materialOcean.uniforms.u_sunDirection = { value: new THREE.Vector3( this.sunDirectionX, this.sunDirectionY, this.sunDirectionZ ) };
+ this.materialOcean.uniforms.u_exposure = { value: this.exposure };
+
+ // Disable blending to prevent default premultiplied alpha values
+ this.materialOceanHorizontal.blending = 0;
+ this.materialOceanVertical.blending = 0;
+ this.materialInitialSpectrum.blending = 0;
+ this.materialPhase.blending = 0;
+ this.materialSpectrum.blending = 0;
+ this.materialNormal.blending = 0;
+ this.materialOcean.blending = 0;
+
+ // Create the simulation plane
+ this.screenQuad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ) );
+ this.scene.add( this.screenQuad );
+
+ // Initialise spectrum data
+ this.generateSeedPhaseTexture();
+
+ // Generate the ocean mesh
+ this.generateMesh();
+
+};
+
+THREE.Ocean.prototype.generateMesh = function () {
+
+ var geometry = new THREE.PlaneBufferGeometry( this.geometrySize, this.geometrySize, this.geometryResolution, this.geometryResolution );
+
+ geometry.rotateX( - Math.PI / 2 );
+
+ this.oceanMesh = new THREE.Mesh( geometry, this.materialOcean );
+
+};
+
+THREE.Ocean.prototype.render = function () {
+
+ this.scene.overrideMaterial = null;
+
+ if ( this.changed )
+ this.renderInitialSpectrum();
+
+ this.renderWavePhase();
+ this.renderSpectrum();
+ this.renderSpectrumFFT();
+ this.renderNormalMap();
+ this.scene.overrideMaterial = null;
+
+};
+
+THREE.Ocean.prototype.generateSeedPhaseTexture = function() {
+
+ // Setup the seed texture
+ this.pingPhase = true;
+ var phaseArray = new window.Float32Array( this.resolution * this.resolution * 4 );
+ for ( var i = 0; i < this.resolution; i ++ ) {
+
+ for ( var j = 0; j < this.resolution; j ++ ) {
+
+ phaseArray[ i * this.resolution * 4 + j * 4 ] = Math.random() * 2.0 * Math.PI;
+ phaseArray[ i * this.resolution * 4 + j * 4 + 1 ] = 0.0;
+ phaseArray[ i * this.resolution * 4 + j * 4 + 2 ] = 0.0;
+ phaseArray[ i * this.resolution * 4 + j * 4 + 3 ] = 0.0;
+
+ }
+
+ }
+
+ this.pingPhaseTexture = new THREE.DataTexture( phaseArray, this.resolution, this.resolution, THREE.RGBAFormat );
+ this.pingPhaseTexture.wrapS = THREE.ClampToEdgeWrapping;
+ this.pingPhaseTexture.wrapT = THREE.ClampToEdgeWrapping;
+ this.pingPhaseTexture.type = THREE.FloatType;
+ this.pingPhaseTexture.needsUpdate = true;
+
+};
+
+THREE.Ocean.prototype.renderInitialSpectrum = function () {
+
+ this.scene.overrideMaterial = this.materialInitialSpectrum;
+ this.materialInitialSpectrum.uniforms.u_wind.value.set( this.windX, this.windY );
+ this.materialInitialSpectrum.uniforms.u_size.value = this.size;
+ this.renderer.render( this.scene, this.oceanCamera, this.initialSpectrumFramebuffer, true );
+
+};
+
+THREE.Ocean.prototype.renderWavePhase = function () {
+
+ this.scene.overrideMaterial = this.materialPhase;
+ this.screenQuad.material = this.materialPhase;
+ if ( this.initial ) {
+
+ this.materialPhase.uniforms.u_phases.value = this.pingPhaseTexture;
+ this.initial = false;
+
+ }else {
+
+ this.materialPhase.uniforms.u_phases.value = this.pingPhase ? this.pingPhaseFramebuffer.texture : this.pongPhaseFramebuffer.texture;
+
+ }
+ this.materialPhase.uniforms.u_deltaTime.value = this.deltaTime;
+ this.materialPhase.uniforms.u_size.value = this.size;
+ this.renderer.render( this.scene, this.oceanCamera, this.pingPhase ? this.pongPhaseFramebuffer : this.pingPhaseFramebuffer );
+ this.pingPhase = ! this.pingPhase;
+
+};
+
+THREE.Ocean.prototype.renderSpectrum = function () {
+
+ this.scene.overrideMaterial = this.materialSpectrum;
+ this.materialSpectrum.uniforms.u_initialSpectrum.value = this.initialSpectrumFramebuffer.texture;
+ this.materialSpectrum.uniforms.u_phases.value = this.pingPhase ? this.pingPhaseFramebuffer.texture : this.pongPhaseFramebuffer.texture;
+ this.materialSpectrum.uniforms.u_choppiness.value = this.choppiness;
+ this.materialSpectrum.uniforms.u_size.value = this.size;
+ this.renderer.render( this.scene, this.oceanCamera, this.spectrumFramebuffer );
+
+};
+
+THREE.Ocean.prototype.renderSpectrumFFT = function() {
+
+ // GPU FFT using Stockham formulation
+ var iterations = Math.log( this.resolution ) / Math.log( 2 ); // log2
+
+ this.scene.overrideMaterial = this.materialOceanHorizontal;
+
+ for ( var i = 0; i < iterations; i ++ ) {
+
+ if ( i === 0 ) {
+
+ this.materialOceanHorizontal.uniforms.u_input.value = this.spectrumFramebuffer.texture;
+ this.materialOceanHorizontal.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.pingTransformFramebuffer );
+
+ } else if ( i % 2 === 1 ) {
+
+ this.materialOceanHorizontal.uniforms.u_input.value = this.pingTransformFramebuffer.texture;
+ this.materialOceanHorizontal.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.pongTransformFramebuffer );
+
+ } else {
+
+ this.materialOceanHorizontal.uniforms.u_input.value = this.pongTransformFramebuffer.texture;
+ this.materialOceanHorizontal.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.pingTransformFramebuffer );
+
+ }
+
+ }
+ this.scene.overrideMaterial = this.materialOceanVertical;
+ for ( var i = iterations; i < iterations * 2; i ++ ) {
+
+ if ( i === iterations * 2 - 1 ) {
+
+ this.materialOceanVertical.uniforms.u_input.value = ( iterations % 2 === 0 ) ? this.pingTransformFramebuffer.texture : this.pongTransformFramebuffer.texture;
+ this.materialOceanVertical.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.displacementMapFramebuffer );
+
+ } else if ( i % 2 === 1 ) {
+
+ this.materialOceanVertical.uniforms.u_input.value = this.pingTransformFramebuffer.texture;
+ this.materialOceanVertical.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.pongTransformFramebuffer );
+
+ } else {
+
+ this.materialOceanVertical.uniforms.u_input.value = this.pongTransformFramebuffer.texture;
+ this.materialOceanVertical.uniforms.u_subtransformSize.value = Math.pow( 2, ( i % ( iterations ) ) + 1 );
+ this.renderer.render( this.scene, this.oceanCamera, this.pingTransformFramebuffer );
+
+ }
+
+ }
+
+};
+
+THREE.Ocean.prototype.renderNormalMap = function () {
+
+ this.scene.overrideMaterial = this.materialNormal;
+ if ( this.changed ) this.materialNormal.uniforms.u_size.value = this.size;
+ this.materialNormal.uniforms.u_displacementMap.value = this.displacementMapFramebuffer.texture;
+ this.renderer.render( this.scene, this.oceanCamera, this.normalMapFramebuffer, true );
+
+};
diff --git a/app/static/js/Octree.js b/app/static/js/Octree.js
new file mode 100644
index 0000000..6c9a14a
--- /dev/null
+++ b/app/static/js/Octree.js
@@ -0,0 +1,2137 @@
+/*!
+ *
+ * threeoctree.js (r60) / https://github.com/collinhover/threeoctree
+ * (sparse) dynamic 3D spatial representation structure for fast searches.
+ *
+ * @author Collin Hover / http://collinhover.com/
+ * based on Dynamic Octree by Piko3D @ http://www.piko3d.com/ and Octree by Marek Pawlowski @ pawlowski.it
+ *
+ */
+ ( function ( THREE ) {
+
+ "use strict";
+
+ /*===================================================
+
+ utility
+
+ =====================================================*/
+
+ function isNumber ( n ) {
+
+ return ! isNaN( n ) && isFinite( n );
+
+ }
+
+ function isArray ( target ) {
+
+ return Object.prototype.toString.call( target ) === '[object Array]';
+
+ }
+
+ function toArray ( target ) {
+
+ return target ? ( isArray ( target ) !== true ? [ target ] : target ) : [];
+
+ }
+
+ function indexOfValue( array, value ) {
+
+ for ( var i = 0, il = array.length; i < il; i ++ ) {
+
+ if ( array[ i ] === value ) {
+
+ return i;
+
+ }
+
+ }
+
+ return - 1;
+
+ }
+
+ function indexOfPropertyWithValue( array, property, value ) {
+
+ for ( var i = 0, il = array.length; i < il; i ++ ) {
+
+ if ( array[ i ][ property ] === value ) {
+
+ return i;
+
+ }
+
+ }
+
+ return - 1;
+
+ }
+
+ /*===================================================
+
+ octree
+
+ =====================================================*/
+
+ THREE.Octree = function ( parameters ) {
+
+ // handle parameters
+
+ parameters = parameters || {};
+
+ parameters.tree = this;
+
+ // static properties ( modification is not recommended )
+
+ this.nodeCount = 0;
+
+ this.INDEX_INSIDE_CROSS = - 1;
+ this.INDEX_OUTSIDE_OFFSET = 2;
+
+ this.INDEX_OUTSIDE_POS_X = isNumber( parameters.INDEX_OUTSIDE_POS_X ) ? parameters.INDEX_OUTSIDE_POS_X : 0;
+ this.INDEX_OUTSIDE_NEG_X = isNumber( parameters.INDEX_OUTSIDE_NEG_X ) ? parameters.INDEX_OUTSIDE_NEG_X : 1;
+ this.INDEX_OUTSIDE_POS_Y = isNumber( parameters.INDEX_OUTSIDE_POS_Y ) ? parameters.INDEX_OUTSIDE_POS_Y : 2;
+ this.INDEX_OUTSIDE_NEG_Y = isNumber( parameters.INDEX_OUTSIDE_NEG_Y ) ? parameters.INDEX_OUTSIDE_NEG_Y : 3;
+ this.INDEX_OUTSIDE_POS_Z = isNumber( parameters.INDEX_OUTSIDE_POS_Z ) ? parameters.INDEX_OUTSIDE_POS_Z : 4;
+ this.INDEX_OUTSIDE_NEG_Z = isNumber( parameters.INDEX_OUTSIDE_NEG_Z ) ? parameters.INDEX_OUTSIDE_NEG_Z : 5;
+
+ this.INDEX_OUTSIDE_MAP = [];
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_X ] = { index: this.INDEX_OUTSIDE_POS_X, count: 0, x: 1, y: 0, z: 0 };
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_X ] = { index: this.INDEX_OUTSIDE_NEG_X, count: 0, x: - 1, y: 0, z: 0 };
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_Y ] = { index: this.INDEX_OUTSIDE_POS_Y, count: 0, x: 0, y: 1, z: 0 };
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_Y ] = { index: this.INDEX_OUTSIDE_NEG_Y, count: 0, x: 0, y: - 1, z: 0 };
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_Z ] = { index: this.INDEX_OUTSIDE_POS_Z, count: 0, x: 0, y: 0, z: 1 };
+ this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_Z ] = { index: this.INDEX_OUTSIDE_NEG_Z, count: 0, x: 0, y: 0, z: - 1 };
+
+ this.FLAG_POS_X = 1 << ( this.INDEX_OUTSIDE_POS_X + 1 );
+ this.FLAG_NEG_X = 1 << ( this.INDEX_OUTSIDE_NEG_X + 1 );
+ this.FLAG_POS_Y = 1 << ( this.INDEX_OUTSIDE_POS_Y + 1 );
+ this.FLAG_NEG_Y = 1 << ( this.INDEX_OUTSIDE_NEG_Y + 1 );
+ this.FLAG_POS_Z = 1 << ( this.INDEX_OUTSIDE_POS_Z + 1 );
+ this.FLAG_NEG_Z = 1 << ( this.INDEX_OUTSIDE_NEG_Z + 1 );
+
+ this.utilVec31Search = new THREE.Vector3();
+ this.utilVec32Search = new THREE.Vector3();
+
+ // pass scene to see octree structure
+
+ this.scene = parameters.scene;
+
+ if ( this.scene ) {
+
+ this.visualGeometry = new THREE.BoxGeometry( 1, 1, 1 );
+ this.visualMaterial = new THREE.MeshBasicMaterial( { color: 0xFF0066, wireframe: true, wireframeLinewidth: 1 } );
+
+ }
+
+ // properties
+
+ this.objects = [];
+ this.objectsMap = {};
+ this.objectsData = [];
+ this.objectsDeferred = [];
+
+ this.depthMax = isNumber( parameters.depthMax ) ? parameters.depthMax : Infinity;
+ this.objectsThreshold = isNumber( parameters.objectsThreshold ) ? parameters.objectsThreshold : 8;
+ this.overlapPct = isNumber( parameters.overlapPct ) ? parameters.overlapPct : 0.15;
+ this.undeferred = parameters.undeferred || false;
+
+ this.root = parameters.root instanceof THREE.OctreeNode ? parameters.root : new THREE.OctreeNode( parameters );
+
+ };
+
+ THREE.Octree.prototype = {
+
+ update: function () {
+
+ // add any deferred objects that were waiting for render cycle
+
+ if ( this.objectsDeferred.length > 0 ) {
+
+ for ( var i = 0, il = this.objectsDeferred.length; i < il; i ++ ) {
+
+ var deferred = this.objectsDeferred[ i ];
+
+ this.addDeferred( deferred.object, deferred.options );
+
+ }
+
+ this.objectsDeferred.length = 0;
+
+ }
+
+ },
+
+ add: function ( object, options ) {
+
+ // add immediately
+
+ if ( this.undeferred ) {
+
+ this.updateObject( object );
+
+ this.addDeferred( object, options );
+
+ } else {
+
+ // defer add until update called
+
+ this.objectsDeferred.push( { object: object, options: options } );
+
+ }
+
+ },
+
+ addDeferred: function ( object, options ) {
+
+ var i, l,
+ geometry,
+ faces,
+ useFaces,
+ vertices,
+ useVertices,
+ objectData;
+
+ // ensure object is not object data
+
+ if ( object instanceof THREE.OctreeObjectData ) {
+
+ object = object.object;
+
+ }
+
+ // check uuid to avoid duplicates
+
+ if ( ! object.uuid ) {
+
+ object.uuid = THREE.Math.generateUUID();
+
+ }
+
+ if ( ! this.objectsMap[ object.uuid ] ) {
+
+ // store
+
+ this.objects.push( object );
+ this.objectsMap[ object.uuid ] = object;
+
+ // check options
+
+ if ( options ) {
+
+ useFaces = options.useFaces;
+ useVertices = options.useVertices;
+
+ }
+
+ if ( useVertices === true ) {
+
+ geometry = object.geometry;
+ vertices = geometry.vertices;
+
+ for ( i = 0, l = vertices.length; i < l; i ++ ) {
+
+ this.addObjectData( object, vertices[ i ] );
+
+ }
+
+ } else if ( useFaces === true ) {
+
+ geometry = object.geometry;
+ faces = geometry.faces;
+
+ for ( i = 0, l = faces.length; i < l; i ++ ) {
+
+ this.addObjectData( object, faces[ i ] );
+
+ }
+
+ } else {
+
+ this.addObjectData( object );
+
+ }
+
+ }
+
+ },
+
+ addObjectData: function ( object, part ) {
+
+ var objectData = new THREE.OctreeObjectData( object, part );
+
+ // add to tree objects data list
+
+ this.objectsData.push( objectData );
+
+ // add to nodes
+
+ this.root.addObject( objectData );
+
+ },
+
+ remove: function ( object ) {
+
+ var i, l,
+ objectData = object,
+ index,
+ objectsDataRemoved;
+
+ // ensure object is not object data for index search
+
+ if ( object instanceof THREE.OctreeObjectData ) {
+
+ object = object.object;
+
+ }
+
+ // check uuid
+
+ if ( this.objectsMap[ object.uuid ] ) {
+
+ this.objectsMap[ object.uuid ] = undefined;
+
+ // check and remove from objects, nodes, and data lists
+
+ index = indexOfValue( this.objects, object );
+
+ if ( index !== - 1 ) {
+
+ this.objects.splice( index, 1 );
+
+ // remove from nodes
+
+ objectsDataRemoved = this.root.removeObject( objectData );
+
+ // remove from objects data list
+
+ for ( i = 0, l = objectsDataRemoved.length; i < l; i ++ ) {
+
+ objectData = objectsDataRemoved[ i ];
+
+ index = indexOfValue( this.objectsData, objectData );
+
+ if ( index !== - 1 ) {
+
+ this.objectsData.splice( index, 1 );
+
+ }
+
+ }
+
+ }
+
+ } else if ( this.objectsDeferred.length > 0 ) {
+
+ // check and remove from deferred
+
+ index = indexOfPropertyWithValue( this.objectsDeferred, 'object', object );
+
+ if ( index !== - 1 ) {
+
+ this.objectsDeferred.splice( index, 1 );
+
+ }
+
+ }
+
+ },
+
+ extend: function ( octree ) {
+
+ var i, l,
+ objectsData,
+ objectData;
+
+ if ( octree instanceof THREE.Octree ) {
+
+ // for each object data
+
+ objectsData = octree.objectsData;
+
+ for ( i = 0, l = objectsData.length; i < l; i ++ ) {
+
+ objectData = objectsData[ i ];
+
+ this.add( objectData, { useFaces: objectData.faces, useVertices: objectData.vertices } );
+
+ }
+
+ }
+
+ },
+
+ rebuild: function () {
+
+ var i, l,
+ node,
+ object,
+ objectData,
+ indexOctant,
+ indexOctantLast,
+ objectsUpdate = [];
+
+ // check all object data for changes in position
+ // assumes all object matrices are up to date
+
+ for ( i = 0, l = this.objectsData.length; i < l; i ++ ) {
+
+ objectData = this.objectsData[ i ];
+
+ node = objectData.node;
+
+ // update object
+
+ objectData.update();
+
+ // if position has changed since last organization of object in tree
+
+ if ( node instanceof THREE.OctreeNode && ! objectData.positionLast.equals( objectData.position ) ) {
+
+ // get octant index of object within current node
+
+ indexOctantLast = objectData.indexOctant;
+
+ indexOctant = node.getOctantIndex( objectData );
+
+ // if object octant index has changed
+
+ if ( indexOctant !== indexOctantLast ) {
+
+ // add to update list
+
+ objectsUpdate.push( objectData );
+
+ }
+
+ }
+
+ }
+
+ // update changed objects
+
+ for ( i = 0, l = objectsUpdate.length; i < l; i ++ ) {
+
+ objectData = objectsUpdate[ i ];
+
+ // remove object from current node
+
+ objectData.node.removeObject( objectData );
+
+ // add object to tree root
+
+ this.root.addObject( objectData );
+
+ }
+
+ },
+
+ updateObject: function ( object ) {
+
+ var i, l,
+ parentCascade = [ object ],
+ parent,
+ parentUpdate;
+
+ // search all parents between object and root for world matrix update
+
+ parent = object.parent;
+
+ while ( parent ) {
+
+ parentCascade.push( parent );
+ parent = parent.parent;
+
+ }
+
+ for ( i = 0, l = parentCascade.length; i < l; i ++ ) {
+
+ parent = parentCascade[ i ];
+
+ if ( parent.matrixWorldNeedsUpdate === true ) {
+
+ parentUpdate = parent;
+
+ }
+
+ }
+
+ // update world matrix starting at uppermost parent that needs update
+
+ if ( typeof parentUpdate !== 'undefined' ) {
+
+ parentUpdate.updateMatrixWorld();
+
+ }
+
+ },
+
+ search: function ( position, radius, organizeByObject, direction ) {
+
+ var i, l,
+ node,
+ objects,
+ objectData,
+ object,
+ results,
+ resultData,
+ resultsObjectsIndices,
+ resultObjectIndex,
+ directionPct;
+
+ // add root objects
+
+ objects = [].concat( this.root.objects );
+
+ // ensure radius (i.e. distance of ray) is a number
+
+ if ( ! ( radius > 0 ) ) {
+
+ radius = Number.MAX_VALUE;
+
+ }
+
+ // if direction passed, normalize and find pct
+
+ if ( direction instanceof THREE.Vector3 ) {
+
+ direction = this.utilVec31Search.copy( direction ).normalize();
+ directionPct = this.utilVec32Search.set( 1, 1, 1 ).divide( direction );
+
+ }
+
+ // search each node of root
+
+ for ( i = 0, l = this.root.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.root.nodesByIndex[ this.root.nodesIndices[ i ] ];
+
+ objects = node.search( position, radius, objects, direction, directionPct );
+
+ }
+
+ // if should organize results by object
+
+ if ( organizeByObject === true ) {
+
+ results = [];
+ resultsObjectsIndices = [];
+
+ // for each object data found
+
+ for ( i = 0, l = objects.length; i < l; i ++ ) {
+
+ objectData = objects[ i ];
+ object = objectData.object;
+
+ resultObjectIndex = indexOfValue( resultsObjectsIndices, object );
+
+ // if needed, create new result data
+
+ if ( resultObjectIndex === - 1 ) {
+
+ resultData = {
+ object: object,
+ faces: [],
+ vertices: []
+ };
+
+ results.push( resultData );
+
+ resultsObjectsIndices.push( object );
+
+ } else {
+
+ resultData = results[ resultObjectIndex ];
+
+ }
+
+ // object data has faces or vertices, add to list
+
+ if ( objectData.faces ) {
+
+ resultData.faces.push( objectData.faces );
+
+ } else if ( objectData.vertices ) {
+
+ resultData.vertices.push( objectData.vertices );
+
+ }
+
+ }
+
+ } else {
+
+ results = objects;
+
+ }
+
+ return results;
+
+ },
+
+ setRoot: function ( root ) {
+
+ if ( root instanceof THREE.OctreeNode ) {
+
+ // store new root
+
+ this.root = root;
+
+ // update properties
+
+ this.root.updateProperties();
+
+ }
+
+ },
+
+ getDepthEnd: function () {
+
+ return this.root.getDepthEnd();
+
+ },
+
+ getNodeCountEnd: function () {
+
+ return this.root.getNodeCountEnd();
+
+ },
+
+ getObjectCountEnd: function () {
+
+ return this.root.getObjectCountEnd();
+
+ },
+
+ toConsole: function () {
+
+ this.root.toConsole();
+
+ }
+
+ };
+
+ /*===================================================
+
+ object data
+
+ =====================================================*/
+
+ THREE.OctreeObjectData = function ( object, part ) {
+
+ // properties
+
+ this.object = object;
+
+ // handle part by type
+
+ if ( part instanceof THREE.Face3 ) {
+
+ this.faces = part;
+ this.face3 = true;
+ this.utilVec31FaceBounds = new THREE.Vector3();
+
+ } else if ( part instanceof THREE.Vector3 ) {
+
+ this.vertices = part;
+
+ }
+
+ this.radius = 0;
+ this.position = new THREE.Vector3();
+
+ // initial update
+
+ if ( this.object instanceof THREE.Object3D ) {
+
+ this.update();
+
+ }
+
+ this.positionLast = this.position.clone();
+
+ };
+
+ THREE.OctreeObjectData.prototype = {
+
+ update: function () {
+
+ if ( this.face3 ) {
+
+ this.radius = this.getFace3BoundingRadius( this.object, this.faces );
+ this.position.copy( this.faces.centroid ).applyMatrix4( this.object.matrixWorld );
+
+ } else if ( this.vertices ) {
+
+ this.radius = this.object.material.size || 1;
+ this.position.copy( this.vertices ).applyMatrix4( this.object.matrixWorld );
+
+ } else {
+
+ if ( this.object.geometry ) {
+
+ if ( this.object.geometry.boundingSphere === null ) {
+
+ this.object.geometry.computeBoundingSphere();
+
+ }
+
+ this.radius = this.object.geometry.boundingSphere.radius;
+ this.position.copy( this.object.geometry.boundingSphere.center ).applyMatrix4( this.object.matrixWorld );
+
+ } else {
+
+ this.radius = this.object.boundRadius;
+ this.position.setFromMatrixPosition( this.object.matrixWorld );
+
+ }
+
+ }
+
+ this.radius = this.radius * Math.max( this.object.scale.x, this.object.scale.y, this.object.scale.z );
+
+ },
+
+ getFace3BoundingRadius: function ( object, face ) {
+
+ if ( face.centroid === undefined ) face.centroid = new THREE.Vector3();
+
+ var geometry = object.geometry || object,
+ vertices = geometry.vertices,
+ centroid = face.centroid,
+ va = vertices[ face.a ], vb = vertices[ face.b ], vc = vertices[ face.c ],
+ centroidToVert = this.utilVec31FaceBounds,
+ radius;
+
+ centroid.addVectors( va, vb ).add( vc ).divideScalar( 3 );
+ radius = Math.max( centroidToVert.subVectors( centroid, va ).length(), centroidToVert.subVectors( centroid, vb ).length(), centroidToVert.subVectors( centroid, vc ).length() );
+
+ return radius;
+
+ }
+
+ };
+
+ /*===================================================
+
+ node
+
+ =====================================================*/
+
+ THREE.OctreeNode = function ( parameters ) {
+
+ // utility
+
+ this.utilVec31Branch = new THREE.Vector3();
+ this.utilVec31Expand = new THREE.Vector3();
+ this.utilVec31Ray = new THREE.Vector3();
+
+ // handle parameters
+
+ parameters = parameters || {};
+
+ // store or create tree
+
+ if ( parameters.tree instanceof THREE.Octree ) {
+
+ this.tree = parameters.tree;
+
+ } else if ( parameters.parent instanceof THREE.OctreeNode !== true ) {
+
+ parameters.root = this;
+
+ this.tree = new THREE.Octree( parameters );
+
+ }
+
+ // basic properties
+
+ this.id = this.tree.nodeCount ++;
+ this.position = parameters.position instanceof THREE.Vector3 ? parameters.position : new THREE.Vector3();
+ this.radius = parameters.radius > 0 ? parameters.radius : 1;
+ this.indexOctant = parameters.indexOctant;
+ this.depth = 0;
+
+ // reset and assign parent
+
+ this.reset();
+ this.setParent( parameters.parent );
+
+ // additional properties
+
+ this.overlap = this.radius * this.tree.overlapPct;
+ this.radiusOverlap = this.radius + this.overlap;
+ this.left = this.position.x - this.radiusOverlap;
+ this.right = this.position.x + this.radiusOverlap;
+ this.bottom = this.position.y - this.radiusOverlap;
+ this.top = this.position.y + this.radiusOverlap;
+ this.back = this.position.z - this.radiusOverlap;
+ this.front = this.position.z + this.radiusOverlap;
+
+ // visual
+
+ if ( this.tree.scene ) {
+
+ this.visual = new THREE.Mesh( this.tree.visualGeometry, this.tree.visualMaterial );
+ this.visual.scale.set( this.radiusOverlap * 2, this.radiusOverlap * 2, this.radiusOverlap * 2 );
+ this.visual.position.copy( this.position );
+ this.tree.scene.add( this.visual );
+
+ }
+
+ };
+
+ THREE.OctreeNode.prototype = {
+
+ setParent: function ( parent ) {
+
+ // store new parent
+
+ if ( parent !== this && this.parent !== parent ) {
+
+ this.parent = parent;
+
+ // update properties
+
+ this.updateProperties();
+
+ }
+
+ },
+
+ updateProperties: function () {
+
+ var i, l;
+
+ // properties
+
+ if ( this.parent instanceof THREE.OctreeNode ) {
+
+ this.tree = this.parent.tree;
+ this.depth = this.parent.depth + 1;
+
+ } else {
+
+ this.depth = 0;
+
+ }
+
+ // cascade
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ this.nodesByIndex[ this.nodesIndices[ i ] ].updateProperties();
+
+ }
+
+ },
+
+ reset: function ( cascade, removeVisual ) {
+
+ var i, l,
+ node,
+ nodesIndices = this.nodesIndices || [],
+ nodesByIndex = this.nodesByIndex;
+
+ this.objects = [];
+ this.nodesIndices = [];
+ this.nodesByIndex = {};
+
+ // unset parent in nodes
+
+ for ( i = 0, l = nodesIndices.length; i < l; i ++ ) {
+
+ node = nodesByIndex[ nodesIndices[ i ] ];
+
+ node.setParent( undefined );
+
+ if ( cascade === true ) {
+
+ node.reset( cascade, removeVisual );
+
+ }
+
+ }
+
+ // visual
+
+ if ( removeVisual === true && this.visual && this.visual.parent ) {
+
+ this.visual.parent.remove( this.visual );
+
+ }
+
+ },
+
+ addNode: function ( node, indexOctant ) {
+
+ node.indexOctant = indexOctant;
+
+ if ( indexOfValue( this.nodesIndices, indexOctant ) === - 1 ) {
+
+ this.nodesIndices.push( indexOctant );
+
+ }
+
+ this.nodesByIndex[ indexOctant ] = node;
+
+ if ( node.parent !== this ) {
+
+ node.setParent( this );
+
+ }
+
+ },
+
+ removeNode: function ( indexOctant ) {
+
+ var index,
+ node;
+
+ index = indexOfValue( this.nodesIndices, indexOctant );
+
+ this.nodesIndices.splice( index, 1 );
+
+ node = node || this.nodesByIndex[ indexOctant ];
+
+ delete this.nodesByIndex[ indexOctant ];
+
+ if ( node.parent === this ) {
+
+ node.setParent( undefined );
+
+ }
+
+ },
+
+ addObject: function ( object ) {
+
+ var index,
+ indexOctant,
+ node;
+
+ // get object octant index
+
+ indexOctant = this.getOctantIndex( object );
+
+ // if object fully contained by an octant, add to subtree
+ if ( indexOctant > - 1 && this.nodesIndices.length > 0 ) {
+
+ node = this.branch( indexOctant );
+
+ node.addObject( object );
+
+ } else if ( indexOctant < - 1 && this.parent instanceof THREE.OctreeNode ) {
+
+ // if object lies outside bounds, add to parent node
+
+ this.parent.addObject( object );
+
+ } else {
+
+ // add to this objects list
+
+ index = indexOfValue( this.objects, object );
+
+ if ( index === - 1 ) {
+
+ this.objects.push( object );
+
+ }
+
+ // node reference
+
+ object.node = this;
+
+ // check if need to expand, split, or both
+
+ this.checkGrow();
+
+ }
+
+ },
+
+ addObjectWithoutCheck: function ( objects ) {
+
+ var i, l,
+ object;
+
+ for ( i = 0, l = objects.length; i < l; i ++ ) {
+
+ object = objects[ i ];
+
+ this.objects.push( object );
+
+ object.node = this;
+
+ }
+
+ },
+
+ removeObject: function ( object ) {
+
+ var i, l,
+ nodesRemovedFrom,
+ removeData;
+
+ // cascade through tree to find and remove object
+
+ removeData = this.removeObjectRecursive( object, { searchComplete: false, nodesRemovedFrom: [], objectsDataRemoved: [] } );
+
+ // if object removed, try to shrink the nodes it was removed from
+
+ nodesRemovedFrom = removeData.nodesRemovedFrom;
+
+ if ( nodesRemovedFrom.length > 0 ) {
+
+ for ( i = 0, l = nodesRemovedFrom.length; i < l; i ++ ) {
+
+ nodesRemovedFrom[ i ].shrink();
+
+ }
+
+ }
+
+ return removeData.objectsDataRemoved;
+
+ },
+
+ removeObjectRecursive: function ( object, removeData ) {
+
+ var i, l,
+ index = - 1,
+ objectData,
+ node,
+ objectRemoved;
+
+ // find index of object in objects list
+
+ // search and remove object data (fast)
+ if ( object instanceof THREE.OctreeObjectData ) {
+
+ // remove from this objects list
+
+ index = indexOfValue( this.objects, object );
+
+ if ( index !== - 1 ) {
+
+ this.objects.splice( index, 1 );
+ object.node = undefined;
+
+ removeData.objectsDataRemoved.push( object );
+
+ removeData.searchComplete = objectRemoved = true;
+
+ }
+
+ } else {
+
+ // search each object data for object and remove (slow)
+
+ for ( i = this.objects.length - 1; i >= 0; i -- ) {
+
+ objectData = this.objects[ i ];
+
+ if ( objectData.object === object ) {
+
+ this.objects.splice( i, 1 );
+ objectData.node = undefined;
+
+ removeData.objectsDataRemoved.push( objectData );
+
+ objectRemoved = true;
+
+ if ( ! objectData.faces && ! objectData.vertices ) {
+
+ removeData.searchComplete = true;
+ break;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ // if object data removed and this is not on nodes removed from
+
+ if ( objectRemoved === true ) {
+
+ removeData.nodesRemovedFrom.push( this );
+
+ }
+
+ // if search not complete, search nodes
+
+ if ( removeData.searchComplete !== true ) {
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ // try removing object from node
+
+ removeData = node.removeObjectRecursive( object, removeData );
+
+ if ( removeData.searchComplete === true ) {
+
+ break;
+
+ }
+
+ }
+
+ }
+
+ return removeData;
+
+ },
+
+ checkGrow: function () {
+
+ // if object count above max
+
+ if ( this.objects.length > this.tree.objectsThreshold && this.tree.objectsThreshold > 0 ) {
+
+ this.grow();
+
+ }
+
+ },
+
+ grow: function () {
+
+ var indexOctant,
+ object,
+ objectsExpand = [],
+ objectsExpandOctants = [],
+ objectsSplit = [],
+ objectsSplitOctants = [],
+ objectsRemaining = [],
+ i, l;
+
+ // for each object
+
+ for ( i = 0, l = this.objects.length; i < l; i ++ ) {
+
+ object = this.objects[ i ];
+
+ // get object octant index
+
+ indexOctant = this.getOctantIndex( object );
+
+ // if lies within octant
+ if ( indexOctant > - 1 ) {
+
+ objectsSplit.push( object );
+ objectsSplitOctants.push( indexOctant );
+
+ } else if ( indexOctant < - 1 ) {
+
+ // lies outside radius
+
+ objectsExpand.push( object );
+ objectsExpandOctants.push( indexOctant );
+
+ } else {
+
+ // lies across bounds between octants
+
+ objectsRemaining.push( object );
+
+ }
+
+ }
+
+ // if has objects to split
+
+ if ( objectsSplit.length > 0 ) {
+
+ objectsRemaining = objectsRemaining.concat( this.split( objectsSplit, objectsSplitOctants ) );
+
+ }
+
+ // if has objects to expand
+
+ if ( objectsExpand.length > 0 ) {
+
+ objectsRemaining = objectsRemaining.concat( this.expand( objectsExpand, objectsExpandOctants ) );
+
+ }
+
+ // store remaining
+
+ this.objects = objectsRemaining;
+
+ // merge check
+
+ this.checkMerge();
+
+ },
+
+ split: function ( objects, octants ) {
+
+ var i, l,
+ indexOctant,
+ object,
+ node,
+ objectsRemaining;
+
+ // if not at max depth
+
+ if ( this.depth < this.tree.depthMax ) {
+
+ objects = objects || this.objects;
+
+ octants = octants || [];
+
+ objectsRemaining = [];
+
+ // for each object
+
+ for ( i = 0, l = objects.length; i < l; i ++ ) {
+
+ object = objects[ i ];
+
+ // get object octant index
+
+ indexOctant = octants[ i ];
+
+ // if object contained by octant, branch this tree
+
+ if ( indexOctant > - 1 ) {
+
+ node = this.branch( indexOctant );
+
+ node.addObject( object );
+
+ } else {
+
+ objectsRemaining.push( object );
+
+ }
+
+ }
+
+ // if all objects, set remaining as new objects
+
+ if ( objects === this.objects ) {
+
+ this.objects = objectsRemaining;
+
+ }
+
+ } else {
+
+ objectsRemaining = this.objects;
+
+ }
+
+ return objectsRemaining;
+
+ },
+
+ branch: function ( indexOctant ) {
+
+ var node,
+ overlap,
+ radius,
+ radiusOffset,
+ offset,
+ position;
+
+ // node exists
+
+ if ( this.nodesByIndex[ indexOctant ] instanceof THREE.OctreeNode ) {
+
+ node = this.nodesByIndex[ indexOctant ];
+
+ } else {
+
+ // properties
+
+ radius = ( this.radiusOverlap ) * 0.5;
+ overlap = radius * this.tree.overlapPct;
+ radiusOffset = radius - overlap;
+ offset = this.utilVec31Branch.set( indexOctant & 1 ? radiusOffset : - radiusOffset, indexOctant & 2 ? radiusOffset : - radiusOffset, indexOctant & 4 ? radiusOffset : - radiusOffset );
+ position = new THREE.Vector3().addVectors( this.position, offset );
+
+ // node
+
+ node = new THREE.OctreeNode( {
+ tree: this.tree,
+ parent: this,
+ position: position,
+ radius: radius,
+ indexOctant: indexOctant
+ } );
+
+ // store
+
+ this.addNode( node, indexOctant );
+
+ }
+
+ return node;
+
+ },
+
+ expand: function ( objects, octants ) {
+
+ var i, l,
+ object,
+ objectsRemaining,
+ objectsExpand,
+ indexOctant,
+ flagsOutside,
+ indexOutside,
+ indexOctantInverse,
+ iom = this.tree.INDEX_OUTSIDE_MAP,
+ indexOutsideCounts,
+ infoIndexOutside1,
+ infoIndexOutside2,
+ infoIndexOutside3,
+ indexOutsideBitwise1,
+ indexOutsideBitwise2,
+ infoPotential1,
+ infoPotential2,
+ infoPotential3,
+ indexPotentialBitwise1,
+ indexPotentialBitwise2,
+ octantX, octantY, octantZ,
+ overlap,
+ radius,
+ radiusOffset,
+ radiusParent,
+ overlapParent,
+ offset = this.utilVec31Expand,
+ position,
+ parent;
+
+ // handle max depth down tree
+
+ if ( this.tree.root.getDepthEnd() < this.tree.depthMax ) {
+
+ objects = objects || this.objects;
+ octants = octants || [];
+
+ objectsRemaining = [];
+ objectsExpand = [];
+
+ // reset counts
+
+ for ( i = 0, l = iom.length; i < l; i ++ ) {
+
+ iom[ i ].count = 0;
+
+ }
+
+ // for all outside objects, find outside octants containing most objects
+
+ for ( i = 0, l = objects.length; i < l; i ++ ) {
+
+ object = objects[ i ];
+
+ // get object octant index
+
+ indexOctant = octants[ i ] ;
+
+ // if object outside this, include in calculations
+
+ if ( indexOctant < - 1 ) {
+
+ // convert octant index to outside flags
+
+ flagsOutside = - indexOctant - this.tree.INDEX_OUTSIDE_OFFSET;
+
+ // check against bitwise flags
+
+ // x
+
+ if ( flagsOutside & this.tree.FLAG_POS_X ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_POS_X ].count ++;
+
+ } else if ( flagsOutside & this.tree.FLAG_NEG_X ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_NEG_X ].count ++;
+
+ }
+
+ // y
+
+ if ( flagsOutside & this.tree.FLAG_POS_Y ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_POS_Y ].count ++;
+
+ } else if ( flagsOutside & this.tree.FLAG_NEG_Y ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_NEG_Y ].count ++;
+
+ }
+
+ // z
+
+ if ( flagsOutside & this.tree.FLAG_POS_Z ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_POS_Z ].count ++;
+
+ } else if ( flagsOutside & this.tree.FLAG_NEG_Z ) {
+
+ iom[ this.tree.INDEX_OUTSIDE_NEG_Z ].count ++;
+
+ }
+
+ // store in expand list
+
+ objectsExpand.push( object );
+
+ } else {
+
+ objectsRemaining.push( object );
+
+ }
+
+ }
+
+ // if objects to expand
+
+ if ( objectsExpand.length > 0 ) {
+
+ // shallow copy index outside map
+
+ indexOutsideCounts = iom.slice( 0 );
+
+ // sort outside index count so highest is first
+
+ indexOutsideCounts.sort( function ( a, b ) {
+
+ return b.count - a.count;
+
+ } );
+
+ // get highest outside indices
+
+ // first is first
+ infoIndexOutside1 = indexOutsideCounts[ 0 ];
+ indexOutsideBitwise1 = infoIndexOutside1.index | 1;
+
+ // second is ( one of next two bitwise OR 1 ) that is not opposite of ( first bitwise OR 1 )
+
+ infoPotential1 = indexOutsideCounts[ 1 ];
+ infoPotential2 = indexOutsideCounts[ 2 ];
+
+ infoIndexOutside2 = ( infoPotential1.index | 1 ) !== indexOutsideBitwise1 ? infoPotential1 : infoPotential2;
+ indexOutsideBitwise2 = infoIndexOutside2.index | 1;
+
+ // third is ( one of next three bitwise OR 1 ) that is not opposite of ( first or second bitwise OR 1 )
+
+ infoPotential1 = indexOutsideCounts[ 2 ];
+ infoPotential2 = indexOutsideCounts[ 3 ];
+ infoPotential3 = indexOutsideCounts[ 4 ];
+
+ indexPotentialBitwise1 = infoPotential1.index | 1;
+ indexPotentialBitwise2 = infoPotential2.index | 1;
+
+ infoIndexOutside3 = indexPotentialBitwise1 !== indexOutsideBitwise1 && indexPotentialBitwise1 !== indexOutsideBitwise2 ? infoPotential1 : indexPotentialBitwise2 !== indexOutsideBitwise1 && indexPotentialBitwise2 !== indexOutsideBitwise2 ? infoPotential2 : infoPotential3;
+
+ // get this octant normal based on outside octant indices
+
+ octantX = infoIndexOutside1.x + infoIndexOutside2.x + infoIndexOutside3.x;
+ octantY = infoIndexOutside1.y + infoIndexOutside2.y + infoIndexOutside3.y;
+ octantZ = infoIndexOutside1.z + infoIndexOutside2.z + infoIndexOutside3.z;
+
+ // get this octant indices based on octant normal
+
+ indexOctant = this.getOctantIndexFromPosition( octantX, octantY, octantZ );
+ indexOctantInverse = this.getOctantIndexFromPosition( - octantX, - octantY, - octantZ );
+
+ // properties
+
+ overlap = this.overlap;
+ radius = this.radius;
+
+ // radius of parent comes from reversing overlap of this, unless overlap percent is 0
+
+ radiusParent = this.tree.overlapPct > 0 ? overlap / ( ( 0.5 * this.tree.overlapPct ) * ( 1 + this.tree.overlapPct ) ) : radius * 2;
+ overlapParent = radiusParent * this.tree.overlapPct;
+
+ // parent offset is difference between radius + overlap of parent and child
+
+ radiusOffset = ( radiusParent + overlapParent ) - ( radius + overlap );
+ offset.set( indexOctant & 1 ? radiusOffset : - radiusOffset, indexOctant & 2 ? radiusOffset : - radiusOffset, indexOctant & 4 ? radiusOffset : - radiusOffset );
+ position = new THREE.Vector3().addVectors( this.position, offset );
+
+ // parent
+
+ parent = new THREE.OctreeNode( {
+ tree: this.tree,
+ position: position,
+ radius: radiusParent
+ } );
+
+ // set self as node of parent
+
+ parent.addNode( this, indexOctantInverse );
+
+ // set parent as root
+
+ this.tree.setRoot( parent );
+
+ // add all expand objects to parent
+
+ for ( i = 0, l = objectsExpand.length; i < l; i ++ ) {
+
+ this.tree.root.addObject( objectsExpand[ i ] );
+
+ }
+
+ }
+
+ // if all objects, set remaining as new objects
+
+ if ( objects === this.objects ) {
+
+ this.objects = objectsRemaining;
+
+ }
+
+ } else {
+
+ objectsRemaining = objects;
+
+ }
+
+ return objectsRemaining;
+
+ },
+
+ shrink: function () {
+
+ // merge check
+
+ this.checkMerge();
+
+ // contract check
+
+ this.tree.root.checkContract();
+
+ },
+
+ checkMerge: function () {
+
+ var nodeParent = this,
+ nodeMerge;
+
+ // traverse up tree as long as node + entire subtree's object count is under minimum
+
+ while ( nodeParent.parent instanceof THREE.OctreeNode && nodeParent.getObjectCountEnd() < this.tree.objectsThreshold ) {
+
+ nodeMerge = nodeParent;
+ nodeParent = nodeParent.parent;
+
+ }
+
+ // if parent node is not this, merge entire subtree into merge node
+
+ if ( nodeParent !== this ) {
+
+ nodeParent.merge( nodeMerge );
+
+ }
+
+ },
+
+ merge: function ( nodes ) {
+
+ var i, l,
+ j, k,
+ node;
+
+ // handle nodes
+
+ nodes = toArray( nodes );
+
+ for ( i = 0, l = nodes.length; i < l; i ++ ) {
+
+ node = nodes[ i ];
+
+ // gather node + all subtree objects
+
+ this.addObjectWithoutCheck( node.getObjectsEnd() );
+
+ // reset node + entire subtree
+
+ node.reset( true, true );
+
+ // remove node
+
+ this.removeNode( node.indexOctant, node );
+
+ }
+
+ // merge check
+
+ this.checkMerge();
+
+ },
+
+ checkContract: function () {
+
+ var i, l,
+ node,
+ nodeObjectsCount,
+ nodeHeaviest,
+ nodeHeaviestObjectsCount,
+ outsideHeaviestObjectsCount;
+
+ // find node with highest object count
+
+ if ( this.nodesIndices.length > 0 ) {
+
+ nodeHeaviestObjectsCount = 0;
+ outsideHeaviestObjectsCount = this.objects.length;
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ nodeObjectsCount = node.getObjectCountEnd();
+ outsideHeaviestObjectsCount += nodeObjectsCount;
+
+ if ( nodeHeaviest instanceof THREE.OctreeNode === false || nodeObjectsCount > nodeHeaviestObjectsCount ) {
+
+ nodeHeaviest = node;
+ nodeHeaviestObjectsCount = nodeObjectsCount;
+
+ }
+
+ }
+
+ // subtract heaviest count from outside count
+
+ outsideHeaviestObjectsCount -= nodeHeaviestObjectsCount;
+
+ // if should contract
+
+ if ( outsideHeaviestObjectsCount < this.tree.objectsThreshold && nodeHeaviest instanceof THREE.OctreeNode ) {
+
+ this.contract( nodeHeaviest );
+
+ }
+
+ }
+
+ },
+
+ contract: function ( nodeRoot ) {
+
+ var i, l,
+ node;
+
+ // handle all nodes
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ // if node is not new root
+
+ if ( node !== nodeRoot ) {
+
+ // add node + all subtree objects to root
+
+ nodeRoot.addObjectWithoutCheck( node.getObjectsEnd() );
+
+ // reset node + entire subtree
+
+ node.reset( true, true );
+
+ }
+
+ }
+
+ // add own objects to root
+
+ nodeRoot.addObjectWithoutCheck( this.objects );
+
+ // reset self
+
+ this.reset( false, true );
+
+ // set new root
+
+ this.tree.setRoot( nodeRoot );
+
+ // contract check on new root
+
+ nodeRoot.checkContract();
+
+ },
+
+ getOctantIndex: function ( objectData ) {
+
+ var i, l,
+ positionObj,
+ radiusObj,
+ position = this.position,
+ radiusOverlap = this.radiusOverlap,
+ overlap = this.overlap,
+ deltaX, deltaY, deltaZ,
+ distX, distY, distZ,
+ distance,
+ indexOctant = 0;
+
+ // handle type
+
+ if ( objectData instanceof THREE.OctreeObjectData ) {
+
+ radiusObj = objectData.radius;
+
+ positionObj = objectData.position;
+
+ // update object data position last
+
+ objectData.positionLast.copy( positionObj );
+
+ } else if ( objectData instanceof THREE.OctreeNode ) {
+
+ positionObj = objectData.position;
+
+ radiusObj = 0;
+
+ }
+
+ // find delta and distance
+
+ deltaX = positionObj.x - position.x;
+ deltaY = positionObj.y - position.y;
+ deltaZ = positionObj.z - position.z;
+
+ distX = Math.abs( deltaX );
+ distY = Math.abs( deltaY );
+ distZ = Math.abs( deltaZ );
+ distance = Math.max( distX, distY, distZ );
+
+ // if outside, use bitwise flags to indicate on which sides object is outside of
+
+ if ( distance + radiusObj > radiusOverlap ) {
+
+ // x
+
+ if ( distX + radiusObj > radiusOverlap ) {
+
+ indexOctant = indexOctant ^ ( deltaX > 0 ? this.tree.FLAG_POS_X : this.tree.FLAG_NEG_X );
+
+ }
+
+ // y
+
+ if ( distY + radiusObj > radiusOverlap ) {
+
+ indexOctant = indexOctant ^ ( deltaY > 0 ? this.tree.FLAG_POS_Y : this.tree.FLAG_NEG_Y );
+
+ }
+
+ // z
+
+ if ( distZ + radiusObj > radiusOverlap ) {
+
+ indexOctant = indexOctant ^ ( deltaZ > 0 ? this.tree.FLAG_POS_Z : this.tree.FLAG_NEG_Z );
+
+ }
+
+ objectData.indexOctant = - indexOctant - this.tree.INDEX_OUTSIDE_OFFSET;
+
+ return objectData.indexOctant;
+
+ }
+
+ // return octant index from delta xyz
+
+ if ( deltaX - radiusObj > - overlap ) {
+
+ // x right
+
+ indexOctant = indexOctant | 1;
+
+ } else if ( ! ( deltaX + radiusObj < overlap ) ) {
+
+ // x left
+
+ objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS;
+ return objectData.indexOctant;
+
+ }
+
+ if ( deltaY - radiusObj > - overlap ) {
+
+ // y right
+
+ indexOctant = indexOctant | 2;
+
+ } else if ( ! ( deltaY + radiusObj < overlap ) ) {
+
+ // y left
+
+ objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS;
+ return objectData.indexOctant;
+
+ }
+
+
+ if ( deltaZ - radiusObj > - overlap ) {
+
+ // z right
+
+ indexOctant = indexOctant | 4;
+
+ } else if ( ! ( deltaZ + radiusObj < overlap ) ) {
+
+ // z left
+
+ objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS;
+ return objectData.indexOctant;
+
+ }
+
+ objectData.indexOctant = indexOctant;
+ return objectData.indexOctant;
+
+ },
+
+ getOctantIndexFromPosition: function ( x, y, z ) {
+
+ var indexOctant = 0;
+
+ if ( x > 0 ) {
+
+ indexOctant = indexOctant | 1;
+
+ }
+
+ if ( y > 0 ) {
+
+ indexOctant = indexOctant | 2;
+
+ }
+
+ if ( z > 0 ) {
+
+ indexOctant = indexOctant | 4;
+
+ }
+
+ return indexOctant;
+
+ },
+
+ search: function ( position, radius, objects, direction, directionPct ) {
+
+ var i, l,
+ node,
+ intersects;
+
+ // test intersects by parameters
+
+ if ( direction ) {
+
+ intersects = this.intersectRay( position, direction, radius, directionPct );
+
+ } else {
+
+ intersects = this.intersectSphere( position, radius );
+
+ }
+
+ // if intersects
+
+ if ( intersects === true ) {
+
+ // gather objects
+
+ objects = objects.concat( this.objects );
+
+ // search subtree
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ objects = node.search( position, radius, objects, direction );
+
+ }
+
+ }
+
+ return objects;
+
+ },
+
+ intersectSphere: function ( position, radius ) {
+
+ var distance = radius * radius,
+ px = position.x,
+ py = position.y,
+ pz = position.z;
+
+ if ( px < this.left ) {
+
+ distance -= Math.pow( px - this.left, 2 );
+
+ } else if ( px > this.right ) {
+
+ distance -= Math.pow( px - this.right, 2 );
+
+ }
+
+ if ( py < this.bottom ) {
+
+ distance -= Math.pow( py - this.bottom, 2 );
+
+ } else if ( py > this.top ) {
+
+ distance -= Math.pow( py - this.top, 2 );
+
+ }
+
+ if ( pz < this.back ) {
+
+ distance -= Math.pow( pz - this.back, 2 );
+
+ } else if ( pz > this.front ) {
+
+ distance -= Math.pow( pz - this.front, 2 );
+
+ }
+
+ return distance >= 0;
+
+ },
+
+ intersectRay: function ( origin, direction, distance, directionPct ) {
+
+ if ( typeof directionPct === 'undefined' ) {
+
+ directionPct = this.utilVec31Ray.set( 1, 1, 1 ).divide( direction );
+
+ }
+
+ var t1 = ( this.left - origin.x ) * directionPct.x,
+ t2 = ( this.right - origin.x ) * directionPct.x,
+ t3 = ( this.bottom - origin.y ) * directionPct.y,
+ t4 = ( this.top - origin.y ) * directionPct.y,
+ t5 = ( this.back - origin.z ) * directionPct.z,
+ t6 = ( this.front - origin.z ) * directionPct.z,
+ tmax = Math.min( Math.min( Math.max( t1, t2 ), Math.max( t3, t4 ) ), Math.max( t5, t6 ) ),
+ tmin;
+
+ // ray would intersect in reverse direction, i.e. this is behind ray
+ if ( tmax < 0 ) {
+
+ return false;
+
+ }
+
+ tmin = Math.max( Math.max( Math.min( t1, t2 ), Math.min( t3, t4 ) ), Math.min( t5, t6 ) );
+
+ // if tmin > tmax or tmin > ray distance, ray doesn't intersect AABB
+ if ( tmin > tmax || tmin > distance ) {
+
+ return false;
+
+ }
+
+ return true;
+
+ },
+
+ getDepthEnd: function ( depth ) {
+
+ var i, l,
+ node;
+
+ if ( this.nodesIndices.length > 0 ) {
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ depth = node.getDepthEnd( depth );
+
+ }
+
+ } else {
+
+ depth = ! depth || this.depth > depth ? this.depth : depth;
+
+ }
+
+ return depth;
+
+ },
+
+ getNodeCountEnd: function () {
+
+ return this.tree.root.getNodeCountRecursive() + 1;
+
+ },
+
+ getNodeCountRecursive: function () {
+
+ var i, l,
+ count = this.nodesIndices.length;
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ count += this.nodesByIndex[ this.nodesIndices[ i ] ].getNodeCountRecursive();
+
+ }
+
+ return count;
+
+ },
+
+ getObjectsEnd: function ( objects ) {
+
+ var i, l,
+ node;
+
+ objects = ( objects || [] ).concat( this.objects );
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ objects = node.getObjectsEnd( objects );
+
+ }
+
+ return objects;
+
+ },
+
+ getObjectCountEnd: function () {
+
+ var i, l,
+ count = this.objects.length;
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ count += this.nodesByIndex[ this.nodesIndices[ i ] ].getObjectCountEnd();
+
+ }
+
+ return count;
+
+ },
+
+ getObjectCountStart: function () {
+
+ var count = this.objects.length,
+ parent = this.parent;
+
+ while ( parent instanceof THREE.OctreeNode ) {
+
+ count += parent.objects.length;
+ parent = parent.parent;
+
+ }
+
+ return count;
+
+ },
+
+ toConsole: function ( space ) {
+
+ var i, l,
+ node,
+ spaceAddition = ' ';
+
+ space = typeof space === 'string' ? space : spaceAddition;
+
+ console.log( ( this.parent ? space + ' octree NODE > ' : ' octree ROOT > ' ), this, ' // id: ', this.id, ' // indexOctant: ', this.indexOctant, ' // position: ', this.position.x, this.position.y, this.position.z, ' // radius: ', this.radius, ' // depth: ', this.depth );
+ console.log( ( this.parent ? space + ' ' : ' ' ), '+ objects ( ', this.objects.length, ' ) ', this.objects );
+ console.log( ( this.parent ? space + ' ' : ' ' ), '+ children ( ', this.nodesIndices.length, ' )', this.nodesIndices, this.nodesByIndex );
+
+ for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) {
+
+ node = this.nodesByIndex[ this.nodesIndices[ i ] ];
+
+ node.toConsole( space + spaceAddition );
+
+ }
+
+ }
+
+ };
+
+ /*===================================================
+
+ raycaster additional functionality
+
+ =====================================================*/
+
+ THREE.Raycaster.prototype.intersectOctreeObject = function ( object, recursive ) {
+
+ var intersects,
+ octreeObject,
+ facesAll,
+ facesSearch;
+
+ if ( object.object instanceof THREE.Object3D ) {
+
+ octreeObject = object;
+ object = octreeObject.object;
+
+ // temporarily replace object geometry's faces with octree object faces
+
+ facesSearch = octreeObject.faces;
+ facesAll = object.geometry.faces;
+
+ if ( facesSearch.length > 0 ) {
+
+ object.geometry.faces = facesSearch;
+
+ }
+
+ // intersect
+
+ intersects = this.intersectObject( object, recursive );
+
+ // revert object geometry's faces
+
+ if ( facesSearch.length > 0 ) {
+
+ object.geometry.faces = facesAll;
+
+ }
+
+ } else {
+
+ intersects = this.intersectObject( object, recursive );
+
+ }
+
+ return intersects;
+
+ };
+
+ THREE.Raycaster.prototype.intersectOctreeObjects = function ( objects, recursive ) {
+
+ var i, il,
+ intersects = [];
+
+ for ( i = 0, il = objects.length; i < il; i ++ ) {
+
+ intersects = intersects.concat( this.intersectOctreeObject( objects[ i ], recursive ) );
+
+ }
+
+ return intersects;
+
+ };
+
+}( THREE ) );
diff --git a/app/static/js/PRNG.js b/app/static/js/PRNG.js
new file mode 100644
index 0000000..ab811a8
--- /dev/null
+++ b/app/static/js/PRNG.js
@@ -0,0 +1,23 @@
+// Park-Miller-Carta Pseudo-Random Number Generator
+// https://github.com/pnitsch/BitmapData.js/blob/master/js/BitmapData.js
+
+var PRNG = function () {
+
+ this.seed = 1;
+ this.next = function() {
+
+ return ( this.gen() / 2147483647 );
+
+ };
+ this.nextRange = function( min, max ) {
+
+ return min + ( ( max - min ) * this.next() )
+
+ };
+ this.gen = function() {
+
+ return this.seed = ( this.seed * 16807 ) % 2147483647;
+
+ };
+
+};
diff --git a/app/static/js/ParametricGeometries.js b/app/static/js/ParametricGeometries.js
new file mode 100644
index 0000000..7e0a564
--- /dev/null
+++ b/app/static/js/ParametricGeometries.js
@@ -0,0 +1,275 @@
+/*
+ * @author zz85
+ *
+ * Experimenting of primitive geometry creation using Surface Parametric equations
+ *
+ */
+
+THREE.ParametricGeometries = {
+
+ klein: function ( v, u, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ u *= Math.PI;
+ v *= 2 * Math.PI;
+
+ u = u * 2;
+ var x, y, z;
+ if ( u < Math.PI ) {
+
+ x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( u ) * Math.cos( v );
+ z = - 8 * Math.sin( u ) - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( u ) * Math.cos( v );
+
+ } else {
+
+ x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( v + Math.PI );
+ z = - 8 * Math.sin( u );
+
+ }
+
+ y = - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( v );
+
+ return result.set( x, y, z );
+
+ },
+
+ plane: function ( width, height ) {
+
+ return function ( u, v, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ var x = u * width;
+ var y = 0;
+ var z = v * height;
+
+ return result.set( x, y, z );
+
+ };
+
+ },
+
+ mobius: function ( u, t, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ // flat mobius strip
+ // http://www.wolframalpha.com/input/?i=M%C3%B6bius+strip+parametric+equations&lk=1&a=ClashPrefs_*Surface.MoebiusStrip.SurfaceProperty.ParametricEquations-
+ u = u - 0.5;
+ var v = 2 * Math.PI * t;
+
+ var x, y, z;
+
+ var a = 2;
+
+ x = Math.cos( v ) * ( a + u * Math.cos( v / 2 ) );
+ y = Math.sin( v ) * ( a + u * Math.cos( v / 2 ) );
+ z = u * Math.sin( v / 2 );
+
+ return result.set( x, y, z );
+
+ },
+
+ mobius3d: function ( u, t, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ // volumetric mobius strip
+
+ u *= Math.PI;
+ t *= 2 * Math.PI;
+
+ u = u * 2;
+ var phi = u / 2;
+ var major = 2.25, a = 0.125, b = 0.65;
+
+ var x, y, z;
+
+ x = a * Math.cos( t ) * Math.cos( phi ) - b * Math.sin( t ) * Math.sin( phi );
+ z = a * Math.cos( t ) * Math.sin( phi ) + b * Math.sin( t ) * Math.cos( phi );
+ y = ( major + x ) * Math.sin( u );
+ x = ( major + x ) * Math.cos( u );
+
+ return result.set( x, y, z );
+
+ }
+
+};
+
+
+/*********************************************
+ *
+ * Parametric Replacement for TubeGeometry
+ *
+ *********************************************/
+
+THREE.ParametricGeometries.TubeGeometry = function ( path, segments, radius, segmentsRadius, closed, debug ) {
+
+ this.path = path;
+ this.segments = segments || 64;
+ this.radius = radius || 1;
+ this.segmentsRadius = segmentsRadius || 8;
+ this.closed = closed || false;
+ if ( debug ) this.debug = new THREE.Object3D();
+
+ var scope = this, numpoints = this.segments + 1;
+
+ var frames = path.computeFrenetFrames( segments, closed ),
+ tangents = frames.tangents,
+ normals = frames.normals,
+ binormals = frames.binormals;
+
+ // proxy internals
+
+ this.tangents = tangents;
+ this.normals = normals;
+ this.binormals = binormals;
+
+ var ParametricTube = function ( u, v, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ v *= 2 * Math.PI;
+
+ var i = u * ( numpoints - 1 );
+ i = Math.floor( i );
+
+ var pos = path.getPointAt( u );
+
+ var tangent = tangents[ i ];
+ var normal = normals[ i ];
+ var binormal = binormals[ i ];
+
+ if ( scope.debug ) {
+
+ scope.debug.add( new THREE.ArrowHelper( tangent, pos, radius, 0x0000ff ) );
+ scope.debug.add( new THREE.ArrowHelper( normal, pos, radius, 0xff0000 ) );
+ scope.debug.add( new THREE.ArrowHelper( binormal, pos, radius, 0x00ff00 ) );
+
+ }
+
+ var cx = - scope.radius * Math.cos( v ); // TODO: Hack: Negating it so it faces outside.
+ var cy = scope.radius * Math.sin( v );
+
+ pos.x += cx * normal.x + cy * binormal.x;
+ pos.y += cx * normal.y + cy * binormal.y;
+ pos.z += cx * normal.z + cy * binormal.z;
+
+ return result.copy( pos );
+
+ };
+
+ THREE.ParametricGeometry.call( this, ParametricTube, segments, segmentsRadius );
+
+};
+
+THREE.ParametricGeometries.TubeGeometry.prototype = Object.create( THREE.Geometry.prototype );
+THREE.ParametricGeometries.TubeGeometry.prototype.constructor = THREE.ParametricGeometries.TubeGeometry;
+
+
+/*********************************************
+ *
+ * Parametric Replacement for TorusKnotGeometry
+ *
+ *********************************************/
+THREE.ParametricGeometries.TorusKnotGeometry = function ( radius, tube, segmentsT, segmentsR, p, q ) {
+
+ this.radius = radius || 200;
+ this.tube = tube || 40;
+ this.segmentsT = segmentsT || 64;
+ this.segmentsR = segmentsR || 8;
+ this.p = p || 2;
+ this.q = q || 3;
+
+ function TorusKnotCurve() {
+
+ THREE.Curve.call( this );
+
+ }
+
+ TorusKnotCurve.prototype = Object.create( THREE.Curve.prototype );
+ TorusKnotCurve.prototype.constructor = TorusKnotCurve;
+
+ TorusKnotCurve.prototype.getPoint = function ( t ) {
+
+ t *= Math.PI * 2;
+
+ var r = 0.5;
+
+ var x = ( 1 + r * Math.cos( q * t ) ) * Math.cos( p * t );
+ var y = ( 1 + r * Math.cos( q * t ) ) * Math.sin( p * t );
+ var z = r * Math.sin( q * t );
+
+ return new THREE.Vector3( x, y, z ).multiplyScalar( radius );
+
+ };
+
+ var segments = segmentsT;
+ var radiusSegments = segmentsR;
+ var extrudePath = new TorusKnotCurve();
+
+ THREE.ParametricGeometries.TubeGeometry.call( this, extrudePath, segments, tube, radiusSegments, true, false );
+
+};
+
+THREE.ParametricGeometries.TorusKnotGeometry.prototype = Object.create( THREE.Geometry.prototype );
+THREE.ParametricGeometries.TorusKnotGeometry.prototype.constructor = THREE.ParametricGeometries.TorusKnotGeometry;
+
+
+/*********************************************
+ *
+ * Parametric Replacement for SphereGeometry
+ *
+ *********************************************/
+THREE.ParametricGeometries.SphereGeometry = function ( size, u, v ) {
+
+ function sphere( u, v, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ u *= Math.PI;
+ v *= 2 * Math.PI;
+
+ var x = size * Math.sin( u ) * Math.cos( v );
+ var y = size * Math.sin( u ) * Math.sin( v );
+ var z = size * Math.cos( u );
+
+ return result.set( x, y, z );
+
+ }
+
+ THREE.ParametricGeometry.call( this, sphere, u, v, ! true );
+
+};
+
+THREE.ParametricGeometries.SphereGeometry.prototype = Object.create( THREE.Geometry.prototype );
+THREE.ParametricGeometries.SphereGeometry.prototype.constructor = THREE.ParametricGeometries.SphereGeometry;
+
+
+/*********************************************
+ *
+ * Parametric Replacement for PlaneGeometry
+ *
+ *********************************************/
+
+THREE.ParametricGeometries.PlaneGeometry = function ( width, depth, segmentsWidth, segmentsDepth ) {
+
+ function plane( u, v, optionalTarget ) {
+
+ var result = optionalTarget || new THREE.Vector3();
+
+ var x = u * width;
+ var y = 0;
+ var z = v * depth;
+
+ return result.set( x, y, z );
+
+ }
+
+ THREE.ParametricGeometry.call( this, plane, segmentsWidth, segmentsDepth );
+
+};
+
+THREE.ParametricGeometries.PlaneGeometry.prototype = Object.create( THREE.Geometry.prototype );
+THREE.ParametricGeometries.PlaneGeometry.prototype.constructor = THREE.ParametricGeometries.PlaneGeometry;
diff --git a/app/static/js/Player.js b/app/static/js/Player.js
new file mode 100644
index 0000000..a6fea75
--- /dev/null
+++ b/app/static/js/Player.js
@@ -0,0 +1,46 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Player = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setId( 'player' );
+ container.setPosition( 'absolute' );
+ container.setDisplay( 'none' );
+
+ //
+
+ var player = new APP.Player();
+ container.dom.appendChild( player.dom );
+
+ window.addEventListener( 'resize', function () {
+
+ player.setSize( container.dom.clientWidth, container.dom.clientHeight );
+
+ } );
+
+ signals.startPlayer.add( function () {
+
+ container.setDisplay( '' );
+
+ player.load( editor.toJSON() );
+ player.setSize( container.dom.clientWidth, container.dom.clientHeight );
+ player.play();
+
+ } );
+
+ signals.stopPlayer.add( function () {
+
+ container.setDisplay( 'none' );
+
+ player.stop();
+ player.dispose();
+
+ } );
+
+ return container;
+
+};
diff --git a/app/static/js/QuickHull.js b/app/static/js/QuickHull.js
new file mode 100644
index 0000000..3f4b655
--- /dev/null
+++ b/app/static/js/QuickHull.js
@@ -0,0 +1,1217 @@
+/**
+ * @author Mugen87 / https://github.com/Mugen87
+ *
+ * Ported from: https://github.com/maurizzzio/quickhull3d/ by Mauricio Poppe (https://github.com/maurizzzio)
+ *
+ */
+
+( function () {
+
+ var Visible = 0;
+ var Deleted = 1;
+
+ function QuickHull() {
+
+ this.tolerance = - 1;
+
+ this.faces = []; // the generated faces of the convex hull
+ this.newFaces = []; // this array holds the faces that are generated within a single iteration
+
+ // the vertex lists work as follows:
+ //
+ // let 'a' and 'b' be 'Face' instances
+ // let 'v' be points wrapped as instance of 'Vertex'
+ //
+ // [v, v, ..., v, v, v, ...]
+ // ^ ^
+ // | |
+ // a.outside b.outside
+ //
+ this.assigned = new VertexList();
+ this.unassigned = new VertexList();
+
+ this.vertices = []; // vertices of the hull (internal representation of given geometry data)
+
+ }
+
+ Object.assign( QuickHull.prototype, {
+
+ setFromPoints: function ( points ) {
+
+ if ( Array.isArray( points ) !== true ) {
+
+ console.error( 'THREE.QuickHull: Points parameter is not an array.' );
+
+ }
+
+ if ( points.length < 4 ) {
+
+ console.error( 'THREE.QuickHull: The algorithm needs at least four points.' );
+
+ }
+
+ this.makeEmpty();
+
+ for ( var i = 0, l = points.length; i < l; i ++ ) {
+
+ this.vertices.push( new VertexNode( points[ i ] ) );
+
+ }
+
+ this.compute();
+
+ return this;
+
+ },
+
+ setFromObject: function ( object ) {
+
+ var points = [];
+
+ object.updateMatrixWorld( true );
+
+ object.traverse( function ( node ) {
+
+ var i, l, point;
+
+ var geometry = node.geometry;
+
+ if ( geometry !== undefined ) {
+
+ if ( geometry.isGeometry ) {
+
+ var vertices = geometry.vertices;
+
+ for ( i = 0, l = vertices.length; i < l; i ++ ) {
+
+ point = vertices[ i ].clone();
+ point.applyMatrix4( node.matrixWorld );
+
+ points.push( point );
+
+ }
+
+ } else if ( geometry.isBufferGeometry ) {
+
+ var attribute = geometry.attributes.position;
+
+ if ( attribute !== undefined ) {
+
+ for ( i = 0, l = attribute.count; i < l; i ++ ) {
+
+ point = new THREE.Vector3();
+
+ point.fromBufferAttribute( attribute, i ).applyMatrix4( node.matrixWorld );
+
+ points.push( point );
+
+ }
+
+ }
+
+ }
+
+ }
+
+ } );
+
+ return this.setFromPoints( points );
+
+ },
+
+ makeEmpty: function () {
+
+ this.faces = [];
+ this.vertices = [];
+
+ return this;
+
+ },
+
+ // Adds a vertex to the 'assigned' list of vertices and assigns it to the given face
+
+ addVertexToFace: function ( vertex, face ) {
+
+ vertex.face = face;
+
+ if ( face.outside === null ) {
+
+ this.assigned.append( vertex );
+
+ } else {
+
+ this.assigned.insertBefore( face.outside, vertex );
+
+ }
+
+ face.outside = vertex;
+
+ return this;
+
+ },
+
+ // Removes a vertex from the 'assigned' list of vertices and from the given face
+
+ removeVertexFromFace: function ( vertex, face ) {
+
+ if ( vertex === face.outside ) {
+
+ // fix face.outside link
+
+ if ( vertex.next !== null && vertex.next.face === face ) {
+
+ // face has at least 2 outside vertices, move the 'outside' reference
+
+ face.outside = vertex.next;
+
+ } else {
+
+ // vertex was the only outside vertex that face had
+
+ face.outside = null;
+
+ }
+
+ }
+
+ this.assigned.remove( vertex );
+
+ return this;
+
+ },
+
+ // Removes all the visible vertices that a given face is able to see which are stored in the 'assigned' vertext list
+
+ removeAllVerticesFromFace: function ( face ) {
+
+ if ( face.outside !== null ) {
+
+ // reference to the first and last vertex of this face
+
+ var start = face.outside;
+ var end = face.outside;
+
+ while ( end.next !== null && end.next.face === face ) {
+
+ end = end.next;
+
+ }
+
+ this.assigned.removeSubList( start, end );
+
+ // fix references
+
+ start.prev = end.next = null;
+ face.outside = null;
+
+ return start;
+
+ }
+
+ },
+
+ // Removes all the visible vertices that 'face' is able to see
+
+ deleteFaceVertices: function ( face, absorbingFace ) {
+
+ var faceVertices = this.removeAllVerticesFromFace( face );
+
+ if ( faceVertices !== undefined ) {
+
+ if ( absorbingFace === undefined ) {
+
+ // mark the vertices to be reassigned to some other face
+
+ this.unassigned.appendChain( faceVertices );
+
+
+ } else {
+
+ // if there's an absorbing face try to assign as many vertices as possible to it
+
+ var vertex = faceVertices;
+
+ do {
+
+ // we need to buffer the subsequent vertex at this point because the 'vertex.next' reference
+ // will be changed by upcoming method calls
+
+ var nextVertex = vertex.next;
+
+ var distance = absorbingFace.distanceToPoint( vertex.point );
+
+ // check if 'vertex' is able to see 'absorbingFace'
+
+ if ( distance > this.tolerance ) {
+
+ this.addVertexToFace( vertex, absorbingFace );
+
+ } else {
+
+ this.unassigned.append( vertex );
+
+ }
+
+ // now assign next vertex
+
+ vertex = nextVertex;
+
+ } while ( vertex !== null );
+
+ }
+
+ }
+
+ return this;
+
+ },
+
+ // Reassigns as many vertices as possible from the unassigned list to the new faces
+
+ resolveUnassignedPoints: function ( newFaces ) {
+
+ if ( this.unassigned.isEmpty() === false ) {
+
+ var vertex = this.unassigned.first();
+
+ do {
+
+ // buffer 'next' reference, see .deleteFaceVertices()
+
+ var nextVertex = vertex.next;
+
+ var maxDistance = this.tolerance;
+
+ var maxFace = null;
+
+ for ( var i = 0; i < newFaces.length; i ++ ) {
+
+ var face = newFaces[ i ];
+
+ if ( face.mark === Visible ) {
+
+ var distance = face.distanceToPoint( vertex.point );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ maxFace = face;
+
+ }
+
+ if ( maxDistance > 1000 * this.tolerance ) break;
+
+ }
+
+ }
+
+ // 'maxFace' can be null e.g. if there are identical vertices
+
+ if ( maxFace !== null ) {
+
+ this.addVertexToFace( vertex, maxFace );
+
+ }
+
+ vertex = nextVertex;
+
+ } while ( vertex !== null );
+
+ }
+
+ return this;
+
+ },
+
+ // Computes the extremes of a simplex which will be the initial hull
+
+ computeExtremes: function () {
+
+ var min = new THREE.Vector3();
+ var max = new THREE.Vector3();
+
+ var minVertices = [];
+ var maxVertices = [];
+
+ var i, l, j;
+
+ // initially assume that the first vertex is the min/max
+
+ for ( i = 0; i < 3; i ++ ) {
+
+ minVertices[ i ] = maxVertices[ i ] = this.vertices[ 0 ];
+
+ }
+
+ min.copy( this.vertices[ 0 ].point );
+ max.copy( this.vertices[ 0 ].point );
+
+ // compute the min/max vertex on all six directions
+
+ for ( i = 0, l = this.vertices.length; i < l; i ++ ) {
+
+ var vertex = this.vertices[ i ];
+ var point = vertex.point;
+
+ // update the min coordinates
+
+ for ( j = 0; j < 3; j ++ ) {
+
+ if ( point.getComponent( j ) < min.getComponent( j ) ) {
+
+ min.setComponent( j, point.getComponent( j ) );
+ minVertices[ j ] = vertex;
+
+ }
+
+ }
+
+ // update the max coordinates
+
+ for ( j = 0; j < 3; j ++ ) {
+
+ if ( point.getComponent( j ) > max.getComponent( j ) ) {
+
+ max.setComponent( j, point.getComponent( j ) );
+ maxVertices[ j ] = vertex;
+
+ }
+
+ }
+
+ }
+
+ // use min/max vectors to compute an optimal epsilon
+
+ this.tolerance = 3 * Number.EPSILON * (
+ Math.max( Math.abs( min.x ), Math.abs( max.x ) ) +
+ Math.max( Math.abs( min.y ), Math.abs( max.y ) ) +
+ Math.max( Math.abs( min.z ), Math.abs( max.z ) )
+ );
+
+ return { min: minVertices, max: maxVertices };
+
+ },
+
+ // Computes the initial simplex assigning to its faces all the points
+ // that are candidates to form part of the hull
+
+ computeInitialHull: function () {
+
+ var line3, plane, closestPoint;
+
+ return function computeInitialHull() {
+
+ if ( line3 === undefined ) {
+
+ line3 = new THREE.Line3();
+ plane = new THREE.Plane();
+ closestPoint = new THREE.Vector3();
+
+ }
+
+ var vertex, vertices = this.vertices;
+ var extremes = this.computeExtremes();
+ var min = extremes.min;
+ var max = extremes.max;
+
+ var v0, v1, v2, v3;
+ var i, l, j;
+
+ // 1. Find the two vertices 'v0' and 'v1' with the greatest 1d separation
+ // (max.x - min.x)
+ // (max.y - min.y)
+ // (max.z - min.z)
+
+ var distance, maxDistance = 0;
+ var index = 0;
+
+ for ( i = 0; i < 3; i ++ ) {
+
+ distance = max[ i ].point.getComponent( i ) - min[ i ].point.getComponent( i );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ index = i;
+
+ }
+
+ }
+
+ v0 = min[ index ];
+ v1 = max[ index ];
+
+ // 2. The next vertex 'v2' is the one farthest to the line formed by 'v0' and 'v1'
+
+ maxDistance = 0;
+ line3.set( v0.point, v1.point );
+
+ for ( i = 0, l = this.vertices.length; i < l; i ++ ) {
+
+ vertex = vertices[ i ];
+
+ if ( vertex !== v0 && vertex !== v1 ) {
+
+ line3.closestPointToPoint( vertex.point, true, closestPoint );
+
+ distance = closestPoint.distanceToSquared( vertex.point );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ v2 = vertex;
+
+ }
+
+ }
+
+ }
+
+ // 3. The next vertex 'v3' is the one farthest to the plane 'v0', 'v1', 'v2'
+
+ maxDistance = 0;
+ plane.setFromCoplanarPoints( v0.point, v1.point, v2.point );
+
+ for ( i = 0, l = this.vertices.length; i < l; i ++ ) {
+
+ vertex = vertices[ i ];
+
+ if ( vertex !== v0 && vertex !== v1 && vertex !== v2 ) {
+
+ distance = Math.abs( plane.distanceToPoint( vertex.point ) );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ v3 = vertex;
+
+ }
+
+ }
+
+ }
+
+ var faces = [];
+
+ if ( plane.distanceToPoint( v3.point ) < 0 ) {
+
+ // the face is not able to see the point so 'plane.normal' is pointing outside the tetrahedron
+
+ faces.push(
+ Face.create( v0, v1, v2 ),
+ Face.create( v3, v1, v0 ),
+ Face.create( v3, v2, v1 ),
+ Face.create( v3, v0, v2 )
+ );
+
+ // set the twin edge
+
+ for ( i = 0; i < 3; i ++ ) {
+
+ j = ( i + 1 ) % 3;
+
+ // join face[ i ] i > 0, with the first face
+
+ faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( j ) );
+
+ // join face[ i ] with face[ i + 1 ], 1 <= i <= 3
+
+ faces[ i + 1 ].getEdge( 1 ).setTwin( faces[ j + 1 ].getEdge( 0 ) );
+
+ }
+
+ } else {
+
+ // the face is able to see the point so 'plane.normal' is pointing inside the tetrahedron
+
+ faces.push(
+ Face.create( v0, v2, v1 ),
+ Face.create( v3, v0, v1 ),
+ Face.create( v3, v1, v2 ),
+ Face.create( v3, v2, v0 )
+ );
+
+ // set the twin edge
+
+ for ( i = 0; i < 3; i ++ ) {
+
+ j = ( i + 1 ) % 3;
+
+ // join face[ i ] i > 0, with the first face
+
+ faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( ( 3 - i ) % 3 ) );
+
+ // join face[ i ] with face[ i + 1 ]
+
+ faces[ i + 1 ].getEdge( 0 ).setTwin( faces[ j + 1 ].getEdge( 1 ) );
+
+ }
+
+ }
+
+ // the initial hull is the tetrahedron
+
+ for ( i = 0; i < 4; i ++ ) {
+
+ this.faces.push( faces[ i ] );
+
+ }
+
+ // initial assignment of vertices to the faces of the tetrahedron
+
+ for ( i = 0, l = vertices.length; i < l; i ++ ) {
+
+ vertex = vertices[ i ];
+
+ if ( vertex !== v0 && vertex !== v1 && vertex !== v2 && vertex !== v3 ) {
+
+ maxDistance = this.tolerance;
+ var maxFace = null;
+
+ for ( j = 0; j < 4; j ++ ) {
+
+ distance = this.faces[ j ].distanceToPoint( vertex.point );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ maxFace = this.faces[ j ];
+
+ }
+
+ }
+
+ if ( maxFace !== null ) {
+
+ this.addVertexToFace( vertex, maxFace );
+
+ }
+
+ }
+
+ }
+
+ return this;
+
+ };
+
+ }(),
+
+ // Removes inactive faces
+
+ reindexFaces: function () {
+
+ var activeFaces = [];
+
+ for ( var i = 0; i < this.faces.length; i ++ ) {
+
+ var face = this.faces[ i ];
+
+ if ( face.mark === Visible ) {
+
+ activeFaces.push( face );
+
+ }
+
+ }
+
+ this.faces = activeFaces;
+
+ return this;
+
+ },
+
+ // Finds the next vertex to create faces with the current hull
+
+ nextVertexToAdd: function () {
+
+ // if the 'assigned' list of vertices is empty, no vertices are left. return with 'undefined'
+
+ if ( this.assigned.isEmpty() === false ) {
+
+ var eyeVertex, maxDistance = 0;
+
+ // grap the first available face and start with the first visible vertex of that face
+
+ var eyeFace = this.assigned.first().face;
+ var vertex = eyeFace.outside;
+
+ // now calculate the farthest vertex that face can see
+
+ do {
+
+ var distance = eyeFace.distanceToPoint( vertex.point );
+
+ if ( distance > maxDistance ) {
+
+ maxDistance = distance;
+ eyeVertex = vertex;
+
+ }
+
+ vertex = vertex.next;
+
+ } while ( vertex !== null && vertex.face === eyeFace );
+
+ return eyeVertex;
+
+ }
+
+ },
+
+ // Computes a chain of half edges in CCW order called the 'horizon'.
+ // For an edge to be part of the horizon it must join a face that can see
+ // 'eyePoint' and a face that cannot see 'eyePoint'.
+
+ computeHorizon: function ( eyePoint, crossEdge, face, horizon ) {
+
+ // moves face's vertices to the 'unassigned' vertex list
+
+ this.deleteFaceVertices( face );
+
+ face.mark = Deleted;
+
+ var edge;
+
+ if ( crossEdge === null ) {
+
+ edge = crossEdge = face.getEdge( 0 );
+
+ } else {
+
+ // start from the next edge since 'crossEdge' was already analyzed
+ // (actually 'crossEdge.twin' was the edge who called this method recursively)
+
+ edge = crossEdge.next;
+
+ }
+
+ do {
+
+ var twinEdge = edge.twin;
+ var oppositeFace = twinEdge.face;
+
+ if ( oppositeFace.mark === Visible ) {
+
+ if ( oppositeFace.distanceToPoint( eyePoint ) > this.tolerance ) {
+
+ // the opposite face can see the vertex, so proceed with next edge
+
+ this.computeHorizon( eyePoint, twinEdge, oppositeFace, horizon );
+
+ } else {
+
+ // the opposite face can't see the vertex, so this edge is part of the horizon
+
+ horizon.push( edge );
+
+ }
+
+ }
+
+ edge = edge.next;
+
+ } while ( edge !== crossEdge );
+
+ return this;
+
+ },
+
+ // Creates a face with the vertices 'eyeVertex.point', 'horizonEdge.tail' and 'horizonEdge.head' in CCW order
+
+ addAdjoiningFace: function ( eyeVertex, horizonEdge ) {
+
+ // all the half edges are created in ccw order thus the face is always pointing outside the hull
+
+ var face = Face.create( eyeVertex, horizonEdge.tail(), horizonEdge.head() );
+
+ this.faces.push( face );
+
+ // join face.getEdge( - 1 ) with the horizon's opposite edge face.getEdge( - 1 ) = face.getEdge( 2 )
+
+ face.getEdge( - 1 ).setTwin( horizonEdge.twin );
+
+ return face.getEdge( 0 ); // the half edge whose vertex is the eyeVertex
+
+
+ },
+
+ // Adds 'horizon.length' faces to the hull, each face will be linked with the
+ // horizon opposite face and the face on the left/right
+
+ addNewFaces: function ( eyeVertex, horizon ) {
+
+ this.newFaces = [];
+
+ var firstSideEdge = null;
+ var previousSideEdge = null;
+
+ for ( var i = 0; i < horizon.length; i ++ ) {
+
+ var horizonEdge = horizon[ i ];
+
+ // returns the right side edge
+
+ var sideEdge = this.addAdjoiningFace( eyeVertex, horizonEdge );
+
+ if ( firstSideEdge === null ) {
+
+ firstSideEdge = sideEdge;
+
+ } else {
+
+ // joins face.getEdge( 1 ) with previousFace.getEdge( 0 )
+
+ sideEdge.next.setTwin( previousSideEdge );
+
+ }
+
+ this.newFaces.push( sideEdge.face );
+ previousSideEdge = sideEdge;
+
+ }
+
+ // perform final join of new faces
+
+ firstSideEdge.next.setTwin( previousSideEdge );
+
+ return this;
+
+ },
+
+ // Adds a vertex to the hull
+
+ addVertexToHull: function ( eyeVertex ) {
+
+ var horizon = [];
+
+ this.unassigned.clear();
+
+ // remove 'eyeVertex' from 'eyeVertex.face' so that it can't be added to the 'unassigned' vertex list
+
+ this.removeVertexFromFace( eyeVertex, eyeVertex.face );
+
+ this.computeHorizon( eyeVertex.point, null, eyeVertex.face, horizon );
+
+ this.addNewFaces( eyeVertex, horizon );
+
+ // reassign 'unassigned' vertices to the new faces
+
+ this.resolveUnassignedPoints( this.newFaces );
+
+ return this;
+
+ },
+
+ cleanup: function () {
+
+ this.assigned.clear();
+ this.unassigned.clear();
+ this.newFaces = [];
+
+ return this;
+
+ },
+
+ compute: function () {
+
+ var vertex;
+
+ this.computeInitialHull();
+
+ // add all available vertices gradually to the hull
+
+ while ( ( vertex = this.nextVertexToAdd() ) !== undefined ) {
+
+ this.addVertexToHull( vertex );
+
+ }
+
+ this.reindexFaces();
+
+ this.cleanup();
+
+ return this;
+
+ }
+
+ } );
+
+ //
+
+ function Face() {
+
+ this.normal = new THREE.Vector3();
+ this.midpoint = new THREE.Vector3();
+ this.area = 0;
+
+ this.constant = 0; // signed distance from face to the origin
+ this.outside = null; // reference to a vertex in a vertex list this face can see
+ this.mark = Visible;
+ this.edge = null;
+
+ }
+
+ Object.assign( Face, {
+
+ create: function ( a, b, c ) {
+
+ var face = new Face();
+
+ var e0 = new HalfEdge( a, face );
+ var e1 = new HalfEdge( b, face );
+ var e2 = new HalfEdge( c, face );
+
+ // join edges
+
+ e0.next = e2.prev = e1;
+ e1.next = e0.prev = e2;
+ e2.next = e1.prev = e0;
+
+ // main half edge reference
+
+ face.edge = e0;
+
+ return face.compute();
+
+ }
+
+ } );
+
+ Object.assign( Face.prototype, {
+
+ getEdge: function ( i ) {
+
+ var edge = this.edge;
+
+ while ( i > 0 ) {
+
+ edge = edge.next;
+ i --;
+
+ }
+
+ while ( i < 0 ) {
+
+ edge = edge.prev;
+ i ++;
+
+ }
+
+ return edge;
+
+ },
+
+ compute: function () {
+
+ var triangle;
+
+ return function compute() {
+
+ if ( triangle === undefined ) triangle = new THREE.Triangle();
+
+ var a = this.edge.tail();
+ var b = this.edge.head();
+ var c = this.edge.next.head();
+
+ triangle.set( a.point, b.point, c.point );
+
+ triangle.normal( this.normal );
+ triangle.midpoint( this.midpoint );
+ this.area = triangle.area();
+
+ this.constant = this.normal.dot( this.midpoint );
+
+ return this;
+
+ };
+
+ }(),
+
+ distanceToPoint: function ( point ) {
+
+ return this.normal.dot( point ) - this.constant;
+
+ }
+
+ } );
+
+ // Entity for a Doubly-Connected Edge List (DCEL).
+
+ function HalfEdge( vertex, face ) {
+
+ this.vertex = vertex;
+ this.prev = null;
+ this.next = null;
+ this.twin = null;
+ this.face = face;
+
+ }
+
+ Object.assign( HalfEdge.prototype, {
+
+ head: function () {
+
+ return this.vertex;
+
+ },
+
+ tail: function () {
+
+ return this.prev ? this.prev.vertex : null;
+
+ },
+
+ length: function () {
+
+ var head = this.head();
+ var tail = this.tail();
+
+ if ( tail !== null ) {
+
+ return tail.point.distanceTo( head.point );
+
+ }
+
+ return - 1;
+
+ },
+
+ lengthSquared: function () {
+
+ var head = this.head();
+ var tail = this.tail();
+
+ if ( tail !== null ) {
+
+ return tail.point.distanceToSquared( head.point );
+
+ }
+
+ return - 1;
+
+ },
+
+ setTwin: function ( edge ) {
+
+ this.twin = edge;
+ edge.twin = this;
+
+ return this;
+
+ }
+
+ } );
+
+ // A vertex as a double linked list node.
+
+ function VertexNode( point ) {
+
+ this.point = point;
+ this.prev = null;
+ this.next = null;
+ this.face = null; // the face that is able to see this vertex
+
+ }
+
+ // A double linked list that contains vertex nodes.
+
+ function VertexList() {
+
+ this.head = null;
+ this.tail = null;
+
+ }
+
+ Object.assign( VertexList.prototype, {
+
+ first: function () {
+
+ return this.head;
+
+ },
+
+ last: function () {
+
+ return this.tail;
+
+ },
+
+ clear: function () {
+
+ this.head = this.tail = null;
+
+ return this;
+
+ },
+
+ // Inserts a vertex before the target vertex
+
+ insertBefore: function ( target, vertex ) {
+
+ vertex.prev = target.prev;
+ vertex.next = target;
+
+ if ( vertex.prev === null ) {
+
+ this.head = vertex;
+
+ } else {
+
+ vertex.prev.next = vertex;
+
+ }
+
+ target.prev = vertex;
+
+ return this;
+
+ },
+
+ // Inserts a vertex after the target vertex
+
+ insertAfter: function ( target, vertex ) {
+
+ vertex.prev = target;
+ vertex.next = target.next;
+
+ if ( vertex.next === null ) {
+
+ this.tail = vertex;
+
+ } else {
+
+ vertex.next.prev = vertex;
+
+ }
+
+ target.next = vertex;
+
+ return this;
+
+ },
+
+ // Appends a vertex to the end of the linked list
+
+ append: function ( vertex ) {
+
+ if ( this.head === null ) {
+
+ this.head = vertex;
+
+ } else {
+
+ this.tail.next = vertex;
+
+ }
+
+ vertex.prev = this.tail;
+ vertex.next = null; // the tail has no subsequent vertex
+
+ this.tail = vertex;
+
+ return this;
+
+ },
+
+ // Appends a chain of vertices where 'vertex' is the head.
+
+ appendChain: function ( vertex ) {
+
+ if ( this.head === null ) {
+
+ this.head = vertex;
+
+ } else {
+
+ this.tail.next = vertex;
+
+ }
+
+ vertex.prev = this.tail;
+
+ // ensure that the 'tail' reference points to the last vertex of the chain
+
+ while ( vertex.next !== null ) {
+
+ vertex = vertex.next;
+
+ }
+
+ this.tail = vertex;
+
+ return this;
+
+ },
+
+ // Removes a vertex from the linked list
+
+ remove: function ( vertex ) {
+
+ if ( vertex.prev === null ) {
+
+ this.head = vertex.next;
+
+ } else {
+
+ vertex.prev.next = vertex.next;
+
+ }
+
+ if ( vertex.next === null ) {
+
+ this.tail = vertex.prev;
+
+ } else {
+
+ vertex.next.prev = vertex.prev;
+
+ }
+
+ return this;
+
+ },
+
+ // Removes a list of vertices whose 'head' is 'a' and whose 'tail' is b
+
+ removeSubList: function ( a, b ) {
+
+ if ( a.prev === null ) {
+
+ this.head = b.next;
+
+ } else {
+
+ a.prev.next = b.next;
+
+ }
+
+ if ( b.next === null ) {
+
+ this.tail = a.prev;
+
+ } else {
+
+ b.next.prev = a.prev;
+
+ }
+
+ return this;
+
+ },
+
+ isEmpty: function () {
+
+ return this.head === null;
+
+ }
+
+ } );
+
+ // export
+
+ THREE.QuickHull = QuickHull;
+
+
+} )();
diff --git a/app/static/js/RollerCoaster.js b/app/static/js/RollerCoaster.js
new file mode 100644
index 0000000..197cc00
--- /dev/null
+++ b/app/static/js/RollerCoaster.js
@@ -0,0 +1,544 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+function RollerCoasterGeometry( curve, divisions ) {
+
+ THREE.BufferGeometry.call( this );
+
+ var vertices = [];
+ var normals = [];
+ var colors = [];
+
+ var color1 = [ 1, 1, 1 ];
+ var color2 = [ 1, 1, 0 ];
+
+ var up = new THREE.Vector3( 0, 1, 0 );
+ var forward = new THREE.Vector3();
+ var right = new THREE.Vector3();
+
+ var quaternion = new THREE.Quaternion();
+ var prevQuaternion = new THREE.Quaternion();
+ prevQuaternion.setFromAxisAngle( up , Math.PI / 2 );
+
+ var point = new THREE.Vector3();
+ var prevPoint = new THREE.Vector3();
+ prevPoint.copy( curve.getPointAt( 0 ) );
+
+ // shapes
+
+ var step = [
+ new THREE.Vector3( -0.225, 0, 0 ),
+ new THREE.Vector3( 0, -0.050, 0 ),
+ new THREE.Vector3( 0, -0.175, 0 ),
+
+ new THREE.Vector3( 0, -0.050, 0 ),
+ new THREE.Vector3( 0.225, 0, 0 ),
+ new THREE.Vector3( 0, -0.175, 0 )
+ ];
+
+ var PI2 = Math.PI * 2;
+
+ var sides = 5;
+ var tube1 = [];
+
+ for ( var i = 0; i < sides; i ++ ) {
+
+ var angle = ( i / sides ) * PI2;
+ tube1.push( new THREE.Vector3( Math.sin( angle ) * 0.06, Math.cos( angle ) * 0.06, 0 ) );
+
+ }
+
+ var sides = 6;
+ var tube2 = [];
+
+ for ( var i = 0; i < sides; i ++ ) {
+
+ var angle = ( i / sides ) * PI2;
+ tube2.push( new THREE.Vector3( Math.sin( angle ) * 0.025, Math.cos( angle ) * 0.025, 0 ) );
+
+ }
+
+ var vector = new THREE.Vector3();
+ var normal = new THREE.Vector3();
+
+ function drawShape( shape, color ) {
+
+ normal.set( 0, 0, -1 ).applyQuaternion( quaternion );
+
+ for ( var j = 0; j < shape.length; j ++ ) {
+
+ vector.copy( shape[ j ] );
+ vector.applyQuaternion( quaternion );
+ vector.add( point );
+
+ vertices.push( vector.x, vector.y, vector.z );
+ normals.push( normal.x, normal.y, normal.z );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+
+ }
+
+ normal.set( 0, 0, 1 ).applyQuaternion( quaternion );
+
+ for ( var j = shape.length - 1; j >= 0; j -- ) {
+
+ vector.copy( shape[ j ] );
+ vector.applyQuaternion( quaternion );
+ vector.add( point );
+
+ vertices.push( vector.x, vector.y, vector.z );
+ normals.push( normal.x, normal.y, normal.z );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+
+ }
+
+ };
+
+ var vector1 = new THREE.Vector3();
+ var vector2 = new THREE.Vector3();
+ var vector3 = new THREE.Vector3();
+ var vector4 = new THREE.Vector3();
+
+ var normal1 = new THREE.Vector3();
+ var normal2 = new THREE.Vector3();
+ var normal3 = new THREE.Vector3();
+ var normal4 = new THREE.Vector3();
+
+ function extrudeShape( shape, offset, color ) {
+
+ for ( var j = 0, jl = shape.length; j < jl; j ++ ) {
+
+ var point1 = shape[ j ];
+ var point2 = shape[ ( j + 1 ) % jl ];
+
+ vector1.copy( point1 ).add( offset );
+ vector1.applyQuaternion( quaternion );
+ vector1.add( point );
+
+ vector2.copy( point2 ).add( offset );
+ vector2.applyQuaternion( quaternion );
+ vector2.add( point );
+
+ vector3.copy( point2 ).add( offset );
+ vector3.applyQuaternion( prevQuaternion );
+ vector3.add( prevPoint );
+
+ vector4.copy( point1 ).add( offset );
+ vector4.applyQuaternion( prevQuaternion );
+ vector4.add( prevPoint );
+
+ vertices.push( vector1.x, vector1.y, vector1.z );
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector3.x, vector3.y, vector3.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ //
+
+ normal1.copy( point1 );
+ normal1.applyQuaternion( quaternion );
+ normal1.normalize();
+
+ normal2.copy( point2 );
+ normal2.applyQuaternion( quaternion );
+ normal2.normalize();
+
+ normal3.copy( point2 );
+ normal3.applyQuaternion( prevQuaternion );
+ normal3.normalize();
+
+ normal4.copy( point1 );
+ normal4.applyQuaternion( prevQuaternion );
+ normal4.normalize();
+
+ normals.push( normal1.x, normal1.y, normal1.z );
+ normals.push( normal2.x, normal2.y, normal2.z );
+ normals.push( normal4.x, normal4.y, normal4.z );
+
+ normals.push( normal2.x, normal2.y, normal2.z );
+ normals.push( normal3.x, normal3.y, normal3.z );
+ normals.push( normal4.x, normal4.y, normal4.z );
+
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+ colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
+
+ }
+
+ };
+
+ var offset = new THREE.Vector3();
+
+ for ( var i = 1; i <= divisions; i ++ ) {
+
+ point.copy( curve.getPointAt( i / divisions ) );
+
+ up.set( 0, 1, 0 );
+
+ forward.subVectors( point, prevPoint ).normalize();
+ right.crossVectors( up, forward ).normalize();
+ up.crossVectors( forward, right );
+
+ var angle = Math.atan2( forward.x, forward.z );
+
+ quaternion.setFromAxisAngle( up, angle );
+
+ if ( i % 2 === 0 ) {
+
+ drawShape( step, color2 );
+
+ }
+
+ extrudeShape( tube1, offset.set( 0, -0.125, 0 ), color2 );
+ extrudeShape( tube2, offset.set( 0.2, 0, 0 ), color1 );
+ extrudeShape( tube2, offset.set( -0.2, 0, 0 ), color1 );
+
+ prevPoint.copy( point );
+ prevQuaternion.copy( quaternion );
+
+ }
+
+ // console.log( vertices.length );
+
+ this.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
+ this.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );
+ this.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );
+
+};
+
+RollerCoasterGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+
+function RollerCoasterLiftersGeometry( curve, divisions ) {
+
+ THREE.BufferGeometry.call( this );
+
+ var vertices = [];
+ var normals = [];
+
+ var quaternion = new THREE.Quaternion();
+
+ var up = new THREE.Vector3( 0, 1, 0 );
+
+ var point = new THREE.Vector3();
+ var tangent = new THREE.Vector3();
+
+ // shapes
+
+ var tube1 = [
+ new THREE.Vector3( 0, 0.05, -0.05 ),
+ new THREE.Vector3( 0, 0.05, 0.05 ),
+ new THREE.Vector3( 0, -0.05, 0 )
+ ];
+
+ var tube2 = [
+ new THREE.Vector3( -0.05, 0, 0.05 ),
+ new THREE.Vector3( -0.05, 0, -0.05 ),
+ new THREE.Vector3( 0.05, 0, 0 )
+ ];
+
+ var tube3 = [
+ new THREE.Vector3( 0.05, 0, -0.05 ),
+ new THREE.Vector3( 0.05, 0, 0.05 ),
+ new THREE.Vector3( -0.05, 0, 0 )
+ ];
+
+ var vector1 = new THREE.Vector3();
+ var vector2 = new THREE.Vector3();
+ var vector3 = new THREE.Vector3();
+ var vector4 = new THREE.Vector3();
+
+ var normal1 = new THREE.Vector3();
+ var normal2 = new THREE.Vector3();
+ var normal3 = new THREE.Vector3();
+ var normal4 = new THREE.Vector3();
+
+ function extrudeShape( shape, fromPoint, toPoint ) {
+
+ for ( var j = 0, jl = shape.length; j < jl; j ++ ) {
+
+ var point1 = shape[ j ];
+ var point2 = shape[ ( j + 1 ) % jl ];
+
+ vector1.copy( point1 );
+ vector1.applyQuaternion( quaternion );
+ vector1.add( fromPoint );
+
+ vector2.copy( point2 );
+ vector2.applyQuaternion( quaternion );
+ vector2.add( fromPoint );
+
+ vector3.copy( point2 );
+ vector3.applyQuaternion( quaternion );
+ vector3.add( toPoint );
+
+ vector4.copy( point1 );
+ vector4.applyQuaternion( quaternion );
+ vector4.add( toPoint );
+
+ vertices.push( vector1.x, vector1.y, vector1.z );
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector3.x, vector3.y, vector3.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ //
+
+ normal1.copy( point1 );
+ normal1.applyQuaternion( quaternion );
+ normal1.normalize();
+
+ normal2.copy( point2 );
+ normal2.applyQuaternion( quaternion );
+ normal2.normalize();
+
+ normal3.copy( point2 );
+ normal3.applyQuaternion( quaternion );
+ normal3.normalize();
+
+ normal4.copy( point1 );
+ normal4.applyQuaternion( quaternion );
+ normal4.normalize();
+
+ normals.push( normal1.x, normal1.y, normal1.z );
+ normals.push( normal2.x, normal2.y, normal2.z );
+ normals.push( normal4.x, normal4.y, normal4.z );
+
+ normals.push( normal2.x, normal2.y, normal2.z );
+ normals.push( normal3.x, normal3.y, normal3.z );
+ normals.push( normal4.x, normal4.y, normal4.z );
+
+ }
+
+ };
+
+ var fromPoint = new THREE.Vector3();
+ var toPoint = new THREE.Vector3();
+
+ for ( var i = 1; i <= divisions; i ++ ) {
+
+ point.copy( curve.getPointAt( i / divisions ) );
+ tangent.copy( curve.getTangentAt( i / divisions ) );
+
+ var angle = Math.atan2( tangent.x, tangent.z );
+
+ quaternion.setFromAxisAngle( up, angle );
+
+ //
+
+ if ( point.y > 10 ) {
+
+ fromPoint.set( -0.75, -0.35, 0 );
+ fromPoint.applyQuaternion( quaternion );
+ fromPoint.add( point );
+
+ toPoint.set( 0.75, -0.35, 0 );
+ toPoint.applyQuaternion( quaternion );
+ toPoint.add( point );
+
+ extrudeShape( tube1, fromPoint, toPoint );
+
+ fromPoint.set( -0.7, -0.3, 0 );
+ fromPoint.applyQuaternion( quaternion );
+ fromPoint.add( point );
+
+ toPoint.set( -0.7, -point.y, 0 );
+ toPoint.applyQuaternion( quaternion );
+ toPoint.add( point );
+
+ extrudeShape( tube2, fromPoint, toPoint );
+
+ fromPoint.set( 0.7, -0.3, 0 );
+ fromPoint.applyQuaternion( quaternion );
+ fromPoint.add( point );
+
+ toPoint.set( 0.7, -point.y, 0 );
+ toPoint.applyQuaternion( quaternion );
+ toPoint.add( point );
+
+ extrudeShape( tube3, fromPoint, toPoint );
+
+ } else {
+
+ fromPoint.set( 0, -0.2, 0 );
+ fromPoint.applyQuaternion( quaternion );
+ fromPoint.add( point );
+
+ toPoint.set( 0, -point.y, 0 );
+ toPoint.applyQuaternion( quaternion );
+ toPoint.add( point );
+
+ extrudeShape( tube3, fromPoint, toPoint );
+
+ }
+
+ }
+
+ this.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
+ this.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );
+
+};
+
+RollerCoasterLiftersGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+
+function RollerCoasterShadowGeometry( curve, divisions ) {
+
+ THREE.BufferGeometry.call( this );
+
+ var vertices = [];
+
+ var up = new THREE.Vector3( 0, 1, 0 );
+ var forward = new THREE.Vector3();
+
+ var quaternion = new THREE.Quaternion();
+ var prevQuaternion = new THREE.Quaternion();
+ prevQuaternion.setFromAxisAngle( up , Math.PI / 2 );
+
+ var point = new THREE.Vector3();
+
+ var prevPoint = new THREE.Vector3();
+ prevPoint.copy( curve.getPointAt( 0 ) );
+ prevPoint.y = 0;
+
+ var vector1 = new THREE.Vector3();
+ var vector2 = new THREE.Vector3();
+ var vector3 = new THREE.Vector3();
+ var vector4 = new THREE.Vector3();
+
+ for ( var i = 1; i <= divisions; i ++ ) {
+
+ point.copy( curve.getPointAt( i / divisions ) );
+ point.y = 0;
+
+ forward.subVectors( point, prevPoint );
+
+ var angle = Math.atan2( forward.x, forward.z );
+
+ quaternion.setFromAxisAngle( up, angle );
+
+ vector1.set( -0.3, 0, 0 );
+ vector1.applyQuaternion( quaternion );
+ vector1.add( point );
+
+ vector2.set( 0.3, 0, 0 );
+ vector2.applyQuaternion( quaternion );
+ vector2.add( point );
+
+ vector3.set( 0.3, 0, 0 );
+ vector3.applyQuaternion( prevQuaternion );
+ vector3.add( prevPoint );
+
+ vector4.set( -0.3, 0, 0 );
+ vector4.applyQuaternion( prevQuaternion );
+ vector4.add( prevPoint );
+
+ vertices.push( vector1.x, vector1.y, vector1.z );
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ vertices.push( vector2.x, vector2.y, vector2.z );
+ vertices.push( vector3.x, vector3.y, vector3.z );
+ vertices.push( vector4.x, vector4.y, vector4.z );
+
+ prevPoint.copy( point );
+ prevQuaternion.copy( quaternion );
+
+ }
+
+ this.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
+
+};
+
+RollerCoasterShadowGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+
+function SkyGeometry() {
+
+ THREE.BufferGeometry.call( this );
+
+ var vertices = [];
+
+ for ( var i = 0; i < 100; i ++ ) {
+
+ var x = Math.random() * 800 - 400;
+ var y = Math.random() * 50 + 50;
+ var z = Math.random() * 800 - 400;
+
+ var size = Math.random() * 40 + 20;
+
+ vertices.push( x - size, y, z - size );
+ vertices.push( x + size, y, z - size );
+ vertices.push( x - size, y, z + size );
+
+ vertices.push( x + size, y, z - size );
+ vertices.push( x + size, y, z + size );
+ vertices.push( x - size, y, z + size );
+
+ }
+
+
+ this.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
+
+};
+
+SkyGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+
+function TreesGeometry( landscape ) {
+
+ THREE.BufferGeometry.call( this );
+
+ var vertices = [];
+ var colors = [];
+
+ var raycaster = new THREE.Raycaster();
+ raycaster.ray.direction.set( 0, -1, 0 );
+
+ for ( var i = 0; i < 2000; i ++ ) {
+
+ var x = Math.random() * 500 - 250;
+ var z = Math.random() * 500 - 250;
+
+ raycaster.ray.origin.set( x, 50, z );
+
+ var intersections = raycaster.intersectObject( landscape );
+
+ if ( intersections.length === 0 ) continue;
+
+ var y = intersections[ 0 ].point.y;
+
+ var height = Math.random() * 5 + 0.5;
+
+ var angle = Math.random() * Math.PI * 2;
+
+ vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
+ vertices.push( x, y + height, z );
+ vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
+
+ angle += Math.PI / 2;
+
+ vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
+ vertices.push( x, y + height, z );
+ vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
+
+ var random = Math.random() * 0.1;
+
+ for ( var j = 0; j < 6; j ++ ) {
+
+ colors.push( 0.2 + random, 0.4 + random, 0 );
+
+ }
+
+ }
+
+ this.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
+ this.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );
+
+};
+
+TreesGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
diff --git a/app/static/js/Script.js b/app/static/js/Script.js
new file mode 100644
index 0000000..4e7f109
--- /dev/null
+++ b/app/static/js/Script.js
@@ -0,0 +1,428 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Script = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setId( 'script' );
+ container.setPosition( 'absolute' );
+ container.setBackgroundColor( '#272822' );
+ container.setDisplay( 'none' );
+
+ var header = new UI.Panel();
+ header.setPadding( '10px' );
+ container.add( header );
+
+ var title = new UI.Text().setColor( '#fff' );
+ header.add( title );
+
+ var buttonSVG = ( function () {
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
+ svg.setAttribute( 'width', 32 );
+ svg.setAttribute( 'height', 32 );
+ var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
+ path.setAttribute( 'd', 'M 12,12 L 22,22 M 22,12 12,22' );
+ path.setAttribute( 'stroke', '#fff' );
+ svg.appendChild( path );
+ return svg;
+ } )();
+
+ var close = new UI.Element( buttonSVG );
+ close.setPosition( 'absolute' );
+ close.setTop( '3px' );
+ close.setRight( '1px' );
+ close.setCursor( 'pointer' );
+ close.onClick( function () {
+
+ container.setDisplay( 'none' );
+
+ } );
+ header.add( close );
+
+
+ var renderer;
+
+ signals.rendererChanged.add( function ( newRenderer ) {
+
+ renderer = newRenderer;
+
+ } );
+
+
+ var delay;
+ var currentMode;
+ var currentScript;
+ var currentObject;
+
+ var codemirror = CodeMirror( container.dom, {
+ value: '',
+ lineNumbers: true,
+ matchBrackets: true,
+ indentWithTabs: true,
+ tabSize: 4,
+ indentUnit: 4,
+ hintOptions: {
+ completeSingle: false
+ }
+ } );
+ codemirror.setOption( 'theme', 'monokai' );
+ codemirror.on( 'change', function () {
+
+ if ( codemirror.state.focused === false ) return;
+
+ clearTimeout( delay );
+ delay = setTimeout( function () {
+
+ var value = codemirror.getValue();
+
+ if ( ! validate( value ) ) return;
+
+ if ( typeof( currentScript ) === 'object' ) {
+
+ if ( value !== currentScript.source ) {
+
+ editor.execute( new SetScriptValueCommand( currentObject, currentScript, 'source', value ) );
+
+ }
+ return;
+ }
+
+ if ( currentScript !== 'programInfo' ) return;
+
+ var json = JSON.parse( value );
+
+ if ( JSON.stringify( currentObject.material.defines ) !== JSON.stringify( json.defines ) ) {
+
+ var cmd = new SetMaterialValueCommand( currentObject, 'defines', json.defines );
+ cmd.updatable = false;
+ editor.execute( cmd );
+
+ }
+ if ( JSON.stringify( currentObject.material.uniforms ) !== JSON.stringify( json.uniforms ) ) {
+
+ var cmd = new SetMaterialValueCommand( currentObject, 'uniforms', json.uniforms );
+ cmd.updatable = false;
+ editor.execute( cmd );
+
+ }
+ if ( JSON.stringify( currentObject.material.attributes ) !== JSON.stringify( json.attributes ) ) {
+
+ var cmd = new SetMaterialValueCommand( currentObject, 'attributes', json.attributes );
+ cmd.updatable = false;
+ editor.execute( cmd );
+
+ }
+
+ }, 300 );
+
+ });
+
+ // prevent backspace from deleting objects
+ var wrapper = codemirror.getWrapperElement();
+ wrapper.addEventListener( 'keydown', function ( event ) {
+
+ event.stopPropagation();
+
+ } );
+
+ // validate
+
+ var errorLines = [];
+ var widgets = [];
+
+ var validate = function ( string ) {
+
+ var valid;
+ var errors = [];
+
+ return codemirror.operation( function () {
+
+ while ( errorLines.length > 0 ) {
+
+ codemirror.removeLineClass( errorLines.shift(), 'background', 'errorLine' );
+
+ }
+
+ while ( widgets.length > 0 ) {
+
+ codemirror.removeLineWidget( widgets.shift() );
+
+ }
+
+ //
+
+ switch ( currentMode ) {
+
+ case 'javascript':
+
+ try {
+
+ var syntax = esprima.parse( string, { tolerant: true } );
+ errors = syntax.errors;
+
+ } catch ( error ) {
+
+ errors.push( {
+
+ lineNumber: error.lineNumber - 1,
+ message: error.message
+
+ } );
+
+ }
+
+ for ( var i = 0; i < errors.length; i ++ ) {
+
+ var error = errors[ i ];
+ error.message = error.message.replace(/Line [0-9]+: /, '');
+
+ }
+
+ break;
+
+ case 'json':
+
+ errors = [];
+
+ jsonlint.parseError = function ( message, info ) {
+
+ message = message.split('\n')[3];
+
+ errors.push( {
+
+ lineNumber: info.loc.first_line - 1,
+ message: message
+
+ } );
+
+ };
+
+ try {
+
+ jsonlint.parse( string );
+
+ } catch ( error ) {
+
+ // ignore failed error recovery
+
+ }
+
+ break;
+
+ case 'glsl':
+
+ try {
+
+ var shaderType = currentScript === 'vertexShader' ?
+ glslprep.Shader.VERTEX : glslprep.Shader.FRAGMENT;
+
+ glslprep.parseGlsl( string, shaderType );
+
+ } catch( error ) {
+
+ if ( error instanceof glslprep.SyntaxError ) {
+
+ errors.push( {
+
+ lineNumber: error.line,
+ message: "Syntax Error: " + error.message
+
+ } );
+
+ } else {
+
+ console.error( error.stack || error );
+
+ }
+
+ }
+
+ if ( errors.length !== 0 ) break;
+ if ( renderer instanceof THREE.WebGLRenderer === false ) break;
+
+ currentObject.material[ currentScript ] = string;
+ currentObject.material.needsUpdate = true;
+ signals.materialChanged.dispatch( currentObject.material );
+
+ var programs = renderer.info.programs;
+
+ valid = true;
+ var parseMessage = /^(?:ERROR|WARNING): \d+:(\d+): (.*)/g;
+
+ for ( var i = 0, n = programs.length; i !== n; ++ i ) {
+
+ var diagnostics = programs[i].diagnostics;
+
+ if ( diagnostics === undefined ||
+ diagnostics.material !== currentObject.material ) continue;
+
+ if ( ! diagnostics.runnable ) valid = false;
+
+ var shaderInfo = diagnostics[ currentScript ];
+ var lineOffset = shaderInfo.prefix.split(/\r\n|\r|\n/).length;
+
+ while ( true ) {
+
+ var parseResult = parseMessage.exec( shaderInfo.log );
+ if ( parseResult === null ) break;
+
+ errors.push( {
+
+ lineNumber: parseResult[ 1 ] - lineOffset,
+ message: parseResult[ 2 ]
+
+ } );
+
+ } // messages
+
+ break;
+
+ } // programs
+
+ } // mode switch
+
+ for ( var i = 0; i < errors.length; i ++ ) {
+
+ var error = errors[ i ];
+
+ var message = document.createElement( 'div' );
+ message.className = 'esprima-error';
+ message.textContent = error.message;
+
+ var lineNumber = Math.max( error.lineNumber, 0 );
+ errorLines.push( lineNumber );
+
+ codemirror.addLineClass( lineNumber, 'background', 'errorLine' );
+
+ var widget = codemirror.addLineWidget( lineNumber, message );
+
+ widgets.push( widget );
+
+ }
+
+ return valid !== undefined ? valid : errors.length === 0;
+
+ });
+
+ };
+
+ // tern js autocomplete
+
+ var server = new CodeMirror.TernServer( {
+ caseInsensitive: true,
+ plugins: { threejs: null }
+ } );
+
+ codemirror.setOption( 'extraKeys', {
+ 'Ctrl-Space': function(cm) { server.complete(cm); },
+ 'Ctrl-I': function(cm) { server.showType(cm); },
+ 'Ctrl-O': function(cm) { server.showDocs(cm); },
+ 'Alt-.': function(cm) { server.jumpToDef(cm); },
+ 'Alt-,': function(cm) { server.jumpBack(cm); },
+ 'Ctrl-Q': function(cm) { server.rename(cm); },
+ 'Ctrl-.': function(cm) { server.selectName(cm); }
+ } );
+
+ codemirror.on( 'cursorActivity', function( cm ) {
+
+ if ( currentMode !== 'javascript' ) return;
+ server.updateArgHints( cm );
+
+ } );
+
+ codemirror.on( 'keypress', function( cm, kb ) {
+
+ if ( currentMode !== 'javascript' ) return;
+ var typed = String.fromCharCode( kb.which || kb.keyCode );
+ if ( /[\w\.]/.exec( typed ) ) {
+
+ server.complete( cm );
+
+ }
+
+ } );
+
+
+ //
+
+ signals.editorCleared.add( function () {
+
+ container.setDisplay( 'none' );
+
+ } );
+
+ signals.editScript.add( function ( object, script ) {
+
+ var mode, name, source;
+
+ if ( typeof( script ) === 'object' ) {
+
+ mode = 'javascript';
+ name = script.name;
+ source = script.source;
+ title.setValue( object.name + ' / ' + name );
+
+ } else {
+
+ switch ( script ) {
+
+ case 'vertexShader':
+
+ mode = 'glsl';
+ name = 'Vertex Shader';
+ source = object.material.vertexShader || "";
+
+ break;
+
+ case 'fragmentShader':
+
+ mode = 'glsl';
+ name = 'Fragment Shader';
+ source = object.material.fragmentShader || "";
+
+ break;
+
+ case 'programInfo':
+
+ mode = 'json';
+ name = 'Program Properties';
+ var json = {
+ defines: object.material.defines,
+ uniforms: object.material.uniforms,
+ attributes: object.material.attributes
+ };
+ source = JSON.stringify( json, null, '\t' );
+
+ }
+ title.setValue( object.material.name + ' / ' + name );
+
+ }
+
+ currentMode = mode;
+ currentScript = script;
+ currentObject = object;
+
+ container.setDisplay( '' );
+ codemirror.setValue( source );
+ codemirror.clearHistory();
+ if ( mode === 'json' ) mode = { name: 'javascript', json: true };
+ codemirror.setOption( 'mode', mode );
+
+ } );
+
+ signals.scriptRemoved.add( function ( script ) {
+
+ if ( currentScript === script ) {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ } );
+
+ return container;
+
+};
diff --git a/app/static/js/ShaderGodRays.js b/app/static/js/ShaderGodRays.js
new file mode 100644
index 0000000..ffe0d02
--- /dev/null
+++ b/app/static/js/ShaderGodRays.js
@@ -0,0 +1,295 @@
+/**
+ * @author huwb / http://huwbowles.com/
+ *
+ * God-rays (crepuscular rays)
+ *
+ * Similar implementation to the one used by Crytek for CryEngine 2 [Sousa2008].
+ * Blurs a mask generated from the depth map along radial lines emanating from the light
+ * source. The blur repeatedly applies a blur filter of increasing support but constant
+ * sample count to produce a blur filter with large support.
+ *
+ * My implementation performs 3 passes, similar to the implementation from Sousa. I found
+ * just 6 samples per pass produced acceptible results. The blur is applied three times,
+ * with decreasing filter support. The result is equivalent to a single pass with
+ * 6*6*6 = 216 samples.
+ *
+ * References:
+ *
+ * Sousa2008 - Crysis Next Gen Effects, GDC2008, http://www.crytek.com/sites/default/files/GDC08_SousaT_CrysisEffects.ppt
+ */
+
+THREE.ShaderGodRays = {
+
+ /**
+ * The god-ray generation shader.
+ *
+ * First pass:
+ *
+ * The input is the depth map. I found that the output from the
+ * THREE.MeshDepthMaterial material was directly suitable without
+ * requiring any treatment whatsoever.
+ *
+ * The depth map is blurred along radial lines towards the "sun". The
+ * output is written to a temporary render target (I used a 1/4 sized
+ * target).
+ *
+ * Pass two & three:
+ *
+ * The results of the previous pass are re-blurred, each time with a
+ * decreased distance between samples.
+ */
+
+ 'godrays_generate': {
+
+ uniforms: {
+
+ tInput: {
+ value: null
+ },
+ fStepSize: {
+ value: 1.0
+ },
+ vSunPositionScreenSpace: {
+ value: new THREE.Vector2( 0.5, 0.5 )
+ }
+
+ },
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vUv = uv;",
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "#define TAPS_PER_PASS 6.0",
+
+ "varying vec2 vUv;",
+
+ "uniform sampler2D tInput;",
+
+ "uniform vec2 vSunPositionScreenSpace;",
+ "uniform float fStepSize;", // filter step size
+
+ "void main() {",
+
+ // delta from current pixel to "sun" position
+
+ "vec2 delta = vSunPositionScreenSpace - vUv;",
+ "float dist = length( delta );",
+
+ // Step vector (uv space)
+
+ "vec2 stepv = fStepSize * delta / dist;",
+
+ // Number of iterations between pixel and sun
+
+ "float iters = dist/fStepSize;",
+
+ "vec2 uv = vUv.xy;",
+ "float col = 0.0;",
+
+ // This breaks ANGLE in Chrome 22
+ // - see http://code.google.com/p/chromium/issues/detail?id=153105
+
+ /*
+ // Unrolling didnt do much on my hardware (ATI Mobility Radeon 3450),
+ // so i've just left the loop
+
+ "for ( float i = 0.0; i < TAPS_PER_PASS; i += 1.0 ) {",
+
+ // Accumulate samples, making sure we dont walk past the light source.
+
+ // The check for uv.y < 1 would not be necessary with "border" UV wrap
+ // mode, with a black border colour. I don't think this is currently
+ // exposed by three.js. As a result there might be artifacts when the
+ // sun is to the left, right or bottom of screen as these cases are
+ // not specifically handled.
+
+ "col += ( i <= iters && uv.y < 1.0 ? texture2D( tInput, uv ).r : 0.0 );",
+ "uv += stepv;",
+
+ "}",
+ */
+
+ // Unrolling loop manually makes it work in ANGLE
+
+ "if ( 0.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ "if ( 1.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ "if ( 2.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ "if ( 3.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ "if ( 4.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ "if ( 5.0 <= iters && uv.y < 1.0 ) col += texture2D( tInput, uv ).r;",
+ "uv += stepv;",
+
+ // Should technically be dividing by 'iters', but 'TAPS_PER_PASS' smooths out
+ // objectionable artifacts, in particular near the sun position. The side
+ // effect is that the result is darker than it should be around the sun, as
+ // TAPS_PER_PASS is greater than the number of samples actually accumulated.
+ // When the result is inverted (in the shader 'godrays_combine', this produces
+ // a slight bright spot at the position of the sun, even when it is occluded.
+
+ "gl_FragColor = vec4( col/TAPS_PER_PASS );",
+ "gl_FragColor.a = 1.0;",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ /**
+ * Additively applies god rays from texture tGodRays to a background (tColors).
+ * fGodRayIntensity attenuates the god rays.
+ */
+
+ 'godrays_combine': {
+
+ uniforms: {
+
+ tColors: {
+ value: null
+ },
+
+ tGodRays: {
+ value: null
+ },
+
+ fGodRayIntensity: {
+ value: 0.69
+ },
+
+ vSunPositionScreenSpace: {
+ value: new THREE.Vector2( 0.5, 0.5 )
+ }
+
+ },
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vUv = uv;",
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "varying vec2 vUv;",
+
+ "uniform sampler2D tColors;",
+ "uniform sampler2D tGodRays;",
+
+ "uniform vec2 vSunPositionScreenSpace;",
+ "uniform float fGodRayIntensity;",
+
+ "void main() {",
+
+ // Since THREE.MeshDepthMaterial renders foreground objects white and background
+ // objects black, the god-rays will be white streaks. Therefore value is inverted
+ // before being combined with tColors
+
+ "gl_FragColor = texture2D( tColors, vUv ) + fGodRayIntensity * vec4( 1.0 - texture2D( tGodRays, vUv ).r );",
+ "gl_FragColor.a = 1.0;",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+
+ /**
+ * A dodgy sun/sky shader. Makes a bright spot at the sun location. Would be
+ * cheaper/faster/simpler to implement this as a simple sun sprite.
+ */
+
+ 'godrays_fake_sun': {
+
+ uniforms: {
+
+ vSunPositionScreenSpace: {
+ value: new THREE.Vector2( 0.5, 0.5 )
+ },
+
+ fAspect: {
+ value: 1.0
+ },
+
+ sunColor: {
+ value: new THREE.Color( 0xffee00 )
+ },
+
+ bgColor: {
+ value: new THREE.Color( 0x000000 )
+ }
+
+ },
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vUv = uv;",
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "varying vec2 vUv;",
+
+ "uniform vec2 vSunPositionScreenSpace;",
+ "uniform float fAspect;",
+
+ "uniform vec3 sunColor;",
+ "uniform vec3 bgColor;",
+
+ "void main() {",
+
+ "vec2 diff = vUv - vSunPositionScreenSpace;",
+
+ // Correct for aspect ratio
+
+ "diff.x *= fAspect;",
+
+ "float prop = clamp( length( diff ) / 0.5, 0.0, 1.0 );",
+ "prop = 0.35 * pow( 1.0 - prop, 3.0 );",
+
+ "gl_FragColor.xyz = mix( sunColor, bgColor, 1.0 - prop );",
+ "gl_FragColor.w = 1.0;",
+
+ "}"
+
+ ].join( "\n" )
+
+ }
+
+};
diff --git a/app/static/js/ShaderSkin.js b/app/static/js/ShaderSkin.js
new file mode 100644
index 0000000..e984c1b
--- /dev/null
+++ b/app/static/js/ShaderSkin.js
@@ -0,0 +1,698 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ *
+ */
+
+
+THREE.ShaderSkin = {
+
+ /* ------------------------------------------------------------------------------------------
+ // Simple skin shader
+ // - per-pixel Blinn-Phong diffuse term mixed with half-Lambert wrap-around term (per color component)
+ // - physically based specular term (Kelemen/Szirmay-Kalos specular reflectance)
+ //
+ // - diffuse map
+ // - bump map
+ // - specular map
+ // - point, directional and hemisphere lights (use with "lights: true" material option)
+ // - fog (use with "fog: true" material option)
+ // - shadow maps
+ //
+ // ------------------------------------------------------------------------------------------ */
+
+ 'skinSimple' : {
+
+ uniforms: THREE.UniformsUtils.merge( [
+
+ THREE.UniformsLib[ "fog" ],
+ THREE.UniformsLib[ "lights" ],
+
+ {
+
+ "enableBump": { value: 0 },
+ "enableSpecular": { value: 0 },
+
+ "tDiffuse": { value: null },
+ "tBeckmann": { value: null },
+
+ "diffuse": { value: new THREE.Color( 0xeeeeee ) },
+ "specular": { value: new THREE.Color( 0x111111 ) },
+ "opacity": { value: 1 },
+
+ "uRoughness": { value: 0.15 },
+ "uSpecularBrightness": { value: 0.75 },
+
+ "bumpMap": { value: null },
+ "bumpScale": { value: 1 },
+
+ "specularMap": { value: null },
+
+ "offsetRepeat": { value: new THREE.Vector4( 0, 0, 1, 1 ) },
+
+ "uWrapRGB": { value: new THREE.Vector3( 0.75, 0.375, 0.1875 ) }
+
+ }
+
+ ] ),
+
+ fragmentShader: [
+
+ "#define USE_BUMPMAP",
+
+ "uniform bool enableBump;",
+ "uniform bool enableSpecular;",
+
+ "uniform vec3 diffuse;",
+ "uniform vec3 specular;",
+ "uniform float opacity;",
+
+ "uniform float uRoughness;",
+ "uniform float uSpecularBrightness;",
+
+ "uniform vec3 uWrapRGB;",
+
+ "uniform sampler2D tDiffuse;",
+ "uniform sampler2D tBeckmann;",
+
+ "uniform sampler2D specularMap;",
+
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+ THREE.ShaderChunk[ "bsdfs" ],
+ THREE.ShaderChunk[ "packing" ],
+ THREE.ShaderChunk[ "lights_pars" ],
+ THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
+ THREE.ShaderChunk[ "fog_pars_fragment" ],
+ THREE.ShaderChunk[ "bumpmap_pars_fragment" ],
+
+ // Fresnel term
+
+ "float fresnelReflectance( vec3 H, vec3 V, float F0 ) {",
+
+ "float base = 1.0 - dot( V, H );",
+ "float exponential = pow( base, 5.0 );",
+
+ "return exponential + F0 * ( 1.0 - exponential );",
+
+ "}",
+
+ // Kelemen/Szirmay-Kalos specular BRDF
+
+ "float KS_Skin_Specular( vec3 N,", // Bumped surface normal
+ "vec3 L,", // Points to light
+ "vec3 V,", // Points to eye
+ "float m,", // Roughness
+ "float rho_s", // Specular brightness
+ ") {",
+
+ "float result = 0.0;",
+ "float ndotl = dot( N, L );",
+
+ "if( ndotl > 0.0 ) {",
+
+ "vec3 h = L + V;", // Unnormalized half-way vector
+ "vec3 H = normalize( h );",
+
+ "float ndoth = dot( N, H );",
+
+ "float PH = pow( 2.0 * texture2D( tBeckmann, vec2( ndoth, m ) ).x, 10.0 );",
+
+ "float F = fresnelReflectance( H, V, 0.028 );",
+ "float frSpec = max( PH * F / dot( h, h ), 0.0 );",
+
+ "result = ndotl * rho_s * frSpec;", // BRDF * dot(N,L) * rho_s
+
+ "}",
+
+ "return result;",
+
+ "}",
+
+ "void main() {",
+
+ "vec3 outgoingLight = vec3( 0.0 );", // outgoing light does not have an alpha, the surface does
+ "vec4 diffuseColor = vec4( diffuse, opacity );",
+
+ "vec4 colDiffuse = texture2D( tDiffuse, vUv );",
+ "colDiffuse.rgb *= colDiffuse.rgb;",
+
+ "diffuseColor = diffuseColor * colDiffuse;",
+
+ "vec3 normal = normalize( vNormal );",
+ "vec3 viewerDirection = normalize( vViewPosition );",
+
+ "float specularStrength;",
+
+ "if ( enableSpecular ) {",
+
+ "vec4 texelSpecular = texture2D( specularMap, vUv );",
+ "specularStrength = texelSpecular.r;",
+
+ "} else {",
+
+ "specularStrength = 1.0;",
+
+ "}",
+
+ "#ifdef USE_BUMPMAP",
+
+ "if ( enableBump ) normal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );",
+
+ "#endif",
+
+ // point lights
+
+ "vec3 totalSpecularLight = vec3( 0.0 );",
+ "vec3 totalDiffuseLight = vec3( 0.0 );",
+
+ "#if NUM_POINT_LIGHTS > 0",
+
+ "for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
+
+ "vec3 lVector = pointLights[ i ].position + vViewPosition.xyz;",
+
+ "float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
+
+ "lVector = normalize( lVector );",
+
+ "float pointDiffuseWeightFull = max( dot( normal, lVector ), 0.0 );",
+ "float pointDiffuseWeightHalf = max( 0.5 * dot( normal, lVector ) + 0.5, 0.0 );",
+ "vec3 pointDiffuseWeight = mix( vec3 ( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), uWrapRGB );",
+
+ "float pointSpecularWeight = KS_Skin_Specular( normal, lVector, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ "totalDiffuseLight += pointLight[ i ].color * ( pointDiffuseWeight * attenuation );",
+ "totalSpecularLight += pointLight[ i ].color * specular * ( pointSpecularWeight * specularStrength * attenuation );",
+
+ "}",
+
+ "#endif",
+
+ // directional lights
+
+ "#if NUM_DIR_LIGHTS > 0",
+
+ "for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
+
+ "vec3 dirVector = directionalLights[ i ].direction;",
+
+ "float dirDiffuseWeightFull = max( dot( normal, dirVector ), 0.0 );",
+ "float dirDiffuseWeightHalf = max( 0.5 * dot( normal, dirVector ) + 0.5, 0.0 );",
+ "vec3 dirDiffuseWeight = mix( vec3 ( dirDiffuseWeightFull ), vec3( dirDiffuseWeightHalf ), uWrapRGB );",
+
+ "float dirSpecularWeight = KS_Skin_Specular( normal, dirVector, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ "totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
+ "totalSpecularLight += directionalLights[ i ].color * ( dirSpecularWeight * specularStrength );",
+
+ "}",
+
+ "#endif",
+
+ // hemisphere lights
+
+ "#if NUM_HEMI_LIGHTS > 0",
+
+ "for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {",
+
+ "vec3 lVector = hemisphereLightDirection[ i ];",
+
+ "float dotProduct = dot( normal, lVector );",
+ "float hemiDiffuseWeight = 0.5 * dotProduct + 0.5;",
+
+ "totalDiffuseLight += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );",
+
+ // specular (sky light)
+
+ "float hemiSpecularWeight = 0.0;",
+ "hemiSpecularWeight += KS_Skin_Specular( normal, lVector, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ // specular (ground light)
+
+ "vec3 lVectorGround = -lVector;",
+ "hemiSpecularWeight += KS_Skin_Specular( normal, lVectorGround, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ "vec3 hemiSpecularColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );",
+
+ "totalSpecularLight += hemiSpecularColor * specular * ( hemiSpecularWeight * specularStrength );",
+
+ "}",
+
+ "#endif",
+
+ "outgoingLight += diffuseColor.xyz * ( totalDiffuseLight + ambientLightColor * diffuse ) + totalSpecularLight;",
+
+ "gl_FragColor = linearToOutputTexel( vec4( outgoingLight, diffuseColor.a ) );", // TODO, this should be pre-multiplied to allow for bright highlights on very transparent objects
+
+ THREE.ShaderChunk[ "fog_fragment" ],
+
+ "}"
+
+ ].join( "\n" ),
+
+ vertexShader: [
+
+ "uniform vec4 offsetRepeat;",
+
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+ THREE.ShaderChunk[ "lights_pars" ],
+ THREE.ShaderChunk[ "shadowmap_pars_vertex" ],
+ THREE.ShaderChunk[ "fog_pars_vertex" ],
+
+ "void main() {",
+
+ "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
+ "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+
+ "vViewPosition = -mvPosition.xyz;",
+
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "vUv = uv * offsetRepeat.zw + offsetRepeat.xy;",
+
+ "gl_Position = projectionMatrix * mvPosition;",
+
+ THREE.ShaderChunk[ "shadowmap_vertex" ],
+ THREE.ShaderChunk[ "fog_vertex" ],
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ /* ------------------------------------------------------------------------------------------
+ // Skin shader
+ // - Blinn-Phong diffuse term (using normal + diffuse maps)
+ // - subsurface scattering approximation by four blur layers
+ // - physically based specular term (Kelemen/Szirmay-Kalos specular reflectance)
+ //
+ // - point and directional lights (use with "lights: true" material option)
+ //
+ // - based on Nvidia Advanced Skin Rendering GDC 2007 presentation
+ // and GPU Gems 3 Chapter 14. Advanced Techniques for Realistic Real-Time Skin Rendering
+ //
+ // http://developer.download.nvidia.com/presentations/2007/gdc/Advanced_Skin.pdf
+ // http://http.developer.nvidia.com/GPUGems3/gpugems3_ch14.html
+ // ------------------------------------------------------------------------------------------ */
+
+ 'skin' : {
+
+ uniforms: THREE.UniformsUtils.merge( [
+
+ THREE.UniformsLib[ "fog" ],
+ THREE.UniformsLib[ "lights" ],
+
+ {
+
+ "passID": { value: 0 },
+
+ "tDiffuse" : { value: null },
+ "tNormal" : { value: null },
+
+ "tBlur1" : { value: null },
+ "tBlur2" : { value: null },
+ "tBlur3" : { value: null },
+ "tBlur4" : { value: null },
+
+ "tBeckmann" : { value: null },
+
+ "uNormalScale": { value: 1.0 },
+
+ "diffuse": { value: new THREE.Color( 0xeeeeee ) },
+ "specular": { value: new THREE.Color( 0x111111 ) },
+ "opacity": { value: 1 },
+
+ "uRoughness": { value: 0.15 },
+ "uSpecularBrightness": { value: 0.75 }
+
+ }
+
+ ] ),
+
+ fragmentShader: [
+
+ "uniform vec3 diffuse;",
+ "uniform vec3 specular;",
+ "uniform float opacity;",
+
+ "uniform float uRoughness;",
+ "uniform float uSpecularBrightness;",
+
+ "uniform int passID;",
+
+ "uniform sampler2D tDiffuse;",
+ "uniform sampler2D tNormal;",
+
+ "uniform sampler2D tBlur1;",
+ "uniform sampler2D tBlur2;",
+ "uniform sampler2D tBlur3;",
+ "uniform sampler2D tBlur4;",
+
+ "uniform sampler2D tBeckmann;",
+
+ "uniform float uNormalScale;",
+
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+ THREE.ShaderChunk[ "lights_pars" ],
+ THREE.ShaderChunk[ "fog_pars_fragment" ],
+
+ "float fresnelReflectance( vec3 H, vec3 V, float F0 ) {",
+
+ "float base = 1.0 - dot( V, H );",
+ "float exponential = pow( base, 5.0 );",
+
+ "return exponential + F0 * ( 1.0 - exponential );",
+
+ "}",
+
+ // Kelemen/Szirmay-Kalos specular BRDF
+
+ "float KS_Skin_Specular( vec3 N,", // Bumped surface normal
+ "vec3 L,", // Points to light
+ "vec3 V,", // Points to eye
+ "float m,", // Roughness
+ "float rho_s", // Specular brightness
+ ") {",
+
+ "float result = 0.0;",
+ "float ndotl = dot( N, L );",
+
+ "if( ndotl > 0.0 ) {",
+
+ "vec3 h = L + V;", // Unnormalized half-way vector
+ "vec3 H = normalize( h );",
+
+ "float ndoth = dot( N, H );",
+
+ "float PH = pow( 2.0 * texture2D( tBeckmann, vec2( ndoth, m ) ).x, 10.0 );",
+ "float F = fresnelReflectance( H, V, 0.028 );",
+ "float frSpec = max( PH * F / dot( h, h ), 0.0 );",
+
+ "result = ndotl * rho_s * frSpec;", // BRDF * dot(N,L) * rho_s
+
+ "}",
+
+ "return result;",
+
+ "}",
+
+ "void main() {",
+
+ "vec3 outgoingLight = vec3( 0.0 );", // outgoing light does not have an alpha, the surface does
+ "vec4 diffuseColor = vec4( diffuse, opacity );",
+
+ "vec4 mSpecular = vec4( specular, opacity );",
+
+ "vec4 colDiffuse = texture2D( tDiffuse, vUv );",
+ "colDiffuse *= colDiffuse;",
+
+ "diffuseColor *= colDiffuse;",
+
+ // normal mapping
+
+ "vec4 posAndU = vec4( -vViewPosition, vUv.x );",
+ "vec4 posAndU_dx = dFdx( posAndU ), posAndU_dy = dFdy( posAndU );",
+ "vec3 tangent = posAndU_dx.w * posAndU_dx.xyz + posAndU_dy.w * posAndU_dy.xyz;",
+ "vec3 normal = normalize( vNormal );",
+ "vec3 binormal = normalize( cross( tangent, normal ) );",
+ "tangent = cross( normal, binormal );", // no normalization required
+ "mat3 tsb = mat3( tangent, binormal, normal );",
+
+ "vec3 normalTex = texture2D( tNormal, vUv ).xyz * 2.0 - 1.0;",
+ "normalTex.xy *= uNormalScale;",
+ "normalTex = normalize( normalTex );",
+
+ "vec3 finalNormal = tsb * normalTex;",
+ "normal = normalize( finalNormal );",
+
+ "vec3 viewerDirection = normalize( vViewPosition );",
+
+ // point lights
+
+ "vec3 totalDiffuseLight = vec3( 0.0 );",
+ "vec3 totalSpecularLight = vec3( 0.0 );",
+
+ "#if NUM_POINT_LIGHTS > 0",
+
+ "for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
+
+ "vec3 pointVector = normalize( pointLights[ i ].direction );",
+ "float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
+
+ "float pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );",
+
+ "totalDiffuseLight += pointLightColor[ i ] * ( pointDiffuseWeight * attenuation );",
+
+ "if ( passID == 1 ) {",
+
+ "float pointSpecularWeight = KS_Skin_Specular( normal, pointVector, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ "totalSpecularLight += pointLightColor[ i ] * mSpecular.xyz * ( pointSpecularWeight * attenuation );",
+
+ "}",
+
+ "}",
+
+ "#endif",
+
+ // directional lights
+
+ "#if NUM_DIR_LIGHTS > 0",
+
+ "for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
+
+ "vec3 dirVector = directionalLights[ i ].direction;",
+
+ "float dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );",
+
+
+ "totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
+
+ "if ( passID == 1 ) {",
+
+ "float dirSpecularWeight = KS_Skin_Specular( normal, dirVector, viewerDirection, uRoughness, uSpecularBrightness );",
+
+ "totalSpecularLight += directionalLights[ i ].color * mSpecular.xyz * dirSpecularWeight;",
+
+ "}",
+
+ "}",
+
+ "#endif",
+
+
+ "outgoingLight += diffuseColor.rgb * ( totalDiffuseLight + totalSpecularLight );",
+
+ "if ( passID == 0 ) {",
+
+ "outgoingLight = sqrt( outgoingLight );",
+
+ "} else if ( passID == 1 ) {",
+
+ //"#define VERSION1",
+
+ "#ifdef VERSION1",
+
+ "vec3 nonblurColor = sqrt(outgoingLight );",
+
+ "#else",
+
+ "vec3 nonblurColor = outgoingLight;",
+
+ "#endif",
+
+ "vec3 blur1Color = texture2D( tBlur1, vUv ).xyz;",
+ "vec3 blur2Color = texture2D( tBlur2, vUv ).xyz;",
+ "vec3 blur3Color = texture2D( tBlur3, vUv ).xyz;",
+ "vec3 blur4Color = texture2D( tBlur4, vUv ).xyz;",
+
+
+ //"gl_FragColor = vec4( blur1Color, gl_FragColor.w );",
+
+ //"gl_FragColor = vec4( vec3( 0.22, 0.5, 0.7 ) * nonblurColor + vec3( 0.2, 0.5, 0.3 ) * blur1Color + vec3( 0.58, 0.0, 0.0 ) * blur2Color, gl_FragColor.w );",
+
+ //"gl_FragColor = vec4( vec3( 0.25, 0.6, 0.8 ) * nonblurColor + vec3( 0.15, 0.25, 0.2 ) * blur1Color + vec3( 0.15, 0.15, 0.0 ) * blur2Color + vec3( 0.45, 0.0, 0.0 ) * blur3Color, gl_FragColor.w );",
+
+
+ "outgoingLight = vec3( vec3( 0.22, 0.437, 0.635 ) * nonblurColor + ",
+ "vec3( 0.101, 0.355, 0.365 ) * blur1Color + ",
+ "vec3( 0.119, 0.208, 0.0 ) * blur2Color + ",
+ "vec3( 0.114, 0.0, 0.0 ) * blur3Color + ",
+ "vec3( 0.444, 0.0, 0.0 ) * blur4Color );",
+
+ "outgoingLight *= sqrt( colDiffuse.xyz );",
+
+ "outgoingLight += ambientLightColor * diffuse * colDiffuse.xyz + totalSpecularLight;",
+
+ "#ifndef VERSION1",
+
+ "outgoingLight = sqrt( outgoingLight );",
+
+ "#endif",
+
+ "}",
+
+ "gl_FragColor = vec4( outgoingLight, diffuseColor.a );", // TODO, this should be pre-multiplied to allow for bright highlights on very transparent objects
+
+ THREE.ShaderChunk[ "fog_fragment" ],
+
+ "}"
+
+ ].join( "\n" ),
+
+ vertexShader: [
+
+ "#ifdef VERTEX_TEXTURES",
+
+ "uniform sampler2D tDisplacement;",
+ "uniform float uDisplacementScale;",
+ "uniform float uDisplacementBias;",
+
+ "#endif",
+
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+ THREE.ShaderChunk[ "fog_pars_vertex" ],
+
+ "void main() {",
+
+ "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+
+ "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
+
+ "vViewPosition = -mvPosition.xyz;",
+
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "vUv = uv;",
+
+ // displacement mapping
+
+ "#ifdef VERTEX_TEXTURES",
+
+ "vec3 dv = texture2D( tDisplacement, uv ).xyz;",
+ "float df = uDisplacementScale * dv.x + uDisplacementBias;",
+ "vec4 displacedPosition = vec4( vNormal.xyz * df, 0.0 ) + mvPosition;",
+ "gl_Position = projectionMatrix * displacedPosition;",
+
+ "#else",
+
+ "gl_Position = projectionMatrix * mvPosition;",
+
+ "#endif",
+
+ THREE.ShaderChunk[ "fog_vertex" ],
+
+ "}",
+
+
+ ].join( "\n" ),
+
+ vertexShaderUV: [
+
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+
+ "void main() {",
+
+ "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+
+ "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
+
+ "vViewPosition = -mvPosition.xyz;",
+
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "vUv = uv;",
+
+ "gl_Position = vec4( uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, 0.0, 1.0 );",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ /* ------------------------------------------------------------------------------------------
+ // Beckmann distribution function
+ // - to be used in specular term of skin shader
+ // - render a screen-aligned quad to precompute a 512 x 512 texture
+ //
+ // - from http://developer.nvidia.com/node/171
+ ------------------------------------------------------------------------------------------ */
+
+ "beckmann" : {
+
+ uniforms: {},
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vUv = uv;",
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "varying vec2 vUv;",
+
+ "float PHBeckmann( float ndoth, float m ) {",
+
+ "float alpha = acos( ndoth );",
+ "float ta = tan( alpha );",
+
+ "float val = 1.0 / ( m * m * pow( ndoth, 4.0 ) ) * exp( -( ta * ta ) / ( m * m ) );",
+ "return val;",
+
+ "}",
+
+ "float KSTextureCompute( vec2 tex ) {",
+
+ // Scale the value to fit within [0,1] invert upon lookup.
+
+ "return 0.5 * pow( PHBeckmann( tex.x, tex.y ), 0.1 );",
+
+ "}",
+
+ "void main() {",
+
+ "float x = KSTextureCompute( vUv );",
+
+ "gl_FragColor = vec4( x, x, x, 1.0 );",
+
+ "}"
+
+ ].join( "\n" )
+
+ }
+
+};
diff --git a/app/static/js/ShaderTerrain.js b/app/static/js/ShaderTerrain.js
new file mode 100644
index 0000000..cbc98b4
--- /dev/null
+++ b/app/static/js/ShaderTerrain.js
@@ -0,0 +1,324 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ *
+ */
+
+THREE.ShaderTerrain = {
+
+ /* -------------------------------------------------------------------------
+ // Dynamic terrain shader
+ // - Blinn-Phong
+ // - height + normal + diffuse1 + diffuse2 + specular + detail maps
+ // - point, directional and hemisphere lights (use with "lights: true" material option)
+ // - shadow maps receiving
+ ------------------------------------------------------------------------- */
+
+ 'terrain' : {
+
+ uniforms: THREE.UniformsUtils.merge( [
+
+ THREE.UniformsLib[ "fog" ],
+ THREE.UniformsLib[ "lights" ],
+
+ {
+
+ "enableDiffuse1": { value: 0 },
+ "enableDiffuse2": { value: 0 },
+ "enableSpecular": { value: 0 },
+ "enableReflection": { value: 0 },
+
+ "tDiffuse1": { value: null },
+ "tDiffuse2": { value: null },
+ "tDetail": { value: null },
+ "tNormal": { value: null },
+ "tSpecular": { value: null },
+ "tDisplacement": { value: null },
+
+ "uNormalScale": { value: 1.0 },
+
+ "uDisplacementBias": { value: 0.0 },
+ "uDisplacementScale": { value: 1.0 },
+
+ "diffuse": { value: new THREE.Color( 0xeeeeee ) },
+ "specular": { value: new THREE.Color( 0x111111 ) },
+ "shininess": { value: 30 },
+ "opacity": { value: 1 },
+
+ "uRepeatBase": { value: new THREE.Vector2( 1, 1 ) },
+ "uRepeatOverlay": { value: new THREE.Vector2( 1, 1 ) },
+
+ "uOffset": { value: new THREE.Vector2( 0, 0 ) }
+
+ }
+
+ ] ),
+
+ fragmentShader: [
+
+ "uniform vec3 diffuse;",
+ "uniform vec3 specular;",
+ "uniform float shininess;",
+ "uniform float opacity;",
+
+ "uniform bool enableDiffuse1;",
+ "uniform bool enableDiffuse2;",
+ "uniform bool enableSpecular;",
+
+ "uniform sampler2D tDiffuse1;",
+ "uniform sampler2D tDiffuse2;",
+ "uniform sampler2D tDetail;",
+ "uniform sampler2D tNormal;",
+ "uniform sampler2D tSpecular;",
+ "uniform sampler2D tDisplacement;",
+
+ "uniform float uNormalScale;",
+
+ "uniform vec2 uRepeatOverlay;",
+ "uniform vec2 uRepeatBase;",
+
+ "uniform vec2 uOffset;",
+
+ "varying vec3 vTangent;",
+ "varying vec3 vBinormal;",
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "common" ],
+ THREE.ShaderChunk[ "bsdfs" ],
+ THREE.ShaderChunk[ "lights_pars" ],
+ THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
+ THREE.ShaderChunk[ "fog_pars_fragment" ],
+
+ "float calcLightAttenuation( float lightDistance, float cutoffDistance, float decayExponent ) {",
+ "if ( decayExponent > 0.0 ) {",
+ "return pow( saturate( - lightDistance / cutoffDistance + 1.0 ), decayExponent );",
+ "}",
+ "return 1.0;",
+ "}",
+
+ "void main() {",
+
+ "vec3 outgoingLight = vec3( 0.0 );", // outgoing light does not have an alpha, the surface does
+ "vec4 diffuseColor = vec4( diffuse, opacity );",
+
+ "vec3 specularTex = vec3( 1.0 );",
+
+ "vec2 uvOverlay = uRepeatOverlay * vUv + uOffset;",
+ "vec2 uvBase = uRepeatBase * vUv;",
+
+ "vec3 normalTex = texture2D( tDetail, uvOverlay ).xyz * 2.0 - 1.0;",
+ "normalTex.xy *= uNormalScale;",
+ "normalTex = normalize( normalTex );",
+
+ "if( enableDiffuse1 && enableDiffuse2 ) {",
+
+ "vec4 colDiffuse1 = texture2D( tDiffuse1, uvOverlay );",
+ "vec4 colDiffuse2 = texture2D( tDiffuse2, uvOverlay );",
+
+ "colDiffuse1 = GammaToLinear( colDiffuse1, float( GAMMA_FACTOR ) );",
+ "colDiffuse2 = GammaToLinear( colDiffuse2, float( GAMMA_FACTOR ) );",
+
+ "diffuseColor *= mix ( colDiffuse1, colDiffuse2, 1.0 - texture2D( tDisplacement, uvBase ) );",
+
+ " } else if( enableDiffuse1 ) {",
+
+ "diffuseColor *= texture2D( tDiffuse1, uvOverlay );",
+
+ "} else if( enableDiffuse2 ) {",
+
+ "diffuseColor *= texture2D( tDiffuse2, uvOverlay );",
+
+ "}",
+
+ "if( enableSpecular )",
+ "specularTex = texture2D( tSpecular, uvOverlay ).xyz;",
+
+ "mat3 tsb = mat3( vTangent, vBinormal, vNormal );",
+ "vec3 finalNormal = tsb * normalTex;",
+
+ "vec3 normal = normalize( finalNormal );",
+ "vec3 viewPosition = normalize( vViewPosition );",
+
+ "vec3 totalDiffuseLight = vec3( 0.0 );",
+ "vec3 totalSpecularLight = vec3( 0.0 );",
+
+ // point lights
+
+ "#if NUM_POINT_LIGHTS > 0",
+
+ "for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {",
+
+ "vec3 lVector = pointLights[ i ].position + vViewPosition.xyz;",
+
+ "float attenuation = calcLightAttenuation( length( lVector ), pointLights[ i ].distance, pointLights[ i ].decay );",
+
+ "lVector = normalize( lVector );",
+
+ "vec3 pointHalfVector = normalize( lVector + viewPosition );",
+
+ "float pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );",
+ "float pointDiffuseWeight = max( dot( normal, lVector ), 0.0 );",
+
+ "float pointSpecularWeight = specularTex.r * max( pow( pointDotNormalHalf, shininess ), 0.0 );",
+
+ "totalDiffuseLight += attenuation * pointLights[ i ].color * pointDiffuseWeight;",
+ "totalSpecularLight += attenuation * pointLights[ i ].color * specular * pointSpecularWeight * pointDiffuseWeight;",
+
+ "}",
+
+ "#endif",
+
+ // directional lights
+
+ "#if NUM_DIR_LIGHTS > 0",
+
+ "vec3 dirDiffuse = vec3( 0.0 );",
+ "vec3 dirSpecular = vec3( 0.0 );",
+
+ "for( int i = 0; i < NUM_DIR_LIGHTS; i++ ) {",
+
+ "vec3 dirVector = directionalLights[ i ].direction;",
+ "vec3 dirHalfVector = normalize( dirVector + viewPosition );",
+
+ "float dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );",
+ "float dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );",
+
+ "float dirSpecularWeight = specularTex.r * max( pow( dirDotNormalHalf, shininess ), 0.0 );",
+
+ "totalDiffuseLight += directionalLights[ i ].color * dirDiffuseWeight;",
+ "totalSpecularLight += directionalLights[ i ].color * specular * dirSpecularWeight * dirDiffuseWeight;",
+
+ "}",
+
+ "#endif",
+
+ // hemisphere lights
+
+ "#if NUM_HEMI_LIGHTS > 0",
+
+ "vec3 hemiDiffuse = vec3( 0.0 );",
+ "vec3 hemiSpecular = vec3( 0.0 );",
+
+ "for( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {",
+
+ "vec3 lVector = hemisphereLightDirection[ i ];",
+
+ // diffuse
+
+ "float dotProduct = dot( normal, lVector );",
+ "float hemiDiffuseWeight = 0.5 * dotProduct + 0.5;",
+
+ "totalDiffuseLight += mix( hemisphereLights[ i ].groundColor, hemisphereLights[ i ].skyColor, hemiDiffuseWeight );",
+
+ // specular (sky light)
+
+ "float hemiSpecularWeight = 0.0;",
+
+ "vec3 hemiHalfVectorSky = normalize( lVector + viewPosition );",
+ "float hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;",
+ "hemiSpecularWeight += specularTex.r * max( pow( hemiDotNormalHalfSky, shininess ), 0.0 );",
+
+ // specular (ground light)
+
+ "vec3 lVectorGround = -lVector;",
+
+ "vec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );",
+ "float hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;",
+ "hemiSpecularWeight += specularTex.r * max( pow( hemiDotNormalHalfGround, shininess ), 0.0 );",
+
+ "totalSpecularLight += specular * mix( hemisphereLights[ i ].groundColor, hemisphereLights[ i ].skyColor, hemiDiffuseWeight ) * hemiSpecularWeight * hemiDiffuseWeight;",
+
+ "}",
+
+ "#endif",
+
+ "outgoingLight += diffuseColor.xyz * ( totalDiffuseLight + ambientLightColor + totalSpecularLight );",
+
+ "gl_FragColor = vec4( outgoingLight, diffuseColor.a );", // TODO, this should be pre-multiplied to allow for bright highlights on very transparent objects
+
+ THREE.ShaderChunk[ "fog_fragment" ],
+
+ "}"
+
+ ].join( "\n" ),
+
+ vertexShader: [
+
+ "attribute vec4 tangent;",
+
+ "uniform vec2 uRepeatBase;",
+
+ "uniform sampler2D tNormal;",
+
+ "#ifdef VERTEX_TEXTURES",
+
+ "uniform sampler2D tDisplacement;",
+ "uniform float uDisplacementScale;",
+ "uniform float uDisplacementBias;",
+
+ "#endif",
+
+ "varying vec3 vTangent;",
+ "varying vec3 vBinormal;",
+ "varying vec3 vNormal;",
+ "varying vec2 vUv;",
+
+ "varying vec3 vViewPosition;",
+
+ THREE.ShaderChunk[ "shadowmap_pars_vertex" ],
+ THREE.ShaderChunk[ "fog_pars_vertex" ],
+
+ "void main() {",
+
+ "vNormal = normalize( normalMatrix * normal );",
+
+ // tangent and binormal vectors
+
+ "vTangent = normalize( normalMatrix * tangent.xyz );",
+
+ "vBinormal = cross( vNormal, vTangent ) * tangent.w;",
+ "vBinormal = normalize( vBinormal );",
+
+ // texture coordinates
+
+ "vUv = uv;",
+
+ "vec2 uvBase = uv * uRepeatBase;",
+
+ // displacement mapping
+
+ "#ifdef VERTEX_TEXTURES",
+
+ "vec3 dv = texture2D( tDisplacement, uvBase ).xyz;",
+ "float df = uDisplacementScale * dv.x + uDisplacementBias;",
+ "vec3 displacedPosition = normal * df + position;",
+
+ "vec4 worldPosition = modelMatrix * vec4( displacedPosition, 1.0 );",
+ "vec4 mvPosition = modelViewMatrix * vec4( displacedPosition, 1.0 );",
+
+ "#else",
+
+ "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+ "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
+
+ "#endif",
+
+ "gl_Position = projectionMatrix * mvPosition;",
+
+ "vViewPosition = -mvPosition.xyz;",
+
+ "vec3 normalTex = texture2D( tNormal, uvBase ).xyz * 2.0 - 1.0;",
+ "vNormal = normalMatrix * normalTex;",
+
+ THREE.ShaderChunk[ "shadowmap_vertex" ],
+ THREE.ShaderChunk[ "fog_vertex" ],
+
+ "}"
+
+ ].join( "\n" )
+
+ }
+
+};
diff --git a/app/static/js/ShaderToon.js b/app/static/js/ShaderToon.js
new file mode 100644
index 0000000..4566ce9
--- /dev/null
+++ b/app/static/js/ShaderToon.js
@@ -0,0 +1,331 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author alteredq / http://alteredqualia.com/
+ *
+ * ShaderToon currently contains:
+ *
+ * toon1
+ * toon2
+ * hatching
+ * dotted
+ */
+
+THREE.ShaderToon = {
+
+ 'toon1' : {
+
+ uniforms: {
+
+ "uDirLightPos": { value: new THREE.Vector3() },
+ "uDirLightColor": { value: new THREE.Color( 0xeeeeee ) },
+
+ "uAmbientLightColor": { value: new THREE.Color( 0x050505 ) },
+
+ "uBaseColor": { value: new THREE.Color( 0xffffff ) }
+
+ },
+
+ vertexShader: [
+
+ "varying vec3 vNormal;",
+ "varying vec3 vRefract;",
+
+ "void main() {",
+
+ "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+ "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
+ "vec3 worldNormal = normalize ( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal );",
+
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "vec3 I = worldPosition.xyz - cameraPosition;",
+ "vRefract = refract( normalize( I ), worldNormal, 1.02 );",
+
+ "gl_Position = projectionMatrix * mvPosition;",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform vec3 uBaseColor;",
+
+ "uniform vec3 uDirLightPos;",
+ "uniform vec3 uDirLightColor;",
+
+ "uniform vec3 uAmbientLightColor;",
+
+ "varying vec3 vNormal;",
+
+ "varying vec3 vRefract;",
+
+ "void main() {",
+
+ "float directionalLightWeighting = max( dot( normalize( vNormal ), uDirLightPos ), 0.0);",
+ "vec3 lightWeighting = uAmbientLightColor + uDirLightColor * directionalLightWeighting;",
+
+ "float intensity = smoothstep( - 0.5, 1.0, pow( length(lightWeighting), 20.0 ) );",
+ "intensity += length(lightWeighting) * 0.2;",
+
+ "float cameraWeighting = dot( normalize( vNormal ), vRefract );",
+ "intensity += pow( 1.0 - length( cameraWeighting ), 6.0 );",
+ "intensity = intensity * 0.2 + 0.3;",
+
+ "if ( intensity < 0.50 ) {",
+
+ "gl_FragColor = vec4( 2.0 * intensity * uBaseColor, 1.0 );",
+
+ "} else {",
+
+ "gl_FragColor = vec4( 1.0 - 2.0 * ( 1.0 - intensity ) * ( 1.0 - uBaseColor ), 1.0 );",
+
+ "}",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ 'toon2' : {
+
+ uniforms: {
+
+ "uDirLightPos": { value: new THREE.Vector3() },
+ "uDirLightColor": { value: new THREE.Color( 0xeeeeee ) },
+
+ "uAmbientLightColor": { value: new THREE.Color( 0x050505 ) },
+
+ "uBaseColor": { value: new THREE.Color( 0xeeeeee ) },
+ "uLineColor1": { value: new THREE.Color( 0x808080 ) },
+ "uLineColor2": { value: new THREE.Color( 0x000000 ) },
+ "uLineColor3": { value: new THREE.Color( 0x000000 ) },
+ "uLineColor4": { value: new THREE.Color( 0x000000 ) }
+
+ },
+
+ vertexShader: [
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform vec3 uBaseColor;",
+ "uniform vec3 uLineColor1;",
+ "uniform vec3 uLineColor2;",
+ "uniform vec3 uLineColor3;",
+ "uniform vec3 uLineColor4;",
+
+ "uniform vec3 uDirLightPos;",
+ "uniform vec3 uDirLightColor;",
+
+ "uniform vec3 uAmbientLightColor;",
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "float camera = max( dot( normalize( vNormal ), vec3( 0.0, 0.0, 1.0 ) ), 0.4);",
+ "float light = max( dot( normalize( vNormal ), uDirLightPos ), 0.0);",
+
+ "gl_FragColor = vec4( uBaseColor, 1.0 );",
+
+ "if ( length(uAmbientLightColor + uDirLightColor * light) < 1.00 ) {",
+
+ "gl_FragColor *= vec4( uLineColor1, 1.0 );",
+
+ "}",
+
+ "if ( length(uAmbientLightColor + uDirLightColor * camera) < 0.50 ) {",
+
+ "gl_FragColor *= vec4( uLineColor2, 1.0 );",
+
+ "}",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ 'hatching' : {
+
+ uniforms: {
+
+ "uDirLightPos": { value: new THREE.Vector3() },
+ "uDirLightColor": { value: new THREE.Color( 0xeeeeee ) },
+
+ "uAmbientLightColor": { value: new THREE.Color( 0x050505 ) },
+
+ "uBaseColor": { value: new THREE.Color( 0xffffff ) },
+ "uLineColor1": { value: new THREE.Color( 0x000000 ) },
+ "uLineColor2": { value: new THREE.Color( 0x000000 ) },
+ "uLineColor3": { value: new THREE.Color( 0x000000 ) },
+ "uLineColor4": { value: new THREE.Color( 0x000000 ) }
+
+ },
+
+ vertexShader: [
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform vec3 uBaseColor;",
+ "uniform vec3 uLineColor1;",
+ "uniform vec3 uLineColor2;",
+ "uniform vec3 uLineColor3;",
+ "uniform vec3 uLineColor4;",
+
+ "uniform vec3 uDirLightPos;",
+ "uniform vec3 uDirLightColor;",
+
+ "uniform vec3 uAmbientLightColor;",
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "float directionalLightWeighting = max( dot( normalize(vNormal), uDirLightPos ), 0.0);",
+ "vec3 lightWeighting = uAmbientLightColor + uDirLightColor * directionalLightWeighting;",
+
+ "gl_FragColor = vec4( uBaseColor, 1.0 );",
+
+ "if ( length(lightWeighting) < 1.00 ) {",
+
+ "if ( mod(gl_FragCoord.x + gl_FragCoord.y, 10.0) == 0.0) {",
+
+ "gl_FragColor = vec4( uLineColor1, 1.0 );",
+
+ "}",
+
+ "}",
+
+ "if ( length(lightWeighting) < 0.75 ) {",
+
+ "if (mod(gl_FragCoord.x - gl_FragCoord.y, 10.0) == 0.0) {",
+
+ "gl_FragColor = vec4( uLineColor2, 1.0 );",
+
+ "}",
+ "}",
+
+ "if ( length(lightWeighting) < 0.50 ) {",
+
+ "if (mod(gl_FragCoord.x + gl_FragCoord.y - 5.0, 10.0) == 0.0) {",
+
+ "gl_FragColor = vec4( uLineColor3, 1.0 );",
+
+ "}",
+ "}",
+
+ "if ( length(lightWeighting) < 0.3465 ) {",
+
+ "if (mod(gl_FragCoord.x - gl_FragCoord.y - 5.0, 10.0) == 0.0) {",
+
+ "gl_FragColor = vec4( uLineColor4, 1.0 );",
+
+ "}",
+ "}",
+
+ "}"
+
+ ].join( "\n" )
+
+ },
+
+ 'dotted' : {
+
+ uniforms: {
+
+ "uDirLightPos": { value: new THREE.Vector3() },
+ "uDirLightColor": { value: new THREE.Color( 0xeeeeee ) },
+
+ "uAmbientLightColor": { value: new THREE.Color( 0x050505 ) },
+
+ "uBaseColor": { value: new THREE.Color( 0xffffff ) },
+ "uLineColor1": { value: new THREE.Color( 0x000000 ) }
+
+ },
+
+ vertexShader: [
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+ "vNormal = normalize( normalMatrix * normal );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform vec3 uBaseColor;",
+ "uniform vec3 uLineColor1;",
+ "uniform vec3 uLineColor2;",
+ "uniform vec3 uLineColor3;",
+ "uniform vec3 uLineColor4;",
+
+ "uniform vec3 uDirLightPos;",
+ "uniform vec3 uDirLightColor;",
+
+ "uniform vec3 uAmbientLightColor;",
+
+ "varying vec3 vNormal;",
+
+ "void main() {",
+
+ "float directionalLightWeighting = max( dot( normalize(vNormal), uDirLightPos ), 0.0);",
+ "vec3 lightWeighting = uAmbientLightColor + uDirLightColor * directionalLightWeighting;",
+
+ "gl_FragColor = vec4( uBaseColor, 1.0 );",
+
+ "if ( length(lightWeighting) < 1.00 ) {",
+
+ "if ( ( mod(gl_FragCoord.x, 4.001) + mod(gl_FragCoord.y, 4.0) ) > 6.00 ) {",
+
+ "gl_FragColor = vec4( uLineColor1, 1.0 );",
+
+ "}",
+
+ "}",
+
+ "if ( length(lightWeighting) < 0.50 ) {",
+
+ "if ( ( mod(gl_FragCoord.x + 2.0, 4.001) + mod(gl_FragCoord.y + 2.0, 4.0) ) > 6.00 ) {",
+
+ "gl_FragColor = vec4( uLineColor1, 1.0 );",
+
+ "}",
+
+ "}",
+
+ "}"
+
+ ].join( "\n" )
+
+ }
+
+};
diff --git a/app/static/js/Sidebar.Animation.js b/app/static/js/Sidebar.Animation.js
new file mode 100644
index 0000000..eab588a
--- /dev/null
+++ b/app/static/js/Sidebar.Animation.js
@@ -0,0 +1,24 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Animation = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var options = {};
+ var possibleAnimations = {};
+
+ var container = new UI.Panel();
+ container.setDisplay( 'none' );
+
+ container.add( new UI.Text( 'Animation' ).setTextTransform( 'uppercase' ) );
+ container.add( new UI.Break() );
+ container.add( new UI.Break() );
+
+ var animationsRow = new UI.Row();
+ container.add( animationsRow );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Geometry.BoxGeometry.js b/app/static/js/Sidebar.Geometry.BoxGeometry.js
new file mode 100644
index 0000000..c8b68f4
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.BoxGeometry.js
@@ -0,0 +1,93 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.BoxGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // width
+
+ var widthRow = new UI.Row();
+ var width = new UI.Number( parameters.width ).onChange( update );
+
+ widthRow.add( new UI.Text( 'Width' ).setWidth( '90px' ) );
+ widthRow.add( width );
+
+ container.add( widthRow );
+
+ // height
+
+ var heightRow = new UI.Row();
+ var height = new UI.Number( parameters.height ).onChange( update );
+
+ heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
+ heightRow.add( height );
+
+ container.add( heightRow );
+
+ // depth
+
+ var depthRow = new UI.Row();
+ var depth = new UI.Number( parameters.depth ).onChange( update );
+
+ depthRow.add( new UI.Text( 'Depth' ).setWidth( '90px' ) );
+ depthRow.add( depth );
+
+ container.add( depthRow );
+
+ // widthSegments
+
+ var widthSegmentsRow = new UI.Row();
+ var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
+
+ widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
+ widthSegmentsRow.add( widthSegments );
+
+ container.add( widthSegmentsRow );
+
+ // heightSegments
+
+ var heightSegmentsRow = new UI.Row();
+ var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
+
+ heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
+ heightSegmentsRow.add( heightSegments );
+
+ container.add( heightSegmentsRow );
+
+ // depthSegments
+
+ var depthSegmentsRow = new UI.Row();
+ var depthSegments = new UI.Integer( parameters.depthSegments ).setRange( 1, Infinity ).onChange( update );
+
+ depthSegmentsRow.add( new UI.Text( 'Depth segments' ).setWidth( '90px' ) );
+ depthSegmentsRow.add( depthSegments );
+
+ container.add( depthSegmentsRow );
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ width.getValue(),
+ height.getValue(),
+ depth.getValue(),
+ widthSegments.getValue(),
+ heightSegments.getValue(),
+ depthSegments.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.BoxBufferGeometry = Sidebar.Geometry.BoxGeometry;
diff --git a/app/static/js/Sidebar.Geometry.BufferGeometry.js b/app/static/js/Sidebar.Geometry.BufferGeometry.js
new file mode 100644
index 0000000..b818116
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.BufferGeometry.js
@@ -0,0 +1,60 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.BufferGeometry = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ function update( object ) {
+
+ if ( object === null ) return; // objectSelected.dispatch( null )
+ if ( object === undefined ) return;
+
+ var geometry = object.geometry;
+
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ container.clear();
+ container.setDisplay( 'block' );
+
+ var index = geometry.index;
+
+ if ( index !== null ) {
+
+ var panel = new UI.Row();
+ panel.add( new UI.Text( 'index' ).setWidth( '90px' ) );
+ panel.add( new UI.Text( ( index.count ).format() ).setFontSize( '12px' ) );
+ container.add( panel );
+
+ }
+
+ var attributes = geometry.attributes;
+
+ for ( var name in attributes ) {
+
+ var attribute = attributes[ name ];
+
+ var panel = new UI.Row();
+ panel.add( new UI.Text( name ).setWidth( '90px' ) );
+ panel.add( new UI.Text( ( attribute.count ).format() + ' (' + attribute.itemSize + ')' ).setFontSize( '12px' ) );
+ container.add( panel );
+
+ }
+
+ } else {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ }
+
+ signals.objectSelected.add( update );
+ signals.geometryChanged.add( update );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Geometry.CircleGeometry.js b/app/static/js/Sidebar.Geometry.CircleGeometry.js
new file mode 100644
index 0000000..57144f7
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.CircleGeometry.js
@@ -0,0 +1,71 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.CircleGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radius
+
+ var radiusRow = new UI.Row();
+ var radius = new UI.Number( parameters.radius ).onChange( update );
+
+ radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
+ radiusRow.add( radius );
+
+ container.add( radiusRow );
+
+ // segments
+
+ var segmentsRow = new UI.Row();
+ var segments = new UI.Integer( parameters.segments ).setRange( 3, Infinity ).onChange( update );
+
+ segmentsRow.add( new UI.Text( 'Segments' ).setWidth( '90px' ) );
+ segmentsRow.add( segments );
+
+ container.add( segmentsRow );
+
+ // thetaStart
+
+ var thetaStartRow = new UI.Row();
+ var thetaStart = new UI.Number( parameters.thetaStart ).onChange( update );
+
+ thetaStartRow.add( new UI.Text( 'Theta start' ).setWidth( '90px' ) );
+ thetaStartRow.add( thetaStart );
+
+ container.add( thetaStartRow );
+
+ // thetaLength
+
+ var thetaLengthRow = new UI.Row();
+ var thetaLength = new UI.Number( parameters.thetaLength ).onChange( update );
+
+ thetaLengthRow.add( new UI.Text( 'Theta length' ).setWidth( '90px' ) );
+ thetaLengthRow.add( thetaLength );
+
+ container.add( thetaLengthRow );
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radius.getValue(),
+ segments.getValue(),
+ thetaStart.getValue(),
+ thetaLength.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.CircleBufferGeometry = Sidebar.Geometry.CircleGeometry;
diff --git a/app/static/js/Sidebar.Geometry.CylinderGeometry.js b/app/static/js/Sidebar.Geometry.CylinderGeometry.js
new file mode 100644
index 0000000..c665e47
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.CylinderGeometry.js
@@ -0,0 +1,93 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.CylinderGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radiusTop
+
+ var radiusTopRow = new UI.Row();
+ var radiusTop = new UI.Number( parameters.radiusTop ).onChange( update );
+
+ radiusTopRow.add( new UI.Text( 'Radius top' ).setWidth( '90px' ) );
+ radiusTopRow.add( radiusTop );
+
+ container.add( radiusTopRow );
+
+ // radiusBottom
+
+ var radiusBottomRow = new UI.Row();
+ var radiusBottom = new UI.Number( parameters.radiusBottom ).onChange( update );
+
+ radiusBottomRow.add( new UI.Text( 'Radius bottom' ).setWidth( '90px' ) );
+ radiusBottomRow.add( radiusBottom );
+
+ container.add( radiusBottomRow );
+
+ // height
+
+ var heightRow = new UI.Row();
+ var height = new UI.Number( parameters.height ).onChange( update );
+
+ heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
+ heightRow.add( height );
+
+ container.add( heightRow );
+
+ // radialSegments
+
+ var radialSegmentsRow = new UI.Row();
+ var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
+
+ radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
+ radialSegmentsRow.add( radialSegments );
+
+ container.add( radialSegmentsRow );
+
+ // heightSegments
+
+ var heightSegmentsRow = new UI.Row();
+ var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
+
+ heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
+ heightSegmentsRow.add( heightSegments );
+
+ container.add( heightSegmentsRow );
+
+ // openEnded
+
+ var openEndedRow = new UI.Row();
+ var openEnded = new UI.Checkbox( parameters.openEnded ).onChange( update );
+
+ openEndedRow.add( new UI.Text( 'Open ended' ).setWidth( '90px' ) );
+ openEndedRow.add( openEnded );
+
+ container.add( openEndedRow );
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radiusTop.getValue(),
+ radiusBottom.getValue(),
+ height.getValue(),
+ radialSegments.getValue(),
+ heightSegments.getValue(),
+ openEnded.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.CylinderBufferGeometry = Sidebar.Geometry.CylinderGeometry;
diff --git a/app/static/js/Sidebar.Geometry.Geometry.js b/app/static/js/Sidebar.Geometry.Geometry.js
new file mode 100644
index 0000000..391c460
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.Geometry.js
@@ -0,0 +1,60 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.Geometry = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ // vertices
+
+ var verticesRow = new UI.Row();
+ var vertices = new UI.Text();
+
+ verticesRow.add( new UI.Text( 'Vertices' ).setWidth( '90px' ) );
+ verticesRow.add( vertices );
+
+ container.add( verticesRow );
+
+ // faces
+
+ var facesRow = new UI.Row();
+ var faces = new UI.Text();
+
+ facesRow.add( new UI.Text( 'Faces' ).setWidth( '90px' ) );
+ facesRow.add( faces );
+
+ container.add( facesRow );
+
+ //
+
+ function update( object ) {
+
+ if ( object === null ) return; // objectSelected.dispatch( null )
+ if ( object === undefined ) return;
+
+ var geometry = object.geometry;
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ container.setDisplay( 'block' );
+
+ vertices.setValue( ( geometry.vertices.length ).format() );
+ faces.setValue( ( geometry.faces.length ).format() );
+
+ } else {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ }
+
+ signals.objectSelected.add( update );
+ signals.geometryChanged.add( update );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Geometry.IcosahedronGeometry.js b/app/static/js/Sidebar.Geometry.IcosahedronGeometry.js
new file mode 100644
index 0000000..3833498
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.IcosahedronGeometry.js
@@ -0,0 +1,52 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.IcosahedronGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radius
+
+ var radiusRow = new UI.Row();
+ var radius = new UI.Number( parameters.radius ).onChange( update );
+
+ radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
+ radiusRow.add( radius );
+
+ container.add( radiusRow );
+
+ // detail
+
+ var detailRow = new UI.Row();
+ var detail = new UI.Integer( parameters.detail ).setRange( 0, Infinity ).onChange( update );
+
+ detailRow.add( new UI.Text( 'Detail' ).setWidth( '90px' ) );
+ detailRow.add( detail );
+
+ container.add( detailRow );
+
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radius.getValue(),
+ detail.getValue()
+ ) ) );
+
+ signals.objectChanged.dispatch( object );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.IcosahedronBufferGeometry = Sidebar.Geometry.IcosahedronGeometry;
diff --git a/app/static/js/Sidebar.Geometry.LatheGeometry.js b/app/static/js/Sidebar.Geometry.LatheGeometry.js
new file mode 100644
index 0000000..405b904
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.LatheGeometry.js
@@ -0,0 +1,150 @@
+/**
+ * @author rfm1201
+ */
+
+Sidebar.Geometry.LatheGeometry = function( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // segments
+
+ var segmentsRow = new UI.Row();
+ var segments = new UI.Integer( parameters.segments ).onChange( update );
+
+ segmentsRow.add( new UI.Text( 'Segments' ).setWidth( '90px' ) );
+ segmentsRow.add( segments );
+
+ container.add( segmentsRow );
+
+ // phiStart
+
+ var phiStartRow = new UI.Row();
+ var phiStart = new UI.Number( parameters.phiStart * 180 / Math.PI ).onChange( update );
+
+ phiStartRow.add( new UI.Text( 'Phi start (°)' ).setWidth( '90px' ) );
+ phiStartRow.add( phiStart );
+
+ container.add( phiStartRow );
+
+ // phiLength
+
+ var phiLengthRow = new UI.Row();
+ var phiLength = new UI.Number( parameters.phiLength * 180 / Math.PI ).onChange( update );
+
+ phiLengthRow.add( new UI.Text( 'Phi length (°)' ).setWidth( '90px' ) );
+ phiLengthRow.add( phiLength );
+
+ container.add( phiLengthRow );
+
+ // points
+
+ var lastPointIdx = 0;
+ var pointsUI = [];
+
+ var pointsRow = new UI.Row();
+ pointsRow.add( new UI.Text( 'Points' ).setWidth( '90px' ) );
+
+ var points = new UI.Span().setDisplay( 'inline-block' );
+ pointsRow.add( points );
+
+ var pointsList = new UI.Div();
+ points.add( pointsList );
+
+ for ( var i = 0; i < parameters.points.length; i ++ ) {
+
+ var point = parameters.points[ i ];
+ pointsList.add( createPointRow( point.x, point.y ) );
+
+ }
+
+ var addPointButton = new UI.Button( '+' ).onClick( function() {
+
+ if( pointsUI.length === 0 ){
+
+ pointsList.add( createPointRow( 0, 0 ) );
+
+ } else {
+
+ var point = pointsUI[ pointsUI.length - 1 ];
+
+ pointsList.add( createPointRow( point.x.getValue(), point.y.getValue() ) );
+
+ }
+
+ update();
+
+ } );
+ points.add( addPointButton );
+
+ container.add( pointsRow );
+
+ //
+
+ function createPointRow( x, y ) {
+
+ var pointRow = new UI.Div();
+ var lbl = new UI.Text( lastPointIdx + 1 ).setWidth( '20px' );
+ var txtX = new UI.Number( x ).setRange( 0, Infinity ).setWidth( '40px' ).onChange( update );
+ var txtY = new UI.Number( y ).setWidth( '40px' ).onChange( update );
+ var idx = lastPointIdx;
+ var btn = new UI.Button( '-' ).onClick( function() {
+
+ deletePointRow( idx );
+
+ } );
+
+ pointsUI.push( { row: pointRow, lbl: lbl, x: txtX, y: txtY } );
+ lastPointIdx ++;
+ pointRow.add( lbl, txtX, txtY, btn );
+
+ return pointRow;
+
+ }
+
+ function deletePointRow( idx ) {
+
+ if ( ! pointsUI[ idx ] ) return;
+
+ pointsList.remove( pointsUI[ idx ].row );
+ pointsUI[ idx ] = null;
+
+ update();
+
+ }
+
+ function update() {
+
+ var points = [];
+ var count = 0;
+
+ for ( var i = 0; i < pointsUI.length; i ++ ) {
+
+ var pointUI = pointsUI[ i ];
+
+ if ( ! pointUI ) continue;
+
+ points.push( new THREE.Vector2( pointUI.x.getValue(), pointUI.y.getValue() ) );
+ count ++;
+ pointUI.lbl.setValue( count );
+
+ }
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ points,
+ segments.getValue(),
+ phiStart.getValue() / 180 * Math.PI,
+ phiLength.getValue() / 180 * Math.PI
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.LatheBufferGeometry = Sidebar.Geometry.LatheGeometry;
diff --git a/app/static/js/Sidebar.Geometry.Modifiers.js b/app/static/js/Sidebar.Geometry.Modifiers.js
new file mode 100644
index 0000000..25500b0
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.Modifiers.js
@@ -0,0 +1,40 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.Modifiers = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row().setPaddingLeft( '90px' );
+
+ var geometry = object.geometry;
+
+ // Compute Vertex Normals
+
+ var button = new UI.Button( 'Compute Vertex Normals' );
+ button.onClick( function () {
+
+ geometry.computeVertexNormals();
+
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ geometry.attributes.normal.needsUpdate = true;
+
+ } else {
+
+ geometry.normalsNeedUpdate = true;
+
+ }
+
+ signals.geometryChanged.dispatch( object );
+
+ } );
+
+ container.add( button );
+
+ //
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Geometry.PlaneGeometry.js b/app/static/js/Sidebar.Geometry.PlaneGeometry.js
new file mode 100644
index 0000000..cf02885
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.PlaneGeometry.js
@@ -0,0 +1,72 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.PlaneGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // width
+
+ var widthRow = new UI.Row();
+ var width = new UI.Number( parameters.width ).onChange( update );
+
+ widthRow.add( new UI.Text( 'Width' ).setWidth( '90px' ) );
+ widthRow.add( width );
+
+ container.add( widthRow );
+
+ // height
+
+ var heightRow = new UI.Row();
+ var height = new UI.Number( parameters.height ).onChange( update );
+
+ heightRow.add( new UI.Text( 'Height' ).setWidth( '90px' ) );
+ heightRow.add( height );
+
+ container.add( heightRow );
+
+ // widthSegments
+
+ var widthSegmentsRow = new UI.Row();
+ var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
+
+ widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
+ widthSegmentsRow.add( widthSegments );
+
+ container.add( widthSegmentsRow );
+
+ // heightSegments
+
+ var heightSegmentsRow = new UI.Row();
+ var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
+
+ heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
+ heightSegmentsRow.add( heightSegments );
+
+ container.add( heightSegmentsRow );
+
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ width.getValue(),
+ height.getValue(),
+ widthSegments.getValue(),
+ heightSegments.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.PlaneBufferGeometry = Sidebar.Geometry.PlaneGeometry;
diff --git a/app/static/js/Sidebar.Geometry.SphereGeometry.js b/app/static/js/Sidebar.Geometry.SphereGeometry.js
new file mode 100644
index 0000000..ed47095
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.SphereGeometry.js
@@ -0,0 +1,105 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.SphereGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radius
+
+ var radiusRow = new UI.Row();
+ var radius = new UI.Number( parameters.radius ).onChange( update );
+
+ radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
+ radiusRow.add( radius );
+
+ container.add( radiusRow );
+
+ // widthSegments
+
+ var widthSegmentsRow = new UI.Row();
+ var widthSegments = new UI.Integer( parameters.widthSegments ).setRange( 1, Infinity ).onChange( update );
+
+ widthSegmentsRow.add( new UI.Text( 'Width segments' ).setWidth( '90px' ) );
+ widthSegmentsRow.add( widthSegments );
+
+ container.add( widthSegmentsRow );
+
+ // heightSegments
+
+ var heightSegmentsRow = new UI.Row();
+ var heightSegments = new UI.Integer( parameters.heightSegments ).setRange( 1, Infinity ).onChange( update );
+
+ heightSegmentsRow.add( new UI.Text( 'Height segments' ).setWidth( '90px' ) );
+ heightSegmentsRow.add( heightSegments );
+
+ container.add( heightSegmentsRow );
+
+ // phiStart
+
+ var phiStartRow = new UI.Row();
+ var phiStart = new UI.Number( parameters.phiStart ).onChange( update );
+
+ phiStartRow.add( new UI.Text( 'Phi start' ).setWidth( '90px' ) );
+ phiStartRow.add( phiStart );
+
+ container.add( phiStartRow );
+
+ // phiLength
+
+ var phiLengthRow = new UI.Row();
+ var phiLength = new UI.Number( parameters.phiLength ).onChange( update );
+
+ phiLengthRow.add( new UI.Text( 'Phi length' ).setWidth( '90px' ) );
+ phiLengthRow.add( phiLength );
+
+ container.add( phiLengthRow );
+
+ // thetaStart
+
+ var thetaStartRow = new UI.Row();
+ var thetaStart = new UI.Number( parameters.thetaStart ).onChange( update );
+
+ thetaStartRow.add( new UI.Text( 'Theta start' ).setWidth( '90px' ) );
+ thetaStartRow.add( thetaStart );
+
+ container.add( thetaStartRow );
+
+ // thetaLength
+
+ var thetaLengthRow = new UI.Row();
+ var thetaLength = new UI.Number( parameters.thetaLength ).onChange( update );
+
+ thetaLengthRow.add( new UI.Text( 'Theta length' ).setWidth( '90px' ) );
+ thetaLengthRow.add( thetaLength );
+
+ container.add( thetaLengthRow );
+
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radius.getValue(),
+ widthSegments.getValue(),
+ heightSegments.getValue(),
+ phiStart.getValue(),
+ phiLength.getValue(),
+ thetaStart.getValue(),
+ thetaLength.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.SphereBufferGeometry = Sidebar.Geometry.SphereGeometry;
diff --git a/app/static/js/Sidebar.Geometry.TeapotBufferGeometry.js b/app/static/js/Sidebar.Geometry.TeapotBufferGeometry.js
new file mode 100644
index 0000000..1e2cf8a
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.TeapotBufferGeometry.js
@@ -0,0 +1,103 @@
+/**
+ * @author tschw
+ */
+
+Sidebar.Geometry.TeapotBufferGeometry = function ( signals, object ) {
+
+ var container = new UI.Row();
+
+ var parameters = object.geometry.parameters;
+
+ // size
+
+ var sizeRow = new UI.Row();
+ var size = new UI.Number( parameters.size ).onChange( update );
+
+ sizeRow.add( new UI.Text( 'Size' ).setWidth( '90px' ) );
+ sizeRow.add( size );
+
+ container.add( sizeRow );
+
+ // segments
+
+ var segmentsRow = new UI.Row();
+ var segments = new UI.Integer( parameters.segments ).setRange( 1, Infinity ).onChange( update );
+
+ segmentsRow.add( new UI.Text( 'Segments' ).setWidth( '90px' ) );
+ segmentsRow.add( segments );
+
+ container.add( segmentsRow );
+
+ // bottom
+
+ var bottomRow = new UI.Row();
+ var bottom = new UI.Checkbox( parameters.bottom ).onChange( update );
+
+ bottomRow.add( new UI.Text( 'Bottom' ).setWidth( '90px' ) );
+ bottomRow.add( bottom );
+
+ container.add( bottomRow );
+
+ // lid
+
+ var lidRow = new UI.Row();
+ var lid = new UI.Checkbox( parameters.lid ).onChange( update );
+
+ lidRow.add( new UI.Text( 'Lid' ).setWidth( '90px' ) );
+ lidRow.add( lid );
+
+ container.add( lidRow );
+
+ // body
+
+ var bodyRow = new UI.Row();
+ var body = new UI.Checkbox( parameters.body ).onChange( update );
+
+ bodyRow.add( new UI.Text( 'Body' ).setWidth( '90px' ) );
+ bodyRow.add( body );
+
+ container.add( bodyRow );
+
+ // fitted lid
+
+ var fitLidRow = new UI.Row();
+ var fitLid = new UI.Checkbox( parameters.fitLid ).onChange( update );
+
+ fitLidRow.add( new UI.Text( 'Fitted Lid' ).setWidth( '90px' ) );
+ fitLidRow.add( fitLid );
+
+ container.add( fitLidRow );
+
+ // blinn-sized
+
+ var blinnRow = new UI.Row();
+ var blinn = new UI.Checkbox( parameters.blinn ).onChange( update );
+
+ blinnRow.add( new UI.Text( 'Blinn-scaled' ).setWidth( '90px' ) );
+ blinnRow.add( blinn );
+
+ container.add( blinnRow );
+
+ function update() {
+
+ object.geometry.dispose();
+
+ object.geometry = new THREE.TeapotBufferGeometry(
+ size.getValue(),
+ segments.getValue(),
+ bottom.getValue(),
+ lid.getValue(),
+ body.getValue(),
+ fitLid.getValue(),
+ blinn.getValue()
+ );
+
+ object.geometry.computeBoundingSphere();
+
+ signals.geometryChanged.dispatch( object );
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Geometry.TorusGeometry.js b/app/static/js/Sidebar.Geometry.TorusGeometry.js
new file mode 100644
index 0000000..7d9cb74
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.TorusGeometry.js
@@ -0,0 +1,83 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.TorusGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radius
+
+ var radiusRow = new UI.Row();
+ var radius = new UI.Number( parameters.radius ).onChange( update );
+
+ radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
+ radiusRow.add( radius );
+
+ container.add( radiusRow );
+
+ // tube
+
+ var tubeRow = new UI.Row();
+ var tube = new UI.Number( parameters.tube ).onChange( update );
+
+ tubeRow.add( new UI.Text( 'Tube' ).setWidth( '90px' ) );
+ tubeRow.add( tube );
+
+ container.add( tubeRow );
+
+ // radialSegments
+
+ var radialSegmentsRow = new UI.Row();
+ var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
+
+ radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
+ radialSegmentsRow.add( radialSegments );
+
+ container.add( radialSegmentsRow );
+
+ // tubularSegments
+
+ var tubularSegmentsRow = new UI.Row();
+ var tubularSegments = new UI.Integer( parameters.tubularSegments ).setRange( 1, Infinity ).onChange( update );
+
+ tubularSegmentsRow.add( new UI.Text( 'Tubular segments' ).setWidth( '90px' ) );
+ tubularSegmentsRow.add( tubularSegments );
+
+ container.add( tubularSegmentsRow );
+
+ // arc
+
+ var arcRow = new UI.Row();
+ var arc = new UI.Number( parameters.arc ).onChange( update );
+
+ arcRow.add( new UI.Text( 'Arc' ).setWidth( '90px' ) );
+ arcRow.add( arc );
+
+ container.add( arcRow );
+
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radius.getValue(),
+ tube.getValue(),
+ radialSegments.getValue(),
+ tubularSegments.getValue(),
+ arc.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.TorusBufferGeometry = Sidebar.Geometry.TorusGeometry;
diff --git a/app/static/js/Sidebar.Geometry.TorusKnotGeometry.js b/app/static/js/Sidebar.Geometry.TorusKnotGeometry.js
new file mode 100644
index 0000000..85a859c
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.TorusKnotGeometry.js
@@ -0,0 +1,94 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry.TorusKnotGeometry = function ( editor, object ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Row();
+
+ var geometry = object.geometry;
+ var parameters = geometry.parameters;
+
+ // radius
+
+ var radiusRow = new UI.Row();
+ var radius = new UI.Number( parameters.radius ).onChange( update );
+
+ radiusRow.add( new UI.Text( 'Radius' ).setWidth( '90px' ) );
+ radiusRow.add( radius );
+
+ container.add( radiusRow );
+
+ // tube
+
+ var tubeRow = new UI.Row();
+ var tube = new UI.Number( parameters.tube ).onChange( update );
+
+ tubeRow.add( new UI.Text( 'Tube' ).setWidth( '90px' ) );
+ tubeRow.add( tube );
+
+ container.add( tubeRow );
+
+ // tubularSegments
+
+ var tubularSegmentsRow = new UI.Row();
+ var tubularSegments = new UI.Integer( parameters.tubularSegments ).setRange( 1, Infinity ).onChange( update );
+
+ tubularSegmentsRow.add( new UI.Text( 'Tubular segments' ).setWidth( '90px' ) );
+ tubularSegmentsRow.add( tubularSegments );
+
+ container.add( tubularSegmentsRow );
+
+ // radialSegments
+
+ var radialSegmentsRow = new UI.Row();
+ var radialSegments = new UI.Integer( parameters.radialSegments ).setRange( 1, Infinity ).onChange( update );
+
+ radialSegmentsRow.add( new UI.Text( 'Radial segments' ).setWidth( '90px' ) );
+ radialSegmentsRow.add( radialSegments );
+
+ container.add( radialSegmentsRow );
+
+ // p
+
+ var pRow = new UI.Row();
+ var p = new UI.Number( parameters.p ).onChange( update );
+
+ pRow.add( new UI.Text( 'P' ).setWidth( '90px' ) );
+ pRow.add( p );
+
+ container.add( pRow );
+
+ // q
+
+ var qRow = new UI.Row();
+ var q = new UI.Number( parameters.q ).onChange( update );
+
+ pRow.add( new UI.Text( 'Q' ).setWidth( '90px' ) );
+ pRow.add( q );
+
+ container.add( qRow );
+
+
+ //
+
+ function update() {
+
+ editor.execute( new SetGeometryCommand( object, new THREE[ geometry.type ](
+ radius.getValue(),
+ tube.getValue(),
+ tubularSegments.getValue(),
+ radialSegments.getValue(),
+ p.getValue(),
+ q.getValue()
+ ) ) );
+
+ }
+
+ return container;
+
+};
+
+Sidebar.Geometry.TorusKnotBufferGeometry = Sidebar.Geometry.TorusKnotGeometry;
diff --git a/app/static/js/Sidebar.Geometry.js b/app/static/js/Sidebar.Geometry.js
new file mode 100644
index 0000000..e12e64e
--- /dev/null
+++ b/app/static/js/Sidebar.Geometry.js
@@ -0,0 +1,186 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Geometry = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+
+ // Actions
+
+ /*
+ var objectActions = new UI.Select().setPosition( 'absolute' ).setRight( '8px' ).setFontSize( '11px' );
+ objectActions.setOptions( {
+
+ 'Actions': 'Actions',
+ 'Center': 'Center',
+ 'Convert': 'Convert',
+ 'Flatten': 'Flatten'
+
+ } );
+ objectActions.onClick( function ( event ) {
+
+ event.stopPropagation(); // Avoid panel collapsing
+
+ } );
+ objectActions.onChange( function ( event ) {
+
+ var action = this.getValue();
+
+ var object = editor.selected;
+ var geometry = object.geometry;
+
+ if ( confirm( action + ' ' + object.name + '?' ) === false ) return;
+
+ switch ( action ) {
+
+ case 'Center':
+
+ var offset = geometry.center();
+
+ var newPosition = object.position.clone();
+ newPosition.sub( offset );
+ editor.execute( new SetPositionCommand( object, newPosition ) );
+
+ editor.signals.geometryChanged.dispatch( object );
+
+ break;
+
+ case 'Convert':
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ editor.execute( new SetGeometryCommand( object, new THREE.BufferGeometry().fromGeometry( geometry ) ) );
+
+ }
+
+ break;
+
+ case 'Flatten':
+
+ var newGeometry = geometry.clone();
+ newGeometry.uuid = geometry.uuid;
+ newGeometry.applyMatrix( object.matrix );
+
+ var cmds = [ new SetGeometryCommand( object, newGeometry ),
+ new SetPositionCommand( object, new THREE.Vector3( 0, 0, 0 ) ),
+ new SetRotationCommand( object, new THREE.Euler( 0, 0, 0 ) ),
+ new SetScaleCommand( object, new THREE.Vector3( 1, 1, 1 ) ) ];
+
+ editor.execute( new MultiCmdsCommand( cmds ), 'Flatten Geometry' );
+
+ break;
+
+ }
+
+ this.setValue( 'Actions' );
+
+ } );
+ container.addStatic( objectActions );
+ */
+
+ // type
+
+ var geometryTypeRow = new UI.Row();
+ var geometryType = new UI.Text();
+
+ geometryTypeRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
+ geometryTypeRow.add( geometryType );
+
+ container.add( geometryTypeRow );
+
+ // uuid
+
+ var geometryUUIDRow = new UI.Row();
+ var geometryUUID = new UI.Input().setWidth( '102px' ).setFontSize( '12px' ).setDisabled( true );
+ var geometryUUIDRenew = new UI.Button( 'New' ).setMarginLeft( '7px' ).onClick( function () {
+
+ geometryUUID.setValue( THREE.Math.generateUUID() );
+
+ editor.execute( new SetGeometryValueCommand( editor.selected, 'uuid', geometryUUID.getValue() ) );
+
+ } );
+
+ geometryUUIDRow.add( new UI.Text( 'UUID' ).setWidth( '90px' ) );
+ geometryUUIDRow.add( geometryUUID );
+ geometryUUIDRow.add( geometryUUIDRenew );
+
+ container.add( geometryUUIDRow );
+
+ // name
+
+ var geometryNameRow = new UI.Row();
+ var geometryName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
+
+ editor.execute( new SetGeometryValueCommand( editor.selected, 'name', geometryName.getValue() ) );
+
+ } );
+
+ geometryNameRow.add( new UI.Text( 'Name' ).setWidth( '90px' ) );
+ geometryNameRow.add( geometryName );
+
+ container.add( geometryNameRow );
+
+ // geometry
+
+ container.add( new Sidebar.Geometry.Geometry( editor ) );
+
+ // buffergeometry
+
+ container.add( new Sidebar.Geometry.BufferGeometry( editor ) );
+
+ // parameters
+
+ var parameters = new UI.Span();
+ container.add( parameters );
+
+
+ //
+
+ function build() {
+
+ var object = editor.selected;
+
+ if ( object && object.geometry ) {
+
+ var geometry = object.geometry;
+
+ container.setDisplay( 'block' );
+
+ geometryType.setValue( geometry.type );
+
+ geometryUUID.setValue( geometry.uuid );
+ geometryName.setValue( geometry.name );
+
+ //
+
+ parameters.clear();
+
+ if ( geometry.type === 'BufferGeometry' || geometry.type === 'Geometry' ) {
+
+ parameters.add( new Sidebar.Geometry.Modifiers( editor, object ) );
+
+ } else if ( Sidebar.Geometry[ geometry.type ] !== undefined ) {
+
+ parameters.add( new Sidebar.Geometry[ geometry.type ]( editor, object ) );
+
+ }
+
+ } else {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ }
+
+ signals.objectSelected.add( build );
+ signals.geometryChanged.add( build );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.History.js b/app/static/js/Sidebar.History.js
new file mode 100644
index 0000000..d805447
--- /dev/null
+++ b/app/static/js/Sidebar.History.js
@@ -0,0 +1,132 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+Sidebar.History = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var config = editor.config;
+
+ var history = editor.history;
+
+ var container = new UI.Panel();
+
+ container.add( new UI.Text( 'HISTORY' ) );
+ container.add( new UI.Break() );
+ container.add( new UI.Break() );
+
+ //
+
+ /*
+ var persistent = new UI.THREE.Boolean( config.getKey( 'settings/history' ), 'persistent' );
+ persistent.setPosition( 'absolute' ).setRight( '8px' );
+ persistent.onChange( function () {
+
+ var value = this.getValue();
+
+ config.setKey( 'settings/history', value );
+
+ if ( value ) {
+
+ alert( 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.' );
+
+ var lastUndoCmd = history.undos[ history.undos.length - 1 ];
+ var lastUndoId = ( lastUndoCmd !== undefined ) ? lastUndoCmd.id : 0;
+ editor.history.enableSerialization( lastUndoId );
+
+ } else {
+
+ signals.historyChanged.dispatch();
+
+ }
+
+ } );
+ container.add( persistent );
+
+ */
+
+ var ignoreObjectSelectedSignal = false;
+
+ var outliner = new UI.Outliner( editor );
+ outliner.onChange( function () {
+
+ ignoreObjectSelectedSignal = true;
+
+ editor.history.goToState( parseInt( outliner.getValue() ) );
+
+ ignoreObjectSelectedSignal = false;
+
+ } );
+ container.add( outliner );
+
+ //
+
+ var refreshUI = function () {
+
+ var options = [];
+ var enumerator = 1;
+
+ function buildOption( object ) {
+
+ var option = document.createElement( 'div' );
+ option.value = object.id;
+
+ return option;
+
+ }
+
+ ( function addObjects( objects ) {
+
+ for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+ var object = objects[ i ];
+
+ var option = buildOption( object );
+ option.innerHTML = ' ' + object.name;
+
+ options.push( option );
+
+ }
+
+ } )( history.undos );
+
+
+ ( function addObjects( objects, pad ) {
+
+ for ( var i = objects.length - 1; i >= 0; i -- ) {
+
+ var object = objects[ i ];
+
+ var option = buildOption( object );
+ option.innerHTML = ' ' + object.name;
+ option.style.opacity = 0.3;
+
+ options.push( option );
+
+ }
+
+ } )( history.redos, ' ' );
+
+ outliner.setOptions( options );
+
+ };
+
+ refreshUI();
+
+ // events
+
+ signals.editorCleared.add( refreshUI );
+
+ signals.historyChanged.add( refreshUI );
+ signals.historyChanged.add( function ( cmd ) {
+
+ outliner.setValue( cmd !== undefined ? cmd.id : null );
+
+ } );
+
+
+ return container;
+
+};
\ No newline at end of file
diff --git a/app/static/js/Sidebar.Material.js b/app/static/js/Sidebar.Material.js
new file mode 100644
index 0000000..c6dae1b
--- /dev/null
+++ b/app/static/js/Sidebar.Material.js
@@ -0,0 +1,111 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Material = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var currentObject;
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+
+ // wireframe
+
+ var materialWireframeRow = new UI.Row();
+ var materialWireframe = new UI.Checkbox( false ).onChange( update );
+
+ materialWireframeRow.add( new UI.Text( 'Wireframe' ).setWidth( '90px' ) );
+ materialWireframeRow.add( materialWireframe );
+
+ container.add( materialWireframeRow );
+
+ //
+
+
+
+ function update() {
+
+ var currentObject = editor.selected;
+
+ if (currentObject) {
+
+ //if ( (editor.getObjectMaterial((currentObject.children)[0])).wireframe !== undefined && (editor.getObjectMaterial((currentObject.children)[0])).wireframe !== materialWireframe.getValue() ) {
+
+ //editor.execute( new SetMaterialValueCommand( (currentObject.children)[3], 'wireframe', materialWireframe.getValue()) );
+ //editor.execute( new SetMaterialValueCommand( (currentObject.children)[7], 'wireframe', materialWireframe.getValue()) );
+
+ var cmds = [];
+ var objects = currentObject.children;
+ var object
+
+ for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+ object = objects[ i ];
+
+ //var material = editor.getObjectMaterial(object)
+
+ //if ( material.wireframe !== undefined && material.wireframe !== materialWireframe.getValue() ) {
+
+ cmds.push(new SetMaterialValueCommand( object, 'wireframe', materialWireframe.getValue()));
+
+ //editor.execute( new SetMaterialValueCommand( object, 'wireframe', materialWireframe.getValue()) );
+
+ //}
+
+ }
+
+ editor.execute( new MultiCmdsCommand(cmds), 'wireframe' );
+
+ //}
+ refreshUI();
+
+ }
+
+ if ( textureWarning ) {
+
+ console.warn( "Can't set texture, model doesn't have texture coordinates" );
+
+ }
+
+ }
+
+
+ function refreshUI( resetTextureSelectors ) {
+
+ if ( ! currentObject ) return;
+
+ var material = currentObject.material;
+
+ material = editor.getObjectMaterial( currentObject );
+
+ if ( material.wireframe !== undefined ) {
+
+ materialWireframe.setValue( material.wireframe );
+
+ }
+ }
+
+ // events
+
+ signals.objectSelected.add( function ( object ) {
+
+ var objectChanged = object;
+
+ currentObject = object;
+ refreshUI( objectChanged );
+ container.setDisplay( '' );
+
+ } );
+
+ signals.materialChanged.add( function () {
+
+ refreshUI();
+
+ } );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Object.js b/app/static/js/Sidebar.Object.js
new file mode 100644
index 0000000..8c7168b
--- /dev/null
+++ b/app/static/js/Sidebar.Object.js
@@ -0,0 +1,735 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Object = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+ container.setDisplay( 'none' );
+
+ // Actions
+
+ var objectActions = new UI.Select().setPosition( 'absolute' ).setRight( '8px' ).setFontSize( '11px' );
+ objectActions.setOptions( {
+
+ 'Actions': 'Actions',
+ 'Reset Position': 'Reset Position',
+ 'Reset Rotation': 'Reset Rotation',
+ 'Reset Scale': 'Reset Scale'
+
+ } );
+ objectActions.onClick( function ( event ) {
+
+ event.stopPropagation(); // Avoid panel collapsing
+
+ } );
+ objectActions.onChange( function ( event ) {
+
+ var object = editor.selected;
+
+ switch ( this.getValue() ) {
+
+ case 'Reset Position':
+ editor.execute( new SetPositionCommand( object, new THREE.Vector3( 0, 0, 0 ) ) );
+ break;
+
+ case 'Reset Rotation':
+ editor.execute( new SetRotationCommand( object, new THREE.Euler( 0, 0, 0 ) ) );
+ break;
+
+ case 'Reset Scale':
+ editor.execute( new SetScaleCommand( object, new THREE.Vector3( 1, 1, 1 ) ) );
+ break;
+
+ }
+
+ this.setValue( 'Actions' );
+
+ } );
+ // container.addStatic( objectActions );
+
+ // type
+
+ var objectTypeRow = new UI.Row();
+ var objectType = new UI.Text();
+
+ objectTypeRow.add( new UI.Text( 'Type' ).setWidth( '90px' ) );
+ objectTypeRow.add( objectType );
+
+ //container.add( objectTypeRow );
+
+ // uuid
+
+ var objectUUIDRow = new UI.Row();
+ var objectUUID = new UI.Input().setWidth( '102px' ).setFontSize( '12px' ).setDisabled( true );
+ var objectUUIDRenew = new UI.Button( 'New' ).setMarginLeft( '7px' ).onClick( function () {
+
+ objectUUID.setValue( THREE.Math.generateUUID() );
+
+ editor.execute( new SetUuidCommand( editor.selected, objectUUID.getValue() ) );
+
+ } );
+
+ objectUUIDRow.add( new UI.Text( 'UUID' ).setWidth( '90px' ) );
+ objectUUIDRow.add( objectUUID );
+ objectUUIDRow.add( objectUUIDRenew );
+
+ //container.add( objectUUIDRow );
+
+ // name
+
+ var objectNameRow = new UI.Row();
+ var objectName = new UI.Input().setWidth( '150px' ).setFontSize( '12px' ).onChange( function () {
+
+ editor.execute( new SetValueCommand( editor.selected, 'name', objectName.getValue() ) );
+
+ } );
+
+ objectNameRow.add( new UI.Text( 'Name' ).setWidth( '90px' ) );
+ objectNameRow.add( objectName );
+
+ container.add( objectNameRow );
+
+ // position
+
+ var objectPositionRow = new UI.Row();
+ var objectPositionX = new UI.Number().setWidth( '50px' ).onChange( update );
+ var objectPositionY = new UI.Number().setWidth( '50px' ).onChange( update );
+ var objectPositionZ = new UI.Number().setWidth( '50px' ).onChange( update );
+
+ objectPositionRow.add( new UI.Text( 'Position' ).setWidth( '90px' ) );
+ objectPositionRow.add( objectPositionZ, objectPositionX, objectPositionY );
+
+ container.add( objectPositionRow );
+
+ // rotation
+
+ var objectRotationRow = new UI.Row();
+ var objectRotationX = new UI.Number().setStep( 10 ).setUnit( '°' ).setWidth( '50px' ).onChange( update );
+ var objectRotationY = new UI.Number().setStep( 10 ).setUnit( '°' ).setWidth( '50px' ).onChange( update );
+ var objectRotationZ = new UI.Number().setStep( 10 ).setUnit( '°' ).setWidth( '50px' ).onChange( update );
+
+ objectRotationRow.add( new UI.Text( 'Rotation' ).setWidth( '90px' ) );
+ objectRotationRow.add( objectRotationZ, objectRotationX, objectRotationY );
+
+ //container.add( objectRotationRow );
+
+ // scale
+
+ var objectScaleRow = new UI.Row();
+ var objectScaleLock = new UI.Checkbox( true ).setPosition( 'absolute' ).setLeft( '75px' );
+ var objectScaleX = new UI.Number( 1 ).setRange( 0.01, Infinity ).setWidth( '50px' ).onChange( updateScaleX );
+ var objectScaleY = new UI.Number( 1 ).setRange( 0.01, Infinity ).setWidth( '50px' ).onChange( updateScaleY );
+ var objectScaleZ = new UI.Number( 1 ).setRange( 0.01, Infinity ).setWidth( '50px' ).onChange( updateScaleZ );
+
+ objectScaleRow.add( new UI.Text( 'Scale' ).setWidth( '90px' ) );
+ //objectScaleRow.add( objectScaleLock );
+ objectScaleRow.add( objectScaleZ, objectScaleX, objectScaleY ); // swap y and z axis
+
+ //container.add( objectScaleRow );
+
+ // fov
+
+ var objectFovRow = new UI.Row();
+ var objectFov = new UI.Number().onChange( update );
+
+ objectFovRow.add( new UI.Text( 'Fov' ).setWidth( '90px' ) );
+ objectFovRow.add( objectFov );
+
+ //container.add( objectFovRow );
+
+ // near
+
+ var objectNearRow = new UI.Row();
+ var objectNear = new UI.Number().onChange( update );
+
+ objectNearRow.add( new UI.Text( 'Near' ).setWidth( '90px' ) );
+ objectNearRow.add( objectNear );
+
+ //container.add( objectNearRow );
+
+ // far
+
+ var objectFarRow = new UI.Row();
+ var objectFar = new UI.Number().onChange( update );
+
+ objectFarRow.add( new UI.Text( 'Far' ).setWidth( '90px' ) );
+ objectFarRow.add( objectFar );
+
+ //container.add( objectFarRow );
+
+ // intensity
+
+ var objectIntensityRow = new UI.Row();
+ var objectIntensity = new UI.Number().setRange( 0, Infinity ).onChange( update );
+
+ objectIntensityRow.add( new UI.Text( 'Intensity' ).setWidth( '90px' ) );
+ objectIntensityRow.add( objectIntensity );
+
+ //container.add( objectIntensityRow );
+
+ // color
+
+ var objectColorRow = new UI.Row();
+ var objectColor = new UI.Color().onChange( update );
+
+ objectColorRow.add( new UI.Text( 'Color' ).setWidth( '90px' ) );
+ objectColorRow.add( objectColor );
+
+ //container.add( objectColorRow );
+
+ // ground color
+
+ var objectGroundColorRow = new UI.Row();
+ var objectGroundColor = new UI.Color().onChange( update );
+
+ objectGroundColorRow.add( new UI.Text( 'Ground color' ).setWidth( '90px' ) );
+ objectGroundColorRow.add( objectGroundColor );
+
+ //container.add( objectGroundColorRow );
+
+ // distance
+
+ var objectDistanceRow = new UI.Row();
+ var objectDistance = new UI.Number().setRange( 0, Infinity ).onChange( update );
+
+ objectDistanceRow.add( new UI.Text( 'Distance' ).setWidth( '90px' ) );
+ objectDistanceRow.add( objectDistance );
+
+ //container.add( objectDistanceRow );
+
+ // angle
+
+ var objectAngleRow = new UI.Row();
+ var objectAngle = new UI.Number().setPrecision( 3 ).setRange( 0, Math.PI / 2 ).onChange( update );
+
+ objectAngleRow.add( new UI.Text( 'Angle' ).setWidth( '90px' ) );
+ objectAngleRow.add( objectAngle );
+
+ //container.add( objectAngleRow );
+
+ // penumbra
+
+ var objectPenumbraRow = new UI.Row();
+ var objectPenumbra = new UI.Number().setRange( 0, 1 ).onChange( update );
+
+ objectPenumbraRow.add( new UI.Text( 'Penumbra' ).setWidth( '90px' ) );
+ objectPenumbraRow.add( objectPenumbra );
+
+ //container.add( objectPenumbraRow );
+
+ // decay
+
+ var objectDecayRow = new UI.Row();
+ var objectDecay = new UI.Number().setRange( 0, Infinity ).onChange( update );
+
+ objectDecayRow.add( new UI.Text( 'Decay' ).setWidth( '90px' ) );
+ objectDecayRow.add( objectDecay );
+
+ //container.add( objectDecayRow );
+
+ // shadow
+
+ var objectShadowRow = new UI.Row();
+
+ objectShadowRow.add( new UI.Text( 'Shadow' ).setWidth( '90px' ) );
+
+ var objectCastShadow = new UI.THREE.Boolean( false, 'cast' ).onChange( update );
+ objectShadowRow.add( objectCastShadow );
+
+ var objectReceiveShadow = new UI.THREE.Boolean( false, 'receive' ).onChange( update );
+ objectShadowRow.add( objectReceiveShadow );
+
+ var objectShadowRadius = new UI.Number( 1 ).onChange( update );
+ objectShadowRow.add( objectShadowRadius );
+
+ //container.add( objectShadowRow );
+
+ // visible
+
+ var objectVisibleRow = new UI.Row();
+ var objectVisible = new UI.Checkbox().onChange( update );
+
+ objectVisibleRow.add( new UI.Text( 'Visible' ).setWidth( '90px' ) );
+ objectVisibleRow.add( objectVisible );
+
+ container.add( objectVisibleRow );
+
+ //wireframe
+ var materialWireframeRow = new UI.Row();
+ var materialWireframe = new UI.Checkbox( false ).onChange( update );
+
+ materialWireframeRow.add( new UI.Text( 'Wireframe' ).setWidth( '90px' ) );
+ materialWireframeRow.add( materialWireframe );
+
+ container.add( materialWireframeRow );
+
+ // user data
+
+ var timeout;
+
+ var objectUserDataRow = new UI.Row();
+ var objectUserData = new UI.TextArea().setWidth( '150px' ).setHeight( '40px' ).setFontSize( '12px' ).onChange( update );
+ objectUserData.onKeyUp( function () {
+
+ try {
+
+ JSON.parse( objectUserData.getValue() );
+
+ objectUserData.dom.classList.add( 'success' );
+ objectUserData.dom.classList.remove( 'fail' );
+
+ } catch ( error ) {
+
+ objectUserData.dom.classList.remove( 'success' );
+ objectUserData.dom.classList.add( 'fail' );
+
+ }
+
+ } );
+
+ objectUserDataRow.add( new UI.Text( 'User data' ).setWidth( '90px' ) );
+ objectUserDataRow.add( objectUserData );
+
+ //container.add( objectUserDataRow );
+
+
+ //
+
+ function updateScaleX() {
+
+ var object = editor.selected;
+
+ if ( objectScaleLock.getValue() === true ) {
+
+ var scale = objectScaleX.getValue() / object.scale.x;
+
+ objectScaleY.setValue( objectScaleY.getValue() * scale );
+ objectScaleZ.setValue( objectScaleZ.getValue() * scale );
+
+ }
+
+ update();
+
+ }
+
+ function updateScaleY() {
+
+ var object = editor.selected;
+
+ if ( objectScaleLock.getValue() === true ) {
+
+ var scale = objectScaleY.getValue() / object.scale.y;
+
+ objectScaleX.setValue( objectScaleX.getValue() * scale );
+ objectScaleZ.setValue( objectScaleZ.getValue() * scale );
+
+ }
+
+ update();
+
+ }
+
+ function updateScaleZ() {
+
+ var object = editor.selected;
+
+ if ( objectScaleLock.getValue() === true ) {
+
+ var scale = objectScaleZ.getValue() / object.scale.z;
+
+ objectScaleX.setValue( objectScaleX.getValue() * scale );
+ objectScaleY.setValue( objectScaleY.getValue() * scale );
+
+ }
+
+ update();
+
+ }
+
+ function update() {
+
+ var object = editor.selected;
+
+ if ( object !== null ) {
+
+ var right_wing = editor.getModel()[0]; // convert entered coordinates to three.js standard
+ var left_wing = editor.getModel()[1];
+ var y_slope = ( right_wing - left_wing ) / editor.getModelWingspan();
+ var x = ( objectPositionX.getValue() * y_slope );
+
+ var z_nose = editor.getModel()[3];
+ var z_tail = editor.getModel()[2];
+ var z_slope = ( z_nose - z_tail ) / editor.getModelHeight();
+ var y = ( ( objectPositionY.getValue() * z_slope ) + z_nose );
+
+ var x_nose = editor.getModel()[4];
+ var x_tail = editor.getModel()[5];
+ var x_slope = ( x_nose - x_tail ) / editor.getModelLength();
+ var z = x_nose + ( objectPositionZ.getValue() * x_slope );
+
+ var newPosition = new THREE.Vector3( x, y, z );
+ if ( object.position.distanceTo( newPosition ) >= 0.01 ) {
+
+ editor.execute( new SetPositionCommand( object, newPosition ) );
+
+ }
+
+ var newRotation = new THREE.Euler( objectRotationX.getValue() * THREE.Math.DEG2RAD, objectRotationY.getValue() * THREE.Math.DEG2RAD, objectRotationZ.getValue() * THREE.Math.DEG2RAD );
+ if ( object.rotation.toVector3().distanceTo( newRotation.toVector3() ) >= 0.01 ) {
+
+ editor.execute( new SetRotationCommand( object, newRotation ) );
+
+ }
+
+ var newScale = new THREE.Vector3( objectScaleX.getValue(), objectScaleY.getValue(), objectScaleZ.getValue() );
+ if ( object.scale.distanceTo( newScale ) >= 0.001 ) {
+
+ editor.execute( new SetScaleCommand( object, newScale ) );
+
+ }
+
+ if ( object.fov !== undefined && Math.abs( object.fov - objectFov.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'fov', objectFov.getValue() ) );
+ object.updateProjectionMatrix();
+
+ }
+
+ if ( object.near !== undefined && Math.abs( object.near - objectNear.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'near', objectNear.getValue() ) );
+
+ }
+
+ if ( object.far !== undefined && Math.abs( object.far - objectFar.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'far', objectFar.getValue() ) );
+
+ }
+
+ if ( object.intensity !== undefined && Math.abs( object.intensity - objectIntensity.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'intensity', objectIntensity.getValue() ) );
+
+ }
+
+ if ( object.color !== undefined && object.color.getHex() !== objectColor.getHexValue() ) {
+
+ editor.execute( new SetColorCommand( object, 'color', objectColor.getHexValue() ) );
+
+ }
+
+ if ( object.groundColor !== undefined && object.groundColor.getHex() !== objectGroundColor.getHexValue() ) {
+
+ editor.execute( new SetColorCommand( object, 'groundColor', objectGroundColor.getHexValue() ) );
+
+ }
+
+ if ( object.distance !== undefined && Math.abs( object.distance - objectDistance.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'distance', objectDistance.getValue() ) );
+
+ }
+
+ if ( object.angle !== undefined && Math.abs( object.angle - objectAngle.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'angle', objectAngle.getValue() ) );
+
+ }
+
+ if ( object.penumbra !== undefined && Math.abs( object.penumbra - objectPenumbra.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'penumbra', objectPenumbra.getValue() ) );
+
+ }
+
+ if ( object.decay !== undefined && Math.abs( object.decay - objectDecay.getValue() ) >= 0.01 ) {
+
+ editor.execute( new SetValueCommand( object, 'decay', objectDecay.getValue() ) );
+
+ }
+
+ if ( object.visible !== objectVisible.getValue() ) {
+
+ editor.execute( new SetValueCommand( object, 'visible', objectVisible.getValue() ) );
+
+ }
+
+ if ( (editor.getObjectMaterial((object.children)[0])).wireframe !== undefined && (editor.getObjectMaterial((object.children)[0])).wireframe !== materialWireframe.getValue() ){
+
+ var objects = object.children;
+
+ var cmds = [];
+ var currentObject;
+
+ editor.execute(new SetMaterialValueCommand( objects[ 3 ], 'wireframe', materialWireframe.getValue()));
+
+ for ( var i = 0, l = objects.length; i < l; i ++ ) {
+ currentObject = objects[ i ];
+
+ cmds.push(new SetMaterialValueCommand( currentObject, 'wireframe', materialWireframe.getValue()));
+ }
+
+ editor.execute( new MultiCmdsCommand(cmds) );
+
+ }
+
+ if ( object.castShadow !== undefined && object.castShadow !== objectCastShadow.getValue() ) {
+
+ editor.execute( new SetValueCommand( object, 'castShadow', objectCastShadow.getValue() ) );
+
+ }
+
+ if ( object.receiveShadow !== undefined && object.receiveShadow !== objectReceiveShadow.getValue() ) {
+
+ editor.execute( new SetValueCommand( object, 'receiveShadow', objectReceiveShadow.getValue() ) );
+ object.material.needsUpdate = true;
+
+ }
+
+ if ( object.shadow !== undefined ) {
+
+ if ( object.shadow.radius !== objectShadowRadius.getValue() ) {
+
+ editor.execute( new SetValueCommand( object.shadow, 'radius', objectShadowRadius.getValue() ) );
+
+ }
+
+ }
+
+ try {
+
+ var userData = JSON.parse( objectUserData.getValue() );
+ if ( JSON.stringify( object.userData ) != JSON.stringify( userData ) ) {
+
+ editor.execute( new SetValueCommand( object, 'userData', userData ) );
+
+ }
+
+ } catch ( exception ) {
+
+ console.warn( exception );
+
+ }
+
+ }
+
+ }
+
+ function updateRows( object ) {
+
+ var properties = {
+ 'fov': objectFovRow,
+ 'near': objectNearRow,
+ 'far': objectFarRow,
+ 'intensity': objectIntensityRow,
+ 'color': objectColorRow,
+ 'groundColor': objectGroundColorRow,
+ 'distance' : objectDistanceRow,
+ 'angle' : objectAngleRow,
+ 'penumbra' : objectPenumbraRow,
+ 'decay' : objectDecayRow,
+ 'castShadow' : objectShadowRow,
+ 'receiveShadow' : objectReceiveShadow,
+ 'shadow': objectShadowRadius
+ };
+
+ for ( var property in properties ) {
+
+ properties[ property ].setDisplay( object[ property ] !== undefined ? '' : 'none' );
+
+ }
+
+ }
+
+ function updateTransformRows( object ) {
+
+ if ( object instanceof THREE.Light ||
+ ( object instanceof THREE.Object3D && object.userData.targetInverse ) ) {
+
+ objectRotationRow.setDisplay( 'none' );
+ objectScaleRow.setDisplay( 'none' );
+
+ } else {
+
+ objectRotationRow.setDisplay( '' );
+ objectScaleRow.setDisplay( '' );
+
+ }
+
+ }
+
+ // events
+
+ signals.objectSelected.add( function ( object ) {
+
+ if ( object !== null ) {
+
+ container.setDisplay( 'block' );
+
+ updateRows( object );
+ updateUI( object );
+
+ } else {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ } );
+
+ signals.objectChanged.add( function ( object ) {
+
+ if ( object !== editor.selected ) return;
+
+ updateUI( object );
+
+ } );
+
+ signals.refreshSidebarObject3D.add( function ( object ) {
+
+ if ( object !== editor.selected ) return;
+
+ updateUI( object );
+
+ } );
+
+ function updateUI( object ) {
+
+ objectType.setValue( object.type );
+ objectUUID.setValue( object.uuid );
+ objectName.setValue( object.name );
+
+
+ var right_wing = editor.getModel()[0]; // convert three.js coordinates back to meters for display
+ var left_wing = editor.getModel()[1];
+ var y_slope = ( right_wing - left_wing ) / editor.getModelWingspan();
+ var x = object.position.x / y_slope;
+ objectPositionX.setValue( x );
+
+ var z_nose = editor.getModel()[3];
+ var z_tail = editor.getModel()[2];
+ var z_slope = ( z_nose - z_tail ) / editor.getModelHeight();
+ var y = ( object.position.y - z_nose ) / z_slope;
+ objectPositionY.setValue( y );
+
+
+ var x_nose = editor.getModel()[4];
+ var x_tail = editor.getModel()[5];
+ var x_slope = ( x_nose - x_tail ) / editor.getModelLength();
+ var z = ( object.position.z - x_nose ) / x_slope;
+ objectPositionZ.setValue( z );
+
+ objectRotationX.setValue( object.rotation.x * THREE.Math.RAD2DEG );
+ objectRotationY.setValue( object.rotation.y * THREE.Math.RAD2DEG );
+ objectRotationZ.setValue( object.rotation.z * THREE.Math.RAD2DEG );
+
+ objectScaleX.setValue( object.scale.x );
+ objectScaleY.setValue( object.scale.y );
+ objectScaleZ.setValue( object.scale.z );
+
+ if ( object.fov !== undefined ) {
+
+ objectFov.setValue( object.fov );
+
+ }
+
+ if ( object.near !== undefined ) {
+
+ objectNear.setValue( object.near );
+
+ }
+
+ if ( object.far !== undefined ) {
+
+ objectFar.setValue( object.far );
+
+ }
+
+ if ( object.intensity !== undefined ) {
+
+ objectIntensity.setValue( object.intensity );
+
+ }
+
+ if ( object.color !== undefined ) {
+
+ objectColor.setHexValue( object.color.getHexString() );
+
+ }
+
+ if ( object.groundColor !== undefined ) {
+
+ objectGroundColor.setHexValue( object.groundColor.getHexString() );
+
+ }
+
+ if ( object.distance !== undefined ) {
+
+ objectDistance.setValue( object.distance );
+
+ }
+
+ if ( object.angle !== undefined ) {
+
+ objectAngle.setValue( object.angle );
+
+ }
+
+ if ( object.penumbra !== undefined ) {
+
+ objectPenumbra.setValue( object.penumbra );
+
+ }
+
+ if ( object.decay !== undefined ) {
+
+ objectDecay.setValue( object.decay );
+
+ }
+
+ if ( object.castShadow !== undefined ) {
+
+ objectCastShadow.setValue( object.castShadow );
+
+ }
+
+ if ( object.receiveShadow !== undefined ) {
+
+ objectReceiveShadow.setValue( object.receiveShadow );
+
+ }
+
+ if ( object.shadow !== undefined ) {
+
+ objectShadowRadius.setValue( object.shadow.radius );
+
+ }
+
+ objectVisible.setValue( object.visible );
+
+ try {
+
+ objectUserData.setValue( JSON.stringify( object.userData, null, ' ' ) );
+
+ } catch ( error ) {
+
+ console.log( error );
+
+ }
+
+ objectUserData.setBorderColor( 'transparent' );
+ objectUserData.setBackgroundColor( '' );
+
+ updateTransformRows( object );
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Project.js b/app/static/js/Sidebar.Project.js
new file mode 100644
index 0000000..d1c58da
--- /dev/null
+++ b/app/static/js/Sidebar.Project.js
@@ -0,0 +1,155 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Project = function ( editor ) {
+
+ var config = editor.config;
+ var signals = editor.signals;
+
+ var rendererTypes = {
+
+ 'WebGLRenderer': THREE.WebGLRenderer,
+ 'CanvasRenderer': THREE.CanvasRenderer,
+ 'SVGRenderer': THREE.SVGRenderer,
+ 'SoftwareRenderer': THREE.SoftwareRenderer,
+ 'RaytracingRenderer': THREE.RaytracingRenderer
+
+ };
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+
+ // class
+
+ var options = {};
+
+ for ( var key in rendererTypes ) {
+
+ if ( key.indexOf( 'WebGL' ) >= 0 && System.support.webgl === false ) continue;
+
+ options[ key ] = key;
+
+ }
+
+ var rendererTypeRow = new UI.Row();
+ var rendererType = new UI.Select().setOptions( options ).setWidth( '150px' ).onChange( function () {
+
+ var value = this.getValue();
+
+ config.setKey( 'project/renderer', value );
+
+ updateRenderer();
+
+ } );
+
+ rendererTypeRow.add( new UI.Text( 'Renderer' ).setWidth( '90px' ) );
+ rendererTypeRow.add( rendererType );
+
+ container.add( rendererTypeRow );
+
+ if ( config.getKey( 'project/renderer' ) !== undefined ) {
+
+ rendererType.setValue( config.getKey( 'project/renderer' ) );
+
+ }
+
+ // antialiasing
+
+ var rendererPropertiesRow = new UI.Row().setMarginLeft( '90px' );
+
+ var rendererAntialias = new UI.THREE.Boolean( config.getKey( 'project/renderer/antialias' ), 'antialias' ).onChange( function () {
+
+ config.setKey( 'project/renderer/antialias', this.getValue() );
+ updateRenderer();
+
+ } );
+ rendererPropertiesRow.add( rendererAntialias );
+
+ // shadow
+
+ var rendererShadows = new UI.THREE.Boolean( config.getKey( 'project/renderer/shadows' ), 'shadows' ).onChange( function () {
+
+ config.setKey( 'project/renderer/shadows', this.getValue() );
+ updateRenderer();
+
+ } );
+ rendererPropertiesRow.add( rendererShadows );
+
+ rendererPropertiesRow.add( new UI.Break() );
+
+ // gamma input
+
+ var rendererGammaInput = new UI.THREE.Boolean( config.getKey( 'project/renderer/gammaInput' ), 'γ input' ).onChange( function () {
+
+ config.setKey( 'project/renderer/gammaInput', this.getValue() );
+ updateRenderer();
+
+ } );
+ rendererPropertiesRow.add( rendererGammaInput );
+
+ // gamma output
+
+ var rendererGammaOutput = new UI.THREE.Boolean( config.getKey( 'project/renderer/gammaOutput' ), 'γ output' ).onChange( function () {
+
+ config.setKey( 'project/renderer/gammaOutput', this.getValue() );
+ updateRenderer();
+
+ } );
+ rendererPropertiesRow.add( rendererGammaOutput );
+
+ container.add( rendererPropertiesRow );
+
+ // VR
+
+ var vrRow = new UI.Row();
+ var vr = new UI.Checkbox( config.getKey( 'project/vr' ) ).setLeft( '100px' ).onChange( function () {
+
+ config.setKey( 'project/vr', this.getValue() );
+ // updateRenderer();
+
+ } );
+
+ vrRow.add( new UI.Text( 'VR' ).setWidth( '90px' ) );
+ vrRow.add( vr );
+
+ container.add( vrRow );
+
+ //
+
+ function updateRenderer() {
+
+ createRenderer( rendererType.getValue(), rendererAntialias.getValue(), rendererShadows.getValue(), rendererGammaInput.getValue(), rendererGammaOutput.getValue() );
+
+ }
+
+ function createRenderer( type, antialias, shadows, gammaIn, gammaOut ) {
+
+ if ( type === 'WebGLRenderer' && System.support.webgl === false ) {
+
+ type = 'CanvasRenderer';
+
+ }
+
+ rendererPropertiesRow.setDisplay( type === 'WebGLRenderer' ? '' : 'none' );
+
+ var renderer = new rendererTypes[ type ]( { antialias: antialias} );
+ renderer.gammaInput = gammaIn;
+ renderer.gammaOutput = gammaOut;
+ if ( shadows && renderer.shadowMap ) {
+
+ renderer.shadowMap.enabled = true;
+ // renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+ }
+
+ signals.rendererChanged.dispatch( renderer );
+
+ }
+
+ createRenderer( config.getKey( 'project/renderer' ), config.getKey( 'project/renderer/antialias' ), config.getKey( 'project/renderer/shadows' ), config.getKey( 'project/renderer/gammaInput' ), config.getKey( 'project/renderer/gammaOutput' ) );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Properties.js b/app/static/js/Sidebar.Properties.js
new file mode 100644
index 0000000..98135c4
--- /dev/null
+++ b/app/static/js/Sidebar.Properties.js
@@ -0,0 +1,35 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Properties = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Span();
+
+ var objectTab = new UI.Text( 'OBJECT' ).onClick( onClick );
+
+ var tabs = new UI.Div();
+ tabs.setId( 'tabs' );
+ tabs.add( objectTab );
+ container.add( tabs );
+
+ function onClick( event ) {
+
+ select( event.target.textContent );
+
+ }
+
+ //
+
+ var object = new UI.Span().add(
+ new Sidebar.Object( editor )
+ );
+ container.add( object );
+
+ //
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Scene.js b/app/static/js/Sidebar.Scene.js
new file mode 100644
index 0000000..48544fd
--- /dev/null
+++ b/app/static/js/Sidebar.Scene.js
@@ -0,0 +1,329 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Scene = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+
+ // outliner
+
+ function buildOption( object, draggable ) {
+
+ var option = document.createElement( 'div' );
+ option.draggable = draggable;
+ option.innerHTML = buildHTML( object );
+ option.value = object.id;
+
+ return option;
+
+ }
+
+ function getMaterialName( material ) {
+
+ if ( Array.isArray( material ) ) {
+
+ var array = [];
+
+ for ( var i = 0; i < material.length; i ++ ) {
+
+ array.push( material[ i ].name );
+
+ }
+
+ return array.join( ',' );
+
+ }
+
+ return material.name;
+
+ }
+
+ function buildHTML( object ) {
+
+ var html = ' ' + object.name;
+
+ if ( object instanceof THREE.Mesh ) {
+
+ var geometry = object.geometry;
+ var material = object.material;
+
+ html += ' ' + geometry.name;
+ html += ' ' + getMaterialName( material );
+
+ }
+
+ html += getScript( object.uuid );
+
+ return html;
+
+ }
+
+ function getScript( uuid ) {
+
+ if ( editor.scripts[ uuid ] !== undefined ) {
+
+ return ' ';
+
+ }
+
+ return '';
+
+ }
+
+ var ignoreObjectSelectedSignal = false;
+
+ var outliner = new UI.Outliner( editor );
+ outliner.setId( 'outliner' );
+ outliner.onChange( function () {
+
+ ignoreObjectSelectedSignal = true;
+
+ editor.selectById( parseInt( outliner.getValue() ) );
+
+ ignoreObjectSelectedSignal = false;
+
+ } );
+ outliner.onDblClick( function () {
+
+ editor.focusById( parseInt( outliner.getValue() ) );
+
+ } );
+ container.add( outliner );
+ container.add( new UI.Break() );
+
+ // background
+
+ function onBackgroundChanged() {
+
+ signals.sceneBackgroundChanged.dispatch( backgroundColor.getHexValue() );
+
+ }
+
+ var backgroundRow = new UI.Row();
+
+ var backgroundColor = new UI.Color().setValue( '#aaaaaa' ).onChange( onBackgroundChanged );
+
+ backgroundRow.add( new UI.Text( 'Background' ).setWidth( '90px' ) );
+ backgroundRow.add( backgroundColor );
+
+ container.add( backgroundRow );
+ container.add( new UI.HorizontalRule() );
+
+ // Model Dimensions
+
+ container.add( new UI.Text( 'Aircraft Dimensions (meters)' ) );
+ container.add( new UI.Break() );
+ container.add( new UI.Break() );
+
+ var lengthRow = new UI.Row();
+ var length = new UI.Number().setWidth( '50px' ).onChange( update );
+ lengthRow.add( new UI.Text( 'Length' ).setMarginLeft('15px').setWidth( '120px' ) );
+ lengthRow.add( length );
+
+ var wingspanRow = new UI.Row();
+ var wingspan = new UI.Number().setWidth( '50px' ).onChange( update );
+ wingspanRow.add( new UI.Text( 'Wingspan' ).setMarginLeft('15px').setWidth( '120px' ) );
+ wingspanRow.add( wingspan );
+
+ var heightRow = new UI.Row();
+ var height = new UI.Number().setWidth( '50px' ).onChange( update );
+ heightRow.add( new UI.Text( 'Height (nose up)' ).setMarginLeft('15px').setWidth( '120px' ) );
+ heightRow.add( height );
+
+ container.add( lengthRow );
+ container.add( wingspanRow );
+ container.add( heightRow );
+
+ // fog
+
+ function onFogChanged() {
+
+ signals.sceneFogChanged.dispatch(
+ fogType.getValue(),
+ fogColor.getHexValue(),
+ fogNear.getValue(),
+ fogFar.getValue(),
+ fogDensity.getValue()
+ );
+
+ }
+
+ var fogTypeRow = new UI.Row();
+ var fogType = new UI.Select().setOptions( {
+
+ 'None': 'None',
+ 'Fog': 'Linear',
+ 'FogExp2': 'Exponential'
+
+ } ).setWidth( '150px' );
+ fogType.onChange( function () {
+
+ onFogChanged();
+ refreshFogUI();
+
+ } );
+
+ fogTypeRow.add( new UI.Text( 'Fog' ).setWidth( '90px' ) );
+ fogTypeRow.add( fogType );
+
+ //container.add( fogTypeRow );
+
+ // fog color
+
+ var fogPropertiesRow = new UI.Row();
+ fogPropertiesRow.setDisplay( 'none' );
+ fogPropertiesRow.setMarginLeft( '90px' );
+ container.add( fogPropertiesRow );
+
+ var fogColor = new UI.Color().setValue( '#aaaaaa' );
+ fogColor.onChange( onFogChanged );
+ fogPropertiesRow.add( fogColor );
+
+ // fog near
+
+ var fogNear = new UI.Number( 0.1 ).setWidth( '40px' ).setRange( 0, Infinity ).onChange( onFogChanged );
+ fogPropertiesRow.add( fogNear );
+
+ // fog far
+
+ var fogFar = new UI.Number( 50 ).setWidth( '40px' ).setRange( 0, Infinity ).onChange( onFogChanged );
+ fogPropertiesRow.add( fogFar );
+
+ // fog density
+
+ var fogDensity = new UI.Number( 0.05 ).setWidth( '40px' ).setRange( 0, 0.1 ).setPrecision( 3 ).onChange( onFogChanged );
+ fogPropertiesRow.add( fogDensity );
+
+ //
+
+ function refreshUI() {
+
+ var camera = editor.camera;
+ var scene = editor.scene;
+
+ var options = [];
+
+ options.push( buildOption( camera, false ) );
+ options.push( buildOption( scene, false ) );
+
+ length.setValue( editor.getModelLength() ); // refresh entered dimensions back for display
+ wingspan.setValue( editor.getModelWingspan() );
+ height.setValue( editor.getModelHeight() );
+
+ ( function addObjects( objects, pad ) {
+
+ for ( var i = 0, l = objects.length; i < l; i ++ ) {
+
+ var object = objects[ i ];
+
+ var option = buildOption( object, true );
+ option.style.paddingLeft = ( pad * 10 ) + 'px';
+ options.push( option );
+
+ //addObjects( object.children, pad + 1 );
+
+ }
+
+ } )( scene.children, 1 );
+
+ outliner.setOptions( options );
+
+ if ( editor.selected !== null ) {
+
+ outliner.setValue( editor.selected.id );
+
+ }
+
+ if ( scene.background ) {
+
+ backgroundColor.setHexValue( scene.background.getHex() );
+
+ }
+
+ if ( scene.fog ) {
+
+ fogColor.setHexValue( scene.fog.color.getHex() );
+
+ if ( scene.fog instanceof THREE.Fog ) {
+
+ fogType.setValue( "Fog" );
+ fogNear.setValue( scene.fog.near );
+ fogFar.setValue( scene.fog.far );
+
+ } else if ( scene.fog instanceof THREE.FogExp2 ) {
+
+ fogType.setValue( "FogExp2" );
+ fogDensity.setValue( scene.fog.density );
+
+ }
+
+ } else {
+
+ fogType.setValue( "None" );
+
+ }
+
+ refreshFogUI();
+
+ }
+
+ function refreshFogUI() {
+
+ var type = fogType.getValue();
+
+ fogPropertiesRow.setDisplay( type === 'None' ? 'none' : '' );
+ fogNear.setDisplay( type === 'Fog' ? '' : 'none' );
+ fogFar.setDisplay( type === 'Fog' ? '' : 'none' );
+ fogDensity.setDisplay( type === 'FogExp2' ? '' : 'none' );
+
+ }
+
+ refreshUI();
+
+ // events
+
+ signals.editorCleared.add( refreshUI );
+
+ signals.sceneGraphChanged.add( refreshUI );
+
+ signals.objectChanged.add( function ( object ) {
+
+ var options = outliner.options;
+
+ for ( var i = 0; i < options.length; i ++ ) {
+
+ var option = options[ i ];
+
+ if ( option.value === object.id ) {
+
+ option.innerHTML = buildHTML( object );
+ return;
+
+ }
+
+ }
+
+ } );
+
+ signals.objectSelected.add( function ( object ) {
+
+ if ( ignoreObjectSelectedSignal === true ) return;
+
+ outliner.setValue( object !== null ? object.id : null );
+
+ } );
+
+ function update() {
+
+ editor.setModelDimensions( length.getValue(), wingspan.getValue(), height.getValue() )
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Script.js b/app/static/js/Sidebar.Script.js
new file mode 100644
index 0000000..e1698e8
--- /dev/null
+++ b/app/static/js/Sidebar.Script.js
@@ -0,0 +1,125 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Script = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setDisplay( 'none' );
+
+ container.add( new UI.Text( 'Script' ).setTextTransform( 'uppercase' ) );
+ container.add( new UI.Break() );
+ container.add( new UI.Break() );
+
+ //
+
+ var scriptsContainer = new UI.Row();
+ container.add( scriptsContainer );
+
+ var newScript = new UI.Button( 'New' );
+ newScript.onClick( function () {
+
+ var script = { name: '', source: 'function update( event ) {}' };
+ editor.execute( new AddScriptCommand( editor.selected, script ) );
+
+ } );
+ container.add( newScript );
+
+ /*
+ var loadScript = new UI.Button( 'Load' );
+ loadScript.setMarginLeft( '4px' );
+ container.add( loadScript );
+ */
+
+ //
+
+ function update() {
+
+ scriptsContainer.clear();
+ scriptsContainer.setDisplay( 'none' );
+
+ var object = editor.selected;
+
+ if ( object === null ) {
+
+ return;
+
+ }
+
+ var scripts = editor.scripts[ object.uuid ];
+
+ if ( scripts !== undefined ) {
+
+ scriptsContainer.setDisplay( 'block' );
+
+ for ( var i = 0; i < scripts.length; i ++ ) {
+
+ ( function ( object, script ) {
+
+ var name = new UI.Input( script.name ).setWidth( '130px' ).setFontSize( '12px' );
+ name.onChange( function () {
+
+ editor.execute( new SetScriptValueCommand( editor.selected, script, 'name', this.getValue() ) );
+
+ } );
+ scriptsContainer.add( name );
+
+ var edit = new UI.Button( 'Edit' );
+ edit.setMarginLeft( '4px' );
+ edit.onClick( function () {
+
+ signals.editScript.dispatch( object, script );
+
+ } );
+ scriptsContainer.add( edit );
+
+ var remove = new UI.Button( 'Remove' );
+ remove.setMarginLeft( '4px' );
+ remove.onClick( function () {
+
+ if ( confirm( 'Are you sure?' ) ) {
+
+ editor.execute( new RemoveScriptCommand( editor.selected, script ) );
+
+ }
+
+ } );
+ scriptsContainer.add( remove );
+
+ scriptsContainer.add( new UI.Break() );
+
+ } )( object, scripts[ i ] )
+
+ }
+
+ }
+
+ }
+
+ // signals
+
+ signals.objectSelected.add( function ( object ) {
+
+ if ( object !== null && editor.camera !== object ) {
+
+ container.setDisplay( 'block' );
+
+ update();
+
+ } else {
+
+ container.setDisplay( 'none' );
+
+ }
+
+ } );
+
+ signals.scriptAdded.add( update );
+ signals.scriptRemoved.add( update );
+ signals.scriptChanged.add( update );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.Settings.js b/app/static/js/Sidebar.Settings.js
new file mode 100644
index 0000000..0249796
--- /dev/null
+++ b/app/static/js/Sidebar.Settings.js
@@ -0,0 +1,47 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Sidebar.Settings = function ( editor ) {
+
+ var config = editor.config;
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setBorderTop( '0' );
+ container.setPaddingTop( '20px' );
+
+ // class
+
+ var options = {
+ 'static/css/light.css': 'light',
+ 'static/css/dark.css': 'dark'
+ };
+
+ var themeRow = new UI.Row();
+ var theme = new UI.Select().setWidth( '150px' );
+ theme.setOptions( options );
+
+ if ( config.getKey( 'theme' ) !== undefined ) {
+
+ theme.setValue( config.getKey( 'theme' ) );
+
+ }
+
+ theme.onChange( function () {
+
+ var value = this.getValue();
+
+ editor.setTheme( value );
+ editor.config.setKey( 'theme', value );
+
+ } );
+
+ themeRow.add( new UI.Text( 'Theme' ).setWidth( '90px' ) );
+ themeRow.add( theme );
+
+ container.add( themeRow );
+
+ return container;
+
+};
diff --git a/app/static/js/Sidebar.js b/app/static/js/Sidebar.js
new file mode 100644
index 0000000..1ecc366
--- /dev/null
+++ b/app/static/js/Sidebar.js
@@ -0,0 +1,72 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Sidebar = function ( editor ) {
+
+ var container = new UI.Panel();
+ container.setId( 'sidebar' );
+
+ //
+
+ var sceneTab = new UI.Text( 'SCENE' ).onClick( onClick );
+ var settingsTab = new UI.Text( 'SETTINGS' ).onClick( onClick );
+
+ var tabs = new UI.Div();
+ tabs.setId( 'tabs' );
+ tabs.add( sceneTab, settingsTab );
+ container.add( tabs );
+
+ function onClick( event ) {
+
+ select( event.target.textContent );
+
+ }
+
+ //
+
+ var scene = new UI.Span().add(
+ new Sidebar.Scene( editor ),
+ new Sidebar.Properties( editor ),
+ new Sidebar.Animation( editor )
+ );
+ container.add( scene );
+
+ var project = new UI.Span().add(
+ new Sidebar.Project( editor )
+ );
+
+ var settings = new UI.Span().add(
+ new Sidebar.Settings( editor ),
+ new Sidebar.History( editor )
+ );
+ container.add( settings );
+
+ //
+
+ function select( section ) {
+
+ sceneTab.setClass( '' );
+ settingsTab.setClass( '' );
+
+ scene.setDisplay( 'none' );
+ settings.setDisplay( 'none' );
+
+ switch ( section ) {
+ case 'SCENE':
+ sceneTab.setClass( 'selected' );
+ scene.setDisplay( '' );
+ break;
+ case 'SETTINGS':
+ settingsTab.setClass( 'selected' );
+ settings.setDisplay( '' );
+ break;
+ }
+
+ }
+
+ select( 'SCENE' );
+
+ return container;
+
+};
diff --git a/app/static/js/SimplexNoise.js b/app/static/js/SimplexNoise.js
new file mode 100644
index 0000000..235b6ac
--- /dev/null
+++ b/app/static/js/SimplexNoise.js
@@ -0,0 +1,324 @@
+// Ported from Stefan Gustavson's java implementation
+// http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
+// Read Stefan's excellent paper for details on how this code works.
+//
+// Sean McCullough banksean@gmail.com
+//
+// Added 4D noise
+// Joshua Koo zz85nus@gmail.com
+
+/**
+ * You can pass in a random number generator object if you like.
+ * It is assumed to have a random() method.
+ */
+var SimplexNoise = function(r) {
+ if (r == undefined) r = Math;
+ this.grad3 = [[ 1,1,0 ],[ -1,1,0 ],[ 1,-1,0 ],[ -1,-1,0 ],
+ [ 1,0,1 ],[ -1,0,1 ],[ 1,0,-1 ],[ -1,0,-1 ],
+ [ 0,1,1 ],[ 0,-1,1 ],[ 0,1,-1 ],[ 0,-1,-1 ]];
+
+ this.grad4 = [[ 0,1,1,1 ], [ 0,1,1,-1 ], [ 0,1,-1,1 ], [ 0,1,-1,-1 ],
+ [ 0,-1,1,1 ], [ 0,-1,1,-1 ], [ 0,-1,-1,1 ], [ 0,-1,-1,-1 ],
+ [ 1,0,1,1 ], [ 1,0,1,-1 ], [ 1,0,-1,1 ], [ 1,0,-1,-1 ],
+ [ -1,0,1,1 ], [ -1,0,1,-1 ], [ -1,0,-1,1 ], [ -1,0,-1,-1 ],
+ [ 1,1,0,1 ], [ 1,1,0,-1 ], [ 1,-1,0,1 ], [ 1,-1,0,-1 ],
+ [ -1,1,0,1 ], [ -1,1,0,-1 ], [ -1,-1,0,1 ], [ -1,-1,0,-1 ],
+ [ 1,1,1,0 ], [ 1,1,-1,0 ], [ 1,-1,1,0 ], [ 1,-1,-1,0 ],
+ [ -1,1,1,0 ], [ -1,1,-1,0 ], [ -1,-1,1,0 ], [ -1,-1,-1,0 ]];
+
+ this.p = [];
+ for (var i = 0; i < 256; i ++) {
+ this.p[i] = Math.floor(r.random() * 256);
+ }
+ // To remove the need for index wrapping, double the permutation table length
+ this.perm = [];
+ for (var i = 0; i < 512; i ++) {
+ this.perm[i] = this.p[i & 255];
+ }
+
+ // A lookup table to traverse the simplex around a given point in 4D.
+ // Details can be found where this table is used, in the 4D noise method.
+ this.simplex = [
+ [ 0,1,2,3 ],[ 0,1,3,2 ],[ 0,0,0,0 ],[ 0,2,3,1 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 1,2,3,0 ],
+ [ 0,2,1,3 ],[ 0,0,0,0 ],[ 0,3,1,2 ],[ 0,3,2,1 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 1,3,2,0 ],
+ [ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],
+ [ 1,2,0,3 ],[ 0,0,0,0 ],[ 1,3,0,2 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 2,3,0,1 ],[ 2,3,1,0 ],
+ [ 1,0,2,3 ],[ 1,0,3,2 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 2,0,3,1 ],[ 0,0,0,0 ],[ 2,1,3,0 ],
+ [ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],
+ [ 2,0,1,3 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 3,0,1,2 ],[ 3,0,2,1 ],[ 0,0,0,0 ],[ 3,1,2,0 ],
+ [ 2,1,0,3 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 0,0,0,0 ],[ 3,1,0,2 ],[ 0,0,0,0 ],[ 3,2,0,1 ],[ 3,2,1,0 ]];
+};
+
+SimplexNoise.prototype.dot = function(g, x, y) {
+ return g[0] * x + g[1] * y;
+};
+
+SimplexNoise.prototype.dot3 = function(g, x, y, z) {
+ return g[0] * x + g[1] * y + g[2] * z;
+};
+
+SimplexNoise.prototype.dot4 = function(g, x, y, z, w) {
+ return g[0] * x + g[1] * y + g[2] * z + g[3] * w;
+};
+
+SimplexNoise.prototype.noise = function(xin, yin) {
+ var n0, n1, n2; // Noise contributions from the three corners
+ // Skew the input space to determine which simplex cell we're in
+ var F2 = 0.5 * (Math.sqrt(3.0) - 1.0);
+ var s = (xin + yin) * F2; // Hairy factor for 2D
+ var i = Math.floor(xin + s);
+ var j = Math.floor(yin + s);
+ var G2 = (3.0 - Math.sqrt(3.0)) / 6.0;
+ var t = (i + j) * G2;
+ var X0 = i - t; // Unskew the cell origin back to (x,y) space
+ var Y0 = j - t;
+ var x0 = xin - X0; // The x,y distances from the cell origin
+ var y0 = yin - Y0;
+ // For the 2D case, the simplex shape is an equilateral triangle.
+ // Determine which simplex we are in.
+ var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
+ if (x0 > y0) {i1 = 1; j1 = 0;} // lower triangle, XY order: (0,0)->(1,0)->(1,1)
+ else {i1 = 0; j1 = 1;} // upper triangle, YX order: (0,0)->(0,1)->(1,1)
+ // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
+ // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
+ // c = (3-sqrt(3))/6
+ var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
+ var y1 = y0 - j1 + G2;
+ var x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords
+ var y2 = y0 - 1.0 + 2.0 * G2;
+ // Work out the hashed gradient indices of the three simplex corners
+ var ii = i & 255;
+ var jj = j & 255;
+ var gi0 = this.perm[ii + this.perm[jj]] % 12;
+ var gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12;
+ var gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12;
+ // Calculate the contribution from the three corners
+ var t0 = 0.5 - x0 * x0 - y0 * y0;
+ if (t0 < 0) n0 = 0.0;
+ else {
+ t0 *= t0;
+ n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0); // (x,y) of grad3 used for 2D gradient
+ }
+ var t1 = 0.5 - x1 * x1 - y1 * y1;
+ if (t1 < 0) n1 = 0.0;
+ else {
+ t1 *= t1;
+ n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1);
+ }
+ var t2 = 0.5 - x2 * x2 - y2 * y2;
+ if (t2 < 0) n2 = 0.0;
+ else {
+ t2 *= t2;
+ n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2);
+ }
+ // Add contributions from each corner to get the final noise value.
+ // The result is scaled to return values in the interval [-1,1].
+ return 70.0 * (n0 + n1 + n2);
+};
+
+// 3D simplex noise
+SimplexNoise.prototype.noise3d = function(xin, yin, zin) {
+ var n0, n1, n2, n3; // Noise contributions from the four corners
+ // Skew the input space to determine which simplex cell we're in
+ var F3 = 1.0 / 3.0;
+ var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D
+ var i = Math.floor(xin + s);
+ var j = Math.floor(yin + s);
+ var k = Math.floor(zin + s);
+ var G3 = 1.0 / 6.0; // Very nice and simple unskew factor, too
+ var t = (i + j + k) * G3;
+ var X0 = i - t; // Unskew the cell origin back to (x,y,z) space
+ var Y0 = j - t;
+ var Z0 = k - t;
+ var x0 = xin - X0; // The x,y,z distances from the cell origin
+ var y0 = yin - Y0;
+ var z0 = zin - Z0;
+ // For the 3D case, the simplex shape is a slightly irregular tetrahedron.
+ // Determine which simplex we are in.
+ var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
+ var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
+ if (x0 >= y0) {
+ if (y0 >= z0)
+ { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0; } // X Y Z order
+ else if (x0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1; } // X Z Y order
+ else { i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1; } // Z X Y order
+ }
+ else { // x0 y0) ? 32 : 0;
+ var c2 = (x0 > z0) ? 16 : 0;
+ var c3 = (y0 > z0) ? 8 : 0;
+ var c4 = (x0 > w0) ? 4 : 0;
+ var c5 = (y0 > w0) ? 2 : 0;
+ var c6 = (z0 > w0) ? 1 : 0;
+ var c = c1 + c2 + c3 + c4 + c5 + c6;
+ var i1, j1, k1, l1; // The integer offsets for the second simplex corner
+ var i2, j2, k2, l2; // The integer offsets for the third simplex corner
+ var i3, j3, k3, l3; // The integer offsets for the fourth simplex corner
+ // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.
+ // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0;
+ j1 = simplex[c][1] >= 3 ? 1 : 0;
+ k1 = simplex[c][2] >= 3 ? 1 : 0;
+ l1 = simplex[c][3] >= 3 ? 1 : 0;
+ // The number 2 in the "simplex" array is at the second largest coordinate.
+ i2 = simplex[c][0] >= 2 ? 1 : 0;
+ j2 = simplex[c][1] >= 2 ? 1 : 0; k2 = simplex[c][2] >= 2 ? 1 : 0;
+ l2 = simplex[c][3] >= 2 ? 1 : 0;
+ // The number 1 in the "simplex" array is at the second smallest coordinate.
+ i3 = simplex[c][0] >= 1 ? 1 : 0;
+ j3 = simplex[c][1] >= 1 ? 1 : 0;
+ k3 = simplex[c][2] >= 1 ? 1 : 0;
+ l3 = simplex[c][3] >= 1 ? 1 : 0;
+ // The fifth corner has all coordinate offsets = 1, so no need to look that up.
+ var x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords
+ var y1 = y0 - j1 + G4;
+ var z1 = z0 - k1 + G4;
+ var w1 = w0 - l1 + G4;
+ var x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords
+ var y2 = y0 - j2 + 2.0 * G4;
+ var z2 = z0 - k2 + 2.0 * G4;
+ var w2 = w0 - l2 + 2.0 * G4;
+ var x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords
+ var y3 = y0 - j3 + 3.0 * G4;
+ var z3 = z0 - k3 + 3.0 * G4;
+ var w3 = w0 - l3 + 3.0 * G4;
+ var x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords
+ var y4 = y0 - 1.0 + 4.0 * G4;
+ var z4 = z0 - 1.0 + 4.0 * G4;
+ var w4 = w0 - 1.0 + 4.0 * G4;
+ // Work out the hashed gradient indices of the five simplex corners
+ var ii = i & 255;
+ var jj = j & 255;
+ var kk = k & 255;
+ var ll = l & 255;
+ var gi0 = perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32;
+ var gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32;
+ var gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32;
+ var gi3 = perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32;
+ var gi4 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32;
+ // Calculate the contribution from the five corners
+ var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0;
+ if (t0 < 0) n0 = 0.0;
+ else {
+ t0 *= t0;
+ n0 = t0 * t0 * this.dot4(grad4[gi0], x0, y0, z0, w0);
+ }
+ var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1;
+ if (t1 < 0) n1 = 0.0;
+ else {
+ t1 *= t1;
+ n1 = t1 * t1 * this.dot4(grad4[gi1], x1, y1, z1, w1);
+ }
+ var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2;
+ if (t2 < 0) n2 = 0.0;
+ else {
+ t2 *= t2;
+ n2 = t2 * t2 * this.dot4(grad4[gi2], x2, y2, z2, w2);
+ } var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3;
+ if (t3 < 0) n3 = 0.0;
+ else {
+ t3 *= t3;
+ n3 = t3 * t3 * this.dot4(grad4[gi3], x3, y3, z3, w3);
+ }
+ var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4;
+ if (t4 < 0) n4 = 0.0;
+ else {
+ t4 *= t4;
+ n4 = t4 * t4 * this.dot4(grad4[gi4], x4, y4, z4, w4);
+ }
+ // Sum up and scale the result to cover the range [-1,1]
+ return 27.0 * (n0 + n1 + n2 + n3 + n4);
+};
diff --git a/app/static/js/Storage.js b/app/static/js/Storage.js
new file mode 100644
index 0000000..36ada10
--- /dev/null
+++ b/app/static/js/Storage.js
@@ -0,0 +1,131 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Storage = function () {
+
+ var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
+
+ if ( indexedDB === undefined ) {
+
+ console.warn( 'Storage: IndexedDB not available.' );
+ return { init: function () {}, get: function () {}, set: function () {}, clear: function () {} };
+
+ }
+
+ var name = 'threejs-editor';
+ var version = 1;
+
+ var database;
+
+ return {
+
+ init: function ( callback ) {
+
+ var request = indexedDB.open( name, version );
+ request.onupgradeneeded = function ( event ) {
+
+ var db = event.target.result;
+
+ if ( db.objectStoreNames.contains( 'states' ) === false ) {
+
+ db.createObjectStore( 'states' );
+
+ }
+
+ };
+ request.onsuccess = function ( event ) {
+
+ database = event.target.result;
+
+ callback();
+
+ };
+ request.onerror = function ( event ) {
+
+ console.error( 'IndexedDB', event );
+
+ };
+
+
+ },
+
+ get: function ( uuid, callback ) {
+
+ if (uuid != "None") {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var myArr = JSON.parse(this.responseText);
+ callback(myArr);
+ }
+ };
+ xhr.open("GET", "/rest/resume_state/" + uuid, true);
+ xhr.send();
+ } else {
+ var transaction = database.transaction( [ 'states' ], 'readwrite' );
+ var objectStore = transaction.objectStore( 'states' );
+
+ var request = objectStore.get( 0 );
+ request.onsuccess = function ( event ) {
+
+ callback( event.target.result );
+
+ };
+ }
+
+
+
+
+ },
+
+ set: function ( data, callback ) {
+
+ var start = performance.now();
+
+ var transaction = database.transaction( [ 'states' ], 'readwrite' );
+ var objectStore = transaction.objectStore( 'states' );
+ //console.log(JSON.stringify(data));
+ var request = objectStore.put( data, 0 );
+
+
+
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var myArr = this.responseText;
+ callback(myArr);
+ }
+};
+ xhr.open('POST', '/rest/save_state');
+ xhr.send(JSON.stringify(data));
+
+ //console.log(data);
+
+ request.onsuccess = function ( event ) {
+
+ console.log( '[' + /\d\d\:\d\d\:\d\d/.exec( new Date() )[ 0 ] + ']', 'Saved state to IndexedDB. ' + ( performance.now() - start ).toFixed( 2 ) + 'ms' );
+
+ };
+
+ },
+
+ clear: function () {
+
+ if ( database === undefined ) return;
+
+ var transaction = database.transaction( [ 'states' ], 'readwrite' );
+ var objectStore = transaction.objectStore( 'states' );
+ var request = objectStore.clear();
+ request.onsuccess = function ( event ) {
+
+ console.log( '[' + /\d\d\:\d\d\:\d\d/.exec( new Date() )[ 0 ] + ']', 'Cleared IndexedDB.' );
+
+ };
+
+ }
+
+ };
+
+};
diff --git a/app/static/js/TimelinerController.js b/app/static/js/TimelinerController.js
new file mode 100644
index 0000000..7e1b291
--- /dev/null
+++ b/app/static/js/TimelinerController.js
@@ -0,0 +1,280 @@
+/**
+ * Controller class for the Timeliner GUI.
+ *
+ * Timeliner GUI library (required to use this class):
+ *
+ * ./libs/timeliner_gui.min.js
+ *
+ * Source code:
+ *
+ * https://github.com/tschw/timeliner_gui
+ * https://github.com/zz85/timeliner (fork's origin)
+ *
+ * @author tschw
+ *
+ */
+
+THREE.TimelinerController = function TimelinerController( scene, trackInfo, onUpdate ) {
+
+ this._scene = scene;
+ this._trackInfo = trackInfo;
+
+ this._onUpdate = onUpdate;
+
+ this._mixer = new THREE.AnimationMixer( scene );
+ this._clip = null;
+ this._action = null;
+
+ this._tracks = {};
+ this._propRefs = {};
+ this._channelNames = [];
+
+};
+
+THREE.TimelinerController.prototype = {
+
+ constructor: THREE.TimelinerController,
+
+ init: function( timeliner ) {
+
+ var tracks = [],
+ trackInfo = this._trackInfo;
+
+ for ( var i = 0, n = trackInfo.length; i !== n; ++ i ) {
+
+ var spec = trackInfo[ i ];
+
+ tracks.push( this._addTrack(
+ spec.type, spec.propertyPath,
+ spec.initialValue, spec.interpolation ) );
+ }
+
+ this._clip = new THREE.AnimationClip( 'editclip', 0, tracks );
+ this._action = this._mixer.clipAction( this._clip ).play();
+
+ },
+
+ setDisplayTime: function( time ) {
+
+ this._action.time = time;
+ this._mixer.update( 0 );
+
+ this._onUpdate();
+
+ },
+
+ setDuration: function( duration ) {
+
+ this._clip.duration = duration;
+
+ },
+
+ getChannelNames: function() {
+
+ return this._channelNames;
+
+ },
+
+ getChannelKeyTimes: function( channelName ) {
+
+ return this._tracks[ channelName ].times;
+
+ },
+
+ setKeyframe: function( channelName, time ) {
+
+ var track = this._tracks[ channelName ],
+ times = track.times,
+ index = Timeliner.binarySearch( times, time ),
+ values = track.values,
+ stride = track.getValueSize(),
+ offset = index * stride;
+
+ if ( index < 0 ) {
+
+ // insert new keyframe
+
+ index = ~ index;
+ offset = index * stride;
+
+ var nTimes = times.length + 1,
+ nValues = values.length + stride;
+
+ for ( var i = nTimes - 1; i !== index; -- i ) {
+
+ times[ i ] = times[ i - 1 ];
+
+ }
+
+ for ( var i = nValues - 1,
+ e = offset + stride - 1; i !== e; -- i ) {
+
+ values[ i ] = values[ i - stride ];
+
+ }
+
+ }
+
+ times[ index ] = time;
+ this._propRefs[ channelName ].getValue( values, offset );
+
+ },
+
+ delKeyframe: function( channelName, time ) {
+
+ var track = this._tracks[ channelName ],
+ times = track.times,
+ index = Timeliner.binarySearch( times, time );
+
+ // we disallow to remove the keyframe when it is the last one we have,
+ // since the animation system is designed to always produce a defined
+ // state
+
+ if ( times.length > 1 && index >= 0 ) {
+
+ var nTimes = times.length - 1,
+ values = track.values,
+ stride = track.getValueSize(),
+ nValues = values.length - stride;
+
+ // note: no track.getValueSize when array sizes are out of sync
+
+ for ( var i = index; i !== nTimes; ++ i ) {
+
+ times[ i ] = times[ i + 1 ];
+
+ }
+
+ times.pop();
+
+ for ( var offset = index * stride; offset !== nValues; ++ offset ) {
+
+ values[ offset ] = values[ offset + stride ];
+
+ }
+
+ values.length = nValues;
+
+ }
+
+ },
+
+ moveKeyframe: function( channelName, time, delta, moveRemaining ) {
+
+ var track = this._tracks[ channelName ],
+ times = track.times,
+ index = Timeliner.binarySearch( times, time );
+
+ if ( index >= 0 ) {
+
+ var endAt = moveRemaining ? times.length : index + 1,
+ needsSort = times[ index - 1 ] <= time ||
+ ! moveRemaining && time >= times[ index + 1 ];
+
+ while ( index !== endAt ) times[ index ++ ] += delta;
+
+ if ( needsSort ) this._sort( track );
+
+ }
+
+ },
+
+ serialize: function() {
+
+ var result = {
+ duration: this._clip.duration,
+ channels: {}
+ },
+
+ names = this._channelNames,
+ tracks = this._tracks,
+
+ channels = result.channels;
+
+ for ( var i = 0, n = names.length; i !== n; ++ i ) {
+
+ var name = names[ i ],
+ track = tracks[ name ];
+
+ channels[ name ] = {
+
+ times: track.times,
+ values: track.values
+
+ };
+
+ }
+
+ return result;
+
+ },
+
+ deserialize: function( structs ) {
+
+ var names = this._channelNames,
+ tracks = this._tracks,
+
+ channels = structs.channels;
+
+ this.setDuration( structs.duration );
+
+ for ( var i = 0, n = names.length; i !== n; ++ i ) {
+
+ var name = names[ i ],
+ track = tracks[ name ],
+ data = channels[ name ];
+
+ this._setArray( track.times, data.times );
+ this._setArray( track.values, data.values );
+
+ }
+
+ // update display
+ this.setDisplayTime( this._mixer.time );
+
+ },
+
+ _sort: function( track ) {
+
+ var times = track.times,
+ order = THREE.AnimationUtils.getKeyframeOrder( times );
+
+ this._setArray( times,
+ THREE.AnimationUtils.sortedArray( times, 1, order ) );
+
+ var values = track.values,
+ stride = track.getValueSize();
+
+ this._setArray( values,
+ THREE.AnimationUtils.sortedArray( values, stride, order ) );
+
+ },
+
+ _setArray: function( dst, src ) {
+
+ dst.length = 0;
+ dst.push.apply( dst, src );
+
+ },
+
+ _addTrack: function( type, prop, initialValue, interpolation ) {
+
+ var track = new type(
+ prop, [ 0 ], initialValue, interpolation );
+
+ // data must be in JS arrays so it can be resized
+ track.times = Array.prototype.slice.call( track.times );
+ track.values = Array.prototype.slice.call( track.values );
+
+ this._channelNames.push( prop );
+ this._tracks[ prop ] = track;
+
+ // for recording the state:
+ this._propRefs[ prop ] =
+ new THREE.PropertyBinding( this._scene, prop );
+
+ return track;
+
+ }
+
+};
diff --git a/app/static/js/Toolbar.js b/app/static/js/Toolbar.js
new file mode 100644
index 0000000..9ec747f
--- /dev/null
+++ b/app/static/js/Toolbar.js
@@ -0,0 +1,86 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Toolbar = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setId( 'toolbar' );
+
+ var buttons = new UI.Panel();
+ container.add( buttons );
+
+ // translate / rotate / scale
+
+ var translate = new UI.Button( 'translate' );
+ translate.dom.title = 'W';
+ translate.dom.className = 'Button selected';
+ translate.onClick( function () {
+
+ signals.transformModeChanged.dispatch( 'translate' );
+
+ } );
+ buttons.add( translate );
+
+ var rotate = new UI.Button( 'rotate' );
+ rotate.dom.title = 'E';
+ rotate.onClick( function () {
+
+ signals.transformModeChanged.dispatch( 'rotate' );
+
+ } );
+ buttons.add( rotate );
+
+ var scale = new UI.Button( 'scale' );
+ scale.dom.title = 'R';
+ scale.onClick( function () {
+
+ signals.transformModeChanged.dispatch( 'scale' );
+
+ } );
+ buttons.add( scale );
+
+ signals.transformModeChanged.add( function ( mode ) {
+
+ translate.dom.classList.remove( 'selected' );
+ rotate.dom.classList.remove( 'selected' );
+ scale.dom.classList.remove( 'selected' );
+
+ switch ( mode ) {
+
+ case 'translate': translate.dom.classList.add( 'selected' ); break;
+ case 'rotate': rotate.dom.classList.add( 'selected' ); break;
+ case 'scale': scale.dom.classList.add( 'selected' ); break;
+
+ }
+
+ } );
+
+ // grid
+
+ var grid = new UI.Number( 25 ).setWidth( '40px' ).onChange( update );
+ buttons.add( new UI.Text( 'grid: ' ) );
+ buttons.add( grid );
+
+ var snap = new UI.THREE.Boolean( false, 'snap' ).onChange( update );
+ buttons.add( snap );
+
+ var local = new UI.THREE.Boolean( false, 'local' ).onChange( update );
+ buttons.add( local );
+
+ var showGrid = new UI.THREE.Boolean( true, 'show' ).onChange( update );
+ buttons.add( showGrid );
+
+ function update() {
+
+ signals.snapChanged.dispatch( snap.getValue() === true ? grid.getValue() : null );
+ signals.spaceChanged.dispatch( local.getValue() === true ? "local" : "world" );
+ signals.showGridChanged.dispatch( showGrid.getValue() );
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/TypedArrayUtils.js b/app/static/js/TypedArrayUtils.js
new file mode 100644
index 0000000..22024ce
--- /dev/null
+++ b/app/static/js/TypedArrayUtils.js
@@ -0,0 +1,602 @@
+
+THREE.TypedArrayUtils = {};
+
+/**
+ * In-place quicksort for typed arrays (e.g. for Float32Array)
+ * provides fast sorting
+ * useful e.g. for a custom shader and/or BufferGeometry
+ *
+ * @author Roman Bolzern , 2013
+ * @author I4DS http://www.fhnw.ch/i4ds, 2013
+ * @license MIT License
+ *
+ * Complexity: http://bigocheatsheet.com/ see Quicksort
+ *
+ * Example:
+ * points: [x, y, z, x, y, z, x, y, z, ...]
+ * eleSize: 3 //because of (x, y, z)
+ * orderElement: 0 //order according to x
+ */
+
+THREE.TypedArrayUtils.quicksortIP = function ( arr, eleSize, orderElement ) {
+
+ var stack = [];
+ var sp = - 1;
+ var left = 0;
+ var right = arr.length / eleSize - 1;
+ var tmp = 0.0, x = 0, y = 0;
+
+ var swapF = function ( a, b ) {
+
+ a *= eleSize; b *= eleSize;
+
+ for ( y = 0; y < eleSize; y ++ ) {
+
+ tmp = arr[ a + y ];
+ arr[ a + y ] = arr[ b + y ];
+ arr[ b + y ] = tmp;
+
+ }
+
+ };
+
+ var i, j, swap = new Float32Array( eleSize ), temp = new Float32Array( eleSize );
+
+ while ( true ) {
+
+ if ( right - left <= 25 ) {
+
+ for ( j = left + 1; j <= right; j ++ ) {
+
+ for ( x = 0; x < eleSize; x ++ ) {
+
+ swap[ x ] = arr[ j * eleSize + x ];
+
+ }
+
+ i = j - 1;
+
+ while ( i >= left && arr[ i * eleSize + orderElement ] > swap[ orderElement ] ) {
+
+ for ( x = 0; x < eleSize; x ++ ) {
+
+ arr[ ( i + 1 ) * eleSize + x ] = arr[ i * eleSize + x ];
+
+ }
+
+ i --;
+
+ }
+
+ for ( x = 0; x < eleSize; x ++ ) {
+
+ arr[ ( i + 1 ) * eleSize + x ] = swap[ x ];
+
+ }
+
+ }
+
+ if ( sp == - 1 ) break;
+
+ right = stack[ sp -- ]; //?
+ left = stack[ sp -- ];
+
+ } else {
+
+ var median = ( left + right ) >> 1;
+
+ i = left + 1;
+ j = right;
+
+ swapF( median, i );
+
+ if ( arr[ left * eleSize + orderElement ] > arr[ right * eleSize + orderElement ] ) {
+
+ swapF( left, right );
+
+ }
+
+ if ( arr[ i * eleSize + orderElement ] > arr[ right * eleSize + orderElement ] ) {
+
+ swapF( i, right );
+
+ }
+
+ if ( arr[ left * eleSize + orderElement ] > arr[ i * eleSize + orderElement ] ) {
+
+ swapF( left, i );
+
+ }
+
+ for ( x = 0; x < eleSize; x ++ ) {
+
+ temp[ x ] = arr[ i * eleSize + x ];
+
+ }
+
+ while ( true ) {
+
+ do i ++; while ( arr[ i * eleSize + orderElement ] < temp[ orderElement ] );
+ do j --; while ( arr[ j * eleSize + orderElement ] > temp[ orderElement ] );
+
+ if ( j < i ) break;
+
+ swapF( i, j );
+
+ }
+
+ for ( x = 0; x < eleSize; x ++ ) {
+
+ arr[ ( left + 1 ) * eleSize + x ] = arr[ j * eleSize + x ];
+ arr[ j * eleSize + x ] = temp[ x ];
+
+ }
+
+ if ( right - i + 1 >= j - left ) {
+
+ stack[ ++ sp ] = i;
+ stack[ ++ sp ] = right;
+ right = j - 1;
+
+ } else {
+
+ stack[ ++ sp ] = left;
+ stack[ ++ sp ] = j - 1;
+ left = i;
+
+ }
+
+ }
+
+ }
+
+ return arr;
+
+};
+
+
+
+/**
+ * k-d Tree for typed arrays (e.g. for Float32Array), in-place
+ * provides fast nearest neighbour search
+ * useful e.g. for a custom shader and/or BufferGeometry, saves tons of memory
+ * has no insert and remove, only buildup and neares neighbour search
+ *
+ * Based on https://github.com/ubilabs/kd-tree-javascript by Ubilabs
+ *
+ * @author Roman Bolzern , 2013
+ * @author I4DS http://www.fhnw.ch/i4ds, 2013
+ * @license MIT License
+ *
+ * Requires typed array quicksort
+ *
+ * Example:
+ * points: [x, y, z, x, y, z, x, y, z, ...]
+ * metric: function(a, b){ return Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2); } //Manhatten distance
+ * eleSize: 3 //because of (x, y, z)
+ *
+ * Further information (including mathematical properties)
+ * http://en.wikipedia.org/wiki/Binary_tree
+ * http://en.wikipedia.org/wiki/K-d_tree
+ *
+ * If you want to further minimize memory usage, remove Node.depth and replace in search algorithm with a traversal to root node (see comments at THREE.TypedArrayUtils.Kdtree.prototype.Node)
+ */
+
+ THREE.TypedArrayUtils.Kdtree = function ( points, metric, eleSize ) {
+
+ var self = this;
+
+ var maxDepth = 0;
+
+ var getPointSet = function ( points, pos ) {
+
+ return points.subarray( pos * eleSize, pos * eleSize + eleSize );
+
+ };
+
+ function buildTree( points, depth, parent, pos ) {
+
+ var dim = depth % eleSize,
+ median,
+ node,
+ plength = points.length / eleSize;
+
+ if ( depth > maxDepth ) maxDepth = depth;
+
+ if ( plength === 0 ) return null;
+ if ( plength === 1 ) {
+
+ return new self.Node( getPointSet( points, 0 ), depth, parent, pos );
+
+ }
+
+ THREE.TypedArrayUtils.quicksortIP( points, eleSize, dim );
+
+ median = Math.floor( plength / 2 );
+
+ node = new self.Node( getPointSet( points, median ), depth, parent, median + pos );
+ node.left = buildTree( points.subarray( 0, median * eleSize ), depth + 1, node, pos );
+ node.right = buildTree( points.subarray( ( median + 1 ) * eleSize, points.length ), depth + 1, node, pos + median + 1 );
+
+ return node;
+
+ }
+
+ this.root = buildTree( points, 0, null, 0 );
+
+ this.getMaxDepth = function () {
+
+ return maxDepth;
+
+ };
+
+ this.nearest = function ( point, maxNodes, maxDistance ) {
+
+ /* point: array of size eleSize
+ maxNodes: max amount of nodes to return
+ maxDistance: maximum distance to point result nodes should have
+ condition (not implemented): function to test node before it's added to the result list, e.g. test for view frustum
+ */
+
+ var i,
+ result,
+ bestNodes;
+
+ bestNodes = new THREE.TypedArrayUtils.Kdtree.BinaryHeap(
+
+ function ( e ) {
+
+ return - e[ 1 ];
+
+ }
+
+ );
+
+ function nearestSearch( node ) {
+
+ var bestChild,
+ dimension = node.depth % eleSize,
+ ownDistance = metric( point, node.obj ),
+ linearDistance = 0,
+ otherChild,
+ i,
+ linearPoint = [];
+
+ function saveNode( node, distance ) {
+
+ bestNodes.push( [ node, distance ] );
+
+ if ( bestNodes.size() > maxNodes ) {
+
+ bestNodes.pop();
+
+ }
+
+ }
+
+ for ( i = 0; i < eleSize; i += 1 ) {
+
+ if ( i === node.depth % eleSize ) {
+
+ linearPoint[ i ] = point[ i ];
+
+ } else {
+
+ linearPoint[ i ] = node.obj[ i ];
+
+ }
+
+ }
+
+ linearDistance = metric( linearPoint, node.obj );
+
+ // if it's a leaf
+
+ if ( node.right === null && node.left === null ) {
+
+ if ( bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[ 1 ] ) {
+
+ saveNode( node, ownDistance );
+
+ }
+
+ return;
+
+ }
+
+ if ( node.right === null ) {
+
+ bestChild = node.left;
+
+ } else if ( node.left === null ) {
+
+ bestChild = node.right;
+
+ } else {
+
+ if ( point[ dimension ] < node.obj[ dimension ] ) {
+
+ bestChild = node.left;
+
+ } else {
+
+ bestChild = node.right;
+
+ }
+
+ }
+
+ // recursive search
+
+ nearestSearch( bestChild );
+
+ if ( bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[ 1 ] ) {
+
+ saveNode( node, ownDistance );
+
+ }
+
+ // if there's still room or the current distance is nearer than the best distance
+
+ if ( bestNodes.size() < maxNodes || Math.abs( linearDistance ) < bestNodes.peek()[ 1 ] ) {
+
+ if ( bestChild === node.left ) {
+
+ otherChild = node.right;
+
+ } else {
+
+ otherChild = node.left;
+
+ }
+
+ if ( otherChild !== null ) {
+
+ nearestSearch( otherChild );
+
+ }
+
+ }
+
+ }
+
+ if ( maxDistance ) {
+
+ for ( i = 0; i < maxNodes; i += 1 ) {
+
+ bestNodes.push( [ null, maxDistance ] );
+
+ }
+
+ }
+
+ nearestSearch( self.root );
+
+ result = [];
+
+ for ( i = 0; i < maxNodes; i += 1 ) {
+
+ if ( bestNodes.content[ i ][ 0 ] ) {
+
+ result.push( [ bestNodes.content[ i ][ 0 ], bestNodes.content[ i ][ 1 ] ] );
+
+ }
+
+ }
+
+ return result;
+
+ };
+
+};
+
+/**
+ * If you need to free up additional memory and agree with an additional O( log n ) traversal time you can get rid of "depth" and "pos" in Node:
+ * Depth can be easily done by adding 1 for every parent (care: root node has depth 0, not 1)
+ * Pos is a bit tricky: Assuming the tree is balanced (which is the case when after we built it up), perform the following steps:
+ * By traversing to the root store the path e.g. in a bit pattern (01001011, 0 is left, 1 is right)
+ * From buildTree we know that "median = Math.floor( plength / 2 );", therefore for each bit...
+ * 0: amountOfNodesRelevantForUs = Math.floor( (pamountOfNodesRelevantForUs - 1) / 2 );
+ * 1: amountOfNodesRelevantForUs = Math.ceil( (pamountOfNodesRelevantForUs - 1) / 2 );
+ * pos += Math.floor( (pamountOfNodesRelevantForUs - 1) / 2 );
+ * when recursion done, we still need to add all left children of target node:
+ * pos += Math.floor( (pamountOfNodesRelevantForUs - 1) / 2 );
+ * and I think you need to +1 for the current position, not sure.. depends, try it out ^^
+ *
+ * I experienced that for 200'000 nodes you can get rid of 4 MB memory each, leading to 8 MB memory saved.
+ */
+THREE.TypedArrayUtils.Kdtree.prototype.Node = function ( obj, depth, parent, pos ) {
+
+ this.obj = obj;
+ this.left = null;
+ this.right = null;
+ this.parent = parent;
+ this.depth = depth;
+ this.pos = pos;
+
+};
+
+/**
+ * Binary heap implementation
+ * @author http://eloquentjavascript.net/appendix2.htm
+ */
+
+THREE.TypedArrayUtils.Kdtree.BinaryHeap = function ( scoreFunction ) {
+
+ this.content = [];
+ this.scoreFunction = scoreFunction;
+
+};
+
+THREE.TypedArrayUtils.Kdtree.BinaryHeap.prototype = {
+
+ push: function ( element ) {
+
+ // Add the new element to the end of the array.
+ this.content.push( element );
+
+ // Allow it to bubble up.
+ this.bubbleUp( this.content.length - 1 );
+
+ },
+
+ pop: function () {
+
+ // Store the first element so we can return it later.
+ var result = this.content[ 0 ];
+
+ // Get the element at the end of the array.
+ var end = this.content.pop();
+
+ // If there are any elements left, put the end element at the
+ // start, and let it sink down.
+ if ( this.content.length > 0 ) {
+
+ this.content[ 0 ] = end;
+ this.sinkDown( 0 );
+
+ }
+
+ return result;
+
+ },
+
+ peek: function () {
+
+ return this.content[ 0 ];
+
+ },
+
+ remove: function ( node ) {
+
+ var len = this.content.length;
+
+ // To remove a value, we must search through the array to find it.
+ for ( var i = 0; i < len; i ++ ) {
+
+ if ( this.content[ i ] == node ) {
+
+ // When it is found, the process seen in 'pop' is repeated
+ // to fill up the hole.
+ var end = this.content.pop();
+
+ if ( i != len - 1 ) {
+
+ this.content[ i ] = end;
+
+ if ( this.scoreFunction( end ) < this.scoreFunction( node ) ) {
+
+ this.bubbleUp( i );
+
+ } else {
+
+ this.sinkDown( i );
+
+ }
+
+ }
+
+ return;
+
+ }
+
+ }
+
+ throw new Error( "Node not found." );
+
+ },
+
+ size: function () {
+
+ return this.content.length;
+
+ },
+
+ bubbleUp: function ( n ) {
+
+ // Fetch the element that has to be moved.
+ var element = this.content[ n ];
+
+ // When at 0, an element can not go up any further.
+ while ( n > 0 ) {
+
+ // Compute the parent element's index, and fetch it.
+ var parentN = Math.floor( ( n + 1 ) / 2 ) - 1,
+ parent = this.content[ parentN ];
+
+ // Swap the elements if the parent is greater.
+ if ( this.scoreFunction( element ) < this.scoreFunction( parent ) ) {
+
+ this.content[ parentN ] = element;
+ this.content[ n ] = parent;
+
+ // Update 'n' to continue at the new position.
+ n = parentN;
+
+ } else {
+
+ // Found a parent that is less, no need to move it further.
+ break;
+
+ }
+
+ }
+
+ },
+
+ sinkDown: function ( n ) {
+
+ // Look up the target element and its score.
+ var length = this.content.length,
+ element = this.content[ n ],
+ elemScore = this.scoreFunction( element );
+
+ while ( true ) {
+
+ // Compute the indices of the child elements.
+ var child2N = ( n + 1 ) * 2, child1N = child2N - 1;
+
+ // This is used to store the new position of the element, if any.
+ var swap = null;
+
+ // If the first child exists (is inside the array)...
+ if ( child1N < length ) {
+
+ // Look it up and compute its score.
+ var child1 = this.content[ child1N ],
+ child1Score = this.scoreFunction( child1 );
+
+ // If the score is less than our element's, we need to swap.
+ if ( child1Score < elemScore ) swap = child1N;
+
+ }
+
+ // Do the same checks for the other child.
+ if ( child2N < length ) {
+
+ var child2 = this.content[ child2N ],
+ child2Score = this.scoreFunction( child2 );
+
+ if ( child2Score < ( swap === null ? elemScore : child1Score ) ) swap = child2N;
+
+ }
+
+ // If the element needs to be moved, swap it, and continue.
+ if ( swap !== null ) {
+
+ this.content[ n ] = this.content[ swap ];
+ this.content[ swap ] = element;
+ n = swap;
+
+ } else {
+
+ // Otherwise, we are done.
+ break;
+
+ }
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/UCSCharacter.js b/app/static/js/UCSCharacter.js
new file mode 100644
index 0000000..37b71bf
--- /dev/null
+++ b/app/static/js/UCSCharacter.js
@@ -0,0 +1,141 @@
+THREE.UCSCharacter = function() {
+
+ var scope = this;
+
+ var mesh;
+
+ this.scale = 1;
+
+ this.root = new THREE.Object3D();
+
+ this.numSkins = undefined;
+ this.numMorphs = undefined;
+
+ this.skins = [];
+ this.materials = [];
+ this.morphs = [];
+
+ this.mixer = new THREE.AnimationMixer( this.root );
+
+ this.onLoadComplete = function () {};
+
+ this.loadCounter = 0;
+
+ this.loadParts = function ( config ) {
+
+ this.numSkins = config.skins.length;
+ this.numMorphs = config.morphs.length;
+
+ // Character geometry + number of skins
+ this.loadCounter = 1 + config.skins.length;
+
+ // SKINS
+ this.skins = loadTextures( config.baseUrl + "skins/", config.skins );
+ this.materials = createMaterials( this.skins );
+
+ // MORPHS
+ this.morphs = config.morphs;
+
+ // CHARACTER
+ var loader = new THREE.JSONLoader();
+ console.log( config.baseUrl + config.character );
+ loader.load( config.baseUrl + config.character, function( geometry ) {
+
+ geometry.computeBoundingBox();
+ geometry.computeVertexNormals();
+
+ mesh = new THREE.SkinnedMesh( geometry, [] );
+ mesh.name = config.character;
+ scope.root.add( mesh );
+
+ var bb = geometry.boundingBox;
+ scope.root.scale.set( config.s, config.s, config.s );
+ scope.root.position.set( config.x, config.y - bb.min.y * config.s, config.z );
+
+ mesh.castShadow = true;
+ mesh.receiveShadow = true;
+
+ scope.mixer.clipAction( geometry.animations[0], mesh ).play();
+
+ scope.setSkin( 0 );
+
+ scope.checkLoadComplete();
+
+ } );
+
+ };
+
+ this.setSkin = function( index ) {
+
+ if ( mesh && scope.materials ) {
+
+ mesh.material = scope.materials[ index ];
+
+ }
+
+ };
+
+ this.updateMorphs = function( influences ) {
+
+ if ( mesh ) {
+
+ for ( var i = 0; i < scope.numMorphs; i ++ ) {
+
+ mesh.morphTargetInfluences[ i ] = influences[ scope.morphs[ i ] ] / 100;
+
+ }
+
+ }
+
+ };
+
+ function loadTextures( baseUrl, textureUrls ) {
+
+ var textureLoader = new THREE.TextureLoader();
+ var textures = [];
+
+ for ( var i = 0; i < textureUrls.length; i ++ ) {
+
+ textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], scope.checkLoadComplete );
+ textures[ i ].mapping = THREE.UVMapping;
+ textures[ i ].name = textureUrls[ i ];
+
+ }
+
+ return textures;
+
+ }
+
+ function createMaterials( skins ) {
+
+ var materials = [];
+
+ for ( var i = 0; i < skins.length; i ++ ) {
+
+ materials[ i ] = new THREE.MeshLambertMaterial( {
+ color: 0xeeeeee,
+ specular: 10.0,
+ map: skins[ i ],
+ skinning: true,
+ morphTargets: true
+ } );
+
+ }
+
+ return materials;
+
+ }
+
+ this.checkLoadComplete = function () {
+
+ scope.loadCounter -= 1;
+
+ if ( scope.loadCounter === 0 ) {
+
+ scope.onLoadComplete();
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/Viewport.Info.js b/app/static/js/Viewport.Info.js
new file mode 100644
index 0000000..06cb004
--- /dev/null
+++ b/app/static/js/Viewport.Info.js
@@ -0,0 +1,84 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Viewport.Info = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setId( 'info' );
+ container.setPosition( 'absolute' );
+ container.setLeft( '10px' );
+ container.setBottom( '10px' );
+ container.setFontSize( '12px' );
+ container.setColor( '#fff' );
+
+ var objectsText = new UI.Text( '0' ).setMarginLeft( '6px' );
+ var verticesText = new UI.Text( '0' ).setMarginLeft( '6px' );
+ var trianglesText = new UI.Text( '0' ).setMarginLeft( '6px' );
+
+ container.add( new UI.Text( 'objects' ), objectsText, new UI.Break() );
+ container.add( new UI.Text( 'vertices' ), verticesText, new UI.Break() );
+ container.add( new UI.Text( 'triangles' ), trianglesText, new UI.Break() );
+
+ signals.objectAdded.add( update );
+ signals.objectRemoved.add( update );
+ signals.geometryChanged.add( update );
+
+ //
+
+ function update() {
+
+ var scene = editor.scene;
+
+ var objects = 0, vertices = 0, triangles = 0;
+
+ for ( var i = 0, l = scene.children.length; i < l; i ++ ) {
+
+ var object = scene.children[ i ];
+
+ object.traverseVisible( function ( object ) {
+
+ objects ++;
+
+ if ( object instanceof THREE.Mesh ) {
+
+ var geometry = object.geometry;
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ vertices += geometry.vertices.length;
+ triangles += geometry.faces.length;
+
+ } else if ( geometry instanceof THREE.BufferGeometry ) {
+
+ if ( geometry.index !== null ) {
+
+ vertices += geometry.index.count * 3;
+ triangles += geometry.index.count;
+
+ } else {
+
+ vertices += geometry.attributes.position.count;
+ triangles += geometry.attributes.position.count / 3;
+
+ }
+
+ }
+
+ }
+
+ } );
+
+ }
+
+ objectsText.setValue( objects.format() );
+ verticesText.setValue( vertices.format() );
+ trianglesText.setValue( triangles.format() );
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Viewport.js b/app/static/js/Viewport.js
new file mode 100644
index 0000000..868b224
--- /dev/null
+++ b/app/static/js/Viewport.js
@@ -0,0 +1,546 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Viewport = function ( editor ) {
+
+ var signals = editor.signals;
+
+ var container = new UI.Panel();
+ container.setId( 'viewport' );
+ container.setPosition( 'absolute' );
+
+ container.add( new Viewport.Info( editor ) );
+
+ //
+
+ var renderer = null;
+
+ var camera = editor.camera;
+ var scene = editor.scene;
+ var sceneHelpers = editor.sceneHelpers;
+
+ var objects = [];
+
+ // helpers
+
+ var grid = new THREE.GridHelper( 60, 60 );
+ sceneHelpers.add( grid );
+
+ //
+
+ var box = new THREE.Box3();
+
+ var selectionBox = new THREE.BoxHelper();
+ selectionBox.material.depthTest = false;
+ selectionBox.material.transparent = true;
+ selectionBox.visible = false;
+ sceneHelpers.add( selectionBox );
+
+ var objectPositionOnDown = null;
+ var objectRotationOnDown = null;
+ var objectScaleOnDown = null;
+
+ var transformControls = new THREE.TransformControls( camera, container.dom );
+ transformControls.addEventListener( 'change', function () {
+
+ var object = transformControls.object;
+
+ if ( object !== undefined ) {
+
+ selectionBox.setFromObject( object );
+
+ if ( editor.helpers[ object.id ] !== undefined ) {
+
+ editor.helpers[ object.id ].update();
+
+ }
+
+ signals.refreshSidebarObject3D.dispatch( object );
+
+ }
+
+ render();
+
+ } );
+ transformControls.addEventListener( 'mouseDown', function () {
+
+ var object = transformControls.object;
+
+ objectPositionOnDown = object.position.clone();
+ objectRotationOnDown = object.rotation.clone();
+ objectScaleOnDown = object.scale.clone();
+
+ controls.enabled = false;
+
+ } );
+ transformControls.addEventListener( 'mouseUp', function () {
+
+ var object = transformControls.object;
+
+ if ( object !== undefined ) {
+
+ switch ( transformControls.getMode() ) {
+
+ case 'translate':
+
+ if ( ! objectPositionOnDown.equals( object.position ) ) {
+
+ editor.execute( new SetPositionCommand( object, object.position, objectPositionOnDown ) );
+
+ }
+
+ break;
+
+ case 'rotate':
+
+ if ( ! objectRotationOnDown.equals( object.rotation ) ) {
+
+ editor.execute( new SetRotationCommand( object, object.rotation, objectRotationOnDown ) );
+
+ }
+
+ break;
+
+ case 'scale':
+
+ if ( ! objectScaleOnDown.equals( object.scale ) ) {
+
+ editor.execute( new SetScaleCommand( object, object.scale, objectScaleOnDown ) );
+
+ }
+
+ break;
+
+ }
+
+ }
+
+ controls.enabled = true;
+
+ } );
+
+ sceneHelpers.add( transformControls );
+
+ // object picking
+
+ var raycaster = new THREE.Raycaster();
+ var mouse = new THREE.Vector2();
+
+ // events
+
+ function getIntersects( point, objects ) {
+
+ mouse.set( ( point.x * 2 ) - 1, - ( point.y * 2 ) + 1 );
+
+ raycaster.setFromCamera( mouse, camera );
+
+ return raycaster.intersectObjects( objects );
+
+ }
+
+ var onDownPosition = new THREE.Vector2();
+ var onUpPosition = new THREE.Vector2();
+ var onDoubleClickPosition = new THREE.Vector2();
+
+ function getMousePosition( dom, x, y ) {
+
+ var rect = dom.getBoundingClientRect();
+ return [ ( x - rect.left ) / rect.width, ( y - rect.top ) / rect.height ];
+
+ }
+
+ function handleClick() {
+
+ if ( onDownPosition.distanceTo( onUpPosition ) === 0 ) {
+
+ var intersects = getIntersects( onUpPosition, objects );
+
+ if ( intersects.length > 0 ) {
+
+ var object = intersects[ 0 ].object;
+
+ if ( object.userData.object !== undefined ) {
+
+ // helper
+
+ editor.select( object.userData.object );
+
+ } else {
+
+ editor.select( object );
+
+ }
+
+ } else {
+
+ editor.select( null );
+
+ }
+
+ render();
+
+ }
+
+ }
+
+ function onMouseDown( event ) {
+
+ event.preventDefault();
+
+ var array = getMousePosition( container.dom, event.clientX, event.clientY );
+ onDownPosition.fromArray( array );
+
+ document.addEventListener( 'mouseup', onMouseUp, false );
+
+ }
+
+ function onMouseUp( event ) {
+
+ var array = getMousePosition( container.dom, event.clientX, event.clientY );
+ onUpPosition.fromArray( array );
+
+ handleClick();
+
+ document.removeEventListener( 'mouseup', onMouseUp, false );
+
+ }
+
+ function onTouchStart( event ) {
+
+ var touch = event.changedTouches[ 0 ];
+
+ var array = getMousePosition( container.dom, touch.clientX, touch.clientY );
+ onDownPosition.fromArray( array );
+
+ document.addEventListener( 'touchend', onTouchEnd, false );
+
+ }
+
+ function onTouchEnd( event ) {
+
+ var touch = event.changedTouches[ 0 ];
+
+ var array = getMousePosition( container.dom, touch.clientX, touch.clientY );
+ onUpPosition.fromArray( array );
+
+ handleClick();
+
+ document.removeEventListener( 'touchend', onTouchEnd, false );
+
+ }
+
+ function onDoubleClick( event ) {
+
+ var array = getMousePosition( container.dom, event.clientX, event.clientY );
+ onDoubleClickPosition.fromArray( array );
+
+ var intersects = getIntersects( onDoubleClickPosition, objects );
+
+ if ( intersects.length > 0 ) {
+
+ var intersect = intersects[ 0 ];
+
+ signals.objectFocused.dispatch( intersect.object );
+
+ }
+
+ }
+
+ container.dom.addEventListener( 'mousedown', onMouseDown, false );
+ container.dom.addEventListener( 'touchstart', onTouchStart, false );
+ container.dom.addEventListener( 'dblclick', onDoubleClick, false );
+
+ // controls need to be added *after* main logic,
+ // otherwise controls.enabled doesn't work.
+
+ var controls = new THREE.EditorControls( camera, container.dom );
+ controls.addEventListener( 'change', function () {
+
+ transformControls.update();
+ signals.cameraChanged.dispatch( camera );
+
+ } );
+
+ // signals
+
+ signals.editorCleared.add( function () {
+
+ controls.center.set( 0, 0, 0 );
+ render();
+
+ } );
+
+ signals.themeChanged.add( function ( value ) {
+
+ switch ( value ) {
+
+ case '/static/css/light.css':
+ sceneHelpers.remove( grid );
+ grid = new THREE.GridHelper( 60, 60, 0x444444, 0x888888 );
+ sceneHelpers.add( grid );
+ break;
+ case '/static/css/dark.css':
+ sceneHelpers.remove( grid );
+ grid = new THREE.GridHelper( 60, 60, 0xbbbbbb, 0x888888 );
+ sceneHelpers.add( grid );
+ break;
+
+ }
+
+ render();
+
+ } );
+
+ signals.transformModeChanged.add( function ( mode ) {
+
+ transformControls.setMode( mode );
+
+ } );
+
+ signals.snapChanged.add( function ( dist ) {
+
+ transformControls.setTranslationSnap( dist );
+
+ } );
+
+ signals.spaceChanged.add( function ( space ) {
+
+ transformControls.setSpace( space );
+
+ } );
+
+ signals.rendererChanged.add( function ( newRenderer ) {
+
+ if ( renderer !== null ) {
+
+ container.dom.removeChild( renderer.domElement );
+
+ }
+
+ renderer = newRenderer;
+
+ renderer.autoClear = false;
+ renderer.autoUpdateScene = false;
+ renderer.setPixelRatio( window.devicePixelRatio );
+ renderer.setSize( container.dom.offsetWidth, container.dom.offsetHeight );
+
+ container.dom.appendChild( renderer.domElement );
+
+ render();
+
+ } );
+
+ signals.sceneGraphChanged.add( function () {
+
+ render();
+
+ } );
+
+ signals.cameraChanged.add( function () {
+
+ render();
+
+ } );
+
+ signals.objectSelected.add( function ( object ) {
+
+ selectionBox.visible = false;
+ transformControls.detach();
+
+ if ( object !== null && object !== scene && object !== camera ) {
+
+ box.setFromObject( object );
+
+ if ( box.isEmpty() === false ) {
+
+ selectionBox.setFromObject( object );
+ selectionBox.visible = true;
+
+ }
+
+ transformControls.attach( object );
+
+ }
+
+ render();
+
+ } );
+
+ signals.objectFocused.add( function ( object ) {
+
+ controls.focus( object );
+
+ } );
+
+ signals.geometryChanged.add( function ( object ) {
+
+ if ( object !== undefined ) {
+
+ selectionBox.setFromObject( object );
+
+ }
+
+ render();
+
+ } );
+
+ signals.objectAdded.add( function ( object ) {
+
+ object.traverse( function ( child ) {
+
+ objects.push( child );
+
+ } );
+
+ } );
+
+ signals.objectChanged.add( function ( object ) {
+
+ if ( editor.selected === object ) {
+
+ selectionBox.setFromObject( object );
+ transformControls.update();
+
+ }
+
+ if ( object instanceof THREE.PerspectiveCamera ) {
+
+ object.updateProjectionMatrix();
+
+ }
+
+ if ( editor.helpers[ object.id ] !== undefined ) {
+
+ editor.helpers[ object.id ].update();
+
+ }
+
+ render();
+
+ } );
+
+ signals.objectRemoved.add( function ( object ) {
+
+ object.traverse( function ( child ) {
+
+ objects.splice( objects.indexOf( child ), 1 );
+
+ } );
+
+ } );
+
+ signals.helperAdded.add( function ( object ) {
+
+ objects.push( object.getObjectByName( 'picker' ) );
+
+ } );
+
+ signals.helperRemoved.add( function ( object ) {
+
+ objects.splice( objects.indexOf( object.getObjectByName( 'picker' ) ), 1 );
+
+ } );
+
+ signals.materialChanged.add( function ( material ) {
+
+ render();
+
+ } );
+
+ // fog
+
+ signals.sceneBackgroundChanged.add( function ( backgroundColor ) {
+
+ scene.background.setHex( backgroundColor );
+
+ render();
+
+ } );
+
+ var currentFogType = null;
+
+ signals.sceneFogChanged.add( function ( fogType, fogColor, fogNear, fogFar, fogDensity ) {
+
+ if ( currentFogType !== fogType ) {
+
+ switch ( fogType ) {
+
+ case 'None':
+ scene.fog = null;
+ break;
+ case 'Fog':
+ scene.fog = new THREE.Fog();
+ break;
+ case 'FogExp2':
+ scene.fog = new THREE.FogExp2();
+ break;
+
+ }
+
+ currentFogType = fogType;
+
+ }
+
+ if ( scene.fog instanceof THREE.Fog ) {
+
+ scene.fog.color.setHex( fogColor );
+ scene.fog.near = fogNear;
+ scene.fog.far = fogFar;
+
+ } else if ( scene.fog instanceof THREE.FogExp2 ) {
+
+ scene.fog.color.setHex( fogColor );
+ scene.fog.density = fogDensity;
+
+ }
+
+ render();
+
+ } );
+
+ //
+
+ signals.windowResize.add( function () {
+
+ // TODO: Move this out?
+
+ editor.DEFAULT_CAMERA.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
+ editor.DEFAULT_CAMERA.updateProjectionMatrix();
+
+ camera.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
+ camera.updateProjectionMatrix();
+
+ renderer.setSize( container.dom.offsetWidth, container.dom.offsetHeight );
+
+ render();
+
+ } );
+
+ signals.showGridChanged.add( function ( showGrid ) {
+
+ grid.visible = showGrid;
+ render();
+
+ } );
+
+ //
+
+ function render() {
+
+ sceneHelpers.updateMatrixWorld();
+ scene.updateMatrixWorld();
+
+ renderer.render( scene, camera );
+
+ if ( renderer instanceof THREE.RaytracingRenderer === false ) {
+
+ renderer.render( sceneHelpers, camera );
+
+ }
+
+ }
+
+ return container;
+
+};
diff --git a/app/static/js/Volume.js b/app/static/js/Volume.js
new file mode 100644
index 0000000..4b0d175
--- /dev/null
+++ b/app/static/js/Volume.js
@@ -0,0 +1,447 @@
+/**
+ * This class had been written to handle the output of the NRRD loader.
+ * It contains a volume of data and informations about it.
+ * For now it only handles 3 dimensional data.
+ * See the webgl_loader_nrrd.html example and the loaderNRRD.js file to see how to use this class.
+ * @class
+ * @author Valentin Demeusy / https://github.com/stity
+ * @param {number} xLength Width of the volume
+ * @param {number} yLength Length of the volume
+ * @param {number} zLength Depth of the volume
+ * @param {string} type The type of data (uint8, uint16, ...)
+ * @param {ArrayBuffer} arrayBuffer The buffer with volume data
+ */
+THREE.Volume = function( xLength, yLength, zLength, type, arrayBuffer ) {
+
+ if ( arguments.length > 0 ) {
+
+ /**
+ * @member {number} xLength Width of the volume in the IJK coordinate system
+ */
+ this.xLength = Number( xLength ) || 1;
+ /**
+ * @member {number} yLength Height of the volume in the IJK coordinate system
+ */
+ this.yLength = Number( yLength ) || 1;
+ /**
+ * @member {number} zLength Depth of the volume in the IJK coordinate system
+ */
+ this.zLength = Number( zLength ) || 1;
+
+ /**
+ * @member {TypedArray} data Data of the volume
+ */
+
+ switch ( type ) {
+
+ case 'Uint8' :
+ case 'uint8' :
+ case 'uchar' :
+ case 'unsigned char' :
+ case 'uint8_t' :
+ this.data = new Uint8Array( arrayBuffer );
+ break;
+ case 'Int8' :
+ case 'int8' :
+ case 'signed char' :
+ case 'int8_t' :
+ this.data = new Int8Array( arrayBuffer );
+ break;
+ case 'Int16' :
+ case 'int16' :
+ case 'short' :
+ case 'short int' :
+ case 'signed short' :
+ case 'signed short int' :
+ case 'int16_t' :
+ this.data = new Int16Array( arrayBuffer );
+ break;
+ case 'Uint16' :
+ case 'uint16' :
+ case 'ushort' :
+ case 'unsigned short' :
+ case 'unsigned short int' :
+ case 'uint16_t' :
+ this.data = new Uint16Array( arrayBuffer );
+ break;
+ case 'Int32' :
+ case 'int32' :
+ case 'int' :
+ case 'signed int' :
+ case 'int32_t' :
+ this.data = new Int32Array( arrayBuffer );
+ break;
+ case 'Uint32' :
+ case 'uint32' :
+ case 'uint' :
+ case 'unsigned int' :
+ case 'uint32_t' :
+ this.data = new Uint32Array( arrayBuffer );
+ break;
+ case 'longlong' :
+ case 'long long' :
+ case 'long long int' :
+ case 'signed long long' :
+ case 'signed long long int' :
+ case 'int64' :
+ case 'int64_t' :
+ case 'ulonglong' :
+ case 'unsigned long long' :
+ case 'unsigned long long int' :
+ case 'uint64' :
+ case 'uint64_t' :
+ throw 'Error in THREE.Volume constructor : this type is not supported in JavaScript';
+ break;
+ case 'Float32' :
+ case 'float32' :
+ case 'float' :
+ this.data = new Float32Array( arrayBuffer );
+ break;
+ case 'Float64' :
+ case 'float64' :
+ case 'double' :
+ this.data = new Float64Array( arrayBuffer );
+ break;
+ default :
+ this.data = new Uint8Array( arrayBuffer );
+
+ }
+
+ if ( this.data.length !== this.xLength * this.yLength * this.zLength ) {
+
+ throw 'Error in THREE.Volume constructor, lengths are not matching arrayBuffer size';
+
+ }
+
+ }
+
+ /**
+ * @member {Array} spacing Spacing to apply to the volume from IJK to RAS coordinate system
+ */
+ this.spacing = [ 1, 1, 1 ];
+ /**
+ * @member {Array} offset Offset of the volume in the RAS coordinate system
+ */
+ this.offset = [ 0, 0, 0 ];
+ /**
+ * @member {THREE.Martrix3} matrix The IJK to RAS matrix
+ */
+ this.matrix = new THREE.Matrix3();
+ this.matrix.identity();
+ /**
+ * @member {THREE.Martrix3} inverseMatrix The RAS to IJK matrix
+ */
+ /**
+ * @member {number} lowerThreshold The voxels with values under this threshold won't appear in the slices.
+ * If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
+ */
+ var lowerThreshold = - Infinity;
+ Object.defineProperty( this, 'lowerThreshold', {
+ get : function() {
+
+ return lowerThreshold;
+
+ },
+ set : function( value ) {
+
+ lowerThreshold = value;
+ this.sliceList.forEach( function( slice ) {
+
+ slice.geometryNeedsUpdate = true
+
+ } );
+
+ }
+ } );
+ /**
+ * @member {number} upperThreshold The voxels with values over this threshold won't appear in the slices.
+ * If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
+ */
+ var upperThreshold = Infinity;
+ Object.defineProperty( this, 'upperThreshold', {
+ get : function() {
+
+ return upperThreshold;
+
+ },
+ set : function( value ) {
+
+ upperThreshold = value;
+ this.sliceList.forEach( function( slice ) {
+
+ slice.geometryNeedsUpdate = true;
+
+ } );
+
+ }
+ } );
+
+
+ /**
+ * @member {Array} sliceList The list of all the slices associated to this volume
+ */
+ this.sliceList = [];
+
+
+ /**
+ * @member {Array} RASDimensions This array holds the dimensions of the volume in the RAS space
+ */
+
+};
+
+THREE.Volume.prototype = {
+
+ constructor : THREE.Volume,
+
+ /**
+ * @member {Function} getData Shortcut for data[access(i,j,k)]
+ * @memberof THREE.Volume
+ * @param {number} i First coordinate
+ * @param {number} j Second coordinate
+ * @param {number} k Third coordinate
+ * @returns {number} value in the data array
+ */
+ getData : function( i, j, k ) {
+
+ return this.data[ k * this.xLength * this.yLength + j * this.xLength + i ];
+
+ },
+
+ /**
+ * @member {Function} access compute the index in the data array corresponding to the given coordinates in IJK system
+ * @memberof THREE.Volume
+ * @param {number} i First coordinate
+ * @param {number} j Second coordinate
+ * @param {number} k Third coordinate
+ * @returns {number} index
+ */
+ access : function( i, j, k ) {
+
+ return k * this.xLength * this.yLength + j * this.xLength + i;
+
+ },
+
+ /**
+ * @member {Function} reverseAccess Retrieve the IJK coordinates of the voxel corresponding of the given index in the data
+ * @memberof THREE.Volume
+ * @param {number} index index of the voxel
+ * @returns {Array} [x,y,z]
+ */
+ reverseAccess : function( index ) {
+
+ var z = Math.floor( index / ( this.yLength * this.xLength ) );
+ var y = Math.floor( ( index - z * this.yLength * this.xLength ) / this.xLength );
+ var x = index - z * this.yLength * this.xLength - y * this.xLength;
+ return [ x, y, z ];
+
+ },
+
+ /**
+ * @member {Function} map Apply a function to all the voxels, be careful, the value will be replaced
+ * @memberof THREE.Volume
+ * @param {Function} functionToMap A function to apply to every voxel, will be called with the following parameters :
+ * value of the voxel
+ * index of the voxel
+ * the data (TypedArray)
+ * @param {Object} context You can specify a context in which call the function, default if this Volume
+ * @returns {THREE.Volume} this
+ */
+ map : function( functionToMap, context ) {
+
+ var length = this.data.length;
+ context = context || this;
+
+ for ( var i = 0; i < length; i ++ ) {
+
+ this.data[ i ] = functionToMap.call( context, this.data[ i ], i, this.data );
+
+ }
+
+ return this;
+
+ },
+
+ /**
+ * @member {Function} extractPerpendicularPlane Compute the orientation of the slice and returns all the information relative to the geometry such as sliceAccess, the plane matrix (orientation and position in RAS coordinate) and the dimensions of the plane in both coordinate system.
+ * @memberof THREE.Volume
+ * @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
+ * @param {number} index the index of the slice
+ * @returns {Object} an object containing all the usefull information on the geometry of the slice
+ */
+ extractPerpendicularPlane : function( axis, RASIndex ) {
+
+ var iLength,
+ jLength,
+ sliceAccess,
+ planeMatrix = ( new THREE.Matrix4() ).identity(),
+ volume = this,
+ planeWidth,
+ planeHeight,
+ firstSpacing,
+ secondSpacing,
+ positionOffset,
+ IJKIndex;
+
+ var axisInIJK = new THREE.Vector3(),
+ firstDirection = new THREE.Vector3(),
+ secondDirection = new THREE.Vector3();
+
+ var dimensions = new THREE.Vector3( this.xLength, this.yLength, this.zLength );
+
+
+ switch ( axis ) {
+
+ case 'x' :
+ axisInIJK.set( 1, 0, 0 );
+ firstDirection.set( 0, 0, - 1 );
+ secondDirection.set( 0, - 1, 0 );
+ firstSpacing = this.spacing[ 2 ];
+ secondSpacing = this.spacing[ 1 ];
+ IJKIndex = new THREE.Vector3( RASIndex, 0, 0 );
+
+ planeMatrix.multiply( ( new THREE.Matrix4() ).makeRotationY( Math.PI / 2 ) );
+ positionOffset = ( volume.RASDimensions[ 0 ] - 1 ) / 2;
+ planeMatrix.setPosition( new THREE.Vector3( RASIndex - positionOffset, 0, 0 ) );
+ break;
+ case 'y' :
+ axisInIJK.set( 0, 1, 0 );
+ firstDirection.set( 1, 0, 0 );
+ secondDirection.set( 0, 0, 1 );
+ firstSpacing = this.spacing[ 0 ];
+ secondSpacing = this.spacing[ 2 ];
+ IJKIndex = new THREE.Vector3( 0, RASIndex, 0 );
+
+ planeMatrix.multiply( ( new THREE.Matrix4() ).makeRotationX( - Math.PI / 2 ) );
+ positionOffset = ( volume.RASDimensions[ 1 ] - 1 ) / 2;
+ planeMatrix.setPosition( new THREE.Vector3( 0, RASIndex - positionOffset, 0 ) );
+ break;
+ case 'z' :
+ default :
+ axisInIJK.set( 0, 0, 1 );
+ firstDirection.set( 1, 0, 0 );
+ secondDirection.set( 0, - 1, 0 );
+ firstSpacing = this.spacing[ 0 ];
+ secondSpacing = this.spacing[ 1 ];
+ IJKIndex = new THREE.Vector3( 0, 0, RASIndex );
+
+ positionOffset = ( volume.RASDimensions[ 2 ] - 1 ) / 2;
+ planeMatrix.setPosition( new THREE.Vector3( 0, 0, RASIndex - positionOffset ) );
+ break;
+ }
+
+ firstDirection.applyMatrix4( volume.inverseMatrix ).normalize();
+ firstDirection.argVar = 'i';
+ secondDirection.applyMatrix4( volume.inverseMatrix ).normalize();
+ secondDirection.argVar = 'j';
+ axisInIJK.applyMatrix4( volume.inverseMatrix ).normalize();
+ iLength = Math.floor( Math.abs( firstDirection.dot( dimensions ) ) );
+ jLength = Math.floor( Math.abs( secondDirection.dot( dimensions ) ) );
+ planeWidth = Math.abs( iLength * firstSpacing );
+ planeHeight = Math.abs( jLength * secondSpacing );
+
+ IJKIndex = Math.abs( Math.round( IJKIndex.applyMatrix4( volume.inverseMatrix ).dot( axisInIJK ) ) );
+ var base = [ new THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 0, 1, 0 ), new THREE.Vector3( 0, 0, 1 ) ];
+ var iDirection = [ firstDirection, secondDirection, axisInIJK ].find( function( x ) {
+
+ return Math.abs( x.dot( base[ 0 ] ) ) > 0.9;
+
+ } );
+ var jDirection = [ firstDirection, secondDirection, axisInIJK ].find( function( x ) {
+
+ return Math.abs( x.dot( base[ 1 ] ) ) > 0.9;
+
+ } );
+ var kDirection = [ firstDirection, secondDirection, axisInIJK ].find( function( x ) {
+
+ return Math.abs( x.dot( base[ 2 ] ) ) > 0.9;
+
+ } );
+ var argumentsWithInversion = [ 'volume.xLength-1-', 'volume.yLength-1-', 'volume.zLength-1-' ];
+ var arguments = [ 'i', 'j', 'k' ];
+ var argArray = [ iDirection, jDirection, kDirection ].map( function( direction, n ) {
+
+ return ( direction.dot( base[ n ] ) > 0 ? '' : argumentsWithInversion[ n ] ) + ( direction === axisInIJK ? 'IJKIndex' : direction.argVar )
+
+ } );
+ var argString = argArray.join( ',' );
+ sliceAccess = eval( '(function sliceAccess (i,j) {return volume.access( ' + argString + ');})' );
+
+
+ return {
+ iLength : iLength,
+ jLength : jLength,
+ sliceAccess : sliceAccess,
+ matrix : planeMatrix,
+ planeWidth : planeWidth,
+ planeHeight : planeHeight
+ }
+
+ },
+
+ /**
+ * @member {Function} extractSlice Returns a slice corresponding to the given axis and index
+ * The coordinate are given in the Right Anterior Superior coordinate format
+ * @memberof THREE.Volume
+ * @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
+ * @param {number} index the index of the slice
+ * @returns {THREE.VolumeSlice} the extracted slice
+ */
+ extractSlice : function( axis, index ) {
+
+ var slice = new THREE.VolumeSlice( this, index, axis );
+ this.sliceList.push( slice );
+ return slice;
+
+ },
+
+ /**
+ * @member {Function} repaintAllSlices Call repaint on all the slices extracted from this volume
+ * @see THREE.VolumeSlice.repaint
+ * @memberof THREE.Volume
+ * @returns {THREE.Volume} this
+ */
+ repaintAllSlices : function() {
+
+ this.sliceList.forEach( function( slice ) {
+
+ slice.repaint();
+
+ } );
+
+ return this;
+
+ },
+
+ /**
+ * @member {Function} computeMinMax Compute the minimum and the maximum of the data in the volume
+ * @memberof THREE.Volume
+ * @returns {Array} [min,max]
+ */
+ computeMinMax : function() {
+
+ var min = Infinity;
+ var max = - Infinity;
+
+ // buffer the length
+ var datasize = this.data.length;
+
+ var i = 0;
+ for ( i = 0; i < datasize; i ++ ) {
+
+ if ( ! isNaN( this.data[ i ] ) ) {
+
+ var value = this.data[ i ];
+ min = Math.min( min, value );
+ max = Math.max( max, value );
+
+ }
+
+ }
+ this.min = min;
+ this.max = max;
+
+ return [ min, max ];
+
+ }
+
+};
diff --git a/app/static/js/VolumeSlice.js b/app/static/js/VolumeSlice.js
new file mode 100644
index 0000000..bd554eb
--- /dev/null
+++ b/app/static/js/VolumeSlice.js
@@ -0,0 +1,217 @@
+/**
+ * This class has been made to hold a slice of a volume data
+ * @class
+ * @author Valentin Demeusy / https://github.com/stity
+ * @param {THREE.Volume} volume The associated volume
+ * @param {number} [index=0] The index of the slice
+ * @param {string} [axis='z'] For now only 'x', 'y' or 'z' but later it will change to a normal vector
+ * @see THREE.Volume
+ */
+THREE.VolumeSlice = function( volume, index, axis ) {
+
+ var slice = this;
+ /**
+ * @member {THREE.Volume} volume The associated volume
+ */
+ this.volume = volume;
+ /**
+ * @member {Number} index The index of the slice, if changed, will automatically call updateGeometry at the next repaint
+ */
+ index = index || 0;
+ Object.defineProperty( this, 'index', {
+ get : function() {
+
+ return index;
+
+ },
+ set : function( value ) {
+
+ index = value;
+ slice.geometryNeedsUpdate = true;
+ return index;
+
+ }
+ } );
+ /**
+ * @member {String} axis The normal axis
+ */
+ this.axis = axis || 'z';
+
+ /**
+ * @member {HTMLCanvasElement} canvas The final canvas used for the texture
+ */
+ /**
+ * @member {CanvasRenderingContext2D} ctx Context of the canvas
+ */
+ this.canvas = document.createElement( 'canvas' );
+ /**
+ * @member {HTMLCanvasElement} canvasBuffer The intermediary canvas used to paint the data
+ */
+ /**
+ * @member {CanvasRenderingContext2D} ctxBuffer Context of the canvas buffer
+ */
+ this.canvasBuffer = document.createElement( 'canvas' );
+ this.updateGeometry();
+
+
+ var canvasMap = new THREE.Texture( this.canvas );
+ canvasMap.minFilter = THREE.LinearFilter;
+ canvasMap.wrapS = canvasMap.wrapT = THREE.ClampToEdgeWrapping;
+ var material = new THREE.MeshBasicMaterial( { map: canvasMap, side: THREE.DoubleSide, transparent : true } );
+ /**
+ * @member {THREE.Mesh} mesh The mesh ready to get used in the scene
+ */
+ this.mesh = new THREE.Mesh( this.geometry, material );
+ /**
+ * @member {Boolean} geometryNeedsUpdate If set to true, updateGeometry will be triggered at the next repaint
+ */
+ this.geometryNeedsUpdate = true;
+ this.repaint();
+
+ /**
+ * @member {Number} iLength Width of slice in the original coordinate system, corresponds to the width of the buffer canvas
+ */
+
+ /**
+ * @member {Number} jLength Height of slice in the original coordinate system, corresponds to the height of the buffer canvas
+ */
+
+ /**
+ * @member {Function} sliceAccess Function that allow the slice to access right data
+ * @see THREE.Volume.extractPerpendicularPlane
+ * @param {Number} i The first coordinate
+ * @param {Number} j The second coordinate
+ * @returns {Number} the index corresponding to the voxel in volume.data of the given position in the slice
+ */
+
+
+};
+
+THREE.VolumeSlice.prototype = {
+
+ constructor : THREE.VolumeSlice,
+
+ /**
+ * @member {Function} repaint Refresh the texture and the geometry if geometryNeedsUpdate is set to true
+ * @memberof THREE.VolumeSlice
+ */
+ repaint : function() {
+
+ if ( this.geometryNeedsUpdate ) {
+
+ this.updateGeometry();
+
+ }
+
+ var iLength = this.iLength,
+ jLength = this.jLength,
+ sliceAccess = this.sliceAccess,
+ volume = this.volume,
+ axis = this.axis,
+ index = this.index,
+ canvas = this.canvasBuffer,
+ ctx = this.ctxBuffer;
+
+
+ // get the imageData and pixel array from the canvas
+ var imgData = ctx.getImageData( 0, 0, iLength, jLength );
+ var data = imgData.data;
+ var volumeData = volume.data;
+ var upperThreshold = volume.upperThreshold;
+ var lowerThreshold = volume.lowerThreshold;
+ var windowLow = volume.windowLow;
+ var windowHigh = volume.windowHigh;
+
+ // manipulate some pixel elements
+ var pixelCount = 0;
+
+ if ( volume.dataType === 'label' ) {
+
+ //this part is currently useless but will be used when colortables will be handled
+ for ( var j = 0; j < jLength; j ++ ) {
+
+ for ( var i = 0; i < iLength; i ++ ) {
+
+ var label = volumeData[ sliceAccess( i, j ) ];
+ label = label >= this.colorMap.length ? ( label % this.colorMap.length ) + 1 : label;
+ var color = this.colorMap[ label ];
+ data[ 4 * pixelCount ] = ( color >> 24 ) & 0xff;
+ data[ 4 * pixelCount + 1 ] = ( color >> 16 ) & 0xff;
+ data[ 4 * pixelCount + 2 ] = ( color >> 8 ) & 0xff;
+ data[ 4 * pixelCount + 3 ] = color & 0xff;
+ pixelCount ++;
+
+ }
+
+ }
+
+ }
+ else {
+
+ for ( var j = 0; j < jLength; j ++ ) {
+
+ for ( var i = 0; i < iLength; i ++ ) {
+
+ var value = volumeData[ sliceAccess( i, j ) ];
+ var alpha = 0xff;
+ //apply threshold
+ alpha = upperThreshold >= value ? ( lowerThreshold <= value ? alpha : 0 ) : 0;
+ //apply window level
+ value = Math.floor( 255 * ( value - windowLow ) / ( windowHigh - windowLow ) );
+ value = value > 255 ? 255 : ( value < 0 ? 0 : value | 0 );
+
+ data[ 4 * pixelCount ] = value;
+ data[ 4 * pixelCount + 1 ] = value;
+ data[ 4 * pixelCount + 2 ] = value;
+ data[ 4 * pixelCount + 3 ] = alpha;
+ pixelCount ++;
+
+ }
+
+ }
+
+ }
+ ctx.putImageData( imgData, 0, 0 );
+ this.ctx.drawImage( canvas, 0, 0, iLength, jLength, 0, 0, this.canvas.width, this.canvas.height );
+
+
+ this.mesh.material.map.needsUpdate = true;
+
+ },
+
+ /**
+ * @member {Function} Refresh the geometry according to axis and index
+ * @see THREE.Volume.extractPerpendicularPlane
+ * @memberof THREE.VolumeSlice
+ */
+ updateGeometry : function() {
+
+ var extracted = this.volume.extractPerpendicularPlane( this.axis, this.index );
+ this.sliceAccess = extracted.sliceAccess;
+ this.jLength = extracted.jLength;
+ this.iLength = extracted.iLength;
+ this.matrix = extracted.matrix;
+
+ this.canvas.width = extracted.planeWidth;
+ this.canvas.height = extracted.planeHeight;
+ this.canvasBuffer.width = this.iLength;
+ this.canvasBuffer.height = this.jLength;
+ this.ctx = this.canvas.getContext( '2d' );
+ this.ctxBuffer = this.canvasBuffer.getContext( '2d' );
+
+ this.geometry = new THREE.PlaneGeometry( extracted.planeWidth, extracted.planeHeight );
+
+ if ( this.mesh ) {
+
+ this.mesh.geometry = this.geometry;
+ //reset mesh matrix
+ this.mesh.matrix = ( new THREE.Matrix4() ).identity();
+ this.mesh.applyMatrix( this.matrix );
+
+ }
+
+ this.geometryNeedsUpdate = false;
+
+ }
+
+};
diff --git a/app/static/js/animation/CCDIKSolver.js b/app/static/js/animation/CCDIKSolver.js
new file mode 100644
index 0000000..1caf691
--- /dev/null
+++ b/app/static/js/animation/CCDIKSolver.js
@@ -0,0 +1,417 @@
+/**
+ * @author takahiro / https://github.com/takahirox
+ *
+ * CCD Algorithm
+ * https://sites.google.com/site/auraliusproject/ccd-algorithm
+ *
+ * mesh.geometry needs to have iks array.
+ *
+ * // ik parameter example
+ * //
+ * // target, effector, index in links are bone index in skeleton.
+ * // the bones relation should be
+ * // <-- parent child -->
+ * // links[ n ], links[ n - 1 ], ..., links[ 0 ], effector
+ * ik = {
+ * target: 1,
+ * effector: 2,
+ * links: [ { index: 5, limitation: new THREE.Vector3( 1, 0, 0 ) }, { index: 4, enabled: false }, { index : 3 } ],
+ * iteration: 10,
+ * minAngle: 0.0,
+ * maxAngle: 1.0,
+ * };
+ */
+
+THREE.CCDIKSolver = function ( mesh ) {
+
+ this.mesh = mesh;
+
+ this._valid();
+
+};
+
+THREE.CCDIKSolver.prototype = {
+
+ constructor: THREE.CCDIKSolver,
+
+ _valid: function () {
+
+ var iks = this.mesh.geometry.iks;
+ var bones = this.mesh.skeleton.bones;
+
+ for ( var i = 0, il = iks.length; i < il; i ++ ) {
+
+ var ik = iks[ i ];
+
+ var effector = bones[ ik.effector ];
+
+ var links = ik.links;
+
+ var link0, link1;
+
+ link0 = effector;
+
+ for ( var j = 0, jl = links.length; j < jl; j ++ ) {
+
+ link1 = bones[ links[ j ].index ];
+
+ if ( link0.parent !== link1 ) {
+
+ console.warn( 'THREE.CCDIKSolver: bone ' + link0.name + ' is not the child of bone ' + link1.name );
+
+ }
+
+ link0 = link1;
+
+ }
+
+ }
+
+ },
+
+ /*
+ * save the bone matrices before solving IK.
+ * they're used for generating VMD and VPD.
+ */
+ _saveOriginalBonesInfo: function () {
+
+ var bones = this.mesh.skeleton.bones;
+
+ for ( var i = 0, il = bones.length; i < il; i ++ ) {
+
+ var bone = bones[ i ];
+
+ if ( bone.userData.ik === undefined ) bone.userData.ik = {};
+
+ bone.userData.ik.originalMatrix = bone.matrix.toArray();
+
+ }
+
+ },
+
+ update: function ( saveOriginalBones ) {
+
+ var q = new THREE.Quaternion();
+
+ var targetPos = new THREE.Vector3();
+ var targetVec = new THREE.Vector3();
+ var effectorPos = new THREE.Vector3();
+ var effectorVec = new THREE.Vector3();
+ var linkPos = new THREE.Vector3();
+ var invLinkQ = new THREE.Quaternion();
+ var linkScale = new THREE.Vector3();
+ var axis = new THREE.Vector3();
+
+ var bones = this.mesh.skeleton.bones;
+ var iks = this.mesh.geometry.iks;
+
+ var boneParams = this.mesh.geometry.bones;
+
+ // for reference overhead reduction in loop
+ var math = Math;
+
+ this.mesh.updateMatrixWorld( true );
+
+ if ( saveOriginalBones === true ) this._saveOriginalBonesInfo();
+
+ for ( var i = 0, il = iks.length; i < il; i++ ) {
+
+ var ik = iks[ i ];
+ var effector = bones[ ik.effector ];
+ var target = bones[ ik.target ];
+
+ // don't use getWorldPosition() here for the performance
+ // because it calls updateMatrixWorld( true ) inside.
+ targetPos.setFromMatrixPosition( target.matrixWorld );
+
+ var links = ik.links;
+ var iteration = ik.iteration !== undefined ? ik.iteration : 1;
+
+ for ( var j = 0; j < iteration; j++ ) {
+
+ var rotated = false;
+
+ for ( var k = 0, kl = links.length; k < kl; k++ ) {
+
+ var link = bones[ links[ k ].index ];
+
+ // skip this link and following links.
+ // this skip is used for MMD performance optimization.
+ if ( links[ k ].enabled === false ) break;
+
+ var limitation = links[ k ].limitation;
+
+ // don't use getWorldPosition/Quaternion() here for the performance
+ // because they call updateMatrixWorld( true ) inside.
+ link.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
+ invLinkQ.inverse();
+ effectorPos.setFromMatrixPosition( effector.matrixWorld );
+
+ // work in link world
+ effectorVec.subVectors( effectorPos, linkPos );
+ effectorVec.applyQuaternion( invLinkQ );
+ effectorVec.normalize();
+
+ targetVec.subVectors( targetPos, linkPos );
+ targetVec.applyQuaternion( invLinkQ );
+ targetVec.normalize();
+
+ var angle = targetVec.dot( effectorVec );
+
+ if ( angle > 1.0 ) {
+
+ angle = 1.0;
+
+ } else if ( angle < -1.0 ) {
+
+ angle = -1.0;
+
+ }
+
+ angle = math.acos( angle );
+
+ // skip if changing angle is too small to prevent vibration of bone
+ // Refer to http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
+ if ( angle < 1e-5 ) continue;
+
+ if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
+
+ angle = ik.minAngle;
+
+ }
+
+ if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
+
+ angle = ik.maxAngle;
+
+ }
+
+ axis.crossVectors( effectorVec, targetVec );
+ axis.normalize();
+
+ q.setFromAxisAngle( axis, angle );
+ link.quaternion.multiply( q );
+
+ // TODO: re-consider the limitation specification
+ if ( limitation !== undefined ) {
+
+ var c = link.quaternion.w;
+
+ if ( c > 1.0 ) {
+
+ c = 1.0;
+
+ }
+
+ var c2 = math.sqrt( 1 - c * c );
+ link.quaternion.set( limitation.x * c2,
+ limitation.y * c2,
+ limitation.z * c2,
+ c );
+
+ }
+
+ link.updateMatrixWorld( true );
+ rotated = true;
+
+ }
+
+ if ( ! rotated ) break;
+
+ }
+
+ }
+
+ // just in case
+ this.mesh.updateMatrixWorld( true );
+
+ }
+
+};
+
+
+THREE.CCDIKHelper = function ( mesh ) {
+
+ if ( mesh.geometry.iks === undefined || mesh.skeleton === undefined ) {
+
+ throw 'THREE.CCDIKHelper requires iks in mesh.geometry and skeleton in mesh.';
+
+ }
+
+ THREE.Object3D.call( this );
+
+ this.root = mesh;
+
+ this.matrix = mesh.matrixWorld;
+ this.matrixAutoUpdate = false;
+
+ this.sphereGeometry = new THREE.SphereBufferGeometry( 0.25, 16, 8 );
+
+ this.targetSphereMaterial = new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0xff8888 ),
+ depthTest: false,
+ depthWrite: false,
+ transparent: true
+ } );
+
+ this.effectorSphereMaterial = new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0x88ff88 ),
+ depthTest: false,
+ depthWrite: false,
+ transparent: true
+ } );
+
+ this.linkSphereMaterial = new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0x8888ff ),
+ depthTest: false,
+ depthWrite: false,
+ transparent: true
+ } );
+
+ this.lineMaterial = new THREE.LineBasicMaterial( {
+ color: new THREE.Color( 0xff0000 ),
+ depthTest: false,
+ depthWrite: false,
+ transparent: true
+ } );
+
+ this._init();
+ this.update();
+
+};
+
+THREE.CCDIKHelper.prototype = Object.create( THREE.Object3D.prototype );
+THREE.CCDIKHelper.prototype.constructor = THREE.CCDIKHelper;
+
+THREE.CCDIKHelper.prototype._init = function () {
+
+ var self = this;
+ var mesh = this.root;
+ var iks = mesh.geometry.iks;
+
+ function createLineGeometry( ik ) {
+
+ var geometry = new THREE.BufferGeometry();
+ var vertices = new Float32Array( ( 2 + ik.links.length ) * 3 );
+ geometry.addAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
+
+ return geometry;
+
+ }
+
+ function createTargetMesh() {
+
+ return new THREE.Mesh( self.sphereGeometry, self.targetSphereMaterial );
+
+ }
+
+ function createEffectorMesh() {
+
+ return new THREE.Mesh( self.sphereGeometry, self.effectorSphereMaterial );
+
+ }
+
+ function createLinkMesh() {
+
+ return new THREE.Mesh( self.sphereGeometry, self.linkSphereMaterial );
+
+ }
+
+ function createLine( ik ) {
+
+ return new THREE.Line( createLineGeometry( ik ), self.lineMaterial );
+
+ }
+
+ for ( var i = 0, il = iks.length; i < il; i ++ ) {
+
+ var ik = iks[ i ];
+
+ this.add( createTargetMesh() );
+ this.add( createEffectorMesh() );
+
+ for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+ this.add( createLinkMesh() );
+
+ }
+
+ this.add( createLine( ik ) );
+
+ }
+
+};
+
+THREE.CCDIKHelper.prototype.update = function () {
+
+ var offset = 0;
+
+ var mesh = this.root;
+ var iks = mesh.geometry.iks;
+ var bones = mesh.skeleton.bones;
+
+ var matrixWorldInv = new THREE.Matrix4().getInverse( mesh.matrixWorld );
+ var vector = new THREE.Vector3();
+
+ function getPosition( bone ) {
+
+ vector.setFromMatrixPosition( bone.matrixWorld );
+ vector.applyMatrix4( matrixWorldInv );
+
+ return vector;
+
+ }
+
+ function setPositionOfBoneToAttributeArray( array, index, bone ) {
+
+ var v = getPosition( bone );
+
+ array[ index * 3 + 0 ] = v.x;
+ array[ index * 3 + 1 ] = v.y;
+ array[ index * 3 + 2 ] = v.z;
+
+ }
+
+ for ( var i = 0, il = iks.length; i < il; i ++ ) {
+
+ var ik = iks[ i ];
+
+ var targetBone = bones[ ik.target ];
+ var effectorBone = bones[ ik.effector ];
+
+ var targetMesh = this.children[ offset ++ ];
+ var effectorMesh = this.children[ offset ++ ];
+
+ targetMesh.position.copy( getPosition( targetBone ) );
+ effectorMesh.position.copy( getPosition( effectorBone ) );
+
+ for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+ var link = ik.links[ j ];
+ var linkBone = bones[ link.index ];
+
+ var linkMesh = this.children[ offset ++ ];
+
+ linkMesh.position.copy( getPosition( linkBone ) );
+
+ }
+
+ var line = this.children[ offset ++ ];
+ var array = line.geometry.attributes.position.array;
+
+ setPositionOfBoneToAttributeArray( array, 0, targetBone );
+ setPositionOfBoneToAttributeArray( array, 1, effectorBone );
+
+ for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+ var link = ik.links[ j ];
+ var linkBone = bones[ link.index ];
+ setPositionOfBoneToAttributeArray( array, j + 2, linkBone );
+
+ }
+
+ line.geometry.attributes.position.needsUpdate = true;
+
+ }
+
+};
diff --git a/app/static/js/animation/MMDPhysics.js b/app/static/js/animation/MMDPhysics.js
new file mode 100644
index 0000000..25f78cf
--- /dev/null
+++ b/app/static/js/animation/MMDPhysics.js
@@ -0,0 +1,1217 @@
+/**
+ * @author takahiro / https://github.com/takahirox
+ *
+ * Dependencies
+ * - Ammo.js https://github.com/kripken/ammo.js
+ *
+ * MMD specific Physics class.
+ *
+ * See THREE.MMDLoader for the passed parameter list of RigidBody/Constraint.
+ *
+ * Requirement:
+ * - don't change object's scale from (1,1,1) after setting physics to object
+ *
+ * TODO
+ * - optimize for the performance
+ * - use Physijs http://chandlerprall.github.io/Physijs/
+ * and improve the performance by making use of Web worker.
+ * - if possible, make this class being non-MMD specific.
+ * - object scale change support
+ */
+
+THREE.MMDPhysics = function ( mesh, params ) {
+
+ if ( params === undefined ) params = {};
+
+ this.mesh = mesh;
+ this.helper = new THREE.MMDPhysics.ResourceHelper();
+
+ /*
+ * I don't know why but 1/60 unitStep easily breaks models
+ * so I set it 1/65 so far.
+ * Don't set too small unitStep because
+ * the smaller unitStep can make the performance worse.
+ */
+ this.unitStep = ( params.unitStep !== undefined ) ? params.unitStep : 1 / 65;
+ this.maxStepNum = ( params.maxStepNum !== undefined ) ? params.maxStepNum : 3;
+
+ this.world = params.world !== undefined ? params.world : null;
+ this.bodies = [];
+ this.constraints = [];
+
+ this.init( mesh );
+
+};
+
+THREE.MMDPhysics.prototype = {
+
+ constructor: THREE.MMDPhysics,
+
+ init: function ( mesh ) {
+
+ var parent = mesh.parent;
+
+ if ( parent !== null ) {
+
+ parent.remove( mesh );
+
+ }
+
+ var helper = this.helper;
+ var currentPosition = helper.allocThreeVector3();
+ var currentRotation = helper.allocThreeVector3();
+ var currentScale = helper.allocThreeVector3();
+
+ currentPosition.copy( mesh.position );
+ currentRotation.copy( mesh.rotation );
+ currentScale.copy( mesh.scale );
+
+ mesh.position.set( 0, 0, 0 );
+ mesh.rotation.set( 0, 0, 0 );
+ mesh.scale.set( 1, 1, 1 );
+
+ mesh.updateMatrixWorld( true );
+
+ if ( this.world === null ) this.initWorld();
+ this.initRigidBodies();
+ this.initConstraints();
+
+ if ( parent !== null ) {
+
+ parent.add( mesh );
+
+ }
+
+ mesh.position.copy( currentPosition );
+ mesh.rotation.copy( currentRotation );
+ mesh.scale.copy( currentScale );
+
+ mesh.updateMatrixWorld( true );
+
+ this.reset();
+
+ helper.freeThreeVector3( currentPosition );
+ helper.freeThreeVector3( currentRotation );
+ helper.freeThreeVector3( currentScale );
+
+ },
+
+ initWorld: function () {
+
+ var config = new Ammo.btDefaultCollisionConfiguration();
+ var dispatcher = new Ammo.btCollisionDispatcher( config );
+ var cache = new Ammo.btDbvtBroadphase();
+ var solver = new Ammo.btSequentialImpulseConstraintSolver();
+ var world = new Ammo.btDiscreteDynamicsWorld( dispatcher, cache, solver, config );
+ world.setGravity( new Ammo.btVector3( 0, -9.8 * 10, 0 ) );
+ this.world = world;
+
+ },
+
+ initRigidBodies: function () {
+
+ var bodies = this.mesh.geometry.rigidBodies;
+
+ for ( var i = 0; i < bodies.length; i++ ) {
+
+ var b = new THREE.MMDPhysics.RigidBody( this.mesh, this.world, bodies[ i ], this.helper );
+ this.bodies.push( b );
+
+ }
+
+ },
+
+ initConstraints: function () {
+
+ var constraints = this.mesh.geometry.constraints;
+
+ for ( var i = 0; i < constraints.length; i++ ) {
+
+ var params = constraints[ i ];
+ var bodyA = this.bodies[ params.rigidBodyIndex1 ];
+ var bodyB = this.bodies[ params.rigidBodyIndex2 ];
+ var c = new THREE.MMDPhysics.Constraint( this.mesh, this.world, bodyA, bodyB, params, this.helper );
+ this.constraints.push( c );
+
+ }
+
+
+ },
+
+ update: function ( delta ) {
+
+ this.updateRigidBodies();
+ this.stepSimulation( delta );
+ this.updateBones();
+
+ },
+
+ stepSimulation: function ( delta ) {
+
+ var unitStep = this.unitStep;
+ var stepTime = delta;
+ var maxStepNum = ( ( delta / unitStep ) | 0 ) + 1;
+
+ if ( stepTime < unitStep ) {
+
+ stepTime = unitStep;
+ maxStepNum = 1;
+
+ }
+
+ if ( maxStepNum > this.maxStepNum ) {
+
+ maxStepNum = this.maxStepNum;
+
+ }
+
+ this.world.stepSimulation( stepTime, maxStepNum, unitStep );
+
+ },
+
+ updateRigidBodies: function () {
+
+ for ( var i = 0; i < this.bodies.length; i++ ) {
+
+ this.bodies[ i ].updateFromBone();
+
+ }
+
+ },
+
+ updateBones: function () {
+
+ for ( var i = 0; i < this.bodies.length; i++ ) {
+
+ this.bodies[ i ].updateBone();
+
+ }
+
+ },
+
+ reset: function () {
+
+ for ( var i = 0; i < this.bodies.length; i++ ) {
+
+ this.bodies[ i ].reset();
+
+ }
+
+ },
+
+ warmup: function ( cycles ) {
+
+ for ( var i = 0; i < cycles; i++ ) {
+
+ this.update( 1 / 60 );
+
+ }
+
+ }
+
+};
+
+/**
+ * This helper class responsibilies are
+ *
+ * 1. manage Ammo.js and Three.js object resources and
+ * improve the performance and the memory consumption by
+ * reusing objects.
+ *
+ * 2. provide simple Ammo object operations.
+ */
+THREE.MMDPhysics.ResourceHelper = function () {
+
+ // for Three.js
+ this.threeVector3s = [];
+ this.threeMatrix4s = [];
+ this.threeQuaternions = [];
+ this.threeEulers = [];
+
+ // for Ammo.js
+ this.transforms = [];
+ this.quaternions = [];
+ this.vector3s = [];
+
+};
+
+THREE.MMDPhysics.ResourceHelper.prototype = {
+
+ allocThreeVector3: function () {
+
+ return ( this.threeVector3s.length > 0 ) ? this.threeVector3s.pop() : new THREE.Vector3();
+
+ },
+
+ freeThreeVector3: function ( v ) {
+
+ this.threeVector3s.push( v );
+
+ },
+
+ allocThreeMatrix4: function () {
+
+ return ( this.threeMatrix4s.length > 0 ) ? this.threeMatrix4s.pop() : new THREE.Matrix4();
+
+ },
+
+ freeThreeMatrix4: function ( m ) {
+
+ this.threeMatrix4s.push( m );
+
+ },
+
+ allocThreeQuaternion: function () {
+
+ return ( this.threeQuaternions.length > 0 ) ? this.threeQuaternions.pop() : new THREE.Quaternion();
+
+ },
+
+ freeThreeQuaternion: function ( q ) {
+
+ this.threeQuaternions.push( q );
+
+ },
+
+ allocThreeEuler: function () {
+
+ return ( this.threeEulers.length > 0 ) ? this.threeEulers.pop() : new THREE.Euler();
+
+ },
+
+ freeThreeEuler: function ( e ) {
+
+ this.threeEulers.push( e );
+
+ },
+
+ allocTransform: function () {
+
+ return ( this.transforms.length > 0 ) ? this.transforms.pop() : new Ammo.btTransform();
+
+ },
+
+ freeTransform: function ( t ) {
+
+ this.transforms.push( t );
+
+ },
+
+ allocQuaternion: function () {
+
+ return ( this.quaternions.length > 0 ) ? this.quaternions.pop() : new Ammo.btQuaternion();
+
+ },
+
+ freeQuaternion: function ( q ) {
+
+ this.quaternions.push( q );
+
+ },
+
+ allocVector3: function () {
+
+ return ( this.vector3s.length > 0 ) ? this.vector3s.pop() : new Ammo.btVector3();
+
+ },
+
+ freeVector3: function ( v ) {
+
+ this.vector3s.push( v );
+
+ },
+
+ setIdentity: function ( t ) {
+
+ t.setIdentity();
+
+ },
+
+ getBasis: function ( t ) {
+
+ var q = this.allocQuaternion();
+ t.getBasis().getRotation( q );
+ return q;
+
+ },
+
+ getBasisAsMatrix3: function ( t ) {
+
+ var q = this.getBasis( t );
+ var m = this.quaternionToMatrix3( q );
+ this.freeQuaternion( q );
+ return m;
+
+ },
+
+ getOrigin: function( t ) {
+
+ return t.getOrigin();
+
+ },
+
+ setOrigin: function( t, v ) {
+
+ t.getOrigin().setValue( v.x(), v.y(), v.z() );
+
+ },
+
+ copyOrigin: function( t1, t2 ) {
+
+ var o = t2.getOrigin();
+ this.setOrigin( t1, o );
+
+ },
+
+ setBasis: function( t, q ) {
+
+ t.setRotation( q );
+
+ },
+
+ setBasisFromMatrix3: function( t, m ) {
+
+ var q = this.matrix3ToQuaternion( m );
+ this.setBasis( t, q );
+ this.freeQuaternion( q );
+
+ },
+
+ setOriginFromArray3: function ( t, a ) {
+
+ t.getOrigin().setValue( a[ 0 ], a[ 1 ], a[ 2 ] );
+
+ },
+
+ setOriginFromThreeVector3: function ( t, v ) {
+
+ t.getOrigin().setValue( v.x, v.y, v.z );
+
+ },
+
+ setBasisFromArray3: function ( t, a ) {
+
+ var thQ = this.allocThreeQuaternion();
+ var thE = this.allocThreeEuler();
+ thE.set( a[ 0 ], a[ 1 ], a[ 2 ] );
+ this.setBasisFromThreeQuaternion( t, thQ.setFromEuler( thE ) );
+
+ this.freeThreeEuler( thE );
+ this.freeThreeQuaternion( thQ );
+
+ },
+
+ setBasisFromThreeQuaternion: function ( t, a ) {
+
+ var q = this.allocQuaternion();
+
+ q.setX( a.x );
+ q.setY( a.y );
+ q.setZ( a.z );
+ q.setW( a.w );
+ this.setBasis( t, q );
+
+ this.freeQuaternion( q );
+
+ },
+
+ multiplyTransforms: function ( t1, t2 ) {
+
+ var t = this.allocTransform();
+ this.setIdentity( t );
+
+ var m1 = this.getBasisAsMatrix3( t1 );
+ var m2 = this.getBasisAsMatrix3( t2 );
+
+ var o1 = this.getOrigin( t1 );
+ var o2 = this.getOrigin( t2 );
+
+ var v1 = this.multiplyMatrix3ByVector3( m1, o2 );
+ var v2 = this.addVector3( v1, o1 );
+ this.setOrigin( t, v2 );
+
+ var m3 = this.multiplyMatrices3( m1, m2 );
+ this.setBasisFromMatrix3( t, m3 );
+
+ this.freeVector3( v1 );
+ this.freeVector3( v2 );
+
+ return t;
+
+ },
+
+ inverseTransform: function ( t ) {
+
+ var t2 = this.allocTransform();
+
+ var m1 = this.getBasisAsMatrix3( t );
+ var o = this.getOrigin( t );
+
+ var m2 = this.transposeMatrix3( m1 );
+ var v1 = this.negativeVector3( o );
+ var v2 = this.multiplyMatrix3ByVector3( m2, v1 );
+
+ this.setOrigin( t2, v2 );
+ this.setBasisFromMatrix3( t2, m2 );
+
+ this.freeVector3( v1 );
+ this.freeVector3( v2 );
+
+ return t2;
+
+ },
+
+ multiplyMatrices3: function ( m1, m2 ) {
+
+ var m3 = [];
+
+ var v10 = this.rowOfMatrix3( m1, 0 );
+ var v11 = this.rowOfMatrix3( m1, 1 );
+ var v12 = this.rowOfMatrix3( m1, 2 );
+
+ var v20 = this.columnOfMatrix3( m2, 0 );
+ var v21 = this.columnOfMatrix3( m2, 1 );
+ var v22 = this.columnOfMatrix3( m2, 2 );
+
+ m3[ 0 ] = this.dotVectors3( v10, v20 );
+ m3[ 1 ] = this.dotVectors3( v10, v21 );
+ m3[ 2 ] = this.dotVectors3( v10, v22 );
+ m3[ 3 ] = this.dotVectors3( v11, v20 );
+ m3[ 4 ] = this.dotVectors3( v11, v21 );
+ m3[ 5 ] = this.dotVectors3( v11, v22 );
+ m3[ 6 ] = this.dotVectors3( v12, v20 );
+ m3[ 7 ] = this.dotVectors3( v12, v21 );
+ m3[ 8 ] = this.dotVectors3( v12, v22 );
+
+ this.freeVector3( v10 );
+ this.freeVector3( v11 );
+ this.freeVector3( v12 );
+ this.freeVector3( v20 );
+ this.freeVector3( v21 );
+ this.freeVector3( v22 );
+
+ return m3;
+
+ },
+
+ addVector3: function( v1, v2 ) {
+
+ var v = this.allocVector3();
+ v.setValue( v1.x() + v2.x(), v1.y() + v2.y(), v1.z() + v2.z() );
+ return v;
+
+ },
+
+ dotVectors3: function( v1, v2 ) {
+
+ return v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z();
+
+ },
+
+ rowOfMatrix3: function( m, i ) {
+
+ var v = this.allocVector3();
+ v.setValue( m[ i * 3 + 0 ], m[ i * 3 + 1 ], m[ i * 3 + 2 ] );
+ return v;
+
+ },
+
+ columnOfMatrix3: function( m, i ) {
+
+ var v = this.allocVector3();
+ v.setValue( m[ i + 0 ], m[ i + 3 ], m[ i + 6 ] );
+ return v;
+
+ },
+
+ negativeVector3: function( v ) {
+
+ var v2 = this.allocVector3();
+ v2.setValue( -v.x(), -v.y(), -v.z() );
+ return v2;
+
+ },
+
+ multiplyMatrix3ByVector3: function ( m, v ) {
+
+ var v4 = this.allocVector3();
+
+ var v0 = this.rowOfMatrix3( m, 0 );
+ var v1 = this.rowOfMatrix3( m, 1 );
+ var v2 = this.rowOfMatrix3( m, 2 );
+ var x = this.dotVectors3( v0, v );
+ var y = this.dotVectors3( v1, v );
+ var z = this.dotVectors3( v2, v );
+
+ v4.setValue( x, y, z );
+
+ this.freeVector3( v0 );
+ this.freeVector3( v1 );
+ this.freeVector3( v2 );
+
+ return v4;
+
+ },
+
+ transposeMatrix3: function( m ) {
+
+ var m2 = [];
+ m2[ 0 ] = m[ 0 ];
+ m2[ 1 ] = m[ 3 ];
+ m2[ 2 ] = m[ 6 ];
+ m2[ 3 ] = m[ 1 ];
+ m2[ 4 ] = m[ 4 ];
+ m2[ 5 ] = m[ 7 ];
+ m2[ 6 ] = m[ 2 ];
+ m2[ 7 ] = m[ 5 ];
+ m2[ 8 ] = m[ 8 ];
+ return m2;
+
+ },
+
+ quaternionToMatrix3: function ( q ) {
+
+ var m = [];
+
+ var x = q.x();
+ var y = q.y();
+ var z = q.z();
+ var w = q.w();
+
+ var xx = x * x;
+ var yy = y * y;
+ var zz = z * z;
+
+ var xy = x * y;
+ var yz = y * z;
+ var zx = z * x;
+
+ var xw = x * w;
+ var yw = y * w;
+ var zw = z * w;
+
+ m[ 0 ] = 1 - 2 * ( yy + zz );
+ m[ 1 ] = 2 * ( xy - zw );
+ m[ 2 ] = 2 * ( zx + yw );
+ m[ 3 ] = 2 * ( xy + zw );
+ m[ 4 ] = 1 - 2 * ( zz + xx );
+ m[ 5 ] = 2 * ( yz - xw );
+ m[ 6 ] = 2 * ( zx - yw );
+ m[ 7 ] = 2 * ( yz + xw );
+ m[ 8 ] = 1 - 2 * ( xx + yy );
+
+ return m;
+
+ },
+
+ matrix3ToQuaternion: function( m ) {
+
+ var t = m[ 0 ] + m[ 4 ] + m[ 8 ];
+ var s, x, y, z, w;
+
+ if( t > 0 ) {
+
+ s = Math.sqrt( t + 1.0 ) * 2;
+ w = 0.25 * s;
+ x = ( m[ 7 ] - m[ 5 ] ) / s;
+ y = ( m[ 2 ] - m[ 6 ] ) / s;
+ z = ( m[ 3 ] - m[ 1 ] ) / s;
+
+ } else if( ( m[ 0 ] > m[ 4 ] ) && ( m[ 0 ] > m[ 8 ] ) ) {
+
+ s = Math.sqrt( 1.0 + m[ 0 ] - m[ 4 ] - m[ 8 ] ) * 2;
+ w = ( m[ 7 ] - m[ 5 ] ) / s;
+ x = 0.25 * s;
+ y = ( m[ 1 ] + m[ 3 ] ) / s;
+ z = ( m[ 2 ] + m[ 6 ] ) / s;
+
+ } else if( m[ 4 ] > m[ 8 ] ) {
+
+ s = Math.sqrt( 1.0 + m[ 4 ] - m[ 0 ] - m[ 8 ] ) * 2;
+ w = ( m[ 2 ] - m[ 6 ] ) / s;
+ x = ( m[ 1 ] + m[ 3 ] ) / s;
+ y = 0.25 * s;
+ z = ( m[ 5 ] + m[ 7 ] ) / s;
+
+ } else {
+
+ s = Math.sqrt( 1.0 + m[ 8 ] - m[ 0 ] - m[ 4 ] ) * 2;
+ w = ( m[ 3 ] - m[ 1 ] ) / s;
+ x = ( m[ 2 ] + m[ 6 ] ) / s;
+ y = ( m[ 5 ] + m[ 7 ] ) / s;
+ z = 0.25 * s;
+
+ }
+
+ var q = this.allocQuaternion();
+ q.setX( x );
+ q.setY( y );
+ q.setZ( z );
+ q.setW( w );
+ return q;
+
+ }
+
+};
+
+THREE.MMDPhysics.RigidBody = function ( mesh, world, params, helper ) {
+
+ this.mesh = mesh;
+ this.world = world;
+ this.params = params;
+ this.helper = helper;
+
+ this.body = null;
+ this.bone = null;
+ this.boneOffsetForm = null;
+ this.boneOffsetFormInverse = null;
+
+ this.init();
+
+};
+
+THREE.MMDPhysics.RigidBody.prototype = {
+
+ constructor: THREE.MMDPhysics.RigidBody,
+
+ init: function () {
+
+ function generateShape( p ) {
+
+ switch( p.shapeType ) {
+
+ case 0:
+ return new Ammo.btSphereShape( p.width );
+
+ case 1:
+ return new Ammo.btBoxShape( new Ammo.btVector3( p.width, p.height, p.depth ) );
+
+ case 2:
+ return new Ammo.btCapsuleShape( p.width, p.height );
+
+ default:
+ throw 'unknown shape type ' + p.shapeType;
+
+ }
+
+ }
+
+ var helper = this.helper;
+ var params = this.params;
+ var bones = this.mesh.skeleton.bones;
+ var bone = ( params.boneIndex === -1 ) ? new THREE.Bone() : bones[ params.boneIndex ];
+
+ var shape = generateShape( params );
+ var weight = ( params.type === 0 ) ? 0 : params.weight;
+ var localInertia = helper.allocVector3();
+ localInertia.setValue( 0, 0, 0 );
+
+ if( weight !== 0 ) {
+
+ shape.calculateLocalInertia( weight, localInertia );
+
+ }
+
+ var boneOffsetForm = helper.allocTransform();
+ helper.setIdentity( boneOffsetForm );
+ helper.setOriginFromArray3( boneOffsetForm, params.position );
+ helper.setBasisFromArray3( boneOffsetForm, params.rotation );
+
+ var vector = helper.allocThreeVector3();
+ var boneForm = helper.allocTransform();
+ helper.setIdentity( boneForm );
+ helper.setOriginFromThreeVector3( boneForm, bone.getWorldPosition( vector ) );
+
+ var form = helper.multiplyTransforms( boneForm, boneOffsetForm );
+ var state = new Ammo.btDefaultMotionState( form );
+
+ var info = new Ammo.btRigidBodyConstructionInfo( weight, state, shape, localInertia );
+ info.set_m_friction( params.friction );
+ info.set_m_restitution( params.restitution );
+
+ var body = new Ammo.btRigidBody( info );
+
+ if ( params.type === 0 ) {
+
+ body.setCollisionFlags( body.getCollisionFlags() | 2 );
+
+ /*
+ * It'd be better to comment out this line though in general I should call this method
+ * because I'm not sure why but physics will be more like MMD's
+ * if I comment out.
+ */
+ body.setActivationState( 4 );
+
+ }
+
+ body.setDamping( params.positionDamping, params.rotationDamping );
+ body.setSleepingThresholds( 0, 0 );
+
+ this.world.addRigidBody( body, 1 << params.groupIndex, params.groupTarget );
+
+ this.body = body;
+ this.bone = bone;
+ this.boneOffsetForm = boneOffsetForm;
+ this.boneOffsetFormInverse = helper.inverseTransform( boneOffsetForm );
+
+ helper.freeVector3( localInertia );
+ helper.freeTransform( form );
+ helper.freeTransform( boneForm );
+ helper.freeThreeVector3( vector );
+
+ },
+
+ reset: function () {
+
+ this.setTransformFromBone();
+
+ },
+
+ updateFromBone: function () {
+
+ if ( this.params.boneIndex === -1 ) {
+
+ return;
+
+ }
+
+ if ( this.params.type === 0 ) {
+
+ this.setTransformFromBone();
+
+ }
+
+ },
+
+ updateBone: function () {
+
+ if ( this.params.type === 0 || this.params.boneIndex === -1 ) {
+
+ return;
+
+ }
+
+ this.updateBoneRotation();
+
+ if ( this.params.type === 1 ) {
+
+ this.updateBonePosition();
+
+ }
+
+ this.bone.updateMatrixWorld( true );
+
+ if ( this.params.type === 2 ) {
+
+ this.setPositionFromBone();
+
+ }
+
+ },
+
+ getBoneTransform: function () {
+
+ var helper = this.helper;
+ var p = helper.allocThreeVector3();
+ var q = helper.allocThreeQuaternion();
+
+ this.bone.getWorldPosition( p );
+ this.bone.getWorldQuaternion( q );
+
+ var tr = helper.allocTransform();
+ helper.setOriginFromThreeVector3( tr, p );
+ helper.setBasisFromThreeQuaternion( tr, q );
+
+ var form = helper.multiplyTransforms( tr, this.boneOffsetForm );
+
+ helper.freeTransform( tr );
+ helper.freeThreeQuaternion( q );
+ helper.freeThreeVector3( p );
+
+ return form;
+
+ },
+
+ getWorldTransformForBone: function () {
+
+ var helper = this.helper;
+
+ var tr = helper.allocTransform();
+ this.body.getMotionState().getWorldTransform( tr );
+ var tr2 = helper.multiplyTransforms( tr, this.boneOffsetFormInverse );
+
+ helper.freeTransform( tr );
+
+ return tr2;
+
+ },
+
+ setTransformFromBone: function () {
+
+ var helper = this.helper;
+ var form = this.getBoneTransform();
+
+ // TODO: check the most appropriate way to set
+ //this.body.setWorldTransform( form );
+ this.body.setCenterOfMassTransform( form );
+ this.body.getMotionState().setWorldTransform( form );
+
+ helper.freeTransform( form );
+
+ },
+
+ setPositionFromBone: function () {
+
+ var helper = this.helper;
+ var form = this.getBoneTransform();
+
+ var tr = helper.allocTransform();
+ this.body.getMotionState().getWorldTransform( tr );
+ helper.copyOrigin( tr, form );
+
+ // TODO: check the most appropriate way to set
+ //this.body.setWorldTransform( tr );
+ this.body.setCenterOfMassTransform( tr );
+ this.body.getMotionState().setWorldTransform( tr );
+
+ helper.freeTransform( tr );
+ helper.freeTransform( form );
+
+ },
+
+ updateBoneRotation: function () {
+
+ this.bone.updateMatrixWorld( true );
+
+ var helper = this.helper;
+
+ var tr = this.getWorldTransformForBone();
+ var q = helper.getBasis( tr );
+
+ var thQ = helper.allocThreeQuaternion();
+ var thQ2 = helper.allocThreeQuaternion();
+ var thQ3 = helper.allocThreeQuaternion();
+
+ thQ.set( q.x(), q.y(), q.z(), q.w() );
+ thQ2.setFromRotationMatrix( this.bone.matrixWorld );
+ thQ2.conjugate();
+ thQ2.multiply( thQ );
+
+ //this.bone.quaternion.multiply( thQ2 );
+
+ thQ3.setFromRotationMatrix( this.bone.matrix );
+ this.bone.quaternion.copy( thQ2.multiply( thQ3 ) );
+
+ helper.freeThreeQuaternion( thQ );
+ helper.freeThreeQuaternion( thQ2 );
+ helper.freeThreeQuaternion( thQ3 );
+
+ helper.freeQuaternion( q );
+ helper.freeTransform( tr );
+
+ },
+
+ updateBonePosition: function () {
+
+ var helper = this.helper;
+
+ var tr = this.getWorldTransformForBone();
+
+ var thV = helper.allocThreeVector3();
+
+ var o = helper.getOrigin( tr );
+ thV.set( o.x(), o.y(), o.z() );
+
+ var v = this.bone.worldToLocal( thV );
+ this.bone.position.add( v );
+
+ helper.freeThreeVector3( thV );
+
+ helper.freeTransform( tr );
+
+ }
+
+};
+
+THREE.MMDPhysics.Constraint = function ( mesh, world, bodyA, bodyB, params, helper ) {
+
+ this.mesh = mesh;
+ this.world = world;
+ this.bodyA = bodyA;
+ this.bodyB = bodyB;
+ this.params = params;
+ this.helper = helper;
+
+ this.constraint = null;
+
+ this.init();
+
+};
+
+THREE.MMDPhysics.Constraint.prototype = {
+
+ constructor: THREE.MMDPhysics.Constraint,
+
+ init: function () {
+
+ var helper = this.helper;
+ var params = this.params;
+ var bodyA = this.bodyA;
+ var bodyB = this.bodyB;
+
+ var form = helper.allocTransform();
+ helper.setIdentity( form );
+ helper.setOriginFromArray3( form, params.position );
+ helper.setBasisFromArray3( form, params.rotation );
+
+ var formA = helper.allocTransform();
+ var formB = helper.allocTransform();
+
+ bodyA.body.getMotionState().getWorldTransform( formA );
+ bodyB.body.getMotionState().getWorldTransform( formB );
+
+ var formInverseA = helper.inverseTransform( formA );
+ var formInverseB = helper.inverseTransform( formB );
+
+ var formA2 = helper.multiplyTransforms( formInverseA, form );
+ var formB2 = helper.multiplyTransforms( formInverseB, form );
+
+ var constraint = new Ammo.btGeneric6DofSpringConstraint( bodyA.body, bodyB.body, formA2, formB2, true );
+
+ var lll = helper.allocVector3();
+ var lul = helper.allocVector3();
+ var all = helper.allocVector3();
+ var aul = helper.allocVector3();
+
+ lll.setValue( params.translationLimitation1[ 0 ],
+ params.translationLimitation1[ 1 ],
+ params.translationLimitation1[ 2 ] );
+ lul.setValue( params.translationLimitation2[ 0 ],
+ params.translationLimitation2[ 1 ],
+ params.translationLimitation2[ 2 ] );
+ all.setValue( params.rotationLimitation1[ 0 ],
+ params.rotationLimitation1[ 1 ],
+ params.rotationLimitation1[ 2 ] );
+ aul.setValue( params.rotationLimitation2[ 0 ],
+ params.rotationLimitation2[ 1 ],
+ params.rotationLimitation2[ 2 ] );
+
+ constraint.setLinearLowerLimit( lll );
+ constraint.setLinearUpperLimit( lul );
+ constraint.setAngularLowerLimit( all );
+ constraint.setAngularUpperLimit( aul );
+
+ for ( var i = 0; i < 3; i++ ) {
+
+ if( params.springPosition[ i ] !== 0 ) {
+
+ constraint.enableSpring( i, true );
+ constraint.setStiffness( i, params.springPosition[ i ] );
+
+ }
+
+ }
+
+ for ( var i = 0; i < 3; i++ ) {
+
+ if( params.springRotation[ i ] !== 0 ) {
+
+ constraint.enableSpring( i + 3, true );
+ constraint.setStiffness( i + 3, params.springRotation[ i ] );
+
+ }
+
+ }
+
+ /*
+ * Currently(10/31/2016) official ammo.js doesn't support
+ * btGeneric6DofSpringConstraint.setParam method.
+ * You need custom ammo.js (add the method into idl) if you wanna use.
+ * By setting this parameter, physics will be more like MMD's
+ */
+ if ( constraint.setParam !== undefined ) {
+
+ for ( var i = 0; i < 6; i ++ ) {
+
+ // this parameter is from http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js
+ constraint.setParam( 2, 0.475, i );
+
+ }
+
+ }
+
+ this.world.addConstraint( constraint, true );
+ this.constraint = constraint;
+
+ helper.freeTransform( form );
+ helper.freeTransform( formA );
+ helper.freeTransform( formB );
+ helper.freeTransform( formInverseA );
+ helper.freeTransform( formInverseB );
+ helper.freeTransform( formA2 );
+ helper.freeTransform( formB2 );
+ helper.freeVector3( lll );
+ helper.freeVector3( lul );
+ helper.freeVector3( all );
+ helper.freeVector3( aul );
+
+ }
+
+};
+
+
+THREE.MMDPhysicsHelper = function ( mesh ) {
+
+ if ( mesh.physics === undefined || mesh.geometry.rigidBodies === undefined ) {
+
+ throw 'THREE.MMDPhysicsHelper requires physics in mesh and rigidBodies in mesh.geometry.';
+
+ }
+
+ THREE.Object3D.call( this );
+
+ this.root = mesh;
+
+ this.matrix = mesh.matrixWorld;
+ this.matrixAutoUpdate = false;
+
+ this.materials = [];
+
+ this.materials.push(
+ new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0xff8888 ),
+ wireframe: true,
+ depthTest: false,
+ depthWrite: false,
+ opacity: 0.25,
+ transparent: true
+ } )
+ );
+
+ this.materials.push(
+ new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0x88ff88 ),
+ wireframe: true,
+ depthTest: false,
+ depthWrite: false,
+ opacity: 0.25,
+ transparent: true
+ } )
+ );
+
+ this.materials.push(
+ new THREE.MeshBasicMaterial( {
+ color: new THREE.Color( 0x8888ff ),
+ wireframe: true,
+ depthTest: false,
+ depthWrite: false,
+ opacity: 0.25,
+ transparent: true
+ } )
+ );
+
+ this._init();
+ this.update();
+
+};
+
+THREE.MMDPhysicsHelper.prototype = Object.create( THREE.Object3D.prototype );
+THREE.MMDPhysicsHelper.prototype.constructor = THREE.MMDPhysicsHelper;
+
+THREE.MMDPhysicsHelper.prototype._init = function () {
+
+ var mesh = this.root;
+ var rigidBodies = mesh.geometry.rigidBodies;
+
+ function createGeometry( param ) {
+
+ switch ( param.shapeType ) {
+
+ case 0:
+ return new THREE.SphereBufferGeometry( param.width, 16, 8 );
+
+ case 1:
+ return new THREE.BoxBufferGeometry( param.width * 2, param.height * 2, param.depth * 2, 8, 8, 8 );
+
+ case 2:
+ return new createCapsuleGeometry( param.width, param.height, 16, 8 );
+
+ default:
+ return null;
+
+ }
+
+ }
+
+ // copy from http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mytest37.js?ver=20160815
+ function createCapsuleGeometry( radius, cylinderHeight, segmentsRadius, segmentsHeight ) {
+
+ var geometry = new THREE.CylinderBufferGeometry( radius, radius, cylinderHeight, segmentsRadius, segmentsHeight, true );
+ var upperSphere = new THREE.Mesh( new THREE.SphereBufferGeometry( radius, segmentsRadius, segmentsHeight, 0, Math.PI * 2, 0, Math.PI / 2 ) );
+ var lowerSphere = new THREE.Mesh( new THREE.SphereBufferGeometry( radius, segmentsRadius, segmentsHeight, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2 ) );
+
+ upperSphere.position.set( 0, cylinderHeight / 2, 0 );
+ lowerSphere.position.set( 0, -cylinderHeight / 2, 0 );
+
+ upperSphere.updateMatrix();
+ lowerSphere.updateMatrix();
+
+ geometry.merge( upperSphere.geometry, upperSphere.matrix );
+ geometry.merge( lowerSphere.geometry, lowerSphere.matrix );
+
+ return geometry;
+
+ }
+
+ for ( var i = 0, il = rigidBodies.length; i < il; i ++ ) {
+
+ var param = rigidBodies[ i ];
+ this.add( new THREE.Mesh( createGeometry( param ), this.materials[ param.type ] ) );
+
+ }
+
+};
+
+THREE.MMDPhysicsHelper.prototype.update = function () {
+
+ var mesh = this.root;
+ var rigidBodies = mesh.geometry.rigidBodies;
+ var bodies = mesh.physics.bodies;
+
+ var matrixWorldInv = new THREE.Matrix4().getInverse( mesh.matrixWorld );
+ var vector = new THREE.Vector3();
+ var quaternion = new THREE.Quaternion();
+ var quaternion2 = new THREE.Quaternion();
+
+ function getPosition( origin ) {
+
+ vector.set( origin.x(), origin.y(), origin.z() );
+ vector.applyMatrix4( matrixWorldInv );
+
+ return vector;
+
+ }
+
+ function getQuaternion( rotation ) {
+
+ quaternion.set( rotation.x(), rotation.y(), rotation.z(), rotation.w() );
+ quaternion2.setFromRotationMatrix( matrixWorldInv );
+ quaternion2.multiply( quaternion );
+
+ return quaternion2;
+
+ }
+
+ for ( var i = 0, il = rigidBodies.length; i < il; i ++ ) {
+
+ var body = bodies[ i ].body;
+ var mesh = this.children[ i ];
+
+ var tr = body.getCenterOfMassTransform();
+
+ mesh.position.copy( getPosition( tr.getOrigin() ) );
+ mesh.quaternion.copy( getQuaternion( tr.getRotation() ) );
+
+ }
+
+};
diff --git a/app/static/js/cameras/CinematicCamera.js b/app/static/js/cameras/CinematicCamera.js
new file mode 100644
index 0000000..fd11c5d
--- /dev/null
+++ b/app/static/js/cameras/CinematicCamera.js
@@ -0,0 +1,188 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author greggman / http://games.greggman.com/
+ * @author zz85 / http://www.lab4games.net/zz85/blog
+ * @author kaypiKun
+ */
+
+THREE.CinematicCamera = function( fov, aspect, near, far ) {
+
+ THREE.PerspectiveCamera.call( this, fov, aspect, near, far );
+
+ this.type = "CinematicCamera";
+
+ this.postprocessing = { enabled : true };
+ this.shaderSettings = {
+ rings: 3,
+ samples: 4
+ };
+
+ this.material_depth = new THREE.MeshDepthMaterial();
+
+ // In case of cinematicCamera, having a default lens set is important
+ this.setLens();
+
+ this.initPostProcessing();
+
+};
+
+THREE.CinematicCamera.prototype = Object.create( THREE.PerspectiveCamera.prototype );
+THREE.CinematicCamera.prototype.constructor = THREE.CinematicCamera;
+
+
+// providing fnumber and coc(Circle of Confusion) as extra arguments
+THREE.CinematicCamera.prototype.setLens = function ( focalLength, filmGauge, fNumber, coc ) {
+
+ // In case of cinematicCamera, having a default lens set is important
+ if ( focalLength === undefined ) focalLength = 35;
+ if ( filmGauge !== undefined ) this.filmGauge = filmGauge;
+
+ this.setFocalLength( focalLength );
+
+ // if fnumber and coc are not provided, cinematicCamera tries to act as a basic PerspectiveCamera
+ if ( fNumber === undefined ) fNumber = 8;
+ if ( coc === undefined ) coc = 0.019;
+
+ this.fNumber = fNumber;
+ this.coc = coc;
+
+ // fNumber is focalLength by aperture
+ this.aperture = focalLength / this.fNumber;
+
+ // hyperFocal is required to calculate depthOfField when a lens tries to focus at a distance with given fNumber and focalLength
+ this.hyperFocal = ( focalLength * focalLength ) / ( this.aperture * this.coc );
+
+};
+
+THREE.CinematicCamera.prototype.linearize = function ( depth ) {
+
+ var zfar = this.far;
+ var znear = this.near;
+ return - zfar * znear / ( depth * ( zfar - znear ) - zfar );
+
+};
+
+THREE.CinematicCamera.prototype.smoothstep = function ( near, far, depth ) {
+
+ var x = this.saturate( ( depth - near ) / ( far - near ) );
+ return x * x * ( 3 - 2 * x );
+
+};
+
+THREE.CinematicCamera.prototype.saturate = function ( x ) {
+
+ return Math.max( 0, Math.min( 1, x ) );
+
+};
+
+// function for focusing at a distance from the camera
+THREE.CinematicCamera.prototype.focusAt = function ( focusDistance ) {
+
+ if ( focusDistance === undefined ) focusDistance = 20;
+
+ var focalLength = this.getFocalLength();
+
+ // distance from the camera (normal to frustrum) to focus on
+ this.focus = focusDistance;
+
+ // the nearest point from the camera which is in focus (unused)
+ this.nearPoint = ( this.hyperFocal * this.focus ) / ( this.hyperFocal + ( this.focus - focalLength ) );
+
+ // the farthest point from the camera which is in focus (unused)
+ this.farPoint = ( this.hyperFocal * this.focus ) / ( this.hyperFocal - ( this.focus - focalLength ) );
+
+ // the gap or width of the space in which is everything is in focus (unused)
+ this.depthOfField = this.farPoint - this.nearPoint;
+
+ // Considering minimum distance of focus for a standard lens (unused)
+ if ( this.depthOfField < 0 ) this.depthOfField = 0;
+
+ this.sdistance = this.smoothstep( this.near, this.far, this.focus );
+
+ this.ldistance = this.linearize( 1 - this.sdistance );
+
+ this.postprocessing.bokeh_uniforms[ 'focalDepth' ].value = this.ldistance;
+
+};
+
+THREE.CinematicCamera.prototype.initPostProcessing = function () {
+
+ if ( this.postprocessing.enabled ) {
+
+ this.postprocessing.scene = new THREE.Scene();
+
+ this.postprocessing.camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, - 10000, 10000 );
+
+ this.postprocessing.scene.add( this.postprocessing.camera );
+
+ var pars = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat };
+ this.postprocessing.rtTextureDepth = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight, pars );
+ this.postprocessing.rtTextureColor = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight, pars );
+
+ var bokeh_shader = THREE.BokehShader;
+
+ this.postprocessing.bokeh_uniforms = THREE.UniformsUtils.clone( bokeh_shader.uniforms );
+
+ this.postprocessing.bokeh_uniforms[ "tColor" ].value = this.postprocessing.rtTextureColor.texture;
+ this.postprocessing.bokeh_uniforms[ "tDepth" ].value = this.postprocessing.rtTextureDepth.texture;
+
+ this.postprocessing.bokeh_uniforms[ "manualdof" ].value = 0;
+ this.postprocessing.bokeh_uniforms[ "shaderFocus" ].value = 0;
+
+ this.postprocessing.bokeh_uniforms[ "fstop" ].value = 2.8;
+
+ this.postprocessing.bokeh_uniforms[ "showFocus" ].value = 1;
+
+ this.postprocessing.bokeh_uniforms[ "focalDepth" ].value = 0.1;
+
+ //console.log( this.postprocessing.bokeh_uniforms[ "focalDepth" ].value );
+
+ this.postprocessing.bokeh_uniforms[ "znear" ].value = this.near;
+ this.postprocessing.bokeh_uniforms[ "zfar" ].value = this.near;
+
+
+ this.postprocessing.bokeh_uniforms[ "textureWidth" ].value = window.innerWidth;
+
+ this.postprocessing.bokeh_uniforms[ "textureHeight" ].value = window.innerHeight;
+
+ this.postprocessing.materialBokeh = new THREE.ShaderMaterial( {
+ uniforms: this.postprocessing.bokeh_uniforms,
+ vertexShader: bokeh_shader.vertexShader,
+ fragmentShader: bokeh_shader.fragmentShader,
+ defines: {
+ RINGS: this.shaderSettings.rings,
+ SAMPLES: this.shaderSettings.samples
+ }
+ } );
+
+ this.postprocessing.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( window.innerWidth, window.innerHeight ), this.postprocessing.materialBokeh );
+ this.postprocessing.quad.position.z = - 500;
+ this.postprocessing.scene.add( this.postprocessing.quad );
+
+ }
+
+};
+
+THREE.CinematicCamera.prototype.renderCinematic = function ( scene, renderer ) {
+
+ if ( this.postprocessing.enabled ) {
+
+ renderer.clear();
+
+ // Render scene into texture
+
+ scene.overrideMaterial = null;
+ renderer.render( scene, camera, this.postprocessing.rtTextureColor, true );
+
+ // Render depth into texture
+
+ scene.overrideMaterial = this.material_depth;
+ renderer.render( scene, camera, this.postprocessing.rtTextureDepth, true );
+
+ // Render bokeh composite
+
+ renderer.render( this.postprocessing.scene, this.postprocessing.camera );
+
+ }
+
+};
diff --git a/app/static/js/commands/AddObjectCommand.js b/app/static/js/commands/AddObjectCommand.js
new file mode 100644
index 0000000..62c2e87
--- /dev/null
+++ b/app/static/js/commands/AddObjectCommand.js
@@ -0,0 +1,66 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @constructor
+ */
+
+var AddObjectCommand = function ( object ) {
+
+ Command.call( this );
+
+ this.type = 'AddObjectCommand';
+
+ this.object = object;
+ if ( object !== undefined ) {
+
+ this.name = 'Add Object: ' + object.name;
+
+ }
+
+};
+
+AddObjectCommand.prototype = {
+
+ execute: function () {
+
+ this.editor.addObject( this.object );
+ this.editor.select( this.object );
+
+ },
+
+ undo: function () {
+
+ this.editor.removeObject( this.object );
+ this.editor.deselect();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+ output.object = this.object.toJSON();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.object.object.uuid );
+
+ if ( this.object === undefined ) {
+
+ var loader = new THREE.ObjectLoader();
+ this.object = loader.parse( json.object );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/AddScriptCommand.js b/app/static/js/commands/AddScriptCommand.js
new file mode 100644
index 0000000..75dd540
--- /dev/null
+++ b/app/static/js/commands/AddScriptCommand.js
@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @constructor
+ */
+
+var AddScriptCommand = function ( object, script ) {
+
+ Command.call( this );
+
+ this.type = 'AddScriptCommand';
+ this.name = 'Add Script';
+
+ this.object = object;
+ this.script = script;
+
+};
+
+AddScriptCommand.prototype = {
+
+ execute: function () {
+
+ if ( this.editor.scripts[ this.object.uuid ] === undefined ) {
+
+ this.editor.scripts[ this.object.uuid ] = [];
+
+ }
+
+ this.editor.scripts[ this.object.uuid ].push( this.script );
+
+ this.editor.signals.scriptAdded.dispatch( this.script );
+
+ },
+
+ undo: function () {
+
+ if ( this.editor.scripts[ this.object.uuid ] === undefined ) return;
+
+ var index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+
+ if ( index !== - 1 ) {
+
+ this.editor.scripts[ this.object.uuid ].splice( index, 1 );
+
+ }
+
+ this.editor.signals.scriptRemoved.dispatch( this.script );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.script = this.script;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.script = json.script;
+ this.object = this.editor.objectByUuid( json.objectUuid );
+
+ }
+
+};
diff --git a/app/static/js/commands/MoveObjectCommand.js b/app/static/js/commands/MoveObjectCommand.js
new file mode 100644
index 0000000..13baa13
--- /dev/null
+++ b/app/static/js/commands/MoveObjectCommand.js
@@ -0,0 +1,107 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newParent THREE.Object3D
+ * @param newBefore THREE.Object3D
+ * @constructor
+ */
+
+var MoveObjectCommand = function ( object, newParent, newBefore ) {
+
+ Command.call( this );
+
+ this.type = 'MoveObjectCommand';
+ this.name = 'Move Object';
+
+ this.object = object;
+ this.oldParent = ( object !== undefined ) ? object.parent : undefined;
+ this.oldIndex = ( this.oldParent !== undefined ) ? this.oldParent.children.indexOf( this.object ) : undefined;
+ this.newParent = newParent;
+
+ if ( newBefore !== undefined ) {
+
+ this.newIndex = ( newParent !== undefined ) ? newParent.children.indexOf( newBefore ) : undefined;
+
+ } else {
+
+ this.newIndex = ( newParent !== undefined ) ? newParent.children.length : undefined;
+
+ }
+
+ if ( this.oldParent === this.newParent && this.newIndex > this.oldIndex ) {
+
+ this.newIndex --;
+
+ }
+
+ this.newBefore = newBefore;
+
+};
+
+MoveObjectCommand.prototype = {
+
+ execute: function () {
+
+ this.oldParent.remove( this.object );
+
+ var children = this.newParent.children;
+ children.splice( this.newIndex, 0, this.object );
+ this.object.parent = this.newParent;
+
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.newParent.remove( this.object );
+
+ var children = this.oldParent.children;
+ children.splice( this.oldIndex, 0, this.object );
+ this.object.parent = this.oldParent;
+
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.newParentUuid = this.newParent.uuid;
+ output.oldParentUuid = this.oldParent.uuid;
+ output.newIndex = this.newIndex;
+ output.oldIndex = this.oldIndex;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.oldParent = this.editor.objectByUuid( json.oldParentUuid );
+ if ( this.oldParent === undefined ) {
+
+ this.oldParent = this.editor.scene;
+
+ }
+ this.newParent = this.editor.objectByUuid( json.newParentUuid );
+ if ( this.newParent === undefined ) {
+
+ this.newParent = this.editor.scene;
+
+ }
+ this.newIndex = json.newIndex;
+ this.oldIndex = json.oldIndex;
+
+ }
+
+};
diff --git a/app/static/js/commands/MultiCmdsCommand.js b/app/static/js/commands/MultiCmdsCommand.js
new file mode 100644
index 0000000..91bc7b4
--- /dev/null
+++ b/app/static/js/commands/MultiCmdsCommand.js
@@ -0,0 +1,85 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param cmdArray array containing command objects
+ * @constructor
+ */
+
+var MultiCmdsCommand = function ( cmdArray ) {
+
+ Command.call( this );
+
+ this.type = 'MultiCmdsCommand';
+ this.name = 'Multiple Changes';
+
+ this.cmdArray = ( cmdArray !== undefined ) ? cmdArray : [];
+
+};
+
+MultiCmdsCommand.prototype = {
+
+ execute: function () {
+
+ this.editor.signals.sceneGraphChanged.active = false;
+
+ for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+ this.cmdArray[ i ].execute();
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.editor.signals.sceneGraphChanged.active = false;
+
+ for ( var i = this.cmdArray.length - 1; i >= 0; i -- ) {
+
+ this.cmdArray[ i ].undo();
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ var cmds = [];
+ for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+ cmds.push( this.cmdArray[ i ].toJSON() );
+
+ }
+ output.cmds = cmds;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ var cmds = json.cmds;
+ for ( var i = 0; i < cmds.length; i ++ ) {
+
+ var cmd = new window[ cmds[ i ].type ](); // creates a new object of type "json.type"
+ cmd.fromJSON( cmds[ i ] );
+ this.cmdArray.push( cmd );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/RemoveObjectCommand.js b/app/static/js/commands/RemoveObjectCommand.js
new file mode 100644
index 0000000..4bcca56
--- /dev/null
+++ b/app/static/js/commands/RemoveObjectCommand.js
@@ -0,0 +1,103 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @constructor
+ */
+
+var RemoveObjectCommand = function ( object ) {
+
+ Command.call( this );
+
+ this.type = 'RemoveObjectCommand';
+ this.name = 'Remove Object';
+
+ this.object = object;
+ this.parent = ( object !== undefined ) ? object.parent : undefined;
+ if ( this.parent !== undefined ) {
+
+ this.index = this.parent.children.indexOf( this.object );
+
+ }
+
+};
+
+RemoveObjectCommand.prototype = {
+
+ execute: function () {
+
+ var scope = this.editor;
+ this.object.traverse( function ( child ) {
+
+ scope.removeHelper( child );
+
+ } );
+
+ this.parent.remove( this.object );
+ this.editor.select( this.parent );
+
+ this.editor.signals.objectRemoved.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ var scope = this.editor;
+
+ this.object.traverse( function ( child ) {
+
+ if ( child.geometry !== undefined ) scope.addGeometry( child.geometry );
+ if ( child.material !== undefined ) scope.addMaterial( child.material );
+
+ scope.addHelper( child );
+
+ } );
+
+ this.parent.children.splice( this.index, 0, this.object );
+ this.object.parent = this.parent;
+ this.editor.select( this.object );
+
+ this.editor.signals.objectAdded.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+ output.object = this.object.toJSON();
+ output.index = this.index;
+ output.parentUuid = this.parent.uuid;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.parent = this.editor.objectByUuid( json.parentUuid );
+ if ( this.parent === undefined ) {
+
+ this.parent = this.editor.scene;
+
+ }
+
+ this.index = json.index;
+
+ this.object = this.editor.objectByUuid( json.object.object.uuid );
+ if ( this.object === undefined ) {
+
+ var loader = new THREE.ObjectLoader();
+ this.object = loader.parse( json.object );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/RemoveScriptCommand.js b/app/static/js/commands/RemoveScriptCommand.js
new file mode 100644
index 0000000..1647185
--- /dev/null
+++ b/app/static/js/commands/RemoveScriptCommand.js
@@ -0,0 +1,81 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @constructor
+ */
+
+var RemoveScriptCommand = function ( object, script ) {
+
+ Command.call( this );
+
+ this.type = 'RemoveScriptCommand';
+ this.name = 'Remove Script';
+
+ this.object = object;
+ this.script = script;
+ if ( this.object && this.script ) {
+
+ this.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+
+ }
+
+};
+
+RemoveScriptCommand.prototype = {
+
+ execute: function () {
+
+ if ( this.editor.scripts[ this.object.uuid ] === undefined ) return;
+
+ if ( this.index !== - 1 ) {
+
+ this.editor.scripts[ this.object.uuid ].splice( this.index, 1 );
+
+ }
+
+ this.editor.signals.scriptRemoved.dispatch( this.script );
+
+ },
+
+ undo: function () {
+
+ if ( this.editor.scripts[ this.object.uuid ] === undefined ) {
+
+ this.editor.scripts[ this.object.uuid ] = [];
+
+ }
+
+ this.editor.scripts[ this.object.uuid ].splice( this.index, 0, this.script );
+
+ this.editor.signals.scriptAdded.dispatch( this.script );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.script = this.script;
+ output.index = this.index;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.script = json.script;
+ this.index = json.index;
+ this.object = this.editor.objectByUuid( json.objectUuid );
+
+ }
+
+};
diff --git a/app/static/js/commands/SetColorCommand.js b/app/static/js/commands/SetColorCommand.js
new file mode 100644
index 0000000..c9241bb
--- /dev/null
+++ b/app/static/js/commands/SetColorCommand.js
@@ -0,0 +1,74 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue integer representing a hex color value
+ * @constructor
+ */
+
+var SetColorCommand = function ( object, attributeName, newValue ) {
+
+ Command.call( this );
+
+ this.type = 'SetColorCommand';
+ this.name = 'Set ' + attributeName;
+ this.updatable = true;
+
+ this.object = object;
+ this.attributeName = attributeName;
+ this.oldValue = ( object !== undefined ) ? this.object[ this.attributeName ].getHex() : undefined;
+ this.newValue = newValue;
+
+};
+
+SetColorCommand.prototype = {
+
+ execute: function () {
+
+ this.object[ this.attributeName ].setHex( this.newValue );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ undo: function () {
+
+ this.object[ this.attributeName ].setHex( this.oldValue );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newValue = cmd.newValue;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.attributeName = json.attributeName;
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+
+ }
+
+};
diff --git a/app/static/js/commands/SetGeometryCommand.js b/app/static/js/commands/SetGeometryCommand.js
new file mode 100644
index 0000000..51f1bff
--- /dev/null
+++ b/app/static/js/commands/SetGeometryCommand.js
@@ -0,0 +1,86 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newGeometry THREE.Geometry
+ * @constructor
+ */
+
+var SetGeometryCommand = function ( object, newGeometry ) {
+
+ Command.call( this );
+
+ this.type = 'SetGeometryCommand';
+ this.name = 'Set Geometry';
+ this.updatable = true;
+
+ this.object = object;
+ this.oldGeometry = ( object !== undefined ) ? object.geometry : undefined;
+ this.newGeometry = newGeometry;
+
+};
+
+SetGeometryCommand.prototype = {
+
+ execute: function () {
+
+ this.object.geometry.dispose();
+ this.object.geometry = this.newGeometry;
+ this.object.geometry.computeBoundingSphere();
+
+ this.editor.signals.geometryChanged.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.object.geometry.dispose();
+ this.object.geometry = this.oldGeometry;
+ this.object.geometry.computeBoundingSphere();
+
+ this.editor.signals.geometryChanged.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newGeometry = cmd.newGeometry;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.oldGeometry = this.object.geometry.toJSON();
+ output.newGeometry = this.newGeometry.toJSON();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+
+ this.oldGeometry = parseGeometry( json.oldGeometry );
+ this.newGeometry = parseGeometry( json.newGeometry );
+
+ function parseGeometry ( data ) {
+
+ var loader = new THREE.ObjectLoader();
+ return loader.parseGeometries( [ data ] )[ data.uuid ];
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/SetGeometryValueCommand.js b/app/static/js/commands/SetGeometryValueCommand.js
new file mode 100644
index 0000000..ba305c5
--- /dev/null
+++ b/app/static/js/commands/SetGeometryValueCommand.js
@@ -0,0 +1,71 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+var SetGeometryValueCommand = function ( object, attributeName, newValue ) {
+
+ Command.call( this );
+
+ this.type = 'SetGeometryValueCommand';
+ this.name = 'Set Geometry.' + attributeName;
+
+ this.object = object;
+ this.attributeName = attributeName;
+ this.oldValue = ( object !== undefined ) ? object.geometry[ attributeName ] : undefined;
+ this.newValue = newValue;
+
+};
+
+SetGeometryValueCommand.prototype = {
+
+ execute: function () {
+
+ this.object.geometry[ this.attributeName ] = this.newValue;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ this.editor.signals.geometryChanged.dispatch();
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.object.geometry[ this.attributeName ] = this.oldValue;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ this.editor.signals.geometryChanged.dispatch();
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.attributeName = json.attributeName;
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+
+ }
+
+};
diff --git a/app/static/js/commands/SetMaterialColorCommand.js b/app/static/js/commands/SetMaterialColorCommand.js
new file mode 100644
index 0000000..f4b271b
--- /dev/null
+++ b/app/static/js/commands/SetMaterialColorCommand.js
@@ -0,0 +1,79 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue integer representing a hex color value
+ * @constructor
+ */
+
+var SetMaterialColorCommand = function ( object, attributeName, newValue, materialSlot ) {
+
+ Command.call( this );
+
+ this.type = 'SetMaterialColorCommand';
+ this.name = 'Set Material.' + attributeName;
+ this.updatable = true;
+
+ this.object = object;
+ this.material = this.editor.getObjectMaterial( object, materialSlot );
+
+ this.oldValue = ( this.material !== undefined ) ? this.material[ attributeName ].getHex() : undefined;
+ this.newValue = newValue;
+
+ this.attributeName = attributeName;
+
+};
+
+SetMaterialColorCommand.prototype = {
+
+ execute: function () {
+
+ this.material[ this.attributeName ].setHex( this.newValue );
+
+ this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ undo: function () {
+
+ this.material[ this.attributeName ].setHex( this.oldValue );
+
+ this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newValue = cmd.newValue;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.attributeName = json.attributeName;
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+
+ }
+
+};
diff --git a/app/static/js/commands/SetMaterialCommand.js b/app/static/js/commands/SetMaterialCommand.js
new file mode 100644
index 0000000..949db26
--- /dev/null
+++ b/app/static/js/commands/SetMaterialCommand.js
@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newMaterial THREE.Material
+ * @constructor
+ */
+
+
+var SetMaterialCommand = function ( object, newMaterial, materialSlot ) {
+
+ Command.call( this );
+
+ this.type = 'SetMaterialCommand';
+ this.name = 'New Material';
+
+ this.object = object;
+ this.materialSlot = materialSlot;
+
+ this.oldMaterial = this.editor.getObjectMaterial( object, materialSlot );
+ this.newMaterial = newMaterial;
+
+};
+
+SetMaterialCommand.prototype = {
+
+ execute: function () {
+
+ this.editor.setObjectMaterial( this.object, this.materialSlot, this.newMaterial );
+ this.editor.signals.materialChanged.dispatch( this.newMaterial );
+
+ },
+
+ undo: function () {
+
+ this.editor.setObjectMaterial( this.object, this.materialSlot, this.oldMaterial );
+ this.editor.signals.materialChanged.dispatch( this.oldMaterial );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.oldMaterial = this.oldMaterial.toJSON();
+ output.newMaterial = this.newMaterial.toJSON();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.oldMaterial = parseMaterial( json.oldMaterial );
+ this.newMaterial = parseMaterial( json.newMaterial );
+
+ function parseMaterial ( json ) {
+
+ var loader = new THREE.ObjectLoader();
+ var images = loader.parseImages( json.images );
+ var textures = loader.parseTextures( json.textures, images );
+ var materials = loader.parseMaterials( [ json ], textures );
+ return materials[ json.uuid ];
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/SetMaterialMapCommand.js b/app/static/js/commands/SetMaterialMapCommand.js
new file mode 100644
index 0000000..8aff3aa
--- /dev/null
+++ b/app/static/js/commands/SetMaterialMapCommand.js
@@ -0,0 +1,131 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param mapName string
+ * @param newMap THREE.Texture
+ * @constructor
+ */
+
+var SetMaterialMapCommand = function ( object, mapName, newMap, materialSlot ) {
+
+ Command.call( this );
+
+ this.type = 'SetMaterialMapCommand';
+ this.name = 'Set Material.' + mapName;
+
+ this.object = object;
+ this.material = this.editor.getObjectMaterial( object, materialSlot );
+
+ this.oldMap = ( object !== undefined ) ? this.material[ mapName ] : undefined;
+ this.newMap = newMap;
+
+ this.mapName = mapName;
+
+};
+
+SetMaterialMapCommand.prototype = {
+
+ execute: function () {
+
+ this.material[ this.mapName ] = this.newMap;
+ this.material.needsUpdate = true;
+
+ this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ undo: function () {
+
+ this.material[ this.mapName ] = this.oldMap;
+ this.material.needsUpdate = true;
+
+ this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.mapName = this.mapName;
+ output.newMap = serializeMap( this.newMap );
+ output.oldMap = serializeMap( this.oldMap );
+
+ return output;
+
+ // serializes a map (THREE.Texture)
+
+ function serializeMap ( map ) {
+
+ if ( map === null || map === undefined ) return null;
+
+ var meta = {
+ geometries: {},
+ materials: {},
+ textures: {},
+ images: {}
+ };
+
+ var json = map.toJSON( meta );
+ var images = extractFromCache( meta.images );
+ if ( images.length > 0 ) json.images = images;
+ json.sourceFile = map.sourceFile;
+
+ return json;
+
+ }
+
+ // Note: The function 'extractFromCache' is copied from Object3D.toJSON()
+
+ // extract data from the cache hash
+ // remove metadata on each item
+ // and return as array
+ function extractFromCache ( cache ) {
+
+ var values = [];
+ for ( var key in cache ) {
+
+ var data = cache[ key ];
+ delete data.metadata;
+ values.push( data );
+
+ }
+ return values;
+
+ }
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.mapName = json.mapName;
+ this.oldMap = parseTexture( json.oldMap );
+ this.newMap = parseTexture( json.newMap );
+
+ function parseTexture ( json ) {
+
+ var map = null;
+ if ( json !== null ) {
+
+ var loader = new THREE.ObjectLoader();
+ var images = loader.parseImages( json.images );
+ var textures = loader.parseTextures( [ json ], images );
+ map = textures[ json.uuid ];
+ map.sourceFile = json.sourceFile;
+
+ }
+ return map;
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/SetMaterialValueCommand.js b/app/static/js/commands/SetMaterialValueCommand.js
new file mode 100644
index 0000000..9153ef0
--- /dev/null
+++ b/app/static/js/commands/SetMaterialValueCommand.js
@@ -0,0 +1,83 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+var SetMaterialValueCommand = function ( object, attributeName, newValue, materialSlot ) {
+
+ Command.call( this );
+
+ this.type = 'SetMaterialValueCommand';
+ this.name = 'Set Material.' + attributeName;
+ this.updatable = true;
+
+ this.object = object;
+ this.material = this.editor.getObjectMaterial( object, materialSlot );
+
+ this.oldValue = ( this.material !== undefined ) ? this.material[ attributeName ] : undefined;
+ this.newValue = newValue;
+
+ this.attributeName = attributeName;
+
+};
+
+SetMaterialValueCommand.prototype = {
+
+ execute: function () {
+
+ this.material[ this.attributeName ] = this.newValue;
+ this.material.needsUpdate = true;
+
+ this.editor.signals.objectChanged.dispatch( this.object );
+ //this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ undo: function () {
+
+ this.material[ this.attributeName ] = this.oldValue;
+ this.material.needsUpdate = true;
+
+ this.editor.signals.objectChanged.dispatch( this.object );
+ this.editor.signals.materialChanged.dispatch( this.material );
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newValue = cmd.newValue;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.attributeName = json.attributeName;
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+ this.object = this.editor.objectByUuid( json.objectUuid );
+
+ }
+
+};
diff --git a/app/static/js/commands/SetPositionCommand.js b/app/static/js/commands/SetPositionCommand.js
new file mode 100644
index 0000000..e0fa899
--- /dev/null
+++ b/app/static/js/commands/SetPositionCommand.js
@@ -0,0 +1,83 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newPosition THREE.Vector3
+ * @param optionalOldPosition THREE.Vector3
+ * @constructor
+ */
+
+var SetPositionCommand = function ( object, newPosition, optionalOldPosition ) {
+
+ Command.call( this );
+
+ this.type = 'SetPositionCommand';
+ this.name = 'Set Position';
+ this.updatable = true;
+
+ this.object = object;
+
+ if ( object !== undefined && newPosition !== undefined ) {
+
+ this.oldPosition = object.position.clone();
+ this.newPosition = newPosition.clone();
+
+ }
+
+ if ( optionalOldPosition !== undefined ) {
+
+ this.oldPosition = optionalOldPosition.clone();
+
+ }
+
+};
+SetPositionCommand.prototype = {
+
+ execute: function () {
+
+ this.object.position.copy( this.newPosition );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ undo: function () {
+
+ this.object.position.copy( this.oldPosition );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ update: function ( command ) {
+
+ this.newPosition.copy( command.newPosition );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.oldPosition = this.oldPosition.toArray();
+ output.newPosition = this.newPosition.toArray();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.oldPosition = new THREE.Vector3().fromArray( json.oldPosition );
+ this.newPosition = new THREE.Vector3().fromArray( json.newPosition );
+
+ }
+
+};
diff --git a/app/static/js/commands/SetRotationCommand.js b/app/static/js/commands/SetRotationCommand.js
new file mode 100644
index 0000000..7de055a
--- /dev/null
+++ b/app/static/js/commands/SetRotationCommand.js
@@ -0,0 +1,84 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newRotation THREE.Euler
+ * @param optionalOldRotation THREE.Euler
+ * @constructor
+ */
+
+var SetRotationCommand = function ( object, newRotation, optionalOldRotation ) {
+
+ Command.call( this );
+
+ this.type = 'SetRotationCommand';
+ this.name = 'Set Rotation';
+ this.updatable = true;
+
+ this.object = object;
+
+ if ( object !== undefined && newRotation !== undefined ) {
+
+ this.oldRotation = object.rotation.clone();
+ this.newRotation = newRotation.clone();
+
+ }
+
+ if ( optionalOldRotation !== undefined ) {
+
+ this.oldRotation = optionalOldRotation.clone();
+
+ }
+
+};
+
+SetRotationCommand.prototype = {
+
+ execute: function () {
+
+ this.object.rotation.copy( this.newRotation );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ undo: function () {
+
+ this.object.rotation.copy( this.oldRotation );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ update: function ( command ) {
+
+ this.newRotation.copy( command.newRotation );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.oldRotation = this.oldRotation.toArray();
+ output.newRotation = this.newRotation.toArray();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.oldRotation = new THREE.Euler().fromArray( json.oldRotation );
+ this.newRotation = new THREE.Euler().fromArray( json.newRotation );
+
+ }
+
+};
diff --git a/app/static/js/commands/SetScaleCommand.js b/app/static/js/commands/SetScaleCommand.js
new file mode 100644
index 0000000..51d7e53
--- /dev/null
+++ b/app/static/js/commands/SetScaleCommand.js
@@ -0,0 +1,84 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newScale THREE.Vector3
+ * @param optionalOldScale THREE.Vector3
+ * @constructor
+ */
+
+var SetScaleCommand = function ( object, newScale, optionalOldScale ) {
+
+ Command.call( this );
+
+ this.type = 'SetScaleCommand';
+ this.name = 'Set Scale';
+ this.updatable = true;
+
+ this.object = object;
+
+ if ( object !== undefined && newScale !== undefined ) {
+
+ this.oldScale = object.scale.clone();
+ this.newScale = newScale.clone();
+
+ }
+
+ if ( optionalOldScale !== undefined ) {
+
+ this.oldScale = optionalOldScale.clone();
+
+ }
+
+};
+
+SetScaleCommand.prototype = {
+
+ execute: function () {
+
+ this.object.scale.copy( this.newScale );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ undo: function () {
+
+ this.object.scale.copy( this.oldScale );
+ this.object.updateMatrixWorld( true );
+ this.editor.signals.objectChanged.dispatch( this.object );
+
+ },
+
+ update: function ( command ) {
+
+ this.newScale.copy( command.newScale );
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.oldScale = this.oldScale.toArray();
+ output.newScale = this.newScale.toArray();
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.oldScale = new THREE.Vector3().fromArray( json.oldScale );
+ this.newScale = new THREE.Vector3().fromArray( json.newScale );
+
+ }
+
+};
diff --git a/app/static/js/commands/SetSceneCommand.js b/app/static/js/commands/SetSceneCommand.js
new file mode 100644
index 0000000..4c2d38f
--- /dev/null
+++ b/app/static/js/commands/SetSceneCommand.js
@@ -0,0 +1,100 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param scene containing children to import
+ * @constructor
+ */
+
+var SetSceneCommand = function ( scene ) {
+
+ Command.call( this );
+
+ this.type = 'SetSceneCommand';
+ this.name = 'Set Scene';
+
+ this.cmdArray = [];
+
+ if ( scene !== undefined ) {
+
+ this.cmdArray.push( new SetUuidCommand( this.editor.scene, scene.uuid ) );
+ this.cmdArray.push( new SetValueCommand( this.editor.scene, 'name', scene.name ) );
+ this.cmdArray.push( new SetValueCommand( this.editor.scene, 'userData', JSON.parse( JSON.stringify( scene.userData ) ) ) );
+
+ while ( scene.children.length > 0 ) {
+
+ var child = scene.children.pop();
+ this.cmdArray.push( new AddObjectCommand( child ) );
+
+ }
+
+ }
+
+};
+
+SetSceneCommand.prototype = {
+
+ execute: function () {
+
+ this.editor.signals.sceneGraphChanged.active = false;
+
+ for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+ this.cmdArray[ i ].execute();
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.editor.signals.sceneGraphChanged.active = false;
+
+ for ( var i = this.cmdArray.length - 1; i >= 0; i -- ) {
+
+ this.cmdArray[ i ].undo();
+
+ }
+
+ this.editor.signals.sceneGraphChanged.active = true;
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ var cmds = [];
+ for ( var i = 0; i < this.cmdArray.length; i ++ ) {
+
+ cmds.push( this.cmdArray[ i ].toJSON() );
+
+ }
+ output.cmds = cmds;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ var cmds = json.cmds;
+ for ( var i = 0; i < cmds.length; i ++ ) {
+
+ var cmd = new window[ cmds[ i ].type ](); // creates a new object of type "json.type"
+ cmd.fromJSON( cmds[ i ] );
+ this.cmdArray.push( cmd );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/SetScriptValueCommand.js b/app/static/js/commands/SetScriptValueCommand.js
new file mode 100644
index 0000000..ad90143
--- /dev/null
+++ b/app/static/js/commands/SetScriptValueCommand.js
@@ -0,0 +1,81 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param script javascript object
+ * @param attributeName string
+ * @param newValue string, object
+ * @constructor
+ */
+
+var SetScriptValueCommand = function ( object, script, attributeName, newValue ) {
+
+ Command.call( this );
+
+ this.type = 'SetScriptValueCommand';
+ this.name = 'Set Script.' + attributeName;
+ this.updatable = true;
+
+ this.object = object;
+ this.script = script;
+
+ this.attributeName = attributeName;
+ this.oldValue = ( script !== undefined ) ? script[ this.attributeName ] : undefined;
+ this.newValue = newValue;
+
+};
+
+SetScriptValueCommand.prototype = {
+
+ execute: function () {
+
+ this.script[ this.attributeName ] = this.newValue;
+
+ this.editor.signals.scriptChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.script[ this.attributeName ] = this.oldValue;
+
+ this.editor.signals.scriptChanged.dispatch();
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newValue = cmd.newValue;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.index = this.editor.scripts[ this.object.uuid ].indexOf( this.script );
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+ this.attributeName = json.attributeName;
+ this.object = this.editor.objectByUuid( json.objectUuid );
+ this.script = this.editor.scripts[ json.objectUuid ][ json.index ];
+
+ }
+
+};
diff --git a/app/static/js/commands/SetUuidCommand.js b/app/static/js/commands/SetUuidCommand.js
new file mode 100644
index 0000000..51c9888
--- /dev/null
+++ b/app/static/js/commands/SetUuidCommand.js
@@ -0,0 +1,71 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param newUuid string
+ * @constructor
+ */
+
+var SetUuidCommand = function ( object, newUuid ) {
+
+ Command.call( this );
+
+ this.type = 'SetUuidCommand';
+ this.name = 'Update UUID';
+
+ this.object = object;
+
+ this.oldUuid = ( object !== undefined ) ? object.uuid : undefined;
+ this.newUuid = newUuid;
+
+};
+
+SetUuidCommand.prototype = {
+
+ execute: function () {
+
+ this.object.uuid = this.newUuid;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.object.uuid = this.oldUuid;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.oldUuid = this.oldUuid;
+ output.newUuid = this.newUuid;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.oldUuid = json.oldUuid;
+ this.newUuid = json.newUuid;
+ this.object = this.editor.objectByUuid( json.oldUuid );
+
+ if ( this.object === undefined ) {
+
+ this.object = this.editor.objectByUuid( json.newUuid );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/commands/SetValueCommand.js b/app/static/js/commands/SetValueCommand.js
new file mode 100644
index 0000000..c20016f
--- /dev/null
+++ b/app/static/js/commands/SetValueCommand.js
@@ -0,0 +1,76 @@
+/**
+ * @author dforrer / https://github.com/dforrer
+ * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
+ */
+
+/**
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newValue number, string, boolean or object
+ * @constructor
+ */
+
+var SetValueCommand = function ( object, attributeName, newValue ) {
+
+ Command.call( this );
+
+ this.type = 'SetValueCommand';
+ this.name = 'Set ' + attributeName;
+ this.updatable = true;
+
+ this.object = object;
+ this.attributeName = attributeName;
+ this.oldValue = ( object !== undefined ) ? object[ attributeName ] : undefined;
+ this.newValue = newValue;
+
+};
+
+SetValueCommand.prototype = {
+
+ execute: function () {
+
+ this.object[ this.attributeName ] = this.newValue;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ // this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ undo: function () {
+
+ this.object[ this.attributeName ] = this.oldValue;
+ this.editor.signals.objectChanged.dispatch( this.object );
+ // this.editor.signals.sceneGraphChanged.dispatch();
+
+ },
+
+ update: function ( cmd ) {
+
+ this.newValue = cmd.newValue;
+
+ },
+
+ toJSON: function () {
+
+ var output = Command.prototype.toJSON.call( this );
+
+ output.objectUuid = this.object.uuid;
+ output.attributeName = this.attributeName;
+ output.oldValue = this.oldValue;
+ output.newValue = this.newValue;
+
+ return output;
+
+ },
+
+ fromJSON: function ( json ) {
+
+ Command.prototype.fromJSON.call( this, json );
+
+ this.attributeName = json.attributeName;
+ this.oldValue = json.oldValue;
+ this.newValue = json.newValue;
+ this.object = this.editor.objectByUuid( json.objectUuid );
+
+ }
+
+};
diff --git a/app/static/js/controls/DeviceOrientationControls.js b/app/static/js/controls/DeviceOrientationControls.js
new file mode 100644
index 0000000..f411129
--- /dev/null
+++ b/app/static/js/controls/DeviceOrientationControls.js
@@ -0,0 +1,111 @@
+/**
+ * @author richt / http://richt.me
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html)
+ */
+
+THREE.DeviceOrientationControls = function( object ) {
+
+ var scope = this;
+
+ this.object = object;
+ this.object.rotation.reorder( "YXZ" );
+
+ this.enabled = true;
+
+ this.deviceOrientation = {};
+ this.screenOrientation = 0;
+
+ this.alpha = 0;
+ this.alphaOffsetAngle = 0;
+
+
+ var onDeviceOrientationChangeEvent = function( event ) {
+
+ scope.deviceOrientation = event;
+
+ };
+
+ var onScreenOrientationChangeEvent = function() {
+
+ scope.screenOrientation = window.orientation || 0;
+
+ };
+
+ // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''
+
+ var setObjectQuaternion = function() {
+
+ var zee = new THREE.Vector3( 0, 0, 1 );
+
+ var euler = new THREE.Euler();
+
+ var q0 = new THREE.Quaternion();
+
+ var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
+
+ return function( quaternion, alpha, beta, gamma, orient ) {
+
+ euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us
+
+ quaternion.setFromEuler( euler ); // orient the device
+
+ quaternion.multiply( q1 ); // camera looks out the back of the device, not the top
+
+ quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation
+
+ }
+
+ }();
+
+ this.connect = function() {
+
+ onScreenOrientationChangeEvent(); // run once on load
+
+ window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
+ window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
+
+ scope.enabled = true;
+
+ };
+
+ this.disconnect = function() {
+
+ window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
+ window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
+
+ scope.enabled = false;
+
+ };
+
+ this.update = function() {
+
+ if ( scope.enabled === false ) return;
+
+ var alpha = scope.deviceOrientation.alpha ? THREE.Math.degToRad( scope.deviceOrientation.alpha ) + this.alphaOffsetAngle : 0; // Z
+ var beta = scope.deviceOrientation.beta ? THREE.Math.degToRad( scope.deviceOrientation.beta ) : 0; // X'
+ var gamma = scope.deviceOrientation.gamma ? THREE.Math.degToRad( scope.deviceOrientation.gamma ) : 0; // Y''
+ var orient = scope.screenOrientation ? THREE.Math.degToRad( scope.screenOrientation ) : 0; // O
+
+ setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient );
+ this.alpha = alpha;
+
+ };
+
+ this.updateAlphaOffsetAngle = function( angle ) {
+
+ this.alphaOffsetAngle = angle;
+ this.update();
+
+ };
+
+ this.dispose = function() {
+
+ this.disconnect();
+
+ };
+
+ this.connect();
+
+};
diff --git a/app/static/js/controls/DragControls.js b/app/static/js/controls/DragControls.js
new file mode 100644
index 0000000..40d08ac
--- /dev/null
+++ b/app/static/js/controls/DragControls.js
@@ -0,0 +1,282 @@
+/*
+ * @author zz85 / https://github.com/zz85
+ * @author mrdoob / http://mrdoob.com
+ * Running this will allow you to drag three.js objects around the screen.
+ */
+
+THREE.DragControls = function ( _objects, _camera, _domElement ) {
+
+ if ( _objects instanceof THREE.Camera ) {
+
+ console.warn( 'THREE.DragControls: Constructor now expects ( objects, camera, domElement )' );
+ var temp = _objects; _objects = _camera; _camera = temp;
+
+ }
+
+ var _plane = new THREE.Plane();
+ var _raycaster = new THREE.Raycaster();
+
+ var _mouse = new THREE.Vector2();
+ var _offset = new THREE.Vector3();
+ var _intersection = new THREE.Vector3();
+
+ var _selected = null, _hovered = null;
+
+ //
+
+ var scope = this;
+
+ function activate() {
+
+ _domElement.addEventListener( 'mousemove', onDocumentMouseMove, false );
+ _domElement.addEventListener( 'mousedown', onDocumentMouseDown, false );
+ _domElement.addEventListener( 'mouseup', onDocumentMouseCancel, false );
+ _domElement.addEventListener( 'mouseleave', onDocumentMouseCancel, false );
+ _domElement.addEventListener( 'touchmove', onDocumentTouchMove, false );
+ _domElement.addEventListener( 'touchstart', onDocumentTouchStart, false );
+ _domElement.addEventListener( 'touchend', onDocumentTouchEnd, false );
+
+ }
+
+ function deactivate() {
+
+ _domElement.removeEventListener( 'mousemove', onDocumentMouseMove, false );
+ _domElement.removeEventListener( 'mousedown', onDocumentMouseDown, false );
+ _domElement.removeEventListener( 'mouseup', onDocumentMouseCancel, false );
+ _domElement.removeEventListener( 'mouseleave', onDocumentMouseCancel, false );
+ _domElement.removeEventListener( 'touchmove', onDocumentTouchMove, false );
+ _domElement.removeEventListener( 'touchstart', onDocumentTouchStart, false );
+ _domElement.removeEventListener( 'touchend', onDocumentTouchEnd, false );
+
+ }
+
+ function dispose() {
+
+ deactivate();
+
+ }
+
+ function onDocumentMouseMove( event ) {
+
+ event.preventDefault();
+
+ var rect = _domElement.getBoundingClientRect();
+
+ _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
+ _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
+
+ _raycaster.setFromCamera( _mouse, _camera );
+
+ if ( _selected && scope.enabled ) {
+
+ if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+ _selected.position.copy( _intersection.sub( _offset ) );
+
+ }
+
+ scope.dispatchEvent( { type: 'drag', object: _selected } );
+
+ return;
+
+ }
+
+ _raycaster.setFromCamera( _mouse, _camera );
+
+ var intersects = _raycaster.intersectObjects( _objects );
+
+ if ( intersects.length > 0 ) {
+
+ var object = intersects[ 0 ].object;
+
+ _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), object.position );
+
+ if ( _hovered !== object ) {
+
+ scope.dispatchEvent( { type: 'hoveron', object: object } );
+
+ _domElement.style.cursor = 'pointer';
+ _hovered = object;
+
+ }
+
+ } else {
+
+ if ( _hovered !== null ) {
+
+ scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+
+ _domElement.style.cursor = 'auto';
+ _hovered = null;
+
+ }
+
+ }
+
+ }
+
+ function onDocumentMouseDown( event ) {
+
+ event.preventDefault();
+
+ _raycaster.setFromCamera( _mouse, _camera );
+
+ var intersects = _raycaster.intersectObjects( _objects );
+
+ if ( intersects.length > 0 ) {
+
+ _selected = intersects[ 0 ].object;
+
+ if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+ _offset.copy( _intersection ).sub( _selected.position );
+
+ }
+
+ _domElement.style.cursor = 'move';
+
+ scope.dispatchEvent( { type: 'dragstart', object: _selected } );
+
+ }
+
+
+ }
+
+ function onDocumentMouseCancel( event ) {
+
+ event.preventDefault();
+
+ if ( _selected ) {
+
+ scope.dispatchEvent( { type: 'dragend', object: _selected } );
+
+ _selected = null;
+
+ }
+
+ _domElement.style.cursor = 'auto';
+
+ }
+
+ function onDocumentTouchMove( event ) {
+
+ event.preventDefault();
+ event = event.changedTouches[ 0 ];
+
+ var rect = _domElement.getBoundingClientRect();
+
+ _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
+ _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
+
+ _raycaster.setFromCamera( _mouse, _camera );
+
+ if ( _selected && scope.enabled ) {
+
+ if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+ _selected.position.copy( _intersection.sub( _offset ) );
+
+ }
+
+ scope.dispatchEvent( { type: 'drag', object: _selected } );
+
+ return;
+
+ }
+
+ }
+
+ function onDocumentTouchStart( event ) {
+
+ event.preventDefault();
+ event = event.changedTouches[ 0 ];
+
+ var rect = _domElement.getBoundingClientRect();
+
+ _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
+ _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
+
+ _raycaster.setFromCamera( _mouse, _camera );
+
+ var intersects = _raycaster.intersectObjects( _objects );
+
+ if ( intersects.length > 0 ) {
+
+ _selected = intersects[ 0 ].object;
+
+ _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _selected.position );
+
+ if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+ _offset.copy( _intersection ).sub( _selected.position );
+
+ }
+
+ _domElement.style.cursor = 'move';
+
+ scope.dispatchEvent( { type: 'dragstart', object: _selected } );
+
+ }
+
+
+ }
+
+ function onDocumentTouchEnd( event ) {
+
+ event.preventDefault();
+
+ if ( _selected ) {
+
+ scope.dispatchEvent( { type: 'dragend', object: _selected } );
+
+ _selected = null;
+
+ }
+
+ _domElement.style.cursor = 'auto';
+
+ }
+
+ activate();
+
+ // API
+
+ this.enabled = true;
+
+ this.activate = activate;
+ this.deactivate = deactivate;
+ this.dispose = dispose;
+
+ // Backward compatibility
+
+ this.setObjects = function () {
+
+ console.error( 'THREE.DragControls: setObjects() has been removed.' );
+
+ };
+
+ this.on = function ( type, listener ) {
+
+ console.warn( 'THREE.DragControls: on() has been deprecated. Use addEventListener() instead.' );
+ scope.addEventListener( type, listener );
+
+ };
+
+ this.off = function ( type, listener ) {
+
+ console.warn( 'THREE.DragControls: off() has been deprecated. Use removeEventListener() instead.' );
+ scope.removeEventListener( type, listener );
+
+ };
+
+ this.notify = function ( type ) {
+
+ console.error( 'THREE.DragControls: notify() has been deprecated. Use dispatchEvent() instead.' );
+ scope.dispatchEvent( { type: type } );
+
+ };
+
+};
+
+THREE.DragControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.DragControls.prototype.constructor = THREE.DragControls;
diff --git a/app/static/js/controls/EditorControls.js b/app/static/js/controls/EditorControls.js
new file mode 100644
index 0000000..dbb1cac
--- /dev/null
+++ b/app/static/js/controls/EditorControls.js
@@ -0,0 +1,291 @@
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ */
+
+THREE.EditorControls = function ( object, domElement ) {
+
+ domElement = ( domElement !== undefined ) ? domElement : document;
+
+ // API
+
+ this.enabled = true;
+ this.center = new THREE.Vector3();
+ this.panSpeed = 0.001;
+ this.zoomSpeed = 0.001;
+ this.rotationSpeed = 0.005;
+
+ // internals
+
+ var scope = this;
+ var vector = new THREE.Vector3();
+
+ var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
+ var state = STATE.NONE;
+
+ var center = this.center;
+ var normalMatrix = new THREE.Matrix3();
+ var pointer = new THREE.Vector2();
+ var pointerOld = new THREE.Vector2();
+ var spherical = new THREE.Spherical();
+
+ // events
+
+ var changeEvent = { type: 'change' };
+
+ this.focus = function ( target ) {
+
+ var box = new THREE.Box3().setFromObject( target );
+ object.lookAt( center.copy( box.getCenter() ) );
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.pan = function ( delta ) {
+
+ var distance = object.position.distanceTo( center );
+
+ delta.multiplyScalar( distance * scope.panSpeed );
+ delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
+
+ object.position.add( delta );
+ center.add( delta );
+
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.zoom = function ( delta ) {
+
+ var distance = object.position.distanceTo( center );
+
+ delta.multiplyScalar( distance * scope.zoomSpeed );
+
+ if ( delta.length() > distance ) return;
+
+ delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
+
+ object.position.add( delta );
+
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.rotate = function ( delta ) {
+
+ vector.copy( object.position ).sub( center );
+
+ spherical.setFromVector3( vector );
+
+ spherical.theta += delta.x;
+ spherical.phi += delta.y;
+
+ spherical.makeSafe();
+
+ vector.setFromSpherical( spherical );
+
+ object.position.copy( center ).add( vector );
+
+ object.lookAt( center );
+
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ // mouse
+
+ function onMouseDown( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ if ( event.button === 0 ) {
+
+ state = STATE.ROTATE;
+
+ } else if ( event.button === 1 ) {
+
+ state = STATE.ZOOM;
+
+ } else if ( event.button === 2 ) {
+
+ state = STATE.PAN;
+
+ }
+
+ pointerOld.set( event.clientX, event.clientY );
+
+ domElement.addEventListener( 'mousemove', onMouseMove, false );
+ domElement.addEventListener( 'mouseup', onMouseUp, false );
+ domElement.addEventListener( 'mouseout', onMouseUp, false );
+ domElement.addEventListener( 'dblclick', onMouseUp, false );
+
+ }
+
+ function onMouseMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ pointer.set( event.clientX, event.clientY );
+
+ var movementX = pointer.x - pointerOld.x;
+ var movementY = pointer.y - pointerOld.y;
+
+ if ( state === STATE.ROTATE ) {
+
+ scope.rotate( new THREE.Vector3( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );
+
+ } else if ( state === STATE.ZOOM ) {
+
+ scope.zoom( new THREE.Vector3( 0, 0, movementY ) );
+
+ } else if ( state === STATE.PAN ) {
+
+ scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );
+
+ }
+
+ pointerOld.set( event.clientX, event.clientY );
+
+ }
+
+ function onMouseUp( event ) {
+
+ domElement.removeEventListener( 'mousemove', onMouseMove, false );
+ domElement.removeEventListener( 'mouseup', onMouseUp, false );
+ domElement.removeEventListener( 'mouseout', onMouseUp, false );
+ domElement.removeEventListener( 'dblclick', onMouseUp, false );
+
+ state = STATE.NONE;
+
+ }
+
+ function onMouseWheel( event ) {
+
+ event.preventDefault();
+
+ // if ( scope.enabled === false ) return;
+
+ scope.zoom( new THREE.Vector3( 0, 0, event.deltaY ) );
+
+ }
+
+ function contextmenu( event ) {
+
+ event.preventDefault();
+
+ }
+
+ this.dispose = function() {
+
+ domElement.removeEventListener( 'contextmenu', contextmenu, false );
+ domElement.removeEventListener( 'mousedown', onMouseDown, false );
+ domElement.removeEventListener( 'wheel', onMouseWheel, false );
+
+ domElement.removeEventListener( 'mousemove', onMouseMove, false );
+ domElement.removeEventListener( 'mouseup', onMouseUp, false );
+ domElement.removeEventListener( 'mouseout', onMouseUp, false );
+ domElement.removeEventListener( 'dblclick', onMouseUp, false );
+
+ domElement.removeEventListener( 'touchstart', touchStart, false );
+ domElement.removeEventListener( 'touchmove', touchMove, false );
+
+ };
+
+ domElement.addEventListener( 'contextmenu', contextmenu, false );
+ domElement.addEventListener( 'mousedown', onMouseDown, false );
+ domElement.addEventListener( 'wheel', onMouseWheel, false );
+
+ // touch
+
+ var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
+ var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
+
+ var prevDistance = null;
+
+ function touchStart( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ break;
+
+ case 2:
+ touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
+ prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
+ break;
+
+ }
+
+ prevTouches[ 0 ].copy( touches[ 0 ] );
+ prevTouches[ 1 ].copy( touches[ 1 ] );
+
+ }
+
+
+ function touchMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ function getClosest( touch, touches ) {
+
+ var closest = touches[ 0 ];
+
+ for ( var i in touches ) {
+
+ if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ];
+
+ }
+
+ return closest;
+
+ }
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
+ break;
+
+ case 2:
+ touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+ touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
+ var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
+ scope.zoom( new THREE.Vector3( 0, 0, prevDistance - distance ) );
+ prevDistance = distance;
+
+
+ var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
+ var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
+ offset0.x = - offset0.x;
+ offset1.x = - offset1.x;
+
+ scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );
+
+ break;
+
+ }
+
+ prevTouches[ 0 ].copy( touches[ 0 ] );
+ prevTouches[ 1 ].copy( touches[ 1 ] );
+
+ }
+
+ domElement.addEventListener( 'touchstart', touchStart, false );
+ domElement.addEventListener( 'touchmove', touchMove, false );
+
+};
+
+THREE.EditorControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.EditorControls.prototype.constructor = THREE.EditorControls;
diff --git a/app/static/js/controls/FirstPersonControls.js b/app/static/js/controls/FirstPersonControls.js
new file mode 100644
index 0000000..51e0c39
--- /dev/null
+++ b/app/static/js/controls/FirstPersonControls.js
@@ -0,0 +1,300 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author alteredq / http://alteredqualia.com/
+ * @author paulirish / http://paulirish.com/
+ */
+
+THREE.FirstPersonControls = function ( object, domElement ) {
+
+ this.object = object;
+ this.target = new THREE.Vector3( 0, 0, 0 );
+
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+ this.enabled = true;
+
+ this.movementSpeed = 1.0;
+ this.lookSpeed = 0.005;
+
+ this.lookVertical = true;
+ this.autoForward = false;
+
+ this.activeLook = true;
+
+ this.heightSpeed = false;
+ this.heightCoef = 1.0;
+ this.heightMin = 0.0;
+ this.heightMax = 1.0;
+
+ this.constrainVertical = false;
+ this.verticalMin = 0;
+ this.verticalMax = Math.PI;
+
+ this.autoSpeedFactor = 0.0;
+
+ this.mouseX = 0;
+ this.mouseY = 0;
+
+ this.lat = 0;
+ this.lon = 0;
+ this.phi = 0;
+ this.theta = 0;
+
+ this.moveForward = false;
+ this.moveBackward = false;
+ this.moveLeft = false;
+ this.moveRight = false;
+
+ this.mouseDragOn = false;
+
+ this.viewHalfX = 0;
+ this.viewHalfY = 0;
+
+ if ( this.domElement !== document ) {
+
+ this.domElement.setAttribute( 'tabindex', - 1 );
+
+ }
+
+ //
+
+ this.handleResize = function () {
+
+ if ( this.domElement === document ) {
+
+ this.viewHalfX = window.innerWidth / 2;
+ this.viewHalfY = window.innerHeight / 2;
+
+ } else {
+
+ this.viewHalfX = this.domElement.offsetWidth / 2;
+ this.viewHalfY = this.domElement.offsetHeight / 2;
+
+ }
+
+ };
+
+ this.onMouseDown = function ( event ) {
+
+ if ( this.domElement !== document ) {
+
+ this.domElement.focus();
+
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( this.activeLook ) {
+
+ switch ( event.button ) {
+
+ case 0: this.moveForward = true; break;
+ case 2: this.moveBackward = true; break;
+
+ }
+
+ }
+
+ this.mouseDragOn = true;
+
+ };
+
+ this.onMouseUp = function ( event ) {
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( this.activeLook ) {
+
+ switch ( event.button ) {
+
+ case 0: this.moveForward = false; break;
+ case 2: this.moveBackward = false; break;
+
+ }
+
+ }
+
+ this.mouseDragOn = false;
+
+ };
+
+ this.onMouseMove = function ( event ) {
+
+ if ( this.domElement === document ) {
+
+ this.mouseX = event.pageX - this.viewHalfX;
+ this.mouseY = event.pageY - this.viewHalfY;
+
+ } else {
+
+ this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
+ this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY;
+
+ }
+
+ };
+
+ this.onKeyDown = function ( event ) {
+
+ //event.preventDefault();
+
+ switch ( event.keyCode ) {
+
+ case 38: /*up*/
+ case 87: /*W*/ this.moveForward = true; break;
+
+ case 37: /*left*/
+ case 65: /*A*/ this.moveLeft = true; break;
+
+ case 40: /*down*/
+ case 83: /*S*/ this.moveBackward = true; break;
+
+ case 39: /*right*/
+ case 68: /*D*/ this.moveRight = true; break;
+
+ case 82: /*R*/ this.moveUp = true; break;
+ case 70: /*F*/ this.moveDown = true; break;
+
+ }
+
+ };
+
+ this.onKeyUp = function ( event ) {
+
+ switch ( event.keyCode ) {
+
+ case 38: /*up*/
+ case 87: /*W*/ this.moveForward = false; break;
+
+ case 37: /*left*/
+ case 65: /*A*/ this.moveLeft = false; break;
+
+ case 40: /*down*/
+ case 83: /*S*/ this.moveBackward = false; break;
+
+ case 39: /*right*/
+ case 68: /*D*/ this.moveRight = false; break;
+
+ case 82: /*R*/ this.moveUp = false; break;
+ case 70: /*F*/ this.moveDown = false; break;
+
+ }
+
+ };
+
+ this.update = function( delta ) {
+
+ if ( this.enabled === false ) return;
+
+ if ( this.heightSpeed ) {
+
+ var y = THREE.Math.clamp( this.object.position.y, this.heightMin, this.heightMax );
+ var heightDelta = y - this.heightMin;
+
+ this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef );
+
+ } else {
+
+ this.autoSpeedFactor = 0.0;
+
+ }
+
+ var actualMoveSpeed = delta * this.movementSpeed;
+
+ if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) );
+ if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed );
+
+ if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed );
+ if ( this.moveRight ) this.object.translateX( actualMoveSpeed );
+
+ if ( this.moveUp ) this.object.translateY( actualMoveSpeed );
+ if ( this.moveDown ) this.object.translateY( - actualMoveSpeed );
+
+ var actualLookSpeed = delta * this.lookSpeed;
+
+ if ( ! this.activeLook ) {
+
+ actualLookSpeed = 0;
+
+ }
+
+ var verticalLookRatio = 1;
+
+ if ( this.constrainVertical ) {
+
+ verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin );
+
+ }
+
+ this.lon += this.mouseX * actualLookSpeed;
+ if ( this.lookVertical ) this.lat -= this.mouseY * actualLookSpeed * verticalLookRatio;
+
+ this.lat = Math.max( - 85, Math.min( 85, this.lat ) );
+ this.phi = THREE.Math.degToRad( 90 - this.lat );
+
+ this.theta = THREE.Math.degToRad( this.lon );
+
+ if ( this.constrainVertical ) {
+
+ this.phi = THREE.Math.mapLinear( this.phi, 0, Math.PI, this.verticalMin, this.verticalMax );
+
+ }
+
+ var targetPosition = this.target,
+ position = this.object.position;
+
+ targetPosition.x = position.x + 100 * Math.sin( this.phi ) * Math.cos( this.theta );
+ targetPosition.y = position.y + 100 * Math.cos( this.phi );
+ targetPosition.z = position.z + 100 * Math.sin( this.phi ) * Math.sin( this.theta );
+
+ this.object.lookAt( targetPosition );
+
+ };
+
+ function contextmenu( event ) {
+
+ event.preventDefault();
+
+ }
+
+ this.dispose = function() {
+
+ this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.removeEventListener( 'mousedown', _onMouseDown, false );
+ this.domElement.removeEventListener( 'mousemove', _onMouseMove, false );
+ this.domElement.removeEventListener( 'mouseup', _onMouseUp, false );
+
+ window.removeEventListener( 'keydown', _onKeyDown, false );
+ window.removeEventListener( 'keyup', _onKeyUp, false );
+
+ };
+
+ var _onMouseMove = bind( this, this.onMouseMove );
+ var _onMouseDown = bind( this, this.onMouseDown );
+ var _onMouseUp = bind( this, this.onMouseUp );
+ var _onKeyDown = bind( this, this.onKeyDown );
+ var _onKeyUp = bind( this, this.onKeyUp );
+
+ this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.addEventListener( 'mousemove', _onMouseMove, false );
+ this.domElement.addEventListener( 'mousedown', _onMouseDown, false );
+ this.domElement.addEventListener( 'mouseup', _onMouseUp, false );
+
+ window.addEventListener( 'keydown', _onKeyDown, false );
+ window.addEventListener( 'keyup', _onKeyUp, false );
+
+ function bind( scope, fn ) {
+
+ return function () {
+
+ fn.apply( scope, arguments );
+
+ };
+
+ }
+
+ this.handleResize();
+
+};
diff --git a/app/static/js/controls/FlyControls.js b/app/static/js/controls/FlyControls.js
new file mode 100644
index 0000000..e55ff69
--- /dev/null
+++ b/app/static/js/controls/FlyControls.js
@@ -0,0 +1,293 @@
+/**
+ * @author James Baicoianu / http://www.baicoianu.com/
+ */
+
+THREE.FlyControls = function ( object, domElement ) {
+
+ this.object = object;
+
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+ if ( domElement ) this.domElement.setAttribute( 'tabindex', - 1 );
+
+ // API
+
+ this.movementSpeed = 1.0;
+ this.rollSpeed = 0.005;
+
+ this.dragToLook = false;
+ this.autoForward = false;
+
+ // disable default target object behavior
+
+ // internals
+
+ this.tmpQuaternion = new THREE.Quaternion();
+
+ this.mouseStatus = 0;
+
+ this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
+ this.moveVector = new THREE.Vector3( 0, 0, 0 );
+ this.rotationVector = new THREE.Vector3( 0, 0, 0 );
+
+ this.handleEvent = function ( event ) {
+
+ if ( typeof this[ event.type ] == 'function' ) {
+
+ this[ event.type ]( event );
+
+ }
+
+ };
+
+ this.keydown = function( event ) {
+
+ if ( event.altKey ) {
+
+ return;
+
+ }
+
+ //event.preventDefault();
+
+ switch ( event.keyCode ) {
+
+ case 16: /* shift */ this.movementSpeedMultiplier = .1; break;
+
+ case 87: /*W*/ this.moveState.forward = 1; break;
+ case 83: /*S*/ this.moveState.back = 1; break;
+
+ case 65: /*A*/ this.moveState.left = 1; break;
+ case 68: /*D*/ this.moveState.right = 1; break;
+
+ case 82: /*R*/ this.moveState.up = 1; break;
+ case 70: /*F*/ this.moveState.down = 1; break;
+
+ case 38: /*up*/ this.moveState.pitchUp = 1; break;
+ case 40: /*down*/ this.moveState.pitchDown = 1; break;
+
+ case 37: /*left*/ this.moveState.yawLeft = 1; break;
+ case 39: /*right*/ this.moveState.yawRight = 1; break;
+
+ case 81: /*Q*/ this.moveState.rollLeft = 1; break;
+ case 69: /*E*/ this.moveState.rollRight = 1; break;
+
+ }
+
+ this.updateMovementVector();
+ this.updateRotationVector();
+
+ };
+
+ this.keyup = function( event ) {
+
+ switch ( event.keyCode ) {
+
+ case 16: /* shift */ this.movementSpeedMultiplier = 1; break;
+
+ case 87: /*W*/ this.moveState.forward = 0; break;
+ case 83: /*S*/ this.moveState.back = 0; break;
+
+ case 65: /*A*/ this.moveState.left = 0; break;
+ case 68: /*D*/ this.moveState.right = 0; break;
+
+ case 82: /*R*/ this.moveState.up = 0; break;
+ case 70: /*F*/ this.moveState.down = 0; break;
+
+ case 38: /*up*/ this.moveState.pitchUp = 0; break;
+ case 40: /*down*/ this.moveState.pitchDown = 0; break;
+
+ case 37: /*left*/ this.moveState.yawLeft = 0; break;
+ case 39: /*right*/ this.moveState.yawRight = 0; break;
+
+ case 81: /*Q*/ this.moveState.rollLeft = 0; break;
+ case 69: /*E*/ this.moveState.rollRight = 0; break;
+
+ }
+
+ this.updateMovementVector();
+ this.updateRotationVector();
+
+ };
+
+ this.mousedown = function( event ) {
+
+ if ( this.domElement !== document ) {
+
+ this.domElement.focus();
+
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( this.dragToLook ) {
+
+ this.mouseStatus ++;
+
+ } else {
+
+ switch ( event.button ) {
+
+ case 0: this.moveState.forward = 1; break;
+ case 2: this.moveState.back = 1; break;
+
+ }
+
+ this.updateMovementVector();
+
+ }
+
+ };
+
+ this.mousemove = function( event ) {
+
+ if ( ! this.dragToLook || this.mouseStatus > 0 ) {
+
+ var container = this.getContainerDimensions();
+ var halfWidth = container.size[ 0 ] / 2;
+ var halfHeight = container.size[ 1 ] / 2;
+
+ this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
+ this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
+
+ this.updateRotationVector();
+
+ }
+
+ };
+
+ this.mouseup = function( event ) {
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( this.dragToLook ) {
+
+ this.mouseStatus --;
+
+ this.moveState.yawLeft = this.moveState.pitchDown = 0;
+
+ } else {
+
+ switch ( event.button ) {
+
+ case 0: this.moveState.forward = 0; break;
+ case 2: this.moveState.back = 0; break;
+
+ }
+
+ this.updateMovementVector();
+
+ }
+
+ this.updateRotationVector();
+
+ };
+
+ this.update = function( delta ) {
+
+ var moveMult = delta * this.movementSpeed;
+ var rotMult = delta * this.rollSpeed;
+
+ this.object.translateX( this.moveVector.x * moveMult );
+ this.object.translateY( this.moveVector.y * moveMult );
+ this.object.translateZ( this.moveVector.z * moveMult );
+
+ this.tmpQuaternion.set( this.rotationVector.x * rotMult, this.rotationVector.y * rotMult, this.rotationVector.z * rotMult, 1 ).normalize();
+ this.object.quaternion.multiply( this.tmpQuaternion );
+
+ // expose the rotation vector for convenience
+ this.object.rotation.setFromQuaternion( this.object.quaternion, this.object.rotation.order );
+
+
+ };
+
+ this.updateMovementVector = function() {
+
+ var forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
+
+ this.moveVector.x = ( - this.moveState.left + this.moveState.right );
+ this.moveVector.y = ( - this.moveState.down + this.moveState.up );
+ this.moveVector.z = ( - forward + this.moveState.back );
+
+ //console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
+
+ };
+
+ this.updateRotationVector = function() {
+
+ this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
+ this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft );
+ this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
+
+ //console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
+
+ };
+
+ this.getContainerDimensions = function() {
+
+ if ( this.domElement != document ) {
+
+ return {
+ size : [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
+ offset : [ this.domElement.offsetLeft, this.domElement.offsetTop ]
+ };
+
+ } else {
+
+ return {
+ size : [ window.innerWidth, window.innerHeight ],
+ offset : [ 0, 0 ]
+ };
+
+ }
+
+ };
+
+ function bind( scope, fn ) {
+
+ return function () {
+
+ fn.apply( scope, arguments );
+
+ };
+
+ }
+
+ function contextmenu( event ) {
+
+ event.preventDefault();
+
+ }
+
+ this.dispose = function() {
+
+ this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.removeEventListener( 'mousedown', _mousedown, false );
+ this.domElement.removeEventListener( 'mousemove', _mousemove, false );
+ this.domElement.removeEventListener( 'mouseup', _mouseup, false );
+
+ window.removeEventListener( 'keydown', _keydown, false );
+ window.removeEventListener( 'keyup', _keyup, false );
+
+ };
+
+ var _mousemove = bind( this, this.mousemove );
+ var _mousedown = bind( this, this.mousedown );
+ var _mouseup = bind( this, this.mouseup );
+ var _keydown = bind( this, this.keydown );
+ var _keyup = bind( this, this.keyup );
+
+ this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+
+ this.domElement.addEventListener( 'mousemove', _mousemove, false );
+ this.domElement.addEventListener( 'mousedown', _mousedown, false );
+ this.domElement.addEventListener( 'mouseup', _mouseup, false );
+
+ window.addEventListener( 'keydown', _keydown, false );
+ window.addEventListener( 'keyup', _keyup, false );
+
+ this.updateMovementVector();
+ this.updateRotationVector();
+
+};
diff --git a/app/static/js/controls/OrbitControls.js b/app/static/js/controls/OrbitControls.js
new file mode 100644
index 0000000..151f0c8
--- /dev/null
+++ b/app/static/js/controls/OrbitControls.js
@@ -0,0 +1,1042 @@
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ * @author erich666 / http://erichaines.com
+ */
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+// Orbit - left mouse / touch: one finger move
+// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
+// Pan - right mouse, or arrow keys / touch: three finger swipe
+
+THREE.OrbitControls = function ( object, domElement ) {
+
+ this.object = object;
+
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+ // Set to false to disable this control
+ this.enabled = true;
+
+ // "target" sets the location of focus, where the object orbits around
+ this.target = new THREE.Vector3();
+
+ // How far you can dolly in and out ( PerspectiveCamera only )
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ // How far you can zoom in and out ( OrthographicCamera only )
+ this.minZoom = 0;
+ this.maxZoom = Infinity;
+
+ // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+ this.minPolarAngle = 0; // radians
+ this.maxPolarAngle = Math.PI; // radians
+
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+ this.minAzimuthAngle = - Infinity; // radians
+ this.maxAzimuthAngle = Infinity; // radians
+
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+ this.enableDamping = false;
+ this.dampingFactor = 0.25;
+
+ // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0;
+
+ // Set to false to disable rotating
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0;
+
+ // Set to false to disable panning
+ this.enablePan = true;
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+
+ // Set to false to disable use of the keys
+ this.enableKeys = true;
+
+ // The four arrow keys
+ this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+ // Mouse buttons
+ this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
+
+ // for reset
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = this.object.zoom;
+
+ //
+ // public methods
+ //
+
+ this.getPolarAngle = function () {
+
+ return spherical.phi;
+
+ };
+
+ this.getAzimuthalAngle = function () {
+
+ return spherical.theta;
+
+ };
+
+ this.saveState = function () {
+
+ scope.target0.copy( scope.target );
+ scope.position0.copy( scope.object.position );
+ scope.zoom0 = scope.object.zoom;
+
+ };
+
+ this.reset = function () {
+
+ scope.target.copy( scope.target0 );
+ scope.object.position.copy( scope.position0 );
+ scope.object.zoom = scope.zoom0;
+
+ scope.object.updateProjectionMatrix();
+ scope.dispatchEvent( changeEvent );
+
+ scope.update();
+
+ state = STATE.NONE;
+
+ };
+
+ // this method is exposed, but perhaps it would be better if we can make it private...
+ this.update = function () {
+
+ var offset = new THREE.Vector3();
+
+ // so camera.up is the orbit axis
+ var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+ var quatInverse = quat.clone().inverse();
+
+ var lastPosition = new THREE.Vector3();
+ var lastQuaternion = new THREE.Quaternion();
+
+ return function update() {
+
+ var position = scope.object.position;
+
+ offset.copy( position ).sub( scope.target );
+
+ // rotate offset to "y-axis-is-up" space
+ offset.applyQuaternion( quat );
+
+ // angle from z-axis around y-axis
+ spherical.setFromVector3( offset );
+
+ if ( scope.autoRotate && state === STATE.NONE ) {
+
+ rotateLeft( getAutoRotationAngle() );
+
+ }
+
+ spherical.theta += sphericalDelta.theta;
+ spherical.phi += sphericalDelta.phi;
+
+ // restrict theta to be between desired limits
+ spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
+
+ // restrict phi to be between desired limits
+ spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+
+ spherical.makeSafe();
+
+
+ spherical.radius *= scale;
+
+ // restrict radius to be between desired limits
+ spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
+
+ // move target to panned location
+ scope.target.add( panOffset );
+
+ offset.setFromSpherical( spherical );
+
+ // rotate offset back to "camera-up-vector-is-up" space
+ offset.applyQuaternion( quatInverse );
+
+ position.copy( scope.target ).add( offset );
+
+ scope.object.lookAt( scope.target );
+
+ if ( scope.enableDamping === true ) {
+
+ sphericalDelta.theta *= ( 1 - scope.dampingFactor );
+ sphericalDelta.phi *= ( 1 - scope.dampingFactor );
+
+ } else {
+
+ sphericalDelta.set( 0, 0, 0 );
+
+ }
+
+ scale = 1;
+ panOffset.set( 0, 0, 0 );
+
+ // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if ( zoomChanged ||
+ lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+ 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+ scope.dispatchEvent( changeEvent );
+
+ lastPosition.copy( scope.object.position );
+ lastQuaternion.copy( scope.object.quaternion );
+ zoomChanged = false;
+
+ return true;
+
+ }
+
+ return false;
+
+ };
+
+ }();
+
+ this.dispose = function () {
+
+ scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
+ scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
+
+ scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
+
+ document.removeEventListener( 'mousemove', onMouseMove, false );
+ document.removeEventListener( 'mouseup', onMouseUp, false );
+
+ window.removeEventListener( 'keydown', onKeyDown, false );
+
+ //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+ };
+
+ //
+ // internals
+ //
+
+ var scope = this;
+
+ var changeEvent = { type: 'change' };
+ var startEvent = { type: 'start' };
+ var endEvent = { type: 'end' };
+
+ var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 };
+
+ var state = STATE.NONE;
+
+ var EPS = 0.000001;
+
+ // current position in spherical coordinates
+ var spherical = new THREE.Spherical();
+ var sphericalDelta = new THREE.Spherical();
+
+ var scale = 1;
+ var panOffset = new THREE.Vector3();
+ var zoomChanged = false;
+
+ var rotateStart = new THREE.Vector2();
+ var rotateEnd = new THREE.Vector2();
+ var rotateDelta = new THREE.Vector2();
+
+ var panStart = new THREE.Vector2();
+ var panEnd = new THREE.Vector2();
+ var panDelta = new THREE.Vector2();
+
+ var dollyStart = new THREE.Vector2();
+ var dollyEnd = new THREE.Vector2();
+ var dollyDelta = new THREE.Vector2();
+
+ function getAutoRotationAngle() {
+
+ return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+
+ }
+
+ function getZoomScale() {
+
+ return Math.pow( 0.95, scope.zoomSpeed );
+
+ }
+
+ function rotateLeft( angle ) {
+
+ sphericalDelta.theta -= angle;
+
+ }
+
+ function rotateUp( angle ) {
+
+ sphericalDelta.phi -= angle;
+
+ }
+
+ var panLeft = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panLeft( distance, objectMatrix ) {
+
+ v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+ v.multiplyScalar( - distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ var panUp = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panUp( distance, objectMatrix ) {
+
+ v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
+ v.multiplyScalar( distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ // deltaX and deltaY are in pixels; right and down are positive
+ var pan = function () {
+
+ var offset = new THREE.Vector3();
+
+ return function pan( deltaX, deltaY ) {
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+ // perspective
+ var position = scope.object.position;
+ offset.copy( position ).sub( scope.target );
+ var targetDistance = offset.length();
+
+ // half of the fov is center to top of screen
+ targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+
+ // we actually don't use screenWidth, since perspective camera is fixed to screen height
+ panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+ panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+ } else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+ // orthographic
+ panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+ panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+ } else {
+
+ // camera neither orthographic nor perspective
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+ scope.enablePan = false;
+
+ }
+
+ };
+
+ }();
+
+ function dollyIn( dollyScale ) {
+
+ if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+ scale /= dollyScale;
+
+ } else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ function dollyOut( dollyScale ) {
+
+ if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+ scale *= dollyScale;
+
+ } else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ //
+ // event callbacks - update the object state
+ //
+
+ function handleMouseDownRotate( event ) {
+
+ //console.log( 'handleMouseDownRotate' );
+
+ rotateStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownDolly( event ) {
+
+ //console.log( 'handleMouseDownDolly' );
+
+ dollyStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownPan( event ) {
+
+ //console.log( 'handleMouseDownPan' );
+
+ panStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseMoveRotate( event ) {
+
+ //console.log( 'handleMouseMoveRotate' );
+
+ rotateEnd.set( event.clientX, event.clientY );
+ rotateDelta.subVectors( rotateEnd, rotateStart );
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+
+ rotateStart.copy( rotateEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMoveDolly( event ) {
+
+ //console.log( 'handleMouseMoveDolly' );
+
+ dollyEnd.set( event.clientX, event.clientY );
+
+ dollyDelta.subVectors( dollyEnd, dollyStart );
+
+ if ( dollyDelta.y > 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ } else if ( dollyDelta.y < 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ }
+
+ dollyStart.copy( dollyEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMovePan( event ) {
+
+ //console.log( 'handleMouseMovePan' );
+
+ panEnd.set( event.clientX, event.clientY );
+
+ panDelta.subVectors( panEnd, panStart );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseUp( event ) {
+
+ // console.log( 'handleMouseUp' );
+
+ }
+
+ function handleMouseWheel( event ) {
+
+ // console.log( 'handleMouseWheel' );
+
+ if ( event.deltaY < 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ } else if ( event.deltaY > 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ }
+
+ scope.update();
+
+ }
+
+ function handleKeyDown( event ) {
+
+ //console.log( 'handleKeyDown' );
+
+ switch ( event.keyCode ) {
+
+ case scope.keys.UP:
+ pan( 0, scope.keyPanSpeed );
+ scope.update();
+ break;
+
+ case scope.keys.BOTTOM:
+ pan( 0, - scope.keyPanSpeed );
+ scope.update();
+ break;
+
+ case scope.keys.LEFT:
+ pan( scope.keyPanSpeed, 0 );
+ scope.update();
+ break;
+
+ case scope.keys.RIGHT:
+ pan( - scope.keyPanSpeed, 0 );
+ scope.update();
+ break;
+
+ }
+
+ }
+
+ function handleTouchStartRotate( event ) {
+
+ //console.log( 'handleTouchStartRotate' );
+
+ rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ }
+
+ function handleTouchStartDolly( event ) {
+
+ //console.log( 'handleTouchStartDolly' );
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyStart.set( 0, distance );
+
+ }
+
+ function handleTouchStartPan( event ) {
+
+ //console.log( 'handleTouchStartPan' );
+
+ panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ }
+
+ function handleTouchMoveRotate( event ) {
+
+ //console.log( 'handleTouchMoveRotate' );
+
+ rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+ rotateDelta.subVectors( rotateEnd, rotateStart );
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+
+ rotateStart.copy( rotateEnd );
+
+ scope.update();
+
+ }
+
+ function handleTouchMoveDolly( event ) {
+
+ //console.log( 'handleTouchMoveDolly' );
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyEnd.set( 0, distance );
+
+ dollyDelta.subVectors( dollyEnd, dollyStart );
+
+ if ( dollyDelta.y > 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ } else if ( dollyDelta.y < 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ }
+
+ dollyStart.copy( dollyEnd );
+
+ scope.update();
+
+ }
+
+ function handleTouchMovePan( event ) {
+
+ //console.log( 'handleTouchMovePan' );
+
+ panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ panDelta.subVectors( panEnd, panStart );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ scope.update();
+
+ }
+
+ function handleTouchEnd( event ) {
+
+ //console.log( 'handleTouchEnd' );
+
+ }
+
+ //
+ // event handlers - FSM: listen for events and reset state
+ //
+
+ function onMouseDown( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ switch ( event.button ) {
+
+ case scope.mouseButtons.ORBIT:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseDownRotate( event );
+
+ state = STATE.ROTATE;
+
+ break;
+
+ case scope.mouseButtons.ZOOM:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseDownDolly( event );
+
+ state = STATE.DOLLY;
+
+ break;
+
+ case scope.mouseButtons.PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseDownPan( event );
+
+ state = STATE.PAN;
+
+ break;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ document.addEventListener( 'mousemove', onMouseMove, false );
+ document.addEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onMouseMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ switch ( state ) {
+
+ case STATE.ROTATE:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseMoveRotate( event );
+
+ break;
+
+ case STATE.DOLLY:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseMoveDolly( event );
+
+ break;
+
+ case STATE.PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseMovePan( event );
+
+ break;
+
+ }
+
+ }
+
+ function onMouseUp( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleMouseUp( event );
+
+ document.removeEventListener( 'mousemove', onMouseMove, false );
+ document.removeEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onMouseWheel( event ) {
+
+ if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ handleMouseWheel( event );
+
+ scope.dispatchEvent( startEvent ); // not sure why these are here...
+ scope.dispatchEvent( endEvent );
+
+ }
+
+ function onKeyDown( event ) {
+
+ if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+
+ handleKeyDown( event );
+
+ }
+
+ function onTouchStart( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 1: // one-fingered touch: rotate
+
+ if ( scope.enableRotate === false ) return;
+
+ handleTouchStartRotate( event );
+
+ state = STATE.TOUCH_ROTATE;
+
+ break;
+
+ case 2: // two-fingered touch: dolly
+
+ if ( scope.enableZoom === false ) return;
+
+ handleTouchStartDolly( event );
+
+ state = STATE.TOUCH_DOLLY;
+
+ break;
+
+ case 3: // three-fingered touch: pan
+
+ if ( scope.enablePan === false ) return;
+
+ handleTouchStartPan( event );
+
+ state = STATE.TOUCH_PAN;
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onTouchMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.touches.length ) {
+
+ case 1: // one-fingered touch: rotate
+
+ if ( scope.enableRotate === false ) return;
+ if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
+
+ handleTouchMoveRotate( event );
+
+ break;
+
+ case 2: // two-fingered touch: dolly
+
+ if ( scope.enableZoom === false ) return;
+ if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
+
+ handleTouchMoveDolly( event );
+
+ break;
+
+ case 3: // three-fingered touch: pan
+
+ if ( scope.enablePan === false ) return;
+ if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
+
+ handleTouchMovePan( event );
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ }
+
+ function onTouchEnd( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleTouchEnd( event );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onContextMenu( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ }
+
+ //
+
+ scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
+
+ scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
+
+ scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
+
+ window.addEventListener( 'keydown', onKeyDown, false );
+
+ // force an update at start
+
+ this.update();
+
+};
+
+THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
+
+Object.defineProperties( THREE.OrbitControls.prototype, {
+
+ center: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
+ return this.target;
+
+ }
+
+ },
+
+ // backward compatibility
+
+ noZoom: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ return ! this.enableZoom;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ this.enableZoom = ! value;
+
+ }
+
+ },
+
+ noRotate: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+ return ! this.enableRotate;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+ this.enableRotate = ! value;
+
+ }
+
+ },
+
+ noPan: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+ return ! this.enablePan;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+ this.enablePan = ! value;
+
+ }
+
+ },
+
+ noKeys: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+ return ! this.enableKeys;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+ this.enableKeys = ! value;
+
+ }
+
+ },
+
+ staticMoving: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+ return ! this.enableDamping;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+ this.enableDamping = ! value;
+
+ }
+
+ },
+
+ dynamicDampingFactor: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+ return this.dampingFactor;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+ this.dampingFactor = value;
+
+ }
+
+ }
+
+} );
diff --git a/app/static/js/controls/OrthographicTrackballControls.js b/app/static/js/controls/OrthographicTrackballControls.js
new file mode 100644
index 0000000..0d52ebf
--- /dev/null
+++ b/app/static/js/controls/OrthographicTrackballControls.js
@@ -0,0 +1,636 @@
+/**
+ * @author Eberhard Graether / http://egraether.com/
+ * @author Mark Lundin / http://mark-lundin.com
+ * @author Patrick Fuller / http://patrick-fuller.com
+ * @author Max Smolens / https://github.com/msmolens
+ */
+
+THREE.OrthographicTrackballControls = function ( object, domElement ) {
+
+ var _this = this;
+ var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
+
+ this.object = object;
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+ // API
+
+ this.enabled = true;
+
+ this.screen = { left: 0, top: 0, width: 0, height: 0 };
+
+ this.radius = 0;
+
+ this.rotateSpeed = 1.0;
+ this.zoomSpeed = 1.2;
+
+ this.noRotate = false;
+ this.noZoom = false;
+ this.noPan = false;
+ this.noRoll = false;
+
+ this.staticMoving = false;
+ this.dynamicDampingFactor = 0.2;
+
+ this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
+
+ // internals
+
+ this.target = new THREE.Vector3();
+
+ var EPS = 0.000001;
+
+ var _changed = true;
+
+ var _state = STATE.NONE,
+ _prevState = STATE.NONE,
+
+ _eye = new THREE.Vector3(),
+
+ _rotateStart = new THREE.Vector3(),
+ _rotateEnd = new THREE.Vector3(),
+
+ _zoomStart = new THREE.Vector2(),
+ _zoomEnd = new THREE.Vector2(),
+
+ _touchZoomDistanceStart = 0,
+ _touchZoomDistanceEnd = 0,
+
+ _panStart = new THREE.Vector2(),
+ _panEnd = new THREE.Vector2();
+
+ // for reset
+
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.up0 = this.object.up.clone();
+
+ this.left0 = this.object.left;
+ this.right0 = this.object.right;
+ this.top0 = this.object.top;
+ this.bottom0 = this.object.bottom;
+
+ // events
+
+ var changeEvent = { type: 'change' };
+ var startEvent = { type: 'start' };
+ var endEvent = { type: 'end' };
+
+
+ // methods
+
+ this.handleResize = function () {
+
+ if ( this.domElement === document ) {
+
+ this.screen.left = 0;
+ this.screen.top = 0;
+ this.screen.width = window.innerWidth;
+ this.screen.height = window.innerHeight;
+
+ } else {
+
+ var box = this.domElement.getBoundingClientRect();
+ // adjustments come from similar code in the jquery offset() function
+ var d = this.domElement.ownerDocument.documentElement;
+ this.screen.left = box.left + window.pageXOffset - d.clientLeft;
+ this.screen.top = box.top + window.pageYOffset - d.clientTop;
+ this.screen.width = box.width;
+ this.screen.height = box.height;
+
+ }
+
+ this.radius = 0.5 * Math.min( this.screen.width, this.screen.height );
+
+ this.left0 = this.object.left;
+ this.right0 = this.object.right;
+ this.top0 = this.object.top;
+ this.bottom0 = this.object.bottom;
+
+ };
+
+ this.handleEvent = function ( event ) {
+
+ if ( typeof this[ event.type ] == 'function' ) {
+
+ this[ event.type ]( event );
+
+ }
+
+ };
+
+ var getMouseOnScreen = ( function () {
+
+ var vector = new THREE.Vector2();
+
+ return function getMouseOnScreen( pageX, pageY ) {
+
+ vector.set(
+ ( pageX - _this.screen.left ) / _this.screen.width,
+ ( pageY - _this.screen.top ) / _this.screen.height
+ );
+
+ return vector;
+
+ };
+
+ }() );
+
+ var getMouseProjectionOnBall = ( function () {
+
+ var vector = new THREE.Vector3();
+ var objectUp = new THREE.Vector3();
+ var mouseOnBall = new THREE.Vector3();
+
+ return function getMouseProjectionOnBall( pageX, pageY ) {
+
+ mouseOnBall.set(
+ ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / _this.radius,
+ ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / _this.radius,
+ 0.0
+ );
+
+ var length = mouseOnBall.length();
+
+ if ( _this.noRoll ) {
+
+ if ( length < Math.SQRT1_2 ) {
+
+ mouseOnBall.z = Math.sqrt( 1.0 - length * length );
+
+ } else {
+
+ mouseOnBall.z = .5 / length;
+
+ }
+
+ } else if ( length > 1.0 ) {
+
+ mouseOnBall.normalize();
+
+ } else {
+
+ mouseOnBall.z = Math.sqrt( 1.0 - length * length );
+
+ }
+
+ _eye.copy( _this.object.position ).sub( _this.target );
+
+ vector.copy( _this.object.up ).setLength( mouseOnBall.y );
+ vector.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) );
+ vector.add( _eye.setLength( mouseOnBall.z ) );
+
+ return vector;
+
+ };
+
+ }() );
+
+ this.rotateCamera = ( function() {
+
+ var axis = new THREE.Vector3(),
+ quaternion = new THREE.Quaternion();
+
+
+ return function rotateCamera() {
+
+ var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() );
+
+ if ( angle ) {
+
+ axis.crossVectors( _rotateStart, _rotateEnd ).normalize();
+
+ angle *= _this.rotateSpeed;
+
+ quaternion.setFromAxisAngle( axis, - angle );
+
+ _eye.applyQuaternion( quaternion );
+ _this.object.up.applyQuaternion( quaternion );
+
+ _rotateEnd.applyQuaternion( quaternion );
+
+ if ( _this.staticMoving ) {
+
+ _rotateStart.copy( _rotateEnd );
+
+ } else {
+
+ quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) );
+ _rotateStart.applyQuaternion( quaternion );
+
+ }
+
+ _changed = true;
+
+ }
+
+ }
+
+ }() );
+
+ this.zoomCamera = function () {
+
+ if ( _state === STATE.TOUCH_ZOOM_PAN ) {
+
+ var factor = _touchZoomDistanceEnd / _touchZoomDistanceStart;
+ _touchZoomDistanceStart = _touchZoomDistanceEnd;
+
+ _this.object.zoom *= factor;
+
+ _changed = true;
+
+ } else {
+
+ var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
+
+ if ( Math.abs( factor - 1.0 ) > EPS && factor > 0.0 ) {
+
+ _this.object.zoom /= factor;
+
+ if ( _this.staticMoving ) {
+
+ _zoomStart.copy( _zoomEnd );
+
+ } else {
+
+ _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
+
+ }
+
+ _changed = true;
+
+ }
+
+ }
+
+ };
+
+ this.panCamera = ( function() {
+
+ var mouseChange = new THREE.Vector2(),
+ objectUp = new THREE.Vector3(),
+ pan = new THREE.Vector3();
+
+ return function panCamera() {
+
+ mouseChange.copy( _panEnd ).sub( _panStart );
+
+ if ( mouseChange.lengthSq() ) {
+
+ // Scale movement to keep clicked/dragged position under cursor
+ var scale_x = ( _this.object.right - _this.object.left ) / _this.object.zoom;
+ var scale_y = ( _this.object.top - _this.object.bottom ) / _this.object.zoom;
+ mouseChange.x *= scale_x;
+ mouseChange.y *= scale_y;
+
+ pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
+ pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
+
+ _this.object.position.add( pan );
+ _this.target.add( pan );
+
+ if ( _this.staticMoving ) {
+
+ _panStart.copy( _panEnd );
+
+ } else {
+
+ _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
+
+ }
+
+ _changed = true;
+
+ }
+
+ }
+
+ }() );
+
+ this.update = function () {
+
+ _eye.subVectors( _this.object.position, _this.target );
+
+ if ( ! _this.noRotate ) {
+
+ _this.rotateCamera();
+
+ }
+
+ if ( ! _this.noZoom ) {
+
+ _this.zoomCamera();
+
+ if ( _changed ) {
+
+ _this.object.updateProjectionMatrix();
+
+ }
+
+ }
+
+ if ( ! _this.noPan ) {
+
+ _this.panCamera();
+
+ }
+
+ _this.object.position.addVectors( _this.target, _eye );
+
+ _this.object.lookAt( _this.target );
+
+ if ( _changed ) {
+
+ _this.dispatchEvent( changeEvent );
+
+ _changed = false;
+
+ }
+
+ };
+
+ this.reset = function () {
+
+ _state = STATE.NONE;
+ _prevState = STATE.NONE;
+
+ _this.target.copy( _this.target0 );
+ _this.object.position.copy( _this.position0 );
+ _this.object.up.copy( _this.up0 );
+
+ _eye.subVectors( _this.object.position, _this.target );
+
+ _this.object.left = _this.left0;
+ _this.object.right = _this.right0;
+ _this.object.top = _this.top0;
+ _this.object.bottom = _this.bottom0;
+
+ _this.object.lookAt( _this.target );
+
+ _this.dispatchEvent( changeEvent );
+
+ _changed = false;
+
+ };
+
+ // listeners
+
+ function keydown( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ window.removeEventListener( 'keydown', keydown );
+
+ _prevState = _state;
+
+ if ( _state !== STATE.NONE ) {
+
+ return;
+
+ } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
+
+ _state = STATE.ROTATE;
+
+ } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
+
+ _state = STATE.ZOOM;
+
+ } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
+
+ _state = STATE.PAN;
+
+ }
+
+ }
+
+ function keyup( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ _state = _prevState;
+
+ window.addEventListener( 'keydown', keydown, false );
+
+ }
+
+ function mousedown( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( _state === STATE.NONE ) {
+
+ _state = event.button;
+
+ }
+
+ if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+ _rotateStart.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );
+ _rotateEnd.copy( _rotateStart );
+
+ } else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+ _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+ _zoomEnd.copy( _zoomStart );
+
+ } else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+ _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+ _panEnd.copy( _panStart )
+
+ }
+
+ document.addEventListener( 'mousemove', mousemove, false );
+ document.addEventListener( 'mouseup', mouseup, false );
+
+ _this.dispatchEvent( startEvent );
+
+ }
+
+ function mousemove( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+ _rotateEnd.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );
+
+ } else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+ _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+ } else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+ _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+ }
+
+ }
+
+ function mouseup( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ _state = STATE.NONE;
+
+ document.removeEventListener( 'mousemove', mousemove );
+ document.removeEventListener( 'mouseup', mouseup );
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function mousewheel( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ _zoomStart.y += event.deltaY * 0.01;
+ _this.dispatchEvent( startEvent );
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function touchstart( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ _state = STATE.TOUCH_ROTATE;
+ _rotateStart.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ _rotateEnd.copy( _rotateStart );
+ break;
+
+ case 2:
+ _state = STATE.TOUCH_ZOOM_PAN;
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
+
+ var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+ var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+ _panStart.copy( getMouseOnScreen( x, y ) );
+ _panEnd.copy( _panStart );
+ break;
+
+ default:
+ _state = STATE.NONE;
+
+ }
+ _this.dispatchEvent( startEvent );
+
+ }
+
+ function touchmove( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ break;
+
+ case 2:
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
+
+ var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+ var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+ _panEnd.copy( getMouseOnScreen( x, y ) );
+ break;
+
+ default:
+ _state = STATE.NONE;
+
+ }
+
+ }
+
+ function touchend( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ _rotateStart.copy( _rotateEnd );
+ break;
+
+ case 2:
+ _touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
+
+ var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+ var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+ _panEnd.copy( getMouseOnScreen( x, y ) );
+ _panStart.copy( _panEnd );
+ break;
+
+ }
+
+ _state = STATE.NONE;
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function contextmenu( event ) {
+
+ event.preventDefault();
+
+ }
+
+ this.dispose = function() {
+
+ this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.removeEventListener( 'mousedown', mousedown, false );
+ this.domElement.removeEventListener( 'wheel', mousewheel, false );
+
+ this.domElement.removeEventListener( 'touchstart', touchstart, false );
+ this.domElement.removeEventListener( 'touchend', touchend, false );
+ this.domElement.removeEventListener( 'touchmove', touchmove, false );
+
+ document.removeEventListener( 'mousemove', mousemove, false );
+ document.removeEventListener( 'mouseup', mouseup, false );
+
+ window.removeEventListener( 'keydown', keydown, false );
+ window.removeEventListener( 'keyup', keyup, false );
+
+ };
+
+ this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.addEventListener( 'mousedown', mousedown, false );
+ this.domElement.addEventListener( 'wheel', mousewheel, false );
+
+ this.domElement.addEventListener( 'touchstart', touchstart, false );
+ this.domElement.addEventListener( 'touchend', touchend, false );
+ this.domElement.addEventListener( 'touchmove', touchmove, false );
+
+ window.addEventListener( 'keydown', keydown, false );
+ window.addEventListener( 'keyup', keyup, false );
+
+ this.handleResize();
+
+ // force an update at start
+ this.update();
+
+};
+
+THREE.OrthographicTrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrthographicTrackballControls.prototype.constructor = THREE.OrthographicTrackballControls;
diff --git a/app/static/js/controls/PointerLockControls.js b/app/static/js/controls/PointerLockControls.js
new file mode 100644
index 0000000..b0d7dc8
--- /dev/null
+++ b/app/static/js/controls/PointerLockControls.js
@@ -0,0 +1,69 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.PointerLockControls = function ( camera ) {
+
+ var scope = this;
+
+ camera.rotation.set( 0, 0, 0 );
+
+ var pitchObject = new THREE.Object3D();
+ pitchObject.add( camera );
+
+ var yawObject = new THREE.Object3D();
+ yawObject.position.y = 10;
+ yawObject.add( pitchObject );
+
+ var PI_2 = Math.PI / 2;
+
+ var onMouseMove = function ( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
+ var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
+
+ yawObject.rotation.y -= movementX * 0.002;
+ pitchObject.rotation.x -= movementY * 0.002;
+
+ pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) );
+
+ };
+
+ this.dispose = function() {
+
+ document.removeEventListener( 'mousemove', onMouseMove, false );
+
+ };
+
+ document.addEventListener( 'mousemove', onMouseMove, false );
+
+ this.enabled = false;
+
+ this.getObject = function () {
+
+ return yawObject;
+
+ };
+
+ this.getDirection = function() {
+
+ // assumes the camera itself is not rotated
+
+ var direction = new THREE.Vector3( 0, 0, - 1 );
+ var rotation = new THREE.Euler( 0, 0, 0, "YXZ" );
+
+ return function( v ) {
+
+ rotation.set( pitchObject.rotation.x, yawObject.rotation.y, 0 );
+
+ v.copy( direction ).applyEuler( rotation );
+
+ return v;
+
+ };
+
+ }();
+
+};
diff --git a/app/static/js/controls/TrackballControls.js b/app/static/js/controls/TrackballControls.js
new file mode 100644
index 0000000..5d45829
--- /dev/null
+++ b/app/static/js/controls/TrackballControls.js
@@ -0,0 +1,625 @@
+/**
+ * @author Eberhard Graether / http://egraether.com/
+ * @author Mark Lundin / http://mark-lundin.com
+ * @author Simone Manini / http://daron1337.github.io
+ * @author Luca Antiga / http://lantiga.github.io
+ */
+
+THREE.TrackballControls = function ( object, domElement ) {
+
+ var _this = this;
+ var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
+
+ this.object = object;
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+ // API
+
+ this.enabled = true;
+
+ this.screen = { left: 0, top: 0, width: 0, height: 0 };
+
+ this.rotateSpeed = 1.0;
+ this.zoomSpeed = 1.2;
+ this.panSpeed = 0.3;
+
+ this.noRotate = false;
+ this.noZoom = false;
+ this.noPan = false;
+
+ this.staticMoving = false;
+ this.dynamicDampingFactor = 0.2;
+
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
+
+ // internals
+
+ this.target = new THREE.Vector3();
+
+ var EPS = 0.000001;
+
+ var lastPosition = new THREE.Vector3();
+
+ var _state = STATE.NONE,
+ _prevState = STATE.NONE,
+
+ _eye = new THREE.Vector3(),
+
+ _movePrev = new THREE.Vector2(),
+ _moveCurr = new THREE.Vector2(),
+
+ _lastAxis = new THREE.Vector3(),
+ _lastAngle = 0,
+
+ _zoomStart = new THREE.Vector2(),
+ _zoomEnd = new THREE.Vector2(),
+
+ _touchZoomDistanceStart = 0,
+ _touchZoomDistanceEnd = 0,
+
+ _panStart = new THREE.Vector2(),
+ _panEnd = new THREE.Vector2();
+
+ // for reset
+
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.up0 = this.object.up.clone();
+
+ // events
+
+ var changeEvent = { type: 'change' };
+ var startEvent = { type: 'start' };
+ var endEvent = { type: 'end' };
+
+
+ // methods
+
+ this.handleResize = function () {
+
+ if ( this.domElement === document ) {
+
+ this.screen.left = 0;
+ this.screen.top = 0;
+ this.screen.width = window.innerWidth;
+ this.screen.height = window.innerHeight;
+
+ } else {
+
+ var box = this.domElement.getBoundingClientRect();
+ // adjustments come from similar code in the jquery offset() function
+ var d = this.domElement.ownerDocument.documentElement;
+ this.screen.left = box.left + window.pageXOffset - d.clientLeft;
+ this.screen.top = box.top + window.pageYOffset - d.clientTop;
+ this.screen.width = box.width;
+ this.screen.height = box.height;
+
+ }
+
+ };
+
+ this.handleEvent = function ( event ) {
+
+ if ( typeof this[ event.type ] == 'function' ) {
+
+ this[ event.type ]( event );
+
+ }
+
+ };
+
+ var getMouseOnScreen = ( function () {
+
+ var vector = new THREE.Vector2();
+
+ return function getMouseOnScreen( pageX, pageY ) {
+
+ vector.set(
+ ( pageX - _this.screen.left ) / _this.screen.width,
+ ( pageY - _this.screen.top ) / _this.screen.height
+ );
+
+ return vector;
+
+ };
+
+ }() );
+
+ var getMouseOnCircle = ( function () {
+
+ var vector = new THREE.Vector2();
+
+ return function getMouseOnCircle( pageX, pageY ) {
+
+ vector.set(
+ ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ),
+ ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional
+ );
+
+ return vector;
+
+ };
+
+ }() );
+
+ this.rotateCamera = ( function() {
+
+ var axis = new THREE.Vector3(),
+ quaternion = new THREE.Quaternion(),
+ eyeDirection = new THREE.Vector3(),
+ objectUpDirection = new THREE.Vector3(),
+ objectSidewaysDirection = new THREE.Vector3(),
+ moveDirection = new THREE.Vector3(),
+ angle;
+
+ return function rotateCamera() {
+
+ moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
+ angle = moveDirection.length();
+
+ if ( angle ) {
+
+ _eye.copy( _this.object.position ).sub( _this.target );
+
+ eyeDirection.copy( _eye ).normalize();
+ objectUpDirection.copy( _this.object.up ).normalize();
+ objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
+
+ objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
+ objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
+
+ moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
+
+ axis.crossVectors( moveDirection, _eye ).normalize();
+
+ angle *= _this.rotateSpeed;
+ quaternion.setFromAxisAngle( axis, angle );
+
+ _eye.applyQuaternion( quaternion );
+ _this.object.up.applyQuaternion( quaternion );
+
+ _lastAxis.copy( axis );
+ _lastAngle = angle;
+
+ } else if ( ! _this.staticMoving && _lastAngle ) {
+
+ _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor );
+ _eye.copy( _this.object.position ).sub( _this.target );
+ quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
+ _eye.applyQuaternion( quaternion );
+ _this.object.up.applyQuaternion( quaternion );
+
+ }
+
+ _movePrev.copy( _moveCurr );
+
+ };
+
+ }() );
+
+
+ this.zoomCamera = function () {
+
+ var factor;
+
+ if ( _state === STATE.TOUCH_ZOOM_PAN ) {
+
+ factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
+ _touchZoomDistanceStart = _touchZoomDistanceEnd;
+ _eye.multiplyScalar( factor );
+
+ } else {
+
+ factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
+
+ if ( factor !== 1.0 && factor > 0.0 ) {
+
+ _eye.multiplyScalar( factor );
+
+ }
+
+ if ( _this.staticMoving ) {
+
+ _zoomStart.copy( _zoomEnd );
+
+ } else {
+
+ _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
+
+ }
+
+ }
+
+ };
+
+ this.panCamera = ( function() {
+
+ var mouseChange = new THREE.Vector2(),
+ objectUp = new THREE.Vector3(),
+ pan = new THREE.Vector3();
+
+ return function panCamera() {
+
+ mouseChange.copy( _panEnd ).sub( _panStart );
+
+ if ( mouseChange.lengthSq() ) {
+
+ mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );
+
+ pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
+ pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
+
+ _this.object.position.add( pan );
+ _this.target.add( pan );
+
+ if ( _this.staticMoving ) {
+
+ _panStart.copy( _panEnd );
+
+ } else {
+
+ _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
+
+ }
+
+ }
+
+ };
+
+ }() );
+
+ this.checkDistances = function () {
+
+ if ( ! _this.noZoom || ! _this.noPan ) {
+
+ if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {
+
+ _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );
+ _zoomStart.copy( _zoomEnd );
+
+ }
+
+ if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {
+
+ _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );
+ _zoomStart.copy( _zoomEnd );
+
+ }
+
+ }
+
+ };
+
+ this.update = function () {
+
+ _eye.subVectors( _this.object.position, _this.target );
+
+ if ( ! _this.noRotate ) {
+
+ _this.rotateCamera();
+
+ }
+
+ if ( ! _this.noZoom ) {
+
+ _this.zoomCamera();
+
+ }
+
+ if ( ! _this.noPan ) {
+
+ _this.panCamera();
+
+ }
+
+ _this.object.position.addVectors( _this.target, _eye );
+
+ _this.checkDistances();
+
+ _this.object.lookAt( _this.target );
+
+ if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {
+
+ _this.dispatchEvent( changeEvent );
+
+ lastPosition.copy( _this.object.position );
+
+ }
+
+ };
+
+ this.reset = function () {
+
+ _state = STATE.NONE;
+ _prevState = STATE.NONE;
+
+ _this.target.copy( _this.target0 );
+ _this.object.position.copy( _this.position0 );
+ _this.object.up.copy( _this.up0 );
+
+ _eye.subVectors( _this.object.position, _this.target );
+
+ _this.object.lookAt( _this.target );
+
+ _this.dispatchEvent( changeEvent );
+
+ lastPosition.copy( _this.object.position );
+
+ };
+
+ // listeners
+
+ function keydown( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ window.removeEventListener( 'keydown', keydown );
+
+ _prevState = _state;
+
+ if ( _state !== STATE.NONE ) {
+
+ return;
+
+ } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
+
+ _state = STATE.ROTATE;
+
+ } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
+
+ _state = STATE.ZOOM;
+
+ } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
+
+ _state = STATE.PAN;
+
+ }
+
+ }
+
+ function keyup( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ _state = _prevState;
+
+ window.addEventListener( 'keydown', keydown, false );
+
+ }
+
+ function mousedown( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( _state === STATE.NONE ) {
+
+ _state = event.button;
+
+ }
+
+ if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+ _movePrev.copy( _moveCurr );
+
+ } else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+ _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+ _zoomEnd.copy( _zoomStart );
+
+ } else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+ _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+ _panEnd.copy( _panStart );
+
+ }
+
+ document.addEventListener( 'mousemove', mousemove, false );
+ document.addEventListener( 'mouseup', mouseup, false );
+
+ _this.dispatchEvent( startEvent );
+
+ }
+
+ function mousemove( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+ _movePrev.copy( _moveCurr );
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+
+ } else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+ _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+ } else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+ _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+ }
+
+ }
+
+ function mouseup( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ _state = STATE.NONE;
+
+ document.removeEventListener( 'mousemove', mousemove );
+ document.removeEventListener( 'mouseup', mouseup );
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function mousewheel( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.deltaMode ) {
+
+ case 2:
+ // Zoom in pages
+ _zoomStart.y -= event.deltaY * 0.025;
+ break;
+
+ case 1:
+ // Zoom in lines
+ _zoomStart.y -= event.deltaY * 0.01;
+ break;
+
+ default:
+ // undefined, 0, assume pixels
+ _zoomStart.y -= event.deltaY * 0.00025;
+ break;
+
+ }
+
+ _this.dispatchEvent( startEvent );
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function touchstart( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ _state = STATE.TOUCH_ROTATE;
+ _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ _movePrev.copy( _moveCurr );
+ break;
+
+ default: // 2 or more
+ _state = STATE.TOUCH_ZOOM_PAN;
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
+
+ var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+ var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+ _panStart.copy( getMouseOnScreen( x, y ) );
+ _panEnd.copy( _panStart );
+ break;
+
+ }
+
+ _this.dispatchEvent( startEvent );
+
+ }
+
+ function touchmove( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ _movePrev.copy( _moveCurr );
+ _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ break;
+
+ default: // 2 or more
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
+
+ var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+ var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+ _panEnd.copy( getMouseOnScreen( x, y ) );
+ break;
+
+ }
+
+ }
+
+ function touchend( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+
+ case 0:
+ _state = STATE.NONE;
+ break;
+
+ case 1:
+ _state = STATE.TOUCH_ROTATE;
+ _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+ _movePrev.copy( _moveCurr );
+ break;
+
+ }
+
+ _this.dispatchEvent( endEvent );
+
+ }
+
+ function contextmenu( event ) {
+
+ if ( _this.enabled === false ) return;
+
+ event.preventDefault();
+
+ }
+
+ this.dispose = function() {
+
+ this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.removeEventListener( 'mousedown', mousedown, false );
+ this.domElement.removeEventListener( 'wheel', mousewheel, false );
+
+ this.domElement.removeEventListener( 'touchstart', touchstart, false );
+ this.domElement.removeEventListener( 'touchend', touchend, false );
+ this.domElement.removeEventListener( 'touchmove', touchmove, false );
+
+ document.removeEventListener( 'mousemove', mousemove, false );
+ document.removeEventListener( 'mouseup', mouseup, false );
+
+ window.removeEventListener( 'keydown', keydown, false );
+ window.removeEventListener( 'keyup', keyup, false );
+
+ };
+
+ this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+ this.domElement.addEventListener( 'mousedown', mousedown, false );
+ this.domElement.addEventListener( 'wheel', mousewheel, false );
+
+ this.domElement.addEventListener( 'touchstart', touchstart, false );
+ this.domElement.addEventListener( 'touchend', touchend, false );
+ this.domElement.addEventListener( 'touchmove', touchmove, false );
+
+ window.addEventListener( 'keydown', keydown, false );
+ window.addEventListener( 'keyup', keyup, false );
+
+ this.handleResize();
+
+ // force an update at start
+ this.update();
+
+};
+
+THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;
diff --git a/app/static/js/controls/TransformControls.js b/app/static/js/controls/TransformControls.js
new file mode 100644
index 0000000..3b74d84
--- /dev/null
+++ b/app/static/js/controls/TransformControls.js
@@ -0,0 +1,1149 @@
+/**
+ * @author arodic / https://github.com/arodic
+ */
+
+( function () {
+
+ 'use strict';
+
+ var GizmoMaterial = function ( parameters ) {
+
+ THREE.MeshBasicMaterial.call( this );
+
+ this.depthTest = false;
+ this.depthWrite = false;
+ this.side = THREE.FrontSide;
+ this.transparent = true;
+
+ this.setValues( parameters );
+
+ this.oldColor = this.color.clone();
+ this.oldOpacity = this.opacity;
+
+ this.highlight = function( highlighted ) {
+
+ if ( highlighted ) {
+
+ this.color.setRGB( 1, 1, 0 );
+ this.opacity = 1;
+
+ } else {
+
+ this.color.copy( this.oldColor );
+ this.opacity = this.oldOpacity;
+
+ }
+
+ };
+
+ };
+
+ GizmoMaterial.prototype = Object.create( THREE.MeshBasicMaterial.prototype );
+ GizmoMaterial.prototype.constructor = GizmoMaterial;
+
+
+ var GizmoLineMaterial = function ( parameters ) {
+
+ THREE.LineBasicMaterial.call( this );
+
+ this.depthTest = false;
+ this.depthWrite = false;
+ this.transparent = true;
+ this.linewidth = 1;
+
+ this.setValues( parameters );
+
+ this.oldColor = this.color.clone();
+ this.oldOpacity = this.opacity;
+
+ this.highlight = function( highlighted ) {
+
+ if ( highlighted ) {
+
+ this.color.setRGB( 1, 1, 0 );
+ this.opacity = 1;
+
+ } else {
+
+ this.color.copy( this.oldColor );
+ this.opacity = this.oldOpacity;
+
+ }
+
+ };
+
+ };
+
+ GizmoLineMaterial.prototype = Object.create( THREE.LineBasicMaterial.prototype );
+ GizmoLineMaterial.prototype.constructor = GizmoLineMaterial;
+
+
+ var pickerMaterial = new GizmoMaterial( { visible: false, transparent: false } );
+
+
+ THREE.TransformGizmo = function () {
+
+ this.init = function () {
+
+ THREE.Object3D.call( this );
+
+ this.handles = new THREE.Object3D();
+ this.pickers = new THREE.Object3D();
+ this.planes = new THREE.Object3D();
+
+ this.add( this.handles );
+ this.add( this.pickers );
+ this.add( this.planes );
+
+ //// PLANES
+
+ var planeGeometry = new THREE.PlaneBufferGeometry( 50, 50, 2, 2 );
+ var planeMaterial = new THREE.MeshBasicMaterial( { visible: false, side: THREE.DoubleSide } );
+
+ var planes = {
+ "XY": new THREE.Mesh( planeGeometry, planeMaterial ),
+ "YZ": new THREE.Mesh( planeGeometry, planeMaterial ),
+ "XZ": new THREE.Mesh( planeGeometry, planeMaterial ),
+ "XYZE": new THREE.Mesh( planeGeometry, planeMaterial )
+ };
+
+ this.activePlane = planes[ "XYZE" ];
+
+ planes[ "YZ" ].rotation.set( 0, Math.PI / 2, 0 );
+ planes[ "XZ" ].rotation.set( - Math.PI / 2, 0, 0 );
+
+ for ( var i in planes ) {
+
+ planes[ i ].name = i;
+ this.planes.add( planes[ i ] );
+ this.planes[ i ] = planes[ i ];
+
+ }
+
+ //// HANDLES AND PICKERS
+
+ var setupGizmos = function( gizmoMap, parent ) {
+
+ for ( var name in gizmoMap ) {
+
+ for ( i = gizmoMap[ name ].length; i --; ) {
+
+ var object = gizmoMap[ name ][ i ][ 0 ];
+ var position = gizmoMap[ name ][ i ][ 1 ];
+ var rotation = gizmoMap[ name ][ i ][ 2 ];
+
+ object.name = name;
+
+ if ( position ) object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] );
+ if ( rotation ) object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] );
+
+ parent.add( object );
+
+ }
+
+ }
+
+ };
+
+ setupGizmos( this.handleGizmos, this.handles );
+ setupGizmos( this.pickerGizmos, this.pickers );
+
+ // reset Transformations
+
+ this.traverse( function ( child ) {
+
+ if ( child instanceof THREE.Mesh ) {
+
+ child.updateMatrix();
+
+ var tempGeometry = child.geometry.clone();
+ tempGeometry.applyMatrix( child.matrix );
+ child.geometry = tempGeometry;
+
+ child.position.set( 0, 0, 0 );
+ child.rotation.set( 0, 0, 0 );
+ child.scale.set( 1, 1, 1 );
+
+ }
+
+ } );
+
+ };
+
+ this.highlight = function ( axis ) {
+
+ this.traverse( function( child ) {
+
+ if ( child.material && child.material.highlight ) {
+
+ if ( child.name === axis ) {
+
+ child.material.highlight( true );
+
+ } else {
+
+ child.material.highlight( false );
+
+ }
+
+ }
+
+ } );
+
+ };
+
+ };
+
+ THREE.TransformGizmo.prototype = Object.create( THREE.Object3D.prototype );
+ THREE.TransformGizmo.prototype.constructor = THREE.TransformGizmo;
+
+ THREE.TransformGizmo.prototype.update = function ( rotation, eye ) {
+
+ var vec1 = new THREE.Vector3( 0, 0, 0 );
+ var vec2 = new THREE.Vector3( 0, 1, 0 );
+ var lookAtMatrix = new THREE.Matrix4();
+
+ this.traverse( function( child ) {
+
+ if ( child.name.search( "E" ) !== - 1 ) {
+
+ child.quaternion.setFromRotationMatrix( lookAtMatrix.lookAt( eye, vec1, vec2 ) );
+
+ } else if ( child.name.search( "X" ) !== - 1 || child.name.search( "Y" ) !== - 1 || child.name.search( "Z" ) !== - 1 ) {
+
+ child.quaternion.setFromEuler( rotation );
+
+ }
+
+ } );
+
+ };
+
+ THREE.TransformGizmoTranslate = function () {
+
+ THREE.TransformGizmo.call( this );
+
+ var arrowGeometry = new THREE.Geometry();
+ var mesh = new THREE.Mesh( new THREE.CylinderGeometry( 0, 0.05, 0.2, 12, 1, false ) );
+ mesh.position.y = 0.5;
+ mesh.updateMatrix();
+
+ arrowGeometry.merge( mesh.geometry, mesh.matrix );
+
+ var lineXGeometry = new THREE.BufferGeometry();
+ lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) );
+
+ var lineYGeometry = new THREE.BufferGeometry();
+ lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
+
+ var lineZGeometry = new THREE.BufferGeometry();
+ lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );
+
+ this.handleGizmos = {
+
+ X: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
+ [ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
+ ],
+
+ Y: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
+ [ new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
+ ],
+
+ Z: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
+ [ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
+ ],
+
+ XYZ: [
+ [ new THREE.Mesh( new THREE.OctahedronGeometry( 0.1, 0 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ 0, 0, 0 ] ]
+ ],
+
+ XY: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ), [ 0.15, 0.15, 0 ] ]
+ ],
+
+ YZ: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0x00ffff, opacity: 0.25 } ) ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ] ]
+ ],
+
+ XZ: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xff00ff, opacity: 0.25 } ) ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ] ]
+ ]
+
+ };
+
+ this.pickerGizmos = {
+
+ X: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
+ ],
+
+ Y: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
+ ],
+
+ Z: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
+ ],
+
+ XYZ: [
+ [ new THREE.Mesh( new THREE.OctahedronGeometry( 0.2, 0 ), pickerMaterial ) ]
+ ],
+
+ XY: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0.2, 0 ] ]
+ ],
+
+ YZ: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0, 0.2, 0.2 ], [ 0, Math.PI / 2, 0 ] ]
+ ],
+
+ XZ: [
+ [ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0, 0.2 ], [ - Math.PI / 2, 0, 0 ] ]
+ ]
+
+ };
+
+ this.setActivePlane = function ( axis, eye ) {
+
+ var tempMatrix = new THREE.Matrix4();
+ eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );
+
+ if ( axis === "X" ) {
+
+ this.activePlane = this.planes[ "XY" ];
+
+ if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];
+
+ }
+
+ if ( axis === "Y" ) {
+
+ this.activePlane = this.planes[ "XY" ];
+
+ if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];
+
+ }
+
+ if ( axis === "Z" ) {
+
+ this.activePlane = this.planes[ "XZ" ];
+
+ if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];
+
+ }
+
+ if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];
+
+ if ( axis === "XY" ) this.activePlane = this.planes[ "XY" ];
+
+ if ( axis === "YZ" ) this.activePlane = this.planes[ "YZ" ];
+
+ if ( axis === "XZ" ) this.activePlane = this.planes[ "XZ" ];
+
+ };
+
+ this.init();
+
+ };
+
+ THREE.TransformGizmoTranslate.prototype = Object.create( THREE.TransformGizmo.prototype );
+ THREE.TransformGizmoTranslate.prototype.constructor = THREE.TransformGizmoTranslate;
+
+ THREE.TransformGizmoRotate = function () {
+
+ THREE.TransformGizmo.call( this );
+
+ var CircleGeometry = function ( radius, facing, arc ) {
+
+ var geometry = new THREE.BufferGeometry();
+ var vertices = [];
+ arc = arc ? arc : 1;
+
+ for ( var i = 0; i <= 64 * arc; ++ i ) {
+
+ if ( facing === 'x' ) vertices.push( 0, Math.cos( i / 32 * Math.PI ) * radius, Math.sin( i / 32 * Math.PI ) * radius );
+ if ( facing === 'y' ) vertices.push( Math.cos( i / 32 * Math.PI ) * radius, 0, Math.sin( i / 32 * Math.PI ) * radius );
+ if ( facing === 'z' ) vertices.push( Math.sin( i / 32 * Math.PI ) * radius, Math.cos( i / 32 * Math.PI ) * radius, 0 );
+
+ }
+
+ geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
+ return geometry;
+
+ };
+
+ this.handleGizmos = {
+
+ X: [
+ [ new THREE.Line( new CircleGeometry( 1, 'x', 0.5 ), new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
+ ],
+
+ Y: [
+ [ new THREE.Line( new CircleGeometry( 1, 'y', 0.5 ), new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
+ ],
+
+ Z: [
+ [ new THREE.Line( new CircleGeometry( 1, 'z', 0.5 ), new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
+ ],
+
+ E: [
+ [ new THREE.Line( new CircleGeometry( 1.25, 'z', 1 ), new GizmoLineMaterial( { color: 0xcccc00 } ) ) ]
+ ],
+
+ XYZE: [
+ [ new THREE.Line( new CircleGeometry( 1, 'z', 1 ), new GizmoLineMaterial( { color: 0x787878 } ) ) ]
+ ]
+
+ };
+
+ this.pickerGizmos = {
+
+ X: [
+ [ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ] ]
+ ],
+
+ Y: [
+ [ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ] ]
+ ],
+
+ Z: [
+ [ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
+ ],
+
+ E: [
+ [ new THREE.Mesh( new THREE.TorusBufferGeometry( 1.25, 0.12, 2, 24 ), pickerMaterial ) ]
+ ],
+
+ XYZE: [
+ [ new THREE.Mesh() ]// TODO
+ ]
+
+ };
+
+ this.setActivePlane = function ( axis ) {
+
+ if ( axis === "E" ) this.activePlane = this.planes[ "XYZE" ];
+
+ if ( axis === "X" ) this.activePlane = this.planes[ "YZ" ];
+
+ if ( axis === "Y" ) this.activePlane = this.planes[ "XZ" ];
+
+ if ( axis === "Z" ) this.activePlane = this.planes[ "XY" ];
+
+ };
+
+ this.update = function ( rotation, eye2 ) {
+
+ THREE.TransformGizmo.prototype.update.apply( this, arguments );
+
+ var tempMatrix = new THREE.Matrix4();
+ var worldRotation = new THREE.Euler( 0, 0, 1 );
+ var tempQuaternion = new THREE.Quaternion();
+ var unitX = new THREE.Vector3( 1, 0, 0 );
+ var unitY = new THREE.Vector3( 0, 1, 0 );
+ var unitZ = new THREE.Vector3( 0, 0, 1 );
+ var quaternionX = new THREE.Quaternion();
+ var quaternionY = new THREE.Quaternion();
+ var quaternionZ = new THREE.Quaternion();
+ var eye = eye2.clone();
+
+ worldRotation.copy( this.planes[ "XY" ].rotation );
+ tempQuaternion.setFromEuler( worldRotation );
+
+ tempMatrix.makeRotationFromQuaternion( tempQuaternion ).getInverse( tempMatrix );
+ eye.applyMatrix4( tempMatrix );
+
+ this.traverse( function( child ) {
+
+ tempQuaternion.setFromEuler( worldRotation );
+
+ if ( child.name === "X" ) {
+
+ quaternionX.setFromAxisAngle( unitX, Math.atan2( - eye.y, eye.z ) );
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
+ child.quaternion.copy( tempQuaternion );
+
+ }
+
+ if ( child.name === "Y" ) {
+
+ quaternionY.setFromAxisAngle( unitY, Math.atan2( eye.x, eye.z ) );
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
+ child.quaternion.copy( tempQuaternion );
+
+ }
+
+ if ( child.name === "Z" ) {
+
+ quaternionZ.setFromAxisAngle( unitZ, Math.atan2( eye.y, eye.x ) );
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );
+ child.quaternion.copy( tempQuaternion );
+
+ }
+
+ } );
+
+ };
+
+ this.init();
+
+ };
+
+ THREE.TransformGizmoRotate.prototype = Object.create( THREE.TransformGizmo.prototype );
+ THREE.TransformGizmoRotate.prototype.constructor = THREE.TransformGizmoRotate;
+
+ THREE.TransformGizmoScale = function () {
+
+ THREE.TransformGizmo.call( this );
+
+ var arrowGeometry = new THREE.Geometry();
+ var mesh = new THREE.Mesh( new THREE.BoxGeometry( 0.125, 0.125, 0.125 ) );
+ mesh.position.y = 0.5;
+ mesh.updateMatrix();
+
+ arrowGeometry.merge( mesh.geometry, mesh.matrix );
+
+ var lineXGeometry = new THREE.BufferGeometry();
+ lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) );
+
+ var lineYGeometry = new THREE.BufferGeometry();
+ lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
+
+ var lineZGeometry = new THREE.BufferGeometry();
+ lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );
+
+ this.handleGizmos = {
+
+ X: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
+ [ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
+ ],
+
+ Y: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
+ [ new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
+ ],
+
+ Z: [
+ [ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
+ [ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
+ ],
+
+ XYZ: [
+ [ new THREE.Mesh( new THREE.BoxBufferGeometry( 0.125, 0.125, 0.125 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ]
+ ]
+
+ };
+
+ this.pickerGizmos = {
+
+ X: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
+ ],
+
+ Y: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
+ ],
+
+ Z: [
+ [ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
+ ],
+
+ XYZ: [
+ [ new THREE.Mesh( new THREE.BoxBufferGeometry( 0.4, 0.4, 0.4 ), pickerMaterial ) ]
+ ]
+
+ };
+
+ this.setActivePlane = function ( axis, eye ) {
+
+ var tempMatrix = new THREE.Matrix4();
+ eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );
+
+ if ( axis === "X" ) {
+
+ this.activePlane = this.planes[ "XY" ];
+ if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];
+
+ }
+
+ if ( axis === "Y" ) {
+
+ this.activePlane = this.planes[ "XY" ];
+ if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];
+
+ }
+
+ if ( axis === "Z" ) {
+
+ this.activePlane = this.planes[ "XZ" ];
+ if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];
+
+ }
+
+ if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];
+
+ };
+
+ this.init();
+
+ };
+
+ THREE.TransformGizmoScale.prototype = Object.create( THREE.TransformGizmo.prototype );
+ THREE.TransformGizmoScale.prototype.constructor = THREE.TransformGizmoScale;
+
+ THREE.TransformControls = function ( camera, domElement ) {
+
+ // TODO: Make non-uniform scale and rotate play nice in hierarchies
+ // TODO: ADD RXYZ contol
+
+ THREE.Object3D.call( this );
+
+ domElement = ( domElement !== undefined ) ? domElement : document;
+
+ this.object = undefined;
+ this.visible = false;
+ this.translationSnap = null;
+ this.rotationSnap = null;
+ this.space = "world";
+ this.size = 1;
+ this.axis = null;
+
+ var scope = this;
+
+ var _mode = "translate";
+ var _dragging = false;
+ var _gizmo = {
+
+ "translate": new THREE.TransformGizmoTranslate(),
+ "rotate": new THREE.TransformGizmoRotate(),
+ "scale": new THREE.TransformGizmoScale()
+ };
+
+ for ( var type in _gizmo ) {
+
+ var gizmoObj = _gizmo[ type ];
+
+ gizmoObj.visible = ( type === _mode );
+ this.add( gizmoObj );
+
+ }
+
+ var changeEvent = { type: "change" };
+ var mouseDownEvent = { type: "mouseDown" };
+ var mouseUpEvent = { type: "mouseUp", mode: _mode };
+ var objectChangeEvent = { type: "objectChange" };
+
+ var ray = new THREE.Raycaster();
+ var pointerVector = new THREE.Vector2();
+
+ var point = new THREE.Vector3();
+ var offset = new THREE.Vector3();
+
+ var rotation = new THREE.Vector3();
+ var offsetRotation = new THREE.Vector3();
+ var scale = 1;
+
+ var lookAtMatrix = new THREE.Matrix4();
+ var eye = new THREE.Vector3();
+
+ var tempMatrix = new THREE.Matrix4();
+ var tempVector = new THREE.Vector3();
+ var tempQuaternion = new THREE.Quaternion();
+ var unitX = new THREE.Vector3( 1, 0, 0 );
+ var unitY = new THREE.Vector3( 0, 1, 0 );
+ var unitZ = new THREE.Vector3( 0, 0, 1 );
+
+ var quaternionXYZ = new THREE.Quaternion();
+ var quaternionX = new THREE.Quaternion();
+ var quaternionY = new THREE.Quaternion();
+ var quaternionZ = new THREE.Quaternion();
+ var quaternionE = new THREE.Quaternion();
+
+ var oldPosition = new THREE.Vector3();
+ var oldScale = new THREE.Vector3();
+ var oldRotationMatrix = new THREE.Matrix4();
+
+ var parentRotationMatrix = new THREE.Matrix4();
+ var parentScale = new THREE.Vector3();
+
+ var worldPosition = new THREE.Vector3();
+ var worldRotation = new THREE.Euler();
+ var worldRotationMatrix = new THREE.Matrix4();
+ var camPosition = new THREE.Vector3();
+ var camRotation = new THREE.Euler();
+
+ domElement.addEventListener( "mousedown", onPointerDown, false );
+ domElement.addEventListener( "touchstart", onPointerDown, false );
+
+ domElement.addEventListener( "mousemove", onPointerHover, false );
+ domElement.addEventListener( "touchmove", onPointerHover, false );
+
+ domElement.addEventListener( "mousemove", onPointerMove, false );
+ domElement.addEventListener( "touchmove", onPointerMove, false );
+
+ domElement.addEventListener( "mouseup", onPointerUp, false );
+ domElement.addEventListener( "mouseout", onPointerUp, false );
+ domElement.addEventListener( "touchend", onPointerUp, false );
+ domElement.addEventListener( "touchcancel", onPointerUp, false );
+ domElement.addEventListener( "touchleave", onPointerUp, false );
+
+ this.dispose = function () {
+
+ domElement.removeEventListener( "mousedown", onPointerDown );
+ domElement.removeEventListener( "touchstart", onPointerDown );
+
+ domElement.removeEventListener( "mousemove", onPointerHover );
+ domElement.removeEventListener( "touchmove", onPointerHover );
+
+ domElement.removeEventListener( "mousemove", onPointerMove );
+ domElement.removeEventListener( "touchmove", onPointerMove );
+
+ domElement.removeEventListener( "mouseup", onPointerUp );
+ domElement.removeEventListener( "mouseout", onPointerUp );
+ domElement.removeEventListener( "touchend", onPointerUp );
+ domElement.removeEventListener( "touchcancel", onPointerUp );
+ domElement.removeEventListener( "touchleave", onPointerUp );
+
+ };
+
+ this.attach = function ( object ) {
+
+ this.object = object;
+ this.visible = true;
+ this.update();
+
+ };
+
+ this.detach = function () {
+
+ this.object = undefined;
+ this.visible = false;
+ this.axis = null;
+
+ };
+
+ this.getMode = function () {
+
+ return _mode;
+
+ };
+
+ this.setMode = function ( mode ) {
+
+ _mode = mode ? mode : _mode;
+
+ if ( _mode === "scale" ) scope.space = "local";
+
+ for ( var type in _gizmo ) _gizmo[ type ].visible = ( type === _mode );
+
+ this.update();
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.setTranslationSnap = function ( translationSnap ) {
+
+ scope.translationSnap = translationSnap;
+
+ };
+
+ this.setRotationSnap = function ( rotationSnap ) {
+
+ scope.rotationSnap = rotationSnap;
+
+ };
+
+ this.setSize = function ( size ) {
+
+ scope.size = size;
+ this.update();
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.setSpace = function ( space ) {
+
+ scope.space = space;
+ this.update();
+ scope.dispatchEvent( changeEvent );
+
+ };
+
+ this.update = function () {
+
+ if ( scope.object === undefined ) return;
+
+ scope.object.updateMatrixWorld();
+ worldPosition.setFromMatrixPosition( scope.object.matrixWorld );
+ worldRotation.setFromRotationMatrix( tempMatrix.extractRotation( scope.object.matrixWorld ) );
+
+ camera.updateMatrixWorld();
+ camPosition.setFromMatrixPosition( camera.matrixWorld );
+ camRotation.setFromRotationMatrix( tempMatrix.extractRotation( camera.matrixWorld ) );
+
+ scale = worldPosition.distanceTo( camPosition ) / 6 * scope.size;
+ this.position.copy( worldPosition );
+ this.scale.set( scale, scale, scale );
+
+ if ( camera instanceof THREE.PerspectiveCamera ) {
+
+ eye.copy( camPosition ).sub( worldPosition ).normalize();
+
+ } else if ( camera instanceof THREE.OrthographicCamera ) {
+
+ eye.copy( camPosition ).normalize();
+
+ }
+
+ if ( scope.space === "local" ) {
+
+ _gizmo[ _mode ].update( worldRotation, eye );
+
+ } else if ( scope.space === "world" ) {
+
+ _gizmo[ _mode ].update( new THREE.Euler(), eye );
+
+ }
+
+ _gizmo[ _mode ].highlight( scope.axis );
+
+ };
+
+ function onPointerHover( event ) {
+
+ if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;
+
+ var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
+
+ var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );
+
+ var axis = null;
+
+ if ( intersect ) {
+
+ axis = intersect.object.name;
+
+ event.preventDefault();
+
+ }
+
+ if ( scope.axis !== axis ) {
+
+ scope.axis = axis;
+ scope.update();
+ scope.dispatchEvent( changeEvent );
+
+ }
+
+ }
+
+ function onPointerDown( event ) {
+
+ if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;
+
+ var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
+
+ if ( pointer.button === 0 || pointer.button === undefined ) {
+
+ var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );
+
+ if ( intersect ) {
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ scope.dispatchEvent( mouseDownEvent );
+
+ scope.axis = intersect.object.name;
+
+ scope.update();
+
+ eye.copy( camPosition ).sub( worldPosition ).normalize();
+
+ _gizmo[ _mode ].setActivePlane( scope.axis, eye );
+
+ var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );
+
+ if ( planeIntersect ) {
+
+ oldPosition.copy( scope.object.position );
+ oldScale.copy( scope.object.scale );
+
+ oldRotationMatrix.extractRotation( scope.object.matrix );
+ worldRotationMatrix.extractRotation( scope.object.matrixWorld );
+
+ parentRotationMatrix.extractRotation( scope.object.parent.matrixWorld );
+ parentScale.setFromMatrixScale( tempMatrix.getInverse( scope.object.parent.matrixWorld ) );
+
+ offset.copy( planeIntersect.point );
+
+ }
+
+ }
+
+ }
+
+ _dragging = true;
+
+ }
+
+ function onPointerMove( event ) {
+
+ if ( scope.object === undefined || scope.axis === null || _dragging === false || ( event.button !== undefined && event.button !== 0 ) ) return;
+
+ var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
+
+ var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );
+
+ if ( planeIntersect === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ point.copy( planeIntersect.point );
+
+ if ( _mode === "translate" ) {
+
+ point.sub( offset );
+ point.multiply( parentScale );
+
+ if ( scope.space === "local" ) {
+
+ point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
+
+ if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
+ if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
+ if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;
+
+ point.applyMatrix4( oldRotationMatrix );
+
+ scope.object.position.copy( oldPosition );
+ scope.object.position.add( point );
+
+ }
+
+ if ( scope.space === "world" || scope.axis.search( "XYZ" ) !== - 1 ) {
+
+ if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
+ if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
+ if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;
+
+ point.applyMatrix4( tempMatrix.getInverse( parentRotationMatrix ) );
+
+ scope.object.position.copy( oldPosition );
+ scope.object.position.add( point );
+
+ }
+
+ if ( scope.translationSnap !== null ) {
+
+ if ( scope.space === "local" ) {
+
+ scope.object.position.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
+
+ }
+
+ if ( scope.axis.search( "X" ) !== - 1 ) scope.object.position.x = Math.round( scope.object.position.x / scope.translationSnap ) * scope.translationSnap;
+ if ( scope.axis.search( "Y" ) !== - 1 ) scope.object.position.y = Math.round( scope.object.position.y / scope.translationSnap ) * scope.translationSnap;
+ if ( scope.axis.search( "Z" ) !== - 1 ) scope.object.position.z = Math.round( scope.object.position.z / scope.translationSnap ) * scope.translationSnap;
+
+ if ( scope.space === "local" ) {
+
+ scope.object.position.applyMatrix4( worldRotationMatrix );
+
+ }
+
+ }
+
+ } else if ( _mode === "scale" ) {
+
+ point.sub( offset );
+ point.multiply( parentScale );
+
+ if ( scope.space === "local" ) {
+
+ if ( scope.axis === "XYZ" ) {
+
+ scale = 1 + ( ( point.y ) / Math.max( oldScale.x, oldScale.y, oldScale.z ) );
+
+ scope.object.scale.x = oldScale.x * scale;
+ scope.object.scale.y = oldScale.y * scale;
+ scope.object.scale.z = oldScale.z * scale;
+
+ } else {
+
+ point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
+
+ if ( scope.axis === "X" ) scope.object.scale.x = oldScale.x * ( 1 + point.x / oldScale.x );
+ if ( scope.axis === "Y" ) scope.object.scale.y = oldScale.y * ( 1 + point.y / oldScale.y );
+ if ( scope.axis === "Z" ) scope.object.scale.z = oldScale.z * ( 1 + point.z / oldScale.z );
+
+ }
+
+ }
+
+ } else if ( _mode === "rotate" ) {
+
+ point.sub( worldPosition );
+ point.multiply( parentScale );
+ tempVector.copy( offset ).sub( worldPosition );
+ tempVector.multiply( parentScale );
+
+ if ( scope.axis === "E" ) {
+
+ point.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );
+ tempVector.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );
+
+ rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
+ offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
+
+ tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
+
+ quaternionE.setFromAxisAngle( eye, rotation.z - offsetRotation.z );
+ quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
+
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionE );
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
+
+ scope.object.quaternion.copy( tempQuaternion );
+
+ } else if ( scope.axis === "XYZE" ) {
+
+ quaternionE.setFromEuler( point.clone().cross( tempVector ).normalize() ); // rotation axis
+
+ tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
+ quaternionX.setFromAxisAngle( quaternionE, - point.clone().angleTo( tempVector ) );
+ quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
+
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
+
+ scope.object.quaternion.copy( tempQuaternion );
+
+ } else if ( scope.space === "local" ) {
+
+ point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
+
+ tempVector.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
+
+ rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
+ offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
+
+ quaternionXYZ.setFromRotationMatrix( oldRotationMatrix );
+
+ if ( scope.rotationSnap !== null ) {
+
+ quaternionX.setFromAxisAngle( unitX, Math.round( ( rotation.x - offsetRotation.x ) / scope.rotationSnap ) * scope.rotationSnap );
+ quaternionY.setFromAxisAngle( unitY, Math.round( ( rotation.y - offsetRotation.y ) / scope.rotationSnap ) * scope.rotationSnap );
+ quaternionZ.setFromAxisAngle( unitZ, Math.round( ( rotation.z - offsetRotation.z ) / scope.rotationSnap ) * scope.rotationSnap );
+
+ } else {
+
+ quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
+ quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
+ quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );
+
+ }
+
+ if ( scope.axis === "X" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionX );
+ if ( scope.axis === "Y" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionY );
+ if ( scope.axis === "Z" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionZ );
+
+ scope.object.quaternion.copy( quaternionXYZ );
+
+ } else if ( scope.space === "world" ) {
+
+ rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
+ offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
+
+ tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
+
+ if ( scope.rotationSnap !== null ) {
+
+ quaternionX.setFromAxisAngle( unitX, Math.round( ( rotation.x - offsetRotation.x ) / scope.rotationSnap ) * scope.rotationSnap );
+ quaternionY.setFromAxisAngle( unitY, Math.round( ( rotation.y - offsetRotation.y ) / scope.rotationSnap ) * scope.rotationSnap );
+ quaternionZ.setFromAxisAngle( unitZ, Math.round( ( rotation.z - offsetRotation.z ) / scope.rotationSnap ) * scope.rotationSnap );
+
+ } else {
+
+ quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
+ quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
+ quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );
+
+ }
+
+ quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
+
+ if ( scope.axis === "X" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
+ if ( scope.axis === "Y" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
+ if ( scope.axis === "Z" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );
+
+ tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
+
+ scope.object.quaternion.copy( tempQuaternion );
+
+ }
+
+ }
+
+ scope.update();
+ scope.dispatchEvent( changeEvent );
+ scope.dispatchEvent( objectChangeEvent );
+
+ }
+
+ function onPointerUp( event ) {
+
+ event.preventDefault(); // Prevent MouseEvent on mobile
+
+ if ( event.button !== undefined && event.button !== 0 ) return;
+
+ if ( _dragging && ( scope.axis !== null ) ) {
+
+ mouseUpEvent.mode = _mode;
+ scope.dispatchEvent( mouseUpEvent );
+
+ }
+
+ _dragging = false;
+
+ if ( 'TouchEvent' in window && event instanceof TouchEvent ) {
+
+ // Force "rollover"
+
+ scope.axis = null;
+ scope.update();
+ scope.dispatchEvent( changeEvent );
+
+ } else {
+
+ onPointerHover( event );
+
+ }
+
+ }
+
+ function intersectObjects( pointer, objects ) {
+
+ var rect = domElement.getBoundingClientRect();
+ var x = ( pointer.clientX - rect.left ) / rect.width;
+ var y = ( pointer.clientY - rect.top ) / rect.height;
+
+ pointerVector.set( ( x * 2 ) - 1, - ( y * 2 ) + 1 );
+ ray.setFromCamera( pointerVector, camera );
+
+ var intersections = ray.intersectObjects( objects, true );
+ return intersections[ 0 ] ? intersections[ 0 ] : false;
+
+ }
+
+ };
+
+ THREE.TransformControls.prototype = Object.create( THREE.Object3D.prototype );
+ THREE.TransformControls.prototype.constructor = THREE.TransformControls;
+
+}() );
diff --git a/app/static/js/controls/VRControls.js b/app/static/js/controls/VRControls.js
new file mode 100644
index 0000000..8829c06
--- /dev/null
+++ b/app/static/js/controls/VRControls.js
@@ -0,0 +1,149 @@
+/**
+ * @author dmarcos / https://github.com/dmarcos
+ * @author mrdoob / http://mrdoob.com
+ */
+
+THREE.VRControls = function ( object, onError ) {
+
+ var scope = this;
+
+ var vrDisplay, vrDisplays;
+
+ var standingMatrix = new THREE.Matrix4();
+
+ var frameData = null;
+
+ if ( 'VRFrameData' in window ) {
+
+ frameData = new VRFrameData();
+
+ }
+
+ function gotVRDisplays( displays ) {
+
+ vrDisplays = displays;
+
+ if ( displays.length > 0 ) {
+
+ vrDisplay = displays[ 0 ];
+
+ } else {
+
+ if ( onError ) onError( 'VR input not available.' );
+
+ }
+
+ }
+
+ if ( navigator.getVRDisplays ) {
+
+ navigator.getVRDisplays().then( gotVRDisplays ).catch( function () {
+
+ console.warn( 'THREE.VRControls: Unable to get VR Displays' );
+
+ } );
+
+ }
+
+ // the Rift SDK returns the position in meters
+ // this scale factor allows the user to define how meters
+ // are converted to scene units.
+
+ this.scale = 1;
+
+ // If true will use "standing space" coordinate system where y=0 is the
+ // floor and x=0, z=0 is the center of the room.
+ this.standing = false;
+
+ // Distance from the users eyes to the floor in meters. Used when
+ // standing=true but the VRDisplay doesn't provide stageParameters.
+ this.userHeight = 1.6;
+
+ this.getVRDisplay = function () {
+
+ return vrDisplay;
+
+ };
+
+ this.setVRDisplay = function ( value ) {
+
+ vrDisplay = value;
+
+ };
+
+ this.getVRDisplays = function () {
+
+ console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' );
+ return vrDisplays;
+
+ };
+
+ this.getStandingMatrix = function () {
+
+ return standingMatrix;
+
+ };
+
+ this.update = function () {
+
+ if ( vrDisplay ) {
+
+ var pose;
+
+ if ( vrDisplay.getFrameData ) {
+
+ vrDisplay.getFrameData( frameData );
+ pose = frameData.pose;
+
+ } else if ( vrDisplay.getPose ) {
+
+ pose = vrDisplay.getPose();
+
+ }
+
+ if ( pose.orientation !== null ) {
+
+ object.quaternion.fromArray( pose.orientation );
+
+ }
+
+ if ( pose.position !== null ) {
+
+ object.position.fromArray( pose.position );
+
+ } else {
+
+ object.position.set( 0, 0, 0 );
+
+ }
+
+ if ( this.standing ) {
+
+ if ( vrDisplay.stageParameters ) {
+
+ object.updateMatrix();
+
+ standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform );
+ object.applyMatrix( standingMatrix );
+
+ } else {
+
+ object.position.setY( object.position.y + this.userHeight );
+
+ }
+
+ }
+
+ object.position.multiplyScalar( scope.scale );
+
+ }
+
+ };
+
+ this.dispose = function () {
+
+ vrDisplay = null;
+
+ };
+
+};
diff --git a/app/static/js/crossfade/gui.js b/app/static/js/crossfade/gui.js
new file mode 100644
index 0000000..40184cf
--- /dev/null
+++ b/app/static/js/crossfade/gui.js
@@ -0,0 +1,39 @@
+var transitionParams = {
+ "useTexture": true,
+ "transition": 0.5,
+ "transitionSpeed": 2.0,
+ "texture": 5,
+ "loopTexture": true,
+ "animateTransition": true,
+ "textureThreshold": 0.3
+};
+
+function initGUI() {
+
+ var gui = new dat.GUI();
+
+ gui.add( transitionParams, "useTexture" ).onChange( function( value ) {
+
+ transition.useTexture( value );
+
+ } );
+
+ gui.add( transitionParams, 'loopTexture' );
+
+ gui.add( transitionParams, 'texture', { Perlin: 0, Squares: 1, Cells: 2, Distort: 3, Gradient: 4, Radial: 5 } ).onChange( function( value ) {
+
+ transition.setTexture( value );
+
+ } ).listen();
+
+ gui.add( transitionParams, "textureThreshold", 0, 1, 0.01 ).onChange( function( value ) {
+
+ transition.setTextureThreshold( value );
+
+ } );
+
+ gui.add( transitionParams, "animateTransition" );
+ gui.add( transitionParams, "transition", 0, 1, 0.01 ).listen();
+ gui.add( transitionParams, "transitionSpeed", 0.5, 5, 0.01 );
+
+}
diff --git a/app/static/js/crossfade/scenes.js b/app/static/js/crossfade/scenes.js
new file mode 100644
index 0000000..7f360b0
--- /dev/null
+++ b/app/static/js/crossfade/scenes.js
@@ -0,0 +1,112 @@
+function generateGeometry( objectType, numObjects ) {
+
+ var geometry = new THREE.Geometry();
+
+ function applyVertexColors( g, c ) {
+
+ g.faces.forEach( function( f ) {
+
+ var n = ( f instanceof THREE.Face3 ) ? 3 : 4;
+
+ for ( var j = 0; j < n; j ++ ) {
+
+ f.vertexColors[ j ] = c;
+
+ }
+
+ } );
+
+ }
+
+ for ( var i = 0; i < numObjects; i ++ ) {
+
+ var position = new THREE.Vector3();
+
+ position.x = Math.random() * 10000 - 5000;
+ position.y = Math.random() * 6000 - 3000;
+ position.z = Math.random() * 8000 - 4000;
+
+ var rotation = new THREE.Euler();
+
+ rotation.x = Math.random() * 2 * Math.PI;
+ rotation.y = Math.random() * 2 * Math.PI;
+ rotation.z = Math.random() * 2 * Math.PI;
+
+ var scale = new THREE.Vector3();
+
+ var geom, color = new THREE.Color();
+
+ scale.x = Math.random() * 200 + 100;
+
+ if ( objectType == "cube" ) {
+
+ geom = new THREE.BoxGeometry( 1, 1, 1 );
+ scale.y = Math.random() * 200 + 100;
+ scale.z = Math.random() * 200 + 100;
+ color.setRGB( 0, 0, Math.random() + 0.1 );
+
+ } else if ( objectType == "sphere" ) {
+
+ geom = new THREE.IcosahedronGeometry( 1, 1 );
+ scale.y = scale.z = scale.x;
+ color.setRGB( Math.random() + 0.1, 0, 0 );
+
+ }
+
+ // give the geom's vertices a random color, to be displayed
+ applyVertexColors( geom, color );
+
+ var mesh = new THREE.Mesh( geom );
+ mesh.position.copy( position );
+ mesh.rotation.copy( rotation );
+ mesh.scale.copy( scale );
+ mesh.updateMatrix();
+
+ geometry.merge( mesh.geometry, mesh.matrix );
+
+ }
+
+ return geometry;
+
+}
+
+function Scene ( type, numObjects, cameraZ, fov, rotationSpeed, clearColor ) {
+
+ this.clearColor = clearColor;
+
+ this.camera = new THREE.PerspectiveCamera( fov, window.innerWidth / window.innerHeight, 1, 10000 );
+ this.camera.position.z = cameraZ;
+
+ // Setup scene
+ this.scene = new THREE.Scene();
+ this.scene.add( new THREE.AmbientLight( 0x555555 ) );
+
+ var light = new THREE.SpotLight( 0xffffff, 1.5 );
+ light.position.set( 0, 500, 2000 );
+ this.scene.add( light );
+
+ this.rotationSpeed = rotationSpeed;
+
+ var defaultMaterial = new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true, vertexColors: THREE.VertexColors } );
+ this.mesh = new THREE.Mesh( generateGeometry( type, numObjects ), defaultMaterial );
+ this.scene.add( this.mesh );
+
+ var renderTargetParameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat, stencilBuffer: false };
+ this.fbo = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight, renderTargetParameters );
+
+ this.render = function( delta, rtt ) {
+
+ this.mesh.rotation.x += delta * this.rotationSpeed.x;
+ this.mesh.rotation.y += delta * this.rotationSpeed.y;
+ this.mesh.rotation.z += delta * this.rotationSpeed.z;
+
+ renderer.setClearColor( this.clearColor );
+
+ if ( rtt )
+ renderer.render( this.scene, this.camera, this.fbo, true );
+ else
+ renderer.render( this.scene, this.camera );
+
+ };
+
+}
diff --git a/app/static/js/crossfade/transition.js b/app/static/js/crossfade/transition.js
new file mode 100644
index 0000000..795ae80
--- /dev/null
+++ b/app/static/js/crossfade/transition.js
@@ -0,0 +1,164 @@
+function Transition ( sceneA, sceneB ) {
+
+ this.scene = new THREE.Scene();
+
+ this.cameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, - 10, 10 );
+
+ this.textures = [];
+
+ var loader = new THREE.TextureLoader();
+
+ for ( var i = 0; i < 6; i ++ )
+ this.textures[ i ] = loader.load( 'textures/transition/transition' + ( i + 1 ) + '.png' );
+
+ this.quadmaterial = new THREE.ShaderMaterial( {
+
+ uniforms: {
+
+ tDiffuse1: {
+ value: null
+ },
+ tDiffuse2: {
+ value: null
+ },
+ mixRatio: {
+ value: 0.0
+ },
+ threshold: {
+ value: 0.1
+ },
+ useTexture: {
+ value: 1
+ },
+ tMixTexture: {
+ value: this.textures[ 0 ]
+ }
+ },
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vUv = vec2( uv.x, uv.y );",
+ "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+ fragmentShader: [
+
+ "uniform float mixRatio;",
+
+ "uniform sampler2D tDiffuse1;",
+ "uniform sampler2D tDiffuse2;",
+ "uniform sampler2D tMixTexture;",
+
+ "uniform int useTexture;",
+ "uniform float threshold;",
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ "vec4 texel1 = texture2D( tDiffuse1, vUv );",
+ "vec4 texel2 = texture2D( tDiffuse2, vUv );",
+
+ "if (useTexture==1) {",
+
+ "vec4 transitionTexel = texture2D( tMixTexture, vUv );",
+ "float r = mixRatio * (1.0 + threshold * 2.0) - threshold;",
+ "float mixf=clamp((transitionTexel.r - r)*(1.0/threshold), 0.0, 1.0);",
+
+ "gl_FragColor = mix( texel1, texel2, mixf );",
+ "} else {",
+
+ "gl_FragColor = mix( texel2, texel1, mixRatio );",
+
+ "}",
+ "}"
+
+ ].join( "\n" )
+
+ } );
+
+ var quadgeometry = new THREE.PlaneBufferGeometry( window.innerWidth, window.innerHeight );
+
+ this.quad = new THREE.Mesh( quadgeometry, this.quadmaterial );
+ this.scene.add( this.quad );
+
+ // Link both scenes and their FBOs
+ this.sceneA = sceneA;
+ this.sceneB = sceneB;
+
+ this.quadmaterial.uniforms.tDiffuse1.value = sceneA.fbo.texture;
+ this.quadmaterial.uniforms.tDiffuse2.value = sceneB.fbo.texture;
+
+ this.needChange = false;
+
+ this.setTextureThreshold = function ( value ) {
+
+ this.quadmaterial.uniforms.threshold.value = value;
+
+ };
+
+ this.useTexture = function ( value ) {
+
+ this.quadmaterial.uniforms.useTexture.value = value ? 1 : 0;
+
+ };
+
+ this.setTexture = function ( i ) {
+
+ this.quadmaterial.uniforms.tMixTexture.value = this.textures[ i ];
+
+ };
+
+ this.render = function( delta ) {
+
+ // Transition animation
+ if ( transitionParams.animateTransition ) {
+
+ var t = ( 1 + Math.sin( transitionParams.transitionSpeed * clock.getElapsedTime() / Math.PI ) ) / 2;
+ transitionParams.transition = THREE.Math.smoothstep( t, 0.3, 0.7 );
+
+ // Change the current alpha texture after each transition
+ if ( transitionParams.loopTexture && ( transitionParams.transition == 0 || transitionParams.transition == 1 ) ) {
+
+ if ( this.needChange ) {
+
+ transitionParams.texture = ( transitionParams.texture + 1 ) % this.textures.length;
+ this.quadmaterial.uniforms.tMixTexture.value = this.textures[ transitionParams.texture ];
+ this.needChange = false;
+
+ }
+
+ } else
+ this.needChange = true;
+
+ }
+
+ this.quadmaterial.uniforms.mixRatio.value = transitionParams.transition;
+
+ // Prevent render both scenes when it's not necessary
+ if ( transitionParams.transition == 0 ) {
+
+ this.sceneB.render( delta, false );
+
+ } else if ( transitionParams.transition == 1 ) {
+
+ this.sceneA.render( delta, false );
+
+ } else {
+
+ // When 0u
+
+ // following results in (wx, wy, wz, w) homogeneous point
+ var hpoint = THREE.NURBSUtils.calcBSplinePoint( this.degree, this.knots, this.controlPoints, u );
+
+ if ( hpoint.w != 1.0 ) {
+
+ // project to 3D space: (wx, wy, wz, w) -> (x, y, z, 1)
+ hpoint.divideScalar( hpoint.w );
+
+ }
+
+ return new THREE.Vector3( hpoint.x, hpoint.y, hpoint.z );
+
+};
+
+
+THREE.NURBSCurve.prototype.getTangent = function ( t ) {
+
+ var u = this.knots[ 0 ] + t * ( this.knots[ this.knots.length - 1 ] - this.knots[ 0 ] );
+ var ders = THREE.NURBSUtils.calcNURBSDerivatives( this.degree, this.knots, this.controlPoints, u, 1 );
+ var tangent = ders[ 1 ].clone();
+ tangent.normalize();
+
+ return tangent;
+
+};
diff --git a/app/static/js/curves/NURBSSurface.js b/app/static/js/curves/NURBSSurface.js
new file mode 100644
index 0000000..5eecd61
--- /dev/null
+++ b/app/static/js/curves/NURBSSurface.js
@@ -0,0 +1,55 @@
+/**
+ * @author renej
+ * NURBS surface object
+ *
+ * Implementation is based on (x, y [, z=0 [, w=1]]) control points with w=weight.
+ *
+ **/
+
+
+/**************************************************************
+ * NURBS surface
+ **************************************************************/
+
+THREE.NURBSSurface = function ( degree1, degree2, knots1, knots2 /* arrays of reals */, controlPoints /* array^2 of Vector(2|3|4) */ ) {
+
+ this.degree1 = degree1;
+ this.degree2 = degree2;
+ this.knots1 = knots1;
+ this.knots2 = knots2;
+ this.controlPoints = [];
+
+ var len1 = knots1.length - degree1 - 1;
+ var len2 = knots2.length - degree2 - 1;
+
+ // ensure Vector4 for control points
+ for ( var i = 0; i < len1; ++ i ) {
+
+ this.controlPoints[ i ] = [];
+ for ( var j = 0; j < len2; ++ j ) {
+
+ var point = controlPoints[ i ][ j ];
+ this.controlPoints[ i ][ j ] = new THREE.Vector4( point.x, point.y, point.z, point.w );
+
+ }
+
+ }
+
+};
+
+
+THREE.NURBSSurface.prototype = {
+
+ constructor: THREE.NURBSSurface,
+
+ getPoint: function ( t1, t2 ) {
+
+ var u = this.knots1[ 0 ] + t1 * ( this.knots1[ this.knots1.length - 1 ] - this.knots1[ 0 ] ); // linear mapping t1->u
+ var v = this.knots2[ 0 ] + t2 * ( this.knots2[ this.knots2.length - 1 ] - this.knots2[ 0 ] ); // linear mapping t2->u
+
+ return THREE.NURBSUtils.calcSurfacePoint( this.degree1, this.degree2, this.knots1, this.knots2, this.controlPoints, u, v );
+
+ }
+};
+
+
diff --git a/app/static/js/curves/NURBSUtils.js b/app/static/js/curves/NURBSUtils.js
new file mode 100644
index 0000000..07128d6
--- /dev/null
+++ b/app/static/js/curves/NURBSUtils.js
@@ -0,0 +1,472 @@
+/**
+ * @author renej
+ * NURBS utils
+ *
+ * See NURBSCurve and NURBSSurface.
+ *
+ **/
+
+
+/**************************************************************
+ * NURBS Utils
+ **************************************************************/
+
+THREE.NURBSUtils = {
+
+ /*
+ Finds knot vector span.
+
+ p : degree
+ u : parametric value
+ U : knot vector
+
+ returns the span
+ */
+ findSpan: function( p, u, U ) {
+
+ var n = U.length - p - 1;
+
+ if ( u >= U[ n ] ) {
+
+ return n - 1;
+
+ }
+
+ if ( u <= U[ p ] ) {
+
+ return p;
+
+ }
+
+ var low = p;
+ var high = n;
+ var mid = Math.floor( ( low + high ) / 2 );
+
+ while ( u < U[ mid ] || u >= U[ mid + 1 ] ) {
+
+ if ( u < U[ mid ] ) {
+
+ high = mid;
+
+ } else {
+
+ low = mid;
+
+ }
+
+ mid = Math.floor( ( low + high ) / 2 );
+
+ }
+
+ return mid;
+
+ },
+
+
+ /*
+ Calculate basis functions. See The NURBS Book, page 70, algorithm A2.2
+
+ span : span in which u lies
+ u : parametric point
+ p : degree
+ U : knot vector
+
+ returns array[p+1] with basis functions values.
+ */
+ calcBasisFunctions: function( span, u, p, U ) {
+
+ var N = [];
+ var left = [];
+ var right = [];
+ N[ 0 ] = 1.0;
+
+ for ( var j = 1; j <= p; ++ j ) {
+
+ left[ j ] = u - U[ span + 1 - j ];
+ right[ j ] = U[ span + j ] - u;
+
+ var saved = 0.0;
+
+ for ( var r = 0; r < j; ++ r ) {
+
+ var rv = right[ r + 1 ];
+ var lv = left[ j - r ];
+ var temp = N[ r ] / ( rv + lv );
+ N[ r ] = saved + rv * temp;
+ saved = lv * temp;
+
+ }
+
+ N[ j ] = saved;
+
+ }
+
+ return N;
+
+ },
+
+
+ /*
+ Calculate B-Spline curve points. See The NURBS Book, page 82, algorithm A3.1.
+
+ p : degree of B-Spline
+ U : knot vector
+ P : control points (x, y, z, w)
+ u : parametric point
+
+ returns point for given u
+ */
+ calcBSplinePoint: function( p, U, P, u ) {
+
+ var span = this.findSpan( p, u, U );
+ var N = this.calcBasisFunctions( span, u, p, U );
+ var C = new THREE.Vector4( 0, 0, 0, 0 );
+
+ for ( var j = 0; j <= p; ++ j ) {
+
+ var point = P[ span - p + j ];
+ var Nj = N[ j ];
+ var wNj = point.w * Nj;
+ C.x += point.x * wNj;
+ C.y += point.y * wNj;
+ C.z += point.z * wNj;
+ C.w += point.w * Nj;
+
+ }
+
+ return C;
+
+ },
+
+
+ /*
+ Calculate basis functions derivatives. See The NURBS Book, page 72, algorithm A2.3.
+
+ span : span in which u lies
+ u : parametric point
+ p : degree
+ n : number of derivatives to calculate
+ U : knot vector
+
+ returns array[n+1][p+1] with basis functions derivatives
+ */
+ calcBasisFunctionDerivatives: function( span, u, p, n, U ) {
+
+ var zeroArr = [];
+ for ( var i = 0; i <= p; ++ i )
+ zeroArr[ i ] = 0.0;
+
+ var ders = [];
+ for ( var i = 0; i <= n; ++ i )
+ ders[ i ] = zeroArr.slice( 0 );
+
+ var ndu = [];
+ for ( var i = 0; i <= p; ++ i )
+ ndu[ i ] = zeroArr.slice( 0 );
+
+ ndu[ 0 ][ 0 ] = 1.0;
+
+ var left = zeroArr.slice( 0 );
+ var right = zeroArr.slice( 0 );
+
+ for ( var j = 1; j <= p; ++ j ) {
+
+ left[ j ] = u - U[ span + 1 - j ];
+ right[ j ] = U[ span + j ] - u;
+
+ var saved = 0.0;
+
+ for ( var r = 0; r < j; ++ r ) {
+
+ var rv = right[ r + 1 ];
+ var lv = left[ j - r ];
+ ndu[ j ][ r ] = rv + lv;
+
+ var temp = ndu[ r ][ j - 1 ] / ndu[ j ][ r ];
+ ndu[ r ][ j ] = saved + rv * temp;
+ saved = lv * temp;
+
+ }
+
+ ndu[ j ][ j ] = saved;
+
+ }
+
+ for ( var j = 0; j <= p; ++ j ) {
+
+ ders[ 0 ][ j ] = ndu[ j ][ p ];
+
+ }
+
+ for ( var r = 0; r <= p; ++ r ) {
+
+ var s1 = 0;
+ var s2 = 1;
+
+ var a = [];
+ for ( var i = 0; i <= p; ++ i ) {
+
+ a[ i ] = zeroArr.slice( 0 );
+
+ }
+ a[ 0 ][ 0 ] = 1.0;
+
+ for ( var k = 1; k <= n; ++ k ) {
+
+ var d = 0.0;
+ var rk = r - k;
+ var pk = p - k;
+
+ if ( r >= k ) {
+
+ a[ s2 ][ 0 ] = a[ s1 ][ 0 ] / ndu[ pk + 1 ][ rk ];
+ d = a[ s2 ][ 0 ] * ndu[ rk ][ pk ];
+
+ }
+
+ var j1 = ( rk >= - 1 ) ? 1 : - rk;
+ var j2 = ( r - 1 <= pk ) ? k - 1 : p - r;
+
+ for ( var j = j1; j <= j2; ++ j ) {
+
+ a[ s2 ][ j ] = ( a[ s1 ][ j ] - a[ s1 ][ j - 1 ] ) / ndu[ pk + 1 ][ rk + j ];
+ d += a[ s2 ][ j ] * ndu[ rk + j ][ pk ];
+
+ }
+
+ if ( r <= pk ) {
+
+ a[ s2 ][ k ] = - a[ s1 ][ k - 1 ] / ndu[ pk + 1 ][ r ];
+ d += a[ s2 ][ k ] * ndu[ r ][ pk ];
+
+ }
+
+ ders[ k ][ r ] = d;
+
+ var j = s1;
+ s1 = s2;
+ s2 = j;
+
+ }
+
+ }
+
+ var r = p;
+
+ for ( var k = 1; k <= n; ++ k ) {
+
+ for ( var j = 0; j <= p; ++ j ) {
+
+ ders[ k ][ j ] *= r;
+
+ }
+ r *= p - k;
+
+ }
+
+ return ders;
+
+ },
+
+
+ /*
+ Calculate derivatives of a B-Spline. See The NURBS Book, page 93, algorithm A3.2.
+
+ p : degree
+ U : knot vector
+ P : control points
+ u : Parametric points
+ nd : number of derivatives
+
+ returns array[d+1] with derivatives
+ */
+ calcBSplineDerivatives: function( p, U, P, u, nd ) {
+
+ var du = nd < p ? nd : p;
+ var CK = [];
+ var span = this.findSpan( p, u, U );
+ var nders = this.calcBasisFunctionDerivatives( span, u, p, du, U );
+ var Pw = [];
+
+ for ( var i = 0; i < P.length; ++ i ) {
+
+ var point = P[ i ].clone();
+ var w = point.w;
+
+ point.x *= w;
+ point.y *= w;
+ point.z *= w;
+
+ Pw[ i ] = point;
+
+ }
+ for ( var k = 0; k <= du; ++ k ) {
+
+ var point = Pw[ span - p ].clone().multiplyScalar( nders[ k ][ 0 ] );
+
+ for ( var j = 1; j <= p; ++ j ) {
+
+ point.add( Pw[ span - p + j ].clone().multiplyScalar( nders[ k ][ j ] ) );
+
+ }
+
+ CK[ k ] = point;
+
+ }
+
+ for ( var k = du + 1; k <= nd + 1; ++ k ) {
+
+ CK[ k ] = new THREE.Vector4( 0, 0, 0 );
+
+ }
+
+ return CK;
+
+ },
+
+
+ /*
+ Calculate "K over I"
+
+ returns k!/(i!(k-i)!)
+ */
+ calcKoverI: function( k, i ) {
+
+ var nom = 1;
+
+ for ( var j = 2; j <= k; ++ j ) {
+
+ nom *= j;
+
+ }
+
+ var denom = 1;
+
+ for ( var j = 2; j <= i; ++ j ) {
+
+ denom *= j;
+
+ }
+
+ for ( var j = 2; j <= k - i; ++ j ) {
+
+ denom *= j;
+
+ }
+
+ return nom / denom;
+
+ },
+
+
+ /*
+ Calculate derivatives (0-nd) of rational curve. See The NURBS Book, page 127, algorithm A4.2.
+
+ Pders : result of function calcBSplineDerivatives
+
+ returns array with derivatives for rational curve.
+ */
+ calcRationalCurveDerivatives: function ( Pders ) {
+
+ var nd = Pders.length;
+ var Aders = [];
+ var wders = [];
+
+ for ( var i = 0; i < nd; ++ i ) {
+
+ var point = Pders[ i ];
+ Aders[ i ] = new THREE.Vector3( point.x, point.y, point.z );
+ wders[ i ] = point.w;
+
+ }
+
+ var CK = [];
+
+ for ( var k = 0; k < nd; ++ k ) {
+
+ var v = Aders[ k ].clone();
+
+ for ( var i = 1; i <= k; ++ i ) {
+
+ v.sub( CK[ k - i ].clone().multiplyScalar( this.calcKoverI( k, i ) * wders[ i ] ) );
+
+ }
+
+ CK[ k ] = v.divideScalar( wders[ 0 ] );
+
+ }
+
+ return CK;
+
+ },
+
+
+ /*
+ Calculate NURBS curve derivatives. See The NURBS Book, page 127, algorithm A4.2.
+
+ p : degree
+ U : knot vector
+ P : control points in homogeneous space
+ u : parametric points
+ nd : number of derivatives
+
+ returns array with derivatives.
+ */
+ calcNURBSDerivatives: function( p, U, P, u, nd ) {
+
+ var Pders = this.calcBSplineDerivatives( p, U, P, u, nd );
+ return this.calcRationalCurveDerivatives( Pders );
+
+ },
+
+
+ /*
+ Calculate rational B-Spline surface point. See The NURBS Book, page 134, algorithm A4.3.
+
+ p1, p2 : degrees of B-Spline surface
+ U1, U2 : knot vectors
+ P : control points (x, y, z, w)
+ u, v : parametric values
+
+ returns point for given (u, v)
+ */
+ calcSurfacePoint: function( p, q, U, V, P, u, v ) {
+
+ var uspan = this.findSpan( p, u, U );
+ var vspan = this.findSpan( q, v, V );
+ var Nu = this.calcBasisFunctions( uspan, u, p, U );
+ var Nv = this.calcBasisFunctions( vspan, v, q, V );
+ var temp = [];
+
+ for ( var l = 0; l <= q; ++ l ) {
+
+ temp[ l ] = new THREE.Vector4( 0, 0, 0, 0 );
+ for ( var k = 0; k <= p; ++ k ) {
+
+ var point = P[ uspan - p + k ][ vspan - q + l ].clone();
+ var w = point.w;
+ point.x *= w;
+ point.y *= w;
+ point.z *= w;
+ temp[ l ].add( point.multiplyScalar( Nu[ k ] ) );
+
+ }
+
+ }
+
+ var Sw = new THREE.Vector4( 0, 0, 0, 0 );
+ for ( var l = 0; l <= q; ++ l ) {
+
+ Sw.add( temp[ l ].multiplyScalar( Nv[ l ] ) );
+
+ }
+
+ Sw.divideScalar( Sw.w );
+ return new THREE.Vector3( Sw.x, Sw.y, Sw.z );
+
+ }
+
+};
+
+
+
diff --git a/app/static/js/effects/AnaglyphEffect.js b/app/static/js/effects/AnaglyphEffect.js
new file mode 100644
index 0000000..a5a8f45
--- /dev/null
+++ b/app/static/js/effects/AnaglyphEffect.js
@@ -0,0 +1,152 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author marklundin / http://mark-lundin.com/
+ * @author alteredq / http://alteredqualia.com/
+ * @author tschw
+ */
+
+THREE.AnaglyphEffect = function ( renderer, width, height ) {
+
+ // Matrices generated with angler.js https://github.com/tschw/angler.js/
+ // (in column-major element order, as accepted by WebGL)
+
+ this.colorMatrixLeft = new THREE.Matrix3().fromArray( [
+
+ 1.0671679973602295, -0.0016435992438346148, 0.0001777536963345483, // r out
+ -0.028107794001698494, -0.00019593400065787137, -0.0002875397040043026, // g out
+ -0.04279090091586113, 0.000015809757314855233, -0.00024287120322696865 // b out
+
+ ] );
+
+ // red green blue in
+
+ this.colorMatrixRight = new THREE.Matrix3().fromArray( [
+
+ -0.0355340838432312, -0.06440307199954987, 0.018319187685847282, // r out
+ -0.10269022732973099, 0.8079727292060852, -0.04835830628871918, // g out
+ 0.0001224992738571018, -0.009558862075209618, 0.567823588848114 // b out
+
+ ] );
+
+ var _camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+ var _scene = new THREE.Scene();
+
+ var _stereo = new THREE.StereoCamera();
+
+ var _params = { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat };
+
+ if ( width === undefined ) width = 512;
+ if ( height === undefined ) height = 512;
+
+ var _renderTargetL = new THREE.WebGLRenderTarget( width, height, _params );
+ var _renderTargetR = new THREE.WebGLRenderTarget( width, height, _params );
+
+ var _material = new THREE.ShaderMaterial( {
+
+ uniforms: {
+
+ "mapLeft": { value: _renderTargetL.texture },
+ "mapRight": { value: _renderTargetR.texture },
+
+ "colorMatrixLeft": { value: this.colorMatrixLeft },
+ "colorMatrixRight": { value: this.colorMatrixRight }
+
+ },
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ " vUv = vec2( uv.x, uv.y );",
+ " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform sampler2D mapLeft;",
+ "uniform sampler2D mapRight;",
+ "varying vec2 vUv;",
+
+ "uniform mat3 colorMatrixLeft;",
+ "uniform mat3 colorMatrixRight;",
+
+ // These functions implement sRGB linearization and gamma correction
+
+ "float lin( float c ) {",
+ " return c <= 0.04045 ? c * 0.0773993808 :",
+ " pow( c * 0.9478672986 + 0.0521327014, 2.4 );",
+ "}",
+
+ "vec4 lin( vec4 c ) {",
+ " return vec4( lin( c.r ), lin( c.g ), lin( c.b ), c.a );",
+ "}",
+
+ "float dev( float c ) {",
+ " return c <= 0.0031308 ? c * 12.92",
+ " : pow( c, 0.41666 ) * 1.055 - 0.055;",
+ "}",
+
+
+ "void main() {",
+
+ " vec2 uv = vUv;",
+
+ " vec4 colorL = lin( texture2D( mapLeft, uv ) );",
+ " vec4 colorR = lin( texture2D( mapRight, uv ) );",
+
+ " vec3 color = clamp(",
+ " colorMatrixLeft * colorL.rgb +",
+ " colorMatrixRight * colorR.rgb, 0., 1. );",
+
+ " gl_FragColor = vec4(",
+ " dev( color.r ), dev( color.g ), dev( color.b ),",
+ " max( colorL.a, colorR.a ) );",
+
+ "}"
+
+ ].join( "\n" )
+
+ } );
+
+ var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), _material );
+ _scene.add( mesh );
+
+ this.setSize = function ( width, height ) {
+
+ renderer.setSize( width, height );
+
+ var pixelRatio = renderer.getPixelRatio();
+
+ _renderTargetL.setSize( width * pixelRatio, height * pixelRatio );
+ _renderTargetR.setSize( width * pixelRatio, height * pixelRatio );
+
+ };
+
+ this.render = function ( scene, camera ) {
+
+ scene.updateMatrixWorld();
+
+ if ( camera.parent === null ) camera.updateMatrixWorld();
+
+ _stereo.update( camera );
+
+ renderer.render( scene, _stereo.cameraL, _renderTargetL, true );
+ renderer.render( scene, _stereo.cameraR, _renderTargetR, true );
+ renderer.render( _scene, _camera );
+
+ };
+
+ this.dispose = function() {
+
+ if ( _renderTargetL ) _renderTargetL.dispose();
+ if ( _renderTargetR ) _renderTargetR.dispose();
+
+ };
+
+};
diff --git a/app/static/js/effects/AsciiEffect.js b/app/static/js/effects/AsciiEffect.js
new file mode 100644
index 0000000..3867d9c
--- /dev/null
+++ b/app/static/js/effects/AsciiEffect.js
@@ -0,0 +1,283 @@
+/*
+ * @author zz85 / https://github.com/zz85
+ *
+ * Ascii generation is based on http://www.nihilogic.dk/labs/jsascii/
+ * Maybe more about this later with a blog post at http://lab4games.net/zz85/blog
+ *
+ * 16 April 2012 - @blurspline
+ */
+
+THREE.AsciiEffect = function ( renderer, charSet, options ) {
+
+ // its fun to create one your own!
+
+ charSet = ( charSet === undefined ) ? ' .:-=+*#%@' : charSet;
+
+ // ' .,:;=|iI+hHOE#`$';
+ // darker bolder character set from https://github.com/saw/Canvas-ASCII-Art/
+ // ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'.split('');
+
+ if ( ! options ) options = {};
+
+ // Some ASCII settings
+
+ var bResolution = ! options[ 'resolution' ] ? 0.15 : options[ 'resolution' ]; // Higher for more details
+ var iScale = ! options[ 'scale' ] ? 1 : options[ 'scale' ];
+ var bColor = ! options[ 'color' ] ? false : options[ 'color' ]; // nice but slows down rendering!
+ var bAlpha = ! options[ 'alpha' ] ? false : options[ 'alpha' ]; // Transparency
+ var bBlock = ! options[ 'block' ] ? false : options[ 'block' ]; // blocked characters. like good O dos
+ var bInvert = ! options[ 'invert' ] ? false : options[ 'invert' ]; // black is white, white is black
+
+ var strResolution = 'low';
+
+ var width, height;
+
+ var domElement = document.createElement( 'div' );
+ domElement.style.cursor = 'default';
+
+ var oAscii = document.createElement( "table" );
+ domElement.appendChild( oAscii );
+
+ var iWidth, iHeight;
+ var oImg;
+
+ this.setSize = function ( w, h ) {
+
+ width = w;
+ height = h;
+
+ renderer.setSize( w, h );
+
+ initAsciiSize();
+
+ };
+
+
+ this.render = function ( scene, camera ) {
+
+ renderer.render( scene, camera );
+ asciifyImage( renderer, oAscii );
+
+ };
+
+ this.domElement = domElement;
+
+
+ // Throw in ascii library from http://www.nihilogic.dk/labs/jsascii/jsascii.js
+
+ /*
+ * jsAscii 0.1
+ * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/
+ * MIT License [http://www.nihilogic.dk/licenses/mit-license.txt]
+ */
+
+ function initAsciiSize() {
+
+ iWidth = Math.round( width * fResolution );
+ iHeight = Math.round( height * fResolution );
+
+ oCanvas.width = iWidth;
+ oCanvas.height = iHeight;
+ // oCanvas.style.display = "none";
+ // oCanvas.style.width = iWidth;
+ // oCanvas.style.height = iHeight;
+
+ oImg = renderer.domElement;
+
+ if ( oImg.style.backgroundColor ) {
+
+ oAscii.rows[ 0 ].cells[ 0 ].style.backgroundColor = oImg.style.backgroundColor;
+ oAscii.rows[ 0 ].cells[ 0 ].style.color = oImg.style.color;
+
+ }
+
+ oAscii.cellSpacing = 0;
+ oAscii.cellPadding = 0;
+
+ var oStyle = oAscii.style;
+ oStyle.display = "inline";
+ oStyle.width = Math.round( iWidth / fResolution * iScale ) + "px";
+ oStyle.height = Math.round( iHeight / fResolution * iScale ) + "px";
+ oStyle.whiteSpace = "pre";
+ oStyle.margin = "0px";
+ oStyle.padding = "0px";
+ oStyle.letterSpacing = fLetterSpacing + "px";
+ oStyle.fontFamily = strFont;
+ oStyle.fontSize = fFontSize + "px";
+ oStyle.lineHeight = fLineHeight + "px";
+ oStyle.textAlign = "left";
+ oStyle.textDecoration = "none";
+
+ }
+
+
+ var aDefaultCharList = ( " .,:;i1tfLCG08@" ).split( "" );
+ var aDefaultColorCharList = ( " CGO08@" ).split( "" );
+ var strFont = "courier new, monospace";
+
+ var oCanvasImg = renderer.domElement;
+
+ var oCanvas = document.createElement( "canvas" );
+ if ( ! oCanvas.getContext ) {
+
+ return;
+
+ }
+
+ var oCtx = oCanvas.getContext( "2d" );
+ if ( ! oCtx.getImageData ) {
+
+ return;
+
+ }
+
+ var aCharList = ( bColor ? aDefaultColorCharList : aDefaultCharList );
+
+ if ( charSet ) aCharList = charSet;
+
+ var fResolution = 0.5;
+
+ switch ( strResolution ) {
+
+ case "low" : fResolution = 0.25; break;
+ case "medium" : fResolution = 0.5; break;
+ case "high" : fResolution = 1; break;
+
+ }
+
+ if ( bResolution ) fResolution = bResolution;
+
+ // Setup dom
+
+ var fFontSize = ( 2 / fResolution ) * iScale;
+ var fLineHeight = ( 2 / fResolution ) * iScale;
+
+ // adjust letter-spacing for all combinations of scale and resolution to get it to fit the image width.
+
+ var fLetterSpacing = 0;
+
+ if ( strResolution == "low" ) {
+
+ switch ( iScale ) {
+ case 1 : fLetterSpacing = - 1; break;
+ case 2 :
+ case 3 : fLetterSpacing = - 2.1; break;
+ case 4 : fLetterSpacing = - 3.1; break;
+ case 5 : fLetterSpacing = - 4.15; break;
+ }
+
+ }
+
+ if ( strResolution == "medium" ) {
+
+ switch ( iScale ) {
+ case 1 : fLetterSpacing = 0; break;
+ case 2 : fLetterSpacing = - 1; break;
+ case 3 : fLetterSpacing = - 1.04; break;
+ case 4 :
+ case 5 : fLetterSpacing = - 2.1; break;
+ }
+
+ }
+
+ if ( strResolution == "high" ) {
+
+ switch ( iScale ) {
+ case 1 :
+ case 2 : fLetterSpacing = 0; break;
+ case 3 :
+ case 4 :
+ case 5 : fLetterSpacing = - 1; break;
+ }
+
+ }
+
+
+ // can't get a span or div to flow like an img element, but a table works?
+
+
+ // convert img element to ascii
+
+ function asciifyImage( canvasRenderer, oAscii ) {
+
+ oCtx.clearRect( 0, 0, iWidth, iHeight );
+ oCtx.drawImage( oCanvasImg, 0, 0, iWidth, iHeight );
+ var oImgData = oCtx.getImageData( 0, 0, iWidth, iHeight ).data;
+
+ // Coloring loop starts now
+ var strChars = "";
+
+ // console.time('rendering');
+
+ for ( var y = 0; y < iHeight; y += 2 ) {
+
+ for ( var x = 0; x < iWidth; x ++ ) {
+
+ var iOffset = ( y * iWidth + x ) * 4;
+
+ var iRed = oImgData[ iOffset ];
+ var iGreen = oImgData[ iOffset + 1 ];
+ var iBlue = oImgData[ iOffset + 2 ];
+ var iAlpha = oImgData[ iOffset + 3 ];
+ var iCharIdx;
+
+ var fBrightness;
+
+ fBrightness = ( 0.3 * iRed + 0.59 * iGreen + 0.11 * iBlue ) / 255;
+ // fBrightness = (0.3*iRed + 0.5*iGreen + 0.3*iBlue) / 255;
+
+ if ( iAlpha == 0 ) {
+
+ // should calculate alpha instead, but quick hack :)
+ //fBrightness *= (iAlpha / 255);
+ fBrightness = 1;
+
+ }
+
+ iCharIdx = Math.floor( ( 1 - fBrightness ) * ( aCharList.length - 1 ) );
+
+ if ( bInvert ) {
+
+ iCharIdx = aCharList.length - iCharIdx - 1;
+
+ }
+
+ // good for debugging
+ //fBrightness = Math.floor(fBrightness * 10);
+ //strThisChar = fBrightness;
+
+ var strThisChar = aCharList[ iCharIdx ];
+
+ if ( strThisChar === undefined || strThisChar == " " )
+ strThisChar = " ";
+
+ if ( bColor ) {
+
+ strChars += "" + strThisChar + "";
+
+ } else {
+
+ strChars += strThisChar;
+
+ }
+
+ }
+ strChars += "
";
+
+ }
+
+ oAscii.innerHTML = "" + strChars + " |
";
+
+ // console.timeEnd('rendering');
+
+ // return oAscii;
+
+ }
+
+ // end modified asciifyImage block
+
+};
diff --git a/app/static/js/effects/OutlineEffect.js b/app/static/js/effects/OutlineEffect.js
new file mode 100644
index 0000000..7e176b6
--- /dev/null
+++ b/app/static/js/effects/OutlineEffect.js
@@ -0,0 +1,517 @@
+/**
+ * @author takahirox / http://github.com/takahirox/
+ *
+ * Reference: https://en.wikipedia.org/wiki/Cel_shading
+ *
+ * // How to set default outline parameters
+ * new THREE.OutlineEffect( renderer, {
+ * defaultThickNess: 0.01,
+ * defaultColor: new THREE.Color( 0x888888 ),
+ * defaultAlpha: 0.8,
+ * defaultKeepAlive: true // keeps outline material in cache even if material is removed from scene
+ * } );
+ *
+ * // How to set outline parameters for each material
+ * material.outlineParameters = {
+ * thickNess: 0.01,
+ * color: new THREE.Color( 0x888888 ),
+ * alpha: 0.8,
+ * visible: true,
+ * keepAlive: true
+ * };
+ *
+ * TODO
+ * - support shader material without objectNormal in its vertexShader
+ */
+
+THREE.OutlineEffect = function ( renderer, parameters ) {
+
+ parameters = parameters || {};
+
+ this.enabled = true;
+
+ var defaultThickness = parameters.defaultThickness !== undefined ? parameters.defaultThickness : 0.003;
+ var defaultColor = parameters.defaultColor !== undefined ? parameters.defaultColor : new THREE.Color( 0x000000 );
+ var defaultAlpha = parameters.defaultAlpha !== undefined ? parameters.defaultAlpha : 1.0;
+ var defaultKeepAlive = parameters.defaultKeepAlive !== undefined ? parameters.defaultKeepAlive : false;
+
+ // object.material.uuid -> outlineMaterial or
+ // object.material[ n ].uuid -> outlineMaterial
+ // save at the outline material creation and release
+ // if it's unused removeThresholdCount frames
+ // unless keepAlive is true.
+ var cache = {};
+
+ var removeThresholdCount = 60;
+
+ // outlineMaterial.uuid -> object.material or
+ // outlineMaterial.uuid -> object.material[ n ]
+ // save before render and release after render.
+ var originalMaterials = {};
+
+ // object.uuid -> originalOnBeforeRender
+ // save before render and release after render.
+ var originalOnBeforeRenders = {};
+
+ //this.cache = cache; // for debug
+
+ // copied from WebGLPrograms and removed some materials
+ var shaderIDs = {
+ MeshBasicMaterial: 'basic',
+ MeshLambertMaterial: 'lambert',
+ MeshPhongMaterial: 'phong',
+ MeshToonMaterial: 'phong',
+ MeshStandardMaterial: 'physical',
+ MeshPhysicalMaterial: 'physical'
+ };
+
+ var uniformsChunk = {
+ outlineThickness: { type: "f", value: defaultThickness },
+ outlineColor: { type: "c", value: defaultColor },
+ outlineAlpha: { type: "f", value: defaultAlpha }
+ };
+
+ var vertexShaderChunk = [
+
+ "#include ",
+
+ "uniform float outlineThickness;",
+
+ "vec4 calculateOutline( vec4 pos, vec3 objectNormal, vec4 skinned ) {",
+
+ " float thickness = outlineThickness;",
+ " const float ratio = 1.0;", // TODO: support outline thickness ratio for each vertex
+ " vec4 pos2 = projectionMatrix * modelViewMatrix * vec4( skinned.xyz + objectNormal, 1.0 );",
+ // NOTE: subtract pos2 from pos because BackSide objectNormal is negative
+ " vec4 norm = normalize( pos - pos2 );",
+ " return pos + norm * thickness * pos.w * ratio;",
+
+ "}"
+
+ ].join( "\n" );
+
+ var vertexShaderChunk2 = [
+
+ "#if ! defined( LAMBERT ) && ! defined( PHONG ) && ! defined( TOON ) && ! defined( PHYSICAL )",
+ " #ifndef USE_ENVMAP",
+ " vec3 objectNormal = normalize( normal );",
+ " #endif",
+ "#endif",
+
+ "#ifdef FLIP_SIDED",
+ " objectNormal = -objectNormal;",
+ "#endif",
+
+ "#ifdef DECLARE_TRANSFORMED",
+ " vec3 transformed = vec3( position );",
+ "#endif",
+
+ "gl_Position = calculateOutline( gl_Position, objectNormal, vec4( transformed, 1.0 ) );",
+
+ "#include "
+
+ ].join( "\n" );
+
+ var fragmentShader = [
+
+ "#include ",
+ "#include ",
+
+ "uniform vec3 outlineColor;",
+ "uniform float outlineAlpha;",
+
+ "void main() {",
+
+ " gl_FragColor = vec4( outlineColor, outlineAlpha );",
+
+ " #include ",
+
+ "}"
+
+ ].join( "\n" );
+
+ function createInvisibleMaterial() {
+
+ return new THREE.ShaderMaterial( { name: 'invisible', visible: false } );
+
+ }
+
+ function createMaterial( originalMaterial ) {
+
+ var shaderID = shaderIDs[ originalMaterial.type ];
+ var originalUniforms, originalVertexShader;
+ var outlineParameters = originalMaterial.outlineParameters;
+
+ if ( shaderID !== undefined ) {
+
+ var shader = THREE.ShaderLib[ shaderID ];
+ originalUniforms = shader.uniforms;
+ originalVertexShader = shader.vertexShader;
+
+ } else if ( originalMaterial.isRawShaderMaterial === true ) {
+
+ originalUniforms = originalMaterial.uniforms;
+ originalVertexShader = originalMaterial.vertexShader;
+
+ if ( ! /attribute\s+vec3\s+position\s*;/.test( originalVertexShader ) ||
+ ! /attribute\s+vec3\s+normal\s*;/.test( originalVertexShader ) ) {
+
+ console.warn( 'THREE.OutlineEffect requires both vec3 position and normal attributes in vertex shader, ' +
+ 'does not draw outline for ' + originalMaterial.name + '(uuid:' + originalMaterial.uuid + ') material.' );
+
+ return createInvisibleMaterial();
+
+ }
+
+ } else if ( originalMaterial.isShaderMaterial === true ) {
+
+ originalUniforms = originalMaterial.uniforms;
+ originalVertexShader = originalMaterial.vertexShader;
+
+ } else {
+
+ return createInvisibleMaterial();
+
+ }
+
+ var uniforms = Object.assign( {}, originalUniforms, uniformsChunk );
+
+ var vertexShader = originalVertexShader
+ // put vertexShaderChunk right before "void main() {...}"
+ .replace( /void\s+main\s*\(\s*\)/, vertexShaderChunk + '\nvoid main()' )
+ // put vertexShaderChunk2 the end of "void main() {...}"
+ // Note: here assums originalVertexShader ends with "}" of "void main() {...}"
+ .replace( /\}\s*$/, vertexShaderChunk2 + '\n}' )
+ // remove any light related lines
+ // Note: here is very sensitive to originalVertexShader
+ // TODO: consider safer way
+ .replace( /#include\s+<[\w_]*light[\w_]*>/g, '' );
+
+ var defines = {};
+
+ if ( ! /vec3\s+transformed\s*=/.test( originalVertexShader ) &&
+ ! /#include\s+/.test( originalVertexShader ) ) defines.DECLARE_TRANSFORMED = true;
+
+ return new THREE.ShaderMaterial( {
+ defines: defines,
+ uniforms: uniforms,
+ vertexShader: vertexShader,
+ fragmentShader: fragmentShader,
+ side: THREE.BackSide,
+ //wireframe: true,
+ skinning: false,
+ morphTargets: false,
+ morphNormals: false,
+ fog: false
+ } );
+
+ }
+
+ function getOutlineMaterialFromCache( originalMaterial ) {
+
+ var data = cache[ originalMaterial.uuid ];
+
+ if ( data === undefined ) {
+
+ data = {
+ material: createMaterial( originalMaterial ),
+ used: true,
+ keepAlive: defaultKeepAlive,
+ count: 0
+ };
+
+ cache[ originalMaterial.uuid ] = data;
+
+ }
+
+ data.used = true;
+
+ return data.material;
+
+ }
+
+ function getOutlineMaterial( originalMaterial ) {
+
+ var outlineMaterial = getOutlineMaterialFromCache( originalMaterial );
+
+ originalMaterials[ outlineMaterial.uuid ] = originalMaterial;
+
+ updateOutlineMaterial( outlineMaterial, originalMaterial );
+
+ return outlineMaterial;
+
+ }
+
+ function setOutlineMaterial( object ) {
+
+ if ( object.material === undefined ) return;
+
+ if ( Array.isArray( object.material ) ) {
+
+ for ( var i = 0, il = object.material.length; i < il; i ++ ) {
+
+ object.material[ i ] = getOutlineMaterial( object.material[ i ] );
+
+ }
+
+ } else {
+
+ object.material = getOutlineMaterial( object.material );
+
+ }
+
+ originalOnBeforeRenders[ object.uuid ] = object.onBeforeRender;
+ object.onBeforeRender = onBeforeRender;
+
+ }
+
+ function restoreOriginalMaterial( object ) {
+
+ if ( object.material === undefined ) return;
+
+ if ( Array.isArray( object.material ) ) {
+
+ for ( var i = 0, il = object.material.length; i < il; i ++ ) {
+
+ object.material[ i ] = originalMaterials[ object.material[ i ].uuid ];
+
+ }
+
+ } else {
+
+ object.material = originalMaterials[ object.material.uuid ];
+
+ }
+
+ object.onBeforeRender = originalOnBeforeRenders[ object.uuid ];
+
+ }
+
+ function onBeforeRender( renderer, scene, camera, geometry, material, group ) {
+
+ var originalMaterial = originalMaterials[ material.uuid ];
+
+ // just in case
+ if ( originalMaterial === undefined ) return;
+
+ updateUniforms( material, originalMaterial );
+
+ }
+
+ function updateUniforms( material, originalMaterial ) {
+
+ var outlineParameters = originalMaterial.outlineParameters;
+
+ material.uniforms.outlineAlpha.value = originalMaterial.opacity;
+
+ if ( outlineParameters !== undefined ) {
+
+ if ( outlineParameters.thickness !== undefined ) material.uniforms.outlineThickness.value = outlineParameters.thickness;
+ if ( outlineParameters.color !== undefined ) material.uniforms.outlineColor.value.copy( outlineParameters.color );
+ if ( outlineParameters.alpha !== undefined ) material.uniforms.outlineAlpha.value = outlineParameters.alpha;
+
+ }
+
+ }
+
+ function updateOutlineMaterial( material, originalMaterial ) {
+
+ if ( material.name === 'invisible' ) return;
+
+ var outlineParameters = originalMaterial.outlineParameters;
+
+ material.skinning = originalMaterial.skinning;
+ material.morphTargets = originalMaterial.morphTargets;
+ material.morphNormals = originalMaterial.morphNormals;
+ material.fog = originalMaterial.fog;
+
+ if ( outlineParameters !== undefined ) {
+
+ if ( originalMaterial.visible === false ) {
+
+ material.visible = false;
+
+ } else {
+
+ material.visible = ( outlineParameters.visible !== undefined ) ? outlineParameters.visible : true;
+
+ }
+
+ material.transparent = ( outlineParameters.alpha !== undefined && outlineParameters.alpha < 1.0 ) ? true : originalMaterial.transparent;
+
+ if ( outlineParameters.keepAlive !== undefined ) cache[ originalMaterial.uuid ].keepAlive = outlineParameters.keepAlive;
+
+ } else {
+
+ material.transparent = originalMaterial.transparent;
+ material.visible = originalMaterial.visible;
+
+ }
+
+ if ( originalMaterial.wireframe === true || originalMaterial.depthTest === false ) material.visible = false;
+
+ }
+
+ function cleanupCache() {
+
+ var keys;
+
+ // clear originialMaterials
+ keys = Object.keys( originalMaterials );
+
+ for ( var i = 0, il = keys.length; i < il; i ++ ) {
+
+ originalMaterials[ keys[ i ] ] = undefined;
+
+ }
+
+ // clear originalOnBeforeRenders
+ keys = Object.keys( originalOnBeforeRenders );
+
+ for ( var i = 0, il = keys.length; i < il; i ++ ) {
+
+ originalOnBeforeRenders[ keys[ i ] ] = undefined;
+
+ }
+
+ // remove unused outlineMaterial from cache
+ keys = Object.keys( cache );
+
+ for ( var i = 0, il = keys.length; i < il; i ++ ) {
+
+ var key = keys[ i ];
+
+ if ( cache[ key ].used === false ) {
+
+ cache[ key ].count++;
+
+ if ( cache[ key ].keepAlive === false && cache[ key ].count > removeThresholdCount ) {
+
+ delete cache[ key ];
+
+ }
+
+ } else {
+
+ cache[ key ].used = false;
+ cache[ key ].count = 0;
+
+ }
+
+ }
+
+ }
+
+ this.render = function ( scene, camera, renderTarget, forceClear ) {
+
+ if ( this.enabled === false ) {
+
+ renderer.render( scene, camera, renderTarget, forceClear );
+ return;
+
+ }
+
+ var currentAutoClear = renderer.autoClear;
+ renderer.autoClear = this.autoClear;
+
+ // 1. render normally
+ renderer.render( scene, camera, renderTarget, forceClear );
+
+ // 2. render outline
+ var currentSceneAutoUpdate = scene.autoUpdate;
+ var currentSceneBackground = scene.background;
+ var currentShadowMapEnabled = renderer.shadowMap.enabled;
+
+ scene.autoUpdate = false;
+ scene.background = null;
+ renderer.autoClear = false;
+ renderer.shadowMap.enabled = false;
+
+ scene.traverse( setOutlineMaterial );
+
+ renderer.render( scene, camera, renderTarget );
+
+ scene.traverse( restoreOriginalMaterial );
+
+ cleanupCache();
+
+ scene.autoUpdate = currentSceneAutoUpdate;
+ scene.background = currentSceneBackground;
+ renderer.autoClear = currentAutoClear;
+ renderer.shadowMap.enabled = currentShadowMapEnabled;
+
+ };
+
+ /*
+ * See #9918
+ *
+ * The following property copies and wrapper methods enable
+ * THREE.OutlineEffect to be called from other *Effect, like
+ *
+ * effect = new THREE.VREffect( new THREE.OutlineEffect( renderer ) );
+ *
+ * function render () {
+ *
+ * effect.render( scene, camera );
+ *
+ * }
+ */
+ this.autoClear = renderer.autoClear;
+ this.domElement = renderer.domElement;
+ this.shadowMap = renderer.shadowMap;
+
+ this.clear = function ( color, depth, stencil ) {
+
+ renderer.clear( color, depth, stencil );
+
+ };
+
+ this.getPixelRatio = function () {
+
+ return renderer.getPixelRatio();
+
+ };
+
+ this.setPixelRatio = function ( value ) {
+
+ renderer.setPixelRatio( value );
+
+ };
+
+ this.getSize = function () {
+
+ return renderer.getSize();
+
+ };
+
+ this.setSize = function ( width, height, updateStyle ) {
+
+ renderer.setSize( width, height, updateStyle );
+
+ };
+
+ this.setViewport = function ( x, y, width, height ) {
+
+ renderer.setViewport( x, y, width, height );
+
+ };
+
+ this.setScissor = function ( x, y, width, height ) {
+
+ renderer.setScissor( x, y, width, height );
+
+ };
+
+ this.setScissorTest = function ( boolean ) {
+
+ renderer.setScissorTest( boolean );
+
+ };
+
+ this.setRenderTarget = function ( renderTarget ) {
+
+ renderer.setRenderTarget( renderTarget );
+
+ };
+
+};
diff --git a/app/static/js/effects/ParallaxBarrierEffect.js b/app/static/js/effects/ParallaxBarrierEffect.js
new file mode 100644
index 0000000..6756149
--- /dev/null
+++ b/app/static/js/effects/ParallaxBarrierEffect.js
@@ -0,0 +1,96 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author marklundin / http://mark-lundin.com/
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+THREE.ParallaxBarrierEffect = function ( renderer ) {
+
+ var _camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+ var _scene = new THREE.Scene();
+
+ var _stereo = new THREE.StereoCamera();
+
+ var _params = { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat };
+
+ var _renderTargetL = new THREE.WebGLRenderTarget( 512, 512, _params );
+ var _renderTargetR = new THREE.WebGLRenderTarget( 512, 512, _params );
+
+ var _material = new THREE.ShaderMaterial( {
+
+ uniforms: {
+
+ "mapLeft": { value: _renderTargetL.texture },
+ "mapRight": { value: _renderTargetR.texture }
+
+ },
+
+ vertexShader: [
+
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ " vUv = vec2( uv.x, uv.y );",
+ " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+ "}"
+
+ ].join( "\n" ),
+
+ fragmentShader: [
+
+ "uniform sampler2D mapLeft;",
+ "uniform sampler2D mapRight;",
+ "varying vec2 vUv;",
+
+ "void main() {",
+
+ " vec2 uv = vUv;",
+
+ " if ( ( mod( gl_FragCoord.y, 2.0 ) ) > 1.00 ) {",
+
+ " gl_FragColor = texture2D( mapLeft, uv );",
+
+ " } else {",
+
+ " gl_FragColor = texture2D( mapRight, uv );",
+
+ " }",
+
+ "}"
+
+ ].join( "\n" )
+
+ } );
+
+ var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), _material );
+ _scene.add( mesh );
+
+ this.setSize = function ( width, height ) {
+
+ renderer.setSize( width, height );
+
+ var pixelRatio = renderer.getPixelRatio();
+
+ _renderTargetL.setSize( width * pixelRatio, height * pixelRatio );
+ _renderTargetR.setSize( width * pixelRatio, height * pixelRatio );
+
+ };
+
+ this.render = function ( scene, camera ) {
+
+ scene.updateMatrixWorld();
+
+ if ( camera.parent === null ) camera.updateMatrixWorld();
+
+ _stereo.update( camera );
+
+ renderer.render( scene, _stereo.cameraL, _renderTargetL, true );
+ renderer.render( scene, _stereo.cameraR, _renderTargetR, true );
+ renderer.render( _scene, _camera );
+
+ };
+
+};
diff --git a/app/static/js/effects/PeppersGhostEffect.js b/app/static/js/effects/PeppersGhostEffect.js
new file mode 100644
index 0000000..7013218
--- /dev/null
+++ b/app/static/js/effects/PeppersGhostEffect.js
@@ -0,0 +1,143 @@
+/**
+ * Created by tpowellmeto on 29/10/2015.
+ *
+ * peppers ghost effect based on http://www.instructables.com/id/Reflective-Prism/?ALLSTEPS
+ */
+
+THREE.PeppersGhostEffect = function ( renderer ) {
+
+ var scope = this;
+
+ scope.cameraDistance = 15;
+ scope.reflectFromAbove = false;
+
+ // Internals
+ var _halfWidth, _width, _height;
+
+ var _cameraF = new THREE.PerspectiveCamera(); //front
+ var _cameraB = new THREE.PerspectiveCamera(); //back
+ var _cameraL = new THREE.PerspectiveCamera(); //left
+ var _cameraR = new THREE.PerspectiveCamera(); //right
+
+ var _position = new THREE.Vector3();
+ var _quaternion = new THREE.Quaternion();
+ var _scale = new THREE.Vector3();
+
+ // Initialization
+ renderer.autoClear = false;
+
+ this.setSize = function ( width, height ) {
+
+ _halfWidth = width / 2;
+ if ( width < height ) {
+
+ _width = width / 3;
+ _height = width / 3;
+
+ } else {
+
+ _width = height / 3;
+ _height = height / 3;
+
+ }
+ renderer.setSize( width, height );
+
+ };
+
+ this.render = function ( scene, camera ) {
+
+ scene.updateMatrixWorld();
+
+ if ( camera.parent === null ) camera.updateMatrixWorld();
+
+ camera.matrixWorld.decompose( _position, _quaternion, _scale );
+
+ // front
+ _cameraF.position.copy( _position );
+ _cameraF.quaternion.copy( _quaternion );
+ _cameraF.translateZ( scope.cameraDistance );
+ _cameraF.lookAt( scene.position );
+
+ // back
+ _cameraB.position.copy( _position );
+ _cameraB.quaternion.copy( _quaternion );
+ _cameraB.translateZ( - ( scope.cameraDistance ) );
+ _cameraB.lookAt( scene.position );
+ _cameraB.rotation.z += 180 * ( Math.PI / 180 );
+
+ // left
+ _cameraL.position.copy( _position );
+ _cameraL.quaternion.copy( _quaternion );
+ _cameraL.translateX( - ( scope.cameraDistance ) );
+ _cameraL.lookAt( scene.position );
+ _cameraL.rotation.x += 90 * ( Math.PI / 180 );
+
+ // right
+ _cameraR.position.copy( _position );
+ _cameraR.quaternion.copy( _quaternion );
+ _cameraR.translateX( scope.cameraDistance );
+ _cameraR.lookAt( scene.position );
+ _cameraR.rotation.x += 90 * ( Math.PI / 180 );
+
+
+ renderer.clear();
+ renderer.setScissorTest( true );
+
+ renderer.setScissor( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+ renderer.setViewport( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+
+ if ( scope.reflectFromAbove ) {
+
+ renderer.render( scene, _cameraB );
+
+ } else {
+
+ renderer.render( scene, _cameraF );
+
+ }
+
+ renderer.setScissor( _halfWidth - ( _width / 2 ), 0, _width, _height );
+ renderer.setViewport( _halfWidth - ( _width / 2 ), 0, _width, _height );
+
+ if ( scope.reflectFromAbove ) {
+
+ renderer.render( scene, _cameraF );
+
+ } else {
+
+ renderer.render( scene, _cameraB );
+
+ }
+
+ renderer.setScissor( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+ renderer.setViewport( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+
+ if ( scope.reflectFromAbove ) {
+
+ renderer.render( scene, _cameraR );
+
+ } else {
+
+ renderer.render( scene, _cameraL );
+
+ }
+
+ renderer.setScissor( _halfWidth + ( _width / 2 ), _height, _width, _height );
+ renderer.setViewport( _halfWidth + ( _width / 2 ), _height, _width, _height );
+
+ if ( scope.reflectFromAbove ) {
+
+ renderer.render( scene, _cameraL );
+
+ } else {
+
+ renderer.render( scene, _cameraR );
+
+ }
+
+ renderer.setScissorTest( false );
+
+ };
+
+
+};
diff --git a/app/static/js/effects/StereoEffect.js b/app/static/js/effects/StereoEffect.js
new file mode 100644
index 0000000..ab5006d
--- /dev/null
+++ b/app/static/js/effects/StereoEffect.js
@@ -0,0 +1,50 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ * @authod mrdoob / http://mrdoob.com/
+ * @authod arodic / http://aleksandarrodic.com/
+ * @authod fonserbc / http://fonserbc.github.io/
+*/
+
+THREE.StereoEffect = function ( renderer ) {
+
+ var _stereo = new THREE.StereoCamera();
+ _stereo.aspect = 0.5;
+
+ this.setEyeSeparation = function ( eyeSep ) {
+
+ _stereo.eyeSep = eyeSep;
+
+ };
+
+ this.setSize = function ( width, height ) {
+
+ renderer.setSize( width, height );
+
+ };
+
+ this.render = function ( scene, camera ) {
+
+ scene.updateMatrixWorld();
+
+ if ( camera.parent === null ) camera.updateMatrixWorld();
+
+ _stereo.update( camera );
+
+ var size = renderer.getSize();
+
+ if ( renderer.autoClear ) renderer.clear();
+ renderer.setScissorTest( true );
+
+ renderer.setScissor( 0, 0, size.width / 2, size.height );
+ renderer.setViewport( 0, 0, size.width / 2, size.height );
+ renderer.render( scene, _stereo.cameraL );
+
+ renderer.setScissor( size.width / 2, 0, size.width / 2, size.height );
+ renderer.setViewport( size.width / 2, 0, size.width / 2, size.height );
+ renderer.render( scene, _stereo.cameraR );
+
+ renderer.setScissorTest( false );
+
+ };
+
+};
diff --git a/app/static/js/effects/VREffect.js b/app/static/js/effects/VREffect.js
new file mode 100644
index 0000000..9a2482e
--- /dev/null
+++ b/app/static/js/effects/VREffect.js
@@ -0,0 +1,533 @@
+/**
+ * @author dmarcos / https://github.com/dmarcos
+ * @author mrdoob / http://mrdoob.com
+ *
+ * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html
+ *
+ * Firefox: http://mozvr.com/downloads/
+ * Chromium: https://webvr.info/get-chrome
+ */
+
+THREE.VREffect = function ( renderer, onError ) {
+
+ var vrDisplay, vrDisplays;
+ var eyeTranslationL = new THREE.Vector3();
+ var eyeTranslationR = new THREE.Vector3();
+ var renderRectL, renderRectR;
+ var headMatrix = new THREE.Matrix4();
+ var eyeMatrixL = new THREE.Matrix4();
+ var eyeMatrixR = new THREE.Matrix4();
+
+ var frameData = null;
+
+ if ( 'VRFrameData' in window ) {
+
+ frameData = new window.VRFrameData();
+
+ }
+
+ function gotVRDisplays( displays ) {
+
+ vrDisplays = displays;
+
+ if ( displays.length > 0 ) {
+
+ vrDisplay = displays[ 0 ];
+
+ } else {
+
+ if ( onError ) onError( 'HMD not available' );
+
+ }
+
+ }
+
+ if ( navigator.getVRDisplays ) {
+
+ navigator.getVRDisplays().then( gotVRDisplays ).catch( function () {
+
+ console.warn( 'THREE.VREffect: Unable to get VR Displays' );
+
+ } );
+
+ }
+
+ //
+
+ this.isPresenting = false;
+
+ var scope = this;
+
+ var rendererSize = renderer.getSize();
+ var rendererUpdateStyle = false;
+ var rendererPixelRatio = renderer.getPixelRatio();
+
+ this.getVRDisplay = function () {
+
+ return vrDisplay;
+
+ };
+
+ this.setVRDisplay = function ( value ) {
+
+ vrDisplay = value;
+
+ };
+
+ this.getVRDisplays = function () {
+
+ console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' );
+ return vrDisplays;
+
+ };
+
+ this.setSize = function ( width, height, updateStyle ) {
+
+ rendererSize = { width: width, height: height };
+ rendererUpdateStyle = updateStyle;
+
+ if ( scope.isPresenting ) {
+
+ var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
+ renderer.setPixelRatio( 1 );
+ renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false );
+
+ } else {
+
+ renderer.setPixelRatio( rendererPixelRatio );
+ renderer.setSize( width, height, updateStyle );
+
+ }
+
+ };
+
+ // VR presentation
+
+ var canvas = renderer.domElement;
+ var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ];
+ var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ];
+
+ function onVRDisplayPresentChange() {
+
+ var wasPresenting = scope.isPresenting;
+ scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting;
+
+ if ( scope.isPresenting ) {
+
+ var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
+ var eyeWidth = eyeParamsL.renderWidth;
+ var eyeHeight = eyeParamsL.renderHeight;
+
+ if ( ! wasPresenting ) {
+
+ rendererPixelRatio = renderer.getPixelRatio();
+ rendererSize = renderer.getSize();
+
+ renderer.setPixelRatio( 1 );
+ renderer.setSize( eyeWidth * 2, eyeHeight, false );
+
+ }
+
+ } else if ( wasPresenting ) {
+
+ renderer.setPixelRatio( rendererPixelRatio );
+ renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle );
+
+ }
+
+ }
+
+ window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );
+
+ this.setFullScreen = function ( boolean ) {
+
+ return new Promise( function ( resolve, reject ) {
+
+ if ( vrDisplay === undefined ) {
+
+ reject( new Error( 'No VR hardware found.' ) );
+ return;
+
+ }
+
+ if ( scope.isPresenting === boolean ) {
+
+ resolve();
+ return;
+
+ }
+
+ if ( boolean ) {
+
+ resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) );
+
+ } else {
+
+ resolve( vrDisplay.exitPresent() );
+
+ }
+
+ } );
+
+ };
+
+ this.requestPresent = function () {
+
+ return this.setFullScreen( true );
+
+ };
+
+ this.exitPresent = function () {
+
+ return this.setFullScreen( false );
+
+ };
+
+ this.requestAnimationFrame = function ( f ) {
+
+ if ( vrDisplay !== undefined ) {
+
+ return vrDisplay.requestAnimationFrame( f );
+
+ } else {
+
+ return window.requestAnimationFrame( f );
+
+ }
+
+ };
+
+ this.cancelAnimationFrame = function ( h ) {
+
+ if ( vrDisplay !== undefined ) {
+
+ vrDisplay.cancelAnimationFrame( h );
+
+ } else {
+
+ window.cancelAnimationFrame( h );
+
+ }
+
+ };
+
+ this.submitFrame = function () {
+
+ if ( vrDisplay !== undefined && scope.isPresenting ) {
+
+ vrDisplay.submitFrame();
+
+ }
+
+ };
+
+ this.autoSubmitFrame = true;
+
+ // render
+
+ var cameraL = new THREE.PerspectiveCamera();
+ cameraL.layers.enable( 1 );
+
+ var cameraR = new THREE.PerspectiveCamera();
+ cameraR.layers.enable( 2 );
+
+ this.render = function ( scene, camera, renderTarget, forceClear ) {
+
+ if ( vrDisplay && scope.isPresenting ) {
+
+ var autoUpdate = scene.autoUpdate;
+
+ if ( autoUpdate ) {
+
+ scene.updateMatrixWorld();
+ scene.autoUpdate = false;
+
+ }
+
+ if ( Array.isArray( scene ) ) {
+
+ console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' );
+ scene = scene[ 0 ];
+
+ }
+
+ // When rendering we don't care what the recommended size is, only what the actual size
+ // of the backbuffer is.
+ var size = renderer.getSize();
+ var layers = vrDisplay.getLayers();
+ var leftBounds;
+ var rightBounds;
+
+ if ( layers.length ) {
+
+ var layer = layers[ 0 ];
+
+ leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds;
+ rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds;
+
+ } else {
+
+ leftBounds = defaultLeftBounds;
+ rightBounds = defaultRightBounds;
+
+ }
+
+ renderRectL = {
+ x: Math.round( size.width * leftBounds[ 0 ] ),
+ y: Math.round( size.height * leftBounds[ 1 ] ),
+ width: Math.round( size.width * leftBounds[ 2 ] ),
+ height: Math.round( size.height * leftBounds[ 3 ] )
+ };
+ renderRectR = {
+ x: Math.round( size.width * rightBounds[ 0 ] ),
+ y: Math.round( size.height * rightBounds[ 1 ] ),
+ width: Math.round( size.width * rightBounds[ 2 ] ),
+ height: Math.round( size.height * rightBounds[ 3 ] )
+ };
+
+ if ( renderTarget ) {
+
+ renderer.setRenderTarget( renderTarget );
+ renderTarget.scissorTest = true;
+
+ } else {
+
+ renderer.setRenderTarget( null );
+ renderer.setScissorTest( true );
+
+ }
+
+ if ( renderer.autoClear || forceClear ) renderer.clear();
+
+ if ( camera.parent === null ) camera.updateMatrixWorld();
+
+ camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );
+
+ cameraR.position.copy( cameraL.position );
+ cameraR.quaternion.copy( cameraL.quaternion );
+ cameraR.scale.copy( cameraL.scale );
+
+ if ( vrDisplay.getFrameData ) {
+
+ vrDisplay.depthNear = camera.near;
+ vrDisplay.depthFar = camera.far;
+
+ vrDisplay.getFrameData( frameData );
+
+ cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix;
+ cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix;
+
+ getEyeMatrices( frameData );
+
+ cameraL.updateMatrix();
+ cameraL.matrix.multiply( eyeMatrixL );
+ cameraL.matrix.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );
+
+ cameraR.updateMatrix();
+ cameraR.matrix.multiply( eyeMatrixR );
+ cameraR.matrix.decompose( cameraR.position, cameraR.quaternion, cameraR.scale );
+
+ } else {
+
+ var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
+ var eyeParamsR = vrDisplay.getEyeParameters( 'right' );
+
+ cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far );
+ cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far );
+
+ eyeTranslationL.fromArray( eyeParamsL.offset );
+ eyeTranslationR.fromArray( eyeParamsR.offset );
+
+ cameraL.translateOnAxis( eyeTranslationL, cameraL.scale.x );
+ cameraR.translateOnAxis( eyeTranslationR, cameraR.scale.x );
+
+ }
+
+ // render left eye
+ if ( renderTarget ) {
+
+ renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
+ renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
+
+ } else {
+
+ renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
+ renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
+
+ }
+ renderer.render( scene, cameraL, renderTarget, forceClear );
+
+ // render right eye
+ if ( renderTarget ) {
+
+ renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
+ renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
+
+ } else {
+
+ renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
+ renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
+
+ }
+ renderer.render( scene, cameraR, renderTarget, forceClear );
+
+ if ( renderTarget ) {
+
+ renderTarget.viewport.set( 0, 0, size.width, size.height );
+ renderTarget.scissor.set( 0, 0, size.width, size.height );
+ renderTarget.scissorTest = false;
+ renderer.setRenderTarget( null );
+
+ } else {
+
+ renderer.setViewport( 0, 0, size.width, size.height );
+ renderer.setScissorTest( false );
+
+ }
+
+ if ( autoUpdate ) {
+
+ scene.autoUpdate = true;
+
+ }
+
+ if ( scope.autoSubmitFrame ) {
+
+ scope.submitFrame();
+
+ }
+
+ return;
+
+ }
+
+ // Regular render mode if not HMD
+
+ renderer.render( scene, camera, renderTarget, forceClear );
+
+ };
+
+ this.dispose = function () {
+
+ window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );
+
+ };
+
+ //
+
+ var poseOrientation = new THREE.Quaternion();
+ var posePosition = new THREE.Vector3();
+
+ // Compute model matrices of the eyes with respect to the head.
+ function getEyeMatrices( frameData ) {
+
+ // Compute the matrix for the position of the head based on the pose
+ if ( frameData.pose.orientation ) {
+
+ poseOrientation.fromArray( frameData.pose.orientation );
+ headMatrix.makeRotationFromQuaternion( poseOrientation );
+
+ } else {
+
+ headMatrix.identity();
+
+ }
+
+ if ( frameData.pose.position ) {
+
+ posePosition.fromArray( frameData.pose.position );
+ headMatrix.setPosition( posePosition );
+
+ }
+
+ // The view matrix transforms vertices from sitting space to eye space. As such, the view matrix can be thought of as a product of two matrices:
+ // headToEyeMatrix * sittingToHeadMatrix
+
+ // The headMatrix that we've calculated above is the model matrix of the head in sitting space, which is the inverse of sittingToHeadMatrix.
+ // So when we multiply the view matrix with headMatrix, we're left with headToEyeMatrix:
+ // viewMatrix * headMatrix = headToEyeMatrix * sittingToHeadMatrix * headMatrix = headToEyeMatrix
+
+ eyeMatrixL.fromArray( frameData.leftViewMatrix );
+ eyeMatrixL.multiply( headMatrix );
+ eyeMatrixR.fromArray( frameData.rightViewMatrix );
+ eyeMatrixR.multiply( headMatrix );
+
+ // The eye's model matrix in head space is the inverse of headToEyeMatrix we calculated above.
+
+ eyeMatrixL.getInverse( eyeMatrixL );
+ eyeMatrixR.getInverse( eyeMatrixR );
+
+ }
+
+ function fovToNDCScaleOffset( fov ) {
+
+ var pxscale = 2.0 / ( fov.leftTan + fov.rightTan );
+ var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5;
+ var pyscale = 2.0 / ( fov.upTan + fov.downTan );
+ var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5;
+ return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] };
+
+ }
+
+ function fovPortToProjection( fov, rightHanded, zNear, zFar ) {
+
+ rightHanded = rightHanded === undefined ? true : rightHanded;
+ zNear = zNear === undefined ? 0.01 : zNear;
+ zFar = zFar === undefined ? 10000.0 : zFar;
+
+ var handednessScale = rightHanded ? - 1.0 : 1.0;
+
+ // start with an identity matrix
+ var mobj = new THREE.Matrix4();
+ var m = mobj.elements;
+
+ // and with scale/offset info for normalized device coords
+ var scaleAndOffset = fovToNDCScaleOffset( fov );
+
+ // X result, map clip edges to [-w,+w]
+ m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ];
+ m[ 0 * 4 + 1 ] = 0.0;
+ m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale;
+ m[ 0 * 4 + 3 ] = 0.0;
+
+ // Y result, map clip edges to [-w,+w]
+ // Y offset is negated because this proj matrix transforms from world coords with Y=up,
+ // but the NDC scaling has Y=down (thanks D3D?)
+ m[ 1 * 4 + 0 ] = 0.0;
+ m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ];
+ m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale;
+ m[ 1 * 4 + 3 ] = 0.0;
+
+ // Z result (up to the app)
+ m[ 2 * 4 + 0 ] = 0.0;
+ m[ 2 * 4 + 1 ] = 0.0;
+ m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale;
+ m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar );
+
+ // W result (= Z in)
+ m[ 3 * 4 + 0 ] = 0.0;
+ m[ 3 * 4 + 1 ] = 0.0;
+ m[ 3 * 4 + 2 ] = handednessScale;
+ m[ 3 * 4 + 3 ] = 0.0;
+
+ mobj.transpose();
+ return mobj;
+
+ }
+
+ function fovToProjection( fov, rightHanded, zNear, zFar ) {
+
+ var DEG2RAD = Math.PI / 180.0;
+
+ var fovPort = {
+ upTan: Math.tan( fov.upDegrees * DEG2RAD ),
+ downTan: Math.tan( fov.downDegrees * DEG2RAD ),
+ leftTan: Math.tan( fov.leftDegrees * DEG2RAD ),
+ rightTan: Math.tan( fov.rightDegrees * DEG2RAD )
+ };
+
+ return fovPortToProjection( fovPort, rightHanded, zNear, zFar );
+
+ }
+
+};
diff --git a/app/static/js/exporters/GLTFExporter.js b/app/static/js/exporters/GLTFExporter.js
new file mode 100644
index 0000000..cfa0469
--- /dev/null
+++ b/app/static/js/exporters/GLTFExporter.js
@@ -0,0 +1,1025 @@
+/**
+ * @author fernandojsg / http://fernandojsg.com
+ */
+
+//------------------------------------------------------------------------------
+// Constants
+//------------------------------------------------------------------------------
+var WEBGL_CONSTANTS = {
+ POINTS: 0x0000,
+ LINES: 0x0001,
+ LINE_LOOP: 0x0002,
+ LINE_STRIP: 0x0003,
+ TRIANGLES: 0x0004,
+ TRIANGLE_STRIP: 0x0005,
+ TRIANGLE_FAN: 0x0006,
+
+ UNSIGNED_BYTE: 0x1401,
+ UNSIGNED_SHORT: 0x1403,
+ FLOAT: 0x1406,
+ UNSIGNED_INT: 0x1405,
+ ARRAY_BUFFER: 0x8892,
+ ELEMENT_ARRAY_BUFFER: 0x8893,
+
+ NEAREST: 0x2600,
+ LINEAR: 0x2601,
+ NEAREST_MIPMAP_NEAREST: 0x2700,
+ LINEAR_MIPMAP_NEAREST: 0x2701,
+ NEAREST_MIPMAP_LINEAR: 0x2702,
+ LINEAR_MIPMAP_LINEAR: 0x2703
+};
+
+var THREE_TO_WEBGL = {
+ // @TODO Replace with computed property name [THREE.*] when available on es6
+ 1003: WEBGL_CONSTANTS.NEAREST,
+ 1004: WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST,
+ 1005: WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR,
+ 1006: WEBGL_CONSTANTS.LINEAR,
+ 1007: WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST,
+ 1008: WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR
+};
+
+//------------------------------------------------------------------------------
+// GLTF Exporter
+//------------------------------------------------------------------------------
+THREE.GLTFExporter = function () {};
+
+THREE.GLTFExporter.prototype = {
+
+ constructor: THREE.GLTFExporter,
+
+ /**
+ * Parse scenes and generate GLTF output
+ * @param {THREE.Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes
+ * @param {Function} onDone Callback on completed
+ * @param {Object} options options
+ * trs: Exports position, rotation and scale instead of matrix
+ */
+ parse: function ( input, onDone, options ) {
+
+ var DEFAULT_OPTIONS = {
+ trs: false,
+ onlyVisible: true,
+ truncateDrawRange: true
+ };
+
+ options = Object.assign( {}, DEFAULT_OPTIONS, options );
+
+ var outputJSON = {
+
+ asset: {
+
+ version: "2.0",
+ generator: "THREE.GLTFExporter"
+
+ }
+
+ };
+
+ var byteOffset = 0;
+ var dataViews = [];
+ var cachedData = {
+
+ images: {},
+ materials: {}
+
+ };
+
+ /**
+ * Compare two arrays
+ */
+ /**
+ * Compare two arrays
+ * @param {Array} array1 Array 1 to compare
+ * @param {Array} array2 Array 2 to compare
+ * @return {Boolean} Returns true if both arrays are equal
+ */
+ function equalArray( array1, array2 ) {
+
+ return ( array1.length === array2.length ) && array1.every( function ( element, index ) {
+
+ return element === array2[ index ];
+
+ } );
+
+ }
+
+ /**
+ * Get the min and he max vectors from the given attribute
+ * @param {THREE.WebGLAttribute} attribute Attribute to find the min/max
+ * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
+ */
+ function getMinMax( attribute ) {
+
+ var output = {
+
+ min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
+ max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
+
+ };
+
+ for ( var i = 0; i < attribute.count; i ++ ) {
+
+ for ( var a = 0; a < attribute.itemSize; a ++ ) {
+
+ var value = attribute.array[ i * attribute.itemSize + a ];
+ output.min[ a ] = Math.min( output.min[ a ], value );
+ output.max[ a ] = Math.max( output.max[ a ], value );
+
+ }
+
+ }
+
+ return output;
+
+ }
+
+ /**
+ * Process a buffer to append to the default one.
+ * @param {THREE.BufferAttribute} attribute Attribute to store
+ * @param {Integer} componentType Component type (Unsigned short, unsigned int or float)
+ * @return {Integer} Index of the buffer created (Currently always 0)
+ */
+ function processBuffer( attribute, componentType, start, count ) {
+
+ if ( ! outputJSON.buffers ) {
+
+ outputJSON.buffers = [
+
+ {
+
+ byteLength: 0,
+ uri: ''
+
+ }
+
+ ];
+
+ }
+
+ var offset = 0;
+ var componentSize = componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ? 2 : 4;
+
+ // Create a new dataview and dump the attribute's array into it
+ var byteLength = count * attribute.itemSize * componentSize;
+
+ var dataView = new DataView( new ArrayBuffer( byteLength ) );
+
+ for ( var i = start; i < start + count; i ++ ) {
+
+ for ( var a = 0; a < attribute.itemSize; a ++ ) {
+
+ var value = attribute.array[ i * attribute.itemSize + a ];
+
+ if ( componentType === WEBGL_CONSTANTS.FLOAT ) {
+
+ dataView.setFloat32( offset, value, true );
+
+ } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {
+
+ dataView.setUint8( offset, value, true );
+
+ } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
+
+ dataView.setUint16( offset, value, true );
+
+ }
+
+ offset += componentSize;
+
+ }
+
+ }
+
+ // We just use one buffer
+ dataViews.push( dataView );
+
+ // Always using just one buffer
+ return 0;
+
+ }
+
+ /**
+ * Process and generate a BufferView
+ * @param {[type]} data [description]
+ * @return {[type]} [description]
+ */
+ function processBufferView( data, componentType, start, count ) {
+
+ var isVertexAttributes = componentType === WEBGL_CONSTANTS.FLOAT;
+
+ if ( ! outputJSON.bufferViews ) {
+
+ outputJSON.bufferViews = [];
+
+ }
+
+ var componentSize = componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ? 2 : 4;
+
+ // Create a new dataview and dump the attribute's array into it
+ var byteLength = count * data.itemSize * componentSize;
+
+ var gltfBufferView = {
+
+ buffer: processBuffer( data, componentType, start, count ),
+ byteOffset: byteOffset,
+ byteLength: byteLength,
+ byteStride: data.itemSize * componentSize,
+ target: isVertexAttributes ? WEBGL_CONSTANTS.ARRAY_BUFFER : WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER
+
+ };
+
+ byteOffset += byteLength;
+
+ outputJSON.bufferViews.push( gltfBufferView );
+
+ // @TODO Ideally we'll have just two bufferviews: 0 is for vertex attributes, 1 for indices
+ var output = {
+
+ id: outputJSON.bufferViews.length - 1,
+ byteLength: 0
+
+ };
+
+ return output;
+
+ }
+
+ /**
+ * Process attribute to generate an accessor
+ * @param {THREE.WebGLAttribute} attribute Attribute to process
+ * @return {Integer} Index of the processed accessor on the "accessors" array
+ */
+ function processAccessor( attribute, geometry ) {
+
+ if ( ! outputJSON.accessors ) {
+
+ outputJSON.accessors = [];
+
+ }
+
+ var types = [
+
+ 'SCALAR',
+ 'VEC2',
+ 'VEC3',
+ 'VEC4'
+
+ ];
+
+ var componentType;
+
+ // Detect the component type of the attribute array (float, uint or ushort)
+ if ( attribute.array.constructor === Float32Array ) {
+
+ componentType = WEBGL_CONSTANTS.FLOAT;
+
+ } else if ( attribute.array.constructor === Uint32Array ) {
+
+ componentType = WEBGL_CONSTANTS.UNSIGNED_INT;
+
+ } else if ( attribute.array.constructor === Uint16Array ) {
+
+ componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;
+
+ } else {
+
+ throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );
+
+ }
+
+ var minMax = getMinMax( attribute );
+
+ var start = 0;
+ var count = attribute.count;
+
+ // @TODO Indexed buffer geometry with drawRange not supported yet
+ if ( options.truncateDrawRange && geometry.index === null ) {
+
+ start = geometry.drawRange.start;
+ count = geometry.drawRange.count !== Infinity ? geometry.drawRange.count : attribute.count;
+
+ }
+
+ var bufferView = processBufferView( attribute, componentType, start, count );
+
+ var gltfAccessor = {
+
+ bufferView: bufferView.id,
+ byteOffset: bufferView.byteOffset,
+ componentType: componentType,
+ count: count,
+ max: minMax.max,
+ min: minMax.min,
+ type: types[ attribute.itemSize - 1 ]
+
+ };
+
+ outputJSON.accessors.push( gltfAccessor );
+
+ return outputJSON.accessors.length - 1;
+
+ }
+
+ /**
+ * Process image
+ * @param {Texture} map Texture to process
+ * @return {Integer} Index of the processed texture in the "images" array
+ */
+ function processImage( map ) {
+
+ if ( cachedData.images[ map.uuid ] ) {
+
+ return cachedData.images[ map.uuid ];
+
+ }
+
+ if ( ! outputJSON.images ) {
+
+ outputJSON.images = [];
+
+ }
+
+ var gltfImage = {};
+
+ if ( options.embedImages ) {
+
+ // @TODO { bufferView, mimeType }
+
+ } else {
+
+ // @TODO base64 based on options
+ gltfImage.uri = map.image.src;
+
+ }
+
+ outputJSON.images.push( gltfImage );
+
+ var index = outputJSON.images.length - 1;
+ cachedData.images[ map.uuid ] = index;
+
+ return index;
+
+ }
+
+ /**
+ * Process sampler
+ * @param {Texture} map Texture to process
+ * @return {Integer} Index of the processed texture in the "samplers" array
+ */
+ function processSampler( map ) {
+
+ if ( ! outputJSON.samplers ) {
+
+ outputJSON.samplers = [];
+
+ }
+
+ var gltfSampler = {
+
+ magFilter: THREE_TO_WEBGL[ map.magFilter ],
+ minFilter: THREE_TO_WEBGL[ map.minFilter ],
+ wrapS: THREE_TO_WEBGL[ map.wrapS ],
+ wrapT: THREE_TO_WEBGL[ map.wrapT ]
+
+ };
+
+ outputJSON.samplers.push( gltfSampler );
+
+ return outputJSON.samplers.length - 1;
+
+ }
+
+ /**
+ * Process texture
+ * @param {Texture} map Map to process
+ * @return {Integer} Index of the processed texture in the "textures" array
+ */
+ function processTexture( map ) {
+
+ if ( ! outputJSON.textures ) {
+
+ outputJSON.textures = [];
+
+ }
+
+ var gltfTexture = {
+
+ sampler: processSampler( map ),
+ source: processImage( map )
+
+ };
+
+ outputJSON.textures.push( gltfTexture );
+
+ return outputJSON.textures.length - 1;
+
+ }
+
+ /**
+ * Process material
+ * @param {THREE.Material} material Material to process
+ * @return {Integer} Index of the processed material in the "materials" array
+ */
+ function processMaterial( material ) {
+
+ if ( cachedData.materials[ material.uuid ] ) {
+
+ return cachedData.materials[ material.uuid ];
+
+ }
+
+ if ( ! outputJSON.materials ) {
+
+ outputJSON.materials = [];
+
+ }
+
+ if ( material instanceof THREE.ShaderMaterial ) {
+
+ console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
+ return null;
+
+ }
+
+
+ if ( ! ( material instanceof THREE.MeshStandardMaterial ) ) {
+
+ console.warn( 'GLTFExporter: Currently just THREE.StandardMaterial is supported. Material conversion may lose information.' );
+
+ }
+
+ // @QUESTION Should we avoid including any attribute that has the default value?
+ var gltfMaterial = {
+
+ pbrMetallicRoughness: {}
+
+ };
+
+ // pbrMetallicRoughness.baseColorFactor
+ var color = material.color.toArray().concat( [ material.opacity ] );
+
+ if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
+
+ gltfMaterial.pbrMetallicRoughness.baseColorFactor = color;
+
+ }
+
+ if ( material instanceof THREE.MeshStandardMaterial ) {
+
+ gltfMaterial.pbrMetallicRoughness.metallicFactor = material.metalness;
+ gltfMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness;
+
+ } else {
+
+ gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.5;
+ gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.5;
+
+ }
+
+ // pbrMetallicRoughness.baseColorTexture
+ if ( material.map ) {
+
+ gltfMaterial.pbrMetallicRoughness.baseColorTexture = {
+
+ index: processTexture( material.map )
+
+ };
+
+ }
+
+ if ( material instanceof THREE.MeshBasicMaterial ||
+ material instanceof THREE.LineBasicMaterial ||
+ material instanceof THREE.PointsMaterial ) {
+
+ } else {
+
+ // emissiveFactor
+ var emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity ).toArray();
+
+ if ( ! equalArray( emissive, [ 0, 0, 0 ] ) ) {
+
+ gltfMaterial.emissiveFactor = emissive;
+
+ }
+
+ // emissiveTexture
+ if ( material.emissiveMap ) {
+
+ gltfMaterial.emissiveTexture = {
+
+ index: processTexture( material.emissiveMap )
+
+ };
+
+ }
+
+ }
+
+ // normalTexture
+ if ( material.normalMap ) {
+
+ gltfMaterial.normalTexture = {
+
+ index: processTexture( material.normalMap )
+
+ };
+
+ if ( material.normalScale.x !== - 1 ) {
+
+ if ( material.normalScale.x !== material.normalScale.y ) {
+
+ console.warn( 'THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.' );
+
+ }
+
+ gltfMaterial.normalTexture.scale = material.normalScale.x;
+
+ }
+
+ }
+
+ // occlusionTexture
+ if ( material.aoMap ) {
+
+ gltfMaterial.occlusionTexture = {
+
+ index: processTexture( material.aoMap )
+
+ };
+
+ if ( material.aoMapIntensity !== 1.0 ) {
+
+ gltfMaterial.occlusionTexture.strength = material.aoMapIntensity;
+
+ }
+
+ }
+
+ // alphaMode
+ if ( material.transparent ) {
+
+ gltfMaterial.alphaMode = 'MASK'; // @FIXME We should detect MASK or BLEND
+
+ if ( material.alphaTest !== 0.5 ) {
+
+ gltfMaterial.alphaCutoff = material.alphaTest;
+
+ }
+
+ }
+
+ // doubleSided
+ if ( material.side === THREE.DoubleSide ) {
+
+ gltfMaterial.doubleSided = true;
+
+ }
+
+ if ( material.name ) {
+
+ gltfMaterial.name = material.name;
+
+ }
+
+ outputJSON.materials.push( gltfMaterial );
+
+ var index = outputJSON.materials.length - 1;
+ cachedData.materials[ material.uuid ] = index;
+
+ return index;
+
+ }
+
+ /**
+ * Process mesh
+ * @param {THREE.Mesh} mesh Mesh to process
+ * @return {Integer} Index of the processed mesh in the "meshes" array
+ */
+ function processMesh( mesh ) {
+
+ if ( ! outputJSON.meshes ) {
+
+ outputJSON.meshes = [];
+
+ }
+
+ var geometry = mesh.geometry;
+
+ var mode;
+
+ // Use the correct mode
+ if ( mesh instanceof THREE.LineSegments ) {
+
+ mode = WEBGL_CONSTANTS.LINES;
+
+ } else if ( mesh instanceof THREE.LineLoop ) {
+
+ mode = WEBGL_CONSTANTS.LINE_LOOP;
+
+ } else if ( mesh instanceof THREE.Line ) {
+
+ mode = WEBGL_CONSTANTS.LINE_STRIP;
+
+ } else if ( mesh instanceof THREE.Points ) {
+
+ mode = WEBGL_CONSTANTS.POINTS;
+
+ } else {
+
+ if ( ! geometry.isBufferGeometry ) {
+
+ var geometryTemp = new THREE.BufferGeometry();
+ geometryTemp.fromGeometry( geometry );
+ geometry = geometryTemp;
+
+ }
+
+ if ( mesh.drawMode === THREE.TriangleFanDrawMode ) {
+
+ console.warn( 'GLTFExporter: TriangleFanDrawMode and wireframe incompatible.' );
+ mode = WEBGL_CONSTANTS.TRIANGLE_FAN;
+
+ } else if ( mesh.drawMode === THREE.TriangleStripDrawMode ) {
+
+ mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINE_STRIP : WEBGL_CONSTANTS.TRIANGLE_STRIP;
+
+ } else {
+
+ mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
+
+ }
+
+ }
+
+ var gltfMesh = {
+ primitives: [
+ {
+ mode: mode,
+ attributes: {},
+ }
+ ]
+ };
+
+ var material = processMaterial( mesh.material );
+ if ( material !== null ) {
+
+ gltfMesh.primitives[ 0 ].material = material;
+
+ }
+
+
+ if ( geometry.index ) {
+
+ gltfMesh.primitives[ 0 ].indices = processAccessor( geometry.index, geometry );
+
+ }
+
+ // We've just one primitive per mesh
+ var gltfAttributes = gltfMesh.primitives[ 0 ].attributes;
+
+ // Conversion between attributes names in threejs and gltf spec
+ var nameConversion = {
+
+ uv: 'TEXCOORD_0',
+ uv2: 'TEXCOORD_1',
+ color: 'COLOR_0',
+ skinWeight: 'WEIGHTS_0',
+ skinIndex: 'JOINTS_0'
+
+ };
+
+ // @QUESTION Detect if .vertexColors = THREE.VertexColors?
+ // For every attribute create an accessor
+ for ( var attributeName in geometry.attributes ) {
+
+ var attribute = geometry.attributes[ attributeName ];
+ attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
+ gltfAttributes[ attributeName ] = processAccessor( attribute, geometry );
+
+ }
+
+ outputJSON.meshes.push( gltfMesh );
+
+ return outputJSON.meshes.length - 1;
+
+ }
+
+ /**
+ * Process camera
+ * @param {THREE.Camera} camera Camera to process
+ * @return {Integer} Index of the processed mesh in the "camera" array
+ */
+ function processCamera( camera ) {
+
+ if ( ! outputJSON.cameras ) {
+
+ outputJSON.cameras = [];
+
+ }
+
+ var isOrtho = camera instanceof THREE.OrthographicCamera;
+
+ var gltfCamera = {
+
+ type: isOrtho ? 'orthographic' : 'perspective'
+
+ };
+
+ if ( isOrtho ) {
+
+ gltfCamera.orthographic = {
+
+ xmag: camera.right * 2,
+ ymag: camera.top * 2,
+ zfar: camera.far,
+ znear: camera.near
+
+ };
+
+ } else {
+
+ gltfCamera.perspective = {
+
+ aspectRatio: camera.aspect,
+ yfov: THREE.Math.degToRad( camera.fov ) / camera.aspect,
+ zfar: camera.far,
+ znear: camera.near
+
+ };
+
+ }
+
+ if ( camera.name ) {
+
+ gltfCamera.name = camera.type;
+
+ }
+
+ outputJSON.cameras.push( gltfCamera );
+
+ return outputJSON.cameras.length - 1;
+
+ }
+
+ /**
+ * Process Object3D node
+ * @param {THREE.Object3D} node Object3D to processNode
+ * @return {Integer} Index of the node in the nodes list
+ */
+ function processNode( object ) {
+
+ if ( object instanceof THREE.Light ) {
+
+ console.warn( 'GLTFExporter: Unsupported node type:', object.constructor.name );
+ return null;
+
+ }
+
+ if ( ! outputJSON.nodes ) {
+
+ outputJSON.nodes = [];
+
+ }
+
+ var gltfNode = {};
+
+ if ( options.trs ) {
+
+ var rotation = object.quaternion.toArray();
+ var position = object.position.toArray();
+ var scale = object.scale.toArray();
+
+ if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
+
+ gltfNode.rotation = rotation;
+
+ }
+
+ if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
+
+ gltfNode.position = position;
+
+ }
+
+ if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
+
+ gltfNode.scale = scale;
+
+ }
+
+ } else {
+
+ object.updateMatrix();
+ if ( ! equalArray( object.matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] ) ) {
+
+ gltfNode.matrix = object.matrix.elements;
+
+ }
+
+ }
+
+ if ( object.name ) {
+
+ gltfNode.name = object.name;
+
+ }
+
+ if ( object.userData && Object.keys( object.userData ).length > 0 ) {
+
+ try {
+
+ gltfNode.extras = JSON.parse( JSON.stringify( object.userData ) );
+
+ } catch ( e ) {
+
+ throw new Error( 'THREE.GLTFExporter: userData can\'t be serialized' );
+
+ }
+
+ }
+
+ if ( object instanceof THREE.Mesh ||
+ object instanceof THREE.Line ||
+ object instanceof THREE.Points ) {
+
+ gltfNode.mesh = processMesh( object );
+
+ } else if ( object instanceof THREE.Camera ) {
+
+ gltfNode.camera = processCamera( object );
+
+ }
+
+ if ( object.children.length > 0 ) {
+
+ var children = [];
+
+ for ( var i = 0, l = object.children.length; i < l; i ++ ) {
+
+ var child = object.children[ i ];
+
+ if ( child.visible || options.onlyVisible === false ) {
+
+ var node = processNode( child );
+
+ if ( node !== null ) {
+
+ children.push( node );
+
+ }
+
+ }
+
+ }
+
+ if ( children.length > 0 ) {
+
+ gltfNode.children = children;
+
+ }
+
+
+ }
+
+ outputJSON.nodes.push( gltfNode );
+
+ return outputJSON.nodes.length - 1;
+
+ }
+
+ /**
+ * Process Scene
+ * @param {THREE.Scene} node Scene to process
+ */
+ function processScene( scene ) {
+
+ if ( ! outputJSON.scenes ) {
+
+ outputJSON.scenes = [];
+ outputJSON.scene = 0;
+
+ }
+
+ var gltfScene = {
+
+ nodes: []
+
+ };
+
+ if ( scene.name ) {
+
+ gltfScene.name = scene.name;
+
+ }
+
+ outputJSON.scenes.push( gltfScene );
+
+ var nodes = [];
+
+ for ( var i = 0, l = scene.children.length; i < l; i ++ ) {
+
+ var child = scene.children[ i ];
+
+ if ( child.visible || options.onlyVisible === false ) {
+
+ var node = processNode( child );
+
+ if ( node !== null ) {
+
+ nodes.push( node );
+
+ }
+
+ }
+
+ }
+
+ if ( nodes.length > 0 ) {
+
+ gltfScene.nodes = nodes;
+
+ }
+
+ }
+
+ /**
+ * Creates a THREE.Scene to hold a list of objects and parse it
+ * @param {Array} objects List of objects to process
+ */
+ function processObjects( objects ) {
+
+ var scene = new THREE.Scene();
+ scene.name = 'AuxScene';
+
+ for ( var i = 0; i < objects.length; i ++ ) {
+
+ // We push directly to children instead of calling `add` to prevent
+ // modify the .parent and break its original scene and hierarchy
+ scene.children.push( objects[ i ] );
+
+ }
+
+ processScene( scene );
+
+ }
+
+ function processInput( input ) {
+
+ input = input instanceof Array ? input : [ input ];
+
+ var objectsWithoutScene = [];
+
+ for ( var i = 0; i < input.length; i ++ ) {
+
+ if ( input[ i ] instanceof THREE.Scene ) {
+
+ processScene( input[ i ] );
+
+ } else {
+
+ objectsWithoutScene.push( input[ i ] );
+
+ }
+
+ }
+
+ if ( objectsWithoutScene.length > 0 ) {
+
+ processObjects( objectsWithoutScene );
+
+ }
+
+ }
+
+ processInput( input );
+
+ // Generate buffer
+ // Create a new blob with all the dataviews from the buffers
+ var blob = new Blob( dataViews, { type: 'application/octet-stream' } );
+
+ // Update the bytlength of the only main buffer and update the uri with the base64 representation of it
+ if ( outputJSON.buffers && outputJSON.buffers.length > 0 ) {
+
+ outputJSON.buffers[ 0 ].byteLength = blob.size;
+ var objectURL = URL.createObjectURL( blob );
+
+ var reader = new window.FileReader();
+ reader.readAsDataURL( blob );
+ reader.onloadend = function () {
+
+ var base64data = reader.result;
+ outputJSON.buffers[ 0 ].uri = base64data;
+ onDone( outputJSON );
+
+ };
+
+ } else {
+
+ onDone( outputJSON );
+
+ }
+
+ }
+
+};
diff --git a/app/static/js/exporters/MMDExporter.js b/app/static/js/exporters/MMDExporter.js
new file mode 100644
index 0000000..dfb4bf8
--- /dev/null
+++ b/app/static/js/exporters/MMDExporter.js
@@ -0,0 +1,210 @@
+/**
+ * @author takahiro / http://github.com/takahirox
+ *
+ * Dependencies
+ * - mmd-parser https://github.com/takahirox/mmd-parser
+ */
+
+THREE.MMDExporter = function () {
+
+ // Unicode to Shift_JIS table
+ var u2sTable;
+
+ function unicodeToShiftjis( str ) {
+
+ if ( u2sTable === undefined ) {
+
+ var encoder = new MMDParser.CharsetEncoder();
+ var table = encoder.s2uTable;
+ u2sTable = {};
+
+ var keys = Object.keys( table );
+
+ for ( var i = 0, il = keys.length; i < il; i ++ ) {
+
+ var key = keys[ i ];
+
+ var value = table[ key ];
+ key = parseInt( key );
+
+ u2sTable[ value ] = key;
+
+ }
+
+ }
+
+ var array = [];
+
+ for ( var i = 0, il = str.length; i < il; i ++ ) {
+
+ var code = str.charCodeAt( i );
+
+ var value = u2sTable[ code ];
+
+ if ( value === undefined ) {
+
+ throw 'cannot convert charcode 0x' + code.toString( 16 );
+
+ } else if ( value > 0xff ) {
+
+ array.push( ( value >> 8 ) & 0xff );
+ array.push( value & 0xff );
+
+ } else {
+
+ array.push( value & 0xff );
+
+ }
+
+ }
+
+ return new Uint8Array( array );
+
+ }
+
+ function getBindBones( skin ) {
+
+ // any more efficient ways?
+ var poseSkin = skin.clone();
+ poseSkin.pose();
+ return poseSkin.skeleton.bones;
+
+ }
+
+ /* TODO: implement
+ // mesh -> pmd
+ this.parsePmd = function ( object ) {
+
+ };
+ */
+
+ /* TODO: implement
+ // mesh -> pmx
+ this.parsePmx = function ( object ) {
+
+ };
+ */
+
+ /*
+ * skeleton -> vpd
+ * Returns Shift_JIS encoded Uint8Array. Otherwise return strings.
+ */
+ this.parseVpd = function ( skin, outputShiftJis, useOriginalBones ) {
+
+ if ( skin.isSkinnedMesh !== true ) {
+
+ console.warn( 'THREE.MMDExporter: parseVpd() requires SkinnedMesh instance.' );
+ return null;
+
+ }
+
+ function toStringsFromNumber( num ) {
+
+ if ( Math.abs( num ) < 1e-6 ) num = 0;
+
+ var a = num.toString();
+
+ if ( a.indexOf( '.' ) === - 1 ) {
+
+ a += '.';
+
+ }
+
+ a += '000000';
+
+ var index = a.indexOf( '.' );
+
+ var d = a.slice( 0, index );
+ var p = a.slice( index + 1, index + 7 );
+
+ return d + '.' + p;
+
+ }
+
+ function toStringsFromArray( array ) {
+
+ var a = [];
+
+ for ( var i = 0, il = array.length; i < il; i ++ ) {
+
+ a.push( toStringsFromNumber( array[ i ] ) );
+
+ }
+
+ return a.join( ',' );
+
+ }
+
+ skin.updateMatrixWorld( true );
+
+ var bones = skin.skeleton.bones;
+ var bones2 = getBindBones( skin );
+
+ var position = new THREE.Vector3();
+ var quaternion = new THREE.Quaternion();
+ var quaternion2 = new THREE.Quaternion();
+ var matrix = new THREE.Matrix4();
+
+ var array = [];
+ array.push( 'Vocaloid Pose Data file' );
+ array.push( '' );
+ array.push( ( skin.name !== '' ? skin.name.replace( /\s/g, '_' ) : 'skin' ) + '.osm;' );
+ array.push( bones.length + ';' );
+ array.push( '' );
+
+ for ( var i = 0, il = bones.length; i < il; i ++ ) {
+
+ var bone = bones[ i ];
+ var bone2 = bones2[ i ];
+
+ /*
+ * use the bone matrix saved before solving IK.
+ * see CCDIKSolver for the detail.
+ */
+ if ( useOriginalBones === true &&
+ bone.userData.ik !== undefined &&
+ bone.userData.ik.originalMatrix !== undefined ) {
+
+ matrix.fromArray( bone.userData.ik.originalMatrix );
+
+ } else {
+
+ matrix.copy( bone.matrix );
+
+ }
+
+ position.setFromMatrixPosition( matrix );
+ quaternion.setFromRotationMatrix( matrix );
+
+ var pArray = position.sub( bone2.position ).toArray();
+ var qArray = quaternion2.copy( bone2.quaternion ).conjugate().multiply( quaternion ).toArray();
+
+ // right to left
+ pArray[ 2 ] = - pArray[ 2 ];
+ qArray[ 0 ] = - qArray[ 0 ];
+ qArray[ 1 ] = - qArray[ 1 ];
+
+ array.push( 'Bone' + i + '{' + bone.name );
+ array.push( ' ' + toStringsFromArray( pArray ) + ';' );
+ array.push( ' ' + toStringsFromArray( qArray ) + ';' );
+ array.push( '}' );
+ array.push( '' );
+
+ }
+
+ array.push( '' );
+
+ var lines = array.join( '\n' );
+
+ return ( outputShiftJis === true ) ? unicodeToShiftjis( lines ) : lines;
+
+ };
+
+ /* TODO: implement
+ // animation + skeleton -> vmd
+ this.parseVmd = function ( object ) {
+
+ };
+ */
+
+};
diff --git a/app/static/js/exporters/OBJExporter.js b/app/static/js/exporters/OBJExporter.js
new file mode 100644
index 0000000..720bd11
--- /dev/null
+++ b/app/static/js/exporters/OBJExporter.js
@@ -0,0 +1,262 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.OBJExporter = function () {};
+
+THREE.OBJExporter.prototype = {
+
+ constructor: THREE.OBJExporter,
+
+ parse: function ( object ) {
+
+ var output = '';
+
+ var indexVertex = 0;
+ var indexVertexUvs = 0;
+ var indexNormals = 0;
+
+ var vertex = new THREE.Vector3();
+ var normal = new THREE.Vector3();
+ var uv = new THREE.Vector2();
+
+ var i, j, k, l, m, face = [];
+
+ var parseMesh = function ( mesh ) {
+
+ var nbVertex = 0;
+ var nbNormals = 0;
+ var nbVertexUvs = 0;
+
+ var geometry = mesh.geometry;
+
+ var normalMatrixWorld = new THREE.Matrix3();
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ geometry = new THREE.BufferGeometry().setFromObject( mesh );
+
+ }
+
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ // shortcuts
+ var vertices = geometry.getAttribute( 'position' );
+ var normals = geometry.getAttribute( 'normal' );
+ var uvs = geometry.getAttribute( 'uv' );
+ var indices = geometry.getIndex();
+
+ // name of the mesh object
+ output += 'o ' + mesh.name + '\n';
+
+ // name of the mesh material
+ if ( mesh.material && mesh.material.name ) {
+
+ output += 'usemtl ' + mesh.material.name + '\n';
+
+ }
+
+ // vertices
+
+ if ( vertices !== undefined ) {
+
+ for ( i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {
+
+ vertex.x = vertices.getX( i );
+ vertex.y = vertices.getY( i );
+ vertex.z = vertices.getZ( i );
+
+ // transfrom the vertex to world space
+ vertex.applyMatrix4( mesh.matrixWorld );
+
+ // transform the vertex to export format
+ output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n';
+
+ }
+
+ }
+
+ // uvs
+
+ if ( uvs !== undefined ) {
+
+ for ( i = 0, l = uvs.count; i < l; i ++, nbVertexUvs ++ ) {
+
+ uv.x = uvs.getX( i );
+ uv.y = uvs.getY( i );
+
+ // transform the uv to export format
+ output += 'vt ' + uv.x + ' ' + uv.y + '\n';
+
+ }
+
+ }
+
+ // normals
+
+ if ( normals !== undefined ) {
+
+ normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );
+
+ for ( i = 0, l = normals.count; i < l; i ++, nbNormals ++ ) {
+
+ normal.x = normals.getX( i );
+ normal.y = normals.getY( i );
+ normal.z = normals.getZ( i );
+
+ // transfrom the normal to world space
+ normal.applyMatrix3( normalMatrixWorld );
+
+ // transform the normal to export format
+ output += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';
+
+ }
+
+ }
+
+ // faces
+
+ if ( indices !== null ) {
+
+ for ( i = 0, l = indices.count; i < l; i += 3 ) {
+
+ for ( m = 0; m < 3; m ++ ) {
+
+ j = indices.getX( i + m ) + 1;
+
+ face[ m ] = ( indexVertex + j ) + '/' + ( uvs ? ( indexVertexUvs + j ) : '' ) + '/' + ( indexNormals + j );
+
+ }
+
+ // transform the face to export format
+ output += 'f ' + face.join( ' ' ) + "\n";
+
+ }
+
+ } else {
+
+ for ( i = 0, l = vertices.count; i < l; i += 3 ) {
+
+ for ( m = 0; m < 3; m ++ ) {
+
+ j = i + m + 1;
+
+ face[ m ] = ( indexVertex + j ) + '/' + ( uvs ? ( indexVertexUvs + j ) : '' ) + '/' + ( indexNormals + j );
+
+ }
+
+ // transform the face to export format
+ output += 'f ' + face.join( ' ' ) + "\n";
+
+ }
+
+ }
+
+ } else {
+
+ console.warn( 'THREE.OBJExporter.parseMesh(): geometry type unsupported', geometry );
+
+ }
+
+ // update index
+ indexVertex += nbVertex;
+ indexVertexUvs += nbVertexUvs;
+ indexNormals += nbNormals;
+
+ };
+
+ var parseLine = function ( line ) {
+
+ var nbVertex = 0;
+
+ var geometry = line.geometry;
+ var type = line.type;
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ geometry = new THREE.BufferGeometry().setFromObject( line );
+
+ }
+
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ // shortcuts
+ var vertices = geometry.getAttribute( 'position' );
+
+ // name of the line object
+ output += 'o ' + line.name + '\n';
+
+ if ( vertices !== undefined ) {
+
+ for ( i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {
+
+ vertex.x = vertices.getX( i );
+ vertex.y = vertices.getY( i );
+ vertex.z = vertices.getZ( i );
+
+ // transfrom the vertex to world space
+ vertex.applyMatrix4( line.matrixWorld );
+
+ // transform the vertex to export format
+ output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n';
+
+ }
+
+ }
+
+ if ( type === 'Line' ) {
+
+ output += 'l ';
+
+ for ( j = 1, l = vertices.count; j <= l; j ++ ) {
+
+ output += ( indexVertex + j ) + ' ';
+
+ }
+
+ output += '\n';
+
+ }
+
+ if ( type === 'LineSegments' ) {
+
+ for ( j = 1, k = j + 1, l = vertices.count; j < l; j += 2, k = j + 1 ) {
+
+ output += 'l ' + ( indexVertex + j ) + ' ' + ( indexVertex + k ) + '\n';
+
+ }
+
+ }
+
+ } else {
+
+ console.warn( 'THREE.OBJExporter.parseLine(): geometry type unsupported', geometry );
+
+ }
+
+ // update index
+ indexVertex += nbVertex;
+
+ };
+
+ object.traverse( function ( child ) {
+
+ if ( child instanceof THREE.Mesh ) {
+
+ parseMesh( child );
+
+ }
+
+ if ( child instanceof THREE.Line ) {
+
+ parseLine( child );
+
+ }
+
+ } );
+
+ return output;
+
+ }
+
+};
diff --git a/app/static/js/exporters/STLBinaryExporter.js b/app/static/js/exporters/STLBinaryExporter.js
new file mode 100644
index 0000000..f92f67c
--- /dev/null
+++ b/app/static/js/exporters/STLBinaryExporter.js
@@ -0,0 +1,94 @@
+/**
+ * @author kovacsv / http://kovacsv.hu/
+ * @author mrdoob / http://mrdoob.com/
+ * @author mudcube / http://mudcu.be/
+ */
+
+THREE.STLBinaryExporter = function () {};
+
+THREE.STLBinaryExporter.prototype = {
+
+ constructor: THREE.STLBinaryExporter,
+
+ parse: ( function () {
+
+ var vector = new THREE.Vector3();
+ var normalMatrixWorld = new THREE.Matrix3();
+
+ return function parse( scene ) {
+
+ // We collect objects first, as we may need to convert from BufferGeometry to Geometry
+ var objects = [];
+ var triangles = 0;
+ scene.traverse( function ( object ) {
+
+ if ( ! ( object instanceof THREE.Mesh ) ) return;
+
+ var geometry = object.geometry;
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ geometry = new THREE.Geometry().fromBufferGeometry( geometry );
+
+ }
+
+ if ( ! ( geometry instanceof THREE.Geometry ) ) return;
+ triangles += geometry.faces.length;
+
+ objects.push( {
+
+ geometry: geometry,
+ matrix: object.matrixWorld
+
+ } );
+
+ } );
+
+ var offset = 80; // skip header
+ var bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4;
+ var arrayBuffer = new ArrayBuffer( bufferLength );
+ var output = new DataView( arrayBuffer );
+ output.setUint32( offset, triangles, true ); offset += 4;
+
+ // Traversing our collected objects
+ objects.forEach( function ( object ) {
+
+ var vertices = object.geometry.vertices;
+ var faces = object.geometry.faces;
+
+ normalMatrixWorld.getNormalMatrix( object.matrix );
+
+ for ( var i = 0, l = faces.length; i < l; i ++ ) {
+
+ var face = faces[ i ];
+
+ vector.copy( face.normal ).applyMatrix3( normalMatrixWorld ).normalize();
+
+ output.setFloat32( offset, vector.x, true ); offset += 4; // normal
+ output.setFloat32( offset, vector.y, true ); offset += 4;
+ output.setFloat32( offset, vector.z, true ); offset += 4;
+
+ var indices = [ face.a, face.b, face.c ];
+
+ for ( var j = 0; j < 3; j ++ ) {
+
+ vector.copy( vertices[ indices[ j ] ] ).applyMatrix4( object.matrix );
+
+ output.setFloat32( offset, vector.x, true ); offset += 4; // vertices
+ output.setFloat32( offset, vector.y, true ); offset += 4;
+ output.setFloat32( offset, vector.z, true ); offset += 4;
+
+ }
+
+ output.setUint16( offset, 0, true ); offset += 2; // attribute byte count
+
+ }
+
+ } );
+
+ return output;
+
+ };
+
+ }() )
+
+};
diff --git a/app/static/js/exporters/STLExporter.js b/app/static/js/exporters/STLExporter.js
new file mode 100644
index 0000000..f794a5f
--- /dev/null
+++ b/app/static/js/exporters/STLExporter.js
@@ -0,0 +1,81 @@
+/**
+ * @author kovacsv / http://kovacsv.hu/
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.STLExporter = function () {};
+
+THREE.STLExporter.prototype = {
+
+ constructor: THREE.STLExporter,
+
+ parse: ( function () {
+
+ var vector = new THREE.Vector3();
+ var normalMatrixWorld = new THREE.Matrix3();
+
+ return function parse( scene ) {
+
+ var output = '';
+
+ output += 'solid exported\n';
+
+ scene.traverse( function ( object ) {
+
+ if ( object instanceof THREE.Mesh ) {
+
+ var geometry = object.geometry;
+ var matrixWorld = object.matrixWorld;
+
+ if ( geometry instanceof THREE.BufferGeometry ) {
+
+ geometry = new THREE.Geometry().fromBufferGeometry( geometry );
+
+ }
+
+ if ( geometry instanceof THREE.Geometry ) {
+
+ var vertices = geometry.vertices;
+ var faces = geometry.faces;
+
+ normalMatrixWorld.getNormalMatrix( matrixWorld );
+
+ for ( var i = 0, l = faces.length; i < l; i ++ ) {
+
+ var face = faces[ i ];
+
+ vector.copy( face.normal ).applyMatrix3( normalMatrixWorld ).normalize();
+
+ output += '\tfacet normal ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';
+ output += '\t\touter loop\n';
+
+ var indices = [ face.a, face.b, face.c ];
+
+ for ( var j = 0; j < 3; j ++ ) {
+
+ vector.copy( vertices[ indices[ j ] ] ).applyMatrix4( matrixWorld );
+
+ output += '\t\t\tvertex ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';
+
+ }
+
+ output += '\t\tendloop\n';
+ output += '\tendfacet\n';
+
+ }
+
+ }
+
+ }
+
+ } );
+
+ output += 'endsolid exported\n';
+
+ return output;
+
+ };
+
+ }() )
+
+};
diff --git a/app/static/js/exporters/TypedGeometryExporter.js b/app/static/js/exporters/TypedGeometryExporter.js
new file mode 100644
index 0000000..dfaf587
--- /dev/null
+++ b/app/static/js/exporters/TypedGeometryExporter.js
@@ -0,0 +1,55 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.TypedGeometryExporter = function () {};
+
+THREE.TypedGeometryExporter.prototype = {
+
+ constructor: THREE.TypedGeometryExporter,
+
+ parse: function ( geometry ) {
+
+ var output = {
+ metadata: {
+ version: 4.0,
+ type: 'TypedGeometry',
+ generator: 'TypedGeometryExporter'
+ }
+ };
+
+ var attributes = [ 'vertices', 'normals', 'uvs' ];
+
+ for ( var key in attributes ) {
+
+ var attribute = attributes[ key ];
+
+ var typedArray = geometry[ attribute ];
+ var array = [];
+
+ for ( var i = 0, l = typedArray.length; i < l; i ++ ) {
+
+ array[ i ] = typedArray[ i ];
+
+ }
+
+ output[ attribute ] = array;
+
+ }
+
+ var boundingSphere = geometry.boundingSphere;
+
+ if ( boundingSphere !== null ) {
+
+ output.boundingSphere = {
+ center: boundingSphere.center.toArray(),
+ radius: boundingSphere.radius
+ };
+
+ }
+
+ return output;
+
+ }
+
+};
diff --git a/app/static/js/geometries/ConvexGeometry.js b/app/static/js/geometries/ConvexGeometry.js
new file mode 100644
index 0000000..439118a
--- /dev/null
+++ b/app/static/js/geometries/ConvexGeometry.js
@@ -0,0 +1,85 @@
+/**
+ * @author Mugen87 / https://github.com/Mugen87
+ */
+
+( function () {
+
+ // ConvexGeometry
+
+ function ConvexGeometry( points ) {
+
+ THREE.Geometry.call( this );
+
+ this.type = 'ConvexGeometry';
+
+ this.fromBufferGeometry( new ConvexBufferGeometry( points ) );
+ this.mergeVertices();
+
+ }
+
+ ConvexGeometry.prototype = Object.create( THREE.Geometry.prototype );
+ ConvexGeometry.prototype.constructor = ConvexGeometry;
+
+ // ConvexBufferGeometry
+
+ function ConvexBufferGeometry( points ) {
+
+ THREE.BufferGeometry.call( this );
+
+ this.type = 'ConvexBufferGeometry';
+
+ // buffers
+
+ var vertices = [];
+ var normals = [];
+
+ // execute QuickHull
+
+ if ( THREE.QuickHull === undefined ) {
+
+ console.error( 'THREE.ConvexBufferGeometry: ConvexBufferGeometry relies on THREE.QuickHull' );
+
+ }
+
+ var quickHull = new THREE.QuickHull().setFromPoints( points );
+
+ // generate vertices and normals
+
+ var faces = quickHull.faces;
+
+ for ( var i = 0; i < faces.length; i ++ ) {
+
+ var face = faces[ i ];
+ var edge = face.edge;
+
+ // we move along a doubly-connected edge list to access all face points (see HalfEdge docs)
+
+ do {
+
+ var point = edge.head().point;
+
+ vertices.push( point.x, point.y, point.z );
+ normals.push( face.normal.x, face.normal.y, face.normal.z );
+
+ edge = edge.next;
+
+ } while ( edge !== face.edge );
+
+ }
+
+ // build geometry
+
+ this.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
+ this.addAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );
+
+ }
+
+ ConvexBufferGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+ ConvexBufferGeometry.prototype.constructor = ConvexBufferGeometry;
+
+ // export
+
+ THREE.ConvexGeometry = ConvexGeometry;
+ THREE.ConvexBufferGeometry = ConvexBufferGeometry;
+
+} )();
diff --git a/app/static/js/geometries/DecalGeometry.js b/app/static/js/geometries/DecalGeometry.js
new file mode 100644
index 0000000..ce9a2d2
--- /dev/null
+++ b/app/static/js/geometries/DecalGeometry.js
@@ -0,0 +1,359 @@
+/**
+ * @author Mugen87 / https://github.com/Mugen87
+ * @author spite / https://github.com/spite
+ *
+ * You can use this geometry to create a decal mesh, that serves different kinds of purposes.
+ * e.g. adding unique details to models, performing dynamic visual environmental changes or covering seams.
+ *
+ * Constructor parameter:
+ *
+ * mesh — Any mesh object
+ * position — Position of the decal projector
+ * orientation — Orientation of the decal projector
+ * size — Size of the decal projector
+ *
+ * reference: http://blog.wolfire.com/2009/06/how-to-project-decals/
+ *
+ */
+
+( function () {
+
+ function DecalGeometry( mesh, position, orientation, size ) {
+
+ THREE.BufferGeometry.call( this );
+
+ this.type = 'DecalGeometry';
+
+ // buffers
+
+ var vertices = [];
+ var normals = [];
+ var uvs = [];
+
+ // helpers
+
+ var plane = new THREE.Vector3();
+
+ // this matrix represents the transformation of the decal projector
+
+ var projectorMatrix = new THREE.Matrix4();
+ projectorMatrix.makeRotationFromEuler( orientation );
+ projectorMatrix.setPosition( position );
+
+ var projectorMatrixInverse = new THREE.Matrix4().getInverse( projectorMatrix );
+
+ // generate buffers
+
+ generate();
+
+ // build geometry
+
+ this.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
+ this.addAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );
+ this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) );
+
+ function generate() {
+
+ var i;
+ var geometry = new THREE.BufferGeometry();
+ var decalVertices = [];
+
+ var vertex = new THREE.Vector3();
+ var normal = new THREE.Vector3();
+
+ // handle different geometry types
+
+ if ( mesh.geometry.isGeometry ) {
+
+ geometry.fromGeometry( mesh.geometry );
+
+ } else {
+
+ geometry.copy( mesh.geometry );
+
+ }
+
+ var positionAttribute = geometry.attributes.position;
+ var normalAttribute = geometry.attributes.normal;
+
+ // first, create an array of 'DecalVertex' objects
+ // three consecutive 'DecalVertex' objects represent a single face
+ //
+ // this data structure will be later used to perform the clipping
+
+ if ( geometry.index !== null ) {
+
+ // indexed BufferGeometry
+
+ var index = geometry.index;
+
+ for ( i = 0; i < index.count; i ++ ) {
+
+ vertex.fromBufferAttribute( positionAttribute, index.getX( i ) );
+ normal.fromBufferAttribute( normalAttribute, index.getX( i ) );
+
+ pushDecalVertex( decalVertices, vertex, normal );
+
+ }
+
+ } else {
+
+ // non-indexed BufferGeometry
+
+ for ( i = 0; i < positionAttribute.count; i ++ ) {
+
+ vertex.fromBufferAttribute( positionAttribute, i );
+ normal.fromBufferAttribute( normalAttribute, i );
+
+ pushDecalVertex( decalVertices, vertex, normal );
+
+ }
+
+ }
+
+ // second, clip the geometry so that it doesn't extend out from the projector
+
+ decalVertices = clipGeometry( decalVertices, plane.set( 1, 0, 0 ) );
+ decalVertices = clipGeometry( decalVertices, plane.set( - 1, 0, 0 ) );
+ decalVertices = clipGeometry( decalVertices, plane.set( 0, 1, 0 ) );
+ decalVertices = clipGeometry( decalVertices, plane.set( 0, - 1, 0 ) );
+ decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, 1 ) );
+ decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, - 1 ) );
+
+ // third, generate final vertices, normals and uvs
+
+ for ( i = 0; i < decalVertices.length; i ++ ) {
+
+ var decalVertex = decalVertices[ i ];
+
+ // create texture coordinates (we are still in projector space)
+
+ uvs.push(
+ 0.5 + ( decalVertex.position.x / size.x ),
+ 0.5 + ( decalVertex.position.y / size.y )
+ );
+
+ // transform the vertex back to world space
+
+ decalVertex.position.applyMatrix4( projectorMatrix );
+
+ // now create vertex and normal buffer data
+
+ vertices.push( decalVertex.position.x, decalVertex.position.y, decalVertex.position.z );
+ normals.push( decalVertex.normal.x, decalVertex.normal.y, decalVertex.normal.z );
+
+ }
+
+ }
+
+ function pushDecalVertex( decalVertices, vertex, normal ) {
+
+ // transform the vertex to world space, then to projector space
+
+ vertex.applyMatrix4( mesh.matrix );
+ vertex.applyMatrix4( projectorMatrixInverse );
+
+ decalVertices.push( new DecalVertex( vertex.clone(), normal.clone() ) );
+
+ }
+
+ function clipGeometry( inVertices, plane ) {
+
+ var outVertices = [];
+
+ var s = 0.5 * Math.abs( size.dot( plane ) );
+
+ // a single iteration clips one face,
+ // which consists of three consecutive 'DecalVertex' objects
+
+ for ( var i = 0; i < inVertices.length; i += 3 ) {
+
+ var v1Out, v2Out, v3Out, total = 0;
+ var nV1, nV2, nV3, nV4;
+
+ var d1 = inVertices[ i + 0 ].position.dot( plane ) - s;
+ var d2 = inVertices[ i + 1 ].position.dot( plane ) - s;
+ var d3 = inVertices[ i + 2 ].position.dot( plane ) - s;
+
+ v1Out = d1 > 0;
+ v2Out = d2 > 0;
+ v3Out = d3 > 0;
+
+ // calculate, how many vertices of the face lie outside of the clipping plane
+
+ total = ( v1Out ? 1 : 0 ) + ( v2Out ? 1 : 0 ) + ( v3Out ? 1 : 0 );
+
+ switch ( total ) {
+
+ case 0: {
+
+ // the entire face lies inside of the plane, no clipping needed
+
+ outVertices.push( inVertices[ i ] );
+ outVertices.push( inVertices[ i + 1 ] );
+ outVertices.push( inVertices[ i + 2 ] );
+ break;
+
+ }
+
+ case 1: {
+
+ // one vertex lies outside of the plane, perform clipping
+
+ if ( v1Out ) {
+
+ nV1 = inVertices[ i + 1 ];
+ nV2 = inVertices[ i + 2 ];
+ nV3 = clip( inVertices[ i ], nV1, plane, s );
+ nV4 = clip( inVertices[ i ], nV2, plane, s );
+
+ }
+
+ if ( v2Out ) {
+
+ nV1 = inVertices[ i ];
+ nV2 = inVertices[ i + 2 ];
+ nV3 = clip( inVertices[ i + 1 ], nV1, plane, s );
+ nV4 = clip( inVertices[ i + 1 ], nV2, plane, s );
+
+ outVertices.push( nV3 );
+ outVertices.push( nV2.clone() );
+ outVertices.push( nV1.clone() );
+
+ outVertices.push( nV2.clone() );
+ outVertices.push( nV3.clone() );
+ outVertices.push( nV4 );
+ break;
+
+ }
+
+ if ( v3Out ) {
+
+ nV1 = inVertices[ i ];
+ nV2 = inVertices[ i + 1 ];
+ nV3 = clip( inVertices[ i + 2 ], nV1, plane, s );
+ nV4 = clip( inVertices[ i + 2 ], nV2, plane, s );
+
+ }
+
+ outVertices.push( nV1.clone() );
+ outVertices.push( nV2.clone() );
+ outVertices.push( nV3 );
+
+ outVertices.push( nV4 );
+ outVertices.push( nV3.clone() );
+ outVertices.push( nV2.clone() );
+
+ break;
+
+ }
+
+ case 2: {
+
+ // two vertices lies outside of the plane, perform clipping
+
+ if ( ! v1Out ) {
+
+ nV1 = inVertices[ i ].clone();
+ nV2 = clip( nV1, inVertices[ i + 1 ], plane, s );
+ nV3 = clip( nV1, inVertices[ i + 2 ], plane, s );
+ outVertices.push( nV1 );
+ outVertices.push( nV2 );
+ outVertices.push( nV3 );
+
+ }
+
+ if ( ! v2Out ) {
+
+ nV1 = inVertices[ i + 1 ].clone();
+ nV2 = clip( nV1, inVertices[ i + 2 ], plane, s );
+ nV3 = clip( nV1, inVertices[ i ], plane, s );
+ outVertices.push( nV1 );
+ outVertices.push( nV2 );
+ outVertices.push( nV3 );
+
+ }
+
+ if ( ! v3Out ) {
+
+ nV1 = inVertices[ i + 2 ].clone();
+ nV2 = clip( nV1, inVertices[ i ], plane, s );
+ nV3 = clip( nV1, inVertices[ i + 1 ], plane, s );
+ outVertices.push( nV1 );
+ outVertices.push( nV2 );
+ outVertices.push( nV3 );
+
+ }
+
+ break;
+
+ }
+
+ case 3: {
+
+ // the entire face lies outside of the plane, so let's discard the corresponding vertices
+
+ break;
+
+ }
+
+ }
+
+ }
+
+ return outVertices;
+
+ }
+
+ function clip( v0, v1, p, s ) {
+
+ var d0 = v0.position.dot( p ) - s;
+ var d1 = v1.position.dot( p ) - s;
+
+ var s0 = d0 / ( d0 - d1 );
+
+ var v = new DecalVertex(
+ new THREE.Vector3(
+ v0.position.x + s0 * ( v1.position.x - v0.position.x ),
+ v0.position.y + s0 * ( v1.position.y - v0.position.y ),
+ v0.position.z + s0 * ( v1.position.z - v0.position.z )
+ ),
+ new THREE.Vector3(
+ v0.normal.x + s0 * ( v1.normal.x - v0.normal.x ),
+ v0.normal.y + s0 * ( v1.normal.y - v0.normal.y ),
+ v0.normal.z + s0 * ( v1.normal.z - v0.normal.z )
+ )
+ );
+
+ // need to clip more values (texture coordinates)? do it this way:
+ // intersectpoint.value = a.value + s * ( b.value - a.value );
+
+ return v;
+
+ }
+
+ }
+
+ DecalGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+ DecalGeometry.prototype.constructor = DecalGeometry;
+
+ // helper
+
+ function DecalVertex( position, normal ) {
+
+ this.position = position;
+ this.normal = normal;
+
+ }
+
+ DecalVertex.prototype.clone = function () {
+
+ return new DecalVertex( this.position.clone(), this.normal.clone() );
+
+ };
+
+ // export
+
+ THREE.DecalGeometry = DecalGeometry;
+
+} )();
diff --git a/app/static/js/geometries/TeapotBufferGeometry.js b/app/static/js/geometries/TeapotBufferGeometry.js
new file mode 100644
index 0000000..7d6c6ff
--- /dev/null
+++ b/app/static/js/geometries/TeapotBufferGeometry.js
@@ -0,0 +1,751 @@
+/**
+ * @author Eric Haines / http://erichaines.com/
+ *
+ * Tessellates the famous Utah teapot database by Martin Newell into triangles.
+ *
+ * THREE.TeapotBufferGeometry = function ( size, segments, bottom, lid, body, fitLid, blinn )
+ *
+ * defaults: size = 50, segments = 10, bottom = true, lid = true, body = true,
+ * fitLid = false, blinn = true
+ *
+ * size is a relative scale: I've scaled the teapot to fit vertically between -1 and 1.
+ * Think of it as a "radius".
+ * segments - number of line segments to subdivide each patch edge;
+ * 1 is possible but gives degenerates, so two is the real minimum.
+ * bottom - boolean, if true (default) then the bottom patches are added. Some consider
+ * adding the bottom heresy, so set this to "false" to adhere to the One True Way.
+ * lid - to remove the lid and look inside, set to true.
+ * body - to remove the body and leave the lid, set this and "bottom" to false.
+ * fitLid - the lid is a tad small in the original. This stretches it a bit so you can't
+ * see the teapot's insides through the gap.
+ * blinn - Jim Blinn scaled the original data vertically by dividing by about 1.3 to look
+ * nicer. If you want to see the original teapot, similar to the real-world model, set
+ * this to false. True by default.
+ * See http://en.wikipedia.org/wiki/File:Original_Utah_Teapot.jpg for the original
+ * real-world teapot (from http://en.wikipedia.org/wiki/Utah_teapot).
+ *
+ * Note that the bottom (the last four patches) is not flat - blame Frank Crow, not me.
+ *
+ * The teapot should normally be rendered as a double sided object, since for some
+ * patches both sides can be seen, e.g., the gap around the lid and inside the spout.
+ *
+ * Segments 'n' determines the number of triangles output.
+ * Total triangles = 32*2*n*n - 8*n [degenerates at the top and bottom cusps are deleted]
+ *
+ * size_factor # triangles
+ * 1 56
+ * 2 240
+ * 3 552
+ * 4 992
+ *
+ * 10 6320
+ * 20 25440
+ * 30 57360
+ *
+ * Code converted from my ancient SPD software, http://tog.acm.org/resources/SPD/
+ * Created for the Udacity course "Interactive Rendering", http://bit.ly/ericity
+ * Lesson: https://www.udacity.com/course/viewer#!/c-cs291/l-68866048/m-106482448
+ * YouTube video on teapot history: https://www.youtube.com/watch?v=DxMfblPzFNc
+ *
+ * See https://en.wikipedia.org/wiki/Utah_teapot for the history of the teapot
+ *
+ */
+/*global THREE */
+
+THREE.TeapotBufferGeometry = function ( size, segments, bottom, lid, body, fitLid, blinn ) {
+
+ "use strict";
+
+ // 32 * 4 * 4 Bezier spline patches
+ var teapotPatches = [
+/*rim*/
+0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
+3,16,17,18,7,19,20,21,11,22,23,24,15,25,26,27,
+18,28,29,30,21,31,32,33,24,34,35,36,27,37,38,39,
+30,40,41,0,33,42,43,4,36,44,45,8,39,46,47,12,
+/*body*/
+12,13,14,15,48,49,50,51,52,53,54,55,56,57,58,59,
+15,25,26,27,51,60,61,62,55,63,64,65,59,66,67,68,
+27,37,38,39,62,69,70,71,65,72,73,74,68,75,76,77,
+39,46,47,12,71,78,79,48,74,80,81,52,77,82,83,56,
+56,57,58,59,84,85,86,87,88,89,90,91,92,93,94,95,
+59,66,67,68,87,96,97,98,91,99,100,101,95,102,103,104,
+68,75,76,77,98,105,106,107,101,108,109,110,104,111,112,113,
+77,82,83,56,107,114,115,84,110,116,117,88,113,118,119,92,
+/*handle*/
+120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,
+123,136,137,120,127,138,139,124,131,140,141,128,135,142,143,132,
+132,133,134,135,144,145,146,147,148,149,150,151,68,152,153,154,
+135,142,143,132,147,155,156,144,151,157,158,148,154,159,160,68,
+/*spout*/
+161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,
+164,177,178,161,168,179,180,165,172,181,182,169,176,183,184,173,
+173,174,175,176,185,186,187,188,189,190,191,192,193,194,195,196,
+176,183,184,173,188,197,198,185,192,199,200,189,196,201,202,193,
+/*lid*/
+203,203,203,203,204,205,206,207,208,208,208,208,209,210,211,212,
+203,203,203,203,207,213,214,215,208,208,208,208,212,216,217,218,
+203,203,203,203,215,219,220,221,208,208,208,208,218,222,223,224,
+203,203,203,203,221,225,226,204,208,208,208,208,224,227,228,209,
+209,210,211,212,229,230,231,232,233,234,235,236,237,238,239,240,
+212,216,217,218,232,241,242,243,236,244,245,246,240,247,248,249,
+218,222,223,224,243,250,251,252,246,253,254,255,249,256,257,258,
+224,227,228,209,252,259,260,229,255,261,262,233,258,263,264,237,
+/*bottom*/
+265,265,265,265,266,267,268,269,270,271,272,273,92,119,118,113,
+265,265,265,265,269,274,275,276,273,277,278,279,113,112,111,104,
+265,265,265,265,276,280,281,282,279,283,284,285,104,103,102,95,
+265,265,265,265,282,286,287,266,285,288,289,270,95,94,93,92
+ ] ;
+
+ var teapotVertices = [
+1.4,0,2.4,
+1.4,-0.784,2.4,
+0.784,-1.4,2.4,
+0,-1.4,2.4,
+1.3375,0,2.53125,
+1.3375,-0.749,2.53125,
+0.749,-1.3375,2.53125,
+0,-1.3375,2.53125,
+1.4375,0,2.53125,
+1.4375,-0.805,2.53125,
+0.805,-1.4375,2.53125,
+0,-1.4375,2.53125,
+1.5,0,2.4,
+1.5,-0.84,2.4,
+0.84,-1.5,2.4,
+0,-1.5,2.4,
+-0.784,-1.4,2.4,
+-1.4,-0.784,2.4,
+-1.4,0,2.4,
+-0.749,-1.3375,2.53125,
+-1.3375,-0.749,2.53125,
+-1.3375,0,2.53125,
+-0.805,-1.4375,2.53125,
+-1.4375,-0.805,2.53125,
+-1.4375,0,2.53125,
+-0.84,-1.5,2.4,
+-1.5,-0.84,2.4,
+-1.5,0,2.4,
+-1.4,0.784,2.4,
+-0.784,1.4,2.4,
+0,1.4,2.4,
+-1.3375,0.749,2.53125,
+-0.749,1.3375,2.53125,
+0,1.3375,2.53125,
+-1.4375,0.805,2.53125,
+-0.805,1.4375,2.53125,
+0,1.4375,2.53125,
+-1.5,0.84,2.4,
+-0.84,1.5,2.4,
+0,1.5,2.4,
+0.784,1.4,2.4,
+1.4,0.784,2.4,
+0.749,1.3375,2.53125,
+1.3375,0.749,2.53125,
+0.805,1.4375,2.53125,
+1.4375,0.805,2.53125,
+0.84,1.5,2.4,
+1.5,0.84,2.4,
+1.75,0,1.875,
+1.75,-0.98,1.875,
+0.98,-1.75,1.875,
+0,-1.75,1.875,
+2,0,1.35,
+2,-1.12,1.35,
+1.12,-2,1.35,
+0,-2,1.35,
+2,0,0.9,
+2,-1.12,0.9,
+1.12,-2,0.9,
+0,-2,0.9,
+-0.98,-1.75,1.875,
+-1.75,-0.98,1.875,
+-1.75,0,1.875,
+-1.12,-2,1.35,
+-2,-1.12,1.35,
+-2,0,1.35,
+-1.12,-2,0.9,
+-2,-1.12,0.9,
+-2,0,0.9,
+-1.75,0.98,1.875,
+-0.98,1.75,1.875,
+0,1.75,1.875,
+-2,1.12,1.35,
+-1.12,2,1.35,
+0,2,1.35,
+-2,1.12,0.9,
+-1.12,2,0.9,
+0,2,0.9,
+0.98,1.75,1.875,
+1.75,0.98,1.875,
+1.12,2,1.35,
+2,1.12,1.35,
+1.12,2,0.9,
+2,1.12,0.9,
+2,0,0.45,
+2,-1.12,0.45,
+1.12,-2,0.45,
+0,-2,0.45,
+1.5,0,0.225,
+1.5,-0.84,0.225,
+0.84,-1.5,0.225,
+0,-1.5,0.225,
+1.5,0,0.15,
+1.5,-0.84,0.15,
+0.84,-1.5,0.15,
+0,-1.5,0.15,
+-1.12,-2,0.45,
+-2,-1.12,0.45,
+-2,0,0.45,
+-0.84,-1.5,0.225,
+-1.5,-0.84,0.225,
+-1.5,0,0.225,
+-0.84,-1.5,0.15,
+-1.5,-0.84,0.15,
+-1.5,0,0.15,
+-2,1.12,0.45,
+-1.12,2,0.45,
+0,2,0.45,
+-1.5,0.84,0.225,
+-0.84,1.5,0.225,
+0,1.5,0.225,
+-1.5,0.84,0.15,
+-0.84,1.5,0.15,
+0,1.5,0.15,
+1.12,2,0.45,
+2,1.12,0.45,
+0.84,1.5,0.225,
+1.5,0.84,0.225,
+0.84,1.5,0.15,
+1.5,0.84,0.15,
+-1.6,0,2.025,
+-1.6,-0.3,2.025,
+-1.5,-0.3,2.25,
+-1.5,0,2.25,
+-2.3,0,2.025,
+-2.3,-0.3,2.025,
+-2.5,-0.3,2.25,
+-2.5,0,2.25,
+-2.7,0,2.025,
+-2.7,-0.3,2.025,
+-3,-0.3,2.25,
+-3,0,2.25,
+-2.7,0,1.8,
+-2.7,-0.3,1.8,
+-3,-0.3,1.8,
+-3,0,1.8,
+-1.5,0.3,2.25,
+-1.6,0.3,2.025,
+-2.5,0.3,2.25,
+-2.3,0.3,2.025,
+-3,0.3,2.25,
+-2.7,0.3,2.025,
+-3,0.3,1.8,
+-2.7,0.3,1.8,
+-2.7,0,1.575,
+-2.7,-0.3,1.575,
+-3,-0.3,1.35,
+-3,0,1.35,
+-2.5,0,1.125,
+-2.5,-0.3,1.125,
+-2.65,-0.3,0.9375,
+-2.65,0,0.9375,
+-2,-0.3,0.9,
+-1.9,-0.3,0.6,
+-1.9,0,0.6,
+-3,0.3,1.35,
+-2.7,0.3,1.575,
+-2.65,0.3,0.9375,
+-2.5,0.3,1.125,
+-1.9,0.3,0.6,
+-2,0.3,0.9,
+1.7,0,1.425,
+1.7,-0.66,1.425,
+1.7,-0.66,0.6,
+1.7,0,0.6,
+2.6,0,1.425,
+2.6,-0.66,1.425,
+3.1,-0.66,0.825,
+3.1,0,0.825,
+2.3,0,2.1,
+2.3,-0.25,2.1,
+2.4,-0.25,2.025,
+2.4,0,2.025,
+2.7,0,2.4,
+2.7,-0.25,2.4,
+3.3,-0.25,2.4,
+3.3,0,2.4,
+1.7,0.66,0.6,
+1.7,0.66,1.425,
+3.1,0.66,0.825,
+2.6,0.66,1.425,
+2.4,0.25,2.025,
+2.3,0.25,2.1,
+3.3,0.25,2.4,
+2.7,0.25,2.4,
+2.8,0,2.475,
+2.8,-0.25,2.475,
+3.525,-0.25,2.49375,
+3.525,0,2.49375,
+2.9,0,2.475,
+2.9,-0.15,2.475,
+3.45,-0.15,2.5125,
+3.45,0,2.5125,
+2.8,0,2.4,
+2.8,-0.15,2.4,
+3.2,-0.15,2.4,
+3.2,0,2.4,
+3.525,0.25,2.49375,
+2.8,0.25,2.475,
+3.45,0.15,2.5125,
+2.9,0.15,2.475,
+3.2,0.15,2.4,
+2.8,0.15,2.4,
+0,0,3.15,
+0.8,0,3.15,
+0.8,-0.45,3.15,
+0.45,-0.8,3.15,
+0,-0.8,3.15,
+0,0,2.85,
+0.2,0,2.7,
+0.2,-0.112,2.7,
+0.112,-0.2,2.7,
+0,-0.2,2.7,
+-0.45,-0.8,3.15,
+-0.8,-0.45,3.15,
+-0.8,0,3.15,
+-0.112,-0.2,2.7,
+-0.2,-0.112,2.7,
+-0.2,0,2.7,
+-0.8,0.45,3.15,
+-0.45,0.8,3.15,
+0,0.8,3.15,
+-0.2,0.112,2.7,
+-0.112,0.2,2.7,
+0,0.2,2.7,
+0.45,0.8,3.15,
+0.8,0.45,3.15,
+0.112,0.2,2.7,
+0.2,0.112,2.7,
+0.4,0,2.55,
+0.4,-0.224,2.55,
+0.224,-0.4,2.55,
+0,-0.4,2.55,
+1.3,0,2.55,
+1.3,-0.728,2.55,
+0.728,-1.3,2.55,
+0,-1.3,2.55,
+1.3,0,2.4,
+1.3,-0.728,2.4,
+0.728,-1.3,2.4,
+0,-1.3,2.4,
+-0.224,-0.4,2.55,
+-0.4,-0.224,2.55,
+-0.4,0,2.55,
+-0.728,-1.3,2.55,
+-1.3,-0.728,2.55,
+-1.3,0,2.55,
+-0.728,-1.3,2.4,
+-1.3,-0.728,2.4,
+-1.3,0,2.4,
+-0.4,0.224,2.55,
+-0.224,0.4,2.55,
+0,0.4,2.55,
+-1.3,0.728,2.55,
+-0.728,1.3,2.55,
+0,1.3,2.55,
+-1.3,0.728,2.4,
+-0.728,1.3,2.4,
+0,1.3,2.4,
+0.224,0.4,2.55,
+0.4,0.224,2.55,
+0.728,1.3,2.55,
+1.3,0.728,2.55,
+0.728,1.3,2.4,
+1.3,0.728,2.4,
+0,0,0,
+1.425,0,0,
+1.425,0.798,0,
+0.798,1.425,0,
+0,1.425,0,
+1.5,0,0.075,
+1.5,0.84,0.075,
+0.84,1.5,0.075,
+0,1.5,0.075,
+-0.798,1.425,0,
+-1.425,0.798,0,
+-1.425,0,0,
+-0.84,1.5,0.075,
+-1.5,0.84,0.075,
+-1.5,0,0.075,
+-1.425,-0.798,0,
+-0.798,-1.425,0,
+0,-1.425,0,
+-1.5,-0.84,0.075,
+-0.84,-1.5,0.075,
+0,-1.5,0.075,
+0.798,-1.425,0,
+1.425,-0.798,0,
+0.84,-1.5,0.075,
+1.5,-0.84,0.075
+ ] ;
+
+ THREE.BufferGeometry.call( this );
+
+ this.type = 'TeapotBufferGeometry';
+
+ this.parameters = {
+ size: size,
+ segments: segments,
+ bottom: bottom,
+ lid: lid,
+ body: body,
+ fitLid: fitLid,
+ blinn: blinn
+ };
+
+ size = size || 50;
+
+ // number of segments per patch
+ segments = segments !== undefined ? Math.max( 2, Math.floor( segments ) || 10 ) : 10;
+
+ // which parts should be visible
+ bottom = bottom === undefined ? true : bottom;
+ lid = lid === undefined ? true : lid;
+ body = body === undefined ? true : body;
+
+ // Should the lid be snug? It's not traditional, but we make it snug by default
+ fitLid = fitLid === undefined ? true : fitLid;
+
+ // Jim Blinn scaled the teapot down in size by about 1.3 for
+ // some rendering tests. He liked the new proportions that he kept
+ // the data in this form. The model was distributed with these new
+ // proportions and became the norm. Trivia: comparing images of the
+ // real teapot and the computer model, the ratio for the bowl of the
+ // real teapot is more like 1.25, but since 1.3 is the traditional
+ // value given, we use it here.
+ var blinnScale = 1.3;
+ blinn = blinn === undefined ? true : blinn;
+
+ // scale the size to be the real scaling factor
+ var maxHeight = 3.15 * ( blinn ? 1 : blinnScale );
+
+ var maxHeight2 = maxHeight / 2;
+ var trueSize = size / maxHeight2;
+
+ // Number of elements depends on what is needed. Subtract degenerate
+ // triangles at tip of bottom and lid out in advance.
+ var numTriangles = bottom ? ( 8 * segments - 4 ) * segments : 0;
+ numTriangles += lid ? ( 16 * segments - 4 ) * segments : 0;
+ numTriangles += body ? 40 * segments * segments : 0;
+
+ var indices = new Uint32Array( numTriangles * 3 );
+
+ var numVertices = bottom ? 4 : 0;
+ numVertices += lid ? 8 : 0;
+ numVertices += body ? 20 : 0;
+ numVertices *= ( segments + 1 ) * ( segments + 1 );
+
+ var vertices = new Float32Array( numVertices * 3 );
+ var normals = new Float32Array( numVertices * 3 );
+ var uvs = new Float32Array( numVertices * 2 );
+
+ // Bezier form
+ var ms = new THREE.Matrix4();
+ ms.set( -1.0, 3.0, -3.0, 1.0,
+ 3.0, -6.0, 3.0, 0.0,
+ -3.0, 3.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0, 0.0 ) ;
+
+ var g = [];
+ var i, r, c;
+
+ var sp = [];
+ var tp = [];
+ var dsp = [];
+ var dtp = [];
+
+ // M * G * M matrix, sort of see
+ // http://www.cs.helsinki.fi/group/goa/mallinnus/curves/surfaces.html
+ var mgm = [];
+
+ var vert = [];
+ var sdir = [];
+ var tdir = [];
+
+ var norm = new THREE.Vector3();
+
+ var tcoord;
+
+ var sstep, tstep;
+ var vertPerRow;
+
+ var s, t, sval, tval, p;
+ var dsval = 0;
+ var dtval = 0;
+
+ var normOut = new THREE.Vector3();
+ var v1, v2, v3, v4;
+
+ var gmx = new THREE.Matrix4();
+ var tmtx = new THREE.Matrix4();
+
+ var vsp = new THREE.Vector4();
+ var vtp = new THREE.Vector4();
+ var vdsp = new THREE.Vector4();
+ var vdtp = new THREE.Vector4();
+
+ var vsdir = new THREE.Vector3();
+ var vtdir = new THREE.Vector3();
+
+ var mst = ms.clone();
+ mst.transpose();
+
+ // internal function: test if triangle has any matching vertices;
+ // if so, don't save triangle, since it won't display anything.
+ var notDegenerate = function ( vtx1, vtx2, vtx3 ) {
+
+ // if any vertex matches, return false
+ return ! ( ( ( vertices[ vtx1 * 3 ] === vertices[ vtx2 * 3 ] ) &&
+ ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx2 * 3 + 1 ] ) &&
+ ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx2 * 3 + 2 ] ) ) ||
+ ( ( vertices[ vtx1 * 3 ] === vertices[ vtx3 * 3 ] ) &&
+ ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) &&
+ ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ) ||
+ ( ( vertices[ vtx2 * 3 ] === vertices[ vtx3 * 3 ] ) &&
+ ( vertices[ vtx2 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) &&
+ ( vertices[ vtx2 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ) );
+
+ };
+
+
+ for ( i = 0; i < 3; i ++ )
+ {
+
+ mgm[ i ] = new THREE.Matrix4();
+
+ }
+
+ var minPatches = body ? 0 : 20;
+ var maxPatches = bottom ? 32 : 28;
+
+ vertPerRow = segments + 1;
+
+ var surfCount = 0;
+
+ var vertCount = 0;
+ var normCount = 0;
+ var uvCount = 0;
+
+ var indexCount = 0;
+
+ for ( var surf = minPatches ; surf < maxPatches ; surf ++ ) {
+
+ // lid is in the middle of the data, patches 20-27,
+ // so ignore it for this part of the loop if the lid is not desired
+ if ( lid || ( surf < 20 || surf >= 28 ) ) {
+
+ // get M * G * M matrix for x,y,z
+ for ( i = 0 ; i < 3 ; i ++ ) {
+
+ // get control patches
+ for ( r = 0 ; r < 4 ; r ++ ) {
+
+ for ( c = 0 ; c < 4 ; c ++ ) {
+
+ // transposed
+ g[ c * 4 + r ] = teapotVertices[ teapotPatches[ surf * 16 + r * 4 + c ] * 3 + i ] ;
+
+ // is the lid to be made larger, and is this a point on the lid
+ // that is X or Y?
+ if ( fitLid && ( surf >= 20 && surf < 28 ) && ( i !== 2 ) ) {
+
+ // increase XY size by 7.7%, found empirically. I don't
+ // increase Z so that the teapot will continue to fit in the
+ // space -1 to 1 for Y (Y is up for the final model).
+ g[ c * 4 + r ] *= 1.077;
+
+ }
+
+ // Blinn "fixed" the teapot by dividing Z by blinnScale, and that's the
+ // data we now use. The original teapot is taller. Fix it:
+ if ( ! blinn && ( i === 2 ) ) {
+
+ g[ c * 4 + r ] *= blinnScale;
+
+ }
+
+ }
+
+ }
+
+ gmx.set( g[ 0 ], g[ 1 ], g[ 2 ], g[ 3 ], g[ 4 ], g[ 5 ], g[ 6 ], g[ 7 ], g[ 8 ], g[ 9 ], g[ 10 ], g[ 11 ], g[ 12 ], g[ 13 ], g[ 14 ], g[ 15 ] );
+
+ tmtx.multiplyMatrices( gmx, ms );
+ mgm[ i ].multiplyMatrices( mst, tmtx );
+
+ }
+
+ // step along, get points, and output
+ for ( sstep = 0 ; sstep <= segments ; sstep ++ ) {
+
+ s = sstep / segments;
+
+ for ( tstep = 0 ; tstep <= segments ; tstep ++ ) {
+
+ t = tstep / segments;
+
+ // point from basis
+ // get power vectors and their derivatives
+ for ( p = 4, sval = tval = 1.0 ; p -- ; ) {
+
+ sp[ p ] = sval ;
+ tp[ p ] = tval ;
+ sval *= s ;
+ tval *= t ;
+
+ if ( p === 3 ) {
+
+ dsp[ p ] = dtp[ p ] = 0.0 ;
+ dsval = dtval = 1.0 ;
+
+ } else {
+
+ dsp[ p ] = dsval * ( 3 - p ) ;
+ dtp[ p ] = dtval * ( 3 - p ) ;
+ dsval *= s ;
+ dtval *= t ;
+
+ }
+
+ }
+
+ vsp.fromArray( sp );
+ vtp.fromArray( tp );
+ vdsp.fromArray( dsp );
+ vdtp.fromArray( dtp );
+
+ // do for x,y,z
+ for ( i = 0 ; i < 3 ; i ++ ) {
+
+ // multiply power vectors times matrix to get value
+ tcoord = vsp.clone();
+ tcoord.applyMatrix4( mgm[ i ] );
+ vert[ i ] = tcoord.dot( vtp );
+
+ // get s and t tangent vectors
+ tcoord = vdsp.clone();
+ tcoord.applyMatrix4( mgm[ i ] );
+ sdir[ i ] = tcoord.dot( vtp ) ;
+
+ tcoord = vsp.clone();
+ tcoord.applyMatrix4( mgm[ i ] );
+ tdir[ i ] = tcoord.dot( vdtp ) ;
+
+ }
+
+ // find normal
+ vsdir.fromArray( sdir );
+ vtdir.fromArray( tdir );
+ norm.crossVectors( vtdir, vsdir );
+ norm.normalize();
+
+ // if X and Z length is 0, at the cusp, so point the normal up or down, depending on patch number
+ if ( vert[ 0 ] === 0 && vert[ 1 ] === 0 )
+ {
+
+ // if above the middle of the teapot, normal points up, else down
+ normOut.set( 0, vert[ 2 ] > maxHeight2 ? 1 : - 1, 0 );
+
+ }
+ else
+ {
+
+ // standard output: rotate on X axis
+ normOut.set( norm.x, norm.z, - norm.y );
+
+ }
+
+ // store it all
+ vertices[ vertCount ++ ] = trueSize * vert[ 0 ];
+ vertices[ vertCount ++ ] = trueSize * ( vert[ 2 ] - maxHeight2 );
+ vertices[ vertCount ++ ] = - trueSize * vert[ 1 ];
+
+ normals[ normCount ++ ] = normOut.x;
+ normals[ normCount ++ ] = normOut.y;
+ normals[ normCount ++ ] = normOut.z;
+
+ uvs[ uvCount ++ ] = 1 - t;
+ uvs[ uvCount ++ ] = 1 - s;
+
+ }
+
+ }
+
+ // save the faces
+ for ( sstep = 0 ; sstep < segments ; sstep ++ ) {
+
+ for ( tstep = 0 ; tstep < segments ; tstep ++ ) {
+
+ v1 = surfCount * vertPerRow * vertPerRow + sstep * vertPerRow + tstep;
+ v2 = v1 + 1;
+ v3 = v2 + vertPerRow;
+ v4 = v1 + vertPerRow;
+
+ // Normals and UVs cannot be shared. Without clone(), you can see the consequences
+ // of sharing if you call geometry.applyMatrix( matrix ).
+ if ( notDegenerate ( v1, v2, v3 ) ) {
+
+ indices[ indexCount ++ ] = v1;
+ indices[ indexCount ++ ] = v2;
+ indices[ indexCount ++ ] = v3;
+
+ }
+ if ( notDegenerate ( v1, v3, v4 ) ) {
+
+ indices[ indexCount ++ ] = v1;
+ indices[ indexCount ++ ] = v3;
+ indices[ indexCount ++ ] = v4;
+
+ }
+
+ }
+
+ }
+
+ // increment only if a surface was used
+ surfCount ++;
+
+ }
+
+ }
+
+ this.setIndex( new THREE.BufferAttribute( indices, 1 ) );
+ this.addAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
+ this.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
+ this.addAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );
+
+ this.computeBoundingSphere();
+
+};
+
+
+THREE.TeapotBufferGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
+THREE.TeapotBufferGeometry.prototype.constructor = THREE.TeapotBufferGeometry;
+
+THREE.TeapotBufferGeometry.prototype.clone = function () {
+
+ var bufferGeometry = new THREE.TeapotBufferGeometry(
+ this.parameters.size,
+ this.parameters.segments,
+ this.parameters.bottom,
+ this.parameters.lid,
+ this.parameters.body,
+ this.parameters.fitLid,
+ this.parameters.blinn
+ );
+
+ return bufferGeometry;
+
+};
diff --git a/app/static/js/geometries/hilbert2D.js b/app/static/js/geometries/hilbert2D.js
new file mode 100644
index 0000000..c63501e
--- /dev/null
+++ b/app/static/js/geometries/hilbert2D.js
@@ -0,0 +1,63 @@
+/**
+ * Hilbert Curve: Generates 2D-Coordinates in a very fast way.
+ *
+ * @author Dylan Grafmyre
+ *
+ * Based on work by:
+ * @author Thomas Diewald
+ * @link http://www.openprocessing.org/sketch/15493
+ *
+ * @param center Center of Hilbert curve.
+ * @param size Total width of Hilbert curve.
+ * @param iterations Number of subdivisions.
+ * @param v0 Corner index -X, -Z.
+ * @param v1 Corner index -X, +Z.
+ * @param v2 Corner index +X, +Z.
+ * @param v3 Corner index +X, -Z.
+ */
+function hilbert2D( center, size, iterations, v0, v1, v2, v3 ) {
+
+ // Default Vars
+ var center = undefined !== center ? center : new THREE.Vector3( 0, 0, 0 ),
+ size = undefined !== size ? size : 10,
+ half = size / 2,
+ iterations = undefined !== iterations ? iterations : 1,
+ v0 = undefined !== v0 ? v0 : 0,
+ v1 = undefined !== v1 ? v1 : 1,
+ v2 = undefined !== v2 ? v2 : 2,
+ v3 = undefined !== v3 ? v3 : 3
+ ;
+
+ var vec_s = [
+ new THREE.Vector3( center.x - half, center.y, center.z - half ),
+ new THREE.Vector3( center.x - half, center.y, center.z + half ),
+ new THREE.Vector3( center.x + half, center.y, center.z + half ),
+ new THREE.Vector3( center.x + half, center.y, center.z - half )
+ ];
+
+ var vec = [
+ vec_s[ v0 ],
+ vec_s[ v1 ],
+ vec_s[ v2 ],
+ vec_s[ v3 ]
+ ];
+
+ // Recurse iterations
+ if ( 0 <= -- iterations ) {
+
+ var tmp = [];
+
+ Array.prototype.push.apply( tmp, hilbert2D( vec[ 0 ], half, iterations, v0, v3, v2, v1 ) );
+ Array.prototype.push.apply( tmp, hilbert2D( vec[ 1 ], half, iterations, v0, v1, v2, v3 ) );
+ Array.prototype.push.apply( tmp, hilbert2D( vec[ 2 ], half, iterations, v0, v1, v2, v3 ) );
+ Array.prototype.push.apply( tmp, hilbert2D( vec[ 3 ], half, iterations, v2, v1, v0, v3 ) );
+
+ // Return recursive call
+ return tmp;
+
+ }
+
+ // Return complete Hilbert Curve.
+ return vec;
+
+}
diff --git a/app/static/js/geometries/hilbert3D.js b/app/static/js/geometries/hilbert3D.js
new file mode 100644
index 0000000..23d6ca8
--- /dev/null
+++ b/app/static/js/geometries/hilbert3D.js
@@ -0,0 +1,88 @@
+/**
+ * Hilbert Curve: Generates 2D-Coordinates in a very fast way.
+ *
+ * @author Dylan Grafmyre
+ *
+ * Based on work by:
+ * @author Thomas Diewald
+ * @link http://www.openprocessing.org/visuals/?visualID=15599
+ *
+ * Based on `examples/canvas_lines_colors.html`:
+ * @author OpenShift guest
+ * @link https://github.com/mrdoob/three.js/blob/8413a860aa95ed29c79cbb7f857c97d7880d260f/examples/canvas_lines_colors.html
+ * @see Line 149 - 186
+ *
+ * @param center Center of Hilbert curve.
+ * @param size Total width of Hilbert curve.
+ * @param iterations Number of subdivisions.
+ * @param v0 Corner index -X, +Y, -Z.
+ * @param v1 Corner index -X, +Y, +Z.
+ * @param v2 Corner index -X, -Y, +Z.
+ * @param v3 Corner index -X, -Y, -Z.
+ * @param v4 Corner index +X, -Y, -Z.
+ * @param v5 Corner index +X, -Y, +Z.
+ * @param v6 Corner index +X, +Y, +Z.
+ * @param v7 Corner index +X, +Y, -Z.
+ */
+function hilbert3D( center, size, iterations, v0, v1, v2, v3, v4, v5, v6, v7 ) {
+
+ // Default Vars
+ var center = undefined !== center ? center : new THREE.Vector3( 0, 0, 0 ),
+ size = undefined !== size ? size : 10,
+ half = size / 2,
+ iterations = undefined !== iterations ? iterations : 1,
+ v0 = undefined !== v0 ? v0 : 0,
+ v1 = undefined !== v1 ? v1 : 1,
+ v2 = undefined !== v2 ? v2 : 2,
+ v3 = undefined !== v3 ? v3 : 3,
+ v4 = undefined !== v4 ? v4 : 4,
+ v5 = undefined !== v5 ? v5 : 5,
+ v6 = undefined !== v6 ? v6 : 6,
+ v7 = undefined !== v7 ? v7 : 7
+ ;
+
+ var vec_s = [
+ new THREE.Vector3( center.x - half, center.y + half, center.z - half ),
+ new THREE.Vector3( center.x - half, center.y + half, center.z + half ),
+ new THREE.Vector3( center.x - half, center.y - half, center.z + half ),
+ new THREE.Vector3( center.x - half, center.y - half, center.z - half ),
+ new THREE.Vector3( center.x + half, center.y - half, center.z - half ),
+ new THREE.Vector3( center.x + half, center.y - half, center.z + half ),
+ new THREE.Vector3( center.x + half, center.y + half, center.z + half ),
+ new THREE.Vector3( center.x + half, center.y + half, center.z - half )
+ ];
+
+ var vec = [
+ vec_s[ v0 ],
+ vec_s[ v1 ],
+ vec_s[ v2 ],
+ vec_s[ v3 ],
+ vec_s[ v4 ],
+ vec_s[ v5 ],
+ vec_s[ v6 ],
+ vec_s[ v7 ]
+ ];
+
+ // Recurse iterations
+ if ( -- iterations >= 0 ) {
+
+ var tmp = [];
+
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 0 ], half, iterations, v0, v3, v4, v7, v6, v5, v2, v1 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 1 ], half, iterations, v0, v7, v6, v1, v2, v5, v4, v3 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 2 ], half, iterations, v0, v7, v6, v1, v2, v5, v4, v3 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 3 ], half, iterations, v2, v3, v0, v1, v6, v7, v4, v5 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 4 ], half, iterations, v2, v3, v0, v1, v6, v7, v4, v5 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 5 ], half, iterations, v4, v3, v2, v5, v6, v1, v0, v7 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 6 ], half, iterations, v4, v3, v2, v5, v6, v1, v0, v7 ) );
+ Array.prototype.push.apply( tmp, hilbert3D( vec[ 7 ], half, iterations, v6, v5, v2, v1, v0, v3, v4, v7 ) );
+
+ // Return recursive call
+ return tmp;
+
+ }
+
+ // Return complete Hilbert Curve.
+ return vec;
+
+}
diff --git a/app/static/js/libs/acorn/acorn.js b/app/static/js/libs/acorn/acorn.js
new file mode 100644
index 0000000..788eab6
--- /dev/null
+++ b/app/static/js/libs/acorn/acorn.js
@@ -0,0 +1,3236 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.acorn = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 6) return;
+ var key = prop.key,
+ name = undefined;
+ switch (key.type) {
+ case "Identifier":
+ name = key.name;break;
+ case "Literal":
+ name = String(key.value);break;
+ default:
+ return;
+ }
+ var kind = prop.kind || "init",
+ other = undefined;
+ if (has(propHash, name)) {
+ other = propHash[name];
+ var isGetSet = kind !== "init";
+ if ((this.strict || isGetSet) && other[kind] || !(isGetSet ^ other.init)) this.raise(key.start, "Redefinition of property");
+ } else {
+ other = propHash[name] = {
+ init: false,
+ get: false,
+ set: false
+ };
+ }
+ other[kind] = true;
+};
+
+// ### Expression parsing
+
+// These nest, from the most general expression type at the top to
+// 'atomic', nondivisible expression types at the bottom. Most of
+// the functions will simply let the function(s) below them parse,
+// and, *if* the syntactic construct they handle is present, wrap
+// the AST node that the inner parser gave them in another node.
+
+// Parse a full expression. The optional arguments are used to
+// forbid the `in` operator (in for loops initalization expressions)
+// and provide reference for storing '=' operator inside shorthand
+// property assignment in contexts where both object expression
+// and object pattern might appear (so it's possible to raise
+// delayed syntax error at correct position).
+
+pp.parseExpression = function (noIn, refShorthandDefaultPos) {
+ var start = this.markPosition();
+ var expr = this.parseMaybeAssign(noIn, refShorthandDefaultPos);
+ if (this.type === tt.comma) {
+ var node = this.startNodeAt(start);
+ node.expressions = [expr];
+ while (this.eat(tt.comma)) node.expressions.push(this.parseMaybeAssign(noIn, refShorthandDefaultPos));
+ return this.finishNode(node, "SequenceExpression");
+ }
+ return expr;
+};
+
+// Parse an assignment expression. This includes applications of
+// operators like `+=`.
+
+pp.parseMaybeAssign = function (noIn, refShorthandDefaultPos) {
+ if (this.type == tt._yield && this.inGenerator) return this.parseYield();
+
+ var failOnShorthandAssign = undefined;
+ if (!refShorthandDefaultPos) {
+ refShorthandDefaultPos = { start: 0 };
+ failOnShorthandAssign = true;
+ } else {
+ failOnShorthandAssign = false;
+ }
+ var start = this.markPosition();
+ var left = this.parseMaybeConditional(noIn, refShorthandDefaultPos);
+ if (this.type.isAssign) {
+ var node = this.startNodeAt(start);
+ node.operator = this.value;
+ node.left = this.type === tt.eq ? this.toAssignable(left) : left;
+ refShorthandDefaultPos.start = 0; // reset because shorthand default was used correctly
+ this.checkLVal(left);
+ this.next();
+ node.right = this.parseMaybeAssign(noIn);
+ return this.finishNode(node, "AssignmentExpression");
+ } else if (failOnShorthandAssign && refShorthandDefaultPos.start) {
+ this.unexpected(refShorthandDefaultPos.start);
+ }
+ return left;
+};
+
+// Parse a ternary conditional (`?:`) operator.
+
+pp.parseMaybeConditional = function (noIn, refShorthandDefaultPos) {
+ var start = this.markPosition();
+ var expr = this.parseExprOps(noIn, refShorthandDefaultPos);
+ if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
+ if (this.eat(tt.question)) {
+ var node = this.startNodeAt(start);
+ node.test = expr;
+ node.consequent = this.parseMaybeAssign();
+ this.expect(tt.colon);
+ node.alternate = this.parseMaybeAssign(noIn);
+ return this.finishNode(node, "ConditionalExpression");
+ }
+ return expr;
+};
+
+// Start the precedence parser.
+
+pp.parseExprOps = function (noIn, refShorthandDefaultPos) {
+ var start = this.markPosition();
+ var expr = this.parseMaybeUnary(refShorthandDefaultPos);
+ if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
+ return this.parseExprOp(expr, start, -1, noIn);
+};
+
+// Parse binary operators with the operator precedence parsing
+// algorithm. `left` is the left-hand side of the operator.
+// `minPrec` provides context that allows the function to stop and
+// defer further parser to one of its callers when it encounters an
+// operator that has a lower precedence than the set it is parsing.
+
+pp.parseExprOp = function (left, leftStart, minPrec, noIn) {
+ var prec = this.type.binop;
+ if (prec != null && (!noIn || this.type !== tt._in)) {
+ if (prec > minPrec) {
+ var node = this.startNodeAt(leftStart);
+ node.left = left;
+ node.operator = this.value;
+ var op = this.type;
+ this.next();
+ var start = this.markPosition();
+ node.right = this.parseExprOp(this.parseMaybeUnary(), start, prec, noIn);
+ this.finishNode(node, op === tt.logicalOR || op === tt.logicalAND ? "LogicalExpression" : "BinaryExpression");
+ return this.parseExprOp(node, leftStart, minPrec, noIn);
+ }
+ }
+ return left;
+};
+
+// Parse unary operators, both prefix and postfix.
+
+pp.parseMaybeUnary = function (refShorthandDefaultPos) {
+ if (this.type.prefix) {
+ var node = this.startNode(),
+ update = this.type === tt.incDec;
+ node.operator = this.value;
+ node.prefix = true;
+ this.next();
+ node.argument = this.parseMaybeUnary();
+ if (refShorthandDefaultPos && refShorthandDefaultPos.start) this.unexpected(refShorthandDefaultPos.start);
+ if (update) this.checkLVal(node.argument);else if (this.strict && node.operator === "delete" && node.argument.type === "Identifier") this.raise(node.start, "Deleting local variable in strict mode");
+ return this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression");
+ }
+ var start = this.markPosition();
+ var expr = this.parseExprSubscripts(refShorthandDefaultPos);
+ if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
+ while (this.type.postfix && !this.canInsertSemicolon()) {
+ var node = this.startNodeAt(start);
+ node.operator = this.value;
+ node.prefix = false;
+ node.argument = expr;
+ this.checkLVal(expr);
+ this.next();
+ expr = this.finishNode(node, "UpdateExpression");
+ }
+ return expr;
+};
+
+// Parse call, dot, and `[]`-subscript expressions.
+
+pp.parseExprSubscripts = function (refShorthandDefaultPos) {
+ var start = this.markPosition();
+ var expr = this.parseExprAtom(refShorthandDefaultPos);
+ if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
+ return this.parseSubscripts(expr, start);
+};
+
+pp.parseSubscripts = function (base, start, noCalls) {
+ if (this.eat(tt.dot)) {
+ var node = this.startNodeAt(start);
+ node.object = base;
+ node.property = this.parseIdent(true);
+ node.computed = false;
+ return this.parseSubscripts(this.finishNode(node, "MemberExpression"), start, noCalls);
+ } else if (this.eat(tt.bracketL)) {
+ var node = this.startNodeAt(start);
+ node.object = base;
+ node.property = this.parseExpression();
+ node.computed = true;
+ this.expect(tt.bracketR);
+ return this.parseSubscripts(this.finishNode(node, "MemberExpression"), start, noCalls);
+ } else if (!noCalls && this.eat(tt.parenL)) {
+ var node = this.startNodeAt(start);
+ node.callee = base;
+ node.arguments = this.parseExprList(tt.parenR, false);
+ return this.parseSubscripts(this.finishNode(node, "CallExpression"), start, noCalls);
+ } else if (this.type === tt.backQuote) {
+ var node = this.startNodeAt(start);
+ node.tag = base;
+ node.quasi = this.parseTemplate();
+ return this.parseSubscripts(this.finishNode(node, "TaggedTemplateExpression"), start, noCalls);
+ }return base;
+};
+
+// Parse an atomic expression — either a single token that is an
+// expression, an expression started by a keyword like `function` or
+// `new`, or an expression wrapped in punctuation like `()`, `[]`,
+// or `{}`.
+
+pp.parseExprAtom = function (refShorthandDefaultPos) {
+ var node = undefined;
+ switch (this.type) {
+ case tt._this:
+ case tt._super:
+ var type = this.type === tt._this ? "ThisExpression" : "Super";
+ node = this.startNode();
+ this.next();
+ return this.finishNode(node, type);
+
+ case tt._yield:
+ if (this.inGenerator) unexpected();
+
+ case tt.name:
+ var start = this.markPosition();
+ var id = this.parseIdent(this.type !== tt.name);
+ if (!this.canInsertSemicolon() && this.eat(tt.arrow)) {
+ return this.parseArrowExpression(this.startNodeAt(start), [id]);
+ }
+ return id;
+
+ case tt.regexp:
+ var value = this.value;
+ node = this.parseLiteral(value.value);
+ node.regex = { pattern: value.pattern, flags: value.flags };
+ return node;
+
+ case tt.num:case tt.string:
+ return this.parseLiteral(this.value);
+
+ case tt._null:case tt._true:case tt._false:
+ node = this.startNode();
+ node.value = this.type === tt._null ? null : this.type === tt._true;
+ node.raw = this.type.keyword;
+ this.next();
+ return this.finishNode(node, "Literal");
+
+ case tt.parenL:
+ return this.parseParenAndDistinguishExpression();
+
+ case tt.bracketL:
+ node = this.startNode();
+ this.next();
+ // check whether this is array comprehension or regular array
+ if (this.options.ecmaVersion >= 7 && this.type === tt._for) {
+ return this.parseComprehension(node, false);
+ }
+ node.elements = this.parseExprList(tt.bracketR, true, true, refShorthandDefaultPos);
+ return this.finishNode(node, "ArrayExpression");
+
+ case tt.braceL:
+ return this.parseObj(false, refShorthandDefaultPos);
+
+ case tt._function:
+ node = this.startNode();
+ this.next();
+ return this.parseFunction(node, false);
+
+ case tt._class:
+ return this.parseClass(this.startNode(), false);
+
+ case tt._new:
+ return this.parseNew();
+
+ case tt.backQuote:
+ return this.parseTemplate();
+
+ default:
+ this.unexpected();
+ }
+};
+
+pp.parseLiteral = function (value) {
+ var node = this.startNode();
+ node.value = value;
+ node.raw = this.input.slice(this.start, this.end);
+ this.next();
+ return this.finishNode(node, "Literal");
+};
+
+pp.parseParenExpression = function () {
+ this.expect(tt.parenL);
+ var val = this.parseExpression();
+ this.expect(tt.parenR);
+ return val;
+};
+
+pp.parseParenAndDistinguishExpression = function () {
+ var start = this.markPosition(),
+ val = undefined;
+ if (this.options.ecmaVersion >= 6) {
+ this.next();
+
+ if (this.options.ecmaVersion >= 7 && this.type === tt._for) {
+ return this.parseComprehension(this.startNodeAt(start), true);
+ }
+
+ var innerStart = this.markPosition(),
+ exprList = [],
+ first = true;
+ var refShorthandDefaultPos = { start: 0 },
+ spreadStart = undefined,
+ innerParenStart = undefined;
+ while (this.type !== tt.parenR) {
+ first ? first = false : this.expect(tt.comma);
+ if (this.type === tt.ellipsis) {
+ spreadStart = this.start;
+ exprList.push(this.parseRest());
+ break;
+ } else {
+ if (this.type === tt.parenL && !innerParenStart) {
+ innerParenStart = this.start;
+ }
+ exprList.push(this.parseMaybeAssign(false, refShorthandDefaultPos));
+ }
+ }
+ var innerEnd = this.markPosition();
+ this.expect(tt.parenR);
+
+ if (!this.canInsertSemicolon() && this.eat(tt.arrow)) {
+ if (innerParenStart) this.unexpected(innerParenStart);
+ return this.parseArrowExpression(this.startNodeAt(start), exprList);
+ }
+
+ if (!exprList.length) this.unexpected(this.lastTokStart);
+ if (spreadStart) this.unexpected(spreadStart);
+ if (refShorthandDefaultPos.start) this.unexpected(refShorthandDefaultPos.start);
+
+ if (exprList.length > 1) {
+ val = this.startNodeAt(innerStart);
+ val.expressions = exprList;
+ this.finishNodeAt(val, "SequenceExpression", innerEnd);
+ } else {
+ val = exprList[0];
+ }
+ } else {
+ val = this.parseParenExpression();
+ }
+
+ if (this.options.preserveParens) {
+ var par = this.startNodeAt(start);
+ par.expression = val;
+ return this.finishNode(par, "ParenthesizedExpression");
+ } else {
+ return val;
+ }
+};
+
+// New's precedence is slightly tricky. It must allow its argument
+// to be a `[]` or dot subscript expression, but not a call — at
+// least, not without wrapping it in parentheses. Thus, it uses the
+
+var empty = [];
+
+pp.parseNew = function () {
+ var node = this.startNode();
+ var meta = this.parseIdent(true);
+ if (this.options.ecmaVersion >= 6 && this.eat(tt.dot)) {
+ node.meta = meta;
+ node.property = this.parseIdent(true);
+ if (node.property.name !== "target") this.raise(node.property.start, "The only valid meta property for new is new.target");
+ return this.finishNode(node, "MetaProperty");
+ }
+ var start = this.markPosition();
+ node.callee = this.parseSubscripts(this.parseExprAtom(), start, true);
+ if (this.eat(tt.parenL)) node.arguments = this.parseExprList(tt.parenR, false);else node.arguments = empty;
+ return this.finishNode(node, "NewExpression");
+};
+
+// Parse template expression.
+
+pp.parseTemplateElement = function () {
+ var elem = this.startNode();
+ elem.value = {
+ raw: this.input.slice(this.start, this.end),
+ cooked: this.value
+ };
+ this.next();
+ elem.tail = this.type === tt.backQuote;
+ return this.finishNode(elem, "TemplateElement");
+};
+
+pp.parseTemplate = function () {
+ var node = this.startNode();
+ this.next();
+ node.expressions = [];
+ var curElt = this.parseTemplateElement();
+ node.quasis = [curElt];
+ while (!curElt.tail) {
+ this.expect(tt.dollarBraceL);
+ node.expressions.push(this.parseExpression());
+ this.expect(tt.braceR);
+ node.quasis.push(curElt = this.parseTemplateElement());
+ }
+ this.next();
+ return this.finishNode(node, "TemplateLiteral");
+};
+
+// Parse an object literal or binding pattern.
+
+pp.parseObj = function (isPattern, refShorthandDefaultPos) {
+ var node = this.startNode(),
+ first = true,
+ propHash = {};
+ node.properties = [];
+ this.next();
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma);
+ if (this.afterTrailingComma(tt.braceR)) break;
+ } else first = false;
+
+ var prop = this.startNode(),
+ isGenerator = undefined,
+ start = undefined;
+ if (this.options.ecmaVersion >= 6) {
+ prop.method = false;
+ prop.shorthand = false;
+ if (isPattern || refShorthandDefaultPos) start = this.markPosition();
+ if (!isPattern) isGenerator = this.eat(tt.star);
+ }
+ this.parsePropertyName(prop);
+ if (this.eat(tt.colon)) {
+ prop.value = isPattern ? this.parseMaybeDefault() : this.parseMaybeAssign(false, refShorthandDefaultPos);
+ prop.kind = "init";
+ } else if (this.options.ecmaVersion >= 6 && this.type === tt.parenL) {
+ if (isPattern) this.unexpected();
+ prop.kind = "init";
+ prop.method = true;
+ prop.value = this.parseMethod(isGenerator);
+ } else if (this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" && (prop.key.name === "get" || prop.key.name === "set") && (this.type != tt.comma && this.type != tt.braceR)) {
+ if (isGenerator || isPattern) this.unexpected();
+ prop.kind = prop.key.name;
+ this.parsePropertyName(prop);
+ prop.value = this.parseMethod(false);
+ } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") {
+ prop.kind = "init";
+ if (isPattern) {
+ if (this.isKeyword(prop.key.name) || this.strict && (reservedWords.strictBind(prop.key.name) || reservedWords.strict(prop.key.name)) || !this.options.allowReserved && this.isReservedWord(prop.key.name)) this.raise(prop.key.start, "Binding " + prop.key.name);
+ prop.value = this.parseMaybeDefault(start, prop.key);
+ } else if (this.type === tt.eq && refShorthandDefaultPos) {
+ if (!refShorthandDefaultPos.start) refShorthandDefaultPos.start = this.start;
+ prop.value = this.parseMaybeDefault(start, prop.key);
+ } else {
+ prop.value = prop.key;
+ }
+ prop.shorthand = true;
+ } else this.unexpected();
+
+ this.checkPropClash(prop, propHash);
+ node.properties.push(this.finishNode(prop, "Property"));
+ }
+ return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression");
+};
+
+pp.parsePropertyName = function (prop) {
+ if (this.options.ecmaVersion >= 6) {
+ if (this.eat(tt.bracketL)) {
+ prop.computed = true;
+ prop.key = this.parseMaybeAssign();
+ this.expect(tt.bracketR);
+ return;
+ } else {
+ prop.computed = false;
+ }
+ }
+ prop.key = this.type === tt.num || this.type === tt.string ? this.parseExprAtom() : this.parseIdent(true);
+};
+
+// Initialize empty function node.
+
+pp.initFunction = function (node) {
+ node.id = null;
+ if (this.options.ecmaVersion >= 6) {
+ node.generator = false;
+ node.expression = false;
+ }
+};
+
+// Parse object or class method.
+
+pp.parseMethod = function (isGenerator) {
+ var node = this.startNode();
+ this.initFunction(node);
+ this.expect(tt.parenL);
+ node.params = this.parseBindingList(tt.parenR, false, false);
+ var allowExpressionBody = undefined;
+ if (this.options.ecmaVersion >= 6) {
+ node.generator = isGenerator;
+ allowExpressionBody = true;
+ } else {
+ allowExpressionBody = false;
+ }
+ this.parseFunctionBody(node, allowExpressionBody);
+ return this.finishNode(node, "FunctionExpression");
+};
+
+// Parse arrow function expression with given parameters.
+
+pp.parseArrowExpression = function (node, params) {
+ this.initFunction(node);
+ node.params = this.toAssignableList(params, true);
+ this.parseFunctionBody(node, true);
+ return this.finishNode(node, "ArrowFunctionExpression");
+};
+
+// Parse function body and check parameters.
+
+pp.parseFunctionBody = function (node, allowExpression) {
+ var isExpression = allowExpression && this.type !== tt.braceL;
+
+ if (isExpression) {
+ node.body = this.parseMaybeAssign();
+ node.expression = true;
+ } else {
+ // Start a new scope with regard to labels and the `inFunction`
+ // flag (restore them to their old value afterwards).
+ var oldInFunc = this.inFunction,
+ oldInGen = this.inGenerator,
+ oldLabels = this.labels;
+ this.inFunction = true;this.inGenerator = node.generator;this.labels = [];
+ node.body = this.parseBlock(true);
+ node.expression = false;
+ this.inFunction = oldInFunc;this.inGenerator = oldInGen;this.labels = oldLabels;
+ }
+
+ // If this is a strict mode function, verify that argument names
+ // are not repeated, and it does not try to bind the words `eval`
+ // or `arguments`.
+ if (this.strict || !isExpression && node.body.body.length && this.isUseStrict(node.body.body[0])) {
+ var nameHash = {},
+ oldStrict = this.strict;
+ this.strict = true;
+ if (node.id) this.checkLVal(node.id, true);
+ for (var i = 0; i < node.params.length; i++) {
+ this.checkLVal(node.params[i], true, nameHash);
+ }this.strict = oldStrict;
+ }
+};
+
+// Parses a comma-separated list of expressions, and returns them as
+// an array. `close` is the token type that ends the list, and
+// `allowEmpty` can be turned on to allow subsequent commas with
+// nothing in between them to be parsed as `null` (which is needed
+// for array literals).
+
+pp.parseExprList = function (close, allowTrailingComma, allowEmpty, refShorthandDefaultPos) {
+ var elts = [],
+ first = true;
+ while (!this.eat(close)) {
+ if (!first) {
+ this.expect(tt.comma);
+ if (allowTrailingComma && this.afterTrailingComma(close)) break;
+ } else first = false;
+
+ if (allowEmpty && this.type === tt.comma) {
+ elts.push(null);
+ } else {
+ if (this.type === tt.ellipsis) elts.push(this.parseSpread(refShorthandDefaultPos));else elts.push(this.parseMaybeAssign(false, refShorthandDefaultPos));
+ }
+ }
+ return elts;
+};
+
+// Parse the next token as an identifier. If `liberal` is true (used
+// when parsing properties), it will also convert keywords into
+// identifiers.
+
+pp.parseIdent = function (liberal) {
+ var node = this.startNode();
+ if (liberal && this.options.allowReserved == "never") liberal = false;
+ if (this.type === tt.name) {
+ if (!liberal && (!this.options.allowReserved && this.isReservedWord(this.value) || this.strict && reservedWords.strict(this.value) && (this.options.ecmaVersion >= 6 || this.input.slice(this.start, this.end).indexOf("\\") == -1))) this.raise(this.start, "The keyword '" + this.value + "' is reserved");
+ node.name = this.value;
+ } else if (liberal && this.type.keyword) {
+ node.name = this.type.keyword;
+ } else {
+ this.unexpected();
+ }
+ this.next();
+ return this.finishNode(node, "Identifier");
+};
+
+// Parses yield expression inside generator.
+
+pp.parseYield = function () {
+ var node = this.startNode();
+ this.next();
+ if (this.type == tt.semi || this.canInsertSemicolon() || this.type != tt.star && !this.type.startsExpr) {
+ node.delegate = false;
+ node.argument = null;
+ } else {
+ node.delegate = this.eat(tt.star);
+ node.argument = this.parseMaybeAssign();
+ }
+ return this.finishNode(node, "YieldExpression");
+};
+
+// Parses array and generator comprehensions.
+
+pp.parseComprehension = function (node, isGenerator) {
+ node.blocks = [];
+ while (this.type === tt._for) {
+ var block = this.startNode();
+ this.next();
+ this.expect(tt.parenL);
+ block.left = this.parseBindingAtom();
+ this.checkLVal(block.left, true);
+ this.expectContextual("of");
+ block.right = this.parseExpression();
+ this.expect(tt.parenR);
+ node.blocks.push(this.finishNode(block, "ComprehensionBlock"));
+ }
+ node.filter = this.eat(tt._if) ? this.parseParenExpression() : null;
+ node.body = this.parseExpression();
+ this.expect(isGenerator ? tt.parenR : tt.bracketR);
+ node.generator = isGenerator;
+ return this.finishNode(node, "ComprehensionExpression");
+};
+
+},{"./identifier":3,"./state":9,"./tokentype":13,"./util":14}],3:[function(require,module,exports){
+
+
+// Test whether a given character code starts an identifier.
+
+"use strict";
+
+exports.isIdentifierStart = isIdentifierStart;
+
+// Test whether a given character is part of an identifier.
+
+exports.isIdentifierChar = isIdentifierChar;
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+// This is a trick taken from Esprima. It turns out that, on
+// non-Chrome browsers, to check whether a string is in a set, a
+// predicate containing a big ugly `switch` statement is faster than
+// a regular expression, and on Chrome the two are about on par.
+// This function uses `eval` (non-lexical) to produce such a
+// predicate from a space-separated string of words.
+//
+// It starts by sorting the words by length.
+
+function makePredicate(words) {
+ words = words.split(" ");
+ var f = "",
+ cats = [];
+ out: for (var i = 0; i < words.length; ++i) {
+ for (var j = 0; j < cats.length; ++j) {
+ if (cats[j][0].length == words[i].length) {
+ cats[j].push(words[i]);
+ continue out;
+ }
+ }cats.push([words[i]]);
+ }
+ function compareTo(arr) {
+ if (arr.length == 1) {
+ return f += "return str === " + JSON.stringify(arr[0]) + ";";
+ }f += "switch(str){";
+ for (var i = 0; i < arr.length; ++i) {
+ f += "case " + JSON.stringify(arr[i]) + ":";
+ }f += "return true}return false;";
+ }
+
+ // When there are more than three length categories, an outer
+ // switch first dispatches on the lengths, to save on comparisons.
+
+ if (cats.length > 3) {
+ cats.sort(function (a, b) {
+ return b.length - a.length;
+ });
+ f += "switch(str.length){";
+ for (var i = 0; i < cats.length; ++i) {
+ var cat = cats[i];
+ f += "case " + cat[0].length + ":";
+ compareTo(cat);
+ }
+ f += "}"
+
+ // Otherwise, simply generate a flat `switch` statement.
+
+ ;
+ } else {
+ compareTo(words);
+ }
+ return new Function("str", f);
+}
+
+// Reserved word lists for various dialects of the language
+
+var reservedWords = {
+ 3: makePredicate("abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile"),
+ 5: makePredicate("class enum extends super const export import"),
+ 6: makePredicate("enum await"),
+ strict: makePredicate("implements interface let package private protected public static yield"),
+ strictBind: makePredicate("eval arguments")
+};
+
+exports.reservedWords = reservedWords;
+// And the keywords
+
+var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this";
+
+var keywords = {
+ 5: makePredicate(ecma5AndLessKeywords),
+ 6: makePredicate(ecma5AndLessKeywords + " let const class extends export import yield super")
+};
+
+exports.keywords = keywords;
+// ## Character categories
+
+// Big ugly regular expressions that match characters in the
+// whitespace, identifier, and identifier-start categories. These
+// are only applied when a character is found to actually have a
+// code point above 128.
+// Generated by `tools/generate-identifier-regex.js`.
+
+var nonASCIIidentifierStartChars = "ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠ-ࢲऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕ℘-ℝℤΩℨK-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ々-〇〡-〩〱-〵〸-〼ぁ-ゖ゛-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞭꞰꞱꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭟꭤꭥꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ";
+var nonASCIIidentifierChars = "·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-٩ٰۖ-ۜ۟-۪ۤۧۨ-ۭ۰-۹ܑܰ-݊ަ-ް߀-߉߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣤ-ःऺ-़ा-ॏ॑-ॗॢॣ०-९ঁ-ঃ়া-ৄেৈো-্ৗৢৣ০-৯ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑ੦-ੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣ૦-૯ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣ୦-୯ஂா-ூெ-ைொ-்ௗ௦-௯ఀ-ఃా-ౄె-ైొ-్ౕౖౢౣ౦-౯ಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣ೦-೯ഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣ൦-൯ංඃ්ා-ුූෘ-ෟ෦-෯ෲෳัิ-ฺ็-๎๐-๙ັິ-ູົຼ່-ໍ໐-໙༘༙༠-༩༹༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှ၀-၉ၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏ-ႝ፝-፟፩-፱ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝០-៩᠋-᠍᠐-᠙ᢩᤠ-ᤫᤰ-᤻᥆-᥏ᦰ-ᧀᧈᧉ᧐-᧚ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼-᪉᪐-᪙᪰-᪽ᬀ-ᬄ᬴-᭄᭐-᭙᭫-᭳ᮀ-ᮂᮡ-ᮭ᮰-᮹᯦-᯳ᰤ-᰷᱀-᱉᱐-᱙᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷼-᷿‿⁀⁔⃐-⃥⃜⃡-⃰⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꘠-꘩꙯ꙴ-꙽ꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-꣄꣐-꣙꣠-꣱꤀-꤉ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀꧐-꧙ꧥ꧰-꧹ꨩ-ꨶꩃꩌꩍ꩐-꩙ꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭꯰-꯹ﬞ︀-️︠-︭︳︴﹍-﹏0-9_";
+
+var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
+var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
+
+nonASCIIidentifierStartChars = nonASCIIidentifierChars = null;
+
+// These are a run-length and offset encoded representation of the
+// >0xffff code points that are a valid part of identifiers. The
+// offset starts at 0x10000, and each pair of numbers represents an
+// offset to the next range, and then a size of the range. They were
+// generated by tools/generate-identifier-regex.js
+var astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 17, 26, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 99, 39, 9, 51, 157, 310, 10, 21, 11, 7, 153, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 98, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 26, 45, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 955, 52, 76, 44, 33, 24, 27, 35, 42, 34, 4, 0, 13, 47, 15, 3, 22, 0, 38, 17, 2, 24, 133, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 32, 4, 287, 47, 21, 1, 2, 0, 185, 46, 82, 47, 21, 0, 60, 42, 502, 63, 32, 0, 449, 56, 1288, 920, 104, 110, 2962, 1070, 13266, 568, 8, 30, 114, 29, 19, 47, 17, 3, 32, 20, 6, 18, 881, 68, 12, 0, 67, 12, 16481, 1, 3071, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 4149, 196, 1340, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42710, 42, 4148, 12, 221, 16355, 541];
+var astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 1306, 2, 54, 14, 32, 9, 16, 3, 46, 10, 54, 9, 7, 2, 37, 13, 2, 9, 52, 0, 13, 2, 49, 13, 16, 9, 83, 11, 168, 11, 6, 9, 8, 2, 57, 0, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 316, 19, 13, 9, 214, 6, 3, 8, 112, 16, 16, 9, 82, 12, 9, 9, 535, 9, 20855, 9, 135, 4, 60, 6, 26, 9, 1016, 45, 17, 3, 19723, 1, 5319, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 4305, 6, 792618, 239];
+
+// This has a complexity linear to the value of the code. The
+// assumption is that looking up astral identifier characters is
+// rare.
+function isInAstralSet(code, set) {
+ var pos = 65536;
+ for (var i = 0; i < set.length; i += 2) {
+ pos += set[i];
+ if (pos > code) {
+ return false;
+ }pos += set[i + 1];
+ if (pos >= code) {
+ return true;
+ }
+ }
+}
+function isIdentifierStart(code, astral) {
+ if (code < 65) {
+ return code === 36;
+ }if (code < 91) {
+ return true;
+ }if (code < 97) {
+ return code === 95;
+ }if (code < 123) {
+ return true;
+ }if (code <= 65535) {
+ return code >= 170 && nonASCIIidentifierStart.test(String.fromCharCode(code));
+ }if (astral === false) {
+ return false;
+ }return isInAstralSet(code, astralIdentifierStartCodes);
+}
+
+function isIdentifierChar(code, astral) {
+ if (code < 48) {
+ return code === 36;
+ }if (code < 58) {
+ return true;
+ }if (code < 65) {
+ return false;
+ }if (code < 91) {
+ return true;
+ }if (code < 97) {
+ return code === 95;
+ }if (code < 123) {
+ return true;
+ }if (code <= 65535) {
+ return code >= 170 && nonASCIIidentifier.test(String.fromCharCode(code));
+ }if (astral === false) {
+ return false;
+ }return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes);
+}
+
+},{}],4:[function(require,module,exports){
+"use strict";
+
+var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+// The `getLineInfo` function is mostly useful when the
+// `locations` option is off (for performance reasons) and you
+// want to find the line/column position for a given character
+// offset. `input` should be the code string that the offset refers
+// into.
+
+exports.getLineInfo = getLineInfo;
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var Parser = require("./state").Parser;
+
+var lineBreakG = require("./whitespace").lineBreakG;
+
+// These are used when `options.locations` is on, for the
+// `startLoc` and `endLoc` properties.
+
+var Position = exports.Position = (function () {
+ function Position(line, col) {
+ _classCallCheck(this, Position);
+
+ this.line = line;
+ this.column = col;
+ }
+
+ _createClass(Position, {
+ offset: {
+ value: function offset(n) {
+ return new Position(this.line, this.column + n);
+ }
+ }
+ });
+
+ return Position;
+})();
+
+var SourceLocation = exports.SourceLocation = function SourceLocation(p, start, end) {
+ _classCallCheck(this, SourceLocation);
+
+ this.start = start;
+ this.end = end;
+ if (p.sourceFile !== null) this.source = p.sourceFile;
+};
+
+function getLineInfo(input, offset) {
+ for (var line = 1, cur = 0;;) {
+ lineBreakG.lastIndex = cur;
+ var match = lineBreakG.exec(input);
+ if (match && match.index < offset) {
+ ++line;
+ cur = match.index + match[0].length;
+ } else {
+ return new Position(line, offset - cur);
+ }
+ }
+}
+
+var pp = Parser.prototype;
+
+// This function is used to raise exceptions on parse errors. It
+// takes an offset integer (into the current `input`) to indicate
+// the location of the error, attaches the position to the end
+// of the error message, and then raises a `SyntaxError` with that
+// message.
+
+pp.raise = function (pos, message) {
+ var loc = getLineInfo(this.input, pos);
+ message += " (" + loc.line + ":" + loc.column + ")";
+ var err = new SyntaxError(message);
+ err.pos = pos;err.loc = loc;err.raisedAt = this.pos;
+ throw err;
+};
+
+pp.curPosition = function () {
+ return new Position(this.curLine, this.pos - this.lineStart);
+};
+
+pp.markPosition = function () {
+ return this.options.locations ? [this.start, this.startLoc] : this.start;
+};
+
+},{"./state":9,"./whitespace":15}],5:[function(require,module,exports){
+"use strict";
+
+var tt = require("./tokentype").types;
+
+var Parser = require("./state").Parser;
+
+var reservedWords = require("./identifier").reservedWords;
+
+var has = require("./util").has;
+
+var pp = Parser.prototype;
+
+// Convert existing expression atom to assignable pattern
+// if possible.
+
+pp.toAssignable = function (node, isBinding) {
+ if (this.options.ecmaVersion >= 6 && node) {
+ switch (node.type) {
+ case "Identifier":
+ case "ObjectPattern":
+ case "ArrayPattern":
+ case "AssignmentPattern":
+ break;
+
+ case "ObjectExpression":
+ node.type = "ObjectPattern";
+ for (var i = 0; i < node.properties.length; i++) {
+ var prop = node.properties[i];
+ if (prop.kind !== "init") this.raise(prop.key.start, "Object pattern can't contain getter or setter");
+ this.toAssignable(prop.value, isBinding);
+ }
+ break;
+
+ case "ArrayExpression":
+ node.type = "ArrayPattern";
+ this.toAssignableList(node.elements, isBinding);
+ break;
+
+ case "AssignmentExpression":
+ if (node.operator === "=") {
+ node.type = "AssignmentPattern";
+ } else {
+ this.raise(node.left.end, "Only '=' operator can be used for specifying default value.");
+ }
+ break;
+
+ case "MemberExpression":
+ if (!isBinding) break;
+
+ default:
+ this.raise(node.start, "Assigning to rvalue");
+ }
+ }
+ return node;
+};
+
+// Convert list of expression atoms to binding list.
+
+pp.toAssignableList = function (exprList, isBinding) {
+ var end = exprList.length;
+ if (end) {
+ var last = exprList[end - 1];
+ if (last && last.type == "RestElement") {
+ --end;
+ } else if (last && last.type == "SpreadElement") {
+ last.type = "RestElement";
+ var arg = last.argument;
+ this.toAssignable(arg, isBinding);
+ if (arg.type !== "Identifier" && arg.type !== "MemberExpression" && arg.type !== "ArrayPattern") this.unexpected(arg.start);
+ --end;
+ }
+ }
+ for (var i = 0; i < end; i++) {
+ var elt = exprList[i];
+ if (elt) this.toAssignable(elt, isBinding);
+ }
+ return exprList;
+};
+
+// Parses spread element.
+
+pp.parseSpread = function (refShorthandDefaultPos) {
+ var node = this.startNode();
+ this.next();
+ node.argument = this.parseMaybeAssign(refShorthandDefaultPos);
+ return this.finishNode(node, "SpreadElement");
+};
+
+pp.parseRest = function () {
+ var node = this.startNode();
+ this.next();
+ node.argument = this.type === tt.name || this.type === tt.bracketL ? this.parseBindingAtom() : this.unexpected();
+ return this.finishNode(node, "RestElement");
+};
+
+// Parses lvalue (assignable) atom.
+
+pp.parseBindingAtom = function () {
+ if (this.options.ecmaVersion < 6) return this.parseIdent();
+ switch (this.type) {
+ case tt.name:
+ return this.parseIdent();
+
+ case tt.bracketL:
+ var node = this.startNode();
+ this.next();
+ node.elements = this.parseBindingList(tt.bracketR, true, true);
+ return this.finishNode(node, "ArrayPattern");
+
+ case tt.braceL:
+ return this.parseObj(true);
+
+ default:
+ this.unexpected();
+ }
+};
+
+pp.parseBindingList = function (close, allowEmpty, allowTrailingComma) {
+ var elts = [],
+ first = true;
+ while (!this.eat(close)) {
+ if (first) first = false;else this.expect(tt.comma);
+ if (allowEmpty && this.type === tt.comma) {
+ elts.push(null);
+ } else if (allowTrailingComma && this.afterTrailingComma(close)) {
+ break;
+ } else if (this.type === tt.ellipsis) {
+ elts.push(this.parseRest());
+ this.expect(close);
+ break;
+ } else {
+ elts.push(this.parseMaybeDefault());
+ }
+ }
+ return elts;
+};
+
+// Parses assignment pattern around given atom if possible.
+
+pp.parseMaybeDefault = function (startPos, left) {
+ startPos = startPos || this.markPosition();
+ left = left || this.parseBindingAtom();
+ if (!this.eat(tt.eq)) return left;
+ var node = this.startNodeAt(startPos);
+ node.operator = "=";
+ node.left = left;
+ node.right = this.parseMaybeAssign();
+ return this.finishNode(node, "AssignmentPattern");
+};
+
+// Verify that a node is an lval — something that can be assigned
+// to.
+
+pp.checkLVal = function (expr, isBinding, checkClashes) {
+ switch (expr.type) {
+ case "Identifier":
+ if (this.strict && (reservedWords.strictBind(expr.name) || reservedWords.strict(expr.name))) this.raise(expr.start, (isBinding ? "Binding " : "Assigning to ") + expr.name + " in strict mode");
+ if (checkClashes) {
+ if (has(checkClashes, expr.name)) this.raise(expr.start, "Argument name clash in strict mode");
+ checkClashes[expr.name] = true;
+ }
+ break;
+
+ case "MemberExpression":
+ if (isBinding) this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " member expression");
+ break;
+
+ case "ObjectPattern":
+ for (var i = 0; i < expr.properties.length; i++) {
+ this.checkLVal(expr.properties[i].value, isBinding, checkClashes);
+ }break;
+
+ case "ArrayPattern":
+ for (var i = 0; i < expr.elements.length; i++) {
+ var elem = expr.elements[i];
+ if (elem) this.checkLVal(elem, isBinding, checkClashes);
+ }
+ break;
+
+ case "AssignmentPattern":
+ this.checkLVal(expr.left, isBinding, checkClashes);
+ break;
+
+ case "RestElement":
+ this.checkLVal(expr.argument, isBinding, checkClashes);
+ break;
+
+ default:
+ this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " rvalue");
+ }
+};
+
+},{"./identifier":3,"./state":9,"./tokentype":13,"./util":14}],6:[function(require,module,exports){
+"use strict";
+
+var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var Parser = require("./state").Parser;
+
+var SourceLocation = require("./location").SourceLocation;
+
+// Start an AST node, attaching a start offset.
+
+var pp = Parser.prototype;
+
+var Node = exports.Node = function Node() {
+ _classCallCheck(this, Node);
+};
+
+pp.startNode = function () {
+ var node = new Node();
+ node.start = this.start;
+ if (this.options.locations) node.loc = new SourceLocation(this, this.startLoc);
+ if (this.options.directSourceFile) node.sourceFile = this.options.directSourceFile;
+ if (this.options.ranges) node.range = [this.start, 0];
+ return node;
+};
+
+pp.startNodeAt = function (pos) {
+ var node = new Node(),
+ start = pos;
+ if (this.options.locations) {
+ node.loc = new SourceLocation(this, start[1]);
+ start = pos[0];
+ }
+ node.start = start;
+ if (this.options.directSourceFile) node.sourceFile = this.options.directSourceFile;
+ if (this.options.ranges) node.range = [start, 0];
+ return node;
+};
+
+// Finish an AST node, adding `type` and `end` properties.
+
+pp.finishNode = function (node, type) {
+ node.type = type;
+ node.end = this.lastTokEnd;
+ if (this.options.locations) node.loc.end = this.lastTokEndLoc;
+ if (this.options.ranges) node.range[1] = this.lastTokEnd;
+ return node;
+};
+
+// Finish node at given position
+
+pp.finishNodeAt = function (node, type, pos) {
+ if (this.options.locations) {
+ node.loc.end = pos[1];pos = pos[0];
+ }
+ node.type = type;
+ node.end = pos;
+ if (this.options.ranges) node.range[1] = pos;
+ return node;
+};
+
+},{"./location":4,"./state":9}],7:[function(require,module,exports){
+
+
+// Interpret and default an options object
+
+"use strict";
+
+exports.getOptions = getOptions;
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _util = require("./util");
+
+var has = _util.has;
+var isArray = _util.isArray;
+
+var SourceLocation = require("./location").SourceLocation;
+
+// A second optional argument can be given to further configure
+// the parser process. These options are recognized:
+
+var defaultOptions = {
+ // `ecmaVersion` indicates the ECMAScript version to parse. Must
+ // be either 3, or 5, or 6. This influences support for strict
+ // mode, the set of reserved words, support for getters and
+ // setters and other features.
+ ecmaVersion: 5,
+ // Source type ("script" or "module") for different semantics
+ sourceType: "script",
+ // `onInsertedSemicolon` can be a callback that will be called
+ // when a semicolon is automatically inserted. It will be passed
+ // th position of the comma as an offset, and if `locations` is
+ // enabled, it is given the location as a `{line, column}` object
+ // as second argument.
+ onInsertedSemicolon: null,
+ // `onTrailingComma` is similar to `onInsertedSemicolon`, but for
+ // trailing commas.
+ onTrailingComma: null,
+ // By default, reserved words are not enforced. Disable
+ // `allowReserved` to enforce them. When this option has the
+ // value "never", reserved words and keywords can also not be
+ // used as property names.
+ allowReserved: true,
+ // When enabled, a return at the top level is not considered an
+ // error.
+ allowReturnOutsideFunction: false,
+ // When enabled, import/export statements are not constrained to
+ // appearing at the top of the program.
+ allowImportExportEverywhere: false,
+ // When enabled, hashbang directive in the beginning of file
+ // is allowed and treated as a line comment.
+ allowHashBang: false,
+ // When `locations` is on, `loc` properties holding objects with
+ // `start` and `end` properties in `{line, column}` form (with
+ // line being 1-based and column 0-based) will be attached to the
+ // nodes.
+ locations: false,
+ // A function can be passed as `onToken` option, which will
+ // cause Acorn to call that function with object in the same
+ // format as tokenize() returns. Note that you are not
+ // allowed to call the parser from the callback—that will
+ // corrupt its internal state.
+ onToken: null,
+ // A function can be passed as `onComment` option, which will
+ // cause Acorn to call that function with `(block, text, start,
+ // end)` parameters whenever a comment is skipped. `block` is a
+ // boolean indicating whether this is a block (`/* */`) comment,
+ // `text` is the content of the comment, and `start` and `end` are
+ // character offsets that denote the start and end of the comment.
+ // When the `locations` option is on, two more parameters are
+ // passed, the full `{line, column}` locations of the start and
+ // end of the comments. Note that you are not allowed to call the
+ // parser from the callback—that will corrupt its internal state.
+ onComment: null,
+ // Nodes have their start and end characters offsets recorded in
+ // `start` and `end` properties (directly on the node, rather than
+ // the `loc` object, which holds line/column data. To also add a
+ // [semi-standardized][range] `range` property holding a `[start,
+ // end]` array with the same numbers, set the `ranges` option to
+ // `true`.
+ //
+ // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678
+ ranges: false,
+ // It is possible to parse multiple files into a single AST by
+ // passing the tree produced by parsing the first file as
+ // `program` option in subsequent parses. This will add the
+ // toplevel forms of the parsed file to the `Program` (top) node
+ // of an existing parse tree.
+ program: null,
+ // When `locations` is on, you can pass this to record the source
+ // file in every node's `loc` object.
+ sourceFile: null,
+ // This value, if given, is stored in every node, whether
+ // `locations` is on or off.
+ directSourceFile: null,
+ // When enabled, parenthesized expressions are represented by
+ // (non-standard) ParenthesizedExpression nodes
+ preserveParens: false,
+ plugins: {}
+};exports.defaultOptions = defaultOptions;
+
+function getOptions(opts) {
+ var options = {};
+ for (var opt in defaultOptions) {
+ options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt];
+ }if (isArray(options.onToken)) {
+ (function () {
+ var tokens = options.onToken;
+ options.onToken = function (token) {
+ return tokens.push(token);
+ };
+ })();
+ }
+ if (isArray(options.onComment)) options.onComment = pushComment(options, options.onComment);
+
+ return options;
+}
+
+function pushComment(options, array) {
+ return function (block, text, start, end, startLoc, endLoc) {
+ var comment = {
+ type: block ? "Block" : "Line",
+ value: text,
+ start: start,
+ end: end
+ };
+ if (options.locations) comment.loc = new SourceLocation(this, startLoc, endLoc);
+ if (options.ranges) comment.range = [start, end];
+ array.push(comment);
+ };
+}
+
+},{"./location":4,"./util":14}],8:[function(require,module,exports){
+"use strict";
+
+var tt = require("./tokentype").types;
+
+var Parser = require("./state").Parser;
+
+var lineBreak = require("./whitespace").lineBreak;
+
+var pp = Parser.prototype;
+
+// ## Parser utilities
+
+// Test whether a statement node is the string literal `"use strict"`.
+
+pp.isUseStrict = function (stmt) {
+ return this.options.ecmaVersion >= 5 && stmt.type === "ExpressionStatement" && stmt.expression.type === "Literal" && stmt.expression.value === "use strict";
+};
+
+// Predicate that tests whether the next token is of the given
+// type, and if yes, consumes it as a side effect.
+
+pp.eat = function (type) {
+ if (this.type === type) {
+ this.next();
+ return true;
+ } else {
+ return false;
+ }
+};
+
+// Tests whether parsed token is a contextual keyword.
+
+pp.isContextual = function (name) {
+ return this.type === tt.name && this.value === name;
+};
+
+// Consumes contextual keyword if possible.
+
+pp.eatContextual = function (name) {
+ return this.value === name && this.eat(tt.name);
+};
+
+// Asserts that following token is given contextual keyword.
+
+pp.expectContextual = function (name) {
+ if (!this.eatContextual(name)) this.unexpected();
+};
+
+// Test whether a semicolon can be inserted at the current position.
+
+pp.canInsertSemicolon = function () {
+ return this.type === tt.eof || this.type === tt.braceR || lineBreak.test(this.input.slice(this.lastTokEnd, this.start));
+};
+
+pp.insertSemicolon = function () {
+ if (this.canInsertSemicolon()) {
+ if (this.options.onInsertedSemicolon) this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc);
+ return true;
+ }
+};
+
+// Consume a semicolon, or, failing that, see if we are allowed to
+// pretend that there is a semicolon at this position.
+
+pp.semicolon = function () {
+ if (!this.eat(tt.semi) && !this.insertSemicolon()) this.unexpected();
+};
+
+pp.afterTrailingComma = function (tokType) {
+ if (this.type == tokType) {
+ if (this.options.onTrailingComma) this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc);
+ this.next();
+ return true;
+ }
+};
+
+// Expect a token of a given type. If found, consume it, otherwise,
+// raise an unexpected token error.
+
+pp.expect = function (type) {
+ this.eat(type) || this.unexpected();
+};
+
+// Raise an unexpected token error.
+
+pp.unexpected = function (pos) {
+ this.raise(pos != null ? pos : this.start, "Unexpected token");
+};
+
+},{"./state":9,"./tokentype":13,"./whitespace":15}],9:[function(require,module,exports){
+"use strict";
+
+exports.Parser = Parser;
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _identifier = require("./identifier");
+
+var reservedWords = _identifier.reservedWords;
+var keywords = _identifier.keywords;
+
+var _tokentype = require("./tokentype");
+
+var tt = _tokentype.types;
+var lineBreak = _tokentype.lineBreak;
+
+function Parser(options, input, startPos) {
+ this.options = options;
+ this.loadPlugins(this.options.plugins);
+ this.sourceFile = this.options.sourceFile || null;
+ this.isKeyword = keywords[this.options.ecmaVersion >= 6 ? 6 : 5];
+ this.isReservedWord = reservedWords[this.options.ecmaVersion];
+ this.input = input;
+
+ // Set up token state
+
+ // The current position of the tokenizer in the input.
+ if (startPos) {
+ this.pos = startPos;
+ this.lineStart = Math.max(0, this.input.lastIndexOf("\n", startPos));
+ this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length;
+ } else {
+ this.pos = this.lineStart = 0;
+ this.curLine = 1;
+ }
+
+ // Properties of the current token:
+ // Its type
+ this.type = tt.eof;
+ // For tokens that include more information than their type, the value
+ this.value = null;
+ // Its start and end offset
+ this.start = this.end = this.pos;
+ // And, if locations are used, the {line, column} object
+ // corresponding to those offsets
+ this.startLoc = this.endLoc = null;
+
+ // Position information for the previous token
+ this.lastTokEndLoc = this.lastTokStartLoc = null;
+ this.lastTokStart = this.lastTokEnd = this.pos;
+
+ // The context stack is used to superficially track syntactic
+ // context to predict whether a regular expression is allowed in a
+ // given position.
+ this.context = this.initialContext();
+ this.exprAllowed = true;
+
+ // Figure out if it's a module code.
+ this.strict = this.inModule = this.options.sourceType === "module";
+
+ // Flags to track whether we are in a function, a generator.
+ this.inFunction = this.inGenerator = false;
+ // Labels in scope.
+ this.labels = [];
+
+ // If enabled, skip leading hashbang line.
+ if (this.pos === 0 && this.options.allowHashBang && this.input.slice(0, 2) === "#!") this.skipLineComment(2);
+}
+
+Parser.prototype.extend = function (name, f) {
+ this[name] = f(this[name]);
+};
+
+// Registered plugins
+
+var plugins = {};
+
+exports.plugins = plugins;
+Parser.prototype.loadPlugins = function (plugins) {
+ for (var _name in plugins) {
+ var plugin = exports.plugins[_name];
+ if (!plugin) throw new Error("Plugin '" + _name + "' not found");
+ plugin(this, plugins[_name]);
+ }
+};
+
+},{"./identifier":3,"./tokentype":13}],10:[function(require,module,exports){
+"use strict";
+
+var tt = require("./tokentype").types;
+
+var Parser = require("./state").Parser;
+
+var lineBreak = require("./whitespace").lineBreak;
+
+var pp = Parser.prototype;
+
+// ### Statement parsing
+
+// Parse a program. Initializes the parser, reads any number of
+// statements, and wraps them in a Program node. Optionally takes a
+// `program` argument. If present, the statements will be appended
+// to its body instead of creating a new node.
+
+pp.parseTopLevel = function (node) {
+ var first = true;
+ if (!node.body) node.body = [];
+ while (this.type !== tt.eof) {
+ var stmt = this.parseStatement(true, true);
+ node.body.push(stmt);
+ if (first && this.isUseStrict(stmt)) this.setStrict(true);
+ first = false;
+ }
+ this.next();
+ if (this.options.ecmaVersion >= 6) {
+ node.sourceType = this.options.sourceType;
+ }
+ return this.finishNode(node, "Program");
+};
+
+var loopLabel = { kind: "loop" },
+ switchLabel = { kind: "switch" };
+
+// Parse a single statement.
+//
+// If expecting a statement and finding a slash operator, parse a
+// regular expression literal. This is to handle cases like
+// `if (foo) /blah/.exec(foo)`, where looking at the previous token
+// does not help.
+
+pp.parseStatement = function (declaration, topLevel) {
+ var starttype = this.type,
+ node = this.startNode();
+
+ // Most types of statements are recognized by the keyword they
+ // start with. Many are trivial to parse, some require a bit of
+ // complexity.
+
+ switch (starttype) {
+ case tt._break:case tt._continue:
+ return this.parseBreakContinueStatement(node, starttype.keyword);
+ case tt._debugger:
+ return this.parseDebuggerStatement(node);
+ case tt._do:
+ return this.parseDoStatement(node);
+ case tt._for:
+ return this.parseForStatement(node);
+ case tt._function:
+ if (!declaration && this.options.ecmaVersion >= 6) this.unexpected();
+ return this.parseFunctionStatement(node);
+ case tt._class:
+ if (!declaration) this.unexpected();
+ return this.parseClass(node, true);
+ case tt._if:
+ return this.parseIfStatement(node);
+ case tt._return:
+ return this.parseReturnStatement(node);
+ case tt._switch:
+ return this.parseSwitchStatement(node);
+ case tt._throw:
+ return this.parseThrowStatement(node);
+ case tt._try:
+ return this.parseTryStatement(node);
+ case tt._let:case tt._const:
+ if (!declaration) this.unexpected(); // NOTE: falls through to _var
+ case tt._var:
+ return this.parseVarStatement(node, starttype);
+ case tt._while:
+ return this.parseWhileStatement(node);
+ case tt._with:
+ return this.parseWithStatement(node);
+ case tt.braceL:
+ return this.parseBlock();
+ case tt.semi:
+ return this.parseEmptyStatement(node);
+ case tt._export:
+ case tt._import:
+ if (!this.options.allowImportExportEverywhere) {
+ if (!topLevel) this.raise(this.start, "'import' and 'export' may only appear at the top level");
+ if (!this.inModule) this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'");
+ }
+ return starttype === tt._import ? this.parseImport(node) : this.parseExport(node);
+
+ // If the statement does not start with a statement keyword or a
+ // brace, it's an ExpressionStatement or LabeledStatement. We
+ // simply start parsing an expression, and afterwards, if the
+ // next token is a colon and the expression was a simple
+ // Identifier node, we switch to interpreting it as a label.
+ default:
+ var maybeName = this.value,
+ expr = this.parseExpression();
+ if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon)) return this.parseLabeledStatement(node, maybeName, expr);else return this.parseExpressionStatement(node, expr);
+ }
+};
+
+pp.parseBreakContinueStatement = function (node, keyword) {
+ var isBreak = keyword == "break";
+ this.next();
+ if (this.eat(tt.semi) || this.insertSemicolon()) node.label = null;else if (this.type !== tt.name) this.unexpected();else {
+ node.label = this.parseIdent();
+ this.semicolon();
+ }
+
+ // Verify that there is an actual destination to break or
+ // continue to.
+ for (var i = 0; i < this.labels.length; ++i) {
+ var lab = this.labels[i];
+ if (node.label == null || lab.name === node.label.name) {
+ if (lab.kind != null && (isBreak || lab.kind === "loop")) break;
+ if (node.label && isBreak) break;
+ }
+ }
+ if (i === this.labels.length) this.raise(node.start, "Unsyntactic " + keyword);
+ return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement");
+};
+
+pp.parseDebuggerStatement = function (node) {
+ this.next();
+ this.semicolon();
+ return this.finishNode(node, "DebuggerStatement");
+};
+
+pp.parseDoStatement = function (node) {
+ this.next();
+ this.labels.push(loopLabel);
+ node.body = this.parseStatement(false);
+ this.labels.pop();
+ this.expect(tt._while);
+ node.test = this.parseParenExpression();
+ if (this.options.ecmaVersion >= 6) this.eat(tt.semi);else this.semicolon();
+ return this.finishNode(node, "DoWhileStatement");
+};
+
+// Disambiguating between a `for` and a `for`/`in` or `for`/`of`
+// loop is non-trivial. Basically, we have to parse the init `var`
+// statement or expression, disallowing the `in` operator (see
+// the second parameter to `parseExpression`), and then check
+// whether the next token is `in` or `of`. When there is no init
+// part (semicolon immediately after the opening parenthesis), it
+// is a regular `for` loop.
+
+pp.parseForStatement = function (node) {
+ this.next();
+ this.labels.push(loopLabel);
+ this.expect(tt.parenL);
+ if (this.type === tt.semi) return this.parseFor(node, null);
+ if (this.type === tt._var || this.type === tt._let || this.type === tt._const) {
+ var _init = this.startNode(),
+ varKind = this.type;
+ this.next();
+ this.parseVar(_init, true, varKind);
+ this.finishNode(_init, "VariableDeclaration");
+ if ((this.type === tt._in || this.options.ecmaVersion >= 6 && this.isContextual("of")) && _init.declarations.length === 1 && !(varKind !== tt._var && _init.declarations[0].init)) return this.parseForIn(node, _init);
+ return this.parseFor(node, _init);
+ }
+ var refShorthandDefaultPos = { start: 0 };
+ var init = this.parseExpression(true, refShorthandDefaultPos);
+ if (this.type === tt._in || this.options.ecmaVersion >= 6 && this.isContextual("of")) {
+ this.toAssignable(init);
+ this.checkLVal(init);
+ return this.parseForIn(node, init);
+ } else if (refShorthandDefaultPos.start) {
+ this.unexpected(refShorthandDefaultPos.start);
+ }
+ return this.parseFor(node, init);
+};
+
+pp.parseFunctionStatement = function (node) {
+ this.next();
+ return this.parseFunction(node, true);
+};
+
+pp.parseIfStatement = function (node) {
+ this.next();
+ node.test = this.parseParenExpression();
+ node.consequent = this.parseStatement(false);
+ node.alternate = this.eat(tt._else) ? this.parseStatement(false) : null;
+ return this.finishNode(node, "IfStatement");
+};
+
+pp.parseReturnStatement = function (node) {
+ if (!this.inFunction && !this.options.allowReturnOutsideFunction) this.raise(this.start, "'return' outside of function");
+ this.next();
+
+ // In `return` (and `break`/`continue`), the keywords with
+ // optional arguments, we eagerly look for a semicolon or the
+ // possibility to insert one.
+
+ if (this.eat(tt.semi) || this.insertSemicolon()) node.argument = null;else {
+ node.argument = this.parseExpression();this.semicolon();
+ }
+ return this.finishNode(node, "ReturnStatement");
+};
+
+pp.parseSwitchStatement = function (node) {
+ this.next();
+ node.discriminant = this.parseParenExpression();
+ node.cases = [];
+ this.expect(tt.braceL);
+ this.labels.push(switchLabel);
+
+ // Statements under must be grouped (by label) in SwitchCase
+ // nodes. `cur` is used to keep the node that we are currently
+ // adding statements to.
+
+ for (var cur, sawDefault; this.type != tt.braceR;) {
+ if (this.type === tt._case || this.type === tt._default) {
+ var isCase = this.type === tt._case;
+ if (cur) this.finishNode(cur, "SwitchCase");
+ node.cases.push(cur = this.startNode());
+ cur.consequent = [];
+ this.next();
+ if (isCase) {
+ cur.test = this.parseExpression();
+ } else {
+ if (sawDefault) this.raise(this.lastTokStart, "Multiple default clauses");
+ sawDefault = true;
+ cur.test = null;
+ }
+ this.expect(tt.colon);
+ } else {
+ if (!cur) this.unexpected();
+ cur.consequent.push(this.parseStatement(true));
+ }
+ }
+ if (cur) this.finishNode(cur, "SwitchCase");
+ this.next(); // Closing brace
+ this.labels.pop();
+ return this.finishNode(node, "SwitchStatement");
+};
+
+pp.parseThrowStatement = function (node) {
+ this.next();
+ if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) this.raise(this.lastTokEnd, "Illegal newline after throw");
+ node.argument = this.parseExpression();
+ this.semicolon();
+ return this.finishNode(node, "ThrowStatement");
+};
+
+// Reused empty array added for node fields that are always empty.
+
+var empty = [];
+
+pp.parseTryStatement = function (node) {
+ this.next();
+ node.block = this.parseBlock();
+ node.handler = null;
+ if (this.type === tt._catch) {
+ var clause = this.startNode();
+ this.next();
+ this.expect(tt.parenL);
+ clause.param = this.parseBindingAtom();
+ this.checkLVal(clause.param, true);
+ this.expect(tt.parenR);
+ clause.guard = null;
+ clause.body = this.parseBlock();
+ node.handler = this.finishNode(clause, "CatchClause");
+ }
+ node.guardedHandlers = empty;
+ node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
+ if (!node.handler && !node.finalizer) this.raise(node.start, "Missing catch or finally clause");
+ return this.finishNode(node, "TryStatement");
+};
+
+pp.parseVarStatement = function (node, kind) {
+ this.next();
+ this.parseVar(node, false, kind);
+ this.semicolon();
+ return this.finishNode(node, "VariableDeclaration");
+};
+
+pp.parseWhileStatement = function (node) {
+ this.next();
+ node.test = this.parseParenExpression();
+ this.labels.push(loopLabel);
+ node.body = this.parseStatement(false);
+ this.labels.pop();
+ return this.finishNode(node, "WhileStatement");
+};
+
+pp.parseWithStatement = function (node) {
+ if (this.strict) this.raise(this.start, "'with' in strict mode");
+ this.next();
+ node.object = this.parseParenExpression();
+ node.body = this.parseStatement(false);
+ return this.finishNode(node, "WithStatement");
+};
+
+pp.parseEmptyStatement = function (node) {
+ this.next();
+ return this.finishNode(node, "EmptyStatement");
+};
+
+pp.parseLabeledStatement = function (node, maybeName, expr) {
+ for (var i = 0; i < this.labels.length; ++i) {
+ if (this.labels[i].name === maybeName) this.raise(expr.start, "Label '" + maybeName + "' is already declared");
+ }var kind = this.type.isLoop ? "loop" : this.type === tt._switch ? "switch" : null;
+ this.labels.push({ name: maybeName, kind: kind });
+ node.body = this.parseStatement(true);
+ this.labels.pop();
+ node.label = expr;
+ return this.finishNode(node, "LabeledStatement");
+};
+
+pp.parseExpressionStatement = function (node, expr) {
+ node.expression = expr;
+ this.semicolon();
+ return this.finishNode(node, "ExpressionStatement");
+};
+
+// Parse a semicolon-enclosed block of statements, handling `"use
+// strict"` declarations when `allowStrict` is true (used for
+// function bodies).
+
+pp.parseBlock = function (allowStrict) {
+ var node = this.startNode(),
+ first = true,
+ oldStrict = undefined;
+ node.body = [];
+ this.expect(tt.braceL);
+ while (!this.eat(tt.braceR)) {
+ var stmt = this.parseStatement(true);
+ node.body.push(stmt);
+ if (first && allowStrict && this.isUseStrict(stmt)) {
+ oldStrict = this.strict;
+ this.setStrict(this.strict = true);
+ }
+ first = false;
+ }
+ if (oldStrict === false) this.setStrict(false);
+ return this.finishNode(node, "BlockStatement");
+};
+
+// Parse a regular `for` loop. The disambiguation code in
+// `parseStatement` will already have parsed the init statement or
+// expression.
+
+pp.parseFor = function (node, init) {
+ node.init = init;
+ this.expect(tt.semi);
+ node.test = this.type === tt.semi ? null : this.parseExpression();
+ this.expect(tt.semi);
+ node.update = this.type === tt.parenR ? null : this.parseExpression();
+ this.expect(tt.parenR);
+ node.body = this.parseStatement(false);
+ this.labels.pop();
+ return this.finishNode(node, "ForStatement");
+};
+
+// Parse a `for`/`in` and `for`/`of` loop, which are almost
+// same from parser's perspective.
+
+pp.parseForIn = function (node, init) {
+ var type = this.type === tt._in ? "ForInStatement" : "ForOfStatement";
+ this.next();
+ node.left = init;
+ node.right = this.parseExpression();
+ this.expect(tt.parenR);
+ node.body = this.parseStatement(false);
+ this.labels.pop();
+ return this.finishNode(node, type);
+};
+
+// Parse a list of variable declarations.
+
+pp.parseVar = function (node, isFor, kind) {
+ node.declarations = [];
+ node.kind = kind.keyword;
+ for (;;) {
+ var decl = this.startNode();
+ decl.id = this.parseBindingAtom();
+ this.checkLVal(decl.id, true);
+ if (this.eat(tt.eq)) {
+ decl.init = this.parseMaybeAssign(isFor);
+ } else if (kind === tt._const && !(this.type === tt._in || this.options.ecmaVersion >= 6 && this.isContextual("of"))) {
+ this.unexpected();
+ } else if (decl.id.type != "Identifier" && !(isFor && (this.type === tt._in || this.isContextual("of")))) {
+ this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value");
+ } else {
+ decl.init = null;
+ }
+ node.declarations.push(this.finishNode(decl, "VariableDeclarator"));
+ if (!this.eat(tt.comma)) break;
+ }
+ return node;
+};
+
+// Parse a function declaration or literal (depending on the
+// `isStatement` parameter).
+
+pp.parseFunction = function (node, isStatement, allowExpressionBody) {
+ this.initFunction(node);
+ if (this.options.ecmaVersion >= 6) node.generator = this.eat(tt.star);
+ if (isStatement || this.type === tt.name) node.id = this.parseIdent();
+ this.expect(tt.parenL);
+ node.params = this.parseBindingList(tt.parenR, false, false);
+ this.parseFunctionBody(node, allowExpressionBody);
+ return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression");
+};
+
+// Parse a class declaration or literal (depending on the
+// `isStatement` parameter).
+
+pp.parseClass = function (node, isStatement) {
+ this.next();
+ node.id = this.type === tt.name ? this.parseIdent() : isStatement ? this.unexpected() : null;
+ node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null;
+ var classBody = this.startNode();
+ classBody.body = [];
+ this.expect(tt.braceL);
+ while (!this.eat(tt.braceR)) {
+ if (this.eat(tt.semi)) continue;
+ var method = this.startNode();
+ var isGenerator = this.eat(tt.star);
+ this.parsePropertyName(method);
+ if (this.type !== tt.parenL && !method.computed && method.key.type === "Identifier" && method.key.name === "static") {
+ if (isGenerator) this.unexpected();
+ method["static"] = true;
+ isGenerator = this.eat(tt.star);
+ this.parsePropertyName(method);
+ } else {
+ method["static"] = false;
+ }
+ method.kind = "method";
+ if (!method.computed && !isGenerator) {
+ if (method.key.type === "Identifier") {
+ if (this.type !== tt.parenL && (method.key.name === "get" || method.key.name === "set")) {
+ method.kind = method.key.name;
+ this.parsePropertyName(method);
+ } else if (!method["static"] && method.key.name === "constructor") {
+ method.kind = "constructor";
+ }
+ } else if (!method["static"] && method.key.type === "Literal" && method.key.value === "constructor") {
+ method.kind = "constructor";
+ }
+ }
+ method.value = this.parseMethod(isGenerator);
+ classBody.body.push(this.finishNode(method, "MethodDefinition"));
+ }
+ node.body = this.finishNode(classBody, "ClassBody");
+ return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression");
+};
+
+// Parses module export declaration.
+
+pp.parseExport = function (node) {
+ this.next();
+ // export * from '...'
+ if (this.eat(tt.star)) {
+ this.expectContextual("from");
+ node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected();
+ this.semicolon();
+ return this.finishNode(node, "ExportAllDeclaration");
+ }
+ if (this.eat(tt._default)) {
+ // export default ...
+ var expr = this.parseMaybeAssign();
+ var needsSemi = true;
+ if (expr.type == "FunctionExpression" || expr.type == "ClassExpression") {
+ needsSemi = false;
+ if (expr.id) {
+ expr.type = expr.type == "FunctionExpression" ? "FunctionDeclaration" : "ClassDeclaration";
+ }
+ }
+ node.declaration = expr;
+ if (needsSemi) this.semicolon();
+ return this.finishNode(node, "ExportDefaultDeclaration");
+ }
+ // export var|const|let|function|class ...
+ if (this.type.keyword) {
+ node.declaration = this.parseStatement(true);
+ node.specifiers = [];
+ node.source = null;
+ } else {
+ // export { x, y as z } [from '...']
+ node.declaration = null;
+ node.specifiers = this.parseExportSpecifiers();
+ if (this.eatContextual("from")) {
+ node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected();
+ } else {
+ node.source = null;
+ }
+ this.semicolon();
+ }
+ return this.finishNode(node, "ExportNamedDeclaration");
+};
+
+// Parses a comma-separated list of module exports.
+
+pp.parseExportSpecifiers = function () {
+ var nodes = [],
+ first = true;
+ // export { x, y as z } [from '...']
+ this.expect(tt.braceL);
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma);
+ if (this.afterTrailingComma(tt.braceR)) break;
+ } else first = false;
+
+ var node = this.startNode();
+ node.local = this.parseIdent(this.type === tt._default);
+ node.exported = this.eatContextual("as") ? this.parseIdent(true) : node.local;
+ nodes.push(this.finishNode(node, "ExportSpecifier"));
+ }
+ return nodes;
+};
+
+// Parses import declaration.
+
+pp.parseImport = function (node) {
+ this.next();
+ // import '...'
+ if (this.type === tt.string) {
+ node.specifiers = empty;
+ node.source = this.parseExprAtom();
+ node.kind = "";
+ } else {
+ node.specifiers = this.parseImportSpecifiers();
+ this.expectContextual("from");
+ node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected();
+ }
+ this.semicolon();
+ return this.finishNode(node, "ImportDeclaration");
+};
+
+// Parses a comma-separated list of module imports.
+
+pp.parseImportSpecifiers = function () {
+ var nodes = [],
+ first = true;
+ if (this.type === tt.name) {
+ // import defaultObj, { x, y as z } from '...'
+ var node = this.startNode();
+ node.local = this.parseIdent();
+ this.checkLVal(node.local, true);
+ nodes.push(this.finishNode(node, "ImportDefaultSpecifier"));
+ if (!this.eat(tt.comma)) return nodes;
+ }
+ if (this.type === tt.star) {
+ var node = this.startNode();
+ this.next();
+ this.expectContextual("as");
+ node.local = this.parseIdent();
+ this.checkLVal(node.local, true);
+ nodes.push(this.finishNode(node, "ImportNamespaceSpecifier"));
+ return nodes;
+ }
+ this.expect(tt.braceL);
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma);
+ if (this.afterTrailingComma(tt.braceR)) break;
+ } else first = false;
+
+ var node = this.startNode();
+ node.imported = this.parseIdent(true);
+ node.local = this.eatContextual("as") ? this.parseIdent() : node.imported;
+ this.checkLVal(node.local, true);
+ nodes.push(this.finishNode(node, "ImportSpecifier"));
+ }
+ return nodes;
+};
+
+},{"./state":9,"./tokentype":13,"./whitespace":15}],11:[function(require,module,exports){
+"use strict";
+
+var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+// The algorithm used to determine whether a regexp can appear at a
+// given point in the program is loosely based on sweet.js' approach.
+// See https://github.com/mozilla/sweet.js/wiki/design
+
+var Parser = require("./state").Parser;
+
+var tt = require("./tokentype").types;
+
+var lineBreak = require("./whitespace").lineBreak;
+
+var TokContext = exports.TokContext = function TokContext(token, isExpr, preserveSpace, override) {
+ _classCallCheck(this, TokContext);
+
+ this.token = token;
+ this.isExpr = isExpr;
+ this.preserveSpace = preserveSpace;
+ this.override = override;
+};
+
+var types = {
+ b_stat: new TokContext("{", false),
+ b_expr: new TokContext("{", true),
+ b_tmpl: new TokContext("${", true),
+ p_stat: new TokContext("(", false),
+ p_expr: new TokContext("(", true),
+ q_tmpl: new TokContext("`", true, true, function (p) {
+ return p.readTmplToken();
+ }),
+ f_expr: new TokContext("function", true)
+};
+
+exports.types = types;
+var pp = Parser.prototype;
+
+pp.initialContext = function () {
+ return [types.b_stat];
+};
+
+pp.braceIsBlock = function (prevType) {
+ var parent = undefined;
+ if (prevType === tt.colon && (parent = this.curContext()).token == "{") return !parent.isExpr;
+ if (prevType === tt._return) return lineBreak.test(this.input.slice(this.lastTokEnd, this.start));
+ if (prevType === tt._else || prevType === tt.semi || prevType === tt.eof) return true;
+ if (prevType == tt.braceL) return this.curContext() === types.b_stat;
+ return !this.exprAllowed;
+};
+
+pp.updateContext = function (prevType) {
+ var update = undefined,
+ type = this.type;
+ if (type.keyword && prevType == tt.dot) this.exprAllowed = false;else if (update = type.updateContext) update.call(this, prevType);else this.exprAllowed = type.beforeExpr;
+};
+
+// Token-specific context update code
+
+tt.parenR.updateContext = tt.braceR.updateContext = function () {
+ if (this.context.length == 1) {
+ this.exprAllowed = true;
+ return;
+ }
+ var out = this.context.pop();
+ if (out === types.b_stat && this.curContext() === types.f_expr) {
+ this.context.pop();
+ this.exprAllowed = false;
+ } else if (out === types.b_tmpl) {
+ this.exprAllowed = true;
+ } else {
+ this.exprAllowed = !out.isExpr;
+ }
+};
+
+tt.braceL.updateContext = function (prevType) {
+ this.context.push(this.braceIsBlock(prevType) ? types.b_stat : types.b_expr);
+ this.exprAllowed = true;
+};
+
+tt.dollarBraceL.updateContext = function () {
+ this.context.push(types.b_tmpl);
+ this.exprAllowed = true;
+};
+
+tt.parenL.updateContext = function (prevType) {
+ var statementParens = prevType === tt._if || prevType === tt._for || prevType === tt._with || prevType === tt._while;
+ this.context.push(statementParens ? types.p_stat : types.p_expr);
+ this.exprAllowed = true;
+};
+
+tt.incDec.updateContext = function () {};
+
+tt._function.updateContext = function () {
+ if (this.curContext() !== types.b_stat) this.context.push(types.f_expr);
+ this.exprAllowed = false;
+};
+
+tt.backQuote.updateContext = function () {
+ if (this.curContext() === types.q_tmpl) this.context.pop();else this.context.push(types.q_tmpl);
+ this.exprAllowed = false;
+};
+
+// tokExprAllowed stays unchanged
+
+},{"./state":9,"./tokentype":13,"./whitespace":15}],12:[function(require,module,exports){
+"use strict";
+
+var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _identifier = require("./identifier");
+
+var isIdentifierStart = _identifier.isIdentifierStart;
+var isIdentifierChar = _identifier.isIdentifierChar;
+
+var _tokentype = require("./tokentype");
+
+var tt = _tokentype.types;
+var keywordTypes = _tokentype.keywords;
+
+var Parser = require("./state").Parser;
+
+var SourceLocation = require("./location").SourceLocation;
+
+var _whitespace = require("./whitespace");
+
+var lineBreak = _whitespace.lineBreak;
+var lineBreakG = _whitespace.lineBreakG;
+var isNewLine = _whitespace.isNewLine;
+var nonASCIIwhitespace = _whitespace.nonASCIIwhitespace;
+
+// Object type used to represent tokens. Note that normally, tokens
+// simply exist as properties on the parser object. This is only
+// used for the onToken callback and the external tokenizer.
+
+var Token = exports.Token = function Token(p) {
+ _classCallCheck(this, Token);
+
+ this.type = p.type;
+ this.value = p.value;
+ this.start = p.start;
+ this.end = p.end;
+ if (p.options.locations) this.loc = new SourceLocation(p, p.startLoc, p.endLoc);
+ if (p.options.ranges) this.range = [p.start, p.end];
+};
+
+// ## Tokenizer
+
+var pp = Parser.prototype;
+
+// Move to the next token
+
+pp.next = function () {
+ if (this.options.onToken) this.options.onToken(new Token(this));
+
+ this.lastTokEnd = this.end;
+ this.lastTokStart = this.start;
+ this.lastTokEndLoc = this.endLoc;
+ this.lastTokStartLoc = this.startLoc;
+ this.nextToken();
+};
+
+pp.getToken = function () {
+ this.next();
+ return new Token(this);
+};
+
+// If we're in an ES6 environment, make parsers iterable
+if (typeof Symbol !== "undefined") pp[Symbol.iterator] = function () {
+ var self = this;
+ return { next: function next() {
+ var token = self.getToken();
+ return {
+ done: token.type === tt.eof,
+ value: token
+ };
+ } };
+};
+
+// Toggle strict mode. Re-reads the next number or string to please
+// pedantic tests (`"use strict"; 010;` should fail).
+
+pp.setStrict = function (strict) {
+ this.strict = strict;
+ if (this.type !== tt.num && this.type !== tt.string) return;
+ this.pos = this.start;
+ if (this.options.locations) {
+ while (this.pos < this.lineStart) {
+ this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
+ --this.curLine;
+ }
+ }
+ this.nextToken();
+};
+
+pp.curContext = function () {
+ return this.context[this.context.length - 1];
+};
+
+// Read a single token, updating the parser object's token-related
+// properties.
+
+pp.nextToken = function () {
+ var curContext = this.curContext();
+ if (!curContext || !curContext.preserveSpace) this.skipSpace();
+
+ this.start = this.pos;
+ if (this.options.locations) this.startLoc = this.curPosition();
+ if (this.pos >= this.input.length) return this.finishToken(tt.eof);
+
+ if (curContext.override) return curContext.override(this);else this.readToken(this.fullCharCodeAtPos());
+};
+
+pp.readToken = function (code) {
+ // Identifier or keyword. '\uXXXX' sequences are allowed in
+ // identifiers, so '\' also dispatches to that.
+ if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */) return this.readWord();
+
+ return this.getTokenFromCode(code);
+};
+
+pp.fullCharCodeAtPos = function () {
+ var code = this.input.charCodeAt(this.pos);
+ if (code <= 55295 || code >= 57344) return code;
+ var next = this.input.charCodeAt(this.pos + 1);
+ return (code << 10) + next - 56613888;
+};
+
+pp.skipBlockComment = function () {
+ var startLoc = this.options.onComment && this.options.locations && this.curPosition();
+ var start = this.pos,
+ end = this.input.indexOf("*/", this.pos += 2);
+ if (end === -1) this.raise(this.pos - 2, "Unterminated comment");
+ this.pos = end + 2;
+ if (this.options.locations) {
+ lineBreakG.lastIndex = start;
+ var match = undefined;
+ while ((match = lineBreakG.exec(this.input)) && match.index < this.pos) {
+ ++this.curLine;
+ this.lineStart = match.index + match[0].length;
+ }
+ }
+ if (this.options.onComment) this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos, startLoc, this.options.locations && this.curPosition());
+};
+
+pp.skipLineComment = function (startSkip) {
+ var start = this.pos;
+ var startLoc = this.options.onComment && this.options.locations && this.curPosition();
+ var ch = this.input.charCodeAt(this.pos += startSkip);
+ while (this.pos < this.input.length && ch !== 10 && ch !== 13 && ch !== 8232 && ch !== 8233) {
+ ++this.pos;
+ ch = this.input.charCodeAt(this.pos);
+ }
+ if (this.options.onComment) this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos, startLoc, this.options.locations && this.curPosition());
+};
+
+// Called at the start of the parse and after every token. Skips
+// whitespace and comments, and.
+
+pp.skipSpace = function () {
+ while (this.pos < this.input.length) {
+ var ch = this.input.charCodeAt(this.pos);
+ if (ch === 32) {
+ // ' '
+ ++this.pos;
+ } else if (ch === 13) {
+ ++this.pos;
+ var next = this.input.charCodeAt(this.pos);
+ if (next === 10) {
+ ++this.pos;
+ }
+ if (this.options.locations) {
+ ++this.curLine;
+ this.lineStart = this.pos;
+ }
+ } else if (ch === 10 || ch === 8232 || ch === 8233) {
+ ++this.pos;
+ if (this.options.locations) {
+ ++this.curLine;
+ this.lineStart = this.pos;
+ }
+ } else if (ch > 8 && ch < 14) {
+ ++this.pos;
+ } else if (ch === 47) {
+ // '/'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next === 42) {
+ // '*'
+ this.skipBlockComment();
+ } else if (next === 47) {
+ // '/'
+ this.skipLineComment(2);
+ } else break;
+ } else if (ch === 160) {
+ // '\xa0'
+ ++this.pos;
+ } else if (ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) {
+ ++this.pos;
+ } else {
+ break;
+ }
+ }
+};
+
+// Called at the end of every token. Sets `end`, `val`, and
+// maintains `context` and `exprAllowed`, and skips the space after
+// the token, so that the next one's `start` will point at the
+// right position.
+
+pp.finishToken = function (type, val) {
+ this.end = this.pos;
+ if (this.options.locations) this.endLoc = this.curPosition();
+ var prevType = this.type;
+ this.type = type;
+ this.value = val;
+
+ this.updateContext(prevType);
+};
+
+// ### Token reading
+
+// This is the function that is called to fetch the next token. It
+// is somewhat obscure, because it works in character codes rather
+// than characters, and because operator parsing has been inlined
+// into it.
+//
+// All in the name of speed.
+//
+pp.readToken_dot = function () {
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next >= 48 && next <= 57) return this.readNumber(true);
+ var next2 = this.input.charCodeAt(this.pos + 2);
+ if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) {
+ // 46 = dot '.'
+ this.pos += 3;
+ return this.finishToken(tt.ellipsis);
+ } else {
+ ++this.pos;
+ return this.finishToken(tt.dot);
+ }
+};
+
+pp.readToken_slash = function () {
+ // '/'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (this.exprAllowed) {
+ ++this.pos;return this.readRegexp();
+ }
+ if (next === 61) return this.finishOp(tt.assign, 2);
+ return this.finishOp(tt.slash, 1);
+};
+
+pp.readToken_mult_modulo = function (code) {
+ // '%*'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next === 61) return this.finishOp(tt.assign, 2);
+ return this.finishOp(code === 42 ? tt.star : tt.modulo, 1);
+};
+
+pp.readToken_pipe_amp = function (code) {
+ // '|&'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next === code) return this.finishOp(code === 124 ? tt.logicalOR : tt.logicalAND, 2);
+ if (next === 61) return this.finishOp(tt.assign, 2);
+ return this.finishOp(code === 124 ? tt.bitwiseOR : tt.bitwiseAND, 1);
+};
+
+pp.readToken_caret = function () {
+ // '^'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next === 61) return this.finishOp(tt.assign, 2);
+ return this.finishOp(tt.bitwiseXOR, 1);
+};
+
+pp.readToken_plus_min = function (code) {
+ // '+-'
+ var next = this.input.charCodeAt(this.pos + 1);
+ if (next === code) {
+ if (next == 45 && this.input.charCodeAt(this.pos + 2) == 62 && lineBreak.test(this.input.slice(this.lastTokEnd, this.pos))) {
+ // A `-->` line comment
+ this.skipLineComment(3);
+ this.skipSpace();
+ return this.nextToken();
+ }
+ return this.finishOp(tt.incDec, 2);
+ }
+ if (next === 61) return this.finishOp(tt.assign, 2);
+ return this.finishOp(tt.plusMin, 1);
+};
+
+pp.readToken_lt_gt = function (code) {
+ // '<>'
+ var next = this.input.charCodeAt(this.pos + 1);
+ var size = 1;
+ if (next === code) {
+ size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2;
+ if (this.input.charCodeAt(this.pos + size) === 61) return this.finishOp(tt.assign, size + 1);
+ return this.finishOp(tt.bitShift, size);
+ }
+ if (next == 33 && code == 60 && this.input.charCodeAt(this.pos + 2) == 45 && this.input.charCodeAt(this.pos + 3) == 45) {
+ if (this.inModule) unexpected();
+ // `
+
+