aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-15 16:30:22 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-16 00:19:32 +0100
commit566eb2c9cce799ed8a3becc6d81560f1540eaacf (patch)
tree4a711b16ff15c61842856a2bf2b830079f30bbcf
Initial Release
-rw-r--r--.DS_Storebin0 -> 6148 bytes
-rw-r--r--.editorconfig10
-rw-r--r--.gitignore4
-rw-r--r--CHANGELOG14
-rw-r--r--LICENSE661
-rw-r--r--Makefile53
-rw-r--r--README.md221
-rw-r--r--benchmark/index.js39
-rw-r--r--dist/configuration.d.ts179
-rw-r--r--dist/image_type.d.ts9
-rw-r--r--dist/index.d.ts71
-rw-r--r--dist/patterns.d.ts16
-rw-r--r--dist/renderer.d.ts12
-rw-r--r--dist/smart-label-positioning.d.ts33
-rw-r--r--dist/stage_type.d.ts149
-rw-r--r--dist/utils.d.ts21
-rw-r--r--eslint.config.js38
-rw-r--r--package.json32
-rw-r--r--pnpm-lock.yaml1530
-rw-r--r--pnpm-workspace.yaml2
-rw-r--r--src/configuration.js176
-rw-r--r--src/image_type.js9
-rw-r--r--src/index.js153
-rw-r--r--src/patterns.js74
-rw-r--r--src/renderer.js776
-rw-r--r--src/smart-label-positioning.js524
-rw-r--r--src/stage_type.js187
-rw-r--r--src/utils.js50
-rw-r--r--test-output-no-bg.pngbin0 -> 204982 bytes
-rw-r--r--test-output.jpegbin0 -> 325379 bytes
-rw-r--r--test-output.pdfbin0 -> 136718 bytes
-rw-r--r--test-output.pngbin0 -> 204929 bytes
-rw-r--r--test/example.wmap122
-rw-r--r--test/huge.wmap1960
-rw-r--r--test/test.js139
-rw-r--r--tsconfig.json9
36 files changed, 7273 insertions, 0 deletions
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..e62e93c
--- /dev/null
+++ b/.DS_Store
Binary files differ
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.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..07d102f
--- /dev/null
+++ b/LICENSE
@@ -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
new file mode 100644
index 0000000..37f3426
--- /dev/null
+++ b/test-output-no-bg.png
Binary files differ
diff --git a/test-output.jpeg b/test-output.jpeg
new file mode 100644
index 0000000..77418d5
--- /dev/null
+++ b/test-output.jpeg
Binary files differ
diff --git a/test-output.pdf b/test-output.pdf
new file mode 100644
index 0000000..095c265
--- /dev/null
+++ b/test-output.pdf
Binary files differ
diff --git a/test-output.png b/test-output.png
new file mode 100644
index 0000000..a0b059a
--- /dev/null
+++ b/test-output.png
Binary files differ
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"]
+}