diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-15 16:30:22 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-16 00:19:32 +0100 |
| commit | 566eb2c9cce799ed8a3becc6d81560f1540eaacf (patch) | |
| tree | 4a711b16ff15c61842856a2bf2b830079f30bbcf | |
Initial Release
36 files changed, 7273 insertions, 0 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differnew file mode 100644 index 0000000..e62e93c --- /dev/null +++ b/.DS_Store diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80f2168 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.js] +charset = utf-8 +indent_size = 4 +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d91b6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +test_output.pdf +test_output.png +test_output.jpeg diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..ba2e9bb --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +## 1.0.0 - 2025-12-16 + +### Added + +* Initial release. @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + wmap-parser-js, Javascript wmap wardley map file parser. + Copyright (C) 2025 Ruben Beltran del Rio + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +<https://www.gnu.org/licenses/>. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7845101 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +default: build + +prepare: + pnpm install + +# Code Quality + +types: + @tsc + +test: + @node --test + +coverage: + @node --test --experimental-test-coverage + +lint: prepare + @pnpm exec eslint . --fix + +format: lint + @pnpm exec prettier . --write + +benchmark: prepare + @node benchmark/index.js + +ci: bundle test + @pnpm exec eslint . + @pnpm exec prettier . --check + +# Deployment + +release: + pnpm publish + +# Auditing + +sbom: + @trivy fs -q --format cyclonedx --scanners vuln,license --output extension-sbom.json extensions/lenislab + @trivy image -q --format cyclonedx --scanners vuln,license --output image-sbom.json --podman-host="$(podman_socket)" --image-src podman $(image_name):$(tag) + +vulnerabilities: sbom + @echo "** Extension **" + @sbom-utility vulnerability list -q --input-file extension-sbom.json + @echo "** Image **" + @sbom-utility vulnerability list -q --input-file image-sbom.json + +licenses: sbom + @echo "** Extension **" + @sbom-utility license list -q --input-file extension-sbom.json + @echo "** Image **" + @sbom-utility license list -q --input-file image-sbom.json + +.PHONY: default prepare test coverage format lint release ci types diff --git a/README.md b/README.md new file mode 100644 index 0000000..a01f268 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# wmap-renderer-canvas + +A Canvas-based renderer for `wmap` formatted wardley maps parsed with [`wmap-parser`][wmap-parser-js]. + +## Features + +- Can render to canvas, raster images (PDF / JPEG), or vector (PDF) +- Rendering logic attempts to replicate [map][map]'s output. + +## Installation + +```bash +npm install wmap-renderer-canvas +``` + +```bash +yarn add wmap-renderer-canvas +``` + +```bash +pnpm add wmap-renderer-canvas +``` + +## Usage + +### Browser + +```javascript +import { parse } from "wmap-parser"; +import { renderToCanvas } from "wmap-renderer-canvas"; + +const wmapSource = ` +Tea (0.9, 0.5) +Cup (0.8, 0.4) +Tea -> Cup +`; + +const map = parse(wmapSource); +const canvas = document.getElementById("myCanvas"); + +await renderToCanvas(map, canvas); +``` + +### Node.js - Render to Canvas + +```javascript +import { createCanvas } from "canvas"; +import { parse } from "wmap-parser"; +import { renderToCanvas, StageType } from "wmap-renderer-canvas"; + +const map = parse(wmapSource); +const canvas = createCanvas(1384, 1084); + +// No overrides +await renderToCanvas(map, canvas); + +// Stage Type override +await renderToCanvas(map, canvas, StageType.DECISION_DRIVERS); +``` + +### Node.js - Render to Image File + +```javascript +import { parse } from "wmap-parser"; +import { renderToImage, ImageType, StageType } from "wmap-renderer-canvas"; +import fs from "fs"; + +const map = parse(wmapSource); + +// PNG +const pngStream = await renderToImage(map, ImageType.PNG); +pngStream.pipe(fs.createWriteStream("map.png")); + +// JPEG, with a stage type override +const jpegStream = await renderToImage(map, ImageType.JPEG, StageType.UBIQUITY); +jpegStream.pipe(fs.createWriteStream("map.jpeg")); + +// PDF, with a stage type override +const pdfStream = await renderToImage(map, ImageType.PDF, StageType.PUBLICATION_TYPES); +pdfStream.pipe(fs.createWriteStream("map.pdf")); +``` + +### Configuration + +All functions accept an optional configuration object that can override +how the map is rendered. See the type definitions for more info on each +value. Here's all the defaults. + +```javascript +{ + options: { + showBackground: true, + smartLabelPositioning: true, + }, + theme: { + colors: { + background: "#FFFFFF", + axis: "#0F261F", + vertex: "#0F261F", + label: "#0F261F", + stageForeground: "#DAE6E3", + stageBackground: "#FFFFFF", + inertia: "#FA2B00", + evolution: "#4F8FE6", + groups: [ + "#4F8FE6", + "#FA2B00", + "#23C17C", + "#FAED8F", + "#FFB3F0", + ], + }, + sizes: { + mapWidth: 1300, + mapHeight: 1000, + padding: 42, + lineWidth: 0.5, + vertexWidth: 25, + vertexHeight: 25, + arrowheadSize: 10, + stageHeight: 100, + }, + fonts: { + family: '"Helvetica Neue", Helvetica, Arial, sans-serif', + axisLabel: 14, + vertexLabel: 12, + note: 12, + }, + lineHeights: { + vertexLabel: 18, + note: 18, + }, + opacity: { + groups: 0.1, + }, + }, +} +``` + +## API + +### `renderToCanvas(map, canvas, stageType, overrides)` + +Renders a parsed Wardley map to a canvas element. + +**Parameters:** + +- `map` (Map): Parsed map object from `wmap-parser` +- `canvas` (HTMLCanvasElement|Canvas): Canvas element (browser) or Canvas instance (Node.js) +- `stageType` (StageType): The type of stage to use. See StageType for all possible values. Defaults to `StageType.ACTIVITIES` +- `overrides` (tConfiguration, optional): + +**Returns:** `Promise<void>` + +**Canvas Size:** The canvas will be automatically sized to 1384×1084 pixels (1300×1000 map + 42px padding on each side). + +### `renderToImage(map, imageType, stageType, overrides)` + +Renders a parsed Wardley map to an image stream. **Node.js only**. + +**Parameters:** + +- `map` (Map): Parsed map object from `wmap-parser` +- `imageType` (ImageType): The image type to use. Can be: `ImageType.PNG`, `ImageType.JPEG`, or `ImageType.PDF` +- `stageType` (StageType): The type of stage to use. See StageType for all possible values. Defaults to `StageType.ACTIVITIES` +- `overrides` (tConfiguration, optional): + +**Returns:** `Promise<Stream>` - Stream of the rendered image + +**Throws:** + +- Error if not in Node.js environment +- Error if `canvas` module cannot be imported. +- Error if `imageType` is not supported + +## Performance + +The drawing is not optimized for live-editing. It's relatively quick to +generate a single image. On an M1 Pro mac, ouexample map renders in about ~7ms, +while a larger map with slightly under 2000 entities does so in ~210ms. + +You can run the benchmarks by using: + +```bash +make benchmark +``` + +## Development + +This project uses a Makefile to run all commands. This is to keep the +commands uniform with the other wmap-parser projects. + +### Running Tests + +```bash +make test +``` + +Or to see coverage + +```bash +make coverage +``` + +### Regenerating Types + +If you change the JSDocs, you'll need to regenerate the types + +```bash +make types +``` + +## License + +AGPL-3.0 + +## See Also + +- [Wardley Maps](https://wardleymaps.com/) - Learn about Wardley Mapping + +[wmap-parser-js]: https://www.npmjs.com/package/wmap-parser diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 0000000..38e4724 --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,39 @@ +import { Bench } from "tinybench"; +import { renderToCanvas, StageType } from "../src/index.js"; + +import { createCanvas } from "canvas"; +import { readFile } from "fs/promises"; +import { parse } from "wmap-parser"; + +// We measure only the canvas rendering. +const exampleSource = await readFile("test/example.wmap", "utf-8"); +const hugeSource = await readFile("test/huge.wmap", "utf-8"); + +const exampleMap = parse(exampleSource); +const hugeMap = parse(hugeSource); +const canvas = createCanvas(1384, 1084); + +const bench = new Bench({ name: "Rendering Benchmark", time: 1000 }); + +bench + .add("With Smart Positioning (Example)", async () => { + await renderToCanvas(exampleMap, canvas); + }) + .add("Without Smart Positioning (Example)", async () => { + await renderToCanvas(exampleMap, canvas, StageType.CERTAINTY, { + smartLabelPositioning: false, + }); + }) + .add("With Smart Positioning (Huge)", async () => { + await renderToCanvas(hugeMap, canvas); + }) + .add("Without Smart Positioning (Huge)", async () => { + await renderToCanvas(hugeMap, canvas, StageType.CERTAINTY, { + smartLabelPositioning: false, + }); + }); + +await bench.run(); + +console.log(bench.name); +console.table(bench.table()); diff --git a/dist/configuration.d.ts b/dist/configuration.d.ts new file mode 100644 index 0000000..af84a53 --- /dev/null +++ b/dist/configuration.d.ts @@ -0,0 +1,179 @@ +/** + * Returns a configuration by extending the defaults with the user overrides. + * + * @param {tConfiguration} [overrides={}] - User overrides. + * @returns {tConfiguration} The extended configuration. + */ +export function extendConfiguration(overrides?: tConfiguration): tConfiguration; +/** + * The configuration options to control the render. + */ +export type tConfiguration = { + /** + * - Options that control how the map is rendered. + */ + options: tConfigurationOptions; + /** + * - Options that control the look of the map. + */ + theme: tConfigurationTheme; +}; +/** + * Options that control how the map is rendered + */ +export type tConfigurationOptions = { + /** + * - Whether to render the background patterns. + */ + showBackground: boolean; + /** + * - Whether to activate smart label positioning. + */ + smartLabelPositioning: boolean; +}; +/** + * Options that control how the map is rendered + */ +export type tConfigurationTheme = { + /** + * - The colors of map elements. + */ + colors: tConfigurationThemeColors; + /** + * - The sizes of map elements. + */ + sizes: tConfigurationThemeSizes; + /** + * - The font family and sizes. + */ + fonts: tConfigurationThemeFonts; + /** + * - The line heights of map text. + */ + lineHeights: tConfigurationThemeLineHeights; + /** + * - Opacity of map elements. + */ + opacity: tConfigurationThemeOpacity; +}; +/** + * The colors of map elements + */ +export type tConfigurationThemeColors = { + /** + * - The color of the background. + */ + background: string; + /** + * : - The color of the axis lines at the edge of the map. + */ + axis: string; + /** + * - The color of component shapes. + */ + vertex: string; + /** + * - The color of component labels. + */ + label: string; + /** + * - The first color of the stage bitmap when the background is drawn. + */ + stageForeground: string; + /** + * - The second color of the stage bitmap when the background is drawn. + */ + stageBackground: string; + /** + * - The color of the inertia element. + */ + inertia: string; + /** + * - The color of the evolution arrow. + */ + evolution: string; + /** + * - A list of colors to use for groups. + */ + groups: Array<string>; +}; +/** + * The sizes of map elements + */ +export type tConfigurationThemeSizes = { + /** + * - The width of the map. + */ + mapWidth: number; + /** + * - The height of the map. + */ + mapHeight: number; + /** + * - The padding around the map for axis labels. + */ + padding: number; + /** + * - The default line width. + */ + lineWidth: number; + /** + * - Width of component shapes. + */ + vertexWidth: number; + /** + * - Height of component shapes. + */ + vertexHeight: number; + /** + * - Size of arrowheads. + */ + arrowheadSize: number; + /** + * - Height of stage lines. + */ + stageHeight: number; +}; +/** + * The sizes of map elements + */ +export type tConfigurationThemeFonts = { + /** + * - The font family to use, in CSS form. + */ + family: string; + /** + * - Font size of the axis labels. + */ + axisLabel: number; + /** + * - Font size of vertex labels. + */ + vertexLabel: number; + /** + * - Font size of note text. + */ + note: number; +}; +/** + * The line heights of map text + */ +export type tConfigurationThemeLineHeights = { + /** + * - Line height of vertex labels. + */ + vertexLabel: number; + /** + * - Line height of note text. + */ + note: number; +}; +/** + * The opacity of map elements. + */ +export type tConfigurationThemeOpacity = { + /** + * - Opacity of the color in groups + */ + groups: number; +}; diff --git a/dist/image_type.d.ts b/dist/image_type.d.ts new file mode 100644 index 0000000..93aa767 --- /dev/null +++ b/dist/image_type.d.ts @@ -0,0 +1,9 @@ +/** + * ImageType + */ +export type ImageType = string; +export namespace ImageType { + let PNG: string; + let JPEG: string; + let PDF: string; +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..5aa1192 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,71 @@ +/** + * @typedef {import('./configuration.js').tConfiguration} tConfiguration + * @typedef {import('./image_type.js').ImageType} ImageType + * @typedef {import('./stage_type.js').StageType} StageType + * @typedef {import('wmap-parser').Map} ParsedMap + * @typedef {import('canvas').Canvas} Canvas + */ +/** + * Render a parsed Wardley map to a canvas element + * Works in both browser and Node.js environments + * + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {HTMLCanvasElement|Canvas} canvas - Canvas element (browser) or Canvas instance (Node.js) + * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map. + * @param {tConfiguration} overrides - User configuration overrides + * @returns {Promise<void>} + * + * @example + * // Browser + * import { parse } from 'wmap-parser'; + * import { renderToCanvas } from 'wmap-renderer-canvas'; + * + * const map = parse(wmapSource); + * const canvas = document.getElementById('myCanvas'); + * await renderToCanvas(map, canvas); + * + * @example + * // Node.js + * import { createCanvas } from 'canvas'; + * import { parse } from 'wmap-parser'; + * import { renderToCanvas } from 'wmap-renderer-canvas'; + * + * const map = parse(wmapSource); + * const canvas = createCanvas(1384, 1084); + * await renderToCanvas(map, canvas); + */ +export function renderToCanvas(map: ParsedMap, canvas: HTMLCanvasElement | Canvas, stageType?: StageType, overrides?: tConfiguration): Promise<void>; +/** + * Render a parsed Wardley map to an image stream + * Only available in Node.js environments + * + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {ImageType} imageType - Output image format ('png', 'jpeg', or 'pdf') + * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map. + * @param {tConfiguration} overrides - User configuration overrides + * @returns {Promise<Stream>} Stream of the rendered image + * + * @throws {Error} If not in Node.js environment + * @throws {Error} If canvas module is not available + * @throws {Error} If imageType is not supported + * + * @example + * import { parse } from 'wmap-parser'; + * import { renderToImage } from 'wmap-renderer-canvas'; + * import fs from 'fs'; + * + * const map = parse(wmapSource); + * const stream = await renderToImage(map, 'png'); + * const out = fs.createWriteStream('map.png'); + * stream.pipe(out); + */ +export function renderToImage(map: ParsedMap, imageType?: ImageType, stageType?: StageType, overrides?: tConfiguration): Promise<Stream>; +export type ImageType = import("./image_type.js").ImageType; +export { ImageType } from "./image_type.js"; +export type StageType = import("./stage_type.js").StageType; +export { StageType } from "./stage_type.js"; +export type tConfiguration = import("./configuration.js").tConfiguration; +export type ParsedMap = import("wmap-parser").Map; +export type Canvas = import("canvas").Canvas; +import { StageType } from "./stage_type.js"; +import { ImageType } from "./image_type.js"; diff --git a/dist/patterns.d.ts b/dist/patterns.d.ts new file mode 100644 index 0000000..bf5188a --- /dev/null +++ b/dist/patterns.d.ts @@ -0,0 +1,16 @@ +/** + * Create a canvas pattern from a pattern definition + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array<number>} patternData - 8x8 pattern array + * @param {number} pixelSize - Size of each pattern pixel + * @param {string} fgColor - Foreground color (for 0 values) + * @param {string} bgColor - Background color (for 1 values) + * @returns {Promise<CanvasPattern>} Canvas pattern + */ +export function createPattern(ctx: CanvasRenderingContext2D, patternData: Array<number>, pixelSize: number, fgColor: string, bgColor: string): Promise<CanvasPattern>; +export namespace patterns { + let stitch: number[]; + let shingles: number[]; + let shadowGrid: number[]; + let wicker: number[]; +} diff --git a/dist/renderer.d.ts b/dist/renderer.d.ts new file mode 100644 index 0000000..eb4102a --- /dev/null +++ b/dist/renderer.d.ts @@ -0,0 +1,12 @@ +/** + * Render a parsed Wardley map to a canvas context + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {CanvasRenderingContext2D} ctx - Canvas context to render to + * @param {StageType} stageType - The labels to use for each stage in the map. + * @param {tConfiguration} configuration - Configuration object + * @returns {Promise<void>} + */ +export function render(map: ParsedMap, ctx: CanvasRenderingContext2D, stageType: StageType, configuration: tConfiguration): Promise<void>; +export type tConfiguration = import("./configuration.js").tConfiguration; +export type StageType = import("./stage_type.js").StageType; +export type ParsedMap = import("wmap-parser").Map; diff --git a/dist/smart-label-positioning.d.ts b/dist/smart-label-positioning.d.ts new file mode 100644 index 0000000..32ba4fd --- /dev/null +++ b/dist/smart-label-positioning.d.ts @@ -0,0 +1,33 @@ +/** + * Calculate optimal label position for a component + * @param {Object} component - Component object with label and coordinates + * @param {Object} mapData - Map data containing all components, edges, notes, etc. + * @param {Object} config - Configuration object + * @param {CanvasRenderingContext2D} ctx - Canvas context for text measurement + * @param {Array} allLines - Pre-collected lines from edges, evolutions, etc. + * @param {Array} noteRects - Pre-collected note rectangles + * @param {Array} inertiaRects - Pre-collected inertia rectangles + * @returns {Object} Position object with x, y coordinates in pixels + */ +export function calculateOptimalLabelPosition(component: any, mapData: any, config: any, ctx: CanvasRenderingContext2D, allLines: any[], noteRects: any[], inertiaRects: any[]): any; +/** + * Collect all lines from edges, evolutions, inertias, and axes + */ +export function collectAllLines(mapData: any, config: any, componentMap: any): { + start: { + x: number; + y: any; + }; + end: { + x: any; + y: any; + }; +}[]; +/** + * Collect all note rectangles + */ +export function collectNoteRects(mapData: any, config: any, ctx: any): any; +/** + * Collect all inertia rectangles + */ +export function collectInertiaRects(mapData: any, config: any, componentMap: any): any; diff --git a/dist/stage_type.d.ts b/dist/stage_type.d.ts new file mode 100644 index 0000000..24a5e77 --- /dev/null +++ b/dist/stage_type.d.ts @@ -0,0 +1,149 @@ +/** + * StageType + */ +export type StageType = tStageType; +export namespace StageType { + namespace BEHAVIOR { + let name: string; + let stages: string[]; + } + namespace CERTAINTY { + let name_1: string; + export { name_1 as name }; + let stages_1: string[]; + export { stages_1 as stages }; + } + namespace COMPARISON { + let name_2: string; + export { name_2 as name }; + let stages_2: string[]; + export { stages_2 as stages }; + } + namespace CYNEFIN { + let name_3: string; + export { name_3 as name }; + let stages_3: string[]; + export { stages_3 as stages }; + } + namespace DATA { + let name_4: string; + export { name_4 as name }; + let stages_4: string[]; + export { stages_4 as stages }; + } + namespace DECISION_DRIVERS { + let name_5: string; + export { name_5 as name }; + let stages_5: string[]; + export { stages_5 as stages }; + } + namespace EFFICIENCY { + let name_6: string; + export { name_6 as name }; + let stages_6: string[]; + export { stages_6 as stages }; + } + namespace FAILURE { + let name_7: string; + export { name_7 as name }; + let stages_7: string[]; + export { stages_7 as stages }; + } + namespace FOCUS_OF_VALUE { + let name_8: string; + export { name_8 as name }; + let stages_8: string[]; + export { stages_8 as stages }; + } + namespace ACTIVITIES { + let name_9: string; + export { name_9 as name }; + let stages_9: string[]; + export { stages_9 as stages }; + } + namespace KNOWLEDGE_MANAGEMENT { + let name_10: string; + export { name_10 as name }; + let stages_10: string[]; + export { stages_10 as stages }; + } + namespace KNOWLEDGE { + let name_11: string; + export { name_11 as name }; + let stages_11: string[]; + export { stages_11 as stages }; + } + namespace MARKET_ACTION { + let name_12: string; + export { name_12 as name }; + let stages_12: string[]; + export { stages_12 as stages }; + } + namespace MARKET_PERCEPTION { + let name_13: string; + export { name_13 as name }; + let stages_13: string[]; + export { stages_13 as stages }; + } + namespace MARKET { + let name_14: string; + export { name_14 as name }; + let stages_14: string[]; + export { stages_14 as stages }; + } + namespace PERCEPTION_IN_INDUSTRY { + let name_15: string; + export { name_15 as name }; + let stages_15: string[]; + export { stages_15 as stages }; + } + namespace EVOLUTION_STAGE { + let name_16: string; + export { name_16 as name }; + let stages_16: string[]; + export { stages_16 as stages }; + } + namespace PRACTICE { + let name_17: string; + export { name_17 as name }; + let stages_17: string[]; + export { stages_17 as stages }; + } + namespace PUBLICATION_TYPES { + let name_18: string; + export { name_18 as name }; + let stages_18: string[]; + export { stages_18 as stages }; + } + namespace UBIQUITY { + let name_19: string; + export { name_19 as name }; + let stages_19: string[]; + export { stages_19 as stages }; + } + namespace UNDERSTANDING { + let name_20: string; + export { name_20 as name }; + let stages_20: string[]; + export { stages_20 as stages }; + } + namespace USER_PERCEPTION { + let name_21: string; + export { name_21 as name }; + let stages_21: string[]; + export { stages_21 as stages }; + } +} +/** + * The definition of a stage type used to analyze different aspects of a map. + */ +export type tStageType = { + /** + * - The name of the stage type + */ + name: string; + /** + * - The four stages + */ + stages: [string, string, string, string]; +}; diff --git a/dist/utils.d.ts b/dist/utils.d.ts new file mode 100644 index 0000000..7769666 --- /dev/null +++ b/dist/utils.d.ts @@ -0,0 +1,21 @@ +/** + * Convert percentage to pixel size + * @param {number} percentage - Percentage value (can be negative) + * @param {number} dimension - Dimension in pixels + * @returns {number} Pixel value + */ +export function percentToPixel(percentage: number, dimension: number): number; +/** + * Get stage values from parsed map or use defaults + * @param {Array} stageOverrides - Stage overrides from parsed map + * @returns {Array<number>} Array of 3 stage values + */ +export function getStageValues(stageOverrides: any[]): Array<number>; +/** + * Creates a render-friendly component map. + * @param {Array<Object>} The list of components + * @param {number} mapWidth - Map width in pixels + * @param {number} mapHeight - Map height in pixels + * @returns {Map} Pixel value + */ +export function createComponentMap(components: any, width: any, height: any): Map<any, any>; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9e4f157 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import prettier from "eslint-config-prettier/flat"; +import globals from "globals"; + +export default defineConfig([ + js.configs.recommended, + { + files: ["**/*.js"], + plugins: { + js, + }, + extends: [js.configs.recommended, prettier], + rules: { + "no-unused-vars": "warn", + }, + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + { + files: ["benchmark/index.js"], + plugins: { + js, + }, + extends: [js.configs.recommended, prettier], + rules: { + "no-unused-vars": "warn", + }, + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, +]); diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb3ed0a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "wmap-renderer-canvas", + "version": "1.0.0", + "description": "Canvas renderer for parsed wardley maps.", + "type": "module", + "main": "src/index.js", + "scripts": {}, + "keywords": [ + "Wardley Maps", + "Wardley", + "wmap", + "Wardley Mapping" + ], + "author": "Rubén Beltrán del RÃo <wmap.renderer@r.bdr.sh>", + "license": "AGPL-3.0", + "packageManager": "pnpm@10.12.1", + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/node": "^25.0.2", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.5.0", + "prettier": "^3.7.4", + "tinybench": "^6.0.0", + "wmap-parser": "^1.0.1" + }, + "dependencies": { + "@turf/concave": "^7.3.1", + "@turf/helpers": "^7.3.1", + "canvas": "^3.2.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..74c068b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1530 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + "@turf/concave": + specifier: ^7.3.1 + version: 7.3.1 + "@turf/helpers": + specifier: ^7.3.1 + version: 7.3.1 + canvas: + specifier: ^3.2.0 + version: 3.2.0 + devDependencies: + "@eslint/js": + specifier: ^9.39.2 + version: 9.39.2 + "@types/node": + specifier: ^25.0.2 + version: 25.0.2 + eslint: + specifier: ^9.39.2 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.2) + globals: + specifier: ^16.5.0 + version: 16.5.0 + prettier: + specifier: ^3.7.4 + version: 3.7.4 + tinybench: + specifier: ^6.0.0 + version: 6.0.0 + wmap-parser: + specifier: ^1.0.1 + version: 1.0.1 + +packages: + "@eslint-community/eslint-utils@4.9.0": + resolution: + { + integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + + "@eslint/config-array@0.21.1": + resolution: + { + integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/config-helpers@0.4.2": + resolution: + { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/core@0.17.0": + resolution: + { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/eslintrc@3.3.3": + resolution: + { + integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/js@9.39.2": + resolution: + { + integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/object-schema@2.1.7": + resolution: + { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/plugin-kit@0.4.1": + resolution: + { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@humanfs/core@0.19.1": + resolution: + { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.7": + resolution: + { + integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, + } + engines: { node: ">=18.18.0" } + + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } + + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } + + "@turf/clone@7.3.1": + resolution: + { + integrity: sha512-r7xDOfw9ohA7PhZW+8X9RMsO4szB4YqkhEROaELJyLtQ1bo8VNFtndpZdE6YHQpD7Pjlvlb6i99q8w1QLisEPg==, + } + + "@turf/concave@7.3.1": + resolution: + { + integrity: sha512-vZWqyAYH4qzOuiqPb+bj2jvpIGzYAH8byUhfFJ2gRFRL3/RfV8jdXL2r0Y6VFScqE6OLVGvtM3ITzXX1/9wTaA==, + } + + "@turf/distance@7.3.1": + resolution: + { + integrity: sha512-DK//doTGgYYjBkcWUywAe7wbZYcdP97hdEJ6rXYVYRoULwGGR3lhY96GNjozg6gaW9q2eSNYnZLpcL5iFVHqgw==, + } + + "@turf/helpers@7.3.1": + resolution: + { + integrity: sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==, + } + + "@turf/invariant@7.3.1": + resolution: + { + integrity: sha512-IdZJfDjIDCLH+Gu2yLFoSM7H23sdetIo5t4ET1/25X8gi3GE2XSqbZwaGjuZgNh02nisBewLqNiJs2bo+hrqZA==, + } + + "@turf/meta@7.3.1": + resolution: + { + integrity: sha512-NWsfOE5RVtWpLQNkfOF/RrYvLRPwwruxhZUV0UFIzHqfiRJ50aO9Y6uLY4bwCUe2TumLJQSR4yaoA72Rmr2mnQ==, + } + + "@turf/tin@7.3.1": + resolution: + { + integrity: sha512-pDtHE8rLXvV4zAC9mWmwToDDda2ZTty8IZqZIoUqTnlf6AJjzF7TJrhoE3a+zukRTUI1wowTFqe2NvwgNX0yew==, + } + + "@types/estree@1.0.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } + + "@types/geojson@7946.0.16": + resolution: + { + integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==, + } + + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } + + "@types/node@25.0.2": + resolution: + { + integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==, + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: + { + integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==, + } + engines: { node: ">=0.4.0" } + hasBin: true + + ajv@6.12.6: + resolution: + { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, + } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + base64-js@1.5.1: + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } + + bl@4.1.0: + resolution: + { + integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==, + } + + brace-expansion@1.1.12: + resolution: + { + integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==, + } + + buffer@5.7.1: + resolution: + { + integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==, + } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } + + canvas@3.2.0: + resolution: + { + integrity: sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==, + } + engines: { node: ^18.12.0 || >= 20.9.0 } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } + + chownr@1.1.4: + resolution: + { + integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==, + } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + commander@2.20.3: + resolution: + { + integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, + } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: + { + integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==, + } + engines: { node: ">=10" } + + deep-extend@0.6.0: + resolution: + { + integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==, + } + engines: { node: ">=4.0.0" } + + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } + + detect-libc@2.1.2: + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } + + end-of-stream@1.4.5: + resolution: + { + integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==, + } + + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } + + eslint-config-prettier@10.1.8: + resolution: + { + integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==, + } + hasBin: true + peerDependencies: + eslint: ">=7.0.0" + + eslint-scope@8.4.0: + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.1: + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint@9.39.2: + resolution: + { + integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + hasBin: true + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + esquery@1.6.0: + resolution: + { + integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, + } + engines: { node: ">=0.10" } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } + + expand-template@2.0.3: + resolution: + { + integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==, + } + engines: { node: ">=6" } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } + + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } + + flatted@3.3.3: + resolution: + { + integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==, + } + + fs-constants@1.0.0: + resolution: + { + integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==, + } + + github-from-package@0.0.0: + resolution: + { + integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==, + } + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } + + globals@14.0.0: + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: ">=18" } + + globals@16.5.0: + resolution: + { + integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, + } + engines: { node: ">=18" } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } + + ieee754@1.2.1: + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, + } + + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } + + import-fresh@3.3.1: + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } + + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + + ini@1.3.8: + resolution: + { + integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, + } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + js-yaml@4.1.1: + resolution: + { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, + } + hasBin: true + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } + + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } + + lodash.merge@4.6.2: + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } + + mimic-response@3.1.0: + resolution: + { + integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==, + } + engines: { node: ">=10" } + + minimatch@3.1.2: + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } + + minimist@1.2.8: + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } + + mkdirp-classic@0.5.3: + resolution: + { + integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + napi-build-utils@2.0.0: + resolution: + { + integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==, + } + + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + + node-abi@3.85.0: + resolution: + { + integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==, + } + engines: { node: ">=10" } + + node-addon-api@7.1.1: + resolution: + { + integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==, + } + + once@1.4.0: + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } + + prebuild-install@7.1.3: + resolution: + { + integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==, + } + engines: { node: ">=10" } + hasBin: true + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } + + prettier@3.7.4: + resolution: + { + integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==, + } + engines: { node: ">=14" } + hasBin: true + + pump@3.0.3: + resolution: + { + integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==, + } + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } + + rc@1.2.8: + resolution: + { + integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==, + } + hasBin: true + + readable-stream@3.6.2: + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, + } + engines: { node: ">= 6" } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } + + safe-buffer@5.2.1: + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, + } + + semver@7.7.3: + resolution: + { + integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, + } + engines: { node: ">=10" } + hasBin: true + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } + + simple-concat@1.0.1: + resolution: + { + integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==, + } + + simple-get@4.0.1: + resolution: + { + integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==, + } + + string_decoder@1.3.0: + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } + + strip-json-comments@2.0.1: + resolution: + { + integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, + } + engines: { node: ">=0.10.0" } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: ">=8" } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } + + tar-fs@2.1.4: + resolution: + { + integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==, + } + + tar-stream@2.2.0: + resolution: + { + integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==, + } + engines: { node: ">=6" } + + tinybench@6.0.0: + resolution: + { + integrity: sha512-BWlWpVbbZXaYjRV0twGLNQO00Zj4HA/sjLOQP2IvzQqGwRGp+2kh7UU3ijyJ3ywFRogYDRbiHDMrUOfaMnN56g==, + } + engines: { node: ">=20.0.0" } + + topojson-client@3.1.0: + resolution: + { + integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==, + } + hasBin: true + + topojson-server@3.0.1: + resolution: + { + integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==, + } + hasBin: true + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + tunnel-agent@0.6.0: + resolution: + { + integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==, + } + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } + + undici-types@7.16.0: + resolution: + { + integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, + } + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } + + util-deprecate@1.0.2: + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } + hasBin: true + + wmap-parser@1.0.1: + resolution: + { + integrity: sha512-0zscjXgFdaD+56gFksdkfT4HPHfld2LXzGV5CxNUPVsAJWiXdkED0vpc4rIooklOBKoU5GEidKxS4AV1q6lvrw==, + } + + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } + + wrappy@1.0.2: + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } + +snapshots: + "@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)": + dependencies: + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 + + "@eslint-community/regexpp@4.12.2": {} + + "@eslint/config-array@0.21.1": + dependencies: + "@eslint/object-schema": 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + "@eslint/config-helpers@0.4.2": + dependencies: + "@eslint/core": 0.17.0 + + "@eslint/core@0.17.0": + dependencies: + "@types/json-schema": 7.0.15 + + "@eslint/eslintrc@3.3.3": + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + "@eslint/js@9.39.2": {} + + "@eslint/object-schema@2.1.7": {} + + "@eslint/plugin-kit@0.4.1": + dependencies: + "@eslint/core": 0.17.0 + levn: 0.4.1 + + "@humanfs/core@0.19.1": {} + + "@humanfs/node@0.16.7": + dependencies: + "@humanfs/core": 0.19.1 + "@humanwhocodes/retry": 0.4.3 + + "@humanwhocodes/module-importer@1.0.1": {} + + "@humanwhocodes/retry@0.4.3": {} + + "@turf/clone@7.3.1": + dependencies: + "@turf/helpers": 7.3.1 + "@types/geojson": 7946.0.16 + tslib: 2.8.1 + + "@turf/concave@7.3.1": + dependencies: + "@turf/clone": 7.3.1 + "@turf/distance": 7.3.1 + "@turf/helpers": 7.3.1 + "@turf/invariant": 7.3.1 + "@turf/meta": 7.3.1 + "@turf/tin": 7.3.1 + "@types/geojson": 7946.0.16 + topojson-client: 3.1.0 + topojson-server: 3.0.1 + tslib: 2.8.1 + + "@turf/distance@7.3.1": + dependencies: + "@turf/helpers": 7.3.1 + "@turf/invariant": 7.3.1 + "@types/geojson": 7946.0.16 + tslib: 2.8.1 + + "@turf/helpers@7.3.1": + dependencies: + "@types/geojson": 7946.0.16 + tslib: 2.8.1 + + "@turf/invariant@7.3.1": + dependencies: + "@turf/helpers": 7.3.1 + "@types/geojson": 7946.0.16 + tslib: 2.8.1 + + "@turf/meta@7.3.1": + dependencies: + "@turf/helpers": 7.3.1 + "@types/geojson": 7946.0.16 + + "@turf/tin@7.3.1": + dependencies: + "@turf/helpers": 7.3.1 + "@types/geojson": 7946.0.16 + tslib: 2.8.1 + + "@types/estree@1.0.8": {} + + "@types/geojson@7946.0.16": {} + + "@types/json-schema@7.0.15": {} + + "@types/node@25.0.2": + dependencies: + undici-types: 7.16.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + canvas@3.2.0: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chownr@1.1.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2): + dependencies: + eslint: 9.39.2 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2: + dependencies: + "@eslint-community/eslint-utils": 4.9.0(eslint@9.39.2) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.1 + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.3 + "@eslint/js": 9.39.2 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.7 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + expand-template@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fs-constants@1.0.0: {} + + github-from-package@0.0.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + has-flag@4.0.0: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + napi-build-utils@2.0.0: {} + + natural-compare@1.4.0: {} + + node-abi@3.85.0: + dependencies: + semver: 7.7.3 + + node-addon-api@7.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.85.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + resolve-from@4.0.0: {} + + safe-buffer@5.2.1: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinybench@6.0.0: {} + + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + + topojson-server@3.0.1: + dependencies: + commander: 2.20.3 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + undici-types@7.16.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wmap-parser@1.0.1: {} + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6af511a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - canvas diff --git a/src/configuration.js b/src/configuration.js new file mode 100644 index 0000000..2c58e88 --- /dev/null +++ b/src/configuration.js @@ -0,0 +1,176 @@ +/** + * The configuration options to control the render. + * @typedef {Object} tConfiguration + * + * @property {tConfigurationOptions} options - Options that control how the map is rendered. + * @property {tConfigurationTheme} theme - Options that control the look of the map. + */ + +/** + * Options that control how the map is rendered + * @typedef {Object} tConfigurationOptions + * + * @property {boolean} showBackground - Whether to render the background patterns. + * @property {boolean} smartLabelPositioning - Whether to activate smart label positioning. + */ + +/** + * Options that control how the map is rendered + * @typedef {Object} tConfigurationTheme + * + * @property {tConfigurationThemeColors} colors - The colors of map elements. + * @property {tConfigurationThemeSizes} sizes - The sizes of map elements. + * @property {tConfigurationThemeFonts} fonts - The font family and sizes. + * @property {tConfigurationThemeLineHeights} lineHeights - The line heights of map text. + * @property {tConfigurationThemeOpacity} opacity - Opacity of map elements. + */ + +/** + * The colors of map elements + * @typedef {Object} tConfigurationThemeColors + * + * @property {string} background - The color of the background. + * @property {string} axis: - The color of the axis lines at the edge of the map. + * @property {string} vertex - The color of component shapes. + * @property {string} label - The color of component labels. + * @property {string} stageForeground - The first color of the stage bitmap when the background is drawn. + * @property {string} stageBackground - The second color of the stage bitmap when the background is drawn. + * @property {string} inertia - The color of the inertia element. + * @property {string} evolution - The color of the evolution arrow. + * @property {Array<string>} groups - A list of colors to use for groups. + */ + +/** + * The sizes of map elements + * @typedef {Object} tConfigurationThemeSizes + * + * @property {number} mapWidth - The width of the map. + * @property {number} mapHeight - The height of the map. + * @property {number} padding - The padding around the map for axis labels. + * @property {number} lineWidth - The default line width. + * @property {number} vertexWidth - Width of component shapes. + * @property {number} vertexHeight - Height of component shapes. + * @property {number} arrowheadSize - Size of arrowheads. + * @property {number} stageHeight - Height of stage lines. + */ + +/** + * The sizes of map elements + * @typedef {Object} tConfigurationThemeFonts + * + * @property {string} family - The font family to use, in CSS form. + * @property {number} axisLabel - Font size of the axis labels. + * @property {number} vertexLabel - Font size of vertex labels. + * @property {number} note - Font size of note text. + */ + +/** + * The line heights of map text + * @typedef {Object} tConfigurationThemeLineHeights + * + * @property {number} vertexLabel - Line height of vertex labels. + * @property {number} note - Line height of note text. + */ + +/** + * The opacity of map elements. + * @typedef {Object} tConfigurationThemeOpacity + * + * @property {number} groups - Opacity of the color in groups + */ + +const internals = { + kDefaults: { + options: { + showBackground: true, + smartLabelPositioning: true, + }, + theme: { + colors: { + background: "#FFFFFF", + axis: "#0F261F", + vertex: "#0F261F", + label: "#0F261F", + stageForeground: "#DAE6E3", + stageBackground: "#FFFFFF", + inertia: "#FA2B00", + evolution: "#4F8FE6", + groups: [ + "#4F8FE6", // Olympic Blue + "#FA2B00", // Jasper Red + "#23C17C", // Light Porcelain Green + "#FAED8F", // Naples Yellow + "#FFB3F0", // Hermosa Pink + ], + }, + sizes: { + mapWidth: 1300, + mapHeight: 1000, + padding: 42, + lineWidth: 0.5, + vertexWidth: 25, + vertexHeight: 25, + arrowheadSize: 10, + stageHeight: 100, + }, + fonts: { + family: '"Helvetica Neue", Helvetica, Arial, sans-serif', + axisLabel: 14, + vertexLabel: 12, + note: 12, + }, + lineHeights: { + vertexLabel: 18, + note: 18, + }, + opacity: { + groups: 0.1, + }, + }, + }, +}; + +/** + * Returns a configuration by extending the defaults with the user overrides. + * + * @param {tConfiguration} [overrides={}] - User overrides. + * @returns {tConfiguration} The extended configuration. + */ +export function extendConfiguration(overrides = {}) { + const configuration = { + options: Object.assign( + {}, + internals.kDefaults.options, + overrides.options, + ), + theme: { + colors: Object.assign( + {}, + internals.kDefaults.theme.colors, + overrides.theme?.colors, + ), + sizes: Object.assign( + {}, + internals.kDefaults.theme.sizes, + overrides.theme?.sizes, + ), + fonts: Object.assign( + {}, + internals.kDefaults.theme.fonts, + overrides.theme?.fonts, + ), + lineHeights: Object.assign( + {}, + internals.kDefaults.theme.lineHeights, + overrides.theme?.lineHeights, + ), + opacity: Object.assign( + {}, + internals.kDefaults.theme.opacity, + overrides.theme?.opacity, + ), + }, + }; + + return configuration; +} diff --git a/src/image_type.js b/src/image_type.js new file mode 100644 index 0000000..c462455 --- /dev/null +++ b/src/image_type.js @@ -0,0 +1,9 @@ +/** + * Image output format for Node.js rendering + * @enum {string} ImageType + */ +export const ImageType = { + PNG: "png", + JPEG: "jpeg", + PDF: "pdf", +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..2a58ad5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,153 @@ +import { render } from "./renderer.js"; +import { extendConfiguration } from "./configuration.js"; +import { ImageType } from "./image_type.js"; +import { StageType } from "./stage_type.js"; + +export { ImageType } from "./image_type.js"; +export { StageType } from "./stage_type.js"; + +/** + * @typedef {import('./configuration.js').tConfiguration} tConfiguration + * @typedef {import('./image_type.js').ImageType} ImageType + * @typedef {import('./stage_type.js').StageType} StageType + * @typedef {import('wmap-parser').Map} ParsedMap + * @typedef {import('canvas').Canvas} Canvas + */ + +/** + * Render a parsed Wardley map to a canvas element + * Works in both browser and Node.js environments + * + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {HTMLCanvasElement|Canvas} canvas - Canvas element (browser) or Canvas instance (Node.js) + * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map. + * @param {tConfiguration} overrides - User configuration overrides + * @returns {Promise<void>} + * + * @example + * // Browser + * import { parse } from 'wmap-parser'; + * import { renderToCanvas } from 'wmap-renderer-canvas'; + * + * const map = parse(wmapSource); + * const canvas = document.getElementById('myCanvas'); + * await renderToCanvas(map, canvas); + * + * @example + * // Node.js + * import { createCanvas } from 'canvas'; + * import { parse } from 'wmap-parser'; + * import { renderToCanvas } from 'wmap-renderer-canvas'; + * + * const map = parse(wmapSource); + * const canvas = createCanvas(1384, 1084); + * await renderToCanvas(map, canvas); + */ +export async function renderToCanvas( + map, + canvas, + stageType = StageType.ACTIVITIES, + overrides = {}, +) { + if (!map) { + throw new Error("Map object is required"); + } + + if (!canvas) { + throw new Error("Canvas is required"); + } + + const configuration = extendConfiguration(overrides); + const { mapWidth, mapHeight, padding } = configuration.theme.sizes; + const totalWidth = mapWidth + padding * 2; + const totalHeight = mapHeight + padding * 2; + + if (canvas.width !== totalWidth || canvas.height !== totalHeight) { + canvas.width = totalWidth; + canvas.height = totalHeight; + } + + const context = canvas.getContext("2d"); + await render(map, context, stageType, configuration); +} + +/** + * Render a parsed Wardley map to an image stream + * Only available in Node.js environments + * + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {ImageType} imageType - Output image format ('png', 'jpeg', or 'pdf') + * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map. + * @param {tConfiguration} overrides - User configuration overrides + * @returns {Promise<Stream>} Stream of the rendered image + * + * @throws {Error} If not in Node.js environment + * @throws {Error} If canvas module is not available + * @throws {Error} If imageType is not supported + * + * @example + * import { parse } from 'wmap-parser'; + * import { renderToImage, ImageType } from 'wmap-renderer-canvas'; + * import fs from 'fs'; + * + * const map = parse(wmapSource); + * const stream = await renderToImage(map, ImageType.PNG); + * const out = fs.createWriteStream('map.png'); + * stream.pipe(out); + */ +export async function renderToImage( + map, + imageType = ImageType.PNG, + stageType = StageType.ACTIVITIES, + overrides = {}, +) { + if (typeof window !== "undefined") { + throw new Error( + "renderToImage is only available in Node.js environments", + ); + } + + if (!map) { + throw new Error("Map object is required"); + } + + const validTypes = ["png", "jpeg", "pdf"]; + if (!validTypes.includes(imageType)) { + throw new Error( + `Invalid image type. Must be one of: ${validTypes.join(", ")}`, + ); + } + + let createCanvas; + try { + const canvasModule = await import("canvas"); + createCanvas = + canvasModule.default?.createCanvas || canvasModule.createCanvas; + } catch { + throw new Error("canvas module is required for renderToImage."); + } + + const configuration = extendConfiguration(overrides); + const { mapWidth, mapHeight, padding } = configuration.theme.sizes; + const totalWidth = mapWidth + padding * 2; + const totalHeight = mapHeight + padding * 2; + + const canvas = + imageType === "pdf" + ? createCanvas(totalWidth, totalHeight, "pdf") + : createCanvas(totalWidth, totalHeight); + const ctx = canvas.getContext("2d"); + + await render(map, ctx, stageType, configuration); + + switch (imageType) { + case "png": + return canvas.createPNGStream(); + case "jpeg": + return canvas.createJPEGStream(); + case "pdf": + return canvas.createPDFStream(); + default: + throw new Error(`Unsupported image type: ${imageType}`); + } +} diff --git a/src/patterns.js b/src/patterns.js new file mode 100644 index 0000000..2960dea --- /dev/null +++ b/src/patterns.js @@ -0,0 +1,74 @@ +/** + * Pattern definitions - 8x8 1-bit patterns + * 0 = foreground color, 1 = background color + */ +export const patterns = { + stitch: [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, + 0, 1, 0, 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, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + ], + shingles: [ + 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, + 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, + ], + shadowGrid: [ + 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, + ], + wicker: [ + 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, + 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, + 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, + ], +}; + +/** + * Create a canvas pattern from a pattern definition + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array<number>} patternData - 8x8 pattern array + * @param {number} pixelSize - Size of each pattern pixel + * @param {string} fgColor - Foreground color (for 0 values) + * @param {string} bgColor - Background color (for 1 values) + * @returns {Promise<CanvasPattern>} Canvas pattern + */ +export async function createPattern( + ctx, + patternData, + pixelSize, + fgColor, + bgColor, +) { + const size = 8; + let patternCanvas; + + const isNode = typeof window === "undefined"; + + if (isNode) { + const { createCanvas } = await import("canvas"); + patternCanvas = createCanvas(size * pixelSize, size * pixelSize); + } else { + patternCanvas = window.document.createElement("canvas"); + patternCanvas.width = size * pixelSize; + patternCanvas.height = size * pixelSize; + } + + const patternCtx = patternCanvas.getContext("2d"); + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const value = patternData[y * size + x]; + patternCtx.fillStyle = value === 0 ? fgColor : bgColor; + patternCtx.fillRect( + x * pixelSize, + y * pixelSize, + pixelSize, + pixelSize, + ); + } + } + + return ctx.createPattern(patternCanvas, "repeat"); +} diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..f2001f0 --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,776 @@ +/** + * @typedef {import('./configuration.js').tConfiguration} tConfiguration + * @typedef {import('./stage_type.js').StageType} StageType + * @typedef {import('wmap-parser').Map} ParsedMap + */ + +import { createPattern, patterns } from "./patterns.js"; +import { createComponentMap, percentToPixel, getStageValues } from "./utils.js"; +import { + calculateOptimalLabelPosition, + collectAllLines, + collectNoteRects, + collectInertiaRects, +} from "./smart-label-positioning.js"; +import concave from "@turf/concave"; +import { featureCollection, point } from "@turf/helpers"; + +// Pattern cache to avoid recreating patterns on every render +const patternCache = new WeakMap(); + +// Offscreen canvas cache for groups +let offscreenCanvasCache = null; +let offscreenCanvasContext = null; + +/** + * Map rendering constants and utilities + */ + +const internals = { + // Padding around the axes + kAxisLabelPadding: 5, + + // The default axis labels + kAxisLabels: { + xAxis: { + leading: "Uncharted", + trailing: "Industrialised", + }, + yAxis: { + top: "Visible", + bottom: "Invisible", + }, + }, +}; + +/** + * Draw the map background with patterns for evolution stages + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array<number>} stages - Stage values + * @param {tConfiguration} configuration - Configuration object + */ +async function drawBackground(ctx, stages, config) { + const { mapWidth, mapHeight } = config.theme.sizes; + const { stageForeground, stageBackground } = config.theme.colors; + const width = mapWidth; + const height = mapHeight; + + // Check cache first + let cachedPatterns = patternCache.get(ctx); + if ( + !cachedPatterns || + cachedPatterns.fg !== stageForeground || + cachedPatterns.bg !== stageBackground + ) { + // Create patterns and cache them + cachedPatterns = { + fg: stageForeground, + bg: stageBackground, + stitch: await createPattern( + ctx, + patterns.stitch, + 1, + stageForeground, + stageBackground, + ), + shingles: await createPattern( + ctx, + patterns.shingles, + 1, + stageForeground, + stageBackground, + ), + shadowGrid: await createPattern( + ctx, + patterns.shadowGrid, + 1, + stageForeground, + stageBackground, + ), + wicker: await createPattern( + ctx, + patterns.wicker, + 1, + stageForeground, + stageBackground, + ), + }; + patternCache.set(ctx, cachedPatterns); + } + + const w = (dim) => percentToPixel(dim, width); + + ctx.fillStyle = cachedPatterns.stitch; + ctx.fillRect(0, 0, w(stages[0]), height); + + ctx.fillStyle = cachedPatterns.shingles; + ctx.fillRect(w(stages[0]), 0, w(stages[1]) - w(stages[0]), height); + + ctx.fillStyle = cachedPatterns.shadowGrid; + ctx.fillRect(w(stages[1]), 0, w(stages[2]) - w(stages[1]), height); + + ctx.fillStyle = cachedPatterns.wicker; + ctx.fillRect(w(stages[2]), 0, width - w(stages[2]), height); +} + +/** + * Draw the map axes (X and Y) + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array<number>} stages - Stage values + * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map. + * @param {tConfiguration} configuration - Configuration object + */ +function drawAxes(ctx, stages, stageType, config) { + const { mapWidth, mapHeight, lineWidth } = config.theme.sizes; + const { axis, label } = config.theme.colors; + const { family: fontFamily, axisLabel } = config.theme.fonts; + const width = mapWidth; + const height = mapHeight; + const xLabels = internals.kAxisLabels.xAxis; + const yLabels = internals.kAxisLabels.yAxis; + + const w = (dim) => percentToPixel(dim, width); + + ctx.strokeStyle = axis; + ctx.lineWidth = lineWidth * 2; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, height); + ctx.lineTo(width, height); + ctx.stroke(); + + ctx.fillStyle = label; + ctx.font = `${axisLabel}px ${fontFamily}`; + ctx.textBaseline = "top"; + + ctx.save(); + ctx.translate(-20, 45); + ctx.rotate(-Math.PI / 2); + ctx.fillText(yLabels.top, 0, 0); + ctx.restore(); + + ctx.save(); + ctx.translate(-20, height); + ctx.rotate(-Math.PI / 2); + ctx.fillText(yLabels.bottom, 0, 0); + ctx.restore(); + + const stageHeight = config.theme.sizes.stageHeight; + + ctx.textAlign = "left"; + ctx.fillText(xLabels.leading, 0, -stageHeight / 4); + ctx.textAlign = "right"; + ctx.fillText(xLabels.trailing, width, -stageHeight / 4); + + ctx.textAlign = "left"; + ctx.fillText(stageType.stages[0], 0, height + internals.kAxisLabelPadding); + ctx.fillText( + stageType.stages[1], + w(stages[0]), + height + internals.kAxisLabelPadding, + ); + ctx.fillText( + stageType.stages[2], + w(stages[1]), + height + internals.kAxisLabelPadding, + ); + ctx.fillText( + stageType.stages[3], + w(stages[2]), + height + internals.kAxisLabelPadding, + ); +} + +/** + * Draw vertical stage lines + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array<number>} stages - Stage values + * @param {tConfiguration} configuration - Configuration object + */ +function drawStages(ctx, stages, config) { + const { mapWidth, mapHeight, lineWidth } = config.theme.sizes; + const { axis } = config.theme.colors; + const width = mapWidth; + const height = mapHeight; + const w = (dim) => percentToPixel(dim, width); + + ctx.strokeStyle = axis; + ctx.lineWidth = lineWidth * 2; + ctx.setLineDash([10, 18]); + ctx.globalAlpha = 1.0; + + ctx.beginPath(); + ctx.moveTo(w(stages[0]), 0); + ctx.lineTo(w(stages[0]), height); + ctx.moveTo(w(stages[1]), 0); + ctx.lineTo(w(stages[1]), height); + ctx.moveTo(w(stages[2]), 0); + ctx.lineTo(w(stages[2]), height); + ctx.stroke(); + + ctx.setLineDash([]); +} + +/** + * Draw a vertex shape + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {number} x - X position in pixels + * @param {number} y - Y position in pixels + * @param {string} shape - Shape type (circle, square, triangle, x) + * @param {tConfiguration} configuration - Configuration object + */ +function drawVertexShape(ctx, x, y, shape, config) { + const { vertexWidth, vertexHeight } = config.theme.sizes; + const { vertex } = config.theme.colors; + const width = vertexWidth; + const height = vertexHeight; + + ctx.fillStyle = vertex; + + switch (shape) { + case "circle": + ctx.beginPath(); + ctx.ellipse( + x + width / 2, + y + height / 2, + width / 2, + height / 2, + 0, + 0, + Math.PI * 2, + ); + ctx.fill(); + break; + + case "square": + ctx.fillRect(x, y, width, height); + break; + + case "triangle": + ctx.beginPath(); + ctx.moveTo(x, y + height); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x + width / 2, y); + ctx.closePath(); + ctx.fill(); + break; + + case "x": + ctx.strokeStyle = vertex; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y + height); + ctx.moveTo(x + width, y); + ctx.lineTo(x, y + height); + ctx.stroke(); + break; + + default: + ctx.beginPath(); + ctx.ellipse( + x + width / 2, + y + height / 2, + width / 2, + height / 2, + 0, + 0, + Math.PI * 2, + ); + ctx.fill(); + } +} + +/** + * Draw map components (vertices) + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} components - Array of component objects + * @param {Object} map - Full map object (for smart label positioning) + * @param {Map} componentMap - Map of component labels to coordinates + * @param {tConfiguration} configuration - Configuration object + */ +function drawVertices(ctx, components, map, componentMap, configuration) { + const { + options: { smartLabelPositioning }, + theme: { + sizes: { mapWidth, mapHeight, vertexWidth }, + colors: { label }, + fonts: { family: fontFamily, vertexLabel: fontSize }, + }, + } = configuration; + + const width = mapWidth; + const height = mapHeight; + const w = (dim) => percentToPixel(dim, width); + const h = (dim) => percentToPixel(dim, height); + + ctx.font = `${fontSize}px ${fontFamily}`; + ctx.fillStyle = label; + ctx.textBaseline = "middle"; + ctx.textAlign = "left"; + + // Collect lines, notes, and inertias once if using smart positioning + let allLines, noteRects, inertiaRects; + if (smartLabelPositioning) { + allLines = collectAllLines(map, configuration, componentMap); + noteRects = collectNoteRects(map, configuration, ctx); + inertiaRects = collectInertiaRects(map, configuration, componentMap); + } + + for (const component of components) { + const [xPercent, yPercent] = component.coordinates; + const x = w(xPercent); + const y = h(yPercent); + + drawVertexShape(ctx, x, y, component.shape, configuration); + + let labelX, labelY; + + if (smartLabelPositioning) { + const position = calculateOptimalLabelPosition( + component, + map, + configuration, + ctx, + allLines, + noteRects, + inertiaRects, + ); + labelX = position.x; + labelY = position.y; + ctx.textBaseline = "top"; + } else { + // Use default positioning (to the right) + labelX = x + vertexWidth + internals.kAxisLabelPadding; + labelY = y + 7; + ctx.textBaseline = "middle"; + } + + ctx.fillText(component.label, labelX, labelY); + } +} + +/** + * Draw dependencies between components + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} dependencies - Array of dependency objects + * @param {Map} componentMap - Map of component labels to coordinates + * @param {tConfiguration} configuration - Configuration object + */ +function drawDependencies(ctx, dependencies, componentMap, configuration) { + const { + theme: { + sizes: { vertexWidth, vertexHeight, lineWidth, arrowheadSize }, + colors: { vertex }, + }, + } = configuration; + + ctx.strokeStyle = vertex; + ctx.lineWidth = lineWidth; + + const halfWidth = vertexWidth / 2; + const halfHeight = vertexHeight / 2; + const arrowAngle = Math.PI / 4; + + for (const dependency of dependencies) { + const fromCoordinates = componentMap[dependency.from.toLowerCase()]; + const toCoordinates = componentMap[dependency.to.toLowerCase()]; + + if (!fromCoordinates || !toCoordinates) continue; + + const originX = fromCoordinates[0] + halfWidth; + const originY = fromCoordinates[1] + halfHeight; + const destX = toCoordinates[0] + halfWidth; + const destY = toCoordinates[1] + halfHeight; + + const angle = Math.atan2(destY - originY, destX - originX); + const cosine = Math.cos(angle); + const sine = Math.sin(angle); + + const offsetOriginX = originX + halfWidth * cosine; + const offsetOriginY = originY + halfHeight * sine; + const offsetDestX = destX - halfWidth * cosine; + const offsetDestY = destY - halfHeight * sine; + + ctx.beginPath(); + ctx.moveTo(offsetOriginX, offsetOriginY); + ctx.lineTo(offsetDestX, offsetDestY); + ctx.stroke(); + + if (dependency.isDirected) { + const upperAngle = angle - arrowAngle; + const lowerAngle = angle + arrowAngle; + + ctx.beginPath(); + ctx.moveTo( + offsetDestX - arrowheadSize * Math.cos(upperAngle), + offsetDestY - arrowheadSize * Math.sin(upperAngle), + ); + ctx.lineTo(offsetDestX, offsetDestY); + ctx.lineTo( + offsetDestX - arrowheadSize * Math.cos(lowerAngle), + offsetDestY - arrowheadSize * Math.sin(lowerAngle), + ); + ctx.stroke(); + } + } +} + +/** + * Draw notes on the map + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} notes - Array of note objects + * @param {tConfiguration} configuration - Configuration object + */ +function drawNotes(ctx, notes, configuration) { + const { + theme: { + sizes: { mapWidth, mapHeight, lineWidth }, + colors: { background, vertex, label }, + fonts: { family: fontFamily, note: fontSize }, + lineHeights: { note: lineHeight }, + }, + } = configuration; + + const w = (dim) => percentToPixel(dim, mapWidth); + const h = (dim) => percentToPixel(dim, mapHeight); + + const maxWidth = 400; + const padding = internals.kAxisLabelPadding; + + ctx.font = `${fontSize}px ${fontFamily}`; + ctx.textBaseline = "top"; + ctx.textAlign = "left"; + ctx.lineWidth = lineWidth; + + // Batch all background fills first + ctx.fillStyle = background; + const noteData = []; + + for (const note of notes) { + const x = w(note.coordinates[0]); + const y = h(note.coordinates[1]); + const lines = note.text.replace(/\\n/g, "\n").split("\n"); + + let widestLine = 0; + for (let i = 0; i < lines.length; i++) { + const lineWidth = ctx.measureText(lines[i]).width; + if (lineWidth > widestLine) widestLine = lineWidth; + } + + const boxWidth = Math.min(maxWidth, widestLine + padding * 2); + const boxHeight = lines.length * lineHeight + padding * 2; + + ctx.fillRect(x, y, boxWidth, boxHeight); + noteData.push({ x, y, boxWidth, boxHeight, lines }); + } + + // Batch all strokes + ctx.strokeStyle = vertex; + for (const { x, y, boxWidth, boxHeight } of noteData) { + ctx.strokeRect(x, y, boxWidth, boxHeight); + } + + // Batch all text + ctx.fillStyle = label; + for (const { x, y, lines } of noteData) { + for (let i = 0; i < lines.length; i++) { + ctx.fillText(lines[i], x + padding, y + padding + i * lineHeight); + } + } +} + +/** + * Draw inertia indicators + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} inertias - Array of inertia objects + * @param {Map} componentMap - Map of component labels to coordinates + * @param {tConfiguration} configuration - Configuration object + */ +function drawInertias(ctx, inertias, componentMap, configuration) { + const { + theme: { + sizes: { vertexWidth, vertexHeight }, + colors: { inertia }, + }, + } = configuration; + + const cornerRadius = 2; + + ctx.fillStyle = inertia; + + for (const inertia of inertias) { + if (!inertia.component) return; + const coordinates = componentMap[inertia.component.toLowerCase()]; + if (!coordinates) return; + + const x = coordinates[0] + 3 * vertexWidth; + const y = coordinates[1] - (vertexHeight * 2) / 3; + const rectWidth = vertexWidth / 2; + const rectHeight = vertexHeight * 2; + + ctx.beginPath(); + ctx.roundRect(x, y, rectWidth, rectHeight, cornerRadius); + ctx.fill(); + } +} + +/** + * Draw evolution arrows (opportunities) + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} evolutions - Array of evolution objects + * @param {Map} componentMap - Map of component labels to coordinates + * @param {tConfiguration} configuration - Configuration object + */ +function drawEvolutions(ctx, evolutions, componentMap, configuration) { + const { + theme: { + sizes: { + mapWidth, + vertexWidth, + vertexHeight, + lineWidth, + arrowheadSize, + }, + colors: { evolution }, + }, + } = configuration; + + ctx.strokeStyle = evolution; + ctx.lineWidth = lineWidth * 2; + ctx.setLineDash([10]); + ctx.globalAlpha = 1.0; + + evolutions.forEach((evolutionItem) => { + const coords = componentMap[evolutionItem.component.toLowerCase()]; + if (!coords) return; + + const originX = coords[0]; + const originY = coords[1]; + const destX = Math.max( + 0, + Math.min( + mapWidth, + originX + percentToPixel(evolutionItem.value, mapWidth), + ), + ); + const destY = originY; + + const multiplier = destX > originX ? 1 : -1; + const upperAngle = -Math.PI / 4; + const lowerAngle = Math.PI / 4; + + const offsetOriginX = + originX + multiplier * (vertexWidth / 2) + vertexWidth / 2; + const offsetOriginY = originY + vertexHeight / 2; + const offsetDestX = + destX - multiplier * (vertexWidth / 2) + vertexWidth / 2; + const offsetDestY = destY + vertexHeight / 2; + + ctx.beginPath(); + ctx.moveTo(offsetOriginX, offsetOriginY); + ctx.lineTo(offsetDestX, offsetDestY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(offsetDestX, offsetDestY); + ctx.lineTo( + offsetDestX - multiplier * arrowheadSize * Math.cos(upperAngle), + offsetDestY - multiplier * arrowheadSize * Math.sin(upperAngle), + ); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(offsetDestX, offsetDestY); + ctx.lineTo( + offsetDestX - multiplier * arrowheadSize * Math.cos(lowerAngle), + offsetDestY - multiplier * arrowheadSize * Math.sin(lowerAngle), + ); + ctx.stroke(); + }); + + ctx.setLineDash([]); + ctx.globalAlpha = 1.0; +} + +/** + * Draw groups with concave hulls + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {Array} groups - Array of group objects + * @param {Map} componentMap - Map of component labels to coordinates + * @param {tConfiguration} configuration - Configuration object + */ +async function drawGroups(ctx, groups, componentMap, configuration) { + const { + theme: { + colors: { groups: groupColors }, + sizes: { mapWidth, mapHeight, vertexWidth, vertexHeight }, + opacity: { groups: groupOpacity }, + }, + } = configuration; + + const width = mapWidth; + const height = mapHeight; + + // Create/reuse offscreen canvas for groups to flatten fill + stroke before applying opacity + const isNode = typeof window === "undefined"; + + if ( + !offscreenCanvasCache || + offscreenCanvasCache.width !== width + vertexWidth || + offscreenCanvasCache.height !== height + vertexHeight + ) { + if (isNode) { + const { createCanvas } = await import("canvas"); + offscreenCanvasCache = createCanvas( + width + vertexWidth, + height + vertexHeight, + ); + } else { + offscreenCanvasCache = document.createElement("canvas"); + offscreenCanvasCache.width = width + vertexWidth; + offscreenCanvasCache.height = height + vertexHeight; + } + offscreenCanvasContext = offscreenCanvasCache.getContext("2d"); + } + + groups.forEach((group, groupIndex) => { + const componentCoords = group.components + .map((label) => componentMap[label.toLowerCase()]) + .filter(Boolean); + + if (componentCoords.length === 0) return; + + const color = groupColors[groupIndex % groupColors.length]; + let coords; + + if (componentCoords.length === 1) { + // For 1 component, create a small circle around it + const [x, y] = componentCoords[0]; + const radius = 3; // Small radius in percentage units + const segments = 16; + coords = []; + for (let i = 0; i <= segments; i++) { + const angle = (i / segments) * 2 * Math.PI; + coords.push([ + x + radius * Math.cos(angle), + y + radius * Math.sin(angle), + ]); + } + } else if (componentCoords.length === 2) { + // For 2 components, create a simple line between them + coords = [componentCoords[0], componentCoords[1]]; + } else { + // For 3+ components, use concave hull + const points = componentCoords.map(([x, y]) => point([x, y])); + const fc = featureCollection(points); + const hull = concave(fc); + + if (!hull) return; + coords = hull.geometry.coordinates[0]; + } + + // Clear the offscreen canvas + offscreenCanvasContext.clearRect( + 0, + 0, + offscreenCanvasCache.width, + offscreenCanvasCache.height, + ); + + // Reset transform and then translate + offscreenCanvasContext.setTransform(1, 0, 0, 1, 0, 0); + offscreenCanvasContext.translate(vertexWidth / 2, vertexHeight / 2); + + // Create path + offscreenCanvasContext.beginPath(); + coords.forEach(([x, y], i) => { + const px = x; + const py = y; + if (i === 0) { + offscreenCanvasContext.moveTo(px, py); + } else { + offscreenCanvasContext.lineTo(px, py); + } + }); + + // For lines (2 components), don't close the path or fill + const isLine = componentCoords.length === 2; + if (!isLine) { + offscreenCanvasContext.closePath(); + offscreenCanvasContext.fillStyle = color; + offscreenCanvasContext.fill(); + } + + // Stroke with thick line + offscreenCanvasContext.strokeStyle = color; + offscreenCanvasContext.lineWidth = 1.75 * vertexWidth; + offscreenCanvasContext.lineCap = "round"; + offscreenCanvasContext.lineJoin = "round"; + offscreenCanvasContext.stroke(); + + // Draw the offscreen canvas to the main canvas with opacity + ctx.save(); + ctx.globalAlpha = groupOpacity; + ctx.drawImage(offscreenCanvasCache, 0, 0); + ctx.restore(); + }); +} + +/** + * Render a parsed Wardley map to a canvas context + * @param {ParsedMap} map - Parsed map object from wmap-parser + * @param {CanvasRenderingContext2D} ctx - Canvas context to render to + * @param {StageType} stageType - The labels to use for each stage in the map. + * @param {tConfiguration} configuration - Configuration object + * @returns {Promise<void>} + */ +export async function render(map, ctx, stageType, configuration) { + const { + options: { showBackground }, + theme: { + colors: { background }, + sizes: { mapWidth, mapHeight, padding }, + }, + } = configuration; + + const stages = getStageValues(map.stages); + + // Create component map once for all functions to use + const componentMap = createComponentMap( + map.components, + mapWidth, + mapHeight, + ); + + // Fill entire canvas with white background BEFORE translate + ctx.fillStyle = background; + const totalWidth = mapWidth + padding * 2; + const totalHeight = mapHeight + padding * 2; + ctx.fillRect(0, 0, totalWidth, totalHeight); + + ctx.save(); + ctx.translate(padding, padding); + + // Fill map area with background color (in case patterns don't fill completely) + ctx.fillStyle = background; + ctx.fillRect(0, 0, mapWidth, mapHeight); + + if (showBackground) { + await drawBackground(ctx, stages, configuration); + } + + drawAxes(ctx, stages, stageType, configuration); + drawStages(ctx, stages, configuration); + drawDependencies(ctx, map.dependencies, componentMap, configuration); + + // Draw groups first (with low opacity) + await drawGroups(ctx, map.groups, componentMap, configuration); + + drawVertices(ctx, map.components, map, componentMap, configuration); + drawInertias(ctx, map.inertias, componentMap, configuration); + drawEvolutions(ctx, map.evolutions, componentMap, configuration); + drawNotes(ctx, map.notes, configuration); + + ctx.restore(); +} diff --git a/src/smart-label-positioning.js b/src/smart-label-positioning.js new file mode 100644 index 0000000..5a48e29 --- /dev/null +++ b/src/smart-label-positioning.js @@ -0,0 +1,524 @@ +/** + * Smart label positioning algorithm + * Finds optimal label positions to avoid collisions with lines and notes + */ + +import { percentToPixel } from "./utils.js"; + +/** + * Calculate optimal label position for a component + * @param {Object} component - Component object with label and coordinates + * @param {Object} mapData - Map data containing all components, edges, notes, etc. + * @param {Object} config - Configuration object + * @param {CanvasRenderingContext2D} ctx - Canvas context for text measurement + * @param {Array} allLines - Pre-collected lines from edges, evolutions, etc. + * @param {Array} noteRects - Pre-collected note rectangles + * @param {Array} inertiaRects - Pre-collected inertia rectangles + * @returns {Object} Position object with x, y coordinates in pixels + */ +export function calculateOptimalLabelPosition( + component, + mapData, + config, + ctx, + allLines, + noteRects, + inertiaRects, +) { + const { mapWidth, mapHeight, vertexWidth, vertexHeight } = + config.theme.sizes; + const { family: fontFamily, vertexLabel: fontSize } = config.theme.fonts; + + const vertexPixelPos = { + x: percentToPixel(component.coordinates[0], mapWidth), + y: percentToPixel(component.coordinates[1], mapHeight), + }; + + // Measure label size + ctx.font = `${fontSize}px ${fontFamily}`; + const actualLabelSize = calculateLabelSize(component.label, ctx, config); + + // Generate 8 candidate positions + const candidatePositions = generateCandidatePositions( + vertexPixelPos, + { width: vertexWidth, height: vertexHeight }, + actualLabelSize, + ); + + // Check default position (right side) first + const defaultPosition = candidatePositions[0]; + const defaultRect = { + x: defaultPosition.x, + y: defaultPosition.y, + width: actualLabelSize.width, + height: actualLabelSize.height, + }; + + if ( + countCollisions(defaultRect, allLines) === 0 && + countNoteCollisions(defaultRect, noteRects) === 0 && + countNoteCollisions(defaultRect, inertiaRects) === 0 + ) { + return defaultPosition; + } + + // Find best position by scoring all candidates + let bestPosition = defaultPosition; + let bestScore = Infinity; + + for (let index = 0; index < candidatePositions.length; index++) { + const position = candidatePositions[index]; + const labelRect = { + x: position.x, + y: position.y, + width: actualLabelSize.width, + height: actualLabelSize.height, + }; + + const collisionCount = countCollisions(labelRect, allLines); + const noteCollisionCount = countNoteCollisions(labelRect, noteRects); + const inertiaCollisionCount = countNoteCollisions( + labelRect, + inertiaRects, + ); + + const distance = Math.sqrt( + Math.pow(position.x - (vertexPixelPos.x + vertexWidth / 2), 2) + + Math.pow(position.y - (vertexPixelPos.y + vertexHeight / 2), 2), + ); + + const ownershipPenalty = calculateOwnershipPenalty( + labelRect, + component, + mapData.components, + config, + ); + + const score = + collisionCount * 100 + + noteCollisionCount * 500 + + inertiaCollisionCount * 300 + + distance * 0.5 + + index * 0.1 + + ownershipPenalty; + + if (score < bestScore) { + bestScore = score; + bestPosition = position; + } + } + + return bestPosition; +} + +/** + * Calculate label size including padding + */ +function calculateLabelSize(text, ctx, config) { + const cleanText = text.replace(/\\n/g, "\n"); + const lines = cleanText.split("\n"); + const lineHeight = config.theme.lineHeights.vertexLabel; + + let maxWidth = 0; + let totalHeight = 0; + + for (const line of lines) { + const metrics = ctx.measureText(line); + maxWidth = Math.max(maxWidth, metrics.width); + totalHeight += lineHeight; + } + + return { + width: Math.ceil(maxWidth) + 4, + height: Math.ceil(totalHeight) + 2, + }; +} + +/** + * Generate 8 candidate positions around the vertex + * Order: right, left, top, bottom, top-right, top-left, bottom-right, bottom-left + */ +function generateCandidatePositions(vertexPixelPos, vertexSize, labelSize) { + const padding = 5; + + return [ + // Right + { + x: vertexPixelPos.x + vertexSize.width + padding, + y: vertexPixelPos.y + (vertexSize.height - labelSize.height) / 2, + }, + // Left + { + x: vertexPixelPos.x - labelSize.width - padding, + y: vertexPixelPos.y + (vertexSize.height - labelSize.height) / 2, + }, + // Top + { + x: vertexPixelPos.x + (vertexSize.width - labelSize.width) / 2, + y: vertexPixelPos.y - labelSize.height - padding, + }, + // Bottom + { + x: vertexPixelPos.x + (vertexSize.width - labelSize.width) / 2, + y: vertexPixelPos.y + vertexSize.height + padding, + }, + // Top-right + { + x: vertexPixelPos.x + vertexSize.width + padding, + y: vertexPixelPos.y - labelSize.height - padding, + }, + // Top-left + { + x: vertexPixelPos.x - labelSize.width - padding, + y: vertexPixelPos.y - labelSize.height - padding, + }, + // Bottom-right + { + x: vertexPixelPos.x + vertexSize.width + padding, + y: vertexPixelPos.y + vertexSize.height + padding, + }, + // Bottom-left + { + x: vertexPixelPos.x - labelSize.width - padding, + y: vertexPixelPos.y + vertexSize.height + padding, + }, + ]; +} + +/** + * Collect all lines from edges, evolutions, inertias, and axes + */ +export function collectAllLines(mapData, config, componentMap) { + const { mapWidth, mapHeight, vertexWidth, vertexHeight } = + config.theme.sizes; + const lines = []; + + // Lines from edges (dependencies) + if (mapData.dependencies) { + mapData.dependencies.forEach((edge) => { + const originCoords = componentMap[edge.origin]; + const destCoords = componentMap[edge.destination]; + if (!originCoords || !destCoords) return; + + const origin = { + x: originCoords[0], + mapWidth, + y: originCoords[1], + mapHeight, + }; + const destination = { + x: destCoords[0], + mapWidth, + y: destCoords[1], + mapHeight, + }; + + const slope = + (destination.y - origin.y) / (destination.x - origin.x); + const angle = Math.atan(slope); + const multiplier = slope < 0 ? -1.0 : 1.0; + + const offsetOrigin = { + x: + origin.x + + multiplier * (vertexWidth / 2.0) * Math.cos(angle), + y: + origin.y + + multiplier * (vertexHeight / 2.0) * Math.sin(angle), + }; + const offsetDestination = { + x: + destination.x - + multiplier * (vertexWidth / 2.0) * Math.cos(angle), + y: + destination.y - + multiplier * (vertexHeight / 2.0) * Math.sin(angle), + }; + + const adjustedOrigin = { + x: offsetOrigin.x + vertexWidth / 2.0, + y: offsetOrigin.y + vertexHeight / 2.0, + }; + const adjustedDestination = { + x: offsetDestination.x + vertexWidth / 2.0, + y: offsetDestination.y + vertexHeight / 2.0, + }; + + lines.push({ start: adjustedOrigin, end: adjustedDestination }); + }); + } + + // Lines from evolutions + if (mapData.evolutions) { + mapData.evolutions.forEach((evolution) => { + const coords = componentMap[evolution.component]; + if (!coords) return; + + const originX = coords[0]; + const originY = coords[1]; + const destX = evolution.value; + + const origin = { + x: originX, + mapWidth, + y: originY, + mapHeight, + }; + const destination = { + x: destX, + mapWidth, + y: originY, + mapHeight, + }; + + const multiplier = destX > originX ? 1.0 : -1.0; + + const offsetOrigin = { + x: origin.x + multiplier * (vertexWidth / 2.0), + y: origin.y, + }; + const offsetDestination = { + x: destination.x - multiplier * (vertexWidth / 2.0), + y: destination.y, + }; + + const adjustedOrigin = { + x: offsetOrigin.x + vertexWidth / 2.0, + y: offsetOrigin.y + vertexHeight / 2.0, + }; + const adjustedDestination = { + x: offsetDestination.x + vertexWidth / 2.0, + y: offsetDestination.y + vertexHeight / 2.0, + }; + + lines.push({ start: adjustedOrigin, end: adjustedDestination }); + }); + } + + // Axis lines (Y-axis and X-axis) + lines.push( + { start: { x: 0, y: 0 }, end: { x: 0, y: mapHeight } }, + { start: { x: 0, y: mapHeight }, end: { x: mapWidth, y: mapHeight } }, + ); + + return lines; +} + +/** + * Collect all note rectangles + */ +export function collectNoteRects(mapData, config, ctx) { + if (!mapData.notes) return []; + + const { mapWidth, mapHeight } = config.theme.sizes; + + return mapData.notes.map((note) => { + const notePixelPos = { + x: percentToPixel(note.coordinates[0], mapWidth), + y: percentToPixel(note.coordinates[1], mapHeight), + }; + const noteSize = calculateLabelSize(note.text, ctx, config); + + return { + x: notePixelPos.x, + y: notePixelPos.y, + width: noteSize.width, + height: noteSize.height, + }; + }); +} + +/** + * Collect all inertia rectangles + */ +export function collectInertiaRects(mapData, config, componentMap) { + if (!mapData.inertias) return []; + + const { vertexWidth, vertexHeight } = config.theme.sizes; + + return mapData.inertias + .map((inertia) => { + if (!inertia.component) return null; + const coordinates = componentMap[inertia.component.toLowerCase()]; + if (!coordinates) return null; + + const x = coordinates[0] + 3 * vertexWidth; + const y = coordinates[1] - (vertexHeight * 2) / 3; + const rectWidth = vertexWidth / 2; + const rectHeight = vertexHeight * 2; + + return { + x, + y, + width: rectWidth, + height: rectHeight, + }; + }) + .filter((rect) => rect !== null); +} + +/** + * Count how many lines intersect with the label rectangle + */ +function countCollisions(labelRect, lines) { + let collisions = 0; + for (const line of lines) { + if (lineIntersectsRect(line, labelRect)) { + collisions++; + } + } + return collisions; +} + +/** + * Count how many notes intersect with the label rectangle + */ +function countNoteCollisions(labelRect, noteRects) { + let collisions = 0; + for (const noteRect of noteRects) { + if (rectsIntersect(labelRect, noteRect)) { + collisions++; + } + } + return collisions; +} + +/** + * Check if two rectangles intersect + */ +function rectsIntersect(rect1, rect2) { + return !( + rect1.x + rect1.width < rect2.x || + rect2.x + rect2.width < rect1.x || + rect1.y + rect1.height < rect2.y || + rect2.y + rect2.height < rect1.y + ); +} + +/** + * Check if a line intersects a rectangle + */ +function lineIntersectsRect(line, rect) { + const rectLines = [ + { + start: { x: rect.x, y: rect.y }, + end: { x: rect.x + rect.width, y: rect.y }, + }, + { + start: { x: rect.x + rect.width, y: rect.y }, + end: { x: rect.x + rect.width, y: rect.y + rect.height }, + }, + { + start: { x: rect.x + rect.width, y: rect.y + rect.height }, + end: { x: rect.x, y: rect.y + rect.height }, + }, + { + start: { x: rect.x, y: rect.y + rect.height }, + end: { x: rect.x, y: rect.y }, + }, + ]; + + for (const rectLine of rectLines) { + if (linesIntersect(line, rectLine)) { + return true; + } + } + + // Check if line endpoints are inside rect + return pointInRect(line.start, rect) || pointInRect(line.end, rect); +} + +/** + * Check if a point is inside a rectangle + */ +function pointInRect(point, rect) { + return ( + point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height + ); +} + +/** + * Check if two line segments intersect + */ +function linesIntersect(line1, line2) { + const x1 = line1.start.x; + const y1 = line1.start.y; + const x2 = line1.end.x; + const y2 = line1.end.y; + const x3 = line2.start.x; + const y3 = line2.start.y; + const x4 = line2.end.x; + const y4 = line2.end.y; + + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + + if (Math.abs(denom) < 1e-10) { + return false; + } + + const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; + const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; + + return t >= 0 && t <= 1 && u >= 0 && u <= 1; +} + +/** + * Calculate penalty if label is closer to another vertex than its own + */ +function calculateOwnershipPenalty( + labelRect, + ownComponent, + allComponents, + config, +) { + if (allComponents.length === 0) return 0; + + const { mapWidth, mapHeight, vertexWidth, vertexHeight } = + config.theme.sizes; + + const labelCenter = { + x: labelRect.x + labelRect.width / 2, + y: labelRect.y + labelRect.height / 2, + }; + + const ownVertexPixelPos = { + x: percentToPixel(ownComponent.coordinates[0], mapWidth), + y: percentToPixel(ownComponent.coordinates[1], mapHeight), + }; + const ownVertexCenter = { + x: ownVertexPixelPos.x + vertexWidth / 2, + y: ownVertexPixelPos.y + vertexHeight / 2, + }; + + const distanceToOwnVertex = Math.sqrt( + Math.pow(labelCenter.x - ownVertexCenter.x, 2) + + Math.pow(labelCenter.y - ownVertexCenter.y, 2), + ); + + for (const otherComponent of allComponents) { + if (otherComponent === ownComponent) continue; + + const otherVertexPixelPos = { + x: percentToPixel(otherComponent.coordinates[0], mapWidth), + y: percentToPixel(otherComponent.coordinates[1], mapHeight), + }; + const otherVertexCenter = { + x: otherVertexPixelPos.x + vertexWidth / 2, + y: otherVertexPixelPos.y + vertexHeight / 2, + }; + + const distanceToOtherVertex = Math.sqrt( + Math.pow(labelCenter.x - otherVertexCenter.x, 2) + + Math.pow(labelCenter.y - otherVertexCenter.y, 2), + ); + + if (distanceToOtherVertex < distanceToOwnVertex) { + return 50.0; + } + } + + return 0; +} diff --git a/src/stage_type.js b/src/stage_type.js new file mode 100644 index 0000000..6b50f73 --- /dev/null +++ b/src/stage_type.js @@ -0,0 +1,187 @@ +/** + * The definition of a stage type used to analyze different aspects of a map. + * @typedef {Object} tStageType + * + * @property {string} name - The name of the stage type + * @property {[string, string, string, string]} stages - The four stages + */ + +/** + * Stage type used to render the image. + * @enum {tStageType} StageType + */ +export const StageType = { + BEHAVIOR: { + name: "Behavior", + stages: [ + "Uncertain when to use", + "Learning when to use", + "Learning through use", + "Known / common usage", + ], + }, + CERTAINTY: { + name: "Certainty", + stages: [ + "Poorly Understood / exploring the unknown", + "Rapid Increase In Learning / discovery becomes refining", + "Rapid increase in use / increasing fit for purpose", + "Commonly understood (in terms of use)", + ], + }, + COMPARISON: { + name: "Comparison", + stages: [ + "Constantly changing / a differential / unstable", + "Learning from others / testing the water / some evidential support", + "Competing models / feature difference / evidential support", + "Essential / any advantage is operational / accepted norm", + ], + }, + CYNEFIN: { + name: "Cynefin", + stages: ["Chaotic", "Complex", "Complicated", "Clear"], + }, + DATA: { + name: "Data", + stages: ["Unmodelled", "Divergent", "Convergent", "Modelled"], + }, + DECISION_DRIVERS: { + name: "Decision Drivers", + stages: [ + "Heritage / culture", + "Analyses & synthesis", + "Analyses & synthesis", + "Previous Experience", + ], + }, + EFFICIENCY: { + name: "Efficiency", + stages: [ + "Reducing the cost of change (experimentation)", + "Reducing cost of waste (Learning)", + "Reducing cost of waste (Learning)", + "Reducing cost of deviation (Volume)", + ], + }, + FAILURE: { + name: "Failure", + stages: [ + "High / tolerated / assumed to be wrong", + "Moderate / unsurprising if wrong but disappointed", + "Not tolerated / focus on constant improvement / assumed to be in the right direction / resistance to changing the model", + "Surprised by failure / focus on operational efficiency", + ], + }, + FOCUS_OF_VALUE: { + name: "Focus Of Value", + stages: [ + "High future worth but immediate investment", + "Seeking ways to profit and a ROI / seeking confirmation of value", + "High profitability per unit / a valuable model / a feeling of understanding / focus on exploitation", + "High volume / reducing margin / important but invisible / an essential component of something more complex", + ], + }, + ACTIVITIES: { + name: "Activities", + stages: [ + "Genesis", + "Custom", + "Product (+rental)", + "Commodity (+utility)", + ], + }, + KNOWLEDGE_MANAGEMENT: { + name: "Knowledge Management", + stages: [ + "Uncertain", + "Learning on use / focused on testing prediction", + "Learning on operation / using prediction / verification", + "Known / accepted", + ], + }, + KNOWLEDGE: { + name: "Knowledge", + stages: ["Concept", "Hypothesis", "Theory", "Accepted"], + }, + MARKET_ACTION: { + name: "Market Action", + stages: [ + "Gambling / driven by gut", + 'Exploring a "found" value', + "Market analysis / listening to customers", + "Metric driven / build what is needed", + ], + }, + MARKET_PERCEPTION: { + name: "Market Perception", + stages: [ + 'Chaotic (non-linear) / domain of the "crazy"', + 'Domain of "experts"', + 'Increasing expectation of use / domain of "professionals"', + "Ordered (appearance of being linear) / trivial / formula to be applied", + ], + }, + MARKET: { + name: "Market", + stages: [ + "Undefined Market", + "Forming Market / an array of competing forms and models of understanding", + "Growing Market / consolidation to a few competing but more accepted forms", + "Mature Market / stabilised to an accepted form", + ], + }, + PERCEPTION_IN_INDUSTRY: { + name: "Perception In Industry", + stages: [ + "Future source of competitive advantage / unpredictable / unknown", + "Seen as a scompetitive advantage / a differential / ROI / case examples", + "Advantage through implementation / features / this model is better than that", + "Cost of doing business / accepted / specific defined models", + ], + }, + EVOLUTION_STAGE: { + name: "Evolution Stage", + stages: ["Stage I", "Stage II", "Stage III", "Stage IV"], + }, + PRACTICE: { + name: "Practice", + stages: ["Novel", "Emerging", "Good", "Best"], + }, + PUBLICATION_TYPES: { + name: "Publication Types", + stages: [ + "Describe the wonder of the thing / the discovery of some marvel / a new land / an unknown frontier", + "Focused on build / construct / awareness and learning / many models of explanation / no accepted forms / a wild west", + "Maintenance / operations / installation / comparison between competing forms / feature analysis", + "Focused on use / increasingly an accepted, almost invisible component", + ], + }, + UBIQUITY: { + name: "Ubiquity", + stages: [ + "Rare", + "Slowly Increasing", + "Rapidly Increasing", + "Widespread in the applicable market / ecosystem", + ], + }, + UNDERSTANDING: { + name: "Understanding", + stages: [ + "Poorly Understood / unpredictable", + "Increasing understanding / development of measures", + "Increasing education / constant refinement of needs / measures", + "Believed to be well defined / stable / measurable", + ], + }, + USER_PERCEPTION: { + name: "User Perception", + stages: [ + "Different / confusing / exciting / surprising / dangerous", + "Leading edge / emerging / unceirtanty over results", + "Increasingly common / disappointed if not used or available / feeling left behind", + "Standard / expected / feeling of shock if not used", + ], + }, +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..8f6458e --- /dev/null +++ b/src/utils.js @@ -0,0 +1,50 @@ +const internals = { + kDefaultStages: [25, 50, 75], +}; + +/** + * Convert percentage to pixel size + * @param {number} percentage - Percentage value (can be negative) + * @param {number} dimension - Dimension in pixels + * @returns {number} Pixel value + */ +export function percentToPixel(percentage, dimension) { + return (percentage * dimension) / 100; +} + +/** + * Get stage values from parsed map or use defaults + * @param {Array} stageOverrides - Stage overrides from parsed map + * @returns {Array<number>} Array of 3 stage values + */ +export function getStageValues(stageOverrides) { + const stages = [...internals.kDefaultStages]; + + if (stageOverrides && stageOverrides.length > 0) { + stageOverrides.forEach((override) => { + const index = { i: 0, ii: 1, iii: 2 }[override.stage]; + if (index !== undefined) { + stages[index] = override.value; + } + }); + } + + return stages; +} + +/** + * Creates a render-friendly component map. + * @param {Array<Object>} The list of components + * @param {number} mapWidth - Map width in pixels + * @param {number} mapHeight - Map height in pixels + * @returns {Map} Pixel value + */ +export function createComponentMap(components, width, height) { + return components.reduce((map, current) => { + map[current.label.toLowerCase()] = [ + percentToPixel(current.coordinates[0], width), + percentToPixel(current.coordinates[1], height), + ]; + return map; + }, {}); +} diff --git a/test-output-no-bg.png b/test-output-no-bg.png Binary files differnew file mode 100644 index 0000000..37f3426 --- /dev/null +++ b/test-output-no-bg.png diff --git a/test-output.jpeg b/test-output.jpeg Binary files differnew file mode 100644 index 0000000..77418d5 --- /dev/null +++ b/test-output.jpeg diff --git a/test-output.pdf b/test-output.pdf Binary files differnew file mode 100644 index 0000000..095c265 --- /dev/null +++ b/test-output.pdf diff --git a/test-output.png b/test-output.png Binary files differnew file mode 100644 index 0000000..a0b059a --- /dev/null +++ b/test-output.png diff --git a/test/example.wmap b/test/example.wmap new file mode 100644 index 0000000..f090da2 --- /dev/null +++ b/test/example.wmap @@ -0,0 +1,122 @@ +Anchor (40, 0) +Tinker (4,30) [x] +Tailor (7.03, 49.21) [Square] +Soldier (28, 15) [Triangle] +AMnchor (28.22, 45.27) +Tonker (67.3, 4.4) [X] +Tailor1 (1,1) [Square] +Tailor2 (10,1) [Square] +Tailor3 (1,10) [Square] +Tailor4 (20,1) [Square] +Tailor5 (1,20) [Square] +Tailor7 (20,10) [Square] +Tailor8 (9.36, 16.97) [Square] +Tailor777(30,10) [Square] +Tailor10 (18.77, 26.25) [Square] +Tailor11 (10,10) [Square] +Tailor12 (30,20) +Tailor13 (31.10, 28.89) +Tailor14 (13.44, 21.48) [Square] +Tailor19 (45, 2) [triangle] +B (50, 50) +Hellele + +TailorX (20, 50) [Square] + +Hello World (90, 20) [x] + +Anchor -> Soldier… this is changing. +Anchor -> A +Masdlajksd -- Tailor13 + +Mabanchorn -- Anchornalsdjalks + +A +--B + +X (58.52, 27.07) + +Tailor11--Tailor12 +Tailor12--Tailor13 +Tailor777--Tailor8 +Tailor777--Tailor7 +Tailor777--Tailor6 +Tailor7-Tailor6 + +Tinker -- Tailor +Soldier -- Tinker + +[Evolution] Tinker +20 +[Inertia] Soldier + +[i] 15 +[ii] 35 +[iii] 80 + +this is kind of ok … a little bit better to be honest. (69.76, 89.84) + +OK. + +This map so far is fst. asdasd asda + +Holy moly, this is still indeed fast. But everything flashes as I type! This is now happening so fast + +Hello my old friend. + +OK so this map should eventually start getting slow too! + +let’s see how long after.asd asda sda + +asda sdasdasd asdasdasdasdasdasd +OK… Well we are typing. asd as asd as dasd a asdasda sdasd asd + + asd asd asd asdasd asdasd +Alright, the whole text is still fa asdlashing. Wow. This is pretty bad. +aas dasd adsa sd a asd asd asdjlkasjd laksdjasd basldjas!!! + +we have deleted this (20, 40) [triangle] + +a(12, 12) +Time to see if we get the same performance loss as withthis the other one. Well I will continue. Hello this is me. and i’m +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +b(75,75) +c(9, 90) + +a -- b +b -- c + +d (45, 88) + +c -- d +d -- a +d -- b + +q (90, 60) +b -- q +q -- a +q -- c + +[Evolution] c +20 +[Evolution] d +10 +[Evolution] a -20 +[Evolution] q +5 + +m(50, 60) +n (65, 65) +o (45, 63) + +m -- o +o -- n +n -- m +o -- c +n -- q +m -- d + +testasd asdas +test2asdasa asdasd asd + +[Note] (5, 35) Notes also influence the placement of\n labels. + +[Group] Tailor2, Tailor7, Tailor11, Tailor12, Tailor10, Anchor, Tinker +[Group] Tailor3, Tailor5, Tailor14pr diff --git a/test/huge.wmap b/test/huge.wmap new file mode 100644 index 0000000..c58f8c3 --- /dev/null +++ b/test/huge.wmap @@ -0,0 +1,1960 @@ +[I] 0.25 +[II] 0.5 +[III] 0.75 +[IV] 1.0 + +Component0 (9.95, 32.86) [X] +Component1 (61.12, 59.35) +Component2 (27.46, 61.14) +Component3 (22.90, 50.28) [Circle] +Component4 (8.84, 45.75) [Square] +Component5 (34.22, 13.13) [Circle] +Component6 (53.14, 32.02) [X] +Component7 (88.66, 94.82) +Component8 (26.58, 14.30) [Square] +Component9 (87.07, 57.55) [X] +Component10 (90.63, 12.80) +Component11 (91.14, 18.89) [X] +Component12 (9.25, 15.82) [Square] +Component13 (23.84, 91.04) [Circle] +Component14 (53.42, 50.15) +Component15 (23.20, 1.09) +Component16 (74.50, 14.99) +Component17 (23.36, 83.91) [Triangle] +Component18 (55.63, 41.97) [Triangle] +Component19 (77.86, 4.08) [X] +Component20 (16.02, 71.88) [X] +Component21 (52.76, 85.80) +Component22 (88.56, 76.18) [Square] +Component23 (87.30, 28.67) +Component24 (63.18, 77.45) +Component25 (64.67, 24.42) +Component26 (77.91, 68.51) [Square] +Component27 (12.78, 49.59) +Component28 (44.77, 15.02) [Circle] +Component29 (58.72, 28.35) [Circle] +Component30 (24.99, 58.90) [X] +Component31 (25.36, 96.80) +Component32 (39.32, 99.80) [Circle] +Component33 (99.43, 39.47) [Circle] +Component34 (81.44, 13.95) +Component35 (16.49, 68.99) +Component36 (7.24, 32.21) +Component37 (3.68, 64.60) [X] +Component38 (86.45, 26.42) [Circle] +Component39 (76.81, 52.14) +Component40 (58.24, 33.89) +Component41 (69.90, 96.05) +Component42 (32.45, 5.36) [Triangle] +Component43 (64.31, 59.01) [Triangle] +Component44 (85.51, 61.89) +Component45 (74.47, 3.03) +Component46 (59.49, 69.43) [Circle] +Component47 (61.68, 28.42) [Square] +Component48 (44.39, 8.48) [X] +Component49 (72.53, 49.68) +Component50 (36.04, 88.93) [Triangle] +Component51 (5.46, 8.03) +Component52 (70.84, 53.51) [X] +Component53 (66.45, 12.27) +Component54 (30.46, 81.10) +Component55 (44.12, 3.73) [X] +Component56 (38.53, 69.63) [Triangle] +Component57 (3.78, 33.06) [Triangle] +Component58 (6.34, 59.23) +Component59 (44.77, 51.09) [Triangle] +Component60 (34.70, 66.24) [Triangle] +Component61 (68.32, 34.01) +Component62 (2.66, 12.55) +Component63 (48.15, 67.40) [X] +Component64 (22.94, 92.05) +Component65 (64.35, 62.72) [X] +Component66 (62.49, 91.18) [Triangle] +Component67 (37.77, 62.48) [Square] +Component68 (61.24, 64.54) +Component69 (8.86, 51.61) [Triangle] +Component70 (4.27, 58.35) [Circle] +Component71 (24.95, 89.36) [X] +Component72 (63.65, 53.32) [Square] +Component73 (71.29, 60.36) [Circle] +Component74 (80.72, 87.52) +Component75 (54.05, 36.18) +Component76 (72.89, 59.69) +Component77 (52.20, 58.81) [Circle] +Component78 (55.08, 77.07) [Triangle] +Component79 (55.84, 53.48) +Component80 (29.05, 95.03) +Component81 (49.75, 3.88) +Component82 (45.87, 5.24) +Component83 (52.70, 54.81) +Component84 (77.16, 2.60) [Triangle] +Component85 (57.90, 6.89) [Square] +Component86 (31.42, 51.43) [Square] +Component87 (84.99, 31.15) +Component88 (97.75, 43.72) [Triangle] +Component89 (62.25, 37.18) [X] +Component90 (47.25, 5.86) [X] +Component91 (44.99, 84.43) [Triangle] +Component92 (83.02, 78.80) [X] +Component93 (86.53, 22.19) [X] +Component94 (29.50, 81.88) [Circle] +Component95 (83.80, 12.03) [Circle] +Component96 (6.33, 24.74) +Component97 (96.48, 25.86) +Component98 (35.34, 79.18) +Component99 (58.59, 46.39) [Square] +Component100 (87.06, 39.03) [Circle] +Component101 (87.26, 56.31) +Component102 (12.81, 16.15) +Component103 (68.88, 6.87) [Square] +Component104 (81.35, 46.02) +Component105 (78.60, 19.95) +Component106 (11.24, 77.61) [Circle] +Component107 (19.09, 51.26) +Component108 (76.93, 23.58) +Component109 (1.85, 22.92) +Component110 (18.90, 65.14) [Square] +Component111 (24.65, 75.25) +Component112 (1.87, 54.60) +Component113 (88.58, 63.71) +Component114 (97.17, 48.17) [Square] +Component115 (51.49, 11.93) [Square] +Component116 (17.79, 63.02) [X] +Component117 (89.77, 18.04) +Component118 (18.87, 79.15) +Component119 (8.76, 76.67) +Component120 (81.32, 63.54) +Component121 (92.71, 98.97) +Component122 (16.09, 28.54) [X] +Component123 (48.43, 97.65) +Component124 (50.37, 37.20) +Component125 (50.74, 43.05) [Square] +Component126 (75.40, 23.34) +Component127 (73.92, 79.49) [Circle] +Component128 (51.55, 20.51) [Circle] +Component129 (70.45, 13.55) [Square] +Component130 (57.22, 85.64) [X] +Component131 (64.87, 52.42) +Component132 (33.35, 93.91) [Square] +Component133 (73.38, 47.59) +Component134 (65.04, 86.19) +Component135 (68.33, 7.91) [Triangle] +Component136 (64.96, 64.20) [Circle] +Component137 (12.52, 38.35) +Component138 (10.65, 57.44) [Circle] +Component139 (14.10, 12.62) +Component140 (52.85, 26.35) [Triangle] +Component141 (99.33, 72.83) [X] +Component142 (58.66, 65.70) [X] +Component143 (42.10, 3.23) [Triangle] +Component144 (36.20, 47.76) [Circle] +Component145 (75.65, 26.66) [Square] +Component146 (21.33, 14.84) +Component147 (21.88, 24.88) [Triangle] +Component148 (55.89, 55.79) [Triangle] +Component149 (10.73, 30.74) [Triangle] +Component150 (13.37, 91.16) +Component151 (83.17, 21.74) +Component152 (95.03, 90.75) +Component153 (84.26, 35.34) [X] +Component154 (94.45, 87.23) [Triangle] +Component155 (36.59, 75.57) [Circle] +Component156 (42.65, 46.60) [Square] +Component157 (6.47, 56.77) [Square] +Component158 (44.90, 59.64) [Square] +Component159 (76.18, 37.02) [Circle] +Component160 (95.09, 86.50) +Component161 (55.14, 41.38) [Triangle] +Component162 (60.83, 35.01) [Circle] +Component163 (10.27, 67.00) [Square] +Component164 (76.09, 65.99) [Square] +Component165 (49.23, 79.02) [Circle] +Component166 (73.02, 29.83) +Component167 (54.87, 64.16) [X] +Component168 (52.68, 30.53) [Circle] +Component169 (29.26, 87.91) [Triangle] +Component170 (76.81, 44.95) [Triangle] +Component171 (78.63, 55.50) [Circle] +Component172 (72.66, 55.50) [X] +Component173 (27.73, 64.42) [Triangle] +Component174 (70.81, 23.22) +Component175 (87.42, 5.68) +Component176 (98.01, 65.20) [Triangle] +Component177 (94.00, 8.83) [Circle] +Component178 (80.21, 42.24) +Component179 (10.48, 27.78) [Circle] +Component180 (24.51, 49.49) [Square] +Component181 (43.33, 70.82) +Component182 (11.32, 32.01) [Circle] +Component183 (99.25, 72.09) +Component184 (48.90, 59.71) +Component185 (57.10, 16.75) [Circle] +Component186 (83.37, 34.12) [Triangle] +Component187 (8.87, 29.27) +Component188 (65.57, 61.54) +Component189 (51.83, 71.22) [X] +Component190 (27.04, 61.86) [Circle] +Component191 (6.69, 41.47) +Component192 (23.48, 62.15) +Component193 (2.86, 64.07) +Component194 (69.27, 74.67) [Circle] +Component195 (66.48, 9.12) [Square] +Component196 (28.76, 87.81) +Component197 (26.97, 6.71) +Component198 (15.19, 94.43) [Triangle] +Component199 (57.23, 82.51) [Triangle] +Component200 (42.97, 37.53) +Component201 (29.44, 2.29) +Component202 (76.46, 16.24) [Circle] +Component203 (94.20, 17.90) [Circle] +Component204 (30.13, 51.44) [Triangle] +Component205 (68.98, 48.39) +Component206 (24.23, 62.30) [Triangle] +Component207 (24.83, 48.06) +Component208 (8.33, 26.57) [X] +Component209 (77.02, 35.30) [X] +Component210 (74.96, 30.66) [Circle] +Component211 (81.52, 58.49) +Component212 (69.30, 92.38) [Circle] +Component213 (40.11, 41.45) [Square] +Component214 (73.47, 31.71) [X] +Component215 (71.39, 7.86) [Triangle] +Component216 (77.89, 60.06) [Circle] +Component217 (44.90, 74.67) [Circle] +Component218 (16.20, 4.97) +Component219 (69.28, 44.39) +Component220 (0.28, 68.43) [Circle] +Component221 (8.56, 43.62) +Component222 (5.11, 15.54) [X] +Component223 (12.17, 11.72) +Component224 (54.72, 10.39) [X] +Component225 (30.14, 65.32) [Circle] +Component226 (81.92, 66.20) [Circle] +Component227 (43.65, 77.82) [Triangle] +Component228 (56.60, 57.50) +Component229 (28.99, 49.69) [Circle] +Component230 (0.07, 44.12) +Component231 (7.19, 10.93) [X] +Component232 (54.32, 11.62) [X] +Component233 (10.16, 30.70) [Triangle] +Component234 (52.36, 88.84) [Square] +Component235 (29.22, 81.16) [Square] +Component236 (56.28, 34.89) +Component237 (70.98, 18.55) [Triangle] +Component238 (95.70, 52.33) [X] +Component239 (86.93, 77.86) [Triangle] +Component240 (43.57, 61.46) [Circle] +Component241 (63.10, 56.33) [Circle] +Component242 (90.98, 98.11) +Component243 (43.27, 79.39) [Triangle] +Component244 (14.72, 5.66) +Component245 (92.51, 69.32) [Square] +Component246 (78.17, 82.06) [X] +Component247 (87.01, 27.90) [Triangle] +Component248 (35.33, 38.07) +Component249 (74.21, 13.77) [Circle] +Component250 (94.92, 12.62) +Component251 (85.66, 59.81) +Component252 (96.82, 40.39) [X] +Component253 (53.31, 54.76) [X] +Component254 (78.65, 34.31) +Component255 (63.15, 35.91) [X] +Component256 (93.70, 14.87) [Square] +Component257 (87.74, 38.01) [X] +Component258 (59.70, 23.06) [Triangle] +Component259 (24.24, 39.58) [Triangle] +Component260 (70.42, 43.34) +Component261 (11.55, 10.52) [Circle] +Component262 (44.08, 56.93) [X] +Component263 (67.31, 47.53) [Circle] +Component264 (37.36, 90.08) +Component265 (66.67, 53.36) [Triangle] +Component266 (29.99, 41.40) +Component267 (41.91, 74.03) [Triangle] +Component268 (19.68, 31.07) [Triangle] +Component269 (22.36, 32.00) +Component270 (91.55, 6.41) [Square] +Component271 (44.01, 17.22) [Square] +Component272 (30.74, 29.46) +Component273 (94.66, 90.42) [Triangle] +Component274 (72.88, 98.16) [X] +Component275 (67.60, 71.55) +Component276 (15.18, 43.38) [Triangle] +Component277 (35.33, 39.53) +Component278 (94.24, 15.72) [Square] +Component279 (7.41, 46.96) [X] +Component280 (89.81, 55.32) +Component281 (46.56, 78.51) [Circle] +Component282 (82.99, 9.68) [Triangle] +Component283 (47.47, 92.82) [X] +Component284 (13.31, 26.85) [Square] +Component285 (46.03, 43.09) +Component286 (2.34, 57.09) +Component287 (49.65, 31.24) [Circle] +Component288 (83.79, 57.32) +Component289 (6.61, 42.28) [Square] +Component290 (47.25, 10.20) [Triangle] +Component291 (20.57, 45.07) [Square] +Component292 (15.35, 49.21) [Circle] +Component293 (72.98, 80.41) [Triangle] +Component294 (40.77, 79.66) +Component295 (91.69, 47.22) [X] +Component296 (10.54, 48.39) [X] +Component297 (71.89, 61.99) [Square] +Component298 (10.28, 43.01) +Component299 (53.88, 72.55) +Component300 (9.35, 56.50) +Component301 (28.09, 6.29) [Circle] +Component302 (58.57, 73.35) +Component303 (84.96, 51.81) +Component304 (83.02, 53.58) [X] +Component305 (79.51, 51.53) [X] +Component306 (14.24, 99.49) +Component307 (8.57, 85.13) +Component308 (43.93, 30.83) +Component309 (80.62, 29.87) [X] +Component310 (68.72, 48.61) [X] +Component311 (2.37, 81.53) +Component312 (72.73, 68.09) [Circle] +Component313 (30.74, 2.94) [Circle] +Component314 (62.13, 68.91) [X] +Component315 (73.38, 43.60) [Circle] +Component316 (27.37, 98.57) +Component317 (85.82, 67.69) [Circle] +Component318 (45.24, 69.35) [Circle] +Component319 (2.34, 66.07) [X] +Component320 (67.45, 72.20) +Component321 (88.21, 91.50) [Triangle] +Component322 (47.99, 43.92) +Component323 (40.33, 85.89) +Component324 (1.64, 52.02) [Square] +Component325 (82.44, 91.99) [Square] +Component326 (64.93, 6.74) +Component327 (69.13, 32.76) +Component328 (18.83, 7.75) [X] +Component329 (83.61, 30.14) [Square] +Component330 (49.46, 98.58) +Component331 (53.31, 1.37) [Circle] +Component332 (15.54, 23.31) [Circle] +Component333 (81.39, 44.90) +Component334 (40.59, 17.35) [Square] +Component335 (96.35, 80.15) [Circle] +Component336 (57.23, 15.80) [X] +Component337 (69.45, 58.61) +Component338 (75.96, 32.46) [X] +Component339 (6.37, 65.81) [Triangle] +Component340 (59.14, 70.50) +Component341 (79.03, 41.50) [Square] +Component342 (35.41, 80.78) +Component343 (87.18, 45.70) +Component344 (81.33, 58.50) +Component345 (50.63, 17.49) [Square] +Component346 (10.10, 83.23) +Component347 (74.57, 16.10) [X] +Component348 (42.80, 0.78) +Component349 (58.17, 89.85) [Triangle] +Component350 (80.79, 52.51) [X] +Component351 (56.34, 15.08) [Circle] +Component352 (98.43, 1.47) [Circle] +Component353 (19.88, 99.79) [Circle] +Component354 (51.99, 2.67) [Square] +Component355 (44.85, 23.76) [Square] +Component356 (14.24, 98.11) [Circle] +Component357 (25.68, 57.61) [Square] +Component358 (5.75, 71.94) [Square] +Component359 (68.05, 63.63) [Square] +Component360 (12.81, 46.56) [Square] +Component361 (56.35, 20.51) [Square] +Component362 (43.13, 82.54) [Circle] +Component363 (72.98, 59.02) [Triangle] +Component364 (67.80, 56.66) [Circle] +Component365 (58.12, 58.28) [Square] +Component366 (81.73, 0.72) +Component367 (22.11, 82.65) +Component368 (98.81, 19.86) [X] +Component369 (17.27, 57.91) +Component370 (71.83, 47.45) +Component371 (44.85, 0.45) [Square] +Component372 (57.89, 51.13) [Circle] +Component373 (63.17, 2.05) [Square] +Component374 (44.72, 91.15) +Component375 (71.04, 46.51) [Triangle] +Component376 (3.32, 46.78) [Triangle] +Component377 (72.48, 23.81) [Circle] +Component378 (98.32, 26.85) +Component379 (23.27, 92.77) [X] +Component380 (70.01, 22.45) [Square] +Component381 (86.92, 0.65) [X] +Component382 (0.25, 21.60) [Square] +Component383 (3.44, 79.21) +Component384 (35.36, 27.11) [X] +Component385 (45.80, 29.73) [Triangle] +Component386 (21.58, 74.37) [X] +Component387 (76.83, 29.78) +Component388 (11.95, 69.94) +Component389 (10.05, 71.48) +Component390 (11.26, 80.96) +Component391 (89.52, 30.54) [Square] +Component392 (59.05, 40.71) [Triangle] +Component393 (60.09, 97.03) [X] +Component394 (17.57, 50.48) [X] +Component395 (36.74, 53.99) [Triangle] +Component396 (70.58, 10.19) +Component397 (70.27, 24.32) [Circle] +Component398 (96.61, 90.28) [Triangle] +Component399 (85.63, 16.61) [X] +Component400 (22.40, 43.77) [Circle] +Component401 (31.86, 93.15) [Triangle] +Component402 (10.49, 77.04) [X] +Component403 (61.76, 14.09) [Circle] +Component404 (14.98, 44.56) [Square] +Component405 (1.37, 41.79) +Component406 (66.89, 99.29) +Component407 (54.72, 26.16) [Circle] +Component408 (4.02, 78.88) [X] +Component409 (61.57, 55.03) [X] +Component410 (15.00, 72.61) [Circle] +Component411 (30.84, 58.13) +Component412 (74.62, 34.29) +Component413 (87.16, 42.02) +Component414 (42.56, 13.70) [Triangle] +Component415 (63.00, 19.68) [Circle] +Component416 (84.00, 85.81) [Circle] +Component417 (99.85, 26.55) +Component418 (2.32, 39.70) +Component419 (50.24, 60.73) +Component420 (50.87, 91.30) [Square] +Component421 (89.47, 44.46) +Component422 (57.31, 31.98) [Circle] +Component423 (35.68, 39.96) [Circle] +Component424 (25.96, 65.24) +Component425 (18.03, 51.91) +Component426 (29.60, 99.06) [Triangle] +Component427 (19.24, 90.42) +Component428 (41.83, 16.49) [X] +Component429 (23.20, 79.00) [Square] +Component430 (58.74, 20.49) [Circle] +Component431 (12.51, 51.74) [Triangle] +Component432 (36.63, 90.02) [Square] +Component433 (2.31, 38.41) +Component434 (71.42, 22.16) +Component435 (19.13, 72.21) [Triangle] +Component436 (96.97, 89.38) +Component437 (36.78, 84.85) +Component438 (41.40, 18.25) +Component439 (99.42, 98.89) [X] +Component440 (65.90, 64.04) [Triangle] +Component441 (66.50, 54.30) +Component442 (25.00, 1.22) +Component443 (91.63, 77.36) [X] +Component444 (91.54, 35.26) [Square] +Component445 (68.43, 4.16) [Circle] +Component446 (51.33, 47.47) [Circle] +Component447 (19.82, 70.34) [Triangle] +Component448 (87.82, 97.43) +Component449 (47.41, 73.13) [X] +Component450 (9.65, 4.74) [Circle] +Component451 (16.64, 31.95) [Circle] +Component452 (98.44, 49.75) [Triangle] +Component453 (12.91, 75.20) +Component454 (68.13, 57.99) +Component455 (88.52, 73.61) [Circle] +Component456 (43.00, 47.91) +Component457 (67.28, 76.60) [Circle] +Component458 (30.11, 20.41) [Triangle] +Component459 (32.38, 7.73) +Component460 (48.65, 59.39) [Circle] +Component461 (95.93, 13.43) [Circle] +Component462 (76.05, 97.64) [Triangle] +Component463 (10.57, 5.86) +Component464 (15.16, 65.67) [Triangle] +Component465 (49.29, 37.41) +Component466 (62.86, 55.21) +Component467 (41.70, 22.98) [X] +Component468 (11.40, 42.51) [Square] +Component469 (99.96, 9.88) [X] +Component470 (74.36, 18.50) [Triangle] +Component471 (27.71, 2.13) [X] +Component472 (37.76, 41.14) +Component473 (54.41, 69.25) [Triangle] +Component474 (19.69, 0.90) +Component475 (33.44, 22.53) [X] +Component476 (78.46, 53.81) +Component477 (27.36, 94.83) [X] +Component478 (53.98, 15.10) [Triangle] +Component479 (49.75, 90.23) [Square] +Component480 (26.13, 83.45) +Component481 (75.99, 39.97) +Component482 (17.03, 54.73) [Triangle] +Component483 (10.89, 86.65) +Component484 (37.28, 90.48) +Component485 (59.96, 76.87) [Triangle] +Component486 (80.36, 98.15) +Component487 (9.26, 96.01) +Component488 (62.92, 30.57) [Square] +Component489 (50.52, 20.80) [X] +Component490 (23.88, 11.84) +Component491 (77.93, 5.51) +Component492 (56.88, 8.00) +Component493 (86.70, 55.53) [Square] +Component494 (62.22, 67.78) [Triangle] +Component495 (50.63, 55.06) [Circle] +Component496 (6.86, 22.43) [X] +Component497 (93.32, 40.43) [Circle] +Component498 (96.81, 51.60) +Component499 (59.56, 57.21) [Square] +Component500 (2.54, 51.36) +Component501 (88.77, 48.72) [Square] +Component502 (80.17, 78.87) [Circle] +Component503 (63.34, 14.75) [Square] +Component504 (21.89, 35.77) [Circle] +Component505 (17.52, 41.69) [X] +Component506 (59.66, 93.27) [X] +Component507 (44.93, 75.12) [Square] +Component508 (2.75, 88.38) [Triangle] +Component509 (21.28, 6.63) [Square] +Component510 (86.03, 50.85) [Circle] +Component511 (44.85, 98.92) [Square] +Component512 (82.29, 88.28) +Component513 (99.39, 16.41) [Square] +Component514 (38.85, 11.93) [Circle] +Component515 (79.06, 95.86) [X] +Component516 (0.49, 66.17) [X] +Component517 (34.87, 73.24) +Component518 (57.08, 37.29) +Component519 (59.70, 27.17) +Component520 (26.20, 98.63) [Square] +Component521 (73.63, 43.73) [X] +Component522 (52.00, 34.57) [X] +Component523 (61.51, 25.40) [Triangle] +Component524 (49.91, 28.64) +Component525 (5.83, 22.08) [X] +Component526 (62.78, 28.57) +Component527 (17.11, 32.92) [X] +Component528 (76.93, 82.48) [Circle] +Component529 (18.24, 51.31) +Component530 (35.40, 4.42) +Component531 (65.91, 71.77) [X] +Component532 (51.22, 26.10) [X] +Component533 (32.79, 13.41) [X] +Component534 (91.34, 95.42) [X] +Component535 (84.52, 23.28) [Circle] +Component536 (46.75, 70.21) +Component537 (73.34, 99.55) [Triangle] +Component538 (21.28, 97.53) [Square] +Component539 (53.13, 34.63) [Circle] +Component540 (45.98, 67.68) +Component541 (76.47, 11.65) [Square] +Component542 (8.72, 99.30) [Square] +Component543 (40.19, 84.35) [Square] +Component544 (54.66, 66.40) [Triangle] +Component545 (30.53, 31.73) [Square] +Component546 (72.43, 41.03) +Component547 (77.05, 93.93) +Component548 (84.24, 4.94) +Component549 (48.93, 2.90) [Square] +Component550 (22.94, 7.40) +Component551 (91.81, 28.17) +Component552 (12.27, 44.62) [Circle] +Component553 (85.09, 90.79) [Circle] +Component554 (14.90, 44.18) [X] +Component555 (77.18, 32.00) [X] +Component556 (90.01, 54.51) +Component557 (21.88, 92.41) [Circle] +Component558 (3.92, 26.93) [X] +Component559 (17.08, 7.74) +Component560 (96.04, 1.34) [Circle] +Component561 (32.55, 12.12) [X] +Component562 (32.12, 73.08) [Triangle] +Component563 (34.09, 73.03) [Triangle] +Component564 (83.10, 17.21) [Triangle] +Component565 (47.20, 62.09) [Square] +Component566 (66.85, 41.75) [X] +Component567 (56.32, 73.47) [Square] +Component568 (63.74, 14.66) +Component569 (29.04, 21.96) [X] +Component570 (40.40, 10.86) +Component571 (5.16, 84.97) [Circle] +Component572 (38.42, 2.42) [Square] +Component573 (85.44, 74.93) [Square] +Component574 (87.91, 0.60) [Square] +Component575 (26.21, 7.64) [X] +Component576 (74.56, 16.79) [Square] +Component577 (77.85, 68.39) +Component578 (62.92, 87.85) [X] +Component579 (70.76, 28.86) [Circle] +Component580 (82.86, 24.47) [X] +Component581 (4.56, 9.62) [Triangle] +Component582 (37.45, 94.56) [Circle] +Component583 (12.14, 30.34) [X] +Component584 (3.95, 57.21) [Square] +Component585 (38.37, 72.37) +Component586 (25.84, 82.02) +Component587 (23.21, 59.31) [Square] +Component588 (45.93, 79.55) +Component589 (55.65, 75.30) [Square] +Component590 (75.80, 51.42) +Component591 (12.33, 87.97) +Component592 (76.12, 25.18) [X] +Component593 (42.48, 48.63) +Component594 (83.10, 44.41) [Triangle] +Component595 (85.27, 79.81) [Triangle] +Component596 (2.29, 76.07) +Component597 (79.85, 49.82) +Component598 (48.10, 41.86) [X] +Component599 (30.99, 12.78) [Circle] +Component600 (85.67, 2.54) [Circle] +Component601 (34.27, 62.27) +Component602 (64.46, 94.92) [Triangle] +Component603 (39.70, 37.16) +Component604 (17.25, 23.20) [Circle] +Component605 (84.77, 93.12) [Circle] +Component606 (20.36, 65.56) +Component607 (40.75, 34.65) +Component608 (78.99, 67.22) [X] +Component609 (33.60, 63.19) [X] +Component610 (96.15, 23.34) [X] +Component611 (94.94, 23.74) +Component612 (18.31, 38.34) [Triangle] +Component613 (60.20, 31.49) [Square] +Component614 (71.73, 44.16) [X] +Component615 (93.85, 69.80) [Triangle] +Component616 (4.89, 51.46) +Component617 (92.34, 23.59) [Square] +Component618 (79.12, 23.87) [X] +Component619 (5.24, 26.52) [Circle] +Component620 (66.10, 17.05) [X] +Component621 (80.15, 91.21) +Component622 (60.56, 35.33) +Component623 (38.84, 73.56) [X] +Component624 (32.94, 65.46) [Square] +Component625 (72.03, 93.52) +Component626 (51.66, 1.88) +Component627 (43.42, 69.63) +Component628 (68.93, 78.58) [Triangle] +Component629 (12.17, 88.50) [Square] +Component630 (25.74, 39.64) +Component631 (89.94, 17.59) [Triangle] +Component632 (18.03, 5.97) [Square] +Component633 (63.08, 82.42) [Circle] +Component634 (64.06, 7.17) [Circle] +Component635 (30.33, 44.27) [Triangle] +Component636 (89.42, 2.98) [Square] +Component637 (11.69, 51.64) [Triangle] +Component638 (27.67, 38.16) [X] +Component639 (82.42, 96.29) [Square] +Component640 (94.21, 94.49) +Component641 (8.58, 71.71) +Component642 (70.45, 91.93) +Component643 (10.96, 1.29) [Triangle] +Component644 (10.57, 19.18) [Triangle] +Component645 (57.76, 83.31) [Square] +Component646 (12.13, 8.42) [Circle] +Component647 (44.58, 80.07) [X] +Component648 (81.68, 24.58) +Component649 (52.65, 92.38) +Component650 (39.35, 41.67) [Triangle] +Component651 (55.90, 65.04) +Component652 (67.25, 14.83) [Square] +Component653 (8.49, 24.18) +Component654 (65.87, 40.83) [X] +Component655 (3.18, 17.64) [Square] +Component656 (25.59, 33.81) +Component657 (73.70, 19.39) +Component658 (21.35, 87.93) [Square] +Component659 (64.18, 97.24) [Square] +Component660 (32.48, 6.54) [X] +Component661 (86.73, 65.34) [Circle] +Component662 (98.31, 91.84) [Triangle] +Component663 (48.72, 75.26) [Square] +Component664 (33.81, 25.98) [Triangle] +Component665 (83.92, 87.11) [Triangle] +Component666 (36.37, 54.35) +Component667 (70.76, 58.70) [Circle] +Component668 (87.91, 69.42) +Component669 (5.06, 12.39) +Component670 (22.26, 4.78) +Component671 (82.28, 96.57) [Triangle] +Component672 (28.71, 40.93) [Circle] +Component673 (6.42, 25.09) [Square] +Component674 (57.90, 76.51) +Component675 (46.54, 26.90) [Triangle] +Component676 (6.60, 37.36) +Component677 (67.94, 26.83) +Component678 (70.57, 66.78) [Triangle] +Component679 (92.63, 29.54) +Component680 (21.06, 56.15) +Component681 (30.12, 87.84) [Square] +Component682 (22.50, 17.49) [Triangle] +Component683 (22.16, 99.28) [Square] +Component684 (86.25, 85.65) [Triangle] +Component685 (42.38, 97.43) [Square] +Component686 (82.82, 98.13) [Triangle] +Component687 (85.99, 22.81) [Square] +Component688 (92.42, 13.14) [Circle] +Component689 (86.08, 22.54) [Circle] +Component690 (78.61, 48.10) [Square] +Component691 (2.95, 70.77) +Component692 (96.12, 92.64) [Square] +Component693 (12.50, 97.26) [Square] +Component694 (86.32, 25.85) +Component695 (80.51, 28.95) [Triangle] +Component696 (46.82, 6.61) +Component697 (66.87, 17.17) [Circle] +Component698 (99.04, 68.95) [Triangle] +Component699 (77.10, 12.58) [Triangle] +Component700 (81.22, 95.50) [Square] +Component701 (58.44, 84.13) +Component702 (80.19, 61.91) +Component703 (96.80, 74.86) [Circle] +Component704 (83.91, 79.21) [Circle] +Component705 (94.51, 96.03) [Triangle] +Component706 (51.67, 13.80) +Component707 (29.79, 8.71) [Circle] +Component708 (36.15, 69.18) [Triangle] +Component709 (68.99, 71.52) [Circle] +Component710 (88.45, 64.95) [Circle] +Component711 (80.05, 71.13) [Circle] +Component712 (50.04, 22.87) [Triangle] +Component713 (17.38, 28.08) [Circle] +Component714 (88.91, 43.58) [Triangle] +Component715 (82.29, 88.78) [Circle] +Component716 (68.36, 82.95) [Square] +Component717 (10.29, 93.22) +Component718 (92.84, 27.40) +Component719 (39.48, 91.46) [Triangle] +Component720 (69.69, 83.40) +Component721 (33.85, 40.52) [Square] +Component722 (97.83, 99.99) [Triangle] +Component723 (62.84, 11.09) [X] +Component724 (43.10, 47.36) [Triangle] +Component725 (17.38, 82.24) [Triangle] +Component726 (54.82, 94.05) +Component727 (5.63, 25.27) [X] +Component728 (80.85, 80.59) [Triangle] +Component729 (59.77, 7.37) [X] +Component730 (68.88, 77.36) [X] +Component731 (93.49, 80.85) [Square] +Component732 (52.99, 75.23) [Circle] +Component733 (37.52, 58.87) +Component734 (9.84, 11.57) [Triangle] +Component735 (32.67, 39.28) [X] +Component736 (46.91, 23.64) [Square] +Component737 (33.46, 92.43) [Triangle] +Component738 (60.48, 16.10) [X] +Component739 (60.30, 74.77) +Component740 (97.63, 8.09) +Component741 (11.78, 92.13) +Component742 (41.21, 41.45) [Circle] +Component743 (4.93, 37.13) [X] +Component744 (13.29, 7.31) +Component745 (69.09, 18.30) [Circle] +Component746 (49.88, 88.56) +Component747 (66.69, 64.64) [X] +Component748 (71.42, 24.62) [Triangle] +Component749 (39.46, 93.61) +Component750 (2.08, 22.34) [Circle] +Component751 (89.58, 41.54) +Component752 (47.94, 84.87) +Component753 (65.47, 55.58) [X] +Component754 (53.54, 5.12) [Square] +Component755 (74.49, 17.69) [Square] +Component756 (12.88, 88.92) [X] +Component757 (52.18, 50.44) [X] +Component758 (68.05, 52.05) [Circle] +Component759 (93.47, 30.01) [Circle] +Component760 (94.90, 99.55) [X] +Component761 (84.05, 4.44) [Circle] +Component762 (42.74, 36.71) [Square] +Component763 (85.63, 69.38) [X] +Component764 (65.40, 66.21) [Triangle] +Component765 (21.67, 14.78) [Circle] +Component766 (53.61, 70.93) [Triangle] +Component767 (5.07, 78.99) +Component768 (53.15, 36.64) [Circle] +Component769 (46.97, 1.41) +Component770 (33.90, 44.43) +Component771 (39.45, 36.48) +Component772 (67.78, 90.87) [Circle] +Component773 (96.24, 3.57) [Triangle] +Component774 (41.37, 82.60) [Triangle] +Component775 (95.15, 52.79) [Square] +Component776 (20.68, 54.81) +Component777 (84.49, 48.27) [Circle] +Component778 (99.01, 87.63) +Component779 (49.10, 13.34) +Component780 (65.15, 86.57) +Component781 (91.51, 32.85) +Component782 (60.79, 26.62) [Triangle] +Component783 (34.55, 24.16) +Component784 (25.73, 57.46) +Component785 (23.74, 52.01) +Component786 (21.19, 90.75) [Triangle] +Component787 (83.22, 80.23) [Square] +Component788 (48.28, 63.70) [Triangle] +Component789 (47.02, 71.16) +Component790 (98.87, 44.60) +Component791 (80.99, 3.44) [Circle] +Component792 (41.82, 95.50) [Triangle] +Component793 (69.54, 96.23) [Triangle] +Component794 (49.78, 89.18) +Component795 (43.14, 80.71) [Triangle] +Component796 (70.04, 8.33) +Component797 (32.29, 29.23) [Square] +Component798 (58.10, 42.58) +Component799 (64.07, 98.61) [Circle] +Component800 (35.86, 61.33) [X] +Component801 (8.55, 54.52) +Component802 (55.95, 99.76) [Triangle] +Component803 (19.90, 4.60) [Circle] +Component804 (49.99, 96.57) +Component805 (73.19, 42.31) [Triangle] +Component806 (52.10, 15.63) [Circle] +Component807 (89.95, 99.16) [Square] +Component808 (82.80, 94.93) +Component809 (90.47, 43.12) +Component810 (36.98, 51.49) +Component811 (99.92, 14.67) [Square] +Component812 (62.57, 8.28) +Component813 (36.85, 24.38) +Component814 (2.59, 63.52) [Square] +Component815 (98.73, 86.84) +Component816 (29.92, 3.73) +Component817 (90.50, 63.09) +Component818 (76.17, 28.16) [Triangle] +Component819 (75.38, 42.93) [Square] +Component820 (86.39, 19.53) +Component821 (65.52, 54.88) +Component822 (31.16, 50.90) [Square] +Component823 (58.12, 53.66) +Component824 (95.46, 46.39) [X] +Component825 (76.11, 29.03) +Component826 (38.60, 28.10) [X] +Component827 (96.21, 96.79) +Component828 (58.19, 88.64) +Component829 (94.16, 47.60) [Square] +Component830 (40.67, 86.94) +Component831 (30.28, 55.52) [Square] +Component832 (17.24, 40.01) [Square] +Component833 (83.82, 84.98) [X] +Component834 (67.50, 79.68) +Component835 (30.15, 53.82) [Circle] +Component836 (92.23, 78.01) [Triangle] +Component837 (27.23, 92.60) [Square] +Component838 (31.61, 25.63) [X] +Component839 (61.71, 44.33) [Square] +Component840 (78.48, 69.79) [Circle] +Component841 (44.89, 97.80) [X] +Component842 (48.90, 49.06) [Triangle] +Component843 (15.44, 16.25) [Triangle] +Component844 (8.31, 94.22) +Component845 (31.64, 49.32) +Component846 (75.81, 94.49) [X] +Component847 (99.41, 85.31) [Circle] +Component848 (91.64, 32.44) [Square] +Component849 (49.66, 55.99) +Component850 (90.85, 80.95) +Component851 (20.72, 44.29) [X] +Component852 (66.52, 1.30) [Square] +Component853 (9.92, 86.11) [X] +Component854 (33.23, 85.49) +Component855 (65.31, 70.68) [Square] +Component856 (17.92, 62.02) [Square] +Component857 (95.52, 64.69) +Component858 (11.31, 0.13) +Component859 (59.34, 97.52) +Component860 (17.28, 96.61) +Component861 (33.92, 80.85) [Square] +Component862 (54.06, 3.36) [Circle] +Component863 (46.14, 16.09) [Triangle] +Component864 (77.52, 15.25) +Component865 (1.65, 25.12) [X] +Component866 (11.77, 25.49) +Component867 (41.81, 74.80) [Circle] +Component868 (6.72, 7.94) [X] +Component869 (78.18, 76.61) +Component870 (84.57, 14.90) [Circle] +Component871 (27.36, 76.49) [Square] +Component872 (74.01, 20.52) [Circle] +Component873 (72.83, 82.87) +Component874 (9.74, 46.82) [X] +Component875 (85.55, 26.40) [X] +Component876 (90.96, 12.96) [Circle] +Component877 (83.21, 30.43) +Component878 (18.32, 76.50) [X] +Component879 (64.86, 10.96) [Circle] +Component880 (20.28, 82.11) [Square] +Component881 (53.75, 89.58) [X] +Component882 (60.19, 95.72) [Square] +Component883 (18.98, 48.08) [X] +Component884 (72.61, 93.24) [Circle] +Component885 (35.46, 4.72) +Component886 (52.80, 41.57) [Square] +Component887 (53.44, 29.95) +Component888 (10.23, 58.60) [Square] +Component889 (68.29, 30.06) [Square] +Component890 (76.88, 68.56) +Component891 (31.04, 37.55) [Square] +Component892 (17.55, 78.76) +Component893 (10.10, 12.05) [X] +Component894 (73.22, 27.89) [Square] +Component895 (60.35, 81.53) +Component896 (4.47, 73.49) [Triangle] +Component897 (8.72, 67.26) +Component898 (26.71, 23.78) +Component899 (81.72, 36.94) [Triangle] +Component900 (16.01, 86.38) [Square] +Component901 (55.07, 85.27) [Triangle] +Component902 (70.30, 12.88) +Component903 (60.31, 62.86) [Triangle] +Component904 (41.00, 37.55) [Triangle] +Component905 (9.64, 61.89) [Circle] +Component906 (54.10, 31.77) [X] +Component907 (76.54, 62.41) +Component908 (86.17, 96.56) +Component909 (46.85, 49.44) +Component910 (37.67, 41.22) [Triangle] +Component911 (26.66, 62.43) [Triangle] +Component912 (69.43, 3.66) +Component913 (33.47, 70.71) [Square] +Component914 (93.21, 77.98) [Circle] +Component915 (90.74, 9.07) +Component916 (93.97, 16.70) [Square] +Component917 (8.15, 60.87) +Component918 (50.09, 96.07) [Square] +Component919 (75.88, 86.17) [Circle] +Component920 (68.31, 86.98) [X] +Component921 (67.83, 79.88) +Component922 (97.10, 82.63) [X] +Component923 (83.01, 1.06) [X] +Component924 (39.58, 86.43) [Circle] +Component925 (47.55, 84.77) +Component926 (96.69, 55.45) [X] +Component927 (77.39, 79.82) +Component928 (43.07, 76.23) [X] +Component929 (78.14, 10.36) [Triangle] +Component930 (30.33, 63.76) [X] +Component931 (41.25, 52.50) +Component932 (31.96, 82.30) +Component933 (65.06, 59.05) [Circle] +Component934 (26.94, 44.22) [Circle] +Component935 (27.72, 29.76) +Component936 (70.78, 0.65) [Triangle] +Component937 (58.58, 74.89) +Component938 (98.48, 3.16) +Component939 (17.30, 75.27) +Component940 (48.66, 72.60) [X] +Component941 (85.22, 46.47) [Triangle] +Component942 (67.36, 93.17) [Triangle] +Component943 (79.00, 3.03) +Component944 (93.55, 16.21) +Component945 (33.57, 39.55) +Component946 (30.28, 29.41) +Component947 (98.43, 70.60) [X] +Component948 (96.67, 13.72) [Triangle] +Component949 (38.28, 48.43) [Triangle] +Component950 (88.84, 46.42) [Circle] +Component951 (73.14, 28.70) [X] +Component952 (64.62, 10.80) [X] +Component953 (17.27, 35.05) +Component954 (28.44, 50.29) [Circle] +Component955 (68.78, 52.58) +Component956 (92.74, 72.49) +Component957 (74.46, 73.66) [Circle] +Component958 (91.38, 5.63) [Square] +Component959 (17.30, 2.57) [Triangle] +Component960 (77.85, 76.45) [Square] +Component961 (67.51, 95.09) +Component962 (22.14, 74.92) [Circle] +Component963 (11.07, 79.09) [Circle] +Component964 (93.58, 6.56) +Component965 (30.32, 76.50) [Circle] +Component966 (88.27, 78.61) [Circle] +Component967 (99.07, 68.94) [X] +Component968 (4.65, 2.36) [Square] +Component969 (58.21, 65.30) [Triangle] +Component970 (97.18, 30.96) [Square] +Component971 (14.96, 34.69) +Component972 (24.39, 36.35) +Component973 (68.87, 87.50) +Component974 (8.47, 85.32) [Square] +Component975 (22.83, 76.01) [Circle] +Component976 (49.17, 0.04) [Circle] +Component977 (68.21, 48.73) +Component978 (89.13, 91.30) [Triangle] +Component979 (65.46, 59.78) [Triangle] +Component980 (90.34, 57.29) +Component981 (96.18, 5.07) [Triangle] +Component982 (76.27, 45.30) [Circle] +Component983 (46.02, 62.58) [Circle] +Component984 (85.41, 20.20) [X] +Component985 (87.32, 88.29) [X] +Component986 (99.07, 66.28) +Component987 (80.59, 74.45) +Component988 (4.96, 92.28) +Component989 (29.93, 45.09) [Triangle] +Component990 (41.97, 25.00) +Component991 (17.13, 20.49) +Component992 (95.65, 87.44) [Circle] +Component993 (98.97, 40.15) [Circle] +Component994 (18.60, 5.01) +Component995 (3.77, 53.31) [Triangle] +Component996 (39.81, 95.10) [X] +Component997 (0.87, 96.79) +Component998 (9.14, 29.01) [Triangle] +Component999 (40.79, 92.24) [Circle] + +Component387 -> Component728 +Component563 -- Component366 +Component537 -- Component292 +Component166 -- Component631 +Component300 -> Component112 +Component878 -> Component414 +Component988 -> Component49 +Component174 -- Component89 +Component585 -- Component354 +Component681 -- Component25 +Component994 -- Component479 +Component68 -- Component702 +Component619 -> Component448 +Component899 -> Component43 +Component927 -> Component175 +Component732 -> Component404 +Component751 -- Component519 +Component982 -- Component595 +Component101 -- Component259 +Component933 -- Component208 +Component717 -- Component607 +Component897 -- Component386 +Component66 -> Component18 +Component517 -> Component829 +Component132 -> Component214 +Component228 -- Component446 +Component578 -- Component369 +Component841 -> Component805 +Component787 -- Component789 +Component478 -- Component421 +Component281 -> Component863 +Component218 -- Component232 +Component853 -> Component598 +Component277 -> Component714 +Component815 -- Component66 +Component17 -> Component878 +Component973 -- Component631 +Component855 -> Component787 +Component326 -- Component670 +Component535 -> Component24 +Component347 -> Component624 +Component921 -> Component172 +Component543 -- Component130 +Component879 -- Component456 +Component50 -> Component120 +Component243 -> Component753 +Component549 -> Component971 +Component563 -- Component895 +Component23 -- Component650 +Component772 -- Component980 +Component379 -- Component342 +Component744 -> Component785 +Component746 -- Component138 +Component188 -> Component96 +Component486 -> Component400 +Component40 -> Component475 +Component860 -- Component88 +Component30 -> Component105 +Component479 -> Component790 +Component542 -> Component193 +Component143 -> Component966 +Component183 -> Component260 +Component8 -- Component437 +Component579 -- Component4 +Component120 -- Component488 +Component961 -> Component470 +Component952 -> Component581 +Component60 -- Component682 +Component918 -- Component235 +Component9 -> Component1 +Component503 -- Component452 +Component74 -- Component911 +Component184 -> Component260 +Component370 -- Component927 +Component678 -> Component506 +Component152 -> Component403 +Component877 -- Component649 +Component108 -- Component944 +Component580 -> Component726 +Component691 -> Component384 +Component953 -- Component452 +Component660 -> Component582 +Component695 -> Component152 +Component222 -> Component858 +Component161 -- Component275 +Component604 -- Component112 +Component130 -- Component203 +Component256 -- Component892 +Component120 -- Component946 +Component97 -> Component208 +Component905 -> Component632 +Component918 -- Component195 +Component961 -- Component860 +Component33 -> Component336 +Component613 -- Component222 +Component466 -> Component796 +Component861 -> Component867 +Component145 -> Component425 +Component239 -- Component549 +Component340 -> Component634 +Component752 -> Component53 +Component633 -> Component681 +Component456 -> Component373 +Component71 -> Component343 +Component358 -- Component157 +Component615 -- Component657 +Component570 -- Component433 +Component561 -> Component90 +Component600 -- Component344 +Component254 -> Component746 +Component14 -> Component41 +Component340 -> Component126 +Component957 -> Component684 +Component259 -> Component949 +Component521 -- Component792 +Component90 -- Component929 +Component150 -> Component617 +Component854 -> Component538 +Component485 -> Component426 +Component639 -> Component596 +Component603 -> Component540 +Component815 -> Component365 +Component724 -> Component309 +Component736 -> Component800 +Component448 -> Component715 +Component507 -> Component303 +Component610 -> Component201 +Component712 -- Component599 +Component263 -> Component206 +Component321 -> Component519 +Component861 -> Component0 +Component160 -- Component223 +Component9 -> Component686 +Component866 -- Component200 +Component829 -- Component8 +Component73 -> Component557 +Component350 -> Component93 +Component379 -- Component467 +Component870 -> Component402 +Component385 -> Component675 +Component415 -- Component296 +Component698 -> Component36 +Component708 -> Component117 +Component550 -- Component427 +Component268 -> Component550 +Component573 -- Component124 +Component385 -- Component737 +Component968 -- Component7 +Component850 -> Component992 +Component166 -- Component337 +Component654 -- Component476 +Component768 -- Component834 +Component381 -- Component663 +Component540 -- Component770 +Component231 -> Component234 +Component602 -> Component180 +Component632 -- Component186 +Component568 -> Component583 +Component842 -- Component311 +Component23 -- Component344 +Component914 -- Component732 +Component514 -> Component426 +Component952 -> Component412 +Component192 -> Component734 +Component828 -> Component295 +Component341 -> Component530 +Component650 -- Component169 +Component668 -- Component446 +Component745 -> Component15 +Component995 -> Component321 +Component311 -> Component585 +Component16 -> Component94 +Component41 -> Component459 +Component576 -> Component647 +Component783 -- Component311 +Component842 -> Component857 +Component646 -> Component312 +Component252 -> Component703 +Component519 -- Component884 +Component767 -- Component465 +Component18 -> Component738 +Component743 -> Component828 +Component844 -> Component119 +Component270 -- Component552 +Component372 -- Component539 +Component259 -- Component720 +Component334 -- Component338 +Component357 -- Component551 +Component417 -- Component367 +Component905 -> Component693 +Component272 -> Component469 +Component127 -- Component888 +Component621 -- Component662 +Component424 -- Component512 +Component982 -> Component456 +Component58 -- Component333 +Component28 -> Component200 +Component110 -- Component703 +Component809 -> Component46 +Component414 -> Component881 +Component909 -- Component662 +Component804 -> Component531 +Component307 -> Component133 +Component815 -> Component64 +Component124 -- Component936 +Component413 -- Component299 +Component802 -- Component799 +Component448 -- Component874 +Component225 -- Component18 +Component553 -- Component730 +Component33 -> Component181 +Component751 -> Component567 +Component279 -> Component873 +Component768 -- Component982 +Component313 -> Component955 +Component375 -- Component681 +Component13 -> Component206 +Component772 -> Component981 +Component140 -> Component46 +Component462 -- Component533 +Component244 -- Component341 +Component471 -- Component172 +Component54 -- Component759 +Component923 -> Component449 +Component669 -> Component697 +Component646 -> Component19 +Component802 -> Component306 +Component756 -- Component343 +Component768 -- Component727 +Component906 -- Component19 +Component238 -- Component296 +Component599 -> Component74 +Component969 -- Component186 +Component249 -> Component355 +Component483 -- Component988 +Component669 -> Component226 +Component772 -- Component104 +Component533 -> Component890 +Component788 -- Component259 +Component967 -- Component260 +Component670 -- Component94 +Component291 -> Component139 +Component361 -> Component901 +Component338 -> Component953 +Component944 -- Component600 +Component40 -- Component464 +Component286 -- Component354 +Component599 -- Component912 +Component213 -- Component185 +Component63 -> Component48 +Component271 -- Component811 +Component112 -- Component782 +Component384 -- Component552 +Component351 -> Component162 +Component502 -- Component173 +Component81 -- Component405 +Component866 -- Component777 +Component219 -> Component279 +Component121 -> Component448 +Component437 -> Component890 +Component889 -> Component471 +Component436 -> Component705 +Component938 -- Component670 +Component733 -> Component709 +Component220 -- Component386 +Component57 -> Component652 +Component500 -- Component495 +Component282 -> Component724 +Component786 -> Component103 +Component844 -> Component822 +Component218 -- Component48 +Component662 -- Component544 +Component8 -- Component580 +Component137 -> Component943 +Component924 -> Component822 +Component562 -> Component337 +Component20 -- Component847 +Component60 -- Component310 +Component709 -- Component48 +Component476 -> Component453 +Component315 -- Component896 +Component29 -- Component429 +Component136 -> Component357 +Component782 -> Component890 +Component737 -> Component89 +Component806 -- Component929 +Component28 -> Component659 +Component698 -- Component852 +Component298 -> Component241 +Component64 -> Component863 +Component489 -> Component295 +Component204 -- Component248 +Component165 -> Component554 +Component970 -- Component465 +Component52 -- Component748 +Component900 -> Component759 +Component509 -> Component135 +Component187 -> Component516 +Component787 -- Component587 +Component939 -> Component917 +Component764 -> Component139 +Component64 -- Component79 +Component93 -> Component852 +Component589 -- Component991 +Component410 -- Component660 +Component242 -> Component87 +Component381 -> Component245 +Component413 -- Component262 +Component912 -- Component869 +Component334 -> Component483 +Component629 -> Component369 +Component293 -> Component159 +Component749 -- Component129 +Component244 -- Component394 +Component29 -- Component388 +Component523 -> Component443 +Component265 -- Component973 +Component380 -- Component983 +Component608 -- Component227 +Component119 -> Component472 +Component743 -- Component766 +Component83 -- Component942 +Component110 -- Component696 +Component754 -> Component24 +Component571 -- Component898 +Component290 -> Component615 +Component793 -- Component490 +Component746 -> Component992 +Component871 -- Component756 +Component623 -> Component259 +Component188 -> Component778 +Component348 -- Component392 +Component5 -- Component401 +Component162 -> Component680 +Component233 -- Component276 +Component859 -- Component42 +Component505 -- Component917 +Component328 -> Component892 +Component34 -- Component828 +Component526 -- Component744 +Component130 -- Component970 +Component673 -> Component884 +Component852 -> Component630 +Component102 -- Component41 +Component756 -- Component573 +Component31 -- Component592 +Component558 -> Component400 +Component1 -- Component576 +Component575 -> Component184 +Component19 -- Component732 +Component30 -- Component534 +Component343 -- Component614 +Component562 -> Component480 +Component361 -- Component979 +Component86 -> Component597 +Component192 -- Component600 +Component553 -- Component491 +Component530 -> Component323 +Component995 -> Component720 +Component40 -- Component963 +Component438 -- Component271 +Component941 -> Component86 +Component161 -> Component997 +Component493 -> Component351 +Component907 -- Component307 +Component803 -> Component218 +Component232 -> Component372 +Component74 -> Component63 +Component386 -- Component365 +Component445 -- Component476 +Component633 -- Component463 +Component605 -- Component387 +Component650 -> Component693 +Component338 -> Component843 +Component518 -> Component188 +Component349 -> Component454 +Component889 -> Component874 +Component764 -> Component65 +Component112 -- Component780 +Component253 -- Component757 +Component924 -- Component893 +Component547 -- Component297 +Component372 -> Component852 +Component682 -> Component865 +Component743 -> Component716 +Component31 -> Component618 +Component896 -> Component951 +Component157 -- Component649 +Component22 -- Component966 +Component811 -> Component86 +Component212 -> Component80 +Component473 -> Component236 +Component979 -> Component43 +Component668 -- Component995 +Component412 -> Component594 +Component516 -- Component472 +Component500 -> Component554 +Component148 -- Component185 +Component769 -- Component438 +Component512 -- Component570 +Component249 -> Component990 +Component185 -> Component233 +Component94 -- Component320 +Component858 -- Component197 +Component268 -- Component6 +Component529 -- Component806 +Component184 -- Component779 +Component235 -- Component894 +Component215 -> Component990 +Component7 -- Component427 +Component583 -> Component297 +Component504 -- Component148 +Component92 -- Component223 +Component693 -> Component151 +Component858 -- Component241 +Component523 -- Component919 +Component938 -> Component926 +Component550 -- Component761 +Component25 -- Component532 +Component789 -- Component151 +Component497 -- Component190 +Component443 -- Component319 +Component82 -> Component780 +Component523 -> Component205 +Component176 -- Component456 +Component600 -> Component231 +Component609 -- Component908 +Component374 -> Component895 +Component919 -> Component605 +Component74 -- Component953 +Component70 -> Component75 +Component757 -- Component405 +Component756 -- Component342 +Component847 -- Component252 +Component596 -> Component131 +Component407 -- Component957 +Component493 -> Component691 +Component618 -- Component506 +Component788 -- Component107 +Component700 -- Component258 +Component419 -> Component896 +Component936 -> Component433 +Component221 -> Component206 +Component167 -> Component57 +Component86 -> Component1 +Component825 -> Component326 +Component643 -- Component121 +Component912 -- Component132 +Component307 -- Component636 +Component218 -- Component84 +Component192 -> Component656 +Component346 -- Component347 +Component45 -- Component260 +Component962 -- Component33 +Component693 -> Component197 +Component308 -- Component622 +Component23 -> Component24 +Component625 -- Component402 +Component437 -> Component114 +Component186 -- Component586 +Component93 -- Component96 +Component830 -- Component485 +Component177 -> Component323 +Component727 -> Component725 +Component519 -> Component669 +Component213 -- Component611 +Component500 -- Component763 +Component638 -> Component337 +Component5 -> Component622 +Component814 -> Component914 +Component699 -> Component337 +Component472 -> Component262 +Component105 -> Component108 +Component519 -> Component575 +Component122 -> Component641 +Component27 -> Component683 +Component30 -> Component368 +Component947 -> Component361 +Component473 -> Component551 +Component195 -- Component865 +Component758 -> Component781 +Component988 -- Component322 +Component292 -- Component562 +Component94 -> Component179 +Component769 -- Component428 +Component813 -- Component177 +Component957 -> Component952 +Component361 -> Component268 +Component982 -- Component600 +Component771 -- Component657 +Component2 -> Component345 +Component90 -- Component447 +Component583 -> Component289 +Component478 -- Component980 +Component712 -- Component343 +Component203 -- Component449 +Component871 -> Component245 +Component446 -> Component557 +Component371 -- Component359 +Component664 -- Component272 +Component130 -> Component795 +Component383 -> Component371 +Component743 -> Component357 +Component949 -- Component591 +Component224 -> Component809 +Component401 -- Component928 +Component697 -- Component52 +Component523 -> Component549 +Component73 -> Component447 +Component39 -- Component682 +Component406 -- Component180 +Component653 -- Component605 +Component287 -> Component464 +Component600 -- Component794 +Component625 -> Component881 +Component402 -> Component53 +Component991 -- Component287 +Component267 -- Component157 +Component935 -- Component182 +Component368 -> Component689 +Component446 -> Component623 +Component95 -> Component969 +Component853 -> Component313 +Component227 -> Component415 +Component70 -- Component535 +Component877 -- Component64 +Component394 -> Component905 +Component811 -> Component339 +Component828 -- Component15 +Component4 -> Component752 +Component453 -> Component49 +Component604 -> Component990 +Component295 -- Component723 +Component562 -- Component691 +Component45 -- Component472 +Component284 -- Component226 +Component488 -- Component236 +Component542 -- Component143 +Component199 -> Component149 +Component629 -> Component805 +Component251 -- Component13 +Component44 -- Component301 +Component949 -> Component961 +Component187 -- Component713 +Component295 -- Component477 +Component284 -- Component505 +Component430 -- Component61 +Component504 -- Component579 +Component928 -- Component585 +Component864 -- Component173 +Component446 -> Component141 +Component471 -- Component749 +Component252 -> Component631 +Component520 -> Component375 +Component711 -> Component337 +Component360 -> Component985 +Component425 -- Component916 +Component288 -> Component466 +Component789 -> Component740 +Component235 -- Component578 +Component126 -- Component959 +Component800 -- Component367 +Component926 -> Component626 +Component19 -- Component799 +Component711 -> Component119 +Component31 -- Component805 +Component962 -> Component268 +Component254 -- Component192 +Component129 -> Component374 +Component173 -> Component244 +Component699 -- Component508 +Component767 -> Component372 +Component491 -> Component357 +Component301 -> Component935 +Component648 -- Component899 +Component748 -> Component97 +Component140 -> Component901 +Component503 -> Component68 +Component28 -- Component646 +Component38 -- Component713 +Component109 -- Component540 +Component186 -> Component519 +Component478 -- Component507 +Component885 -- Component794 +Component929 -- Component165 +Component207 -- Component615 +Component198 -- Component434 +Component919 -- Component880 +Component668 -> Component164 +Component1 -- Component792 +Component17 -- Component22 +Component215 -- Component796 +Component118 -> Component670 +Component480 -> Component506 +Component403 -- Component709 +Component864 -- Component180 +Component316 -> Component167 +Component652 -- Component916 +Component86 -- Component250 +Component664 -> Component26 + +[Note] (2.94, 40.45) Note 0: Important information here +[Note] (52.51, 36.04) Note 1: Important information here +[Note] (58.98, 66.79) Note 2: Important information here +[Note] (78.33, 13.50) Note 3: Important information here +[Note] (79.45, 45.42) Note 4: Important information here +[Note] (73.48, 17.13) Note 5: Important information here +[Note] (50.74, 42.61) Note 6: Important information here +[Note] (78.16, 67.95) Note 7: Important information here +[Note] (39.81, 97.76) Note 8: Important information here +[Note] (20.44, 8.41) Note 9: Important information here +[Note] (57.71, 68.13) Note 10: Important information here +[Note] (4.50, 43.31) Note 11: Important information here +[Note] (81.32, 0.57) Note 12: Important information here +[Note] (28.05, 35.60) Note 13: Important information here +[Note] (32.09, 5.25) Note 14: Important information here +[Note] (15.56, 33.99) Note 15: Important information here +[Note] (6.30, 36.55) Note 16: Important information here +[Note] (26.69, 87.57) Note 17: Important information here +[Note] (84.58, 54.27) Note 18: Important information here +[Note] (51.43, 97.01) Note 19: Important information here +[Note] (52.32, 80.60) Note 20: Important information here +[Note] (25.39, 94.75) Note 21: Important information here +[Note] (14.65, 36.06) Note 22: Important information here +[Note] (33.20, 19.14) Note 23: Important information here +[Note] (61.89, 14.32) Note 24: Important information here +[Note] (19.45, 14.75) Note 25: Important information here +[Note] (72.54, 34.44) Note 26: Important information here +[Note] (30.25, 60.76) Note 27: Important information here +[Note] (97.08, 95.72) Note 28: Important information here +[Note] (36.72, 82.06) Note 29: Important information here +[Note] (66.76, 96.51) Note 30: Important information here +[Note] (44.97, 98.71) Note 31: Important information here +[Note] (33.53, 71.85) Note 32: Important information here +[Note] (1.12, 36.27) Note 33: Important information here +[Note] (93.86, 41.54) Note 34: Important information here +[Note] (45.49, 97.58) Note 35: Important information here +[Note] (11.85, 27.02) Note 36: Important information here +[Note] (22.05, 41.78) Note 37: Important information here +[Note] (24.21, 47.46) Note 38: Important information here +[Note] (35.59, 77.67) Note 39: Important information here +[Note] (60.38, 33.28) Note 40: Important information here +[Note] (11.99, 57.45) Note 41: Important information here +[Note] (36.12, 68.56) Note 42: Important information here +[Note] (12.09, 2.53) Note 43: Important information here +[Note] (76.81, 69.61) Note 44: Important information here +[Note] (90.95, 51.34) Note 45: Important information here +[Note] (62.74, 25.63) Note 46: Important information here +[Note] (23.00, 12.92) Note 47: Important information here +[Note] (89.56, 11.80) Note 48: Important information here +[Note] (91.20, 54.14) Note 49: Important information here +[Note] (9.82, 29.43) Note 50: Important information here +[Note] (39.10, 54.60) Note 51: Important information here +[Note] (18.37, 26.61) Note 52: Important information here +[Note] (73.22, 5.28) Note 53: Important information here +[Note] (48.00, 29.66) Note 54: Important information here +[Note] (29.62, 96.82) Note 55: Important information here +[Note] (43.04, 99.15) Note 56: Important information here +[Note] (35.35, 59.90) Note 57: Important information here +[Note] (14.89, 41.05) Note 58: Important information here +[Note] (63.43, 25.91) Note 59: Important information here +[Note] (8.89, 85.37) Note 60: Important information here +[Note] (40.78, 28.82) Note 61: Important information here +[Note] (45.40, 95.24) Note 62: Important information here +[Note] (38.42, 69.68) Note 63: Important information here +[Note] (98.58, 97.72) Note 64: Important information here +[Note] (55.42, 94.26) Note 65: Important information here +[Note] (79.78, 9.46) Note 66: Important information here +[Note] (74.35, 80.17) Note 67: Important information here +[Note] (51.98, 69.94) Note 68: Important information here +[Note] (10.33, 99.47) Note 69: Important information here +[Note] (49.91, 26.62) Note 70: Important information here +[Note] (99.67, 89.15) Note 71: Important information here +[Note] (83.91, 73.43) Note 72: Important information here +[Note] (96.77, 80.50) Note 73: Important information here +[Note] (79.78, 29.10) Note 74: Important information here +[Note] (73.99, 22.14) Note 75: Important information here +[Note] (0.40, 71.89) Note 76: Important information here +[Note] (96.44, 0.13) Note 77: Important information here +[Note] (37.22, 36.88) Note 78: Important information here +[Note] (49.86, 14.12) Note 79: Important information here +[Note] (54.71, 0.77) Note 80: Important information here +[Note] (91.08, 10.71) Note 81: Important information here +[Note] (82.91, 82.87) Note 82: Important information here +[Note] (16.74, 3.24) Note 83: Important information here +[Note] (72.61, 55.76) Note 84: Important information here +[Note] (11.39, 18.81) Note 85: Important information here +[Note] (95.79, 22.32) Note 86: Important information here +[Note] (2.01, 62.41) Note 87: Important information here +[Note] (13.64, 62.81) Note 88: Important information here +[Note] (57.72, 29.73) Note 89: Important information here +[Note] (75.37, 73.59) Note 90: Important information here +[Note] (61.03, 3.80) Note 91: Important information here +[Note] (48.59, 37.15) Note 92: Important information here +[Note] (22.25, 75.74) Note 93: Important information here +[Note] (93.34, 15.31) Note 94: Important information here +[Note] (29.08, 62.29) Note 95: Important information here +[Note] (90.50, 87.93) Note 96: Important information here +[Note] (16.57, 55.43) Note 97: Important information here +[Note] (94.92, 72.00) Note 98: Important information here +[Note] (76.78, 69.76) Note 99: Important information here + +[Group] Component166, Component358, Component162, Component735, Component888, Component887 +[Group] Component813, Component533, Component171, Component890, Component903, Component427, Component946, Component701, Component485 +[Group] Component361, Component442, Component616, Component723, Component141 +[Group] Component928, Component96, Component157, Component210, Component556 +[Group] Component640, Component776, Component24, Component77, Component122, Component770, Component749, Component138, Component885 +[Group] Component321, Component517, Component33, Component560, Component553, Component851, Component109, Component798, Component164 +[Group] Component750, Component439, Component500, Component242, Component758 +[Group] Component631, Component186, Component77, Component263, Component342, Component988, Component55, Component868 +[Group] Component700, Component735, Component71, Component491, Component265, Component584, Component703, Component858 +[Group] Component34, Component461, Component400, Component364, Component53 +[Group] Component328, Component186, Component561, Component769, Component403, Component477, Component319, Component319, Component512 +[Group] Component773, Component847, Component838, Component153, Component187, Component545, Component450, Component125, Component610, Component328 +[Group] Component7, Component340, Component456, Component183, Component990, Component303, Component767 +[Group] Component777, Component607, Component466, Component751, Component848, Component803, Component994, Component561 +[Group] Component773, Component278, Component623, Component201, Component281 +[Group] Component357, Component963, Component582, Component828, Component733, Component258, Component904, Component261, Component277, Component949 +[Group] Component906, Component471, Component230, Component135, Component370, Component930, Component74, Component177, Component389 +[Group] Component836, Component154, Component590, Component2, Component459, Component732, Component512, Component759, Component760, Component840 +[Group] Component625, Component499, Component9, Component58, Component259, Component602, Component488, Component176, Component221, Component781 +[Group] Component202, Component212, Component576, Component111, Component612, Component856, Component916 +[Group] Component517, Component296, Component112, Component53, Component760, Component856, Component209, Component31, Component638 +[Group] Component237, Component717, Component314, Component724, Component896, Component946, Component228, Component563, Component994, Component18 +[Group] Component102, Component958, Component317, Component552, Component821, Component802, Component725, Component856, Component878, Component922 +[Group] Component638, Component280, Component163, Component527, Component205, Component74, Component753, Component170, Component172, Component610 +[Group] Component885, Component857, Component835, Component834, Component599, Component965 +[Group] Component355, Component341, Component305, Component329, Component705, Component665 +[Group] Component933, Component454, Component769, Component783, Component454, Component118, Component616 +[Group] Component659, Component141, Component574, Component505, Component259, Component538, Component250, Component383, Component374, Component948 +[Group] Component22, Component487, Component234, Component640, Component450, Component55 +[Group] Component110, Component166, Component875, Component745, Component109 +[Group] Component536, Component346, Component210, Component131, Component732, Component786, Component506, Component834, Component773, Component285 +[Group] Component562, Component401, Component560, Component186, Component710, Component41, Component519, Component902, Component489, Component304 +[Group] Component895, Component503, Component939, Component501, Component255 +[Group] Component344, Component39, Component167, Component74, Component680, Component962, Component789, Component528, Component199 +[Group] Component24, Component917, Component390, Component67, Component184, Component502, Component131 +[Group] Component146, Component868, Component949, Component619, Component382 +[Group] Component263, Component544, Component629, Component77, Component983, Component303 +[Group] Component873, Component161, Component398, Component672, Component847, Component834, Component364 +[Group] Component78, Component511, Component14, Component998, Component276 +[Group] Component161, Component681, Component53, Component139, Component606 +[Group] Component651, Component241, Component157, Component273, Component316, Component233, Component725, Component317 +[Group] Component347, Component471, Component365, Component748, Component266, Component849, Component868, Component478, Component320 +[Group] Component702, Component625, Component547, Component230, Component791, Component492, Component484 +[Group] Component294, Component37, Component146, Component889, Component343, Component761, Component100 +[Group] Component62, Component902, Component833, Component184, Component124, Component322, Component346, Component874, Component785 +[Group] Component838, Component501, Component979, Component48, Component404, Component24, Component644, Component759, Component138, Component470 +[Group] Component764, Component133, Component294, Component987, Component134, Component160, Component60 +[Group] Component916, Component310, Component269, Component134, Component967, Component418, Component946, Component333 +[Group] Component983, Component468, Component392, Component878, Component42, Component643, Component428, Component967, Component559, Component133 +[Group] Component928, Component925, Component32, Component478, Component979, Component488, Component674 + +[Inertia] Component269 +[Inertia] Component778 +[Inertia] Component942 +[Inertia] Component159 +[Inertia] Component579 +[Inertia] Component156 +[Inertia] Component864 +[Inertia] Component25 +[Inertia] Component940 +[Inertia] Component571 +[Inertia] Component124 +[Inertia] Component206 +[Inertia] Component353 +[Inertia] Component695 +[Inertia] Component549 +[Inertia] Component118 +[Inertia] Component914 +[Inertia] Component67 +[Inertia] Component73 +[Inertia] Component688 +[Inertia] Component413 +[Inertia] Component828 +[Inertia] Component720 +[Inertia] Component31 +[Inertia] Component203 +[Inertia] Component835 +[Inertia] Component73 +[Inertia] Component770 +[Inertia] Component340 +[Inertia] Component608 +[Inertia] Component82 +[Inertia] Component349 +[Inertia] Component972 +[Inertia] Component63 +[Inertia] Component30 +[Inertia] Component392 +[Inertia] Component455 +[Inertia] Component910 +[Inertia] Component76 +[Inertia] Component843 +[Inertia] Component120 +[Inertia] Component831 +[Inertia] Component555 +[Inertia] Component531 +[Inertia] Component850 +[Inertia] Component991 +[Inertia] Component789 +[Inertia] Component834 +[Inertia] Component341 +[Inertia] Component611 +[Inertia] Component990 +[Inertia] Component599 +[Inertia] Component872 +[Inertia] Component44 +[Inertia] Component574 +[Inertia] Component727 +[Inertia] Component525 +[Inertia] Component73 +[Inertia] Component539 +[Inertia] Component596 +[Inertia] Component458 +[Inertia] Component982 +[Inertia] Component1 +[Inertia] Component604 +[Inertia] Component573 +[Inertia] Component208 +[Inertia] Component317 +[Inertia] Component815 +[Inertia] Component81 +[Inertia] Component703 +[Inertia] Component949 +[Inertia] Component634 +[Inertia] Component651 +[Inertia] Component687 +[Inertia] Component430 +[Inertia] Component407 +[Inertia] Component802 +[Inertia] Component716 +[Inertia] Component348 +[Inertia] Component769 +[Inertia] Component684 +[Inertia] Component260 +[Inertia] Component52 +[Inertia] Component707 +[Inertia] Component99 +[Inertia] Component100 +[Inertia] Component776 +[Inertia] Component104 +[Inertia] Component489 +[Inertia] Component702 +[Inertia] Component923 +[Inertia] Component927 +[Inertia] Component409 +[Inertia] Component149 +[Inertia] Component816 +[Inertia] Component170 +[Inertia] Component953 +[Inertia] Component816 +[Inertia] Component83 +[Inertia] Component65 + +[Evolution] Component902 + 9.56 +[Evolution] Component565 + 6.91 +[Evolution] Component691 + 46.82 +[Evolution] Component502 - 45.86 +[Evolution] Component298 - 11.79 +[Evolution] Component902 - 29.01 +[Evolution] Component313 + 20.43 +[Evolution] Component452 - 21.03 +[Evolution] Component489 + 42.37 +[Evolution] Component59 + 26.46 +[Evolution] Component124 - 48.39 +[Evolution] Component920 - 47.99 +[Evolution] Component835 - 44.60 +[Evolution] Component408 + 9.26 +[Evolution] Component381 + 7.89 +[Evolution] Component822 + 37.66 +[Evolution] Component244 + 3.26 +[Evolution] Component294 + 38.15 +[Evolution] Component985 - 14.38 +[Evolution] Component810 + 3.40 +[Evolution] Component0 - 19.05 +[Evolution] Component976 + 6.90 +[Evolution] Component516 + 30.13 +[Evolution] Component193 + 10.17 +[Evolution] Component653 - 28.96 +[Evolution] Component29 + 48.14 +[Evolution] Component810 + 6.28 +[Evolution] Component617 + 2.99 +[Evolution] Component228 + 35.90 +[Evolution] Component164 + 33.70 +[Evolution] Component297 - 42.43 +[Evolution] Component42 + 47.32 +[Evolution] Component752 + 21.97 +[Evolution] Component930 - 38.23 +[Evolution] Component305 - 49.76 +[Evolution] Component748 + 41.84 +[Evolution] Component2 - 41.73 +[Evolution] Component524 + 6.93 +[Evolution] Component350 + 36.75 +[Evolution] Component129 + 24.85 +[Evolution] Component934 + 42.24 +[Evolution] Component568 - 45.29 +[Evolution] Component18 + 27.95 +[Evolution] Component125 + 42.13 +[Evolution] Component843 - 25.92 +[Evolution] Component502 - 7.53 +[Evolution] Component845 + 41.54 +[Evolution] Component269 + 47.41 +[Evolution] Component440 + 23.12 +[Evolution] Component370 - 14.15 +[Evolution] Component885 + 27.46 +[Evolution] Component512 - 18.65 +[Evolution] Component313 - 41.73 +[Evolution] Component173 + 21.42 +[Evolution] Component182 + 32.77 +[Evolution] Component681 + 0.34 +[Evolution] Component71 - 0.81 +[Evolution] Component256 + 30.35 +[Evolution] Component380 - 30.76 +[Evolution] Component790 - 4.48 +[Evolution] Component329 - 8.79 +[Evolution] Component456 - 22.37 +[Evolution] Component829 - 40.43 +[Evolution] Component586 + 20.49 +[Evolution] Component918 - 13.15 +[Evolution] Component638 + 22.34 +[Evolution] Component41 + 31.82 +[Evolution] Component958 - 26.38 +[Evolution] Component43 - 19.16 +[Evolution] Component287 + 19.71 +[Evolution] Component33 - 17.17 +[Evolution] Component826 - 44.03 +[Evolution] Component290 + 24.18 +[Evolution] Component17 - 4.30 +[Evolution] Component550 + 3.36 +[Evolution] Component262 - 29.09 +[Evolution] Component472 + 10.38 +[Evolution] Component792 - 2.78 +[Evolution] Component79 + 20.09 +[Evolution] Component695 + 36.78 +[Evolution] Component413 + 7.77 +[Evolution] Component937 + 41.23 +[Evolution] Component749 + 39.04 +[Evolution] Component970 + 37.45 +[Evolution] Component748 - 10.69 +[Evolution] Component16 - 19.91 +[Evolution] Component10 + 2.90 +[Evolution] Component337 + 21.52 +[Evolution] Component190 + 13.97 +[Evolution] Component773 + 25.37 +[Evolution] Component16 - 41.09 +[Evolution] Component387 + 46.74 +[Evolution] Component355 - 46.48 +[Evolution] Component511 - 26.32 +[Evolution] Component4 + 36.69 +[Evolution] Component811 + 30.11 +[Evolution] Component885 - 28.46 +[Evolution] Component854 - 41.72 +[Evolution] Component493 + 40.34 +[Evolution] Component541 - 6.05
\ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..e6b92cc --- /dev/null +++ b/test/test.js @@ -0,0 +1,139 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { createCanvas } from "canvas"; +import { parse } from "wmap-parser"; +import { renderToCanvas, renderToImage } from "../src/index.js"; +import { StageType } from "../src/stage_type.js"; +import fs from "fs"; + +const wmapSource = fs.readFileSync("./test/example.wmap", "utf-8"); + +describe("wmap-renderer-canvas", () => { + describe("renderToCanvas", () => { + it("should render a map to canvas", async () => { + const map = parse(wmapSource); + const canvas = createCanvas(1384, 1084); + + await renderToCanvas(map, canvas); + + assert.strictEqual(canvas.width, 1384); + assert.strictEqual(canvas.height, 1084); + }); + + it("should throw error without map", async () => { + const canvas = createCanvas(1384, 1084); + + await assert.rejects( + async () => await renderToCanvas(null, canvas), + { message: "Map object is required" }, + ); + }); + + it("should throw error without canvas", async () => { + const map = parse(wmapSource); + + await assert.rejects(async () => await renderToCanvas(map, null), { + message: "Canvas is required", + }); + }); + + it("should render without background when option is set", async () => { + const map = parse(wmapSource); + const canvas = createCanvas(1384, 1084); + + await renderToCanvas(map, canvas, StageType.ACTIVITIES, { + showBackground: false, + }); + + assert.strictEqual(canvas.width, 1384); + assert.strictEqual(canvas.height, 1084); + }); + }); + + describe("renderToImage", () => { + it("should render a map to PNG stream", async () => { + const map = parse(wmapSource); + const stream = await renderToImage(map, "png"); + + assert.ok(stream); + + const out = fs.createWriteStream("test-output.png"); + stream.pipe(out); + + await new Promise((resolve, reject) => { + out.on("finish", resolve); + out.on("error", reject); + }); + }); + + it("should render a map to JPEG stream", async () => { + const map = parse(wmapSource); + const stream = await renderToImage(map, "jpeg"); + + assert.ok(stream); + + const out = fs.createWriteStream("test-output.jpeg"); + stream.pipe(out); + + await new Promise((resolve, reject) => { + out.on("finish", resolve); + out.on("error", reject); + }); + }); + + it("should render a map to PDF stream", async () => { + const map = parse(wmapSource); + const stream = await renderToImage(map, "pdf"); + + assert.ok(stream); + + const out = fs.createWriteStream("test-output.pdf"); + stream.pipe(out); + + await new Promise((resolve, reject) => { + out.on("finish", resolve); + out.on("error", reject); + }); + }); + + it("should render without background when option is set", async () => { + const map = parse(wmapSource); + const stream = await renderToImage( + map, + "png", + StageType.ACTIVITIES, + { + showBackground: false, + }, + ); + + assert.ok(stream); + + const out = fs.createWriteStream("test-output-no-bg.png"); + stream.pipe(out); + + await new Promise((resolve, reject) => { + out.on("finish", resolve); + out.on("error", reject); + }); + }); + + it("should throw error for invalid image type", async () => { + const map = parse(wmapSource); + + await assert.rejects( + async () => await renderToImage(map, "invalid"), + { + message: + "Invalid image type. Must be one of: png, jpeg, pdf", + }, + ); + }); + + it("should throw error without map", async () => { + await assert.rejects(async () => await renderToImage(null, "png"), { + message: "Map object is required", + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..440d298 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist" + }, + "include": ["src/**/*.js"] +} |