From 02608402cc502294b9bf1c3598244f5d5f2e3379 Mon Sep 17 00:00:00 2001 From: sciner Date: Wed, 22 Oct 2025 22:48:16 +0400 Subject: [PATCH 1/5] Added Tesera plugin --- plugins/tesera/LICENSE.MD | 674 +++++++++++++++++++++++++++++++ plugins/tesera/README.md | 14 + plugins/tesera/about.md | 12 + plugins/tesera/changelog.json | 18 + plugins/tesera/icon.png | Bin 0 -> 43346 bytes plugins/tesera/members.yml | 2 + plugins/tesera/tesera.js | 722 ++++++++++++++++++++++++++++++++++ 7 files changed, 1442 insertions(+) create mode 100644 plugins/tesera/LICENSE.MD create mode 100644 plugins/tesera/README.md create mode 100644 plugins/tesera/about.md create mode 100644 plugins/tesera/changelog.json create mode 100644 plugins/tesera/icon.png create mode 100644 plugins/tesera/members.yml create mode 100644 plugins/tesera/tesera.js diff --git a/plugins/tesera/LICENSE.MD b/plugins/tesera/LICENSE.MD new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/plugins/tesera/LICENSE.MD @@ -0,0 +1,674 @@ + 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. + + + Copyright (C) + + 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: + + Copyright (C) + 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 +. diff --git a/plugins/tesera/README.md b/plugins/tesera/README.md new file mode 100644 index 000000000..9f1647949 --- /dev/null +++ b/plugins/tesera/README.md @@ -0,0 +1,14 @@ +# Tesera Plugin Readme + +This plugin is specifically designed as a tool for the developers of the Tesera game. + +It introduces the following features: +1. **Adding a Display**: The \"Add display\" feature in the \"Edit\" section allows developers to introduce display elements into the game. + +2. **Cube Flags Management**: This feature enables developers to assign flags to cubes, providing enhanced flexibility and control over their behavior. + +3. **Custom JSON Assignment to Cubes**: With this feature, developers can assign custom JSON to cubes. This opens up creative possibilities for interpreting and utilizing these JSONs within the game as the developers see fit. + +4. **Animation Tools Enhancement**: Adds a search bar for quickly finding animations by name and introduces customizable color highlighting for animation groups, improving organization and readability within the animation workspace. + +These features are intended to facilitate the development process and provide expanded customization opportunities in Tesera. \ No newline at end of file diff --git a/plugins/tesera/about.md b/plugins/tesera/about.md new file mode 100644 index 000000000..bdfa7f839 --- /dev/null +++ b/plugins/tesera/about.md @@ -0,0 +1,12 @@ +This plugin is specifically designed as a tool for the developers of the Tesera game. + +It introduces the following features: +1. **Adding a Display**: The \"Add display\" feature in the \"Edit\" section allows developers to introduce display elements into the game. + +2. **Cube Flags Management**: This feature enables developers to assign flags to cubes, providing enhanced flexibility and control over their behavior. + +3. **Custom JSON Assignment to Cubes**: With this feature, developers can assign custom JSON to cubes. This opens up creative possibilities for interpreting and utilizing these JSONs within the game as the developers see fit. + +4. **Animation Tools Enhancement**: Adds a search bar for quickly finding animations by name and introduces customizable color highlighting for animation groups, improving organization and readability within the animation workspace. + +These features are intended to facilitate the development process and provide expanded customization opportunities in Tesera. \ No newline at end of file diff --git a/plugins/tesera/changelog.json b/plugins/tesera/changelog.json new file mode 100644 index 000000000..6df491546 --- /dev/null +++ b/plugins/tesera/changelog.json @@ -0,0 +1,18 @@ +{ + "0.0.3": { + "title": "0.0.3", + "author": "SCINER", + "categories": [ + { + "title": "Initial Release", + "list": [ + "Added 'Add display' feature in the 'Edit' section to introduce display elements into the game.", + "Enabled cube flags management for enhanced flexibility and control over cube behavior.", + "Allowed assignment of custom JSON to cubes for creative interpretation and utilization within the game.", + "Enhanced animation tools with a search bar for quickly finding animations by name and customizable color highlighting for animation groups to improve organization and readability." + ] + } + ] + } +} + diff --git a/plugins/tesera/icon.png b/plugins/tesera/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bce9d235772ffb64312a971d51ae4d16cfbf0836 GIT binary patch literal 43346 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 o`&|= .panel_handle { + background-color: #3e90ff55; + color: white; + } + .panel_tab_list > .panel_handle.selected { + background-color: var(--color-accent); + } + .panel_tab_bar { + border-bottom: 1px solid var(--color-accent); + } + .panel_menu_button { + color: white; + } + ` + + class TeseraPropertyEditWidget extends Widget { + + controls = [] + + constructor(id, data) { + if (typeof id == 'object') { + data = id + id = data.id + } + super(id, data) + this.property_name = data.property_name + this.value_name = data.value_name + } + + cloneObject(object) { + return JSON.parse(JSON.stringify(object)) + } + + getProps() { + if(this.props) { + return this.props + } + if(!this.element) { + return null + } + // clone default prop value + return this.props = this.element[this.property_name] = this.cloneObject(default_value) + } + + setElementValue(name, value) { + if(this.element) { + const props = this.getProps() + props[name] = value + // remove props if equal with default + if(JSON.stringify(props) == default_value_string) { + this.props = this.element[this.property_name] = null + } + } + } + + // When select another cube + update() { + const element = Outliner.selected[0] + this.element = element + this.props = element ? element[this.property_name] : null + for(const control of this.controls) { + control.update() + } + } + + createWidgetNode() { + const nodes = [] + for(const control of this.controls) { + nodes.push(...control.nodes) + } + this.node = Interface.createElement('div', {class: 'widget madcraft-widget'}, [ + ...nodes + ]) + } + + } + + // Edit JSON + class TeseraJSONWidget extends TeseraPropertyEditWidget { + + constructor(id, data) { + super(id, data) + this.type = 'jsonedit' + this.controls.push(this.createJSONControl(this.value_name)) + this.createWidgetNode() + } + + createJSONControl(value_name, options) { + + const code = this.jq_code = Interface.createElement('code', {class: 'language-json', id: 'highlighting-content'}) + const textarea = this.jq_textarea = Interface.createElement('textarea', {class: 'dark_bordered focusable_input input-property'}) + textarea.addEventListener('input', () => { + if(this.element) { + const text = textarea.value + try { + const json = text.trim().length > 0 ? JSON.parse(text) : null + this.setElementValue(value_name, json) + } catch(e) { + // do nothing + } + this.updateJSON(text) + } + }) + + const nodes = [ + textarea, + Interface.createElement('pre', {id: 'highlighting', 'aria-hidden': true}, [ + code + ]) + ] + + return {update: () => { + const element = this.element + const text = element ? ((this.props && this.props[value_name]) ? JSON.stringify(this.props[value_name]) : '') : '' + this.updateJSON(text) + }, nodes} + + } + + updateJSON(text) { + const ta = this.jq_textarea + ta.value = text + const code = this.jq_code + code.textContent = text + Prism.highlightElement(code) + ta.readOnly = !this.element + } + + } + + // Editor multiselect + class TeseraMultiselectWidget extends TeseraPropertyEditWidget { + + constructor(id, data) { + super(id, data) + this.type = 'flagsedit' + this.controls.push(this.createMultiSelectPropertyControl(this.value_name, data.options)) + this.createWidgetNode() + } + + createMultiSelectPropertyControl(property_name, options) { + + const div_flags = Interface.createElement('div', {}) + const select = Interface.createElement('select', {class: 'madcraft-select'}) + const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [div_flags, select]) + + const redrawSelect = () => { + const props = this.getProps() + const options_html = [] + options_html.push(``) + for(const [title, value] of Object.entries(options)) { + if(!props || !props[property_name].includes(value)) { + options_html.push(``) + } + } + if(props) { + div_flags.innerHTML = '' + for(const value of props[property_name]) { + let title = value + for(let k in options) { + if(options[k] == value) { + title = k + break + } + } + const delete_tag_button = Interface.createElement('span', {class: 'madcraft-delete-tag', 'data-value': value}, ['×']) + div_flags.appendChild(Interface.createElement('span', {class: 'madcraft-tag'}, [title, delete_tag_button])) + delete_tag_button.addEventListener('click', (e) => { + if(this.element) { + const value = e.srcElement.dataset.value + if(value) { + const props = this.getProps() + if(props) { + const list = props[property_name] + const index = list.indexOf(value) + if(index >= 0) { + list.splice(index, 1) + redrawSelect() + } + } + } + } + }) + } + } + select.innerHTML = options_html.join('\n') + } + + // + select.addEventListener('change', (e) => { + if(this.element) { + const value = e.srcElement.value + if(value) { + const props = this.getProps() + if(props) { + props[property_name].push(value) + redrawSelect() + } + } + } + }) + + redrawSelect() + + return {update: () => {redrawSelect()}, nodes: [select_control_box]} + } + + } + + // Editor select + class TeseraSelectWidget extends TeseraPropertyEditWidget { + + constructor(id, data) { + super(id, data) + this.type = 'selectedit' + this.controls.push(this.createSelectPropertyControl(this.value_name, data.options)) + this.createWidgetNode() + } + + createSelectPropertyControl(property_name, options) { + + const select = Interface.createElement('select', {class: 'madcraft-select'}) + const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [select]) + + const redrawSelect = () => { + const props = this.getProps() + const options_html = [] + options_html.push(``) + for(const [title, value] of Object.entries(options)) { + const selected = props ? (props[property_name] == value ? 'selected' : null) : null + options_html.push(``) + } + select.innerHTML = options_html.join('\n') + } + + // + select.addEventListener('change', (e) => { + if(this.element) { + const value = e.srcElement.value + if(value) { + const props = this.getProps() + if(props) { + props[property_name] = value + // redrawSelect() + } + } + } + }) + + redrawSelect() + + return {update: () => {redrawSelect()}, nodes: [select_control_box]} + } + + } + + // + class MyToolbar extends Toolbar { + + constructor(options) { + super(options) + this.condition = () => (selected.length && Modes.edit) + } + + update() { + super.update() + for(const control of this.children) { + control.update() + } + } + + } + + // Create toolbar + function createToolbar(name, widgets) { + const toolbar = new MyToolbar({ + id: `tb_${name}`.toLowerCase().replaceAll(' ', '_').trim(), + name: name, + label: true, + children: widgets.map((w) => w.id) + }) + Interface.Panels.element.addToolbar(toolbar) + removables.push(toolbar) + } + + // Init display + function initDisplay() { + try { + class DisplayElement extends Cube { + has_uv_cube_face_bar = true; + constructor(data, uuid) { + super(data, uuid) + this.name = 'display' + this.type = 'display' + } + + // Если нужно кастомизировать экспорт в JSON (для сохранения модели) + toJSON() { + let json = super.toJSON() + // json.customProperty = this.customProperty // кастомные данные + json.type = 'display' + return json + } + + // get type() { + // // TODO: dirty hack + // return new Error().stack.includes('vue.min.js') ? 'cube' : 'display' + // } + } + DisplayElement.prototype.title = 'Display'; + // DisplayElement.prototype.type = 'display'; + DisplayElement.prototype.icon = 'fa fa-image'; + DisplayElement.prototype.needsUniqueName = true; + // TODO: clone peoperties + // DisplayElement.properties + // deletables.push(new Property(DisplayElement, 'string', 'name', {default: 'display'})) + OutlinerElement.registerType(DisplayElement, 'display') + let add_action = new Action('add_display', { + name: 'Add Display', + icon: 'fa-image', + category: 'edit', + condition: () => Modes.edit, + click() { + Undo.initEdit({outliner: true, elements: [], selection: true}); + var base_display = new DisplayElement().init() + var group = getCurrentGroup(); + base_display.addTo(group) + if (Format.bone_rig && group) { + var pos1 = group.origin.slice() + base_display.extend({ + position: pos1 + }) + } + + if (Group.selected) Group.selected.unselect() + base_display.select() + Undo.finishEdit('Add display', {outliner: true, elements: selected, selection: true}); + Blockbench.dispatchEvent( 'add_display', {object: base_display} ) + return base_display + } + }) + Interface.Panels.outliner.menu.addAction(add_action, '3') + Toolbars.outliner.add(add_action, '3') + MenuBar.menus.edit.addAction(add_action, '8') + deletables.push(add_action) + } catch(e) { + console.error(e) + } + } + + Plugin.register('tesera', { + ...info, + + onload() { + // Create new property for all Cubes + deletables.push(new Property(Cube, 'instance', property_name, {default: null, exposed: true, label: "Tesera cube properties" })) + // CSS + Blockbench.addCSS(madcraft_css) + // Display + initDisplay() + // Create toolbars + createToolbar('Tesera JSON', [new TeseraJSONWidget('madcraft_cube_json_widget', {property_name, value_name: 'json'})]) + createToolbar('Tesera material', [new TeseraSelectWidget('madcraft_cube_material_widget', {property_name, value_name: 'material', options: { + 'Regular': 'regular', + 'Singleface': 'singleface', + 'Doubleface': 'doubleface', + 'Doubleface solid': 'doubleface_solid', + 'Transparent': 'transparent', + 'Doubleface + Transparent': 'doubleface_transparent', + 'Decal 1': 'decal1', + 'Decal 2': 'decal2', + }})]) + createToolbar('Tesera flags', [new TeseraMultiselectWidget('madcraft_cube_flags_widget', {property_name, value_name: 'flags', options: { + 'ENCHANTED_ANIMATION': 'FLAG_ENCHANTED_ANIMATION', + 'FLUID_ERASE': 'FLAG_FLUID_ERASE', + 'LEAVES': 'FLAG_LEAVES', + 'LIGHT_GRID': 'FLAG_LIGHT_GRID', + 'LOOK_AT_CAMERA': 'FLAG_LOOK_AT_CAMERA', + 'LOOK_AT_CAMERA_HOR': 'FLAG_LOOK_AT_CAMERA_HOR', + 'MIR2_TEX': 'FLAG_MIR2_TEX', + 'NO_CAN_TAKE_AO': 'FLAG_NO_CAN_TAKE_AO', + 'NO_CAN_TAKE_LIGHT': 'FLAG_NO_CAN_TAKE_LIGHT', + 'NO_FOG': 'FLAG_NO_FOG', + 'RAIN_OPACITY': 'FLAG_RAIN_OPACITY', + 'TORCH_FLAME': 'FLAG_TORCH_FLAME', + 'WAVES_VERTEX': 'FLAG_WAVES_VERTEX', + 'NORMAL_UP': 'FLAG_NORMAL_UP', + }})]) + + // animations search + + const def_colors = { + idle: '#DFCFBE', + firstperson: '#D65076', + emote: '#f4d90a', + walk: '#009B77', + sitting: '#88B04B', + levitate: '#92A8D1', + sneak: '#B565A7', + sleep: '#EFC050', + attack: '#E15D44', + } + + class TeseraProject { +x + constructor(id) { + this.id = id + this.groups = {} + this.save_key = `madcraft_project_${id}` + this.load() + } + + load() { + let saved_data = localStorage.getItem(this.save_key) + saved_data = saved_data ? JSON.parse(saved_data) : null + if(saved_data) { + Object.assign(this, saved_data) + } + } + + save() { + const saved_data = { + groups: this.groups + } + localStorage.setItem(this.save_key, JSON.stringify(saved_data)) + } + + getGroup(name) { + let group = this.groups[name] + if(!group) { + group = this.groups[name] = {folded: false} + } + return group + } + + } + + class TeseraPlugin { + projects = new Map() + div + input + last_query = '' + anim_colors = {} + dialog = null + MadCraftProjects = new Map() + panel = Interface.Panels.animations + colors_key = 'madcraft-anim_colors' + + constructor() { + + this.anim_colors = this.loadColors() + + this.createDialog() + + this.panel.on('update', (args) => { + console.log('panel:on', args) + if(args.show) { + this.addSearchField() + } + }) + + } + + // dialog + createDialog() { + const that = this + let prop = JSON.stringify(this.anim_colors, null, 4) + this.dialog = new Dialog({ + id: this.colors_key, + title: 'Enter JSON Colors', + form: { + custom_text: { label: 'Colors', type: 'textarea', value: prop }, + }, + onConfirm(form_result) { + prop = form_result.custom_text + that.saveColors(prop) + } + }) + } + + getProjectID() { + return Project.name + } + + getProject() { + const id = this.getProjectID() + // return Project.madcraft_project + let project = this.projects.get(id) + if(project) { + return project + } + project = new TeseraProject(id) + this.projects.set(project.id, project) + return project + } + + loadColors() { + let str = localStorage.getItem(this.colors_key) || JSON.stringify(def_colors, null, 4) + let json = JSON.parse(str) || def_colors + return json + } + + saveColors(colors) { + let json = JSON.parse(colors) + if(json) { + localStorage.setItem(this.colors_key, colors) + Object.assign(this.anim_colors, json) + this.filterAnimations(this.last_query) + } + } + + toggleGroup(el, group_name, is_group) { + const { input } = this + el.style.backgroundColor = is_group ? '#ffffff22' : 'revert-layer' + // toggler + let toggler = el.querySelector('.icon-open-state') + if(!toggler) { + toggler = document.createElement('i') + toggler.classList.add('icon-open-state', 'fa', 'fa-angle-down') + toggler.style.display = 'flex' + toggler.style.alignItems = 'center' + toggler.onclick = () => { + const project = this.getProject() + const group = project.getGroup(group_name) + group.folded = !group.folded + project.save() + this.filterAnimations(input.value) + } + el.prepend(toggler) + } + toggler.style.display = is_group ? 'flex' : 'none' + // + for(const child of el.children) { + const classes = child.classList + if(classes.contains('material-icons') || classes.contains('in_list_button')) { + child.style.display = is_group ? 'none' : 'unset' + } else if(child.tagName == 'LABEL') { + child.style.textAlign = is_group ? 'center' : 'left' + } + } + } + + filterAnimations(query) { + const lower = this.last_query = query.toLowerCase() + let group_name = '_root_' + const project = this.getProject() + + Animator.animations.forEach(anim => { + const list_elem = this.panel.node.querySelector(`[anim_id="${anim.uuid}"]`) + if (!list_elem) { + console.log('no list_elem') + return + } + const name = anim.name.toLowerCase() + let color = '#ffffff22' + for(const [key, value] of Object.entries(this.anim_colors)) { + if(name.startsWith(key)) { + color = value + break + } + } + let is_group = name.includes('888') || name.includes('---') || name.includes('===') + if(is_group) { + group_name = name + } + const group_visible = !project.getGroup(group_name).folded + let is_visible = (name.includes(lower) && group_visible) || is_group + list_elem.style.borderLeft = `4px solid ${color}` + list_elem.style.display = is_visible ? 'revert-layer' : 'none' + this.toggleGroup(list_elem, group_name, is_group) + }) + } + + addSearchField() { + const anim_list = this.panel.node.querySelector('.toolbar') + if (!anim_list) return + let div = this.div + if (!div || !document.body.contains(div)) { + div = this.div = document.createElement('div') + div.style.display = 'flex' + div.style.flexDirection = 'row' + const input = this.input = document.createElement('input') + input.placeholder = 'Search animations...' + input.style.margin = '4px' + input.style.width = '100%' + input.style.paddingLeft = '.9em' + input.style.margin = '0px' + input.value = this.last_query + input.oninput = () => this.filterAnimations(input.value) + div.appendChild(input) + const button = document.createElement('button') + button.innerHTML = 'tune' + button.style.margin = '0px' + button.style.minWidth = 'auto' + button.onclick = () => this.dialog.show() + div.appendChild(button) + } + if (div.parentElement !== anim_list.parentElement) { + anim_list.appendChild(div) + } + this.filterAnimations(this.input.value) + } + + } + + const madcraft_plugin = new TeseraPlugin() + + Blockbench.on('load_project', data => { + // Project.madcraft_project = data.model.madcraft_project || { + // groups: {} + // } + // Project.madcraft_project = data.model.madcraft_project || { + // groups: {} + // } + madcraft_plugin.addSearchField() + }) + + // Blockbench.on('save_project', (event) => { + // event.model.madcraft_project = Project.madcraft_project + // }) + + + }, + + onunload() { + deletables.forEach(action => { + action.delete(); + }) + removables.forEach(action => { + action.remove(); + }) + } + + }) + +})() From 13e97bc19607650ac94a4421971de6661766d12c Mon Sep 17 00:00:00 2001 From: sciner Date: Wed, 22 Oct 2025 22:55:22 +0400 Subject: [PATCH 2/5] Added Tesera plugin to plugins.json --- plugins.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins.json b/plugins.json index 24bfeb900..f82a0082c 100644 --- a/plugins.json +++ b/plugins.json @@ -1257,5 +1257,20 @@ "Importer" ], "repository": "https://github.com/JannisX11/blockbench-plugins/tree/master/plugins/gltf_importer" + }, + "tesera": { + "title": "Tesera game plugin", + "author": "SCINER", + "icon": "icon.png", + "description": "This plugin is specifically designed as a tool for the developers of the Tesera game'", + "tags": ["Tesera", "Game"], + "version": "0.0.3", + "min_version": "4.8.0", + "creation_date": "2025-10-22", + "await_loading": true, + "variant": "both", + "has_changelog": true, + "website": "https://github.com/bernie-g/geckolib/wiki", + "repository": "https://github.com/sciner/blockbench-plugins/tree/master/plugins/tesera" } } From 4457917412d0a5532627c0980f78e6e6f41a2d0c Mon Sep 17 00:00:00 2001 From: sciner Date: Wed, 22 Oct 2025 22:56:57 +0400 Subject: [PATCH 3/5] Added Tesera plugin to plugins.json --- plugins.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins.json b/plugins.json index f82a0082c..07dbcc33b 100644 --- a/plugins.json +++ b/plugins.json @@ -1270,7 +1270,7 @@ "await_loading": true, "variant": "both", "has_changelog": true, - "website": "https://github.com/bernie-g/geckolib/wiki", + "website": "https://tesera.io", "repository": "https://github.com/sciner/blockbench-plugins/tree/master/plugins/tesera" } } From 0ba3da3a0cf865746551816c9691e4aceb0315d5 Mon Sep 17 00:00:00 2001 From: sciner Date: Sun, 25 Jan 2026 22:18:26 +0400 Subject: [PATCH 4/5] Small refactoring --- plugins/tesera/tesera.js | 605 +++++++++++++++++++++++---------------- 1 file changed, 366 insertions(+), 239 deletions(-) diff --git a/plugins/tesera/tesera.js b/plugins/tesera/tesera.js index 765050e52..f89e492e1 100644 --- a/plugins/tesera/tesera.js +++ b/plugins/tesera/tesera.js @@ -1,5 +1,7 @@ (function() { + console.log('Loaded Tesera plugin') + const info = { title: 'Tesera game plugin', author: 'Tesera', @@ -15,10 +17,11 @@ const property_name = 'madcraft' const removables = [] const deletables = [] + const toolbars = [] const default_value = {flags: [], json: null, material: 'regular'} const default_value_string = JSON.stringify(default_value) - const madcraft_css = ` - .madcraft-widget { + const tesera_css = ` + .tesera-widget { position: relative; margin: 4px; width: 100%; @@ -49,7 +52,7 @@ font-family: var(--font-code); font-size: 14px; } - .madcraft-tag { + .tesera-tag { padding: 3px 8px; border-radius: 2px; background-color: var(--color-button); @@ -57,13 +60,13 @@ margin: 2px; line-height: 1em; } - .madcraft-tag:hover { + .tesera-tag:hover { background-color: rgba(255, 255, 255, .35); } .select_control_box { padding: 4px; } - .madcraft-delete-tag { + .tesera-delete-tag { border-radius: 50px; width: 16px; height: 16px; @@ -74,10 +77,10 @@ justify-content: center; cursor: pointer; } - .madcraft-delete-tag:hover { + .tesera-delete-tag:hover { background-color: var(--color-close); } - .madcraft-select { + .tesera-select { width: 100%; border-radius: 4px; appearance: auto; @@ -97,6 +100,16 @@ .panel_menu_button { color: white; } + #bb-tesera-anim-search + margin: 4px; + width: 100%; + padding-left: .9em; + margin: 0px; + } + #bb-tesera-anim-colors-button { + margin: 0px; + minWidth: auto; + } ` class TeseraPropertyEditWidget extends Widget { @@ -154,9 +167,10 @@ for(const control of this.controls) { nodes.push(...control.nodes) } - this.node = Interface.createElement('div', {class: 'widget madcraft-widget'}, [ + this.node = Interface.createElement('div', {class: 'widget tesera-widget'}, [ ...nodes ]) + removables.push(this.node) } } @@ -227,7 +241,7 @@ createMultiSelectPropertyControl(property_name, options) { const div_flags = Interface.createElement('div', {}) - const select = Interface.createElement('select', {class: 'madcraft-select'}) + const select = Interface.createElement('select', {class: 'tesera-select'}) const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [div_flags, select]) const redrawSelect = () => { @@ -249,8 +263,8 @@ break } } - const delete_tag_button = Interface.createElement('span', {class: 'madcraft-delete-tag', 'data-value': value}, ['×']) - div_flags.appendChild(Interface.createElement('span', {class: 'madcraft-tag'}, [title, delete_tag_button])) + const delete_tag_button = Interface.createElement('span', {class: 'tesera-delete-tag', 'data-value': value}, ['×']) + div_flags.appendChild(Interface.createElement('span', {class: 'tesera-tag'}, [title, delete_tag_button])) delete_tag_button.addEventListener('click', (e) => { if(this.element) { const value = e.srcElement.dataset.value @@ -305,7 +319,7 @@ createSelectPropertyControl(property_name, options) { - const select = Interface.createElement('select', {class: 'madcraft-select'}) + const select = Interface.createElement('select', {class: 'tesera-select'}) const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [select]) const redrawSelect = () => { @@ -366,7 +380,84 @@ children: widgets.map((w) => w.id) }) Interface.Panels.element.addToolbar(toolbar) - removables.push(toolbar) + toolbars.push(toolbar) + } + + // добавляем новый режим в UI + function initViewDisplay() { + + // debugger + + // const tooltipSlot = new DisplaySlot({ + // id: 'tooltip_view', + // name: 'Tooltip View', + // translation: [0, 8, 0], + // rotation: [30, 225, 0], + // scale: [0.65, 0.65, 0.65], + // reference: 'player' + // }); + + // DisplayMode + + const ID = 'tooltip_view'; + + // 1. Создаём точную копию структуры как у ground/fixed + DisplayMode[ID] = { + translation: [0, 8, 0], + rotation: [30, 225, 0], + scale: [0.65, 0.65, 0.65] + }; + + // 2. Создаём функцию загрузки — один в один как loadGround() + DisplayMode['load_' + ID] = function() { + DisplayMode.selected = ID; + loadDisp(ID); + display_preview.loadAnglePreset({ + position: [-30, 45, -30], + target: [0, 8, 0] + }); + setDisplayArea(10, 6, 10, 0, 8, 0, 0.65, 0.65, 0.65); + Canvas.ground_animation = false; + displayReferenceObjects.bar(['player']); + updateDisplayPreview(); + }; + + /* + // 3. Добавляем кнопку в боковую панель + const NAME = 'Tooltip View'; + new Action('display_' + ID, { + name: NAME, + icon: 'fa-info-circle', + click: DisplayMode['load_' + ID] + }).addTo('#display_panel_bar'); + + // 4. Добавляем в экспорт + const formats = ['bedrock', 'bedrock_old', 'java_block', 'optifine_entity', 'modded_entity']; + formats.forEach(f => { + if (Formats[f]?.display_slots) { + Formats[f].display_slots.push(ID); + } + }); + + console.log('%c[tooltip_view] УСПЕШНО ЗАГРУЖЕН В 5.0.3', 'color: #00ff00; font-weight: bold'); + */ + + // добавляем сохранение и экспорт + // const orig_toJSON = Model.prototype.export + // Model.prototype.export = function(format, options) { + // const data = orig_toJSON.call(this, format, options) + // if (!data.display) data.display = {} + // const disp = DisplayMode.all['custom_view'] + // if (disp) { + // data.display.custom_view = { + // rotation: disp.rotation, + // translation: disp.translation, + // scale: disp.scale, + // } + // } + // return data + // } + } // Init display @@ -434,19 +525,266 @@ } } + const def_colors = { + idle: '#DFCFBE', + firstperson: '#D65076', + emote: '#f4d90a', + walk: '#009B77', + sitting: '#88B04B', + levitate: '#92A8D1', + sneak: '#B565A7', + sleep: '#EFC050', + attack: '#E15D44', + } + + class TeseraProject { + + constructor(id) { + this.id = id + this.groups = {} + this.save_key = `tesera_project_${id}` + this.load() + } + + load() { + let saved_data = localStorage.getItem(this.save_key) + saved_data = saved_data ? JSON.parse(saved_data) : null + if(saved_data) { + Object.assign(this, saved_data) + } + } + + save() { + const saved_data = { + groups: this.groups + } + localStorage.setItem(this.save_key, JSON.stringify(saved_data)) + } + + getGroup(name) { + let group = this.groups[name] + if(!group) { + group = this.groups[name] = {folded: false} + } + return group + } + + } + + class TeseraPlugin { + projects = new Map() + div + input + last_query = '' + anim_colors = {} + dialog = null + TeseraProjects = new Map() + panel = Interface.Panels.animations + colors_key = 'madcraft-anim_colors' + + static on_keydown = async (e) => { + if(e.code !== 'KeyV' || !e.ctrlKey) return + + const el = document.activeElement + if( + !el || + !(el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') || + el.readOnly || + el.disabled + ) return + + e.preventDefault() + + const text = await navigator.clipboard.readText() + + const start = el.selectionStart + const end = el.selectionEnd + + el.value = + el.value.slice(0, start) + + text + + el.value.slice(end) + + const pos = start + text.length + el.setSelectionRange(pos, pos) + + el.dispatchEvent(new Event('input', { bubbles: true })) + } + + constructor() { + + this.anim_colors = this.loadColors() + + this.createDialog() + + this.panel.on('update', (args) => { + // console.log('panel:on', args) + if(args.show) { + this.addSearchField() + } + }) + + } + + // dialog + createDialog() { + const that = this + let prop = JSON.stringify(this.anim_colors, null, 4) + this.dialog = new Dialog({ + id: this.colors_key, + title: 'Enter JSON Colors', + form: { + custom_text: { label: 'Colors', type: 'textarea', value: prop }, + }, + onConfirm(form_result) { + prop = form_result.custom_text + that.saveColors(prop) + } + }) + } + + getProjectID() { + return Project.name + } + + getProject() { + const id = this.getProjectID() + // return Project.tesera_project + let project = this.projects.get(id) + if(project) { + return project + } + project = new TeseraProject(id) + this.projects.set(project.id, project) + return project + } + + loadColors() { + let str = localStorage.getItem(this.colors_key) || JSON.stringify(def_colors, null, 4) + let json = JSON.parse(str) || def_colors + return json + } + + saveColors(colors) { + let json = JSON.parse(colors) + if(json) { + localStorage.setItem(this.colors_key, colors) + Object.assign(this.anim_colors, json) + this.filterAnimations(this.last_query) + } + } + + toggleGroup(el, group_name, is_group) { + const { input } = this + el.style.backgroundColor = is_group ? '#ffffff22' : 'revert-layer' + // toggler + let toggler = el.querySelector('.icon-open-state') + if(!toggler) { + toggler = document.createElement('i') + toggler.classList.add('icon-open-state', 'fa', 'fa-angle-down') + toggler.style.display = 'flex' + toggler.style.alignItems = 'center' + toggler.onclick = () => { + const project = this.getProject() + const group = project.getGroup(group_name) + group.folded = !group.folded + project.save() + this.filterAnimations(input.value) + } + el.prepend(toggler) + } + toggler.style.display = is_group ? 'flex' : 'none' + // + for(const child of el.children) { + const classes = child.classList + if(classes.contains('material-icons') || classes.contains('in_list_button')) { + child.style.display = is_group ? 'none' : 'unset' + } else if(child.tagName == 'LABEL') { + child.style.textAlign = is_group ? 'center' : 'left' + } + } + } + + filterAnimations(query) { + const lower = this.last_query = query.toLowerCase() + let group_name = '_root_' + const project = this.getProject() + + Animator.animations.forEach(anim => { + const list_elem = this.panel.node.querySelector(`[anim_id="${anim.uuid}"]`) + if (!list_elem) { + console.log('no list_elem') + return + } + const name = anim.name.toLowerCase() + let color = '#ffffff22' + for(const [key, value] of Object.entries(this.anim_colors)) { + if(name.startsWith(key)) { + color = value + break + } + } + let is_group = name.includes('888') || name.includes('---') || name.includes('===') + if(is_group) { + group_name = name + } + const group_visible = !project.getGroup(group_name).folded + let is_visible = (name.includes(lower) && group_visible) || is_group + list_elem.style.borderLeft = `4px solid ${color}` + list_elem.style.display = is_visible ? 'revert-layer' : 'none' + this.toggleGroup(list_elem, group_name, is_group) + }) + } + + addSearchField() { + const anim_list = this.panel.node.querySelector('.toolbar') + if (!anim_list) return + let div = this.div + if (!div /*|| !document.body.contains(div)*/) { + div = this.div = document.createElement('div') + div.style.display = 'flex' + div.style.flexDirection = 'row' + const input = this.input = document.createElement('input') + input.id = 'bb-tesera-anim-search' + input.placeholder = 'Search animations...' + input.value = this.last_query + input.oninput = () => this.filterAnimations(input.value) + div.appendChild(input) + const button = document.createElement('button') + button.id = 'bb-tesera-anim-colors-button' + button.innerHTML = 'tune' + button.onclick = () => this.dialog.show() + div.appendChild(button) + removables.push(this.div) + if (div.parentElement !== anim_list.parentElement) { + anim_list.appendChild(div) + } + this.filterAnimations(this.input.value) + } + } + + } + Plugin.register('tesera', { ...info, onload() { + + console.log('Loading Tesera plugin...') + + // Keydown listener (Fix Ctrl+V and Ctrl+A in textarea and input) + window.addEventListener('keydown', TeseraPlugin.on_keydown) // Create new property for all Cubes deletables.push(new Property(Cube, 'instance', property_name, {default: null, exposed: true, label: "Tesera cube properties" })) // CSS - Blockbench.addCSS(madcraft_css) + Blockbench.addCSS(tesera_css) // Display initDisplay() + // Init display + initViewDisplay() // Create toolbars - createToolbar('Tesera JSON', [new TeseraJSONWidget('madcraft_cube_json_widget', {property_name, value_name: 'json'})]) - createToolbar('Tesera material', [new TeseraSelectWidget('madcraft_cube_material_widget', {property_name, value_name: 'material', options: { + createToolbar('Tesera JSON', [new TeseraJSONWidget('tesera_cube_json_widget', {property_name, value_name: 'json'})]) + createToolbar('Tesera material', [new TeseraSelectWidget('tesera_cube_material_widget', {property_name, value_name: 'material', options: { 'Regular': 'regular', 'Singleface': 'singleface', 'Doubleface': 'doubleface', @@ -456,7 +794,7 @@ 'Decal 1': 'decal1', 'Decal 2': 'decal2', }})]) - createToolbar('Tesera flags', [new TeseraMultiselectWidget('madcraft_cube_flags_widget', {property_name, value_name: 'flags', options: { + createToolbar('Tesera flags', [new TeseraMultiselectWidget('tesera_cube_flags_widget', {property_name, value_name: 'flags', options: { 'ENCHANTED_ANIMATION': 'FLAG_ENCHANTED_ANIMATION', 'FLUID_ERASE': 'FLAG_FLUID_ERASE', 'LEAVES': 'FLAG_LEAVES', @@ -473,248 +811,37 @@ 'NORMAL_UP': 'FLAG_NORMAL_UP', }})]) - // animations search - - const def_colors = { - idle: '#DFCFBE', - firstperson: '#D65076', - emote: '#f4d90a', - walk: '#009B77', - sitting: '#88B04B', - levitate: '#92A8D1', - sneak: '#B565A7', - sleep: '#EFC050', - attack: '#E15D44', - } - - class TeseraProject { -x - constructor(id) { - this.id = id - this.groups = {} - this.save_key = `madcraft_project_${id}` - this.load() - } - - load() { - let saved_data = localStorage.getItem(this.save_key) - saved_data = saved_data ? JSON.parse(saved_data) : null - if(saved_data) { - Object.assign(this, saved_data) - } - } - - save() { - const saved_data = { - groups: this.groups - } - localStorage.setItem(this.save_key, JSON.stringify(saved_data)) - } - - getGroup(name) { - let group = this.groups[name] - if(!group) { - group = this.groups[name] = {folded: false} - } - return group - } - - } - - class TeseraPlugin { - projects = new Map() - div - input - last_query = '' - anim_colors = {} - dialog = null - MadCraftProjects = new Map() - panel = Interface.Panels.animations - colors_key = 'madcraft-anim_colors' - - constructor() { - - this.anim_colors = this.loadColors() - - this.createDialog() - - this.panel.on('update', (args) => { - console.log('panel:on', args) - if(args.show) { - this.addSearchField() - } - }) - - } - - // dialog - createDialog() { - const that = this - let prop = JSON.stringify(this.anim_colors, null, 4) - this.dialog = new Dialog({ - id: this.colors_key, - title: 'Enter JSON Colors', - form: { - custom_text: { label: 'Colors', type: 'textarea', value: prop }, - }, - onConfirm(form_result) { - prop = form_result.custom_text - that.saveColors(prop) - } - }) - } - - getProjectID() { - return Project.name - } - - getProject() { - const id = this.getProjectID() - // return Project.madcraft_project - let project = this.projects.get(id) - if(project) { - return project - } - project = new TeseraProject(id) - this.projects.set(project.id, project) - return project - } - - loadColors() { - let str = localStorage.getItem(this.colors_key) || JSON.stringify(def_colors, null, 4) - let json = JSON.parse(str) || def_colors - return json - } - - saveColors(colors) { - let json = JSON.parse(colors) - if(json) { - localStorage.setItem(this.colors_key, colors) - Object.assign(this.anim_colors, json) - this.filterAnimations(this.last_query) - } - } - - toggleGroup(el, group_name, is_group) { - const { input } = this - el.style.backgroundColor = is_group ? '#ffffff22' : 'revert-layer' - // toggler - let toggler = el.querySelector('.icon-open-state') - if(!toggler) { - toggler = document.createElement('i') - toggler.classList.add('icon-open-state', 'fa', 'fa-angle-down') - toggler.style.display = 'flex' - toggler.style.alignItems = 'center' - toggler.onclick = () => { - const project = this.getProject() - const group = project.getGroup(group_name) - group.folded = !group.folded - project.save() - this.filterAnimations(input.value) - } - el.prepend(toggler) - } - toggler.style.display = is_group ? 'flex' : 'none' - // - for(const child of el.children) { - const classes = child.classList - if(classes.contains('material-icons') || classes.contains('in_list_button')) { - child.style.display = is_group ? 'none' : 'unset' - } else if(child.tagName == 'LABEL') { - child.style.textAlign = is_group ? 'center' : 'left' - } - } - } - - filterAnimations(query) { - const lower = this.last_query = query.toLowerCase() - let group_name = '_root_' - const project = this.getProject() - - Animator.animations.forEach(anim => { - const list_elem = this.panel.node.querySelector(`[anim_id="${anim.uuid}"]`) - if (!list_elem) { - console.log('no list_elem') - return - } - const name = anim.name.toLowerCase() - let color = '#ffffff22' - for(const [key, value] of Object.entries(this.anim_colors)) { - if(name.startsWith(key)) { - color = value - break - } - } - let is_group = name.includes('888') || name.includes('---') || name.includes('===') - if(is_group) { - group_name = name - } - const group_visible = !project.getGroup(group_name).folded - let is_visible = (name.includes(lower) && group_visible) || is_group - list_elem.style.borderLeft = `4px solid ${color}` - list_elem.style.display = is_visible ? 'revert-layer' : 'none' - this.toggleGroup(list_elem, group_name, is_group) - }) - } - - addSearchField() { - const anim_list = this.panel.node.querySelector('.toolbar') - if (!anim_list) return - let div = this.div - if (!div || !document.body.contains(div)) { - div = this.div = document.createElement('div') - div.style.display = 'flex' - div.style.flexDirection = 'row' - const input = this.input = document.createElement('input') - input.placeholder = 'Search animations...' - input.style.margin = '4px' - input.style.width = '100%' - input.style.paddingLeft = '.9em' - input.style.margin = '0px' - input.value = this.last_query - input.oninput = () => this.filterAnimations(input.value) - div.appendChild(input) - const button = document.createElement('button') - button.innerHTML = 'tune' - button.style.margin = '0px' - button.style.minWidth = 'auto' - button.onclick = () => this.dialog.show() - div.appendChild(button) - } - if (div.parentElement !== anim_list.parentElement) { - anim_list.appendChild(div) - } - this.filterAnimations(this.input.value) - } - - } - - const madcraft_plugin = new TeseraPlugin() + const tesera_plugin = new TeseraPlugin() Blockbench.on('load_project', data => { - // Project.madcraft_project = data.model.madcraft_project || { + // Project.tesera_project = data.model.tesera_project || { // groups: {} // } - // Project.madcraft_project = data.model.madcraft_project || { + // Project.tesera_project = data.model.tesera_project || { // groups: {} // } - madcraft_plugin.addSearchField() + tesera_plugin.addSearchField() }) // Blockbench.on('save_project', (event) => { - // event.model.madcraft_project = Project.madcraft_project + // event.model.tesera_project = Project.tesera_project // }) - }, onunload() { + console.log('Unloading Tesera plugin...') + window.removeEventListener('keydown', TeseraPlugin.on_keydown) deletables.forEach(action => { action.delete(); }) removables.forEach(action => { action.remove(); }) + toolbars.forEach(toolbar => { + // Interface.Panels.element.removeToolbar(toolbar) + toolbar.remove() + }) } }) From 93a580c5aff8468c674f0907c9c0556df9832b22 Mon Sep 17 00:00:00 2001 From: sciner Date: Mon, 26 Jan 2026 15:55:59 +0400 Subject: [PATCH 5/5] Refactoring --- plugins/tesera/tesera.js | 332 ++++++++++++++++++++++++--------------- 1 file changed, 209 insertions(+), 123 deletions(-) diff --git a/plugins/tesera/tesera.js b/plugins/tesera/tesera.js index f89e492e1..ded89bda4 100644 --- a/plugins/tesera/tesera.js +++ b/plugins/tesera/tesera.js @@ -14,10 +14,10 @@ min_version: "4.8.0", } - const property_name = 'madcraft' const removables = [] const deletables = [] const toolbars = [] + const listeners = [] const default_value = {flags: [], json: null, material: 'regular'} const default_value_string = JSON.stringify(default_value) const tesera_css = ` @@ -112,6 +112,11 @@ } ` + function CreateBBListener(event, callback) { + Blockbench.on(event, callback) + listeners.push({event, callback}) + } + class TeseraPropertyEditWidget extends Widget { controls = [] @@ -124,6 +129,7 @@ super(id, data) this.property_name = data.property_name this.value_name = data.value_name + // deletables.push(this) } cloneObject(object) { @@ -131,14 +137,7 @@ } getProps() { - if(this.props) { - return this.props - } - if(!this.element) { - return null - } - // clone default prop value - return this.props = this.element[this.property_name] = this.cloneObject(default_value) + return this.element?.[this.property_name] || null } setElementValue(name, value) { @@ -171,10 +170,15 @@ ...nodes ]) removables.push(this.node) + deletables.push(new Property(Cube, 'array', this.property_name, { + default: [], + exposed: true, + })) } } + /* // Edit JSON class TeseraJSONWidget extends TeseraPropertyEditWidget { @@ -227,6 +231,7 @@ } } + */ // Editor multiselect class TeseraMultiselectWidget extends TeseraPropertyEditWidget { @@ -234,8 +239,9 @@ constructor(id, data) { super(id, data) this.type = 'flagsedit' - this.controls.push(this.createMultiSelectPropertyControl(this.value_name, data.options)) + this.controls.push(this.createMultiSelectPropertyControl(this.property_name, data.options)) this.createWidgetNode() + } createMultiSelectPropertyControl(property_name, options) { @@ -244,18 +250,22 @@ const select = Interface.createElement('select', {class: 'tesera-select'}) const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [div_flags, select]) + removables.push(div_flags) + removables.push(select) + removables.push(select_control_box) + const redrawSelect = () => { const props = this.getProps() const options_html = [] options_html.push(``) for(const [title, value] of Object.entries(options)) { - if(!props || !props[property_name].includes(value)) { + if(!props || !props.includes(value)) { options_html.push(``) } } if(props) { div_flags.innerHTML = '' - for(const value of props[property_name]) { + for(const value of props) { let title = value for(let k in options) { if(options[k] == value) { @@ -264,17 +274,22 @@ } } const delete_tag_button = Interface.createElement('span', {class: 'tesera-delete-tag', 'data-value': value}, ['×']) - div_flags.appendChild(Interface.createElement('span', {class: 'tesera-tag'}, [title, delete_tag_button])) + const ttg = Interface.createElement('span', {class: 'tesera-tag'}, [title, delete_tag_button]) + removables.push(delete_tag_button, ttg) + div_flags.appendChild(ttg) delete_tag_button.addEventListener('click', (e) => { if(this.element) { const value = e.srcElement.dataset.value if(value) { const props = this.getProps() if(props) { - const list = props[property_name] + const list = props const index = list.indexOf(value) if(index >= 0) { + Undo.initEdit({ elements: [this.element] }) list.splice(index, 1) + this.element[property_name] = JSON.parse(JSON.stringify(list)) + Undo.finishEdit('Edit Tesera Flags', { elements: [this.element] }) redrawSelect() } } @@ -293,7 +308,10 @@ if(value) { const props = this.getProps() if(props) { - props[property_name].push(value) + const before = JSON.parse(JSON.stringify(this.element[property_name])) + Undo.initEdit({ elements: [this.element] }) + this.element[property_name] = [...before, value] + Undo.finishEdit('Edit Tesera Flags') redrawSelect() } } @@ -302,52 +320,8 @@ redrawSelect() - return {update: () => {redrawSelect()}, nodes: [select_control_box]} - } - - } - - // Editor select - class TeseraSelectWidget extends TeseraPropertyEditWidget { - - constructor(id, data) { - super(id, data) - this.type = 'selectedit' - this.controls.push(this.createSelectPropertyControl(this.value_name, data.options)) - this.createWidgetNode() - } - - createSelectPropertyControl(property_name, options) { - - const select = Interface.createElement('select', {class: 'tesera-select'}) - const select_control_box = Interface.createElement('div', {class: 'select_control_box'}, [select]) - - const redrawSelect = () => { - const props = this.getProps() - const options_html = [] - options_html.push(``) - for(const [title, value] of Object.entries(options)) { - const selected = props ? (props[property_name] == value ? 'selected' : null) : null - options_html.push(``) - } - select.innerHTML = options_html.join('\n') - } - - // - select.addEventListener('change', (e) => { - if(this.element) { - const value = e.srcElement.value - if(value) { - const props = this.getProps() - if(props) { - props[property_name] = value - // redrawSelect() - } - } - } - }) - - redrawSelect() + CreateBBListener('undo', redrawSelect) + CreateBBListener('redo', redrawSelect) return {update: () => {redrawSelect()}, nodes: [select_control_box]} } @@ -581,8 +555,10 @@ TeseraProjects = new Map() panel = Interface.Panels.animations colors_key = 'madcraft-anim_colors' + storage_name = 'madcraft' static on_keydown = async (e) => { + if(e.code !== 'KeyV' || !e.ctrlKey) return const el = document.activeElement @@ -596,7 +572,6 @@ e.preventDefault() const text = await navigator.clipboard.readText() - const start = el.selectionStart const end = el.selectionEnd @@ -607,22 +582,22 @@ const pos = start + text.length el.setSelectionRange(pos, pos) - el.dispatchEvent(new Event('input', { bubbles: true })) + } constructor() { - - this.anim_colors = this.loadColors() + this.anim_colors = this.loadColors() + this.createCubeProperties() this.createDialog() - this.panel.on('update', (args) => { + deletables.push(this.panel.on('update', (args) => { // console.log('panel:on', args) if(args.show) { this.addSearchField() } - }) + })) } @@ -641,6 +616,7 @@ that.saveColors(prop) } }) + deletables.push(this.dialog) } getProjectID() { @@ -744,14 +720,18 @@ div = this.div = document.createElement('div') div.style.display = 'flex' div.style.flexDirection = 'row' - const input = this.input = document.createElement('input') - input.id = 'bb-tesera-anim-search' + const input = this.input = document.createElement('input', {id: 'bb-tesera-anim-search'}) + input.style.margin = '4px' + input.style.width = '100%' + input.style.paddingLeft = '.9em' + input.style.margin = '0px' input.placeholder = 'Search animations...' input.value = this.last_query input.oninput = () => this.filterAnimations(input.value) div.appendChild(input) - const button = document.createElement('button') - button.id = 'bb-tesera-anim-colors-button' + const button = document.createElement('button', {id: 'bb-tesera-anim-colors-button'}) + button.style.margin = '0px' + button.style.minWidth = 'auto' button.innerHTML = 'tune' button.onclick = () => this.dialog.show() div.appendChild(button) @@ -763,38 +743,39 @@ } } - } - - Plugin.register('tesera', { - ...info, - - onload() { - - console.log('Loading Tesera plugin...') + /** + * Create properties for all Cubes + */ + createCubeProperties() { + + const { storage_name } = this + + // ---------- UI properties ---------- + + deletables.push(new Property(Cube, 'string', 'tesera_material', { + default: 'regular', + exposed: true, + label: 'Material', + inputs: { + element_panel: { + input: { + type: 'select', + options: { + regular: 'Regular', + singleface: 'Singleface', + doubleface: 'Doubleface', + doubleface_solid: 'Doubleface solid', + transparent: 'Transparent', + doubleface_transparent: 'Doubleface + Transparent', + decal1: 'Decal 1', + decal2: 'Decal 2', + }, + }, + }, + }, + })) - // Keydown listener (Fix Ctrl+V and Ctrl+A in textarea and input) - window.addEventListener('keydown', TeseraPlugin.on_keydown) - // Create new property for all Cubes - deletables.push(new Property(Cube, 'instance', property_name, {default: null, exposed: true, label: "Tesera cube properties" })) - // CSS - Blockbench.addCSS(tesera_css) - // Display - initDisplay() - // Init display - initViewDisplay() - // Create toolbars - createToolbar('Tesera JSON', [new TeseraJSONWidget('tesera_cube_json_widget', {property_name, value_name: 'json'})]) - createToolbar('Tesera material', [new TeseraSelectWidget('tesera_cube_material_widget', {property_name, value_name: 'material', options: { - 'Regular': 'regular', - 'Singleface': 'singleface', - 'Doubleface': 'doubleface', - 'Doubleface solid': 'doubleface_solid', - 'Transparent': 'transparent', - 'Doubleface + Transparent': 'doubleface_transparent', - 'Decal 1': 'decal1', - 'Decal 2': 'decal2', - }})]) - createToolbar('Tesera flags', [new TeseraMultiselectWidget('tesera_cube_flags_widget', {property_name, value_name: 'flags', options: { + createToolbar('Tesera flags', [new TeseraMultiselectWidget('tesera_cube_flags_widget', {property_name: 'tesera_flags', options: { 'ENCHANTED_ANIMATION': 'FLAG_ENCHANTED_ANIMATION', 'FLUID_ERASE': 'FLAG_FLUID_ERASE', 'LEAVES': 'FLAG_LEAVES', @@ -811,37 +792,142 @@ 'NORMAL_UP': 'FLAG_NORMAL_UP', }})]) - const tesera_plugin = new TeseraPlugin() + deletables.push(new Property(Cube, 'string', 'tesera_json', { + default: '', + exposed: true, + label: 'Tesera JSON', + inputs: { + element_panel: { + input: { type: 'textarea' }, + }, + }, + })) - Blockbench.on('load_project', data => { - // Project.tesera_project = data.model.tesera_project || { - // groups: {} - // } - // Project.tesera_project = data.model.tesera_project || { - // groups: {} - // } - tesera_plugin.addSearchField() + // ---------- LOAD: tesera -> UI ---------- + + CreateBBListener('load_project', (data) => { + for(const el of data.model.elements) { + if(!el[this.storage_name]) continue + for(const k in el[this.storage_name]) { + switch(k) { + case 'json': + el.tesera_json = el[this.storage_name].json ? JSON.stringify(el[this.storage_name].json, null, 4) : '' + break + default: + el[`tesera_${k}`] = el[this.storage_name][k] + break + } + } + delete(el[this.storage_name]) + } }) - - // Blockbench.on('save_project', (event) => { - // event.model.tesera_project = Project.tesera_project - // }) + + // ---------- SAVE: UI -> madcraft ---------- + CreateBBListener('save_project', (data) => { + for(const el of data.model.elements) { + const tesera = {} + for(const k in el) { + if(!k.startsWith('tesera_')) continue + const value = el[k] + const ik = k.slice(7) + switch(ik) { + case 'json': { + try { + tesera[ik] = JSON.parse(value) || null + } catch { + tesera[ik] = null + } + break + } + default: { + tesera[ik] = value + break + } + } + if(tesera[ik] == null || tesera[ik] === '' || (Array.isArray(tesera[ik]) && tesera[ik].length === 0)) { + delete tesera[ik] + } + delete el[k] + } + if(Object.keys(tesera).length === 0) { + delete el[this.storage_name] + continue + } + if(Object.keys(tesera).length) { + el[this.storage_name] = tesera + } + } + }) + } + + } + + Plugin.register('tesera', { + ...info, + + onload() { + + console.log('Loading Tesera plugin...') + + const tesera_plugin = new TeseraPlugin() + + // Keydown listener (Fix Ctrl+V and Ctrl+A in textarea and input) + window.addEventListener('keydown', TeseraPlugin.on_keydown) + + // CSS + deletables.push(Blockbench.addCSS(tesera_css)) + + // Display + // initDisplay() + // Init display + // initViewDisplay() + + /* + // Create toolbars + // createToolbar('Tesera JSON', [new TeseraJSONWidget('tesera_cube_json_widget', {property_name, value_name: 'json'})]) + + CreateBBListener('load_project', data => { + + CreateBBListener('load_project', () => { + Cube.all.forEach(c => { + const old = c[property_name] + if(!old) return + if(old.material) c.tesera_material = old.material + if(Array.isArray(old.flags)) c.tesera_flags = old.flags + if(old.json != null) c.tesera_json = JSON.stringify(old.json) + c[property_name] = null + }) + }) + }) + */ }, onunload() { console.log('Unloading Tesera plugin...') window.removeEventListener('keydown', TeseraPlugin.on_keydown) - deletables.forEach(action => { - action.delete(); - }) - removables.forEach(action => { - action.remove(); - }) - toolbars.forEach(toolbar => { + for(const item of deletables) { + item.delete() + } + for(const item of removables) { + item.remove() + } + for(const toolbar of toolbars) { // Interface.Panels.element.removeToolbar(toolbar) toolbar.remove() - }) + } + for(const listener of listeners) { + Blockbench.removeListener(listener.event, listener.callback) + } + deletables.length = 0 + removables.length = 0 + toolbars.length = 0 + listeners.length = 0 + + console.log(Interface.Panels.element.toolbars.length) + console.log(Interface.Panels.element.toolbars.map(t => t.id)) + console.log(Object.keys(Cube.properties).filter(k => k.startsWith('tesera'))) + } })