[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true,\n    \"node\": true\n  },\n  \"extends\": \"airbnb-base\",\n  \"ignores\": [\"*.d.ts\"],\n  \"parserOptions\": {\n    \"ecmaVersion\": \"latest\",\n    \"ecmaFeatures\": { \"jsx\": true },\n    \"sourceType\": \"module\"\n  },\n  \"rules\": {\n    \"arrow-parens\": 0,\n    \"camelcase\": [\"error\", { \"allow\": [\".*\"] }],\n    \"func-names\": [\"error\", \"never\"],\n    \"indent\": [\"error\", \"tab\"],\n    \"no-bitwise\": 0,\n    \"no-continue\": 0,\n    \"no-labels\": 0,\n    \"no-plusplus\": 0,\n    \"no-param-reassign\": [\"error\", {\n    \t\"props\": true,\n      \"ignorePropertyModificationsFor\": [\n\t      \"graph\",\n\t      \"epsilon\",\n\t      \"faces_edges\"\n      ]\n    }],\n    \"no-sparse-arrays\": 0,\n    \"no-tabs\": 0,\n    \"no-restricted-syntax\": [\"error\", \"ForInStatement\", \"ForOfStatement\", \"WithStatement\"],\n    \"import/extensions\": [\"error\", \"always\", { \"ignorePackages\": true }],\n    \"import/no-relative-packages\": 0,\n    \"object-shorthand\": 0,\n    \"object-curly-newline\": 0,\n    \"import/prefer-default-export\": 0,\n    \"prefer-rest-params\": 0,\n    \"prefer-default-export\": 0,\n    \"prefer-destructuring\": 0,\n    \"quotes\": [\"error\", \"double\", { \"allowTemplateLiterals\": true }]\n  }\n}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non:\n  # Triggers the workflow on push or pull request events but only for the main branch\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  test:\n    name: run all tests\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 18\n\n      - name: install dependencies\n        run: npm install\n\n      - name: run tests\n        run: npm run test\n"
  },
  {
    "path": ".gitignore",
    "content": "*.DS_Store\nnode_modules/\ncoverage/\ndocs/\nmodule/\ntests/tmp/\ntypes/\npackage-lock.json\n*.d.ts.map\n"
  },
  {
    "path": ".npmignore",
    "content": ".DS_Store\n.eslintrc.js\n.git\n.gitignore\n.travis.yml\n\ncoverage/\ndocs/\nsrc/\ntests/\n\nbuild-shaders.py\nchangelog.md\ncontribute.md\ndevelopers.md\ndocs-style.css\nrollup.config.js\ntsconfig.json\ntypedoc.config.json\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - lts/*"
  },
  {
    "path": "bundle-shaders.py",
    "content": "# due to the issues around loading raw text files,\n# this bundler will convert the shader files into an\n# es6 Javascript file with named exports of the\n# raw text as strings\n\n# IMPORTANT! this will ignore any filenames containing\n# the substring \"sketch\". there are a few files that\n# are being worked on in development we want to ignore\n\nimport re\nfrom os import listdir\nfrom os.path import isfile, join\n\n# list here the locations where shader files exist, and the\n# location and filename to write the bundled named export file\nsources = [{\n\t\"read\": \"src/webgl/creasePattern/shaders/\",\n\t\"write\": \"src/webgl/creasePattern/shaders.js\"\n}, {\n\t\"read\": \"src/webgl/foldedForm/shaders/\",\n\t\"write\": \"src/webgl/foldedForm/shaders.js\"\n}]\n\n# convert a raw text file into a javascript string which follows this format:\n# export const FILENAME = `FILECONTENTS`;\n# where FILENAME is the file's name but made into a valid javascript variable\ndef makeNamedExport(file, path):\n\tf = open(join(path, file))\n\t# convert the file into a valid JS variable name\n\tvariablename = re.sub(\"[-\\.]\", \"_\", file)\n\t# compress multiple consecutive newlines into just one\n\tcontents = re.sub(\"[\\n]{2,}\", \"\\n\", f.read())\n\tstring = \"export const \" + variablename + \" = `\" + contents + \"`;\"\n\tf.close()\n\treturn string\n\n# run makeNamedExport on all files in a directory and\n# join them into one large newline-separated string\ndef makeBundle(files, path):\n\treturn \"\\n\\n\".join(map(lambda file: makeNamedExport(file, path), files)) + \"\\n\"\n\n# iterate over the locations from sources, convert them to bundles,\n# save the stringified bundle as a new file\nfor source in sources:\n\t# ignore \"sketch\" files\n\tfiles = [f for f in listdir(source[\"read\"]) if (isfile(join(source[\"read\"], f)) and \"sketch\" not in f)]\n\tbundle = makeBundle(files, source[\"read\"])\n\twith open(source[\"write\"], 'w') as f:\n\t\tf.write(bundle)\n"
  },
  {
    "path": "changelog.md",
    "content": "# 0.9.33 alpha\n\n`pleat()` when given parallel lines will now correct for the case when the lines' vectors point in the opposite directions.\n\nmany of the intersection methods have been refactored and have small or dramatic speedups.\n\nnew method: `getEdgesRectOverlap()`\n\nnew method: `findSymmetryLines()` and `findSymmetryLine()` using the lines in the graph to uncover a line of symmetry.\n\nnew method: `getEdgesLine()` and its subroutine `clusterParallelVectors()` which performs a similar function as `clusterScalars()`.\n\nnew overlap methods:\n\n```javascript\near.graph.getFacesLineOverlap()\near.graph.getFacesRayOverlap()\near.graph.getFacesSegmentOverlap()\n```\n\nnew method `ear.graph.getFramesByClassName()`\n\nnew method `ear.graph.getEdgeBetweenVertices()`\n\near.graph.makePlanarFaces now returns an object already in FOLD form, instead of the data being inverted into an array of objects\n\nAll axiom methods return an *array* of lines. Before, some would and others would not. The system is now consistent.\n\nreplace `getGraphKeysWithPrefix` with `filterKeysWithPrefix`, maintaining the functionality of `getGraphKeysWithPrefix` where it will add the `_` character to match against. same with Suffix methods.\n\nnew method: `subgraph` and `subgraphWithFaces`, where *subgraphWithEdges* and *subgraphWithVertices* could be written in similar form.\n\nMethods renamed:\n\n- `{graph/cp/origami}.copy` -> `{graph/cp/origami}.clone`\n- `fragment` -> `planarize`\n- `getBoundingBox` -> `boundingBox`\n- `getBoundaryVertices` -> `boundaryVertices`\n- `getBoundary` -> `boundary`\n- `getPlanarBoundary` -> `planarBoundary`\n- `makeFacesFacesOverlap` -> `getFacesFaces2DOverlap`\n- `makeFacesCenter` -> `makeFacesCentroid2D`\n- `makeFacesCenterQuick` -> `makeFacesConvexCenter`\n- `getDuplicateEdges` -> `duplicateEdges`\n- `getCircularEdges` -> `circularEdges`\n- `getVerticesClusters` -> `verticesClusters`\n- `getDuplicateVertices` -> `duplicateVertices`\n- `getEdgeIsolatedVertices` -> `edgeIsolatedVertices`\n- `getFaceIsolatedVertices` -> `faceIsolatedVertices`\n- `getIsolatedVertices` -> `isolatedVertices`\n- `getCoplanarFacesGroups` -> `coplanarFacesGroups`\n- `getOverlappingFacesGroups` -> `overlappingFacesGroups`\n- `makeTrianglePairs` -> `chooseTwoPairs`\n- `clusterArrayValues` -> `clusterScalars`\n- `getDisjointedVertices` -> `disjointVerticesSets`\n\n`makeEdgesAssignment` has been renamed to `makeEdgesAssignmentSimple` and `makeEdgesAssignment` now also assigns \"B\" boundary edges by checking edges_faces for # of incident faces.\n\nnew WebGL implementation. see: [readme.md](https://github.com/robbykraft/Origami/tree/master/src/webgl) and [foldfile.com](https://foldfile.com)\n\nVarious methods will now throw Errors instead of console.error or console.warn. Not all console.warn have been removed however. The distinction is that if the function is still able to generate a solution, it will console.warn. If the function generated something which contains errors or misleading information, it throws an error.\n\nNew subcategory `ear.webgl` containing at least: `createProgram`, `initialize`, `foldedForm`, `creasePattern`, `rebuildViewport`, `makeProjectionMatrix`, `makeModelMatrix`, `drawProgram`, `deallocProgram`, with many more methods specific to drawing different styles with different shaders.\n\nnew Matrix4 type. new Quaternion type. new projection matrices.\n\nnew method `ear.graph.nearest`, which was already in the `graph()` object as a prototype method. now exists in the top level.\n\nall matrix scale methods takes an array of values instead of one number, allowing non-uniform axis scaling.\n\n`ear.graph.getBoundingBox` will return \"undefined\" if the graph does not contain any vertices_coords. previously this would throw an error.\n\nnew `nudgeVerticesWithFacesLayer`, `nudgeVerticesWithFaceOrders` for nudging vertices in an exploded graph based on layer order.\n\n`src/layer/topological.js` contains a new topological sort method. currently not being exported.\n\nnew `.convert` with the first (of many, hopefully) file conversions: `.obj` to `.fold`, which includes computing edges_foldAngle and giving them a \"M\" \"V\" assignment.\n\nnew methods `getCoplanarFacesGroups`,  `getOverlappingFacesGroups` which will find all groups of faces which share the same plane in 3D space (`getCoplanarFacesGroups`), and then in the case of `getOverlappingFacesGroups` actually compute whether or not they overlap.\n\nnew method `selfRelationalUniqueIndexPairs`, given any array of self-referential data (vertices_vertices, faces_faces, etc), create a list of unique pairwise combinations of related indices.\n\nrewrite of `getVerticesClusters`, implemented a line sweep and the runtime has been massively improved.\n\nrefactor fold/keys and fold/spec, simplify methods that get exposed in the final build.\n\n# 0.9.32 alpha\n\nnew layer solver implementation at ear.layer.solver(), huge performance improvement. solver returns data bound to a prototype which includes methods to process and analyze the data. methods include\n\n```javascript\ncount()\nsolution(...indices)\nallSolutions()\nfacesLayer(...indices)\nallFacesLayers()\nfaceOrders(...indices)\nallFaceOrders()\n```\n\near.layer.assignmentSolver renamed to ear.layer.singleVertexAssignmentSolver.\n\near.math.enclosingBoundingBoxes, new method. test if one bounding box is entirely inside another.\n\near.graph.makeEdgesEdgesSimilar, answers which edges have the same endpoints (vertices) as other edges. endpoints can be in any order.\n\nperformance improvements for ear.graph.makeFacesFacesOverlap, ear.graph.makeEdgesFacesOverlap.\n\n# 0.9.31 alpha\n\naxiom function wrapper has changed. `validate()` is now exposed to the user, as is the `axiomInBoundary` methods. Normal-distance parameterization methods are now named as such.\n\nnew convex hull algorithm. two entrypoints: `convexHull` returns points, `convexHullIndices` returns indices of points from your points parameter array.\n\n`onLeave` added to the other three methods `onMove` `onPress` `onRelease` for SVG elements.\n\n# 0.9.3 alpha\n\nno longer requiring @xmldom/xmldom as a dependency. This library now has zero dependencies! [1]\n\n`clean()` takes no options now and performs one consistent task: removes all bad edges and vertices. if you like you can call each submethod now `removeDuplicateVertices`, `removeCircularEdges`, `removeDuplicateEdges`, `removeIsolatedVertices`.\n\naxiom function parameters have now changed to the much simpler \"point, line\" to consistenly separate types. This replaces \"point, origin, vector\" inputs where \"origin, vector\" are two components which describe a line, and \"point\" is points... **to update: modify your axiom function parameters, whever it asks for a line, make a ear.line() objects from your previous origin/vector pairs.**\n\n```javascript\near.graph.getBoundaryVertices // unsorted\near.graph.getBoundary.vertices // sorted\n```\n\n> [1] if you use Rabbit Ear in NodeJS and create a SVG elements (virtually), you will need to import @xmldom. Because this is such a unique rare case, this is left to the user. This will be explained in the documentation.\n"
  },
  {
    "path": "license",
    "content": " GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"rabbit-ear\",\n\t\"version\": \"0.9.4\",\n\t\"description\": \"origami design library\",\n\t\"exports\": {\n\t\t\".\": {\n\t\t\t\"import\": \"./module/index.js\",\n\t\t\t\"require\": \"./rabbit-ear.js\",\n\t\t\t\"types\": \"./types/index.d.ts\"\n\t\t},\n\t\t\"./*\": {\n\t\t\t\"import\": \"./module/*\",\n\t\t\t\"types\": \"./types/*\"\n\t\t}\n\t},\n\t\"type\": \"module\",\n\t\"types\": \"./types/index.d.ts\",\n\t\"scripts\": {\n\t\t\"build\": \"npm run clean && rollup -c && npm run types && npm run docs\",\n\t\t\"clean\": \"node tests/clean.js\",\n\t\t\"docs\": \"typedoc --options typedoc.config.json\",\n\t\t\"test\": \"mkdir -p ./tests/tmp && vitest\",\n\t\t\"types\": \"tsc\"\n\t},\n\t\"files\": [\n\t\t\"./rabbit-ear.js\",\n\t\t\"./rabbit-ear.module.js\",\n\t\t\"./license\",\n\t\t\"./package.json\",\n\t\t\"./readme.md\",\n\t\t\"./module\",\n\t\t\"./types\"\n\t],\n\t\"license\": \"GPLv3\",\n\t\"author\": {\n\t\t\"name\": \"Maya Kraft\",\n\t\t\"email\": \"maya@kraft.work\",\n\t\t\"url\": \"https://kraft.work\"\n\t},\n\t\"keywords\": [\n\t\t\"origami\",\n\t\t\"design\",\n\t\t\"geometry\",\n\t\t\"paper\",\n\t\t\"fold\",\n\t\t\"folding\",\n\t\t\"foldability\",\n\t\t\"generative\",\n\t\t\"parametric\",\n\t\t\"diagram\",\n\t\t\"architecture\",\n\t\t\"creative\",\n\t\t\"art\"\n\t],\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git://github.com/rabbit-ear/rabbit-ear.git\"\n\t},\n\t\"homepage\": \"https://rabbitear.org\",\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/rabbit-ear/rabbit-ear/issues\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@rollup/plugin-terser\": \"^0.4.3\",\n\t\t\"@xmldom/xmldom\": \"^0.8.10\",\n\t\t\"earcut\": \"^2.2.4\",\n\t\t\"eslint\": \"^8.47.0\",\n\t\t\"eslint-config-airbnb-base\": \"^15.0.0\",\n\t\t\"eslint-plugin-import\": \"^2.28.1\",\n\t\t\"rollup\": \"^3.28.1\",\n\t\t\"rollup-plugin-cleanup\": \"^3.2.1\",\n\t\t\"typedoc\": \"^0.25.13\",\n\t\t\"typedoc-github-wiki-theme\": \"^1.1.0\",\n\t\t\"typedoc-plugin-markdown\": \"^3.17.1\",\n\t\t\"typescript\": \"^5.2.2\",\n\t\t\"vitest\": \"^0.34.6\"\n\t}\n}\n"
  },
  {
    "path": "rabbit-ear.js",
    "content": "/* Rabbit Ear 0.9.4 alpha 2024-04-20 (c) Kraft, GNU GPLv3 License */\n!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).ear=t()}(this,(function(){\"use strict\";const e=\"object\"==typeof window&&\"object\"==typeof window.document,t=\"object\"==typeof process&&\"object\"==typeof process.versions&&(null!=process.versions.node||null!=process.versions.bun),r=\"object\"==typeof window&&\"Deno\"in window&&\"object\"==typeof window.Deno,s=t||r;var a=\"valid manifold required\",o=\"cycle not allowed\",c=\"replace() generated undefined\",n=\"foldAngles cannot be determined from flat-folded faces without an assignment\",i=\"WebGl not Supported\",l=\"only convex faces are supported\",d=\"window not set; if using node/deno include package @xmldom/xmldom and set ear.window = xmldom\",_=\"non-convex triangulation requires vertices_coords\",m=\"svgToFold found <style> in <svg>. rendering will be incomplete unless run in a major browser.\",g=\"LayerSolver bad input. no solution possible\";const v={window:void 0};e&&(v.window=window);const RabbitEarWindow$1=()=>{if(void 0===v.window)throw new Error(d);return v.window},p=1e-6,u=180/Math.PI,h=Math.PI/180,b=2*Math.PI;var y=Object.freeze({__proto__:null,D2R:h,EPSILON:p,R2D:u,TWO_PI:b});const safeAdd=(e,t)=>e+(t||0),magnitude=e=>Math.sqrt(e.map((e=>e*e)).reduce(safeAdd,0)),magnitude2=e=>Math.sqrt(e[0]*e[0]+e[1]*e[1]),magnitude3=e=>Math.sqrt(e[0]*e[0]+e[1]*e[1]+e[2]*e[2]),magSquared2=e=>e[0]*e[0]+e[1]*e[1],magSquared=e=>e.map((e=>e*e)).reduce(safeAdd,0),normalize$2=e=>{const t=magnitude(e);return 0===t?e:e.map((e=>e/t))},normalize2=e=>{const t=magnitude2(e);return 0===t?[e[0],e[1]]:[e[0]/t,e[1]/t]},normalize3=e=>{const t=magnitude3(e);return 0===t?e:[e[0]/t,e[1]/t,e[2]/t]},scale$1=(e,t)=>e.map((e=>e*t)),scale2$1=(e,t)=>[e[0]*t,e[1]*t],scale3$1=(e,t)=>[e[0]*t,e[1]*t,e[2]*t],scaleNonUniform=(e,t)=>e.map(((e,r)=>e*t[r])),scaleNonUniform2=(e,t)=>[e[0]*t[0],e[1]*t[1]],scaleNonUniform3=(e,t)=>[e[0]*t[0],e[1]*t[1],e[2]*t[2]],add=(e,t)=>e.map(((e,r)=>e+(t[r]||0))),add2=(e,t)=>[e[0]+t[0],e[1]+t[1]],add3=(e,t)=>[e[0]+t[0],e[1]+t[1],e[2]+t[2]],subtract=(e,t)=>e.map(((e,r)=>e-(t[r]||0))),subtract2=(e,t)=>[e[0]-t[0],e[1]-t[1]],subtract3=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],dot=(e,t)=>e.map(((r,s)=>e[s]*t[s])).reduce(safeAdd,0),dot2=(e,t)=>e[0]*t[0]+e[1]*t[1],dot3=(e,t)=>e[0]*t[0]+e[1]*t[1]+e[2]*t[2],midpoint=(e,t)=>e.map(((e,r)=>(e+t[r])/2)),midpoint2=(e,t)=>scale2$1(add2(e,t),.5),average=(...e)=>{if(0===e.length)return;const t=e[0].length>0?e[0].length:0,r=Array(t).fill(0);return Array.from(e).forEach((e=>r.forEach(((t,s)=>{r[s]+=e[s]||0})))),r.map((t=>t/e.length))},average2=(...e)=>{if(!e||!e.length)return;const t=e.reduce(((e,t)=>add2(e,t)),[0,0]);return[t[0]/e.length,t[1]/e.length]},average3=(...e)=>{if(!e||!e.length)return;const t=e.reduce(((e,t)=>add3(e,t)),[0,0,0]);return[t[0]/e.length,t[1]/e.length,t[2]/e.length]},lerp=(e,t,r=0)=>{const s=1-r;return e.map(((e,a)=>e*s+(t[a]||0)*r))},cross2=(e,t)=>e[0]*t[1]-e[1]*t[0],cross3=(e,t)=>[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]],distance=(e,t)=>Math.sqrt(e.map(((r,s)=>(e[s]-t[s])**2)).reduce(safeAdd,0)),distance2=(e,t)=>{const r=e[0]-t[0],s=e[1]-t[1];return Math.sqrt(r*r+s*s)},distance3=(e,t)=>{const r=e[0]-t[0],s=e[1]-t[1],a=e[2]-t[2];return Math.sqrt(r*r+s*s+a*a)},flip=e=>e.map((e=>-e)),flip2=e=>[-e[0],-e[1]],flip3=e=>[-e[0],-e[1],-e[2]],rotate90=e=>[-e[1],e[0]],rotate270=e=>[e[1],-e[0]],parallelNormalized=(e,t,r=p)=>1-Math.abs(dot(e,t))<r,parallel=(e,t,r=p)=>parallelNormalized(normalize$2(e),normalize$2(t),r),parallel2=(e,t,r=p)=>Math.abs(cross2(e,t))<r,parallel3=(e,t,r=p)=>magnitude3(cross3(e,t))<r,resize=(e,t)=>t.length===e?t:Array(e).fill(0).map(((e,r)=>t[r]?t[r]:e)),resize2=e=>[e[0]||0,e[1]||0],resize3=e=>[e[0]||0,e[1]||0,e[2]||0],basisVectors2=(e=[1,0])=>{const t=normalize2(e);return[t,rotate90(t)]},basisVectors3=(e=[1,0,0])=>{const t=normalize3(e),r=[[1,0,0],[0,1,0],[0,0,1]].map((e=>cross3(e,t))),s=r.map(magnitude3).map(((e,t)=>({n:e,i:t}))).sort(((e,t)=>t.n-e.n)).map((e=>e.i)).shift(),a=normalize3(r[s]);return[t,a,cross3(t,a)]};var E=Object.freeze({__proto__:null,add:add,add2:add2,add3:add3,average:average,average2:average2,average3:average3,basisVectors:e=>2===e.length?basisVectors2([e[0],e[1]]):basisVectors3([e[0],e[1],e[2]]),basisVectors2:basisVectors2,basisVectors3:basisVectors3,cross2:cross2,cross3:cross3,degenerate:(e,t=p)=>e.map((e=>Math.abs(e))).reduce(safeAdd,0)<t,distance:distance,distance2:distance2,distance3:distance3,dot:dot,dot2:dot2,dot3:dot3,flip:flip,flip2:flip2,flip3:flip3,lerp:lerp,magSquared:magSquared,magSquared2:magSquared2,magnitude:magnitude,magnitude2:magnitude2,magnitude3:magnitude3,midpoint:midpoint,midpoint2:midpoint2,midpoint3:(e,t)=>scale3$1(add3(e,t),.5),normalize:normalize$2,normalize2:normalize2,normalize3:normalize3,parallel:parallel,parallel2:parallel2,parallel3:parallel3,parallelNormalized:parallelNormalized,resize:resize,resize2:resize2,resize3:resize3,resizeUp:(e,t)=>[e,t].map((r=>resize(Math.max(e.length,t.length),r))),rotate270:rotate270,rotate90:rotate90,scale:scale$1,scale2:scale2$1,scale3:scale3$1,scaleNonUniform:scaleNonUniform,scaleNonUniform2:scaleNonUniform2,scaleNonUniform3:scaleNonUniform3,subtract:subtract,subtract2:subtract2,subtract3:subtract3});const vectorToAngle=e=>Math.atan2(e[1],e[0]),angleToVector=e=>[Math.cos(e),Math.sin(e)],pointsToLine2=(e,t)=>({vector:subtract2(t,e),origin:resize2(e)}),pointsToLine3=(e,t)=>({vector:subtract3(t,e),origin:resize3(e)}),pointsToLine=(e,t)=>3===e.length&&3===t.length?pointsToLine3(e,t):pointsToLine2(e,t),vecLineToUniqueLine=({vector:e,origin:t})=>{const r=magnitude(e),s=rotate90([e[0],e[1]]),a=dot(t,s)/r;return{normal:scale2$1(s,1/r),distance:a}},uniqueLineToVecLine=({normal:e,distance:t})=>({vector:rotate270(e),origin:scale2$1(e,t)});var M=Object.freeze({__proto__:null,angleToVector:angleToVector,pointsToLine:pointsToLine,pointsToLine2:pointsToLine2,pointsToLine3:pointsToLine3,uniqueLineToVecLine:uniqueLineToVecLine,vecLineToUniqueLine:vecLineToUniqueLine,vectorToAngle:vectorToAngle});const epsilonEqual=(e,t,r=p)=>Math.abs(e-t)<r,epsilonCompare=(e,t,r=p)=>epsilonEqual(e,t,r)?0:Math.sign(e-t),epsilonEqualVectors=(e,t,r=p)=>{for(let s=0;s<Math.max(e.length,t.length);s+=1)if(!epsilonEqual(e[s]||0,t[s]||0,r))return!1;return!0},include=(e,t=p)=>e>-t,exclude=(e,t=p)=>e>t,includeL=(e,t)=>!0,A=include,x=exclude,includeS=(e,t=p)=>e>-t&&e<1+t,excludeS=(e,t=p)=>e>t&&e<1-t;var O=Object.freeze({__proto__:null,epsilonCompare:epsilonCompare,epsilonEqual:epsilonEqual,epsilonEqualVectors:epsilonEqualVectors,exclude:exclude,excludeL:(e,t)=>!0,excludeR:x,excludeS:excludeS,include:include,includeL:includeL,includeR:A,includeS:includeS});const isCounterClockwiseBetween=(e,t,r)=>{for(;r<t;)r+=b;for(;e>t;)e-=b;for(;e<t;)e+=b;return e<r},clockwiseAngleRadians=(e,t)=>{for(;e<0;)e+=b;for(;t<0;)t+=b;for(;e>b;)e-=b;for(;t>b;)t-=b;const r=e-t;return r>=0?r:b-(t-e)},counterClockwiseAngleRadians=(e,t)=>{for(;e<0;)e+=b;for(;t<0;)t+=b;for(;e>b;)e-=b;for(;t>b;)t-=b;const r=t-e;return r>=0?r:b-(e-t)},clockwiseAngle2=(e,t)=>{const r=t[0]*e[0]+t[1]*e[1],s=t[0]*e[1]-t[1]*e[0];let a=Math.atan2(s,r);return a<0&&(a+=b),a},counterClockwiseAngle2=(e,t)=>{const r=e[0]*t[0]+e[1]*t[1],s=e[0]*t[1]-e[1]*t[0];let a=Math.atan2(s,r);return a<0&&(a+=b),a},clockwiseBisect2=(e,t)=>angleToVector(vectorToAngle(e)-clockwiseAngle2(e,t)/2),clockwiseSubsectRadians=(e,t,r)=>{const s=clockwiseAngleRadians(e,t)/r;return Array.from(Array(r-1)).map(((t,r)=>e+s*(r+1)))},counterClockwiseSubsectRadians=(e,t,r)=>{const s=counterClockwiseAngleRadians(e,t)/r;return Array.from(Array(r-1)).map(((t,r)=>e+s*(r+1)))},counterClockwiseSubsect2=(e,t,r)=>{const s=Math.atan2(e[1],e[0]),a=Math.atan2(t[1],t[0]);return counterClockwiseSubsectRadians(s,a,r).map(angleToVector)},counterClockwiseOrderRadians=e=>{const t=e.map(((e,t)=>t)).sort(((t,r)=>e[t]-e[r]));return t.slice(t.indexOf(0),t.length).concat(t.slice(0,t.indexOf(0)))},counterClockwiseOrder2=e=>counterClockwiseOrderRadians(e.map(vectorToAngle)),counterClockwiseSectorsRadians=e=>counterClockwiseOrderRadians(e).map((t=>e[t])).map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>counterClockwiseAngleRadians(e[0],e[1]))),counterClockwiseSectors2=e=>counterClockwiseSectorsRadians(e.map(vectorToAngle)),threePointTurnDirection=(e,t,r,s=p)=>{const a=normalize2(subtract2(t,e)),o=normalize2(subtract2(r,e)),c=cross2(a,o);return epsilonEqual(c,0,s)?epsilonEqual(distance2(e,t)+distance2(t,r),distance2(e,r))?0:void 0:Math.sign(c)};var j=Object.freeze({__proto__:null,clockwiseAngle2:clockwiseAngle2,clockwiseAngleRadians:clockwiseAngleRadians,clockwiseBisect2:clockwiseBisect2,clockwiseSubsect2:(e,t,r)=>{const s=Math.atan2(e[1],e[0]),a=Math.atan2(t[1],t[0]);return clockwiseSubsectRadians(s,a,r).map(angleToVector)},clockwiseSubsectRadians:clockwiseSubsectRadians,counterClockwiseAngle2:counterClockwiseAngle2,counterClockwiseAngleRadians:counterClockwiseAngleRadians,counterClockwiseBisect2:(e,t)=>angleToVector(vectorToAngle(e)+counterClockwiseAngle2(e,t)/2),counterClockwiseOrder2:counterClockwiseOrder2,counterClockwiseOrderRadians:counterClockwiseOrderRadians,counterClockwiseSectors2:counterClockwiseSectors2,counterClockwiseSectorsRadians:counterClockwiseSectorsRadians,counterClockwiseSubsect2:counterClockwiseSubsect2,counterClockwiseSubsectRadians:counterClockwiseSubsectRadians,isCounterClockwiseBetween:isCounterClockwiseBetween,threePointTurnDirection:threePointTurnDirection});const clampLine=e=>e,clampSegment=e=>e<-p?0:e>1.000001?1:e,collinearPoints=(e,t,r,s=p)=>{const a=[[e,t],[t,r]].map((e=>subtract(e[1],e[0]))).map((e=>normalize$2(e)));return epsilonEqual(1,Math.abs(dot(a[0],a[1])),s)},collinearBetween=(e,t,r,s=!1,a=p)=>{if([e,r].map((e=>epsilonEqualVectors(t,e,a))).reduce(((e,t)=>e||t),!1))return s;const o=[[e,t],[t,r]].map((e=>subtract(e[1],e[0]))).map((e=>normalize$2(e)));return epsilonEqual(1,dot(o[0],o[1]),p)},collinearLines3=(e,t,r=p)=>parallel3(e.vector,t.vector,r)&&parallel3(e.vector,subtract3(t.origin,e.origin),r),pleat$2=(e,t,r,s=p)=>{const a=cross2(e.vector,t.vector),o=cross2(subtract2(t.origin,e.origin),t.vector)/a,[c,n]=[e.vector,t.vector].map(normalize2);if(Math.abs(cross2(c,n))<s)return((e,t,r)=>{const s=dot(e.vector,t.vector)<0,a=e.vector,o=s?flip(t.vector):t.vector,c=Array.from(Array(r-1)).map(((s,a)=>lerp(e.origin,t.origin,(a+1)/r))),n=Array.from(Array(r-1)).map(((e,t)=>lerp(a,o,(t+1)/r))).map(((e,t)=>({vector:[e[0],e[1]],origin:[c[t][0],c[t][1]]}))),i=[n,n];return i[s?0:1]=[],i})(e,t,r);const i=a>-s?[[e.vector,t.vector],[flip2(t.vector),e.vector]]:[[t.vector,e.vector],[flip2(e.vector),t.vector]],l=[counterClockwiseSubsect2(i[0][0],i[0][1],r),counterClockwiseSubsect2(i[1][0],i[1][1],r)],d=add2(e.origin,scale2$1(e.vector,o)),_=Array.from(Array(r-1)).map((()=>d)),[m,g]=l.map((e=>e.map(((e,t)=>({vector:e,origin:_[t]})))));return[m,g]},bisectLines2=(e,t,r=p)=>{const[s,a]=pleat$2(e,t,2,r).map((e=>e[0])),o=[s,a];return o.forEach(((e,t)=>{void 0===e&&delete o[t]})),o};var w=Object.freeze({__proto__:null,bisectLines2:bisectLines2,clampLine:clampLine,clampRay:e=>e<-p?0:e,clampSegment:clampSegment,collinearBetween:collinearBetween,collinearLines2:(e,t,r=p)=>parallel2(e.vector,t.vector,r)&&parallel2(e.vector,subtract2(t.origin,e.origin),r),collinearLines3:collinearLines3,collinearPoints:collinearPoints,pleat:pleat$2});const rangeUnion=(e,t)=>{const r=t[0]<=t[1];return e[0]<=e[1]?[Math.min(e[0],r?t[0]:t[1]),Math.max(e[1],r?t[1]:t[0])]:[Math.min(e[1],r?t[0]:t[1]),Math.max(e[0],r?t[1]:t[0])]},doRangesOverlap=(e,t,r=p)=>{const s=e[0]<e[1]?e:[e[1],e[0]],a=t[0]<t[1]?t:[t[1],t[0]];return Math.min(s[1],a[1])-Math.max(s[0],a[0])>r};var F=Object.freeze({__proto__:null,doRangesOverlap:doRangesOverlap,rangeUnion:rangeUnion});const clusterSortedGeneric=(e,t)=>{if(!e.length)return[];const r=e.map(((e,t)=>t)),s=[[r.shift()]];return r.forEach((r=>{const a=s[s.length-1],o=a[a.length-1];t(e[o],e[r])?a.push(r):s.push([r])})),s},clusterUnsortedIndices=(e,t)=>{if(!e.length)return[];const r=e.slice(),s=[[r.shift()]];return r.forEach((e=>{const r=s.map(((r,s)=>t(r[0],e)?s:void 0)).filter((e=>void 0!==e)).shift();void 0!==r?s[r].push(e):s.push([e])})),s},clusterScalars=(e,t=p)=>{const r=e.map(((e,t)=>({v:e,i:t}))).sort(((e,t)=>e.v-t.v)).map((e=>e.i)).filter((()=>!0)),s=r.map((t=>e[t]));return clusterSortedGeneric(s,((e,r)=>Math.abs(e-r)<t)).map((e=>e.map((e=>r[e]))))},clusterParallelVectors=(e,t=p)=>{const r=e.map(normalize$2),s=[[0]];e:for(let e=1;e<r.length;e+=1){for(let a=0;a<s.length;a+=1)if(parallelNormalized(r[e],r[s[a][0]],t)){s[a].push(e);continue e}s.push([e])}return s};var S=Object.freeze({__proto__:null,clusterParallelVectors:clusterParallelVectors,clusterRanges:(e,t=p)=>{const r=e.map((([e,t],r)=>({v:Math.min(e,t),i:r}))).sort(((e,t)=>e.v-t.v)).map((e=>e.i)).filter((()=>!0)),s=r.map((t=>e[t]));let a=[...s[0]];return clusterSortedGeneric(s,((e,r)=>{const s=doRangesOverlap(a,r,t);return a=s?rangeUnion(a,r):[...r],s})).map((e=>e.map((e=>r[e]))))},clusterScalars:clusterScalars,clusterSortedGeneric:clusterSortedGeneric,clusterUnsortedIndices:clusterUnsortedIndices});const intersectLineLine=(e,t,r=includeL,s=includeL,a=p)=>{const o=cross2(normalize2(e.vector),normalize2(t.vector));if(Math.abs(o)<a)return{a:void 0,b:void 0,point:void 0};const c=cross2(e.vector,t.vector),n=-c,i=[t.origin[0]-e.origin[0],t.origin[1]-e.origin[1]],l=[-i[0],-i[1]],d=cross2(i,t.vector)/c,_=cross2(l,e.vector)/n;return r(d,a/magnitude2(e.vector))&&s(_,a/magnitude2(t.vector))?{a:d,b:_,point:add2(e.origin,scale2$1(e.vector,d))}:{a:void 0,b:void 0,point:void 0}},intersectCircleLine=(e,t,r=include,s=includeL,a=p)=>{const o=t.vector[0]**2+t.vector[1]**2,c=Math.sqrt(o),n=0===c?t.vector:scale2$1(t.vector,1/c),i=rotate90(n),l=subtract2(t.origin,e.origin),d=cross2(l,n);if(Math.abs(d)>e.radius+a)return;const _=Math.sqrt(e.radius**2-d**2),f=(t,r)=>e.origin[r]-i[r]*d+n[r]*t,m=Math.abs(e.radius-Math.abs(d))<a?[_].map((e=>[f(e,0),f(e,1)])):[-_,_].map((e=>[f(e,0),f(e,1)])),g=m.map((e=>e.map(((e,r)=>e-t.origin[r])))).map((e=>e[0]*t.vector[0]+t.vector[1]*e[1])).map((e=>e/o));return m.filter(((e,t)=>s(g[t],a)))},intersectPolygonLine=(e,t,r=includeL,s=p)=>{const a=e.map(((e,t,r)=>({vector:subtract2(r[(t+1)%r.length],e),origin:resize2(e)}))).map((e=>intersectLineLine(t,e,r,includeS,s))).filter((({point:e})=>void 0!==e)).sort(((e,t)=>e.a-t.a)).map((({a:e,point:t})=>({a:e,point:t})));return clusterSortedGeneric(a,((e,t)=>epsilonEqual(e.a,t.a,s))).map((([e])=>a[e]))};var C=Object.freeze({__proto__:null,intersectCircleLine:intersectCircleLine,intersectLineLine:intersectLineLine,intersectPolygonLine:intersectPolygonLine});const cubeRootSigned=e=>e<0?-((-e)**(1/3)):e**(1/3),normalAxiom1=(e,t)=>{const r=normalize2(rotate90(subtract2(t,e)));return[{normal:r,distance:dot2(add2(e,t),r)/2}]},axiom1=(e,t)=>[{vector:normalize2(subtract2(t,e)),origin:e}],normalAxiom2=(e,t)=>{const r=normalize2(subtract2(t,e));return[{normal:r,distance:dot2(add2(e,t),r)/2}]},axiom2=(e,t)=>[{vector:normalize2(rotate90(subtract2(t,e))),origin:midpoint2(e,t)}],normalAxiom3=(e,t)=>{const r=cross2(e.normal,t.normal);if(Math.abs(r)<p)return[{normal:e.normal,distance:(e.distance+t.distance*dot2(e.normal,t.normal))/2}];const s=[(e.distance*t.normal[1]-t.distance*e.normal[1])/r,(t.distance*e.normal[0]-e.distance*t.normal[0])/r],[a,o]=[add2,subtract2].map((r=>normalize2(r(e.normal,t.normal)))).map((e=>({normal:e,distance:dot2(s,e)})));return[a,o]},axiom3=(e,t)=>bisectLines2(e,t),normalAxiom4=(e,t)=>{const r=rotate90(e.normal);return[{normal:r,distance:dot2(t,r)}]},axiom4=({vector:e},t)=>[{vector:rotate90(normalize2(e)),origin:t}],normalAxiom5=(e,t,r)=>{const s=dot2(t,e.normal),a=e.distance-s,o=distance2(t,r);if(a>o)return[];const c=Math.sqrt(o*o-a*a),n=scale2$1(e.normal,a),i=add2(t,n),l=scale2$1(rotate90(e.normal),c),d=c<p?[i]:[add2(i,l),subtract2(i,l)],[_,m]=d.map((e=>normalize2(subtract2(r,e)))).map((e=>({normal:e,distance:dot2(t,e)})));return[_,m]},axiom5=(e,t,r)=>(intersectCircleLine({radius:distance2(t,r),origin:t},e)||[]).map((e=>({vector:normalize2(rotate90(subtract2(e,r))),origin:midpoint2(r,e)}))),normalAxiom6=(e,t,r,s)=>{if(Math.abs(1-dot2(e.normal,r)/e.distance)<.02)return[];const a=rotate90(e.normal),o=subtract2(add2(r,scale2$1(e.normal,e.distance)),scale2$1(s,2)),c=subtract2(scale2$1(e.normal,e.distance),r),n=dot2(s,t.normal)-t.distance,i=2*dot2(c,a),l=dot2(c,c),d=dot2(add2(o,c),a),_=dot2(o,c),m=dot2(a,t.normal),g=dot2(c,t.normal),v=m,u=n+d*m+g,h=n*i+_*m+d*g,b=n*l+_*g;let y=[];return Math.abs(v)>p?y=[b,h,u,v]:Math.abs(u)>p?y=[b,h,u]:Math.abs(h)>p&&(y=[b,h]),(e=>{const[t,r,s,a]=e;switch(e.length){case 2:return[-t/r];case 3:{const e=r**2-4*t*s;if(e<-p)return[];const a=-r/(2*s);if(e<p)return[a];const o=Math.sqrt(e)/(2*s);return[a+o,a-o]}case 4:{const e=s/a,o=r/a,c=(9*e*o-t/a*27-2*e**3)/54,n=((3*o-e**2)/9)**3+c**2,i=-e/3;if(n>0){const e=Math.sqrt(n);return[i+cubeRootSigned(c+e)+cubeRootSigned(c-e)]}if(Math.abs(n)<p){if(c<0)return[];const e=c**(1/3);return[i+2*e,i-e]}const l=Math.sqrt(-n),d=Math.atan2(l,c)/3,_=(c**2-n)**(1/6),m=_*Math.cos(d),g=_*Math.sin(d);return[i+2*m,i-m-Math.sqrt(3)*g,i-m+Math.sqrt(3)*g]}default:return[]}})(y).map((t=>add2(scale2$1(e.normal,e.distance),scale2$1(a,t)))).map((e=>({p:e,normal:normalize2(subtract2(e,r))}))).map((e=>({normal:e.normal,distance:dot2(e.normal,midpoint2(e.p,r))})))},axiom6=(e,t,r,s)=>normalAxiom6(vecLineToUniqueLine(e),vecLineToUniqueLine(t),r,s).map(uniqueLineToVecLine),normalAxiom7=(e,t,r)=>{const s=rotate90(e.normal),a=dot2(s,t.normal);if(Math.abs(a)<p)return;const o=dot2(r,s),c=dot2(r,t.normal);return[{normal:s,distance:(t.distance+2*o*a-c)/(2*a)}]},axiom7=(e,t,r)=>{const s=intersectLineLine(e,{vector:t.vector,origin:r},includeL,includeL).point;return void 0===s?[]:[{vector:normalize2(rotate90(subtract2(s,r))),origin:midpoint2(r,s)}]};var V=Object.freeze({__proto__:null,axiom1:axiom1,axiom2:axiom2,axiom3:axiom3,axiom4:axiom4,axiom5:axiom5,axiom6:axiom6,axiom7:axiom7,normalAxiom1:normalAxiom1,normalAxiom2:normalAxiom2,normalAxiom3:normalAxiom3,normalAxiom4:normalAxiom4,normalAxiom5:normalAxiom5,normalAxiom6:normalAxiom6,normalAxiom7:normalAxiom7});const z=[1,0,0,1],T=z.concat(0,0),multiplyMatrix2Vector2=(e,t)=>[e[0]*t[0]+e[2]*t[1]+e[4],e[1]*t[0]+e[3]*t[1]+e[5]],multiplyMatrix2Line2=(e,{vector:t,origin:r})=>({vector:[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1]],origin:[e[0]*r[0]+e[2]*r[1]+e[4],e[1]*r[0]+e[3]*r[1]+e[5]]}),multiplyMatrices2=(e,t)=>[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]],determinant2=e=>e[0]*e[3]-e[1]*e[2],invertMatrix2=e=>{const t=determinant2(e);if(!(Math.abs(t)<1e-12||Number.isNaN(t))&&Number.isFinite(e[4])&&Number.isFinite(e[5]))return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[3]*e[4])/t,(e[1]*e[4]-e[0]*e[5])/t]},makeMatrix2Scale=(e=[1,1],t=[0,0])=>[e[0],0,0,e[1],e[0]*-t[0]+t[0],e[1]*-t[1]+t[1]],makeMatrix2Reflect=(e,t=[0,0])=>{const r=Math.atan2(e[1],e[0]),s=Math.cos(r),a=Math.sin(r),o=Math.cos(-r),c=Math.sin(-r),n=s*o+a*c,i=s*-c+a*o,l=a*o+-s*c,d=a*-c+-s*o;return[n,i,l,d,t[0]+n*-t[0]+-t[1]*l,t[1]+i*-t[0]+-t[1]*d]};var P=Object.freeze({__proto__:null,determinant2:determinant2,identity2x2:z,identity2x3:T,invertMatrix2:invertMatrix2,makeMatrix2Reflect:makeMatrix2Reflect,makeMatrix2Rotate:(e,t=[0,0])=>{const r=Math.cos(e),s=Math.sin(e);return[r,s,-s,r,t[0],t[1]]},makeMatrix2Scale:makeMatrix2Scale,makeMatrix2Translate:(e=0,t=0)=>z.concat(e,t),makeMatrix2UniformScale:(e=1,t=[0,0])=>makeMatrix2Scale([e,e],t),multiplyMatrices2:multiplyMatrices2,multiplyMatrix2Line2:multiplyMatrix2Line2,multiplyMatrix2Vector2:multiplyMatrix2Vector2});const overlapLinePoint=({vector:e,origin:t},r,s=includeL,a=p)=>{const o=subtract2(r,t),c=magSquared(e),n=Math.sqrt(c);if(n<a)return!1;const i=[e[0]/n,e[1]/n],l=cross2(o,i),d=dot2(o,e)/c;return Math.abs(l)<a&&s(d,a/n)},overlapConvexPolygonPoint=(e,t,r=exclude,s=p)=>{const a=e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((([e,r])=>[subtract2(r,e),subtract2(t,e)])).map((([e,t])=>cross2(e,t))),o=Math.sign(a.reduce(((e,t)=>e+t),0));return{overlap:a.map((e=>e*o)).map((e=>r(e,s))).map(((e,t,r)=>e===r[0])).reduce(((e,t)=>e&&t),!0),t:a}},overlapConvexPolygons=(e,t,r=p)=>{for(let s=0;s<2;s+=1){const a=0===s?e:t,o=0===s?t:e;for(let e=0;e<a.length;e+=1){const t=a[e],s=rotate90(subtract2(a[(e+1)%a.length],a[e])),c=o.map((e=>subtract2(e,t))).map((e=>dot2(s,e))),n=a[(e+2)%a.length],i=dot2(s,subtract2(n,t))>0;if(c.map((e=>i?e<r:e>-r)).reduce(((e,t)=>e&&t),!0))return!1}}return!0},overlapBoundingBoxes=(e,t,r=p)=>{const s=Math.min(e.min.length,t.min.length);for(let a=0;a<s;a+=1)if(e.min[a]>t.max[a]+r||e.max[a]<t.min[a]-r)return!1;return!0};var $=Object.freeze({__proto__:null,overlapBoundingBoxes:overlapBoundingBoxes,overlapConvexPolygonPoint:overlapConvexPolygonPoint,overlapConvexPolygons:overlapConvexPolygons,overlapLinePoint:overlapLinePoint});const clipLineConvexPolygon=(e,{vector:t,origin:r},s=include,a=includeL,o=p)=>{const c=intersectPolygonLine(e,{vector:t,origin:r},includeL,o).map((({a:e})=>e));if(c.length<2)return;const n=((e,t,r)=>{if(e.length<2)return;let s=0,a=e.length-1;for(;s<a&&!t(e[s+1]-e[s],r);)s+=1;for(;a>s&&!t(e[a]-e[a-1],r);)a-=1;return s>=a?void 0:[e[s],e[a]]})(c,s,2*o/magnitude2(t));if(void 0===n)return;const i=n.map((e=>a(e)?e:e<.5?0:1));if(Math.abs(i[0]-i[1])<2*o/magnitude2(t))return;const l=add2(r,scale2$1(t,(i[0]+i[1])/2));return overlapConvexPolygonPoint(e,l,s,o).overlap?i.map((e=>add2(r,scale2$1(t,e)))):void 0},clipPolygonPolygon=(e,t,r=p)=>{const inside=(e,t,s)=>(s[0]-t[0])*(e[1]-t[1])>(s[1]-t[1])*(e[0]-t[0])+r,intersection=(e,t,r,s)=>{const a=subtract2(e,t),o=subtract2(s,r),c=cross2(e,t),n=cross2(s,r),i=1/cross2(a,o);return scale2$1(subtract2(scale2$1(o,c),scale2$1(a,n)),i)};let s=e,a=t[t.length-1];for(let e=0;e<t.length;e+=1){const r=t[e],o=s;s=[];let c=o[o.length-1];for(let e=0;e<o.length;e+=1){const t=o[e];inside(t,a,r)?(inside(c,a,r)||s.push(intersection(a,r,t,c)),s.push(t)):inside(c,a,r)&&s.push(intersection(a,r,t,c)),c=t}a=r}return 0===s.length?void 0:s};var B=Object.freeze({__proto__:null,clipLineConvexPolygon:clipLineConvexPolygon,clipPolygonPolygon:clipPolygonPolygon});const reflectPoint=(e,t)=>{const r=makeMatrix2Reflect(e.vector,e.origin);return multiplyMatrix2Vector2(r,t)},validateAxiom1And2=(e,t,r)=>[[t,r].map((t=>overlapConvexPolygonPoint(e,t,include).overlap)).reduce(((e,t)=>e&&t),!0)],validateAxiom3=(e,t,r,s)=>{const a=[r,s].map((t=>clipLineConvexPolygon(e,t,include,includeL)));if(void 0===a[0]||void 0===a[1])return[!1,!1];const o=t.map((t=>void 0===t?void 0:clipLineConvexPolygon(e,t,include,includeL))),c=[0,1].map((e=>void 0!==o[e])),n=t.map((e=>void 0===e?void 0:[reflectPoint(e,a[0][0]),reflectPoint(e,a[0][1])])),i=n.map((e=>void 0!==e&&(overlapLinePoint({vector:subtract2(a[1][1],a[1][0]),origin:a[1][0]},e[0],includeS)||overlapLinePoint({vector:subtract2(a[1][1],a[1][0]),origin:a[1][0]},e[1],includeS)||overlapLinePoint({vector:subtract2(e[1],e[0]),origin:e[0]},a[1][0],includeS)||overlapLinePoint({vector:subtract2(e[1],e[0]),origin:e[0]},a[1][1],includeS))));return[0,1].map((e=>!0===i[e]&&!0===c[e]))},validateAxiom4=(e,t,r,s)=>{const a={vector:rotate90(r.vector),origin:s},o=intersectLineLine(r,a).point;return o?[[s,o].map((t=>overlapConvexPolygonPoint(e,t,include).overlap)).reduce(((e,t)=>e&&t),!0)]:[!1]},validateAxiom5=(e,t,r,s,a)=>{if(0===t.length)return[];const o=[s,a].map((t=>overlapConvexPolygonPoint(e,t,include).overlap)).reduce(((e,t)=>e&&t),!0),c=t.map((e=>reflectPoint(e,a))).map((t=>overlapConvexPolygonPoint(e,t,include).overlap));return c.map((e=>e&&o))},validateAxiom6=function(e,t,r,s,a,o){if(0===t.length)return[];if(![a,o].map((t=>overlapConvexPolygonPoint(e,t,include).overlap)).reduce(((e,t)=>e&&t),!0))return t.map((()=>!1));const c=t.map((e=>reflectPoint(e,a))).map((t=>overlapConvexPolygonPoint(e,t,include).overlap)),n=t.map((e=>reflectPoint(e,o))).map((t=>overlapConvexPolygonPoint(e,t,include).overlap));return t.map(((e,t)=>c[t]&&n[t]))},validateAxiom7=(e,t,r,s,a)=>{const o=overlapConvexPolygonPoint(e,a,include).overlap;if(!t.length)return[!1];const c=reflectPoint(t[0],a),n=overlapConvexPolygonPoint(e,c,include).overlap,i=intersectPolygonLine(e,s,includeL).length>=2,l=intersectLineLine(s,t[0],includeL,includeL).point,d=!!l&&overlapConvexPolygonPoint(e,l,include).overlap;return[o&&n&&i&&d]};var N=Object.freeze({__proto__:null,validateAxiom1And2:validateAxiom1And2,validateAxiom3:validateAxiom3,validateAxiom4:validateAxiom4,validateAxiom5:validateAxiom5,validateAxiom6:validateAxiom6,validateAxiom7:validateAxiom7});var R=Object.freeze({__proto__:null,axiom1InPolygon:(e,t,r)=>{const s=validateAxiom1And2(e,t,r);return axiom1(t,r).filter(((e,t)=>s[t]))},axiom2InPolygon:(e,t,r)=>{const s=validateAxiom1And2(e,t,r);return axiom2(t,r).filter(((e,t)=>s[t]))},axiom3InPolygon:(e,t,r)=>{const s=axiom3(t,r),a=validateAxiom3(e,s,t,r);return s.filter(((e,t)=>a[t]))},axiom4InPolygon:(e,t,r)=>{const s=axiom4(t,r),a=validateAxiom4(e,0,t,r);return s.filter(((e,t)=>a[t]))},axiom5InPolygon:(e,t,r,s)=>{const a=axiom5(t,r,s),o=validateAxiom5(e,a,0,r,s);return a.filter(((e,t)=>o[t]))},axiom6InPolygon:(e,t,r,s,a)=>{const o=axiom6(t,r,s,a),c=validateAxiom6(e,o,0,0,s,a);return o.filter(((e,t)=>c[t]))},axiom7InPolygon:(e,t,r,s)=>{const a=axiom7(t,r,s),o=validateAxiom7(e,a,0,r,s);return a.filter(((e,t)=>o[t]))},normalAxiom1InPolygon:(e,t,r)=>{const s=validateAxiom1And2(e,t,r);return normalAxiom1(t,r).filter(((e,t)=>s[t]))},normalAxiom2InPolygon:(e,t,r)=>{const s=validateAxiom1And2(e,t,r);return normalAxiom2(t,r).filter(((e,t)=>s[t]))},normalAxiom3InPolygon:(e,t,r)=>{const s=normalAxiom3(t,r),a=validateAxiom3(e,s.map(uniqueLineToVecLine),uniqueLineToVecLine(t),uniqueLineToVecLine(r));return s.filter(((e,t)=>a[t]))},normalAxiom4InPolygon:(e,t,r)=>{const s=normalAxiom4(t,r),a=validateAxiom4(e,s.map(uniqueLineToVecLine),uniqueLineToVecLine(t),r);return s.filter(((e,t)=>a[t]))},normalAxiom5InPolygon:(e,t,r,s)=>{const a=normalAxiom5(t,r,s),o=validateAxiom5(e,a.map(uniqueLineToVecLine),uniqueLineToVecLine(t),r,s);return a.filter(((e,t)=>o[t]))},normalAxiom6InPolygon:(e,t,r,s,a)=>{const o=normalAxiom6(t,r,s,a),c=validateAxiom6(e,o.map(uniqueLineToVecLine),uniqueLineToVecLine(t),uniqueLineToVecLine(r),s,a);return o.filter(((e,t)=>c[t]))},normalAxiom7InPolygon:(e,t,r,s)=>{const a=normalAxiom7(t,r,s),o=validateAxiom7(e,a.map(uniqueLineToVecLine),uniqueLineToVecLine(t),uniqueLineToVecLine(r),s);return a.filter(((e,t)=>o[t]))}}),L={...V,...R,...N};const I=\"Rabbit Ear\",makePairsMap=(e,t)=>{const r={};return(t||e.map(((e,t)=>t))).forEach((t=>e[t].map(((e,t,r)=>[0,1].map((e=>(t+e)%r.length)).map((e=>r[e])).join(\" \"))).forEach((e=>{r[e]=t})))),r},makeVerticesToEdge=({edges_vertices:e},t)=>makePairsMap(e,t),makeVerticesToFace=({faces_vertices:e},t)=>makePairsMap(e,t),makeEdgesToFace=({faces_edges:e},t)=>makePairsMap(e,t);var U=Object.freeze({__proto__:null,makeEdgesToFace:makeEdgesToFace,makeVerticesToEdge:makeVerticesToEdge,makeVerticesToFace:makeVerticesToFace});const makeVerticesEdgesUnsorted=({edges_vertices:e})=>{const t=[];return e.forEach(((e,r)=>e.forEach((e=>{void 0===t[e]&&(t[e]=[]),t[e].push(r)})))),t},makeVerticesEdges=({edges_vertices:e,vertices_vertices:t})=>{const r=makeVerticesToEdge({edges_vertices:e});return t.map(((e,t)=>e.map((e=>r[`${t} ${e}`]))))};var D=Object.freeze({__proto__:null,makeVerticesEdges:makeVerticesEdges,makeVerticesEdgesUnsorted:makeVerticesEdgesUnsorted});const uniqueElements=e=>Array.from(new Set(e)),uniqueSortedNumbers=e=>{const t={};return e.forEach((e=>{t[e]=!0})),Object.keys(t).map(parseFloat)},epsilonUniqueSortedNumbers=(e,t=p)=>{const r=e.slice().sort(((e,t)=>e-t));if(r.length<2)return r;const s=[!0];for(let e=1;e<r.length;e+=1)s[e]=!epsilonEqual(r[e],r[e-1],t);return r.filter(((e,t)=>s[t]))},rotateCircularArray=(e,t)=>t<=0?e:e.slice(t).concat(e.slice(0,t)),splitCircularArray$1=(e,t)=>(t.sort(((e,t)=>e-t)),[e.slice(t[1]).concat(e.slice(0,t[0]+1)),e.slice(t[0],t[1]+1)]),chooseTwoPairs=e=>{const t=Array(e.length*(e.length-1)/2);let r=0;for(let s=0;s<e.length-1;s+=1)for(let a=s+1;a<e.length;a+=1,r+=1)t[r]=[e[s],e[a]];return t},setDifferenceSortedEpsilonNumbers=(e,t,r=p)=>{const s=[];let a=0,o=0;for(;a<e.length&&o<t.length;)epsilonEqual(e[a],t[o],r)?a+=1:e[a]>t[o]?o+=1:t[o]>e[a]&&(s.push(e[a]),a+=1);return s},arrayMinimumIndex=(e,t)=>{if(!e.length)return;const r=\"function\"==typeof t?e.map((e=>t(e))):e;let s=0;return r.forEach(((e,t,r)=>{e<r[s]&&(s=t)})),s},arrayMaximumIndex=(e,t)=>{if(!e.length)return;const r=\"function\"==typeof t?e.map((e=>t(e))):e;let s=0;return r.forEach(((e,t,r)=>{e>r[s]&&(s=t)})),s},mergeArraysWithHoles=(...e)=>{const t=[];return e.forEach((e=>e.forEach(((e,r)=>{t[r]=e})))),t},clustersToReflexiveArrays=e=>{const t=[];return e.flat().forEach((e=>{t[e]=[]})),e.flatMap(chooseTwoPairs).forEach((([e,r])=>{t[e].push(r),t[r].push(e)})),t},arrayArrayToLookupArray=e=>e.map((e=>{const t=[];return e.forEach((e=>{t[e]=!0})),t}));var Q=Object.freeze({__proto__:null,arrayArrayToLookupArray:arrayArrayToLookupArray,arrayIntersection:(e,t)=>{const r={};return t.forEach((e=>{r[e]=0})),t.forEach((e=>{r[e]+=1})),e.filter((e=>r[e]>0&&(r[e]-=1,!0)))},arrayMaximumIndex:arrayMaximumIndex,arrayMinimumIndex:arrayMinimumIndex,chooseTwoPairs:chooseTwoPairs,clustersToReflexiveArrays:clustersToReflexiveArrays,epsilonUniqueSortedNumbers:epsilonUniqueSortedNumbers,lookupArrayToArrayArray:e=>e.map((e=>e.map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)))),mergeArraysWithHoles:mergeArraysWithHoles,nonUniqueElements:e=>{const t={};return e.forEach((e=>{void 0===t[e]&&(t[e]=0),t[e]+=1})),e.filter((e=>t[e]>1))},rotateCircularArray:rotateCircularArray,setDifferenceSortedEpsilonNumbers:setDifferenceSortedEpsilonNumbers,setDifferenceSortedNumbers:(e,t)=>{const r=[];let s=0,a=0;for(;s<e.length&&a<t.length;)e[s]===t[a]?s+=1:e[s]>t[a]?a+=1:t[a]>e[s]&&(r.push(e[s]),s+=1);return r},splitCircularArray:splitCircularArray$1,uniqueElements:uniqueElements,uniqueSortedNumbers:uniqueSortedNumbers});const edgeifyFaces=({vertices_coords:e,faces_vertices:t},r=0)=>t.map((t=>[t.reduce(((t,s)=>e[t][r]<e[s][r]?t:s)),t.reduce(((t,s)=>e[t][r]>e[s][r]?t:s))])),sweepValues=({edges_vertices:e,vertices_edges:t},r,s=p)=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:e}));const a=e.map((e=>e.map((e=>r[e])))),o=a.map((([e,t])=>epsilonEqual(e,t,s))),c=a.map((([e,t])=>Math.sign(e-t))),n=e.map((([e,t],r)=>o[r]?{[e]:0,[t]:0}:{[e]:c[r],[t]:-c[r]}));return clusterScalars(r,s).map((e=>e.filter((e=>t[e])))).filter((e=>e.length)).map((e=>({vertices:e,t:e.reduce(((e,t)=>e+r[t]),0)/e.length,start:uniqueElements(e.flatMap((e=>t[e].filter((t=>n[t][e]<=0))))),end:uniqueElements(e.flatMap((e=>t[e].filter((t=>n[t][e]>=0)))))})))},sweepEdges=({vertices_coords:e,edges_vertices:t,vertices_edges:r},s=0,a=p)=>sweepValues({edges_vertices:t,vertices_edges:r},e.map((e=>e[s])),a),sweepFaces=({vertices_coords:e,faces_vertices:t},r=0,s=p)=>sweepValues({edges_vertices:edgeifyFaces({vertices_coords:e,faces_vertices:t},r)},e.map((e=>e[r])),s);var W=Object.freeze({__proto__:null,sweep:({vertices_coords:e,edges_vertices:t,faces_vertices:r},s=0,a=p)=>{const o=e.map((e=>e[s])),c=edgeifyFaces({vertices_coords:e,faces_vertices:r},s),n=makeVerticesEdgesUnsorted({edges_vertices:t}),i=makeVerticesEdgesUnsorted({edges_vertices:c}),l=t.map((e=>e.map((e=>o[e])))),d=c.map((e=>e.map((e=>o[e])))),_=l.map((([e,t])=>epsilonEqual(e,t,a))),m=d.map((([e,t])=>epsilonEqual(e,t,a))),g=l.map((([e,t])=>Math.sign(e-t))),v=d.map((([e,t])=>Math.sign(e-t))),u=t.map((([e,t],r)=>_[r]?{[e]:0,[t]:0}:{[e]:g[r],[t]:-g[r]})),h=r.map((([e,t],r)=>m[r]?{[e]:0,[t]:0}:{[e]:v[r],[t]:-v[r]}));return clusterScalars(o,a).map((e=>({vertices:e,t:e.reduce(((e,t)=>e+o[t]),0)/e.length,edges:{start:uniqueElements(e.filter((e=>void 0!==n[e])).flatMap((e=>n[e].filter((t=>u[t][e]<=0))))),end:uniqueElements(e.filter((e=>void 0!==n[e])).flatMap((e=>n[e].filter((t=>u[t][e]>=0)))))},faces:{start:uniqueElements(e.filter((e=>void 0!==i[e])).flatMap((e=>i[e].filter((t=>h[t][e]<=0))))),end:uniqueElements(e.filter((e=>void 0!==i[e])).flatMap((e=>i[e].filter((t=>h[t][e]>=0)))))}})))},sweepEdges:sweepEdges,sweepFaces:sweepFaces,sweepValues:sweepValues,sweepVertices:({vertices_coords:e},t=0,r=p)=>clusterScalars(e.map((e=>e[t])),r).map((r=>({vertices:r,t:r.reduce(((r,s)=>r+e[s][t]),0)/r.length})))});const doEdgesOverlap=({vertices_coords:e,edges_vertices:t},r=p)=>{const s=(({vertices_coords:e,edges_vertices:t},r=0)=>t.map((t=>t.map((t=>e[t])))).map((([e,t])=>[Math.min(e[1],t[1])-r,Math.max(e[1],t[1])+r])).map((e=>epsilonEqual(e[0],e[1],Math.abs(2.5*r))?void 0:e)))({vertices_coords:e,edges_vertices:t},-r),a=e.map(resize2),o=t.map((e=>[a[e[0]],a[e[1]]])).map((([e,t])=>pointsToLine2(e,t))),c=[],n=sweepEdges({vertices_coords:e,edges_vertices:t});try{n.forEach((({start:e,end:t})=>{e.forEach((e=>{c[e]=!0})),Object.keys(c).map((e=>parseInt(e,10))).forEach((t=>e.forEach((e=>{if(t!==e&&(!s[t]||!s[e]||doRangesOverlap(s[t],s[e]))&&intersectLineLine(o[t],o[e],excludeS,excludeS,r).point)throw new Error})))),t.forEach((e=>{delete c[e]}))}))}catch(e){return!0}return!1};var q=Object.freeze({__proto__:null,doEdgesOverlap:doEdgesOverlap});const G={file:[\"file_spec\",\"file_creator\",\"file_author\",\"file_title\",\"file_description\",\"file_classes\",\"file_frames\"],frame:[\"frame_author\",\"frame_title\",\"frame_description\",\"frame_attributes\",\"frame_classes\",\"frame_unit\",\"frame_parent\",\"frame_inherit\"],graph:[\"vertices_coords\",\"vertices_vertices\",\"vertices_edges\",\"vertices_faces\",\"edges_vertices\",\"edges_faces\",\"edges_assignment\",\"edges_foldAngle\",\"edges_length\",\"faces_vertices\",\"faces_edges\",\"faces_faces\"],orders:[\"edgeOrders\",\"faceOrders\"]},H=[\"vertices\",\"edges\",\"faces\"],J=Array.from(\"BbMmVvFfJjCcUu\"),Z={B:\"boundary\",M:\"mountain\",V:\"valley\",F:\"flat\",J:\"join\",C:\"cut\",U:\"unassigned\"};Object.keys(Z).forEach((e=>{Z[e.toLowerCase()]=Z[e]}));const Y={B:0,b:0,M:-180,m:-180,V:180,v:180,F:0,f:0,J:0,j:0,C:0,c:0,U:0,u:0},X={B:!1,b:!1,M:!0,m:!0,V:!0,v:!0,F:!1,f:!1,J:!1,j:!1,C:!1,c:!1,U:!0,u:!0},K={B:!0,b:!0,M:!1,m:!1,V:!1,v:!1,F:!1,f:!1,J:!1,j:!1,C:!0,c:!0,U:!1,u:!1},edgeAssignmentToFoldAngle=e=>Y[e]||0,edgeFoldAngleToAssignment=e=>e>p?\"V\":e<-p?\"M\":\"U\",edgeFoldAngleIsFlatFolded=e=>epsilonEqual(-180,e)||epsilonEqual(180,e),edgeFoldAngleIsFlat=e=>epsilonEqual(0,e)||edgeFoldAngleIsFlatFolded(e),edgesFoldAngleAreAllFlat=({edges_foldAngle:e})=>{if(!e)return!0;for(let t=0;t<e.length;t+=1)if(!edgeFoldAngleIsFlat(e[t]))return!1;return!0},filterKeys=(e,t)=>Object.keys(e).filter((e=>t(e))),filterKeysWithPrefix=(e,t)=>filterKeys(e,(e=>e.substring(0,t.length+1)===`${t}_`)),filterKeysWithSuffix=(e,t)=>filterKeys(e,(e=>e.substring(e.length-t.length-1,e.length)===`_${t}`)),getAllPrefixes=e=>{const t={};return Object.keys(e).filter((e=>e.includes(\"_\"))).map((e=>e.substring(0,e.indexOf(\"_\")))).forEach((e=>{t[e]=!0})),Object.keys(t)},getAllSuffixes=e=>{const t={};return Object.keys(e).filter((e=>e.includes(\"_\"))).map((e=>e.substring(e.lastIndexOf(\"_\")+1,e.length))).forEach((e=>{t[e]=!0})),Object.keys(t)},ee=Object.freeze([].concat(G.file).concat(G.frame).concat(G.graph).concat(G.orders)),getDimensionQuick=({vertices_coords:e})=>{if(!e||!e.length)return;if(void 0!==e[0])return e[0].length;const t=e.filter((()=>!0)).shift();return t?t.length:void 0},isFoldedForm=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s,frame_classes:a,file_classes:o},c=p)=>{if(a&&a.includes(\"foldedForm\")||o&&o.includes(\"foldedForm\"))return!0;if(a&&a.includes(\"creasePattern\")||o&&o.includes(\"creasePattern\"))return!1;if(!e)return!1;const n=getDimensionQuick({vertices_coords:e});if(!r&&!s)return 3===n;if(t&&2===n)return doEdgesOverlap({vertices_coords:e,edges_vertices:t});for(let t=0;t<e.length;t+=1)if(e[t]&&\"number\"==typeof e[t][2]&&!epsilonEqual(e[t][2],0,c))return!0;return 3===n},makeEdgesIsFolded=({edges_vertices:e,edges_foldAngle:t,edges_assignment:r})=>void 0===r?void 0===t?e.map((()=>!0)):t.map((e=>e<-p||e>p)):r.map((e=>X[e])),te={M:\"V\",m:\"v\",V:\"M\",v:\"m\"},invertAssignment=e=>te[e]||e,invertAssignments=e=>(e.edges_assignment&&(e.edges_assignment=e.edges_assignment.map((e=>te[e]?te[e]:e))),e.edges_foldAngle&&(e.edges_foldAngle=e.edges_foldAngle.map((e=>-e))),e),sortEdgesByAssignment=({edges_vertices:e,edges_assignment:t=[]})=>{const r=Array.from(new Set(J.map((e=>e.toUpperCase())))),s=e.map(((e,r)=>t[r]||\"U\")).map((e=>e.toUpperCase())),a={};return r.forEach((e=>{a[e]=[]})),s.forEach(((e,t)=>a[e].push(t))),a};var re=Object.freeze({__proto__:null,VEF:H,assignmentCanBeFolded:X,assignmentFlatFoldAngle:Y,assignmentIsBoundary:K,edgeAssignmentToFoldAngle:edgeAssignmentToFoldAngle,edgeFoldAngleIsFlat:edgeFoldAngleIsFlat,edgeFoldAngleIsFlatFolded:edgeFoldAngleIsFlatFolded,edgeFoldAngleToAssignment:edgeFoldAngleToAssignment,edgesAssignmentNames:Z,edgesAssignmentValues:J,edgesFoldAngleAreAllFlat:edgesFoldAngleAreAllFlat,filterKeysWithPrefix:filterKeysWithPrefix,filterKeysWithSuffix:filterKeysWithSuffix,foldFileClasses:[\"singleModel\",\"multiModel\",\"animation\",\"diagrams\"],foldFrameAttributes:[\"2D\",\"3D\",\"abstract\",\"manifold\",\"nonManifold\",\"orientable\",\"nonOrientable\",\"selfTouching\",\"nonSelfTouching\",\"selfIntersecting\",\"nonSelfIntersecting\"],foldFrameClasses:[\"creasePattern\",\"foldedForm\",\"graph\",\"linkage\"],foldKeys:G,getAllPrefixes:getAllPrefixes,getAllSuffixes:getAllSuffixes,getDimension:({vertices_coords:e},t=p)=>{for(let r=0;r<e.length;r+=1)if(e[r]&&3===e[r].length&&!epsilonEqual(0,e[r][2],t))return 3;return 2},getDimensionQuick:getDimensionQuick,getFileMetadata:(e={})=>{const t={};return G.file.filter((e=>\"file_frames\"!==e)).filter((t=>void 0!==e[t])).forEach((r=>{t[r]=e[r]})),t},invertAssignment:invertAssignment,invertAssignments:invertAssignments,isFoldObject:(e={})=>0===Object.keys(e).length?0:ee.filter((t=>e[t])).length/Object.keys(e).length,isFoldedForm:isFoldedForm,makeEdgesIsFolded:makeEdgesIsFolded,sortEdgesByAssignment:sortEdgesByAssignment,transposeGraphArrayAtIndex:(e,t,r)=>{const s=filterKeysWithPrefix(e,t);if(0===s.length)return;const a={};return s.forEach((t=>{a[t]=e[t][r]})),a},transposeGraphArrays:(e,t)=>{const r=filterKeysWithPrefix(e,t);if(0===r.length)return[];const s=Math.max(...r.map((t=>e[t].length))),a=Array.from(Array(s)).map((()=>({})));return r.forEach((t=>a.forEach(((r,s)=>{a[s][t]=e[t][s]})))),a}});const se={edges:\"edgeOrders\",faces:\"faceOrders\"},maxArraysLength=e=>Math.max(0,...e.filter((e=>void 0!==e)).map((e=>e.length))),count=(e,t)=>maxArraysLength(filterKeysWithPrefix(e,t).map((t=>e[t]))),countEdges=({edges_vertices:e,edges_faces:t})=>maxArraysLength([e,t]),countImplied=(e,t)=>Math.max((e=>{let t=-1;return e.filter((e=>void 0!==e)).forEach((e=>e.forEach((e=>e.forEach((e=>{e>t&&(t=e)})))))),t})(filterKeysWithSuffix(e,t).map((t=>e[t]))),e[se[t]]?(e=>{let t=-1;return e.forEach((e=>{e[0]>t&&(t=e[0]),e[1]>t&&(t=e[1])})),t})(e[se[t]]):-1)+1,countImpliedVertices=e=>countImplied(e,\"vertices\"),countImpliedEdges=e=>countImplied(e,\"edges\");var ae=Object.freeze({__proto__:null,count:count,countEdges:countEdges,countFaces:({faces_vertices:e,faces_edges:t,faces_faces:r})=>maxArraysLength([e,t,r]),countImplied:countImplied,countImpliedEdges:countImpliedEdges,countImpliedFaces:e=>countImplied(e,\"faces\"),countImpliedVertices:countImpliedVertices,countVertices:({vertices_coords:e,vertices_vertices:t,vertices_edges:r,vertices_faces:s})=>maxArraysLength([e,t,r,s])});const makeVerticesFacesUnsorted=({vertices_coords:e,vertices_edges:t,faces_vertices:r})=>{const s=e||t;if(!r)return(s||[]).map((()=>[]));const a=void 0!==s?s.map((()=>[])):Array.from(Array(countImpliedVertices({faces_vertices:r}))).map((()=>[]));return r.forEach(((e,t)=>{const r=[];e.forEach((e=>{r[e]=t})),r.forEach(((e,t)=>a[t].push(e)))})),a},makeVerticesFaces=({vertices_coords:e,vertices_vertices:t,faces_vertices:r})=>{if(!r)return e.map((()=>[]));if(!t)return makeVerticesFacesUnsorted({vertices_coords:e,faces_vertices:r});const s=makeVerticesToFace({faces_vertices:r});return t.map(((e,t)=>e.map((e=>[t,e].join(\" \"))))).map((e=>e.map((e=>s[e]))))};var oe=Object.freeze({__proto__:null,makeVerticesFaces:makeVerticesFaces,makeVerticesFacesUnsorted:makeVerticesFacesUnsorted});const projectPointOnPlane=(e,t=[1,0,0],r=[0,0,0])=>{const s=resize3(e),a=subtract3(s,resize3(r)),o=normalize3(resize3(t)),c=dot3(o,a),n=scale3$1(o,c);return subtract3(s,n)};var ce=Object.freeze({__proto__:null,projectPointOnPlane:projectPointOnPlane});const sortPointsAlongVector=(e,t)=>{return r=t,s=dot,e.map(((e,t)=>({i:t,n:s(e,r)}))).sort(((e,t)=>e.n-t.n)).map((e=>e.i));var r,s},radialSortUnitVectors2=e=>{const t=[[],[]];e.map((e=>e[1]>=0?0:1)).forEach(((e,r)=>t[e].push(r)));const r=[(t,r)=>e[r][0]-e[t][0],(t,r)=>e[t][0]-e[r][0]];return t.flatMap(((e,t)=>e.sort(r[t])))},radialSortVectors3=(e,t=[1,0,0],r=[0,0,0])=>{const s=basisVectors3(t),a=[s[1],s[2],s[0]],o=e.map((e=>projectPointOnPlane(e,t,r))).map((e=>subtract(e,r))).map((e=>[dot(e,a[0]),dot(e,a[1])])).map(normalize2);return radialSortUnitVectors2(o)};var ne=Object.freeze({__proto__:null,radialSortUnitVectors2:radialSortUnitVectors2,radialSortVectors3:radialSortVectors3,sortPointsAlongVector:sortPointsAlongVector});const sortVerticesCounterClockwise=({vertices_coords:e},t,r)=>t.map((t=>e[t])).map((t=>subtract(t,e[r]))).map((e=>Math.atan2(e[1],e[0]))).map((e=>e>-p?e:e+2*Math.PI)).map(((e,t)=>({a:e,i:t}))).sort(((e,t)=>e.a-t.a)).map((e=>e.i)).map((e=>t[e]));var ie=Object.freeze({__proto__:null,sortVerticesAlongVector:({vertices_coords:e},t,r)=>sortPointsAlongVector(t.map((t=>e[t])),r).map((e=>t[e])),sortVerticesCounterClockwise:sortVerticesCounterClockwise});const makeVerticesVertices2D=({vertices_coords:e,vertices_edges:t,edges_vertices:r})=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:r}));const s=t.map(((e,t)=>e.map((e=>r[e].filter((e=>e!==t)))).reduce(((e,t)=>e.concat(t)),[])));return void 0===e?s:s.map(((t,r)=>sortVerticesCounterClockwise({vertices_coords:e},t,r)))},makeVerticesVerticesFromFaces=({vertices_coords:e,vertices_faces:t,faces_vertices:r})=>{t||(t=makeVerticesFacesUnsorted({vertices_coords:e,faces_vertices:r}));const s=t.map((e=>e.map((e=>r[e])))),a=s.map(((e,t)=>e.map((e=>e.indexOf(t))))),o=s.map(((e,t)=>e.map(((e,r)=>[(a[t][r]+e.length-1)%e.length,a[t][r],(a[t][r]+1)%e.length])))),c=o.map(((e,t)=>e.map(((e,r)=>e.map((e=>s[t][r][e])))))),n=c.map((e=>{const t=e.map((e=>[[0,1],[1,2]].map((t=>t.map((t=>e[t])).join(\" \"))))),r={},s={};return t.forEach(((e,t)=>{r[e[0]]=t,s[e[1]]=t})),{facesVerts:t,to:s,from:r}}));return n.map((e=>{const t=Object.keys(e.to),r=t.map((e=>e.split(\" \").reverse().join(\" \"))),s=t.filter(((t,s)=>!(r[s]in e.from)));if(s.length>2)return console.warn(\"vertices_vertices found an unsolvable vertex\"),[];const a=s.length?s:[t[0]],o=[],c={};for(let t=0;t<a.length;t+=1){const r=a[t],s=[r];c[r]=!0;let n=!1;do{const t=s[s.length-1],r=e.to[t];if(!(r in e.facesVerts))break;let a;if(e.facesVerts[r][0]===t&&(a=e.facesVerts[r][1]),e.facesVerts[r][1]===t&&(a=e.facesVerts[r][0]),void 0===a)return[];const o=a.split(\" \").reverse().join(\" \");s.push(a),n=o in c,n||s.push(o),c[a]=!0,c[o]=!0}while(!n);const i=s.filter(((e,t)=>t%2==0)).map((e=>e.split(\" \")[1])).map((e=>parseInt(e,10)));o.push(...i)}return o}))},makeVerticesVertices=e=>{if(!e.vertices_coords||!e.vertices_coords.length)return[];return 3===e.vertices_coords.filter((()=>!0)).shift().length?makeVerticesVerticesFromFaces(e):makeVerticesVertices2D(e)},makeVerticesVerticesUnsorted=({vertices_edges:e,edges_vertices:t})=>(e||(e=makeVerticesEdgesUnsorted({edges_vertices:t})),e.map(((e,r)=>e.flatMap((e=>t[e].filter((e=>e!==r)))))));var le=Object.freeze({__proto__:null,makeVerticesVertices:makeVerticesVertices,makeVerticesVertices2D:makeVerticesVertices2D,makeVerticesVerticesFromFaces:makeVerticesVerticesFromFaces,makeVerticesVerticesUnsorted:makeVerticesVerticesUnsorted});const makeFacesVerticesFromEdges=({edges_vertices:e,faces_edges:t})=>t.map((t=>t.map((t=>e[t])).map(((e,t,r)=>{const s=r[(t+1)%r.length];return e[0]===s[0]||e[0]===s[1]?e[1]:e[0]}))));var fe=Object.freeze({__proto__:null,makeFacesVerticesFromEdges:makeFacesVerticesFromEdges});const makeFacesEdgesFromVertices=({edges_vertices:e,faces_vertices:t})=>{const r=makeVerticesToEdge({edges_vertices:e});return t.map((e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]].join(\" \"))))).map((e=>e.map((e=>r[e]))))};var de=Object.freeze({__proto__:null,makeFacesEdgesFromVertices:makeFacesEdgesFromVertices});const angleArray=e=>Array.from(Array(Math.floor(e))).map(((t,r)=>b*(r/e))),anglesToVecs=(e,t)=>e.map((e=>[t*Math.cos(e),t*Math.sin(e)])),makePolygonCircumradius=(e=3,t=1)=>anglesToVecs(angleArray(e),t),makePolygonCircumradiusSide=(e=3,t=1)=>{const r=Math.PI/e,s=angleArray(e).map((e=>e+r));return anglesToVecs(s,t)},makePolygonNonCollinear=(e,t=p)=>{const r=e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>subtract(e[1],e[0]))).map(((e,t,r)=>[e,r[(t+r.length-1)%r.length]])).map((e=>!parallel(e[1],e[0],t)));return e.filter(((e,t)=>r[t]))},makePolygonNonCollinear3=(e,t=p)=>{const r=e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>subtract3(e[1],e[0]))).map(((e,t,r)=>[e,r[(t+r.length-1)%r.length]])).map((e=>!parallel(e[1],e[0],t)));return e.filter(((e,t)=>r[t]))},signedArea=e=>.5*e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((([e,t])=>cross2(e,t))).reduce(((e,t)=>e+t),0),centroid=e=>{const t=1/(6*signedArea(e)),r=e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((([e,t])=>scale2$1(add2(e,t),cross2(e,t)))).reduce(((e,t)=>add2(e,t)),[0,0]);return[r[0]*t,r[1]*t]},boundingBox$1=(e,t=0)=>{if(!e||!e.length)return;const r=(e=>{for(let t=0;t<e.length;t+=1)if(e[t]&&e[t].length)return e[t].length;return 0})(e),s=Array(r).fill(1/0),a=Array(r).fill(-1/0);e.filter((e=>void 0!==e)).forEach((e=>e.forEach(((e,r)=>{e<s[r]&&(s[r]=e-t),e>a[r]&&(a[r]=e+t)}))));const o=a.map(((e,t)=>e-s[t]));return{min:s,max:a,span:o}};var _e=Object.freeze({__proto__:null,boundingBox:boundingBox$1,centroid:centroid,makePolygonCircumradius:makePolygonCircumradius,makePolygonCircumradiusSide:makePolygonCircumradiusSide,makePolygonInradius:(e=3,t=1)=>makePolygonCircumradius(e,t/Math.cos(Math.PI/e)),makePolygonInradiusSide:(e=3,t=1)=>makePolygonCircumradiusSide(e,t/Math.cos(Math.PI/e)),makePolygonNonCollinear:makePolygonNonCollinear,makePolygonNonCollinear3:makePolygonNonCollinear3,makePolygonSideLength:(e=3,t=1)=>makePolygonCircumradius(e,t/2/Math.sin(Math.PI/e)),makePolygonSideLengthSide:(e=3,t=1)=>makePolygonCircumradiusSide(e,t/2/Math.sin(Math.PI/e)),signedArea:signedArea});const makeEdgesCoords=({vertices_coords:e,edges_vertices:t})=>t.map((t=>[e[t[0]],e[t[1]]])),makeEdgesVector=({vertices_coords:e,edges_vertices:t})=>{const r=2===getDimensionQuick({vertices_coords:e})?resize2:resize3;return makeEdgesCoords({vertices_coords:e,edges_vertices:t}).map((([e,t])=>r(subtract(t,e))))},makeEdgesLength=({vertices_coords:e,edges_vertices:t})=>makeEdgesVector({vertices_coords:e,edges_vertices:t}).map(magnitude);var me=Object.freeze({__proto__:null,makeEdgesBoundingBox:({vertices_coords:e,edges_vertices:t},r)=>makeEdgesCoords({vertices_coords:e,edges_vertices:t}).map((e=>boundingBox$1(e,r))),makeEdgesCoords:makeEdgesCoords,makeEdgesLength:makeEdgesLength,makeEdgesVector:makeEdgesVector});const walkSingleFace=({vertices_vertices:e,vertices_sectors:t},r,s,a={})=>{const o={};let c=r,n=s;const i={vertices:[r],edges:[`${r} ${s}`],angles:[]};for(;;){const r=e[n],s=(r.indexOf(c)+r.length-1)%r.length,l=r[s],d=`${n} ${l}`;if(a[d])return;if(o[d])return Object.assign(a,o),i.vertices.pop(),i.edges.pop(),i.angles.length||delete i.angles,i;o[d]=!0,i.vertices.push(n),i.edges.push(d),t&&i.angles.push(t[n][s]),c=n,n=l}},walkPlanarFaces=({vertices_vertices:e,vertices_sectors:t})=>{const r={},s={vertices_vertices:e,vertices_sectors:t};return e.flatMap(((e,t)=>e.map((e=>walkSingleFace(s,t,e,r))).filter((e=>void 0!==e))))},filterWalkedBoundaryFace=e=>e.filter((e=>e.angles.map((e=>Math.PI-e)).reduce(((e,t)=>e+t),0)>0));var ge=Object.freeze({__proto__:null,filterWalkedBoundaryFace:filterWalkedBoundaryFace,walkPlanarFaces:walkPlanarFaces,walkSingleFace:walkSingleFace});const makeVerticesVerticesVector=({vertices_coords:e,vertices_vertices:t,vertices_edges:r,vertices_faces:s,edges_vertices:a,edges_vector:o,faces_vertices:c})=>{o||(o=makeEdgesVector({vertices_coords:e,edges_vertices:a})),t||(t=makeVerticesVertices({vertices_coords:e,vertices_edges:r,vertices_faces:s,edges_vertices:a,faces_vertices:c}));const n={};return a.map((e=>e.join(\" \"))).forEach(((e,t)=>{n[e]=t})),t.map(((e,r)=>t[r].map((e=>{const t=n[`${r} ${e}`],s=n[`${e} ${r}`];return void 0!==t?resize2(o[t]):void 0!==s?flip2(o[s]):void 0}))))},makeVerticesSectors=({vertices_coords:e,vertices_vertices:t,edges_vertices:r,edges_vector:s})=>makeVerticesVerticesVector({vertices_coords:e,vertices_vertices:t,edges_vertices:r,edges_vector:s}).map((e=>1===e.length?[b]:counterClockwiseSectors2(e)));var ve=Object.freeze({__proto__:null,makeVerticesSectors:makeVerticesSectors,makeVerticesVerticesVector:makeVerticesVerticesVector});const makePlanarFaces=({vertices_coords:e,vertices_vertices:t,vertices_edges:r,vertices_sectors:s,edges_vertices:a,edges_vector:o})=>{t||(t=makeVerticesVertices({vertices_coords:e,edges_vertices:a,vertices_edges:r})),s||(s=makeVerticesSectors({vertices_coords:e,vertices_vertices:t,edges_vertices:a,edges_vector:o}));const c=makeVerticesToEdge({edges_vertices:a}),n=filterWalkedBoundaryFace(walkPlanarFaces({vertices_vertices:t,vertices_sectors:s})).map((e=>({...e,edges:e.edges.map((e=>c[e]))})));return{faces_vertices:n.map((e=>e.vertices)),faces_edges:n.map((e=>e.edges)),faces_sectors:n.map((e=>e.angles))}},makeFacesPolygon=({vertices_coords:e,faces_vertices:t},r)=>t.map((t=>t.map((t=>e[t])))).map((e=>makePolygonNonCollinear(e,r))),makeFacesPolygonQuick=({vertices_coords:e,faces_vertices:t})=>t.map((t=>t.map((t=>e[t])))),makeFacesCenter2DQuick=({vertices_coords:e,faces_vertices:t})=>makeFacesPolygonQuick({vertices_coords:e,faces_vertices:t}).map((e=>e.map(resize2))).map((e=>average2(...e))),makeFacesCenter3DQuick=({vertices_coords:e,faces_vertices:t})=>makeFacesPolygonQuick({vertices_coords:e,faces_vertices:t}).map((e=>e.map(resize3))).map((e=>average3(...e))).map((e=>Number.isNaN(e[2])?[e[0],e[1],0]:e)),makeFacesCenterQuick=({vertices_coords:e,faces_vertices:t})=>2===getDimensionQuick({vertices_coords:e})?makeFacesCenter2DQuick({vertices_coords:e,faces_vertices:t}):makeFacesCenter3DQuick({vertices_coords:e,faces_vertices:t});var pe=Object.freeze({__proto__:null,makeFacesCenter2DQuick:makeFacesCenter2DQuick,makeFacesCenter3DQuick:makeFacesCenter3DQuick,makeFacesCenterQuick:makeFacesCenterQuick,makeFacesCentroid2D:({vertices_coords:e,faces_vertices:t})=>t.map((t=>t.map((t=>e[t])))).map((e=>e.map(resize2))).map((e=>centroid(e))),makeFacesPolygon:makeFacesPolygon,makeFacesPolygonQuick:makeFacesPolygonQuick,makePlanarFaces:makePlanarFaces});const makeEdgesFacesUnsorted=({edges_vertices:e,faces_vertices:t,faces_edges:r})=>{r||(r=makeFacesEdgesFromVertices({edges_vertices:e,faces_vertices:t}));const s=void 0!==e?e.map((()=>[])):Array.from(Array(countImpliedEdges({faces_edges:r}))).map((()=>[]));return r.forEach(((e,t)=>{const r=[];e.forEach((e=>{r[e]=t})),r.forEach(((e,t)=>s[t].push(e)))})),s};var ue=Object.freeze({__proto__:null,makeEdgesFaces:({vertices_coords:e,edges_vertices:t,edges_vector:r,faces_vertices:s,faces_edges:a,faces_center:o})=>{if(!t||!s&&!a)return makeEdgesFacesUnsorted({faces_edges:a});s||(s=makeFacesVerticesFromEdges({edges_vertices:t,faces_edges:a})),a||(a=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:s})),r||(r=makeEdgesVector({vertices_coords:e,edges_vertices:t}));const c=t.map((t=>e[t[0]]));o||(o=makeFacesCenterQuick({vertices_coords:e,faces_vertices:s}));const n=t.map((()=>[]));return a.forEach(((e,t)=>{const r=[];e.forEach((e=>{r[e]=t})),r.forEach(((e,t)=>n[t].push(e)))})),n.forEach(((e,t)=>{const s=e.map((e=>o[e])).map((e=>subtract2(e,c[t]))).map((e=>cross2(e,r[t])));e.sort(((e,t)=>s[e]-s[t]))})),n},makeEdgesFacesUnsorted:makeEdgesFacesUnsorted});const makeFacesNormal=({vertices_coords:e,faces_vertices:t})=>{const r=e.map(resize3);return t.map((e=>e.map((e=>r[e])))).map((e=>{let t,r,s=0;do{t=subtract3(e[(s+1)%e.length],e[s]),r=subtract3(e[(s+2)%e.length],e[s]),s+=1}while(s<e.length&&parallel(t,r));return normalize3(cross3(t,r))}))},makeVerticesNormal=({vertices_coords:e,faces_vertices:t,faces_normal:r})=>{r||(r=makeFacesNormal({vertices_coords:e,faces_vertices:t}));const s=e.map((()=>[0,0,0]));return t.forEach(((e,t)=>e.forEach((e=>{s[e][0]+=r[t][0],s[e][1]+=r[t][1],s[e][2]+=r[t][2]})))),s.map((e=>normalize3(e)))};var he=Object.freeze({__proto__:null,makeFacesNormal:makeFacesNormal,makeVerticesNormal:makeVerticesNormal});const be={M:-180,m:-180,V:180,v:180},makeEdgesFoldAngle=({edges_assignment:e})=>e.map((e=>be[e]||0)),makeEdgesFoldAngleFromFaces=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,faces_vertices:o,faces_edges:c,faces_normal:i,faces_center:l})=>(r||(c||(c=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:o})),r=makeEdgesFacesUnsorted({edges_vertices:t,faces_edges:c})),i||(i=makeFacesNormal({vertices_coords:e,faces_vertices:o})),l||(l=makeFacesCenterQuick({vertices_coords:e,faces_vertices:o})),r.map(((e,t)=>{if(e.length>2)throw new Error(a);if(e.length<2)return 0;const r=i[e[0]],o=i[e[1]],c=normalize$2(subtract(l[e[1]],l[e[0]]));let d=Math.sign(dot(r,c));if(0===d){if(!s||!s[t])throw new Error(n);\"F\"!==s[t]&&\"F\"!==s[t]||(d=0),\"M\"!==s[t]&&\"m\"!==s[t]||(d=-1),\"V\"!==s[t]&&\"v\"!==s[t]||(d=1)}return Math.acos(dot(r,o))*(180/Math.PI)*d})));var ye=Object.freeze({__proto__:null,makeEdgesFoldAngle:makeEdgesFoldAngle,makeEdgesFoldAngleFromFaces:makeEdgesFoldAngleFromFaces});const makeEdgesAssignmentSimple=({edges_foldAngle:e})=>e.map((e=>0===e?\"F\":e<0?\"M\":\"V\")),makeEdgesAssignment=({edges_vertices:e,edges_foldAngle:t,edges_faces:r,faces_vertices:s,faces_edges:a})=>(e&&!r&&(!a&&s&&(a=makeFacesEdgesFromVertices({edges_vertices:e,faces_vertices:s})),a&&(r=makeEdgesFacesUnsorted({edges_vertices:e,faces_edges:a}))),t?r?t.map(((e,t)=>r[t].length<2?\"B\":0===e?\"F\":e<0?\"M\":\"V\")):makeEdgesAssignmentSimple({edges_foldAngle:t}):e.map((()=>\"U\")));var Ee=Object.freeze({__proto__:null,makeEdgesAssignment:makeEdgesAssignment,makeEdgesAssignmentSimple:makeEdgesAssignmentSimple});const pairify=e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])),parseFace=e=>e.slice(1).map((e=>parseInt(e,10)-1)),parseVertex=e=>{const[t,r,s]=e.slice(1).map((e=>parseFloat(e)));return[t||0,r||0,s||0]};var Me=Object.freeze({__proto__:null,objToFold:e=>{const t=e.split(\"\\n\").map((e=>e.trim().split(/\\s+/))),r={file_spec:1.2,file_creator:I,file_classes:[\"singleModel\"],frame_classes:[],frame_attributes:[],vertices_coords:[],faces_vertices:[]},s=t.map((e=>e[0].toLowerCase()));return r.vertices_coords=t.filter(((e,t)=>\"v\"===s[t])).map(parseVertex),r.faces_vertices=t.filter(((e,t)=>\"f\"===s[t])).map(parseFace),r.edges_vertices=(({faces_vertices:e})=>{const t={},r=[];return e.flatMap(pairify).forEach((e=>{const s=[e.join(\" \"),`${e[1]} ${e[0]}`];s[0]in t||s[1]in t||(r.push(e),t[s[0]]=!0)})),r})(r),r.faces_edges=makeFacesEdgesFromVertices(r),r.edges_faces=makeEdgesFacesUnsorted(r),r.edges_foldAngle=makeEdgesFoldAngleFromFaces(r),r.edges_assignment=makeEdgesAssignment(r),r.vertices_vertices=makeVerticesVerticesFromFaces(r),delete r.edges_faces,(e=>{if(!e.edges_foldAngle||!e.edges_foldAngle.length)return;let t=!0;for(let r=0;r<e.edges_foldAngle.length;r+=1)if(0!==e.edges_foldAngle[r]&&-180!==e.edges_foldAngle[r]&&180!==e.edges_foldAngle[r]){t=!1;break}e.frame_classes.push(t?\"creasePattern\":\"foldedForm\"),e.frame_attributes.push(t?\"2D\":\"3D\")})(r),r}});const invertFlatMap=e=>{const t=[];return e.forEach(((e,r)=>{t[e]=r})),t},invertArrayToFlatMap=e=>{const t=[];return e.forEach(((e,r)=>e.forEach((e=>{t[e]=r})))),t},invertFlatToArrayMap=e=>{const t=[];return e.forEach(((e,r)=>{void 0===t[e]&&(t[e]=[]),t[e].push(r)})),t},invertArrayMap=e=>{const t=[];return e.forEach(((e,r)=>e.forEach((e=>{void 0===t[e]&&(t[e]=[]),t[e].push(r)})))),t},mergeFlatNextmaps=(...e)=>{if(0===e.length)return[];const t=e[0].map(((e,t)=>t));return e.forEach((e=>t.forEach(((r,s)=>{t[s]=e[r]})))),t},mergeNextmaps=(...e)=>{if(0===e.length)return[];const t=e[0].map(((e,t)=>[t]));return e.forEach((e=>{t.forEach(((r,s)=>r.forEach(((r,a)=>{t[s][a]=e[r]})))),t.forEach(((e,r)=>{t[r]=e.flat().filter((e=>void 0!==e))}))})),t},mergeBackmaps=(...e)=>{if(0===e.length)return[];let t=e[0].flat().map(((e,t)=>[t]));return e.forEach((e=>{const r=[];e.forEach(((e,s)=>{r[s]=\"number\"==typeof e?t[e]:e.map((e=>t[e])).reduce(((e,t)=>e.concat(t)),[])})),t=r})),t},remapKey=(e,t,r)=>{const s=[];r.forEach(((e,t)=>{s[e]=void 0===s[e]?t:s[e]})),filterKeysWithSuffix(e,t).forEach((t=>e[t].forEach(((s,a)=>e[t][a].forEach(((s,o)=>{e[t][a][o]=r[s]})))))),filterKeysWithPrefix(e,t).forEach((t=>{e[t]=s.map((r=>e[t][r]))})),\"faces\"===t&&e.faceOrders&&(e.faceOrders=e.faceOrders.map((([e,t,s])=>[r[e],r[t],s]))),\"edges\"===t&&e.edgeOrders&&(e.edgeOrders=e.edgeOrders.map((([e,t,s])=>[r[e],r[t],s])))};var Ae=Object.freeze({__proto__:null,invertArrayMap:invertArrayMap,invertArrayToFlatMap:invertArrayToFlatMap,invertFlatMap:invertFlatMap,invertFlatToArrayMap:invertFlatToArrayMap,mergeBackmaps:mergeBackmaps,mergeFlatBackmaps:(...e)=>{if(0===e.length)return[];let t=e[0].map(((e,t)=>t));return e.forEach((e=>{const r=e.map((e=>t[e]));t=r})),t},mergeFlatNextmaps:mergeFlatNextmaps,mergeNextmaps:mergeNextmaps,remapKey:remapKey});const remove=(e,t,r)=>{const s=((e,t,r)=>{const s=uniqueSortedNumbers(r),a=count(e,t),o=[];for(let e=0,t=0,r=0;e<a;e+=1,t+=1){for(;e===s[r];)o[e]=void 0,e+=1,r+=1;e<a&&(o[e]=t)}return o})(e,t,r);return remapKey(e,t,s),s};var xe=Object.freeze({__proto__:null,remove:remove});const edgeIsolatedVertices=({vertices_coords:e,edges_vertices:t})=>{if(!e||!t)return[];let r=e.length;const s=Array(r).fill(!1);return t.forEach((e=>{e.filter((e=>!s[e])).forEach((e=>{s[e]=!0,r-=1}))})),s.map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e))},isolatedVertices=({vertices_coords:e,edges_vertices:t,faces_vertices:r})=>{if(!e)return[];let s=e.length;const a=Array(s).fill(!1);return t&&t.forEach((e=>{e.filter((e=>!a[e])).forEach((e=>{a[e]=!0,s-=1}))})),r&&r.forEach((e=>{e.filter((e=>!a[e])).forEach((e=>{a[e]=!0,s-=1}))})),a.map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e))},removeIsolatedVertices=(e,t)=>(t||(t=isolatedVertices(e)),{map:remove(e,\"vertices\",t),remove:t});var Oe=Object.freeze({__proto__:null,edgeIsolatedVertices:edgeIsolatedVertices,faceIsolatedVertices:({vertices_coords:e,faces_vertices:t})=>{if(!e||!t)return[];let r=e.length;const s=Array(r).fill(!1);return t.forEach((e=>{e.filter((e=>!s[e])).forEach((e=>{s[e]=!0,r-=1}))})),s.map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e))},isolatedVertices:isolatedVertices,removeIsolatedVertices:removeIsolatedVertices});const getOtherVerticesInEdges=({edges_vertices:e},t,r)=>r.map((r=>e[r][0]===t?e[r][1]:e[r][0])),isVertexCollinear=({vertices_coords:e,vertices_edges:t,edges_vertices:r},s,a=p)=>{if(!e||!r)return!1;t||(t=makeVerticesEdgesUnsorted({edges_vertices:r}));const o=t[s];if(void 0===o||2!==o.length)return!1;const c=getOtherVerticesInEdges({edges_vertices:r},s,o),[n,i,l]=[c[0],s,c[1]].map((t=>e[t]));return collinearBetween(n,i,l,!1,a)};var je=Object.freeze({__proto__:null,getOtherVerticesInEdges:getOtherVerticesInEdges,isVertexCollinear:isVertexCollinear});const getVerticesClusters=({vertices_coords:e},t=p)=>{if(!e)return[];const r=getDimensionQuick({vertices_coords:e}),s=Array.from(Array(r)),a=[];let o=0;const c=e.map(((e,t)=>({i:t,d:e[0]}))).sort(((e,t)=>e.d-t.d)).map((e=>e.i)).filter((()=>!0)),n=s.map((()=>[0,0])),isInsideCluster=t=>s.map(((r,s)=>e[t][s]>n[s][0]&&e[t][s]<n[s][1])).reduce(((e,t)=>e&&t),!0);let i=0;const updateRange=s=>{const a=s[s.length-1];for(;e[a][0]-e[s[i]][0]>t;)i+=1;const o=s.slice(i,s.length).map((t=>e[t]));n[0]=[o[0][0]-t,o[o.length-1][0]+t];for(let e=1;e<r;e+=1){const r=o.map((t=>t[e]));n[e]=[Math.min(...r)-t,Math.max(...r)+t]}};for(;o<e.length&&c.length;){const t=[],r=c.shift();t.push(r),o+=1,i=0,updateRange(t);let s=0;for(;s<c.length&&e[c[s]][0]<n[0][1];)if(isInsideCluster(c[s])){const e=c.splice(s,1).shift();t.push(e),o+=1,updateRange(t)}else s+=1;a.push(t)}return a};var we=Object.freeze({__proto__:null,getVerticesClusters:getVerticesClusters});const replace=(e,t,r)=>{Object.entries(r).map((([e,t])=>[parseInt(e,10),t])).filter((([e,t])=>e<t)).forEach((([e,t])=>{delete r[e],r[t]=e}));const s=Object.keys(r).map((e=>parseInt(e,10))),a=uniqueSortedNumbers(s),o=((e,t,r,s)=>{const a=count(e,t),o=[];for(let e=0,t=0,n=0;e<a;e+=1,t+=1){for(;e===s[n];){if(o[e]=o[r[s[n]]],void 0===o[e])throw new Error(c);e+=1,n+=1}e<a&&(o[e]=t)}return o})(e,t,r,a);return remapKey(e,t,o),o};var ke=Object.freeze({__proto__:null,replace:replace});const removeDuplicateVertices=(e,t=p,r=!0)=>{const s=[],a=[],o=getVerticesClusters(e,t).filter((e=>e.length>1));o.forEach((e=>{Math.min(...e)!==e[0]&&e.sort(((e,t)=>e-t));for(let t=1;t<e.length;t+=1)s[e[t]]=e[0],a.push(e[t])}));const c=getDimensionQuick(e);return r&&o.map((t=>t.map((t=>e.vertices_coords[t])))).map((e=>average(...e))).forEach((([t,r,s],a)=>{const n=2===c?[t,r]:[t,r,s];e.vertices_coords[o[a][0]]=n})),{map:replace(e,\"vertices\",s),remove:a}};var Fe=Object.freeze({__proto__:null,duplicateVertices:({vertices_coords:e},t)=>getVerticesClusters({vertices_coords:e},t).filter((e=>e.length>1)),removeDuplicateVertices:removeDuplicateVertices});const nearestPointOnLine=({vector:e,origin:t},r,s=clampLine,a=p)=>{const o=resize(e.length,t),c=resize(e.length,r),n=magSquared(e),i=subtract(c,o),l=s(dot(e,i)/n,a),[d,_,m]=add(o,scale$1(e,l));return 2===e.length?[d,_]:[d,_,m]};var Se=Object.freeze({__proto__:null,nearestPoint:(e,t)=>{const r=arrayMinimumIndex(e,(e=>distance(e,t)));return void 0===r?void 0:e[r]},nearestPoint2:(e,t)=>{const r=arrayMinimumIndex(e,(e=>distance2(e,t)));return void 0===r?void 0:e[r]},nearestPointOnCircle:({radius:e,origin:t},r)=>add2(t,scale2$1(normalize2(subtract2(r,t)),e)),nearestPointOnLine:nearestPointOnLine,nearestPointOnPolygon:(e,t)=>e.map(((e,t,r)=>subtract2(r[(t+1)%r.length],e))).map(((t,r)=>({vector:t,origin:e[r]}))).map((e=>nearestPointOnLine(e,t,clampSegment))).map(((e,r)=>({point:e,edge:r,distance:distance2(e,t)}))).sort(((e,t)=>e.distance-t.distance)).shift()});const connectedComponents=e=>{const t=[],recurse=(r,s)=>void 0!==t[r]?0:(t[r]=s,e[r].forEach((e=>recurse(e,s))),1);for(let t=0,r=0;t<e.length;t+=1)t in e&&(r+=recurse(t,r));return t},connectedComponentsPairs=e=>{const t=[],r=[];return e.forEach(((e,s)=>e.forEach((e=>{s<e&&t.push([s,e]),s!==e||r[s]||(r[s]=!0,t.push([s,e]))})))),t};var Ce=Object.freeze({__proto__:null,connectedComponents:connectedComponents,connectedComponentsPairs:connectedComponentsPairs});const edgeToLine2=({vertices_coords:e,edges_vertices:t},r)=>{const[s,a]=t[r].map((t=>e[t]));return{vector:subtract2(a,s),origin:resize2(s)}},edgesToLines2=({vertices_coords:e,edges_vertices:t})=>{const r=e.map(resize2);return t.map((e=>[r[e[0]],r[e[1]]])).map((([e,t])=>pointsToLine2(e,t)))},edgesToLines3=({vertices_coords:e,edges_vertices:t})=>{const r=e.map(resize3);return t.map((e=>[r[e[0]],r[e[1]]])).map((([e,t])=>pointsToLine3(e,t)))},getEdgesLine=({vertices_coords:e,edges_vertices:t},r=p)=>{if(!e||!t||!t.length)return{edges_line:[],lines:[]};const s=edgesToLines3({vertices_coords:e,edges_vertices:t}),a=s.map((e=>nearestPointOnLine(e,[0,0,0],clampLine))).map((e=>magnitude(e))),o=clusterScalars(a,r),c=o.map((e=>e.map((e=>s[e].vector)))).map((e=>clusterParallelVectors(e,.001))).map(((e,t)=>e.map((e=>e.map((e=>o[t][e])))))).map((o=>o.map((o=>{if(Math.abs(a[o[0]])<r)return[o];const c=s[o[0]].vector,n=o.map((r=>e[t[r][0]])).map((e=>projectPointOnPlane(e,c))),i=radialSortVectors3(n,c),compareFn=(e,t)=>epsilonEqualVectors(n[e],n[t],r),remap=e=>e.map((e=>i[e])).map((e=>o[e])),l=clusterSortedGeneric(i,compareFn);if(1===l.length)return l.map(remap);const d=l[0][0],_=l[l.length-1],m=[d,_[_.length-1]].map((e=>i[e]));if(compareFn(m[0],m[1])){const e=l.pop();l[0]=e.concat(l[0])}return l.map(remap)})))).flatMap((e=>e.flatMap((e=>e)))),n=invertArrayToFlatMap(c),i=c.map((e=>e.flatMap((e=>t[e])))).map(uniqueElements),l=c.map((e=>s[e[0]].vector)),d=i.map(((t,r)=>t.map((t=>dot(e[t],l[r]))))),_=d.map(((e,t)=>i[t][arrayMinimumIndex(e)])),m=d.map(((e,t)=>i[t][arrayMaximumIndex(e)])),g=i.map(((t,r)=>subtract(e[m[r]],e[_[r]]))),v=2===getDimensionQuick({vertices_coords:e})?g.map(resize2):g.map(resize3),u=_.map((t=>e[t])),h=v.map(((e,t)=>({vector:e,origin:u[t]})));return{lines:h,edges_line:n}};var Ve=Object.freeze({__proto__:null,edgeToLine2:edgeToLine2,edgesToLines:({vertices_coords:e,edges_vertices:t})=>t.map((t=>[e[t[0]],e[t[1]]])).map((([e,t])=>pointsToLine(e,t))),edgesToLines2:edgesToLines2,edgesToLines3:edgesToLines3,getEdgesLine:getEdgesLine,getEdgesLineBruteForce:({vertices_coords:e,edges_vertices:t},r=p)=>{if(!e||!t||!t.length)return{edges_line:[],lines:[]};const s=edgesToLines3({vertices_coords:e,edges_vertices:t}),a=s.map((()=>[]));s.forEach(((e,t)=>s.forEach(((s,o)=>{o<=t||collinearLines3(e,s,r)&&(a[t].push(o),a[o].push(t))}))));const o=connectedComponents(a),c=invertFlatToArrayMap(o),n=c.map((e=>e.flatMap((e=>t[e])))).map(uniqueElements),i=c.map((e=>s[e[0]].vector)),l=n.map(((t,r)=>t.map((t=>dot(e[t],i[r]))))),d=l.map(((e,t)=>n[t][arrayMinimumIndex(e)])),_=l.map(((e,t)=>n[t][arrayMaximumIndex(e)])),m=n.map(((t,r)=>subtract(e[_[r]],e[d[r]]))),g=2===getDimensionQuick({vertices_coords:e})?m.map(resize2):m.map(resize3),v=d.map((t=>e[t])),u=g.map(((e,t)=>({vector:e,origin:v[t]})));return{lines:u,edges_line:o}}});const duplicateEdges=({edges_vertices:e})=>{if(!e)return[];const t={},r=[];return e.map((e=>e[0]<e[1]?e:e.slice().reverse())).map((e=>e.join(\" \"))).forEach(((e,s)=>{void 0!==t[e]?r[s]=t[e]:t[e]=s})),r},removeDuplicateEdges=(e,t)=>{t||(t=duplicateEdges(e));const r=Object.keys(t).map((e=>parseInt(e,10))),s=replace(e,\"edges\",t);return r.length&&(e.vertices_edges||e.vertices_vertices||e.vertices_faces)&&(e.vertices_edges=makeVerticesEdgesUnsorted(e),e.vertices_vertices=makeVerticesVertices(e),e.vertices_edges=makeVerticesEdges(e),e.vertices_faces=makeVerticesFaces(e)),{map:s,remove:r}};var ze=Object.freeze({__proto__:null,duplicateEdges:duplicateEdges,getSimilarEdges:({vertices_coords:e,vertices_edges:t,edges_vertices:r},s=p)=>{const a=getVerticesClusters({vertices_coords:e},s),o=invertArrayToFlatMap(a),comparison=(e,t)=>{const[s,a]=r[e].map((e=>o[e])),[c,n]=r[t].map((e=>o[e]));return s===c&&a===n||s===n&&a===c};return sweepEdges({vertices_coords:e,vertices_edges:t,edges_vertices:r}).map((({start:e})=>e)).flatMap((e=>clusterUnsortedIndices(e,comparison)))},removeDuplicateEdges:removeDuplicateEdges});const circularEdges=({edges_vertices:e=[]})=>e.map(((e,t)=>e[0]===e[1]?t:void 0)).filter((e=>void 0!==e)),removeCircularEdges=(e,t)=>(t||(t=circularEdges(e)),t.length&&((e,t,r)=>{const s={};r.forEach((e=>{s[e]=!0})),filterKeysWithSuffix(e,t).forEach((t=>e[t].forEach(((r,a)=>{for(let o=r.length-1;o>=0;o-=1)!0===s[r[o]]&&e[t][a].splice(o,1)}))))})(e,\"edges\",t),{map:remove(e,\"edges\",t),remove:t});var Te=Object.freeze({__proto__:null,circularEdges:circularEdges,removeCircularEdges:removeCircularEdges});const planarize$2=({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s},a=p)=>{const{lines:o,edges_line:c}=getEdgesLine({vertices_coords:e,edges_vertices:t},a),n=o.map((({vector:e})=>magSquared2(e))),i=invertFlatToArrayMap(c),l=t.map(((t,r)=>t.map((t=>e[t])).map((e=>dot2(subtract2(e,o[c[r]].origin),o[c[r]].vector))))),d=i.map((e=>e.flatMap((e=>l[e])))).map((e=>epsilonUniqueSortedNumbers(e,a))),_=((e,t=p)=>{const r=e.map((({vector:e,origin:t})=>({vector:resize2(e),origin:resize2(t)}))),s=r.map((()=>[]));for(let e=0;e<r.length-1;e+=1)for(let a=e+1;a<r.length;a+=1){const{a:o,b:c,point:n}=intersectLineLine(r[e],r[a],includeS,includeS,t);void 0!==n&&(s[e].push(o),s[a].push(c))}return s})(o,a).map((e=>epsilonUniqueSortedNumbers(e,a))).map(((e,t)=>e.map((e=>e*n[t])))).map(((e,t)=>setDifferenceSortedEpsilonNumbers(e,d[t],a))),m=i.map((e=>e.flatMap((e=>l[e])))),g=i.map((e=>invertFlatMap(e).map((e=>[2*e,2*e+1])))),v=i.map(((e,t)=>sweepValues({edges_vertices:g[t]},m[t],a))),u=v.map((e=>e.map((e=>e.t)))),h=v.map((e=>{const t={},r=e.map((e=>(e.start.forEach((e=>{t[e]=!0})),e.end.forEach((e=>{delete t[e]})),Object.keys(t).map((e=>parseInt(e,10))))));return r.pop(),r}));_.forEach(((e,t)=>{const r=u[t],s=h[t];let a=0,o=0;for(;a<e.length&&o<r.length-1;){if(e[a]<=r[o])throw new Error(\"bad algorithm\");e[a]>r[o+1]?o+=1:(r.splice(o+1,0,e[a]),s.splice(o+1,0,s[o]),a+=1)}}));const b=u.flatMap(((e,t)=>e.map((e=>e/n[t])).map((e=>add2(o[t].origin,scale2$1(o[t].vector,e))))));let y=0;const E=h.map((e=>{const t=e.map((()=>[y,++y]));return y+=1,t})).flatMap(((e,t)=>e.filter(((e,r)=>h[t][r].length)))).map(resize2),M={vertices_coords:b,edges_vertices:E};if(r||s){const e=h.flatMap((e=>e.filter((e=>e.length))));r&&(M.edges_assignment=e.map((e=>r[e[0]]))),s&&(M.edges_foldAngle=e.map((e=>s[e[0]])))}removeIsolatedVertices(M,edgeIsolatedVertices(M)),removeDuplicateVertices(M,a),removeCircularEdges(M),M.vertices_edges=makeVerticesEdgesUnsorted(M);const A=M.vertices_edges.map(((e,t)=>2===e.length?t:void 0)).filter((e=>void 0!==e)).filter((e=>isVertexCollinear(M,e,a))).reverse(),x=A.map((e=>(({edges_vertices:e,vertices_edges:t},r)=>{const s=t[r].sort(((e,t)=>e-t)),a=s.flatMap((t=>e[t])).filter((e=>e!==r)),o=[a[0],a[1]];return e[s[0]]=o,e[s[1]]=void 0,o.forEach((e=>{const r=t[e].indexOf(s[1]);-1!==r&&(t[e][r]=s[0])})),s[1]})(M,e)));remove(M,\"edges\",x),remove(M,\"vertices\",A);const O=duplicateEdges(M);return O.length&&removeDuplicateEdges(M,O),circularEdges(M).length&&console.error(\"planarize: found circular edges\"),delete M.vertices_edges,M},planarizeGraph=(e,t)=>{const r=planarize$2(e,t);r.vertices_vertices=makeVerticesVertices(r);const s=makePlanarFaces(r);return r.faces_vertices=s.faces_vertices,r.faces_edges=s.faces_edges,delete r.vertices_edges,r};var Pe=Object.freeze({__proto__:null,planarizeGraph:planarizeGraph});const disjointGraphsIndices=e=>{const t=e.edges_vertices||[],r=e.faces_vertices||[],s=e.vertices_edges?e.vertices_edges:makeVerticesEdgesUnsorted({edges_vertices:t}),a=e.vertices_vertices?e.vertices_vertices:makeVerticesVerticesUnsorted({vertices_edges:s,edges_vertices:t}),o=e.vertices_faces?e.vertices_faces:makeVerticesFacesUnsorted({vertices_edges:s,faces_vertices:r}),c=invertFlatToArrayMap(connectedComponents(a)),n=c.map((e=>e.flatMap((e=>s[e])))).map(uniqueElements),i=c.map((e=>e.flatMap((e=>o[e])))).map(uniqueElements);return Array.from(Array(c.length)).map(((e,t)=>({vertices:c[t]||[],edges:n[t]||[],faces:i[t]||[]})))},disjointGraphs=e=>{const t=disjointGraphsIndices(e),r=filterKeysWithPrefix(e,\"vertices\"),s=filterKeysWithPrefix(e,\"edges\"),a=filterKeysWithPrefix(e,\"faces\");return t.map((({vertices:t,edges:o,faces:c})=>{const n={};return r.forEach((r=>{n[r]=[],t.forEach((t=>{n[r][t]=e[r][t]}))})),s.forEach((t=>{n[t]=[],o.forEach((r=>{n[t][r]=e[t][r]}))})),a.forEach((t=>{n[t]=[],c.forEach((r=>{n[t][r]=e[t][r]}))})),n}))};var $e=Object.freeze({__proto__:null,disjointGraphs:disjointGraphs,disjointGraphsIndices:disjointGraphsIndices});const boundingBox=({vertices_coords:e},t)=>boundingBox$1(e,t),boundaryVertices=({edges_vertices:e,edges_assignment:t=[]})=>uniqueElements(e.filter(((e,r)=>K[t[r]])).flat()),boundary=({vertices_edges:e,edges_vertices:t,edges_assignment:r})=>{if(!r||!t)return{vertices:[],edges:[]};e||(e=makeVerticesEdgesUnsorted({edges_vertices:t}));const s=r.map((e=>\"B\"===e||\"b\"===e)),a={},o=[],c=[];let n=s.map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)).shift();if(void 0===n)return{vertices:[],edges:[]};s[n]=!1,c.push(n),o.push(t[n][0]),a[t[n][0]]=!0;let i=t[n][1];for(;!a[i];){if(o.push(i),a[i]=!0,n=e[i].filter((e=>s[e])).shift(),void 0===n)return{vertices:[],edges:[]};t[n][0]===i?[,i]=t[n]:[i]=t[n],c.push(n),s[n]=!1}return{vertices:o,edges:c}},boundaries$1=({vertices_edges:e,edges_vertices:t,edges_assignment:r})=>{if(!r||!t)return[{vertices:[],edges:[]}];e||(e=makeVerticesEdgesUnsorted({edges_vertices:t}));const s=[...t];r.map((e=>\"B\"===e||\"b\"===e)).map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e)).forEach((e=>delete s[e]));const a=makeVerticesEdgesUnsorted({edges_vertices:s}),o=makeVerticesVerticesUnsorted({vertices_edges:a,edges_vertices:s}),c=connectedComponents(o),n=invertFlatToArrayMap(c).map((e=>e[0])),i=n.map((e=>(e=>{let t,r,s=e;const a=[],filterFunc=e=>e!==t;for(;;){if(o[s]=o[s].filter(filterFunc),r=o[s].shift(),void 0===r)return a;a.push(s),t=s,s=r}})(e))),l=makeVerticesToEdge({edges_vertices:s}),d=i.map((e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>l[e.join(\" \")]))));return i.map(((e,t)=>({vertices:e,edges:d[t]})))},planarBoundary=({vertices_coords:e,vertices_edges:t,vertices_vertices:r,edges_vertices:s})=>{r||(r=makeVerticesVertices2D({vertices_coords:e,vertices_edges:t,edges_vertices:s}));const a=makeVerticesToEdge({edges_vertices:s}),o=[],c=[],n={vertices:c,edges:o};let i=-1/0,l=-1;if(e.forEach(((e,t)=>{e[0]>i&&(i=e[0],l=t)})),-1===l)return n;c.push(l);const d=e[l],_=r[l];if(!_)return n;const m=_.map((t=>e[t])).map((e=>[e[0]-d[0],e[1]-d[1]])).map((e=>Math.atan2(e[1],e[0]))).map((e=>e<0?e+2*Math.PI:e)).map(((e,t)=>({a:e,i:t}))).sort(((e,t)=>e.a-t.a)).shift().i,g=_[m],v=a[l<g?`${l} ${g}`:`${g} ${l}`];o.push(v);let p=l,u=g;const h={[`${p} ${u}`]:!0};for(;;){const e=r[u],t=e.indexOf(p),s=e[(t+1)%e.length],i=a[u<s?`${u} ${s}`:`${s} ${u}`];if(h[`${u} ${s}`])return i!==o[0]&&console.warn(\"bad boundary\"),n;h[`${u} ${s}`]=!0,c.push(u),o.push(i),p=u,u=s}},planarBoundaries=({vertices_coords:e,vertices_edges:t,vertices_vertices:r,edges_vertices:s})=>(r||(r=makeVerticesVertices2D({vertices_coords:e,vertices_edges:t,edges_vertices:s})),disjointGraphs({vertices_coords:e,vertices_vertices:r,edges_vertices:s}).map(planarBoundary));var Be=Object.freeze({__proto__:null,boundaries:boundaries$1,boundary:boundary,boundaryPolygon:({vertices_coords:e,vertices_edges:t,edges_vertices:r,edges_assignment:s})=>boundary({vertices_edges:t,edges_vertices:r,edges_assignment:s}).vertices.map((t=>e[t])),boundaryPolygons:({vertices_coords:e,vertices_edges:t,edges_vertices:r,edges_assignment:s})=>boundaries$1({vertices_edges:t,edges_vertices:r,edges_assignment:s}).map((({vertices:t})=>t.map((t=>e[t])))),boundaryVertices:boundaryVertices,boundingBox:boundingBox,planarBoundaries:planarBoundaries,planarBoundary:planarBoundary});const shortestEdgeLength=({vertices_coords:e,edges_vertices:t})=>{const r=t.map((t=>t.map((t=>e[t])))).map((([e,t])=>distance(e,t))).filter((e=>e>1e-4)).reduce(((e,t)=>Math.min(e,t)),1/0);return r===1/0?void 0:r},makeEpsilon=({vertices_coords:e,edges_vertices:t})=>{const r=shortestEdgeLength({vertices_coords:e,edges_vertices:t});if(r)return Math.max(1e-4*r,1e-10);const s=boundingBox({vertices_coords:e});return s&&s.span?Math.max(1e-6*Math.max(...s.span),1e-10):1e-6},countPlaces=function(e){const t=`${e}`.match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0))},cleanNumber=function(e,t=15){const r=\"number\"==typeof e?e:parseFloat(e);if(Number.isNaN(r))return e;const s=parseFloat(r.toFixed(t));return countPlaces(s)===Math.min(t,countPlaces(r))?r:s};var Ne=Object.freeze({__proto__:null,cleanNumber:cleanNumber});const findEpsilonInObject=(e,t,r=\"epsilon\")=>\"object\"==typeof t&&\"number\"==typeof t[r]?t[r]:\"number\"==typeof t?t:(({vertices_coords:e,edges_vertices:t})=>{const r=shortestEdgeLength({vertices_coords:e,edges_vertices:t}),s=boundingBox({vertices_coords:e}),a=.01*(s&&s.span?Math.max(...s.span):1),o=r/20;return void 0===r?a:Math.min(a,o)})(e),invertVertical=e=>{const t=boundingBox({vertices_coords:e}),r=t.min[1]+t.span[1]/2,s=Math.min(-t.min[1],-t.max[1]),a=Math.max(-t.min[1],-t.max[1]),o=cleanNumber(r-(s+(a-s)/2),8);for(let t=0;t<e.length;t+=1)e[t][1]=-e[t][1]+o};var Re=Object.freeze({__proto__:null,findEpsilonInObject:findEpsilonInObject,invertVertical:invertVertical});const xmlStringToDocument=(e,t=\"text/xml\")=>(new(RabbitEarWindow$1().DOMParser)).parseFromString(e,t),getContainingValue=(e,t)=>null==e?null:Array.from(e.childNodes).filter((e=>e.attributes&&e.attributes.length)).filter((e=>void 0!==Array.from(e.attributes).filter((e=>e.nodeValue===t)).shift())).shift(),parseOriLineProxy=e=>Array.from(e.childNodes).filter((e=>\"void\"===e.nodeName)).filter((e=>e.childNodes)).map((e=>getContainingValue(e,\"oripa.OriLineProxy\"))).filter((e=>e)).map((e=>[\"type\",\"x0\",\"x1\",\"y0\",\"y1\"].map((t=>getContainingValue(e,t))).map((e=>e?Array.from(e.childNodes):[])).map((e=>e.filter((e=>\"double\"===e.nodeName||\"int\"===e.nodeName)).shift())).map((e=>e&&e.childNodes[0]&&\"data\"in e.childNodes[0]?e.childNodes[0].data:\"0\")).map(parseFloat))),Le=[\"F\",\"B\",\"M\",\"V\",\"U\"],makeLineGraph=e=>{const t=e.flatMap((e=>[[e[1],e[3]],[e[2],e[4]]])),r=e.map(((e,t)=>[2*t,2*t+1])),s=e.map((e=>Le[e[0]]));return{vertices_coords:t,edges_vertices:r,edges_assignment:s,edges_foldAngle:makeEdgesFoldAngle({edges_assignment:s})}};var Ie=Object.freeze({__proto__:null,opxEdgeGraph:e=>{const t=xmlStringToDocument(e,\"text/xml\"),r=Array.from(t.getElementsByClassName(\"oripa.OriLineProxy\")).filter((e=>\"array\"===e.nodeName||\"array\"===e.tagName)).shift(),s=parseOriLineProxy(r);return makeLineGraph(s)},opxToFold:(e,t)=>{const r=xmlStringToDocument(e,\"text/xml\"),s=Array.from(r.getElementsByClassName(\"oripa.OriLineProxy\")).filter((e=>\"array\"===e.nodeName||\"array\"===e.tagName)).shift();if(void 0===Array.from(r.getElementsByClassName(\"oripa.DataSet\")).filter((e=>\"object\"===e.nodeName||\"object\"===e.tagName)).shift()||void 0===s)return;const a=parseOriLineProxy(s),o=(e=>{const t=Array.from(e.getElementsByTagName(\"string\")).map((e=>Array.from(e.childNodes).map((e=>e.nodeValue)).filter((e=>\"\"!==e)).shift())),r=t.indexOf(\"title\"),s=t.indexOf(\"editorName\"),a=t.indexOf(\"originalAuthorName\"),o=t.indexOf(\"reference\"),c=t.indexOf(\"memo\"),n={file_spec:1.2,file_creator:\"Rabbit Ear\",file_classes:[\"singleModel\"],frame_classes:[\"creasePattern\"]},i=[],l=[];return-1!==r&&t[r+1]&&(n.file_title=t[r+1]),-1!==s&&t[s+1]&&i.push(t[s+1]),-1!==a&&t[a+1]&&i.push(t[a+1]),-1!==o&&t[o+1]&&l.push(t[o+1]),-1!==c&&t[c+1]&&l.push(t[c+1]),i.length&&(n.file_author=i.join(\", \")),l.length&&(n.file_description=l.join(\", \")),n})(r),c=makeLineGraph(a);t&&\"object\"==typeof t&&t.invertVertical&&c.vertices_coords&&invertVertical(c.vertices_coords);const n=findEpsilonInObject(c,t);return{...o,...planarizeGraph(c,n)}}});const Ue={black:\"#000000\",silver:\"#c0c0c0\",gray:\"#808080\",white:\"#ffffff\",maroon:\"#800000\",red:\"#ff0000\",purple:\"#800080\",fuchsia:\"#ff00ff\",green:\"#008000\",lime:\"#00ff00\",olive:\"#808000\",yellow:\"#ffff00\",navy:\"#000080\",blue:\"#0000ff\",teal:\"#008080\",aqua:\"#00ffff\",orange:\"#ffa500\",aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",blanchedalmond:\"#ffebcd\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",darkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",gainsboro:\"#dcdcdc\",ghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",lightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",moccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",oldlace:\"#fdf5e6\",olivedrab:\"#6b8e23\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",seashell:\"#fff5ee\",sienna:\"#a0522d\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",whitesmoke:\"#f5f5f5\",yellowgreen:\"#9acd32\"},hslToRgb=(e,t,r,s)=>{const a=r/100,k=t=>(t+e/30)%12,o=t/100*Math.min(a,1-a),f=e=>a-o*Math.max(-1,Math.min(k(e)-3,Math.min(9-k(e),1)));return void 0===s?[255*f(0),255*f(8),255*f(4)]:[255*f(0),255*f(8),255*f(4),s]},mapHexNumbers=(e,t)=>{const r=Array.from(Array(t.length)).map(((t,r)=>e[r]||\"0\"));return e.length<=4?t.map((e=>r[e])).join(\"\"):r.join(\"\")},hexToRgb=e=>{const t=e.replace(/#(?=\\S)/g,\"\"),r=4===t.length||8===t.length,s=mapHexNumbers(t,r?[0,0,1,1,2,2,3,3]:[0,0,1,1,2,2]),a=parseInt(s,16);return r?[a>>24&255,a>>16&255,a>>8&255,(o=(255&a)/256,Math.round(100*o)/100)]:[a>>16&255,a>>8&255,255&a];var o},rgbToHex=(e,t,r,s)=>{const to16=e=>`00${Math.max(0,Math.min(Math.round(e),255)).toString(16)}`.slice(-2),a=`#${[e,t,r].map(to16).join(\"\")}`;return void 0===s?a:`${a}${to16(255*s)}`};var De=Object.freeze({__proto__:null,hexToRgb:hexToRgb,hslToRgb:hslToRgb,rgbToHex:rgbToHex});const getParenNumbers=e=>{const t=e.match(/\\(([^\\)]+)\\)/g);return null!=t&&t.length?t[0].substring(1,t[0].length-1).split(/[\\s,]+/).map(parseFloat):[]},parseColorToRgb=e=>{if(Ue[e])return hexToRgb(Ue[e]);if(\"#\"===e[0])return hexToRgb(e);if(\"rgba\"===e.substring(0,4)||\"rgb\"===e.substring(0,3)){const t=getParenNumbers(e);return[0,1,2].filter((e=>void 0===t[e])).forEach((e=>{t[e]=0})),t}if(\"hsla\"===e.substring(0,4)||\"hsl\"===e.substring(0,3)){const t=getParenNumbers(e);return[0,1,2].filter((e=>void 0===t[e])).forEach((e=>{t[e]=0})),hslToRgb(t[0],t[1],t[2],t[3])}},parseColorToHex=e=>{if(Ue[e])return Ue[e].toUpperCase();if(\"#\"===e[0]){const[t,r,s,a]=hexToRgb(e);return rgbToHex(t,r,s,a)}if(\"rgba\"===e.substring(0,4)||\"rgb\"===e.substring(0,3)){const[t,r,s,a]=getParenNumbers(e);return rgbToHex(t,r,s,a)}if(\"hsla\"===e.substring(0,4)||\"hsl\"===e.substring(0,3)){const t=getParenNumbers(e);[0,1,2].filter((e=>void 0===t[e])).forEach((e=>{t[e]=0}));const[r,s,a,o]=t,[c,n,i]=hslToRgb(r,s,a,o);return rgbToHex(c,n,i,o)}};var Qe=Object.freeze({__proto__:null,parseColorToHex:parseColorToHex,parseColorToRgb:parseColorToRgb});const We=\"object\"==typeof window&&\"object\"==typeof window.document;\"object\"==typeof process&&\"object\"==typeof process.versions&&(null!=process.versions.node||process.versions.bun);const qe=\"window not set; svg.window = @xmldom/xmldom\",Ge={window:void 0};We&&(Ge.window=window);const RabbitEarWindow=()=>{if(void 0===Ge.window)throw new Error(qe);return Ge.window},svg_add2=(e,t)=>[e[0]+t[0],e[1]+t[1]],svg_sub2=(e,t)=>[e[0]-t[0],e[1]-t[1]],svg_scale2=(e,t)=>[e[0]*t,e[1]*t],svg_magnitudeSq2=e=>e[0]**2+e[1]**2,svg_magnitude2=e=>Math.sqrt(svg_magnitudeSq2(e)),svg_distanceSq2=(e,t)=>svg_magnitudeSq2(svg_sub2(e,t)),svg_distance2=(e,t)=>Math.sqrt(svg_distanceSq2(e,t)),svg_polar_to_cart=(e,t)=>[Math.cos(e)*t,Math.sin(e)*t],svg_multiplyMatrices2=(e,t)=>[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]];var He=Object.freeze({__proto__:null,svg_add2:svg_add2,svg_distance2:svg_distance2,svg_distanceSq2:svg_distanceSq2,svg_magnitude2:svg_magnitude2,svg_magnitudeSq2:svg_magnitudeSq2,svg_multiplyMatrices2:svg_multiplyMatrices2,svg_polar_to_cart:svg_polar_to_cart,svg_scale2:svg_scale2,svg_sub2:svg_sub2});const parseTransform=e=>{const t=e.match(/(\\w+\\((\\-?\\d+\\.?\\d*e?\\-?\\d*,?\\s*)+\\))+/g);if(!t)return[];return t.map((e=>e.match(/[\\w\\.\\-]+/g))).map((e=>({transform:e.shift(),parameters:e.map((e=>parseFloat(e)))})))},matrixForm=function(e,t){switch(e){case\"translate\":return function(e){switch(e.length){case 1:return[1,0,0,1,e[0],0];case 2:return[1,0,0,1,e[0],e[1]];default:console.warn(`improper translate, ${e}`)}}(t);case\"rotate\":return function(e){const t=Math.cos(e[0]/(180*Math.PI)),r=Math.sin(e[0]/(180*Math.PI));switch(e.length){case 1:return[t,r,-r,t,0,0];case 3:return[t,r,-r,t,-e[1]*t+e[2]*r+e[1],-e[1]*r-e[2]*t+e[2]];default:console.warn(`improper rotate, ${e}`)}}(t);case\"scale\":return function(e){switch(e.length){case 1:return[e[0],0,0,e[0],0,0];case 2:return[e[0],0,0,e[1],0,0];default:console.warn(`improper scale, ${e}`)}}(t);case\"skewX\":return function(e){return[1,0,Math.tan(e[0]/(180*Math.PI)),1,0,0]}(t);case\"skewY\":return function(e){return[1,Math.tan(e[0]/(180*Math.PI)),0,1,0,0]}(t);case\"matrix\":return t;default:console.warn(`unknown transform type ${e}`)}},transformStringToMatrix=function(e){return parseTransform(e).map((e=>matrixForm(e.transform,e.parameters))).filter((e=>void 0!==e)).reduce(((e,t)=>svg_multiplyMatrices2(e,t)),[1,0,0,1,0,0])};var Je=Object.freeze({__proto__:null,parseTransform:parseTransform,transformStringToMatrix:transformStringToMatrix});const xmlStringToElement=(e,t=\"text/xml\")=>{const r=(new(RabbitEarWindow().DOMParser)).parseFromString(e,t);return r?r.documentElement:null},getRootParent=e=>{let t=e;for(;t&&null!=t.parentElement;)t=t.parentElement;return t},findElementTypeInParents=(e,t)=>{if((e.nodeName||\"\")===t)return e;const r=e.parentElement;return r?findElementTypeInParents(r,t):null},addClass=(e,...t)=>{if(e&&t.length)return e.classList?e.classList.add(...t):((e,...t)=>{const r={},s=e.getAttribute(\"class\"),a=s?s.split(\" \"):[];a.push(...t),a.forEach((e=>{r[e]=!0}));const o=Object.keys(r).join(\" \");e.setAttribute(\"class\",o)})(e,...t)},flattenDomTree=e=>null!=e.childNodes&&e.childNodes.length?Array.from(e.childNodes).flatMap((e=>flattenDomTree(e))):[e],Ze={svg:[\"viewBox\",\"xmlns\",\"version\"],line:[\"x1\",\"y1\",\"x2\",\"y2\"],rect:[\"x\",\"y\",\"width\",\"height\"],circle:[\"cx\",\"cy\",\"r\"],ellipse:[\"cx\",\"cy\",\"rx\",\"ry\"],polygon:[\"points\"],polyline:[\"points\"],path:[\"d\"]},attrAssign=(e,t)=>{const r=(e=>{const t={};return e.forEach((e=>{t[e.nodeName]=e.value})),t})((e=>{const t=e.attributes;if(null==t)return[];const r=Array.from(t);return Ze[e.nodeName]?r.filter((t=>!Ze[e.nodeName].includes(t.name))):r})(t));if(!r.transform&&!e.transform)return{...e,...r};const s=r.transform||\"\",a=e.transform||\"\",o=transformStringToMatrix(s),c=transformStringToMatrix(a),n=`matrix(${svg_multiplyMatrices2(c,o).join(\", \")})`;return{...e,...r,transform:n}},flattenDomTreeWithStyle=(e,t={})=>null!=e.childNodes&&e.childNodes.length?Array.from(e.childNodes).flatMap((e=>flattenDomTreeWithStyle(e,attrAssign(t,e)))):[{element:e,attributes:t}];var Ye=Object.freeze({__proto__:null,addClass:addClass,findElementTypeInParents:findElementTypeInParents,flattenDomTree:flattenDomTree,flattenDomTreeWithStyle:flattenDomTreeWithStyle,getRootParent:getRootParent,xmlStringToElement:xmlStringToElement});const invisibleParent=e=>{if(!RabbitEarWindow$1().document.body)return;const t=RabbitEarWindow$1().document.createElement(\"div\");return t.setAttribute(\"display\",\"none\"),RabbitEarWindow$1().document.body.appendChild(t),t.appendChild(e),t};var Xe=Object.freeze({__proto__:null,invisibleParent:invisibleParent});const Ke={B:\"black\",M:\"crimson\",V:\"royalblue\",F:\"lightgray\",J:\"gold\",C:\"limegreen\",U:\"orchid\"};Object.keys(Ke).forEach((e=>{Ke[e.toLowerCase()]=Ke[e]}));const et={M:[1,0,0],V:[0,0,1],J:[1,1,0],U:[1,0,1],C:[0,1,0]},rgbToAssignment=(e=0,t=0,r=0)=>{const s=scale3$1([e,t,r],1/255),a=magnitude3(s);if(a<.05)return\"B\";const o=s.reduce(((e,t)=>e+t),0)/3,c=distance3(s,[o,o,o]),n=Object.keys(et).map((e=>({key:e,dist:distance3(s,et[e])}))).sort(((e,t)=>e.dist-t.dist)).shift();return n.dist<4*c?n.key:a<.1?\"B\":\"F\"};var tt=Object.freeze({__proto__:null,assignmentColor:Ke,rgbToAssignment:rgbToAssignment});const colorToAssignment=(e,t)=>{const r=parseColorToHex(e).toUpperCase();return t&&t[r]?t[r]:rgbToAssignment(...parseColorToRgb(e))},opacityToFoldAngle=(e,t)=>{switch(t){case\"M\":case\"m\":return-180*e;case\"V\":case\"v\":return 180*e;default:return 0}},getEdgeStroke=(e,t)=>{const r=null!=RabbitEarWindow$1().getComputedStyle?RabbitEarWindow$1().getComputedStyle(e).stroke:\"\";return\"\"!==r&&\"none\"!==r?r:void 0!==t.stroke?t.stroke:void 0},getEdgeOpacity=(e,t)=>{const r=null!=RabbitEarWindow$1().getComputedStyle?RabbitEarWindow$1().getComputedStyle(e).opacity:\"\";if(\"\"!==r){const e=parseFloat(r);if(!Number.isNaN(e))return e}if(void 0!==t.opacity){const e=parseFloat(t.opacity);if(!Number.isNaN(e))return e}};var rt=Object.freeze({__proto__:null,colorToAssignment:colorToAssignment,getEdgeOpacity:getEdgeOpacity,getEdgeStroke:getEdgeStroke,opacityToFoldAngle:opacityToFoldAngle});const st=/[MmLlSsQqLlHhVvCcSsQqTtAaZz]/g,at=/-?[0-9]*\\.?\\d+/g,ot={m:\"move\",l:\"line\",v:\"vertical\",h:\"horizontal\",a:\"ellipse\",c:\"curve\",s:\"smoothCurve\",q:\"quadCurve\",t:\"smoothQuadCurve\",z:\"close\"};Object.keys(ot).forEach((e=>{const t=ot[e];ot[e.toUpperCase()]=t.charAt(0).toUpperCase()+t.slice(1)}));const add2path=(e,t)=>[e[0]+(t[0]||0),e[1]+(t[1]||0)],parsePathCommands=e=>{const t=[];let r=st.exec(e);for(;null!==r;)t.push(r),r=st.exec(e);return t.map(((t,r,s)=>({command:t[0],start:t.index,end:r===s.length-1?e.length-1:s[(r+1)%s.length].index-1}))).map((({command:t,start:r,end:s})=>{const a=e.substring(r+1,s+1).match(at);return{command:t,values:a?a.map(parseFloat):[]}}))},parsePathCommandsWithEndpoints=e=>{let t=[0,0];const r=parsePathCommands(e).map((e=>({...e,end:void 0,start:void 0})));if(!r.length)return r;r.forEach(((e,s)=>{r[s].end=((e,t,r=[0,0])=>{const s=e.toUpperCase();let a=e===s?[0,0]:r;switch(\"V\"===e&&(a=[r[0],0]),\"H\"===e&&(a=[0,r[1]]),s){case\"V\":return add2path(a,[0,t[0]]);case\"H\":return add2path(a,[t[0],0]);case\"M\":case\"L\":case\"T\":return add2path(a,t);case\"A\":return add2path(a,[t[5],t[6]]);case\"C\":return add2path(a,[t[4],t[5]]);case\"S\":case\"Q\":return add2path(a,[t[2],t[3]]);case\"Z\":return;default:return a}})(e.command,e.values,t),r[s].start=0===s?t:r[s-1].end,t=r[s].end}));const s=r[r.length-1],a=r.filter((e=>\"M\"!==e.command.toUpperCase()&&\"Z\"!==e.command.toUpperCase())).shift();return\"Z\"===s.command.toUpperCase()&&(s.end=[...a.start]),r};var ct=Object.freeze({__proto__:null,parsePathCommands:parsePathCommands,parsePathCommandsWithEndpoints:parsePathCommandsWithEndpoints,pathCommandNames:ot});const nt={L:!0,V:!0,H:!0,Z:!0},getAttributesFloatValue=(e,t)=>t.map((t=>e.getAttribute(t))).map((e=>null==e?0:e)).map(parseFloat),lineToSegments=e=>{const[t,r,s,a]=getAttributesFloatValue(e,[\"x1\",\"y1\",\"x2\",\"y2\"]);return[[t,r,s,a]]},pathToSegments=e=>parsePathCommandsWithEndpoints(e.getAttribute(\"d\")||\"\").filter((e=>nt[e.command.toUpperCase()])).map((e=>[e.start,e.end])).filter((([e,t])=>!epsilonEqualVectors(e,t))).map((([e,t])=>[e[0],e[1],t[0],t[1]])),polygonToSegments=e=>(e=>{const t=e.split(/[\\s,]+/).map(parseFloat);return Array.from(Array(Math.floor(t.length/2))).map(((e,r)=>[t[2*r+0],t[2*r+1]]))})(e.getAttribute(\"points\")||\"\").map(((e,t,r)=>[r[t][0],r[t][1],r[(t+1)%r.length][0],r[(t+1)%r.length][1]])),polylineToSegments=function(e){const t=polygonToSegments(e);return t.pop(),t},rectToSegments=function(e){const[t,r,s,a]=getAttributesFloatValue(e,[\"x\",\"y\",\"width\",\"height\"]);return[[t,r,t+s,r],[t+s,r,t+s,r+a],[t+s,r+a,t,r+a],[t,r+a,t,r]]};var it=Object.freeze({__proto__:null,lineToSegments:lineToSegments,pathToSegments:pathToSegments,polygonToSegments:polygonToSegments,polylineToSegments:polylineToSegments,rectToSegments:rectToSegments});const lt={line:lineToSegments,rect:rectToSegments,polygon:polygonToSegments,polyline:polylineToSegments,path:pathToSegments},flatSegments=e=>flattenDomTreeWithStyle(e).filter((e=>lt[e.element.nodeName])).flatMap((e=>lt[e.element.nodeName](e.element).map((t=>((e,t)=>{const r=[[e[0],e[1]],[e[2],e[3]]];if(!t)return r;const s=transformStringToMatrix(t);return s?r.map((e=>multiplyMatrix2Vector2(s,e))):r})(t,e.attributes.transform))).map((t=>({...e,segment:t}))))),svgSegments=e=>{const t=\"string\"==typeof e?xmlStringToElement(e,\"image/svg+xml\"):e;(e=>flattenDomTree(e).map((e=>\"style\"===e.nodeName)).reduce(((e,t)=>e||t),!1))(t)&&s&&console.warn(m);const r=getRootParent(t)===RabbitEarWindow$1().document?void 0:invisibleParent(t),a=flatSegments(t),o=a.map((e=>({data:{assignment:e.attributes[\"data-assignment\"],foldAngle:e.attributes[\"data-foldAngle\"]},stroke:getEdgeStroke(e.element,e.attributes),opacity:getEdgeOpacity(e.element,e.attributes)}))).map(((e,t)=>({...a[t],...e})));return r&&r.parentNode&&r.parentNode.removeChild(r),o},makeAssignmentFoldAngle=(e,t)=>{const r=(e=>{if(!e||!e.assignments)return;const t={};return Object.keys(e.assignments).forEach((r=>{const s=parseColorToHex(r).toUpperCase();t[s]=e.assignments[r]})),t})(t);r&&e.forEach((e=>{delete e.data.assignment,delete e.data.foldAngle}));const s=e.map((e=>((e,t=\"#f0f\",r)=>e||colorToAssignment(t,r))(e.data.assignment,e.stroke,r))),a=e.map(((e,t)=>((e,t=1,r)=>e?parseFloat(e):opacityToFoldAngle(t,r))(e.data.foldAngle,e.opacity,s[t])));return{edges_assignment:s,edges_foldAngle:a}},passthrough=e=>e,svgEdgeGraph=(e,t)=>{const r=svgSegments(e),{edges_assignment:s,edges_foldAngle:a}=makeAssignmentFoldAngle(r,t),o=t&&t.fast?passthrough:cleanNumber;return{vertices_coords:r.flatMap((e=>e.segment)).map((([e,t])=>[o(e,12),o(t,12)])),edges_vertices:r.map(((e,t)=>[2*t,2*t+1])),edges_assignment:s,edges_foldAngle:a}};var ft=Object.freeze({__proto__:null,svgEdgeGraph:svgEdgeGraph,svgSegments:svgSegments,svgToFold:(e,t)=>{const r=svgEdgeGraph(e,t),s=findEpsilonInObject(r,t);t&&t.invertVertical&&r.vertices_coords&&invertVertical(r.vertices_coords);const a=planarizeGraph(r,s),o=t&&t.fast?passthrough:cleanNumber;if(a.vertices_coords=a.vertices_coords.map((e=>e.map((e=>o(e,12))))).map(resize2),\"object\"!=typeof t||!1!==t.boundary){a.edges_assignment.map(((e,t)=>t)).filter((e=>\"B\"===a.edges_assignment[e]||\"b\"===a.edges_assignment[e])).forEach((e=>{a.edges_assignment[e]=\"F\"}));const{edges:e}=planarBoundary(a);e.forEach((e=>{a.edges_assignment[e]=\"B\"}))}return{file_spec:1.2,file_creator:I,frame_classes:[\"creasePattern\"],...a}}});const dt=\"http://www.w3.org/2000/svg\",_t={presentation:[\"color\",\"color-interpolation\",\"cursor\",\"direction\",\"display\",\"fill\",\"fill-opacity\",\"fill-rule\",\"font-family\",\"font-size\",\"font-size-adjust\",\"font-stretch\",\"font-style\",\"font-variant\",\"font-weight\",\"image-rendering\",\"letter-spacing\",\"opacity\",\"overflow\",\"paint-order\",\"pointer-events\",\"preserveAspectRatio\",\"shape-rendering\",\"stroke\",\"stroke-dasharray\",\"stroke-dashoffset\",\"stroke-linecap\",\"stroke-linejoin\",\"stroke-miterlimit\",\"stroke-opacity\",\"stroke-width\",\"tabindex\",\"transform-origin\",\"user-select\",\"vector-effect\",\"visibility\"],animation:[\"accumulate\",\"additive\",\"attributeName\",\"begin\",\"by\",\"calcMode\",\"dur\",\"end\",\"from\",\"keyPoints\",\"keySplines\",\"keyTimes\",\"max\",\"min\",\"repeatCount\",\"repeatDur\",\"restart\",\"to\",\"values\"],effects:[\"azimuth\",\"baseFrequency\",\"bias\",\"color-interpolation-filters\",\"diffuseConstant\",\"divisor\",\"edgeMode\",\"elevation\",\"exponent\",\"filter\",\"filterRes\",\"filterUnits\",\"flood-color\",\"flood-opacity\",\"in\",\"in2\",\"intercept\",\"k1\",\"k2\",\"k3\",\"k4\",\"kernelMatrix\",\"lighting-color\",\"limitingConeAngle\",\"mode\",\"numOctaves\",\"operator\",\"order\",\"pointsAtX\",\"pointsAtY\",\"pointsAtZ\",\"preserveAlpha\",\"primitiveUnits\",\"radius\",\"result\",\"seed\",\"specularConstant\",\"specularExponent\",\"stdDeviation\",\"stitchTiles\",\"surfaceScale\",\"targetX\",\"targetY\",\"type\",\"xChannelSelector\",\"yChannelSelector\"],text:[\"dx\",\"dy\",\"alignment-baseline\",\"baseline-shift\",\"dominant-baseline\",\"lengthAdjust\",\"method\",\"overline-position\",\"overline-thickness\",\"rotate\",\"spacing\",\"startOffset\",\"strikethrough-position\",\"strikethrough-thickness\",\"text-anchor\",\"text-decoration\",\"text-rendering\",\"textLength\",\"underline-position\",\"underline-thickness\",\"word-spacing\",\"writing-mode\"],gradient:[\"gradientTransform\",\"gradientUnits\",\"spreadMethod\"]},mt={svg:[\"svg\"],defs:[\"defs\"],header:[\"desc\",\"filter\",\"metadata\",\"style\",\"script\",\"title\",\"view\"],cdata:[\"cdata\"],group:[\"g\"],visible:[\"circle\",\"ellipse\",\"line\",\"path\",\"polygon\",\"polyline\",\"rect\",\"arc\",\"arrow\",\"curve\",\"parabola\",\"roundRect\",\"wedge\",\"origami\"],text:[\"text\"],invisible:[\"marker\",\"symbol\",\"clipPath\",\"mask\"],patterns:[\"linearGradient\",\"radialGradient\",\"pattern\"],childrenOfText:[\"textPath\",\"tspan\"],gradients:[\"stop\"],filter:[\"feBlend\",\"feColorMatrix\",\"feComponentTransfer\",\"feComposite\",\"feConvolveMatrix\",\"feDiffuseLighting\",\"feDisplacementMap\",\"feDistantLight\",\"feDropShadow\",\"feFlood\",\"feFuncA\",\"feFuncB\",\"feFuncG\",\"feFuncR\",\"feGaussianBlur\",\"feImage\",\"feMerge\",\"feMergeNode\",\"feMorphology\",\"feOffset\",\"fePointLight\",\"feSpecularLighting\",\"feSpotLight\",\"feTile\",\"feTurbulence\"]},gt=\"class\",vt=\"function\",pt=\"number\",ut=\"string\",ht=\"object\",bt=\"svg\",yt=\"path\",Et=\"id\",Mt=\"style\",At=\"viewBox\",xt=\"transform\",Ot=\"points\",jt=\"stroke\",wt=\"none\",kt=\"arrow\",Ft=\"head\",St=\"tail\",Ct={svg:[At],line:[\"x1\",\"y1\",\"x2\",\"y2\"],rect:[\"x\",\"y\",\"width\",\"height\"],circle:[\"cx\",\"cy\",\"r\"],ellipse:[\"cx\",\"cy\",\"rx\",\"ry\"],polygon:[Ot],polyline:[Ot],path:[\"d\"],text:[\"x\",\"y\"],mask:[Et],symbol:[Et],clipPath:[Et,\"clip-rule\"],marker:[Et,\"markerHeight\",\"markerUnits\",\"markerWidth\",\"orient\",\"refX\",\"refY\"],linearGradient:[\"x1\",\"x2\",\"y1\",\"y2\"],radialGradient:[\"cx\",\"cy\",\"r\",\"fr\",\"fx\",\"fy\"],stop:[\"offset\",\"stop-color\",\"stop-opacity\"],pattern:[\"patternContentUnits\",\"patternTransform\",\"patternUnits\"]};[{nodes:[bt,\"defs\",\"g\"].concat(mt.visible,mt.text),attr:_t.presentation},{nodes:[\"filter\"],attr:_t.effects},{nodes:mt.childrenOfText.concat(\"text\"),attr:_t.text},{nodes:mt.filter,attr:_t.effects},{nodes:mt.gradients,attr:_t.gradient}].forEach((e=>e.nodes.forEach((t=>{Ct[t]||(Ct[t]=[]),Ct[t].push(...e.attr)}))));const Vt=[mt.header,mt.invisible,mt.patterns].flat(),zt=[mt.group,mt.visible,mt.text].flat(),Tt={svg:[[\"svg\",\"defs\"],Vt,zt].flat(),defs:Vt,filter:mt.filter,g:zt,text:mt.childrenOfText,marker:zt,symbol:zt,clipPath:zt,mask:zt,linearGradient:mt.gradients,radialGradient:mt.gradients},Pt={cssColors:Ue,...De,...Qe},makeCDATASection=e=>(new(RabbitEarWindow().DOMParser)).parseFromString(\"<root></root>\",\"text/xml\").createCDATASection(e);const makeCoordinates=(...e)=>e.filter((e=>typeof e===pt)).concat(e.filter((e=>typeof e===ht&&null!==e)).map((e=>typeof e.x===pt?[e.x,e.y]:typeof e[0]===pt?[e[0],e[1]]:void 0)).filter((e=>void 0!==e)).reduce(((e,t)=>e.concat(t)),[])),makeViewBox=(...e)=>{const t=makeCoordinates(...e.flat());return 2===t.length&&t.unshift(0,0),4===t.length?function(e,t,r,s,a=0){const o=r/1-r;return[e-o-a,t-o-a,r+2*o+2*a,s+2*o+2*a].join(\" \")}(t[0],t[1],t[2],t[3]):void 0},setViewBox=(e,...t)=>{const r=1===t.length&&typeof t[0]===ut?t[0]:makeViewBox(...t);return r&&e.setAttribute(At,r),e},getViewBox$1=function(e){const t=e.getAttribute(At);return null==t?void 0:t.split(\" \").map((e=>parseFloat(e)))},convertToViewBox=function(e,t,r){const s=e.createSVGPoint();s.x=t,s.y=r;const a=s.matrixTransform(e.getScreenCTM().inverse());return[a.x,a.y]};const $t={...He,...Ye,...Object.freeze({__proto__:null,makeCDATASection:makeCDATASection}),...ct,...Je,...Object.freeze({__proto__:null,convertToViewBox:convertToViewBox,foldToViewBox:({vertices_coords:e})=>{if(!e)return;const t=[1/0,1/0],r=[-1/0,-1/0];return e.forEach((e=>[0,1].forEach((s=>{t[s]=Math.min(e[s],t[s]),r[s]=Math.max(e[s],r[s])})))),[t[0],t[1],r[0]-t[0],r[1]-t[1]].join(\" \")},getViewBox:getViewBox$1,setViewBox:setViewBox})},getSVGFrame=function(e){const t=getViewBox$1(e);if(void 0!==t)return t;if(typeof e.getBoundingClientRect===vt){const t=e.getBoundingClientRect();return[t.x,t.y,t.width,t.height]}return[]},Bt=\"svg-background-rectangle\",getAttr=e=>{const t=e.getAttribute(xt);return null==t||\"\"===t?void 0:t},Nt={clearTransform:e=>(e.removeAttribute(xt),e)};[\"translate\",\"rotate\",\"scale\",\"matrix\"].forEach((e=>{Nt[e]=(t,...r)=>(t.setAttribute(xt,[getAttr(t),`${e}(${r.join(\" \")})`].filter((e=>void 0!==e)).join(\" \")),t)}));const makeUUID=()=>Math.random().toString(36).replace(/[^a-z]+/g,\"\").concat(\"aaaaa\").substring(0,5),toCamel$1=e=>e.replace(/([-_][a-z])/gi,(e=>e.toUpperCase().replace(\"-\",\"\").replace(\"_\",\"\"))),removeChildren=e=>{for(;e.lastChild;)e.removeChild(e.lastChild);return e},appendTo=(e,t)=>(t&&t.appendChild&&t.appendChild(e),e),setAttributes=(e,t)=>(Object.keys(t).forEach((r=>e.setAttribute(r.replace(/([a-z0-9])([A-Z])/g,\"$1-$2\").replace(/([A-Z])([A-Z])(?=[a-z])/g,\"$1-$2\").toLowerCase(),t[r]))),e);var Rt=Object.freeze({__proto__:null,appendTo:appendTo,removeChildren:removeChildren,setAttributes:setAttributes});const stylesheet$1=function(e,t){let r=function(e,t){const r=e.getElementsByTagName(t);return r.length?r[0]:null}(e,Mt);return null==r&&(r=RabbitEarWindow().document.createElementNS(dt,Mt),r.setTextContent=e=>(r.textContent=\"\",r.appendChild(makeCDATASection(e)),r),e.insertBefore(r,e.firstChild)),r.textContent=\"\",r.appendChild(makeCDATASection(t)),r},Lt={clear:e=>(Array.from(e.attributes).filter((e=>\"xmlns\"!==e.name&&\"version\"!==e.name)).forEach((t=>e.removeAttribute(t.name))),removeChildren(e)),size:setViewBox,setViewBox:setViewBox,getViewBox:getViewBox$1,padding:function(e,t){const r=getViewBox$1(e);return void 0!==r&&setViewBox(e,...[-t,-t,2*t,2*t].map(((e,t)=>r[t]+e))),e},background:function(e,t){let r=Array.from(e.childNodes).filter((e=>e.getAttribute(gt)===Bt)).shift();return null==r&&(r=RabbitEarWindow().document.createElementNS(dt,\"rect\"),getSVGFrame(e).forEach(((e,t)=>r.setAttribute(Ct.rect[t],e))),r.setAttribute(gt,Bt),r.setAttribute(jt,wt),e.insertBefore(r,e.firstChild)),r.setAttribute(\"fill\",t),e},getWidth:e=>getSVGFrame(e)[2],getHeight:e=>getSVGFrame(e)[3],stylesheet:function(e,t){return stylesheet$1.call(this,e,t)},...Nt,...Rt},It={move:[\"mousemove\",\"touchmove\"],press:[\"mousedown\",\"touchstart\"],release:[\"mouseup\",\"touchend\"],leave:[\"mouseleave\",\"touchcancel\"]},defineGetter=(e,t,r)=>Object.defineProperty(e,t,{get:()=>r,enumerable:!0,configurable:!0}),TouchEvents=function(e){const t=[];Object.keys(It).forEach((e=>{It[e].forEach((e=>{t[e]=[]}))}));Object.keys(It).forEach((r=>{var s;Object.defineProperty(e,`on${s=r,s.charAt(0).toUpperCase()+s.slice(1)}`,{set:s=>{e.addEventListener&&(null!=s?It[r].forEach((r=>{const handlerFunc=t=>{const r=null!=t.touches?t.touches[0]:t;if(void 0!==r){const{clientX:s,clientY:a}=r,[o,c]=convertToViewBox(e,s,a);defineGetter(t,\"x\",o),defineGetter(t,\"y\",c)}s(t)};t[r].push(handlerFunc),e.addEventListener(r,handlerFunc)})):(r=>{It[r].forEach((r=>t[r].forEach((t=>e.removeEventListener(r,t)))))})(r))},enumerable:!0})})),Object.defineProperty(e,\"off\",{value:()=>((e,t)=>Object.values(It).flat().forEach((r=>{t[r].forEach((t=>e.removeEventListener(r,t))),t[r]=[]})))(e,t)})},Ut={svg:{args:(...e)=>[makeViewBox(makeCoordinates(...e))].filter((e=>null!=e)),methods:Lt,init:(e,...t)=>{const r=RabbitEarWindow().document.createElementNS(dt,\"svg\");return r.setAttribute(\"version\",\"1.1\"),r.setAttribute(\"xmlns\",dt),t.filter((e=>null!=e)).filter((e=>e.appendChild)).forEach((e=>e.appendChild(r))),TouchEvents(r),function(e){let t,r,s=0;const a={},stop=()=>{RabbitEarWindow().cancelAnimationFrame&&RabbitEarWindow().cancelAnimationFrame(r),Object.keys(a).forEach((e=>delete a[e]))};Object.defineProperty(e,\"play\",{set:e=>{if(stop(),!e||!RabbitEarWindow().requestAnimationFrame)return;t=performance.now(),s=0;const o=makeUUID();a[o]=c=>{e({time:.001*(c-t),frame:s}),s+=1,a[o]&&(r=RabbitEarWindow().requestAnimationFrame(a[o]))},r=RabbitEarWindow().requestAnimationFrame(a[o])},enumerable:!0}),Object.defineProperty(e,\"stop\",{value:stop,enumerable:!0})}(r),r}}},Dt={};[\"clip-path\",\"mask\",\"symbol\",\"marker-end\",\"marker-mid\",\"marker-start\"].forEach((e=>{Dt[toCamel$1(e)]=(t,r)=>(t.setAttribute(e,function(e){if(null==e)return\"\";if(typeof e===ut)return\"url\"===e.slice(0,3)?e:`url(#${e})`;if(null!=e.getAttribute)return`url(#${e.getAttribute(Et)})`;return\"\"}(r)),t)}));const Qt={g:{methods:{...Nt,...Dt,...Rt}}},setRadius=(e,t)=>(e.setAttribute(Ct.circle[2],t),e),setOrigin$1=(e,t,r)=>([...makeCoordinates(...[t,r].flat()).slice(0,2)].forEach(((t,r)=>e.setAttribute(Ct.circle[r],t))),e),Wt={circle:{args:(e,t,r,s)=>{const a=makeCoordinates(...[e,t,r,s].flat());switch(a.length){case 0:case 1:return[,,...a];case 2:case 3:return a;default:return((e,t,r,s)=>[e,t,svg_distance2([e,t],[r,s])])(...a)}},methods:{radius:setRadius,setRadius:setRadius,origin:setOrigin$1,setOrigin:setOrigin$1,center:setOrigin$1,setCenter:setOrigin$1,position:setOrigin$1,setPosition:setOrigin$1,...Nt,...Dt,...Rt}}},setRadii=(e,t,r)=>([,,t,r].forEach(((t,r)=>e.setAttribute(Ct.ellipse[r],t))),e),setOrigin=(e,t,r)=>([...makeCoordinates(...[t,r].flat()).slice(0,2)].forEach(((t,r)=>e.setAttribute(Ct.ellipse[r],t))),e),qt={ellipse:{args:(e,t,r,s)=>{const a=makeCoordinates(...[e,t,r,s].flat()).slice(0,4);switch(a.length){case 0:case 1:case 2:return[,,...a];default:return a}},methods:{radius:setRadii,setRadius:setRadii,origin:setOrigin,setOrigin:setOrigin,center:setOrigin,setCenter:setOrigin,position:setOrigin,setPosition:setOrigin,...Nt,...Dt,...Rt}}},svgIsIterable=e=>null!=e&&typeof e[Symbol.iterator]===vt,svgSemiFlattenArrays=function(){switch(arguments.length){case 0:return Array.from(arguments);case 1:return svgIsIterable(arguments[0])&&typeof arguments[0]!==ut?svgSemiFlattenArrays(...arguments[0]):[arguments[0]];default:return Array.from(arguments).map((e=>svgIsIterable(e)?[...svgSemiFlattenArrays(e)]:e))}},Args$1=(...e)=>makeCoordinates(...svgSemiFlattenArrays(...e)).slice(0,4),Gt={line:{args:Args$1,methods:{setPoints:(e,...t)=>(Args$1(...t).forEach(((t,r)=>e.setAttribute(Ct.line[r],t))),e),...Nt,...Dt,...Rt}}},getD=e=>{const t=e.getAttribute(\"d\");return null==t?\"\":t},appendPathCommand=(e,t,...r)=>(e.setAttribute(\"d\",`${getD(e)}${t}${r.flat().join(\" \")}`),e),getCommands=e=>parsePathCommands(getD(e)),Ht={addCommand:appendPathCommand,appendCommand:appendPathCommand,clear:e=>(e.removeAttribute(\"d\"),e),getCommands:getCommands,get:getCommands,getD:e=>e.getAttribute(\"d\"),...Nt,...Dt,...Rt};Object.keys(ot).forEach((e=>{Ht[ot[e]]=(t,...r)=>appendPathCommand(t,e,...r)}));const Jt={path:{methods:Ht}},setRectSize=(e,t,r)=>([,,t,r].forEach(((t,r)=>e.setAttribute(Ct.rect[r],t))),e),setRectOrigin=(e,t,r)=>([...makeCoordinates(...[t,r].flat()).slice(0,2)].forEach(((t,r)=>e.setAttribute(Ct.rect[r],t))),e),fixNegatives=function(e){return[0,1].forEach((t=>{e[2+t]<0&&(void 0===e[0+t]&&(e[0+t]=0),e[0+t]+=e[2+t],e[2+t]=-e[2+t])})),e},Zt={rect:{args:(e,t,r,s)=>{const a=makeCoordinates(...[e,t,r,s].flat()).slice(0,4);switch(a.length){case 0:case 1:case 2:case 3:return fixNegatives([,,...a]);default:return fixNegatives(a)}},methods:{origin:setRectOrigin,setOrigin:setRectOrigin,center:setRectOrigin,setCenter:setRectOrigin,size:setRectSize,setSize:setRectSize,...Nt,...Dt,...Rt}}},Yt={style:{init:(e,t)=>{const r=RabbitEarWindow().document.createElementNS(dt,\"style\");return r.setAttribute(\"type\",\"text/css\"),r.textContent=\"\",r.appendChild(makeCDATASection(t)),r},methods:{setTextContent:(e,t)=>(e.textContent=\"\",e.appendChild(makeCDATASection(t)),e)}}},Xt={text:{args:(e,t,r)=>makeCoordinates(...[e,t,r].flat()).slice(0,2),init:(e,t,r,s,a)=>{const o=RabbitEarWindow().document.createElementNS(dt,\"text\"),c=[t,r,s,a].filter((e=>typeof e===ut)).shift();return o.appendChild(RabbitEarWindow().document.createTextNode(c||\"\")),o},methods:{...Nt,...Dt,appendTo:appendTo,setAttributes:setAttributes}}},makeIDString=function(){return Array.from(arguments).filter((e=>typeof e===ut||e instanceof String)).shift()||makeUUID()},maskArgs=(...e)=>[makeIDString(...e)],Kt={mask:{args:maskArgs,methods:{...Nt,...Dt,...Rt}},clipPath:{args:maskArgs,methods:{...Nt,...Dt,...Rt}},symbol:{args:maskArgs,methods:{...Nt,...Dt,...Rt}},marker:{args:maskArgs,methods:{size:setViewBox,setViewBox:setViewBox,...Nt,...Dt,...Rt}}},getPoints=e=>{const t=e.getAttribute(Ot);return null==t?\"\":t},polyString=function(){return Array.from(Array(Math.floor(arguments.length/2))).map(((e,t)=>`${arguments[2*t+0]},${arguments[2*t+1]}`)).join(\" \")},stringifyArgs=(...e)=>[polyString(...makeCoordinates(...svgSemiFlattenArrays(...e)))],setPoints$2=(e,...t)=>(e.setAttribute(Ot,stringifyArgs(...t)[0]),e),addPoint=(e,...t)=>(e.setAttribute(Ot,[getPoints(e),stringifyArgs(...t)[0]].filter((e=>\"\"!==e)).join(\" \")),e),Args=function(...e){return 1===e.length&&typeof e[0]===ut?[e[0]]:stringifyArgs(...e)},er={polyline:{args:Args,methods:{setPoints:setPoints$2,addPoint:addPoint,...Nt,...Dt,...Rt}},polygon:{args:Args,methods:{setPoints:setPoints$2,addPoint:addPoint,...Nt,...Dt,...Rt}}},arcPath=(e,t,r,s,a,o=!1)=>{if(null==a)return\"\";const c=svg_polar_to_cart(s,r),n=svg_polar_to_cart(a,r),i=[n[0]-c[0],n[1]-c[1]],l=c[0]*n[1]-c[1]*n[0],d=c[0]*n[0]+c[1]*n[1],_=Math.atan2(l,d)>0?0:1;let m=o?`M ${e},${t} l ${c[0]},${c[1]} `:`M ${e+c[0]},${t+c[1]} `;return m+=[\"a \",r,r,0,_,1,i[0],i[1]].join(\" \"),o&&(m+=\" Z\"),m},arcArguments=(e,t,r,s,a)=>[arcPath(e,t,r,s,a,!1)],tr={arc:{nodeName:yt,attributes:[\"d\"],args:arcArguments,methods:{setArc:(e,...t)=>e.setAttribute(\"d\",arcArguments(...t)),...Nt}}},rr=[St,Ft],stringifyPoint=e=>e.join(\",\"),pointsToPath=e=>\"M\"+e.map((e=>e.join(\",\"))).join(\"L\")+\"Z\",setArrowheadOptions=(e,t,r)=>{\"boolean\"==typeof t?e.options[r].visible=t:typeof t===ht?(Object.assign(e.options[r],t),null==t.visible&&(e.options[r].visible=!0)):null==t&&(e.options[r].visible=!0)},setArrowStyle=(e,t={},r=Ft)=>{const s=Array.from(e.childNodes).filter((e=>e.getAttribute(\"class\")===`${kt}-${r}`)).shift();Object.keys(t).map((e=>({key:e,fn:s[toCamel$1(e)]}))).filter((e=>typeof e.fn===vt&&\"class\"!==e.key)).forEach((e=>e.fn(t[e.key]))),Object.keys(t).filter((e=>\"class\"===e)).forEach((e=>s.classList.add(t[e])))},redraw=e=>{const t=(e=>{let t=[[e.points[0]||0,e.points[1]||0],[e.points[2]||0,e.points[3]||0]],r=svg_sub2(t[1],t[0]),s=svg_add2(t[0],svg_scale2(r,.5));const a=svg_magnitude2(r),o=rr.map((t=>e[t].visible?(1+e[t].padding)*e[t].height*2.5:0)).reduce(((e,t)=>e+t),0);if(a<o){const e=0===a?[o,0]:svg_scale2(r,o/a);t=[svg_sub2(s,svg_scale2(e,.5)),svg_add2(s,svg_scale2(e,.5))],r=svg_sub2(t[1],t[0])}let c=[r[1],-r[0]],n=svg_add2(s,svg_scale2(c,e.bend));const i=t.map((e=>svg_sub2(n,e))),l=i.map((e=>svg_magnitude2(e))),d=i.map(((e,t)=>0===l[t]?e:svg_scale2(e,1/l[t]))),_=d.map((e=>svg_scale2(e,-1))),m=[[_[0][1],-_[0][0]],[_[1][1],-_[1][0]]],g=rr.map(((t,r)=>e[t].padding?e[t].padding:e.padding?e.padding:0)),v=rr.map(((t,r)=>e[t].height*(e[t].visible?1:0))).map(((e,t)=>e+g[t])),p=t.map(((e,t)=>svg_add2(e,svg_scale2(d[t],v[t]))));r=svg_sub2(p[1],p[0]),c=[r[1],-r[0]],s=svg_add2(p[0],svg_scale2(r,.5)),n=svg_add2(s,svg_scale2(c,e.bend));const u=p.map(((t,r)=>svg_add2(t,svg_scale2(svg_sub2(n,t),e.pinch)))),h=rr.map(((t,r)=>[svg_add2(p[r],svg_scale2(_[r],e[t].height)),svg_add2(p[r],svg_scale2(m[r],e[t].width/2)),svg_add2(p[r],svg_scale2(m[r],-e[t].width/2))]));return{line:`M${stringifyPoint(p[0])}C${stringifyPoint(u[0])},${stringifyPoint(u[1])},${stringifyPoint(p[1])}`,tail:pointsToPath(h[0]),head:pointsToPath(h[1])}})(e.options);return Object.keys(t).map((t=>({path:t,element:Array.from(e.childNodes).filter((e=>e.getAttribute(\"class\")===`${kt}-${t}`)).shift()}))).filter((e=>e.element)).map((e=>(e.element.setAttribute(\"d\",t[e.path]),e))).filter((t=>e.options[t.path])).forEach((t=>t.element.setAttribute(\"visibility\",e.options[t.path].visible?\"visible\":\"hidden\"))),e},setPoints$1=(e,...t)=>(e.options.points=makeCoordinates(...svgSemiFlattenArrays(...t)).slice(0,4),redraw(e)),sr={setPoints:setPoints$1,points:setPoints$1,bend:(e,t)=>(e.options.bend=t,redraw(e)),pinch:(e,t)=>(e.options.pinch=t,redraw(e)),padding:(e,t)=>(e.options.padding=t,redraw(e)),head:(e,t)=>(setArrowheadOptions(e,t,Ft),setArrowStyle(e,t,Ft),redraw(e)),tail:(e,t)=>(setArrowheadOptions(e,t,St),setArrowStyle(e,t,St),redraw(e)),getLine:e=>Array.from(e.childNodes).filter((e=>e.getAttribute(\"class\")===`${kt}-line`)).shift(),getHead:e=>Array.from(e.childNodes).filter((e=>e.getAttribute(\"class\")===`${kt}-${Ft}`)).shift(),getTail:e=>Array.from(e.childNodes).filter((e=>e.getAttribute(\"class\")===`${kt}-${St}`)).shift(),...Nt},ar=Object.keys({head:{visible:!1,width:8,height:10,padding:0},tail:{visible:!1,width:8,height:10,padding:0},bend:0,padding:0,pinch:.618,points:[]}),or={arrow:{nodeName:\"g\",attributes:[],args:()=>[],methods:sr,init:function(e,...t){const r=RabbitEarWindow().document.createElementNS(dt,\"g\");r.setAttribute(gt,kt);const s=[\"line\",St,Ft].map((e=>{const t=RabbitEarWindow().document.createElementNS(dt,yt);return t.setAttribute(gt,`${kt}-${e}`),r.appendChild(t),t}));s[0].setAttribute(Mt,\"fill:none;\"),s[1].setAttribute(jt,wt),s[2].setAttribute(jt,wt),r.options={head:{visible:!1,width:8,height:10,padding:0},tail:{visible:!1,width:8,height:10,padding:0},bend:0,padding:0,pinch:.618,points:[]};const a=((...e)=>{for(let t=0;t<e.length;t+=1){if(typeof e[t]!==ht)continue;const r=Object.keys(e[t]);for(let s=0;s<r.length;s+=1)if(ar.includes(r[s]))return e[t]}})(...t);return sr.setPoints(r,...a.segment||t),a&&Object.keys(a).filter((e=>sr[e])).forEach((e=>sr[e](r,a[e]))),r}}},makeCurvePath=(e=[],t=0,r=.5)=>{const s=[e[0]||0,e[1]||0],a=[e[2]||0,e[3]||0],o=svg_sub2(a,s),c=svg_add2(s,svg_scale2(o,.5)),n=[o[1],-o[0]],i=svg_add2(c,svg_scale2(n,t)),l=svg_add2(s,svg_scale2(svg_sub2(i,s),r)),d=svg_add2(a,svg_scale2(svg_sub2(i,a),r));return`M${s[0]},${s[1]}C${l[0]},${l[1]} ${d[0]},${d[1]} ${a[0]},${a[1]}`},getNumbersFromPathCommand=e=>e.slice(1).split(/[, ]+/).map((e=>parseFloat(e))),getCurveEndpoints=e=>{const t=(e=>e.match(/[Mm][(0-9), .-]+/).map((e=>getNumbersFromPathCommand(e))))(e).shift(),r=(e=>e.match(/[Cc][(0-9), .-]+/).map((e=>getNumbersFromPathCommand(e))))(e).shift();return[...t?[t[t.length-2],t[t.length-1]]:[0,0],...r?[r[r.length-2],r[r.length-1]]:[0,0]]},setPoints=(e,...t)=>{const r=makeCoordinates(...t.flat()).slice(0,4);return e.setAttribute(\"d\",makeCurvePath(r,e._bend,e._pinch)),e},cr={setPoints:setPoints,bend:(e,t)=>(e._bend=t,setPoints(e,...getCurveEndpoints(e.getAttribute(\"d\")))),pinch:(e,t)=>(e._pinch=t,setPoints(e,...getCurveEndpoints(e.getAttribute(\"d\")))),...Nt},nr={curve:{nodeName:yt,attributes:[\"d\"],args:(...e)=>[makeCurvePath(makeCoordinates(...e.flat()))],methods:cr}},wedgeArguments=(e,t,r,s,a)=>[arcPath(e,t,r,s,a,!0)],ir={wedge:{nodeName:yt,args:wedgeArguments,attributes:[\"d\"],methods:{setArc:(e,...t)=>e.setAttribute(\"d\",wedgeArguments(...t)),...Nt}}},lr={},getChildWithClass=(e,t)=>{const r=e?e.childNodes:void 0;return r?Array.from(r).filter((e=>e.getAttribute(\"class\")===t)).shift():null},fr={...Ut,...Qt,...Wt,...qt,...Gt,...Jt,...Zt,...Yt,...Xt,...Kt,...er,...tr,...or,...nr,...ir,...{origami:{nodeName:\"g\",init:(e,t,r={})=>{const s=RabbitEarWindow().document.createElementNS(dt,\"g\");return lr.ear.convert.renderSVG(t,s,{viewBox:!0,strokeWidth:!0,...r}),((e,t,r,s={})=>{const a=lr.ear.graph.boundingBox(r)||{min:[0,0],max:[1,1],span:[1,1]},o=findElementTypeInParents(e,\"svg\");if(o&&s&&s.viewBox){const e=[a.min,a.span].flatMap((e=>[e[0],e[1]])).join(\" \");o.setAttributeNS(null,\"viewBox\",e)}})(e,0,t,{viewBox:!0,strokeWidth:!0,...r}),s},args:()=>[],methods:{vertices:(...e)=>getChildWithClass(e[0],\"vertices\"),edges:(...e)=>getChildWithClass(e[0],\"edges\"),faces:(...e)=>getChildWithClass(e[0],\"faces\"),boundaries:(...e)=>getChildWithClass(e[0],\"boundaries\"),...Nt,...Dt,...Rt}}}},dr=Object.values(mt).flat(),passthroughArgs=(...e)=>e,Constructor=(e,t,...r)=>{const s=fr[e]&&fr[e].nodeName?fr[e].nodeName:e,{init:a,args:o,methods:c}=fr[e]||{},n=Ct[s]||[],i=Tt[s]||[],l=a?a(t,...r):RabbitEarWindow().document.createElementNS(dt,s);t&&!l.parentElement&&t.appendChild(l);return(o||passthroughArgs)(...r).forEach(((e,t)=>{l.setAttribute(Ct[s][t],e)})),c&&Object.keys(c).forEach((e=>Object.defineProperty(l,e,{value:function(){return c[e](l,...arguments)}}))),n.forEach((e=>{const t=toCamel$1(e);l[t]||Object.defineProperty(l,t,{value:function(){return l.setAttribute(e,...arguments),l}})})),i.forEach((e=>{if(l[e])return;Object.defineProperty(l,e,{value:function(){return Constructor(e,l,...arguments)}})})),l},_r={};dr.forEach((e=>{_r[e]=(...t)=>Constructor(e,null,...t)}));const mr=Object.assign(_r),gr={NS:dt,nodes_attributes:Ct,nodes_children:Tt,extensions:fr,...Pt,...$t,...mr,window:void 0},vr=Object.assign(((...e)=>{const t=Constructor(bt,null,...e),initialize=()=>e.filter((e=>\"function\"==typeof e)).forEach((e=>e.call(t,t)));return\"loading\"===RabbitEarWindow().document.readyState?RabbitEarWindow().document.addEventListener(\"DOMContentLoaded\",initialize):initialize(),t}),gr);Object.defineProperty(vr,\"window\",{enumerable:!1,set:e=>(e.document||(e.document=(e=>(new e.DOMParser).parseFromString(\"<!DOCTYPE html><title>.</title>\",\"text/html\"))(e)),Ge.window=e,Ge.window)});const pr={min:[0,0],span:[1,1]},setKeysAndValues=(e,t={})=>Object.keys(t).forEach((r=>e.setAttributeNS(null,r,t[r]))),boundingBoxToViewBox=e=>[e.min,e.span].flatMap((e=>[e[0],e[1]])).join(\" \"),getNthPercentileEdgeLength=({vertices_coords:e,edges_vertices:t,edges_length:r},s=.1)=>{if(!e||!t)return;r||(r=makeEdgesLength({vertices_coords:e,edges_vertices:t}));const a=r.slice().sort(((e,t)=>e-t));return a[Math.max(0,Math.min(Math.floor(a.length*s),a.length-1))]},getStrokeWidth=(e,t)=>{const r=void 0===t?Math.max(...(boundingBox(e)||pr).span):t,s=getNthPercentileEdgeLength(e,.1);return s?.1*s:.01*r};var ur=Object.freeze({__proto__:null,boundingBoxToViewBox:boundingBoxToViewBox,getNthPercentileEdgeLength:getNthPercentileEdgeLength,getStrokeWidth:getStrokeWidth,getViewBox:e=>{const t=boundingBox(e);return void 0===t?\"\":boundingBoxToViewBox(t)},setKeysAndValues:setKeysAndValues});const drawVertices=(e,t={})=>{const r=vr.g();return e&&e.vertices_coords?(e.vertices_coords.map((e=>vr.circle(e[0],e[1],.01))).forEach((e=>r.appendChild(e))),setKeysAndValues(r,t),r):r};var hr=Object.freeze({__proto__:null,drawVertices:drawVertices});const clonePolyfill=function(e){let t,r;if(\"object\"!=typeof e)return e;if(!e)return e;if(\"[object Array]\"===Object.prototype.toString.apply(e)){for(t=[],r=0;r<e.length;r+=1)t[r]=clonePolyfill(e[r]);return t}for(r in t={},e)e.hasOwnProperty(r)&&(t[r]=clonePolyfill(e[r]));return t},br=\"function\"==typeof structuredClone?structuredClone:clonePolyfill,yr={foldedForm:{},creasePattern:{stroke:\"black\"}},Er={foldedForm:{},creasePattern:{}};Object.keys(Ke).forEach((e=>{Er.creasePattern[e]={stroke:Ke[e]}}));const setDataValue=(e,t,r)=>e.setAttribute(`data-${t}`,r),edgesPathData=e=>e.vertices_coords&&e.edges_vertices?makeEdgesCoords(e).map((e=>{return`M${(t=e)[0][0]} ${t[0][1]}L${t[1][0]} ${t[1][1]}`;var t})).join(\"\"):\"\",getStyles=(e,t)=>{const r=isFoldedForm(e)?\"foldedForm\":\"creasePattern\",s=br(yr[r]),a=br(Er[r]),o=(e=>{const t={boolean:!0,number:!0,string:!0},r=br(e);return Object.keys(r).filter((e=>!t[typeof r[e]])).forEach((e=>delete r[e])),r})((e=>{const t=br(e);return Object.keys(t).filter((e=>void 0!==Z[e])).forEach((e=>delete t[e])),t})(t));return Object.assign(s,o),J.forEach((e=>{a[e]={...a[e],...o}})),{groupStyle:s,edgeStyle:a}},edgesPaths=(e,t={})=>{const r=vr.g();if(!e)return r;const{groupStyle:s,edgeStyle:a}=getStyles(e,t),o=(({vertices_coords:e,edges_vertices:t,edges_assignment:r})=>{if(!e||!t)return{};if(!r)return{U:edgesPathData({vertices_coords:e,edges_vertices:t})};const s=sortEdgesByAssignment({vertices_coords:e,edges_vertices:t,edges_assignment:r});Object.keys(s).forEach((e=>{s[e].length||delete s[e]}));const a={};return Object.keys(s).forEach((r=>{const o=edgesPathData({vertices_coords:e,edges_vertices:s[r].map((e=>t[e]))});a[r]=vr.path(o)})),a})(e);return Object.keys(o).forEach((e=>{addClass(o[e],Z[e]),setKeysAndValues(o[e],a[e]),setKeysAndValues(o[e],t[e]),setKeysAndValues(o[e],t[Z[e]])})),setKeysAndValues(r,s),Object.keys(o).forEach((e=>r.appendChild(o[e]))),Object.keys(o).forEach((e=>setDataValue(o[e],\"assignment\",e))),Object.keys(o).forEach((e=>setDataValue(o[e],\"foldAngle\",Y[e]))),r},edgesLines=(e,t={})=>{const r=vr.g();if(!e)return r;const{groupStyle:s,edgeStyle:a}=getStyles(e,t),o={};Array.from(new Set(J.map((e=>e.toUpperCase())))).forEach((e=>{const r=vr.g();addClass(r,Z[e]),setKeysAndValues(r,a[e]),setKeysAndValues(r,t[e]),setKeysAndValues(r,t[Z[e]]),o[e]=r}));const c=makeEdgesCoords(e).map((e=>vr.line(e[0][0],e[0][1],e[1][0],e[1][1])));return e.edges_foldAngle&&e.edges_foldAngle.forEach(((e,t)=>setDataValue(c[t],\"foldAngle\",e))),e.edges_assignment&&e.edges_assignment.forEach(((e,t)=>setDataValue(c[t],\"assignment\",e))),e.edges_foldAngle&&c.forEach(((t,r)=>{const s=e.edges_foldAngle[r];var a;null!=s&&0!==s&&180!==s&&-180!==s&&t.setAttributeNS(null,\"opacity\",(a=s,String(Math.abs(a)/180)))})),e.edges_assignment?c.forEach(((t,r)=>{const s=e.edges_assignment[r]||\"U\";o[s].appendChild(t)})):c.forEach((e=>o.U.appendChild(e))),Object.keys(o).filter((e=>o[e].childNodes.length)).forEach((e=>r.appendChild(o[e]))),setKeysAndValues(r,s),r},drawEdges=(e,t)=>edgesFoldAngleAreAllFlat(e)?edgesPaths(e,t):edgesLines(e,t);var Mr=Object.freeze({__proto__:null,drawEdges:drawEdges,edgesLines:edgesLines,edgesPaths:edgesPaths});const makeFacesWinding=({vertices_coords:e,faces_vertices:t})=>t.map((t=>t.map((t=>e[t])).map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>(e[1][0]-e[0][0])*(e[1][1]+e[0][1]))).reduce(((e,t)=>e+t),0))).map((e=>e<0)),makeFacesWindingFromMatrix2=e=>e.map((e=>e[0]*e[3]-e[1]*e[2])).map((e=>e>=0));var Ar=Object.freeze({__proto__:null,makeFacesWinding:makeFacesWinding,makeFacesWindingFromMatrix:e=>e.map((e=>e[0]*e[4]-e[1]*e[3])).map((e=>e>=0)),makeFacesWindingFromMatrix2:makeFacesWindingFromMatrix2});const topologicalSortQuick=e=>{const t=uniqueSortedNumbers(e.flat()),r=[];t.forEach((e=>{r[e]=[]})),e.forEach((e=>{r[e[1]].push(e[0])}));const s=[],a={},recurse=e=>{a[e]||(a[e]=!0,r[e].forEach(recurse),s.push(e))};return t.forEach(recurse),s},topologicalSort=e=>{const t=topologicalSortQuick(e),r=invertFlatMap(t);return e.filter((([e,t])=>r[e]>r[t])).length?void 0:t};var xr=Object.freeze({__proto__:null,topologicalSort:topologicalSort,topologicalSortQuick:topologicalSortQuick});const faceOrdersSubset=(e,t)=>{const r={};return t.forEach((e=>{r[e]=!0})),e.filter((e=>r[e[0]]&&r[e[1]]))},overlappingFaceOrdersClusters=({faceOrders:e})=>{const t=connectedComponents(makeVerticesVerticesUnsorted({edges_vertices:e.map((([e,t])=>[e,t]))})),r=invertFlatToArrayMap(t),s=r.map((t=>faceOrdersSubset(e,t)));return{clusters_faces:r,clusters_faceOrders:s}},faceOrdersToDirectedEdges=({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_normal:s},a)=>{if(!r||!r.length)return[];s||(s=makeFacesNormal({vertices_coords:e,faces_vertices:t}));const o=uniqueSortedNumbers(r.flatMap((([e,t])=>[e,t]))),c=void 0!==a&&o.includes(a)?s[a]:s[o[0]],n={};return o.forEach((e=>{n[e]=dot(s[e],c)>0})),r.map((e=>-1===e[2]!=!n[e[1]]?[e[0],e[1]]:[e[1],e[0]]))},linearizeFaceOrders=({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_normal:s},a)=>topologicalSort(faceOrdersToDirectedEdges({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_normal:s},a)),fillInMissingFaces=({faces_vertices:e},t)=>{if(!e)return t;return e.map(((e,t)=>t)).filter((e=>null==t[e])).concat(invertFlatMap(t))},linearize2DFaces=({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_layer:s,faces_normal:a},o)=>{if(a||(a=makeFacesNormal({vertices_coords:e,faces_vertices:t})),r){const e=linearizeFaceOrders({faceOrders:r,faces_normal:a},o);return e?fillInMissingFaces({faces_vertices:t},invertFlatMap(e)):[]}return s?fillInMissingFaces({faces_vertices:t},s):t.map(((e,t)=>t)).filter((()=>!0))},nudgeFacesWithFaceOrders=({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_normal:s})=>{const a=s?s.map(resize3):makeFacesNormal({vertices_coords:e,faces_vertices:t}),{clusters_faces:o,clusters_faceOrders:c}=overlappingFaceOrdersClusters({faceOrders:r}),n=c.map((e=>linearizeFaceOrders({faceOrders:e,faces_normal:a})));if(n.includes(void 0))return;const i=o.map((e=>a[e[0]])),l=[];return n.forEach(((e,t)=>e.forEach(((e,r)=>{l[e]={vector:i[t],layer:r}})))),l};var Or=Object.freeze({__proto__:null,faceOrdersSubset:faceOrdersSubset,faceOrdersToDirectedEdges:faceOrdersToDirectedEdges,flipFacesLayer:e=>invertArrayToFlatMap(invertFlatToArrayMap(e).reverse()),linearize2DFaces:linearize2DFaces,linearizeFaceOrders:linearizeFaceOrders,makeFacesLayer:({vertices_coords:e,faces_vertices:t,faceOrders:r,faces_normal:s})=>{s||(s=makeFacesNormal({vertices_coords:e,faces_vertices:t}));const a=linearizeFaceOrders({faceOrders:r,faces_normal:s});return a?invertFlatMap(a):[]},nudgeFacesWithFaceOrders:nudgeFacesWithFaceOrders,nudgeFacesWithFacesLayer:({faces_layer:e})=>{const t=[];return invertFlatMap(e).forEach(((e,r)=>{t[e]={vector:[0,0,1],layer:r}})),t},overlappingFaceOrdersClusters:overlappingFaceOrdersClusters});const jr=[\"front\",\"back\"],wr={foldedForm:{ordered:{back:{fill:\"white\"},front:{fill:\"#ddd\"}},unordered:{back:{opacity:.1},front:{opacity:.1}}},creasePattern:{}},kr={foldedForm:{ordered:{stroke:\"black\",\"stroke-linejoin\":\"bevel\"},unordered:{stroke:\"none\",fill:\"black\",\"stroke-linejoin\":\"bevel\"}},creasePattern:{fill:\"none\"}},finalize_faces=(e,t,r,s={})=>{const a=isFoldedForm(e),o=!(!e.faceOrders&&!e.faces_layer);makeFacesWinding(e).map((e=>e?jr[0]:jr[1])).forEach(((e,r)=>{addClass(t[r],e),t[r].setAttribute(\"data-side\",e);const c=o?wr.foldedForm.ordered[e]:wr.foldedForm.unordered[e],n=a?c:wr.creasePattern[e];setKeysAndValues(t[r],n),setKeysAndValues(t[r],s[e])})),linearize2DFaces(e).forEach((e=>r.appendChild(t[e])));const c=o?kr.foldedForm.ordered:kr.foldedForm.unordered;return setKeysAndValues(r,a?c:kr.creasePattern),r},facesVerticesPolygon=(e,t)=>{const r=e.faces_vertices.map((t=>t.map((t=>[0,1].map((r=>e.vertices_coords[t][r])))))).map((e=>vr.polygon(e)));return r.forEach(((e,t)=>e.setAttributeNS(null,\"index\",t))),finalize_faces(e,r,vr.g(),t)},facesEdgesPolygon=function(e,t){const r=e.faces_edges.map((t=>t.map((t=>e.edges_vertices[t])).map(((e,t,r)=>{const s=r[(t+1)%r.length];return e[1]===s[0]||e[1]===s[1]?e[0]:e[1]})).map((t=>[0,1].map((r=>e.vertices_coords[t][r])))))).map((e=>vr.polygon(e)));return r.forEach(((e,t)=>e.setAttributeNS(null,\"index\",t))),finalize_faces(e,r,vr.g(),t)},drawFaces=(e,t)=>e&&e.vertices_coords&&e.faces_vertices?facesVerticesPolygon(e,t):e&&e.vertices_coords&&e.edges_vertices&&e.faces_edges?facesEdgesPolygon(e,t):vr.g();var Fr=Object.freeze({__proto__:null,drawFaces:drawFaces,facesEdgesPolygon:facesEdgesPolygon,facesVerticesPolygon:facesVerticesPolygon});const Sr={fill:\"none\"},Cr={stroke:\"black\",fill:\"white\"},drawBoundaries=(e,t={})=>{const r=vr.g();if(!e)return r;const s=boundaries$1(e).map((({vertices:t})=>t.map((t=>e.vertices_coords[t])))).filter((e=>e.length));return s.forEach((e=>{const t=vr.polygon(e);addClass(t,\"boundary\"),r.appendChild(t)})),setKeysAndValues(r,isFoldedForm(e)?Sr:Cr),setKeysAndValues(r,t),r};var Vr=Object.freeze({__proto__:null,drawBoundaries:drawBoundaries});const zr={vertices:drawVertices,edges:drawEdges,faces:drawFaces,boundaries:drawBoundaries},Tr={min:[0,0],max:[1,1],span:[1,1]},Pr=[\"boundaries\",\"faces\",\"edges\",\"vertices\"],renderSVG=(e,t,r={})=>{((e,t)=>{void 0===t.vertices&&(t.vertices=!1),isFoldedForm(e)||void 0===t.faces&&(t.faces=!1)})(e,r);const s=((e,t={})=>Pr.map((r=>!1===t[r]?vr.g():zr[r](e,t[r]))).map(((e,t)=>(addClass(e,Pr[t]),e))))(e,r);return s.filter((e=>e.childNodes.length>0)).forEach((e=>t.appendChild(e))),((e,t,r,s)=>{const a=t[3]&&t[3].childNodes.length;if(!(a||void 0!==s.strokeWidth&&!1!==s.strokeWidth||void 0!==s.viewBox&&!1!==s.viewBox))return;const o=boundingBox(r)||Tr,c=Math.max(...o.span),n=findElementTypeInParents(e,\"svg\");if(n&&s.viewBox){const e=boundingBoxToViewBox(o);n.setAttributeNS(null,\"viewBox\",e)}if(n&&s.padding){const e=n.getAttribute(\"viewBox\");if(null!=e){const t=s.padding*c,r=e.split(\" \").map((e=>parseFloat(e))),a=[-t,-t,2*t,2*t].map(((e,t)=>r[t]+e)).join(\" \");n.setAttributeNS(null,\"viewBox\",a)}}if(s.strokeWidth||s[\"stroke-width\"]){const t=s.strokeWidth?s.strokeWidth:s[\"stroke-width\"],a=\"number\"==typeof t?c*t:getStrokeWidth(r);e.setAttributeNS(null,\"stroke-width\",a)}if(a){const e=s.vertices&&null!=s.vertices.radius?s.vertices.radius:s.radius,r=\"string\"==typeof e?parseFloat(e):e,a=\"number\"!=typeof r||Number.isNaN(r)?.02*c:c*r;for(let e=0;e<t[3].childNodes.length;e+=1)t[3].childNodes[e].setAttributeNS(null,\"r\",a)}})(t,s,e,r),addClass(t,...[e.file_classes||[],e.frame_classes||[]].flat()),t},foldToSvg=(e,t={})=>{const r=renderSVG(\"string\"==typeof e?JSON.parse(e):e,vr.svg(),{viewBox:!0,strokeWidth:!0,...t});return t&&t.string?(new(RabbitEarWindow$1().XMLSerializer)).serializeToString(r):r};const foldToObj=e=>{const t=\"string\"==typeof e?JSON.parse(e):e,r=(e=>[\"file_title\",\"file_author\",\"file_description\",\"frame_title\",\"frame_author\",\"frame_description\"].filter((t=>e[t])).map((t=>`# ${t.split(\"_\")[1]}: ${e[t]}`)).join(\"\\n\"))(t);return`${[r,(t.vertices_coords||[]).map((e=>e.join(\" \"))).map((e=>`v ${e}`)).join(\"\\n\"),(t.faces_vertices||[]).map((e=>e.map((e=>e+1)).join(\" \"))).map((e=>`f ${e}`)).join(\"\\n\")].filter((e=>\"\"!==e)).join(\"\\n\")}\\n`};const parseCSSStyleSheet=e=>{if(!e.cssRules)return{};const t={};for(let r=0;r<e.cssRules.length;r+=1){const s=e.cssRules[r];if(\"CSSStyleRule\"!==s.constructor.name)continue;const a=s,o=a.selectorText.split(/,/gm).filter(Boolean).map((e=>e.trim())),c={};Object.values(a.style).forEach((e=>{c[e]=a.style[e]})),o.forEach((e=>{t[e]=c}))}return t};var $r={...Me,...Ie,...ft,...Object.freeze({__proto__:null,foldToSvg:foldToSvg,renderSVG:renderSVG}),...Object.freeze({__proto__:null,foldToObj:foldToObj}),...Xe,...Re,...Pe,...ur,...{...rt,...Vr,...hr,...Mr,...Fr,...it,...Object.freeze({__proto__:null,getStylesheetStyle:(e,t,r,s=[])=>{const a=r.class?r.class.split(/\\s/).filter(Boolean).map((e=>e.trim())).map((e=>`.${e}`)):[],o=r.id?`#${r.id}`:null;if(o)for(let t=0;t<s.length;t+=1)if(s[t][o]&&s[t][o][e])return s[t][o][e];for(let r=0;r<s.length;r+=1){for(let t=0;t<a.length;t+=1)if(s[r][a[t]]&&s[r][a[t]][e])return s[r][a[t]][e];if(s[r][t]&&s[r][t][e])return s[r][t][e]}},parseCSSStyleSheet:parseCSSStyleSheet,parseStyleElement:e=>{const t=\"sheet\"in e?e.sheet:void 0;if(t)return parseCSSStyleSheet(t);if(!(getRootParent(e).constructor===RabbitEarWindow$1().HTMLDocument)){const r=e.parentNode;null!=r&&r.removeChild(e);const s=null!=RabbitEarWindow$1().document.body?RabbitEarWindow$1().document.body:RabbitEarWindow$1().document.createElement(\"body\");s.appendChild(e);const a=parseCSSStyleSheet(t);return s.removeChild(e),null!=r&&r.appendChild(e),a}return[]}})}};var Br={...Q,...S,...Ne,...ne,...Object.freeze({__proto__:null,capitalized:e=>e.charAt(0).toUpperCase()+e.slice(1),toCamel:e=>e.replace(/([-_][a-z])/gi,(e=>e.toUpperCase().replace(\"-\",\"\").replace(\"_\",\"\"))),toKebab:e=>e.replace(/([a-z0-9])([A-Z])/g,\"$1-$2\").replace(/([A-Z])([A-Z])(?=[a-z])/g,\"$1-$2\").toLowerCase()})};const flattenFrame=(e,t=0)=>{if(!e.file_frames||e.file_frames.length<t)return e;const r={},s={};filterKeysWithPrefix(e,\"file\").filter((e=>\"file_frames\"!==e)).forEach((t=>{s[t]=e[t]}));const recurse=(t,s)=>{if(r[t])throw new Error(o);r[t]=!0;const a=[t].concat(s),c=t>0?{...e.file_frames[t-1]}:{...e};return c.frame_inherit&&null!=c.frame_parent?recurse(c.frame_parent,a):a},a=recurse(t,[]).map((t=>{const r=t>0?{...e.file_frames[t-1]}:{...e};return[\"file_frames\",\"frame_parent\",\"frame_inherit\"].forEach((e=>delete r[e])),r})).reduce(((e,t)=>({...e,...t})),s);return br(a)},countFrames=({file_frames:e})=>e?e.length+1:1;var Nr=Object.freeze({__proto__:null,countFrames:countFrames,flattenFrame:flattenFrame,getFileFramesAsArray:e=>{if(!e)return[];if(!e.file_frames||!e.file_frames.length)return[e];const t={...e};return delete t.file_frames,[t,...e.file_frames]},getFramesByClassName:(e,t)=>Array.from(Array(countFrames(e))).map(((t,r)=>flattenFrame(e,r))).filter((e=>e.frame_classes&&e.frame_classes.includes(t)))});const addVertices=(e,t=[])=>{e.vertices_coords||(e.vertices_coords=[]);const r=t.map(((t,r)=>e.vertices_coords.length+r));return r.forEach(((r,s)=>{e.vertices_coords[r]=t[s],e.vertices_vertices&&(e.vertices_vertices[r]=[]),e.vertices_edges&&(e.vertices_edges[r]=[]),e.vertices_faces&&(e.vertices_faces[r]=[])})),r};var Rr=Object.freeze({__proto__:null,addVertex:(e,t,r=[],s=[],a=[])=>{e.vertices_coords||(e.vertices_coords=[]);const o=e.vertices_coords.length;return e.vertices_coords[o]=t,e.vertices_vertices&&(e.vertices_vertices[o]=r),e.vertices_edges&&(e.vertices_edges[o]=s),e.vertices_faces&&(e.vertices_faces[o]=a),o},addVertices:addVertices});const addEdge=(e,t,r=[],s=\"U\",a=0)=>{e.edges_vertices||(e.edges_vertices=[]);const o=e.edges_vertices.length;return e.edges_vertices[o]=t,e.edges_faces&&(e.edges_faces[o]=r),e.edges_assignment&&(e.edges_assignment[o]=s),e.edges_foldAngle&&(e.edges_foldAngle[o]=a),o},addIsolatedEdge=(e,t,r=\"U\",s=0)=>{const a=addEdge(e,t,[],r,s);if(e.vertices_vertices){t.filter((t=>!e.vertices_vertices[t])).forEach((t=>{e.vertices_vertices[t]=[]}));const r=[t[1],t[0]];t.forEach(((t,s)=>{e.vertices_vertices[t].push(r[s])}))}return e.vertices_edges&&(t.filter((t=>!e.vertices_edges[t])).forEach((t=>{e.vertices_edges[t]=[]})),t.forEach((t=>{e.vertices_edges[t].push(a)}))),e.vertices_faces&&t.filter((t=>!e.vertices_faces[t])).forEach((t=>{e.vertices_faces[t]=[]})),a};var Lr=Object.freeze({__proto__:null,addEdge:addEdge,addIsolatedEdge:addIsolatedEdge});const Ir=Object.freeze([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),multiplyMatrix4Vector3=(e,t)=>[e[0]*t[0]+e[4]*t[1]+e[8]*t[2]+e[12],e[1]*t[0]+e[5]*t[1]+e[9]*t[2]+e[13],e[2]*t[0]+e[6]*t[1]+e[10]*t[2]+e[14]],multiplyMatrix4Line3=(e,t,r)=>({vector:[e[0]*t[0]+e[4]*t[1]+e[8]*t[2],e[1]*t[0]+e[5]*t[1]+e[9]*t[2],e[2]*t[0]+e[6]*t[1]+e[10]*t[2]],origin:[e[0]*r[0]+e[4]*r[1]+e[8]*r[2]+e[12],e[1]*r[0]+e[5]*r[1]+e[9]*r[2]+e[13],e[2]*r[0]+e[6]*r[1]+e[10]*r[2]+e[14]]}),multiplyMatrices4=(e,t)=>[e[0]*t[0]+e[4]*t[1]+e[8]*t[2]+e[12]*t[3],e[1]*t[0]+e[5]*t[1]+e[9]*t[2]+e[13]*t[3],e[2]*t[0]+e[6]*t[1]+e[10]*t[2]+e[14]*t[3],e[3]*t[0]+e[7]*t[1]+e[11]*t[2]+e[15]*t[3],e[0]*t[4]+e[4]*t[5]+e[8]*t[6]+e[12]*t[7],e[1]*t[4]+e[5]*t[5]+e[9]*t[6]+e[13]*t[7],e[2]*t[4]+e[6]*t[5]+e[10]*t[6]+e[14]*t[7],e[3]*t[4]+e[7]*t[5]+e[11]*t[6]+e[15]*t[7],e[0]*t[8]+e[4]*t[9]+e[8]*t[10]+e[12]*t[11],e[1]*t[8]+e[5]*t[9]+e[9]*t[10]+e[13]*t[11],e[2]*t[8]+e[6]*t[9]+e[10]*t[10]+e[14]*t[11],e[3]*t[8]+e[7]*t[9]+e[11]*t[10]+e[15]*t[11],e[0]*t[12]+e[4]*t[13]+e[8]*t[14]+e[12]*t[15],e[1]*t[12]+e[5]*t[13]+e[9]*t[14]+e[13]*t[15],e[2]*t[12]+e[6]*t[13]+e[10]*t[14]+e[14]*t[15],e[3]*t[12]+e[7]*t[13]+e[11]*t[14]+e[15]*t[15]],determinant4=e=>{const t=e[10]*e[15]-e[11]*e[14],r=e[9]*e[15]-e[11]*e[13],s=e[9]*e[14]-e[10]*e[13],a=e[8]*e[15]-e[11]*e[12],o=e[8]*e[14]-e[10]*e[12],c=e[8]*e[13]-e[9]*e[12];return e[0]*(e[5]*t-e[6]*r+e[7]*s)-e[1]*(e[4]*t-e[6]*a+e[7]*o)+e[2]*(e[4]*r-e[5]*a+e[7]*c)-e[3]*(e[4]*s-e[5]*o+e[6]*c)},invertMatrix4=e=>{const t=determinant4(e);if(Math.abs(t)<1e-12||Number.isNaN(t)||!Number.isFinite(e[12])||!Number.isFinite(e[13])||!Number.isFinite(e[14]))return;const r=e[10]*e[15]-e[11]*e[14],s=e[9]*e[15]-e[11]*e[13],a=e[9]*e[14]-e[10]*e[13],o=e[8]*e[15]-e[11]*e[12],c=e[8]*e[14]-e[10]*e[12],n=e[8]*e[13]-e[9]*e[12],i=e[6]*e[15]-e[7]*e[14],l=e[5]*e[15]-e[7]*e[13],d=e[5]*e[14]-e[6]*e[13],_=e[6]*e[11]-e[7]*e[10],m=e[5]*e[11]-e[7]*e[9],g=e[5]*e[10]-e[6]*e[9],v=e[4]*e[15]-e[7]*e[12],p=e[4]*e[14]-e[6]*e[12],u=e[4]*e[11]-e[7]*e[8],h=e[4]*e[10]-e[6]*e[8],b=e[4]*e[13]-e[5]*e[12],y=e[4]*e[9]-e[5]*e[8],E=[+(e[5]*r-e[6]*s+e[7]*a),-(e[1]*r-e[2]*s+e[3]*a),+(e[1]*i-e[2]*l+e[3]*d),-(e[1]*_-e[2]*m+e[3]*g),-(e[4]*r-e[6]*o+e[7]*c),+(e[0]*r-e[2]*o+e[3]*c),-(e[0]*i-e[2]*v+e[3]*p),+(e[0]*_-e[2]*u+e[3]*h),+(e[4]*s-e[5]*o+e[7]*n),-(e[0]*s-e[1]*o+e[3]*n),+(e[0]*l-e[1]*v+e[3]*b),-(e[0]*m-e[1]*u+e[3]*y),-(e[4]*a-e[5]*c+e[6]*n),+(e[0]*a-e[1]*c+e[2]*n),-(e[0]*d-e[1]*p+e[2]*b),+(e[0]*g-e[1]*h+e[2]*y)],M=1/t;return E.map((e=>e*M))},Ur=Object.freeze([1,0,0,0,0,1,0,0,0,0,1,0]),makeMatrix4Translate=(e=0,t=0,r=0)=>[...Ur,e,t,r,1],singleAxisRotate4=(e,t,r,s,a)=>{const o=Math.cos(e),c=Math.sin(e),n=[...Ir];n[4*r+r]=o,n[4*r+s]=(a?1:-1)*c,n[4*s+r]=(a?-1:1)*c,n[4*s+s]=o;const i=[0,1,2].map((e=>t[e]||0)),l=[...Ir],d=[...Ir];return[12,13,14].forEach(((e,t)=>{l[e]=-i[t],d[e]=i[t]})),multiplyMatrices4(d,multiplyMatrices4(n,l))},makeMatrix4Scale=(e=[1,1,1],t=[0,0,0])=>[e[0],0,0,0,0,e[1],0,0,0,0,e[2],0,e[0]*-t[0]+t[0],e[1]*-t[1]+t[1],e[2]*-t[2]+t[2],1],makePerspectiveMatrix4=(e,t,r,s)=>{const a=Math.tan(.5*Math.PI-.5*e),o=1/(r-s);return[t<1?a:a/t,0,0,0,0,t<1?a*t:a,0,0,0,0,(r+s)*o,-1,0,0,r*s*o*2,0]},makeOrthographicMatrix4=(e,t,r,s,a,o)=>[2/(t-s),0,0,0,0,2/(e-r),0,0,0,0,2/(a-o),0,(s+t)/(s-t),(r+e)/(r-e),(a+o)/(a-o),1];var Dr=Object.freeze({__proto__:null,determinant4:determinant4,identity4x4:Ir,invertMatrix4:invertMatrix4,isIdentity4x4:e=>Ir.map(((t,r)=>Math.abs(t-e[r])<p)).reduce(((e,t)=>e&&t),!0),makeLookAtMatrix4:(e,t,r)=>{const s=normalize3(subtract3(e,t)),a=normalize3(cross3(r,s)),o=normalize3(cross3(s,a));return[a[0],a[1],a[2],0,o[0],o[1],o[2],0,s[0],s[1],s[2],0,e[0],e[1],e[2],1]},makeMatrix4ReflectZ:(e,t=[0,0])=>{const r=makeMatrix2Reflect(e,t);return[r[0],r[1],0,0,r[2],r[3],0,0,0,0,1,0,r[4],r[5],0,1]},makeMatrix4Rotate:(e,t=[0,0,1],r=[0,0,0])=>{const s=[0,1,2].map((e=>r[e]||0)),[a,o,c]=resize(3,normalize$2(t)),n=Math.cos(e),i=Math.sin(e),l=1-n,d=makeMatrix4Translate(-s[0],-s[1],-s[2]),_=makeMatrix4Translate(s[0],s[1],s[2]);return multiplyMatrices4(_,multiplyMatrices4([l*a*a+n,l*o*a+c*i,l*c*a-o*i,0,l*a*o-c*i,l*o*o+n,l*c*o+a*i,0,l*a*c+o*i,l*o*c-a*i,l*c*c+n,0,0,0,0,1],d))},makeMatrix4RotateX:(e,t=[0,0,0])=>singleAxisRotate4(e,t,1,2,!0),makeMatrix4RotateY:(e,t=[0,0,0])=>singleAxisRotate4(e,t,0,2,!1),makeMatrix4RotateZ:(e,t=[0,0,0])=>singleAxisRotate4(e,t,0,1,!0),makeMatrix4Scale:makeMatrix4Scale,makeMatrix4Translate:makeMatrix4Translate,makeMatrix4UniformScale:(e=1,t=[0,0,0])=>makeMatrix4Scale([e,e,e],t),makeOrthographicMatrix4:makeOrthographicMatrix4,makePerspectiveMatrix4:makePerspectiveMatrix4,multiplyMatrices4:multiplyMatrices4,multiplyMatrix4Line3:multiplyMatrix4Line3,multiplyMatrix4Vector3:multiplyMatrix4Vector3});const quaternionFromTwoVectors=(e,t)=>{const r=cross3(e,t),s=[r[0],r[1],r[2],dot(e,t)];s[3]+=magnitude(s);const[a,o,c,n]=normalize$2(s);return[a,o,c,n]},matrix4FromQuaternion=e=>multiplyMatrices4([+e[3],+e[2],-e[1],+e[0],-e[2],+e[3],+e[0],+e[1],+e[1],-e[0],+e[3],+e[2],-e[0],-e[1],-e[2],+e[3]],[+e[3],+e[2],-e[1],-e[0],-e[2],+e[3],+e[0],-e[1],+e[1],-e[0],+e[3],-e[2],+e[0],+e[1],+e[2],+e[3]]);var Qr=Object.freeze({__proto__:null,matrix4FromQuaternion:matrix4FromQuaternion,quaternionFromTwoVectors:quaternionFromTwoVectors});const makeFacesFaces=({faces_vertices:e})=>{const t={},r=e.map((e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>e.join(\" \")))));return r.flat().forEach((e=>{t[e]=[]})),r.forEach(((e,r)=>e.forEach((e=>t[e].push(r))))),e.map(((e,r)=>e.map(((e,t,r)=>[r[(t+1)%r.length],e])).map((e=>e.join(\" \"))).map((e=>t[e])).map((e=>void 0===e?[void 0]:e)).flatMap((e=>e.filter((e=>r!==e)).shift()))))};var Wr=Object.freeze({__proto__:null,makeFacesFaces:makeFacesFaces});const selfRelationalArraySubset=(e,t)=>{const r={};t.forEach((e=>{r[e]=!0}));const s=[];return t.forEach((t=>{s[t]=e[t].filter((e=>r[e]))})),s},subgraphExclusive=(e,t={})=>{const r={vertices:[],edges:[],faces:[],...t},s=Object.keys(r),a={...e};G.graph.forEach((e=>delete a[e])),delete a.file_frames;const o={};s.forEach((e=>{o[e]={}})),s.forEach((e=>r[e].forEach((t=>{o[e][t]=!0}))));const c={};return s.forEach((t=>{filterKeysWithPrefix(e,t).forEach((e=>{c[e]={}})),filterKeysWithSuffix(e,t).forEach((e=>{c[e]={}}))})),s.forEach((t=>{filterKeysWithPrefix(e,t).forEach((e=>{c[e].prefix=t})),filterKeysWithSuffix(e,t).forEach((e=>{c[e].suffix=t}))})),Object.keys(c).forEach((e=>{a[e]=[]})),Object.keys(c).forEach((t=>{const{prefix:s,suffix:n}=c[t];s&&n?r[s].forEach((r=>{a[t][r]=e[t][r].filter((e=>o[n][e]))})):s?r[s].forEach((r=>{a[t][r]=e[t][r]})):a[t]=n?e[t].map((e=>e.filter((e=>o[n][e])))):e[t]})),a},subgraph=(e,t={})=>{const r={vertices:[],edges:[],faces:[],...t},s={vertices:{},edges:{},faces:{}};return r.vertices.forEach((e=>{s.vertices[e]=!0})),r.edges.forEach((e=>{s.edges[e]=!0})),r.edges.forEach((t=>e.edges_vertices[t].forEach((e=>{s.vertices[e]=!0})))),r.faces.forEach((e=>{s.faces[e]=!0})),r.faces.forEach((t=>e.faces_vertices[t].forEach((e=>{s.vertices[e]=!0})))),e.faces_vertices.map(((e,t)=>t)).filter((t=>e.faces_vertices[t].map((e=>s.vertices[e])).reduce(((e,t)=>e&&t),!0))).forEach((e=>{s.faces[e]=!0})),e.edges_vertices.map(((e,t)=>t)).filter((t=>e.edges_vertices[t].map((e=>s.vertices[e])).reduce(((e,t)=>e&&t),!0))).forEach((e=>{s.edges[e]=!0})),subgraphExclusive(e,{vertices:Object.keys(s.vertices),edges:Object.keys(s.edges),faces:Object.keys(s.faces)})},subgraphWithFaces=(e,t)=>{let r=[];e.faces_vertices&&(r=uniqueSortedNumbers(t.flatMap((t=>e.faces_vertices[t]))));let s=[];if(e.faces_edges)s=uniqueSortedNumbers(t.flatMap((t=>e.faces_edges[t])));else if(e.edges_vertices){const t={};r.forEach((e=>{t[e]=!0})),s=e.edges_vertices.map(((e,r)=>t[e[0]]&&t[e[1]]?r:void 0)).filter((e=>void 0!==e))}return subgraphExclusive(e,{vertices:r,edges:s,faces:t})};var qr=Object.freeze({__proto__:null,selfRelationalArraySubset:selfRelationalArraySubset,subgraph:subgraph,subgraphExclusive:subgraphExclusive,subgraphWithFaces:subgraphWithFaces,subgraphWithVertices:(e,t=[])=>{const r={vertices:[],edges:[]};return t.forEach((e=>{r.vertices[e]=!0})),e.vertices_edges&&r.vertices.forEach(((t,s)=>e.vertices_edges[s].forEach((e=>{r.edges[e]=!0})))),e.edges_vertices&&r.edges.forEach(((t,s)=>e.edges_vertices[s].forEach((e=>{r.vertices[e]=!0})))),subgraphExclusive(e,{vertices:r.vertices.map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)),edges:r.edges.map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e))})}});const getFacesPlane=({vertices_coords:e,faces_vertices:t},r=p)=>{const s=makeFacesNormal({vertices_coords:e,faces_vertices:t}),a=t.map((()=>[]));for(let e=0;e<t.length-1;e+=1)for(let o=e+1;o<t.length;o+=1)parallelNormalized(s[e],s[o],r)&&(a[e].push(o),a[o].push(e));const o=connectedComponents(a),c=invertFlatToArrayMap(o),n=c.map((e=>s[e[0]])),i=[];c.forEach(((e,t)=>e.forEach((e=>{i[e]=dot3(s[e],n[t])>0}))));const l=t.map((t=>e[t[0]])).map(resize3),d=c.map(((e,t)=>e.map((e=>dot3(n[t],l[e]))))),_=d.map(((e,t)=>clusterScalars(e).map((e=>e.map((e=>c[t][e])))))),m=_.flat(),g=_.flatMap(((e,t)=>e.map((()=>n[t])))).map(resize3),v=m.map((e=>e[0])).map((e=>l[e])).map(((e,t)=>dot3(g[t],e))).map(((e,t)=>scale3$1(g[t],e))),u=m.map(((e,t)=>({normal:g[t],origin:v[t]}))),h=invertArrayToFlatMap(m),b=[0,0,1],y=u.map((({normal:e})=>Math.abs(dot(e,b)+1)<r?[1,0,0,0,0,-1,0,0,0,0,-1,0,0,0,0,1]:matrix4FromQuaternion(quaternionFromTwoVectors(e,b))));return u.forEach((({origin:e},t)=>{const r=multiplyMatrix4Vector3(y[t],flip3(e));y[t][12]=r[0],y[t][13]=r[1],y[t][14]=r[2]})),{planes:u,planes_faces:m,planes_transform:y,faces_plane:h,faces_winding:i}},getCoplanarAdjacentOverlappingFaces=({vertices_coords:e,faces_vertices:t,faces_faces:r},s=p)=>{r||(r=makeFacesFaces({faces_vertices:t}));const{planes:a,planes_faces:o,planes_transform:c,faces_plane:n,faces_winding:i}=getFacesPlane({vertices_coords:e,faces_vertices:t},s),l=e.map(resize3),d=t.map(((e,t)=>i[t]?e:e.slice().reverse())).map((e=>e.map((e=>l[e])))).map((e=>makePolygonNonCollinear3(e,s))).map(((e,t)=>e.map((e=>multiplyMatrix4Vector3(c[n[t]],e))).map(resize2))),_=o.map((e=>selfRelationalArraySubset(r,e))),m=_.map(connectedComponents),g=m.map((e=>invertFlatToArrayMap(e))),v=m.map((e=>{const t=e.map(((e,t)=>t));return e.map((r=>t.filter((t=>e[t]!==r))))})),u=g.map((e=>e.map((()=>[]))));v.forEach(((e,t)=>{const r={};e.forEach(((e,a)=>e.forEach((e=>{const o=[a,e].map((e=>m[t][e])),c=o.join(\" \");if(r[c])return;if(overlapConvexPolygons(d[a],d[e],s)){const e=o.reverse().join(\" \");r[c]=!0,r[e]=!0,u[t][o[0]].push(o[1]),u[t][o[1]].push(o[0])}}))))}));const h=u.map((e=>invertFlatToArrayMap(connectedComponents(e)))),b=h.flatMap(((e,t)=>e.map((()=>t)))),y=invertFlatToArrayMap(b),E=h.flatMap(((e,t)=>e.map((e=>e.flatMap((e=>g[t][e])))))),M=invertArrayToFlatMap(E);return{planes:a,planes_faces:o,planes_transform:c,planes_clusters:y,faces_winding:i,faces_plane:n,faces_cluster:M,clusters_plane:b,clusters_faces:E}};var Gr=Object.freeze({__proto__:null,getCoplanarAdjacentOverlappingFaces:getCoplanarAdjacentOverlappingFaces,getFacesPlane:getFacesPlane});const Hr=Object.freeze([1,0,0,0,1,0,0,0,1]),Jr=Object.freeze(Hr.concat(0,0,0)),multiplyMatrix3Vector3=(e,t)=>[e[0]*t[0]+e[3]*t[1]+e[6]*t[2]+e[9],e[1]*t[0]+e[4]*t[1]+e[7]*t[2]+e[10],e[2]*t[0]+e[5]*t[1]+e[8]*t[2]+e[11]],multiplyMatrices3=(e,t)=>[e[0]*t[0]+e[3]*t[1]+e[6]*t[2],e[1]*t[0]+e[4]*t[1]+e[7]*t[2],e[2]*t[0]+e[5]*t[1]+e[8]*t[2],e[0]*t[3]+e[3]*t[4]+e[6]*t[5],e[1]*t[3]+e[4]*t[4]+e[7]*t[5],e[2]*t[3]+e[5]*t[4]+e[8]*t[5],e[0]*t[6]+e[3]*t[7]+e[6]*t[8],e[1]*t[6]+e[4]*t[7]+e[7]*t[8],e[2]*t[6]+e[5]*t[7]+e[8]*t[8],e[0]*t[9]+e[3]*t[10]+e[6]*t[11]+e[9],e[1]*t[9]+e[4]*t[10]+e[7]*t[11]+e[10],e[2]*t[9]+e[5]*t[10]+e[8]*t[11]+e[11]],determinant3=e=>e[0]*e[4]*e[8]-e[0]*e[7]*e[5]-e[3]*e[1]*e[8]+e[3]*e[7]*e[2]+e[6]*e[1]*e[5]-e[6]*e[4]*e[2],invertMatrix3=e=>{const t=determinant3(e);if(Math.abs(t)<1e-12||Number.isNaN(t)||!Number.isFinite(e[9])||!Number.isFinite(e[10])||!Number.isFinite(e[11]))return;const r=[e[4]*e[8]-e[7]*e[5],-e[1]*e[8]+e[7]*e[2],e[1]*e[5]-e[4]*e[2],-e[3]*e[8]+e[6]*e[5],e[0]*e[8]-e[6]*e[2],-e[0]*e[5]+e[3]*e[2],e[3]*e[7]-e[6]*e[4],-e[0]*e[7]+e[6]*e[1],e[0]*e[4]-e[3]*e[1],-e[3]*e[7]*e[11]+e[3]*e[8]*e[10]+e[6]*e[4]*e[11]-e[6]*e[5]*e[10]-e[9]*e[4]*e[8]+e[9]*e[5]*e[7],e[0]*e[7]*e[11]-e[0]*e[8]*e[10]-e[6]*e[1]*e[11]+e[6]*e[2]*e[10]+e[9]*e[1]*e[8]-e[9]*e[2]*e[7],-e[0]*e[4]*e[11]+e[0]*e[5]*e[10]+e[3]*e[1]*e[11]-e[3]*e[2]*e[10]-e[9]*e[1]*e[5]+e[9]*e[2]*e[4]],s=1/t;return r.map((e=>e*s))},singleAxisRotate=(e,t,r,s,a)=>{const o=Math.cos(e),c=Math.sin(e),n=Hr.concat([0,0,0]);n[3*r+r]=o,n[3*r+s]=(a?1:-1)*c,n[3*s+r]=(a?-1:1)*c,n[3*s+s]=o;const i=[0,1,2].map((e=>t[e]||0)),l=Hr.concat(flip(i)),d=Hr.concat(i);return multiplyMatrices3(d,multiplyMatrices3(n,l))},makeMatrix3RotateX=(e,t=[0,0,0])=>singleAxisRotate(e,t,1,2,!0),makeMatrix3RotateY=(e,t=[0,0,0])=>singleAxisRotate(e,t,0,2,!1),makeMatrix3RotateZ=(e,t=[0,0,0])=>singleAxisRotate(e,t,0,1,!0),makeMatrix3Rotate=(e,t=[0,0,1],r=[0,0,0])=>{const s=[0,1,2].map((e=>r[e]||0)),[a,o,c]=resize(3,normalize$2(t)),n=Math.cos(e),i=Math.sin(e),l=1-n,d=Hr.concat(-s[0],-s[1],-s[2]),_=Hr.concat(s[0],s[1],s[2]);return multiplyMatrices3(_,multiplyMatrices3([l*a*a+n,l*o*a+c*i,l*c*a-o*i,l*a*o-c*i,l*o*o+n,l*c*o+a*i,l*a*c+o*i,l*o*c-a*i,l*c*c+n,0,0,0],d))},makeMatrix3Scale=(e=[1,1,1],t=[0,0,0])=>[e[0],0,0,0,e[1],0,0,0,e[2],e[0]*-t[0]+t[0],e[1]*-t[1]+t[1],e[2]*-t[2]+t[2]];var Zr=Object.freeze({__proto__:null,determinant3:determinant3,identity3x3:Hr,identity3x4:Jr,invertMatrix3:invertMatrix3,isIdentity3x4:e=>Jr.map(((t,r)=>Math.abs(t-e[r])<p)).reduce(((e,t)=>e&&t),!0),makeMatrix3ReflectZ:(e,t=[0,0])=>{const r=makeMatrix2Reflect(e,t);return[r[0],r[1],0,r[2],r[3],0,0,0,1,r[4],r[5],0]},makeMatrix3Rotate:makeMatrix3Rotate,makeMatrix3RotateX:makeMatrix3RotateX,makeMatrix3RotateY:makeMatrix3RotateY,makeMatrix3RotateZ:makeMatrix3RotateZ,makeMatrix3Scale:makeMatrix3Scale,makeMatrix3Translate:(e=0,t=0,r=0)=>Hr.concat(e,t,r),makeMatrix3UniformScale:(e=1,t=[0,0,0])=>makeMatrix3Scale([e,e,e],t),multiplyMatrices3:multiplyMatrices3,multiplyMatrix3Line3:(e,t,r)=>({vector:[e[0]*t[0]+e[3]*t[1]+e[6]*t[2],e[1]*t[0]+e[4]*t[1]+e[7]*t[2],e[2]*t[0]+e[5]*t[1]+e[8]*t[2]],origin:[e[0]*r[0]+e[3]*r[1]+e[6]*r[2]+e[9],e[1]*r[0]+e[4]*r[1]+e[7]*r[2]+e[10],e[2]*r[0]+e[5]*r[1]+e[8]*r[2]+e[11]]}),multiplyMatrix3Vector3:multiplyMatrix3Vector3});const minimumSpanningTrees=(e=[],t=[])=>{if(0===e.length)return[];const r=[],s={};e.forEach(((e,t)=>{s[t]=!0}));do{const a=t.filter((e=>s[e])).shift(),o=void 0!==a?a:parseInt(Object.keys(s).shift(),10);delete s[o];const c=[];let n=[{index:o}];do{c.push(n);const t=n.flatMap((t=>e[t.index].filter((e=>s[e]&&null!=e)).map((e=>({index:e,parent:t.index}))))),r={};t.forEach(((e,t)=>{s[e.index]||(r[t]=!0),delete s[e.index]})),n=t.filter(((e,t)=>!r[t]))}while(n.length);r.push(c)}while(Object.keys(s).length);return r};var Yr=Object.freeze({__proto__:null,minimumSpanningTrees:minimumSpanningTrees});const facesSharedEdgesVertices=(e,t)=>{const r={};t.forEach((e=>{r[e]=!0}));const s=e.map((e=>r[e]?e:void 0));return rotateCircularArray(s,s.indexOf(void 0)).map(((e,t,r)=>[e,r[(t+1)%r.length]])).filter((e=>void 0!==e[0]&&void 0!==e[1]))},Xr={U:!0,u:!0},makeFacesMatrix=({vertices_coords:e,edges_vertices:t,edges_foldAngle:r,edges_assignment:s,faces_vertices:a,faces_faces:o},c)=>{!s&&r&&(s=makeEdgesAssignmentSimple({edges_foldAngle:r})),r||(r=s?makeEdgesFoldAngle({edges_assignment:s}):Array(t.length).fill(0)),o||(o=makeFacesFaces({faces_vertices:a}));const n=makeVerticesToEdge({edges_vertices:t}),i=a.map((()=>[...Jr]));return minimumSpanningTrees(o,c).forEach((t=>t.slice(1).forEach((t=>t.forEach((t=>{const o=facesSharedEdgesVertices(a[t.index],a[t.parent]).shift(),c=o.map((t=>e[t])).map(resize3),l=o.join(\" \"),d=n[l],_=Xr[s[d]]?Math.PI:r[d]*Math.PI/180,m=makeMatrix3Rotate(_,subtract3(c[1],c[0]),c[0]);i[t.index]=multiplyMatrices3(i[t.parent],m)})))))),i},makeFacesMatrix2=({vertices_coords:e,edges_vertices:t,edges_foldAngle:r,edges_assignment:s,faces_vertices:a,faces_faces:o},c)=>{r||(r=s?makeEdgesFoldAngle({edges_assignment:s}):Array(t.length).fill(0)),o||(o=makeFacesFaces({faces_vertices:a}));const n=makeEdgesIsFolded({edges_vertices:t,edges_foldAngle:r,edges_assignment:s}),i=makeVerticesToEdge({edges_vertices:t}),l=a.map((()=>T));return minimumSpanningTrees(o,c).forEach((t=>t.slice(1).forEach((t=>t.forEach((t=>{const r=facesSharedEdgesVertices(a[t.index],a[t.parent]).shift(),s=r.map((t=>e[t])),o=r.join(\" \"),c=i[o],d=subtract2(s[1],s[0]),_=resize2(s[0]),m=n[c]?makeMatrix2Reflect(d,_):T;l[t.index]=multiplyMatrices2(l[t.parent],m)})))))),l};var Kr=Object.freeze({__proto__:null,facesSharedEdgesVertices:facesSharedEdgesVertices,makeFacesMatrix:makeFacesMatrix,makeFacesMatrix2:makeFacesMatrix2});const makeVerticesCoords3DFolded=({vertices_coords:e,vertices_faces:t,edges_vertices:r,edges_foldAngle:s,edges_assignment:a,faces_vertices:o,faces_faces:c,faces_matrix:n},i)=>{if(!e||!e.length)return[];if(!o||!o.length)return e.map(resize3);n=makeFacesMatrix({vertices_coords:e,edges_vertices:r,edges_foldAngle:s,edges_assignment:a,faces_vertices:o,faces_faces:c},i),t||(t=makeVerticesFaces({faces_vertices:o}));const l=t.map((e=>e.find((e=>null!=e)))).map((e=>void 0===e?[...Jr]:n[e]));return e.map(resize3).map(((e,t)=>multiplyMatrix3Vector3(l[t],e)))},makeVerticesCoordsFlatFolded=({vertices_coords:e,edges_vertices:t,edges_foldAngle:r,edges_assignment:s,faces_vertices:a,faces_faces:o},c=[])=>{if(!e||!e.length)return[];if(!a||!a.length)return e.map(resize2);o||(o=makeFacesFaces({faces_vertices:a}));const n=makeEdgesIsFolded({edges_vertices:t,edges_foldAngle:r,edges_assignment:s}),i=[],l=[],d=makeVerticesToEdge({edges_vertices:t});return minimumSpanningTrees(o,c).forEach((r=>{const s=r.shift();if(!s||!s.length)return;const o=s[0];l[o.index]=!1,a[o.index].forEach((t=>{i[t]=[...e[t]]})),r.forEach((r=>r.forEach((r=>{const s=facesSharedEdgesVertices(a[r.index],a[r.parent]).shift().join(\" \"),o=d[s],c=t[o].map((e=>i[e]));if(void 0===c[0]||void 0===c[1])return;const _=t[o].map((t=>e[t])),m=_[0],g=normalize2(subtract2(_[1],_[0])),v=rotate90(g);l[r.index]=n[o]?!l[r.parent]:l[r.parent];const p=normalize2(subtract2(c[1],c[0])),u=c[0],h=l[r.index]?rotate270(p):rotate90(p);a[r.index].filter((e=>void 0===i[e])).forEach((t=>{const r=subtract2(e[t],m),s=dot(r,v),a=dot(r,g),o=scale2$1(p,a),c=scale2$1(h,s),n=add2(add2(u,o),c);i[t]=n}))}))))})),i},makeVerticesCoordsFolded=(e,t)=>edgesFoldAngleAreAllFlat(e)?makeVerticesCoordsFlatFolded(e,t):makeVerticesCoords3DFolded(e,t),makeVerticesCoordsFoldedFromMatrix2=({vertices_coords:e,vertices_faces:t,faces_vertices:r},s)=>{t||(t=makeVerticesFaces({faces_vertices:r}));const a=t.map((e=>e.find((e=>null!=e)))),o=a.map((e=>void 0===e?T:s[e]));return e.map(resize2).map(((e,t)=>multiplyMatrix2Vector2(o[t],e)))};var es=Object.freeze({__proto__:null,makeVerticesCoords3DFolded:makeVerticesCoords3DFolded,makeVerticesCoordsFlatFolded:makeVerticesCoordsFlatFolded,makeVerticesCoordsFolded:makeVerticesCoordsFolded,makeVerticesCoordsFoldedFromMatrix2:makeVerticesCoordsFoldedFromMatrix2});const faceContainingPoint=({vertices_coords:e,faces_vertices:t},r,s)=>{const a=(({vertices_coords:e,faces_vertices:t=[]},r,s=include)=>e?t.map(((t,r)=>({face:t.map((t=>e[t])),i:r}))).filter((e=>overlapConvexPolygonPoint(e.face,r,s).overlap)).map((e=>e.i)):[])({vertices_coords:e,faces_vertices:t},r,include);switch(a.length){case 0:return;case 1:return a[0]}if(!s)return a[0];const o=add2(r,scale2$1(s,.01)),c=a.map((r=>t[r].map((t=>e[t])))),n=a.filter(((e,t)=>overlapConvexPolygonPoint(c[t],o,exclude).overlap));return 0===n.length?a.find(((e,t)=>overlapConvexPolygonPoint(c[t],o,include).overlap)):n[0]},makeVerticesFacesForVertex=({vertices_vertices:e,vertices_edges:t,edges_vertices:r},s,a)=>e?e[s].map((e=>[s,e].join(\" \"))).map((e=>a[e])):t&&r?t[s].map((e=>r[e])).map((([e,t])=>s===e?[s,t]:[s,e])).map((e=>e.join(\" \"))).map((e=>a[e])):void 0,makeEdgesFacesForEdge=({vertices_faces:e,edges_vertices:t,edges_faces:r,faces_vertices:s,faces_edges:a},o)=>{if(r&&r[o])return r[o];if(!t)return[];if(s){const r={[t[o][0]]:!0,[t[o][1]]:!0},a=e?uniqueElements(t[o].flatMap((t=>e[t]))):s.map(((e,t)=>t));return(({faces_vertices:e},t,r)=>t.filter((t=>2===new Set(e[t].filter((e=>r[e]))).size)))({faces_vertices:s},a,r)}return a?a.map(((e,t)=>t)).filter((e=>a[e].includes(o))):[]},updateFacesVertices=({faces_vertices:e},t,r,s)=>{if(!e)return;s.map((t=>e[t])).forEach((e=>e.map(((e,t,s)=>{return a=e,o=s[(t+1)%s.length],a===r[0]&&o===r[1]||a===r[1]&&o===r[0]?(t+1)%s.length:void 0;var a,o})).filter((e=>void 0!==e)).sort(((e,t)=>t-e)).forEach((r=>e.splice(r,0,t)))))},splitEdge=(e,t,r=void 0)=>{const s=e.edges_vertices[t];if(!r){const[t,a]=s.map((t=>e.vertices_coords[t]));r=midpoint(t,a)}const a=e.vertices_coords.length;e.vertices_coords[a]=3===r.length?[r[0],r[1],r[2]]:[r[0],r[1]];const[o,c]=[0,1].map((t=>t+e.edges_vertices.length)),n=[o,c];((e,t,r)=>{const s=e.edges_vertices[t],a=[{edges_vertices:[s[0],r]},{edges_vertices:[r,s[1]]}];return a.forEach((r=>[\"edges_assignment\",\"edges_foldAngle\"].filter((r=>e[r]&&void 0!==e[r][t])).forEach((s=>{r[s]=e[s][t]})))),a})(e,t,a).forEach(((t,r)=>Object.keys(t).forEach((s=>{e[s][n[r]]=t[s]})))),(({vertices_vertices:e},t,r)=>{if(!e)return;e[t]=[...r];const s=r.map(((t,r,s)=>e[t].indexOf(s[(r+1)%s.length])));r.forEach(((r,a)=>-1===s[a]?e[r].push(t):e[r].splice(s[a],1,t)))})(e,a,s),(({vertices_edges:e},t,r,s,a)=>{e&&(e[r]=[...a],s.map((r=>e[r].indexOf(t))).map(((e,t)=>({index:e,vertex:s[t],edge:a[t]}))).filter((e=>-1!==e.index)).forEach((({index:t,vertex:r,edge:s})=>{e[r][t]=s})))})(e,t,a,s,n);const i=makeEdgesFacesForEdge(e,t).filter((e=>void 0!==e));updateFacesVertices(e,a,s,i),(({edges_vertices:e,faces_vertices:t,faces_edges:r},s,a)=>{if(!r||!t)return;const o=s.flatMap((e=>r[e])).concat(a).filter((e=>void 0!==e)),c=makeVerticesToEdge({edges_vertices:e},o);(({faces_vertices:e},t,r)=>t.map((t=>e[t].map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>e.join(\" \"))).map((e=>r[e])))))({faces_vertices:t},s,c).forEach(((e,t)=>{r[s[t]]=e}))})(e,i,n),(({vertices_vertices:e,vertices_edges:t,vertices_faces:r,edges_vertices:s,faces_vertices:a},o,c)=>{if(!r)return;if(!a)return void(r[o]=[...c]);const n=makeVerticesToFace({faces_vertices:a},c),i=makeVerticesFacesForVertex({vertices_vertices:e,vertices_edges:t,edges_vertices:s},o,n);r[o]=void 0===i?[...c]:i})(e,a,i),(({edges_faces:e},t,r)=>{e&&t.forEach((t=>{e[t]=[...r]}))})(e,n,i),(({faces_vertices:e,faces_faces:t},r,s)=>{if(!e||!t)return;const a=s.map((t=>e[t].indexOf(r))),o=a.map(((e,r)=>(e+t[s[r]].length-1)%t[s[r]].length)).map(((e,r)=>t[s[r]][e]));s.forEach(((e,r)=>-1===a[r]?void 0:t[e].splice(a[r],0,o[r])))})(e,a,i);const l=remove(e,\"edges\",[t]);n.forEach(((e,t)=>{n[t]=l[n[t]]}));const d=l.slice();return d.splice(-2),d[t]=n,{vertex:a,edges:{map:d,add:n,remove:t}}};var ts=Object.freeze({__proto__:null,splitEdge:splitEdge});const build_faces=(e,t,r)=>{const s=[0,1].map((t=>e.faces_vertices.length+t));return(({edges_vertices:e,faces_vertices:t,faces_edges:r},s,a)=>{const o=a.map((e=>t[s].indexOf(e))),c=((e,t)=>(t.sort(((e,t)=>e-t)),[e.slice(t[1]).concat(e.slice(0,t[0]+1)),e.slice(t[0],t[1]+1)]))(t[s],o).map((e=>({faces_vertices:e,faces_edges:[]})));if(r){const t=makeVerticesToEdge({edges_vertices:e});c.map((e=>e.faces_vertices.map(((e,t,r)=>`${e} ${r[(t+1)%r.length]}`)).map((e=>t[e])))).forEach(((e,t)=>{c[t].faces_edges=e}))}return c})(e,t,r).forEach(((t,r)=>Object.keys(t).forEach((a=>{e[a][s[r]]=t[a]})))),s},rebuild_edge=(e,t,r)=>{const s=e.edges_vertices.length,a=(({vertices_coords:e},t,r)=>{const s=t.slice().map((t=>e[t])).reverse();return{edges_vertices:[...t],edges_foldAngle:0,edges_assignment:\"U\",edges_length:distance(s[0],s[1]),edges_vector:subtract(s[0],s[1]),edges_faces:[r,r]}})(e,r,t);return Object.keys(a).filter((t=>void 0!==e[t])).forEach((t=>{e[t][s]=a[t]})),s},splitFaceWithLine=(e,t,r,s)=>{const a=(({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a,{vector:o,origin:c},n=p)=>{const i=e.map(resize2),l=r[a].map((e=>i[e])).map((e=>overlapLinePoint({vector:o,origin:c},e,(()=>!0),n))).map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)),d=l.map((e=>r[a][e]));if(l.concat(l.map((e=>e+r[a].length))).map(((e,t,r)=>r[t+1]-e==1)).reduce(((e,t)=>e||t),!1))return;if(d.length>1)return{vertices:d,edges:[]};const _=s[a].map((e=>t[e].map((e=>i[e])))).map((e=>intersectLineLine({vector:o,origin:c},{vector:subtract2(e[1],e[0]),origin:e[0]},includeL,excludeS,n).point)).map(((e,t)=>({coords:e,edge:s[a][t]}))).filter((e=>void 0!==e.coords)).filter((e=>!d.map((r=>t[e.edge].includes(r))).reduce(((e,t)=>e||t),!1)));return _.length+d.length===2?{vertices:d,edges:_}:void 0})(e,t,r,s);if(void 0===a)return;const o=((e,{vertices:t,edges:r})=>{let s;const a=r.map((t=>{const r=splitEdge(e,s?s[t.edge]:t.edge,t.coords);return s=s?mergeNextmaps(s,r.edges.map):r.edges.map,r}));let o;return t.push(...a.map((e=>e.vertex))),a.forEach((e=>{e.edges.remove=o?o[e.edges.remove]:e.edges.remove;const t=invertFlatMap(e.edges.map);o=o?mergeBackmaps(o,t):t})),{vertices:t,edges:{map:s,remove:a.map((e=>e.edges.remove))}}})(e,a);o.edges.new=rebuild_edge(e,t,o.vertices),(({vertices_coords:e,vertices_vertices:t,edges_vertices:r},s)=>{const a=r[s][0],o=r[s][1];t[a]=sortVerticesCounterClockwise({vertices_coords:e},t[a].concat(o),a),t[o]=sortVerticesCounterClockwise({vertices_coords:e},t[o].concat(a),o)})(e,o.edges.new),(({edges_vertices:e,vertices_edges:t,vertices_vertices:r},s)=>{if(!t||!r)return;const a=e[s];a.map((e=>r[e])).map(((e,t)=>e.indexOf(a[(t+1)%a.length]))).forEach(((e,r)=>t[a[r]].splice(e,0,s)))})(e,o.edges.new);const c=build_faces(e,t,o.vertices);((e,t,r)=>{const s={};r.forEach((t=>e.faces_vertices[t].forEach((e=>{s[e]||(s[e]=[]),s[e].push(t)})))),e.faces_vertices[t].forEach((r=>{const a=e.vertices_faces[r].indexOf(t),o=s[r];if(-1===a||!o)throw new Error(l);e.vertices_faces[r].splice(a,1,...o)}))})(e,t,c),((e,t,r,s)=>{const a={};s.forEach((t=>e.faces_edges[t].forEach((e=>{a[e]||(a[e]=[]),a[e].push(t)})))),[...e.faces_edges[t],r].forEach((r=>{const s=a[r],o=[];for(let s=0;s<e.edges_faces[r].length;s+=1)e.edges_faces[r][s]===t&&o.push(s);if(0===o.length||!s)throw new Error(l);o.reverse().forEach((t=>e.edges_faces[r].splice(t,1)));const c=o[o.length-1];e.edges_faces[r].splice(c,0,...s)}))})(e,t,o.edges.new,c),(({faces_vertices:e,faces_faces:t},r,s)=>{const a=t[r],o=s.map((t=>e[t])),c=a.map((t=>{if(null==t)return;const r=e[t],a=[0,0];for(let e=0;e<o.length;e+=1){let t=0;for(let s=0;s<r.length;s+=1)-1!==o[e].indexOf(r[s])&&(t+=1);a[e]=t}return a[0]>=2?s[0]:a[1]>=2?s[1]:void 0}));s.forEach(((e,r,a)=>{t[e]=[a[(r+1)%s.length]]})),a.forEach(((e,s)=>{if(null!=e)for(let a=0;a<t[e].length;a+=1)t[e][a]===r&&(t[e][a]=c[s],t[c[s]].push(e))}))})(e,t,c);const n=remove(e,\"faces\",[t]);c.forEach(((e,t)=>{c[t]=n[c[t]]})),n.splice(-2);const i=n.slice();return i[t]=c,o.faces={map:i,new:c,remove:t},o},buildFacesIfNeeded=(e,t)=>{const r={faces_vertices:!1,faces_edges:!1};return e.faces_vertices&&e.faces_vertices.length||e.faces_edges&&e.faces_edges.length||!t||!e.vertices_coords?(e.faces_vertices||e.faces_edges?e.faces_vertices&&!e.faces_edges?(e.faces_edges=makeFacesEdgesFromVertices(e),r.faces_edges=!0):e.faces_edges&&!e.faces_vertices&&(e.faces_vertices=makeFacesVerticesFromEdges(e),r.faces_vertices=!0):(e.faces_vertices=[],e.faces_edges=[]),r):((e=>{const{faces_vertices:t,faces_edges:r}=makePlanarFaces(e);e.faces_vertices=t,e.faces_edges=r})(e),r.faces_vertices=!0,r.faces_edges=!0,r)},populate=(e,t={})=>{if(\"object\"!=typeof e)return e;if(!e.edges_vertices)return e;const r={vertices_vertices:!1,faces_vertices:!1,faces_edges:!1};e.vertices_vertices&&!e.vertices_edges?e.vertices_edges=makeVerticesEdges(e):e.vertices_edges&&e.vertices_vertices||(e.vertices_edges=makeVerticesEdgesUnsorted(e),e.vertices_vertices=makeVerticesVertices(e),e.vertices_edges=makeVerticesEdges(e),r.vertices_vertices=!0),(e=>{if(e.edges_vertices){if(e.edges_assignment||(e.edges_assignment=[]),e.edges_foldAngle||(e.edges_foldAngle=[]),e.edges_assignment.length>e.edges_foldAngle.length)for(let t=e.edges_foldAngle.length;t<e.edges_assignment.length;t+=1)e.edges_foldAngle[t]=edgeAssignmentToFoldAngle(e.edges_assignment[t]);if(e.edges_foldAngle.length>e.edges_assignment.length)for(let t=e.edges_assignment.length;t<e.edges_foldAngle.length;t+=1)e.edges_assignment[t]=edgeFoldAngleToAssignment(e.edges_foldAngle[t]);for(let t=e.edges_assignment.length;t<e.edges_vertices.length;t+=1)e.edges_assignment[t]=\"U\",e.edges_foldAngle[t]=0}})(e);const s=\"object\"==typeof t&&t.faces;return Object.assign(r,buildFacesIfNeeded(e,s)),(!e.vertices_faces||r.vertices_vertices||r.faces_vertices)&&(e.vertices_faces=makeVerticesFaces(e)),e.edges_faces&&!r.faces_edges||(e.edges_faces=makeEdgesFacesUnsorted(e)),e.faces_faces&&!r.faces_vertices||(e.faces_faces=makeFacesFaces(e)),e};var rs=Object.freeze({__proto__:null,populate:populate});const make_face_side=(e,t,r,s)=>{const a=subtract2(r,t),o=cross2(e,a);return s?o>0:o<0},make_face_center=(e,t)=>e.faces_vertices[t]?e.faces_vertices[t].map((t=>e.vertices_coords[t])).reduce(((e,t)=>[e[0]+t[0],e[1]+t[1]]),[0,0]).map((r=>r/e.faces_vertices[t].length)):[0,0],ss={F:!0,f:!0,U:!0,u:!0},as={M:\"V\",m:\"V\",V:\"M\",v:\"M\"},face_snapshot=(e,t)=>({center:e.faces_center[t],matrix:e.faces_matrix2[t],winding:e.faces_winding[t],crease:e.faces_crease[t],side:e.faces_side[t],layer:e.faces_layer[t]}),getVerticesCollinearToLine=({vertices_coords:e},{vector:t,origin:r},s=p)=>{const a=normalize2(t);return e.map((e=>{const t=subtract2(e,r),o=magnitude2(t);if(Math.abs(o)<s)return!0;const c=scale2$1(t,1/o),n=Math.abs(dot2(c,a));return Math.abs(1-n)<s})).map(((e,t)=>({a:e,i:t}))).filter((e=>e.a)).map((e=>e.i))},getEdgesCollinearToLine=({vertices_coords:e,edges_vertices:t,vertices_edges:r},{vector:s,origin:a},o=p)=>{r||(r=makeVerticesEdgesUnsorted({edges_vertices:t}));const c=getVerticesCollinearToLine({vertices_coords:e},{vector:s,origin:a},o),n=t.map((()=>0));return c.forEach((e=>r[e].forEach((e=>{n[e]+=1})))),n.map(((e,t)=>({count:e,i:t}))).filter((e=>2===e.count)).map((e=>e.i))};var os=Object.freeze({__proto__:null,flatFold:(e,{vector:t,origin:r},s=\"V\",a=p)=>{const o=as[c=s]||c;var c;populate(e),e.faces_layer||(e.faces_layer=Array(e.faces_vertices.length).fill(0)),e.faces_center=e.faces_vertices.map(((t,r)=>make_face_center(e,r))),e.faces_matrix2||(e.faces_matrix2=makeFacesMatrix2(e,[faceContainingPoint(e,r,t)])),e.faces_winding=makeFacesWindingFromMatrix2(e.faces_matrix2),e.faces_crease=e.faces_matrix2.map(invertMatrix2).map((e=>multiplyMatrix2Line2(e,{vector:t,origin:r}))),e.faces_side=e.faces_vertices.map(((t,r)=>make_face_side(e.faces_crease[r].vector,e.faces_crease[r].origin,e.faces_center[r],e.faces_winding[r])));const n=makeVerticesCoordsFoldedFromMatrix2(e,e.faces_matrix2),i=getEdgesCollinearToLine({vertices_coords:n,edges_vertices:e.edges_vertices},{vector:t,origin:r},a).filter((t=>ss[e.edges_assignment[t]]));i.map((t=>e.edges_faces[t].find((e=>null!=e)))).map((t=>e.faces_winding[t])).map((e=>e?s:o)).forEach(((t,r)=>{e.edges_assignment[i[r]]=t,e.edges_foldAngle[i[r]]=edgeAssignmentToFoldAngle(t)}));const l=face_snapshot(e,0),d=e.faces_vertices.map(((e,t)=>t)).reverse().map((t=>{const r=face_snapshot(e,t),c=splitFaceWithLine(e,t,r.crease,a);if(void 0===c)return;e.edges_assignment[c.edges.new]=r.winding?s:o,e.edges_foldAngle[c.edges.new]=edgeAssignmentToFoldAngle(e.edges_assignment[c.edges.new]);return c.faces.map[c.faces.remove].forEach((t=>{e.faces_center[t]=make_face_center(e,t),e.faces_side[t]=make_face_side(r.crease.vector,r.crease.origin,e.faces_center[t],r.winding),e.faces_layer[t]=r.layer})),c})).filter((e=>void 0!==e)),_=mergeNextmaps(...d.map((e=>e.faces.map))),m=mergeNextmaps(...d.map((e=>e.edges.map)).filter((e=>void 0!==e))),g=d.map((e=>e.faces.remove)).reverse();e.faces_layer=((e,t)=>{const r=[],s=e.map(((e,t)=>t)),a=s.filter((e=>t[e])),o=s.filter((e=>!t[e]));return o.sort(((t,r)=>e[t]-e[r])).forEach(((e,t)=>{r[e]=t})),a.sort(((t,r)=>e[r]-e[t])).forEach(((e,t)=>{r[e]=o.length+t})),r})(e.faces_layer,e.faces_side);const v=_&&_[0]&&2===_[0].length,u=v?_[0].filter((t=>e.faces_side[t])).shift():0;let h=l.matrix;return s!==o&&(h=v||e.faces_side[0]?multiplyMatrices2(l.matrix,makeMatrix2Reflect(l.crease.vector,l.crease.origin)):l.matrix),e.faces_matrix2=makeFacesMatrix2(e,[u]).map((e=>multiplyMatrices2(h,e))),delete e.faces_center,delete e.faces_winding,delete e.faces_crease,delete e.faces_side,{faces:{map:_,remove:g},edges:{map:m}}},getEdgesCollinearToLine:getEdgesCollinearToLine,getVerticesCollinearToLine:getVerticesCollinearToLine});const intersectLineVertices=({vertices_coords:e},{vector:t,origin:r},s=includeL,a=p)=>{const o=magSquared(t),c=Math.sqrt(o);return c<a?Array(e.length).fill(void 0):e.map((e=>subtract2(e,r))).map((e=>{const r=dot2(e,t)/o;return Math.abs(cross2(e,t))<a&&s(r,a/c)?r:void 0}))},intersectLineVerticesEdges=({vertices_coords:e,edges_vertices:t},{vector:r,origin:s},a=includeL,o=p)=>{if(!e)return{vertices:[],edges:[]};const c=intersectLineVertices({vertices_coords:e},{vector:r,origin:s},a,o);if(!t)return{vertices:c,edges:[]};const n=t.map((e=>e.map((e=>void 0!==c[e]?e:void 0)).filter((e=>void 0!==e)))),i=t.map((t=>t.map((t=>e[t])))).map((([e,t],o)=>0===n[o].length?intersectLineLine({vector:r,origin:s},pointsToLine2(e,t),a,includeS):void 0)).map((e=>void 0!==e&&e.point?e:void 0));return{vertices:c,edges:i}},intersectLine=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},{vector:a,origin:o},c=includeL,n=p)=>{const{vertices:i,edges:l}=intersectLineVerticesEdges({vertices_coords:e,edges_vertices:t},{vector:a,origin:o},c,n);if(!r)return{vertices:i,edges:l,faces:[]};s||(s=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:r}));const d=s.map((e=>e.map((e=>l[e]?{...l[e],edge:e}:void 0)).filter((e=>void 0!==e)))),_=r.map((e=>e.map((e=>void 0!==i[e]?{a:i[e],vertex:e}:void 0)).filter((e=>void 0!==e)))),m=r.map(((e,t)=>[..._[t],...d[t]])),epsilonEqual=(e,t)=>Math.abs(e.a-t.a)<2*n,g=m.map((e=>e.sort(((e,t)=>e.a-t.a)))).map((e=>clusterSortedGeneric(e,epsilonEqual).map((t=>t.map((t=>e[t])))))).map((e=>e.map((e=>e[0]))));return{vertices:i,edges:l,faces:g}},intersectLineAndPoints=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},{vector:a,origin:o},c=includeL,n=[],i=p)=>{const{vertices:l,edges:d,faces:_}=intersectLine({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},{vector:a,origin:o},c,i);if(!e||!r)return{vertices:l,edges:d,faces:[]};const m=e.map(resize2),g=n.length?_.map(((e,t)=>{const s=r[t].map((e=>m[e]));return n.map((e=>({...overlapConvexPolygonPoint(s,e,exclude,i),point:e}))).filter((e=>e.overlap))})):_.map((()=>[]));return{vertices:l,edges:d,faces:_.map(((e,t)=>({edges:e.map((e=>\"edge\"in e&&\"a\"in e&&\"b\"in e&&\"point\"in e?e:void 0)).filter((e=>void 0!==e)),vertices:e.map((e=>\"vertex\"in e&&\"a\"in e?e:void 0)).filter((e=>void 0!==e)),points:g[t]})))}},filterCollinearFacesData=({edges_vertices:e},{vertices:t,faces:r})=>{const s=[];e.map((e=>void 0!==t[e[0]]&&void 0!==t[e[1]])).map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)).map((t=>e[t])).forEach((e=>s.push(e)));const a=r.map((e=>e.vertices.map((({vertex:e})=>e)))),o=[];a.forEach(((e,t)=>{o[t]={}})),a.forEach(((e,t)=>e.forEach((e=>{o[t][e]=!0})))),r.forEach(((e,t)=>{const a={};s.filter((e=>o[t][e[0]]&&o[t][e[1]])).forEach((e=>{a[e[0]]=!0,a[e[1]]=!0})),r[t].vertices=e.vertices.filter((e=>!a[e.vertex]))}))};var cs=Object.freeze({__proto__:null,filterCollinearFacesData:filterCollinearFacesData,intersectLine:intersectLine,intersectLineAndPoints:intersectLineAndPoints,intersectLineVertices:intersectLineVertices,intersectLineVerticesEdges:intersectLineVerticesEdges});const updateVerticesVertices=({vertices_vertices:e,faces_vertices:t},r,s)=>{if(!e)return;s.map((s=>((e,t,r)=>{const s=e.indexOf(r);if(-1===s)return-1;const a=e[(s+e.length-1)%e.length],o=e[(s+1)%e.length],c=t.indexOf(a);return-1===c||t[(c+t.length-1)%t.length]!==o?-1:c})(t[r],e[s],s))).forEach(((t,r)=>{const a=s[(r+1)%s.length];-1!==t?e[s[r]].splice(t,0,a):e[s[r]].push(a)}))},updateVerticesEdges=({vertices_edges:e,vertices_vertices:t},r,s)=>{if(!e)return;if(!t)return void r.forEach((t=>e[t].push(s)));const a=r.map(((e,r,s)=>t[e].indexOf(s[(r+1)%s.length])));if(a.some((e=>-1===e)))throw new Error(`splitFace() vertices_edges ${r.join(\", \")}`);a.forEach(((t,a)=>{e[r[a]].splice(t,0,s)}))},updateFacesEdges=({edges_vertices:e,faces_vertices:t,faces_edges:r},s,a,o)=>{if(!r)return;const c=[...r[s],o],n=makeVerticesToEdge({edges_vertices:e},c),i=a.map((e=>t[e].map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>e.join(\" \"))).map((e=>n[e]))));if(i.flat().some((e=>void 0===e)))throw new Error(`splitFace() faces_edges ${s}`);a.forEach(((e,t)=>{r[e]=i[t]}))},splitFaceWithLeafEdge=(e,t,r,s,a=\"U\",o=0)=>{const c=[r,s],n=addEdge(e,c,[t,t],a,o);return e.vertices_faces&&(e.vertices_faces[s]=Array.from(new Set([...e.vertices_faces[s],t]))),(({faces_vertices:e},t,r,s)=>{e&&(e[t]=((e,t,r)=>{const s=[...e],a=e[t];return s.splice(t,0,a,r),s})(e[t],e[t].indexOf(r),s))})(e,t,r,s),updateFacesEdges(e,t,[t],n),updateVerticesVertices(e,t,c),updateVerticesEdges(e,c,n),{edge:n,faces:{}}},splitFaceWithEdge=(e,t,r,s=\"U\",a=0)=>{if(!e.vertices_coords||!e.edges_vertices||!e.faces_vertices)return{};if(2!==r.length)return{};if(o=e.faces_vertices[t],Array.from(new Set(o)).length!==o.length)return{};var o;if((({faces_vertices:e},t,r)=>e[t].map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((([e,t])=>e===r[0]&&t===r[1]||e===r[1]&&t===r[0])).reduce(((e,t)=>e||t),!1))(e,t,r))return{};const c=addEdge(e,r,[t,t],s,a),n=[0,1].map((t=>e.faces_vertices.length+t));(({faces_vertices:e},t,r)=>{const[s,a]=r.map((r=>e[t].indexOf(r)));splitCircularArray$1(e[t],[s,a]).map((t=>e.push(t)))})(e,t,r),updateFacesEdges(e,t,n,c),updateVerticesVertices(e,t,r),updateVerticesEdges(e,r,c),(({vertices_vertices:e,vertices_edges:t,vertices_faces:r,edges_vertices:s,faces_vertices:a},o,c)=>{if(!r||!a)return;const n=uniqueElements(c.flatMap((e=>a[e]))),i=uniqueElements([...c,...n.flatMap((e=>r[e]))]).filter((e=>e!==o)).filter((e=>null!=e)),l=makeVerticesToFace({faces_vertices:a},i);n.map((r=>makeVerticesFacesForVertex({vertices_vertices:e,vertices_edges:t,edges_vertices:s},r,l))).forEach(((e,t)=>{r[n[t]]=e||[]}))})(e,t,n),(({edges_vertices:e,faces_vertices:t,edges_faces:r,faces_edges:s},a,o,c)=>{if(!r)return;if(!s)return r.forEach(((e,t)=>delete r[t])),void Object.assign(r,makeEdgesFacesUnsorted({edges_vertices:e,faces_vertices:t,faces_edges:s}));const n={};[...o,a].forEach((e=>{n[e]=!0}));const i=Array.from(new Set(o.flatMap((e=>s[e])))),l=[];i.forEach((e=>{l[e]=r[e].filter((e=>!n[e]))}));const d=[];i.forEach((e=>{d[e]=[]})),o.forEach((e=>s[e].forEach((t=>d[t].push(e))))),i.forEach((e=>{r[e]=Array.from(new Set([...d[e],...l[e]]))})),r[c]=[...o]})(e,t,n,c),(({edges_vertices:e,edges_faces:t,faces_vertices:r,faces_edges:s,faces_faces:a},o,c)=>{if(!a)return;const n=uniqueElements([...a[o],...c]).filter((e=>null!=e));if(t&&s)n.forEach((e=>{a[e]=s[e].map((r=>t[r].find((t=>t!==e))))}));else if(e&&r){const e=makeVerticesToFace({faces_vertices:r},n);n.forEach((t=>{a[t]=r[t].map(((e,t,r)=>[r[(t+1)%r.length],e])).map((e=>e.join(\" \"))).map((t=>e[t]))}))}})(e,t,n),(({faceOrders:e},t,r)=>{if(!e)return;const s=r[0],a=r.slice(1),o=[];e.forEach((([r,c,n],i)=>{if(r===t){e[i]=[s,c,n];const t=a.map((e=>[e,c,n]));o.push(...t)}if(c===t){e[i]=[r,s,n];const t=a.map((e=>[r,e,n]));o.push(...t)}})),e.push(...o)})(e,t,n);const i=remove(e,\"faces\",[t]);n.forEach(((e,t)=>{n[t]=i[n[t]]}));const l=i.slice();return l.splice(-2),l[t]=n,{edge:c,faces:{map:l,new:n,remove:t}}},splitFace=(e,t,r,s=\"U\",a=0)=>{if(!e.vertices_coords||!e.edges_vertices||!e.faces_vertices)return{};if(2!==r.length)return{};const o=r.map((r=>({vertex:r,index:e.faces_vertices[t].indexOf(r)})));switch(o.filter((({index:e})=>-1!==e)).length){case 2:return splitFaceWithEdge(e,t,r,s,a);case 1:return splitFaceWithLeafEdge(e,t,o.filter((({index:e})=>-1!==e)).shift().vertex,o.filter((({index:e})=>-1===e)).shift().vertex,s,a);default:return{edge:addIsolatedEdge(e,r,s,a),faces:{}}}};var ns=Object.freeze({__proto__:null,splitFace:splitFace,splitFaceWithEdge:splitFaceWithEdge,splitFaceWithLeafEdge:splitFaceWithLeafEdge});const splitGraphWithLineAndPoints=(e,{vector:t,origin:r},s=includeL,a=[],o=p)=>{if(!e.vertices_coords||!e.edges_vertices)return{};let c=e.edges_vertices.map(((e,t)=>[t])),n=e.faces_vertices.map(((e,t)=>[t]));const i=[],l=[],d=intersectLineAndPoints(e,{vector:t,origin:r},s,a,o);filterCollinearFacesData(e,d);const _={};d.edges.map(((e,t)=>e?{...e,edge:t}:void 0)).filter((e=>void 0!==e)).forEach((({a:t,b:r,point:s,edge:a})=>{const o=c[a][0],[n,l]=e.edges_vertices[o],d=[n,l],{vertex:m,edges:{map:g}}=splitEdge(e,o,s);i[m]={a:t,b:r,vertices:d,edge:a,point:s},c=mergeNextmaps(c,g),_[a]=m}));const m={};return d.faces.map((({vertices:e,edges:t,points:r},s)=>({vertices:e,edges:t,points:r,face:s}))).filter((({vertices:e,edges:t,points:r})=>2===((...e)=>e.map((e=>e.length)).reduce(((e,t)=>e+t),0))(e,t,r))).forEach((({vertices:t,edges:r,points:s,face:a})=>{const o=n[a][0],c=r.map((({edge:e})=>_[e])),d=addVertices(e,s.map((({point:e})=>e))),g=t.map((({vertex:e})=>e)).concat(c).concat(d),v=[g[0],g[1]],{edge:p,faces:{map:u}}=splitFace(e,o,v);l[p]={face:a,faces:void 0},d.forEach(((e,t)=>{i[e]={...s[t],face:a,faces:void 0}})),n=void 0===u?n:mergeNextmaps(n,u),m[a]=p})),i.forEach((({face:e},t)=>{void 0!==e&&(i[t].faces=n[e])})),l.forEach((({face:e},t)=>{void 0!==e&&(l[t].faces=n[e])})),{vertices:{intersect:d.vertices,source:i},edges:{intersect:d.edges,new:Object.values(m),map:c,source:l},faces:{intersect:d.faces,map:n}}};var is=Object.freeze({__proto__:null,splitGraphWithLine:(e,t,r=p)=>splitGraphWithLineAndPoints(e,t,includeL,[],r),splitGraphWithLineAndPoints:splitGraphWithLineAndPoints,splitGraphWithRay:(e,t,r=p)=>splitGraphWithLineAndPoints(e,t,A,[t.origin],r),splitGraphWithSegment:(e,t,r=p)=>splitGraphWithLineAndPoints(e,pointsToLine2(t[0],t[1]),includeS,t,r)});const trilateration2=(e,t)=>{if(void 0===e[0]||void 0===e[1]||void 0===e[2])return;const r=scale2$1(subtract2(e[1],e[0]),1/distance2(e[1],e[0])),s=dot2(r,subtract2(e[2],e[0])),a=scale2$1(r,s),o=subtract2(subtract2(e[2],e[0]),a),c=scale2$1(o,1/magnitude2(o)),n=distance2(e[1],e[0]),i=dot2(c,subtract2(e[2],e[0])),l=(t[0]**2-t[1]**2+n**2)/(2*n),d=(t[0]**2-t[2]**2+s**2+i**2)/(2*i)-s*l/i;return add2(add2(e[0],scale2$1(r,l)),scale2$1(c,d))};var ls=Object.freeze({__proto__:null,circumcircle:(e,t,r)=>{const s=t[0]-e[0],a=t[1]-e[1],o=r[0]-e[0],c=r[1]-e[1],n=s*(e[0]+t[0])+a*(e[1]+t[1]),i=o*(e[0]+r[0])+c*(e[1]+r[1]),l=2*(s*(r[1]-t[1])-a*(r[0]-t[0]));if(Math.abs(l)<p){const s=Math.min(e[0],t[0],r[0]),a=Math.min(e[1],t[1],r[1]),o=.5*(Math.max(e[0],t[0],r[0])-s),c=.5*(Math.max(e[1],t[1],r[1])-a);return{origin:[s+o,a+c],radius:Math.sqrt(o*o+c*c)}}const d=[(c*n-a*i)/l,(s*i-o*n)/l],_=d[0]-e[0],m=d[1]-e[1];return{origin:d,radius:Math.sqrt(_*_+m*m)}},trilateration2:trilateration2,trilateration3:(e,t)=>{if(void 0===e[0]||void 0===e[1]||void 0===e[2])return;const r=scale3$1(subtract3(e[1],e[0]),1/distance3(e[1],e[0])),s=dot3(r,subtract3(e[2],e[0])),a=scale3$1(r,s),o=subtract3(subtract3(e[2],e[0]),a),c=scale3$1(o,1/magnitude3(o)),n=distance3(e[1],e[0]),i=dot3(c,subtract3(e[2],e[0])),l=(t[0]**2-t[1]**2+n**2)/(2*n),d=(t[0]**2-t[2]**2+s**2+i**2)/(2*i)-s*l/i;return add3(add3(e[0],scale3$1(r,l)),scale3$1(c,d))}});const transferPointInFaceBetweenGraphs=(e,t,r,s)=>{const a=t.faces_vertices[r].filter((r=>e.vertices_coords[r]&&t.vertices_coords[r])),o=a.map(((e,t,r)=>[r[(t+r.length-1)%r.length],e,r[(t+1)%r.length]])).map((r=>{const[s,a,o]=r.map((t=>e.vertices_coords[t])),[c,n,i]=r.map((e=>t.vertices_coords[e]));return[s,a,o,c,n,i]})).map((([e,t,r,s,a,o])=>collinearPoints(e,t,r)||collinearPoints(s,a,o))),c=a.filter(((e,t)=>!o[t]));if(c.length<3)return;const[n,i,l]=c,d=[resize2(t.vertices_coords[n]),resize2(t.vertices_coords[i]),resize2(t.vertices_coords[l])],_=[distance2(e.vertices_coords[n],s),distance2(e.vertices_coords[i],s),distance2(e.vertices_coords[l],s)];return trilateration2(d,_)};var fs=Object.freeze({__proto__:null,transferPointInFaceBetweenGraphs:transferPointInFaceBetweenGraphs});const makeNewFlatFoldFaceOrders=({edges_faces:e,edges_assignment:t,edges_foldAngle:r},s)=>{const a=s.filter((t=>2===e[t].length)),o=a.map((t=>e[t]));if(t){const e=a.map((e=>t[e]));return o.map((([t,r],s)=>((e,t,r)=>{switch(r){case\"V\":case\"v\":return[e,t,1];case\"M\":case\"m\":return[e,t,-1];default:return}})(t,r,e[s]))).filter((e=>void 0!==e))}if(r){const e=a.map((e=>r[e]));return o.map((([t,r],s)=>((e,t,r)=>epsilonEqual(r,180)?[e,t,1]:epsilonEqual(r,-180)?[e,t,-1]:void 0)(t,r,e[s]))).filter((e=>void 0!==e))}return[]},updateFaceOrders=(e,t,r,s,a,o,c)=>{const n=edgeFoldAngleIsFlatFolded(s);if(!e.faceOrders&&n&&(e.faceOrders=[]),n){const t=makeNewFlatFoldFaceOrders(e,o);e.faceOrders=e.faceOrders.concat(t)}if(e.faceOrders){const o=(({vertices_coords:e,faces_vertices:t,faceOrders:r},s,a)=>{if(!r)return[];const o=invertFlatMap(a),c=t.map((t=>t.map((t=>e[t])))).map((e=>e.map(resize2))).map((e=>average2(...e))).map((e=>cross2(subtract2(e,s.origin),s.vector))).map(Math.sign);return r.map((([e,t],r)=>void 0===o[e]&&void 0===o[t]||!(1===c[e]&&-1===c[t]||-1===c[e]&&1===c[t])?void 0:r)).filter((e=>void 0!==e))})(t,r,c);if(n)(({faceOrders:e},t,r,s)=>{const a={true:1,false:-1},o={true:-1,false:1};t.forEach((t=>{const[c,n]=e[t],i=r>0?a[s[n]]:o[s[n]];e[t]=[c,n,i]}))})(e,o,s,a);else{const t={};o.forEach((e=>{t[e]=!0})),e.faceOrders=e.faceOrders.filter(((e,r)=>!t[r]))}}},foldGraph=(e,{vector:t,origin:r},s=includeL,a=[],o=\"V\",c=void 0,n=void 0,i=p)=>{if(void 0!==c&&!e.edges_foldAngle&&e.edges_assignment&&(e.edges_foldAngle=makeEdgesFoldAngle(e)),e.edges_faces||(e.edges_faces=makeEdgesFacesUnsorted(e)),void 0===c&&(c=Y[o]||0),void 0===n){const s=faceContainingPoint(e,r,t);n=makeVerticesCoordsFolded(e,[s])}const l=invertAssignment(o),d=0===c?0:-c,_=br(e.vertices_coords);Object.assign(e,{vertices_coords:br(n)});const m=splitGraphWithLineAndPoints(e,{vector:t,origin:r},s,a,i),g=Array.from(new Set(m.edges.new.flatMap((t=>e.edges_faces[t])))),v=makeFacesWinding(e),u=br(e.vertices_coords);Object.assign(e,{vertices_coords:_});const h={...e,vertices_coords:u},b=m.vertices.source.map(((e,t)=>({...e,vertex:t})));b.map((e=>\"point\"in e&&\"face\"in e&&\"faces\"in e&&\"vertex\"in e?e:void 0)).filter((e=>void 0!==e)).forEach((({point:t,faces:r,vertex:s})=>{e.vertices_coords[s]=transferPointInFaceBetweenGraphs(h,e,r[0],t)})),b.map((e=>\"vertices\"in e&&\"vertex\"in e&&\"b\"in e?e:void 0)).filter((e=>void 0!==e)).forEach((({b:t,vertices:r,vertex:s})=>{e.vertices_coords[s]=((e,t)=>{const r=pointsToLine2(e[0],e[1]);return add2(r.origin,scale2$1(r.vector,t))})(r.map((t=>e.vertices_coords[t])).map(resize2),t)}));const y=m.edges.source.map((({faces:e})=>({assign:v[e[0]]?o:l,angle:v[e[0]]?c:d})));e.edges_assignment&&y.forEach((({assign:t},r)=>{e.edges_assignment[r]=t})),e.edges_foldAngle&&y.forEach((({angle:t},r)=>{e.edges_foldAngle[r]=t}));const E=(({edges_vertices:e,edges_faces:t,edges_assignment:r,edges_foldAngle:s},{assignment:a,foldAngle:o,oppositeAssignment:c,oppositeFoldAngle:n},i,l)=>{const d=l.vertices.intersect.map((e=>void 0!==e)),_=e.map((e=>d[e[0]]&&d[e[1]])).map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)).map((e=>({edge:e,faces:t[e].filter((e=>void 0!==e))}))).filter((({faces:e})=>2===e.length)).filter((({faces:[e,t]})=>i[e]===i[t]));return _.forEach((({edge:e,faces:t})=>{const l=t.map((e=>i[e])).shift();r[e]=l?a:c,s[e]=l?o:n})),_.map((({edge:e})=>e))})(e,{assignment:o,foldAngle:c,oppositeAssignment:l,oppositeFoldAngle:d},v,m);return updateFaceOrders(e,{...e,vertices_coords:u},{vector:t,origin:r},c,v,[...m.edges.new,...E],g),{edges:{map:m.edges.map,new:m.edges.new,reassigned:E},faces:{map:m.faces.map,new:g}}};var ds=Object.freeze({__proto__:null,foldGraph:foldGraph,foldLine:(e,t,r=\"V\",s=void 0,a=void 0,o=p)=>foldGraph(e,t,includeL,[],r,s,a,o),foldRay:(e,t,r=\"V\",s=void 0,a=void 0,o=p)=>foldGraph(e,t,A,[t.origin],r,s,a,o),foldSegment:(e,t,r=\"V\",s=void 0,a=void 0,o=p)=>foldGraph(e,pointsToLine2(t[0],t[1]),includeS,t,r,s,a,o)});var _s=Object.freeze({__proto__:null,foldGraphIntoSegments:({vertices_coords:e,edges_vertices:t,edges_foldAngle:r,edges_assignment:s,faces_vertices:a,faces_edges:o,faces_faces:c},{vector:n,origin:i},l=\"V\",d=p)=>{o||(o=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:a}));const _=faceContainingPoint({vertices_coords:e,faces_vertices:a},i,n),m=invertAssignment(l),g=makeVerticesCoordsFlatFolded({vertices_coords:e,edges_vertices:t,edges_foldAngle:r,edges_assignment:s,faces_vertices:a,faces_faces:c},[_]),v=edgesToLines2({vertices_coords:e,edges_vertices:t}),u=makeFacesWinding({vertices_coords:g,faces_vertices:a});u[_]||u.forEach(((e,t)=>{u[t]=!e}));const{faces:h}=intersectLine({vertices_coords:g,edges_vertices:t,faces_vertices:a,faces_edges:o},{vector:n,origin:i},includeL,d);h.forEach(((e,t)=>{2!==e.length&&delete h[t]}));const remapPoint=({vertex:t,edge:r,b:s})=>void 0!==t?resize2(e[t]):add2(scale2$1(v[r].vector,s),v[r].origin);return h.map(((e,t)=>({intersections:e,assignment:u[t]?l:m,points:e.map(remapPoint)})))}});const splitLineToSegments=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},{vector:a,origin:o},c=includeL,n=[],i=p)=>{const{vertices:l,edges:d,faces:_}=intersectLineAndPoints({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},{vector:a,origin:o},c,n,i);_.map((e=>[\"vertices\",\"edges\",\"points\"].map((t=>e[t].length)).reduce(((e,t)=>e+t),0))).map(((e,t)=>2!==e?t:void 0)).filter((e=>void 0!==e)).forEach((e=>delete _[e]));const m={vertices:[],edges_face:_.map(((e,t)=>t)).filter((e=>void 0!==e))},g={},v={};return m.edges_vertices=_.map(((t,r)=>{const s=t.vertices.map((({a:t,vertex:r})=>{const s=m.vertices.length;return void 0!==g[r]?g[r]:(m.vertices.push({a:t,vertex:r,point:[...e[r]]}),g[r]=s,s)})),a=t.edges.map((({a:e,b:t,point:r,edge:s})=>{const a=m.vertices.length;return void 0!==v[s]?v[s]:(m.vertices.push({a:e,b:t,point:r,edge:s}),v[s]=a,a)})),o=t.points.map((({point:e,t:t})=>{const s=m.vertices.length;return m.vertices.push({point:e,t:t,face:r}),s}));return s.concat(a).concat(o)})).filter((e=>void 0!==e)),{vertices:l,edges:d,faces:_,segments:m}},splitLineIntoEdges=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a,o=includeL,c=[],n=p)=>{if(!e||!t||!r)return;const{vertices:i,segments:l}=splitLineToSegments({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a,o,c,n),d={...l,vertices_info:l.vertices};delete d.vertices;let _=0;const m=d.vertices_info.map((t=>void 0===t.vertex?e.length+_++:t.vertex)),g=d.edges_vertices.map(((e,r)=>t.length+r));remapKey(d,\"vertices\",m),remapKey(d,\"edges\",g),d.vertices=d.vertices_info,delete d.vertices_info,d.vertices.map(((e,t)=>t)).filter((t=>void 0!==e[t])).forEach((e=>delete d.vertices[e]));const v=i.map((e=>void 0!==e)),u=t.map((e=>v[e[0]]&&v[e[1]]));return{...d,edges_collinear:u}};var ms=Object.freeze({__proto__:null,splitLineIntoEdges:splitLineIntoEdges,splitLineToSegments:splitLineToSegments});const transferPoint=(e,t,{vertex:r,edge:s,face:a,point:o,b:c})=>{if(void 0!==r)return t.vertices_coords[r];if(void 0!==s)return((e,t,r)=>{const s=e.edges_vertices[t].map((t=>e.vertices_coords[t])),a=pointsToLine2(s[0],s[1]);return add2(a.origin,scale2$1(a.vector,r))})(t,s,c);if(void 0!==a)return transferPointInFaceBetweenGraphs(e,t,a,o);throw new Error(\"transferPoint() failed\")};var gs=Object.freeze({__proto__:null,foldGraphIntoSubgraph:(e,t,r,s=includeL,a=[],o=\"V\",c=void 0,n=p)=>{void 0===c&&(c=Y[o]||0);const i=invertAssignment(o),l=0===c?0:-c,d=makeFacesWinding(t),{vertices:_,edges_vertices:m,edges_collinear:g,edges_face:v}=splitLineIntoEdges(t,r,s,a,n),u=_.map((r=>transferPoint(t,e,r))),h=v.map((e=>d[e]?o:i)),b=v.map((e=>d[e]?c:l)),y=e.edges_faces?e.edges_faces:makeEdgesFacesUnsorted(e),E={F:!0,f:!0,U:!0,u:!0};return g.map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)).forEach((e=>{if(!E[h[e]])return;const t=y[e].filter((e=>void 0!==e)).shift(),r=d[t];h[e]=r?o:i,b[e]=r?c:l})),{vertices_coords:u,edges_vertices:m,edges_assignment:h,edges_foldAngle:b}},transferPoint:transferPoint});const noNulls=e=>e.map(((e,t)=>null==e?t:void 0)).filter((e=>void 0!==e)),arraysHaveSameIndices=(e=[])=>{if(e.length<2)return[];const t={};return e[0].forEach(((e,r)=>{t[r]=!0})),Array.from(Array(e.length-1)).map(((t,r)=>e[r+1])).flatMap(((e,r)=>e.map(((e,s)=>t[s]?void 0:[0,r+1,s])).filter((e=>void 0!==e))))},validateReferences=e=>{const t=getAllPrefixes(e).filter((e=>\"file\"!==e&&\"frame\"!==e)),r=getAllSuffixes(e),s=t.map((t=>filterKeysWithPrefix(e,t))),a=s.map((t=>t.map((t=>e[t])).filter((e=>e.constructor===Array)))).map(arraysHaveSameIndices).flatMap(((e,t)=>e.map((e=>[s[t][e[0]],s[t][e[1]],e[2]])))).map((([e,t,r])=>`array indices differ ${e}, ${t} at index ${r}`));let o=[];try{o=((e,t)=>{const r={};return e.forEach((e=>{r[e]=1})),t.forEach((e=>{r[e]=1===r[e]?2:1})),Object.keys(r).filter((e=>2===r[e]))})(t,r).flatMap((r=>{const a=s[t.indexOf(r)],o=e[a[0]];return filterKeysWithSuffix(e,r).flatMap((t=>e[t].flatMap(((e,s)=>e.map(((e,c)=>null==e||void 0!==o[e]?void 0:`${t}[${s}][${c}] references ${r} ${e}, missing in ${a[0]}`)).filter((e=>void 0!==e))))))}))}catch(e){o.push(\"reference validation failed due to bad index access\")}return a.concat(o)},pairwiseReferenceTest=(e,t,r,s)=>{try{const a={};e.forEach(((e,t)=>{a[t]={}})),e.forEach(((e,t)=>e.filter((e=>null!=e)).forEach((e=>{a[t][e]=!0}))));const o={};t.forEach(((e,t)=>{o[t]={}})),t.forEach(((e,t)=>e.filter((e=>null!=e)).forEach((e=>{o[t][e]=!0}))));const c=e.flatMap(((e,t)=>e.filter((e=>null!=e)).map((e=>o[e]&&o[e][t]?void 0:`${s}_${r}[${e}] missing ${t} referenced in ${r}_${s}`)).filter((e=>void 0!==e)))),n=t.flatMap(((e,t)=>e.filter((e=>null!=e)).map((e=>a[e]&&a[e][t]?void 0:`${r}_${s}[${e}] missing ${t} referenced in ${s}_${r}`)).filter((e=>void 0!==e))));return c.concat(n)}catch(e){return[\"pairwise reference validation failed due to bad index access\"]}},reflexiveTest=(e,t)=>pairwiseReferenceTest(e,e,t,t),validateWinding=e=>{const t=(({vertices_vertices:e,vertices_edges:t,vertices_faces:r,edges_vertices:s,faces_vertices:a,faces_edges:o})=>{const c=[],n=s?makeVerticesToEdge({edges_vertices:s}):void 0,i=a?makeVerticesToFace({faces_vertices:a}):void 0,l=o?makeEdgesToFace({faces_edges:o}):void 0;try{if(e&&t&&n){const r=e.flatMap(((e,r)=>e.map((e=>[r,e])).map((e=>e.join(\" \"))).map((e=>n[e])).map(((e,s)=>t[r][s]!==e?[r,s,e,t[r][s]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of vertices_vertices and vertices_edges at [${e}][${t}] do not match (${r} ${s})`));c.push(...r)}if(e&&r&&i){const t=e.flatMap(((e,t)=>e.map((e=>[t,e])).map((e=>e.join(\" \"))).map((e=>i[e])).map(((e,s)=>r[t][s]!=e?[t,s,e,r[t][s]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of vertices_vertices and vertices_faces at [${e}][${t}] do not match (${r} ${s})`));c.push(...t)}if(t&&r&&l){const e=t.flatMap(((e,t)=>e.map(((e,t,r)=>[r[(t+1)%r.length],e])).map((e=>e.join(\" \"))).map((e=>l[e])).map(((e,s)=>r[t][s]!=e?[t,s,e,r[t][s]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of vertices_edges and vertices_faces at [${e}][${t}] do not match (${r} ${s})`));c.push(...e)}}catch(e){c.push(\"vertices winding validation failed due to bad index access\")}return c})(e),r=(({edges_vertices:e,edges_faces:t,faces_vertices:r,faces_edges:s,faces_faces:a})=>{const o=[],c=e?makeVerticesToEdge({edges_vertices:e}):void 0,n=r?makeVerticesToFace({faces_vertices:r}):void 0;try{if(r&&s&&c){const e=r.flatMap(((e,t)=>e.map(((e,t,r)=>[0,1].map((e=>r[(t+e)%r.length])))).map((e=>e.join(\" \"))).map((e=>c[e])).map(((e,r)=>s[t][r]!==e?[t,r,e,s[t][r]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of faces_vertices and faces_edges at [${e}][${t}] do not match (${r} ${s})`));o.push(...e)}if(r&&a&&n){const e=r.flatMap(((e,t)=>e.map(((e,t,r)=>[1,0].map((e=>r[(t+e)%r.length])))).map((e=>e.join(\" \"))).map((e=>n[e])).map(((e,r)=>a[t][r]!=e?[t,r,e,a[t][r]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of faces_vertices and faces_faces at [${e}][${t}] do not match (${r} ${s})`));o.push(...e)}if(s&&a&&t){const e=s.flatMap(((e,r)=>e.map((e=>t[e].filter((e=>e!==r)).shift())).map(((e,t)=>a[r][t]!=e?[r,t,e,a[r][t]]:void 0)))).filter((e=>void 0!==e)).map((([e,t,r,s])=>`windings of faces_edges and faces_faces at [${e}][${t}] do not match (${r} ${s})`));o.push(...e)}}catch(e){o.push(\"faces winding validation failed due to bad index access\")}return o})(e);return t.concat(r)},validateAssignments=e=>{const t=[];return e.edges_assignment&&e.edges_foldAngle&&t.push(...(({edges_assignment:e,edges_foldAngle:t})=>{const r=t.map(Math.sign);return e.map((e=>Y[e])).map(Math.sign).map(((s,a)=>s===r[a]?void 0:`assignment does not match fold angle at ${a}: ${e[a]}, ${t[a]}`)).filter((e=>void 0!==e))})(e)),t},ordersTest=(e,t=\"orders\")=>{const r=e.map((([e,t],r)=>e===t?[e,t,r]:void 0)).filter((e=>void 0!==e)).map((([e,r,s])=>`${t} between the same face ${e}, ${r} at index ${s}`)),s={},a=e.filter((([e,t])=>{const r=e<t?`${e} ${t}`:`${t} ${e}`,a=!0===s[r];return s[r]=!0,a})).map((([e,r])=>`${t} duplicate orders found at indices ${e}, ${r}`));return r.concat(a)};var vs=Object.freeze({__proto__:null,validate:e=>(e=>{const t=[];try{G.graph.filter((t=>e[t])).forEach((r=>t.push(...noNulls(e[r]).map((e=>`${r}[${e}] is undefined or null`)))))}catch(e){t.push(\"validation error: undefined or null array index\")}try{[\"vertices_coords\",\"vertices_vertices\",\"vertices_edges\",\"edges_vertices\",\"faces_vertices\",\"faces_edges\"].filter((t=>e[t])).flatMap((t=>e[t].map(noNulls).flatMap(((e,r)=>e.map((e=>`${t}[${r}][${e}] is undefined or null`)))))).forEach((e=>t.push(e)))}catch(e){t.push(\"validation error: undefined or null array index\")}if(e.vertices_coords)try{t.push(...e.vertices_coords.map(((e,t)=>2!==e.length&&3!==e.length?t:void 0)).filter((e=>void 0!==e)).map((e=>`vertices_coords[${e}] is not length 2 or 3`)))}catch(e){t.push(\"validation error: coordinate value undefined or null\")}if(e.edges_vertices)try{t.push(...e.edges_vertices.map(((e,t)=>2!==e.length?t:void 0)).filter((e=>void 0!==e)).map((e=>`edges_vertices[${e}] is not length 2`)));const r=circularEdges(e);0!==r.length&&t.push(`circular edges_vertices: ${r.join(\", \")}`);const s=duplicateEdges(e);if(0!==s.length){const e=s.map(((e,t)=>`${t}(${e})`)).filter((e=>e)).join(\", \");t.push(`duplicate edges_vertices: ${e}`)}}catch(e){t.push(\"validation error: edge value undefined or null\")}if(e.faces_vertices)try{t.push(...e.faces_vertices.map(((e,t)=>0===e.length?t:void 0)).filter((e=>void 0!==e)).map((e=>`faces_vertices[${e}] contains no vertices`)))}catch(e){t.push(\"validation error: face (faces_vertices) value undefined or null\")}if(e.faces_edges)try{t.push(...e.faces_edges.map(((e,t)=>0===e.length?t:void 0)).filter((e=>void 0!==e)).map((e=>`faces_edges[${e}] contains no edges`)))}catch(e){t.push(\"validation error: face (faces_edges) value undefined or null\")}return t})(e).concat(validateReferences(e)).concat((e=>{const t=[];return e.faces_vertices&&e.vertices_faces&&t.push(...pairwiseReferenceTest(e.faces_vertices,e.vertices_faces,\"faces\",\"vertices\")),e.edges_vertices&&e.vertices_edges&&t.push(...pairwiseReferenceTest(e.edges_vertices,e.vertices_edges,\"edges\",\"vertices\")),e.faces_edges&&e.edges_faces&&t.push(...pairwiseReferenceTest(e.faces_edges,e.edges_faces,\"faces\",\"edges\")),e.vertices_vertices&&t.push(...reflexiveTest(e.vertices_vertices,\"vertices\")),e.faces_faces&&t.push(...reflexiveTest(e.faces_faces,\"faces\")),t})(e)).concat(validateWinding(e)).concat(validateAssignments(e)).concat((e=>{const t=[];return e.faceOrders&&t.push(...ordersTest(e.faceOrders,\"faceOrders\")),e.edgeOrders&&t.push(...ordersTest(e.edgeOrders,\"edgeOrders\")),t})(e))});const clean=(e,t)=>{const r=removeDuplicateVertices(e,t),s=removeCircularEdges(e),a=removeDuplicateEdges(e),o=removeIsolatedVertices(e),c=invertFlatMap(r.map),n=o.remove.map((e=>c[e])),i=invertFlatMap(s.map),l=a.remove.map((e=>i[e]));return{vertices:{map:mergeFlatNextmaps(r.map,o.map),remove:r.remove.concat(n)},edges:{map:mergeFlatNextmaps(s.map,a.map),remove:s.remove.concat(l)}}};var ps=Object.freeze({__proto__:null,clean:clean});const join=(e,t)=>{const r=getDimensionQuick(t),s=getDimensionQuick(e),a={};H.forEach((e=>{const r=filterKeysWithPrefix(t,e).shift();a[e]=void 0!==r?t[r]:[]}));const o={};H.forEach((t=>{o[t]=count(e,t)}));const c={vertices:[],edges:[],faces:[]};H.forEach((e=>a[e].forEach(((t,r)=>{c[e][r]=o[e]++}))));const n=br(t);H.forEach((e=>remapKey(n,e,c[e]))),Object.keys(n).filter((e=>n[e].constructor===Array)).filter((t=>!(t in e))).forEach((t=>{e[t]=[]})),Object.keys(n).filter((e=>n[e].constructor===Array)).forEach((t=>n[t].forEach(((r,s)=>{e[t][s]=r}))));const i={},l={};H.forEach((t=>{const r=filterKeysWithPrefix(e,t).shift();l[t]=void 0!==r?e[r]:[]})),H.forEach((e=>{const t=l[e].map((()=>0));c[e].forEach((e=>{t[e]=1})),i[e]=invertFlatToArrayMap(t)}));return(r!==s?(e.vertices_coords||[]).map(((e,t)=>2===e.length?t:void 0)).filter((e=>void 0!==e)):[]).forEach((t=>{e.vertices_coords[t][2]=0})),i};var us=Object.freeze({__proto__:null,join:join});const makeEdgesVerticesFromFaces=({faces_vertices:e})=>{const t={},r=[];return e.map((e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).forEach((([e,s])=>{t[`${e} ${s}`]||t[`${s} ${e}`]||(t[`${e} ${s}`]=!0,r.push([e,s]))})))),r};var hs=Object.freeze({__proto__:null,makeEdgesVerticesFromFaces:makeEdgesVerticesFromFaces});const bs={B:1,C:2,V:3,M:4,J:5,F:6,U:7};Object.keys(bs).forEach((e=>{bs[e.toLowerCase()]=bs[e]}));const highestPriorityAssignmentIndex=e=>{if(1===e.length)return 0;let t=0;return e.forEach(((r,s)=>{bs[r]<bs[e[t]]&&(t=s)})),t},planarizeCollinearEdges=({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s},a=p)=>{const{lines:o,edges_line:c}=getEdgesLine({vertices_coords:e,edges_vertices:t},a),n=makeVerticesEdgesUnsorted({edges_vertices:t}),i=invertFlatToArrayMap(c),l=i.map((e=>uniqueElements(e.flatMap((e=>t[e]))))).map(((t,r)=>t.map((t=>({v:t,p:dot2(subtract2(e[t],o[r].origin),o[r].vector)}))).sort(((e,t)=>e.p-t.p)))),d=l.map((e=>e.map((({v:e})=>e)))),_=l.map((e=>e.map((({p:e})=>e)))).map(((e,t)=>clusterSortedGeneric(e,epsilonEqual).map((e=>e.map((e=>d[t][e])))))),m=(e=>{const t=[];let r=0;e.map((e=>e.map((e=>{const s=e.map((e=>t[e])).shift(),a=void 0!==s,o=a?s:r;return e.forEach((e=>{t[e]=o})),a?s:r++}))));const s=invertFlatToArrayMap(t).filter((e=>e));return invertArrayToFlatMap(s)})(_),g=invertFlatToArrayMap(m),v=_.map(((e,t)=>{const r={};i[t].forEach((e=>{r[e]=!0}));const s=new Set;return Array.from(Array(e.length-1)).map(((t,r)=>e[r])).map((e=>{const t=uniqueElements(e.flatMap((e=>n[e])).filter((e=>r[e]))),a=t.map((e=>!s.has(e)));return t.forEach(((e,t)=>a[t]?s.add(e):s.delete(e))),Array.from(s)}))})),u=_.map((e=>e.map((e=>m[e[0]])))).flatMap(((e,t)=>Array.from(Array(e.length-1)).map(((r,s)=>v[t][s].length?[e[s],e[s+1]]:void 0)))).filter((e=>void 0!==e)).map((([e,t])=>[e,t])),h=(e=>{const t=[];let r=0;return e.map((e=>e.map((e=>(e.filter((e=>void 0===t[e])).forEach((e=>{t[e]=[]})),e.forEach((e=>t[e].push(r))),e.length?r++:r))))),t})(v),b=g.map((t=>e[t[0]])).map(resize2),y={vertices_coords:b,edges_vertices:u};if(r||s){const e=invertArrayMap(h),t=r?e.map((e=>e.map((e=>r[e])))).map(highestPriorityAssignmentIndex):e.map((()=>0));r&&(y.edges_assignment=t.map(((t,s)=>r[e[s][t]]))),s&&(y.edges_foldAngle=t.map(((t,r)=>s[e[r][t]])))}return{result:y,changes:{vertices:{map:m},edges:{map:h},edges_line:c,lines:o}}};var ys=Object.freeze({__proto__:null,planarizeCollinearEdges:planarizeCollinearEdges});const intersectAllEdges=({vertices_coords:e,vertices_edges:t,edges_vertices:r},s=p)=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:r}));const a=r.map((()=>({})));r.forEach(((e,r)=>e.flatMap((e=>t[e])).forEach((e=>{a[r][e]=!0,a[e][r]=!0}))));const o=edgesToLines2({vertices_coords:e,edges_vertices:r}),c=[];for(let e=0;e<r.length-1;e+=1)for(let t=e+1;t<r.length;t+=1){if(a[e][t])continue;const{a:r,b:n,point:i}=intersectLineLine(o[e],o[t],includeS,includeS,s);if(i){if((epsilonEqual(r,0)||epsilonEqual(r,1))&&(epsilonEqual(n,0)||epsilonEqual(n,1)))continue;c.push({i:e,j:t,a:r,b:n,point:i})}}return c};var Es=Object.freeze({__proto__:null,intersectAllEdges:intersectAllEdges});const planarizeOverlaps=({vertices_coords:e,vertices_edges:t,edges_vertices:r,edges_assignment:s,edges_foldAngle:a},o=p)=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:r}));const c=r.map((()=>[])),n=intersectAllEdges({vertices_coords:e,vertices_edges:t,edges_vertices:r},o);n.filter((({a:e})=>!epsilonEqual(e,0)&&!epsilonEqual(e,1))).forEach((({i:e,a:t})=>c[e].push(t))),n.filter((({b:e})=>!epsilonEqual(e,0)&&!epsilonEqual(e,1))).forEach((({j:e,b:t})=>c[e].push(t))),c.forEach((e=>e.sort(((e,t)=>e-t))));const i=c.map((e=>clusterSortedGeneric(e,epsilonEqual))),average=e=>e.length?e.reduce(((e,t)=>e+t),0)/e.length:0,l=i.map(((e,t)=>e.map((e=>e.map((e=>c[t][e])))).map(average)));let d=0;const _=l.map((e=>Array.from(Array(e.length+1)).map((()=>d++)))),m=invertArrayToFlatMap(_);let g=e.length;const v=l.map((e=>e.map((()=>g++)))).map(((e,t)=>[r[t][0],...e,r[t][1]])).flatMap((e=>Array.from(Array(e.length-1)).map(((t,r)=>[e[r],e[r+1]])))).map((([e,t])=>[e,t])),u=l.flatMap(((t,s)=>{if(!t.length)return[];const a=edgeToLine2({vertices_coords:e,edges_vertices:r},s);return t.map((e=>add2(a.origin,scale2$1(a.vector,e))))})),h={vertices_coords:e.concat(u).map(resize2),edges_vertices:v};s&&(h.edges_assignment=m.map((e=>s[e]))),a&&(h.edges_foldAngle=m.map((e=>a[e])));const b=e.map(((e,t)=>t)),{map:y}=removeDuplicateVertices(h,o),{map:E}=removeCircularEdges(h),M=mergeNextmaps(_,E);return{result:h,changes:{vertices:{map:mergeFlatNextmaps(b,y)},edges:{map:M}}}};var Ms=Object.freeze({__proto__:null,planarizeOverlaps:planarizeOverlaps});const planarizeCollinearVertices=(e,t=p)=>{e.vertices_edges||(e.vertices_edges=makeVerticesEdgesUnsorted(e));const r=e.vertices_edges.map(((e,t)=>2===e.length?t:void 0)).filter((e=>void 0!==e)).filter((r=>isVertexCollinear(e,r,t))).reverse(),s=r.map((t=>(({edges_vertices:e,vertices_edges:t},r)=>{const s=t[r].sort(((e,t)=>e-t)),[a,o]=s.flatMap((t=>e[t])).filter((e=>e!==r));return e[s[0]]=[a,o],e[s[1]]=void 0,[a,o].forEach((e=>{const r=t[e].indexOf(s[1]);-1!==r&&(t[e][r]=s[0])})),s[1]})(e,t)));delete e.vertices_edges;const a=remove(e,\"edges\",s),o=remove(e,\"vertices\",r),{map:c}=removeDuplicateEdges(e),n=mergeNextmaps(a,c);return{result:e,changes:{vertices:{map:o},edges:{map:n}}}};var As=Object.freeze({__proto__:null,planarizeCollinearVertices:planarizeCollinearVertices});const planarizeEdgesVerbose=(e,t=p)=>{const{result:r,changes:{vertices:{map:s},edges:{map:a}}}=planarizeCollinearEdges(e,t),{result:o,changes:{vertices:{map:c},edges:{map:n}}}=planarizeOverlaps(r,t),{result:i,changes:{vertices:{map:l},edges:{map:d}}}=planarizeCollinearVertices(o,t);return{result:i,changes:{vertices:{map:mergeNextmaps(s,c,l)},edges:{map:mergeNextmaps(a,n,d)}}}},planarizeVerbose=(e,t=p)=>{const r=(e=>e.edges_vertices?e:e.faces_vertices?{...e,edges_vertices:makeEdgesVerticesFromFaces(e)}:{})(e),{result:s,changes:{vertices:{map:a},edges:{map:o}}}=planarizeEdgesVerbose(r,t),{faces_vertices:c,faces_edges:n}=makePlanarFaces(s);s.faces_vertices=c,s.faces_edges=n;const i=((e,t,r)=>{if(!e.faces_vertices)return[];const s=t.faces_edges||makeFacesEdgesFromVertices(t),a=makeFacesFaces(t).map((e=>e.filter((e=>null!=e)))),o=makeEdgesFacesUnsorted(t),c=makeEdgesFacesUnsorted(e),n=connectedComponents(a),i=invertFlatToArrayMap(n).map((e=>e.flatMap((e=>s[e])))).map(uniqueElements),l=i.map((e=>e.find((e=>1===o[e].length)))),d=l.map((e=>o[e][0])),_={};o.forEach(((e,t)=>chooseTwoPairs(e).forEach((e=>{_[e.join(\" \")]=t,_[e.reverse().join(\" \")]=t}))));const m=invertArrayMap(r.edges.map),g=[];minimumSpanningTrees(a,d).forEach(((e,t)=>{const r=e.shift();if(!r||!r.length)return;const s=r[0],a=l[t],o=m[a];g[s.index]=new Set(o.flatMap((e=>c[e]))),e.forEach((e=>e.forEach((({index:e,parent:t})=>{const r=_[`${e} ${t}`],s=m[r].flatMap((e=>c[e])),a=new Set(g[t]);s.forEach((e=>a.has(e)?a.delete(e):a.add(e))),g[e]=a}))))}));const v=g.map((e=>Array.from(e)));return invertArrayMap(v)})(r,s,{vertices:{map:a},edges:{map:o}});return((e,t)=>{const r=invertArrayMap(t);e.faces_vertices.map(((e,t)=>t)).filter((e=>void 0===r[e])).forEach((t=>{delete e.faces_vertices[t],delete e.faces_edges[t]}))})(s,i),{result:s,changes:{vertices:{map:a},edges:{map:o},faces:{map:i}}}};var xs=Object.freeze({__proto__:null,planarize:(e,t=p)=>{const{result:r}=planarizeVerbose(e,t);return r},planarizeEdges:({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s},a=p)=>{const{result:o}=planarizeCollinearEdges({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s},a),{result:c}=planarizeOverlaps(o,a),{result:n}=planarizeCollinearVertices(c,a);return n},planarizeEdgesVerbose:planarizeEdgesVerbose,planarizeVerbose:planarizeVerbose});const makeOneSide=(e,t)=>(e.edges_faces.forEach(((r,s)=>{if(2!==r.length)return;const[a,o]=r.map((e=>t[e]));a===o&&e.edges_assignment&&(e.edges_assignment[s]=\"J\"),a===o&&e.edges_foldAngle&&(e.edges_foldAngle[s]=0)})),e),correctFaceWinding=(e,t,r)=>e.faces_vertices.map(((e,t)=>t)).filter((e=>!t[r[e]])).forEach((t=>{e.faces_vertices[t].reverse(),e.faces_edges[t].reverse(),e.faces_edges[t].push(e.faces_edges[t].shift())})),fixCycles=e=>{const{result:t,changes:{faces:{map:r}}}=planarizeVerbose(e);t.edges_faces=makeEdgesFacesUnsorted(t);const s=invertArrayMap(r);!t.edges_assignment&&t.edges_vertices&&(t.edges_assignment=t.edges_vertices.map((()=>\"U\")));const a=makeFacesNormal(e),o=faceOrdersToDirectedEdges({...e,faces_normal:a}),c=makeFacesWinding(e),n=s.map((e=>{const t={};e.forEach((e=>{t[e]=!0}));const r=o.filter((([e,r])=>t[e]&&t[r])),s=topologicalSort(r);return e.filter((e=>-1===s.indexOf(e))).concat(s)})),i=n.map((e=>e[0])),l=n.map((e=>e.slice().reverse()[0])),d=makeOneSide(structuredClone(t),i),_=makeOneSide(t,l);correctFaceWinding(d,c,i),correctFaceWinding(_,c,l);const m=d.faces_vertices.map(((e,t)=>c[i[t]]?[d.faces_vertices.length+t,t,-1]:[d.faces_vertices.length+t,t,1]));return join(d,_),{...d,faceOrders:m,frame_classes:[\"foldedForm\"]}};var Os=Object.freeze({__proto__:null,fixCycles:fixCycles});const explodeFaces=({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s,faces_vertices:a,faces_edges:o})=>{if(!a)return t?br({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s}):e?br({vertices_coords:e}):{};let c=0;const n=a.map((e=>e.map((()=>c++))));if(!e)return{faces_vertices:n};const i=getDimensionQuick({vertices_coords:e}),l=br(a.flatMap((t=>t.map((t=>e[t]))))),d=3===i?l.map(resize3):l.map(resize2);if(!t)return{vertices_coords:d,faces_vertices:n};o||(o=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:a}));let _=0;const m={vertices_coords:d,faces_vertices:n,edges_vertices:o.flatMap((e=>e.map(((e,t,r)=>t===r.length-1?[_,++_-r.length]:[_,++_])))).map((([e,t])=>[e,t]))},g=o.flatMap((e=>e));return r&&(m.edges_assignment=g.map((e=>r[e]))),s&&(m.edges_foldAngle=g.map((e=>s[e]))),m};var js=Object.freeze({__proto__:null,explodeEdges:({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s})=>{if(!t)return e?{vertices_coords:e}:{};let a=0;const o={edges_vertices:t.map((()=>[a++,a++]))};return r&&(o.edges_assignment=r),s&&(o.edges_foldAngle=s),e&&(o.vertices_coords=structuredClone(t.flatMap((t=>t.map((t=>e[t])))))),o},explodeFaces:explodeFaces});const nearestEdge=({vertices_coords:e,edges_vertices:t},r)=>{if(!e||!t)return;const s=2===getDimensionQuick({vertices_coords:e})?(({vertices_coords:e,edges_vertices:t},r)=>t.map((t=>t.map((t=>e[t])))).map((e=>nearestPointOnLine({vector:subtract2(e[1],e[0]),origin:e[0]},r,clampSegment))))({vertices_coords:e,edges_vertices:t},r):(({vertices_coords:e,edges_vertices:t},r)=>t.map((t=>t.map((t=>e[t])))).map((e=>nearestPointOnLine({vector:subtract3(e[1],e[0]),origin:e[0]},r,clampSegment))))({vertices_coords:e,edges_vertices:t},r);return arrayMinimumIndex(s,(e=>distance(e,r)))};var ws=Object.freeze({__proto__:null,nearestEdge:nearestEdge,nearestFace:(e,t)=>{const r=faceContainingPoint(e,t);if(void 0!==r)return r;if(e.edges_faces){const r=nearestEdge(e,t);if(void 0===r)return;const s=e.edges_faces[r];if(1===s.length)return s[0];if(s.length>1){const r=makeFacesCenterQuick({vertices_coords:e.vertices_coords,faces_vertices:s.map((t=>e.faces_vertices[t]))}).map((e=>distance(e,t)));let a=0;for(let e=0;e<r.length;e+=1)r[e]<r[a]&&(a=e);return s[a]}}},nearestVertex:({vertices_coords:e},t)=>{if(!e)return;const r=getDimensionQuick({vertices_coords:e});if(void 0===r)return;const s=resize(r,t),a=e.map(((e,t)=>({d:distance(s,e),i:t}))).sort(((e,t)=>e.d-t.d)).shift();return a?a.i:void 0}});var ks=Object.freeze({__proto__:null,normalize:e=>{const t={vertices:[],edges:[],faces:[]};let r=0,s=0,a=0;return e.vertices_coords.forEach(((e,s)=>{t.vertices[s]=r++})),e.edges_vertices.forEach(((e,r)=>{t.edges[r]=s++})),e.faces_vertices.forEach(((e,r)=>{t.faces[r]=a++})),remapKey(e,\"vertices\",t.vertices),remapKey(e,\"edges\",t.edges),remapKey(e,\"faces\",t.faces),e}});const getFacesFacesOverlap=({vertices_coords:e,faces_vertices:t},r=p)=>{const s=makeFacesPolygon({vertices_coords:e,faces_vertices:t}).map((e=>e.map(resize2))),a=s.map((e=>boundingBox$1(e))),o=t.map((()=>[])),c={},n=[];return sweepFaces({vertices_coords:e,faces_vertices:t},0,r).forEach((({start:e,end:t})=>{e.forEach((e=>{n[e]=!0})),n.forEach(((t,n)=>e.filter((e=>e!==n)).forEach((e=>{const t=n<e?`${n} ${e}`:`${e} ${n}`;c[t]||(c[t]=!0,overlapBoundingBoxes(a[n],a[e],r)&&overlapConvexPolygons(s[n],s[e],r)&&(o[n].push(e),o[e].push(n)))})))),t.forEach((e=>delete n[e]))})),o},getEdgesEdgesCollinearOverlap=({vertices_coords:e,edges_vertices:t},r=p)=>{const{lines:s,edges_line:a}=getEdgesLine({vertices_coords:e,edges_vertices:t},r),o=makeEdgesCoords({vertices_coords:e,edges_vertices:t}).map(((e,t)=>e.map((e=>dot(s[a[t]].vector,e))))),c=t.map((()=>[]));return invertFlatToArrayMap(a).flatMap((e=>chooseTwoPairs(e))).filter((e=>{const[t,s]=e.map((e=>o[e]));return doRangesOverlap(t,s,r)})).forEach((([e,t])=>{c[e].push(t),c[t].push(e)})),c},getOverlappingComponents=({vertices_coords:e,edges_vertices:t,faces_vertices:r},s=p)=>{const a=e.map(resize2),o=edgesToLines2({vertices_coords:e,edges_vertices:t}),c=r.map((e=>e.map((e=>a[e])))),n=getVerticesClusters({vertices_coords:e},s),i=arrayArrayToLookupArray(clustersToReflexiveArrays(n));e.forEach(((e,t)=>{i[t][t]=!0}));const l=e.map((()=>[]));e.map(((e,t)=>o.map(((e,t)=>t)).filter((t=>overlapLinePoint(o[t],e,excludeS,s))).forEach((e=>{l[t][e]=!0}))));const d=t.map((()=>[]));o.map(((e,t)=>o.map(((e,t)=>t)).filter((r=>t!==r&&void 0!==intersectLineLine(e,o[r],excludeS,excludeS,s).point)).forEach((e=>{d[t][e]=!0,d[e][t]=!0}))));const _=r.map((()=>[]));return c.map(((t,r)=>e.map(((e,t)=>t)).filter((e=>overlapConvexPolygonPoint(t,a[e],exclude,s).overlap)).forEach((e=>{_[r][e]=!0})))),{verticesVertices:i,verticesEdges:l,edgesEdges:d,facesVertices:_}},getFacesEdgesOverlap=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a=p)=>{const{verticesVertices:o,verticesEdges:c,edgesEdges:n,facesVertices:i}=getOverlappingComponents({vertices_coords:e,edges_vertices:t,faces_vertices:r},a),l=r.map((()=>[]));return r.forEach(((e,a)=>t.forEach(((e,d)=>{const _=((e,s)=>{const a=r[s].filter((t=>c[t][e])),[n,i]=t[e],l=r[s].filter((e=>o[e][n]||o[e][i]));return((e,t)=>{const r={};return t.forEach((e=>{r[e]=!0})),e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).filter((([e,t])=>r[e]&&r[t])).forEach((([e,t])=>{r[e]=!1,r[t]=!1})),t.filter((e=>r[e]))})(r[s],Array.from(new Set([...l,...a])))})(d,a),m=((e,r)=>{const[a,o]=t[e],i=s[r].filter((e=>c[a][e]||c[o][e])),l=s[r].filter((t=>n[e][t]));return Array.from(new Set([...i,...l]))})(d,a),g=((e,r)=>t[e].filter((e=>i[r][e])))(d,a);_.length+m.length+g.length===2&&(1===_.length&&1===m.length&&t[m[0]].includes(_[0])||l[a].push(d))})))),l},getEdgesFacesOverlap=({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a=p)=>{const o=t.map((()=>[]));return getFacesEdgesOverlap({vertices_coords:e,edges_vertices:t,faces_vertices:r,faces_edges:s},a).forEach(((e,t)=>e.forEach((e=>{o[e].push(t)})))),o};var Fs=Object.freeze({__proto__:null,getEdgesEdgesCollinearOverlap:getEdgesEdgesCollinearOverlap,getEdgesFacesOverlap:getEdgesFacesOverlap,getFacesEdgesOverlap:getFacesEdgesOverlap,getFacesFacesOverlap:getFacesFacesOverlap,getOverlappingComponents:getOverlappingComponents});var Ss=Object.freeze({__proto__:null,planarizeMakeFaces:(e,t,{edges:{map:r}})=>{const s=invertArrayMap(r),{faces_vertices:a,faces_edges:o}=makePlanarFaces(t),c=(({edges_vertices:e,edges_faces:t,faces_vertices:r,faces_edges:s},a,o)=>{if(!r&&!s)return[];s||(s=makeFacesEdgesFromVertices({edges_vertices:e,faces_vertices:r})),t||(t=makeEdgesFacesUnsorted({edges_vertices:e,faces_vertices:r,faces_edges:s}));const c=a.map((e=>e.filter((e=>void 0!==o[e])).map((e=>o[e])))),n=c.map((e=>e.map((e=>e.flatMap((e=>t[e])))))),i=n.map((e=>invertFlatToArrayMap(e.flat()).map((e=>e.length)))).map((e=>invertFlatToArrayMap(e))).map((e=>e.pop())).map((e=>void 0===e?[]:e));return i})(e,o,s);return{faces_vertices:a,faces_edges:o,faceMap:invertArrayMap(c)}}});const pleat=({vertices_coords:e,edges_vertices:t},r,s,a,o=p)=>{const c=edgesToLines2({vertices_coords:e,edges_vertices:t});return pleat$2(r,s,a,o).map((e=>e.map((e=>{const t=c.map((t=>intersectLineLine(e,t,includeL,includeS,o).a)).filter((e=>void 0!==e));if(t.length<2)return;const r=Math.min(...t),s=Math.max(...t);return Math.abs(s-r)<o?void 0:[add2(e.origin,scale2$1(e.vector,r)),add2(e.origin,scale2$1(e.vector,s))]})).filter((e=>void 0!==e))))};var Cs=Object.freeze({__proto__:null,pleat:pleat,pleatEdges:({vertices_coords:e,edges_vertices:t},r,s,a,o=p)=>{const c=edgeToLine2({vertices_coords:e,edges_vertices:t},r),n=edgeToLine2({vertices_coords:e,edges_vertices:t},s);return pleat({vertices_coords:e,edges_vertices:t},c,n,a,o)}});var Vs=Object.freeze({__proto__:null,raycast:(e,t)=>{}});const triangulateConvexFacesVertices=({faces_vertices:e})=>e.flatMap((e=>{return e.length<4?[e]:(t=e,Array.from(Array(t.length-2)).map(((e,r)=>[t[0],t[r+1],t[r+2]])));var t})),groupByThree=e=>3===e.length?[e]:Array.from(Array(Math.floor(e.length/3))).map(((t,r)=>[3*r+0,3*r+1,3*r+2].map((t=>e[t])))),triangulateNonConvexFacesVertices=({vertices_coords:e,faces_vertices:t},r)=>{if(!e||!e.length)throw new Error(_);const s=e.filter((()=>!0)).shift().length;if(3===s||!r)return triangulateConvexFacesVertices({faces_vertices:t});const a=makeFacesWinding({vertices_coords:e,faces_vertices:t});return t.map((t=>t.flatMap((t=>e[t])))).map((e=>r(e,null,s))).map(((e,r)=>e.map((e=>t[r][e])))).flatMap(((e,t)=>a[t]?groupByThree(e):groupByThree(e).map((e=>e.reverse()))))},rebuildTriangleEdges=({edges_vertices:e,edges_assignment:t,edges_foldAngle:r,faces_vertices:s},{faces_vertices:a})=>{const o=e?makeVerticesToEdge({edges_vertices:e}):{};let c=e?e.length:0;const n=[],i=a.map((e=>e.map(((e,t,r)=>{const s=[e,r[(t+1)%r.length]],a=s.join(\" \");return a in o?o[a]:(n.push(s),o[a]=c,o[s.slice().reverse().join(\" \")]=c,c++)})))),l=t?t.concat(n.map((()=>\"J\"))):(({edges_vertices:e,faces_vertices:t},{faces_vertices:r,faces_edges:s})=>{const a=e?e.map((()=>\"U\")):Array.from(Array(countImpliedEdges({faces_edges:s}))).map((()=>\"U\")),o=makeVerticesToFace({faces_vertices:t});return r.map(((e,t)=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).forEach((([e,r],c)=>{const n=[`${e} ${r}`,`${r} ${e}`];if(void 0===o[n[0]]&&void 0===o[n[1]]){const e=s[t][c];a[e]=\"J\"}})))),Array.from(Array(a.length)).map(((e,t)=>t)).filter((e=>void 0===a[e])).forEach((e=>{a[e]=\"U\"})),a})({edges_vertices:e,faces_vertices:s},{faces_vertices:a,faces_edges:i}),d={edges_vertices:e?e.concat(n):n,faces_edges:i,edges_assignment:l};if(r){const e=n.map((()=>0));d.edges_foldAngle=r.concat(e)}return d},triangulate=({vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s,faces_vertices:a,faceOrders:o},c)=>{if(!a){const a={vertices_coords:e,edges_vertices:t,edges_assignment:r,edges_foldAngle:s};return Object.keys(a).filter((e=>!a[e])).forEach((e=>delete a[e])),{result:a,changes:{}}}const n=(({faces_vertices:e})=>{let t=0;return e.map((e=>Math.max(3,e.length))).map((e=>Array.from(Array(e-2)).map((()=>t++))))})({faces_vertices:a}),i=c?triangulateNonConvexFacesVertices({vertices_coords:e,faces_vertices:a},c):triangulateConvexFacesVertices({faces_vertices:a}),l=rebuildTriangleEdges({edges_vertices:t,edges_assignment:r,edges_foldAngle:s,faces_vertices:a},{faces_vertices:i}),d={...l,vertices_coords:e,faces_vertices:i},_=t?t.length:0;return{result:d,changes:{faces:{map:n},edges:{new:Array.from(Array(l.edges_vertices.length-_)).map(((e,t)=>_+t))}}}};var zs=Object.freeze({__proto__:null,triangulate:triangulate,triangulateConvexFacesVertices:triangulateConvexFacesVertices,triangulateNonConvexFacesVertices:triangulateNonConvexFacesVertices});const prepareForRenderingWithCycles=(e,{earcut:t,layerNudge:r}={})=>{const s=br(e),{planes_faces:a,planes_transform:o}=getFacesPlane(s);if(!s.faceOrders)return triangulate(s,t).result;const c=o.map(invertMatrix4),n=a.map((e=>faceOrdersSubset(s.faceOrders,e))),i={...s,vertices_coords:s.vertices_coords.map(resize3)},l=a.map((e=>subgraphWithFaces(i,e))),d=l.map(((e,t)=>({...e,vertices_coords:e.vertices_coords.map((e=>multiplyMatrix4Vector3(o[t],e)))}))).map(((e,t)=>fixCycles({...e,faceOrders:n[t]}))).map(((e,t)=>({...e,vertices_coords:e.vertices_coords.map(resize3).map((e=>multiplyMatrix4Vector3(c[t],e)))}))),_=d.map((e=>nudgeFacesWithFaceOrders(e))),m=d.map((e=>triangulate(e,t))),g=m.map((({result:e})=>e)).map(explodeFaces);return m.forEach((({changes:e},t)=>{const s=invertArrayToFlatMap(e.faces.map),a=g[t].vertices_coords.map((()=>{}));s.forEach(((e,s)=>{const o=_[t][e];o&&g[t].faces_vertices[s].forEach((e=>{a[e]=scale3$1(o.vector,o.layer*r)}))})),a.forEach(((e,r)=>{e&&(g[t].vertices_coords[r]=add3(resize3(g[t].vertices_coords[r]),e))}))})),g.length>1&&g.forEach(((e,t)=>{0!==t&&join(g[0],e)})),g[0]},prepareForRendering=(e,{earcut:t,layerNudge:r=5e-6}={})=>{const s=br(e);if(!s.edges_assignment){const e=countEdges(s)||countImpliedEdges(s);e&&(s.edges_assignment=Array(e).fill(\"U\"))}if(!s.faceOrders)return explodeFaces(triangulate(s,t).result);const a=nudgeFacesWithFaceOrders(s);if(!a)return prepareForRenderingWithCycles(e,{earcut:t,layerNudge:r});const{changes:o,result:c}=triangulate(s,t),n=explodeFaces(c);if(o.faces){invertArrayToFlatMap(o.faces.map).forEach(((e,t)=>{const s=a[e];s&&n.faces_vertices[t].forEach((e=>{const t=scale3$1(s.vector,s.layer*r);n.vertices_coords[e]=add3(resize3(n.vertices_coords[e]),t)}))}))}return n};var Ts=Object.freeze({__proto__:null,prepareForRendering:prepareForRendering,prepareForRenderingWithCycles:prepareForRenderingWithCycles});const transform=({vertices_coords:e},t)=>e.map(resize3).map((e=>multiplyMatrix3Vector3(t,e)));var Ps=Object.freeze({__proto__:null,rotate:(e,t,r=[0,0,1],s=[0,0,0])=>{const a=makeMatrix3Rotate(t,resize3(r),resize3(s)),o=transform(e,a);return Object.assign(e,{vertices_coords:o})},rotateX:(e,t,r=[0,0,0])=>{const s=makeMatrix3RotateX(t,resize3(r)),a=transform(e,s);return Object.assign(e,{vertices_coords:a})},rotateY:(e,t,r=[0,0,0])=>{const s=makeMatrix3RotateY(t,resize3(r)),a=transform(e,s);return Object.assign(e,{vertices_coords:a})},rotateZ:(e,t,r=[0,0,0])=>{const s=2===getDimensionQuick(e)?resize2:resize3,a=makeMatrix3RotateZ(t,resize3(r)),o=transform(e,a).map((e=>s(e)));return Object.assign(e,{vertices_coords:o})},scale:(e,t=[1,1,1],r=[0,0,0])=>{const s=resize3(r),a=e.vertices_coords.map((e=>add(scaleNonUniform(subtract(e,s),t),s)));return Object.assign(e,{vertices_coords:a})},scale2:(e,t=[1,1],r=[0,0])=>{const s=e.vertices_coords.map((e=>add2(scaleNonUniform2(subtract2(e,r),t),r)));return Object.assign(e,{vertices_coords:s})},scale3:(e,t=[1,1,1],r=[0,0,0])=>{const s=[t[0]||1,t[1]||1,t[2]||1],a=resize3(r),o=e.vertices_coords.map(resize3).map((e=>add3(scaleNonUniform3(subtract3(e,a),s),a)));return Object.assign(e,{vertices_coords:o})},scaleUniform:(e,t=1,r=[0,0,0])=>{if(!e.vertices_coords)return e;const s=resize3(r),a=e.vertices_coords.map((e=>add(scale$1(subtract(e,s),t),s)));return Object.assign(e,{vertices_coords:a})},scaleUniform2:(e,t=1,r=[0,0])=>{if(!e.vertices_coords)return e;const s=e.vertices_coords.map((e=>add2(scale2$1(subtract2(e,r),t),r)));return Object.assign(e,{vertices_coords:s})},scaleUniform3:(e,t=1,r=[0,0,0])=>{if(!e.vertices_coords)return e;const s=resize3(r),a=e.vertices_coords.map(resize3).map((e=>add3(scale3$1(subtract3(e,s),t),s)));return Object.assign(e,{vertices_coords:a})},transform:transform,translate:(e,t)=>{if(!e.vertices_coords)return e;const r=e.vertices_coords.map((e=>add(e,t)));return Object.assign(e,{vertices_coords:r})},translate2:(e,t)=>{if(!e.vertices_coords)return e;const r=e.vertices_coords.map((e=>add2(e,t)));return Object.assign(e,{vertices_coords:r})},translate3:(e,t)=>{if(!e.vertices_coords)return e;const r=resize3(t),s=e.vertices_coords.map(resize3).map((e=>add3(e,r)));return Object.assign(e,{vertices_coords:s})},unitize:e=>{if(!e.vertices_coords)return e;const t=boundingBox$1(e.vertices_coords),r=Math.max(...t.span),s=0===r?1:1/r,a=t.min,o=e.vertices_coords.map((e=>subtract(e,a))).map((e=>scale$1(e,s)));return Object.assign(e,{vertices_coords:o})}});var $s={...me,...Ee,...Object.freeze({__proto__:null,makeEdgesEdges:({edges_vertices:e,vertices_edges:t})=>e.map(((e,r)=>{const s=t[e[0]].filter((e=>e!==r)),a=t[e[1]].filter((e=>e!==r));return s.concat(a)}))}),...ue,...ye,...hs,...pe,...de,...Wr,...fe,...U,...ve,...D,...oe,...le};var Bs=Object.freeze({__proto__:null,bird:()=>populate({vertices_coords:[[0,0],[.5,0],[1,0],[1,.5],[1,1],[.5,1],[0,1],[0,.5],[.5,.5],[.5,(Math.SQRT2-1)/2],[(3-Math.SQRT2)/2,.5],[.5,(3-Math.SQRT2)/2],[(Math.SQRT2-1)/2,.5],[Math.SQRT1_2/2,Math.SQRT1_2/2],[1-Math.SQRT1_2/2,1-Math.SQRT1_2/2]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],[0,9],[9,2],[2,10],[10,4],[4,11],[11,6],[6,12],[12,0],[1,9],[9,8],[3,10],[10,8],[5,11],[11,8],[7,12],[12,8],[2,8],[6,8],[0,13],[13,8],[13,9],[13,12],[4,14],[14,8],[14,10],[14,11]],edges_assignment:Array.from(\"BBBBBBBBVVVVVVVVMVMVMVMVMMFFFFFFFF\")},{faces:!0}),blintz:()=>populate({vertices_coords:[[0,0],[.5,0],[1,0],[1,.5],[1,1],[.5,1],[0,1],[0,.5]],vertices_vertices:[[1,7],[2,3,7,0],[3,1],[4,5,1,2],[5,3],[6,7,3,4],[7,5],[0,1,5,6]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],[7,1],[1,3],[3,5],[5,7]],edges_assignment:Array.from(\"BBBBBBBBVVVV\"),faces_vertices:[[7,1,3,5],[1,7,0],[3,1,2],[5,3,4],[7,5,6]]}),fish:()=>populate({vertices_coords:[[0,0],[Math.SQRT1_2,0],[1,0],[1,1-Math.SQRT1_2],[1,1],[1-Math.SQRT1_2,1],[0,1],[0,Math.SQRT1_2],[.5,.5],[Math.SQRT1_2,1-Math.SQRT1_2],[1-Math.SQRT1_2,Math.SQRT1_2]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],[9,0],[9,2],[9,4],[10,0],[10,6],[10,4],[9,1],[10,7],[9,3],[10,5],[8,0],[8,9],[8,4],[8,10]],edges_assignment:Array.from(\"BBBBBBBBVVVVVVMMFFFFFF\")},{faces:!0}),frog:()=>populate({vertices_coords:[[0,1],[0,Math.SQRT1_2],[0,.5],[0,1-Math.SQRT1_2],[0,0],[.5,.5],[1,1],[(1-Math.SQRT1_2)/2,Math.SQRT1_2/2],[Math.SQRT1_2/2,(1-Math.SQRT1_2)/2],[1-Math.SQRT1_2,0],[.5,0],[Math.SQRT1_2,0],[1,0],[.5,(1-Math.SQRT1_2)/2],[1-Math.SQRT1_2/2,(1-Math.SQRT1_2)/2],[(1-Math.SQRT1_2)/2,1-Math.SQRT1_2/2],[(1-Math.SQRT1_2)/2,.5],[(1+Math.SQRT1_2)/2,1-Math.SQRT1_2/2],[1,Math.SQRT1_2],[Math.SQRT1_2,1],[1-Math.SQRT1_2/2,(1+Math.SQRT1_2)/2],[Math.SQRT1_2/2,(1+Math.SQRT1_2)/2],[.5,1],[1,.5],[(1+Math.SQRT1_2)/2,Math.SQRT1_2/2],[.5,(1+Math.SQRT1_2)/2],[(1+Math.SQRT1_2)/2,.5],[1-Math.SQRT1_2,1],[1,1-Math.SQRT1_2]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[4,7],[4,8],[4,9],[9,10],[10,11],[11,12],[8,13],[13,14],[15,16],[16,7],[3,7],[7,5],[5,17],[17,18],[19,20],[20,5],[5,8],[8,9],[2,15],[14,10],[21,22],[23,24],[10,8],[7,2],[12,14],[0,15],[22,25],[25,5],[5,13],[13,10],[2,16],[16,5],[5,26],[26,23],[6,17],[6,20],[11,14],[14,5],[5,21],[21,27],[28,24],[24,5],[5,15],[15,1],[12,5],[5,0],[20,25],[25,21],[24,26],[26,17],[12,24],[0,21],[12,28],[28,23],[23,18],[18,6],[6,19],[19,22],[22,27],[27,0],[22,20],[17,23]],edges_assignment:Array.from(\"BBBBFFVVBBBBMMMMFVVFFVVFVVVVVVVVVMMVVMMVVVFVVFFVVFMMMMMMVVBBBBBBBBVV\")},{faces:!0}),kite:()=>populate({vertices_coords:[[0,0],[1,0],[1,Math.SQRT2-1],[1,1],[Math.SQRT2-1,1],[0,1]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,0],[0,2],[0,4],[0,3]],edges_assignment:Array.from(\"BBBBBBVVF\")},{faces:!0}),squareFish:()=>populate({vertices_coords:[[0,0],[2-Math.SQRT2,0],[1,0],[0,1],[0,2-Math.SQRT2],[.5,.5],[Math.SQRT1_2,Math.SQRT1_2],[1,1],[Math.SQRT1_2,1-Math.SQRT1_2],[1,Math.SQRT2-1],[1-Math.SQRT1_2,Math.SQRT1_2],[Math.SQRT2-1,1],[Math.SQRT1_2,1],[1,Math.SQRT1_2]],edges_vertices:[[0,1],[1,2],[3,4],[4,0],[0,5],[5,6],[6,7],[0,8],[8,9],[0,10],[10,11],[8,1],[10,4],[8,6],[6,12],[3,10],[10,5],[5,8],[8,2],[10,6],[6,13],[7,12],[12,11],[11,3],[11,6],[6,9],[2,9],[9,13],[13,7]],edges_assignment:Array.from(\"BBBBFFFVFVFMMVVVFFVVVBBBMMBBB\")},{faces:!0}),waterbomb:()=>populate({vertices_coords:[[0,0],[.5,0],[1,0],[1,.5],[1,1],[.5,1],[0,1],[0,.5],[.5,.5]],vertices_vertices:[[1,8,7],[2,8,0],[3,8,1],[4,8,2],[5,8,3],[6,8,4],[7,8,5],[0,8,6],[0,1,2,3,4,5,6,7]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],[0,8],[1,8],[2,8],[3,8],[4,8],[5,8],[6,8],[7,8]],edges_assignment:Array.from(\"BBBBBBBBVFVMVFVM\"),faces_vertices:[[0,1,8],[1,2,8],[2,3,8],[3,4,8],[4,5,8],[5,6,8],[6,7,8],[7,0,8]]}),windmill:()=>populate({vertices_coords:[[0,0],[.25,0],[.5,0],[.75,0],[1,0],[0,1],[0,.75],[0,.5],[0,.25],[.25,.25],[.5,.5],[.75,.75],[1,1],[.25,1],[.25,.75],[.25,.5],[1,.25],[.75,.25],[.5,.25],[.5,1],[1,.5],[.5,.75],[.75,.5],[.75,1],[1,.75]],edges_vertices:[[0,1],[1,2],[2,3],[3,4],[5,6],[6,7],[7,8],[8,0],[0,9],[9,10],[10,11],[11,12],[13,14],[14,15],[15,9],[9,1],[16,17],[17,18],[18,9],[9,8],[7,14],[14,19],[20,17],[17,2],[2,9],[9,7],[19,21],[21,10],[10,18],[18,2],[20,22],[22,10],[10,15],[15,7],[4,17],[17,10],[10,14],[14,5],[23,11],[11,22],[22,17],[17,3],[6,14],[14,21],[21,11],[11,24],[12,23],[23,19],[19,13],[13,5],[4,16],[16,20],[20,24],[24,12],[19,11],[11,20]],edges_assignment:Array.from(\"BBBBBBBBVFFVFVVFFVVFMFMFMFFFFFFFFFVFFVFVVFFVVFBBBBBBBBMF\")},{faces:!0})});const makeRectCoords=(e,t)=>[[0,0],[e,0],[e,t],[0,t]],makeGraphWithBoundaryCoords=e=>({vertices_coords:e,edges_vertices:e.map(((e,t,r)=>[t,(t+1)%r.length])),edges_assignment:Array(e.length).fill(\"B\"),faces_vertices:[e.map(((e,t)=>t))],faces_edges:[e.map(((e,t)=>t))]}),polygon=(e=3,t=1)=>populate(makeGraphWithBoundaryCoords(makePolygonCircumradius(e,t)));var Ns=Object.freeze({__proto__:null,circle:(e=1,t=128)=>polygon(t,e),polygon:polygon,rectangle:(e=1,t=1)=>populate(makeGraphWithBoundaryCoords(makeRectCoords(e,t))),square:(e=1)=>populate(makeGraphWithBoundaryCoords(makeRectCoords(e,e)))});const Rs={};Rs.prototype=Object.create(Object.prototype),Rs.prototype.constructor=Rs,Object.entries({clean:clean,populate:populate,subgraph:subgraph,boundary:boundary,boundaries:boundaries$1,planarBoundary:planarBoundary,planarBoundaries:planarBoundaries,boundingBox:boundingBox,invertAssignments:invertAssignments,svg:foldToSvg,obj:foldToObj,...ts,...js,...ws,...Ps,...vs}).forEach((([e,t])=>{Rs.prototype[e]=function(){return t.apply(null,[this,...arguments])}})),Rs.prototype.clone=function(){return Object.assign(Object.create(Object.getPrototypeOf(this)),br(this))},Rs.prototype.planarize=function(){const e=planarize$2(this);return this.clear(),Object.assign(this,e),this},Rs.prototype.clear=function(){return G.graph.forEach((e=>delete this[e])),G.orders.forEach((e=>delete this[e])),delete this.file_frames,this},Rs.prototype.folded=function(){const e=this.faces_matrix2?makeVerticesCoordsFoldedFromMatrix2(this,this.faces_matrix2):makeVerticesCoordsFolded(this,...arguments);return Object.assign(this,{vertices_coords:e,frame_classes:[\"foldedForm\"]}),this},Rs.prototype.flatFolded=function(){const e=this.faces_matrix2?makeVerticesCoordsFoldedFromMatrix2(this,this.faces_matrix2):makeVerticesCoordsFlatFolded(this,...arguments);return Object.assign(this,{vertices_coords:e,frame_classes:[\"foldedForm\"]}),this};var Ls=Rs.prototype;const makeGraphInstance=(...e)=>Object.assign(Object.create(Ls),{...e.reduce(((e,t)=>({...e,...t})),{}),file_spec:1.2,file_creator:I}),Graph=function(){return populate(makeGraphInstance(...arguments))};(Graph.prototype=Ls).constructor=Graph,Object.keys(Ns).forEach((e=>{Graph[e]=(...t)=>makeGraphInstance(Ns[e](...t))})),Object.keys(Bs).forEach((e=>{Graph[e]=(...t)=>makeGraphInstance(Bs[e](...t))}));const Is=Graph,Us={...tt,...Nr,...re,...Rr,...Lr,...Te,...ze,...Ve,...q,...Gr,...Kr,...Ar,...os,...ds,..._s,...gs,...we,...je,...Fe,...es,...Oe,...ie,...Be,...ps,...Ce,...ae,...Os,...xr,...$e,...js,...cs,...us,...$s,...Ae,...ws,...ks,...he,...Or,...Fs,...xs,...Es,...ys,...As,...Ss,...Ms,...Cs,...rs,...Vs,...xe,...Ts,...ke,...ts,...ns,...ms,...is,...qr,...W,...fs,...Ps,...Yr,...zs,...ge,...vs},Ds=Object.assign(Is,Us),convexHullRadialSortPoints=(e,t=p)=>{const r=((e,t=p)=>{if(!e||!e.length)return;const r=((e,t)=>{let r=[0];for(let s=1;s<e.length;s+=1)switch(t(e[r[0]],e[s])){case 0:r.push(s);break;case 1:r=[s]}return r})(e,((e,r)=>epsilonCompare(e[0],r[0],t)));let s=0;for(let t=1;t<r.length;t+=1)e[r[t]][1]<e[r[s]][1]&&(s=t);return r[s]})(e,t);if(void 0===r)return[];const s=e.map((t=>subtract2(t,e[r]))).map((e=>normalize2(e))).map((e=>dot2([0,1],e))),a=s.map(((e,t)=>({a:e,i:t}))).sort(((e,t)=>e.a-t.a)).map((e=>e.i)).filter((e=>e!==r));return[[r]].concat(clusterScalars(a.map((e=>s[e])),t).map((e=>e.map((e=>a[e])))).map((t=>1===t.length?t:t.map((t=>({i:t,len:distance2(e[t],e[r])}))).sort(((e,t)=>e.len-t.len)).map((e=>e.i)))))},convexHull=(e=[],t=!1,r=p)=>{if(e.length<2)return[];const s=convexHullRadialSortPoints(e,r).map((e=>1===e.length?e:(e=>e.concat(e.slice(0,-1).reverse()))(e))).flat();s.push(s[0]);const a=[s[0]];let o=1;const c={\"-1\":()=>a.pop(),1:e=>{a.push(e),o+=1},undefined:()=>{o+=1}};for(c[0]=t?c[1]:c[-1];o<s.length;){if(a.length<2){a.push(s[o]),o+=1;continue}const t=a[a.length-2],n=a[a.length-1],i=s[o],l=[t,n,i].map((t=>e[t]));c[threePointTurnDirection(l[0],l[1],l[2],r)](i)}return a.pop(),a};const recurseSkeleton=(e,t,r)=>{const s=e.map(((e,t)=>({vector:r[t],origin:e}))).map(((e,t,r)=>intersectLineLine(e,r[(t+1)%r.length],x,x).point)),a=t.map(((e,t)=>nearestPointOnLine(e,s[t]))).map((([e,t])=>[e,t]));if(3===e.length)return e.map((e=>({type:\"skeleton\",points:[e,s[0]]}))).concat([{type:\"perpendicular\",points:[a[0],s[0]]}]);const o=s.map(((e,t)=>distance(e,a[t])));let c=0;o.forEach(((e,t)=>{e<o[c]&&(c=t)}));const n=[{type:\"skeleton\",points:[e[c],s[c]]},{type:\"skeleton\",points:[e[(c+1)%e.length],s[c]]},{type:\"perpendicular\",points:[a[c],s[c]]}],i=clockwiseBisect2(flip2(t[(c+t.length-1)%t.length].vector),t[(c+1)%t.length].vector),l=c===e.length-1;return e.splice(c,2,s[c]),t.splice(c,1),r.splice(c,2,i),l&&(e.splice(0,1),r.splice(0,1),t.push(t.shift())),n.concat(recurseSkeleton(e,t,r))};var Qs={...y,...M,...O,...E,...P,...Zr,...Dr,...Qr,...Object.freeze({__proto__:null,convexHull:convexHull,convexHullRadialSortPoints:convexHullRadialSortPoints}),...w,...Se,...ce,..._e,...j,...F,...Object.freeze({__proto__:null,straightSkeleton:e=>{const t=e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((e=>({vector:subtract2(e[1],e[0]),origin:e[0]}))),r=e.map(((e,t,r)=>[(t-1+r.length)%r.length,t,(t+1)%r.length].map((e=>r[e])))).map((e=>[subtract2(e[0],e[1]),subtract2(e[2],e[1])])).map((([e,t])=>clockwiseBisect2(e,t)));return recurseSkeleton([...e],t,r)}}),...ls,...Object.freeze({__proto__:null,enclosingBoundingBoxes:(e,t,r=p)=>{const s=Math.min(e.min.length,t.min.length);for(let a=0;a<s;a+=1)if(t.min[a]<e.min[a]-r||t.max[a]>e.max[a]+r)return!1;return!0},pointInBoundingBox:(e,t,r=p)=>{for(let s=0;s<e.length;s+=1)if(e[s]<t.min[s]-r||e[s]>t.max[s]+r)return!1;return!0}}),...$,...C,...B};var Ws=Object.freeze({__proto__:null,foldDegree4:(e,t,r=0)=>{const s=(e=>e.filter((e=>\"M\"===e)).length>e.filter((e=>\"V\"===e)).length?e.indexOf(\"V\"):e.indexOf(\"M\"))(t.map((e=>e.toUpperCase())));if(-1===s)return;const a=e[(s+1)%e.length],o=e[(s+2)%e.length],c=Math.max(-Math.PI,Math.min(Math.PI,r)),n=-Math.cos(a)*Math.cos(o)+Math.sin(a)*Math.sin(o)*Math.cos(Math.PI-c),i=Math.cos(Math.PI-c)-Math.sin(Math.PI-c)**2*Math.sin(a)*Math.sin(o)/(1-n),l=-Math.acos(i)+Math.PI;return s%2==0?[l,c,l,c].map(((e,t)=>s===t?-e:e)):[c,l,c,l].map(((e,t)=>s===t?-e:e))}});const alternatingSum=e=>[0,1].map((t=>e.filter(((e,r)=>r%2===t)).reduce(((e,t)=>e+t),0))),kawasakiSolutionsRadians=e=>e.map(((e,t,r)=>[e,r[(t+1)%r.length]])).map((([e,t])=>counterClockwiseAngleRadians(e,t))).map(((e,t,r)=>r.slice(t+1,r.length).concat(r.slice(0,t)))).map((e=>alternatingSum(e).map((e=>Math.PI-e)))).map(((t,r)=>e[r]+t[0])).map(((t,r)=>isCounterClockwiseBetween(t,e[r],e[(r+1)%e.length])?t:void 0)),kawasakiSolutionsVectors=e=>kawasakiSolutionsRadians(e.map((e=>Math.atan2(e[1],e[0])))).map((e=>void 0===e?void 0:[Math.cos(e),Math.sin(e)]));var qs=Object.freeze({__proto__:null,alternatingSum:alternatingSum,alternatingSumDifference:e=>{const t=e.reduce(((e,t)=>e+t),0)/2;return alternatingSum(e).map((e=>e-t))},kawasakiSolutions:({vertices_coords:e,vertices_edges:t,edges_assignment:r,edges_vertices:s},a)=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:s}));const o=r?t[a].filter((e=>X[r[e]])):t[a];if(o.length%2==0)return[];const c=o.map((e=>s[e][0]===a?s[e]:[s[e][1],s[e][0]])).map((t=>t.map((t=>e[t])))).map((e=>subtract2(e[1],e[0]))),n=counterClockwiseOrder2(c).map((e=>c[e])),i=kawasakiSolutionsVectors(n),l=n.map(normalize2),d=i.filter((e=>void 0!==e)).filter((e=>!l.map((t=>dot2(e,t))).map((e=>Math.abs(1-e)<.001)).reduce(((e,t)=>e||t),!1)));return d},kawasakiSolutionsRadians:kawasakiSolutionsRadians,kawasakiSolutionsVectors:kawasakiSolutionsVectors});const getVerticesWithNoFolds=({vertices_edges:e,edges_assignment:t})=>e.map((e=>e.map((e=>!X[t[e]])).reduce(((e,t)=>e&&t),!0))).map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)),verticesFlatFoldabilityMaekawa=({edges_vertices:e,vertices_edges:t,edges_assignment:r})=>{t||(t=makeVerticesEdgesUnsorted({edges_vertices:e}));const s=t.map((e=>e.map((e=>Y[r[e]])).filter((e=>0!==e&&void 0!==e)).map(Math.sign).reduce(((e,t)=>e+t),0))).map((e=>Math.abs(e)-2));return boundaryVertices({edges_vertices:e,edges_assignment:r}).forEach((e=>{s[e]=0})),getVerticesWithNoFolds({vertices_edges:t,edges_assignment:r}).forEach((e=>{s[e]=0})),s},verticesFlatFoldabilityKawasaki=({vertices_coords:e,vertices_vertices:t,vertices_edges:r,edges_vertices:s,edges_assignment:a})=>{t||(t=makeVerticesVertices({vertices_coords:e,vertices_edges:r,edges_vertices:s})),r||(r=makeVerticesEdgesUnsorted({edges_vertices:s}));const o=makeVerticesVerticesVector({vertices_coords:e,vertices_vertices:t,edges_vertices:s}).map(((e,t)=>e.filter(((e,s)=>X[a[r[t][s]]])))).map((e=>e.map(resize2))).map((e=>e.length>1?counterClockwiseSectors2(e):[0,0])).map((e=>alternatingSum(e))).map((([e,t])=>e-t));return boundaryVertices({edges_vertices:s,edges_assignment:a}).forEach((e=>{o[e]=0})),getVerticesWithNoFolds({vertices_edges:r,edges_assignment:a}).forEach((e=>{o[e]=0})),o},verticesFlatFoldableMaekawa=e=>verticesFlatFoldabilityMaekawa(e).map((e=>0===e)),verticesFlatFoldableKawasaki=(e,t=p)=>verticesFlatFoldabilityKawasaki(e).map(Math.abs).map((e=>e<t)),verticesFlatFoldability=(e,t)=>{const r=verticesFlatFoldableMaekawa(e).map((e=>e?0:1)),s=verticesFlatFoldableKawasaki(e,t).map((e=>e?0:1));return r.map(((e,t)=>e|s[t]<<1))};const verticesFoldability=({vertices_coords:e,vertices_vertices:t,vertices_edges:r,vertices_faces:s,edges_vertices:a,edges_foldAngle:o,edges_vector:c,faces_vertices:n})=>{t||(t=makeVerticesVertices({vertices_coords:e,vertices_edges:r,vertices_faces:s,edges_vertices:a,faces_vertices:n})),r||(r=makeVerticesEdges({edges_vertices:a,vertices_vertices:t})),s||(s=makeVerticesFaces({vertices_coords:e,vertices_vertices:t,faces_vertices:n}));const i=makeVerticesVerticesVector({vertices_coords:e,vertices_vertices:t,vertices_edges:r,vertices_faces:s,edges_vertices:a,edges_vector:c,faces_vertices:n});return e.map(((e,t)=>{if(s[t].includes(void 0)||s[t].includes(null))return 0;const a=i[t].map((e=>Math.atan2(e[1],e[0]))),c=r[t].map((e=>o[e])).map((e=>e*h)),n=a.map((e=>makeMatrix3RotateZ(e))),l=n.map((e=>invertMatrix3(e))),d=c.map((e=>makeMatrix3RotateX(e))),_=i[t].map(((e,t)=>multiplyMatrices3(n[t],multiplyMatrices3(d[t],l[t]))));let m=[...Jr];return _.forEach((e=>{m=multiplyMatrices3(m,e)})),Array.from(Array(9)).map(((e,t)=>Math.abs(m[t]-Jr[t]))).reduce(((e,t)=>e+t),0)}))};var Gs={...Ws,...Object.freeze({__proto__:null,verticesFlatFoldability:verticesFlatFoldability,verticesFlatFoldabilityKawasaki:verticesFlatFoldabilityKawasaki,verticesFlatFoldabilityMaekawa:verticesFlatFoldabilityMaekawa,verticesFlatFoldable:(e,t)=>verticesFlatFoldability(e,t).map((e=>0===e)),verticesFlatFoldableKawasaki:verticesFlatFoldableKawasaki,verticesFlatFoldableMaekawa:verticesFlatFoldableMaekawa}),...Object.freeze({__proto__:null,verticesFoldability:verticesFoldability,verticesFoldable:(e,t=p)=>verticesFoldability(e).map((e=>Math.abs(e)<t))}),...qs,...Object.freeze({__proto__:null,maekawaSolver:e=>{const t=(r=e).map(((e,t)=>t)).filter((e=>\"U\"===r[e]||\"u\"===r[e]));var r;const s=Array.from(Array(2**t.length)).map(((e,t)=>t.toString(2))).map((e=>Array(t.length-e.length+1).join(\"0\")+e)).map((e=>Array.from(e).map((e=>\"0\"===e?\"V\":\"M\")))).map((r=>{const s=e.slice();return t.forEach(((e,t)=>{s[e]=r[t]})),s}));if(e.filter((e=>K[e])).length>0)return s;const a=s.map((e=>e.filter((e=>\"M\"===e||\"m\"===e)).length)),o=s.map((e=>e.filter((e=>\"V\"===e||\"v\"===e)).length));return s.filter(((e,t)=>2===Math.abs(a[t]-o[t])))}})};const Hs={B:[.5,.5,.5],b:[.5,.5,.5],V:[.2,.4,.6],v:[.2,.4,.6],M:[.75,.25,.15],m:[.75,.25,.15],F:[.3,.3,.3],f:[.3,.3,.3],J:[.3,.2,0],j:[.3,.2,0],C:[.5,.8,.1],c:[.5,.8,.1],U:[.6,.25,.9],u:[.6,.25,.9]},Js={B:[0,0,0],b:[0,0,0],V:[.2,.5,.8],v:[.2,.5,.8],M:[.75,.25,.15],m:[.75,.25,.15],F:[.75,.75,.75],f:[.75,.75,.75],J:[1,.75,.25],j:[1,.75,.25],C:[.5,.8,.1],c:[.5,.8,.1],U:[.6,.25,.9],u:[.6,.25,.9]},parseColorToWebGLColor=e=>{if(\"string\"==typeof e){const[t,r,s]=parseColorToRgb(e).slice(0,3).map((e=>e/255));return[t,r,s]}if(e&&e.constructor===Array){const[t,r,s]=e;return[t,r,s]}};var Zs=Object.freeze({__proto__:null,dark:Hs,light:Js,parseColorToWebGLColor:parseColorToWebGLColor});var Ys=Object.freeze({__proto__:null,deallocModel:(e,t)=>{t.vertexArrays.forEach((t=>e.disableVertexAttribArray(t.location))),t.vertexArrays.forEach((t=>e.deleteBuffer(t.buffer))),t.elementArrays.forEach((t=>e.deleteBuffer(t.buffer))),e.deleteProgram(t.program)},drawModel:(e,t,r,s={})=>{e.useProgram(r.program),r.flags.forEach((t=>e.enable(t)));const a=e.getProgramParameter(r.program,e.ACTIVE_UNIFORMS);for(let t=0;t<a;t+=1){const a=e.getActiveUniform(r.program,t).name;if(!s[a])continue;const{func:o,value:c}=s[a],n=e.getUniformLocation(r.program,a);switch(o){case\"uniformMatrix2fv\":case\"uniformMatrix3fv\":case\"uniformMatrix4fv\":e[o](n,!1,c);break;default:e[o](n,c)}}r.vertexArrays.forEach((t=>{e.bindBuffer(e.ARRAY_BUFFER,t.buffer),e.bufferData(e.ARRAY_BUFFER,t.data,e.STATIC_DRAW),e.vertexAttribPointer(t.location,t.length,t.type,!1,0,0),e.enableVertexAttribArray(t.location)})),r.elementArrays.forEach((r=>{e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,r.buffer),e.bufferData(e.ELEMENT_ARRAY_BUFFER,r.data,e.STATIC_DRAW),e.drawElements(r.mode,r.data.length,2===t?e.UNSIGNED_INT:e.UNSIGNED_SHORT,0)})),r.flags.forEach((t=>e.disable(t)))}});var Xs=Object.freeze({__proto__:null,makeModelMatrix:e=>{if(!e)return[...Ir];const t=boundingBox(e);if(!t)return[...Ir];const r=Math.max(...t.span);if(0===r)return[...Ir];const s=[r,0,0,0,0,r,0,0,0,0,r,0,...resize(3,midpoint(t.min,t.max)),1];return invertMatrix4(s)||[...Ir]},makeProjectionMatrix:([e,t],r=\"perspective\",s=45)=>{const a=Math.min(e,t),o=[(e-a)/a/2,(t-a)/a/2].map((e=>e+.5));return\"orthographic\"===r?makeOrthographicMatrix4(o[1],o[0],-o[1],-o[0],-100,100):makePerspectiveMatrix4(s*(Math.PI/180),e/t,.1,20)},rebuildViewport:(e,t)=>{if(!e)return;const r=window.devicePixelRatio||1,s=[t.clientWidth,t.clientHeight].map((e=>e*r));t.width===s[0]&&t.height===s[1]||(t.width=s[0],t.height=s[1]),e.viewport(0,0,e.canvas.width,e.canvas.height)}});const compileShader=(e,t,r)=>{const s=e.createShader(r);if(e.shaderSource(s,t),e.compileShader(s),!e.getShaderParameter(s,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(s));return s},createProgram=(e,t,r)=>((e,t,r)=>{const s=e.createProgram();if(e.attachShader(s,t),e.attachShader(s,r),e.linkProgram(s),!e.getProgramParameter(s,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(s));return e.deleteShader(t),e.deleteShader(r),s})(e,compileShader(e,t,e.VERTEX_SHADER),compileShader(e,r,e.FRAGMENT_SHADER));var Ks={...Zs,...Ys,...Xs,...Object.freeze({__proto__:null,createProgram:createProgram,initializeWebGL:(e,t)=>{const r=RabbitEarWindow$1().devicePixelRatio||1;switch(e.width=e.clientWidth*r,e.height=e.clientHeight*r,t){case 1:return{gl:e.getContext(\"webgl\"),version:1};case 2:return{gl:e.getContext(\"webgl2\"),version:2}}const s=e.getContext(\"webgl2\");if(s)return{gl:s,version:2};const a=e.getContext(\"webgl\");if(a)return{gl:a,version:1};throw new Error(i)}})};const makeFacesVertexData=({vertices_coords:e,edges_assignment:t,faces_vertices:r,faces_edges:s,faces_normal:a},o={})=>{const c=e.map((e=>[...e].concat(Array(3-e.length).fill(0)))).map(resize3),n=makeVerticesNormal({vertices_coords:c,faces_vertices:r,faces_normal:a}),i=c.map(((e,t)=>t%3)).map((e=>[0===e?1:0,1===e?1:0,2===e?1:0])),l=(({edges_assignment:e,faces_vertices:t,faces_edges:r})=>r&&e?r.map((t=>t.map((t=>e[t])).map((e=>\"J\"===e||\"j\"===e)))):t?t.map((e=>e.map((()=>!1)))):[])({edges_assignment:t,faces_vertices:r,faces_edges:s});if(!o.showTriangulation)for(let e=0;e<l.length;e+=1)l[e][0]&&(i[3*e+0][2]=i[3*e+1][2]=100),l[e][1]&&(i[3*e+1][0]=i[3*e+2][0]=100),l[e][2]&&(i[3*e+0][1]=i[3*e+2][1]=100);return{vertices_coords:c,vertices_normal:n,vertices_barycentric:i}},makeThickEdgesVertexData=(e,t)=>{if(!e||!e.vertices_coords||!e.edges_vertices)return;const r={...t&&t.dark?Hs:Js,...t},s=e.vertices_coords.map((e=>[...e].concat(Array(3-e.length).fill(0)))),a=e.edges_vertices.flatMap((e=>e.map((e=>s[e])))).flatMap((e=>[e,e,e,e])),o=makeEdgesVector(e);return{vertices_coords:a,vertices_color:e.edges_assignment?e.edges_assignment.flatMap((e=>Array(8).fill(r[e]))):e.edges_vertices.flatMap((()=>Array(8).fill(r.U))),verticesEdgesVector:o.flatMap((e=>[e,e,e,e,e,e,e,e])),vertices_vector:e.edges_vertices.flatMap((()=>[[1,0],[0,1],[-1,0],[0,-1],[1,0],[0,1],[-1,0],[0,-1]]))}};var ea=Object.freeze({__proto__:null,makeFacesVertexData:makeFacesVertexData,makeThickEdgesVertexData:makeThickEdgesVertexData});const makeFoldedVertexArrays=(e,t,{vertices_coords:r,edges_vertices:s,edges_assignment:a,faces_vertices:o,faces_edges:c,faces_normal:n}={},i={})=>{if(!r||!o)return[];!c&&s&&o&&(c=makeFacesEdgesFromVertices({edges_vertices:s,faces_vertices:o}));const{vertices_coords:l,vertices_normal:d,vertices_barycentric:_}=makeFacesVertexData({vertices_coords:r,edges_assignment:a,faces_vertices:o,faces_edges:c,faces_normal:n},i);return[{location:e.getAttribLocation(t,\"v_position\"),buffer:e.createBuffer(),type:e.FLOAT,length:l.length?l[0].length:3,data:new Float32Array(l.flat())},{location:e.getAttribLocation(t,\"v_normal\"),buffer:e.createBuffer(),type:e.FLOAT,length:d.length?d[0].length:3,data:new Float32Array(d.flat())},{location:e.getAttribLocation(t,\"v_barycentric\"),buffer:e.createBuffer(),type:e.FLOAT,length:3,data:new Float32Array(_.flat())}].filter((e=>-1!==e.location))},makeFoldedElementArrays=(e,t=1,r={})=>r&&r.vertices_coords&&r.faces_vertices?[{mode:e.TRIANGLES,buffer:e.createBuffer(),data:2===t?new Uint32Array(r.faces_vertices.flat()):new Uint16Array(r.faces_vertices.flat())}]:[],makeThickEdgesVertexArrays=(e,t,r,s={})=>{if(!r||!r.vertices_coords||!r.edges_vertices)return[];const{vertices_coords:a,vertices_color:o,verticesEdgesVector:c,vertices_vector:n}=makeThickEdgesVertexData(r,s.assignment_color);return a?[{location:e.getAttribLocation(t,\"v_position\"),buffer:e.createBuffer(),type:e.FLOAT,length:a.length?a[0].length:3,data:new Float32Array(a.flat())},{location:e.getAttribLocation(t,\"v_color\"),buffer:e.createBuffer(),type:e.FLOAT,length:o.length?o[0].length:3,data:new Float32Array(o.flat())},{location:e.getAttribLocation(t,\"edge_vector\"),buffer:e.createBuffer(),type:e.FLOAT,length:c.length?c[0].length:3,data:new Float32Array(c.flat())},{location:e.getAttribLocation(t,\"vertex_vector\"),buffer:e.createBuffer(),type:e.FLOAT,length:n.length?n[0].length:3,data:new Float32Array(n.flat())}].filter((e=>-1!==e.location)):[]},makeThickEdgesElementArrays=(e,t=1,r={})=>{if(!r||!r.edges_vertices)return[];const s=r.edges_vertices.map(((e,t)=>8*t)).flatMap((e=>[e+0,e+1,e+4,e+4,e+1,e+5,e+1,e+2,e+5,e+5,e+2,e+6,e+2,e+3,e+6,e+6,e+3,e+7,e+3,e+0,e+7,e+7,e+0,e+4]));return[{mode:e.TRIANGLES,buffer:e.createBuffer(),data:2===t?new Uint32Array(s):new Uint16Array(s)}]};var ta=Object.freeze({__proto__:null,makeFoldedElementArrays:makeFoldedElementArrays,makeFoldedVertexArrays:makeFoldedVertexArrays,makeThickEdgesElementArrays:makeThickEdgesElementArrays,makeThickEdgesVertexArrays:makeThickEdgesVertexArrays});const makeUniforms$1=({projectionMatrix:e,modelViewMatrix:t,frontColor:r,backColor:s,outlineColor:a,strokeWidth:o,opacity:c})=>({u_matrix:{func:\"uniformMatrix4fv\",value:multiplyMatrices4(e||Ir,t||Ir)},u_projection:{func:\"uniformMatrix4fv\",value:e||Ir},u_modelView:{func:\"uniformMatrix4fv\",value:t||Ir},u_frontColor:{func:\"uniform3fv\",value:parseColorToWebGLColor(r||\"gray\")},u_backColor:{func:\"uniform3fv\",value:parseColorToWebGLColor(s||\"white\")},u_outlineColor:{func:\"uniform3fv\",value:parseColorToWebGLColor(a||\"black\")},u_strokeWidth:{func:\"uniform1f\",value:void 0!==o?o:.05},u_opacity:{func:\"uniform1f\",value:void 0!==c?c:1}});var ra=Object.freeze({__proto__:null,makeUniforms:makeUniforms$1});const sa=\"#version 300 es\\nuniform mat4 u_modelView;\\nuniform mat4 u_matrix;\\nuniform vec3 u_frontColor;\\nuniform vec3 u_backColor;\\nin vec3 v_position;\\nin vec3 v_normal;\\nout vec3 front_color;\\nout vec3 back_color;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 1);\\n\\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\\n\\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\\n\\tfront_color = u_frontColor * brightness;\\n\\tback_color = u_backColor * brightness;\\n}\\n\",aa=\"#version 300 es\\nuniform mat4 u_matrix;\\nuniform mat4 u_projection;\\nuniform mat4 u_modelView;\\nuniform float u_strokeWidth;\\nin vec3 v_position;\\nin vec3 v_color;\\nin vec3 edge_vector;\\nin vec2 vertex_vector;\\nout vec3 blend_color;\\nvoid main () {\\n\\tvec3 edge_norm = normalize(edge_vector);\\n\\t// axis most dissimilar to edge_vector\\n\\tvec3 absNorm = abs(edge_norm);\\n\\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\\n\\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\\n\\t// two perpendiculars. with edge_vector these make basis vectors\\n\\tvec3 one = cross(axis, edge_norm);\\n\\tvec3 two = cross(one, edge_norm);\\n\\tvec3 displaceNormal = normalize(\\n\\t\\tone * vertex_vector.x + two * vertex_vector.y\\n\\t);\\n\\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\\n\\tgl_Position = u_matrix * vec4(v_position + displace, 1);\\n\\tblend_color = v_color;\\n}\\n\",oa=\"#version 300 es\\n#ifdef GL_FRAGMENT_PRECISION_HIGH\\n  precision highp float;\\n#else\\n  precision mediump float;\\n#endif\\nuniform float u_opacity;\\nin vec3 front_color;\\nin vec3 back_color;\\nin vec3 outline_color;\\nin vec3 barycentric;\\nout vec4 outColor;\\nfloat edgeFactor(vec3 barycenter) {\\n\\tvec3 d = fwidth(barycenter);\\n\\tvec3 a3 = smoothstep(vec3(0.0), d*3.5, barycenter);\\n\\treturn min(min(a3.x, a3.y), a3.z);\\n}\\nvoid main () {\\n\\tgl_FragDepth = gl_FragCoord.z;\\n\\tvec3 color = gl_FrontFacing ? front_color : back_color;\\n\\t// vec4 color4 = gl_FrontFacing\\n\\t// \\t? vec4(front_color, u_opacity)\\n\\t// \\t: vec4(back_color, u_opacity);\\n\\t// vec4 outline4 = vec4(outline_color, 1);\\n\\t// outColor = vec4(mix(vec3(0.0), color, edgeFactor(barycentric)), u_opacity);\\n\\toutColor = vec4(mix(outline_color, color, edgeFactor(barycentric)), u_opacity);\\n\\t// outColor = mix(outline4, color4, edgeFactor(barycentric));\\n}\\n\",ca=\"#version 100\\nprecision mediump float;\\nuniform float u_opacity;\\nvarying vec3 barycentric;\\nvarying vec3 front_color;\\nvarying vec3 back_color;\\nvarying vec3 outline_color;\\nvoid main () {\\n\\tvec3 color = gl_FrontFacing ? front_color : back_color;\\n\\t// vec3 boundary = vec3(0.0, 0.0, 0.0);\\n\\tvec3 boundary = outline_color;\\n\\t// gl_FragDepth = 0.5;\\n\\tgl_FragColor = any(lessThan(barycentric, vec3(0.02)))\\n\\t\\t? vec4(boundary, u_opacity)\\n\\t\\t: vec4(color, u_opacity);\\n}\\n\",na=\"#version 100\\nattribute vec3 v_position;\\nattribute vec3 v_normal;\\nuniform mat4 u_projection;\\nuniform mat4 u_modelView;\\nuniform mat4 u_matrix;\\nuniform vec3 u_frontColor;\\nuniform vec3 u_backColor;\\nvarying vec3 normal_color;\\nvarying vec3 front_color;\\nvarying vec3 back_color;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 1);\\n\\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\\n\\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\\n\\tfront_color = u_frontColor * brightness;\\n\\tback_color = u_backColor * brightness;\\n}\\n\",ia=\"#version 100\\nattribute vec3 v_position;\\nattribute vec3 v_color;\\nattribute vec3 edge_vector;\\nattribute vec2 vertex_vector;\\nuniform mat4 u_matrix;\\nuniform mat4 u_projection;\\nuniform mat4 u_modelView;\\nuniform float u_strokeWidth;\\nvarying vec3 blend_color;\\nvoid main () {\\n\\tvec3 edge_norm = normalize(edge_vector);\\n\\t// axis most dissimilar to edge_vector\\n\\tvec3 absNorm = abs(edge_norm);\\n\\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\\n\\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\\n\\t// two perpendiculars. with edge_vector these make basis vectors\\n\\tvec3 one = cross(axis, edge_norm);\\n\\tvec3 two = cross(one, edge_norm);\\n\\tvec3 displaceNormal = normalize(\\n\\t\\tone * vertex_vector.x + two * vertex_vector.y\\n\\t);\\n\\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\\n\\tgl_Position = u_matrix * vec4(v_position + displace, 1);\\n\\tblend_color = v_color;\\n}\\n\",la=\"#version 100\\nprecision mediump float;\\nuniform float u_opacity;\\nvarying vec3 front_color;\\nvarying vec3 back_color;\\nvoid main () {\\n\\tvec3 color = gl_FrontFacing ? front_color : back_color;\\n\\tgl_FragColor = vec4(color, u_opacity);\\n}\\n\",fa=\"#version 300 es\\n#ifdef GL_FRAGMENT_PRECISION_HIGH\\n  precision highp float;\\n#else\\n  precision mediump float;\\n#endif\\nin vec3 blend_color;\\nout vec4 outColor;\\n \\nvoid main() {\\n\\toutColor = vec4(blend_color.rgb, 1);\\n}\\n\",da=\"#version 100\\nattribute vec3 v_position;\\nattribute vec3 v_normal;\\nattribute vec3 v_barycentric;\\nuniform mat4 u_projection;\\nuniform mat4 u_modelView;\\nuniform mat4 u_matrix;\\nuniform vec3 u_frontColor;\\nuniform vec3 u_backColor;\\nuniform vec3 u_outlineColor;\\nvarying vec3 normal_color;\\nvarying vec3 barycentric;\\nvarying vec3 front_color;\\nvarying vec3 back_color;\\nvarying vec3 outline_color;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 1);\\n\\tbarycentric = v_barycentric;\\n\\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\\n\\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\\n\\tfront_color = u_frontColor * brightness;\\n\\tback_color = u_backColor * brightness;\\n\\toutline_color = u_outlineColor;\\n}\\n\",_a=\"#version 300 es\\nuniform mat4 u_modelView;\\nuniform mat4 u_matrix;\\nuniform vec3 u_frontColor;\\nuniform vec3 u_backColor;\\nuniform vec3 u_outlineColor;\\nin vec3 v_position;\\nin vec3 v_normal;\\nin vec3 v_barycentric;\\nin float v_rawEdge;\\nout vec3 front_color;\\nout vec3 back_color;\\nout vec3 outline_color;\\nout vec3 barycentric;\\n// flat out int rawEdge;\\nflat out int provokedVertex;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 1);\\n\\tprovokedVertex = gl_VertexID;\\n\\tbarycentric = v_barycentric;\\n\\t// rawEdge = int(v_rawEdge);\\n\\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\\n\\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\\n\\tfront_color = u_frontColor * brightness;\\n\\tback_color = u_backColor * brightness;\\n\\toutline_color = u_outlineColor;\\n}\\n\",ma=\"#version 300 es\\n#ifdef GL_FRAGMENT_PRECISION_HIGH\\n  precision highp float;\\n#else\\n  precision mediump float;\\n#endif\\nuniform float u_opacity;\\nin vec3 front_color;\\nin vec3 back_color;\\nout vec4 outColor;\\nvoid main () {\\n\\tgl_FragDepth = gl_FragCoord.z;\\n\\tvec3 color = gl_FrontFacing ? front_color : back_color;\\n\\toutColor = vec4(color, u_opacity);\\n}\\n\",ga=\"#version 100\\nprecision mediump float;\\nvarying vec3 blend_color;\\nvoid main () {\\n\\tgl_FragColor = vec4(blend_color.rgb, 1);\\n}\\n\";var va=Object.freeze({__proto__:null,model_100_frag:la,model_100_vert:na,model_300_frag:ma,model_300_vert:sa,outlined_model_100_frag:ca,outlined_model_100_vert:da,outlined_model_300_frag:oa,outlined_model_300_vert:_a,simple_100_frag:ga,simple_300_frag:fa,thick_edges_100_vert:ia,thick_edges_300_vert:aa});const foldedFormFaces=(e,t=1,r={},s={})=>{const a=prepareForRendering(r,s),o=1===t?createProgram(e,na,la):createProgram(e,sa,ma);return{program:o,vertexArrays:makeFoldedVertexArrays(e,o,a,s),elementArrays:makeFoldedElementArrays(e,t,a),flags:[e.DEPTH_TEST],makeUniforms:makeUniforms$1}},foldedFormFacesOutlined=(e,t=1,r={},s={})=>{const a=prepareForRendering(r,s),o=1===t?createProgram(e,da,ca):createProgram(e,_a,oa);return{program:o,vertexArrays:makeFoldedVertexArrays(e,o,a,s),elementArrays:makeFoldedElementArrays(e,t,a),flags:[e.DEPTH_TEST],makeUniforms:makeUniforms$1}},foldedFormEdges=(e,t=1,r={},s={})=>{const a=1===t?createProgram(e,ia,ga):createProgram(e,aa,fa);return{program:a,vertexArrays:makeThickEdgesVertexArrays(e,a,r,s),elementArrays:makeThickEdgesElementArrays(e,t,r),flags:[e.DEPTH_TEST],makeUniforms:makeUniforms$1}};var pa={...ta,...ea,...Object.freeze({__proto__:null,foldedForm:(e,t=1,r={},s={})=>{const a=[];return!1!==s.faces&&(!1===s.outlines?a.push(foldedFormFaces(e,t,r,s)):a.push(foldedFormFacesOutlined(e,t,r,s))),!0===s.edges&&a.push(foldedFormEdges(e,t,r,s)),a},foldedFormEdges:foldedFormEdges,foldedFormFaces:foldedFormFaces,foldedFormFacesOutlined:foldedFormFacesOutlined}),...va,...ra};const make2D=e=>e.map((e=>[0,1].map((t=>e[t]||0)))),makeCPEdgesVertexData=(e,t)=>{if(!e||!e.vertices_coords||!e.edges_vertices)return;const r={...t&&t.dark?Hs:Js,...t},s=make2D(e.edges_vertices.flatMap((t=>t.map((t=>e.vertices_coords[t])))).flatMap((e=>[e,e]))),a=make2D(makeEdgesVector(e));return{vertices_coords:s,vertices_color:e.edges_assignment?e.edges_assignment.flatMap((e=>[r[e],r[e],r[e],r[e]])):e.edges_vertices.flatMap((()=>[r.U,r.U,r.U,r.U])),verticesEdgesVector:a.flatMap((e=>[e,e,e,e])),vertices_vector:e.edges_vertices.flatMap((()=>[[1,0],[-1,0],[-1,0],[1,0]]))}};var ua=Object.freeze({__proto__:null,makeCPEdgesVertexData:makeCPEdgesVertexData});const makeCPEdgesVertexArrays=(e,t,r,s)=>{if(!r||!r.vertices_coords||!r.edges_vertices)return[];const{vertices_coords:a,vertices_color:o,verticesEdgesVector:c,vertices_vector:n}=makeCPEdgesVertexData(r,s);return a?[{location:e.getAttribLocation(t,\"v_position\"),buffer:e.createBuffer(),type:e.FLOAT,length:2,data:new Float32Array(a.flat())},{location:e.getAttribLocation(t,\"v_color\"),buffer:e.createBuffer(),type:e.FLOAT,length:o.length?o[0].length:2,data:new Float32Array(o.flat())},{location:e.getAttribLocation(t,\"edge_vector\"),buffer:e.createBuffer(),type:e.FLOAT,length:c.length?c[0].length:2,data:new Float32Array(c.flat())},{location:e.getAttribLocation(t,\"vertex_vector\"),buffer:e.createBuffer(),type:e.FLOAT,length:n.length?n[0].length:2,data:new Float32Array(n.flat())}].filter((e=>-1!==e.location)):[]},makeCPEdgesElementArrays=(e,t=1,r={})=>{if(!r||!r.edges_vertices)return[];const s=r.edges_vertices.map(((e,t)=>4*t)).flatMap((e=>[e+0,e+1,e+2,e+2,e+3,e+0]));return[{mode:e.TRIANGLES,buffer:e.createBuffer(),data:2===t?new Uint32Array(s):new Uint16Array(s)}]},makeCPFacesVertexArrays=(e,t,r)=>r&&r.vertices_coords?[{location:e.getAttribLocation(t,\"v_position\"),buffer:e.createBuffer(),type:e.FLOAT,length:2,data:new Float32Array(r.vertices_coords.flatMap(resize2))}].filter((e=>-1!==e.location)):[],makeCPFacesElementArrays=(e,t=1,r={})=>r&&r.vertices_coords&&r.faces_vertices?[{mode:e.TRIANGLES,buffer:e.createBuffer(),data:2===t?new Uint32Array(triangulateConvexFacesVertices(r).flat()):new Uint16Array(triangulateConvexFacesVertices(r).flat())}]:[];var ha=Object.freeze({__proto__:null,makeCPEdgesElementArrays:makeCPEdgesElementArrays,makeCPEdgesVertexArrays:makeCPEdgesVertexArrays,makeCPFacesElementArrays:makeCPFacesElementArrays,makeCPFacesVertexArrays:makeCPFacesVertexArrays});const makeUniforms=({projectionMatrix:e,modelViewMatrix:t,cpColor:r,strokeWidth:s})=>({u_matrix:{func:\"uniformMatrix4fv\",value:multiplyMatrices4(e||Ir,t||Ir)},u_projection:{func:\"uniformMatrix4fv\",value:e||Ir},u_modelView:{func:\"uniformMatrix4fv\",value:t||Ir},u_cpColor:{func:\"uniform3fv\",value:parseColorToWebGLColor(r||\"white\")},u_strokeWidth:{func:\"uniform1f\",value:s||.05}});var ba=Object.freeze({__proto__:null,makeUniforms:makeUniforms});const ya=\"#version 300 es\\n#ifdef GL_FRAGMENT_PRECISION_HIGH\\n  precision highp float;\\n#else\\n  precision mediump float;\\n#endif\\nin vec3 blend_color;\\nout vec4 outColor;\\nvoid main() {\\n\\toutColor = vec4(blend_color.rgb, 1);\\n}\\n\",Ea=\"#version 100\\nprecision mediump float;\\nvarying vec3 blend_color;\\nvoid main () {\\n\\tgl_FragColor = vec4(blend_color.rgb, 1);\\n}\\n\",Ma=\"#version 300 es\\nuniform mat4 u_matrix;\\nuniform float u_strokeWidth;\\nin vec2 v_position;\\nin vec3 v_color;\\nin vec2 edge_vector;\\nin vec2 vertex_vector;\\nout vec3 blend_color;\\nvoid main () {\\n\\tfloat sign = vertex_vector[0];\\n\\tfloat halfWidth = u_strokeWidth * 0.5;\\n\\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\\n\\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\\n\\tblend_color = v_color;\\n}\\n\",Aa=\"#version 100\\nuniform mat4 u_matrix;\\nuniform float u_strokeWidth;\\nattribute vec2 v_position;\\nattribute vec3 v_color;\\nattribute vec2 edge_vector;\\nattribute vec2 vertex_vector;\\nvarying vec3 blend_color;\\nvoid main () {\\n\\tfloat sign = vertex_vector[0];\\n\\tfloat halfWidth = u_strokeWidth * 0.5;\\n\\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\\n\\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\\n\\tblend_color = v_color;\\n}\\n\",xa=\"#version 100\\nuniform mat4 u_matrix;\\nuniform vec3 u_cpColor;\\nattribute vec2 v_position;\\nvarying vec3 blend_color;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 0, 1);\\n\\tblend_color = u_cpColor;\\n}\\n\",Oa=\"#version 300 es\\nuniform mat4 u_matrix;\\nuniform vec3 u_cpColor;\\nin vec2 v_position;\\nout vec3 blend_color;\\nvoid main () {\\n\\tgl_Position = u_matrix * vec4(v_position, 0, 1);\\n\\tblend_color = u_cpColor;\\n}\\n\";var ja=Object.freeze({__proto__:null,cp_100_frag:Ea,cp_100_vert:xa,cp_300_frag:ya,cp_300_vert:Oa,thick_edges_100_vert:Aa,thick_edges_300_vert:Ma});const cpFacesV1=(e,t={},r=void 0)=>{const s=createProgram(e,xa,Ea);return{program:s,vertexArrays:makeCPFacesVertexArrays(e,s,t),elementArrays:makeCPFacesElementArrays(e,1,t),flags:[],makeUniforms:makeUniforms}},cpEdgesV1=(e,t={},r=void 0)=>{const s=createProgram(e,Aa,Ea);return{program:s,vertexArrays:makeCPEdgesVertexArrays(e,s,t,r),elementArrays:makeCPEdgesElementArrays(e,1,t),flags:[],makeUniforms:makeUniforms}},cpFacesV2=(e,t={},r=void 0)=>{const s=createProgram(e,Oa,ya);return{program:s,vertexArrays:makeCPFacesVertexArrays(e,s,t),elementArrays:makeCPFacesElementArrays(e,2,t),flags:[],makeUniforms:makeUniforms}},cpEdgesV2=(e,t={},r=void 0)=>{const s=createProgram(e,Ma,ya);return{program:s,vertexArrays:makeCPEdgesVertexArrays(e,s,t,r),elementArrays:makeCPEdgesElementArrays(e,2,t),flags:[],makeUniforms:makeUniforms}};var wa={...Ks,...pa,...{...ha,...ua,...Object.freeze({__proto__:null,cpEdgesV1:cpEdgesV1,cpEdgesV2:cpEdgesV2,cpFacesV1:cpFacesV1,cpFacesV2:cpFacesV2,creasePattern:(e,t=1,r={},s=void 0)=>1===t?[cpFacesV1(e,r,s),cpEdgesV1(e,r,s)]:[cpFacesV2(e,r,s),cpEdgesV2(e,r,s)]}),...ja,...ba}};const constraints3DFaceClusters=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i=p)=>{const{planes_transform:l,faces_winding:d,faces_plane:_,faces_cluster:m,clusters_plane:g,clusters_faces:v}=getCoplanarAdjacentOverlappingFaces({vertices_coords:e,faces_vertices:o,faces_faces:n},i),u=g.map((e=>l[e])),h=v.map((i=>subgraphWithFaces({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i))),b=e.map(resize3);h.forEach((({vertices_coords:e},t)=>{h[t].vertices_coords=e.map(((e,r)=>multiplyMatrix4Vector3(u[t],b[r]))).map((([e,t])=>[e,t]))}));const y=mergeArraysWithHoles(...h.map((e=>makeFacesPolygon(e,i)))),E=y.map((e=>average2(...e)));h.forEach((({faces_vertices:e},t)=>{h[t].faces_center=e.map(((e,t)=>E[t]))})),d.map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e)).forEach((e=>y[e].reverse()));const M=mergeArraysWithHoles(...h.map((e=>getFacesFacesOverlap(e,i)))),A=connectedComponentsPairs(M).map((e=>e.join(\" \")));return{planes_transform:l,faces_plane:_,faces_cluster:m,faces_winding:d,faces_polygon:y,faces_center:E,clusters_faces:v,clusters_graph:h,clusters_transform:u,facesFacesOverlap:M,facePairs:A}};var ka=Object.freeze({__proto__:null,constraints3DFaceClusters:constraints3DFaceClusters});const Fa=[\"taco_taco\",\"taco_tortilla\",\"tortilla_tortilla\",\"transitivity\"],emptyCategoryObject=()=>({taco_taco:void 0,taco_tortilla:void 0,tortilla_tortilla:void 0,transitivity:void 0}),Sa={taco_taco:e=>[[e[0],e[2]],[e[1],e[3]],[e[1],e[2]],[e[0],e[3]],[e[0],e[1]],[e[2],e[3]]],taco_tortilla:e=>[[e[0],e[2]],[e[0],e[1]],[e[1],e[2]]],tortilla_tortilla:e=>[[e[0],e[2]],[e[1],e[3]]],transitivity:e=>[[e[0],e[1]],[e[1],e[2]],[e[2],e[0]]]},sortedPairString=e=>e[0]<e[1]?`${e[0]} ${e[1]}`:`${e[1]} ${e[0]}`,Ca={taco_taco:e=>[sortedPairString([e[0],e[2]]),sortedPairString([e[1],e[3]]),sortedPairString([e[1],e[2]]),sortedPairString([e[0],e[3]]),sortedPairString([e[0],e[1]]),sortedPairString([e[2],e[3]])],taco_tortilla:e=>[sortedPairString([e[0],e[2]]),sortedPairString([e[0],e[1]]),sortedPairString([e[1],e[2]])],tortilla_tortilla:e=>[sortedPairString([e[0],e[2]]),sortedPairString([e[1],e[3]])],transitivity:e=>[sortedPairString([e[0],e[1]]),sortedPairString([e[1],e[2]]),sortedPairString([e[2],e[0]])]},Va={0:0,1:1,2:-1},solverSolutionToFaceOrders=(e,t)=>{const r=Object.keys(e),s=r.map((e=>e.split(\" \").map((e=>parseInt(e,10))))),a=s.map(((s,a)=>{const o=Va[e[r[a]]];return t[s[1]]?o:-o}));return s.map((([e,t],r)=>[e,t,a[r]]))},mergeWithoutOverwrite=e=>{const t={};return e.forEach((e=>Object.keys(e).forEach((r=>{if(void 0!==t[r]&&t[r]!==e[r])throw new Error(`two competing results: ${t[r]}, ${e[r]}, for \"${r}\"`);t[r]=e[r]})))),t};var za=Object.freeze({__proto__:null,constraintToFacePairs:Sa,constraintToFacePairsStrings:Ca,emptyCategoryObject:emptyCategoryObject,mergeWithoutOverwrite:mergeWithoutOverwrite,solverSolutionToFaceOrders:solverSolutionToFaceOrders,tacoTypeNames:Fa});const getOverlapFacesWith3DEdge=({edges_faces:e},{clusters_graph:t,faces_plane:r},s=p)=>{const a=e.map((e=>2===e.length&&r[e[0]]!==r[e[1]])),o=t.map((e=>({vertices_coords:e.vertices_coords,edges_vertices:e.edges_vertices,faces_vertices:e.faces_vertices,faces_edges:e.faces_edges}))).map((e=>getFacesEdgesOverlap(e,s))).map((e=>e.map((e=>e.filter((e=>a[e])))))),c=o.flatMap((t=>t.flatMap(((t,s)=>t.map((t=>({edge:t,faces:e[t],facesPlanes:e[t].map((e=>r[e])),tortilla:s,tortillaPlane:r[s]})))))));return c.map((({edge:e,faces:t,facesPlanes:r,tortilla:s,tortillaPlane:a})=>({edge:e,tortilla:s,coplanar:t.filter(((e,t)=>r[t]===a)).shift(),angled:t.filter(((e,t)=>r[t]!==a)).shift()})))},solveOverlapFacesWith3DEdge=({edges_foldAngle:e},t,r)=>{const s=t.map((({tortilla:e,coplanar:t})=>[e,t])),a=s.map((([e,t])=>e<t));s.map(((e,t)=>t)).filter((e=>!a[e])).forEach((e=>s[e].reverse()));const o=s.map((e=>e.join(\" \"))),c=t.map((({edge:t})=>e[t])).map(Math.sign).map((e=>1===e)),n=t.map((({coplanar:e})=>r[e])),i=c.map(((e,t)=>!(e!==n[t]))).map((e=>e?1:0)).map(((e,t)=>a[t]?e:1-e)).map((e=>e+1));return mergeWithoutOverwrite(o.map(((e,t)=>({[e]:i[t]}))))},solveFacePair3D=({edges_foldAngle:e,faces_winding:t},r,s)=>{const a=s.map((e=>t[e])),o=r.map((t=>e[t])).map(((e,t)=>a[t]?e:-e)),c=o[0]>o[1],n=s[0]<s[1],i=c!==n?2:1,l=n?s.join(\" \"):[s[1],s[0]].join(\" \");return{[l]:i}},getSolvable3DEdgePairs=({edges_faces:e,faces_plane:t,edgePairs:r,facesFacesLookup:s})=>{const a=(({edges_faces:e,faces_plane:t,facesFacesLookup:r})=>{const s=[],a=e.map(((e,t)=>t)).filter((t=>2===e[t].length));return a.filter((r=>t[e[r][0]]===t[e[r][1]])).forEach((t=>{const[a,o]=e[t];s[t]=r[a][o]?2:1})),a.filter((r=>t[e[r][0]]!==t[e[r][1]])).forEach((e=>{s[e]=0})),s})({edges_faces:e,faces_plane:t,facesFacesLookup:s}),o=r.map((r=>({faces:r.flatMap((t=>e[t])),planes:r.flatMap((r=>e[r].map((e=>t[e])))),angleClasses:r.map((e=>a[e]))}))).map((({faces:e,planes:t,angleClasses:r})=>({faces:[e[0],e[1],e[2],e[3]],planes:[t[0],t[1],t[2],t[3]],angleClasses:[r[0],r[1]]}))),c=o.map((e=>(({faces:e,planes:t,angleClasses:r},s)=>{const[a,o]=r,[c,n,i,l]=e,[d,_,m,g]=t;return a&&o?0:s[c][i]||s[c][l]||s[n][i]||s[n][l]?1===a||1===o?3:2===a||2===o?5:d===m&&_!==g&&s[c][i]||d===g&&_!==m&&s[c][l]||_===m&&d!==g&&s[n][i]||_===g&&d!==m&&s[n][l]?2:d===m&&_===g&&s[c][i]&&!s[n][l]||d===g&&_===m&&s[c][l]&&!s[n][i]||_===m&&d===g&&s[n][i]&&!s[c][l]||_===g&&d===m&&s[n][l]&&!s[c][i]?1:d===m&&_===g&&s[c][i]&&s[n][l]||d===g&&_===m&&s[c][l]&&s[n][i]||_===m&&d===g&&s[n][i]&&s[c][l]||_===g&&d===m&&s[n][l]&&s[c][i]?4:6:0})(e,s))),[,n,i,l,d,_,m]=invertFlatToArrayMap(c);return m&&m.length&&console.warn(\"getSolvable3DEdgePairs uncaught edge pairs\"),{tJunctions:n||[],yJunctions:i||[],bentFlatTortillas:l||[],bentTortillas:d||[],bentTortillasFlatTaco:_||[]}},constraints3DEdges=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_foldAngle:s},{faces_plane:a,faces_winding:o,facesFacesOverlap:c},n=p)=>{const i=t.slice();r.map(((e,t)=>t)).filter((e=>2!==r[e].length)).forEach((e=>delete i[e]));const l=getEdgesEdgesCollinearOverlap({vertices_coords:e,edges_vertices:i},n),d=connectedComponentsPairs(l),_=arrayArrayToLookupArray(c),{tJunctions:m,yJunctions:g,bentFlatTortillas:v,bentTortillas:u,bentTortillasFlatTaco:h}=getSolvable3DEdgePairs({edges_faces:r,faces_plane:a,edgePairs:d,facesFacesLookup:_}),b=u.map((e=>d[e])).map((e=>{const t=e.flatMap((e=>r[e])),s=[t[0],t[1],t[2],t[3]];return a[s[0]]!==a[s[2]]&&([s[2],s[3]]=[s[3],s[2]]),o[s[0]]!==o[s[1]]&&([s[1],s[3]]=[s[3],s[1]]),s})),y=h.map((e=>d[e])).map((e=>{const[t,s,a,o]=e.flatMap((e=>r[e])),c=[[t,a,s],[a,s,o],[a,t,o],[t,o,s]].filter((([e,t,r])=>_[e][t]&&_[e][r])).shift();return c?[c[0],c[1],c[2]]:void 0})).filter((e=>void 0!==e)),E=[...m,...g,...v].map((e=>d[e])).map((e=>{const[t,a,c,n]=e.flatMap((e=>r[e])),i=[[t,c],[t,n],[a,c],[a,n]].filter((([e,t])=>_[e][t])).shift();if(i)return solveFacePair3D({edges_foldAngle:s,faces_winding:o},e,i)}));return{orders:mergeWithoutOverwrite(E),tortilla_tortilla:b,taco_tortilla:y}};var Ta=Object.freeze({__proto__:null,constraints3DEdges:constraints3DEdges,getOverlapFacesWith3DEdge:getOverlapFacesWith3DEdge,getSolvable3DEdgePairs:getSolvable3DEdgePairs,solveFacePair3D:solveFacePair3D,solveOverlapFacesWith3DEdge:solveOverlapFacesWith3DEdge});const makeEdgesFacesSide=({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_center:s})=>{const a=t.map((t=>t.map((t=>e[t])))).map((([e,t])=>pointsToLine(e,t)));return r.map(((e,t)=>e.map((e=>cross2(subtract2(s[e],a[t].origin),a[t].vector))).map((e=>Math.sign(e)))))},makeEdgePairsFacesSide=({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_center:s},a)=>{const o=a.map((([r])=>edgeToLine2({vertices_coords:e,edges_vertices:t},r)));return a.map((e=>e.map((e=>r[e])))).map(((e,t)=>e.map((e=>e.map((e=>s[e])).map((e=>cross2(subtract2(e,o[t].origin),o[t].vector))).map(Math.sign))))).map((e=>[[e[0][0],e[0][1]],[e[1][0],e[1][1]]]))};var Pa=Object.freeze({__proto__:null,makeEdgePairsFacesSide:makeEdgePairsFacesSide,makeEdgesFacesSide:makeEdgesFacesSide,makeEdgesFacesSide2D:({vertices_coords:e,edges_faces:t,faces_vertices:r,faces_center:s},{lines:a,edges_line:o})=>(s||(s=makeFacesCenter2DQuick({vertices_coords:e,faces_vertices:r})),t.map(((e,t)=>e.map((e=>{const{vector:r,origin:c}=a[o[t]];return cross2(subtract2(s[e],c),r)})).map(Math.sign)))),makeEdgesFacesSide3D:({vertices_coords:e,edges_faces:t,faces_vertices:r,faces_center:s},{lines:a,edges_line:o,planes_transform:c,faces_plane:n})=>{s||(s=makeFacesCenter3DQuick({vertices_coords:e,faces_vertices:r}).map(((e,t)=>multiplyMatrix4Vector3(c[n[t]],e))));const i=invertFlatToArrayMap(o).map((e=>e.flatMap((e=>t[e].map((e=>n[e])))))).map((e=>uniqueElements(e))),l=a.map((({vector:e,origin:t})=>({vector:resize3(e),origin:resize3(t)}))),d=a.map((()=>[]));return i.forEach(((e,t)=>e.forEach((e=>{const{vector:r,origin:s}=l[t];d[t][e]=multiplyMatrix4Line3(c[e],r,s)})))),t.map(((e,t)=>e.map((e=>{const{vector:r,origin:a}=d[o[t]][n[e]];return cross2(subtract2(s[e],a),r)})).map(Math.sign)))}});const classifyEdgePair=e=>{const t=e.map((([e,t])=>e===t));if(t[0]&&t[1])return e[0][0]!==e[1][0]?0:1;if(!t[0]&&!t[1])return e[0][0]===e[1][0]?2:3;const r=(t[0]?e[0][0]:e[1][0])===(t[0]?e[1]:e[0])[0]?0:1;return 4+(t[0]?0:2)+r},makeTortillaTortillaFacesCrossing=(e,t,r)=>{const s=t.map((e=>2===e.length&&e[0]!==e[1])).map(((e,t)=>e?t:void 0)).filter((e=>void 0!==e)),a=[];return s.forEach((e=>{a[e]=r[e]})),a.flatMap(((t,r)=>t.map((t=>[...e[r],t,t])))).filter((e=>4===e.length)).map((e=>[e[0],e[1],e[2],e[3]]))},makeTacosAndTortillas=({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_vertices:s,faces_edges:a,faces_center:o},c=p)=>{o||(o=makeFacesCenterQuick({vertices_coords:e,faces_vertices:s}));const n=getEdgesFacesOverlap({vertices_coords:e,edges_vertices:t,faces_vertices:s,faces_edges:a},c),i=makeEdgesFacesSide({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_center:o}),l=connectedComponentsPairs(getEdgesEdgesCollinearOverlap({vertices_coords:e,edges_vertices:t},c)).filter((e=>e.every((e=>r[e].length>1)))),d=makeEdgePairsFacesSide({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_center:o},l).map(classifyEdgePair),_=makeTortillaTortillaFacesCrossing(r,i,n),m=l.map((e=>[[r[e[0]][0],r[e[0]][1]],[r[e[1]][0],r[e[1]][1]]])),g=n.map(((e,t)=>i[t].length>1&&i[t][0]===i[t][1]?e:[])).map(((e,t)=>({taco:r[t],tortillas:e}))).filter((({tortillas:e})=>e.length)).flatMap((({taco:[e,t],tortillas:r})=>r.map((r=>[e,r,t])))).map((e=>[e[0],e[1],e[2]]));return{taco_taco:d.map(((e,t)=>1===e?t:void 0)).filter((e=>void 0!==e)).map((e=>l[e].map((e=>r[e])))).map((e=>[e[0][0],e[1][0],e[0][1],e[1][1]])),taco_tortilla:d.map(((e,t)=>e>3?t:void 0)).filter((e=>void 0!==e)).map((e=>(([e,t],r)=>{switch(r){case 4:return[e[0],t[0],e[1]];case 5:return[e[0],t[1],e[1]];case 6:return[t[0],e[0],t[1]];case 7:return[t[0],e[1],t[1]];default:return}})(m[e],d[e]))).concat(g),tortilla_tortilla:d.map(((e,t)=>2===e||3===e?t:void 0)).filter((e=>void 0!==e)).map((e=>(([e,t],r)=>{switch(r){case 2:return[...e,...t];case 3:return[...e,t[1],t[0]];default:return}})(m[e],d[e]))).concat(_)}};var $a=Object.freeze({__proto__:null,makeTacosAndTortillas:makeTacosAndTortillas,makeTortillaTortillaFacesCrossing:makeTortillaTortillaFacesCrossing});const makeTransitivity=({faces_polygon:e},t,r=p)=>{const s=t.map((()=>({})));t.forEach(((e,t)=>e.forEach((e=>{s[t][e]=!0,s[e][t]=!0}))));const a=[];t.forEach(((t,s)=>t.forEach((t=>{const o=clipPolygonPolygon(e[s],e[t],r);o&&(a[s]||(a[s]=[]),a[t]||(a[t]=[]),a[s][t]=o,a[t][s]=o)}))));const o=[];for(let t=0;t<a.length-1;t+=1)if(a[t])for(let c=t+1;c<a.length;c+=1)if(a[t][c])for(let n=c+1;n<a.length;n+=1){if(t===n||c===n)continue;if(!s[t][n]||!s[c][n])continue;if(clipPolygonPolygon(a[t][c],e[n],r)){const[e,r,s]=[t,c,n].sort(((e,t)=>e-t));o.push([e,r,s])}}return o},getTransitivityTriosFromTacos=({taco_taco:e,taco_tortilla:t})=>{const r=e.map((e=>e.slice().sort(((e,t)=>e-t)))).flatMap((([e,t,r,s])=>[[e,t,r],[e,t,s],[e,r,s],[t,r,s]])),s=t.map((e=>e.slice().sort(((e,t)=>e-t)))),a={};return r.concat(s).map((e=>e.join(\" \"))).forEach((e=>{a[e]=!0})),a};var Ba=Object.freeze({__proto__:null,getTransitivityTriosFromTacos:getTransitivityTriosFromTacos,makeTransitivity:makeTransitivity});const solveFlatAdjacentEdges=({edges_faces:e,edges_assignment:t},r)=>{const s={0:0,1:2,2:1},a={M:1,m:1,V:2,v:2},o={};return e.forEach(((e,c)=>{const n=t[c],i=a[n];if(2!==e.length||void 0===i)return;const l=r[e[0]]?i:s[i],d=e[0]<e[1],_=d?e.join(\" \"):e.slice().reverse().join(\" \"),m=d?l:s[l];o[_]=m})),o};var Na=Object.freeze({__proto__:null,solveFlatAdjacentEdges:solveFlatAdjacentEdges});const makeConstraintsLookup=e=>{const t={taco_taco:void 0,taco_tortilla:void 0,tortilla_tortilla:void 0,transitivity:void 0};return Fa.forEach((e=>{t[e]={}})),Fa.forEach((r=>e[r].forEach((e=>Ca[r](e).forEach((e=>{t[r][e]=[]})))))),Fa.forEach((r=>e[r].forEach(((e,s)=>Ca[r](e).forEach((e=>t[r][e].push(s))))))),t},makeSolverConstraintsFlat=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,faces_vertices:a,faces_edges:o,faces_center:c},n=p)=>{const i=makeFacesWinding({vertices_coords:e,faces_vertices:a}),l=makeFacesPolygon({vertices_coords:e,faces_vertices:a},n);i.map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e)).forEach((e=>l[e].reverse()));const d=getFacesFacesOverlap({vertices_coords:e,faces_vertices:a},n),{taco_taco:_,taco_tortilla:m,tortilla_tortilla:g}=makeTacosAndTortillas({vertices_coords:e,edges_vertices:t,edges_faces:r,faces_vertices:a,faces_edges:o,faces_center:c},n),v=getTransitivityTriosFromTacos({taco_taco:_,taco_tortilla:m}),u=makeTransitivity({faces_polygon:l},d,n).filter((e=>void 0===v[e.join(\" \")])),h=connectedComponentsPairs(d).map((e=>e.join(\" \")));return{constraints:{taco_taco:_,taco_tortilla:m,tortilla_tortilla:g,transitivity:u},lookup:makeConstraintsLookup({taco_taco:_,taco_tortilla:m,tortilla_tortilla:g,transitivity:u}),facePairs:h,faces_winding:i,orders:solveFlatAdjacentEdges({edges_faces:r,edges_assignment:s},i)}};var Ra=Object.freeze({__proto__:null,makeConstraintsLookup:makeConstraintsLookup,makeSolverConstraintsFlat:makeSolverConstraintsFlat});const makeSolverConstraints3D=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i=p)=>{const{faces_plane:l,faces_winding:d,faces_polygon:_,clusters_graph:m,facesFacesOverlap:v,facePairs:u}=constraints3DFaceClusters({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i);let h;try{h=constraints3DEdges({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_foldAngle:a},{faces_plane:l,faces_winding:d,facesFacesOverlap:v},i)}catch(e){throw new Error(g,{cause:e})}const{orders:b,tortilla_tortilla:y,taco_tortilla:E}=h,M=a.map(edgeFoldAngleIsFlat).map(((e,t)=>e?void 0:t)).filter((e=>void 0!==e));[\"edges_vertices\",\"edges_faces\",\"edges_assignment\",\"edges_foldAngle\"].forEach((e=>m.forEach(((t,r)=>M.forEach((t=>delete m[r][e][t]))))));const A=m.map((e=>makeTacosAndTortillas(e,i))),x=A.flatMap((e=>e.taco_taco)),O=A.flatMap((e=>e.taco_tortilla)),j=A.flatMap((e=>e.tortilla_tortilla)),w=getTransitivityTriosFromTacos({taco_taco:x,taco_tortilla:O}),F=makeTransitivity({faces_polygon:_},v,i).filter((e=>void 0===w[e.join(\" \")]));O.push(...E),j.push(...y);const S=makeConstraintsLookup({taco_taco:x,taco_tortilla:O,tortilla_tortilla:j,transitivity:F}),C=m.map((e=>solveFlatAdjacentEdges(e,d))).reduce(((e,t)=>Object.assign(e,t)),{}),V=getOverlapFacesWith3DEdge({edges_faces:r},{clusters_graph:m,faces_plane:l},i);let z,T;try{z=solveOverlapFacesWith3DEdge({edges_foldAngle:a},V,d)}catch(e){throw new Error(g,{cause:e})}try{T=mergeWithoutOverwrite([b,C,z])}catch(e){throw new Error(g,{cause:e})}return{constraints:{taco_taco:x,taco_tortilla:O,tortilla_tortilla:j,transitivity:F},lookup:S,facePairs:u,faces_winding:d,orders:T}};var La=Object.freeze({__proto__:null,makeSolverConstraints3D:makeSolverConstraints3D});const getBranchCount=({branches:e})=>e?e.map((e=>({choices:e.length,branches:e.flatMap(getBranchCount)}))):\"leaf\",getBranchStructure=({branches:e})=>void 0===e?[]:e.map((e=>e.map(getBranchStructure))),getBranchLeafStructure=({branches:e})=>void 0===e?\"leaf\":e.map((e=>e.map(getBranchLeafStructure))),gather=({orders:e,branches:t},r=[])=>[e,...(t||[]).flatMap((e=>gather(e[r.shift()||0],r)))],compile=({orders:e,branches:t},r)=>gather({orders:e,branches:t},r).flat(),gatherAll=({orders:e,branches:t})=>{if(!t)return[[e]];const r=t.map((e=>e.flatMap(gatherAll)));return((...e)=>{const t=e.reduce(((e,t)=>e*t),1),r=e.slice();for(let e=r.length-2;e>=0;e-=1)r[e]*=r[e+1];return r.push(1),r.shift(),Array.from(Array(t)).map(((t,s)=>e.map(((e,t)=>Math.floor(s/r[t])%e))))})(...r.map((e=>e.length))).map((e=>e.flatMap(((e,t)=>r[t][e])))).map((t=>[e,...t]))},compileAll=({orders:e,branches:t})=>gatherAll({orders:e,branches:t}).map((e=>e.flat())),Ia={count:function(){return getBranchCount(this)},structure:function(){return getBranchStructure(this)},leaves:function(){return getBranchLeafStructure(this)},gather:function(...e){return gather(this,e)},gatherAll:function(){return gatherAll(this)},compile:function(...e){return compile(this,e)},compileAll:function(){return compileAll(this)},faceOrders:function(...e){return compile(this,e)}};var Ua=Object.freeze({__proto__:null,LayerPrototype:Ia,compile:compile,compileAll:compileAll,gather:gather,gatherAll:gatherAll,getBranchStructure:getBranchStructure});const makeLookupEntry=e=>{const t=e[0].length,r=Array.from(Array(t+1)).map((()=>({})));Array.from(Array(2**t)).map(((e,t)=>t.toString(2))).map((e=>Array.from(e).map((e=>parseInt(e,10)+1)).join(\"\"))).map((e=>`11111${e}`.slice(-t))).forEach((e=>{r[0][e]=!1})),e.forEach((e=>{r[0][e]=!0})),Array.from(Array(t)).map(((e,t)=>t+1)).map((e=>Array.from(Array(3**t)).map(((e,t)=>t.toString(3))).map((e=>`000000${e}`.slice(-t))).forEach((t=>((e,t,r)=>{const s=Array.from(r).map((e=>parseInt(e,10)));if(s.filter((e=>0===e)).length!==t)return;e[t][r]=!1;const a=[];for(let o=0;o<s.length;o+=1){const c=[];if(0===s[o]){for(let r=1;r<=2;r+=1)s[o]=r,!1!==e[t-1][s.join(\"\")]&&c.push([o,r]);s[o]=0,c.length&&(e[t][r]=!0),1===c.length&&a.push(c[0])}}a.length&&(e[t][r]=[a[0][0],a[0][1]])})(r,e,t)))));const s=r.reduce(((e,t)=>({...e,...t})));return Object.keys(s).filter((e=>\"object\"==typeof s[e])).forEach((e=>{s[e]=Object.freeze(s[e])})),Object.freeze(s)},Da={taco_taco:makeLookupEntry([\"111112\",\"111121\",\"111222\",\"112111\",\"121112\",\"121222\",\"122111\",\"122212\",\"211121\",\"211222\",\"212111\",\"212221\",\"221222\",\"222111\",\"222212\",\"222221\"]),taco_tortilla:makeLookupEntry([\"112\",\"121\",\"212\",\"221\"]),tortilla_tortilla:makeLookupEntry([\"11\",\"22\"]),transitivity:makeLookupEntry([\"112\",\"121\",\"122\",\"211\",\"212\",\"221\"])};var Qa=Object.freeze({__proto__:null,table:Da});const buildRuleAndLookup=(e,t,...r)=>{const s={0:0,1:2,2:1},a=Sa[e](t),o=a.map((e=>e[1]<e[0])),c=a.map(((e,t)=>o[t]?`${e[1]} ${e[0]}`:`${e[0]} ${e[1]}`)),n=c.map((e=>r.find((t=>t[e])))).map(((e,t)=>void 0===e?0:e[c[t]])).map(((e,t)=>o[t]?s[e]:e)).join(\"\");if(!0===Da[e][n]||!1===Da[e][n])return Da[e][n];const[i,l]=Da[e][n];return[c[i],o[i]?s[l]:l]},getConstraintIndicesFromFacePairs=(e,t,r)=>{const s={taco_taco:void 0,taco_tortilla:void 0,tortilla_tortilla:void 0,transitivity:void 0};return Fa.forEach((a=>{const o=r.flatMap((e=>t[a][e]));s[a]=uniqueElements(o).filter((t=>e[a][t]))})),s},propagate=(e,t,r,...s)=>{let a=r;const o={};do{const r=getConstraintIndicesFromFacePairs(e,t,a),c={};for(let t=0;t<Fa.length;t+=1){const a=Fa[t],n=r[a];for(let t=0;t<n.length;t+=1){const r=buildRuleAndLookup(a,e[a][n[t]],...s,o);if(!0!==r){if(!1===r)throw new Error(`invalid ${a} ${n[t]}:${e[a][n[t]]}`);if(o[r[0]]){if(o[r[0]]!==r[1])throw new Error(`conflict ${a} ${n[t]}:${e[a][n[t]]}`)}else{const[e,t]=r;c[e]=!0,o[r[0]]=t}}}}a=Object.keys(c)}while(a.length);return o},getBranches=(e,t,r)=>{const s={};e.forEach((e=>{s[e]=!0}));let a=0;const o=[];for(;a<e.length;){if(!s[e[a]]){a+=1;continue}const c=[],n=[e[a]],i={[e[a]]:!0};do{const e=n.pop();delete s[e],c.push(e);const a={};Fa.forEach((s=>{const o=r[s][e];o&&o.map((e=>t[s][e])).map((e=>Ca[s](e).forEach((e=>{a[e]=!0}))))}));const o=Object.keys(a).filter((e=>s[e])).filter((e=>!i[e]));n.push(...o),o.forEach((e=>{i[e]=!0}))}while(n.length);a+=1,o.push(c)}return o},solveBranch$1=(e,t,r,...s)=>{const a=r[0];return[1,2].map((r=>{const o={[a]:r};try{const r=propagate(e,t,[a],...s,o);return Object.assign(r,o)}catch(e){return}})).filter((e=>void 0!==e)).map((a=>Object.keys(a).length===r.length?{orders:a}:{orders:a,branches:getBranches(r.filter((e=>!(e in a))),e,t).map((r=>solveBranch$1(e,t,r,...s,a)))}))},solver$1=({constraints:e,lookup:t,facePairs:r,orders:s})=>{let a;try{a=propagate(e,t,Object.keys(s),s)}catch(e){throw new Error(g,{cause:e})}const o={...s,...a},c=r.filter((e=>!(e in o)));try{return 0===c.length?{orders:o}:{orders:o,branches:getBranches(c,e,t).map((r=>solveBranch$1(e,t,r,s,a)))}}catch(e){throw new Error(g,{cause:e})}};var Wa=Object.freeze({__proto__:null,solver:solver$1});const solveBranch=(e,t,r,...s)=>{if(!r.length)return[];const a=r[0],o=[1,2].map((r=>{const o={[a]:r};try{const r=propagate(e,t,[a],...s,o);return Object.assign(r,o)}catch(e){return}})).filter((e=>void 0!==e)),c=o.map((e=>Object.keys(e).length)),n=o.filter(((e,t)=>c[t]===r.length)),i=o.filter(((e,t)=>c[t]!==r.length)).map((a=>solveBranch(e,t,r.filter((e=>!(e in a))),...s,a)));return n.map((e=>[...s,e])).concat(...i)},solver=({constraints:e,lookup:t,facePairs:r,orders:s})=>{let a;try{a=propagate(e,t,Object.keys(s),s)}catch(e){throw new Error(g,{cause:e})}const o=r.filter((e=>!(e in s))).filter((e=>!(e in a)));let c;try{c=getBranches(o,e,t).map((r=>solveBranch(e,t,r,s,a)))}catch(e){throw new Error(g,{cause:e})}const n={...s,...a};c.forEach((e=>e.forEach((e=>e.splice(0,2)))));return{root:n,branches:c.map((e=>e.map((e=>Object.assign({},...e)))))}},layerSolutionToFaceOrdersTree=({orders:e,branches:t},r)=>void 0===t?{orders:solverSolutionToFaceOrders(e,r)}:{orders:solverSolutionToFaceOrders(e,r),branches:t.map((e=>e.map((e=>layerSolutionToFaceOrdersTree(e,r)))))},solveLayerOrders=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n,edges_vector:i},l)=>{if(!e||!t||!o)return{orders:{},faces_winding:[]};c||(c=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:o})),r||(r=makeEdgesFacesUnsorted({edges_vertices:t,faces_vertices:o,faces_edges:c})),n||(n=makeFacesFaces({faces_vertices:o})),!a&&s&&(a=makeEdgesFoldAngle({edges_assignment:s})),s||(s=makeEdgesAssignmentSimple({edges_foldAngle:a})),void 0===l&&(l=makeEpsilon({vertices_coords:e,edges_vertices:t}));const{constraints:d,lookup:_,facePairs:m,faces_winding:g,orders:v}=makeSolverConstraintsFlat({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n,edges_vector:i},l);return{...solver$1({constraints:d,lookup:_,facePairs:m,orders:v}),faces_winding:g}},solveLayerOrders3D=({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i)=>{if(!e||!t||!o)return{orders:{},faces_winding:[]};c||(c=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:o})),r||(r=makeEdgesFacesUnsorted({edges_vertices:t,faces_vertices:o,faces_edges:c})),n||(n=makeFacesFaces({faces_vertices:o})),!a&&s&&(a=makeEdgesFoldAngle({edges_assignment:s})),s||(s=makeEdgesAssignmentSimple({edges_foldAngle:a})),void 0===i&&(i=makeEpsilon({vertices_coords:e,edges_vertices:t}));const{constraints:l,lookup:d,facePairs:_,faces_winding:m,orders:g}=makeSolverConstraints3D({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,edges_foldAngle:a,faces_vertices:o,faces_edges:c,faces_faces:n},i);return{...solver$1({constraints:l,lookup:d,facePairs:_,orders:g}),faces_winding:m}},solveFaceOrders=(e,t)=>{const{faces_winding:r,...s}=solveLayerOrders(e,t);return layerSolutionToFaceOrdersTree(s,r)},solveFaceOrders3D=(e,t)=>{const{faces_winding:r,...s}=solveLayerOrders3D(e,t);return layerSolutionToFaceOrdersTree(s,r)};var qa=Object.freeze({__proto__:null,solveFaceOrders:solveFaceOrders,solveFaceOrders3D:solveFaceOrders3D,solveLayerOrders:solveLayerOrders,solveLayerOrders3D:solveLayerOrders3D,solveLayerOrdersSingleBranches:({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,faces_vertices:a,faces_edges:o,edges_vector:c},n)=>{if(!e||!t||!a)return{orders:{},faces_winding:[]};o||(o=makeFacesEdgesFromVertices({edges_vertices:t,faces_vertices:a})),r||(r=makeEdgesFacesUnsorted({edges_vertices:t,faces_vertices:a,faces_edges:o})),void 0===n&&(n=makeEpsilon({vertices_coords:e,edges_vertices:t}));const{constraints:i,lookup:l,facePairs:d,faces_winding:_,orders:m}=makeSolverConstraintsFlat({vertices_coords:e,edges_vertices:t,edges_faces:r,edges_assignment:s,faces_vertices:a,faces_edges:o,edges_vector:c},n);return{...solver({constraints:i,lookup:l,facePairs:d,orders:m}),faces_winding:_}}});const Ga={...La,...ka,...Ta,...Ra,...Pa,...za,...Na,...Ua,...qa,...Wa,...Qa,...$a,...Ba,layer3D:(e,t)=>Object.assign(Object.create(Ia),solveFaceOrders3D(e,t))},Ha=Object.assign(((e,t)=>Object.assign(Object.create(Ia),solveFaceOrders(e,t))),Ga),perpendicularBalancedSegment=(e,t,r)=>{const s=void 0===r?((e,t)=>{const r=clipLineConvexPolygon(e,t);return void 0===r?void 0:midpoint2(r[0],r[1])})(e,t):r,a=rotate90(t.vector),o=clipLineConvexPolygon(e,{vector:a,origin:s});if(!o)return;const c=o.map((e=>distance2(s,e))).sort(((e,t)=>e-t)).shift(),n=scale2$1(normalize2(a),c);return[add2(s,flip2(n)),add2(s,n)]},betweenTwoSegments=(e,t,r)=>{const s=r.map((e=>midpoint2(e[0],e[1]))),a=midpoint2(s[0],s[1]),o={vector:rotate90(e.vector),origin:a};return t.map((e=>intersectLineLine(e,o).point))},arrowFromSegment=(e,t={})=>{if(void 0===e)return;const r=subtract2(e[1],e[0]),s=magnitude2(r),a=t.padding?t.padding:.05*s,o=dot2(r,[1,0]),c=t&&t.vmin?t.vmin:s;return{segment:[e[0],e[1]],head:{width:.0666*c,height:.1*c},bend:o>0?.3:-.3,padding:a}},arrowFromSegmentInPolygon=(e,t,r={})=>{const s=r.vmin?r.vmin:Math.min(...(boundingBox$1(e)?.span||[1,1]).slice(0,2));return arrowFromSegment(t,{...r,vmin:s})},foldLineArrow=({vertices_coords:e},t,r)=>{const s=e.map(resize2),a=convexHull(s).map((e=>s[e])),o=perpendicularBalancedSegment(a,t);if(void 0!==o)return arrowFromSegmentInPolygon(a,o,r)};var Ja=Object.freeze({__proto__:null,arrowFromLine:(e,t,r)=>{const s=clipLineConvexPolygon(e,t);return void 0===s?void 0:arrowFromSegmentInPolygon(e,s,r)},arrowFromSegment:arrowFromSegment,arrowFromSegmentInPolygon:arrowFromSegmentInPolygon,foldLineArrow:foldLineArrow});const diagramReflectPoint=({vector:e,origin:t},r)=>multiplyMatrix2Vector2(makeMatrix2Reflect(e,t),r);var Za=Object.freeze({__proto__:null,axiom1Arrows:({vertices_coords:e},t,r,s)=>{const a=e.map(resize2),o=convexHull(a).map((e=>a[e]));return axiom1(t,r).map((e=>perpendicularBalancedSegment(o,e))).map((e=>arrowFromSegmentInPolygon(o,e,s)))},axiom2Arrows:({vertices_coords:e},t,r,s)=>{const a=e.map(resize2),o=convexHull(a).map((e=>a[e]));return[arrowFromSegmentInPolygon(o,[t,r],s)]},axiom3Arrows:({vertices_coords:e},t,r,s)=>{const a=e.map(resize2),o=convexHull(a).map((e=>a[e])),c=axiom3(t,r),n=[t,r].map((e=>clipLineConvexPolygon(o,e))).filter((e=>void 0!==e));if(2!==n.length)return c.map((t=>foldLineArrow({vertices_coords:e},t,s)));const[i,l]=n.map((([e,t])=>pointsToLine2(e,t))),d=intersectLineLine(i,l,includeS,includeS).point,_=d?c.map((e=>((e,t,r,s)=>{const a=e.map((e=>e.vector)),o=a.map(flip2),c=a.concat(o).map((e=>({vector:e,origin:t}))),n=c.map((e=>dot2(e.vector,r.vector))),i=c.map((e=>cross2(e.vector,r.vector))),l=c.filter(((e,t)=>n[t]>0&&i[t]>0)).shift(),d=c.filter(((e,t)=>n[t]>0&&i[t]<0)).shift(),_=c.filter(((e,t)=>n[t]<0&&i[t]>0)).shift(),m=c.filter(((e,t)=>n[t]<0&&i[t]<0)).shift(),g=[l,d,_,m].map((e=>clipLineConvexPolygon(s,e,includeS,A)));if(g.includes(void 0))return;const v=g.map((e=>e.shift())),p=v.map((e=>distance2(e,t)));return[[p[0]<p[1]?v[0]:v[1],p[0]<p[1]?add2(d.origin,scale2$1(normalize2(d.vector),p[0])):add2(l.origin,scale2$1(normalize2(l.vector),p[1]))],[p[2]<p[3]?v[2]:v[3],p[2]<p[3]?add2(m.origin,scale2$1(normalize2(m.vector),p[2])):add2(_.origin,scale2$1(normalize2(_.vector),p[3]))]]})([t,r],d,e,o))):[betweenTwoSegments(c.filter((e=>void 0!==e)).shift(),[t,r],n)];return _.map((e=>arrowFromSegmentInPolygon(o,e,s)))},axiom4Arrows:({vertices_coords:e},t,r,s)=>{const a=e.map(resize2),o=convexHull(a).map((e=>a[e])),c=axiom4(t,r).shift(),n=intersectLineLine(c,t).point,i=perpendicularBalancedSegment(o,c,n);return[arrowFromSegmentInPolygon(o,i,s)]},axiom5Arrows:({vertices_coords:e},t,r,s,a)=>{const o=e.map(resize2),c=convexHull(o).map((e=>o[e]));return axiom5(t,r,s).map((e=>[s,diagramReflectPoint(e,s)])).map((e=>arrowFromSegmentInPolygon(c,e,a)))},axiom6Arrows:({vertices_coords:e},t,r,s,a,o)=>{const c=e.map(resize2),n=convexHull(c).map((e=>c[e]));return axiom6(t,r,s,a).flatMap((e=>[s,a].map((t=>[t,diagramReflectPoint(e,t)])))).map((e=>arrowFromSegmentInPolygon(n,e,o)))},axiom7Arrows:({vertices_coords:e},t,r,s,a)=>{const o=e.map(resize2),c=convexHull(o).map((e=>o[e]));return axiom7(t,r,s).flatMap((e=>[[s,diagramReflectPoint(e,s)],perpendicularBalancedSegment(c,e,intersectLineLine(e,r).point)])).filter((e=>void 0!==e)).map((e=>arrowFromSegmentInPolygon(c,e,a)))},diagramReflectPoint:diagramReflectPoint});const Ya={axiom:L,convert:$r,diagram:{...Ja,...Za},general:Br,graph:Ds,layer:Ha,math:Qs,singleVertex:Gs,svg:vr,webgl:wa,...Object.freeze({__proto__:null,__types__:()=>{}})};lr.ear=Ya;const Xa=Ya;return Object.defineProperty(Xa,\"window\",{enumerable:!1,set:e=>{var t;vr.window=((t=e).document||(t.document=(e=>(new e.DOMParser).parseFromString(\"<!DOCTYPE html><title>.</title>\",\"text/html\"))(t)),v.window=t,v.window)}}),Ya}));\n"
  },
  {
    "path": "readme.md",
    "content": "# Rabbit Ear\n\nThis is a Javascript library for modeling origami.\n\n# overview\n\nThis library assists in encoding, modifying, and rendering origami models. Origami models are encoded in [FOLD](https://github.com/edemaine/FOLD/) format, which is a mesh based data structure. Rabbit Ear contains methods for modifying FOLD graphs, a math library, an SVG and WebGL rendering library, and various methods for making origami-related calculations.\n\n# usage\n\nRabbit Ear source code is distributed as an ES6 module as well (as individual files), as well as a single UMD/CommonJS bundle file. These URLs link to the bundled files:\n\n### UMD module\n\nfor node.js require() and \\<script\\>\n\n```\nhttps://rabbit-ear.github.io/rabbit-ear/rabbit-ear.js\n```\n\n### ES6 module\n\nfor ES 2015 import/export and \\<script type=\"module\"\\>\n\n```\nhttps://rabbit-ear.github.io/rabbit-ear/src/index.js\n```\n\n### NPM\n\nThe package on [npm](https://www.npmjs.com/package/rabbit-ear) contains both UMD and ES6-module formats.\n\n```bash\nnpm install rabbit-ear\n```\n\n# learn\n\nThe [docs](https://rabbitear.org/docs/) contain technical references for coding with this library.\n\n[FOLD validator/viewer](https://foldfile.com/) validate and visualize a FOLD file, the mesh file format used by this library.\n\n# license\n\nGNU GPLv3\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import cleanup from \"rollup-plugin-cleanup\";\nimport terser from \"@rollup/plugin-terser\";\n\nconst version = \"0.9.4 alpha 2024-04-20\";\nconst input = \"src/index.js\";\nconst name = \"ear\";\nconst banner = `/* Rabbit Ear ${version} (c) Kraft, GNU GPLv3 License */\\n`;\n\nconst minifiedUMD = {\n\tinput,\n\toutput: {\n\t\tname,\n\t\tfile: \"rabbit-ear.js\",\n\t\tformat: \"umd\",\n\t\tbanner,\n\t},\n\tplugins: [\n\t\tcleanup(),\n\t\tterser({\n\t\t\tkeep_fnames: true,\n\t\t\tformat: {\n\t\t\t\tcomments: \"all\",\n\t\t\t},\n\t\t}),\n\t],\n};\n\nconst moduleFolder = {\n\tinput,\n\toutput: {\n\t\tname,\n\t\tdir: \"module/\",\n\t\tformat: \"es\",\n\t\tbanner,\n\t\tpreserveModules: true,\n\t\tgeneratedCode: {\n\t\t\tconstBindings: true,\n\t\t\tobjectShorthand: true,\n\t\t},\n\t\t// sourcemap: true,\n\t},\n\tplugins: [\n\t\tcleanup(),\n\t],\n};\n\n// const commonJS = {\n// \tinput,\n// \toutput: {\n// \t\tname,\n// \t\tfile: \"rabbit-ear.js\",\n// \t\tformat: \"cjs\",\n// \t\tbanner,\n// \t},\n// };\n\n// const umd = {\n// \tinput,\n// \toutput: {\n// \t\tname,\n// \t\tfile: \"rabbit-ear.js\",\n// \t\tformat: \"umd\",\n// \t\tbanner,\n// \t\tcompact: true,\n// \t\tgeneratedCode: {\n// \t\t\tconstBindings: true,\n// \t\t\tobjectShorthand: true,\n// \t\t},\n// \t},\n// \tplugins: [\n// \t\tcleanup(),\n// \t\tterser({\n// \t\t\tkeep_fnames: true,\n// \t\t\tformat: {\n// \t\t\t\tcomments: false,\n// \t\t\t},\n// \t\t}),\n// \t],\n// };\n\n// const moduleFile = {\n// \tinput,\n// \toutput: {\n// \t\tname,\n// \t\tfile: \"rabbit-ear.module.js\",\n// \t\tformat: \"es\",\n// \t\tbanner,\n// \t\tcompact: true,\n// \t\tgeneratedCode: {\n// \t\t\tconstBindings: true,\n// \t\t\tobjectShorthand: true,\n// \t\t},\n// \t\t// sourcemap: true,\n// \t},\n// \tplugins: [\n// \t\tcleanup(),\n// \t\tterser({ compress: false, format: { comments: false } }),\n// \t],\n// };\n\nexport default [minifiedUMD, moduleFolder];\n// export default [minifiedUMD];\n"
  },
  {
    "path": "src/axioms/axioms.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tvecLineToUniqueLine,\n\tuniqueLineToVecLine,\n} from \"../math/convert.js\";\nimport {\n\tincludeL,\n} from \"../math/compare.js\";\nimport {\n\tnormalize2,\n\tdistance2,\n\tdot2,\n\tcross2,\n\tadd2,\n\tsubtract2,\n\tscale2,\n\tmidpoint2,\n\trotate90,\n} from \"../math/vector.js\";\nimport {\n\tbisectLines2,\n} from \"../math/line.js\";\nimport {\n\tintersectLineLine,\n\tintersectCircleLine,\n} from \"../math/intersect.js\";\nimport {\n\tpolynomialSolver,\n} from \"../math/polynomial.js\";\n\n/*           _                       _              _\n\t\t\t\t\t\t(_)                     (_)            (_)\n\t ___  _ __ _  __ _  __ _ _ __ ___  _    __ ___  ___  ___  _ __ ___  ___\n\t/ _ \\| '__| |/ _` |/ _` | '_ ` _ \\| |  / _` \\ \\/ / |/ _ \\| '_ ` _ \\/ __|\n | (_) | |  | | (_| | (_| | | | | | | | | (_| |>  <| | (_) | | | | | \\__ \\\n\t\\___/|_|  |_|\\__, |\\__,_|_| |_| |_|_|  \\__,_/_/\\_\\_|\\___/|_| |_| |_|___/\n\t\t\t\t\t\t\t\t__/ |\n\t\t\t\t\t\t\t |___/\n*/\n\n/**\n * @description origami axiom 1: form a line that passes between the given points\n * @param {[number, number]} point1 a 2D point\n * @param {[number, number]} point2 a 2D point\n * @returns {[UniqueLine]} an array of one solution line in {normal, distance} form\n */\nexport const normalAxiom1 = (point1, point2) => {\n\tconst normal = normalize2(rotate90(subtract2(point2, point1)));\n\treturn [{\n\t\tnormal,\n\t\tdistance: dot2(add2(point1, point2), normal) / 2.0,\n\t}];\n};\n\n/**\n * @description origami axiom 1: form a line that passes between the given points\n * @param {[number, number]} point1 one 2D point\n * @param {[number, number]} point2 one 2D point\n * @returns {[VecLine2]} an array of one solution line in {vector, origin} form\n */\nexport const axiom1 = (point1, point2) => [{\n\tvector: normalize2(subtract2(point2, point1)),\n\torigin: point1,\n}];\n\n/**\n * @description origami axiom 2: form a perpendicular bisector between the given points\n * @param {[number, number]} point1 one 2D point\n * @param {[number, number]} point2 one 2D point\n * @returns {[UniqueLine]} an array of one solution line in {normal, distance} form\n */\nexport const normalAxiom2 = (point1, point2) => {\n\tconst normal = normalize2(subtract2(point2, point1));\n\treturn [{\n\t\tnormal,\n\t\tdistance: dot2(add2(point1, point2), normal) / 2.0,\n\t}];\n};\n\n/**\n * @description origami axiom 2: form a perpendicular bisector between the given points\n * @param {[number, number]} point1 one 2D point\n * @param {[number, number]} point2 one 2D point\n * @returns {[VecLine2]} an array of one solution line in {vector, origin} form\n */\nexport const axiom2 = (point1, point2) => [{\n\tvector: normalize2(rotate90(subtract2(point2, point1))),\n\torigin: midpoint2(point1, point2),\n}];\n\n/**\n * @description origami axiom 3: form two lines that make the two angular bisectors between\n * two input lines, and in the case of parallel inputs only one solution will be given\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form\n * @returns {[UniqueLine?, UniqueLine?]} an array of solutions in {normal, distance} form\n */\nexport const normalAxiom3 = (line1, line2) => {\n\tconst determ = cross2(line1.normal, line2.normal);\n\n\t// lines are parallel, only one solution exists\n\tif (Math.abs(determ) < EPSILON) {\n\t\treturn [{\n\t\t\tnormal: line1.normal,\n\t\t\tdistance: (line1.distance + line2.distance * dot2(line1.normal, line2.normal)) / 2,\n\t\t}];\n\t}\n\tconst x = line1.distance * line2.normal[1] - line2.distance * line1.normal[1];\n\tconst y = line2.distance * line1.normal[0] - line1.distance * line2.normal[0];\n\t/** @type {[number, number]} */\n\tconst intersect = [x / determ, y / determ];\n\n\tconst [resultA, resultB] = [add2, subtract2]\n\t\t.map(f => normalize2(f(line1.normal, line2.normal)))\n\t\t.map(normal => ({ normal, distance: dot2(intersect, normal) }));\n\treturn [resultA, resultB];\n};\n\n/**\n * @description origami axiom 3: form two lines that make the two angular bisectors between\n * two input lines, and in the case of parallel inputs only one solution will be given\n * @param {VecLine2} line1 one 2D line in {vector, origin} form\n * @param {VecLine2} line2 one 2D line in {vector, origin} form\n * @returns {[VecLine2?, VecLine2?]} an array of lines in {vector, origin} form\n */\nexport const axiom3 = (line1, line2) => bisectLines2(line1, line2);\n\n/**\n * @description origami axiom 4: form a line perpendicular to a given line that\n * passes through a point.\n * @param {UniqueLine} line one 2D line in {normal, distance} form\n * @param {[number, number]} point one 2D point\n * @returns {[UniqueLine]} an array of one solution in {normal, distance} form\n */\nexport const normalAxiom4 = (line, point) => {\n\tconst normal = rotate90(line.normal);\n\tconst distance = dot2(point, normal);\n\treturn [{ normal, distance }];\n};\n\n/**\n * @description origami axiom 4: form a line perpendicular to a given line that\n * passes through a point.\n * @param {VecLine2} line one 2D line in {vector, origin} form\n * @param {[number, number]} point one 2D point\n * @returns {[VecLine2]} the line in {vector, origin} form\n */\nexport const axiom4 = ({ vector }, point) => [{\n\tvector: rotate90(normalize2(vector)),\n\torigin: point,\n}];\n\n/**\n * @description origami axiom 5: form up to two lines that pass through a point that also\n * brings another point onto a given line\n * @param {UniqueLine} line one 2D line in {normal, distance} form\n * @param {[number, number]} point1 one 2D point, the point that the line(s) pass through\n * @param {[number, number]} point2 one 2D point, the point that is being brought onto the line\n * @returns {[UniqueLine?, UniqueLine?]} an array of solutions in {normal, distance} form\n */\nexport const normalAxiom5 = (line, point1, point2) => {\n\tconst p1base = dot2(point1, line.normal);\n\tconst a = line.distance - p1base;\n\tconst c = distance2(point1, point2);\n\tif (a > c) { return []; }\n\tconst b = Math.sqrt(c * c - a * a);\n\tconst a_vec = scale2(line.normal, a);\n\tconst base_center = add2(point1, a_vec);\n\tconst base_vector = scale2(rotate90(line.normal), b);\n\t// if b is near 0 we have one solution, otherwise two\n\tconst mirrors = b < EPSILON\n\t\t? [base_center]\n\t\t: [add2(base_center, base_vector), subtract2(base_center, base_vector)];\n\tconst [resultA, resultB] = mirrors\n\t\t.map(pt => normalize2(subtract2(point2, pt)))\n\t\t.map(normal => ({ normal, distance: dot2(point1, normal) }));\n\treturn [resultA, resultB];\n};\n\n/**\n * @description origami axiom 5: form up to two lines that pass through a point that also\n * brings another point onto a given line\n * @param {VecLine2} line one 2D line in {vector, origin} form\n * @param {[number, number]} point1 one 2D point, the point that the line(s) pass through\n * @param {[number, number]} point2 one 2D point, the point that is being brought onto the line\n * @returns {VecLine2[]} an array of lines in {vector, origin} form\n */\nexport const axiom5 = (line, point1, point2) => (\n\tintersectCircleLine(\n\t\t{ radius: distance2(point1, point2), origin: point1 },\n\t\tline,\n\t) || []).map(sect => ({\n\tvector: normalize2(rotate90(subtract2(sect, point2))),\n\torigin: midpoint2(point2, sect),\n}));\n\n/**\n * @description origami axiom 6: form up to three lines that are made by bringing\n * a point to a line and a second point to a second line.\n * @attribution a refactoring from C++ of Robert Lang's cubic solver from\n * Reference Finder: https://langorigami.com/article/referencefinder/\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form\n * @param {[number, number]} point1 the point to bring to the first line\n * @param {[number, number]} point2 the point to bring to the second line\n * @returns {UniqueLine[]}\n * an array of solutions in {normal, distance} form\n */\nexport const normalAxiom6 = (line1, line2, point1, point2) => {\n\t// at least pointA must not be on lineA\n\t// for some reason this epsilon is much higher than 1e-6\n\tif (Math.abs(1 - (dot2(line1.normal, point1) / line1.distance)) < 0.02) { return []; }\n\n\t// line vec is the first line's vector, along the line, not the normal\n\tconst line_vec = rotate90(line1.normal);\n\tconst vec1 = subtract2(\n\t\tadd2(point1, scale2(line1.normal, line1.distance)),\n\t\tscale2(point2, 2),\n\t);\n\tconst vec2 = subtract2(scale2(line1.normal, line1.distance), point1);\n\tconst c1 = dot2(point2, line2.normal) - line2.distance;\n\tconst c2 = 2 * dot2(vec2, line_vec);\n\tconst c3 = dot2(vec2, vec2);\n\tconst c4 = dot2(add2(vec1, vec2), line_vec);\n\tconst c5 = dot2(vec1, vec2);\n\tconst c6 = dot2(line_vec, line2.normal);\n\tconst c7 = dot2(vec2, line2.normal);\n\tconst d = c6;\n\tconst c = c1 + c4 * c6 + c7;\n\tconst b = c1 * c2 + c5 * c6 + c4 * c7;\n\tconst a = c1 * c3 + c5 * c7;\n\n\t// construct the solution from the root, the solution being the parameter\n\t// point reflected across the fold line, lying on the parameter line\n\tlet coefficients = [];\n\tif (Math.abs(d) > EPSILON) {\n\t\tcoefficients = [a, b, c, d];\n\t} else if (Math.abs(c) > EPSILON) {\n\t\tcoefficients = [a, b, c];\n\t} else if (Math.abs(b) > EPSILON) {\n\t\tcoefficients = [a, b];\n\t}\n\treturn polynomialSolver(coefficients)\n\t\t.map(n => add2(\n\t\t\tscale2(line1.normal, line1.distance),\n\t\t\tscale2(line_vec, n),\n\t\t))\n\t\t.map(p => ({ p, normal: normalize2(subtract2(p, point1)) }))\n\t\t.map(el => ({\n\t\t\tnormal: el.normal,\n\t\t\tdistance: dot2(el.normal, midpoint2(el.p, point1)),\n\t\t}));\n};\n\n/**\n * @description origami axiom 6: form up to three lines that are made by bringing\n * a point to a line and a second point to a second line.\n * @param {VecLine2} line1 one 2D line in {vector, origin} form\n * @param {VecLine2} line2 one 2D line in {vector, origin} form\n * @param {[number, number]} point1 the point to bring to the first line\n * @param {[number, number]} point2 the point to bring to the second line\n * @returns {VecLine2[]} an array of lines in {vector, origin} form\n */\nexport const axiom6 = (line1, line2, point1, point2) => normalAxiom6(\n\tvecLineToUniqueLine(line1),\n\tvecLineToUniqueLine(line2),\n\tpoint1,\n\tpoint2,\n).map(uniqueLineToVecLine);\n\n/**\n * @description origami axiom 7: form a line by bringing a point onto a given line\n * while being perpendicular to another given line.\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form,\n * the line the point will be brought onto.\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form,\n * the line which the perpendicular will be based off.\n * @param {[number, number]} point the point to bring onto the line\n * @returns {[UniqueLine?]} an array of one solution in {normal, distance} form\n */\nexport const normalAxiom7 = (line1, line2, point) => {\n\tconst normal = rotate90(line1.normal);\n\tconst norm_norm = dot2(normal, line2.normal);\n\t// if norm_norm is close to 0, the two input lines are parallel, no solution\n\tif (Math.abs(norm_norm) < EPSILON) { return undefined; }\n\tconst a = dot2(point, normal);\n\tconst b = dot2(point, line2.normal);\n\tconst distance = (line2.distance + 2.0 * a * norm_norm - b) / (2.0 * norm_norm);\n\treturn [{ normal, distance }];\n};\n\n/**\n * @description origami axiom 7: form a line by bringing a point onto a given line\n * while being perpendicular to another given line.\n * @param {VecLine2} line1 one 2D line in {vector, origin} form,\n * the line the point will be brought onto.\n * @param {VecLine2} line2 one 2D line in {vector, origin} form,\n * the line which the perpendicular will be based off.\n * @param {[number, number]} point the point to bring onto the line\n * @returns {[VecLine2?]} the line in {vector, origin} form\n * or undefined if the given lines are parallel\n */\nexport const axiom7 = (line1, line2, point) => {\n\tconst intersect = intersectLineLine(\n\t\tline1,\n\t\t{ vector: line2.vector, origin: point },\n\t\tincludeL,\n\t\tincludeL,\n\t).point;\n\treturn intersect === undefined\n\t\t? []\n\t\t: [{\n\t\t\t// todo: switch this out, but test it as you do\n\t\t\tvector: normalize2(rotate90(subtract2(intersect, point))),\n\t\t\t// vector: normalize2(rotate90(line2.vector)),\n\t\t\torigin: midpoint2(point, intersect),\n\t\t}];\n};\n\n/**\n * @description Perform one of the seven origami axioms\n * @param {number} number the axiom number, 1-7\n * @param {...any} args the axiom input parameters\n * @returns {VecLine2[]} an array of solution lines in {vector, origin} form\n */\n// export const axiom = (number, ...args) => [\n// \tnull, axiom1, axiom2, axiom3, axiom4, axiom5, axiom6, axiom7,\n// ][number](...args);\n\n/**\n * @description Perform one of the seven origami axioms\n * @param {number} number the axiom number, 1-7\n * @param {...any} args the lines or points, as required by this axiom\n * @returns {UniqueLine[]} an array of solution lines in {normal, distance} form\n */\n// export const normalAxiom = (number, ...args) => [\n// \tnull,\n// \tnormalAxiom1,\n// \tnormalAxiom2,\n// \tnormalAxiom3,\n// \tnormalAxiom4,\n// \tnormalAxiom5,\n// \tnormalAxiom6,\n// \tnormalAxiom7,\n// ][number](...args);\n\n// * @param {line?} line1 a line parameter, if required by the axiom\n// * @param {line?} line2 a line parameter, if required by the axiom\n// * @param {number[]?} point1 a point parameter, if required by the axiom\n// * @param {number[]?} point2 a point parameter, if required by the axiom\n"
  },
  {
    "path": "src/axioms/boundary.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\taxiom1,\n\taxiom2,\n\taxiom3,\n\taxiom4,\n\taxiom5,\n\taxiom6,\n\taxiom7,\n\tnormalAxiom1,\n\tnormalAxiom2,\n\tnormalAxiom3,\n\tnormalAxiom4,\n\tnormalAxiom5,\n\tnormalAxiom6,\n\tnormalAxiom7,\n} from \"./axioms.js\";\nimport {\n\tvalidateAxiom1And2,\n\tvalidateAxiom3,\n\tvalidateAxiom4,\n\tvalidateAxiom5,\n\tvalidateAxiom6,\n\tvalidateAxiom7,\n} from \"./validate.js\";\nimport {\n\tuniqueLineToVecLine,\n} from \"../math/convert.js\";\n\n/**\n * @param {[number, number][]} polygon\n * @param {[number, number]} point1\n * @param {[number, number]} point2\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom1InPolygon = (polygon, point1, point2) => {\n\tconst isValid = validateAxiom1And2(polygon, point1, point2);\n\treturn normalAxiom1(point1, point2).filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {[number, number]} point1\n * @param {[number, number]} point2\n * @returns {VecLine2[]}\n */\nexport const axiom1InPolygon = (polygon, point1, point2) => {\n\tconst isValid = validateAxiom1And2(polygon, point1, point2);\n\treturn axiom1(point1, point2).filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {[number, number]} point1\n * @param {[number, number]} point2\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom2InPolygon = (polygon, point1, point2) => {\n\tconst isValid = validateAxiom1And2(polygon, point1, point2);\n\treturn normalAxiom2(point1, point2).filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {[number, number]} point1\n * @param {[number, number]} point2\n * @returns {VecLine2[]}\n */\nexport const axiom2InPolygon = (polygon, point1, point2) => {\n\tconst isValid = validateAxiom1And2(polygon, point1, point2);\n\treturn axiom2(point1, point2).filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom3InPolygon = (polygon, line1, line2) => {\n\tconst solutions = normalAxiom3(line1, line2);\n\tconst isValid = validateAxiom3(\n\t\tpolygon,\n\t\tsolutions.map(uniqueLineToVecLine),\n\t\tuniqueLineToVecLine(line1),\n\t\tuniqueLineToVecLine(line2),\n\t);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line1 one 2D line in {vector, origin} form\n * @param {VecLine2} line2 one 2D line in {vector, origin} form\n * @returns {VecLine2[]}\n */\nexport const axiom3InPolygon = (polygon, line1, line2) => {\n\tconst solutions = axiom3(line1, line2);\n\tconst isValid = validateAxiom3(polygon, solutions, line1, line2);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {UniqueLine} line one 2D line in {normal, distance} form\n * @param {[number, number]} point one 2D point\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom4InPolygon = (polygon, line, point) => {\n\tconst solutions = normalAxiom4(line, point);\n\tconst isValid = validateAxiom4(\n\t\tpolygon,\n\t\tsolutions.map(uniqueLineToVecLine),\n\t\tuniqueLineToVecLine(line),\n\t\tpoint,\n\t);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line one 2D line in {vector, origin} form\n * @param {[number, number]} point one 2D point\n * @returns {VecLine2[]}\n */\nexport const axiom4InPolygon = (polygon, line, point) => {\n\tconst solutions = axiom4(line, point);\n\tconst isValid = validateAxiom4(polygon, solutions, line, point);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {UniqueLine} line one 2D line in {normal, distance} form\n * @param {[number, number]} point1 one 2D point, the point that the line(s) pass through\n * @param {[number, number]} point2 one 2D point, the point that is being brought onto the line\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom5InPolygon = (polygon, line, point1, point2) => {\n\tconst solutions = normalAxiom5(line, point1, point2);\n\tconst isValid = validateAxiom5(\n\t\tpolygon,\n\t\tsolutions.map(uniqueLineToVecLine),\n\t\tuniqueLineToVecLine(line),\n\t\tpoint1,\n\t\tpoint2,\n\t);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line one 2D line in {vector, origin} form\n * @param {[number, number]} point1 one 2D point, the point that the line(s) pass through\n * @param {[number, number]} point2 one 2D point, the point that is being brought onto the line\n * @returns {VecLine2[]}\n */\nexport const axiom5InPolygon = (polygon, line, point1, point2) => {\n\tconst solutions = axiom5(line, point1, point2);\n\tconst isValid = validateAxiom5(polygon, solutions, line, point1, point2);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form\n * @param {[number, number]} point1 the point to bring to the first line\n * @param {[number, number]} point2 the point to bring to the second line\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom6InPolygon = (polygon, line1, line2, point1, point2) => {\n\tconst solutions = normalAxiom6(line1, line2, point1, point2);\n\tconst isValid = validateAxiom6(\n\t\tpolygon,\n\t\tsolutions.map(uniqueLineToVecLine),\n\t\tuniqueLineToVecLine(line1),\n\t\tuniqueLineToVecLine(line2),\n\t\tpoint1,\n\t\tpoint2,\n\t);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line1 one 2D line in {vector, origin} form\n * @param {VecLine2} line2 one 2D line in {vector, origin} form\n * @param {[number, number]} point1 the point to bring to the first line\n * @param {[number, number]} point2 the point to bring to the second line\n * @returns {VecLine2[]}\n */\nexport const axiom6InPolygon = (polygon, line1, line2, point1, point2) => {\n\tconst solutions = axiom6(line1, line2, point1, point2);\n\tconst isValid = validateAxiom6(polygon, solutions, line1, line2, point1, point2);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {UniqueLine} line1 one 2D line in {normal, distance} form,\n * the line the point will be brought onto.\n * @param {UniqueLine} line2 one 2D line in {normal, distance} form,\n * the line which the perpendicular will be based off.\n * @param {[number, number]} point the point to bring onto the line\n * @returns {UniqueLine[]}\n */\nexport const normalAxiom7InPolygon = (polygon, line1, line2, point) => {\n\tconst solutions = normalAxiom7(line1, line2, point);\n\tconst isValid = validateAxiom7(\n\t\tpolygon,\n\t\tsolutions.map(uniqueLineToVecLine),\n\t\tuniqueLineToVecLine(line1),\n\t\tuniqueLineToVecLine(line2),\n\t\tpoint,\n\t);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line1 one 2D line in {vector, origin} form,\n * the line the point will be brought onto.\n * @param {VecLine2} line2 one 2D line in {vector, origin} form,\n * the line which the perpendicular will be based off.\n * @param {[number, number]} point the point to bring onto the line\n * @returns {VecLine2[]}\n */\nexport const axiom7InPolygon = (polygon, line1, line2, point) => {\n\tconst solutions = axiom7(line1, line2, point);\n\tconst isValid = validateAxiom7(polygon, solutions, line1, line2, point);\n\treturn solutions.filter((_, i) => isValid[i]);\n};\n"
  },
  {
    "path": "src/axioms/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\nimport * as Axioms from \"./axioms.js\";\nimport * as AxiomsBoundary from \"./boundary.js\";\nimport * as ValidateAxioms from \"./validate.js\";\n\nexport default {\n\t...Axioms,\n\t...AxiomsBoundary,\n\t...ValidateAxioms,\n};\n"
  },
  {
    "path": "src/axioms/validate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tinclude,\n\tincludeL,\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tsubtract2,\n\trotate90,\n} from \"../math/vector.js\";\nimport {\n\tmakeMatrix2Reflect,\n\tmultiplyMatrix2Vector2,\n} from \"../math/matrix2.js\";\nimport {\n\toverlapLinePoint,\n\toverlapConvexPolygonPoint,\n} from \"../math/overlap.js\";\nimport {\n\tintersectLineLine,\n\tintersectPolygonLine,\n} from \"../math/intersect.js\";\nimport {\n\tclipLineConvexPolygon,\n} from \"../math/clip.js\";\n\n/**\n * @param {VecLine2} foldLine\n * @param {[number, number]} point\n * @returns {[number, number]}\n */\nconst reflectPoint = (foldLine, point) => {\n\tconst matrix = makeMatrix2Reflect(foldLine.vector, foldLine.origin);\n\treturn multiplyMatrix2Vector2(matrix, point);\n};\n\n/**\n * @description Validate the input parameters to origami axiom 1 with\n * respect to a boundary polygon that represents the folding surface.\n * To validate axiom 1 check if the input points are inside the\n * boundary polygon, if so, the solution is valid.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {[number, number]} point1 the point parameter for axiom 1 or 2\n * @param {[number, number]} point2 the point parameter for axiom 1 or 2\n * @returns {boolean[]} true if the parameters/solutions are valid\n */\nexport const validateAxiom1And2 = (boundary, point1, point2) => [\n\t[point1, point2]\n\t\t.map(p => overlapConvexPolygonPoint(boundary, p, include).overlap)\n\t\t.reduce((a, b) => a && b, true),\n];\n\n/**\n * @description Validate the input parameters to origami axiom 2 with\n * respect to a boundary polygon that represents the folding surface.\n * To validate axiom 2 check if the input points are inside the\n * boundary polygon, if so, the solution is valid.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {[number, number]} point1 the point parameter for axiom 2\n * @param {[number, number]} point2 the point parameter for axiom 2\n * @returns {boolean[]} true if the parameters/solutions are valid\n */\n// export const validateAxiom2 = validateAxiom1;\n\n/**\n * @description Validate the input parameters to origami axiom 3 with\n * respect to a boundary polygon that represents the folding surface.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {VecLine2} line1 the line parameter for axiom 3\n * @param {VecLine2} line2 the line parameter for axiom 3\n * @returns {boolean[]} array of booleans (true if valid) matching the solutions array\n */\nexport const validateAxiom3 = (boundary, solutions, line1, line2) => {\n\tconst segments = [line1, line2]\n\t\t.map(line => clipLineConvexPolygon(\n\t\t\tboundary,\n\t\t\tline,\n\t\t\tinclude,\n\t\t\tincludeL,\n\t\t));\n\n\t// if line parameters lie outside polygon, no solution possible\n\tif (segments[0] === undefined || segments[1] === undefined) {\n\t\treturn [false, false];\n\t}\n\n\t// test A:\n\t// make sure the results themselves lie in the polygon\n\t// exclusive! an exterior line collinear to polygon's point is excluded\n\t// const results_clip = results\n\t//   .map(line => line === undefined ? undefined : math\n\t//     .intersectConvexPolygonLine(\n\t//       boundary,\n\t//       line,\n\t//       includeS,\n\t//       excludeL));\n\tconst results_clip = solutions.map(line => (line === undefined\n\t\t? undefined\n\t\t: clipLineConvexPolygon(\n\t\t\tboundary,\n\t\t\tline,\n\t\t\tinclude,\n\t\t\tincludeL,\n\t\t)));\n\tconst results_inside = [0, 1].map((i) => results_clip[i] !== undefined);\n\t// test B:\n\t// make sure that for each of the results, the result lies between two\n\t// of the parameters, in other words, reflect the segment 0 both ways\n\t// (both fold solutions) and make sure there is overlap with segment 1\n\tconst seg0Reflect = solutions.map(foldLine => (foldLine === undefined\n\t\t? undefined\n\t\t: [\n\t\t\treflectPoint(foldLine, segments[0][0]),\n\t\t\treflectPoint(foldLine, segments[0][1]),\n\t\t]));\n\tconst reflectMatch = seg0Reflect.map(seg => (seg === undefined\n\t\t? false\n\t\t: (overlapLinePoint(\n\t\t\t{ vector: subtract2(segments[1][1], segments[1][0]), origin: segments[1][0] },\n\t\t\tseg[0],\n\t\t\tincludeS,\n\t\t)\n\t\t|| overlapLinePoint(\n\t\t\t{ vector: subtract2(segments[1][1], segments[1][0]), origin: segments[1][0] },\n\t\t\tseg[1],\n\t\t\tincludeS,\n\t\t)\n\t\t|| overlapLinePoint(\n\t\t\t{ vector: subtract2(seg[1], seg[0]), origin: seg[0] },\n\t\t\tsegments[1][0],\n\t\t\tincludeS,\n\t\t)\n\t\t|| overlapLinePoint(\n\t\t\t{ vector: subtract2(seg[1], seg[0]), origin: seg[0] },\n\t\t\tsegments[1][1],\n\t\t\tincludeS,\n\t\t))));\n\t// valid if A and B\n\treturn [0, 1].map(i => reflectMatch[i] === true && results_inside[i] === true);\n};\n\n/**\n * @description Validate the input parameters to origami axiom 4 with\n * respect to a boundary polygon that represents the folding surface.\n * To validate axiom 4 check if the input point lies within\n * the boundary and the intersection between the solution line and the\n * input line lies within the boundary.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {VecLine2} line the line parameter for axiom 4\n * @param {[number, number]} point the point parameter for axiom 4\n * @returns {boolean[]} true if the parameters/solutions are valid\n */\nexport const validateAxiom4 = (boundary, solutions, line, point) => {\n\tconst perpendicular = { vector: rotate90(line.vector), origin: point };\n\tconst intersect = intersectLineLine(line, perpendicular).point;\n\n\t// todo: false or...?\n\tif (!intersect) { return [false]; }\n\treturn [\n\t\t[point, intersect]\n\t\t\t.map(p => overlapConvexPolygonPoint(boundary, p, include).overlap)\n\t\t\t.reduce((a, b) => a && b, true),\n\t];\n};\n\n/**\n * @description Validate the input parameters to origami axiom 5 with\n * respect to a boundary polygon that represents the folding surface.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {VecLine2} line the line parameter for axiom 5\n * @param {[number, number]} point1 the point parameter for axiom 5\n * @param {[number, number]} point2 the point parameter for axiom 5\n * @returns {boolean[]} array of booleans (true if valid) matching the solutions array\n */\nexport const validateAxiom5 = (boundary, solutions, line, point1, point2) => {\n\tif (solutions.length === 0) { return []; }\n\tconst testParamPoints = [point1, point2]\n\t\t.map(point => overlapConvexPolygonPoint(boundary, point, include).overlap)\n\t\t.reduce((a, b) => a && b, true);\n\tconst testReflections = solutions\n\t\t.map(foldLine => reflectPoint(foldLine, point2))\n\t\t.map(point => overlapConvexPolygonPoint(boundary, point, include).overlap);\n\treturn testReflections.map(ref => ref && testParamPoints);\n};\n\n/**\n * @description Validate the input parameters to origami axiom 6 with\n * respect to a boundary polygon that represents the folding surface.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {VecLine2} line1 the line parameter for axiom 6\n * @param {VecLine2} line2 the line parameter for axiom 6\n * @param {[number, number]} point1 the point parameter for axiom 6\n * @param {[number, number]} point2 the point parameter for axiom 6\n * @returns {boolean[]} array of booleans (true if valid) matching the solutions array\n */\nexport const validateAxiom6 = function (boundary, solutions, line1, line2, point1, point2) {\n\tif (solutions.length === 0) { return []; }\n\tconst testParamPoints = [point1, point2]\n\t\t.map(point => overlapConvexPolygonPoint(boundary, point, include).overlap)\n\t\t.reduce((a, b) => a && b, true);\n\tif (!testParamPoints) { return solutions.map(() => false); }\n\tconst testReflect0 = solutions\n\t\t.map(foldLine => reflectPoint(foldLine, point1))\n\t\t.map(point => overlapConvexPolygonPoint(boundary, point, include).overlap);\n\tconst testReflect1 = solutions\n\t\t.map(foldLine => reflectPoint(foldLine, point2))\n\t\t.map(point => overlapConvexPolygonPoint(boundary, point, include).overlap);\n\treturn solutions.map((_, i) => testReflect0[i] && testReflect1[i]);\n};\n\n/**\n * @description Validate the input parameters to origami axiom 7 with\n * respect to a boundary polygon that represents the folding surface.\n * @param {[number, number][]} boundary an array of 2D points,\n * each point is an array of numbers\n * @param {VecLine2[]} solutions an array of solutions in vector-origin form\n * @param {VecLine2} line1 the line parameter for axiom 7\n * @param {VecLine2} line2 the line parameter for axiom 7\n * @param {[number, number]} point the point parameter for axiom 7\n * @returns {boolean[]} true if the parameters/solutions are valid\n */\nexport const validateAxiom7 = (boundary, solutions, line1, line2, point) => {\n\t// check if the point parameter is inside the polygon\n\tconst paramPointTest = overlapConvexPolygonPoint(\n\t\tboundary,\n\t\tpoint,\n\t\tinclude,\n\t).overlap;\n\t// check if the reflected point on the fold line is inside the polygon\n\tif (!solutions.length) { return [false]; }\n\tconst reflected = reflectPoint(solutions[0], point);\n\tconst reflectTest = overlapConvexPolygonPoint(boundary, reflected, include).overlap;\n\t// check if the line to fold onto itself is somewhere inside the polygon\n\tconst paramLineTest = (\n\t\tintersectPolygonLine(boundary, line2, includeL).length >= 2\n\t);\n\t// same test we do for axiom 4\n\tconst intersect = intersectLineLine(\n\t\tline2,\n\t\tsolutions[0],\n\t\tincludeL,\n\t\tincludeL,\n\t).point;\n\tconst intersectInsideTest = intersect\n\t\t? overlapConvexPolygonPoint(boundary, intersect, include).overlap\n\t\t: false;\n\treturn [paramPointTest && reflectTest && paramLineTest && intersectInsideTest];\n};\n\n// /**\n//  * @description Validate an axiom, this will run one of\n//  * the submethods (\"validateAxiom1\", \"validateAxiom2\", ...).\n//  * @param {number} number the axiom number, 1-7\n//  * @param {number[][]} boundary an array of points, each point is an array of numbers\n//  * @param {VecLine[]} solutions an array of solutions in vector-origin form\n//  * @param {number[] | VecLine} ...args the input parameters to the axiom method\n//  * @returns {boolean|boolean[]} for every solution, true if valid. Axioms 1, 2, 4, 7\n//  * return one boolean, 3, 5, 6 return arrays of booleans.\n//  */\n// export const validateAxiom = (number, boundary, solutions, ...args) => [null,\n// \tvalidateAxiom1,\n// \tvalidateAxiom2,\n// \tvalidateAxiom3,\n// \tvalidateAxiom4,\n// \tvalidateAxiom5,\n// \tvalidateAxiom6,\n// \tvalidateAxiom7,\n// ][number](boundary, solutions, ...args);\n"
  },
  {
    "path": "src/convert/foldToObj.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description if they exist, get any title, author, or description\n * metadata, convert it into a python-style comment (with a #) and\n * join all of them together as one string.\n * @param {FOLD} graph a FOLD object\n * @returns {string} a string of comments containing metadata\n */\nconst getTitleAuthorDescription = (graph) => [\n\t\"file_title\",\n\t\"file_author\",\n\t\"file_description\",\n\t\"frame_title\",\n\t\"frame_author\",\n\t\"frame_description\",\n].filter(key => graph[key])\n\t.map(key => `# ${key.split(\"_\")[1]}: ${graph[key]}`)\n\t.join(\"\\n\");\n\n// todo:\n// should we add\n// param {number} frame_num the frame number inside the FOLD object to be\n//   converted, if frames exist, if not this is ignored.\n\n/**\n * @description Convert a FOLD object into an OBJ file. For FOLD objects\n * with many frames, this will only work on one frame at a time.\n * @param {FOLD|string} file a FOLD object or a string encoding of a FOLD file\n * @returns {string} an OBJ representation of the FOLD object\n * @example\n * // from FOLD file\n * const foldfile = fs.readFileSync(\"./crane.fold\", \"utf-8\");\n * const objFile = foldToObj(foldFile);\n * fs.writeFileSync(\"./crane.obj\", objFile);\n * @example\n * // from FOLD object\n * const birdBase = ear.graph.bird();\n * const objFile = foldToObj(birdBase);\n * fs.writeFileSync(\"./bird-base.obj\", objFile);\n */\nexport const foldToObj = (file) => {\n\t/** @type {FOLD} */\n\tconst graph = typeof file === \"string\" ? JSON.parse(file) : file;\n\n\t// the comment section that will sit at the beginning of the file\n\tconst metadata = getTitleAuthorDescription(graph);\n\n\t// a list of vertices in the form of \"v _ _ _\" where _ are floats\n\tconst vertices = (graph.vertices_coords || [])\n\t\t.map(coords => coords.join(\" \"))\n\t\t.map(str => `v ${str}`)\n\t\t.join(\"\\n\");\n\n\t// a list of faces in the form of \"f _ _ _\" where _ are integers\n\t// obj vertex indices begin from 1 not 0\n\tconst faces = (graph.faces_vertices || [])\n\t\t.map(verts => verts.map(v => v + 1).join(\" \"))\n\t\t.map(str => `f ${str}`)\n\t\t.join(\"\\n\");\n\n\t// join everything together, adding a final newline at the file's end\n\tconst fileString = [metadata, vertices, faces]\n\t\t.filter(s => s !== \"\")\n\t\t.join(\"\\n\");\n\treturn `${fileString}\\n`;\n};\n"
  },
  {
    "path": "src/convert/foldToSvg.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport window from \"../environment/window.js\";\nimport SVG from \"../svg/index.js\";\nimport {\n\tfindElementTypeInParents,\n\taddClass,\n} from \"../svg/general/dom.js\";\nimport {\n\tisFoldedForm,\n} from \"../fold/spec.js\";\nimport {\n\tboundingBox,\n} from \"../graph/boundary.js\";\nimport {\n\tboundingBoxToViewBox,\n\tgetStrokeWidth,\n} from \"./general/svg.js\";\nimport {\n\tdrawVertices,\n} from \"./svg/drawVertices.js\";\nimport {\n\tdrawEdges,\n} from \"./svg/drawEdges.js\";\nimport {\n\tdrawFaces,\n} from \"./svg/drawFaces.js\";\nimport {\n\tdrawBoundaries,\n} from \"./svg/drawBoundaries.js\";\n\n// draw a graph component\nconst draw = {\n\tvertices: drawVertices,\n\tedges: drawEdges,\n\tfaces: drawFaces,\n\tboundaries: drawBoundaries,\n};\n\n/**\n * some default values for rendering\n */\nconst DEFAULT_CIRCLE_RADIUS = 1 / 50;\nconst unitBounds = { min: [0, 0], max: [1, 1], span: [1, 1] };\nconst groupNames = [\"boundaries\", \"faces\", \"edges\", \"vertices\"];\n\n/**\n * @description The options object will be modified in place,\n * if the user customizes something, it will be left alone,\n * otherwise, some default styles will be applied here.\n * Elsewhere as well, default styles are applied inside each\n * component draw method.\n */\nconst setDefaultOptions = (graph, options) => {\n\t// do not render the vertices unless the user explicitly wants them\n\tif (options.vertices === undefined) {\n\t\toptions.vertices = false;\n\t}\n\n\t// in the case of a creasePattern, do not render the faces\n\tif (!isFoldedForm(graph)) {\n\t\tif (options.faces === undefined) {\n\t\t\toptions.faces = false;\n\t\t}\n\t}\n};\n\n/**\n * @description a subroutine of render(). there are style properties which\n * are impossible to set universally, because they are dependent upon the input\n * FOLD object (imagine, FOLD within 1x1 square, and FOLD within 600x600).\n * this includes:\n * - \"viewBox\": calculate the viewBox to fit the 2D bounds of the graph.\n * - \"padding\": padding incorporated into the viewBox, as a scale of the vmax.\n * - \"stroke-width\": as a scale of the vmax.\n * - \"radius\": the radius of the vertices (circles), as a scale of the vmax.\n */\nconst applyTopLevelOptions = (element, groups, graph, options) => {\n\tconst hasVertices = groups[3] && groups[3].childNodes.length;\n\n\t// we can skip this method if all of these are true:\n\t// - there are no vertices\n\t// - \"strokeWidth\" and \"viewBox\" are both false or undefined\n\tif (!hasVertices\n\t\t&& (options.strokeWidth === undefined || options.strokeWidth === false)\n\t\t&& (options.viewBox === undefined || options.viewBox === false)) { return; }\n\n\t// get the bounding box of all vertices, and the maximum of each dimension.\n\t// these are needed for both strokeWidth and viewBox.\n\tconst box = boundingBox(graph) || unitBounds;\n\tconst vmax = Math.max(...box.span);\n\n\t// because the function argument \"element\" is able to be any SVGElement type\n\t// including a <g> group element, and some attributes must be set on the\n\t// SVG element itself, we have to find the SVG element in the chain of parents\n\tconst svgElement = findElementTypeInParents(element, \"svg\");\n\n\t// set the SVG element viewBox\n\tif (svgElement && options.viewBox) {\n\t\tconst viewBoxValue = boundingBoxToViewBox(box);\n\t\tsvgElement.setAttributeNS(null, \"viewBox\", viewBoxValue);\n\t}\n\n\t// the \"padding\" option is incredibly useful despite \"padding\" as a CSS style\n\t// property to be problematic (possibly conflicts with global style).\n\t// padding the viewBox itself is much more reliable.\n\t// parse the viewbox, modify the numbers, join back into a string.\n\tif (svgElement && options.padding) {\n\t\tconst viewBoxString = svgElement.getAttribute(\"viewBox\");\n\t\tif (viewBoxString != null) {\n\t\t\t// options.padding will be applied as a scale of vmax\n\t\t\tconst pad = options.padding * vmax;\n\t\t\tconst viewBox = viewBoxString.split(\" \").map(n => parseFloat(n));\n\t\t\tconst newViewBox = [-pad, -pad, pad * 2, pad * 2]\n\t\t\t\t.map((nudge, i) => viewBox[i] + nudge)\n\t\t\t\t.join(\" \");\n\t\t\tsvgElement.setAttributeNS(null, \"viewBox\", newViewBox);\n\t\t}\n\t}\n\n\t// set the stroke-width attribute, which does not require to be set\n\t// on an SVG element, whatever element was provided works (such as <g>).\n\tif (options.strokeWidth || options[\"stroke-width\"]) {\n\t\tconst strokeWidth = options.strokeWidth\n\t\t\t? options.strokeWidth\n\t\t\t: options[\"stroke-width\"];\n\n\t\t// options.strokeWidth will be applied as a scale of vmax\n\t\tconst strokeWidthValue = typeof strokeWidth === \"number\"\n\t\t\t? vmax * strokeWidth\n\t\t\t: getStrokeWidth(graph);\n\t\telement.setAttributeNS(null, \"stroke-width\", strokeWidthValue);\n\t}\n\n\t// if vertices exist, set their radius, even if no options are supplied.\n\tif (hasVertices) {\n\t\tconst userRadius = options.vertices && options.vertices.radius != null\n\t\t\t? options.vertices.radius\n\t\t\t: options.radius;\n\t\tconst radius = typeof userRadius === \"string\"\n\t\t\t? parseFloat(userRadius)\n\t\t\t: userRadius;\n\t\tconst r = typeof radius === \"number\" && !Number.isNaN(radius)\n\t\t\t? vmax * radius\n\t\t\t: vmax * DEFAULT_CIRCLE_RADIUS;\n\n\t\t// iterate and set all circles \"r\" attribute\n\t\tfor (let i = 0; i < groups[3].childNodes.length; i += 1) {\n\t\t\tgroups[3].childNodes[i].setAttributeNS(null, \"r\", r);\n\t\t}\n\t}\n};\n\n/**\n * @description renders a FOLD object into SVG elements, all elements\n * are sorted into 4 groups (boundaries, faces, edges, vertices) and\n * each group is given a descriptive class name.\n * If the options objects specifies an element to be not drawn, this will\n * still return an empty <g> group element in its place.\n * @param {FOLD} graph a FOLD object\n * @param {object} options (optional)\n * @returns {SVGElement[]} An array of four <g> elements: boundaries, faces,\n * edges, vertices, each of the graph components drawn into an SVG group.\n */\nconst drawGroups = (graph, options = {}) => groupNames\n\t.map(key => (options[key] === false ? (SVG.g()) : draw[key](graph, options[key])))\n\t.map((group, i) => {\n\t\taddClass(group, groupNames[i]);\n\t\treturn group;\n\t});\n\n/**\n * @description renders a FOLD object into an SVG, ensuring visibility by\n * setting the viewBox and the stroke-width attributes on the SVG.\n * @param {SVGElement} element an already initialized SVG DOM element.\n * @param {FOLD} graph a FOLD object\n * @param {object} options an optional options object to style the rendering\n * @returns {SVGElement} the first SVG parameter object.\n */\nexport const renderSVG = (graph, element, options = {}) => {\n\t// this will modify the options object and set some default\n\t// rendering settings, as long as the user has not already specified\n\tsetDefaultOptions(graph, options);\n\n\t// groups will be an array of four <g> elements, even if some are empty\n\tconst groups = drawGroups(graph, options);\n\n\t// for those which contain children, append them to the top-level element\n\tgroups.filter(group => group.childNodes.length > 0)\n\t\t.forEach(group => element.appendChild(group));\n\n\t// apply top level options to the SVG (or group) itself\n\tapplyTopLevelOptions(element, groups, graph, options);\n\n\t// classes in the FOLD object, if they exist, can be carried over\n\t// and added to the svg elements too.\n\taddClass(\n\t\telement,\n\t\t...[graph.file_classes || [], graph.frame_classes || []].flat(),\n\t);\n\n\treturn element;\n};\n\n/**\n * @description renders a FOLD object as an SVG. This is able to render\n * in a couple styles (creasePattern, foldedForm), and will do some work\n * to ensure a nice rendering by finding a decent stroke-width and viewBox.\n *\n * deprecated param {boolean} tell the draw method to resize the viewbox/stroke\n * @param {FOLD|string} file a FOLD object or a FOLD file as a string\n * @param {{\n *   string?: boolean,\n *   vertices?: boolean,\n *   edges?: { mountain: object, valley: object },\n *   faces?: { front: object, back: object },\n *   boundaries?: object,\n *   viewBox?: boolean,\n *   strokeWidth?: number,\n *   radius?: number,\n * }} [options={}] optional options object to style components\n * - \"string\" {boolean} if true, the return value will be a string\n *   otherwise it will be a Javascript DOM element object type.\n * @returns {SVGElement|string} SVG element, containing the rendering of the origami.\n * @example\n * // as string\n * const fold = fs.readFileSync(\"./crane.fold\", \"utf-8\");\n * const svg = foldToSvg(fold, { string: true });\n * fs.writeFileSync(\"./crane.svg\", svg);\n * @example\n * // as DOM Element\n * const birdBase = ear.graph.bird();\n * const svg = foldToSvg(birdBase);\n * document.body.appendChild(svg);\n * @example\n * // with options\n * const birdBase = ear.graph.bird();\n * const svg = foldToSvg(birdBase, {\n *   string: true,\n *   vertices: false,\n *   edges: { mountain: \"lightgray\", valley: \"blue\" },\n *   faces: false,\n *   boundaries: false,\n * });\n * fs.writeFileSync(\"./bird-base.svg\", svg);\n */\nexport const foldToSvg = (file, options = {}) => {\n\t// render the file into a DOM element type.\n\tconst element = renderSVG(\n\t\ttypeof file === \"string\" ? JSON.parse(file) : file,\n\t\tSVG.svg(),\n\t\t{\n\t\t\tviewBox: true,\n\t\t\tstrokeWidth: true,\n\t\t\t...options,\n\t\t},\n\t);\n\n\t// if the user requests it, convert the DOM element into a string\n\treturn options && options.string\n\t\t? (new (window().XMLSerializer)()).serializeToString(element)\n\t\t: element;\n};\n"
  },
  {
    "path": "src/convert/foldToSvg.md",
    "content": "# FOLD to SVG conversion\n\nrender a FOLD object into an SVG. This includes interpreting the origami as a crease pattern or of a folded model, and will style the rendering accordingly.\n\n## API\n\nmain entry point\n\n```javascript\near.convert.foldToSvg(FOLD, options)\n```\n\ncreate as a child of an SVG that was initialized using this library.\n\n```javascript\nconst mySVG = ear.svg()\nmySVG.origami(FOLD)\n```\n\nthis is the main draw subroutine which draws and appends the components onto the `element`:\n\n```javascript\near.convert.foldToSvg.drawInto(element, FOLD, options)\n```\n\nthese subroutines are made available to the user which draw individual components and return one `<g>` group container:\n\n```javascript\near.convert.foldToSvg.boundaries(FOLD, options)\near.convert.foldToSvg.faces(FOLD, options)\near.convert.foldToSvg.edges(FOLD, options)\near.convert.foldToSvg.vertices(FOLD, options)\n```\n\nconvenience method, return the string value for a viewBox which contains the FOLD object, for example: \"-10 -15 310 310\"\n\n```javascript\near.convert.foldToSvg.getViewBox(FOLD)\n```\n\n## options object\n\n```javascript\noptions = {\n\t// component level\n\tvertices: false,\n\tedges: {},\n\tfaces: {},\n\tboundaries: {},\n\t// top level\n\tviewBox: true,\n\tstrokeWidth: 0.01,\n\tradius: 0.2,\n};\n```\n\nAll top level methods accept the same options object. The subroutines which draw individual components take the individual component entries from this example options object (for example, drawing edges takes the \"edges:\" options entry only).\n\n### component level\n\nvertices, edges, faces, boundaries: these can be either an `object` or a `boolean`\n\n- `boolean` with a \"false\" will turn skip rendering this layer\n- `object` with css-style tags (kebab-case keys) will apply style as attribute tags.\n\n\"edges\" and \"faces\" contains special subcategories. Edges can target any of the FOLD-spec edges_assignment classes: mountain, valley, boundary, flat, unassigned. Faces can target front and back, which \n\n```javascript\noptions = {\n\tedges: {\n\t\tmountain: { stroke: \"red\" },\n\t\tvalley: { stroke: \"blue\" },\n\t},\n\tfaces: {\n\t\tfront: { fill: \"#369\" },\n\t\tback: { fill: \"white\" },\n\t},\n};\n```\n\n### top level\n\nviewBox: `boolean`, if this is present, the SVG viewbox will be reset to fit the x-y coordinates from the FOLD object. If the element being draw into is not an `<svg>` (like a `<g>` for example), this will crawl up the parent-tree until it finds an `<svg>`, there the viewBox is set.\n\nstrokeWidth, radius: either `number` or `boolean`. \"radius\" means the radius of all circles (vertices):\n\n- `number`: set this attribute to be a scale of the vmax (max length of x or y axis) of the FOLD object. I remember it as a \"percentage of the longest length\".\n- `boolean`: set the attribute to its default: `stroke-width: 1/100` `radius: 1/50` factors of vmax, the longest side-length of the FOLD object.\n\nstroke-width will be set to the top level group, radius needs to be set on each individual `<circle>` element.\n"
  },
  {
    "path": "src/convert/general/dom.js",
    "content": "import window from \"../../environment/window.js\";\n\nexport const invisibleParent = (child) => {\n\tif (!window().document.body) { return undefined; }\n\n\t// create an invisible element, add the svg, then call getComputedStyle\n\tconst parent = window().document.createElement(\"div\");\n\n\t// visibility:hidden causes the DOM window layout to resize\n\tparent.setAttribute(\"display\", \"none\");\n\t// parent.setAttribute(\"visibility\", \"hidden\");\n\n\twindow().document.body.appendChild(parent);\n\n\tparent.appendChild(child);\n\treturn parent;\n};\n"
  },
  {
    "path": "src/convert/general/options.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { getEpsilon } from \"../../graph/epsilon.js\";\nimport { boundingBox } from \"../../graph/boundary.js\";\nimport { cleanNumber } from \"../../general/number.js\";\n\n/**\n * @description Given an options object, typically the final parameter in\n * a method's parameters, look for an epsilon value which may be located inside\n * the object under an \"epsilon\" parameter, or if the object itself is a number.\n * @param {FOLD} graph a FOLD object\n * @param {object|number} object the options object\n * @param {string} key the key under which this epsilon value might lie.\n * @returns {number} an epsilon value, either from the user, or found via\n * analysis by looking at the vertices of the graph.\n */\nexport const findEpsilonInObject = (graph, object, key = \"epsilon\") => {\n\tif (typeof object === \"object\" && typeof object[key] === \"number\") {\n\t\treturn object[key];\n\t}\n\treturn typeof object === \"number\"\n\t\t? object\n\t\t: getEpsilon(graph);\n};\n\n/**\n * @description Invert the vertices and re-center them to\n * be inside the same bounding box as before.\n * @param {[number, number][]|[number, number, number][]} vertices_coords,\n * modified in place\n * @returns {undefined}\n */\nexport const invertVertical = (vertices_coords) => {\n\tconst box = boundingBox({ vertices_coords });\n\tconst center = box.min[1] + box.span[1] / 2;\n\tconst invMin = Math.min(-box.min[1], -box.max[1]);\n\tconst invMax = Math.max(-box.min[1], -box.max[1]);\n\tconst invSpan = invMax - invMin;\n\tconst invCenter = invMin + invSpan / 2;\n\tconst difference = center - invCenter;\n\tconst yTranslate = cleanNumber(difference, 8);\n\tfor (let i = 0; i < vertices_coords.length; i += 1) {\n\t\tvertices_coords[i][1] = -vertices_coords[i][1] + yTranslate;\n\t}\n};\n"
  },
  {
    "path": "src/convert/general/planarize.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tplanarize,\n} from \"../../graph/planarize.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"../../graph/make/verticesVertices.js\";\nimport {\n\tmakePlanarFaces,\n} from \"../../graph/make/faces.js\";\n\n/**\n * @description Resolve all crossing edges, build faces,\n * walk and discover the boundary.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FOLD}\n */\nexport const planarizeGraph = (graph, epsilon) => {\n\tconst planar = planarize(graph, epsilon);\n\tplanar.vertices_vertices = makeVerticesVertices(planar);\n\tconst faces = makePlanarFaces(planar);\n\tplanar.faces_vertices = faces.faces_vertices;\n\tplanar.faces_edges = faces.faces_edges;\n\tdelete planar.vertices_edges;\n\treturn planar;\n};\n"
  },
  {
    "path": "src/convert/general/svg.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { boundingBox } from \"../../graph/boundary.js\";\nimport { makeEdgesLength } from \"../../graph/make/edges.js\";\n\n/**\n * @description the default bounds, the unit square\n */\nconst unitBounds = { min: [0, 0], span: [1, 1] };\n\n/**\n * @description Given an attribute dictionary with keys and values,\n * Set each key/value as an attribute on the SVG element.\n * @param {Element} el any SVG / DOM element\n * @param {object} attributes an object with keys and values, intended that\n * the values be simple primitives (boolean, number, string)\n */\nexport const setKeysAndValues = (el, attributes = {}) => Object\n\t.keys(attributes)\n\t.forEach(key => el.setAttributeNS(null, key, attributes[key]));\n\n/**\n * @description Convert a bounding box type into a viewbox string\n * @param {Box?} box an object with \"min\" and \"span\" as two points\n * @returns {string} an SVG viewBox string\n */\nexport const boundingBoxToViewBox = (box) => [box.min, box.span]\n\t.flatMap(p => [p[0], p[1]])\n\t.join(\" \");\n\n/**\n * @description Given a FOLD graph, get the 2D viewbox that\n * encloses all vertices.\n * @param {FOLD} graph a FOLD object\n * @returns {string} an SVG viewBox string\n */\nexport const getViewBox = (graph) => {\n\tconst box = boundingBox(graph);\n\treturn box === undefined ? \"\" : boundingBoxToViewBox(box);\n};\n\n/**\n * @todo this method is O(n log n) but could be improved to O(n)\n * possibly with a kind of bucket sort, but then the nth selection\n * would be much less precise and this method should be no longer\n * exported but rather hard coded as a subroutine for getStrokeWidth.\n * @description Get the Nth percentile edge length of edges from a graph.\n * This is useful to get a sense for how thick the strokeWidth should be\n * to make a reasonable rendering.\n * @param {FOLD} graph a FOLD object\n * @param {number} n a scale between 0.0 and 1.0, looking for the\n * nth smallest or largest edge length.\n * @returns {number} the length of the edge at the nth percent of edges\n * sorted by length.\n */\nexport const getNthPercentileEdgeLength = (\n\t{ vertices_coords, edges_vertices, edges_length },\n\tn = 0.1,\n) => {\n\tif (!vertices_coords || !edges_vertices) { return undefined; }\n\tif (!edges_length) {\n\t\tedges_length = makeEdgesLength({ vertices_coords, edges_vertices });\n\t}\n\tconst sortedLengths = edges_length\n\t\t.slice()\n\t\t.sort((a, b) => a - b);\n\tconst index_tenth_percent = Math.max(\n\t\t0,\n\t\tMath.min(\n\t\t\tMath.floor(sortedLengths.length * n),\n\t\t\tsortedLengths.length - 1,\n\t\t),\n\t);\n\treturn sortedLengths[index_tenth_percent];\n};\n\n/**\n * @description Given a FOLD graph, find a suitable stroke-width for\n * the purposes of rendering the edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} [vmax] if the dimensions of the graph are already\n * calculated, provide the longest length along one axis here.\n * @returns {number} a suitable value for a stroke-width\n */\nexport const getStrokeWidth = (graph, vmax) => {\n\tconst v_max = (vmax === undefined\n\t\t? Math.max(...(boundingBox(graph) || unitBounds).span)\n\t\t: vmax);\n\tconst edgeTenthPercent = getNthPercentileEdgeLength(graph, 0.1);\n\treturn edgeTenthPercent\n\t\t? edgeTenthPercent * 0.1\n\t\t: v_max * 0.01;\n};\n"
  },
  {
    "path": "src/convert/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as objToFold from \"./objToFold.js\";\nimport * as opxToFold from \"./opxToFold.js\";\nimport * as svgToFold from \"./svgToFold.js\";\nimport * as foldToSvg from \"./foldToSvg.js\";\nimport * as foldToObj from \"./foldToObj.js\";\nimport * as generalDom from \"./general/dom.js\";\nimport * as generalOptions from \"./general/options.js\";\nimport * as generalPlanarize from \"./general/planarize.js\";\nimport * as generalSVG from \"./general/svg.js\";\nimport svg from \"./svg/index.js\";\n\nexport default {\n\t...objToFold,\n\t...opxToFold,\n\t...svgToFold,\n\t...foldToSvg,\n\t...foldToObj,\n\t...generalDom,\n\t...generalOptions,\n\t...generalPlanarize,\n\t...generalSVG,\n\t...svg,\n};\n"
  },
  {
    "path": "src/convert/objToFold.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfile_spec,\n\tfile_creator,\n} from \"../fold/rabbitear.js\";\nimport {\n\tmakeVerticesVerticesFromFaces,\n} from \"../graph/make/verticesVertices.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../graph/make/edgesFaces.js\";\nimport {\n\tmakeEdgesFoldAngleFromFaces,\n} from \"../graph/make/edgesFoldAngle.js\";\nimport {\n\tmakeEdgesAssignment,\n} from \"../graph/make/edgesAssignment.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../graph/make/facesEdges.js\";\n\n/**\n * @returns {FOLD} graph a FOLD object\n */\nconst newFoldFile = () => ({\n\tfile_spec: file_spec,\n\tfile_creator: file_creator,\n\tfile_classes: [\"singleModel\"],\n\tframe_classes: [],\n\tframe_attributes: [],\n\tvertices_coords: [],\n\tfaces_vertices: [],\n});\n\n/**\n * @param {FOLD} graph a FOLD object\n */\nconst updateMetadata = (graph) => {\n\tif (!graph.edges_foldAngle || !graph.edges_foldAngle.length) { return; }\n\tlet is2D = true;\n\tfor (let i = 0; i < graph.edges_foldAngle.length; i += 1) {\n\t\tif (graph.edges_foldAngle[i] !== 0\n\t\t\t&& graph.edges_foldAngle[i] !== -180\n\t\t\t&& graph.edges_foldAngle[i] !== 180) {\n\t\t\tis2D = false;\n\t\t\tbreak;\n\t\t}\n\t}\n\tgraph.frame_classes.push(is2D ? \"creasePattern\" : \"foldedForm\");\n\tgraph.frame_attributes.push(is2D ? \"2D\" : \"3D\");\n};\n\n/**\n * @param {any[]} list an array of any type.\n */\nconst pairify = (list) => list\n\t.map((val, i, arr) => [val, arr[(i + 1) % arr.length]]);\n\n/**\n * @param {FOLD} graph a FOLD object\n */\nconst makeEdgesVertices = ({ faces_vertices }) => {\n\tconst edgeExists = {};\n\tconst edges_vertices = [];\n\tfaces_vertices\n\t\t.flatMap(pairify)\n\t\t.forEach(edge => {\n\t\t\tconst keys = [edge.join(\" \"), `${edge[1]} ${edge[0]}`];\n\t\t\tif (keys[0] in edgeExists || keys[1] in edgeExists) { return; }\n\t\t\tedges_vertices.push(edge);\n\t\t\tedgeExists[keys[0]] = true;\n\t\t});\n\treturn edges_vertices;\n};\n\n/**\n * @param {string[]} face\n * @returns {number[]} face as a list of vertex indices\n */\nconst parseFace = (face) => face\n\t.slice(1)\n\t.map(str => parseInt(str, 10) - 1);\n\n/**\n * @param {string[]} vertex\n * @returns {[number, number, number]}\n */\nconst parseVertex = (vertex) => {\n\tconst [a, b, c] = vertex.slice(1).map(str => parseFloat(str));\n\treturn [a || 0, b || 0, c || 0];\n};\n\n/**\n * @description Convert an OBJ mesh file into a FOLD object. The conversion\n * will create edge definitions and give them assignments and fold angles\n * depending on the dihedral angles, or boundary edges\n * if only one face is adjacent.\n * @param {string} file a string containing the contents of an OBJ file,\n * expected to contain at least vertices and faces. All groups or object\n * separations are currently ignored, the contents are treated as one object.\n * @returns {FOLD} a FOLD representation of the OBJ file\n * @example\n * const objFile = fs.readFileSync(\"./stanford-bunny.obj\", \"utf-8\");\n * const fold = objToFold(objFile);\n * fs.writeFileSync(\"./bunny.fold\", JSON.stringify(fold, null, 2));\n */\nexport const objToFold = (file) => {\n\tconst lines = file.split(\"\\n\").map(line => line.trim().split(/\\s+/));\n\tconst graph = newFoldFile();\n\tconst linesCharKey = lines\n\t\t.map(line => line[0].toLowerCase());\n\n\tgraph.vertices_coords = lines\n\t\t.filter((_, i) => linesCharKey[i] === \"v\")\n\t\t.map(parseVertex);\n\tgraph.faces_vertices = lines\n\t\t.filter((_, i) => linesCharKey[i] === \"f\")\n\t\t.map(parseFace);\n\tgraph.edges_vertices = makeEdgesVertices(graph);\n\tgraph.faces_edges = makeFacesEdgesFromVertices(graph);\n\tgraph.edges_faces = makeEdgesFacesUnsorted(graph);\n\tgraph.edges_foldAngle = makeEdgesFoldAngleFromFaces(graph);\n\tgraph.edges_assignment = makeEdgesAssignment(graph);\n\tgraph.vertices_vertices = makeVerticesVerticesFromFaces(graph);\n\n\t// edges_faces was built unsorted. the sorted method is slower to construct,\n\t// and unnecessary for our purposes. the user can build this (and the\n\t// other incomplete fields) if they want them.\n\tdelete graph.edges_faces;\n\tupdateMetadata(graph);\n\treturn graph;\n};\n"
  },
  {
    "path": "src/convert/opxToFold.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport RabbitEarWindow from \"../environment/window.js\";\nimport {\n\tplanarizeGraph,\n} from \"./general/planarize.js\";\nimport {\n\tfindEpsilonInObject,\n\tinvertVertical,\n} from \"./general/options.js\";\nimport {\n\tmakeEdgesFoldAngle,\n} from \"../graph/make/edgesFoldAngle.js\";\n\n// there is probably a better way of coding this using XPath\n// although that requires an additional dependency\n\n/**\n * @param {string} input a DOM string\n * @param {string} [mimeType=\"text/xml\"]\n * @returns {Document}\n */\nconst xmlStringToDocument = (input, mimeType = \"text/xml\") => (\n\t(new (RabbitEarWindow().DOMParser)()).parseFromString(input, mimeType)\n);\n\n/**\n * @description given a parsed xml object, get the branch which\n * contains a node which has some node containing the value specified.\n * @param {any} oripa\n * @param {string} value a value to match\n * @returns {Element} a child branch which contains this value\n */\nconst getContainingValue = (oripa, value) => (oripa == null\n\t? null\n\t: Array.from(oripa.childNodes)\n\t\t.filter(el => el.attributes && el.attributes.length)\n\t\t.filter(el => Array.from(el.attributes)\n\t\t\t.filter(attr => attr.nodeValue === value)\n\t\t\t.shift() !== undefined)\n\t\t.shift());\n\n/**\n * @description For each ORIPA line, extract the coordinates\n * and the assignment type. Return each line as a simple\n * Javascript object. XML Entries will be missing if\n * their value is 0, this is taken care of. All values will be\n * parsed into floats.\n * @param {any} oriLineProxy\n * @returns {number[][]} array of array of numbers, each inner\n * array describes one line.\n */\nconst parseOriLineProxy = (oriLineProxy) => Array\n\t.from(oriLineProxy.childNodes)\n\t.filter(line => line.nodeName === \"void\")\n\t.filter(line => line.childNodes)\n\t.map(line => getContainingValue(line, \"oripa.OriLineProxy\"))\n\t.filter(lineData => lineData)\n\t.map(lineData => [\"type\", \"x0\", \"x1\", \"y0\", \"y1\"]\n\t\t.map(key => getContainingValue(lineData, key))\n\t\t.map(el => (el ? Array.from(el.childNodes) : []))\n\t\t.map(children => children\n\t\t\t.filter(child => child.nodeName === \"double\" || child.nodeName === \"int\")\n\t\t\t.shift())\n\t\t.map(node => (node && node.childNodes[0] && \"data\" in node.childNodes[0]\n\t\t\t? node.childNodes[0].data\n\t\t\t: \"0\"))\n\t\t.map(parseFloat));\n\n/**\n * @description For each ORIPA line, extract the coordinates\n * and the assignment type. Return each line as a simple\n * Javascript object. XML Entries will be missing if\n * their value is 0, this is taken care of. All values will be\n * parsed into floats.\n * @param {any} parsed\n * @returns {object} object\n */\nconst parseFileMetadata = (parsed) => {\n\t// this generates a list of strings looking like this\n\t// [\n\t// \t\"lines\", \"title\", \"one single crease\", \"editorName\", \"Kraft\",\n\t// \t\"originalAuthorName\", \"traditional\", \"reference\", \"no references\",\n\t// \t\"memo\", \"this is a square with one diagonal crease\",\n\t// ]\n\tconst strings = Array\n\t\t.from(parsed.getElementsByTagName(\"string\"))\n\t\t.map(el => Array.from(el.childNodes)\n\t\t\t.map(ch => ch.nodeValue)\n\t\t\t.filter(str => str !== \"\")\n\t\t\t.shift());\n\n\tconst titleIndex = strings.indexOf(\"title\");\n\tconst editorNameIndex = strings.indexOf(\"editorName\");\n\tconst originalAuthorNameIndex = strings.indexOf(\"originalAuthorName\");\n\tconst referenceIndex = strings.indexOf(\"reference\");\n\tconst memoIndex = strings.indexOf(\"memo\");\n\n\tconst metadata = {\n\t\tfile_spec: 1.2,\n\t\tfile_creator: \"Rabbit Ear\",\n\t\tfile_classes: [\"singleModel\"],\n\t\tframe_classes: [\"creasePattern\"],\n\t};\n\n\tconst file_authors = [];\n\tconst file_descriptions = [];\n\n\tif (titleIndex !== -1 && strings[titleIndex + 1]) {\n\t\tmetadata.file_title = strings[titleIndex + 1];\n\t}\n\tif (editorNameIndex !== -1 && strings[editorNameIndex + 1]) {\n\t\tfile_authors.push(strings[editorNameIndex + 1]);\n\t}\n\tif (originalAuthorNameIndex !== -1 && strings[originalAuthorNameIndex + 1]) {\n\t\tfile_authors.push(strings[originalAuthorNameIndex + 1]);\n\t}\n\tif (referenceIndex !== -1 && strings[referenceIndex + 1]) {\n\t\tfile_descriptions.push(strings[referenceIndex + 1]);\n\t}\n\tif (memoIndex !== -1 && strings[memoIndex + 1]) {\n\t\tfile_descriptions.push(strings[memoIndex + 1]);\n\t}\n\n\tif (file_authors.length) {\n\t\tmetadata.file_author = file_authors.join(\", \");\n\t}\n\tif (file_descriptions.length) {\n\t\tmetadata.file_description = file_descriptions.join(\", \");\n\t}\n\n\treturn metadata;\n};\n\n/**\n * @description ORIPA line assignments are numbered.\n */\nconst opxAssignment = [\"F\", \"B\", \"M\", \"V\", \"U\"];\n\n/**\n * @param {number[][]} lines\n * @returns {FOLD}\n */\nconst makeLineGraph = (lines) => {\n\t/** @type {[number, number][]} */\n\tconst vertices_coords = lines\n\t\t.flatMap(line => [[line[1], line[3]], [line[2], line[4]]]);\n\t/** @type {[number, number][]} */\n\tconst edges_vertices = lines.map((_, i) => [i * 2, i * 2 + 1]);\n\tconst edges_assignment = lines.map(line => opxAssignment[line[0]]);\n\tconst edges_foldAngle = makeEdgesFoldAngle({ edges_assignment });\n\treturn {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t};\n};\n\n/**\n * @param {string} file an ORIPA file as a string\n */\nexport const opxEdgeGraph = (file) => {\n\tconst parsed = xmlStringToDocument(file, \"text/xml\");\n\tconst arrayOriLineProxy = Array\n\t\t.from(parsed.getElementsByClassName(\"oripa.OriLineProxy\"))\n\t\t.filter(el => el.nodeName === \"array\" || el.tagName === \"array\")\n\t\t.shift();\n\tconst lines = parseOriLineProxy(arrayOriLineProxy);\n\treturn makeLineGraph(lines);\n};\n\n/**\n * @description Convert an ORIPA file into a FOLD object\n * @param {string} file an ORIPA file as a string\n * @param {number | {\n *   epsilon?: number,\n *   invertVertical?: boolean,\n * }} options an epsilon as a number, or an options object with options\n * @returns {FOLD|undefined} a FOLD representation of the ORIPA file\n * @example\n * const opxFile = fs.readFileSync(\"./crane.opx\", \"utf-8\");\n * const fold = opxToFold(opxFile);\n * fs.writeFileSync(\"./crane.fold\", JSON.stringify(fold, null, 2));\n * @example\n * // with options\n * const opxFile = fs.readFileSync(\"./crane.opx\", \"utf-8\");\n * const fold = opxToFold(opxFile, { invertVertical: true, epsilon: 0.1 });\n * fs.writeFileSync(\"./crane.fold\", JSON.stringify(fold, null, 2));\n */\nexport const opxToFold = (file, options) => {\n\tconst parsed = xmlStringToDocument(file, \"text/xml\");\n\n\t// this will match with one container element and many line elements\n\t// inside of this container. get the container element only (nodeName \"array\")\n\t// this will get us the <array class=\"oripa.OriLineProxy\" length=\"28\">\n\tconst arrayOriLineProxy = Array\n\t\t.from(parsed.getElementsByClassName(\"oripa.OriLineProxy\"))\n\t\t.filter(el => el.nodeName === \"array\" || el.tagName === \"array\")\n\t\t.shift();\n\n\tconst firstDataSet = Array\n\t\t.from(parsed.getElementsByClassName(\"oripa.DataSet\"))\n\t\t.filter(el => el.nodeName === \"object\" || el.tagName === \"object\")\n\t\t.shift();\n\n\tif (firstDataSet === undefined || arrayOriLineProxy === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst lines = parseOriLineProxy(arrayOriLineProxy);\n\tconst file_metadata = parseFileMetadata(parsed);\n\tconst graph = makeLineGraph(lines);\n\n\tif (options\n\t\t&& typeof options === \"object\"\n\t\t&& options.invertVertical\n\t\t&& graph.vertices_coords) {\n\t\tinvertVertical(graph.vertices_coords);\n\t}\n\t// analysis on vertices_coords to find an appropriate epsilon\n\tconst epsilon = findEpsilonInObject(graph, options);\n\tconst planarGraph = planarizeGraph(graph, epsilon);\n\n\treturn {\n\t\t...file_metadata,\n\t\t...planarGraph,\n\t};\n};\n"
  },
  {
    "path": "src/convert/svg/color.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport window from \"../../environment/window.js\";\nimport {\n\trgbToAssignment,\n} from \"../../fold/colors.js\";\nimport {\n\tparseColorToHex,\n\tparseColorToRgb,\n} from \"../../svg/colors/parseColor.js\";\n\n/**\n *\n */\nexport const colorToAssignment = (color, customAssignments) => {\n\tconst hex = parseColorToHex(color).toUpperCase();\n\treturn customAssignments && customAssignments[hex]\n\t\t? customAssignments[hex]\n\t\t: rgbToAssignment(...parseColorToRgb(color));\n};\n\n/**\n *\n */\nexport const opacityToFoldAngle = (opacity, assignment) => {\n\tswitch (assignment) {\n\tcase \"M\": case \"m\": return -180 * opacity;\n\tcase \"V\": case \"v\": return 180 * opacity;\n\t// \"F\", \"B\", \"U\", \"C\", opacity value doesn't matter.\n\tdefault: return 0;\n\t}\n};\n\n/**\n * @returns {string|undefined}\n */\nexport const getEdgeStroke = (element, attributes) => {\n\tconst computedStroke = window().getComputedStyle != null\n\t\t? window().getComputedStyle(element).stroke\n\t\t: \"\";\n\tif (computedStroke !== \"\" && computedStroke !== \"none\") {\n\t\treturn computedStroke;\n\t}\n\tif (attributes.stroke !== undefined) {\n\t\treturn attributes.stroke;\n\t}\n\treturn undefined;\n};\n\n/**\n *\n */\nexport const getEdgeOpacity = (element, attributes) => {\n\tconst computedOpacity = window().getComputedStyle != null\n\t\t? window().getComputedStyle(element).opacity\n\t\t: \"\";\n\tif (computedOpacity !== \"\") {\n\t\tconst floatOpacity = parseFloat(computedOpacity);\n\t\tif (!Number.isNaN(floatOpacity)) { return floatOpacity; }\n\t}\n\tif (attributes.opacity !== undefined) {\n\t\tconst floatOpacity = parseFloat(attributes.opacity);\n\t\tif (!Number.isNaN(floatOpacity)) { return floatOpacity; }\n\t}\n\treturn undefined;\n};\n"
  },
  {
    "path": "src/convert/svg/drawBoundaries.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport SVG from \"../../svg/index.js\";\nimport { addClass } from \"../../svg/general/dom.js\";\nimport { boundaries } from \"../../graph/boundary.js\";\nimport { isFoldedForm } from \"../../fold/spec.js\";\nimport { setKeysAndValues } from \"../general/svg.js\";\n\n/**\n * @description the default styles of the boundary polygon,\n * depending on if it is a folded model or crease pattern\n */\nconst BOUNDARY_FOLDED = {\n\tfill: \"none\",\n};\nconst BOUNDARY_CP = {\n\tstroke: \"black\",\n\tfill: \"white\",\n};\n\nexport const drawBoundaries = (graph, options = {}) => {\n\tconst g = SVG.g();\n\tif (!graph) { return g; }\n\n\t// get the boundary of the graph as a list of points\n\tconst polygons = boundaries(graph)\n\t\t.map(({ vertices }) => vertices.map(v => graph.vertices_coords[v]))\n\t\t.filter(polygon => polygon.length);\n\n\t// give the boundary polygons a class and add them to the group\n\tpolygons.forEach(polygon => {\n\t\tconst svgPolygon = SVG.polygon(polygon);\n\t\taddClass(svgPolygon, \"boundary\");\n\t\tg.appendChild(svgPolygon);\n\t});\n\n\t// default and user styles will be applied to the group, not the polygon\n\tsetKeysAndValues(g, isFoldedForm(graph) ? BOUNDARY_FOLDED : BOUNDARY_CP);\n\tsetKeysAndValues(g, options);\n\treturn g;\n};\n"
  },
  {
    "path": "src/convert/svg/drawEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tassignmentFlatFoldAngle,\n\tedgesFoldAngleAreAllFlat,\n\tedgesAssignmentNames,\n\tisFoldedForm,\n\tedgesAssignmentValues,\n\tsortEdgesByAssignment,\n} from \"../../fold/spec.js\";\nimport {\n\tassignmentColor,\n} from \"../../fold/colors.js\";\nimport {\n\tmakeEdgesCoords,\n} from \"../../graph/make/edges.js\";\nimport {\n\taddClass,\n} from \"../../svg/general/dom.js\";\nimport SVG from \"../../svg/index.js\";\nimport {\n\tclone,\n} from \"../../general/clone.js\";\nimport {\n\tsetKeysAndValues,\n} from \"../general/svg.js\";\n\n/**\n * @description default style to be applied to the group <g> element\n */\nconst GROUP_STYLE = {\n\tfoldedForm: {},\n\tcreasePattern: { stroke: \"black\" },\n};\n\n/**\n * @description default style to be applied to individual edges\n */\nconst EDGE_STYLE = {\n\tfoldedForm: {},\n\tcreasePattern: {}, // this will be filled below\n};\n\nObject.keys(assignmentColor).forEach(key => {\n\tEDGE_STYLE.creasePattern[key] = { stroke: assignmentColor[key] };\n});\n\n/**\n * @description Apply a data attribute (\"data-\") to an element, enabling\n * the user to be able to get this data using the .dataset selector.\n * https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes\n */\nconst setDataValue = (el, key, value) => el.setAttribute(`data-${key}`, value);\n\n/**\n * @description convert a line segment (array of two points) into an\n * SVG path command.\n * @param {number[][]} s a segment as an array of two points\n * @returns {string} an SVG path definition for the line segment.\n */\nconst segmentToPath = s => `M${s[0][0]} ${s[0][1]}L${s[1][0]} ${s[1][1]}`;\n\n/**\n * @description convert a line segment (array of two points) into an\n * SVG path command.\n * @param {FOLD} graph a FOLD object\n * @returns {string} an SVG path definition for all of the edges\n */\nconst edgesPathData = (graph) => (\n\tgraph.vertices_coords && graph.edges_vertices\n\t\t? makeEdgesCoords(graph).map(segment => segmentToPath(segment)).join(\"\")\n\t\t: \"\"\n);\n\n/**\n * @description Group edges by similar assignments and create a single\n * SVG path for every group, each path contains all edges of the same\n * assignment.\n * @param {FOLD} graph a FOLD object\n * @returns {object} keys are assignments, and values are a single\n * SVG path which renders all edges that are this assignment.\n */\nconst edgesPathsByAssignment = ({\n\tvertices_coords, edges_vertices, edges_assignment,\n}) => {\n\tif (!vertices_coords || !edges_vertices) { return {}; }\n\tif (!edges_assignment) {\n\t\treturn { U: edgesPathData({ vertices_coords, edges_vertices }) };\n\t}\n\n\t// a dictionary with key: assignment, and value: array of edge indices\n\tconst assignmentEdges = sortEdgesByAssignment({\n\t\tvertices_coords, edges_vertices, edges_assignment,\n\t});\n\n\t// if an assignment has no edges (empty array), delete the assignment\n\tObject.keys(assignmentEdges).forEach(key => {\n\t\tif (!assignmentEdges[key].length) { delete assignmentEdges[key]; }\n\t});\n\n\t// convert assignmentEdges which contains array of indices [1, 2, 3...]\n\t// into one SVG path that traces out all edges with that assignment\n\tconst assignmentPaths = {};\n\tObject.keys(assignmentEdges).forEach(assignment => {\n\t\t// create the path \"d\" attribute as a string.\n\t\tconst pathString = edgesPathData({\n\t\t\tvertices_coords,\n\t\t\tedges_vertices: assignmentEdges[assignment].map(i => edges_vertices[i]),\n\t\t});\n\n\t\t// create an SVG path\n\t\tassignmentPaths[assignment] = SVG.path(pathString);\n\t});\n\n\treturn assignmentPaths;\n};\n\n/**\n * @description Create a copy of the object, but filter out any\n * keys/values in the object where the key is an edge assignment.\n */\nconst nonAssignmentObject = (object) => {\n\tconst copy = clone(object);\n\tObject.keys(copy)\n\t\t.filter(key => edgesAssignmentNames[key] !== undefined)\n\t\t.forEach(key => delete copy[key]);\n\treturn copy;\n};\n\n/**\n * @description Create a copy of the object, but filter out any\n * keys/values where the value is not a primitive type (boolean, number, string)\n */\nconst objectWithPrimitiveValues = (object) => {\n\t// these are the primitive types we are allowing\n\tconst valid = { boolean: true, number: true, string: true };\n\tconst copy = clone(object);\n\tObject.keys(copy)\n\t\t// filter the keys which are not primitive types, delete these from the obj\n\t\t.filter(key => !valid[typeof copy[key]])\n\t\t.forEach(key => delete copy[key]);\n\treturn copy;\n};\n\n/**\n * @description Create a style object for group element and individual edges\n * (by assignment). Determine if the graph is folded or cp, and process\n * any style options that were provided by the user.\n * @param {FOLD} graph a FOLD object\n * @param {object} options optional custom edge styles\n * @returns {object} groupStyle and edgeStyle as objects.\n */\nconst getStyles = (graph, options) => {\n\t// the styles depend on whether the graph is foldedForm or creasePattern\n\tconst foldedClass = isFoldedForm(graph) ? \"foldedForm\" : \"creasePattern\";\n\tconst groupStyle = clone(GROUP_STYLE[foldedClass]);\n\tconst edgeStyle = clone(EDGE_STYLE[foldedClass]);\n\n\t// override any edge styles if the user has supplied custom style.\n\t// user style can come in two forms:\n\t// - specific to a certain assignment: { M: { stroke: \"red\" }}\n\t// - global to all edges: { stroke: \"black\" }\n\t// global to all edges cannot simply be set on the group which contains\n\t// all of the paths, as we are still setting default styles on each path\n\t// and these path styles would override the group styles, so,\n\t// we have to set these on each path too.\n\t// So, this object contains all valid (string/number/boolean) values\n\t// not including any styles specific to an assignment.\n\tconst override = objectWithPrimitiveValues(nonAssignmentObject(options));\n\n\t// update the group styles, and each assignment's path style dictionaries\n\tObject.assign(groupStyle, override);\n\tedgesAssignmentValues.forEach(key => {\n\t\tedgeStyle[key] = { ...edgeStyle[key], ...override };\n\t});\n\n\treturn {\n\t\tgroupStyle,\n\t\tedgeStyle,\n\t};\n};\n\n/**\n * @description Convert the edges of a FOLD graph into SVG path elements,\n * where all edges of similar assignments are included in the same path.\n * Also, allow the user to supply an options object for custom styles.\n * @param {FOLD} graph a FOLD object\n * @param {object} options an options object for styling edges.\n * @returns {SVGElement} an SVG group containing SVG paths.\n */\nexport const edgesPaths = (graph, options = {}) => {\n\tconst group = SVG.g();\n\tif (!graph) { return group; }\n\n\tconst {\n\t\tgroupStyle,\n\t\tedgeStyle,\n\t} = getStyles(graph, options);\n\n\t// create the path objects, stored under assignment keys\n\tconst paths = edgesPathsByAssignment(graph);\n\n\t// apply the styles to the path elements, based on their assignment\n\tObject.keys(paths).forEach(key => {\n\t\t// add the class name, and any default styles according to the assignment\n\t\taddClass(paths[key], edgesAssignmentNames[key]);\n\t\tsetKeysAndValues(paths[key], edgeStyle[key]);\n\n\t\t// add any custom user styles according to the assignment, allowing\n\t\t// the user to specify letters or words (\"M\" or \"mountain\") as the key\n\t\tsetKeysAndValues(paths[key], options[key]);\n\t\tsetKeysAndValues(paths[key], options[edgesAssignmentNames[key]]);\n\t});\n\n\t// apply the style to the top level group container\n\tsetKeysAndValues(group, groupStyle);\n\n\t// add paths as children to the group\n\tObject.keys(paths).forEach(key => group.appendChild(paths[key]));\n\n\t// set data values to contain the FOLD file data\n\tObject.keys(paths)\n\t\t.forEach(assign => setDataValue(paths[assign], \"assignment\", assign));\n\tObject.keys(paths)\n\t\t.forEach(assign => setDataValue(paths[assign], \"foldAngle\", assignmentFlatFoldAngle[assign]));\n\n\treturn group;\n};\n\n/**\n * @param {number} foldAngle\n * @returns {string}\n */\nconst angleToOpacity = (foldAngle) => String(Math.abs(foldAngle) / 180);\n\n/**\n * @description Convert the edges of a FOLD graph into SVG line elements.\n * Allow the user to supply an options object for custom styles.\n * @param {FOLD} graph a FOLD object\n * @param {object} options an options object for styling edges.\n * @returns {SVGElement} an SVG group containing SVG lines.\n */\nexport const edgesLines = (graph, options = {}) => {\n\tconst group = SVG.g();\n\tif (!graph) { return group; }\n\n\tconst {\n\t\tgroupStyle,\n\t\tedgeStyle,\n\t} = getStyles(graph, options);\n\n\t// a dictionary with assignment keys, and SVG groups as values\n\tconst groupsByAssignment = {};\n\n\t// set line styles according to assignment\n\t// array in the form [\"B\", \"M\", \"V\", \"F\"...]\n\tArray.from(new Set(edgesAssignmentValues.map(s => s.toUpperCase())))\n\t\t.forEach(assign => {\n\t\t\tconst assignmentGroup = SVG.g();\n\n\t\t\t// add the class name, and any default styles according to the assignment\n\t\t\taddClass(assignmentGroup, edgesAssignmentNames[assign]);\n\t\t\tsetKeysAndValues(assignmentGroup, edgeStyle[assign]);\n\n\t\t\t// add any custom user styles according to the assignment, allowing\n\t\t\t// the user to specify letters or words (\"M\" or \"mountain\") as the key\n\t\t\tsetKeysAndValues(assignmentGroup, options[assign]);\n\t\t\tsetKeysAndValues(assignmentGroup, options[edgesAssignmentNames[assign]]);\n\n\t\t\tgroupsByAssignment[assign] = assignmentGroup;\n\t\t});\n\n\t// for every edge, create an SVG line element\n\tconst lines = makeEdgesCoords(graph)\n\t\t.map(s => SVG.line(s[0][0], s[0][1], s[1][0], s[1][1]));\n\n\t// set data value attributes for fold angle and assignment\n\tif (graph.edges_foldAngle) {\n\t\tgraph.edges_foldAngle\n\t\t\t.forEach((angle, i) => setDataValue(lines[i], \"foldAngle\", angle));\n\t}\n\tif (graph.edges_assignment) {\n\t\tgraph.edges_assignment\n\t\t\t.forEach((assign, i) => setDataValue(lines[i], \"assignment\", assign));\n\t}\n\n\tif (graph.edges_foldAngle) {\n\t\tlines.forEach((line, i) => {\n\t\t\tconst angle = graph.edges_foldAngle[i];\n\t\t\tif (angle === undefined\n\t\t\t\t|| angle === null\n\t\t\t\t|| angle === 0\n\t\t\t\t|| angle === 180\n\t\t\t\t|| angle === -180) { return; }\n\t\t\tline.setAttributeNS(null, \"opacity\", angleToOpacity(angle));\n\t\t});\n\t}\n\n\tif (graph.edges_assignment) {\n\t\t// add lines to their corresponding assignment's line group.\n\t\tlines.forEach((line, i) => {\n\t\t\tconst assignment = graph.edges_assignment[i] || \"U\";\n\t\t\tgroupsByAssignment[assignment].appendChild(line);\n\t\t});\n\t} else {\n\t\t// add all lines to the \"unassigned\" group if no assignments exist\n\t\tlines.forEach(line => groupsByAssignment.U.appendChild(line));\n\t}\n\n\t// add the assignmentGroup to the main group, if it contains children\n\tObject.keys(groupsByAssignment)\n\t\t.filter(key => groupsByAssignment[key].childNodes.length)\n\t\t.forEach(key => group.appendChild(groupsByAssignment[key]));\n\n\t// top level group style\n\tsetKeysAndValues(group, groupStyle);\n\treturn group;\n};\n\n/**\n * @description Convert the edges of a FOLD graph into SVG line or path elements\n * and return the result as a group element <g> containing the lines.\n *\n * If the fold angles are all flat, all edges of the same assignment can have\n * the same style, so, we draw them all as SVG path objects. Otherwise if there\n * exists edges with non flat fold angles, simply draw all of them as lines,\n * ensuring that some can have the style associated with fold angle (opacity).\n */\nexport const drawEdges = (graph, options) => (\n\tedgesFoldAngleAreAllFlat(graph)\n\t\t? edgesPaths(graph, options)\n\t\t: edgesLines(graph, options)\n);\n"
  },
  {
    "path": "src/convert/svg/drawFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { isFoldedForm } from \"../../fold/spec.js\";\nimport { makeFacesWinding } from \"../../graph/faces/winding.js\";\nimport { linearize2DFaces } from \"../../graph/orders.js\";\nimport { addClass } from \"../../svg/general/dom.js\";\nimport SVG from \"../../svg/index.js\";\nimport { setKeysAndValues } from \"../general/svg.js\";\n\n/**\n * @description each face is given a name depending on the winding order\n * front: counter-clockwise, back: clockwise.\n */\nconst facesSideNames = [\"front\", \"back\"];\n\n/**\n * @description default style to be applied to individual face polygons\n */\nconst FACE_STYLE = {\n\tfoldedForm: {\n\t\tordered: {\n\t\t\tback: { fill: \"white\" },\n\t\t\tfront: { fill: \"#ddd\" },\n\t\t},\n\t\tunordered: {\n\t\t\tback: { opacity: 0.1 },\n\t\t\tfront: { opacity: 0.1 },\n\t\t},\n\t},\n\tcreasePattern: {},\n};\n\n/**\n * @description default style to be applied to the group <g> element\n */\nconst GROUP_STYLE = {\n\tfoldedForm: {\n\t\tordered: {\n\t\t\tstroke: \"black\",\n\t\t\t\"stroke-linejoin\": \"bevel\",\n\t\t},\n\t\tunordered: {\n\t\t\tstroke: \"none\",\n\t\t\tfill: \"black\",\n\t\t\t\"stroke-linejoin\": \"bevel\",\n\t\t},\n\t},\n\tcreasePattern: {\n\t\tfill: \"none\",\n\t},\n};\n\n/**\n * @description this method will check for layer order, face windings,\n * and apply a style to each face accordingly, adds them to the group,\n * and applies style attributes to the group itself too.\n */\nconst finalize_faces = (graph, svg_faces, group, options = {}) => {\n\tconst isFolded = isFoldedForm(graph);\n\n\t// capable of determining order from faceOrders (spec) or faces_layer\n\tconst orderIsCertain = !!(graph.faceOrders || graph.faces_layer);\n\tconst faces_winding = makeFacesWinding(graph);\n\n\t// set the style of each individual face, depending on the\n\t// face's visible side (front/back) and foldedForm vs. crease pattern\n\tfaces_winding\n\t\t.map(w => (w ? facesSideNames[0] : facesSideNames[1]))\n\t\t.forEach((className, i) => {\n\t\t\t// counter-clockwise faces are \"face up\", their front facing the camera\n\t\t\t// clockwise faces means \"flipped\", their back is facing the camera.\n\t\t\t// set these class names, and apply the style as attributes on each face.\n\t\t\taddClass(svg_faces[i], className);\n\n\t\t\t// Apply a data attribute (\"data-\") to an element, enabling the user\n\t\t\t// to be able to get this data using the .dataset selector.\n\t\t\t// https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes\n\t\t\tsvg_faces[i].setAttribute(\"data-side\", className);\n\n\t\t\t// cp / folded style, which is based on known or unknown face order\n\t\t\tconst foldedFaceStyle = orderIsCertain\n\t\t\t\t? FACE_STYLE.foldedForm.ordered[className]\n\t\t\t\t: FACE_STYLE.foldedForm.unordered[className];\n\t\t\tconst faceStyle = isFolded\n\t\t\t\t? foldedFaceStyle\n\t\t\t\t: FACE_STYLE.creasePattern[className];\n\n\t\t\t// set the face style (front/back in the context of CP or foldedForm)\n\t\t\tsetKeysAndValues(svg_faces[i], faceStyle);\n\n\t\t\t// set any custom user style that was provided in the options object\n\t\t\tsetKeysAndValues(svg_faces[i], options[className]);\n\t\t});\n\n\t// get a list of face indices, 0...N-1, or in the case of a layer order\n\t// existing, give us these indices in layer-sorted order.\n\t// using this order, append the faces to the parent group.\n\tlinearize2DFaces(graph).forEach(f => group.appendChild(svg_faces[f]));\n\n\t// set style attributes of the group\n\tconst groupStyleFolded = orderIsCertain\n\t\t? GROUP_STYLE.foldedForm.ordered\n\t\t: GROUP_STYLE.foldedForm.unordered;\n\tsetKeysAndValues(group, isFolded ? groupStyleFolded : GROUP_STYLE.creasePattern);\n\n\treturn group;\n};\n\n/**\n * @description build SVG faces using faces_vertices data. this is\n * slightly faster than the other method which uses faces_edges.\n * @returns {SVGElement[]} an SVG <g> group element containing all\n * of the <polygon> faces as children.\n */\nexport const facesVerticesPolygon = (graph, options) => {\n\tconst svg_faces = graph.faces_vertices\n\t\t.map(fv => fv.map(v => [0, 1].map(i => graph.vertices_coords[v][i])))\n\t\t.map(face => SVG.polygon(face));\n\tsvg_faces.forEach((face, i) => face.setAttributeNS(null, \"index\", i));\n\treturn finalize_faces(graph, svg_faces, SVG.g(), options);\n};\n\n/**\n * @description build SVG faces using faces_edges data. this is\n * slightly slower than the other method which uses faces_vertices.\n * @returns {SVGElement[]} an SVG <g> group element containing all\n * of the <polygon> faces as children.\n */\nexport const facesEdgesPolygon = function (graph, options) {\n\tconst svg_faces = graph.faces_edges\n\t\t.map(face_edges => face_edges\n\t\t\t.map(edge => graph.edges_vertices[edge])\n\t\t\t.map((vi, i, arr) => {\n\t\t\t\tconst next = arr[(i + 1) % arr.length];\n\t\t\t\treturn (vi[1] === next[0] || vi[1] === next[1] ? vi[0] : vi[1]);\n\t\t\t}).map(v => [0, 1].map(i => graph.vertices_coords[v][i])))\n\t\t.map(face => SVG.polygon(face));\n\tsvg_faces.forEach((face, i) => face.setAttributeNS(null, \"index\", i));\n\treturn finalize_faces(graph, svg_faces, SVG.g(), options);\n};\n\n/**\n * @description Convert the faces of a FOLD graph into SVG polygon elements.\n * Return the result as a group element <g> with all faces (if they exist)\n * as childNodes in the group.\n */\nexport const drawFaces = (graph, options) => {\n\tif (graph && graph.vertices_coords && graph.faces_vertices) {\n\t\treturn facesVerticesPolygon(graph, options);\n\t}\n\tif (graph && graph.vertices_coords && graph.edges_vertices && graph.faces_edges) {\n\t\treturn facesEdgesPolygon(graph, options);\n\t}\n\treturn SVG.g();\n};\n"
  },
  {
    "path": "src/convert/svg/drawVertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport SVG from \"../../svg/index.js\";\nimport { setKeysAndValues } from \"../general/svg.js\";\n\n/**\n * @description Convert the vertices of a FOLD graph into SVG circle elements.\n * Return the result as a group element <g> containing the circles.\n */\nexport const drawVertices = (graph, options = {}) => {\n\tconst g = SVG.g();\n\tif (!graph || !graph.vertices_coords) { return g; }\n\n\t// radius will be overwritten inside render(), in applyTopLevelOptions()\n\t// leave a default radius here in case this method is called directly\n\tgraph.vertices_coords\n\t\t.map(v => SVG.circle(v[0], v[1], 0.01))\n\t\t.forEach(v => g.appendChild(v));\n\n\t// any user-specified style will be applied to the group\n\tsetKeysAndValues(g, options);\n\n\treturn g;\n};\n"
  },
  {
    "path": "src/convert/svg/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as color from \"./color.js\";\nimport * as drawBoundaries from \"./drawBoundaries.js\";\nimport * as drawVertices from \"./drawVertices.js\";\nimport * as drawEdges from \"./drawEdges.js\";\nimport * as drawFaces from \"./drawFaces.js\";\nimport * as parse from \"./parse.js\";\nimport * as stylesheet from \"./stylesheet.js\";\n\nexport default {\n\t...color,\n\t...drawBoundaries,\n\t...drawVertices,\n\t...drawEdges,\n\t...drawFaces,\n\t...parse,\n\t...stylesheet,\n};\n"
  },
  {
    "path": "src/convert/svg/parse.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tepsilonEqualVectors,\n} from \"../../math/compare.js\";\nimport {\n\tparsePathCommandsWithEndpoints,\n} from \"../../svg/general/path.js\";\n\nconst straightPathLines = {\n\tL: true, V: true, H: true, Z: true,\n};\n\n/**\n * @description Get the values of a list of attributes on an element\n * SVG is allowed to leave out coordinates with an implied value of 0\n * @param {Element} element an SVG element\n * @param {string[]} attributes a list of names of element attributes\n * @returns {number[]} a list of numbers, one for every attribute\n */\nconst getAttributesFloatValue = (element, attributes) => attributes\n\t.map(attr => element.getAttribute(attr))\n\t.map(str => (str == null ? 0 : str))\n\t.map(parseFloat);\n\n/**\n * @description convert an SVG path into segments\n * @param {Element} line an SVG line element\n * @returns {[number, number, number, number][]} a list of segments\n * in the form of 4 numbers (x1, y1, x2, y2)\n */\nexport const lineToSegments = (line) => {\n\tconst [a, b, c, d] = getAttributesFloatValue(line, [\"x1\", \"y1\", \"x2\", \"y2\"]);\n\treturn [[a, b, c, d]];\n};\n\n/**\n * @description convert an SVG path into segments\n * @param {Element} path an SVG path element\n * @returns {[number, number, number, number][]} a list of segments\n * in the form of 4 numbers (x1, y1, x2, y2)\n */\nexport const pathToSegments = (path) => (\n\tparsePathCommandsWithEndpoints(path.getAttribute(\"d\") || \"\")\n\t\t.filter(command => straightPathLines[command.command.toUpperCase()])\n\t\t.map(el => [el.start, el.end])\n\t\t.filter(([a, b]) => !epsilonEqualVectors(a, b))\n\t\t.map(([a, b]) => [a[0], a[1], b[0], b[1]])\n);\n\nconst pointsStringToArray = str => {\n\tconst list = str.split(/[\\s,]+/).map(parseFloat);\n\treturn Array\n\t\t.from(Array(Math.floor(list.length / 2)))\n\t\t.map((_, i) => [list[i * 2 + 0], list[i * 2 + 1]]);\n};\n\n// export const pointStringToArray = function (str) {\n// \treturn str.split(/[\\s,]+/)\n// \t\t.filter(s => s !== \"\")\n// \t\t.map(p => p.split(\",\")\n// \t\t\t.map(n => parseFloat(n)));\n// };\n\n/**\n * @description convert an SVG polygon into segments\n * @param {Element} polygon an SVG polygon element\n * @returns {[number, number, number, number][]} a list of segments\n * in the form of 4 numbers (x1, y1, x2, y2)\n */\nexport const polygonToSegments = (polygon) => (\n\tpointsStringToArray(polygon.getAttribute(\"points\") || \"\")\n\t\t.map((_, i, arr) => [\n\t\t\tarr[i][0],\n\t\t\tarr[i][1],\n\t\t\tarr[(i + 1) % arr.length][0],\n\t\t\tarr[(i + 1) % arr.length][1],\n\t\t])\n);\n\n/**\n * @description convert an SVG polyline into segments\n * @param {Element} polyline an SVG polygon element\n * @returns {[number, number, number, number][]} a list of segments\n * in the form of 4 numbers (x1, y1, x2, y2)\n */\nexport const polylineToSegments = function (polyline) {\n\tconst circularPath = polygonToSegments(polyline);\n\tcircularPath.pop();\n\treturn circularPath;\n};\n\n/**\n * @description convert an SVG rect into segments\n * @param {Element} rect an SVG polygon element\n * @returns {[number, number, number, number][]} a list of segments\n * in the form of 4 numbers (x1, y1, x2, y2)\n */\nexport const rectToSegments = function (rect) {\n\tconst [x, y, w, h] = getAttributesFloatValue(\n\t\trect,\n\t\t[\"x\", \"y\", \"width\", \"height\"],\n\t);\n\treturn [\n\t\t[x, y, x + w, y],\n\t\t[x + w, y, x + w, y + h],\n\t\t[x + w, y + h, x, y + h],\n\t\t[x, y + h, x, y],\n\t];\n};\n"
  },
  {
    "path": "src/convert/svg/stylesheet.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport window from \"../../environment/window.js\";\nimport {\n\tgetRootParent,\n} from \"../../svg/general/dom.js\";\n\n/**\n * @description Convert a style element, CSSStyleSheet, into a nested\n * object with selectors as keys, then attributes as 2nd layer keys.\n * @param {CSSStyleSheet} sheet the \"sheet\" property on a style element\n * @returns {object} dictionary representation of a style element\n */\nexport const parseCSSStyleSheet = (sheet) => {\n\tif (!sheet.cssRules) { return {}; }\n\tconst stylesheets = {};\n\t// convert the array of type {CSSRule[]} to an object\n\t// with the key:value being the key and the contents of that rule.\n\tfor (let i = 0; i < sheet.cssRules.length; i += 1) {\n\t\tconst cssRules = sheet.cssRules[i];\n\t\tif (cssRules.constructor.name !== \"CSSStyleRule\") { continue; }\n\t\t/** @type {any} CSSStyleRule */\n\t\tconst cssStyleRule = cssRules;\n\t\t// if (cssRules.type !== 1) { continue; }\n\t\tconst selectorList = cssStyleRule.selectorText\n\t\t\t.split(/,/gm)\n\t\t\t.filter(Boolean)\n\t\t\t.map(str => str.trim());\n\t\tconst style = {};\n\t\tObject.values(cssStyleRule.style)\n\t\t\t.forEach(key => { style[key] = cssStyleRule.style[key]; });\n\t\tselectorList.forEach(selector => {\n\t\t\tstylesheets[selector] = style;\n\t\t});\n\t}\n\treturn stylesheets;\n};\n\n/**\n * @description This is a very strange function, and it's all because\n * of a very strange quirk that CSSStyleSheet property does not exist\n * on a node *which is not bound to the window.document*\n * (ie: converted from XML string), but it does exist if it is.\n * So, we detect if this is the case, quickly append it to the body,\n * process the style sheets, then remove it from the DOM.\n * @param {SVGStyleElement} style the SVG style DOM element\n * @returns {object[]} style sheets as objects, with CSS selectors as\n * keys, for example: { \".redline\": { \"stroke-width\": \"0.5\" } }.\n */\nexport const parseStyleElement = (style) => {\n\t/** @type {CSSStyleSheet|undefined} */\n\tconst styleSheet = \"sheet\" in style ? style.sheet : undefined;\n\tif (styleSheet) { return parseCSSStyleSheet(styleSheet); }\n\tconst rootParent = getRootParent(style);\n\tconst isHTMLBound = rootParent.constructor === window().HTMLDocument;\n\tif (!isHTMLBound) {\n\t\t// remove style from parent. append to document.body\n\t\tconst prevParent = style.parentNode;\n\t\tif (prevParent != null) {\n\t\t\tprevParent.removeChild(style);\n\t\t}\n\t\tconst body = window().document.body != null\n\t\t\t? window().document.body\n\t\t\t: window().document.createElement(\"body\");\n\t\tbody.appendChild(style);\n\t\t// parse style sheet.\n\t\tconst parsedStyle = parseCSSStyleSheet(styleSheet);\n\t\t// remove style from document.body. append to previous parent\n\t\tbody.removeChild(style);\n\t\tif (prevParent != null) {\n\t\t\tprevParent.appendChild(style);\n\t\t}\n\t\treturn parsedStyle;\n\t}\n\treturn [];\n};\n\n/**\n * @description Given one or many parsed stylesheets, hunt for a match based\n * on nodeName selector, id selector, or class selector, and return the value\n * for one attribute if it exists. So for example, you need \"stroke\" and this\n * will search for a \"stroke\" property defined on line, .className, or #id.\n */\nexport const getStylesheetStyle = (key, nodeName, attributes, sheets = []) => {\n\tconst classes = attributes.class\n\t\t? attributes.class\n\t\t\t.split(/\\s/)\n\t\t\t.filter(Boolean)\n\t\t\t.map(i => i.trim())\n\t\t\t.map(str => `.${str}`)\n\t\t: [];\n\tconst id = attributes.id\n\t\t? `#${attributes.id}`\n\t\t: null;\n\t// look for a matching id in the style sheets\n\tif (id) {\n\t\tfor (let s = 0; s < sheets.length; s += 1) {\n\t\t\tif (sheets[s][id] && sheets[s][id][key]) {\n\t\t\t\treturn sheets[s][id][key];\n\t\t\t}\n\t\t}\n\t}\n\t// look for a matching class in the style sheets\n\tfor (let s = 0; s < sheets.length; s += 1) {\n\t\tfor (let c = 0; c < classes.length; c += 1) {\n\t\t\tif (sheets[s][classes[c]] && sheets[s][classes[c]][key]) {\n\t\t\t\treturn sheets[s][classes[c]][key];\n\t\t\t}\n\t\t}\n\t\tif (sheets[s][nodeName] && sheets[s][nodeName][key]) {\n\t\t\treturn sheets[s][nodeName][key];\n\t\t}\n\t}\n\treturn undefined;\n};\n"
  },
  {
    "path": "src/convert/svgToFold.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport window from \"../environment/window.js\";\nimport Messages from \"../environment/messages.js\";\nimport {\n\tisBackend,\n} from \"../environment/detect.js\";\nimport {\n\tfile_spec,\n\tfile_creator,\n} from \"../fold/rabbitear.js\";\nimport {\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tmultiplyMatrix2Vector2,\n} from \"../math/matrix2.js\";\nimport {\n\tcleanNumber,\n} from \"../general/number.js\";\nimport {\n\tplanarBoundary,\n} from \"../graph/boundary.js\";\nimport {\n\tparseColorToHex,\n} from \"../svg/colors/parseColor.js\";\nimport {\n\tgetRootParent,\n\txmlStringToElement,\n\tflattenDomTree,\n\tflattenDomTreeWithStyle,\n} from \"../svg/general/dom.js\";\nimport {\n\ttransformStringToMatrix,\n} from \"../svg/general/transforms.js\";\nimport {\n\tfindEpsilonInObject,\n\tinvertVertical,\n} from \"./general/options.js\";\nimport {\n\tplanarizeGraph,\n} from \"./general/planarize.js\";\nimport {\n\tinvisibleParent,\n} from \"./general/dom.js\";\nimport {\n\tcolorToAssignment,\n\topacityToFoldAngle,\n\tgetEdgeStroke,\n\tgetEdgeOpacity,\n} from \"./svg/color.js\";\nimport {\n\tlineToSegments,\n\trectToSegments,\n\tpolygonToSegments,\n\tpolylineToSegments,\n\tpathToSegments,\n} from \"./svg/parse.js\";\n\n/**\n * @description Given an svg drawing element, convert\n * it into an array of line segments {number[][]} where\n * each segment is an array of 4 values: x1, y1, x2, y2.\n * @param {Element[]} elements a flat array of svg drawing elements\n * @returns {object[]} an array of line segement objects, each with\n * \"segment\" and \"attributes\" properties.\n */\nconst parsers = {\n\tline: lineToSegments,\n\trect: rectToSegments,\n\tpolygon: polygonToSegments,\n\tpolyline: polylineToSegments,\n\tpath: pathToSegments,\n\t// circle: circleToSegments,\n\t// ellipse: ellipseToSegments,\n};\n\n/**\n * @description Transform a 2D segment (4 numbers) with a CSS transform.\n * @param {[number, number, number, number]} segment a 2D segment as 4 numbers\n * @param {string} transform a CSS or SVG transform string\n * @return {[number, number][]} segment\n */\nconst transformSegment = (segment, transform) => {\n\t/** @type {[number, number][]} */\n\tconst seg = [[segment[0], segment[1]], [segment[2], segment[3]]];\n\tif (!transform) { return seg; }\n\tconst matrix = transformStringToMatrix(transform);\n\treturn matrix\n\t\t? seg.map(p => multiplyMatrix2Vector2(matrix, p))\n\t\t: seg;\n};\n\n/**\n * @description Get a flat array of all elements in the tree, with all\n * styles also flattened (nested transformed computed, for example)\n * convert all elements <path> <rect> etc into arrays of line segments.\n * @param {Element} svgElement\n * @returns {{\n *   element: Element,\n *   segment: [number, number][],\n *   attributes: { [key: string]: string },\n * }[]} an array of objects with a segment coordinates and attributes\n */\nconst flatSegments = (svgElement) => flattenDomTreeWithStyle(svgElement)\n\t.filter(el => parsers[el.element.nodeName])\n\t.flatMap(el => parsers[el.element.nodeName](el.element)\n\t\t.map(segment => transformSegment(segment, el.attributes.transform))\n\t\t.map(segment => ({ ...el, segment })));\n\n/**\n * @description does an Element contain a <style> as a child somewhere?\n * @param {Element} svgElement\n * @returns {boolean}\n */\nconst containsStylesheet = (svgElement) => flattenDomTree(svgElement)\n\t.map(el => el.nodeName === \"style\")\n\t.reduce((a, b) => a || b, false);\n\n/**\n * @description Given an SVG element (as a string or Element object),\n * Extract all straight lines from the SVG, including those inside of\n * complex path objects. Return the straight lines as a flat array with\n * additional attribute information.\n * @param {Element | string} svg an SVG image as a DOM element\n * or a string.\n * @returns {{\n *   element: Element,\n *   attributes?: { [key: string]: string },\n *   segment: [number, number][],\n *   data: { assignment: string, foldAngle: string },\n *   stroke: string,\n *   opacity: number,\n * }[]} array of objects, one for each straight line segment\n * with these values:\n * - .element a pointer to the element that this segment comes from.\n * - .attributes the attributes of the element as a Javascript object.\n *    this includes those which were inherited from its parents\n * - .segment a pair of vertices, the endpoints of the segment.\n * - .data two \"data-\" attributes representing assignment and foldAngle.\n * - .stroke the stroke attribute taken from getComputedStyle if possible.\n * - .opacity the opacity attribute taken from getComputedStyle if possible.\n */\nexport const svgSegments = (svg) => {\n\tconst svgElement = typeof svg === \"string\"\n\t\t? xmlStringToElement(svg, \"image/svg+xml\")\n\t\t: svg;\n\n\tif (containsStylesheet(svgElement) && isBackend) {\n\t\tconsole.warn(Messages.backendStylesheet);\n\t}\n\t// ensure the svg is a child of the DOM so we can call getComputedStyle.\n\t// If the element is already a child of the HTML document, do nothing.\n\tconst parent = getRootParent(svgElement) === window().document\n\t\t? undefined\n\t\t: invisibleParent(svgElement);\n\n\tconst segments = flatSegments(svgElement);\n\tconst segmentsWithAttrs = segments.map(el => ({\n\t\tdata: {\n\t\t\tassignment: el.attributes[\"data-assignment\"],\n\t\t\tfoldAngle: el.attributes[\"data-foldAngle\"],\n\t\t},\n\t\tstroke: getEdgeStroke(el.element, el.attributes),\n\t\topacity: getEdgeOpacity(el.element, el.attributes),\n\t})).map((addition, i) => ({\n\t\t...segments[i],\n\t\t...addition,\n\t}));\n\n\t// we no longer need computed style, remove invisible svg from DOM.\n\tif (parent && parent.parentNode) {\n\t\tparent.parentNode.removeChild(parent);\n\t}\n\n\treturn segmentsWithAttrs;\n};\n\n/**\n *\n */\nconst getUserAssignmentOptions = (options) => {\n\tif (!options || !options.assignments) { return undefined; }\n\tconst assignments = {};\n\tObject.keys(options.assignments).forEach(key => {\n\t\tconst hex = parseColorToHex(key).toUpperCase();\n\t\tassignments[hex] = options.assignments[key];\n\t});\n\treturn assignments;\n};\n\n/**\n *\n */\nconst getEdgeAssignment = (dataAssignment, stroke = \"#f0f\", customAssignments = undefined) => {\n\tif (dataAssignment) { return dataAssignment; }\n\treturn colorToAssignment(stroke, customAssignments);\n};\n\n/**\n *\n */\nconst getEdgeFoldAngle = (dataFoldAngle, opacity = 1, assignment = undefined) => {\n\tif (dataFoldAngle) { return parseFloat(dataFoldAngle); }\n\treturn opacityToFoldAngle(opacity, assignment);\n};\n\n/**\n *\n */\nconst makeAssignmentFoldAngle = (segments, options) => {\n\t// if the user provides a dictionary of custom stroke-assignment\n\t// matches, this takes precidence over the \"data-\" attribute\n\t// todo: this can be improved\n\tconst customAssignments = getUserAssignmentOptions(options);\n\t// if user specified customAssignments, ignore the data- attributes\n\t// otherwise the user's assignments would be ignored.\n\tif (customAssignments) {\n\t\tsegments.forEach(seg => {\n\t\t\tdelete seg.data.assignment;\n\t\t\tdelete seg.data.foldAngle;\n\t\t});\n\t}\n\t// convert SVG data into FOLD arrays\n\tconst edges_assignment = segments.map(segment => getEdgeAssignment(\n\t\tsegment.data.assignment,\n\t\tsegment.stroke,\n\t\tcustomAssignments,\n\t));\n\tconst edges_foldAngle = segments.map((segment, i) => getEdgeFoldAngle(\n\t\tsegment.data.foldAngle,\n\t\tsegment.opacity,\n\t\tedges_assignment[i],\n\t));\n\treturn {\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t};\n};\n\n/**\n * @param {number} n\n * @returns {number}\n */\nconst passthrough = (n) => n;\n\n/**\n * @description This method will handle all of the SVG parsing\n * and result in a very simple graph representation basically\n * only containing line segments and their assignment/foldAngle.\n * The graph will not be planar (edges will overlap), no faces\n * will exist, and duplicate vertices will exist and need to\n * be merged\n * @param {Element | string} svg an SVG image as a DOM element\n * or a string.\n * @returns {FOLD} a FOLD representation of the SVG image, not\n * yet a planar graph, no faces, and possible edge overlaps.\n */\nexport const svgEdgeGraph = (svg, options) => {\n\tconst segments = svgSegments(svg); // , options);\n\tconst {\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t} = makeAssignmentFoldAngle(segments, options);\n\t// by default the parser will change numbers like 15.000000000001 into 15.\n\t// to turn this off, options.fast = true\n\tconst fixNumber = options && options.fast ? passthrough : cleanNumber;\n\t/** @type {[number, number][]} */\n\tconst vertices_coords = segments\n\t\t.flatMap(el => el.segment)\n\t\t.map(([a, b]) => [fixNumber(a, 12), fixNumber(b, 12)]);\n\t/** @type {[number, number][]} */\n\tconst edges_vertices = segments.map((_, i) => [i * 2, i * 2 + 1]);\n\treturn {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t};\n};\n\n/**\n * @description Convert an SVG to a FOLD object. This only works\n * with SVGs of crease patterns, this will not work\n * with an SVG of a folded form.\n * @param {string | SVGElement} file the SVG element as a\n * document element node, or as a string\n * @param {number | object} options an options object or an epsilon number\n * @returns {FOLD} a FOLD representation of the SVG\n * @example\n * const svgFile = fs.readFileSync(\"./crane.svg\", \"utf-8\");\n * const fold = svgToFold(svgFile);\n * fs.writeFileSync(\"./crane.fold\", JSON.stringify(fold, null, 2));\n * @example\n * // with options\n * const svgFile = fs.readFileSync(\"./crane.svg\", \"utf-8\");\n * const fold = svgToFold(svgFile, { boundary: false, epsilon: 0.1, invertVertical: true });\n * fs.writeFileSync(\"./crane.fold\", JSON.stringify(fold, null, 2));\n */\nexport const svgToFold = (file, options) => {\n\tconst graph = svgEdgeGraph(file, options);\n\tconst epsilon = findEpsilonInObject(graph, options);\n\t// if the user chooses, we can flip the y axis numbers.\n\tif (options && options.invertVertical && graph.vertices_coords) {\n\t\tinvertVertical(graph.vertices_coords);\n\t}\n\tconst planarGraph = planarizeGraph(graph, epsilon);\n\t// by default the parser will change numbers like 15.000000000001 into 15.\n\t// to turn this off, options.fast = true\n\tconst fixNumber = options && options.fast ? passthrough : cleanNumber;\n\tplanarGraph.vertices_coords = planarGraph.vertices_coords\n\t\t.map(coord => coord.map(n => fixNumber(n, 12)))\n\t\t.map(resize2);\n\t// optionally, discover the boundary by walking.\n\tif (typeof options !== \"object\" || options.boundary !== false) {\n\t\t// clear all previous boundary assignments and set them to flat.\n\t\t// this is because both flat and boundary were imported as black\n\t\t// colors (grayscale), so the assignment should go to the next in line.\n\t\tplanarGraph.edges_assignment\n\t\t\t.map((_, i) => i)\n\t\t\t.filter(i => planarGraph.edges_assignment[i] === \"B\"\n\t\t\t\t|| planarGraph.edges_assignment[i] === \"b\")\n\t\t\t.forEach(i => { planarGraph.edges_assignment[i] = \"F\"; });\n\t\tconst { edges } = planarBoundary(planarGraph);\n\t\tedges.forEach(e => { planarGraph.edges_assignment[e] = \"B\"; });\n\t}\n\treturn {\n\t\tfile_spec,\n\t\tfile_creator,\n\t\tframe_classes: [\"creasePattern\"],\n\t\t...planarGraph,\n\t};\n};\n"
  },
  {
    "path": "src/convert/svgToFold.md",
    "content": "# SVG to FOLD\n\nConvert an SVG image into a FOLD object.\n\n```typescript\nfunction svgToFold(svg: (string | SVGElement), options?: (number | object)) : object\n```\n\nThis only works for crease patterns `frame_classes: [\"creasePattern\"]`. This will not work for SVG renderings of a folded origami model `frame_classes: [\"foldedForm\"]`.\n\n# api\n\n```javascript\near.convert.svgToFold(svg);\n```\n\nuse an optional *epsilon* as a second parameter:\n\n```javascript\near.convert.svgToFold(svg, 1e-2);\n```\n\nuse an *options object* as a second parameter:\n\n```javascript\near.convert.svgToFold(svg, options);\n```\n\nwhere options can contain:\n\n```javascript\nlet options = {\n\tepsilon: 1e-2,\n\tassignments: {\n\t\t\"#888\": \"F\",\n\t\t\"black\": \"C\",\n\t},\n\tboundary: false,\n};\n```\n\n- options.epsilon: *(default: computed)* specify the vertex-merge distance\n- options.assignment: *(default: see below)* which color is to convert into which assignment\n- options.boundary: *(default: true)* should the boundary be discovered via walking?\n\nassignment object should be:\n\n- keys: any parseable CSS color (rgb, hsl, hex, named)\n- value: any FOLD spec edges_assignment key (B, M, V, F, J, C, U)\n\n# algorithm\n\n> presently, this only supports straight lines (this includes the parts of an svg &lt;path&gt; which are straight).\n\n1. the endpoints of the straight line elements are extracted after transforms are applied.\n2. two edge attributes are attempted to be discovered: **assignment** and **foldAngle**. see below.\n3. if an epsilon is not provided, one is inferred.\n4. the graph is planarized, new edges/vertices are potentially made, nearby vertices are merged, faces are discovered.\n5. the boundary is discovered by walking, these edges are assigned with \"B\". Also, before this runs, any pre-existing \"B\" creases will be reset to \"F\" (flat) so that only the walked edges will be \"B\".\n\n### assignment\n\n1. if the attribute \"data-assignment\" exists, return this value\n2. otherwise, find the stroke color via window.getComputedStyle or style or attributes\n\nonce the stroke color is found, if the user has specified an `options.assignments` table, if a match is found (via conversion to #hex and string matching), this assignment is returned. Otherwise, the distances to each of these colors is computed and the nearest one is returned:\n\n- **boundary**: black #000\n- **mountain**: red #f00\n- **valley**: blue #00f\n- **join**: yellow #ff0\n- **unassigned**: magenta #f0f\n- **cut**: green #0f0\n- **flat**: gray #888\n\n### foldAngle\n\n1. if the attribute \"data-foldAngle\" exists, return this value\n2. otherwise, find the opacity via window.getComputedStyle or style or attributes\n\nthe opacity relates to fold angle:\n\n```javascript\nabs(foldAngle) = opacity * 180\n```\n\nthen the assignment modifies the result:\n\n- **valley**: positive\n- **mountain**: negative\n- **else**: 0\n"
  },
  {
    "path": "src/diagrams/arrows.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tconvexHull,\n} from \"../math/convexHull.js\";\nimport {\n\tmagnitude2,\n\tdot2,\n\tsubtract2,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tperpendicularBalancedSegment,\n} from \"./general.js\";\nimport {\n\tboundingBox,\n} from \"../math/polygon.js\";\nimport {\n\tclipLineConvexPolygon,\n} from \"../math/clip.js\";\n\n/**\n * @description Given a segment representing an arrow endpoints,\n * and a polygon representing the enclosing space for the arrow,\n * create an arrow definition which includes the segment, as well as\n * some additional details like the size of the arrow head and\n * the direction of the bend in the path.\n * @param {[number, number][]} points an array of two points, each an array of numbers\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow} an arrow definition including a head but no tail.\n */\nexport const arrowFromSegment = (points, options = {}) => {\n\tif (points === undefined) { return undefined; }\n\tconst vector = subtract2(points[1], points[0]);\n\tconst length = magnitude2(vector);\n\n\t// we need a good padding value, arrowheads should not be exactly\n\t// on top of their targets, but spaced a little behind it.\n\tconst padding = options.padding ? options.padding : length * 0.05;\n\n\t// a good arrow whose path bends should always bend up then back down\n\t// like a ball thrown up and returning to Earth.\n\t// \"bend\" will be positive or negative based on the left/right direction\n\tconst direction = dot2(vector, [1, 0]);\n\n\t// prefer to base the size of the head off of the size of the canvas,\n\t// so, please use the options.vmin if possible.\n\t// Otherwise, base the size of the head off of the length of the arrow,\n\t// which has the side effect of a shrinking head as the length shrinks.\n\tconst vmin = options && options.vmin ? options.vmin : length;\n\treturn {\n\t\tsegment: [points[0], points[1]],\n\t\thead: {\n\t\t\twidth: vmin * 0.0666,\n\t\t\theight: vmin * 0.1,\n\t\t},\n\t\tbend: direction > 0 ? 0.3 : -0.3,\n\t\tpadding,\n\t};\n};\n\n/**\n * @description Given a segment representing an arrow endpoints,\n * and a polygon representing the enclosing space for the arrow,\n * create an arrow definition which includes the segment, as well as\n * some additional details like the size of the arrow head and\n * the direction of the bend in the path.\n * @param {[number, number][]} polygon an array of points, each an array of numbers\n * @param {[number, number][]} segment an array of two points, each an array of numbers\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow} an arrow definition including a head but no tail.\n */\nexport const arrowFromSegmentInPolygon = (polygon, segment, options = {}) => {\n\tconst vmin = options.vmin\n\t\t? options.vmin\n\t\t: Math.min(...(boundingBox(polygon)?.span || [1, 1]).slice(0, 2));\n\treturn arrowFromSegment(segment, { ...options, vmin });\n};\n\n/**\n * @param {[number, number][]} polygon\n * @param {VecLine2} line\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow} an arrow definition including a head but no tail.\n */\nexport const arrowFromLine = (polygon, line, options) => {\n\tconst segment = clipLineConvexPolygon(polygon, line)\n\treturn segment === undefined\n\t\t? undefined\n\t\t: arrowFromSegmentInPolygon(polygon, segment, options)\n};\n\n/**\n * @description given an origami model and a fold line, without knowing\n * any of the parameters that determined the fold line, find an arrow\n * that best fits the origami as a diagram step.\n * This is sufficient in many cases, but a more precise arrow might be\n * generated knowing which axiom construction created the fold line.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} foldLine a line specifying the crease.\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow} an arrow definition including a head but no tail.\n */\nexport const foldLineArrow = ({ vertices_coords }, foldLine, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2)\n\t\t.map(v => vertices_coords2[v]);\n\tconst segment = perpendicularBalancedSegment(hull, foldLine);\n\tif (segment === undefined) { return undefined; }\n\treturn arrowFromSegmentInPolygon(hull, segment, options);\n};\n"
  },
  {
    "path": "src/diagrams/axiomArrows.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../math/convert.js\";\nimport {\n\tconvexHull,\n} from \"../math/convexHull.js\";\nimport {\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tmakeMatrix2Reflect,\n\tmultiplyMatrix2Vector2,\n} from \"../math/matrix2.js\";\nimport {\n\tintersectLineLine,\n} from \"../math/intersect.js\";\nimport {\n\tclipLineConvexPolygon,\n} from \"../math/clip.js\";\nimport {\n\taxiom1,\n\taxiom3,\n\taxiom4,\n\taxiom5,\n\taxiom6,\n\taxiom7,\n} from \"../axioms/axioms.js\";\nimport {\n\tperpendicularBalancedSegment,\n\tbetweenTwoSegments,\n\tbetweenTwoIntersectingSegments,\n} from \"./general.js\";\nimport {\n\tfoldLineArrow,\n\tarrowFromSegmentInPolygon,\n} from \"./arrows.js\";\n\n/**\n * @description Reflect a point across a line\n * @param {VecLine2} line\n * @param {[number, number]} point a 2D point to be reflected\n */\nexport const diagramReflectPoint = ({ vector, origin }, point) => (\n\tmultiplyMatrix2Vector2(makeMatrix2Reflect(vector, origin), point)\n);\n\n/**\n * @description Create an arrow which neatly describes the action of\n * performing origami axiom 1 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} point1 the first axiom 1 input point\n * @param {[number, number]} point2 the second axiom 1 input point\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in always one arrow.\n */\nexport const axiom1Arrows = ({ vertices_coords }, point1, point2, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\treturn axiom1(point1, point2)\n\t\t.map(line => perpendicularBalancedSegment(hull, line))\n\t\t.map(segment => arrowFromSegmentInPolygon(hull, segment, options));\n};\n\n/**\n * @description Create an arrow which neatly describes the action of\n * performing origami axiom 2 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} point1 the first axiom 2 input point\n * @param {[number, number]} point2 the second axiom 2 input point\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in always one arrow.\n */\nexport const axiom2Arrows = ({ vertices_coords }, point1, point2, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\treturn [arrowFromSegmentInPolygon(hull, [point1, point2], options)];\n};\n\n/**\n * @description Create arrows which neatly describes the action of\n * performing origami axiom 3 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line1 the first axiom 3 input line\n * @param {VecLine2} line2 the second axiom 3 input line\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in one or two arrows. (one arrow per solution)\n */\nexport const axiom3Arrows = ({ vertices_coords }, line1, line2, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\tconst foldLines = axiom3(line1, line2);\n\n\t// clip the input lines inside the convex hull\n\tconst segments = [line1, line2]\n\t\t.map(line => clipLineConvexPolygon(hull, line))\n\t\t.filter(a => a !== undefined);\n\n\t// if this results in fewer than two segments, we cannot construct an arrow\n\t// with respect to axiom 3, so, construct simple arrows instead.\n\tif (segments.length !== 2) {\n\t\treturn foldLines\n\t\t\t.map(foldLine => foldLineArrow({ vertices_coords }, foldLine, options));\n\t}\n\n\t// perform a segment-segment intersection, we need to know if\n\t// the two axiom 3 input segments intersect each other\n\tconst [lineA, lineB] = segments.map(([a, b]) => pointsToLine2(a, b));\n\tconst intersect = intersectLineLine(lineA, lineB, includeS, includeS).point;\n\n\t// if the segments don't intersect, get the one axiom 3 solution\n\t// (there should be only one), and construct an arrow that connects\n\t// the two segments.\n\t// if the segments do intersect,\n\tconst result = !intersect\n\t\t? [betweenTwoSegments(\n\t\t\tfoldLines.filter(a => a !== undefined).shift(),\n\t\t\t[line1, line2],\n\t\t\tsegments,\n\t\t)]\n\t\t: foldLines.map(foldLine => betweenTwoIntersectingSegments(\n\t\t\t[line1, line2],\n\t\t\tintersect,\n\t\t\tfoldLine,\n\t\t\thull,\n\t\t));\n\n\treturn result.map(seg => arrowFromSegmentInPolygon(hull, seg, options));\n};\n\n/**\n * @description Create an arrow which neatly describes the action of\n * performing origami axiom 4 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line the line needed for axiom 4\n * @param {[number, number]} point the point needed for axiom 4\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in always one arrow.\n */\nexport const axiom4Arrows = ({ vertices_coords }, line, point, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\tconst foldLine = axiom4(line, point).shift();\n\tconst origin = intersectLineLine(foldLine, line).point;\n\tconst segment = perpendicularBalancedSegment(hull, foldLine, origin);\n\treturn [arrowFromSegmentInPolygon(hull, segment, options)];\n};\n\n/**\n * @description Create an array of arrows which neatly describes the action of\n * performing origami axiom 5 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line the line needed for axiom 5\n * @param {[number, number]} point1 the first point needed for axiom 5\n * @param {[number, number]} point2 the second point needed for axiom 5\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in one or two arrows (one per solution).\n */\nexport const axiom5Arrows = ({ vertices_coords }, line, point1, point2, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\treturn axiom5(line, point1, point2)\n\t\t.map(foldLine => [point2, diagramReflectPoint(foldLine, point2)])\n\t\t.map(segment => arrowFromSegmentInPolygon(hull, segment, options));\n};\n\n/**\n * @description Create an array of arrows which neatly describes the action of\n * performing origami axiom 6 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line1 the first line needed for axiom 6\n * @param {VecLine2} line2 the second line needed for axiom 6\n * @param {[number, number]} point1 the first point needed for axiom 6\n * @param {[number, number]} point2 the second point needed for axiom 6\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in zero, two, four, or six arrows (two per solution).\n */\nexport const axiom6Arrows = ({ vertices_coords }, line1, line2, point1, point2, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\treturn axiom6(line1, line2, point1, point2)\n\t\t.flatMap(foldLine => [point1, point2]\n\t\t\t.map(point => [point, diagramReflectPoint(foldLine, point)]))\n\t\t.map(segment => arrowFromSegmentInPolygon(hull, segment, options));\n};\n\n/**\n * @description Create an array of arrows which neatly describes the action of\n * performing origami axiom 7 on a given FOLD object with 2D vertices.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line1 the first line needed for axiom 7\n * @param {VecLine2} line2 the second line needed for axiom 7\n * @param {[number, number]} point the point needed for axiom 7\n * @param {{ vmin?: number, padding?: number }} options\n * @returns {Arrow[]} an array of definitions for arrows.\n * This will result in always two arrows (two per solution).\n */\nexport const axiom7Arrows = ({ vertices_coords }, line1, line2, point, options) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst hull = convexHull(vertices_coords2).map(v => vertices_coords2[v]);\n\treturn axiom7(line1, line2, point)\n\t\t.flatMap(foldLine => [\n\t\t\t[point, diagramReflectPoint(foldLine, point)],\n\t\t\tperpendicularBalancedSegment(\n\t\t\t\thull,\n\t\t\t\tfoldLine,\n\t\t\t\tintersectLineLine(foldLine, line2).point,\n\t\t\t),\n\t\t])\n\t\t.filter(a => a !== undefined) // is this?\n\t\t.map(segment => arrowFromSegmentInPolygon(hull, segment, options));\n};\n"
  },
  {
    "path": "src/diagrams/general.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tincludeR,\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tnormalize2,\n\tdot2,\n\tcross2,\n\tscale2,\n\tadd2,\n\tdistance2,\n\tmidpoint2,\n\tflip2,\n\trotate90,\n} from \"../math/vector.js\";\nimport {\n\tclipLineConvexPolygon,\n} from \"../math/clip.js\";\nimport {\n\tintersectLineLine,\n\t// intersectConvexPolygonLine,\n} from \"../math/intersect.js\";\n\n/**\n * @description Given a polygon and a line, clip the line and return the\n * midpoint of the resulting segment.\n * @param {[number, number][]} polygon a convex polygon as an array of 2D points,\n * each point an array of numbers\n * @param {VecLine2} line a line in the form of a vector and origin\n * @returns {[number, number]} a 2D point\n */\nconst getLineMidpointInPolygon = (polygon, line) => {\n\tconst segment = clipLineConvexPolygon(polygon, line);\n\treturn segment === undefined\n\t\t? undefined\n\t\t: midpoint2(segment[0], segment[1]);\n};\n\n/**\n * @description Given a polygon and a line which passes through it,\n * create a segment perpendicular to the line which fits nicely\n * inside the polygon, but is also balanced in its length on either side\n * of the input line, so, whichever side of the perpendicular segment\n * is the shortest, the other side will be equal in length to it.\n * By default the midpoint of the line (the line's segment inside the polygon)\n * will be chosen to build the perpendicular line through,\n * if you like, you can specify this point.\n * @param {[number, number][]} polygon a convex polygon as an array of 2D points,\n * each point an array of numbers\n * @param {VecLine2} line a line in the form of a vector and origin\n * @param {[number, number]} [point] an optional point along the line\n * through which the perpendicular line will pass.\n * @returns {[number, number][]|undefined} a segment as an array of two points.\n */\nexport const perpendicularBalancedSegment = (polygon, line, point) => {\n\t// if provided, use the point as the origin, otherwise use the line midpoint\n\tconst origin = point === undefined\n\t\t? getLineMidpointInPolygon(polygon, line)\n\t\t: point;\n\n\t// the vector of our line is simply the perpendicular\n\tconst vector = rotate90(line.vector); // rotate270\n\n\t// compare the two lengths from the origin of our new line to both\n\t// ends of the segment clipped in the polygon, return the shortest.\n\tconst clip = clipLineConvexPolygon(polygon, { vector, origin });\n\tif (!clip) {\n\t\treturn undefined;\n\t}\n\n\tconst shortest = clip\n\t\t.map(pt => distance2(origin, pt))\n\t\t.sort((a, b) => a - b)\n\t\t.shift();\n\n\t// build a vector in the direction of our perpendicular line but\n\t// with a scale that is half of the length of the final segment.\n\tconst scaled = scale2(normalize2(vector), shortest);\n\n\t// our segment stretches in both directions from the origin\n\t// by an equal amount\n\treturn [\n\t\tadd2(origin, flip2(scaled)),\n\t\tadd2(origin, scaled),\n\t];\n};\n\n/**\n * like in axiom 3 when two segments don't intersect and the fold\n * line lies exactly between them\n * In this case, the segments are parallel, and the fold line sits\n * @param {VecLine2} foldLine the fold line, the (single) result of axiom 3\n * @param {VecLine2[]} lines two lines, the inputs to axiom 3\n * @param {[number, number][][]} segments two segments the visible portion of the input lines\n */\nexport const betweenTwoSegments = (foldLine, lines, segments) => {\n\t// two midpoints, the midpoint of each of the two segments\n\tconst midpoints = segments.map(seg => midpoint2(seg[0], seg[1]));\n\n\t// construct a line through the two midpoints\n\t// const midpointLine = pointsToLine(...midpoints);\n\n\t// find the place... this is not needed. just get the midpoint\n\t// const origin = intersectLineLine(foldLine, midpointLine).point;\n\tconst origin = midpoint2(midpoints[0], midpoints[1]);\n\n\t// perpendicular to the fold line, passing through\n\t// the midpoint between the two segment's midpoints.\n\tconst perpendicular = { vector: rotate90(foldLine.vector), origin };\n\n\treturn lines.map(line => intersectLineLine(line, perpendicular).point);\n};\n\n/**\n * please refactor dear god please refactor\n */\nexport const betweenTwoIntersectingSegments = (lines, intersect, foldLine, boundary) => {\n\t// the input lines as vectors, and the same vectors flipped around\n\tconst paramVectors = lines.map(l => l.vector);\n\tconst flippedVectors = paramVectors.map(flip2);\n\n\t// four rays, extending outwards from the intersection point,\n\t// tracing the path of each of the two input lines.\n\tconst paramRays = paramVectors\n\t\t.concat(flippedVectors)\n\t\t.map(vector => ({ vector, origin: intersect }));\n\n\t// for each ray, apply the dot and cross product with the foldLine.\n\t// to be used in the upcoming section.\n\tconst dots = paramRays.map(ray => dot2(ray.vector, foldLine.vector));\n\tconst crosses = paramRays.map(ray => cross2(ray.vector, foldLine.vector));\n\n\t// we know the two \"lines\" intersect, and the \"foldLine\" passes through this\n\t// intersection and is an angle bisector between the two lines,\n\t// therefore, the four rays that follow \"lines\" from the intersection lie\n\t// each one in one of the four quadrants formed by the axis of the foldLine.\n\t// We can find exactly which ray is in which quadrant by consulting\n\t// their dot and cross products.\n\tconst a1 = paramRays\n\t\t.filter((ray, i) => dots[i] > 0 && crosses[i] > 0)\n\t\t.shift();\n\tconst a2 = paramRays\n\t\t.filter((ray, i) => dots[i] > 0 && crosses[i] < 0)\n\t\t.shift();\n\tconst b1 = paramRays\n\t\t.filter((ray, i) => dots[i] < 0 && crosses[i] > 0)\n\t\t.shift();\n\tconst b2 = paramRays\n\t\t.filter((ray, i) => dots[i] < 0 && crosses[i] < 0)\n\t\t.shift();\n\n\t// intersect each of the four rays with the polygon, returning a list\n\t// of four points, and we know the order of these points now.\n\tconst rayClips = [a1, a2, b1, b2].map(ray => clipLineConvexPolygon(\n\t\tboundary,\n\t\tray,\n\t\tincludeS,\n\t\tincludeR,\n\t));\n\n\t// just added\n\tif (rayClips.includes(undefined)) { return; }\n\n\tconst rayEndpoints = rayClips.map(clips => clips.shift()); // ).shift().shift());\n\n\t// we can now build two arrows between the four points, however,\n\t// we still need to... do something\n\tconst rayLengths = rayEndpoints.map(pt => distance2(pt, intersect));\n\n\tconst arrow1Start = (rayLengths[0] < rayLengths[1]\n\t\t? rayEndpoints[0]\n\t\t: rayEndpoints[1]);\n\tconst arrow1End = (rayLengths[0] < rayLengths[1]\n\t\t? add2(a2.origin, scale2(normalize2(a2.vector), rayLengths[0]))\n\t\t: add2(a1.origin, scale2(normalize2(a1.vector), rayLengths[1])));\n\tconst arrow2Start = (rayLengths[2] < rayLengths[3]\n\t\t? rayEndpoints[2]\n\t\t: rayEndpoints[3]);\n\tconst arrow2End = (rayLengths[2] < rayLengths[3]\n\t\t? add2(b2.origin, scale2(normalize2(b2.vector), rayLengths[2]))\n\t\t: add2(b1.origin, scale2(normalize2(b1.vector), rayLengths[3])));\n\n\treturn [\n\t\t[arrow1Start, arrow1End],\n\t\t[arrow2Start, arrow2End],\n\t];\n};\n"
  },
  {
    "path": "src/diagrams/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as arrows from \"./arrows.js\";\nimport * as axiomArrows from \"./axiomArrows.js\";\n\nexport default {\n\t...arrows,\n\t...axiomArrows,\n};\n"
  },
  {
    "path": "src/diagrams/svgArrows.js",
    "content": "// /**\n//  * Rabbit Ear (c) Kraft\n//  */\n// import {\n// \tfoldLineArrow,\n// \taxiom1Arrows,\n// \taxiom2Arrows,\n// \taxiom3Arrows,\n// \taxiom4Arrows,\n// \taxiom5Arrows,\n// \taxiom6Arrows,\n// \taxiom7Arrows,\n// } from \"./arrows.js\";\n// // import Constructor from \"../svg/constructor/index.js\";\n// import SVG from \"../svg/index.js\";\n\n// /**\n//  * @param {{\n//  *   segment: [[number, number], [number, number]],\n//  *   head: {\n//  *     width: number,\n//  *     height: number,\n//  *   },\n//  *   bend: number,\n//  *   padding: number,\n//  * }} arrow\n//  * @returns {SVGElement}\n//  */\n// const drawSVGArrow = (arrow) => {\n// \treturn SVG.arrow(arrow)\n// };\n\n// /**\n//  *\n//  */\n// export const axiom1SVGArrows = ({ vertices_coords }, point1, point2, options) => (\n// \taxiom1Arrows({ vertices_coords }, point1, point2, options)\n// \t\t.map(drawSVGArrow)\n// );\n"
  },
  {
    "path": "src/environment/detect.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\nconst isBrowser = typeof window === \"object\"\n\t&& typeof window.document === \"object\";\n\nconst isNodeOrBun = typeof process === \"object\"\n\t&& typeof process.versions === \"object\"\n\t&& (process.versions.node != null || process.versions.bun != null);\n\nconst isDeno = typeof window === \"object\"\n\t&& \"Deno\" in window\n\t&& typeof window.Deno === \"object\";\n\nconst isBackend = isNodeOrBun || isDeno;\n\nconst isWebWorker = typeof self === \"object\"\n\t&& self.constructor\n\t&& self.constructor.name === \"DedicatedWorkerGlobalScope\";\n\nexport {\n\tisBrowser,\n\tisBackend,\n\tisWebWorker,\n};\n"
  },
  {
    "path": "src/environment/messages.js",
    "content": "export default {\n\tplanarize: \"graph could not planarize\",\n\tmanifold: \"valid manifold required\",\n\tgraphCycle: \"cycle not allowed\",\n\tplanarBoundary: \"planar boundary detection error, bad graph\",\n\tcircularEdge: \"circular edges not allowed\",\n\treplaceModifyParam: \"replace() index < value. indices parameter modified\",\n\treplaceUndefined: \"replace() generated undefined\",\n\tflatFoldAngles: \"foldAngles cannot be determined from flat-folded faces without an assignment\",\n\tnoWebGL: \"WebGl not Supported\",\n\tconvexFace: \"only convex faces are supported\",\n\twindow: \"window not set; if using node/deno include package @xmldom/xmldom and set ear.window = xmldom\",\n\tnonConvexTriangulation: \"non-convex triangulation requires vertices_coords\",\n\tbackendStylesheet: \"svgToFold found <style> in <svg>. rendering will be incomplete unless run in a major browser.\",\n\tnoLayerSolution: \"LayerSolver bad input. no solution possible\",\n};\n"
  },
  {
    "path": "src/environment/window.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * maintain one \"window\" object for both browser and nodejs.\n * if a browser window object exists, it will set to this,\n * including inside a node/react website for example in\n * backend-specific nodejs you will need to assign this\n * window object yourself to a XML DOM library, (url below),\n * by running: ear.window = xmldom (the default export)\n *\n * - @xmldom/xmldom: https://www.npmjs.com/package/@xmldom/xmldom\n *\n * note: xmldom supports DOMParser, XMLSerializer, and document,\n * but not cancelAnimationFrame, requestAnimationFrame, fetch,\n * which are used by this library. These parts of the library\n * will not work in Node.\n */\nimport { isBrowser } from \"./detect.js\";\nimport Messages from \"./messages.js\";\n\n// store a pointer to the window object here.\nconst windowContainer = { window: undefined };\n\n// if we are in the browser, by default use the browser's \"window\".\nif (isBrowser) { windowContainer.window = window; }\n\n/**\n * @description create the window.document object, if it does not exist.\n */\nconst buildDocument = (newWindow) => new newWindow.DOMParser()\n\t.parseFromString(\"<!DOCTYPE html><title>.</title>\", \"text/html\");\n\n/**\n * @description This method allows the app to run in both a browser\n * environment, as well as some back-end environment like node.js.\n * In the case of a browser, no need to call this.\n * In the case of a back end environment, include some library such\n * as the popular @XMLDom package and pass it in as the argument here.\n */\nexport const setWindow = (newWindow) => {\n\t// make sure window has a document. xmldom does not, and requires it be built.\n\tif (!newWindow.document) { newWindow.document = buildDocument(newWindow); }\n\twindowContainer.window = newWindow;\n\treturn windowContainer.window;\n};\n\n/**\n * @description get the \"window\" object, which should have\n * DOMParser, XMLSerializer, and document.\n */\nconst RabbitEarWindow = () => {\n\tif (windowContainer.window === undefined) {\n\t\tthrow new Error(Messages.window);\n\t}\n\treturn windowContainer.window;\n};\n\nexport default RabbitEarWindow;\n"
  },
  {
    "path": "src/fold/bases.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tpopulate,\n} from \"../graph/populate.js\";\n\n/**\n * @description Create a blintz base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const blintz = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5],\n\t],\n\tvertices_vertices: [\n\t\t[1, 7], [2, 3, 7, 0], [3, 1], [4, 5, 1, 2], [5, 3], [6, 7, 3, 4], [7, 5], [0, 1, 5, 6],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6],\n\t\t[6, 7], [7, 0], [7, 1], [1, 3], [3, 5], [5, 7],\n\t],\n\tedges_assignment: Array.from(\"BBBBBBBBVVVV\"),\n\tfaces_vertices: [\n\t\t[7, 1, 3, 5], [1, 7, 0], [3, 1, 2], [5, 3, 4], [7, 5, 6],\n\t],\n});\n\n/**\n * @description Create a waterbomb base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const waterbomb = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5],\n\t\t[0.5, 0.5],\n\t],\n\tvertices_vertices: [\n\t\t[1, 8, 7], [2, 8, 0], [3, 8, 1], [4, 8, 2], [5, 8, 3], [6, 8, 4], [7, 8, 5], [0, 8, 6],\n\t\t[0, 1, 2, 3, 4, 5, 6, 7]\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0],\n\t\t[0, 8], [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8],\n\t],\n\tedges_assignment: Array.from(\"BBBBBBBBVFVMVFVM\"),\n\tfaces_vertices: [\n\t\t[0, 1, 8], [1, 2, 8], [2, 3, 8], [3, 4, 8],\n\t\t[4, 5, 8], [5, 6, 8], [6, 7, 8], [7, 0, 8],\n\t],\n});\n\n/**\n * @description Create a kite base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const kite = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [1, 0], [1, Math.SQRT2 - 1], [1, 1], [Math.SQRT2 - 1, 1], [0, 1],\n\t],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [0, 2], [0, 4], [0, 3]],\n\tedges_assignment: Array.from(\"BBBBBBVVF\"),\n}, { faces: true });\n\n/**\n * @description Create a fish base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const fish = () => populate({\n\tvertices_coords: [\n\t\t[0, 0],\n\t\t[Math.SQRT1_2, 0],\n\t\t[1, 0],\n\t\t[1, 1 - Math.SQRT1_2],\n\t\t[1, 1],\n\t\t[1 - Math.SQRT1_2, 1],\n\t\t[0, 1],\n\t\t[0, Math.SQRT1_2],\n\t\t[0.5, 0.5],\n\t\t[Math.SQRT1_2, 1 - Math.SQRT1_2],\n\t\t[1 - Math.SQRT1_2, Math.SQRT1_2],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0],\n\t\t[9, 0], [9, 2], [9, 4], [10, 0], [10, 6], [10, 4],\n\t\t[9, 1], [10, 7], [9, 3], [10, 5],\n\t\t[8, 0], [8, 9], [8, 4], [8, 10],\n\t],\n\tedges_assignment: Array.from(\"BBBBBBBBVVVVVVMMFFFFFF\"),\n}, { faces: true });\n\n/**\n * @description Create a bird base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const bird = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5],\n\t\t[0.5, 0.5],\n\t\t[0.5, (Math.SQRT2 - 1) / 2],\n\t\t[(3 - Math.SQRT2) / 2, 0.5],\n\t\t[0.5, (3 - Math.SQRT2) / 2],\n\t\t[(Math.SQRT2 - 1) / 2, 0.5],\n\t\t[Math.SQRT1_2 / 2, Math.SQRT1_2 / 2],\n\t\t[1 - (Math.SQRT1_2 / 2), 1 - (Math.SQRT1_2 / 2)],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0],\n\t\t[0, 9], [9, 2], [2, 10], [10, 4], [4, 11], [11, 6], [6, 12], [12, 0],\n\t\t[1, 9], [9, 8], [3, 10], [10, 8], [5, 11], [11, 8], [7, 12], [12, 8],\n\t\t[2, 8], [6, 8],\n\t\t[0, 13], [13, 8], [13, 9], [13, 12],\n\t\t[4, 14], [14, 8], [14, 10], [14, 11],\n\t],\n\tedges_assignment: Array\n\t\t.from(\"BBBBBBBBVVVVVVVVMVMVMVMVMMFFFFFFFF\"),\n}, { faces: true });\n\n/**\n * @description Create a frog base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const frog = () => populate({\n\tvertices_coords: [\n\t\t[0, 1], [0, Math.SQRT1_2], [0, 0.5], [0, 1 - Math.SQRT1_2], [0, 0],\n\t\t[0.5, 0.5], [1, 1], [(1 - Math.SQRT1_2) / 2, Math.SQRT1_2 / 2],\n\t\t[Math.SQRT1_2 / 2, (1 - Math.SQRT1_2) / 2], [1 - Math.SQRT1_2, 0],\n\t\t[0.5, 0], [Math.SQRT1_2, 0], [1, 0], [0.5, (1 - Math.SQRT1_2) / 2],\n\t\t[1 - (Math.SQRT1_2 / 2), (1 - Math.SQRT1_2) / 2],\n\t\t[(1 - Math.SQRT1_2) / 2, 1 - (Math.SQRT1_2 / 2)],\n\t\t[(1 - Math.SQRT1_2) / 2, 0.5],\n\t\t[(1 + Math.SQRT1_2) / 2, 1 - (Math.SQRT1_2 / 2)], [1, Math.SQRT1_2],\n\t\t[Math.SQRT1_2, 1], [1 - (Math.SQRT1_2 / 2), (1 + Math.SQRT1_2) / 2],\n\t\t[Math.SQRT1_2 / 2, (1 + Math.SQRT1_2) / 2], [0.5, 1], [1, 0.5],\n\t\t[(1 + Math.SQRT1_2) / 2, Math.SQRT1_2 / 2],\n\t\t[0.5, (1 + Math.SQRT1_2) / 2],\n\t\t[(1 + Math.SQRT1_2) / 2, 0.5],\n\t\t[1 - Math.SQRT1_2, 1], [1, 1 - Math.SQRT1_2],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [4, 7], [4, 8], [4, 9],\n\t\t[9, 10], [10, 11], [11, 12], [8, 13], [13, 14], [15, 16], [16, 7], [3, 7],\n\t\t[7, 5], [5, 17], [17, 18], [19, 20], [20, 5], [5, 8], [8, 9], [2, 15],\n\t\t[14, 10], [21, 22], [23, 24], [10, 8], [7, 2], [12, 14], [0, 15], [22, 25],\n\t\t[25, 5], [5, 13], [13, 10], [2, 16], [16, 5], [5, 26], [26, 23], [6, 17],\n\t\t[6, 20], [11, 14], [14, 5], [5, 21], [21, 27], [28, 24], [24, 5], [5, 15],\n\t\t[15, 1], [12, 5], [5, 0], [20, 25], [25, 21], [24, 26], [26, 17], [12, 24],\n\t\t[0, 21], [12, 28], [28, 23], [23, 18], [18, 6], [6, 19], [19, 22],\n\t\t[22, 27], [27, 0], [22, 20], [17, 23],\n\t],\n\tedges_assignment: Array\n\t\t.from(\"BBBBFFVVBBBBMMMMFVVFFVVFVVVVVVVVVMMVVMMVVVFVVFFVVFMMMMMMVVBBBBBBBBVV\"),\n}, { faces: true });\n\n/**\n * @description Create a windmill base FOLD object in crease pattern form.\n * @returns {FOLD} a FOLD object\n */\nexport const windmill = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [0.25, 0], [0.5, 0], [0.75, 0], [1, 0], [0, 1], [0, 0.75],\n\t\t[0, 0.5], [0, 0.25], [0.25, 0.25], [0.5, 0.5], [0.75, 0.75], [1, 1],\n\t\t[0.25, 1], [0.25, 0.75], [0.25, 0.5], [1, 0.25], [0.75, 0.25], [0.5, 0.25],\n\t\t[0.5, 1], [1, 0.5], [0.5, 0.75], [0.75, 0.5], [0.75, 1], [1, 0.75],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [5, 6], [6, 7], [7, 8], [8, 0],\n\t\t[0, 9], [9, 10], [10, 11], [11, 12], [13, 14], [14, 15], [15, 9],\n\t\t[9, 1], [16, 17], [17, 18], [18, 9], [9, 8], [7, 14], [14, 19],\n\t\t[20, 17], [17, 2], [2, 9], [9, 7], [19, 21], [21, 10], [10, 18],\n\t\t[18, 2], [20, 22], [22, 10], [10, 15], [15, 7], [4, 17], [17, 10],\n\t\t[10, 14], [14, 5], [23, 11], [11, 22], [22, 17], [17, 3], [6, 14],\n\t\t[14, 21], [21, 11], [11, 24], [12, 23], [23, 19], [19, 13], [13, 5],\n\t\t[4, 16], [16, 20], [20, 24], [24, 12], [19, 11], [11, 20],\n\t],\n\tedges_assignment: Array\n\t\t.from(\"BBBBBBBBVFFVFVVFFVVFMFMFMFFFFFFFFFVFFVFVVFFVVFBBBBBBBBMF\"),\n}, { faces: true });\n\n/**\n * @description todo: I don't know what the name of this base is.\n * @returns {FOLD} a FOLD object\n */\nexport const squareFish = () => populate({\n\tvertices_coords: [\n\t\t[0, 0], [2 - Math.SQRT2, 0], [1, 0], [0, 1], [0, 2 - Math.SQRT2],\n\t\t[0.5, 0.5], [Math.SQRT1_2, Math.SQRT1_2], [1, 1],\n\t\t[Math.SQRT1_2, 1 - Math.SQRT1_2], [1, Math.SQRT2 - 1],\n\t\t[1 - Math.SQRT1_2, Math.SQRT1_2], [Math.SQRT2 - 1, 1],\n\t\t[Math.SQRT1_2, 1], [1, Math.SQRT1_2],\n\t],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [3, 4], [4, 0], [0, 5], [5, 6], [6, 7], [0, 8], [8, 9],\n\t\t[0, 10], [10, 11], [8, 1], [10, 4], [8, 6], [6, 12], [3, 10], [10, 5],\n\t\t[5, 8], [8, 2], [10, 6], [6, 13], [7, 12], [12, 11], [11, 3], [11, 6],\n\t\t[6, 9], [2, 9], [9, 13], [13, 7],\n\t],\n\tedges_assignment: Array.from(\"BBBBFFFVFVFMMVVVFFVVVBBBMMBBB\"),\n}, { faces: true });\n"
  },
  {
    "path": "src/fold/colors.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmagnitude3,\n\tdistance3,\n\tscale3,\n} from \"../math/vector.js\";\n\n/**\n * @description CSS named colors for a standard stroke color, intended\n * for light-background renders.\n */\nexport const assignmentColor = {\n\tB: \"black\",\n\tM: \"crimson\",\n\tV: \"royalblue\",\n\tF: \"lightgray\",\n\tJ: \"gold\",\n\tC: \"limegreen\",\n\tU: \"orchid\",\n};\nObject.keys(assignmentColor).forEach(key => {\n\tassignmentColor[key.toLowerCase()] = assignmentColor[key];\n});\n\n/**\n * How desaturated can a color be but still be considered\n * a color instead of a grayscale value? please be: 0 < n < Inf.\n * 1 means nothing is changed. >1 is permissive\n * and between 0 and 1 is not permissive.\n */\nconst DESATURATION_RATIO = 4;\n\n/**\n * @description used interally by the method \"rgbToAssignment\".\n * note: colors used by rgbToAssignment are for values between 0 and 255\n * \"normalized\" here means each scalar value is normalized 0 to 1. The color\n * as a vector in 3D is not normalized, ie: yellow is [1, 1, 0].\n */\nconst colorMatchNormalized = {\n\tM: [1, 0, 0], // red\n\tV: [0, 0, 1], // blue\n\tJ: [1, 1, 0], // yellow // should these be normalized? [0.707, 0.707, 0]\n\tU: [1, 0, 1], // magenta\n\tC: [0, 1, 0], // green\n\t// and \"boundary\" and \"flat\" are black and gray\n};\n\n/**\n * @description Map a color to an edge_assignment value, of course, this uses\n * one of many color schemes, hopefully this is one of the more widely-accepted\n * schemes, however. The benefit of using this method is that it will,\n * for example, match a red color to \"mountain\", accepting any red color,\n * so long as this red color is not too close to yellow or gray, it counts.\n * \"Mountain\" and \"valley\" are red and blue respectively,\n * \"unassigned\" is purple (being blue + red), and then, following the standard\n * put forth by Origami Simulator, yellow is \"join\" and green is \"cut\".\n * This leaves \"boundary\" and \"flat\", which both take a grayscale value, and\n * should be detected geometrically (boundary edges), but this method\n * will still differentiate, \"boundary\" is black and \"flat\" is gray.\n * @param {number} red the red channel from 0 to 255\n * @param {number} green the green channel from 0 to 255\n * @param {number} blue the blue channel from 0 to 255\n * @returns {string} FOLD assignment character\n */\nexport const rgbToAssignment = (red = 0, green = 0, blue = 0) => {\n\tconst color = scale3([red, green, blue], 1 / 255);\n\n\t// the distance to black (0, 0, 0)\n\tconst blackDistance = magnitude3(color);\n\n\t// if the distance to black is too small, it's difficult\n\t// to infer any color information. the color implies \"boundary\".\n\tif (blackDistance < 0.05) { return \"B\"; }\n\n\t// the nearest grayscale value\n\tconst grayscale = color.reduce((a, b) => a + b, 0) / 3;\n\n\t// the distance from the color to the nearest grayscale value\n\tconst grayDistance = distance3(color, [grayscale, grayscale, grayscale]);\n\n\t// the nearest color from \"colorMatchNormalized\" to this color\n\tconst nearestColor = Object.keys(colorMatchNormalized)\n\t\t.map(key => ({ key, dist: distance3(color, colorMatchNormalized[key]) }))\n\t\t.sort((a, b) => a.dist - b.dist)\n\t\t.shift();\n\n\t// the color is allowed to be heavily desaturated, closer to the gray\n\t// version of itself, and still count as the color instead of the gray.\n\tif (nearestColor.dist < grayDistance * DESATURATION_RATIO) {\n\t\treturn nearestColor.key;\n\t}\n\n\t// is it black or gray? more permissivly select gray over black.\n\t// boundary might also be decided later by planar walk.\n\treturn blackDistance < 0.1 ? \"B\" : \"F\";\n};\n"
  },
  {
    "path": "src/fold/frames.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../environment/messages.js\";\nimport {\n\tclone,\n} from \"../general/clone.js\";\nimport {\n\tfilterKeysWithPrefix,\n} from \"./spec.js\";\n\n/**\n * @description Frames can be children of other frames via. the\n * frame_parent and frame_inherit properties potentially creating a\n * recursive inheritance. This method will \"flatten\" a frame by\n * gathering all of its inherited parent frames and collapsing the\n * stack into one single frame. The input graph itself will not be modified.\n * @param {FOLD} graph a FOLD object\n * @param {number} frameNumber which frame number to flatten (0 is the top level)\n * @returns {FOLD} one single, flattened FOLD frame (with no file_frames)\n */\nexport const flattenFrame = (graph, frameNumber = 0) => {\n\tif (!graph.file_frames || graph.file_frames.length < frameNumber) {\n\t\treturn graph;\n\t}\n\n\t// prevent cycles. never visit a frame twice\n\tconst visited = {};\n\n\t// this is entirely optional, for the final result,\n\t// we can copy over all the file_ metadata into the frame.\n\tconst fileMetadata = {};\n\tfilterKeysWithPrefix(graph, \"file\")\n\t\t.filter(key => key !== \"file_frames\")\n\t\t.forEach(key => { fileMetadata[key] = graph[key]; });\n\n\t/**\n\t * @description recurse from the desired frame up through its parent\n\t * frames until we reach frame index 0, or a frame with no parent.\n\t * @param {number} currentIndex\n\t * @param {number[]} previousOrders\n\t * @returns {number[]} a list of frame indices, from parent to child.\n\t */\n\tconst recurse = (currentIndex, previousOrders) => {\n\t\t// prevent cycles\n\t\tif (visited[currentIndex]) { throw new Error(Messages.graphCycle); }\n\t\tvisited[currentIndex] = true;\n\n\t\t// add currentIndex to the start of the list of previous frame indices\n\t\tconst thisOrders = [currentIndex].concat(previousOrders);\n\n\t\t// get a reference to the current frame\n\t\t/** @type {FOLDInternalFrame} */\n\t\tconst frame = currentIndex > 0\n\t\t\t? { ...graph.file_frames[currentIndex - 1] }\n\t\t\t: { ...graph };\n\n\t\t// if the frame inherits and contains a parent, recurse\n\t\t// if not, we are done, return the list of orders.\n\t\treturn frame.frame_inherit && frame.frame_parent != null\n\t\t\t? recurse(frame.frame_parent, thisOrders)\n\t\t\t: thisOrders;\n\t};\n\n\t// recurse, get a list of frame indices from parent to child,\n\t// convert the indices into shallow copies of the frames, and\n\t// sequentially reduce all frames into a single frame object.\n\tconst flattened = recurse(frameNumber, []).map((frameNum) => {\n\t\t// shallow copy reference to the frame. this allows us to be able to\n\t\t// delete any key/values we need to and not affect the input graph.\n\t\tconst frame = frameNum > 0\n\t\t\t? { ...graph.file_frames[frameNum - 1] }\n\t\t\t: { ...graph };\n\n\t\t// remove any reference of these keys from the frame\n\t\t[\"file_frames\", \"frame_parent\", \"frame_inherit\"]\n\t\t\t.forEach(key => delete frame[key]);\n\t\treturn frame;\n\t}).reduce((a, b) => ({ ...a, ...b }), fileMetadata);\n\n\t// this is optional, but this ensures that this method can be treated\n\t// \"functionally\" and using this method will not cause any side effects\n\treturn clone(flattened);\n};\n\n/**\n * @description Get a flat array of all file_frames, where the top-level\n * is index 0, and the file_frames follow in sequence.\n * This does not perform any frame-collapsing if a frame inherits from\n * a parent, the frames are maintained in their original form.\n * The objects in the result are a shallow copy so they still hold\n * references to the original FOLD object provided in the input.\n * @param {FOLD} graph a FOLD object\n * @returns {FOLD[]} an array of FOLD objects, single frames in a flat array.\n */\nexport const getFileFramesAsArray = (graph) => {\n\tif (!graph) { return []; }\n\tif (!graph.file_frames || !graph.file_frames.length) {\n\t\treturn [graph];\n\t}\n\tconst frame0 = { ...graph };\n\tdelete frame0.file_frames;\n\treturn [frame0, ...graph.file_frames];\n};\n\n/**\n * @description Get the number of file frames in a FOLD object.\n * The top level is the first frame, then every entry inside of\n * \"file_frames\" increments the count by 1.\n * @param {FOLD} graph a FOLD object.\n * @returns {number} the number of frames in the FOLD object.\n */\nexport const countFrames = ({ file_frames }) => (!file_frames\n\t? 1\n\t: file_frames.length + 1);\n\n/**\n * @description Get all frames inside a FOLD object which contain a\n * frame_classes class matching the provided className string\n * @param {FOLD} graph a FOLD object\n * @param {string} className the name of the class inside frame_classes\n * @returns {FOLD[]} an array of FOLD object frames.\n */\nexport const getFramesByClassName = (graph, className) => Array\n\t.from(Array(countFrames(graph)))\n\t.map((_, i) => flattenFrame(graph, i))\n\t.filter(frame => frame.frame_classes\n\t\t&& frame.frame_classes.includes(className));\n"
  },
  {
    "path": "src/fold/primitives.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakePolygonCircumradius,\n} from \"../math/polygon.js\";\nimport {\n\tpopulate,\n} from \"../graph/populate.js\";\n\n/**\n * @description Create a square or rectangle vertices_coords,\n * counter-clockwise order.\n * @param {number} w the width of the rectangle\n * @param {number} h the height of the rectangle\n * @returns {[number, number][]} a list of 2D points\n */\nconst makeRectCoords = (w, h) => [[0, 0], [w, 0], [w, h], [0, h]];\n\n/**\n * @description Given an already initialized vertices_coords array,\n * create a fully-populated graph that sets these vertices to be\n * the closed boundary of a polygon.\n * @param {[number, number][]} vertices_coords\n * @returns {FOLD} graph a FOLD object with the vertices_coords\n * as counter-clockwise consecutive points in the boundary forming one face.\n */\nconst makeGraphWithBoundaryCoords = (vertices_coords) => ({\n\tvertices_coords,\n\tedges_vertices: vertices_coords\n\t\t.map((_, i, arr) => [i, (i + 1) % arr.length]),\n\tedges_assignment: Array(vertices_coords.length).fill(\"B\"),\n\tfaces_vertices: [vertices_coords.map((_, i) => i)],\n\tfaces_edges: [vertices_coords.map((_, i) => i)],\n});\n\n/**\n * @description Create a new FOLD object which contains one square face,\n * including vertices and boundary edges.\n * @param {number} [scale=1] the length of the sides.\n * @returns {FOLD} a FOLD object\n */\nexport const square = (scale = 1) => populate(\n\tmakeGraphWithBoundaryCoords(makeRectCoords(scale, scale)),\n);\n\n/**\n * @description Create a new FOLD object which contains one rectangular face,\n * including vertices and boundary edges.\n * @param {number} [width=1] the width of the rectangle\n * @param {number} [height=1] the height of the rectangle\n * @returns {FOLD} a FOLD object\n */\nexport const rectangle = (width = 1, height = 1) => populate(\n\tmakeGraphWithBoundaryCoords(makeRectCoords(width, height)),\n);\n\n/**\n * @description Create a new FOLD object with a regular-polygon shaped boundary.\n * @param {number} [sides=3] the number of sides to the polygon\n * @param {number} [circumradius=1] the circumradius of the polygon (the\n * distance from the center to any vertex)\n * @returns {FOLD} a FOLD object\n */\nexport const polygon = (sides = 3, circumradius = 1) => populate(\n\tmakeGraphWithBoundaryCoords(makePolygonCircumradius(sides, circumradius)),\n);\n\n/**\n * @description Create a new FOLD object that approximates a circle\n * @param {number} [radius=1] the radius of the circle\n * @param {number} [sides=128] the number of edges along the circle\n * @returns {FOLD} a FOLD object\n */\nexport const circle = (radius = 1, sides = 128) => polygon(sides, radius);\n"
  },
  {
    "path": "src/fold/rabbitear.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\nexport const file_spec = 1.2;\nexport const file_creator = \"Rabbit Ear\";\n"
  },
  {
    "path": "src/fold/spec.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tepsilonEqual,\n} from \"../math/compare.js\";\nimport {\n\tdoEdgesOverlap,\n} from \"../graph/edges/overlap.js\";\n\n/**\n * FOLD spec: https://github.com/edemaine/FOLD/\n */\n\n/**\n * @description All FOLD format keys as described in the spec.\n */\nexport const foldKeys = {\n\tfile: [\n\t\t\"file_spec\",\n\t\t\"file_creator\",\n\t\t\"file_author\",\n\t\t\"file_title\",\n\t\t\"file_description\",\n\t\t\"file_classes\",\n\t\t\"file_frames\",\n\t],\n\tframe: [\n\t\t\"frame_author\",\n\t\t\"frame_title\",\n\t\t\"frame_description\",\n\t\t\"frame_attributes\",\n\t\t\"frame_classes\",\n\t\t\"frame_unit\",\n\t\t\"frame_parent\", // inside file_frames only\n\t\t\"frame_inherit\", // inside file_frames only\n\t],\n\tgraph: [\n\t\t\"vertices_coords\",\n\t\t\"vertices_vertices\",\n\t\t\"vertices_edges\",\n\t\t\"vertices_faces\",\n\t\t\"edges_vertices\",\n\t\t\"edges_faces\",\n\t\t\"edges_assignment\",\n\t\t\"edges_foldAngle\",\n\t\t\"edges_length\",\n\t\t\"faces_vertices\",\n\t\t\"faces_edges\",\n\t\t\"faces_faces\",\n\t],\n\torders: [\n\t\t\"edgeOrders\",\n\t\t\"faceOrders\",\n\t],\n};\n\n/**\n * @description All \"file_classes\" values according to the FOLD spec\n */\nexport const foldFileClasses = [\n\t\"singleModel\",\n\t\"multiModel\",\n\t\"animation\",\n\t\"diagrams\",\n];\n\n/**\n * @description All \"frame_classes\" values according to the FOLD spec\n */\nexport const foldFrameClasses = [\n\t\"creasePattern\",\n\t\"foldedForm\",\n\t\"graph\",\n\t\"linkage\",\n];\n\n/**\n * @description All \"frame_attributes\" values according to the FOLD spec\n */\nexport const foldFrameAttributes = [\n\t\"2D\",\n\t\"3D\",\n\t\"abstract\",\n\t\"manifold\",\n\t\"nonManifold\",\n\t\"orientable\",\n\t\"nonOrientable\",\n\t\"selfTouching\",\n\t\"nonSelfTouching\",\n\t\"selfIntersecting\",\n\t\"nonSelfIntersecting\",\n];\n\n/**\n * @description Names of graph components\n * @constant {string[]}\n */\nexport const VEF = [\"vertices\", \"edges\", \"faces\"];\n\n/**\n * @description All possible valid edge assignment characters\n * @constant {string[]}\n */\nexport const edgesAssignmentValues = Array.from(\"BbMmVvFfJjCcUu\");\n\n/**\n * @description Get the English word for every FOLD spec\n * assignment character (like \"M\", or \"b\").\n * @constant {object}\n */\nexport const edgesAssignmentNames = {\n\tB: \"boundary\",\n\tM: \"mountain\",\n\tV: \"valley\",\n\tF: \"flat\",\n\tJ: \"join\",\n\tC: \"cut\",\n\tU: \"unassigned\",\n};\nObject.keys(edgesAssignmentNames).forEach(key => {\n\tedgesAssignmentNames[key.toLowerCase()] = edgesAssignmentNames[key];\n});\n\n/**\n * @description Get the foldAngle in degrees for every FOLD assignment spec\n * character (like \"M\", or \"b\"), assuming the creases are flat folded.\n * @constant {object}\n */\nexport const assignmentFlatFoldAngle = {\n\tB: 0,\n\tb: 0,\n\tM: -180,\n\tm: -180,\n\tV: 180,\n\tv: 180,\n\tF: 0,\n\tf: 0,\n\tJ: 0,\n\tj: 0,\n\tC: 0,\n\tc: 0,\n\tU: 0,\n\tu: 0,\n};\n\n/**\n * @description For every assignment type, can this edge be a folded edge?\n * @constant {object}\n */\nexport const assignmentCanBeFolded = {\n\tB: false,\n\tb: false,\n\tM: true,\n\tm: true,\n\tV: true,\n\tv: true,\n\tF: false,\n\tf: false,\n\tJ: false,\n\tj: false,\n\tC: false,\n\tc: false,\n\tU: true,\n\tu: true,\n};\n\n/**\n * @description For every assignment type, can this edge be considered\n * a part of the boundary? Boundary edges are treated differently\n * when solving single-vertex flat foldability, for example.\n * @constant {object}\n */\nexport const assignmentIsBoundary = {\n\tB: true,\n\tb: true,\n\tM: false,\n\tm: false,\n\tV: false,\n\tv: false,\n\tF: false,\n\tf: false,\n\tJ: false,\n\tj: false,\n\tC: true,\n\tc: true,\n\tU: false,\n\tu: false,\n};\n\n/**\n * @description Convert an assignment character to a foldAngle in degrees.\n * This assumes that all assignments are flat folded.\n * @param {string} assignment a FOLD edge assignment character\n * @returns {number} fold angle in degrees. M/V are assumed to be flat-folded.\n */\nexport const edgeAssignmentToFoldAngle = (assignment) => (\n\tassignmentFlatFoldAngle[assignment] || 0\n);\n\n/**\n * @description Convert a foldAngle to an edge assignment character.\n * This method only considered the fold angle, no boundary detection\n * is performed. This method will only return \"M\", \"V\", or \"U\".\n * @todo should \"U\" be \"F\" instead?\n * @param {number} angle fold angle in degrees\n * @returns {string} a FOLD edge assignment character\n */\nexport const edgeFoldAngleToAssignment = (angle) => {\n\tif (angle > EPSILON) { return \"V\"; }\n\tif (angle < -EPSILON) { return \"M\"; }\n\treturn \"U\";\n};\n\n/**\n * @description Test if a fold angle is a flat fold, +/- 180.\n * and the epsilon around either number.\n * @param {number} angle fold angle in degrees\n * @returns {boolean} true if the fold angle is flat folded\n */\nexport const edgeFoldAngleIsFlatFolded = (angle) => (\n\tepsilonEqual(-180, angle) || epsilonEqual(180, angle)\n);\n\n/**\n * @description Test if a fold angle is flat, which includes unfolded\n * (0 angle), as well as +/- 180, and the epsilon around each of these.\n * @param {number} angle fold angle in degrees\n * @returns {boolean} true if the fold angle is flat\n */\nexport const edgeFoldAngleIsFlat = (angle) => (\n\tepsilonEqual(0, angle) || edgeFoldAngleIsFlatFolded(angle)\n);\n\n/**\n * @description Using edges_foldAngle, determine if a FOLD object\n * edges are all flat-folded, meaning all edges are either:\n * -180, 0, 180, or any of those three within an epsilon.\n * If a graph contains no edges_foldAngle the edges are assumed to be flat.\n * @param {FOLD} graph a FOLD object\n * @returns {boolean} are the edges of the graph flat folded?\n */\nexport const edgesFoldAngleAreAllFlat = ({ edges_foldAngle }) => {\n\tif (!edges_foldAngle) { return true; }\n\tfor (let i = 0; i < edges_foldAngle.length; i += 1) {\n\t\tif (!edgeFoldAngleIsFlat(edges_foldAngle[i])) { return false; }\n\t}\n\treturn true;\n};\n\n/**\n * @description subroutine for filterKeysWithPrefix and filterKeysWithSuffix\n * @param {object} obj\n * @param {function} matchFunction\n * @returns {string[]} array of matching keys\n */\nconst filterKeys = (obj, matchFunction) => Object\n\t.keys(obj)\n\t.filter(key => matchFunction(key));\n\n/**\n * @description Get all keys in an object which begin with a string and are\n * immediately followed by \"_\". For example, provide \"vertices\" and this will\n * match \"vertices_coords\", \"vertices_faces\", but not \"faces_vertices\"\n * @param {FOLD} obj an object, FOLD object or otherwise.\n * @param {string} prefix a prefix to match against the keys\n * @returns {string[]} array of matching keys\n */\nexport const filterKeysWithPrefix = (obj, prefix) => filterKeys(\n\tobj,\n\t/** @param {string} s */\n\ts => s.substring(0, prefix.length + 1) === `${prefix}_`,\n);\n\n/**\n * @description Get all keys in an object which end with a string and are\n * immediately preceded by \"_\". For example, provide \"vertices\" and this will\n * match \"edges_vertices\", \"faces_vertices\", but not \"vertices_edges\"\n * @param {FOLD} obj an object, FOLD object or otherwise.\n * @param {string} suffix a suffix to match against the keys\n * @returns {string[]} array of matching keys\n */\nexport const filterKeysWithSuffix = (obj, suffix) => filterKeys(\n\tobj,\n\t/** @param {string} s */\n\ts => s.substring(s.length - suffix.length - 1, s.length) === `_${suffix}`,\n);\n\n/**\n * @description Find all keys in an object that contain a _ character,\n * and return every prefix substring that comes before the _.\n * @param {FOLD} obj an object, FOLD object or otherwise.\n * @returns {string[]} array of prefixes\n */\nexport const getAllPrefixes = (obj) => {\n\tconst hash = {};\n\tObject.keys(obj)\n\t\t.filter(s => s.includes(\"_\"))\n\t\t.map(k => k.substring(0, k.indexOf(\"_\")))\n\t\t.forEach(k => { hash[k] = true; });\n\treturn Object.keys(hash);\n};\n\n/**\n * @description Find all keys in an object that contain a _ character,\n * and return every suffix substring that comes after the _.\n * @param {FOLD} obj an object, FOLD object or otherwise.\n * @returns {string[]} array of suffixes\n */\nexport const getAllSuffixes = (obj) => {\n\tconst hash = {};\n\tObject.keys(obj)\n\t\t.filter(s => s.includes(\"_\"))\n\t\t.map(k => k.substring(k.lastIndexOf(\"_\") + 1, k.length))\n\t\t.forEach(k => { hash[k] = true; });\n\treturn Object.keys(hash);\n};\n\n/**\n * @description This takes in a geometry_key (vectors, edges, faces), and flattens\n * across all related arrays, creating one object with the keys for every index.\n * @param {FOLD} graph a FOLD object\n * @param {string} geometry_key a geometry item like \"vertices\"\n * @returns {object[]} an array of objects with FOLD keys but the\n * values are from this single element\n */\nexport const transposeGraphArrays = (graph, geometry_key) => {\n\tconst matching_keys = filterKeysWithPrefix(graph, geometry_key);\n\tif (matching_keys.length === 0) { return []; }\n\tconst len = Math.max(...matching_keys.map(arr => graph[arr].length));\n\tconst geometry = Array.from(Array(len))\n\t\t.map(() => ({}));\n\tmatching_keys\n\t\t.forEach(key => geometry\n\t\t\t.forEach((_, i) => { geometry[i][key] = graph[key][i]; }));\n\treturn geometry;\n};\n\n/**\n * @description This takes in a geometry_key (vectors, edges, faces), and flattens\n * across all related arrays, creating one object with the keys.\n * @param {FOLD} graph a FOLD object\n * @param {string} geometry_key a geometry item like \"vertices\"\n * @param {number} index the index of an element\n * @returns {object} an object with FOLD keys but the values are from this single element\n */\nexport const transposeGraphArrayAtIndex = (\n\tgraph,\n\tgeometry_key,\n\tindex,\n) => {\n\tconst matching_keys = filterKeysWithPrefix(graph, geometry_key);\n\tif (matching_keys.length === 0) { return undefined; }\n\tconst geometry = {};\n\tmatching_keys.forEach((key) => { geometry[key] = graph[key][index]; });\n\treturn geometry;\n};\n\n// used in isFoldObject\nconst allFOLDKeys = Object.freeze([]\n\t.concat(foldKeys.file)\n\t.concat(foldKeys.frame)\n\t.concat(foldKeys.graph)\n\t.concat(foldKeys.orders));\n\n/**\n * @description Using heuristics by checking the names of the keys\n * of an object, determine if an object is a FOLD object.\n * @param {FOLD} object a Javascript object, find out if it is a FOLD object\n * @returns {number} value between 0 and 1 where\n * 0 means no chance, 1 means 100% chance.\n */\nexport const isFoldObject = (object = {}) => (\n\tObject.keys(object).length === 0\n\t\t? 0\n\t\t: allFOLDKeys\n\t\t\t.filter(key => object[key]).length / Object.keys(object).length);\n\n/**\n * @description Check the coordinates of each vertex and if any of them\n * contain a third dimension AND that number is not 0, then the graph\n * is in 3D, otherwise the graph is considered 2D.\n * This method is O(n).\n * @param {FOLD} graph a FOLD object\n * @returns {number} the dimension of the vertices, either 2 or 3.\n */\nexport const getDimension = ({ vertices_coords }, epsilon = EPSILON) => {\n\tfor (let i = 0; i < vertices_coords.length; i += 1) {\n\t\tif (vertices_coords[i] && vertices_coords[i].length === 3\n\t\t\t&& !epsilonEqual(0, vertices_coords[i][2], epsilon)) {\n\t\t\treturn 3;\n\t\t}\n\t}\n\treturn 2;\n};\n\n/**\n * @description Infer the dimensions of (the vertices of) a graph\n * by querying the first point in vertices_coords. This also works\n * when the vertices_coords array has holes (ie: index 0 is not set).\n * @param {FOLD} graph a FOLD object\n * @returns {number | undefined} the dimension of the vertices, or\n * undefined if no vertices exist. number should be 2 or 3 in most cases.\n */\nexport const getDimensionQuick = ({ vertices_coords }) => {\n\tif (!vertices_coords || !vertices_coords.length) { return undefined; }\n\t// return length of first vertex, if it exists\n\tif (vertices_coords[0] !== undefined) {\n\t\treturn vertices_coords[0].length;\n\t}\n\t// in case of an array with holes, get the first vertex.\n\tconst vertex = vertices_coords.filter(() => true).shift();\n\tif (!vertex) { return undefined; }\n\treturn vertex.length;\n};\n\n/**\n * @description Infer if a FOLD object is in its folded state\n * (as opposed to crease pattern state).\n * A graph will be considered \"folded\" if the \"foldedForm\" key can be found\n * in the metadata, or if the vertices_coords are in 3D and a Z number is not 0,\n * or, if vertices are in 2D and any edges are overlapping.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the graph is in a folded state\n */\nexport const isFoldedForm = ({\n\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n\tframe_classes, file_classes,\n}, epsilon = EPSILON) => {\n\t// FOLD spec only describes \"foldedForm\" to be in the frame_classes,\n\t// accounting for mistakes, check both class arrays.\n\tif ((frame_classes && frame_classes.includes(\"foldedForm\"))\n\t\t|| (file_classes && file_classes.includes(\"foldedForm\"))) {\n\t\treturn true;\n\t}\n\tif ((frame_classes && frame_classes.includes(\"creasePattern\"))\n\t\t|| (file_classes && file_classes.includes(\"creasePattern\"))) {\n\t\treturn false;\n\t}\n\t// unfortunately, anything beyond this point cannot be calculated precisely,\n\t// or it cannot be calculated precisely without a significant overhead\n\n\t// if vertices don't exist, the graph is abstract. return false.\n\tif (!vertices_coords) { return false; }\n\n\tconst dimensions = getDimensionQuick({ vertices_coords });\n\n\t// if there are no faces, the idea of \"folded\" kinda doesn't exist, does it?\n\tif (!faces_vertices && !faces_edges) {\n\t\t// our best guess is yes, it's folded if the vertices are in 3D.\n\t\treturn dimensions === 3;\n\t}\n\n\t// if the coordinates have only 2 dimensions, we only know that it's flat.\n\t// we have to check if it's a crease pattern or a flat-folded model,\n\t// do this by checking if any two edges overlap.\n\tif (edges_vertices && dimensions === 2) {\n\t\treturn doEdgesOverlap({ vertices_coords, edges_vertices });\n\t}\n\n\t// iterate over every vertex, check each vertex's Z component, if the\n\t// Z value is not 0, consider the graph to be folded.\n\tfor (let i = 0; i < vertices_coords.length; i += 1) {\n\t\tif (!vertices_coords[i]) { continue; }\n\t\tif (typeof vertices_coords[i][2] !== \"number\") { continue; }\n\t\tif (!epsilonEqual(vertices_coords[i][2], 0, epsilon)) { return true; }\n\t}\n\n\t// no evidence that the graph is folded.\n\t// our best guess is yes, it's folded if the vertices are in 3D.\n\treturn dimensions === 3;\n};\n\n/**\n * @description For every edge, give us a boolean:\n * - \"true\" if the edge is folded, valley or mountain, or unassigned.\n * - \"false\" if the edge is not folded, any other assignment.\n * \"unassigned\" is considered folded so that an unsolved crease pattern\n * can be fed into here and we still compute the folded state.\n * @param {FOLD} graph a FOLD object\n * @returns {boolean[]} for every edge, is it folded? or does it have\n * the potential to be folded? where \"unassigned\" is yes.\n */\nexport const makeEdgesIsFolded = ({ edges_vertices, edges_foldAngle, edges_assignment }) => {\n\tif (edges_assignment === undefined) {\n\t\treturn edges_foldAngle === undefined\n\t\t\t? edges_vertices.map(() => true)\n\t\t\t: edges_foldAngle.map(angle => angle < -EPSILON || angle > EPSILON);\n\t}\n\treturn edges_assignment.map(a => assignmentCanBeFolded[a]);\n};\nconst flipAssignmentLookup = { M: \"V\", m: \"v\", V: \"M\", v: \"m\" };\n\n/**\n * @description for a mountain or valley, return the opposite.\n * in the case of any other crease (boundary, flat, ...) return itself.\n * @param {string} assign a FOLD edge assignment\n * @returns {string} a FOLD edge assignment\n */\nexport const invertAssignment = (assign) => (\n\tflipAssignmentLookup[assign] || assign\n);\n\n/**\n * @description Given a fold graph, make all mountains into valleys\n * and visa versa. This includes reversing the fold_angles.\n * @param {FOLD} graph a FOLD object containing edges_assignment\n * @returns {FOLD} the same FOLD object, modified in place.\n */\nexport const invertAssignments = (graph) => {\n\tif (graph.edges_assignment) {\n\t\tgraph.edges_assignment = graph.edges_assignment\n\t\t\t.map(a => (flipAssignmentLookup[a] ? flipAssignmentLookup[a] : a));\n\t}\n\tif (graph.edges_foldAngle) {\n\t\tgraph.edges_foldAngle = graph.edges_foldAngle.map(n => -n);\n\t}\n\treturn graph;\n};\n\n/**\n * @description This method sorts edges by their assignment.\n * Given a graph with edges_vertices, return a dictionary with\n * all assignments as keys, and the values are an array of edge indices\n * from this graph matching those assignments under the key.\n * @param {FOLD} graph a FOLD object\n * @returns {{\n *   B?: number[],\n *   V?: number[],\n *   M?: number[],\n *   F?: number[],\n *   J?: number[],\n *   C?: number[],\n *   U?: number[],\n * }} keys: assignment characters, values: array of edge indices\n */\nexport const sortEdgesByAssignment = ({ edges_vertices, edges_assignment = [] }) => {\n\t// get an array of all uppercase assignments as strings (B, M, V, F...)\n\tconst allAssignments = Array\n\t\t.from(new Set(edgesAssignmentValues.map(s => s.toUpperCase())));\n\n\t// for every edge, return that edge's assignment (uppercase), ensuring\n\t// that this array matches in length to edges_vertices, and if any\n\t// edge assignment is unknown, it is given a \"U\" (unassigned).\n\tconst edges_upperAssignment = edges_vertices\n\t\t.map((_, i) => edges_assignment[i] || \"U\")\n\t\t.map(a => a.toUpperCase());\n\n\t// dictionary with assignments as keys and arrays of edge indices as values\n\tconst assignmentIndices = {};\n\tallAssignments.forEach(a => { assignmentIndices[a] = []; });\n\tedges_upperAssignment.forEach((a, i) => assignmentIndices[a].push(i));\n\treturn assignmentIndices;\n};\n\n/**\n * @description Get a FOLD object's metadata, which includes all relevant\n * data inside any of the \"file_\" keys. Note: this does not include\n * any \"frames_\" frame metadata.\n * @param {FOLD} FOLD a FOLD object\n * @returns {{\n *   file_spec?: number,\n *   file_creator?: string,\n *   file_author?: string,\n *   file_title?: string,\n *   file_description?: string,\n *   file_classes?: string[],\n * }} an object containing the metadata keys and values\n */\nexport const getFileMetadata = (FOLD = {}) => {\n\t// return all \"file_\" metadata keys (do not include file_frames)\n\tconst metadata = {};\n\tfoldKeys.file\n\t\t.filter(key => key !== \"file_frames\")\n\t\t.filter(key => FOLD[key] !== undefined)\n\t\t.forEach(key => { metadata[key] = FOLD[key]; });\n\treturn metadata;\n};\n"
  },
  {
    "path": "src/general/array.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tepsilonEqual,\n} from \"../math/compare.js\";\n\n/**\n * @description Given a list of any type, return a copy of the array\n * with all duplicates removed. This only works with arrays with primitives,\n * arrays with objects or arrays will not work.\n * @param {number[]} array an array of integers\n * @returns {number[]} set of unique integers\n * @example [1,2,3,2,1] will result in [1,2,3]\n */\nexport const uniqueElements = (array) => Array.from(new Set(array));\n\n/**\n * @description Given an array of any type, return a copy of\n * the same array but filter out any items which only appear once.\n * The comparison uses conversion-to-string,\n * then matching to compare, so this works for primitives\n * (bool, number, string), not objects or arrays.\n * @param {any[]} array an array of any type.\n * @returns {any[]} the same input array but filtered to\n * remove elements which appear only once.\n * @example [1,2,3,2,1] will result in [1,2,2,1]\n */\nexport const nonUniqueElements = (array) => {\n\tconst count = {};\n\tarray.forEach(el => {\n\t\tif (count[el] === undefined) { count[el] = 0; }\n\t\tcount[el] += 1;\n\t});\n\treturn array.filter(el => count[el] > 1);\n};\n\n/**\n * @description Given a list of numbers (can contain duplicates),\n * this will return a sorted set of unique numbers (removing duplicates).\n * @param {number[]} array an array of numbers\n * @returns {number[]} set of sorted, unique numbers\n * @example [3, 2, 1.5, 2, 3] will result in [1.5, 2, 3]\n */\nexport const uniqueSortedNumbers = (array) => {\n\tconst hash = {};\n\tarray.forEach(n => { hash[n] = true; });\n\treturn Object.keys(hash).map(parseFloat);\n};\n\n/**\n * @description Given an array of numbers, sort the list and\n * filter out any two numbers which are close to each other within\n * an epsilon. The result list may be smaller than the input list.\n * @param {number[]} array an array of numbers.\n * @param {number} [epsilon=1e-6] an optional epsilon.\n * @returns {number[]} a sorted and filtered array of the input array.\n */\nexport const epsilonUniqueSortedNumbers = (array, epsilon = EPSILON) => {\n\tconst numbers = array.slice().sort((a, b) => a - b);\n\tif (numbers.length < 2) { return numbers; }\n\tconst keep = [true];\n\tfor (let i = 1; i < numbers.length; i += 1) {\n\t\tkeep[i] = !epsilonEqual(numbers[i], numbers[i - 1], epsilon);\n\t}\n\treturn numbers.filter((_, i) => keep[i]);\n};\n\n/**\n * @description Return the intersection of two arrays. This assumes that\n * the array values are primitives (this does not work if values are objects),\n * and will stringify the values to compare, so 5 === \"5\" will match.\n * If inside each arrays contains duplicates, the number of duplicates in\n * the result will match the number shared between the two.\n * @param {any[]} array1 an array of any primitive type\n * @param {any[]} array2 an array of any primitive type\n * @returns {any[]} an array of values found inside both arrays.\n */\nexport const arrayIntersection = (array1, array2) => {\n\t// create a lookup table for all values in array2, where the number\n\t// of appearances is stored as the hash value.\n\tconst hash = {};\n\tarray2.forEach(value => { hash[value] = 0; });\n\tarray2.forEach(value => { hash[value] += 1; });\n\n\t// for every item in array1, modify the lookup table each time we encounter\n\t// a value from both arrays by decrementing the appearance counter by one.\n\t// filter out any array1 values if the counter is 0,\n\t// or it does not appear in array2.\n\treturn array1.filter(value => {\n\t\tif (hash[value] > 0) {\n\t\t\thash[value] -= 1;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n};\n\n/**\n * @description Given an array considered to be circular, where the end\n * connects back to the start, rotate the array so that the value currently\n * in the newStartIndex spot becomes the first (0) index.\n * @param {any[]} array an array containing any type\n * @param {number} newStartIndex the current index to become the new 0 index.\n * @returns {any[]} a copy of the original array, rotated.\n */\nexport const rotateCircularArray = (array, newStartIndex) => (\n\tnewStartIndex <= 0\n\t\t? array\n\t\t: array\n\t\t\t.slice(newStartIndex)\n\t\t\t.concat(array.slice(0, newStartIndex)));\n\n/**\n * @description A circular array (data wraps around) requires 2 indices\n * if you intend to split it into two arrays. The pair of indices can be\n * provided in any order, they will be sorted, smaller index first.\n * @param {any[]} array an array that is meant to be thought of as circular\n * @param {[number, number]} indices two numbers\n * these indices will divide the array into 2 parts\n * @returns {[any[], any[]]} the input array split into two arrays\n */\nexport const splitCircularArray = (array, indices) => {\n\tindices.sort((a, b) => a - b);\n\treturn [\n\t\tarray.slice(indices[1]).concat(array.slice(0, indices[0] + 1)),\n\t\tarray.slice(indices[0], indices[1] + 1),\n\t];\n};\n\n/**\n * @description convert a list of items {any} into a list of pairs\n * where each item is uniqely matched with another item (non-ordered)\n * the number of pairs is (length * (length-1)) / 2\n * @param {any[]} array an array containing any values\n * @returns {[any, any][]} an array of arrays, type matching the\n * input array type, where each inner array is a list of two.\n */\nexport const chooseTwoPairs = (array) => {\n\tconst pairs = Array((array.length * (array.length - 1)) / 2);\n\tlet index = 0;\n\tfor (let i = 0; i < array.length - 1; i += 1) {\n\t\tfor (let j = i + 1; j < array.length; j += 1, index += 1) {\n\t\t\tpairs[index] = [array[i], array[j]];\n\t\t}\n\t}\n\treturn pairs;\n};\n\n/**\n * @description Return a modified copy of set \"a\" that filters\n * out any number that exists in set \"b\". This method assumes that\n * both input arrays are sorted, so this method will run in O(n) time.\n * Numbers are compared within an epsilon range of each other.\n * If arrays are not sorted, sort them before using this method.\n * @param {number[]} a an array of numbers, in sorted order\n * @param {number[]} b an array of numbers, in sorted order\n * @returns {number[]} a copy of \"a\" with no values found in \"b\".\n */\nexport const setDifferenceSortedNumbers = (a, b) => {\n\tconst result = [];\n\tlet ai = 0;\n\tlet bi = 0;\n\twhile (ai < a.length && bi < b.length) {\n\t\tif (a[ai] === b[bi]) {\n\t\t\tai += 1;\n\t\t} else if (a[ai] > b[bi]) {\n\t\t\tbi += 1;\n\t\t} else if (b[bi] > a[ai]) {\n\t\t\tresult.push(a[ai]);\n\t\t\tai += 1;\n\t\t}\n\t}\n\treturn result;\n};\n\n/**\n * @description Return a modified copy of set \"a\" that filters\n * out any number that exists in set \"b\". This method assumes that\n * both input arrays are sorted, so this method will run in O(n) time.\n * Numbers are compared within an epsilon range of each other.\n * If arrays are not sorted, sort them before using this method.\n * @param {number[]} a an array of numbers, in sorted order\n * @param {number[]} b an array of numbers, in sorted order\n * @returns {number[]} a copy of \"a\" with no values found in \"b\".\n */\nexport const setDifferenceSortedEpsilonNumbers = (a, b, epsilon = EPSILON) => {\n\tconst result = [];\n\tlet ai = 0;\n\tlet bi = 0;\n\twhile (ai < a.length && bi < b.length) {\n\t\tif (epsilonEqual(a[ai], b[bi], epsilon)) {\n\t\t\tai += 1;\n\t\t} else if (a[ai] > b[bi]) {\n\t\t\tbi += 1;\n\t\t} else if (b[bi] > a[ai]) {\n\t\t\tresult.push(a[ai]);\n\t\t\tai += 1;\n\t\t}\n\t}\n\treturn result;\n};\n\n/**\n * @description Return the index of the smallest value in the array.\n * An optional map function parameter can be provided to map the\n * values into another state before searching for the minimum value.\n * @param {any[]} array an array of any comparable type\n * @param {Function} [map] an optional map function to run on all elements\n * @returns {number|undefined} an index from the input array,\n * or undefined if the array has no length.\n */\nexport const arrayMinimumIndex = (array, map) => {\n\tif (!array.length) { return undefined; }\n\tconst arrayValues = typeof map === \"function\"\n\t\t? array.map(value => map(value))\n\t\t: array;\n\tlet index = 0;\n\tarrayValues.forEach((value, i, arr) => {\n\t\tif (value < arr[index]) { index = i; }\n\t});\n\treturn index;\n};\n\n/**\n * @description Return the index of the largest value in the array.\n * An optional map function parameter can be provided to map the\n * values into another state before searching for the maximum value.\n * @param {any[]} array an array of any comparable type\n * @param {Function} [map] an optional map function to run on all elements\n * @returns {number|undefined} an index from the input array,\n * or undefined if the array has no length.\n */\nexport const arrayMaximumIndex = (array, map) => {\n\tif (!array.length) { return undefined; }\n\tconst arrayValues = typeof map === \"function\"\n\t\t? array.map(value => map(value))\n\t\t: array;\n\tlet index = 0;\n\tarrayValues.forEach((value, i, arr) => {\n\t\tif (value > arr[index]) { index = i; }\n\t});\n\treturn index;\n};\n\n/**\n * @description Given a list of arrays which contain holes, this method\n * will splice all arrays together into one, maintaining indices,\n * filling holes where indices exist. This is intended for the specific\n * case where all arrays originally came from a single array, such as\n * in the subgraph() method, this method will re-join these arrays.\n * In the case where some arrays double up on indices, the index will be\n * overwritten, by the last array parameter in the sequence.\n * @param {...any[]} arrays arrays containing any type.\n * @returns {any[]} one array\n */\nexport const mergeArraysWithHoles = (...arrays) => {\n\tconst flattened = [];\n\tarrays.forEach(array => array.forEach((value, i) => {\n\t\tflattened[i] = value;\n\t}));\n\treturn flattened;\n};\n\n/**\n * @description Clusters are arrays of arrays of indices, where every index\n * in the inner arrays are clustered together. This method converts this array\n * into a vertices_vertices or faces_faces style array, where for every index\n * its list contains the set of all indices (not including itself) which are\n * a member of the same cluster.\n * @param {number[][]} clusters a cluster array\n * @returns {number[][]} a reflexive array (like vertices_vertices)\n */\nexport const clustersToReflexiveArrays = (clusters) => {\n\tconst result = [];\n\tclusters.flat().forEach(i => { result[i] = []; });\n\tclusters\n\t\t.flatMap(chooseTwoPairs)\n\t\t.forEach(([a, b]) => {\n\t\t\tresult[a].push(b);\n\t\t\tresult[b].push(a);\n\t\t});\n\treturn result;\n};\n\n/**\n * @description Convert an array of arrays of numbers into an array of array\n * of booleans where the booleans are at the index positions of the numbers,\n * making this array act as a quick hash lookup for whether or not an index\n * exists.\n * @param {number[][]} array_array an array of arrays of numbers\n * @returns {boolean[][]} for every top level array, an array that contains\n * true values at every index of a number in the array.\n */\nexport const arrayArrayToLookupArray = (array_array) => array_array\n\t.map(array => {\n\t\tconst lookup = [];\n\t\tarray.forEach(i => { lookup[i] = true; });\n\t\treturn lookup;\n\t});\n\n/**\n * @description Convert an array of arrays of booleans into an array\n * of arrays of numbers, where those numbers are the indices in the array\n * at which those booleans appear.\n * @param {boolean[][]} lookupArray an array of arrays of booleans\n * @returns {number[][]} for every top level array, an array that contains\n * the indices with truthy values.\n */\nexport const lookupArrayToArrayArray = (lookupArray) => lookupArray\n\t.map(array => array\n\t\t.map((overlap, i) => (overlap ? i : undefined))\n\t\t.filter(a => a !== undefined));\n"
  },
  {
    "path": "src/general/clone.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description This is a polyfill for \"structuredClone\"\n * similar to running JSON.parse(JSON.stringify()).\n * This method will deep copy an object, with a few caveats:\n *  - it doesn't detect recursive cycles\n *  - weird behavior around Proxys\n * @author https://jsperf.com/deep-copy-vs-json-stringify-json-parse/5\n * @param {object} o\n * @returns {object} a deep copy of the input\n */\nconst clonePolyfill = function (o) {\n\tlet newO;\n\tlet i;\n\tif (typeof o !== \"object\") {\n\t\treturn o;\n\t}\n\tif (!o) {\n\t\treturn o;\n\t}\n\tif (Object.prototype.toString.apply(o) === \"[object Array]\") {\n\t\tnewO = [];\n\t\tfor (i = 0; i < o.length; i += 1) {\n\t\t\tnewO[i] = clonePolyfill(o[i]);\n\t\t}\n\t\treturn newO;\n\t}\n\tnewO = {};\n\tfor (i in o) {\n\t\tif (o.hasOwnProperty(i)) {\n\t\t\t// this is where a self-similar reference causes an infinite loop\n\t\t\tnewO[i] = clonePolyfill(o[i]);\n\t\t}\n\t}\n\treturn newO;\n};\n\n/**\n * @description Export \"structuredClone\" if it exists,\n * otherwise export the polyfill method.\n * @param {object} object\n * @returns {object} a deep copy of the input object\n */\nexport const clone = (typeof structuredClone === \"function\"\n\t? structuredClone\n\t: clonePolyfill);\n"
  },
  {
    "path": "src/general/cluster.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tnormalize,\n\tparallelNormalized,\n} from \"../math/vector.js\";\nimport {\n\tdoRangesOverlap,\n\trangeUnion,\n} from \"../math/range.js\";\n\n/**\n * @description Given a list of pre-sorted elements, create clusters\n * where similarity is determined by a custom comparison function.\n * Comparisons only need to happen to neighbors due to the array\n * already being sorted.\n * The type of elements in the list doesn't matter, so long as the\n * comparison function can compare them.\n * @param {any[]} elements a list of elements of any type\n * @param {Function} comparison a function which takes two \"any\" types\n * (from elements) and returns true if they are similar, false otherwise.\n * @returns {number[][]} a list of lists of indices referencing the input list,\n * where each inner list is a cluster of similar element indices.\n */\nexport const clusterSortedGeneric = (elements, comparison) => {\n\tif (!elements.length) { return []; }\n\n\t// get a list of indices, we will iterate over this list.\n\t// this allows this method to work with arrays with holes\n\tconst indices = elements.map((_, i) => i);\n\n\t// set the first element's index, at the same time, remove it from the list\n\tconst groups = [[indices.shift()]];\n\n\t// iterate through the list of indices (starting from the second element)\n\t// and compare each element to one element from the most recent cluster\n\tindices.forEach((index) => {\n\t\t// a pointer to the the current group list\n\t\tconst group = groups[groups.length - 1];\n\n\t\t// the index from \"elements\" of the most recently added element\n\t\tconst prevElement = group[group.length - 1];\n\n\t\t// compare the two elements, if true, add to the current group,\n\t\t// if false, create a new group and add it to the groups container.\n\t\tif (comparison(elements[prevElement], elements[index])) {\n\t\t\tgroup.push(index);\n\t\t} else {\n\t\t\tgroups.push([index]);\n\t\t}\n\t});\n\n\treturn groups;\n};\n\n/**\n * @description Given a list of unsorted elements, create clusters\n * where similarity is determined by a custom comparison function.\n * Because the elements are not sorted, comparisons need to happen to\n * all members of a group before an element is added, hence it's much\n * preferred to use clusterSortedGeneric if possible.\n * The type of elements in the list doesn't matter, so long as the\n * comparison function can compare them.\n * @param {any[]} indices a list of elements of any type\n * @param {Function} comparison a function which takes two \"any\" types\n * (from elements) and returns true if they are similar, false otherwise.\n * @returns {number[][]} a list of lists of indices referencing the input list,\n * where each inner list is a cluster of similar element indices.\n */\nexport const clusterUnsortedIndices = (indices, comparison) => {\n\tif (!indices.length) { return []; }\n\n\tconst indicesCopy = indices.slice();\n\n\t// set the first element's index, at the same time, remove it from the list\n\tconst groups = [[indicesCopy.shift()]];\n\n\t// iterate through the list of indices (starting from the second element)\n\t// and compare each element to one element from the most recent cluster\n\tindicesCopy.forEach((index) => {\n\t\t// compare the two elements, if true, add to the current group,\n\t\t// if false, create a new group and add it to the groups container.\n\t\tconst matchFound = groups\n\t\t\t.map((group, g) => (comparison(group[0], index) ? g : undefined))\n\t\t\t.filter(a => a !== undefined)\n\t\t\t.shift();\n\n\t\tif (matchFound !== undefined) {\n\t\t\tgroups[matchFound].push(index);\n\t\t} else {\n\t\t\tgroups.push([index]);\n\t\t}\n\t});\n\n\treturn groups;\n};\n\n/**\n * @description Given an unsorted array of floats, make a sorted copy of the\n * array then walk through the array and group similar values into clusters.\n * Cluster epsilon is relative to the nearest neighbor, not the start\n * of the group or some other metric, so for example, the values\n * [1, 2, 3, 4, 5] will all be in one cluster if the epsilon is 1.5.\n * @param {number[]} numbers an array of numbers\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} array of array of indices to the input array.\n */\nexport const clusterScalars = (numbers, epsilon = EPSILON) => {\n\t// sort the numbers list but save it as a list of indices\n\tconst indices = numbers\n\t\t.map((v, i) => ({ v, i }))\n\t\t.sort((a, b) => a.v - b.v)\n\t\t.map(el => el.i)\n\t\t.filter(() => true);\n\n\t// prepare data for the method clusterSortedGeneric,\n\t// the values will be the sorted numbers,\n\t// the comparison function will be a simple: is \"a\" epsilon similar to \"b\"?\n\tconst sortedNumbers = indices.map(i => numbers[i]);\n\t/** @param {number} a @param {number} b */\n\tconst compFn = (a, b) => Math.abs(a - b) < epsilon;\n\n\t// call the cluster method which results in a list of indices that refer\n\t// to the sorted list \"sortedNumbers\", which of course does not match our\n\t// input array \"numbers\", so, remap these indices so that our\n\t// final result of indices relates to the input array \"numbers\".\n\treturn clusterSortedGeneric(sortedNumbers, compFn)\n\t\t.map(arr => arr.map(i => indices[i]));\n};\n\n/**\n * @description Given a list of unsorted ranges, each range being a pair\n * of numbers, this method will create clusters where every range in\n * the cluster is overlapped by at least one other range.\n * Range pairs do not necessarily need to be in increasing order.\n * @param {[number, number][]} ranges a list of ranges,\n * each range a pair of numbers\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} an array of clusters, where each cluster is\n * a list of indices which point to the \"ranges\" input array.\n */\nexport const clusterRanges = (ranges, epsilon = EPSILON) => {\n\t// sort the ranges by the minimum element, save it as a list of indices\n\tconst indices = ranges\n\t\t.map(([a, b], i) => ({ v: Math.min(a, b), i }))\n\t\t.sort((a, b) => a.v - b.v)\n\t\t.map(el => el.i)\n\t\t.filter(() => true);\n\n\t// prepare data for the method clusterSortedGeneric,\n\t// the values will be the sorted ranges,\n\t// the comparison function will be a bit more complex with a side effect,\n\t// maintain the current range outside of the function,\n\t// if there is an overlap, update the range to be a union with the new range,\n\t// if there is no overlap, reset it to be the new range.\n\tconst sortedRanges = indices.map(i => ranges[i]);\n\n\tlet currentRange = [...sortedRanges[0]];\n\n\t/**\n\t * @param {[number, number]} _ an unused range, the cumulative result\n\t * @param {[number, number]} b a new range to compare\n\t * @returns {boolean} true if \"b\" overlaps with the \"currentRange\"\n\t */\n\tconst comparison = (_, b) => {\n\t\tconst overlap = doRangesOverlap(currentRange, b, epsilon);\n\t\tcurrentRange = overlap ? rangeUnion(currentRange, b) : [...b];\n\t\treturn overlap;\n\t};\n\n\t// call the cluster method which results in a list of indices that refer\n\t// to the sorted list \"sortedRanges\", which of course does not match our\n\t// input array \"ranges\", so, remap these indices so that our\n\t// final result of indices relates to the input array \"ranges\".\n\treturn clusterSortedGeneric(sortedRanges, comparison)\n\t\t.map(arr => arr.map(i => indices[i]));\n};\n\n/**\n * @description Given an array of vectors, group the vectors into clusters\n * that all contain vectors which are parallel to one another.\n * This works for any N-dimensional vectors (including 3D or 2D).\n * @note, this is an n^2 algorithm. Currently it's used by the method\n * getEdgesLine to find all unique lines in a graph, which initially clusters\n * lines by distance-to-origin, then INSIDE each cluster this method is called,\n * in effect, reducing the total time spent in this method to below n^2.\n * If you use this method, try to optimize similarly if you can.\n * @param {number[][]} vectors an array of vectors\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} array of array of indices to the input array.\n */\nexport const clusterParallelVectors = (vectors, epsilon = EPSILON) => {\n\t// for the parallel test, we will test against normalized vectors.\n\t// much faster if we normalize the entire list at the beginning.\n\tconst normalized = vectors.map(normalize);\n\n\t// start the list of groups with the first group containing the first index\n\tconst groups = [[0]];\n\n\t// create an nested loop where for ever vector compare against every group\n\tloop1: for (let i = 1; i < normalized.length; i += 1) {\n\t\t// iterate over all groups, only testing against the first vector\n\t\t// found inside that group, compare the two vectors\n\t\tfor (let g = 0; g < groups.length; g += 1) {\n\t\t\t// if vectors are parallel, add to the group and break out of the inner\n\t\t\t// loop by continuing with the first loop. (does not create a new group)\n\t\t\tif (parallelNormalized(normalized[i], normalized[groups[g][0]], epsilon)) {\n\t\t\t\tgroups[g].push(i);\n\t\t\t\tcontinue loop1;\n\t\t\t}\n\t\t}\n\n\t\t// if no match is found, make a new group with this vector inside\n\t\tgroups.push([i]);\n\t}\n\treturn groups;\n};\n"
  },
  {
    "path": "src/general/get.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n// import { distance2 } from \"../algebra/vector.js\";\n// import { identity2x3 } from \"../algebra/matrix2.js\";\n// import { identity3x4 } from \"../algebra/matrix3.js\";\n\n// /**\n//  * @param {object} obj\n//  * @returns {boolean}\n//  */\n// const isIterable = (obj) => obj != null\n// \t&& typeof obj[Symbol.iterator] === \"function\";\n\n// /**\n//  * @description flatten only until the point of comma separated entities.\n//  * This will preserve vectors (number[]) in an array of array of vectors.\n//  * @param {any[][]} args any array, intended to contain arrays of arrays.\n//  * @returns {array[]} a flattened copy, flattened up until the point before\n//  * combining arrays of elements.\n//  */\n// const semiFlattenArrays = function () {\n// \tswitch (arguments.length) {\n// \tcase 0: return Array.from(arguments);\n// \t// only if its an array (is iterable) and NOT a string\n// \tcase 1: return isIterable(arguments[0]) && typeof arguments[0] !== \"string\"\n// \t\t? semiFlattenArrays(...arguments[0])\n// \t\t: [arguments[0]];\n// \tdefault:\n// \t\treturn Array.from(arguments).map(a => (isIterable(a)\n// \t\t\t? [...semiFlattenArrays(a)]\n// \t\t\t: a));\n// \t}\n// };\n\n// /**\n//  * @description Totally flatten, recursive\n//  * @param {any[][]} args any array, intended to contain arrays of arrays.\n//  * @returns {any[]} fully, recursively flattened array\n//  */\n// const flattenArrays = function () {\n// \tswitch (arguments.length) {\n// \tcase 0: return Array.from(arguments);\n// \t// only if its an array (is iterable) and NOT a string\n// \tcase 1: return isIterable(arguments[0]) && typeof arguments[0] !== \"string\"\n// \t\t? flattenArrays(...arguments[0])\n// \t\t: [arguments[0]];\n// \tdefault:\n// \t\treturn Array.from(arguments).map(a => (isIterable(a)\n// \t\t\t? [...flattenArrays(a)]\n// \t\t\t: a)).flat();\n// \t}\n// };\n\n// /**\n//  * @description Coerce the function arguments into a vector.\n//  * This will object notation {x:, y:}, or array [number, number, ...]\n//  * and work for n-dimensions.\n//  * @param {any[]} ...args an argument list that contains at least one\n//  * object with {x: y:} or a list of numbers to become the vector.\n//  * @returns {number[]} vector in array form, or empty array for bad inputs\n// */\n// export const getVector = function () {\n// \tlet list = flattenArrays(arguments);\n// \t// if the arguments's first element is an object with an \"x\" property\n// \tconst a = list[0];\n// \tif (typeof a === \"object\" && a !== null && !Number.isNaN(a.x)) {\n// \t\tlist = [\"x\", \"y\", \"z\"].map(c => a[c]).filter(b => b !== undefined);\n// \t}\n// \treturn list.filter(n => typeof n === \"number\");\n// };\n\n// /**\n//  * @description Coerce the function arguments into an array of vectors.\n//  * @param {any[][]} ...args an argument list that contains any number of\n//  * objects with {x: y:} or a list of list of numbers to become vectors.\n//  * @returns {number[][]} vectors in array form, or empty array.\n// */\n// export const getArrayOfVectors = function () {\n// \treturn semiFlattenArrays(arguments).map(el => getVector(el));\n// };\n\n// /**\n//  * @description Coerce the function arguments into a segment (a pair of points)\n//  * @param {any[]} ...args an argument list that contains a pair of\n//  * objects with {x: y:} or a list of list of numbers to become endpoints.\n//  * @returns {number[][]} segment in array form [[a1, a2], [b1, b2]]\n// */\n// export const getSegment = function () {\n// \tconst args = semiFlattenArrays(arguments);\n// \treturn args.length === 4\n// \t\t? [[0, 1], [2, 3]].map(s => s.map(i => args[i]))\n// \t\t: args.map(el => getVector(el));\n// };\n\n// // store two parameters in an object under the keys \"vector\" and \"object\"\n// const vectorOriginForm = (vector, origin = []) => ({ vector, origin });\n// // \t{ vector: vector || [], origin: origin || [] });\n\n// /**\n//  * @description Coerce the function arguments into a line.\n//  * @param {any[]} ...args an argument list that contains an object with\n//  * {vector: origin:} or a list of list of numbers.\n//  * @returns {VecLine} a line in \"vector\" \"origin\" form.\n//  */\n// export const getLine = function () {\n// \tconst args = semiFlattenArrays(arguments);\n// \tif (args.length === 0 || args[0] == null) { return vectorOriginForm([], []); }\n// \tif (args[0].constructor === Object && args[0].vector !== undefined) {\n// \t\treturn vectorOriginForm(args[0].vector, args[0].origin || []);\n// \t}\n// \treturn typeof args[0] === \"number\"\n// \t\t? vectorOriginForm(getVector(args))\n// \t\t: vectorOriginForm(...args.map(a => getVector(a)));\n// };\n\n// /**\n//  * a matrix2 is a 2x3 matrix, 2x2 with a column to represent translation\n//  *\n//  * @returns {number[]} array of 6 numbers, or undefined if bad inputs\n// */\n// export const getMatrix2 = function () {\n// \tconst m = getVector(arguments);\n// \tif (m.length === 6) { return m; }\n// \tif (m.length > 6) { return [m[0], m[1], m[2], m[3], m[4], m[5]]; }\n// \tif (m.length < 6) {\n// \t\treturn identity2x3.map((n, i) => m[i] || n);\n// \t}\n// \treturn [...identity2x3];\n// };\n// const maps3x4 = [\n// \t[0, 1, 3, 4, 9, 10],\n// \t[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n// \t[0, 1, 2, undefined, 3, 4, 5, undefined, 6, 7, 8, undefined, 9, 10, 11],\n// ];\n// [11, 7, 3].forEach(i => delete maps3x4[2][i]);\n// // eslint-disable-next-line no-nested-ternary\n// const matrixMap3x4 = len => (len < 8\n// \t? maps3x4[0]\n// \t: (len < 13 ? maps3x4[1] : maps3x4[2]));\n// /**\n//  * @description Get a 3x4 matrix\n//  *\n//  * @returns {number[]} array of 12 numbers, or undefined if bad inputs\n// */\n// export const getMatrix3x4 = function () {\n// \tconst mat = flattenArrays(arguments);\n// \tconst matrix = [...identity3x4];\n// \tmatrixMap3x4(mat.length)\n// \t\t// .filter((_, i) => mat[i] != null)\n// \t\t.forEach((n, i) => { if (mat[i] != null) { matrix[n] = mat[i]; } });\n// \treturn matrix;\n// };\n\n// export const get_rect_params = (x = 0, y = 0, width = 0, height = 0) => ({\n// \tx, y, width, height,\n// });\n\n// export const getRect = function () {\n// \tconst list = flattenArrays(arguments);\n// \tif (list.length > 0\n// \t\t&& typeof list[0] === \"object\"\n// \t\t&& list[0] !== null\n// \t\t&& !Number.isNaN(list[0].width)) {\n// \t\treturn get_rect_params(...[\"x\", \"y\", \"width\", \"height\"]\n// \t\t\t.map(c => list[0][c])\n// \t\t\t.filter(a => a !== undefined));\n// \t}\n// \tconst numbers = list.filter(n => typeof n === \"number\");\n// \tconst rect_params = numbers.length < 4\n// \t\t? [, , ...numbers]\n// \t\t: numbers;\n// \treturn get_rect_params(...rect_params);\n// };\n\n// /**\n//  * radius is the first parameter so that the origin can be N-dimensional\n//  * ...args is a list of numbers that become the origin.\n//  */\n// const get_circle_params = (radius = 1, ...args) => ({\n// \tradius,\n// \torigin: [...args],\n// });\n\n// export const getCircle = function () {\n// \tconst vectors = getArrayOfVectors(arguments);\n// \tconst numbers = flattenArrays(arguments).filter(a => typeof a === \"number\");\n// \tif (arguments.length === 2) {\n// \t\tif (vectors[1].length === 1) {\n// \t\t\treturn get_circle_params(vectors[1][0], ...vectors[0]);\n// \t\t}\n// \t\tif (vectors[0].length === 1) {\n// \t\t\treturn get_circle_params(vectors[0][0], ...vectors[1]);\n// \t\t}\n// \t\tif (vectors[0].length > 1 && vectors[1].length > 1) {\n// \t\t\treturn get_circle_params(distance2(...vectors), ...vectors[0]);\n// \t\t}\n// \t} else {\n// \t\tswitch (numbers.length) {\n// \t\tcase 0: return get_circle_params(1, 0, 0, 0);\n// \t\tcase 1: return get_circle_params(numbers[0], 0, 0, 0);\n// \t\tdefault: return get_circle_params(numbers.pop(), ...numbers);\n// \t\t}\n// \t}\n// \treturn get_circle_params(1, 0, 0, 0);\n// };\n"
  },
  {
    "path": "src/general/hashCode.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description a javascript re-implementation of Java's .hashCode()\n * https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript\n * @param {string} string a string\n * @returns {number} a unique number\n */\nexport const hashCode = (string) => {\n\tlet hash = 0;\n\tfor (let i = 0; i < string.length; i += 1) {\n\t\thash = ((hash << 5) - hash) + string.charCodeAt(i);\n\t\thash |= 0; // Convert to 32bit integer\n\t}\n\treturn hash;\n};\n"
  },
  {
    "path": "src/general/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as arrayMethods from \"./array.js\";\nimport * as clusterMethods from \"./cluster.js\";\n// import * as getMethods from \"./get.js\";\nimport * as numberMethods from \"./number.js\";\nimport * as sortMethods from \"./sort.js\";\nimport * as stringMethods from \"./string.js\";\n\nexport default {\n\t...arrayMethods,\n\t...clusterMethods,\n\t// ...getMethods,\n\t...numberMethods,\n\t...sortMethods,\n\t...stringMethods,\n};\n"
  },
  {
    "path": "src/general/number.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Count the number of places deep past the decimal point.\n * @param {number} num any number\n * @returns {number} an integer, the number of decimal digits.\n */\nconst countPlaces = function (num) {\n\tconst m = (`${num}`).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);\n\treturn Math.max(0, (m[1] ? m[1].length : 0) - (m[2] ? +m[2] : 0));\n};\n\n/**\n * @description clean floating point numbers, for example,\n * 15.0000000000000002 becomes 15. this method involves\n * encoding and parsing so it is relatively expensive.\n * @param {number} number the floating point number to clean\n * @param {number} [places=15] an integer, the number of decimal places\n * to keep, beyond this point can be considered to be noise.\n * @returns {number} the cleaned floating point number\n */\nexport const cleanNumber = function (number, places = 15) {\n\tconst num = typeof number === \"number\" ? number : parseFloat(number);\n\tif (Number.isNaN(num)) { return number; }\n\tconst crop = parseFloat(num.toFixed(places));\n\tif (countPlaces(crop) === Math.min(places, countPlaces(num))) {\n\t\treturn num;\n\t}\n\treturn crop;\n};\n"
  },
  {
    "path": "src/general/sort.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tnormalize2,\n\tdot,\n\tsubtract,\n\tbasisVectors3,\n} from \"../math/vector.js\";\nimport {\n\tprojectPointOnPlane,\n} from \"../math/plane.js\";\n\n/**\n * @description Provide a comparison function and use it to sort an array\n * of any type of object against a single item. The returned array will be\n * the indices of the original array in sorted order.\n * @param {any[]} array an array of elements to be sorted\n * @param {any} item the item which to compare against all array elements\n * @param {function} compareFn the comparison function to be run against\n * every element in the array with the input item parameter, placing\n * the array element first, the input item second: fn(arrayElem, paramItem)\n * @returns {number[]} the indices of the original array, in sorted order\n */\nconst sortAgainstItem = (array, item, compareFn) => array\n\t.map((el, i) => ({ i, n: compareFn(el, item) }))\n\t.sort((a, b) => a.n - b.n)\n\t.map(a => a.i);\n\n/**\n * @description Sort an array of n-dimensional points along an\n * n-dimensional vector, get the indices in sorted order.\n * @param {number[][]} points array of points (which are arrays of numbers)\n * @param {number[]} vector one vector\n * @returns {number[]} a list of sorted indices to the points array.\n */\nexport const sortPointsAlongVector = (points, vector) => (\n\tsortAgainstItem(points, vector, dot)\n);\n\n/**\n * @description Radially sort a list of 2D unit vectors\n * counter-clockwise, starting from the +X axis, [1, 0].\n * Vectors must be normalized within the 2nd dimension.\n * @param {[number, number][]} vectors a list of 2D unit vectors\n * @returns {number[]} a list of indices that reference the input list.\n * @notes\n *\n *       (+y)\n *        |\n *  -d +c |  +d +c\n *        |\n * -------|------- (+x, [1, 0])\n *        |\n *  -d -c |  +d -c\n *        |\n *\n * which maps to these indices:\n *\n * 0 | 0\n * -----\n * 1 | 1\n */\nexport const radialSortUnitVectors2 = (vectors) => {\n\t// we filter each vector into two categories based on the sign of the Y value\n\t// and later will sort the vectors within each category based on the X value.\n\t// we are storing the indices here, not the vectors themselves.\n\tconst sidesVectors = [[], []];\n\tvectors.map(vec => (vec[1] >= 0 ? 0 : 1))\n\t\t.forEach((s, v) => sidesVectors[s].push(v));\n\n\t// each side can be sorted by simply comparing the x value since these\n\t// vectors are normalized. decreasing or increasing, for +Y and -Y.\n\tconst sorts = [\n\t\t/** @param {number} a @param {number} b */\n\t\t(a, b) => vectors[b][0] - vectors[a][0],\n\t\t/** @param {number} a @param {number} b */\n\t\t(a, b) => vectors[a][0] - vectors[b][0],\n\t];\n\n\t// now we can sort each side, and since the sides themselves are\n\t// already sorted counter-clockwise, the flattened list will be sorted.\n\treturn sidesVectors.flatMap((indices, i) => indices.sort(sorts[i]));\n};\n\n/**\n * @description Radially sort a list of points in 3D space around a line.\n * Imagine the line as a plane's normal, project the points down into the\n * plane, normalized, and use them as input to the related method which\n * radially sorts 2D normalized vectors, using our projected plane.\n * @param {[number, number, number][]} points a list of 3D points\n * @param {[number, number, number]} vector a 3D vector describing the line's vector\n * @param {[number, number, number]} origin a point which this line passes through,\n * by default this is set to be the origin.\n * @returns {number[]} a list of indices that reference the input list.\n */\nexport const radialSortVectors3 = (\n\tpoints,\n\tvector = [1, 0, 0],\n\torigin = [0, 0, 0],\n) => {\n\t// the line's vector is the plane's normal, using the plane's normal,\n\t// generate three orthogonal vectors to be our basis vectors in 3D.\n\tconst threeVectors = basisVectors3(vector);\n\n\t// order the basis vectors such that the plane's normal is the Z vector\n\tconst basis = [threeVectors[1], threeVectors[2], threeVectors[0]];\n\n\t// project the input points down into the 3D plane.\n\tconst projectedPoints = points\n\t\t.map(point => projectPointOnPlane(point, vector, origin));\n\n\t// convert the projected points into vectors\n\tconst projectedVectors = projectedPoints\n\t\t.map(point => subtract(point, origin));\n\n\t// convert the 2D vectors into our new basis frame (UV space)\n\tconst pointsUV = projectedVectors\n\t\t.map(vec => [dot(vec, basis[0]), dot(vec, basis[1])]);\n\n\t// normalize the 2D vectors and send them to the sorting method\n\tconst vectorsUV = pointsUV.map(normalize2);\n\treturn radialSortUnitVectors2(vectorsUV);\n};\n"
  },
  {
    "path": "src/general/string.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Convert a string into camel case from kebab or snake case.\n * @param {string} s a string in kebab or snake case\n * @returns {string} a string in camel case\n */\nexport const toCamel = (s) => s\n\t.replace(/([-_][a-z])/ig, $1 => $1\n\t\t.toUpperCase()\n\t\t.replace(\"-\", \"\")\n\t\t.replace(\"_\", \"\"));\n\n/**\n * @description Convert a string into kebab case from camel case.\n * Snake case does not work in this method.\n * @param {string} s a string in camel case\n * @returns {string} a string in kebab case\n */\nexport const toKebab = (s) => s\n\t.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n\t.replace(/([A-Z])([A-Z])(?=[a-z])/g, \"$1-$2\")\n\t.toLowerCase();\n\n/**\n * @description Capitalize the first letter of a string.\n * @param {string} s a string\n * @returns {string} a copy of the string, capitalized.\n */\nexport const capitalized = (s) => s\n\t.charAt(0).toUpperCase() + s.slice(1);\n"
  },
  {
    "path": "src/graph/add/edge.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Add an edge to a graph, and only update the edges_ properties,\n * making a graph invalid assuming fields like vertices_vertices faces_edges.\n * For creasePatterns, call planarize() to fix the graph.\n * For foldedForms, use an alternate method like splitFaceWithEdge.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} vertices the two vertices to connect and make a new edge\n * @param {number[]} [faces=[]] two faces to become the edges_faces\n * @param {string} [assignment=\"U\"] this will become edges_assignment\n * @param {number} [foldAngle=0] this will become edges_foldAngle\n * @returns {number} the index of the new edge\n */\nexport const addEdge = (\n\tgraph,\n\tvertices,\n\tfaces = [],\n\tassignment = \"U\",\n\tfoldAngle = 0,\n) => {\n\tif (!graph.edges_vertices) { graph.edges_vertices = []; }\n\n\t// the index of our new edge\n\tconst edge = graph.edges_vertices.length;\n\n\t// construct data for our new edge (vertices, assignent, foldAngle...)\n\tgraph.edges_vertices[edge] = vertices;\n\tif (graph.edges_faces) { graph.edges_faces[edge] = faces; }\n\tif (graph.edges_assignment) { graph.edges_assignment[edge] = assignment; }\n\tif (graph.edges_foldAngle) { graph.edges_foldAngle[edge] = foldAngle; }\n\n\treturn edge;\n};\n\n/**\n * @description Add an isolated edge to a graph, specifically, this is a\n * face-isolated edge, it has no face associations, it's possible to connect\n * two existing vertices with this method, and these two vertice's existing\n * data will be maintained (and appended to, although, in an unsorted manner!)\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} vertices the two vertices to connect and make a new edge\n * @param {string} [assignment=\"U\"] this will become edges_assignment\n * @param {number} [foldAngle=0] this will become edges_foldAngle\n * @returns {number} the index of the new edge\n */\nexport const addIsolatedEdge = (\n\tgraph,\n\tvertices,\n\tassignment = \"U\",\n\tfoldAngle = 0,\n) => {\n\tconst edge = addEdge(graph, vertices, [], assignment, foldAngle);\n\n\t// No face data is to be touched.\n\t// Regarding vertex data, do not overwrite existing vertex arrays\n\t// for our two vertices, instead, ensure it exists and then append this new\n\t// data to the arrays.\n\t// This makes this method able to be\n\tif (graph.vertices_vertices) {\n\t\t// ensure the arrays for these vertices exist\n\t\tvertices\n\t\t\t.filter(vertex => !graph.vertices_vertices[vertex])\n\t\t\t.forEach(vertex => { graph.vertices_vertices[vertex] = []; });\n\t\tconst otherVertices = [vertices[1], vertices[0]];\n\t\tvertices.forEach((vertex, i) => {\n\t\t\tgraph.vertices_vertices[vertex].push(otherVertices[i]);\n\t\t});\n\t}\n\tif (graph.vertices_edges) {\n\t\t// ensure the arrays for these vertices exist\n\t\tvertices\n\t\t\t.filter(vertex => !graph.vertices_edges[vertex])\n\t\t\t.forEach(vertex => { graph.vertices_edges[vertex] = []; });\n\t\tvertices.forEach((vertex) => { graph.vertices_edges[vertex].push(edge); });\n\t}\n\tif (graph.vertices_faces) {\n\t\t// ensure the arrays for these vertices exist. that is all. empty arrays.\n\t\tvertices\n\t\t\t.filter(vertex => !graph.vertices_faces[vertex])\n\t\t\t.forEach(vertex => { graph.vertices_faces[vertex] = []; });\n\t}\n\treturn edge;\n};\n\n/**\n * @description Add an edge to a graph, ideally a graph without faces,\n * update the edges_ properties, and update the affected vertices_vertices\n * and vertices_edges unsorted, which will break sorting order in any\n * graph that contains faces.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} vertices the two vertices to connect and make a new edge\n * @param {number[]} [faces=[]] two faces to become the edges_faces\n * @param {string} [assignment=\"U\"] this will become edges_assignment\n * @param {number} [foldAngle=0] this will become edges_foldAngle\n * @returns {number} the index of the new edge\n */\n// export const addUnsortedEdge = (\n// \tgraph,\n// \tvertices,\n// \tfaces = [],\n// \tassignment = \"U\",\n// \tfoldAngle = 0,\n// ) => {\n// \tconst edge = addEdge(graph, vertices, faces, assignment, foldAngle);\n\n// \t// for each of the two vertices_vertices, add the opposite vertex.\n// \tif (graph.vertices_vertices) {\n// \t\tconst opposite = [vertices[1], vertices[0]];\n// \t\tvertices.forEach((v, i) => {\n// \t\t\tgraph.vertices_vertices[v] = Array\n// \t\t\t\t.from(new Set([...graph.vertices_vertices[v], opposite[i]]));\n// \t\t});\n// \t}\n\n// \t// for each of the two vertices_edges, add this new edge\n// \tif (graph.vertices_edges) {\n// \t\tvertices.forEach(v => {\n// \t\t\tgraph.vertices_edges[v] = Array\n// \t\t\t\t.from(new Set([...graph.vertices_edges[v], edge]));\n// \t\t});\n// \t}\n\n// \t// we will not be creating or modifying any faces, so no changes to\n// \t// vertices_faces, edges_faces, faces_vertices, or faces_edges.\n// \treturn edge;\n// };\n"
  },
  {
    "path": "src/graph/add/vertex.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Add a vertex to the graph by setting its coordinates.\n * This method will maintain that all other arrays in the graph are valid,\n * any \"vertices_\" arrays that exist in the graph will be filled with\n * empty arrays. This vertex will be initialized as isolated.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]|[number, number, number]} coords\n * the position of the new vertex\n * @param {number[]} [vertices] optional vertices to become vertices_vertices\n * @param {number[]} [edges] optional edges to become vertices_edges\n * @param {number[]} [faces] optional faces to become vertices_faces\n * @returns {number} the index of the newly created vertex\n */\nexport const addVertex = (\n\tgraph,\n\tcoords,\n\tvertices = [],\n\tedges = [],\n\tfaces = [],\n) => {\n\tif (!graph.vertices_coords) { graph.vertices_coords = []; }\n\n\t// the index of the new vertex\n\tconst vertex = graph.vertices_coords.length;\n\n\t// construct the new data for our vertex, it will be initially isolated.\n\tgraph.vertices_coords[vertex] = coords;\n\tif (graph.vertices_vertices) { graph.vertices_vertices[vertex] = vertices; }\n\tif (graph.vertices_edges) { graph.vertices_edges[vertex] = edges; }\n\tif (graph.vertices_faces) { graph.vertices_faces[vertex] = faces; }\n\n\treturn vertex;\n};\n\n/**\n * @description Add vertices to the graph by setting their coordinates.\n * This method will maintain that all other arrays in the graph are valid,\n * any \"vertices_\" arrays that exist in the graph will be filled with\n * empty arrays. The new vertices will be initialized as isolated.\n * @param {FOLD} graph a FOLD object, modified in place.\n * @param {[number, number][]|[number, number, number][]} points\n * array of points to be added to the graph\n * @returns {number[]} index of vertex in new vertices_coords array.\n * the size of this array matches array size of source vertices.\n * duplicate (non-added) vertices returns their pre-existing counterpart's index.\n */\nexport const addVertices = (graph, points = []) => {\n\tif (!graph.vertices_coords) { graph.vertices_coords = []; }\n\n\t// the indices of the new vertices\n\tconst vertices = points.map((_, i) => graph.vertices_coords.length + i);\n\n\t// construct the new data for our vertices, they will be initially isolated.\n\tvertices.forEach((vertex, i) => {\n\t\tgraph.vertices_coords[vertex] = points[i];\n\t\tif (graph.vertices_vertices) { graph.vertices_vertices[vertex] = []; }\n\t\tif (graph.vertices_edges) { graph.vertices_edges[vertex] = []; }\n\t\tif (graph.vertices_faces) { graph.vertices_faces[vertex] = []; }\n\t});\n\n\treturn vertices;\n};\n"
  },
  {
    "path": "src/graph/boundary.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tboundingBox as BoundingBox,\n} from \"../math/polygon.js\";\nimport {\n\tassignmentIsBoundary,\n} from \"../fold/spec.js\";\nimport {\n\tuniqueElements,\n} from \"../general/array.js\";\nimport {\n\tdisjointGraphs,\n} from \"./disjoint.js\";\nimport {\n\tinvertFlatToArrayMap,\n} from \"./maps.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"./make/verticesEdges.js\";\nimport {\n\tmakeVerticesVertices2D,\n\tmakeVerticesVerticesUnsorted,\n} from \"./make/verticesVertices.js\";\nimport {\n\tmakeVerticesToEdge,\n} from \"./make/lookup.js\";\nimport {\n\tconnectedComponents,\n} from \"./connectedComponents.js\";\n\n/**\n * @description Make an axis-aligned bounding box that encloses the vertices of\n * a FOLD object. the optional padding is used to make the bounding box\n * inclusive / exclusive by adding padding on all sides, or inset in the case\n * of negative number. (positive=inclusive boundary, negative=exclusive boundary)\n * @param {FOLD} graph a FOLD object\n * @param {number} [padding] an optional padding around the vertices\n * to be included in the bounding box.\n * @returns {Box?} dimensions stored as \"span\" \"min\" and \"max\".\n * \"undefined\" if no vertices exist in the graph.\n */\nexport const boundingBox = ({ vertices_coords }, padding) => (\n\tBoundingBox(vertices_coords, padding)\n);\n\n/**\n * @description A vertex is a boundary vertex if it is a member of a boundary\n * edge, as defined by edges_assignment. If edges_assignment is not present,\n * or does not contain boundary edges, this will return an empty array.\n * If you need *sorted* vertices, use the boundary() or boundaries() method.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} unsorted list of vertex indices which lie along the boundary.\n */\nexport const boundaryVertices = ({ edges_vertices, edges_assignment = [] }) => (\n\tuniqueElements(edges_vertices\n\t\t.filter((_, i) => assignmentIsBoundary[edges_assignment[i]])\n\t\t.flat()));\n\n/**\n * @description return value for \"boundary\" method.\n */\nconst emptyBoundaryObject = () => ({ vertices: [], edges: [] });\n\n/**\n * @description Use this method if edges are already marked with a \"boundary\"\n * assignment, and this will get the boundary of a FOLD graph in terms of\n * both vertices and edges. Use this method when you know there is only one\n * connected boundary in the graph. If there are more than one,\n * use the \"boundaries\" method.\n * @param {FOLD} graph a FOLD object\n * @returns {{\n *   vertices: number[],\n *   edges: number[],\n * }} with \"vertices\" and \"edges\", each arrays of indices.\n */\nexport const boundary = ({ vertices_edges, edges_vertices, edges_assignment }) => {\n\tif (!edges_assignment || !edges_vertices) { return emptyBoundaryObject(); }\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\t// true or false if an edge is a boundary edge, additionally,\n\t// turn a true into a false once we use the edge\n\tconst edgesBoundary = edges_assignment.map(a => a === \"B\" || a === \"b\");\n\n\t// inverse of \"edgesBoundary\", every time we visit a vertex, mark it true.\n\tconst usedVertices = {};\n\n\t// the resulting boundary is stored in these two arrays\n\tconst vertices = [];\n\tconst edges = [];\n\n\t// get the first available edge index that is a boundary\n\tlet edgeIndex = edgesBoundary\n\t\t.map((isBoundary, e) => (isBoundary ? e : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.shift();\n\tif (edgeIndex === undefined) { return emptyBoundaryObject(); }\n\n\t// add the first edge and remove it from the available edge list\n\tedgesBoundary[edgeIndex] = false;\n\tedges.push(edgeIndex);\n\n\t// add the edge's first vertex to the result, mark it used, set the second\n\t// vertex to the \"nextVertex\" which we will use to walk around the boundary\n\tvertices.push(edges_vertices[edgeIndex][0]);\n\tusedVertices[edges_vertices[edgeIndex][0]] = true;\n\tlet nextVertex = edges_vertices[edgeIndex][1];\n\n\t// loop as long as \"nextVertex\" is not in the used vertices list.\n\twhile (!usedVertices[nextVertex]) {\n\t\t// add nextVertex to our solutions list, mark it as used\n\t\tvertices.push(nextVertex);\n\t\tusedVertices[nextVertex] = true;\n\n\t\t// find the next edge index by consulting the nextVertex's adjacent edges,\n\t\t// filtering out the first one that has not yet been visited.\n\t\tedgeIndex = vertices_edges[nextVertex]\n\t\t\t.filter(v => edgesBoundary[v])\n\t\t\t.shift();\n\n\t\t// the boundary is not a nice, neat cycle.\n\t\tif (edgeIndex === undefined) { return emptyBoundaryObject(); }\n\n\t\t// advance nextVertex, look at our current edge and select the\n\t\t// other vertex that isn't the current \"nextVertex\".\n\t\tif (edges_vertices[edgeIndex][0] === nextVertex) {\n\t\t\t[, nextVertex] = edges_vertices[edgeIndex];\n\t\t} else {\n\t\t\t[nextVertex] = edges_vertices[edgeIndex];\n\t\t}\n\n\t\t// add the next edge to our solution, mark it as used\n\t\tedges.push(edgeIndex);\n\t\tedgesBoundary[edgeIndex] = false;\n\t}\n\treturn { vertices, edges };\n};\n\n/**\n * @description Use this method if edges are already marked with a \"boundary\"\n * assignment, and this will get the boundaries of a FOLD graph in terms of\n * both vertices and edges. This method will safely find all boundaries,\n * in case of a graph that has two disjoint sets.\n * @param {FOLD} graph a FOLD object\n * @returns {{\n *   vertices: number[],\n *   edges: number[],\n * }[]} an array of boundary solutions, where each boundary\n * is an object with \"vertices\" and \"edges\", each arrays of indices.\n */\nexport const boundaries = ({ vertices_edges, edges_vertices, edges_assignment }) => {\n\tif (!edges_assignment || !edges_vertices) {\n\t\treturn [emptyBoundaryObject()];\n\t}\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\n\t// create a copy of edges_vertices that only contains boundary edges, this\n\t// array will have holes, no indices change they will match the input graph.\n\tconst edges_verticesBoundary = [...edges_vertices];\n\n\t// delete the non-boundary edges\n\tedges_assignment.map(a => a === \"B\" || a === \"b\")\n\t\t.map((isBoundary, e) => (!isBoundary ? e : undefined))\n\t\t.filter(e => e !== undefined)\n\t\t.forEach(e => delete edges_verticesBoundary[e]);\n\n\t// a copy of vertices_edges, but only includes boundary edges\n\tconst vertices_edgesBoundary = makeVerticesEdgesUnsorted({\n\t\tedges_vertices: edges_verticesBoundary,\n\t});\n\n\t// an adjacent vertices_vertices list, but only include boundary vertices\n\tconst verticesVertices = makeVerticesVerticesUnsorted({\n\t\tvertices_edges: vertices_edgesBoundary,\n\t\tedges_vertices: edges_verticesBoundary,\n\t});\n\n\t// using the boundary-only vertices_vertices, make a connected components,\n\t// each vertex (index) will contain a group number that it is a part of (value)\n\tconst connectedVertices = connectedComponents(verticesVertices);\n\n\t// using the connected components, create a list of groups where, each group\n\t// is a list of vertices, then, for each group, take just one vertex.\n\tconst groupsVertex = invertFlatToArrayMap(connectedVertices)\n\t\t.map(vertices => vertices[0]);\n\n\t// given a start vertex and the list of verticesVertices, walk through\n\t// the adjacent vertex list, modifying the verticesVertices object as we go\n\t// by removing visited vertices, until we reach a verticesVertices with no\n\t// available vertices to travel to. return the list of vertices walked.\n\tconst walkVerticesVertices = (startVertex) => {\n\t\tlet prevVertex;\n\t\tlet currVertex = startVertex;\n\t\tlet nextVertex;\n\t\tconst result = [];\n\t\tconst filterFunc = (v) => v !== prevVertex;\n\t\twhile (true) {\n\t\t\t// in an ideal situation, the current verticesVertices array will have\n\t\t\t// the vertex from which we just came, and the one to head to next.\n\t\t\t// remove the vertex from which we just came, and set the next vertex.\n\t\t\tverticesVertices[currVertex] = verticesVertices[currVertex]\n\t\t\t\t.filter(filterFunc);\n\t\t\tnextVertex = verticesVertices[currVertex].shift();\n\n\t\t\t// if the array was empty, there is nowhere else to go. we're done.\n\t\t\tif (nextVertex === undefined) { return result; }\n\n\t\t\t// add the current vertex to the result list, then increment the walk.\n\t\t\tresult.push(currVertex);\n\t\t\tprevVertex = currVertex;\n\t\t\tcurrVertex = nextVertex;\n\t\t}\n\t};\n\n\t// for each group of connected vertices, using the first vertex from\n\t// the group, walk the connected vertices and get back a list of vertices.\n\tconst boundariesVertices = groupsVertex\n\t\t.map(vertex => walkVerticesVertices(vertex));\n\n\t// backwards lookup, which edge is made of a pair of vertices.\n\t// we only need to use the boundary edges, should be a little faster.\n\tconst edgeMap = makeVerticesToEdge({\n\t\tedges_vertices: edges_verticesBoundary,\n\t});\n\n\t// group vertices into pairs, find the edge that connects each pair.\n\tconst boundariesEdges = boundariesVertices\n\t\t.map(vertices => vertices\n\t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t\t.map(pair => edgeMap[pair.join(\" \")]));\n\n\treturn boundariesVertices.map((vertices, i) => ({\n\t\tvertices,\n\t\tedges: boundariesEdges[i],\n\t}));\n};\n\n/**\n * @description Get a FOLD object's boundary as a list of points. Use this\n * method in the case where there might be more than one boundary cycle.\n * @param {FOLD} graph a FOLD object\n * @returns {([number, number]|[number, number, number])[][]} array of polygons\n */\nexport const boundaryPolygons = ({\n\tvertices_coords, vertices_edges, edges_vertices, edges_assignment,\n}) => (\n\tboundaries({ vertices_edges, edges_vertices, edges_assignment })\n\t\t.map(({ vertices }) => vertices.map(v => vertices_coords[v]))\n);\n\n/**\n * @description Get a FOLD object's boundary as a list of points. Use this\n * method if you are certain that there is only one boundary.\n * @param {FOLD} graph a FOLD object\n * @returns {([number, number]|[number, number, number])[]} a polygon\n */\nexport const boundaryPolygon = ({\n\tvertices_coords, vertices_edges, edges_vertices, edges_assignment,\n}) => (\n\tboundary({ vertices_edges, edges_vertices, edges_assignment })\n\t\t.vertices\n\t\t.map(v => vertices_coords[v])\n);\n\n/**\n * @description Use this method when you know there is only one connected\n * boundary in the graph. If there are more than one, use \"planarBoundaries\".\n * When a graph does not have boundary assignment information,\n * this method is used to uncover the boundary, so long as the graph is planar.\n * Get the boundary as two arrays of vertices and edges\n * by walking the boundary edges in 2D and uncovering the concave hull.\n * Does not consult edges_assignment, but does require vertices_coords.\n * For repairing crease patterns, this will uncover boundary edges_assignments.\n * @param {FOLD} graph a FOLD object\n * (vertices_coords, vertices_vertices, edges_vertices)\n * (vertices edges only required in case vertices_vertices needs to be built)\n * @returns {{\n *   vertices: number[],\n *   edges: number[],\n * }} \"vertices\" and \"edges\" with arrays of indices.\n * @usage call populate() before to ensure this works.\n */\nexport const planarBoundary = ({\n\tvertices_coords, vertices_edges, vertices_vertices, edges_vertices,\n}) => {\n\tif (!vertices_vertices) {\n\t\tvertices_vertices = makeVerticesVertices2D({\n\t\t\tvertices_coords, vertices_edges, edges_vertices,\n\t\t});\n\t}\n\tconst edge_map = makeVerticesToEdge({ edges_vertices });\n\tconst edge_walk = [];\n\tconst vertex_walk = [];\n\tconst walk = {\n\t\tvertices: vertex_walk,\n\t\tedges: edge_walk,\n\t};\n\n\tlet largestX = -Infinity;\n\tlet first_vertex_i = -1;\n\tvertices_coords.forEach((v, i) => {\n\t\tif (v[0] > largestX) {\n\t\t\tlargestX = v[0];\n\t\t\tfirst_vertex_i = i;\n\t\t}\n\t});\n\n\tif (first_vertex_i === -1) { return walk; }\n\tvertex_walk.push(first_vertex_i);\n\tconst first_vc = vertices_coords[first_vertex_i];\n\tconst first_neighbors = vertices_vertices[first_vertex_i];\n\tif (!first_neighbors) { return walk; }\n\t// sort adjacent vertices by next most clockwise vertex;\n\tconst counter_clock_first_i = first_neighbors\n\t\t.map(i => vertices_coords[i])\n\t\t.map(vc => [vc[0] - first_vc[0], vc[1] - first_vc[1]])\n\t\t.map(vec => Math.atan2(vec[1], vec[0]))\n\t\t.map(angle => (angle < 0 ? angle + Math.PI * 2 : angle))\n\t\t.map((a, i) => ({ a, i }))\n\t\t.sort((a, b) => a.a - b.a)\n\t\t.shift()\n\t\t.i;\n\tconst second_vertex_i = first_neighbors[counter_clock_first_i];\n\t// find this edge that connects these 2 vertices\n\tconst first_edge_lookup = first_vertex_i < second_vertex_i\n\t\t? `${first_vertex_i} ${second_vertex_i}`\n\t\t: `${second_vertex_i} ${first_vertex_i}`;\n\tconst first_edge = edge_map[first_edge_lookup];\n\t// vertex_walk.push(second_vertex_i);\n\tedge_walk.push(first_edge);\n\n\t// now we begin the loop\n\n\t// walking the graph, we look at 3 vertices at a time. in sequence:\n\t// prev_vertex, this_vertex, next_vertex\n\tlet prev_vertex_i = first_vertex_i;\n\tlet this_vertex_i = second_vertex_i;\n\t// because this is an infinite loop, and it relies on vertices_vertices\n\t// being well formed (if it was user-made, we cannot guarantee), we will\n\t// break the loop if we walk past the same pair of vertices (in the same dir)\n\tconst visitedVertexPairs = { [`${prev_vertex_i} ${this_vertex_i}`]: true };\n\twhile (true) {\n\t\tconst next_neighbors = vertices_vertices[this_vertex_i];\n\t\tconst from_neighbor_i = next_neighbors.indexOf(prev_vertex_i);\n\t\tconst next_neighbor_i = (from_neighbor_i + 1) % next_neighbors.length;\n\t\tconst next_vertex_i = next_neighbors[next_neighbor_i];\n\t\tconst next_edge_lookup = this_vertex_i < next_vertex_i\n\t\t\t? `${this_vertex_i} ${next_vertex_i}`\n\t\t\t: `${next_vertex_i} ${this_vertex_i}`;\n\t\tconst next_edge_i = edge_map[next_edge_lookup];\n\t\t// exit loop condition\n\t\tif (visitedVertexPairs[`${this_vertex_i} ${next_vertex_i}`]) {\n\t\t\t// if the first and upcoming edge do not match, this means we somehow\n\t\t\t// walked around the boundary and ended up somewhere in the middle,\n\t\t\t// which would put us in a never ending cycle, so we need to break\n\t\t\t// out anyway, but at least warn the user the result is poorly formed.\n\t\t\t// todo: we can get rid of this message if we modify the structure\n\t\t\t// of the return object so that it traverses the circular part only,\n\t\t\t// or, includes a there-and-back traversal of the branch(es).\n\t\t\tif (next_edge_i !== edge_walk[0]) { console.warn(\"bad boundary\"); }\n\t\t\treturn walk;\n\t\t}\n\t\tvisitedVertexPairs[`${this_vertex_i} ${next_vertex_i}`] = true;\n\t\tvertex_walk.push(this_vertex_i);\n\t\tedge_walk.push(next_edge_i);\n\t\tprev_vertex_i = this_vertex_i;\n\t\tthis_vertex_i = next_vertex_i;\n\t}\n};\n\n/**\n * @description When a graph does not have boundary assignment information,\n * this method is used to uncover the boundaries, so long as the graph is planar.\n * This works for disjoint graphs, the return value is an array of results.\n * Each boundary result is two arrays of vertices and edges, discovered\n * by walking the boundary edges in 2D and uncovering the concave hull.\n * Does not consult edges_assignment, but does require vertices_coords.\n * @param {FOLD} graph a FOLD object\n * (vertices_coords, vertices_vertices, edges_vertices)\n * (vertices edges only required in case vertices_vertices needs to be built)\n * @returns {{\n *   vertices: number[],\n *   edges: number[],\n * }[]} array of objects with \"vertices\" and \"edges\" with arrays of indices.\n * @usage call populate() before to ensure this works.\n */\nexport const planarBoundaries = ({\n\tvertices_coords, vertices_edges, vertices_vertices, edges_vertices,\n}) => {\n\tif (!vertices_vertices) {\n\t\tvertices_vertices = makeVerticesVertices2D({\n\t\t\tvertices_coords, vertices_edges, edges_vertices,\n\t\t});\n\t}\n\treturn disjointGraphs({\n\t\tvertices_coords, vertices_vertices, edges_vertices,\n\t}).map(planarBoundary);\n};\n"
  },
  {
    "path": "src/graph/clean.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tremoveDuplicateVertices,\n} from \"./vertices/duplicate.js\";\nimport {\n\tremoveIsolatedVertices,\n} from \"./vertices/isolated.js\";\nimport {\n\tremoveDuplicateEdges,\n} from \"./edges/duplicate.js\";\nimport {\n\tremoveCircularEdges,\n} from \"./edges/circular.js\";\nimport {\n\tmergeFlatNextmaps,\n\tinvertFlatMap,\n} from \"./maps.js\";\n\n/**\n * @description This method will remove bad graph data. this includes:\n * - duplicate (distance in 2D/3D) and isolated vertices\n * - circular and duplicate edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {object} summary of changes, a nextmap and the indices removed.\n */\nexport const clean = (graph, epsilon) => {\n\t// duplicate vertices has to be done first as it's possible that\n\t// this will create circular/duplicate edges.\n\tconst change_v1 = removeDuplicateVertices(graph, epsilon);\n\tconst change_e1 = removeCircularEdges(graph);\n\tconst change_e2 = removeDuplicateEdges(graph);\n\n\t// isolated vertices is last. removing edges can create isolated vertices\n\tconst change_v2 = removeIsolatedVertices(graph);\n\n\t// return a summary of changes.\n\t// use the maps to update the removed indices from the second step\n\t// to their previous index before change 1 occurred.\n\tconst change_v1_backmap = invertFlatMap(change_v1.map);\n\tconst change_v2_remove = change_v2.remove.map(e => change_v1_backmap[e]);\n\tconst change_e1_backmap = invertFlatMap(change_e1.map);\n\tconst change_e2_remove = change_e2.remove.map(e => change_e1_backmap[e]);\n\treturn {\n\t\tvertices: {\n\t\t\tmap: mergeFlatNextmaps(change_v1.map, change_v2.map),\n\t\t\tremove: change_v1.remove.concat(change_v2_remove),\n\t\t},\n\t\tedges: {\n\t\t\tmap: mergeFlatNextmaps(change_e1.map, change_e2.map),\n\t\t\tremove: change_e1.remove.concat(change_e2_remove),\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "src/graph/connectedComponents.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Given a self-relational array of arrays, for example,\n * vertices_vertices, edges_edges, faces_faces, where the values in the\n * inner arrays relate to the indices of the outer array, create\n * collection groups where each item is included in a group if it\n * points to another member in that group.\n * The result will be in the form of an array matching in length the\n * number of components, for each component, the value is its group number.\n * @param {number[][]} array_array an array of arrays of numbers\n * @returns {number[]} component array, the value is the group number\n */\nexport const connectedComponents = (array_array) => {\n\tconst components = [];\n\t/** @param {number} index @param {number} currentGroup */\n\tconst recurse = (index, currentGroup) => {\n\t\t// only set unset component entries.\n\t\t// the return value is only meaningful during the first call to recurse()\n\t\t// for each row, meaning, if this row has already been set at time of the\n\t\t// first call, then no components have this group index, so, use this group\n\t\t// index for the next component and next recursion.\n\t\tif (components[index] !== undefined) { return 0; }\n\n\t\t// set this component to be a member of the current group, and recurse\n\t\t// through all of its adjacent components, setting every adjacent\n\t\t// component to be a member of this group. in this case do nothing with\n\t\t// the return value of recurse().\n\t\tcomponents[index] = currentGroup;\n\t\tarray_array[index].forEach(i => recurse(i, currentGroup));\n\n\t\t// this group number has been applied to one or more components,\n\t\t// return and tell the main loop to use the next group index for the\n\t\t// next set of components and their recursion.\n\t\treturn 1;\n\t};\n\n\t// iterate through every row, increment the group index only if one or more\n\t// components were set to this group, so we don't skip any numbers.\n\tfor (let row = 0, group = 0; row < array_array.length; row += 1) {\n\t\t// allow arrays with holes\n\t\tif (!(row in array_array)) { continue; }\n\t\tgroup += recurse(row, group);\n\t}\n\treturn components;\n};\n\n/**\n * @description Given a self-relational array of arrays, for example,\n * vertices_vertices, edges_edges, faces_faces, where the values in the\n * inner arrays relate to the indices of the outer array, create a list of\n * all pairwise combinations of connected indices. All pairs are sorted\n * so that for [i, j], i <= j. This allows for circular references (i === j).\n * @param {number[][]} array_array an array of arrays of integers\n * @returns {[number, number][]} array of two-dimensional array pairs of indices.\n */\nexport const connectedComponentsPairs = (array_array) => {\n\tconst pairs = [];\n\n\t// if a component's index is self-adjacent, this functions as a hash\n\t// lookup to ensure that the reference appears only once.\n\tconst circular = [];\n\tarray_array.forEach((arr, i) => arr.forEach(j => {\n\t\t// assuming that i exists in j's adjacent, and visa versa, we will visit\n\t\t// every permutation twice. only include the one where i < j.\n\t\tif (i < j) { pairs.push([i, j]); }\n\n\t\t// if an index is self-referential, ensure that it is only added once\n\t\tif (i === j && !circular[i]) {\n\t\t\tcircular[i] = true;\n\t\t\tpairs.push([i, j]);\n\t\t}\n\t}));\n\treturn pairs;\n};\n"
  },
  {
    "path": "src/graph/count.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfilterKeysWithPrefix,\n\tfilterKeysWithSuffix,\n} from \"../fold/spec.js\";\n\nconst ordersArrayNames = {\n\tedges: \"edgeOrders\",\n\tfaces: \"faceOrders\",\n};\n\n/**\n * @description Given an array of arrays, count the length of each array\n * and return the maximum length. Ensure the array exists too.\n * @param {any[]} arrays any number of arrays\n * @returns {number} the length of the longest array\n */\nconst maxArraysLength = (arrays) => Math.max(0, ...(arrays\n\t.filter(el => el !== undefined)\n\t.map(el => el.length)));\n\n/**\n * @description Search inside arrays inside arrays inside arrays\n * and return the largest number.\n * @param {number[][][]} arrays an array of arrays of numbers\n * @returns {number} largest number in array in arrays.\n */\nconst maxValueInArrayInArray = (arrays) => {\n\tlet max = -1; // will become 0 if nothing is found\n\tarrays\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(arr => arr\n\t\t\t.forEach(el => el\n\t\t\t\t.forEach((e) => {\n\t\t\t\t\tif (e > max) { max = e; }\n\t\t\t\t})));\n\treturn max;\n};\n\n/**\n * @description Search inside arrays inside arrays and return\n * the largest number by only checking indices 0 and 1 in the\n * inner arrays; meant for faceOrders or edgeOrders.\n * @param {number[][]} array a faceOrders or edgeOrders array\n * @returns {number} largest number in indices 0 or 1 of array in arrays.\n */\nconst maxValueInOrders = (array) => {\n\tlet max = -1; // will become 0 if nothing is found\n\tarray.forEach(el => {\n\t\t// exception. index 2 is orientation, not index. check only 0, 1\n\t\tif (el[0] > max) { max = el[0]; }\n\t\tif (el[1] > max) { max = el[1]; }\n\t});\n\treturn max;\n};\n\n/**\n * @description Get the number of vertices, edges, or faces in the graph by\n * simply checking the length of arrays starting with the key; in the case\n * of differing array lengths (which shouldn't happen) return the largest number.\n *\n * This works even with custom component names in place of \"vertices\", \"edges\"...\n *\n * This will fail in the case of abstract graphs, for example where no vertices\n * are defined in a vertex_ array, but still exist as mentions in faces_vertices.\n * In that case, use the implied count method. \"count_implied.js\"\n * @param {FOLD} graph a FOLD object\n * @param {string} key the prefix for a key, eg: \"vertices\"\n * @returns {number} the number of the requested element type in the graph\n */\nexport const count = (graph, key) => (\n\tmaxArraysLength(filterKeysWithPrefix(graph, key).map(k => graph[k])));\n\n/**\n * @description Get the number of vertices in a graph.\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of vertices in the graph\n */\nexport const countVertices = ({\n\tvertices_coords,\n\tvertices_vertices,\n\tvertices_edges,\n\tvertices_faces,\n}) => (\n\tmaxArraysLength([\n\t\tvertices_coords,\n\t\tvertices_vertices,\n\t\tvertices_edges,\n\t\tvertices_faces,\n\t]));\n\n/**\n * @description Get the number of edges in a graph.\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of edges in the graph\n */\nexport const countEdges = ({ edges_vertices, edges_faces }) => (\n\tmaxArraysLength([edges_vertices, edges_faces]));\n\n/**\n * @description Get the number of faces in a graph.\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of faces in the graph\n */\nexport const countFaces = ({ faces_vertices, faces_edges, faces_faces }) => (\n\tmaxArraysLength([faces_vertices, faces_edges, faces_faces]));\n\n/**\n * @description Get the number of vertices, edges, or faces in the graph, as\n * evidenced by their appearance in other arrays; ie: searching faces_vertices\n * for the largest vertex index, and inferring number of vertices is that long.\n * @param {FOLD} graph a FOLD object\n * @param {string} key the prefix for a key, eg: \"vertices\"\n * @returns {number} the number of vertices, edges, or faces in the graph.\n */\nexport const countImplied = (graph, key) => Math.max(\n\t// return the maximum value between (1/2):\n\t// 1. a found geometry in another geometry's array (\"vertex\" in \"faces_vertices\")\n\tmaxValueInArrayInArray(\n\t\tfilterKeysWithSuffix(graph, key).map(str => graph[str]),\n\t),\n\t// 2. a found geometry in a faceOrders or edgeOrders type of array (special case)\n\tgraph[ordersArrayNames[key]]\n\t\t? maxValueInOrders(graph[ordersArrayNames[key]])\n\t\t: -1,\n) + 1;\n\n/**\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of components\n */\nexport const countImpliedVertices = graph => countImplied(graph, \"vertices\");\n\n/**\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of components\n */\nexport const countImpliedEdges = graph => countImplied(graph, \"edges\");\n\n/**\n * @param {FOLD} graph a FOLD object\n * @returns {number} the number of components\n */\nexport const countImpliedFaces = graph => countImplied(graph, \"faces\");\n"
  },
  {
    "path": "src/graph/cycles.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\ttopologicalSort,\n} from \"./directedGraph.js\";\nimport {\n\tmakeFacesWinding,\n} from \"./faces/winding.js\";\nimport {\n\tjoin,\n} from \"./join.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"./make/edgesFaces.js\";\nimport {\n\tinvertArrayMap,\n} from \"./maps.js\";\nimport {\n\tmakeFacesNormal,\n} from \"./normals.js\";\nimport {\n\tfaceOrdersToDirectedEdges,\n} from \"./orders.js\";\nimport {\n\tplanarizeVerbose,\n} from \"./planarize/planarize.js\";\n\n/**\n * @description Given a graph, reassign edge assignments to be \"J\" join\n * if the two adjacent faces (according to the face map) are from\n * the same face.\n * @param {FOLD} planar\n * @param {number[]} flatBackmap\n * @return {FOLD}\n */\nconst makeOneSide = (planar, flatBackmap) => {\n\tplanar.edges_faces.forEach((faces, e) => {\n\t\tif (faces.length !== 2) { return; }\n\t\t// get the top-most face in the stack, this is the face which will be visible\n\t\tconst [a, b] = faces.map(f => flatBackmap[f]);\n\t\t// if a === b, the two adjacent faces came from the same face. change the\n\t\t// assignment between them to be a \"join\".\n\t\tif (a === b && planar.edges_assignment) {\n\t\t\tplanar.edges_assignment[e] = \"J\";\n\t\t}\n\t\t// this is kind of unnecessary, but it will ensure that the graph is valid\n\t\tif (a === b && planar.edges_foldAngle) {\n\t\t\tplanar.edges_foldAngle[e] = 0;\n\t\t}\n\t});\n\treturn planar;\n};\n\n/**\n * @description Flip the winding order of faces in a graph according to\n * the facemap, make the winding of a face match the winding of the reference\n * face from the facemap.\n * @param {FOLD} planar\n * @param {boolean[]} faces_winding\n * @param {number[]} flatBackmap\n */\nconst correctFaceWinding = (planar, faces_winding, flatBackmap) => (\n\tplanar.faces_vertices\n\t\t.map((_, i) => i)\n\t\t.filter(f => !faces_winding[flatBackmap[f]])\n\t\t.forEach(f => {\n\t\t\tplanar.faces_vertices[f].reverse();\n\t\t\tplanar.faces_edges[f].reverse();\n\t\t\t// winding order between the two is not correct until this happens\n\t\t\tplanar.faces_edges[f].push(planar.faces_edges[f].shift());\n\t\t}));\n\n/**\n * @description Create a copy of a graph suitable for rendering, fixing\n * any cycles of orders between faces so that no cycles exist. The resulting\n * graph will be very different in its graph structure, but appear correct\n * from both front and back.\n * @param {FOLD} graph a FOLD object in foldedForm, can contain holes.\n * @returns {FOLD} a (heavily modified) version of the graph but one which\n * appears the same in renderings.\n */\nexport const fixCycles = (graph) => {\n\tconst {\n\t\tresult: planar,\n\t\tchanges: { faces: { map } },\n\t} = planarizeVerbose(graph);\n\tplanar.edges_faces = makeEdgesFacesUnsorted(planar);\n\tconst facesBackMap = invertArrayMap(map);\n\n\tif (!planar.edges_assignment && planar.edges_vertices) {\n\t\tplanar.edges_assignment = planar.edges_vertices.map(() => \"U\");\n\t}\n\n\t// this should happen on the input graph, not the planar graph.\n\tconst faces_normal = makeFacesNormal(graph);\n\tconst directedFacesOld = faceOrdersToDirectedEdges({ ...graph, faces_normal });\n\tconst faces_winding = makeFacesWinding(graph);\n\n\t// todo: need to optimize this block\n\t// backmap contains values with old face indices\n\tconst facesBackMapOrdered = facesBackMap.map(faces => {\n\t\tconst lookup = {};\n\t\tfaces.forEach(f => { lookup[f] = true; });\n\t\tconst theseFaces = directedFacesOld.filter(([a, b]) => lookup[a] && lookup[b]);\n\t\tconst facesSorted = topologicalSort(theseFaces);\n\t\tconst missingFaces = faces.filter(f => facesSorted.indexOf(f) === -1);\n\t\treturn missingFaces.concat(facesSorted);\n\t});\n\n\t// now that our backmap is ordered along one axis, create two flat backmaps\n\t// one for each side: front and back. create each by grabbing the first or\n\t// last face from the ordered list, respectively.\n\tconst faceMapFront = facesBackMapOrdered.map(arr => arr[0])\n\tconst faceMapBack = facesBackMapOrdered.map(arr => arr.slice().reverse()[0]);\n\n\t// duplicate the graph so that each graph represents what the graph appears\n\t// like from both front and back directions. This involves reassigning some\n\t// creases to be \"J\" join assignment in the case where two adjacent faces\n\t// came from the same original face (as appears from this side).\n\tconst front = makeOneSide(structuredClone(planar), faceMapFront);\n\tconst back = makeOneSide(planar, faceMapBack);\n\n\t// all face windings are upright due to the planarize method.\n\t// correct the winding of each front/back graph to match the\n\t// topmost/bottommost face winding from the original graph.\n\tcorrectFaceWinding(front, faces_winding, faceMapFront);\n\tcorrectFaceWinding(back, faces_winding, faceMapBack);\n\n\t// we are about to join two copies of the same graph, create an ordering between\n\t// pairs of copies of the same face (index difference by +length) where one set\n\t// is always placed in front of the other. \"in front of\" means something relative\n\t// to the g face ([f, g, order]) depending on its winding, so, order accordingly.\n\t/** @type {[number, number, number][]} */\n\tconst faceOrders = front.faces_vertices\n\t\t.map((_, f) => (faces_winding[faceMapFront[f]]\n\t\t\t? [front.faces_vertices.length + f, f, -1]\n\t\t\t: [front.faces_vertices.length + f, f, 1]))\n\n\t// console.log(front.faces_vertices, back.faces_vertices);\n\t// join two graphs into one, the result is stored into \"front\" graph.\n\tjoin(front, back);\n\treturn {\n\t\t...front,\n\t\tfaceOrders,\n\t\tframe_classes: [\"foldedForm\"],\n\t}\n};\n"
  },
  {
    "path": "src/graph/directedGraph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tuniqueSortedNumbers,\n} from \"../general/array.js\";\nimport {\n\tinvertFlatMap,\n} from \"./maps.js\";\n\n/**\n * @description Perform a topological sort on a directed acyclic graph.\n * This method assumes your graph is acyclic and will *not* do any testing.\n * If your graph contains cycles this will still return an (invalid) ordering.\n * @param {[number, number][]} directedEdges an array of directed edges\n * where each edge contains two vertex indices with the direction going\n * from index 0 towards 1\n * @returns {number[]} an ordering of the vertices from the edges provided.\n */\nexport const topologicalSortQuick = (directedEdges) => {\n\t// flat list of all vertices involved (can contain holes)\n\tconst vertices = uniqueSortedNumbers(directedEdges.flat());\n\n\t// array where indices are vertices and values are arrays of vertices\n\t// which are \"parents\" of this vertex (other vertices point to this one).\n\tconst verticesParents = [];\n\tvertices.forEach(v => { verticesParents[v] = []; });\n\tdirectedEdges.forEach(edge => { verticesParents[edge[1]].push(edge[0]); });\n\tconst ordering = [];\n\tconst visited = {};\n\t/** @param {number} vertex */\n\tconst recurse = (vertex) => {\n\t\tif (visited[vertex]) { return; }\n\t\tvisited[vertex] = true;\n\t\tverticesParents[vertex].forEach(recurse);\n\t\tordering.push(vertex);\n\t};\n\tvertices.forEach(recurse);\n\treturn ordering;\n};\n\n/**\n * @description Perform a topological sort on a directed acyclic graph.\n * If a cycle exists, this method will return undefined.\n * @param {[number, number][]} directedEdges an array of directed edges\n * where each edge contains two vertex indices with the direction going\n * from index 0 towards 1\n * @returns {number[]|undefined} an ordering of the vertices\n * from the edges provided, or undefined if a cycle is detected.\n */\nexport const topologicalSort = (directedEdges) => {\n\tconst ordering = topologicalSortQuick(directedEdges);\n\tconst orderMap = invertFlatMap(ordering);\n\tconst violations = directedEdges\n\t\t.filter(([a, b]) => orderMap[a] > orderMap[b]);\n\treturn violations.length ? undefined : ordering;\n};\n"
  },
  {
    "path": "src/graph/disjoint.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfilterKeysWithPrefix,\n} from \"../fold/spec.js\";\nimport {\n\tuniqueElements,\n} from \"../general/array.js\";\nimport {\n\tconnectedComponents,\n} from \"./connectedComponents.js\";\nimport {\n\tmakeVerticesVerticesUnsorted,\n} from \"./make/verticesVertices.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"./make/verticesEdges.js\";\nimport {\n\tmakeVerticesFacesUnsorted,\n} from \"./make/verticesFaces.js\";\nimport {\n\tinvertFlatToArrayMap,\n} from \"./maps.js\";\n\n/**\n * @description In the case that your graph is a disjoint union\n * of graphs, return an array of all graphs separated, where each\n * graph's components maintain their indices, the component arrays\n * will have holes.\n * @param {FOLD} graph a FOLD object\n * @returns {{ vertices: number[], edges: number[], faces: number[] }[]}\n * an array of objects where each element represents a disjoint graph, and\n * includes all vertices, edges, and faces indices of this graph.\n */\nexport const disjointGraphsIndices = (graph) => {\n\tconst edges_vertices = graph.edges_vertices || [];\n\tconst faces_vertices = graph.faces_vertices || [];\n\tconst vertices_edges = graph.vertices_edges\n\t\t? graph.vertices_edges\n\t\t: makeVerticesEdgesUnsorted({ edges_vertices });\n\tconst vertices_vertices = graph.vertices_vertices\n\t\t? graph.vertices_vertices\n\t\t: makeVerticesVerticesUnsorted({ vertices_edges, edges_vertices });\n\tconst vertices_faces = graph.vertices_faces\n\t\t? graph.vertices_faces\n\t\t: makeVerticesFacesUnsorted({ vertices_edges, faces_vertices });\n\n\t// everything needs to come from just the one array, whether it's\n\t// vertices_vertices or faces_faces or whatever, we need to pick just one.\n\tconst vertices = invertFlatToArrayMap(connectedComponents(vertices_vertices));\n\tconst edges = vertices\n\t\t.map(verts => verts.flatMap(v => vertices_edges[v]))\n\t\t.map(uniqueElements);\n\tconst faces = vertices\n\t\t.map(verts => verts.flatMap(v => vertices_faces[v]))\n\t\t.map(uniqueElements);\n\treturn Array.from(Array(vertices.length)).map((_, i) => ({\n\t\tvertices: vertices[i] || [],\n\t\tedges: edges[i] || [],\n\t\tfaces: faces[i] || [],\n\t}));\n};\n\n/**\n * @description In the case that your graph is a disjoint union\n * of graphs, return an array of all graphs separated, where each\n * graph's components maintain their indices, the component arrays\n * will have holes.\n * @param {FOLD} graph a FOLD object\n * @returns {FOLD[]} an array of graphs where each graph is a disjoint subgraph\n * of the input, and the component array indices are not modified, so the\n * arrays have holes, but the indices are still relevant to the input graph.\n */\nexport const disjointGraphs = (graph) => {\n\t// get our disjoint graphs as lists of vertices, edges, and faces indices.\n\tconst graphs = disjointGraphsIndices(graph);\n\n\t// get all relevant geometry keys from the graphs. any additional metadata\n\t// keys/values will not be carried over into the subgraphs.\n\tconst verticesKeys = filterKeysWithPrefix(graph, \"vertices\");\n\tconst edgesKeys = filterKeysWithPrefix(graph, \"edges\");\n\tconst facesKeys = filterKeysWithPrefix(graph, \"faces\");\n\n\t// iterate through all graphs, create shallow copies of the vertices, edges,\n\t// and faces arrays but only include those indices which are a member of\n\t// the disjoint graph. we won't need to do any filtering inside each component\n\t// value array because we already established that graphs are disjoint,\n\t// and will not contain any spurious references in their value arrays\n\treturn graphs.map(({ vertices, edges, faces }) => {\n\t\tconst subgraph = {};\n\t\tverticesKeys.forEach(key => {\n\t\t\tsubgraph[key] = [];\n\t\t\tvertices.forEach(v => { subgraph[key][v] = graph[key][v]; });\n\t\t});\n\t\tedgesKeys.forEach(key => {\n\t\t\tsubgraph[key] = [];\n\t\t\tedges.forEach(v => { subgraph[key][v] = graph[key][v]; });\n\t\t});\n\t\tfacesKeys.forEach(key => {\n\t\t\tsubgraph[key] = [];\n\t\t\tfaces.forEach(v => { subgraph[key][v] = graph[key][v]; });\n\t\t});\n\t\treturn subgraph;\n\t});\n};\n"
  },
  {
    "path": "src/graph/edges/circular.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { filterKeysWithSuffix } from \"../../fold/spec.js\";\nimport { remove } from \"../remove.js\";\n\n/**\n * @description Get the indices of all circular edges. Circular edges are\n * edges where both of its edges_vertices is the same vertex.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} array of indices of circular edges. empty if none.\n */\nexport const circularEdges = ({ edges_vertices = [] }) => edges_vertices\n\t.map((vertices, i) => (vertices[0] === vertices[1] ? i : undefined))\n\t.filter(a => a !== undefined);\n\n/**\n * @description Given a set of graph geometry (vertices/edges/faces) indices,\n * get all the arrays which reference these geometries, (eg: end in _edges),\n * and remove (splice) that entry from the array if it contains a remove value.\n * @param {FOLD} graph a FOLD object\n * @param {string} suffix a component intended as a suffix,\n * like \"vertices\" for \"edges_vertices\"\n * @example\n * removing indices [4, 7] from \"edges\", then a faces_edges entry\n * which was [15, 13, 4, 9, 2] will become [15, 13, 9, 2].\n */\nconst spliceRemoveValuesFromSuffixes = (graph, suffix, remove_indices) => {\n\tconst remove_map = {};\n\tremove_indices.forEach(n => { remove_map[n] = true; });\n\tfilterKeysWithSuffix(graph, suffix)\n\t\t.forEach(sKey => graph[sKey] // faces_edges or vertices_edges...\n\t\t\t.forEach((elem, i) => { // faces_edges[0], faces_edges[1], ...\n\t\t\t\t// reverse iterate through array, remove elements with splice\n\t\t\t\tfor (let j = elem.length - 1; j >= 0; j -= 1) {\n\t\t\t\t\tif (remove_map[elem[j]] === true) {\n\t\t\t\t\t\tgraph[sKey][i].splice(j, 1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}));\n};\n\n/**\n * @description Find and remove all circular edges from a graph.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [remove_indices=undefined] Leave this empty. Otherwise, if\n * circularEdges() has already been called, provide the result here to speed\n * up the algorithm.\n * @returns {{ remove: number[], map: number[] }} a summary of changes where\n * \"remove\" contains all indices which were removed\n * \"map\" is a nextmap of changes to the list of edges\n */\nexport const removeCircularEdges = (graph, remove_indices) => {\n\tif (!remove_indices) {\n\t\tremove_indices = circularEdges(graph);\n\t}\n\tif (remove_indices.length) {\n\t\t// remove every instance of a circular edge in every _edge array.\n\t\t// assumption is we can simply remove them because a face that includes\n\t\t// a circular edge is still the same face when you just remove the edge\n\t\tspliceRemoveValuesFromSuffixes(graph, \"edges\", remove_indices);\n\t\t// console.warn(\"circular edge found. please rebuild\");\n\t\t// todo: figure out which arrays need to be rebuilt, if it exists.\n\t}\n\treturn {\n\t\tmap: remove(graph, \"edges\", remove_indices),\n\t\tremove: remove_indices,\n\t};\n};\n"
  },
  {
    "path": "src/graph/edges/duplicate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n\tmakeVerticesEdges,\n} from \"../make/verticesEdges.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"../make/verticesVertices.js\";\nimport {\n\tmakeVerticesFaces,\n} from \"../make/verticesFaces.js\";\nimport {\n\treplace,\n} from \"../replace.js\";\nimport {\n\tinvertArrayToFlatMap,\n} from \"../maps.js\";\nimport {\n\tclusterUnsortedIndices,\n} from \"../../general/cluster.js\";\nimport {\n\tgetVerticesClusters,\n} from \"../vertices/clusters.js\";\nimport {\n\tsweepEdges,\n} from \"../sweep.js\";\n\n/**\n * @description Get the indices of all duplicate edges by marking the\n * second/third/... as duplicate (not the first of the duplicates).\n * The result is given as an array with holes, where:\n * - the indices are the indices of the duplicate edges.\n * - the values are the indices of the first occurence of the duplicate.\n * Under this system, many edges can be duplicates of the same edge.\n * Order is not important. [5,9] and [9,5] are still duplicate.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} an array where the redundant edges are the indices,\n * and the values are the indices of the first occurence of the duplicate.\n * @example\n * {number[]} array, [4:3, 7:5, 8:3, 12:3, 14:9] where indices\n * (3, 4, 8, 12) are all duplicates. (5,7), (9,14) are also duplicates.\n */\nexport const duplicateEdges = ({ edges_vertices }) => {\n\tif (!edges_vertices) { return []; }\n\n\tconst hash = {};\n\tconst duplicates = [];\n\n\t// convert edges to space-separated vertex pair strings (\"a b\" where a < b)\n\t// store them into the hash table with a value of the current edge index,\n\t// unless a match already exists, then add an entry into duplicates where\n\t// the index is the edge index, and the value is the matching edge vertex.\n\tedges_vertices\n\t\t.map(verts => (verts[0] < verts[1] ? verts : verts.slice().reverse()))\n\t\t.map(pair => pair.join(\" \"))\n\t\t.forEach((key, e) => {\n\t\t\tif (hash[key] !== undefined) {\n\t\t\t\tduplicates[e] = hash[key];\n\t\t\t} else {\n\t\t\t\t// only update the hash once, at the first appearance of these vertices,\n\t\t\t\t// this prevents chains of duplicate relationships where\n\t\t\t\t// A points to B points to C points to D...\n\t\t\t\thash[key] = e;\n\t\t\t}\n\t\t});\n\n\treturn duplicates;\n};\n\n/**\n * @description Get similar edges, where \"similarity\" is defined geometrically,\n * two similar edges have both of their vertices in the same place, order\n * of vertices does not matter.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} clusters_edges, the result is an array of clusters\n * where each cluster contains a list of edge indices.\n */\nexport const getSimilarEdges = (\n\t{ vertices_coords, vertices_edges, edges_vertices },\n\tepsilon = EPSILON,\n) => {\n\tconst clusters_vertices = getVerticesClusters({ vertices_coords }, epsilon);\n\tconst vertices_cluster = invertArrayToFlatMap(clusters_vertices);\n\n\t/**\n\t * @param {number} a edge index\n\t * @param {number} b edge index\n\t */\n\tconst comparison = (a, b) => {\n\t\tconst [a0, a1] = edges_vertices[a].map(v => vertices_cluster[v]);\n\t\tconst [b0, b1] = edges_vertices[b].map(v => vertices_cluster[v]);\n\t\treturn (a0 === b0 && a1 === b1) || (a0 === b1 && a1 === b0);\n\t};\n\n\tconst edgeSweep = sweepEdges({\n\t\tvertices_coords, vertices_edges, edges_vertices,\n\t});\n\n\treturn edgeSweep\n\t\t.map(({ start }) => start)\n\t\t.flatMap(edges => clusterUnsortedIndices(edges, comparison));\n};\n\n/**\n * @description Find and remove all duplicate edges from a graph.\n * If an edge is removed, it will mess up the vertices data (vertices_vertices,\n * vertices_edges, vertices_faces) so if this method successfully found and\n * removed a duplicate edge, the vertices arrays will be rebuilt as well.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [replace_indices=undefined] Leave this empty. Otherwise, if\n * duplicateEdges() has already been called, provide the result here to speed\n * up the algorithm.\n * @returns {{ remove: number[], map: number[] }} a summary of changes where\n * \"remove\" contains all indices which were removed\n * \"map\" is a nextmap of changes to the list of edges\n */\nexport const removeDuplicateEdges = (graph, replace_indices) => {\n\t// index: edge to remove, value: the edge which should replace it.\n\tif (!replace_indices) {\n\t\treplace_indices = duplicateEdges(graph);\n\t}\n\tconst removeObject = Object.keys(replace_indices).map(n => parseInt(n, 10));\n\tconst map = replace(graph, \"edges\", replace_indices);\n\n\t// if edges were removed, we need to rebuild vertices_edges and then\n\t// vertices_vertices since that was built from vertices_edges, and then\n\t// vertices_faces since that was built from vertices_vertices.\n\tif (removeObject.length) {\n\t\t// currently we are rebuilding the entire arrays, if possible,\n\t\t// update these specific vertices directly:\n\t\t// const vertices = removeObject\n\t\t//   .map(edge => graph.edges_vertices[edge])\n\t\t//   .reduce((a, b) => a.concat(b), []);\n\t\tif (graph.vertices_edges || graph.vertices_vertices || graph.vertices_faces) {\n\t\t\tgraph.vertices_edges = makeVerticesEdgesUnsorted(graph);\n\t\t\tgraph.vertices_vertices = makeVerticesVertices(graph);\n\t\t\tgraph.vertices_edges = makeVerticesEdges(graph);\n\t\t\tgraph.vertices_faces = makeVerticesFaces(graph);\n\t\t}\n\t}\n\treturn { map, remove: removeObject };\n};\n"
  },
  {
    "path": "src/graph/edges/lines.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tepsilonEqualVectors,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine,\n\tpointsToLine2,\n\tpointsToLine3,\n} from \"../../math/convert.js\";\nimport {\n\tmagnitude,\n\tsubtract,\n\tsubtract2,\n\tresize2,\n\tresize3,\n\tdot,\n} from \"../../math/vector.js\";\nimport {\n\tclampLine,\n\tcollinearLines3,\n} from \"../../math/line.js\";\nimport {\n\tprojectPointOnPlane,\n} from \"../../math/plane.js\";\nimport {\n\tnearestPointOnLine,\n} from \"../../math/nearest.js\";\nimport {\n\tuniqueElements,\n\tarrayMinimumIndex,\n\tarrayMaximumIndex,\n} from \"../../general/array.js\";\nimport {\n\tclusterScalars,\n\tclusterSortedGeneric,\n\tclusterParallelVectors,\n} from \"../../general/cluster.js\";\nimport {\n\tradialSortVectors3,\n} from \"../../general/sort.js\";\nimport {\n\tgetDimensionQuick,\n} from \"../../fold/spec.js\";\nimport {\n\tconnectedComponents,\n} from \"../connectedComponents.js\";\nimport {\n\tinvertArrayToFlatMap,\n\tinvertFlatToArrayMap,\n} from \"../maps.js\";\n\n/**\n * @description convert an edge to a vector-origin line.\n * @param {FOLD} graph a FOLD object\n * @param {number} edge the index of the edge\n * @returns {VecLine2} a line form of the edge\n */\nexport const edgeToLine2 = ({ vertices_coords, edges_vertices }, edge) => {\n\tconst [a, b] = edges_vertices[edge].map(v => vertices_coords[v])\n\treturn ({ vector: subtract2(b, a), origin: resize2(a) });\n};\n\n/**\n * @description convert an edge to a vector-origin line.\n * @param {FOLD} graph a FOLD object\n * @param {number} edge the index of the edge\n * @returns {VecLine} a line form of the edge\n */\n// export const edgeToLine = ({ vertices_coords, edges_vertices }, edge) => (\n// \tpointsToLine(\n// \t\tvertices_coords[edges_vertices[edge][0]],\n// \t\tvertices_coords[edges_vertices[edge][1]],\n// \t));\n\n/**\n * @description convert the edges of a graph into vector-origin line form.\n * @param {FOLD} graph a FOLD object\n * @returns {VecLine[]} a line for every edge\n */\nexport const edgesToLines = ({ vertices_coords, edges_vertices }) => (\n\tedges_vertices\n\t\t.map(ev => [vertices_coords[ev[0]], vertices_coords[ev[1]]])\n\t\t.map(([a, b]) => pointsToLine(a, b))\n);\n\n/**\n * @description convert an edge to a vector-origin line.\n * @param {FOLD} graph a FOLD object\n * @returns {VecLine2[]} a line form of the edge\n */\nexport const edgesToLines2 = ({ vertices_coords, edges_vertices }) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\treturn edges_vertices\n\t\t.map(ev => [vertices_coords2[ev[0]], vertices_coords2[ev[1]]])\n\t\t.map(([a, b]) => pointsToLine2(a, b))\n};\n\n/**\n * @description convert an edge to a vector-origin line.\n * @param {FOLD} graph a FOLD object\n * @returns {VecLine3[]} a line form of the edge\n */\nexport const edgesToLines3 = ({ vertices_coords, edges_vertices }) => {\n\tconst vertices_coords3 = vertices_coords.map(resize3);\n\treturn edges_vertices\n\t\t.map(ev => [vertices_coords3[ev[0]], vertices_coords3[ev[1]]])\n\t\t.map(([a, b]) => pointsToLine3(a, b))\n};\n\n/**\n * @description Most origami models have many edges which lie along\n * the same infinite line. This method finds all lines which cover all edges,\n * returning a list of lines, and a mapping of each edge to each line.\n * @param {FOLD} graph a FOLD object, can be 2D or 3D.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ lines: VecLine[], edges_line: number[] }}\n */\nexport const getEdgesLine = (\n\t{ vertices_coords, edges_vertices },\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_coords || !edges_vertices || !edges_vertices.length) {\n\t\treturn { edges_line: [], lines: [] };\n\t}\n\n\t// a vector-origin line representation of every edge. we will apply\n\t// clustering operations to this list to group edges with similar lines.\n\tconst edgesLine = edgesToLines3({ vertices_coords, edges_vertices });\n\t// const edgesLine = edgesToLines({ vertices_coords, edges_vertices });\n\n\t// this is the distance from the origin to the nearest point along the line\n\t// no epsilon is needed in nearestPointOnLine, because there is no clamp.\n\tconst edgesOriginDistances = edgesLine\n\t\t.map(line => nearestPointOnLine(line, [0, 0, 0], clampLine))\n\t\t.map(point => magnitude(point));\n\n\t// begin clustering, we will cluster into 3 parts:\n\t// 1. cluster lines with similar distance-to-origin scalars.\n\t// 2. sub-cluster those which have parallel-vectors.\n\t// 3. sub-cluster those which are actually collinear, because\n\t//    a similar distance/vector can still be rotationally anywhere\n\t//    around the origin in 3D, or on opposite sides of the origin in 2D.\n\t// cluster edge indices based on a shared distance-to-origin\n\tconst distanceClusters = clusterScalars(edgesOriginDistances, epsilon);\n\n\t// further subcluster the previous clusters based on whether the\n\t// line's vectors are parallel (these inner clusters share the same line)\n\t// we use a fixed epsilon here, the comparison is testing parallel-ness\n\t// with dot(v, u) with normalized vectors. so anything similar within\n\t// 1e-3 should suffice. We can't feed in the user epsilon, because an\n\t// epsilon of something like 5 would be meaningless here.\n\tconst parallelDistanceClusters = distanceClusters\n\t\t.map(cluster => cluster.map(i => edgesLine[i].vector))\n\t\t.map(cluster => clusterParallelVectors(cluster, 1e-3))\n\t\t.map((clusters, i) => clusters\n\t\t\t.map(cluster => cluster\n\t\t\t\t.map(index => distanceClusters[i][index])));\n\n\t// one final time, cluster each subcluster once more. because we only\n\t// measured the distance to the origin, and the vector, we could be on equal\n\t// but opposite sides of the origin (unless it passes through the origin).\n\tconst collinearParallelDistanceClusters = parallelDistanceClusters\n\t\t.map(clusters => clusters.map(cluster => {\n\t\t\t// values in \"cluster\" are edge indices.\n\t\t\t// if the cluster passes through the origin, all edges are collinear.\n\t\t\tif (Math.abs(edgesOriginDistances[cluster[0]]) < epsilon) {\n\t\t\t\treturn [cluster];\n\t\t\t}\n\n\t\t\t// establish a shared vector for all lines in the cluster\n\t\t\tconst clusterVector = edgesLine[cluster[0]].vector;\n\n\t\t\t// edges run orthogonal to the plane and will be\n\t\t\t// collapsed into one coplanar point\n\t\t\tconst clusterPoints = cluster\n\t\t\t\t.map(e => vertices_coords[edges_vertices[e][0]])\n\t\t\t\t.map(point => projectPointOnPlane(point, clusterVector));\n\n\t\t\t// these points are all the same distance away from the origin,\n\t\t\t// radially sort them around the origin, using the line's vector\n\t\t\t// as the plane's normal.\n\t\t\tconst sortedIndices = radialSortVectors3(clusterPoints, clusterVector);\n\n\t\t\t// values in \"sortedIndices\" now relate to indices of \"cluster\"\n\t\t\t// this comparison function will be used if two or more points satisfy\n\t\t\t// both #1 and #2 conditions, and need to be radially sorted in their plane.\n\t\t\t/** @param {number} i @param {number} j @returns {boolean} */\n\t\t\tconst compareFn = (i, j) => (\n\t\t\t\tepsilonEqualVectors(clusterPoints[i], clusterPoints[j], epsilon)\n\t\t\t);\n\n\t\t\t// indices are multi-layered related to indices of other arrays.\n\t\t\t// when all is done, this maps back to the original edge indices.\n\t\t\t/** @param {number[]} cl */\n\t\t\tconst remap = cl => cl.map(i => sortedIndices[i]).map(i => cluster[i]);\n\n\t\t\t// now that the list is sorted, cluster any neighboring points\n\t\t\t// that are within an epsilon distance away from each other.\n\t\t\tconst clusterResult = clusterSortedGeneric(sortedIndices, compareFn);\n\n\t\t\t// values in \"clusterResult\" now relate to indices of \"sortedIndices\"\n\t\t\t// one special case, since these are radially sorted, if the\n\t\t\t// first and last cluster are equivalent, merge them together\n\t\t\tif (clusterResult.length === 1) { return clusterResult.map(remap); }\n\n\t\t\t// get the first from cluster[0] and the last from cluster[n - 1]\n\t\t\tconst firstFirst = clusterResult[0][0];\n\t\t\tconst last = clusterResult[clusterResult.length - 1];\n\t\t\tconst lastLast = last[last.length - 1];\n\n\t\t\t// map these back to relate to indices of \"cluster\".\n\t\t\tconst endIndices = [firstFirst, lastLast].map(i => sortedIndices[i]);\n\n\t\t\t// if two points from either end clusters are similar,\n\t\t\t// merge the 0 and n-1 clusters into the 0 index.\n\t\t\tif (compareFn(endIndices[0], endIndices[1])) {\n\t\t\t\tconst lastCluster = clusterResult.pop();\n\t\t\t\tclusterResult[0] = lastCluster.concat(clusterResult[0]);\n\t\t\t}\n\t\t\treturn clusterResult.map(remap);\n\t\t}));\n\n\t// now we have all edges clustered according to which line they lie along.\n\t// here are the clusters, all edges inside of each cluster.\n\tconst lines_edges = collinearParallelDistanceClusters\n\t\t.flatMap(clusterOfClusters => clusterOfClusters\n\t\t\t.flatMap(clusters => clusters));\n\tconst edges_line = invertArrayToFlatMap(lines_edges);\n\n\t// get the most precise form of a line possible.\n\t// the fastest way to do this is, for every segment, build a vector\n\t// from the furthest two points possible.\n\t// so, step 1, for each line/cluster, get a list of all vertices involved.\n\tconst lines_vertices = lines_edges\n\t\t.map(edges => edges.flatMap(e => edges_vertices[e]))\n\t\t.map(uniqueElements);\n\n\t// for each line/cluster, find the two vertices furthest on either end.\n\t// use one vector from the line, it doesn't matter which one.\n\tconst lines_firstVector = lines_edges.map(edges => edgesLine[edges[0]].vector);\n\n\t// project each vertex onto the line, get the dot product\n\t// find the minimum and maximum vertices along the line's vector.\n\tconst lines_vertProjects = lines_vertices\n\t\t.map((vertices, i) => vertices\n\t\t\t.map(v => dot(vertices_coords[v], lines_firstVector[i])));\n\tconst lines_vertProjectsMin = lines_vertProjects\n\t\t.map((projections, i) => lines_vertices[i][arrayMinimumIndex(projections)]);\n\tconst lines_vertProjectsMax = lines_vertProjects\n\t\t.map((projections, i) => lines_vertices[i][arrayMaximumIndex(projections)]);\n\n\t// for each line/cluster, create a vector from the furthest two vertices\n\tconst lines_vector = lines_vertices.map((_, i) => subtract(\n\t\tvertices_coords[lines_vertProjectsMax[i]],\n\t\tvertices_coords[lines_vertProjectsMin[i]],\n\t));\n\n\t// the \"line\" result will have both vector and origin matching\n\t// in dimensions with the input graph's vertices_coords\n\tconst lines_vectorN = getDimensionQuick({ vertices_coords }) === 2\n\t\t? lines_vector.map(resize2)\n\t\t: lines_vector.map(resize3);\n\n\t// for each line's origin, we want to use an existing vertex.\n\tconst lines_origin = lines_vertProjectsMin.map(v => vertices_coords[v]);\n\tconst lines = lines_vectorN\n\t\t.map((vector, i) => ({ vector, origin: lines_origin[i] }));\n\treturn {\n\t\tlines,\n\t\tedges_line,\n\t};\n};\n\n/**\n * @description edge endpoints are exclusive\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\n// export const getOverlappingCollinearEdges = ({\n// \tvertices_coords, edges_vertices,\n// }, epsilon = EPSILON) => {\n// \tconst {\n// \t\tlines,\n// \t\tedges_line,\n// \t} = getEdgesLine({ vertices_coords, edges_vertices }, epsilon);\n\n// \t// we're going to project each edge onto the shared line, we can't use\n// \t// each edge's vector, we have to use the edge's line's common vector.\n// \tconst edges_range = makeEdgesCoords({ vertices_coords, edges_vertices })\n// \t\t.map((points, e) => points\n// \t\t\t.map(point => dot(lines[edges_line[e]].vector, point)));\n\n// \t// the invertFlatToArrayMap creates lines_edges, each a list of edge indices.\n// \t// each time we call clusterRanges, the result is a list of indices 0...n,\n// \t// where the indices relate to the input array \"edges\", all we have to do is\n// \t// remap these indices back to their actual edge index inside \"edges\".\n// \treturn invertFlatToArrayMap(edges_line)\n// \t\t.flatMap(edges => clusterRanges(edges.map(e => edges_range[e]), epsilon)\n// \t\t\t.map(indices => indices.map(i => edges[i])));\n// };\n\n\n\n/**\n * @description This extends getEdgesLine and also returns information about\n * clusters of overlapping edges within each line group. Each cluster is\n * defined as a group of edges where every edge overlaps at least one other\n * edge from the group. This does not define a list of, for every edge,\n * a list of edges which overlap this edge.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   lines: VecLine[],\n *   edges_line: number[],\n *   clusters_edges: number[][],\n *   edges_cluster: number[],\n * }}\n */\n// export const getCollinearOverlappingEdgeClusters = ({\n// \tvertices_coords, edges_vertices,\n// }, epsilon = EPSILON) => {\n// \tconst {\n// \t\tlines,\n// \t\tedges_line,\n// \t} = getEdgesLine({ vertices_coords, edges_vertices }, epsilon);\n\n// \t// we're going to project each edge onto the shared line, we can't use\n// \t// each edge's vector, we have to use the edge's line's common vector.\n// \tconst edges_range = makeEdgesCoords({ vertices_coords, edges_vertices })\n// \t\t.map((points, e) => points\n// \t\t\t.map(point => dot(lines[edges_line[e]].vector, point)));\n\n// \t// the invertFlatToArrayMap creates lines_edges, each a list of edge indices.\n// \t// each time we call clusterRanges, the result is a list of indices 0...n,\n// \t// where the indices relate to the input array \"edges\", all we have to do is\n// \t// remap these indices back to their actual edge index inside \"edges\".\n// \tconst clusters_edges = invertFlatToArrayMap(edges_line)\n// \t\t.flatMap(edges => clusterRanges(edges.map(e => edges_range[e]), epsilon)\n// \t\t\t.map(indices => indices.map(i => edges[i])));\n// \tconst edges_cluster = invertArrayToFlatMap(clusters_edges);\n// \treturn {\n// \t\tlines,\n// \t\tedges_line,\n// \t\tclusters_edges,\n// \t\tedges_cluster,\n// \t};\n// };\n\n/**\n * @description Most origami models have many edges which lie along\n * the same infinite line. This method finds all lines which cover all edges,\n * returning a list of lines, and a mapping of each edge to each line.\n * @param {FOLD} graph a FOLD object, can be 2D or 3D.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ lines: VecLine[], edges_line: number[] }}\n */\nexport const getEdgesLineBruteForce = (\n\t{ vertices_coords, edges_vertices },\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_coords || !edges_vertices || !edges_vertices.length) {\n\t\treturn { edges_line: [], lines: [] };\n\t}\n\n\t// a vector-origin line representation of every edge. we will apply\n\t// clustering operations to this list to group edges with similar lines.\n\t// const edgesLine = edgesToLines3({ vertices_coords, edges_vertices });\n\tconst edgesLine = edgesToLines3({ vertices_coords, edges_vertices });\n\n\tconst lines_lines = edgesLine.map(() => []);\n\n\tedgesLine.forEach((line0, e0) => edgesLine.forEach((line1, e1) => {\n\t\tif (e1 <= e0) { return; }\n\t\tif (collinearLines3(line0, line1, epsilon)) {\n\t\t\tlines_lines[e0].push(e1);\n\t\t\tlines_lines[e1].push(e0);\n\t\t}\n\t}));\n\n\tconst edges_line = connectedComponents(lines_lines);\n\tconst lines_edges = invertFlatToArrayMap(edges_line);\n\n\t// get the most precise form of a line possible.\n\t// the fastest way to do this is, for every segment, build a vector\n\t// from the furthest two points possible.\n\t// so, step 1, for each line/cluster, get a list of all vertices involved.\n\tconst lines_vertices = lines_edges\n\t\t.map(edges => edges.flatMap(e => edges_vertices[e]))\n\t\t.map(uniqueElements);\n\n\t// for each line/cluster, find the two vertices furthest on either end.\n\t// use one vector from the line, it doesn't matter which one.\n\tconst lines_firstVector = lines_edges.map(edges => edgesLine[edges[0]].vector);\n\n\t// project each vertex onto the line, get the dot product\n\t// find the minimum and maximum vertices along the line's vector.\n\tconst lines_vertProjects = lines_vertices\n\t\t.map((vertices, i) => vertices\n\t\t\t.map(v => dot(vertices_coords[v], lines_firstVector[i])));\n\tconst lines_vertProjectsMin = lines_vertProjects\n\t\t.map((projections, i) => lines_vertices[i][arrayMinimumIndex(projections)]);\n\tconst lines_vertProjectsMax = lines_vertProjects\n\t\t.map((projections, i) => lines_vertices[i][arrayMaximumIndex(projections)]);\n\n\t// for each line/cluster, create a vector from the furthest two vertices\n\tconst lines_vector = lines_vertices.map((_, i) => subtract(\n\t\tvertices_coords[lines_vertProjectsMax[i]],\n\t\tvertices_coords[lines_vertProjectsMin[i]],\n\t));\n\n\t// the \"line\" result will have both vector and origin matching\n\t// in dimensions with the input graph's vertices_coords\n\tconst lines_vectorN = getDimensionQuick({ vertices_coords }) === 2\n\t\t? lines_vector.map(resize2)\n\t\t: lines_vector.map(resize3);\n\n\t// for each line's origin, we want to use an existing vertex.\n\tconst lines_origin = lines_vertProjectsMin.map(v => vertices_coords[v]);\n\tconst lines = lines_vectorN\n\t\t.map((vector, i) => ({ vector, origin: lines_origin[i] }));\n\treturn {\n\t\tlines,\n\t\tedges_line,\n\t};\n};\n"
  },
  {
    "path": "src/graph/edges/overlap.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tepsilonEqual,\n\texcludeS,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../../math/convert.js\";\nimport {\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tdoRangesOverlap,\n} from \"../../math/range.js\";\nimport {\n\tintersectLineLine,\n} from \"../../math/intersect.js\";\nimport {\n\tsweepEdges,\n} from \"../sweep.js\";\n\n/**\n * @param {FOLD} graph a FOLD object\n */\nconst edgesSpanY2D = (\n\t{ vertices_coords, edges_vertices },\n\tpad = 0,\n) => (edges_vertices\n\t.map(verts => verts.map(v => vertices_coords[v]))\n\t.map(([a, b]) => [Math.min(a[1], b[1]) - pad, Math.max(a[1], b[1]) + pad]))\n\t.map(range => (epsilonEqual(range[0], range[1], Math.abs(pad * 2.5))\n\t\t? undefined\n\t\t: range));\n\n/**\n * @description\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean}\n */\nexport const doEdgesOverlap = (\n\t{ vertices_coords, edges_vertices },\n\tepsilon = EPSILON,\n) => {\n\tconst edgesYSpan = edgesSpanY2D({ vertices_coords, edges_vertices }, -epsilon);\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst edges_line = edges_vertices\n\t\t.map(ev => [vertices_coords2[ev[0]], vertices_coords2[ev[1]]])\n\t\t.map(([a, b]) => pointsToLine2(a, b))\n\n\t// currently overlapped edges\n\tconst stack = [];\n\tconst sweep = sweepEdges({ vertices_coords, edges_vertices });\n\ttry {\n\t\tsweep.forEach(({ start, end }) => {\n\t\t\tstart.forEach(e => { stack[e] = true; });\n\t\t\tObject.keys(stack).map(n => parseInt(n, 10)).forEach(e0 => start.forEach(e1 => {\n\t\t\t\tif (e0 === e1) { return; }\n\t\t\t\tif (edgesYSpan[e0]\n\t\t\t\t\t&& edgesYSpan[e1]\n\t\t\t\t\t&& !doRangesOverlap(edgesYSpan[e0], edgesYSpan[e1])) { return; }\n\t\t\t\tif (intersectLineLine(\n\t\t\t\t\tedges_line[e0],\n\t\t\t\t\tedges_line[e1],\n\t\t\t\t\texcludeS,\n\t\t\t\t\texcludeS,\n\t\t\t\t\tepsilon,\n\t\t\t\t).point) {\n\t\t\t\t\tthrow new Error();\n\t\t\t\t}\n\t\t\t}));\n\t\t\tend.forEach(e => { delete stack[e]; });\n\t\t});\n\t} catch (error) {\n\t\treturn true;\n\t}\n\treturn false;\n};\n"
  },
  {
    "path": "src/graph/epsilon.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tboundingBox,\n} from \"./boundary.js\";\nimport {\n\tdistance,\n} from \"../math/vector.js\";\n\n/**\n * @description We ignore any segment lengths which are smaller than 1e-4,\n * assuming that these are errors, circular edges, etc...\n * @param {FOLD} graph a FOLD object\n * @returns {number} the length of the shortest edge in the graph\n */\nexport const shortestEdgeLength = ({ vertices_coords, edges_vertices }) => {\n\tconst lengths = edges_vertices\n\t\t.map(ev => ev.map(v => vertices_coords[v]))\n\t\t.map(([a, b]) => distance(a, b))\n\t\t.filter(len => len > 1e-4);\n\tconst minLen = lengths\n\t\t.reduce((a, b) => Math.min(a, b), Infinity);\n\treturn minLen === Infinity ? undefined : minLen;\n};\n\n/**\n * @description Epsilon will be based on a factor of the edge lengths\n * and a factor of the size of the total crease pattern, whichever is\n * smaller\n * @param {FOLD} graph a FOLD object\n * @returns {number} a suitable epsilon\n */\nexport const getEpsilon = ({ vertices_coords, edges_vertices }) => {\n\tconst shortest = shortestEdgeLength({ vertices_coords, edges_vertices });\n\tconst bounds = boundingBox({ vertices_coords });\n\tconst graphSpan = bounds && bounds.span ? Math.max(...bounds.span) : 1;\n\tconst spanScale = graphSpan * 1e-2;\n\tconst edgeScale = shortest / 20;\n\treturn shortest === undefined ? spanScale : Math.min(spanScale, edgeScale);\n};\n\n// currently used by the layer solver, it's so similar to the other method,\n// we need to deprecate this method in favor of the other.\nexport const makeEpsilon = ({ vertices_coords, edges_vertices }) => {\n\tconst shortest = shortestEdgeLength({ vertices_coords, edges_vertices });\n\tif (shortest) { return Math.max(shortest * 1e-4, 1e-10); }\n\tconst bounds = boundingBox({ vertices_coords });\n\treturn bounds && bounds.span\n\t\t? Math.max(1e-6 * Math.max(...bounds.span), 1e-10)\n\t\t: 1e-6;\n};\n"
  },
  {
    "path": "src/graph/explode.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tgetDimensionQuick,\n} from \"../fold/spec.js\";\nimport {\n\tresize2,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tclone,\n} from \"../general/clone.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./make/facesEdges.js\";\n\n/**\n * @description Create a modified graph which contains vertices, edges,\n * and faces, but that for every face, all of its vertices and edges\n * have been duplicated so that faces do not share vertices or edges.\n * Edges are also duplicated so that they do not share vertices.\n * Edge assignments and foldAngles will remain and be correctly re-indexed.\n * Additionally, if you only provide a graph with only vertices_coords and\n * faces_vertices, then a simple face-vertex only graph will be calculated.\n * @param {FOLD} graph a FOLD object. not modified.\n * @returns {FOLD} a new FOLD graph with exploded faces,\n * if no faces exist, the input graph will be returned, because technically,\n * all faces are triangles.\n */\nexport const explodeFaces = ({\n\tvertices_coords, edges_vertices, edges_assignment, edges_foldAngle,\n\tfaces_vertices, faces_edges,\n}) => {\n\tif (!faces_vertices) {\n\t\tif (edges_vertices) {\n\t\t\treturn clone({\n\t\t\t\tvertices_coords, edges_vertices, edges_assignment, edges_foldAngle,\n\t\t\t});\n\t\t}\n\t\treturn vertices_coords ? clone({ vertices_coords }) : {};\n\t}\n\n\tlet f = 0;\n\tconst faces_verticesNew = faces_vertices.map(face => face.map(() => f++));\n\n\t// if vertices exist, add vertices\n\tif (!vertices_coords) {\n\t\treturn { faces_vertices: faces_verticesNew };\n\t}\n\n\t// typescript ensure vertices_coords is in the correct form\n\tconst dimensions = getDimensionQuick({ vertices_coords });\n\tconst vertices_coordsFlat = clone(faces_vertices\n\t\t.flatMap(face => face.map(v => vertices_coords[v])));\n\t/** @type {[number, number][] | [number, number, number][]} */\n\tconst vertices_coordsNew = dimensions === 3\n\t\t? vertices_coordsFlat.map(resize3)\n\t\t: vertices_coordsFlat.map(resize2);\n\n\t// if no edges exist, return the vertex-face graph.\n\tif (!edges_vertices) {\n\t\treturn {\n\t\t\tvertices_coords: vertices_coordsNew,\n\t\t\tfaces_vertices: faces_verticesNew,\n\t\t};\n\t}\n\n\t// get faces_edges from the old graph data\n\tif (!faces_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\n\tlet e = 0;\n\t/** @type {[number, number][]} */\n\tconst edges_verticesNew = faces_edges\n\t\t.flatMap(face => face\n\t\t\t.map((_, i, arr) => (i === arr.length - 1\n\t\t\t\t? [e, (++e - arr.length)]\n\t\t\t\t: [e, (++e)])))\n\t\t.map(([a, b]) => [a, b]);\n\n\tconst result = {\n\t\tvertices_coords: vertices_coordsNew,\n\t\tfaces_vertices: faces_verticesNew,\n\t\tedges_vertices: edges_verticesNew,\n\t};\n\n\tconst edgesMap = faces_edges.flatMap(edges => edges);\n\tif (edges_assignment) {\n\t\tresult.edges_assignment = edgesMap.map(i => edges_assignment[i]);\n\t}\n\tif (edges_foldAngle) {\n\t\tresult.edges_foldAngle = edgesMap.map(i => edges_foldAngle[i]);\n\t}\n\treturn result;\n};\n\n/**\n * @description Create a modified graph which contains new vertices and\n * edges data such that no two edges share vertices (creating copies of\n * existing vertices), and removing all face data.\n * @param {FOLD} graph a FOLD object.\n * @returns {FOLD} a new FOLD graph with shallow pointers to the input graph.\n */\nexport const explodeEdges = ({\n\tvertices_coords, edges_vertices, edges_assignment, edges_foldAngle,\n}) => {\n\t// if no edges exist, return the vertices (if the exist) as they are\n\t// technically exploded (weird yes).\n\tif (!edges_vertices) {\n\t\treturn vertices_coords ? { vertices_coords } : {};\n\t}\n\tlet e = 0;\n\t// duplicate vertices are simply duplicate references, changing\n\t// one will still change the others. we need to deep copy the array\n\tconst result = {\n\t\t/** @type {[number, number][]} */\n\t\tedges_vertices: edges_vertices.map(() => [e++, e++]),\n\t};\n\tif (edges_assignment) { result.edges_assignment = edges_assignment; }\n\tif (edges_foldAngle) { result.edges_foldAngle = edges_foldAngle; }\n\tif (vertices_coords) {\n\t\tresult.vertices_coords = structuredClone(edges_vertices\n\t\t\t.flatMap(edge => edge.map(v => vertices_coords[v])));\n\t}\n\treturn result;\n};\n"
  },
  {
    "path": "src/graph/faces/facePoint.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tinclude,\n\texclude,\n} from \"../../math/compare.js\";\nimport {\n\tadd2,\n\tscale2,\n} from \"../../math/vector.js\";\nimport {\n\toverlapConvexPolygonPoint,\n} from \"../../math/overlap.js\";\n\n/**\n * @description Given a point, get the indices of all faces which this point\n * lies inside. An optional domain function allows you to specify\n * inclusive or exclusive. This method is 2D only.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} point the point to test\n * @param {function} [domainFunction=include], an optional domain function\n * to specificy inclusive or exclusive.\n * @returns {number[]} an array of face indices\n */\nexport const facesContainingPoint = (\n\t{ vertices_coords, faces_vertices = [] },\n\tpoint,\n\t// vector = undefined, // would be nice to include this nudging behavior\n\tdomainFunction = include,\n) => (!vertices_coords\n\t? []\n\t: faces_vertices\n\t\t.map((fv, i) => ({ face: fv.map(v => vertices_coords[v]), i }))\n\t\t.filter(f => overlapConvexPolygonPoint(f.face, point, domainFunction).overlap)\n\t\t.map(el => el.i)\n);\n\n/**\n * @description Given a point, get the index of a face that this point\n * exists within. In the case when the point lies along an edge or\n * vertex, an additional vector parameter can be used to nudge the point\n * by a very tiny amount, then there will be a preference to return the\n * index of the face which encloses the nudged point.\n * This is a 2D-only method, any z-axis data will be ignored.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} point the point to test\n * @param {[number, number]} [vector] an optional vector, used in the case that\n * the point exists along an edge or a vertex.\n * @returns {number|undefined} the index of a face, or undefined\n * if the point does not lie inside any face.\n */\nexport const faceContainingPoint = (\n\t{ vertices_coords, faces_vertices },\n\tpoint,\n\tvector,\n) => {\n\t// The inclusive test includes the boundary around each face when computing\n\t// whether a point is inside a face or not. If this method returns:\n\t// - 0 solutions: then no solutions are possible. exit with no solution.\n\t// - 1 solution: we are done, exit and return this solution.\n\t// - 2 solutions: proceed with the rest of the algorithm.\n\t// this variable contains a list of face indices\n\tconst facesInclusive = facesContainingPoint(\n\t\t{ vertices_coords, faces_vertices },\n\t\tpoint,\n\t\tinclude,\n\t);\n\tswitch (facesInclusive.length) {\n\tcase 0: return undefined;\n\tcase 1: return facesInclusive[0];\n\tdefault: break;\n\t}\n\n\t// from this point on, we will use the optional vector parameter to nudge\n\t// the point and find the best match for a face. if user did not provide\n\t// a vector, we can't do anything, simply return the first face in the list.\n\tif (!vector) { return facesInclusive[0]; }\n\n\t// continue search by nudging point.\n\tconst nudgePoint = add2(point, scale2(vector, 1e-2));\n\tconst polygons = facesInclusive\n\t\t.map(face => faces_vertices[face]\n\t\t\t.map(v => vertices_coords[v]));\n\n\t// filter the list of face indices to include only those which\n\t// the nudged point lies inside of, excluding the area around the boundary.\n\tconst facesExclusive = facesInclusive.filter((_, i) => (\n\t\toverlapConvexPolygonPoint(polygons[i], nudgePoint, exclude).overlap\n\t));\n\n\tswitch (facesExclusive.length) {\n\t// it's possible that the nudge went in the exact same direction as an edge,\n\t// in which case, we have to re-run an inclusive test with the nudged point,\n\t// then return the first face which satisfies the inclusive overlap test.\n\tcase 0: return facesInclusive.find((_, i) => (\n\t\toverlapConvexPolygonPoint(polygons[i], nudgePoint, include).overlap\n\t));\n\n\t// the ideal case, only one face exists underneath the nudged point\n\tcase 1: return facesExclusive[0];\n\n\t// the nudged point lies inside of all these faces, so it shouldn't matter\n\t// this implies that there are faces that overlap each other.\n\tdefault: return facesExclusive[0];\n\t}\n};\n"
  },
  {
    "path": "src/graph/faces/matrix.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeEdgesIsFolded,\n} from \"../../fold/spec.js\";\nimport {\n\tsubtract2,\n\tsubtract3,\n\tresize2,\n\tresize3,\n} from \"../../math/vector.js\";\nimport {\n\tidentity2x3,\n\tmakeMatrix2Reflect,\n\tmultiplyMatrices2,\n} from \"../../math/matrix2.js\";\nimport {\n\tidentity3x4,\n\tmakeMatrix3Rotate,\n\tmultiplyMatrices3,\n} from \"../../math/matrix3.js\";\nimport {\n\tmakeVerticesToEdge,\n} from \"../make/lookup.js\";\nimport {\n\tmakeEdgesFoldAngle,\n} from \"../make/edgesFoldAngle.js\";\nimport {\n\tmakeEdgesAssignmentSimple,\n} from \"../make/edgesAssignment.js\";\nimport {\n\tmakeFacesFaces,\n} from \"../make/facesFaces.js\";\nimport {\n\trotateCircularArray,\n} from \"../../general/array.js\";\nimport {\n\tminimumSpanningTrees,\n} from \"../trees.js\";\n\n/**\n * @description Given two lists of vertices intended to represent adjacent\n * faces, find the shared vertices between the two. This method is strict\n * about maintaining winding order with the first list, and will return\n * the list of vertices as a list of pairs of vertices, where for each pair,\n * these vertices appear next to each other in the face.\n * This ensures that \"vertices in common\" list can be easily remapped to\n * \"edges in common\", via these pairs of vertices.\n * @param {number[]} verticesA a list of vertices comprising a face\n * @param {number[]} verticesB a list of vertices comprising a face\n * @returns {number[][]} a list of pairs of vertices, where each pair are\n * adjacent vertices from the first \"verticesA\", maintaining winding order.\n */\nexport const facesSharedEdgesVertices = (verticesA, verticesB) => {\n\tconst hash = {};\n\tverticesB.forEach(v => { hash[v] = true; });\n\tconst inCommon = verticesA.map(v => (hash[v] ? v : undefined));\n\treturn rotateCircularArray(inCommon, inCommon.indexOf(undefined))\n\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t.filter(pair => pair[0] !== undefined && pair[1] !== undefined);\n};\n\nconst unassigned_angle = { U: true, u: true };\n\n/**\n * @description Create one transformation matrix for every face which\n * represents the transformation of the face AFTER the graph has been folded\n * along all of the creases (this works in 3D too).\n * This traverses a face-adjacency tree (edge-adjacent faces) and\n * recursively applies the affine transform that represents a fold\n * across the edge between the faces. \"flat\" creases are ignored.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [rootFaces=[]] the index of the face that will remain in place\n * @returns {number[][]} for every face, a 3x4 matrix (an array of 12 numbers).\n */\nexport const makeFacesMatrix = (\n\t{\n\t\tvertices_coords, edges_vertices, edges_foldAngle,\n\t\tedges_assignment, faces_vertices, faces_faces,\n\t},\n\trootFaces,\n) => {\n\tif (!edges_assignment && edges_foldAngle) {\n\t\tedges_assignment = makeEdgesAssignmentSimple({ edges_foldAngle });\n\t}\n\tif (!edges_foldAngle) {\n\t\tif (edges_assignment) {\n\t\t\tedges_foldAngle = makeEdgesFoldAngle({ edges_assignment });\n\t\t} else {\n\t\t\t// if no edges_foldAngle data exists, everyone gets identity matrix\n\t\t\tedges_foldAngle = Array(edges_vertices.length).fill(0);\n\t\t}\n\t}\n\tif (!faces_faces) {\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\tconst edge_map = makeVerticesToEdge({ edges_vertices });\n\tconst faces_matrix = faces_vertices.map(() => [...identity3x4]);\n\tminimumSpanningTrees(faces_faces, rootFaces)\n\t\t.forEach(tree => tree\n\t\t\t.slice(1) // remove the first level, it has no parent face\n\t\t\t.forEach(level => level\n\t\t\t\t.forEach((entry) => {\n\t\t\t\t\tconst edge_vertices = facesSharedEdgesVertices(\n\t\t\t\t\t\tfaces_vertices[entry.index],\n\t\t\t\t\t\tfaces_vertices[entry.parent],\n\t\t\t\t\t).shift();\n\t\t\t\t\tconst coords = edge_vertices\n\t\t\t\t\t\t.map(v => vertices_coords[v])\n\t\t\t\t\t\t.map(resize3);\n\t\t\t\t\tconst edgeKey = edge_vertices.join(\" \");\n\t\t\t\t\tconst edge = edge_map[edgeKey];\n\t\t\t\t\t// if the assignment is unassigned, assume it is a flat fold.\n\t\t\t\t\tconst foldAngle = unassigned_angle[edges_assignment[edge]]\n\t\t\t\t\t\t? Math.PI\n\t\t\t\t\t\t: (edges_foldAngle[edge] * Math.PI) / 180;\n\t\t\t\t\tconst local_matrix = makeMatrix3Rotate(\n\t\t\t\t\t\tfoldAngle, // rotation angle\n\t\t\t\t\t\tsubtract3(coords[1], coords[0]), // line-vector\n\t\t\t\t\t\tcoords[0], // line-origin\n\t\t\t\t\t);\n\t\t\t\t\tfaces_matrix[entry.index] = multiplyMatrices3(faces_matrix[entry.parent], local_matrix);\n\t\t\t\t\t// to build the inverse matrix, switch these two parameters\n\t\t\t\t\t// .multiplyMatrices3(local_matrix, faces_matrix[entry.parent]);\n\t\t\t\t})));\n\treturn faces_matrix;\n};\n\n/**\n * @description Create one transformation matrix for every face which\n * represents the transformation of the face AFTER the graph has been folded\n * along all of the creases.\n * This ignores any 3D data, and treats all creases as flat-folded.\n * This will generate a 2D matrix for every face by virtually folding the graph\n * at every edge according to the assignment or foldAngle.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [rootFaces=[]] the index of the face that will remain in place\n * @returns {number[][]} for every face, a 2x3 matrix (an array of 6 numbers).\n */\nexport const makeFacesMatrix2 = (\n\t{\n\t\tvertices_coords, edges_vertices, edges_foldAngle,\n\t\tedges_assignment, faces_vertices, faces_faces,\n\t},\n\trootFaces,\n) => {\n\tif (!edges_foldAngle) {\n\t\tif (edges_assignment) {\n\t\t\tedges_foldAngle = makeEdgesFoldAngle({ edges_assignment });\n\t\t} else {\n\t\t\t// if no edges_foldAngle data exists, everyone gets identity matrix\n\t\t\tedges_foldAngle = Array(edges_vertices.length).fill(0);\n\t\t}\n\t}\n\tif (!faces_faces) {\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\n\t// However, if there is no edges_assignments, and we have to use edges_foldAngle,\n\t// the \"unassigned\" trick will no longer work, only +/- non zero numbers get\n\t// counted as folded edges (true).\n\t// For this reason, treating \"unassigned\" as a folded edge, this method's\n\t// functionality is better considered to be specific to makeFacesMatrix2,\n\t// instead of a generalized method.\n\tconst edges_is_folded = makeEdgesIsFolded({ edges_vertices, edges_foldAngle, edges_assignment });\n\tconst edge_map = makeVerticesToEdge({ edges_vertices });\n\tconst faces_matrix = faces_vertices.map(() => identity2x3);\n\tminimumSpanningTrees(faces_faces, rootFaces)\n\t\t.forEach(tree => tree\n\t\t\t.slice(1) // remove the first level, it has no parent face\n\t\t\t.forEach(level => level\n\t\t\t\t.forEach((entry) => {\n\t\t\t\t\tconst edge_vertices = facesSharedEdgesVertices(\n\t\t\t\t\t\tfaces_vertices[entry.index],\n\t\t\t\t\t\tfaces_vertices[entry.parent],\n\t\t\t\t\t).shift();\n\t\t\t\t\t// const edge_vertices = chooseTwoPairs(arrayIntersection(\n\t\t\t\t\t// \tfaces_vertices[entry.index],\n\t\t\t\t\t// \tfaces_vertices[entry.parent],\n\t\t\t\t\t// )).find(pair => edge_map[pair.join(\" \")] !== undefined)\n\t\t\t\t\tconst coords = edge_vertices.map(v => vertices_coords[v]);\n\t\t\t\t\tconst edgeKey = edge_vertices.join(\" \");\n\t\t\t\t\tconst edge = edge_map[edgeKey];\n\t\t\t\t\tconst reflect_vector = subtract2(coords[1], coords[0]);\n\t\t\t\t\tconst reflect_origin = resize2(coords[0]);\n\t\t\t\t\tconst local_matrix = edges_is_folded[edge]\n\t\t\t\t\t\t? makeMatrix2Reflect(reflect_vector, reflect_origin)\n\t\t\t\t\t\t: identity2x3;\n\t\t\t\t\tfaces_matrix[entry.index] = multiplyMatrices2(faces_matrix[entry.parent], local_matrix);\n\t\t\t\t\t// to build the inverse matrix, switch these two parameters\n\t\t\t\t\t// .multiplyMatrices2(local_matrix, faces_matrix[entry.parent]);\n\t\t\t\t})));\n\treturn faces_matrix;\n};\n"
  },
  {
    "path": "src/graph/faces/planes.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tdot,\n\tdot3,\n\tscale3,\n\tresize2,\n\tresize3,\n\tflip3,\n\tparallelNormalized,\n} from \"../../math/vector.js\";\nimport {\n\tmakePolygonNonCollinear3,\n} from \"../../math/polygon.js\";\nimport {\n\tmultiplyMatrix4Vector3,\n} from \"../../math/matrix4.js\";\nimport {\n\tmatrix4FromQuaternion,\n\tquaternionFromTwoVectors,\n} from \"../../math/quaternion.js\";\nimport {\n\toverlapConvexPolygons,\n} from \"../../math/overlap.js\";\nimport {\n\tclusterScalars,\n} from \"../../general/cluster.js\";\nimport {\n\tconnectedComponents,\n} from \"../connectedComponents.js\";\nimport {\n\tinvertArrayToFlatMap,\n\tinvertFlatToArrayMap,\n} from \"../maps.js\";\nimport {\n\tmakeFacesFaces,\n} from \"../make/facesFaces.js\";\nimport {\n\tmakeFacesNormal,\n} from \"../normals.js\";\nimport {\n\tselfRelationalArraySubset,\n} from \"../subgraph.js\";\n\n/**\n * @description Cluster the faces of a graph into groups of face indices where\n * all faces in the same group lie in the same plane in 3D.\n * Faces in the same plane are not required to overlap, only be coplanar.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   planes: { normal: number[], origin: number[] }[],\n *   planes_faces: number[][],\n *   planes_transform: number[][],\n *   faces_plane: number[],\n *   faces_winding: boolean[],\n * }} an object with:\n * - planes: a list of planes\n * - planes_faces: for every plane, a list of faces within this plane\n * - planes_transform: for every plane, a matrix which transforms the\n *   plane into the 2D XY plane\n * - faces_plane: for every face, which plane is it in\n * - faces_winding: for every face within its plane, is the face's normal\n * aligned with the plane's normal (true) or flipped 180 degrees (false).\n */\nexport const getFacesPlane = (\n\t{ vertices_coords, faces_vertices },\n\tepsilon = EPSILON,\n) => {\n\t// face normals will be always 3D. These vectors are normalized.\n\tconst faces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\n\t// for every face, get the indices of all faces with matching normals\n\tconst facesNormalMatch = faces_vertices.map(() => []);\n\n\t// todo: this is n^2.\n\t// We need to create a linear sorting of 3D vectors. Possibly we can project\n\t// all vectors onto one axis, sort and compare the non-degenerate vectors,\n\t// then gather all degenerate cases and run these at n^2.\n\t// also, the parallel test is non-directional, the sorted array should be\n\t// treated as a wrapping array, compare the start with the end.\n\tfor (let a = 0; a < faces_vertices.length - 1; a += 1) {\n\t\tfor (let b = a + 1; b < faces_vertices.length; b += 1) {\n\t\t\tif (parallelNormalized(faces_normal[a], faces_normal[b], epsilon)) {\n\t\t\t\tfacesNormalMatch[a].push(b);\n\t\t\t\tfacesNormalMatch[b].push(a);\n\t\t\t}\n\t\t}\n\t}\n\n\t// create disjoint sets of faces which all share the same normal\n\tconst facesNormalMatchCluster = connectedComponents(facesNormalMatch);\n\tconst normalClustersFaces = invertFlatToArrayMap(facesNormalMatchCluster);\n\n\t// for each cluster, choose one normal, this normal is now associated with the cluster.\n\tconst normalClustersNormal = normalClustersFaces\n\t\t.map(faces => faces_normal[faces[0]]);\n\n\t// coplanar faces are clustered together even if their normals are flipped.\n\t// for each face, is this face's normal aligned (true) with the cluster's\n\t// normal, or is it 180deg flipped (false).\n\t// \"faces_winding\" can be a flat array as faces are only in one plane.\n\t/** @type {boolean[]} */\n\tconst faces_winding = [];\n\tnormalClustersFaces.forEach((faces, i) => faces.forEach(f => {\n\t\tfaces_winding[f] = dot3(faces_normal[f], normalClustersNormal[i]) > 0;\n\t}));\n\n\t// using each cluster's shared normal, find the plane (dot prod)\n\t// for each face. make facesOneVertex always 3D.\n\tconst facesOneVertex = faces_vertices\n\t\t.map(fv => vertices_coords[fv[0]])\n\t\t.map(resize3);\n\tconst normalClustersFacesDot = normalClustersFaces\n\t\t.map((faces, i) => faces\n\t\t\t.map(f => dot3(normalClustersNormal[i], facesOneVertex[f])));\n\n\t// for every cluster of a shared normal, further divide into clusters where\n\t// each inner cluster contains faces which share the same plane\n\tconst clustersClusters = normalClustersFacesDot\n\t\t.map((dots, i) => clusterScalars(dots)\n\t\t\t.map(cluster => cluster.map(index => normalClustersFaces[i][index])));\n\n\t// flatten the data one level.\n\t// there is no need to return these as nested clusters.\n\t// before flattening, create a matching array of normals.\n\tconst planes_faces = clustersClusters.flat();\n\n\t// use \"resize3\" to create a clone of every vector. no overlapping pointers\n\tconst planes_normal = clustersClusters\n\t\t.flatMap((cluster, i) => cluster\n\t\t\t.map(() => normalClustersNormal[i]))\n\t\t.map(resize3);\n\n\t// the plane's origin will be the point in the plane\n\t// nearest to the world origin.\n\tconst planes_origin = planes_faces\n\t\t.map(faces => faces[0])\n\t\t.map(face => facesOneVertex[face])\n\t\t.map((point, i) => dot3(planes_normal[i], point))\n\t\t.map((mag, i) => scale3(planes_normal[i], mag));\n\n\t// make each plane with the origin and normal\n\tconst planes = planes_faces.map((_, i) => ({\n\t\tnormal: planes_normal[i],\n\t\torigin: planes_origin[i],\n\t}));\n\n\tconst faces_plane = invertArrayToFlatMap(planes_faces);\n\n\t// all polygon sets will be planar to each other, however the polygon-polygon\n\t// intersection algorithm is 2D only, so we just need to create a transform\n\t// for each cluster which rotates this cluster's plane into the XY plane.\n\t/** @type {[number, number, number]} */\n\tconst targetVector = [0, 0, 1];\n\n\t// if dot is -1, this plane is already in the XY plane, but the plane's\n\t// normal and target are exactly 180deg flipped, meaning that the result of\n\t// the quaternion constructor will be undefined, in which case we manually\n\t// build a rotation matrix that rotates 180 degrees around the X axis.\n\t// 180 degree rotate around the X axis, or general rotation matrix\n\tconst planes_transform = planes\n\t\t.map(({ normal }) => (Math.abs(dot(normal, targetVector) + 1) < epsilon\n\t\t\t? [1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1]\n\t\t\t: matrix4FromQuaternion(quaternionFromTwoVectors(normal, targetVector))));\n\n\t// computing the translation vector as a single matrix * vector operation,\n\t// then overwriting it into the column vector of the rotation matrix\n\t// (which currently has a 0, 0, 0, translation), is the same as building an\n\t// entire translation matrix then computing the matrix product with: Mr * Mt.\n\t// this approach just cuts down on the number of operations.\n\tplanes.forEach(({ origin }, p) => {\n\t\tconst translation = multiplyMatrix4Vector3(planes_transform[p], flip3(origin));\n\t\tplanes_transform[p][12] = translation[0];\n\t\tplanes_transform[p][13] = translation[1];\n\t\tplanes_transform[p][14] = translation[2];\n\t});\n\n\treturn {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t};\n};\n\n/**\n * @description Cluster the faces of a graph into groups of face indices\n * where all faces in the same group both lie in the same plane and\n * overlap or are edge-connected to at least one face in the group.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   planes: { normal: number[], origin: number[] }[],\n *   planes_faces: number[][],\n *   planes_transform: number[][],\n *   planes_clusters: number[][],\n *   faces_winding: boolean[],\n *   faces_plane: number[],\n *   faces_cluster: number[],\n *   clusters_plane: number[],\n *   clusters_faces: number[][],\n * }} an object with:\n * - planes: a list of planes\n * - planes_faces: for every plane, a list of faces within this plane\n * - faces_plane: for every face, which plane is it in\n * - faces_winding: for every face within its plane, is the face's normal\n *   aligned with the plane's normal (true) or flipped 180 degrees (false).\n * - planes_transform: for every plane, a matrix which transforms the\n *   plane into the 2D XY plane\n * - planes_clusters: for every plane, a list of clusters which represent\n *   a cluster of coplanar and overlapping faces.\n * - clusters_plane: for every cluster, which plane does it inhabit\n * - clusters_faces: for every cluster, a list of faces which are a member\n * - faces_cluster: for every face, which clusters is it a member of.\n */\nexport const getCoplanarAdjacentOverlappingFaces = (\n\t{ vertices_coords, faces_vertices, faces_faces },\n\tepsilon = EPSILON,\n) => {\n\tif (!faces_faces) {\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\n\t// get our initial groups of faces which share a common plane in 3D.\n\t// upcoming, we will further subgroup each plane's groups of faces into\n\t// those which are connected and/or overlap geometrically.\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t} = getFacesPlane(\n\t\t{ vertices_coords, faces_vertices },\n\t\tepsilon,\n\t);\n\n\t// make sure we are using 3D points for this next part\n\tconst vertices_coords3D = vertices_coords.map(resize3);\n\n\t// if the face's winding is flipped from its plane's normal,\n\t// reverse the winding order, much easier than transforming a 180 rotation.\n\tconst faces_polygon = faces_vertices\n\t\t.map((verts, f) => (faces_winding[f] ? verts : verts.slice().reverse()))\n\t\t.map(verts => verts.map(v => vertices_coords3D[v]))\n\t\t.map(polygon => makePolygonNonCollinear3(polygon, epsilon))\n\t\t.map((polygon, f) => polygon\n\t\t\t.map(point => multiplyMatrix4Vector3(planes_transform[faces_plane[f]], point))\n\t\t\t.map(resize2));\n\n\t// for each plane group, create a faces_faces which only includes\n\t// those faces inside each group, these faces_faces arrays have holes.\n\tconst planes_facesFaces = planes_faces\n\t\t.map(faces => selfRelationalArraySubset(faces_faces, faces));\n\n\t// for each plane, group its faces into an array of subgroups, each subgroup\n\t// contains only faces which are connected to each other through the graph.\n\t// this contains: for every plane, for every face, which group is it a member of?\n\t//\n\t// uh oh. is this causing issues.\n\t// - I can't remember why I wrote that.\n\t//\n\tconst planes_faces_connectedGroup = planes_facesFaces.map(connectedComponents);\n\n\t// this contains: for each plane, for each group, a list of its connected faces.\n\tconst planes_connectedGroups_faces = planes_faces_connectedGroup\n\t\t.map(faces => invertFlatToArrayMap(faces));\n\n\t// now we have, for every plane, groups of faces where inside each group\n\t// we know that these faces are connected because they are connected via\n\t// the graph. now we need to handle the cases of being geometrically\n\t// overlapping, within each plane set, we will shrink the number of face\n\t// groups, if groups are found to be overlapping they will be merged.\n\n\t// now that we know a bunch about connected faces within groups we can\n\t// cut down on the number of calls to the polygon-polygon overlap method.\n\t// first, for each plane's groups of faces, gather together all other faces\n\t// in the same plane that aren't a part of this connected set of faces.\n\n\t// this contains: for every plane, for every face, a list of other coplanar\n\t// faces from another group which are a candidate for testing overlap.\n\tconst planes_faces_possibleOverlapFaces = planes_faces_connectedGroup\n\t\t.map(faces_group => {\n\t\t\tconst faces = faces_group.map((_, i) => i);\n\t\t\treturn faces_group.map(groupIndex => faces\n\t\t\t\t.filter(face => faces_group[face] !== groupIndex));\n\t\t});\n\n\t// inside each plane, for every overlappingGroup, store a list of other\n\t// group indices from the same plane which overlap this group. Shared\n\t// group indices mean these groups can be merged.\n\tconst planes_overlappingGroups = planes_connectedGroups_faces\n\t\t.map(connectedGroups_faces => connectedGroups_faces\n\t\t\t.map(() => []));\n\n\tplanes_faces_possibleOverlapFaces.forEach((faces_possibleOverlapFaces, p) => {\n\t\t// within this plane group, store pairs of group indices (\"a b\" and \"b a\")\n\t\t// where both groups have been shown to contain faces inside them which\n\t\t// overlap each other, meaning, these groups are able to be merged.\n\t\t// This is used to prevent duplicate work.\n\t\tconst overlappingGroupsKeys = {};\n\n\t\t// For every face, iterate through all of its possible overlapping faces,\n\t\t// and confirm whether or not these faces overlap. We don't care whether or\n\t\t// not the faces overlap, we care if the groups do, so we store the overlap\n\t\t// information between groups in two places:\n\t\t// - planes_overlappingGroups this will be used later to merge groups\n\t\t// - overlappingGroupsKeys lookup table to prevent doing duplicate work.\n\t\tfaces_possibleOverlapFaces\n\t\t\t.forEach((otherFaces, face) => otherFaces\n\t\t\t\t.forEach(otherFace => {\n\t\t\t\t\t// convert faces into their respective groups they inhabit\n\t\t\t\t\tconst groups = [face, otherFace]\n\t\t\t\t\t\t.map(f => planes_faces_connectedGroup[p][f]);\n\n\t\t\t\t\t// if we have already discovered these groups overlap, skip\n\t\t\t\t\tconst groupsKey1 = groups.join(\" \");\n\t\t\t\t\tif (overlappingGroupsKeys[groupsKey1]) { return; }\n\n\t\t\t\t\t// run the overlap method\n\t\t\t\t\tconst overlap = overlapConvexPolygons(\n\t\t\t\t\t\tfaces_polygon[face],\n\t\t\t\t\t\tfaces_polygon[otherFace],\n\t\t\t\t\t\tepsilon,\n\t\t\t\t\t);\n\t\t\t\t\tif (overlap) {\n\t\t\t\t\t\t// store the result of the overlap in the hash and the usable array.\n\t\t\t\t\t\t// this reverses the groups array in place, but it's fine.\n\t\t\t\t\t\tconst groupsKey2 = groups.reverse().join(\" \");\n\t\t\t\t\t\toverlappingGroupsKeys[groupsKey1] = true;\n\t\t\t\t\t\toverlappingGroupsKeys[groupsKey2] = true;\n\t\t\t\t\t\tplanes_overlappingGroups[p][groups[0]].push(groups[1]);\n\t\t\t\t\t\tplanes_overlappingGroups[p][groups[1]].push(groups[0]);\n\t\t\t\t\t}\n\t\t\t\t}));\n\t});\n\n\t// prevent duplicate entries for every group's group. but, this is not\n\t// needed because overlappingGroupsKeys hash already prevents duplicates\n\t// planes_overlappingGroups\n\t// \t.forEach((array1, i) => array1\n\t// \t\t.forEach((array2, j) => {\n\t// \t\t\tplanes_overlappingGroups[i][j] = Array.from(new Set(array2));\n\t// \t\t}));\n\n\t// for every plane, for every new cluster, an list of the group indices\n\t// from \"connectedGroups\" that are included in this new cluster.\n\tconst planes_clusters_groupIndices = planes_overlappingGroups\n\t\t.map(set_set => invertFlatToArrayMap(connectedComponents(set_set)));\n\n\t// relate a new cluster to the original plane it inhabits\n\tconst clusters_plane = planes_clusters_groupIndices\n\t\t.flatMap((arrays, i) => arrays.map(() => i));\n\n\tconst planes_clusters = invertFlatToArrayMap(clusters_plane);\n\n\tconst clusters_faces = planes_clusters_groupIndices\n\t\t.flatMap((clusters_groupIndices, p) => clusters_groupIndices\n\t\t\t.map(groupIndices => groupIndices\n\t\t\t\t.flatMap(g => planes_connectedGroups_faces[p][g])));\n\n\tconst faces_cluster = invertArrayToFlatMap(clusters_faces);\n\n\treturn {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\tplanes_clusters,\n\t\tfaces_winding,\n\t\tfaces_plane,\n\t\tfaces_cluster,\n\t\tclusters_plane,\n\t\tclusters_faces,\n\t};\n};\n"
  },
  {
    "path": "src/graph/faces/winding.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description For every face, return a boolean if the face's vertices are\n * in counter-clockwise winding. For origami models, this translates to\n * true meaning the face is upright, false meaning the face is flipped over.\n * @param {FOLD} graph a FOLD object\n * @returns {boolean[]} boolean for every face, true if face is counter-clockwise.\n * @attribution https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order\n */\nexport const makeFacesWinding = ({ vertices_coords, faces_vertices }) => faces_vertices\n\t.map(vertices => vertices\n\t\t.map(v => vertices_coords[v])\n\t\t.map((point, i, arr) => [point, arr[(i + 1) % arr.length]])\n\t\t.map(pts => (pts[1][0] - pts[0][0]) * (pts[1][1] + pts[0][1]))\n\t\t.reduce((a, b) => a + b, 0))\n\t.map(face => face < 0);\n\n/**\n * @description For every face, return a boolean indicating if the face has\n * been flipped over or not (false=flipped), by using the faces_matrix and\n * checking the determinant.\n * @param {number[][]} faces_matrix for every face, a 3x4 transform matrix\n * @returns {boolean[]} true if a face is counter-clockwise.\n */\nexport const makeFacesWindingFromMatrix = faces_matrix => faces_matrix\n\t.map(m => m[0] * m[4] - m[1] * m[3])\n\t.map(c => c >= 0);\n\n/**\n * @description For every face, return a boolean indicating if the face has\n * been flipped over or not (false=flipped), by using a faces_matrix containing\n * 2D matrices.\n * @param {number[][]} faces_matrix2 for every face, a 2x3 transform matrix\n * @returns {boolean[]} true if a face is counter-clockwise.\n */\nexport const makeFacesWindingFromMatrix2 = faces_matrix2 => faces_matrix2\n\t.map(m => m[0] * m[3] - m[1] * m[2])\n\t.map(c => c >= 0);\n"
  },
  {
    "path": "src/graph/flaps.js",
    "content": "// /**\n//  * Rabbit Ear (c) Kraft\n//  */\n// import {\n// \tEPSILON,\n// } from \"../math/constant.js\";\n// import {\n// \texcludeS,\n// } from \"../math/compare.js\";\n// import {\n// \tcross2,\n// \tsubtract2,\n// } from \"../math/vector.js\";\n// import {\n// \tgetEdgesLineIntersection,\n// \tgetEdgesCollinearToLine,\n// } from \"./intersect/edges.js\";\n// import {\n// \tmakeFacesEdgesFromVertices,\n// } from \"./make/facesEdges.js\";\n// import {\n// \tmakeFacesNormal,\n// } from \"./normals.js\";\n// import {\n// \tfaceOrdersSubset,\n// \tlinearizeFaceOrders,\n// } from \"./orders.js\";\n\n// /**\n//  * @param {FOLD} graph a FOLD object\n//  * @param {VecLine2} line\n//  * @param {number} [epsilon=1e-6] an optional epsilon\n//  */\n// export const getEdgesSide = ({ vertices_coords, edges_vertices }, line, epsilon = EPSILON) => {\n// \t/** @param {[number, number]} edge_vertices @returns {number} */\n// \tconst edgeSide = (edge_vertices) => edge_vertices\n// \t\t.map(v => vertices_coords[v])\n// \t\t.map(coord => subtract2(coord, line.origin))\n// \t\t.map(vec => cross2(line.vector, vec))\n// \t\t.sort((a, b) => Math.abs(b) - Math.abs(a))\n// \t\t.map(Math.sign)\n// \t\t.shift();\n// \tconst edgesIntersection = getEdgesLineIntersection({\n// \t\tvertices_coords, edges_vertices,\n// \t}, line, epsilon, excludeS);\n// \tconst edgesCollinear = {};\n// \tgetEdgesCollinearToLine({ vertices_coords, edges_vertices }, line, epsilon)\n// \t\t.forEach(e => { edgesCollinear[e] = true; });\n// \t// -1: left, +1: right (todo check these), 0: both, 2: collinear (neither side)\n// \treturn edges_vertices.map((edge_vertices, e) => {\n// \t\tif (edgesCollinear[e] === true) { return 2; }\n// \t\tif (edgesIntersection[e].point !== undefined) { return 0; }\n// \t\treturn edgeSide(edge_vertices);\n// \t});\n// };\n\n// /**\n//  * @param {FOLD} graph a FOLD object\n//  * @param {VecLine2} line\n//  * @param {number} [epsilon=1e-6] an optional epsilon\n//  */\n// export const getFacesSide = ({\n// \tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n// }, line, epsilon = EPSILON) => {\n// \tif (!faces_edges) {\n// \t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n// \t}\n// \tconst edgesSide = getEdgesSide({ vertices_coords, edges_vertices }, line, epsilon);\n// \t// filter out \"collinear\" edges, they don't matter here\n// \tconst facesEdgesSide = faces_edges\n// \t\t.map(edges => edges\n// \t\t\t.map(e => edgesSide[e])\n// \t\t\t.filter(side => side !== 2));\n// \tconst facesOverlapLine = facesEdgesSide\n// \t\t.map(sides => sides.includes(0));\n// \tconst facesEdgesSameSide = facesEdgesSide\n// \t\t.map((sides, i) => (facesOverlapLine[i]\n// \t\t\t? false\n// \t\t\t: sides.reduce((a, b) => a && (b === sides[0]), true)));\n// \treturn facesEdgesSameSide\n// \t\t.map((sameSide, f) => (sameSide ? facesEdgesSide[f][0] : 0));\n// };\n\n// /**\n//  * @description flat folded crease patterns only (2D)\n//  * @algorithm two approaches.\n//  * split the graph with a line (create a copy in memory), rebuild a new\n//  * faces_faces array, and save the faces-map, separate all faces in the graph\n//  * based on which side of the line\n//  * alternatively (without modifying the graph):\n//  *\n//  */\n// export const getFlapsThroughLine = ({\n// \tvertices_coords, edges_vertices, faces_vertices, faces_edges, faceOrders,\n// }, line, epsilon = EPSILON) => {\n// \tif (!faceOrders) { throw new Error(\"faceOrders required\"); }\n// \t// for every face, get the intersection\n// \tconst facesSide = getFacesSide({\n// \t\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n// \t}, line, epsilon);\n// \tconst sidesFaces = [-1, 1]\n// \t\t.map(side => facesSide\n// \t\t\t.map((s, f) => ({ s, f }))\n// \t\t\t.filter(el => el.s === side || el.s === 0)\n// \t\t\t.map(el => el.f));\n// \tconst sidesFaceOrders = sidesFaces\n// \t\t.map(faces => faceOrdersSubset(faceOrders, faces));\n// \tconsole.log(\"facesSide\", facesSide);\n// \tconsole.log(\"sidesFaces\", sidesFaces);\n// \tconsole.log(\"sidesFaceOrders\", sidesFaceOrders);\n// \tconst faces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n// \tconst sidesLayersFace = sidesFaceOrders.map(orders => linearizeFaceOrders({\n// \t\tfaceOrders: orders, faces_normal,\n// \t}));\n// \tconsole.log(\"sidesLayersFace\", sidesLayersFace);\n// };\n"
  },
  {
    "path": "src/graph/fold/flatFold.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tmagnitude2,\n\tnormalize2,\n\tdot2,\n\tcross2,\n\tscale2,\n\tsubtract2,\n} from \"../../math/vector.js\";\nimport {\n\tinvertMatrix2,\n\tmultiplyMatrix2Line2,\n\tmultiplyMatrices2,\n\tmakeMatrix2Reflect,\n} from \"../../math/matrix2.js\";\nimport {\n\tedgeAssignmentToFoldAngle,\n} from \"../../fold/spec.js\";\nimport {\n\tmergeNextmaps,\n} from \"../maps.js\";\nimport {\n\tmakeFacesMatrix2,\n} from \"../faces/matrix.js\";\nimport {\n\tmakeVerticesCoordsFoldedFromMatrix2,\n} from \"../vertices/folded.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\nimport {\n\tfaceContainingPoint,\n} from \"../faces/facePoint.js\";\nimport {\n\tmakeFacesWindingFromMatrix2,\n} from \"../faces/winding.js\";\nimport {\n\tsplitFaceWithLine,\n} from \"./flatFoldSplitFace.js\";\nimport {\n\tpopulate,\n} from \"../populate.js\";\n\n/**\n * @description this determines which side of a line (using cross product)\n * a face lies in a folded form, except, the face is the face in\n * the crease pattern and the line (vector origin) is transformed\n * by the face matrix. because of this, we use face_winding to know\n * if this face was flipped over, reversing the result.\n * @note by flipping the < and > in the return, this one change\n * will modify the entire method to toggle which side of the line\n * are the faces which will be folded over.\n */\nconst make_face_side = (vector, origin, face_center, face_winding) => {\n\tconst center_vector = subtract2(face_center, origin);\n\tconst determinant = cross2(vector, center_vector);\n\treturn face_winding ? determinant > 0 : determinant < 0;\n};\n\n/**\n * for quickly determining which side of a crease a face lies\n * this uses point average, not centroid, faces must be convex\n * and again it's not precise, but this doesn't matter because\n * the faces which intersect the line (and could potentially cause\n * discrepencies) don't use this method, it's only being used\n * for faces which lie completely on one side or the other.\n */\nconst make_face_center = (graph, face) => (!graph.faces_vertices[face]\n\t? [0, 0]\n\t: graph.faces_vertices[face]\n\t\t.map(v => graph.vertices_coords[v])\n\t\t.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0])\n\t\t.map(el => el / graph.faces_vertices[face].length));\n\nconst unfolded_assignment = {\n\tF: true, f: true, U: true, u: true,\n};\nconst opposite_lookup = {\n\tM: \"V\", m: \"V\", V: \"M\", v: \"M\",\n};\n\n/**\n * @description for a mountain or valley, return the opposite.\n * in the case of any other crease (boundary, flat, ...) return the input.\n */\nconst get_opposite_assignment = assign => opposite_lookup[assign] || assign;\n\n/**\n * @description shallow copy these entries for one face in the graph.\n * this is intended to capture the values, in the case of the face\n * being removed from the graph (not deep deleted, just unreferenced).\n */\nconst face_snapshot = (graph, face) => ({\n\tcenter: graph.faces_center[face],\n\tmatrix: graph.faces_matrix2[face],\n\twinding: graph.faces_winding[face],\n\tcrease: graph.faces_crease[face],\n\tside: graph.faces_side[face],\n\tlayer: graph.faces_layer[face],\n});\n\n/**\n * @description this builds a new faces_layer array. it first separates the\n * folding faces from the non-folding using faces_folding, an array of [t,f].\n * it flips the folding faces over, appends them to the non-folding ordering,\n * and (re-indexes/normalizes) all the z-index values to be the minimum\n * whole number set starting with 0.\n * @param {number[]} faces_layer each index is a face, each value is the z-layer order.\n * @param {boolean[]} faces_folding each index is a face, T/F will the face be folded over?\n * @returns {number[]} each index is a face, each value is the z-layer order.\n */\nconst foldFacesLayer = (faces_layer, faces_folding) => {\n\tconst new_faces_layer = [];\n\t// filter face indices into two arrays, those folding and not folding\n\tconst arr = faces_layer.map((_, i) => i);\n\tconst folding = arr.filter(i => faces_folding[i]);\n\tconst not_folding = arr.filter(i => !faces_folding[i]);\n\t// sort all the non-folding indices by their current layer, bottom to top,\n\t// give each face index a new layering index.\n\t// compress whatever current layer numbers down into [0...n]\n\tnot_folding\n\t\t.sort((a, b) => faces_layer[a] - faces_layer[b])\n\t\t.forEach((face, i) => { new_faces_layer[face] = i; });\n\t// sort the folding faces in reverse order (flip them), compress their\n\t// layers down into [0...n] and and set each face to this layer index\n\tfolding\n\t\t.sort((a, b) => faces_layer[b] - faces_layer[a]) // reverse order here\n\t\t.forEach((face, i) => { new_faces_layer[face] = not_folding.length + i; });\n\treturn new_faces_layer;\n};\n\n/**\n *\n */\nexport const getVerticesCollinearToLine = (\n\t{ vertices_coords },\n\t{ vector, origin },\n\tepsilon = EPSILON,\n) => {\n\tconst normalizedLineVec = normalize2(vector);\n\tconst pointIsCollinear = (point) => {\n\t\tconst originToPoint = subtract2(point, origin);\n\t\tconst mag = magnitude2(originToPoint);\n\t\t// point and origin are the same\n\t\tif (Math.abs(mag) < epsilon) { return true; }\n\t\t// normalize both vectors, compare dot product\n\t\tconst originToPointUnit = scale2(originToPoint, 1 / mag);\n\t\tconst dotprod = Math.abs(dot2(originToPointUnit, normalizedLineVec));\n\t\treturn Math.abs(1.0 - dotprod) < epsilon;\n\t};\n\treturn vertices_coords\n\t\t.map(pointIsCollinear)\n\t\t.map((a, i) => ({ a, i }))\n\t\t.filter(el => el.a)\n\t\t.map(el => el.i);\n};\n\n/**\n * @description Find all edges in a graph which lie parallel\n * and on top of a line (infinite line). Can be 2D or 3D.\n * O(n) where n=edges\n * @param {FOLD} graph a FOLD object\n * @param {VecLine} line a line with a vector and origin component\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[]} array of edge indices which are collinear to the line\n */\nexport const getEdgesCollinearToLine = (\n\t{ vertices_coords, edges_vertices, vertices_edges },\n\t{ vector, origin },\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst verticesCollinear = getVerticesCollinearToLine(\n\t\t{ vertices_coords },\n\t\t{ vector, origin },\n\t\tepsilon,\n\t);\n\tconst edgesCollinearCount = edges_vertices.map(() => 0);\n\tverticesCollinear\n\t\t.forEach(vertex => vertices_edges[vertex]\n\t\t\t.forEach(edge => { edgesCollinearCount[edge] += 1; }));\n\treturn edgesCollinearCount\n\t\t.map((count, i) => ({ count, i }))\n\t\t.filter(el => el.count === 2)\n\t\t.map(el => el.i);\n};\n\n/**\n * @description make a crease that passes through the entire origami and modify the\n * faces order to simulate one side of the faces flipped over and set on top.\n * @param {FOLDExtended & {\n *   faces_matrix2: number[][],\n *   faces_winding: boolean[],\n *   faces_crease: VecLine2[],\n *   faces_side: boolean[],\n * }} graph a FOLD graph in crease pattern form, will be modified in place\n * @param {VecLine2} line a fold line in 2D\n * @param {string} assignment (M/V/F) a FOLD spec encoding of the direction of the fold\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {object} a summary of changes to faces/edges.\n * @algorithm Because we want to return the new modified origami in crease pattern form,\n * as we iterate through the faces, splitting faces which cross the crease\n * line, we have to be modifying the crease pattern, as opposed to modifying\n * a folded form then unfolding the vertices, which would be less precise.\n * So, we will create copies of the crease line, one per face, transformed\n * into place by its face's matrix, which superimposes many copies of the\n * crease line onto the crease pattern, each in place\n */\nexport const flatFold = (\n\tgraph,\n\t{ vector, origin },\n\tassignment = \"V\",\n\tepsilon = EPSILON,\n) => {\n\tconst opposite_assignment = get_opposite_assignment(assignment);\n\t// make sure the input graph contains the necessary data.\n\t// this takes care of all standard FOLD-spec arrays.\n\t// todo: this could be optimized by trusting that the existing arrays\n\t// are accurate, checking if they exist and skipping them if so.\n\tpopulate(graph);\n\t// additionally, we need to ensure faces layer exists.\n\t// todo: if it doesn't exist, should we use the solver?\n\tif (!graph.faces_layer) {\n\t\tgraph.faces_layer = Array(graph.faces_vertices.length).fill(0);\n\t}\n\t// these will be properties on the graph. as we iterate through faces,\n\t// splitting (removing 1 face, adding 2) inside \"splitFaceWithLine\", the remove\n\t// method will automatically shift indices for arrays starting with \"faces_\".\n\t// we will remove these arrays at the end of this method.\n\tgraph.faces_center = graph.faces_vertices\n\t\t.map((_, i) => make_face_center(graph, i));\n\t// faces_matrix is built from the crease pattern, but reflects\n\t// the faces in their folded state.\n\tif (!graph.faces_matrix2) {\n\t\tgraph.faces_matrix2 = makeFacesMatrix2(\n\t\t\tgraph,\n\t\t\t[faceContainingPoint(graph, origin, vector)],\n\t\t);\n\t}\n\tgraph.faces_winding = makeFacesWindingFromMatrix2(graph.faces_matrix2);\n\tgraph.faces_crease = graph.faces_matrix2\n\t\t.map(invertMatrix2)\n\t\t.map(matrix => multiplyMatrix2Line2(matrix, { vector, origin }));\n\tgraph.faces_side = graph.faces_vertices\n\t\t.map((_, i) => make_face_side(\n\t\t\tgraph.faces_crease[i].vector,\n\t\t\tgraph.faces_crease[i].origin,\n\t\t\tgraph.faces_center[i],\n\t\t\tgraph.faces_winding[i],\n\t\t));\n\t// before we start splitting faces, we have to handle the case where\n\t// a flat crease already exists along the fold crease, already splitting\n\t// two faces (assignment \"F\" or \"U\" only), the splitFaceWithLine method\n\t// will not catch these. we need to find these edges before we modify\n\t// the graph, find the face they are attached to and whether the face\n\t// is flipped, and set the edge to the proper \"V\" or \"M\" (and foldAngle).\n\tconst vertices_coords_folded = makeVerticesCoordsFoldedFromMatrix2(\n\t\tgraph,\n\t\tgraph.faces_matrix2,\n\t);\n\t// get all (folded) edges which lie parallel and overlap the crease line\n\tconst collinear_edges = getEdgesCollinearToLine({\n\t\tvertices_coords: vertices_coords_folded,\n\t\tedges_vertices: graph.edges_vertices,\n\t}, { vector, origin }, epsilon)\n\t\t.filter(e => unfolded_assignment[graph.edges_assignment[e]]);\n\t// get the first valid adjacent face for each edge, get that face's winding,\n\t// which determines the crease assignment, and assign it to the edge\n\tcollinear_edges\n\t\t.map(e => graph.edges_faces[e].find(f => f != null))\n\t\t.map(f => graph.faces_winding[f])\n\t\t.map(winding => (winding ? assignment : opposite_assignment))\n\t\t.forEach((assign, e) => {\n\t\t\tgraph.edges_assignment[collinear_edges[e]] = assign;\n\t\t\tgraph.edges_foldAngle[collinear_edges[e]] = edgeAssignmentToFoldAngle(\n\t\t\t\tassign,\n\t\t\t);\n\t\t});\n\t// before we start splitting, capture the state of face 0. we will use\n\t// it when rebuilding the graph's matrices after all splitting is finished.\n\tconst face0 = face_snapshot(graph, 0);\n\t// now, iterate through faces (reverse order), rebuilding the custom\n\t// arrays for the newly added faces when a face is split.\n\tconst split_changes = graph.faces_vertices\n\t\t.map((_, i) => i)\n\t\t.reverse()\n\t\t.map((i) => {\n\t\t\t// this is the face about to be removed. if the face is successfully\n\t\t\t// split the face will be removed but we still need to reference\n\t\t\t// values from it to complete the 2 new faces which replace it.\n\t\t\tconst face = face_snapshot(graph, i);\n\t\t\t// split the polygon (if possible), get back a summary of changes.\n\t\t\tconst change = splitFaceWithLine(graph, i, face.crease, epsilon);\n\t\t\t// console.log(\"split convex polygon change\", change);\n\t\t\tif (change === undefined) { return undefined; }\n\t\t\t// const face_winding = folded.faces_winding[i];\n\t\t\t// console.log(\"face change\", face, change);\n\t\t\t// update the assignment of the newly added edge separating the 2 new faces\n\t\t\tgraph.edges_assignment[change.edges.new] = face.winding\n\t\t\t\t? assignment\n\t\t\t\t: opposite_assignment;\n\t\t\tgraph.edges_foldAngle[change.edges.new] = edgeAssignmentToFoldAngle(\n\t\t\t\tgraph.edges_assignment[change.edges.new],\n\t\t\t);\n\t\t\t// these are the two faces that replaced the removed face after the split\n\t\t\tconst new_faces = change.faces.map[change.faces.remove];\n\t\t\tnew_faces.forEach(f => {\n\t\t\t\t// no need right now to build faces_winding, faces_matrix, ...\n\t\t\t\tgraph.faces_center[f] = make_face_center(graph, f);\n\t\t\t\tgraph.faces_side[f] = make_face_side(\n\t\t\t\t\tface.crease.vector,\n\t\t\t\t\tface.crease.origin,\n\t\t\t\t\tgraph.faces_center[f],\n\t\t\t\t\tface.winding,\n\t\t\t\t);\n\t\t\t\tgraph.faces_layer[f] = face.layer;\n\t\t\t});\n\t\t\treturn change;\n\t\t})\n\t\t.filter(a => a !== undefined);\n\t// all faces have been split. get a summary of changes to the graph.\n\t// \"faces_map\" is actually needed. the others are just included in the return\n\tconst faces_map = mergeNextmaps(...split_changes.map(el => el.faces.map));\n\tconst edges_map = mergeNextmaps(...split_changes.map(el => el.edges.map)\n\t\t.filter(a => a !== undefined));\n\tconst faces_remove = split_changes.map(el => el.faces.remove).reverse();\n\t// const vert_dict = {};\n\t// split_changes.forEach(el => el.vertices.forEach(v => { vert_dict[v] = true; }));\n\t// const new_vertices = Object.keys(vert_dict).map(s => parseInt(s));\n\t// build a new face layer ordering\n\tgraph.faces_layer = foldFacesLayer(\n\t\tgraph.faces_layer,\n\t\tgraph.faces_side,\n\t);\n\t// build new face matrices for the folded state. use face 0 as reference\n\t// we need its original matrix, and if face 0 was split we need to know\n\t// which of its two new faces doesn't move as the new faces matrix\n\t// calculation requires we provide the one face that doesn't move.\n\tconst face0_was_split = faces_map && faces_map[0] && faces_map[0].length === 2;\n\tconst face0_newIndex = (face0_was_split\n\t\t? faces_map[0].filter(f => graph.faces_side[f]).shift()\n\t\t: 0);\n\t// only if face 0 lies on the not-flipped side (sidedness is false),\n\t// and it wasn't creased-through, can we use its original matrix.\n\t// if face 0 lies on the flip side (sidedness is true), or it was split,\n\t// face 0 needs to be multiplied by its crease's reflection matrix, but\n\t// only for valley or mountain folds, \"flat\" folds need to copy the matrix\n\tlet face0_preMatrix = face0.matrix;\n\t// only if the assignment is valley or mountain, do this. otherwise skip\n\tif (assignment !== opposite_assignment) {\n\t\tface0_preMatrix = (!face0_was_split && !graph.faces_side[0]\n\t\t\t? face0.matrix\n\t\t\t: multiplyMatrices2(\n\t\t\t\tface0.matrix,\n\t\t\t\tmakeMatrix2Reflect(\n\t\t\t\t\tface0.crease.vector,\n\t\t\t\t\tface0.crease.origin,\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\t// build our new faces_matrices using face 0 as the starting point,\n\t// setting face 0 as the identity matrix, then multiply every\n\t// face's matrix by face 0's actual starting matrix\n\tgraph.faces_matrix2 = makeFacesMatrix2(graph, [face0_newIndex])\n\t\t.map(matrix => multiplyMatrices2(face0_preMatrix, matrix));\n\t// these are no longer needed. some of them haven't even been fully rebuilt.\n\tdelete graph.faces_center;\n\tdelete graph.faces_winding;\n\tdelete graph.faces_crease;\n\tdelete graph.faces_side;\n\t// summary of changes to the graph\n\treturn {\n\t\tfaces: { map: faces_map, remove: faces_remove },\n\t\tedges: { map: edges_map },\n\t\t// vertices: { new: new_vertices },\n\t};\n};\n"
  },
  {
    "path": "src/graph/fold/flatFoldSplitFace.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../../environment/messages.js\";\nimport { EPSILON } from \"../../math/constant.js\";\nimport {\n\tincludeL,\n\texcludeS,\n} from \"../../math/compare.js\";\nimport { overlapLinePoint } from \"../../math/overlap.js\";\nimport { intersectLineLine } from \"../../math/intersect.js\";\nimport {\n\tdistance,\n\tsubtract,\n\tsubtract2,\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tsortVerticesCounterClockwise,\n} from \"../vertices/sort.js\";\nimport {\n\tmakeVerticesToEdge,\n} from \"../make/lookup.js\";\nimport {\n\tmergeNextmaps,\n\tmergeBackmaps,\n\tinvertFlatMap,\n} from \"../maps.js\";\nimport {\n\tsplitEdge,\n} from \"../split/splitEdge.js\";\nimport {\n\tremove,\n} from \"../remove.js\";\n\n/**\n * @description intersect a convex face with a line and return the location\n * of the intersections as components of the graph. this is an EXCLUSIVE\n * intersection. line collinear to the edge counts as no intersection.\n * there are 5 cases:\n * - no intersection (undefined)\n * - intersect one vertex (undefined)\n * - intersect two vertices (valid, or undefined if neighbors)\n * - intersect one vertex and one edge (valid)\n * - intersect two edges (valid)\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face\n * @param {VecLine2} line the vector component describing the line\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {object|undefined} \"vertices\" and \"edges\" keys, indices of the\n * components which intersect the line. or undefined if no intersection\n */\nconst intersectConvexFaceLine = ({\n\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n}, face, { vector, origin }, epsilon = EPSILON) => {\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\t// give us back the indices in the faces_vertices[face] array\n\t// we can count on these being sorted (important later)\n\tconst face_vertices_indices = faces_vertices[face]\n\t\t.map(v => vertices_coords2[v])\n\t\t.map(coord => overlapLinePoint({ vector, origin }, coord, () => true, epsilon))\n\t\t.map((overlap, i) => (overlap ? i : undefined))\n\t\t.filter(i => i !== undefined);\n\t// o-----o---o  we have to test against cases like this, where more than two\n\t// |         |  vertices lie along one line.\n\t// o---------o\n\tconst vertices = face_vertices_indices.map(i => faces_vertices[face][i]);\n\t// concat a duplication of the array where the second array's vertices'\n\t// indices' are all increased by the faces_vertices[face].length.\n\t// ask every neighbor pair if they are 1 away from each other, if so, the line\n\t// lies along an outside edge of the convex poly, return \"no intersection\".\n\t// the concat is needed to detect neighbors across the end-beginning loop.\n\tconst vertices_are_neighbors = face_vertices_indices\n\t\t.concat(face_vertices_indices.map(i => i + faces_vertices[face].length))\n\t\t.map((n, i, arr) => arr[i + 1] - n === 1)\n\t\t.reduce((a, b) => a || b, false);\n\t// if vertices are neighbors\n\t// because convex polygon, if collinear vertices lie along an edge,\n\t// it must be an outside edge. this case returns no intersection.\n\tif (vertices_are_neighbors) { return undefined; }\n\tif (vertices.length > 1) { return { vertices, edges: [] }; }\n\t// run the line-segment intersection on every side of the face polygon\n\tconst edges = faces_edges[face]\n\t\t.map(edge => edges_vertices[edge]\n\t\t\t.map(v => vertices_coords2[v]))\n\t\t.map(seg => intersectLineLine(\n\t\t\t{ vector, origin },\n\t\t\t{ vector: subtract2(seg[1], seg[0]), origin: seg[0] },\n\t\t\tincludeL,\n\t\t\texcludeS,\n\t\t\tepsilon,\n\t\t).point).map((coords, face_edge_index) => ({\n\t\t\tcoords,\n\t\t\tedge: faces_edges[face][face_edge_index],\n\t\t}))\n\t\t// remove edges with no intersection\n\t\t.filter(el => el.coords !== undefined)\n\t\t// remove edges which share a vertex with a previously found vertex.\n\t\t// these edges are because the intersection is near a vertex but also\n\t\t// intersects the edge very close to the end.\n\t\t.filter(el => !(vertices\n\t\t\t.map(v => edges_vertices[el.edge].includes(v))\n\t\t\t.reduce((a, b) => a || b, false)));\n\t// only return the case with 2 intersections. for example, only 1 vertex\n\t// intersection implies outside the polygon, collinear with one vertex.\n\treturn (edges.length + vertices.length === 2\n\t\t? { vertices, edges }\n\t\t: undefined);\n};\n\n/**\n * @description A circular array (data wraps around) requires 2 indices\n * if you intend to split it into two arrays. The pair of indices can be\n * provided in any order, they will be sorted, smaller index first.\n * @param {any[]} array an array that is meant to be thought of as circular\n * @param {number[]} indices two numbers, indices that divide the array into 2 parts\n * @returns {any[][]} the same array split into two arrays\n */\nconst splitCircularArray = (array, indices) => {\n\tindices.sort((a, b) => a - b);\n\treturn [\n\t\tarray.slice(indices[1]).concat(array.slice(0, indices[0] + 1)),\n\t\tarray.slice(indices[0], indices[1] + 1),\n\t];\n};\n\n/**\n * this must be done AFTER edges_vertices has been updated with the new edge.\n *\n * @param {FOLD} graph a FOLD object\n * @param {number} face the face that will be replaced by these 2 new\n * @param {number[]} vertices (in the face) that split the face into 2 sides\n */\nconst make_faces = ({\n\tedges_vertices, faces_vertices, faces_edges,\n}, face, vertices) => {\n\t// the indices of the two vertices inside the face_vertices array.\n\t// this is where we will split the face into two.\n\tconst indices = vertices.map(el => faces_vertices[face].indexOf(el));\n\tconst faces = splitCircularArray(faces_vertices[face], indices)\n\t\t.map(fv => ({ faces_vertices: fv, faces_edges: [] }));\n\tif (faces_edges) {\n\t\t// table to build faces_edges\n\t\tconst vertices_to_edge = makeVerticesToEdge({ edges_vertices });\n\t\tfaces\n\t\t\t.map(this_face => this_face.faces_vertices\n\t\t\t\t.map((fv, i, arr) => `${fv} ${arr[(i + 1) % arr.length]}`)\n\t\t\t\t.map(key => vertices_to_edge[key]))\n\t\t\t.forEach((face_edges, i) => { faces[i].faces_edges = face_edges; });\n\t}\n\treturn faces;\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} face\n * @param {number[]} vertices\n */\nconst build_faces = (graph, face, vertices) => {\n\t// new face indices at the end of the list\n\tconst faces = [0, 1].map(i => graph.faces_vertices.length + i);\n\t// construct new face data for faces_vertices, faces_edges\n\t// add them to the graph\n\tmake_faces(graph, face, vertices)\n\t\t.forEach((newface, i) => Object.keys(newface)\n\t\t\t.forEach((key) => { graph[key][faces[i]] = newface[key]; }));\n\treturn faces;\n};\n\n/**\n * @description given two vertices and incident faces, create all new\n * \"edges_\" entries to describe a new edge that sits between the params.\n * @param {object} FOLD graph\n * @param {number[]} vertices two incident vertices that make up this edge\n * @param {number} face\n * @returns {object} all FOLD spec \"edges_\" entries for this new edge.\n */\n// const make_edge = ({ vertices_coords }, vertices, faces) => {\nconst make_edge = ({ vertices_coords }, vertices, face) => {\n\t// coords reversed for \"vector\", so index [0] comes last in subtract\n\tconst new_edge_coords = vertices\n\t\t.slice() // todo: i just added this without testing\n\t\t.map(v => vertices_coords[v])\n\t\t.reverse();\n\n\treturn {\n\t\tedges_vertices: [...vertices],\n\t\tedges_foldAngle: 0,\n\t\tedges_assignment: \"U\",\n\t\tedges_length: distance(new_edge_coords[0], new_edge_coords[1]),\n\t\tedges_vector: subtract(new_edge_coords[0], new_edge_coords[1]),\n\t\tedges_faces: [face, face],\n\t};\n};\n/**\n *\n */\nconst rebuild_edge = (graph, face, vertices) => {\n\t// now that 2 vertices are in place, add a new edge between them.\n\tconst edge = graph.edges_vertices.length;\n\t// construct data for our new edge (vertices, assignent, foldAngle...)\n\t// and the entry for edges_faces will be [x, x] where x is the index of\n\t// the old face, twice, and will be replaced later in this function.\n\tconst new_edge = make_edge(graph, vertices, face);\n\t// ignoring any keys that aren't a part of our graph, add the new edge\n\tObject.keys(new_edge)\n\t\t.filter(key => graph[key] !== undefined)\n\t\t.forEach((key) => { graph[key][edge] = new_edge[key]; });\n\treturn edge;\n};\n\n/**\n * @description this is a highly specific method, it takes in the output\n * from intersectConvexFaceLine and applies it to a graph by splitting\n * the edges (in the case of edge, not vertex intersection),\n * @param {FOLD} graph a FOLD object. modified in place.\n * @param {object} the result from calling \"intersectConvexFaceLine\".\n * each value must be an array. these will be modified in place.\n * @returns {object} with \"vertices\" and \"edges\" keys where\n * - vertices is an array of indices (the new vertices)\n * - edges is an object with keys \"map\", the changes to edge array, and\n * \"remove\", the indices of edges that have been removed.\n * look inside of \"map\" at the indices from \"removed\" for the indices\n * of the new edges which replaced them.\n */\nconst split_at_intersections = (graph, { vertices, edges }) => {\n\t// intersection will contain 2 items, either in \"vertices\" or \"edges\",\n\t// however we will split edges and store their new vertex in \"vertices\"\n\t// so in the end, \"vertices\" will contain 2 items.\n\tlet map;\n\t// split the edge (modifying the graph), and store the changes so that during\n\t// the next loop the second edge to split will be updated to the new index\n\tconst split_results = edges.map((el) => {\n\t\tconst res = splitEdge(graph, map ? map[el.edge] : el.edge, el.coords);\n\t\tmap = map ? mergeNextmaps(map, res.edges.map) : res.edges.map;\n\t\treturn res;\n\t});\n\tvertices.push(...split_results.map(res => res.vertex));\n\t// if two edges were split, the second one contains a \"remove\" key that was\n\t// based on the mid-operation graph, update this value to match the graph\n\t// before any changes occurred.\n\tlet bkmap;\n\t// todo: if we extend this to include non-convex polygons, this is the\n\t// only part of the code we need to test. cumulative backmap merge.\n\t// this was written without any testing, as convex polygons never have\n\t// more than 2 intersections\n\tsplit_results.forEach(res => {\n\t\tres.edges.remove = bkmap ? bkmap[res.edges.remove] : res.edges.remove;\n\t\tconst inverted = invertFlatMap(res.edges.map);\n\t\tbkmap = bkmap ? mergeBackmaps(bkmap, inverted) : inverted;\n\t});\n\treturn {\n\t\tvertices,\n\t\tedges: {\n\t\t\tmap,\n\t\t\tremove: split_results.map(res => res.edges.remove),\n\t\t},\n\t};\n};\n\n/**\n * @description a newly-added edge needs to update its two endpoints'\n * vertices_vertices. each vertices_vertices gains one additional\n * vertex, then the whole array is re-sorted counter-clockwise\n * @param {object} FOLD object\n * @param {number} edge index of the newly-added edge\n */\nconst update_vertices_vertices = ({\n\tvertices_coords, vertices_vertices, edges_vertices,\n}, edge) => {\n\tconst v0 = edges_vertices[edge][0];\n\tconst v1 = edges_vertices[edge][1];\n\tvertices_vertices[v0] = sortVerticesCounterClockwise(\n\t\t{ vertices_coords },\n\t\tvertices_vertices[v0].concat(v1),\n\t\tv0,\n\t);\n\tvertices_vertices[v1] = sortVerticesCounterClockwise(\n\t\t{ vertices_coords },\n\t\tvertices_vertices[v1].concat(v0),\n\t\tv1,\n\t);\n};\n\n/**\n* @param {FOLD} graph a FOLD object. modified in place.\n* @param {number} edge\n * vertices_vertices was just run before this method. use it.\n * vertices_edges should be up to date, except for the addition\n * of this one new edge at both ends of\n */\nconst update_vertices_edges = ({\n\tedges_vertices, vertices_edges, vertices_vertices,\n}, edge) => {\n\t// the expensive way, rebuild all arrays\n\t// graph.vertices_edges = makeVerticesEdges(graph);\n\tif (!vertices_edges || !vertices_vertices) { return; }\n\tconst vertices = edges_vertices[edge];\n\t// for each of the two vertices, check its vertices_vertices for the\n\t// index of the opposite vertex. this is the edge. return its position\n\t// in the vertices_vertices to be used to insert into vertices_edges.\n\tvertices\n\t\t.map(v => vertices_vertices[v])\n\t\t.map((vert_vert, i) => vert_vert\n\t\t\t.indexOf(vertices[(i + 1) % vertices.length]))\n\t\t.forEach((radial_index, i) => vertices_edges[vertices[i]]\n\t\t\t.splice(radial_index, 0, edge));\n};\n/**\n * @description search inside vertices_faces for an occurence\n * of the removed face, determine which of our two new faces\n * needs to be put in its place by checking faces_vertices\n * by way of this map we build at the beginning.\n */\nconst update_vertices_faces = (graph, old_face, new_faces) => {\n\t// for each of the vertices (only the vertices involved in this split),\n\t// use the new faces_vertices data (built in the previous method) to get\n\t// a list of the new faces to be added to this vertex's vertices_faces.\n\tconst vertices_replacement_faces = {};\n\tnew_faces\n\t\t.forEach(f => graph.faces_vertices[f]\n\t\t\t.forEach(v => {\n\t\t\t\tif (!vertices_replacement_faces[v]) {\n\t\t\t\t\tvertices_replacement_faces[v] = [];\n\t\t\t\t}\n\t\t\t\tvertices_replacement_faces[v].push(f);\n\t\t\t}));\n\t// these vertices need updating\n\tgraph.faces_vertices[old_face].forEach(v => {\n\t\tconst index = graph.vertices_faces[v].indexOf(old_face);\n\t\tconst replacements = vertices_replacement_faces[v];\n\t\tif (index === -1 || !replacements) {\n\t\t\tthrow new Error(Messages.convexFace);\n\t\t}\n\t\tgraph.vertices_faces[v].splice(index, 1, ...replacements);\n\t});\n};\n/**\n * @description called near the end of the split_convex_face method.\n * update the \"edges_faces\" array for every edge involved.\n * figure out where the old_face's index is in each edges_faces array,\n * figure out which of the new faces (or both) need to be added and\n * substitute the old index with the new face's index/indices.\n */\nconst update_edges_faces = (graph, old_face, new_edge, new_faces) => {\n\t// for each of the edges (only the edges involved in this split),\n\t// use the new faces_edges data (built in the previous method) to get\n\t// a list of the new faces to be added to this edge's edges_faces.\n\t// most will be length of 1, except the edge which split the face will be 2.\n\tconst edges_replacement_faces = {};\n\tnew_faces\n\t\t.forEach(f => graph.faces_edges[f]\n\t\t\t.forEach(e => {\n\t\t\t\tif (!edges_replacement_faces[e]) { edges_replacement_faces[e] = []; }\n\t\t\t\tedges_replacement_faces[e].push(f);\n\t\t\t}));\n\t// these edges need updating\n\tconst edges = [...graph.faces_edges[old_face], new_edge];\n\tedges.forEach(e => {\n\t\t// these are the faces which should be inserted into this edge's\n\t\t// edges_faces array, we just need to find the old index to replace.\n\t\tconst replacements = edges_replacement_faces[e];\n\t\t// basically rewriting .indexOf(), but supporting multiple results.\n\t\t// these will be the indices containing a reference to the old face.\n\t\tconst indices = [];\n\t\tfor (let i = 0; i < graph.edges_faces[e].length; i += 1) {\n\t\t\tif (graph.edges_faces[e][i] === old_face) { indices.push(i); }\n\t\t}\n\t\tif (indices.length === 0 || !replacements) {\n\t\t\tthrow new Error(Messages.convexFace);\n\t\t}\n\t\t// \"indices\" will most often be length 1, except for the one edge which\n\t\t// was added which splits the face in half. the previous methods which\n\t\t// did this gave that edge two references both to the same face, knowing\n\t\t// that here we will replace both references to the pair of the new\n\t\t// faces which the edge now divides.\n\t\t// remove the old indices.\n\t\tindices.reverse().forEach(index => graph.edges_faces[e].splice(index, 1));\n\t\t// in both cases when \"indices\" is length 1 or 2, get just one index\n\t\t// at which to insert the new reference(s).\n\t\tconst index = indices[indices.length - 1];\n\t\tgraph.edges_faces[e].splice(index, 0, ...replacements);\n\t});\n};\n/**\n * @description one face was removed and two faces put in its place.\n * regarding the faces_faces array, updates need to be made to the two\n * new faces, as well as all the previously neighboring faces of\n * the removed face.\n * @param {FOLD} graph\n * @param {number} old_face\n * @param {number[]} new_faces\n */\nconst update_faces_faces = ({ faces_vertices, faces_faces }, old_face, new_faces) => {\n\t// this is presuming that new_faces have their updated faces_vertices by now\n\tconst incident_faces = faces_faces[old_face];\n\tconst new_faces_vertices = new_faces.map(f => faces_vertices[f]);\n\t// for each of the incident faces (to the old face), set one of two\n\t// indices, one of the two new faces. this is the new incident face.\n\tconst incident_face_face = incident_faces.map(f => {\n\t\tif (f === undefined || f === null) { return undefined; }\n\t\tconst incident_face_vertices = faces_vertices[f];\n\t\tconst score = [0, 0];\n\t\tfor (let n = 0; n < new_faces_vertices.length; n += 1) {\n\t\t\tlet count = 0;\n\t\t\tfor (let j = 0; j < incident_face_vertices.length; j += 1) {\n\t\t\t\tif (new_faces_vertices[n].indexOf(incident_face_vertices[j]) !== -1) {\n\t\t\t\t\tcount += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\tscore[n] = count;\n\t\t}\n\t\tif (score[0] >= 2) { return new_faces[0]; }\n\t\tif (score[1] >= 2) { return new_faces[1]; }\n\t\treturn undefined;\n\t});\n\t// prepare the new faces' face_faces empty arrays, filled with one\n\t// face, the opposite of the pair of the new faces.\n\tnew_faces.forEach((f, i, arr) => {\n\t\tfaces_faces[f] = [arr[(i + 1) % new_faces.length]];\n\t});\n\t// 2 things, fill the new face's arrays and update each of the\n\t// incident faces to point to the correct of the two new faces.\n\tincident_faces.forEach((f, i) => {\n\t\tif (f === undefined || f === null) { return; }\n\t\tfor (let j = 0; j < faces_faces[f].length; j += 1) {\n\t\t\tif (faces_faces[f][j] === old_face) {\n\t\t\t\tfaces_faces[f][j] = incident_face_face[i];\n\t\t\t\tfaces_faces[incident_face_face[i]].push(f);\n\t\t\t}\n\t\t}\n\t});\n};\n\n/**\n * @description divide a **convex** face into two polygons with a straight line cut.\n * if the line ends exactly along existing vertices, they will be\n * used, otherwise, new vertices will be added (splitting edges).\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} face index of face to split\n * @param {VecLine2} line with a \"vector\" and an \"origin\" component\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {object|undefined} a summary of changes to the FOLD object,\n *  or undefined if no change (no intersection).\n */\nexport const splitFaceWithLine = (graph, face, line, epsilon) => {\n\t// survey face for any intersections which cross directly over a vertex\n\tconst intersect = intersectConvexFaceLine(graph, face, line, epsilon);\n\t// if no intersection exists, return undefined.\n\tif (intersect === undefined) { return undefined; }\n\t// this result will be appended to (vertices, edges) and returned by this method.\n\tconst result = split_at_intersections(graph, intersect);\n\t// this modifies the graph by only adding an edge between existing vertices\n\tresult.edges.new = rebuild_edge(graph, face, result.vertices);\n\t// update all changes to vertices and edges (anything other than faces).\n\tupdate_vertices_vertices(graph, result.edges.new);\n\tupdate_vertices_edges(graph, result.edges.new);\n\t// done: vertices_coords, vertices_edges, vertices_vertices, edges_vertices\n\t// at this point the graph is once again technically valid, except\n\t// the face data is a little weird as one face is ignoring the newly-added\n\t// edge that cuts through it.\n\tconst faces = build_faces(graph, face, result.vertices);\n\t// update all arrays having to do with face data\n\tupdate_vertices_faces(graph, face, faces);\n\tupdate_edges_faces(graph, face, result.edges.new, faces);\n\tupdate_faces_faces(graph, face, faces);\n\t// remove old data\n\tconst faces_map = remove(graph, \"faces\", [face]);\n\t// the graph is now complete, however our return object needs updating.\n\t// shift our new face indices since these relate to the graph before remove().\n\tfaces.forEach((_, i) => { faces[i] = faces_map[faces[i]]; });\n\t// we had to run \"remove\" with the new faces added. to return the change info,\n\t// we need to adjust the map to exclude these faces.\n\tfaces_map.splice(-2);\n\t// replace the \"undefined\" in the map with the two new edge indices.\n\n\t/** @type {(number|number[])[]} */\n\tconst facesMap = faces_map.slice();\n\t// set the location of the old face in the map to be the new faces\n\tfacesMap[face] = faces;\n\n\tresult.faces = {\n\t\tmap: facesMap,\n\t\tnew: faces,\n\t\tremove: face,\n\t};\n\treturn result;\n};\n"
  },
  {
    "path": "src/graph/fold/foldGraph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeL,\n\tincludeR,\n\tincludeS,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../../math/convert.js\";\nimport {\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tclone,\n} from \"../../general/clone.js\";\nimport {\n\tassignmentFlatFoldAngle,\n\tinvertAssignment,\n} from \"../../fold/spec.js\";\nimport {\n\tmakeVerticesCoordsFolded,\n} from \"../vertices/folded.js\";\nimport {\n\tfaceContainingPoint,\n} from \"../faces/facePoint.js\";\nimport {\n\tmakeFacesWinding,\n} from \"../faces/winding.js\";\nimport {\n\tsplitGraphWithLineAndPoints,\n} from \"../split/splitGraph.js\";\nimport {\n\ttransferPointInFaceBetweenGraphs,\n} from \"../transfer.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../make/edgesFaces.js\";\nimport {\n\tmakeEdgesFoldAngle,\n} from \"../make/edgesFoldAngle.js\";\nimport {\n\trecalculatePointAlongEdge,\n\treassignCollinearEdges,\n\tupdateFaceOrders,\n} from \"./general.js\";\n\n/**\n * @typedef FoldGraphEvent\n * @type {{\n *   edges?: {\n *     new: number[],\n *     map: (number|number[])[],\n *     reassigned: number[],\n *   },\n *   faces?: {\n *     new: number[],\n *     map: (number|number[])[],\n *   },\n * }}\n * @description an object which summarizes the changes to the graph.\n */\n\n/**\n * @description Crease a fold line/ray/segment through a folded origami model.\n * This method takes in and returns a crease pattern but performs the fold\n * on the folded form; this approach maintains better precision especially\n * in the case of repeated calls to fold an origami model.\n * @param {FOLD} graph a FOLD object, in creasePattern form, modified in place\n * @param {VecLine2} foldLine a fold line\n * @param {Function} [lineDomain=includeL] a domain function\n * characterizing the line into a line, ray, or segment\n * @param {[number, number][]} interiorPoints in the case of a ray or segement,\n * supply the endpoint(s) here.\n * @param {string} [assignment=\"V\"] the assignment to be applied to the\n * intersected faces with counter-clockwise winding. Clockwise-wound faces\n * will get the opposite assignment\n * @param {number} [foldAngle] the fold angle to be applied, similarly as\n * the assignment\n * @param {[number, number][]|[number, number, number][]} vertices_coordsFolded\n * a copy of the vertices_coords, in folded form\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FoldGraphEvent} an object summarizing the changes to the graph\n */\nexport const foldGraph = (\n\tgraph,\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tassignment = \"V\",\n\tfoldAngle = undefined,\n\tvertices_coordsFolded = undefined,\n\tepsilon = EPSILON,\n) => {\n\t// if the user asks for a foldAngle, but edges_foldAngle array does not exist,\n\t// we have to explicitly add it otherwise it will be skipped later on.\n\tif (foldAngle !== undefined && !graph.edges_foldAngle && graph.edges_assignment) {\n\t\tgraph.edges_foldAngle = makeEdgesFoldAngle(graph);\n\t}\n\tif (!graph.edges_faces) {\n\t\tgraph.edges_faces = makeEdgesFacesUnsorted(graph);\n\t}\n\n\t// if user only specifies assignment, fill in the (flat) fold angle for them\n\tif (foldAngle === undefined) {\n\t\tfoldAngle = assignmentFlatFoldAngle[assignment] || 0;\n\t}\n\n\tif (vertices_coordsFolded === undefined) {\n\t\tconst rootFace = faceContainingPoint(graph, origin, vector);\n\t\tvertices_coordsFolded = makeVerticesCoordsFolded(graph, [rootFace]);\n\t}\n\n\t// Only M and V will exchange. all others, this will be the same assignment\n\tconst oppositeAssignment = invertAssignment(assignment);\n\tconst oppositeFoldAngle = foldAngle === 0 ? 0 : -foldAngle;\n\n\t// we will run the method on the same graph, but swap out the vertices_coords.\n\t// run the splitGraph method on the folded form but then swap out the coords\n\t// for the crease pattern coords once finished. backup the cp coords here.\n\tconst vertices_coordsCP = clone(graph.vertices_coords);\n\n\t// the split operation will happen to the folded graph.\n\t// the vertices_coords will be modified in place, so, create a copy in case\n\t// the user is passing in an argument they don't want modified.\n\tObject.assign(graph, { vertices_coords: clone(vertices_coordsFolded) });\n\n\t// split all edges and faces that are crossed by our line, and place\n\t// new vertices at the split edges, and inside faces in the case of segment.\n\tconst splitGraphResult = splitGraphWithLineAndPoints(\n\t\tgraph,\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tinteriorPoints,\n\t\tepsilon,\n\t);\n\n\t// new faces, used for the return object, and used to update faceOrders\n\tconst newFaces = Array.from(new Set(splitGraphResult.edges.new\n\t\t.flatMap(e => graph.edges_faces[e])));\n\n\t// now that the split operation is complete and new faces have been built,\n\t// capture the winding of the faces while still in folded form.\n\tconst faces_winding = makeFacesWinding(graph);\n\n\t// we need to hold onto these for the upcoming point-transfer methods.\n\t// vertices_coords from the crease pattern and folded form now differ\n\t// in length, the folded form contain additional vertices_coords at the end,\n\t// however, during the parts that do overlap, the vertices match 1:1.\n\t// (albeit, folded and cp coordinates are different, of course)\n\tconst vertices_coordsFoldedNew = clone(graph.vertices_coords);\n\n\t// reassign the crease pattern's vertices back onto the graph. it's likely\n\t// that the graph is now invalid, as the split created new vertices which\n\t// are no longer here, but this is only temporary, in the upcoming section\n\t// we will rebuild and set these new vertices back into the crease pattern\n\t// space using the intersection information that made them.\n\tObject.assign(graph, { vertices_coords: vertices_coordsCP });\n\n\t// build a copy of the folded form for the transfer method\n\tconst foldedForm = {\n\t\t...graph,\n\t\tvertices_coords: vertices_coordsFoldedNew,\n\t};\n\n\t// at this point, the crease pattern coords have been returned to the graph,\n\t// aside from the additional vertices that were created during\n\t// the splitFace / splitEdge methods. Currently, these are still in\n\t// the folded-form space. the splitGraph method result contains\n\t// information on how these points were made in folded form space,\n\t// transfer these points into cp space, via the paramters that created them.\n\tconst splitGraphVerticesSource = splitGraphResult.vertices.source\n\t\t.map((intersect, vertex) => ({ ...intersect, vertex }));\n\n\t// these points lie somewhere in the inside of a face. use trilateration\n\t// to move the point from the same location in the folded face to the cp face.\n\tsplitGraphVerticesSource\n\t\t.map(el => (\"point\" in el && \"face\" in el && \"faces\" in el && \"vertex\" in el\n\t\t\t? el\n\t\t\t: undefined))\n\t\t.filter(a => a !== undefined)\n\t\t// .forEach(({ point, face, faces, vertex }) => {\n\t\t.forEach(({ point, faces, vertex }) => {\n\t\t\t// \"face\" relates to the graph before splitGraphWithLineAndPoints was called.\n\t\t\t// \"faces\" indices relate to the new graph, it will have one or two indices.\n\t\t\t// which of the two face indices should we use?\n\t\t\t// console.log(\"transfer in face\", point, face, vertex);\n\t\t\tgraph.vertices_coords[vertex] = transferPointInFaceBetweenGraphs(\n\t\t\t\tfoldedForm,\n\t\t\t\tgraph,\n\t\t\t\tfaces[0], // todo, ensure that this is okay\n\t\t\t\tpoint,\n\t\t\t);\n\t\t});\n\n\t// these points were made along an edge, instead of using trilateration,\n\t// we can use the edge vector intersection parameter for more precision.\n\tsplitGraphVerticesSource\n\t\t.map(el => (\"vertices\" in el && \"vertex\" in el && \"b\" in el ? el : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(({ b, vertices, vertex }) => {\n\t\t\tgraph.vertices_coords[vertex] = recalculatePointAlongEdge(\n\t\t\t\tvertices.map(v => graph.vertices_coords[v]).map(resize2),\n\t\t\t\tb,\n\t\t\t);\n\t\t});\n\n\t// the result of calling splitGraph contains a list of all new edges that\n\t// were created, each edge contains a reference to the face(s) it lies inside:\n\t// \"face\": the index of the edge's face before the graph was modified\n\t// \"faces\": the indices of the edge's new faces in the graph after being split\n\t// use \"faces\" to grab the edge's face, look up the winding of this face, and\n\t// assign either the assignment or the inverted assignment accordingly.\n\tconst edgesAttributes = splitGraphResult.edges.source\n\t\t.map(({ faces }) => ({\n\t\t\tassign: faces_winding[faces[0]] ? assignment : oppositeAssignment,\n\t\t\tangle: faces_winding[faces[0]] ? foldAngle : oppositeFoldAngle,\n\t\t}));\n\n\t// only apply the assignment and fold angle if the graph contains these\n\t// arrays, this way, a simple flat-folded graph won't be forced to\n\t// include edges_foldAngle if it is unnecessary.\n\tif (graph.edges_assignment) {\n\t\tedgesAttributes.forEach(({ assign }, edge) => {\n\t\t\tgraph.edges_assignment[edge] = assign;\n\t\t});\n\t}\n\tif (graph.edges_foldAngle) {\n\t\tedgesAttributes.forEach(({ angle }, edge) => {\n\t\t\tgraph.edges_foldAngle[edge] = angle;\n\t\t});\n\t}\n\n\t// collinear edges should be dealt in this way: folded edges can be ignored,\n\t// flat edges which lie collinear to the fold line must be folded,\n\t// these edges were missed in the edge construction and assignment inside\n\t// \"splitFace\", because these edges already existed.\n\tconst edgesReassigned = reassignCollinearEdges(\n\t\tgraph,\n\t\t{ assignment, foldAngle, oppositeAssignment, oppositeFoldAngle },\n\t\tfaces_winding,\n\t\tsplitGraphResult,\n\t);\n\n\tupdateFaceOrders(\n\t\tgraph,\n\t\t{ ...graph, vertices_coords: vertices_coordsFoldedNew },\n\t\t{ vector, origin },\n\t\tfoldAngle,\n\t\tfaces_winding,\n\t\t[...splitGraphResult.edges.new, ...edgesReassigned],\n\t\tnewFaces,\n\t);\n\n\treturn {\n\t\tedges: {\n\t\t\tmap: splitGraphResult.edges.map,\n\t\t\tnew: splitGraphResult.edges.new,\n\t\t\treassigned: edgesReassigned,\n\t\t},\n\t\tfaces: {\n\t\t\tmap: splitGraphResult.faces.map,\n\t\t\tnew: newFaces,\n\t\t},\n\t};\n};\n\n/**\n* @description Crease a fold line through a folded origami model.\n* This method takes in and returns a crease pattern but performs the fold\n* on the folded form; this approach maintains better precision especially\n* in the case of repeated calls to fold an origami model.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line the fold line\n * @param {string} [assignment=\"V\"]\n * @param {number} [foldAngle]\n * @param {[number, number][]|[number, number, number][]} [vertices_coordsFolded]\n * @param {number} [epsilon=1e-6]\n * @returns {FoldGraphEvent} an object summarizing the changes to the graph\n */\nexport const foldLine = (\n\tgraph,\n\tline,\n\tassignment = \"V\",\n\tfoldAngle = undefined,\n\tvertices_coordsFolded = undefined,\n\tepsilon = EPSILON,\n) => (\n\tfoldGraph(\n\t\tgraph,\n\t\tline,\n\t\tincludeL,\n\t\t[],\n\t\tassignment,\n\t\tfoldAngle,\n\t\tvertices_coordsFolded,\n\t\tepsilon,\n\t));\n\n/**\n* @description Crease a fold ray through a folded origami model.\n* This method takes in and returns a crease pattern but performs the fold\n* on the folded form; this approach maintains better precision especially\n* in the case of repeated calls to fold an origami model.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} ray the fold line as a ray\n * @param {string} [assignment=\"V\"]\n * @param {number} [foldAngle]\n * @param {[number, number][]|[number, number, number][]} [vertices_coordsFolded]\n * @param {number} [epsilon=1e-6]\n * @returns {FoldGraphEvent} an object summarizing the changes to the graph\n */\nexport const foldRay = (\n\tgraph,\n\tray,\n\tassignment = \"V\",\n\tfoldAngle = undefined,\n\tvertices_coordsFolded = undefined,\n\tepsilon = EPSILON,\n) => (\n\tfoldGraph(\n\t\tgraph,\n\t\tray,\n\t\tincludeR,\n\t\t[ray.origin],\n\t\tassignment,\n\t\tfoldAngle,\n\t\tvertices_coordsFolded,\n\t\tepsilon,\n\t));\n\n/**\n* @description Crease a fold segment through a folded origami model.\n* This method takes in and returns a crease pattern but performs the fold\n* on the folded form; this approach maintains better precision especially\n* in the case of repeated calls to fold an origami model.\n * @param {FOLD} graph a FOLD object\n * @param {[[number, number], [number, number]]} segment the fold segment\n * @param {string} [assignment=\"V\"]\n * @param {number} [foldAngle]\n * @param {[number, number][]|[number, number, number][]} [vertices_coordsFolded]\n * @param {number} [epsilon=1e-6]\n * @returns {FoldGraphEvent} an object summarizing the changes to the graph\n */\nexport const foldSegment = (\n\tgraph,\n\tsegment,\n\tassignment = \"V\",\n\tfoldAngle = undefined,\n\tvertices_coordsFolded = undefined,\n\tepsilon = EPSILON,\n) => (\n\tfoldGraph(\n\t\tgraph,\n\t\tpointsToLine2(segment[0], segment[1]),\n\t\tincludeS,\n\t\tsegment,\n\t\tassignment,\n\t\tfoldAngle,\n\t\tvertices_coordsFolded,\n\t\tepsilon,\n\t));\n"
  },
  {
    "path": "src/graph/fold/foldGraphIntoSegments.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeL,\n} from \"../../math/compare.js\";\nimport {\n\tscale2,\n\tadd2,\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tinvertAssignment,\n} from \"../../fold/spec.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../make/facesEdges.js\";\nimport {\n\tmakeVerticesCoordsFlatFolded,\n} from \"../vertices/folded.js\";\nimport {\n\tmakeFacesWinding,\n} from \"../faces/winding.js\";\nimport {\n\tfaceContainingPoint,\n} from \"../faces/facePoint.js\";\nimport {\n\tintersectLine,\n} from \"../intersect.js\";\nimport {\n\tedgesToLines2,\n} from \"../edges/lines.js\";\n\n/**\n * @description Given a flat-foldable crease pattern, perform a fold through\n * its folded form and return a list of new crease edges as edges in the\n * crease pattern space.\n * This method does not modify the input graph, it returns the fold line\n * as a list of segments, mapped to the face inside which they lie.\n * @param {FOLD} graph a FOLD object in crease pattern form\n * @param {VecLine2} line a fold line\n * @param {string} assignment the segment's assignment through the first face\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   intersections: (FaceEdgeEvent|FaceVertexEvent)[],\n *   assignment: string,\n *   points: [number, number][],\n * }[]} For each intersected face, a new segment object:\n * - intersections: information about the intersection events\n * - assignment: the assignment of the new segment\n * - points: the new segment's two endpoints\n */\nexport const foldGraphIntoSegments = ({\n\tvertices_coords, edges_vertices, edges_foldAngle, edges_assignment,\n\tfaces_vertices, faces_edges, faces_faces,\n}, { vector, origin }, assignment = \"V\", epsilon = EPSILON) => {\n\tif (!faces_edges) {\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\n\t// the face under the point's crease will get the assignment in the method\n\t// input parameter, and all other creases will be valley/mountain accordingly\n\tconst startFace = faceContainingPoint(\n\t\t{ vertices_coords, faces_vertices },\n\t\torigin,\n\t\tvector,\n\t);\n\n\t// Only M and V will exchange. all others, this will be the same assignment\n\tconst oppositeAssignment = invertAssignment(assignment);\n\n\t// this assumes the model is flat folded.\n\t// another approach would be to check for any non-flat edges, fold a 3D\n\t// graph, then find all faces that are in the same plane as startFace.\n\tconst vertices_coordsFolded = makeVerticesCoordsFlatFolded({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_foldAngle,\n\t\tedges_assignment,\n\t\tfaces_vertices,\n\t\tfaces_faces,\n\t}, [startFace]);\n\n\t// edge line data for the crease pattern state, needed to remap the edge\n\t// intersections, which were calculated in the folded state, into points\n\t// in the crease pattern state.\n\tconst edges_line = edgesToLines2({ vertices_coords, edges_vertices });\n\n\t// note that this copy of faces_winding will be forced to be the case that\n\t// our startFace is \"true\" instead of T/F based on face winding direction.\n\tconst faces_winding = makeFacesWinding({\n\t\tvertices_coords: vertices_coordsFolded,\n\t\tfaces_vertices,\n\t});\n\tif (!faces_winding[startFace]) {\n\t\tfaces_winding.forEach((w, i) => { faces_winding[i] = !w; });\n\t}\n\n\tconst { faces } = intersectLine(\n\t\t{ vertices_coords: vertices_coordsFolded, edges_vertices, faces_vertices, faces_edges },\n\t\t{ vector, origin },\n\t\tincludeL,\n\t\tepsilon,\n\t);\n\n\t// only keep simple, convex faces\n\tfaces.forEach((arr, f) => { if (arr.length !== 2) { delete faces[f]; } });\n\n\t/**\n\t * @param {({ edge: number, a: number, b: number, point: [number, number], vertex?: never }\n\t *   | { a: number, vertex: number, b?: never, edge?: never })} el\n\t * @returns {[number, number]} a point in 2D\n\t */\n\tconst remapPoint = ({ vertex, edge, b }) => (vertex !== undefined\n\t\t? resize2(vertices_coords[vertex])\n\t\t: add2(scale2(edges_line[edge].vector, b), edges_line[edge].origin));\n\n\t// the return object will be, for every intersected face,\n\t// an object which describes a new segment, including:\n\t// - edges: which two edges were intersected\n\t// - assignment: the assignment of the new segment\n\t// - points: the new segment's two endpoints\n\t// Note: the points will be remapped back into crease pattern space,\n\t// as all intersection data was calculated using the folded form's vertices.\n\treturn faces.map((intersections, f) => ({\n\t\tintersections,\n\t\tassignment: faces_winding[f] ? assignment : oppositeAssignment,\n\t\tpoints: intersections.map(remapPoint),\n\t}));\n};\n"
  },
  {
    "path": "src/graph/fold/foldGraphIntoSubgraph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeL,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../../math/convert.js\";\nimport {\n\tadd2,\n\tscale2,\n} from \"../../math/vector.js\";\nimport {\n\tassignmentFlatFoldAngle,\n\tinvertAssignment,\n} from \"../../fold/spec.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../make/edgesFaces.js\";\nimport {\n\tsplitLineIntoEdges,\n} from \"../split/splitLine.js\";\nimport {\n\tmakeFacesWinding,\n} from \"../faces/winding.js\";\nimport {\n\ttransferPointInFaceBetweenGraphs,\n} from \"../transfer.js\";\n\nconst transferPointOnEdgeBetweenGraphs = (to, edge, parameter) => {\n\tconst edgeSegment = to.edges_vertices[edge]\n\t\t.map(v => to.vertices_coords[v]);\n\tconst edgeLine = pointsToLine2(edgeSegment[0], edgeSegment[1]);\n\treturn add2(edgeLine.origin, scale2(edgeLine.vector, parameter));\n};\n\n/**\n * @description Transfer a point from the \"from\" graph space into the\n * \"to\" graph space. The point object comes from the \"splitLineIntoEdges\"\n * method, which returns information about the point's construction which\n * we can use to re-parameterize into the other graph's space.\n * @param {FOLD} from a FOLD object\n * @param {FOLD} to a FOLD object\n * @param {object} point a point, the result of calling splitLineIntoEdges\n * @returns {number[]} a point\n */\nexport const transferPoint = (from, to, { vertex, edge, face, point, b }) => {\n\tif (vertex !== undefined) {\n\t\treturn to.vertices_coords[vertex];\n\t}\n\tif (edge !== undefined) {\n\t\treturn transferPointOnEdgeBetweenGraphs(to, edge, b);\n\t}\n\tif (face !== undefined) {\n\t\treturn transferPointInFaceBetweenGraphs(from, to, face, point);\n\t}\n\tthrow new Error(\"transferPoint() failed\");\n};\n\n/**\n * @description This creates an edge-and-vertex-only graph (no faces) from a\n * creasePattern and foldedForm graph, given a fold line, perform a fold\n * operation through the folded form, generating a separate (ish) graph\n * that contains the fold line as a series of edges and vertices through the\n * crease pattern. The vertices are generated from the foldedForm but mapped\n * back into creasePattern space. Edge assignments and fold angles will reflect\n * mountain/valley accordingly, or you can choose to make all flat/unassigned.\n * The result's component arrays will contain holes where existing components\n * from the crease pattern are indexed, all new components will begin indexing\n * after these, so that there are no overlapping component indices.\n * The reason for this is that the result graph may create edges which use\n * existing vertices from the creasePattern graph (instead of creating\n * duplicate vertices in the same location), so, these edges_vertices will\n * contain indices which map to the vertices_ from the crease pattern graph.\n * In this regard, the result graph is still dependent upon the source graph.\n * The intention is that the user can easily merge the two graphs into one\n * (via merge methods, mergeArraysWithHoles).\n * If you do merge, make sure to list in order (cp, newGraph) because the\n * newGraph will have indices that exist in the cp that need to be overwritten\n * (for example, in edges_assignment, some assignments need to be overwritten).\n * @param {FOLD} cp a FOLD object in creasePattern\n * @param {FOLD} folded a FOLD object in foldedForm\n * @param {VecLine2} line a fold line\n * @param {function} [lineDomain=() => true] the function which characterizes\n * \"line\" parameter into a line, ray, or segment.\n * @param {[number, number][]} [interiorPoints=[]] in the case of a ray or segment,\n * place in here the endpoint(s), and they will be included in the result.\n * @param {string} [assignment=\"V\"] what is the assignment of the new crease\n * @param {number} foldAngle the fold angle of the new crease\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\nexport const foldGraphIntoSubgraph = (\n\tcp,\n\tfolded,\n\tline,\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tassignment = \"V\",\n\tfoldAngle = undefined,\n\tepsilon = EPSILON,\n) => {\n\t// if user only specifies assignment, fill in the (flat) fold angle for them\n\tif (foldAngle === undefined) {\n\t\tfoldAngle = assignmentFlatFoldAngle[assignment] || 0;\n\t}\n\n\t// Only M and V will exchange. all others, this will be the same assignment\n\tconst oppositeAssignment = invertAssignment(assignment);\n\tconst oppositeFoldAngle = foldAngle === 0 ? 0 : -foldAngle;\n\tconst faces_winding = makeFacesWinding(folded);\n\n\tconst {\n\t\tvertices,\n\t\tedges_vertices,\n\t\tedges_collinear,\n\t\tedges_face,\n\t} = splitLineIntoEdges(\n\t\tfolded,\n\t\tline,\n\t\tlineDomain,\n\t\tinteriorPoints,\n\t\tepsilon,\n\t);\n\n\t// splitSegmentWithGraph created new points in the foldedForm coordinate\n\t// space. we need to transfer these to their respective position in the\n\t// crease pattern space. 2 different methods depending on how the point was made\n\tconst vertices_coords = vertices\n\t\t.map(pointInfo => transferPoint(folded, cp, pointInfo));\n\n\t// get the first face in these instances\n\tconst edges_assignment = edges_face\n\t\t.map((face) => (faces_winding[face] ? assignment : oppositeAssignment));\n\tconst edges_foldAngle = edges_face\n\t\t.map((face) => (faces_winding[face] ? foldAngle : oppositeFoldAngle));\n\n\t// this is the crease pattern's edges_faces (not edges_face from above)\n\tconst edges_faces = cp.edges_faces ? cp.edges_faces : makeEdgesFacesUnsorted(cp);\n\n\t// collinear edges should be dealt in this way:\n\t// if the edge is alredy a M or V, we can ignore it\n\t// if the edge is a F or U, we need to fold it, and we need to know which\n\t// direction, this is done by checking one of its two neighboring faces\n\t// (edges_faces), they should be the same winding, so just grab one.\n\tconst reassignable = { F: true, f: true, U: true, u: true };\n\n\tedges_collinear\n\t\t.map((collinear, e) => (collinear ? e : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(edge => {\n\t\t\tif (!reassignable[edges_assignment[edge]]) { return; }\n\t\t\tconst face = edges_faces[edge]\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.shift();\n\t\t\tconst winding = faces_winding[face];\n\t\t\tedges_assignment[edge] = winding ? assignment : oppositeAssignment;\n\t\t\tedges_foldAngle[edge] = winding ? foldAngle : oppositeFoldAngle;\n\t\t});\n\n\treturn {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t};\n};\n"
  },
  {
    "path": "src/graph/fold/general.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tedgeFoldAngleIsFlatFolded,\n} from \"../../fold/spec.js\";\nimport {\n\tepsilonEqual,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../../math/convert.js\";\nimport {\n\tscale2,\n\tcross2,\n\tadd2,\n\tsubtract2,\n\tresize2,\n\taverage2,\n} from \"../../math/vector.js\";\nimport {\n\tinvertFlatMap,\n} from \"../maps.js\";\n\n/**\n * @param {[number, number][]} points the points which make up the edge\n * in order, where point[0] is at parameter 0, point[1] is at parameter 1.\n * @param {number} parameter\n * @returns {[number, number]} a recalculated point\n */\nexport const recalculatePointAlongEdge = (points, parameter) => {\n\tconst edgeLine = pointsToLine2(points[0], points[1]);\n\treturn add2(edgeLine.origin, scale2(edgeLine.vector, parameter));\n};\n\n/**\n * @param {FOLD} graph a FOLD object, with edges_faces among other arrays\n * @param {object} assignment info about assignment\n * @param {boolean[]} faces_winding the winding direction for each face\n * @param {{ vertices?: { intersect: number[] } }} splitGraphResult\n * @returns {number[]} a list of edge indices which were reassigned\n */\nexport const reassignCollinearEdges = (\n\t{ edges_vertices, edges_faces, edges_assignment, edges_foldAngle },\n\t{ assignment, foldAngle, oppositeAssignment, oppositeFoldAngle },\n\tfaces_winding,\n\tsplitGraphResult,\n) => {\n\t// using the overlapped vertices, make a list of edges collinear to the line\n\t// these (old) indices will match with the graph from its original state.\n\tconst verticesCollinear = splitGraphResult.vertices.intersect\n\t\t.map(v => v !== undefined);\n\n\t// these are new edge indices, relating to the graph after modification.\n\tconst collinearEdges = edges_vertices\n\t\t.map(verts => verticesCollinear[verts[0]] && verticesCollinear[verts[1]])\n\t\t.map((collinear, e) => (collinear ? e : undefined))\n\t\t.filter(a => a !== undefined);\n\n\t// This upcoming section can be done without edges_assignments.\n\t// Now, from the list of collinear edges, we need to filter out the ones to\n\t// ignore from the ones we need to change. One way of doing this, which works\n\t// in 2D at least, is to check the adjacent faces' windings, if they are the\n\t// same winding (assuming they lie in the same plane) the crease between them\n\t// is flat, and should become folded, in which case we can simply take either\n\t// of its adjacent faces to know which assignment direction to assign.\n\tconst reassignableCollinearEdges = collinearEdges\n\t\t.map(edge => ({\n\t\t\tedge,\n\t\t\tfaces: edges_faces[edge].filter(a => a !== undefined),\n\t\t}))\n\t\t.filter(({ faces }) => faces.length === 2)\n\t\t.filter(({ faces: [f0, f1] }) => faces_winding[f0] === faces_winding[f1]);\n\n\treassignableCollinearEdges.forEach(({ edge, faces }) => {\n\t\tconst winding = faces.map(face => faces_winding[face]).shift();\n\t\tedges_assignment[edge] = winding ? assignment : oppositeAssignment;\n\t\tedges_foldAngle[edge] = winding ? foldAngle : oppositeFoldAngle;\n\t});\n\n\t// list of edge indices which were reassigned\n\treturn reassignableCollinearEdges.map(({ edge }) => edge);\n};\n\n/**\n * @param {number} f0 face index\n * @param {number} f1 face index\n * @param {string} assignment\n * @returns {[number, number, number]|undefined}\n */\nconst adjacentFacesOrdersAssignments = (f0, f1, assignment) => {\n\tswitch (assignment) {\n\tcase \"V\":\n\tcase \"v\": return [f0, f1, 1];\n\tcase \"M\":\n\tcase \"m\": return [f0, f1, -1];\n\tdefault: return undefined;\n\t}\n};\n\n/**\n * @param {number} f0 face index\n * @param {number} f1 face index\n * @param {number} foldAngle\n * @returns {[number, number, number]|undefined}\n */\nconst adjacentFacesOrdersFoldAngles = (f0, f1, foldAngle) => {\n\tif (epsilonEqual(foldAngle, 180)) { return [f0, f1, 1]; }\n\tif (epsilonEqual(foldAngle, -180)) { return [f0, f1, -1]; }\n\treturn undefined;\n};\n\n/**\n * @description all pairs of faces are adjacent faces, so they should have\n * similar windings if unfolded. so there are only two arrangements:\n * V: both face's normals point towards the other face. (+1 order)\n * M: both face's normals point away from the other face. (-1 order)\n * in both cases, it doesn't matter which face comes first with respect\n * to the +1 or -1 ordering.\n * @param {FOLD} graph a FOLD object, with edges_faces among other arrays\n * @param {number[]} newEdges a list of new edges\n * @returns {[number, number, number][]} new faceOrders\n */\nexport const makeNewFlatFoldFaceOrders = ({\n\tedges_faces, edges_assignment, edges_foldAngle,\n}, newEdges) => {\n\tconst edges = newEdges.filter(e => edges_faces[e].length === 2);\n\tconst edgesAdjacentFaces = edges.map(e => edges_faces[e]);\n\n\tif (edges_assignment) {\n\t\tconst assignments = edges.map(e => edges_assignment[e]);\n\t\treturn edgesAdjacentFaces\n\t\t\t.map(([f0, f1], i) => adjacentFacesOrdersAssignments(f0, f1, assignments[i]))\n\t\t\t.filter(a => a !== undefined);\n\t}\n\n\tif (edges_foldAngle) {\n\t\tconst angles = edges.map(e => edges_foldAngle[e]);\n\t\treturn edgesAdjacentFaces\n\t\t\t.map(([f0, f1], i) => adjacentFacesOrdersFoldAngles(f0, f1, angles[i]))\n\t\t\t.filter(a => a !== undefined);\n\t}\n\n\treturn [];\n};\n\n/**\n * @description This method is meant to accompany foldGraph, or any operation\n * which divides many overlapping faces in the same graph. During the splitting\n * splitFace() creates a bunch of new faceOrders from all the old faces\n * corresponding to the new faces (new faces count twice as many as old).\n * This creates a bunch of faceOrders between faces which no longer overlap\n * and can be easily calculated since we know the dividing line, compare\n * the center of the faces to find which side of the line they lie, for face\n * pairs which lie on opposite sides, return this index in the faceOrders array.\n * @param {FOLD} graph a FOLD object with vertices_coords in the same state\n * when the split occured (in most cases this is the folded form vertices).\n * @param {VecLine2} line the line used to split the graph\n * @param {number[]} newFaces a list of the new faces that were created during\n * the splitting operation.\n * @returns {number[]} a list of indices in the faceOrders array which are now\n * invalid due to now being two faces on the opposite sides of the split line.\n */\nexport const getInvalidFaceOrders = (\n\t{ vertices_coords, faces_vertices, faceOrders },\n\tline,\n\tnewFaces,\n) => {\n\tif (!faceOrders) { return []; }\n\n\tconst newFacesLookup = invertFlatMap(newFaces);\n\n\t// this is a 2D only method, but could be extended into 3D.\n\tconst facesSide = faces_vertices\n\t\t.map(vertices => vertices.map(v => vertices_coords[v]))\n\t\t.map(poly => poly.map(resize2)).map(poly => average2(...poly))\n\t\t.map(point => cross2(subtract2(point, line.origin), line.vector))\n\t\t.map(Math.sign);\n\n\t// these are indices of faceOrders which have a relationship between\n\t// two faces from either side of the cut line. These are new faces which\n\t// were just made from two old faces which used to be overlapping before\n\t// becoming split. We can either:\n\t// - throw these relationships away\n\t// - in the case of a flat-fold, we can calculate the new relationship\n\t//   between the faces.\n\treturn faceOrders\n\t\t.map(([a, b], i) => (\n\t\t\t(newFacesLookup[a] !== undefined || newFacesLookup[b] !== undefined)\n\t\t\t&& ((facesSide[a] === 1 && facesSide[b] === -1)\n\t\t\t|| (facesSide[a] === -1 && facesSide[b] === 1))\n\t\t\t\t? i\n\t\t\t\t: undefined))\n\t\t.filter(a => a !== undefined);\n};\n\n/**\n * @description Given a graph with vertices in foldedForm which has just\n * been split by a cutting line, in the special case where this is a flat-fold\n * in 2D, update the faceOrders to match the state after the fold.\n * Note: the vertices_coords in the folded state refers to the folded state of\n * the graph before the split, so, everything is folded except the new crease\n * line.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} invalidFaceOrders\n * @param {number} foldAngle\n * @param {boolean[]} faces_winding calculated on new vertices in folded form\n * @returns {undefined}\n */\nexport const updateFlatFoldedInvalidFaceOrders = (\n\t{ faceOrders },\n\tinvalidFaceOrders,\n\tfoldAngle,\n\tfaces_winding,\n) => {\n\t// valley fold:\n\t// if B's winding is true, A is in front of B, if false A is behind B\n\t// mountain fold:\n\t// if B's winding is true, A is behind B, if false A is in front of B\n\t// \"true\" and \"false\" keys here are B's winding.\n\tconst valley = { true: 1, false: -1 };\n\tconst mountain = { true: -1, false: 1 };\n\tinvalidFaceOrders.forEach(i => {\n\t\t// face b's normal decides the order.\n\t\tconst [a, b] = faceOrders[i];\n\t\t/** @type {number} */\n\t\tconst newOrder = foldAngle > 0\n\t\t\t? valley[faces_winding[b]]\n\t\t\t: mountain[faces_winding[b]];\n\t\tfaceOrders[i] = [a, b, newOrder];\n\t});\n};\n\n/**\n * @description This method accompanies foldGraph() and accomplishes two things:\n * Due to many faces being split by a common line, many new faceOrders have\n * been updated/correct to include pairs of faces which don't even overlap.\n * This method finds and removes the now non-overlapping faces.\n * Additionally, in the specific case that the fold line is flat 180 M or V,\n * we are able to create new faceOrders between all edge-adjacent faces,\n * by considering the winding directions, and the assignment of the fold line.\n * @param {FOLD} graph a FOLD graph, faceOrders array is modified in place\n * @param {FOLD} folded the same graph with folded vertices_coords\n * @param {VecLine2} line\n * @param {number} foldAngle\n * @param {boolean[]} faces_winding\n * @param {number[]} newEdges\n * @param {number[]} newFaces\n * @returns {undefined}\n */\nexport const updateFaceOrders = (\n\tgraph,\n\tfolded,\n\tline,\n\tfoldAngle,\n\tfaces_winding,\n\tnewEdges,\n\tnewFaces,\n) => {\n\t// true if 180deg \"M\" or \"V\", false if flat \"F\" or 3D.\n\tconst isFlatFolded = edgeFoldAngleIsFlatFolded(foldAngle);\n\tif (!graph.faceOrders && isFlatFolded) { graph.faceOrders = []; }\n\n\t// if the assignment is 180 M or V, we generate new face orders between\n\t// new faces which were just made by splitting a face with a new edge,\n\t// depending on the edge's assignemnt, we can make a new faceOrder.\n\tif (isFlatFolded) {\n\t\tconst newFaceOrders = makeNewFlatFoldFaceOrders(graph, newEdges);\n\t\tgraph.faceOrders = graph.faceOrders.concat(newFaceOrders);\n\t}\n\n\t// the splitGraph operation created many new faceOrders out of the old ones,\n\t// for every old face's orders, each old face became two new faces, so\n\t// every one of the old face's orders was replaced with two, referencing the\n\t// new indices.\n\t// This generates a bunch of relationships between faces which no longer\n\t// overlap, we will identify these as \"nowInvalidFaceOrders\" and do one\n\t// of two things with these:\n\t// if 3D or \"F\": we have to delete these faceOrders\n\t// if 180deg \"M\" or \"V\": we can update these face orders to new orders\n\t// based on the crease direction and face winding.\n\tif (graph.faceOrders) {\n\t\tconst nowInvalidFaceOrders = getInvalidFaceOrders(\n\t\t\tfolded,\n\t\t\tline,\n\t\t\tnewFaces,\n\t\t);\n\n\t\tif (isFlatFolded) {\n\t\t\tupdateFlatFoldedInvalidFaceOrders(graph, nowInvalidFaceOrders, foldAngle, faces_winding);\n\t\t} else {\n\t\t\tconst invalidOrderLookup = {};\n\t\t\tnowInvalidFaceOrders.forEach(i => { invalidOrderLookup[i] = true; });\n\t\t\tgraph.faceOrders = graph.faceOrders.filter((_, i) => !invalidOrderLookup[i]);\n\t\t}\n\t}\n};\n"
  },
  {
    "path": "src/graph/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as foldColors from \"../fold/colors.js\";\nimport * as foldFileFrames from \"../fold/frames.js\";\nimport * as foldSpecMethods from \"../fold/spec.js\";\nimport * as addVertices from \"./add/vertex.js\";\nimport * as addEdges from \"./add/edge.js\";\nimport * as edgesCircular from \"./edges/circular.js\";\nimport * as edgesDuplicate from \"./edges/duplicate.js\";\nimport * as edgesLines from \"./edges/lines.js\";\nimport * as edgesOverlap from \"./edges/overlap.js\";\nimport * as facesPlanes from \"./faces/planes.js\";\nimport * as facesMatrix from \"./faces/matrix.js\";\nimport * as facesWinding from \"./faces/winding.js\";\nimport * as flatFold from \"./fold/flatFold.js\";\nimport * as foldGraph from \"./fold/foldGraph.js\";\nimport * as foldGraphIntoSegments from \"./fold/foldGraphIntoSegments.js\";\nimport * as foldGraphIntoSubgraph from \"./fold/foldGraphIntoSubgraph.js\";\nimport * as splitEdge from \"./split/splitEdge.js\";\nimport * as splitFace from \"./split/splitFace.js\";\nimport * as splitLine from \"./split/splitLine.js\";\nimport * as splitGraph from \"./split/splitGraph.js\";\nimport * as validate from \"./validate/validate.js\";\nimport * as verticesClusters from \"./vertices/clusters.js\";\nimport * as verticesCollinear from \"./vertices/collinear.js\";\nimport * as verticesDuplicate from \"./vertices/duplicate.js\";\nimport * as verticesFolded from \"./vertices/folded.js\";\nimport * as verticesIsolated from \"./vertices/isolated.js\";\nimport * as verticesSort from \"./vertices/sort.js\";\nimport * as boundary from \"./boundary.js\";\nimport * as clean from \"./clean.js\";\nimport * as connectedComponents from \"./connectedComponents.js\";\nimport * as count from \"./count.js\";\nimport * as cycles from \"./cycles.js\";\nimport * as directedGraph from \"./directedGraph.js\";\nimport * as disjoint from \"./disjoint.js\";\nimport * as explodeMethods from \"./explode.js\";\n// import * as flaps from \"./flaps.js\";\nimport * as intersect from \"./intersect.js\";\nimport * as join from \"./join.js\";\nimport * as maps from \"./maps.js\";\nimport * as nearestMethods from \"./nearest.js\";\nimport * as normalize from \"./normalize.js\";\nimport * as normals from \"./normals.js\";\nimport * as orders from \"./orders.js\";\nimport * as overlap from \"./overlap.js\";\n// import * as planarize from \"./planarize.js\";\nimport * as intersectAllEdges from \"./planarize/intersectAllEdges.js\";\nimport * as planarize from \"./planarize/planarize.js\";\nimport * as planarizeCollinearEdges from \"./planarize/planarizeCollinearEdges.js\";\nimport * as planarizeCollinearVertices from \"./planarize/planarizeCollinearVertices.js\";\nimport * as planarizeMakeFaces from \"./planarize/planarizeMakeFaces.js\";\nimport * as planarizeOverlaps from \"./planarize/planarizeOverlaps.js\";\nimport * as pleat from \"./pleat.js\";\nimport * as populate from \"./populate.js\";\nimport * as raycast from \"./raycast.js\";\nimport * as remove from \"./remove.js\";\nimport * as rendering from \"./rendering.js\";\nimport * as replace from \"./replace.js\";\nimport * as subgraphMethods from \"./subgraph.js\";\nimport * as sweep from \"./sweep.js\";\nimport * as transfer from \"./transfer.js\";\nimport * as transform from \"./transform.js\";\nimport * as trees from \"./trees.js\";\nimport * as triangulateMethods from \"./triangulate.js\";\nimport * as walk from \"./walk.js\";\nimport make from \"./make/index.js\";\nimport { graphConstructor } from \"../prototypes/index.js\";\n\n// these are included via. a backdoor system, in src/index.js, all of these\n// methods are bound to the the prototype, constructor graph(), which already\n// contains references to the methods in these files:\n// import * as foldBases from \"../fold/bases.js\";\n\nconst graphMethods = {\n\t...foldColors,\n\t...foldFileFrames,\n\t...foldSpecMethods,\n\t...addVertices,\n\t...addEdges,\n\t...edgesCircular,\n\t...edgesDuplicate,\n\t...edgesLines,\n\t...edgesOverlap,\n\t...facesPlanes,\n\t...facesMatrix,\n\t...facesWinding,\n\t...flatFold,\n\t...foldGraph,\n\t...foldGraphIntoSegments,\n\t...foldGraphIntoSubgraph,\n\t...verticesClusters,\n\t...verticesCollinear,\n\t...verticesDuplicate,\n\t...verticesFolded,\n\t...verticesIsolated,\n\t...verticesSort,\n\t...boundary,\n\t...clean,\n\t...connectedComponents,\n\t...count,\n\t...cycles,\n\t...directedGraph,\n\t...disjoint,\n\t...explodeMethods,\n\t// ...flaps,\n\t...intersect,\n\t...join,\n\t...make,\n\t...maps,\n\t...nearestMethods,\n\t...normalize,\n\t...normals,\n\t...orders,\n\t...overlap,\n\t...planarize,\n\t...intersectAllEdges,\n\t...planarizeCollinearEdges,\n\t...planarizeCollinearVertices,\n\t...planarizeMakeFaces,\n\t...planarizeOverlaps,\n\t...pleat,\n\t...populate,\n\t...raycast,\n\t...remove,\n\t...rendering,\n\t...replace,\n\t...splitEdge,\n\t...splitFace,\n\t...splitLine,\n\t...splitGraph,\n\t...subgraphMethods,\n\t...sweep,\n\t...transfer,\n\t...transform,\n\t...trees,\n\t...triangulateMethods,\n\t...walk,\n\t...validate,\n};\n\nconst graphExport = Object.assign(graphConstructor, graphMethods);\n\nexport default graphExport;\n"
  },
  {
    "path": "src/graph/intersect.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\texclude,\n\tincludeL,\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tmagSquared,\n\tdot2,\n\tcross2,\n\tsubtract2,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tpointsToLine2,\n} from \"../math/convert.js\";\nimport {\n\tintersectLineLine,\n} from \"../math/intersect.js\";\nimport {\n\toverlapConvexPolygonPoint,\n} from \"../math/overlap.js\";\nimport {\n\tclusterSortedGeneric,\n} from \"../general/cluster.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./make/facesEdges.js\";\n\n/**\n * @description Intersect a line/ray/segment with a FOLD graph but only\n * consider the graph's vertices.\n * @param {FOLD} graph a fold object in creasePattern or foldedForm\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} lineDomain the function which characterizes the line\n * into a line, ray, or segment.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {(number|undefined)[]} an array matching the length of vertices,\n * in the case of an intersection, the number is the parameter along the line's\n * vector, if there is no intersection the value is undefined.\n */\nexport const intersectLineVertices = (\n\t{ vertices_coords },\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\tconst lineMagSq = magSquared(vector);\n\tconst lineMag = Math.sqrt(lineMagSq);\n\tif (lineMag < epsilon) {\n\t\treturn Array(vertices_coords.length).fill(undefined);\n\t}\n\treturn vertices_coords\n\t\t.map(coord => subtract2(coord, origin))\n\t\t.map(vec => {\n\t\t\tconst parameter = dot2(vec, vector) / lineMagSq;\n\t\t\treturn Math.abs(cross2(vec, vector)) < epsilon\n\t\t\t\t&& lineDomain(parameter, epsilon / lineMag)\n\t\t\t\t? parameter\n\t\t\t\t: undefined;\n\t\t});\n};\n\n/**\n * @description Intersect a line/ray/segment with a FOLD graph but only\n * consider the graph's vertices and edges.\n * Intersections are endpoint-exclusive, and will not include collinear edges.\n * Vertex intersection information is available under the \"vertices\" key,\n * and collinear edges can be found by checking if both vertices are overlapped.\n * @param {FOLD} graph a fold object in creasePattern or foldedForm\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} lineDomain the function which characterizes \"line\"\n * parameter into a line, ray, or segment.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ vertices: number[], edges: LineLineEvent[] }} an object\n * summarizing the intersections with edges and vertices:\n * - vertices: for every vertex, a number or undefined. If there is an\n *   intersection with the line, the number is the parameter along the line's\n *   vector, if there is no intersection the value is undefined.\n * - edges: a list of intersections, undefined if no intersection or collinear,\n *   or an intersection object which describes:\n *   - a: {number} the input line's parameter of the intersection\n *   - b: {number} the edge's parameter of the intersection\n *   - point: {number[]} the intersection point\n */\nexport const intersectLineVerticesEdges = (\n\t{ vertices_coords, edges_vertices },\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_coords) { return { vertices: [], edges: [] }; }\n\n\t// for every vertex, does that vertex lie along the line.\n\tconst vertices = intersectLineVertices(\n\t\t{ vertices_coords },\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tepsilon,\n\t);\n\n\tif (!edges_vertices) { return { vertices, edges: [] }; }\n\n\t// for every edge, a list of its vertices that lie along the line (0, 1, or 2)\n\t// if an edge has 1-2 overlapping vertices, we can skip the intersection call\n\t// if an edge has no overlapping vertices, we must run the edge-intersection.\n\tconst edgesVerticesOverlap = edges_vertices\n\t\t.map(ev => ev\n\t\t\t.map(v => (vertices[v] !== undefined ? v : undefined))\n\t\t\t.filter(a => a !== undefined));\n\n\t// if the edge contains no vertices which overlap the line,\n\t// perform a line-segment intersection, otherwise do nothing.\n\t// it's possible the intersection returns no result, which looks like\n\t// an object with all undefined values, if so, replace these objects\n\t// with a single undefined.\n\t// Additionally, add a { vertex: undefined } key/value to all intersections.\n\t// const edgesNoOverlapIntersection = edges_vertices\n\tconst edges = edges_vertices\n\t\t.map(ev => ev.map(v => vertices_coords[v]))\n\t\t.map(([s0, s1], e) => (edgesVerticesOverlap[e].length === 0\n\t\t\t? intersectLineLine(\n\t\t\t\t{ vector, origin },\n\t\t\t\tpointsToLine2(s0, s1),\n\t\t\t\tlineDomain,\n\t\t\t\tincludeS,\n\t\t\t)\n\t\t\t: undefined))\n\t\t.map(res => (res === undefined || !res.point ? undefined : res));\n\n\t// if our line crosses the edge at one vertex, we still want to include the\n\t// intersection information, but we can construct it ourselves without\n\t// running the intersection algorithm. this should save us a little time.\n\t// const edges = edgesVerticesOverlap\n\t// \t.map((verts, e) => (verts.length === 1\n\t// \t\t? ({\n\t// \t\t\ta: (dot2(vector, subtract2(vertices_coords[verts[0]], origin))\n\t// \t\t\t\t/ magSquared(vector)),\n\t// \t\t\tb: edges_vertices[e][0] === verts[0] ? 0 : 1,\n\t// \t\t\tpoint: [...vertices_coords[verts[0]]],\n\t// \t\t\tvertex: verts[0],\n\t// \t\t})\n\t// \t\t: edgesNoOverlapIntersection[e]));\n\n\treturn { vertices, edges };\n};\n\n/**\n * @description Intersect a line/ray/segment with a FOLD graph, and return\n * intersect information with vertices, edges, and faces.\n * - Vertex intersection is padded with an epsilon, inclusive to this region.\n * - Edge intersection is endpoint-exclusive, if there is a vertex intersection\n *   look for it in the \"vertices\" array. Also, edges parallel with the line\n *   are excluded, find these collinear edges by checking \"vertices\" array.\n * - Face intersections very simply include all vertex and edge intersections\n *   which are included in the face. This can cause some issues, for example,\n *   two vertices which are neighbors are simply a collinear edge,\n *   \"filterCollinearFacesData\" will handle these, but if you are dealing with\n *   non-convex polygons you might have a lot of work parsing this data.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} lineDomain the function which characterizes \"line\"\n * parameter into a line, ray, or segment.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   vertices: number[],\n *   edges: LineLineEvent[],\n *   faces: (FaceEdgeEvent | FaceVertexEvent)[][],\n * }} an object summarizing the intersections with vertices, edges, and faces:\n * - vertices: for every vertex, true or false, does the vertex overlap the line\n * - edges: a list of intersections, undefined if no intersection or collinear,\n *   or an intersection object which describes:\n *   - a: {number} the input line's parameter of the intersection\n *   - b: {number} the edge's parameter of the intersection\n *   - point: {number[]} the intersection point\n * - faces: for every intersected face, an array of intersection objects,\n *   objects similar to but slightly different from those in the \"edges\" array.\n *   Basically there are two types of intersection: \"vertex\" and \"edge\":\n *   - a: {number} the input line's parameter of the intersection\n *   - b: {number|undefined} the edge's parameter of the intersection, or\n *     undefined if intersected with a vertex\n *   - point: {number[]} the intersection point\n *   - vertex: {number|undefined} if the intersection crosses a vertex\n *   - edge: {number|undefined} if the intersection crosses an edge\n */\nexport const intersectLine = (\n\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\t// intersect the line with every edge. the intersection should be inclusive\n\t// with respect to the segment endpoints. this will cause duplicate points\n\t// for every face when a line crosses exactly at its vertex, but this is\n\t// necessary because we need to know this point, so we will filter later.\n\tconst { vertices, edges } = intersectLineVerticesEdges(\n\t\t{ vertices_coords, edges_vertices },\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tepsilon,\n\t);\n\n\tif (!faces_vertices) { return { vertices, edges, faces: [] }; }\n\n\tif (!faces_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\n\t// for every face, get every edge of that face's intersection with our line,\n\t// filter out any edges which had no intersection.\n\t// it's possible for faces to have 0, 1, 2, 3... any number of intersections.\n\tconst facesEdgeIntersections = faces_edges\n\t\t.map(fe => fe\n\t\t\t.map(edge => (edges[edge]\n\t\t\t\t? { ...edges[edge], edge }\n\t\t\t\t: undefined))\n\t\t\t.filter(a => a !== undefined));\n\tconst facesVertexIntersections = faces_vertices\n\t\t.map(fv => fv\n\t\t\t.map(vertex => (vertices[vertex] !== undefined\n\t\t\t\t? { a: vertices[vertex], vertex }\n\t\t\t\t: undefined))\n\t\t\t.filter(a => a !== undefined));\n\n\tconst facesIntersections = faces_vertices.map((_, v) => [\n\t\t...facesVertexIntersections[v],\n\t\t...facesEdgeIntersections[v],\n\t]);\n\n\t// this epsilon function will compare the object's \"a\" property\n\t// which is the intersections's \"a\" parameter (line parameter).\n\tconst epsilonEqual = (p, q) => Math.abs(p.a - q.a) < epsilon * 2;\n\n\t// For every face, sort and cluster the face's intersection events using\n\t// our input line's parameter. This results in, for every face,\n\t// its intersection events are clustered inside of sub arrays.\n\t// Finally, filter out any invalid intersections from the face which\n\t// includes two vertices that form a collinear edge\n\tconst faces = facesIntersections\n\t\t.map(intersections => intersections.sort((p, q) => p.a - q.a))\n\t\t.map(intersections => clusterSortedGeneric(intersections, epsilonEqual)\n\t\t\t.map(cluster => cluster.map(index => intersections[index])))\n\t\t.map(clusters => clusters\n\t\t\t.map(cluster => cluster[0]));\n\n\treturn { vertices, edges, faces };\n};\n\n/**\n * @description Intersect a line/ray/segment with a FOLD graph and\n * check a list of input points to see which faces each point lies inside,\n * returning the intersect information with vertices, edges, and faces.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} lineDomain the function which characterizes \"line\"\n * parameter into a line, ray, or segment.\n * @param {[number, number][]} [interiorPoints=[]] in the case of a ray or segment,\n * include the endpoint(s) and they will be included if they appear in a face.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   vertices: number[],\n *   edges: LineLineEvent[],\n *   faces: {\n *     vertices: FaceVertexEvent[],\n *     edges: FaceEdgeEvent[],\n *     points: FacePointEvent[],\n *   }[],\n * }} an object summarizing the intersections with vertices, edges, and faces:\n * - vertices: for every vertex, true or false, does the vertex overlap the line\n * - edges: a list of intersections, undefined if no intersection or collinear,\n *   or an intersection object which describes:\n *   - a: {number} the input line's parameter of the intersection\n *   - b: {number} the edge's parameter of the intersection\n *   - point: {number[]} the intersection point\n *   - vertex: {number|undefined} in the case of an intersection which crosses\n *     a vertex, indicate which vertex, otherwise, mark it as undefined.\n * - faces: for every intersected face, a list of intersection objects\n *   filtered into three categories: \"vertices\", \"edges\", \"points\" where\n *   each category holds a list of objects with intersection information:\n *   - vertices: { a, point, vertex }\n *   - edges: { a, b, point, edge }\n *   - points: { t, point }\n *   where the data in each object is:\n *   - point: {number[]} the intersection point\n *   - a: {number} the input line's parameter of the intersection\n *   - b: {number} the edge's parameter of the intersection\n *   - t: {number[]} the point-in-polygon's overlap parameters\n *   - vertex: {number} if the intersection crosses a vertex\n *   - edge: {number} if the intersection crosses an edge\n */\nexport const intersectLineAndPoints = (\n\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tepsilon = EPSILON,\n) => {\n\t// intersect the line with every edge. the intersection should be inclusive\n\t// with respect to the segment endpoints. this will cause duplicate points\n\t// for every face when a line crosses exactly at its vertex, but this is\n\t// necessary because we need to know this point, so we will filter later.\n\tconst { vertices, edges, faces } = intersectLine(\n\t\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tepsilon,\n\t);\n\n\tif (!vertices_coords || !faces_vertices) {\n\t\treturn { vertices, edges, faces: [] };\n\t}\n\n\t// If there are ray or segment points, we have to query every single face,\n\t// does a point lie inside of the face, and if so, include it in this list.\n\t// The result is an object containing a \"point\" {number[]} and \"t\" {number[]}\n\t// this \"t\" parameter can be used later to trilaterate the position again.\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\t/** @type {{ point: [number, number], overlap: boolean, t: number[] }[][]} */\n\tconst facesInteriorPoints = !interiorPoints.length\n\t\t? faces.map(() => [])\n\t\t: faces.map((_, face) => {\n\t\t\tconst polygon = faces_vertices[face].map(v => vertices_coords2[v]);\n\t\t\tconst pointsOverlap = interiorPoints.map(point => ({\n\t\t\t\t...overlapConvexPolygonPoint(polygon, point, exclude, epsilon),\n\t\t\t\tpoint,\n\t\t\t}));\n\t\t\treturn pointsOverlap.filter(el => el.overlap);\n\t\t});\n\n\t// Every face in this list will contain a list of intersection events\n\t// that occur inside this face. The events are one of three categories:\n\t// - edges: intersections event that crosses over an edge\n\t// - vertices: intersections that cross exactly over a vertex\n\t// - point: an object describing a point lying interior to the face\n\tconst newFacesData = faces.map((intersections, f) => ({\n\t\tedges: intersections\n\t\t\t.map(el => (\"edge\" in el && \"a\" in el && \"b\" in el && \"point\" in el ? el : undefined))\n\t\t\t.filter(a => a !== undefined),\n\t\tvertices: intersections\n\t\t\t.map(el => (\"vertex\" in el && \"a\" in el ? el : undefined))\n\t\t\t.filter(a => a !== undefined),\n\t\tpoints: facesInteriorPoints[f],\n\t}));\n\n\treturn { vertices, edges, faces: newFacesData };\n};\n\n/**\n * @description This is a helper method to accompany the intersection\n * methods. Having already computed vertices/edges/faces intersections\n * (via. intersectLineAndPoints), pass the result in here, and this method\n * will filter out any collinear edges from the faces, edges are not stored\n * but vertices are, so it will filter out pairs of vertices which\n * form a collinear edge.\n * @param {FOLD} graph a FOLD object\n * @param {{\n *   vertices: number[],\n *   edges: LineLineEvent[],\n *   faces: {\n *     vertices: FaceVertexEvent[],\n *     edges: FaceEdgeEvent[],\n *     points: FacePointEvent[],\n *   }[],\n * }} the result of intersectLineAndPoints, modified in place\n */\nexport const filterCollinearFacesData = ({ edges_vertices }, { vertices, faces }) => {\n\t// For the upcoming filtering, we need a list of collinear edges, but in\n\t// the form of vertices, so, pairs of vertices which form a collinear edge.\n\tconst collinearVertices = [];\n\tedges_vertices\n\t\t.map(verts => (vertices[verts[0]] !== undefined\n\t\t\t&& vertices[verts[1]] !== undefined))\n\t\t.map((collinear, edge) => (collinear ? edge : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(edge => edges_vertices[edge])\n\t\t.forEach(verts => collinearVertices.push(verts));\n\n\tconst facesVertices = faces.map(face => face.vertices.map(({ vertex }) => vertex));\n\tconst facesVerticesHash = [];\n\tfacesVertices.forEach((_, f) => { facesVerticesHash[f] = {}; });\n\tfacesVertices\n\t\t.forEach((verts, f) => verts\n\t\t\t.forEach(v => { facesVerticesHash[f][v] = true; }));\n\n\tfaces.forEach((face, f) => {\n\t\tconst removeVertices = {};\n\t\tcollinearVertices\n\t\t\t.filter(pair => facesVerticesHash[f][pair[0]] && facesVerticesHash[f][pair[1]])\n\t\t\t.forEach(pair => {\n\t\t\t\tremoveVertices[pair[0]] = true;\n\t\t\t\tremoveVertices[pair[1]] = true;\n\t\t\t});\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces[f].vertices = face.vertices.filter(el => !removeVertices[el.vertex]);\n\t});\n};\n"
  },
  {
    "path": "src/graph/join.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tcount,\n} from \"./count.js\";\nimport {\n\tclone,\n} from \"../general/clone.js\";\nimport {\n\tremapKey,\n\tinvertFlatToArrayMap,\n} from \"./maps.js\";\nimport {\n\tVEF,\n\tfilterKeysWithPrefix,\n\tgetDimensionQuick,\n} from \"../fold/spec.js\";\n\n/**\n * @description Join two graphs into one. The result will be written into\n * the first parameter, the \"target\". All source components will be\n * re-indexed and pushed to the end of the component arrays in the target.\n * No geometric operations will occur, no testing for duplicate vertices,\n * The source components inside of the result will be disjoint from\n * the original contents in the target.\n * @param {FOLD} target a fold graph, will be modified in place\n * to become the union of the two graphs.\n * @param {FOLD} source the graph to be merged into the target.\n * @return {object} an object which describes which indices came\n * from which graph. The object has keys \"vertices\", \"edges\", and \"faces\",\n * each with a value of type {number[][]}, with only two top level\n * arrays, 0 and 1, where 0 is the \"target\" graph and 1 is the \"source\",\n * and the contents of the inner array are the indices of the component\n * in the final union graph.\n */\nexport const join = (target, source) => {\n\t// if vertices are 2D and 3D (mismatch), we have to resize\n\t// all vertices up to 3D.\n\tconst sourceDimension = getDimensionQuick(source);\n\tconst targetDimension = getDimensionQuick(target);\n\tconst sourceKeyArrays = {};\n\tVEF.forEach(key => {\n\t\tconst arrayName = filterKeysWithPrefix(source, key).shift();\n\t\tsourceKeyArrays[key] = (arrayName !== undefined ? source[arrayName] : []);\n\t});\n\tconst keyCount = {};\n\tVEF.forEach(key => { keyCount[key] = count(target, key); });\n\tconst indexMaps = { vertices: [], edges: [], faces: [] };\n\tVEF.forEach(key => sourceKeyArrays[key]\n\t\t.forEach((_, i) => { indexMaps[key][i] = keyCount[key]++; }));\n\tconst sourceClone = clone(source);\n\tVEF.forEach(key => remapKey(sourceClone, key, indexMaps[key]));\n\tObject.keys(sourceClone)\n\t\t.filter(key => sourceClone[key].constructor === Array)\n\t\t.filter(key => !(key in target))\n\t\t// eslint-disable-next-line no-param-reassign\n\t\t.forEach(key => { target[key] = []; });\n\tObject.keys(sourceClone)\n\t\t.filter(key => sourceClone[key].constructor === Array)\n\t\t.forEach(key => sourceClone[key]\n\t\t\t// eslint-disable-next-line no-param-reassign\n\t\t\t.forEach((v, i) => { target[key][i] = v; }));\n\tconst summary = {};\n\tconst targetKeyArrays = {};\n\tVEF.forEach(key => {\n\t\tconst arrayName = filterKeysWithPrefix(target, key).shift();\n\t\ttargetKeyArrays[key] = (arrayName !== undefined ? target[arrayName] : []);\n\t});\n\tVEF.forEach(key => {\n\t\tconst map = targetKeyArrays[key].map(() => 0);\n\t\tindexMaps[key].forEach(v => { map[v] = 1; });\n\t\tsummary[key] = invertFlatToArrayMap(map);\n\t});\n\tconst target2DVertices = sourceDimension !== targetDimension\n\t\t? (target.vertices_coords || [])\n\t\t\t.map((coords, i) => (coords.length === 2 ? i : undefined))\n\t\t\t.filter(a => a !== undefined)\n\t\t: [];\n\t// eslint-disable-next-line no-param-reassign\n\ttarget2DVertices.forEach(v => { target.vertices_coords[v][2] = 0; });\n\treturn summary;\n};\n\n/**\n * @description Join two planar graphs, creating new vertices, edges, faces.\n */\n// const joinPlanarGraphs = (...graphs) => {\n// export const joinPlanarGraphs = (graph) => {\n\n// };\n\n/**\n *\n */\n// const makeVerticesMapAndConsiderDuplicates = (target, source, epsilon = EPSILON) => {\n// \tlet index = target.vertices_coords.length;\n// \treturn source.vertices_coords\n// \t\t.map(vertex => target.vertices_coords\n// \t\t\t.map(v => distance(v, vertex) < epsilon)\n// \t\t\t.map((onVertex, i) => (onVertex ? i : undefined))\n// \t\t\t.filter(a => a !== undefined)\n// \t\t\t.shift())\n// \t\t.map(el => (el === undefined ? index++ : el));\n// };\n/**\n *\n */\n// const getEdgesDuplicateFromSourceInTarget = (target, source) => {\n// \tconst source_duplicates = {};\n// \tconst target_map = {};\n// \tfor (let i = 0; i < target.edges_vertices.length; i += 1) {\n// \t\t// we need to store both, but only need to test one\n// \t\ttarget_map[`${target.edges_vertices[i][0]} ${target.edges_vertices[i][1]}`] = i;\n// \t\ttarget_map[`${target.edges_vertices[i][1]} ${target.edges_vertices[i][0]}`] = i;\n// \t}\n// \tfor (let i = 0; i < source.edges_vertices.length; i += 1) {\n// \t\tconst index = target_map[`${source.edges_vertices[i][0]} ${source.edges_vertices[i][1]}`];\n// \t\tif (index !== undefined) {\n// \t\t\tsource_duplicates[i] = index;\n// \t\t}\n// \t}\n// \treturn source_duplicates;\n// };\n\n/**\n * @description updateSuffixes\n * @param {object} FOLD graph\n * @param {string[]} array of strings like \"vertices_edges\"\n * @param {string[]} array of any combination of \"vertices\", \"edges\", or \"faces\"\n * @param {object} object with keys VEF each with an array of index maps\n */\n// const updateSuffixes = (source, suffixes, keys, maps) => keys\n// \t.forEach(geom => suffixes[geom]\n// \t\t.forEach(key => source[key]\n// \t\t\t.forEach((arr, i) => arr\n// \t\t\t\t.forEach((el, j) => { source[key][i][j] = maps[geom][el]; }))));\n\n/**\n * @description join graphs\n */\n// export const joinGraphs = (target, source, epsilon = EPSILON) => {\n// \t// these all relate to the source, not target\n// \tconst prefixes = {};\n// \tconst suffixes = {};\n// \tconst maps = {};\n// \tconst dimensions = [target, source]\n// \t\t.map(g => g.vertices_coords)\n// \t\t.map(coords => (coords && coords.length ? coords[0].length : 0))\n// \t\t.reduce((a, b) => Math.max(a, b));\n// \ttarget.vertices_coords = target.vertices_coords\n// \t\t.map(coord => resize(dimensions, coord));\n// \t// gather info\n// \tVEF.forEach(key => {\n// \t\tprefixes[key] = filterKeysWithPrefix(source, key);\n// \t\tsuffixes[key] = filterKeysWithSuffix(source, key);\n// \t});\n// \t// if source keys don't exist in the target, create empty arrays\n// \tVEF.forEach(geom => prefixes[geom].filter(key => !target[key]).forEach(key => {\n// \t\ttarget[key] = [];\n// \t}));\n// \t// vertex map\n// \tmaps.vertices = makeVerticesMapAndConsiderDuplicates(target, source, epsilon);\n// \t// correct indices in all vertex suffixes, like \"faces_vertices\", \"edges_vertices\"\n// \tupdateSuffixes(source, suffixes, [\"vertices\"], maps);\n// \t// edge map\n// \tconst target_edges_count = count.edges(target);\n// \tmaps.edges = Array.from(Array(count.edges(source)))\n// \t\t.map((_, i) => target_edges_count + i);\n// \tconst edge_dups = getEdgesDuplicateFromSourceInTarget(target, source);\n// \tObject.keys(edge_dups).forEach(i => { maps.edges[i] = edge_dups[i]; });\n// \t// faces map\n// \tconst target_faces_count = count.faces(target);\n// \tmaps.faces = Array.from(Array(count.faces(source)))\n// \t\t.map((_, i) => target_faces_count + i);\n// \t// todo find duplicate faces, correct map\n// \t// correct indices in all edges and faces suffixes\n// \tupdateSuffixes(source, suffixes, [\"edges\", \"faces\"], maps);\n// \t// copy all geometry arrays from source to target\n// \tVEF.forEach(geom => prefixes[geom].forEach(key => source[key].forEach((el, i) => {\n// \t\tconst new_index = maps[geom][i];\n// \t\ttarget[key][new_index] = el;\n// \t})));\n// \treturn maps;\n// };\n"
  },
  {
    "path": "src/graph/make/edges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tsubtract,\n\tmagnitude,\n\tresize2,\n\tresize3,\n} from \"../../math/vector.js\";\nimport {\n\tboundingBox,\n} from \"../../math/polygon.js\";\nimport {\n\tgetDimensionQuick,\n} from \"../../fold/spec.js\";\n\n/**\n * @description map vertices_coords onto edges_vertices so that the result\n * is an edge array where each edge contains its two points. Each point being\n * the 2D or 3D coordinate as an array of numbers.\n * @param {FOLD} graph a FOLD object with vertices and edges\n * @returns {[([number, number]|[number, number, number]),\n * ([number, number]|[number, number, number])][]} an array\n * of array of points\n * (which are arrays of numbers)\n */\nexport const makeEdgesCoords = ({ vertices_coords, edges_vertices }) => (\n\tedges_vertices.map(ev => [vertices_coords[ev[0]], vertices_coords[ev[1]]])\n);\n\n/**\n * @description Turn every edge into a vector, basing the direction on the order of\n * the pair of vertices in each edges_vertices entry.\n * @param {FOLD} graph a FOLD object, with vertices_coords, edges_vertices\n * @returns {([number, number]|[number, number, number])[]} each entry\n * relates to an edge, each array contains a 2D vector\n */\nexport const makeEdgesVector = ({ vertices_coords, edges_vertices }) => {\n\tconst dimensions = getDimensionQuick({ vertices_coords });\n\tconst resize = dimensions === 2 ? resize2 : resize3;\n\treturn makeEdgesCoords({ vertices_coords, edges_vertices })\n\t\t.map(([a, b]) => resize(subtract(b, a)));\n};\n\n/**\n * @description For every edge, find the length between the edges pair of vertices.\n * @param {FOLD} graph a FOLD object, with vertices_coords, edges_vertices\n * @returns {number[]} the distance between each edge's pair of vertices\n */\nexport const makeEdgesLength = ({ vertices_coords, edges_vertices }) => (\n\tmakeEdgesVector({ vertices_coords, edges_vertices }).map(magnitude)\n);\n\n/**\n * @description Make an array of axis-aligned bounding boxes, one for each edge,\n * that encloses the edge, and will work in n-dimensions. Intended for\n * fast line-sweep algorithms.\n * @param {FOLD} graph a FOLD object with vertices and edges.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {Box[]} an array of boxes, length matching the number of edges\n */\nexport const makeEdgesBoundingBox = ({\n\tvertices_coords, edges_vertices,\n}, epsilon) => (\n\tmakeEdgesCoords({ vertices_coords, edges_vertices })\n\t\t.map(coords => boundingBox(coords, epsilon))\n);\n"
  },
  {
    "path": "src/graph/make/edgesAssignment.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./facesEdges.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"./edgesFaces.js\";\n\n/**\n * @description Convert edges fold angle into assignment for every edge. This simple\n * method will only result in \"M\" \"V\" and \"F\", depending on crease angle.\n * \"makeEdgesAssignment()\" will also assign \"B\"\n * @param {FOLD} graph a FOLD object, with edges_foldAngle\n * @returns {string[]} array of fold assignments\n */\nexport const makeEdgesAssignmentSimple = ({ edges_foldAngle }) => edges_foldAngle\n\t.map(a => {\n\t\tif (a === 0) { return \"F\"; }\n\t\treturn a < 0 ? \"M\" : \"V\";\n\t});\n\n/**\n * @description Convert edges fold angle into assignment for every edge. This method\n * will assign \"M\" \"V\" \"F\" and \"B\" for edges with only one incident face.\n * @param {FOLD} graph a FOLD object, with edges_foldAngle\n * @returns {string[]} array of fold assignments\n */\nexport const makeEdgesAssignment = ({\n\tedges_vertices, edges_foldAngle, edges_faces, faces_vertices, faces_edges,\n}) => {\n\tif (edges_vertices && !edges_faces) {\n\t\tif (!faces_edges && faces_vertices) {\n\t\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t\t}\n\t\tif (faces_edges) {\n\t\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_edges });\n\t\t}\n\t}\n\tif (edges_foldAngle) {\n\t\t// if edges_faces exists, assign boundaries. otherwise skip it.\n\t\treturn edges_faces\n\t\t\t? edges_foldAngle.map((a, i) => {\n\t\t\t\tif (edges_faces[i].length < 2) { return \"B\"; }\n\t\t\t\tif (a === 0) { return \"F\"; }\n\t\t\t\treturn a < 0 ? \"M\" : \"V\";\n\t\t\t})\n\t\t\t: makeEdgesAssignmentSimple({ edges_foldAngle });\n\t}\n\t// no data to use, everything gets \"unassigned\"\n\treturn edges_vertices.map(() => \"U\");\n};\n"
  },
  {
    "path": "src/graph/make/edgesEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Make `edges_edges` containing all vertex-adjacent edges.\n * This will be radially sorted if you call makeVerticesEdges before calling this.\n * @param {FOLD} graph a FOLD object, with entries edges_vertices, vertices_edges\n * @returns {number[][]} each entry relates to an edge, each array contains indices\n * of other edges.\n */\nexport const makeEdgesEdges = ({ edges_vertices, vertices_edges }) => (\n\tedges_vertices.map((verts, i) => {\n\t\tconst side0 = vertices_edges[verts[0]].filter(e => e !== i);\n\t\tconst side1 = vertices_edges[verts[1]].filter(e => e !== i);\n\t\treturn side0.concat(side1);\n\t}));\n"
  },
  {
    "path": "src/graph/make/edgesFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tsubtract2,\n\tcross2,\n} from \"../../math/vector.js\";\nimport {\n\tcountImpliedEdges,\n} from \"../count.js\";\nimport {\n\tmakeFacesVerticesFromEdges,\n} from \"./facesVertices.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./facesEdges.js\";\nimport {\n\tmakeEdgesVector,\n} from \"./edges.js\";\nimport {\n\tmakeFacesCenterQuick,\n} from \"./faces.js\";\n\n/**\n * @description Make `edges_faces` where each edge is paired with its incident faces.\n * This is unsorted, prefer makeEdgesFaces()\n * @param {FOLD} graph a FOLD object, with entries edges_vertices, faces_edges\n * @returns {(number | null | undefined)[][]} each entry relates\n * to an edge, each array contains indices\n * of adjacent faces.\n */\nexport const makeEdgesFacesUnsorted = ({ edges_vertices, faces_vertices, faces_edges }) => {\n\t// faces_vertices is only needed to build this array, if it doesn't exist.\n\tif (!faces_edges) {\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\t// instead of initializing the array ahead of time (we would need to know\n\t// the length of something like edges_vertices)\n\tconst edges_faces = edges_vertices !== undefined\n\t\t? edges_vertices.map(() => [])\n\t\t: Array.from(Array(countImpliedEdges({ faces_edges }))).map(() => []);\n\tfaces_edges.forEach((face, f) => {\n\t\tconst hash = [];\n\t\t// in the case that one face visits the same edge multiple times,\n\t\t// this hash acts as a set allowing one occurence of each edge index.\n\t\tface.forEach((edge) => { hash[edge] = f; });\n\t\thash.forEach((fa, e) => edges_faces[e].push(fa));\n\t});\n\treturn edges_faces;\n};\n\n/**\n * @description Make `edges_faces` where each edge is paired with its incident faces.\n * This is sorted according to the FOLD spec, sorting faces on either side of an edge.\n * @param {FOLDExtended} graph a FOLD object, with entries vertices_coords,\n * edges_vertices, faces_vertices, faces_edges\n * @returns {(number | null | undefined)[][]} each entry relates to an edge,\n * each array contains indices of adjacent faces.\n */\nexport const makeEdgesFaces = ({\n\tvertices_coords, edges_vertices, edges_vector, faces_vertices, faces_edges, faces_center,\n}) => {\n\tif (!edges_vertices || (!faces_vertices && !faces_edges)) {\n\t\t// alert, we just made UNSORTED edges faces\n\t\treturn makeEdgesFacesUnsorted({ faces_edges });\n\t}\n\tif (!faces_vertices) {\n\t\tfaces_vertices = makeFacesVerticesFromEdges({ edges_vertices, faces_edges });\n\t}\n\tif (!faces_edges) {\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tif (!edges_vector) {\n\t\tedges_vector = makeEdgesVector({ vertices_coords, edges_vertices });\n\t}\n\tconst edges_origin = edges_vertices.map(pair => vertices_coords[pair[0]]);\n\tif (!faces_center) {\n\t\tfaces_center = makeFacesCenterQuick({ vertices_coords, faces_vertices });\n\t}\n\tconst edges_faces = edges_vertices.map(() => []);\n\tfaces_edges.forEach((face, f) => {\n\t\tconst hash = [];\n\t\t// in the case that one face visits the same edge multiple times,\n\t\t// this hash acts as a set allowing one occurence of each edge index.\n\t\tface.forEach((edge) => { hash[edge] = f; });\n\t\thash.forEach((fa, e) => edges_faces[e].push(fa));\n\t});\n\t// sort edges_faces in 2D based on which side of the edge's vector\n\t// each face lies, sorting the face on the left first. see FOLD spec.\n\tedges_faces.forEach((faces, e) => {\n\t\tconst faces_cross = faces\n\t\t\t.map(f => faces_center[f])\n\t\t\t.map(center => subtract2(center, edges_origin[e]))\n\t\t\t.map(vector => cross2(vector, edges_vector[e]));\n\t\tfaces.sort((a, b) => faces_cross[a] - faces_cross[b]);\n\t});\n\treturn edges_faces;\n};\n"
  },
  {
    "path": "src/graph/make/edgesFoldAngle.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../../environment/messages.js\";\nimport {\n\tnormalize,\n\tdot,\n\tsubtract,\n} from \"../../math/vector.js\";\nimport {\n\tmakeFacesNormal,\n} from \"../normals.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./facesEdges.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"./edgesFaces.js\";\nimport {\n\tmakeFacesCenterQuick,\n} from \"./faces.js\";\n\nconst assignment_angles = { M: -180, m: -180, V: 180, v: 180 };\n\n/**\n * @description Convert edges assignment into fold angle in degrees for every edge.\n * @param {FOLD} graph a FOLD object, with edges_assignment\n * @returns {number[]} array of fold angles in degrees\n */\nexport const makeEdgesFoldAngle = ({ edges_assignment }) => edges_assignment\n\t.map(a => assignment_angles[a] || 0);\n\n// angle between two 3D vectors\n// α = arccos[(xa * xb + ya * yb + za * zb) / (√(xa2 + ya2 + za2) * √(xb2 + yb2 + zb2))]\n// angle between two 2D vectors\n// α = arccos[(xa * xb + ya * yb) / (√(xa2 + ya2) * √(xb2 + yb2))]\n\n/**\n * @description Inspecting adjacent faces, and referencing their normals, infer\n * the foldAngle for every edge. This will result in a negative number for\n * mountain creases, and positive for valley. This works well for 3D models,\n * but will fail for flat-folded models, in which case, edges_assignment\n * will be consulted to differentiate between 180 degree M or V folds.\n * @param {FOLDExtended} graph a FOLD object\n * @returns {number[]} for every edge, an angle in degrees.\n */\nexport const makeEdgesFoldAngleFromFaces = ({\n\tvertices_coords,\n\tedges_vertices,\n\tedges_faces,\n\tedges_assignment,\n\tfaces_vertices,\n\tfaces_edges,\n\tfaces_normal,\n\tfaces_center,\n}) => {\n\tif (!edges_faces) {\n\t\tif (!faces_edges) {\n\t\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t\t}\n\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_edges });\n\t}\n\tif (!faces_normal) {\n\t\tfaces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\t}\n\tif (!faces_center) {\n\t\tfaces_center = makeFacesCenterQuick({ vertices_coords, faces_vertices });\n\t}\n\t// get the angle between two adjacent face normals, where parallel normals have 0 angle.\n\t// additionally, create a vector from one face's center to the other and check the sign of\n\t// the dot product with one of the normals, this clarifies if the fold is mountain or valley.\n\treturn edges_faces.map((faces, e) => {\n\t\tif (faces.length > 2) { throw new Error(Messages.manifold); }\n\t\tif (faces.length < 2) { return 0; }\n\t\tconst a = faces_normal[faces[0]];\n\t\tconst b = faces_normal[faces[1]];\n\t\tconst a2b = normalize(subtract(\n\t\t\tfaces_center[faces[1]],\n\t\t\tfaces_center[faces[0]],\n\t\t));\n\t\t// for mountain creases (faces facing away from each other), set the sign to negative.\n\t\tlet sign = Math.sign(dot(a, a2b));\n\t\t// if the sign is zero, the faces are coplanar, it's impossible to tell if\n\t\t// this was because of a mountain or a valley fold.\n\t\tif (sign === 0) {\n\t\t\tif (edges_assignment && edges_assignment[e]) {\n\t\t\t\tif (edges_assignment[e] === \"F\" || edges_assignment[e] === \"F\") { sign = 0; }\n\t\t\t\tif (edges_assignment[e] === \"M\" || edges_assignment[e] === \"m\") { sign = -1; }\n\t\t\t\tif (edges_assignment[e] === \"V\" || edges_assignment[e] === \"v\") { sign = 1; }\n\t\t\t} else {\n\t\t\t\tthrow new Error(Messages.flatFoldAngles);\n\t\t\t}\n\t\t}\n\t\treturn (Math.acos(dot(a, b)) * (180 / Math.PI)) * sign;\n\t});\n};\n"
  },
  {
    "path": "src/graph/make/edgesVertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Create edges_vertices from faces_vertices\n * @param {FOLD} graph a FOLD object\n * @returns {[number, number][]} an edges_vertices array\n */\nexport const makeEdgesVerticesFromFaces = ({ faces_vertices }) => {\n\tconst hash = {};\n\tconst edges_vertices = [];\n\tfaces_vertices\n\t\t.map(vertices => vertices\n\t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t\t.forEach(([a, b]) => {\n\t\t\t\tif (hash[`${a} ${b}`] || hash[`${b} ${a}`]) { return; }\n\t\t\t\thash[`${a} ${b}`] = true;\n\t\t\t\tedges_vertices.push([a, b]);\n\t\t\t}));\n\treturn edges_vertices;\n};\n"
  },
  {
    "path": "src/graph/make/faces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tgetDimensionQuick,\n} from \"../../fold/spec.js\";\nimport {\n\tmakePolygonNonCollinear,\n\tcentroid,\n} from \"../../math/polygon.js\";\nimport {\n\taverage2,\n\taverage3,\n\tresize2,\n\tresize3,\n} from \"../../math/vector.js\";\nimport {\n\twalkPlanarFaces,\n\tfilterWalkedBoundaryFace,\n} from \"../walk.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"./verticesVertices.js\";\nimport {\n\tmakeVerticesSectors,\n} from \"./vertices.js\";\nimport {\n\tmakeVerticesToEdge,\n} from \"./lookup.js\";\n\n/**\n * @description Rebuild all faces in a 2D planar graph by walking counter-clockwise\n * down every edge (both ways). This does not include the outside face which winds\n * around the boundary backwards enclosing the outside space.\n * @param {FOLDExtended} graph a FOLD object\n * @returns {{\n *   faces_vertices: number[][],\n *   faces_edges: number[][],\n *   faces_sectors: number[][],\n * }} array of faces as objects containing \"vertices\", \"edges\", and \"sectors\"\n * @example\n * // to convert the return object into faces_vertices and faces_edges\n * const { faces_vertices, faces_edges } = makePlanarFaces(graph);\n */\nexport const makePlanarFaces = ({\n\tvertices_coords, vertices_vertices, vertices_edges,\n\tvertices_sectors, edges_vertices, edges_vector,\n}) => {\n\tif (!vertices_vertices) {\n\t\tvertices_vertices = makeVerticesVertices({\n\t\t\tvertices_coords, edges_vertices, vertices_edges,\n\t\t});\n\t}\n\tif (!vertices_sectors) {\n\t\tvertices_sectors = makeVerticesSectors({\n\t\t\tvertices_coords, vertices_vertices, edges_vertices, edges_vector,\n\t\t});\n\t}\n\tconst vertices_edges_map = makeVerticesToEdge({ edges_vertices });\n\t// removes the one face that outlines the piece with opposite winding.\n\t// walkPlanarFaces stores edges as vertex pair strings, \"4 9\",\n\t// convert these into edge indices\n\tconst res = filterWalkedBoundaryFace(walkPlanarFaces({\n\t\tvertices_vertices, vertices_sectors,\n\t})).map(f => ({ ...f, edges: f.edges.map(e => vertices_edges_map[e]) }));\n\treturn {\n\t\tfaces_vertices: res.map(el => el.vertices),\n\t\tfaces_edges: res.map(el => el.edges),\n\t\tfaces_sectors: res.map(el => el.angles),\n\t};\n};\n\n// export const makePlanarFacesVertices = graph => makePlanarFaces(graph)\n// \t.faces_vertices;\n// export const makePlanarFacesEdges = graph => makePlanarFaces(graph)\n// \t.faces_edges;\n\n/**\n * @description map vertices_coords onto each face's set of vertices,\n * turning each face into an array of points, with an additional step:\n * ensure that each polygon has 0 collinear vertices.\n * this can result in a polygon with fewer vertices than is contained\n * in that polygon's faces_vertices array.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {([number, number] | [number, number, number])[][]} an array\n * of array of points, where each point is an array of 2 or 3 numbers\n */\nexport const makeFacesPolygon = ({ vertices_coords, faces_vertices }, epsilon) => (\n\tfaces_vertices\n\t\t.map(verts => verts.map(v => vertices_coords[v]))\n\t\t.map(polygon => makePolygonNonCollinear(polygon, epsilon))\n);\n\n/**\n * @description map vertices_coords onto each face's set of vertices,\n * turning each face into an array of points. \"Quick\" meaning collinear vertices\n * are not removed, which in some cases, this will be the preferred method.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @returns {([number, number] | [number, number, number])[][]} an\n * array of array of points, where each point is an array of numbers\n */\nexport const makeFacesPolygonQuick = ({ vertices_coords, faces_vertices }) => (\n\tfaces_vertices.map(verts => verts.map(v => vertices_coords[v]))\n);\n\n/**\n * @description For every face, get the face's centroid.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @returns {[number, number][]} array of points, where each point is an array of numbers\n */\nexport const makeFacesCentroid2D = ({ vertices_coords, faces_vertices }) => (\n\tfaces_vertices\n\t\t.map(fv => fv.map(v => vertices_coords[v]))\n\t\t.map(coords => coords.map(resize2))\n\t\t.map(coords => centroid(coords))\n);\n\n/**\n * @description For every face, get the average of the face's vertices.\n * This is not not precise, and only relevant for faces which are convex.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @returns {[number, number][]} array of points, where each point is an array of numbers\n */\nexport const makeFacesCenter2DQuick = ({ vertices_coords, faces_vertices }) => (\n\tmakeFacesPolygonQuick({ vertices_coords, faces_vertices })\n\t\t.map(coords => coords.map(resize2))\n\t\t.map(coords => average2(...coords))\n);\n\n/**\n * @description For every face, get the average of the face's vertices.\n * This is not not precise, and only relevant for faces which are convex.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @returns {[number, number, number][]} array of points, where each point is an array of numbers\n */\nexport const makeFacesCenter3DQuick = ({ vertices_coords, faces_vertices }) => (\n\tmakeFacesPolygonQuick({ vertices_coords, faces_vertices })\n\t\t.map(coords => coords.map(resize3))\n\t\t.map(coords => average3(...coords))\n\t\t// assume vertices_coords is 3D, if not, center point[2] will be NaN, fix it\n\t\t.map(point => (Number.isNaN(point[2]) ? [point[0], point[1], 0] : point))\n);\n\n/**\n * @description For every face, get the average of the face's vertices.\n * This is not not precise, and only relevant for faces which are convex.\n * @param {FOLD} graph a FOLD object, with vertices_coords, faces_vertices\n * @returns {[number, number][]|[number, number, number][]} array of points,\n * where each point is an array of either 2 or 3 numbers.\n */\nexport const makeFacesCenterQuick = ({ vertices_coords, faces_vertices }) => {\n\tconst dimensions = getDimensionQuick({ vertices_coords });\n\treturn dimensions === 2\n\t\t? makeFacesCenter2DQuick({ vertices_coords, faces_vertices })\n\t\t: makeFacesCenter3DQuick({ vertices_coords, faces_vertices });\n};\n"
  },
  {
    "path": "src/graph/make/facesEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeVerticesToEdge,\n} from \"./lookup.js\";\n\n/**\n * @description Make `faces_edges` from `faces_vertices`.\n * @param {FOLD} graph a FOLD object, with\n * edges_vertices and faces_vertices\n * @returns {number[][]} a `faces_edges` array\n */\nexport const makeFacesEdgesFromVertices = ({ edges_vertices, faces_vertices }) => {\n\tconst map = makeVerticesToEdge({ edges_vertices });\n\treturn faces_vertices\n\t\t.map(face => face\n\t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]].join(\" \")))\n\t\t.map(face => face.map(pair => map[pair]));\n};\n"
  },
  {
    "path": "src/graph/make/facesFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description faces_faces is an array of edge-adjacent face indices for each face.\n * @param {FOLD} graph a FOLD object, with faces_vertices\n * @returns {(number | null | undefined)[][]} each index relates to a face,\n * each entry is an array of numbers, each number is an index\n * of an edge-adjacent face to this face.\n */\nexport const makeFacesFaces = ({ faces_vertices }) => {\n\t// create a map that relates these space-separated vertex pair strings\n\t// to an array of faces which contain this edge (vertex-pair).\n\t/** @type {{ [key: string]: number[] }} */\n\tconst vertexPairToFaces = {};\n\n\t// for every face's faces_vertices, pair a vertex with the next vertex,\n\t// join the pairs of vertices into a space-separated string\n\tconst facesVerticesKeys = faces_vertices\n\t\t.map(face => face\n\t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t\t.map(pair => pair.join(\" \")));\n\n\tfacesVerticesKeys\n\t\t.flat()\n\t\t.forEach(key => { vertexPairToFaces[key] = []; });\n\n\tfacesVerticesKeys\n\t\t.forEach((keys, f) => keys\n\t\t\t.forEach(key => vertexPairToFaces[key].push(f)));\n\n\t// in the case of faces with leaf edges whose vertices double back\n\t// on themselves, the face would choose itself as an adjacent face.\n\t// filter to prevent this from happening, replace these instances with\n\t// \"undefined\".\n\treturn faces_vertices\n\t\t.map((face, f1) => face\n\t\t\t.map((v, i, arr) => [arr[(i + 1) % arr.length], v])\n\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t.map(key => vertexPairToFaces[key])\n\t\t\t.map(faces => (faces === undefined ? [undefined] : faces))\n\t\t\t.flatMap(faces => faces.filter(f2 => f1 !== f2).shift()));\n};\n\n// export const makeFacesFacesManifold = ({ faces_vertices }) => {\n// \t// create a map that relates these space-separated vertex pair strings\n// \t// to an array of faces which contain this edge (vertex-pair).\n// \tconst vertexPairToFace = {};\n\n// \t// for every face's faces_vertices, pair a vertex with the next vertex,\n// \t// join the pairs of vertices into a space-separated string (where v0 < v1)\n// \tfaces_vertices\n// \t\t.map((face, f) => face\n// \t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n// \t\t\t.map(pair => pair.join(\" \"))\n// \t\t\t.forEach(key => { vertexPairToFace[key] = f; }));\n\n// \t// in the case of faces with leaf edges whose vertices double back\n// \t// on themselves, the face would choose itself as an adjacent face.\n// \t// filter to prevent this from happening, replace these instances with\n// \t// \"undefined\".\n// \treturn faces_vertices\n// \t\t.map((face, f1) => face\n// \t\t\t.map((v, i, arr) => [arr[(i + 1) % arr.length], v])\n// \t\t\t.map(pair => pair.join(\" \"))\n// \t\t\t.map(key => vertexPairToFace[key])\n// \t\t\t.map(f2 => (f1 === f2 ? undefined : f2)));\n// };\n\n// export const makeFacesFacesFirst = ({ faces_vertices }) => {\n// \t// for every face's faces_vertices, pair a vertex with the next vertex,\n// \t// join the pairs of vertices into a space-separated string (where v0 < v1)\n// \tconst facesVerticesEdgeVertexPairs = faces_vertices\n// \t\t.map(face => face\n// \t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n// \t\t\t.map(([v0, v1]) => (v1 < v0 ? [v1, v0] : [v0, v1]))\n// \t\t\t.map(pair => pair.join(\" \")));\n\n// \t// create a map that relates these space-separated vertex pair strings\n// \t// to an array of faces which contain this edge (vertex-pair).\n// \tconst edgePairToFaces = {};\n// \tfacesVerticesEdgeVertexPairs\n// \t\t.flat()\n// \t\t.forEach(key => { edgePairToFaces[key] = []; });\n// \tfacesVerticesEdgeVertexPairs\n// \t\t.forEach((edgeKeys, f) => edgeKeys\n// \t\t\t.forEach(key => { edgePairToFaces[key].push(f); }));\n\n// \t// for each face's edge (vertex-pair), get the array of faces that\n// \t// are adjacent to this edge. This generates a list of faces that contains\n// \t// duplicates and contains this face itself. these will be filtered out.\n// \tconst newFacesEdgesFaces = facesVerticesEdgeVertexPairs\n// \t\t.map((edgeKeys) => edgeKeys\n// \t\t\t.map(key => edgePairToFaces[key]));\n\n// \t// each face's entry contains an array of array of faces, flatten this list\n// \t// and remove any mention of this face itself, and remove any duplicates.\n// \t// using a Set appears to maintain the insertion order, which is what we want.\n// \treturn newFacesEdgesFaces\n// \t\t.map((edgesFaces, face) => edgesFaces.flat().filter(f => f !== face))\n// \t\t.map(faces => Array.from(new Set(faces)));\n// };\n"
  },
  {
    "path": "src/graph/make/facesVertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Make `faces_vertices` from `faces_edges`.\n * @param {FOLD} graph a FOLD object, with faces_edges, edges_vertices\n * @returns {number[][]} a `faces_vertices` array\n */\nexport const makeFacesVerticesFromEdges = ({ edges_vertices, faces_edges }) => faces_edges\n\t.map(edges => edges\n\t\t.map(edge => edges_vertices[edge])\n\t\t.map((pairs, i, arr) => {\n\t\t\tconst next = arr[(i + 1) % arr.length];\n\t\t\treturn (pairs[0] === next[0] || pairs[0] === next[1])\n\t\t\t\t? pairs[1]\n\t\t\t\t: pairs[0];\n\t\t}));\n"
  },
  {
    "path": "src/graph/make/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as makeEdges from \"./edges.js\";\nimport * as makeEdgesAssignment from \"./edgesAssignment.js\";\nimport * as makeEdgesEdges from \"./edgesEdges.js\";\nimport * as makeEdgesFaces from \"./edgesFaces.js\";\nimport * as makeEdgesFoldAngle from \"./edgesFoldAngle.js\";\nimport * as makeEdgesVertices from \"./edgesVertices.js\";\nimport * as makeFaces from \"./faces.js\";\nimport * as makeFacesEdges from \"./facesEdges.js\";\nimport * as makeFacesFaces from \"./facesFaces.js\";\nimport * as makeFacesVertices from \"./facesVertices.js\";\nimport * as makeLookup from \"./lookup.js\";\nimport * as makeVertices from \"./vertices.js\";\nimport * as makeVerticesEdges from \"./verticesEdges.js\";\nimport * as makeVerticesFaces from \"./verticesFaces.js\";\nimport * as makeVerticesVertices from \"./verticesVertices.js\";\n\nexport default {\n\t...makeEdges,\n\t...makeEdgesAssignment,\n\t...makeEdgesEdges,\n\t...makeEdgesFaces,\n\t...makeEdgesFoldAngle,\n\t...makeEdgesVertices,\n\t...makeFaces,\n\t...makeFacesEdges,\n\t...makeFacesFaces,\n\t...makeFacesVertices,\n\t...makeLookup,\n\t...makeVertices,\n\t...makeVerticesEdges,\n\t...makeVerticesFaces,\n\t...makeVerticesVertices,\n};\n"
  },
  {
    "path": "src/graph/make/lookup.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Consider an array of arrays of indices to each be cyclical,\n * convert each inner array into pairs [i, (i+1) % length], and build map\n * that relates these pairs as a space-separated string to the index in the\n * top level array in which this pair resides.\n * @param {number[][]} array\n * @param {number[]} subsetIndices\n * @returns {{[key: string]: number}}\n */\nconst makePairsMap = (array, subsetIndices) => {\n\t/** @type {{ [key: string]: number }} */\n\tconst map = {};\n\tconst indices = !subsetIndices\n\t\t? array.map((_, i) => i)\n\t\t: subsetIndices;\n\tindices\n\t\t.forEach(i => array[i]\n\t\t\t.map((_, j, arr) => [0, 1]\n\t\t\t\t.map(offset => (j + offset) % arr.length)\n\t\t\t\t.map(n => arr[n])\n\t\t\t\t.join(\" \"))\n\t\t\t.forEach(key => { map[key] = i; }));\n\treturn map;\n};\n\n/**\n * @description Make an object which answers the question: \"which edge\n * connects these two vertices?\". This is accomplished by building an\n * object with keys containing vertex pairs (space separated string),\n * and the value is the edge index. This is bidirectional, so \"7 15\"\n * and \"15 7\" are both keys that point to the same edge.\n * @param {FOLD} graph a FOLD object, containing edges_vertices\n * @param {number[]} [edges=undefined] a list of edge indices to be used,\n * if left empty, all edges will be used as input.\n * @returns {{[key: string]: number}} object mapping a space-separated\n * vertex pair to an edge index\n */\nexport const makeVerticesToEdge = ({ edges_vertices }, edges) => (\n\tmakePairsMap(edges_vertices, edges)\n);\n\n/**\n * @description Make an object which answers the question: \"which face\n * contains these two vertices in this exact order?\". This is accomplished\n * by building an object with keys containing vertex pairs\n * (space separated string), and the value is the face index.\n * This will not work with non-manifold graphs.\n * @param {FOLD} graph a FOLD object, containing faces_vertices\n * @param {number[]} [faces=undefined] a list of edge indices to be used,\n * if left empty, all edges will be used as input.\n * @returns {{[key: string]: number}} object mapping a space-separated\n * vertex pair to a face index\n */\nexport const makeVerticesToFace = ({ faces_vertices }, faces) => (\n\tmakePairsMap(faces_vertices, faces)\n);\n\n/**\n * @description Make an object which answers the question: \"which face\n * contains these two edges in this exact order?\". This is accomplished\n * by building an object with keys containing edge pairs\n * (space separated string), and the value is the face index.\n * This will not work with non-manifold graphs.\n * @param {FOLD} graph a FOLD object, containing faces_edges\n * @param {number[]} [faces=undefined] a list of edge indices to be used,\n * if left empty, all edges will be used as input.\n * @returns {{[key: string]: number}} object mapping a space-separated\n * edge pair to a face index\n */\nexport const makeEdgesToFace = ({ faces_edges }, faces) => (\n\tmakePairsMap(faces_edges, faces)\n);\n"
  },
  {
    "path": "src/graph/make/vertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tTWO_PI,\n} from \"../../math/constant.js\";\nimport {\n\tresize2,\n\tflip2,\n} from \"../../math/vector.js\";\nimport {\n\tcounterClockwiseSectors2,\n} from \"../../math/radial.js\";\nimport {\n\tmakeEdgesVector,\n} from \"./edges.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"./verticesVertices.js\";\n\n/**\n * @description For every vertex, make an array of vectors that point towards each\n * of the incident vertices. This is accomplised by taking the vertices_vertices\n * array and converting it into vectors, indices will be aligned with vertices_vertices.\n * @param {FOLDExtended} graph a FOLD object, containing vertices_coords,\n * vertices_vertices, edges_vertices\n * @returns {[number, number][][]} array of array of array of numbers, where each row corresponds\n * to a vertex index, inner arrays correspond to vertices_vertices, and inside is a 2D vector\n * @todo this can someday be rewritten without edges_vertices\n */\nexport const makeVerticesVerticesVector = ({\n\tvertices_coords, vertices_vertices, vertices_edges, vertices_faces,\n\tedges_vertices, edges_vector, faces_vertices,\n}) => {\n\tif (!edges_vector) {\n\t\tedges_vector = makeEdgesVector({ vertices_coords, edges_vertices });\n\t}\n\tif (!vertices_vertices) {\n\t\tvertices_vertices = makeVerticesVertices({\n\t\t\tvertices_coords, vertices_edges, vertices_faces, edges_vertices, faces_vertices,\n\t\t});\n\t}\n\tconst edge_map = {};\n\tedges_vertices\n\t\t.map(vertices => vertices.join(\" \"))\n\t\t.forEach((key, e) => { edge_map[key] = e; });\n\treturn vertices_vertices\n\t\t.map((_, a) => vertices_vertices[a]\n\t\t\t.map((b) => {\n\t\t\t\tconst edge_a = edge_map[`${a} ${b}`];\n\t\t\t\tconst edge_b = edge_map[`${b} ${a}`];\n\t\t\t\tif (edge_a !== undefined) { return resize2(edges_vector[edge_a]); }\n\t\t\t\tif (edge_b !== undefined) { return flip2(edges_vector[edge_b]); }\n\t\t\t\treturn undefined;\n\t\t\t}));\n};\n\n/**\n * @description Between pairs of counter-clockwise adjacent edges around a vertex\n * is the sector measured in radians. This builds an array of of sector angles,\n * index matched to vertices_vertices.\n * @param {FOLDExtended} graph a FOLD object, containing vertices_coords,\n * vertices_vertices, edges_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds\n * to a vertex index, inner arrays contains angles in radians\n */\nexport const makeVerticesSectors = ({\n\tvertices_coords, vertices_vertices, edges_vertices, edges_vector,\n}) => makeVerticesVerticesVector({\n\tvertices_coords, vertices_vertices, edges_vertices, edges_vector,\n}).map(vectors => (vectors.length === 1 // leaf node\n\t? [TWO_PI] // interior_angles gives 0 for leaf nodes. we want 2pi\n\t: counterClockwiseSectors2(vectors)));\n"
  },
  {
    "path": "src/graph/make/verticesEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeVerticesToEdge,\n} from \"./lookup.js\";\n\n/**\n * @description Make `vertices_edges` from `edges_vertices`, unsorted, which should\n * be used sparingly. Prefer makeVerticesEdges().\n * @param {FOLD} graph a FOLD object, containing edges_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds to a\n * vertex index and the values in the inner array are edge indices.\n */\nexport const makeVerticesEdgesUnsorted = ({ edges_vertices }) => {\n\tconst vertices_edges = [];\n\t// iterate over edges_vertices and swap the index for each of the contents\n\t// each edge (index 0: [3, 4]) will be converted into (index 3: [0], index 4: [0])\n\t// repeat. append to each array.\n\tedges_vertices.forEach((ev, i) => ev\n\t\t.forEach((v) => {\n\t\t\tif (vertices_edges[v] === undefined) {\n\t\t\t\tvertices_edges[v] = [];\n\t\t\t}\n\t\t\tvertices_edges[v].push(i);\n\t\t}));\n\treturn vertices_edges;\n};\n\n/**\n * @description Make `vertices_edges` sorted, so that the edges are sorted\n * radially around the vertex, corresponding with the order in `vertices_vertices`.\n * @param {FOLD} graph a FOLD object, containing edges_vertices, vertices_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds to a\n * vertex index and the values in the inner array are edge indices.\n */\nexport const makeVerticesEdges = ({ edges_vertices, vertices_vertices }) => {\n\tconst edge_map = makeVerticesToEdge({ edges_vertices });\n\treturn vertices_vertices\n\t\t.map((verts, i) => verts\n\t\t\t.map(v => edge_map[`${i} ${v}`]));\n};\n"
  },
  {
    "path": "src/graph/make/verticesFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tcountImpliedVertices,\n} from \"../count.js\";\nimport {\n\tmakeVerticesToFace,\n} from \"./lookup.js\";\n\n/**\n * @description Make `vertices_faces` **not sorted** counter-clockwise,\n * which should be used sparingly. Prefer makeVerticesFaces().\n * @param {FOLD} graph a FOLD object, containing vertices_coords, faces_vertices\n * @returns {(number | null | undefined)[][]} array of array of numbers,\n * where each row corresponds to a vertex index\n * and the values in the inner array are face indices.\n */\nexport const makeVerticesFacesUnsorted = ({ vertices_coords, vertices_edges, faces_vertices }) => {\n\tconst vertArray = vertices_coords || vertices_edges;\n\tif (!faces_vertices) { return (vertArray || []).map(() => []); }\n\t// instead of initializing the array ahead of time (we would need to know\n\t// the length of something like vertices_coords or vertices_edges)\n\tconst vertices_faces = vertArray !== undefined\n\t\t? vertArray.map(() => [])\n\t\t: Array.from(Array(countImpliedVertices({ faces_vertices }))).map(() => []);\n\t// iterate over every face, then iterate over each of the face's vertices\n\tfaces_vertices.forEach((face, f) => {\n\t\t// in the case that one face visits the same vertex multiple times,\n\t\t// this hash acts as an intermediary, basically functioning like a set,\n\t\t// and only allow one occurence of each vertex index.\n\t\tconst hash = [];\n\t\tface.forEach((vertex) => { hash[vertex] = f; });\n\t\thash.forEach((fa, v) => vertices_faces[v].push(fa));\n\t});\n\treturn vertices_faces;\n};\n\n/**\n * @description Make `vertices_faces` sorted counter-clockwise.\n * @param {FOLD} graph a FOLD object, containing vertices_coords, vertices_vertices, faces_vertices\n * @returns {(number | null | undefined)[][]} array of array of numbers,\n * where each row corresponds to a vertex index\n * and the values in the inner array are face indices.\n */\nexport const makeVerticesFaces = ({ vertices_coords, vertices_vertices, faces_vertices }) => {\n\tif (!faces_vertices) { return vertices_coords.map(() => []); }\n\tif (!vertices_vertices) {\n\t\treturn makeVerticesFacesUnsorted({ vertices_coords, faces_vertices });\n\t}\n\tconst face_map = makeVerticesToFace({ faces_vertices });\n\treturn vertices_vertices\n\t\t.map((verts, v) => verts.map(vert => [v, vert].join(\" \")))\n\t\t.map(keys => keys\n\t\t\t// .filter(key => face_map[key] !== undefined) // removed. read below.\n\t\t\t.map(key => face_map[key]));\n};\n\n// this method used to contain a filter to remove \"undefined\",\n// because in the case of a boundary vertex of a closed polygon shape, there\n// is no face that winds backwards around the piece and encloses infinity.\n// unfortunately, this disconnects the index match with vertices_vertices.\n"
  },
  {
    "path": "src/graph/make/verticesVertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"./verticesEdges.js\";\nimport {\n\tmakeVerticesFacesUnsorted,\n} from \"./verticesFaces.js\";\nimport {\n\tsortVerticesCounterClockwise,\n} from \"../vertices/sort.js\";\n\n/**\n * @description Make `vertices_vertices` sorted radially counter-clockwise.\n * @param {FOLD} graph a FOLD object, containing vertices_coords, vertices_edges, edges_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds to a\n * vertex index and the values in the inner array are vertex indices.\n */\nexport const makeVerticesVertices2D = ({ vertices_coords, vertices_edges, edges_vertices }) => {\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\t// use adjacent edges to find adjacent vertices\n\tconst vertices_vertices = vertices_edges\n\t\t.map((edges, v) => edges\n\t\t\t// the adjacent edge's edges_vertices also contains this vertex,\n\t\t\t// filter it out and we're left with the adjacent vertices\n\t\t\t.map(edge => edges_vertices[edge]\n\t\t\t\t.filter(i => i !== v))\n\t\t\t.reduce((a, b) => a.concat(b), []));\n\treturn vertices_coords === undefined\n\t\t? vertices_vertices\n\t\t: vertices_vertices\n\t\t\t.map((verts, i) => sortVerticesCounterClockwise({ vertices_coords }, verts, i));\n};\n\n/**\n * @description Make `vertices_vertices` sorted radially counter-clockwise.\n * @param {FOLD} graph a FOLD object, containing vertices_coords, vertices_edges, edges_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds to a\n * vertex index and the values in the inner array are vertex indices.\n */\nexport const makeVerticesVerticesFromFaces = ({\n\tvertices_coords, vertices_faces, faces_vertices,\n}) => {\n\tif (!vertices_faces) {\n\t\tvertices_faces = makeVerticesFacesUnsorted({ vertices_coords, faces_vertices });\n\t}\n\t// every iterate through every vertices_faces's faces_vertices\n\tconst vertices_faces_vertices = vertices_faces\n\t\t.map(faces => faces.map(f => faces_vertices[f]));\n\t// for every vertex, find its index in its faces_vertices array.\n\tconst vertices_faces_indexOf = vertices_faces_vertices\n\t\t.map((faces, vertex) => faces.map(verts => verts.indexOf(vertex)));\n\t// get the three vertices (before, this vertex, after) in this vertex's\n\t// faces_vertices array maintaining the counter clockwise order.\n\tconst vertices_faces_threeIndices = vertices_faces_vertices\n\t\t.map((faces, vertex) => faces.map((verts, j) => [\n\t\t\t(vertices_faces_indexOf[vertex][j] + verts.length - 1) % verts.length,\n\t\t\tvertices_faces_indexOf[vertex][j],\n\t\t\t(vertices_faces_indexOf[vertex][j] + 1) % verts.length,\n\t\t]));\n\t// convert these three indices in face_vertices arrays into absolute\n\t// indices to vertices, so that we have three consecutive vertex indices.\n\t// for example, vertex #7's entry might be an array containing:\n\t// [141, 7, 34]\n\t// [34, 7, 120]\n\t// [120, 7, 141]\n\tconst vertices_faces_threeVerts = vertices_faces_threeIndices\n\t\t.map((faces, vertex) => faces\n\t\t\t.map((indices, j) => indices\n\t\t\t\t.map(index => vertices_faces_vertices[vertex][j][index])));\n\t// convert the three neighbor vertices into two pairs, maintaining order,\n\t// which include the vertex in the middle, these represent the pairs of\n\t// vertices which make up the edge of the face, for all faces, in counter-\n\t// clockwise order around this vertex.\n\tconst vertices_verticesLookup = vertices_faces_threeVerts.map(faces => {\n\t\t// facesVerts matches the order in this vertex's faces_vertices array.\n\t\t// it contains vertex pair keys ([141, 7, 34] becomes [\"141 7\", \"7 34\"])\n\t\t// which represent this face's adjacent vertices to our vertex\n\t\t// coming to and from this vertex.\n\t\tconst facesVerts = faces\n\t\t\t.map(verts => [[0, 1], [1, 2]]\n\t\t\t\t.map(p => p.map(x => verts[x]).join(\" \")));\n\t\tconst from = {};\n\t\tconst to = {};\n\t\tfacesVerts.forEach((keys, i) => {\n\t\t\tfrom[keys[0]] = i;\n\t\t\tto[keys[1]] = i;\n\t\t});\n\t\treturn { facesVerts, to, from };\n\t});\n\t// using the data from above, walk around the vertex by starting with an\n\t// edge, an edge represented as a pair of vertices, and alternate:\n\t// 1. using the vertex-pair's adjacent face to get the other pair in\n\t//    the same face, and,\n\t// 2. swapping the vertices in the string (\"141 7\" becomes \"7 141\") to\n\t//    find jump to another face, this being the adjacent face in the walk.\n\t// care needs to be taken because this vertex may be adjacent to holes.\n\t// a solution is possible if there are up to two holes, but a vertex\n\t// with more than two holes is technically unsolvable.\n\treturn vertices_verticesLookup.map(lookup => {\n\t\t// locate any holes if they exist, holes are when the inverse of\n\t\t// a \"to\" key does not exist in the \"from\" lookup, or visa versa.\n\t\tconst toKeys = Object.keys(lookup.to);\n\t\tconst toKeysInverse = toKeys\n\t\t\t.map(key => key.split(\" \").reverse().join(\" \"));\n\t\t// hole keys are made from \"from\" indices, so each one can be\n\t\t// the start of a counter clockwise walk path\n\t\tconst holeKeys = toKeys\n\t\t\t.filter((_, i) => !(toKeysInverse[i] in lookup.from));\n\t\t// console.log(\"holeKeys\", holeKeys);\n\t\tif (holeKeys.length > 2) {\n\t\t\tconsole.warn(\"vertices_vertices found an unsolvable vertex\");\n\t\t\treturn [];\n\t\t}\n\t\t// the start keys will be either each hole key, or just pick a key\n\t\t// if no holes exist\n\t\tconst startKeys = holeKeys.length\n\t\t\t? holeKeys\n\t\t\t: [toKeys[0]];\n\t\t// vertex_vertices is each vertex's vertices_vertices\n\t\tconst vertex_vertices = [];\n\t\t// in the case of no holes, \"visited\" will indicate we finished.\n\t\tconst visited = {};\n\t\tfor (let s = 0; s < startKeys.length; s += 1) {\n\t\t\tconst startKey = startKeys[s];\n\t\t\tconst walk = [startKey];\n\t\t\tvisited[startKey] = true;\n\t\t\tlet isDone = false;\n\t\t\tdo {\n\t\t\t\tconst prev = walk[walk.length - 1];\n\t\t\t\tconst faceIndex = lookup.to[prev];\n\t\t\t\t// this indicates the end of a walk which ended at a hole\n\t\t\t\tif (!(faceIndex in lookup.facesVerts)) { break; }\n\t\t\t\tlet nextKey;\n\t\t\t\tif (lookup.facesVerts[faceIndex][0] === prev) {\n\t\t\t\t\tnextKey = lookup.facesVerts[faceIndex][1];\n\t\t\t\t}\n\t\t\t\tif (lookup.facesVerts[faceIndex][1] === prev) {\n\t\t\t\t\tnextKey = lookup.facesVerts[faceIndex][0];\n\t\t\t\t}\n\t\t\t\tif (nextKey === undefined) { return []; }\n\t\t\t\tconst nextKeyFlipped = nextKey.split(\" \").reverse().join(\" \");\n\t\t\t\twalk.push(nextKey);\n\t\t\t\t// this indicates the end of a walk which completed a cycle\n\t\t\t\tisDone = (nextKeyFlipped in visited);\n\t\t\t\tif (!isDone) { walk.push(nextKeyFlipped); }\n\t\t\t\t// update the visited dictionary\n\t\t\t\tvisited[nextKey] = true;\n\t\t\t\tvisited[nextKeyFlipped] = true;\n\t\t\t} while (!isDone);\n\t\t\t// walk now contains keys like \"4 0\", \"1 4\", \"4 1\", \"2 4\", \"4 2\",\n\t\t\t// mod 2 so that every edge is represented only once, which\n\t\t\t// still works with odd numbers since we start at a hole, and get the\n\t\t\t// one vertex which isn't our vertex. now we have our vertices_vertices\n\t\t\tconst vertexKeys = walk\n\t\t\t\t.filter((_, i) => i % 2 === 0)\n\t\t\t\t.map(key => key.split(\" \")[1])\n\t\t\t\t.map(str => parseInt(str, 10));\n\t\t\tvertex_vertices.push(...vertexKeys);\n\t\t}\n\t\treturn vertex_vertices;\n\t});\n};\n\n/**\n * @description Make `vertices_vertices` sorted radially counter-clockwise.\n * @param {FOLD} graph a FOLD object, containing vertices_coords, vertices_edges, edges_vertices\n * @returns {number[][]} array of array of numbers, where each row corresponds to a\n * vertex index and the values in the inner array are vertex indices.\n */\nexport const makeVerticesVertices = (graph) => {\n\tif (!graph.vertices_coords || !graph.vertices_coords.length) { return []; }\n\tconst dimensions = graph.vertices_coords.filter(() => true).shift().length;\n\tswitch (dimensions) {\n\tcase 3:\n\t\treturn makeVerticesVerticesFromFaces(graph);\n\tdefault:\n\t\treturn makeVerticesVertices2D(graph);\n\t}\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n */\nexport const makeVerticesVerticesUnsorted = ({ vertices_edges, edges_vertices }) => {\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\t// use adjacent edges to find adjacent vertices\n\treturn vertices_edges\n\t\t.map((edges, v) => edges\n\t\t\t// the adjacent edge's edges_vertices also contains this vertex,\n\t\t\t// filter it out and we're left with the adjacent vertices\n\t\t\t.flatMap(edge => edges_vertices[edge].filter(i => i !== v)));\n};\n"
  },
  {
    "path": "src/graph/maps.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfilterKeysWithSuffix,\n\tfilterKeysWithPrefix,\n} from \"../fold/spec.js\";\n\n/**\n * @description Invert an array of integer values where\n * values become indices and the top level indices become values.\n * If indices and values are not bijective, values will be overwritten.\n * @param {number[]} map an array of integers\n * @returns {number[]} the inverted array\n */\nexport const invertFlatMap = (map) => {\n\tconst inv = [];\n\tmap.forEach((n, i) => { inv[n] = i; });\n\treturn inv;\n};\n\n/**\n * @description Invert an array of arrays of integer values where\n * values become indices and the top level indices become values.\n * If indices and values are not bijective, values will be overwritten.\n * @param {number[][]} map an array of arrays of integers\n * @returns {number[]} the inverted flat array\n */\nexport const invertArrayToFlatMap = (map) => {\n\tconst inv = [];\n\tmap.forEach((arr, i) => arr.forEach(n => { inv[n] = i; }));\n\treturn inv;\n};\n\n/**\n * @description Invert an array of integer values where values become indices\n * and the indices are stored inside array values.\n * This ensures that for non-bijective maps, no data is lost.\n * @param {number[]} map an array of integers\n * @returns {number[][]} the inverted array of arrays of integers\n */\nexport const invertFlatToArrayMap = (map) => {\n\tconst inv = [];\n\tmap.forEach((n, i) => {\n\t\tif (inv[n] === undefined) { inv[n] = []; }\n\t\tinv[n].push(i);\n\t});\n\treturn inv;\n};\n\n/**\n * @description Invert an array of arrays of integer values where\n * values become indices and indices are stored inside array values.\n * This ensures that for non-bijective maps, no data is lost.\n * @param {number[][]} map an array of arrays of integers\n * @returns {number[][]} the inverted array of arrays of integers\n */\nexport const invertArrayMap = (map) => {\n\tconst inv = [];\n\tmap.forEach((arr, i) => arr.forEach(m => {\n\t\tif (inv[m] === undefined) { inv[m] = []; }\n\t\tinv[m].push(i);\n\t}));\n\treturn inv;\n};\n\n/**\n * @description Provide two or more simple nextmaps in the order\n * they were made and this will merge them into one nextmap which\n * reflects all changes to the graph.\n * @param {...number[]} maps a sequence of simple nextmaps\n * @returns {number[]} one nextmap reflecting the sum of changes\n */\nexport const mergeFlatNextmaps = (...maps) => {\n\tif (maps.length === 0) { return []; }\n\tconst solution = maps[0].map((_, i) => i);\n\tmaps.forEach(map => solution.forEach((s, i) => { solution[i] = map[s]; }));\n\treturn solution;\n};\n\n/**\n * @description Provide two or more nextmaps in the order\n * they were made and this will merge them into one nextmap which\n * reflects all changes to the graph.\n * @param {...(number|number[])[]} maps a sequence of nextmaps\n * @returns {number[][]} one nextmap reflecting the sum of changes\n */\nexport const mergeNextmaps = (...maps) => {\n\tif (maps.length === 0) { return []; }\n\t/** @type {any[]} */\n\tconst solution = maps[0].map((_, i) => [i]);\n\tmaps.forEach(map => {\n\t\tsolution.forEach((s, i) => s\n\t\t\t.forEach((indx, j) => { solution[i][j] = map[indx]; }));\n\t\tsolution.forEach((arr, i) => {\n\t\t\tsolution[i] = arr.flat()\n\t\t\t\t.filter(a => a !== undefined);\n\t\t});\n\t});\n\treturn solution;\n};\n\n/**\n * @description Provide two or more simple backmaps in the order\n * they were made and this will merge them into one backmap which\n * reflects all changes to the graph.\n * @param {...number[]} maps a sequence of simplebackmaps\n * @returns {number[]} one backmap reflecting the sum of changes\n */\nexport const mergeFlatBackmaps = (...maps) => {\n\tif (maps.length === 0) { return []; }\n\tlet solution = maps[0].map((_, i) => i);\n\tmaps.forEach(map => {\n\t\tconst next = map.map(n => solution[n]);\n\t\tsolution = next;\n\t});\n\treturn solution;\n};\n\n/**\n * @description Provide two or more  backmaps in the order\n * they were made and this will merge them into one backmap which\n * reflects all changes to the graph.\n * @param {...(number|number[])[]} maps a sequence of backmaps\n * @returns {number[][]} one backmap reflecting the sum of changes\n */\nexport const mergeBackmaps = (...maps) => {\n\t// const cat = (a, b) => a.concat(b)\n\tif (maps.length === 0) { return []; }\n\t// let solution = maps[0].reduce((a, b) => a.concat(b), []).map((_, i) => [i]);\n\tlet solution = maps[0].flat().map((_, i) => [i]);\n\tmaps.forEach(map => {\n\t\tconst next = [];\n\t\tmap.forEach((el, j) => {\n\t\t\tif (typeof el === \"number\") {\n\t\t\t\tnext[j] = solution[el];\n\t\t\t} else {\n\t\t\t\tnext[j] = el.map(n => solution[n]).reduce((a, b) => a.concat(b), []);\n\t\t\t}\n\t\t});\n\t\tsolution = next;\n\t});\n\treturn solution;\n};\n\n/**\n * @description Move indices of a particular component in a graph to a\n * new set of indices. This will update all references to this component too.\n * This is accomplished by providing an index map parameter which describes,\n * for every current index (index), what should the new index be (value).\n * @param {FOLD} graph a FOLD object, will be modified in place.\n * @param {string} key a component like \"vertices\", \"edges\", \"faces\"\n * @param {number[]} indexMap an array which maps a current index (index)\n * to a destination index (value).\n */\nexport const remapKey = (graph, key, indexMap) => {\n\t// Perform a simple invertFlatMap, indices/values swap, but in the case of\n\t// non-bijective maps, the first encounter will be kept, skipping duplicates.\n\tconst invertedMap = [];\n\tindexMap.forEach((n, i) => {\n\t\tinvertedMap[n] = (invertedMap[n] === undefined ? i : invertedMap[n]);\n\t});\n\t// if a key was not included in indexMap for whatever reason, it will be\n\t// registered as \"undefined\". we can't just assume these are errors and\n\t// remove them, because _faces fields are allowed to contain undefineds.\n\n\t// update every component that points to vertices_coords\n\t// these arrays do not change their size, only their contents\n\tfilterKeysWithSuffix(graph, key)\n\t\t.forEach(sKey => graph[sKey]\n\t\t\t.forEach((_, ii) => graph[sKey][ii]\n\t\t\t\t.forEach((v, jj) => { graph[sKey][ii][jj] = indexMap[v]; })));\n\n\t// set the top-level arrays\n\tfilterKeysWithPrefix(graph, key).forEach(prefix => {\n\t\tgraph[prefix] = invertedMap.map(old => graph[prefix][old]);\n\t});\n\n\tif (key === \"faces\" && graph.faceOrders) {\n\t\tgraph.faceOrders = graph.faceOrders\n\t\t\t.map(([a, b, order]) => [indexMap[a], indexMap[b], order]);\n\t}\n\tif (key === \"edges\" && graph.edgeOrders) {\n\t\tgraph.edgeOrders = graph.edgeOrders\n\t\t\t.map(([a, b, order]) => [indexMap[a], indexMap[b], order]);\n\t}\n};\n"
  },
  {
    "path": "src/graph/nearest.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tsubtract2,\n\tsubtract3,\n\tdistance,\n\tresize,\n} from \"../math/vector.js\";\nimport {\n\tnearestPointOnLine,\n} from \"../math/nearest.js\";\nimport {\n\tarrayMinimumIndex,\n} from \"../general/array.js\";\nimport {\n\tclampSegment,\n} from \"../math/line.js\";\nimport {\n\tgetDimensionQuick,\n} from \"../fold/spec.js\";\nimport {\n\tmakeFacesCenterQuick,\n} from \"./make/faces.js\";\nimport {\n\tfaceContainingPoint,\n} from \"./faces/facePoint.js\";\n\n/**\n * @description Iterate through all vertices in a graph and find the one nearest to a\n * provided point. This is the only of the \"nearest\" graph operations that works in 3D.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} point the point to find the nearest vertex\n * @returns {number} the index of the nearest vertex\n * @todo improve with space partitioning\n */\nexport const nearestVertex = ({ vertices_coords }, point) => {\n\tif (!vertices_coords) { return undefined; }\n\tconst dimension = getDimensionQuick({ vertices_coords });\n\tif (dimension === undefined) { return undefined; }\n\t// resize our point to be the same dimension as the first vertex\n\tconst p = resize(dimension, point);\n\t// sort by distance, hold onto the original index in vertices_coords\n\tconst nearest = vertices_coords\n\t\t.map((v, i) => ({ d: distance(p, v), i }))\n\t\t.sort((a, b) => a.d - b.d)\n\t\t.shift();\n\t// return index, not vertex\n\treturn nearest ? nearest.i : undefined;\n};\n\nconst nearestPoints2 = ({ vertices_coords, edges_vertices }, point) => edges_vertices\n\t.map(e => e.map(ev => vertices_coords[ev]))\n\t.map(e => nearestPointOnLine(\n\t\t{ vector: subtract2(e[1], e[0]), origin: e[0] },\n\t\tpoint,\n\t\tclampSegment,\n\t));\n\nconst nearestPoints3 = ({ vertices_coords, edges_vertices }, point) => edges_vertices\n\t.map(e => e.map(ev => vertices_coords[ev]))\n\t.map(e => nearestPointOnLine(\n\t\t{ vector: subtract3(e[1], e[0]), origin: e[0] },\n\t\tpoint,\n\t\tclampSegment,\n\t));\n\n/**\n * @description Iterate through all edges in a graph and find the one nearest to a provided point.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} point the point to find the nearest edge\n * @returns {number|undefined} the index of the nearest edge, or undefined\n * if there are no vertices_coords or edges_vertices\n */\nexport const nearestEdge = ({ vertices_coords, edges_vertices }, point) => {\n\tif (!vertices_coords || !edges_vertices) { return undefined; }\n\tconst nearest_points = getDimensionQuick({ vertices_coords }) === 2\n\t\t? nearestPoints2({ vertices_coords, edges_vertices }, point)\n\t\t: nearestPoints3({ vertices_coords, edges_vertices }, point);\n\treturn arrayMinimumIndex(nearest_points, p => distance(p, point));\n};\n\n/**\n * @description Iterate through all faces in a graph and find one nearest to a point.\n * This method assumes the graph is in 2D, it ignores any z components.\n * @param {FOLD} graph a FOLD object\n * @param {[number, number]} point the point to find the nearest face\n * @returns {number|undefined} the index of the face, or undefined if edges_faces is not defined.\n * @todo make this work if edges_faces is not defined (not hard)\n */\nexport const nearestFace = (graph, point) => {\n\tconst face = faceContainingPoint(graph, point);\n\tif (face !== undefined) { return face; }\n\tif (graph.edges_faces) {\n\t\tconst edge = nearestEdge(graph, point);\n\t\tif (edge === undefined) { return undefined; }\n\t\tconst faces = graph.edges_faces[edge];\n\t\tif (faces.length === 1) { return faces[0]; }\n\t\tif (faces.length > 1) {\n\t\t\tconst faces_center = makeFacesCenterQuick({\n\t\t\t\tvertices_coords: graph.vertices_coords,\n\t\t\t\tfaces_vertices: faces.map(f => graph.faces_vertices[f]),\n\t\t\t});\n\t\t\tconst distances = faces_center\n\t\t\t\t.map(center => distance(center, point));\n\t\t\tlet shortest = 0;\n\t\t\tfor (let i = 0; i < distances.length; i += 1) {\n\t\t\t\tif (distances[i] < distances[shortest]) { shortest = i; }\n\t\t\t}\n\t\t\treturn faces[shortest];\n\t\t}\n\t}\n\treturn undefined;\n};\n"
  },
  {
    "path": "src/graph/normalize.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tremapKey,\n} from \"./maps.js\";\n\n/**\n * @description This method will re-index all component arrays so that\n * there are no arrays with holes (if vertices_ contains vertex 4 but not 3).\n * Arrays with holes are a result of some methods, like subgraph() which is\n * designed so that the user can re-merge the graphs. Alternatively, you can\n * run this method to make a subgraph a valid FOLD object with no holes.\n * @param {FOLD} graph a FOLD object\n * @returns {FOLD} a copy of the input FOLD graph, with no array holes.\n */\nexport const normalize = (graph) => {\n\tconst maps = { vertices: [], edges: [], faces: [] };\n\tlet v = 0;\n\tlet e = 0;\n\tlet f = 0;\n\tgraph.vertices_coords.forEach((_, i) => { maps.vertices[i] = v++; });\n\tgraph.edges_vertices.forEach((_, i) => { maps.edges[i] = e++; });\n\tgraph.faces_vertices.forEach((_, i) => { maps.faces[i] = f++; });\n\tremapKey(graph, \"vertices\", maps.vertices);\n\tremapKey(graph, \"edges\", maps.edges);\n\tremapKey(graph, \"faces\", maps.faces);\n\treturn graph;\n};\n"
  },
  {
    "path": "src/graph/normals.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tnormalize3,\n\tcross3,\n\tsubtract3,\n\tparallel,\n\tresize3,\n} from \"../math/vector.js\";\n\n/**\n * @description Make one vector for every face that represents the\n * face's normal direction. This assumes that the faces are planar\n * and convex. It works by taking the first 3 adjacent non-parallel\n * vertices and compute the cross product from 0 to 1 and 0 to 2.\n * The windings may be flipped if faces are non-convex, otherwise\n * they will still be correct.\n * @param {FOLD} graph a FOLD object\n * @returns {[number, number, number][]} an array of 3D vectors, one for every face\n */\nexport const makeFacesNormal = ({ vertices_coords, faces_vertices }) => {\n\t// ensure that all points are in 3D\n\tconst vertices_coords3D = vertices_coords.map(resize3);\n\n\treturn faces_vertices\n\t\t.map(vertices => vertices\n\t\t\t.map(vertex => vertices_coords3D[vertex]))\n\t\t.map(polygon => {\n\t\t\t// we have to ensure that the two edges we choose are not parallel\n\t\t\tlet a;\n\t\t\tlet b;\n\t\t\tlet i = 0;\n\t\t\tdo {\n\t\t\t\ta = subtract3(polygon[(i + 1) % polygon.length], polygon[i]);\n\t\t\t\tb = subtract3(polygon[(i + 2) % polygon.length], polygon[i]);\n\t\t\t\ti += 1;\n\t\t\t} while (i < polygon.length && parallel(a, b));\n\n\t\t\t// cross product unit vectors from point 0 to point 1 and 2.\n\t\t\t// as long as the face winding data is consistent,\n\t\t\t// this gives consistent face normals.\n\t\t\treturn normalize3(cross3(a, b));\n\t\t});\n};\n\n/**\n * @description Make one vector for every vertex that represents the\n * vertex's normal direction. In this case a vertex normal is the average\n * of all of the face's normals incident to this vertex.\n * @param {FOLDExtended} graph a FOLD object\n * @returns {number[][]} an array of 3D vectors, one for every vertex\n */\nexport const makeVerticesNormal = ({ vertices_coords, faces_vertices, faces_normal }) => {\n\tif (!faces_normal) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\t}\n\n\t/** @returns {[number, number, number]} */\n\tconst mkzero = () => [0, 0, 0];\n\n\t// for every vertex's face, add the vector to the vertex's vector\n\t/** @type {[number, number, number][]} */\n\tconst vertices_normals = vertices_coords.map(mkzero);\n\n\tfaces_vertices\n\t\t.forEach((vertices, f) => vertices\n\t\t\t// .forEach(v => add3(vertices_normals[v], faces_normal[f])));\n\t\t\t.forEach(v => {\n\t\t\t\tvertices_normals[v][0] += faces_normal[f][0];\n\t\t\t\tvertices_normals[v][1] += faces_normal[f][1];\n\t\t\t\tvertices_normals[v][2] += faces_normal[f][2];\n\t\t\t}));\n\n\t// normalize all summed vectors and return them\n\treturn vertices_normals.map(v => normalize3(v));\n};\n"
  },
  {
    "path": "src/graph/orders.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tdot,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tuniqueSortedNumbers,\n} from \"../general/array.js\";\nimport {\n\tmakeFacesNormal,\n} from \"./normals.js\";\nimport {\n\ttopologicalSort,\n} from \"./directedGraph.js\";\nimport {\n\tmakeVerticesVerticesUnsorted,\n} from \"./make/verticesVertices.js\";\nimport {\n\tconnectedComponents,\n} from \"./connectedComponents.js\";\nimport {\n\tinvertFlatMap,\n\tinvertFlatToArrayMap,\n\tinvertArrayToFlatMap,\n} from \"./maps.js\";\n// import {\n// \tmakeFacesWinding,\n// } from \"../graph/faces/winding.js\";\n// import {\n// \tmakeEdgesFaces,\n// } from \"../graph/make.js\";\n// import {\n// \tallLayerConditions,\n// } from \"./globalSolver/index.js\";\n\n/**\n * @description\n * @param {[number, number, number][]} faceOrders\n * @param {number[][]} nextMap\n * @returns {[number, number, number][]} a new copy of face orders\n */\n// export const updateFaceOrdersWithMap = (faceOrders, nextMap) => {\n// \tconst faceOrdersResult = faceOrders\n// };\n\n/**\n * @description given faceOrders and a list of faces, filter the list\n * of faceOrders so that it only contains orders between faces where\n * both faces are contained in the argument subset faces array.\n * @param {[number, number, number][]} faceOrders faceOrders array, as in the FOLD spec\n * @param {number[]} faces a list of face indices\n * @returns {[number, number, number][]} a subset of faceOrders\n */\nexport const faceOrdersSubset = (faceOrders, faces) => {\n\tconst facesHash = {};\n\tfaces.forEach(f => { facesHash[f] = true; });\n\treturn faceOrders\n\t\t.filter(order => facesHash[order[0]] && facesHash[order[1]]);\n};\n\n/**\n * @description Using a graph's faceOrders data, cluster faces into groups\n * where at least one face overlaps a face from the same group. Separate the\n * faceOrders array into clusters as well, each containing only those faces\n * present in that cluster.\n * @param {FOLD} graph a FOLD object with faceOrders\n * @returns {{\n *   clusters_faces: number[][],\n *   clusters_faceOrders: [number, number, number][][],\n * }} clusters_faces, for every cluster, a list of faces\n */\nexport const overlappingFaceOrdersClusters = ({ faceOrders }) => {\n\tconst faces_cluster = connectedComponents(makeVerticesVerticesUnsorted({\n\t\tedges_vertices: faceOrders.map(([a, b]) => [a, b]),\n\t}));\n\tconst clusters_faces = invertFlatToArrayMap(faces_cluster);\n\tconst clusters_faceOrders = clusters_faces\n\t\t.map(faces => faceOrdersSubset(faceOrders, faces));\n\treturn {\n\t\tclusters_faces,\n\t\tclusters_faceOrders,\n\t};\n};\n\n/**\n * @description Convert a set of faceOrders into a list of directed edges\n * of pairs of faces. A global direction will be decided, and for every pair\n * of overlapping faces, the two will be sorted such that, for faces [A, B],\n * along the global direction, the ordering goes from face A to B.\n * The optional rootFace parameter will decide the global direction, otherwise\n * face index 0 will be chosen.\n * @param {FOLDExtended} graph a FOLD object with faceOrders, and either faces_normal\n * pre-calculated, or faces_vertices and vertices_coords to get the normals.\n * @param {number} [rootFace] the user can choose which face determines the normal\n * direction, which for flat foldable models for example will linearize upwards\n * or downwards depending on this chosen face's winding.\n * @returns {[number, number][]} a list of directed edges of face pairs\n */\nexport const faceOrdersToDirectedEdges = (\n\t{ vertices_coords, faces_vertices, faceOrders, faces_normal },\n\trootFace,\n) => {\n\tif (!faceOrders || !faceOrders.length) { return []; }\n\tif (!faces_normal) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\t}\n\n\t// get a flat, unique, array of all faces present in faceOrders\n\tconst faces = uniqueSortedNumbers(faceOrders.flatMap(([a, b]) => [a, b]));\n\n\t// we need to pick one face which determines the linearization direction.\n\t// if the user supplied rootFace is not in \"faces\", ignore it.\n\tconst normal = rootFace !== undefined && faces.includes(rootFace)\n\t\t? faces_normal[rootFace]\n\t\t: faces_normal[faces[0]];\n\n\t// create a lookup. for every face, does its normal match the normal\n\t// we just chose to represent the linearization direction?\n\t/** @type {{[key: number]: boolean}} */\n\tconst facesNormalMatch = {};\n\tfaces.forEach(f => {\n\t\t// normal will likely be near +1 or -1, no need to bother with epsilon here\n\t\tfacesNormalMatch[f] = dot(faces_normal[f], normal) > 0;\n\t});\n\n\t// this pair states face [0] is above face [1]. according to the +1 -1 order,\n\t// and whether or not the reference face [1] normal is flipped. (xor either)\n\t/** @type {[number, number][]} */\n\treturn faceOrders\n\t\t.map(order => ((order[2] === -1) !== (!facesNormalMatch[order[1]]) // a ^ b\n\t\t\t? [order[0], order[1]]\n\t\t\t: [order[1], order[0]]));\n};\n\n/**\n * @description Find a topological ordering from a set of faceOrders.\n * The user can supply the face for which the normal will set the\n * direction of the linearization, if none is selected the face with\n * the lowest index number is chosen.\n * The faceOrders should contain faces which all lie in a plane, otherwise\n * this will attempt to linearize faces along a direction that is\n * meaningless (a vector inside of the plane of the faces).\n * So, for 3D models, this method should be run multiple times, each time\n * on a subset of faceOrders, containing only those faces which are coplanar\n * (and ideally, connected and overlapping).\n * This will not return a linearization including all faces in a graph,\n * it only includes faces found in faceOrders.\n * @param {FOLDExtended} graph a FOLD object with faceOrders, and either faces_normal\n * pre-calculated, or faces_vertices and vertices_coords to get the normals.\n * @param {number} [rootFace] the user can choose which face determines the normal\n * direction, which for flat foldable models for example will linearize upwards\n * or downwards depending on this chosen face's winding.\n * @returns {number[]|undefined} layers_face, for every layer (key)\n * which face (value) inhabits it. This only includes faces which are found\n * in faceOrders. Undefined if no ordering exists (if a cycle is present)\n */\nexport const linearizeFaceOrders = (\n\t{ vertices_coords, faces_vertices, faceOrders, faces_normal },\n\trootFace,\n) => (topologicalSort(faceOrdersToDirectedEdges({\n\tvertices_coords, faces_vertices, faceOrders, faces_normal,\n}, rootFace)));\n\n/**\n * todo: assuming faces_vertices instead of faces_edges\n * @returns {number[]} layers_face\n */\nconst fillInMissingFaces = ({ faces_vertices }, faces_layer) => {\n\tif (!faces_vertices) { return faces_layer; }\n\tconst missingFaces = faces_vertices\n\t\t.map((_, i) => i)\n\t\t.filter(i => faces_layer[i] == null);\n\treturn missingFaces.concat(invertFlatMap(faces_layer));\n};\n\n/**\n * @description Find a topological ordering of all faces in a graph.\n * This method is intended for 2D flat foldings. This requires\n * the graph with folded vertices_coords, a crease pattern will not work.\n * This method is inclusive and will include all faces from the graph\n * in the result, even those which have no ordering. The method will first\n * sort all faces which do have an ordering, then find any remaining faces,\n * and add these faces in no particular order onto the beginning of the list,\n * so that the faces with an order will be at the end (on top, painters algorithm).\n * @param {FOLDExtended} graph a FOLD object with either faceOrders or faces_layer.\n * @param {number} [rootFace] the user can choose which face determines the normal\n * direction, which for flat foldable models for example will linearize upwards\n * or downwards depending on this chosen face's winding.\n * @returns {number[]} layers_face, for every layer (key),\n * which face (value) inhabits it.\n */\nexport const linearize2DFaces = ({\n\tvertices_coords, faces_vertices, faceOrders, faces_layer, faces_normal,\n}, rootFace) => {\n\tif (!faces_normal) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\t}\n\tif (faceOrders) {\n\t\tconst linearization = linearizeFaceOrders(\n\t\t\t{ faceOrders, faces_normal },\n\t\t\trootFace,\n\t\t);\n\t\treturn !linearization\n\t\t\t? []\n\t\t\t: fillInMissingFaces({ faces_vertices }, invertFlatMap(linearization));\n\t}\n\tif (faces_layer) {\n\t\treturn fillInMissingFaces({ faces_vertices }, faces_layer);\n\t}\n\t// no face order exists, just return all face indices.\n\t// if the array has any holes filter these out\n\treturn faces_vertices.map((_, i) => i).filter(() => true);\n};\n\n/**\n * @description Given a graph which contains a faceOrders, get an array\n * of information for each face, what is its displacement vector, and\n * what is its displacement magnitude integer, indicating which layer\n * this face lies on.\n * @param {FOLDExtended} graph a FOLD object with faceOrders.\n * @returns {{\n *   vector: [number, number, number],\n *   layer: number,\n * }[]} face-aligned array, one object per face,\n * each object with properties \"vector\" and \"layer\".\n */\nexport const nudgeFacesWithFaceOrders = ({\n\tvertices_coords, faces_vertices, faceOrders, faces_normal: facesNormal,\n}) => {\n\tconst faces_normal = facesNormal\n\t\t? facesNormal.map(resize3)\n\t\t: makeFacesNormal({ vertices_coords, faces_vertices });\n\n\t// create a graph where the vertices are the faces, and edges\n\t// are connections between faces according to faceOrders\n\t// using this representation, find the disjoint sets of faces,\n\t// those which are isolated from each other according to layer orders\n\tconst {\n\t\tclusters_faces,\n\t\tclusters_faceOrders,\n\t} = overlappingFaceOrdersClusters({ faceOrders });\n\n\t// if a cluster contains a cycle, it's entry will be undefined.\n\tconst clusters_layers_face = clusters_faceOrders\n\t\t.map(orders => linearizeFaceOrders({ faceOrders: orders, faces_normal }));\n\n\t// if one of the clusters contains a cycle, even though some\n\t// clusters may be valid, exit early.\n\tif (clusters_layers_face.includes(undefined)) { return undefined; }\n\n\tconst clusters_normals = clusters_faces.map(faces => faces_normal[faces[0]]);\n\t/** @type {{ vector: [number, number, number], layer: number }[]} */\n\tconst faces_nudge = [];\n\tclusters_layers_face.forEach((set, i) => set.forEach((face, index) => {\n\t\tfaces_nudge[face] = {\n\t\t\tvector: clusters_normals[i],\n\t\t\tlayer: index,\n\t\t};\n\t}));\n\treturn faces_nudge;\n};\n\n/**\n * @description Given a graph with a faces_layer, a topological sorting\n * of faces, for a flat-folded 2D graph, get an array where every face\n * is given a layer and a vector, which will always be [0, 0, 1].\n * @param {FOLDExtended} graph a FOLD object with the parameter faces_layer.\n * @returns {{\n *     vector: [number, number]|[number, number, number],\n *     layer: number,\n * }[]} face-aligned array, one object per face,\n * each object with properties \"vector\" and \"layer\".\n */\nexport const nudgeFacesWithFacesLayer = ({ faces_layer }) => {\n\t/** @type {{ vector: [number, number, number], layer: number }[]} */\n\tconst faces_nudge = [];\n\tconst layers_face = invertFlatMap(faces_layer);\n\tlayers_face.forEach((face, layer) => {\n\t\tfaces_nudge[face] = {\n\t\t\tvector: [0, 0, 1],\n\t\t\tlayer,\n\t\t};\n\t});\n\treturn faces_nudge;\n};\n\n/**\n * @description for a flat-foldable origami, one in which all\n * of its folded state vertices are in 2D, this will return\n * one valid layer arrangement of the faces.\n * first it finds all pairwise face layer conditions, then\n * finds a topological ordering of each condition.\n * @param {FOLDExtended} graph a FOLD object, make sure the vertices\n * have already been folded.\n * @returns {number[]} a faces_layer object, describing,\n * for each face (key) which layer the face inhabits (value)\n */\nexport const makeFacesLayer = ({ vertices_coords, faces_vertices, faceOrders, faces_normal }) => {\n\tif (!faces_normal) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_normal = makeFacesNormal({ vertices_coords, faces_vertices });\n\t}\n\tconst linearization = linearizeFaceOrders({ faceOrders, faces_normal });\n\treturn !linearization ? [] : invertFlatMap(linearization);\n};\n\n/**\n * @description Flip a model over by reversing the order of the faces\n * in a faces_layer encoding.\n * @param {number[]} faces_layer a faces_layer array\n * @returns {number[]} a new faces_layer array\n */\nexport const flipFacesLayer = (faces_layer) => invertArrayToFlatMap(\n\tinvertFlatToArrayMap(faces_layer).reverse(),\n);\n\n// export const faceOrdersToFacesLayer = (graph) => {\n// \treturn topologicalOrder({ faceOrders, faces_normal }, faces);\n// };\n\n// const makeFacesLayer = (graph, epsilon) => {\n// \tconst conditions = allLayerConditions(graph, epsilon)[0];\n// \treturn invertMap(topologicalOrder(conditions, graph));\n// };\n\n/**\n * @description Given a faces_layer ordering of faces in a graph,\n * complute the edges_assignments, including \"B\", \"F\", \"V\", and \"M\".\n * @param {FOLD} graph a FOLD object, with the vertices already folded.\n * @param {number[]} faces_layer a faces_layer array\n * @returns {string[]} an edges_assignment array.\n */\n// export const facesLayerToEdgesAssignments = (graph, faces_layer) => {\n// \tconst edges_assignment = [];\n// \tconst faces_winding = makeFacesWinding(graph);\n// \t// set boundary creases\n// \tconst edges_faces = graph.edges_faces\n// \t\t? graph.edges_faces\n// \t\t: makeEdgesFaces(graph);\n// \tedges_faces.forEach((faces, e) => {\n// \t\tif (faces.length === 1) { edges_assignment[e] = \"B\"; }\n// \t\tif (faces.length === 2) {\n// \t\t\tconst windings = faces.map(f => faces_winding[f]);\n// \t\t\tif (windings[0] === windings[1]) {\n// \t\t\t\tedges_assignment[e] = \"F\";\n// \t\t\t\treturn;\n// \t\t\t}\n// \t\t\tconst layers = faces.map(f => faces_layer[f]);\n// \t\t\tconst local_dir = layers[0] < layers[1];\n// \t\t\tconst global_dir = windings[0] ? local_dir : !local_dir;\n// \t\t\tedges_assignment[e] = global_dir ? \"V\" : \"M\";\n// \t\t}\n// \t});\n// \treturn edges_assignment;\n// };\n\n/**\n * @description Convert a set of face-pair layer orders (+1,-1,0)\n * into a face-face relationship matrix.\n * @param {number[]} faceOrders an array of FOLD spec faceOrders.\n * @returns {number[][]} NxN matrix, number of faces, containing +1,-1,0\n * as values showing the relationship between i to j in face[i][j].\n */\n// export const faceOrdersToMatrix = (faceOrders) => {\n// \tconst faces = [];\n// \tfaceOrders.forEach(order => {\n// \t\tfaces[order[0]] = undefined;\n// \t\tfaces[order[1]] = undefined;\n// \t});\n// \tconst matrix = faces.map(() => []);\n// \tfaceOrders\n// \t\t// .filter((_, i) => orders[condition_keys[i]] !== 0)\n// \t\t.forEach(([a, b, c]) => {\n// \t\t\tmatrix[a][b] = c;\n// \t\t\tmatrix[b][a] = -c;\n// \t\t});\n// \treturn matrix;\n// };\n"
  },
  {
    "path": "src/graph/overlap.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\texclude,\n\texcludeS,\n} from \"../math/compare.js\";\nimport {\n\tdot,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tboundingBox,\n} from \"../math/polygon.js\";\nimport {\n\tintersectLineLine,\n} from \"../math/intersect.js\";\nimport {\n\toverlapLinePoint,\n\toverlapConvexPolygonPoint,\n\toverlapBoundingBoxes,\n\toverlapConvexPolygons,\n} from \"../math/overlap.js\";\nimport {\n\tdoRangesOverlap,\n} from \"../math/range.js\";\nimport {\n\tchooseTwoPairs,\n\tclustersToReflexiveArrays,\n\tarrayArrayToLookupArray,\n} from \"../general/array.js\";\nimport {\n\tgetEdgesLine,\n\tedgesToLines2,\n} from \"./edges/lines.js\";\nimport {\n\tmakeFacesPolygon,\n} from \"./make/faces.js\";\nimport {\n\tmakeEdgesCoords,\n} from \"./make/edges.js\";\nimport {\n\tsweepFaces,\n} from \"./sweep.js\";\nimport {\n\tinvertFlatToArrayMap,\n} from \"./maps.js\";\nimport {\n\tgetVerticesClusters,\n} from \"./vertices/clusters.js\";\n// import {\n// \tgetSimilarEdges,\n// } from \"./edges/duplicate.js\";\n\n/**\n * @description For every face, get a list of all other faces\n * that geometrically overlap this face.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} for every face, a list of overlapping face indices\n */\nexport const getFacesFacesOverlap = ({\n\tvertices_coords, faces_vertices,\n}, epsilon = EPSILON) => {\n\t// these polygons have no collinear vertices\n\tconst facesPolygon = makeFacesPolygon({ vertices_coords, faces_vertices })\n\t\t.map(poly => poly.map(resize2));\n\tconst facesBounds = facesPolygon.map(polygon => boundingBox(polygon));\n\n\t/** @type {number[][]} */\n\tconst facesFacesOverlap = faces_vertices.map(() => []);\n\n\t// store here string-separated face pair keys \"a b\" where a < b\n\t// to avoid doing duplicate work\n\tconst history = {};\n\n\t// as we progress through the line sweep, maintain a list (hash table)\n\t// of the set of faces which are currently overlapping this sweep line.\n\t// at the beginning of an event, add new faces, at the end, remove faces.\n\tconst setOfFaces = [];\n\tsweepFaces({ vertices_coords, faces_vertices }, 0, epsilon)\n\t\t.forEach(({ start, end }) => {\n\t\t\t// these are new faces to the sweep line, add them to the set\n\t\t\tstart.forEach(e => { setOfFaces[e] = true; });\n\n\t\t\t// iterate through the set of all current faces crossed by the line,\n\t\t\t// but compare them only to the list of new faces which just joined.\n\t\t\tsetOfFaces.forEach((_, f1) => start\n\t\t\t\t.filter(f2 => f2 !== f1)\n\t\t\t\t.forEach(f2 => {\n\t\t\t\t\t// prevent ourselves from doing duplicate work\n\t\t\t\t\tconst key = f1 < f2 ? `${f1} ${f2}` : `${f2} ${f1}`;\n\t\t\t\t\tif (history[key]) { return; }\n\t\t\t\t\thistory[key] = true;\n\n\t\t\t\t\t// computing the bounding box overlap first will remove all cases\n\t\t\t\t\t// where the pair of faces are far away in the cross-axis.\n\t\t\t\t\tif (!overlapBoundingBoxes(facesBounds[f1], facesBounds[f2], epsilon)\n\t\t\t\t\t\t|| !overlapConvexPolygons(facesPolygon[f1], facesPolygon[f2], epsilon)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tfacesFacesOverlap[f1].push(f2);\n\t\t\t\t\tfacesFacesOverlap[f2].push(f1);\n\t\t\t\t}));\n\n\t\t\t// these are faces which are leaving the sweep line, remove them from the set\n\t\t\tend.forEach(e => delete setOfFaces[e]);\n\t\t});\n\n\treturn facesFacesOverlap;\n};\n\n/**\n * @description For every edge, a list of other edge indices which are\n * collinear and overlap this edge.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} for every edge, a list of other edges which\n * are collinear and overlap this edge.\n */\nexport const getEdgesEdgesCollinearOverlap = ({\n\tvertices_coords, edges_vertices,\n}, epsilon = EPSILON) => {\n\t//\n\tconst {\n\t\tlines,\n\t\tedges_line,\n\t} = getEdgesLine({ vertices_coords, edges_vertices }, epsilon);\n\n\t// we're going to project each edge onto the shared line, we can't use\n\t// each edge's vector, we have to use the edge's line's common vector.\n\tconst edges_range = makeEdgesCoords({ vertices_coords, edges_vertices })\n\t\t.map((points, e) => points\n\t\t\t.map(point => dot(lines[edges_line[e]].vector, point)));\n\n\t/** @type {number[][]} */\n\tconst edgesEdgesOverlap = edges_vertices.map(() => []);\n\n\tinvertFlatToArrayMap(edges_line)\n\t\t.flatMap(edges => chooseTwoPairs(edges))\n\t\t.filter(pair => {\n\t\t\tconst [a, b] = pair.map(edge => edges_range[edge]);\n\t\t\treturn doRangesOverlap(a, b, epsilon);\n\t\t})\n\t\t.forEach(([a, b]) => {\n\t\t\tedgesEdgesOverlap[a].push(b);\n\t\t\tedgesEdgesOverlap[b].push(a);\n\t\t});\n\n\treturn edgesEdgesOverlap;\n};\n\n/**\n * @description Given a FOLD graph with overlapping components, compute all\n * overlap between vertices and vertices, vertices and edges, edges and edges,\n * and faces and vertices. This leaves out faces and edges, but those can be\n * computed via edges-edges using faces_edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   verticesVertices: boolean[][],\n *   verticesEdges: boolean[][],\n *   edgesEdges: boolean[][],\n *   facesVertices: boolean[][],\n * }}\n */\nexport const getOverlappingComponents = ({\n\tvertices_coords, edges_vertices, faces_vertices,\n}, epsilon = EPSILON) => {\n\t// prepare edges and faces into their geometric forms (line, polygon)\n\tconst vertices_coords2 = vertices_coords.map(resize2);\n\tconst edgesLine = edgesToLines2({ vertices_coords, edges_vertices });\n\tconst facesPolygon = faces_vertices\n\t\t.map(vertices => vertices\n\t\t\t.map(v => vertices_coords2[v]));\n\n\tconst similarVertices = getVerticesClusters({ vertices_coords }, epsilon);\n\tconst verticesVertices = arrayArrayToLookupArray(\n\t\tclustersToReflexiveArrays(similarVertices),\n\t);\n\tvertices_coords.forEach((_, v) => { verticesVertices[v][v] = true; });\n\n\tconst verticesEdges = vertices_coords.map(() => []);\n\tvertices_coords\n\t\t.map((coord, v) => edgesLine\n\t\t\t.map((_, e) => e)\n\t\t\t// .filter(e => overlapLinePoint(edgesLine[e], coord, excludeS, epsilon)\n\t\t\t// \t&& !edges_vertices[e].includes(v))\n\t\t\t.filter(e => overlapLinePoint(edgesLine[e], coord, excludeS, epsilon))\n\t\t\t.forEach(e => { verticesEdges[v][e] = true; }));\n\n\tconst edgesEdges = edges_vertices.map(() => []);\n\tedgesLine\n\t\t.map((line1, e1) => edgesLine\n\t\t\t.map((_, e2) => e2)\n\t\t\t.filter(e2 => e1 !== e2\n\t\t\t\t&& intersectLineLine(line1, edgesLine[e2], excludeS, excludeS, epsilon).point !== undefined)\n\t\t\t.forEach(e2 => {\n\t\t\t\tedgesEdges[e1][e2] = true;\n\t\t\t\tedgesEdges[e2][e1] = true;\n\t\t\t}));\n\t// edges_vertices.forEach((_, e) => { edgesEdges[e][e] = true; });\n\n\tconst facesVertices = faces_vertices.map(() => []);\n\tfacesPolygon\n\t\t.map((polygon, f) => vertices_coords\n\t\t\t.map((_, v) => v)\n\t\t\t// .filter(v => !faces_vertices[f].includes(v)\n\t\t\t// \t&& overlapConvexPolygonPoint(polygon, vertices_coords[v], exclude, epsilon).overlap)\n\t\t\t.filter(v => overlapConvexPolygonPoint(\n\t\t\t\tpolygon,\n\t\t\t\tvertices_coords2[v],\n\t\t\t\texclude,\n\t\t\t\tepsilon,\n\t\t\t).overlap)\n\t\t\t.forEach(v => { facesVertices[f][v] = true; }));\n\n\treturn {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t};\n};\n\n/**\n * @description Given a FOLD object with overlapping components (typically a\n * folded form), get all overlapping faces edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} for every face, a list of edge indices\n * which overlap this face.\n */\nexport const getFacesEdgesOverlap = ({\n\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n}, epsilon = EPSILON) => {\n\t// - vertex overlaps: taken from both vertices-vertices and edges-vertices\n\t//   using faces_vertices[face]\n\t// - edge overlaps: taken from edges-edges using faces_edges[face] and\n\t//   vertex-edge overlaps\n\t// - point overlaps: taken from vertices-polygon overlaps. simple.\n\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = getOverlappingComponents({\n\t\tvertices_coords, edges_vertices, faces_vertices,\n\t}, epsilon);\n\n\t/**\n\t * @description Given a face's vertices, and a list of vertex indices\n\t * in no particular order, filter out any pairs of indices which are\n\t * neighbors according to the ordering in face_vertices.\n\t * @param {number[]} face_vertices the vertices of one face\n\t * @param {number[]} indices a list of vertices from this face\n\t * @returns {number[]} a copy of indices, where all neighbor indices\n\t * have been removed.\n\t */\n\tconst filterFaceVerticesNeighbors = (face_vertices, indices) => {\n\t\t// create a lookup for all indices\n\t\tconst lookup = {};\n\t\tindices.forEach(i => { lookup[i] = true; });\n\n\t\t// iterate through face_vertices, take each adjacent pairwise combination\n\t\t// of vertices, if \"lookup\" contains both pairs of vertices, we can mark\n\t\t// both vertices as \"false\" (to be removed).\n\t\tface_vertices\n\t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t\t.filter(([a, b]) => lookup[a] && lookup[b])\n\t\t\t.forEach(([a, b]) => {\n\t\t\t\tlookup[a] = false;\n\t\t\t\tlookup[b] = false;\n\t\t\t});\n\t\treturn indices.filter(i => lookup[i]);\n\t};\n\n\t/**\n\t * @description Get the vertices involved in the overlap between\n\t * an edge and a face. Get the vertices of the edge which are a\n\t * @example\n\t * - an edge crossing a face at two vertices should return both\n\t * of the face's vertices, but only if they are not adjacent in the face.\n\t * - a square face with vertices (4, 5, 6, 7) and an edge\n\t * between vertices (4, 6) should return vertices (4, 6).\n\t * - a square face with vertices (4, 5, 6, 7) and an edge\n\t * between vertices (8, 9) where 8 and 9 lie on top of 4 and 6 respectively\n\t * should return vertices (4, 6).\n\t * @param {number} edge an edge index\n\t * @param {number} face a face index\n\t * @returns {number[]} a list of vertices involved in the overlap\n\t */\n\tconst getVerticesOverlap = (edge, face) => {\n\t\t// a list of the face's vertices which this edge crosses and\n\t\t// overlaps somewhere in the interior (not endpoints) of the edge.\n\t\tconst verticesInterior = faces_vertices[face]\n\t\t\t.filter(v => verticesEdges[v][edge]);\n\n\t\t// a list of the face's vertices which lie on top of one of the edge's\n\t\t// endpoints. This can include an edge endpoint itself\n\t\t// if it is also a face vertex.\n\t\tconst [v0, v1] = edges_vertices[edge];\n\t\tconst verticesEndpoints = faces_vertices[face]\n\t\t\t.filter(v => verticesVertices[v][v0] || verticesVertices[v][v1]);\n\n\t\t// get the union of both arrays (no duplicates), run it through the filter\n\t\t// method which will remove vertices if they are neighbors in the face.\n\t\treturn filterFaceVerticesNeighbors(\n\t\t\tfaces_vertices[face],\n\t\t\tArray.from(new Set([...verticesEndpoints, ...verticesInterior])),\n\t\t);\n\t};\n\n\tconst getEdgesOverlap = (edge, face) => {\n\t\tconst [v0, v1] = edges_vertices[edge];\n\t\tconst edgesEndpoints = faces_edges[face]\n\t\t\t.filter(e => verticesEdges[v0][e]\n\t\t\t\t|| verticesEdges[v1][e]);\n\t\tconst edgesMiddle = faces_edges[face]\n\t\t\t.filter(e => edgesEdges[edge][e]);\n\t\treturn Array.from(new Set([...edgesEndpoints, ...edgesMiddle]));\n\t};\n\n\tconst getPointsOverlap = (edge, face) => edges_vertices[edge]\n\t\t.filter(v => facesVertices[face][v]);\n\n\t// for every face, gather all intersected edges and vertices and filter\n\t//   by the formula i've written many times, either: edges + vertces = 2\n\t//   where vertices are not adjacent\n\t/** @type {number[][]} */\n\tconst facesEdgesOverlap = faces_vertices.map(() => []);\n\tfaces_vertices.forEach((_, face) => edges_vertices.forEach((__, edge) => {\n\t\tconst vertices = getVerticesOverlap(edge, face);\n\t\tconst edges = getEdgesOverlap(edge, face);\n\t\tconst points = getPointsOverlap(edge, face);\n\t\tif (vertices.length + edges.length + points.length !== 2) { return; }\n\t\t// filter faces which have one edge being crossed and\n\t\t// one vertex being overlapped, and if that edge's edges_vertices\n\t\t// contains this vertex, then the \"overlap\" is simply a collinear overlap.\n\t\tif (vertices.length === 1 && edges.length === 1) {\n\t\t\tif (edges_vertices[edges[0]].includes(vertices[0])) { return; }\n\t\t}\n\n\t\tfacesEdgesOverlap[face].push(edge);\n\t}));\n\n\treturn facesEdgesOverlap;\n};\n\n// a valid face has 2 of these conditions:\n// - face_vertices which are crossed by this edge\n// - face_edges which are crossed by this edge\n// - this edge's one or two endpoints lie inside of this face\n\n// the data we have available is:\n// - vertices-vertices overlap: are edge endpoints touching or not?\n// - edges-vertices-overlap, exclusive.\n// - edges-edges, exclusive\n// - vertices-polygon-overlap, exclusive. (must be exclusive)\n// and how this plays out in terms of between a face and an edge\n// - vertex overlaps: taken from both vertices-vertices and edges-vertices\n//   using faces_vertices[face]\n// - edge overlaps: taken from edges-edges using faces_edges[face]\n// - point overlaps: taken from vertices-polygon overlaps. simple.\n\n// alternatively:\n// - edges-vertices-overlap, exclusive. this edge, iterate over face_vertices\n//   exclude case where 2 vertices make up an edge\n// - edges-edges, exclusive\n// - vertices-polygon-overlap, inclusive\n// this does not work because vertices-polygon overlap now includes cases where\n// both points of the same edge lie along a single polygon boundary edge.\n\n/**\n * @description Given a FOLD object with overlapping components (typically a\n * folded form), get all overlapping faces edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} for every edge, a list of face indices\n * which overlap this edge.\n */\nexport const getEdgesFacesOverlap = ({\n\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n}, epsilon = EPSILON) => {\n\tconst edgesFaces = edges_vertices.map(() => []);\n\tconst facesEdges = getFacesEdgesOverlap({\n\t\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n\t}, epsilon);\n\tfacesEdges\n\t\t.forEach((edges, face) => edges\n\t\t\t.forEach(edge => {\n\t\t\t\tedgesFaces[edge].push(face);\n\t\t\t}));\n\treturn edgesFaces;\n};\n"
  },
  {
    "path": "src/graph/planarize/intersectAllEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeS,\n\tepsilonEqual,\n} from \"../../math/compare.js\";\nimport {\n\tintersectLineLine,\n} from \"../../math/intersect.js\";\nimport {\n\tedgesToLines2,\n} from \"../edges/lines.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\n\n/**\n * @param {FOLD} graph a FOLD object\n * @todo line sweep\n */\nexport const intersectAllEdges = ({\n\tvertices_coords,\n\tvertices_edges,\n\tedges_vertices,\n}, epsilon = EPSILON) => {\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst edgesEdgesLookup = edges_vertices.map(() => ({}));\n\tedges_vertices.forEach((vertices, e) => vertices\n\t\t.flatMap(v => vertices_edges[v])\n\t\t.forEach(edge => {\n\t\t\tedgesEdgesLookup[e][edge] = true;\n\t\t\tedgesEdgesLookup[edge][e] = true;\n\t\t}));\n\tconst lines = edgesToLines2({ vertices_coords, edges_vertices });\n\tconst results = [];\n\t// todo: line sweep\n\tfor (let i = 0; i < edges_vertices.length - 1; i += 1) {\n\t\tfor (let j = i + 1; j < edges_vertices.length; j += 1) {\n\t\t\tif (edgesEdgesLookup[i][j]) { continue; }\n\t\t\tconst { a, b, point } = intersectLineLine(\n\t\t\t\tlines[i],\n\t\t\t\tlines[j],\n\t\t\t\tincludeS,\n\t\t\t\tincludeS,\n\t\t\t\tepsilon,\n\t\t\t);\n\t\t\tif (point) {\n\t\t\t\tif ((epsilonEqual(a, 0) || epsilonEqual(a, 1))\n\t\t\t\t\t&& (epsilonEqual(b, 0) || epsilonEqual(b, 1))) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tresults.push({ i, j, a, b, point });\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n};\n"
  },
  {
    "path": "src/graph/planarize/makeFacesMap.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tchooseTwoPairs,\n\tuniqueElements,\n} from \"../../general/array.js\";\nimport {\n\tconnectedComponents,\n} from \"../connectedComponents.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../make/edgesFaces.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../make/facesEdges.js\";\nimport {\n\tmakeFacesFaces,\n} from \"../make/facesFaces.js\";\nimport {\n\tinvertArrayMap,\n\tinvertFlatToArrayMap,\n} from \"../maps.js\";\nimport {\n\tminimumSpanningTrees,\n} from \"../trees.js\";\n\n/**\n * @param {FOLD} oldGraph a FOLD object\n * @param {FOLD} newGraph a FOLD object\n * @param {{ vertices: { map: number[][] }, edges: { map: number[][] }}} changes\n * @returns {number[][]} a face map\n */\nexport const makeFacesMap = (\n\toldGraph,\n\tnewGraph,\n\tchanges,\n) => {\n\tif (!oldGraph.faces_vertices) { return []; }\n\t// get rid of the undefinedes, these will break the connected components\n\tconst faces_edgesNew = newGraph.faces_edges || makeFacesEdgesFromVertices(newGraph);\n\tconst faces_facesNew = makeFacesFaces(newGraph)\n\t\t.map(arr => arr.filter(a => a !== null && a !== undefined));\n\tconst edges_facesNew = makeEdgesFacesUnsorted(newGraph);\n\tconst edges_facesOld = makeEdgesFacesUnsorted(oldGraph);\n\n\t// this must work even with disjoint sets of faces. get the connected sets of\n\t// faces and invert so that the array contains arrays of (connected) faces.\n\t// if all faces connect, \"groups\" will contain only one array.\n\tconst connectedFaces = connectedComponents(faces_facesNew);\n\tconst facesGroups = invertFlatToArrayMap(connectedFaces);\n\tconst edgesGroups = facesGroups\n\t\t.map(faces => faces.flatMap(f => faces_edgesNew[f]))\n\t\t.map(uniqueElements);\n\n\t// for each group, get the first edge which contains only one adjacent face\n\t// convert this list of edges into a list of faces. these are the starting\n\t// faces from which we build the minimum spanning tree.\n\t// These relate to the newGraph.\n\tconst startEdges = edgesGroups\n\t\t.map(group => group.find(e => edges_facesNew[e].length === 1));\n\tconst startFaces = startEdges\n\t\t.map(edge => edges_facesNew[edge][0]);\n\n\t//\n\tconst facesEdgeMap = {};\n\tedges_facesNew\n\t\t.forEach((faces, e) => chooseTwoPairs(faces)\n\t\t\t.forEach(pair => {\n\t\t\t\tfacesEdgeMap[pair.join(\" \")] = e;\n\t\t\t\tfacesEdgeMap[pair.reverse().join(\" \")] = e;\n\t\t\t}));\n\n\t// console.log(\"faces_facesNew\", faces_facesNew);\n\t// console.log(\"connectedFaces\", connectedFaces);\n\t// console.log(\"facesGroups\", facesGroups);\n\t// console.log(\"edgesGroups\", edgesGroups);\n\t// console.log(\"startEdges\", startEdges);\n\t// console.log(\"startFaces\", startFaces);\n\t// console.log(\"facesEdgeMap\", facesEdgeMap);\n\t// console.log(\"changes.edges.map\", changes.edges.map);\n\t// console.log(\"edgesBackmap\", edgesBackmap);\n\n\tconst edgesBackmap = invertArrayMap(changes.edges.map);\n\tconst faceBackMapSets = [];\n\tminimumSpanningTrees(faces_facesNew, startFaces)\n\t\t.forEach((tree, t) => {\n\t\t\tconst rootRow = tree.shift();\n\t\t\tif (!rootRow || !rootRow.length) { return; }\n\t\t\t// root tree item is the first item in the first row (only item in the row)\n\t\t\tconst root = rootRow[0];\n\t\t\tconst startNewEdge = startEdges[t];\n\t\t\tconst startOldEdges = edgesBackmap[startNewEdge];\n\t\t\tfaceBackMapSets[root.index] = new Set(startOldEdges.flatMap(edge => edges_facesOld[edge]));\n\t\t\ttree.forEach(level => level.forEach(({ index, parent }) => {\n\t\t\t\tconst edge = facesEdgeMap[`${index} ${parent}`];\n\t\t\t\tconst oldEdges = edgesBackmap[edge];\n\t\t\t\tconst oldFaces = oldEdges.flatMap(e => edges_facesOld[e]);\n\t\t\t\t// create a copy of the parent's faces, modify it and set it to this face.\n\t\t\t\tconst parentFaces = new Set(faceBackMapSets[parent]);\n\t\t\t\t// add oldFaces to the stack if they don't exist, remove if they do.\n\t\t\t\toldFaces.forEach(f => (parentFaces.has(f) ? parentFaces.delete(f) : parentFaces.add(f)));\n\t\t\t\tfaceBackMapSets[index] = parentFaces;\n\t\t\t}));\n\t\t});\n\tconst faceBackMap = faceBackMapSets.map(set => Array.from(set));\n\treturn invertArrayMap(faceBackMap);\n};\n"
  },
  {
    "path": "src/graph/planarize/planarize.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tinvertArrayMap,\n\tmergeNextmaps,\n} from \"../maps.js\";\nimport {\n\tmakePlanarFaces,\n} from \"../make/faces.js\";\nimport {\n\tmakeEdgesVerticesFromFaces,\n} from \"../make/edgesVertices.js\";\nimport {\n\tplanarizeCollinearEdges,\n} from \"./planarizeCollinearEdges.js\";\nimport {\n\tplanarizeOverlaps,\n} from \"./planarizeOverlaps.js\";\nimport {\n\tplanarizeCollinearVertices,\n} from \"./planarizeCollinearVertices.js\";\nimport {\n\tmakeFacesMap,\n} from \"./makeFacesMap.js\";\n\n/**\n * @description We need edges to planarize, if a graph is missing\n * edges_vertices but contains faces_vertices, we can use the face outlines\n * as edges and planarize those. This will create edges_vertices from\n * faces_vertices for the purpose of planarizing.\n * @param {FOLD} graph a FOLD object\n * @returns {FOLD} an edges_vertices array\n */\nconst makeSureEdgesExist = (graph) => {\n\tif (graph.edges_vertices) { return graph; }\n\t// exit with an empty graph if no faces exist. nothing to planarize\n\t// no need to check faces_edges, if there are no edges_vertices then\n\t// faces_edges doesn't help us because we would still need to connect\n\t// edges to vertices.\n\tif (!graph.faces_vertices) { return {}; }\n\treturn {\n\t\t...graph,\n\t\tedges_vertices: makeEdgesVerticesFromFaces(graph),\n\t};\n};\n\n/**\n * @description Using a face's map, remove all faces which exist in the\n * new planar graph which have no mapping to any previously existing face\n * implying that these faces were never faces but holes in the graph.\n * @param {FOLD} graph a FOLD object\n * @param {number[][]} facesNextmap\n */\nconst removeHoles = (graph, facesNextmap) => {\n\tconst backmap = invertArrayMap(facesNextmap);\n\tgraph.faces_vertices\n\t\t.map((_, i) => i)\n\t\t.filter(i => backmap[i] === undefined)\n\t\t.forEach(f => {\n\t\t\tdelete graph.faces_vertices[f];\n\t\t\tdelete graph.faces_edges[f];\n\t\t});\n};\n\n/**\n * @description Planarize a graph into the 2D XY plane forming a valid\n * planar graph. This will neatly resolve any overlapping collinear edges,\n * resolve all crossing edges, but do nothing with face information.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   result: FOLD,\n *   changes: {\n *     vertices: { map: number[][] },\n *     edges: { map: number[][] },\n *   }\n * }}\n */\nexport const planarizeEdgesVerbose = (graph, epsilon = EPSILON) => {\n\t// first step: resolve all collinear and overlapping edges,\n\t// after this point, no two edges parallel-overlap each other.\n\tconst {\n\t\tresult: graphNonCollinear,\n\t\tchanges: {\n\t\t\tvertices: { map: verticesMap1 },\n\t\t\tedges: { map: edgesMap1 },\n\t\t\t// edges_line,\n\t\t\t// lines,\n\t\t},\n\t} = planarizeCollinearEdges(graph, epsilon);\n\n\t// second step: resolve all crossing edges,\n\t// after this point the graph is planar, no two edges overlap.\n\tconst {\n\t\tresult: graphNoOverlaps,\n\t\tchanges: {\n\t\t\tvertices: { map: verticesMap2 },\n\t\t\tedges: { map: edgesMap2 },\n\t\t}\n\t} = planarizeOverlaps(graphNonCollinear, epsilon);\n\n\t// third step: remove all degree-2 vertices which lie between\n\t// two parallel edges of the same assignment (currently, any assignment)\n\tconst {\n\t\tresult: planarGraph,\n\t\tchanges: {\n\t\t\tvertices: { map: verticesMap3 },\n\t\t\tedges: { map: edgesMap3 }\n\t\t}\n\t} = planarizeCollinearVertices(graphNoOverlaps, epsilon);\n\n\tconst vertexNextMap = mergeNextmaps(verticesMap1, verticesMap2, verticesMap3);\n\tconst edgeNextMap = mergeNextmaps(edgesMap1, edgesMap2, edgesMap3);\n\n\treturn {\n\t\tresult: planarGraph,\n\t\tchanges: {\n\t\t\tvertices: { map: vertexNextMap },\n\t\t\tedges: { map: edgeNextMap },\n\t\t}\n\t};\n};\n\n/**\n * @description Planarize a graph into the 2D XY plane forming a valid\n * planar graph. This will neatly resolve any overlapping collinear edges,\n * resolve all crossing edges, but do nothing with face information.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FOLD}\n */\nexport const planarizeEdges = ({\n\tvertices_coords,\n\tedges_vertices,\n\tedges_assignment,\n\tedges_foldAngle,\n}, epsilon = EPSILON) => {\n\t// first step: resolve all collinear and overlapping edges,\n\t// after this point, no two edges parallel-overlap each other.\n\tconst { result: result1 } = planarizeCollinearEdges({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t}, epsilon);\n\n\t// second step: resolve all crossing edges,\n\t// after this point the graph is planar, no two edges overlap.\n\tconst { result: result2 } = planarizeOverlaps(result1, epsilon);\n\n\t// third step: remove all degree-2 vertices which lie between\n\t// two parallel edges of the same assignment (currently, any assignment)\n\tconst { result: result3 } = planarizeCollinearVertices(result2, epsilon);\n\treturn result3;\n};\n\n/**\n * @description Planarize a graph into the 2D XY plane forming a valid\n * planar graph. This will neatly resolve any overlapping collinear edges,\n * resolve all crossing edges, rebuild faces, and remove holes.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   result: FOLD,\n *   changes: {\n *     vertices: { map: number[][] },\n *     edges: { map: number[][] },\n *     faces: { map: number[][] },\n *   }\n * }}\n */\nexport const planarizeVerbose = (graph, epsilon = EPSILON) => {\n\tconst graphWithEdges = makeSureEdgesExist(graph);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices: { map: vertexNextMap },\n\t\t\tedges: { map: edgeNextMap },\n\t\t}\n\t} = planarizeEdgesVerbose(graphWithEdges, epsilon);\n\n\tconst {\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t} = makePlanarFaces(result);\n\tresult.faces_vertices = faces_vertices;\n\tresult.faces_edges = faces_edges;\n\n\tconst faceMap = makeFacesMap(graphWithEdges, result, {\n\t\tvertices: { map: vertexNextMap },\n\t\tedges: { map: edgeNextMap },\n\t});\n\n\tremoveHoles(result, faceMap);\n\n\treturn {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices: { map: vertexNextMap },\n\t\t\tedges: { map: edgeNextMap },\n\t\t\tfaces: { map: faceMap },\n\t\t}\n\t};\n};\n\n/**\n * @description Planarize a graph into the 2D XY plane forming a valid\n * planar graph. This will neatly resolve any overlapping collinear edges,\n * resolve all crossing edges, rebuild faces, and remove holes.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FOLD} a planar graph\n */\nexport const planarize = (graph, epsilon = EPSILON) => {\n\t// unfortunately, for removeHoles to work, we need to create the face map,\n\t// which requires the edge map, which require we run the verbose methods\n\t// and capture all change data. so this method does not save any time,\n\t// it only simplifies the type of the return object,\n\tconst { result } = planarizeVerbose(graph, epsilon);\n\treturn result;\n};\n"
  },
  {
    "path": "src/graph/planarize/planarizeCollinearEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tepsilonEqual,\n} from \"../../math/compare.js\";\nimport {\n\tdot2,\n\tsubtract2,\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tuniqueElements,\n} from \"../../general/array.js\";\nimport {\n\tclusterSortedGeneric,\n} from \"../../general/cluster.js\";\nimport {\n\tinvertFlatToArrayMap,\n\tinvertArrayMap,\n\tinvertArrayToFlatMap,\n} from \"../maps.js\";\nimport {\n\tgetEdgesLine,\n} from \"../edges/lines.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\n\n/**\n * @param {number[][][]} lines_verticesClusters\n * @returns {number[]} a nextmap for each vertex\n */\nconst lineVertexClustersToNewVertices = (lines_verticesClusters) => {\n\tconst nextMap = [];\n\tlet newIndex = 0;\n\tlines_verticesClusters\n\t\t.map(clusters => clusters\n\t\t\t.map(verticesCluster => {\n\t\t\t\tconst match = verticesCluster.map(v => nextMap[v]).shift();\n\t\t\t\tconst matchFound = match !== undefined;\n\t\t\t\tconst index = matchFound ? match : newIndex;\n\t\t\t\tverticesCluster.forEach(v => { nextMap[v] = index; });\n\t\t\t\treturn matchFound ? match : newIndex++;\n\t\t\t}));\n\t// it's possible for two or more vertices to lie at the same point and\n\t// each be involved in two or more crossing lines (folded form windmill base).\n\t// as a result, multiple lines are trying to claim the same vertex, causing\n\t// nextMap[v] above to be overwritten multiple times, one of those values\n\t// possibly getting left out, causing a situation where when the backmap is\n\t// created, a row is missing. a real simple and elegant solution is simply to\n\t// create the backmap, remove any empty rows, then convert it back into\n\t// a next map, this will decrement all indices to cover the gaps in the counting.\n\tconst backMap = invertFlatToArrayMap(nextMap).filter(a => a);\n\treturn invertArrayToFlatMap(backMap);\n};\n\n/**\n * @param {number[][][]} lines_edgesClusters\n */\nconst lineEdgeClustersToNewEdges = (lines_edgesClusters) => {\n\tconst map = [];\n\tlet newIndex = 0;\n\t// edgeClusters can contain empty arrays which are the gap between\n\t// collinear edges where no edge exists, this area does not get turned\n\t// into an edge, and should be skipped.\n\tlines_edgesClusters\n\t\t.map(clusters => clusters\n\t\t\t.map(cluster => {\n\t\t\t\tcluster\n\t\t\t\t\t.filter(i => map[i] === undefined)\n\t\t\t\t\t.forEach(i => { map[i] = []; });\n\t\t\t\tcluster.forEach(i => map[i].push(newIndex));\n\t\t\t\treturn cluster.length ? newIndex++ : newIndex;\n\t\t\t}));\n\treturn map;\n};\n\n/**\n * @description When more than one overlapping edge is going to be merged\n * into one, we need to know which assignment to carry over. This is a\n * subjective ranking of which assignment should win out (lower is better).\n * The logic goes like this:\n * - boundary is most important and similarly are cut lines.\n * - valley and mountain come before anything remaining.\n * - join and flat, it's unclear which should come first, to be honest.\n * - unassigned should be last. it is the absense of information. everything\n *   should be able to override unassigned.\n */\nconst assignmentPriority = { B: 1, C: 2, V: 3, M: 4, J: 5, F: 6, U: 7 };\nObject.keys(assignmentPriority).forEach(key => {\n\tassignmentPriority[key.toLowerCase()] = assignmentPriority[key];\n});\n\n/**\n * @description Given a list of assignments of overlapping edges, competing\n * to be the assignment which \"wins out\", return the index in the array\n * which has the best priority value, according to the priority lookup.\n * @param {string[]} assignments a list of edges_assignments\n * @returns {number} the index of the input array with the highest priority\n */\nconst highestPriorityAssignmentIndex = (assignments) => {\n\tif (assignments.length === 1) { return 0; }\n\tlet index = 0;\n\tassignments.forEach((a, i) => {\n\t\tif (assignmentPriority[a] < assignmentPriority[assignments[index]]) {\n\t\t\tindex = i;\n\t\t}\n\t});\n\treturn index;\n}\n\n/**\n * @description Make one step to planarize a graph into the 2D XY plane\n * by fixing all instances of two collinear edges overlapping. This does not\n * resolve edges crossing edges, or multiple vertices overlapping at the same\n * coordinate. Call \"planarize\" instead for the complete method.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   result: FOLD,\n *   changes: {\n *     vertices: { map: number[] },\n *     edges: { map: number[][] },\n *   }\n * }}\n * a new FOLD object, with\n * an info object which describes all changes to the graph.\n */\nexport const planarizeCollinearEdges = ({\n\tvertices_coords,\n\tedges_vertices,\n\tedges_assignment,\n\tedges_foldAngle,\n}, epsilon = EPSILON) => {\n\tconst {\n\t\tlines,\n\t\tedges_line,\n\t} = getEdgesLine({ vertices_coords, edges_vertices }, epsilon);\n\n\tconst vertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\n\t// one to many mapping of a line and the edges along it.\n\tconst lines_edges = invertFlatToArrayMap(edges_line);\n\n\t// for every line, project every vertex down onto the line, sort the list\n\t// in order of the parameter along the line. this only includes vertices\n\t// in edges which lie collinear to the line, it does not include other\n\t// orthogonal edges which happen to have one collinear vertex.\n\t// this method ignores these types of overlap entirely, dealing with these\n\t// happens if you instead call the main \"planarize\" method.\n\tconst lines_verticesInfo = lines_edges\n\t\t.map(edges => uniqueElements(edges.flatMap(edge => edges_vertices[edge])))\n\t\t.map((vertices, l) => vertices\n\t\t\t.map(v => ({\n\t\t\t\tv,\n\t\t\t\tp: dot2(subtract2(vertices_coords[v], lines[l].origin), lines[l].vector),\n\t\t\t})).sort((a, b) => a.p - b.p));\n\n\tconst lines_vertices = lines_verticesInfo\n\t\t.map(objs => objs.map(({ v }) => v));\n\tconst lines_verticesParameter = lines_verticesInfo\n\t\t.map(objs => objs.map(({ p }) => p));\n\n\t// for each line, all vertices along the line are put into arrays where\n\t// similar vertices are grouped into the same list. even unique vertices\n\t// are placed into arrays with just one item.\n\tconst lines_verticesClusters = lines_verticesParameter\n\t\t.map((params, l) => clusterSortedGeneric(params, epsilonEqual)\n\t\t\t.map(cluster => cluster.map(i => lines_vertices[l][i])));\n\n\tconst vertexNextMap = lineVertexClustersToNewVertices(lines_verticesClusters);\n\tconst vertexBackMap = invertFlatToArrayMap(vertexNextMap);\n\n\t// along each line, for every fencepost between clusters of vertices,\n\t// each fencepost contains a list of edges which are currently between\n\t// these two vertices. it can contain an empty list which represents a\n\t// gap between collinear edges, where no edge exists in this gap.\n\t// [[ 12, 28], [29], [], [13]]\n\t// a graph with circular edges will break here.\n\tconst lines_edgesClusters = lines_verticesClusters\n\t\t.map((verticesClusters, l) => {\n\t\t\t// this lookup contains all edges which lie along this line\n\t\t\tconst edgesLookup = {};\n\t\t\tlines_edges[l].forEach(e => { edgesLookup[e] = true; });\n\t\t\t// push edges onto the stack and pop them off.\n\t\t\t/** @type {Set<number>} */\n\t\t\tconst edges = new Set();\n\t\t\t// fencepost between the clusters of vertices to make new edges\n\t\t\treturn Array\n\t\t\t\t.from(Array(verticesClusters.length - 1))\n\t\t\t\t.map((_, i) => verticesClusters[i])\n\t\t\t\t.map(vertices => {\n\t\t\t\t\t// we want to return a list of all edges which are currently on the stack.\n\t\t\t\t\t// these are edges which either start or end at this point along the line.\n\t\t\t\t\tconst adjacentEdges = uniqueElements(vertices\n\t\t\t\t\t\t.flatMap(vertex => vertices_edges[vertex])\n\t\t\t\t\t\t// only edges which are along this line are allowed\n\t\t\t\t\t\t.filter(edge => edgesLookup[edge]));\n\t\t\t\t\t// true: if the edge is not already on the stack, false if it is.\n\t\t\t\t\tconst adjacentEdgesIsNew = adjacentEdges.map(e => !edges.has(e));\n\t\t\t\t\tadjacentEdges.forEach((edge, i) => (adjacentEdgesIsNew[i]\n\t\t\t\t\t\t? edges.add(edge)\n\t\t\t\t\t\t: edges.delete(edge)));\n\t\t\t\t\treturn Array.from(edges);\n\t\t\t\t});\n\t\t});\n\n\t/** @type {[number, number][]} */\n\tconst newEdgesVertices = lines_verticesClusters\n\t\t// .map(clusters => clusters.map(cluster => vertexNextMap[cluster[0]][0]))\n\t\t.map(clusters => clusters.map(cluster => vertexNextMap[cluster[0]]))\n\t\t.flatMap((vertices, i) => Array\n\t\t\t.from(Array(vertices.length - 1))\n\t\t\t// if the edgeCluster is empty, no edge exists between these vertices\n\t\t\t.map((_, j) => (lines_edgesClusters[i][j].length\n\t\t\t\t? [vertices[j], vertices[j + 1]]\n\t\t\t\t: undefined)))\n\t\t// filter out the undefineds, where no edge exists between vertices\n\t\t.filter(a => a !== undefined)\n\t\t.map(([a, b]) => [a, b]);\n\n\tconst edgesNextMap = lineEdgeClustersToNewEdges(lines_edgesClusters);\n\n\tconst newVerticesCoords = vertexBackMap\n\t\t.map(vertices => vertices_coords[vertices[0]])\n\t\t.map(resize2);\n\n\tconst result = {\n\t\tvertices_coords: newVerticesCoords,\n\t\tedges_vertices: newEdgesVertices,\n\t};\n\n\tif (edges_assignment || edges_foldAngle) {\n\t\tconst edgesBackMap = invertArrayMap(edgesNextMap);\n\t\t// for the trivial cases (new edge maps to one old edge) simply carry over\n\t\t// the previous assignment and fold angle (index 0 in backmap inner array).\n\t\t// any other time, when a new edge comes from more than one previous edge,\n\t\t// we have to choose which assignment \"wins out\". This list contains,\n\t\t// for every edge, the index in the backmap (0, 1, 2...) of the edge which\n\t\t// \"wins out\", use this edge to carry over assignment/foldAngle if exists.\n\t\tconst edgesBackMapIndexToUse = edges_assignment\n\t\t\t? edgesBackMap\n\t\t\t\t.map(edges => edges.map(edge => edges_assignment[edge]))\n\t\t\t\t.map(highestPriorityAssignmentIndex)\n\t\t\t: edgesBackMap.map(() => 0);\n\t\tif (edges_assignment) {\n\t\t\tresult.edges_assignment = edgesBackMapIndexToUse\n\t\t\t\t.map((index, i) => edges_assignment[edgesBackMap[i][index]]);\n\t\t}\n\t\tif (edges_foldAngle) {\n\t\t\tresult.edges_foldAngle = edgesBackMapIndexToUse\n\t\t\t\t.map((index, i) => edges_foldAngle[edgesBackMap[i][index]]);\n\t\t}\n\t}\n\n\t// vertex list can only shrink\n\t// edge list may expand or shrink\n\tconst changes = {\n\t\tvertices: { map: vertexNextMap },\n\t\tedges: { map: edgesNextMap },\n\t\tedges_line,\n\t\tlines,\n\t};\n\n\treturn { result, changes };\n};\n"
  },
  {
    "path": "src/graph/planarize/planarizeCollinearVertices.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tisVertexCollinear,\n\t// removeCollinearVertex, // this method could move here someday\n} from \"../vertices/collinear.js\";\nimport {\n\tremoveDuplicateEdges,\n} from \"../edges/duplicate.js\";\nimport {\n\tmergeNextmaps,\n} from \"../maps.js\";\nimport {\n\tremove,\n} from \"../remove.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\n\n/**\n * @description This will remove a collinear vertex between two edges by\n * rebuilding the first edge (the one with the smaller index) to span across\n * the two adjacent vertices, leaving the other edge in place and containing\n * invalid info. This method returns the index of the edge to be removed.\n * No edges are removed or added, indices do not shift around.\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex\n * @returns {number} the index of the edge which should be removed.\n */\nconst removeCollinearVertex = ({ edges_vertices, vertices_edges }, vertex) => {\n\t// edges[0] will remain. edges[1] will be removed\n\t// [0] and [1] are sorted so that [0] < [1].\n\tconst edges = vertices_edges[vertex].sort((a, b) => a - b);\n\n\t// for each edge, the other vertex (not the vertex they share in common)\n\tconst [v0, v1] = edges\n\t\t.flatMap(e => edges_vertices[e])\n\t\t.filter(v => v !== vertex)\n\n\t/** @type {[number, number]} */\n\tedges_vertices[edges[0]] = [v0, v1];\n\tedges_vertices[edges[1]] = undefined;\n\n\t[v0, v1].forEach(v => {\n\t\tconst oldEdgeIndex = vertices_edges[v].indexOf(edges[1]);\n\t\tif (oldEdgeIndex === -1) { return; }\n\t\tvertices_edges[v][oldEdgeIndex] = edges[0];\n\t});\n\treturn edges[1];\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   result: FOLD,\n *   changes: {\n *     vertices: { map: number[] },\n *     edges: { map: number[][] },\n *   }\n * }}\n */\nexport const planarizeCollinearVertices = (\n\tgraph,\n\t// { vertices_coords, vertices_edges, edges_vertices, edges_assignment, edges_foldAngle },\n\tepsilon = EPSILON,\n) => {\n\t// if (!vertices_edges) {\n\t// \tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t// }\n\tif (!graph.vertices_edges) {\n\t\tgraph.vertices_edges = makeVerticesEdgesUnsorted(graph);\n\t}\n\n\t// const graph = {\n\t// \tvertices_coords,\n\t// \tvertices_edges,\n\t// \tedges_vertices,\n\t// \tedges_assignment,\n\t// \tedges_foldAngle,\n\t// };\n\n\tconst collinearVertices = graph.vertices_edges\n\t\t.map((edges, i) => (edges.length === 2 ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.filter(v => isVertexCollinear(graph, v, epsilon))\n\t\t.reverse();\n\n\t// const collinearVertices = vertices_edges\n\t// \t.map((edges, i) => (edges.length === 2 ? i : undefined))\n\t// \t.filter(a => a !== undefined)\n\t// \t.filter(v => isVertexCollinear({ vertices_coords, vertices_edges, edges_vertices }, v, epsilon))\n\t// \t.reverse();\n\n\t// console.log(\"collinearVertices\", result.vertices_edges\n\t// \t.map((edges, i) => (edges.length === 2 ? i : undefined))\n\t// \t.filter(a => a !== undefined));\n\n\tconst edgesToRemove = collinearVertices\n\t\t.map(v => removeCollinearVertex(graph, v));\n\n\tdelete graph.vertices_edges;\n\n\tconst edgesMapCollinear = remove(graph, \"edges\", edgesToRemove);\n\tconst verticesMap = remove(graph, \"vertices\", collinearVertices);\n\tconst { map: edgesMapDuplicates } = removeDuplicateEdges(graph);\n\tconst edgesMap = mergeNextmaps(edgesMapCollinear, edgesMapDuplicates);\n\n\treturn {\n\t\tresult: graph,\n\t\tchanges: {\n\t\t\tvertices: { map: verticesMap },\n\t\t\tedges: { map: edgesMap },\n\t\t},\n\t}\n};\n"
  },
  {
    "path": "src/graph/planarize/planarizeMakeFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakePlanarFaces,\n} from \"../make/faces.js\";\nimport {\n\tarrayIntersection,\n} from \"../../general/array.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../make/edgesFaces.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../make/facesEdges.js\";\nimport {\n\tinvertArrayMap,\n\tinvertFlatMap,\n\tinvertFlatToArrayMap,\n} from \"../maps.js\";\n\n// the face-matching algorithm should go like this:\n// prerequisite: vertex map\n// the data structure should do something like\n// - for every face, here is a list of its vertices.\n// - using the vertex-map, find a face with 3 or more matching vertices\n// we can't depend on them being in order, because it's possible that for\n// every vertex in the face, each edge was split inserting new vertices\n// between every pair of old vertices.\n\n// call arrayIntersection, but inside the reduce, where the initial\n// value is []- in which case, don't perform an intersection just\n// return the second array (the current value).\nconst callArrayIntersection = (a, b) => (!a.length\n\t? b\n\t: arrayIntersection(a, b));\n\n/**\n * @param {FOLD} graph the input graph from the very start of the planarize()\n * @param {number[][]} faces_edgesNew the faces_edges from the new graph\n * @param {number[][]} edgesBackmap\n * @returns {number[][]}\n */\nconst makeFaceBackmapOld = (\n\t{ edges_vertices, edges_faces, faces_vertices, faces_edges },\n\tfaces_edgesNew,\n\tedgesBackmap,\n) => {\n\tif (!faces_vertices && !faces_edges) { return []; }\n\tif (!faces_edges) {\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tif (!edges_faces) {\n\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_vertices, faces_edges });\n\t}\n\n\t// for each of the new faces_edges, use the backmap to replace all current\n\t// edge indices with (a list of) the old edge indices. new edges map\n\t// one-to-many to old edges, so this creates nested arrays.\n\t// convert [4, 15, 0] into [[7], [1, 15], [10]]\n\tconst faces_backEdges = faces_edgesNew\n\t\t.map(edges => edges\n\t\t\t.filter(e => edgesBackmap[e] !== undefined)\n\t\t\t.map(e => edgesBackmap[e]));\n\n\t// for every face, a list of its old edges' adjacent faces. these are\n\t// contenders for matching the new face to one of the old faces from this list\n\tconst faces_backEdges_faces = faces_backEdges\n\t\t.map(backEdges => backEdges.map(edges => edges.flatMap(e => edges_faces[e])));\n\n\tconst faces_faceAppearanceCount = faces_backEdges_faces\n\t\t.map(edgesFaces => invertFlatToArrayMap(edgesFaces.flat())\n\t\t\t.map(el => el.length));\n\n\t// get the appearance with the most appearances (last in the list)\n\tconst facesBackMap = faces_faceAppearanceCount\n\t\t.map(indexCounts => invertFlatToArrayMap(indexCounts))\n\t\t.map(faces => faces.pop())\n\t\t.map(res => (res === undefined ? [] : res));\n\n\t// const faces_backFaces = faces_backEdges_faces\n\t// \t.map(backEdges_faces => backEdges_faces\n\t// \t\t.filter(a => a.length)\n\t// \t\t.reduce((a, b) => callArrayIntersection(a, b), []));\n\n\t// console.log(\"faces_edgesNew\", faces_edgesNew);\n\t// console.log(\"faces_backEdges\", faces_backEdges);\n\t// console.log(\"faces_backEdges_faces\", faces_backEdges_faces);\n\t// console.log(\"faces_faceAppearanceCount\", faces_faceAppearanceCount);\n\t// console.log(\"facesBackMap\", facesBackMap);\n\t// console.log(\"faces_backFaces\", faces_backFaces);\n\n\t// return faces_backFaces;\n\treturn facesBackMap;\n};\n\n/**\n * @description This is the final step of a planarize method, rebuild faces\n * and create a map that relates each of the new faces to the old faces\n * from which it came.\n * @param {FOLD} oldGraph the input graph from before any changes,\n * \"edgeBackmap\" relates this graph to the newGraph\n * @param {FOLD} newGraph the planar graph after all changes to it,\n * \"edgeBackmap\" relates this graph to the oldGraph\n * @param {{ edges: { map: number[][] }}} edgeBackmap\n * @returns {{\n *   faces_vertices: number[][],\n *   faces_edges: number[][],\n *   faceMap: number[][],\n * }} new face information for the newGraph, and a map relating the new\n * faces to the old faces.\n */\nexport const planarizeMakeFaces = (oldGraph, newGraph, { edges: { map: edgeNextMap } }) => {\n\t// const vertexBackMap = invertArrayMap(vertexNextMap);\n\tconst edgeBackMap = invertArrayMap(edgeNextMap);\n\n\tconst {\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t} = makePlanarFaces(newGraph);\n\tconst faceBackMap = makeFaceBackmapOld(\n\t\toldGraph,\n\t\tfaces_edges,\n\t\tedgeBackMap,\n\t);\n\treturn {\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaceMap: invertArrayMap(faceBackMap),\n\t};\n};\n\n/**\n * @description Turns out this is not an ideal way of finding faces,\n * a face can be cut so that only one vertex survives, a vertex which\n * is adjacent to more than one face, so the only way to tell which face\n * it ISN'T is by process of elimination, and I'm not sure this will actually\n * generate a definitive mapping. One way which does work is using edges\n * instead of vertices, which was implemented above.\n * @param {FOLD} oldGraph\n * @param {FOLD} newGraph\n * @param {number[][]} vertexBackmap\n * @returns {number[][]}\n */\n// const matchFacesUsingVertices = (oldGraph, newGraph, vertexBackmap) => {\n// \tif (!oldGraph.faces_vertices) { return undefined; }\n// \tconst vertices_faces = makeVerticesFacesUnsorted(oldGraph);\n// \tconsole.log(\"vertices_faces\", vertices_faces);\n// \t// vertexBackmap\n// \tconst res = newGraph.faces_vertices\n// \t\t.map(vertices => vertices.filter(v => vertexBackmap[v] !== undefined))\n// \t\t.map(vertices => vertices\n// \t\t\t.map(newV => vertexBackmap[newV].flatMap(oldV => vertices_faces[oldV])));\n// \tconsole.log(\"RES\", res);\n// \tconst res2 = newGraph.faces_vertices\n// \t\t.map(vertices => vertices.filter(v => vertexBackmap[v] !== undefined))\n// \t\t.map(vertices => vertices\n// \t\t\t.map(newV => vertexBackmap[newV].flatMap(oldV => vertices_faces[oldV]))\n// \t\t\t.reduce((a, b) => arrayIntersection(a, b)));\n// \tconsole.log(\"RES\", res2);\n\n// \t// if (oldGraph.faces_vertices) {}\n// \treturn [];\n// };\n"
  },
  {
    "path": "src/graph/planarize/planarizeOverlaps.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tepsilonEqual,\n} from \"../../math/compare.js\";\nimport {\n\tscale2,\n\tadd2,\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tclusterSortedGeneric,\n} from \"../../general/cluster.js\";\nimport {\n\tremoveDuplicateVertices,\n} from \"../vertices/duplicate.js\";\nimport {\n\tremoveCircularEdges,\n} from \"../edges/circular.js\";\nimport {\n\tinvertArrayToFlatMap,\n\tmergeFlatNextmaps,\n\tmergeNextmaps,\n} from \"../maps.js\";\nimport {\n\tedgeToLine2,\n} from \"../edges/lines.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\nimport {\n\tintersectAllEdges,\n} from \"./intersectAllEdges.js\";\nimport {\n\tremoveDuplicateEdges,\n} from \"../edges/duplicate.js\";\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   result: FOLD,\n *   changes: {\n *     vertices: { map: number[] },\n *     edges: { map: number[][] },\n *   }\n * }}\n */\nexport const planarizeOverlaps = (\n\t{ vertices_coords, vertices_edges, edges_vertices, edges_assignment, edges_foldAngle },\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\n\tconst edgesParams = edges_vertices.map(() => []);\n\n\tconst intersections = intersectAllEdges({\n\t\tvertices_coords, vertices_edges, edges_vertices,\n\t}, epsilon);\n\n\tintersections\n\t\t.filter(({ a }) => !epsilonEqual(a, 0) && !epsilonEqual(a, 1))\n\t\t.forEach(({ i, a }) => edgesParams[i].push(a));\n\tintersections\n\t\t.filter(({ b }) => !epsilonEqual(b, 0) && !epsilonEqual(b, 1))\n\t\t.forEach(({ j, b }) => edgesParams[j].push(b));\n\n\tedgesParams.forEach(points => points.sort((a, b) => a - b));\n\n\tconst edgesPointClusters = edgesParams\n\t\t.map(points => clusterSortedGeneric(points, epsilonEqual))\n\n\t/** @param {number[]} numbers */\n\tconst average = (numbers) => (numbers.length\n\t\t? numbers.reduce((a, b) => a + b, 0) / numbers.length\n\t\t: 0);\n\n\tconst edgesSplitParams = edgesPointClusters\n\t\t.map((clusters, e) => clusters\n\t\t\t.map(cluster => cluster.map(i => edgesParams[e][i]))\n\t\t\t.map(average));\n\n\tlet newEdgeIndex = 0;\n\tconst edgeNextmapPlanarized = edgesSplitParams\n\t\t.map(params => Array.from(Array(params.length + 1)).map(() => newEdgeIndex++));\n\tconst edgeBackmapPlanarized = invertArrayToFlatMap(edgeNextmapPlanarized);\n\n\t// this new edges_vertices array will entirely replace the old one.\n\tlet newVertexIndex = vertices_coords.length;\n\t/** @type {[number, number][]} */\n\tconst edges_verticesNew = edgesSplitParams\n\t\t.map(params => params.map(() => newVertexIndex++))\n\t\t.map((verts, e) => [\n\t\t\tedges_vertices[e][0],\n\t\t\t...verts,\n\t\t\tedges_vertices[e][1],\n\t\t])\n\t\t.flatMap(vertices => Array.from(Array(vertices.length - 1))\n\t\t\t.map((_, i) => [vertices[i], vertices[i + 1]]))\n\t\t.map(([a, b]) => [a, b]);\n\n\t// this vertices_coords array is meant to be appended to the existing\n\t// vertices_coords array, this only contains the new vertices.\n\tconst additionalVertices_coords = edgesSplitParams.flatMap((params, edge) => {\n\t\tif (!params.length) { return []; }\n\t\tconst line = edgeToLine2({ vertices_coords, edges_vertices }, edge);\n\t\treturn params.map(t => add2(line.origin, scale2(line.vector, t)));\n\t});\n\n\tconst vertices_coordsNew = vertices_coords\n\t\t.concat(additionalVertices_coords)\n\t\t.map(resize2);\n\n\tconst result = {\n\t\tvertices_coords: vertices_coordsNew,\n\t\tedges_vertices: edges_verticesNew,\n\t};\n\n\tif (edges_assignment) {\n\t\tresult.edges_assignment = edgeBackmapPlanarized\n\t\t\t.map(e => edges_assignment[e]);\n\t}\n\tif (edges_foldAngle) {\n\t\tresult.edges_foldAngle = edgeBackmapPlanarized\n\t\t\t.map(e => edges_foldAngle[e]);\n\t}\n\n\t// this does not include the new vertices, which should have a\n\t// value of \"undefined\" anyway as they did not exist prior.\n\t/** @type {number[]} */\n\tconst startNextmap = vertices_coords.map((_, i) => i);\n\n\tconst { map: verticesMapDuplicate } = removeDuplicateVertices(result, epsilon);\n\t// circular edges will be created at points where for example many lines cross\n\t// at a point (but not exactly), creating little small edges with lengths\n\t// around an epsilon, and by calling remove duplicate vertices these edges'\n\t// two vertices become the same vertex, creating circular edges.\n\tconst { map: edgeMapCircular } = removeCircularEdges(result);\n\t// const { map: edgeMapDuplicate } = removeDuplicateEdges(result);\n\n\tconst edgesMap = mergeNextmaps(edgeNextmapPlanarized, edgeMapCircular);\n\t// const edgesMap = mergeNextmaps(edgeNextmapPlanarized, edgeMapCircular, edgeMapDuplicate);\n\tconst verticesMap = mergeFlatNextmaps(startNextmap, verticesMapDuplicate);\n\n\treturn {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices: { map: verticesMap },\n\t\t\tedges: { map: edgesMap },\n\t\t}\n\t};\n}\n"
  },
  {
    "path": "src/graph/planarize.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tmagSquared2,\n\tscale2,\n\tdot2,\n\tadd2,\n\tsubtract2,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tintersectLineLine,\n} from \"../math/intersect.js\";\nimport {\n\tepsilonUniqueSortedNumbers,\n\tsetDifferenceSortedEpsilonNumbers,\n} from \"../general/array.js\";\nimport {\n\tsweepValues,\n} from \"./sweep.js\";\nimport {\n\tinvertFlatToArrayMap,\n\tinvertFlatMap,\n} from \"./maps.js\";\nimport {\n\tremove,\n} from \"./remove.js\";\nimport {\n\tedgeIsolatedVertices,\n\tremoveIsolatedVertices,\n} from \"./vertices/isolated.js\";\nimport {\n\tisVertexCollinear,\n\t// removeCollinearVertex, // this method could move here someday\n} from \"./vertices/collinear.js\";\nimport {\n\tremoveDuplicateVertices,\n} from \"./vertices/duplicate.js\";\nimport {\n\tgetEdgesLine,\n} from \"./edges/lines.js\";\nimport {\n\tduplicateEdges,\n\tremoveDuplicateEdges,\n} from \"./edges/duplicate.js\";\nimport {\n\tcircularEdges,\n\tremoveCircularEdges,\n} from \"./edges/circular.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"./make/verticesEdges.js\";\n\n/**\n * @param {VecLine[]} lines\n * @param {number} [epsilon=1e-6]\n */\nconst getLinesIntersections = (lines, epsilon = EPSILON) => {\n\tconst lines2D = lines.map(({ vector, origin }) => ({\n\t\tvector: resize2(vector),\n\t\torigin: resize2(origin),\n\t}));\n\tconst linesIntersect = lines2D.map(() => []);\n\tfor (let i = 0; i < lines2D.length - 1; i += 1) {\n\t\tfor (let j = i + 1; j < lines2D.length; j += 1) {\n\t\t\tconst { a, b, point } = intersectLineLine(\n\t\t\t\tlines2D[i],\n\t\t\t\tlines2D[j],\n\t\t\t\tincludeS,\n\t\t\t\tincludeS,\n\t\t\t\tepsilon,\n\t\t\t);\n\t\t\t// lines are parallel\n\t\t\tif (point === undefined) { continue; }\n\t\t\tlinesIntersect[i].push(a);\n\t\t\tlinesIntersect[j].push(b);\n\t\t}\n\t}\n\treturn linesIntersect;\n};\n\n/**\n * @description NOTICE this method is used internally and not yet ready for\n * general use. It only works on graphs with no faces and requires cleanup later.\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex\n * @returns {number}\n */\nconst removeCollinearVertex = ({ edges_vertices, vertices_edges }, vertex) => {\n\t// edges[0] will remain. edges[1] will be removed\n\tconst edges = vertices_edges[vertex].sort((a, b) => a - b);\n\tconst otherVertices = edges\n\t\t.flatMap(e => edges_vertices[e])\n\t\t.filter(v => v !== vertex);\n\t/** @type {[number, number]} */\n\tconst newEdgeVertices = [otherVertices[0], otherVertices[1]];\n\t// eslint-disable-next-line no-param-reassign\n\tedges_vertices[edges[0]] = newEdgeVertices;\n\t// eslint-disable-next-line no-param-reassign\n\tedges_vertices[edges[1]] = undefined;\n\tnewEdgeVertices.forEach(v => {\n\t\tconst oldEdgeIndex = vertices_edges[v].indexOf(edges[1]);\n\t\tif (oldEdgeIndex === -1) { return; }\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges[v][oldEdgeIndex] = edges[0];\n\t});\n\treturn edges[1];\n};\n\n/**\n * @description Planarize a graph into the 2D XY plane, split edges, rebuild faces.\n * The graph provided as a method argument will be modified in place.\n * @algorithm\n * - create an axis-aligned bounding box of all the vertices.\n * - create unique lines that represent all edges, with a mapping of\n * edges to lines and visa-versa (one line to many edges. one edge to one line).\n * - intersect all lines against each other, reject those which lie outside\n * of the bounding box enclosing the entire graph.\n * - for each line, gather all edges, project each endpoint down to the line,\n * each edge is now two numbers (sort these).\n * - add the set of intersection points to this set, for each line.\n * - also, sort the larger array of values by their start points.\n * - walk down the line and group edges into connected edge groups.\n * groups join connected edges and separate them from the empty spaces between.\n * as we walk, if an intersection point is in the empty space, ignore it.\n * - build each group into a connected set of segments (optional challenge:\n * do this by re-using the vertices in place).\n * do this for every line.\n * - somehow we need to apply the edge-assignment/fold angle/possibly\n * other attributes.\n * - if lines overlap, competing assignments will need to be resolved:\n * M/V above all, then perhaps Cut/Join, then unassigned, then boundary/flat.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FOLD} a planarized FOLD object\n */\nexport const planarize = ({\n\tvertices_coords,\n\tedges_vertices,\n\tedges_assignment,\n\tedges_foldAngle,\n}, epsilon = EPSILON) => {\n\tconst {\n\t\tlines,\n\t\tedges_line,\n\t} = getEdgesLine({ vertices_coords, edges_vertices }, epsilon);\n\n\t// \"compress\" all edges down into a smaller set of infinite lines,\n\t// we will be projecting points down onto non-normalized vectors,\n\t// the dot products will be scaled by this much, we need to divide\n\t// by this to convert the parameters back into coordinate space.\n\tconst linesSquareLength = lines.map(({ vector }) => magSquared2(vector));\n\n\t// one to many mapping of a line and the edges along it.\n\tconst lines_edges = invertFlatToArrayMap(edges_line);\n\n\t// for each edge and its corresponding line, project the edge's endpoints\n\t// onto the line as a scalar of the line's vector from its origin.\n\t// /** @type {[number, number][]} */\n\tconst edges_scalars = edges_vertices\n\t\t.map((verts, e) => verts\n\t\t\t.map(v => vertices_coords[v])\n\t\t\t.map(point => dot2(\n\t\t\t\tsubtract2(point, lines[edges_line[e]].origin),\n\t\t\t\tlines[edges_line[e]].vector,\n\t\t\t)));\n\n\t// for each line, a flat sorted list of all scalars along that line\n\t// coming from all of the edges' endpoints.\n\tconst lines_flatEdgeScalars = lines_edges\n\t\t.map(edges => edges.flatMap(edge => edges_scalars[edge]))\n\t\t.map(numbers => epsilonUniqueSortedNumbers(numbers, epsilon));\n\n\t// for each line, get the smallest and largest value (defining the range)\n\t// compare every line against every other, gather all intersections.\n\t// \"intersections\" contains some intersections that are outside the\n\t// relevant areas. more filtering will happen when we start to apply them.\n\t// for every line, an array of sorted scalars of the line's vector\n\t// at the location of an intersection with another line.\n\tconst lines_intersections = getLinesIntersections(lines, epsilon)\n\t\t.map(numbers => epsilonUniqueSortedNumbers(numbers, epsilon))\n\t\t.map((numbers, i) => numbers.map(n => n * linesSquareLength[i]))\n\t\t// for every line, the subset of all intersections along the line\n\t\t// that are not duplicates of endpoints of collinear edges to the line.\n\t\t.map((sects, i) => (\n\t\t\tsetDifferenceSortedEpsilonNumbers(sects, lines_flatEdgeScalars[i], epsilon)\n\t\t));\n\n\t// walk the line\n\t// create an alternative form of the graph for the sweep method.\n\tconst sweepScalars = lines_edges\n\t\t.map(edges => edges.flatMap(edge => edges_scalars[edge]));\n\t/** @type {[number, number][][]} */\n\tconst sweepEdgesVertices = lines_edges\n\t\t.map(edges => invertFlatMap(edges)\n\t\t\t.map(e => [e * 2, e * 2 + 1]));\n\tconst lineSweeps = lines_edges.map((_, i) => sweepValues(\n\t\t{ edges_vertices: sweepEdgesVertices[i] },\n\t\tsweepScalars[i],\n\t\tepsilon,\n\t));\n\tconst lineSweeps_vertices = lineSweeps.map(sweep => sweep.map(el => el.t));\n\tconst lineSweeps_edges = lineSweeps.map(sweep => {\n\t\tconst current = {};\n\t\tconst edges = sweep.map(el => {\n\t\t\tel.start.forEach(n => { current[n] = true; });\n\t\t\tel.end.forEach(n => { delete current[n]; });\n\t\t\treturn Object.keys(current).map(n => parseInt(n, 10));\n\t\t});\n\t\tedges.pop();\n\t\treturn edges;\n\t});\n\tlines_intersections.forEach((points, i) => {\n\t\tconst vertices = lineSweeps_vertices[i];\n\t\tconst edges = lineSweeps_edges[i];\n\t\t// insert points into vertices (and make corresponding duplicate in edges)\n\t\tlet pi = 0;\n\t\tlet vi = 0;\n\t\twhile (pi < points.length && vi < vertices.length - 1) {\n\t\t\tif (points[pi] <= vertices[vi]) { throw new Error(\"bad algorithm\"); }\n\t\t\tif (points[pi] > vertices[vi + 1]) { vi += 1; continue; }\n\t\t\tvertices.splice(vi + 1, 0, points[pi]);\n\t\t\tedges.splice(vi + 1, 0, edges[vi]);\n\t\t\tpi += 1;\n\t\t}\n\t});\n\n\t// walk lineSweeps_vertices, lineSweeps_edges, remove edges which span\n\t// across the empty space in a line where no previous edges existed.\n\tconst new_vertices_coords = lineSweeps_vertices\n\t\t.flatMap((scalars, i) => scalars\n\t\t\t.map(s => s / linesSquareLength[i])\n\t\t\t.map(s => add2(lines[i].origin, scale2(lines[i].vector, s))));\n\n\t// create a connected list of vertices, for every line, until the new line.\n\t// [0,1] [1,2], [2,3] [3,4] then a new line [5, 6]... (don't connect 4-5)\n\t// console.log(\"new_vertices_coords\", new_vertices_coords);\n\t// console.log(\"new_vertices_coords2\", new_vertices_coords2);\n\tlet e = 0;\n\tconst new_edges_vertices = lineSweeps_edges\n\t\t.map(edges => {\n\t\t\tconst vertices = edges.map(() => [e, ++e]);\n\t\t\te += 1;\n\t\t\treturn vertices;\n\t\t})\n\t\t.flatMap((edges, i) => edges\n\t\t\t.filter((_, j) => lineSweeps_edges[i][j].length))\n\t\t.map(resize2);\n\tconst result = {\n\t\tvertices_coords: new_vertices_coords,\n\t\tedges_vertices: new_edges_vertices,\n\t};\n\tif (edges_assignment || edges_foldAngle) {\n\t\tconst edges_prevEdge = lineSweeps_edges\n\t\t\t.flatMap(edges => edges.filter(arr => arr.length));\n\t\tif (edges_assignment) {\n\t\t\tresult.edges_assignment = edges_prevEdge\n\t\t\t\t.map(prev => edges_assignment[prev[0]]);\n\t\t}\n\t\tif (edges_foldAngle) {\n\t\t\tresult.edges_foldAngle = edges_prevEdge\n\t\t\t\t.map(prev => edges_foldAngle[prev[0]]);\n\t\t}\n\t}\n\tremoveIsolatedVertices(result, edgeIsolatedVertices(result));\n\n\t// this single method call takes up the majority of the time of this method.\n\tremoveDuplicateVertices(result, epsilon);\n\tremoveCircularEdges(result);\n\n\t// remove collinear vertices\n\t// these vertices_edges are unsorted and will be removed at the end.\n\tresult.vertices_edges = makeVerticesEdgesUnsorted(result);\n\tconst collinearVertices = result.vertices_edges\n\t\t.map((edges, i) => (edges.length === 2 ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.filter(v => isVertexCollinear(result, v, epsilon))\n\t\t.reverse();\n\tconst edgesToRemove = collinearVertices\n\t\t.map(v => removeCollinearVertex(result, v));\n\tremove(result, \"edges\", edgesToRemove);\n\tremove(result, \"vertices\", collinearVertices);\n\tconst dupEdges = duplicateEdges(result);\n\tif (dupEdges.length) {\n\t\tremoveDuplicateEdges(result, dupEdges);\n\t}\n\tif (circularEdges(result).length) {\n\t\tconsole.error(\"planarize: found circular edges\");\n\t}\n\tdelete result.vertices_edges;\n\treturn result;\n};\n"
  },
  {
    "path": "src/graph/pleat.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tincludeL,\n\tincludeS,\n} from \"../math/compare.js\";\nimport {\n\tpleat as Pleat,\n} from \"../math/line.js\";\nimport {\n\tintersectLineLine,\n} from \"../math/intersect.js\";\nimport {\n\tadd2,\n\tscale2,\n} from \"../math/vector.js\";\nimport {\n\tedgeToLine2,\n\tedgesToLines2,\n} from \"./edges/lines.js\";\n\n/**\n * @description Create a series of pleat lines as segments, using two\n * lines as inputs. This is akin to origami axiom 3, but\n * that the result is not one bisector, but a fan of sectors.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine2} lineA one of the two input lines\n * @param {VecLine2} lineB one of the two input lines\n * @param {number} count the number of pleats\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {[[number, number], [number, number]][][]} an\n * array of arrays of segments, where each segment is an array\n * of points, each point is an array of numbers.\n * The outer array always contains two inner arrays where the pleat lines\n * are sorted into two halves, both two angles between the lines.\n */\nexport const pleat = (\n\t{ vertices_coords, edges_vertices },\n\tlineA,\n\tlineB,\n\tcount,\n\tepsilon = EPSILON,\n) => {\n\tconst edges_lines = edgesToLines2({ vertices_coords, edges_vertices });\n\n\t// the pleat() method returns two lists of lines (or one if parallel)\n\t// convert these two lists of lines into two lists of segments.\n\treturn Pleat(lineA, lineB, count, epsilon)\n\t\t.map(lines => lines.map(line => {\n\t\t\t// intersect these lines with every edge in the graph,\n\t\t\t// gather a list of the line's intersection parameter.\n\t\t\t// these parameters can be converted back into points by\n\t\t\t// scaling the line's vector by the parameter amount (and add to origin).\n\t\t\tconst dots = edges_lines\n\t\t\t\t.map(edgeLine => intersectLineLine(\n\t\t\t\t\tline,\n\t\t\t\t\tedgeLine,\n\t\t\t\t\tincludeL,\n\t\t\t\t\tincludeS,\n\t\t\t\t\tepsilon,\n\t\t\t\t).a)\n\t\t\t\t.filter(a => a !== undefined);\n\t\t\tif (dots.length < 2) { return undefined; }\n\n\t\t\t// if the max and min parameter are not epsilon equal,\n\t\t\t// we can create a valid segment.\n\t\t\tconst min = Math.min(...dots);\n\t\t\tconst max = Math.max(...dots);\n\t\t\t/** @type {[[number, number], [number, number]]} */\n\t\t\tconst segment = Math.abs(max - min) < epsilon\n\t\t\t\t? undefined\n\t\t\t\t: [\n\t\t\t\t\tadd2(line.origin, scale2(line.vector, min)),\n\t\t\t\t\tadd2(line.origin, scale2(line.vector, max)),\n\t\t\t\t];\n\t\t\treturn segment;\n\t\t}).filter(a => a !== undefined));\n};\n\n/**\n * @description Create a series of pleat lines as segments, using two\n * of a graph's edges as inputs. This is akin to origami axiom 3, but\n * that the result is not one bisector, but a fan of sectors.\n * @param {FOLD} graph a FOLD object\n * @param {number} edgeA one of two input edges\n * @param {number} edgeB one of two input edges\n * @param {number} count the number of pleats\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][][][]} an array of arrays of segments.\n * The outer array always contains two inner arrays.\n * And each segment is an array of points, each point an array of numbers.\n */\nexport const pleatEdges = (\n\t{ vertices_coords, edges_vertices },\n\tedgeA,\n\tedgeB,\n\tcount,\n\tepsilon = EPSILON,\n) => {\n\tconst lineA = edgeToLine2({ vertices_coords, edges_vertices }, edgeA);\n\tconst lineB = edgeToLine2({ vertices_coords, edges_vertices }, edgeB);\n\treturn pleat({ vertices_coords, edges_vertices }, lineA, lineB, count, epsilon);\n};\n"
  },
  {
    "path": "src/graph/populate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeVerticesVertices,\n} from \"./make/verticesVertices.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n\tmakeVerticesEdges,\n} from \"./make/verticesEdges.js\";\nimport {\n\tmakeVerticesFaces,\n} from \"./make/verticesFaces.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"./make/edgesFaces.js\";\nimport {\n\tmakeFacesFaces,\n} from \"./make/facesFaces.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"./make/facesEdges.js\";\nimport {\n\tmakeFacesVerticesFromEdges,\n} from \"./make/facesVertices.js\";\nimport {\n\tmakePlanarFaces,\n} from \"./make/faces.js\";\nimport {\n\tedgeAssignmentToFoldAngle,\n\tedgeFoldAngleToAssignment,\n} from \"../fold/spec.js\";\n\n/**\n * @description Ensure that both edges_assignment and edges_foldAngle both\n * exist, if in the case only one exists, build the other.\n * If neither exist, fill them both with \"unassigned\" and fold angle 0.\n * @param {FOLD} graph a FOLD object, modified in place\n */\nconst buildAssignmentsIfNeeded = (graph) => {\n\tif (!graph.edges_vertices) { return; }\n\n\tif (!graph.edges_assignment) { graph.edges_assignment = []; }\n\tif (!graph.edges_foldAngle) { graph.edges_foldAngle = []; }\n\n\t// ensure that both arrays have the same length. This would be a strange\n\t// instance if they were not equal, but in the case that they are not, we\n\t// don't want to overwrite any data, even if it partially exists.\n\tif (graph.edges_assignment.length > graph.edges_foldAngle.length) {\n\t\tfor (let i = graph.edges_foldAngle.length; i < graph.edges_assignment.length; i += 1) {\n\t\t\tgraph.edges_foldAngle[i] = edgeAssignmentToFoldAngle(graph.edges_assignment[i]);\n\t\t}\n\t}\n\tif (graph.edges_foldAngle.length > graph.edges_assignment.length) {\n\t\tfor (let i = graph.edges_assignment.length; i < graph.edges_foldAngle.length; i += 1) {\n\t\t\tgraph.edges_assignment[i] = edgeFoldAngleToAssignment(graph.edges_foldAngle[i]);\n\t\t}\n\t}\n\n\t// the two arrays are now the same length. Now we need to make sure they\n\t// are the same length as edges_vertices. If not, fill any remaining\n\t// assignments with \"unassigned\" and fold angle 0.\n\tfor (let i = graph.edges_assignment.length; i < graph.edges_vertices.length; i += 1) {\n\t\tgraph.edges_assignment[i] = \"U\";\n\t\tgraph.edges_foldAngle[i] = 0;\n\t}\n};\n\n/**\n * @description Rebuild both faces_vertices and faces_edges by walking\n * the planar faces in 2D, set them both onto the graph.\n * @param {FOLD} graph a FOLD object, modified in place\n */\nconst rebuildFaces = (graph) => {\n\tconst { faces_vertices, faces_edges } = makePlanarFaces(graph);\n\tgraph.faces_vertices = faces_vertices;\n\tgraph.faces_edges = faces_edges;\n};\n\n/**\n * @description Ensure that faces_vertices and faces_edges exist, and\n * if possible ensure that both are filled with face data.\n * \"reface\" will only cause a rebuild of faces if currently\n * faces do not exist. If the user wants to force a rebuild of faces, they\n * should call that method directly.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {boolean} reface should be set to \"true\" to force the algorithm into\n * rebuilding the faces from scratch (walking edge to edge in the plane).\n * @returns {object} information about which face components were rebuilt.\n */\nconst buildFacesIfNeeded = (graph, reface) => {\n\t// in the main method, we need to react accordingly, whether or not certain\n\t// component arrays were rebuilt (causing others to require be rebuild)\n\tconst didRebuild = {\n\t\tfaces_vertices: false,\n\t\tfaces_edges: false,\n\t};\n\n\t// check if a face exists, either array (and the array is not empty)\n\tconst facesExist = (graph.faces_vertices && graph.faces_vertices.length)\n\t\t|| (graph.faces_edges && graph.faces_edges.length);\n\n\t// if the user requests to rebuild the faces, only if no faces exist, do it\n\tif (!facesExist && reface && graph.vertices_coords) {\n\t\trebuildFaces(graph);\n\t\tdidRebuild.faces_vertices = true;\n\t\tdidRebuild.faces_edges = true;\n\t\treturn didRebuild;\n\t}\n\n\t// if neither exist, check if the user has requested to rebuild the faces\n\tif (!graph.faces_vertices && !graph.faces_edges) {\n\t\t// the user has not requested we rebuild faces, and we can't assume\n\t\t// that the graph is a crease pattern, it could be a 2D flat folded model\n\t\t// with overlapping faces, so, we cannot assume. make empty face arrays.\n\t\tgraph.faces_vertices = [];\n\t\tgraph.faces_edges = [];\n\t} else if (graph.faces_vertices && !graph.faces_edges) {\n\t\tgraph.faces_edges = makeFacesEdgesFromVertices(graph);\n\t\tdidRebuild.faces_edges = true;\n\t} else if (graph.faces_edges && !graph.faces_vertices) {\n\t\tgraph.faces_vertices = makeFacesVerticesFromEdges(graph);\n\t\tdidRebuild.faces_vertices = true;\n\t}\n\treturn didRebuild;\n};\n\n/**\n * @description Take a FOLD graph, containing any number of component fields,\n * and build and set as many missing component arrays as possible.\n * This method is not destructive, rather, it simply builds component arrays\n * if they do not already exist and if it's possible to be built without\n * making any assumptions. If your graph contains errors, this method will not\n * find them and will not correct them.\n * Regarding faces, this method is capable of walking and building faces\n * from scratch for FOLD objects which are creasePattern, not foldedForm,\n * but, the user needs to explicitly request this.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {object} options object, with the ability to request that the\n * faces be rebuilt by walking 2D planar faces, specify { \"faces\": true }.\n * @return {FOLD} graph the same input graph object\n */\nexport const populate = (graph, options = {}) => {\n\tif (typeof graph !== \"object\") { return graph; }\n\tif (!graph.edges_vertices) { return graph; }\n\n\t// later in the method we need to react accordingly, whether or not certain\n\t// component arrays were rebuilt (causing others to require be rebuild)\n\tconst didRebuild = {\n\t\tvertices_vertices: false,\n\t\tfaces_vertices: false,\n\t\tfaces_edges: false,\n\t};\n\n\t// if vertices_vertices exists, rely on its winding order to determine\n\t// vertices_edges and vertices_faces.\n\t// otherwise, if vertices_vertices and/or vertices_edges are missing,\n\t// vertices_edges and vertices_vertices are mutually dependent, so,\n\t// we need to rebuild one even if it exists if the other does not.\n\tif (graph.vertices_vertices && !graph.vertices_edges) {\n\t\tgraph.vertices_edges = makeVerticesEdges(graph);\n\t} else if (!graph.vertices_edges || !graph.vertices_vertices) {\n\t\tgraph.vertices_edges = makeVerticesEdgesUnsorted(graph);\n\t\tgraph.vertices_vertices = makeVerticesVertices(graph);\n\t\tgraph.vertices_edges = makeVerticesEdges(graph);\n\t\tdidRebuild.vertices_vertices = true;\n\t}\n\n\t// make sure \"edges_foldAngle\" and \"edges_assignment\" exist\n\tbuildAssignmentsIfNeeded(graph);\n\n\t// make sure \"faces_vertices\" and \"faces_edges\" exist. this also has the\n\t// option of rebuilding planar faces in 2D, but only if the user has\n\t// explicitly requested it using the options, as we can't assume that even\n\t// a 2D graph has non-overlapping faces and will result in a valid re-facing.\n\tconst reface = typeof options === \"object\" ? options.faces : false;\n\tObject.assign(didRebuild, buildFacesIfNeeded(graph, reface));\n\n\t// makeVerticesFaces dependencies are vertices_vertices and faces_vertices\n\t// rebuild if a depenency was rebuilt as well\n\tif (!graph.vertices_faces\n\t\t|| didRebuild.vertices_vertices\n\t\t|| didRebuild.faces_vertices) {\n\t\tgraph.vertices_faces = makeVerticesFaces(graph);\n\t}\n\n\t// makeEdgesFacesUnsorted's dependencies are edges_vertices and faces_edges\n\t// rebuild if a depenency was rebuilt as well\n\tif (!graph.edges_faces || didRebuild.faces_edges) {\n\t\tgraph.edges_faces = makeEdgesFacesUnsorted(graph);\n\t}\n\n\t// makeFacesFaces's dependency is faces_vertices\n\t// rebuild if a depenency was rebuilt as well\n\tif (!graph.faces_faces || didRebuild.faces_vertices) {\n\t\tgraph.faces_faces = makeFacesFaces(graph);\n\t}\n\treturn graph;\n};\n"
  },
  {
    "path": "src/graph/raycast.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @todo\n * @description Cast a ray and intersect a FOLD mesh, return\n * the intersected face, if exists, and the nearest edge\n * and vertex to the intersection.\n * @param {FOLD} graph a FOLD object\n * @param {VecLine} ray a ray defined by a vector and origin\n */\nexport const raycast = (graph, ray) => {};\n"
  },
  {
    "path": "src/graph/remove.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tcount,\n} from \"./count.js\";\nimport {\n\tuniqueSortedNumbers,\n} from \"../general/array.js\";\nimport {\n\tremapKey,\n} from \"./maps.js\";\n\n/**\n * @param {FOLD} graph\n * @param {string} key\n * @param {number[]} removeIndices\n * @returns {number[]}\n */\nconst makeIndexMap = (graph, key, removeIndices) => {\n\tconst sortedIndices = uniqueSortedNumbers(removeIndices);\n\tconst arrayLength = count(graph, key);\n\tconst indexMap = [];\n\tfor (let i = 0, j = 0, walk = 0; i < arrayLength; i += 1, j += 1) {\n\t\twhile (i === sortedIndices[walk]) {\n\t\t\t// this prevents arrays with holes\n\t\t\tindexMap[i] = undefined;\n\t\t\ti += 1;\n\t\t\twalk += 1;\n\t\t}\n\t\tif (i < arrayLength) { indexMap[i] = j; }\n\t}\n\treturn indexMap;\n};\n\n/**\n * @description Removes vertices, edges, or faces (or anything really)\n * remove elements from inside arrays, shift up remaining components,\n * and updates all relevant references across other arrays due to shifting.\n * For outer arrays, this will remove the entire element, for inner arrays,\n * this will replace an occurrence with \"undefined\".\n * @param {FOLD} graph a FOLD object\n * @param {string} key like \"vertices\", the prefix of the arrays\n * @param {number[]} removeIndices an array of vertex indices, like [1,9,25]\n * @returns {number[]} a map of changes to the graph (a nextmap).\n * @example remove(foldObject, \"vertices\", [2,6,11,15]);\n * @example\n * removing index 5 from a 10-long vertices list will shift all\n * indices > 5 up by one, and then will look through all other arrays like\n * edges_vertices, faces_vertices and update any reference to indices 6-9\n * to match their new positions 5-8.\n *\n * this can handle removing multiple indices at once; and is faster than\n * otherwise calling this multiple times with only one or a few removals.\n * @example\n * given removeIndices: [4, 6, 7]\n * given a geometry array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n * map becomes (where _ is undefined): [0, 1, 2, 3, _, 4, _, _, 5, 6]\n */\nexport const remove = (graph, key, removeIndices) => {\n\tconst indexMap = makeIndexMap(graph, key, removeIndices);\n\tremapKey(graph, key, indexMap);\n\treturn indexMap;\n};\n"
  },
  {
    "path": "src/graph/rendering.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tscale3,\n\tadd3,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tinvertMatrix4,\n\tmultiplyMatrix4Vector3,\n} from \"../math/matrix4.js\";\nimport {\n\tclone,\n} from \"../general/clone.js\";\nimport {\n\tfaceOrdersSubset,\n\tnudgeFacesWithFaceOrders,\n} from \"./orders.js\";\nimport {\n\tcountEdges,\n\tcountImpliedEdges,\n} from \"./count.js\";\nimport {\n\tinvertArrayToFlatMap,\n} from \"./maps.js\";\nimport {\n\ttriangulate,\n} from \"./triangulate.js\";\nimport {\n\texplodeFaces,\n} from \"./explode.js\";\nimport {\n\tfixCycles,\n} from \"./cycles.js\";\nimport {\n\tgetFacesPlane,\n} from \"./faces/planes.js\";\nimport {\n\tsubgraphWithFaces,\n} from \"./subgraph.js\";\nimport {\n\tjoin,\n} from \"./join.js\";\n\nconst LAYER_NUDGE = 5e-6;\n\n/**\n * @description This is a subroutine of prepareForRendering, if the layer order\n * of faces in a graph is found to have cycles, this method is called instead.\n * This explodes a graph so that faces which otherwise share\n * vertices are separated (creating more vertices). This method will also\n * nudge coplanar faces away from each other, based on their layer ordering\n * (via. faceOrders or faces_layer) by a tiny amount in the cross axis to\n * prevent z-fighting between coplanar faces.\n * @param {FOLDExtended} inputGraph a FOLD object\n * @param {{ earcut?: Function, layerNudge?: number }} options a small amount\n * to nudge the faces in the cross axis to prevent Z-fighting.\n * @returns {FOLD} a copy of the input FOLD graph, with exploded faces\n */\nexport const prepareForRenderingWithCycles = (inputGraph, { earcut, layerNudge } = {}) => {\n\tconst graph = clone(inputGraph);\n\tconst {\n\t\t// planes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\t// faces_plane,\n\t\t// faces_winding,\n\t} = getFacesPlane(graph);\n\tif (!graph.faceOrders) {\n\t\treturn triangulate(graph, earcut).result;\n\t}\n\tconst planes_inverseTransform = planes_transform.map(invertMatrix4);\n\n\tconst planes_faceOrders = planes_faces\n\t\t.map(faces => faceOrdersSubset(graph.faceOrders, faces));\n\n\t// ensure the vertices are in 3D before creating a bunch of subgraphs\n\tconst graph3 = {\n\t\t...graph,\n\t\tvertices_coords: graph.vertices_coords.map(resize3),\n\t};\n\n\tconst planes_graphs = planes_faces\n\t\t.map(faces => subgraphWithFaces(graph3, faces));\n\n\tconst planes_graphXY = planes_graphs\n\t\t.map((g, p) => ({\n\t\t\t...g,\n\t\t\tvertices_coords: g.vertices_coords\n\t\t\t\t.map(coord => multiplyMatrix4Vector3(planes_transform[p], coord)),\n\t\t}));\n\n\t// this resizes the length of the coordinates back to 2.\n\tconst planes_graphXYFixed = planes_graphXY\n\t\t.map((g, p) => fixCycles({\n\t\t\t...g,\n\t\t\tfaceOrders: planes_faceOrders[p],\n\t\t}));\n\n\tconst planes_graphFixed = planes_graphXYFixed\n\t\t.map((graphXY, p) => ({\n\t\t\t...graphXY,\n\t\t\tvertices_coords: graphXY.vertices_coords\n\t\t\t\t.map(resize3)\n\t\t\t\t.map(coord => multiplyMatrix4Vector3(planes_inverseTransform[p], coord)),\n\t\t}));\n\n\tconst planes_facesNudge = planes_graphFixed\n\t\t.map(graphXY => nudgeFacesWithFaceOrders(graphXY));\n\n\t// triangulate will modify faces and edges.\n\t// this will store the changes to the graph from the triangulation\n\tconst planes_triangulatedVerbose = planes_graphFixed\n\t\t.map(g => triangulate(g, earcut));\n\n\tconst planes_graphExploded = planes_triangulatedVerbose\n\t\t.map(({ result }) => result)\n\t\t.map(explodeFaces);\n\n\tplanes_triangulatedVerbose.forEach(({ changes }, p) => {\n\t\tconst backmap = invertArrayToFlatMap(changes.faces.map);\n\t\tconst verticesOffset = planes_graphExploded[p].vertices_coords\n\t\t\t.map(() => undefined);\n\t\tbackmap.forEach((oldFace, face) => {\n\t\t\tconst nudge = planes_facesNudge[p][oldFace];\n\t\t\tif (!nudge) { return; }\n\t\t\tplanes_graphExploded[p].faces_vertices[face].forEach(v => {\n\t\t\t\tverticesOffset[v] = scale3(nudge.vector, nudge.layer * layerNudge);\n\t\t\t});\n\t\t});\n\t\tverticesOffset.forEach((offset, v) => {\n\t\t\tif (!offset) { return; }\n\t\t\tplanes_graphExploded[p].vertices_coords[v] = add3(\n\t\t\t\tresize3(planes_graphExploded[p].vertices_coords[v]),\n\t\t\t\toffset,\n\t\t\t);\n\t\t});\n\t});\n\n\t// join all graphs into one\n\tif (planes_graphExploded.length > 1) {\n\t\tplanes_graphExploded.forEach((exploded, i) => {\n\t\t\tif (i === 0) { return; }\n\t\t\tjoin(planes_graphExploded[0], exploded);\n\t\t});\n\t}\n\n\treturn planes_graphExploded[0];\n};\n\n/**\n * @description This explodes a graph so that faces which otherwise share\n * vertices are separated (creating more vertices). This method will also\n * nudge coplanar faces away from each other, based on their layer ordering\n * (via. faceOrders or faces_layer) by a tiny amount in the cross axis to\n * prevent z-fighting between coplanar faces.\n * @param {FOLDExtended} inputGraph a FOLD object\n * @param {{ earcut?: Function, layerNudge?: number }} options a small amount to nudge\n * the faces in the cross axis to prevent Z-fighting\n * @returns {FOLD} a copy of the input FOLD graph, with exploded faces\n */\nexport const prepareForRendering = (inputGraph, { earcut, layerNudge = LAYER_NUDGE } = {}) => {\n\t// todo: remove the structured clone as long as everything is working.\n\t// update: shallow copy is not working. the input parameter is still modified.\n\tconst graph = clone(inputGraph);\n\t// const copy = { ...graph };\n\n\t// we render \"J\" joined edges differently from all others. if edges_assignment\n\t// doesn't exist, make it with all assignments set to \"U\".\n\t// the user will never see this data, it's just for visualization.\n\tif (!graph.edges_assignment) {\n\t\tconst edgeCount = countEdges(graph) || countImpliedEdges(graph);\n\t\tif (edgeCount) {\n\t\t\tgraph.edges_assignment = Array(edgeCount).fill(\"U\");\n\t\t}\n\t}\n\n\t// if no faceOrders exist, all we need to do is triangulate the graph\n\t// and return the modified copy.\n\tif (!graph.faceOrders) {\n\t\treturn explodeFaces(triangulate(graph, earcut).result);\n\t}\n\n\t// figure out as soon as possible whether or not this graph contains cycles\n\t// if so, we need to heavily modify the graph: isolated the coplanar sets\n\t// of faces, find any with cycles and planarize them (creating a vastly\n\t// different graph), build new face orders, and reassemble the graph\n\tconst faces_nudge = nudgeFacesWithFaceOrders(graph);\n\tif (!faces_nudge) {\n\t\treturn prepareForRenderingWithCycles(inputGraph, { earcut, layerNudge });\n\t}\n\n\t// triangulate will modify faces and edges.\n\t// use face information to match data.\n\tconst {\n\t\tchanges,\n\t\tresult: triangulated,\n\t} = triangulate(graph, earcut);\n\n\t// explode will modify edges and vertices.\n\t// we don't need the return information for anything just yet.\n\tconst exploded = explodeFaces(triangulated);\n\t// Object.assign(changes, change2);\n\n\tif (changes.faces) {\n\t\tconst backmap = invertArrayToFlatMap(changes.faces.map);\n\t\tbackmap.forEach((oldFace, face) => {\n\t\t\tconst nudge = faces_nudge[oldFace];\n\t\t\tif (!nudge) { return; }\n\t\t\texploded.faces_vertices[face].forEach(v => {\n\t\t\t\tconst vec = scale3(nudge.vector, nudge.layer * layerNudge);\n\t\t\t\texploded.vertices_coords[v] = add3(\n\t\t\t\t\tresize3(exploded.vertices_coords[v]),\n\t\t\t\t\tvec,\n\t\t\t\t);\n\t\t\t});\n\t\t});\n\t}\n\n\treturn exploded;\n};\n"
  },
  {
    "path": "src/graph/replace.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { uniqueSortedNumbers } from \"../general/array.js\";\nimport Messages from \"../environment/messages.js\";\nimport { count } from \"./count.js\";\nimport { remapKey } from \"./maps.js\";\n\n/**\n * @param {FOLD} graph\n * @param {string} key\n * @param {number[]} replaceIndices\n * @param {number[]} replaces\n * @returns {number[]}\n */\nconst makeIndexMap = (graph, key, replaceIndices, replaces) => {\n\tconst arrayLength = count(graph, key);\n\tconst indexMap = [];\n\tfor (let i = 0, j = 0, walk = 0; i < arrayLength; i += 1, j += 1) {\n\t\twhile (i === replaces[walk]) {\n\t\t\t// this prevents arrays with holes\n\t\t\tindexMap[i] = indexMap[replaceIndices[replaces[walk]]];\n\t\t\tif (indexMap[i] === undefined) {\n\t\t\t\tthrow new Error(Messages.replaceUndefined);\n\t\t\t}\n\t\t\ti += 1;\n\t\t\twalk += 1;\n\t\t}\n\t\tif (i < arrayLength) { indexMap[i] = j; }\n\t}\n\treturn indexMap;\n};\n\n/**\n * @description Replaces vertices, edges, or faces (or anything really)\n * replace elements from inside arrays, shift up remaining components,\n * and updates all relevant references across other arrays due to shifting.\n * @param {FOLD} graph a FOLD object\n * @param {string} key like \"vertices\", the prefix of the arrays\n * @param {number[]} replaceIndices an array of vertex indices, like [1,9,25]\n * @returns {number[]} a map of changes to the graph\n * @example replace(foldObject, \"vertices\", [2,6,11,15]);\n * @example\n * replaceIndices: [4:3, 7:5, 8:3, 12:3, 14:9] where\n * - keys are indices to remove.\n * - values are the new indices that these old ones will become.\n * note: all values are less than their indices.\n *\n * for example: removing index 5 from a 10-long vertices list will shift all\n * indices > 5 up by one, and then will look through all other arrays like\n * edges_vertices, faces_vertices and update any reference to indices 6-9\n * to match their new positions 5-8.\n *\n * this can handle removing multiple indices at once; and is faster than\n * otherwise calling this multiple times with only one or a few removals.\n */\nexport const replace = (graph, key, replaceIndices) => {\n\t// make sure replace indices are well-formed. values cannot be larger than keys.\n\t// if this is the case, flip the index/value, assuming the two geometry items\n\t// are interchangeable and it doesn't matter which one we remove, but warn\n\t// the user that this took place.\n\tObject.entries(replaceIndices)\n\t\t.map(([index, value]) => [parseInt(index, 10), value])\n\t\t.filter(([index, value]) => index < value)\n\t\t.forEach(([index, value]) => {\n\t\t\t// eslint-disable-next-line no-param-reassign\n\t\t\tdelete replaceIndices[index];\n\t\t\t// eslint-disable-next-line no-param-reassign\n\t\t\treplaceIndices[value] = index;\n\t\t});\n\tconst removes = Object.keys(replaceIndices).map(n => parseInt(n, 10));\n\tconst replaces = uniqueSortedNumbers(removes);\n\tconst indexMap = makeIndexMap(graph, key, replaceIndices, replaces);\n\tremapKey(graph, key, indexMap);\n\treturn indexMap;\n};\n"
  },
  {
    "path": "src/graph/split/general.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tuniqueElements,\n} from \"../../general/array.js\";\n\n/**\n * @description create a vertices_faces entry for a single vertex by\n * matching the winding order with vertices_vertices.\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex\n * @param {{ [key: string]: number }} verticesToFace,\n * @returns {number[]|undefined} face indices or undefined if\n * not enough sufficient input data exists.\n */\nexport const makeVerticesFacesForVertex = (\n\t{ vertices_vertices, vertices_edges, edges_vertices },\n\tvertex,\n\tverticesToFace,\n) => {\n\tif (vertices_vertices) {\n\t\treturn vertices_vertices[vertex]\n\t\t\t.map(v => [vertex, v].join(\" \"))\n\t\t\t.map(key => verticesToFace[key]);\n\t}\n\tif (vertices_edges && edges_vertices) {\n\t\treturn vertices_edges[vertex]\n\t\t\t.map(edge => edges_vertices[edge])\n\t\t\t// for each edge's edges_vertices, get the vertex that isn't \"vertex\".\n\t\t\t.map(([a, b]) => (vertex === a ? [vertex, b] : [vertex, a]))\n\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t.map(key => verticesToFace[key]);\n\t}\n\treturn undefined;\n};\n\n/**\n * @description Make a faces_edges entry for a single vertex using\n * the faces_vertices array, matching winding order.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} faces\n * @param {{ [key: string]: number }} verticesToEdge\n * @return {number[][]}\n */\nexport const makeFacesEdgesForVertex = (\n\t{ faces_vertices },\n\tfaces,\n\tverticesToEdge,\n) => (faces\n\t// iterate through faces vertices, pairwise adjacent vertices, create\n\t// keys for the lookup table, convert the keys into edge indices.\n\t.map(f => faces_vertices[f]\n\t\t.map((vertex, i, arr) => [vertex, arr[(i + 1) % arr.length]])\n\t\t.map(pair => pair.join(\" \"))\n\t\t.map(key => verticesToEdge[key])));\n\n/**\n * for each face, filter it's vertices to only include those inside this edge\n * and make sure the list only contains unique numbers, as it's possible for\n * a face to visit a vertex twice, make sure vertices are unique and check if\n * the number of matching vertices in this face is 2.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} faces\n * @param {{ [key: number]: boolean }} verticesHash\n * @returns {number[]} face indices\n */\nconst filterFacesWithTwoMatches = ({ faces_vertices }, faces, verticesHash) => (faces\n\t.filter(face => new Set(faces_vertices[face].filter(v => verticesHash[v])).size === 2));\n// const filterFacesWithTwoMatches = ({ faces_vertices }, faces, verticesHash) => (faces\n// \t.filter(face => faces_vertices[face]\n// \t\t.map(vvv => vvv.filter(v => verticesHash[v]))\n// \t\t.map(subset => new Set(subset))\n// \t\t.map(set => set.size === 2)\n// \t\t.reduce((a, b) => a || b, false)));\n\n/**\n  * @description Get an edge's adjacent face(s). This does not follow the FOLD\n  * spec recommendation to order the result according to the edge's direction,\n  * and if the edge is a boundary edge this will not necessarily include nulls.\n  * If edges_faces does not exist, this method will hunt via the edge's\n  * vertices, via faces_vertices or faces_edges until matches are found.\n  * @param {FOLD} graph a FOLD object\n  * @param {number} edge index of the edge in the graph\n  * @returns {number[]} array of 0, 1, or 2 face indices adjacent to the edge\n  */\nexport const makeEdgesFacesForEdge = (\n\t{ vertices_faces, edges_vertices, edges_faces, faces_vertices, faces_edges },\n\tedge,\n) => {\n\t// if edges_faces already exists, return this entry\n\tif (edges_faces && edges_faces[edge]) { return edges_faces[edge]; }\n\n\t// if edges_vertices does not exist, we have no way of knowing the edge's\n\t// vertices (unless we can really trust the graph winding for example and\n\t// match a faces_edges in winding order to faces_vertices), but practically,\n\t// we can't go from edges to faces without going through vertices first.\n\tif (!edges_vertices) { return []; }\n\n\tif (faces_vertices) {\n\t\t// here is a vertex hash so we can quickly identify which edge\n\t\t// (according to its vertices) is our edge.\n\t\tconst vertexHash = {\n\t\t\t[edges_vertices[edge][0]]: true,\n\t\t\t[edges_vertices[edge][1]]: true,\n\t\t};\n\n\t\t// if edges_vertices and vertices_faces both exist we can use these to get\n\t\t// a subset of faces, otherwise we will have to test all faces.\n\t\t// vertices_faces will bring the O(n) run time down to O(1).\n\t\tconst faces = (vertices_faces\n\t\t\t? uniqueElements(edges_vertices[edge].flatMap(v => vertices_faces[v]))\n\t\t\t: faces_vertices.map((_, f) => f));\n\n\t\treturn filterFacesWithTwoMatches({ faces_vertices }, faces, vertexHash);\n\t}\n\n\tif (faces_edges) {\n\t\treturn faces_edges\n\t\t\t.map((_, f) => f)\n\t\t\t.filter(face => faces_edges[face].includes(edge));\n\t}\n\n\treturn [];\n};\n"
  },
  {
    "path": "src/graph/split/splitEdge.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmidpoint,\n} from \"../../math/vector.js\";\nimport {\n\tmakeVerticesToEdge,\n\tmakeVerticesToFace,\n} from \"../make/lookup.js\";\nimport {\n\tremove,\n} from \"../remove.js\";\nimport {\n\tmakeVerticesFacesForVertex,\n\tmakeFacesEdgesForVertex,\n\tmakeEdgesFacesForEdge,\n} from \"./general.js\";\n\n/**\n * @description This is a subroutine of splitEdge(). This will build two\n * edges that share one vertex, returning an array of two objects containing:\n * { edges_vertices, edges_assignment, edges_foldAngle }\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} edgeIndex the index of the edge that will be split by the new vertex\n * @param {number} newVertex the index of the new vertex\n * @returns {{\n *   edges_vertices: [number, number],\n *   edges_assignment?: string,\n *   edges_foldAngle?: number,\n * }[]} array of two edge objects, containing edge data as FOLD keys\n */\nconst makeNewEdges = (graph, edgeIndex, newVertex) => {\n\tconst edge_vertices = graph.edges_vertices[edgeIndex];\n\t/** @type {{ edges_vertices: [number, number] }[]} */\n\tconst new_edges = [\n\t\t{ edges_vertices: [edge_vertices[0], newVertex] },\n\t\t{ edges_vertices: [newVertex, edge_vertices[1]] },\n\t];\n\tnew_edges.forEach(edgeDef => [\"edges_assignment\", \"edges_foldAngle\"]\n\t\t.filter(key => graph[key] && graph[key][edgeIndex] !== undefined)\n\t\t.forEach(key => { edgeDef[key] = graph[key][edgeIndex]; }));\n\treturn new_edges;\n};\n\n/**\n * @description Update three vertices' vertices_vertices arrays,\n * the vertex which was added, and the two vertices adjacent to it.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} vertex index of the new vertex\n * @param {[number, number]} vertices the two vertices that made up the edge\n * which was just now split by the addition of our new vertex.\n * @returns {undefined}\n */\nconst updateVerticesVertices = (\n\t{ vertices_vertices },\n\tvertex,\n\tvertices,\n) => {\n\tif (!vertices_vertices) { return; }\n\n\t// create a new entry for this new vertex\n\t// only 2 vertices, no need to worry about winding order.\n\tvertices_vertices[vertex] = [...vertices];\n\n\t// for each incident vertex's vertices_vertices array, get the index\n\t// of the other incident vertex, we will splice in our new vertex here.\n\tconst verticesSpliceIndex = vertices\n\t\t.map((v, i, arr) => vertices_vertices[v].indexOf(arr[(i + 1) % arr.length]));\n\n\t// update the incident vertices' existing entries in vertices_vertices.\n\tvertices.forEach((v, i) => (verticesSpliceIndex[i] === -1\n\t\t? vertices_vertices[v].push(vertex)\n\t\t: vertices_vertices[v].splice(verticesSpliceIndex[i], 1, vertex)));\n};\n\n/**\n * @description Update three vertices' vertices_edges arrays,\n * the vertex which was added, and the two vertices adjacent to it.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} oldEdge the index of the old edge\n * @param {number} newVertex the index of the new vertex which split the edge\n * @param {[number, number]} vertices the old edge's two vertices,\n * must be aligned with \"newEdges\"\n * @param {[number, number]} newEdges the two new edges, must be aligned with \"vertices\"\n * @returns {undefined}\n */\nconst updateVerticesEdges = (\n\t{ vertices_edges },\n\toldEdge,\n\tnewVertex,\n\tvertices,\n\tnewEdges,\n) => {\n\tif (!vertices_edges) { return; }\n\n\t// our new vertex is adjacent to only the two new edges\n\tvertices_edges[newVertex] = [...newEdges];\n\n\t// the new vertex replaces the alternate vertex from each array.\n\t// find the matching index, replace it with the edge from the same index.\n\t// as \"vertices\" and \"newEdges\" are index-aligned.\n\tvertices\n\t\t.map(vertex => vertices_edges[vertex].indexOf(oldEdge))\n\t\t.map((index, i) => ({ index, vertex: vertices[i], edge: newEdges[i] }))\n\t\t.filter(el => el.index !== -1)\n\t\t.forEach(({ index, vertex, edge }) => {\n\t\t\tvertices_edges[vertex][index] = edge;\n\t\t});\n};\n\n/**\n * @description Update a vertex's vertices_faces entry, provided a list of\n * the new faces which have just been added to the graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} vertex the index of the new vertex\n * @param {number[]} faces a list of faces which were just added to the graph\n * @returns {undefined}\n */\nconst updateVerticesFaces = (\n\t{ vertices_vertices, vertices_edges, vertices_faces, edges_vertices, faces_vertices },\n\tvertex,\n\tfaces,\n) => {\n\tif (!vertices_faces) { return; }\n\n\t// if no faces exist, we should not be building vertices_faces, but we can't\n\t// proceed because everything after this point requires a faceMap.\n\t// so, simply add an unsorted set of faces to the list.\n\tif (!faces_vertices) {\n\t\tvertices_faces[vertex] = [...faces];\n\t\treturn;\n\t}\n\n\tconst verticesToFace = makeVerticesToFace({ faces_vertices }, faces);\n\n\t// we can use either vertices_vertices or vertices_edges to match winding order\n\t// these methods will also include any undefineds in the case of a boundary vertex.\n\tconst vertex_faces = makeVerticesFacesForVertex(\n\t\t{ vertices_vertices, vertices_edges, edges_vertices },\n\t\tvertex,\n\t\tverticesToFace,\n\t);\n\n\t// if neither vertices_vertices or vertices_edges exists, we cannot\n\t// respect the winding order anyway, simply add the faces to the entry.\n\tvertices_faces[vertex] = vertex_faces === undefined\n\t\t? [...faces]\n\t\t: vertex_faces;\n};\n\n/**\n * @description Update the two new edges' edges_faces to include\n * the (0, 1, or 2) faces on either side of the edges.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]} newEdges array of 2 new edges\n * @param {number[]} faces array of 0, 1, or 2 incident faces.\n * @returns {undefined}\n */\nconst updateEdgesFaces = ({ edges_faces }, newEdges, faces) => {\n\tif (!edges_faces) { return; }\n\tnewEdges.forEach(edge => { edges_faces[edge] = [...faces]; });\n};\n\n/**\n * @description Rebuild two faces' faces_vertices to include a\n * new vertex which was added in between two existing vertices\n * in each of the faces. Find the location of the two vertices and\n * splice in the new vertex.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} newVertex index of the new vertex\n * @param {[number, number]} incidentVertices neighbor vertices\n * @param {number[]} faces the faces adjacent to the old edge\n * @returns {undefined}\n */\nconst updateFacesVertices = ({ faces_vertices }, newVertex, incidentVertices, faces) => {\n\tif (!faces_vertices) { return; }\n\n\t// provide two vertices, do these vertices match (in any order) to the\n\t// incideVertices which made up the original edge before the splitting?\n\t/** @param {number} a @param {number} b @returns {boolean} */\n\tconst matchFound = (a, b) => (\n\t\t(a === incidentVertices[0] && b === incidentVertices[1])\n\t\t|| (a === incidentVertices[1] && b === incidentVertices[0]));\n\n\tfaces\n\t\t.map(i => faces_vertices[i])\n\t\t.forEach(face_vertices => face_vertices\n\t\t\t// iterate through the vertices of the face, search for a matching index\n\t\t\t// where i and i+1 are both incident vertices, in which case return the\n\t\t\t// i+1 index, as this will be the location we will splice into.\n\t\t\t.map((vertex, i, arr) => (matchFound(vertex, arr[(i + 1) % arr.length])\n\t\t\t\t? (i + 1) % arr.length\n\t\t\t\t: undefined))\n\t\t\t.filter(a => a !== undefined)\n\t\t\t// it's possible for a non-convex face to walk twice across our edges\n\t\t\t// in two different directions, if this happens, sort the splice indices\n\t\t\t// from largest to smallest so that multiple splice calls will work.\n\t\t\t.sort((a, b) => b - a)\n\t\t\t.forEach(i => face_vertices.splice(i, 0, newVertex)));\n};\n\n/**\n * @description Update two faces' faces_edges so that one edge is replaced\n * with two new edges, and because the direction of the edges matters,\n * and faces_edges must be aligned with faces_vertices, we will use\n * faces_vertices to reverse-reference edges and build faces_edges.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number[]} faces the zero, one, or two adjacent faces\n * @param {[number, number]} newEdges the two new edges\n * @returns {undefined}\n */\nconst updateFacesEdges = (\n\t{ edges_vertices, faces_vertices, faces_edges },\n\tfaces,\n\tnewEdges,\n) => {\n\tif (!faces_edges || !faces_vertices) { return; }\n\n\t// create a vertex-pair (\"a b\" string) to edge lookup table that only\n\t// includes the edges involved (both faces_edges and the new edges).\n\tconst allEdges = faces\n\t\t.flatMap(f => faces_edges[f])\n\t\t.concat(newEdges)\n\t\t.filter(a => a !== undefined);\n\tconst verticesToEdge = makeVerticesToEdge({ edges_vertices }, allEdges);\n\n\t// iterate through faces vertices, pairwise adjacent vertices, create\n\t// keys for the lookup table, convert the keys into edge indices.\n\tmakeFacesEdgesForVertex({ faces_vertices }, faces, verticesToEdge)\n\t\t.forEach((edges, i) => { faces_edges[faces[i]] = edges; });\n};\n\n/**\n * @description Update faces_faces for a list of faces.\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex\n * @param {number[]} faces\n * @returns {undefined}\n */\nconst updateFacesFaces = ({ faces_vertices, faces_faces }, vertex, faces) => {\n\tif (!faces_vertices || !faces_faces) { return; }\n\n\tconst facesSpliceIndex = faces\n\t\t.map(f => faces_vertices[f].indexOf(vertex));\n\n\tconst facesGrabIndex = facesSpliceIndex\n\t\t.map((index, i) => (index + faces_faces[faces[i]].length - 1)\n\t\t\t% faces_faces[faces[i]].length);\n\n\tconst facesCopyItem = facesGrabIndex\n\t\t.map((index, i) => faces_faces[faces[i]][index]);\n\n\t// update the incident vertices' existing entries in vertices_vertices.\n\tfaces.forEach((f, i) => (facesSpliceIndex[i] === -1\n\t\t? undefined\n\t\t: faces_faces[f].splice(facesSpliceIndex[i], 0, facesCopyItem[i])));\n};\n\n/**\n * @description Split an edge, place a new vertex between the existing\n * vertices and build two new edges between the three vertices.\n * This method creates a new vertex and new edges, but no new faces.\n * rebuilding these:\n * - vertices_coords, vertices_vertices, vertices_edges, vertices_faces,\n * - edges_vertices, edges_faces, edges_assignment, edges_foldAngle\n * - faces_vertices, faces_edges,\n * without needing to rebuild:\n * - faces_faces, faceOrders\n * without rebuilding (todo):\n * - edgeOrders\n * @param {FOLD} graph FOLD object, modified in place\n * @param {number} oldEdge index of old edge to be split\n * @param {number[]} [coords=undefined] coordinates of the new vertex to be\n * added, if omitted, a vertex will be generated at the edge's midpoint.\n * @returns {{\n *   vertex: number,\n *   edges: {\n *     map: (number|number[])[],\n *     add: [number, number],\n *     remove: number,\n *   },\n * }} a summary of the changes to the graph:\n * - \"vertex\" is the index of the new vertex (or old index, if similar)\n * - \"edges\" object contains:\n *   - \"remove\" the index of the edge that was removed\n *   - \"add\" the two new edges\n *   - \"map\" a nextmap, all values will be numbers except the value at\n *     the remove index, this will be an array containing both new indices.\n */\nexport const splitEdge = (\n\tgraph,\n\toldEdge,\n\tcoords = undefined,\n) => {\n\t// the old edge's two vertices, one vertex will be placed in between these,\n\t// and two new edges will be built to connect these to the new vertex.\n\tconst incidentVertices = graph.edges_vertices[oldEdge];\n\n\t// if the user did not supply any coordinate parameter,\n\t// as a convenience, select the the midpoint of the two points\n\tif (!coords) {\n\t\tconst [a, b] = incidentVertices.map(v => graph.vertices_coords[v]);\n\t\tcoords = midpoint(a, b);\n\t}\n\n\t// the index of the new vertex, added to the end of the existing vertex list\n\tconst vertex = graph.vertices_coords.length;\n\tgraph.vertices_coords[vertex] = coords.length === 3\n\t\t? [coords[0], coords[1], coords[2]]\n\t\t: [coords[0], coords[1]];\n\n\t// the new edge indices, they will be added to the end of the edges_ arrays.\n\t// \"newEdges\" and \"incidentVertices\" are aligned in their indices 0 and 1,\n\t// so that this vertex is in this edge. This is important for the update methods\n\tconst [e0, e1] = [0, 1].map(i => i + graph.edges_vertices.length);\n\t/** @type {[number, number]} */\n\tconst newEdges = [e0, e1];\n\n\t// make 2 new edges_vertices, edges_assignment, and edges_foldAngle.\n\t// add these fields to the graph.\n\tmakeNewEdges(graph, oldEdge, vertex)\n\t\t.forEach((edge, i) => Object.keys(edge)\n\t\t\t.forEach((key) => { graph[key][newEdges[i]] = edge[key]; }));\n\n\t// at this point we are finished with:\n\t// vertices_coords, edges_vertices, edges_assignment, edges_foldAngle\n\n\t// update the relevant vertices arrays\n\tupdateVerticesVertices(graph, vertex, incidentVertices);\n\tupdateVerticesEdges(graph, oldEdge, vertex, incidentVertices, newEdges);\n\n\t// we are now done with all data which does not relate to faces\n\n\t// we don't need to make any new faces, we only need to modify the faces\n\t// (if they exist) incident to the old edge.\n\t// note: \"incidentFaces\" may only include 1 face, in the case of a\n\t// boundary edge. this needs to be taken account, for example,\n\t// to ensure winding order matches across component arrays.\n\tconst incidentFaces = makeEdgesFacesForEdge(graph, oldEdge)\n\t\t.filter(a => a !== undefined);\n\tupdateFacesVertices(graph, vertex, incidentVertices, incidentFaces);\n\tupdateFacesEdges(graph, incidentFaces, newEdges);\n\tupdateVerticesFaces(graph, vertex, incidentFaces);\n\tupdateEdgesFaces(graph, newEdges, incidentFaces);\n\tupdateFacesFaces(graph, vertex, incidentFaces);\n\n\t// until now we never removed the old edge, this way, when we call this\n\t// method, no matter where the edge was inside the edges_ arrays, all\n\t// the indices after it will shift up to fill in the hole and this\n\t// method handles all re-indexing, including inside the _edges arrays.\n\tconst edgeMap = remove(graph, \"edges\", [oldEdge]);\n\n\t// at this point our graph is complete. prepare the changelog info to return\n\n\t// shift our new edge indices since these relate to the graph before remove()\n\tnewEdges.forEach((_, i) => { newEdges[i] = edgeMap[newEdges[i]]; });\n\n\t// we had to run \"remove\" with the new edges added, but the edgeMap should\n\t// relate to the graph before any changes. remove those new edge entries,\n\t// and set them to be the new edges under the \"oldEdge\" index.\n\t/** @type {(number|number[])[]} */\n\tconst edgesMap = edgeMap.slice();\n\tedgesMap.splice(-2);\n\tedgesMap[oldEdge] = newEdges;\n\n\treturn {\n\t\tvertex,\n\t\tedges: {\n\t\t\tmap: edgesMap,\n\t\t\tadd: newEdges,\n\t\t\tremove: oldEdge,\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "src/graph/split/splitFace.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tuniqueElements,\n\tsplitCircularArray,\n} from \"../../general/array.js\";\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../make/edgesFaces.js\";\nimport {\n\tmakeVerticesToEdge,\n\tmakeVerticesToFace,\n} from \"../make/lookup.js\";\nimport {\n\taddEdge,\n\taddIsolatedEdge,\n} from \"../add/edge.js\";\nimport {\n\tremove,\n} from \"../remove.js\";\nimport {\n\tmakeVerticesFacesForVertex,\n} from \"./general.js\";\n\n/**\n * @description Return true if all of the contents of an array are unique\n * (the conversion into a set results in an array of the same length).\n * @param {any[]} array an array of any type\n * @returns {boolean}\n */\nconst arrayValuesAreUnique = (array) => (\n\tArray.from(new Set(array)).length === array.length\n);\n\n/**\n * @description Splice in the leaf vertex and duplicate the vertex\n * at the splice index to be on either side of the leaf vertex, like:\n * _ _ _ 5 8 5 _ _, where 5 got spliced into the spot where 8 was.\n * @param {any[]} array an array of any type\n * @param {number} spliceIndex the index to splice into the array\n * @param {any} newElement matching type as array, the new element to splice in\n * @returns {any[]} a copy of the input array, with new elements.\n * @example\n * splitArrayWithLeaf(array, 3, 99)\n * // [0, 1, 2, 3, 99, 3, 4, 5, 6]\n */\nconst splitArrayWithLeaf = (array, spliceIndex, newElement) => {\n\tconst arrayCopy = [...array];\n\tconst duplicateElement = array[spliceIndex];\n\tarrayCopy.splice(spliceIndex, 0, duplicateElement, newElement);\n\treturn arrayCopy;\n};\n\n/**\n * @description ensure that the two vertices are not next to each other\n * in the current face's faces_vertices vertices list.\n * @param {FOLD} graph a FOLD object\n * @param {number} face\n * @param {number[]} vertices\n * @returns {boolean}\n */\nconst edgeExistsInFace = ({ faces_vertices }, face, vertices) => (\n\tfaces_vertices[face]\n\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t.map(([v0, v1]) => (v0 === vertices[0] && v1 === vertices[1])\n\t\t\t|| (v0 === vertices[1] && v1 === vertices[0]))\n\t\t.reduce((a, b) => a || b, false));\n\n/**\n * @description A new cycle (face) of vertices has been created,\n * using this cycle, we want to update the adjacent vertices list for a\n * particular vertex, specifically we want to find the index of\n * @param {number[]} cycle\n * @param {number[]} adjacent\n * @param {number} vertex\n * @returns {number}\n */\nconst getAdjacencySpliceIndex = (cycle, adjacent, vertex) => {\n\t// the index of this vertex inside cycle\n\tconst indexInCycle = cycle.indexOf(vertex);\n\tif (indexInCycle === -1) { return -1; }\n\n\t// the previous and next vertex in the cycle from this vertex\n\tconst prevVertex = cycle[(indexInCycle + cycle.length - 1) % cycle.length];\n\tconst nextVertex = cycle[(indexInCycle + 1) % cycle.length];\n\n\t// both the cycle and the adjacent vertices windings are counter-clockwise,\n\t// this means that when walking around the cycle the \"prev\" and \"next\"\n\t// vertices, when observed in the adjacency list, will be visited in reverse,\n\t// \"prev\" will appear right after \"next\". (feels a bit backwards).\n\t// so, in the adjacency, we want to splice inbetween \"next\", ___, \"prev\",\n\t// and for Javascript splice() this means we use the \"prev\" index.\n\tconst prevIndex = adjacent.indexOf(prevVertex);\n\tif (prevIndex === -1) { return -1; }\n\n\t// quickly verify that \"nextVertex\" is the vertex previous to \"prev\"\n\t// in the adjacency array, verifying that the adjacency was built correctly\n\tconst nextTest = adjacent[(prevIndex + adjacent.length - 1) % adjacent.length];\n\treturn (nextTest !== nextVertex ? -1 : prevIndex);\n};\n\n/**\n * @description Update vertices_vertices for two vertices, following\n * a new edge having just been made to connect the pair of vertices.\n * These vertices are both members of the same face, whose faces_vertices\n * was just updated.\n * ```\n *     (4)     ___---O.  (3)                (3)     ___---O.  (2)\n *         O---         \\.                      O---         \\.\n *        /   .            O  (2)              /   .  (6) O    O  (1)\n *       /       .        /                   /       .   |   /\n *  (0) O____      .   /                 (4) O____      . | /\n *           ----____O  (1)                       ----____O  (5)(0)\n * ```\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face containing these two vertices\n * @param {number[]} vertices the two vertices newly connected by an edge\n * @returns {undefined}\n*/\nconst updateVerticesVertices = (\n\t{ vertices_vertices, faces_vertices },\n\tface, // todo: is this okay, for leaf, this contents is actually the updated contents\n\tvertices,\n) => {\n\tif (!vertices_vertices) { return; }\n\n\tconst verticesSpliceIndex = vertices\n\t\t.map(v => getAdjacencySpliceIndex(faces_vertices[face], vertices_vertices[v], v));\n\n\tverticesSpliceIndex.forEach((index, i) => {\n\t\tconst otherVertex = vertices[(i + 1) % vertices.length];\n\t\tif (index !== -1) {\n\t\t\tvertices_vertices[vertices[i]].splice(index, 0, otherVertex);\n\t\t} else {\n\t\t\tvertices_vertices[vertices[i]].push(otherVertex);\n\t\t}\n\t});\n};\n\n/**\n * @description We are building a new edge between these two vertices.\n * Update vertices_edges, having just updated vertices_vertices.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} vertices two vertex indices\n * @param {number} edge an edge index\n * @returns {undefined}\n */\nconst updateVerticesEdges = (\n\t{ vertices_edges, vertices_vertices },\n\tvertices,\n\tedge,\n) => {\n\tif (!vertices_edges) { return; }\n\n\t// vertices_vertices does not exist, create an unsorted solution.\n\tif (!vertices_vertices) {\n\t\tvertices.forEach((v) => vertices_edges[v].push(edge));\n\t\treturn;\n\t}\n\n\t// for each of the two vertices, check its vertices_vertices for the\n\t// index of the opposite vertex. this is the edge. return its position\n\t// in the vertices_vertices to be used to insert into vertices_edges.\n\tconst spliceIndices = vertices\n\t\t.map((v, i, arr) => vertices_vertices[v].indexOf(arr[(i + 1) % arr.length]));\n\n\tif (spliceIndices.some(i => i === -1)) {\n\t\tthrow new Error(`splitFace() vertices_edges ${vertices.join(\", \")}`);\n\t}\n\n\t// vvIndex is an index inside vertices_edges (or vertices_vertices)\n\t// splice the new edge so that it gets inserted before the\n\t// edge at the current index\n\tspliceIndices.forEach((index, i) => {\n\t\tvertices_edges[vertices[i]].splice(index, 0, edge);\n\t});\n};\n\n/**\n * @description Update vertices_faces\n * @param {FOLD} graph a FOLD object\n * @param {number} face\n * @param {number[]} faces\n * @returns {undefined}\n */\nconst updateVerticesFaces = (\n\t{ vertices_vertices, vertices_edges, vertices_faces, edges_vertices, faces_vertices },\n\tface,\n\tfaces,\n) => {\n\tif (!vertices_faces || !faces_vertices) { return; }\n\n\t// all faces which are in need of an update includes more than just our\n\t// new vertices. other vertices which are associated with the face-to-be-\n\t// removed need to know which of the new faces replaces the old face index\n\tconst allVertices = uniqueElements(faces.flatMap(f => faces_vertices[f]));\n\n\t// get all faces involved, but filter out the old face.\n\t// due to vertices_faces, we have to filter out any null values\n\tconst allFaces = uniqueElements([\n\t\t...faces,\n\t\t...allVertices.flatMap(v => vertices_faces[v])\n\t]).filter(f => f !== face)\n\t\t.filter(a => a !== undefined && a !== null);\n\n\tconst verticesToFace = makeVerticesToFace({ faces_vertices }, allFaces);\n\n\tallVertices.map(vertex => makeVerticesFacesForVertex(\n\t\t{ vertices_vertices, vertices_edges, edges_vertices },\n\t\tvertex,\n\t\tverticesToFace,\n\t)).forEach((v_f, i) => { vertices_faces[allVertices[i]] = v_f || []; });\n};\n\n/**\n * @description called near the end of the split_convex_face method.\n * update the \"edges_faces\" array for every edge involved.\n * figure out where the old_face's index is in each edges_faces array,\n * figure out which of the new faces (or both) need to be added and\n * substitute the old index with the new face's index/indices.\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face being replaced\n * @param {number[]} faces a list of faces which will replace the old face\n * @param {number} edge the new edge index\n * @returns {undefined}\n */\nconst updateEdgesFaces = (\n\t{ edges_vertices, faces_vertices, edges_faces, faces_edges },\n\tface,\n\tfaces,\n\tedge,\n) => {\n\tif (!edges_faces) { return; }\n\n\t// it's probably rare for a graph to have edges_faces but no faces_edges.\n\t// This is an easy solution, just build the entire array from scratch.\n\tif (!faces_edges) {\n\t\tedges_faces.forEach((_, i) => delete edges_faces[i]);\n\t\tObject.assign(\n\t\t\tedges_faces,\n\t\t\tmakeEdgesFacesUnsorted({ edges_vertices, faces_vertices, faces_edges }),\n\t\t);\n\t\treturn;\n\t}\n\n\tconst facesHash = {};\n\t[...faces, face].forEach(f => { facesHash[f] = true; });\n\n\tconst edges = Array.from(new Set(faces.flatMap(f => faces_edges[f])));\n\n\tconst edgesOtherFaces = [];\n\tedges.forEach(e => {\n\t\tedgesOtherFaces[e] = edges_faces[e].filter(f => !facesHash[f]);\n\t});\n\n\tconst edgesTheseFaces = [];\n\tedges.forEach(e => { edgesTheseFaces[e] = []; });\n\tfaces.forEach(f => faces_edges[f]\n\t\t.forEach(e => edgesTheseFaces[e].push(f)));\n\n\tedges.forEach(e => {\n\t\tedges_faces[e] = Array\n\t\t\t.from(new Set([...edgesTheseFaces[e], ...edgesOtherFaces[e]]));\n\t});\n\n\tedges_faces[edge] = [...faces];\n};\n\n/**\n * @description A subroutine of splitFaceWithEdge only (where one face becomes\n * two faces). This method will create two new faces's faces_vertices and\n * append them to the graph. Momentarily, these will coexist with the face they\n * are meant to replace, but the old face will be deleted at the end of the\n * main method.\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face to be split into two faces\n * @param {[number, number]} vertices two vertices, members of this face's\n * vertices, which will become a new edge and split the face into two faces.\n */\nconst updateFacesVerticesSplit = ({ faces_vertices }, face, vertices) => {\n\tconst [i0, i1] = vertices\n\t\t.map(vertex => faces_vertices[face].indexOf(vertex));\n\treturn splitCircularArray(faces_vertices[face], [i0, i1])\n\t\t.map((face_vertices) => faces_vertices.push(face_vertices));\n};\n\n/**\n * @description A subroutine of splitFaceWithLeafEdge only (where a leaf edge\n * is added but the number of faces remains the same). This method will\n * create a new set of faces_vertices which cycles around the face, visits\n * the leaf vertex, then returns to the vertex from which it just came (making\n * this vertex appear twice in the face).\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face to get the new edge.\n * @param {number} vertexFace the vertex which is currently in the face.\n * @param {number} vertexLeaf the vertex which is not currently in the face.\n * @returns {undefined}\n */\nconst updateFacesVerticesLeaf = (\n\t{ faces_vertices },\n\tface,\n\tvertexFace,\n\tvertexLeaf,\n) => {\n\tif (!faces_vertices) { return; }\n\t// this method will splice in the leaf vertex and duplicate the vertex\n\t// at the splice index to be on either side of the leaf vertex, like:\n\t// _ _ _ 5 8 5 _ _, where 5 got spliced into the spot where 8 was.\n\tfaces_vertices[face] = splitArrayWithLeaf(\n\t\tfaces_vertices[face],\n\t\tfaces_vertices[face].indexOf(vertexFace),\n\t\tvertexLeaf,\n\t);\n};\n\n/**\n * @description In the case where we are building two new faces in place of\n * an old face, construct new faces_edges for the new faces by consulting\n * the newly created faces_vertices for each face. Do this by gathering all\n * edges involved (from the old face's faces_edges, and the new edge), create\n * a vertex-pair to edge lookup, then convert faces_vertices to faces_edges.\n * @param {FOLD} graph a FOLD object\n * @param {number} face the index of the face being replaced\n * @param {number[]} faces a list of faces which will replace the old face\n * @param {number} edge the new edge index\n * @returns {undefined}\n */\nconst updateFacesEdges = (\n\t{ edges_vertices, faces_vertices, faces_edges },\n\tface,\n\tfaces,\n\tedge,\n) => {\n\tif (!faces_edges) { return; }\n\n\t// all edges involved in this rewrite, duplicates are okay here.\n\tconst allEdges = [...faces_edges[face], edge];\n\n\t// create a reverse lookup, pairs of vertices to an edge.\n\t// const verticesToEdge = makeVerticesToEdgeLookup({ edges_vertices }, allEdges);\n\tconst verticesToEdge = makeVerticesToEdge({ edges_vertices }, allEdges);\n\n\t// simply rebuild the faces_edges in question using the vertices-edge lookup\n\tconst newFacesEdges = faces\n\t\t.map(f => faces_vertices[f]\n\t\t\t.map((fv, i, arr) => [fv, arr[(i + 1) % arr.length]])\n\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t.map(key => verticesToEdge[key]));\n\n\tif (newFacesEdges.flat().some(e => e === undefined)) {\n\t\tthrow new Error(`splitFace() faces_edges ${face}`);\n\t}\n\n\tfaces.forEach((f, i) => { faces_edges[f] = newFacesEdges[i]; });\n};\n\n/**\n * @description one face was removed and one or two faces put in its place.\n * regarding the faces_faces array, updates need to be made to the two\n * new faces, as well as all the previously neighboring faces of\n * the removed face.\n * @param {FOLD} graph a FOLD object\n * @param {number} oldFace\n * @param {number[]} faces a list of faces which will replace the old face\n * @returns {undefined}\n */\nconst updateFacesFaces = (\n\t{ edges_vertices, edges_faces, faces_vertices, faces_edges, faces_faces },\n\toldFace,\n\tfaces,\n) => {\n\tif (!faces_faces) { return; }\n\n\t// these are all of the faces which need their faces_faces updated\n\t// due to faces_faces we have to filter out any undefined or null\n\tconst allFaces = uniqueElements([...faces_faces[oldFace], ...faces])\n\t\t.filter(a => a !== undefined && a !== null);\n\n\t// if both edges_faces and faces_edges exists, this is the easiest way:\n\t// for every edge, get the face across it, this preserves winding order.\n\tif (edges_faces && faces_edges) {\n\t\tallFaces.forEach(face => {\n\t\t\tfaces_faces[face] = faces_edges[face]\n\t\t\t\t.map(edge => edges_faces[edge].find(f => f !== face));\n\t\t});\n\t\treturn;\n\t}\n\t// if (edges_faces && faces_vertices && edges_vertices) {\n\t// \t// this is not great, we can't pass in a subset of edges here.\n\t// \tconst verticesToEdge = makeVerticesToEdge({ edges_vertices });\n\t// \tallFaces.forEach(face => {\n\t// \t\tconst face_edges = faces_vertices[face]\n\t// \t\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t// \t\t\t.map(pair => pair.join(\" \"))\n\t// \t\t\t.map(key => verticesToEdge[key]);\n\t// \t\tfaces_faces[face] = face_edges\n\t// \t\t\t.map(edge => edges_faces[edge].find(f => f !== face));\n\t// \t});\n\t// \treturn;\n\t// }\n\n\tif (edges_vertices && faces_vertices) {\n\t\tconst verticesToFace = makeVerticesToFace({ faces_vertices }, allFaces);\n\t\tallFaces.forEach(face => {\n\t\t\tfaces_faces[face] = faces_vertices[face]\n\t\t\t\t// build the vertex pair in reverse to get the face opposite\n\t\t\t\t.map((v, i, arr) => [arr[(i + 1) % arr.length], v])\n\t\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t\t.map(key => verticesToFace[key]);\n\t\t});\n\t}\n};\n\n/**\n * @description\n * @param {FOLD} graph a FOLD graph\n * @param {number} oldFace\n * @param {number[]} newFaces\n */\nconst updateFaceOrders = ({ faceOrders }, oldFace, newFaces) => {\n\tif (!faceOrders) { return; }\n\t// a single new face\n\tconst newFace = newFaces[0];\n\t// all the other new faces besides the first face\n\tconst faces = newFaces.slice(1);\n\t/** @type {[number, number, number][]} */\n\tconst newFaceOrders = [];\n\tfaceOrders.forEach(([f0, f1, order], i) => {\n\t\tif (f0 === oldFace) {\n\t\t\tfaceOrders[i] = [newFace, f1, order];\n\t\t\t/** @type {[number, number, number][]} */\n\t\t\tconst newOrders = faces.map(f => [f, f1, order]);\n\t\t\tnewFaceOrders.push(...newOrders);\n\t\t}\n\t\tif (f1 === oldFace) {\n\t\t\tfaceOrders[i] = [f0, newFace, order];\n\t\t\t/** @type {[number, number, number][]} */\n\t\t\tconst newOrders = faces.map(f => [f0, f, order]);\n\t\t\tnewFaceOrders.push(...newOrders);\n\t\t}\n\t});\n\tfaceOrders.push(...newFaceOrders);\n};\n\n/**\n * @throws\n * @description Cut a face through one of the face's existing vertices to a\n * new vertex which is intended to be newly created, but not yet associated\n * with any face. This will create a new edge between the two vertices, which\n * the faces_edges will traverse twice, and this face's faces_vertices will\n * visit the faceVertex twice, going to and returning from the new leaf vertex.\n * @param {FOLD} graph a FOLD object\n * @param {number} face\n * @param {number} vertexFace\n * @param {number} vertexLeaf\n * @param {string} [assignment=\"U\"]\n * @param {number} [foldAngle=0]\n * @returns {{ edge: number, faces: {} }} a summary of changes to the graph,\n * faces will not be changed so this object will be empty.\n */\nexport const splitFaceWithLeafEdge = (\n\tgraph,\n\tface,\n\tvertexFace,\n\tvertexLeaf,\n\tassignment = \"U\",\n\tfoldAngle = 0,\n) => {\n\t// validate inputs\n\t/** @type {[number, number]} */\n\tconst vertices = [vertexFace, vertexLeaf];\n\n\t// construct edge\n\tconst edge = addEdge(graph, vertices, [face, face], assignment, foldAngle);\n\n\t// rebuild face\n\n\t// this is a unique situation. from this point on we are associating\n\t// this leaf vertex with this face. make vertices_faces include this face.\n\tif (graph.vertices_faces) {\n\t\tgraph.vertices_faces[vertexLeaf] = Array\n\t\t\t.from(new Set([...graph.vertices_faces[vertexLeaf], face]));\n\t}\n\n\t// skip vertices_faces and edges_faces, these were previously set.\n\t// also, skip faces_faces, adjacent face data does not change.\n\tupdateFacesVerticesLeaf(graph, face, vertexFace, vertexLeaf);\n\tupdateFacesEdges(graph, face, [face], edge);\n\tupdateVerticesVertices(graph, face, vertices);\n\tupdateVerticesEdges(graph, vertices, edge);\n\n\treturn {\n\t\tedge,\n\t\tfaces: {},\n\t};\n};\n\n/**\n * @description Cut a face through one of the face's existing vertices to a\n * new point which is intended to become a new vertex and to be associated with\n * this face. This will create a new edge between the two vertices, which\n * the faces_edges will traverse twice, and this face's faces_vertices will\n * visit the faceVertex twice, going to and returning from the new leaf vertex.\n * @param {FOLD} graph a FOLD object\n * @param {number} face\n * @param {number} vertex\n * @param {[number, number]} point\n * @param {string} [assignment=\"U\"]\n * @param {number} [foldAngle=0]\n * @returns {{ edge: number, faces: {} }} a summary of changes to the graph,\n * faces will not be changed so this object will be empty.\n */\n// export const splitFaceLeafEdgePoint = (\n// \tgraph,\n// \tface,\n// \tvertex,\n// \tpoint,\n// \tassignment = \"U\",\n// \tfoldAngle = 0,\n// ) => splitFaceWithLeafEdge(\n// \tgraph,\n// \tface,\n// \tvertex,\n// \taddVertex(graph, point),\n// \tassignment,\n// \tfoldAngle,\n// );\n\n/**\n * @throws Throws an error if the input graph data is poorly formed.\n * @description Split a face into two faces by specifying two vertices,\n * place a new edge between the two vertices, and rebuild all component arrays.\n * @param {FOLD} graph a FOLD object, modified in place, must contain\n * vertices_coords, edges_vertices, and faces_vertices to work.\n * @param {number} face index of face to split\n * @param {[number, number]} vertices the vertices which will create a new edge\n * @param {string} [assignment=\"U\"] the assignment of the new edge\n * @param {number} [foldAngle=0] the fold angle of the new edge\n * @returns {{\n *   edge?: number,\n *   faces?: { map?: (number|number[])[], new?: number[], remove?: number },\n * }} a summary of changes to the FOLD object\n */\nexport const splitFaceWithEdge = (\n\tgraph,\n\tface,\n\tvertices,\n\tassignment = \"U\",\n\tfoldAngle = 0,\n) => {\n\tif (!graph.vertices_coords\n\t\t|| !graph.edges_vertices\n\t\t|| !graph.faces_vertices) { return {}; }\n\tif (vertices.length !== 2) { return {}; }\n\n\t// this method will work fine with non-convex polygons, but if a face\n\t// contains a leaf edge, where the sequence of face_vertices visits the same\n\t// vertex more than once, this method might fail. don't proceed.\n\tif (!arrayValuesAreUnique(graph.faces_vertices[face])) { return {}; }\n\n\t// ensure that the two vertices are not next to each other in\n\t// the current face's faces_vertices vertices list.\n\tif (edgeExistsInFace(graph, face, vertices)) { return {}; }\n\n\t// edges_faces will be [x, x] where x is the index of\n\t// the old face, twice, and will be replaced later in this function.\n\tconst edge = addEdge(graph, vertices, [face, face], assignment, foldAngle);\n\n\tconst faces = [0, 1].map(i => graph.faces_vertices.length + i);\n\n\tupdateFacesVerticesSplit(graph, face, vertices);\n\tupdateFacesEdges(graph, face, faces, edge);\n\tupdateVerticesVertices(graph, face, vertices);\n\tupdateVerticesEdges(graph, vertices, edge);\n\tupdateVerticesFaces(graph, face, faces);\n\tupdateEdgesFaces(graph, face, faces, edge);\n\tupdateFacesFaces(graph, face, faces);\n\tupdateFaceOrders(graph, face, faces);\n\n\t// remove old data\n\tconst faceMap = remove(graph, \"faces\", [face]);\n\n\t// the graph is now complete, however our return object needs updating.\n\t// shift our new face indices since these relate to the graph before remove().\n\tfaces.forEach((_, i) => { faces[i] = faceMap[faces[i]]; });\n\n\t// we had to run \"remove\" with the new faces added, but the face map\n\t// should represent the graph before any changes - to after changes.\n\t// the correct place for the new faces is in a value position, not index.\n\t// remove these two faces\n\t/** @type {(number|number[])[]} */\n\tconst facesMap = faceMap.slice();\n\tfacesMap.splice(-2);\n\n\t// set the location of the old face in the map to be the new faces\n\tfacesMap[face] = faces;\n\n\treturn {\n\t\tedge,\n\t\tfaces: {\n\t\t\tmap: facesMap,\n\t\t\tnew: faces,\n\t\t\tremove: face,\n\t\t},\n\t};\n};\n\n/**\n * @throws Throws an error if the input graph data is poorly formed.\n * @description Split a face in a graph with a new edge between two vertices,\n * where one or two of these vertices is already a part of the face's\n * faces_vertices. This method will have a different result depending on the\n * number of vertices of the new edge which are a part of the face:\n * - 0: this method will create an edge, unassociated with any faces,\n *      no faces will be modified and the method will return early.\n * - 1: this method will create a face with an edge that doubles back on itself.\n * - 2: this method will create two new faces, split by an edge, as long as\n *      these two new edge vertices are not already an edge on the face's boundary\n * This method will create no new vertices, one new edge, and will replace\n * the face with either one or two new face(s).\n * New edges will be added to the end of the arrays, so all old edge\n * indices will still relate. Face indices will be more heavily modified.\n * @param {FOLD} graph a FOLD object, modified in place, must contain\n * vertices_coords, edges_vertices, and faces_vertices to work.\n * @param {number} face index of face to split\n * @param {[number, number]} vertices the vertices which will create a new edge\n * @param {string} [assignment=\"U\"] the assignment of the new edge\n * @param {number} [foldAngle=0] the fold angle of the new edge\n * @returns {{\n *   edge?: number,\n *   faces?: { map?: (number|number[])[], new?: number[], remove?: number },\n * }} a summary of changes to the FOLD object\n */\nexport const splitFace = (\n\tgraph,\n\tface,\n\tvertices,\n\tassignment = \"U\",\n\tfoldAngle = 0,\n) => {\n\tif (!graph.vertices_coords\n\t\t|| !graph.edges_vertices\n\t\t|| !graph.faces_vertices) { return {}; }\n\tif (vertices.length !== 2) { return {}; }\n\n\t// run the correct method depending on how many vertices are a part of the face\n\tconst verticesIndexOf = vertices.map(vertex => ({\n\t\tvertex,\n\t\tindex: graph.faces_vertices[face].indexOf(vertex),\n\t}));\n\n\tconst matchCount = verticesIndexOf.filter(({ index }) => index !== -1).length;\n\n\tswitch (matchCount) {\n\tcase 2:\n\t\t// this edge will split the existing face into two faces\n\t\treturn splitFaceWithEdge(graph, face, vertices, assignment, foldAngle);\n\tcase 1:\n\t\t// this edge will connect to only one face vertex, the other point\n\t\t// will lie somewhere inside the face, the face will become non-convex.\n\t\treturn splitFaceWithLeafEdge(\n\t\t\tgraph,\n\t\t\tface,\n\t\t\tverticesIndexOf.filter(({ index }) => index !== -1).shift().vertex,\n\t\t\tverticesIndexOf.filter(({ index }) => index === -1).shift().vertex,\n\t\t\tassignment,\n\t\t\tfoldAngle,\n\t\t);\n\tdefault:\n\t\t// add the edge to the graph without any face associations\n\t\treturn {\n\t\t\tedge: addIsolatedEdge(graph, vertices, assignment, foldAngle),\n\t\t\tfaces: {},\n\t\t}\n\t}\n};\n"
  },
  {
    "path": "src/graph/split/splitGraph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeL,\n\tincludeR,\n\tincludeS,\n} from \"../../math/compare.js\";\nimport {\n\tpointsToLine2,\n} from \"../../math/convert.js\";\nimport {\n\tmergeNextmaps,\n} from \"../maps.js\";\nimport {\n\tintersectLineAndPoints,\n\tfilterCollinearFacesData,\n} from \"../intersect.js\";\nimport {\n\tsplitEdge,\n} from \"./splitEdge.js\";\nimport {\n\tsplitFace,\n} from \"./splitFace.js\";\nimport {\n\taddVertices,\n} from \"../add/vertex.js\";\n\n/**\n * @typedef SplitGraphEvent\n * @type {{\n *   vertices?: {\n *     intersect: number[],\n *     source: ((FacePointEvent & { face: number, faces: number[] })\n *       |(FaceEdgeEvent & { vertices: [number, number] }))[],\n *   },\n *   edges?: {\n *     intersect: LineLineEvent[],\n *     new: number[],\n *     map: (number|number[])[],\n *     source: { face: number, faces: number[] }[],\n *   },\n *   faces?: {\n *     intersect: {\n *       vertices: FaceVertexEvent[],\n *       edges: FaceEdgeEvent[],\n *       points: FacePointEvent[],\n *     }[],\n *     map: (number|number[])[],\n *   },\n * }}\n * @description The source info for both edges and vertices will contain\n * an entry for both \"face\" and \"faces\" (vertices, in the case of vertex-face),\n * where \"face\" is the index of the face in relation to the graph as it was\n * before running this method, and \"faces\" (containing one or two indices)\n * are indices from the graph after being modified by the method.\n */\n\n/**\n * @description Iterate over a list of arrays, return the sum of\n * the lengths of all the arrays.\n * @param {...any[]} arrays a list of arrays\n * @returns {number} the sum of all the array lengths\n */\nconst arraysLengthSum = (...arrays) => arrays\n\t.map(arr => arr.length)\n\t.reduce((a, b) => a + b, 0);\n\n/**\n * @description The internal function for splitting a graph with a line\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {VecLine2} line a splitting line\n * @param {Function} lineDomain the domain function for the line\n * @param {[number, number][]} interiorPoints if the line is a segment or ray,\n * place its endpoints here\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SplitGraphEvent} an object describing the changes\n */\nexport const splitGraphWithLineAndPoints = (\n\tgraph,\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tepsilon = EPSILON,\n) => {\n\t// todo: test with graph with no faces_vertices\n\tif (!graph.vertices_coords || !graph.edges_vertices) {\n\t\treturn {};\n\t}\n\n\t// this method will modify the input graph in this manner:\n\t// - vertices will not shift around, new vertices will be added to the end\n\t// - edge indices will be heavily modified, an edge map will be generated\n\t// - face indices will be heavily modified, a face map will be generated\n\tlet edgeMap = graph.edges_vertices.map((_, i) => [i]);\n\tlet faceMap = graph.faces_vertices.map((_, i) => [i]);\n\n\t// for each new vertex (index), an object that describes how that vertex\n\t// was created, which will be 1 of 2 ways:\n\t// - splitting an edge: { a, b, vertices, point }\n\t// - interior point in face: { face, point }\n\t/**\n\t * @type {((FacePointEvent & { face: number, faces: number[], vertices?: never })|\n\t * (FaceEdgeEvent & { vertices: [number, number], face?: never, faces?: never }))[]}\n\t */\n\tconst verticesSource = [];\n\n\t// for each new edge (index), an object that describes how that edge\n\t// was created. this does not include edges split from 1 into 2, but\n\t// does include entirely new edges made to split a face.\n\t/** @type {{ face: number, faces: number[] }[]} */\n\tconst edgesSource = [];\n\n\t// split all edges and create a list of new vertices\n\t// for each face that has a new edge crossing it, re-build the face\n\t// intersections contains: { vertices, edges, faces }\n\tconst intersections = intersectLineAndPoints(\n\t\tgraph,\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tinteriorPoints,\n\t\tepsilon,\n\t);\n\n\t// intersectLineAndPoints will return face intersections that includes all\n\t// vertex overlaps. When a face is convex and only two of its vertices\n\t// are overlapped and those vertices are neighbors, we want to exclude this\n\t// intersection as it does not cross through the face.\n\tfilterCollinearFacesData(graph, intersections);\n\n\t// This is required for the splitFace method, where we are given a face's\n\t// intersected edges, which were split in splitEdge and a new vertex made,\n\t// so, we need to provide the new vertex index for the old edge index.\n\t// this object maps an old edge's index (index) to a new vertex's index (value)\n\tconst oldEdgeNewVertex = {};\n\n\t// edge entry will be undefined if no intersection, filter these out,\n\t// for each intersected edge, get the current index of this (old) edge index,\n\t// split the edge creating two edges and one vertex, and update the edge map.\n\t// store the source for how this vertex was made (edge split object).\n\tintersections.edges\n\t\t.map((intersection, edge) => (intersection\n\t\t\t? ({ ...intersection, edge })\n\t\t\t: undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(({ a, b, point, edge }) => {\n\t\t\tconst newEdge = edgeMap[edge][0];\n\t\t\tconst [v0, v1] = graph.edges_vertices[newEdge];\n\t\t\t/** @type {[number, number]} */\n\t\t\tconst vertices = [v0, v1];\n\t\t\tconst { vertex, edges: { map } } = splitEdge(graph, newEdge, point);\n\n\t\t\t// the intersection information that created this vertex\n\t\t\tverticesSource[vertex] = { a, b, vertices, edge, point };\n\n\t\t\t// edge indices were heavily modified, update the edge map\n\t\t\tedgeMap = mergeNextmaps(edgeMap, map);\n\t\t\toldEdgeNewVertex[edge] = vertex;\n\t\t});\n\n\t// collect all new edges, store them as values under\n\t// the index of the face (original index) they were made to be apart of.\n\tconst oldFaceNewEdge = {};\n\n\t// faces' intersections can be from overlapped vertices, overlapped edges,\n\t// or from interior points inside the face. We are considering only the cases\n\t// where two of these events exist per face. This cuts out non-convex faces.\n\tintersections.faces\n\t\t.map(({ vertices, edges, points }, face) => ({ vertices, edges, points, face }))\n\t\t.filter(({ vertices, edges, points }) => (\n\t\t\tarraysLengthSum(vertices, edges, points) === 2))\n\t\t.forEach(({ vertices, edges, points, face }) => {\n\t\t\tconst newFace = faceMap[face][0];\n\n\t\t\t// for faces intersected at the edge, this edge was just split and a new\n\t\t\t// vertex was created. use the edge indices to get these new vertices\n\t\t\tconst splitEdgesVertices = edges.map(({ edge }) => oldEdgeNewVertex[edge]);\n\n\t\t\t// for faces containing an isolated point inside it, add this point(s)\n\t\t\t// to the graph as a new vertex (or vertices).\n\t\t\tconst isolatedPointVertices = addVertices(\n\t\t\t\tgraph,\n\t\t\t\tpoints.map(({ point }) => point),\n\t\t\t);\n\n\t\t\t// the list of all vertices involved in splitting this face,\n\t\t\t// which will sum to length 2, comes from one of three places:\n\t\t\t// - overlapped, pre-existing vertices\n\t\t\t// - vertices created by intersected edges\n\t\t\t// - isolated points inside of the face\n\t\t\tconst allNewVertices = vertices.map(({ vertex }) => vertex)\n\t\t\t\t.concat(splitEdgesVertices)\n\t\t\t\t.concat(isolatedPointVertices);\n\t\t\t/** @type {[number, number]} */\n\t\t\tconst newEdgeVertices = [allNewVertices[0], allNewVertices[1]];\n\n\t\t\t// split face, make one new edge, this changes the face indice to the\n\t\t\t// point of needing a map. new edge simply added to end of edge arrays.\n\t\t\t// this will create either\n\t\t\t// - splitting edge between two faces\n\t\t\t// - leaf edge with a leaf vertex that cuts into one non-split face\n\t\t\t// - isolated edge, inside the face but unassociated with any face\n\t\t\tconst {\n\t\t\t\tedge: newEdgeIndex,\n\t\t\t\tfaces: { map },\n\t\t\t} = splitFace(graph, newFace, newEdgeVertices);\n\n\t\t\t// new edge (and possibly new vertices) were just created, update\n\t\t\t// the source information that created these new components.\n\t\t\t// \"face\" is the index as it relates to the input graph,\n\t\t\t// \"faces\" will be set to the finished graph's new indices for\n\t\t\t// whatever the original face turned into (either one or two faces).\n\t\t\tedgesSource[newEdgeIndex] = { face, faces: undefined };\n\t\t\tisolatedPointVertices.forEach((vertex, i) => {\n\t\t\t\tverticesSource[vertex] = { ...points[i], face, faces: undefined };\n\t\t\t});\n\n\t\t\t// face indices were heavily modified, update the face map\n\t\t\tfaceMap = map === undefined ? faceMap : mergeNextmaps(faceMap, map);\n\t\t\toldFaceNewEdge[face] = newEdgeIndex;\n\t\t});\n\n\t// these were set to the old face indices and need updating\n\tverticesSource.forEach(({ face }, i) => {\n\t\tif (face !== undefined) { verticesSource[i].faces = faceMap[face]; }\n\t});\n\n\t// each edge's new faces, found in faceMap[face], will contain two faces\n\t// whenever a segment fully crosses a face and splits it into two. \"faces\"\n\t// will contain only one element if a segment was used as input to this method\n\t// and a leaf edge was added to a face, not fully splitting it into two.\n\tedgesSource.forEach(({ face }, i) => {\n\t\tif (face !== undefined) { edgesSource[i].faces = faceMap[face]; }\n\t});\n\n\t// // using the overlapped vertices, make a list of edges collinear to the line\n\t// // these (old) indices will match with the graph from its original state.\n\t// const verticesCollinear = intersections.vertices.map(v => v !== undefined);\n\t// const edges_collinear = graph.edges_vertices\n\t// \t.map(verts => verticesCollinear[verts[0]] && verticesCollinear[verts[1]]);\n\n\t// // these are new edge indices, relating to the graph after modification.\n\t// const collinearEdges = edges_collinear\n\t// \t.map((collinear, e) => (collinear ? e : undefined))\n\t// \t.filter(a => a !== undefined)\n\t// \t.flatMap(edge => (edgeMap[edge] || []));\n\n\treturn {\n\t\tvertices: {\n\t\t\tintersect: intersections.vertices,\n\t\t\tsource: verticesSource,\n\t\t},\n\t\tedges: {\n\t\t\tintersect: intersections.edges,\n\t\t\tnew: Object.values(oldFaceNewEdge),\n\t\t\tmap: edgeMap,\n\t\t\tsource: edgesSource,\n\t\t\t// collinear: collinearEdges,\n\t\t},\n\t\tfaces: {\n\t\t\tintersect: intersections.faces,\n\t\t\tmap: faceMap,\n\t\t},\n\t};\n};\n\n/**\n * @description Split a graph with a line, modifying the graph in place,\n * returning an object describing the changes to the components.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {VecLine2} line a splitting line\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SplitGraphEvent} an object describing the changes\n */\nexport const splitGraphWithLine = (graph, line, epsilon = EPSILON) => (\n\tsplitGraphWithLineAndPoints(\n\t\tgraph,\n\t\tline,\n\t\tincludeL,\n\t\t[],\n\t\tepsilon,\n\t));\n\n/**\n * @description Split a graph with a ray, modifying the graph in place,\n * returning an object describing the changes to the components.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {VecLine2} ray a splitting ray\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SplitGraphEvent} an object describing the changes\n */\nexport const splitGraphWithRay = (graph, ray, epsilon = EPSILON) => (\n\tsplitGraphWithLineAndPoints(\n\t\tgraph,\n\t\tray,\n\t\tincludeR,\n\t\t[ray.origin],\n\t\tepsilon,\n\t));\n\n/**\n * @description Split a graph with a segment, modifying the graph in place,\n * returning an object describing the changes to the components.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number][]} segment a pair of points forming a line segment\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SplitGraphEvent} an object describing the changes\n */\nexport const splitGraphWithSegment = (graph, segment, epsilon = EPSILON) => (\n\tsplitGraphWithLineAndPoints(\n\t\tgraph,\n\t\tpointsToLine2(segment[0], segment[1]),\n\t\tincludeS,\n\t\tsegment,\n\t\tepsilon,\n\t));\n"
  },
  {
    "path": "src/graph/split/splitLine.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tincludeL,\n} from \"../../math/compare.js\";\nimport {\n\tremapKey,\n} from \"../maps.js\";\nimport {\n\tintersectLineAndPoints,\n} from \"../intersect.js\";\n\n/**\n * @description\n * @param {FOLD} graph a fold object in creasePattern or foldedForm\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} [lineDomain=() => true] the function which characterizes\n * \"line\" parameter into a line, ray, or segment.\n * @param {[number, number][]} [interiorPoints=[]] in the case of a ray or segment,\n * place in here the endpoint(s), and they will be included in the result.\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\nexport const splitLineToSegments = (\n\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t{ vector, origin },\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tepsilon = EPSILON,\n) => {\n\t// intersect the line with the graph, vertices, edges, and faces info\n\tconst { vertices, edges, faces } = intersectLineAndPoints(\n\t\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t\t{ vector, origin },\n\t\tlineDomain,\n\t\tinteriorPoints,\n\t\tepsilon,\n\t);\n\n\t// sum the number of all these intersection events inside of this face.\n\t// any face that contains only 2 intersection events is simple enough\n\t// for us to create a new straight edge between the pair of points.\n\t// delete all faces which contain anything other than 2 points.\n\tfaces\n\t\t.map(face => [\"vertices\", \"edges\", \"points\"]\n\t\t\t.map(key => face[key].length)\n\t\t\t.reduce((a, b) => a + b, 0))\n\t\t.map((count, f) => (count !== 2 ? f : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(f => delete faces[f]);\n\n\t// our segments will be a little mini graph, with components:\n\t// - vertices: with any combination of { a, b, t, point, vertex, edge, face}\n\t//   - where vertices from a vertex have { a, point, vertex}\n\t//   - vertices from an edge have { a, b, point, edge }\n\t//   - vertices from inside a face have { t, point, face }\n\t// - edges_vertices: pairs of indices relating to \"vertices\" array.\n\t// - edges_face: for each of these new edges, which face FROM THE INPUT GRAPH\n\t//   does this edge lie inside? For example, this is useful to reference the\n\t//   winding of the face (flipped or no) and give an assignment based on this\n\t/** @type {{ vertices: any[], edges_face: number[], edges_vertices?: any }} */\n\tconst segments = {\n\t\tvertices: [],\n\t\t// because of the previous delete operation on this faces array, we can do this\n\t\tedges_face: faces\n\t\t\t.map((_, face) => face)\n\t\t\t.filter(a => a !== undefined)\n\t};\n\n\t// keep track of any vertices we have already created and added to the graph,\n\t// if one of these exists, use its reference, don't create a duplicate entry.\n\tconst vertexVertex = {};\n\tconst edgeVertex = {};\n\n\tsegments.edges_vertices = faces.map((el, face) => {\n\t\tconst vertsVerts = el.vertices.map(({ a, vertex }) => {\n\t\t\tconst index = segments.vertices.length;\n\t\t\tif (vertexVertex[vertex] !== undefined) { return vertexVertex[vertex]; }\n\t\t\tsegments.vertices.push({ a, vertex, point: [...vertices_coords[vertex]] });\n\t\t\tvertexVertex[vertex] = index;\n\t\t\treturn index;\n\t\t});\n\t\tconst edgesVerts = el.edges.map(({ a, b, point, edge }) => {\n\t\t\tconst index = segments.vertices.length;\n\t\t\tif (edgeVertex[edge] !== undefined) { return edgeVertex[edge]; }\n\t\t\tsegments.vertices.push({ a, b, point, edge });\n\t\t\tedgeVertex[edge] = index;\n\t\t\treturn index;\n\t\t});\n\t\tconst pointsVerts = el.points.map(({ point, t }) => {\n\t\t\tconst index = segments.vertices.length;\n\t\t\tsegments.vertices.push({ point, t, face });\n\t\t\treturn index;\n\t\t});\n\t\t// create an edge_vertices by joining all results from the new vertex\n\t\t// indices from the vertices/edges/points. it will result in only 2.\n\t\treturn vertsVerts.concat(edgesVerts).concat(pointsVerts);\n\t}).filter(a => a !== undefined);\n\n\treturn {\n\t\tvertices, edges, faces, segments,\n\t};\n};\n\n/**\n * @description Given a 2D line and a graph with vertices in 2D, split\n * the line at every vertex/edge, resulting in a graph containing only\n * the new line, prepared as a list of vertices and edges. The indices\n * of the resulting graph are made into arrays with a large hole\n * at the start making sure there is no overlap with the existing graph's\n * indices. This allows the two graphs to very simply be able to be merged.\n * The edge indices will make use of existing vertices when possible, so,\n * edges_vertices may reference vertex indices from the source graph.\n * @param {FOLD} graph a fold object in creasePattern or foldedForm\n * @param {VecLine2} line a line/ray/segment in vector origin form\n * @param {Function} [lineDomain=() => true] the function which characterizes\n * \"line\" parameter into a line, ray, or segment.\n * @param {[number, number][]} [interiorPoints=[]] in the case of a ray or segment,\n * place in here the endpoint(s), and they will be included in the result.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   vertices?: number[],\n *   edges_vertices?: number[][],\n *   edges_collinear?: boolean[],\n *   edges_face?: number[],\n * }} graph a FOLD graph containing only the line's geometry,\n * as a list of vertices and edges, where all new geometry's indices are\n * indexed to start after the input graph's components end, so there is no\n * confusion as to which graph's indices we are referring.\n * - vertices: for each vertex, this is the result of the\n *   intersection algorithm which calculated this vertex's coordinate.\n *   including a \"point\" property on all entries, which is the vertex's coords.\n * - edges_vertices: new edges, as pairs of vertices, can include references\n *   to new vertices or vertices from the input graph.\n * - edges_face: note \"face\" not \"faces\", which face does this new edge lie in\n * - edges_collinear: list of edges from the input graph containing a boolean,\n *   true if the edge lies collinear to the input line.\n */\nexport const splitLineIntoEdges = (\n\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\tline,\n\tlineDomain = includeL,\n\tinteriorPoints = [],\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_coords || !edges_vertices || !faces_vertices) {\n\t\treturn undefined;\n\t}\n\n\t// intersect the line with the graph, vertices, edges, and faces info.\n\tconst { vertices, segments } = splitLineToSegments(\n\t\t{ vertices_coords, edges_vertices, faces_vertices, faces_edges },\n\t\tline,\n\t\tlineDomain,\n\t\tinteriorPoints,\n\t\tepsilon,\n\t);\n\n\t// rename \"vertices\" into any name that includes a \"_\" so that when\n\t// we run \"remapKey\" it will include this array in the remapping.\n\t// treat is as a FOLD type, so we can pass it into remapKey\n\tconst segmentsAsFOLD = {\n\t\t...segments,\n\t\tvertices_info: segments.vertices,\n\t};\n\tdelete segmentsAsFOLD.vertices;\n\n\t// create a map that moves the vertices and edges so that there is a large\n\t// hole at the start of the array and that the indices start after the\n\t// current graph's components. this allows the two arrays to be later merged.\n\t// additionally, vertices will be mapped so that the final vertex list\n\t// excludes those vertices which are pointing to existing vertex indices\n\t// from the input graph; only including vertices which will become new points.\n\tlet count = 0;\n\tconst vertexMap = segmentsAsFOLD.vertices_info\n\t\t.map(el => (el.vertex === undefined\n\t\t\t? vertices_coords.length + (count++)\n\t\t\t: el.vertex));\n\tconst edgeMap = segmentsAsFOLD.edges_vertices\n\t\t.map((_, i) => edges_vertices.length + i);\n\tremapKey(segmentsAsFOLD, \"vertices\", vertexMap);\n\tremapKey(segmentsAsFOLD, \"edges\", edgeMap);\n\n\t// we are done requiring the vertices_ array have an underbar. rename it back\n\tsegmentsAsFOLD.vertices = segmentsAsFOLD.vertices_info;\n\tdelete segmentsAsFOLD.vertices_info;\n\n\t// this also populates the coords for vertices which already exist in the\n\t// input graph, we just need to check the input graph and delete these.\n\tsegmentsAsFOLD.vertices\n\t\t.map((_, v) => v)\n\t\t.filter(v => vertices_coords[v] !== undefined)\n\t\t.forEach(v => delete segmentsAsFOLD.vertices[v]);\n\n\t// using the overlapped vertices, make a list of edges collinear to the line\n\tconst verticesCollinear = vertices.map(v => v !== undefined);\n\tconst edges_collinear = edges_vertices\n\t\t.map(verts => verticesCollinear[verts[0]] && verticesCollinear[verts[1]]);\n\n\treturn {\n\t\t...segmentsAsFOLD,\n\t\tedges_collinear,\n\t};\n};\n"
  },
  {
    "path": "src/graph/subgraph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfoldKeys,\n\tfilterKeysWithPrefix,\n\tfilterKeysWithSuffix,\n} from \"../fold/spec.js\";\nimport {\n\tuniqueSortedNumbers,\n} from \"../general/array.js\";\n\n/**\n * @description Given a self-relational array like faces_faces or\n * vertices_vertices, and a list of indices with which to keep,\n * copy the array so that only those elements found in \"indices\"\n * are copied over, including the data in the inner most arrays.\n * This will produce an array with holes, but the inner arrays\n * will have no holes.\n * @param {number[][]} array_array a self-relational index array,\n * such as \"faces_faces\".\n * @param {number[]} indices a list of indices to keep.\n * @returns {number[][]} a copy of the array_array but excluding\n * all indices which were not included in the \"indices\" parameter set.\n */\nexport const selfRelationalArraySubset = (array_array, indices) => {\n\t// quick lookup, is an index to be included?\n\tconst hash = {};\n\tindices.forEach(f => { hash[f] = true; });\n\n\t// only include those faces which are in the group, both at\n\t// the top level and inside the inside reference arrays.\n\tconst result = [];\n\tindices.forEach(i => {\n\t\tresult[i] = array_array[i].filter(j => hash[j]);\n\t});\n\treturn result;\n};\n\n/**\n * @description Create a subgraph from a graph, with shallow pointers\n * to arrays by providing a list of vertices, edges, and faces which\n * will be carried over.\n * This subgraph method is exclusive, meaning, if an edge is carried\n * over but its vertices are not explicitly included, those vertices\n * will not exist, and the edge's edges_vertices entry will be empty.\n * The subgraph component arrays will contain holes, meaning the\n * indices are preserved, making it useful for performing operations\n * on a subgraph, then carrying that information back to the original.\n * If you want to close all holes and remap indices, call normalize().\n * @param {FOLD} graph a FOLD object\n * @param {object} indicesToKeep an object containing:\n * { vertices: [], edges: [], faces: [] }\n * all of which contains a list of indices to keep in the copied graph.\n * the values can be integers or integer-strings, doesn't matter.\n * @returns {FOLD} a shallow copy of the graph parameter provided.\n */\nexport const subgraphExclusive = (graph, indicesToKeep = {}) => {\n\tconst indices = {\n\t\tvertices: [],\n\t\tedges: [],\n\t\tfaces: [],\n\t\t...indicesToKeep,\n\t};\n\tconst components = Object.keys(indices);\n\t// shallow copy, excluding all geometry arrays, to preserve metadata.\n\tconst copy = { ...graph };\n\tfoldKeys.graph.forEach(key => delete copy[key]);\n\tdelete copy.file_frames;\n\t// create a lookup which will be used when a component is a suffix\n\t// and we need to filter out elements which don't appear in other arrays\n\tconst lookup = {};\n\tcomponents.forEach(component => { lookup[component] = {}; });\n\tcomponents.forEach(component => indices[component].forEach(i => {\n\t\tlookup[component][i] = true;\n\t}));\n\t// get all prefix arrays (\"edges_\") and suffix arrays (\"_edges\")\n\t// for all geometry component type.\n\tconst keys = {};\n\tcomponents.forEach(c => {\n\t\tfilterKeysWithPrefix(graph, c).forEach(key => { keys[key] = {}; });\n\t\tfilterKeysWithSuffix(graph, c).forEach(key => { keys[key] = {}; });\n\t});\n\tcomponents.forEach(c => {\n\t\tfilterKeysWithPrefix(graph, c).forEach(key => { keys[key].prefix = c; });\n\t\tfilterKeysWithSuffix(graph, c).forEach(key => { keys[key].suffix = c; });\n\t});\n\t// use prefixes and suffixes to make sure we initialize all\n\t// geometry array types. this even supports out of spec arrays,\n\t// like: faces_matrix, colors_edges...\n\tObject.keys(keys).forEach(key => { copy[key] = []; });\n\tObject.keys(keys).forEach(key => {\n\t\tconst { prefix, suffix } = keys[key];\n\t\t// if prefix exists, filter outer array elements (creating holes)\n\t\t// if suffix exists, filter inner elements using the quick lookup\n\t\tif (prefix && suffix) {\n\t\t\tindices[prefix].forEach(i => {\n\t\t\t\tcopy[key][i] = graph[key][i].filter(j => lookup[suffix][j]);\n\t\t\t});\n\t\t} else if (prefix) {\n\t\t\tindices[prefix].forEach(i => { copy[key][i] = graph[key][i]; });\n\t\t} else if (suffix) {\n\t\t\tcopy[key] = graph[key].map(arr => arr.filter(j => lookup[suffix][j]));\n\t\t} else {\n\t\t\tcopy[key] = graph[key];\n\t\t}\n\t});\n\treturn copy;\n};\n\n/**\n * @description Create a subgraph from a graph, with shallow pointers\n * to arrays by providing a list of vertices, edges, and faces which\n * will be carried over.\n * This subgraph method is inclusive, meaning, if an edge or a face\n * is included, even if the user didn't explicitly include its vertices,\n * they will be included anyway.\n * The subgraph component arrays will contain holes, meaning the\n * indices are preserved, making it useful for performing operations\n * on a subgraph, then carrying that information back to the original.\n * If you want to close all holes and remap indices, call normalize().\n * @param {FOLD} graph a FOLD object\n * @param {object} indicesToKeep an object containing:\n * { vertices: [], edges: [], faces: [] }\n * all of which contains a list of indices to keep in the copied graph.\n * the values can be integers or integer-strings, doesn't matter.\n * @returns {FOLD} a shallow copy of the graph parameter provided.\n */\nexport const subgraph = (graph, indicesToKeep = {}) => {\n\tconst indices = {\n\t\tvertices: [],\n\t\tedges: [],\n\t\tfaces: [],\n\t\t...indicesToKeep,\n\t};\n\tconst lookup = { vertices: {}, edges: {}, faces: {} };\n\t// add vertices to the lookup table, all vertices from\n\t// vertices, edges' edges_vertices, and faces' faces_vertices.\n\tindices.vertices.forEach(v => { lookup.vertices[v] = true; });\n\tindices.edges.forEach(e => { lookup.edges[e] = true; });\n\tindices.edges\n\t\t.forEach(edge => graph.edges_vertices[edge]\n\t\t\t.forEach(v => { lookup.vertices[v] = true; }));\n\tindices.faces.forEach(f => { lookup.faces[f] = true; });\n\tindices.faces\n\t\t.forEach(face => graph.faces_vertices[face]\n\t\t\t.forEach(v => { lookup.vertices[v] = true; }));\n\t// now, our selection of vertices is complete.\n\t// use this to check all edges and faces, and if the full set of\n\t// the element's vertices are included, include it as well.\n\tgraph.faces_vertices\n\t\t.map((_, f) => f)\n\t\t.filter(f => graph.faces_vertices[f]\n\t\t\t.map(v => lookup.vertices[v])\n\t\t\t.reduce((a, b) => a && b, true))\n\t\t.forEach(f => { lookup.faces[f] = true; });\n\tgraph.edges_vertices\n\t\t.map((_, e) => e)\n\t\t.filter(e => graph.edges_vertices[e]\n\t\t\t.map(v => lookup.vertices[v])\n\t\t\t.reduce((a, b) => a && b, true))\n\t\t.forEach(e => { lookup.edges[e] = true; });\n\treturn subgraphExclusive(graph, {\n\t\tvertices: Object.keys(lookup.vertices),\n\t\tedges: Object.keys(lookup.edges),\n\t\tfaces: Object.keys(lookup.faces),\n\t});\n};\n\n/**\n * @description Create a subgraph from a graph, with shallow pointers\n * to arrays by providing a list of faces which will be carried over,\n * and this list of faces will determine which vertices and edges\n * get carried over as well.\n * The subgraph component arrays will contain holes, meaning the\n * indices are preserved, making it useful for performing operations\n * on a subgraph, then carrying that information back to the original.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} faces a list of face indices which will\n * be carried over into the subgraph.\n * @returns {FOLD} a shallow copy of the graph parameter provided.\n */\nexport const subgraphWithFaces = (graph, faces) => {\n\t// vertices will be take from one place:\n\t// - faces_vertices, every vertex involved in the subset of faces\n\t// there is no way to take it from edges_vertices, as edges_vertices\n\t// itself needs to be determined by this \"vertices\" array.\n\tlet vertices = [];\n\tif (graph.faces_vertices) {\n\t\tvertices = uniqueSortedNumbers(\n\t\t\tfaces.flatMap(f => graph.faces_vertices[f]),\n\t\t);\n\t}\n\t// edges will be taken from one of two places, either:\n\t// - faces edges, the edges involved in the subset of faces\n\t// - edges_vertices where BOTH vertices involved are in \"vertices\".\n\t// otherwise, no edges will be carried over.\n\tlet edges = [];\n\tif (graph.faces_edges) {\n\t\tedges = uniqueSortedNumbers(\n\t\t\tfaces.flatMap(f => graph.faces_edges[f]),\n\t\t);\n\t} else if (graph.edges_vertices) {\n\t\tconst vertices_lookup = {};\n\t\tvertices.forEach(v => { vertices_lookup[v] = true; });\n\t\tedges = graph.edges_vertices\n\t\t\t.map((v, i) => (vertices_lookup[v[0]] && vertices_lookup[v[1]]\n\t\t\t\t? i\n\t\t\t\t: undefined))\n\t\t\t.filter(a => a !== undefined);\n\t}\n\treturn subgraphExclusive(graph, {\n\t\tvertices,\n\t\tedges,\n\t\tfaces,\n\t});\n};\n\nexport const subgraphWithVertices = (graph, vertices = []) => {\n\t// these will be in the form of index:value number:boolean.\n\t// later to be converted into a list of indices.\n\tconst components = { vertices: [], edges: [] };\n\tvertices.forEach(v => { components.vertices[v] = true; });\n\tif (graph.vertices_edges) {\n\t\tcomponents.vertices\n\t\t\t.forEach((_, v) => graph.vertices_edges[v]\n\t\t\t\t.forEach(e => { components.edges[e] = true; }));\n\t}\n\tif (graph.edges_vertices) {\n\t\tcomponents.edges\n\t\t\t.forEach((_, e) => graph.edges_vertices[e]\n\t\t\t\t.forEach(v => { components.vertices[v] = true; }));\n\t}\n\treturn subgraphExclusive(graph, {\n\t\tvertices: components.vertices\n\t\t\t.map((v, i) => (v ? i : undefined))\n\t\t\t.filter(a => a !== undefined),\n\t\tedges: components.edges\n\t\t\t.map((e, i) => (e ? i : undefined))\n\t\t\t.filter(a => a !== undefined),\n\t});\n};\n"
  },
  {
    "path": "src/graph/sweep.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tepsilonEqual,\n} from \"../math/compare.js\";\nimport {\n\tuniqueElements,\n} from \"../general/array.js\";\nimport {\n\tclusterScalars,\n} from \"../general/cluster.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"./make/verticesEdges.js\";\n// import { invertArrayToFlatMap } from \"./maps.js\";\n\n/**\n * @description convert a faces_vertices into an edge-style list, where\n * each face only has two vertices, the min and max vertex along the\n * spanning axis. This is useful for projecting faces down to an axis.\n */\nconst edgeifyFaces = ({ vertices_coords, faces_vertices }, axis = 0) => (\n\tfaces_vertices.map(vertices => [\n\t\tvertices.reduce((a, b) => (vertices_coords[a][axis] < vertices_coords[b][axis]\n\t\t\t? a\n\t\t\t: b)),\n\t\tvertices.reduce((a, b) => (vertices_coords[a][axis] > vertices_coords[b][axis]\n\t\t\t? a\n\t\t\t: b)),\n\t]));\n\n/**\n * @description Perform a line sweep through the vertices of a graph,\n * the default direction is to sweep along the +X axis.\n * This method will sort the vertices along the sweep direction and\n * group those which have a similar value within an epsilon.\n * This is not interchangeable with getVerticesClusters, this only clusters\n * vertices along one axis, it does not make vertices clusters.\n * @param {FOLD} graph a FOLD object\n * @param {number} [axis=0] which axis to sweep along\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ t: number, vertices: number[] }[]} an array of event objects,\n * each event contains:\n * - t: the position along the axis\n * - vertices: the vertices (one or more) at this event along the axis\n */\nexport const sweepVertices = (\n\t{ vertices_coords },\n\taxis = 0,\n\tepsilon = EPSILON,\n) => clusterScalars(vertices_coords.map(p => p[axis]), epsilon)\n\t.map(vertices => ({\n\t\tvertices,\n\t\tt: vertices.reduce((p, c) => p + vertices_coords[c][axis], 0) / vertices.length,\n\t}));\n\n/**\n * @description This is the sweep method used by sweepEdges and sweepFaces.\n * note: sweepFaces constructs new edges that span the entire\n * face, so this has no relation to the graph's original edges_vertices.\n * @param {FOLD} graph FOLD format graph with edges_vertices defining\n * connections between indices in the \"values\" array.\n * @param {number[]} values an array of scalars constructed from the\n * vertices_coords array where for each coord, only the relevant value is taken\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SweepEvent[]} an array of sweep line events, indicating at every\n * event a list of values which start, end, or are currently overlapping.\n */\nexport const sweepValues = (\n\t{ edges_vertices, vertices_edges },\n\tvalues,\n\tepsilon = EPSILON,\n) => {\n\tif (!vertices_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst edgesValues = edges_vertices.map(edge => edge.map(e => values[e]));\n\n\t// is the edge degenerate along the sweep axis?\n\t// (both values are an epsilon away from each other)\n\tconst isDegenerate = edgesValues.map(([a, b]) => epsilonEqual(a, b, epsilon));\n\n\t// for each edge, which of its two vertices is first along the sweep axis.\n\t// this does not handle the degenerate case, the upcoming object will\n\t// combine this and the degenerate information into a more complete version.\n\tconst edgesDirection = edgesValues.map(([a, b]) => Math.sign(a - b));\n\n\t// for each edge, for each of its two vertices, what is the ordering of\n\t// each of its vertices, where each vertex is stored as:\n\t// -1: first point's coordinate is smaller\n\t//  0: coordinates are same (within an epsilon)\n\t// +1: second point's coordinate is smaller\n\tconst edgesVertexSide = edges_vertices\n\t\t.map(([v1, v2], i) => (isDegenerate[i]\n\t\t\t? { [v1]: 0, [v2]: 0 }\n\t\t\t: { [v1]: edgesDirection[i], [v2]: -edgesDirection[i] }));\n\n\t// within each cluster, if there are a lot of consecutive lines orthogonal\n\t// to the sweep axis, there will be repeats of edges when converting these\n\t// vertices into their adjacent edges by looking at vertices_edges.\n\t// we have to pile these into a Set and extract an array of unique values.\n\treturn clusterScalars(values, epsilon)\n\t\t// ensure that every vertex is used in the graph, filter out any vertices\n\t\t// which aren't found in vertices_edges, and if this creates empty clusters\n\t\t// with no vertices, filter out these clusters\n\t\t.map(vertices => vertices.filter(v => vertices_edges[v]))\n\t\t.filter(vertices => vertices.length)\n\t\t.map(vertices => ({\n\t\t\tvertices,\n\t\t\t// values will be equivalent within an epsilon, we can choose to either\n\t\t\t// grab just one, grab the mode value, or average them all together.\n\t\t\tt: vertices.reduce((p, c) => p + values[c], 0) / vertices.length,\n\t\t\tstart: uniqueElements(vertices.flatMap(v => vertices_edges[v]\n\t\t\t\t.filter(edge => edgesVertexSide[edge][v] <= 0))),\n\t\t\tend: uniqueElements(vertices.flatMap(v => vertices_edges[v]\n\t\t\t\t.filter(edge => edgesVertexSide[edge][v] >= 0))),\n\t\t}));\n};\n\n/**\n * @description Perform a line sweep through the edges of a graph,\n * This method will create an array of events, each event will either\n * \"start\" edges or \"end\" edges, and for those edges which are orthogonal\n * to the sweep axis within an epsilon, they will only exist inside one\n * event and be present in both the \"start\" and the \"end\" arrays.\n * @param {FOLD} graph a FOLD object\n * @param {number} [axis=0] which axis to sweep along\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SweepEvent[]} an array of event objects, each event contains:\n * - t: the position along the axis\n * - vertices: the vertices (one or more) at this event along the axis\n * - start: the edges which begin at this event in the sweep\n * - end: the edges which end at this event in the sweep\n */\nexport const sweepEdges = (\n\t{ vertices_coords, edges_vertices, vertices_edges },\n\taxis = 0,\n\tepsilon = EPSILON,\n) => sweepValues(\n\t{ edges_vertices, vertices_edges },\n\tvertices_coords.map(p => p[axis]),\n\tepsilon,\n);\n\n/**\n * @description Perform a line sweep through the faces of a graph,\n * This method will create an array of events, each event will either\n * \"start\" faces or \"end\" faces. In the case of degenerate faces whose\n * vertices all lie on a line and that line is orthogonal to the sweep\n * axis, within an epsilon, these faces will only exist inside one\n * event and be present in both the \"start\" and the \"end\" arrays.\n * @param {FOLD} graph a FOLD object\n * @param {number} [axis=0] which axis to sweep along\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {SweepEvent[]} an array of event objects, each event contains:\n * - t: the position along the axis\n * - vertices: the vertices (one or more) at this event along the axis\n * - start: the faces which begin at this event in the sweep\n * - end: the faces which end at this event in the sweep\n */\nexport const sweepFaces = (\n\t{ vertices_coords, faces_vertices },\n\taxis = 0,\n\tepsilon = EPSILON,\n) => sweepValues(\n\t// get the min/max vertex for each face along the sweep axis.\n\t// this will serve as the \"edge\" that spans the breadth of the face\n\t{ edges_vertices: edgeifyFaces({ vertices_coords, faces_vertices }, axis) },\n\tvertices_coords.map(p => p[axis]),\n\tepsilon,\n);\n\n/**\n * @description Perform a line sweep through all components of a graph,\n * This method will create an array of events, each event occurs at\n * a vertex (or vertices), and each event will either \"start\" or \"end\"\n * edges and faces. In the case of degenerate edges or faces whose\n * vertices all lie on a line orthogonal to the sweep axis within an epsilon,\n * These components will only exist inside one event and be present in both\n * \"start\" and \"end\" arrays.\n * @param {FOLD} graph a FOLD object\n * @param {number} [axis=0] which axis to sweep along\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   vertices: number[],\n *   t: number,\n *   edges: { start: number[], end: number[] },\n *   faces: { start: number[], end: number[] },\n * }[]} an array of event objects, each event contains:\n * - t: the position along the axis\n * - vertices: the vertices (one or more) at this event along the axis\n * - edges, faces: where each contains:\n *   - start: the index which begin at this event in the sweep\n *   - end: the index which end at this event in the sweep\n */\nexport const sweep = ({\n\tvertices_coords, edges_vertices, faces_vertices,\n}, axis = 0, epsilon = EPSILON) => {\n\tconst values = vertices_coords.map(p => p[axis]);\n\t// \"faces_vertices\" contains only two vertices for each face,\n\t// and can be treated just like edges_vertices.\n\tconst faces_edgeVertices = edgeifyFaces({ vertices_coords, faces_vertices }, axis);\n\tconst vertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\tconst vertices_faces = makeVerticesEdgesUnsorted({ edges_vertices: faces_edgeVertices });\n\tconst edgesValues = edges_vertices.map(edge => edge.map(e => values[e]));\n\tconst facesValues = faces_edgeVertices.map(face => face.map(e => values[e]));\n\t// is the span degenerate? (both values are an epsilon away from each other)\n\tconst edgesDegenerate = edgesValues.map(([a, b]) => epsilonEqual(a, b, epsilon));\n\tconst facesDegenerate = facesValues.map(([a, b]) => epsilonEqual(a, b, epsilon));\n\t// which vertex comes first along the sweep axis\n\tconst edgesDirection = edgesValues.map(([a, b]) => Math.sign(a - b));\n\tconst facesDirection = facesValues.map(([a, b]) => Math.sign(a - b));\n\t// for each edge, for each of its two vertices, is the vertex +1, 0, -1?\n\t// -1: first point's coordinate is smaller\n\t//  0: coordinates are same (within an epsilon)\n\t// +1: second point's coordinate is smaller\n\tconst edgesVertexSide = edges_vertices\n\t\t.map(([v1, v2], i) => (edgesDegenerate[i]\n\t\t\t? { [v1]: 0, [v2]: 0 }\n\t\t\t: { [v1]: edgesDirection[i], [v2]: -edgesDirection[i] }));\n\tconst facesVertexSide = faces_vertices\n\t\t.map(([v1, v2], i) => (facesDegenerate[i]\n\t\t\t? { [v1]: 0, [v2]: 0 }\n\t\t\t: { [v1]: facesDirection[i], [v2]: -facesDirection[i] }));\n\t// within each cluster, if there are a lot of consecutive lines orthogonal\n\t// to the sweep axis, there will be repeats of edges when converting these\n\t// vertices into their adjacent edges by looking at vertices_edges.\n\t// we have to pile these into a Set and extract an array of unique values.\n\treturn clusterScalars(values, epsilon)\n\t\t.map(vertices => ({\n\t\t\tvertices,\n\t\t\tt: vertices.reduce((p, c) => p + values[c], 0) / vertices.length,\n\t\t\tedges: {\n\t\t\t\tstart: uniqueElements(vertices\n\t\t\t\t\t.filter(v => vertices_edges[v] !== undefined)\n\t\t\t\t\t.flatMap(v => vertices_edges[v]\n\t\t\t\t\t\t.filter(edge => edgesVertexSide[edge][v] <= 0))),\n\t\t\t\tend: uniqueElements(vertices\n\t\t\t\t\t.filter(v => vertices_edges[v] !== undefined)\n\t\t\t\t\t.flatMap(v => vertices_edges[v]\n\t\t\t\t\t\t.filter(edge => edgesVertexSide[edge][v] >= 0))),\n\t\t\t},\n\t\t\tfaces: {\n\t\t\t\tstart: uniqueElements(vertices\n\t\t\t\t\t.filter(v => vertices_faces[v] !== undefined)\n\t\t\t\t\t.flatMap(v => vertices_faces[v]\n\t\t\t\t\t\t.filter(face => facesVertexSide[face][v] <= 0))),\n\t\t\t\tend: uniqueElements(vertices\n\t\t\t\t\t.filter(v => vertices_faces[v] !== undefined)\n\t\t\t\t\t.flatMap(v => vertices_faces[v]\n\t\t\t\t\t\t.filter(face => facesVertexSide[face][v] >= 0))),\n\t\t\t},\n\t\t}));\n};\n\n/**\n *\n */\n// export const sweepRanges = (ranges, epsilon = EPSILON) => {\n// \tconst values = vertices_coords.map(p => p[axis]);\n\n// };\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} axis the direction of movement the sweep line sweeps\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\n// export const edgeSweep = (\n// \t{ vertices_coords, edges_vertices, vertices_edges },\n// \taxis = 0,\n// \tepsilon = EPSILON,\n// ) => {\n// \tif (!vertices_edges) {\n// \t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n// \t}\n\n// \t// let's call the \"sweep axis\" the direction that the sweep line travels.\n// \t// let's call the \"cross axis\" the direction of the sweep line itself.\n// \tconst crossAxis = axis === 0 ? 1 : 0;\n\n// \t// these are the values of the vertices_coords along the sweep axis\n// \tconst sweepValues = vertices_coords.map(p => p[axis]);\n// \tconst crossValues = vertices_coords.map(p => p[crossAxis]);\n\n// \t// cluster the vertices along the sweep axis\n// \tconst clusters_vertices = clusterScalars(sweepValues, epsilon)\n// \t\t.map(vertices => vertices.sort((a, b) => crossValues[a] - crossValues[b]));\n// \tconst vertices_cluster = invertArrayToFlatMap(clusters_vertices);\n\n// \tconsole.log(\"clusters_vertices\", clusters_vertices);\n// \tconsole.log(\"vertices_cluster\", vertices_cluster);\n\n// \tconst edgesValues = edges_vertices.map(edge => edge.map(e => sweepValues[e]));\n// \t// is the span degenerate along the sweep axis (both endpoints similar)\n// \tconst edgesDegenerate = edgesValues.map(([a, b]) => epsilonEqual(a, b, epsilon));\n// \t// which vertex comes first along the sweep axis\n// \tconst edgesDirection = edgesValues.map(([a, b]) => Math.sign(a - b));\n\n// \t// for each edge, for each of its two vertices, is the vertex +1, 0, -1?\n// \t// -1: first point's coordinate is smaller\n// \t//  0: coordinates are same (within an epsilon)\n// \t// +1: second point's coordinate is smaller\n// \tconst edgesVertexSide = edges_vertices\n// \t\t.map(([v1, v2], i) => (edgesDegenerate[i]\n// \t\t\t? { [v1]: 0, [v2]: 0 }\n// \t\t\t: { [v1]: edgesDirection[i], [v2]: -edgesDirection[i] }));\n\n// \t// build an initial ordering of the edges along the cross axis.\n// \t// const set0Verts = clusters_vertices[0];\n// \tconst clusters_vertices_edges = clusters_vertices\n// \t\t.map(vertices => vertices.map(v => vertices_edges[v]));\n\n// \tconsole.log(\"clusters_vertices_edges\", clusters_vertices_edges);\n\n// \tconst edgeSeen = {};\n// \tconst clusterEdgeSpan = clusters_vertices_edges.map(cluster => {\n// \t\tconst start = {};\n// \t\tconst end = {};\n// \t\tcluster.forEach((edges, i) => edges.forEach(edge => {\n// \t\t\tif (!edgeSeen[edge]) {\n// \t\t\t\tstart[edge] = i;\n// \t\t\t\tedgeSeen[edge] = true;\n// \t\t\t} else {\n// \t\t\t\tend[edge] = i;\n// \t\t\t\tdelete edgeSeen[edge];\n// \t\t\t}\n// \t\t}));\n// \t\treturn { start, end };\n// \t});\n\n// \tconsole.log(\"clusterEdgeSpan\", clusterEdgeSpan);\n\n// \t// const fencepost = Array.from(Array(clusters_vertices_edges.length - 1))\n// \t// \t.map((_, i) => );\n\n// \tclusters_vertices.forEach(vertices => {\n// \t\t// const edges = vertices.map(v => vertices_edges[v]);\n// \t\tconst start = uniqueElements(vertices\n// \t\t\t.filter(v => vertices_edges[v] !== undefined)\n// \t\t\t.flatMap(v => vertices_edges[v]\n// \t\t\t\t.filter(edge => edgesVertexSide[edge][v] <= 0)));\n// \t\tconst end = uniqueElements(vertices\n// \t\t\t.filter(v => vertices_edges[v] !== undefined)\n// \t\t\t.flatMap(v => vertices_edges[v]\n// \t\t\t\t.filter(edge => edgesVertexSide[edge][v] >= 0)));\n// \t});\n\n// \tconsole.log(\"edgesVertexSide\", edgesVertexSide);\n// \t// within each cluster, if there are a lot of consecutive lines orthogonal\n// \t// to the sweep axis, there will be repeats of edges when converting these\n// \t// vertices into their adjacent edges by looking at vertices_edges.\n// \t// we have to pile these into a Set and extract an array of unique sweepValues.\n// \treturn clusters_vertices\n// \t\t.map(vertices => ({\n// \t\t\tvertices,\n// \t\t\tt: vertices.reduce((p, c) => p + sweepValues[c], 0) / vertices.length,\n// \t\t\tstart: uniqueElements(vertices\n// \t\t\t\t.filter(v => vertices_edges[v] !== undefined)\n// \t\t\t\t.flatMap(v => vertices_edges[v]\n// \t\t\t\t\t.filter(edge => edgesVertexSide[edge][v] <= 0))),\n// \t\t\tend: uniqueElements(vertices\n// \t\t\t\t.filter(v => vertices_edges[v] !== undefined)\n// \t\t\t\t.flatMap(v => vertices_edges[v]\n// \t\t\t\t\t.filter(edge => edgesVertexSide[edge][v] >= 0))),\n// \t\t}));\n// };\n"
  },
  {
    "path": "src/graph/symmetry.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tvecLineToUniqueLine,\n} from \"../math/convert.js\";\nimport {\n\tflip2,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tmultiplyMatrix2Line2,\n\tmakeMatrix2Reflect,\n} from \"../math/matrix2.js\";\nimport {\n\tclusterScalars,\n\tclusterParallelVectors,\n} from \"../general/cluster.js\";\nimport {\n\tgetEdgesLine,\n} from \"./edges/lines.js\";\n\n/**\n * @description Convert a normal-distance line so that the distance\n * is always positive, and the normal is now possible to point anywhere.\n * @param {UniqueLine} line a line in normal-distance parameterization\n * @returns {UniqueLine} a line in normal-distance parameterization\n */\nconst fixLineDirection = ({ normal, distance }) => (distance < 0\n\t? ({ normal: flip2(normal), distance: -distance })\n\t: ({ normal, distance }));\n\n/**\n * @description Discover the lines of symmetry in a 2D FOLD graph.\n * All possible lines will be returned and sorted to put the best candidate\n * for a symmtry line first, with an error value, where 0 is perfect symmetry.\n * This searches by checking the edges in the graph to find a line,\n * if an edge doesn't exist along the line of symmetry, this will fail.\n * @todo we need a way to detect a symmetry line where no edge lies collinear.\n * One approach might be to run Axiom 3 between all pairs of boundary lines\n * (not boundary edges, lines which have an edge that is boundary to cut down),\n * This would solve the issue in all cases that I can imagine.\n * @param {FOLD} graph a FOLD object with 2D vertices\n * @returns {{ line: VecLine2, error: number }[]} array of symmetry lines\n */\nexport const findSymmetryLines = (graph, epsilon = EPSILON) => {\n\t// get a list of lines that cover all edges of the graph\n\t/** @type {VecLine2[]} */\n\tconst lines = getEdgesLine(graph, epsilon).lines.map(({ vector, origin }) => ({\n\t\tvector: resize2(vector),\n\t\torigin: resize2(origin),\n\t}));\n\n\t// convert the lines into normal-distance parameterization,\n\t// where the distance value is always positive. this will be used when\n\t// we create reflections and need to compare and cluster similar lines\n\tconst uniqueLines = lines.map(vecLineToUniqueLine).map(fixLineDirection);\n\n\t// using reflection matrices, for every line, perform a reflection across\n\t// this line for every other line in the list. every line will get reflected\n\t// N-1 times. this is quite an expensive operation.\n\tconst linesMatrices = lines\n\t\t.map(({ vector, origin }) => makeMatrix2Reflect(vector, origin));\n\tconst reflectionsLines = linesMatrices\n\t\t.map(matrix => lines\n\t\t\t.map(line => multiplyMatrix2Line2(matrix, line)));\n\n\t// convert the reflected lines into normal-distance parameterization, and\n\t// for every reflection group, superimpose (concat) our original list of\n\t// lines that cover the non-transformed set of edges.\n\tconst reflectionsUniqueLines = reflectionsLines\n\t\t.map(group => group.map(line => (line.vector[0] < 0\n\t\t\t? ({ vector: flip2(line.vector), origin: line.origin })\n\t\t\t: line)))\n\t\t.map(group => group.map(vecLineToUniqueLine).map(fixLineDirection))\n\t\t.map(group => group.concat(uniqueLines));\n\n\t// for every reflection set, we now also have in the set the original lines.\n\t// being as resourceful as possible, compare all lines with each other and\n\t// group similar lines together, resulting in clusters of 1 (no match) or 2. (i think)\n\t// First, cluster lines by similar distance from origin.\n\tconst groupsClusters = reflectionsUniqueLines\n\t\t.map(group => clusterScalars(group.map(el => el.distance)));\n\n\t// Second, within each cluster, cluster again by similar (parallel) vectors.\n\t// this creates a cluster where the indices are the indices of elements\n\t// according to the first cluster variable's arrays, not the original data.\n\tconst groupsClusterClustersUnindexed = groupsClusters\n\t\t.map((clusters, g) => clusters\n\t\t\t.map(cluster => cluster.map(i => reflectionsUniqueLines[g][i].normal))\n\t\t\t.map(cluster => clusterParallelVectors(cluster, epsilon)));\n\n\t// fix the mismatch between indices. for every cluster cluster, pass in\n\t// the index from the cluster cluster into the first cluster variable\n\t// to get out the index which now relates to the original input set.\n\t// this now contains, for every reflection line, all clusters of lines and\n\t// their reflection lines, so, the reflection with the smaller number of\n\t// clusters should be the best candidate.\n\tconst groupsClusterClusters = groupsClusterClustersUnindexed\n\t\t.map((group, g) => group\n\t\t\t.flatMap((clusters, c) => clusters\n\t\t\t\t.map(cluster => cluster\n\t\t\t\t\t.map(index => groupsClusters[g][c][index]))));\n\n\t// create some kind of error heuristic, for each reflection group.\n\t// todo: this could be better thought out.\n\t// currently it is, for every reflection line, the number of clusters\n\t// containing only one index (meaning there was no match) divided by\n\t// the total number of lines that were included (reflectionsUniqueLines),\n\t// so that a 0 means all edges are reflections, and 1 means all edges\n\t// have no matching reflection line.\n\t// const groupsError = groupsClusterClusters\n\t// \t.map(group => (group.length - lines.length) / lines.length);\n\n\tconst groupsError = groupsClusterClusters\n\t\t.map(group => group.filter(clusters => clusters.length < 2))\n\t\t.map((noMatchList, i) => noMatchList.length / reflectionsUniqueLines[i].length);\n\n\t// return the data in sorted order, with the best matches for a\n\t// reflection line in the beginning of the list.\n\treturn groupsError\n\t\t.map((error, i) => ({ error, i }))\n\t\t.map(el => ({ line: lines[el.i], error: el.error }))\n\t\t.sort((a, b) => a.error - b.error);\n};\n\n/**\n * @description This method calls findSymmetryLines() and returns the\n * first value only. Use this if you are confident.\n * This searches by checking the edges in the graph to find a line,\n * if an edge doesn't exist along the line of symmetry, this will fail.\n * @param {FOLD} graph a FOLD object with 2D vertices\n * @returns {VecLine2} symmetry lines\n */\nexport const findSymmetryLine = (graph, epsilon = EPSILON) => (\n\t(findSymmetryLines(graph, epsilon)[0] || {}).line\n);\n\n// const bucketVertices = ({ vertices_coords }) => {\n// \tconst size = boundingBox({ vertices_coords }).span;\n// \tconst bucketID = point => point\n// \t\t.map((p, i) => Math.floor((p / size[i]) * 100));\n// \tconst buckets = [];\n// \tvertices_coords\n// \t\t.map(coord => bucketID(coord))\n// \t\t.forEach((id, i) => {\n// \t\t\t// get a pointer to the inner-most array inside \"buckets\"\n// \t\t\tlet bucket = buckets;\n// \t\t\tid.forEach(index => {\n// \t\t\t\tif (!bucket[index]) { bucket[index] = []; }\n// \t\t\t\tbucket = bucket[index];\n// \t\t\t});\n// \t\t\tbucket.push(i);\n// \t\t});\n// \treturn buckets;\n// };\n//\n// export const findSymmetryLines = (graph, epsilon = EPSILON) => {\n// \tconst { lines, edges_line } = getEdgesLine(graph, epsilon);\n// \tconst linesMatrices = lines\n// \t\t.map(({ vector, origin }) => makeMatrix2Reflect(vector, origin));\n// \tconst linesCoordsSide = lines\n// \t\t.map(line => graph.vertices_coords\n// \t\t\t.map(coord => cross2(subtract2(coord, line.origin), line.vector) < 0));\n// \t// for each line, a copy of the vertices_coords only on one side of the line\n// \tconst linesReflections = lines\n// \t\t.map((_, i) => graph.vertices_coords\n// \t\t\t.map((coord, j) => (linesCoordsSide[i][j]\n// \t\t\t\t? coord\n// \t\t\t\t: multiplyMatrix2Vector2(linesMatrices[i], coord))));\n// \tconst linesReflBuckets = linesReflections\n// \t\t.map((coords, i) => coords\n// \t\t\t.filter(point => !overlapLinePoint(lines[i], point)))\n// \t\t.map(vertices_coords => bucketVertices({ vertices_coords }));\n// \tconst lengths = linesReflBuckets\n// \t\t.map(group => group.flatMap(buckets => buckets.map(bucket => bucket.length)))\n// \t\t.map(group => group.map(len => len % 2));\n// \t// values between 0 and 1. lower is better.\n// \t// 0 means every vertex aligns with another after reflection\n// \tconst groupRating = lengths\n// \t\t.map(group => (group.length === 0 ? Infinity : group.reduce((a, b) => a + b, 0)))\n// \t\t.map((sum, i) => sum / linesReflections[i].length)\n// \t\t.map(n => (Number.isNaN(n) ? 1 : n));\n// \tconst validLines = groupRating\n// \t\t.map((rating, i) => ({ rating, i }))\n// \t\t.filter(el => el.rating < 0.8)\n// \t\t.sort((a, b) => a.rating - b.rating)\n// \t\t.map(el => el.i);\n// \t// console.log(\"linesReflBuckets\", linesReflBuckets);\n// \t// console.log(\"lengths\", lengths);\n// \t// console.log(\"groupRating\", groupRating);\n// \t// console.log(\"validLines\", validLines);\n// \treturn validLines.map(i => lines[i]);\n// };\n"
  },
  {
    "path": "src/graph/transfer.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tdistance2,\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tcollinearPoints,\n} from \"../math/line.js\";\nimport {\n\ttrilateration2,\n} from \"../math/triangle.js\";\n\n/**\n * @description Transfer a point from one graph to another, given the\n * two graphs are isomorphic, but contain different vertices coords.\n * Supply a face index which contains the point and the new point's\n * coordinates will be calculated via. trilateration.\n * @param {FOLD} from a fold graph which the point now lies inside\n * @param {FOLD} to a fold graph we want to move the point into\n * @param {number} face the index of the face which contains this point\n * @param {[number, number]} point the point, as it exists inside the \"from\" graph\n * @returns {[number, number]} a new point\n */\nexport const transferPointInFaceBetweenGraphs = (from, to, face, point) => {\n\t// Assuming the graphs are isomorphic except for the vertices_coords,\n\t// we have to account for the possibility that vertices_coords might have\n\t// invalid references or non existent coordinate data.\n\n\t// Filter out any vertices which have non existent coords or are collinear.\n\tconst faceVerticesInitial = to.faces_vertices[face]\n\t\t.filter(v => from.vertices_coords[v] && to.vertices_coords[v]);\n\n\t// Trilateration will fail if the three chosen vertices are collinear.\n\t// For every vertex, check collinearity between prev-this-next vertex.\n\tconst faceVertsInitialCollinear = faceVerticesInitial\n\t\t.map((v, i, arr) => [\n\t\t\tarr[(i + arr.length - 1) % arr.length], v, arr[(i + 1) % arr.length],\n\t\t])\n\t\t.map(verts => {\n\t\t\tconst [a, b, c] = verts.map(v => from.vertices_coords[v]);\n\t\t\tconst [d, e, f] = verts.map(v => to.vertices_coords[v]);\n\t\t\treturn [a, b, c, d, e, f];\n\t\t})\n\t\t.map(([a, b, c, d, e, f]) => collinearPoints(a, b, c) || collinearPoints(d, e, f));\n\n\t// we want only vertices which are not collinear\n\tconst faceVertsValid = faceVerticesInitial\n\t\t.filter((_, i) => !faceVertsInitialCollinear[i]);\n\n\tif (faceVertsValid.length < 3) { return undefined; }\n\t// const faceVertices = resize3(faceVertsValid);\n\tconst [a, b, c] = faceVertsValid;\n\n\t/** @type {[[number, number], [number, number], [number, number]]} */\n\tconst toPoly = [\n\t\tresize2(to.vertices_coords[a]),\n\t\tresize2(to.vertices_coords[b]),\n\t\tresize2(to.vertices_coords[c]),\n\t];\n\n\t/** @type {[number, number, number]} */\n\tconst distances = [\n\t\tdistance2(from.vertices_coords[a], point),\n\t\tdistance2(from.vertices_coords[b], point),\n\t\tdistance2(from.vertices_coords[c], point),\n\t];\n\n\treturn trilateration2(toPoly, distances);\n};\n"
  },
  {
    "path": "src/graph/transform.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tscale as Scale,\n\tscale2 as Scale2,\n\tscale3 as Scale3,\n\tscaleNonUniform,\n\tscaleNonUniform2,\n\tscaleNonUniform3,\n\tadd,\n\tadd2,\n\tadd3,\n\tsubtract2,\n\tsubtract3,\n\tsubtract,\n\tresize2,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tmakeMatrix3Rotate,\n\tmakeMatrix3RotateX,\n\tmakeMatrix3RotateY,\n\tmakeMatrix3RotateZ,\n\tmultiplyMatrix3Vector3,\n} from \"../math/matrix3.js\";\nimport {\n\tboundingBox,\n} from \"../math/polygon.js\";\nimport {\n\tgetDimensionQuick,\n} from \"../fold/spec.js\";\n\n/**\n * @description Alter the vertices by moving the corner of the graph\n * to the origin and shrink or expand the vertices until they\n * aspect fit inside the unit square.\n * @param {FOLD} graph a FOLD object, modified in place\n * @returns {FOLD} the same input graph, modified\n */\nexport const unitize = (graph) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst box = boundingBox(graph.vertices_coords);\n\tconst longest = Math.max(...box.span);\n\tconst scaleAmount = longest === 0 ? 1 : (1 / longest);\n\tconst origin = box.min;\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => subtract(coord, origin))\n\t\t.map(coord => Scale(coord, scaleAmount));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a translation to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} translation a 2D vector\n * @returns {FOLD} the same input graph, modified\n */\nexport const translate2 = (graph, translation) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add2(coord, translation));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a translation to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} translation a 3D vector\n * @returns {FOLD} the same input graph, modified\n */\nexport const translate3 = (graph, translation) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst tr3 = resize3(translation);\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(resize3)\n\t\t.map(coord => add3(coord, tr3));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a translation to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} translation a 3D vector\n * @returns {FOLD} the same input graph, modified\n */\nexport const translate = (graph, translation) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add(coord, translation));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} scaleAmount the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin the\n * homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scaleUniform2 = (graph, scaleAmount = 1, origin = [0, 0]) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add2(Scale2(subtract2(coord, origin), scaleAmount), origin));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} scaleAmount the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin the\n * homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scaleUniform3 = (graph, scaleAmount = 1, origin = [0, 0, 0]) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst origin3 = resize3(origin);\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(resize3)\n\t\t.map(coord => add3(Scale3(subtract3(coord, origin3), scaleAmount), origin3));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} scaleAmount the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin the\n * homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scaleUniform = (graph, scaleAmount = 1, origin = [0, 0, 0]) => {\n\tif (!graph.vertices_coords) { return graph; }\n\tconst origin3 = resize3(origin);\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add(Scale(subtract(coord, origin3), scaleAmount), origin3));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} scaleAmounts\n * the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin\n * the homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scale2 = (graph, scaleAmounts = [1, 1], origin = [0, 0]) => {\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add2(scaleNonUniform2(subtract2(coord, origin), scaleAmounts), origin));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} scaleAmounts\n * the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin\n * the homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scale3 = (graph, scaleAmounts = [1, 1, 1], origin = [0, 0, 0]) => {\n\t/** @type {[number, number, number]} */\n\tconst sc3 = [scaleAmounts[0] || 1, scaleAmounts[1] || 1, scaleAmounts[2] || 1];\n\tconst origin3 = resize3(origin);\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(resize3)\n\t\t.map(coord => add3(scaleNonUniform3(subtract3(coord, origin3), sc3), origin3));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a uniform affine scale to a graph.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {[number, number]|[number, number, number]} scaleAmounts\n * the scale amount to be applied to all dimensions\n * @param {[number, number]|[number, number, number]} origin\n * the homothetic center\n * @returns {FOLD} the same input graph, modified.\n */\nexport const scale = (graph, scaleAmounts = [1, 1, 1], origin = [0, 0, 0]) => {\n\tconst origin3 = resize3(origin);\n\tconst vertices_coords = graph.vertices_coords\n\t\t.map(coord => add(scaleNonUniform(subtract(coord, origin3), scaleAmounts), origin3));\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number[]} matrix\n * @returns {[number, number]|[number, number, number][]} vertices coords transformed and in 3D\n */\nexport const transform = ({ vertices_coords }, matrix) => vertices_coords\n\t.map(resize3)\n\t.map(v => multiplyMatrix3Vector3(matrix, v));\n\n/**\n * @description Apply a rotation to a graph in 3D. This will modify\n * 2D vertices to become 3D.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} angle the rotation amount in radians\n * @param {number[]} vector the axis of rotation\n * @param {number[]} origin the center of rotation\n * @returns {FOLD} the same input graph, modified\n */\nexport const rotate = (graph, angle, vector = [0, 0, 1], origin = [0, 0, 0]) => {\n\tconst matrix = makeMatrix3Rotate(angle, resize3(vector), resize3(origin));\n\tconst vertices_coords = transform(graph, matrix);\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a rotation to a graph around the +X axis.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} angle the rotation amount in radians\n * @param {number[]} origin the center of rotation\n * @returns {FOLD} the same input graph, modified\n */\nexport const rotateX = (graph, angle, origin = [0, 0, 0]) => {\n\tconst matrix = makeMatrix3RotateX(angle, resize3(origin));\n\tconst vertices_coords = transform(graph, matrix);\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a rotation to a graph around the +Y axis.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} angle the rotation amount in radians\n * @param {number[]} origin the center of rotation\n * @returns {FOLD} the same input graph, modified\n */\nexport const rotateY = (graph, angle, origin = [0, 0, 0]) => {\n\tconst matrix = makeMatrix3RotateY(angle, resize3(origin));\n\tconst vertices_coords = transform(graph, matrix);\n\treturn Object.assign(graph, { vertices_coords });\n};\n\n/**\n * @description Apply a rotation to a graph around the +Z axis.\n * This is the only of the three axis rotation methods which can result\n * in 2D vertices (if the input vertices are in 2D).\n * rotateX and rotateY result vertices will remain 3D always.\n * @param {FOLD} graph a FOLD object, modified in place\n * @param {number} angle the rotation amount in radians\n * @param {number[]} origin the center of rotation\n * @returns {FOLD} the same input graph, modified\n */\nexport const rotateZ = (graph, angle, origin = [0, 0, 0]) => {\n\t// get the dimensions of the vertices of the incoming graph\n\tconst resizeFn = getDimensionQuick(graph) === 2 ? resize2 : resize3;\n\tconst matrix = makeMatrix3RotateZ(angle, resize3(origin));\n\t// return vertices_coords to the same dimension as the input graph\n\tconst vertices_coords = transform(graph, matrix).map(coord => resizeFn(coord));\n\treturn Object.assign(graph, { vertices_coords });\n};\n"
  },
  {
    "path": "src/graph/trees.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @typedef BreadthFirstTreeNode\n * @type {{ index: number, parent?: number }}\n * @description A node in a tree specifically intended to map edge-\n * connected faces.\n * @property {number} index - the index of this index\n * @property {number} parent - this node's parent node's index\n */\n\n/**\n * @description Create an array of breadth first minimum spanning trees which\n * fully cover a graph, the input being a self-referencing array, such as\n * vertices_vertices, faces_faces. The tree makes no decision about\n * which path to take, only by choosing the first adjacent index.\n * @param {(number|undefined)[][]} array_array an array of array of indices,\n * indices which are meant to reference this array itself, for example:\n * vertices_vertices or faces_faces or anything following that pattern.\n * @param {number[]} [rootIndices=[]] the indices of faces to become\n * root nodes. In the case of a disjoint graph and multiple trees,\n * the indices at the beginning of the list will be prioritized.\n * @returns {BreadthFirstTreeNode[][][]} an array of trees, where each tree is\n * an array of array of BreadthFirstTreeNode. Each tree is organized into depths,\n * where each array contains an array of tree nodes at that depth.\n */\nexport const minimumSpanningTrees = (array_array = [], rootIndices = []) => {\n\tif (array_array.length === 0) { return []; }\n\tconst trees = [];\n\n\t// this serves two functions: a hash lookup to ensure we aren't using the\n\t// same node twice, and when we need to start a new tree, query from here.\n\t/** @type {{[key: number]: boolean}} */\n\tconst unvisited = {};\n\tarray_array.forEach((_, i) => { unvisited[i] = true; });\n\n\tdo {\n\t\t// pick a starting index. grab the first available (unvisited) item from\n\t\t// the user's root list. if there is none, get the first unvisited index.\n\t\tconst rootIndex = rootIndices.filter(i => unvisited[i]).shift();\n\t\tconst startIndex = rootIndex !== undefined\n\t\t\t? rootIndex\n\t\t\t: parseInt(Object.keys(unvisited).shift(), 10);\n\n\t\t// this also invalidates \"rootIndex\" for any subsequent disjoint sets.\n\t\tdelete unvisited[startIndex];\n\n\t\t// each disjoint set starts a new tree\n\t\tconst tree = [];\n\t\tlet currentLevel = [{ index: startIndex }];\n\n\t\t// breadth first tree traversal. add the current level to the tree,\n\t\t// and iterate through all of the current level's children.\n\t\tdo {\n\t\t\ttree.push(currentLevel);\n\n\t\t\t// for each current level's indices, gather all their children\n\t\t\t// but filter out any which already appeared in a previous level.\n\t\t\tconst nextLevel = currentLevel\n\t\t\t\t.flatMap(current => array_array[current.index]\n\t\t\t\t\t.filter(i => unvisited[i] && i !== null && i !== undefined)\n\t\t\t\t\t.map(index => ({ index, parent: current.index })));\n\n\t\t\t// the above list we just made might contain duplicates.\n\t\t\t// Iterate through the list and mark any duplicates to be removed\n\t\t\t// by also updating the \"unvisited\" list.\n\t\t\t/** @type {{[key: number]: boolean}} */\n\t\t\tconst duplicates = {};\n\t\t\tnextLevel.forEach((el, i) => {\n\t\t\t\tif (!unvisited[el.index]) { duplicates[i] = true; }\n\t\t\t\tdelete unvisited[el.index];\n\t\t\t});\n\n\t\t\t// filter out the next level's duplicates and set this to be \"current\".\n\t\t\t// if this list is empty we are done with the current tree.\n\t\t\tcurrentLevel = nextLevel.filter((_, i) => !duplicates[i]);\n\t\t} while (currentLevel.length);\n\n\t\t// this tree is done, add it to the list of trees\n\t\t// if there are no more unvisited keys, we are done with all trees.\n\t\ttrees.push(tree);\n\t} while (Object.keys(unvisited).length);\n\treturn trees;\n};\n"
  },
  {
    "path": "src/graph/triangulate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../environment/messages.js\";\nimport { countImpliedEdges } from \"./count.js\";\nimport { makeFacesWinding } from \"./faces/winding.js\";\nimport { makeVerticesToEdge, makeVerticesToFace } from \"./make/lookup.js\";\n\n/**\n * @description Convert an array of indices into an array of array of\n * indices where each inner array forms a triangle fan: [0, 1, 2, 3, 4]\n * becomes [[0, 1, 2], [1, 2, 3], [1, 3, 4]].\n * @param {number[]} indices an array of indices\n * @returns {number[][]} an array of arrays where the inner arrays are\n * all of length 3.\n */\nconst makeTriangleFan = (indices) => Array\n\t.from(Array(indices.length - 2))\n\t.map((_, i) => [indices[0], indices[i + 1], indices[i + 2]]);\n\n/**\n * @description Triangulate a faces_vertices with the capability to handle\n * only convex faces. This will increase the number of faces.\n * @param {FOLD} graph a FOLD object.\n * @returns {number[][]} faces_vertices where all faces have only 3 vertices\n */\nexport const triangulateConvexFacesVertices = ({ faces_vertices }) => (\n\tfaces_vertices.flatMap(vertices => (vertices.length < 4\n\t\t? [vertices]\n\t\t: makeTriangleFan(vertices)))\n);\n\n/**\n * @description convert an array of any values into an array of arrays\n * where each of the inner arrays contains 3 elements.\n * This assumes the length % 3 is 0, if not, the couple remainders\n * will be chopped off.\n * @param {any[]} array an array containing any type\n * @returns {any[][]} array of arrays where each inner array is length 3.\n */\nconst groupByThree = (array) => (array.length === 3 ? [array] : Array\n\t.from(Array(Math.floor(array.length / 3)))\n\t.map((_, i) => [i * 3 + 0, i * 3 + 1, i * 3 + 2]\n\t\t.map(j => array[j])));\n\n/**\n * @description Triangulate a faces_vertices with the capability to handle\n * both convex and nonconvex. This will increase the number of faces.\n * You will need to link a reference to the package \"Earcut\" by Mapbox.\n * Earcut is a small and capable library with zero dependencies.\n * https://www.npmjs.com/package/earcut\n * @param {FOLD} graph a FOLD object.\n * @param {any} earcut the earcut npm package from Mapbox\n * @returns {number[][]} faces_vertices where all faces have only 3 vertices\n */\nexport const triangulateNonConvexFacesVertices = (\n\t{ vertices_coords, faces_vertices },\n\tearcut,\n) => {\n\tif (!vertices_coords || !vertices_coords.length) {\n\t\tthrow new Error(Messages.nonConvexTriangulation);\n\t}\n\tconst dimensions = vertices_coords.filter(() => true).shift().length;\n\n\t// Small note on Earcut documentation: Earcut does not truly work in 3D,\n\t// it simply projects points down into 2D and ignores the Z component.\n\t// if we refactor this to include the plane which the face lies in, we can\n\t// filter only if the plane is orthogonal to the XY, therefore still\n\t// use earcut for some 3D faces, or even better, transform those planes\n\t// into the XY so that earcut works for everything.\n\tif (dimensions === 3 || !earcut) {\n\t\treturn triangulateConvexFacesVertices({ faces_vertices });\n\t}\n\n\t// dimensions are 2 from here on\n\t// earcut does not maintain winding order, create a lookup for use later\n\tconst faces_winding = makeFacesWinding({ vertices_coords, faces_vertices });\n\n\treturn faces_vertices\n\t\t.map(fv => fv.flatMap(v => vertices_coords[v]))\n\t\t.map(polygon => earcut(polygon, null, dimensions))\n\t\t// earcut returns vertices [0...n] local to this one polygon\n\t\t// convert these indices back to the face's faces_vertices.\n\t\t.map((vertices, i) => vertices\n\t\t\t.map(j => faces_vertices[i][j]))\n\t\t// finally, earcut does not maintain winding order, before we flatten\n\t\t// the list, reverse the triangles if they come from a face with an\n\t\t// upside-down winding.\n\t\t.flatMap((res, face) => (faces_winding[face]\n\t\t\t? groupByThree(res)\n\t\t\t: groupByThree(res).map(arr => arr.reverse())));\n};\n\n/**\n * @description construct an entirely new set of edges_assignment, potentially\n * without any other edges_ information like edges_vertices. We can do this\n * by looking at the new faces created, because the new faces_vertices and\n * new faces_edges match winding, we can iterate over adjacent pairs of\n * face vertices in the new triangulated face and back-check if they exist\n * in the original faces_vertices list, if they don't use the matching\n * faces_edges edge and update the edges_assignment to be \"J\".\n * @param {FOLD} oldGraph a FOLD object, the old graph\n * @param {FOLD} newGraph a FOLD object, the new graph\n * @returns {string[]} edges_assignment\n */\nconst makeNewEdgesAssignment = (\n\t{ edges_vertices, faces_vertices },\n\t{ faces_vertices: faces_verticesNew, faces_edges: faces_edgesNew },\n) => {\n\tconst edges_assignment = edges_vertices\n\t\t? edges_vertices.map(() => \"U\")\n\t\t: Array.from(Array(countImpliedEdges({ faces_edges: faces_edgesNew })))\n\t\t\t.map(() => \"U\");\n\tconst lookup = makeVerticesToFace({ faces_vertices });\n\tfaces_verticesNew.map((verts, i) => verts\n\t\t.map((v, j, arr) => [v, arr[(j + 1) % arr.length]])\n\t\t.forEach(([v0, v1], j) => {\n\t\t\tconst keys = [`${v0} ${v1}`, `${v1} ${v0}`];\n\t\t\tif (lookup[keys[0]] === undefined && lookup[keys[1]] === undefined) {\n\t\t\t\tconst edge = faces_edgesNew[i][j];\n\t\t\t\tedges_assignment[edge] = \"J\";\n\t\t\t}\n\t\t}));\n\tArray.from(Array(edges_assignment.length))\n\t\t.map((_, i) => i)\n\t\t.filter(i => edges_assignment[i] === undefined)\n\t\t.forEach(i => { edges_assignment[i] = \"U\"; });\n\treturn edges_assignment;\n};\n\n/**\n * @description A subroutine for both convex and non-convex triangulation\n * methods. This will run just after faces_vertices was modified to contain\n * only triangulated faces. This method rebuild faces_edges and\n * add new joined edges edges_vertices, assignment, and foldAngle.\n * @param {FOLD} graph a FOLD object\n * @param {FOLD} graph a FOLD object\n * @returns {FOLD} the same FOLD object as the parameter\n */\nconst rebuildTriangleEdges = (\n\t{ edges_vertices, edges_assignment, edges_foldAngle, faces_vertices },\n\t{ faces_vertices: faces_verticesNew },\n) => {\n\t// if (!edges_vertices) { edges_vertices = []; }\n\tconst edgeLookup = edges_vertices ? makeVerticesToEdge({ edges_vertices }) : {};\n\tlet e = edges_vertices ? edges_vertices.length : 0;\n\t// as we traverse the new faces_edges, if we encounter a new edge, add\n\t// it here in the form of a new edges_vertices\n\n\t// these are additional edges_vertices, to be appended to the current list\n\t/** @type {[number, number][]} */\n\tconst edges_verticesAppended = [];\n\n\t// this is an entirely new list of faces_edges, to replace entirely the old one\n\tconst faces_edgesNew = faces_verticesNew\n\t\t.map(vertices => vertices\n\t\t\t.map((v, i, arr) => {\n\t\t\t\t/** @type {[number, number]} */\n\t\t\t\tconst edge_vertices = [v, arr[(i + 1) % arr.length]];\n\t\t\t\tconst vertexPair = edge_vertices.join(\" \");\n\t\t\t\tif (vertexPair in edgeLookup) { return edgeLookup[vertexPair]; }\n\t\t\t\tedges_verticesAppended.push(edge_vertices);\n\t\t\t\tedgeLookup[vertexPair] = e;\n\t\t\t\tedgeLookup[edge_vertices.slice().reverse().join(\" \")] = e;\n\t\t\t\treturn e++;\n\t\t\t}));\n\n\tconst edges_assignmentNew = edges_assignment\n\t\t? edges_assignment.concat(edges_verticesAppended.map(() => \"J\"))\n\t\t: makeNewEdgesAssignment(\n\t\t\t{ edges_vertices, faces_vertices },\n\t\t\t{ faces_vertices: faces_verticesNew, faces_edges: faces_edgesNew },\n\t\t);\n\n\tconst result = {\n\t\tedges_vertices: edges_vertices\n\t\t\t? edges_vertices.concat(edges_verticesAppended)\n\t\t\t: edges_verticesAppended,\n\t\tfaces_edges: faces_edgesNew,\n\t\tedges_assignment: edges_assignmentNew,\n\t};\n\tif (edges_foldAngle) {\n\t\tconst edges_foldAngleAppended = edges_verticesAppended.map(() => 0);\n\t\tresult.edges_foldAngle = edges_foldAngle.concat(edges_foldAngleAppended);\n\t}\n\treturn result;\n};\n\n/**\n * @description Given a faces_vertices, generate a nextmap which\n * describes how the faces will change after triangulation,\n * specifically by triangulateConvexFacesVertices or\n * triangulateNonConvexFacesVertices.\n * @param {FOLD} graph a FOLD object\n * @returns {number[][]} a nextmap of faces, arrayMap type\n */\nconst makeTriangulatedFacesNextMap = ({ faces_vertices }) => {\n\tlet count = 0;\n\treturn faces_vertices\n\t\t.map(verts => Math.max(3, verts.length))\n\t\t.map(length => Array.from(Array(length - 2)).map(() => count++));\n};\n\n/**\n * @description Modify a fold graph so that all faces are triangles.\n * This will increase the number of faces and edges, and give all\n * new edges a \"J\" join assignment.\n * This method is capable of parsing models with convex faces only without\n * any additional help. If your model contains non-convex faces, this method\n * can still triangulate your model, however, you will need to link\n * a reference to the package \"Earcut\" by Mapbox. Earcut is a small and\n * capable library with zero dependencies: https://www.npmjs.com/package/earcut\n * @param {FOLD} graph a FOLD object, modified in place.\n * @param {any} earcut an optional reference to the Earcut library\n * by Mapbox, required to operate on a graph with non-convex faces.\n * @returns {{\n *   result: FOLD,\n *   changes: { faces?: { map: number[][] }, edges?: { new: number[] } },\n * }}\n * a summary of changes to the input parameter. the faceMap is an arrayMap\n * @todo preserve faceOrders, match preexisting faces against new ones,\n * this may create too much unnecessary data but at least it will work.\n */\nexport const triangulate = ({\n\tvertices_coords, edges_vertices, edges_assignment, edges_foldAngle,\n\tfaces_vertices, faceOrders,\n}, earcut) => {\n\tif (!faces_vertices) {\n\t\tconst result = {\n\t\t\tvertices_coords, edges_vertices, edges_assignment, edges_foldAngle,\n\t\t};\n\t\tObject.keys(result)\n\t\t\t.filter(key => !result[key])\n\t\t\t.forEach(key => delete result[key]);\n\t\treturn { result, changes: {} };\n\t}\n\tconst nextMap = makeTriangulatedFacesNextMap({ faces_vertices });\n\tconst faces_verticesNew = earcut\n\t\t? triangulateNonConvexFacesVertices({ vertices_coords, faces_vertices }, earcut)\n\t\t: triangulateConvexFacesVertices({ faces_vertices });\n\t// if the graph did not contain edges_vertices after this method, it will\n\n\t// this graph contains edges_vertices and faces_edges, and if they exist,\n\t// edges_assignment and edges_foldAngle\n\tconst edgeGraph = rebuildTriangleEdges({\n\t\tedges_vertices, edges_assignment, edges_foldAngle, faces_vertices,\n\t}, { faces_vertices: faces_verticesNew });\n\n\t// add vertices_coords and faces_vertices to the new triangulated edge graph\n\tconst result = {\n\t\t...edgeGraph,\n\t\tvertices_coords,\n\t\tfaces_vertices: faces_verticesNew,\n\t};\n\n\t// create an edge map\n\tconst startingEdgeCount = edges_vertices ? edges_vertices.length : 0;\n\tconst newEdges = Array\n\t\t.from(Array(edgeGraph.edges_vertices.length - startingEdgeCount))\n\t\t.map((_, i) => startingEdgeCount + i);\n\tconst changes = {\n\t\tfaces: { map: nextMap },\n\t\tedges: { new: newEdges },\n\t};\n\treturn { result, changes };\n};\n"
  },
  {
    "path": "src/graph/validate/validate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tvalidateTypes,\n} from \"./validateTypes.js\";\nimport {\n\tvalidateReferences,\n} from \"./validateReferences.js\";\nimport {\n\tvalidateReflexive,\n} from \"./validateReflexive.js\";\nimport {\n\tvalidateWinding,\n} from \"./validateWinding.js\";\nimport {\n\tvalidateAssignments,\n} from \"./validateAssignments.js\";\nimport {\n\tvalidateOrders,\n} from \"./validateOrders.js\";\n\n/**\n * @notes\n * should non-planar faces be an invalid case?\n */\n\n/**\n * @description Validate a graph, ensuring that all references across\n * different arrays point to valid data, there are no mismatching\n * array references.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} array of error messages. an empty array means no errors.\n */\nexport const validate = (graph) => (validateTypes(graph)\n\t.concat(validateReferences(graph))\n\t.concat(validateReflexive(graph))\n\t.concat(validateWinding(graph))\n\t.concat(validateAssignments(graph))\n\t.concat(validateOrders(graph))\n);\n\n// const extraVerticesTest = (graph, epsilon) => {\n// \tconst errors = [];\n// \tconst isolated_vertices = isolatedVertices(graph);\n// \tconst duplicate_vertices = duplicateVertices(graph, epsilon);\n// \tif (isolated_vertices.length !== 0) {\n// \t\terrors.push(`contains isolated vertices: ${isolated_vertices.join(\", \")}`);\n// \t}\n// \tif (duplicate_vertices.length !== 0) {\n// \t\terrors.push(`contains duplicate vertices`);\n// \t}\n// \treturn errors;\n// };\n"
  },
  {
    "path": "src/graph/validate/validateAssignments.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tassignmentFlatFoldAngle,\n} from \"../../fold/spec.js\";\n\n/**\n * @param {FOLD} graph a FOLD object\n * @returns {string[]}\n */\nconst assignmentsAndFoldAngleMatch = ({ edges_assignment, edges_foldAngle }) => {\n\tconst angleSign = edges_foldAngle\n\t\t.map(Math.sign);\n\tconst assignmentSign = edges_assignment\n\t\t.map(assign => assignmentFlatFoldAngle[assign])\n\t\t.map(Math.sign);\n\treturn assignmentSign\n\t\t.map((s, i) => (s === angleSign[i]\n\t\t\t? undefined\n\t\t\t: `assignment does not match fold angle at ${i}: ${edges_assignment[i]}, ${edges_foldAngle[i]}`))\n\t\t.filter(a => a !== undefined);\n};\n\n/**\n * @description If a graph has both edges_assignment and edges_foldAngle,\n * ensure that they both match each other, V is +, M is -, all else is 0.\n * @description test reflexive component relationships.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateAssignments = (graph) => {\n\tconst assignmentErrors = [];\n\tif (graph.edges_assignment && graph.edges_foldAngle) {\n\t\tassignmentErrors.push(...assignmentsAndFoldAngleMatch(graph));\n\t}\n\n\treturn assignmentErrors;\n};\n"
  },
  {
    "path": "src/graph/validate/validateOrders.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @param {[number, number, number][]} orders, such as faceOrders or edgeOrders\n * @param {string} name a descriptive string of which orders these are,\n * \"faceOrders\" or \"edgeOrders\".\n * @returns {string[]}\n */\nconst ordersTest = (orders, name = \"orders\") => {\n\t// orders between two faces that are actually the same face\n\tconst sameFaceOrders = orders\n\t\t.map(([a, b], i) => (a === b ? [a, b, i] : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(([a, b, i]) => `${name} between the same face ${a}, ${b} at index ${i}`);\n\tconst pairHash = {};\n\tconst duplicateOrders = orders\n\t\t.filter(([a, b]) => {\n\t\t\tconst key = a < b ? `${a} ${b}` : `${b} ${a}`;\n\t\t\tconst isDuplicate = pairHash[key] === true;\n\t\t\tpairHash[key] = true;\n\t\t\treturn isDuplicate;\n\t\t})\n\t\t.map(([a, b]) => `${name} duplicate orders found at indices ${a}, ${b}`);\n\treturn sameFaceOrders.concat(duplicateOrders);\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateOrders = (graph) => {\n\tconst ordersErrors = [];\n\tif (graph.faceOrders) {\n\t\tordersErrors.push(...ordersTest(graph.faceOrders, \"faceOrders\"));\n\t}\n\tif (graph.edgeOrders) {\n\t\tordersErrors.push(...ordersTest(graph.edgeOrders, \"edgeOrders\"));\n\t}\n\treturn ordersErrors;\n};\n"
  },
  {
    "path": "src/graph/validate/validateReferences.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfilterKeysWithPrefix,\n\tfilterKeysWithSuffix,\n\tgetAllPrefixes,\n\tgetAllSuffixes,\n} from \"../../fold/spec.js\";\n\n/**\n *\n */\nconst intersectionOfStrings = (array1, array2) => {\n\tconst counts = {};\n\tarray1.forEach(key => { counts[key] = 1; });\n\tarray2.forEach(key => {\n\t\tcounts[key] = counts[key] === 1 ? 2 : 1;\n\t});\n\treturn Object.keys(counts).filter(key => counts[key] === 2);\n};\n\n/**\n * @description Provide a list of arrays. This method will ensure that\n * all arrays contain the same indices. This works with arrays with holes.\n * O(n)\n * @param {any[][]} arrays any number of arrays\n * @returns {number[][]} array of error messages. empty if all tests pass.\n */\nconst arraysHaveSameIndices = (arrays = []) => {\n\t// base case test passes.\n\tif (arrays.length < 2) { return []; }\n\t// store arrays[0]'s indices into a hash lookup.\n\tconst indices = {};\n\tarrays[0].forEach((_, i) => { indices[i] = true; });\n\t// arrays 1...N-1. test each array against array 0.\n\treturn Array.from(Array(arrays.length - 1))\n\t\t.map((_, i) => arrays[i + 1])\n\t\t.flatMap((arr, p) => arr\n\t\t\t// if error, return [a, b, i], where a and b are the indices\n\t\t\t// of the arrays inside \"arrays\" param, and i is the out of place index.\n\t\t\t// (p is off by one due to i + 1 mapping 0...N-2 to 1...N-1).\n\t\t\t.map((_, i) => (indices[i] ? undefined : [0, p + 1, i]))\n\t\t\t.filter(a => a !== undefined));\n};\n\n/**\n * @description test 1: for every similar prefix array (vertices_), ensure\n * that each of them are the same length (all of their indices match).\n * test 2: in the case of a suffix referencing another array's prefix,\n * check that the indices from the suffix exist in the prefix\n *\n * Inspect every array whose key contains a \"_\" symbol.\n * Ensure that every matching prefix (all \"edges_\" arrays) all match\n * in length and contain the same indices.\n * Ensure that every suffix array (all \"_edges\" arrays) with a matching\n * prefix array (all \"edges_\" arrays) contain only indices which match\n * with some value in the prefix array.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateReferences = (graph) => {\n\tconst allPrefixes = getAllPrefixes(graph)\n\t\t.filter(key => key !== \"file\" && key !== \"frame\");\n\tconst allSuffixes = getAllSuffixes(graph);\n\tconst prefixKeys = allPrefixes\n\t\t.map(prefix => filterKeysWithPrefix(graph, prefix));\n\n\tconst prefixTestErrors = prefixKeys\n\t\t.map(keys => keys\n\t\t\t.map(key => graph[key])\n\t\t\t// only allow array values\n\t\t\t.filter(arr => arr.constructor === Array))\n\t\t.map(arraysHaveSameIndices)\n\t\t.flatMap((prefixErrors, p) => prefixErrors\n\t\t\t.map(err => [prefixKeys[p][err[0]], prefixKeys[p][err[1]], err[2]]))\n\t\t.map(([a, b, i]) => `array indices differ ${a}, ${b} at index ${i}`);\n\n\tlet referenceErrors = [];\n\ttry {\n\t\treferenceErrors = intersectionOfStrings(allPrefixes, allSuffixes)\n\t\t\t.flatMap(match => {\n\t\t\t\tconst prefixArrayKeys = prefixKeys[allPrefixes.indexOf(match)];\n\t\t\t\tconst prefixArray = graph[prefixArrayKeys[0]];\n\t\t\t\treturn filterKeysWithSuffix(graph, match)\n\t\t\t\t\t.flatMap(key => graph[key]\n\t\t\t\t\t\t.flatMap((arr, i) => arr\n\t\t\t\t\t\t\t// \"index\" can be null, ie: vertices_faces can be [[2, null, 5]]\n\t\t\t\t\t\t\t.map((index, j) => (index === null\n\t\t\t\t\t\t\t\t|| index === undefined\n\t\t\t\t\t\t\t\t|| prefixArray[index] !== undefined\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: `${key}[${i}][${j}] references ${match} ${index}, missing in ${prefixArrayKeys[0]}`))\n\t\t\t\t\t\t\t.filter(a => a !== undefined)));\n\t\t\t});\n\t} catch (error) {\n\t\treferenceErrors.push(\"reference validation failed due to bad index access\");\n\t}\n\treturn prefixTestErrors.concat(referenceErrors);\n};\n"
  },
  {
    "path": "src/graph/validate/validateReflexive.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description In the case where two identical but opposite arrays exist,\n * this will test to make sure that every item in one array appears in the\n * other; for example, in vertices_faces and faces_vertices.\n */\nconst pairwiseReferenceTest = (a_b, b_a, aName, bName) => {\n\ttry {\n\t\t// when we iterate through each array at its second level depth, we need\n\t\t// to be sure to filter out any null or undefined, as for example it's\n\t\t// requird by the spec to include undefined inside of vertices_faces,\n\t\t// but only inside the inner arrays. at the top level no nulls should exist.\n\t\tconst abHash = {};\n\t\ta_b.forEach((_, a) => { abHash[a] = {}; });\n\t\ta_b.forEach((arr, a) => arr\n\t\t\t.filter(el => el !== null && el !== undefined)\n\t\t\t.forEach(b => { abHash[a][b] = true; }));\n\n\t\tconst baHash = {};\n\t\tb_a.forEach((_, b) => { baHash[b] = {}; });\n\t\tb_a.forEach((arr, b) => arr\n\t\t\t.filter(el => el !== null && el !== undefined)\n\t\t\t.forEach(a => { baHash[b][a] = true; }));\n\n\t\tconst abErrors = a_b\n\t\t\t.flatMap((arr, a) => arr\n\t\t\t\t.filter(el => el !== null && el !== undefined)\n\t\t\t\t.map(b => (!baHash[b] || !baHash[b][a]\n\t\t\t\t\t? `${bName}_${aName}[${b}] missing ${a} referenced in ${aName}_${bName}`\n\t\t\t\t\t: undefined))\n\t\t\t\t.filter(el => el !== undefined));\n\t\tconst baErrors = b_a\n\t\t\t.flatMap((arr, b) => arr\n\t\t\t\t.filter(el => el !== null && el !== undefined)\n\t\t\t\t.map(a => (!abHash[a] || !abHash[a][b]\n\t\t\t\t\t? `${aName}_${bName}[${a}] missing ${b} referenced in ${bName}_${aName}`\n\t\t\t\t\t: undefined))\n\t\t\t\t.filter(el => el !== undefined));\n\n\t\treturn abErrors.concat(baErrors);\n\t} catch (error) {\n\t\treturn [\"pairwise reference validation failed due to bad index access\"]\n\t}\n};\n\n/**\n * @description This will test a reflexive array, like vertices_vertices,\n * to ensure that every reference also exists\n */\nconst reflexiveTest = (a_a, aName) => (\n\tpairwiseReferenceTest(a_a, a_a, aName, aName)\n);\n\n/**\n * @description test reflexive component relationships.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateReflexive = (graph) => {\n\tconst reflexiveErrors = [];\n\tif (graph.faces_vertices && graph.vertices_faces) {\n\t\treflexiveErrors.push(...pairwiseReferenceTest(\n\t\t\tgraph.faces_vertices,\n\t\t\tgraph.vertices_faces,\n\t\t\t\"faces\",\n\t\t\t\"vertices\",\n\t\t));\n\t}\n\tif (graph.edges_vertices && graph.vertices_edges) {\n\t\treflexiveErrors.push(...pairwiseReferenceTest(\n\t\t\tgraph.edges_vertices,\n\t\t\tgraph.vertices_edges,\n\t\t\t\"edges\",\n\t\t\t\"vertices\",\n\t\t));\n\t}\n\tif (graph.faces_edges && graph.edges_faces) {\n\t\treflexiveErrors.push(...pairwiseReferenceTest(\n\t\t\tgraph.faces_edges,\n\t\t\tgraph.edges_faces,\n\t\t\t\"faces\",\n\t\t\t\"edges\",\n\t\t));\n\t}\n\n\tif (graph.vertices_vertices) {\n\t\treflexiveErrors.push(...reflexiveTest(graph.vertices_vertices, \"vertices\"));\n\t}\n\tif (graph.faces_faces) {\n\t\treflexiveErrors.push(...reflexiveTest(graph.faces_faces, \"faces\"));\n\t}\n\treturn reflexiveErrors;\n};\n"
  },
  {
    "path": "src/graph/validate/validateTypes.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfoldKeys,\n} from \"../../fold/spec.js\";\nimport {\n\tduplicateEdges,\n} from \"../edges/duplicate.js\";\nimport {\n\tcircularEdges,\n} from \"../edges/circular.js\";\n\n/**\n * @returns {number[]} indices\n */\nconst noNulls = (array) => array\n\t.map((el, i) => (el === null || el === undefined ? i : undefined))\n\t.filter(a => a !== undefined);\n\n/**\n * @description\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateTypes = (graph) => {\n\tconst errors = [];\n\t// check if any graph array has null values at the top level\n\ttry {\n\t\tfoldKeys.graph\n\t\t\t.filter(key => graph[key])\n\t\t\t.forEach(key => errors.push(...noNulls(graph[key])\n\t\t\t\t.map(i => `${key}[${i}] is undefined or null`)));\n\t} catch (error) {\n\t\terrors.push(\"validation error: undefined or null array index\");\n\t}\n\n\ttry {\n\t\t[\n\t\t\t\"vertices_coords\",\n\t\t\t\"vertices_vertices\",\n\t\t\t\"vertices_edges\",\n\t\t\t\"edges_vertices\",\n\t\t\t\"faces_vertices\",\n\t\t\t\"faces_edges\",\n\t\t].filter(key => graph[key])\n\t\t\t.flatMap(key => graph[key]\n\t\t\t\t.map(noNulls)\n\t\t\t\t.flatMap((indices, i) => indices.map(j => `${key}[${i}][${j}] is undefined or null`)))\n\t\t\t.forEach(error => errors.push(error));\n\t} catch (error) {\n\t\terrors.push(\"validation error: undefined or null array index\");\n\t}\n\n\tif (graph.vertices_coords) {\n\t\ttry {\n\t\t\terrors.push(...graph.vertices_coords\n\t\t\t\t.map((coords, v) => (coords.length !== 2 && coords.length !== 3\n\t\t\t\t\t? v\n\t\t\t\t\t: undefined))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(e => `vertices_coords[${e}] is not length 2 or 3`));\n\t\t} catch (error) {\n\t\t\terrors.push(\"validation error: coordinate value undefined or null\");\n\t\t}\n\t}\n\n\tif (graph.edges_vertices) {\n\t\ttry {\n\t\t\terrors.push(...graph.edges_vertices\n\t\t\t\t.map((vertices, e) => (vertices.length !== 2 ? e : undefined))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(e => `edges_vertices[${e}] is not length 2`));\n\n\t\t\tconst circular_edges = circularEdges(graph);\n\t\t\tif (circular_edges.length !== 0) {\n\t\t\t\terrors.push(`circular edges_vertices: ${circular_edges.join(\", \")}`);\n\t\t\t}\n\n\t\t\tconst duplicate_edges = duplicateEdges(graph);\n\t\t\tif (duplicate_edges.length !== 0) {\n\t\t\t\tconst dups = duplicate_edges\n\t\t\t\t\t.map((dup, e) => `${e}(${dup})`)\n\t\t\t\t\t.filter(a => a)\n\t\t\t\t\t.join(\", \");\n\t\t\t\terrors.push(`duplicate edges_vertices: ${dups}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\terrors.push(\"validation error: edge value undefined or null\");\n\t\t}\n\t}\n\n\tif (graph.faces_vertices) {\n\t\ttry {\n\t\t\terrors.push(...graph.faces_vertices\n\t\t\t\t.map((vertices, f) => (vertices.length === 0 ? f : undefined))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(f => `faces_vertices[${f}] contains no vertices`));\n\t\t} catch (error) {\n\t\t\terrors.push(\"validation error: face (faces_vertices) value undefined or null\");\n\t\t}\n\t}\n\n\tif (graph.faces_edges) {\n\t\ttry {\n\t\t\terrors.push(...graph.faces_edges\n\t\t\t\t.map((edges, f) => (edges.length === 0 ? f : undefined))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(f => `faces_edges[${f}] contains no edges`));\n\t\t} catch (error) {\n\t\t\terrors.push(\"validation error: face (faces_edges) value undefined or null\");\n\t\t}\n\t}\n\treturn errors;\n};\n"
  },
  {
    "path": "src/graph/validate/validateWinding.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeVerticesToEdge,\n\tmakeVerticesToFace,\n\tmakeEdgesToFace,\n} from \"../make/lookup.js\";\n\n/**\n * @description Validate that the winding around each vertex\n * matches across arrays.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} list of errors, empty if none\n */\nexport const validateVerticesWinding = ({\n\tvertices_vertices,\n\tvertices_edges,\n\tvertices_faces,\n\tedges_vertices,\n\tfaces_vertices,\n\tfaces_edges,\n}) => {\n\tconst errors = [];\n\tconst verticesToEdge = edges_vertices\n\t\t? makeVerticesToEdge({ edges_vertices })\n\t\t: undefined;\n\tconst verticesToFace = faces_vertices\n\t\t? makeVerticesToFace({ faces_vertices })\n\t\t: undefined;\n\tconst edgesToFace = faces_edges\n\t\t? makeEdgesToFace({ faces_edges })\n\t\t: undefined;\n\ttry {\n\t\tif (vertices_vertices && vertices_edges && verticesToEdge) {\n\t\t\tconst vAndE = vertices_vertices\n\t\t\t\t.flatMap((vertices, v) => vertices\n\t\t\t\t\t.map(v2 => [v, v2])\n\t\t\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t\t\t.map(key => verticesToEdge[key])\n\t\t\t\t\t.map((edge, i) => (vertices_edges[v][i] !== edge\n\t\t\t\t\t\t? [v, i, edge, vertices_edges[v][i]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, e1, e2]) => `windings of vertices_vertices and vertices_edges at [${a}][${b}] do not match (${e1} ${e2})`);\n\t\t\terrors.push(...vAndE);\n\t\t}\n\n\t\tif (vertices_vertices && vertices_faces && verticesToFace) {\n\t\t\tconst vAndF = vertices_vertices\n\t\t\t\t.flatMap((vertices, v) => vertices\n\t\t\t\t\t.map(vertex => [v, vertex])\n\t\t\t\t\t// .map((vertex, index, arr) => [arr[(index + 1) % arr.length], v, vertex])\n\t\t\t\t\t.map(three => three.join(\" \"))\n\t\t\t\t\t.map(key => verticesToFace[key])\n\t\t\t\t\t// single not-equals to be able to compare null == undefined as true\n\t\t\t\t\t.map((face, i) => (vertices_faces[v][i] != face\n\t\t\t\t\t\t? [v, i, face, vertices_faces[v][i]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, f1, f2]) => `windings of vertices_vertices and vertices_faces at [${a}][${b}] do not match (${f1} ${f2})`);\n\t\t\terrors.push(...vAndF);\n\t\t}\n\n\t\tif (vertices_edges && vertices_faces && edgesToFace) {\n\t\t\tconst eAndF = vertices_edges\n\t\t\t\t.flatMap((edges, v) => edges\n\t\t\t\t\t.map((edge, index, arr) => [arr[(index + 1) % arr.length], edge])\n\t\t\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t\t\t.map(key => edgesToFace[key])\n\t\t\t\t\t// single not-equals to be able to compare null == undefined as true\n\t\t\t\t\t.map((face, i) => (vertices_faces[v][i] != face\n\t\t\t\t\t\t? [v, i, face, vertices_faces[v][i]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, f1, f2]) => `windings of vertices_edges and vertices_faces at [${a}][${b}] do not match (${f1} ${f2})`);\n\t\t\terrors.push(...eAndF);\n\t\t}\n\t} catch (error) {\n\t\terrors.push(\"vertices winding validation failed due to bad index access\");\n\t}\n\treturn errors;\n};\n\n/**\n * @description Validate that the winding around each face definition matches\n * across arrays.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} list of errors, empty if none\n */\nexport const validateFacesWinding = ({\n\tedges_vertices,\n\tedges_faces,\n\tfaces_vertices,\n\tfaces_edges,\n\tfaces_faces,\n}) => {\n\tconst errors = [];\n\tconst verticesToEdge = edges_vertices\n\t\t? makeVerticesToEdge({ edges_vertices })\n\t\t: undefined;\n\tconst verticesToFace = faces_vertices\n\t\t? makeVerticesToFace({ faces_vertices })\n\t\t: undefined;\n\ttry {\n\t\tif (faces_vertices && faces_edges && verticesToEdge) {\n\t\t\tconst vAndE = faces_vertices\n\t\t\t\t.flatMap((vertices, f) => vertices\n\t\t\t\t\t.map((_, i, arr) => [0, 1].map(n => arr[(i + n) % arr.length]))\n\t\t\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t\t\t.map(key => verticesToEdge[key])\n\t\t\t\t\t.map((edge, j) => (faces_edges[f][j] !== edge\n\t\t\t\t\t\t? [f, j, edge, faces_edges[f][j]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, e1, e2]) => `windings of faces_vertices and faces_edges at [${a}][${b}] do not match (${e1} ${e2})`);\n\t\t\terrors.push(...vAndE);\n\t\t}\n\n\t\tif (faces_vertices && faces_faces && verticesToFace) {\n\t\t\tconst vAndF = faces_vertices\n\t\t\t\t.flatMap((vertices, i) => vertices\n\t\t\t\t\t.map((_, index, arr) => [1, 0].map(n => arr[(index + n) % arr.length]))\n\t\t\t\t\t.map(pair => pair.join(\" \"))\n\t\t\t\t\t.map(key => verticesToFace[key])\n\t\t\t\t\t// single not-equals to be able to compare null == undefined as true\n\t\t\t\t\t.map((face, j) => (faces_faces[i][j] != face\n\t\t\t\t\t\t? [i, j, face, faces_faces[i][j]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, f1, f2]) => `windings of faces_vertices and faces_faces at [${a}][${b}] do not match (${f1} ${f2})`);\n\t\t\terrors.push(...vAndF);\n\t\t}\n\n\t\tif (faces_edges && faces_faces && edges_faces) {\n\t\t\tconst eAndF = faces_edges\n\t\t\t\t.flatMap((edges, i) => edges\n\t\t\t\t\t.map(edge => edges_faces[edge].filter(f => f !== i).shift())\n\t\t\t\t\t// single not-equals to be able to compare null == undefined as true\n\t\t\t\t\t.map((face, j) => (faces_faces[i][j] != face\n\t\t\t\t\t\t? [i, j, face, faces_faces[i][j]]\n\t\t\t\t\t\t: undefined)))\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.map(([a, b, f1, f2]) => `windings of faces_edges and faces_faces at [${a}][${b}] do not match (${f1} ${f2})`);\n\t\t\terrors.push(...eAndF);\n\t\t}\n\t} catch (error) {\n\t\terrors.push(\"faces winding validation failed due to bad index access\");\n\t}\n\treturn errors;\n};\n\n/**\n * @description Ensure that component indices across different suffix\n * values match in their winding orders. For example, a face's vertices in\n * faces_vertices should match it's edges in faces_edges where index 0\n * vertex is in the edge in index 0 from its faces_edges.\n * @param {FOLD} graph a FOLD object\n * @returns {string[]} a list of errors if they exist\n */\nexport const validateWinding = (graph) => {\n\tconst verticesWindingErrors = validateVerticesWinding(graph);\n\tconst facesWindingErrors = validateFacesWinding(graph);\n\treturn verticesWindingErrors.concat(facesWindingErrors);\n};\n"
  },
  {
    "path": "src/graph/vertices/clusters.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"../../math/constant.js\";\nimport { getDimensionQuick } from \"../../fold/spec.js\";\n\n/**\n * @description Find all clusters of vertices which lie within an epsilon\n * of each other. Each cluster is an array of vertex indices. If no clusters\n * exist, the method returns N-number of arrays, each with a single vertex\n * entry. This is an implementation of a density-based spatial clustering\n * of applications with noise (DBSCAN).\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} a list of clusters_vertices, for every cluster,\n * an of vertex indices which are a part of this cluster.\n * @example\n * no clusters: [ [0], [1], [2], [3], [4], ... ]\n * clusters: [ [0, 5], [1], [3], [2, 4]]\n */\nexport const getVerticesClusters = ({ vertices_coords }, epsilon = EPSILON) => {\n\tif (!vertices_coords) { return []; }\n\tconst dimensions = getDimensionQuick({ vertices_coords });\n\tconst dimensionArray = Array.from(Array(dimensions));\n\n\t// the return value, the clusters\n\tconst clusters = [];\n\n\t// increment every time we add a vertex to a cluster.\n\t// once this number equals the number of vertices, we are done.\n\tlet visited = 0;\n\n\t// as we add points to clusters, they will be removed from here.\n\t// sort vertices (any dimension, let's use the X-axis). store their indices.\n\t// the last line is necessary to remove any holes.\n\tconst vertices = vertices_coords\n\t\t.map((point, i) => ({ i, d: point[0] }))\n\t\t.sort((a, b) => a.d - b.d)\n\t\t.map(a => a.i)\n\t\t.filter(() => true);\n\n\t// for each X and Y axis, there is an array of two values: [0, 0].\n\t// As we walk through the vertices, we maintain the min and max of a\n\t// subset of the most recently added points (more info in \"updateRange\").\n\tconst ranges = dimensionArray.map(() => [0, 0]);\n\n\t/**\n\t * @description Test if a vertex is inside the current cluster's bounding box\n\t * @param {number} index an index in vertices_coords\n\t */\n\tconst isInsideCluster = (index) => dimensionArray\n\t\t.map((_, d) => vertices_coords[index][d] > ranges[d][0]\n\t\t\t&& vertices_coords[index][d] < ranges[d][1])\n\t\t.reduce((a, b) => a && b, true);\n\n\t// \"rangeStart\" is the index of the cluster which is still within epsilon\n\t// range of the most recently added point, every update to cluster will\n\t// refresh the bounding box as rect range of indices (rangeStart...length).\n\tlet rangeStart = 0;\n\n\t// Each time we add a point to the current cluster, form a bounding\n\t// box NOT with all points in the cluster, but all points that are within\n\t// epsilon range along the X-axis of the most recently added point.\n\tconst updateRange = (cluster) => {\n\t\t// find the start index of the values within epsilon of the newest point\n\t\tconst newVertex = cluster[cluster.length - 1];\n\t\twhile (vertices_coords[newVertex][0]\n\t\t\t- vertices_coords[cluster[rangeStart]][0] > epsilon) {\n\t\t\trangeStart += 1;\n\t\t}\n\n\t\t// the subset of points within the epsilon of the newest point\n\t\tconst points = cluster.slice(rangeStart, cluster.length)\n\t\t\t.map(v => vertices_coords[v]);\n\n\t\t// set all dimensions of the range.\n\t\t// for the X-axis, we know that the values are sorted, so, we can\n\t\t// make a little shortcut and only grab the first and last entry.\n\t\tranges[0] = [\n\t\t\tpoints[0][0] - epsilon,\n\t\t\tpoints[points.length - 1][0] + epsilon,\n\t\t];\n\t\t// set the new ranges with the min and max padded with the epsilon\n\t\tfor (let d = 1; d < dimensions; d += 1) {\n\t\t\tconst scalars = points.map(p => p[d]);\n\t\t\tranges[d] = [\n\t\t\t\tMath.min(...scalars) - epsilon,\n\t\t\t\tMath.max(...scalars) + epsilon,\n\t\t\t];\n\t\t}\n\t};\n\n\t// here is our main loop. we loop until all vertices have been visited.\n\t// inside here we setup the conditions of the new cluster, then\n\t// there is another loop inside here which walks through the vertices\n\t// (remember these are sorted along the X axis), and add vertices to the\n\t// cluster if the vertex is inside the cluster's \"range\" AABB, and when the\n\t// cluster is done, restart this loop and a new cluster.\n\t// Note, a cluster is called \"done\" only when the next point lies outside\n\t// of epsilon range along the X axis, not Y or Z, as it's possible for the\n\t// next point to be far away in one dimension, but that the point after it\n\t// is back within range again.\n\twhile (visited < vertices_coords.length && vertices.length) {\n\t\t// start a new cluster, add the first vertex, first in array, min X axis.\n\t\tconst cluster = [];\n\t\tconst startVertex = vertices.shift();\n\t\tcluster.push(startVertex);\n\t\tvisited += 1;\n\n\t\t// rangeStart is an index in the array \"cluster\". as we build the cluster,\n\t\t// this will increment to stay with epsilon range of the newest vertex.\n\t\t// each time we start a new cluster, reset this value back to 0.\n\t\trangeStart = 0;\n\n\t\t// this will update the AABB ranges, and walk \"rangeStart\" forward.\n\t\tupdateRange(cluster);\n\n\t\t// walk is an index in the array \"vertices\"\n\t\tlet walk = 0;\n\n\t\t// this boolean check tests whether or not the vertex is still inside\n\t\t// the bounding box ONLY according to the X axis. it's possible a\n\t\t// vertex is too far away in the Y (and rejected from the cluster), but\n\t\t// the next few vertices to its right are still within the cluster.\n\t\t// however, if we move too far away in the X direction from the cluster,\n\t\t// we know this cluster is finished and we can start a new one.\n\t\twhile (walk < vertices.length\n\t\t\t&& vertices_coords[vertices[walk]][0] < ranges[0][1]) {\n\t\t\t// if the point is inside the bounding box of the cluster, add it.\n\t\t\tif (isInsideCluster(vertices[walk])) {\n\t\t\t\tconst newVertex = vertices.splice(walk, 1).shift();\n\t\t\t\tcluster.push(newVertex);\n\t\t\t\tvisited += 1;\n\n\t\t\t\t// update new bounding box, walk x-start pointer forward\n\t\t\t\tupdateRange(cluster);\n\n\t\t\t\t// don't increment the walk. the vertices array got smaller instead\n\t\t\t} else {\n\t\t\t\t// skip over this vertex. even though our vertices are sorted\n\t\t\t\t// left to right, it's possible that this one is too far away in the\n\t\t\t\t// Y axis, but that the next few points will be inside the cluster,\n\t\t\t\t// so we need to keep looping and progressing through the vertices\n\t\t\t\t// until we reach a vertex which is too far away in all axes.\n\t\t\t\twalk += 1;\n\t\t\t}\n\t\t}\n\n\t\t// cluster is done, no more possible points can be added.\n\t\t// start a new cluster next round, unless all vertices are finished.\n\t\tclusters.push(cluster);\n\t}\n\treturn clusters;\n};\n"
  },
  {
    "path": "src/graph/vertices/collinear.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../../math/constant.js\";\nimport {\n\tcollinearBetween,\n} from \"../../math/line.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../make/verticesEdges.js\";\n\n/**\n * @description Given one vertex, and a list of edges which contain this vertex,\n * get one vertex for every edge which is not the input parameter vertex.\n * @param {FOLD} graph a FOLD object with edges_vertices\n * @param {number} vertex one vertex index\n * @param {number[]} edges a list of edge indices\n * @returns {number[]} for every edge, one vertex that is the opposite vertex\n */\nexport const getOtherVerticesInEdges = ({ edges_vertices }, vertex, edges) => (\n\tedges.map(edge => (edges_vertices[edge][0] === vertex\n\t\t? edges_vertices[edge][1]\n\t\t: edges_vertices[edge][0]))\n);\n\n/**\n * @description determine if a vertex exists between two and only two edges,\n * and those edges are both parallel and on opposite ends of the vertex.\n * In a lot of cases, this vertex can be removed and the graph would\n * function the same.\n * O(1) if vertices_edges exists, if not, O(n) where n=edges\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex an index of a vertex in the graph\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the vertex is collinear and can be removed.\n */\nexport const isVertexCollinear = ({\n\tvertices_coords, vertices_edges, edges_vertices,\n}, vertex, epsilon = EPSILON) => {\n\tif (!vertices_coords || !edges_vertices) { return false; }\n\tif (!vertices_edges) {\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst edges = vertices_edges[vertex];\n\tif (edges === undefined || edges.length !== 2) { return false; }\n\t// don't just check if they are parallel, use the direction of the vertex\n\t// to make sure the center vertex is inbetween, instead of the odd\n\t// case where the two edges are on top of one another with\n\t// a leaf-like vertex.\n\tconst vertices = getOtherVerticesInEdges({ edges_vertices }, vertex, edges);\n\tconst [a, b, c] = [vertices[0], vertex, vertices[1]]\n\t\t.map(v => vertices_coords[v]);\n\treturn collinearBetween(a, b, c, false, epsilon);\n};\n\n// /**\n//  * @description this is located in planarize.js. see if we can generalize\n//  * it and bring it out here.\n//  * Also, there is a method makeFacesNonCollinear inside make.js.\n//  */\n// export const removeCollinearVertex = ({ edges_vertices, vertices_edges }, vertex) => {};\n"
  },
  {
    "path": "src/graph/vertices/duplicate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"../../math/constant.js\";\nimport { average } from \"../../math/vector.js\";\nimport { getDimensionQuick } from \"../../fold/spec.js\";\nimport { getVerticesClusters } from \"./clusters.js\";\nimport { replace } from \"../replace.js\";\n\n/**\n * @description Get the indices of all vertices which\n * lie close to other vertices.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} arrays of clusters of similar vertices.\n */\nexport const duplicateVertices = ({ vertices_coords }, epsilon) => (\n\tgetVerticesClusters({ vertices_coords }, epsilon)\n\t\t.filter(arr => arr.length > 1)\n);\n\n/**\n * @description This will shrink the number of vertices in the graph,\n * if vertices are close within an epsilon, it will keep the first one,\n * find the average of close points, and assign it to the remaining vertex.\n * **this has the potential to create circular and duplicate edges**.\n * Important note: if vertices are mismatched in dimension (2D and 3D),\n * this might treat all vertices as 2D and duplicate vertices will be declared\n * when they are actually not the same. Just make sure your graph's\n * vertices are all of the same dimension.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @param {boolean} [makeAverage=true] an optional request, should the\n * merged vertex be a geometric average of all vertices which went into it?\n * - if \"true\", average will be calculated\n * - if \"false\", the smallest vertex index in the cluster is used\n * @returns {{ map: number[], remove: number[] }} summary of changes\n */\nexport const removeDuplicateVertices = (graph, epsilon = EPSILON, makeAverage = true) => {\n\t// replaces array will be [index:value] index is the element to delete,\n\t// value is the index this element will be replaced by.\n\tconst replace_indices = [];\n\n\t// \"remove\" is only needed for the return value summary.\n\tconst remove_indices = [];\n\n\t// clusters is array of indices, for example: [ [4, 13, 7], [0, 9] ]\n\tconst clusters = getVerticesClusters(graph, epsilon)\n\t\t.filter(arr => arr.length > 1);\n\n\t// for each cluster of n, all indices from [1...n] will be replaced with [0]\n\tclusters.forEach(cluster => {\n\t\t// replace() must maintain index > value, ensure index[0] is the\n\t\t// smallest of the set (most of the time it is)\n\t\tif (Math.min(...cluster) !== cluster[0]) {\n\t\t\tcluster.sort((a, b) => a - b);\n\t\t}\n\t\tfor (let i = 1; i < cluster.length; i += 1) {\n\t\t\treplace_indices[cluster[i]] = cluster[0];\n\t\t\tremove_indices.push(cluster[i]);\n\t\t}\n\t});\n\n\tconst dimensions = getDimensionQuick(graph);\n\n\t// for each cluster, average all vertices-to-merge to get their new point.\n\t// set the vertex at the index[0] (the index to keep) to the new point.\n\t// otherwise, this will use the value of the lowest index vertex.\n\tif (makeAverage) {\n\t\tclusters\n\t\t\t.map(arr => arr.map(i => graph.vertices_coords[i]))\n\t\t\t.map(arr => average(...arr))\n\t\t\t.forEach(([a, b, c], i) => {\n\t\t\t\t/** @type {[number, number]|[number, number, number]} */\n\t\t\t\tconst point = (dimensions === 2 ? [a, b] : [a, b, c]);\n\t\t\t\tgraph.vertices_coords[clusters[i][0]] = point;\n\t\t\t});\n\t}\n\treturn {\n\t\tmap: replace(graph, \"vertices\", replace_indices),\n\t\tremove: remove_indices,\n\t};\n};\n"
  },
  {
    "path": "src/graph/vertices/folded.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeEdgesIsFolded,\n\tedgesFoldAngleAreAllFlat,\n} from \"../../fold/spec.js\";\nimport {\n\tnormalize2,\n\tdot,\n\tscale2,\n\tadd2,\n\tsubtract2,\n\trotate270,\n\trotate90,\n\tresize2,\n\tresize3,\n} from \"../../math/vector.js\";\nimport {\n\tidentity2x3,\n\tmultiplyMatrix2Vector2,\n} from \"../../math/matrix2.js\";\nimport {\n\tidentity3x4,\n\tmultiplyMatrix3Vector3,\n} from \"../../math/matrix3.js\";\nimport {\n\tmakeVerticesFaces,\n} from \"../make/verticesFaces.js\";\nimport {\n\tmakeVerticesToEdge,\n} from \"../make/lookup.js\";\nimport {\n\tmakeFacesFaces,\n} from \"../make/facesFaces.js\";\nimport {\n\tminimumSpanningTrees,\n} from \"../trees.js\";\nimport {\n\tmakeFacesMatrix,\n\tfacesSharedEdgesVertices,\n} from \"../faces/matrix.js\";\n\n/**\n * @description Fold a graph along its edges and return the position\n * of the folded vertices. This method works in both 2D and 3D\n * unassigned edges are treated as flat fold (mountain/valley 180deg)\n * as a way of (assuming the user is giving a flat folded origami), help\n * solve things about an origami that is currently being figured out.\n * @param {FOLDExtended} graph a FOLD object\n * @param {number[]} [rootFaces=[]] the index of the face that will remain in place\n * @returns {[number, number, number][]} a new set of `vertices_coords` with the new positions.\n */\nexport const makeVerticesCoords3DFolded = ({\n\tvertices_coords, vertices_faces, edges_vertices, edges_foldAngle,\n\tedges_assignment, faces_vertices, faces_faces, faces_matrix,\n}, rootFaces) => {\n\tif (!vertices_coords || !vertices_coords.length) { return []; }\n\tif (!faces_vertices || !faces_vertices.length) {\n\t\treturn vertices_coords.map(resize3);\n\t}\n\tfaces_matrix = makeFacesMatrix({\n\t\tvertices_coords, edges_vertices, edges_foldAngle, edges_assignment, faces_vertices, faces_faces,\n\t}, rootFaces);\n\tif (!vertices_faces) {\n\t\tvertices_faces = makeVerticesFaces({ faces_vertices });\n\t}\n\n\t// assign one matrix to every vertex from faces, identity matrix if none exist\n\tconst vertices_matrix = vertices_faces\n\t\t.map(faces => faces.find(f => f != null))\n\t\t.map(face => (face === undefined\n\t\t\t? [...identity3x4]\n\t\t\t: faces_matrix[face]));\n\treturn vertices_coords\n\t\t.map(resize3)\n\t\t.map((coord, i) => multiplyMatrix3Vector3(vertices_matrix[i], coord));\n};\n\n/**\n * @description Fold a graph along its edges and return the position of the folded\n * vertices. this method works for 2D only (no z value).\n * if an edges_assignment is \"U\", assumed to be folded (\"V\" or \"M\").\n * Finally, if no edge foldAngle or assignments exist, this method will\n * assume all edges are flat-folded (except boundary) and will fold everything.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [rootFaces=[]] the index of the face that will remain in place\n * @returns {[number, number][]} a new set of `vertices_coords` with the new positions.\n */\nexport const makeVerticesCoordsFlatFolded = (\n\t{\n\t\tvertices_coords, edges_vertices, edges_foldAngle,\n\t\tedges_assignment, faces_vertices, faces_faces,\n\t},\n\trootFaces = [],\n) => {\n\tif (!vertices_coords || !vertices_coords.length) { return []; }\n\tif (!faces_vertices || !faces_vertices.length) {\n\t\treturn vertices_coords.map(resize2);\n\t}\n\tif (!faces_faces) {\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\tconst edges_isFolded = makeEdgesIsFolded({\n\t\tedges_vertices, edges_foldAngle, edges_assignment,\n\t});\n\tconst vertices_coordsFolded = [];\n\tconst faces_flipped = [];\n\tconst edgesMap = makeVerticesToEdge({ edges_vertices });\n\n\t// if the graph is disjoint, make sure we fold all disjoint sets,\n\t// each set chooses a starting face (first set is decided by rootFaces),\n\t// ensure this exists, then set all of its vertices to \"no change\".\n\tminimumSpanningTrees(faces_faces, rootFaces).forEach(tree => {\n\t\tconst rootRow = tree.shift();\n\t\tif (!rootRow || !rootRow.length) { return; }\n\t\t// root tree item is the first item in the first row (only item in the row)\n\t\tconst root = rootRow[0];\n\t\t// set this root face's initial conditions.\n\t\tfaces_flipped[root.index] = false;\n\t\tfaces_vertices[root.index]\n\t\t\t.forEach(v => { vertices_coordsFolded[v] = [...vertices_coords[v]]; });\n\t\ttree.forEach(level => level\n\t\t\t.forEach(entry => {\n\t\t\t\t// coordinates and vectors of the reflecting edge\n\t\t\t\tconst edgeKey = facesSharedEdgesVertices(\n\t\t\t\t\tfaces_vertices[entry.index],\n\t\t\t\t\tfaces_vertices[entry.parent],\n\t\t\t\t).shift().join(\" \");\n\t\t\t\tconst edge = edgesMap[edgeKey];\n\t\t\t\t// build a basis axis using the folding edge, normalized.\n\t\t\t\tconst coords = edges_vertices[edge].map(v => vertices_coordsFolded[v]);\n\t\t\t\tif (coords[0] === undefined || coords[1] === undefined) { return; }\n\t\t\t\tconst coords_cp = edges_vertices[edge].map(v => vertices_coords[v]);\n\t\t\t\t// the basis axis origin, x-basis axis (vector) and y-basis (normal)\n\t\t\t\tconst origin_cp = coords_cp[0];\n\t\t\t\tconst vector_cp = normalize2(subtract2(coords_cp[1], coords_cp[0]));\n\t\t\t\tconst normal_cp = rotate90(vector_cp);\n\t\t\t\t// if we are crossing a flipping edge (m/v), set this face to be\n\t\t\t\t// flipped opposite of the parent face. otherwise keep it the same.\n\t\t\t\tfaces_flipped[entry.index] = edges_isFolded[edge]\n\t\t\t\t\t? !faces_flipped[entry.parent]\n\t\t\t\t\t: faces_flipped[entry.parent];\n\t\t\t\tconst vector_folded = normalize2(subtract2(coords[1], coords[0]));\n\t\t\t\tconst origin_folded = coords[0];\n\t\t\t\tconst normal_folded = faces_flipped[entry.index]\n\t\t\t\t\t? rotate270(vector_folded)\n\t\t\t\t\t: rotate90(vector_folded);\n\t\t\t\t// remaining_faces_vertices\n\t\t\t\tfaces_vertices[entry.index]\n\t\t\t\t\t.filter(v => vertices_coordsFolded[v] === undefined)\n\t\t\t\t\t.forEach(v => {\n\t\t\t\t\t\tconst to_point = subtract2(vertices_coords[v], origin_cp);\n\t\t\t\t\t\tconst project_norm = dot(to_point, normal_cp);\n\t\t\t\t\t\tconst project_line = dot(to_point, vector_cp);\n\t\t\t\t\t\tconst walk_up = scale2(vector_folded, project_line);\n\t\t\t\t\t\tconst walk_perp = scale2(normal_folded, project_norm);\n\t\t\t\t\t\tconst folded_coords = add2(add2(origin_folded, walk_up), walk_perp);\n\t\t\t\t\t\tvertices_coordsFolded[v] = folded_coords;\n\t\t\t\t\t});\n\t\t\t}));\n\t});\n\treturn vertices_coordsFolded;\n};\n\n/**\n * @description Fold a graph along its edges and return the position of the folded\n * vertices. This method will fold vertices in either 2D or 3D.\n * If an edges_assignment is \"U\", assumed to be folded (\"V\" or \"M\").\n * Finally, if no edge foldAngle or assignments exist, this method will\n * assume all edges are flat-folded (except boundary) and will fold everything.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [rootFaces=[]] the indices of the faces that will remain in place\n * @returns {[number, number][]|[number, number, number][]} a new set of\n * `vertices_coords` with the new positions.\n */\nexport const makeVerticesCoordsFolded = (graph, rootFaces) => (\n\tedgesFoldAngleAreAllFlat(graph)\n\t\t? makeVerticesCoordsFlatFolded(graph, rootFaces)\n\t\t: makeVerticesCoords3DFolded(graph, rootFaces));\n\n/**\n *\n */\n// const makeVerticesCoordsUnfolded = ({\n// \tvertices_coords, vertices_faces, edges_vertices, edges_foldAngle,\n// \tedges_assignment, faces_vertices, faces_faces, faces_matrix,\n// }) => {};\n\n/**\n * @description Given a FOLD object and a set of 2x3 matrices, one per face,\n * \"fold\" the vertices by finding one matrix per vertex and multiplying them.\n * @param {FOLD} graph a FOLD object with vertices_coords, faces_vertices, and\n * if vertices_faces does not exist it will be built.\n * @param {number[][]} faces_matrix an array of 2x3 matrices. one per face.\n * @returns {[number, number][]} a new set of vertices_coords, transformed.\n */\nexport const makeVerticesCoordsFoldedFromMatrix2 = ({\n\tvertices_coords, vertices_faces, faces_vertices,\n}, faces_matrix) => {\n\tif (!vertices_faces) {\n\t\tvertices_faces = makeVerticesFaces({ faces_vertices });\n\t}\n\n\t// get the first face in each vertex's adjacent faces list. the null filtering\n\t// is important, this check removes undefined and null, null often arises when\n\t// importing a FOLD object from a FOLD file with JSON decoding.\n\tconst vertices_face = vertices_faces\n\t\t.map(faces => faces.find(f => f != null));\n\n\tconst vertices_matrix = vertices_face\n\t\t.map(face => (face === undefined\n\t\t\t? identity2x3\n\t\t\t: faces_matrix[face]));\n\n\treturn vertices_coords\n\t\t.map(resize2)\n\t\t.map((point, i) => multiplyMatrix2Vector2(vertices_matrix[i], point))\n};\n"
  },
  {
    "path": "src/graph/vertices/isolated.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tremove,\n} from \"../remove.js\";\n\n/**\n * @description Get the indices of all vertices which make no appearance in any edge.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} the indices of the isolated vertices\n */\nexport const edgeIsolatedVertices = ({ vertices_coords, edges_vertices }) => {\n\tif (!vertices_coords || !edges_vertices) { return []; }\n\tlet count = vertices_coords.length;\n\tconst seen = Array(count).fill(false);\n\tedges_vertices.forEach((ev) => {\n\t\tev.filter(v => !seen[v]).forEach((v) => {\n\t\t\tseen[v] = true;\n\t\t\tcount -= 1;\n\t\t});\n\t});\n\treturn seen\n\t\t.map((s, i) => (s ? undefined : i))\n\t\t.filter(a => a !== undefined);\n};\n\n/**\n * @description Get the indices of all vertices which make no appearance in any face.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} the indices of the isolated vertices\n */\nexport const faceIsolatedVertices = ({ vertices_coords, faces_vertices }) => {\n\tif (!vertices_coords || !faces_vertices) { return []; }\n\tlet count = vertices_coords.length;\n\tconst seen = Array(count).fill(false);\n\tfaces_vertices.forEach((fv) => {\n\t\tfv.filter(v => !seen[v]).forEach((v) => {\n\t\t\tseen[v] = true;\n\t\t\tcount -= 1;\n\t\t});\n\t});\n\treturn seen\n\t\t.map((s, i) => (s ? undefined : i))\n\t\t.filter(a => a !== undefined);\n};\n\n// todo this could be improved. for loop instead of forEach + filter.\n// break the loop early.\n/**\n * @description Get the indices of all vertices which make no appearance in any edge or face.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} the indices of the isolated vertices\n */\nexport const isolatedVertices = ({ vertices_coords, edges_vertices, faces_vertices }) => {\n\tif (!vertices_coords) { return []; }\n\tlet count = vertices_coords.length;\n\tconst seen = Array(count).fill(false);\n\tif (edges_vertices) {\n\t\tedges_vertices.forEach((ev) => {\n\t\t\tev.filter(v => !seen[v]).forEach((v) => {\n\t\t\t\tseen[v] = true;\n\t\t\t\tcount -= 1;\n\t\t\t});\n\t\t});\n\t}\n\tif (faces_vertices) {\n\t\tfaces_vertices.forEach((fv) => {\n\t\t\tfv.filter(v => !seen[v]).forEach((v) => {\n\t\t\t\tseen[v] = true;\n\t\t\t\tcount -= 1;\n\t\t\t});\n\t\t});\n\t}\n\treturn seen\n\t\t.map((s, i) => (s ? undefined : i))\n\t\t.filter(a => a !== undefined);\n};\n\n/**\n * @description Remove any vertices which are not a part of any edge or\n * face. This will shift up the remaining vertices indices so that the\n * vertices arrays will not have any holes, and, additionally it searches\n * through all _vertices reference arrays and updates the index\n * references for the shifted vertices.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} [remove_indices] Leave this empty. Otherwise, if\n * isolatedVertices() has already been called, provide the result here to speed\n * up the algorithm.\n * @returns {object} summary of changes\n */\nexport const removeIsolatedVertices = (graph, remove_indices) => {\n\tif (!remove_indices) {\n\t\tremove_indices = isolatedVertices(graph);\n\t}\n\treturn {\n\t\tmap: remove(graph, \"vertices\", remove_indices),\n\t\tremove: remove_indices,\n\t};\n};\n"
  },
  {
    "path": "src/graph/vertices/sort.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"../../math/constant.js\";\nimport { subtract } from \"../../math/vector.js\";\nimport { sortPointsAlongVector } from \"../../general/sort.js\";\n\n/**\n * @description This is a subroutine for building vertices_vertices. This will\n * take a set of vertices indices and a vertex index to be the center point,\n * and sort the indices radially counter-clockwise.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} vertices an array of vertex indices to be sorted\n * @param {number} vertex the origin vertex, around which\n * the vertices will be sorted\n * @returns {number[]} indices of vertices, in sorted order\n */\nexport const sortVerticesCounterClockwise = ({ vertices_coords }, vertices, vertex) => (\n\tvertices\n\t\t.map(v => vertices_coords[v])\n\t\t.map(coord => subtract(coord, vertices_coords[vertex]))\n\t\t.map(vec => Math.atan2(vec[1], vec[0]))\n\t\t// optional line, this makes the cycle loop start/end along the +X axis\n\t\t.map(angle => (angle > -EPSILON ? angle : angle + Math.PI * 2))\n\t\t.map((a, i) => ({ a, i }))\n\t\t.sort((a, b) => a.a - b.a)\n\t\t.map(el => el.i)\n\t\t.map(i => vertices[i])\n);\n\n/**\n * @description sort a subset of vertices from a graph along a vector.\n * eg: given the vector [1,0], points according to their X value.\n * @param {FOLD} graph a FOLD object\n * @param {number[]} vertices the indices of vertices to be sorted\n * @param {number[]} vector a vector along which to sort vertices\n * @returns {number[]} indices of vertices, in sorted order\n */\nexport const sortVerticesAlongVector = ({ vertices_coords }, vertices, vector) => (\n\tsortPointsAlongVector(\n\t\tvertices.map(v => vertices_coords[v]),\n\t\tvector,\n\t).map(i => vertices[i])\n);\n"
  },
  {
    "path": "src/graph/walk.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Discover a single face by walking counter-clockwise\n * from vertex to vertex until returning from which we began. A second vertex\n * as an input is required to tell the algorithm which direction to begin\n * the walk from the starting vertex. An optional hash table of walked edges\n * exists as an input in case you are trying to build more than one face,\n * initialize an empty object and pass it in every time you call this method.\n * If you use a global \"walkedEdges\" and this method is trying to build a\n * face that was already previously built, the method will return undefined.\n * @param {FOLDExtended} graph a FOLD object with the additional vertices_sectors data\n * @param {number} vertex0 starting vertex\n * @param {number} vertex1 second vertex, this sets the direction of the walk\n * @param {object} [walkedEdges={}] memo object, to prevent walking down\n * duplicate paths, or finding duplicate faces, this dictionary will\n * store and check against vertex pairs \"i j\".\n * @returns {{ vertices: number[], edges: string[], angles?: number[] } | undefined}\n * the walked face, an object arrays of numbers\n * under \"vertices\", \"edges\", and \"angles\", or if you are using a global\n * \"walkedEdges\" hash, if the faces was previously built, returns undefined.\n */\nexport const walkSingleFace = (\n\t{ vertices_vertices, vertices_sectors },\n\tvertex0,\n\tvertex1,\n\twalkedEdges = {},\n) => {\n\t// each time we visit an edge (vertex pair as string, \"4 9\") add it here.\n\t// this gives us a quick lookup to see if we've visited this edge before.\n\tconst thisWalkedEdges = {};\n\n\t// walking the graph, we look at 3 vertices at a time. in sequence:\n\t// prevVertex, currVertex, nextVertex\n\tlet prevVertex = vertex0;\n\tlet currVertex = vertex1;\n\n\tconst face = {\n\t\tvertices: [vertex0],\n\t\tedges: [`${vertex0} ${vertex1}`],\n\t\tangles: [],\n\t};\n\twhile (true) {\n\t\t// even though vertices_vertices are sorted counter-clockwise,\n\t\t// to make a counter-clockwise wound face, when we visit a vertex's\n\t\t// vertices_vertices array we have to select the [n-1] vertex, not [n+1],\n\t\t// it's a little counter-intuitive.\n\n\t\t// within the list of the current vertex's adjacent vertices,\n\t\t// find the location in the array of the previous vertex. Once found,\n\t\t// the vertex at the [-1] index location from it, this vertex is the\n\t\t// next neighbor vertex.\n\t\tconst v_v = vertices_vertices[currVertex];\n\n\t\t// both of these are indices inside the current vertex's vertices_vertices\n\t\tconst fromIndex = v_v.indexOf(prevVertex);\n\t\tconst nextIndex = (fromIndex + v_v.length - 1) % v_v.length;\n\n\t\t// the vertex which we will travel to next, and we can use this to\n\t\t// build the edge key \"v0 v1\" for the next edge we are walking across.\n\t\tconst nextVertex = v_v[nextIndex];\n\t\tconst nextEdgeVertices = `${currVertex} ${nextVertex}`;\n\n\t\t// check if this edge was already walked 2 ways:\n\n\t\t// 1. if the user supplied a non-empty \"walkedEdges\" parameter, make sure\n\t\t// we have not encountered one of these edges. If so, the face we are\n\t\t// trying to build was already previously built, exit and return undefined.\n\t\tif (walkedEdges[nextEdgeVertices]) { return undefined; }\n\n\t\t// 2. if we are seeing an edge that we have previously seen during\n\t\t// the construction of this face, then we are done, return this face.\n\t\tif (thisWalkedEdges[nextEdgeVertices]) {\n\t\t\t// store this face's edges into our global hash\n\t\t\tObject.assign(walkedEdges, thisWalkedEdges);\n\t\t\tface.vertices.pop();\n\t\t\tface.edges.pop();\n\t\t\tif (!face.angles.length) { delete face.angles; }\n\t\t\treturn face;\n\t\t}\n\t\tthisWalkedEdges[nextEdgeVertices] = true;\n\n\t\t// we are not yet done, add the current data, increment the\n\t\t// previous and current vertices, and loop again.\n\t\tface.vertices.push(currVertex);\n\t\tface.edges.push(nextEdgeVertices);\n\t\tif (vertices_sectors) {\n\t\t\tface.angles.push(vertices_sectors[currVertex][nextIndex]);\n\t\t}\n\t\tprevVertex = currVertex;\n\t\tcurrVertex = nextVertex;\n\t}\n};\n\n/**\n * @description Given a planar graph, discover all faces by counter-clockwise\n * walking by starting at every edge.\n * @param {FOLDExtended} graph a FOLD object with the additional vertices_sectors data\n * @returns {{ vertices: number[], edges: string[], angles?: number[] }[]}\n * an array of face objects, where each face\n * has number arrays, \"vertices\", \"edges\", and \"angles\".\n * vertices and edges are indices, angles are radians.\n */\nexport const walkPlanarFaces = ({ vertices_vertices, vertices_sectors }) => {\n\t// walked edges is maintained globally, no walking down the same edge twice\n\tconst walkedEdges = {};\n\tconst graph = { vertices_vertices, vertices_sectors };\n\treturn vertices_vertices\n\t\t.flatMap((adj_verts, v) => adj_verts\n\t\t\t.map(adj_vert => walkSingleFace(graph, v, adj_vert, walkedEdges))\n\t\t\t.filter(a => a !== undefined));\n};\n\n/**\n * @description This should be used in conjuction with walkPlanarFaces() and\n * walkSingleFace(). There will be one face in the which winds around the\n * outside of the boundary and encloses the space outside around. This method will\n * find that face and remove it from the set.\n * Important usage note, the graph provided to walkPlanarFaces should have\n * vertices_sectors computed, otherwise angles will not exist and the\n * boundary face will not be found.\n * @algorithm 180 - sector angle = the turn angle. counter clockwise\n * turns are +, clockwise will be -, this removes the one face that\n * outlines the piece with opposite winding enclosing Infinity.\n * @param {{ vertices: number[], edges: string[], angles?: number[] }[]}\n * walkedFaces the result from calling \"walkPlanarFaces()\"\n * @returns {{ vertices: number[], edges: string[], angles?: number[] }[]}\n * a copy of the same input array with one fewer element\n */\nexport const filterWalkedBoundaryFace = (walkedFaces) => walkedFaces\n\t.filter(face => face.angles\n\t\t.map(a => Math.PI - a)\n\t\t.reduce((a, b) => a + b, 0) > 0);\n"
  },
  {
    "path": "src/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { setWindow } from \"./environment/window.js\";\nimport axiom from \"./axioms/index.js\";\nimport convert from \"./convert/index.js\";\nimport general from \"./general/index.js\";\nimport graph from \"./graph/index.js\";\nimport math from \"./math/index.js\";\nimport singleVertex from \"./singleVertex/index.js\";\nimport svg from \"./svg/index.js\";\nimport webgl from \"./webgl/index.js\";\nimport layer from \"./layer/index.js\";\nimport diagram from \"./diagrams/index.js\";\nimport svgLink from \"./svg/environment/lib.js\";\nimport * as placeholder from \"./types.js\";\n\n/**\n * @description Rabbit Ear, the main entrypoint into the library methods\n */\nconst ear = {\n\taxiom,\n\tconvert,\n\tdiagram,\n\tgeneral,\n\tgraph,\n\tlayer,\n\tmath,\n\tsingleVertex,\n\tsvg,\n\twebgl,\n\t...placeholder,\n};\n\n// bind the SVG library to Rabbit Ear, opening up the ability to\n// draw FOLD objects.\nsvgLink.ear = ear;\n\n// give backend Javascript the ability to use window (draw SVG elements).\n// this gives the user the ability to set the window to a 3rd party library.\nconst earExport = ear;\nObject.defineProperty(earExport, \"window\", {\n\tenumerable: false,\n\tset: value => { svg.window = setWindow(value); },\n});\n\nexport default ear;\n"
  },
  {
    "path": "src/layer/constraints3D.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../environment/messages.js\";\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tedgeFoldAngleIsFlat,\n} from \"../fold/spec.js\";\nimport {\n\tconstraints3DFaceClusters,\n} from \"./constraints3DFaces.js\";\nimport {\n\tconstraints3DEdges,\n\tgetOverlapFacesWith3DEdge,\n\tsolveOverlapFacesWith3DEdge,\n} from \"./constraints3DEdges.js\";\nimport {\n\tmakeTacosAndTortillas,\n} from \"./tacosTortillas.js\";\nimport {\n\tmakeTransitivity,\n\tgetTransitivityTriosFromTacos,\n} from \"./transitivity.js\";\nimport {\n\tmakeConstraintsLookup,\n} from \"./constraintsFlat.js\";\nimport {\n\tsolveFlatAdjacentEdges,\n} from \"./initialSolutionsFlat.js\";\nimport {\n\tmergeWithoutOverwrite,\n} from \"./general.js\";\n\n/**\n * @description Convert a 3D folded graph into the input parameters for the\n * solver including taco-taco, taco-tortilla, tortilla-tortilla,\n * and transitivity constraints.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   constraints: {\n *     taco_taco: TacoTacoConstraint[],\n *     taco_tortilla: TacoTortillaConstraint[],\n *     tortilla_tortilla: TortillaTortillaConstraint[],\n *     transitivity: TransitivityConstraint[],\n *   },\n *   lookup: {\n *     taco_taco: number[][],\n *     taco_tortilla: number[][],\n *     tortilla_tortilla: number[][],\n *     transitivity: number[][],\n *   },\n *   facePairs: string[],\n *   faces_winding: boolean[],\n *   orders: { [key: string]: number },\n * }} all data required for the solver, including:\n * - constraints\n * - lookup: which tells us location of faces inside of constraints\n * - facePairs: all conditions that need to be solved, a list of\n * space-separated pairs of face indices, \"a b\" where a < b.\n * - faces_winding: for every face, which direction is the winding.\n * - orders: solutions to facePairs that were discovered during\n *   the construction of these constraints.\n */\nexport const makeSolverConstraints3D = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment, edges_foldAngle,\n\tfaces_vertices, faces_edges, faces_faces,\n}, epsilon = EPSILON) => {\n\tconst {\n\t\t// planes_transform,\n\t\tfaces_plane,\n\t\t// faces_cluster,\n\t\tfaces_winding,\n\t\tfaces_polygon,\n\t\t// faces_center,\n\t\t// clusters_faces,\n\t\tclusters_graph,\n\t\t// clusters_transform,\n\t\tfacesFacesOverlap,\n\t\tfacePairs,\n\t} = constraints3DFaceClusters({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaces_faces,\n\t}, epsilon);\n\n\tlet edgeResults;\n\ttry {\n\t\tedgeResults = constraints3DEdges({\n\t\t\tvertices_coords,\n\t\t\tedges_vertices,\n\t\t\tedges_faces,\n\t\t\tedges_foldAngle,\n\t\t}, {\n\t\t\tfaces_plane,\n\t\t\tfaces_winding,\n\t\t\tfacesFacesOverlap,\n\t\t}, epsilon);\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\tconst {\n\t\torders: orders3D,\n\t\ttortilla_tortilla: tortilla_tortilla3D,\n\t\ttaco_tortilla: taco_tortilla3D,\n\t} = edgeResults;\n\n\t// get a list of all edge indices which are non-flat edges.\n\t// non-flat edges are anything other than 0, -180, or +180 fold angles.\n\tconst nonFlatEdges = edges_foldAngle\n\t\t.map(edgeFoldAngleIsFlat)\n\t\t.map((flat, i) => (!flat ? i : undefined))\n\t\t.filter(a => a !== undefined);\n\n\t// remove any non-flat edges from the shallow copies.\n\t[\"edges_vertices\", \"edges_faces\", \"edges_assignment\", \"edges_foldAngle\"]\n\t\t.forEach(key => clusters_graph\n\t\t\t.forEach((_, c) => nonFlatEdges\n\t\t\t\t.forEach(e => delete clusters_graph[c][key][e])));\n\n\t// now that we have all faces separated into coplanar-overlapping sets,\n\t// run the 2D taco/tortilla algorithms on each set individually\n\tconst clusters_TacosAndTortillas = clusters_graph\n\t\t.map(el => makeTacosAndTortillas(el, epsilon));\n\n\t// now that we have computed these separately, we can flatten them into the\n\t// same array. The fact that these face pairs are from different 2D planes\n\t// does not matter, the solver simply solves them all at once.\n\tconst taco_taco = clusters_TacosAndTortillas\n\t\t.flatMap(el => el.taco_taco);\n\tconst taco_tortilla = clusters_TacosAndTortillas\n\t\t.flatMap(el => el.taco_tortilla);\n\tconst tortilla_tortilla = clusters_TacosAndTortillas\n\t\t.flatMap(el => el.tortilla_tortilla);\n\n\t// transitivity can be built once, globally. transitivity is built from\n\t// facesFacesOverlap, which inheritely includes the clusters-information by\n\t// the fact that no two faces in different sets overlap one another.\n\tconst tacosTrios = getTransitivityTriosFromTacos({ taco_taco, taco_tortilla });\n\tconst transitivity = makeTransitivity({ faces_polygon }, facesFacesOverlap, epsilon)\n\t\t.filter(trio => tacosTrios[trio.join(\" \")] === undefined);\n\n\t// 3D-tortillas are a constraint that follow the exact same rules as the\n\t// 2D tortilla-tortillas. we can simply add them to the this array.\n\ttaco_tortilla.push(...taco_tortilla3D);\n\ttortilla_tortilla.push(...tortilla_tortilla3D);\n\n\t// this is building a massive lookup table, it takes quite a bit of time.\n\t// any way we can speed this up?\n\tconst lookup = makeConstraintsLookup({\n\t\ttaco_taco,\n\t\ttaco_tortilla,\n\t\ttortilla_tortilla,\n\t\ttransitivity,\n\t});\n\n\t// before we run the solver, solve all of the conditions that we can.\n\t// at this point, this means adjacent faces with an M or V edge between them.\n\t// this is a 2D-only algorithm, we need to run it on each cluster individually\n\tconst adjacentOrders = clusters_graph\n\t\t.map(el => solveFlatAdjacentEdges(el, faces_winding))\n\t\t.reduce((a, b) => Object.assign(a, b), ({}));\n\n\t// solutions where an edge with a 3D fold angle is crossing somewhere\n\t// in the interior of another face, where one of the edge's face's is\n\t// co-planar with the face, this results in a known layer ordering\n\t// between two faces.\n\tconst edgeFace3DOverlaps = getOverlapFacesWith3DEdge(\n\t\t{ edges_faces },\n\t\t{ clusters_graph, faces_plane },\n\t\tepsilon,\n\t);\n\n\tlet orders3DEdgeFace;\n\ttry {\n\t\torders3DEdgeFace = solveOverlapFacesWith3DEdge(\n\t\t\t{ edges_foldAngle },\n\t\t\tedgeFace3DOverlaps,\n\t\t\tfaces_winding,\n\t\t);\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\tlet orders;\n\ttry {\n\t\torders = mergeWithoutOverwrite([\n\t\t\torders3D,\n\t\t\tadjacentOrders,\n\t\t\torders3DEdgeFace,\n\t\t]);\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\treturn {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tlookup,\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t};\n};\n"
  },
  {
    "path": "src/layer/constraints3DEdges.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tarrayArrayToLookupArray,\n} from \"../general/array.js\";\nimport {\n\tconnectedComponentsPairs,\n} from \"../graph/connectedComponents.js\";\nimport {\n\tinvertFlatToArrayMap,\n} from \"../graph/maps.js\";\nimport {\n\tmergeWithoutOverwrite,\n} from \"./general.js\";\nimport {\n\tgetEdgesEdgesCollinearOverlap,\n\tgetFacesEdgesOverlap,\n} from \"../graph/overlap.js\";\n\n/**\n * @description There are two kinds of arrangements of edges/faces that\n * don't generate solver conditions, instead, they solve relationships\n * between pairs of faces.\n * @param {FOLD} graph a FOLD object\n * @param {{ clusters_graph: FOLD[], faces_plane: number[] }} clusterInfo,\n * the result of calling constraints3DFaceClusters()\n * @returns {{ edge: number, tortilla: number, coplanar: number, angled: number}[]}\n * solutions to face-pair layer orders\n */\nexport const getOverlapFacesWith3DEdge = (\n\t{ edges_faces },\n\t{ clusters_graph, faces_plane },\n\tepsilon = EPSILON,\n) => {\n\t// edges that we care about are edges which have two adjacent faces,\n\t// and both adjacent faces are not in the same plane (fold angle is 3D).\n\tconst edgesKeep = edges_faces.map(faces => faces.length === 2\n\t\t&& faces_plane[faces[0]] !== faces_plane[faces[1]]);\n\n\t// makes use of vertices_coords, edges_vertices, faces_vertices, faces_edges\n\tconst clusters_graphNoBoundary = clusters_graph\n\t\t.map(graph => ({\n\t\t\tvertices_coords: graph.vertices_coords,\n\t\t\tedges_vertices: graph.edges_vertices,\n\t\t\tfaces_vertices: graph.faces_vertices,\n\t\t\tfaces_edges: graph.faces_edges,\n\t\t}));\n\n\t// each clusters_graph's vertices_coords have been transformed into 2D,\n\t// compute the faces-edges overlap between only the components within\n\t// each subgraph, then, filter out any boundary edges from the result.\n\t//\n\t// we can't filter out boundary edges before computing faces-edges overlap,\n\t// because boundary edges are needed as a part of each face definition.\n\tconst clustersFacesEdges = clusters_graphNoBoundary\n\t\t.map(graph => getFacesEdgesOverlap(graph, epsilon))\n\t\t.map(facesEdges => facesEdges\n\t\t\t.map(edges => edges.filter(edge => edgesKeep[edge])));\n\n\tconst facesEdges3DInfo = clustersFacesEdges\n\t\t.flatMap(facesEdges => facesEdges\n\t\t\t.flatMap((edges, face) => edges\n\t\t\t\t.map(edge => ({\n\t\t\t\t\tedge,\n\t\t\t\t\tfaces: edges_faces[edge],\n\t\t\t\t\tfacesPlanes: edges_faces[edge].map(f => faces_plane[f]),\n\t\t\t\t\ttortilla: face,\n\t\t\t\t\ttortillaPlane: faces_plane[face],\n\t\t\t\t}))));\n\n\treturn facesEdges3DInfo\n\t\t.map(({ edge, faces, facesPlanes, tortilla, tortillaPlane }) => ({\n\t\t\tedge,\n\t\t\ttortilla,\n\t\t\tcoplanar: faces.filter((_, i) => facesPlanes[i] === tortillaPlane).shift(),\n\t\t\tangled: faces.filter((_, i) => facesPlanes[i] !== tortillaPlane).shift(),\n\t\t}));\n};\n\n/**\n * @description There are two kinds of arrangements of edges/faces that\n * don't generate solver conditions, instead, they solve relationships\n * between pairs of faces.\n * @param {FOLD} graph a FOLD object\n * @param {{ edge: number, tortilla: number, coplanar: number, angled: number}[]}\n * edgeFace3DOverlaps an array of edge-face-3D overlap objects\n * @param {boolean[]} faces_winding for every face true if counter-clockwise.\n * @returns {{ [key: string]: number }} solutions to face-pair layer orders\n */\nexport const solveOverlapFacesWith3DEdge = (\n\t{ edges_foldAngle },\n\tedgeFace3DOverlaps,\n\tfaces_winding,\n) => {\n\t// get the two faces whose order will be solved.\n\tconst facePairs = edgeFace3DOverlaps\n\t\t.map(({ tortilla, coplanar }) => [tortilla, coplanar]);\n\n\t// are the face pairs in the correct order for the solver? where A < B?\n\tconst facePairsCorrectOrder = facePairs.map(([a, b]) => a < b);\n\n\t// order the pair of faces so the smaller index comes first.\n\tfacePairs\n\t\t.map((_, i) => i)\n\t\t.filter(i => !facePairsCorrectOrder[i])\n\t\t.forEach(i => facePairs[i].reverse());\n\n\tconst facePairKeys = facePairs.map(pair => pair.join(\" \"));\n\n\t// true is positive/valley, false is negative/mountain\n\t// a valley places the coplanar face above the tortilla face\n\t// a mountain places the coplanar face below the tortilla face\n\tconst facePairLocalBendDirection = edgeFace3DOverlaps\n\t\t.map(({ edge }) => edges_foldAngle[edge])\n\t\t.map(Math.sign)\n\t\t.map(n => n === 1);\n\n\t// for every face-pair, check the face adjacent to the 3D fold angle,\n\t// is it aligned with the normal of the plane?\n\tconst facePairsAligned = edgeFace3DOverlaps\n\t\t.map(({ coplanar }) => faces_winding[coplanar]);\n\n\t// now, if the coplanar face from the coplanar-angled pair is flipped\n\t// in relation to the plane's normal (the face is upside-down),\n\t// then the bend direction should be inverted. valley = false, M = true.\n\t// This amounts to the XNOR between bend-dir and winding\n\tconst facePairGlobalBendDirection = facePairLocalBendDirection\n\t\t// .map((dir, i) => !(dir ^ facePairsAligned[i]));\n\t\t.map((dir, i) => !(dir !== facePairsAligned[i]));\n\n\t// solver notation, where 1 means A is above B. 2 means B is above A.\n\t// also, if we flipped the order of the faces for the solution key,\n\tconst facePairSolution = facePairGlobalBendDirection\n\t\t.map(bool => (bool ? 1 : 0))\n\t\t.map((result, i) => (facePairsCorrectOrder[i] ? result : 1 - result))\n\t\t.map(result => result + 1);\n\n\t// a dictionary with keys: face pairs (\"5 23\"), and values: 1 or 2.\n\treturn mergeWithoutOverwrite(facePairKeys\n\t\t.map((key, i) => ({ [key]: facePairSolution[i] })));\n};\n\n/**\n * @description Given a situation where two non-boundary edges are\n * parallel overlapping, and two of the faces lie in the same plane,\n * and need to be layer-solved. Consult the fold-angles, which imply the\n * position of the two other faces, this will determine the layer order\n * between the two faces. (given that the special case where both edges\n * are flat 0 angles which would be the tortilla-tortilla 2D case).\n * @param {{ edges_foldAngle: number[], faces_winding: boolean[] }} graph\n * a FOLD object with faces_winding\n * @param {number[]} edges\n * @param {number[]} faces\n * @returns {{ [key: string]: number }} face pair order solution\n */\nexport const solveFacePair3D = ({ edges_foldAngle, faces_winding }, edges, faces) => {\n\t// so the idea is, if we align the faces within the plane, meaning that if\n\t// a face is flipped, we \"flip\" it by negating the foldAngle, then we can\n\t// say that the face (edge) with the larger fold angle should be on top.\n\t// then we have to work backwards-if we flipped a face, we need to flip\n\t// the order, or if the faces are not ordered where A < B, also flip the order.\n\tconst facesAligned = faces.map(face => faces_winding[face]);\n\tconst edgesFoldAngleAligned = edges\n\t\t.map(edge => edges_foldAngle[edge])\n\t\t.map((angle, i) => (facesAligned[i] ? angle : -angle));\n\tconst indicesInOrder = edgesFoldAngleAligned[0] > edgesFoldAngleAligned[1];\n\tconst facesInOrder = faces[0] < faces[1];\n\t// 0 becomes 1, 1 becomes 2.\n\t// const orderValue = (indicesInOrder ^ facesInOrder) + 1;\n\tconst orderValue = (indicesInOrder !== facesInOrder) ? 2 : 1;\n\tconst faceKey = (facesInOrder\n\t\t? faces.join(\" \")\n\t\t: [faces[1], faces[0]].join(\" \"));\n\treturn { [faceKey]: orderValue };\n};\n\n/**\n * @param {{\n *   edges_faces: number[][],\n *   faces_plane: number[],\n *   facesFacesLookup: boolean[][],\n * }} info\n * - 0: bent (3D)\n * - 1: tortilla\n * - 2: taco\n * @returns {number[]} a classification for edges\n */\nconst getEdgesAngleClass = ({ edges_faces, faces_plane, facesFacesLookup }) => {\n\t/** @type {number[]} for every edge, a 0, 1, or 2 */\n\tconst edges_angleClass = [];\n\n\t// filter only edges with two adjacent faces\n\tconst degreeTwoEdges = edges_faces\n\t\t.map((_, edge) => edge)\n\t\t.filter(edge => edges_faces[edge].length === 2);\n\n\t// filter only edges whose both faces are in the same plane\n\tdegreeTwoEdges\n\t\t.filter(e => faces_plane[edges_faces[e][0]] === faces_plane[edges_faces[e][1]])\n\t\t.forEach(edge => {\n\t\t\tconst [f0, f1] = edges_faces[edge];\n\t\t\tedges_angleClass[edge] = facesFacesLookup[f0][f1] ? 2 : 1;\n\t\t});\n\n\t// filter only edges whose both faces are not in the same plane\n\tdegreeTwoEdges\n\t\t.filter(e => faces_plane[edges_faces[e][0]] !== faces_plane[edges_faces[e][1]])\n\t\t.forEach(edge => { edges_angleClass[edge] = 0; });\n\treturn edges_angleClass;\n};\n\n/**\n * @description Given an overlapping pair of edges, classify it based on the\n * arrangements of the four faces in 3D space. The four faces\n * - 0: flat, or no overlaps to solve (ignore)\n * - 1: T-junction (generates order)\n * - 2: Y-junction (generates order)\n * - 3: bent-flat-tortillas (generates order)\n * - 4: bent-tortillas (generates tortilla_tortilla)\n * - 5: bent-tortilla-flat-taco (generates taco_tortilla)\n * - 6: unclassified (something went wrong)\n * @param {{\n *   faces: [number, number, number, number],\n *   planes: [number, number, number, number],\n *   angleClasses: [number, number],\n * }} for each edge, it's\n * - faces: four faces with stride matching the edges [e0, e0, e1, e1],\n * - planes: for every face (4), the index of the plane the face lies in\n * - angleClasses:\n * @param {boolean[][]} facesFacesLookup\n * @returns {number} 0-6 class id number\n */\nconst classifyEdgePair = ({ faces, planes, angleClasses }, facesFacesLookup) => {\n\tconst [c0, c1] = angleClasses;\n\tconst [f0, f1, f2, f3] = faces;\n\tconst [p0, p1, p2, p3] = planes;\n\n\t// additional:\n\t// - four-plane: A-B and C-D\n\n\tif (c0 && c1) { return 0; }\n\n\t// true if any one pair's face overlaps any face from the other pair.\n\tconst interPairOverlap = facesFacesLookup[f0][f2]\n\t\t|| facesFacesLookup[f0][f3]\n\t\t|| facesFacesLookup[f1][f2]\n\t\t|| facesFacesLookup[f1][f3];\n\n\tif (!interPairOverlap) { return 0; }\n\n\t// (letters are face planes in 3D: A, B, C, ...)\n\n\t// bent-flat tortillas\n\t// true if either includes \"tortilla\" and interPairOverlap\n\t// A1-A2 and A3-B, where A1-A2 do NOT overlap,\n\t// but A3 does overlap with either A1 or A2\n\tif (c0 === 1 || c1 === 1) { return 3; }\n\n\t// bent-tortilla-flat-taco\n\t// true if either includes \"taco\" and interPairOverlap\n\t// A1-A2 and A3-B, where A1-A2-A3 all overlap\n\tif (c0 === 2 || c1 === 2) { return 5; }\n\n\t// Y-junction\n\t// A1-B and A2-C, where A1-A2 overlap\n\tif ((p0 === p2 && p1 !== p3 && facesFacesLookup[f0][f2])\n\t\t|| (p0 === p3 && p1 !== p2 && facesFacesLookup[f0][f3])\n\t\t|| (p1 === p2 && p0 !== p3 && facesFacesLookup[f1][f2])\n\t\t|| (p1 === p3 && p0 !== p2 && facesFacesLookup[f1][f3])) {\n\t\treturn 2;\n\t}\n\n\t// T-junction\n\t// A1-B1 and A2-B2, where A1-A2 overlap and B1-B2 do NOT overlap\n\tif ((p0 === p2 && p1 === p3 && facesFacesLookup[f0][f2] && !facesFacesLookup[f1][f3])\n\t\t|| (p0 === p3 && p1 === p2 && facesFacesLookup[f0][f3] && !facesFacesLookup[f1][f2])\n\t\t|| (p1 === p2 && p0 === p3 && facesFacesLookup[f1][f2] && !facesFacesLookup[f0][f3])\n\t\t|| (p1 === p3 && p0 === p2 && facesFacesLookup[f1][f3] && !facesFacesLookup[f0][f2])) {\n\t\treturn 1;\n\t}\n\n\t// bent-tortillas\n\t// A1-B1 and A2-B2, where A1-A2 overlap and B1-B2 overlap\n\tif ((p0 === p2 && p1 === p3 && facesFacesLookup[f0][f2] && facesFacesLookup[f1][f3])\n\t\t|| (p0 === p3 && p1 === p2 && facesFacesLookup[f0][f3] && facesFacesLookup[f1][f2])\n\t\t|| (p1 === p2 && p0 === p3 && facesFacesLookup[f1][f2] && facesFacesLookup[f0][f3])\n\t\t|| (p1 === p3 && p0 === p2 && facesFacesLookup[f1][f3] && facesFacesLookup[f0][f2])) {\n\t\treturn 4;\n\t}\n\n\t// unclassified\n\treturn 6;\n};\n\n/**\n * @description Given a FOLD graph in 3D, find all overlapping edge pairs\n * which create solvable face-pairs between faces adjacent to the edges.\n * Important: ensure that all edgePairs are edges which have been\n * pre-established to be edges which have two adjacent faces (no boundary edges)\n * @param {{\n *   edges_faces: number[][],\n *   edgePairs: [number, number][],\n *   faces_plane: number[],\n *   facesFacesLookup: boolean[][],\n * }} edgeAndFaceInfo\n * @returns {{\n *   tJunctions: number[],\n *   yJunctions: number[],\n *   bentFlatTortillas: number[],\n *   bentTortillas: number[],\n *   bentTortillasFlatTaco: number[],\n * }} for each class, a list of edgePairs indices which fit under this class.\n */\nexport const getSolvable3DEdgePairs = ({\n\tedges_faces,\n\tfaces_plane,\n\tedgePairs,\n\tfacesFacesLookup,\n}) => {\n\tconst edges_angleClass = getEdgesAngleClass({\n\t\tedges_faces, faces_plane, facesFacesLookup,\n\t});\n\n\t// gather info regarding each edgePair to pass off to the classification\n\t// edge pairs has been\n\t/**\n\t * @type {{\n\t *   faces: [number, number, number, number],\n\t *   planes: [number, number, number, number],\n\t *   angleClasses: [number, number],\n\t * }[]}\n\t * the second map function is there to satisfy the typescript linter\n\t */\n\tconst edgePairsClassInfo = edgePairs.map(edges => ({\n\t\tfaces: edges.flatMap(e => edges_faces[e]),\n\t\tplanes: edges.flatMap(e => edges_faces[e].map(face => faces_plane[face])),\n\t\tangleClasses: edges.map(edge => edges_angleClass[edge]),\n\t})).map(({ faces, planes, angleClasses }) => ({\n\t\tfaces: [faces[0], faces[1], faces[2], faces[3]],\n\t\tplanes: [planes[0], planes[1], planes[2], planes[3]],\n\t\tangleClasses: [angleClasses[0], angleClasses[1]],\n\t}));\n\n\t// each edgePair gets one of these assignments.\n\t// 0: ignore the first entry\n\t// 1: T-junction (generates order)\n\t// 2: Y-junction (generates order)\n\t// 3: bent-flat-tortillas (generates order)\n\t// 4: bent-tortillas (generates tortilla_tortilla)\n\t// 5: bent-tortilla-flat-taco (generates taco_tortilla)\n\tconst edgePairsClass = edgePairsClassInfo\n\t\t.map(info => classifyEdgePair(info, facesFacesLookup));\n\n\t// invert map to collate each edgePair index into its class array\n\tconst [\n\t\t,\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t\tuncaught,\n\t] = invertFlatToArrayMap(edgePairsClass);\n\n\tif (uncaught && uncaught.length) {\n\t\tconsole.warn(\"getSolvable3DEdgePairs uncaught edge pairs\");\n\t}\n\n\treturn {\n\t\ttJunctions: (tJunctions || []),\n\t\tyJunctions: (yJunctions || []),\n\t\tbentFlatTortillas: (bentFlatTortillas || []),\n\t\tbentTortillas: (bentTortillas || []),\n\t\tbentTortillasFlatTaco: (bentTortillasFlatTaco || []),\n\t};\n};\n\n/**\n * @description Given a FOLD object which has 3D edges, generate an additional\n * set of solutions and constraints in preparation of solving the layer order,\n * the set of solutions includes solved orderings between pairs of faces, and\n * the additional constraints includes taco-tortilla and tortilla-tortilla.\n * @param {FOLD} graph a FOLD object\n * @param {{\n *   faces_plane: number[],\n *   faces_winding: boolean[],\n *   facesFacesOverlap: number[][],\n * }} info additional graph information where\n * - faces_plane: for every face, which plane does it inhabit\n * - faces_winding: for every face, is it aligned in its plane (true) or flipped\n * - facesFacesOverlap: for every face, a list of other faces which overlap.\n * @returns {{\n *   orders: { [key: string]: number },\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n * }}\n */\nexport const constraints3DEdges = ({\n\tvertices_coords,\n\tedges_vertices,\n\tedges_faces,\n\tedges_foldAngle,\n}, {\n\tfaces_plane,\n\tfaces_winding,\n\tfacesFacesOverlap,\n}, epsilon = EPSILON) => {\n\t// a copy of edges_vertices with only edges with two adjacent faces\n\tconst edges_vertices2 = edges_vertices.slice();\n\tedges_faces\n\t\t.map((_, e) => e)\n\t\t.filter(e => edges_faces[e].length !== 2)\n\t\t.forEach(e => delete edges_vertices2[e]);\n\n\t// all edge-edge overlap between pairs of edges, only including edges\n\t// which have two adjacent faces.\n\tconst edgesEdgesOverlap = getEdgesEdgesCollinearOverlap({\n\t\tvertices_coords, edges_vertices: edges_vertices2,\n\t}, epsilon);\n\n\t// all pairings of overlapping edges, only including edges which have\n\t// two adjacent faces. otherwise, this including pairs of edges which\n\t// hold no value to us at this stage\n\tconst edgePairs = connectedComponentsPairs(edgesEdgesOverlap);\n\n\tconst facesFacesLookup = arrayArrayToLookupArray(facesFacesOverlap);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs({\n\t\tedges_faces,\n\t\tfaces_plane,\n\t\tedgePairs,\n\t\tfacesFacesLookup,\n\t});\n\n\t/**\n\t * @description Given a pair of edges, already established to be the case\n\t * where one face from each edge (and only one) lie coplanar together,\n\t * solve the order between these two faces by checking the fold angle\n\t * of the faces with the two other non-overlapping faces.\n\t * @param {[number, number]} edges a pair of overlapping edges\n\t * already established where one face from each edge (and only one)\n\t * lie coplanar.\n\t * @returns {{ [key: string]: number }} a face-pair solution for the solver\n\t * where the key is a space-separated pair of face indices, and the value\n\t * is a 1 or 2.\n\t */\n\tconst solveYTJunction = (edges) => {\n\t\tconst [f0, f1, f2, f3] = edges.flatMap(e => edges_faces[e]);\n\t\t// one face from edge[0] (f0 or f1) should be overlapping with another\n\t\t// face from edge[1] (f2 or f3). find and get this pair of faces.\n\t\tconst faces = [[f0, f2], [f0, f3], [f1, f2], [f1, f3]]\n\t\t\t.filter(([a, b]) => facesFacesLookup[a][b])\n\t\t\t.shift();\n\t\tif (!faces) { return undefined; }\n\t\treturn solveFacePair3D({ edges_foldAngle, faces_winding }, edges, faces);\n\t};\n\n\t/**\n\t * @description This creates a taco-tortilla constraint\n\t * @param {[number, number]} edges a pair of overlapping edges\n\t * already established to form a bent-tortilla-flat-taco combo.\n\t * @returns {TacoTortillaConstraint | undefined}\n\t */\n\tconst makeBentTortillaFlatTaco = (edges) => {\n\t\tconst [f0, f1, f2, f3] = edges.flatMap(e => edges_faces[e]);\n\t\t// every permutation of 3 faces (the fourth face is ignored in the overlap)\n\t\t// the order of the 3 faces is already in TacoTortillaConstraint form, where\n\t\t// the first and last are connected (taco) and the middle face is the tortilla\n\t\tconst result = [[f0, f2, f1], [f2, f1, f3], [f2, f0, f3], [f0, f3, f1]]\n\t\t\t.filter(([a, b, c]) => facesFacesLookup[a][b] && facesFacesLookup[a][c])\n\t\t\t.shift();\n\t\treturn result ? [result[0], result[1], result[2]] : undefined;\n\t};\n\n\t/**\n\t * @description This creates a tortilla-tortilla constraint\n\t * @param {[number, number]} edges a pair of overlapping edges\n\t * already established to form a bent-tortilla combo.\n\t * @returns {TortillaTortillaConstraint}\n\t */\n\tconst makeBentTortillas = (edges) => {\n\t\tconst faces = edges.flatMap(edge => edges_faces[edge]);\n\t\t/** @type {[number, number, number, number]} */\n\t\tconst tortillas = [faces[0], faces[1], faces[2], faces[3]];\n\n\t\t// we now have a tortilla-tortilla pair in the form of [A, B, X, Y]\n\t\t// where A-B are connected, and X-Y are connected, now we need to make sure\n\t\t// that A is above/below X, and B is above/below Y.\n\t\t// if A is not in the same plane as X, reverse the X and Y.\n\t\tif (faces_plane[tortillas[0]] !== faces_plane[tortillas[2]]) {\n\t\t\t[tortillas[2], tortillas[3]] = [tortillas[3], tortillas[2]];\n\t\t}\n\n\t\t// Finally, each planar cluster chose a normal for that plane at random,\n\t\t// (the normal from whichever was the first face from that cluster).\n\t\t// Because these are bent tortillas, it's possible that the normal direction\n\t\t// flips moving from one face to the other inside of one tortilla.\n\t\t// The solver works by placing a face \"above\" or \"below\" the other, and\n\t\t// repeating that same action on the other side of the edge, but if the\n\t\t// normal flips, \"below\" and \"above\" flip too, and if we don't fix this,\n\t\t// the solver will have just created a self-intersecting tortilla-tortilla.\n\t\t// The solution is to check faces A and B's winding direction, do they match?\n\t\t// If they don't match, swap B and Y, so that the result is [A, Y, X, B]\n\t\t// which no longer means that each pair shares an edge in common, but that\n\t\t// does not matter.\n\t\tif (faces_winding[tortillas[0]] !== faces_winding[tortillas[1]]) {\n\t\t\t// swap face order [A, B, X, Y] into [A, Y, X, B]\n\t\t\t[tortillas[1], tortillas[3]] = [tortillas[3], tortillas[1]];\n\t\t}\n\t\treturn tortillas;\n\t};\n\n\tconst tortilla_tortilla = bentTortillas\n\t\t.map(i => edgePairs[i])\n\t\t.map(makeBentTortillas);\n\n\t// taco-tortilla constraints could be undefined, filter these out.\n\tconst taco_tortilla = bentTortillasFlatTaco\n\t\t.map(i => edgePairs[i])\n\t\t.map(makeBentTortillaFlatTaco)\n\t\t.filter(a => a !== undefined);\n\n\t// T-Junctions, Y-Junctions, and bent-flat-tortillas all get solved\n\t// by passing them into the same \"solveYTJunction\" method.\n\tconst arrayOfOrders = [...tJunctions, ...yJunctions, ...bentFlatTortillas]\n\t\t.map(i => edgePairs[i])\n\t\t.map(solveYTJunction);\n\n\tconst orders = mergeWithoutOverwrite(arrayOfOrders);\n\n\treturn {\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t};\n};\n"
  },
  {
    "path": "src/layer/constraints3DFaces.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\taverage2,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tmultiplyMatrix4Vector3,\n} from \"../math/matrix4.js\";\nimport {\n\tmergeArraysWithHoles,\n} from \"../general/array.js\";\nimport {\n\tmakeFacesPolygon,\n} from \"../graph/make/faces.js\";\nimport {\n\tconnectedComponentsPairs,\n} from \"../graph/connectedComponents.js\";\nimport {\n\tgetCoplanarAdjacentOverlappingFaces,\n} from \"../graph/faces/planes.js\";\nimport {\n\tsubgraphWithFaces,\n} from \"../graph/subgraph.js\";\nimport {\n\tgetFacesFacesOverlap,\n} from \"../graph/overlap.js\";\n\n/**\n * @description The first subroutine to initialize solver constraints for a\n * 3D model. This method locates all coplanar-overlapping clusters of faces\n * \"clusters\", clone one subgraph per cluster which contain only components\n * from this cluster's faces, rotate these graphs's vertices to place them\n * into the XY plane, and compute the overlap state between every pair of faces.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   planes_transform: number[][],\n *   faces_plane: number[],\n *   faces_cluster: number[],\n *   faces_winding: boolean[],\n *   faces_polygon: [number, number][][],\n *   faces_center: [number, number][],\n *   clusters_faces: number[][],\n *   clusters_graph: FOLD[],\n *   clusters_transform: number[][],\n *   facesFacesOverlap: number[][],\n *   facePairs: string[],\n * }}\n */\nexport const constraints3DFaceClusters = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment, edges_foldAngle,\n\tfaces_vertices, faces_edges, faces_faces,\n}, epsilon = EPSILON) => {\n\t// cluster faces into coplanar-adjacent-overlapping sets. this creates:\n\t// - \"planes\": every unique plane that at least one face inhabits\n\t// - \"clusters\": a coplanar set of faces, multiple of these clusters can be\n\t//   from the same plane, but individual clusters do not overlap each other.\n\tconst {\n\t\t// planes,\n\t\t// planes_faces,\n\t\tplanes_transform,\n\t\t// planes_clusters,\n\t\tfaces_winding,\n\t\tfaces_plane,\n\t\tfaces_cluster,\n\t\tclusters_plane,\n\t\tclusters_faces,\n\t} = getCoplanarAdjacentOverlappingFaces({\n\t\tvertices_coords, faces_vertices, faces_faces,\n\t}, epsilon);\n\n\t// for each cluster, get the transform which, when applied, brings\n\t// all points into the XY plane.\n\tconst clusters_transform = clusters_plane.map(p => planes_transform[p]);\n\n\t// for every cluster, make a shallow copy of the input graph, containing\n\t// only the faces included in that cluster, and by extension, all edges and\n\t// vertices which are used by this subset of faces.\n\t/** @type {FOLDExtended[]} */\n\tconst clusters_graph = clusters_faces.map(faces => subgraphWithFaces({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaces_faces,\n\t}, faces));\n\n\t// ensure all vertices_coords are 3D (make a copy array here) for use in\n\t// multiplyMatrix4Vector3, which requires points to be in 3D.\n\tconst vertices_coords3D = vertices_coords.map(resize3);\n\n\t// transform all vertices_coords by the inverse transform\n\t// to bring them all into the XY plane. convert back into a 2D point.\n\tclusters_graph.forEach(({ vertices_coords: coords }, c) => {\n\t\tclusters_graph[c].vertices_coords = coords\n\t\t\t.map((_, v) => multiplyMatrix4Vector3(\n\t\t\t\tclusters_transform[c],\n\t\t\t\tvertices_coords3D[v],\n\t\t\t))\n\t\t\t.map(([x, y]) => [x, y]);\n\t});\n\n\t// now, any arrays referencing edges (_edges) are out of sync with\n\t// the edge arrays themselves (edges_). Therefore this method really\n\t// isn't intended to be used outside of this higly specific context.\n\n\t// faces_polygon is a flat array of polygons in 2D, where every face\n\t// is re-oriented into 2D via each set's transformation.\n\t// collinear vertices (if exist) are removed from every polygon.\n\t/** @type {[number, number][][]} */\n\tconst faces_polygon = mergeArraysWithHoles(...clusters_graph\n\t\t.map(copy => makeFacesPolygon(copy, epsilon)));\n\n\t// simple faces center by averaging all the face's vertices.\n\t// we don't have to be precise here, these are used to tell which\n\t// side of a face's edge the face is (assuming all faces are convex).\n\tconst faces_center = faces_polygon.map(coords => average2(...coords));\n\n\t// populate individual graph copies with faces_center data.\n\tclusters_graph.forEach(({ faces_vertices: fv }, c) => {\n\t\tclusters_graph[c].faces_center = fv.map((_, f) => faces_center[f]);\n\t});\n\n\t// ensure that all faces are counter-clockwise, flip winding if necessary.\n\tfaces_winding\n\t\t.map((upright, i) => (upright ? undefined : i))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(f => faces_polygon[f].reverse());\n\n\t// for every face, a list of face indices which overlap this face.\n\t// compute face-face-overlap for every cluster's graph one at a time,\n\t// this is important because vertices have been translated into 2D now,\n\t// and it's possible that faces from other clusters overlap each other\n\t// in this transformed state; we don't want that. After we compute face-face\n\t// overlap information separately, we can merge all of the results into\n\t// a flat array since none of the resulting arrays will overlap.\n\t/** @type {number[][]} */\n\tconst facesFacesOverlap = mergeArraysWithHoles(...clusters_graph\n\t\t.map(graph => getFacesFacesOverlap(graph, epsilon)));\n\n\t// these are all the variables we need to solve- all overlapping faces in\n\t// pairwise combinations, as a space-separated string, smallest index first\n\tconst facePairs = connectedComponentsPairs(facesFacesOverlap)\n\t\t.map(pair => pair.join(\" \"));\n\n\treturn {\n\t\tplanes_transform,\n\t\tfaces_plane,\n\t\tfaces_cluster,\n\t\tfaces_winding,\n\t\tfaces_polygon,\n\t\tfaces_center,\n\t\tclusters_faces,\n\t\tclusters_graph,\n\t\tclusters_transform,\n\t\tfacesFacesOverlap,\n\t\tfacePairs,\n\t};\n};\n"
  },
  {
    "path": "src/layer/constraintsFlat.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tconnectedComponentsPairs,\n} from \"../graph/connectedComponents.js\";\nimport {\n\tgetFacesFacesOverlap,\n} from \"../graph/overlap.js\";\nimport {\n\tmakeFacesWinding,\n} from \"../graph/faces/winding.js\";\nimport {\n\tmakeFacesPolygon,\n} from \"../graph/make/faces.js\";\nimport {\n\tsolveFlatAdjacentEdges,\n} from \"./initialSolutionsFlat.js\";\nimport {\n\ttacoTypeNames,\n\temptyCategoryObject,\n\tconstraintToFacePairsStrings,\n} from \"./general.js\";\nimport {\n\tmakeTacosAndTortillas,\n} from \"./tacosTortillas.js\";\nimport {\n\tmakeTransitivity,\n\tgetTransitivityTriosFromTacos,\n} from \"./transitivity.js\";\n\n/**\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints\n * @returns {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} for a particular model, a set of layer constraints sorted into\n * taco/tortilla/transitivity types, needed to be solved.\n */\nexport const makeConstraintsLookup = (constraints) => {\n\tconst lookup = emptyCategoryObject();\n\t// fill the top layer with \"taco / tortilla\" category names\n\ttacoTypeNames.forEach(key => { lookup[key] = {}; });\n\n\t// fill every entry with an empty array\n\ttacoTypeNames\n\t\t.forEach(type => constraints[type]\n\t\t\t.forEach(constraint => constraintToFacePairsStrings[type](constraint)\n\t\t\t\t.forEach(key => { lookup[type][key] = []; })));\n\n\t// populate arrays\n\ttacoTypeNames\n\t\t.forEach(type => constraints[type]\n\t\t\t.forEach((constraint, i) => constraintToFacePairsStrings[type](constraint)\n\t\t\t\t.forEach(key => lookup[type][key].push(i))));\n\n\treturn lookup;\n};\n\n/**\n * @description Convert a folded graph into the input parameters for the solver\n * including taco-taco, taco-tortilla, tortilla-tortilla, and transitivity\n * constraints.\n * @param {FOLDExtended} graph a FOLD object\n * @param {number} [epsilon=1e-6] optional epsilon. it will be calculated\n * if you leave this empty.\n * @returns {{\n *   constraints: {\n *     taco_taco: TacoTacoConstraint[],\n *     taco_tortilla: TacoTortillaConstraint[],\n *     tortilla_tortilla: TortillaTortillaConstraint[],\n *     transitivity: TransitivityConstraint[],\n *   },\n *   lookup: {\n *     taco_taco: number[][],\n *     taco_tortilla: number[][],\n *     tortilla_tortilla: number[][],\n *     transitivity: number[][],\n *   },\n *   facePairs: string[],\n *   faces_winding: boolean[],\n *   orders: { [key: string]: number },\n * }} all data required for the solver, including:\n * - constraints\n * - lookup: which tells us location of faces inside of constraints\n * - facePairs: all conditions that need to be solved, a list of\n * space-separated pairs of face indices, \"a b\" where a < b.\n * - faces_winding: for every face, which direction is the winding\n */\nexport const makeSolverConstraintsFlat = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment,\n\tfaces_vertices, faces_edges, faces_center,\n}, epsilon = EPSILON) => {\n\t// create a polygon (array of points) for every face. ensure that\n\t// every polygon has the same winding (reverse if necessary).\n\tconst faces_winding = makeFacesWinding({ vertices_coords, faces_vertices });\n\tconst faces_polygon = makeFacesPolygon({ vertices_coords, faces_vertices }, epsilon);\n\tfaces_winding\n\t\t.map((upright, i) => (upright ? undefined : i))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(f => faces_polygon[f].reverse());\n\n\t// for every face (index) get an array of other faces (value) which\n\t// overlap this face. This array can contain holes.\n\tconst faces_facesOverlap = getFacesFacesOverlap({\n\t\tvertices_coords, faces_vertices,\n\t}, epsilon);\n\n\t// create all of the tacos/tortillas constraints that\n\t// will be used by the solver.\n\tconst {\n\t\ttaco_taco, taco_tortilla, tortilla_tortilla,\n\t} = makeTacosAndTortillas({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaces_center,\n\t}, epsilon);\n\n\t// ...and the transitivity constraints\n\tconst tacosTrios = getTransitivityTriosFromTacos({ taco_taco, taco_tortilla });\n\tconst transitivity = makeTransitivity({ faces_polygon }, faces_facesOverlap, epsilon)\n\t\t.filter(trio => tacosTrios[trio.join(\" \")] === undefined);\n\n\t// these are all the variables we need to solve- all overlapping faces in\n\t// pairwise combinations, as a space-separated string, smallest index first\n\tconst facePairs = connectedComponentsPairs(faces_facesOverlap)\n\t\t.map(pair => pair.join(\" \"));\n\n\t// this is building a massive lookup table, it takes quite a bit of time.\n\t// any way we can speed this up?\n\tconst lookup = makeConstraintsLookup({\n\t\ttaco_taco,\n\t\ttaco_tortilla,\n\t\ttortilla_tortilla,\n\t\ttransitivity,\n\t});\n\n\t// before we run the solver, solve all of the conditions that we can.\n\t// at this point, this means adjacent faces with an M or V edge between them.\n\tconst orders = solveFlatAdjacentEdges({\n\t\tedges_faces,\n\t\tedges_assignment,\n\t}, faces_winding);\n\n\treturn {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tlookup,\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t};\n};\n"
  },
  {
    "path": "src/layer/facesSide.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tcross2,\n\tsubtract2,\n\tresize3,\n} from \"../math/vector.js\";\nimport {\n\tmultiplyMatrix4Vector3,\n\tmultiplyMatrix4Line3,\n} from \"../math/matrix4.js\";\nimport {\n\tpointsToLine,\n} from \"../math/convert.js\";\nimport {\n\tedgeToLine2,\n} from \"../graph/edges/lines.js\";\nimport {\n\tmakeFacesCenter2DQuick,\n\tmakeFacesCenter3DQuick,\n} from \"../graph/make/faces.js\";\nimport {\n\tuniqueElements,\n} from \"../general/array.js\";\nimport { invertFlatToArrayMap } from \"../graph/maps.js\";\n\n/**\n * @description An edge is adjacent to one or two faces,\n * this is stored in its edges_faces entry. for each of these\n * faces, which side of the edge (using the edge's vector)\n * is each face on. each result is an array of length\n * matching the number of adjacent edges.\n * @param {FOLDExtended} graph the fold object with edges_faces and faces_center\n * @returns {number[][]} for every edge, for each of its 1 or 2 adjacent faces,\n * a +1 or -1 for each face indicating which side of the edge the face lies on.\n */\nexport const makeEdgesFacesSide = ({\n\tvertices_coords, edges_vertices, edges_faces, faces_center,\n}) => {\n\t// convert edges into a line form with a vector and origin\n\tconst edgesLine = edges_vertices\n\t\t.map(verts => verts.map(v => vertices_coords[v]))\n\t\t.map(([a, b]) => pointsToLine(a, b));\n\n\t// for each edge, for each of it's adjacent faces (either 1 or 2),\n\t// which side of the edge's vector does that face lie on?\n\t// compute the cross product of the edge's vector with the vector\n\t// from the edge origin to the face's center. return +1 or -1\n\treturn edges_faces\n\t\t.map((faces, i) => faces\n\t\t\t.map(face => cross2(\n\t\t\t\tsubtract2(faces_center[face], edgesLine[i].origin),\n\t\t\t\tedgesLine[i].vector,\n\t\t\t))\n\t\t\t.map(cross => Math.sign(cross)));\n};\n\n/**\n * @description having already pre-computed a the tacos in the form of\n * edges and the edges' adjacent faces, give each face a +1 or -1 based\n * on which side of the edge it is on. \"side\" determined by the cross-\n * product against the edge's vector. This method works in 2D only.\n * @param {FOLDExtended} graph the fold graph with faces_center\n * @param {[number, number][]} edgePairs\n * @returns {[[number,number],[number,number]][]}\n */\nexport const makeEdgePairsFacesSide = (\n\t{ vertices_coords, edges_vertices, edges_faces, faces_center },\n\tedgePairs,\n) => {\n\t// there are two edges involved in a taco, grab the first one.\n\t// we have to use the same origin/vector so that the face-sidedness is\n\t// consistent globally, not local to its edge.\n\tconst edgePairsLine = edgePairs\n\t\t.map(([edge]) => edgeToLine2({ vertices_coords, edges_vertices }, edge));\n\n\treturn edgePairs\n\t\t// convert pairs of edges into pairs of face-pairs (two faces for each edge)\n\t\t.map(pair => pair.map(edge => edges_faces[edge]))\n\t\t// cross product of every edge-pair's line vector against\n\t\t// every face's vector (vector from edge origin to face center)\n\t\t.map((faces, i) => faces\n\t\t\t.map(face_pair => face_pair\n\t\t\t\t.map(face => faces_center[face])\n\t\t\t\t.map(center => cross2(\n\t\t\t\t\tsubtract2(center, edgePairsLine[i].origin),\n\t\t\t\t\tedgePairsLine[i].vector,\n\t\t\t\t))\n\t\t\t\t.map(Math.sign)))\n\t\t.map(arr => [[arr[0][0], arr[0][1]], [arr[1][0], arr[1][1]]]);\n};\n\n/**\n * @description For every edge in a 2D graph, for each of its faces, get\n * which side of the edge this face lies along. \"Sidedness\" is simply local\n * to each edge, similar or collinear edges will not have a consistent side\n * between them.\n * @param {FOLDExtended} graph a FOLD object where, if faces_center exists then\n * only edges_faces is needed, otherwise vertices and face data is needed.\n * @param {{\n *   lines: VecLine[],\n *   edges_line: number[],\n *   faces_plane: number[],\n *   planes_transform: number[][],\n * }} edgesLine-facesPlane-data the joined-results from\n *   getEdgesLine() and getFacesPlane()\n * @returns {number[][]} for every edge, for each of its 1 or 2 adjacent faces,\n * a +1 or -1 for each face indicating which side of the edge the face lies on.\n */\nexport const makeEdgesFacesSide2D = (\n\t{ vertices_coords, edges_faces, faces_vertices, faces_center },\n\t{ lines, edges_line },\n) => {\n\tif (!faces_center) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_center = makeFacesCenter2DQuick({ vertices_coords, faces_vertices });\n\t}\n\n\t// for every edge, for all of its (0, 1, 2) adjacent faces, compute the\n\t// cross product of the vector to the faces' center with the vector of\n\t// the shared line that this edge runs along.\n\t// the result is +1 or -1, the sign of the result of the cross product.\n\treturn edges_faces\n\t\t.map((faces, e) => faces\n\t\t\t.map(face => {\n\t\t\t\tconst { vector, origin } = lines[edges_line[e]];\n\t\t\t\treturn cross2(subtract2(faces_center[face], origin), vector);\n\t\t\t})\n\t\t\t.map(Math.sign));\n};\n\n/**\n * @description For every edge, for each of its 1 or 2 adjacent faces,\n * get which side of the edge this face lies on. All collinear edges\n * are established to have a similar direction for sidedness, and this\n * method works in 3D.\n * @param {FOLDExtended} graph a FOLD object where, if faces_center exists then\n * only edges_faces is needed, otherwise vertices and face data is needed.\n * @param {{\n *   lines: VecLine[],\n *   edges_line: number[],\n *   faces_plane: number[],\n *   planes_transform: number[][],\n * }} edgesLine-facesPlane-data the joined-results from\n *   getEdgesLine() and getFacesPlane()\n * @returns {number[][]} for every edge, for each of its 1 or 2 adjacent faces,\n * a +1 or -1 for each face indicating which side of the edge the face lies on.\n */\nexport const makeEdgesFacesSide3D = (\n\t{ vertices_coords, edges_faces, faces_vertices, faces_center },\n\t{ lines, edges_line, planes_transform, faces_plane },\n) => {\n\tif (!faces_center) {\n\t\t// this method will always return 3D points, necessary for the\n\t\t// multiply matrix and vector method\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_center = makeFacesCenter3DQuick({ vertices_coords, faces_vertices })\n\t\t\t.map((center, f) => multiplyMatrix4Vector3(\n\t\t\t\tplanes_transform[faces_plane[f]],\n\t\t\t\tcenter,\n\t\t\t));\n\t}\n\n\t// for every line, a list of all planes that this line is a member of\n\tconst lines_planes = invertFlatToArrayMap(edges_line)\n\t\t.map(edges => edges.flatMap(e => edges_faces[e].map(f => faces_plane[f])))\n\t\t.map(planes => uniqueElements(planes));\n\n\t// ensure lines's vectors and origins are in 3D.\n\tconst lines3D = lines.map(({ vector, origin }) => ({\n\t\tvector: resize3(vector),\n\t\torigin: resize3(origin),\n\t}));\n\n\t// fill lines planes\n\t/** @type {VecLine[][]} */\n\tconst lines2DInPlane = lines.map(() => []);\n\tlines_planes.forEach((planes, l) => planes.forEach(p => {\n\t\tconst { vector, origin } = lines3D[l];\n\t\tlines2DInPlane[l][p] = multiplyMatrix4Line3(planes_transform[p], vector, origin);\n\t}));\n\n\treturn edges_faces\n\t\t.map((faces, e) => faces\n\t\t\t.map(face => {\n\t\t\t\tconst { vector, origin } = lines2DInPlane[edges_line[e]][faces_plane[face]];\n\t\t\t\treturn cross2(subtract2(faces_center[face], origin), vector);\n\t\t\t})\n\t\t\t.map(Math.sign));\n};\n"
  },
  {
    "path": "src/layer/general.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @constant\n * @type {string[]}\n */\nexport const tacoTypeNames = [\n\t\"taco_taco\",\n\t\"taco_tortilla\",\n\t\"tortilla_tortilla\",\n\t\"transitivity\",\n];\n\nexport const emptyCategoryObject = () => ({\n\ttaco_taco: undefined,\n\ttaco_tortilla: undefined,\n\ttortilla_tortilla: undefined,\n\ttransitivity: undefined,\n});\n\n/**\n * @description Convert an array of faces which are involved in one\n * taco/tortilla/transitivity condition into an array of arrays where\n * each face is paired with the others in the precise combination that\n * the solver is expecting for this particular condition.\n * @type {{\n *   taco_taco: (f: TacoTacoConstraint) => [number, number][],\n *   taco_tortilla: (f: TacoTortillaConstraint) => [number, number][],\n *   tortilla_tortilla: (f: TortillaTortillaConstraint) => [number, number][],\n *   transitivity: (f: TransitivityConstraint) => [number, number][],\n * }}\n */\nexport const constraintToFacePairs = ({\n\t// taco_taco (A,C) (B,D) (B,C) (A,D) (A,B) (C,D)\n\ttaco_taco: f => [\n\t\t[f[0], f[2]],\n\t\t[f[1], f[3]],\n\t\t[f[1], f[2]],\n\t\t[f[0], f[3]],\n\t\t[f[0], f[1]],\n\t\t[f[2], f[3]],\n\t],\n\t// taco_tortilla (A,C) (A,B) (B,C)\n\ttaco_tortilla: f => [[f[0], f[2]], [f[0], f[1]], [f[1], f[2]]],\n\t// tortilla_tortilla (A,C) (B,D)\n\ttortilla_tortilla: f => [[f[0], f[2]], [f[1], f[3]]],\n\t// transitivity (A,B) (B,C) (C,A)\n\ttransitivity: f => [[f[0], f[1]], [f[1], f[2]], [f[2], f[0]]],\n});\n\n/**\n * @description Given an array of a pair of integers, sort the smallest\n * to be first, and format them into a space-separated string.\n * @param {[number, number]} pair a pair of face indices\n * @returns {string} a space-separated string encoding of the face pair\n */\nconst sortedPairString = pair => (pair[0] < pair[1]\n\t? `${pair[0]} ${pair[1]}`\n\t: `${pair[1]} ${pair[0]}`);\n\n/**\n * @description Convert an array of faces which are involved in one\n * taco/tortilla/transitivity condition into an array of arrays where\n * each face is paired with the others in the precise combination that\n * the solver is expecting for this particular condition.\n * @type {{\n *   taco_taco: (f: TacoTacoConstraint) => string[],\n *   taco_tortilla: (f: TacoTortillaConstraint) => string[],\n *   tortilla_tortilla: (f: TortillaTortillaConstraint) => string[],\n *   transitivity: (f: TransitivityConstraint) => string[],\n * }}\n */\nexport const constraintToFacePairsStrings = ({\n\t// taco_taco (A,C) (B,D) (B,C) (A,D) (A,B) (C,D)\n\ttaco_taco: f => [\n\t\tsortedPairString([f[0], f[2]]),\n\t\tsortedPairString([f[1], f[3]]),\n\t\tsortedPairString([f[1], f[2]]),\n\t\tsortedPairString([f[0], f[3]]),\n\t\tsortedPairString([f[0], f[1]]),\n\t\tsortedPairString([f[2], f[3]]),\n\t],\n\t// taco_tortilla (A,C) (A,B) (B,C)\n\ttaco_tortilla: f => [\n\t\tsortedPairString([f[0], f[2]]),\n\t\tsortedPairString([f[0], f[1]]),\n\t\tsortedPairString([f[1], f[2]]),\n\t],\n\t// tortilla_tortilla (A,C) (B,D)\n\ttortilla_tortilla: f => [\n\t\tsortedPairString([f[0], f[2]]),\n\t\tsortedPairString([f[1], f[3]]),\n\t],\n\t// transitivity (A,B) (B,C) (C,A)\n\ttransitivity: f => [\n\t\tsortedPairString([f[0], f[1]]),\n\t\tsortedPairString([f[1], f[2]]),\n\t\tsortedPairString([f[2], f[0]]),\n\t],\n});\n\nconst signedLayerSolverValue = { 0: 0, 1: 1, 2: -1 };\n\n/**\n * @description Convert encodings of layer solutions between pairs of faces.\n * Convert from the solver's 1, 2 encoding, where for faces \"A B\", a value of\n * - 1: face A is above face B\n * - 2: face A is below face B\n * into the +1/-1 \"faceOrders\" encoding, as described in the FOLD spec, where:\n * - +1: face A lies above face B, on the same side pointed by B's normal.\n * - −1: face A lies below face B, on the opposite side pointed by B's normal.\n * hence the additional faces_winding data required for conversion.\n * @param {object} facePairOrders an object with face-pair keys and 1, 2 values\n * @param {boolean[]} faces_winding for every face, is the face aligned\n * with the stacking-axis which was used in the layer solver.\n * @returns {[number, number, number][]} faceOrders array\n */\nexport const solverSolutionToFaceOrders = (facePairOrders, faces_winding) => {\n\t// convert the space-separated face pair keys into arrays of two integers\n\tconst keys = Object.keys(facePairOrders);\n\tconst faceOrdersPairs = keys\n\t\t.map(string => string.split(\" \").map(n => parseInt(n, 10)));\n\n\t// convert the value (1 or 2) into the faceOrder value (-1 or +1).\n\tconst solutions = faceOrdersPairs.map((faces, i) => {\n\t\t// according to the FOLD spec, for order [f, g, s]:\n\t\t// +1 indicates that face f lies above face g\n\t\t// −1 indicates that face f lies below face g\n\t\t// where \"above\" means on the side pointed to by g's normal vector,\n\t\t// and \"below\" means on the side opposite g's normal vector.\n\t\tconst value = signedLayerSolverValue[facePairOrders[keys[i]]];\n\t\tconst side = (!faces_winding[faces[1]]) ? -value : value;\n\t\t// const side = (((value === 1) ^ (faces_aligned[faces[1]])) * -2) + 1;\n\t\treturn side;\n\t});\n\n\t/** @type {[number, number, number][]} */\n\treturn faceOrdersPairs.map(([a, b], i) => [a, b, solutions[i]]);\n};\n\n/**\n * @description Merge two or more objects into a single object, carefully\n * checking if keys already exist, and if so, do the values match. If two\n * similar keys have different values between objects, the method will\n * throw an error.\n * @throws an error is thrown if two objects contain the same key with\n * different values.\n * @param {{ [key: string]: number }[]} orders an array of face-pair orders\n * where\n * @returns {{ [key: string]: number }} a single object merge of all input params\n */\nexport const mergeWithoutOverwrite = (orders) => {\n\t/** @type {{ [key: string]: number }} */\n\tconst result = {};\n\t// iterate through the objects\n\torders.forEach(order => Object.keys(order).forEach(key => {\n\t\tif (result[key] !== undefined && result[key] !== order[key]) {\n\t\t\tthrow new Error(`two competing results: ${result[key]}, ${order[key]}, for \"${key}\"`);\n\t\t}\n\t\tresult[key] = order[key];\n\t}));\n\treturn result;\n};\n"
  },
  {
    "path": "src/layer/getBranches.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\ttacoTypeNames,\n\tconstraintToFacePairsStrings,\n} from \"./general.js\";\n\n/**\n * @param {string[]} remainingKeys array of facePair keys which are unsolved\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints the constraints generated by the solver\n * @param {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} lookup the untouched lookup generated by the solver\n * @returns {string[][]}\n */\nexport const getBranches = (\n\tremainingKeys,\n\tconstraints,\n\tlookup,\n) => {\n\t// move remainingKeys into a dictionary.\n\t// we will delete keys from this dictionary as we visit them.\n\tconst keys = {};\n\tremainingKeys.forEach(key => { keys[key] = true; });\n\t// from this point on, \"keys\" will only shrink. not increase.\n\t// iterate through all remainingKeys\n\tlet i = 0;\n\t// the number of groups will grow as needed\n\t/** @type {string[][]} */\n\tconst groups = [];\n\twhile (i < remainingKeys.length) {\n\t\t// begin iterating through all keys in the remaining keys\n\t\t// if the key already been visited, move onto the next.\n\t\tif (!keys[remainingKeys[i]]) { i += 1; continue; }\n\t\t// this marks the beginning of a new group.\n\t\t/** @type {string[]} */\n\t\tconst group = [];\n\t\t// create a new stack (and stackHash containing duplicate data)\n\t\t// beginning with the first unvisited key\n\t\tconst stack = [remainingKeys[i]];\n\t\tconst stackHash = { [remainingKeys[i]]: true };\n\t\tdo {\n\t\t\t// pop a key off of the stack\n\t\t\tconst key = stack.pop(); // (is this faster than .shift()?)\n\t\t\t// const key = stack.shift();\n\t\t\t// mark the key as \"visited\" by removing it from \"keys\"\n\t\t\tdelete keys[key];\n\t\t\t// add this key to the current group\n\t\t\tgroup.push(key);\n\t\t\t// we are about to loop through all of this key's neighbors\n\t\t\t// collect all neighbors into one hash to remove duplicates.\n\t\t\tconst neighborsHash = {};\n\t\t\t// visit each taco/tortilla/transitivity type, and inside each type,\n\t\t\t// visit all constraints, store the constraints in the neighborsHash.\n\t\t\ttacoTypeNames.forEach(type => {\n\t\t\t\t// skip if lookup for a type/key doesn't exist.\n\t\t\t\tconst indices = lookup[type][key];\n\t\t\t\tif (!indices) { return; }\n\t\t\t\t// for each constraint index, convert it into its 3 or 4 face indices,\n\t\t\t\t// then convert these into all permutations of face-pair strings.\n\t\t\t\tindices\n\t\t\t\t\t.map(c => constraints[type][c])\n\t\t\t\t\t.map(faces => constraintToFacePairsStrings[type](faces)\n\t\t\t\t\t\t// add each facePair to the neighborsHash.\n\t\t\t\t\t\t.forEach(facePair => { neighborsHash[facePair] = true; }));\n\t\t\t});\n\t\t\t// get all neighbors from the hash, filtering out facePairs\n\t\t\t// which were already visited any time in this method (\"keys\"),\n\t\t\t// and already visited and included inside this stack (\"stackHash\")\n\t\t\tconst neighbors = Object.keys(neighborsHash)\n\t\t\t\t.filter(facePair => keys[facePair])\n\t\t\t\t.filter(facePair => !stackHash[facePair]);\n\t\t\t// add these facePairs to the stack (and hash) to be visited next loop.\n\t\t\tstack.push(...neighbors);\n\t\t\tneighbors.forEach(facePair => { stackHash[facePair] = true; });\n\t\t} while (stack.length);\n\t\ti += 1;\n\t\tgroups.push(group);\n\t}\n\treturn groups;\n};\n"
  },
  {
    "path": "src/layer/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as constraints3D from \"./constraints3D.js\";\nimport * as constraints3DFaces from \"./constraints3DFaces.js\";\nimport * as constraints3DEdges from \"./constraints3DEdges.js\";\nimport * as constraintsFlat from \"./constraintsFlat.js\";\nimport * as facesSide from \"./facesSide.js\";\nimport * as general from \"./general.js\";\nimport * as initialSolutionsFlat from \"./initialSolutionsFlat.js\";\nimport * as prototype from \"./prototype.js\";\nimport * as solve from \"./solve.js\";\nimport * as solver from \"./solver.js\";\nimport * as table from \"./table.js\";\nimport * as tacosTortillas from \"./tacosTortillas.js\";\nimport * as transitivity from \"./transitivity.js\";\nimport { layer, layer3D } from \"./layer.js\";\n\nconst layerMethods = {\n\t...constraints3D,\n\t...constraints3DFaces,\n\t...constraints3DEdges,\n\t...constraintsFlat,\n\t...facesSide,\n\t...general,\n\t...initialSolutionsFlat,\n\t...prototype,\n\t...solve,\n\t...solver,\n\t...table,\n\t...tacosTortillas,\n\t...transitivity,\n\tlayer3D,\n};\n\nconst layerExport = Object.assign(layer, layerMethods);\n\nexport default layerExport;\n"
  },
  {
    "path": "src/layer/initialSolutionsFlat.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Given a FOLD graph, already folded, find the layer arrangement\n * between neighboring faces (using edges_faces), and assign a layer ordering\n * between the pair of faces to be 1 or 2, where:\n * - 1: face A is above face B\n * - 2: face A is below face B\n * faces_winding is necessary because a valley fold will place a neighboring\n * face on top of this face, unless this face is already flipped over,\n * then the neighbor will be below this face.\n * This method is able to work in 3D, in preparation, all coplanar faces are\n * clustered, and the plane's normal is established, and all faces_winding\n * winding orders are based off of this common plane normal.\n * @param {FOLD} graph a FOLD object\n * @param {boolean[]} faces_winding for every face, true if the face's\n * winding is counter-clockwise, false if clockwise.\n * @returns {{[key: string]: number}} an object describing all the\n * solved facePairs (keys) and their layer order 1 or 2 (value),\n * the object only includes those facePairs\n * which are solved, so, no 0-value entries will exist.\n */\nexport const solveFlatAdjacentEdges = (\n\t{ edges_faces, edges_assignment },\n\tfaces_winding,\n) => {\n\t// flip 1 and 2 to be the other, leaving 0 to be 0.\n\tconst flipCondition = { 0: 0, 1: 2, 2: 1 };\n\n\t// neighbor faces determined by crease between them\n\tconst assignmentOrder = { M: 1, m: 1, V: 2, v: 2 };\n\n\t// \"solution\" contains solved orders (1, 2) for face-pair keys.\n\t/** @type {{ [key: string]: number }} */\n\tconst solution = {};\n\tedges_faces.forEach((faces, edge) => {\n\t\t// the crease assignment determines the order between pairs of faces.\n\t\tconst assignment = edges_assignment[edge];\n\t\tconst localOrder = assignmentOrder[assignment];\n\n\t\t// skip boundary edges, non-manifold edges, and irrelevant assignments\n\t\tif (faces.length !== 2 || localOrder === undefined) { return; }\n\n\t\t// face[0] is the origin face.\n\t\t// the direction of \"m\" or \"v\" will be inverted if face[0] is flipped.\n\t\tconst upright = faces_winding[faces[0]];\n\n\t\t// now we know from a global perspective the order between the face pair.\n\t\tconst globalOrder = upright\n\t\t\t? localOrder\n\t\t\t: flipCondition[localOrder];\n\n\t\t// all face-pairs are stored \"a b\" where a < b. Our globalOrder is the\n\t\t// relationship from faces[0] to faces[1], so if faces[0] > [1] we need\n\t\t// to flip the order of faces, and flip the result.\n\t\tconst inOrder = faces[0] < faces[1];\n\t\tconst key = inOrder ? faces.join(\" \") : faces.slice().reverse().join(\" \");\n\t\tconst value = inOrder ? globalOrder : flipCondition[globalOrder];\n\n\t\tsolution[key] = value;\n\t});\n\treturn solution;\n};\n"
  },
  {
    "path": "src/layer/layer.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tsolveFaceOrders,\n\tsolveFaceOrders3D,\n} from \"./solve.js\";\nimport {\n\tLayerPrototype,\n} from \"./prototype.js\";\n\n/**\n * @description Find all possible layer orderings of the faces\n * in a flat-foldable origami model. The result contains all possible\n * solutions, use the prototype methods available on this return object\n * to choose one solution, among other available options.\n * @param {FOLD} graph a FOLD object with folded vertices in 2D\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {typeof LayerPrototype} a layer solution object\n */\nexport const layer = (graph, epsilon) => Object.assign(\n\tObject.create(LayerPrototype),\n\tsolveFaceOrders(graph, epsilon),\n);\n\n/**\n * @description Find all possible layer orderings of the faces\n * in a valid 3D-foldable origami model. The result contains all possible\n * solutions, use the prototype methods available on this return object\n * to choose one solution, among other available options.\n * @param {FOLD} graph a FOLD object with folded vertices in 3D or 2D\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {typeof LayerPrototype} a layer solution object\n */\nexport const layer3D = (graph, epsilon) => Object.assign(\n\tObject.create(LayerPrototype),\n\tsolveFaceOrders3D(graph, epsilon),\n);\n"
  },
  {
    "path": "src/layer/propagate.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\ttable,\n} from \"./table.js\";\nimport {\n\tconstraintToFacePairs,\n\ttacoTypeNames,\n\temptyCategoryObject,\n} from \"./general.js\";\nimport {\n\tuniqueElements,\n} from \"../general/array.js\";\n\n/**\n * @description Given one particular taco/tortilla/transitivity constraint,\n * arrange its faces in all combinations of facePairs and check if any of\n * these orders are known (1 or 2), arrange this into a string (eg. 010021),\n * and use this string to reference the lookup table which will return one\n * of three states: valid, invalid, or the change implied by this state.\n * @param {string} type one of the four:\n * \"taco_taco\", \"taco_tortilla\", \"tortilla_tortilla\", \"transitivity\"\n * @param {number[]} constraint an array of the faces\n * involved in this particular condition.\n * @param {...{[key: string]: number}} orders an array of solutions to\n * facePair orders. each taking the form of facePair keys,\n * and orders (0,1,2) values.\n * @returns {(boolean | [string, number])} true if valid, false if invalid,\n * and in the case of an implied change, return an array where the\n * first item is a facePair (\"3 5\"), and the second is the order (like 1 or 2).\n */\nconst buildRuleAndLookup = (type, constraint, ...orders) => {\n\t// flip face order\n\tconst flipFacePairOrder = { 0: 0, 1: 2, 2: 1 };\n\t// regroup the N faces into an array of pairs, giving us the\n\t// facePair (\"3 5\") and booleans stating if the order was flipped.\n\t/** @type {[number, number]} */\n\tconst facePairsArray = constraintToFacePairs[type](constraint);\n\t// are the two faces in each pair out of order (not sorted),\n\t// meaning taht when we apply the order, we need to flip it first.\n\tconst flipped = facePairsArray.map(pair => pair[1] < pair[0]);\n\tconst facePairs = facePairsArray.map((pair, i) => (flipped[i]\n\t\t? `${pair[1]} ${pair[0]}`\n\t\t: `${pair[0]} ${pair[1]}`));\n\t// consult all \"orders\" parameters for a solution (1 or 2, not 0) to\n\t// the facePair. for each facePair get the first solution found, and\n\t// in the case of no solution, that facePair will be 0 (unknown).\n\t// join this together into a string, (eg: \"010021\")\n\n\t// for each facePair, get the first available entry in orders, or 0 if none.\n\tconst key = facePairs\n\t\t.map(facePair => orders.find(o => o[facePair]))\n\t\t.map((order, i) => (order === undefined ? 0 : order[facePairs[i]]))\n\t\t.map((value, i) => (flipped[i] ? flipFacePairOrder[value] : value))\n\t\t.join(\"\");\n\n\t// now, consult the lookup table. if the result is a boolean, return it.\n\tif (table[type][key] === true || table[type][key] === false) {\n\t\treturn table[type][key];\n\t}\n\n\t// now, we know the table value is an array (not a boolean).\n\t// the table is giving us an implied state. return the implication's\n\t// facePair and order as an array. make sure to flip the order if necessary.\n\t/** @type {[number, number]} */\n\tconst [pairIndex, suggestedOrder] = table[type][key];\n\tconst facePair = facePairs[pairIndex];\n\t/** @type {number} */\n\tconst order = flipped[pairIndex]\n\t\t? flipFacePairOrder[suggestedOrder]\n\t\t: suggestedOrder;\n\treturn [facePair, order];\n};\n\n// const getFacesWithUnknownOrdersArray = (...orders) => {\n// \tconst unknownKeys = orders\n// \t\t.map(facePairsOrder => Object.keys(facePairsOrder)\n// \t\t\t.filter(key => facePairsOrder[key] !== 0))\n// \t\t.flat();\n// \treturn uniqueElements(unknownKeys.map(key => key.split(\" \")).flat());\n// };\n/**\n * @description Given a current set of modified facePairs keys, (modified\n * since the last time we ran this), get all condition indices that\n * include one or more of these facePairs. Additionally, filter out\n * the faces which only ever appear in solved facePairs.\n */\n// const getConstraintIndicesFromFacePairs = (\n// \tconstraints,\n// \tlookup,\n// \tfacePairsSubsetArray,\n// \t...orders\n// ) => {\n// \tconst facesWithUnknownOrders = {};\n// \tgetFacesWithUnknownOrdersArray(...orders)\n// \t\t.forEach(f => { facesWithUnknownOrders[f] = true; });\n\n// \tconst constraintIndices = {};\n// \ttacoTypeNames.forEach(type => {\n// \t\t// given the array of modified facePairs since last round, get all\n// \t\t// the indices in the constraints array in which these facePairs exist.\n// \t\tconst duplicates = facePairsSubsetArray\n// \t\t\t.flatMap(facePair => lookup[type][facePair]);\n// \t\t// filter these constraint indices so that (1) no duplicates and\n// \t\t// (2) only faces which appear in an unknown order (0) are included,\n// \t\t// which is done by consulting constraints[i] and checking all faces\n// \t\t// mentioned in this array, testing if any of them are unknown\n// \t\tconstraintIndices[type] = uniqueElements(duplicates)\n// \t\t\t.filter(i => constraints[type][i]\n// \t\t\t\t.map(f => facesWithUnknownOrders[f])\n// \t\t\t\t.reduce((a, b) => a || b, false));\n// \t});\n// \treturn constraintIndices;\n// };\n\n/**\n * @description Given a current set of modified facePairs keys, (modified\n * since the last time we ran this), get all condition indices that\n * include one or more of these facePairs.\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints an object containing all four cases, inside\n * of each is an (very large, typically) array of all constraints as a list of faces.\n * @param {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} lookup a map which contains, for every\n * taco/tortilla/transitivity case (top level keys), inside each is an object\n * which relates each facePair (key) to an array of indices (value),\n * where each index is an index in the \"constraints\" array\n * in which **both** of these faces appear.\n * @param {string[]} facePairsSubsetArray an array of facePair string keys.\n * @returns {{\n *   taco_taco: number[],\n *   taco_tortilla: number[],\n *   tortilla_tortilla: number[],\n *   transitivity: number[],\n * }}\n */\nconst getConstraintIndicesFromFacePairs = (\n\tconstraints,\n\tlookup,\n\tfacePairsSubsetArray,\n) => {\n\t/**\n\t * @type {{\n\t *   taco_taco: number[],\n\t *   taco_tortilla: number[],\n\t *   tortilla_tortilla: number[],\n\t *   transitivity: number[],\n\t * }}\n\t */\n\tconst constraintIndices = emptyCategoryObject();\n\ttacoTypeNames.forEach(type => {\n\t\t// given the array of modified facePairs since last round, get all\n\t\t// the indices in the constraints array in which these facePairs exist.\n\t\t// this array will contain duplicates\n\t\t/** @type {number[]} */\n\t\tconst constraintIndicesWithDups = facePairsSubsetArray\n\t\t\t.flatMap(facePair => lookup[type][facePair]);\n\n\t\t// filter these constraint indices to remove duplicates\n\t\tconstraintIndices[type] = uniqueElements(constraintIndicesWithDups)\n\t\t\t.filter(i => constraints[type][i]);\n\t});\n\treturn constraintIndices;\n};\n\n/**\n * @description Consult the lookup table for the current layer-orders,\n * check for any implied layer order changes (and check for validity), propagate\n * these implied states, check these updated conditions for new implications, repeat.\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints an object containing all four cases, inside\n * of each is an (very large, typically) array of all constraints as a list of faces.\n * @param {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} constraintsLookup a map which contains, for every taco/tortilla/transitivity case\n * (top level keys), inside each is an object which relates each facePair (key) to\n * an array of indices (value), where each index is an index in the \"constraints\"\n * array in which **both** of these faces appear.\n * @param {string[]} initiallyModifiedFacePairs an array of facePair string keys.\n * These are the keys which had a layer change since the last time running this method.\n * @param {...{[key: string]: number}} orders any number of facePairsOrder solutions\n * which relate facePairs (key) like \"3 5\" to an order, either 0, 1, or 2.\n * @returns {{[key: string]: number}} an object that maps face-pair strings\n * to an order value, either 1 or 2\n */\nexport const propagate = (\n\tconstraints,\n\tconstraintsLookup,\n\tinitiallyModifiedFacePairs,\n\t...orders\n) => {\n\t// \"modifiedFacePairs\" is an array of facePair strings, as we make updates and\n\t// apply changes and repeat, this will hold all changed facePair keys.\n\t// the moment this array is empty, we have finished propagating all changes.\n\tlet modifiedFacePairs = initiallyModifiedFacePairs;\n\n\t// this is the result which will be returned, it maps facePairs (keys)\n\t// to layer orders, either 1 or 2 (values) and contains only those facePairs\n\t// which were changed by this method, so it will never contain a 0 value condition.\n\t/** @type {{[key: string]: number}} */\n\tconst newOrders = {};\n\tdo {\n\t\t// using the facePairs which were modified in the last loop,\n\t\t// get all constraint indices which involve any of the individual faces\n\t\t// from any of these facePairs.\n\t\tconst modifiedConstraintIndices = getConstraintIndicesFromFacePairs(\n\t\t\tconstraints,\n\t\t\tconstraintsLookup,\n\t\t\tmodifiedFacePairs,\n\t\t);\n\n\t\t// todo: do you get better results by fast forwarding through all taco-taco\n\t\t// newOrders (or transitivity, or any one in particular), before then\n\t\t// moving onto the other sets, or is it faster to depth-first search through all?\n\t\t// the modifications that happened this round\n\t\tconst roundModificationsFacePairs = {};\n\t\tfor (let t = 0; t < tacoTypeNames.length; t += 1) {\n\t\t\tconst type = tacoTypeNames[t];\n\t\t\t/** @type {number[]} */\n\t\t\tconst indices = modifiedConstraintIndices[type];\n\t\t\tfor (let i = 0; i < indices.length; i += 1) {\n\t\t\t\tconst lookupResult = buildRuleAndLookup(\n\t\t\t\t\ttype,\n\t\t\t\t\tconstraints[type][indices[i]],\n\t\t\t\t\t...orders,\n\t\t\t\t\tnewOrders,\n\t\t\t\t);\n\t\t\t\tif (lookupResult === true) { continue; }\n\t\t\t\tif (lookupResult === false) {\n\t\t\t\t\tthrow new Error(`invalid ${type} ${indices[i]}:${constraints[type][indices[i]]}`);\n\t\t\t\t}\n\t\t\t\tif (newOrders[lookupResult[0]]) {\n\t\t\t\t\t// rule already exists. make sure the results match\n\t\t\t\t\tif (newOrders[lookupResult[0]] !== lookupResult[1]) {\n\t\t\t\t\t\tthrow new Error(`conflict ${type} ${indices[i]}:${constraints[type][indices[i]]}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst [key, value] = lookupResult;\n\t\t\t\t\troundModificationsFacePairs[key] = true;\n\t\t\t\t\tnewOrders[lookupResult[0]] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmodifiedFacePairs = Object.keys(roundModificationsFacePairs);\n\t} while (modifiedFacePairs.length);\n\treturn newOrders;\n};\n"
  },
  {
    "path": "src/layer/prototype.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n// each \"branches\" is an \"and\" list (containing a list of \"andItem\")\n// each \"andItem\" is also a list, a \"subgroup\"\n// each \"subgroup\" is an \"or\" list (containing a list of \"orItem\")\n// to compile a solution, when you encounter a:\n// - \"and\" list: gather all list elements into the solution.\n// - \"or\" list: only choose one list element to be in the solution.\n\n/**\n *\n */\nconst makePermutations = (...counts) => {\n\tconst totalLength = counts.reduce((a, b) => a * b, 1);\n\tconst maxPlace = counts.slice();\n\tfor (let i = maxPlace.length - 2; i >= 0; i -= 1) {\n\t\tmaxPlace[i] *= maxPlace[i + 1];\n\t}\n\tmaxPlace.push(1);\n\tmaxPlace.shift();\n\treturn Array.from(Array(totalLength))\n\t\t.map((_, i) => counts\n\t\t\t.map((c, j) => Math.floor(i / maxPlace[j]) % c));\n};\n\n/**\n * @param {LayerSolverSolution} solution\n */\nconst getBranchCount = ({ branches }) => {\n\tif (!branches) { return \"leaf\"; }\n\treturn branches.map(choices => ({\n\t\tchoices: choices.length,\n\t\tbranches: choices.flatMap(getBranchCount),\n\t}));\n};\n\n/**\n * @param {LayerSolverSolution} solution\n */\nexport const getBranchStructure = ({ branches }) => {\n\tif (branches === undefined) { return []; }\n\treturn branches.map(branch => branch.map(getBranchStructure));\n};\n\n/**\n * @param {LayerSolverSolution} solution\n */\nconst getBranchLeafStructure = ({ branches }) => {\n\tif (branches === undefined) { return \"leaf\"; }\n\treturn branches.map(branch => branch.map(getBranchLeafStructure));\n};\n\n/**\n * @param {LayerSolverSolution} solution\n * @param {number[]} [pattern]\n */\nexport const gather = ({ orders, branches }, pattern = []) => [\n\torders,\n\t...(branches || []).flatMap(branch => gather(branch[pattern.shift() || 0], pattern)),\n];\n\n/**\n * @param {LayerSolverSolution} solution\n * @param {number[]} [pattern]\n */\nexport const compile = ({ orders, branches }, pattern) => (\n\tgather({ orders, branches }, pattern).flat()\n);\n\n/**\n * @param {LayerSolverSolution} solution\n * @returns {{[key:string]: number}[][]}\n */\nexport const gatherAll = ({ orders, branches }) => {\n\tif (!branches) { return [[orders]]; }\n\n\t// each recursion returns an array of solution-lists, and we don't have to\n\t// maintain separation, so for each branch, we can flatten all recursion\n\t// results into a single array of solution-lists.\n\tconst branchResults = branches.map(andItem => andItem.flatMap(gatherAll));\n\n\t// each branch contains a list of leaves. we have to \"and\" every branch\n\t// with every other branch, meaning we need to choose one leaf from every\n\t// branch when we pair it with another set of branches. the number of\n\t// permutations is the product of the lengths of all the branches.\n\treturn makePermutations(...branchResults.map(arr => arr.length))\n\t\t.map(indices => indices\n\t\t\t.flatMap((index, i) => branchResults[i][index]))\n\t\t.map(solution => [orders, ...solution]);\n};\n\n/**\n * @param {LayerSolverSolution} solution\n */\nexport const compileAll = ({ orders, branches }) => (\n\tgatherAll({ orders, branches }).map(arr => arr.flat())\n);\n\n// const getBranchCountFirst = ({ branches }, counter) => {\n// \tif (branches === undefined) { return counter.value++; }\n// \treturn branches.map(branch => branch.map(el => getBranchCountFirst(el, counter)));\n// };\n\n// const getBranchCountNotWorking = ({ branches }, counts = []) => {\n// \tif (branches === undefined) { return counts; }\n// \tbranches.forEach(branch => {\n// \t\tcounts.push(branch.length);\n// \t\tbranch.forEach(el => getBranchCountNotWorking(el, counts))\n// \t});\n// \treturn counts;\n// };\n\n// const getDecisionPoints = ({ branches }) => {\n// \tif (branches === undefined) { return \"leaf\"; }\n// \treturn branches.map(choices => {\n// \t\tconst chooseOne = choices.map(getDecisionPoints);\n// \t\treturn { chooseOne };\n// \t});\n// };\n\n// const getBranchCount = ({ branches }) => {\n// \tif (branches === undefined) { return undefined; }\n// \t// return branches.map(branch => (!branch.length ? undefined : ({\n// \t// \tcount: branch.length,\n// \t// \tbranches: branch\n// \t// \t\t.map(getBranchCount)\n// \t// \t\t.filter(a => a !== undefined)\n// \t// })));\n// \tconst outerResult = branches.map(branch => {\n// \t// return branches.map(branch => {\n// \t\tconst innerResult = branch.map(getBranchCount).filter(a => a !== undefined);\n// \t\treturn (innerResult === undefined || !innerResult.length\n// \t\t\t? undefined\n// \t\t\t: ({ count: branch.length, branches: innerResult }));\n// \t}).filter(a => a !== undefined && a.length !== 0);\n// \treturn outerResult;\n// \t// console.log(\"outerResult\", outerResult);\n// \t// return outerResult === undefined || outerResult.length === 0 ? undefined : outerResult;\n// };\n\nexport const LayerPrototype = {\n\t/**\n\t * @this {LayerSolverSolution}\n\t */\n\tcount: function () {\n\t\treturn getBranchCount(this);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t */\n\tstructure: function () {\n\t\treturn getBranchStructure(this);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t */\n\tleaves: function () {\n\t\treturn getBranchLeafStructure(this);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t * @param {number[]} pattern\n\t */\n\tgather: function (...pattern) {\n\t\treturn gather(this, pattern);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t */\n\tgatherAll: function () {\n\t\treturn gatherAll(this);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t * @param {number[]} pattern\n\t */\n\tcompile: function (...pattern) {\n\t\treturn compile(this, pattern);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t */\n\tcompileAll: function () {\n\t\treturn compileAll(this);\n\t},\n\n\t/**\n\t * @this {LayerSolverSolution}\n\t * @param {number[]} pattern\n\t */\n\tfaceOrders: function (...pattern) {\n\t\treturn compile(this, pattern);\n\t},\n};\n"
  },
  {
    "path": "src/layer/prototypeOneDepth.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { invertFlatMap } from \"../graph/maps.js\";\nimport { topologicalSort } from \"../graph/directedGraph.js\";\nimport { solverSolutionToFaceOrders } from \"./general.js\";\n\nconst makePermutations = (counts) => {\n\tconst totalLength = counts.reduce((a, b) => a * b, 1);\n\tconst maxPlace = counts.slice();\n\tfor (let i = maxPlace.length - 2; i >= 0; i -= 1) {\n\t\tmaxPlace[i] *= maxPlace[i + 1];\n\t}\n\tmaxPlace.push(1);\n\tmaxPlace.shift();\n\treturn Array.from(Array(totalLength))\n\t\t.map((_, i) => counts\n\t\t\t.map((c, j) => Math.floor(i / maxPlace[j]) % c));\n};\n\nexport const LayerPrototype = {\n\t/**\n\t * @description For every branch, get the total number of states.\n\t * @returns {number[]} the total number of states in each branch.\n\t */\n\tcount: function () {\n\t\treturn this.branches.map(arr => arr.length);\n\t},\n\n\t/**\n\t * @description Generate a FOLD-spec faceOrders array, which\n\t * is an array of relationships between pairs of faces, [A, B],\n\t * in relation to face B using face B's normal, which side is face A?\n\t * @param {number[]} indices optionally specify which state from\n\t * each branch, otherwise this will return index 0 from each branch.\n\t * @returns {number[][]} a faceOrders ordering\n\t */\n\tfaceOrders: function (...indices) {\n\t\treturn solverSolutionToFaceOrders(\n\t\t\tthis.compile(...indices),\n\t\t\tthis.faces_winding,\n\t\t);\n\t},\n\n\t/**\n\t * @description Get one complete layer solution by merging the\n\t * root solution with one state from each branch.\n\t * @param {...number} indices optionally specify which state from\n\t * each branch, otherwise this will return index 0 from each branch.\n\t * @returns {number[]} a faces_layer ordering, where, for each face (index),\n\t * the value is that face's layer in the +Z order stack.\n\t */\n\tfacesLayer: function (...indices) {\n\t\treturn invertFlatMap(this.linearize(...indices).reverse());\n\t},\n\n\t/**\n\t * @description The solution is not yet compiled, there are branches,\n\t * one of which needs to be selected and merged to create a final solution.\n\t * This is the first step before generating a solution in any usable format.\n\t * @param {...number} indices optionally specify which state from\n\t * each branch, otherwise this will return index 0 from each branch.\n\t * @returns {object} an object with space-separated face pair keys, with\n\t * a value of +1 or -1, indicating the stacking order between the pair.\n\t */\n\tcompile: function (...indices) {\n\t\t// the \"indices\" is an array of numbers, the length matching \"branches\"\n\t\t// if \"indices\" is provided, use these, otherwise fill it with 0.\n\t\tconst option = Array(this.branches.length)\n\t\t\t.fill(0)\n\t\t\t.map((n, i) => (indices[i] != null ? indices[i] : n));\n\t\t// for each branch, get one state\n\t\tconst branchesSolution = this.branches\n\t\t\t? this.branches.map((options, i) => options[option[i]])\n\t\t\t: [];\n\t\t// merge the root with all branches (one state from each branch)\n\t\treturn Object.assign({}, this.root, ...branchesSolution);\n\t},\n\n\t/**\n\t * @description create an array of face pairs where, for every pair,\n\t * the first face is above the second.\n\t * @param {...number} indices optionally specify which state from\n\t * each branch, otherwise this will return index 0 from each branch.\n\t * @returns {[number, number][]} directed edges of pairs of faces\n\t */\n\tdirectedPairs: function (...indices) {\n\t\tconst orders = this.compile(...indices);\n\t\treturn Object.keys(orders)\n\t\t\t.map(pair => (orders[pair] === 1\n\t\t\t\t? pair.split(\" \")\n\t\t\t\t: pair.split(\" \").reverse()))\n\t\t\t.map(pair => pair.map(n => parseInt(n, 10)))\n\t\t\t.map(([a, b]) => [a, b]);\n\t},\n\n\t/**\n\t * @description Create a topological sorting of all faces involved\n\t * in the solution. The first face in the array is \"above\" all others.\n\t * @param {...number} indices optionally specify which state from\n\t * each branch, otherwise this will return index 0 from each branch.\n\t */\n\tlinearize: function (...indices) {\n\t\treturn topologicalSort(this.directedPairs(...indices));\n\t},\n\n\t/**\n\t *\n\t */\n\tallSolutions: function () {\n\t\treturn makePermutations(this.count())\n\t\t\t.map(count => this.compile(...count));\n\t},\n\n\tallFacesLayers: function () {\n\t\treturn makePermutations(this.count())\n\t\t\t.map(count => this.facesLayer(...count));\n\t},\n};\n"
  },
  {
    "path": "src/layer/solve.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeEdgesFacesUnsorted,\n} from \"../graph/make/edgesFaces.js\";\nimport {\n\tmakeEdgesAssignmentSimple,\n} from \"../graph/make/edgesAssignment.js\";\nimport {\n\tmakeEdgesFoldAngle,\n} from \"../graph/make/edgesFoldAngle.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../graph/make/facesEdges.js\";\nimport {\n\tmakeFacesFaces,\n} from \"../graph/make/facesFaces.js\";\nimport {\n\tmakeEpsilon,\n} from \"../graph/epsilon.js\";\nimport {\n\tmakeSolverConstraintsFlat,\n} from \"./constraintsFlat.js\";\nimport {\n\tmakeSolverConstraints3D,\n} from \"./constraints3D.js\";\nimport {\n\tsolver,\n} from \"./solver.js\";\nimport {\n\tsolver as solverOneDepth,\n} from \"./solverOneDepth.js\";\nimport {\n\tsolverSolutionToFaceOrders,\n} from \"./general.js\";\n\n/**\n * @param {LayerFork} solutionBranch\n * @param {boolean[]} faces_winding\n * @returns {FaceOrdersSolverSolution}\n */\nconst layerSolutionToFaceOrdersTree = ({ orders, branches }, faces_winding) => (\n\tbranches === undefined\n\t\t? ({ orders: solverSolutionToFaceOrders(orders, faces_winding) })\n\t\t: ({\n\t\t\torders: solverSolutionToFaceOrders(orders, faces_winding),\n\t\t\tbranches: branches\n\t\t\t\t.map(inner => inner\n\t\t\t\t\t.map(b => layerSolutionToFaceOrdersTree(b, faces_winding))),\n\t\t}));\n\n/**\n * @description Find all possible layer orderings of the faces\n * in a flat-foldable origami model. The result contains all possible\n * solutions, use the prototype methods available on this return object\n * to choose one solution, among other available options.\n * @param {FOLDExtended} graph a FOLD object with folded vertices in 2D\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   orders: {[key:string]: number},\n *   branches?: LayerBranch[],\n *   faces_winding: boolean[],\n * }} an object that describes all layer orderings, where the \"root\" orders\n * are true for all solutions, and each object in \"branches\" can be appended\n * to the root object to create a complete solution.\n */\nexport const solveLayerOrders = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment,\n\tedges_foldAngle, faces_vertices, faces_edges, faces_faces, edges_vector,\n}, epsilon) => {\n\t// todo: need some sort of decision to be able to handle graphs which\n\t// have variations of populated/absent edges_assignment and foldAngle\n\n\t// necessary conditions for the layer solver to work\n\tif (!vertices_coords || !edges_vertices || !faces_vertices) {\n\t\treturn { orders: {}, faces_winding: [] };\n\t}\n\tif (!faces_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tif (!edges_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_vertices, faces_edges });\n\t}\n\n\t// these are needed for the 3D solver.\n\tif (!faces_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\t// edges_foldAngle needs to be present so we can ignore foldAngles\n\t// which are not flat when doing taco/tortilla things. if we need to\n\t// build it here, all of them are flat, but we need the array to exist\n\tif (!edges_foldAngle && edges_assignment) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_foldAngle = makeEdgesFoldAngle({ edges_assignment });\n\t}\n\tif (!edges_assignment) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_assignment = makeEdgesAssignmentSimple({ edges_foldAngle });\n\t}\n\n\t// find an appropriate epsilon, but only if it is not specified\n\tif (epsilon === undefined) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tepsilon = makeEpsilon({ vertices_coords, edges_vertices });\n\t}\n\n\t// convert the graph into conditions for the solver\n\tconst {\n\t\tconstraints,\n\t\tlookup,\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t// } = makeSolverConstraints3D({\n\t} = makeSolverConstraintsFlat({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaces_faces,\n\t\tedges_vector,\n\t}, epsilon);\n\n\t// include faces_winding along with the solver result\n\treturn {\n\t\t...solver({ constraints, lookup, facePairs, orders }),\n\t\tfaces_winding,\n\t};\n};\n\n/**\n * @description Keeping this around for legacy reasons.\n * This is the layer solver that builds one top-level branch\n * and no more.\n * @param {FOLDExtended} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\nexport const solveLayerOrdersSingleBranches = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment,\n\tfaces_vertices, faces_edges, edges_vector,\n}, epsilon) => {\n\t// necessary conditions for the layer solver to work\n\tif (!vertices_coords || !edges_vertices || !faces_vertices) {\n\t\treturn { orders: {}, faces_winding: [] };\n\t}\n\tif (!faces_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tif (!edges_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_vertices, faces_edges });\n\t}\n\n\t// find an appropriate epsilon, but only if it is not specified\n\tif (epsilon === undefined) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tepsilon = makeEpsilon({ vertices_coords, edges_vertices });\n\t}\n\n\t// convert the graph into conditions for the solver\n\tconst {\n\t\tconstraints,\n\t\tlookup,\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t} = makeSolverConstraintsFlat({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tedges_assignment,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tedges_vector,\n\t}, epsilon);\n\n\t// include faces_winding along with the solver result\n\treturn {\n\t\t...solverOneDepth({ constraints, lookup, facePairs, orders }),\n\t\tfaces_winding,\n\t};\n};\n\n/**\n * @description Find all possible layer orderings of the faces\n * in a 3D folded origami. The result contains all possible\n * solutions, use the prototype methods available on this return object\n * to choose one solution, among other available options.\n * This layer solver extends the taco/tortilla method into 3D by\n * sorting faces into groups of coplanar-overlapping faces\n * and adding a few new types of constraints which join faces between groups.\n * This algorithm still requires faces be convex, and faces must be planar.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{\n *   orders: {[key:string]: number},\n *   branches?: LayerBranch[],\n *   faces_winding: boolean[],\n * }} an object that describes all layer orderings, where the \"root\" orders\n * are true for all solutions, and each object in \"branches\" can be appended\n * to the root object to create a complete solution.\n */\nexport const solveLayerOrders3D = ({\n\tvertices_coords, edges_vertices, edges_faces, edges_assignment,\n\tedges_foldAngle, faces_vertices, faces_edges, faces_faces,\n}, epsilon) => {\n\t// todo: need some sort of decision to be able to handle graphs which\n\t// have variations of populated/absent edges_assignment and foldAngle\n\n\t// necessary conditions for the layer solver to work\n\tif (!vertices_coords || !edges_vertices || !faces_vertices) {\n\t\treturn { orders: {}, faces_winding: [] };\n\t}\n\tif (!faces_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tif (!edges_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_faces = makeEdgesFacesUnsorted({ edges_vertices, faces_vertices, faces_edges });\n\t}\n\n\t// these are needed for the 3D solver.\n\tif (!faces_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_faces = makeFacesFaces({ faces_vertices });\n\t}\n\t// edges_foldAngle needs to be present so we can ignore foldAngles\n\t// which are not flat when doing taco/tortilla things. if we need to\n\t// build it here, all of them are flat, but we need the array to exist\n\tif (!edges_foldAngle && edges_assignment) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_foldAngle = makeEdgesFoldAngle({ edges_assignment });\n\t}\n\tif (!edges_assignment) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tedges_assignment = makeEdgesAssignmentSimple({ edges_foldAngle });\n\t}\n\n\t// find an appropriate epsilon, but only if it is not specified\n\tif (epsilon === undefined) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tepsilon = makeEpsilon({ vertices_coords, edges_vertices });\n\t}\n\n\t// convert the graph into conditions for the solver\n\tconst {\n\t\tconstraints,\n\t\tlookup,\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t} = makeSolverConstraints3D({\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_faces,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t\tfaces_edges,\n\t\tfaces_faces,\n\t}, epsilon);\n\n\t// include faces_winding along with the solver result\n\treturn {\n\t\t...solver({ constraints, lookup, facePairs, orders }),\n\t\tfaces_winding,\n\t};\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FaceOrdersSolverSolution}\n */\nexport const solveFaceOrders = (graph, epsilon) => {\n\tconst {\n\t\tfaces_winding,\n\t\t...result\n\t} = solveLayerOrders(graph, epsilon);\n\n\treturn layerSolutionToFaceOrdersTree(result, faces_winding);\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {FaceOrdersSolverSolution}\n */\nexport const solveFaceOrders3D = (graph, epsilon) => {\n\tconst {\n\t\tfaces_winding,\n\t\t...result\n\t} = solveLayerOrders3D(graph, epsilon);\n\n\treturn layerSolutionToFaceOrdersTree(result, faces_winding);\n};\n"
  },
  {
    "path": "src/layer/solver.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../environment/messages.js\";\nimport {\n\tpropagate,\n} from \"./propagate.js\";\nimport {\n\tgetBranches,\n} from \"./getBranches.js\";\n\n/**\n * @description Given an array of unsolved facePair keys, attempt to solve\n * the entire set by guessing both states (1, 2) for one key, propagate any\n * implications, repeat another guess (1, 2) on another key, propagate, repeat...\n * This method is recursive but will only branch a maximum of two times each\n * recursion (one for each guess 1, 2). For each recursion, all solutions are\n * gathered into a single object, and that round's solved facePairs are added\n * to the \"...orders\" parameter. When all unsolvedKeys are solved, the result\n * is an array of solutions, each solving represeting the set of solutions from\n * each depth. Combine these solutions using Object.assign({}, ...orders)\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints an object containing all constraints for the solver.\n * @param {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} lookup a map which relates each facePair (key) to an array of\n * indices (value), where each index is an index in the \"constraints\" array\n * in which **both** of these faces appear.\n * @param {string[]} unsolvedKeys array of facePair keys to be solved\n * @param {...{[key: string]: number}} orders any number of facePairsOrder\n * solutions which relate facePairs (key) like \"3 5\" to an order,\n * either 0, 1, or 2.\n * @returns {LayerFork[]} an array of arrays of solution\n * objects, where each top level array entry is a \"branch\" and inside each\n * branch is an array of solution objects when taken together compose\n * a complete solution.\n */\nconst solveBranch = (\n\tconstraints,\n\tlookup,\n\tunsolvedKeys,\n\t...orders\n) => {\n\t// the purpose of this branch is to solve the unsolved keys, where currently,\n\t// there is nothing to solve, so we make the first step by choosing one key\n\t// guessing the value (either 1 or 2), and propagating that guess'\n\t// state until we find it is to be either successful, impossible, or\n\t// currently successful but not yet complete.\n\tconst guessKey = unsolvedKeys[0];\n\n\t/**\n\t * @param {number} guessValue\n\t * @returns {{[key: string]: number} | undefined} a new set of face-pair\n\t * solutions discovered via. propagate, or undefined if the guess was bad.\n\t */\n\tconst tryPropagate = (guessValue) => {\n\t\t// our new guess key/value object\n\t\tconst guess = { [guessKey]: guessValue };\n\n\t\t// propagate a guess with the one new guess key/value, if successful,\n\t\t// the one guess is left out of the result, so, append it to the result.\n\t\t// if propagate throws, it's not a problem, simply throw away this guess.\n\t\ttry {\n\t\t\tconst result = propagate(constraints, lookup, [guessKey], ...orders, guess);\n\t\t\treturn Object.assign(result, guess);\n\t\t} catch (error) { return undefined; }\n\t};\n\n\t// now that we have our guessKey, choose all possible values that the key\n\t// can be (either 1 or 2) and run propagate with a new solution subset object.\n\tconst guessResults = [1, 2]\n\t\t.map(tryPropagate)\n\t\t.filter(a => a !== undefined);\n\n\t// For every unfinished solution, where each solution contains a different\n\t// result to one or more of the face-pairs from each other, consider each\n\t// of these to now be a new branch in the total set of possible solutions\n\t// to explore. Each branch may or may not end up being successful, and if\n\t// unsuccessful, the recursed branch will become undefined, which will get\n\t// filtered out later in the return statement. Recurse with the subset of\n\t// unfinishedKeys, and append the new branch's set of orders to the end.\n\n\treturn guessResults.map(order => (\n\t\t// check if all variables are solved or more work is required.\n\t\t// store the result as either completed or unfinished\n\t\tObject.keys(order).length === unsolvedKeys.length\n\t\t\t? { orders: order }\n\t\t\t: {\n\t\t\t\torders: order,\n\t\t\t\tbranches: getBranches(\n\t\t\t\t\tunsolvedKeys.filter(key => !(key in order)),\n\t\t\t\t\tconstraints,\n\t\t\t\t\tlookup,\n\t\t\t\t).map(branchUnsolvedKeys => solveBranch(\n\t\t\t\t\tconstraints,\n\t\t\t\t\tlookup,\n\t\t\t\t\tbranchUnsolvedKeys,\n\t\t\t\t\t...orders,\n\t\t\t\t\torder,\n\t\t\t\t)),\n\t\t\t}));\n};\n\n/**\n * @description Recursively calculate all layer order solutions between faces\n * in a flat-folded FOLD graph.\n * @param {{\n *   constraints: {\n *     taco_taco: TacoTacoConstraint[],\n *     taco_tortilla: TacoTortillaConstraint[],\n *     tortilla_tortilla: TortillaTortillaConstraint[],\n *     transitivity: TransitivityConstraint[],\n *   },\n *   lookup: {\n *     taco_taco: number[][],\n *     taco_tortilla: number[][],\n *     tortilla_tortilla: number[][],\n *     transitivity: number[][],\n *   },\n *   facePairs: string[],\n *   orders: { [key: string]: number },\n * }} solverParams the parameters for the solver which include:\n * - constraints for each taco/tortilla type, an array of each\n *   condition, each condition being an array of all faces involved.\n * - lookup for each taco/tortilla type, a reverse lookup table\n *   where each face-pair contains an array of all constraints its involved in\n * - facePairs an array of space-separated face pair strings\n * - orders a prelimiary solution to some of the facePairs\n *   with solutions in 1,2 value encoding. Useful for any pre-calculations,\n *   for example, pre-calculating edge-adjacent face pairs with known assignments.\n * @returns {LayerSolverSolution} a set of solutions\n * where keys are space-separated face pair strings,\n * and values are 1 or 2 describing the relationship of the two faces.\n * Results are stored in \"root\" and \"branches\", to compile a complete solution,\n * append the \"root\" to one selection from each array in \"branches\".\n */\nexport const solver = ({ constraints, lookup, facePairs, orders }) => {\n\t// \"orders\" is any pre-solved orders between faces, in the default case,\n\t// this contains orders which were solved bewteen edge-adjacent pairs of\n\t// faces. Whatever this input set is, we use it as the seed for the\n\t// first run through propagate which will stop the moment that it finds\n\t// a branch. The result is a set of orders which are true for all cases.\n\t/** @type {{ [key: string]: number }} */\n\tlet initialResult;\n\ttry {\n\t\tinitialResult = propagate(constraints, lookup, Object.keys(orders), orders);\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\t// the result from the first call to propagate and the initial orders\n\t// given in this method's input (usually the edge-adjacent face solutions)\n\t// together make up the set of initial orders.\n\tconst rootOrders = { ...orders, ...initialResult };\n\n\t// get all keys unsolved after the first round of propagate\n\tconst remainingKeys = facePairs.filter(key => !(key in rootOrders));\n\n\t// group the remaining keys into groups that are isolated from one another.\n\t// recursively solve each branch, each branch could have more than one solution.\n\t/** @type {{[key: string]: number}[][][]} */\n\ttry {\n\t\treturn remainingKeys.length === 0\n\t\t\t? { orders: rootOrders }\n\t\t\t: {\n\t\t\t\torders: rootOrders,\n\t\t\t\tbranches: getBranches(remainingKeys, constraints, lookup)\n\t\t\t\t\t.map(unsolvedKeys => solveBranch(\n\t\t\t\t\t\tconstraints,\n\t\t\t\t\t\tlookup,\n\t\t\t\t\t\tunsolvedKeys,\n\t\t\t\t\t\torders,\n\t\t\t\t\t\tinitialResult,\n\t\t\t\t\t)),\n\t\t\t};\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n};\n"
  },
  {
    "path": "src/layer/solverOneDepth.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../environment/messages.js\";\nimport {\n\tpropagate,\n} from \"./propagate.js\";\nimport {\n\tgetBranches,\n} from \"./getBranches.js\";\n\n/**\n * @description Given an array of unsolved facePair keys, attempt to solve\n * the entire set by guessing both states (1, 2) for one key, propagate any\n * implications, repeat another guess (1, 2) on another key, propagate, repeat...\n * This method is recursive but will only branch a maximum of two times each\n * recursion (one for each guess 1, 2). For each recursion, all solutions are\n * gathered into a single object, and that round's solved facePairs are added\n * to the \"...orders\" parameter. When all unsolvedKeys are solved, the result\n * is an array of solutions, each solving represeting the set of solutions from\n * each depth. Combine these solutions using Object.assign({}, ...orders)\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n *   transitivity: TransitivityConstraint[],\n * }} constraints an object containing all constraints for the solver.\n * @param {{\n *   taco_taco: number[][],\n *   taco_tortilla: number[][],\n *   tortilla_tortilla: number[][],\n *   transitivity: number[][],\n * }} lookup a map which relates each facePair (key) to an array of\n * indices (value), where each index is an index in the \"constraints\" array\n * in which **both** of these faces appear.\n * @param {string[]} unsolvedKeys array of facePair keys to be solved\n * @param {...{[key: string]: number}} orders any number of facePairsOrder\n * solutions which relate facePairs (key) like \"3 5\" to an order,\n * either 0, 1, or 2.\n * @returns {{[key: string]: number}[][]} an array of arrays of solution\n * objects, where each top level array entry is a \"branch\" and inside each\n * branch is an array of solution objects when taken together compose\n * a complete solution.\n */\nconst solveBranch = (\n\tconstraints,\n\tlookup,\n\tunsolvedKeys,\n\t...orders\n) => {\n\tif (!unsolvedKeys.length) { return []; }\n\t// the purpose of this branch is to solve the unsolved keys, where currently,\n\t// there is nothing to solve, so we make the first step by choosing one key\n\t// guessing the value (either 1 or 2), and propagating that guess'\n\t// state until we find it is to be either successful, impossible, or\n\t// currently successful but not yet complete.\n\tconst guessKey = unsolvedKeys[0];\n\n\t/**\n\t * @param {number} guessValue\n\t * @returns {{[key: string]: number} | undefined} a new set of face-pair\n\t * solutions discovered via. propagate, or undefined if the guess was bad.\n\t */\n\tconst tryPropagate = (guessValue) => {\n\t\t// our new guess key/value object\n\t\tconst guess = { [guessKey]: guessValue };\n\n\t\t// propagate a guess with the one new guess key/value, if successful,\n\t\t// the one guess is left out of the result, so, append it to the result.\n\t\t// if propagate throws, it's not a problem, simply throw away this guess.\n\t\ttry {\n\t\t\tconst result = propagate(constraints, lookup, [guessKey], ...orders, guess);\n\t\t\treturn Object.assign(result, guess);\n\t\t} catch (error) { return undefined; }\n\t};\n\n\t// now that we have our guessKey, choose all possible values that the key\n\t// can be (either 1 or 2) and run propagate with a new solution subset object.\n\tconst guessResults = [1, 2]\n\t\t.map(tryPropagate)\n\t\t.filter(a => a !== undefined);\n\n\t// check if all variables are solved or more work is required.\n\t// store the result as either completed or unfinished\n\tconst resultCount = guessResults\n\t\t.map(result => Object.keys(result).length);\n\tconst completed = guessResults\n\t\t.filter((_, i) => resultCount[i] === unsolvedKeys.length);\n\tconst unfinished = guessResults\n\t\t.filter((_, i) => resultCount[i] !== unsolvedKeys.length);\n\n\t// For every unfinished solution, where each solution contains a different\n\t// result to one or more of the face-pairs from each other, consider each\n\t// of these to now be a new branch in the total set of possible solutions\n\t// to explore. Each branch may or may not end up being successful, and if\n\t// unsuccessful, the recursed branch will become undefined, which will get\n\t// filtered out later in the return statement. Recurse with the subset of\n\t// unfinishedKeys, and append the new branch's set of orders to the end.\n\tconst recursed = unfinished\n\t\t.map(order => solveBranch(\n\t\t\tconstraints,\n\t\t\tlookup,\n\t\t\tunsolvedKeys.filter(key => !(key in order)),\n\t\t\t...orders,\n\t\t\torder,\n\t\t));\n\n\t// each branch is an individual entry in this array, where each branch itself\n\t// is a list of order objects, where each object covers a subset of the total\n\t// solution.\n\treturn completed\n\t\t.map(order => [...orders, order])\n\t\t.concat(...recursed);\n};\n\n/**\n * @description Recursively calculate all layer order solutions between faces\n * in a flat-folded FOLD graph.\n * @param {{\n *   constraints: {\n *     taco_taco: TacoTacoConstraint[],\n *     taco_tortilla: TacoTortillaConstraint[],\n *     tortilla_tortilla: TortillaTortillaConstraint[],\n *     transitivity: TransitivityConstraint[],\n *   },\n *   lookup: {\n *     taco_taco: number[][],\n *     taco_tortilla: number[][],\n *     tortilla_tortilla: number[][],\n *     transitivity: number[][],\n *   },\n *   facePairs: string[],\n *   orders: { [key: string]: number },\n * }} solverParams the parameters for the solver which include:\n * - constraints for each taco/tortilla type, an array of each\n *   condition, each condition being an array of all faces involved.\n * - lookup for each taco/tortilla type, a reverse lookup table\n *   where each face-pair contains an array of all constraints its involved in\n * - facePairs an array of space-separated face pair strings\n * - orders a prelimiary solution to some of the facePairs\n *   with solutions in 1,2 value encoding. Useful for any pre-calculations,\n *   for example, pre-calculating edge-adjacent face pairs with known assignments.\n * @returns {{\n *   root: {[key:string]: number},\n *   branches: {[key: string]: number}[][],\n * }} a set of solutions where keys are space-separated face pair strings,\n * and values are 1 or 2 describing the relationship of the two faces.\n * Results are stored in \"root\" and \"branches\", to compile a complete solution,\n * append the \"root\" to one selection from each array in \"branches\".\n */\nexport const solver = ({ constraints, lookup, facePairs, orders }) => {\n\t// \"orders\" is any pre-solved orders between faces, in the default case,\n\t// this contains orders which were solved bewteen edge-adjacent pairs of\n\t// faces. Whatever this input set is, we use it as the seed for the\n\t// first run through propagate which will stop the moment that it finds\n\t// a branch. The result is a set of orders which are true for all cases.\n\t/** @type {{ [key: string]: number }} */\n\tlet initialResult;\n\ttry {\n\t\tinitialResult = propagate(constraints, lookup, Object.keys(orders), orders);\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\t// get all keys unsolved after the first round of propagate\n\tconst remainingKeys = facePairs\n\t\t.filter(key => !(key in orders))\n\t\t.filter(key => !(key in initialResult));\n\n\t// group the remaining keys into groups that are isolated from one another.\n\t// recursively solve each branch, each branch could have more than one solution.\n\t/** @type {{[key: string]: number}[][][]} */\n\tlet branchResults;\n\ttry {\n\t\tbranchResults = getBranches(remainingKeys, constraints, lookup)\n\t\t\t.map(unsolvedKeys => solveBranch(\n\t\t\t\tconstraints,\n\t\t\t\tlookup,\n\t\t\t\tunsolvedKeys,\n\t\t\t\torders,\n\t\t\t\tinitialResult,\n\t\t\t));\n\t} catch (error) {\n\t\tthrow new Error(Messages.noLayerSolution, { cause: error });\n\t}\n\n\t// solver is finished.\n\t// the set of face-pair solutions which are true for all branches\n\tconst root = { ...orders, ...initialResult };\n\n\t// due to the recursive nature of getBranches, each branch contains every\n\t// solution object which compiles into a result, including the first two\n\t// (orders, initialResult) which are returned as \"root\". remove these two\n\t// from the result, intending that to \"compile\" a result you will need\n\t// to join the root data with the branches data.\n\tbranchResults\n\t\t.forEach(branch => branch\n\t\t\t.forEach(solution => solution.splice(0, 2)));\n\n\t// each branch result is spread across multiple objects\n\t// containing a solution for a subset of the entire set of faces, one for\n\t// each recursion depth. for each branch solution, merge its objects into one.\n\t/** @type {{[key: string]: number}[][]} */\n\tconst branches = branchResults\n\t\t.map(branch => branch\n\t\t\t.map(solution => Object.assign({}, ...solution)));\n\n\treturn { root, branches };\n};\n"
  },
  {
    "path": "src/layer/table.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description each state encodes a valid layer combination. each number\n * describes a relationship between pairs of faces, for pairs \"A B\":\n * - 1: face A is above face B\n * - 2: face A is below face B\n * each taco/tortilla set has its own encoding of face pairs.\n */\n// A-C and B-D are connected\n// \"(A,C) (B,D) (B,C) (A,D) (A,B) (C,D)\"\nconst taco_taco_valid_states = [\n\t\"111112\",\n\t\"111121\",\n\t\"111222\",\n\t\"112111\",\n\t\"121112\",\n\t\"121222\",\n\t\"122111\",\n\t\"122212\",\n\t\"211121\",\n\t\"211222\",\n\t\"212111\",\n\t\"212221\",\n\t\"221222\",\n\t\"222111\",\n\t\"222212\",\n\t\"222221\",\n];\n// A-C is the taco, B is the tortilla\n// (A,C) (A,B) (B,C)\nconst taco_tortilla_valid_states = [\"112\", \"121\", \"212\", \"221\"];\n\n// A-B and C-D are connected, A is above/below C, B is above/below D\n// (A,C) (B,D)\n// in the case of tortilla-crossing face, the crossing face\n// appears twice, the same index appears in place of both C and D\nconst tortilla_tortilla_valid_states = [\"11\", \"22\"];\n\n// const tortilla_face_valid_states = [];\n// (A,B) (B,C) (C,A)\nconst transitivity_valid_states = [\n\t\"112\",\n\t\"121\",\n\t\"122\",\n\t\"211\",\n\t\"212\",\n\t\"221\",\n];\n\n/**\n * @description Find a solution for a state. Solutions will be either:\n * - false: indicating the state is invalid\n * - true: indicating the state is valid (if not yet complete)\n * - [a, b]: a modification suggestion to change index [a] to value b.\n * When this method is called for a key, all states in the \"states\" array\n * with one fewer zero (unknown) than our current key have at this point\n * already been solved. So, modify our key by replacing a zero with a guess,\n * check the state and see if we can find a modification suggestion, ultimately\n * building a chain of suggestions that lead keys to solutions (where possible).\n * @param {{[key: string]: (boolean | [number, number])}[]} states an array of\n * objects encoding a layer state to a result.\n * @param {number} t a number between 0 and N, indicating the number of zeros\n * in this key, and indicating the index in \"states\" we should be writing to.\n * @param {string} key a layer order as a string, like \"001221\"\n */\nconst setState = (states, t, key) => {\n\t// convert the key into an array of integers (0, 1, 2)\n\tconst characters = Array.from(key).map(char => parseInt(char, 10));\n\n\t// filter out the keys which do not below in this set.\n\t// the reason we aren't doing this ahead of time is because it requires\n\t// parsing each character, which we are already doing here.\n\t// only keys with the number of \"0\"s matching the number \"t\" are allowed.\n\tif (characters.filter(x => x === 0).length !== t) { return; }\n\n\t// initialize the result to false (no solution possible).\n\t// eslint-disable-next-line no-param-reassign\n\tstates[t][key] = false;\n\n\t// solution will either be true, false, or an array of two integers\n\t// where the two integers describe a modification to be made.\n\t// if we encounter a valid suggestion (modify one character takes us\n\t// to a valid state), store it here.\n\tconst modifications = [];\n\n\t// iterate through every character (only the zero characters),\n\t// replace it with a 1 and a 2 and see if either lead us to a valid solution.\n\tfor (let i = 0; i < characters.length; i += 1) {\n\t\tconst roundModifications = [];\n\n\t\t// look at the unknown layers only (where the character is zero)\n\t\tif (characters[i] !== 0) { continue; }\n\n\t\t// in place of the unknowns, try each of the possible states (1, 2)\n\t\tfor (let x = 1; x <= 2; x += 1) {\n\t\t\t// temporarily modify the character in place to our guess\n\t\t\tcharacters[i] = x;\n\n\t\t\t// if this state exists in the previous set, save the modification\n\t\t\t// instruction which would lead our key to this next key.\n\t\t\tif (states[t - 1][characters.join(\"\")] !== false) {\n\t\t\t\troundModifications.push([i, x]);\n\t\t\t}\n\t\t}\n\n\t\t// undo our guess, set it back to 0, in preparation for the next iteration\n\t\tcharacters[i] = 0;\n\n\t\t// if we found a modifications instruction (even if we aren't using them),\n\t\t// we can say the solution is no longer false. now we just need to know if\n\t\t// the solution will be a modification instruction or simply \"true\".\n\t\tif (roundModifications.length) {\n\t\t\t// eslint-disable-next-line no-param-reassign\n\t\t\tstates[t][key] = true;\n\t\t}\n\n\t\t// if roundModifications is length 2, this means both possible states\n\t\t// led us to a solution, therefore we can't infer anything.\n\t\t// only add a modification instruction if this round found only 1.\n\t\tif (roundModifications.length === 1) {\n\t\t\tmodifications.push(roundModifications[0]);\n\t\t}\n\t}\n\n\t// if found, the result will be the (first) modification instruction.\n\t// it doesn't matter which one, we just need one.\n\tif (modifications.length) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tstates[t][key] = [modifications[0][0], modifications[0][1]];\n\t}\n};\n\n/**\n * @description make a lookup table for every possible state of a taco/\n * tortilla combination, given the particular taco/tortilla valid states.\n * the value of each state is one of 3 values (2 numbers, 1 array):\n * - false: layer order is not valid\n * - true: layer order is currently valid. and is either solved (key contains\n *   no zeros / unknowns), or it is not yet invalid and can still be solved.\n * - (Array): two numbers, [index, value], modify the current layer by\n *   changing the number at index to the value.\n * @param {string[]} valid_states, one of the 3 set of valid states above\n * @returns {{[key: string]: Readonly<(boolean | [number, number])>}} an object that\n * maps a state key to a result.\n */\n// const flip = { 0:0, 1:2, 2:1 };\nconst makeLookupEntry = (valid_states) => {\n\t// the choose count can be inferred by the length of the valid states\n\t// (assuming they are all the same length)\n\tconst chooseCount = valid_states[0].length;\n\n\t// this array will contain all states (keys) and their solutions (values).\n\t// the states are sorted into arrays of objects, where keys are sorted by\n\t// how many 0s they contain (index [0]: no zeros, index [1]: 1 zero...)\n\t/** @type {{[key: string]: (boolean | [number, number])}[]} */\n\tconst states = Array\n\t\t.from(Array(chooseCount + 1))\n\t\t.map(() => ({}));\n\n\t// this array initializer will create all permutations of 1s and 2s (no zeros)\n\t// all strings having the length of chooseCount. (ie. for 6: 111112, 212221)\n\t// because all these keys contain no zeros, they are all inside the [0] index.\n\t// initialize all keys to \"false\" for now.\n\tArray.from(Array(2 ** chooseCount))\n\t\t.map((_, i) => i.toString(2))\n\t\t.map(str => Array.from(str).map(n => parseInt(n, 10) + 1).join(\"\"))\n\t\t.map(str => (`11111${str}`).slice(-chooseCount))\n\t\t.forEach(key => { states[0][key] = false; });\n\n\t// set all valid cases to be \"true\" (indicating the solution is possible)\n\tvalid_states.forEach(s => { states[0][s] = true; });\n\n\t// now we fill in the rest of the states. In a similar manner as before,\n\t// create all permuations, length of chooseCount, but this time include\n\t// 0, 1, or 2 for every character. \"t\" relates to the number of unknowns\n\t// (number of zeros) this will be the index in \"states\" we store this key.\n\t// level [0] is already complete, start incrementing from level [1].\n\t// this is generating, for every level, the same set of strings, which is\n\t// too much, however they will get filtered out inside setState.\n\tArray.from(Array(chooseCount))\n\t\t.map((_, i) => i + 1)\n\t\t.map(t => Array.from(Array(3 ** chooseCount))\n\t\t\t.map((_, i) => i.toString(3))\n\t\t\t.map(str => (`000000${str}`).slice(-chooseCount))\n\t\t\t.forEach(key => setState(states, t, key)));\n\n\t// gather all solutions together into one object.\n\t/** @type {{[key: string]: Readonly<(boolean | [number, number])>}} */\n\tconst lookup = states.reduce((a, b) => ({ ...a, ...b }));\n\n\t// recursively freeze result, this is intended to be an immutable reference\n\tObject.keys(lookup)\n\t\t.filter(key => typeof lookup[key] === \"object\")\n\t\t.forEach(key => { lookup[key] = Object.freeze(lookup[key]); });\n\treturn Object.freeze(lookup);\n};\n\n/**\n * @name table\n * @memberof layer\n * @constant\n * @type {{\n *   taco_taco: {[key:string]: Readonly<(boolean | [number, number])>},\n *   taco_tortilla: {[key:string]: Readonly<(boolean | [number, number])>},\n *   tortilla_tortilla: {[key:string]: Readonly<(boolean | [number, number])>},\n *   transitivity: {[key:string]: Readonly<(boolean | [number, number])>},\n * }}\n * @description This lookup table encodes all possible taco-taco,\n * taco-tortilla, tortilla-tortilla, and transitivity constraints between\n * groups of faces in a folded graph. Given an encoded state, as a key\n * of this object, the value represents either:\n * - {boolean} true: the layer order is so far valid\n * - {boolean} false: the layer order is invalid\n * - {[number, number]}: the layer order is valid, and, here is another\n * relationship which can be inferred from the current state.\n * This is an implementation of an algorithm designed by [Jason Ku](//jasonku.mit.edu).\n */\nexport const table = {\n\ttaco_taco: makeLookupEntry(taco_taco_valid_states),\n\ttaco_tortilla: makeLookupEntry(taco_tortilla_valid_states),\n\ttortilla_tortilla: makeLookupEntry(tortilla_tortilla_valid_states),\n\ttransitivity: makeLookupEntry(transitivity_valid_states),\n};\n"
  },
  {
    "path": "src/layer/tacosTortillas.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tmakeFacesCenterQuick,\n} from \"../graph/make/faces.js\";\nimport {\n\tgetEdgesEdgesCollinearOverlap,\n\tgetEdgesFacesOverlap,\n} from \"../graph/overlap.js\";\nimport {\n\tconnectedComponentsPairs,\n} from \"../graph/connectedComponents.js\";\nimport {\n\tmakeEdgesFacesSide,\n\tmakeEdgePairsFacesSide,\n} from \"./facesSide.js\";\n\n/**\n * @description Assign a classification to a face-pair which will assist us in\n * assembling the faces in the appropriate order later, without any additional\n * information:\n * (in 4, 5 taco is the first set of faces, in 6, 7 taco is the second set)\n * @param {[[number,number],[number,number]]} edgePairFacesSide\n * @returns {number} a classifaction number\n * - 0: invalid\n * - 1: taco-taco\n * - 2: tortilla-tortilla (faces are aligned)\n * - 3: tortilla-tortilla (faces are not aligned)\n * - 4: taco-tortilla, taco ([0]) is on top of tortilla's ([1]) 0 index\n * - 5: taco-tortilla, taco ([0]) is on top of tortilla's ([1]) 1 index\n * - 6: tortilla-taco, taco ([1]) is on top of tortilla's ([0]) 0 index\n * - 7: tortilla-taco, taco ([1]) is on top of tortilla's ([0]) 1 index\n */\nconst classifyEdgePair = (edgePairFacesSide) => {\n\t// a taco means both faces of an edge are on the same side\n\tconst isTaco = edgePairFacesSide.map(([f0, f1]) => f0 === f1);\n\n\t// break each case into taco-taco, tortilla-tortilla, or taco-tortilla\n\n\t// both edges are tacos. taco-taco is the only case where it might result as\n\t// invalid. taco-taco is only valid if both tacos are on the same side.\n\tif (isTaco[0] && isTaco[1]) {\n\t\tif (edgePairFacesSide[0][0] !== edgePairFacesSide[1][0]) { return 0; }\n\t\treturn 1;\n\t}\n\n\t// both are tortillas. all cases are valid.\n\t// if the tortilla's are aligned meaning both tortilla's index 0\n\t// are on top of each other, the result is 2, otherwise 3.\n\tif (!isTaco[0] && !isTaco[1]) {\n\t\treturn edgePairFacesSide[0][0] === edgePairFacesSide[1][0] ? 2 : 3;\n\t}\n\n\t// taco-tortilla. all cases are valid.\n\t// depending on the taco-position, result is either 4/5 or 6/7\n\t// for the taco, which side (+1/-1) are its faces on?\n\tconst tacoSide = isTaco[0] ? edgePairFacesSide[0][0] : edgePairFacesSide[1][0];\n\n\t// get the tortilla's facesSides array\n\tconst tortillaFacesSide = isTaco[0] ? edgePairFacesSide[1] : edgePairFacesSide[0];\n\n\t// is the taco on top of the [0] index of the tortilla or the [1] index?\n\tconst tortillaSideMatch = tacoSide === tortillaFacesSide[0] ? 0 : 1;\n\tconst whichIsTaco = isTaco[0] ? 0 : 2;\n\treturn 4 + whichIsTaco + tortillaSideMatch;\n};\n\n/**\n * @param {[[number, number], [number, number]]} facePairs two pairs of\n * faces indices, the adjacent faces to the two edges involved.\n * @param {number} classification the class id number that is\n * the result of calling classifyEdgePair()\n * @returns {TacoTortillaConstraint?}\n */\nconst formatTacoTortilla = ([faces0, faces1], classification) => {\n\tswitch (classification) {\n\t// taco: faces0\n\tcase 4: return [faces0[0], faces1[0], faces0[1]];\n\tcase 5: return [faces0[0], faces1[1], faces0[1]];\n\t// taco: faces1\n\tcase 6: return [faces1[0], faces0[0], faces1[1]];\n\tcase 7: return [faces1[0], faces0[1], faces1[1]];\n\tdefault: return undefined;\n\t}\n};\n\n/**\n* @param {[[number, number], [number, number]]} facePairs two pairs of\n* faces indices, the adjacent faces to the two edges involved.\n* @param {number} classification the class id number that is\n* the result of calling classifyEdgePair()\n* @returns {TortillaTortillaConstraint?} four face indices involved in a\n* tortilla-tortilla where [0] is above/below [2] and [1] is above/below [3]\n*/\nconst formatTortillaTortilla = ([faces0, faces1], classification) => {\n\tswitch (classification) {\n\tcase 2: return [...faces0, ...faces1];\n\t// [0] from one is above [1] in the other, we need to flip one of them.\n\tcase 3: return [...faces0, faces1[1], faces1[0]];\n\tdefault: return undefined;\n\t}\n};\n\n/**\n * @description Tortilla-tortillas can be generated in two ways:\n * 1. two tortillas (4 faces) where the two dividing edges are collinear\n * 2. two tortillas (4 faces) where the dividing edges lie on top of\n * each other, crossing each other, but are not collinear.\n * This method generates all tortillas from the second set.\n * @param {number[][]} edges_faces the array from the FOLD graph\n * @param {number[][]} edgesFacesSide for each edge's pair of faces,\n * which side of the edge does this face lie? (+1 or -1)\n * @param {number[][]} edgesFacesOverlap for every edge, a list of faces\n * which overlap this edge.\n * @returns {TortillaTortillaConstraint[]} array of tortilla-tortilla conditions\n */\nexport const makeTortillaTortillaFacesCrossing = (\n\tedges_faces,\n\tedgesFacesSide,\n\tedgesFacesOverlap,\n) => {\n\t// a tortilla-edge is defined by an edge having two adjacent faces,\n\t// and both of those faces are on either side of the edge\n\tconst tortilla_edge_indices = edgesFacesSide\n\t\t.map(side => side.length === 2 && side[0] !== side[1])\n\t\t.map((isTortilla, i) => (isTortilla ? i : undefined))\n\t\t.filter(a => a !== undefined);\n\n\t/** @type {number[][]} */\n\tconst tortillas_faces_crossing = [];\n\ttortilla_edge_indices.forEach(edge => {\n\t\ttortillas_faces_crossing[edge] = edgesFacesOverlap[edge];\n\t});\n\n\t/** @type {TortillaTortillaConstraint[]} */\n\treturn tortillas_faces_crossing\n\t\t.flatMap((faces, e) => faces\n\t\t\t.map(face => [...edges_faces[e], face, face]))\n\t\t.filter(arr => arr.length === 4)\n\t\t.map(arr => [arr[0], arr[1], arr[2], arr[3]]);\n};\n\n/**\n * @description Given a FOLD object, find all instances of edges overlapping which\n * classify as taco/tortillas to determine layer order.\n * @param {FOLDExtended} graph a FOLD object. vertices_coords should already be folded.\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n *   tortilla_tortilla: TortillaTortillaConstraint[],\n * }} For a particular model, a set of layer constraints sorted into\n * taco/tortilla/transitivity types needed to be solved.\n * @notes due to the face_center calculation to determine face-edge sidedness, this\n * is currently hardcoded to only work with convex polygons.\n */\nexport const makeTacosAndTortillas = ({\n\tvertices_coords, edges_vertices, edges_faces, faces_vertices, faces_edges,\n\tfaces_center,\n}, epsilon = EPSILON) => {\n\tif (!faces_center) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_center = makeFacesCenterQuick({ vertices_coords, faces_vertices });\n\t}\n\n\tconst edgesFacesOverlap = getEdgesFacesOverlap({\n\t\tvertices_coords, edges_vertices, faces_vertices, faces_edges,\n\t}, epsilon);\n\n\t// for each edge, for its adjacent faces (1 or 2), which side of the edge\n\t// (using the edge's vector) is each face on?\n\tconst edgesFacesSide = makeEdgesFacesSide({\n\t\tvertices_coords, edges_vertices, edges_faces, faces_center,\n\t});\n\n\t// every permutation of pairs of overlapping, parallel edges, where both\n\t// edges have two adjacent faces (not one). Each of these will become\n\t// either a taco-taco, taco-tortilla, or tortilla-tortilla condition.\n\t// this will contain unique pairs ([4, 9] but not [9, 4]), smallest first.\n\tconst edgePairs = connectedComponentsPairs(\n\t\tgetEdgesEdgesCollinearOverlap({ vertices_coords, edges_vertices }, epsilon),\n\t).filter(pair => pair.every(edge => edges_faces[edge].length > 1));\n\n\t// convert every face into a +1 or -1 based on which side of the edge is it on.\n\t// ie: tacos will have similar numbers, tortillas will have one of either.\n\t// the +1/-1 is determined by the cross product to the vector of the edge.\n\tconst edgePairsFacesSide = makeEdgePairsFacesSide({\n\t\tvertices_coords, edges_vertices, edges_faces, faces_center,\n\t}, edgePairs);\n\n\t// classify each edgePairFaces into its taco/tortilla type using an encoding\n\t// that includes additional details about the location of faces, for example\n\t// which of the faces is the taco in the taco-tortilla relationship.\n\tconst edgePairsFacesType = edgePairsFacesSide.map(classifyEdgePair);\n\n\t// tortilla-tortilla has both (1) edge-aligned tortillas where 4 unique faces\n\t// are involved (this will come from edgePairs), and (2) the case where an\n\t// edge crosses one tortilla, where only 3 faces are involved (below).\n\tconst tortillaTortillaCrossing = makeTortillaTortillaFacesCrossing(\n\t\tedges_faces,\n\t\tedgesFacesSide,\n\t\tedgesFacesOverlap,\n\t);\n\n\t/** @type {[[number, number], [number, number]][]} */\n\tconst edgePairsFaces = edgePairs.map(edgePair => [\n\t\t[edges_faces[edgePair[0]][0], edges_faces[edgePair[0]][1]],\n\t\t[edges_faces[edgePair[1]][0], edges_faces[edgePair[1]][1]],\n\t]);\n\n\t// taco-tortilla has both (1) those tacos which are edge-aligned with another\n\t// tortilla-tortilla (this will come from edgePairs), and (2) those tacos\n\t// which simply cross through the middle of a face (below).\n\t/** @type {TacoTortillaConstraint[]} */\n\tconst tacoTortillaCrossing = edgesFacesOverlap\n\t\t.map((faces, e) => (edgesFacesSide[e].length > 1\n\t\t\t&& edgesFacesSide[e][0] === edgesFacesSide[e][1]\n\t\t\t? faces\n\t\t\t: []))\n\t\t.map((tortillas, edge) => ({ taco: edges_faces[edge], tortillas }))\n\t\t.filter(({ tortillas }) => tortillas.length)\n\t\t.flatMap(({ taco: [a, b], tortillas }) => tortillas\n\t\t\t.map(tortilla => [a, tortilla, b]))\n\t\t.map(arr => [arr[0], arr[1], arr[2]]);\n\n\t// taco-taco\n\t// map faces [[a, b], [c, d]] into [a, c, b, d]\n\t/** @type {TacoTacoConstraint[]} */\n\tconst taco_taco = edgePairsFacesType\n\t\t.map((n, i) => (n === 1 ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(i => edgePairs[i].map(edge => edges_faces[edge]))\n\t\t.map(el => [el[0][0], el[1][0], el[0][1], el[1][1]]);\n\n\t// taco-tortilla\n\t// map faces { taco: [a, b], tortilla: c } into [a, c, b]\n\t/** @type {TacoTortillaConstraint[]} */\n\tconst taco_tortilla = edgePairsFacesType\n\t\t.map((n, i) => (n > 3 ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(i => formatTacoTortilla(edgePairsFaces[i], edgePairsFacesType[i]))\n\t\t.concat(tacoTortillaCrossing);\n\n\t// tortilla-tortilla\n\t// tortilla-tortilla contains one additional required ordering:\n\t// [a, b], [c, d] means a and b are connected, and c and d are connected,\n\t// additionally, a is above/below c and b is above/below d.\n\t/** @type {TortillaTortillaConstraint[]} */\n\tconst tortilla_tortilla = edgePairsFacesType\n\t\t.map((n, i) => (n === 2 || n === 3 ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(i => formatTortillaTortilla(edgePairsFaces[i], edgePairsFacesType[i]))\n\t\t.concat(tortillaTortillaCrossing);\n\n\treturn {\n\t\ttaco_taco,\n\t\ttaco_tortilla,\n\t\ttortilla_tortilla,\n\t};\n};\n"
  },
  {
    "path": "src/layer/transitivity.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tclipPolygonPolygon,\n} from \"../math/clip.js\";\n\n/**\n * @description Given a folded graph, find all trios of faces which overlap\n * each other, meaning there exists at least one point that lies at the\n * intersection of all three faces.\n * @param {FOLDExtended} graph a FOLD object\n * @param {number[][]} facesFacesOverlap an overlap-relationship between every face\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {TransitivityConstraint[]} list of arrays containing three face indices.\n */\nexport const makeTransitivity = (\n\t{ faces_polygon },\n\tfacesFacesOverlap,\n\tepsilon = EPSILON,\n) => {\n\t// convert facesFacesOverlap into index-based sparse arrays for fast access.\n\tconst overlap_matrix = facesFacesOverlap.map(() => ({}));\n\tfacesFacesOverlap.forEach((faces, i) => faces.forEach(j => {\n\t\toverlap_matrix[i][j] = true;\n\t\toverlap_matrix[j][i] = true;\n\t}));\n\n\t// for every pair of faces that overlap, compute their intersection,\n\t// if it exists, save the new polygon in this sparse matrix.\n\tconst facesFacesIntersection = [];\n\tfacesFacesOverlap.forEach((faces, i) => faces.forEach(j => {\n\t\tconst polygon = clipPolygonPolygon(faces_polygon[i], faces_polygon[j], epsilon);\n\t\tif (polygon) {\n\t\t\tif (!facesFacesIntersection[i]) { facesFacesIntersection[i] = []; }\n\t\t\tif (!facesFacesIntersection[j]) { facesFacesIntersection[j] = []; }\n\t\t\tfacesFacesIntersection[i][j] = polygon;\n\t\t\tfacesFacesIntersection[j][i] = polygon;\n\t\t}\n\t}));\n\n\t/** @type {[number, number, number][]} an array of three face indices */\n\tconst trios = [];\n\tfor (let i = 0; i < facesFacesIntersection.length - 1; i += 1) {\n\t\tif (!facesFacesIntersection[i]) { continue; }\n\t\tfor (let j = i + 1; j < facesFacesIntersection.length; j += 1) {\n\t\t\tif (!facesFacesIntersection[i][j]) { continue; }\n\t\t\tfor (let k = j + 1; k < facesFacesIntersection.length; k += 1) {\n\t\t\t\tif (i === k || j === k) { continue; }\n\t\t\t\tif (!overlap_matrix[i][k] || !overlap_matrix[j][k]) { continue; }\n\t\t\t\tconst polygon = clipPolygonPolygon(\n\t\t\t\t\tfacesFacesIntersection[i][j],\n\t\t\t\t\tfaces_polygon[k],\n\t\t\t\t\tepsilon,\n\t\t\t\t);\n\t\t\t\tif (polygon) {\n\t\t\t\t\tconst [t, u, v] = [i, j, k].sort((a, b) => a - b);\n\t\t\t\t\ttrios.push([t, u, v]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn trios;\n};\n\n/**\n * @description When we calculate taco-taco and taco-tortilla constraints,\n * we are already establishing a relationship between three faces, therefore,\n * we can remove these cases from our list of transitivity constraints.\n * This method returns a lookup table of all permutations of three faces found\n * in the taco-taco and taco-tortillas, the intention is that you use this list\n * to filter out any matches from the already computed transitivity list.\n * @param {{\n *   taco_taco: TacoTacoConstraint[],\n *   taco_tortilla: TacoTortillaConstraint[],\n * }} the two sets of constrains.\n * @returns {{[key: string]: boolean}} object where keys are space-separated\n * trios of faces \"a b c\" where a < b and b < c.\n */\nexport const getTransitivityTriosFromTacos = ({ taco_taco, taco_tortilla }) => {\n\t// using the list of all taco-taco conditions, store all permutations of\n\t// the three faces (sorted low to high) into a dictionary for quick lookup.\n\t// store them as space-separated strings.\n\tconst tacoTacoTrios = taco_taco\n\t\t.map(arr => arr.slice().sort((a, b) => a - b))\n\t\t.flatMap(([t0, t1, t2, t3]) => [\n\t\t\t[t0, t1, t2],\n\t\t\t[t0, t1, t3],\n\t\t\t[t0, t2, t3],\n\t\t\t[t1, t2, t3],\n\t\t]);\n\n\tconst tacoTortillaTrios = taco_tortilla\n\t\t.map(arr => arr.slice().sort((a, b) => a - b));\n\n\t// will contain taco-taco and taco-tortilla events encoded as all\n\t// permutations of 3 faces involved in each event.\n\t/** @type {{[key: string]: boolean}} */\n\tconst tacos_trios = {};\n\n\ttacoTacoTrios\n\t\t.concat(tacoTortillaTrios)\n\t\t.map(faces => faces.join(\" \"))\n\t\t.forEach(key => { tacos_trios[key] = true; });\n\n\treturn tacos_trios;\n};\n"
  },
  {
    "path": "src/libTypes.d.ts",
    "content": "type FOLDFrame = {\n    frame_author?: string;\n    frame_title?: string;\n    frame_description?: string;\n    frame_classes?: string[];\n    frame_attributes?: string[];\n    frame_unit?: string;\n    vertices_coords?: [number, number][] | [number, number, number][];\n    vertices_vertices?: number[][];\n    vertices_edges?: number[][];\n    vertices_faces?: (number | null | undefined)[][];\n    edges_vertices?: [number, number][];\n    edges_faces?: (number | null | undefined)[][];\n    edges_assignment?: string[];\n    edges_foldAngle?: number[];\n    edges_length?: number[];\n    faces_vertices?: number[][];\n    faces_edges?: number[][];\n    faces_faces?: (number | null | undefined)[][];\n    faceOrders?: [number, number, number][];\n    edgeOrders?: [number, number, number][];\n};\ntype FOLDInternalFrame = FOLDFrame & {\n    frame_parent?: number;\n    frame_inherit?: boolean;\n};\ntype FOLDFileMetadata = {\n    file_frames?: FOLDInternalFrame[];\n    file_spec?: number;\n    file_creator?: string;\n    file_author?: string;\n    file_title?: string;\n    file_description?: string;\n    file_classes?: string[];\n};\ntype FOLDOutOfSpec = {\n    faces_center?: ([number, number] | [number, number, number])[];\n    faces_normal?: ([number, number] | [number, number, number])[];\n    edges_vector?: ([number, number] | [number, number, number])[];\n    faces_polygon?: ([number, number] | [number, number, number])[][];\n    faces_matrix?: number[][];\n    faces_layer?: number[];\n    vertices_sectors?: number[][];\n};\ntype FOLD = FOLDFileMetadata & FOLDFrame;\ntype FOLDExtended = FOLD & FOLDOutOfSpec;\ntype VecLine2 = {\n    vector: [number, number];\n    origin: [number, number];\n};\ntype VecLine3 = {\n    vector: [number, number, number];\n    origin: [number, number, number];\n};\ntype VecLine = {\n    vector: [number, number] | [number, number, number];\n    origin: [number, number] | [number, number, number];\n};\ntype UniqueLine = {\n    normal: [number, number];\n    distance: number;\n};\ntype Box = {\n    min: number[];\n    max: number[];\n    span?: number[];\n};\ntype Circle = {\n    radius: number;\n    origin: [number, number];\n};\ntype SweepEvent = {\n    vertices: number[];\n    t: number;\n    start: number[];\n    end: number[];\n};\n/**\n * Intersection related events\n */\ntype LineLineEvent = {\n    a: number;\n    b: number;\n    point: [number, number];\n};\n/**\n * Intersection related events\n */\ntype FaceVertexEvent = {\n    a: number;\n    vertex: number;\n};\n/**\n * Intersection related events\n */\ntype FaceEdgeEvent = {\n    a: number;\n    b: number;\n    point: [number, number];\n    edge: number;\n};\n/**\n * Intersection related events\n */\ntype FacePointEvent = {\n    point: [number, number];\n    overlap: boolean;\n    t: number[];\n};\ntype WebGLVertexArray = {\n    location: number;\n    buffer: WebGLBuffer;\n    type: number;\n    length: number;\n    data: Float32Array;\n};\ntype WebGLElementArray = {\n    mode: number;\n    buffer: WebGLBuffer;\n    data: Uint16Array | Uint32Array;\n};\ntype WebGLUniform = {\n    func: string;\n    value: any;\n};\ntype WebGLModel = {\n    program: WebGLProgram;\n    vertexArrays: WebGLVertexArray[];\n    elementArrays: WebGLElementArray[];\n    flags: number[];\n    makeUniforms: (options: object) => ({\n        [key: string]: WebGLUniform;\n    });\n};\ntype Arrow = {\n\tsegment: [[number, number], [number, number]],\n\thead?: {},\n\ttail?: {},\n\tbend?: number,\n\tpadding?: number,\n}\ntype TacoTacoConstraint = [number, number, number, number];\ntype TacoTortillaConstraint = [number, number, number];\ntype TortillaTortillaConstraint = [number, number, number, number];\ntype TransitivityConstraint = [number, number, number];\ntype LayerBranch = LayerFork[];\ntype LayerOrders = {\n    [key: string]: number;\n};\ntype LayerFork = {\n    orders: LayerOrders;\n    branches?: LayerFork[][];\n};\ntype LayerSolverSolution = LayerFork;\ntype FaceOrdersBranch = FaceOrdersFork[];\ntype FaceOrders = [number, number, number][];\ntype FaceOrdersFork = {\n    orders: [number, number, number][];\n    branches?: FaceOrdersFork[][];\n};\ntype FaceOrdersSolverSolution = FaceOrdersFork;\n"
  },
  {
    "path": "src/math/clip.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tinclude,\n\tincludeL,\n} from \"./compare.js\";\nimport {\n\tmagnitude2,\n\tcross2,\n\tadd2,\n\tsubtract2,\n\tscale2,\n} from \"./vector.js\";\nimport {\n\tintersectPolygonLine,\n} from \"./intersect.js\";\nimport {\n\toverlapConvexPolygonPoint,\n} from \"./overlap.js\";\n\n/**\n * @description Given a list of sorted numbers and a domain function which\n * returns true for positive numbers and can customize its behavior around\n * the epsilon space around include, walk two indices from either ends\n * of the array inwards until the domain function passes true when we pass\n * in the difference between this index's value and the neighbor index's value.\n * @param {number[]} numbers\n * @param {Function} [func=include]\n * @param {number} [scaled_epsilon=1e-6]\n * @returns {[number, number]} a pair of numbers\n */\nconst getMinMax = (numbers, func, scaled_epsilon) => {\n\tif (numbers.length < 2) { return undefined; }\n\tlet a = 0;\n\tlet b = numbers.length - 1;\n\twhile (a < b) {\n\t\tif (func(numbers[a + 1] - numbers[a], scaled_epsilon)) { break; }\n\t\ta += 1;\n\t}\n\twhile (b > a) {\n\t\tif (func(numbers[b] - numbers[b - 1], scaled_epsilon)) { break; }\n\t\tb -= 1;\n\t}\n\tif (a >= b) { return undefined; }\n\treturn [numbers[a], numbers[b]];\n};\n\n/**\n * @description find the overlap between one line and one convex polygon and\n * clip the line into a segment (two endpoints) or return undefined if no overlap.\n * The input line can be a line, ray, or segment, as determined by \"fnLine\".\n * @param {[number, number][]} poly array of points (which are arrays of numbers)\n * @param {VecLine2} line a line in \"vector\" \"origin\" form\n * @param {function} [fnPoly=include] include or exclude polygon boundary in clip\n * @param {function} [fnLine=includeL] function to determine line/ray/segment,\n * and inclusive or exclusive.\n * @param {number} [epsilon=1e-6] optional epsilon\n */\nexport const clipLineConvexPolygon = (\n\tpoly,\n\t{ vector, origin },\n\tfnPoly = include,\n\tfnLine = includeL,\n\tepsilon = EPSILON,\n) => {\n\t// clip the polygon with an infinite line, the actual line domain\n\t// function will be used later to clip these paramterization results.\n\tconst numbers = intersectPolygonLine(poly, { vector, origin }, includeL, epsilon)\n\t\t.map(({ a }) => a);\n\tif (numbers.length < 2) { return undefined; }\n\tconst scaled_epsilon = (epsilon * 2) / magnitude2(vector);\n\t// ends is now an array, length 2, of the min and max parameter on the line\n\t// this also verifies the two intersections are not the same point\n\tconst ends = getMinMax(numbers, fnPoly, scaled_epsilon);\n\tif (ends === undefined) { return undefined; }\n\t// ends_clip is the intersection between 2 domains, the result\n\t// and the valid inclusive/exclusive function\n\t// todo: this line hardcodes the parameterization that segments and rays are cropping\n\t// their lowest point at 0 and highest (if segment) at 1\n\t/** @param {number} t */\n\tconst clip_fn = (t) => {\n\t\tif (fnLine(t)) { return t; }\n\t\treturn t < 0.5 ? 0 : 1;\n\t};\n\tconst ends_clip = ends.map(clip_fn);\n\t// if endpoints are the same, exit\n\tif (Math.abs(ends_clip[0] - ends_clip[1]) < (epsilon * 2) / magnitude2(vector)) {\n\t\treturn undefined;\n\t}\n\t// test if the solution is collinear to an edge by getting the segment midpoint\n\t// then test inclusive or exclusive depending on user parameter\n\t// const mid = paramToPoint(vector, origin, (ends_clip[0] + ends_clip[1]) / 2);\n\tconst mid = add2(origin, scale2(vector, (ends_clip[0] + ends_clip[1]) / 2));\n\treturn overlapConvexPolygonPoint(poly, mid, fnPoly, epsilon).overlap\n\t\t? ends_clip.map(t => add2(origin, scale2(vector, t)))\n\t\t: undefined;\n};\n\n/**\n * @description Clip two 2D polygons and return their intersection. This works\n * for non-convex poylgons, but both polygons must have counter-clockwise\n * winding; will not work even if both are similarly-clockwise.\n * Sutherland-Hodgman algorithm.\n * Implementation is from Rosetta Code, refactored to incorporate an epsilon\n * to specify inclusivity around the edges.\n * @attribution https://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#JavaScript\n * @param {number[][]} polygon1 an array of points, where each point\n * is an array of numbers.\n * @param {number[][]} polygon2 an array of points, where each point\n * is an array of numbers.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} a polygon as an array of points.\n */\nexport const clipPolygonPolygon = (polygon1, polygon2, epsilon = EPSILON) => {\n\tconst inside = (p, cp1, cp2) => (\n\t\t(cp2[0] - cp1[0]) * (p[1] - cp1[1])) > ((cp2[1] - cp1[1]) * (p[0] - cp1[0]) + epsilon\n\t);\n\tconst intersection = (cp1, cp2, e, s) => {\n\t\tconst dc = subtract2(cp1, cp2);\n\t\tconst dp = subtract2(s, e);\n\t\tconst n1 = cross2(cp1, cp2);\n\t\tconst n2 = cross2(s, e);\n\t\tconst n3 = 1.0 / cross2(dc, dp);\n\t\treturn scale2(subtract2(scale2(dp, n1), scale2(dc, n2)), n3);\n\t};\n\tlet outputList = polygon1;\n\tlet cp1 = polygon2[polygon2.length - 1];\n\tfor (let j = 0; j < polygon2.length; j += 1) {\n\t\tconst cp2 = polygon2[j];\n\t\tconst inputList = outputList;\n\t\toutputList = [];\n\t\tlet s = inputList[inputList.length - 1];\n\t\tfor (let i = 0; i < inputList.length; i += 1) {\n\t\t\tconst e = inputList[i];\n\t\t\tif (inside(e, cp1, cp2)) {\n\t\t\t\tif (!inside(s, cp1, cp2)) {\n\t\t\t\t\toutputList.push(intersection(cp1, cp2, e, s));\n\t\t\t\t}\n\t\t\t\toutputList.push(e);\n\t\t\t} else if (inside(s, cp1, cp2)) {\n\t\t\t\toutputList.push(intersection(cp1, cp2, e, s));\n\t\t\t}\n\t\t\ts = e;\n\t\t}\n\t\tcp1 = cp2;\n\t}\n\treturn outputList.length === 0 ? undefined : outputList;\n};\n"
  },
  {
    "path": "src/math/compare.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"./constant.js\";\n\n/**\n * @description Are two inputs equal within an epsilon of each other?\n * @param {number} a any number input\n * @param {number} b any number input\n * @returns {boolean} true if the numbers are near each other\n */\nexport const epsilonEqual = (a, b, epsilon = EPSILON) => Math.abs(a - b) < epsilon;\n\n/**\n * @description Compare two numbers within an epsilon of each other,\n * so that \"-1\": a < b, \"+1\": a > b, and \"0\": a ~= b (epsilon equal).\n * This can be used inside Javascript's Array.sort() to sort increasing.\n * @param {number} a any number\n * @param {number} b any number\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number} -1, 0, +1\n */\nexport const epsilonCompare = (a, b, epsilon = EPSILON) => (\n\tepsilonEqual(a, b, epsilon) ? 0 : Math.sign(a - b)\n);\n\n/**\n * @description are two vectors equal to each other within an epsilon.\n * This method uses a axis-aligned bounding box to check equality\n * for speed. If the two vectors are of differing lengths, assume\n * the remaining values are zero, compare until the end of the\n * longest vector.\n * @param {number[]} a an array of numbers\n * @param {number[]} b an array of numbers\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the vectors are similar within an epsilon\n */\nexport const epsilonEqualVectors = (a, b, epsilon = EPSILON) => {\n\tfor (let i = 0; i < Math.max(a.length, b.length); i += 1) {\n\t\tif (!epsilonEqual(a[i] || 0, b[i] || 0, epsilon)) { return false; }\n\t}\n\treturn true;\n};\n\n/**\n * @description the inclusive test used in intersection algorithms, returns\n * true if the number is positive, including the epsilon between -epsilon and 0.\n * @param {number} n the number to test against\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} -Infinity...{false}...-epsilon...{true}...+Infinity\n */\nexport const include = (n, epsilon = EPSILON) => n > -epsilon;\n\n/**\n * @description the exclusive test used in intersection algorithms, returns\n * true if the number is positive, excluding the epsilon between 0 and +epsilon.\n * @param {number} n the number to test against\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} -Infinity...{false}...+epsilon...{true}...+Infinity\n */\nexport const exclude = (n, epsilon = EPSILON) => n > epsilon;\n\n/**\n * @description the domain function for an inclusive line\n * @param {number} _ the number to test against\n * @param {number} [__] an optional epsilon\n * @returns {boolean} true\n */\n// eslint-disable-next-line no-unused-vars\nexport const includeL = (_, __) => true;\n\n/**\n * @description the domain function for an exclusive line\n * @param {number} _ the number to test against\n * @param {number} [__=1e-6] an optional epsilon\n * @returns {boolean} true\n */\n// eslint-disable-next-line no-unused-vars\nexport const excludeL = (_, __) => true;\n\n/**\n * @description the domain function for an inclusive ray\n */\nexport const includeR = include;\n\n/**\n * @description the domain function for an exclusive ray\n */\nexport const excludeR = exclude;\n\n/**\n * @description the domain function for an inclusive segment\n * @param {number} n the number to test against\n * @param {number} [e=1e-6] an optional epsilon\n */\nexport const includeS = (n, e = EPSILON) => n > -e && n < 1 + e;\n\n/**\n * @description the domain function for an exclusive segment\n * @param {number} n the number to test against\n * @param {number} [e=1e-6] an optional epsilon\n */\nexport const excludeS = (n, e = EPSILON) => n > e && n < 1 - e;\n"
  },
  {
    "path": "src/math/constant.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description this epsilon is used throughout the library\n * @constant {number}\n * @default\n */\nexport const EPSILON = 1e-6;\n\n/**\n * @description radians to degrees\n * @constant {number}\n * @default\n */\nexport const R2D = 180 / Math.PI;\n\n/**\n * @description degrees to radians\n * @constant {number}\n * @default\n */\nexport const D2R = Math.PI / 180;\n\n/**\n * @description pi x 2\n * @constant {number}\n * @default\n */\nexport const TWO_PI = Math.PI * 2;\n"
  },
  {
    "path": "src/math/convert.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmagnitude,\n\tdot,\n\tscale2,\n\tsubtract2,\n\tsubtract3,\n\tresize2,\n\tresize3,\n\trotate90,\n\trotate270,\n} from \"./vector.js\";\n\n/**\n * @description Convert a 2D vector to an angle in radians.\n * @param {[number, number]} v a 2D vector\n * @returns {number} the angle in radians\n */\nexport const vectorToAngle = (v) => Math.atan2(v[1], v[0]);\n\n/**\n * @description Convert an angle in radians to a 2D vector.\n * @param {number} a the angle in radians\n * @returns {[number, number]} a 2D vector\n */\nexport const angleToVector = (a) => [Math.cos(a), Math.sin(a)];\n\n/**\n * @description Given two points, create a vector-origin line representation\n * of a line that passes through both points. This will work in n-dimensions.\n * If there are more than two points, the rest will be ignored.\n * @param {[number, number]|[number, number, number]} a\n * one point, this will become the origin\n * @param {[number, number]|[number, number, number]} b\n * one point\n * @returns {VecLine2} an object with \"vector\" and \"origin\".\n */\nexport const pointsToLine2 = (a, b) => ({\n\tvector: subtract2(b, a),\n\torigin: resize2(a),\n});\n\n/**\n * @description Given two points, create a vector-origin line representation\n * of a line that passes through both points. This will work in n-dimensions.\n * If there are more than two points, the rest will be ignored.\n * @param {[number, number, number]} a one point, this will become the origin\n * @param {[number, number, number]} b one point\n * @returns {VecLine3} an object with \"vector\" and \"origin\".\n */\nexport const pointsToLine3 = (a, b) => ({\n\tvector: subtract3(b, a),\n\torigin: resize3(a),\n});\n\n/**\n * @description Given two points, create a vector-origin line representation\n * of a line that passes through both points. This will work in n-dimensions.\n * If there are more than two points, the rest will be ignored.\n * @param {[number, number]|[number, number, number]} a\n * one point, this will become the origin\n * @param {[number, number]|[number, number, number]} b\n * one point\n * @returns {VecLine} an object with \"vector\" and \"origin\".\n */\nexport const pointsToLine = (a, b) => (a.length === 3 && b.length === 3\n\t? pointsToLine3(a, b)\n\t: pointsToLine2(a, b)\n);\n\n/**\n * @description Convert a line from one parameterization into another.\n * Convert vector-origin where origin is a point on the line into\n * normal-distance form where distance the shortest length from the\n * origin to a point on the line.\n * @param {VecLine} line a line in vector origin form\n * @returns {UniqueLine} line a line in normal distance form\n */\nexport const vecLineToUniqueLine = ({ vector, origin }) => {\n\tconst mag = magnitude(vector);\n\tconst normal = rotate90([vector[0], vector[1]]);\n\tconst distance = dot(origin, normal) / mag;\n\treturn { normal: scale2(normal, 1 / mag), distance };\n};\n\n/**\n * @description Convert a line from one parameterization into another.\n * Convert from normal-distance form where distance the shortest length\n * from the origin to a point on the line, to vector-origin where origin\n * is a point on the line.\n * @param {UniqueLine} line a line in normal distance form\n * @returns {VecLine2} line a line in vector origin form\n */\nexport const uniqueLineToVecLine = ({ normal, distance }) => ({\n\tvector: rotate270(normal),\n\torigin: scale2(normal, distance),\n});\n"
  },
  {
    "path": "src/math/convexHull.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tepsilonCompare,\n} from \"./compare.js\";\nimport {\n\tthreePointTurnDirection,\n} from \"./radial.js\";\nimport {\n\tnormalize2,\n\tdistance2,\n\tdot2,\n\tsubtract2,\n} from \"./vector.js\";\nimport {\n\tclusterScalars,\n} from \"../general/cluster.js\";\n\n/**\n * @description mirror an array and join it at the end, except\n * do not duplicate the final element, it should only appear once.\n * @param {any[]} arr\n */\nconst mirrorArray = (arr) => arr.concat(arr.slice(0, -1).reverse());\n\n/**\n * @description Sort and cluster a list of elements and return the first\n * cluster only. Because we are only getting the first, no other clusters\n * are constructed during the operation of this method. The sorting uses\n * a comparison function which returns -1,0,+1 for comparisons, and the first\n * cluster is the first group which among each other return 0, but compared\n * to every other item returns -1. The input array can be unsorted.\n * @param {any[]} elements an array of any objects\n * @param {function} comparison a function which accepts two paramters, type\n * matching the any[] parameter, which returns -1, 0, or +1.\n * @returns {number[]} an array of indices, indices of the \"elements\" array\n */\nconst minimumCluster = (elements, comparison) => {\n\t// find the set of all vectors that share the smallest X value within an epsilon\n\tlet smallSet = [0];\n\tfor (let i = 1; i < elements.length; i += 1) {\n\t\tswitch (comparison(elements[smallSet[0]], elements[i])) {\n\t\tcase 0: smallSet.push(i); break;\n\t\tcase 1: smallSet = [i]; break;\n\t\tdefault: break;\n\t\t}\n\t}\n\treturn smallSet;\n};\n\n/**\n * @description Get the index of the point in an array\n * considered the absolute minimum. First check the X values,\n * and in the case of multiple minimums, check the Y values.\n * If there are more than two points that share both X and Y,\n * return the first one found.\n * @param {number[][]} points array of points\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number|undefined} the index of the point in the array with\n * the smallest component values, or undefined if points is empty.\n */\nconst smallestVector2 = (points, epsilon = EPSILON) => {\n\tif (!points || !points.length) { return undefined; }\n\t// find the set of all points that share the smallest X value\n\t// const smallSet = smallestVectorSearch(points, 0, epsilonCompare, epsilon);\n\t// compare each point's x axis only\n\tconst comparison = (a, b) => epsilonCompare(a[0], b[0], epsilon);\n\tconst smallSet = minimumCluster(points, comparison);\n\t// from this set, find the point with the smallest Y value\n\tlet sm = 0;\n\tfor (let i = 1; i < smallSet.length; i += 1) {\n\t\tif (points[smallSet[i]][1] < points[smallSet[sm]][1]) { sm = i; }\n\t}\n\treturn smallSet[sm];\n};\n\n/**\n * @description Locate the point with the lowest value in both axes,\n * making this the bottom-left-most point of the set, use this point\n * as the origin. Radially sort all other points around the lowest-\n * value point, while making clusters of similarly-angled points\n * (within an epsilon). Within these clusters of similarly-angled\n * points, the points are sorted by distance so the nearest point\n * appears first in the cluster array.\n * @param {[number, number][]} points an array of 2D points\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[][]} this returns indices in clusters.\n * @todo there is a wrap-around point where the cycle will not cluster\n * values which otherwise should be clustered.\n */\nexport const convexHullRadialSortPoints = (points, epsilon = EPSILON) => {\n\tconst first = smallestVector2(points, epsilon);\n\tif (first === undefined) { return []; }\n\tconst angles = points\n\t\t.map(p => subtract2(p, points[first]))\n\t\t.map(v => normalize2(v))\n\t\t.map(vec => dot2([0, 1], vec));\n\t\t// .map((p, i) => Math.atan2(unitVecs[i][1], unitVecs[i][0]));\n\tconst rawOrder = angles\n\t\t.map((a, i) => ({ a, i }))\n\t\t.sort((a, b) => a.a - b.a)\n\t\t.map(el => el.i)\n\t\t.filter(i => i !== first);\n\n\t// todo: can we use a cluster method that takes as input a pre-sorted list?\n\treturn [[first]]\n\t\t.concat(clusterScalars(rawOrder.map(i => angles[i]), epsilon)\n\t\t\t.map(arr => arr.map(i => rawOrder[i]))\n\t\t\t.map(cluster => (cluster.length === 1 ? cluster : cluster\n\t\t\t\t.map(i => ({ i, len: distance2(points[i], points[first]) }))\n\t\t\t\t.sort((a, b) => a.len - b.len)\n\t\t\t\t.map(el => el.i))));\n};\n\n/**\n * @description Convex hull from a set of 2D points, choose whether\n * to include or exclude points which lie collinear inside one of\n * the boundary lines. modified Graham scan algorithm.\n * @param {[number, number][]} points array of points, each point an array of numbers\n * @param {boolean} [includeCollinear=false] true will include\n * points collinear along the boundary\n * @param {number} [epsilon=1e-6] undefined behavior when larger than 0.01\n * @returns {number[]} not the points, but the indices\n * of points in your \"points\" array\n */\nexport const convexHull = (points = [], includeCollinear = false, epsilon = EPSILON) => {\n\tif (points.length < 2) { return []; }\n\t// if includeCollinear is true, we need to walk collinear points,\n\t// problem is we don't know if we should be going towards or away from\n\t// the origin point, so to work around that, make a mirror of all collinear\n\t// vertices so that it walks both directions, ie: 1,6,5,13,5,6,1.\n\t// half of them will be ignored due to being rejected from the\n\t// threePointTurnDirection call, and the correct half will be saved.\n\tconst order = convexHullRadialSortPoints(points, epsilon)\n\t\t.map(arr => (arr.length === 1 ? arr : mirrorArray(arr)))\n\t\t.flat();\n\torder.push(order[0]);\n\tconst stack = [order[0]];\n\tlet i = 1;\n\t// threePointTurnDirection returns -1,0,1, with 0 as the collinear case.\n\t// setup our operation for each case, depending on includeCollinear\n\tconst funcs = {\n\t\t\"-1\": () => stack.pop(),\n\t\t1: (next) => { stack.push(next); i += 1; },\n\t\tundefined: () => { i += 1; },\n\t};\n\tfuncs[0] = includeCollinear ? funcs[\"1\"] : funcs[\"-1\"];\n\twhile (i < order.length) {\n\t\tif (stack.length < 2) {\n\t\t\tstack.push(order[i]);\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst prev = stack[stack.length - 2];\n\t\tconst curr = stack[stack.length - 1];\n\t\tconst next = order[i];\n\t\tconst pts = [prev, curr, next].map(j => points[j]);\n\t\tconst turn = threePointTurnDirection(pts[0], pts[1], pts[2], epsilon);\n\t\tfuncs[turn](next);\n\t}\n\tstack.pop();\n\treturn stack;\n};\n\n/**\n * @description Convex hull from a set of 2D points, choose whether\n * to include or exclude points which lie collinear inside one of\n * the boundary lines. modified Graham scan algorithm.\n * @param {number[][]} points array of points, each point an array of numbers\n * @param {boolean} [includeCollinear=false] true will include\n * points collinear along the boundary\n * @param {number} [epsilon=1e-6] undefined behavior when larger than 0.01\n * @returns {number[][]} the convex hull as a list of points,\n * where each point is an array of numbers\n */\n// export const convexHullAsPoints = (points = [], includeCollinear = false, epsilon = EPSILON) => (\n// \tconvexHull(points, includeCollinear, epsilon)\n// \t\t.map(i => points[i]));\n"
  },
  {
    "path": "src/math/encloses.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"./constant.js\";\n\n/**\n * @description Is a point fully contained inside of a bounding box?\n * @param {number[]} point the point\n * @param {Box} box the bounding box\n * @param {number} [epsilon=1e-6] an optional epsilon to pad the area\n * around the outer bounding box; a negative number will make\n * the boundary exclusive.\n * @returns {boolean} is the \"inner\" polygon completely inside the \"outer\"\n */\nexport const pointInBoundingBox = (point, box, epsilon = EPSILON) => {\n\tfor (let d = 0; d < point.length; d += 1) {\n\t\tif (point[d] < box.min[d] - epsilon\n\t\t\t|| point[d] > box.max[d] + epsilon) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n};\n\n/**\n * @description does one bounding box (outer) completely enclose\n * another bounding box (inner)?\n * @param {Box} outer an n-dimensional bounding box\n * @param {Box} inner an n-dimensional bounding box\n * @param {number} [epsilon=1e-6] an optional epsilon to pad the area\n * around the outer bounding box; a negative number will make\n * the boundary exclusive.\n * @returns {boolean} is the \"inner\" polygon completely inside the \"outer\"\n */\nexport const enclosingBoundingBoxes = (outer, inner, epsilon = EPSILON) => {\n\tconst dimensions = Math.min(outer.min.length, inner.min.length);\n\tfor (let d = 0; d < dimensions; d += 1) {\n\t\t// if one minimum is above the other's maximum, or visa versa\n\t\tif (inner.min[d] < outer.min[d] - epsilon\n\t\t\t|| inner.max[d] > outer.max[d] + epsilon) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n};\n"
  },
  {
    "path": "src/math/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as constant from \"./constant.js\";\nimport * as convert from \"./convert.js\";\nimport * as compare from \"./compare.js\";\nimport * as vector from \"./vector.js\";\nimport * as matrix2 from \"./matrix2.js\";\nimport * as matrix3 from \"./matrix3.js\";\nimport * as matrix4 from \"./matrix4.js\";\nimport * as quaternion from \"./quaternion.js\";\nimport * as convexHullMethods from \"./convexHull.js\";\nimport * as lineMethods from \"./line.js\";\nimport * as nearestMethods from \"./nearest.js\";\nimport * as planeMethods from \"./plane.js\";\nimport * as polygonMethods from \"./polygon.js\";\nimport * as radialMethods from \"./radial.js\";\nimport * as rangeMethods from \"./range.js\";\nimport * as straightSkeleton from \"./straightSkeleton.js\";\nimport * as triangle from \"./triangle.js\";\nimport * as encloses from \"./encloses.js\";\nimport * as overlapMethods from \"./overlap.js\";\nimport * as intersectMethods from \"./intersect.js\";\nimport * as clip from \"./clip.js\";\n\nexport default {\n\t...constant,\n\t...convert,\n\t...compare,\n\t...vector,\n\t...matrix2,\n\t...matrix3,\n\t...matrix4,\n\t...quaternion,\n\t...convexHullMethods,\n\t...lineMethods,\n\t...nearestMethods,\n\t...planeMethods,\n\t...polygonMethods,\n\t...radialMethods,\n\t...rangeMethods,\n\t...straightSkeleton,\n\t...triangle,\n\t...encloses,\n\t...overlapMethods,\n\t...intersectMethods,\n\t...clip,\n};\n"
  },
  {
    "path": "src/math/intersect.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tinclude,\n\tincludeL,\n\tincludeS,\n\tepsilonEqual,\n} from \"./compare.js\";\nimport {\n\tmagnitude2,\n\tnormalize2,\n\tcross2,\n\tscale2,\n\tadd2,\n\tsubtract2,\n\trotate90,\n\tresize2,\n} from \"./vector.js\";\nimport {\n\tclusterSortedGeneric,\n} from \"../general/cluster.js\";\n\n/**\n * @description Find the intersection of two lines. Lines can be\n * lines/rays/segments, and can be inclusive or exclusive in terms\n * of their endpoints and the epsilon value.\n * @param {VecLine2} a line object with \"vector\" and \"origin\"\n * @param {VecLine2} b line object with \"vector\" and \"origin\"\n * @param {Function} [aDomain=includeL] the domain of the first line\n * @param {Function} [bDomain=includeL] the domain of the second line\n * @param {number} [epsilon=1e-6] optional epsilon\n * @returns {{\n *   point: ([number, number] | undefined)\n *   a: (number | undefined)\n *   b: (number | undefined)\n * }} object with properties:\n * - point: one 2D point or undefined\n * - a: the intersection parameter along the first line\n * - b: the intersection parameter along the second line\n * @example\n * // intersect two lines\n * const { a, b, point } = intersectLine(line1, line2);\n * @example\n * // intersect a line and a segment\n * const { a, b, point } = intersectLine(line, segment, ear.math.includeL, ear.math.includeS);\n*/\nexport const intersectLineLine = (\n\ta,\n\tb,\n\taDomain = includeL,\n\tbDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\t// a normalized determinant gives consistent values across all epsilon ranges\n\tconst det_norm = cross2(normalize2(a.vector), normalize2(b.vector));\n\t// lines are parallel\n\tif (Math.abs(det_norm) < epsilon) {\n\t\treturn { a: undefined, b: undefined, point: undefined };\n\t}\n\tconst determinant0 = cross2(a.vector, b.vector);\n\tconst determinant1 = -determinant0;\n\t/** @type {[number, number]} */\n\tconst a2b = [b.origin[0] - a.origin[0], b.origin[1] - a.origin[1]];\n\t/** @type {[number, number]} */\n\tconst b2a = [-a2b[0], -a2b[1]];\n\tconst t0 = cross2(a2b, b.vector) / determinant0;\n\tconst t1 = cross2(b2a, a.vector) / determinant1;\n\tif (aDomain(t0, epsilon / magnitude2(a.vector))\n\t\t&& bDomain(t1, epsilon / magnitude2(b.vector))) {\n\t\treturn { a: t0, b: t1, point: add2(a.origin, scale2(a.vector, t0)) };\n\t}\n\treturn { a: undefined, b: undefined, point: undefined };\n};\n\n// export const intersectCircleLine = (\n// \tcircle,\n// \tline,\n// \t_ = include,\n// \tlineDomain = includeL,\n// \tepsilon = EPSILON,\n// ) => {\n// \tconst magSq = line.vector[0] ** 2 + line.vector[1] ** 2;\n// \tconst mag = Math.sqrt(magSq);\n// \tconst norm = mag === 0 ? line.vector : scale2(line.vector, 1 / mag);\n// \tconst rot90 = rotate90(norm);\n// \tconst bvec = subtract2(line.origin, circle.origin);\n// \tconst det = cross2(bvec, norm);\n// \tif (Math.abs(det) > circle.radius + epsilon) { return undefined; }\n// \tconst side = Math.sqrt((circle.radius ** 2) - (det ** 2));\n// \t/** @param {number} s @param {number} i */\n// \tconst f = (s, i) => circle.origin[i] - rot90[i] * det + norm[i] * s;\n// \tconst isDegenerate = Math.abs(circle.radius - Math.abs(det)) < epsilon;\n// \t/** @type {[number, number][]} */\n// \tconst results = isDegenerate\n// \t\t? [side].map(s => s.map(f)) // tangent to circle\n// \t\t: [-side, side].map(s => s.map(f));\n// \t// ? [side].map((s, i) => [f(s, i), f(s, i)]) // tangent to circle\n// \t// : [-side, side].map((s, i) => [f(s, i), f(s, i)]);\n// \tconst ts = results\n// \t\t.map(res => res.map((n, i) => n - line.origin[i]))\n// \t\t.map(v => v[0] * line.vector[0] + v[1] * line.vector[1])\n// \t\t.map(d => d / magSq);\n// \treturn results.filter((__, i) => lineDomain(ts[i], epsilon));\n// };\n/**\n * @description Calculate the intersection of a circle and a line;\n * the line can be a line, ray, or segment.\n * @param {Circle} circle a circle in \"radius\" \"origin\" form\n * @param {VecLine2} line a line in \"vector\" \"origin\" form\n * @param {Function} [_=include] the inclusivity of\n * the circle boundary (currently not used).\n * @param {Function} [lineDomain=includeL] set the line/ray/segment\n * and inclusive/exclusive\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {[number, number][]} a list of 2D points\n */\nexport const intersectCircleLine = (\n\tcircle,\n\tline,\n\t// eslint-disable-next-line no-unused-vars\n\t_ = include,\n\tlineDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\tconst magSq = line.vector[0] ** 2 + line.vector[1] ** 2;\n\tconst mag = Math.sqrt(magSq);\n\tconst norm = mag === 0 ? line.vector : scale2(line.vector, 1 / mag);\n\tconst rot90 = rotate90(norm);\n\tconst bvec = subtract2(line.origin, circle.origin);\n\tconst det = cross2(bvec, norm);\n\tif (Math.abs(det) > circle.radius + epsilon) { return undefined; }\n\tconst side = Math.sqrt((circle.radius ** 2) - (det ** 2));\n\tconst f = (s, i) => circle.origin[i] - rot90[i] * det + norm[i] * s;\n\t/** @type {[number, number][]} */\n\tconst results = Math.abs(circle.radius - Math.abs(det)) < epsilon\n\t\t? [side].map((s) => [f(s, 0), f(s, 1)]) // tangent to circle\n\t\t: [-side, side].map((s) => [f(s, 0), f(s, 1)]);\n\tconst ts = results.map(res => res.map((n, i) => n - line.origin[i]))\n\t\t.map(v => v[0] * line.vector[0] + line.vector[1] * v[1])\n\t\t.map(d => d / magSq);\n\treturn results.filter((__, i) => lineDomain(ts[i], epsilon));\n};\n\n/**\n * @description Get all unique intersections between a polygon and a line/ray/\n * segment. \"Unique\", meaning that intersections which cross a polygon vertex\n * will not register twice (two of its segment's endpoints) only one point\n * in these moments will be returned. An array of length zero means no\n * no intersections occurred.\n * @param {([number, number]|[number, number, number])[]} polygon\n * @param {VecLine2} line\n * @param {Function} domainFunc the function that classifies the line into\n * a line, ray, or segment\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ a: number, point: [number, number] }[]} a sorted list of the\n * intersection events, sorted increasing along the line's vector\n */\nexport const intersectPolygonLine = (\n\tpolygon,\n\tline,\n\tdomainFunc = includeL,\n\tepsilon = EPSILON,\n) => {\n\tconst intersections = polygon\n\t\t.map((p, i, arr) => ({\n\t\t\tvector: subtract2(arr[(i + 1) % arr.length], p),\n\t\t\torigin: resize2(p),\n\t\t}))\n\t\t.map(sideLine => intersectLineLine(\n\t\t\tline,\n\t\t\tsideLine,\n\t\t\tdomainFunc,\n\t\t\tincludeS,\n\t\t\tepsilon,\n\t\t))\n\t\t.filter(({ point }) => point !== undefined)\n\t\t.sort((m, n) => m.a - n.a)\n\t\t.map(({ a, point }) => ({ a, point }));\n\n\t/** @param {{ a: number }} m @param {{ a: number }} n @returns {boolean} */\n\tconst compare = (m, n) => epsilonEqual(m.a, n.a, epsilon);\n\n\t// cluster any intersections which have too similar of \"a\" parameters\n\t// return only one element from each cluster\n\treturn clusterSortedGeneric(intersections, compare)\n\t\t.map(([i0]) => intersections[i0]);\n};\n"
  },
  {
    "path": "src/math/line.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tepsilonEqual,\n\tepsilonEqualVectors,\n} from \"./compare.js\";\nimport {\n\tdot,\n\tcross2,\n\tnormalize,\n\tnormalize2,\n\tadd2,\n\tsubtract,\n\tsubtract2,\n\tsubtract3,\n\tscale2,\n\tlerp,\n\tflip,\n\tflip2,\n\tparallel2,\n\tparallel3,\n} from \"./vector.js\";\nimport {\n\tcounterClockwiseSubsect2,\n} from \"./radial.js\";\n\n/**\n * @description These clamp functions process lines/rays/segments intersections.\n * The line method allows all values.\n * @param {number} dist the length along the vector\n * @returns {number} the clamped input value (line does not clamp)\n */\nexport const clampLine = dist => dist;\n\n/**\n * @description These clamp functions process lines/rays/segments intersections.\n * The ray method clamps values below -epsilon to be 0.\n * @param {number} dist the length along the vector\n * @returns {number} the clamped input value\n */\nexport const clampRay = dist => (dist < -EPSILON ? 0 : dist);\n\n/**\n * @description These clamp functions process lines/rays/segments intersections.\n * The segment method clamps values below -epsilon to be 0 and above 1+epsilon to 1.\n * @param {number} dist the length along the vector\n * @returns {number} the clamped input value\n */\nexport const clampSegment = (dist) => {\n\tif (dist < -EPSILON) { return 0; }\n\tif (dist > 1 + EPSILON) { return 1; }\n\treturn dist;\n};\n\n// /**\n//  * @description Resize an n-dimensional line into 2D.\n//  * @param {VecLine} line\n//  * @returns {VecLine2} a 2D line\n//  */\n// export const resizeLine2 = ({ vector, origin }) => ({\n// \tvector: resize2(vector),\n// \torigin: resize2(origin),\n// });\n\n// /**\n//  * @description Resize an n-dimensional line into 3D, filling 0 if\n//  * any fields were previously empty.\n//  * @param {VecLine} line\n//  * @returns {VecLine3} a 3D line\n//  */\n// export const resizeLine3 = ({ vector, origin }) => ({\n// \tvector: resize3(vector),\n// \torigin: resize3(origin),\n// });\n\n/**\n * @description Do three points lie collinear to each other?\n * @param {number[]} p0 a point\n * @param {number[]} p1 a point\n * @param {number[]} p2 a point\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the points lies collinear.\n */\nexport const collinearPoints = (p0, p1, p2, epsilon = EPSILON) => {\n\tconst vectors = [[p0, p1], [p1, p2]]\n\t\t.map(pts => subtract(pts[1], pts[0]))\n\t\t.map(vector => normalize(vector));\n\treturn epsilonEqual(1.0, Math.abs(dot(vectors[0], vectors[1])), epsilon);\n};\n\n/**\n * @description Check if a point is collinear and between two other points.\n * @param {number[]} p0 a point\n * @param {number[]} p1 the point to test collinearity and between-ness\n * @param {number[]} p2 a point\n * @param {boolean} [inclusive=false] if the point is the same as the endpoints\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the point lies collinear and between the other two points.\n */\nexport const collinearBetween = (p0, p1, p2, inclusive = false, epsilon = EPSILON) => {\n\tconst similar = [p0, p2]\n\t\t.map(p => epsilonEqualVectors(p1, p, epsilon))\n\t\t.reduce((a, b) => a || b, false);\n\tif (similar) { return inclusive; }\n\tconst vectors = [[p0, p1], [p1, p2]]\n\t\t.map(segment => subtract(segment[1], segment[0]))\n\t\t.map(vector => normalize(vector));\n\treturn epsilonEqual(1.0, dot(vectors[0], vectors[1]), EPSILON);\n};\n\n/**\n * @description Test if two lines are parallel and collinear in 2D\n * @param {VecLine2} a a line in 2D\n * @param {VecLine2} b a line in 2D\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the two lines are parallel and collinear\n */\nexport const collinearLines2 = (a, b, epsilon = EPSILON) => (\n\tparallel2(a.vector, b.vector, epsilon)\n\t&& parallel2(a.vector, subtract2(b.origin, a.origin), epsilon)\n);\n\n/**\n * @description Test if two lines are parallel and collinear in 3D\n * @param {VecLine3} a a line in 3D\n * @param {VecLine3} b a line in 3D\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} true if the two lines are parallel and collinear\n */\nexport const collinearLines3 = (a, b, epsilon = EPSILON) => (\n\tparallel3(a.vector, b.vector, epsilon)\n\t&& parallel3(a.vector, subtract3(b.origin, a.origin), epsilon)\n);\n\n/**\n * @description linear interpolate between two lines\n * @param {VecLine} a a line with a \"vector\" and \"origin\" component\n * @param {VecLine} b a line with a \"vector\" and \"origin\" component\n * @param {number} t one scalar between 0 and 1 (not clamped)\n * @returns {VecLine} one line\n */\n// export const lerpLines = (a, b, t) => {\n// \tconst vector = lerp(a.vector, b.vector, t);\n// \tconst origin = lerp(a.origin, b.origin, t);\n// \treturn { vector, origin };\n// };\n\n/**\n * @description a subroutine of pleat(). If we know that the two lines are\n * parallel, this will pleat between the two parallel lines, additionally,\n * it will handle the case where two lines are parallel, but their vectors\n * are 180 flipped from each other, ensuring the pleat lines do not lerp\n * between two 180-degree flipped states.\n * @param {VecLine2} a\n * @param {VecLine2} b\n * @param {number} count\n * @returns {[VecLine2[], VecLine2[]]} an array of lines\n */\nconst parallelPleat = (a, b, count) => {\n\tconst isOpposite = dot(a.vector, b.vector) < 0;\n\t// if the vectors are parallel but opposite, we need to\n\t// orient the vectors in the same direction.\n\tconst aVector = a.vector; // isParallel && dotProd < 0 ? a.vector : a.vector;\n\tconst bVector = isOpposite ? flip(b.vector) : b.vector;\n\tconst origins = Array\n\t\t.from(Array(count - 1))\n\t\t.map((_, i) => lerp(a.origin, b.origin, (i + 1) / count));\n\tconst vectors = Array\n\t\t.from(Array(count - 1))\n\t\t.map((_, i) => lerp(aVector, bVector, (i + 1) / count));\n\t/** @type {VecLine2[]} */\n\tconst lines = vectors.map((vector, i) => ({\n\t\tvector: [vector[0], vector[1]],\n\t\torigin: [origins[i][0], origins[i][1]],\n\t}));\n\t/** @type {[VecLine2[], VecLine2[]]} */\n\tconst solution = [lines, lines];\n\tsolution[(isOpposite ? 0 : 1)] = [];\n\treturn solution;\n};\n\n/**\n * @description Between two lines, make a repeating sequence of\n * evenly-spaced lines to simulate a series of pleats.\n * @param {VecLine2} a a line with a \"vector\" and \"origin\" component\n * @param {VecLine2} b a line with a \"vector\" and \"origin\" component\n * @param {number} count the number of faces, the number of lines will be n-1.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {[VecLine2[], VecLine2[]]} an array of lines, objects with \"vector\" and \"origin\"\n */\nexport const pleat = (a, b, count, epsilon = EPSILON) => {\n\tconst determinant = cross2(a.vector, b.vector);\n\tconst numerator = cross2(subtract2(b.origin, a.origin), b.vector);\n\tconst t = numerator / determinant;\n\tconst [aNormalized, bNormalized] = [a.vector, b.vector].map(normalize2);\n\tconst isParallel = Math.abs(cross2(aNormalized, bNormalized)) < epsilon;\n\tif (isParallel) {\n\t\treturn parallelPleat(a, b, count);\n\t}\n\t// two sets of pleats will be generated, between either pairs\n\t// of interior angles, unless the lines are parallel.\n\t/** @type {[[[number, number], [number, number]], [[number, number], [number, number]]]} */\n\tconst sides = determinant > -epsilon\n\t\t? [[a.vector, b.vector], [flip2(b.vector), a.vector]]\n\t\t: [[b.vector, a.vector], [flip2(a.vector), b.vector]];\n\t/** @type {[[number, number][], [number, number][]]} */\n\t// const pleatVectors = sides\n\t// \t.map(pair => counterClockwiseSubsect2(pair[0], pair[1], count));\n\tconst pleatVectors = [\n\t\tcounterClockwiseSubsect2(sides[0][0], sides[0][1], count),\n\t\tcounterClockwiseSubsect2(sides[1][0], sides[1][1], count),\n\t];\n\t// there is an intersection as long as the lines are not parallel\n\tconst intersection = add2(a.origin, scale2(a.vector, t));\n\t// the origin of the lines will be either the intersection,\n\t// or in the case of parallel, a lerp between the two line origins.\n\tconst origins = Array.from(Array(count - 1)).map(() => intersection);\n\n\tconst [sideA, sideB] = pleatVectors\n\t\t.map(side => side\n\t\t\t.map((vector, i) => ({ vector, origin: origins[i] })));\n\treturn [sideA, sideB];\n};\n\n/**\n * @description given two lines, find two lines which bisect the given lines,\n * if the given lines have an intersection, or return one\n * line if they are parallel.\n * @param {VecLine2} a a line with a \"vector\" and \"origin\" component\n * @param {VecLine2} b a line with a \"vector\" and \"origin\" component\n * @param {number} [epsilon=1e-6] an optional epsilon for testing parallel-ness.\n * @returns {[VecLine2?, VecLine2?]} an array of lines, objects with \"vector\" and \"origin\"\n */\nexport const bisectLines2 = (a, b, epsilon = EPSILON) => {\n\tconst [biA, biB] = pleat(a, b, 2, epsilon).map(arr => arr[0]);\n\t/** @type {[VecLine2, VecLine2]} */\n\tconst solution = [biA, biB];\n\tsolution.forEach((val, i) => {\n\t\tif (val === undefined) { delete solution[i]; }\n\t});\n\treturn solution;\n};\n"
  },
  {
    "path": "src/math/matrix2.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * 2x3 matrix methods for two dimensional transformations.\n * the third column is a 2D translation vector\n */\n\n/**\n * @description the identity matrix for 2x2 matrices\n * @constant {number[]}\n * @default\n */\nexport const identity2x2 = [1, 0, 0, 1];\n\n/**\n * @description the identity matrix for 2x3 matrices (zero translation)\n * @constant {number[]}\n * @default\n */\nexport const identity2x3 = identity2x2.concat(0, 0);\n\n/**\n * @param {[number, number]} vector, in array form\n * @param {number[]} matrix, in array form\n * @returns {[number, number]} vector, the input vector transformed by the matrix\n */\nexport const multiplyMatrix2Vector2 = (matrix, vector) => [\n\tmatrix[0] * vector[0] + matrix[2] * vector[1] + matrix[4],\n\tmatrix[1] * vector[0] + matrix[3] * vector[1] + matrix[5],\n];\n\n/**\n * @param {number[]} matrix, in array form\n * @param {VecLine2} line to be transformed\n * @returns {VecLine2} the transformed line in vector-origin form\n */\nexport const multiplyMatrix2Line2 = (matrix, { vector, origin }) => ({\n\tvector: [\n\t\tmatrix[0] * vector[0] + matrix[2] * vector[1],\n\t\tmatrix[1] * vector[0] + matrix[3] * vector[1],\n\t],\n\torigin: [\n\t\tmatrix[0] * origin[0] + matrix[2] * origin[1] + matrix[4],\n\t\tmatrix[1] * origin[0] + matrix[3] * origin[1] + matrix[5],\n\t],\n});\n\n/**\n * @description Multiply two matrices where the left/right order\n * matches what you would see on a page\n * @param {number[]} m1 a matrix as an array of numbers\n * @param {number[]} m2 a matrix as an array of numbers\n * @returns {number[]} matrix\n */\nexport const multiplyMatrices2 = (m1, m2) => [\n\tm1[0] * m2[0] + m1[2] * m2[1],\n\tm1[1] * m2[0] + m1[3] * m2[1],\n\tm1[0] * m2[2] + m1[2] * m2[3],\n\tm1[1] * m2[2] + m1[3] * m2[3],\n\tm1[0] * m2[4] + m1[2] * m2[5] + m1[4],\n\tm1[1] * m2[4] + m1[3] * m2[5] + m1[5],\n];\n\n/**\n * @description calculate the determinant of a 2x3 or 2x2 matrix.\n * in the case of 2x3, the translation component is ignored.\n * @param {number[]} m a matrix as an array of numbers\n * @returns {number} the determinant of the matrix\n */\nexport const determinant2 = m => m[0] * m[3] - m[1] * m[2];\n\n/**\n * @description invert a 2x3 matrix\n * @param {number[]} m a matrix as an array of numbers\n * @returns {number[]|undefined} the inverted matrix, or undefined if not possible\n */\nexport const invertMatrix2 = (m) => {\n\tconst det = determinant2(m);\n\tif (Math.abs(det) < 1e-12\n\t\t|| Number.isNaN(det)\n\t\t|| !Number.isFinite(m[4])\n\t\t|| !Number.isFinite(m[5])) {\n\t\treturn undefined;\n\t}\n\treturn [\n\t\tm[3] / det,\n\t\t-m[1] / det,\n\t\t-m[2] / det,\n\t\tm[0] / det,\n\t\t(m[2] * m[5] - m[3] * m[4]) / det,\n\t\t(m[1] * m[4] - m[0] * m[5]) / det,\n\t];\n};\n\n/**\n * @param {number} x\n * @param {number} y\n * @returns {number[]} matrix\n */\nexport const makeMatrix2Translate = (x = 0, y = 0) => identity2x2.concat(x, y);\n\n/**\n * @param {[number, number]} scale non-uniform scaling vector for each axis\n * @param {[number, number]} origin homothetic center of the scale, default [0, 0]\n * @returns {number[]} matrix\n */\nexport const makeMatrix2Scale = (scale = [1, 1], origin = [0, 0]) => [\n\tscale[0],\n\t0,\n\t0,\n\tscale[1],\n\tscale[0] * -origin[0] + origin[0],\n\tscale[1] * -origin[1] + origin[1],\n];\n\n/**\n * @param {number} scale scale factor\n * @param {[number, number]} origin homothetic center of the scale, default [0, 0]\n * @returns {number[]} matrix\n */\nexport const makeMatrix2UniformScale = (scale = 1, origin = [0, 0]) => (\n\tmakeMatrix2Scale([scale, scale], origin)\n);\n\n/**\n * @param {number} angle the angle of rotation, origin of transformation\n * @param {[number, number]} [origin=[0, 0]] optional origin of rotation\n * @returns {number[]} matrix\n */\nexport const makeMatrix2Rotate = (angle, origin = [0, 0]) => {\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\treturn [\n\t\tcos,\n\t\tsin,\n\t\t-sin,\n\t\tcos,\n\t\torigin[0],\n\t\torigin[1],\n\t];\n};\n\n/**\n * remember vector comes before origin. origin comes last, so that it's easy\n * to leave it empty and make a reflection through the origin.\n * @param {[number, number]} vector one 2D vector specifying the reflection axis\n * @param {[number, number]} [origin=[0,0]] a 2D origin specifying a point of reflection\n * @returns {number[]} matrix\n */\nexport const makeMatrix2Reflect = (vector, origin = [0, 0]) => {\n\t// the line of reflection passes through origin, runs along vector\n\tconst angle = Math.atan2(vector[1], vector[0]);\n\tconst cosAngle = Math.cos(angle);\n\tconst sinAngle = Math.sin(angle);\n\tconst cos_Angle = Math.cos(-angle);\n\tconst sin_Angle = Math.sin(-angle);\n\tconst a = cosAngle * cos_Angle + sinAngle * sin_Angle;\n\tconst b = cosAngle * -sin_Angle + sinAngle * cos_Angle;\n\tconst c = sinAngle * cos_Angle + -cosAngle * sin_Angle;\n\tconst d = sinAngle * -sin_Angle + -cosAngle * cos_Angle;\n\tconst tx = origin[0] + a * -origin[0] + -origin[1] * c;\n\tconst ty = origin[1] + b * -origin[0] + -origin[1] * d;\n\treturn [a, b, c, d, tx, ty];\n};\n\n//               __                                           _\n//   _________  / /_  ______ ___  ____     ____ ___  ____ _  (_)___  _____\n//  / ___/ __ \\/ / / / / __ `__ \\/ __ \\   / __ `__ \\/ __ `/ / / __ \\/ ___/\n// / /__/ /_/ / / /_/ / / / / / / / / /  / / / / / / /_/ / / / /_/ / /\n// \\___/\\____/_/\\__,_/_/ /_/ /_/_/ /_/  /_/ /_/ /_/\\__,_/_/ /\\____/_/\n//                                                     /___/\n"
  },
  {
    "path": "src/math/matrix3.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tnormalize,\n\tresize,\n\tflip,\n} from \"./vector.js\";\nimport {\n\tmakeMatrix2Reflect,\n} from \"./matrix2.js\";\n\n/**\n * 3x4 matrix methods. the fourth column is a translation vector\n * these methods depend on arrays of 12 items, 3x3 matrices won't work.\n */\n\n/**\n * @description the identity matrix for 3x3 matrices\n * @constant {number[]}\n * @default\n */\nexport const identity3x3 = Object.freeze([1, 0, 0, 0, 1, 0, 0, 0, 1]);\n\n/**\n * @description the identity matrix for 3x4 matrices (zero translation)\n * @constant {number[]}\n * @default\n */\nexport const identity3x4 = Object.freeze(identity3x3.concat(0, 0, 0));\n\n/**\n * @description test if a 3x4 matrix is the identity matrix within an epsilon\n * @param {number[]} m a 3x4 matrix\n * @returns {boolean} true if the matrix is the identity matrix\n */\nexport const isIdentity3x4 = m => identity3x4\n\t.map((n, i) => Math.abs(n - m[i]) < EPSILON)\n\t.reduce((a, b) => a && b, true);\n\n/**\n * @description multiply one 3D vector by a 3x4 matrix\n * @param {number[]} m one matrix in array form\n * @param {[number, number, number]} vector in array form\n * @returns {[number, number, number]} the transformed vector\n */\nexport const multiplyMatrix3Vector3 = (m, vector) => [\n\tm[0] * vector[0] + m[3] * vector[1] + m[6] * vector[2] + m[9],\n\tm[1] * vector[0] + m[4] * vector[1] + m[7] * vector[2] + m[10],\n\tm[2] * vector[0] + m[5] * vector[1] + m[8] * vector[2] + m[11],\n];\n\n/**\n * @description multiply one 3D line by a 3x4 matrix\n * @param {number[]} m one matrix in array form\n * @param {[number, number, number]} vector the vector of the line\n * @param {[number, number, number]} origin the origin of the line\n * @returns {VecLine} the transformed line in vector-origin form\n */\nexport const multiplyMatrix3Line3 = (m, vector, origin) => ({\n\tvector: [\n\t\tm[0] * vector[0] + m[3] * vector[1] + m[6] * vector[2],\n\t\tm[1] * vector[0] + m[4] * vector[1] + m[7] * vector[2],\n\t\tm[2] * vector[0] + m[5] * vector[1] + m[8] * vector[2],\n\t],\n\torigin: [\n\t\tm[0] * origin[0] + m[3] * origin[1] + m[6] * origin[2] + m[9],\n\t\tm[1] * origin[0] + m[4] * origin[1] + m[7] * origin[2] + m[10],\n\t\tm[2] * origin[0] + m[5] * origin[1] + m[8] * origin[2] + m[11],\n\t],\n});\n\n/**\n * @description multiply two 3x4 matrices together\n * @param {number[]} m1 the first matrix\n * @param {number[]} m2 the second matrix\n * @returns {number[]} one matrix, the product of the two\n */\nexport const multiplyMatrices3 = (m1, m2) => [\n\tm1[0] * m2[0] + m1[3] * m2[1] + m1[6] * m2[2],\n\tm1[1] * m2[0] + m1[4] * m2[1] + m1[7] * m2[2],\n\tm1[2] * m2[0] + m1[5] * m2[1] + m1[8] * m2[2],\n\tm1[0] * m2[3] + m1[3] * m2[4] + m1[6] * m2[5],\n\tm1[1] * m2[3] + m1[4] * m2[4] + m1[7] * m2[5],\n\tm1[2] * m2[3] + m1[5] * m2[4] + m1[8] * m2[5],\n\tm1[0] * m2[6] + m1[3] * m2[7] + m1[6] * m2[8],\n\tm1[1] * m2[6] + m1[4] * m2[7] + m1[7] * m2[8],\n\tm1[2] * m2[6] + m1[5] * m2[7] + m1[8] * m2[8],\n\tm1[0] * m2[9] + m1[3] * m2[10] + m1[6] * m2[11] + m1[9],\n\tm1[1] * m2[9] + m1[4] * m2[10] + m1[7] * m2[11] + m1[10],\n\tm1[2] * m2[9] + m1[5] * m2[10] + m1[8] * m2[11] + m1[11],\n];\n\n/**\n * @description calculate the determinant of a 3x4 or 3x3 matrix.\n * in the case of 3x4, the translation component is ignored.\n * @param {number[]} m one matrix in array form\n * @returns {number} the determinant of the matrix\n */\nexport const determinant3 = m => (\n\tm[0] * m[4] * m[8]\n\t- m[0] * m[7] * m[5]\n\t- m[3] * m[1] * m[8]\n\t+ m[3] * m[7] * m[2]\n\t+ m[6] * m[1] * m[5]\n\t- m[6] * m[4] * m[2]\n);\n\n/**\n * @description invert a 3x4 matrix\n * @param {number[]} m one matrix in array form\n * @returns {number[]|undefined} the inverted matrix, or undefined if not possible\n */\nexport const invertMatrix3 = (m) => {\n\tconst det = determinant3(m);\n\tif (Math.abs(det) < 1e-12 || Number.isNaN(det)\n\t\t|| !Number.isFinite(m[9]) || !Number.isFinite(m[10]) || !Number.isFinite(m[11])) {\n\t\treturn undefined;\n\t}\n\tconst inv = [\n\t\tm[4] * m[8] - m[7] * m[5],\n\t\t-m[1] * m[8] + m[7] * m[2],\n\t\tm[1] * m[5] - m[4] * m[2],\n\t\t-m[3] * m[8] + m[6] * m[5],\n\t\tm[0] * m[8] - m[6] * m[2],\n\t\t-m[0] * m[5] + m[3] * m[2],\n\t\tm[3] * m[7] - m[6] * m[4],\n\t\t-m[0] * m[7] + m[6] * m[1],\n\t\tm[0] * m[4] - m[3] * m[1],\n\t\t-m[3] * m[7] * m[11] + m[3] * m[8] * m[10] + m[6] * m[4] * m[11]\n\t\t\t- m[6] * m[5] * m[10] - m[9] * m[4] * m[8] + m[9] * m[5] * m[7],\n\t\tm[0] * m[7] * m[11] - m[0] * m[8] * m[10] - m[6] * m[1] * m[11]\n\t\t\t+ m[6] * m[2] * m[10] + m[9] * m[1] * m[8] - m[9] * m[2] * m[7],\n\t\t-m[0] * m[4] * m[11] + m[0] * m[5] * m[10] + m[3] * m[1] * m[11]\n\t\t\t- m[3] * m[2] * m[10] - m[9] * m[1] * m[5] + m[9] * m[2] * m[4],\n\t];\n\tconst invDet = 1.0 / det;\n\treturn inv.map(n => n * invDet);\n};\n\n/**\n * @description make a 3x4 matrix representing a translation in 3D\n * @param {number} [x=0] the x component of the translation\n * @param {number} [y=0] the y component of the translation\n * @param {number} [z=0] the z component of the translation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3Translate = (x = 0, y = 0, z = 0) => identity3x3.concat(x, y, z);\n\n// i0 and i1 direct which columns and rows are filled\n// sgn manages right hand rule\nconst singleAxisRotate = (angle, origin, i0, i1, sgn) => {\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rotate = identity3x3.concat([0, 0, 0]);\n\trotate[i0 * 3 + i0] = cos;\n\trotate[i0 * 3 + i1] = (sgn ? +1 : -1) * sin;\n\trotate[i1 * 3 + i0] = (sgn ? -1 : +1) * sin;\n\trotate[i1 * 3 + i1] = cos;\n\tconst origin3 = [0, 1, 2].map(i => origin[i] || 0);\n\tconst trans = identity3x3.concat(flip(origin3));\n\tconst trans_inv = identity3x3.concat(origin3);\n\treturn multiplyMatrices3(trans_inv, multiplyMatrices3(rotate, trans));\n};\n\n/**\n * @description make a 3x4 matrix representing a rotation in 3D around the x-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3RotateX = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate(angle, origin, 1, 2, true));\n\n/**\n * @description make a 3x4 matrix representing a rotation in 3D around the y-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3RotateY = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate(angle, origin, 0, 2, false));\n\n/**\n * @description make a 3x4 matrix representing a rotation in 3D around the z-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3RotateZ = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate(angle, origin, 0, 1, true));\n\n/**\n * @description make a 3x4 matrix representing a rotation in 3D\n * around a given vector and around a given center of rotation.\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [vector=[0,0,1]] the axis of rotation\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3Rotate = (angle, vector = [0, 0, 1], origin = [0, 0, 0]) => {\n\tconst pos = [0, 1, 2].map(i => origin[i] || 0);\n\tconst [x, y, z] = resize(3, normalize(vector));\n\tconst c = Math.cos(angle);\n\tconst s = Math.sin(angle);\n\tconst t = 1 - c;\n\tconst trans = identity3x3.concat(-pos[0], -pos[1], -pos[2]);\n\tconst trans_inv = identity3x3.concat(pos[0], pos[1], pos[2]);\n\treturn multiplyMatrices3(trans_inv, multiplyMatrices3([\n\t\tt * x * x + c, t * y * x + z * s, t * z * x - y * s,\n\t\tt * x * y - z * s, t * y * y + c, t * z * y + x * s,\n\t\tt * x * z + y * s, t * y * z - x * s, t * z * z + c,\n\t\t0, 0, 0], trans));\n};\n\n// leave this in for legacy, testing. eventually this can be removed.\n// const makeMatrix3RotateOld = (angle, vector = [0, 0, 1], origin = [0, 0, 0]) => {\n// \t// normalize inputs\n// \tconst vec = resize(3, normalize(vector));\n// \tconst pos = [0, 1, 2].map(i => origin[i] || 0);\n// \tconst [a, b, c] = vec;\n// \tconst cos = Math.cos(angle);\n// \tconst sin = Math.sin(angle);\n// \tconst d = Math.sqrt((vec[1] * vec[1]) + (vec[2] * vec[2]));\n// \tconst b_d = Math.abs(d) < 1e-6 ? 0 : b / d;\n// \tconst c_d = Math.abs(d) < 1e-6 ? 1 : c / d;\n// \tconst t     = identity3x3.concat(-pos[0], -pos[1], -pos[2]);\n// \tconst t_inv = identity3x3.concat(pos[0], pos[1], pos[2]);\n// \tconst rx     = [1, 0, 0, 0, c_d, b_d, 0, -b_d, c_d, 0, 0, 0];\n// \tconst rx_inv = [1, 0, 0, 0, c_d, -b_d, 0, b_d, c_d, 0, 0, 0];\n// \tconst ry     = [d, 0, a, 0, 1, 0, -a, 0, d, 0, 0, 0];\n// \tconst ry_inv = [d, 0, -a, 0, 1, 0, a, 0, d, 0, 0, 0];\n// \tconst rz     = [cos, sin, 0, -sin, cos, 0, 0, 0, 1, 0, 0, 0];\n// \treturn multiplyMatrices3(t_inv,\n// \t\tmultiplyMatrices3(rx_inv,\n// \t\t\tmultiplyMatrices3(ry_inv,\n// \t\t\t\tmultiplyMatrices3(rz,\n// \t\t\t\t\tmultiplyMatrices3(ry,\n// \t\t\t\t\t\tmultiplyMatrices3(rx, t))))));\n// };\n\n/**\n * @description make a 3x4 matrix representing a non-uniform scale.\n * @param {[number, number, number]} [scale=[1,1,1]] non-uniform scaling vector\n * @param {[number, number, number]} [origin=[0,0,0]] the center of transformation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3Scale = (scale = [1, 1, 1], origin = [0, 0, 0]) => [\n\tscale[0], 0, 0,\n\t0, scale[1], 0,\n\t0, 0, scale[2],\n\tscale[0] * -origin[0] + origin[0],\n\tscale[1] * -origin[1] + origin[1],\n\tscale[2] * -origin[2] + origin[2],\n];\n\n/**\n * @description make a 3x4 matrix representing a uniform scale.\n * @param {number} [scale=1] the uniform scale factor\n * @param {[number, number, number]} [origin=[0,0,0]] the center of transformation\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3UniformScale = (scale = 1, origin = [0, 0, 0]) => (\n\tmakeMatrix3Scale([scale, scale, scale], origin)\n);\n\n/**\n * @description make a 3x4 representing a reflection across a line in the XY plane\n * This is a 2D operation, assumes everything is in the XY plane.\n * @param {[number, number]} vector one 2D vector specifying the reflection axis\n * @param {[number, number]} [origin=[0,0]] a 2D origin specifying a point of reflection\n * @returns {number[]} one 3x4 matrix\n */\nexport const makeMatrix3ReflectZ = (vector, origin = [0, 0]) => {\n\tconst m = makeMatrix2Reflect(vector, origin);\n\treturn [m[0], m[1], 0, m[2], m[3], 0, 0, 0, 1, m[4], m[5], 0];\n};\n\n/**\n * 2D operation, assuming everything is 0 in the z plane\n * @param line in vector-origin form\n * @returns matrix3\n */\n// export const make_matrix3_reflect = (vector, origin = [0, 0, 0]) => {\n//   // the line of reflection passes through origin, runs along vector\n//   return [];\n// };\n\n//               __                                           _\n//   _________  / /_  ______ ___  ____     ____ ___  ____ _  (_)___  _____\n//  / ___/ __ \\/ / / / / __ `__ \\/ __ \\   / __ `__ \\/ __ `/ / / __ \\/ ___/\n// / /__/ /_/ / / /_/ / / / / / / / / /  / / / / / / /_/ / / / /_/ / /\n// \\___/\\____/_/\\__,_/_/ /_/ /_/_/ /_/  /_/ /_/ /_/\\__,_/_/ /\\____/_/\n//                                                     /___/\n"
  },
  {
    "path": "src/math/matrix4.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tnormalize,\n\tnormalize3,\n\tsubtract3,\n\tcross3,\n\tresize,\n} from \"./vector.js\";\nimport {\n\tmakeMatrix2Reflect,\n} from \"./matrix2.js\";\n\n/**\n * 4x4 matrix methods. the fourth column is a translation vector\n * these methods depend on arrays of 16 items.\n */\n\n/**\n * @description the identity matrix for 3x3 matrices\n * @constant {number[]}\n * @default\n */\nexport const identity4x4 = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);\n\n/**\n * @description test if a 4x4 matrix is the identity matrix within an epsilon\n * @param {number[]} m a 4x4 matrix\n * @returns {boolean} true if the matrix is the identity matrix\n */\nexport const isIdentity4x4 = m => identity4x4\n\t.map((n, i) => Math.abs(n - m[i]) < EPSILON)\n\t.reduce((a, b) => a && b, true);\n\n/**\n * @description multiply one 3D vector by a 4x4 matrix\n * @param {number[]} m one matrix in array form\n * @param {[number, number, number]} vector in array form\n * @returns {[number, number, number]} the transformed vector\n */\nexport const multiplyMatrix4Vector3 = (m, vector) => [\n\tm[0] * vector[0] + m[4] * vector[1] + m[8] * vector[2] + m[12],\n\tm[1] * vector[0] + m[5] * vector[1] + m[9] * vector[2] + m[13],\n\tm[2] * vector[0] + m[6] * vector[1] + m[10] * vector[2] + m[14],\n];\n\n/**\n * @description multiply one 3D line by a 4x4 matrix\n * @param {number[]} m one matrix in array form\n * @param {[number, number, number]} vector the vector of the line\n * @param {[number, number, number]} origin the origin of the line\n * @returns {VecLine} the transformed line in vector-origin form\n */\nexport const multiplyMatrix4Line3 = (m, vector, origin) => ({\n\tvector: [\n\t\tm[0] * vector[0] + m[4] * vector[1] + m[8] * vector[2],\n\t\tm[1] * vector[0] + m[5] * vector[1] + m[9] * vector[2],\n\t\tm[2] * vector[0] + m[6] * vector[1] + m[10] * vector[2],\n\t],\n\torigin: [\n\t\tm[0] * origin[0] + m[4] * origin[1] + m[8] * origin[2] + m[12],\n\t\tm[1] * origin[0] + m[5] * origin[1] + m[9] * origin[2] + m[13],\n\t\tm[2] * origin[0] + m[6] * origin[1] + m[10] * origin[2] + m[14],\n\t],\n});\n\n/**\n * @description multiply two 4x4 matrices together\n * @param {number[]} m1 the first matrix\n * @param {number[]} m2 the second matrix\n * @returns {number[]} one matrix, the product of the two\n */\nexport const multiplyMatrices4 = (m1, m2) => [\n\tm1[0] * m2[0] + m1[4] * m2[1] + m1[8] * m2[2] + m1[12] * m2[3],\n\tm1[1] * m2[0] + m1[5] * m2[1] + m1[9] * m2[2] + m1[13] * m2[3],\n\tm1[2] * m2[0] + m1[6] * m2[1] + m1[10] * m2[2] + m1[14] * m2[3],\n\tm1[3] * m2[0] + m1[7] * m2[1] + m1[11] * m2[2] + m1[15] * m2[3],\n\tm1[0] * m2[4] + m1[4] * m2[5] + m1[8] * m2[6] + m1[12] * m2[7],\n\tm1[1] * m2[4] + m1[5] * m2[5] + m1[9] * m2[6] + m1[13] * m2[7],\n\tm1[2] * m2[4] + m1[6] * m2[5] + m1[10] * m2[6] + m1[14] * m2[7],\n\tm1[3] * m2[4] + m1[7] * m2[5] + m1[11] * m2[6] + m1[15] * m2[7],\n\tm1[0] * m2[8] + m1[4] * m2[9] + m1[8] * m2[10] + m1[12] * m2[11],\n\tm1[1] * m2[8] + m1[5] * m2[9] + m1[9] * m2[10] + m1[13] * m2[11],\n\tm1[2] * m2[8] + m1[6] * m2[9] + m1[10] * m2[10] + m1[14] * m2[11],\n\tm1[3] * m2[8] + m1[7] * m2[9] + m1[11] * m2[10] + m1[15] * m2[11],\n\tm1[0] * m2[12] + m1[4] * m2[13] + m1[8] * m2[14] + m1[12] * m2[15],\n\tm1[1] * m2[12] + m1[5] * m2[13] + m1[9] * m2[14] + m1[13] * m2[15],\n\tm1[2] * m2[12] + m1[6] * m2[13] + m1[10] * m2[14] + m1[14] * m2[15],\n\tm1[3] * m2[12] + m1[7] * m2[13] + m1[11] * m2[14] + m1[15] * m2[15],\n];\n\n/**\n * @description calculate the determinant of a 4x4 or 3x3 matrix.\n * in the case of 4x4, the translation component is ignored.\n * @param {number[]} m one matrix in array form\n * @returns {number} the determinant of the matrix\n */\nexport const determinant4 = (m) => {\n\tconst A2323 = m[10] * m[15] - m[11] * m[14];\n\tconst A1323 = m[9] * m[15] - m[11] * m[13];\n\tconst A1223 = m[9] * m[14] - m[10] * m[13];\n\tconst A0323 = m[8] * m[15] - m[11] * m[12];\n\tconst A0223 = m[8] * m[14] - m[10] * m[12];\n\tconst A0123 = m[8] * m[13] - m[9] * m[12];\n\treturn (\n\t\tm[0] * (m[5] * A2323 - m[6] * A1323 + m[7] * A1223)\n\t\t- m[1] * (m[4] * A2323 - m[6] * A0323 + m[7] * A0223)\n\t\t+ m[2] * (m[4] * A1323 - m[5] * A0323 + m[7] * A0123)\n\t\t- m[3] * (m[4] * A1223 - m[5] * A0223 + m[6] * A0123)\n\t);\n};\n\n/**\n * @description invert a 4x4 matrix\n * @param {number[]} m one matrix in array form\n * @returns {number[]|undefined} the inverted matrix, or undefined if not possible\n */\nexport const invertMatrix4 = (m) => {\n\tconst det = determinant4(m);\n\tif (Math.abs(det) < 1e-12 || Number.isNaN(det)\n\t\t|| !Number.isFinite(m[12]) || !Number.isFinite(m[13]) || !Number.isFinite(m[14])) {\n\t\treturn undefined;\n\t}\n\tconst A2323 = m[10] * m[15] - m[11] * m[14];\n\tconst A1323 = m[9] * m[15] - m[11] * m[13];\n\tconst A1223 = m[9] * m[14] - m[10] * m[13];\n\tconst A0323 = m[8] * m[15] - m[11] * m[12];\n\tconst A0223 = m[8] * m[14] - m[10] * m[12];\n\tconst A0123 = m[8] * m[13] - m[9] * m[12];\n\tconst A2313 = m[6] * m[15] - m[7] * m[14];\n\tconst A1313 = m[5] * m[15] - m[7] * m[13];\n\tconst A1213 = m[5] * m[14] - m[6] * m[13];\n\tconst A2312 = m[6] * m[11] - m[7] * m[10];\n\tconst A1312 = m[5] * m[11] - m[7] * m[9];\n\tconst A1212 = m[5] * m[10] - m[6] * m[9];\n\tconst A0313 = m[4] * m[15] - m[7] * m[12];\n\tconst A0213 = m[4] * m[14] - m[6] * m[12];\n\tconst A0312 = m[4] * m[11] - m[7] * m[8];\n\tconst A0212 = m[4] * m[10] - m[6] * m[8];\n\tconst A0113 = m[4] * m[13] - m[5] * m[12];\n\tconst A0112 = m[4] * m[9] - m[5] * m[8];\n\tconst inv = [\n\t\t+(m[5] * A2323 - m[6] * A1323 + m[7] * A1223),\n\t\t-(m[1] * A2323 - m[2] * A1323 + m[3] * A1223),\n\t\t+(m[1] * A2313 - m[2] * A1313 + m[3] * A1213),\n\t\t-(m[1] * A2312 - m[2] * A1312 + m[3] * A1212),\n\t\t-(m[4] * A2323 - m[6] * A0323 + m[7] * A0223),\n\t\t+(m[0] * A2323 - m[2] * A0323 + m[3] * A0223),\n\t\t-(m[0] * A2313 - m[2] * A0313 + m[3] * A0213),\n\t\t+(m[0] * A2312 - m[2] * A0312 + m[3] * A0212),\n\t\t+(m[4] * A1323 - m[5] * A0323 + m[7] * A0123),\n\t\t-(m[0] * A1323 - m[1] * A0323 + m[3] * A0123),\n\t\t+(m[0] * A1313 - m[1] * A0313 + m[3] * A0113),\n\t\t-(m[0] * A1312 - m[1] * A0312 + m[3] * A0112),\n\t\t-(m[4] * A1223 - m[5] * A0223 + m[6] * A0123),\n\t\t+(m[0] * A1223 - m[1] * A0223 + m[2] * A0123),\n\t\t-(m[0] * A1213 - m[1] * A0213 + m[2] * A0113),\n\t\t+(m[0] * A1212 - m[1] * A0212 + m[2] * A0112),\n\t];\n\tconst invDet = 1.0 / det;\n\treturn inv.map(n => n * invDet);\n};\nconst identity4x3 = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]);\n\n/**\n * @description make a 4x4 matrix representing a translation in 3D\n * @param {number} [x=0] the x component of the translation\n * @param {number} [y=0] the y component of the translation\n * @param {number} [z=0] the z component of the translation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4Translate = (x = 0, y = 0, z = 0) => [...identity4x3, x, y, z, 1];\n// i0 and i1 direct which columns and rows are filled\n// sgn manages right hand rule\nconst singleAxisRotate4 = (angle, origin, i0, i1, sgn) => {\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rotate = [...identity4x4];\n\trotate[i0 * 4 + i0] = cos;\n\trotate[i0 * 4 + i1] = (sgn ? +1 : -1) * sin;\n\trotate[i1 * 4 + i0] = (sgn ? -1 : +1) * sin;\n\trotate[i1 * 4 + i1] = cos;\n\tconst origin3 = [0, 1, 2].map(i => origin[i] || 0);\n\tconst trans = [...identity4x4];\n\tconst trans_inv = [...identity4x4];\n\t[12, 13, 14].forEach((i, j) => {\n\t\ttrans[i] = -origin3[j];\n\t\ttrans_inv[i] = origin3[j];\n\t});\n\treturn multiplyMatrices4(trans_inv, multiplyMatrices4(rotate, trans));\n};\n\n/**\n * @description make a 4x4 matrix representing a rotation in 3D around the x-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4RotateX = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate4(angle, origin, 1, 2, true));\n\n/**\n * @description make a 4x4 matrix representing a rotation in 3D around the y-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4RotateY = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate4(angle, origin, 0, 2, false));\n\n/**\n * @description make a 4x4 matrix representing a rotation in 3D around the z-axis\n * (allowing you to specify the center of rotation if needed).\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4RotateZ = (angle, origin = [0, 0, 0]) => (\n\tsingleAxisRotate4(angle, origin, 0, 1, true));\n\n/**\n * @description make a 4x4 matrix representing a rotation in 3D\n * around a given vector and around a given center of rotation.\n * @param {number} angle the angle of rotation in radians\n * @param {[number, number, number]} [vector=[0,0,1]] the axis of rotation\n * @param {[number, number, number]} [origin=[0,0,0]] the center of rotation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4Rotate = (angle, vector = [0, 0, 1], origin = [0, 0, 0]) => {\n\tconst pos = [0, 1, 2].map(i => origin[i] || 0);\n\tconst [x, y, z] = resize(3, normalize(vector));\n\tconst c = Math.cos(angle);\n\tconst s = Math.sin(angle);\n\tconst t = 1 - c;\n\tconst trans = makeMatrix4Translate(-pos[0], -pos[1], -pos[2]);\n\tconst trans_inv = makeMatrix4Translate(pos[0], pos[1], pos[2]);\n\treturn multiplyMatrices4(trans_inv, multiplyMatrices4([\n\t\tt * x * x + c, t * y * x + z * s, t * z * x - y * s, 0,\n\t\tt * x * y - z * s, t * y * y + c, t * z * y + x * s, 0,\n\t\tt * x * z + y * s, t * y * z - x * s, t * z * z + c, 0,\n\t\t0, 0, 0, 1], trans));\n};\n\n/**\n * @description make a 4x4 matrix representing a non-uniform scale.\n * @param {[number, number, number]} [scale=[1,1,1]] non-uniform scaling vector\n * @param {[number, number, number]} [origin=[0,0,0]] the center of transformation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4Scale = (scale = [1, 1, 1], origin = [0, 0, 0]) => [\n\tscale[0], 0, 0, 0,\n\t0, scale[1], 0, 0,\n\t0, 0, scale[2], 0,\n\tscale[0] * -origin[0] + origin[0],\n\tscale[1] * -origin[1] + origin[1],\n\tscale[2] * -origin[2] + origin[2],\n\t1,\n];\n\n/**\n * @description make a 4x4 matrix representing a uniform scale.\n * @param {number} [scale=1] the uniform scale factor\n * @param {[number, number, number]} [origin=[0,0,0]] the center of transformation\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4UniformScale = (scale = 1, origin = [0, 0, 0]) => (\n\tmakeMatrix4Scale([scale, scale, scale], origin)\n);\n\n/**\n * @description make a 4x4 representing a reflection across a line in the XY plane\n * This is a 2D operation, assumes everything is in the XY plane.\n * @param {[number, number]} vector one 2D vector specifying the reflection axis\n * @param {[number, number]} [origin=[0,0]] a 2D origin specifying a point of reflection\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeMatrix4ReflectZ = (vector, origin = [0, 0]) => {\n\tconst m = makeMatrix2Reflect(vector, origin);\n\treturn [m[0], m[1], 0, 0, m[2], m[3], 0, 0, 0, 0, 1, 0, m[4], m[5], 0, 1];\n};\n\n/**\n * @param {number} FOV field of view in radians\n * @param {number} aspect aspect ratio\n * @param {number} near z-near\n * @param {number} far z-far\n * @returns {number[]} one 4x4 matrix\n */\nexport const makePerspectiveMatrix4 = (FOV, aspect, near, far) => {\n\tconst f = Math.tan(Math.PI * 0.5 - 0.5 * FOV);\n\tconst rangeInv = 1.0 / (near - far);\n\tconst x = aspect < 1 ? f : f / aspect;\n\tconst y = aspect < 1 ? f * aspect : f;\n\treturn [\n\t\tx, 0, 0, 0,\n\t\t0, y, 0, 0,\n\t\t0, 0, (near + far) * rangeInv, -1,\n\t\t0, 0, near * far * rangeInv * 2, 0,\n\t];\n};\n\n/**\n * @param {number} top\n * @param {number} right\n * @param {number} bottom\n * @param {number} left\n * @param {number} near\n * @param {number} far\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeOrthographicMatrix4 = (top, right, bottom, left, near, far) => [\n\t2 / (right - left), 0, 0, 0,\n\t0, 2 / (top - bottom), 0, 0,\n\t0, 0, 2 / (near - far), 0,\n\t(left + right) / (left - right),\n\t(bottom + top) / (bottom - top),\n\t(near + far) / (near - far),\n\t1,\n];\n\n/**\n * @param {[number, number, number]} position the location of the camera in 3D space\n * @param {[number, number, number]} target the point in space the camera is looking towards\n * @param {[number, number, number]} up the vector pointing up out the top of the camera.\n * @returns {number[]} one 4x4 matrix\n */\nexport const makeLookAtMatrix4 = (position, target, up) => {\n\tconst zAxis = normalize3(subtract3(position, target));\n\tconst xAxis = normalize3(cross3(up, zAxis));\n\tconst yAxis = normalize3(cross3(zAxis, xAxis));\n\treturn [\n\t\txAxis[0], xAxis[1], xAxis[2], 0,\n\t\tyAxis[0], yAxis[1], yAxis[2], 0,\n\t\tzAxis[0], zAxis[1], zAxis[2], 0,\n\t\tposition[0], position[1], position[2], 1,\n\t];\n};\n\n/**\n * 2D operation, assuming everything is 0 in the z plane\n * @param line in vector-origin form\n * @returns matrix3\n */\n// export const make_matrix3_reflect = (vector, origin = [0, 0, 0]) => {\n//   // the line of reflection passes through origin, runs along vector\n//   return [];\n// };\n\n//               __                                           _\n//   _________  / /_  ______ ___  ____     ____ ___  ____ _  (_)___  _____\n//  / ___/ __ \\/ / / / / __ `__ \\/ __ \\   / __ `__ \\/ __ `/ / / __ \\/ ___/\n// / /__/ /_/ / / /_/ / / / / / / / / /  / / / / / / /_/ / / / /_/ / /\n// \\___/\\____/_/\\__,_/_/ /_/ /_/_/ /_/  /_/ /_/ /_/\\__,_/_/ /\\____/_/\n//                                                     /___/\n"
  },
  {
    "path": "src/math/nearest.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tclampLine,\n\tclampSegment,\n} from \"./line.js\";\nimport {\n\tarrayMinimumIndex,\n} from \"../general/array.js\";\nimport {\n\tmagSquared,\n\tdistance,\n\tdistance2,\n\tadd,\n\tadd2,\n\tsubtract,\n\tsubtract2,\n\tnormalize2,\n\tdot,\n\tscale,\n\tscale2,\n\tresize,\n} from \"./vector.js\";\n\n/**\n * @description find the one point in an array of 2D points closest to a 2D point.\n * @param {[number, number][]} points an array of 2D points to test against\n * @param {[number, number]} point the 2D point to test nearness to\n * @returns {[number, number]} one point from the array of points\n */\nexport const nearestPoint2 = (points, point) => {\n\t// todo speed up with partitioning\n\tconst index = arrayMinimumIndex(points, el => distance2(el, point));\n\treturn index === undefined ? undefined : points[index];\n};\n\n/**\n * @description find the one point in an array of points closest to a point.\n * @param {number[][]} points an array of points to test against\n * @param {number[]} point the point to test nearness to\n * @returns {number[]} one point from the array of points\n */\nexport const nearestPoint = (points, point) => {\n\t// todo speed up with partitioning\n\t// const index = arrayMinimumIndex(points, point, distance);\n\tconst index = arrayMinimumIndex(points, el => distance(el, point));\n\treturn index === undefined ? undefined : points[index];\n};\n\n/**\n * @description find the nearest point on a line, ray, or segment.\n * @param {VecLine} line a line with a vector and origin\n * @param {[number, number]|[number, number, number]} point the point to test nearness to\n * @param {function} clampFunc a clamp function to bound a calculation between 0 and 1\n * for segments, greater than 0 for rays, or unbounded for lines.\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {[number, number]|[number, number, number]} a point\n */\nexport const nearestPointOnLine = (\n\t{ vector, origin },\n\tpoint,\n\tclampFunc = clampLine,\n\tepsilon = EPSILON,\n) => {\n\tconst originN = resize(vector.length, origin);\n\tconst pointN = resize(vector.length, point);\n\tconst magSq = magSquared(vector);\n\tconst vectorToPoint = subtract(pointN, originN);\n\tconst dotProd = dot(vector, vectorToPoint);\n\tconst dist = dotProd / magSq;\n\t// clamp depending on line, ray, segment\n\tconst d = clampFunc(dist, epsilon);\n\tconst [a, b, c] = add(originN, scale(vector, d));\n\treturn vector.length === 2 ? [a, b] : [a, b, c];\n};\n\n/**\n * @description given a polygon and a point, in 2D,\n * find a point on the boundary of the polygon\n * that is closest to the provided point.\n * @param {[number, number][]} polygon an array of points (which are arrays of numbers)\n * @param {[number, number]} point the point to test nearness to\n * @returns {object} a point\n * edge index matches vertices such that edge(N) = [vert(N), vert(N + 1)]\n */\nexport const nearestPointOnPolygon = (polygon, point) => polygon\n\t.map((p, i, arr) => subtract2(arr[(i + 1) % arr.length], p))\n\t.map((vector, i) => ({ vector, origin: polygon[i] }))\n\t.map(line => nearestPointOnLine(line, point, clampSegment))\n\t.map((p, edge) => ({ point: p, edge, distance: distance2(p, point) }))\n\t.sort((a, b) => a.distance - b.distance)\n\t.shift();\n\n/**\n * @description find the nearest point on the boundary of a circle to another point\n * that is closest to the provided point.\n * @param {Circle} circle object with \"radius\" (number) and \"origin\" (number[])\n * @param {[number, number]} point the point to test nearness to\n * @returns {[number, number]} a point\n */\nexport const nearestPointOnCircle = ({ radius, origin }, point) => (\n\tadd2(origin, scale2(normalize2(subtract2(point, origin)), radius))\n);\n\n// todo\n// const nearestPointOnEllipse = () => false;\n"
  },
  {
    "path": "src/math/overlap.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\texclude,\n\tincludeL,\n} from \"./compare.js\";\nimport {\n\tdot2,\n\tmagSquared,\n\tcross2,\n\tsubtract2,\n\trotate90,\n} from \"./vector.js\";\n\n/**\n * @description check if a point lies collinear along a line,\n * and specify if the line is a line/ray/segment and test whether\n * the point lies within endpoint(s).\n * @param {VecLine2} line a line in \"vector\" \"origin\" form\n * @param {[number, number]|[number, number, number]} point one 2D point\n * @parma {function} [lineDomain=includeL] the domain of the line\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean} is the point collinear to the line,\n * and in the case of ray/segment,\n * does the point lie within the bounds of the ray/segment?\n */\nexport const overlapLinePoint = (\n\t{ vector, origin },\n\tpoint,\n\tlineDomain = includeL,\n\tepsilon = EPSILON,\n) => {\n\tconst p2p = subtract2(point, origin);\n\tconst lineMagSq = magSquared(vector);\n\tconst lineMag = Math.sqrt(lineMagSq);\n\t// the line is degenerate\n\tif (lineMag < epsilon) { return false; }\n\t/** @type {[number, number]} */\n\tconst vecScaled = [vector[0] / lineMag, vector[1] / lineMag];\n\tconst cross = cross2(p2p, vecScaled);\n\tconst proj = dot2(p2p, vector) / lineMagSq;\n\treturn Math.abs(cross) < epsilon && lineDomain(proj, epsilon / lineMag);\n};\n\n/**\n * @description Test if a point is inside a convex polygon.\n * @param {([number, number]|[number, number, number])[]} polygon\n * a polygon in array of array form\n * @param {[number, number]|[number, number, number]} point a point in array form\n * @param {function} polyDomain determines if the polygon boundary\n * is inclusive or exclusive\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {{ overlap: boolean, t: number[] }} an object with\n * - \"overlap\" {boolean}: true or false if the point is inside the polygon\n * - \"t\" {number[]}: the array of cross-product parameters of the point against\n *   every polygon's edge's vector. Can be used to trilaterate the point back\n *   into position.\n */\nexport const overlapConvexPolygonPoint = (\n\tpolygon,\n\tpoint,\n\tpolyDomain = exclude,\n\tepsilon = EPSILON,\n) => {\n\tconst t = polygon\n\t\t.map((p, i, arr) => [p, arr[(i + 1) % arr.length]])\n\t\t.map(([a, b]) => [subtract2(b, a), subtract2(point, a)])\n\t\t.map(([a, b]) => cross2(a, b));\n\tconst sign = Math.sign(t.reduce((a, b) => a + b, 0));\n\tconst overlap = t\n\t\t.map(n => n * sign)\n\t\t.map(side => polyDomain(side, epsilon))\n\t\t.map((s, _, arr) => s === arr[0])\n\t\t.reduce((prev, curr) => prev && curr, true);\n\treturn { overlap, t };\n};\n\n/**\n * @description Find out if two convex polygons are overlapping by searching\n * for a dividing axis, which should be one side from one of the polygons.\n * This method is hard-coded to be exclusive, if two otherwise non-overlapping\n * polygons share an overlapping edge, the method will still count the\n * two polygons as not overlapping.\n * @param {[number, number][]} poly1 a polygon as an array of points\n * @param {[number, number][]} poly2 a polygon as an array of points\n * @param {number} [epsilon=1e-6] an optional epsilon\n */\nexport const overlapConvexPolygons = (poly1, poly2, epsilon = EPSILON) => {\n\tfor (let p = 0; p < 2; p += 1) {\n\t\t// for non-overlapping convex polygons, it's possible that only only\n\t\t// one edge on one polygon holds the property of being a dividing axis.\n\t\t// we must run the algorithm on both polygons\n\t\tconst polyA = p === 0 ? poly1 : poly2;\n\t\tconst polyB = p === 0 ? poly2 : poly1;\n\t\tfor (let i = 0; i < polyA.length; i += 1) {\n\t\t\t// each edge of polygonA will become a line\n\t\t\tconst origin = polyA[i];\n\t\t\tconst vector = rotate90(subtract2(polyA[(i + 1) % polyA.length], polyA[i]));\n\t\t\t// project each point from the other polygon on to the line's perpendicular\n\t\t\t// also, subtracting the origin (from the first poly) such that the\n\t\t\t// numberline is centered around zero. if the test passes, this polygon's\n\t\t\t// projections will be entirely above or below 0.\n\t\t\tconst projected = polyB\n\t\t\t\t.map(point => subtract2(point, origin))\n\t\t\t\t.map(v => dot2(vector, v));\n\t\t\t// is the first polygon on the positive or negative side?\n\t\t\tconst other_test_point = polyA[(i + 2) % polyA.length];\n\t\t\tconst side_a = dot2(vector, subtract2(other_test_point, origin));\n\t\t\tconst side = side_a > 0; // use 0. not epsilon\n\t\t\t// is the second polygon on whichever side of 0 that the first isn't?\n\t\t\tconst one_sided = projected\n\t\t\t\t.map(dotProd => (side ? dotProd < epsilon : dotProd > -epsilon))\n\t\t\t\t.reduce((a, b) => a && b, true);\n\t\t\t// if true, we found a dividing axis\n\t\t\tif (one_sided) { return false; }\n\t\t}\n\t}\n\treturn true;\n};\n\n/**\n * @description Test if two axis-aligned bounding boxes overlap each other.\n * By default, the boundaries are treated as inclusive.\n * @param {Box} box1 an axis-aligned bounding box\n * @param {Box} box2 an axis-aligned bounding box\n * @param {number} [epsilon=1e-6] an optional epsilon,\n * positive value (default) is inclusive, negative is exclusive.\n * @returns {boolean} true if the bounding boxes overlap each other\n */\nexport const overlapBoundingBoxes = (box1, box2, epsilon = EPSILON) => {\n\tconst dimensions = Math.min(box1.min.length, box2.min.length);\n\tfor (let d = 0; d < dimensions; d += 1) {\n\t\t// if one minimum is above the other's maximum, or visa versa\n\t\tif (box1.min[d] > box2.max[d] + epsilon\n\t\t\t|| box1.max[d] < box2.min[d] - epsilon) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n};\n"
  },
  {
    "path": "src/math/plane.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tnormalize3,\n\tdot3,\n\tscale3,\n\tsubtract3,\n\tresize3,\n} from \"./vector.js\";\n\n/**\n * @description Project a point onto a plane in 3D.\n * @param {[number, number] | [number, number, number]} point a point as an array of numbers\n * @param {[number, number, number]} [vector=[1, 0, 0]] a vector that defines a plane's normal\n * @param {[number, number, number]} [origin=[0, 0, 0]] a point that the plane passes through\n * @returns {[number, number, number]} the transformed point\n */\nexport const projectPointOnPlane = (point, vector = [1, 0, 0], origin = [0, 0, 0]) => {\n\tconst point3 = resize3(point);\n\tconst originToPoint = subtract3(point3, resize3(origin));\n\tconst normalized = normalize3(resize3(vector));\n\tconst magnitude = dot3(normalized, originToPoint);\n\tconst planeToPoint = scale3(normalized, magnitude);\n\treturn subtract3(point3, planeToPoint);\n};\n"
  },
  {
    "path": "src/math/polygon.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n\tTWO_PI,\n} from \"./constant.js\";\nimport {\n\tcross2,\n\tscale2,\n\tadd2,\n\tsubtract,\n\tsubtract3,\n\tparallel,\n} from \"./vector.js\";\n\n/**\n * the radius parameter measures from the center to the midpoint of the edge\n * vertex-axis aligned\n * todo: also possible to parameterize the radius as the center to the points\n * todo: can be edge-aligned\n */\nconst angleArray = count => Array\n\t.from(Array(Math.floor(count)))\n\t.map((_, i) => TWO_PI * (i / count));\n\nconst anglesToVecs = (angles, radius) => angles\n\t.map(a => [radius * Math.cos(a), radius * Math.sin(a)]);\n\n// a = 2r tan(π/n)\n\n/**\n * @description Make a regular polygon from a circumradius,\n * the first point is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [circumradius=1] the polygon's circumradius\n * @returns {[number, number][]} an array of 2D points\n */\nexport const makePolygonCircumradius = (sides = 3, circumradius = 1) => (\n\tanglesToVecs(angleArray(sides), circumradius)\n);\n\n/**\n * @description Make a regular polygon from a circumradius,\n * the middle of the first side is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [circumradius=1] the polygon's circumradius\n * @returns {[number, number][]} an array of points, each point as an arrays of numbers\n */\nexport const makePolygonCircumradiusSide = (sides = 3, circumradius = 1) => {\n\tconst halfwedge = Math.PI / sides;\n\tconst angles = angleArray(sides).map(a => a + halfwedge);\n\treturn anglesToVecs(angles, circumradius);\n};\n\n/**\n * @description Make a regular polygon from a inradius,\n * the first point is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [inradius=1] the polygon's inradius\n * @returns {[number, number][]} an array of points, each point as an arrays of numbers\n */\nexport const makePolygonInradius = (sides = 3, inradius = 1) => (\n\tmakePolygonCircumradius(sides, inradius / Math.cos(Math.PI / sides)));\n\n/**\n * @description Make a regular polygon from a inradius,\n * the middle of the first side is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [inradius=1] the polygon's inradius\n * @returns {[number, number][]} an array of points, each point as an arrays of numbers\n */\nexport const makePolygonInradiusSide = (sides = 3, inradius = 1) => (\n\tmakePolygonCircumradiusSide(sides, inradius / Math.cos(Math.PI / sides)));\n\n/**\n * @description Make a regular polygon from a side length,\n * the first point is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [length=1] the polygon's side length\n * @returns {[number, number][]} an array of points, each point as an arrays of numbers\n */\nexport const makePolygonSideLength = (sides = 3, length = 1) => (\n\tmakePolygonCircumradius(sides, (length / 2) / Math.sin(Math.PI / sides)));\n\n/**\n * @description Make a regular polygon from a side length,\n * the middle of the first side is +X aligned.\n * @param {number} sides the number of sides in the polygon\n * @param {number} [length=1] the polygon's side length\n * @returns {[number, number][]} an array of points, each point as an arrays of numbers\n */\nexport const makePolygonSideLengthSide = (sides = 3, length = 1) => (\n\tmakePolygonCircumradiusSide(sides, (length / 2) / Math.sin(Math.PI / sides)));\n\n/**\n * @description Remove any collinear vertices from a n-dimensional polygon.\n * @param {([number, number] | [number, number, number])[]} polygon a polygon\n * as an array of ordered points in array form\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {([number, number] | [number, number, number])[]} a copy of\n * the polygon with collinear points removed\n */\nexport const makePolygonNonCollinear = (polygon, epsilon = EPSILON) => {\n\t// index map [i] to [i, i+1]\n\tconst edges_vector = polygon\n\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t.map(pair => subtract(pair[1], pair[0]));\n\t// the vertex to be removed. true=valid, false=collinear.\n\t// ask if an edge is parallel to its predecessor, this way,\n\t// the edge index will match to the collinear vertex.\n\tconst vertex_collinear = edges_vector\n\t\t.map((vector, i, arr) => [vector, arr[(i + arr.length - 1) % arr.length]])\n\t\t.map(pair => !parallel(pair[1], pair[0], epsilon));\n\treturn polygon.filter((_, v) => vertex_collinear[v]);\n};\n\n/**\n * @description Remove any collinear vertices from a n-dimensional polygon.\n * @param {[number, number, number][]} polygon a polygon\n * as an array of ordered points in array form\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {[number, number, number][]} a copy of\n * the polygon with collinear points removed\n */\nexport const makePolygonNonCollinear3 = (polygon, epsilon = EPSILON) => {\n\t// index map [i] to [i, i+1]\n\tconst edges_vector = polygon\n\t\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t\t.map(pair => subtract3(pair[1], pair[0]));\n\t// the vertex to be removed. true=valid, false=collinear.\n\t// ask if an edge is parallel to its predecessor, this way,\n\t// the edge index will match to the collinear vertex.\n\tconst vertex_collinear = edges_vector\n\t\t.map((vector, i, arr) => [vector, arr[(i + arr.length - 1) % arr.length]])\n\t\t.map(pair => !parallel(pair[1], pair[0], epsilon));\n\treturn polygon.filter((_, v) => vertex_collinear[v]);\n};\n\n/**\n * @description Calculates the signed area of a polygon.\n * This requires the polygon be non-self-intersecting.\n * @param {[number, number][]} points an array of 2D points,\n * which are arrays of numbers\n * @returns {number} the area of the polygon\n * @example\n * var area = polygon.signedArea([ [1,2], [5,6], [7,0] ])\n */\nexport const signedArea = points => 0.5 * points\n\t.map((el, i, arr) => [el, arr[(i + 1) % arr.length]])\n\t.map(([a, b]) => cross2(a, b))\n\t.reduce((a, b) => a + b, 0);\n\n/**\n * @description Calculates the centroid or the center of mass of the polygon.\n * @param {[number, number][]} points an array of 2D points, which are arrays of numbers\n * @returns {[number, number]} one 2D point as an array of numbers\n * @example\n * var centroid = polygon.centroid([ [1,2], [8,9], [8,0] ])\n */\nexport const centroid = (points) => {\n\tconst sixthArea = 1 / (6 * signedArea(points));\n\tconst sum = points\n\t\t.map((el, i, arr) => [el, arr[(i + 1) % arr.length]])\n\t\t.map(([a, b]) => scale2(add2(a, b), cross2(a, b)))\n\t\t.reduce((a, b) => add2(a, b), [0, 0]);\n\treturn [sum[0] * sixthArea, sum[1] * sixthArea];\n};\n\n/**\n * @description Given a list of points, get the dimension of the first\n * point in the list, return the dimension. Expecting either 2 or 3.\n * @param {number[][]} points\n * @returns {number} the dimension of the first point in the list\n */\nconst getDimension = (points) => {\n\tfor (let i = 0; i < points.length; i += 1) {\n\t\tif (points[i] && points[i].length) { return points[i].length; }\n\t}\n\treturn 0;\n};\n\n/**\n * @description Make an axis-aligned bounding box that encloses a set of points.\n * the optional padding is used to make the bounding box inclusive / exclusive\n * by adding padding on all sides, or inset in the case of negative number.\n * (positive=inclusive boundary, negative=exclusive boundary)\n * @param {number[][]} points an array of unsorted points, in any dimension\n * @param {number} [padding=0] optionally add padding around the box\n * @returns {Box?} an object where \"min\" and \"max\" are two points and\n * \"span\" is the lengths. returns \"undefined\" if no points were provided.\n */\nexport const boundingBox = (points, padding = 0) => {\n\tif (!points || !points.length) { return undefined; }\n\tconst dimension = getDimension(points);\n\tconst min = Array(dimension).fill(Infinity);\n\tconst max = Array(dimension).fill(-Infinity);\n\tpoints\n\t\t.filter(p => p !== undefined)\n\t\t.forEach(point => point\n\t\t\t.forEach((c, i) => {\n\t\t\t\tif (c < min[i]) { min[i] = c - padding; }\n\t\t\t\tif (c > max[i]) { max[i] = c + padding; }\n\t\t\t}));\n\tconst span = max.map((m, i) => m - min[i]);\n\treturn { min, max, span };\n};\n"
  },
  {
    "path": "src/math/polynomial.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"./constant.js\";\n\n/**\n * @description perform a cube root but preserve the sign of the input\n */\nconst cubeRootSigned = (n) => (n < 0\n\t? -((-n) ** (1 / 3))\n\t: (n ** (1 / 3))\n);\n\n/**\n * @description This polynomial solver will solve anything polynomial up\n * to a degree 3 (cubic, via Cardano's formula). Polynomial is inputted via an\n * array of its coefficients, which includes the constant, in order from\n * highest exponent down to the constant. Do not skip any 0 coefficients.\n * To solve a quadratic: supply [a, b, c], where a is the x^2 coeff.\n * To solve a cubic: supply [a, b, c, d], where a is the x^3 coeff.\n * @param {number[]} coefficients a list of coefficients to the polynomial,\n * including the constant (at the end).\n * @returns {[]|[number]|[number,number]|[number,number,number]}\n * a solution array with either zero, one, two, or three numbers.\n */\nexport const polynomialSolver = (coefficients) => {\n\tconst [a, b, c, d] = coefficients;\n\n\t// the degree of the polynomial is the length of the array - 1,\n\t// because the constant is included as the last element.\n\tswitch (coefficients.length) {\n\tcase 2: return [-a / b];\n\tcase 3: {\n\t\t// quadratic\n\t\tconst discriminant = (b ** 2) - (4 * a * c);\n\n\t\t// no solution\n\t\tif (discriminant < -EPSILON) { return []; }\n\n\t\t// one solution\n\t\tconst q1 = -b / (2 * c);\n\t\tif (discriminant < EPSILON) { return [q1]; }\n\n\t\t// two solutions\n\t\tconst q2 = Math.sqrt(discriminant) / (2 * c);\n\t\treturn [q1 + q2, q1 - q2];\n\t}\n\tcase 4: {\n\t\t// cubic: Cardano's formula\n\t\tconst a2 = c / d;\n\t\tconst a1 = b / d;\n\t\tconst a0 = a / d;\n\t\tconst q = (3 * a1 - (a2 ** 2)) / 9;\n\t\tconst r = (9 * a2 * a1 - 27 * a0 - 2 * (a2 ** 3)) / 54;\n\t\tconst d0 = (q ** 3) + (r ** 2);\n\t\tconst u = -a2 / 3;\n\n\t\t// one solution\n\t\tif (d0 > 0) {\n\t\t\tconst sqrt_d0 = Math.sqrt(d0);\n\t\t\tconst s = cubeRootSigned(r + sqrt_d0);\n\t\t\tconst t = cubeRootSigned(r - sqrt_d0);\n\t\t\treturn [u + s + t];\n\t\t}\n\n\t\t// two solutions\n\t\tif (Math.abs(d0) < EPSILON) {\n\t\t\t// if r is negative, s will be NaN\n\t\t\tif (r < 0) { return []; }\n\t\t\tconst s = (r ** (1 / 3));\n\t\t\treturn [u + 2 * s, u - s];\n\t\t}\n\n\t\t// three solutions\n\t\tconst sqrt_d0 = Math.sqrt(-d0);\n\t\tconst phi = Math.atan2(sqrt_d0, r) / 3;\n\t\tconst r_s = ((r ** 2) - d0) ** (1 / 6);\n\t\tconst s_r = r_s * Math.cos(phi);\n\t\tconst s_i = r_s * Math.sin(phi);\n\t\treturn [\n\t\t\tu + 2 * s_r,\n\t\t\tu - s_r - Math.sqrt(3) * s_i,\n\t\t\tu - s_r + Math.sqrt(3) * s_i,\n\t\t];\n\t}\n\tdefault: return [];\n\t}\n};\n"
  },
  {
    "path": "src/math/quaternion.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tdot,\n\tcross3,\n\tmagnitude,\n\tnormalize,\n} from \"./vector.js\";\nimport {\n\tmultiplyMatrices4,\n} from \"./matrix4.js\";\n\n/**\n * Quaternions encoded as an array of numbers\n * with indices 0-3 as x, y, z, w in that order.\n */\n\n/**\n * @description Create a quaternion which represents a rotation from\n * one 3D vector to another. Quaternion encoded as 0:x, 1:y, 2:z, 3:w.\n * @param {[number, number, number]} u a 3D vector\n * @param {[number, number, number]} v a 3D vector\n * @returns {[number, number, number, number]} a quaternion representing a rotation\n */\nexport const quaternionFromTwoVectors = (u, v) => {\n\tconst w = cross3(u, v);\n\tconst q = [w[0], w[1], w[2], dot(u, v)];\n\tq[3] += magnitude(q);\n\tconst [a, b, c, d] = normalize(q);\n\treturn [a, b, c, d];\n};\n\n/**\n * @description Create a 4x4 matrix from a quaternion,\n * the quaternion encoded as 0:x, 1:y, 2:z, 3:w.\n * @param {[number, number, number, number]} q a quaternion\n * @returns {number[]} a 4x4 matrix (array of 16 numbers)\n */\nexport const matrix4FromQuaternion = (q) => multiplyMatrices4([\n\t+q[3], +q[2], -q[1], +q[0],\n\t-q[2], +q[3], +q[0], +q[1],\n\t+q[1], -q[0], +q[3], +q[2],\n\t-q[0], -q[1], -q[2], +q[3],\n], [\n\t+q[3], +q[2], -q[1], -q[0],\n\t-q[2], +q[3], +q[0], -q[1],\n\t+q[1], -q[0], +q[3], -q[2],\n\t+q[0], +q[1], +q[2], +q[3],\n]);\n"
  },
  {
    "path": "src/math/radial.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n\tTWO_PI,\n} from \"./constant.js\";\nimport {\n\tvectorToAngle,\n\tangleToVector,\n} from \"./convert.js\";\nimport {\n\tepsilonEqual,\n} from \"./compare.js\";\nimport {\n\tcross2,\n\tnormalize2,\n\tsubtract2,\n\tdistance2,\n} from \"./vector.js\";\n\n/**\n * measurements involving vectors and radians\n */\n\n/**\n * @description check if the first parameter is counter-clockwise between A and B.\n * floor and ceiling can be unbounded, this method takes care of 0-2pi wrap around.\n * @param {number} angle angle in radians\n * @param {number} floor angle in radians, lower bound\n * @param {number} ceiling angle in radians, upper bound\n * @returns {boolean} is the angle between floor and ceiling\n */\nexport const isCounterClockwiseBetween = (angle, floor, ceiling) => {\n\t// eslint-disable-next-line no-param-reassign\n\twhile (ceiling < floor) { ceiling += TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (angle > floor) { angle -= TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (angle < floor) { angle += TWO_PI; }\n\treturn angle < ceiling;\n};\n\n/**\n * @description There are 2 interior angles between 2 vectors (as an angle in radians),\n * A-to-B clockwise, and A-to-B counter-clockwise. Get the clockwise one from A to B.\n * @param {number} a vector as an angle in radians\n * @param {number} b vector as an angle in radians\n * @returns {number} interior angle in radians\n */\nexport const clockwiseAngleRadians = (a, b) => {\n\t// this is on average 50 to 100 times faster than clockwiseAngle2\n\t// eslint-disable-next-line no-param-reassign\n\twhile (a < 0) { a += TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (b < 0) { b += TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (a > TWO_PI) { a -= TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (b > TWO_PI) { b -= TWO_PI; }\n\tconst a_b = a - b;\n\treturn (a_b >= 0)\n\t\t? a_b\n\t\t: TWO_PI - (b - a);\n};\n\n/**\n * @description There are 2 interior angles between 2 vectors (as an angle in radians),\n * A-to-B clockwise, and A-to-B counter-clockwise. Get the counter-clockwise one from A to B.\n * @param {number} a vector as an angle in radians\n * @param {number} b vector as an angle in radians\n * @returns {number} interior angle in radians, counter-clockwise from a to b\n */\nexport const counterClockwiseAngleRadians = (a, b) => {\n\t// this is on average 50 to 100 times faster than counterClockwiseAngle2\n\t// eslint-disable-next-line no-param-reassign\n\twhile (a < 0) { a += TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (b < 0) { b += TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (a > TWO_PI) { a -= TWO_PI; }\n\t// eslint-disable-next-line no-param-reassign\n\twhile (b > TWO_PI) { b -= TWO_PI; }\n\tconst b_a = b - a;\n\treturn (b_a >= 0)\n\t\t? b_a\n\t\t: TWO_PI - (a - b);\n};\n\n/**\n * @description There are 2 interior angles between 2 vectors, A-to-B clockwise,\n * and A-to-B counter-clockwise. Get the clockwise one from A to B.\n * @param {[number, number]} a 2D vector as an array of two numbers\n * @param {[number, number]} b 2D vector as an array of two numbers\n * @returns {number} interior angle in radians, clockwise from a to b\n */\nexport const clockwiseAngle2 = (a, b) => {\n\tconst dotProduct = b[0] * a[0] + b[1] * a[1];\n\tconst determinant = b[0] * a[1] - b[1] * a[0];\n\tlet angle = Math.atan2(determinant, dotProduct);\n\tif (angle < 0) { angle += TWO_PI; }\n\treturn angle;\n};\n\n/**\n * @description There are 2 interior angles between 2 vectors, A-to-B clockwise,\n * and A-to-B counter-clockwise. Get the counter-clockwise one from A to B.\n * @param {[number, number]} a 2D vector as an array of two numbers\n * @param {[number, number]} b 2D vector as an array of two numbers\n * @returns {number} interior angle in radians, counter-clockwise from a to b\n */\nexport const counterClockwiseAngle2 = (a, b) => {\n\tconst dotProduct = a[0] * b[0] + a[1] * b[1];\n\tconst determinant = a[0] * b[1] - a[1] * b[0];\n\tlet angle = Math.atan2(determinant, dotProduct);\n\tif (angle < 0) { angle += TWO_PI; }\n\treturn angle;\n};\n\n/**\n * this calculates an angle bisection between the pair of vectors\n * clockwise from the first vector to the second\n *\n *     a  x\n *       /     . bisection\n *      /   .\n *     / .\n *     --------x  b\n */\n\n/**\n * @description calculate the angle bisection clockwise from the first vector to the second.\n * @param {[number, number]} a one 2D vector\n * @param {[number, number]} b one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const clockwiseBisect2 = (a, b) => (\n\tangleToVector(vectorToAngle(a) - clockwiseAngle2(a, b) / 2)\n);\n\n/**\n * @description calculate the angle bisection counter-clockwise from the first vector to the second.\n * @param {[number, number]} a one 2D vector\n * @param {[number, number]} b one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const counterClockwiseBisect2 = (a, b) => (\n\tangleToVector(vectorToAngle(a) + counterClockwiseAngle2(a, b) / 2)\n);\n\n/**\n * @description subsect into n-divisions the angle clockwise from one angle to the next\n * @param {number} divisions number of angles minus 1\n * @param {number} angleA one angle in radians\n * @param {number} angleB one angle in radians\n * @returns {number[]} array of angles in radians\n */\nexport const clockwiseSubsectRadians = (angleA, angleB, divisions) => {\n\tconst angle = clockwiseAngleRadians(angleA, angleB) / divisions;\n\treturn Array.from(Array(divisions - 1))\n\t\t.map((_, i) => angleA + angle * (i + 1));\n};\n\n/**\n * @description subsect into n-divisions the angle counter-clockwise from one angle to the next\n * @param {number} divisions number of angles minus 1\n * @param {number} angleA one angle in radians\n * @param {number} angleB one angle in radians\n * @returns {number[]} array of angles in radians\n */\nexport const counterClockwiseSubsectRadians = (angleA, angleB, divisions) => {\n\tconst angle = counterClockwiseAngleRadians(angleA, angleB) / divisions;\n\treturn Array.from(Array(divisions - 1))\n\t\t.map((_, i) => angleA + angle * (i + 1));\n};\n\n/**\n * @description subsect into n-divisions the angle clockwise from one vector to the next\n * @param {number} divisions number of angles minus 1\n * @param {[number, number]} vectorA one vector in array form\n * @param {[number, number]} vectorB one vector in array form\n * @returns {[number, number][]} array of vectors (which are arrays of numbers)\n */\nexport const clockwiseSubsect2 = (vectorA, vectorB, divisions) => {\n\tconst angleA = Math.atan2(vectorA[1], vectorA[0]);\n\tconst angleB = Math.atan2(vectorB[1], vectorB[0]);\n\treturn clockwiseSubsectRadians(angleA, angleB, divisions)\n\t\t.map(angleToVector);\n};\n\n/**\n * @description subsect into n-divisions the angle counter-clockwise from one vector to the next\n * @param {number} divisions number of angles minus 1\n * @param {[number, number]} vectorA one 2D vector in array form\n * @param {[number, number]} vectorB one 2D vector in array form\n * @returns {[number, number][]} array of vectors (which are arrays of numbers)\n */\nexport const counterClockwiseSubsect2 = (vectorA, vectorB, divisions) => {\n\tconst angleA = Math.atan2(vectorA[1], vectorA[0]);\n\tconst angleB = Math.atan2(vectorB[1], vectorB[0]);\n\treturn counterClockwiseSubsectRadians(angleA, angleB, divisions)\n\t\t.map(angleToVector);\n};\n\n/**\n * @description sort an array of angles in radians by getting an array of\n * reference indices to the input array, instead of an array of angles.\n * @todo maybe there is such thing as an absolute radial origin (x axis?)\n * but this chooses the first element as the first element\n * and sort everything else counter-clockwise around it.\n * @param {number[]} radians array of angles in radians\n * @returns {number[]} array of indices of the input array, indicating\n * the counter-clockwise sorted arrangement.\n */\nexport const counterClockwiseOrderRadians = (radians) => {\n\tconst counter_clockwise = radians\n\t\t.map((_, i) => i)\n\t\t.sort((a, b) => radians[a] - radians[b]);\n\treturn counter_clockwise\n\t\t.slice(counter_clockwise.indexOf(0), counter_clockwise.length)\n\t\t.concat(counter_clockwise.slice(0, counter_clockwise.indexOf(0)));\n};\n\n/**\n * @description sort an array of vectors by getting an array of\n * reference indices to the input array, instead of a sorted array of vectors.\n * @param {[number, number][]} vectors array of vectors (which are arrays of numbers)\n * @returns {number[]} array of indices of the input array, indicating\n * the counter-clockwise sorted arrangement.\n */\nexport const counterClockwiseOrder2 = (vectors) => (\n\tcounterClockwiseOrderRadians(vectors.map(vectorToAngle))\n);\n\n/**\n * @description given an array of angles, return the sector angles between\n * consecutive parameters. if radially unsorted, this will sort them.\n * @param {number[]} radians array of angles in radians\n * @returns {number[]} array of sector angles in radians\n */\nexport const counterClockwiseSectorsRadians = (radians) => (\n\tcounterClockwiseOrderRadians(radians)\n\t\t.map(i => radians[i])\n\t\t.map((rad, i, arr) => [rad, arr[(i + 1) % arr.length]])\n\t\t.map(pair => counterClockwiseAngleRadians(pair[0], pair[1]))\n);\n\n/**\n * @description given an array of vectors, return the sector angles between\n * consecutive parameters. if radially unsorted, this will sort them.\n * @param {[number, number][]} vectors array of 2D vectors (higher dimensions will be ignored)\n * @returns {number[]} array of sector angles in radians\n */\nexport const counterClockwiseSectors2 = (vectors) => (\n\tcounterClockwiseSectorsRadians(vectors.map(vectorToAngle))\n);\n\n/**\n * subsect the angle between two lines, can handle parallel lines\n */\n// export const subsect = function (divisions, pointA, vectorA, pointB, vectorB) {\n//   const denominator = vectorA[0] * vectorB[1] - vectorB[0] * vectorA[1];\n//   if (Math.abs(denominator) < EPSILON) { /* parallel */\n//     const solution = [midpoint(pointA, pointB), [vectorA[0], vectorA[1]]];\n//     const array = [solution, solution];\n//     const dot = vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1];\n//     delete array[(dot > 0 ? 1 : 0)];\n//     return array;\n//   }\n//   const numerator = (pointB[0] - pointA[0]) * vectorB[1] - vectorB[0] * (pointB[1] - pointA[1]);\n//   const t = numerator / denominator;\n//   const x = pointA[0] + vectorA[0] * t;\n//   const y = pointA[1] + vectorA[1] * t;\n//   const bisects = bisect_vectors(vectorA, vectorB);\n//   bisects[1] = [-bisects[0][1], bisects[0][0]];\n//   return bisects.map(el => [[x, y], el]);\n// };\n\n/**\n * @description which turn direction do 3 points make?\n * clockwise or counter-clockwise?\n * @param {[number, number]} p0 the start point\n * @param {[number, number]} p1 the middle point\n * @param {[number, number]} p2 the end point\n * @param {number} [epsilon=1e-6] optional epsilon\n * @returns {number|undefined} with 4 possible results:\n * - \"0\": collinear, no turn, forward\n * - \"1\": counter-clockwise turn, 0+epsilon < x < 180-epsilon\n * - \"-1\": clockwise turn, 0-epsilon > x > -180+epsilon\n * - \"undefined\": collinear but with a 180 degree turn.\n */\nexport const threePointTurnDirection = (p0, p1, p2, epsilon = EPSILON) => {\n\tconst v = normalize2(subtract2(p1, p0));\n\tconst u = normalize2(subtract2(p2, p0));\n\t// not collinear\n\tconst cross = cross2(v, u);\n\tif (!epsilonEqual(cross, 0, epsilon)) {\n\t\treturn Math.sign(cross);\n\t}\n\t// collinear. now we have to ensure the order is 0, 1, 2, and point\n\t// 1 lies between 0 and 2. otherwise we made a 180 degree turn (return undefined)\n\treturn epsilonEqual(distance2(p0, p1) + distance2(p1, p2), distance2(p0, p2))\n\t\t? 0\n\t\t: undefined;\n};\n"
  },
  {
    "path": "src/math/range.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"./constant.js\";\n\n/**\n * @description Get a new range that fully encloses two ranges.\n * Note that if the two ranges are separated and do not overlap,\n * this \"union\" will include the area between them.\n * @param {number[]} a a range\n * @param {number[]} b another range\n * @returns a range that is built from the minimum and maximum-most value\n * from both ranges.\n */\nexport const rangeUnion = (a, b) => {\n\tconst bSorted = b[0] <= b[1];\n\treturn a[0] <= a[1]\n\t\t? [\n\t\t\tMath.min(a[0], bSorted ? b[0] : b[1]),\n\t\t\tMath.max(a[1], bSorted ? b[1] : b[0]),\n\t\t]\n\t\t: [\n\t\t\tMath.min(a[1], bSorted ? b[0] : b[1]),\n\t\t\tMath.max(a[0], bSorted ? b[1] : b[0]),\n\t\t];\n};\n\n/**\n * @description a range is an array of two numbers [start, end]\n * not necessarily in sorted order.\n * Do the two spans overlap on the numberline?\n * @param {number[]} a range with two numbers\n * @param {number[]} b range with two numbers\n * @param {number} [epsilon=1e-6] a positive value makes the range\n * endpoints exclusive, a negative value makes range endpoints inclusive.\n */\nexport const doRangesOverlap = (a, b, epsilon = EPSILON) => {\n\t// make sure ranges are well formed (sorted low to high)\n\tconst r1 = a[0] < a[1] ? a : [a[1], a[0]];\n\tconst r2 = b[0] < b[1] ? b : [b[1], b[0]];\n\tconst overlap = Math.min(r1[1], r2[1]) - Math.max(r1[0], r2[0]);\n\treturn overlap > epsilon;\n};\n"
  },
  {
    "path": "src/math/straightSkeleton.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\texcludeR,\n} from \"./compare.js\";\nimport {\n\tsubtract2,\n\tdistance,\n\tflip2,\n} from \"./vector.js\";\nimport {\n\tclockwiseBisect2,\n} from \"./radial.js\";\nimport {\n\tnearestPointOnLine,\n} from \"./nearest.js\";\nimport {\n\tintersectLineLine,\n} from \"./intersect.js\";\n\n/**\n * @description this recursive algorithm works outwards-to-inwards, each repeat\n * decreases the size of the polygon by one point/side. (removes 2, adds 1)\n * and repeating the algorithm on the smaller polygon.\n * @param {[number, number][]} points array of point objects (arrays of numbers, [x, y]).\n * the counter-clockwise sorted points of the polygon. as we recurse this\n * list shrinks by removing the points that are \"finished\".\n * @param {VecLine2[]} lines\n * @param {[number, number][]} bisectors 2D vectors\n * @returns {object[]} array of line segments as objects with keys:\n * \"points\": array of 2 points in array form [ [x, y], [x, y] ]\n * \"type\": \"skeleton\" or \"kawasaki\", the latter being the projected\n * perpendicular dropped edges down to the sides of the polygon.\n */\nconst recurseSkeleton = (points, lines, bisectors) => {\n\t// every point has an interior angle bisector vector, this ray is\n\t// tested for intersections with its neighbors on both sides.\n\t// \"intersects\" is fencepost mapped (i) to \"points\" (i, i+1)\n\t// because one point/ray intersects with both points on either side,\n\t// so in reverse, every point (i) relates to intersection (i-1, i)\n\tconst intersects = points\n\t\t// .map((p, i) => math.ray(bisectors[i], p))\n\t\t// .map((ray, i, arr) => ray.intersect(arr[(i + 1) % arr.length]));\n\t\t.map((origin, i) => ({ vector: bisectors[i], origin }))\n\t\t.map((ray, i, arr) => intersectLineLine(\n\t\t\tray,\n\t\t\tarr[(i + 1) % arr.length],\n\t\t\texcludeR,\n\t\t\texcludeR,\n\t\t).point);\n\t// project each intersection point down perpendicular to the edge of the polygon\n\t// const projections = lines.map((line, i) => line.nearestPoint(intersects[i]));\n\t/** @type {[number, number][]} */\n\tconst projections = lines.map((line, i) => (\n\t\tnearestPointOnLine(line, intersects[i])\n\t)).map(([a, b]) => [a, b]);\n\t// when we reach only 3 points remaining, we are at the end. we can return early\n\t// and skip unnecessary calculations, all 3 projection lengths will be the same.\n\tif (points.length === 3) {\n\t\treturn points.map(p => ({ type: \"skeleton\", points: [p, intersects[0]] }))\n\t\t\t.concat([{ type: \"perpendicular\", points: [projections[0], intersects[0]] }]);\n\t}\n\t// measure the lengths of the projected lines, these will be used to identify\n\t// the smallest length, or the point we want to operate on this round.\n\tconst projectionLengths = intersects\n\t\t.map((intersect, i) => distance(intersect, projections[i]));\n\tlet shortest = 0;\n\tprojectionLengths.forEach((len, i) => {\n\t\tif (len < projectionLengths[shortest]) { shortest = i; }\n\t});\n\t// we have the shortest length, we now have the solution for this round\n\t// (all that remains is to prepare the arguments for the next recursive call)\n\tconst solutions = [\n\t\t{\n\t\t\ttype: \"skeleton\",\n\t\t\tpoints: [points[shortest], intersects[shortest]],\n\t\t},\n\t\t{\n\t\t\ttype: \"skeleton\",\n\t\t\tpoints: [points[(shortest + 1) % points.length], intersects[shortest]],\n\t\t},\n\t\t// perpendicular projection\n\t\t// we could expand this algorithm here to include all three instead of just one.\n\t\t// two more of the entries in \"intersects\" will have the same length as shortest\n\t\t{ type: \"perpendicular\", points: [projections[shortest], intersects[shortest]] },\n\t\t// ...projections.map(p => ({ type: \"perpendicular\", points: [p, intersects[shortest]] }))\n\t];\n\t// our new smaller polygon, missing two points now, but gaining one more (the intersection)\n\t// this is to calculate the new angle bisector at this new point.\n\t// we are now operating on the inside of the polygon, the lines that will be built from\n\t// this bisection will become interior skeleton lines.\n\t// first, flip the first vector so that both of the vectors originate at the\n\t// center point, and extend towards the neighbors.\n\tconst newVector = clockwiseBisect2(\n\t\tflip2(lines[(shortest + lines.length - 1) % lines.length].vector),\n\t\tlines[(shortest + 1) % lines.length].vector,\n\t);\n\t// delete 2 entries from \"points\" and \"bisectors\" and add each array's new element.\n\t// delete 1 entry from lines.\n\tconst shortest_is_last_index = shortest === points.length - 1;\n\tpoints.splice(shortest, 2, intersects[shortest]);\n\tlines.splice(shortest, 1);\n\tbisectors.splice(shortest, 2, newVector);\n\tif (shortest_is_last_index) {\n\t\t// in the case the index was at the end of the array,\n\t\t// we tried to remove two elements but only removed one because\n\t\t// it was the last element. remove the first element too.\n\t\tpoints.splice(0, 1);\n\t\tbisectors.splice(0, 1);\n\t\t// also, the fencepost mapping of the lines array is off by one,\n\t\t// move the first element to the end of the array.\n\t\tlines.push(lines.shift());\n\t}\n\treturn solutions.concat(recurseSkeleton(points, lines, bisectors));\n};\n\n/**\n * @description create a straight skeleton inside of a convex polygon\n * @param {[number, number][]} points counter-clockwise polygon as an array of points\n * (which are arrays of numbers)\n * @returns {object[]} list of objects containing \"points\" {number[][]}: two points\n * defining a line segment, and \"type\" {string}: either \"skeleton\" or \"perpendicular\"\n *\n * make sure:\n *  - your polygon is convex (todo: make this algorithm work with non-convex)\n *  - your polygon points are sorted counter-clockwise\n */\nexport const straightSkeleton = (points) => {\n\t// first time running this function, create the 2nd and 3rd parameters\n\t// convert the edges of the polygons into lines\n\tconst lines = points\n\t\t.map((p, i, arr) => [p, arr[(i + 1) % arr.length]])\n\t\t// .map(side => math.line.fromPoints(...side));\n\t\t.map(side => ({ vector: subtract2(side[1], side[0]), origin: side[0] }));\n\t// get the interior angle bisectors for every corner of the polygon\n\t// index map match to \"points\"\n\tconst bisectors = points\n\t\t// each element into 3 (previous, current, next)\n\t\t.map((_, i, ar) => [(i - 1 + ar.length) % ar.length, i, (i + 1) % ar.length]\n\t\t\t.map(j => ar[j]))\n\t\t// make 2 vectors, from current point to previous/next neighbors\n\t\t.map(p => [subtract2(p[0], p[1]), subtract2(p[2], p[1])])\n\t\t// it is a little counter-intuitive but the interior angle between three\n\t\t// consecutive points in a counter-clockwise wound polygon is measured\n\t\t// in the clockwise direction\n\t\t.map(([a, b]) => clockwiseBisect2(a, b));\n\t// points is modified in place. create a copy\n\t// const points_clone = JSON.parse(JSON.stringify(points));\n\t// console.log(\"ss points\", points_clone, points);\n\treturn recurseSkeleton([...points], lines, bisectors);\n};\n"
  },
  {
    "path": "src/math/triangle.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"./constant.js\";\nimport {\n\tmagnitude2,\n\tmagnitude3,\n\tdot2,\n\tdot3,\n\tscale2,\n\tscale3,\n\tdistance2,\n\tdistance3,\n\tadd2,\n\tadd3,\n\tsubtract2,\n\tsubtract3,\n} from \"./vector.js\";\n\n/**\n * @description Given a point has known distances to three triangle points,\n * and given the location of that triangle's points in space, find the\n * location of the point in space. This method works for 2D faces and points.\n * https://stackoverflow.com/questions/9747227/2d-trilateration\n * @param {[[number, number], [number, number], [number, number]]} pts\n * three 2D triangle points\n * @param {[number, number, number]} radii three distances to each of the triangle points\n * @returns {[number, number] | undefined} the 2D location of the point\n * inside the triangle, undefined if bad inputs.\n */\nexport const trilateration2 = (pts, radii) => {\n\tif (pts[0] === undefined || pts[1] === undefined || pts[2] === undefined) {\n\t\treturn undefined;\n\t}\n\tconst ex = scale2(subtract2(pts[1], pts[0]), 1 / distance2(pts[1], pts[0]));\n\tconst i = dot2(ex, subtract2(pts[2], pts[0]));\n\tconst exi = scale2(ex, i);\n\tconst p2p0exi = subtract2(subtract2(pts[2], pts[0]), exi);\n\tconst ey = scale2(p2p0exi, (1 / magnitude2(p2p0exi)));\n\tconst d = distance2(pts[1], pts[0]);\n\tconst j = dot2(ey, subtract2(pts[2], pts[0]));\n\tconst x = ((radii[0] ** 2) - (radii[1] ** 2) + (d ** 2)) / (2 * d);\n\tconst y = ((radii[0] ** 2) - (radii[2] ** 2) + (i ** 2) + (j ** 2)) / (2 * j) - ((i * x) / j);\n\treturn add2(add2(pts[0], scale2(ex, x)), scale2(ey, y));\n};\n\n/**\n * @description Given a point has known distances to three triangle points,\n * and given the location of that triangle's points in space, find the\n * location of the point in space. This method works for 3D faces and points.\n * https://stackoverflow.com/questions/9747227/2d-trilateration\n * @param {[[number, number, number], [number, number, number], [number, number, number]]} pts\n * three 3D triangle points\n * @param {[number, number, number]} radii three distances to each of the triangle points\n * @returns {[number, number, number] | undefined} the 3D location of the point\n * inside the triangle, undefined if bad inputs.\n */\nexport const trilateration3 = (pts, radii) => {\n\tif (pts[0] === undefined || pts[1] === undefined || pts[2] === undefined) {\n\t\treturn undefined;\n\t}\n\tconst ex = scale3(subtract3(pts[1], pts[0]), 1 / distance3(pts[1], pts[0]));\n\tconst i = dot3(ex, subtract3(pts[2], pts[0]));\n\tconst exi = scale3(ex, i);\n\tconst p2p0exi = subtract3(subtract3(pts[2], pts[0]), exi);\n\tconst ey = scale3(p2p0exi, (1 / magnitude3(p2p0exi)));\n\tconst d = distance3(pts[1], pts[0]);\n\tconst j = dot3(ey, subtract3(pts[2], pts[0]));\n\tconst x = ((radii[0] ** 2) - (radii[1] ** 2) + (d ** 2)) / (2 * d);\n\tconst y = ((radii[0] ** 2) - (radii[2] ** 2) + (i ** 2) + (j ** 2)) / (2 * j) - ((i * x) / j);\n\treturn add3(add3(pts[0], scale3(ex, x)), scale3(ey, y));\n};\n\n/**\n * @description Calculates the circumcircle with a boundary that\n * lies on three points provided by the user.\n * @param {[number, number]} a one 2D point as an array of numbers\n * @param {[number, number]} b one 2D point as an array of numbers\n * @param {[number, number]} c one 2D point as an array of numbers\n * @returns {Circle} a circle in \"radius\" (number) \"origin\" (number[]) form\n */\nexport const circumcircle = (a, b, c) => {\n\tconst A = b[0] - a[0];\n\tconst B = b[1] - a[1];\n\tconst C = c[0] - a[0];\n\tconst D = c[1] - a[1];\n\tconst E = A * (a[0] + b[0]) + B * (a[1] + b[1]);\n\tconst F = C * (a[0] + c[0]) + D * (a[1] + c[1]);\n\tconst G = 2 * (A * (c[1] - b[1]) - B * (c[0] - b[0]));\n\tif (Math.abs(G) < EPSILON) {\n\t\tconst minx = Math.min(a[0], b[0], c[0]);\n\t\tconst miny = Math.min(a[1], b[1], c[1]);\n\t\tconst dx = (Math.max(a[0], b[0], c[0]) - minx) * 0.5;\n\t\tconst dy = (Math.max(a[1], b[1], c[1]) - miny) * 0.5;\n\t\treturn {\n\t\t\torigin: [minx + dx, miny + dy],\n\t\t\tradius: Math.sqrt(dx * dx + dy * dy),\n\t\t};\n\t}\n\t/** @type {[number, number]} */\n\tconst origin = [(D * E - B * F) / G, (A * F - C * E) / G];\n\tconst dx = origin[0] - a[0];\n\tconst dy = origin[1] - a[1];\n\treturn {\n\t\torigin,\n\t\tradius: Math.sqrt(dx * dx + dy * dy),\n\t};\n};\n"
  },
  {
    "path": "src/math/vector.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { EPSILON } from \"./constant.js\";\n\n/**\n * This entire library operates on a vector/point object type that is\n * nothing more than a Javascript array; no fancy prototype, no nothing.\n * Vectors as Javascript Objects, like { x:_ y:_ }, will not work.\n * Vector-related method naming follows a convention, if it ends with a\n * number, that relates to the dimension, (magnitude2() is for 2D vectors),\n * and if it is missing a number, it will work with any dimension,\n * (magnitude() for N-dimensional vectors).\n */\n\n/**\n * @description many methods here are operations on two arrays where\n * the first array determines the number of loops. it's possible the\n * array sizes mismatch, in which case, fill in any empty data with 0.\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nconst safeAdd = (a, b) => a + (b || 0);\n\n/**\n * @description compute the magnitude an n-dimensional vector.\n * @param {number[]} v one vector, n-dimensions\n * @returns {number} one scalar\n */\nexport const magnitude = v => Math.sqrt(v\n\t.map(n => n * n)\n\t.reduce(safeAdd, 0));\n\n/**\n * @description compute the magnitude a 2D vector.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {number} one scalar\n */\nexport const magnitude2 = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]);\n\n/**\n * @description compute the magnitude a 3D vector.\n * @param {[number, number, number]} v one 3D vector\n * @returns {number} one scalar\n */\nexport const magnitude3 = v => Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);\n\n/**\n * @description compute the square-magnitude a 2D vector.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {number} one scalar\n */\nexport const magSquared2 = v => v[0] * v[0] + v[1] * v[1];\n\n/**\n * @description compute the square-magnitude an n-dimensional vector.\n * @param {number[]} v one vector, n-dimensions\n * @returns {number} one scalar\n */\nexport const magSquared = v => v\n\t.map(n => n * n)\n\t.reduce(safeAdd, 0);\n\n/**\n * @description normalize the input vector and return a new vector as a copy.\n * @param {number[]} v one vector, n-dimensions\n * @returns {number[]} one vector, dimension matching the input vector\n */\nexport const normalize = (v) => {\n\tconst m = magnitude(v);\n\treturn m === 0 ? v : v.map(c => c / m);\n};\n\n/**\n * @description normalize the input vector and return a new vector as a copy.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const normalize2 = (v) => {\n\tconst m = magnitude2(v);\n\treturn m === 0 ? [v[0], v[1]] : [v[0] / m, v[1] / m];\n};\n\n/**\n * @description normalize the input vector and return a new vector as a copy.\n * @param {[number, number, number]} v one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const normalize3 = (v) => {\n\tconst m = magnitude3(v);\n\treturn m === 0 ? v : [v[0] / m, v[1] / m, v[2] / m];\n};\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {number[]} v one vector, n-dimensions\n * @param {number} s one scalar\n * @returns {number[]} one vector\n */\nexport const scale = (v, s) => v.map(n => n * s);\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {number} s one scalar\n * @returns {[number, number]} one 2D vector\n */\nexport const scale2 = (v, s) => [v[0] * s, v[1] * s];\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {[number, number, number]} v one 3D vector\n * @param {number} s one scalar\n * @returns {[number, number, number]} one 3D vector\n */\nexport const scale3 = (v, s) => [v[0] * s, v[1] * s, v[2] * s];\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} s one scale vector, n-dimensions\n * @returns {number[]} one vector\n */\nexport const scaleNonUniform = (v, s) => v.map((n, i) => n * s[i]);\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} s one 2D scale vector\n * @returns {[number, number]} one 2D vector\n */\nexport const scaleNonUniform2 = (v, s) => [v[0] * s[0], v[1] * s[1]];\n\n/**\n * @description scale an input vector by one number, return a copy.\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} s one 3D scale vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const scaleNonUniform3 = (v, s) => [v[0] * s[0], v[1] * s[1], v[2] * s[2]];\n\n/**\n * @description add two vectors and return the sum as another vector,\n * do not modify the input vectors.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @returns {number[]} one vector, dimension matching first parameter\n */\nexport const add = (v, u) => v.map((n, i) => n + (u[i] || 0));\n\n/**\n * @description add two vectors and return the sum as another vector,\n * do not modify the input vectors.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const add2 = (v, u) => [v[0] + u[0], v[1] + u[1]];\n\n/**\n * @description add two vectors and return the sum as another vector,\n * do not modify the input vectors.\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const add3 = (v, u) => [v[0] + u[0], v[1] + u[1], v[2] + u[2]];\n\n/**\n * @description subtract the second vector from the first,\n * return the result as a copy.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @returns {number[]} one vector, dimension matching first parameter\n */\nexport const subtract = (v, u) => v.map((n, i) => n - (u[i] || 0));\n\n/**\n * @description subtract the second vector from the first,\n * return the result as a copy.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const subtract2 = (v, u) => [v[0] - u[0], v[1] - u[1]];\n\n/**\n * @description subtract the second vector from the first,\n * return the result as a copy.\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const subtract3 = (v, u) => [v[0] - u[0], v[1] - u[1], v[2] - u[2]];\n\n/**\n * @description compute the dot product of two vectors.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @returns {number} one scalar\n */\nexport const dot = (v, u) => v\n\t.map((_, i) => v[i] * u[i])\n\t.reduce(safeAdd, 0);\n\n/**\n * @description compute the dot product of two 2D vectors.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {number} one scalar\n */\nexport const dot2 = (v, u) => v[0] * u[0] + v[1] * u[1];\n\n/**\n * @description compute the dot product of two 3D vectors.\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {number} one scalar\n */\nexport const dot3 = (v, u) => v[0] * u[0] + v[1] * u[1] + v[2] * u[2];\n\n/**\n * @description compute the midpoint of two vectors.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @returns {number[]} one vector, dimension matching first parameter\n */\nexport const midpoint = (v, u) => v.map((n, i) => (n + u[i]) / 2);\n\n/**\n * @description compute the midpoint of two 2D vectors.\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const midpoint2 = (v, u) => scale2(add2(v, u), 0.5);\n\n/**\n * @description compute the midpoint of two 3D vectors.\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const midpoint3 = (v, u) => scale3(add3(v, u), 0.5);\n\n/**\n * @description the average of any number of vectors (not numbers),\n * similar to midpoint but this can accept more than two inputs.\n * @param {...number[]} args any number of input vectors\n * @returns {number[]} one vector, dimension matching first parameter\n */\nexport const average = (...args) => {\n\tif (args.length === 0) { return undefined; }\n\tconst dimension = (args[0].length > 0) ? args[0].length : 0;\n\tconst sum = Array(dimension).fill(0);\n\tArray.from(args)\n\t\t.forEach(vec => sum\n\t\t\t.forEach((_, i) => { sum[i] += vec[i] || 0; }));\n\treturn sum.map(n => n / args.length);\n};\n\n/**\n * @description the average of any number of 2D vectors (not numbers),\n * similar to midpoint but this can accept more than two inputs.\n * @param {...[number, number]} vectors any number of input vectors\n * @returns {[number, number]} one 2D vector\n */\nexport const average2 = (...vectors) => {\n\tif (!vectors || !vectors.length) { return undefined; }\n\tconst sum = vectors.reduce((a, b) => add2(a, b), [0, 0]);\n\treturn [sum[0] / vectors.length, sum[1] / vectors.length];\n};\n\n/**\n * @description the average of any number of 3D vectors (not numbers),\n * similar to midpoint but this can accept more than two inputs.\n * @param {...[number, number, number]} vectors any number of input vectors\n * @returns {[number, number, number]} one 3D vector\n */\nexport const average3 = (...vectors) => {\n\tif (!vectors || !vectors.length) { return undefined; }\n\tconst sum = vectors.reduce((a, b) => add3(a, b), [0, 0, 0]);\n\treturn [\n\t\tsum[0] / vectors.length,\n\t\tsum[1] / vectors.length,\n\t\tsum[2] / vectors.length,\n\t];\n};\n\n/**\n * @description linear interpolate between two vectors\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @param {number} t one scalar between 0 and 1 (not clamped)\n * @returns {number[]} one vector, dimensions matching first parameter\n */\nexport const lerp = (v, u, t = 0) => {\n\tconst inv = 1.0 - t;\n\treturn v.map((n, i) => n * inv + (u[i] || 0) * t);\n};\n\n/**\n * @description the determinant of the matrix of the 2 vectors\n * (possible bad name, 2D cross product is undefined)\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {number} one scalar; the determinant; the magnitude\n * of the 2D cross product vector.\n */\nexport const cross2 = (v, u) => v[0] * u[1] - v[1] * u[0];\n\n/**\n * @description the 3D cross product of two 3D vectors\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const cross3 = (v, u) => [\n\tv[1] * u[2] - v[2] * u[1],\n\tv[2] * u[0] - v[0] * u[2],\n\tv[0] * u[1] - v[1] * u[0],\n];\n\n/**\n * @description compute the distance between two vectors\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @returns {number} one scalar\n */\nexport const distance = (v, u) => Math.sqrt(v\n\t.map((_, i) => (v[i] - u[i]) ** 2)\n\t.reduce(safeAdd, 0));\n\n/**\n * @description compute the distance between two 2D vectors\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @returns {number} one scalar\n */\nexport const distance2 = (v, u) => {\n\tconst p = v[0] - u[0];\n\tconst q = v[1] - u[1];\n\treturn Math.sqrt((p * p) + (q * q));\n};\n\n/**\n * @description compute the distance between two 3D vectors\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @returns {number} one scalar\n */\nexport const distance3 = (v, u) => {\n\tconst a = v[0] - u[0];\n\tconst b = v[1] - u[1];\n\tconst c = v[2] - u[2];\n\treturn Math.sqrt((a * a) + (b * b) + (c * c));\n};\n\n/**\n * @description return a copy of the input vector where\n * each element's sign flipped\n * @param {number[]} v one vector, n-dimensions\n * @returns {number[]} one vector, dimensions matching input parameter\n */\nexport const flip = v => v.map(n => -n);\n\n/**\n * @description return a copy of the input vector where\n * each element's sign flipped\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const flip2 = v => [-v[0], -v[1]];\n\n/**\n * @description return a copy of the input vector where\n * each element's sign flipped\n * @param {[number, number, number]} v one 3D vector\n * @returns {[number, number, number]} one 3D vector\n */\nexport const flip3 = v => [-v[0], -v[1], -v[2]];\n\n/**\n * @description return a copy of the input vector rotated\n * 90 degrees counter-clockwise\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const rotate90 = v => [-v[1], v[0]];\n\n/**\n * @description return a copy of the input vector rotated\n * 270 degrees counter-clockwise\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @returns {[number, number]} one 2D vector\n */\nexport const rotate270 = v => [v[1], -v[0]];\n\n/**\n * @description check if a vector is degenerate, meaning its\n * magnitude is below an epsilon limit.\n * @param {number[]} v one vector, n-dimensions\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {boolean} is the magnitude of the vector smaller than the epsilon?\n */\nexport const degenerate = (v, epsilon = EPSILON) => v\n\t.map(n => Math.abs(n))\n\t.reduce(safeAdd, 0) < epsilon;\n\n/**\n * @description check if two already normalized vectors are parallel\n * to each other, within an epsilon. Parallel includes the case where\n * the vectors are exactly 180 degrees flipped from one another.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {boolean} are the two vectors parallel within an epsilon?\n */\nexport const parallelNormalized = (v, u, epsilon = EPSILON) => 1 - Math\n\t.abs(dot(v, u)) < epsilon;\n\n/**\n * @description check if two vectors are parallel to each other,\n * within an epsilon. Parallel includes the case where the\n * vectors are exactly 180 degrees flipped from one another.\n * @param {number[]} v one vector, n-dimensions\n * @param {number[]} u one vector, n-dimensions\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {boolean} are the two vectors parallel within an epsilon?\n */\nexport const parallel = (v, u, epsilon = EPSILON) => parallelNormalized(\n\tnormalize(v),\n\tnormalize(u),\n\tepsilon,\n);\n\n/**\n * @description check if two 2D vectors are parallel to each other within an epsilon\n * @param {[number, number] | [number, number, number]} v one 2D vector\n * @param {[number, number] | [number, number, number]} u one 2D vector\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {boolean} are the two vectors parallel within an epsilon?\n */\nexport const parallel2 = (v, u, epsilon = EPSILON) => Math\n\t.abs(cross2(v, u)) < epsilon;\n\n/**\n * @description check if two 3D vectors are parallel to each other within an epsilon\n * @param {[number, number, number]} v one 3D vector\n * @param {[number, number, number]} u one 3D vector\n * @param {number} [epsilon=1e-6] an optional epsilon with a default value of 1e-6\n * @returns {boolean} are the two vectors parallel within an epsilon?\n */\nexport const parallel3 = (v, u, epsilon = EPSILON) => (\n\t(magnitude3(cross3(v, u))) < epsilon\n);\n\n/**\n * @description Resize a vector to a particular length (duplicating it\n * in memory in the process) by either lengthening or shortening it.\n * In the case of lengthening, fill 0.\n * @param {number} dimension the desired length\n * @param {number[]} vector the vector to resize\n * @returns {number[]} a copy of the vector resized to the desired length.\n */\nexport const resize = (dimension, vector) => (vector.length === dimension\n\t? vector\n\t: Array(dimension).fill(0).map((z, i) => (vector[i] ? vector[i] : z)));\n\n/**\n * @description Resize a vector to 2D, filling any missing values with 0.\n * @param {number[]} vector the vector to resize\n * @returns {[number, number]} a copy of the vector in 2D.\n */\nexport const resize2 = (vector) => [vector[0] || 0, vector[1] || 0];\n\n/**\n * @description Resize a vector to 3D, filling any missing values with 0.\n * @param {number[]} vector the vector to resize\n * @returns {[number, number, number]} a copy of the vector in 3D.\n */\nexport const resize3 = (vector) => [vector[0] || 0, vector[1] || 0, vector[2] || 0];\n\n/**\n * @description Make the two vectors match in dimension by appending the\n * smaller vector with 0s to match the dimension of the larger vector.\n * @param {number[]} a a vector\n * @param {number[]} b a vector\n * @returns {number[][]} an array containing two vectors, a copy of\n * each of the input parameters.\n */\nexport const resizeUp = (a, b) => [a, b]\n\t.map(v => resize(Math.max(a.length, b.length), v));\n\n/**\n * @description Make the two vectors match in dimension by clamping the\n * larger vector to match the dimension of the smaller vector.\n * @param {number[]} a a vector\n * @param {number[]} b a vector\n * @returns {number[][]} an array containing two vectors, a copy of\n * each of the input parameters.\n */\n// export const resizeDown = (a, b) => [a, b]\n//   .map(v => resize(Math.min(a.length, b.length), v));\n\n/**\n * @description Using a given 2D vector as the first basis vector for\n * two-dimensions, return two normalized basis vectors, the second\n * vector being the 90 degree rotation of the first.\n * @param {[number, number] | [number, number, number]} vector a 2D vector\n * @returns {[number, number][]} array of two 2D basis vectors\n */\nexport const basisVectors2 = (vector = [1, 0]) => {\n\tconst normalized = normalize2(vector);\n\treturn [normalized, rotate90(normalized)];\n};\n\n/**\n * @description Using a 3D vector as the first basis vector\n * find additional perpendicular vectors which, taken together,\n * span 3D space most effectively (vectors are perpendicular).\n * @param {[number, number, number]} vector a 3D vector\n * @returns {[number, number, number][]} array of three 3D basis vectors\n */\nexport const basisVectors3 = (vector = [1, 0, 0]) => {\n\tconst normalized = normalize3(vector);\n\n\t// find a good candidate for a second basis vector\n\t/** @type {[number, number, number][]} */\n\tconst candidates = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];\n\tconst crosses = candidates.map(v => cross3(v, normalized));\n\n\t// before normalizing the cross products, find the result with\n\t// the largest magnitude, use this for the second basis vector\n\tconst index = crosses\n\t\t.map(magnitude3)\n\t\t.map((n, i) => ({ n, i }))\n\t\t.sort((a, b) => b.n - a.n)\n\t\t.map(el => el.i)\n\t\t.shift();\n\n\t// normalize the second basis vector\n\tconst perpendicular = normalize3(crosses[index]);\n\n\t// the third basis vector is the cross product of the previous two\n\treturn [normalized, perpendicular, cross3(normalized, perpendicular)];\n};\n\n/**\n * @description Given a vector (2D or 3D), using this vector as the\n * first basis vector, find additional perpendicular vectors which\n * all three span the space (2D or 3D) most effectively.\n * (the vectors are all perpendicular).\n * @param {number[]} vector a 2D or 3D vector\n * @returns {number[][]} an array of basis vectors\n */\nexport const basisVectors = (vector) => (vector.length === 2\n\t? basisVectors2([vector[0], vector[1]])\n\t: basisVectors3([vector[0], vector[1], vector[2]])\n);\n"
  },
  {
    "path": "src/prototypes/graph.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n// import { count } from \"../graph/count.js\";\nimport { clone } from \"../general/clone.js\";\nimport { foldToSvg } from \"../convert/foldToSvg.js\";\nimport { foldToObj } from \"../convert/foldToObj.js\";\nimport { clean } from \"../graph/clean.js\";\nimport { planarize } from \"../graph/planarize.js\";\nimport { populate } from \"../graph/populate.js\";\n// import { flatFold } from \"../graph/fold/flatFold.js\";\nimport * as splitEdge from \"../graph/split/splitEdge.js\";\nimport * as transform from \"../graph/transform.js\";\nimport * as explode from \"../graph/explode.js\";\nimport * as nearest from \"../graph/nearest.js\";\nimport * as validate from \"../graph/validate/validate.js\";\n// import {\n// \tgetLine,\n// } from \"../general/get.js\";\nimport {\n\tfoldKeys,\n\tinvertAssignments,\n} from \"../fold/spec.js\";\nimport {\n\tsubgraph,\n} from \"../graph/subgraph.js\";\nimport {\n\tboundary,\n\tboundaries,\n\tplanarBoundary,\n\tplanarBoundaries,\n\tboundingBox,\n} from \"../graph/boundary.js\";\nimport {\n\tmakeVerticesCoordsFolded,\n\tmakeVerticesCoordsFlatFolded,\n\tmakeVerticesCoordsFoldedFromMatrix2,\n} from \"../graph/vertices/folded.js\";\n\n/**\n * @description a graph which includes faces, edges, and vertices, and\n * additional origami-specific information like fold angles of edges\n * and layer order of faces.\n * @param {FOLD} [graph] an optional FOLD object,\n * otherwise the graph will initialize empty\n */\nconst Graph = {};\nGraph.prototype = Object.create(Object.prototype);\nGraph.prototype.constructor = Graph;\n\n/**\n * methods where \"graph\" is the first parameter, followed by ...arguments\n * func(graph, ...args)\n */\nObject.entries({\n\t// count,\n\tclean,\n\tpopulate,\n\tsubgraph,\n\tboundary,\n\tboundaries,\n\tplanarBoundary,\n\tplanarBoundaries,\n\tboundingBox,\n\tinvertAssignments,\n\tsvg: foldToSvg,\n\tobj: foldToObj,\n\t...splitEdge,\n\t...explode,\n\t...nearest,\n\t...transform,\n\t...validate,\n}).forEach(([key, value]) => {\n\tGraph.prototype[key] = function () {\n\t\treturn value.apply(null, [this, ...arguments]);\n\t};\n});\n\n/**\n * @returns {FOLD} a deep copy of this object\n */\nGraph.prototype.clone = function () {\n\treturn Object.assign(Object.create(Object.getPrototypeOf(this)), clone(this));\n};\n\n/**\n * @description convert a graph into a planar graph.\n * the core \"planarize\" method normally does not modify the input object,\n * in this case, the object itself is modified in place.\n */\nGraph.prototype.planarize = function () {\n\tconst result = planarize(this);\n\tthis.clear();\n\tObject.assign(this, result);\n\treturn this;\n};\n\n/**\n * @description this clears all components from the graph,\n * leaving metadata and other keys untouched.\n */\nGraph.prototype.clear = function () {\n\tfoldKeys.graph.forEach(key => delete this[key]);\n\tfoldKeys.orders.forEach(key => delete this[key]);\n\t// the code above just avoided deleting all \"file_\" keys,\n\t// however, file_frames needs to be removed as it contains geometry.\n\tdelete this.file_frames;\n\treturn this;\n};\n\n/**\n * @description return a shallow copy of this graph with the vertices folded.\n * This method works for both 2D and 3D origami.\n * The angle of the fold is searched for in this order:\n * - faces_matrix2 if it exists\n * - edges_foldAngle if it exists\n * - edges_assignment if it exists\n * Repeated calls to this method will repeatedly fold the vertices\n * resulting in a behavior that is surely unintended.\n */\nGraph.prototype.folded = function () {\n\tconst vertices_coords = this.faces_matrix2\n\t\t? makeVerticesCoordsFoldedFromMatrix2(this, this.faces_matrix2)\n\t\t: makeVerticesCoordsFolded(this, ...arguments);\n\tObject.assign(this, {\n\t\tvertices_coords,\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\treturn this;\n};\n\n/**\n * @description return a copy of this graph with the vertices folded.\n * This method will work for 2D only.\n * The angle of the fold is searched for in this order:\n * - faces_matrix2 if it exists\n * - edges_assignment or edges_foldAngle if it exists\n * If neither exists, this method will assume that ALL edges are flat-folded.\n */\nGraph.prototype.flatFolded = function () {\n\tconst vertices_coords = this.faces_matrix2\n\t\t? makeVerticesCoordsFoldedFromMatrix2(this, this.faces_matrix2)\n\t\t: makeVerticesCoordsFlatFolded(this, ...arguments);\n\tObject.assign(this, {\n\t\tvertices_coords,\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\treturn this;\n};\n\n// Graph.prototype.flatFold = function () {\n// \tflatFold(this, getLine(arguments));\n// \treturn this;\n// };\n\nexport default Graph.prototype;\n"
  },
  {
    "path": "src/prototypes/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tfile_spec,\n\tfile_creator,\n} from \"../fold/rabbitear.js\";\nimport * as bases from \"../fold/bases.js\";\nimport * as primitives from \"../fold/primitives.js\";\nimport {\n\tpopulate,\n} from \"../graph/populate.js\";\nimport graphPrototype from \"./graph.js\";\n// import cpPrototype from \"./cp.js\";\n// import origamiPrototype from \"./origami.js\";\n\n/**\n * @description Create a FOLD object that inherits from the Graph prototype\n * with basic FOLD metadata, file spec and creator.\n * @param {...any} args\n * @returns {FOLD} a FOLD object\n */\nconst makeGraphInstance = (...args) => Object\n\t.assign(Object.create(graphPrototype), {\n\t\t...args.reduce((a, b) => ({ ...a, ...b }), ({})),\n\t\tfile_spec,\n\t\tfile_creator,\n\t});\n\n/**\n * @description Create a FOLD object that inherits from the CP prototype.\n * By default, the crease pattern will initialize to a flat square graph with\n * FOLD metadata including the file spec, creator, and \"creasePattern\" class.\n * @returns {FOLD} a FOLD object\n */\n// const makeCPInstance = (...args) => Object\n// \t.assign(Object.create(cpPrototype), {\n// \t\t...(args.length\n// \t\t\t? args.reduce((a, b) => ({ ...a, ...b }), ({}))\n// \t\t\t: bases.square()),\n// \t\tfile_spec,\n// \t\tfile_creator,\n// \t\tframe_classes: [\"creasePattern\"],\n// \t});\n\n/**\n * @description Create a FOLD object that inherits from the Origami prototype.\n * By default, the FOLD object will initialize to a flat square graph with\n * FOLD metadata including the file spec, creator, and \"foldedForm\" class.\n * @returns {FOLD} a FOLD object\n */\n// const makeOrigamiInstance = (...args) => Object\n// \t.assign(Object.create(origamiPrototype), {\n// \t\t...(args.length\n// \t\t\t? args.reduce((a, b) => ({ ...a, ...b }), ({}))\n// \t\t\t: bases.square()),\n// \t\tfile_spec,\n// \t\tfile_creator,\n// \t\tframe_classes: [\"foldedForm\"],\n// \t});\n\n/**\n * @description Create a FOLD object that inherits from the Graph\n * prototype which includes a bunch of helper methods bound to it.\n * By default the graph will be empty, an optional FOLD object can be\n * passed in and the graph will populate it and initilize itself to it.\n * @param {...any} args\n * @returns {FOLD} a FOLD object\n */\nconst Graph = function () {\n\treturn populate(makeGraphInstance(...arguments));\n};\n// const Graph = (...args) => populate(makeGraphInstance(...args));\nGraph.prototype = graphPrototype;\nGraph.prototype.constructor = Graph;\n\n/**\n * @description Create a populated FOLD object that inherits from the\n * CreasePattern object prototype. By default, the crease pattern\n * object will initialize to a flat square graph. Any user arguments\n * will be passed into the CreasePattern prototype constructor.\n * The most basic FOLD metadata will be added, including the\n * file spec and creator, and a frame class of \"creasePattern\".\n * @returns {FOLD} a FOLD object\n */\n// const cp = (...args) => populate(makeCPInstance(...args));\n// cp.prototype = cpPrototype;\n// cp.prototype.constructor = cp;\n\n// const origami = (...args) => populate(makeOrigamiInstance(...args));\n// origami.prototype = origamiPrototype;\n// origami.prototype.constructor = origami;\n\n// static constructors for all the primitive FOLD shapes\nObject.keys(primitives).forEach(baseName => {\n\t/** @param {...any} args */\n\tGraph[baseName] = (...args) => makeGraphInstance(primitives[baseName](...args));\n\t// cp[baseName] = (...args) => makeCPInstance(primitives[baseName](...args));\n\t// origami[baseName] = (...args) => origami(primitives[baseName](...args));\n});\n\n// static constructors for all the origami bases\nObject.keys(bases).forEach(baseName => {\n\t/** @param {...any} args */\n\tGraph[baseName] = (...args) => makeGraphInstance(bases[baseName](...args));\n\t// cp[baseName] = (...args) => makeCPInstance(bases[baseName](...args));\n\t// origami[baseName] = (...args) => origami(bases[baseName](...args));\n});\n\n/** @type {Function} */\nexport const graphConstructor = Graph;\n"
  },
  {
    "path": "src/singleVertex/degree4.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Given an array of assignments, mountain and valley,\n * identify which assignment occurs less than the other, and return\n * the index of the first occurence of that assignment.\n * In a valid flat-foldable degree-4 single vertex, 3 assignments will be\n * one kind, with 1 as the other. This identifies the location of the other.\n * @param {string[]} assignments an array of FOLD assignments, in uppercase.\n * @returns {number} the index of the odd-one-out assignment.\n */\nconst oddAssignmentIndex = (assignments) => (\n\tassignments.filter(a => a === \"M\").length\n\t> assignments.filter(a => a === \"V\").length\n\t\t? assignments.indexOf(\"V\")\n\t\t: assignments.indexOf(\"M\")\n);\n\n/**\n * @description Fold a degree-4 single vertex in 3D.\n * @usage this only works for degree-4 vertices\n * @param {number[]} sectors an array of sector angles in sorted order\n * around the central vertex.\n * @param {string[]} assignments an array of FOLD spec characters, \"M\" or \"V\".\n * @param {number} foldAngle the fold amount in radians, between 0 and PI.\n * This value will be internally clamped between -PI and +PI.\n * @todo is it possible to validate self-intersection (and fully validate\n * the folding), if you simply check the assignments around the smallest angle?\n * @returns {number[]|undefined} four fold angles as numbers in an array,\n * or \"undefined\" if the operation could not be completed.\n */\nexport const foldDegree4 = (sectors, assignments, foldAngle = 0) => {\n\tconst odd = oddAssignmentIndex(assignments.map(a => a.toUpperCase()));\n\tif (odd === -1) { return undefined; }\n\tconst a = sectors[(odd + 1) % sectors.length];\n\tconst b = sectors[(odd + 2) % sectors.length];\n\n\t// const pab = (odd + 2) % sectors.length;\n\t// const pbc = (odd + 3) % sectors.length;\n\t// const pbc = Math.PI * foldAngle; // when input was between 0 and 1.\n\tconst pbc = Math.max(-Math.PI, Math.min(Math.PI, foldAngle));\n\n\tconst cosE = -Math.cos(a) * Math.cos(b)\n\t\t+ Math.sin(a) * Math.sin(b) * Math.cos(Math.PI - pbc);\n\tconst res = Math.cos(Math.PI - pbc)\n\t\t- ((Math.sin(Math.PI - pbc) ** 2) * Math.sin(a) * Math.sin(b))\n\t\t/ (1 - cosE);\n\n\tconst pab = -Math.acos(res) + Math.PI;\n\treturn (odd % 2 === 0\n\t\t? [pab, pbc, pab, pbc].map((n, i) => (odd === i ? -n : n))\n\t\t: [pbc, pab, pbc, pab].map((n, i) => (odd === i ? -n : n)));\n};\n"
  },
  {
    "path": "src/singleVertex/flatFoldable.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n} from \"../math/constant.js\";\nimport {\n\tresize2,\n} from \"../math/vector.js\";\nimport {\n\tcounterClockwiseSectors2,\n} from \"../math/radial.js\";\nimport {\n\tassignmentCanBeFolded,\n\tassignmentFlatFoldAngle,\n} from \"../fold/spec.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"../graph/make/verticesVertices.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../graph/make/verticesEdges.js\";\nimport {\n\tmakeVerticesVerticesVector,\n} from \"../graph/make/vertices.js\";\nimport {\n\tboundaryVertices,\n} from \"../graph/boundary.js\";\nimport {\n\talternatingSum,\n} from \"./kawasaki.js\";\n\n/**\n * @description Get a list of vertex indices which are not able to be\n * used in Maekawa's and Kawasaki's theorems, where the edges around each\n * vertex have no mountains or valleys, so that all surrounding assignments\n * are either flat, cut, join, or boundary.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} a list of vertex indices which have no folds.\n */\nconst getVerticesWithNoFolds = ({ vertices_edges, edges_assignment }) => (\n\tvertices_edges\n\t\t.map(edges => edges\n\t\t\t.map(e => !assignmentCanBeFolded[edges_assignment[e]])\n\t\t\t.reduce((a, b) => a && b, true))\n\t\t.map((valid, i) => (valid ? i : undefined))\n\t\t.filter(a => a !== undefined)\n);\n\n/**\n * @description using edges_assignment, check if Maekawa's theorem is satisfied\n * for each vertex. This assumes that you are passing in a flat-foldable\n * crease pattern, it assumes that all mountain/valley are flat folded.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} for every vertex, the number of its edges which violate\n * Maekawa's theorem (0 means that the theorem is satisfied).\n */\nexport const verticesFlatFoldabilityMaekawa = ({\n\tedges_vertices, vertices_edges, edges_assignment,\n}) => {\n\tif (!vertices_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst verticesValidCount = vertices_edges\n\t\t.map(edges => edges\n\t\t\t.map(e => assignmentFlatFoldAngle[edges_assignment[e]])\n\t\t\t.filter(a => a !== 0 && a !== undefined)\n\t\t\t.map(Math.sign)\n\t\t\t.reduce((a, b) => a + b, 0))\n\t\t.map(sum => Math.abs(sum) - 2);\n\n\t// set all boundary and flat vertices to be valid\n\tboundaryVertices({ edges_vertices, edges_assignment })\n\t\t.forEach(v => { verticesValidCount[v] = 0; });\n\tgetVerticesWithNoFolds({ vertices_edges, edges_assignment })\n\t\t.forEach(v => { verticesValidCount[v] = 0; });\n\n\treturn verticesValidCount;\n};\n\n/**\n * @description For every vertex, check if Kawasaki's theorem is satisfied.\n * The result is in the form of a scalar value, how much the vertex deviates\n * from being satisfied, where a 0 (or near to 0) means the vertex is valid.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} for every vertex, a deviation value, where a value\n * of near-zero indicates the theorem is satisfied for this vertex, if not,\n * the number will be positive if even-numbered-sectors (sectors including [0])\n * are larger than odd-numbered ones, and negative if visa-versa.\n */\nexport const verticesFlatFoldabilityKawasaki = ({\n\tvertices_coords,\n\tvertices_vertices,\n\tvertices_edges,\n\tedges_vertices,\n\tedges_assignment,\n}) => {\n\tif (!vertices_vertices) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_vertices = makeVerticesVertices({\n\t\t\tvertices_coords, vertices_edges, edges_vertices,\n\t\t});\n\t}\n\tif (!vertices_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\tconst verticesValidAmount = makeVerticesVerticesVector({\n\t\tvertices_coords, vertices_vertices, edges_vertices,\n\t})\n\t\t.map((vectors, v) => vectors.filter((_, i) => (\n\t\t\tassignmentCanBeFolded[edges_assignment[vertices_edges[v][i]]]\n\t\t)))\n\t\t.map(vectors => vectors.map(resize2))\n\t\t.map(vectors => (vectors.length > 1\n\t\t\t? counterClockwiseSectors2(vectors)\n\t\t\t: [0, 0]))\n\t\t.map(sectors => alternatingSum(sectors))\n\t\t.map(([a, b]) => a - b);\n\n\t// set all boundary and flat vertices to be valid\n\tboundaryVertices({ edges_vertices, edges_assignment })\n\t\t.forEach(v => { verticesValidAmount[v] = 0; });\n\tgetVerticesWithNoFolds({ vertices_edges, edges_assignment })\n\t\t.forEach(v => { verticesValidAmount[v] = 0; });\n\n\treturn verticesValidAmount;\n};\n\n/**\n * @description using edges_assignment, check if Maekawa's theorem is satisfied\n * for each vertex. This assumes that you are passing in a flat-foldable\n * crease pattern, it assumes that all mountain/valley are flat folded.\n * @param {FOLD} graph a FOLD object\n * @returns {boolean[]} for every vertex, true if Maekawa's theorm is satisfied\n */\nexport const verticesFlatFoldableMaekawa = (graph) => (\n\tverticesFlatFoldabilityMaekawa(graph).map(deviation => deviation === 0)\n);\n\n/**\n * @description For every vertex in a graph, check if Kawasaki's theorem\n * is satisfied.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean[]} for every vertex, is Kawasaki's theorem satisfied?\n */\nexport const verticesFlatFoldableKawasaki = (graph, epsilon = EPSILON) => (\n\tverticesFlatFoldabilityKawasaki(graph)\n\t\t.map(Math.abs)\n\t\t.map(deviation => deviation < epsilon)\n);\n\n/**\n * @description Perform Kawasaki's and Maekawa's theorem on all vertices,\n * return for every vertex, is either (or both) theorem violated?\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {number[]} for every vertex, an indicator as to which theorems\n * (if any) have been violated where, 0: no violations, 1: Maekawa only,\n * 2: Kawasaki only, 3: Maekawa and Kawasaki are both violated.\n */\nexport const verticesFlatFoldability = (graph, epsilon) => {\n\t// convert results into 1 (false) or 0 (true)\n\tconst maekawa = verticesFlatFoldableMaekawa(graph)\n\t\t.map(m => (m ? 0 : 1));\n\tconst kawasaki = verticesFlatFoldableKawasaki(graph, epsilon)\n\t\t.map(k => (k ? 0 : 1));\n\t// \"logical or\" maekawa in the ones place, kawasaki in the twos place\n\t// results: 1: maekawa, 2: kawasaki, 3: maekawa and kawasaki\n\treturn maekawa.map((mae, v) => mae | (kawasaki[v] << 1));\n};\n\n/**\n * @description Perform Kawasaki's and Maekawa's theorem on all vertices and\n * return a boolean for every vertex, true if both theorems are valid.\n * @param {FOLD} graph a FOLD object\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean[]} for every vertex, true if both theorems are valid.\n */\nexport const verticesFlatFoldable = (graph, epsilon) => (\n\tverticesFlatFoldability(graph, epsilon).map(n => n === 0)\n);\n\n/**\n * @description using edges_assignment, check if Maekawa's theorem is satisfied\n * for all vertices, and if not, return the vertices which violate the theorem.\n * todo: this assumes that valley/mountain folds are flat folded.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} indices of vertices which violate the theorem.\n * an empty array has no errors.\n */\n// export const validateMaekawa = (graph) => (\n// \tverticesFlatFoldableMaekawa(graph)\n// \t\t.map((valid, v) => (!valid ? v : undefined))\n// \t\t.filter(a => a !== undefined)\n// );\n\n/**\n *\n */\n// export const validateKawasaki = (graph, epsilon) => (\n// \tverticesFlatFoldableKawasaki(graph, epsilon)\n// \t\t.map((valid, v) => (!valid ? v : undefined))\n// \t\t.filter(a => a !== undefined)\n// );\n"
  },
  {
    "path": "src/singleVertex/foldable.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tEPSILON,\n\tD2R,\n} from \"../math/constant.js\";\nimport {\n\tidentity3x4,\n\tinvertMatrix3,\n\tmakeMatrix3RotateX,\n\tmakeMatrix3RotateZ,\n\tmultiplyMatrices3,\n} from \"../math/matrix3.js\";\nimport {\n\tmakeVerticesVertices,\n} from \"../graph/make/verticesVertices.js\";\nimport {\n\tmakeVerticesEdges,\n} from \"../graph/make/verticesEdges.js\";\nimport {\n\tmakeVerticesFaces,\n} from \"../graph/make/verticesFaces.js\";\nimport {\n\tmakeVerticesVerticesVector,\n} from \"../graph/make/vertices.js\";\n\n/**\n * @description Given a crease pattern, this method will test every vertex\n * to determine if it is possible to be folded by walking around each vertex\n * face by face and checking if we meet back up exactly where we started.\n * This does not test for self-intersection, if the faces of a single vertex\n * pokes through one another, this method may still consider it to be valid.\n * This method supports 3D or 2D foldings.\n * @attribution Implementation of the algorithm described in\n * the origami foldability paper by belcastro-Hull.\n * @param {FOLDExtended} graph a FOLD object with vertices in creasePattern layout\n * @returns {number[]} for every vertex, 0 if the vertex has\n * a valid folded state, or a number indicating the amount of error.\n */\nexport const verticesFoldability = ({\n\tvertices_coords, vertices_vertices, vertices_edges, vertices_faces,\n\tedges_vertices, edges_foldAngle, edges_vector, faces_vertices,\n}) => {\n\tif (!vertices_vertices) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_vertices = makeVerticesVertices({\n\t\t\tvertices_coords, vertices_edges, vertices_faces, edges_vertices, faces_vertices,\n\t\t});\n\t}\n\tif (!vertices_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges = makeVerticesEdges({ edges_vertices, vertices_vertices });\n\t}\n\tif (!vertices_faces) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_faces = makeVerticesFaces({ vertices_coords, vertices_vertices, faces_vertices });\n\t}\n\tconst vertices_vectors = makeVerticesVerticesVector({\n\t\tvertices_coords,\n\t\tvertices_vertices,\n\t\tvertices_edges,\n\t\tvertices_faces,\n\t\tedges_vertices,\n\t\tedges_vector,\n\t\tfaces_vertices,\n\t});\n\n\t// for each vertex, create a sequence of matrices which represent the\n\t// 3D transformations involved in walking around the vertex along the\n\t// folded faces. If the product of the matrices becomes the identity\n\t// matrix (we return from where we started), the folding is valid.\n\treturn vertices_coords.map((_, v) => {\n\t\t// if the vertex lies along a boundary (missing a face), it's foldable\n\t\tif (vertices_faces[v].includes(undefined)\n\t\t\t|| vertices_faces[v].includes(null)) { return 0; }\n\n\t\t// this vertex's edges as a sorted list of radians of the angle\n\t\t// of the edge's vector (not the sector angle or fold angle).\n\t\tconst edgesAngles = vertices_vectors[v]\n\t\t\t.map(vec => Math.atan2(vec[1], vec[0]));\n\n\t\t// the vertex's edges' fold angles. in radians\n\t\tconst edgesFoldAngle = vertices_edges[v]\n\t\t\t.map(e => edges_foldAngle[e])\n\t\t\t.map(angle => angle * D2R);\n\n\t\t// for each edge, create two (three) 3D rotation matrices:\n\t\t// aM: a rotation around the Z axis which rotates the edge to lie along\n\t\t//     the (1, 0, 0) X axis (and it's inverse matrix).\n\t\t// fM: a rotation around the X axis which rotates the YZ plane\n\t\t//     by the amount of the fold-angle.\n\t\tconst aM = edgesAngles.map(a => makeMatrix3RotateZ(a));\n\t\tconst aiM = aM.map(m => invertMatrix3(m));\n\t\tconst fM = edgesFoldAngle.map(a => makeMatrix3RotateX(a));\n\n\t\t// for each edge, create a single transform that represents the motion\n\t\t// from face[i] to face[i+1]. Use the inverse of the XY-plane angle to\n\t\t// bring the edge along the X axis, apply the fold angle matrix, then\n\t\t// apply the XY-plane transformation to move the X-axis to the edge.\n\t\tconst localMatrices = vertices_vectors[v]\n\t\t\t.map((__, i) => multiplyMatrices3(\n\t\t\t\taM[i],\n\t\t\t\tmultiplyMatrices3(fM[i], aiM[i]),\n\t\t\t));\n\n\t\t// walk around the vertex and cumulatively apply each edge's transform,\n\t\t// if the vertex is foldable, the matrix will end up back as the identity.\n\t\tlet matrix = [...identity3x4];\n\t\tlocalMatrices.forEach(m => { matrix = multiplyMatrices3(matrix, m); });\n\n\t\t// only check the first 9 elements, we don't care about the translate part.\n\t\treturn Array.from(Array(9))\n\t\t\t.map((__, i) => Math.abs(matrix[i] - identity3x4[i]))\n\t\t\t.reduce((a, b) => a + b, 0);\n\t});\n};\n\n/**\n * @description Given a crease pattern, this method will test every vertex\n * to determine if it is possible to be folded by walking around each vertex\n * face by face and checking if we meet back up exactly where we started.\n * This does not test for self-intersection, if the faces of a single vertex\n * pokes through one another, this method may still consider it to be valid.\n * This method supports 3D or 2D foldings.\n * @attribution Implementation of the algorithm described in\n * the origami foldability paper by belcastro-Hull.\n * @param {FOLD} graph a FOLD object with vertices in creasePattern layout\n * @param {number} [epsilon=1e-6] an optional epsilon\n * @returns {boolean[]} indices of vertices which have a valid folded state.\n */\nexport const verticesFoldable = (graph, epsilon = EPSILON) => (\n\tverticesFoldability(graph)\n\t\t.map(deviation => Math.abs(deviation) < epsilon)\n);\n\n// would be nice if we can use faces_matrix. but this doesn't work\n// keeping this around as a reminder. try this again sometime.\n// export const VerticesFoldableFirst = derived(\n// \t[FrameEdgesAreFlat, VerticesMatrices, VerticesInverseMatrices],\n// \t([$FrameEdgesAreFlat, $VerticesMatrices, $VerticesInverseMatrices]) => {\n// \t\tif ($FrameEdgesAreFlat) { return []; }\n// \t\ttry {\n// \t\t\treturn $VerticesMatrices.map((matrices, v) => {\n// \t\t\t\tif (matrices.includes(undefined)) { return true; }\n// \t\t\t\tif (matrices.length < 2) { return true; }\n// \t\t\t\tconst locals = matrices\n// \t\t\t\t\t.map((_, i, arr) => [i, (i + 1) % arr.length])\n// \t\t\t\t\t.map(([a, b]) => [\n// \t\t\t\t\t\t$VerticesMatrices[v][b],\n// \t\t\t\t\t\t$VerticesInverseMatrices[v][a],\n// \t\t\t\t\t])\n// \t\t\t\t\t.map(([m1, m2]) => multiplyMatrices3(m1, m2));\n// \t\t\t\tlet result = identity3x4;\n// \t\t\t\tlocals.forEach(m => { result = multiplyMatrices3(m, result); });\n// \t\t\t\t// console.log(\"locals\", locals);\n// \t\t\t\t// console.log(\"result\", result); // [9], result[10], result[11]);\n// \t\t\t\treturn Array.from(Array(9))\n// \t\t\t\t\t.map((_, i) => Math.abs(result[i] - identity3x4[i]) < 1e-3)\n// \t\t\t\t\t.reduce((a, b) => a && b, true);\n// \t\t\t});\n// \t\t} catch (error) {}\n// \t\treturn [];\n// \t},\n// \t[],\n// );\n"
  },
  {
    "path": "src/singleVertex/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as degree4 from \"./degree4.js\";\nimport * as flatFoldable from \"./flatFoldable.js\";\nimport * as foldable from \"./foldable.js\";\nimport * as kawasaki from \"./kawasaki.js\";\nimport * as maekawa from \"./maekawa.js\";\n\n/**\n * @description A collection of operations done on single vertices\n * (one vertex in a graph typically surrounded by edges).\n */\nexport default {\n\t...degree4,\n\t...flatFoldable,\n\t...foldable,\n\t...kawasaki,\n\t...maekawa,\n};\n"
  },
  {
    "path": "src/singleVertex/kawasaki.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tdot2,\n\tnormalize2,\n\tsubtract2,\n} from \"../math/vector.js\";\nimport {\n\tassignmentCanBeFolded,\n} from \"../fold/spec.js\";\nimport {\n\tmakeVerticesEdgesUnsorted,\n} from \"../graph/make/verticesEdges.js\";\nimport {\n\tcounterClockwiseOrder2,\n\tcounterClockwiseAngleRadians,\n\tisCounterClockwiseBetween,\n} from \"../math/radial.js\";\n\n/**\n * @description Given a list of numbers, this method will sort them by\n * even and odd indices and sum the two categories, returning two sums.\n * @param {number[]} numbers one list of numbers\n * @returns {number[]} one array of two sums, even and odd indices\n */\nexport const alternatingSum = (numbers) => [0, 1]\n\t.map(even_odd => numbers\n\t\t.filter((_, i) => i % 2 === even_odd)\n\t\t.reduce((a, b) => a + b, 0));\n\n/**\n * @description Separate out a list of numbers by their indices,\n * odd and even, sum the numbers in each of the two lists, and\n * return for each list, the deviation from the average of the sum.\n * @param {number[]} sectors one list of numbers\n * @returns {number[]} one array of two numbers. if both alternating sets sum\n * to the same, the result will be [0, 0]. if the first set is 2 more than the\n * second, the result will be [1, -1]. (not [2, 0] or something with a 2 in it)\n */\nexport const alternatingSumDifference = (sectors) => {\n\tconst halfsum = sectors.reduce((a, b) => a + b, 0) / 2;\n\treturn alternatingSum(sectors).map(s => s - halfsum);\n};\n\n/**\n * @description given a set of edges around a single vertex (expressed as an array\n * of radian angles), find all possible single-ray additions which\n * when added to the set, the set satisfies Kawasaki's theorem.\n * @usage this is hard coded to work for flat-plane, where sectors sum to 360deg\n * @param {number[]} radians the angle of the edges in radians,\n * like vectors around a vertex. pre-sorted.\n * @returns {number[]} for every sector either one vector (as an angle in radians)\n * or undefined if that sector contains no solution.\n */\nexport const kawasakiSolutionsRadians = (radians) => radians\n\t// counter clockwise angle between this index and the next\n\t.map((v, i, arr) => [v, arr[(i + 1) % arr.length]])\n\t.map(([a, b]) => counterClockwiseAngleRadians(a, b))\n\n\t// for every sector, make an array of all the OTHER sectors\n\t.map((_, i, arr) => arr.slice(i + 1, arr.length).concat(arr.slice(0, i)))\n\n\t// for every sector, use the sector score from the OTHERS two to split it\n\t.map(opposite_sectors => alternatingSum(opposite_sectors).map(s => Math.PI - s))\n\n\t// add the deviation to the edge to get the absolute position\n\t.map((kawasakis, i) => radians[i] + kawasakis[0])\n\n\t// sometimes this results in a solution OUTSIDE the sector. ignore these\n\t.map((angle, i) => (isCounterClockwiseBetween(\n\t\tangle,\n\t\tradians[i],\n\t\tradians[(i + 1) % radians.length],\n\t)\n\t\t? angle\n\t\t: undefined));\n\n// or should we remove the indices so the array reports [ empty x2, ...]\n/**\n * @description given a set of edges around a single vertex (expressed as an array\n * of vectors), find all possible single-ray additions which\n * when added to the set, the set satisfies Kawasaki's theorem.\n * @usage this is hard coded to work for flat-plane, where sectors sum to 360deg\n * @param {number[][]} vectors array of vectors, the edges around a single vertex. pre-sorted.\n * @returns {[number, number][]} for every sector either one vector\n * or undefined if that sector contains no solution.\n */\nexport const kawasakiSolutionsVectors = (vectors) => (\n\tkawasakiSolutionsRadians(vectors.map(v => Math.atan2(v[1], v[0])))\n\t\t.map(a => (a === undefined\n\t\t\t? undefined\n\t\t\t: [Math.cos(a), Math.sin(a)]))\n);\n\n// todo: this is doing too much work in preparation\n/**\n * @description given a single vertex in a graph which does not yet satisfy Kawasaki's theorem,\n * find all possible single-ray additions which when added to the set, the set\n * satisfies Kawasaki's theorem.\n * @usage this is hard coded to work for flat-plane, where sectors sum to 360deg\n * @param {FOLD} graph a FOLD object\n * @param {number} vertex the index of the vertex\n * @returns {number[][]} for every sector either one vector or\n * undefined if that sector contains no solution.\n */\nexport const kawasakiSolutions = (\n\t{ vertices_coords, vertices_edges, edges_assignment, edges_vertices },\n\tvertex,\n) => {\n\t// to calculate Kawasaki's theorem, we need the 3 edges\n\t// as vectors, and we need them sorted radially.\n\tif (!vertices_edges) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tvertices_edges = makeVerticesEdgesUnsorted({ edges_vertices });\n\t}\n\t// for each of the vertex's adjacent edges,\n\t// get the edge's vertices, order them such that\n\t// the vertex is in spot 0, the other is spot 1.\n\tconst edges = edges_assignment\n\t\t? vertices_edges[vertex]\n\t\t\t.filter(e => assignmentCanBeFolded[edges_assignment[e]])\n\t\t: vertices_edges[vertex];\n\tif (edges.length % 2 === 0) { return []; }\n\tconst vert_edges_vertices = edges\n\t\t.map(edge => (edges_vertices[edge][0] === vertex\n\t\t\t? edges_vertices[edge]\n\t\t\t: [edges_vertices[edge][1], edges_vertices[edge][0]]));\n\tconst vert_edges_coords = vert_edges_vertices\n\t\t.map(ev => ev.map(v => vertices_coords[v]));\n\tconst vert_edges_vector = vert_edges_coords\n\t\t.map(coords => subtract2(coords[1], coords[0]));\n\tconst sortedVectors = counterClockwiseOrder2(vert_edges_vector)\n\t\t.map(i => vert_edges_vector[i]);\n\tconst result = kawasakiSolutionsVectors(sortedVectors);\n\tconst normals = sortedVectors.map(normalize2);\n\tconst filteredResults = result\n\t\t.filter(a => a !== undefined)\n\t\t.filter(vector => !normals\n\t\t\t.map(v => dot2(vector, v))\n\t\t\t.map(d => Math.abs(1 - d) < 1e-3)\n\t\t\t.reduce((a, b) => a || b, false));\n\treturn filteredResults;\n};\n"
  },
  {
    "path": "src/singleVertex/maekawa.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport { assignmentIsBoundary } from \"../fold/spec.js\";\n\n/**\n *\n */\nconst getUnassignedIndices = (edges_assignment) => edges_assignment\n\t.map((_, i) => i)\n\t.filter(i => edges_assignment[i] === \"U\" || edges_assignment[i] === \"u\");\n\n// sectors and assignments are fenceposted.\n// sectors[i] is bounded by assignment[i] assignment[i + 1]\n\n/**\n * @description given a set of assignments (M/V/F/B/U characters),\n * which contains some U (unassigned), find all permutations\n * of mountain valley to replace all U which satisfy Maekawa's theorem.\n * This function solves only one single vertex, the assignments\n * are sorted radially around the vertex.\n * Additionally, if the set contains a boundary (\"B\" or \"C\"), then it returns\n * all possible permutations. If it doesn't incude a boundary, it runs\n * Maekawa's theorem and if M-V=+/-2 is not satisfied it returns an empty array.\n * @param {string[]} vertices_edgesAssignments array of FOLD spec\n * assignment characters of the edges radially around a single vertex.\n * @returns {string[][]} array of arrays of strings, all permutations where \"U\"\n * assignments have been replaced with \"V\" or \"M\".\n */\nexport const maekawaSolver = (vertices_edgesAssignments) => {\n\tconst unassigneds = getUnassignedIndices(vertices_edgesAssignments);\n\tconst permuts = Array.from(Array(2 ** unassigneds.length))\n\t\t.map((_, i) => i.toString(2))\n\t\t.map(l => Array(unassigneds.length - l.length + 1).join(\"0\") + l)\n\t\t.map(str => Array.from(str).map(l => (l === \"0\" ? \"V\" : \"M\")));\n\tconst all = permuts.map(perm => {\n\t\tconst array = vertices_edgesAssignments.slice();\n\t\tunassigneds.forEach((index, i) => { array[index] = perm[i]; });\n\t\treturn array;\n\t});\n\tconst boundaryCount = vertices_edgesAssignments\n\t\t.filter(a => assignmentIsBoundary[a])\n\t\t.length;\n\tif (boundaryCount > 0) { return all; }\n\tconst count_m = all.map(a => a.filter(l => l === \"M\" || l === \"m\").length);\n\tconst count_v = all.map(a => a.filter(l => l === \"V\" || l === \"v\").length);\n\treturn all.filter((_, i) => Math.abs(count_m[i] - count_v[i]) === 2);\n};\n"
  },
  {
    "path": "src/svg/arguments/makeCoordinates.js",
    "content": "/* SVG (c) Kraft */\nimport { str_number, str_object } from '../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * this will extract coordinates from a set of inputs\n * and present them as a stride-2 flat array. length % 2 === 0\n * a 1D array of numbers, alternating x y\n *\n * use flatten() everytime you call this!\n * it's necessary the entries sit at the top level of ...args\n * findCoordinates(...flatten(...args));\n */\nconst makeCoordinates = (...args) => args\n\t.filter(a => typeof a === str_number)\n\t.concat(args\n\t\t.filter(a => typeof a === str_object && a !== null)\n\t\t.map((el) => {\n\t\t\tif (typeof el.x === str_number) { return [el.x, el.y]; }\n\t\t\tif (typeof el[0] === str_number) { return [el[0], el[1]]; }\n\t\t\treturn undefined;\n\t\t}).filter(a => a !== undefined)\n\t\t.reduce((a, b) => a.concat(b), []));\n\nexport { makeCoordinates as default };\n"
  },
  {
    "path": "src/svg/arguments/makeViewBox.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from './makeCoordinates.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @param {number} x\n * @param {number} y\n * @param {number} width\n * @param {number} height\n * @param {number} [padding=0]\n * @returns {string}\n */\nconst viewBoxValuesToString = function (x, y, width, height, padding = 0) {\n\tconst scale = 1.0;\n\tconst d = (width / scale) - width;\n\tconst X = (x - d) - padding;\n\tconst Y = (y - d) - padding;\n\tconst W = (width + d * 2) + padding * 2;\n\tconst H = (height + d * 2) + padding * 2;\n\treturn [X, Y, W, H].join(\" \");\n};\n\n/**\n * @returns {string | undefined}\n */\nconst makeViewBox = (...args) => {\n\tconst nums = makeCoordinates(...args.flat());\n\tif (nums.length === 2) { nums.unshift(0, 0); }\n\treturn nums.length === 4\n\t\t? viewBoxValuesToString(nums[0], nums[1], nums[2], nums[3])\n\t\t: undefined;\n};\n\nexport { makeViewBox as default };\n"
  },
  {
    "path": "src/svg/arguments/semiFlattenArrays.js",
    "content": "/* SVG (c) Kraft */\nimport { str_string, str_function } from '../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst svgIsIterable = (obj) => obj != null\n\t&& typeof obj[Symbol.iterator] === str_function;\n\n/**\n * @description flatten only until the point of comma separated entities.\n * This will preserve vectors (number[]) in an array of array of vectors.\n * @param {any[][]} args any array, intended to contain arrays of arrays.\n * @returns {array[]} a flattened copy, flattened up until the point before\n * combining arrays of elements.\n */\nconst svgSemiFlattenArrays = function () {\n\tswitch (arguments.length) {\n\tcase 0: return Array.from(arguments);\n\t// only if its an array (is iterable) and NOT a string\n\tcase 1: return svgIsIterable(arguments[0]) && typeof arguments[0] !== str_string\n\t\t? svgSemiFlattenArrays(...arguments[0])\n\t\t: [arguments[0]];\n\tdefault:\n\t\treturn Array.from(arguments).map(a => (svgIsIterable(a)\n\t\t\t? [...svgSemiFlattenArrays(a)]\n\t\t\t: a));\n\t}\n};\n\nexport { svgSemiFlattenArrays as default, svgSemiFlattenArrays };\n"
  },
  {
    "path": "src/svg/colors/convert.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @param {number} n\n */\nconst roundF = n => Math.round(n * 100) / 100;\n\n/**\n * @description Convert hue-saturation-lightness values into\n * three RGB values, each between 0 and 1 (not 0-255).\n * @param {number} hue value between 0 and 360\n * @param {number} saturation value between 0 and 100\n * @param {number} lightness value between 0 and 100\n * @param {number | undefined} alpha the alpha component from 0 to 1\n * @returns {number[]} three values between 0 and 255, or four\n * if an alpha value is provided, where the fourth is between 0 and 1.\n * @linkcode Origami ./src/convert/svgParsers/colors/hexToRGB.js 10\n */\nconst hslToRgb = (hue, saturation, lightness, alpha) => {\n\tconst s = saturation / 100;\n\tconst l = lightness / 100;\n\t/** @param {number} n */\n\tconst k = n => (n + hue / 30) % 12;\n\tconst a = s * Math.min(l, 1 - l);\n\t/** @param {number} n */\n\tconst f = n => (\n\t\tl - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))\n\t);\n\treturn alpha === undefined\n\t\t? [f(0) * 255, f(8) * 255, f(4) * 255]\n\t\t: [f(0) * 255, f(8) * 255, f(4) * 255, alpha];\n};\n\n/**\n *\n */\nconst mapHexNumbers = (numbers, map) => {\n\t// ensure a minimum number of characters (fill 0 if needed)\n\tconst chars = Array.from(Array(map.length))\n\t\t.map((_, i) => numbers[i] || \"0\");\n\t// handle abbreviated hex codes: #fb4 or #fb48 (with alpha)\n\treturn numbers.length <= 4\n\t\t? map.map(i => chars[i]).join(\"\")\n\t\t: chars.join(\"\");\n};\n\n/**\n * @description Convert a hex string into an array of\n * three numbers, the rgb values (between 0 and 1).\n * This ignores any alpha values.\n * @param {string} string a hex color code as a string\n * @returns {number[]} three values between 0 and 255\n * @linkcode Origami ./src/convert/svgParsers/colors/hexToRGB.js 10\n */\nconst hexToRgb = (string) => {\n\tconst numbers = string.replace(/#(?=\\S)/g, \"\");\n\tconst hasAlpha = numbers.length === 4 || numbers.length === 8;\n\tconst hexString = hasAlpha\n\t\t? mapHexNumbers(numbers, [0, 0, 1, 1, 2, 2, 3, 3])\n\t\t: mapHexNumbers(numbers, [0, 0, 1, 1, 2, 2]);\n\tconst c = parseInt(hexString, 16);\n\treturn hasAlpha\n\t\t? [(c >> 24) & 255, (c >> 16) & 255, (c >> 8) & 255, roundF((c & 255) / 256)]\n\t\t: [(c >> 16) & 255, (c >> 8) & 255, c & 255];\n};\n\n/**\n * @param {number} red the red component from 0 to 255\n * @param {number} green the green component from 0 to 255\n * @param {number} blue the blue component from 0 to 255\n * @param {number | undefined} alpha the alpha component from 0 to 1\n * @returns {string} hex string, with our without alpha.\n */\nconst rgbToHex = (red, green, blue, alpha) => {\n\t/** @param {number} n */\n\tconst to16 = n => `00${Math.max(0, Math.min(Math.round(n), 255)).toString(16)}`\n\t\t.slice(-2);\n\tconst hex = `#${[red, green, blue].map(to16).join(\"\")}`;\n\treturn alpha === undefined\n\t\t? hex\n\t\t: `${hex}${to16(alpha * 255)}`;\n};\n\nexport { hexToRgb, hslToRgb, rgbToHex };\n"
  },
  {
    "path": "src/svg/colors/cssColors.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst cssColors = {\n\tblack: \"#000000\",\n\tsilver: \"#c0c0c0\",\n\tgray: \"#808080\",\n\twhite: \"#ffffff\",\n\tmaroon: \"#800000\",\n\tred: \"#ff0000\",\n\tpurple: \"#800080\",\n\tfuchsia: \"#ff00ff\",\n\tgreen: \"#008000\",\n\tlime: \"#00ff00\",\n\tolive: \"#808000\",\n\tyellow: \"#ffff00\",\n\tnavy: \"#000080\",\n\tblue: \"#0000ff\",\n\tteal: \"#008080\",\n\taqua: \"#00ffff\",\n\torange: \"#ffa500\",\n\taliceblue: \"#f0f8ff\",\n\tantiquewhite: \"#faebd7\",\n\taquamarine: \"#7fffd4\",\n\tazure: \"#f0ffff\",\n\tbeige: \"#f5f5dc\",\n\tbisque: \"#ffe4c4\",\n\tblanchedalmond: \"#ffebcd\",\n\tblueviolet: \"#8a2be2\",\n\tbrown: \"#a52a2a\",\n\tburlywood: \"#deb887\",\n\tcadetblue: \"#5f9ea0\",\n\tchartreuse: \"#7fff00\",\n\tchocolate: \"#d2691e\",\n\tcoral: \"#ff7f50\",\n\tcornflowerblue: \"#6495ed\",\n\tcornsilk: \"#fff8dc\",\n\tcrimson: \"#dc143c\",\n\tcyan: \"#00ffff\",\n\tdarkblue: \"#00008b\",\n\tdarkcyan: \"#008b8b\",\n\tdarkgoldenrod: \"#b8860b\",\n\tdarkgray: \"#a9a9a9\",\n\tdarkgreen: \"#006400\",\n\tdarkgrey: \"#a9a9a9\",\n\tdarkkhaki: \"#bdb76b\",\n\tdarkmagenta: \"#8b008b\",\n\tdarkolivegreen: \"#556b2f\",\n\tdarkorange: \"#ff8c00\",\n\tdarkorchid: \"#9932cc\",\n\tdarkred: \"#8b0000\",\n\tdarksalmon: \"#e9967a\",\n\tdarkseagreen: \"#8fbc8f\",\n\tdarkslateblue: \"#483d8b\",\n\tdarkslategray: \"#2f4f4f\",\n\tdarkslategrey: \"#2f4f4f\",\n\tdarkturquoise: \"#00ced1\",\n\tdarkviolet: \"#9400d3\",\n\tdeeppink: \"#ff1493\",\n\tdeepskyblue: \"#00bfff\",\n\tdimgray: \"#696969\",\n\tdimgrey: \"#696969\",\n\tdodgerblue: \"#1e90ff\",\n\tfirebrick: \"#b22222\",\n\tfloralwhite: \"#fffaf0\",\n\tforestgreen: \"#228b22\",\n\tgainsboro: \"#dcdcdc\",\n\tghostwhite: \"#f8f8ff\",\n\tgold: \"#ffd700\",\n\tgoldenrod: \"#daa520\",\n\tgreenyellow: \"#adff2f\",\n\tgrey: \"#808080\",\n\thoneydew: \"#f0fff0\",\n\thotpink: \"#ff69b4\",\n\tindianred: \"#cd5c5c\",\n\tindigo: \"#4b0082\",\n\tivory: \"#fffff0\",\n\tkhaki: \"#f0e68c\",\n\tlavender: \"#e6e6fa\",\n\tlavenderblush: \"#fff0f5\",\n\tlawngreen: \"#7cfc00\",\n\tlemonchiffon: \"#fffacd\",\n\tlightblue: \"#add8e6\",\n\tlightcoral: \"#f08080\",\n\tlightcyan: \"#e0ffff\",\n\tlightgoldenrodyellow: \"#fafad2\",\n\tlightgray: \"#d3d3d3\",\n\tlightgreen: \"#90ee90\",\n\tlightgrey: \"#d3d3d3\",\n\tlightpink: \"#ffb6c1\",\n\tlightsalmon: \"#ffa07a\",\n\tlightseagreen: \"#20b2aa\",\n\tlightskyblue: \"#87cefa\",\n\tlightslategray: \"#778899\",\n\tlightslategrey: \"#778899\",\n\tlightsteelblue: \"#b0c4de\",\n\tlightyellow: \"#ffffe0\",\n\tlimegreen: \"#32cd32\",\n\tlinen: \"#faf0e6\",\n\tmagenta: \"#ff00ff\",\n\tmediumaquamarine: \"#66cdaa\",\n\tmediumblue: \"#0000cd\",\n\tmediumorchid: \"#ba55d3\",\n\tmediumpurple: \"#9370db\",\n\tmediumseagreen: \"#3cb371\",\n\tmediumslateblue: \"#7b68ee\",\n\tmediumspringgreen: \"#00fa9a\",\n\tmediumturquoise: \"#48d1cc\",\n\tmediumvioletred: \"#c71585\",\n\tmidnightblue: \"#191970\",\n\tmintcream: \"#f5fffa\",\n\tmistyrose: \"#ffe4e1\",\n\tmoccasin: \"#ffe4b5\",\n\tnavajowhite: \"#ffdead\",\n\toldlace: \"#fdf5e6\",\n\tolivedrab: \"#6b8e23\",\n\torangered: \"#ff4500\",\n\torchid: \"#da70d6\",\n\tpalegoldenrod: \"#eee8aa\",\n\tpalegreen: \"#98fb98\",\n\tpaleturquoise: \"#afeeee\",\n\tpalevioletred: \"#db7093\",\n\tpapayawhip: \"#ffefd5\",\n\tpeachpuff: \"#ffdab9\",\n\tperu: \"#cd853f\",\n\tpink: \"#ffc0cb\",\n\tplum: \"#dda0dd\",\n\tpowderblue: \"#b0e0e6\",\n\trosybrown: \"#bc8f8f\",\n\troyalblue: \"#4169e1\",\n\tsaddlebrown: \"#8b4513\",\n\tsalmon: \"#fa8072\",\n\tsandybrown: \"#f4a460\",\n\tseagreen: \"#2e8b57\",\n\tseashell: \"#fff5ee\",\n\tsienna: \"#a0522d\",\n\tskyblue: \"#87ceeb\",\n\tslateblue: \"#6a5acd\",\n\tslategray: \"#708090\",\n\tslategrey: \"#708090\",\n\tsnow: \"#fffafa\",\n\tspringgreen: \"#00ff7f\",\n\tsteelblue: \"#4682b4\",\n\ttan: \"#d2b48c\",\n\tthistle: \"#d8bfd8\",\n\ttomato: \"#ff6347\",\n\tturquoise: \"#40e0d0\",\n\tviolet: \"#ee82ee\",\n\twheat: \"#f5deb3\",\n\twhitesmoke: \"#f5f5f5\",\n\tyellowgreen: \"#9acd32\",\n};\n\nexport { cssColors as default };\n"
  },
  {
    "path": "src/svg/colors/index.js",
    "content": "/* SVG (c) Kraft */\nimport cssColors from './cssColors.js';\nimport * as convert from './convert.js';\nimport * as parseColor from './parseColor.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst colors = {\n\tcssColors,\n\t...convert,\n\t...parseColor,\n};\n\nexport { colors as default };\n"
  },
  {
    "path": "src/svg/colors/parseColor.js",
    "content": "/* SVG (c) Kraft */\nimport cssColors from './cssColors.js';\nimport { hexToRgb, hslToRgb, rgbToHex } from './convert.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n *\n */\nconst getParenNumbers = str => {\n\tconst match = str.match(/\\(([^\\)]+)\\)/g);\n\tif (match == null || !match.length) { return []; }\n\treturn match[0]\n\t\t.substring(1, match[0].length - 1)\n\t\t.split(/[\\s,]+/)\n\t\t.map(parseFloat);\n};\n\n/**\n * @description input a color as a string and get back the RGB\n * values as three numbers in an array. This supports CSS/SVG\n * color strings like named colors, hex colors, rgb(), hsl().\n * @param {string} string a CSS/SVG color string in any form\n * @returns {number[] | undefined} red green blue values between 0 and 255,\n * with possible 4th value between 0 and 1.\n */\nconst parseColorToRgb = (string) => {\n\tif (cssColors[string]) { return hexToRgb(cssColors[string]); }\n\tif (string[0] === \"#\") { return hexToRgb(string); }\n\tif (string.substring(0, 4) === \"rgba\"\n\t\t|| string.substring(0, 3) === \"rgb\") {\n\t\tconst values = getParenNumbers(string);\n\t\t[0, 1, 2]\n\t\t\t.filter(i => values[i] === undefined)\n\t\t\t.forEach(i => { values[i] = 0; });\n\t\treturn values;\n\t}\n\tif (string.substring(0, 4) === \"hsla\"\n\t\t|| string.substring(0, 3) === \"hsl\") {\n\t\tconst values = getParenNumbers(string);\n\t\t[0, 1, 2]\n\t\t\t.filter(i => values[i] === undefined)\n\t\t\t.forEach(i => { values[i] = 0; });\n\t\treturn hslToRgb(values[0], values[1], values[2], values[3]);\n\t}\n\treturn undefined;\n};\n\n/**\n * @description input a color as a string and return the\n * same color as a hex value string. This supports CSS/SVG\n * color strings like named colors, hex colors, rgb(), hsl().\n * @param {string} string a CSS/SVG color string in any form\n * @returns {string} a hex-color form of the input color string.\n */\nconst parseColorToHex = (string) => {\n\tif (cssColors[string]) { return cssColors[string].toUpperCase(); }\n\t// convert back and forth, this converts 3 or 4 digit hex to 6 or 8.\n\tif (string[0] === \"#\") {\n\t\tconst [r, g, b, a] = hexToRgb(string);\n\t\treturn rgbToHex(r, g, b, a);\n\t}\n\tif (string.substring(0, 4) === \"rgba\"\n\t\t|| string.substring(0, 3) === \"rgb\") {\n\t\tconst [r, g, b, a] = getParenNumbers(string);\n\t\treturn rgbToHex(r, g, b, a);\n\t}\n\tif (string.substring(0, 4) === \"hsla\"\n\t\t|| string.substring(0, 3) === \"hsl\") {\n\t\tconst values = getParenNumbers(string);\n\t\t[0, 1, 2]\n\t\t\t.filter(i => values[i] === undefined)\n\t\t\t.forEach(i => { values[i] = 0; });\n\t\tconst [h, s, l, a] = values;\n\t\tconst [r, g, b] = hslToRgb(h, s, l, a);\n\t\treturn rgbToHex(r, g, b, a);\n\t}\n\treturn undefined;\n};\n\nexport { parseColorToHex, parseColorToRgb };\n"
  },
  {
    "path": "src/svg/constructor/elements.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../environment/window.js';\nimport { str_svg } from '../environment/strings.js';\nimport { nodeNames } from '../spec/nodes.js';\nimport Constructor from './index.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @type {{[key: string]: Function }}\n */\nconst constructorList = {};\n\nnodeNames.forEach(nodeName => {\n\tconstructorList[nodeName] = (...args) => Constructor(nodeName, null, ...args);\n});\n\n/**\n * @type {{\n *   svg: (...args) => SVGElement,\n *   defs: (...args) => SVGElement,\n *   desc: (...args) => SVGElement,\n *   filter: (...args) => SVGElement,\n *   metadata: (...args) => SVGElement,\n *   style: (...args) => SVGElement,\n *   script: (...args) => SVGElement,\n *   title: (...args) => SVGElement,\n *   view: (...args) => SVGElement,\n *   cdata: (...args) => SVGElement,\n *   g: (...args) => SVGElement,\n *   circle: (...args) => SVGElement,\n *   ellipse: (...args) => SVGElement,\n *   line: (...args) => SVGElement,\n *   path: (...args) => SVGElement,\n *   polygon: (...args) => SVGElement,\n *   polyline: (...args) => SVGElement,\n *   rect: (...args) => SVGElement,\n *   arc: (...args) => SVGElement,\n *   arrow: (...args) => SVGElement,\n *   curve: (...args) => SVGElement,\n *   parabola: (...args) => SVGElement,\n *   roundRect: (...args) => SVGElement,\n *   wedge: (...args) => SVGElement,\n *   origami: (...args) => SVGElement,\n *   text: (...args) => SVGElement,\n *   marker: (...args) => SVGElement,\n *   symbol: (...args) => SVGElement,\n *   clipPath: (...args) => SVGElement,\n *   mask: (...args) => SVGElement,\n *   linearGradient: (...args) => SVGElement,\n *   radialGradient: (...args) => SVGElement,\n *   pattern: (...args) => SVGElement,\n *   textPath: (...args) => SVGElement,\n *   tspan: (...args) => SVGElement,\n *   stop: (...args) => SVGElement,\n *   feBlend: (...args) => SVGElement,\n *   feColorMatrix: (...args) => SVGElement,\n *   feComponentTransfer: (...args) => SVGElement,\n *   feComposite: (...args) => SVGElement,\n *   feConvolveMatrix: (...args) => SVGElement,\n *   feDiffuseLighting: (...args) => SVGElement,\n *   feDisplacementMap: (...args) => SVGElement,\n *   feDistantLight: (...args) => SVGElement,\n *   feDropShadow: (...args) => SVGElement,\n *   feFlood: (...args) => SVGElement,\n *   feFuncA: (...args) => SVGElement,\n *   feFuncB: (...args) => SVGElement,\n *   feFuncG: (...args) => SVGElement,\n *   feFuncR: (...args) => SVGElement,\n *   feGaussianBlur: (...args) => SVGElement,\n *   feImage: (...args) => SVGElement,\n *   feMerge: (...args) => SVGElement,\n *   feMergeNode: (...args) => SVGElement,\n *   feMorphology: (...args) => SVGElement,\n *   feOffset: (...args) => SVGElement,\n *   fePointLight: (...args) => SVGElement,\n *   feSpecularLighting: (...args) => SVGElement,\n *   feSpotLight: (...args) => SVGElement,\n *   feTile: (...args) => SVGElement,\n *   feTurbulence: (...args) => SVGElement,\n * }}\n */\nconst constructors = Object.assign(constructorList);\n\n/**\n * @description Create an SVG element\n * @param {...any} args a series of options objects\n * @returns {SVGElement} an svg element\n */\nconst svg = (...args) => {\n\tconst svgElement = Constructor(str_svg, null, ...args);\n\tconst initialize = () => args\n\t\t.filter(arg => typeof arg === \"function\")\n\t\t.forEach(func => func.call(svgElement, svgElement));\n\t// call initialize as soon as possible. check if page has loaded\n\tif (RabbitEarWindow().document.readyState === \"loading\") {\n\t\tRabbitEarWindow().document.addEventListener(\"DOMContentLoaded\", initialize);\n\t} else {\n\t\tinitialize();\n\t}\n\treturn svgElement;\n};\n\nexport { constructors, svg };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arc/index.js",
    "content": "/* SVG (c) Kraft */\nimport arcPath from '../shared/makeArcPath.js';\nimport { str_path } from '../../../environment/strings.js';\nimport TransformMethods from '../shared/transforms.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst arcArguments = (a, b, c, d, e) => [arcPath(a, b, c, d, e, false)];\n\nconst arcDef = {\n\tarc: {\n\t\tnodeName: str_path,\n\t\tattributes: [\"d\"],\n\t\targs: arcArguments,\n\t\tmethods: {\n\t\t\tsetArc: (el, ...args) => el.setAttribute(\"d\", arcArguments(...args)),\n\t\t\t...TransformMethods,\n\t\t},\n\t},\n};\n\nexport { arcDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arrow/index.js",
    "content": "/* SVG (c) Kraft */\nimport ArrowMethods from './methods.js';\nimport init from './init.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst arrowDef = {\n\tarrow: {\n\t\tnodeName: \"g\",\n\t\tattributes: [],\n\t\targs: () => [], // one function\n\t\tmethods: ArrowMethods, // object of functions\n\t\tinit,\n\t},\n};\n\nexport { arrowDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arrow/init.js",
    "content": "/* SVG (c) Kraft */\nimport { str_class, str_arrow, str_tail, str_head, str_path, str_style, str_stroke, str_none, str_object } from '../../../environment/strings.js';\nimport NS from '../../../spec/namespace.js';\nimport RabbitEarWindow from '../../../environment/window.js';\nimport ArrowMethods from './methods.js';\nimport { makeArrowOptions } from './options.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n// import Library from \"../../../library.js\";\n\nconst arrowKeys = Object.keys(makeArrowOptions());\n\nconst matchingOptions = (...args) => {\n\tfor (let a = 0; a < args.length; a += 1) {\n\t\tif (typeof args[a] !== str_object) { continue; }\n\t\tconst keys = Object.keys(args[a]);\n\t\tfor (let i = 0; i < keys.length; i += 1) {\n\t\t\tif (arrowKeys.includes(keys[i])) {\n\t\t\t\treturn args[a];\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n};\n\nconst init = function (parent, ...args) {\n\tconst element = RabbitEarWindow().document.createElementNS(NS, \"g\");\n\telement.setAttribute(str_class, str_arrow);\n\tconst paths = [\"line\", str_tail, str_head].map(key => {\n\t\tconst path = RabbitEarWindow().document.createElementNS(NS, str_path);\n\t\tpath.setAttribute(str_class, `${str_arrow}-${key}`);\n\t\telement.appendChild(path);\n\t\treturn path;\n\t});\n\tpaths[0].setAttribute(str_style, \"fill:none;\");\n\tpaths[1].setAttribute(str_stroke, str_none);\n\tpaths[2].setAttribute(str_stroke, str_none);\n\telement.options = makeArrowOptions();\n\tconst options = matchingOptions(...args);\n\tArrowMethods.setPoints(element, ...(options.segment || args));\n\tif (options) {\n\t\tObject.keys(options)\n\t\t\t.filter(key => ArrowMethods[key])\n\t\t\t.forEach(key => ArrowMethods[key](element, options[key]));\n\t}\n\treturn element;\n};\n\nexport { init as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arrow/makeArrowPaths.js",
    "content": "/* SVG (c) Kraft */\nimport { str_tail, str_head } from '../../../environment/strings.js';\nimport { svg_sub2, svg_add2, svg_scale2, svg_magnitude2 } from '../../../general/algebra.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst ends = [str_tail, str_head];\nconst stringifyPoint = p => p.join(\",\");\nconst pointsToPath = (points) => \"M\" + points.map(pt => pt.join(\",\")).join(\"L\") + \"Z\";\n\n/**\n * @description\n * @param {{\n *   points: [number, number, number, number],\n *   padding: number,\n *   bend: number,\n *   pinch: number,\n * }} options\n */\nconst makeArrowPaths = (options) => {\n\t// throughout, tail is 0, head is 1\n\t/** @type {[[number, number], [number, number]]} */\n\tlet pts = [\n\t\t[options.points[0] || 0, options.points[1] || 0],\n\t\t[options.points[2] || 0, options.points[3] || 0],\n\t];\n\tlet vector = svg_sub2(pts[1], pts[0]);\n\tlet midpoint = svg_add2(pts[0], svg_scale2(vector, 0.5));\n\t// make sure arrow isn't too small\n\tconst len = svg_magnitude2(vector);\n\tconst minLength = ends\n\t\t.map(s => (options[s].visible\n\t\t\t? (1 + options[s].padding) * options[s].height * 2.5\n\t\t\t: 0))\n\t\t.reduce((a, b) => a + b, 0);\n\tif (len < minLength) {\n\t\t// check len === exactly 0. don't compare to epsilon here\n\t\t/** @type {[number, number]} */\n\t\tconst minVec = len === 0 ? [minLength, 0] : svg_scale2(vector, minLength / len);\n\t\t// pts = [svg_sub2, svg_add2].map(f => f(midpoint, svg_scale2(minVec, 0.5)));\n\t\tpts = [\n\t\t\tsvg_sub2(midpoint, svg_scale2(minVec, 0.5)),\n\t\t\tsvg_add2(midpoint, svg_scale2(minVec, 0.5)),\n\t\t];\n\t\tvector = svg_sub2(pts[1], pts[0]);\n\t}\n\t/** @type {[number, number]} */\n\tlet perpendicular = [vector[1], -vector[0]];\n\tlet bezPoint = svg_add2(midpoint, svg_scale2(perpendicular, options.bend));\n\tconst bezs = pts.map(pt => svg_sub2(bezPoint, pt));\n\tconst bezsLen = bezs.map(v => svg_magnitude2(v));\n\tconst bezsNorm = bezs.map((bez, i) => (bezsLen[i] === 0\n\t\t? bez\n\t\t: svg_scale2(bez, 1 / bezsLen[i])));\n\tconst vectors = bezsNorm.map(norm => svg_scale2(norm, -1));\n\t/** @type {[[number, number], [number, number]]} */\n\tconst normals = [\n\t\t[vectors[0][1], -vectors[0][0]],\n\t\t[vectors[1][1], -vectors[1][0]],\n\t];\n\t// get padding from either head/tail options or root of options\n\tconst pad = ends.map((s, i) => (options[s].padding\n\t\t? options[s].padding\n\t\t: (options.padding ? options.padding : 0.0)));\n\tconst scales = ends\n\t\t.map((s, i) => options[s].height * (options[s].visible ? 1 : 0))\n\t\t.map((n, i) => n + pad[i]);\n\t\t// .map((s, i) => options[s].height * ((options[s].visible ? 1 : 0) + pad[i]));\n\tconst arcs = pts.map((pt, i) => svg_add2(pt, svg_scale2(bezsNorm[i], scales[i])));\n\t// readjust bezier curve now that the arrow heads push inwards\n\tvector = svg_sub2(arcs[1], arcs[0]);\n\tperpendicular = [vector[1], -vector[0]];\n\tmidpoint = svg_add2(arcs[0], svg_scale2(vector, 0.5));\n\tbezPoint = svg_add2(midpoint, svg_scale2(perpendicular, options.bend));\n\t// done adjust\n\tconst controls = arcs\n\t\t.map((arc, i) => svg_add2(arc, svg_scale2(svg_sub2(bezPoint, arc), options.pinch)));\n\tconst polyPoints = ends.map((s, i) => [\n\t\tsvg_add2(arcs[i], svg_scale2(vectors[i], options[s].height)),\n\t\tsvg_add2(arcs[i], svg_scale2(normals[i], options[s].width / 2)),\n\t\tsvg_add2(arcs[i], svg_scale2(normals[i], -options[s].width / 2)),\n\t]);\n\treturn {\n\t\tline: `M${stringifyPoint(arcs[0])}C${stringifyPoint(controls[0])},${stringifyPoint(controls[1])},${stringifyPoint(arcs[1])}`,\n\t\ttail: pointsToPath(polyPoints[0]),\n\t\thead: pointsToPath(polyPoints[1]),\n\t};\n};\n\nexport { makeArrowPaths as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arrow/methods.js",
    "content": "/* SVG (c) Kraft */\nimport { str_arrow, str_head, str_tail, str_boolean, str_object, str_function } from '../../../environment/strings.js';\nimport { toCamel } from '../../../general/string.js';\nimport { svgSemiFlattenArrays } from '../../../arguments/semiFlattenArrays.js';\nimport makeCoordinates from '../../../arguments/makeCoordinates.js';\nimport makeArrowPaths from './makeArrowPaths.js';\nimport TransformMethods from '../shared/transforms.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// end is \"head\" or \"tail\"\nconst setArrowheadOptions = (element, options, which) => {\n\tif (typeof options === str_boolean) {\n\t\telement.options[which].visible = options;\n\t} else if (typeof options === str_object) {\n\t\tObject.assign(element.options[which], options);\n\t\tif (options.visible == null) {\n\t\t\telement.options[which].visible = true;\n\t\t}\n\t} else if (options == null) {\n\t\telement.options[which].visible = true;\n\t}\n};\n\nconst setArrowStyle = (element, options = {}, which = str_head) => {\n\tconst path = Array.from(element.childNodes)\n\t\t.filter(el => el.getAttribute(\"class\") === `${str_arrow}-${which}`)\n\t\t.shift();\n\t// const path = element.getElementsByClassName(`${str_arrow}-${which}`)[0];\n\t// find options which translate to object methods (el.stroke(\"red\"))\n\tObject.keys(options)\n\t\t.map(key => ({ key, fn: path[toCamel(key)] }))\n\t\t.filter(el => typeof el.fn === str_function && el.key !== \"class\")\n\t\t.forEach(el => el.fn(options[el.key]));\n\t// find options which don't work as methods, set as attributes\n\t// Object.keys(options)\n\t// \t.map(key => ({ key, fn: path[toCamel(key)] }))\n\t// \t.filter(el => typeof el.fn !== S.str_function && el.key !== \"class\")\n\t// \t.forEach(el => path.setAttribute(el.key, options[el.key]));\n\t//\n\t// apply a class attribute (add, don't overwrite existing classes)\n\tObject.keys(options)\n\t\t.filter(key => key === \"class\")\n\t\t.forEach(key => path.classList.add(options[key]));\n};\n\nconst redraw = (element) => {\n\tconst paths = makeArrowPaths(element.options);\n\tObject.keys(paths)\n\t\t.map(path => ({\n\t\t\tpath,\n\t\t\telement: Array.from(element.childNodes)\n\t\t\t\t.filter(el => el.getAttribute(\"class\") === `${str_arrow}-${path}`)\n\t\t\t\t.shift(),\n\t\t}))\n\t\t.filter(el => el.element)\n\t\t.map(el => { el.element.setAttribute(\"d\", paths[el.path]); return el; })\n\t\t.filter(el => element.options[el.path])\n\t\t.forEach(el => el.element.setAttribute(\n\t\t\t\"visibility\",\n\t\t\telement.options[el.path].visible\n\t\t\t\t? \"visible\"\n\t\t\t\t: \"hidden\",\n\t\t));\n\treturn element;\n};\n\nconst setPoints = (element, ...args) => {\n\telement.options.points = makeCoordinates(...svgSemiFlattenArrays(...args)).slice(0, 4);\n\treturn redraw(element);\n};\n\nconst bend = (element, amount) => {\n\telement.options.bend = amount;\n\treturn redraw(element);\n};\n\nconst pinch = (element, amount) => {\n\telement.options.pinch = amount;\n\treturn redraw(element);\n};\n\nconst padding = (element, amount) => {\n\telement.options.padding = amount;\n\treturn redraw(element);\n};\n\nconst head = (element, options) => {\n\tsetArrowheadOptions(element, options, str_head);\n\tsetArrowStyle(element, options, str_head);\n\treturn redraw(element);\n};\n\nconst tail = (element, options) => {\n\tsetArrowheadOptions(element, options, str_tail);\n\tsetArrowStyle(element, options, str_tail);\n\treturn redraw(element);\n};\n\n// const getLine = element => element.getElementsByClassName(`${str_arrow}-line`)[0];\n// const getHead = element => element.getElementsByClassName(`${str_arrow}-${str_head}`)[0];\n// const getTail = element => element.getElementsByClassName(`${str_arrow}-${str_tail}`)[0];\nconst getLine = element => Array.from(element.childNodes)\n\t.filter(el => el.getAttribute(\"class\") === `${str_arrow}-line`)\n\t.shift();\nconst getHead = element => Array.from(element.childNodes)\n\t.filter(el => el.getAttribute(\"class\") === `${str_arrow}-${str_head}`)\n\t.shift();\nconst getTail = element => Array.from(element.childNodes)\n\t.filter(el => el.getAttribute(\"class\") === `${str_arrow}-${str_tail}`)\n\t.shift();\n\nconst ArrowMethods = {\n\tsetPoints,\n\tpoints: setPoints,\n\tbend,\n\tpinch,\n\tpadding,\n\thead,\n\ttail,\n\tgetLine,\n\tgetHead,\n\tgetTail,\n\t...TransformMethods,\n};\n\nexport { ArrowMethods as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/arrow/options.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst endOptions = () => ({\n\tvisible: false,\n\twidth: 8,\n\theight: 10,\n\tpadding: 0.0,\n});\n\nconst makeArrowOptions = () => ({\n\thead: endOptions(),\n\ttail: endOptions(),\n\tbend: 0.0,\n\tpadding: 0.0,\n\tpinch: 0.618,\n\tpoints: [],\n});\n\nexport { makeArrowOptions };\n"
  },
  {
    "path": "src/svg/constructor/extensions/circle.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport nodes_attributes from '../../spec/nodes_attributes.js';\nimport { svg_distance2 } from '../../general/algebra.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst setRadius = (el, r) => {\n\tel.setAttribute(nodes_attributes.circle[2], r);\n\treturn el;\n};\n\nconst setOrigin = (el, a, b) => {\n\t[...makeCoordinates(...[a, b].flat()).slice(0, 2)]\n\t\t.forEach((value, i) => el.setAttribute(nodes_attributes.circle[i], value));\n\treturn el;\n};\n\nconst fromPoints = (a, b, c, d) => [a, b, svg_distance2([a, b], [c, d])];\n/**\n * @name circle\n * @memberof svg\n * @description Draw an SVG Circle element.\n * @param {number} radius the radius of the circle\n * @param {...number|number[]} center the center of the circle\n * @returns {Element} an SVG node element\n * @linkcode SVG ./src/nodes/spec/circle.js 28\n */\nconst circleDef = {\n\tcircle: {\n\t\targs: (a, b, c, d) => {\n\t\t\tconst coords = makeCoordinates(...[a, b, c, d].flat());\n\t\t\t// console.log(\"SVG circle coords\", coords);\n\t\t\tswitch (coords.length) {\n\t\t\tcase 0: case 1: return [, , ...coords];\n\t\t\tcase 2: case 3: return coords;\n\t\t\t// case 4\n\t\t\tdefault: return fromPoints(...coords);\n\t\t\t}\n\t\t\t// return makeCoordinates(...flatten(a, b, c)).slice(0, 3);\n\t\t},\n\t\tmethods: {\n\t\t\tradius: setRadius,\n\t\t\tsetRadius,\n\t\t\torigin: setOrigin,\n\t\t\tsetOrigin,\n\t\t\tcenter: setOrigin,\n\t\t\tsetCenter: setOrigin,\n\t\t\tposition: setOrigin,\n\t\t\tsetPosition: setOrigin,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { circleDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/curve/arguments.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from '../../../arguments/makeCoordinates.js';\nimport makeCurvePath from './makeCurvePath.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst curveArguments = (...args) => [\n\tmakeCurvePath(makeCoordinates(...args.flat())),\n];\n\nexport { curveArguments as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/curve/getCurveEndpoints.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst getNumbersFromPathCommand = str => str\n\t.slice(1)\n\t.split(/[, ]+/)\n\t.map(s => parseFloat(s));\n\n// this gets the parameter numbers, in an array\nconst getCurveTos = d => d\n\t.match(/[Cc][(0-9), .-]+/)\n\t.map(curve => getNumbersFromPathCommand(curve));\n\nconst getMoveTos = d => d\n\t.match(/[Mm][(0-9), .-]+/)\n\t.map(curve => getNumbersFromPathCommand(curve));\n\nconst getCurveEndpoints = (d) => {\n\t// get only the first Move and Curve commands\n\tconst move = getMoveTos(d).shift();\n\tconst curve = getCurveTos(d).shift();\n\tconst start = move\n\t\t? [move[move.length - 2], move[move.length - 1]]\n\t\t: [0, 0];\n\tconst end = curve\n\t\t? [curve[curve.length - 2], curve[curve.length - 1]]\n\t\t: [0, 0];\n\treturn [...start, ...end];\n};\n\nexport { getCurveEndpoints as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/curve/index.js",
    "content": "/* SVG (c) Kraft */\nimport curveArguments from './arguments.js';\nimport curve_methods from './methods.js';\nimport { str_path } from '../../../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst curveDef = {\n\tcurve: {\n\t\tnodeName: str_path,\n\t\tattributes: [\"d\"],\n\t\targs: curveArguments, // one function\n\t\tmethods: curve_methods, // object of functions\n\t},\n};\n\nexport { curveDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/curve/makeCurvePath.js",
    "content": "/* SVG (c) Kraft */\nimport { svg_sub2, svg_add2, svg_scale2 } from '../../../general/algebra.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// endpoints is an array of 4 numbers\nconst makeCurvePath = (endpoints = [], bend = 0, pinch = 0.5) => {\n\t/** @type {[number, number]} */\n\tconst tailPt = [endpoints[0] || 0, endpoints[1] || 0];\n\t/** @type {[number, number]} */\n\tconst headPt = [endpoints[2] || 0, endpoints[3] || 0];\n\tconst vector = svg_sub2(headPt, tailPt);\n\tconst midpoint = svg_add2(tailPt, svg_scale2(vector, 0.5));\n\t/** @type {[number, number]} */\n\tconst perpendicular = [vector[1], -vector[0]];\n\tconst bezPoint = svg_add2(midpoint, svg_scale2(perpendicular, bend));\n\tconst tailControl = svg_add2(tailPt, svg_scale2(svg_sub2(bezPoint, tailPt), pinch));\n\tconst headControl = svg_add2(headPt, svg_scale2(svg_sub2(bezPoint, headPt), pinch));\n\treturn `M${tailPt[0]},${tailPt[1]}C${tailControl[0]},${tailControl[1]} ${headControl[0]},${headControl[1]} ${headPt[0]},${headPt[1]}`;\n};\n\nexport { makeCurvePath as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/curve/methods.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from '../../../arguments/makeCoordinates.js';\nimport makeCurvePath from './makeCurvePath.js';\nimport getCurveEndpoints from './getCurveEndpoints.js';\nimport TransformMethods from '../shared/transforms.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst setPoints = (element, ...args) => {\n\tconst coords = makeCoordinates(...args.flat()).slice(0, 4);\n\telement.setAttribute(\"d\", makeCurvePath(coords, element._bend, element._pinch));\n\treturn element;\n};\n\nconst bend = (element, amount) => {\n\telement._bend = amount;\n\treturn setPoints(element, ...getCurveEndpoints(element.getAttribute(\"d\")));\n};\n\nconst pinch = (element, amount) => {\n\telement._pinch = amount;\n\treturn setPoints(element, ...getCurveEndpoints(element.getAttribute(\"d\")));\n};\n\nconst curve_methods = {\n\tsetPoints,\n\tbend,\n\tpinch,\n\t...TransformMethods,\n};\n\nexport { curve_methods as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/ellipse.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport nodes_attributes from '../../spec/nodes_attributes.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// const setRadii = (el, rx, ry) => [,,rx,ry]\n//   .forEach((value, i) => el.setAttribute(nodes_attributes.ellipse[i], value));\nconst setRadii = (el, rx, ry) => {\n\t[, , rx, ry].forEach((value, i) => el.setAttribute(nodes_attributes.ellipse[i], value));\n\treturn el;\n};\n\nconst setOrigin = (el, a, b) => {\n\t[...makeCoordinates(...[a, b].flat()).slice(0, 2)]\n\t\t.forEach((value, i) => el.setAttribute(nodes_attributes.ellipse[i], value));\n\treturn el;\n};\n\nconst ellipseDef = {\n\tellipse: {\n\t\targs: (a, b, c, d) => {\n\t\t\tconst coords = makeCoordinates(...[a, b, c, d].flat()).slice(0, 4);\n\t\t\tswitch (coords.length) {\n\t\t\tcase 0: case 1: case 2: return [, , ...coords];\n\t\t\tdefault: return coords;\n\t\t\t}\n\t\t},\n\t\tmethods: {\n\t\t\tradius: setRadii,\n\t\t\tsetRadius: setRadii,\n\t\t\torigin: setOrigin,\n\t\t\tsetOrigin,\n\t\t\tcenter: setOrigin,\n\t\t\tsetCenter: setOrigin,\n\t\t\tposition: setOrigin,\n\t\t\tsetPosition: setOrigin,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { ellipseDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/g.js",
    "content": "/* SVG (c) Kraft */\nimport '../../environment/window.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst gDef = {\n\tg: {\n\t\t// init,\n\t\tmethods: {\n\t\t\t// load: loadGroup,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { gDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/index.js",
    "content": "/* SVG (c) Kraft */\nimport svgDef from './svg/index.js';\nimport gDef from './g.js';\nimport circleDef from './circle.js';\nimport ellipseDef from './ellipse.js';\nimport lineDef from './line.js';\nimport pathDef from './path.js';\nimport rectDef from './rect.js';\nimport styleDef from './style.js';\nimport textDef from './text.js';\nimport maskTypes from './maskTypes.js';\nimport polyDefs from './polys.js';\nimport arcDef from './arc/index.js';\nimport arrowDef from './arrow/index.js';\nimport curveDef from './curve/index.js';\nimport wedgeDef from './wedge/index.js';\nimport origamiDef from './origami/index.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * in each of these instances, arguments maps the arguments to attributes\n * as the attributes are listed in the \"attributes\" folder.\n *\n * arguments: function. this should convert the array of arguments into\n * an array of (processed) arguments. 1:1. arguments into arguments.\n * make sure it is returning an array.\n *\n */\nconst extensions = {\n\t...svgDef,\n\t...gDef,\n\t...circleDef,\n\t...ellipseDef,\n\t...lineDef,\n\t...pathDef,\n\t...rectDef,\n\t...styleDef,\n\t...textDef,\n\t// multiple\n\t...maskTypes,\n\t...polyDefs,\n\t// extensions\n\t...arcDef,\n\t...arrowDef,\n\t...curveDef,\n\t...wedgeDef,\n\t...origamiDef,\n};\n\nexport { extensions as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/line.js",
    "content": "/* SVG (c) Kraft */\nimport { svgSemiFlattenArrays } from '../../arguments/semiFlattenArrays.js';\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport nodes_attributes from '../../spec/nodes_attributes.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst Args = (...args) => makeCoordinates(...svgSemiFlattenArrays(...args)).slice(0, 4);\n\nconst setPoints = (element, ...args) => {\n\tArgs(...args).forEach((value, i) => element.setAttribute(nodes_attributes.line[i], value));\n\treturn element;\n};\n/**\n * @name line\n * @description SVG Line element\n * @memberof SVG\n * @linkcode SVG ./src/nodes/spec/line.js 18\n */\nconst lineDef = {\n\tline: {\n\t\targs: Args,\n\t\tmethods: {\n\t\t\tsetPoints,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { lineDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/maskTypes.js",
    "content": "/* SVG (c) Kraft */\nimport { str_string } from '../../environment/strings.js';\nimport { makeUUID } from '../../general/string.js';\nimport { setViewBox } from '../../general/viewBox.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst makeIDString = function () {\n\treturn Array.from(arguments)\n\t\t.filter(a => typeof a === str_string || a instanceof String)\n\t\t.shift() || makeUUID();\n};\n\nconst maskArgs = (...args) => [makeIDString(...args)];\n\nconst maskTypes = {\n\tmask: {\n\t\targs: maskArgs,\n\t\tmethods: {\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n\tclipPath: {\n\t\targs: maskArgs,\n\t\tmethods: {\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n\tsymbol: {\n\t\targs: maskArgs,\n\t\tmethods: {\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n\tmarker: {\n\t\targs: maskArgs,\n\t\tmethods: {\n\t\t\tsize: setViewBox,\n\t\t\tsetViewBox: setViewBox,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { maskTypes as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/origami/index.js",
    "content": "/* SVG (c) Kraft */\nimport init from './init.js';\nimport methods from './methods.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst origamiDef = {\n\torigami: {\n\t\tnodeName: \"g\",\n\t\tinit,\n\t\targs: () => [],\n\t\tmethods,\n\t},\n};\n\nexport { origamiDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/origami/init.js",
    "content": "/* SVG (c) Kraft */\nimport NS from '../../../spec/namespace.js';\nimport RabbitEarWindow from '../../../environment/window.js';\nimport lib from '../../../environment/lib.js';\nimport { findElementTypeInParents } from '../../../general/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// this is a patch on the system, foldToSvg will normally check the parent\n// chain of the <g> element until an SVG is found, and if the options ask\n// for it, set the viewBox to auto-size.\n// unfortunately, node.parentElement does not get updated until after the next\n// draw call, before foldToSvg checks... so this method got copied out here\n// and is duplicated in a way that makes me really sad.\n// I wish there was an elegant way to wait until the parent property populates.\nconst applyViewBox = (parent, element, graph, options = {}) => {\n\tconst unitBounds = { min: [0, 0], max: [1, 1], span: [1, 1] };\n\tconst box = lib.ear.graph.boundingBox(graph) || unitBounds;\n\tconst svgElement = findElementTypeInParents(parent, \"svg\");\n\tif (svgElement && options && options.viewBox) {\n\t\tconst viewBoxValue = [box.min, box.span]\n\t\t\t.flatMap(p => [p[0], p[1]])\n\t\t\t.join(\" \");\n\t\tsvgElement.setAttributeNS(null, \"viewBox\", viewBoxValue);\n\t}\n};\n\nconst init = (parent, graph, options = {}) => {\n\tconst g = RabbitEarWindow().document.createElementNS(NS, \"g\");\n\t// this call to renderSVG mimics the call inside of the foldToSvg method\n\tlib.ear.convert.renderSVG(graph, g, {\n\t\tviewBox: true,\n\t\tstrokeWidth: true,\n\t\t...options,\n\t});\n\t// mimic the call to renderSVG\n\tapplyViewBox(parent, g, graph, {\n\t\tviewBox: true,\n\t\tstrokeWidth: true,\n\t\t...options,\n\t});\n\treturn g;\n};\n\nexport { init as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/origami/methods.js",
    "content": "/* SVG (c) Kraft */\nimport TransformMethods from '../shared/transforms.js';\nimport methods$1 from '../shared/urls.js';\nimport * as dom from '../shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// const clearSVG = (element) => {\n// \tArray.from(element.attributes)\n// \t\t.filter(attr => attr.name !== \"xmlns\" && attr.name !== \"version\")\n// \t\t.forEach(attr => element.removeAttribute(attr.name));\n// \treturn DOM.removeChildren(element);\n// };\n\n// const vertices = (...args) => {\n// \tlib.ear.convert.foldToSvg.vertices(...args);\n// \tconst g = window().document.createElementNS(NS, \"g\");\n// \tlib.ear.convert.foldToSvg.drawInto(g, ...args);\n// \treturn g;\n// };\n\n/**\n * @description Given an Element, search its children for the first\n * one which contains a \"class\" that matches the parameter string\n * @param {any} group\n * @param {string} className\n * @returns {Element|null}\n */\nconst getChildWithClass = (group, className) => {\n\tconst childNodes = group ? group.childNodes : undefined;\n\tif (!childNodes) { return null; }\n\treturn Array.from(childNodes)\n\t\t.filter(el => el.getAttribute(\"class\") === className)\n\t\t.shift();\n};\n\nconst vertices = (...args) => getChildWithClass(args[0], \"vertices\");\nconst edges = (...args) => getChildWithClass(args[0], \"edges\");\nconst faces = (...args) => getChildWithClass(args[0], \"faces\");\nconst boundaries = (...args) => getChildWithClass(args[0], \"boundaries\");\n\n// these will end up as methods on the <svg> nodes\nconst methods = {\n\tvertices,\n\tedges,\n\tfaces,\n\tboundaries,\n\t...TransformMethods,\n\t...methods$1,\n\t...dom,\n};\n\nexport { methods as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/path.js",
    "content": "/* SVG (c) Kraft */\nimport { pathCommandNames, parsePathCommands } from '../../general/path.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * @param {SVGElement} el one svg element, intended to be a <path> element\n * @returns {string} the \"d\" attribute, or if unset, returns an empty string \"\".\n */\nconst getD = (el) => {\n\tconst attr = el.getAttribute(\"d\");\n\treturn (attr == null) ? \"\" : attr;\n};\n\nconst clear = element => {\n\telement.removeAttribute(\"d\");\n\treturn element;\n};\n\n// todo: would be great if for arguments > 2 it alternated space and comma\nconst appendPathCommand = (el, command, ...args) => {\n\tel.setAttribute(\"d\", `${getD(el)}${command}${args.flat().join(\" \")}`);\n\treturn el;\n};\n\n// break out the path commands into an array of descriptive objects\nconst getCommands = element => parsePathCommands(getD(element));\n\n// const setters = {\n//   string: setPathString,\n//   object: setPathCommands,\n// };\n// const appenders = {\n//   string: appendPathString,\n//   object: appendPathCommands,\n// };\n\n// depending on the user's argument, different setters will get called\n// const noClearSet = (element, ...args) => {\n//   if (args.length === 1) {\n//     const typ = typeof args[0];\n//     if (setters[typ]) {\n//       setters[typ](element, args[0]);\n//     }\n//   }\n// };\n\n// const clearAndSet = (element, ...args) => {\n//   if (args.length === 1) {\n//     const typ = typeof args[0];\n//     if (setters[typ]) {\n//       clear(element);\n//       setters[typ](element, args[0]);\n//     }\n//   }\n// };\n\nconst path_methods = {\n\taddCommand: appendPathCommand,\n\tappendCommand: appendPathCommand,\n\tclear,\n\tgetCommands: getCommands,\n\tget: getCommands,\n\tgetD: el => el.getAttribute(\"d\"),\n\t// set: clearAndSet,\n\t// add: noClearSet,\n\t...TransformMethods,\n\t...methods,\n\t...dom,\n};\n\nObject.keys(pathCommandNames).forEach((key) => {\n\tpath_methods[pathCommandNames[key]] = (el, ...args) => appendPathCommand(el, key, ...args);\n});\n\nconst pathDef = {\n\tpath: {\n\t\tmethods: path_methods,\n\t},\n};\n\nexport { pathDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/polys.js",
    "content": "/* SVG (c) Kraft */\nimport { str_points, str_string } from '../../environment/strings.js';\nimport { svgSemiFlattenArrays } from '../../arguments/semiFlattenArrays.js';\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst getPoints = (el) => {\n\tconst attr = el.getAttribute(str_points);\n\treturn (attr == null) ? \"\" : attr;\n};\n\nconst polyString = function () {\n\treturn Array\n\t\t.from(Array(Math.floor(arguments.length / 2)))\n\t\t.map((_, i) => `${arguments[i * 2 + 0]},${arguments[i * 2 + 1]}`)\n\t\t.join(\" \");\n};\n\nconst stringifyArgs = (...args) => [\n\tpolyString(...makeCoordinates(...svgSemiFlattenArrays(...args))),\n];\n\nconst setPoints = (element, ...args) => {\n\telement.setAttribute(str_points, stringifyArgs(...args)[0]);\n\treturn element;\n};\n\nconst addPoint = (element, ...args) => {\n\telement.setAttribute(str_points, [getPoints(element), stringifyArgs(...args)[0]]\n\t\t.filter(a => a !== \"\")\n\t\t.join(\" \"));\n\treturn element;\n};\n\n// this should be improved\n// right now the special case is if there is only 1 argument and it's a string\n// it should be able to take strings or numbers at any point,\n// converting the strings to coordinates\nconst Args = function (...args) {\n\treturn args.length === 1 && typeof args[0] === str_string\n\t\t? [args[0]]\n\t\t: stringifyArgs(...args);\n};\n\nconst polyDefs = {\n\tpolyline: {\n\t\targs: Args,\n\t\tmethods: {\n\t\t\tsetPoints,\n\t\t\taddPoint,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n\tpolygon: {\n\t\targs: Args,\n\t\tmethods: {\n\t\t\tsetPoints,\n\t\t\taddPoint,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { polyDefs as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/rect.js",
    "content": "/* SVG (c) Kraft */\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport nodes_attributes from '../../spec/nodes_attributes.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport * as dom from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst setRectSize = (el, rx, ry) => {\n\t[, , rx, ry]\n\t\t.forEach((value, i) => el.setAttribute(nodes_attributes.rect[i], value));\n\treturn el;\n};\n\nconst setRectOrigin = (el, a, b) => {\n\t[...makeCoordinates(...[a, b].flat()).slice(0, 2)]\n\t\t.forEach((value, i) => el.setAttribute(nodes_attributes.rect[i], value));\n\treturn el;\n};\n\n// can handle negative widths and heights\nconst fixNegatives = function (arr) {\n\t[0, 1].forEach(i => {\n\t\tif (arr[2 + i] < 0) {\n\t\t\tif (arr[0 + i] === undefined) { arr[0 + i] = 0; }\n\t\t\tarr[0 + i] += arr[2 + i];\n\t\t\tarr[2 + i] = -arr[2 + i];\n\t\t}\n\t});\n\treturn arr;\n};\n/**\n * @name rect\n * @memberof svg\n * @description Draw an SVG Rect element.\n * @param {number} x the x coordinate of the corner\n * @param {number} y the y coordinate of the corner\n * @param {number} width the length along the x dimension\n * @param {number} height the length along the y dimension\n * @returns {Element} an SVG node element\n * @linkcode SVG ./src/nodes/spec/rect.js 40\n */\nconst rectDef = {\n\trect: {\n\t\targs: (a, b, c, d) => {\n\t\t\tconst coords = makeCoordinates(...[a, b, c, d].flat()).slice(0, 4);\n\t\t\tswitch (coords.length) {\n\t\t\tcase 0: case 1: case 2: case 3: return fixNegatives([, , ...coords]);\n\t\t\tdefault: return fixNegatives(coords);\n\t\t\t}\n\t\t},\n\t\tmethods: {\n\t\t\torigin: setRectOrigin,\n\t\t\tsetOrigin: setRectOrigin,\n\t\t\tcenter: setRectOrigin,\n\t\t\tsetCenter: setRectOrigin,\n\t\t\tsize: setRectSize,\n\t\t\tsetSize: setRectSize,\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\t...dom,\n\t\t},\n\t},\n};\n\nexport { rectDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/shared/dom.js",
    "content": "/* SVG (c) Kraft */\nimport { toKebab } from '../../../general/string.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst removeChildren = (element) => {\n\twhile (element.lastChild) {\n\t\telement.removeChild(element.lastChild);\n\t}\n\treturn element;\n};\n\nconst appendTo = (element, parent) => {\n\tif (parent && parent.appendChild) {\n\t\tparent.appendChild(element);\n\t}\n\treturn element;\n};\n\nconst setAttributes = (element, attrs) => {\n\tObject.keys(attrs)\n\t\t.forEach(key => element.setAttribute(toKebab(key), attrs[key]));\n\treturn element;\n};\n\nexport { appendTo, removeChildren, setAttributes };\n"
  },
  {
    "path": "src/svg/constructor/extensions/shared/makeArcPath.js",
    "content": "/* SVG (c) Kraft */\nimport { svg_polar_to_cart } from '../../../general/algebra.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst arcPath = (x, y, radius, startAngle, endAngle, includeCenter = false) => {\n\tif (endAngle == null) { return \"\"; }\n\tconst start = svg_polar_to_cart(startAngle, radius);\n\tconst end = svg_polar_to_cart(endAngle, radius);\n\tconst arcVec = [end[0] - start[0], end[1] - start[1]];\n\tconst py = start[0] * end[1] - start[1] * end[0];\n\tconst px = start[0] * end[0] + start[1] * end[1];\n\tconst arcdir = (Math.atan2(py, px) > 0 ? 0 : 1);\n\tlet d = (includeCenter\n\t\t? `M ${x},${y} l ${start[0]},${start[1]} `\n\t\t: `M ${x + start[0]},${y + start[1]} `);\n\td += [\"a \", radius, radius, 0, arcdir, 1, arcVec[0], arcVec[1]].join(\" \");\n\tif (includeCenter) { d += \" Z\"; }\n\treturn d;\n};\n\nexport { arcPath as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/shared/transforms.js",
    "content": "/* SVG (c) Kraft */\nimport { str_transform } from '../../../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst getAttr = (element) => {\n\tconst t = element.getAttribute(str_transform);\n\treturn (t == null || t === \"\") ? undefined : t;\n};\n\nconst TransformMethods = {\n\tclearTransform: (el) => { el.removeAttribute(str_transform); return el; },\n};\n\n[\"translate\", \"rotate\", \"scale\", \"matrix\"].forEach(key => {\n\tTransformMethods[key] = (element, ...args) => {\n\t\telement.setAttribute(\n\t\t\tstr_transform,\n\t\t\t[getAttr(element), `${key}(${args.join(\" \")})`]\n\t\t\t\t.filter(a => a !== undefined)\n\t\t\t\t.join(\" \"),\n\t\t);\n\t\treturn element;\n\t};\n});\n\nexport { TransformMethods as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/shared/urls.js",
    "content": "/* SVG (c) Kraft */\nimport { str_string, str_id } from '../../../environment/strings.js';\nimport { toCamel } from '../../../general/string.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// for the clip-path and mask values. looks for the ID as a \"url(#id-name)\" string\nconst findIdURL = function (arg) {\n\tif (arg == null) { return \"\"; }\n\tif (typeof arg === str_string) {\n\t\treturn arg.slice(0, 3) === \"url\"\n\t\t\t? arg\n\t\t\t: `url(#${arg})`;\n\t}\n\tif (arg.getAttribute != null) {\n\t\tconst idString = arg.getAttribute(str_id);\n\t\treturn `url(#${idString})`;\n\t}\n\treturn \"\";\n};\n\nconst methods = {};\n\n// these do not represent the nodes that these methods are applied to\n// every node gets these attribute-setting method (pointing to a mask)\n[\"clip-path\",\n\t\"mask\",\n\t\"symbol\",\n\t\"marker-end\",\n\t\"marker-mid\",\n\t\"marker-start\",\n].forEach(attr => {\n\tmethods[toCamel(attr)] = (element, parent) => {\n\t\telement.setAttribute(attr, findIdURL(parent));\n\t\treturn element;\n\t};\n});\n\nexport { methods as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/style.js",
    "content": "/* SVG (c) Kraft */\nimport NS from '../../spec/namespace.js';\nimport RabbitEarWindow from '../../environment/window.js';\nimport { makeCDATASection } from '../../general/cdata.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst styleDef = {\n\tstyle: {\n\t\tinit: (parent, text) => {\n\t\t\tconst el = RabbitEarWindow().document.createElementNS(NS, \"style\");\n\t\t\tel.setAttribute(\"type\", \"text/css\");\n\t\t\tel.textContent = \"\";\n\t\t\tel.appendChild(makeCDATASection(text));\n\t\t\treturn el;\n\t\t},\n\t\tmethods: {\n\t\t\tsetTextContent: (el, text) => {\n\t\t\t\tel.textContent = \"\";\n\t\t\t\tel.appendChild(makeCDATASection(text));\n\t\t\t\treturn el;\n\t\t\t},\n\t\t},\n\t},\n};\n\nexport { styleDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/animation.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../../../environment/window.js';\nimport { makeUUID } from '../../../general/string.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst Animation = function (element) {\n\tlet start;\n\tlet frame = 0;\n\tlet requestId;\n\tconst handlers = {};\n\n\tconst stop = () => {\n\t\tif (RabbitEarWindow().cancelAnimationFrame) {\n\t\t\tRabbitEarWindow().cancelAnimationFrame(requestId);\n\t\t}\n\t\tObject.keys(handlers).forEach(uuid => delete handlers[uuid]);\n\t};\n\n\tconst play = (handler) => {\n\t\tstop();\n\t\tif (!handler || !(RabbitEarWindow().requestAnimationFrame)) { return; }\n\t\tstart = performance.now();\n\t\tframe = 0;\n\t\tconst uuid = makeUUID();\n\t\thandlers[uuid] = (now) => {\n\t\t\tconst time = (now - start) * 1e-3;\n\t\t\thandler({ time, frame });\n\t\t\tframe += 1;\n\t\t\tif (handlers[uuid]) {\n\t\t\t\trequestId = RabbitEarWindow().requestAnimationFrame(handlers[uuid]);\n\t\t\t}\n\t\t};\n\t\trequestId = RabbitEarWindow().requestAnimationFrame(handlers[uuid]);\n\t};\n\n\tObject.defineProperty(element, \"play\", { set: play, enumerable: true });\n\tObject.defineProperty(element, \"stop\", { value: stop, enumerable: true });\n};\n\nexport { Animation as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/getSVGFrame.js",
    "content": "/* SVG (c) Kraft */\nimport { str_function } from '../../../environment/strings.js';\nimport { getViewBox } from '../../../general/viewBox.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst getSVGFrame = function (element) {\n\tconst viewBox = getViewBox(element);\n\tif (viewBox !== undefined) {\n\t\treturn viewBox;\n\t}\n\tif (typeof element.getBoundingClientRect === str_function) {\n\t\tconst rr = element.getBoundingClientRect();\n\t\treturn [rr.x, rr.y, rr.width, rr.height];\n\t}\n\treturn [];\n};\n\nexport { getSVGFrame as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/index.js",
    "content": "/* SVG (c) Kraft */\nimport NS from '../../../spec/namespace.js';\nimport RabbitEarWindow from '../../../environment/window.js';\nimport makeViewBox from '../../../arguments/makeViewBox.js';\nimport makeCoordinates from '../../../arguments/makeCoordinates.js';\nimport methods from './methods.js';\nimport TouchEvents from './touch.js';\nimport Animation from './animation.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n// import Controls from \"./controls.js\";\n\nconst svgDef = {\n\tsvg: {\n\t\targs: (...args) => [makeViewBox(makeCoordinates(...args))].filter(a => a != null),\n\t\tmethods,\n\t\tinit: (_, ...args) => {\n\t\t\tconst element = RabbitEarWindow().document.createElementNS(NS, \"svg\");\n\t\t\telement.setAttribute(\"version\", \"1.1\");\n\t\t\telement.setAttribute(\"xmlns\", NS);\n\t\t\t// args.filter(a => typeof a === str_string)\n\t\t\t// \t.forEach(string => loadSVG(element, string));\n\t\t\targs.filter(a => a != null)\n\t\t\t\t.filter(el => el.appendChild)\n\t\t\t\t.forEach(parent => parent.appendChild(element));\n\t\t\tTouchEvents(element);\n\t\t\tAnimation(element);\n\t\t\t// Controls(element);\n\t\t\treturn element;\n\t\t},\n\t},\n};\n\nexport { svgDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/makeBackground.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../../../environment/window.js';\nimport { str_class, str_stroke, str_none, str_fill } from '../../../environment/strings.js';\nimport NS from '../../../spec/namespace.js';\nimport nodes_attributes from '../../../spec/nodes_attributes.js';\nimport getSVGFrame from './getSVGFrame.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst bgClass = \"svg-background-rectangle\";\n\n// i prevented circular dependency by passing a pointer to Constructor through 'this'\n// every function is bound\nconst makeBackground = function (element, color) {\n\tlet backRect = Array.from(element.childNodes)\n\t\t.filter(child => child.getAttribute(str_class) === bgClass)\n\t\t.shift();\n\tif (backRect == null) {\n\t\tbackRect = RabbitEarWindow().document.createElementNS(NS, \"rect\");\n\t\tgetSVGFrame(element).forEach((n, i) => backRect.setAttribute(nodes_attributes.rect[i], n));\n\t\t// backRect = this.Constructor(\"rect\", null, ...getSVGFrame(element));\n\t\tbackRect.setAttribute(str_class, bgClass);\n\t\tbackRect.setAttribute(str_stroke, str_none);\n\t\telement.insertBefore(backRect, element.firstChild);\n\t}\n\tbackRect.setAttribute(str_fill, color);\n\treturn element;\n};\n\nexport { makeBackground as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/methods.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../../../environment/window.js';\nimport { str_style } from '../../../environment/strings.js';\nimport NS from '../../../spec/namespace.js';\nimport { makeCDATASection } from '../../../general/cdata.js';\nimport { setViewBox, getViewBox } from '../../../general/viewBox.js';\nimport makeBackground from './makeBackground.js';\nimport getSVGFrame from './getSVGFrame.js';\nimport TransformMethods from '../shared/transforms.js';\nimport * as dom from '../shared/dom.js';\nimport { removeChildren } from '../shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// check if the loader is running synchronously or asynchronously\n// export const loadSVG = (target, data) => {\n// \tconst result = Load(data);\n// \tif (result == null) { return; }\n// \treturn (typeof result.then === str_function)\n// \t\t? result.then(svg => assignSVG(target, svg))\n// \t\t: assignSVG(target, result);\n// };\n\nconst setPadding = function (element, padding) {\n\tconst viewBox = getViewBox(element);\n\tif (viewBox !== undefined) {\n\t\tsetViewBox(element, ...[-padding, -padding, padding * 2, padding * 2]\n\t\t\t.map((nudge, i) => viewBox[i] + nudge));\n\t}\n\treturn element;\n};\n/**\n * @description Locate the first instance of an element matching a nodeName\n */\nconst findOneElement = function (element, nodeName) {\n\tconst styles = element.getElementsByTagName(nodeName);\n\treturn styles.length ? styles[0] : null;\n};\n/**\n * @description Locate an existing stylesheet and append text to it, or\n * create a new stylesheet with the text contents.\n */\nconst stylesheet = function (element, textContent) {\n\tlet styleSection = findOneElement(element, str_style);\n\tif (styleSection == null) {\n\t\tstyleSection = RabbitEarWindow().document.createElementNS(NS, str_style);\n\t\tstyleSection.setTextContent = (text) => {\n\t\t\tstyleSection.textContent = \"\";\n\t\t\tstyleSection.appendChild(makeCDATASection(text));\n\t\t\treturn styleSection;\n\t\t};\n\t\telement.insertBefore(styleSection, element.firstChild);\n\t}\n\tstyleSection.textContent = \"\";\n\tstyleSection.appendChild(makeCDATASection(textContent));\n\treturn styleSection;\n};\n\nconst clearSVG = (element) => {\n\tArray.from(element.attributes)\n\t\t.filter(attr => attr.name !== \"xmlns\" && attr.name !== \"version\")\n\t\t.forEach(attr => element.removeAttribute(attr.name));\n\treturn removeChildren(element);\n};\n\n// these will end up as methods on the <svg> nodes\nconst methods = {\n\tclear: clearSVG,\n\tsize: setViewBox,\n\tsetViewBox,\n\tgetViewBox,\n\tpadding: setPadding,\n\tbackground: makeBackground,\n\tgetWidth: el => getSVGFrame(el)[2],\n\tgetHeight: el => getSVGFrame(el)[3],\n\t// this is named \"stylesheet\" because \"style\" property is already taken.\n\tstylesheet: function (el, text) { return stylesheet.call(this, el, text); },\n\t// load: loadSVG,\n\t// save: Save,\n\t...TransformMethods,\n\t...dom,\n};\n\n// svg.load = function (element, data, callback) {\n//   return Load(data, (svg, error) => {\n//     if (svg != null) { replaceSVG(element, svg); }\n//     if (callback != null) { callback(element, error); }\n//   });\n// };\n\nexport { methods as default, findOneElement };\n"
  },
  {
    "path": "src/svg/constructor/extensions/svg/touch.js",
    "content": "/* SVG (c) Kraft */\nimport { capitalized } from '../../../general/string.js';\nimport { convertToViewBox } from '../../../general/viewBox.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst eventNameCategories = {\n\tmove: [\"mousemove\", \"touchmove\"],\n\tpress: [\"mousedown\", \"touchstart\"], // \"mouseover\",\n\trelease: [\"mouseup\", \"touchend\"],\n\tleave: [\"mouseleave\", \"touchcancel\"],\n};\n\nconst off = (el, handlers) => Object.values(eventNameCategories)\n\t.flat()\n\t.forEach((handlerName) => {\n\t\thandlers[handlerName].forEach(func => el\n\t\t\t.removeEventListener(handlerName, func));\n\t\thandlers[handlerName] = [];\n\t});\n\nconst defineGetter = (obj, prop, value) => Object\n\t.defineProperty(obj, prop, {\n\t\tget: () => value,\n\t\tenumerable: true,\n\t\tconfigurable: true,\n\t});\n\n// todo, more pointers for multiple screen touches\nconst TouchEvents = function (element) {\n\t// hold onto all handlers to be able to turn them off\n\tconst handlers = [];\n\tObject.keys(eventNameCategories).forEach((key) => {\n\t\teventNameCategories[key].forEach((handler) => {\n\t\t\thandlers[handler] = [];\n\t\t});\n\t});\n\n\tconst removeHandler = category => eventNameCategories[category]\n\t\t.forEach(handlerName => handlers[handlerName]\n\t\t\t.forEach(func => element.removeEventListener(handlerName, func)));\n\n\t// assign handlers for onMove, onPress, onRelease, onLeave\n\tObject.keys(eventNameCategories).forEach((category) => {\n\t\tObject.defineProperty(element, `on${capitalized(category)}`, {\n\t\t\tset: (handler) => {\n\t\t\t\tif (!element.addEventListener) { return; }\n\t\t\t\tif (handler == null) {\n\t\t\t\t\tremoveHandler(category);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\teventNameCategories[category].forEach((handlerName) => {\n\t\t\t\t\tconst handlerFunc = (e) => {\n\t\t\t\t\t\tconst pointer = (e.touches != null ? e.touches[0] : e);\n\t\t\t\t\t\t// for onRelease, pointer will be undefined\n\t\t\t\t\t\tif (pointer !== undefined) {\n\t\t\t\t\t\t\tconst { clientX, clientY } = pointer;\n\t\t\t\t\t\t\tconst [x, y] = convertToViewBox(element, clientX, clientY);\n\t\t\t\t\t\t\tdefineGetter(e, \"x\", x);\n\t\t\t\t\t\t\tdefineGetter(e, \"y\", y);\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandler(e);\n\t\t\t\t\t};\n\t\t\t\t\thandlers[handlerName].push(handlerFunc);\n\t\t\t\t\telement.addEventListener(handlerName, handlerFunc);\n\t\t\t\t});\n\t\t\t},\n\t\t\tenumerable: true,\n\t\t});\n\t});\n\n\tObject.defineProperty(element, \"off\", { value: () => off(element, handlers) });\n};\n\nexport { TouchEvents as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/text.js",
    "content": "/* SVG (c) Kraft */\nimport NS from '../../spec/namespace.js';\nimport RabbitEarWindow from '../../environment/window.js';\nimport makeCoordinates from '../../arguments/makeCoordinates.js';\nimport { str_string } from '../../environment/strings.js';\nimport TransformMethods from './shared/transforms.js';\nimport methods from './shared/urls.js';\nimport { appendTo, setAttributes } from './shared/dom.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * @description SVG text element\n * @memberof SVG\n * @linkcode SVG ./src/nodes/spec/text.js 11\n */\nconst textDef = {\n\ttext: {\n\t\t// assuming people will at most supply coordinate (x,y,z) and text\n\t\targs: (a, b, c) => makeCoordinates(...[a, b, c].flat()).slice(0, 2),\n\t\tinit: (parent, a, b, c, d) => {\n\t\t\tconst element = RabbitEarWindow().document.createElementNS(NS, \"text\");\n\t\t\tconst text = [a, b, c, d].filter(el => typeof el === str_string).shift();\n\t\t\telement.appendChild(RabbitEarWindow().document.createTextNode(text || \"\"));\n\t\t\treturn element;\n\t\t},\n\t\tmethods: {\n\t\t\t...TransformMethods,\n\t\t\t...methods,\n\t\t\tappendTo,\n\t\t\tsetAttributes,\n\t\t},\n\t},\n};\n\nexport { textDef as default };\n"
  },
  {
    "path": "src/svg/constructor/extensions/wedge/index.js",
    "content": "/* SVG (c) Kraft */\nimport arcPath from '../shared/makeArcPath.js';\nimport { str_path } from '../../../environment/strings.js';\nimport TransformMethods from '../shared/transforms.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst wedgeArguments = (a, b, c, d, e) => [arcPath(a, b, c, d, e, true)];\n\nconst wedgeDef = {\n\twedge: {\n\t\tnodeName: str_path,\n\t\targs: wedgeArguments,\n\t\tattributes: [\"d\"],\n\t\tmethods: {\n\t\t\tsetArc: (el, ...args) => el.setAttribute(\"d\", wedgeArguments(...args)),\n\t\t\t...TransformMethods,\n\t\t},\n\t},\n};\n\nexport { wedgeDef as default };\n"
  },
  {
    "path": "src/svg/constructor/index.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../environment/window.js';\nimport NS from '../spec/namespace.js';\nimport nodes_children from '../spec/nodes_children.js';\nimport nodes_attributes from '../spec/nodes_attributes.js';\nimport { toCamel } from '../general/string.js';\nimport extensions from './extensions/index.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst passthroughArgs = (...args) => args;\n/**\n * @description This is the main constructor for the library which generates\n * SVGElements (DOM elements) using createElementNS in the svg namespace.\n * Additionally, this element will be bound with methods to operate on the\n * element itself, which do things like set an attribute value or\n * create a child of this object.\n * Using this constructor, this library has full support for all elements\n * in the SVG spec (I think so, double check me on this), additionally,\n * some custom elements, for example \"arrow\" which makes a few shapes under\n * a single <g> group. So this library is highly extendable, you can write\n * your own \"arrow\" objects, see more inside this directory's subdirectories.\n * @param {string} name the name of the element, although, slightly abstracted\n * from the actual element name, like \"line\" for <line> because it supports\n * custom elements, \"arrow\", which in turn will create a <g> or <path> etc..\n * @param {object} parent the parent to append this new node as a child to.\n */\nconst Constructor = (name, parent, ...initArgs) => {\n\t// the node name (like \"line\" for <line>) which is usually the\n\t// same as \"name\", but can sometimes differ in the case of custom elements\n\tconst nodeName = extensions[name] && extensions[name].nodeName\n\t\t? extensions[name].nodeName\n\t\t: name;\n\tconst { init, args, methods } = extensions[name] || {};\n\tconst attributes = nodes_attributes[nodeName] || [];\n\tconst children = nodes_children[nodeName] || [];\n\n\t// create the element itself under the svg namespace.\n\t// or, if the extension specifies a custom initializer, run it instead\n\tconst element = init\n\t\t?\tinit(parent, ...initArgs)\n\t\t: RabbitEarWindow().document.createElementNS(NS, nodeName);\n\n\t// if the parent exists, and the element has no parent yet (could have been\n\t// added during the init), make this element a child.\n\tif (parent && !element.parentElement) { parent.appendChild(element); }\n\n\t// some element initializers can set some attributes set right after\n\t// initialization, if the extension specifies how to assign them,\n\t// if so, they will map to the indices in the nodes nodes_attributes.\n\tconst processArgs = args || passthroughArgs;\n\tprocessArgs(...initArgs).forEach((v, i) => {\n\t\telement.setAttribute(nodes_attributes[nodeName][i], v);\n\t});\n\n\t// if the extension specifies methods these will be bound to the object\n\tif (methods) {\n\t\tObject.keys(methods)\n\t\t\t.forEach(methodName => Object.defineProperty(element, methodName, {\n\t\t\t\tvalue: function () {\n\t\t\t\t\treturn methods[methodName](element, ...arguments);\n\t\t\t\t},\n\t\t\t}));\n\t}\n\n\t// camelCase functional style attribute setters, like .stroke() .strokeWidth()\n\tattributes.forEach((attribute) => {\n\t\tconst attrNameCamel = toCamel(attribute);\n\t\tif (element[attrNameCamel]) { return; }\n\t\tObject.defineProperty(element, attrNameCamel, {\n\t\t\tvalue: function () {\n\t\t\t\telement.setAttribute(attribute, ...arguments);\n\t\t\t\treturn element;\n\t\t\t},\n\t\t});\n\t});\n\n\t// allow this element to initialize another element, and this\n\t// child element will be automatically appended to this element\n\tchildren.forEach((childNode) => {\n\t\tif (element[childNode]) { return; }\n\t\tconst value = function () { return Constructor(childNode, element, ...arguments); };\n\t\tObject.defineProperty(element, childNode, { value });\n\t});\n\n\treturn element;\n};\n\nexport { Constructor as default };\n"
  },
  {
    "path": "src/svg/environment/detect.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst isBrowser = typeof window === \"object\"\n\t&& typeof window.document === \"object\";\n\ntypeof process === \"object\"\n\t&& typeof process.versions === \"object\"\n\t&& (process.versions.node != null || process.versions.bun != null);\n\nexport { isBrowser };\n"
  },
  {
    "path": "src/svg/environment/lib.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst lib = {};\n\nexport { lib as default };\n"
  },
  {
    "path": "src/svg/environment/messages.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst Messages = {\n\twindow: \"window not set; svg.window = @xmldom/xmldom\",\n};\n\nexport { Messages as default };\n"
  },
  {
    "path": "src/svg/environment/strings.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n// frequently-used strings\nconst str_class = \"class\";\nconst str_function = \"function\";\nconst str_boolean = \"boolean\";\nconst str_number = \"number\";\nconst str_string = \"string\";\nconst str_object = \"object\";\n\nconst str_svg = \"svg\";\nconst str_path = \"path\";\n\nconst str_id = \"id\";\nconst str_style = \"style\";\nconst str_viewBox = \"viewBox\";\nconst str_transform = \"transform\";\nconst str_points = \"points\";\nconst str_stroke = \"stroke\";\nconst str_fill = \"fill\";\nconst str_none = \"none\";\n\nconst str_arrow = \"arrow\";\nconst str_head = \"head\";\nconst str_tail = \"tail\";\n\nexport { str_arrow, str_boolean, str_class, str_fill, str_function, str_head, str_id, str_none, str_number, str_object, str_path, str_points, str_string, str_stroke, str_style, str_svg, str_tail, str_transform, str_viewBox };\n"
  },
  {
    "path": "src/svg/environment/window.js",
    "content": "/* SVG (c) Kraft */\nimport { isBrowser } from './detect.js';\nimport Messages from './messages.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// store a pointer to the window object here.\nconst windowContainer = { window: undefined };\n\n// if we are in the browser, by default use the browser's \"window\".\nif (isBrowser) { windowContainer.window = window; }\n\n/**\n * @description create the window.document object, if it does not exist.\n */\nconst buildDocument = (newWindow) => new newWindow.DOMParser()\n\t.parseFromString(\"<!DOCTYPE html><title>.</title>\", \"text/html\");\n\n/**\n * @description This method allows the app to run in both a browser\n * environment, as well as some back-end environment like node.js.\n * In the case of a browser, no need to call this.\n * In the case of a back end environment, include some library such\n * as the popular @XMLDom package and pass it in as the argument here.\n */\nconst setWindow = (newWindow) => {\n\t// make sure window has a document. xmldom does not, and requires it be built.\n\tif (!newWindow.document) { newWindow.document = buildDocument(newWindow); }\n\twindowContainer.window = newWindow;\n\treturn windowContainer.window;\n};\n\n/**\n * @description get the \"window\" object, which should have\n * DOMParser, XMLSerializer, and document.\n */\nconst RabbitEarWindow = () => {\n\tif (windowContainer.window === undefined) {\n\t\tthrow new Error(Messages.window);\n\t}\n\treturn windowContainer.window;\n};\n\nexport { RabbitEarWindow as default, setWindow };\n"
  },
  {
    "path": "src/svg/general/algebra.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/** @param {[number, number]} a @param {[number, number]} b @returns {[number, number]} */\nconst svg_add2 = (a, b) => [a[0] + b[0], a[1] + b[1]];\n\n/** @param {[number, number]} a @param {[number, number]} b @returns {[number, number]} */\nconst svg_sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];\n\n/** @param {[number, number]} a @param {number} s @returns {[number, number]} */\nconst svg_scale2 = (a, s) => [a[0] * s, a[1] * s];\n\n/** @param {[number, number]} a */\nconst svg_magnitudeSq2 = (a) => (a[0] ** 2) + (a[1] ** 2);\n\n/** @param {[number, number]} a */\nconst svg_magnitude2 = (a) => Math.sqrt(svg_magnitudeSq2(a));\n\n/** @param {[number, number]} a @param {[number, number]} b */\nconst svg_distanceSq2 = (a, b) => svg_magnitudeSq2(svg_sub2(a, b));\n\n/** @param {[number, number]} a @param {[number, number]} b */\nconst svg_distance2 = (a, b) => Math.sqrt(svg_distanceSq2(a, b));\n\n/** @param {number} a @param {number} d @returns {[number, number]} */\nconst svg_polar_to_cart = (a, d) => [Math.cos(a) * d, Math.sin(a) * d];\n\n/** @param {number[]} m1 @param {number[]} m2 */\nconst svg_multiplyMatrices2 = (m1, m2) => [\n\tm1[0] * m2[0] + m1[2] * m2[1],\n\tm1[1] * m2[0] + m1[3] * m2[1],\n\tm1[0] * m2[2] + m1[2] * m2[3],\n\tm1[1] * m2[2] + m1[3] * m2[3],\n\tm1[0] * m2[4] + m1[2] * m2[5] + m1[4],\n\tm1[1] * m2[4] + m1[3] * m2[5] + m1[5],\n];\n\nexport { svg_add2, svg_distance2, svg_distanceSq2, svg_magnitude2, svg_magnitudeSq2, svg_multiplyMatrices2, svg_polar_to_cart, svg_scale2, svg_sub2 };\n"
  },
  {
    "path": "src/svg/general/cdata.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../environment/window.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Create a CDATASection containing text from the method\n * parameter. The CDATA is useful to wrap text which may contain\n * invalid characters or characters in need of escaping.\n * @param {string} text the text content to be placed inside the CData\n * @returns {CDATASection} a CDATA containing the given text.\n */\nconst makeCDATASection = (text) => (new (RabbitEarWindow()).DOMParser())\n\t.parseFromString(\"<root></root>\", \"text/xml\")\n\t.createCDATASection(text);\n\nexport { makeCDATASection };\n"
  },
  {
    "path": "src/svg/general/dom.js",
    "content": "/* SVG (c) Kraft */\nimport RabbitEarWindow from '../environment/window.js';\nimport { transformStringToMatrix } from './transforms.js';\nimport { svg_multiplyMatrices2 } from './algebra.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description Parse a string into an XML Element\n * @param {string} input an SVG as a string\n * @param {string} mimeType default to XML, for SVG use \"image/svg+xml\".\n * @returns {Element|null} the document element or null if unsuccessful.\n */\nconst xmlStringToElement = (input, mimeType = \"text/xml\") => {\n\tconst result = (new (RabbitEarWindow().DOMParser)()).parseFromString(input, mimeType);\n\treturn result ? result.documentElement : null;\n};\n\n/**\n * @description Get the furthest root parent up the DOM tree\n * @param {Element} el an element\n * @returns {Element} the top-most parent in the parent node chain.\n */\nconst getRootParent = (el) => {\n\t/** @type {Element} */\n\tlet parent = el;\n\twhile (parent && parent.parentElement != null) {\n\t\tparent = parent.parentElement;\n\t}\n\treturn parent;\n};\n\n/**\n * @description search up the parent-chain until we find the first\n * <Element> with the nodeName matching the parameter,\n * return undefined if none exists.\n * Note: there is no protection against a dependency cycle.\n * @param {Element} element a DOM element\n * @param {string} nodeName the name of the element, like \"svg\" or \"div\"\n * @returns {Element|null} the element if it exists\n */\nconst findElementTypeInParents = (element, nodeName) => {\n\tif ((element.nodeName || \"\") === nodeName) {\n\t\t/** @type {Element} */\n\t\treturn element;\n\t}\n\t/** @type {Element} */\n\tconst parent = element.parentElement;\n\treturn parent\n\t\t? findElementTypeInParents(parent, nodeName)\n\t\t: null;\n};\n\n// polyfil for adding to a classlist\nconst polyfillClassListAdd = (el, ...classes) => {\n\tconst hash = {};\n\tconst getClass = el.getAttribute(\"class\");\n\tconst classArray = getClass ? getClass.split(\" \") : [];\n\tclassArray.push(...classes);\n\tclassArray.forEach(str => { hash[str] = true; });\n\tconst classString = Object.keys(hash).join(\" \");\n\tel.setAttribute(\"class\", classString);\n};\n\n/**\n * @description Add classes to an Element, essentially classList.add(), but\n * it will call a polyfill if classList doesn't exist (as in @xmldom/xmldom)\n * @param {Element} el a DOM element\n * @param {...string} classes a list of class strings to be added to the element\n */\nconst addClass = (el, ...classes) => {\n\tif (!el || !classes.length) { return undefined; }\n\treturn el.classList\n\t\t? el.classList.add(...classes)\n\t\t: polyfillClassListAdd(el, ...classes);\n};\n\n/**\n * @description Recurse through a DOM element and flatten all elements\n * into one array. This ignores all style attributes, including\n * \"transform\" which by its absense really makes this function useful\n * for treating all elements on an individual bases, and not a reliable\n * reflection of where the element will end up, globally speaking.\n * @param {Element|ChildNode} el an element\n * @returns {(Element|ChildNode)[]} a flat list of all elements\n */\nconst flattenDomTree = (el) => (\n\tel.childNodes == null || !el.childNodes.length\n\t\t? [el]\n\t\t: Array.from(el.childNodes).flatMap(child => flattenDomTree(child))\n);\n\nconst nodeSpecificAttrs = {\n\tsvg: [\"viewBox\", \"xmlns\", \"version\"],\n\tline: [\"x1\", \"y1\", \"x2\", \"y2\"],\n\trect: [\"x\", \"y\", \"width\", \"height\"],\n\tcircle: [\"cx\", \"cy\", \"r\"],\n\tellipse: [\"cx\", \"cy\", \"rx\", \"ry\"],\n\tpolygon: [\"points\"],\n\tpolyline: [\"points\"],\n\tpath: [\"d\"],\n};\n\nconst getAttributes = element => {\n\tconst attributeValue = element.attributes;\n\tif (attributeValue == null) { return []; }\n\tconst attributes = Array.from(attributeValue);\n\treturn nodeSpecificAttrs[element.nodeName]\n\t\t? attributes\n\t\t\t.filter(a => !nodeSpecificAttrs[element.nodeName].includes(a.name))\n\t\t: attributes;\n};\n\nconst objectifyAttributes = (list) => {\n\tconst obj = {};\n\tlist.forEach((a) => { obj[a.nodeName] = a.value; });\n\treturn obj;\n};\n\n/**\n * @param {object} parentAttrs the parent element's attribute object\n * @param {Element|ChildNode} element the current element\n */\nconst attrAssign = (parentAttrs, element) => {\n\tconst attrs = objectifyAttributes(getAttributes(element));\n\tif (!attrs.transform && !parentAttrs.transform) {\n\t\treturn { ...parentAttrs, ...attrs };\n\t}\n\tconst elemTransform = attrs.transform || \"\";\n\tconst parentTransform = parentAttrs.transform || \"\";\n\tconst elemMatrix = transformStringToMatrix(elemTransform);\n\tconst parentMatrix = transformStringToMatrix(parentTransform);\n\tconst matrix = svg_multiplyMatrices2(parentMatrix, elemMatrix);\n\tconst transform = `matrix(${matrix.join(\", \")})`;\n\treturn { ...parentAttrs, ...attrs, transform };\n};\n\n/**\n * @description Recurse through a DOM element and flatten all elements\n * into one array, where each element also has a style object which\n * contains a flat object of all attributes from the parents down\n * to the element itself, the closer to the element gets priority, and\n * the parent attributes will be overwritten, except in the case of\n * \"transform\", where the parent-child values are computed and merged.\n * @param {Element|ChildNode} element\n * @param {object} attributes key value pairs of attributes\n * @returns {{ element: Element|ChildNode, attributes: { [key: string]: string } }[]}\n * a flat array of objects containing the element and an object describing\n * the attributes.\n */\nconst flattenDomTreeWithStyle = (element, attributes = {}) => (\n\telement.childNodes == null || !element.childNodes.length\n\t\t? [{ element, attributes }]\n\t\t: Array.from(element.childNodes)\n\t\t\t.flatMap(child => flattenDomTreeWithStyle(child, attrAssign(attributes, child)))\n);\n\nexport { addClass, findElementTypeInParents, flattenDomTree, flattenDomTreeWithStyle, getRootParent, xmlStringToElement };\n"
  },
  {
    "path": "src/svg/general/index.js",
    "content": "/* SVG (c) Kraft */\nimport * as algebra from './algebra.js';\nimport * as dom from './dom.js';\nimport * as cdata from './cdata.js';\nimport * as path from './path.js';\nimport * as transforms from './transforms.js';\nimport * as viewBox from './viewBox.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst general = {\n\t...algebra,\n\t...dom,\n\t...cdata,\n\t...path,\n\t...transforms,\n\t...viewBox,\n};\n\nexport { general as default };\n"
  },
  {
    "path": "src/svg/general/path.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst markerRegEx = /[MmLlSsQqLlHhVvCcSsQqTtAaZz]/g;\nconst digitRegEx = /-?[0-9]*\\.?\\d+/g;\n\n/**\n * @description Path names in English\n */\nconst pathCommandNames = {\n\tm: \"move\",\n\tl: \"line\",\n\tv: \"vertical\",\n\th: \"horizontal\",\n\ta: \"ellipse\",\n\tc: \"curve\",\n\ts: \"smoothCurve\",\n\tq: \"quadCurve\",\n\tt: \"smoothQuadCurve\",\n\tz: \"close\",\n};\n\n// make capitalized copies of each command\nObject.keys(pathCommandNames).forEach((key) => {\n\tconst s = pathCommandNames[key];\n\tpathCommandNames[key.toUpperCase()] = s.charAt(0).toUpperCase() + s.slice(1);\n});\n\n/**\n * if the command is relative, it will build upon offset coordinate\n */\nconst add2path = (a, b) => [a[0] + (b[0] || 0), a[1] + (b[1] || 0)];\nconst getEndpoint = (command, values, offset = [0, 0]) => {\n\tconst upper = command.toUpperCase();\n\tlet origin = command === upper ? [0, 0] : offset;\n\t// H and V (uppercase) absolutely position themselves along their\n\t// horiz or vert axis, but they should carry over the other component\n\tif (command === \"V\") { origin = [offset[0], 0]; }\n\tif (command === \"H\") { origin = [0, offset[1]]; }\n\tswitch (upper) {\n\tcase \"V\": return add2path(origin, [0, values[0]]);\n\tcase \"H\": return add2path(origin, [values[0], 0]);\n\tcase \"M\":\n\tcase \"L\":\n\tcase \"T\": return add2path(origin, values);\n\tcase \"A\": return add2path(origin, [values[5], values[6]]);\n\tcase \"C\": return add2path(origin, [values[4], values[5]]);\n\tcase \"S\":\n\tcase \"Q\": return add2path(origin, [values[2], values[3]]);\n\tcase \"Z\": return undefined; // cannot be set locally.\n\tdefault: return origin;\n\t}\n};\n\n/**\n * @description Parse a path \"d\" attribute into an array of objects,\n * where each object contains a command, and the numerical values.\n * @param {string} d the \"d\" attribute of a path.\n */\nconst parsePathCommands = (d) => {\n\tconst results = [];\n\tlet match = markerRegEx.exec(d);\n\twhile (match !== null) {\n\t\tresults.push(match);\n\t\tmatch = markerRegEx.exec(d);\n\t}\n\treturn results\n\t\t.map((result, i, arr) => ({\n\t\t\tcommand: result[0],\n\t\t\tstart: result.index,\n\t\t\tend: i === arr.length - 1\n\t\t\t\t? d.length - 1\n\t\t\t\t: arr[(i + 1) % arr.length].index - 1,\n\t\t}))\n\t\t.map(({ command, start, end }) => {\n\t\t\tconst valueString = d.substring(start + 1, end + 1);\n\t\t\tconst strings = valueString.match(digitRegEx);\n\t\t\tconst values = strings ? strings.map(parseFloat) : [];\n\t\t\treturn { command, values };\n\t\t});\n};\n\nconst parsePathCommandsWithEndpoints = (d) => {\n\tlet pen = [0, 0];\n\tconst parsedCommands = parsePathCommands(d);\n\tconst commands = parsedCommands\n\t\t.map(obj => ({ ...obj, end: undefined, start: undefined }));\n\tif (!commands.length) { return commands; }\n\tcommands.forEach((command, i) => {\n\t\tcommands[i].end = getEndpoint(command.command, command.values, pen);\n\t\tcommands[i].start = i === 0 ? pen : commands[i - 1].end;\n\t\tpen = commands[i].end;\n\t});\n\tconst last = commands[commands.length - 1];\n\tconst firstDrawCommand = commands\n\t\t.filter(el => el.command.toUpperCase() !== \"M\"\n\t\t\t&& el.command.toUpperCase() !== \"Z\")\n\t\t.shift();\n\tif (last.command.toUpperCase() === \"Z\") {\n\t\tlast.end = [...firstDrawCommand.start];\n\t}\n\treturn commands;\n};\n\nexport { parsePathCommands, parsePathCommandsWithEndpoints, pathCommandNames };\n"
  },
  {
    "path": "src/svg/general/string.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @description make a unique identifier string\n * @returns {string} a unique identifier\n */\nconst makeUUID = () => Math.random()\n\t.toString(36)\n\t.replace(/[^a-z]+/g, \"\")\n\t.concat(\"aaaaa\")\n\t.substring(0, 5);\n\n/**\n * @description Convert a string into camel case from kebab or snake case.\n * @param {string} s a string in kebab or snake case\n * @returns {string} a string in camel case\n */\nconst toCamel = (s) => s\n\t.replace(/([-_][a-z])/ig, $1 => $1\n\t\t.toUpperCase()\n\t\t.replace(\"-\", \"\")\n\t\t.replace(\"_\", \"\"));\n\n/**\n * @description Convert a string into kebab case from camel case.\n * Snake case does not work in this method.\n * @param {string} s a string in camel case\n * @returns {string} a string in kebab case\n */\nconst toKebab = (s) => s\n\t.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n\t.replace(/([A-Z])([A-Z])(?=[a-z])/g, \"$1-$2\")\n\t.toLowerCase();\n\n/**\n * @description Capitalize the first letter of a string.\n * @param {string} s a string\n * @returns {string} a copy of the string, capitalized.\n */\nconst capitalized = (s) => s\n\t.charAt(0).toUpperCase() + s.slice(1);\n\nexport { capitalized, makeUUID, toCamel, toKebab };\n"
  },
  {
    "path": "src/svg/general/transforms.js",
    "content": "/* SVG (c) Kraft */\nimport { svg_multiplyMatrices2 } from './algebra.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n/** SVG transforms are in DEGREES ! */\n\n/**\n * @description parse the value of a SVG transform attribute\n * @param {string} transform a CSS/SVG transform, for example\n * \"translate(20 30) rotate(30) skewY(10)\"\n * @returns {{ transform: string, parameters: number[] }[]} array of objects\n * representing method calls where the \"transform\" is the transform name and\n * the \"parameters\" is a list of floats.\n */\nconst parseTransform = (transform) => {\n\tconst parsed = transform.match(/(\\w+\\((\\-?\\d+\\.?\\d*e?\\-?\\d*,?\\s*)+\\))+/g);\n\tif (!parsed) { return []; }\n\tconst listForm = parsed.map(a => a.match(/[\\w\\.\\-]+/g));\n\treturn listForm.map(a => ({\n\t\ttransform: a.shift(),\n\t\tparameters: a.map(p => parseFloat(p)),\n\t}));\n};\n\n/**\n * @description convert the arguments of each SVG affine transform type\n * into matrix form.\n */\nconst matrixFormTranslate = function (params) {\n\tswitch (params.length) {\n\tcase 1: return [1, 0, 0, 1, params[0], 0];\n\tcase 2: return [1, 0, 0, 1, params[0], params[1]];\n\tdefault: console.warn(`improper translate, ${params}`);\n\t}\n\treturn undefined;\n};\n\nconst matrixFormRotate = function (params) {\n\tconst cos_p = Math.cos(params[0] / (180 * Math.PI));\n\tconst sin_p = Math.sin(params[0] / (180 * Math.PI));\n\tswitch (params.length) {\n\tcase 1: return [cos_p, sin_p, -sin_p, cos_p, 0, 0];\n\tcase 3: return [cos_p, sin_p, -sin_p, cos_p,\n\t\t-params[1] * cos_p + params[2] * sin_p + params[1],\n\t\t-params[1] * sin_p - params[2] * cos_p + params[2]];\n\tdefault: console.warn(`improper rotate, ${params}`);\n\t}\n\treturn undefined;\n};\n\nconst matrixFormScale = function (params) {\n\tswitch (params.length) {\n\tcase 1: return [params[0], 0, 0, params[0], 0, 0];\n\tcase 2: return [params[0], 0, 0, params[1], 0, 0];\n\tdefault: console.warn(`improper scale, ${params}`);\n\t}\n\treturn undefined;\n};\n\nconst matrixFormSkewX = function (params) {\n\treturn [1, 0, Math.tan(params[0] / (180 * Math.PI)), 1, 0, 0];\n};\n\nconst matrixFormSkewY = function (params) {\n\treturn [1, Math.tan(params[0] / (180 * Math.PI)), 0, 1, 0, 0];\n};\n\nconst matrixForm = function (transformType, params) {\n\tswitch (transformType) {\n\tcase \"translate\": return matrixFormTranslate(params);\n\tcase \"rotate\": return matrixFormRotate(params);\n\tcase \"scale\": return matrixFormScale(params);\n\tcase \"skewX\": return matrixFormSkewX(params);\n\tcase \"skewY\": return matrixFormSkewY(params);\n\tcase \"matrix\": return params;\n\tdefault: console.warn(`unknown transform type ${transformType}`);\n\t}\n\treturn undefined;\n};\n\nconst transformStringToMatrix = function (string) {\n\treturn parseTransform(string)\n\t\t.map(el => matrixForm(el.transform, el.parameters))\n\t\t.filter(a => a !== undefined)\n\t\t.reduce((a, b) => svg_multiplyMatrices2(a, b), [1, 0, 0, 1, 0, 0]);\n};\n\nexport { parseTransform, transformStringToMatrix };\n"
  },
  {
    "path": "src/svg/general/viewBox.js",
    "content": "/* SVG (c) Kraft */\nimport makeViewBox from '../arguments/makeViewBox.js';\nimport { str_string, str_viewBox } from '../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst setViewBox = (element, ...args) => {\n\t// are they giving us pre-formatted string, or a list of numbers\n\tconst viewBox = args.length === 1 && typeof args[0] === str_string\n\t\t? args[0]\n\t\t: makeViewBox(...args);\n\tif (viewBox) {\n\t\telement.setAttribute(str_viewBox, viewBox);\n\t}\n\treturn element;\n};\n\nconst getViewBox = function (element) {\n\tconst vb = element.getAttribute(str_viewBox);\n\treturn (vb == null\n\t\t? undefined\n\t\t: vb.split(\" \").map(n => parseFloat(n)));\n};\n\nconst convertToViewBox = function (svg, x, y) {\n\tconst pt = svg.createSVGPoint();\n\tpt.x = x;\n\tpt.y = y;\n\t// todo: i thought this threw an error once. something about getScreenCTM.\n\tconst svgPoint = pt.matrixTransform(svg.getScreenCTM().inverse());\n\treturn [svgPoint.x, svgPoint.y];\n};\n\nconst foldToViewBox = ({ vertices_coords }) => {\n\tif (!vertices_coords) { return undefined; }\n\tconst min = [Infinity, Infinity];\n\tconst max = [-Infinity, -Infinity];\n\tvertices_coords.forEach(coord => [0, 1].forEach(i => {\n\t\tmin[i] = Math.min(coord[i], min[i]);\n\t\tmax[i] = Math.max(coord[i], max[i]);\n\t}));\n\treturn [min[0], min[1], max[0] - min[0], max[1] - min[1]].join(\" \");\n};\n\n/*\nexport const translateViewBox = function (svg, dx, dy) {\n\tconst viewBox = getViewBox(svg);\n\tif (viewBox == null) {\n\t\tsetDefaultViewBox(svg);\n\t}\n\tviewBox[0] += dx;\n\tviewBox[1] += dy;\n\tsvg.setAttributeNS(null, vB, viewBox.join(\" \"));\n};\n\nexport const scaleViewBox = function (svg, scale, origin_x = 0, origin_y = 0) {\n\tif (Math.abs(scale) < 1e-8) { scale = 0.01; }\n\tconst matrix = svg.createSVGMatrix()\n\t\t.translate(origin_x, origin_y)\n\t\t.scale(1 / scale)\n\t\t.translate(-origin_x, -origin_y);\n\tconst viewBox = getViewBox(svg);\n\tif (viewBox == null) {\n\t\tsetDefaultViewBox(svg);\n\t}\n\tconst top_left = svg.createSVGPoint();\n\tconst bot_right = svg.createSVGPoint();\n\t[top_left.x, top_left.y] = viewBox;\n\tbot_right.x = viewBox[0] + viewBox[2];\n\tbot_right.y = viewBox[1] + viewBox[3];\n\tconst new_top_left = top_left.matrixTransform(matrix);\n\tconst new_bot_right = bot_right.matrixTransform(matrix);\n\tsetViewBox(svg,\n\t\tnew_top_left.x,\n\t\tnew_top_left.y,\n\t\tnew_bot_right.x - new_top_left.x,\n\t\tnew_bot_right.y - new_top_left.y);\n};\n\n*/\n\nexport { convertToViewBox, foldToViewBox, getViewBox, setViewBox };\n"
  },
  {
    "path": "src/svg/index.js",
    "content": "/* SVG (c) Kraft */\nimport { setWindow } from './environment/window.js';\nimport NS from './spec/namespace.js';\nimport nodes_attributes from './spec/nodes_attributes.js';\nimport nodes_children from './spec/nodes_children.js';\nimport colors from './colors/index.js';\nimport general from './general/index.js';\nimport extensions from './constructor/extensions/index.js';\nimport { constructors, svg } from './constructor/elements.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst library = {\n\tNS,\n\tnodes_attributes,\n\tnodes_children,\n\textensions,\n\t...colors,\n\t...general,\n\t...constructors,\n\twindow: undefined, // set below\n};\n\n// the top level container object is also an <svg> constructor\nconst SVG = Object.assign(svg, library);\n\n// the window object, from which the document is used to createElement.\n// if using a browser, no need to interact with this,\n// if using node.js, set this to the library @xmldom/xmldom.\nObject.defineProperty(SVG, \"window\", {\n\tenumerable: false,\n\tset: setWindow,\n});\n\nexport { SVG as default };\n"
  },
  {
    "path": "src/svg/spec/classes_attributes.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst classes_attributes = {\n\tpresentation: [\n\t\t\"color\",\n\t\t\"color-interpolation\",\n\t\t\"cursor\", // mouse cursor\n\t\t\"direction\", // rtl right to left\n\t\t\"display\", // none, inherit\n\t\t\"fill\",\n\t\t\"fill-opacity\",\n\t\t\"fill-rule\",\n\t\t\"font-family\",\n\t\t\"font-size\",\n\t\t\"font-size-adjust\",\n\t\t\"font-stretch\",\n\t\t\"font-style\",\n\t\t\"font-variant\",\n\t\t\"font-weight\",\n\t\t\"image-rendering\", // provides a hint to the browser about how to make speed vs. quality tradeoffs as it performs image processing\n\t\t\"letter-spacing\",\n\t\t\"opacity\",\n\t\t\"overflow\",\n\t\t\"paint-order\",\n\t\t\"pointer-events\",\n\t\t\"preserveAspectRatio\",\n\t\t\"shape-rendering\",\n\t\t\"stroke\",\n\t\t\"stroke-dasharray\",\n\t\t\"stroke-dashoffset\",\n\t\t\"stroke-linecap\",\n\t\t\"stroke-linejoin\",\n\t\t\"stroke-miterlimit\",\n\t\t\"stroke-opacity\",\n\t\t\"stroke-width\",\n\t\t\"tabindex\",\n\t\t\"transform-origin\", // added by Robby\n\t\t\"user-select\", // added by Robby\n\t\t\"vector-effect\",\n\t\t\"visibility\",\n\t],\n\tanimation: [\n\t\t\"accumulate\", // controls whether or not an animation is cumulative\n\t\t\"additive\", // controls whether or not an animation is additive\n\t\t\"attributeName\", // used by: <animate>, <animateColor>, <animateTransform>, and <set>\n\t\t\"begin\",\n\t\t\"by\",\n\t\t\"calcMode\",\n\t\t\"dur\",\n\t\t\"end\",\n\t\t\"from\",\n\t\t\"keyPoints\", // used by: <animate>, <animateColor>, <animateMotion>, <animateTransform>, and <set>\n\t\t\"keySplines\",\n\t\t\"keyTimes\",\n\t\t\"max\",\n\t\t\"min\",\n\t\t\"repeatCount\",\n\t\t\"repeatDur\",\n\t\t\"restart\",\n\t\t\"to\", // final value of the attribute that will be modified during the animation\n\t\t\"values\",\n\t],\n\teffects: [\n\t\t\"azimuth\", // only used by: <feDistantLight>\n\t\t\"baseFrequency\",\n\t\t\"bias\",\n\t\t\"color-interpolation-filters\",\n\t\t\"diffuseConstant\",\n\t\t\"divisor\",\n\t\t\"edgeMode\",\n\t\t\"elevation\",\n\t\t\"exponent\",\n\t\t\"filter\",\n\t\t\"filterRes\",\n\t\t\"filterUnits\",\n\t\t\"flood-color\",\n\t\t\"flood-opacity\",\n\t\t\"in\", // identifies input for the given filter primitive.\n\t\t\"in2\", // identifies the second input for the given filter primitive.\n\t\t\"intercept\", // defines the intercept of the linear function of color component transfers\n\t\t\"k1\", // only used by: <feComposite>\n\t\t\"k2\", // only used by: <feComposite>\n\t\t\"k3\", // only used by: <feComposite>\n\t\t\"k4\", // only used by: <feComposite>\n\t\t\"kernelMatrix\", // only used by: <feConvolveMatrix>\n\t\t\"lighting-color\",\n\t\t\"limitingConeAngle\",\n\t\t\"mode\",\n\t\t\"numOctaves\",\n\t\t\"operator\",\n\t\t\"order\",\n\t\t\"pointsAtX\",\n\t\t\"pointsAtY\",\n\t\t\"pointsAtZ\",\n\t\t\"preserveAlpha\",\n\t\t\"primitiveUnits\",\n\t\t\"radius\",\n\t\t\"result\",\n\t\t\"seed\",\n\t\t\"specularConstant\",\n\t\t\"specularExponent\",\n\t\t\"stdDeviation\",\n\t\t\"stitchTiles\",\n\t\t\"surfaceScale\",\n\t\t\"targetX\", // only used in: <feConvolveMatrix>\n\t\t\"targetY\", // only used in: <feConvolveMatrix>\n\t\t\"type\", // many different uses, in animate, and <style> <script>\n\t\t\"xChannelSelector\", // <feDisplacementMap>\n\t\t\"yChannelSelector\",\n\t],\n\ttext: [\n\t\t// \"x\",   // <text>\n\t\t// \"y\",   // <text>\n\t\t\"dx\", // <text>\n\t\t\"dy\", // <text>\n\t\t\"alignment-baseline\", // specifies how a text alignts vertically\n\t\t\"baseline-shift\",\n\t\t\"dominant-baseline\",\n\t\t\"lengthAdjust\", // <text>\n\t\t\"method\", // for <textPath> only\n\t\t\"overline-position\",\n\t\t\"overline-thickness\",\n\t\t\"rotate\", // rotates each individual glyph\n\t\t\"spacing\",\n\t\t\"startOffset\", // <textPath>\n\t\t\"strikethrough-position\",\n\t\t\"strikethrough-thickness\",\n\t\t\"text-anchor\",\n\t\t\"text-decoration\",\n\t\t\"text-rendering\",\n\t\t\"textLength\", // <text>\n\t\t\"underline-position\",\n\t\t\"underline-thickness\",\n\t\t\"word-spacing\",\n\t\t\"writing-mode\",\n\t],\n\tgradient: [\n\t\t\"gradientTransform\", // linear/radial gradient\n\t\t\"gradientUnits\", // linear/radial gradient\n\t\t\"spreadMethod\", // linear/radial gradient\n\t],\n};\n\n// const unused = [\n// \t// specific to various elements\n// \t\"marker-end\", // assign to <marker>\n// \t\"marker-mid\", // assign to <marker>\n// \t\"marker-start\", // assign to <marker>\n// \t\"media\", // for <style> only. @media\n// \t\"target\", // only used in: <a>\n// \t\"href\", // anmations, effect images, gradients\n// \t\"requiredExtensions\", // used in conjunction with <switch>\n// \t\"systemLanguage\", // used in conjunction with <switch>\n// ];\n\nexport { classes_attributes as default };\n"
  },
  {
    "path": "src/svg/spec/classes_nodes.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\nconst classes_nodes = {\n\tsvg: [\n\t\t\"svg\",\n\t],\n\tdefs: [\n\t\t\"defs\", // can only be inside svg\n\t],\n\t// anywhere, usually top level SVG, or <defs>\n\theader: [\n\t\t\"desc\",\n\t\t\"filter\",\n\t\t\"metadata\",\n\t\t\"style\",\n\t\t\"script\",\n\t\t\"title\",\n\t\t\"view\",\n\t],\n\tcdata: [\n\t\t\"cdata\",\n\t],\n\tgroup: [\n\t\t\"g\",\n\t],\n\tvisible: [\n\t\t\"circle\",\n\t\t\"ellipse\",\n\t\t\"line\",\n\t\t\"path\",\n\t\t\"polygon\",\n\t\t\"polyline\",\n\t\t\"rect\",\n\n\t\t// extensions to the SVG spec\n\t\t\"arc\",\n\t\t\"arrow\",\n\t\t\"curve\",\n\t\t\"parabola\",\n\t\t\"roundRect\",\n\t\t\"wedge\",\n\t\t\"origami\",\n\t],\n\ttext: [\n\t\t\"text\",\n\t],\n\t// can contain drawings\n\t// anywhere, usually top level SVG, or <defs>\n\tinvisible: [\n\t\t\"marker\",\n\t\t\"symbol\",\n\t\t\"clipPath\",\n\t\t\"mask\",\n\t],\n\t// inside <defs>\n\tpatterns: [\n\t\t\"linearGradient\",\n\t\t\"radialGradient\",\n\t\t\"pattern\",\n\t],\n\tchildrenOfText: [\n\t\t\"textPath\",\n\t\t\"tspan\",\n\t],\n\tgradients: [ // children of gradients (<linearGradient> <radialGrandient>)\n\t\t\"stop\",\n\t],\n\tfilter: [ // children of filter\n\t\t\"feBlend\",\n\t\t\"feColorMatrix\",\n\t\t\"feComponentTransfer\",\n\t\t\"feComposite\",\n\t\t\"feConvolveMatrix\",\n\t\t\"feDiffuseLighting\",\n\t\t\"feDisplacementMap\",\n\t\t\"feDistantLight\",\n\t\t\"feDropShadow\",\n\t\t\"feFlood\",\n\t\t\"feFuncA\",\n\t\t\"feFuncB\",\n\t\t\"feFuncG\",\n\t\t\"feFuncR\",\n\t\t\"feGaussianBlur\",\n\t\t\"feImage\",\n\t\t\"feMerge\",\n\t\t\"feMergeNode\",\n\t\t\"feMorphology\",\n\t\t\"feOffset\",\n\t\t\"fePointLight\",\n\t\t\"feSpecularLighting\",\n\t\t\"feSpotLight\",\n\t\t\"feTile\",\n\t\t\"feTurbulence\",\n\t],\n};\n\nexport { classes_nodes as default };\n"
  },
  {
    "path": "src/svg/spec/namespace.js",
    "content": "/* SVG (c) Kraft */\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * @description The XML namespace for SVG. The value of the SVG attribute xmlns.\n * @constant {string}\n * @default\n */\nconst NS = \"http://www.w3.org/2000/svg\";\n\nexport { NS as default };\n"
  },
  {
    "path": "src/svg/spec/nodes.js",
    "content": "/* SVG (c) Kraft */\nimport classes_nodes from './classes_nodes.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\nconst nodeNames = Object.values(classes_nodes).flat();\n\nexport { classes_nodes as default, nodeNames };\n"
  },
  {
    "path": "src/svg/spec/nodes_attributes.js",
    "content": "/* SVG (c) Kraft */\nimport classes_attributes from './classes_attributes.js';\nimport classes_nodes from './classes_nodes.js';\nimport { str_viewBox, str_points, str_id, str_svg } from '../environment/strings.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n/**\n * @description This is the base object containing mostly the unique\n * attributes associated with an individual SVG element type.\n * After initialization, this object will be modified to contain all\n * nodes in the spec, and each node with a list of all possible attributes.\n * @constant {object}\n * @note the order of these indices matter. the rest which will be added,\n * not important.\n */\nconst nodes_attributes = {\n\tsvg: [str_viewBox],\n\tline: [\"x1\", \"y1\", \"x2\", \"y2\"],\n\trect: [\"x\", \"y\", \"width\", \"height\"],\n\tcircle: [\"cx\", \"cy\", \"r\"],\n\tellipse: [\"cx\", \"cy\", \"rx\", \"ry\"],\n\tpolygon: [str_points],\n\tpolyline: [str_points],\n\tpath: [\"d\"],\n\ttext: [\"x\", \"y\"],\n\tmask: [str_id],\n\tsymbol: [str_id],\n\tclipPath: [str_id, \"clip-rule\"],\n\tmarker: [\n\t\tstr_id,\n\t\t\"markerHeight\",\n\t\t\"markerUnits\",\n\t\t\"markerWidth\",\n\t\t\"orient\",\n\t\t\"refX\",\n\t\t\"refY\",\n\t],\n\tlinearGradient: [\"x1\", \"x2\", \"y1\", \"y2\"],\n\tradialGradient: [\"cx\", \"cy\", \"r\", \"fr\", \"fx\", \"fy\"],\n\tstop: [\"offset\", \"stop-color\", \"stop-opacity\"],\n\tpattern: [\"patternContentUnits\", \"patternTransform\", \"patternUnits\"],\n};\n\n// Fill out the keys of nodes_attributes, make sure it includes one\n// key for every SVG element type in the SVG spec.\n// nodes\n// \t.filter(nodeName => !nodes_attributes[nodeName])\n// \t.forEach(nodeName => { nodes_attributes[nodeName] = []; });\n\n// Fill out the values in nodes_attributes, these are values like\n// \"stroke\" or \"fill\" which end up getting repeatedly applied to many\n// elements. This way the library can be shipped in a smaller state and\n// build upon initialization.\nconst additionalNodeAttributes = [{\n\tnodes: [str_svg, \"defs\", \"g\"].concat(classes_nodes.visible, classes_nodes.text),\n\tattr: classes_attributes.presentation,\n}, {\n\tnodes: [\"filter\"],\n\tattr: classes_attributes.effects,\n}, {\n\t// todo: should we include \"svg\" here?\n\tnodes: classes_nodes.childrenOfText.concat(\"text\"),\n\tattr: classes_attributes.text,\n}, {\n\tnodes: classes_nodes.filter,\n\tattr: classes_attributes.effects,\n}, {\n\tnodes: classes_nodes.gradients,\n\tattr: classes_attributes.gradient,\n}];\n\n// for every node in the set, add all attributes associated with the node.\nadditionalNodeAttributes\n\t.forEach(el => el.nodes\n\t\t.forEach(nodeName => {\n\t\t\tif (!nodes_attributes[nodeName]) { nodes_attributes[nodeName] = []; }\n\t\t\tnodes_attributes[nodeName].push(...el.attr);\n\t\t}));\n\nexport { nodes_attributes as default };\n"
  },
  {
    "path": "src/svg/spec/nodes_children.js",
    "content": "/* SVG (c) Kraft */\nimport classes_nodes from './classes_nodes.js';\n\n/**\n * Rabbit Ear (c) Kraft\n */\n\n// const customPrimitives = [];\n\nconst headerStuff = [\n\tclasses_nodes.header,\n\tclasses_nodes.invisible,\n\tclasses_nodes.patterns,\n].flat();\n\nconst drawingShapes = [\n\tclasses_nodes.group,\n\tclasses_nodes.visible,\n\tclasses_nodes.text,\n\t// customPrimitives,\n].flat();\n\nconst nodes_children = {\n\t// svg\n\tsvg: [[\"svg\", \"defs\"], headerStuff, drawingShapes].flat(),\n\n\t// defs\n\tdefs: headerStuff,\n\n\t// header\n\t// desc: [],\n\tfilter: classes_nodes.filter,\n\t// metadata: [],\n\t// style: [],\n\t// script: [],\n\t// title: [],\n\t// view: [],\n\n\t// cdata\n\t// cdata: [],\n\n\t// group\n\tg: drawingShapes,\n\n\t// visible drawing primitives\n\t// circle: [],\n\t// ellipse: [],\n\t// line: [],\n\t// path: [],\n\t// polygon: [],\n\t// polyline: [],\n\t// rect: [],\n\n\t// text\n\ttext: classes_nodes.childrenOfText,\n\n\t// invisible. can contain drawing primitives\n\tmarker: drawingShapes,\n\tsymbol: drawingShapes,\n\tclipPath: drawingShapes,\n\tmask: drawingShapes,\n\n\t// patterns and gradients\n\tlinearGradient: classes_nodes.gradients,\n\tradialGradient: classes_nodes.gradients,\n\t// pattern: [],\n\n\t// can be a child of text\n\t// textPath: [],\n\t// tspan: [],\n\n\t// can be a child of gradients\n\t// stop: [],\n\n\t// can be a child of filter\n\t// feBlend: [],\n\t// feColorMatrix: [],\n\t// feComponentTransfer: [],\n\t// feComposite: [],\n\t// feConvolveMatrix: [],\n\t// feDiffuseLighting: [],\n\t// feDisplacementMap: [],\n\t// feDistantLight: [],\n\t// feDropShadow: [],\n\t// feFlood: [],\n\t// feFuncA: [],\n\t// feFuncB: [],\n\t// feFuncG: [],\n\t// feFuncR: [],\n\t// feGaussianBlur: [],\n\t// feImage: [],\n\t// feMerge: [],\n\t// feMergeNode: [],\n\t// feMorphology: [],\n\t// feOffset: [],\n\t// fePointLight: [],\n\t// feSpecularLighting: [],\n\t// feSpotLight: [],\n\t// feTile: [],\n\t// feTurbulence: [],\n};\n\nexport { nodes_children as default };\n"
  },
  {
    "path": "src/text/axioms.json",
    "content": "{\n\t\"ar\": [null,\n\t\t\"اصنع خطاً يمر بنقطتين\",\n\t\t\"اصنع خطاً عن طريق طي نقطة واحدة إلى أخرى\",\n\t\t\"اصنع خطاً عن طريق طي خط واحد على آخر\",\n\t\t\"اصنع خطاً يمر عبر نقطة واحدة ويجعل خطاً واحداً فوق نفسه\",\n\t\t\"اصنع خطاً يمر بالنقطة الأولى ويجعل النقطة الثانية على الخط\",\n\t\t\"اصنع خطاً يجلب النقطة الأولى إلى الخط الأول والنقطة الثانية إلى الخط الثاني\",\n\t\t\"اصنع خطاً يجلب نقطة إلى خط ويجعل خط ثاني فوق نفسه\"\n\t],\n\t\"de\": [null,\n\t\t\"Falte eine Linie durch zwei Punkte\",\n\t\t\"Falte zwei Punkte aufeinander\",\n\t\t\"Falte zwei Linien aufeinander\",\n\t\t\"Falte eine Linie auf sich selbst, falte dabei durch einen Punkt\",\n\t\t\"Falte einen Punkt auf eine Linie, falte dabei durch einen anderen Punkt\",\n\t\t\"Falte einen Punkt auf eine Linie und einen weiteren Punkt auf eine weitere Linie\",\n\t\t\"Falte einen Punkt auf eine Linie und eine weitere Linie in sich selbst zusammen\"\n\t],\n\t\"en\": [null,\n\t\t\"fold a line through two points\",\n\t\t\"fold two points together\",\n\t\t\"fold two lines together\",\n\t\t\"fold a line on top of itself, creasing through a point\",\n\t\t\"fold a point to a line, creasing through another point\",\n\t\t\"fold a point to a line and another point to another line\",\n\t\t\"fold a point to a line and another line onto itself\"\n\t],\n\t\"es\": [null,\n\t\t\"dobla una línea entre dos puntos\",\n\t\t\"dobla dos puntos juntos\",\n\t\t\"dobla y une dos líneas\",\n\t\t\"dobla una línea sobre sí misma, doblándola hacia un punto\",\n\t\t\"dobla un punto hasta una línea, doblándola a través de otro punto\",\n\t\t\"dobla un punto hacia una línea y otro punto hacia otra línea\",\n\t\t\"dobla un punto hacia una línea y otra línea sobre sí misma\"\n\t],\n\t\"fr\":[null,\n\t\t\"créez un pli passant par deux points\",\n\t\t\"pliez pour superposer deux points\",\n\t\t\"pliez pour superposer deux lignes\",\n\t\t\"rabattez une ligne sur elle-même à l'aide d'un pli qui passe par un point\",\n\t\t\"rabattez un point sur une ligne à l'aide d'un pli qui passe par un autre point\",\n\t\t\"rabattez un point sur une ligne et un autre point sur une autre ligne\",\n\t\t\"rabattez un point sur une ligne et une autre ligne sur elle-même\"\n\t],\n\t\"hi\": [null,\n\t\t\"एक क्रीज़ बनाएँ जो दो स्थानों से गुजरता है\",\n\t\t\"एक स्थान को दूसरे स्थान पर मोड़कर एक क्रीज़ बनाएँ\",\n\t\t\"एक रेखा पर दूसरी रेखा को मोड़कर क्रीज़ बनाएँ\",\n\t\t\"एक क्रीज़ बनाएँ जो एक स्थान से गुजरता है और एक रेखा को स्वयं के ऊपर ले आता है\",\n\t\t\"एक क्रीज़ बनाएँ जो पहले स्थान से गुजरता है और दूसरे स्थान को रेखा पर ले आता है\",\n\t\t\"एक क्रीज़ बनाएँ जो पहले स्थान को पहली रेखा पर और दूसरे स्थान को दूसरी रेखा पर ले आता है\",\n\t\t\"एक क्रीज़ बनाएँ जो एक स्थान को एक रेखा पर ले आता है और दूसरी रेखा को स्वयं के ऊपर ले आता है\"\n\t],\n\t\"jp\": [null,\n\t\t\"2点に沿って折り目を付けます\",\n\t\t\"2点を合わせて折ります\",\n\t\t\"2つの線を合わせて折ります\",\n\t\t\"点を通過させ、既にある線に沿って折ります\",\n\t\t\"点を線沿いに合わせ別の点を通過させ折ります\",\n\t\t\"線に向かって点を折り、同時にもう一方の線に向かってもう一方の点を折ります\",\n\t\t\"線に向かって点を折り、同時に別の線をその上に折ります\"\n\t],\n\t\"ko\": [null,\n\t\t\"두 점을 통과하는 선으로 접으세요\",\n\t\t\"두 점을 함께 접으세요\",\n\t\t\"두 선을 함께 접으세요\",\n\t\t\"그 위에 선을 접으면서 점을 통과하게 접으세요\",\n\t\t\"점을 선으로 접으면서, 다른 점을 지나게 접으세요\",\n\t\t\"점을 선으로 접고 다른 점을 다른 선으로 접으세요\",\n\t\t\"점을 선으로 접고 다른 선을 그 위에 접으세요\"\n\t],\n\t\"ms\": [null,\n\t\t\"lipat garisan melalui dua titik\",\n\t\t\"lipat dua titik bersama\",\n\t\t\"lipat dua garisan bersama\",\n\t\t\"lipat satu garisan di atasnya sendiri, melipat melalui satu titik\",\n\t\t\"lipat satu titik ke garisan, melipat melalui titik lain\",\n\t\t\"lipat satu titik ke garisan dan satu lagi titik ke garisan lain\",\n\t\t\"lipat satu titik ke garisan dan satu lagi garisan di atasnya sendiri\"\n\t],\n\t\"pt\": [null,\n\t\t\"dobre uma linha entre dois pontos\",\n\t\t\"dobre os dois pontos para uni-los\",\n\t\t\"dobre as duas linhas para uni-las\",\n\t\t\"dobre uma linha sobre si mesma, criando uma dobra ao longo de um ponto\",\n\t\t\"dobre um ponto até uma linha, criando uma dobra ao longo de outro ponto\",\n\t\t\"dobre um ponto até uma linha e outro ponto até outra linha\",\n\t\t\"dobre um ponto até uma linha e outra linha sobre si mesma\"\n\t],\n\t\"ru\": [null,\n\t\t\"сложите линию через две точки\",\n\t\t\"сложите две точки вместе\",\n\t\t\"сложите две линии вместе\",\n\t\t\"сверните линию сверху себя, сгибая через точку\",\n\t\t\"сложите точку в линию, сгибая через другую точку\",\n\t\t\"сложите точку в линию и другую точку в другую линию\",\n\t\t\"сложите точку в линию и другую линию на себя\"\n\t],\n\t\"tr\": [null,\n\t\t\"iki noktadan geçen bir çizgi boyunca katla\",\n\t\t\"iki noktayı birbirine katla\",\n\t\t\"iki çizgiyi birbirine katla\",\n\t\t\"bir noktadan kıvırarak kendi üzerindeki bir çizgi boyunca katla\",\n\t\t\"başka bir noktadan kıvırarak bir noktayı bir çizgiye katla\",\n\t\t\"bir noktayı bir çizgiye ve başka bir noktayı başka bir çizgiye katla\",\n\t\t\"bir noktayı bir çizgiye ve başka bir çizgiyi kendi üzerine katla\"\n\t],\n\t\"vi\": [null,\n\t\t\"tạo một nếp gấp đi qua hai điểm\",\n\t\t\"tạo nếp gấp bằng cách gấp một điểm này sang điểm khác\",\n\t\t\"tạo nếp gấp bằng cách gấp một đường lên một đường khác\",\n\t\t\"tạo một nếp gấp đi qua một điểm và đưa một đường lên trên chính nó\",\n\t\t\"tạo một nếp gấp đi qua điểm đầu tiên và đưa điểm thứ hai lên đường thẳng\",\n\t\t\"tạo một nếp gấp mang điểm đầu tiên đến đường đầu tiên và điểm thứ hai cho đường thứ hai\",\n\t\t\"tạo một nếp gấp mang lại một điểm cho một đường và đưa một đường thứ hai lên trên chính nó\"\n\t],\n\t\"zh\": [null,\n\t\t\"通過兩點折一條線\",\n\t\t\"將兩點折疊起來\",\n\t\t\"將兩條線折疊在一起\",\n\t\t\"通過一個點折疊一條線在自身上面\",\n\t\t\"將一個點，通過另一個點折疊成一條線，\",\n\t\t\"將一個點折疊為一條線，再將另一個點折疊到另一條線\",\n\t\t\"將一個點折疊成一條線，另一條線折疊到它自身上\"\n\t]\n}\n"
  },
  {
    "path": "src/text/edge_assignments.json",
    "content": "{\n\t\"en\": {\n\t\t\"B\": \"boundary\",\n\t\t\"M\": \"mountain\",\n\t\t\"V\": \"valley\",\n\t\t\"F\": \"flat\",\n\t\t\"J\": \"join\",\n\t\t\"C\": \"cut\",\n\t\t\"U\": \"unassigned\",\n\t}\n}"
  },
  {
    "path": "src/text/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n// import axioms from \"./axioms.json\";\n// import instructions from \"./instructions.json\";\n/**\n * @description A multi-lingual raw-text reference containing a rudimentary\n * set of origami operations, the aim being to parameterize origami instructions\n * allowing us to be able to easily convert from one language to another.\n */\n// export default {\n// \taxioms,\n// \tinstructions,\n// };\n\nexport default {};\n"
  },
  {
    "path": "src/text/instructions.json",
    "content": "{\n\t\"fold\": {\n\t\t\"es\": \"doblez\"\n\t},\n\t\"valley fold\": {\n\t\t\"es\": \"doblez de valle\",\n\t\t\"zh\": \"谷摺\"\n\t},\n\t\"mountain fold\": {\n\t\t\"es\": \"doblez de montaña\",\n\t\t\"zh\": \"山摺\"\n\t},\n\t\"inside reverse fold\": {\n\t\t\"zh\": \"內中割摺\"\n\t},\n\t\"outside reverse fold\": {\n\t\t\"zh\": \"外中割摺\"\n\t},\n\t\"sink\": {},\n\t\"open sink\": {\n\t\t\"zh\": \"開放式沉壓摺\"\n\t},\n\t\"closed sink\": {\n\t\t\"zh\": \"封閉式沉壓摺\"\n\t},\n\t\"rabbit ear\": {\n\t\t\"zh\": \"兔耳摺\"\n\t},\n\t\"double rabbit ear\": {\n\t\t\"zh\": \"雙兔耳摺\"\n\t},\n\t\"petal fold\": {\n\t\t\"zh\": \"花瓣摺\"\n\t},\n\t\"blintz\": {\n\t\t\"zh\": \"坐墊基\"\n\t},\n\t\"squash\": {\n\t\t\"zh\": \"壓摺\"\n\t},\n\t\"flip over\": {\n\t\t\"es\": \"dale la vuelta a tu papel\"\n\t}\n}"
  },
  {
    "path": "src/types.js",
    "content": "\n/**\n * @typedef FOLDFrame\n * @type {{\n *   frame_author?: string,\n *   frame_title?: string,\n *   frame_description?: string,\n *   frame_classes?: string[],\n *   frame_attributes?: string[],\n *   frame_unit?: string,\n *   vertices_coords?: [number, number][]|[number, number, number][],\n *   vertices_vertices?: number[][],\n *   vertices_edges?: number[][],\n *   vertices_faces?: (number | null | undefined)[][],\n *   edges_vertices?: [number, number][],\n *   edges_faces?: (number | null | undefined)[][],\n *   edges_assignment?: string[],\n *   edges_foldAngle?: number[],\n *   edges_length?: number[],\n *   faces_vertices?: number[][],\n *   faces_edges?: number[][],\n *   faces_faces?: (number | null | undefined)[][],\n *   faceOrders?: [number, number, number][],\n *   edgeOrders?: [number, number, number][],\n * }}\n * @property {string} [frame_author] metadata\n * @property {string} [frame_title] metadata\n * @property {string} [frame_description] metadata\n * @property {string[]} [frame_classes] metadata\n * @property {string[]} [frame_attributes] metadata\n * @property {string} [frame_unit] metadata\n * @property {([number, number] | [number, number, number])[]} [vertices_coords]\n * xy or xyz coordinates of the vertices\n * @property {number[][]} [vertices_vertices] for each vertex all adjacent vertices\n * @property {number[][]} [vertices_edges] for each vertex all adjacent edges\n * @property {(number | null | undefined)[][]} [vertices_faces] for each vertex all adjacent faces\n * @property {[number, number][]} [edges_vertices] each edge connects two vertex indices\n * @property {(number | null | undefined)[][]} [edges_faces] for each edge all adjacent faces\n * @property {string[]} [edges_assignment] single-character fold assignment of each edge\n * @property {number[]} [edges_foldAngle] in degrees, the fold angle of each edge\n * @property {number[]} [edges_length] length of each edge\n * @property {number[][]} [faces_vertices] for each face, all adjacent vertices\n * @property {number[][]} [faces_edges] for each face, all adjacent edges\n * @property {(number | null | undefined)[][]} [faces_faces] for each face, all adjacent faces\n * @property {[number, number, number][]} [faceOrders] a series of layer\n * ordering rules between pairs of faces\n * @property {[number, number, number][]} [edgeOrders] a series of layer\n * ordering rules between pairs of edges\n */\n\n/**\n * @typedef FOLDInternalFrame\n * @type {FOLDFrame & {\n *   frame_parent?: number,\n *   frame_inherit?: boolean,\n * }}\n * @property {number} [frame_parent] metadata\n * @property {boolean} [frame_inherit] metadata\n */\n\n/**\n * @typedef FOLDFileMetadata\n * @type {{\n *   file_frames?: FOLDInternalFrame[],\n *   file_spec?: number,\n *   file_creator?: string,\n *   file_author?: string,\n *   file_title?: string,\n *   file_description?: string,\n *   file_classes?: string[],\n * }}\n * @property {FOLDInternalFrame[]} [file_frames] array of embedded FOLD frames,\n * good for representing a linear sequence like diagram steps for example.\n * @property {number} [file_spec] metadata\n * @property {string} [file_creator] metadata\n * @property {string} [file_author] metadata\n * @property {string} [file_title] metadata\n * @property {string} [file_description] metadata\n * @property {string[]} [file_classes] metadata\n */\n\n/**\n * @typedef FOLDOutOfSpec\n * @type {{\n *   faces_center?: ([number, number] | [number, number, number])[],\n *   faces_normal?: ([number, number] | [number, number, number])[],\n *   edges_vector?: ([number, number] | [number, number, number])[],\n *   faces_polygon?: ([number, number] | [number, number, number])[][],\n *   faces_matrix?: number[][],\n *   faces_layer?: number[],\n *   vertices_sectors?: number[][],\n * }}\n * @property {([number, number] | [number, number, number])[]} [faces_center]\n * @property {([number, number] | [number, number, number])[]} [faces_normal]\n * @property {([number, number] | [number, number, number])[]} [edges_vector]\n * @property {([number, number] | [number, number, number])[][]} [faces_polygon]\n * @property {number[][]} [faces_matrix]\n * @property {number[]} [faces_layer]\n * @property {number[][]} [vertices_sectors]\n */\n\n/**\n * @typedef FOLD\n * @type {FOLDFileMetadata & FOLDFrame & FOLDOutOfSpec}\n * @description A Javascript object encoding of a FOLD file\n * which adheres to the FOLD file format specification.\n * @example\n * {\n *   vertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n *   vertices_faces: [[0, 1, null], [0, null], [1, 0, null], [1, null]],\n *   edges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n *   edges_assignment: [\"B\", \"B\", \"B\", \"B\", \"V\"],\n *   edges_foldAngle: [0, 0, 0, 0, 180],\n *   faces_vertices: [[0, 1, 2], [0, 2, 3]],\n * }\n */\n\n/**\n * @typedef FOLDExtended\n * @type {FOLD & FOLDOutOfSpec}\n * @description An extension of the FOLD format spec that includes a few\n * additional fields that this library makes repeated use of.\n */\n\n// /**\n//  * @typedef Coord\n//  * @type {[number, number]|[number, number, number]}\n//  * @description a 2D or 3D point or vector\n//  */\n\n/**\n * @typedef VecLine2\n * @type {{ vector: [number, number], origin: [number, number] }}\n * @description a 2D line defined by a 2D vector and a 2D point along the line.\n * @property {[number, number]} vector\n * a vector describing the direction of the line\n * @property {[number, number]} origin\n * a point which the line passes through\n */\n\n/**\n * @typedef VecLine3\n * @type {{ vector: [number, number, number], origin: [number, number, number] }}\n * @description a 3D line defined by a 3D vector and a 3D point along the line.\n * @property {[number, number, number]} vector\n * a vector describing the direction of the line\n * @property {[number, number, number]} origin\n * a point which the line passes through\n */\n\n/**\n * @typedef VecLine\n * @type {{\n *   vector: [number, number]|[number, number, number],\n *   origin: [number, number]|[number, number, number],\n * }}\n * @description a line defined by a vector and a point along the line,\n * capable of representing a line in any dimension.\n * @property {[number, number]|[number, number, number]} vector\n * a vector describing the direction of the line\n * @property {[number, number]|[number, number, number]} origin\n * a point which the line passes through\n */\n\n/**\n * @typedef UniqueLine\n * @type {{ normal: [number, number], distance: number }}\n * @description a 2D line defined by a unit normal vector and a value that\n * describes the shortest distance from the origin to a point on the line.\n * @property {[number, number]} normal - a unit vector that is normal to the line\n * @property {number} distance - the shortest distance\n * from the line to the origin\n */\n\n/**\n * @typedef Box\n * @type {{ min: number[], max: number[], span?: number[] }}\n * @description an axis-aligned bounding box, capable of representing\n * a bounding box with any number of dimensions.\n * @property {number[]} min - the point representing the absolute minimum\n * for all axes.\n * @property {number[]} max - the point representing the absolute maximum\n * for all axes.\n */\n\n/**\n * @typedef Circle\n * @type {{ radius: number, origin: [number, number] }}\n * @description a circle primitive in 2D\n * @property {number} radius - the radius of the circle\n * @property {[number, number]} origin - the center of the circle as an array of numbers\n */\n\n/**\n * @typedef SweepEvent\n * @type {{ vertices: number[], t: number, start: number[], end: number[] }}\n * @property {number} t - the scalar along the sweep axis which this event occurs\n * @property {number[]} vertices - a list of indices currently overlapping this region\n * @property {number[]} start - a list of indices beginning to overlap at this event\n * @property {number[]} end - a list of indices no longer overlapping after this event\n */\n\n/**\n * Intersection related events\n *\n * @typedef LineLineEvent\n * @type {{ a: number, b: number, point: [number, number] }}\n * @description used in line-line intersection and graph edge-line intersection\n *\n * @typedef FaceVertexEvent\n * @type {{ a: number, vertex: number }}\n * @description used in face-graph intersection, the vertex-related data\n *\n * @typedef FaceEdgeEvent\n * @type {{ a: number, b: number, point: [number, number], edge: number }}\n * @description used in face-graph intersection, the edge-related data\n *\n * @typedef FacePointEvent\n * @type {{ point: [number, number], overlap: boolean, t: number[] }}\n * @description used in face-graph intersection, the data that describes\n * any points that lie inside of faces\n */\n\n/**\n * @typedef WebGLVertexArray\n * @type {{\n *   location: number,\n *   buffer: WebGLBuffer,\n *   type: number,\n *   length: number,\n *   data: Float32Array,\n * }}\n *\n * @typedef WebGLElementArray\n * @type {{\n *   mode: number,\n *   buffer: WebGLBuffer,\n *   data: Uint16Array | Uint32Array,\n * }}\n *\n * @typedef WebGLUniform\n * @type {{\n *   func: string,\n *   value: any\n * }}\n *\n * @typedef WebGLModel\n * @type {{\n *   program: WebGLProgram,\n *   vertexArrays: WebGLVertexArray[],\n *   elementArrays: WebGLElementArray[],\n *   flags: number[],\n *   makeUniforms: (options: object) => ({ [key: string]: WebGLUniform }),\n * }}\n * @property {WebGLProgram} program\n * @property {WebGLVertexArray[]} vertexArrays\n * @property {WebGLElementArray[]} elementArrays\n * @property {number[]} flags these flags will be enabled at the beginning of\n * the model's rendering process via gl.enable, and then gl.disable at the end.\n * @property {Function} makeUniforms\n */\n\n/**\n * @typedef Arrow\n * @type {{\n *   segment: [[number, number], [number, number]],\n *   head?: { width: number, height: number },\n *   tail?: { width: number, height: number },\n *   bend?: number,\n *   padding?: number,\n * }}\n * @description An arrow intended for being rendered in SVG for\n * the use in diagrams in 2D.\n * @property {[[number, number], [number, number]]} segment the two 2D points \"from\" and \"to\"\n * @property {{ width: number, height: number }} [head] the size of the arrow head\n * @property {{ width: number, height: number }} [tail] the size of the arrow tail\n * @property {number} [bend] the magnitude of the bend of the path\n * @property {number} [padding] the little space to inset the endpoints\n */\n\n/**\n * @typedef TacoTacoConstraint\n * @type {[number, number, number, number]}\n * @description four face indices involved\n * in the taco-taco relationship, encoding this relationship:\n * 0 and 2 are connected (A and C) and 1 and 3 are connected (B and D)\n * (A,C) (B,D) (B,C) (A,D) (A,B) (C,D)\n *\n * @typedef TacoTortillaConstraint\n * @type {[number, number, number]}\n * @description three face indices involved\n * in the taco-tortilla relationship, encoding this relationship:\n * 0 and 2 are a connected taco, 1 is the tortilla face\n * (A,C) (A,B) (B,C)\n *\n * @typedef TortillaTortillaConstraint\n * @type {[number, number, number, number]}\n * @description four face indices involved\n * in the tortilla-tortilla relationship, encoding this relationship:\n * 0 and 1 are a connected tortilla, 2 and 3 are a connected tortilla.\n * where 0 is above/below 2, and 1 is above/below 3\n * (A,C) (B,D)\n *\n * @typedef TransitivityConstraint\n * @type {[number, number, number]}\n * @description three face indices encoding a transitivity constraint,\n * where the three faces involved are in sorted order.\n * (A,B) (B,C) (C,A)\n */\n\n/**\n * @typedef LayerBranch\n * @type {LayerFork[]}\n * @description To compile a solution, you must include\n * a selection from every Branch inside this LayerBranches array.\n *\n * @typedef LayerOrders\n * @type {{[key: string]: number}}\n * @description an object with (many) face-pair keys, and value numbers 1 or 2.\n *\n * @typedef LayerFork\n * @type {{ orders: LayerOrders, branches?: LayerBranch[] }}\n * @description To compile a solution, you must choose only one item\n * from this list. Each item is a copy of one another, but with\n * different values.\n *\n * @typedef LayerSolverSolution\n * @type {LayerFork}\n * @example In this example there are three \"branches\", one top-level,\n * and two more inside of this one each at a similar depth.\n * The top-level branch contains two all-branches (each happen to be\n * identical in structure), and each of these all-branches contain\n * two choice-branches.\n * {\n *   \"orders: LayerOrders,\n *   \"branches: [\n *     [\n *       {\n *         \"orders\": LayerOrders,\n *         \"branches\": [\n *           [\n *             { \"orders\": LayerOrders },\n *             { \"orders\": LayerOrders },\n *           ],\n *         ],\n *       },\n *       {\n *         \"orders\": LayerOrders\n *       },\n *     ],\n *     [\n *       {\n *         \"orders\": LayerOrders,\n *         \"branches\": [\n *           [\n *             { \"orders\": LayerOrders },\n *             { \"orders\": LayerOrders },\n *           ],\n *         ],\n *       },\n *       {\n *         \"orders\": LayerOrders\n *       },\n *     ],\n *   ],\n * }\n *\n */\n\n/**\n * @typedef FaceOrdersBranch\n * @type {FaceOrdersFork[]}\n * @description To compile a solution, you must include\n * a selection from every Branch inside this FaceOrdersBranchs array.\n *\n * @typedef FaceOrders\n * @type {[number, number, number][]}\n * @description an array of faceOrders. faces a and b: [a, b, solution]\n *\n * @typedef FaceOrdersFork\n * @type {{ orders: FaceOrders, branches?: FaceOrdersBranch[] }}\n * @description To compile a solution, you must choose only one item\n * from this list. Each item is a copy of one another, but with\n * different values.\n *\n * @typedef FaceOrdersSolverSolution\n * @type {FaceOrdersFork}\n * @example In this example there are three \"branches\", one top-level,\n * and two more inside of this one each at a similar depth.\n * The top-level branch contains two all-branches (each happen to be\n * identical in structure), and each of these all-branches contain\n * two choice-branches.\n */\n\n/**\n * @description ignore\n */\n// eslint-disable-next-line\nexport const __types__ = () => {};\n"
  },
  {
    "path": "src/webgl/creasePattern/arrays.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\ttriangulateConvexFacesVertices,\n} from \"../../graph/triangulate.js\";\nimport {\n\tresize2,\n} from \"../../math/vector.js\";\nimport {\n\tmakeCPEdgesVertexData,\n} from \"./data.js\";\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {object} program\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLVertexArray[]}\n */\nexport const makeCPEdgesVertexArrays = (gl, program, graph, options) => {\n\tif (!graph || !graph.vertices_coords || !graph.edges_vertices) {\n\t\treturn [];\n\t}\n\tconst {\n\t\tvertices_coords,\n\t\tvertices_color,\n\t\tverticesEdgesVector,\n\t\tvertices_vector,\n\t} = makeCPEdgesVertexData(graph, options);\n\n\t// todo: better\n\tif (!vertices_coords) { return []; }\n\treturn [{\n\t\tlocation: gl.getAttribLocation(program, \"v_position\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: 2,\n\t\tdata: new Float32Array(vertices_coords.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"v_color\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_color.length ? vertices_color[0].length : 2,\n\t\tdata: new Float32Array(vertices_color.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"edge_vector\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: verticesEdgesVector.length ? verticesEdgesVector[0].length : 2,\n\t\tdata: new Float32Array(verticesEdgesVector.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"vertex_vector\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_vector.length ? vertices_vector[0].length : 2,\n\t\tdata: new Float32Array(vertices_vector.flat()),\n\t}].filter(el => el.location !== -1);\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {number} version the WebGL version\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLElementArray[]}\n */\nexport const makeCPEdgesElementArrays = (gl, version = 1, graph = {}) => {\n\tif (!graph || !graph.edges_vertices) { return []; }\n\tconst edgesTriangles = graph.edges_vertices\n\t\t.map((_, i) => i * 4)\n\t\t.flatMap(i => [i + 0, i + 1, i + 2, i + 2, i + 3, i + 0]);\n\treturn [{\n\t\tmode: gl.TRIANGLES,\n\t\tbuffer: gl.createBuffer(),\n\t\tdata: version === 2\n\t\t\t? new Uint32Array(edgesTriangles)\n\t\t\t: new Uint16Array(edgesTriangles),\n\t}];\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {object} program\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLVertexArray[]}\n */\nexport const makeCPFacesVertexArrays = (gl, program, graph) => {\n\tif (!graph || !graph.vertices_coords) { return []; }\n\treturn [{\n\t\tlocation: gl.getAttribLocation(program, \"v_position\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: 2,\n\t\tdata: new Float32Array(graph.vertices_coords.flatMap(resize2)),\n\t}].filter(el => el.location !== -1);\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {number} version the WebGL version\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLElementArray[]}\n */\nexport const makeCPFacesElementArrays = (gl, version = 1, graph = {}) => {\n\tif (!graph || !graph.vertices_coords || !graph.faces_vertices) { return []; }\n\treturn [{\n\t\tmode: gl.TRIANGLES,\n\t\tbuffer: gl.createBuffer(),\n\t\tdata: version === 2\n\t\t\t? new Uint32Array(triangulateConvexFacesVertices(graph).flat())\n\t\t\t: new Uint16Array(triangulateConvexFacesVertices(graph).flat()),\n\t}];\n};\n"
  },
  {
    "path": "src/webgl/creasePattern/data.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeEdgesVector,\n} from \"../../graph/make/edges.js\";\nimport {\n\tlight,\n\tdark,\n} from \"../general/colors.js\";\n\nconst make2D = (coords) => coords\n\t.map(coord => [0, 1]\n\t\t.map(i => coord[i] || 0));\n\n/**\n * @returns {{\n *   vertices_coords: any,\n *   vertices_color: any,\n *   verticesEdgesVector: any,\n *   vertices_vector: any,\n * }}\n */\nexport const makeCPEdgesVertexData = (graph, options) => {\n\tif (!graph || !graph.vertices_coords || !graph.edges_vertices) { return undefined; }\n\tconst assignmentColors = options && options.dark ? dark : light;\n\tconst assignment_color = {\n\t\t...assignmentColors,\n\t\t...options,\n\t};\n\tconst vertices_coords = make2D(graph.edges_vertices\n\t\t.flatMap(edge => edge\n\t\t\t.map(v => graph.vertices_coords[v]))\n\t\t.flatMap(coord => [coord, coord]));\n\tconst edgesVector = make2D(makeEdgesVector(graph));\n\tconst vertices_color = graph.edges_assignment\n\t\t? graph.edges_assignment.flatMap(a => [\n\t\t\tassignment_color[a],\n\t\t\tassignment_color[a],\n\t\t\tassignment_color[a],\n\t\t\tassignment_color[a],\n\t\t])\n\t\t: graph.edges_vertices.flatMap(() => [\n\t\t\tassignment_color.U,\n\t\t\tassignment_color.U,\n\t\t\tassignment_color.U,\n\t\t\tassignment_color.U,\n\t\t]);\n\tconst verticesEdgesVector = edgesVector\n\t\t.flatMap(el => [el, el, el, el]);\n\tconst vertices_vector = graph.edges_vertices\n\t\t.flatMap(() => [[1, 0], [-1, 0], [-1, 0], [1, 0]]);\n\treturn {\n\t\tvertices_coords,\n\t\tvertices_color,\n\t\tverticesEdgesVector,\n\t\tvertices_vector,\n\t};\n};\n"
  },
  {
    "path": "src/webgl/creasePattern/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as arrays from \"./arrays.js\";\nimport * as data from \"./data.js\";\nimport * as models from \"./models.js\";\nimport * as shaders from \"./shaders.js\";\nimport * as uniforms from \"./uniforms.js\";\n\nexport default {\n\t...arrays,\n\t...data,\n\t...models,\n\t...shaders,\n\t...uniforms,\n};\n"
  },
  {
    "path": "src/webgl/creasePattern/models.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tcreateProgram,\n} from \"../general/webgl.js\";\nimport {\n\tmakeCPEdgesVertexArrays,\n\tmakeCPEdgesElementArrays,\n\tmakeCPFacesVertexArrays,\n\tmakeCPFacesElementArrays,\n} from \"./arrays.js\";\nimport {\n\tmakeUniforms,\n} from \"./uniforms.js\";\nimport {\n\tcp_100_vert,\n\tcp_100_frag,\n\tcp_300_vert,\n\tcp_300_frag,\n\tthick_edges_100_vert,\n\tthick_edges_300_vert,\n} from \"./shaders.js\";\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {FOLD} graph a FOLD object\n * @param {object} options\n * @returns {WebGLModel}\n */\nexport const cpFacesV1 = (gl, graph = {}, options = undefined) => {\n\tconst program = createProgram(gl, cp_100_vert, cp_100_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeCPFacesVertexArrays(gl, program, graph), // , options),\n\t\telementArrays: makeCPFacesElementArrays(gl, 1, graph),\n\t\tflags: [],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {FOLD} graph a FOLD object\n * @param {object} options\n * @returns {WebGLModel}\n */\nexport const cpEdgesV1 = (gl, graph = {}, options = undefined) => {\n\tconst program = createProgram(gl, thick_edges_100_vert, cp_100_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeCPEdgesVertexArrays(gl, program, graph, options),\n\t\telementArrays: makeCPEdgesElementArrays(gl, 1, graph),\n\t\tflags: [],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {FOLD} graph a FOLD object\n * @param {object} options\n * @returns {WebGLModel}\n */\nexport const cpFacesV2 = (gl, graph = {}, options = undefined) => {\n\tconst program = createProgram(gl, cp_300_vert, cp_300_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeCPFacesVertexArrays(gl, program, graph), // , options),\n\t\telementArrays: makeCPFacesElementArrays(gl, 2, graph),\n\t\tflags: [],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {FOLD} graph a FOLD object\n * @param {object} options\n * @returns {WebGLModel}\n */\nexport const cpEdgesV2 = (gl, graph = {}, options = undefined) => {\n\tconst program = createProgram(gl, thick_edges_300_vert, cp_300_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeCPEdgesVertexArrays(gl, program, graph, options),\n\t\telementArrays: makeCPEdgesElementArrays(gl, 2, graph),\n\t\tflags: [],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {number} version the version of the WebGL\n * @param {FOLD} graph a FOLD object\n * @param {object} options\n * @returns {WebGLModel[]}\n */\nexport const creasePattern = (gl, version = 1, graph = {}, options = undefined) => {\n\tswitch (version) {\n\tcase 1:\n\t\treturn [cpFacesV1(gl, graph, options), cpEdgesV1(gl, graph, options)];\n\tcase 2:\n\tdefault:\n\t\treturn [cpFacesV2(gl, graph, options), cpEdgesV2(gl, graph, options)];\n\t}\n};\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/cp-100.frag",
    "content": "#version 100\nprecision mediump float;\n\nvarying vec3 blend_color;\n\nvoid main () {\n\tgl_FragColor = vec4(blend_color.rgb, 1);\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/cp-100.vert",
    "content": "#version 100\n\nuniform mat4 u_matrix;\nuniform vec3 u_cpColor;\n\nattribute vec2 v_position;\nvarying vec3 blend_color;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 0, 1);\n\tblend_color = u_cpColor;\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/cp-300.frag",
    "content": "#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\n\nin vec3 blend_color;\nout vec4 outColor;\n\nvoid main() {\n\toutColor = vec4(blend_color.rgb, 1);\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/cp-300.vert",
    "content": "#version 300 es\n\nuniform mat4 u_matrix;\nuniform vec3 u_cpColor;\n\nin vec2 v_position;\nout vec3 blend_color;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 0, 1);\n\tblend_color = u_cpColor;\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/thick-edges-100.vert",
    "content": "#version 100\n\nuniform mat4 u_matrix;\nuniform float u_strokeWidth;\n\nattribute vec2 v_position;\nattribute vec3 v_color;\nattribute vec2 edge_vector;\nattribute vec2 vertex_vector;\nvarying vec3 blend_color;\n\nvoid main () {\n\tfloat sign = vertex_vector[0];\n\tfloat halfWidth = u_strokeWidth * 0.5;\n\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\n\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\n\tblend_color = v_color;\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders/thick-edges-300.vert",
    "content": "#version 300 es\n\nuniform mat4 u_matrix;\nuniform float u_strokeWidth;\n\nin vec2 v_position;\nin vec3 v_color;\nin vec2 edge_vector;\nin vec2 vertex_vector;\nout vec3 blend_color;\n\nvoid main () {\n\tfloat sign = vertex_vector[0];\n\tfloat halfWidth = u_strokeWidth * 0.5;\n\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\n\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\n\tblend_color = v_color;\n}\n"
  },
  {
    "path": "src/webgl/creasePattern/shaders.js",
    "content": "export const cp_300_frag = `#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\nin vec3 blend_color;\nout vec4 outColor;\nvoid main() {\n\toutColor = vec4(blend_color.rgb, 1);\n}\n`;\n\nexport const cp_100_frag = `#version 100\nprecision mediump float;\nvarying vec3 blend_color;\nvoid main () {\n\tgl_FragColor = vec4(blend_color.rgb, 1);\n}\n`;\n\nexport const thick_edges_300_vert = `#version 300 es\nuniform mat4 u_matrix;\nuniform float u_strokeWidth;\nin vec2 v_position;\nin vec3 v_color;\nin vec2 edge_vector;\nin vec2 vertex_vector;\nout vec3 blend_color;\nvoid main () {\n\tfloat sign = vertex_vector[0];\n\tfloat halfWidth = u_strokeWidth * 0.5;\n\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\n\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\n\tblend_color = v_color;\n}\n`;\n\nexport const thick_edges_100_vert = `#version 100\nuniform mat4 u_matrix;\nuniform float u_strokeWidth;\nattribute vec2 v_position;\nattribute vec3 v_color;\nattribute vec2 edge_vector;\nattribute vec2 vertex_vector;\nvarying vec3 blend_color;\nvoid main () {\n\tfloat sign = vertex_vector[0];\n\tfloat halfWidth = u_strokeWidth * 0.5;\n\tvec2 side = normalize(vec2(edge_vector.y * sign, -edge_vector.x * sign)) * halfWidth;\n\tgl_Position = u_matrix * vec4(side + v_position, 0, 1);\n\tblend_color = v_color;\n}\n`;\n\nexport const cp_100_vert = `#version 100\nuniform mat4 u_matrix;\nuniform vec3 u_cpColor;\nattribute vec2 v_position;\nvarying vec3 blend_color;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 0, 1);\n\tblend_color = u_cpColor;\n}\n`;\n\nexport const cp_300_vert = `#version 300 es\nuniform mat4 u_matrix;\nuniform vec3 u_cpColor;\nin vec2 v_position;\nout vec3 blend_color;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 0, 1);\n\tblend_color = u_cpColor;\n}\n`;\n"
  },
  {
    "path": "src/webgl/creasePattern/uniforms.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tidentity4x4,\n\tmultiplyMatrices4,\n} from \"../../math/matrix4.js\";\nimport {\n\tparseColorToWebGLColor,\n} from \"../general/colors.js\";\n\n/**\n * @description Uniforms must exist so there are protections to ensure\n * that at least some value gets passed.\n * @return {{ [key: string]: WebGLUniform }}\n */\nexport const makeUniforms = ({\n\tprojectionMatrix,\n\tmodelViewMatrix,\n\tcpColor,\n\tstrokeWidth,\n}) => ({\n\tu_matrix: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: multiplyMatrices4(\n\t\t\tprojectionMatrix || identity4x4,\n\t\t\tmodelViewMatrix || identity4x4,\n\t\t),\n\t},\n\tu_projection: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: projectionMatrix || identity4x4,\n\t},\n\tu_modelView: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: modelViewMatrix || identity4x4,\n\t},\n\tu_cpColor: {\n\t\tfunc: \"uniform3fv\",\n\t\tvalue: parseColorToWebGLColor(cpColor || \"white\"),\n\t},\n\tu_strokeWidth: {\n\t\tfunc: \"uniform1f\",\n\t\tvalue: strokeWidth || 0.05,\n\t},\n});\n"
  },
  {
    "path": "src/webgl/foldedForm/arrays.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tmakeFacesVertexData,\n\tmakeThickEdgesVertexData,\n} from \"./data.js\";\nimport {\n\tmakeFacesEdgesFromVertices,\n} from \"../../graph/make/facesEdges.js\";\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {object} program\n * @param {FOLDExtended} graph a FOLD object\n * @param {{ showTriangulation?: boolean }} options\n * @returns {WebGLVertexArray[]}\n */\nexport const makeFoldedVertexArrays = (gl, program, {\n\tvertices_coords, edges_vertices, edges_assignment,\n\tfaces_vertices, faces_edges, faces_normal,\n} = {}, options = {}) => {\n\tif (!vertices_coords || !faces_vertices) {\n\t\treturn [];\n\t}\n\tif (!faces_edges && edges_vertices && faces_vertices) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tfaces_edges = makeFacesEdgesFromVertices({ edges_vertices, faces_vertices });\n\t}\n\tconst {\n\t\tvertices_coords: vertices_coords3,\n\t\tvertices_normal,\n\t\tvertices_barycentric,\n\t} = makeFacesVertexData({\n\t\tvertices_coords, edges_assignment, faces_vertices, faces_edges, faces_normal,\n\t}, options);\n\treturn [{\n\t\tlocation: gl.getAttribLocation(program, \"v_position\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_coords3.length ? vertices_coords3[0].length : 3,\n\t\tdata: new Float32Array(vertices_coords3.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"v_normal\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_normal.length ? vertices_normal[0].length : 3,\n\t\tdata: new Float32Array(vertices_normal.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"v_barycentric\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: 3,\n\t\tdata: new Float32Array(vertices_barycentric.flat()),\n\t},\n\t\t// { location: gl.getAttribLocation(program, \"v_rawEdge\"),\n\t\t// \tbuffer: gl.createBuffer(),\n\t\t// \ttype: gl.FLOAT,\n\t\t// \t// type: gl.INT,\n\t\t// \t// type: gl.UNSIGNED_BYTE,\n\t\t// \tlength: 1,\n\t\t// \tdata: new Float32Array(rawEdges.flat()) },\n\t].filter(el => el.location !== -1);\n};\n\n/**\n * WebGL 2 can handle Uint32Array. WebGL 1 cannot and must use 16 bit.\n */\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {number} version the WebGL version\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLElementArray[]}\n */\nexport const makeFoldedElementArrays = (gl, version = 1, graph = {}) => {\n\tif (!graph || !graph.vertices_coords || !graph.faces_vertices) { return []; }\n\treturn [{\n\t\tmode: gl.TRIANGLES,\n\t\tbuffer: gl.createBuffer(),\n\t\tdata: version === 2\n\t\t\t? new Uint32Array(graph.faces_vertices.flat())\n\t\t\t: new Uint16Array(graph.faces_vertices.flat()),\n\t}];\n};\n\n// thick edges\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {object} program\n * @param {FOLD} graph a FOLD object\n * @param {{ assignment_color?: any }} options\n * @returns {WebGLVertexArray[]}\n */\nexport const makeThickEdgesVertexArrays = (gl, program, graph, options = {}) => {\n\tif (!graph || !graph.vertices_coords || !graph.edges_vertices) {\n\t\treturn [];\n\t}\n\tconst {\n\t\tvertices_coords,\n\t\tvertices_color,\n\t\tverticesEdgesVector,\n\t\tvertices_vector,\n\t} = makeThickEdgesVertexData(graph, options.assignment_color);\n\n\t// todo: better\n\tif (!vertices_coords) { return []; }\n\n\treturn [{\n\t\tlocation: gl.getAttribLocation(program, \"v_position\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_coords.length ? vertices_coords[0].length : 3,\n\t\tdata: new Float32Array(vertices_coords.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"v_color\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_color.length ? vertices_color[0].length : 3,\n\t\tdata: new Float32Array(vertices_color.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"edge_vector\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: verticesEdgesVector.length ? verticesEdgesVector[0].length : 3,\n\t\tdata: new Float32Array(verticesEdgesVector.flat()),\n\t}, {\n\t\tlocation: gl.getAttribLocation(program, \"vertex_vector\"),\n\t\tbuffer: gl.createBuffer(),\n\t\ttype: gl.FLOAT,\n\t\tlength: vertices_vector.length ? vertices_vector[0].length : 3,\n\t\tdata: new Float32Array(vertices_vector.flat()),\n\t}].filter(el => el.location !== -1);\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL context\n * @param {number} version the WebGL version\n * @param {FOLD} graph a FOLD object\n * @returns {WebGLElementArray[]}\n */\nexport const makeThickEdgesElementArrays = (gl, version = 1, graph = {}) => {\n\tif (!graph || !graph.edges_vertices) { return []; }\n\tconst edgesTriangles = graph.edges_vertices\n\t\t.map((_, i) => i * 8)\n\t\t.flatMap(i => [\n\t\t\ti + 0, i + 1, i + 4,\n\t\t\ti + 4, i + 1, i + 5,\n\t\t\ti + 1, i + 2, i + 5,\n\t\t\ti + 5, i + 2, i + 6,\n\t\t\ti + 2, i + 3, i + 6,\n\t\t\ti + 6, i + 3, i + 7,\n\t\t\ti + 3, i + 0, i + 7,\n\t\t\ti + 7, i + 0, i + 4,\n\t\t]);\n\treturn [{\n\t\tmode: gl.TRIANGLES,\n\t\tbuffer: gl.createBuffer(),\n\t\tdata: version === 2\n\t\t\t? new Uint32Array(edgesTriangles)\n\t\t\t: new Uint16Array(edgesTriangles),\n\t}];\n};\n"
  },
  {
    "path": "src/webgl/foldedForm/data.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n// import { resize } from \"../../math/vector.js\";\nimport { makeVerticesNormal } from \"../../graph/normals.js\";\nimport { makeEdgesVector } from \"../../graph/make/edges.js\";\nimport { light, dark } from \"../general/colors.js\";\nimport { resize3 } from \"../../math/vector.js\";\n\nconst getFaceEdgeIsJoined = ({\n\tedges_assignment, faces_vertices, faces_edges,\n}) => {\n\tif (faces_edges && edges_assignment) {\n\t\treturn faces_edges\n\t\t\t.map(edges => edges\n\t\t\t\t.map(e => edges_assignment[e])\n\t\t\t\t.map(a => a === \"J\" || a === \"j\"));\n\t}\n\treturn faces_vertices\n\t\t? faces_vertices.map(arr => arr.map(() => false))\n\t\t: [];\n};\n\n/**\n * @param {FOLDExtended} graph a FOLD object\n * @param {{ showTriangulation?: boolean }} options\n * @returns {{\n *   vertices_coords: [number, number][]|[number, number, number][],\n *   vertices_normal: number[][],\n *   vertices_barycentric: [number, number, number][],\n * }}\n */\nexport const makeFacesVertexData = ({\n\tvertices_coords, edges_assignment, faces_vertices, faces_edges, faces_normal,\n}, options = {}) => {\n\tconst vertices_coords3 = vertices_coords\n\t\t.map(coord => [...coord].concat(Array(3 - coord.length).fill(0)))\n\t\t.map(resize3);\n\tconst vertices_normal = makeVerticesNormal({\n\t\tvertices_coords: vertices_coords3, faces_vertices, faces_normal,\n\t});\n\t/** @type {[number, number, number][]} */\n\tconst vertices_barycentric = vertices_coords3\n\t\t.map((_, i) => i % 3)\n\t\t.map(n => [n === 0 ? 1 : 0, n === 1 ? 1 : 0, n === 2 ? 1 : 0]);\n\t// const rawEdges = faces_rawEdge.flatMap(n => [n, n, n]);\n\tconst facesEdgesIsJoined = getFaceEdgeIsJoined({\n\t\tedges_assignment, faces_vertices, faces_edges,\n\t});\n\tif (!options.showTriangulation) {\n\t\tfor (let i = 0; i < facesEdgesIsJoined.length; i += 1) {\n\t\t\tif (facesEdgesIsJoined[i][0]) {\n\t\t\t\tvertices_barycentric[i * 3 + 0][2] = vertices_barycentric[i * 3 + 1][2] = 100;\n\t\t\t}\n\t\t\tif (facesEdgesIsJoined[i][1]) {\n\t\t\t\tvertices_barycentric[i * 3 + 1][0] = vertices_barycentric[i * 3 + 2][0] = 100;\n\t\t\t}\n\t\t\tif (facesEdgesIsJoined[i][2]) {\n\t\t\t\tvertices_barycentric[i * 3 + 0][1] = vertices_barycentric[i * 3 + 2][1] = 100;\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tvertices_coords: vertices_coords3,\n\t\tvertices_normal,\n\t\tvertices_barycentric,\n\t};\n};\n\n/**\n * @param {FOLD} graph a FOLD object\n * @param {{ assignment_color?: any, dark: boolean }} options\n * @returns {{\n *   vertices_coords: any,\n *   vertices_color: any,\n *   verticesEdgesVector: any,\n *   vertices_vector: any,\n * } | undefined}\n */\nexport const makeThickEdgesVertexData = (graph, options) => {\n\tif (!graph || !graph.vertices_coords || !graph.edges_vertices) { return undefined; }\n\tconst assignmentColors = options && options.dark ? dark : light;\n\tconst assignment_color = {\n\t\t...assignmentColors,\n\t\t...options,\n\t};\n\tconst vertices_coords3D = graph.vertices_coords\n\t\t.map(coord => [...coord].concat(Array(3 - coord.length).fill(0)));\n\tconst vertices_coords = graph.edges_vertices\n\t\t.flatMap(edge => edge\n\t\t\t.map(v => vertices_coords3D[v]))\n\t\t.flatMap(coord => [coord, coord, coord, coord]);\n\tconst edgesVector = makeEdgesVector(graph);\n\tconst vertices_color = graph.edges_assignment\n\t\t? graph.edges_assignment\n\t\t\t.flatMap(a => Array(8).fill(assignment_color[a]))\n\t\t: graph.edges_vertices\n\t\t\t.flatMap(() => Array(8).fill(assignment_color.U));\n\tconst verticesEdgesVector = edgesVector\n\t\t.flatMap(el => [el, el, el, el, el, el, el, el]);\n\tconst vertices_vector = graph.edges_vertices\n\t\t.flatMap(() => [\n\t\t\t[1, 0],\n\t\t\t[0, 1],\n\t\t\t[-1, 0],\n\t\t\t[0, -1],\n\n\t\t\t[1, 0],\n\t\t\t[0, 1],\n\t\t\t[-1, 0],\n\t\t\t[0, -1],\n\t\t]);\n\treturn {\n\t\tvertices_coords,\n\t\tvertices_color,\n\t\tverticesEdgesVector,\n\t\tvertices_vector,\n\t};\n};\n"
  },
  {
    "path": "src/webgl/foldedForm/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as arrays from \"./arrays.js\";\nimport * as data from \"./data.js\";\nimport * as models from \"./models.js\";\nimport * as shaders from \"./shaders.js\";\nimport * as uniforms from \"./uniforms.js\";\n\nexport default {\n\t...arrays,\n\t...data,\n\t...models,\n\t...shaders,\n\t...uniforms,\n};\n"
  },
  {
    "path": "src/webgl/foldedForm/models.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tprepareForRendering,\n} from \"../../graph/rendering.js\";\nimport {\n\tcreateProgram,\n} from \"../general/webgl.js\";\nimport {\n\tmakeFoldedVertexArrays,\n\tmakeFoldedElementArrays,\n\tmakeThickEdgesVertexArrays,\n\tmakeThickEdgesElementArrays,\n} from \"./arrays.js\";\nimport {\n\tmakeUniforms,\n} from \"./uniforms.js\";\nimport {\n\tmodel_100_vert,\n\tmodel_100_frag,\n\tmodel_300_vert,\n\tmodel_300_frag,\n\toutlined_model_100_vert,\n\toutlined_model_100_frag,\n\toutlined_model_300_vert,\n\toutlined_model_300_frag,\n\tthick_edges_100_vert,\n\tthick_edges_300_vert,\n\tsimple_100_frag,\n\tsimple_300_frag,\n} from \"./shaders.js\";\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {number} version the version of WebGL (1 or 2)\n * @param {FOLD} graph a FOLD object\n * @param {{ earcut?: Function, layerNudge?: number, showTriangulation?: boolean }} options\n * @returns {WebGLModel}\n */\nexport const foldedFormFaces = (gl, version = 1, graph = {}, options = {}) => {\n\tconst exploded = prepareForRendering(graph, options);\n\tconst program = version === 1\n\t\t? createProgram(gl, model_100_vert, model_100_frag)\n\t\t: createProgram(gl, model_300_vert, model_300_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeFoldedVertexArrays(gl, program, exploded, options),\n\t\telementArrays: makeFoldedElementArrays(gl, version, exploded), // , options),\n\t\tflags: [gl.DEPTH_TEST],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {number} version the version of WebGL (1 or 2)\n * @param {FOLD} graph a FOLD object\n * @param {{ earcut?: Function, layerNudge?: number, showTriangulation?: boolean }} options\n * @returns {WebGLModel}\n */\nexport const foldedFormFacesOutlined = (gl, version = 1, graph = {}, options = {}) => {\n\tconst exploded = prepareForRendering(graph, options);\n\tconst program = version === 1\n\t\t? createProgram(gl, outlined_model_100_vert, outlined_model_100_frag)\n\t\t: createProgram(gl, outlined_model_300_vert, outlined_model_300_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeFoldedVertexArrays(gl, program, exploded, options),\n\t\telementArrays: makeFoldedElementArrays(gl, version, exploded), // , options),\n\t\tflags: [gl.DEPTH_TEST],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {number} version the version of WebGL (1 or 2)\n * @param {FOLD} graph a FOLD object\n * @param {{ assignment_color?: any }} options\n * @returns {WebGLModel}\n */\nexport const foldedFormEdges = (gl, version = 1, graph = {}, options = {}) => {\n\tconst program = version === 1\n\t\t? createProgram(gl, thick_edges_100_vert, simple_100_frag)\n\t\t: createProgram(gl, thick_edges_300_vert, simple_300_frag);\n\treturn {\n\t\tprogram,\n\t\tvertexArrays: makeThickEdgesVertexArrays(gl, program, graph, options),\n\t\telementArrays: makeThickEdgesElementArrays(gl, version, graph),\n\t\tflags: [gl.DEPTH_TEST],\n\t\tmakeUniforms,\n\t};\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {number} version the version of the WebGL\n * @param {FOLD} graph a FOLD object\n * @param {{\n *   edges?: boolean,\n *   faces?: boolean,\n *   outlines?: boolean,\n *   earcut?: Function,\n *   layerNudge?: number,\n *   showTriangulation?: boolean,\n *   assignment_color?: any,\n * }} options\n * @returns {WebGLModel[]}\n */\nexport const foldedForm = (gl, version = 1, graph = {}, options = {}) => {\n\tconst programs = [];\n\t// either Faces, or FacesOutlined\n\tif (options.faces !== false) {\n\t\tif (options.outlines === false) {\n\t\t\tprograms.push(foldedFormFaces(gl, version, graph, options));\n\t\t} else {\n\t\t\tprograms.push(foldedFormFacesOutlined(gl, version, graph, options));\n\t\t}\n\t}\n\t// if edges option is on, also add thick edges\n\tif (options.edges === true) {\n\t\tprograms.push(foldedFormEdges(gl, version, graph, options));\n\t}\n\treturn programs;\n};\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/model-100.frag",
    "content": "#version 100\nprecision mediump float;\n\nuniform float u_opacity;\nvarying vec3 front_color;\nvarying vec3 back_color;\n\nvoid main () {\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\tgl_FragColor = vec4(color, u_opacity);\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/model-100.vert",
    "content": "#version 100\n\nattribute vec3 v_position;\nattribute vec3 v_normal;\n\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nvarying vec3 normal_color;\nvarying vec3 front_color;\nvarying vec3 back_color;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/model-300.frag",
    "content": "#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\n\nuniform float u_opacity;\nin vec3 front_color;\nin vec3 back_color;\nout vec4 outColor;\n\nvoid main () {\n\tgl_FragDepth = gl_FragCoord.z;\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\toutColor = vec4(color, u_opacity);\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/model-300.vert",
    "content": "#version 300 es\n\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\n\nin vec3 v_position;\nin vec3 v_normal;\nout vec3 front_color;\nout vec3 back_color;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/outlined-model-100.frag",
    "content": "#version 100\nprecision mediump float;\n\nuniform float u_opacity;\nvarying vec3 barycentric;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvarying vec3 outline_color;\n\nvoid main () {\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\t// vec3 boundary = vec3(0.0, 0.0, 0.0);\n\tvec3 boundary = outline_color;\n\t// gl_FragDepth = 0.5;\n\tgl_FragColor = any(lessThan(barycentric, vec3(0.02)))\n\t\t? vec4(boundary, u_opacity)\n\t\t: vec4(color, u_opacity);\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/outlined-model-100.vert",
    "content": "#version 100\n\nattribute vec3 v_position;\nattribute vec3 v_normal;\nattribute vec3 v_barycentric;\n\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nuniform vec3 u_outlineColor;\nvarying vec3 normal_color;\nvarying vec3 barycentric;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvarying vec3 outline_color;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tbarycentric = v_barycentric;\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n\toutline_color = u_outlineColor;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/outlined-model-300.frag",
    "content": "#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\n\nuniform float u_opacity;\n\nin vec3 front_color;\nin vec3 back_color;\nin vec3 outline_color;\nin vec3 barycentric;\nout vec4 outColor;\n\nfloat edgeFactor(vec3 barycenter) {\n\tvec3 d = fwidth(barycenter);\n\tvec3 a3 = smoothstep(vec3(0.0), d*3.5, barycenter);\n\treturn min(min(a3.x, a3.y), a3.z);\n}\n\nvoid main () {\n\tgl_FragDepth = gl_FragCoord.z;\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\t// vec4 color4 = gl_FrontFacing\n\t// \t? vec4(front_color, u_opacity)\n\t// \t: vec4(back_color, u_opacity);\n\t// vec4 outline4 = vec4(outline_color, 1);\n\t// outColor = vec4(mix(vec3(0.0), color, edgeFactor(barycentric)), u_opacity);\n\toutColor = vec4(mix(outline_color, color, edgeFactor(barycentric)), u_opacity);\n\t// outColor = mix(outline4, color4, edgeFactor(barycentric));\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/outlined-model-300.vert",
    "content": "#version 300 es\n\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nuniform vec3 u_outlineColor;\n\nin vec3 v_position;\nin vec3 v_normal;\nin vec3 v_barycentric;\nin float v_rawEdge;\n\nout vec3 front_color;\nout vec3 back_color;\nout vec3 outline_color;\nout vec3 barycentric;\n// flat out int rawEdge;\nflat out int provokedVertex;\n\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tprovokedVertex = gl_VertexID;\n\tbarycentric = v_barycentric;\n\t// rawEdge = int(v_rawEdge);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n\toutline_color = u_outlineColor;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/simple-100.frag",
    "content": "#version 100\nprecision mediump float;\n\nvarying vec3 blend_color;\n\nvoid main () {\n\tgl_FragColor = vec4(blend_color.rgb, 1);\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/simple-300.frag",
    "content": "#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\n\nin vec3 blend_color;\nout vec4 outColor;\n \nvoid main() {\n\toutColor = vec4(blend_color.rgb, 1);\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/thick-edges-100.vert",
    "content": "#version 100\n\nattribute vec3 v_position;\nattribute vec3 v_color;\nattribute vec3 edge_vector;\nattribute vec2 vertex_vector;\n\nuniform mat4 u_matrix;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform float u_strokeWidth;\nvarying vec3 blend_color;\n\nvoid main () {\n\tvec3 edge_norm = normalize(edge_vector);\n\t// axis most dissimilar to edge_vector\n\tvec3 absNorm = abs(edge_norm);\n\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\n\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\n\t// two perpendiculars. with edge_vector these make basis vectors\n\tvec3 one = cross(axis, edge_norm);\n\tvec3 two = cross(one, edge_norm);\n\tvec3 displaceNormal = normalize(\n\t\tone * vertex_vector.x + two * vertex_vector.y\n\t);\n\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\n\tgl_Position = u_matrix * vec4(v_position + displace, 1);\n\tblend_color = v_color;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders/thick-edges-300.vert",
    "content": "#version 300 es\n\nuniform mat4 u_matrix;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform float u_strokeWidth;\n\nin vec3 v_position;\nin vec3 v_color;\nin vec3 edge_vector;\nin vec2 vertex_vector;\nout vec3 blend_color;\n\nvoid main () {\n\tvec3 edge_norm = normalize(edge_vector);\n\t// axis most dissimilar to edge_vector\n\tvec3 absNorm = abs(edge_norm);\n\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\n\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\n\t// two perpendiculars. with edge_vector these make basis vectors\n\tvec3 one = cross(axis, edge_norm);\n\tvec3 two = cross(one, edge_norm);\n\tvec3 displaceNormal = normalize(\n\t\tone * vertex_vector.x + two * vertex_vector.y\n\t);\n\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\n\tgl_Position = u_matrix * vec4(v_position + displace, 1);\n\tblend_color = v_color;\n}\n"
  },
  {
    "path": "src/webgl/foldedForm/shaders.js",
    "content": "export const model_300_vert = `#version 300 es\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nin vec3 v_position;\nin vec3 v_normal;\nout vec3 front_color;\nout vec3 back_color;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n}\n`;\n\nexport const thick_edges_300_vert = `#version 300 es\nuniform mat4 u_matrix;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform float u_strokeWidth;\nin vec3 v_position;\nin vec3 v_color;\nin vec3 edge_vector;\nin vec2 vertex_vector;\nout vec3 blend_color;\nvoid main () {\n\tvec3 edge_norm = normalize(edge_vector);\n\t// axis most dissimilar to edge_vector\n\tvec3 absNorm = abs(edge_norm);\n\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\n\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\n\t// two perpendiculars. with edge_vector these make basis vectors\n\tvec3 one = cross(axis, edge_norm);\n\tvec3 two = cross(one, edge_norm);\n\tvec3 displaceNormal = normalize(\n\t\tone * vertex_vector.x + two * vertex_vector.y\n\t);\n\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\n\tgl_Position = u_matrix * vec4(v_position + displace, 1);\n\tblend_color = v_color;\n}\n`;\n\nexport const outlined_model_300_frag = `#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\nuniform float u_opacity;\nin vec3 front_color;\nin vec3 back_color;\nin vec3 outline_color;\nin vec3 barycentric;\nout vec4 outColor;\nfloat edgeFactor(vec3 barycenter) {\n\tvec3 d = fwidth(barycenter);\n\tvec3 a3 = smoothstep(vec3(0.0), d*3.5, barycenter);\n\treturn min(min(a3.x, a3.y), a3.z);\n}\nvoid main () {\n\tgl_FragDepth = gl_FragCoord.z;\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\t// vec4 color4 = gl_FrontFacing\n\t// \t? vec4(front_color, u_opacity)\n\t// \t: vec4(back_color, u_opacity);\n\t// vec4 outline4 = vec4(outline_color, 1);\n\t// outColor = vec4(mix(vec3(0.0), color, edgeFactor(barycentric)), u_opacity);\n\toutColor = vec4(mix(outline_color, color, edgeFactor(barycentric)), u_opacity);\n\t// outColor = mix(outline4, color4, edgeFactor(barycentric));\n}\n`;\n\nexport const outlined_model_100_frag = `#version 100\nprecision mediump float;\nuniform float u_opacity;\nvarying vec3 barycentric;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvarying vec3 outline_color;\nvoid main () {\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\t// vec3 boundary = vec3(0.0, 0.0, 0.0);\n\tvec3 boundary = outline_color;\n\t// gl_FragDepth = 0.5;\n\tgl_FragColor = any(lessThan(barycentric, vec3(0.02)))\n\t\t? vec4(boundary, u_opacity)\n\t\t: vec4(color, u_opacity);\n}\n`;\n\nexport const model_100_vert = `#version 100\nattribute vec3 v_position;\nattribute vec3 v_normal;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nvarying vec3 normal_color;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n}\n`;\n\nexport const thick_edges_100_vert = `#version 100\nattribute vec3 v_position;\nattribute vec3 v_color;\nattribute vec3 edge_vector;\nattribute vec2 vertex_vector;\nuniform mat4 u_matrix;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform float u_strokeWidth;\nvarying vec3 blend_color;\nvoid main () {\n\tvec3 edge_norm = normalize(edge_vector);\n\t// axis most dissimilar to edge_vector\n\tvec3 absNorm = abs(edge_norm);\n\tvec3 xory = absNorm.x < absNorm.y ? vec3(1,0,0) : vec3(0,1,0);\n\tvec3 axis = absNorm.x > absNorm.z && absNorm.y > absNorm.z ? vec3(0,0,1) : xory;\n\t// two perpendiculars. with edge_vector these make basis vectors\n\tvec3 one = cross(axis, edge_norm);\n\tvec3 two = cross(one, edge_norm);\n\tvec3 displaceNormal = normalize(\n\t\tone * vertex_vector.x + two * vertex_vector.y\n\t);\n\tvec3 displace = displaceNormal * (u_strokeWidth * 0.5);\n\tgl_Position = u_matrix * vec4(v_position + displace, 1);\n\tblend_color = v_color;\n}\n`;\n\nexport const model_100_frag = `#version 100\nprecision mediump float;\nuniform float u_opacity;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvoid main () {\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\tgl_FragColor = vec4(color, u_opacity);\n}\n`;\n\nexport const simple_300_frag = `#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\nin vec3 blend_color;\nout vec4 outColor;\n \nvoid main() {\n\toutColor = vec4(blend_color.rgb, 1);\n}\n`;\n\nexport const outlined_model_100_vert = `#version 100\nattribute vec3 v_position;\nattribute vec3 v_normal;\nattribute vec3 v_barycentric;\nuniform mat4 u_projection;\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nuniform vec3 u_outlineColor;\nvarying vec3 normal_color;\nvarying vec3 barycentric;\nvarying vec3 front_color;\nvarying vec3 back_color;\nvarying vec3 outline_color;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tbarycentric = v_barycentric;\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n\toutline_color = u_outlineColor;\n}\n`;\n\nexport const outlined_model_300_vert = `#version 300 es\nuniform mat4 u_modelView;\nuniform mat4 u_matrix;\nuniform vec3 u_frontColor;\nuniform vec3 u_backColor;\nuniform vec3 u_outlineColor;\nin vec3 v_position;\nin vec3 v_normal;\nin vec3 v_barycentric;\nin float v_rawEdge;\nout vec3 front_color;\nout vec3 back_color;\nout vec3 outline_color;\nout vec3 barycentric;\n// flat out int rawEdge;\nflat out int provokedVertex;\nvoid main () {\n\tgl_Position = u_matrix * vec4(v_position, 1);\n\tprovokedVertex = gl_VertexID;\n\tbarycentric = v_barycentric;\n\t// rawEdge = int(v_rawEdge);\n\tvec3 light = abs(normalize((vec4(v_normal, 1) * u_modelView).xyz));\n\tfloat brightness = 0.5 + light.x * 0.15 + light.z * 0.35;\n\tfront_color = u_frontColor * brightness;\n\tback_color = u_backColor * brightness;\n\toutline_color = u_outlineColor;\n}\n`;\n\nexport const model_300_frag = `#version 300 es\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n  precision highp float;\n#else\n  precision mediump float;\n#endif\nuniform float u_opacity;\nin vec3 front_color;\nin vec3 back_color;\nout vec4 outColor;\nvoid main () {\n\tgl_FragDepth = gl_FragCoord.z;\n\tvec3 color = gl_FrontFacing ? front_color : back_color;\n\toutColor = vec4(color, u_opacity);\n}\n`;\n\nexport const simple_100_frag = `#version 100\nprecision mediump float;\nvarying vec3 blend_color;\nvoid main () {\n\tgl_FragColor = vec4(blend_color.rgb, 1);\n}\n`;\n"
  },
  {
    "path": "src/webgl/foldedForm/uniforms.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tidentity4x4,\n\tmultiplyMatrices4,\n} from \"../../math/matrix4.js\";\nimport {\n\tparseColorToWebGLColor,\n} from \"../general/colors.js\";\n\n/**\n * @description Uniforms must exist so there are protections to ensure\n * that at least some value gets passed.\n * @return {{ [key: string]: WebGLUniform }}\n */\nexport const makeUniforms = ({\n\tprojectionMatrix,\n\tmodelViewMatrix,\n\tfrontColor,\n\tbackColor,\n\toutlineColor,\n\tstrokeWidth,\n\topacity,\n}) => ({\n\tu_matrix: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: multiplyMatrices4(\n\t\t\tprojectionMatrix || identity4x4,\n\t\t\tmodelViewMatrix || identity4x4,\n\t\t),\n\t},\n\tu_projection: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: projectionMatrix || identity4x4,\n\t},\n\tu_modelView: {\n\t\tfunc: \"uniformMatrix4fv\",\n\t\tvalue: modelViewMatrix || identity4x4,\n\t},\n\tu_frontColor: {\n\t\tfunc: \"uniform3fv\",\n\t\tvalue: parseColorToWebGLColor(frontColor || \"gray\"),\n\t},\n\tu_backColor: {\n\t\tfunc: \"uniform3fv\",\n\t\tvalue: parseColorToWebGLColor(backColor || \"white\"),\n\t},\n\tu_outlineColor: {\n\t\tfunc: \"uniform3fv\",\n\t\tvalue: parseColorToWebGLColor(outlineColor || \"black\"),\n\t},\n\tu_strokeWidth: {\n\t\tfunc: \"uniform1f\",\n\t\tvalue: strokeWidth !== undefined ? strokeWidth : 0.05,\n\t},\n\tu_opacity: {\n\t\tfunc: \"uniform1f\",\n\t\tvalue: opacity !== undefined ? opacity : 1,\n\t},\n});\n"
  },
  {
    "path": "src/webgl/general/colors.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tparseColorToRgb,\n} from \"../../svg/colors/parseColor.js\";\n\nexport const dark = {\n\tB: [0.5, 0.5, 0.5],\n\tb: [0.5, 0.5, 0.5],\n\tV: [0.2, 0.4, 0.6],\n\tv: [0.2, 0.4, 0.6],\n\tM: [0.75, 0.25, 0.15],\n\tm: [0.75, 0.25, 0.15],\n\tF: [0.3, 0.3, 0.3],\n\tf: [0.3, 0.3, 0.3],\n\tJ: [0.3, 0.2, 0.0],\n\tj: [0.3, 0.2, 0.0],\n\tC: [0.5, 0.8, 0.1],\n\tc: [0.5, 0.8, 0.1],\n\tU: [0.6, 0.25, 0.9],\n\tu: [0.6, 0.25, 0.9],\n};\n\nexport const light = {\n\tB: [0.0, 0.0, 0.0],\n\tb: [0.0, 0.0, 0.0],\n\tV: [0.2, 0.5, 0.8],\n\tv: [0.2, 0.5, 0.8],\n\tM: [0.75, 0.25, 0.15],\n\tm: [0.75, 0.25, 0.15],\n\tF: [0.75, 0.75, 0.75],\n\tf: [0.75, 0.75, 0.75],\n\tJ: [1.0, 0.75, 0.25],\n\tj: [1.0, 0.75, 0.25],\n\tC: [0.5, 0.8, 0.1],\n\tc: [0.5, 0.8, 0.1],\n\tU: [0.6, 0.25, 0.9],\n\tu: [0.6, 0.25, 0.9],\n};\n\n/**\n * @description Convert a color into a WebGL RGB with three\n * float values,, red, green, and blue, between 0.0 and 1.0.\n * @param {number[]|string} color a color as an array of 0-255 values,\n * or a RGB/HSL/hex encoded string\n * @returns {[number, number, number]|undefined} list of three numbers\n * red, green, blue, each value between 0.0 and 1.0.\n */\nexport const parseColorToWebGLColor = (color) => {\n\tif (typeof color === \"string\") {\n\t\tconst [r, g, b] = parseColorToRgb(color).slice(0, 3).map(n => n / 255);\n\t\treturn [r, g, b];\n\t}\n\n\t// assuming the three values are already in WebGL format (0...1) not (0...255)\n\tif (color && color.constructor === Array) {\n\t\tconst [r, g, b] = color;\n\t\treturn [r, g, b];\n\t}\n\treturn undefined;\n};\n"
  },
  {
    "path": "src/webgl/general/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport * as colors from \"./colors.js\";\nimport * as model from \"./model.js\";\nimport * as view from \"./view.js\";\nimport * as webgl from \"./webgl.js\";\n\nexport default {\n\t...colors,\n\t...model,\n\t...view,\n\t...webgl,\n};\n"
  },
  {
    "path": "src/webgl/general/model.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl a WebGL context\n * @param {number} version 1 or 2, which WebGL version.\n * @param {WebGLModel} model the result of calling CreasePattern() FoldedForm()...\n * @param {{ [key: string]: WebGLUniform }} uniforms the result of calling makeUniforms()\n */\nexport const drawModel = (gl, version, model, uniforms = {}) => {\n\tgl.useProgram(model.program);\n\tmodel.flags.forEach(flag => gl.enable(flag));\n\n\t// set uniforms\n\t/** @type {number} */\n\tconst uniformCount = gl.getProgramParameter(model.program, gl.ACTIVE_UNIFORMS);\n\tfor (let i = 0; i < uniformCount; i += 1) {\n\t\tconst uniformName = gl.getActiveUniform(model.program, i).name;\n\t\tif (!uniforms[uniformName]) { continue; }\n\t\tconst { func, value } = uniforms[uniformName];\n\t\tconst index = gl.getUniformLocation(model.program, uniformName);\n\t\tswitch (func) {\n\t\tcase \"uniformMatrix2fv\":\n\t\tcase \"uniformMatrix3fv\":\n\t\tcase \"uniformMatrix4fv\": gl[func](index, false, value); break;\n\t\t// all other WebGLRenderingContext.uniform[1234][fi][v]()\n\t\tdefault: gl[func](index, value); break;\n\t\t}\n\t}\n\n\t// set vertex arrays\n\tmodel.vertexArrays.forEach(el => {\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, el.buffer);\n\t\tgl.bufferData(gl.ARRAY_BUFFER, el.data, gl.STATIC_DRAW);\n\t\tgl.vertexAttribPointer(el.location, el.length, el.type, false, 0, 0);\n\t\tgl.enableVertexAttribArray(el.location);\n\t});\n\n\t// gl.linkProgram(model.program);\n\t// draw elements\n\t// WebGL 2 supports UNSIGNED_INT (Uint32Array)\n\t// WebGL 1 cannot and must use UNSIGNED_SHORT (Uint16Array)\n\tmodel.elementArrays.forEach(el => {\n\t\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, el.buffer);\n\t\tgl.bufferData(gl.ELEMENT_ARRAY_BUFFER, el.data, gl.STATIC_DRAW);\n\t\tgl.drawElements(\n\t\t\tel.mode, // GL.TRIANGLES for example\n\t\t\tel.data.length,\n\t\t\tversion === 2 ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT,\n\t\t\t0, // offset\n\t\t);\n\t});\n\tmodel.flags.forEach(flag => gl.disable(flag));\n};\n\n/**\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl a WebGL context\n * @param {WebGLModel} model the result of calling CreasePattern() FoldedForm()...\n */\nexport const deallocModel = (gl, model) => {\n\tmodel.vertexArrays.forEach(vert => gl.disableVertexAttribArray(vert.location));\n\tmodel.vertexArrays.forEach(vert => gl.deleteBuffer(vert.buffer));\n\tmodel.elementArrays.forEach(elements => gl.deleteBuffer(elements.buffer));\n\tgl.deleteProgram(model.program);\n\t// gl.deleteTexture(someTexture);\n\t// gl.deleteRenderbuffer(someRenderbuffer);\n\t// gl.deleteFramebuffer(someFramebuffer);\n};\n"
  },
  {
    "path": "src/webgl/general/view.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport {\n\tidentity4x4,\n\tmakeOrthographicMatrix4,\n\tmakePerspectiveMatrix4,\n\tinvertMatrix4,\n} from \"../../math/matrix4.js\";\nimport {\n\tmidpoint,\n\tresize,\n} from \"../../math/vector.js\";\nimport {\n\tboundingBox,\n} from \"../../graph/boundary.js\";\n\n/**\n * @description Initialize a viewport for a WebGL context\n * based on the dimensions of the canvas.\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl a WebGL instance\n * @param {HTMLCanvasElement} canvas an HTML canvas\n * @returns {undefined}\n */\nexport const rebuildViewport = (gl, canvas) => {\n\tif (!gl) { return; }\n\tconst devicePixelRatio = window.devicePixelRatio || 1;\n\tconst size = [canvas.clientWidth, canvas.clientHeight]\n\t\t.map(n => n * devicePixelRatio);\n\tif (canvas.width !== size[0] || canvas.height !== size[1]) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tcanvas.width = size[0];\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tcanvas.height = size[1];\n\t}\n\tgl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n};\n\n/**\n * @description Create a 4x4 projection matrix for either a\n * perspective or orthographic view.\n * @param {[number, number]} size the size of the HTML canvas\n * @param {string} perspective \"orthographic\" or \"perspective\"\n * @param {number} fov the field of view (perspective only)\n * @returns {number[]} a 4x4 projection matrix\n */\nexport const makeProjectionMatrix = ([width, height], perspective = \"perspective\", fov = 45) => {\n\tconst Z_NEAR = 0.1;\n\tconst Z_FAR = 20;\n\tconst ORTHO_FAR = -100;\n\tconst ORTHO_NEAR = 100;\n\tconst vmin = Math.min(width, height);\n\tconst padding = [\n\t\t((width - vmin) / vmin) / 2,\n\t\t((height - vmin) / vmin) / 2,\n\t];\n\tconst side = padding.map(p => p + 0.5);\n\treturn perspective === \"orthographic\"\n\t\t? makeOrthographicMatrix4(side[1], side[0], -side[1], -side[0], ORTHO_FAR, ORTHO_NEAR)\n\t\t: makePerspectiveMatrix4(fov * (Math.PI / 180), width / height, Z_NEAR, Z_FAR);\n};\n\n/**\n * @description build an aspect-fit model matrix\n * (possibly an inverse-model matrix)\n * which brings the vertices inside of a 2x2x2 origin-centered bounding box.\n * @param {FOLD} graph a FOLD object\n * @returns {number[]} a 4x4 model matrix\n */\nexport const makeModelMatrix = (graph) => {\n\tif (!graph) { return [...identity4x4]; }\n\tconst bounds = boundingBox(graph);\n\tif (!bounds) { return [...identity4x4]; }\n\tconst scale = Math.max(...bounds.span); // * Math.SQRT2;\n\tif (scale === 0) { return [...identity4x4]; }\n\tconst center = resize(3, midpoint(bounds.min, bounds.max));\n\tconst scalePositionMatrix = [scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, scale, 0, ...center, 1];\n\treturn invertMatrix4(scalePositionMatrix) || [...identity4x4];\n};\n"
  },
  {
    "path": "src/webgl/general/webgl.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport Messages from \"../../environment/messages.js\";\nimport Window from \"../../environment/window.js\";\n\n/**\n * @description Initialize a WebGL context\n * @param {HTMLCanvasElement} canvasElement an HTML canvas element\n * @param {number} [preferredVersion] the preferred version of WebGL\n * @returns {{\n *   gl: WebGLRenderingContext | WebGL2RenderingContext,\n *   version: number,\n * }} an object with a\n * WebGL rendering context, and the version of WebGL it was initialized under.\n */\nexport const initializeWebGL = (canvasElement, preferredVersion) => {\n\t// set the size of the drawingBuffer to include retina display level pixels (if exist),\n\t// the size can still change after, even with CSS, this only matters for getContext\n\tconst devicePixelRatio = Window().devicePixelRatio || 1;\n\t// eslint-disable-next-line no-param-reassign\n\tcanvasElement.width = canvasElement.clientWidth * devicePixelRatio;\n\t// eslint-disable-next-line no-param-reassign\n\tcanvasElement.height = canvasElement.clientHeight * devicePixelRatio;\n\n\t// if there was a user preference\n\tswitch (preferredVersion) {\n\tcase 1: return { gl: canvasElement.getContext(\"webgl\"), version: 1 };\n\tcase 2: return { gl: canvasElement.getContext(\"webgl2\"), version: 2 };\n\tdefault: break;\n\t}\n\n\t// no user preference. attempt version 2, if fails, return version 1.\n\tconst gl2 = canvasElement.getContext(\"webgl2\");\n\tif (gl2) { return { gl: gl2, version: 2 }; }\n\tconst gl1 = canvasElement.getContext(\"webgl\");\n\tif (gl1) { return { gl: gl1, version: 1 }; }\n\tthrow new Error(Messages.noWebGL);\n};\n\n// WebGL boilerplate from https://webglfundamentals.org\n\n/**\n * @description Creates and compiles a shader.\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {string} shaderSource The GLSL source code for the shader.\n * @param {number} shaderType The type of shader, VERTEX_SHADER or\n *     FRAGMENT_SHADER.\n * @returns {WebGLShader} The shader.\n */\nconst compileShader = (gl, shaderSource, shaderType) => {\n\tconst shader = gl.createShader(shaderType);\n\tgl.shaderSource(shader, shaderSource);\n\tgl.compileShader(shader);\n\tif (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n\t\tthrow new Error(gl.getShaderInfoLog(shader));\n\t}\n\treturn shader;\n};\n\n/**\n * @description Creates a program from 2 shaders.\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL context.\n * @param {WebGLShader} vertexShader A vertex shader.\n * @param {WebGLShader} fragmentShader A fragment shader.\n * @returns {WebGLProgram} A program.\n */\nconst createProgramAndAttachShaders = (gl, vertexShader, fragmentShader) => {\n\tconst program = gl.createProgram();\n\tgl.attachShader(program, vertexShader);\n\tgl.attachShader(program, fragmentShader);\n\tgl.linkProgram(program);\n\tif (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n\t\t// something went wrong with the link\n\t\tthrow new Error(gl.getProgramInfoLog(program));\n\t}\n\tgl.deleteShader(vertexShader);\n\tgl.deleteShader(fragmentShader);\n\treturn program;\n};\n\n/**\n * Creates a program from 2 script tags.\n *\n * @param {WebGLRenderingContext|WebGL2RenderingContext} gl The WebGL Context.\n * @param {string} vertexSource vertex shader as raw text\n * @param {string} fragmentSource fragment shader as raw text\n * @returns {WebGLProgram} a WebGL program\n */\nexport const createProgram = (gl, vertexSource, fragmentSource) => {\n\tconst vertexShader = compileShader(gl, vertexSource, gl.VERTEX_SHADER);\n\tconst fragmentShader = compileShader(gl, fragmentSource, gl.FRAGMENT_SHADER);\n\treturn createProgramAndAttachShaders(gl, vertexShader, fragmentShader);\n};\n"
  },
  {
    "path": "src/webgl/index.js",
    "content": "/**\n * Rabbit Ear (c) Kraft\n */\nimport general from \"./general/index.js\";\nimport foldedForm from \"./foldedForm/index.js\";\nimport creasePattern from \"./creasePattern/index.js\";\n\nexport default {\n\t...general,\n\t...foldedForm,\n\t...creasePattern,\n};\n"
  },
  {
    "path": "src/webgl/readme.md",
    "content": "# FOLD in WebGL\n\nCreate and manage a simple WebGL instance and draw a FOLD format object with support for different drawing styles.\n\n```javascript\near.webgl\n```\n\n# usage\n\n1. initialize the WebGL context\n2. create a program with your FOLD file and customize it with options.\n3. set the shader uniforms and draw to the screen\n\nwhere step 3 can go inside an animation loop if you choose.\n\n# example\n\nthese variables are a [FOLD format](https://github.com/edemaine/fold) object and an HTML canvas:\n\n```javascript\nvar FOLD;\nvar canvas;\n```\n\ninitialize and draw:\n\n```javascript\n// gl is the WebGL context. version is either 1 or 2.\nconst { gl, version } = ear.webgl.initializeWebGL(canvas);\n\n// Initialize a WebGL viewport based on the dimensions of the canvas\near.webgl.rebuildViewport(gl, canvas);\n\n// draw creasePattern style, or foldedForm style\nconst models = ear.webgl.creasePattern(gl, version, FOLD);\n// models = ear.webgl.foldedForm(gl, version, FOLD);\n\n// [canvas.clientWidth, canvas.clientHeight]\nconst projectionMatrix = ear.webgl.makeProjectionMatrix([canvas.width, canvas.height]);\nconst modelViewMatrix = ear.webgl.makeModelMatrix(FOLD);\n\ngl.enable(gl.BLEND);\ngl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\ngl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n// prepare the uniforms and draw the frame\nconst modelUniforms = models.map(model => model.makeUniforms(gl, {\n\tprojectionMatrix,\n\tmodelViewMatrix,\n\tcanvas,\n\tfrontColor: \"#369\",\n\tbackColor: \"white\",\n\tcpColor: \"white\",\n\tstrokeWidth: 0.05,\n\topacity: 1,\n}));\n\nmodels.forEach((model, i) => {\n\tear.webgl.drawModel(gl, version, model, modelUniforms[i]);\n});\n```\n\ndealloc:\n\n```javascript\nmodels.forEach(model => ear.webgl.deallocModel(gl, model));\n```\n\n# uniforms\n\nThese uniforms are **required**, although foldedForm and creasePattern require a different set\n\n- **projectionMatrix**: `{number[]}` an array of 16 numbers, the projection matrix\n- **modelViewMatrix**: `{number[]}` an array of 16 numbers, the modelView matrix\n- **canvas**: `{number[]}` an array of 2 numbers, the width and height in pixels of the canvas\n- **frontColor**: `{number[]}` the color of the front of the mesh\n- **backColor**: `{number[]}` the color of the back of the mesh\n- **cpColor**: `{number[]}` the color of the space inside the crease pattern\n- **strokeWidth**: `{number[]}` stroke width of the crease pattern and foldedForm edges\n- **opacity**: `{number[]}` opacity of the mesh faces\n\nrequired by **foldedForm**: projectionMatrix, modelViewMatrix, canvas, frontColor, backColor, strokeWidth, opacity\n\nrequired by **creasePattern**: projectionMatrix, modelViewMatrix, canvas, cpColor, strokeWidth\n\n# options\n\n```javascript\near.webgl.creasePattern(gl, version, FOLD, options);\near.webgl.foldedForm(gl, version, FOLD, options);\n```\n\nat initialization, both programs can take an optional 4th parameter, an options object. The object itself can contain any number of the following:\n\n```javascript\nconst options = {\n\tlayerNudge: 1e-5,\n\toutlines: true,\n\tedges: false,\n\tfaces: true,\n\tdark: true,\n\tB: [0.5, 0.5, 0.5],\n\tb: [0.5, 0.5, 0.5],\n\tV: [0.2, 0.4, 0.6],\n\tv: [0.2, 0.4, 0.6],\n\tM: [0.75, 0.25, 0.15],\n\tm: [0.75, 0.25, 0.15],\n\tF: [0.3, 0.3, 0.3],\n\tf: [0.3, 0.3, 0.3],\n\tJ: [0.3, 0.2, 0.0],\n\tj: [0.3, 0.2, 0.0],\n\tC: [0.5, 0.8, 0.1],\n\tc: [0.5, 0.8, 0.1],\n\tU: [0.6, 0.25, 0.9],\n\tu: [0.6, 0.25, 0.9],\n};\n```\n\n- **layerNudge**: for foldedForm, if there is faceOrders present this is the space between faces\n- **outlines**: for foldedForm, a dark outline around every face (excluding \"J\" edges)\n- **edges**: for foldedForm, show the creasePattern style colored edges but on the foldedForm\n- **faces**: for foldedForm, show or hide the mesh faces\n- **dark**: both creasePattern and foldedForm, render colors appropriate for dark mode.\n\nand finally, any crease assignment's RGB values can be specified (for foldedForm be sure to turn on \"edges\" option). because FOLD spec supports both capital and lowercase, unless you know the contents of your FOLD format, you should duplicate the color for every assignment.\n\n# design notes\n\nthe *options* (given at initialization) and the *uniforms* (given each draw frame) both include similar information; options can be moved to the uniforms, and visa-versa. Consider however that the uniforms is *required* and the options isn't, so making all fields like this in the uniforms isn't necessarily desirable.\n"
  },
  {
    "path": "tests/axioms.axioms.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// test(\"axiom single function\", () => {\n// \tear.axiom.axiom(1, [0.75, 0.75], [0.15, 0.85]);\n// \tear.axiom.axiom(2, [0.75, 0.75], [0.15, 0.85]);\n// \tear.axiom.axiom(\n// \t\t3,\n// \t\t{ vector: [0.0, -0.5], origin: [0.25, 0.75] },\n// \t\t{ vector: [0.5, 0.0], origin: [0.5, 0.75] },\n// \t);\n// \tear.axiom.axiom(\n// \t\t4,\n// \t\t{ vector: [-0.10, 0.11], origin: [0.62, 0.37] },\n// \t\t[0.83, 0.51],\n// \t);\n// \tear.axiom.axiom(\n// \t\t5,\n// \t\t{ vector: [-0.37, -0.13], origin: [0.49, 0.71] },\n// \t\t[0.75, 0.75],\n// \t\t[0.15, 0.85],\n// \t);\n// \tear.axiom.axiom(\n// \t\t6,\n// \t\t{ vector: [0.04, -0.52], origin: [0.43, 0.81] },\n// \t\t{ vector: [0.05, -0.28], origin: [0.10, 0.52] },\n// \t\t[0.75, 0.75],\n// \t\t[0.15, 0.85],\n// \t);\n// \tear.axiom.axiom(\n// \t\t7,\n// \t\t{ vector: [-0.20, -0.25], origin: [0.62, 0.93] },\n// \t\t{ vector: [-0.42, 0.61], origin: [0.52, 0.25] },\n// \t\t[0.25, 0.85],\n// \t);\n// });\n\ntest(\"axiom 1\", () => {\n\tconst res0 = ear.axiom.axiom1([2 / 3, 1 / 3], [1 / 3, 2 / 3]);\n\tconst res1 = ear.axiom.axiom1([2 / 3, 1 / 3, 0], [1 / 3, 2 / 3, 0]);\n\tconst expected = {\n\t\torigin: [2 / 3, 1 / 3],\n\t\tvector: [-Math.SQRT1_2, Math.SQRT1_2],\n\t};\n\texpect(ear.math.epsilonEqualVectors(res0[0].vector, expected.vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res0[0].origin, expected.origin)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res1[0].vector, expected.vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res1[0].origin, expected.origin)).toBe(true);\n});\n\ntest(\"axiom 2\", () => {\n\tconst res0 = ear.axiom.axiom2([2 / 3, 1 / 3], [1 / 3, 2 / 3]);\n\tconst res1 = ear.axiom.axiom2([2 / 3, 1 / 3, 0], [1 / 3, 2 / 3, 0]);\n\tconst expected = {\n\t\torigin: [0.5, 0.5],\n\t\tvector: [-Math.SQRT1_2, -Math.SQRT1_2],\n\t};\n\texpect(ear.math.epsilonEqualVectors(res0[0].vector, expected.vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res0[0].origin, expected.origin)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res1[0].vector, expected.vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res1[0].origin, expected.origin)).toBe(true);\n});\n\ntest(\"axiom 3\", () => {\n\tconst res = ear.axiom.axiom3(\n\t\t{ vector: [0, 1], origin: [0.5, 0.5] },\n\t\t{ vector: [1, 0], origin: [0, 0.5] },\n\t);\n\tconst expected = [\n\t\t{ origin: [0.5, 0.5], vector: [Math.SQRT1_2, Math.SQRT1_2] },\n\t\t{ origin: [0.5, 0.5], vector: [Math.SQRT1_2, -Math.SQRT1_2] },\n\t];\n\texpect(ear.math.epsilonEqualVectors(res[0].vector, expected[0].vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res[0].origin, expected[0].origin)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res[1].vector, expected[1].vector)).toBe(true);\n\texpect(ear.math.epsilonEqualVectors(res[1].origin, expected[1].origin)).toBe(true);\n});\n\ntest(\"axiom 4\", () => {\n\tconst line = { vector: [1, 1] }; // no origin needed for axiom 4\n\tconst pointB = [3, 1];\n\tconst res = ear.axiom.axiom4(line, pointB);\n\texpect(res[0].vector[0]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(res[0].vector[1]).toBeCloseTo(Math.SQRT1_2);\n\texpect(res[0].origin[0]).toBe(3);\n\texpect(res[0].origin[1]).toBe(1);\n});\n\ntest(\"axiom 5\", () => {\n\tconst res = ear.axiom.axiom5({ vector: [0, 1], origin: [0.5, 0.5] }, [0, 0], [1, 0]);\n\texpect(res[0].vector[0]).toBeCloseTo(Math.sqrt(3) / 2);\n\texpect(res[0].vector[1]).toBeCloseTo(-0.5);\n\texpect(res[1].vector[0]).toBeCloseTo(-Math.sqrt(3) / 2);\n\texpect(res[1].vector[1]).toBeCloseTo(-0.5);\n\texpect(res[0].origin[0]).toBeCloseTo(0.75);\n\texpect(res[0].origin[1]).toBeCloseTo(-0.4330127);\n\texpect(res[1].origin[0]).toBeCloseTo(0.75);\n\texpect(res[1].origin[1]).toBeCloseTo(0.4330127);\n});\n\ntest(\"axiom 6\", () => {\n\tconst params = {\n\t\tpoints: [\n\t\t\t[0.30, 0.86],\n\t\t\t[0.14, 0.07],\n\t\t],\n\t\tlines: [\n\t\t\t{ vector: [0.74, 0.42], origin: [-0.16, 0.21] },\n\t\t\t{ vector: [-1.17, 0.44], origin: [0.91, 0.25] },\n\t\t],\n\t};\n\n\texpect(true).toBe(true);\n});\n\ntest(\"axiom 6 with no params\", () => {\n\ttry {\n\t\tear.axiom.axiom6({ vector: [], origin: [] }, { vector: [], origin: [] }, [], []);\n\t} catch (err) {\n\t\texpect(err).not.toBe(undefined);\n\t}\n});\n\ntest(\"axiom 6 with 3 results\", () => {\n\tconst vectorA = [0, 1];\n\tconst originA = [1, 0];\n\tconst vectorB = [1, 0];\n\tconst originB = [0, 1];\n\tconst pointA = [0.75, 0];\n\tconst pointB = [0, 0.75];\n\tconst res = ear.axiom.axiom6(\n\t\t{ vector: vectorA, origin: originA },\n\t\t{ vector: vectorB, origin: originB },\n\t\tpointA,\n\t\tpointB,\n\t);\n\tconst lines = [{\n\t\torigin: [0.8535533905932738, 0.14644660940672635],\n\t\tvector: [0.16910197872576288, -0.9855985596534887],\n\t},\n\t{\n\t\torigin: [0.14644660940672627, 0.8535533905932738],\n\t\tvector: [0.9855985596534889, -0.16910197872576277],\n\t},\n\t{\n\t\torigin: [0.4999999999999999, 0.4999999999999999],\n\t\tvector: [0.7071067811865475, -0.7071067811865475],\n\t}];\n\tfor (let i = 0; i < lines.length; i += 1) {\n\t\texpect(res[i].vector[0]).toBeCloseTo(lines[i].vector[0]);\n\t\texpect(res[i].vector[1]).toBeCloseTo(lines[i].vector[1]);\n\t\texpect(res[i].origin[0]).toBeCloseTo(lines[i].origin[0]);\n\t\texpect(res[i].origin[1]).toBeCloseTo(lines[i].origin[1]);\n\t}\n});\n\ntest(\"axiom 6 with 2 results\", () => {\n\tconst vectorA = [1.00, 0.00];\n\tconst originA = [0.00, 1.00];\n\tconst vectorB = [1.00, 0.00];\n\tconst originB = [0.00, 0.00];\n\tconst pointA = [0.90, 0.10];\n\tconst pointB = [0.10, 0.90];\n\tconst res = ear.axiom.axiom6(\n\t\t{ vector: vectorA, origin: originA },\n\t\t{ vector: vectorB, origin: originB },\n\t\tpointA,\n\t\tpointB,\n\t);\n\tconst lines = [{\n\t\torigin: [-0.0625, 0.0846405430584631],\n\t\tvector: [0.8044504602885519, 0.5940197445721286],\n\t},\n\t{\n\t\torigin: [-0.06249999999999996, 0.41535945694153714],\n\t\tvector: [0.9888677651443275, 0.14879698605302136],\n\t}];\n\tfor (let i = 0; i < lines.length; i += 1) {\n\t\texpect(res[i].vector[0]).toBeCloseTo(lines[i].vector[0]);\n\t\texpect(res[i].vector[1]).toBeCloseTo(lines[i].vector[1]);\n\t\texpect(res[i].origin[0]).toBeCloseTo(lines[i].origin[0]);\n\t\texpect(res[i].origin[1]).toBeCloseTo(lines[i].origin[1]);\n\t}\n});\n\ntest(\"axiom 7\", () => {\n\tconst line1 = { vector: [1, 1], origin: [-1, 0] };\n\tconst line2 = { vector: [1, -1] }; // no origin needed\n\tconst res = ear.axiom.axiom7(line1, line2, [1, 0]);\n\texpect(res[0].vector[0]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(res[0].vector[1]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(res[0].origin[0]).toBe(0.5);\n\texpect(res[0].origin[1]).toBe(0.5);\n});\n"
  },
  {
    "path": "tests/axioms.boundary.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"validate off for now\", () => {});\n\ntest(\"validate, valid results\", () => {\n\tconst boundary = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\n\tconst args1 = [[0.75, 0.75], [0.15, 0.85]];\n\tconst args2 = [[0.75, 0.75], [0.15, 0.85]];\n\tconst args3 = [\n\t\t{ vector: [0.0, -0.5], origin: [0.25, 0.75] },\n\t\t{ vector: [0.5, 0.0], origin: [0.5, 0.75] },\n\t];\n\tconst args4 = [\n\t\t{ vector: [-0.10, 0.11], origin: [0.62, 0.37] },\n\t\t[0.83, 0.51],\n\t];\n\tconst args5 = [\n\t\t{ vector: [-0.37, -0.13], origin: [0.49, 0.71] },\n\t\t[0.5, 0.5],\n\t\t[0.15, 0.85],\n\t];\n\tconst args6 = [\n\t\t{ vector: [0.1, -0.5], origin: [0.45, 0.8] },\n\t\t{ vector: [0.2, -0.25], origin: [0.35, 0.5] },\n\t\t[0.65, 0.55],\n\t\t[0.45, 0.25],\n\t];\n\tconst args7 = [\n\t\t{ vector: [-0.20, -0.25], origin: [0.62, 0.93] },\n\t\t{ vector: [-0.42, 0.61], origin: [0.52, 0.25] },\n\t\t[0.25, 0.85],\n\t];\n\n\tconst solutions1 = ear.axiom.axiom1InPolygon(boundary, ...args1)\n\t\t.filter(a => a);\n\tconst solutions2 = ear.axiom.axiom2InPolygon(boundary, ...args2)\n\t\t.filter(a => a);\n\tconst solutions3 = ear.axiom.axiom3InPolygon(boundary, ...args3)\n\t\t.filter(a => a);\n\tconst solutions4 = ear.axiom.axiom4InPolygon(boundary, ...args4)\n\t\t.filter(a => a);\n\tconst solutions5 = ear.axiom.axiom5InPolygon(boundary, ...args5)\n\t\t.filter(a => a);\n\tconst solutions6 = ear.axiom.axiom6InPolygon(boundary, ...args6)\n\t\t.filter(a => a);\n\tconst solutions7 = ear.axiom.axiom7InPolygon(boundary, ...args7)\n\t\t.filter(a => a);\n\n\texpect(solutions1.length).toBe(1);\n\texpect(solutions2.length).toBe(1);\n\texpect(solutions3.length).toBe(2);\n\texpect(solutions4.length).toBe(1);\n\texpect(solutions5.length).toBe(2);\n\texpect(solutions6.length).toBe(3);\n\texpect(solutions7.length).toBe(1);\n});\n\ntest(\"validate, invalid results due to point (or line) outside boundary\", () => {\n\tconst boundary = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\n\tconst args1 = [[1.75, 0.75], [0.15, 0.85]];\n\tconst args2 = [[0.75, 0.75], [-0.15, 0.85]];\n\tconst args3 = [\n\t\t{ vector: [0, -1], origin: [1.25, 0] },\n\t\t{ vector: [1, 0], origin: [0.5, 0.75] },\n\t];\n\tconst args4 = [\n\t\t{ vector: [-0.10, 0.11], origin: [0.62, 0.37] },\n\t\t[1.1, 0.5],\n\t];\n\tconst args5 = [\n\t\t{ vector: [-0.37, -0.13], origin: [0.49, 0.71] },\n\t\t[0.75, 0.75],\n\t\t[1.1, 0.85],\n\t];\n\tconst args6 = [\n\t\t{ vector: [0.1, -0.5], origin: [0.4, 0.8] },\n\t\t{ vector: [0.2, -0.25], origin: [0.1, 0.5] },\n\t\t[0.65, 0.7],\n\t\t[0.15, 0.15],\n\t];\n\tconst args7 = [\n\t\t{ vector: [-0.20, -0.25], origin: [0.62, 0.93] },\n\t\t{ vector: [-0.42, 0.61], origin: [0.52, 0.25] },\n\t\t[0.25, 1.1],\n\t];\n\n\tconst solution1 = ear.axiom.axiom1InPolygon(boundary, ...args1)\n\t\t.filter(a => a);\n\tconst solution2 = ear.axiom.axiom2InPolygon(boundary, ...args2)\n\t\t.filter(a => a);\n\tconst solution3 = ear.axiom.axiom3InPolygon(boundary, ...args3)\n\t\t.filter(a => a);\n\tconst solution4 = ear.axiom.axiom4InPolygon(boundary, ...args4)\n\t\t.filter(a => a);\n\tconst solution5 = ear.axiom.axiom5InPolygon(boundary, ...args5)\n\t\t.filter(a => a);\n\tconst solution6 = ear.axiom.axiom6InPolygon(boundary, ...args6)\n\t\t.filter(a => a);\n\tconst solution7 = ear.axiom.axiom7InPolygon(boundary, ...args7)\n\t\t.filter(a => a);\n\n\texpect(solution1.length).toBe(0);\n\texpect(solution2.length).toBe(0);\n\texpect(solution3.length).toBe(0);\n\texpect(solution4.length).toBe(0);\n\texpect(solution5.length).toBe(0);\n\texpect(solution6.length).toBe(1); // still need an axiom 6 that fails all.\n\t// expect(solution7.length).toBe(0);\n});\n"
  },
  {
    "path": "tests/axioms.validate.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"validate, valid results\", () => {\n\tconst boundary = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\n\tconst args1 = [[0.75, 0.75], [0.15, 0.85]];\n\tconst args2 = [[0.75, 0.75], [0.15, 0.85]];\n\tconst args3 = [\n\t\t{ vector: [0.0, -0.5], origin: [0.25, 0.75] },\n\t\t{ vector: [0.5, 0.0], origin: [0.5, 0.75] },\n\t];\n\tconst args4 = [\n\t\t{ vector: [-0.10, 0.11], origin: [0.62, 0.37] },\n\t\t[0.83, 0.51],\n\t];\n\tconst args5 = [\n\t\t{ vector: [-0.37, -0.13], origin: [0.49, 0.71] },\n\t\t[0.5, 0.5],\n\t\t[0.15, 0.85],\n\t];\n\tconst args6 = [\n\t\t{ vector: [0.1, -0.5], origin: [0.45, 0.8] },\n\t\t{ vector: [0.2, -0.25], origin: [0.35, 0.5] },\n\t\t[0.65, 0.55],\n\t\t[0.45, 0.25],\n\t];\n\tconst args7 = [\n\t\t{ vector: [-0.20, -0.25], origin: [0.62, 0.93] },\n\t\t{ vector: [-0.42, 0.61], origin: [0.52, 0.25] },\n\t\t[0.25, 0.85],\n\t];\n\n\t// const solutions1 = ear.axiom.axiom1(...args1);\n\t// const solutions2 = ear.axiom.axiom2(...args2);\n\tconst solutions3 = ear.axiom.axiom3(...args3);\n\tconst solutions4 = ear.axiom.axiom4(...args4);\n\tconst solutions5 = ear.axiom.axiom5(...args5);\n\tconst solutions6 = ear.axiom.axiom6(...args6);\n\tconst solutions7 = ear.axiom.axiom7(...args7);\n\n\tconst valid1 = ear.axiom.validateAxiom1And2(boundary, ...args1);\n\tconst valid2 = ear.axiom.validateAxiom1And2(boundary, ...args2);\n\tconst valid3 = ear.axiom.validateAxiom3(boundary, solutions3, ...args3);\n\tconst valid4 = ear.axiom.validateAxiom4(boundary, solutions4, ...args4);\n\tconst valid5 = ear.axiom.validateAxiom5(boundary, solutions5, ...args5);\n\tconst valid6 = ear.axiom.validateAxiom6(boundary, solutions6, ...args6);\n\tconst valid7 = ear.axiom.validateAxiom7(boundary, solutions7, ...args7);\n\n\texpect(valid1.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid2.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid3.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid4.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid5.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid6.reduce((a, b) => a && b, true)).toBe(true);\n\texpect(valid7.reduce((a, b) => a && b, true)).toBe(true);\n});\n\ntest(\"validate, invalid results due to point (or line) outside boundary\", () => {\n\tconst boundary = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\n\tconst args1 = [[1.75, 0.75], [0.15, 0.85]];\n\tconst args2 = [[0.75, 0.75], [-0.15, 0.85]];\n\tconst args3 = [\n\t\t{ vector: [0, -1], origin: [1.25, 0] },\n\t\t{ vector: [1, 0], origin: [0.5, 0.75] },\n\t];\n\tconst args4 = [\n\t\t{ vector: [-0.10, 0.11], origin: [0.62, 0.37] },\n\t\t[1.1, 0.5],\n\t];\n\tconst args5 = [\n\t\t{ vector: [-0.37, -0.13], origin: [0.49, 0.71] },\n\t\t[0.75, 0.75],\n\t\t[1.1, 0.85],\n\t];\n\tconst args6 = [\n\t\t{ vector: [0.1, -0.5], origin: [0.4, 0.8] },\n\t\t{ vector: [0.2, -0.25], origin: [0.1, 0.5] },\n\t\t[0.65, 0.7],\n\t\t[0.15, 0.15],\n\t];\n\tconst args7 = [\n\t\t{ vector: [-0.20, -0.25], origin: [0.62, 0.93] },\n\t\t{ vector: [-0.42, 0.61], origin: [0.52, 0.25] },\n\t\t[0.25, 1.1],\n\t];\n\n\t// const solutions1 = ear.axiom.axiom1(...args1);\n\t// const solutions2 = ear.axiom.axiom2(...args2);\n\tconst solutions3 = ear.axiom.axiom3(...args3);\n\tconst solutions4 = ear.axiom.axiom4(...args4);\n\tconst solutions5 = ear.axiom.axiom5(...args5);\n\tconst solutions6 = ear.axiom.axiom6(...args6);\n\tconst solutions7 = ear.axiom.axiom7(...args7);\n\n\tconst valid1 = ear.axiom.validateAxiom1And2(boundary, ...args1);\n\tconst valid2 = ear.axiom.validateAxiom1And2(boundary, ...args2);\n\tconst valid3 = ear.axiom.validateAxiom3(boundary, solutions3, ...args3);\n\tconst valid4 = ear.axiom.validateAxiom4(boundary, solutions4, ...args4);\n\tconst valid5 = ear.axiom.validateAxiom5(boundary, solutions5, ...args5);\n\tconst valid6 = ear.axiom.validateAxiom6(boundary, solutions6, ...args6);\n\tconst valid7 = ear.axiom.validateAxiom7(boundary, solutions7, ...args7);\n\n\texpect(valid1.reduce((a, b) => a || b, false)).toBe(false);\n\texpect(valid2.reduce((a, b) => a || b, false)).toBe(false);\n\texpect(valid3.reduce((a, b) => a || b, false)).toBe(false);\n\texpect(valid4.reduce((a, b) => a || b, false)).toBe(false);\n\texpect(valid5.reduce((a, b) => a || b, false)).toBe(false);\n\texpect(valid6[0]).toBe(false);\n\texpect(valid6[1]).toBe(false);\n\texpect(valid6[2]).toBe(true);\n\t// expect(valid7.reduce((a, b) => a || b, false)).toBe(false);\n});\n\ntest(\"axiom 6 with 2 results\", () => {\n\tconst args6 = [\n\t\t{ vector: [0, 0.5], origin: [0.2, 0.85] },\n\t\t{ vector: [0, -0.25], origin: [0.5, 0.5] },\n\t\t[0.35, 0.25],\n\t\t[0.25, 0.6],\n\t];\n\tconst result = ear.axiom.validateAxiom6(\n\t\t[[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tear.axiom.axiom6(...args6),\n\t\t...args6,\n\t);\n\texpect(result.length).toBe(2);\n\texpect(result[0]).toBe(true);\n\texpect(result[1]).toBe(true);\n});\n"
  },
  {
    "path": "tests/clean.js",
    "content": "import fs from \"fs\";\n\nfs.readdirSync(\"./\")\n\t.filter(s => s.match(/rabbit-ear(.*).js/))\n\t.forEach(path => fs.unlinkSync(`./${path}`));\n\nfs.rm(\"./docs\", { recursive: true, force: true }, () => {});\nfs.rm(\"./module\", { recursive: true, force: true }, () => {});\nfs.rm(\"./types\", { recursive: true, force: true }, () => {});\n"
  },
  {
    "path": "tests/convert.foldToObj.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"convert foldToObj no param\", () => {\n\tlet error;\n\ttry {\n\t\tear.convert.foldToObj();\n\t} catch (err) {\n\t\terror = err;\n\t}\n\texpect(error).not.toBe(undefined);\n});\n\ntest(\"convert foldToObj empty\", () => {\n\tconst empty = {};\n\tear.convert.foldToObj(empty);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert foldToObj FOLD object\", () => {\n\tconst cp = ear.graph.fish();\n\tear.convert.foldToObj(cp);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert foldToObj FOLD string\", () => {\n\tconst cp = ear.graph.fish();\n\tconst FOLD = JSON.stringify(cp);\n\tear.convert.foldToObj(FOLD);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert FOLD file\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tear.convert.foldToObj(FOLD);\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/convert.foldToSvg.origami.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"svg.origami(), no options\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\t// create a Rabbit Ear SVG element.\n\tconst svg = ear.svg();\n\t// draw an origami and append it to this SVG\n\t// by default, \"viewBox\" option is set to true.\n\tsvg.origami(graph);\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-origami()-crane.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"svg.origami(), turn off viewBox\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\t// create a Rabbit Ear SVG element.\n\tconst svg = ear.svg()\n\t\t.strokeWidth(0.05);\n\t// draw an origami and append it to this SVG\n\tsvg.origami(graph, { viewBox: false });\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-origami()-crane-no-viewbox.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"svg.origami(), options\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\t// create a Rabbit Ear SVG element.\n\tconst svg = ear.svg();\n\t// draw an origami and append it to this SVG\n\t// style with an options object\n\tsvg.origami(graph, {\n\t\tfaces: false,\n\t\tedges: { mountain: { stroke: \"red\" }, valley: { stroke: \"blue\" } },\n\t\tvertices: true,\n\t});\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-origami()-crane-options.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"svg.origami(), custom methods\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst svg = ear.svg()\n\t\t.viewBox([-1, -1, 3, 3])\n\t\t.strokeWidth(0.05);\n\tsvg.circle(0.5, 0.5, 0.5)\n\t\t.fill(\"#f003\")\n\t\t.stroke(\"purple\");\n\tsvg.origami(graph)\n\t\t.fill(\"red\")\n\t\t.stroke(\"blue\")\n\t\t.strokeDasharray(\"0.1\");\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-origami()-crane-style.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"svg.origami(), custom getters\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst svg = ear.svg()\n\t\t.viewBox([-1, -1, 3, 3])\n\t\t.strokeWidth(0.05);\n\tconst origami = svg.origami(graph);\n\t// returns the group\n\torigami.edges()\n\t\t.strokeDasharray(\"0.1\");\n\torigami.faces()\n\t\t.fill(\"#F083\")\n\t\t.strokeDasharray(\"0.1\");\n\texpect(origami.vertices()).toBeUndefined();\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-origami()-crane-getters.svg\", serializer.serializeToString(svg));\n});\n"
  },
  {
    "path": "tests/convert.foldToSvg.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"convert foldToSvg no param\", () => {\n\tlet error;\n\ttry {\n\t\tear.convert.foldToSvg();\n\t} catch (err) {\n\t\terror = err;\n\t}\n\texpect(error).not.toBe(undefined);\n});\n\ntest(\"convert foldToSvg empty\", () => {\n\tconst empty = {};\n\tear.convert.foldToSvg(empty);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert foldToSvg FOLD object\", () => {\n\tconst cp = ear.graph.fish();\n\tear.convert.foldToSvg(cp);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert foldToSvg FOLD string\", () => {\n\tconst cp = ear.graph.fish();\n\tconst FOLD = JSON.stringify(cp);\n\tear.convert.foldToSvg(FOLD);\n\texpect(true).toBe(true);\n});\n\ntest(\"convert FOLD file\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tear.convert.foldToSvg(FOLD);\n\texpect(true).toBe(true);\n});\n\ntest(\"foldToSvg CP all assignments, no opacity\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane-cp-bmvfcj.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst svg = ear.convert.foldToSvg(graph);\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-crane-cp-bmvfcj.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"foldToSvg CP foldAngles and opacity\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/bird-base-3d-cp.fold\", \"utf-8\");\n});\n\ntest(\"foldToSvg folded form, flat folded\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst svg = ear.convert.foldToSvg(graph);\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-crane-folded.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"foldToSvg folded form, with foldAngles\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/bird-base-3d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst svg = ear.convert.foldToSvg(graph);\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-bird-base-3d.svg\", serializer.serializeToString(svg));\n});\n\ntest(\"foldToSvg, custom methods on SVG element\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst svg = ear.convert.foldToSvg(graph);\n\tsvg.fill(\"red\");\n\tsvg.stroke(\"blue\");\n\tsvg.strokeDasharray(\"0.1\");\n\tsvg.circle(0.5, 0.5, 0.5).fill(\"#f003\").stroke(\"purple\");\n\tconst serializer = new xmldom.XMLSerializer();\n\tfs.writeFileSync(\"./tests/tmp/svg-crane-folded-style.svg\", serializer.serializeToString(svg));\n});\n"
  },
  {
    "path": "tests/convert.objToFold.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"convert objToFold no param\", () => {\n\tlet error;\n\ttry {\n\t\tear.convert.objToFold();\n\t} catch (err) {\n\t\terror = err;\n\t}\n\texpect(error).not.toBe(undefined);\n});\n\ntest(\"convert objToFold empty\", () => {\n\tear.convert.objToFold(\"\");\n\texpect(true).toBe(true);\n});\n\ntest(\"convert objToFold sphere with holes\", () => {\n\tconst sphere = fs.readFileSync(\"./tests/files/obj/sphere-with-holes.obj\", \"utf-8\");\n\tconst result = ear.convert.objToFold(sphere);\n\n\texpect(result.vertices_coords.length).toBe(39);\n\texpect(result.edges_vertices.length).toBe(102);\n\texpect(result.faces_vertices.length).toBe(62);\n\n\tconst boundaries = result.edges_assignment\n\t\t.filter(a => a === \"B\" || a === \"b\");\n\texpect(boundaries.length > 0 && boundaries.length < 102).toBe(true);\n});\n"
  },
  {
    "path": "tests/convert.opxToFold.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\n// this fails and will throw an error even inside the testing environment\n// test(\"convert opxToFold no param\", () => {\n// \tlet error;\n// \ttry {\n// \t\tear.convert.opxToFold();\n// \t} catch (err) {\n// \t\terror = err;\n// \t}\n// \texpect(error).not.toBe(undefined);\n// });\n\ntest(\"opxToFold, empty xml\", () => {\n\texpect(ear.convert.opxToFold(\"<void></void>\")).toBeUndefined();\n});\n\ntest(\"opxToFold, almost empty\", () => {\n\tear.convert.opxToFold(\"<void></void>\");\n\texpect(true).toBe(true);\n});\n\ntest(\"opxToFold, invalid\", () => {\n\tear.convert.opxToFold(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"1.5.0_13\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\"></object>\n</java>\n`);\n\texpect(true).toBe(true);\n});\n\ntest(\"opxToFold, file metadata\", () => {\n\tconst oneCreaseOPX = fs.readFileSync(\"./tests/files/opx/one-crease.opx\", \"utf-8\");\n\texpect(ear.convert.opxToFold(oneCreaseOPX)).toMatchObject({\n\t\tfile_spec: 1.2,\n\t\tfile_creator: \"Rabbit Ear\",\n\t\tfile_classes: [\"singleModel\"],\n\t\tframe_classes: [\"creasePattern\"],\n\t\tfile_title: \"one single crease\",\n\t\tfile_author: \"Kraft, traditional\",\n\t\tfile_description: \"no references, this is a square with one diagonal crease\",\n\t});\n});\n\ntest(\"opxToFold, test file\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/opx/test.opx\", \"utf-8\");\n\tconst result = ear.convert.opxToFold(testfile);\n\texpect(result.vertices_coords.length).toBe(8);\n\texpect(result.edges_vertices.length).toBe(10);\n\texpect(result.faces_vertices.length).toBe(3);\n\tconst boundaries = result.edges_assignment.filter(a => a === \"B\" || a === \"b\");\n\texpect(boundaries.length > 0 && boundaries.length < result.edges_vertices.length)\n\t\t.toBe(true);\n});\n\ntest(\"opxToFold, old OPX file format\", () => {\n\tconst birdBase = fs.readFileSync(\"./tests/files/opx/bird-base-2012.opx\", \"utf-8\");\n\tconst result = ear.convert.opxToFold(birdBase);\n\texpect(result.vertices_coords.length).toBe(9);\n\texpect(result.edges_vertices.length).toBe(20);\n\texpect(result.faces_vertices.length).toBe(12);\n\tconst boundaries = result.edges_assignment.filter(a => a === \"B\" || a === \"b\");\n\texpect(boundaries.length > 0 && boundaries.length < result.edges_vertices.length)\n\t\t.toBe(true);\n});\n\ntest(\"opxToFold, bird base\", () => {\n\tconst birdBase = fs.readFileSync(\"./tests/files/opx/bird-base.opx\", \"utf-8\");\n\tconst result = ear.convert.opxToFold(birdBase);\n\texpect(result.vertices_coords.length).toBe(13);\n\texpect(result.edges_vertices.length).toBe(28);\n\texpect(result.faces_vertices.length).toBe(16);\n\tconst boundaries = result.edges_assignment.filter(a => a === \"B\" || a === \"b\");\n\texpect(boundaries.length > 0 && boundaries.length < result.edges_vertices.length)\n\t\t.toBe(true);\n});\n"
  },
  {
    "path": "tests/convert.svgToFold.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"convert svgToFold empty svg\", () => {\n\tear.convert.svgToFold(\"<svg xmlns='http://www.w3.org/2000/svg'></svg>\");\n\texpect(true).toBe(true);\n});\n\ntest(\"convert svgToFold, fish base\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/svg/fish.svg\", \"utf-8\");\n\tconst result = ear.convert.svgToFold(testfile);\n\texpect(result.vertices_coords.length).toBe(11);\n\texpect(result.edges_vertices.length).toBe(22);\n\texpect(result.faces_vertices.length).toBe(12);\n\tconst boundaries = result.edges_assignment.filter(a => a === \"B\" || a === \"b\");\n\texpect(boundaries.length).toBe(8);\n});\n\ntest(\"convert svgToFold, crane all assignments\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/svg/crane-cp-all-assignments.svg\", \"utf-8\");\n\tconst result = ear.convert.svgToFold(testfile);\n\texpect(result.vertices_coords.length).toBe(68);\n\texpect(result.edges_vertices.length).toBe(187);\n\texpect(result.faces_vertices.length).toBe(120);\n\tconst b = result.edges_assignment.filter(a => a === \"B\" || a === \"b\");\n\tconst m = result.edges_assignment.filter(a => a === \"M\" || a === \"m\");\n\tconst v = result.edges_assignment.filter(a => a === \"V\" || a === \"v\");\n\tconst f = result.edges_assignment.filter(a => a === \"F\" || a === \"f\");\n\tconst u = result.edges_assignment.filter(a => a === \"U\" || a === \"u\");\n\tconst j = result.edges_assignment.filter(a => a === \"J\" || a === \"j\");\n\tconst c = result.edges_assignment.filter(a => a === \"C\" || a === \"c\");\n\texpect(b.length).toBe(14);\n\texpect(m.length).toBe(61);\n\texpect(v.length).toBe(42);\n\texpect(f.length).toBe(8);\n\texpect(u.length).toBe(14);\n\texpect(j.length).toBe(46);\n\texpect(c.length).toBe(2);\n});\n\ntest(\"convert svgToFold, crane all assignments\", () => {\n\tconst svg01 = fs.readFileSync(\"./tests/files/svg/backslash-01.svg\", \"utf-8\");\n\tconst svg02 = fs.readFileSync(\"./tests/files/svg/backslash-02.svg\", \"utf-8\");\n\tconst svg03 = fs.readFileSync(\"./tests/files/svg/backslash-03.svg\", \"utf-8\");\n\tconst svg04 = fs.readFileSync(\"./tests/files/svg/backslash-04.svg\", \"utf-8\");\n\tconst svg05 = fs.readFileSync(\"./tests/files/svg/backslash-05.svg\", \"utf-8\");\n\tconst svg06 = fs.readFileSync(\"./tests/files/svg/backslash-06.svg\", \"utf-8\");\n\tconst svg07 = fs.readFileSync(\"./tests/files/svg/backslash-07.svg\", \"utf-8\");\n\tconst svg08 = fs.readFileSync(\"./tests/files/svg/backslash-08.svg\", \"utf-8\");\n\tconst svg09 = fs.readFileSync(\"./tests/files/svg/backslash-09.svg\", \"utf-8\");\n\tconst svg10 = fs.readFileSync(\"./tests/files/svg/backslash-10.svg\", \"utf-8\");\n\tconst res01 = ear.convert.svgToFold(svg01);\n\tconst res02 = ear.convert.svgToFold(svg02);\n\tconst res03 = ear.convert.svgToFold(svg03);\n\tconst res04 = ear.convert.svgToFold(svg04);\n\tconst res05 = ear.convert.svgToFold(svg05);\n\tconst res06 = ear.convert.svgToFold(svg06);\n\tconst res07 = ear.convert.svgToFold(svg07);\n\tconst res08 = ear.convert.svgToFold(svg08);\n\tconst res09 = ear.convert.svgToFold(svg09);\n\tconst res10 = ear.convert.svgToFold(svg10);\n\texpect(res01.vertices_coords.length).toBe(7);\n\texpect(res01.edges_vertices.length).toBe(10);\n\texpect(res01.faces_vertices.length).toBe(4);\n\n\texpect(res02.vertices_coords.length).toBe(7);\n\texpect(res02.edges_vertices.length).toBe(10);\n\texpect(res02.faces_vertices.length).toBe(4);\n\n\texpect(res03.vertices_coords.length).toBe(7);\n\texpect(res03.edges_vertices.length).toBe(10);\n\texpect(res03.faces_vertices.length).toBe(4);\n\n\texpect(res04.vertices_coords.length).toBe(7);\n\texpect(res04.edges_vertices.length).toBe(10);\n\texpect(res04.faces_vertices.length).toBe(4);\n\n\texpect(res05.vertices_coords.length).toBe(7);\n\texpect(res05.edges_vertices.length).toBe(10);\n\texpect(res05.faces_vertices.length).toBe(4);\n\n\texpect(res06.vertices_coords.length).toBe(7);\n\texpect(res06.edges_vertices.length).toBe(10);\n\texpect(res06.faces_vertices.length).toBe(4);\n\n\t// expect(res07.vertices_coords.length).toBe(7);\n\t// expect(res07.edges_vertices.length).toBe(10);\n\t// expect(res07.faces_vertices.length).toBe(4);\n\n\texpect(res08.vertices_coords.length).toBe(7);\n\texpect(res08.edges_vertices.length).toBe(10);\n\texpect(res08.faces_vertices.length).toBe(4);\n\n\texpect(res09.vertices_coords.length).toBe(7);\n\texpect(res09.edges_vertices.length).toBe(10);\n\texpect(res09.faces_vertices.length).toBe(4);\n\n\texpect(res10.vertices_coords.length).toBe(7);\n\texpect(res10.edges_vertices.length).toBe(10);\n\texpect(res10.faces_vertices.length).toBe(4);\n\n\texpect(res01.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res01.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res01.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\texpect(res02.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res02.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res02.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\texpect(res03.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res03.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res03.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\texpect(res04.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res04.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res04.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\texpect(res05.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res05.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res05.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\t// expect(res06.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\t// expect(res06.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\t// expect(res06.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\t// expect(res07.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\t// expect(res07.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\t// expect(res07.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\t// expect(res08.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\t// expect(res08.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\t// expect(res08.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\t// expect(res09.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\t// expect(res09.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\t// expect(res09.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n\n\texpect(res10.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(res10.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(2);\n\texpect(res10.edges_assignment.filter(a => a === \"F\" || a === \"f\").length).toBe(2);\n});\n\n// these fail (which is fine), but I can't seem to setup a\n// try-catch that won't break the testing environment.\n\n// test(\"convert svgToFold no param\", () => {\n// \tlet error;\n// \ttry {\n// \t\tear.convert.svgToFold();\n// \t} catch (err) {\n// \t\terror = err;\n// \t}\n// \texpect(error).not.toBe(undefined);\n// });\n\n// test(\"convert svgToFold empty\", () => {\n// \tear.convert.svgToFold(\"\");\n// \texpect(true).toBe(true);\n// });\n\n\n// <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-10 -10 20 20\" stroke-width=\"0.2\" fill=\"none\">\n// <line x1=\"0\" y1=\"9\" x2=\"0\" y2=\"7.5\" stroke=\"black\" />\n// <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" stroke=\"purple\" />\n// <rect x=\"0\" y=\"1\" width=\"6\" height=\"6\" stroke=\"red\" />\n// <rect x=\"0\" y=\"2\" width=\"4\" height=\"4\" stroke=\"green\" />\n// <rect x=\"0\" y=\"3\" width=\"2\" height=\"2\" stroke=\"blue\" />\n// <line x1=\"0\" y1=\"-1\" x2=\"0\" y2=\"2.5\" stroke=\"black\" />\n// </svg>\n\ntest(\"svgEdgeGraph, overlapping edges\", () => {\n\tconst svg = `<svg>\n\t\t<line x1=\"0\" y1=\"9\" x2=\"0\" y2=\"7.5\" stroke=\"black\" />\n\t\t<rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" stroke=\"purple\" />\n\t\t<rect x=\"0\" y=\"1\" width=\"6\" height=\"6\" stroke=\"red\" />\n\t\t<rect x=\"0\" y=\"2\" width=\"4\" height=\"4\" stroke=\"green\" />\n\t\t<rect x=\"0\" y=\"3\" width=\"2\" height=\"2\" stroke=\"blue\" />\n\t\t<line x1=\"0\" y1=\"-1\" x2=\"0\" y2=\"2.5\" stroke=\"black\" />\n\t</svg>`;\n\tconst graph = ear.convert.svgEdgeGraph(svg);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/svgEdgeGraph-overlapping-edges.fold\",\n\t\tJSON.stringify(graph, null, 2),\n\t\t\"utf8\",\n\t);\n});\n"
  },
  {
    "path": "tests/diagram.arrows.axiom.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\nconst nonPlanarShape = () => ({\n\tvertices_coords: [[1, 0], [3, 2], [0, 4], [-3, 3], [-1, 2], [-2, -2], [4, -1]],\n\tedges_vertices: [[0, 1], [2, 0], [3, 4], [4, 5], [6, 5], [1, 6], [2, 3]],\n\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\tfaces_vertices: [[0, 2, 3, 4, 5, 6, 1]]\n});\n\n/**\n * @param {FOLD} graph\n * @param {(VecLine2|[[number, number], [number, number]])[]} params\n * @param {VecLine2[]} resultLines\n * @param {object} arrows\n * @param {string} name\n */\nconst renderAxiomStepSVG = (graph, params, resultLines, arrows, name = \"\") => {\n\t// clip the lines in the boundary of the graph\n\t// const polygon = ear.graph.boundaryPolygon(graph);\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst vmax = Math.max(...ear.math.boundingBox(polygon).span)\n\n\t// create the svg from the graph\n\tconst svg = ear.convert.foldToSvg(graph, {\n\t\tstrokeWidth: 0.02,\n\t\tpadding: 0.1,\n\t});\n\tconst lineLayer = svg.g()\n\t\t.stroke(\"blue\")\n\t\t.strokeLinecap(\"round\")\n\t\t.strokeDasharray(vmax / 20);\n\tconst paramLayer = svg.g()\n\t\t.stroke(\"#8b3\")\n\t\t.fill(\"#8b3\");\n\tconst arrowLayer = svg.g()\n\t\t.stroke(\"black\");\n\n\t// draw the result lines from computing the axioms, these are the fold lines\n\tresultLines\n\t\t.map(line => ear.math.clipLineConvexPolygon(polygon, line))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(seg => lineLayer.line(...seg));\n\n\t// draw the input parameters to the axiom. this includes points and lines\n\tparams\n\t\t.filter(param => param.constructor === Array)\n\t\t.forEach(param => paramLayer.circle(vmax / 66).center(param[0], param[1]));\n\tparams\n\t\t.filter(param => param.vector)\n\t\t.map(param => ear.math.clipLineConvexPolygon(polygon, param))\n\t\t.filter(a => a !== undefined)\n\t\t.forEach(segment => paramLayer.line(segment[0], segment[1]));\n\n\t// draw an arrow by passing the object into the constructor layer.arrow()\n\tarrows.forEach(arrow => arrowLayer.arrow(arrow));\n\n\t// serialize to string and write file\n\tconst serialzer = new xmldom.XMLSerializer();\n\tconst string = serialzer.serializeToString(svg);\n\tfs.writeFileSync(`./tests/tmp/${name}.svg`, string);\n};\n\ntest(\"axiom1Arrows, square A\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [[0, 0], [1, 1]];\n\tconst axiomResults = ear.axiom.axiom1InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom1Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-1-A\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom1Arrows, square B\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [[0.45, 0.45], [0.55, 0.55]];\n\tconst axiomResults = ear.axiom.axiom1InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom1Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-1-B\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom2Arrows, square A\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [[0, 0], [1, 1]];\n\tconst axiomResults = ear.axiom.axiom2InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom2Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-2-A\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: 0.3,\n\t}));\n});\n\ntest(\"axiom2Arrows, square B\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [[0.45, 0.45], [0.55, 0.55]];\n\tconst axiomResults = ear.axiom.axiom2InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom2Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-2-B\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: 0.3,\n\t}));\n});\n\n// test(\"axiom3Arrows, square A\", () => {\n// \t// input lines intersect at the corner, only one axiom solution\n// \tconst graph = ear.graph.square();\n// \tconst polygon = ear.graph.boundaryPolygon(graph);\n// \tconst params = [{ vector: [1, 0], origin: [0, 0] }, { vector: [1, 1], origin: [0, 0] }];\n// \tconst axiomResults = ear.axiom.axiom3InPolygon(polygon, ...params);\n// \tconst arrowResults = ear.diagram.axiom3Arrows(graph, ...params);\n// \trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-3-A\");\n// \texpect(arrowResults).toHaveLength(2);\n// \tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n// \t\thead: { width: 0.0666, height: 0.1 },\n// \t\tbend: -0.3,\n// \t}));\n// });\n\ntest(\"axiom3Arrows, square B\", () => {\n\t// input lines intersect in the middle, two axiom solutions\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [1, 0], origin: [0, 0.5] }, { vector: [1, 1], origin: [0, 0] }];\n\tconst axiomResults = ear.axiom.axiom3InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom3Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-3-B\");\n\texpect(arrowResults).toHaveLength(2);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom3Arrows, square C\", () => {\n\t// input lines are parallel\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [1, 1], origin: [0, 0] }, { vector: [1, 1], origin: [0, 0.5] }];\n\tconst axiomResults = ear.axiom.axiom3InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom3Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-3-C\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom4Arrows, square\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [1, 1], origin: [0, 0] }, [0.5, 1]];\n\tconst axiomResults = ear.axiom.axiom4InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom4Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-4\");\n\texpect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom5Arrows, square A\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [0, 1], origin: [0.5, 0.5] }, [0, 0], [1, 0]];\n\tconst axiomResults = ear.axiom.axiom5InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom5Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-5-A\");\n\t// expect(arrowResults).toHaveLength(1);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom5Arrows, square B\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [0, 1], origin: [0.5, 0.5] }, [0, 0.5], [0.6, 0.25]];\n\tconst axiomResults = ear.axiom.axiom5InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom5Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-5-B\");\n\texpect(arrowResults).toHaveLength(2);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom5Arrows, square C\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [{ vector: [0, 1], origin: [0.5, 0.5] }, [0, 0.5], [0.75, 0.25]];\n\tconst axiomResults = ear.axiom.axiom5InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom5Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-5-C\");\n\t// expect(arrowResults).toHaveLength(0);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}));\n});\n\ntest(\"axiom6Arrows, square\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t\t{ vector: [1, 0], origin: [0, 1] },\n\t\t[0.75, 0],\n\t\t[0, 0.75],\n\t];\n\tconst axiomResults = ear.axiom.axiom6InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom6Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-6\");\n\texpect(arrowResults).toHaveLength(6);\n\tarrowResults.forEach(arrow => expect(arrow).toMatchObject({\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: 0.3,\n\t}));\n});\n\ntest(\"axiom7Arrows, square A\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [\n\t\t{ vector: [1, 0], origin: [0.5, 0.25] },\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[0.5, 0.0],\n\t];\n\tconst axiomResults = ear.axiom.axiom7InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom7Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-7-A\");\n\texpect(arrowResults).toHaveLength(2);\n\texpect(arrowResults).toMatchObject([{\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: 0.3,\n\t}, {\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t}]);\n});\n\ntest(\"axiom7Arrows, square B\", () => {\n\tconst graph = ear.graph.square();\n\tconst polygon = ear.graph.boundaryPolygon(graph);\n\tconst params = [\n\t\t{ vector: [1, 0], origin: [0.5, 0.5] },\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[0.75, 0.0],\n\t];\n\tconst axiomResults = ear.axiom.axiom7InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom7Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-square-7-B\");\n\t// expect(arrowResults).toHaveLength(0);\n});\n\ntest(\"axiom1Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [[0, 2], [3, 1]];\n\tconst axiomResults = ear.axiom.axiom1InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom1Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-1\");\n});\n\ntest(\"axiom2Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [[0, 2], [3, 1]];\n\tconst axiomResults = ear.axiom.axiom2InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom2Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-2\");\n});\n\n// test(\"axiom3Arrows, non-convex\", () => {\n// \tconst graph = nonPlanarShape();\n// \tconst polygon = ear.math.convexHull(graph.vertices_coords)\n// \t\t.map(i => graph.vertices_coords[i]);\n// \tconst params = [{ vector: [1, 0], origin: [0, 0] }, { vector: [1, 1], origin: [0, 0] }];\n// \tconst axiomResults = ear.axiom.axiom3InPolygon(polygon, ...params);\n// \tconst arrowResults = ear.diagram.axiom3Arrows(graph, ...params);\n// \trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-3\");\n// });\n\ntest(\"axiom4Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [{ vector: [1, 1], origin: [0, 0] }, [0.5, 1]];\n\tconst axiomResults = ear.axiom.axiom4InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom4Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-4\");\n});\n\ntest(\"axiom5Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [{ vector: [0, 1], origin: [0.5, 0.5] }, [0, 0], [1, 0]];\n\tconst axiomResults = ear.axiom.axiom5InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom5Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-5\");\n});\n\ntest(\"axiom6Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t\t{ vector: [1, 0], origin: [0, 1] },\n\t\t[0.75, 0],\n\t\t[0, 0.75],\n\t];\n\tconst axiomResults = ear.axiom.axiom6InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom6Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-6\");\n});\n\ntest(\"axiom7Arrows, non-convex\", () => {\n\tconst graph = nonPlanarShape();\n\tconst polygon = ear.math.convexHull(graph.vertices_coords)\n\t\t.map(i => graph.vertices_coords[i]);\n\tconst params = [\n\t\t{ vector: [1, 0], origin: [0.5, 0.5] },\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[0.75, 0.25],\n\t];\n\tconst axiomResults = ear.axiom.axiom7InPolygon(polygon, ...params);\n\tconst arrowResults = ear.diagram.axiom7Arrows(graph, ...params);\n\trenderAxiomStepSVG(graph, params, axiomResults, arrowResults, \"svg-axiom-non-convex-7\");\n});\n"
  },
  {
    "path": "tests/diagram.arrows.simple.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"arrowFromLine with options\", () => {\n\tconst polygon = ear.graph.square().vertices_coords;\n\tconst line = { vector: [0.4, 0.8], origin: [0.5, 0.5] }\n\tear.diagram.arrowFromLine(polygon, line);\n});\n\ntest(\"segmentToArrow with options\", () => {\n\tconst polygon = ear.graph.square().vertices_coords;\n\tconst segment = [[0.4, 0.8], [0.5, 0.5]];\n\tear.diagram.arrowFromSegment(polygon, segment);\n});\n\ntest(\"foldLineArrow\", () => {\n\tconst graph = ear.graph.square();\n\tconst foldLine = { vector: [1, 1], origin: [0, 0] };\n\tconst options = {};\n\tconst arrow = ear.diagram.foldLineArrow(graph, foldLine, options);\n\texpect(arrow).toMatchObject({\n\t\tsegment: [[1, 0], [0, 1]],\n\t\thead: { width: 0.0666, height: 0.1 },\n\t\tbend: -0.3,\n\t});\n\texpect(arrow.padding).toBeCloseTo(0.07071067811865477);\n});\n"
  },
  {
    "path": "tests/diagram.general.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"arrowFromSegmentInPolygon\", () => {\n\tconst polygon = [[0, 0], [5, 0], [5, 5], [0, 5]];\n\tconst segment = [[0.5, 0.5], [1, 2]];\n\tconst arrow = ear.diagram.arrowFromSegmentInPolygon(polygon, segment);\n\n\texpect(arrow).toMatchObject({\n\t\tsegment: [[0.5, 0.5], [1, 2]],\n\t\thead: { width: 0.333, height: 0.5 },\n\t\tbend: 0.3,\n\t});\n\texpect(arrow.padding).toBeCloseTo(0.0790569415042095);\n});\n"
  },
  {
    "path": "tests/docs.js",
    "content": "// this is no longer needed, the package.json contains a simple one line script\n\n// import fs from \"fs\";\n// import TypeDoc from \"typedoc\";\n\n// // const prepareCache = () => {\n// // \tfs.rmSync(\"./tmp\", { recursive: true, force: true });\n// // \t// fs.rmSync(\"./docs\", { recursive: true, force: true });\n// // \tfs.mkdirSync(\"./tmp\");\n// // \t// fs.mkdirSync(\"./docs\");\n// // };\n\n// const outputDir = \"./docs\";\n// // const entryPoints = [\"./src/index.js\"];\n// const entryPoints = [\"./src/*\"];\n\n// const makeDocs = async () => {\n// \t// prepareCache();\n// \tconst app = await TypeDoc.Application.bootstrapWithPlugins({ entryPoints });\n// \tconst project = await app.convert();\n// \tif (project) {\n// \t\t// Rendered docs\n// \t\tawait app.generateDocs(project, outputDir);\n// \t\t// Alternatively generate JSON output\n// \t\tawait app.generateJson(project, `${outputDir}/documentation.json`);\n// \t}\n// };\n\n// makeDocs();\n"
  },
  {
    "path": "tests/environment.window.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\nimport window from \"../src/environment/window.js\";\n\near.window = xmldom;\n\ntest(\"window, document methods\", () => {\n\texpect(window).toBeDefined();\n\texpect(typeof window).toBe(\"function\");\n\n\texpect(window().DOMImplementation).toBeDefined();\n\texpect(window().XMLSerializer).toBeDefined();\n\texpect(window().DOMParser).toBeDefined();\n\texpect(window().document).toBeDefined();\n});\n\ntest(\"document methods\", () => {\n\tconst documentProtoMethods = [\n\t\t\"insertBefore\",\n\t\t\"removeChild\",\n\t\t\"replaceChild\",\n\t\t\"importNode\",\n\t\t\"getElementById\",\n\t\t\"getElementsByClassName\",\n\t\t\"createElement\",\n\t\t\"createDocumentFragment\",\n\t\t\"createTextNode\",\n\t\t\"createComment\",\n\t\t\"createCDATASection\",\n\t\t\"createProcessingInstruction\",\n\t\t\"createAttribute\",\n\t\t\"createEntityReference\",\n\t\t\"createElementNS\",\n\t\t\"createAttributeNS\",\n\t\t\"constructor\",\n\t\t\"getElementsByTagName\",\n\t\t\"getElementsByTagNameNS\",\n\t];\n\n\tconst documentMethods = [\n\t\t// \"documentURI\",\n\t\t\"ownerDocument\", \"implementation\", \"childNodes\", \"doctype\",\n\t\t\"firstChild\", \"lastChild\", \"_inc\", \"documentElement\",\n\t];\n\n\tdocumentProtoMethods.forEach(name => {\n\t\texpect(typeof window().document[name]).toBe(\"function\");\n\t\texpect(window().document[name]).toBeDefined();\n\t});\n\n\tdocumentMethods.forEach(name => {\n\t\texpect(window().document[name]).toBeDefined();\n\t});\n\n\t// as of today, these methods are not available.\n\t// if this test fails, it will be a great day!\n\texpect(window().document.evaluate).not.toBeDefined();\n\texpect(window().document.querySelector).not.toBeDefined();\n});\n\n/**\n * this works even when Rabbit Ear is built without SVG library,\n * and it uses svgMini instead.\n */\ntest(\"window, svg mini library\", () => {\n\t// const svg = ear.svg();\n\tconst p = ear.svg.path(\"M1 2L3 4L-5 6z\");\n\tp.setAttribute(\"stroke\", \"red\");\n\texpect(p.getAttribute(\"d\")).toBe(\"M1 2L3 4L-5 6z\");\n\texpect(p.getAttribute(\"stroke\")).toBe(\"red\");\n});\n/**\n * this ONLY WORKS when the SVG library is included.\n */\ntest(\"window, svg library\", () => {\n\tconst svg = ear.svg();\n\tconst p = svg.path()\n\t\t.Move(1, 2)\n\t\t.Line(3, 4)\n\t\t.Line(-5, 6)\n\t\t.close()\n\t\t.stroke(\"red\");\n\texpect(p.getAttribute(\"d\")).toBe(\"M1 2L3 4L-5 6z\");\n\texpect(p.getAttribute(\"stroke\")).toBe(\"red\");\n});\n"
  },
  {
    "path": "tests/files/cp/bird-base.cp",
    "content": "2 -200.0 200.0 -6.866836743578443E-14 117.15728752538102\n3 -117.15728752538104 2.1805418562157054E-14 0.0 -117.157287525381\n2 200.0 200.0 -5.445751272058243E-14 117.15728752538097\n2 -200.0 -200.0 -2.4731605323635646E-16 -117.15728752538098\n1 -200.0 200.0 -200.0 0.0\n1 -200.0 -200.0 0.0 -200.0\n1 200.0 200.0 200.0 0.0\n1 -200.0 200.0 0.0 200.0\n3 117.15728752538102 -3.1900472378611834E-14 -2.425944645077817E-14 117.15728752538101\n2 0.0 -4.022499833786352E-14 0.0 117.15728752538098\n2 -200.0 -200.0 -117.157287525381 -2.260350242284923E-14\n3 200.0 -200.0 3.6415315207705135E-14 -3.6415315207705135E-14\n2 3.6415315207705135E-14 -3.6415315207705135E-14 -117.15728752538098 -2.2603502422849217E-14\n1 -200.0 -200.0 -200.0 0.0\n1 200.0 -200.0 0.0 -200.0\n1 200.0 -200.0 200.0 0.0\n1 200.0 200.0 0.0 200.0\n2 -200.0 200.0 -117.15728752538101 2.180541856215705E-14\n2 0.0 -1.4210854715202007E-14 117.157287525381 -5.886328755950317E-15\n2 200.0 200.0 117.157287525381 -5.886328755950317E-15\n2 200.0 -200.0 117.157287525381 -5.886328755950317E-15\n2 0.0 -3.641531520770513E-14 0.0 -117.157287525381\n3 3.6415315207705135E-14 -3.6415315207705135E-14 -200.0 200.0\n2 200.0 -200.0 0.0 -117.157287525381\n"
  },
  {
    "path": "tests/files/cp/square-fish.cp",
    "content": "1 200.0 -200.0 200.0 -82.84271247461493\n1 -200.0 -200.0 -200.0 -82.84271247461493\n1 -200.0 -82.84271247461493 -200.0 -34.314575050757206\n2 -200.0 -34.314575050757206 -82.84271247461491 -82.84271247461491\n1 200.0 -82.84271247461493 200.0 34.314575050753774\n1 200.0 -200.0 82.84271247461493 -200.0\n1 200.0 200.0 82.84271247461493 200.0\n1 200.0 200.0 200.0 34.314575050753774\n1 -200.0 -200.0 -34.314575050753774 -200.0\n1 82.84271247461493 200.0 34.314575050757206 200.0\n3 82.84271247461491 -200.0 82.84271247461493 -82.8427124746313\n2 82.84271247461493 -82.8427124746313 200.0 34.314575050753774\n3 -200.0 200.0 82.84271247461491 82.84271247461491\n3 -200.0 200.0 -82.84271247461491 -82.84271247461491\n3 -200.0 -200.0 -82.84271247461491 -82.84271247461491\n3 82.84271247461493 -82.8427124746313 82.84271247461491 82.84271247461491\n3 200.0 -82.84271247461491 82.84271247463128 -82.84271247461493\n3 82.84271247463136 -82.84271247461493 -82.84271247461491 -82.84271247461491\n1 -200.0 200.0 -200.0 -34.314575050757206\n1 -200.0 200.0 34.314575050757206 200.0\n2 -34.314575050753774 -200.0 82.84271247461493 -82.8427124746313\n2 82.84271247461491 82.84271247461491 34.314575050757206 200.0\n1 82.84271247461493 -200.0 -34.314575050753774 -200.0\n3 200.0 200.0 82.84271247461491 82.84271247461491\n"
  },
  {
    "path": "tests/files/cp/windmill.cp",
    "content": "2 -200.0 0.0 -100.0 -100.0\n2 0.0 200.0 -100.0 100.0\n3 -200.0 -200.0 -100.0 -100.0\n3 200.0 -200.0 100.0 -100.0\n3 200.0 200.0 100.0 100.0\n3 100.0 -100.0 -100.0 -100.0\n3 100.0 -100.0 100.0 100.0\n3 -100.0 100.0 -100.0 -100.0\n3 -200.0 200.0 -100.0 100.0\n3 -100.0 100.0 100.0 100.0\n1 -200.0 -200.0 0.0 -200.0\n1 200.0 200.0 200.0 0.0\n1 -200.0 200.0 0.0 200.0\n1 -200.0 200.0 -200.0 0.0\n2 0.0 -200.0 100.0 -100.0\n2 200.0 0.0 100.0 100.0\n1 200.0 -200.0 0.0 -200.0\n1 200.0 -200.0 200.0 0.0\n1 200.0 200.0 0.0 200.0\n1 -200.0 -200.0 -200.0 0.0\n"
  },
  {
    "path": "tests/files/fold/abstract-graph.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_attributes\": [\"abstract\"],\n\t\"frame_title\": \"abstract graph\",\n\t\"edges_vertices\": [[8,9], [5,9], [0,5], [0,8], [5,7], [9,10], [2,10], [2,6], [6,7], [3,6], [3,7], [1,8], [1,4], [4,9], [4,10]],\n\t\"faces_vertices\": [[8,9,5,0], [7,5,9,10,2,6], [6,3,7], [8,1,4,9], [9,4,10]],\n\t\"faces_edges\": [[0,1,2,3], [4,1,5,6,7,8], [9,10,8], [11,12,13,0], [13,14,5]]\n}"
  },
  {
    "path": "tests/files/fold/bad-edges.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_title\": \"graph with bad edges\",\n\t\"vertices_coords\": [[0,0], [1,0], [1,1], [0,1], [0.5,0.5]],\n\t\"edges_vertices\": [[0,1], [1,2], [4, 4], [2,3], [3,0], [0,4], [4,3], [4,0], [4,2], [1,4]],\n\t\"edges_assignment\": [\"B\", \"B\", \"U\", \"B\", \"B\", \"V\", \"M\", \"V\", \"M\"],\n\t\"edges_foldAngle\": [0, 30, -45, 0, 0, -90, 90, 0, 0],\n\t\"faces_vertices\": [[0,1,4], [1,2,4], [2,3,4], [3,0,4]]\n}\n"
  },
  {
    "path": "tests/files/fold/bird-base-3d-cp.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"bird base 3D\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[1,1],[0,1],[0.5,0.5],[0,0.5],[1,0.5],[0.5,1],[0.5,0],[0.5,0.2071067811865475],[0.7928932188134525,0.5],[0.5,0.7928932188134525],[0.2071067811865475,0.5]\n\t],\n\t\"edges_vertices\":[\n\t\t[2,4],[4,0],[1,4],[4,3],[3,5],[5,0],[1,6],[6,2],[2,7],[7,3],[0,8],[8,1],[8,9],[9,4],[0,9],[6,10],[10,4],[1,10],[4,11],[11,7],[2,11],[4,12],[12,5],[3,12],[12,0],[9,1],[10,2],[11,3]\n\t],\n\t\"edges_assignment\":[\n\t\t\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t15.953245421827488,15.953219805035081,15.953114036239375,15.953199003501613,0,0,0,0,0,0,0,0,38.59487786356588,-38.50309226545691,-84.81762354401174,38.594901255859405,-38.50315489586749,-84.8176768689456,-38.50316941048558,38.594890005406434,-84.81767917727996,-38.50319148002102,38.5949709327751,-84.81776495099888,-84.8177673867906,-84.81762306260427,-84.81767408676271,-84.81768225446281\n\t],\n\t\"faces_vertices\":[\n\t\t[4,12,0],[12,5,0],[4,9,1],[9,4,0],[9,0,8],[9,8,1],[6,10,1],[10,6,2],[10,2,4],[10,4,1],[2,11,4],[11,2,7],[11,7,3],[11,3,4],[3,5,12],[3,12,4]],\"faces_edges\":[[24,1,21],[5,24,22],[25,2,13],[1,14,13],[10,12,14],[11,25,12],[17,6,15],[7,26,15],[0,16,26],[2,17,16],[18,0,20],[8,19,20],[9,27,19],[3,18,27],[22,23,4],[21,3,23]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"frame_attributes\": [\"3D\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0,0],\n\t\t\t[0.9346209175670507,-0.04333363833467212,-0.1260682968362098],\n\t\t\t[0.9346205765489879,0.8467131796034029,-0.43691523838099755],\n\t\t\t[0,0.8912867402155733,-0.3112791141908985],\n\t\t\t[0.5,0.5,0],\n\t\t\t[0.1608095607890277,0.433390498961924,-0.1905595984682338],\n\t\t\t[0.7692278131266708,0.4051810911838078,-0.2726276991566967],\n\t\t\t[0.46528351453278666,0.7092268990725705,-0.3328948322877804],\n\t\t\t[0.4651768887644341,0.12902291207529834,-0.13024419495336886],\n\t\t\t[0.4943598174831106,0.21274696370343693,0.05692380087877624],\n\t\t\t[0.7812133568337621,0.473381585788132,-0.07743994828276657],\n\t\t\t[0.4943598320965127,0.7602349554348762,-0.13428475007510515],\n\t\t\t[0.2071067811865475,0.5,0]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/bird-base-3d.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"bird base 3D\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"frame_attributes\": [\"3D\"],\n\t\"vertices_coords\":[\n\t\t[0,0,0],\n\t\t[0.9346209175670507,-0.04333363833467212,-0.1260682968362098],\n\t\t[0.9346205765489879,0.8467131796034029,-0.43691523838099755],\n\t\t[0,0.8912867402155733,-0.3112791141908985],\n\t\t[0.5,0.5,0],\n\t\t[0.1608095607890277,0.433390498961924,-0.1905595984682338],\n\t\t[0.7692278131266708,0.4051810911838078,-0.2726276991566967],\n\t\t[0.46528351453278666,0.7092268990725705,-0.3328948322877804],\n\t\t[0.4651768887644341,0.12902291207529834,-0.13024419495336886],\n\t\t[0.4943598174831106,0.21274696370343693,0.05692380087877624],\n\t\t[0.7812133568337621,0.473381585788132,-0.07743994828276657],\n\t\t[0.4943598320965127,0.7602349554348762,-0.13428475007510515],\n\t\t[0.2071067811865475,0.5,0]\n\t],\n\t\"edges_vertices\":[\n\t\t[2,4],[4,0],[1,4],[4,3],[3,5],[5,0],[1,6],[6,2],[2,7],[7,3],[0,8],[8,1],[8,9],[9,4],[0,9],[6,10],[10,4],[1,10],[4,11],[11,7],[2,11],[4,12],[12,5],[3,12],[12,0],[9,1],[10,2],[11,3]\n\t],\n\t\"edges_assignment\":[\n\t\t\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t15.953245421827488,15.953219805035081,15.953114036239375,15.953199003501613,0,0,0,0,0,0,0,0,38.59487786356588,-38.50309226545691,-84.81762354401174,38.594901255859405,-38.50315489586749,-84.8176768689456,-38.50316941048558,38.594890005406434,-84.81767917727996,-38.50319148002102,38.5949709327751,-84.81776495099888,-84.8177673867906,-84.81762306260427,-84.81767408676271,-84.81768225446281\n\t],\n\t\"faces_vertices\":[\n\t\t[4,12,0],[12,5,0],[4,9,1],[9,4,0],[9,0,8],[9,8,1],[6,10,1],[10,6,2],[10,2,4],[10,4,1],[2,11,4],[11,2,7],[11,7,3],[11,3,4],[3,5,12],[3,12,4]],\"faces_edges\":[[24,1,21],[5,24,22],[25,2,13],[1,14,13],[10,12,14],[11,25,12],[17,6,15],[7,26,15],[0,16,26],[2,17,16],[18,0,20],[8,19,20],[9,27,19],[3,18,27],[22,23,4],[21,3,23]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"creasePattern\"],\n\t\t\"frame_attributes\": [\"2D\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0],[1,0],[1,1],[0,1],[0.5,0.5],[0,0.5],[1,0.5],[0.5,1],[0.5,0],[0.5,0.2071067811865475],[0.7928932188134525,0.5],[0.5,0.7928932188134525],[0.2071067811865475,0.5]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/bird-disjoint-edges.fold",
    "content": "{\n\t\"file_description\":\"all edges are isolated. all vertices have only 1 adjacent vertex. no faces.\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],\n\t\t[0.5,0],\n\t\t[0.5,0],\n\t\t[1,0],\n\t\t[1,0],\n\t\t[1,0.5],\n\t\t[1,0.5],\n\t\t[1,1],\n\t\t[1,1],\n\t\t[0.5,1],\n\t\t[0.5,1],\n\t\t[0,1],\n\t\t[0,1],\n\t\t[0,0.5],\n\t\t[0,0.5],\n\t\t[0,0],\n\t\t[0,0],\n\t\t[0.5,0.5],\n\t\t[1,0],\n\t\t[0.5,0.5],\n\t\t[1,1],\n\t\t[0.5,0.5],\n\t\t[0,1],\n\t\t[0.5,0.5],\n\t\t[0.5,0],\n\t\t[0.5,0.20710678118654757],\n\t\t[0.5,0.20710678118654757],\n\t\t[0.5,0.5],\n\t\t[1,0.5],\n\t\t[0.7928932188134524,0.5],\n\t\t[0.7928932188134524,0.5],\n\t\t[0.5,0.5],\n\t\t[0.5,1],\n\t\t[0.5,0.7928932188134524],\n\t\t[0.5,0.7928932188134524],\n\t\t[0.5,0.5],\n\t\t[0,0.5],\n\t\t[0.20710678118654757,0.5],\n\t\t[0.20710678118654757,0.5],\n\t\t[0.5,0.5],\n\t\t[0,0],\n\t\t[0.5,0.20710678118654757],\n\t\t[0.5,0.20710678118654757],\n\t\t[1,0],\n\t\t[1,0],\n\t\t[0.7928932188134524,0.5],\n\t\t[0.7928932188134524,0.5],\n\t\t[1,1],\n\t\t[1,1],\n\t\t[0.5,0.7928932188134524],\n\t\t[0.5,0.7928932188134524],\n\t\t[0,1],\n\t\t[0,1],\n\t\t[0.20710678118654757,0.5],\n\t\t[0.20710678118654757,0.5],\n\t\t[0,0],\n\t\t[0.20710678118654757,0.5],\n\t\t[0.5,0.20710678118654757],\n\t\t[0.5,0.7928932188134524],\n\t\t[0.7928932188134524,0.5],\n\t\t[0,0],\n\t\t[0.3535533905932738,0.3535533905932738],\n\t\t[1,1],\n\t\t[0.6464466094067263,0.6464466094067263],\n\t\t[0.5,0.7928932188134524],\n\t\t[0.6464466094067263,0.6464466094067263],\n\t\t[0.7928932188134524,0.5],\n\t\t[0.6464466094067263,0.6464466094067263],\n\t\t[0.5,0.5],\n\t\t[0.6464466094067263,0.6464466094067263],\n\t\t[0.20710678118654757,0.5],\n\t\t[0.3535533905932738,0.3535533905932738],\n\t\t[0.5,0.5],\n\t\t[0.3535533905932738,0.3535533905932738],\n\t\t[0.5,0.20710678118654757],\n\t\t[0.3535533905932738,0.3535533905932738]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[18,19],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,-180,-180,-180,180,-180,180,-180,180,-180,180,180,180,180,180,180,180,180,180,0,0,0,0,0,0,0,0\n\t]\n}"
  },
  {
    "path": "tests/files/fold/blintz-frames.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"blintz base\",\n\t\"file_author\": \"Kraft\",\n\t\"file_classes\": [\"singleModel\", \"animation\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[0.0, 0.0], [0.5, 0.0],\n\t\t[1.0, 0.0], [1.0, 0.5],\n\t\t[1.0, 1.0], [0.5, 1.0],\n\t\t[0.0, 1.0], [0.0, 0.5]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1], [1,2], [2,3], [3,4], [4,5], [5,6], [6,7], [7,0], [1,3], [3,5], [5,7], [7,1]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\"\n\t],\n\t\"faces_vertices\": [\n\t\t[0,1,7], [2,3,1], [4,5,3], [6,7,5], [1,3,5,7]\n\t],\n\t\"faceOrders\": [\n\t\t[0,4,1], [1,4,1], [2,4,1], [3,4,1]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"frame_attributes\": [\"3D\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0.0012038183319507678, 0.0012038183319507678, 0.02450428508239015], [0.5, 0.0, 0.0],\n\t\t\t[0.9987961816680493, 0.0012038183319507678, 0.02450428508239015], [1.0, 0.5, 0.0],\n\t\t\t[0.9987961816680493, 0.9987961816680493, 0.02450428508239015], [0.5, 1.0, 0.0],\n\t\t\t[0.0012038183319507678, 0.9987961816680493, 0.02450428508239015], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.004803679899192392, 0.004803679899192392, 0.04877258050403206], [0.5, 0.0, 0.0],\n\t\t\t[0.9951963201008076, 0.004803679899192392, 0.04877258050403206], [1.0, 0.5, 0.0],\n\t\t\t[0.9951963201008076, 0.9951963201008076, 0.04877258050403206], [0.5, 1.0, 0.0],\n\t\t\t[0.004803679899192392, 0.9951963201008076, 0.04877258050403206], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.010764916066947794, 0.010764916066947794, 0.07257116931361558], [0.5, 0.0, 0.0],\n\t\t\t[0.9892350839330522, 0.010764916066947794, 0.07257116931361558], [1.0, 0.5, 0.0],\n\t\t\t[0.9892350839330522, 0.9892350839330522, 0.07257116931361558], [0.5, 1.0, 0.0],\n\t\t\t[0.010764916066947794, 0.9892350839330522, 0.07257116931361558], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.019030116872178315, 0.019030116872178315, 0.09567085809127245], [0.5, 0.0, 0.0],\n\t\t\t[0.9809698831278217, 0.019030116872178315, 0.09567085809127245], [1.0, 0.5, 0.0],\n\t\t\t[0.9809698831278217, 0.9809698831278217, 0.09567085809127245], [0.5, 1.0, 0.0],\n\t\t\t[0.019030116872178315, 0.9809698831278217, 0.09567085809127245], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.029519683912911238, 0.029519683912911238, 0.11784918420649941], [0.5, 0.0, 0.0],\n\t\t\t[0.9704803160870887, 0.029519683912911238, 0.11784918420649941], [1.0, 0.5, 0.0],\n\t\t\t[0.9704803160870887, 0.9704803160870887, 0.11784918420649941], [0.5, 1.0, 0.0],\n\t\t\t[0.029519683912911238, 0.9704803160870887, 0.11784918420649941], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.04213259692436369, 0.04213259692436369, 0.13889255825490054], [0.5, 0.0, 0.0],\n\t\t\t[0.9578674030756363, 0.04213259692436369, 0.13889255825490054], [1.0, 0.5, 0.0],\n\t\t\t[0.9578674030756363, 0.9578674030756363, 0.13889255825490054], [0.5, 1.0, 0.0],\n\t\t\t[0.04213259692436369, 0.9578674030756363, 0.13889255825490054], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.05674738665931575, 0.05674738665931575, 0.15859832104091137], [0.5, 0.0, 0.0],\n\t\t\t[0.9432526133406842, 0.05674738665931575, 0.15859832104091137], [1.0, 0.5, 0.0],\n\t\t\t[0.9432526133406842, 0.9432526133406842, 0.15859832104091137], [0.5, 1.0, 0.0],\n\t\t\t[0.05674738665931575, 0.9432526133406842, 0.15859832104091137], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.0732233047033631, 0.0732233047033631, 0.17677669529663687], [0.5, 0.0, 0.0],\n\t\t\t[0.9267766952966369, 0.0732233047033631, 0.17677669529663687], [1.0, 0.5, 0.0],\n\t\t\t[0.9267766952966369, 0.9267766952966369, 0.17677669529663687], [0.5, 1.0, 0.0],\n\t\t\t[0.0732233047033631, 0.9267766952966369, 0.17677669529663687], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.09140167895908863, 0.09140167895908863, 0.19325261334068422], [0.5, 0.0, 0.0],\n\t\t\t[0.9085983210409114, 0.09140167895908863, 0.19325261334068422], [1.0, 0.5, 0.0],\n\t\t\t[0.9085983210409114, 0.9085983210409114, 0.19325261334068422], [0.5, 1.0, 0.0],\n\t\t\t[0.09140167895908863, 0.9085983210409114, 0.19325261334068422], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.11110744174509943, 0.11110744174509943, 0.2078674030756363], [0.5, 0.0, 0.0],\n\t\t\t[0.8888925582549005, 0.11110744174509943, 0.2078674030756363], [1.0, 0.5, 0.0],\n\t\t\t[0.8888925582549005, 0.8888925582549005, 0.2078674030756363], [0.5, 1.0, 0.0],\n\t\t\t[0.11110744174509943, 0.8888925582549005, 0.2078674030756363], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.13215081579350055, 0.13215081579350055, 0.22048031608708873], [0.5, 0.0, 0.0],\n\t\t\t[0.8678491842064995, 0.13215081579350055, 0.22048031608708873], [1.0, 0.5, 0.0],\n\t\t\t[0.8678491842064995, 0.8678491842064995, 0.22048031608708873], [0.5, 1.0, 0.0],\n\t\t\t[0.13215081579350055, 0.8678491842064995, 0.22048031608708873], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.15432914190872754, 0.15432914190872754, 0.23096988312782168], [0.5, 0.0, 0.0],\n\t\t\t[0.8456708580912724, 0.15432914190872754, 0.23096988312782168], [1.0, 0.5, 0.0],\n\t\t\t[0.8456708580912724, 0.8456708580912724, 0.23096988312782168], [0.5, 1.0, 0.0],\n\t\t\t[0.15432914190872754, 0.8456708580912724, 0.23096988312782168], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.17742883068638443, 0.17742883068638443, 0.23923508393305223], [0.5, 0.0, 0.0],\n\t\t\t[0.8225711693136155, 0.17742883068638443, 0.23923508393305223], [1.0, 0.5, 0.0],\n\t\t\t[0.8225711693136155, 0.8225711693136155, 0.23923508393305223], [0.5, 1.0, 0.0],\n\t\t\t[0.17742883068638443, 0.8225711693136155, 0.23923508393305223], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.20122741949596792, 0.20122741949596792, 0.2451963201008076], [0.5, 0.0, 0.0],\n\t\t\t[0.7987725805040321, 0.20122741949596792, 0.2451963201008076], [1.0, 0.5, 0.0],\n\t\t\t[0.7987725805040321, 0.7987725805040321, 0.2451963201008076], [0.5, 1.0, 0.0],\n\t\t\t[0.20122741949596792, 0.7987725805040321, 0.2451963201008076], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.2254957149176098, 0.2254957149176098, 0.2487961816680492], [0.5, 0.0, 0.0],\n\t\t\t[0.7745042850823902, 0.2254957149176098, 0.2487961816680492], [1.0, 0.5, 0.0],\n\t\t\t[0.7745042850823902, 0.7745042850823902, 0.2487961816680492], [0.5, 1.0, 0.0],\n\t\t\t[0.2254957149176098, 0.7745042850823902, 0.2487961816680492], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.25, 0.25, 0.25], [0.5, 0.0, 0.0],\n\t\t\t[0.75, 0.25, 0.25], [1.0, 0.5, 0.0],\n\t\t\t[0.75, 0.75, 0.25], [0.5, 1.0, 0.0],\n\t\t\t[0.25, 0.75, 0.25], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.27450428508239016, 0.27450428508239016, 0.24879618166804923], [0.5, 0.0, 0.0],\n\t\t\t[0.7254957149176098, 0.27450428508239016, 0.24879618166804923], [1.0, 0.5, 0.0],\n\t\t\t[0.7254957149176098, 0.7254957149176098, 0.24879618166804923], [0.5, 1.0, 0.0],\n\t\t\t[0.27450428508239016, 0.7254957149176098, 0.24879618166804923], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.29877258050403205, 0.29877258050403205, 0.2451963201008076], [0.5, 0.0, 0.0],\n\t\t\t[0.701227419495968, 0.29877258050403205, 0.2451963201008076], [1.0, 0.5, 0.0],\n\t\t\t[0.701227419495968, 0.701227419495968, 0.2451963201008076], [0.5, 1.0, 0.0],\n\t\t\t[0.29877258050403205, 0.701227419495968, 0.2451963201008076], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.3225711693136155, 0.3225711693136155, 0.23923508393305223], [0.5, 0.0, 0.0],\n\t\t\t[0.6774288306863845, 0.3225711693136155, 0.23923508393305223], [1.0, 0.5, 0.0],\n\t\t\t[0.6774288306863845, 0.6774288306863845, 0.23923508393305223], [0.5, 1.0, 0.0],\n\t\t\t[0.3225711693136155, 0.6774288306863845, 0.23923508393305223], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.3456708580912724, 0.3456708580912724, 0.23096988312782168], [0.5, 0.0, 0.0],\n\t\t\t[0.6543291419087276, 0.3456708580912724, 0.23096988312782168], [1.0, 0.5, 0.0],\n\t\t\t[0.6543291419087276, 0.6543291419087276, 0.23096988312782168], [0.5, 1.0, 0.0],\n\t\t\t[0.3456708580912724, 0.6543291419087276, 0.23096988312782168], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.3678491842064994, 0.3678491842064994, 0.22048031608708876], [0.5, 0.0, 0.0],\n\t\t\t[0.6321508157935005, 0.3678491842064994, 0.22048031608708876], [1.0, 0.5, 0.0],\n\t\t\t[0.6321508157935005, 0.6321508157935005, 0.22048031608708876], [0.5, 1.0, 0.0],\n\t\t\t[0.3678491842064994, 0.6321508157935005, 0.22048031608708876], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.3888925582549005, 0.3888925582549005, 0.20786740307563634], [0.5, 0.0, 0.0],\n\t\t\t[0.6111074417450995, 0.3888925582549005, 0.20786740307563634], [1.0, 0.5, 0.0],\n\t\t\t[0.6111074417450995, 0.6111074417450995, 0.20786740307563634], [0.5, 1.0, 0.0],\n\t\t\t[0.3888925582549005, 0.6111074417450995, 0.20786740307563634], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4085983210409113, 0.4085983210409113, 0.19325261334068428], [0.5, 0.0, 0.0],\n\t\t\t[0.5914016789590887, 0.4085983210409113, 0.19325261334068428], [1.0, 0.5, 0.0],\n\t\t\t[0.5914016789590887, 0.5914016789590887, 0.19325261334068428], [0.5, 1.0, 0.0],\n\t\t\t[0.4085983210409113, 0.5914016789590887, 0.19325261334068428], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.42677669529663687, 0.42677669529663687, 0.1767766952966369], [0.5, 0.0, 0.0],\n\t\t\t[0.5732233047033631, 0.42677669529663687, 0.1767766952966369], [1.0, 0.5, 0.0],\n\t\t\t[0.5732233047033631, 0.5732233047033631, 0.1767766952966369], [0.5, 1.0, 0.0],\n\t\t\t[0.42677669529663687, 0.5732233047033631, 0.1767766952966369], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4432526133406842, 0.4432526133406842, 0.15859832104091137], [0.5, 0.0, 0.0],\n\t\t\t[0.5567473866593158, 0.4432526133406842, 0.15859832104091137], [1.0, 0.5, 0.0],\n\t\t\t[0.5567473866593158, 0.5567473866593158, 0.15859832104091137], [0.5, 1.0, 0.0],\n\t\t\t[0.4432526133406842, 0.5567473866593158, 0.15859832104091137], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.45786740307563634, 0.45786740307563634, 0.13889255825490054], [0.5, 0.0, 0.0],\n\t\t\t[0.5421325969243637, 0.45786740307563634, 0.13889255825490054], [1.0, 0.5, 0.0],\n\t\t\t[0.5421325969243637, 0.5421325969243637, 0.13889255825490054], [0.5, 1.0, 0.0],\n\t\t\t[0.45786740307563634, 0.5421325969243637, 0.13889255825490054], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.47048031608708873, 0.47048031608708873, 0.11784918420649947], [0.5, 0.0, 0.0],\n\t\t\t[0.5295196839129113, 0.47048031608708873, 0.11784918420649947], [1.0, 0.5, 0.0],\n\t\t\t[0.5295196839129113, 0.5295196839129113, 0.11784918420649947], [0.5, 1.0, 0.0],\n\t\t\t[0.47048031608708873, 0.5295196839129113, 0.11784918420649947], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4809698831278217, 0.4809698831278217, 0.09567085809127247], [0.5, 0.0, 0.0],\n\t\t\t[0.5190301168721783, 0.4809698831278217, 0.09567085809127247], [1.0, 0.5, 0.0],\n\t\t\t[0.5190301168721783, 0.5190301168721783, 0.09567085809127247], [0.5, 1.0, 0.0],\n\t\t\t[0.4809698831278217, 0.5190301168721783, 0.09567085809127247], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4892350839330522, 0.4892350839330522, 0.0725711693136156], [0.5, 0.0, 0.0],\n\t\t\t[0.5107649160669478, 0.4892350839330522, 0.0725711693136156], [1.0, 0.5, 0.0],\n\t\t\t[0.5107649160669478, 0.5107649160669478, 0.0725711693136156], [0.5, 1.0, 0.0],\n\t\t\t[0.4892350839330522, 0.5107649160669478, 0.0725711693136156], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4951963201008076, 0.4951963201008076, 0.04877258050403215], [0.5, 0.0, 0.0],\n\t\t\t[0.5048036798991924, 0.4951963201008076, 0.04877258050403215], [1.0, 0.5, 0.0],\n\t\t\t[0.5048036798991924, 0.5048036798991924, 0.04877258050403215], [0.5, 1.0, 0.0],\n\t\t\t[0.4951963201008076, 0.5048036798991924, 0.04877258050403215], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.4987961816680492, 0.4987961816680492, 0.024504285082390206], [0.5, 0.0, 0.0],\n\t\t\t[0.5012038183319508, 0.4987961816680492, 0.024504285082390206], [1.0, 0.5, 0.0],\n\t\t\t[0.5012038183319508, 0.5012038183319508, 0.024504285082390206], [0.5, 1.0, 0.0],\n\t\t\t[0.4987961816680492, 0.5012038183319508, 0.024504285082390206], [0.0, 0.5, 0.0]\n\t\t]\n\t},\n\t{\n\t\t\"frame_parent\": 1,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0.5, 0.5, 0.0], [0.5, 0.0, 0.0],\n\t\t\t[0.5, 0.5, 0.0], [1.0, 0.5, 0.0],\n\t\t\t[0.5, 0.5, 0.0], [0.5, 1.0, 0.0],\n\t\t\t[0.5, 0.5, 0.0], [0.0, 0.5, 0.0]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/bowtie.fold",
    "content": "{\n\t\"frame_description\": \"a twisted square, two edges overlapping\",\n\t\"vertices_coords\":[[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\"edges_vertices\": [[0, 2], [1, 2], [1, 3], [3, 0]],\n\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,0,0],\n\t\"faces_vertices\": [[0, 1, 2, 3]]\n}\n"
  },
  {
    "path": "tests/files/fold/command-strip-with-back.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],[15,0],[16,0],[17,0],[18,0],[19,0],[20,0],[0,2],[0,1],[20,1],[19,1],[18,1],[17,1],[16,1],[15,1],[14,1],[13,1],[12,1],[11,1],[10,1],[9,1],[8,1],[7,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1],[1,2],[4,2],[5,2],[6,2],[9,2],[10,2],[11,2],[14,2],[15,2],[16,2],[19,2],[20,2],[5.5,1.5],[10.5,1.5],[15.5,1.5]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[21,22],[22,0],[23,24],[24,25],[25,26],[26,27],[27,28],[28,29],[29,30],[30,31],[31,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[38,39],[39,40],[40,41],[41,42],[42,22],[1,42],[42,43],[40,44],[42,21],[2,41],[21,43],[43,44],[44,45],[45,46],[46,47],[47,48],[48,49],[49,50],[50,51],[51,52],[52,53],[53,54],[41,43],[39,45],[38,55],[55,46],[3,40],[4,39],[39,44],[35,47],[37,55],[55,45],[5,38],[38,45],[36,46],[34,48],[6,37],[37,46],[33,56],[56,49],[7,36],[8,35],[30,50],[32,56],[56,48],[9,34],[34,47],[31,49],[29,51],[28,57],[57,52],[10,33],[33,48],[11,32],[32,49],[12,31],[25,53],[27,57],[57,51],[26,52],[24,54],[13,30],[14,29],[29,50],[15,28],[28,51],[16,27],[27,52],[17,26],[18,25],[19,24],[24,53],[20,23],[23,54]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,90,90,-90,90,90,90,90,-90,90,90,90,90,-90,90,90,90,90,-90,90,90,90,-180,-180,-180,-90,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,180,-180,-90,90,-180,-180,-180,-180,90,-180,-180,-180,90,-180,180,-180,-90,-90,-180,-180,-180,90,-180,-180,-180,180,-180,90,-180,90,-180,-90,-180,-180,-180,-180,-180,-90,90,-180,90,-180,90,-180,-90,-90,90,-180,0,0],\n\t\"vertices_edges\":[[0,21],[1,42,0],[2,46,1],[3,63,2],[4,64,3],[5,69,4],[6,73,5],[7,77,6],[8,78,7],[9,82,8],[10,88,9],[11,90,10],[12,92,11],[13,98,12],[14,99,13],[15,101,14],[16,103,15],[17,105,16],[18,106,17],[19,107,18],[109,19],[47,20,45],[41,20,21],[110,22,109],[22,97,108,23,107],[23,93,24,106],[24,96,25,105],[25,104,94,26,103],[26,86,102,27,101],[27,85,100,28,99],[28,79,29,98],[29,84,30,92],[30,91,80,31,90],[31,75,89,32,88],[32,72,83,33,82],[33,66,34,78],[34,71,35,77],[35,74,67,36,73],[36,61,70,37,69],[37,60,65,38,64],[38,44,39,63],[39,59,40,46],[40,43,45,41,42],[48,47,43,59],[49,48,44,65],[50,49,60,70,68],[51,50,62,74,71],[52,51,66,83],[53,52,72,89,81],[54,53,76,91,84],[55,54,79,100],[56,55,85,102,95],[57,56,87,104,96],[58,57,93,108],[58,97,110],[62,68,61,67],[76,81,75,80],[87,95,86,94]],\n\t\"vertices_vertices\":[[1,22],[2,42,0],[3,41,1],[4,40,2],[5,39,3],[6,38,4],[7,37,5],[8,36,6],[9,35,7],[10,34,8],[11,33,9],[12,32,10],[13,31,11],[14,30,12],[15,29,13],[16,28,14],[17,27,15],[18,26,16],[19,25,17],[20,24,18],[23,19],[43,22,42],[42,21,0],[54,24,20],[23,54,53,25,19],[24,53,26,18],[25,52,27,17],[26,52,57,28,16],[27,57,51,29,15],[28,51,50,30,14],[29,50,31,13],[30,49,32,12],[31,49,56,33,11],[32,56,48,34,10],[33,48,47,35,9],[34,47,36,8],[35,46,37,7],[36,46,55,38,6],[37,55,45,39,5],[38,45,44,40,4],[39,44,41,3],[40,43,42,2],[41,43,21,22,1],[44,21,42,41],[45,43,40,39],[46,44,39,38,55],[47,45,55,37,36],[48,46,35,34],[49,47,34,33,56],[50,48,56,32,31],[51,49,30,29],[52,50,29,28,57],[53,51,57,27,26],[54,52,25,24],[53,24,23],[46,45,38,37],[49,48,33,32],[52,51,28,27]],\n\t\"faces_vertices\":[[0,1,42,22],[1,2,41,42],[2,3,40,41],[3,4,39,40],[4,5,38,39],[5,6,37,38],[6,7,36,37],[7,8,35,36],[8,9,34,35],[9,10,33,34],[10,11,32,33],[11,12,31,32],[12,13,30,31],[13,14,29,30],[14,15,28,29],[15,16,27,28],[16,17,26,27],[17,18,25,26],[18,19,24,25],[19,20,23,24],[21,22,42],[21,42,43],[23,54,24],[24,54,53],[24,53,25],[25,53,52,26],[26,52,27],[27,52,57],[27,57,28],[28,57,51],[28,51,29],[29,51,50],[29,50,30],[30,50,49,31],[31,49,32],[32,49,56],[32,56,33],[33,56,48],[33,48,34],[34,48,47],[34,47,35],[35,47,46,36],[36,46,37],[37,46,55],[37,55,38],[38,55,45],[38,45,39],[39,45,44],[39,44,40],[40,44,43,41],[41,43,42],[45,55,46],[48,56,49],[51,57,52]],\n\t\"faces_edges\":[[0,42,41,21],[1,46,40,42],[2,63,39,46],[3,64,38,63],[4,69,37,64],[5,73,36,69],[6,77,35,73],[7,78,34,77],[8,82,33,78],[9,88,32,82],[10,90,31,88],[11,92,30,90],[12,98,29,92],[13,99,28,98],[14,101,27,99],[15,103,26,101],[16,105,25,103],[17,106,24,105],[18,107,23,106],[19,109,22,107],[20,41,45],[45,43,47],[110,97,22],[97,58,108],[108,93,23],[93,57,96,24],[96,104,25],[104,87,94],[94,86,26],[86,95,102],[102,85,27],[85,55,100],[100,79,28],[79,54,84,29],[84,91,30],[91,76,80],[80,75,31],[75,81,89],[89,72,32],[72,52,83],[83,66,33],[66,51,71,34],[71,74,35],[74,62,67],[67,61,36],[61,68,70],[70,60,37],[60,49,65],[65,44,38],[44,48,59,39],[59,43,40],[68,62,50],[81,76,53],[95,87,56]],\n\t\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[6,5,null],[7,6,null],[8,7,null],[9,8,null],[10,9,null],[11,10,null],[12,11,null],[13,12,null],[14,13,null],[15,14,null],[16,15,null],[17,16,null],[18,17,null],[19,18,null],[19,null],[null,20,21],[20,null,0],[22,19,null],[22,23,24,18,19],[24,25,17,18],[25,26,16,17],[26,27,28,15,16],[28,29,30,14,15],[30,31,32,13,14],[32,33,12,13],[33,34,11,12],[34,35,36,10,11],[36,37,38,9,10],[38,39,40,8,9],[40,41,7,8],[41,42,6,7],[42,43,44,5,6],[44,45,46,4,5],[46,47,48,3,4],[48,49,2,3],[49,50,1,2],[50,21,20,0,1],[null,21,50,49],[null,49,48,47],[null,47,46,45,51],[null,51,43,42,41],[null,41,40,39],[null,39,38,37,52],[null,52,35,34,33],[null,33,32,31],[null,31,30,29,53],[null,53,27,26,25],[null,25,24,23],[23,22,null],[51,45,44,43],[52,37,36,35],[53,29,28,27]],\n\t\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[20],[0],[19,22],[18,24],[17,25],[16,26],[15,28],[14,30],[13,32],[12,33],[11,34],[10,36],[9,38],[8,40],[7,41],[6,42],[5,44],[4,46],[3,48],[2,49],[1,50],[0,20],[0,1],[21,50],[48,49],[20,21],[1,2],[21],[49],[47],[51],[41],[39],[52],[33],[31],[53],[25],[23],[49,50],[46,47],[44,45],[43,51],[2,3],[3,4],[47,48],[40,41],[43,44],[45,51],[4,5],[45,46],[41,42],[38,39],[5,6],[42,43],[36,37],[35,52],[6,7],[7,8],[32,33],[35,36],[37,52],[8,9],[39,40],[33,34],[30,31],[28,29],[27,53],[9,10],[37,38],[10,11],[34,35],[11,12],[24,25],[27,28],[29,53],[25,26],[22,23],[12,13],[13,14],[31,32],[14,15],[29,30],[15,16],[26,27],[16,17],[17,18],[18,19],[23,24],[19],[22]],\n\t\"faces_faces\":[[1,20],[2,50,0],[3,49,1],[4,48,2],[5,46,3],[6,44,4],[7,42,5],[8,41,6],[9,40,7],[10,38,8],[11,36,9],[12,34,10],[13,33,11],[14,32,12],[15,30,13],[16,28,14],[17,26,15],[18,25,16],[19,24,17],[22,18],[0,21],[20,50],[23,19],[22,24],[23,25,18],[24,26,17],[25,27,16],[26,53,28],[27,29,15],[28,53,30],[29,31,14],[30,32],[31,33,13],[32,34,12],[33,35,11],[34,52,36],[35,37,10],[36,52,38],[37,39,9],[38,40],[39,41,8],[40,42,7],[41,43,6],[42,51,44],[43,45,5],[44,51,46],[45,47,4],[46,48],[47,49,3],[48,50,2],[49,21,1],[45,43],[37,35],[29,27]],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0,0,0],[1,0,0],[1,0,1],[2,0,1],[2,0,0],[3,0,0],[3,0,1],[2,0,1],[2,0,2],[3,0,2],[3,0,3],[2,0,3],[2,0,2],[1,0,2],[1,0,3],[0,0,3],[0,0,2],[1,0,2],[1,0,1],[0,0,1],[0,0,0],[0,1,1],[0,1,0],[0,1,0],[0,1,1],[1,1,1],[1,1,2],[0,1,2],[0,1,3],[1,1,3],[1,1,2],[2,1,2],[2,1,3],[3,1,3],[3,1,2],[2,1,2],[2,1,1],[3,1,1],[3,1,0],[2,1,0],[2,1,1],[1,1,1],[1,1,0],[0,1,0],[3,1,0],[3,1,1],[3,1,0],[3,1,3],[2,1,3],[3,1,3],[0,1,3],[0,1,2],[0,1,3],[0,1,0],[1,1,0],[2.5,1,0.5],[2.5,1,2.5],[0.5,1,2.5]\n\t\t],\n\t\t\"frame_classes\": [\"foldedForm\"]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/command-strip.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],[15,0],[16,0],[17,0],[18,0],[19,0],[20,0],\n\t\t[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[20,1]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,41],\n\t\t[41,40],[40,39],[39,38],[38,37],[37,36],[36,35],[35,34],[34,33],[33,32],[32,31],[31,30],[30,29],[29,28],[28,27],[27,26],[26,25],[25,24],[24,23],[23,22],[22,21],[21,0],\n\t\t[1,22],[2,23],[3,24],[4,25],[5,26],[6,27],[7,28],[8,29],[9,30],[10,31],[11,32],[12,33],[13,34],[14,35],[15,36],[16,37],[17,38],[18,39],[19,40]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\n\t\t\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t90,-90,-90,90,90,90,-90,-90,90,90,90,-90,-90,90,90,90,-90,-90,90\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,22,21],[1,2,23,22],[2,3,24,23],[3,4,25,24],[4,5,26,25],[5,6,27,26],[6,7,28,27],[7,8,29,28],[8,9,30,29],[9,10,31,30],[10,11,32,31],[11,12,33,32],[12,13,34,33],[13,14,35,34],[14,15,36,35],[15,16,37,36],[16,17,38,37],[17,18,39,38],[18,19,40,39],[19,20,41,40]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0,0],[1,0,0],[1,0,1],[2,0,1],[2,0,0],[3,0,0],[3,0,1],[2,0,1],[2,0,2],[3,0,2],[3,0,3],[2,0,3],[2,0,2],[1,0,2],[1,0,3],[0,0,3],[0,0,2],[1,0,2],[1,0,1],[0,0,1],[0,0,0],[0,1,0],[1,1,0],[1,1,1],[2,1,1],[2,1,0],[3,1,0],[3,1,1],[2,1,1],[2,1,2],[3,1,2],[3,1,3],[2,1,3],[2,1,2],[1,1,2],[1,1,3],[0,1,3],[0,1,2],[1,1,2],[1,1,1],[0,1,1],[0,1,0]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/crane-cp-bmvfcj-simple.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"crane\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\": [\n\t\t[0,0],[1,0],[1,1],[0,1],[0.5,0.5],[0.5,0.792893218813],[0.207106781187,0.5],[0.5,0.207106781187],[0.792893218813,0.5],[0.66591068104,0.5],[0.716772751325,0.576120467489],[0.865619448968,0.675576651179],[0.5,0.33408931896],[0.423879532511,0.283227248675],[0.324423348821,0.134380551032],[0.5,0.66591068104],[0.576120467489,0.716772751325],[0.675576651179,0.865619448968],[0.33408931896,0.5],[0.283227248675,0.423879532511],[0.134380551032,0.324423348821],[0.5,1],[0.5,0],[0.476712746783,0.905175938977],[0.523287253217,0.094824061023],[0.461939766256,0.808658283817],[0.404137552497,0.72996392064],[0.353553390593,0.646446609407],[0.27003607936,0.595862447503],[0.191341716183,0.538060233744],[0.094824061023,0.523287253217],[0,0.5],[0.538060233744,0.191341716183],[0.595862447503,0.27003607936],[0.646446609407,0.353553390593],[0.72996392064,0.404137552497],[0.808658283817,0.461939766256],[0.905175938977,0.476712746783],[1,0.5],[0.373017462227,0.207106781187],[0.207106781187,0.373017462227],[0.180807836521,0.436508731113],[0.117316567635,0.410209786448],[0,0.410209786448],[0.436508731113,0.180807836521],[0.410209786448,0.117316567635],[0.410209786448,0],[0.180807836521,1],[0.150809885227,0.970002048705],[0.16704465948,0.930807836521],[0.127850447296,0.914573062268],[0.127850447296,0.872149552704],[0.085426937732,0.872149552704],[0.069192163479,0.83295534052],[0.029997951295,0.849190114773],[0,0.819192163479],[0.6269825377729115,0.7928932188129114],[0.7928932188129119,0.6269825377729118],[0.8191921634781523,0.5634912688861793],[0.5634912688861791,0.8191921634781522],[0.5897902135510019,0.8826834323653202],[0.8826834323653201,0.5897902135510024],[0.5897902135509463,1],[1,0.5897902135509463],[0.5,0.9005438163099272],[0.09945618369009784,0.5],[0.5,0.09945618369009787],[0.9005438163099272,0.5]\n\t\t],\n\t\"edges_vertices\": [\n\t\t[9,8],[9,10],[12,13],[7,12],[12,4],[4,15],[15,16],[0,20],[6,18],[18,19],[22,1],[0,14],[21,23],[18,4],[22,24],[24,1],[4,9],[16,10],[26,27],[27,28],[28,29],[30,31],[29,6],[28,18],[27,4],[26,15],[25,5],[24,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[1,38],[8,36],[36,1],[37,1],[9,35],[35,1],[7,32],[32,1],[12,33],[33,1],[4,34],[34,1],[13,7],[14,39],[39,13],[19,13],[20,40],[40,19],[6,19],[40,39],[40,41],[41,42],[20,41],[41,6],[42,20],[42,43],[0,43],[43,31],[0,46],[46,22],[39,44],[14,44],[44,7],[45,46],[44,45],[14,45],[23,25],[25,26],[5,15],[3,47],[47,21],[30,29],[23,48],[49,50],[50,51],[51,52],[52,53],[53,54],[54,55],[31,55],[55,3],[3,54],[54,30],[3,49],[49,25],[3,53],[53,29],[3,52],[52,28],[3,51],[51,27],[3,50],[50,26],[47,48],[48,49],[3,48],[17,56],[56,16],[5,16],[2,17],[2,11],[10,8],[10,57],[57,11],[56,57],[8,58],[58,11],[57,58],[5,59],[59,17],[56,59],[17,60],[59,60],[11,61],[58,61],[2,62],[62,21],[60,62],[2,63],[63,38],[61,63],[60,64],[64,23],[5,64],[64,21],[30,65],[65,42],[6,65],[65,31],[45,66],[66,24],[7,66],[66,22],[61,67],[67,37],[8,67],[67,38],[15,27],[18,27],[12,34],[34,9],[10,4],[4,16],[4,19],[4,13],[35,8],[33,7],[6,28],[5,26],[23,5],[6,30],[7,24],[8,37]\n\t],\n\t\"edges_assignment\": [\n\t\t\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"B\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"B\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"U\",\"U\",\"U\",\"M\",\"M\",\"V\",\"U\",\"B\",\"B\",\"B\",\"B\",\"U\",\"M\",\"M\",\"U\",\"U\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"C\",\"C\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"U\",\"M\",\"M\",\"U\",\"M\",\"M\",\"U\",\"V\",\"U\",\"V\",\"U\",\"B\",\"B\",\"U\",\"B\",\"B\",\"U\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t-180,180,180,-180,-180,-180,180,-180,-180,180,0,-180,-180,-180,-180,180,-180,180,180,180,-180,-180,-180,-180,180,-180,-180,180,-180,180,180,-180,180,-180,0,-180,-180,180,-180,-180,-180,-180,-180,-180,180,-180,180,-180,-180,180,-180,-180,180,0,0,0,-180,-180,180,0,0,0,0,0,0,-180,-180,0,0,180,180,-180,-180,0,0,180,180,180,-180,-180,180,-180,180,0,0,180,180,-180,-180,-180,-180,-180,-180,0,0,-180,-180,180,-180,180,-180,-180,180,-180,-180,180,-180,-180,0,-180,-180,0,-180,-180,0,180,0,180,0,0,0,0,0,0,0,180,180,0,0,180,180,0,0,180,180,0,0,180,180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\": [\n\t\t[0,20,42,43],[20,0,14,39,40],[14,0,46,45],[39,14,44],[20,40,41],[41,42,20],[44,14,45],[47,3,48],[50,49,3],[51,50,3],[52,51,3],[53,52,3],[54,53,3],[55,54,3],[3,49,48],[17,56,57,11,2],[17,59,56],[58,11,57],[62,60,17,2],[60,59,17],[61,63,2,11],[61,11,58],[21,64,60,62],[65,31,43,42],[66,45,46,22],[67,38,63,61],[56,16,10,57],[9,4,34],[4,9,10],[4,10,16],[4,16,15],[4,15,27],[4,27,18],[4,18,19],[4,19,13],[4,13,12],[4,12,34],[13,19,40,39],[36,35,1],[35,9,34],[35,34,1],[34,33,1],[33,34,12],[33,32,1],[27,28,18],[28,27,51,52],[28,52,53,29],[26,25,49,50],[26,50,51,27],[26,27,15],[21,23,64],[23,21,47,48],[23,48,49,25],[23,25,5],[23,5,64],[59,5,16,56],[5,59,60,64],[5,25,26],[5,26,15],[5,15,16],[18,6,19],[6,18,28],[6,28,29],[6,29,30],[6,30,65],[6,65,42,41],[6,41,40,19],[30,29,53,54],[30,54,55,31],[30,31,65],[33,7,32],[7,33,12],[7,12,13],[7,13,39,44],[7,44,45,66],[7,66,24],[7,24,32],[32,24,1],[24,66,22],[24,22,1],[67,8,37],[8,67,61,58],[8,58,57,10],[8,10,9],[8,9,35],[8,35,36],[8,36,37],[38,37,1],[37,38,67],[37,36,1]\n\t]\n}"
  },
  {
    "path": "tests/files/fold/crane-cp-bmvfcj.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"crane\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[1,1],[0,1],[0.5,0.5],[0.5,0.792893218813],[0.207106781187,0.5],[0.5,0.207106781187],[0.792893218813,0.5],[0.66591068104,0.5],[0.716772751325,0.576120467489],[0.865619448968,0.675576651179],[0.5,0.33408931896],[0.423879532511,0.283227248675],[0.324423348821,0.134380551032],[0.5,0.66591068104],[0.576120467489,0.716772751325],[0.675576651179,0.865619448968],[0.33408931896,0.5],[0.283227248675,0.423879532511],[0.134380551032,0.324423348821],[0.5,1],[0.5,0],[0.476712746783,0.905175938977],[0.523287253217,0.094824061023],[0.461939766256,0.808658283817],[0.404137552497,0.72996392064],[0.353553390593,0.646446609407],[0.27003607936,0.595862447503],[0.191341716183,0.538060233744],[0.094824061023,0.523287253217],[0,0.5],[0.538060233744,0.191341716183],[0.595862447503,0.27003607936],[0.646446609407,0.353553390593],[0.72996392064,0.404137552497],[0.808658283817,0.461939766256],[0.905175938977,0.476712746783],[1,0.5],[0.373017462227,0.207106781187],[0.207106781187,0.373017462227],[0.180807836521,0.436508731113],[0.117316567635,0.410209786448],[0,0.410209786448],[0.436508731113,0.180807836521],[0.410209786448,0.117316567635],[0.410209786448,0],[0.180807836521,1],[0.150809885227,0.970002048705],[0.16704465948,0.930807836521],[0.127850447296,0.914573062268],[0.127850447296,0.872149552704],[0.085426937732,0.872149552704],[0.069192163479,0.83295534052],[0.029997951295,0.849190114773],[0,0.819192163479],[0.6269825377729115,0.7928932188129114],[0.7928932188129119,0.6269825377729118],[0.8191921634781523,0.5634912688861793],[0.5634912688861791,0.8191921634781522],[0.5897902135510019,0.8826834323653202],[0.8826834323653201,0.5897902135510024],[0.5897902135509463,1],[1,0.5897902135509463],[0.5,0.9005438163099272],[0.09945618369009784,0.5],[0.5,0.09945618369009787],[0.9005438163099272,0.5]\n\t],\n\t\"edges_vertices\":[\n\t\t[9,8],[9,10],[12,13],[7,12],[12,4],[4,15],[15,16],[0,20],[6,18],[18,19],[22,1],[0,14],[21,23],[18,4],[22,24],[24,1],[4,9],[16,10],[26,27],[27,28],[28,29],[30,31],[29,6],[28,18],[27,4],[26,15],[25,5],[24,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[1,38],[8,36],[36,1],[37,1],[9,35],[35,1],[7,32],[32,1],[12,33],[33,1],[4,34],[34,1],[13,7],[14,39],[39,13],[19,13],[20,40],[40,19],[6,19],[40,39],[40,41],[41,42],[20,41],[41,6],[42,20],[42,43],[0,43],[43,31],[0,46],[46,22],[39,44],[14,44],[44,7],[45,46],[44,45],[14,45],[23,25],[25,26],[5,15],[3,47],[47,21],[30,29],[23,48],[49,50],[50,51],[51,52],[52,53],[53,54],[54,55],[31,55],[55,3],[3,54],[54,30],[3,49],[49,25],[3,53],[53,29],[3,52],[52,28],[3,51],[51,27],[3,50],[50,26],[47,48],[48,49],[3,48],[17,56],[56,16],[5,16],[2,17],[2,11],[10,8],[10,57],[57,11],[56,57],[8,58],[58,11],[57,58],[5,59],[59,17],[56,59],[17,60],[59,60],[11,61],[58,61],[2,62],[62,21],[60,62],[2,63],[63,38],[61,63],[60,64],[64,23],[5,64],[64,21],[30,65],[65,42],[6,65],[65,31],[45,66],[66,24],[7,66],[66,22],[61,67],[67,37],[8,67],[67,38],[15,27],[18,27],[12,34],[34,9],[10,4],[4,16],[4,19],[4,13],[35,8],[33,7],[6,28],[5,26],[23,5],[6,30],[7,24],[8,37],[62,64],[43,65],[64,59],[65,41],[41,19],[59,16],[13,44],[10,58],[58,67],[44,66],[66,46],[67,63],[43,20],[62,17],[20,14],[17,11],[14,46],[11,63],[13,40],[10,56],[40,14],[56,11],[28,51],[51,26],[28,53],[26,49],[53,30],[49,23],[30,55],[23,47]\n\t],\n\t\"edges_assignment\":[\n\t\t\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"B\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"B\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"U\",\"U\",\"U\",\"M\",\"M\",\"V\",\"U\",\"B\",\"B\",\"B\",\"B\",\"U\",\"M\",\"M\",\"U\",\"U\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"C\",\"C\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"U\",\"M\",\"M\",\"U\",\"M\",\"M\",\"U\",\"V\",\"U\",\"V\",\"U\",\"B\",\"B\",\"U\",\"B\",\"B\",\"U\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\",\"J\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t-180,180,180,-180,-180,-180,180,-180,-180,180,0,-180,-180,-180,-180,180,-180,180,180,180,-180,-180,-180,-180,180,-180,-180,180,-180,180,180,-180,180,-180,0,-180,-180,180,-180,-180,-180,-180,-180,-180,180,-180,180,-180,-180,180,-180,-180,180,0,0,0,-180,-180,180,0,0,0,0,0,0,-180,-180,0,0,180,180,-180,-180,0,0,180,180,180,-180,-180,180,-180,180,0,0,180,180,-180,-180,-180,-180,-180,-180,0,0,-180,-180,180,-180,180,-180,-180,180,-180,-180,180,-180,-180,0,-180,-180,0,-180,-180,0,180,0,180,0,0,0,0,0,0,0,180,180,0,0,180,180,0,0,180,180,0,0,180,180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[9,4,34],[4,12,34],[36,35,1],[35,9,34],[35,34,1],[34,33,1],[33,34,12],[33,32,1],[33,7,32],[7,33,12],[7,24,32],[32,24,1],[24,22,1],[8,9,35],[8,35,36],[8,36,37],[38,37,1],[37,36,1],[15,27,4],[27,18,4],[6,41,19],[41,6,65],[41,65,42],[19,4,18],[19,18,6],[59,60,64],[59,64,5],[59,5,16],[16,5,15],[16,15,4],[7,66,24],[66,7,44],[66,44,45],[66,22,24],[38,67,37],[67,61,58],[67,58,8],[67,8,37],[43,42,65],[43,65,31],[62,21,64],[62,64,60],[20,41,42],[20,42,43],[20,43,0],[17,2,62],[17,62,60],[17,60,59],[46,22,66],[46,66,45],[63,61,67],[63,67,38],[12,13,7],[13,12,4],[13,4,19],[13,39,44],[13,44,7],[57,10,58],[10,16,4],[10,4,9],[10,9,8],[10,8,58],[19,40,13],[40,19,41],[40,41,20],[40,20,14],[40,14,39],[40,39,13],[44,14,45],[14,44,39],[14,20,0],[14,0,46],[14,46,45],[17,56,11],[56,17,59],[56,59,16],[56,16,10],[56,10,57],[56,57,11],[2,11,63],[11,2,17],[11,57,58],[11,58,61],[11,61,63],[51,50,3],[51,3,52],[27,28,18],[28,27,51],[28,51,52],[28,29,6],[28,6,18],[5,26,15],[26,5,25],[26,50,51],[26,51,27],[26,27,15],[52,53,28],[53,52,3],[53,3,54],[53,29,28],[49,48,3],[49,3,50],[49,50,26],[49,26,25],[29,30,6],[30,29,53],[30,53,54],[30,54,55],[30,55,31],[30,31,65],[30,65,6],[55,54,3],[21,23,64],[23,21,47],[23,47,48],[23,48,49],[23,49,25],[23,25,5],[23,5,64],[47,3,48]\n\t]\n}"
  },
  {
    "path": "tests/files/fold/crane-cp.fold",
    "content": "{\"file_spec\":1,\"file_creator\":\"Rabbit Ear\",\"file_author\":\"Kraft\",\"file_classes\":[\"singleModel\"],\"vertices_coords\":[[0,1],[1,1],[1,0],[0,0],[0.5,0.5],[0.5,0.207106781187],[0.207106781187,0.5],[0.5,0.792893218813],[0.792893218813,0.5],[0.66591068104,0.5],[0.716772751325,0.423879532511],[0.865619448968,0.324423348821],[0.5,0.66591068104],[0.423879532511,0.716772751325],[0.324423348821,0.865619448968],[0.5,0.33408931896],[0.576120467489,0.283227248675],[0.675576651179,0.134380551032],[0.33408931896,0.5],[0.283227248675,0.576120467489],[0.134380551032,0.675576651179],[0.5,0],[0.5,1],[0.476712746783,0.094824061023],[0.523287253217,0.905175938977],[0.461939766256,0.191341716183],[0.404137552497,0.27003607936],[0.353553390593,0.353553390593],[0.27003607936,0.404137552497],[0.191341716183,0.461939766256],[0.094824061023,0.476712746783],[0,0.5],[0.538060233744,0.808658283817],[0.595862447503,0.72996392064],[0.646446609407,0.646446609407],[0.72996392064,0.595862447503],[0.808658283817,0.538060233744],[0.905175938977,0.523287253217],[1,0.5],[0.373017462227,0.792893218813],[0.207106781187,0.626982537773],[0.180807836521,0.563491268887],[0.117316567635,0.589790213552],[0,0.589790213552],[0.436508731113,0.819192163479],[0.410209786448,0.882683432365],[0.410209786448,1],[0.180807836521,0],[0.150809885227,0.029997951295],[0.16704465948,0.069192163479],[0.127850447296,0.08542693773200005],[0.127850447296,0.127850447296],[0.085426937732,0.127850447296],[0.069192163479,0.16704465948],[0.029997951295,0.150809885227],[0,0.180807836521]],\"faces_vertices\":[[10,11,8],[37,36,8,11],[10,8,9],[36,35,9,8],[4,15,16,10,9],[16,17,2,11,10],[7,13,12],[19,18,4,12,13],[33,32,7,12],[34,33,12,4],[25,23,17,5],[16,5,17],[27,26,15,4],[5,16,15],[43,42,20,0],[40,39,14,0,20],[29,28,18,6],[19,6,18],[24,1,22],[45,46,0,14],[17,23,21,2],[48,47,21,23],[28,27,4,18],[46,45,24,22],[32,1,24],[35,34,4,9],[51,50,26,27],[52,51,27,28],[53,52,28,29],[42,43,31,30],[55,54,30,31],[41,42,30,29,6],[25,5,15,26],[45,44,7,32,24],[1,32,33],[1,33,34],[1,34,35],[1,35,36],[1,36,37],[11,2,38,37],[1,37,38],[44,39,13,7],[44,14,39],[40,19,13,39],[41,40,20],[41,6,19,40],[20,42,41],[45,14,44],[49,48,23,25],[50,49,25,26],[48,3,47],[54,53,29,30],[3,49,50],[3,50,51],[3,51,52],[3,52,53],[3,53,54],[3,54,55],[48,49,3]],\"edges_vertices\":[[8,11],[9,8],[9,10],[10,11],[10,8],[12,13],[7,12],[12,4],[5,17],[4,15],[15,16],[0,20],[6,18],[18,19],[22,1],[0,14],[21,2],[21,23],[18,4],[22,24],[24,1],[4,9],[16,10],[26,27],[27,28],[28,29],[30,31],[29,6],[28,18],[27,4],[26,15],[25,5],[24,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[1,38],[8,36],[36,1],[11,37],[37,1],[9,35],[35,1],[7,32],[32,1],[12,33],[33,1],[4,34],[34,1],[13,7],[14,39],[39,13],[19,13],[20,40],[40,19],[6,19],[40,39],[40,41],[41,42],[20,41],[41,6],[30,42],[42,20],[42,43],[0,43],[43,31],[0,46],[46,22],[39,44],[14,44],[44,7],[45,46],[44,45],[14,45],[45,24],[38,2],[23,25],[17,23],[2,17],[2,11],[25,26],[5,15],[16,5],[17,16],[3,47],[47,21],[30,29],[23,48],[49,50],[50,51],[51,52],[52,53],[53,54],[54,55],[31,55],[55,3],[3,54],[54,30],[3,49],[49,25],[3,53],[53,29],[3,52],[52,28],[3,51],[51,27],[3,50],[50,26],[47,48],[48,49],[3,48]],\"edges_assignment\":[\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"B\",\"M\",\"B\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"B\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\"]}"
  },
  {
    "path": "tests/files/fold/crane-step.fold",
    "content": "{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0.5,0.5],[0.6659106810403506,0.5],[0.7928932188134525,0.5],[0.8656194489676547,0.6755766511785422],[0.7167727513247393,0.5761204674887133],[0.5,0.6659106810403506],[0.5,0.7928932188134525],[0.6755766511785422,0.8656194489676547],[0.13438055103234534,0.3244233488214578],[0.2071067811865476,0.5],[1,0],[1,0.5],[0.9055909171082465,0.4746265108370956],[0.5,0],[0.5253734891629044,0.09440908289175358],[0.42387953251128685,0.2832272486752607],[0.5,0.33408931895964944],[0.2832272486752608,0.4238795325112868],[0.33408931895964944,0.5],[0.5761204674887133,0.7167727513247393],[1,1],[0,1],[0.5,1],[0.4746265108370956,0.9055909171082464],[0,0.5],[0.09440908289175362,0.5253734891629045],[0.3244233488214578,0.13438055103234528],[0.5,0.20710678118654752],[0.46193976625564337,0.8086582838174552],[0.8086582838174552,0.4619397662556434],[0.5380602337443566,0.19134171618254486],[0.19134171618254492,0.5380602337443566],[0.5976310729378175,0.2688543216254588],[0.7311456783745411,0.4023689270621825],[0.26885432162545886,0.5976310729378174],[0.40236892706218264,0.7311456783745411],[0,0],[0.6464466094067263,0.35355339059327373],[0.35355339059327373,0.6464466094067262]],\"edges_vertices\":[[0,1],[2,3],[4,2],[0,5],[4,3],[6,7],[8,9],[10,11],[10,12],[1,2],[1,4],[13,10],[14,10],[15,16],[17,18],[5,19],[3,20],[21,22],[21,23],[24,21],[25,21],[23,22],[26,27],[12,11],[28,23],[29,12],[18,0],[16,0],[13,14],[14,30],[24,25],[25,31],[30,10],[32,10],[10,33],[31,21],[34,21],[21,35],[36,8],[36,26],[8,17],[9,17],[26,15],[15,27],[9,18],[27,16],[35,28],[33,29],[10,29],[21,28],[31,34],[30,32],[9,31],[28,6],[11,20],[29,2],[27,30],[36,13],[36,24],[22,20],[7,20],[19,7],[6,19],[5,6],[0,37],[37,10],[34,38],[21,38],[38,0],[38,35],[32,37],[37,33],[17,15],[19,4],[23,7],[35,5],[18,34],[8,25],[26,14],[16,32],[33,1],[12,3]],\"edges_assignment\":[\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"B\",\"V\",\"M\",\"V\",\"B\",\"V\",\"V\",\"V\",\"V\",\"M\",\"B\",\"V\",\"B\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"B\",\"M\",\"M\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\"],\"faces_vertices\":[[16,0,18,17,15],[37,0,16,32],[1,0,37,33],[5,0,1,4,19],[38,0,5,35],[18,0,38,34],[2,1,33,29],[4,1,2],[3,2,29,12],[4,2,3],[4,3,20,7,19],[20,3,12,11],[6,5,19],[35,5,6,28],[7,6,19],[28,6,7,23],[23,7,20,22],[36,8,25,24],[17,8,36,26,15],[9,8,17],[25,8,9,31],[18,9,17],[31,9,18,34],[12,10,11],[29,10,12],[33,10,29],[37,10,33],[32,10,37],[30,10,32],[14,10,30],[13,10,14],[36,13,14,26],[26,14,30,27],[27,15,26],[16,15,27],[32,16,27,30],[25,21,24],[31,21,25],[34,21,31],[38,21,34],[35,21,38],[28,21,35],[23,21,28],[22,21,23]]}"
  },
  {
    "path": "tests/files/fold/crane.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"crane\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[1,1],[0,1],[0.5,0.5],[0.5,0.792893218813],[0.207106781187,0.5],[0.5,0.207106781187],[0.792893218813,0.5],[0.66591068104,0.5],[0.716772751325,0.576120467489],[0.865619448968,0.675576651179],[0.5,0.33408931896],[0.423879532511,0.283227248675],[0.324423348821,0.134380551032],[0.5,0.66591068104],[0.576120467489,0.716772751325],[0.675576651179,0.865619448968],[0.33408931896,0.5],[0.283227248675,0.423879532511],[0.134380551032,0.324423348821],[0.5,1],[0.5,0],[0.476712746783,0.905175938977],[0.523287253217,0.094824061023],[0.461939766256,0.808658283817],[0.404137552497,0.72996392064],[0.353553390593,0.646446609407],[0.27003607936,0.595862447503],[0.191341716183,0.538060233744],[0.094824061023,0.523287253217],[0,0.5],[0.538060233744,0.191341716183],[0.595862447503,0.27003607936],[0.646446609407,0.353553390593],[0.72996392064,0.404137552497],[0.808658283817,0.461939766256],[0.905175938977,0.476712746783],[1,0.5],[0.373017462227,0.207106781187],[0.207106781187,0.373017462227],[0.180807836521,0.436508731113],[0.117316567635,0.410209786448],[0,0.410209786448],[0.436508731113,0.180807836521],[0.410209786448,0.117316567635],[0.410209786448,0],[0.180807836521,1],[0.150809885227,0.970002048705],[0.16704465948,0.930807836521],[0.127850447296,0.914573062268],[0.127850447296,0.872149552704],[0.085426937732,0.872149552704],[0.069192163479,0.83295534052],[0.029997951295,0.849190114773],[0,0.819192163479]\n\t],\n\t\"edges_vertices\":[\n\t\t[8,11],[8,9],[9,10],[10,11],[8,10],[12,13],[7,12],[4,12],[5,17],[4,15],[15,16],[0,20],[6,18],[18,19],[1,22],[0,14],[2,21],[21,23],[4,18],[22,24],[1,24],[4,9],[10,16],[26,27],[27,28],[28,29],[30,31],[6,29],[18,28],[4,27],[15,26],[5,25],[24,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[1,38],[8,36],[1,36],[11,37],[1,37],[9,35],[1,35],[7,32],[1,32],[12,33],[1,33],[4,34],[1,34],[7,13],[14,39],[13,39],[13,19],[20,40],[19,40],[6,19],[39,40],[40,41],[41,42],[20,41],[6,41],[30,42],[20,42],[42,43],[0,43],[31,43],[0,46],[22,46],[39,44],[14,44],[7,44],[45,46],[44,45],[14,45],[24,45],[2,38],[23,25],[17,23],[2,17],[2,11],[25,26],[5,15],[5,16],[16,17],[3,47],[21,47],[29,30],[23,48],[49,50],[50,51],[51,52],[52,53],[53,54],[54,55],[31,55],[3,55],[3,54],[30,54],[3,49],[25,49],[3,53],[29,53],[3,52],[28,52],[3,51],[27,51],[3,50],[26,50],[47,48],[48,49],[3,48]\n\t],\n\t\"edges_assignment\":[\n\t\t\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"B\",\"M\",\"B\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"B\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\"\n\t],\n\t\"faces_vertices\":[\n\t\t[10,11,2,17,16],[20,0,14,39,40],[2,21,23,17],[37,38,2,11],[9,10,16,15,4],[13,12,4,18,19],[0,20,42,43],[14,0,46,45],[22,1,24],[38,37,1],[33,32,1],[36,35,1],[37,36,1],[24,1,32],[34,33,1],[35,34,1],[23,21,47,48],[31,30,54,55],[29,28,52,53],[26,25,49,50],[25,23,48,49],[30,29,53,54],[27,26,50,51],[28,27,51,52],[39,13,19,40],[18,4,27,28],[4,12,33,34],[4,15,26,27],[9,4,34,35],[11,8,36,37],[5,17,23,25],[22,24,45,46],[30,31,43,42],[6,29,30,42,41],[24,32,7,44,45],[8,11,10],[17,5,16],[26,15,5,25],[6,18,28,29],[8,9,35,36],[12,7,32,33],[40,19,6,41],[7,13,39,44],[9,8,10],[18,6,19],[12,13,7],[15,16,5],[39,14,44],[20,40,41],[41,42,20],[44,14,45],[47,3,48],[55,54,3],[54,53,3],[3,49,48],[50,49,3],[53,52,3],[52,51,3],[51,50,3]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0.08416343347856403],[1,0.6186778931720701],[0.8244150837130928,0.9085785171916573],[0.21247604016228555,0.915836566521436],[0.5300954508285951,0.6142588843071597],[0.36337677112096295,0.44754020459941846],[0.36337677112301514,0.44754020460103877],[0.3633767711224743,0.4475402046015796],[0.3633767711208539,0.4475402045995275],[0.5300954508285951,0.37848306636590406],[0.42192058606529065,0.30620293257065856],[0.6334467618644971,0.4475402046006131],[0.5300954508285952,0.37848306636590406],[0.421920586065291,0.30620293257065856],[0.46103831259104405,0.27513175532716005],[0.29431963288733953,0.6142588843071597],[0.22203949909209403,0.5060840195438552],[0.3633767711220486,0.7176101953430616],[0.2943196328873394,0.6142588843071597],[0.22203949909209403,0.5060840195438552],[0.19096832184859544,0.5452017460696084],[0.3219800425778917,0.4061434760572793],[0.3219800425793797,0.40614347605941986],[0.20329351652063368,0.47802867400269494],[0.3938652405230047,0.2874569500010774],[0.3219800425773731,0.4061434760571329],[0.20329351652273853,0.4780286740024881],[0.3219800425786922,0.4061434760572569],[0.20329351652273853,0.4780286740024883],[0.3219800425804855,0.40614347605804296],[0.2032935165229471,0.478028674002286],[0.32198004258117024,0.4061434760584639],[0.3219800425794786,0.40614347605905],[0.39386524052392374,0.287456950001303],[0.3219800425786924,0.40614347605725676],[0.3938652405239234,0.287456950001303],[0.3219800425785681,0.4061434760559374],[0.3938652405241303,0.287456949999198],[0.3219800425787148,0.40614347605645607],[0.5300954508271741,0.37848306636448314],[0.2943196328859185,0.6142588843057385],[0.3633767711220429,0.5452017460715137],[0.2943196328843865,0.6142588843055607],[0.41220754185545017,0.49637097533469027],[0.46103831259464495,0.44754020459975685],[0.5300954508289534,0.37848306636448503],[0.4122075418596681,0.49637097533713387],[0.4576586834036543,0.8389807902397584],[0.4297314582699964,0.8924104037442164],[0.45765868340317795,0.8389807902388311],[0.42973145827541087,0.892410403742492],[0.45765868340767657,0.8389807902374496],[0.42973145827541137,0.8924104037424917],[0.4576586834006455,0.8389807902418371],[0.4297314582667466,0.8924104037468474],[0.45765868340112154,0.8389807902427644]\n\t\t],\n\t\t\"faceOrders\":[\n\t\t\t[0,2,-1],[4,2,-1],[27,2,-1],[0,3,-1],[4,3,-1],[28,3,-1],[4,0,1],[28,0,1],[27,0,1],[27,4,-1],[28,4,-1],[26,0,1],[25,0,1],[25,4,-1],[26,4,-1],[5,0,1],[26,3,-1],[25,2,-1],[5,3,-1],[5,2,-1],[24,3,-1],[24,2,-1],[1,3,-1],[1,2,-1],[1,29,1],[1,9,1],[1,30,1],[1,16,1],[1,35,-1],[1,39,-1],[1,12,-1],[24,29,1],[47,9,1],[24,9,1],[1,36,-1],[1,37,-1],[1,20,-1],[24,30,1],[48,16,1],[24,16,1],[1,0,1],[1,43,1],[24,35,-1],[1,28,1],[1,11,1],[47,12,-1],[24,12,-1],[24,39,-1],[5,29,1],[42,9,1],[50,9,1],[5,9,1],[1,46,1],[24,36,-1],[1,27,1],[1,19,1],[48,20,-1],[24,20,-1],[24,37,-1],[5,30,1],[41,16,1],[49,16,1],[5,16,1],[1,4,-1],[24,0,1],[24,43,1],[1,26,-1],[1,15,-1],[24,28,1],[47,11,1],[24,11,1],[42,12,-1],[50,12,-1],[5,12,-1],[5,39,-1],[5,35,-1],[26,29,1],[34,9,1],[45,9,1],[7,9,1],[26,9,1],[24,46,1],[1,25,-1],[1,22,-1],[24,27,1],[48,19,1],[24,19,1],[41,20,-1],[49,20,-1],[5,20,-1],[5,37,-1],[5,36,-1],[25,30,1],[33,16,1],[44,16,1],[6,16,1],[25,16,1],[24,4,-1],[5,4,-1],[1,40,1],[1,14,1],[24,26,-1],[47,15,-1],[24,15,-1],[5,28,1],[42,11,1],[50,11,1],[5,11,1],[34,12,-1],[45,12,-1],[7,12,-1],[26,12,-1],[5,43,1],[26,35,-1],[31,9,1],[40,9,1],[7,3,-1],[1,38,1],[1,23,1],[24,25,-1],[48,22,-1],[24,22,-1],[5,27,1],[41,19,1],[49,19,1],[5,19,1],[33,20,-1],[44,20,-1],[6,20,-1],[25,20,-1],[5,46,1],[25,36,-1],[32,16,1],[38,16,1],[6,2,-1],[26,28,1],[25,27,1],[1,45,-1],[1,34,-1],[1,10,-1],[47,14,1],[24,14,1],[5,26,-1],[42,15,-1],[50,15,-1],[5,15,-1],[34,11,1],[45,11,1],[7,11,1],[26,11,1],[31,12,-1],[40,12,-1],[7,29,1],[26,43,1],[31,3,-1],[1,44,-1],[1,33,-1],[1,18,-1],[48,23,1],[24,23,1],[5,25,-1],[41,22,-1],[49,22,-1],[5,22,-1],[33,19,1],[44,19,1],[6,19,1],[25,19,1],[32,20,-1],[38,20,-1],[6,30,1],[25,46,1],[32,2,-1],[1,5,1],[1,42,1],[1,31,1],[1,13,1],[47,10,-1],[24,10,-1],[42,14,1],[50,14,1],[5,14,1],[26,14,1],[26,15,-1],[34,15,-1],[45,15,-1],[7,15,-1],[31,11,1],[40,11,1],[7,39,-1],[7,35,-1],[31,29,1],[26,39,-1],[1,41,1],[1,32,1],[1,21,1],[48,18,-1],[24,18,-1],[41,23,1],[49,23,1],[5,23,1],[25,22,-1],[25,23,1],[33,22,-1],[44,22,-1],[6,22,-1],[32,19,1],[38,19,1],[6,37,-1],[6,36,-1],[32,30,1],[25,37,-1],[1,24,-1],[24,5,1],[1,8,-1],[47,13,1],[24,13,1],[42,10,-1],[50,10,-1],[5,10,-1],[34,14,1],[45,14,1],[7,14,1],[40,14,1],[7,26,-1],[40,15,-1],[31,15,-1],[7,28,1],[7,43,1],[7,0,1],[31,35,-1],[1,17,-1],[48,21,1],[24,21,1],[41,18,-1],[49,18,-1],[5,18,-1],[33,23,1],[44,23,1],[6,23,1],[38,22,-1],[38,23,1],[6,25,-1],[32,22,-1],[6,27,1],[6,46,1],[6,0,1],[32,36,-1],[24,17,-1],[24,8,-1],[5,17,-1],[5,21,1],[5,8,-1],[5,13,1],[47,8,-1],[42,13,1],[50,13,1],[34,10,-1],[45,10,-1],[7,10,-1],[26,10,-1],[31,14,1],[7,40,1],[31,26,-1],[7,4,-1],[31,28,1],[31,0,1],[31,43,1],[48,17,-1],[41,21,1],[49,21,1],[33,18,-1],[44,18,-1],[6,18,-1],[25,18,-1],[32,23,1],[6,38,1],[32,25,-1],[6,4,-1],[32,27,1],[32,0,1],[32,46,1],[41,17,-1],[42,8,-1],[25,17,-1],[44,17,-1],[25,21,1],[44,21,1],[45,8,-1],[26,8,-1],[45,13,1],[26,13,1],[50,8,-1],[34,13,1],[7,13,1],[31,10,-1],[40,10,-1],[31,8,-1],[14,8,-1],[7,45,-1],[31,4,-1],[31,13,1],[31,39,-1],[49,17,-1],[33,21,1],[6,21,1],[32,18,-1],[38,18,-1],[32,17,-1],[23,17,-1],[6,44,-1],[32,4,-1],[32,21,1],[32,37,-1],[33,17,-1],[34,8,-1],[38,17,-1],[6,17,-1],[38,21,1],[40,8,-1],[7,8,-1],[40,13,1],[7,34,-1],[7,31,1],[7,5,1],[7,42,1],[6,33,-1],[6,32,1],[6,5,1],[6,41,1],[31,5,1],[7,24,-1],[32,5,1],[6,24,-1],[31,24,-1],[1,7,-1],[32,24,-1],[1,6,-1],[0,35,-1],[4,35,-1],[28,35,-1],[0,29,1],[0,43,1],[4,43,1],[35,43,1],[4,29,1],[28,29,1],[28,43,1],[0,39,-1],[4,39,-1],[35,39,-1],[43,39,-1],[28,39,-1],[0,36,-1],[4,36,-1],[27,36,-1],[0,30,1],[0,46,1],[4,46,1],[36,46,1],[4,30,1],[27,30,1],[27,46,1],[0,37,-1],[4,37,-1],[36,37,-1],[46,37,-1],[27,37,-1],[1,47,-1],[1,50,1],[1,48,-1],[1,49,1],[16,2,-1],[38,2,-1],[33,2,-1],[16,30,1],[16,0,1],[41,2,-1],[44,2,-1],[48,2,-1],[49,2,-1],[38,30,1],[33,30,1],[16,36,-1],[41,30,1],[44,30,1],[48,30,1],[49,30,1],[16,4,-1],[33,0,1],[16,37,-1],[38,0,1],[41,0,1],[44,0,1],[16,46,1],[48,0,1],[49,0,1],[33,36,-1],[38,36,-1],[41,36,-1],[44,36,-1],[48,36,-1],[49,36,-1],[16,27,1],[33,4,-1],[38,4,-1],[41,4,-1],[44,4,-1],[48,4,-1],[49,4,-1],[33,37,-1],[33,46,1],[38,37,-1],[41,37,-1],[44,37,-1],[48,37,-1],[49,37,-1],[38,46,1],[41,46,1],[44,46,1],[48,46,1],[49,46,1],[22,2,-1],[16,22,-1],[33,27,1],[38,27,1],[41,27,1],[44,27,1],[48,27,1],[49,27,1],[22,30,1],[22,0,1],[23,2,-1],[22,36,-1],[23,30,1],[22,4,-1],[23,0,1],[22,37,-1],[22,46,1],[23,16,1],[23,36,-1],[23,20,-1],[22,27,1],[23,4,-1],[23,37,-1],[23,46,1],[23,22,-1],[23,27,1],[23,19,1],[2,30,1],[2,36,-1],[30,36,-1],[2,46,1],[2,37,-1],[30,46,1],[30,37,-1],[9,3,-1],[34,3,-1],[40,3,-1],[9,29,1],[9,0,1],[42,3,-1],[45,3,-1],[47,3,-1],[50,3,-1],[34,29,1],[40,29,1],[9,35,-1],[42,29,1],[45,29,1],[47,29,1],[50,29,1],[9,4,-1],[34,0,1],[9,39,-1],[40,0,1],[42,0,1],[9,43,1],[45,0,1],[47,0,1],[50,0,1],[34,35,-1],[40,35,-1],[42,35,-1],[45,35,-1],[47,35,-1],[50,35,-1],[9,28,1],[34,4,-1],[40,4,-1],[42,4,-1],[45,4,-1],[47,4,-1],[50,4,-1],[34,39,-1],[34,43,1],[40,39,-1],[42,39,-1],[45,39,-1],[47,39,-1],[50,39,-1],[40,43,1],[42,43,1],[45,43,1],[47,43,1],[50,43,1],[15,3,-1],[9,15,-1],[34,28,1],[40,28,1],[42,28,1],[45,28,1],[47,28,1],[50,28,1],[15,29,1],[14,3,-1],[15,0,1],[15,35,-1],[14,29,1],[14,9,1],[14,0,1],[15,4,-1],[15,39,-1],[15,43,1],[14,35,-1],[14,12,-1],[14,15,-1],[14,4,-1],[14,28,1],[14,39,-1],[14,43,1],[15,28,1],[14,11,1],[3,29,1],[3,35,-1],[29,35,-1],[3,43,1],[3,39,-1],[29,43,1],[29,39,-1],[44,5,1],[44,25,-1],[38,5,1],[41,5,1],[38,25,-1],[38,44,-1],[41,25,-1],[33,5,1],[38,24,-1],[48,5,1],[38,41,1],[33,25,-1],[23,18,-1],[44,24,-1],[44,41,1],[48,25,-1],[33,24,-1],[49,5,1],[48,44,-1],[23,21,1],[49,25,-1],[41,24,-1],[48,24,-1],[48,41,1],[48,38,1],[49,24,-1],[49,44,-1],[49,38,1],[49,41,1],[45,5,1],[45,26,-1],[40,5,1],[42,5,1],[40,26,-1],[40,45,-1],[42,26,-1],[34,5,1],[40,24,-1],[47,5,1],[40,42,1],[34,26,-1],[14,10,-1],[45,24,-1],[45,42,1],[47,26,-1],[34,24,-1],[50,5,1],[47,45,-1],[14,13,1],[50,26,-1],[42,24,-1],[47,24,-1],[47,42,1],[47,40,1],[50,24,-1],[50,45,-1],[50,40,1],[50,42,1],[6,49,1],[6,48,-1],[49,48,-1],[7,50,1],[7,47,-1],[50,47,-1],[8,13,1],[34,31,1],[50,31,1],[42,31,1],[50,34,-1],[47,31,1],[42,34,-1],[45,31,1],[40,31,1],[47,34,-1],[45,34,-1],[40,34,-1],[8,10,-1],[13,10,-1],[12,9,1],[12,29,1],[12,3,-1],[12,15,-1],[12,0,1],[12,4,-1],[12,28,1],[12,35,-1],[12,39,-1],[12,43,1],[11,29,1],[11,3,-1],[11,15,-1],[11,0,1],[11,4,-1],[11,28,1],[11,35,-1],[11,39,-1],[11,12,-1],[11,43,1],[11,9,1],[20,16,1],[20,30,1],[20,2,-1],[20,22,-1],[20,0,1],[20,4,-1],[20,27,1],[20,36,-1],[20,37,-1],[20,46,1],[19,30,1],[19,2,-1],[19,22,-1],[19,0,1],[19,4,-1],[19,27,1],[19,36,-1],[19,37,-1],[19,20,-1],[19,46,1],[19,16,1],[51,16,1],[51,22,-1],[51,20,-1],[51,19,1],[54,16,1],[51,54,1],[55,16,1],[51,55,-1],[54,20,-1],[54,22,-1],[55,20,-1],[55,22,-1],[54,19,1],[54,55,-1],[55,19,1],[17,21,1],[33,32,1],[49,32,1],[41,32,1],[49,33,-1],[48,32,1],[41,33,-1],[44,32,1],[38,32,1],[48,33,-1],[44,33,-1],[38,33,-1],[17,18,-1],[21,18,-1],[17,52,1],[23,52,1],[21,52,1],[18,52,1],[17,53,-1],[53,52,1],[17,56,1],[56,52,1],[21,53,-1],[23,53,-1],[21,56,1],[23,56,1],[18,53,-1],[56,53,-1],[18,56,1],[58,22,-1],[57,22,-1],[58,19,1],[57,19,1],[58,55,-1],[58,16,1],[58,20,-1],[57,16,1],[57,20,-1],[58,51,-1],[58,54,1],[23,57,-1],[23,58,1],[18,57,-1],[57,58,1],[18,58,1],[23,51,-1],[23,54,1],[23,55,-1],[56,57,-1],[18,16,1],[17,57,-1],[18,19,1],[18,20,-1],[21,57,-1],[18,22,-1],[57,54,1],[57,55,-1],[56,58,1],[56,55,-1],[17,58,1],[21,58,1],[57,51,-1],[56,22,-1],[18,51,-1],[18,54,1],[18,55,-1],[56,51,-1],[56,54,1],[56,16,1],[56,19,1],[56,20,-1],[18,2,-1],[17,16,1],[21,16,1],[18,0,1],[18,4,-1],[18,27,1],[18,30,1],[18,36,-1],[18,37,-1],[18,46,1],[52,57,-1],[17,19,1],[17,20,-1],[17,22,-1],[17,51,-1],[17,54,1],[17,55,-1],[21,20,-1],[21,19,1],[53,57,-1],[21,22,-1],[21,51,-1],[21,54,1],[21,55,-1],[52,54,1],[53,54,1],[52,55,-1],[53,55,-1],[53,58,1],[53,22,-1],[52,51,-1],[53,51,-1],[53,16,1],[53,19,1],[53,20,-1],[21,2,-1],[17,2,-1],[52,16,1],[17,0,1],[17,4,-1],[17,27,1],[17,30,1],[17,36,-1],[17,37,-1],[17,46,1],[21,0,1],[21,4,-1],[21,27,1],[21,30,1],[21,36,-1],[21,37,-1],[21,46,1],[52,58,1],[52,19,1],[52,20,-1],[52,22,-1],[8,0,1],[8,35,-1],[8,4,-1],[8,3,-1],[8,9,1],[10,0,1],[8,11,1],[8,12,-1],[13,0,1],[8,15,-1],[8,28,1],[8,29,1],[8,39,-1],[8,43,1],[10,35,-1],[13,35,-1],[10,4,-1],[13,4,-1],[10,3,-1],[13,3,-1],[13,12,-1],[13,9,1],[10,9,1],[10,11,1],[10,12,-1],[10,15,-1],[10,28,1],[10,29,1],[10,39,-1],[10,43,1],[13,15,-1],[13,11,1],[13,28,1],[13,29,1],[13,39,-1],[13,43,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/cube-octagon.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_attributes\": [\"3D\"],\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,5],[0,4],[0,3],[0,2],[0,1],[5,5],[4,4],[3,3],[2,2],[1,1],[1,2],[0.5,1.5],[4,3],[4.5,3.5],[5,4],[1,5],[1,4],[1,3],[2,1],[3,1],[4,1],[5,1],[2,3],[2,4],[2,5],[5,2],[4,2],[3,2],[1.5,4.5],[3.5,0.5],[5,3],[3,5],[3,4],[4,5]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[6,7],[7,8],[8,9],[9,10],[10,0],[11,12],[12,13],[14,15],[15,0],[16,17],[17,10],[18,19],[19,20],[21,22],[22,23],[23,16],[16,15],[15,1],[10,15],[15,24],[24,25],[25,26],[26,27],[9,17],[17,15],[15,2],[2,24],[24,14],[14,28],[28,29],[29,30],[31,32],[32,33],[33,14],[14,16],[16,9],[8,22],[22,34],[34,30],[31,26],[26,35],[35,3],[25,35],[35,4],[8,23],[23,28],[28,13],[13,18],[18,36],[37,38],[38,13],[13,33],[33,25],[25,3],[5,26],[26,33],[28,22],[22,6],[39,12],[12,18],[18,32],[32,26],[26,4],[7,22],[22,29],[29,38],[38,12],[12,20],[29,34],[34,21],[5,27],[27,31],[31,36],[36,20],[20,11],[11,39],[39,37],[37,30],[30,21],[21,6],[37,12],[12,19],[19,36]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"F\",\"F\",\"M\",\"V\",\"F\",\"F\",\"F\",\"M\",\"V\",\"F\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"F\",\"F\",\"M\",\"V\",\"F\",\"F\",\"F\",\"M\",\"V\",\"F\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,-180,-180,-180,-180,180,180,180,180,90,90,90,-90,-90,90,90,90,-90,-90,180,-180,180,0,0,-90,90,0,0,0,-90,90,0,180,-180,180,180,-180,180,180,180,0,0,-90,90,0,0,0,-90,90,0,-180,-180,-180,-180,-90,-90,90,90,90,-90,-90,90,90,90,180,180,0,0,0,0,0,0,0,0,0,0,180,-180,180\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,15],[0,15,10],[1,2,15],[2,3,25,24],[2,24,15],[3,4,35],[3,35,25],[4,5,26],[4,26,35],[5,27,26],[6,7,22],[6,22,21],[7,8,22],[8,23,22],[8,9,16,23],[9,10,17],[9,17,16],[10,15,17],[11,39,12],[11,12,20],[12,39,37],[12,37,38],[12,38,13],[12,13,18],[12,18,19],[12,19,20],[13,38,29,28],[13,28,14,33],[13,33,32,18],[14,28,23,16],[14,16,15],[14,15,24],[14,24,25,33],[15,16,17],[18,36,19],[18,32,31,36],[19,36,20],[21,22,34],[21,34,30],[22,29,34],[22,23,28],[22,28,29],[25,26,33],[25,35,26],[26,27,31],[26,31,32],[26,32,33],[29,38,37,30],[29,30,34]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_classes\":[\"creasePattern\"],\n\t\t\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,5],[0,4],[0,3],[0,2],[0,1],[5,5],[4,4],[3,3],[2,2],[1,1],[1,2],[0.5,1.5],[4,3],[4.5,3.5],[5,4],[1,5],[1,4],[3,1],[4,1],[5,1],[2,3],[2,4],[3,2],[1.5,4.5],[2,5],[5,2],[3.5,0.5],[4,5],[5,3],[3,5]],\n\t\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[6,7],[7,8],[8,9],[9,10],[10,0],[11,12],[12,13],[14,15],[15,0],[16,17],[17,10],[18,19],[19,20],[21,22],[22,16],[16,15],[15,1],[10,15],[15,23],[23,24],[24,25],[9,17],[17,15],[15,2],[14,26],[26,27],[28,14],[14,16],[8,22],[22,29],[29,30],[31,24],[24,32],[32,3],[23,32],[32,4],[26,13],[13,18],[13,28],[28,23],[5,24],[24,28],[26,22],[22,6],[33,12],[12,18],[18,24],[24,4],[7,22],[22,27],[27,12],[12,20],[27,29],[29,21],[5,25],[25,31],[31,34],[34,20],[20,11],[11,33],[33,35],[35,30],[30,21],[21,6],[35,12],[12,19],[19,34]],\n\t\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\"],\n\t\t\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,-180,-180,-180,-180,180,180,180,180,90,90,-90,-90,90,90,-90,-90,180,-180,180,-90,90,-90,90,180,-180,180,180,-180,180,180,180,-90,90,-90,90,-180,-180,-180,-180,-90,-90,90,90,-90,-90,90,90,180,180,0,0,0,0,0,0,0,0,0,0,180,-180,180],\n\t\t\"faces_vertices\":[[0,1,15],[0,15,10],[1,2,15],[2,3,32,23,15],[3,4,32],[4,5,24],[4,24,32],[5,25,24],[6,7,22],[6,22,21],[7,8,22],[8,9,17,16,22],[9,10,17],[10,15,17],[11,33,12],[11,12,20],[12,33,35],[12,35,30,29,27],[12,27,26,13],[12,13,18],[12,18,19],[12,19,20],[13,26,14,28],[13,28,24,18],[14,26,22,16],[14,16,15],[14,15,23,28],[15,16,17],[18,24,31,34,19],[19,34,20],[21,22,29],[21,29,30],[22,27,29],[22,26,27],[23,24,28],[23,32,24],[24,25,31]]\n\t},{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[2,1,1],[2,1,0],[2,0,0],[3,0,0],[3,1,0],[3,1,1],[2,2,1],[2,2,0],[1,2,0],[1,1,0],[2,1,0],[3,2,1],[4,2,0],[3,2,1],[2,1,1],[1,1,0],[2,1,0],[1.5,0.5,0],[3,2,0],[3.5,2.5,0],[3,2,0],[2,2,0],[2,3,0],[2,2,0],[2,1,0],[3,1,0],[3,0,0],[3,1,0],[2,2,1],[2,2,0],[2,3,0],[4,1,0],[3,1,0],[3,1,1],[1.5,2.5,0],[3.5,0.5,0],[4,2,0],[3,3,0],[3,2,0],[3,2,0]\n\t\t],\n\t\t\"faceOrders\": [\n\t\t\t[37,48,1],[13,48,1],[16,33,1],[2,16,1],[16,17,-1],[4,16,1],[6,43,1],[6,44,1],[30,31,-1],[40,41,-1],[12,48,1],[39,48,1],[42,46,-1],[22,23,-1],[20,34,1],[24,34,1],[21,34,1],[25,34,1],[6,45,-1],[6,8,-1],[0,1,-1],[18,19,-1],[15,17,1],[25,36,1],[15,16,1],[17,33,-1],[2,4,1],[12,13,1],[37,39,-1],[38,48,1],[44,45,1],[8,43,-1],[5,6,1],[5,8,1],[37,38,1],[20,21,1],[24,25,-1],[34,36,1],[10,11,-1],[7,9,-1],[38,39,-1],[15,33,-1],[5,43,-1],[24,36,1],[13,39,-1],[2,33,-1],[4,33,-1],[43,44,1],[43,45,-1],[12,39,-1],[20,24,-1],[21,24,-1],[2,17,-1],[20,25,-1],[1,30,-1],[0,31,1],[10,40,1],[11,41,-1],[9,46,1],[7,42,-1],[18,22,1],[19,23,-1],[12,37,-1],[8,44,-1],[13,37,-1],[4,17,-1],[8,45,1],[21,25,-1],[0,30,-1],[10,41,-1],[9,42,-1],[19,22,1],[2,15,1],[20,36,1],[12,38,1],[5,44,-1],[18,23,-1],[1,31,1],[11,40,1],[7,46,1],[4,15,1],[21,36,1],[13,38,1],[5,45,1]\n\t\t]\n\t},{\n\t\t\"frame_parent\":1,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[2,2,0],[2,2,-1],[2,1,-1],[3,1,-1],[3,2,-1],[3,2,0],[2,3,0],[2,3,-1],[1,3,-1],[1,2,-1],[2,2,-1],[3,3,0],[4,3,-1],[3,3,0],[2,2,0],[1,2,-1],[2,2,-1],[1.5,1.5,-1],[3,3,-1],[3.5,3.5,-1],[3,3,-1],[2,3,-1],[2,4,-1],[3,2,-1],[3,1,-1],[3,2,-1],[2,3,0],[2,3,-1],[3,2,0],[1.5,3.5,-1],[2,4,-1],[4,2,-1],[3.5,1.5,-1],[3,3,-1],[4,3,-1],[3,4,-1]\n\t\t],\n\t\t\"faceOrders\": [\n\t\t\t[25,26,-1],[0,1,-1],[14,15,-1],[18,19,-1],[11,27,1],[12,13,1],[20,28,1],[21,29,1],[11,12,1],[13,27,-1],[2,3,1],[10,11,1],[30,32,-1],[17,31,1],[28,36,1],[6,35,-1],[3,4,1],[3,35,1],[4,6,1],[17,32,1],[30,31,1],[16,17,1],[20,21,-1],[28,29,1],[24,33,-1],[8,9,-1],[5,7,-1],[23,34,-1],[12,27,-1],[11,13,-1],[21,28,1],[20,29,1],[17,30,-1],[31,32,-1],[3,6,-1],[4,35,-1],[3,27,-1],[2,13,-1],[16,21,-1],[17,20,-1],[1,25,-1],[0,26,1],[8,24,1],[9,33,-1],[7,23,1],[5,34,-1],[14,18,1],[15,19,-1],[11,32,-1],[10,30,-1],[6,36,-1],[28,35,-1],[3,11,1],[17,28,1],[3,28,-1],[11,17,1],[2,27,-1],[16,20,-1],[0,25,-1],[8,33,-1],[5,23,1],[15,18,1],[11,30,-1],[6,28,1],[2,11,1],[16,28,1],[3,36,1],[10,17,1],[2,12,1],[16,29,1],[10,31,1],[4,36,-1],[3,13,-1],[17,21,-1],[1,26,1],[9,24,1],[7,34,-1],[14,19,-1],[10,32,-1],[35,36,1],[3,12,1],[17,29,1],[4,28,1],[11,31,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/cycles-3d.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"overlapping cycles in 3D\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"vertices_coords\": [\n\t\t[-3, -3, 3], [3, -3, 3], [3, 3, 3], [-3, 3, 3],\n\t\t[-3, -3, 1], [3, -3, 1], [3, 3, 1], [-3, 3, 1],\n\t\t[-3, -3, -1], [3, -3, -1], [3, 3, -1], [-3, 3, -1],\n\t\t[-3, -3, -3], [3, -3, -3], [3, 3, -3], [-3, 3, -3],\n\n\t\t[-3, -3, -3], [3, -3, -3], [3, -3, 3], [-3, -3, 3],\n\t\t[-3, -1, -3], [3, -1, -3], [3, -1, 3], [-3, -1, 3],\n\t\t[-3, 1, -3], [3, 1, -3], [3, 1, 3], [-3, 1, 3],\n\t\t[-3, 3, -3], [3, 3, -3], [3, 3, 3], [-3, 3, 3],\n\n\t\t[3, -3, -3], [3, 3, -3], [3, 3, 3], [3, -3, 3],\n\t\t[1, -3, -3], [1, 3, -3], [1, 3, 3], [1, -3, 3],\n\t\t[-1, -3, -3], [-1, 3, -3], [-1, 3, 3], [-1, -3, 3],\n\t\t[-3, -3, -3], [-3, 3, -3], [-3, 3, 3], [-3, -3, 3]\n\t],\n\t\"faces_vertices\": [\n\t\t[0, 4, 5, 1], [1, 5, 6, 2], [2, 6, 7, 3], [3, 7, 4, 0],\n\t\t[8, 12, 13, 9], [9, 13, 14, 10], [10, 14, 15, 11], [11, 15, 12, 8],\n\t\t[16, 20, 21, 17], [17, 21, 22, 18], [18, 22, 23, 19], [19, 23, 20, 16],\n\t\t[24, 28, 29, 25], [25, 29, 30, 26], [26, 30, 31, 27], [27, 31, 28, 24],\n\t\t[32, 36, 37, 33], [33, 37, 38, 34], [34, 38, 39, 35], [35, 39, 36, 32],\n\t\t[40, 44, 45, 41], [41, 45, 46, 42], [42, 46, 47, 43], [43, 47, 44, 40]\n\t],\n\t\"faceOrders\": [\n\t\t[0, 19, 1], [19, 4, 1], [4, 23, 1], [23, 0, 1],\n\t\t[2, 17, 1], [17, 6, 1], [6, 21, 1], [21, 2, 1],\n\t\t[8, 16, 1], [16, 12, 1], [12, 20, 1], [20, 8, 1],\n\t\t[10, 18, 1], [18, 14, 1], [14, 22, 1], [22, 10, 1],\n\t\t[11, 3, 1], [7, 11, 1], [15, 7, 1], [3, 15, 1],\n\t\t[9, 1, 1], [5, 9, 1], [13, 5, 1], [1, 13, 1]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/disjoint-cps.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[0,3],[0,2],[0,1],[1,1],[1,0.4142135623730949],[0.41421356237309487,1],[-1,2],[-1,3]\n\t],\n\t\"edges_vertices\":[[0,1],[2,3],[4,0],[0,5],[0,6],[0,7],[5,7],[7,4],[1,6],[6,5],[8,9],[3,8],[8,2],[9,2]],\n\t\"edges_assignment\":[\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"F\"],\n\t\"faces_vertices\":[[0,1,6],[0,6,5],[0,5,7],[0,7,4],[2,9,8],[2,8,3]]\n}"
  },
  {
    "path": "tests/files/fold/disjoint-triangles-3d.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"separated triangles overlapping in 3D\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"frame_attributes\": [\"3D\"],\n\t\"vertices_coords\":[\n\t\t[-0.5,0,0],[0.5,0,1],[0.5,1,0],[0.5,0,0],[0,0,0],[0,0.5,0],[0,0,0.5],\n\t\t[0.5,0,0],[-0.5,1,0],[-0.5,0,1],[-0.5,0,0],[0,0,0],[0,0,0.5],[0,0.5,0],\n\t\t[0.6,0,0],[1.6,0,1],[1.6,1,0],[1.6,0,0],\n\t\t[0.6,0,0],[1.6,1,0],[1.6,0,-1],[1.6,0,0],\n\t\t[1.1,0,0.5],[1.6,-0.5,0.5],[1.6,0,1],[1.6,0,0.5]\n\t],\n\t\"edges_vertices\":[\n\t\t[1,3],[3,2],[3,4],[4,0],[2,5],[5,0],[0,6],[6,1],[5,4],[4,6],\n\t\t[8,10],[10,9],[7,11],[11,10],[9,12],[12,7],[7,13],[13,8],[12,11],[11,13],\n\t\t[14,15],[16,14],[15,17],[17,16],[17,14],\n\t\t[18,19],[20,18],[19,21],[21,20],[21,18],\n\t\t[22,23],[24,22],[23,25],[25,24],[25,22]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\n\t\t\"B\",\"B\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,90,90,0,0,0,0,0,0,\n\t\t0,0,90,90,0,0,0,0,0,0,\n\t\t0,0,0,0,90,\n\t\t0,0,0,0,90,\n\t\t0,0,0,0,90\n\t],\n\t\"faces_vertices\":[\n\t\t[2,5,4,3],[5,0,4],[3,4,6,1],[4,0,6],\n\t\t[11,13,8,10],[11,7,13],[12,11,10,9],[12,7,11],\n\t\t[17,16,14],[17,14,15],\n\t\t[21,20,18],[21,18,19],\n\t\t[25,24,22],[25,22,23]\n\t],\n\t\"faceOrders\": [\n\t\t[1,4,1],[2,7,1],[3,6,1],[0,5,1],[8,11,-1],[9,12,-1]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"creasePattern\"],\n\t\t\"frame_attributes\": [\"2D\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0],[1,-1],[1,1],[1,0],[0.5,0],[0.5,0.5],[0.5,-0.5],\n\t\t\t[-0.5,0],[-1.5,1],[-1.5,-1],[-1.5,0],[-1,0],[-1,-0.5],[-1,0.5],\n\t\t\t[1.5,0],[2.5,-1],[2.5,1],[2.5,0],\n\t\t\t[3,0],[4,-1],[4,1],[4,0],\n\t\t\t[4.5,0],[5,-0.5],[5,0.5],[5,0]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/fan-cp.fold",
    "content": "{\"file_spec\":1.1,\"file_creator\":\"Rabbit Ear\",\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.41421356237309503,1],[1,0.41421356237309503],[0.19891236737965798,1],[0.6681786379192989,1],[1,0.6681786379192989],[1,0.19891236737965798],[0.09849140335716426,1],[0.3033466836073424,1],[0.5345111359507916,1],[0.8206787908286601,1],[1,0.8206787908286601],[1,0.5345111359507916],[1,0.3033466836073424],[1,0.09849140335716426]],\"edges_vertices\":[[0,1],[3,0],[2,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[6,10],[10,3],[10,0],[4,11],[11,6],[11,0],[7,12],[12,4],[12,0],[2,13],[13,7],[13,0],[8,14],[14,2],[14,0],[5,15],[15,8],[15,0],[9,16],[16,5],[16,0],[1,17],[17,9],[17,0]],\"edges_assignment\":[\"B\",\"B\",\"F\",\"M\",\"V\",\"V\",\"M\",\"V\",\"F\",\"B\",\"B\",\"V\",\"B\",\"B\",\"F\",\"B\",\"B\",\"V\",\"B\",\"B\",\"V\",\"B\",\"B\",\"M\",\"B\",\"B\",\"F\",\"B\",\"B\",\"M\",\"B\",\"B\",\"V\"],\"edges_foldAngle\":[0,0,0,-180,180,180,-180,180,0,0,0,180,0,0,0,0,0,180,0,0,180,0,0,-180,0,0,0,0,0,-180,0,0,180],\"faces_vertices\":[[17,9,0],[17,0,1],[0,9,16],[0,16,5],[0,5,15],[0,15,8],[0,8,14],[0,14,2],[0,2,13],[0,13,7],[0,7,12],[0,12,4],[0,4,11],[0,11,6],[0,6,10],[0,10,3]],\"faces_edges\":[[8,32,31],[0,30,32],[27,29,8],[28,4,29],[24,26,4],[25,7,26],[21,23,7],[22,2,23],[18,20,2],[19,6,20],[15,17,6],[16,3,17],[12,14,3],[13,5,14],[9,11,5],[10,1,11]]}"
  },
  {
    "path": "tests/files/fold/fan-flat-cp.fold",
    "content": "{\"file_spec\":1.1,\"file_creator\":\"Rabbit Ear\",\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.41421356237309503,1],[1,0.41421356237309503],[0.19891236737965798,1],[0.6681786379192989,1],[1,0.6681786379192989],[1,0.19891236737965798],[0.09849140335716426,1],[0.3033466836073424,1],[0.5345111359507916,1],[0.8206787908286601,1],[1,0.8206787908286601],[1,0.5345111359507916],[1,0.3033466836073424],[1,0.09849140335716426]],\"edges_vertices\":[[0,1],[3,0],[2,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[6,10],[10,3],[10,0],[4,11],[11,6],[11,0],[7,12],[12,4],[12,0],[2,13],[13,7],[13,0],[8,14],[14,2],[14,0],[5,15],[15,8],[15,0],[9,16],[16,5],[16,0],[1,17],[17,9],[17,0]],\"edges_assignment\":[\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"faces_vertices\":[[17,9,0],[17,0,1],[0,9,16],[0,16,5],[0,5,15],[0,15,8],[0,8,14],[0,14,2],[0,2,13],[0,13,7],[0,7,12],[0,12,4],[0,4,11],[0,11,6],[0,6,10],[0,10,3]],\"faces_edges\":[[8,32,31],[0,30,32],[27,29,8],[28,4,29],[24,26,4],[25,7,26],[21,23,7],[22,2,23],[18,20,2],[19,6,20],[15,17,6],[16,3,17],[12,14,3],[13,5,14],[9,11,5],[10,1,11]]}"
  },
  {
    "path": "tests/files/fold/fan-folded-through-cp.fold",
    "content": "{\"file_spec\":1.1,\"file_creator\":\"Rabbit Ear\",\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.41421356237309503,1],[1,0.41421356237309503],[0.19891236737965798,1],[0.6681786379192989,1],[1,0.6681786379192989],[1,0.19891236737965798],[0.09849140335716426,1],[0.3033466836073424,1],[0.5345111359507916,1],[0.8206787908286601,1],[1,0.8206787908286601],[1,0.5345111359507916],[1,0.3033466836073424],[1,0.09849140335716426],[0.08966060458566985,0.9103393954143302],[0.19509032201612825,0.9807852804032304],[0.32544550765210234,0.7856949583871023],[0.26553620700502856,0.8753555629727721],[0.43120740190360446,0.8067323071512256],[0.4724736459167271,0.7071067811865476],[0.5803075381709217,0.7071067811865476],[0.6197842773467515,0.508643811307547],[0.6013448869350451,0.6013448869350451],[0.7071067811865472,0.4724736459167268],[0.7071067811865475,0.2928932188134524],[0.7071067811865474,0.3779564488505292],[0.7672555679753956,0.23274443202460413],[0.7506605550357304,0.14931566810068517],[0.7353665035184762,0.0724272789133858],[0.7653668647301795,0]],\"edges_vertices\":[[3,0],[6,10],[10,3],[4,11],[7,12],[12,4],[2,13],[13,7],[5,15],[15,8],[9,16],[1,17],[17,9],[10,18],[18,0],[3,18],[6,19],[19,0],[11,6],[18,19],[4,20],[20,0],[11,21],[21,0],[19,21],[21,20],[12,22],[22,0],[2,14],[20,22],[7,23],[23,0],[24,0],[22,23],[14,25],[13,24],[2,26],[14,8],[23,24],[25,0],[26,0],[24,26],[26,25],[8,27],[27,0],[5,16],[25,27],[5,28],[28,0],[15,29],[29,0],[30,0],[27,29],[29,28],[16,30],[28,30],[9,31],[31,0],[17,32],[32,0],[33,0],[30,31],[31,32],[1,33],[32,33]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"M\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"B\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"F\",\"B\",\"M\",\"M\",\"F\",\"V\",\"V\",\"V\",\"V\",\"B\",\"M\",\"V\",\"V\",\"F\",\"F\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"B\",\"V\",\"V\",\"B\",\"M\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,180,180,180,180,180,0,-180,-180,-180,0,0,180,180,180,180,0,-180,-180,-180,180,180,-180,180,0,0,-180,-180,0,180,180,180,180,0,-180,180,180,0,0,-180,180,180,-180,-180,0,0,180,180,0,180,180,0,-180],\"faces_vertices\":[[18,10,3],[18,3,0],[6,19,21,11],[19,6,10,18],[19,18,0],[19,0,21],[20,4,11,21],[20,21,0],[22,12,4,20],[22,20,0],[23,7,12,22],[23,22,0],[24,13,7,23],[24,23,0],[24,0,26],[25,26,0],[8,27,29,15],[27,25,0],[27,0,29],[28,5,15,29],[28,29,0],[16,30,31,9],[30,16,5,28],[30,28,0],[30,0,31],[17,32,33,1],[32,17,9,31],[32,31,0],[32,0,33],[24,26,2,13],[14,2,26,25],[14,25,27,8]],\"faces_edges\":[[2,15,13],[0,14,15],[24,22,18,16],[1,13,19,16],[14,17,19],[23,24,17],[3,22,25,20],[23,21,25],[5,20,29,26],[21,27,29],[4,26,33,30],[27,31,33],[7,30,38,35],[31,32,38],[40,41,32],[40,39,42],[52,49,9,43],[39,44,46],[50,52,44],[8,49,53,47],[50,48,53],[61,56,10,54],[45,47,55,54],[48,51,55],[57,61,51],[64,63,11,58],[12,56,62,58],[57,59,62],[59,60,64],[41,36,6,35],[34,28,36,42],[34,46,43,37]]}"
  },
  {
    "path": "tests/files/fold/fish-cp-3d.fold",
    "content": "{\n\t\"file_spec\":1.1,\n\t\"file_creator\":\"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[0,0],[0.7071067811865476,0],[1,0],[0,1],[0,0.7071067811865476],[0.7071067811865475,0.2928932188134523],[0.29289321881345237,0.7071067811865475],[1,0.2928932188134524],[0.2928932188134524,1],[1,1]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[3,4],[4,0],[5,0],[6,0],[5,7],[6,8],[5,9],[6,9],[3,6],[6,5],[5,2],[5,1],[6,4],[2,7],[7,9],[9,8],[8,3]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t0,0,0,0,93.2049770887881,93.20493504245142,-70.10687282200827,-70.10710172666417,93.20496702592628,93.2049286553313,180,39.785314657484214,180,-70.10694839533419,-70.10667899851353,0,0,0,0\n\t],\n\t\"vertices_edges\": [\n\t\t[0,4,5,3],[1,13,0],[15,12,1],[18,2,10],[14,2,3],[6,8,11,4,13,12],[9,7,10,14,5,11],[16,6,15],[17,18,7],[17,9,8,16]\n\t],\n\t\"vertices_vertices\": [\n\t\t[1,5,6,4],[2,5,0],[7,5,1],[8,4,6],[6,3,0],[7,9,6,0,1,2],[9,8,3,4,0,5],[9,5,2],[9,3,6],[8,6,5,7]\n\t],\n\t\"faces_vertices\": [\n\t\t[0,1,5],[0,5,6],[0,6,4],[1,2,5],[2,7,5],[3,4,6],[3,6,8],[5,7,9],[5,9,6],[6,9,8]\n\t],\n\t\"faces_edges\": [\n\t\t[13,4,0],[11,5,4],[14,3,5],[12,13,1],[6,12,15],[14,10,2],[7,18,10],[16,8,6],[9,11,8],[17,7,9]\n\t],\n\t\"vertices_faces\": [\n\t\t[0,1,2,null],[3,0,null],[4,3,null],[null,5,6],[5,null,2],[7,8,1,0,3,4],[9,6,5,2,1,8],[7,4,null],[null,6,9],[9,8,7,null]\n\t],\n\t\"edges_faces\": [\n\t\t[0],[3],[5],[2],[0,1],[1,2],[4,7],[6,9],[7,8],[8,9],[5,6],[1,8],[3,4],[0,3],[2,5],[4],[7],[9],[6]\n\t],\n\t\"faces_faces\": [\n\t\t[3,1],[0,8,2],[1,5],[0,4],[3,7],[2,6],[5,9],[4,8],[1,7,9],[6,8]\n\t],\n\t\"faceOrders\": [\n\t\t[5,6,1],[3,4,1]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/flat-pleat-fish.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.41421356237309503,1],[0.198912367379658,1],[0.6681786379192989,1],[1,0.41421356237309503],[0.7071067811865475,0.2928932188134524],[0.585786437626905,0],[0.7071067811865475,0]],\n\t\"edges_vertices\":[[3,0],[0,2],[0,4],[4,5],[5,3],[0,5],[2,6],[6,4],[0,6],[1,7],[7,2],[0,8],[8,7],[0,9],[2,8],[8,9],[8,1],[9,10],[10,1],[8,10]],\n\t\"edges_assignment\":[\"B\",\"F\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"F\",\"B\",\"B\",\"V\",\"F\",\"B\",\"V\",\"F\",\"V\",\"B\",\"B\",\"M\"],\n\t\"faces_vertices\":[[0,6,4],[0,4,5],[0,5,3],[2,6,0],[7,8,1],[8,7,2],[8,2,0],[8,0,9],[8,9,10],[8,10,1]]\n}\n"
  },
  {
    "path": "tests/files/fold/invalid-box-pleat-3d.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"file_description\": \"invalid because 90deg valley should be 90deg mountain, otherwise valid.\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[52,8],[68,8],[76,8],[52,16],[68,16],[76,16],[52,24],[68,24],[76,24],[76,32],[52,32],[68,32]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[3,4],[4,5],[6,7],[7,8],[7,9],[10,11],[11,9],[10,6],[6,3],[3,0],[4,2],[11,7],[7,4],[4,1],[9,8],[8,5],[5,2]\n\t],\n\t\"edges_assignment\": [\"B\",\"B\",\"M\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"M\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\": [0,0,-90,-90,90,-90,180,0,0,0,0,0,180,-180,-90,-180,0,0,0],\n\t\"vertices_edges\": [\n\t\t[0,11],[1,15,0],[18,12,1],[2,10,11],[3,14,2,15,12],[17,3,18],[4,9,10],[5,6,13,4,14],[16,5,17],[8,6,16],[7,9],[8,7,13]\n\t],\n\t\"vertices_vertices\": [\n\t\t[1,3],[2,4,0],[5,4,1],[4,6,0],[5,7,3,1,2],[8,4,2],[7,10,3],[8,9,11,6,4],[9,7,5],[11,7,8],[11,6],[9,10,7]\n\t],\n\t\"faces_vertices\": [\n\t\t[0,1,4,3],[1,2,4],[2,5,4],[3,4,7,6],[4,5,8,7],[6,7,11,10],[7,8,9],[7,9,11]\n\t],\n\t\"faces_edges\": [\n\t\t[15,2,11,0],[12,15,1],[3,12,18],[14,4,10,2],[17,5,14,3],[13,7,9,4],[16,6,5],[8,13,6]\n\t],\n\t\"vertices_faces\": [\n\t\t[0,null],[1,0,null],[2,1,null],[3,null,0],[4,3,0,1,2],[4,2,null],[5,null,3],[6,7,5,3,4],[6,4,null],[7,6,null],[null,5],[null,5,7]\n\t],\n\t\"edges_faces\": [\n\t\t[0],[1],[0,3],[2,4],[3,5],[4,6],[6,7],[5],[7],[5],[3],[0],[1,2],[5,7],[3,4],[0,1],[6],[4],[2]\n\t],\n\t\"faces_faces\": [\n\t\t[1,3],[0,2],[1,4],[0,4,5],[2,3,6],[3,7],[4,7],[5,6]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/invalid-key-names.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"file_description\": \"FOLD file with invalid keys\",\n\t\"vertices_coord\": [[-1, 0], [1, 0], [0, 1.7320508075688772], [2, 1.7320508075688772]],\n\t\"edges_vertices\": [[0, 1], [1, 2], [2, 0], [1, 3], [3, 2]],\n\t\"faces_vertices\": [[0, 1, 2], [1, 3, 2]],\n\t\"facesOrder\": [[0, 1, 1]]\n}\n"
  },
  {
    "path": "tests/files/fold/invalid-mismatch-length.fold",
    "content": "{\n\t\"file_description\": \"edges_ arrays mismatch in length\",\n\t\"vertices_coords\": [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\"edges_vertices\": [[0, 1], [1, 2], [2, 3], [3, 0]],\n\t\"edges_assignment\": [\"B\", \"B\", \"B\", \"B\", \"V\"],\n\t\"edges_foldAngle\": [0, 0, 0, 0, 180],\n\t\"faces_vertices\": [[0, 1, 2, 3]]\n}"
  },
  {
    "path": "tests/files/fold/invalid-mismatch-references.fold",
    "content": "{\n\t\"file_description\": \"faces vertices contains a vertex index which does not exist\",\n\t\"vertices_coords\": [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\"edges_vertices\": [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t\"edges_assignment\": [\"B\", \"B\", \"B\", \"B\", \"V\"],\n\t\"edges_foldAngle\": [0, 0, 0, 0, 180],\n\t\"faces_vertices\": [[0, 1, 2], [2, 4, 0]]\n}"
  },
  {
    "path": "tests/files/fold/invalid-self-intersect.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[0.5,0],[1,0],[0,1],[0,0.5],[0.5,0.5],[1,1],[0.5,1],[1,0.5],[0.29289321881345254,1],[1,0.2928932188134521]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[3,4],[4,0],[0,5],[5,6],[7,5],[5,1],[4,5],[5,8],[5,9],[5,10],[3,5],[5,2],[2,10],[10,8],[8,6],[6,7],[7,9],[9,3]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"F\",\"M\",\"M\",\"F\",\"M\",\"M\",\"F\",\"F\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,180,0,0,-90,-90,0,-180,-180,0,0,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,5],[0,5,4],[1,2,5],[2,10,5],[3,4,5],[3,5,9],[5,8,6],[5,6,7],[5,7,9],[5,10,8]\n\t],\n\t\"faceOrders\":[\n\t\t[0,1,1],[5,8,-1],[3,9,-1],[4,7,-1],[2,6,-1]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/invalid-single-vertex-2d.fold",
    "content": "{\n\t\"file_description\": \"frame 0 is valid, frame 1 violates Maekawa, frame 2 violates Kawasaki, frame 3 violates Maekawa and Kawasaki, frame 4 violates neither but violates shortest sector assignment\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[0.7071067811865477,0],[1,0],[0,1],[0,0.7071067811865476],[0.5,0.5],[1,1],[0.5,1],[1,0.5]],\n\t\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[5,7],[5,8],[5,1],[5,4],[2,8],[8,6],[6,7],[7,3]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,0,0,180,0,-180,180,180,0,0,0,0],\n\t\"vertices_edges\":[[0,3],[1,7,0],[9,1],[12,2],[2,3,8],[6,4,5,8,7],[11,4,10],[11,12,5],[10,6,9]],\n\t\"vertices_vertices\":[[1,4],[2,5,0],[8,1],[7,4],[3,0,5],[8,6,7,4,1],[7,5,8],[6,3,5],[6,5,2]],\n\t\"faces_vertices\":[[0,1,5,4],[1,2,8,5],[3,4,5,7],[5,8,6],[5,6,7]],\n\t\"faces_edges\":[[0,7,8,3],[1,9,6,7],[2,8,5,12],[6,10,4],[4,11,5]],\n\t\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[null,0,2],[3,4,2,0,1],[4,3,null],[null,2,4],[3,1,null]],\n\t\"edges_faces\":[[0],[1],[2],[0],[3,4],[2,4],[1,3],[0,1],[0,2],[1],[3],[4],[2]],\n\t\"faces_faces\":[[1,2],[3,0],[0,4],[1,4],[3,2]],\n\t\"file_frames\":[\n\t\t{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[0.7071067811865477,0],[1,0],[0,1],[0,0.7071067811865476],[0.5,0.5],[1,1],[0.5,1],[1,0.5]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[5,7],[5,8],[5,1],[5,4],[2,8],[8,6],[6,7],[7,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"M\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,0,-180,-180,180,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[9,1],[12,2],[2,3,8],[6,4,5,8,7],[11,4,10],[11,12,5],[10,6,9]],\"vertices_vertices\":[[1,4],[2,5,0],[8,1],[7,4],[3,0,5],[8,6,7,4,1],[7,5,8],[6,3,5],[6,5,2]],\"faces_vertices\":[[0,1,5,4],[1,2,8,5],[3,4,5,7],[5,8,6],[5,6,7]],\"faces_edges\":[[0,7,8,3],[1,9,6,7],[2,8,5,12],[6,10,4],[4,11,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[null,0,2],[3,4,2,0,1],[4,3,null],[null,2,4],[3,1,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[2,4],[1,3],[0,1],[0,2],[1],[3],[4],[2]],\"faces_faces\":[[1,2],[3,0],[0,4],[1,4],[3,2]]},\n\t\t{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[0.6953124762130963,0],[1,0],[0,1],[0,0.7071067811865476],[0.5,0.5],[1,1],[0.5,1],[1,0.5]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[5,7],[5,8],[5,1],[5,4],[2,8],[8,6],[6,7],[7,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,0,-180,180,180,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[9,1],[12,2],[2,3,8],[6,4,5,8,7],[11,4,10],[11,12,5],[10,6,9]],\"vertices_vertices\":[[1,4],[2,5,0],[8,1],[7,4],[3,0,5],[8,6,7,4,1],[7,5,8],[6,3,5],[6,5,2]],\"faces_vertices\":[[0,1,5,4],[1,2,8,5],[3,4,5,7],[5,8,6],[5,6,7]],\"faces_edges\":[[0,7,8,3],[1,9,6,7],[2,8,5,12],[6,10,4],[4,11,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[null,0,2],[3,4,2,0,1],[4,3,null],[null,2,4],[3,1,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[2,4],[1,3],[0,1],[0,2],[1],[3],[4],[2]],\"faces_faces\":[[1,2],[3,0],[0,4],[1,4],[3,2]]},\n\t\t{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[0.6953124762130963,0],[1,0],[0,1],[0,0.7071067811865476],[0.5,0.5],[1,1],[0.5,1],[1,0.5]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[5,7],[5,8],[5,1],[5,4],[2,8],[8,6],[6,7],[7,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"M\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,0,-180,-180,180,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[9,1],[12,2],[2,3,8],[6,4,5,8,7],[11,4,10],[11,12,5],[10,6,9]],\"vertices_vertices\":[[1,4],[2,5,0],[8,1],[7,4],[3,0,5],[8,6,7,4,1],[7,5,8],[6,3,5],[6,5,2]],\"faces_vertices\":[[0,1,5,4],[1,2,8,5],[3,4,5,7],[5,8,6],[5,6,7]],\"faces_edges\":[[0,7,8,3],[1,9,6,7],[2,8,5,12],[6,10,4],[4,11,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[null,0,2],[3,4,2,0,1],[4,3,null],[null,2,4],[3,1,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[2,4],[1,3],[0,1],[0,2],[1],[3],[4],[2]],\"faces_faces\":[[1,2],[3,0],[0,4],[1,4],[3,2]]},\n\t\t{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[0.7071067811865477,0],[1,0],[0,1],[0,0.7071067811865476],[0.5,0.5],[1,1],[0.5,1],[1,0.5]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[5,7],[5,8],[5,1],[5,4],[2,8],[8,6],[6,7],[7,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"V\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,0,180,-180,180,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[9,1],[12,2],[2,3,8],[6,4,5,8,7],[11,4,10],[11,12,5],[10,6,9]],\"vertices_vertices\":[[1,4],[2,5,0],[8,1],[7,4],[3,0,5],[8,6,7,4,1],[7,5,8],[6,3,5],[6,5,2]],\"faces_vertices\":[[0,1,5,4],[1,2,8,5],[3,4,5,7],[5,8,6],[5,6,7]],\"faces_edges\":[[0,7,8,3],[1,9,6,7],[2,8,5,12],[6,10,4],[4,11,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[null,0,2],[3,4,2,0,1],[4,3,null],[null,2,4],[3,1,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[2,4],[1,3],[0,1],[0,2],[1],[3],[4],[2]],\"faces_faces\":[[1,2],[3,0],[0,4],[1,4],[3,2]]}\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/invalid-single-vertex-3d.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[0,1],[0.5,0.5],[1,1],[0.5,1]],\n\t\"edges_vertices\":[[0,1],[2,0],[0,3],[3,4],[5,3],[2,3],[3,1],[1,4],[4,5],[5,2]],\n\t\"edges_assignment\":[\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,90,-180,-180,90,90,0,0,0],\n\t\"faces_vertices\":[[0,1,3],[0,3,2],[1,4,3],[2,3,5],[3,4,5]]\n}\n"
  },
  {
    "path": "tests/files/fold/isolated-line-in-face.fold",
    "content": "{\n\t\"file_description\":\"an edge sits inside a square, unassociated with any face\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[0,1],[0.25,0.85],[0.75,0.15],[1,1]],\n\t\"edges_vertices\":[[0,1],[2,0],[3,4],[1,5],[5,2]],\n\t\"edges_assignment\":[\"B\",\"B\",\"V\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,180,0,0],\n\t\"vertices_edges\":[[0,1],[3,0],[4,1],[2],[2],[4,3]],\n\t\"vertices_vertices\":[[1,2],[5,0],[5,0],[4],[3],[2,1]],\n\t\"faces_vertices\":[[0,1,5,2]],\n\t\"faces_edges\":[[0,3,4,1]],\n\t\"vertices_faces\":[[0,null],[0,null],[null,0],[null],[null],[0,null]],\n\t\"edges_faces\":[[0],[0],[],[0],[0]],\n\t\"faces_faces\":[[]]\n}\n"
  },
  {
    "path": "tests/files/fold/kabuto.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"Kabuto\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"file_description\": \"all flap layerings\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\":[\n\t\t[0.5,0],[0.5,0.5],[1,0.5],[0.75,0.25],[0.5,0],[1,0.5],[0.75,0.25],[0.5,0.35355339059327323],[0.6464466094067266,0.5],[0.5,0.125],[0.875,0.5],[0.39644660940672627,0.25],[0.75,0.6035533905932738],[0.625,0.125],[0.875,0.375],[0.5,0.35355339059327334],[0.6464466094067267,0.5],[0.5,0.5],[0.625,0.375]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[3,4],[0,2],[5,6],[5,4],[3,7],[6,8],[9,10],[1,5],[4,1],[0,3],[11,3],[3,1],[6,2],[1,6],[6,12],[13,14],[3,15],[6,16],[5,17],[17,4],[13,18],[18,14],[11,7],[7,0],[2,8],[8,12],[4,15],[15,11],[0,9],[9,13],[14,10],[10,2],[12,16],[16,5]\n\t],\n\t\"edges_assignment\": [\n\t\t\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\": [\n\t\t[1,0,2],[5,4,1],[4,5,17],[2,0,9,10],[10,9,13,14],[14,13,18],[3,7,0],[8,6,2],[0,1,3],[1,2,6],[4,3,1],[6,5,1],[3,4,15],[5,6,16],[11,3,15],[6,12,16],[7,3,11],[6,8,12]\n\t],\n\t\"faceOrders\": [\n\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,-1],[5,16,-1],[5,10,1],[5,12,-1],[5,14,1],[5,6,1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,-1],[5,17,-1],[5,11,1],[5,13,-1],[5,15,1],[5,7,1]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,1],[5,16,1],[5,14,-1],[5,10,-1],[5,12,1],[5,6,-1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,1],[5,17,1],[5,15,-1],[5,11,-1],[5,13,1],[5,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,-1],[5,16,-1],[5,10,1],[5,12,-1],[5,14,1],[5,6,1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,1],[5,17,1],[5,15,-1],[5,11,-1],[5,13,1],[5,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,1],[5,16,1],[5,14,-1],[5,10,-1],[5,12,1],[5,6,-1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,-1],[5,17,-1],[5,11,1],[5,13,-1],[5,15,1],[5,7,1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,-1],[5,16,-1],[5,10,1],[5,12,-1],[5,14,1],[5,6,1],[3,9,1],[3,17,1],[4,7,-1],[5,7,-1],[3,15,-1],[4,9,1],[3,11,-1],[4,17,1],[5,17,1],[4,15,-1],[3,13,1],[5,9,1],[5,15,-1],[4,11,-1],[4,13,1],[5,11,-1],[5,13,1],[3,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,1],[3,16,1],[4,6,-1],[5,6,-1],[3,14,-1],[4,8,1],[3,10,-1],[4,16,1],[5,16,1],[4,14,-1],[3,12,1],[5,8,1],[5,14,-1],[4,10,-1],[4,12,1],[5,10,-1],[5,12,1],[3,6,-1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,-1],[5,17,-1],[5,11,1],[5,13,-1],[5,15,1],[5,7,1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,-1],[3,16,-1],[4,6,1],[3,10,1],[3,12,-1],[4,8,-1],[4,16,-1],[3,14,1],[4,10,1],[4,12,-1],[4,14,1],[3,6,1],[5,8,1],[5,16,1],[5,14,-1],[5,10,-1],[5,12,1],[5,6,-1],[3,9,1],[3,17,1],[4,7,-1],[5,7,-1],[3,15,-1],[4,9,1],[3,11,-1],[4,17,1],[5,17,1],[4,15,-1],[3,13,1],[5,9,1],[5,15,-1],[4,11,-1],[4,13,1],[5,11,-1],[5,13,1],[3,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,1],[3,16,1],[4,6,-1],[5,6,-1],[3,14,-1],[4,8,1],[3,10,-1],[4,16,1],[5,16,1],[4,14,-1],[3,12,1],[5,8,1],[5,14,-1],[4,10,-1],[4,12,1],[5,10,-1],[5,12,1],[3,6,-1],[3,9,-1],[3,17,-1],[4,7,1],[3,11,1],[3,13,-1],[4,9,-1],[4,17,-1],[3,15,1],[4,11,1],[4,13,-1],[4,15,1],[3,7,1],[5,9,1],[5,17,1],[5,15,-1],[5,11,-1],[5,13,1],[5,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"faceOrders\": [\n\t\t\t[0,8,1],[0,9,1],[10,12,1],[0,3,1],[11,13,1],[1,2,1],[6,16,1],[7,17,1],[3,4,1],[1,11,-1],[1,10,-1],[6,8,-1],[14,16,-1],[8,10,-1],[7,9,-1],[9,11,-1],[15,17,-1],[4,5,-1],[12,14,-1],[13,15,-1],[0,10,-1],[0,6,-1],[0,11,-1],[0,7,-1],[0,12,1],[1,12,1],[8,12,1],[0,13,1],[3,5,-1],[1,13,1],[9,13,1],[6,14,-1],[7,15,-1],[10,14,-1],[0,4,-1],[11,15,-1],[0,16,1],[8,16,1],[0,17,1],[9,17,1],[1,15,-1],[1,9,1],[1,14,-1],[1,8,1],[0,14,-1],[1,16,1],[8,14,-1],[10,16,1],[12,16,1],[0,15,-1],[1,17,1],[9,15,-1],[11,17,1],[13,17,1],[2,14,-1],[2,15,-1],[2,16,1],[2,17,1],[0,1,-1],[2,10,-1],[0,5,1],[2,11,-1],[6,12,-1],[7,13,-1],[2,7,-1],[2,6,-1],[1,6,-1],[6,10,1],[1,7,-1],[7,11,1],[2,12,1],[2,13,1],[1,4,-1],[1,3,1],[2,9,1],[2,8,1],[2,4,-1],[1,5,1],[0,2,1],[2,5,1],[2,3,1],[3,8,1],[3,16,1],[4,6,-1],[5,6,-1],[3,14,-1],[4,8,1],[3,10,-1],[4,16,1],[5,16,1],[4,14,-1],[3,12,1],[5,8,1],[5,14,-1],[4,10,-1],[4,12,1],[5,10,-1],[5,12,1],[3,6,-1],[3,9,1],[3,17,1],[4,7,-1],[5,7,-1],[3,15,-1],[4,9,1],[3,11,-1],[4,17,1],[5,17,1],[4,15,-1],[3,13,1],[5,9,1],[5,15,-1],[4,11,-1],[4,13,1],[5,11,-1],[5,13,1],[3,7,-1]\n\t\t]\n\t},{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"creasePattern\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0.5,0],[0.5,0.5],[1,0.5],[0.25,0.25],[0,0.5],[0.5,1],[0.75,0.75],[0.14644660940672669,0],[1,0.8535533905932734],[0.625,0],[1,0.375],[0,0],[1,1],[0.75,0],[1,0.25],[0,0.14644660940672669],[0.8535533905932734,1],[0,1],[1,0]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/kissing-squares.fold",
    "content": "{\n\t\"vertices_coords\": [[0,0],[1,0],[0,2],[0,1],[-1,2],[1,1],[-1,1]],\n\t\"edges_vertices\": [[0,1],[2,3],[3,0],[4,3],[3,1],[1,5],[4,6],[5,3],[3,6],[2,4]],\n\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\": [0,0,0,-180,180,0,0,0,0,0],\n\t\"vertices_edges\": [[0,2],[5,4,0],[9,1],[7,1,3,8,2,4],[9,6,3],[7,5],[8,6]],\n\t\"vertices_vertices\": [[1,3],[5,3,0],[4,3],[5,2,4,6,0,1],[2,6,3],[3,1],[3,4]],\n\t\"faces_vertices\": [[0,1,3],[1,5,3],[2,4,3],[3,4,6]],\n\t\"faces_edges\": [[4,2,0],[7,4,5],[3,1,9],[6,8,3]],\n\t\"vertices_faces\": [[0,null],[1,0,null],[2,null],[null,2,3,null,0,1],[null,3,2],[1,null],[3,null]],\n\t\"edges_faces\": [[0],[2],[0],[2,3],[0,1],[1],[3],[1],[3],[2]],\n\t\"faces_faces\": [[1],[0],[3],[2]]\n}"
  },
  {
    "path": "tests/files/fold/kraft-bird-base.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"bird base\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"frame_attributes\": [\"2D\"],\n\t\"vertices_coords\":[\n\t\t[0.6213203435596424,0.6213203435596424],[0.6642135623730948,0.6642135623730948],[0.585786437626905,0.82842712474619],[0.5606601717798212,0.7677669529663682],[0.7677669529663687,0.5606601717798212],[0.82842712474619,0.5857864376269049],[0.3964466094067261,0.25],[0.5,0.20710678118654746],[0.20710678118654763,0.5],[0.25,0.39644660940672627],[0.46966991411008924,0.2803300858899106],[0.28033008588991065,0.4696699141100894],[0.3232233047033631,0.32322330470336313],[0.5428932188134522,0.810660171779821],[0.8106601717798212,0.5428932188134523],[0.5606601717798213,0.47487373415291634],[0.4748737341529164,0.5606601717798212],[0.7071067811865476,0.41421356237309503],[0.35355339059327373,0.6464466094067263],[0.41421356237309515,0.7071067811865476],[0.6464466094067263,0.35355339059327373],[0.5,0.7928932188134522],[0.7928932188134523,0.5],[0.9497474683058329,0.5857864376269049],[0.9142135623730951,0.5],[0.5,0.914213562373094],[0.5857864376269046,0.9497474683058319],[0.5857864376269051,0.20710678118654757],[0.5732233047033631,0.17677669529663675],[0.1767766952966373,0.5732233047033632],[0.20710678118654824,0.5857864376269052],[0.18933982822017842,0.33578643762690474],[0.33578643762690497,0.1893398282201788],[0.21966991411008926,0.2196699141100893],[0.25,0.25],[0.7677669529663683,0.6819805153394634],[0.7071067811865476,0.7071067811865476],[0.7248737341529159,0.7248737341529159],[0.7426406871192842,0.7426406871192842],[0.6819805153394638,0.7677669529663682],[0.7071067811865476,0.82842712474619],[0.7677669529663682,0.8033008588991061],[0.8033008588991063,0.7677669529663682],[0.82842712474619,0.7071067811865476],[1,0.914213562373094],[1,0.8786796564403574],[0.82842712474619,0.82842712474619],[0.8535533905932733,0.8535533905932733],[0.914213562373095,0.914213562373095],[0.9571067811865472,0.9571067811865472],[1,1],[0.8786796564403576,1],[0.914213562373095,1],[0,1],[0.0857864376269059,0.914213562373094],[0.1464466094067272,0.8535533905932728],[0.20710678118654777,0.7928932188134522],[0.7928932188134525,0.20710678118654746],[0.8535533905932736,0.14644660940672638],[0.9142135623730949,0.08578643762690508],[1,0],[0.6464466094067263,0.14644660940672616],[0.14644660940672682,0.6464466094067267],[0.6464466094067263,0.06066017177982119],[0.06066017177982191,0.6464466094067274],[0.9142135623730953,0],[1,0.3786796564403576],[1,0.34314575050762],[1,0.2928932188134524],[0.9571067811865475,0.3964466094067265],[0.9748737341529163,0.35355339059327384],[0.2928932188134524,1],[0.3535533905932759,0.9748737341529159],[0.39644660940672793,0.9571067811865469],[0.34314575050762175,1],[0.37867965644035884,1],[0.35355339059327384,0.14644660940672627],[0.43933982822017864,0.14644660940672627],[0.14644660940672638,0.4393398282201786],[0.1464466094067262,0.3535533905932734],[0,0.914213562373094],[0.41421356237309503,0.41421356237309503],[0.5,0],[0.2928932188134524,0.08578643762690463],[0.35355339059327384,0.06066017177982108],[0.06066017177982158,0.3535533905932734],[0.08578643762690544,0.2928932188134524],[0,0.2928932188134524],[0,0.08578643762690508],[0,0],[0.08578643762690635,0],[0.2928932188134524,0],[0.060660171779822046,0.88908729652601],[0.8890872965260113,0.060660171779821304],[0.20710678118654902,0.9142135623730943],[0.914213562373095,0.20710678118654746],[0,0.7928932188134511],[0.7928932188134523,0],[0,0.5],[0.17677669529663698,0.42677669529663675],[0.4267766952966368,0.17677669529663687],[0.67157287525381,0.914213562373094],[0.7071067811865476,0.8786796564403568],[0.8786796564403578,0.7071067811865476],[0.9142135623730951,0.67157287525381],[0.9142135623730951,0.7426406871192847],[0.742640687119285,0.9142135623730943],[0.8890872965260112,0.8535533905932733],[0.8786796564403574,0.82842712474619],[0.82842712474619,0.8786796564403572],[0.8535533905932736,0.8890872965260113],[0.9748737341529158,0.9393398282201786],[0.9393398282201786,0.9748737341529161],[0.914213562373095,0.82842712474619],[0.82842712474619,0.9142135623730946],[0.8890872965260115,0.9748737341529163],[0.9748737341529169,0.7677669529663664],[1,0.7071067811865476],[0.7071067811865476,1],[0.76776695296637,0.9748737341529161],[0.9748737341529169,0.8890872965260112],[0.9748737341529164,0.8535533905932733],[0.8535533905932736,0.9748737341529169],[0.7071067811865476,0.9142135623730941],[0.9142135623730951,0.7071067811865476],[0.6066017177982129,1],[1,0.6066017177982127],[0.20710678118654743,0.2928932188134524],[0.2928932188134524,0.20710678118654757],[0.2928932188134524,0.1464466094067265],[0.14644660940672585,0.2928932188134524],[0.2928932188134524,0.12132034355964294],[1,0.41421356237309515],[0.6035533905932741,0.25],[0.75,0.3106601717798214],[0.12132034355964215,0.2928932188134524],[0.4142135623730951,1],[0.25,0.6035533905932736],[0.3106601717798212,0.75],[0.2928932188134524,0.7071067811865476],[0.7071067811865476,0.2928932188134524],[0.5355339059327376,0.46446609406726236],[0.5,0.5],[0.4644660940672624,0.5355339059327375],[0.5857864376269042,0.878679656440357],[0.6360389693210715,0.9289321881345243],[0.10355339059327345,0.39644660940672627],[0.5355339059327371,0.82842712474619],[0.5606601717798207,0.8535533905932736],[0.8786796564403576,0.5857864376269049],[0.9289321881345249,0.6360389693210722],[0.853553390593274,0.5606601717798212],[0.82842712474619,0.5355339059327374],[0.3964466094067264,0.10355339059327362],[1,0.46446609406726247],[0.4644660940672619,1],[0.9142135623730954,0.6213203435596426],[0.931980515339464,0.6286796564403574],[0.8713203435596426,0.6035533905932737],[0.9748737341529168,0.6464466094067264],[1,0.65685424949238],[0.621320343559643,0.9142135623730957],[0.6286796564403573,0.9319805153394631],[0.6035533905932736,0.8713203435596418],[0.6464466094067263,0.974873734152916],[0.65685424949238,1],[0.573223304703363,0.03033008588991],[0.5857864376269049,0],[0.5606601717798212,0.06066017177982119],[0.5428932188134524,0.10355339059327362],[0.1035533905932739,0.5428932188134525],[0.06066017177982136,0.5606601717798214],[0.030330085889910763,0.5732233047033631],[0,0.5857864376269051],[0.853553390593274,0.06066017177982119],[0.8786796564403576,0],[0.060660171779822025,0.8535533905932733],[0,0.8786796564403573],[0.10355339059327376,0.04289321881345254],[0.1464466094067262,0.14644660940672627],[0.18933982822017856,0.25],[0.042893218813452316,0.10355339059327384],[0.25,0.1893398282201788],[0.6642135623730949,0.5177669529663687],[0.5177669529663689,0.6642135623730948],[0.9393398282201787,0.8535533905932733],[0.9571067811865477,0.8713203435596422],[0.8713203435596428,0.9571067811865476],[0.853553390593274,0.9393398282201787],[0.82842712474619,0.9497474683058327],[0.82842712474619,1],[0.82842712474619,0.9748737341529168],[0.9497474683058325,0.82842712474619],[1,0.82842712474619],[0.9748737341529166,0.82842712474619],[0.7928932188134528,0.7928932188134526],[0.785533905932738,0.8106601717798213],[0.8106601717798211,0.785533905932737],[0.6035533905932737,0.957106781186547],[0.6464466094067264,0.853553390593273],[0.6109127034739887,0.9393398282201779],[0.5857864376269047,1],[0.8535533905932736,0.35355339059327395],[0.664213562373095,0.8106601717798209],[0.65685424949238,0.82842712474619],[0.5961940777125587,0.9748737341529161],[0.896446609406726,0.25],[0.9571067811865476,0.603553390593273],[0.8535533905932742,0.6464466094067254],[0.9393398282201794,0.6109127034739876],[1,0.5857864376269042],[0.3535533905932745,0.8535533905932732],[0.8106601717798209,0.6642135623730945],[0.82842712474619,0.6568542494923795],[0.9748737341529157,0.5961940777125584],[0.25,0.8964466094067255],[0.3786796564403575,0.08578643762690474],[0.39644660940672627,0.04289321881345243],[0.41421356237309503,0],[0.2928932188134524,0.2928932188134524],[0.3232233047033631,0.2196699141100894],[0.035533905932738044,0.914213562373094],[0.04289321881345292,0.8964466094067252],[0.12132034355964252,0.7071067811865476],[0.08578643762690497,0.3786796564403574],[0.04289321881345265,0.39644660940672616],[0,0.41421356237309503],[0.2196699141100893,0.32322330470336313],[0.9142135623730951,0.03553390593273764],[0.8964466094067263,0.04289321881345243],[0.7071067811865476,0.1213203435596425],[0.82842712474619,0.7781745930520221],[0.7781745930520231,0.82842712474619],[1,0.7781745930520214],[0.778174593052023,1],[1,0.5],[0.9748737341529167,0.4748737341529162],[0.75,0.25],[0.7677669529663689,0.26776695296636865],[0.9393398282201788,0.43933982822017836],[0.5,1],[0.4748737341529161,0.9748737341529158],[0.25,0.75],[0.26776695296636877,0.7677669529663687],[0.4393398282201786,0.9393398282201784]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[2,3],[4,5],[6,7],[8,9],[6,10],[11,9],[12,6],[13,3],[4,14],[0,15],[16,0],[1,4],[0,4],[15,17],[18,19],[20,17],[19,21],[17,22],[21,3],[4,22],[9,12],[5,14],[14,22],[21,13],[13,2],[23,24],[25,26],[27,28],[29,30],[31,9],[32,6],[33,34],[35,4],[1,36],[36,37],[37,38],[3,39],[40,41],[42,43],[44,45],[46,47],[48,49],[49,50],[51,52],[53,54],[55,56],[57,58],[59,60],[61,57],[56,62],[65,60],[66,67],[67,68],[69,70],[70,68],[71,72],[72,73],[71,74],[74,75],[7,28],[28,61],[62,29],[62,30],[27,61],[7,27],[76,77],[78,79],[77,7],[78,8],[53,80],[12,81],[77,82],[7,82],[83,84],[85,86],[87,88],[88,89],[89,90],[90,91],[68,60],[7,20],[10,20],[53,71],[55,94],[58,95],[53,94],[95,60],[94,72],[70,95],[74,72],[70,67],[92,54],[93,59],[96,64],[63,97],[75,73],[69,66],[58,93],[92,55],[78,99],[99,9],[6,100],[100,77],[8,11],[11,81],[81,10],[10,7],[8,99],[99,79],[76,100],[100,7],[52,48],[95,59],[54,94],[48,44],[101,102],[103,104],[103,105],[102,106],[48,107],[107,108],[109,110],[110,48],[48,111],[112,48],[108,113],[114,109],[51,115],[115,48],[116,117],[118,119],[48,120],[120,45],[52,112],[112,49],[49,111],[111,44],[112,50],[50,111],[120,44],[115,52],[22,24],[43,104],[43,103],[102,40],[101,40],[25,21],[50,44],[52,50],[120,121],[122,115],[121,45],[122,51],[114,110],[107,113],[123,102],[103,124],[105,124],[124,104],[101,123],[123,106],[109,46],[46,108],[47,107],[110,47],[47,48],[113,116],[119,114],[48,113],[114,48],[124,117],[105,117],[118,123],[118,106],[3,1],[3,0],[19,16],[18,11],[8,18],[30,8],[29,8],[98,8],[98,78],[36,35],[39,36],[39,37],[37,35],[42,35],[39,41],[41,38],[38,42],[106,109],[108,105],[127,34],[34,128],[128,129],[130,127],[83,76],[86,79],[90,83],[86,88],[85,79],[76,84],[91,84],[87,85],[32,76],[79,31],[128,32],[31,127],[129,32],[130,31],[129,131],[131,83],[132,66],[20,133],[133,27],[17,134],[131,76],[7,133],[69,132],[86,135],[135,130],[75,136],[30,137],[137,18],[138,19],[135,79],[8,137],[73,136],[137,139],[139,138],[133,140],[140,134],[81,141],[141,15],[81,142],[142,0],[16,143],[143,81],[54,55],[139,18],[18,143],[143,142],[142,141],[141,20],[20,140],[58,59],[25,144],[145,101],[98,146],[146,79],[25,147],[147,13],[25,148],[148,2],[146,78],[21,147],[147,148],[148,144],[145,118],[149,24],[104,150],[5,151],[151,24],[14,152],[152,24],[76,153],[153,82],[117,150],[149,151],[151,152],[152,22],[77,153],[154,132],[136,155],[150,156],[156,149],[150,157],[158,149],[117,159],[117,160],[160,126],[5,158],[158,156],[156,157],[157,159],[159,160],[144,161],[161,145],[162,145],[144,163],[164,118],[125,165],[165,118],[2,163],[163,161],[161,162],[162,164],[164,165],[82,166],[166,63],[82,167],[167,97],[82,168],[168,61],[28,169],[169,82],[7,169],[169,168],[168,166],[166,167],[98,170],[170,29],[98,171],[171,62],[64,172],[172,98],[96,173],[173,98],[8,170],[170,171],[171,172],[172,173],[63,174],[174,93],[97,175],[175,65],[57,174],[174,175],[92,176],[176,64],[80,177],[177,96],[56,176],[176,177],[83,91],[86,87],[89,178],[178,131],[89,179],[179,33],[130,180],[180,33],[127,180],[180,179],[179,178],[178,90],[89,181],[181,135],[33,182],[182,129],[128,182],[182,179],[179,181],[181,88],[15,183],[183,4],[0,183],[183,17],[3,184],[184,16],[0,184],[184,19],[48,185],[48,186],[186,121],[113,185],[185,186],[186,120],[122,187],[187,48],[188,48],[114,188],[188,187],[187,115],[119,189],[189,188],[190,51],[119,191],[191,122],[114,189],[189,191],[191,190],[185,192],[192,116],[45,193],[121,194],[194,116],[113,192],[192,194],[194,193],[192,121],[189,122],[38,195],[195,46],[41,196],[196,46],[42,195],[195,196],[46,197],[197,42],[41,195],[195,197],[26,198],[198,164],[163,199],[199,40],[26,200],[200,162],[201,125],[134,202],[202,69],[3,203],[203,40],[2,204],[204,40],[125,205],[205,26],[69,206],[206,58],[95,206],[206,202],[202,22],[4,36],[39,203],[203,204],[204,199],[199,161],[161,200],[200,198],[198,205],[205,201],[159,207],[207,23],[43,208],[208,158],[157,209],[209,23],[126,210],[138,211],[211,73],[43,212],[212,4],[43,213],[213,5],[23,214],[214,126],[55,215],[215,73],[94,215],[215,211],[211,21],[3,36],[35,212],[212,213],[213,208],[208,156],[156,209],[209,207],[207,214],[214,210],[153,216],[216,84],[84,217],[217,82],[91,218],[218,82],[34,219],[219,12],[128,220],[220,6],[80,221],[221,54],[80,222],[222,92],[64,223],[223,56],[53,221],[221,222],[222,176],[176,223],[223,62],[9,219],[219,220],[220,32],[76,216],[216,217],[217,218],[85,224],[224,146],[98,225],[225,85],[98,226],[226,87],[9,227],[227,127],[59,228],[228,65],[93,229],[229,65],[63,230],[230,57],[60,228],[228,229],[229,174],[174,230],[230,61],[6,219],[219,227],[227,31],[79,224],[224,225],[225,226],[197,231],[231,105],[43,231],[231,46],[196,232],[232,106],[40,232],[232,46],[24,156],[156,104],[105,113],[25,161],[161,101],[106,114],[101,118],[104,117],[193,233],[233,117],[43,105],[105,116],[116,233],[118,234],[234,190],[40,106],[106,119],[119,234],[210,235],[235,154],[24,236],[236,154],[140,237],[237,57],[134,238],[238,57],[24,239],[239,69],[61,237],[237,238],[238,202],[202,239],[239,236],[236,235],[155,240],[240,201],[25,241],[241,155],[56,242],[242,139],[56,243],[243,138],[73,244],[244,25],[62,242],[242,243],[243,211],[211,244],[244,241],[241,240]\n\t],\n\t\"edges_assignment\":[\n\t\t\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"B\",\"V\",\"V\",\"M\",\"B\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"B\",\"V\",\"V\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"B\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"V\",\"V\",\"M\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"V\",\"V\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"F\",\"M\",\"V\",\"V\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"V\",\"V\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"M\",\"M\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"F\",\"F\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"F\",\"F\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t180,180,180,180,180,-180,-180,180,180,180,-180,-180,180,-180,-180,180,180,-180,-180,-180,-180,180,-180,180,180,-180,180,180,180,180,180,180,180,180,-180,180,-180,180,-180,-180,0,180,180,-180,0,180,-180,-180,180,-180,-180,0,0,0,180,180,180,180,0,0,-180,180,180,-180,-180,180,-180,-180,180,180,0,180,180,-180,180,180,0,0,0,0,0,-180,180,0,180,180,-180,-180,180,180,-180,-180,-180,-180,-180,-180,180,180,-180,-180,180,180,180,180,-180,-180,-180,-180,-180,180,180,-180,-180,180,180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,180,180,180,-180,-180,-180,180,-180,-180,180,-180,0,0,180,180,-180,-180,-180,-180,180,180,180,-180,-180,180,-180,-180,180,180,-180,180,180,180,180,180,-180,180,-180,180,-180,-180,180,-180,180,-180,-180,180,-180,-180,180,180,180,180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,-180,-180,-180,-180,180,180,180,180,180,180,0,180,180,-180,0,0,0,180,180,0,180,180,-180,0,0,0,0,0,0,0,-180,-180,180,180,-180,-180,0,0,0,0,0,0,0,0,180,-180,-180,-180,180,180,-180,-180,0,0,0,0,0,180,-180,-180,-180,180,180,-180,-180,0,0,0,0,0,0,0,0,0,-180,180,180,0,0,0,0,0,0,0,0,0,-180,180,180,0,0,0,0,0,0,0,180,180,0,0,-180,-180,180,180,0,0,0,0,180,180,-180,-180,180,180,0,0,0,0,0,0,180,180,0,0,0,0,180,180,0,0,0,0,0,0,0,0,-180,-180,180,180,0,0,0,0,0,0,180,180,0,0,0,0,180,180,0,0,180,180,0,0,180,-180,-180,0,0,0,-180,-180,180,0,0,0,180,180,0,-180,-180,0,0,0,180,180,0,-180,-180,0,0,0,0,0,180,180,-180,-180,0,0,-180,-180,0,0,180,180,180,180,-180,-180,0,0,0,-180,-180,-180,-180,180,180,-180,-180,0,0,0,0,0,0,0,0,0,0,0,0,180,180,180,180,-180,-180,0,0,0,-180,-180,-180,-180,180,180,-180,-180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,180,0,0,-180,-180,-180,-180,180,180,-180,-180,180,180,0,0,0,0,0,0,0,0,0,0,0,0,0,180,180,0,0,-180,-180,180,180,-180,-180,180,180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,180,180,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,180,180,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[112,49,50],[50,49,111],[92,55,54],[93,59,58],[97,63,166,167],[142,143,81],[141,142,81],[64,96,173,172],[91,84,83],[85,87,86],[11,81,143,18],[139,137,18],[180,33,34,127],[130,180,127],[83,131,178,90],[89,90,178],[83,90,91],[133,140,20],[141,81,10,20],[182,129,128],[34,33,182,128],[179,178,131,129,182],[33,179,182],[89,178,179],[181,89,179],[180,130,135,181,179],[33,180,179],[88,89,181],[135,86,88,181],[86,87,88],[15,17,183],[20,140,134,17],[15,141,20,17],[15,183,0],[142,141,15,0],[16,143,142,0],[184,16,0],[19,16,184],[18,143,16,19],[138,139,18,19],[120,186,121],[45,120,121],[51,122,115],[187,115,122],[67,70,68],[66,69,70,67],[132,69,66],[72,74,71],[75,74,72,73],[136,75,73],[196,41,195],[38,195,41],[42,197,195],[38,42,195],[58,59,95],[206,58,95],[70,69,206,95],[4,183,17,22],[14,4,22],[152,14,22],[0,183,4],[1,0,4],[38,41,39,37],[164,198,200,162],[198,26,200],[205,26,198],[164,165,125,205,198],[125,201,205],[73,72,94,215],[55,215,94],[54,55,94],[13,21,3],[19,184,3,21],[13,147,21],[203,3,39],[36,39,3],[1,36,3],[0,1,3],[184,0,3],[37,36,35],[4,35,36],[1,4,36],[37,39,36],[4,212,35],[42,38,37,35],[159,157,209,207],[23,207,209],[126,160,159,207,214],[23,214,207],[126,214,210],[94,72,71,53],[54,94,53],[221,54,53],[80,221,53],[222,92,54,221],[80,222,221],[176,92,222],[80,177,176,222],[55,92,176,56],[223,56,176],[64,223,176],[177,96,64,176],[30,29,8],[170,8,29],[18,8,11],[9,11,8],[99,9,8],[78,99,8],[137,30,8],[18,137,8],[81,11,9,12],[128,32,220],[129,131,76,32],[128,129,32],[100,76,77],[153,77,76],[216,153,76],[84,216,76],[83,84,76],[131,83,76],[84,217,216],[84,91,218,217],[228,65,60],[59,228,60],[95,59,60],[68,70,95,60],[229,65,228],[59,93,229,228],[174,175,65,229],[93,174,229],[58,57,174,93],[63,97,175,174],[230,63,174],[57,230,174],[7,169,28],[27,7,28],[133,7,27],[100,77,7],[6,100,7],[10,6,7],[20,10,7],[133,20,7],[32,76,100,6],[220,32,6],[219,220,6],[12,219,6],[10,81,12,6],[34,128,220,219],[227,127,34,219],[9,227,219],[12,9,219],[31,127,227],[9,31,227],[130,127,31],[79,135,130,31],[9,99,79,31],[86,135,79],[85,86,79],[224,85,79],[146,224,79],[78,146,79],[99,78,79],[225,85,224],[226,87,85,225],[149,158,5,151],[14,152,151,5],[4,14,5],[213,212,4,5],[158,208,213,5],[187,122,189,188],[122,191,189],[51,190,191,122],[199,163,2,204],[3,203,204,2],[13,3,2],[148,147,13,2],[163,144,148,2],[196,46,232],[107,47,46,108],[197,231,46],[195,197,46],[196,195,46],[47,110,109,46],[121,192,194],[121,186,185,192],[45,121,194,193],[152,22,24],[151,152,24],[149,151,24],[156,149,24],[23,209,156,24],[157,156,209],[158,149,156],[208,158,156],[157,150,156],[185,113,192],[107,108,113],[112,50,52],[51,115,52],[200,26,25,161],[144,161,25],[148,144,25],[147,148,25],[21,147,25],[163,199,161],[144,163,161],[162,200,161],[145,162,161],[188,114,48],[110,48,114],[109,110,114],[188,189,114],[111,48,44],[120,44,48],[186,120,48],[185,186,48],[113,185,48],[107,113,48],[47,107,48],[110,47,48],[187,188,48],[115,187,48],[52,115,48],[112,52,48],[49,112,48],[111,49,48],[120,45,44],[50,111,44],[118,101,123],[102,123,101],[145,161,101],[118,145,101],[164,162,145,118],[165,164,118],[117,104,150],[156,150,104],[124,103,104],[117,124,104],[159,160,117],[150,157,159,117],[105,43,103],[104,103,43],[208,156,104,43],[213,208,43],[212,213,43],[42,35,212,43],[231,197,42,43],[105,231,43],[116,105,117],[124,117,105],[103,124,105],[108,46,231,105],[113,108,105],[116,113,105],[233,116,117],[192,113,116],[194,192,116],[233,193,194,116],[106,40,232],[41,196,232,40],[203,39,41,40],[204,203,40],[199,204,40],[101,161,199,40],[102,101,40],[106,102,40],[119,106,114],[109,114,106],[232,46,109,106],[123,102,106],[118,123,106],[119,118,106],[190,234,119,191],[189,191,119],[114,189,119],[234,118,119],[166,82,167],[217,218,82],[153,216,217,82],[77,153,82],[7,77,82],[169,7,82],[168,169,82],[166,168,82],[63,230,61,168,166],[61,28,169,168],[57,61,230],[27,28,61],[237,140,133,27,61],[57,237,61],[238,237,57],[238,134,140,237],[58,206,202,238,57],[202,134,238],[69,202,206],[22,17,134,202],[239,24,22,202],[69,239,202],[132,154,236,239,69],[236,24,239],[235,236,154],[235,210,214,23,24,236],[170,98,8],[78,8,98],[146,78,98],[225,224,146,98],[226,225,98],[172,173,98],[171,172,98],[170,171,98],[29,62,171,170],[62,223,64,172,171],[137,139,242,62,30],[29,30,62],[56,223,62],[242,56,62],[138,243,242,139],[243,56,242],[211,243,138],[211,215,55,56,243],[25,244,211,21],[138,19,21,211],[73,215,211],[244,73,211],[241,244,25],[241,155,136,73,244],[26,205,201,240,241,25],[240,155,241]\n\t]\n}"
  },
  {
    "path": "tests/files/fold/layer-4-flaps.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,-3],[0,-1],[0,1],[0,3],[-3,0],[-1,0],[1,0],[3,0],[1,3],[1,1],[1,-1],[-1,-3],[-1,-1],[-1,1],[3,-1],[-3,1]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[2,3],[4,5],[6,7],[8,9],[9,6],[6,10],[11,12],[12,5],[5,13],[14,10],[10,1],[1,12],[15,13],[13,2],[2,9],[3,8],[0,11],[7,14],[4,15]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"V\",\"B\",\"B\",\"V\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,12,11],[1,10,6,9,2,13,5,12],[2,9,8,3],[4,5,13,15],[6,10,14,7]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0,-3],[0,-1],[0,-3],[0,-1],[1,-2],[-1,-2],[1,-2],[-1,-2],[1,-1],[1,-3],[1,-1],[-1,-3],[-1,-1],[-1,-3],[-1,-1],[1,-3]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/layer-solver-conflict.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[0.5857864376269049,0],[1,0],[0,1],[0,0.5857864376269049],[0.41421356237309503,0.41421356237309503],[0.5,0.5],[0.7071067811865476,0.7071067811865475],[1,1],[0.7071067811865476,0.2928932188134524],[1,0.41421356237309515],[0.2071067811865475,0.5],[0.2928932188134524,0.7071067811865476],[0.41421356237309515,1],[0.12132034355964227,0.8786796564403577],[0.7071067811865476,1],[1,0.7071067811865476]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[3,4],[4,0],[0,5],[5,6],[6,7],[7,8],[0,9],[9,10],[0,11],[11,12],[12,13],[4,14],[9,1],[12,4],[9,5],[5,11],[11,4],[9,7],[7,15],[3,14],[14,12],[12,6],[6,9],[9,2],[12,7],[7,16],[13,14],[8,15],[15,13],[13,3],[13,7],[7,10],[2,10],[10,16],[16,8]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"V\",\"F\",\"V\",\"V\",\"F\",\"M\",\"F\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"M\",\"M\",\"B\",\"B\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,9],[0,9,5],[0,5,11],[0,11,4],[1,2,9],[2,10,9],[3,4,14],[3,14,13],[4,12,14],[4,11,12],[5,6,12,11],[5,9,6],[6,7,12],[6,9,7],[7,16,8],[7,8,15],[7,15,13],[7,13,12],[7,9,10],[7,10,16],[12,13,14]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/layers-3d-edge-edge.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[6,0],[7,8],[8,9],[9,10],[10,11],[11,6],[11,1],[10,2],[9,3],[8,4],[7,5]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"F\",\"V\",\"V\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,120,120,-60,0],\"vertices_edges\":[[0,5],[1,11,0],[2,12,1],[3,13,2],[4,14,3],[15,4],[10,5],[6,15],[6,7,14],[7,8,13],[8,9,12],[9,10,11]],\"vertices_vertices\":[[1,6],[2,11,0],[3,10,1],[4,9,2],[5,8,3],[7,4],[11,0],[8,5],[7,9,4],[8,10,3],[9,11,2],[10,6,1]],\"faces_vertices\":[[0,1,11,6],[1,2,10,11],[2,3,9,10],[3,4,8,9],[4,5,7,8]],\"faces_edges\":[[0,11,10,5],[1,12,9,11],[2,13,8,12],[3,14,7,13],[4,15,6,14]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[4,null],[null,0],[4,null],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[0],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,null,null,3]],\"file_frames\":[{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"F\",\"V\",\"F\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,-90,0,180,0,-90,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"F\",\"V\",\"F\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,-120,0,180,0,-120,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"F\",\"V\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,-90,0,180,0,90,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"F\",\"V\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,-180,0,180,0,60,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[0,1],[11,1],[10,1],[9,1],[8,1],[7,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[12,0],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,12],[23,1],[22,2],[21,3],[20,4],[19,5],[6,18],[17,7],[16,8],[15,9],[10,14],[13,11]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"F\",\"F\",\"M\",\"M\",\"V\",\"F\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,90,0,0,0,-120,-120,60,0,0,90,0],\"vertices_edges\":[[0,11],[1,23,0],[2,24,1],[3,25,2],[4,26,3],[5,27,4],[6,28,5],[7,29,6],[8,30,7],[9,31,8],[10,32,9],[33,10],[22,11],[12,33],[12,13,32],[13,14,31],[14,15,30],[15,16,29],[16,17,28],[17,18,27],[18,19,26],[19,20,25],[20,21,24],[21,22,23]],\"vertices_vertices\":[[1,12],[2,23,0],[3,22,1],[4,21,2],[5,20,3],[6,19,4],[7,18,5],[8,17,6],[9,16,7],[10,15,8],[11,14,9],[13,10],[23,0],[14,11],[13,15,10],[14,16,9],[15,17,8],[16,18,7],[17,19,6],[18,20,5],[19,21,4],[20,22,3],[21,23,2],[22,12,1]],\"faces_vertices\":[[0,1,23,12],[1,2,22,23],[2,3,21,22],[3,4,20,21],[4,5,19,20],[5,6,18,19],[6,7,17,18],[7,8,16,17],[8,9,15,16],[9,10,14,15],[10,11,13,14]],\"faces_edges\":[[0,23,22,11],[1,24,21,23],[2,25,20,24],[3,26,19,25],[4,27,18,26],[5,28,17,27],[6,29,16,28],[7,30,15,29],[8,31,14,30],[9,32,13,31],[10,33,12,32]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[6,5,null],[7,6,null],[8,7,null],[9,8,null],[10,9,null],[10,null],[null,0],[10,null],[null,9,10],[null,8,9],[null,7,8],[null,6,7],[null,5,6],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[0],[10],[9],[8],[7],[6],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,6,null,4],[null,7,null,5],[null,8,null,6],[null,9,null,7],[null,10,null,8],[null,null,null,9]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[0,1],[12,1],[11,1],[10,1],[9,1],[8,1],[7,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[13,0],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24,25],[25,13],[25,1],[24,2],[23,3],[22,4],[21,5],[20,6],[19,7],[8,18],[17,9],[16,10],[15,11],[12,14]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"V\",\"F\",\"V\",\"M\",\"M\",\"F\",\"M\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,135,0,90,0,180,-180,-180,0,-90,0,135,0],\"vertices_edges\":[[0,12],[1,25,0],[2,26,1],[3,27,2],[4,28,3],[5,29,4],[6,30,5],[7,31,6],[8,32,7],[9,33,8],[10,34,9],[11,35,10],[36,11],[24,12],[13,36],[13,14,35],[14,15,34],[15,16,33],[16,17,32],[17,18,31],[18,19,30],[19,20,29],[20,21,28],[21,22,27],[22,23,26],[23,24,25]],\"vertices_vertices\":[[1,13],[2,25,0],[3,24,1],[4,23,2],[5,22,3],[6,21,4],[7,20,5],[8,19,6],[9,18,7],[10,17,8],[11,16,9],[12,15,10],[14,11],[25,0],[15,12],[14,16,11],[15,17,10],[16,18,9],[17,19,8],[18,20,7],[19,21,6],[20,22,5],[21,23,4],[22,24,3],[23,25,2],[24,13,1]],\"faces_vertices\":[[0,1,25,13],[1,2,24,25],[2,3,23,24],[3,4,22,23],[4,5,21,22],[5,6,20,21],[6,7,19,20],[7,8,18,19],[8,9,17,18],[9,10,16,17],[10,11,15,16],[11,12,14,15]],\"faces_edges\":[[0,25,24,12],[1,26,23,25],[2,27,22,26],[3,28,21,27],[4,29,20,28],[5,30,19,29],[6,31,18,30],[7,32,17,31],[8,33,16,32],[9,34,15,33],[10,35,14,34],[11,36,13,35]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[6,5,null],[7,6,null],[8,7,null],[9,8,null],[10,9,null],[11,10,null],[11,null],[null,0],[11,null],[null,10,11],[null,9,10],[null,8,9],[null,7,8],[null,6,7],[null,5,6],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[0],[11],[10],[9],[8],[7],[6],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,6,null,4],[null,7,null,5],[null,8,null,6],[null,9,null,7],[null,10,null,8],[null,11,null,9],[null,null,null,10]]},{\"frame_description\":\"invalid\",\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[0,1],[7,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[8,0],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,8],[15,1],[14,2],[13,3],[12,4],[11,5],[10,6],[9,7]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\"V\",\"V\",\"M\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,120,120,-60,60,0],\"vertices_edges\":[[0,7],[1,15,0],[2,16,1],[3,17,2],[4,18,3],[5,19,4],[6,20,5],[21,6],[14,7],[8,21],[8,9,20],[9,10,19],[10,11,18],[11,12,17],[12,13,16],[13,14,15]],\"vertices_vertices\":[[1,8],[2,15,0],[3,14,1],[4,13,2],[5,12,3],[6,11,4],[7,10,5],[9,6],[15,0],[10,7],[9,11,6],[10,12,5],[11,13,4],[12,14,3],[13,15,2],[14,8,1]],\"faces_vertices\":[[0,1,15,8],[1,2,14,15],[2,3,13,14],[3,4,12,13],[4,5,11,12],[5,6,10,11],[6,7,9,10]],\"faces_edges\":[[0,15,14,7],[1,16,13,15],[2,17,12,16],[3,18,11,17],[4,19,10,18],[5,20,9,19],[6,21,8,20]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[6,5,null],[6,null],[null,0],[6,null],[null,5,6],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[0],[6],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,6,null,4],[null,null,null,5]]},{\"frame_description\":\"invalid\",\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[6,0],[7,8],[8,9],[9,10],[10,11],[11,6],[11,1],[10,2],[9,3],[8,4],[7,5]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"F\",\"V\",\"V\",\"F\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,120,120,0,0],\"vertices_edges\":[[0,5],[1,11,0],[2,12,1],[3,13,2],[4,14,3],[15,4],[10,5],[6,15],[6,7,14],[7,8,13],[8,9,12],[9,10,11]],\"vertices_vertices\":[[1,6],[2,11,0],[3,10,1],[4,9,2],[5,8,3],[7,4],[11,0],[8,5],[7,9,4],[8,10,3],[9,11,2],[10,6,1]],\"faces_vertices\":[[0,1,11,6],[1,2,10,11],[2,3,9,10],[3,4,8,9],[4,5,7,8]],\"faces_edges\":[[0,11,10,5],[1,12,9,11],[2,13,8,12],[3,14,7,13],[4,15,6,14]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[4,null],[null,0],[4,null],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[0],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,null,null,3]]},{\"frame_description\":\"invalid\",\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"V\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,90,0,180,0,90,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]},{\"frame_description\":\"invalid\",\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"V\",\"F\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,120,0,180,0,120,0],\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]}]}"
  },
  {
    "path": "tests/files/fold/layers-3d-edge-face.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[2,0],[0,2],[0,1],[2,2],[1,1],[2,1.4142135623730951],[1,2],[2,1]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[6,7],[8,6],[6,1],[4,6],[6,9],[2,9],[9,7],[7,5],[5,8],[8,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,180,-90,90,135,-180,0,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[10,1],[14,2],[8,2,3],[13,4,12],[9,5,4,6,8,7],[12,5,11],[13,14,6],[11,9,10]],\"vertices_vertices\":[[1,4],[2,6,0],[9,1],[8,4],[6,3,0],[8,6,7],[9,7,5,8,4,1],[5,6,9],[5,3,6],[7,6,2]],\"faces_vertices\":[[0,1,6,4],[1,2,9,6],[3,4,6,8],[5,8,6],[5,6,7],[6,9,7]],\"faces_edges\":[[0,7,8,3],[1,10,9,7],[2,8,6,14],[13,6,4],[4,5,12],[9,11,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[2,null,0],[3,4,null],[5,4,3,2,0,1],[4,5,null],[null,2,3],[5,1,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[4,5],[2,3],[0,1],[0,2],[1,5],[1],[5],[4],[3],[2]],\"faces_faces\":[[null,1,2,null],[null,null,5,0],[null,0,3,null],[null,2,4],[3,5,null],[1,null,4]],\"file_frames\":[{\"vertices_coords\":[[0,0],[1,0],[1.5,0],[0,2],[0,1],[1.5,1.5],[1,1],[1.5,1.2071067811865475],[1,2],[1.5,1],[1.5,2]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[6,7],[8,6],[6,1],[4,6],[6,9],[10,5],[5,7],[7,9],[9,2],[10,8],[8,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,180,-90,90,135,-180,0,0,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[13,1],[15,2],[8,2,3],[10,4,11],[9,5,4,6,8,7],[11,5,12],[14,15,6],[12,9,13],[14,10]],\"vertices_vertices\":[[1,4],[2,6,0],[9,1],[8,4],[6,3,0],[10,6,7],[9,7,5,8,4,1],[5,6,9],[10,3,6],[7,6,2],[8,5]],\"faces_vertices\":[[0,1,6,4],[1,2,9,6],[3,4,6,8],[5,10,8,6],[5,6,7],[6,9,7]],\"faces_edges\":[[0,7,8,3],[1,13,9,7],[2,8,6,15],[10,14,6,4],[4,5,11],[9,12,5]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,2],[2,null,0],[3,4,null],[5,4,3,2,0,1],[4,5,null],[null,2,3],[5,1,null],[3,null]],\"edges_faces\":[[0],[1],[2],[0],[3,4],[4,5],[2,3],[0,1],[0,2],[1,5],[3],[4],[5],[1],[3],[2]],\"faces_faces\":[[null,1,2,null],[null,null,5,0],[null,0,3,null],[null,null,2,4],[3,5,null],[1,null,4]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[0,2],[0,1],[2,2],[1,1],[2,1.4142135623730951],[1,2],[2,1]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[5,6],[6,7],[8,6],[6,1],[4,6],[6,9],[6,2],[2,9],[9,7],[7,5],[5,8],[8,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"F\",\"B\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,180,-90,90,135,-180,0,0,0,0,0,0],\"vertices_edges\":[[0,3],[1,7,0],[11,10,1],[15,2],[8,2,3],[14,4,13],[9,5,4,6,8,7,10],[13,5,12],[14,15,6],[12,9,11]],\"vertices_vertices\":[[1,4],[2,6,0],[9,6,1],[8,4],[6,3,0],[8,6,7],[9,7,5,8,4,1,2],[5,6,9],[5,3,6],[7,6,2]],\"faces_vertices\":[[0,1,6,4],[1,2,6],[2,9,6],[3,4,6,8],[5,8,6],[5,6,7],[6,9,7]],\"faces_edges\":[[0,7,8,3],[1,10,7],[11,9,10],[2,8,6,15],[14,6,4],[4,5,13],[9,12,5]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[null,3],[3,null,0],[4,5,null],[6,5,4,3,0,1,2],[5,6,null],[null,3,4],[6,2,null]],\"edges_faces\":[[0],[1],[3],[0],[4,5],[5,6],[3,4],[0,1],[0,3],[2,6],[1,2],[2],[6],[5],[4],[3]],\"faces_faces\":[[null,1,3,null],[null,2,0],[null,6,1],[null,0,4,null],[null,3,5],[4,6,null],[2,null,5]]},{\"vertices_coords\":[[0,0],[2,0],[3,0],[4,0],[5,0],[0,1],[5,1],[4,1],[3,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[5,0],[6,7],[7,8],[8,9],[9,5],[9,1],[8,2],[7,3],[4,6]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,120,120,-60,0],\"vertices_edges\":[[0,4],[1,9,0],[2,10,1],[3,11,2],[12,3],[8,4],[5,12],[5,6,11],[6,7,10],[7,8,9]],\"vertices_vertices\":[[1,5],[2,9,0],[3,8,1],[4,7,2],[6,3],[9,0],[7,4],[6,8,3],[7,9,2],[8,5,1]],\"faces_vertices\":[[0,1,9,5],[1,2,8,9],[2,3,7,8],[3,4,6,7]],\"faces_edges\":[[0,9,8,4],[1,10,7,9],[2,11,6,10],[3,12,5,11]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[3,null],[null,0],[3,null],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[0],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,null,null,2]]},{\"vertices_coords\":[[0,2],[0,1],[3,3],[2,2],[3,0],[4,0],[5,0],[2,1],[5,3],[4,3]],\"edges_vertices\":[[0,1],[2,3],[4,5],[5,6],[1,7],[3,0],[3,7],[7,4],[8,9],[9,2],[2,4],[9,5],[6,8]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"V\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,120,0,0,0,120,-60,0],\"vertices_edges\":[[5,0],[4,0],[9,1,10],[1,5,6],[2,10,7],[3,11,2],[12,3],[6,4,7],[8,12],[8,9,11]],\"vertices_vertices\":[[3,1],[7,0],[9,3,4],[2,0,7],[5,2,7],[6,9,4],[8,5],[3,1,4],[9,6],[8,2,5]],\"faces_vertices\":[[0,1,7,3],[2,3,7,4],[2,4,5,9],[5,6,8,9]],\"faces_edges\":[[0,4,6,5],[1,6,7,10],[10,2,11,9],[3,12,8,11]],\"vertices_faces\":[[null,0],[0,null],[null,1,2],[null,0,1],[2,1,null],[3,2,null],[3,null],[0,null,1],[3,null],[null,2,3]],\"edges_faces\":[[0],[1],[2],[3],[0],[0],[0,1],[1],[3],[2],[1,2],[2,3],[3]],\"faces_faces\":[[null,null,1,null],[null,0,null,2],[1,null,3,null],[null,null,null,2]]},{\"vertices_coords\":[[0,3],[0,0],[2,0],[3,1],[4,1],[5,1],[5,2],[4,2],[3,2],[2,3]],\"edges_vertices\":[[0,1],[1,2],[3,4],[4,5],[2,3],[6,7],[7,8],[9,2],[9,0],[8,3],[8,9],[7,4],[5,6]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"V\",\"B\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,120,0,120,0,-60,0],\"vertices_edges\":[[8,0],[1,0],[4,7,1],[2,9,4],[3,11,2],[12,3],[5,12],[5,6,11],[6,10,9],[8,7,10]],\"vertices_vertices\":[[9,1],[2,0],[3,9,1],[4,8,2],[5,7,3],[6,4],[7,5],[6,8,4],[7,9,3],[0,2,8]],\"faces_vertices\":[[0,1,2,9],[2,3,8,9],[3,4,7,8],[4,5,6,7]],\"faces_edges\":[[0,1,7,8],[4,9,10,7],[2,11,6,9],[3,12,5,11]],\"vertices_faces\":[[null,0],[0,null],[1,0,null],[2,1,null],[3,2,null],[3,null],[3,null],[null,2,3],[null,1,2],[0,1,null]],\"edges_faces\":[[0],[0],[2],[3],[1],[3],[2],[0,1],[0],[1,2],[1],[2,3],[3]],\"faces_faces\":[[null,null,1,null],[null,2,null,0],[null,3,null,1],[null,null,null,2]]}]}"
  },
  {
    "path": "tests/files/fold/layers-cycle-nonconvex.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[3,0],[6,0],[0,3],[0,2],[0,-3],[2,4],[3,6],[1,-1],[3,1],[4,1],[-1,2],[-3,3],[2,3],[3,3]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[4,5],[5,0],[0,6],[7,8],[1,9],[10,11],[9,6],[12,13],[5,12],[14,7],[11,3],[2,10],[10,15],[15,8],[15,14],[14,4],[4,13]],\n\t\"edges_assignment\":[\"V\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"V\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,2,10,15,14,4,5],[0,6,9,1],[2,3,11,10],[4,13,12,5],[7,14,15,8]],\n\t\"faces_edges\":[[0,1,14,15,17,18,3,4],[5,9,7,0],[2,13,8,14],[19,10,11,3],[12,17,16,6]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[0,0],[1,0],[3,0],[0,0],[0,3],[0,2],[0,3],[2,2],[3,0],[1,1],[3,1],[2,1],[1,2],[3,3],[2,3],[3,3]\n\t\t],\n\t\t\"faceOrders\":[\n\t\t\t[0,1,1],[0,2,1],[0,3,1],[0,4,1],\n\t\t\t[1,2,-1],[2,4,-1],[4,3,-1],[3,1,-1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/layers-flat-grid.fold",
    "content": "{\n\t\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[0,8],[0,7],[0,6],[0,5],[0,4],[0,3],[0,2],[0,1],[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7],[1,8],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],[8,1],[4,2],[5,3],[6,4],[7,5],[8,6],[2,8],[2,7],[2,6],[2,5],[2,4],[2,3],[2,2],[3,2],[5,2],[6,2],[7,2],[8,2],[3,3],[3,4],[3,5],[3,6],[3,7],[3,8],[8,3],[7,3],[6,3],[4,3],[4,8],[4,7],[4,6],[4,5],[4,4],[8,4],[7,4],[5,4],[5,8],[5,7],[5,6],[5,5],[8,5],[6,5],[6,6],[6,7],[6,8],[7,6],[7,8],[7,7],[8,7],[8,8]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,0],[1,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[16,17],[17,25],[25,26],[26,27],[27,28],[28,29],[29,30],[30,31],[2,26],[26,32],[32,33],[33,34],[34,35],[35,36],[37,38],[38,39],[39,40],[40,41],[41,42],[42,43],[43,25],[25,2],[15,18],[18,43],[43,44],[44,32],[32,45],[45,46],[46,47],[47,48],[3,26],[26,44],[44,49],[49,50],[50,51],[51,52],[52,53],[53,54],[55,56],[56,57],[57,33],[33,58],[58,49],[49,42],[42,19],[19,14],[59,60],[60,61],[61,62],[62,63],[63,58],[58,32],[32,27],[27,4],[64,65],[65,34],[34,66],[66,63],[63,50],[50,41],[41,20],[20,13],[67,68],[68,69],[69,70],[70,66],[66,33],[33,45],[45,28],[28,5],[71,35],[35,72],[72,70],[70,62],[62,51],[51,40],[40,21],[21,12],[6,29],[29,46],[46,57],[57,34],[34,72],[72,73],[73,74],[74,75],[36,76],[76,73],[73,69],[69,61],[61,52],[52,39],[39,22],[22,11],[77,78],[78,76],[76,35],[35,65],[65,56],[56,47],[47,30],[30,7],[10,23],[23,38],[38,53],[53,60],[60,68],[68,74],[74,78],[78,79],[8,31],[31,48],[48,55],[55,64],[64,71],[71,36],[36,79],[79,80],[80,77],[77,75],[75,67],[67,59],[59,54],[54,37],[37,24],[24,9]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,17,16],[1,2,25,17],[2,3,26],[2,26,25],[3,4,27,26],[4,5,28,27],[5,6,29,28],[6,7,30,29],[7,8,31,30],[9,10,23,24],[10,11,22,23],[11,12,21,22],[12,13,20,21],[13,14,19,20],[14,15,18,19],[15,16,17,18],[17,25,43,18],[18,43,42,19],[19,42,41,20],[20,41,40,21],[21,40,39,22],[22,39,38,23],[23,38,37,24],[25,26,44,43],[26,27,32],[26,32,44],[27,28,45,32],[28,29,46,45],[29,30,47,46],[30,31,48,47],[32,45,33],[32,33,58],[32,58,49,44],[33,57,34],[33,34,66],[33,66,63,58],[33,45,46,57],[34,65,35],[34,35,72],[34,72,70,66],[34,57,56,65],[35,71,36],[35,36,76],[35,76,73,72],[35,65,64,71],[36,79,78,76],[37,38,53,54],[38,39,52,53],[39,40,51,52],[40,41,50,51],[41,42,49,50],[42,43,44,49],[46,47,56,57],[47,48,55,56],[49,58,63,50],[50,63,62,51],[51,62,61,52],[52,61,60,53],[53,60,59,54],[55,64,65,56],[59,60,68,67],[60,61,69,68],[61,62,70,69],[62,63,66,70],[67,68,74,75],[68,69,73,74],[69,70,72,73],[73,76,78,74],[74,78,77,75],[77,78,79,80]]\n}\n"
  },
  {
    "path": "tests/files/fold/layers-zipper.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],[15,0],[16,0],[17,0],[18,0],[19,0],[20,0],[21,0],[22,0],[23,0],[24,0],[25,0],[0,1],[25,1],[24,1],[23,1],[22,1],[21,1],[20,1],[19,1],[18,1],[17,1],[16,1],[15,1],[14,1],[13,1],[12,1],[11,1],[10,1],[9,1],[8,1],[7,1],[6,1],[5,1],[4,1],[3,1],[2,1],[1,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24,25],[26,0],[27,28],[28,29],[29,30],[30,31],[31,32],[32,33],[33,34],[34,35],[35,36],[36,37],[37,38],[38,39],[39,40],[40,41],[41,42],[42,43],[43,44],[44,45],[45,46],[46,47],[47,48],[48,49],[49,50],[50,51],[51,26],[51,1],[50,2],[49,3],[48,4],[47,5],[46,6],[45,7],[44,8],[43,9],[42,10],[41,11],[40,12],[39,13],[38,14],[37,15],[36,16],[35,17],[34,18],[33,19],[32,20],[31,21],[30,22],[29,23],[24,28],[27,25]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,-180,180,-180,180,-180,180,-180,180,-180,180,-180,180,180,-180,180,-180,180,-180,180,-180,180,-180,180,0],\"vertices_edges\":[[0,25],[1,51,0],[2,52,1],[3,53,2],[4,54,3],[5,55,4],[6,56,5],[7,57,6],[8,58,7],[9,59,8],[10,60,9],[11,61,10],[12,62,11],[13,63,12],[14,64,13],[15,65,14],[16,66,15],[17,67,16],[18,68,17],[19,69,18],[20,70,19],[21,71,20],[22,72,21],[23,73,22],[24,74,23],[75,24],[50,25],[26,75],[26,27,74],[27,28,73],[28,29,72],[29,30,71],[30,31,70],[31,32,69],[32,33,68],[33,34,67],[34,35,66],[35,36,65],[36,37,64],[37,38,63],[38,39,62],[39,40,61],[40,41,60],[41,42,59],[42,43,58],[43,44,57],[44,45,56],[45,46,55],[46,47,54],[47,48,53],[48,49,52],[49,50,51]],\"vertices_vertices\":[[1,26],[2,51,0],[3,50,1],[4,49,2],[5,48,3],[6,47,4],[7,46,5],[8,45,6],[9,44,7],[10,43,8],[11,42,9],[12,41,10],[13,40,11],[14,39,12],[15,38,13],[16,37,14],[17,36,15],[18,35,16],[19,34,17],[20,33,18],[21,32,19],[22,31,20],[23,30,21],[24,29,22],[25,28,23],[27,24],[51,0],[28,25],[27,29,24],[28,30,23],[29,31,22],[30,32,21],[31,33,20],[32,34,19],[33,35,18],[34,36,17],[35,37,16],[36,38,15],[37,39,14],[38,40,13],[39,41,12],[40,42,11],[41,43,10],[42,44,9],[43,45,8],[44,46,7],[45,47,6],[46,48,5],[47,49,4],[48,50,3],[49,51,2],[50,26,1]],\"faces_vertices\":[[0,1,51,26],[1,2,50,51],[2,3,49,50],[3,4,48,49],[4,5,47,48],[5,6,46,47],[6,7,45,46],[7,8,44,45],[8,9,43,44],[9,10,42,43],[10,11,41,42],[11,12,40,41],[12,13,39,40],[13,14,38,39],[14,15,37,38],[15,16,36,37],[16,17,35,36],[17,18,34,35],[18,19,33,34],[19,20,32,33],[20,21,31,32],[21,22,30,31],[22,23,29,30],[23,24,28,29],[24,25,27,28]],\"faces_edges\":[[0,51,50,25],[1,52,49,51],[2,53,48,52],[3,54,47,53],[4,55,46,54],[5,56,45,55],[6,57,44,56],[7,58,43,57],[8,59,42,58],[9,60,41,59],[10,61,40,60],[11,62,39,61],[12,63,38,62],[13,64,37,63],[14,65,36,64],[15,66,35,65],[16,67,34,66],[17,68,33,67],[18,69,32,68],[19,70,31,69],[20,71,30,70],[21,72,29,71],[22,73,28,72],[23,74,27,73],[24,75,26,74]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[6,5,null],[7,6,null],[8,7,null],[9,8,null],[10,9,null],[11,10,null],[12,11,null],[13,12,null],[14,13,null],[15,14,null],[16,15,null],[17,16,null],[18,17,null],[19,18,null],[20,19,null],[21,20,null],[22,21,null],[23,22,null],[24,23,null],[24,null],[null,0],[24,null],[null,23,24],[null,22,23],[null,21,22],[null,20,21],[null,19,20],[null,18,19],[null,17,18],[null,16,17],[null,15,16],[null,14,15],[null,13,14],[null,12,13],[null,11,12],[null,10,11],[null,9,10],[null,8,9],[null,7,8],[null,6,7],[null,5,6],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[20],[21],[22],[23],[24],[0],[24],[23],[22],[21],[20],[19],[18],[17],[16],[15],[14],[13],[12],[11],[10],[9],[8],[7],[6],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,6,null,4],[null,7,null,5],[null,8,null,6],[null,9,null,7],[null,10,null,8],[null,11,null,9],[null,12,null,10],[null,13,null,11],[null,14,null,12],[null,15,null,13],[null,16,null,14],[null,17,null,15],[null,18,null,16],[null,19,null,17],[null,20,null,18],[null,21,null,19],[null,22,null,20],[null,23,null,21],[null,24,null,22],[null,null,null,23]]}"
  },
  {
    "path": "tests/files/fold/maze-8x8.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_title\": \"Maze Folding\",\n\t\"file_author\": \"Demaine, Demaine, Ku\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"vertices_coords\": [\n\t\t[0,0,0],[18,0,0],[18,18,0],[0,18,0],[0,1,0],[1,1,0],[2,1,0],[0,2,0],[1,2,1],[1,2,0],[0,1,0],[2,1,0],[1,0,0],[0,0,0],[0,0,0],[2,1,1],[2,0,0],[0,1,0],[1,0,0],[1,2,0],[1,1,0],[1,2,0],[1,1,0],[1,1,2],[2,1,2],[1,2,2],[1,1,0],[1,1,0],[2,1,0],[1,1,0],[2,1,0],[1,2,0],[1,0,0],[1,1,2],[1,2,1],[2,3,0],[0,3,0],[2,2,0],[0,2,0],[1,3,0],[1,2,0],[1,4,0],[1,3,0],[1,4,0],[2,3,0],[0,3,0],[2,4,0],[0,4,0],[1,3,0],[1,2,0],[1,4,0],[1,3,0],[1,4,0],[1,3,2],[1,2,2],[1,4,2],[1,3,2],[1,4,2],[2,5,0],[0,5,0],[2,4,0],[0,4,0],[1,5,0],[1,4,0],[1,6,0],[1,5,0],[1,6,0],[2,5,0],[0,5,0],[2,6,0],[0,6,0],[1,5,0],[1,4,0],[1,6,0],[1,5,0],[1,6,0],[1,5,2],[1,4,2],[1,6,2],[1,5,2],[1,6,2],[2,7,0],[0,7,0],[2,6,0],[0,6,0],[1,7,0],[1,6,0],[1,8,0],[1,7,0],[1,8,0],[2,7,0],[0,7,0],[2,8,0],[0,8,0],[1,7,0],[1,6,0],[1,8,0],[1,7,0],[1,8,0],[1,7,2],[1,6,2],[1,8,2],[1,7,2],[1,8,2],[2,9,0],[0,9,0],[2,8,0],[0,8,0],[1,9,0],[1,8,0],[1,10,0],[1,9,0],[1,10,0],[2,9,0],[0,9,0],[2,10,0],[0,10,0],[1,9,0],[1,8,0],[1,10,0],[1,9,0],[1,10,0],[1,9,2],[1,8,2],[1,10,2],[1,9,2],[1,10,2],[2,11,0],[0,11,0],[2,10,0],[0,10,0],[1,11,0],[1,10,0],[1,12,0],[1,11,0],[1,12,0],[2,11,0],[0,11,0],[2,12,0],[0,12,0],[1,11,0],[1,10,0],[1,12,0],[1,11,0],[1,12,0],[1,11,2],[1,10,2],[1,12,2],[1,11,2],[1,12,2],[2,13,0],[0,13,0],[2,12,0],[0,12,0],[1,13,0],[1,12,0],[1,14,0],[1,13,0],[1,14,0],[2,13,0],[0,13,0],[2,14,0],[0,14,0],[1,13,0],[1,12,0],[1,14,0],[1,13,0],[1,14,0],[1,13,2],[1,12,2],[1,14,2],[1,13,2],[1,14,2],[2,15,0],[0,15,0],[2,14,0],[0,14,0],[1,15,0],[1,14,0],[1,16,0],[1,15,0],[1,16,0],[2,15,0],[0,15,0],[2,16,0],[0,16,0],[1,15,0],[1,14,0],[1,16,0],[1,15,0],[1,16,0],[1,15,2],[1,14,2],[1,16,2],[1,15,2],[1,16,2],[1,18,0],[1,17,0],[2,18,0],[2,17,1],[2,17,0],[1,18,0],[1,16,0],[0,17,0],[0,18,0],[0,18,0],[1,16,1],[0,16,0],[1,18,0],[0,17,0],[2,17,0],[1,17,0],[2,17,0],[1,17,0],[1,17,2],[2,17,2],[1,17,0],[1,17,0],[1,16,0],[1,17,0],[2,17,0],[0,17,0],[1,17,2],[2,17,1],[2,1,1],[2,0,0],[3,1,0],[3,0,0],[3,2,0],[3,1,2],[2,1,2],[3,1,2],[2,1,0],[3,1,0],[4,1,1],[4,0,0],[3,1,0],[3,0,0],[3,2,0],[4,1,0],[4,1,2],[3,1,2],[4,1,2],[4,1,0],[3,1,0],[4,1,0],[3,2,2],[3,4,0],[3,3,0],[3,3,0],[3,3,0],[3,2,0],[3,2,0],[3,3,0],[3,2,1],[3,3,2],[2,4,0],[3,4,0],[3,3,0],[4,3,0],[4,4,0],[3,3,0],[3,2,0],[3,2,0],[3,3,0],[3,2,1],[4,4,0],[4,2,0],[4,3,0],[3,5,0],[3,5,0],[3,5,0],[4,5,0],[4,5,0],[3,5,0],[4,5,0],[4,5,1],[3,5,2],[3,5,0],[3,6,0],[2,6,0],[3,5,0],[4,5,0],[4,5,0],[3,5,0],[4,5,0],[4,5,1],[4,6,0],[3,6,0],[4,5,2],[3,7,0],[4,7,0],[4,6,0],[3,7,0],[3,7,0],[3,8,0],[3,8,0],[3,7,0],[3,8,0],[3,8,1],[3,7,2],[4,8,0],[3,7,0],[3,7,0],[3,8,0],[3,8,0],[3,7,0],[3,8,0],[3,8,1],[4,7,0],[3,8,2],[3,10,0],[3,9,0],[4,10,0],[4,9,1],[4,9,0],[3,10,0],[3,8,0],[2,9,0],[2,10,0],[3,8,1],[3,10,0],[4,9,0],[3,9,0],[4,9,0],[3,9,0],[3,9,2],[4,9,2],[3,9,0],[3,9,0],[3,8,0],[3,9,0],[4,9,0],[3,9,2],[4,9,1],[3,11,0],[4,11,0],[4,10,0],[3,11,0],[3,11,0],[3,12,0],[3,12,0],[3,11,0],[3,12,0],[3,12,1],[3,11,2],[4,12,0],[3,11,0],[3,11,0],[3,12,0],[3,12,0],[3,11,0],[3,12,0],[3,12,1],[4,11,0],[3,12,2],[4,13,0],[4,12,0],[3,13,0],[3,12,0],[3,14,0],[3,13,0],[3,14,0],[4,13,0],[4,14,0],[3,13,0],[3,12,0],[3,14,0],[3,13,0],[3,14,0],[3,13,2],[3,12,2],[3,14,2],[3,13,2],[3,14,2],[3,16,1],[3,15,0],[4,15,0],[3,16,0],[3,15,2],[3,16,2],[3,15,2],[3,16,2],[3,16,0],[3,15,0],[3,16,0],[3,14,1],[3,15,0],[4,15,0],[3,14,2],[3,15,2],[3,14,0],[3,15,0],[4,15,2],[4,17,1],[4,18,0],[3,17,0],[3,18,0],[4,17,0],[3,17,2],[4,17,2],[3,17,2],[4,17,2],[4,17,0],[3,17,0],[4,17,0],[2,17,1],[2,18,0],[3,17,0],[3,18,0],[2,17,2],[3,17,2],[2,17,0],[3,17,0],[5,0,0],[5,2,0],[4,0,0],[4,2,0],[5,1,0],[4,1,0],[6,1,0],[5,1,0],[6,1,0],[5,0,0],[5,2,0],[6,0,0],[6,2,0],[5,1,0],[4,1,0],[6,1,0],[5,1,0],[6,1,0],[5,1,2],[4,1,2],[6,1,2],[5,1,2],[6,1,2],[5,3,0],[6,3,0],[5,4,1],[5,4,0],[4,3,0],[6,3,0],[5,2,0],[6,3,1],[5,4,0],[5,3,0],[5,4,0],[5,3,0],[5,3,2],[6,3,2],[5,4,2],[5,3,0],[5,3,0],[6,3,0],[5,3,0],[6,3,0],[5,4,0],[5,3,2],[5,4,1],[5,4,1],[6,4,0],[5,5,0],[6,5,0],[5,5,2],[5,4,2],[5,5,2],[5,4,0],[5,5,0],[5,6,1],[6,6,0],[5,5,0],[6,5,0],[5,6,0],[5,6,2],[5,5,2],[5,6,2],[5,6,0],[5,5,0],[5,6,0],[5,8,0],[5,7,0],[5,7,0],[5,7,0],[5,6,0],[5,6,0],[5,7,0],[5,6,1],[5,7,2],[4,8,0],[5,8,0],[5,7,0],[6,7,0],[6,8,0],[5,7,0],[5,6,0],[5,6,0],[5,7,0],[5,6,1],[6,8,0],[6,6,0],[6,7,0],[4,9,1],[5,9,0],[5,10,0],[5,9,2],[4,9,2],[5,9,2],[4,9,0],[5,9,0],[6,9,1],[5,9,0],[5,10,0],[6,9,0],[6,9,2],[5,9,2],[6,9,2],[6,9,0],[5,9,0],[6,9,0],[5,10,2],[5,12,0],[5,11,0],[5,11,0],[5,11,0],[5,10,0],[5,10,0],[5,11,0],[5,10,1],[5,11,2],[4,12,0],[5,12,0],[5,11,0],[6,11,0],[6,12,0],[5,11,0],[5,10,0],[5,10,0],[5,11,0],[5,10,1],[6,12,0],[6,10,0],[6,11,0],[5,13,0],[5,13,0],[5,13,0],[6,13,0],[6,13,0],[5,13,0],[6,13,0],[6,13,1],[5,13,2],[5,13,0],[5,14,0],[4,14,0],[5,13,0],[6,13,0],[6,13,0],[5,13,0],[6,13,0],[6,13,1],[6,14,0],[5,14,0],[6,13,2],[6,15,0],[5,15,0],[5,16,0],[6,16,0],[5,15,0],[5,15,0],[4,15,0],[4,15,0],[5,15,0],[4,15,1],[5,15,2],[6,16,0],[4,16,0],[6,15,0],[5,15,0],[5,15,0],[4,15,0],[4,15,0],[5,15,0],[4,15,1],[6,14,0],[5,16,0],[5,18,0],[4,18,0],[5,17,0],[4,17,0],[6,17,0],[5,17,0],[6,17,0],[5,18,0],[6,18,0],[5,17,0],[4,17,0],[6,17,0],[5,17,0],[6,17,0],[5,17,2],[4,17,2],[6,17,2],[5,17,2],[6,17,2],[6,1,1],[6,0,0],[7,1,0],[7,0,0],[7,2,0],[7,1,2],[6,1,2],[7,1,2],[6,1,0],[7,1,0],[8,1,1],[8,0,0],[7,1,0],[7,0,0],[7,2,0],[8,1,0],[8,1,2],[7,1,2],[8,1,2],[8,1,0],[7,1,0],[8,1,0],[7,2,2],[8,3,0],[7,3,0],[8,2,0],[7,2,1],[7,2,0],[8,3,0],[6,3,0],[7,4,0],[8,4,0],[8,4,0],[6,3,1],[6,4,0],[8,3,0],[7,4,0],[7,3,0],[7,2,0],[7,3,0],[7,3,2],[7,3,0],[7,3,0],[6,3,0],[7,3,0],[7,4,0],[7,3,2],[7,2,1],[7,5,0],[8,5,0],[7,6,1],[7,6,0],[6,5,0],[8,5,0],[7,4,0],[8,5,1],[7,6,0],[7,5,0],[7,6,0],[7,5,0],[7,5,2],[8,5,2],[7,6,2],[7,5,0],[7,5,0],[8,5,0],[7,5,0],[8,5,0],[7,6,0],[7,5,2],[7,6,1],[7,8,0],[7,7,0],[7,7,0],[7,7,0],[7,6,0],[7,6,0],[7,7,0],[7,6,1],[7,7,2],[6,8,0],[7,8,0],[7,7,0],[8,7,0],[8,8,0],[7,7,0],[7,6,0],[7,6,0],[7,7,0],[7,6,1],[8,8,0],[8,6,0],[8,7,0],[7,10,0],[6,10,0],[7,9,0],[6,9,0],[8,9,0],[7,9,0],[8,9,0],[7,10,0],[8,10,0],[7,9,0],[6,9,0],[8,9,0],[7,9,0],[8,9,0],[7,9,2],[6,9,2],[8,9,2],[7,9,2],[8,9,2],[7,11,0],[7,11,0],[7,11,0],[8,11,0],[8,11,0],[7,11,0],[8,11,0],[8,11,1],[7,11,2],[7,11,0],[7,12,0],[6,12,0],[7,11,0],[8,11,0],[8,11,0],[7,11,0],[8,11,0],[8,11,1],[8,12,0],[7,12,0],[8,11,2],[7,13,0],[7,14,0],[6,13,1],[6,13,0],[7,12,0],[7,14,0],[8,13,0],[8,12,0],[7,14,1],[8,14,0],[8,13,0],[7,13,0],[6,13,0],[7,13,0],[7,13,2],[7,14,2],[7,13,0],[7,13,0],[7,14,0],[7,13,0],[7,14,0],[8,13,0],[7,13,2],[6,13,1],[8,15,0],[8,14,0],[7,15,0],[7,14,0],[7,16,0],[7,15,0],[7,16,0],[8,15,0],[8,16,0],[7,15,0],[7,14,0],[7,16,0],[7,15,0],[7,16,0],[7,15,2],[7,14,2],[7,16,2],[7,15,2],[7,16,2],[8,17,1],[8,18,0],[7,17,0],[7,18,0],[8,17,0],[7,17,2],[8,17,2],[7,17,2],[8,17,2],[8,17,0],[7,17,0],[8,17,0],[6,17,1],[6,18,0],[7,17,0],[7,18,0],[6,17,2],[7,17,2],[6,17,0],[7,17,0],[9,0,0],[9,2,0],[8,0,0],[8,2,0],[9,1,0],[8,1,0],[10,1,0],[9,1,0],[10,1,0],[9,0,0],[9,2,0],[10,0,0],[10,2,0],[9,1,0],[8,1,0],[10,1,0],[9,1,0],[10,1,0],[9,1,2],[8,1,2],[10,1,2],[9,1,2],[10,1,2],[9,3,0],[9,3,0],[9,3,0],[10,3,0],[10,3,0],[9,3,0],[10,3,0],[10,3,1],[9,3,2],[9,3,0],[9,4,0],[8,4,0],[9,3,0],[10,3,0],[10,3,0],[9,3,0],[10,3,0],[10,3,1],[10,4,0],[9,4,0],[10,3,2],[9,6,0],[8,6,0],[9,5,0],[8,5,0],[10,5,0],[9,5,0],[10,5,0],[9,6,0],[10,6,0],[9,5,0],[8,5,0],[10,5,0],[9,5,0],[10,5,0],[9,5,2],[8,5,2],[10,5,2],[9,5,2],[10,5,2],[9,7,0],[9,7,0],[9,7,0],[10,7,0],[10,7,0],[9,7,0],[10,7,0],[10,7,1],[9,7,2],[9,7,0],[9,8,0],[8,8,0],[9,7,0],[10,7,0],[10,7,0],[9,7,0],[10,7,0],[10,7,1],[10,8,0],[9,8,0],[10,7,2],[9,9,0],[9,10,0],[8,9,1],[8,9,0],[9,8,0],[9,10,0],[10,9,0],[10,8,0],[9,10,1],[10,10,0],[10,9,0],[9,9,0],[8,9,0],[9,9,0],[9,9,2],[9,10,2],[9,9,0],[9,9,0],[9,10,0],[9,9,0],[9,10,0],[10,9,0],[9,9,2],[8,9,1],[9,10,1],[10,10,0],[9,11,0],[10,11,0],[9,11,2],[9,10,2],[9,11,2],[9,10,0],[9,11,0],[9,12,1],[10,12,0],[9,11,0],[10,11,0],[9,12,0],[9,12,2],[9,11,2],[9,12,2],[9,12,0],[9,11,0],[9,12,0],[9,14,0],[9,13,0],[10,14,0],[10,13,1],[10,13,0],[9,14,0],[9,12,0],[8,13,0],[8,14,0],[9,12,1],[9,14,0],[10,13,0],[9,13,0],[10,13,0],[9,13,0],[9,13,2],[10,13,2],[9,13,0],[9,13,0],[9,12,0],[9,13,0],[10,13,0],[9,13,2],[10,13,1],[9,15,0],[9,15,0],[9,15,0],[10,15,0],[10,15,0],[9,15,0],[10,15,0],[10,15,1],[9,15,2],[9,15,0],[9,16,0],[8,16,0],[9,15,0],[10,15,0],[10,15,0],[9,15,0],[10,15,0],[10,15,1],[10,16,0],[9,16,0],[10,15,2],[9,18,0],[8,18,0],[9,17,0],[8,17,0],[10,17,0],[9,17,0],[10,17,0],[9,18,0],[10,18,0],[9,17,0],[8,17,0],[10,17,0],[9,17,0],[10,17,0],[9,17,2],[8,17,2],[10,17,2],[9,17,2],[10,17,2],[11,0,0],[11,2,0],[10,0,0],[10,2,0],[11,1,0],[10,1,0],[12,1,0],[11,1,0],[12,1,0],[11,0,0],[11,2,0],[12,0,0],[12,2,0],[11,1,0],[10,1,0],[12,1,0],[11,1,0],[12,1,0],[11,1,2],[10,1,2],[12,1,2],[11,1,2],[12,1,2],[11,4,0],[10,4,0],[11,3,0],[10,3,0],[12,3,0],[11,3,0],[12,3,0],[11,4,0],[12,4,0],[11,3,0],[10,3,0],[12,3,0],[11,3,0],[12,3,0],[11,3,2],[10,3,2],[12,3,2],[11,3,2],[12,3,2],[11,6,0],[10,6,0],[11,5,0],[10,5,0],[12,5,0],[11,5,0],[12,5,0],[11,6,0],[12,6,0],[11,5,0],[10,5,0],[12,5,0],[11,5,0],[12,5,0],[11,5,2],[10,5,2],[12,5,2],[11,5,2],[12,5,2],[11,8,0],[10,8,0],[11,7,0],[10,7,0],[12,7,0],[11,7,0],[12,7,0],[11,8,0],[12,8,0],[11,7,0],[10,7,0],[12,7,0],[11,7,0],[12,7,0],[11,7,2],[10,7,2],[12,7,2],[11,7,2],[12,7,2],[11,9,0],[11,9,0],[11,9,0],[12,9,0],[12,9,0],[11,9,0],[12,9,0],[12,9,1],[11,9,2],[11,9,0],[11,10,0],[10,10,0],[11,9,0],[12,9,0],[12,9,0],[11,9,0],[12,9,0],[12,9,1],[12,10,0],[11,10,0],[12,9,2],[11,11,0],[11,11,0],[11,11,0],[12,11,0],[12,11,0],[11,11,0],[12,11,0],[12,11,1],[11,11,2],[11,11,0],[11,12,0],[10,12,0],[11,11,0],[12,11,0],[12,11,0],[11,11,0],[12,11,0],[12,11,1],[12,12,0],[11,12,0],[12,11,2],[10,13,1],[11,13,0],[11,14,0],[11,13,2],[10,13,2],[11,13,2],[10,13,0],[11,13,0],[12,13,1],[11,13,0],[11,14,0],[12,13,0],[12,13,2],[11,13,2],[12,13,2],[12,13,0],[11,13,0],[12,13,0],[11,14,2],[12,15,0],[11,15,0],[12,14,0],[11,14,1],[11,14,0],[12,15,0],[10,15,0],[11,16,0],[12,16,0],[12,16,0],[10,15,1],[10,16,0],[12,15,0],[11,16,0],[11,15,0],[11,14,0],[11,15,0],[11,15,2],[11,15,0],[11,15,0],[10,15,0],[11,15,0],[11,16,0],[11,15,2],[11,14,1],[11,18,0],[10,18,0],[11,17,0],[10,17,0],[12,17,0],[11,17,0],[12,17,0],[11,18,0],[12,18,0],[11,17,0],[10,17,0],[12,17,0],[11,17,0],[12,17,0],[11,17,2],[10,17,2],[12,17,2],[11,17,2],[12,17,2],[13,0,0],[13,2,0],[12,0,0],[12,2,0],[13,1,0],[12,1,0],[14,1,0],[13,1,0],[14,1,0],[13,0,0],[13,2,0],[14,0,0],[14,2,0],[13,1,0],[12,1,0],[14,1,0],[13,1,0],[14,1,0],[13,1,2],[12,1,2],[14,1,2],[13,1,2],[14,1,2],[13,4,0],[12,4,0],[13,3,0],[12,3,0],[14,3,0],[13,3,0],[14,3,0],[13,4,0],[14,4,0],[13,3,0],[12,3,0],[14,3,0],[13,3,0],[14,3,0],[13,3,2],[12,3,2],[14,3,2],[13,3,2],[14,3,2],[13,6,0],[12,6,0],[13,5,0],[12,5,0],[14,5,0],[13,5,0],[14,5,0],[13,6,0],[14,6,0],[13,5,0],[12,5,0],[14,5,0],[13,5,0],[14,5,0],[13,5,2],[12,5,2],[14,5,2],[13,5,2],[14,5,2],[13,8,0],[12,8,0],[13,7,0],[12,7,0],[14,7,0],[13,7,0],[14,7,0],[13,8,0],[14,8,0],[13,7,0],[12,7,0],[14,7,0],[13,7,0],[14,7,0],[13,7,2],[12,7,2],[14,7,2],[13,7,2],[14,7,2],[13,10,0],[12,10,0],[13,9,0],[12,9,0],[14,9,0],[13,9,0],[14,9,0],[13,10,0],[14,10,0],[13,9,0],[12,9,0],[14,9,0],[13,9,0],[14,9,0],[13,9,2],[12,9,2],[14,9,2],[13,9,2],[14,9,2],[13,11,0],[13,12,0],[12,11,1],[12,11,0],[13,10,0],[13,12,0],[14,11,0],[14,10,0],[13,12,1],[14,12,0],[14,11,0],[13,11,0],[12,11,0],[13,11,0],[13,11,2],[13,12,2],[13,11,0],[13,11,0],[13,12,0],[13,11,0],[13,12,0],[14,11,0],[13,11,2],[12,11,1],[13,13,2],[13,13,0],[14,13,0],[13,13,0],[13,13,0],[13,13,2],[14,13,0],[13,14,0],[13,13,0],[13,14,0],[13,14,2],[13,13,2],[11,13,2],[14,13,2],[14,15,0],[14,14,0],[13,15,0],[13,14,0],[13,16,0],[13,15,0],[13,16,0],[14,15,0],[14,16,0],[13,15,0],[13,14,0],[13,16,0],[13,15,0],[13,16,0],[13,15,2],[13,14,2],[13,16,2],[13,15,2],[13,16,2],[14,17,1],[14,18,0],[13,17,0],[13,18,0],[14,17,0],[13,17,2],[14,17,2],[13,17,2],[14,17,2],[14,17,0],[13,17,0],[14,17,0],[12,17,1],[12,18,0],[13,17,0],[13,18,0],[12,17,2],[13,17,2],[12,17,0],[13,17,0],[15,0,0],[15,2,0],[14,0,0],[14,2,0],[15,1,0],[14,1,0],[16,1,0],[15,1,0],[16,1,0],[15,0,0],[15,2,0],[16,0,0],[16,2,0],[15,1,0],[14,1,0],[16,1,0],[15,1,0],[16,1,0],[15,1,2],[14,1,2],[16,1,2],[15,1,2],[16,1,2],[14,3,1],[15,3,0],[15,4,0],[15,3,2],[14,3,2],[15,3,2],[14,3,0],[15,3,0],[16,3,1],[15,3,0],[15,4,0],[16,3,0],[16,3,2],[15,3,2],[16,3,2],[16,3,0],[15,3,0],[16,3,0],[15,4,2],[16,5,0],[15,5,0],[16,4,0],[15,4,1],[15,4,0],[16,5,0],[14,5,0],[15,6,0],[16,6,0],[16,6,0],[14,5,1],[14,6,0],[16,5,0],[15,6,0],[15,5,0],[15,4,0],[15,5,0],[15,5,2],[15,5,0],[15,5,0],[14,5,0],[15,5,0],[15,6,0],[15,5,2],[15,4,1],[14,7,1],[15,7,0],[15,8,0],[15,7,2],[14,7,2],[15,7,2],[14,7,0],[15,7,0],[16,7,1],[15,7,0],[15,8,0],[16,7,0],[16,7,2],[15,7,2],[16,7,2],[16,7,0],[15,7,0],[16,7,0],[15,8,2],[15,8,1],[16,8,0],[15,9,0],[16,9,0],[15,9,2],[15,8,2],[15,9,2],[15,8,0],[15,9,0],[15,10,1],[16,10,0],[15,9,0],[16,9,0],[15,10,0],[15,10,2],[15,9,2],[15,10,2],[15,10,0],[15,9,0],[15,10,0],[15,12,0],[15,11,0],[15,11,0],[15,11,0],[15,10,0],[15,10,0],[15,11,0],[15,10,1],[15,11,2],[14,12,0],[15,12,0],[15,11,0],[16,11,0],[16,12,0],[15,11,0],[15,10,0],[15,10,0],[15,11,0],[15,10,1],[16,12,0],[16,10,0],[16,11,0],[16,13,0],[15,13,0],[15,14,0],[16,14,0],[15,13,0],[15,13,0],[14,13,0],[14,13,0],[15,13,0],[14,13,1],[15,13,2],[16,14,0],[14,14,0],[16,13,0],[15,13,0],[15,13,0],[14,13,0],[14,13,0],[15,13,0],[14,13,1],[16,12,0],[15,14,0],[15,15,0],[16,15,0],[16,14,0],[15,15,0],[15,15,0],[15,16,0],[15,16,0],[15,15,0],[15,16,0],[15,16,1],[15,15,2],[16,16,0],[15,15,0],[15,15,0],[15,16,0],[15,16,0],[15,15,0],[15,16,0],[15,16,1],[16,15,0],[15,16,2],[16,17,1],[16,18,0],[15,17,0],[15,18,0],[16,17,0],[15,17,2],[16,17,2],[15,17,2],[16,17,2],[16,17,0],[15,17,0],[16,17,0],[14,17,1],[14,18,0],[15,17,0],[15,18,0],[14,17,2],[15,17,2],[14,17,0],[15,17,0],[17,0,0],[17,1,0],[17,2,0],[16,0,0],[16,1,1],[16,1,0],[17,0,0],[17,2,0],[18,1,0],[18,0,0],[18,0,0],[17,2,1],[18,2,0],[17,0,0],[18,1,0],[17,1,0],[16,1,0],[17,1,0],[17,1,2],[17,2,2],[17,1,0],[17,1,0],[17,2,0],[17,1,0],[17,2,0],[18,1,0],[17,1,2],[16,1,1],[17,2,1],[18,2,0],[17,3,0],[18,3,0],[17,3,2],[17,2,2],[17,3,2],[17,2,0],[17,3,0],[17,4,1],[18,4,0],[17,3,0],[18,3,0],[17,4,0],[17,4,2],[17,3,2],[17,4,2],[17,4,0],[17,3,0],[17,4,0],[18,5,0],[18,4,0],[17,5,0],[17,4,0],[17,6,0],[17,5,0],[17,6,0],[18,5,0],[18,6,0],[17,5,0],[17,4,0],[17,6,0],[17,5,0],[17,6,0],[17,5,2],[17,4,2],[17,6,2],[17,5,2],[17,6,2],[17,6,1],[18,6,0],[17,7,0],[18,7,0],[17,7,2],[17,6,2],[17,7,2],[17,6,0],[17,7,0],[17,8,1],[18,8,0],[17,7,0],[18,7,0],[17,8,0],[17,8,2],[17,7,2],[17,8,2],[17,8,0],[17,7,0],[17,8,0],[18,9,0],[18,8,0],[17,9,0],[17,8,0],[17,10,0],[17,9,0],[17,10,0],[18,9,0],[18,10,0],[17,9,0],[17,8,0],[17,10,0],[17,9,0],[17,10,0],[17,9,2],[17,8,2],[17,10,2],[17,9,2],[17,10,2],[18,11,0],[18,10,0],[17,11,0],[17,10,0],[17,12,0],[17,11,0],[17,12,0],[18,11,0],[18,12,0],[17,11,0],[17,10,0],[17,12,0],[17,11,0],[17,12,0],[17,11,2],[17,10,2],[17,12,2],[17,11,2],[17,12,2],[18,13,0],[18,12,0],[17,13,0],[17,12,0],[17,14,0],[17,13,0],[17,14,0],[18,13,0],[18,14,0],[17,13,0],[17,12,0],[17,14,0],[17,13,0],[17,14,0],[17,13,2],[17,12,2],[17,14,2],[17,13,2],[17,14,2],[18,15,0],[18,14,0],[17,15,0],[17,14,0],[17,16,0],[17,15,0],[17,16,0],[18,15,0],[18,16,0],[17,15,0],[17,14,0],[17,16,0],[17,15,0],[17,16,0],[17,15,2],[17,14,2],[17,16,2],[17,15,2],[17,16,2],[18,17,0],[17,17,0],[18,16,0],[17,16,1],[17,16,0],[18,17,0],[16,17,0],[17,18,0],[18,18,0],[18,18,0],[16,17,1],[16,18,0],[18,17,0],[17,18,0],[17,17,0],[17,16,0],[17,17,0],[17,17,2],[17,17,0],[17,17,0],[16,17,0],[17,17,0],[17,18,0],[17,17,2],[17,16,1],[1,1,0],[1,17,0],[2,2,0],[4,2,0],[4,4,0],[4,6,0],[4,8,0],[2,8,0],[3,9,0],[4,12,0],[2,12,0],[5,3,0],[4,6,0],[6,6,0],[4,10,0],[6,10,0],[6,12,0],[6,14,0],[4,16,0],[4,14,0],[7,3,0],[7,5,0],[6,6,0],[8,6,0],[8,10,0],[8,12,0],[7,13,0],[10,2,0],[10,4,0],[10,6,0],[10,8,0],[9,9,0],[9,13,0],[10,14,0],[10,16,0],[12,8,0],[12,10,0],[12,10,0],[12,12,0],[11,15,0],[13,11,0],[15,5,0],[14,10,0],[16,10,0],[14,14,0],[14,12,0],[16,16,0],[14,16,0],[17,1,0],[17,17,0]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,18],[18,14],[14,16],[16,32],[32,227],[227,225],[225,235],[235,237],[237,414],[414,416],[416,425],[425,423],[423,608],[608,606],[606,616],[616,618],[618,801],[801,803],[803,812],[812,810],[810,993],[993,995],[995,1004],[1004,1002],[1002,1178],[1178,1180],[1180,1189],[1189,1187],[1187,1354],[1354,1356],[1356,1365],[1365,1363],[1363,1545],[1545,1548],[1548,1554],[1554,1558],[1558,1],[1,1559],[1559,1555],[1555,1557],[1557,1570],[1570,1576],[1576,1574],[1574,1583],[1583,1585],[1585,1593],[1593,1594],[1594,1601],[1601,1600],[1600,1615],[1615,1613],[1613,1622],[1622,1624],[1624,1632],[1632,1633],[1633,1640],[1640,1639],[1639,1651],[1651,1652],[1652,1659],[1659,1658],[1658,1670],[1670,1671],[1671,1678],[1678,1677],[1677,1689],[1689,1690],[1690,1697],[1697,1696],[1696,1708],[1708,1710],[1710,1716],[1716,1720],[1720,2],[2,1721],[1721,1717],[1717,1719],[1719,1730],[1730,1528],[1528,1526],[1526,1538],[1538,1540],[1540,1337],[1337,1335],[1335,1347],[1347,1349],[1349,1166],[1166,1167],[1167,1160],[1160,1159],[1159,981],[981,982],[982,975],[975,974],[974,784],[784,782],[782,794],[794,796],[796,593],[593,594],[594,587],[587,586],[586,397],[397,395],[395,407],[407,409],[409,196],[196,198],[198,204],[204,208],[208,3],[3,209],[209,205],[205,207],[207,221],[221,183],[183,185],[185,176],[176,174],[174,160],[160,162],[162,153],[153,151],[151,137],[137,139],[139,130],[130,128],[128,114],[114,116],[116,107],[107,105],[105,91],[91,93],[93,84],[84,82],[82,68],[68,70],[70,61],[61,59],[59,45],[45,47],[47,38],[38,36],[36,4],[4,7],[7,13],[13,17],[17,0],[4,20],[20,33],[33,5],[5,6],[7,21],[21,8],[8,9],[9,10],[9,11],[10,12],[11,12],[13,10],[12,14],[13,1733],[1733,14],[11,15],[15,28],[28,16],[17,1733],[1733,12],[10,1733],[1733,18],[19,20],[20,21],[21,22],[22,9],[9,26],[26,23],[23,24],[25,33],[33,26],[26,11],[11,27],[27,28],[28,29],[29,30],[31,5],[5,23],[23,29],[29,32],[5,34],[34,26],[33,34],[34,23],[23,15],[15,27],[27,14],[33,8],[8,22],[22,13],[35,39],[39,53],[53,48],[48,36],[37,40],[40,54],[54,49],[49,38],[31,39],[39,40],[40,41],[41,42],[42,43],[44,42],[42,56],[56,51],[51,45],[46,41],[41,55],[55,50],[50,47],[19,48],[48,49],[49,50],[50,51],[51,52],[25,53],[53,54],[54,55],[55,56],[56,57],[58,62],[62,76],[76,71],[71,59],[60,63],[63,77],[77,72],[72,61],[43,62],[62,63],[63,64],[64,65],[65,66],[67,65],[65,79],[79,74],[74,68],[69,64],[64,78],[78,73],[73,70],[52,71],[71,72],[72,73],[73,74],[74,75],[57,76],[76,77],[77,78],[78,79],[79,80],[81,85],[85,99],[99,94],[94,82],[83,86],[86,100],[100,95],[95,84],[66,85],[85,86],[86,87],[87,88],[88,89],[90,88],[88,102],[102,97],[97,91],[92,87],[87,101],[101,96],[96,93],[75,94],[94,95],[95,96],[96,97],[97,98],[80,99],[99,100],[100,101],[101,102],[102,103],[104,108],[108,122],[122,117],[117,105],[106,109],[109,123],[123,118],[118,107],[89,108],[108,109],[109,110],[110,111],[111,112],[113,111],[111,125],[125,120],[120,114],[115,110],[110,124],[124,119],[119,116],[98,117],[117,118],[118,119],[119,120],[120,121],[103,122],[122,123],[123,124],[124,125],[125,126],[127,131],[131,145],[145,140],[140,128],[129,132],[132,146],[146,141],[141,130],[112,131],[131,132],[132,133],[133,134],[134,135],[136,134],[134,148],[148,143],[143,137],[138,133],[133,147],[147,142],[142,139],[121,140],[140,141],[141,142],[142,143],[143,144],[126,145],[145,146],[146,147],[147,148],[148,149],[150,154],[154,168],[168,163],[163,151],[152,155],[155,169],[169,164],[164,153],[135,154],[154,155],[155,156],[156,157],[157,158],[159,157],[157,171],[171,166],[166,160],[161,156],[156,170],[170,165],[165,162],[144,163],[163,164],[164,165],[165,166],[166,167],[149,168],[168,169],[169,170],[170,171],[171,172],[173,177],[177,191],[191,186],[186,174],[175,178],[178,192],[192,187],[187,176],[158,177],[177,178],[178,179],[179,180],[180,181],[182,180],[180,194],[194,189],[189,183],[184,179],[179,193],[193,188],[188,185],[167,186],[186,187],[187,188],[188,189],[189,190],[172,191],[191,192],[192,193],[193,194],[194,195],[196,211],[211,222],[222,197],[197,181],[198,212],[212,199],[199,200],[200,201],[200,202],[201,203],[202,203],[204,201],[203,205],[204,1734],[1734,205],[202,206],[206,218],[218,207],[208,1734],[1734,203],[201,1734],[1734,209],[210,211],[211,212],[212,213],[213,200],[200,216],[216,214],[214,195],[215,222],[222,216],[216,202],[202,217],[217,218],[218,219],[219,190],[220,197],[197,214],[214,219],[219,221],[197,223],[223,216],[222,223],[223,214],[214,206],[206,217],[217,205],[222,199],[199,213],[213,204],[224,230],[230,232],[232,225],[226,231],[231,233],[233,227],[226,228],[226,6],[229,224],[226,224],[230,231],[231,24],[232,233],[233,30],[234,240],[240,243],[243,235],[236,241],[241,244],[244,237],[236,238],[236,239],[229,234],[236,234],[240,241],[241,242],[243,244],[244,245],[226,229],[229,236],[224,234],[230,240],[232,243],[246,229],[247,248],[44,248],[248,46],[248,1735],[1735,249],[250,251],[250,252],[252,253],[253,228],[46,250],[250,254],[254,255],[256,1735],[1735,251],[251,254],[37,252],[252,254],[257,258],[259,258],[258,260],[258,1736],[1736,249],[261,262],[261,263],[263,264],[264,238],[260,261],[261,265],[265,255],[266,1736],[1736,262],[262,265],[267,263],[263,265],[46,1735],[1735,1736],[1736,260],[251,249],[249,262],[35,253],[253,255],[255,264],[264,268],[249,255],[255,246],[58,269],[247,269],[269,256],[269,1737],[1737,270],[271,272],[271,273],[273,274],[274,275],[256,271],[271,276],[276,277],[60,1737],[1737,272],[272,276],[266,273],[273,276],[67,278],[279,278],[278,280],[278,1738],[1738,270],[281,282],[281,283],[283,284],[284,285],[280,281],[281,286],[286,277],[69,1738],[1738,282],[282,286],[287,283],[283,286],[256,1737],[1737,1738],[1738,280],[272,270],[270,282],[257,274],[274,277],[277,284],[284,288],[270,277],[277,289],[288,290],[291,290],[290,292],[290,1739],[1739,293],[294,295],[294,296],[296,297],[297,298],[292,294],[294,299],[299,300],[287,1739],[1739,295],[295,299],[301,296],[296,299],[279,302],[81,302],[302,83],[302,1740],[1740,293],[303,304],[303,305],[305,306],[306,307],[83,303],[303,308],[308,300],[280,1740],[1740,304],[304,308],[92,305],[305,308],[292,1739],[1739,1740],[1740,83],[295,293],[293,304],[309,297],[297,300],[300,306],[306,90],[293,300],[300,310],[311,323],[323,333],[333,312],[312,298],[313,324],[324,314],[314,315],[315,316],[315,317],[316,318],[317,318],[319,316],[318,115],[319,1741],[1741,115],[317,320],[320,330],[330,106],[321,1741],[1741,318],[316,1741],[1741,113],[322,323],[323,324],[324,325],[325,315],[315,328],[328,326],[326,310],[327,333],[333,328],[328,317],[317,329],[329,330],[330,331],[331,307],[332,312],[312,326],[326,331],[331,104],[312,334],[334,328],[333,334],[334,326],[326,320],[320,329],[329,115],[333,314],[314,325],[325,319],[311,335],[336,335],[335,337],[335,1742],[1742,338],[339,340],[339,341],[341,342],[342,343],[337,339],[339,344],[344,345],[313,1742],[1742,340],[340,344],[346,341],[341,344],[321,347],[127,347],[347,129],[347,1743],[1743,338],[348,349],[348,350],[350,351],[351,352],[129,348],[348,353],[353,345],[319,1743],[1743,349],[349,353],[138,350],[350,353],[337,1742],[1742,1743],[1743,129],[340,338],[338,349],[354,342],[342,345],[345,351],[351,136],[338,345],[345,355],[356,358],[358,370],[370,365],[365,150],[357,359],[359,371],[371,366],[366,152],[343,358],[358,359],[359,360],[360,361],[361,362],[363,361],[361,373],[373,368],[368,159],[364,360],[360,372],[372,367],[367,161],[352,365],[365,366],[366,367],[367,368],[368,369],[355,370],[370,371],[371,372],[372,373],[373,374],[375,380],[380,383],[383,184],[376,381],[381,384],[384,182],[376,377],[376,378],[379,375],[376,375],[380,381],[381,382],[383,384],[384,385],[386,389],[389,391],[391,175],[387,390],[390,392],[392,173],[387,388],[387,362],[379,386],[387,386],[389,390],[390,374],[391,392],[392,369],[376,379],[379,387],[375,386],[380,389],[383,391],[393,379],[394,400],[400,403],[403,395],[396,401],[401,404],[404,397],[396,378],[396,398],[399,394],[396,394],[400,401],[401,402],[403,404],[404,405],[406,410],[410,412],[412,407],[408,411],[411,413],[413,409],[408,385],[408,220],[399,406],[408,406],[410,411],[411,215],[412,413],[413,210],[396,399],[399,408],[394,406],[400,410],[403,412],[382,399],[414,418],[418,432],[432,427],[427,415],[416,419],[419,433],[433,428],[428,417],[245,418],[418,419],[419,420],[420,421],[421,422],[423,421],[421,435],[435,430],[430,424],[425,420],[420,434],[434,429],[429,426],[239,427],[427,428],[428,429],[429,430],[430,431],[242,432],[432,433],[433,434],[434,435],[435,436],[259,446],[446,458],[458,437],[437,438],[260,447],[447,439],[439,440],[440,441],[440,442],[441,443],[442,443],[267,441],[443,417],[267,1744],[1744,417],[442,444],[444,454],[454,426],[268,1744],[1744,443],[441,1744],[1744,415],[445,446],[446,447],[447,448],[448,440],[440,452],[452,449],[449,450],[451,458],[458,452],[452,442],[442,453],[453,454],[454,455],[455,456],[457,437],[437,449],[449,455],[455,424],[437,459],[459,452],[458,459],[459,449],[449,444],[444,453],[453,417],[458,439],[439,448],[448,267],[460,465],[465,467],[467,461],[462,466],[466,468],[468,463],[462,275],[462,445],[464,460],[462,460],[465,466],[466,451],[467,468],[468,457],[469,474],[474,477],[477,470],[471,475],[475,478],[478,472],[471,285],[471,473],[464,469],[471,469],[474,475],[475,476],[477,478],[478,479],[462,464],[464,471],[460,469],[465,474],[467,477],[289,464],[480,481],[309,481],[481,301],[481,1745],[1745,482],[483,484],[483,485],[485,486],[486,473],[301,483],[483,487],[487,488],[489,1745],[1745,484],[484,487],[292,485],[485,487],[490,491],[492,491],[491,493],[491,1746],[1746,482],[494,495],[494,496],[496,497],[497,479],[493,494],[494,498],[498,488],[499,1746],[1746,495],[495,498],[500,496],[496,498],[301,1745],[1745,1746],[1746,493],[484,482],[482,495],[291,486],[486,488],[488,497],[497,501],[482,488],[488,476],[502,506],[506,508],[508,489],[503,507],[507,509],[509,480],[503,504],[503,322],[505,502],[503,502],[506,507],[507,327],[508,509],[509,332],[510,514],[514,517],[517,499],[511,515],[515,518],[518,490],[511,512],[511,513],[505,510],[511,510],[514,515],[515,516],[517,518],[518,519],[503,505],[505,511],[502,510],[506,514],[508,517],[520,505],[521,522],[354,522],[522,346],[522,1747],[1747,523],[524,525],[524,526],[526,527],[527,504],[346,524],[524,528],[528,529],[530,1747],[1747,525],[525,528],[337,526],[526,528],[531,532],[533,532],[532,534],[532,1748],[1748,523],[535,536],[535,537],[537,538],[538,512],[534,535],[535,539],[539,529],[540,1748],[1748,536],[536,539],[541,537],[537,539],[346,1747],[1747,1748],[1748,534],[525,523],[523,536],[336,527],[527,529],[529,538],[538,542],[523,529],[529,520],[356,543],[521,543],[543,530],[543,1749],[1749,544],[545,546],[545,547],[547,548],[548,549],[530,545],[545,550],[550,551],[357,1749],[1749,546],[546,550],[540,547],[547,550],[363,552],[553,552],[552,554],[552,1750],[1750,544],[555,556],[555,557],[557,558],[558,559],[554,555],[555,560],[560,551],[364,1750],[1750,556],[556,560],[561,557],[557,560],[530,1749],[1749,1750],[1750,554],[546,544],[544,556],[531,548],[548,551],[551,558],[558,562],[544,551],[551,563],[564,565],[566,565],[565,567],[565,1751],[1751,568],[569,570],[569,571],[571,572],[572,377],[567,569],[569,573],[573,574],[575,1751],[1751,570],[570,573],[576,571],[571,573],[577,578],[562,578],[578,561],[578,1752],[1752,568],[579,580],[579,581],[581,582],[582,388],[561,579],[579,583],[583,574],[584,1752],[1752,580],[580,583],[554,581],[581,583],[567,1751],[1751,1752],[1752,561],[570,568],[568,580],[585,572],[572,574],[574,582],[582,553],[568,574],[574,393],[585,588],[588,600],[600,595],[595,586],[576,589],[589,601],[601,596],[596,587],[398,588],[588,589],[589,590],[590,591],[591,592],[566,591],[591,603],[603,598],[598,593],[567,590],[590,602],[602,597],[597,594],[405,595],[595,596],[596,597],[597,598],[598,599],[402,600],[600,601],[601,602],[602,603],[603,604],[605,611],[611,613],[613,606],[607,612],[612,614],[614,608],[607,609],[607,431],[610,605],[607,605],[611,612],[612,436],[613,614],[614,422],[615,621],[621,624],[624,616],[617,622],[622,625],[625,618],[617,619],[617,620],[610,615],[617,615],[621,622],[622,623],[624,625],[625,626],[607,610],[610,617],[605,615],[611,621],[613,624],[627,610],[628,642],[642,651],[651,629],[629,456],[630,643],[643,631],[631,632],[632,633],[632,634],[633,635],[634,635],[636,633],[635,637],[636,1753],[1753,637],[634,638],[638,648],[648,639],[640,1753],[1753,635],[633,1753],[1753,641],[619,642],[642,643],[643,644],[644,632],[632,646],[646,645],[645,450],[627,651],[651,646],[646,634],[634,647],[647,648],[648,649],[649,438],[609,629],[629,645],[645,649],[649,650],[629,652],[652,646],[651,652],[652,645],[645,638],[638,647],[647,637],[651,631],[631,644],[644,636],[472,662],[662,674],[674,653],[653,654],[470,663],[663,655],[655,656],[656,657],[656,658],[657,659],[658,659],[461,657],[659,639],[461,1754],[1754,639],[658,660],[660,670],[670,637],[463,1754],[1754,659],[657,1754],[1754,650],[661,662],[662,663],[663,664],[664,656],[656,668],[668,665],[665,666],[667,674],[674,668],[668,658],[658,669],[669,670],[670,671],[671,672],[673,653],[653,665],[665,671],[671,641],[653,675],[675,668],[674,675],[675,665],[665,660],[660,669],[669,639],[674,655],[655,664],[664,461],[676,677],[492,677],[677,493],[677,1755],[1755,678],[679,680],[679,681],[681,682],[682,661],[493,679],[679,683],[683,684],[685,1755],[1755,680],[680,683],[500,681],[681,683],[686,687],[688,687],[687,689],[687,1756],[1756,678],[690,691],[690,692],[692,693],[693,673],[689,690],[690,694],[694,684],[695,1756],[1756,691],[691,694],[696,692],[692,694],[493,1755],[1755,1756],[1756,689],[680,678],[678,691],[501,682],[682,684],[684,693],[693,697],[678,684],[684,667],[676,700],[700,712],[712,707],[707,698],[685,701],[701,713],[713,708],[708,699],[519,700],[700,701],[701,702],[702,703],[703,704],[686,703],[703,715],[715,710],[710,705],[695,702],[702,714],[714,709],[709,706],[513,707],[707,708],[708,709],[709,710],[710,711],[516,712],[712,713],[713,714],[714,715],[715,716],[542,717],[698,717],[717,699],[717,1757],[1757,718],[719,720],[719,721],[721,722],[722,723],[699,719],[719,724],[724,725],[541,1757],[1757,720],[720,724],[706,721],[721,724],[533,726],[727,726],[726,728],[726,1758],[1758,718],[729,730],[729,731],[731,732],[732,733],[728,729],[729,734],[734,725],[534,1758],[1758,730],[730,734],[735,731],[731,734],[699,1757],[1757,1758],[1758,728],[720,718],[718,730],[705,722],[722,725],[725,732],[732,736],[718,725],[725,737],[727,749],[749,760],[760,738],[738,739],[728,750],[750,740],[740,741],[741,742],[741,743],[742,744],[743,744],[735,742],[744,745],[735,1759],[1759,745],[743,746],[746,756],[756,747],[736,1759],[1759,744],[742,1759],[1759,748],[549,749],[749,750],[750,751],[751,741],[741,754],[754,752],[752,753],[563,760],[760,754],[754,743],[743,755],[755,756],[756,757],[757,758],[559,738],[738,752],[752,757],[757,759],[738,761],[761,754],[760,761],[761,752],[752,746],[746,755],[755,745],[760,740],[740,751],[751,735],[762,764],[764,776],[776,771],[771,577],[763,765],[765,777],[777,772],[772,584],[758,764],[764,765],[765,766],[766,767],[767,768],[769,767],[767,779],[779,774],[774,564],[770,766],[766,778],[778,773],[773,575],[739,771],[771,772],[772,773],[773,774],[774,775],[753,776],[776,777],[777,778],[778,779],[779,780],[781,787],[787,790],[790,782],[783,788],[788,791],[791,784],[783,768],[783,785],[786,781],[783,781],[787,788],[788,789],[790,791],[791,792],[793,797],[797,799],[799,794],[795,798],[798,800],[800,796],[795,775],[795,592],[786,793],[795,793],[797,798],[798,604],[799,800],[800,599],[783,786],[786,795],[781,793],[787,797],[790,799],[780,786],[801,805],[805,819],[819,814],[814,802],[803,806],[806,820],[820,815],[815,804],[626,805],[805,806],[806,807],[807,808],[808,809],[810,808],[808,822],[822,817],[817,811],[812,807],[807,821],[821,816],[816,813],[620,814],[814,815],[815,816],[816,817],[817,818],[623,819],[819,820],[820,821],[821,822],[822,823],[628,824],[802,824],[824,804],[824,1760],[1760,825],[826,827],[826,828],[828,829],[829,830],[804,826],[826,831],[831,832],[630,1760],[1760,827],[827,831],[813,828],[828,831],[640,833],[834,833],[833,835],[833,1761],[1761,825],[836,837],[836,838],[838,839],[839,840],[835,836],[836,841],[841,832],[636,1761],[1761,837],[837,841],[842,838],[838,841],[804,1760],[1760,1761],[1761,835],[827,825],[825,837],[811,829],[829,832],[832,839],[839,843],[825,832],[832,844],[834,847],[847,859],[859,854],[854,845],[835,848],[848,860],[860,855],[855,846],[672,847],[847,848],[848,849],[849,850],[850,851],[843,850],[850,862],[862,857],[857,852],[842,849],[849,861],[861,856],[856,853],[654,854],[854,855],[855,856],[856,857],[857,858],[666,859],[859,860],[860,861],[861,862],[862,863],[697,864],[845,864],[864,846],[864,1762],[1762,865],[866,867],[866,868],[868,869],[869,870],[846,866],[866,871],[871,872],[696,1762],[1762,867],[867,871],[853,868],[868,871],[688,873],[874,873],[873,875],[873,1763],[1763,865],[876,877],[876,878],[878,879],[879,880],[875,876],[876,881],[881,872],[689,1763],[1763,877],[877,881],[882,878],[878,881],[846,1762],[1762,1763],[1763,875],[867,865],[865,877],[852,869],[869,872],[872,879],[879,883],[865,872],[872,884],[874,896],[896,907],[907,885],[885,886],[875,897],[897,887],[887,888],[888,889],[888,890],[889,891],[890,891],[882,889],[891,892],[882,1764],[1764,892],[890,893],[893,903],[903,894],[883,1764],[1764,891],[889,1764],[1764,895],[704,896],[896,897],[897,898],[898,888],[888,901],[901,899],[899,900],[716,907],[907,901],[901,890],[890,902],[902,903],[903,904],[904,905],[711,885],[885,899],[899,904],[904,906],[885,908],[908,901],[907,908],[908,899],[899,893],[893,902],[902,892],[907,887],[887,898],[898,882],[909,914],[914,916],[916,910],[911,915],[915,917],[917,912],[911,723],[911,886],[913,909],[911,909],[914,915],[915,900],[916,917],[917,905],[918,923],[923,926],[926,919],[920,924],[924,927],[927,921],[920,733],[920,922],[913,918],[920,918],[923,924],[924,925],[926,927],[927,928],[911,913],[913,920],[909,918],[914,923],[916,926],[737,913],[929,941],[941,951],[951,930],[930,928],[931,942],[942,932],[932,933],[933,934],[933,935],[934,936],[935,936],[937,934],[936,747],[937,1765],[1765,747],[935,938],[938,948],[948,745],[939,1765],[1765,936],[934,1765],[1765,759],[940,941],[941,942],[942,943],[943,933],[933,946],[946,944],[944,925],[945,951],[951,946],[946,935],[935,947],[947,948],[948,949],[949,922],[950,930],[930,944],[944,949],[949,748],[930,952],[952,946],[951,952],[952,944],[944,938],[938,947],[947,747],[951,932],[932,943],[943,937],[762,953],[939,953],[953,937],[953,1766],[1766,954],[955,956],[955,957],[957,958],[958,959],[937,955],[955,960],[960,961],[763,1766],[1766,956],[956,960],[931,957],[957,960],[769,962],[963,962],[962,964],[962,1767],[1767,954],[965,966],[965,967],[967,968],[968,969],[964,965],[965,970],[970,961],[770,1767],[1767,966],[966,970],[971,967],[967,970],[937,1766],[1766,1767],[1767,964],[956,954],[954,966],[929,958],[958,961],[961,968],[968,972],[954,961],[961,973],[963,976],[976,988],[988,983],[983,974],[964,977],[977,989],[989,984],[984,975],[785,976],[976,977],[977,978],[978,979],[979,980],[972,979],[979,991],[991,986],[986,981],[971,978],[978,990],[990,985],[985,982],[792,983],[983,984],[984,985],[985,986],[986,987],[789,988],[988,989],[989,990],[990,991],[991,992],[993,997],[997,1011],[1011,1006],[1006,994],[995,998],[998,1012],[1012,1007],[1007,996],[809,997],[997,998],[998,999],[999,1000],[1000,1001],[1002,1000],[1000,1014],[1014,1009],[1009,1003],[1004,999],[999,1013],[1013,1008],[1008,1005],[818,1006],[1006,1007],[1007,1008],[1008,1009],[1009,1010],[823,1011],[1011,1012],[1012,1013],[1013,1014],[1014,1015],[994,1018],[1018,1030],[1030,1025],[1025,1016],[996,1019],[1019,1031],[1031,1026],[1026,1017],[830,1018],[1018,1019],[1019,1020],[1020,1021],[1021,1022],[1003,1021],[1021,1033],[1033,1028],[1028,1023],[1005,1020],[1020,1032],[1032,1027],[1027,1024],[840,1025],[1025,1026],[1026,1027],[1027,1028],[1028,1029],[844,1030],[1030,1031],[1031,1032],[1032,1033],[1033,1034],[1016,1037],[1037,1049],[1049,1044],[1044,1035],[1017,1038],[1038,1050],[1050,1045],[1045,1036],[851,1037],[1037,1038],[1038,1039],[1039,1040],[1040,1041],[1023,1040],[1040,1052],[1052,1047],[1047,1042],[1024,1039],[1039,1051],[1051,1046],[1046,1043],[858,1044],[1044,1045],[1045,1046],[1046,1047],[1047,1048],[863,1049],[1049,1050],[1050,1051],[1051,1052],[1052,1053],[1035,1056],[1056,1068],[1068,1063],[1063,1054],[1036,1057],[1057,1069],[1069,1064],[1064,1055],[870,1056],[1056,1057],[1057,1058],[1058,1059],[1059,1060],[1042,1059],[1059,1071],[1071,1066],[1066,1061],[1043,1058],[1058,1070],[1070,1065],[1065,1062],[880,1063],[1063,1064],[1064,1065],[1065,1066],[1066,1067],[884,1068],[1068,1069],[1069,1070],[1070,1071],[1071,1072],[895,1073],[1054,1073],[1073,1055],[1073,1768],[1768,1074],[1075,1076],[1075,1077],[1077,1078],[1078,1079],[1055,1075],[1075,1080],[1080,1081],[892,1768],[1768,1076],[1076,1080],[1062,1077],[1077,1080],[906,1082],[1083,1082],[1082,1084],[1082,1769],[1769,1074],[1085,1086],[1085,1087],[1087,1088],[1088,1089],[1084,1085],[1085,1090],[1090,1081],[894,1769],[1769,1086],[1086,1090],[1091,1087],[1087,1090],[1055,1768],[1768,1769],[1769,1084],[1076,1074],[1074,1086],[1061,1078],[1078,1081],[1081,1088],[1088,1092],[1074,1081],[1081,1093],[912,1094],[1083,1094],[1094,1084],[1094,1770],[1770,1095],[1096,1097],[1096,1098],[1098,1099],[1099,1100],[1084,1096],[1096,1101],[1101,1102],[910,1770],[1770,1097],[1097,1101],[1091,1098],[1098,1101],[921,1103],[1104,1103],[1103,1105],[1103,1771],[1771,1095],[1106,1107],[1106,1108],[1108,1109],[1109,1110],[1105,1106],[1106,1111],[1111,1102],[919,1771],[1771,1107],[1107,1111],[1112,1108],[1108,1111],[1084,1770],[1770,1771],[1771,1105],[1097,1095],[1095,1107],[1092,1099],[1099,1102],[1102,1109],[1109,1113],[1095,1102],[1102,1114],[1115,1119],[1119,1121],[1121,1105],[1116,1120],[1120,1122],[1122,1104],[1116,1117],[1116,940],[1118,1115],[1116,1115],[1119,1120],[1120,945],[1121,1122],[1122,950],[1123,1127],[1127,1130],[1130,1112],[1124,1128],[1128,1131],[1131,1113],[1124,1125],[1124,1126],[1118,1123],[1124,1123],[1127,1128],[1128,1129],[1130,1131],[1131,1132],[1116,1118],[1118,1124],[1115,1123],[1119,1127],[1121,1130],[1133,1118],[1134,1148],[1148,1157],[1157,1135],[1135,959],[1136,1149],[1149,1137],[1137,1138],[1138,1139],[1138,1140],[1139,1141],[1140,1141],[1142,1139],[1141,1143],[1142,1772],[1772,1143],[1140,1144],[1144,1154],[1154,1145],[1146,1772],[1772,1141],[1139,1772],[1772,1147],[1125,1148],[1148,1149],[1149,1150],[1150,1138],[1138,1152],[1152,1151],[1151,973],[1133,1157],[1157,1152],[1152,1140],[1140,1153],[1153,1154],[1154,1155],[1155,969],[1117,1135],[1135,1151],[1151,1155],[1155,1156],[1135,1158],[1158,1152],[1157,1158],[1158,1151],[1151,1144],[1144,1153],[1153,1143],[1157,1137],[1137,1150],[1150,1142],[1156,1161],[1161,1173],[1173,1168],[1168,1159],[1145,1162],[1162,1174],[1174,1169],[1169,1160],[980,1161],[1161,1162],[1162,1163],[1163,1164],[1164,1165],[1147,1164],[1164,1176],[1176,1171],[1171,1166],[1143,1163],[1163,1175],[1175,1170],[1170,1167],[987,1168],[1168,1169],[1169,1170],[1170,1171],[1171,1172],[992,1173],[1173,1174],[1174,1175],[1175,1176],[1176,1177],[1178,1182],[1182,1196],[1196,1191],[1191,1179],[1180,1183],[1183,1197],[1197,1192],[1192,1181],[1001,1182],[1182,1183],[1183,1184],[1184,1185],[1185,1186],[1187,1185],[1185,1199],[1199,1194],[1194,1188],[1189,1184],[1184,1198],[1198,1193],[1193,1190],[1010,1191],[1191,1192],[1192,1193],[1193,1194],[1194,1195],[1015,1196],[1196,1197],[1197,1198],[1198,1199],[1199,1200],[1179,1203],[1203,1215],[1215,1210],[1210,1201],[1181,1204],[1204,1216],[1216,1211],[1211,1202],[1022,1203],[1203,1204],[1204,1205],[1205,1206],[1206,1207],[1188,1206],[1206,1218],[1218,1213],[1213,1208],[1190,1205],[1205,1217],[1217,1212],[1212,1209],[1029,1210],[1210,1211],[1211,1212],[1212,1213],[1213,1214],[1034,1215],[1215,1216],[1216,1217],[1217,1218],[1218,1219],[1201,1222],[1222,1234],[1234,1229],[1229,1220],[1202,1223],[1223,1235],[1235,1230],[1230,1221],[1041,1222],[1222,1223],[1223,1224],[1224,1225],[1225,1226],[1208,1225],[1225,1237],[1237,1232],[1232,1227],[1209,1224],[1224,1236],[1236,1231],[1231,1228],[1048,1229],[1229,1230],[1230,1231],[1231,1232],[1232,1233],[1053,1234],[1234,1235],[1235,1236],[1236,1237],[1237,1238],[1220,1241],[1241,1253],[1253,1248],[1248,1239],[1221,1242],[1242,1254],[1254,1249],[1249,1240],[1060,1241],[1241,1242],[1242,1243],[1243,1244],[1244,1245],[1227,1244],[1244,1256],[1256,1251],[1251,1246],[1228,1243],[1243,1255],[1255,1250],[1250,1247],[1067,1248],[1248,1249],[1249,1250],[1250,1251],[1251,1252],[1072,1253],[1253,1254],[1254,1255],[1255,1256],[1256,1257],[1239,1260],[1260,1272],[1272,1267],[1267,1258],[1240,1261],[1261,1273],[1273,1268],[1268,1259],[1079,1260],[1260,1261],[1261,1262],[1262,1263],[1263,1264],[1246,1263],[1263,1275],[1275,1270],[1270,1265],[1247,1262],[1262,1274],[1274,1269],[1269,1266],[1089,1267],[1267,1268],[1268,1269],[1269,1270],[1270,1271],[1093,1272],[1272,1273],[1273,1274],[1274,1275],[1275,1276],[1258,1288],[1288,1299],[1299,1277],[1277,1278],[1259,1289],[1289,1279],[1279,1280],[1280,1281],[1280,1282],[1281,1283],[1282,1283],[1266,1281],[1283,1284],[1266,1773],[1773,1284],[1282,1285],[1285,1295],[1295,1286],[1265,1773],[1773,1283],[1281,1773],[1773,1287],[1100,1288],[1288,1289],[1289,1290],[1290,1280],[1280,1293],[1293,1291],[1291,1292],[1114,1299],[1299,1293],[1293,1282],[1282,1294],[1294,1295],[1295,1296],[1296,1297],[1110,1277],[1277,1291],[1291,1296],[1296,1298],[1277,1300],[1300,1293],[1299,1300],[1300,1291],[1291,1285],[1285,1294],[1294,1284],[1299,1279],[1279,1290],[1290,1266],[1132,1304],[1304,1301],[1301,1302],[1302,1303],[1278,1304],[1304,1313],[1313,1305],[1297,1302],[1126,1309],[1309,1306],[1306,1305],[1305,1307],[1308,1309],[1309,1313],[1313,1302],[1310,1305],[1304,1312],[1312,1309],[1292,1301],[1301,1313],[1313,1306],[1306,1311],[1129,1312],[1312,1313],[1313,1314],[1315,1317],[1317,1329],[1329,1324],[1324,1134],[1316,1318],[1318,1330],[1330,1325],[1325,1136],[1310,1317],[1317,1318],[1318,1319],[1319,1320],[1320,1321],[1322,1320],[1320,1332],[1332,1327],[1327,1146],[1323,1319],[1319,1331],[1331,1326],[1326,1142],[1308,1324],[1324,1325],[1325,1326],[1326,1327],[1327,1328],[1311,1329],[1329,1330],[1330,1331],[1331,1332],[1332,1333],[1334,1340],[1340,1343],[1343,1335],[1336,1341],[1341,1344],[1344,1337],[1336,1321],[1336,1338],[1339,1334],[1336,1334],[1340,1341],[1341,1342],[1343,1344],[1344,1345],[1346,1350],[1350,1352],[1352,1347],[1348,1351],[1351,1353],[1353,1349],[1348,1328],[1348,1165],[1339,1346],[1348,1346],[1350,1351],[1351,1177],[1352,1353],[1353,1172],[1336,1339],[1339,1348],[1334,1346],[1340,1350],[1343,1352],[1333,1339],[1354,1358],[1358,1372],[1372,1367],[1367,1355],[1356,1359],[1359,1373],[1373,1368],[1368,1357],[1186,1358],[1358,1359],[1359,1360],[1360,1361],[1361,1362],[1363,1361],[1361,1375],[1375,1370],[1370,1364],[1365,1360],[1360,1374],[1374,1369],[1369,1366],[1195,1367],[1367,1368],[1368,1369],[1369,1370],[1370,1371],[1200,1372],[1372,1373],[1373,1374],[1374,1375],[1375,1376],[1377,1381],[1381,1383],[1383,1357],[1378,1382],[1382,1384],[1384,1355],[1378,1379],[1378,1214],[1380,1377],[1378,1377],[1381,1382],[1382,1219],[1383,1384],[1384,1207],[1385,1389],[1389,1392],[1392,1366],[1386,1390],[1390,1393],[1393,1364],[1386,1387],[1386,1388],[1380,1385],[1386,1385],[1389,1390],[1390,1391],[1392,1393],[1393,1394],[1378,1380],[1380,1386],[1377,1385],[1381,1389],[1383,1392],[1395,1380],[1396,1410],[1410,1419],[1419,1397],[1397,1226],[1398,1411],[1411,1399],[1399,1400],[1400,1401],[1400,1402],[1401,1403],[1402,1403],[1404,1401],[1403,1405],[1404,1774],[1774,1405],[1402,1406],[1406,1416],[1416,1407],[1408,1774],[1774,1403],[1401,1774],[1774,1409],[1387,1410],[1410,1411],[1411,1412],[1412,1400],[1400,1414],[1414,1413],[1413,1238],[1395,1419],[1419,1414],[1414,1402],[1402,1415],[1415,1416],[1416,1417],[1417,1233],[1379,1397],[1397,1413],[1413,1417],[1417,1418],[1397,1420],[1420,1414],[1419,1420],[1420,1413],[1413,1406],[1406,1415],[1415,1405],[1419,1399],[1399,1412],[1412,1404],[1421,1425],[1425,1427],[1427,1407],[1422,1426],[1426,1428],[1428,1418],[1422,1423],[1422,1252],[1424,1421],[1422,1421],[1425,1426],[1426,1257],[1427,1428],[1428,1245],[1429,1433],[1433,1436],[1436,1405],[1430,1434],[1434,1437],[1437,1409],[1430,1431],[1430,1432],[1424,1429],[1430,1429],[1433,1434],[1434,1435],[1436,1437],[1437,1438],[1422,1424],[1424,1430],[1421,1429],[1425,1433],[1427,1436],[1439,1424],[1440,1445],[1445,1447],[1447,1441],[1442,1446],[1446,1448],[1448,1443],[1442,1264],[1442,1423],[1444,1440],[1442,1440],[1445,1446],[1446,1439],[1447,1448],[1448,1431],[1449,1454],[1454,1457],[1457,1450],[1451,1455],[1455,1458],[1458,1452],[1451,1271],[1451,1453],[1444,1449],[1451,1449],[1454,1455],[1455,1456],[1457,1458],[1458,1459],[1442,1444],[1444,1451],[1440,1449],[1445,1454],[1447,1457],[1276,1444],[1460,1461],[1298,1461],[1461,1286],[1461,1775],[1775,1462],[1463,1464],[1463,1465],[1465,1466],[1466,1453],[1286,1463],[1463,1467],[1467,1468],[1469,1775],[1775,1464],[1464,1467],[1284,1465],[1465,1467],[1470,1471],[1472,1471],[1471,1473],[1471,1776],[1776,1462],[1474,1475],[1474,1476],[1476,1477],[1477,1459],[1473,1474],[1474,1478],[1478,1468],[1479,1776],[1776,1475],[1475,1478],[1480,1476],[1476,1478],[1286,1775],[1775,1776],[1776,1473],[1464,1462],[1462,1475],[1287,1466],[1466,1468],[1468,1477],[1477,1481],[1462,1468],[1468,1456],[1482,1483],[1484,1483],[1483,1485],[1483,1777],[1777,1486],[1487,1488],[1487,1489],[1489,1490],[1490,1307],[1485,1487],[1487,1491],[1491,1492],[1493,1777],[1777,1488],[1488,1491],[1494,1489],[1489,1491],[1495,1496],[1470,1496],[1496,1479],[1496,1778],[1778,1486],[1497,1498],[1497,1499],[1499,1500],[1500,1303],[1479,1497],[1497,1501],[1501,1492],[1502,1778],[1778,1498],[1498,1501],[1469,1499],[1499,1501],[1485,1777],[1777,1778],[1778,1479],[1488,1486],[1486,1498],[1503,1490],[1490,1492],[1492,1500],[1500,1460],[1486,1492],[1492,1314],[1484,1504],[1505,1504],[1504,1506],[1504,1779],[1779,1507],[1508,1509],[1508,1510],[1510,1511],[1511,1512],[1506,1508],[1508,1513],[1513,1514],[1485,1779],[1779,1509],[1509,1513],[1515,1510],[1510,1513],[1503,1516],[1315,1516],[1516,1316],[1516,1780],[1780,1507],[1517,1518],[1517,1519],[1519,1520],[1520,1521],[1316,1517],[1517,1522],[1522,1514],[1494,1780],[1780,1518],[1518,1522],[1323,1519],[1519,1522],[1506,1779],[1779,1780],[1780,1316],[1509,1507],[1507,1518],[1523,1511],[1511,1514],[1514,1520],[1520,1322],[1507,1514],[1514,1524],[1525,1531],[1531,1534],[1534,1526],[1527,1532],[1532,1535],[1535,1528],[1527,1512],[1527,1529],[1530,1525],[1527,1525],[1531,1532],[1532,1533],[1534,1535],[1535,1536],[1537,1541],[1541,1543],[1543,1538],[1539,1542],[1542,1544],[1544,1540],[1539,1521],[1539,1338],[1530,1537],[1539,1537],[1541,1542],[1542,1342],[1543,1544],[1544,1345],[1527,1530],[1530,1539],[1525,1537],[1531,1541],[1534,1543],[1524,1530],[1545,1560],[1560,1571],[1571,1546],[1546,1547],[1548,1561],[1561,1549],[1549,1550],[1550,1551],[1550,1552],[1551,1553],[1552,1553],[1554,1551],[1553,1555],[1554,1781],[1781,1555],[1552,1556],[1556,1567],[1567,1557],[1558,1781],[1781,1553],[1551,1781],[1781,1559],[1362,1560],[1560,1561],[1561,1562],[1562,1550],[1550,1565],[1565,1563],[1563,1564],[1376,1571],[1571,1565],[1565,1552],[1552,1566],[1566,1567],[1567,1568],[1568,1569],[1371,1546],[1546,1563],[1563,1568],[1568,1570],[1546,1572],[1572,1565],[1571,1572],[1572,1563],[1563,1556],[1556,1566],[1566,1555],[1571,1549],[1549,1562],[1562,1554],[1573,1578],[1578,1580],[1580,1574],[1575,1579],[1579,1581],[1581,1576],[1575,1394],[1575,1547],[1577,1573],[1575,1573],[1578,1579],[1579,1564],[1580,1581],[1581,1569],[1582,1587],[1587,1590],[1590,1583],[1584,1588],[1588,1591],[1591,1585],[1584,1388],[1584,1586],[1577,1582],[1584,1582],[1587,1588],[1588,1589],[1590,1591],[1591,1592],[1575,1577],[1577,1584],[1573,1582],[1578,1587],[1580,1590],[1391,1577],[1593,1595],[1595,1607],[1607,1602],[1602,1396],[1594,1596],[1596,1608],[1608,1603],[1603,1398],[1592,1595],[1595,1596],[1596,1597],[1597,1598],[1598,1599],[1600,1598],[1598,1610],[1610,1605],[1605,1408],[1601,1597],[1597,1609],[1609,1604],[1604,1404],[1586,1602],[1602,1603],[1603,1604],[1604,1605],[1605,1606],[1589,1607],[1607,1608],[1608,1609],[1609,1610],[1610,1611],[1612,1617],[1617,1619],[1619,1613],[1614,1618],[1618,1620],[1620,1615],[1614,1438],[1614,1606],[1616,1612],[1614,1612],[1617,1618],[1618,1611],[1619,1620],[1620,1599],[1621,1626],[1626,1629],[1629,1622],[1623,1627],[1627,1630],[1630,1624],[1623,1432],[1623,1625],[1616,1621],[1623,1621],[1626,1627],[1627,1628],[1629,1630],[1630,1631],[1614,1616],[1616,1623],[1612,1621],[1617,1626],[1619,1629],[1435,1616],[1632,1634],[1634,1646],[1646,1641],[1641,1443],[1633,1635],[1635,1647],[1647,1642],[1642,1441],[1631,1634],[1634,1635],[1635,1636],[1636,1637],[1637,1638],[1639,1637],[1637,1649],[1649,1644],[1644,1452],[1640,1636],[1636,1648],[1648,1643],[1643,1450],[1625,1641],[1641,1642],[1642,1643],[1643,1644],[1644,1645],[1628,1646],[1646,1647],[1647,1648],[1648,1649],[1649,1650],[1651,1653],[1653,1665],[1665,1660],[1660,1481],[1652,1654],[1654,1666],[1666,1661],[1661,1480],[1638,1653],[1653,1654],[1654,1655],[1655,1656],[1656,1657],[1658,1656],[1656,1668],[1668,1663],[1663,1472],[1659,1655],[1655,1667],[1667,1662],[1662,1473],[1645,1660],[1660,1661],[1661,1662],[1662,1663],[1663,1664],[1650,1665],[1665,1666],[1666,1667],[1667,1668],[1668,1669],[1670,1672],[1672,1684],[1684,1679],[1679,1495],[1671,1673],[1673,1685],[1685,1680],[1680,1502],[1657,1672],[1672,1673],[1673,1674],[1674,1675],[1675,1676],[1677,1675],[1675,1687],[1687,1682],[1682,1482],[1678,1674],[1674,1686],[1686,1681],[1681,1493],[1664,1679],[1679,1680],[1680,1681],[1681,1682],[1682,1683],[1669,1684],[1684,1685],[1685,1686],[1686,1687],[1687,1688],[1689,1691],[1691,1703],[1703,1698],[1698,1505],[1690,1692],[1692,1704],[1704,1699],[1699,1506],[1676,1691],[1691,1692],[1692,1693],[1693,1694],[1694,1695],[1696,1694],[1694,1706],[1706,1701],[1701,1523],[1697,1693],[1693,1705],[1705,1700],[1700,1515],[1683,1698],[1698,1699],[1699,1700],[1700,1701],[1701,1702],[1688,1703],[1703,1704],[1704,1705],[1705,1706],[1706,1707],[1708,1722],[1722,1731],[1731,1709],[1709,1529],[1710,1723],[1723,1711],[1711,1712],[1712,1713],[1712,1714],[1713,1715],[1714,1715],[1716,1713],[1715,1717],[1716,1782],[1782,1717],[1714,1718],[1718,1728],[1728,1719],[1720,1782],[1782,1715],[1713,1782],[1782,1721],[1695,1722],[1722,1723],[1723,1724],[1724,1712],[1712,1726],[1726,1725],[1725,1533],[1707,1731],[1731,1726],[1726,1714],[1714,1727],[1727,1728],[1728,1729],[1729,1536],[1702,1709],[1709,1725],[1725,1729],[1729,1730],[1709,1732],[1732,1726],[1731,1732],[1732,1725],[1725,1718],[1718,1727],[1727,1717],[1731,1711],[1711,1724],[1724,1716]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,90,90,-90,90,90,180,180,90,90,90,-90,90,90,180,180,90,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,180,180,180,-180,-180,-180,90,90,-180,180,180,-180,-90,90,90,90,180,-180,90,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,-180,-180,-180,180,180,180,180,90,-90,90,-90,90,-180,180,-180,180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180\n\t],\n\t\"faces_vertices\": [\n\t\t[0,18,1733,17],[1,1559,1781,1558],[2,1721,1782,1720],[3,209,1734,208],[4,20,19,48,36],[4,7,21,20],[5,6,226,228,253,35,39,31],[5,31,39,53,25,33],[5,33,34],[5,34,23],[5,23,24,231,226,6],[7,13,22,21],[8,33,20,21],[8,21,22],[8,22,9],[8,9,26,33],[9,22,13,10],[9,10,12,11],[9,11,26],[10,13,1733],[10,1733,12],[11,15,23,26],[11,12,14,27],[11,27,15],[12,1733,14],[13,17,1733],[14,16,28,27],[14,1733,18],[15,27,28],[15,28,29,23],[16,32,29,28],[19,20,33,25,53,48],[23,34,26],[23,29,30,233,231,24],[26,34,33],[29,32,227,233,30],[35,253,252,37,40,39],[36,48,49,38],[37,252,250,46,41,40],[38,49,50,47],[39,40,54,53],[40,41,55,54],[41,46,248,44,42],[41,42,56,55],[42,44,248,247,269,58,62,43],[42,43,62,76,57,56],[45,51,52,71,59],[45,47,50,51],[46,1735,248],[46,250,251,1735],[48,53,54,49],[49,54,55,50],[50,55,56,51],[51,56,57,76,71,52],[58,269,1737,60,63,62],[59,71,72,61],[60,1737,1738,69,64,63],[61,72,73,70],[62,63,77,76],[63,64,78,77],[64,69,1738,278,67,65],[64,65,79,78],[65,67,278,279,302,81,85,66],[65,66,85,99,80,79],[68,74,75,94,82],[68,70,73,74],[71,76,77,72],[72,77,78,73],[73,78,79,74],[74,79,80,99,94,75],[81,302,83,86,85],[82,94,95,84],[83,1740,304,303],[83,303,305,92,87,86],[83,302,1740],[84,95,96,93],[85,86,100,99],[86,87,101,100],[87,92,305,306,90,88],[87,88,102,101],[88,90,306,307,331,104,108,89],[88,89,108,122,103,102],[91,97,98,117,105],[91,93,96,97],[94,99,100,95],[95,100,101,96],[96,101,102,97],[97,102,103,122,117,98],[104,331,330,106,109,108],[105,117,118,107],[106,330,329,115,110,109],[107,118,119,116],[108,109,123,122],[109,110,124,123],[110,115,1741,113,111],[110,111,125,124],[111,113,1741,321,347,127,131,112],[111,112,131,145,126,125],[114,120,121,140,128],[114,116,119,120],[115,318,1741],[115,329,317,318],[117,122,123,118],[118,123,124,119],[119,124,125,120],[120,125,126,145,140,121],[127,347,129,132,131],[128,140,141,130],[129,1743,349,348],[129,348,350,138,133,132],[129,347,1743],[130,141,142,139],[131,132,146,145],[132,133,147,146],[133,138,350,351,136,134],[133,134,148,147],[134,136,351,352,365,150,154,135],[134,135,154,168,149,148],[137,143,144,163,151],[137,139,142,143],[140,145,146,141],[141,146,147,142],[142,147,148,143],[143,148,149,168,163,144],[150,365,366,152,155,154],[151,163,164,153],[152,366,367,161,156,155],[153,164,165,162],[154,155,169,168],[155,156,170,169],[156,161,367,368,159,157],[156,157,171,170],[157,159,368,369,392,173,177,158],[157,158,177,191,172,171],[160,166,167,186,174],[160,162,165,166],[163,168,169,164],[164,169,170,165],[165,170,171,166],[166,171,172,191,186,167],[173,392,391,175,178,177],[174,186,187,176],[175,391,383,184,179,178],[176,187,188,185],[177,178,192,191],[178,179,193,192],[179,184,383,384,182,180],[179,180,194,193],[180,182,384,385,408,220,197,181],[180,181,197,214,195,194],[183,189,190,219,221],[183,185,188,189],[186,191,192,187],[187,192,193,188],[188,193,194,189],[189,194,195,214,219,190],[196,198,212,211],[196,211,210,413,409],[197,220,408,411,215,222],[197,222,223],[197,223,214],[198,204,213,212],[199,212,213],[199,213,200],[199,200,216,222],[199,222,211,212],[200,213,204,201],[200,201,203,202],[200,202,216],[201,204,1734],[201,1734,203],[202,203,205,217],[202,217,206],[202,206,214,216],[203,1734,205],[204,208,1734],[205,1734,209],[205,207,218,217],[206,217,218],[206,218,219,214],[207,221,219,218],[210,211,222,215,411,413],[214,223,216],[216,223,222],[224,234,229],[224,229,226],[224,226,231,230],[224,230,240,234],[225,235,243,232],[225,232,233,227],[226,229,246,255,253,228],[229,236,238,264,255,246],[229,234,236],[230,231,233,232],[230,232,243,240],[234,240,241,236],[235,237,244,243],[236,239,427,415,1744,268,264,238],[236,241,242,432,427,239],[237,414,418,245,244],[240,243,244,241],[241,244,245,418,432,242],[247,248,1735,256,269],[249,262,1736],[249,1736,1735],[249,1735,251],[249,251,254,255],[249,255,265,262],[250,252,254],[250,254,251],[252,253,255,254],[255,264,263,265],[256,271,272,1737],[256,1737,269],[256,1735,1736,266,273,271],[257,274,273,266,1736,258],[257,258,259,446,445,462,275,274],[258,1736,260],[258,260,447,446,259],[260,1736,262,261],[260,261,263,267,448,447],[261,262,265],[261,265,263],[263,264,268,1744,267],[267,441,440,448],[267,1744,441],[270,277,286,282],[270,282,1738],[270,1738,1737],[270,1737,272],[270,272,276,277],[271,273,276],[271,276,272],[273,274,277,276],[274,275,462,464,289,277],[277,289,464,471,285,284],[277,284,283,286],[278,280,1740,302,279],[278,1738,280],[280,1738,282,281],[280,281,283,287,1739,1740],[281,282,286],[281,286,283],[283,284,288,290,1739,287],[284,285,471,473,486,291,290,288],[290,291,486,485,292],[290,292,1739],[292,485,483,301,296,294],[292,294,295,1739],[293,295,299,300],[293,300,308,304],[293,304,1740],[293,1740,1739],[293,1739,295],[294,296,299],[294,299,295],[296,301,481,309,297],[296,297,300,299],[297,309,481,480,509,332,312,298],[297,298,312,326,310,300],[300,310,326,331,307,306],[300,306,305,308],[301,1745,481],[301,483,484,1745],[303,304,308],[303,308,305],[311,335,1742,313,324,323],[311,323,322,503,504,527,336,335],[312,332,509,507,327,333],[312,333,334],[312,334,326],[313,1742,1743,319,325,324],[314,324,325],[314,325,315],[314,315,328,333],[314,333,323,324],[315,325,319,316],[315,316,318,317],[315,317,328],[316,319,1741],[316,1741,318],[317,329,320],[317,320,326,328],[319,1743,347,321,1741],[320,329,330],[320,330,331,326],[322,323,333,327,507,503],[326,334,328],[328,334,333],[335,336,527,526,337],[335,337,1742],[337,526,524,346,341,339],[337,339,340,1742],[338,340,344,345],[338,345,353,349],[338,349,1743],[338,1743,1742],[338,1742,340],[339,341,344],[339,344,340],[341,346,522,354,342],[341,342,345,344],[342,354,522,521,543,356,358,343],[342,343,358,370,355,345],[345,355,370,365,352,351],[345,351,350,353],[346,1747,522],[346,524,525,1747],[348,349,353],[348,353,350],[356,543,1749,357,359,358],[357,1749,1750,364,360,359],[358,359,371,370],[359,360,372,371],[360,364,1750,552,363,361],[360,361,373,372],[361,363,552,553,582,388,387,362],[361,362,387,390,374,373],[365,370,371,366],[366,371,372,367],[367,372,373,368],[368,373,374,390,392,369],[375,376,381,380],[375,380,389,386],[375,386,379],[375,379,376],[376,377,572,585,588,398,396,378],[376,378,396,399,382,381],[376,379,393,574,572,377],[379,386,387],[379,387,388,582,574,393],[380,381,384,383],[380,383,391,389],[381,382,399,408,385,384],[386,389,390,387],[389,391,392,390],[394,400,410,406],[394,406,399],[394,399,396],[394,396,401,400],[395,407,412,403],[395,403,404,397],[396,398,588,600,402,401],[397,404,405,595,586],[399,406,408],[400,401,404,403],[400,403,412,410],[401,402,600,595,405,404],[406,410,411,408],[407,409,413,412],[410,412,413,411],[414,416,419,418],[415,427,428,417,1744],[416,425,420,419],[417,453,442,443],[417,443,1744],[417,428,429,426,454,453],[418,419,433,432],[419,420,434,433],[420,421,435,434],[420,425,423,421],[421,422,614,612,436,435],[421,423,608,614,422],[424,455,454,426,429,430],[424,430,431,607,609,629,456,455],[427,432,433,428],[428,433,434,429],[429,434,435,430],[430,435,436,612,607,431],[437,438,649,650,1754,463,468,457],[437,457,468,466,451,458],[437,458,459],[437,459,449],[437,449,450,645,649,438],[439,458,446,447],[439,447,448],[439,448,440],[439,440,452,458],[440,441,443,442],[440,442,452],[441,1744,443],[442,444,449,452],[442,453,444],[444,453,454],[444,454,455,449],[445,446,458,451,466,462],[449,459,452],[449,455,456,629,645,450],[452,459,458],[460,465,474,469],[460,469,464],[460,464,462],[460,462,466,465],[461,657,656,664],[461,664,663,470,477,467],[461,467,468,463,1754],[461,1754,657],[464,469,471],[465,467,477,474],[465,466,468,467],[469,474,475,471],[470,663,662,472,478,477],[471,475,476,488,486,473],[472,662,661,682,501,497,479,478],[474,477,478,475],[475,478,479,497,488,476],[480,481,1745,489,508,509],[482,495,1746],[482,1746,1745],[482,1745,484],[482,484,487,488],[482,488,498,495],[483,485,487],[483,487,484],[485,486,488,487],[488,497,496,498],[489,1745,1746,499,517,508],[490,518,517,499,1746,491],[490,491,492,677,676,700,519,518],[491,1746,493],[491,493,677,492],[493,1755,677],[493,1746,495,494],[493,494,496,500,681,679],[493,679,680,1755],[494,495,498],[494,498,496],[496,497,501,682,681,500],[502,510,505],[502,505,503],[502,503,507,506],[502,506,514,510],[503,505,520,529,527,504],[505,511,512,538,529,520],[505,510,511],[506,507,509,508],[506,508,517,514],[510,514,515,511],[511,513,707,698,717,542,538,512],[511,515,516,712,707,513],[514,517,518,515],[515,518,519,700,712,516],[521,522,1747,530,543],[523,536,1748],[523,1748,1747],[523,1747,525],[523,525,528,529],[523,529,539,536],[524,526,528],[524,528,525],[526,527,529,528],[529,538,537,539],[530,545,546,1749],[530,1749,543],[530,1747,1748,540,547,545],[531,548,547,540,1748,532],[531,532,533,726,727,749,549,548],[532,1748,534],[532,534,1758,726,533],[534,1748,536,535],[534,535,537,541,1757,1758],[535,536,539],[535,539,537],[537,538,542,717,1757,541],[544,551,560,556],[544,556,1750],[544,1750,1749],[544,1749,546],[544,546,550,551],[545,547,550],[545,550,546],[547,548,551,550],[548,549,749,760,563,551],[551,563,760,738,559,558],[551,558,557,560],[552,554,581,582,553],[552,1750,554],[554,1750,556,555],[554,555,557,561,579,581],[555,556,560],[555,560,557],[557,558,562,578,561],[558,559,738,739,771,577,578,562],[561,578,1752],[561,1752,580,579],[564,774,775,795,592,591,566,565],[564,565,1751,575,773,774],[565,566,591,590,567],[565,567,1751],[567,590,589,576,571,569],[567,569,570,1751],[568,1751,570],[568,570,573,574],[568,574,583,580],[568,580,1752],[568,1752,1751],[569,571,573],[569,573,570],[571,576,589,588,585,572],[571,572,574,573],[574,582,581,583],[575,1751,1752,584,772,773],[577,771,772,584,1752,578],[579,580,583],[579,583,581],[586,595,596,587],[587,596,597,594],[588,589,601,600],[589,590,602,601],[590,591,603,602],[591,592,795,798,604,603],[593,594,597,598],[593,598,599,800,796],[595,600,601,596],[596,601,602,597],[597,602,603,598],[598,603,604,798,800,599],[605,615,610],[605,610,607],[605,607,612,611],[605,611,621,615],[606,616,624,613],[606,613,614,608],[607,610,627,651,629,609],[610,617,619,642,651,627],[610,615,617],[611,612,614,613],[611,613,624,621],[615,621,622,617],[616,618,625,624],[617,620,814,802,824,628,642,619],[617,622,623,819,814,620],[618,801,805,626,625],[621,624,625,622],[622,625,626,805,819,623],[628,824,1760,630,643,642],[629,651,652],[629,652,645],[630,1760,1761,636,644,643],[631,643,644],[631,644,632],[631,632,646,651],[631,651,642,643],[632,644,636,633],[632,633,635,634],[632,634,646],[633,636,1753],[633,1753,635],[634,635,637,647],[634,647,638],[634,638,645,646],[635,1753,637],[636,1761,833,640,1753],[637,670,669,639,648,647],[637,1753,641,671,670],[638,647,648],[638,648,649,645],[639,669,658,659],[639,659,1754],[639,1754,650,649,648],[640,833,834,847,672,671,641,1753],[645,652,646],[646,652,651],[653,654,854,845,864,697,693,673],[653,673,693,684,667,674],[653,674,675],[653,675,665],[653,665,666,859,854,654],[655,674,662,663],[655,663,664],[655,664,656],[655,656,668,674],[656,657,659,658],[656,658,668],[657,1754,659],[658,660,665,668],[658,669,660],[660,669,670],[660,670,671,665],[661,662,674,667,684,682],[665,675,668],[665,671,672,847,859,666],[668,675,674],[676,677,1755,685,701,700],[678,691,1756],[678,1756,1755],[678,1755,680],[678,680,683,684],[678,684,694,691],[679,681,683],[679,683,680],[681,682,684,683],[684,693,692,694],[685,1755,1756,695,702,701],[686,703,702,695,1756,687],[686,687,688,873,874,896,704,703],[687,1756,689],[687,689,1763,873,688],[689,1756,691,690],[689,690,692,696,1762,1763],[690,691,694],[690,694,692],[692,693,697,864,1762,696],[698,707,708,699,717],[699,719,720,1757],[699,1757,717],[699,708,709,706,721,719],[700,701,713,712],[701,702,714,713],[702,703,715,714],[703,704,896,907,716,715],[705,722,721,706,709,710],[705,710,711,885,886,911,723,722],[707,712,713,708],[708,713,714,709],[709,714,715,710],[710,715,716,907,885,711],[718,725,734,730],[718,730,1758],[718,1758,1757],[718,1757,720],[718,720,724,725],[719,721,724],[719,724,720],[721,722,725,724],[722,723,911,913,737,725],[725,737,913,920,733,732],[725,732,731,734],[726,728,750,749,727],[726,1758,728],[728,1758,730,729],[728,729,731,735,751,750],[729,730,734],[729,734,731],[731,732,736,1759,735],[732,733,920,922,949,748,1759,736],[735,1759,742],[735,742,741,751],[738,752,753,776,771,739],[738,760,761],[738,761,752],[740,741,754,760],[740,760,749,750],[740,750,751],[740,751,741],[741,743,754],[741,742,744,743],[742,1759,744],[743,755,746],[743,746,752,754],[743,744,745,755],[744,1759,745],[745,948,947,747,756,755],[745,1759,748,949,948],[746,756,757,752],[746,755,756],[747,936,1765],[747,1765,759,757,756],[747,947,935,936],[752,757,758,764,776,753],[752,761,754],[754,761,760],[757,759,1765,939,953,762,764,758],[762,953,1766,763,765,764],[763,1766,1767,770,766,765],[764,765,777,776],[765,766,778,777],[766,770,1767,962,769,767],[766,767,779,778],[767,769,962,963,976,785,783,768],[767,768,783,786,780,779],[771,776,777,772],[772,777,778,773],[773,778,779,774],[774,779,780,786,795,775],[781,787,797,793],[781,793,786],[781,786,783],[781,783,788,787],[782,794,799,790],[782,790,791,784],[783,785,976,988,789,788],[784,791,792,983,974],[786,793,795],[787,788,791,790],[787,790,799,797],[788,789,988,983,792,791],[793,797,798,795],[794,796,800,799],[797,799,800,798],[801,803,806,805],[802,814,815,804,824],[803,812,807,806],[804,826,827,1760],[804,1760,824],[804,815,816,813,828,826],[805,806,820,819],[806,807,821,820],[807,808,822,821],[807,812,810,808],[808,809,997,1011,823,822],[808,810,993,997,809],[811,829,828,813,816,817],[811,817,818,1006,994,1018,830,829],[814,819,820,815],[815,820,821,816],[816,821,822,817],[817,822,823,1011,1006,818],[825,832,841,837],[825,837,1761],[825,1761,1760],[825,1760,827],[825,827,831,832],[826,828,831],[826,831,827],[828,829,832,831],[829,830,1018,1030,844,832],[832,844,1030,1025,840,839],[832,839,838,841],[833,835,848,847,834],[833,1761,835],[835,1761,837,836],[835,836,838,842,849,848],[836,837,841],[836,841,838],[838,839,843,850,849,842],[839,840,1025,1016,1037,851,850,843],[845,854,855,846,864],[846,866,867,1762],[846,1762,864],[846,855,856,853,868,866],[847,848,860,859],[848,849,861,860],[849,850,862,861],[850,851,1037,1049,863,862],[852,869,868,853,856,857],[852,857,858,1044,1035,1056,870,869],[854,859,860,855],[855,860,861,856],[856,861,862,857],[857,862,863,1049,1044,858],[865,872,881,877],[865,877,1763],[865,1763,1762],[865,1762,867],[865,867,871,872],[866,868,871],[866,871,867],[868,869,872,871],[869,870,1056,1068,884,872],[872,884,1068,1063,880,879],[872,879,878,881],[873,875,897,896,874],[873,1763,875],[875,1763,877,876],[875,876,878,882,898,897],[876,877,881],[876,881,878],[878,879,883,1764,882],[879,880,1063,1054,1073,895,1764,883],[882,1764,889],[882,889,888,898],[885,899,900,915,911,886],[885,907,908],[885,908,899],[887,888,901,907],[887,907,896,897],[887,897,898],[887,898,888],[888,890,901],[888,889,891,890],[889,1764,891],[890,902,893],[890,893,899,901],[890,891,892,902],[891,1764,892],[892,1768,1769,894,903,902],[892,1764,895,1073,1768],[893,903,904,899],[893,902,903],[894,1769,1082,906,904,903],[899,904,905,917,915,900],[899,908,901],[901,908,907],[904,906,1082,1083,1094,912,917,905],[909,914,923,918],[909,918,913],[909,913,911],[909,911,915,914],[910,1770,1771,919,926,916],[910,916,917,912,1094,1770],[913,918,920],[914,916,926,923],[914,915,917,916],[918,923,924,920],[919,1771,1103,921,927,926],[920,924,925,944,949,922],[921,1103,1104,1122,950,930,928,927],[923,926,927,924],[924,927,928,930,944,925],[929,958,957,931,942,941],[929,941,940,1116,1117,1135,959,958],[930,950,1122,1120,945,951],[930,951,952],[930,952,944],[931,957,955,937,943,942],[932,942,943],[932,943,933],[932,933,946,951],[932,951,941,942],[933,943,937,934],[933,934,936,935],[933,935,946],[934,937,1765],[934,1765,936],[935,947,938],[935,938,944,946],[937,955,956,1766],[937,1766,953],[937,953,939,1765],[938,947,948],[938,948,949,944],[940,941,951,945,1120,1116],[944,952,946],[946,952,951],[954,961,970,966],[954,966,1767],[954,1767,1766],[954,1766,956],[954,956,960,961],[955,957,960],[955,960,956],[957,958,961,960],[958,959,1135,1151,973,961],[961,973,1151,1155,969,968],[961,968,967,970],[962,964,977,976,963],[962,1767,964],[964,1767,966,965],[964,965,967,971,978,977],[965,966,970],[965,970,967],[967,968,972,979,978,971],[968,969,1155,1156,1161,980,979,972],[974,983,984,975],[975,984,985,982],[976,977,989,988],[977,978,990,989],[978,979,991,990],[979,980,1161,1173,992,991],[981,982,985,986],[981,986,987,1168,1159],[983,988,989,984],[984,989,990,985],[985,990,991,986],[986,991,992,1173,1168,987],[993,995,998,997],[994,1006,1007,996,1019,1018],[995,1004,999,998],[996,1007,1008,1005,1020,1019],[997,998,1012,1011],[998,999,1013,1012],[999,1000,1014,1013],[999,1004,1002,1000],[1000,1001,1182,1196,1015,1014],[1000,1002,1178,1182,1001],[1003,1021,1020,1005,1008,1009],[1003,1009,1010,1191,1179,1203,1022,1021],[1006,1011,1012,1007],[1007,1012,1013,1008],[1008,1013,1014,1009],[1009,1014,1015,1196,1191,1010],[1016,1025,1026,1017,1038,1037],[1017,1026,1027,1024,1039,1038],[1018,1019,1031,1030],[1019,1020,1032,1031],[1020,1021,1033,1032],[1021,1022,1203,1215,1034,1033],[1023,1040,1039,1024,1027,1028],[1023,1028,1029,1210,1201,1222,1041,1040],[1025,1030,1031,1026],[1026,1031,1032,1027],[1027,1032,1033,1028],[1028,1033,1034,1215,1210,1029],[1035,1044,1045,1036,1057,1056],[1036,1045,1046,1043,1058,1057],[1037,1038,1050,1049],[1038,1039,1051,1050],[1039,1040,1052,1051],[1040,1041,1222,1234,1053,1052],[1042,1059,1058,1043,1046,1047],[1042,1047,1048,1229,1220,1241,1060,1059],[1044,1049,1050,1045],[1045,1050,1051,1046],[1046,1051,1052,1047],[1047,1052,1053,1234,1229,1048],[1054,1063,1064,1055,1073],[1055,1075,1076,1768],[1055,1768,1073],[1055,1064,1065,1062,1077,1075],[1056,1057,1069,1068],[1057,1058,1070,1069],[1058,1059,1071,1070],[1059,1060,1241,1253,1072,1071],[1061,1078,1077,1062,1065,1066],[1061,1066,1067,1248,1239,1260,1079,1078],[1063,1068,1069,1064],[1064,1069,1070,1065],[1065,1070,1071,1066],[1066,1071,1072,1253,1248,1067],[1074,1081,1090,1086],[1074,1086,1769],[1074,1769,1768],[1074,1768,1076],[1074,1076,1080,1081],[1075,1077,1080],[1075,1080,1076],[1077,1078,1081,1080],[1078,1079,1260,1272,1093,1081],[1081,1093,1272,1267,1089,1088],[1081,1088,1087,1090],[1082,1084,1094,1083],[1082,1769,1084],[1084,1096,1097,1770],[1084,1770,1094],[1084,1769,1086,1085],[1084,1085,1087,1091,1098,1096],[1085,1086,1090],[1085,1090,1087],[1087,1088,1092,1099,1098,1091],[1088,1089,1267,1258,1288,1100,1099,1092],[1095,1102,1111,1107],[1095,1107,1771],[1095,1771,1770],[1095,1770,1097],[1095,1097,1101,1102],[1096,1098,1101],[1096,1101,1097],[1098,1099,1102,1101],[1099,1100,1288,1299,1114,1102],[1102,1114,1299,1277,1110,1109],[1102,1109,1108,1111],[1103,1105,1121,1122,1104],[1103,1771,1105],[1105,1771,1107,1106],[1105,1106,1108,1112,1130,1121],[1106,1107,1111],[1106,1111,1108],[1108,1109,1113,1131,1130,1112],[1109,1110,1277,1278,1304,1132,1131,1113],[1115,1123,1118],[1115,1118,1116],[1115,1116,1120,1119],[1115,1119,1127,1123],[1116,1118,1133,1157,1135,1117],[1118,1124,1125,1148,1157,1133],[1118,1123,1124],[1119,1120,1122,1121],[1119,1121,1130,1127],[1123,1127,1128,1124],[1124,1126,1309,1308,1324,1134,1148,1125],[1124,1128,1129,1312,1309,1126],[1127,1130,1131,1128],[1128,1131,1132,1304,1312,1129],[1134,1324,1325,1136,1149,1148],[1135,1157,1158],[1135,1158,1151],[1136,1325,1326,1142,1150,1149],[1137,1149,1150],[1137,1150,1138],[1137,1138,1152,1157],[1137,1157,1148,1149],[1138,1150,1142,1139],[1138,1139,1141,1140],[1138,1140,1152],[1139,1142,1772],[1139,1772,1141],[1140,1141,1143,1153],[1140,1153,1144],[1140,1144,1151,1152],[1141,1772,1143],[1142,1326,1327,1146,1772],[1143,1163,1162,1145,1154,1153],[1143,1772,1147,1164,1163],[1144,1153,1154],[1144,1154,1155,1151],[1145,1162,1161,1156,1155,1154],[1146,1327,1328,1348,1165,1164,1147,1772],[1151,1158,1152],[1152,1158,1157],[1159,1168,1169,1160],[1160,1169,1170,1167],[1161,1162,1174,1173],[1162,1163,1175,1174],[1163,1164,1176,1175],[1164,1165,1348,1351,1177,1176],[1166,1167,1170,1171],[1166,1171,1172,1353,1349],[1168,1173,1174,1169],[1169,1174,1175,1170],[1170,1175,1176,1171],[1171,1176,1177,1351,1353,1172],[1178,1180,1183,1182],[1179,1191,1192,1181,1204,1203],[1180,1189,1184,1183],[1181,1192,1193,1190,1205,1204],[1182,1183,1197,1196],[1183,1184,1198,1197],[1184,1185,1199,1198],[1184,1189,1187,1185],[1185,1186,1358,1372,1200,1199],[1185,1187,1354,1358,1186],[1188,1206,1205,1190,1193,1194],[1188,1194,1195,1367,1355,1384,1207,1206],[1191,1196,1197,1192],[1192,1197,1198,1193],[1193,1198,1199,1194],[1194,1199,1200,1372,1367,1195],[1201,1210,1211,1202,1223,1222],[1202,1211,1212,1209,1224,1223],[1203,1204,1216,1215],[1204,1205,1217,1216],[1205,1206,1218,1217],[1206,1207,1384,1382,1219,1218],[1208,1225,1224,1209,1212,1213],[1208,1213,1214,1378,1379,1397,1226,1225],[1210,1215,1216,1211],[1211,1216,1217,1212],[1212,1217,1218,1213],[1213,1218,1219,1382,1378,1214],[1220,1229,1230,1221,1242,1241],[1221,1230,1231,1228,1243,1242],[1222,1223,1235,1234],[1223,1224,1236,1235],[1224,1225,1237,1236],[1225,1226,1397,1413,1238,1237],[1227,1244,1243,1228,1231,1232],[1227,1232,1233,1417,1418,1428,1245,1244],[1229,1234,1235,1230],[1230,1235,1236,1231],[1231,1236,1237,1232],[1232,1237,1238,1413,1417,1233],[1239,1248,1249,1240,1261,1260],[1240,1249,1250,1247,1262,1261],[1241,1242,1254,1253],[1242,1243,1255,1254],[1243,1244,1256,1255],[1244,1245,1428,1426,1257,1256],[1246,1263,1262,1247,1250,1251],[1246,1251,1252,1422,1423,1442,1264,1263],[1248,1253,1254,1249],[1249,1254,1255,1250],[1250,1255,1256,1251],[1251,1256,1257,1426,1422,1252],[1258,1267,1268,1259,1289,1288],[1259,1268,1269,1266,1290,1289],[1260,1261,1273,1272],[1261,1262,1274,1273],[1262,1263,1275,1274],[1263,1264,1442,1444,1276,1275],[1265,1773,1266,1269,1270],[1265,1270,1271,1451,1453,1466,1287,1773],[1266,1773,1281],[1266,1281,1280,1290],[1267,1272,1273,1268],[1268,1273,1274,1269],[1269,1274,1275,1270],[1270,1275,1276,1444,1451,1271],[1277,1291,1292,1301,1304,1278],[1277,1299,1300],[1277,1300,1291],[1279,1280,1293,1299],[1279,1299,1288,1289],[1279,1289,1290],[1279,1290,1280],[1280,1282,1293],[1280,1281,1283,1282],[1281,1773,1283],[1282,1294,1285],[1282,1285,1291,1293],[1282,1283,1284,1294],[1283,1773,1284],[1284,1465,1463,1286,1295,1294],[1284,1773,1287,1466,1465],[1285,1295,1296,1291],[1285,1294,1295],[1286,1775,1461],[1286,1461,1298,1296,1295],[1286,1463,1464,1775],[1291,1296,1297,1302,1301,1292],[1291,1300,1293],[1293,1300,1299],[1296,1298,1461,1460,1500,1303,1302,1297],[1301,1302,1313],[1301,1313,1304],[1302,1303,1500,1492,1314,1313],[1304,1313,1312],[1305,1307,1490,1503,1516,1315,1317,1310],[1305,1310,1317,1329,1311,1306],[1305,1306,1313],[1305,1313,1314,1492,1490,1307],[1306,1311,1329,1324,1308,1309],[1306,1309,1313],[1309,1312,1313],[1315,1516,1316,1318,1317],[1316,1780,1518,1517],[1316,1517,1519,1323,1319,1318],[1316,1516,1780],[1317,1318,1330,1329],[1318,1319,1331,1330],[1319,1323,1519,1520,1322,1320],[1319,1320,1332,1331],[1320,1322,1520,1521,1539,1338,1336,1321],[1320,1321,1336,1339,1333,1332],[1324,1329,1330,1325],[1325,1330,1331,1326],[1326,1331,1332,1327],[1327,1332,1333,1339,1348,1328],[1334,1340,1350,1346],[1334,1346,1339],[1334,1339,1336],[1334,1336,1341,1340],[1335,1347,1352,1343],[1335,1343,1344,1337],[1336,1338,1539,1542,1342,1341],[1337,1344,1345,1544,1540],[1339,1346,1348],[1340,1341,1344,1343],[1340,1343,1352,1350],[1341,1342,1542,1544,1345,1344],[1346,1350,1351,1348],[1347,1349,1353,1352],[1350,1352,1353,1351],[1354,1356,1359,1358],[1355,1367,1368,1357,1383,1384],[1356,1365,1360,1359],[1357,1368,1369,1366,1392,1383],[1358,1359,1373,1372],[1359,1360,1374,1373],[1360,1361,1375,1374],[1360,1365,1363,1361],[1361,1362,1560,1571,1376,1375],[1361,1363,1545,1560,1362],[1364,1393,1392,1366,1369,1370],[1364,1370,1371,1546,1547,1575,1394,1393],[1367,1372,1373,1368],[1368,1373,1374,1369],[1369,1374,1375,1370],[1370,1375,1376,1571,1546,1371],[1377,1385,1380],[1377,1380,1378],[1377,1378,1382,1381],[1377,1381,1389,1385],[1378,1380,1395,1419,1397,1379],[1380,1386,1387,1410,1419,1395],[1380,1385,1386],[1381,1382,1384,1383],[1381,1383,1392,1389],[1385,1389,1390,1386],[1386,1388,1584,1586,1602,1396,1410,1387],[1386,1390,1391,1577,1584,1388],[1389,1392,1393,1390],[1390,1393,1394,1575,1577,1391],[1396,1602,1603,1398,1411,1410],[1397,1419,1420],[1397,1420,1413],[1398,1603,1604,1404,1412,1411],[1399,1411,1412],[1399,1412,1400],[1399,1400,1414,1419],[1399,1419,1410,1411],[1400,1412,1404,1401],[1400,1401,1403,1402],[1400,1402,1414],[1401,1404,1774],[1401,1774,1403],[1402,1403,1405,1415],[1402,1415,1406],[1402,1406,1413,1414],[1403,1774,1405],[1404,1604,1605,1408,1774],[1405,1436,1427,1407,1416,1415],[1405,1774,1409,1437,1436],[1406,1415,1416],[1406,1416,1417,1413],[1407,1427,1428,1418,1417,1416],[1408,1605,1606,1614,1438,1437,1409,1774],[1413,1420,1414],[1414,1420,1419],[1421,1429,1424],[1421,1424,1422],[1421,1422,1426,1425],[1421,1425,1433,1429],[1422,1424,1439,1446,1442,1423],[1424,1430,1431,1448,1446,1439],[1424,1429,1430],[1425,1426,1428,1427],[1425,1427,1436,1433],[1429,1433,1434,1430],[1430,1432,1623,1625,1641,1443,1448,1431],[1430,1434,1435,1616,1623,1432],[1433,1436,1437,1434],[1434,1437,1438,1614,1616,1435],[1440,1445,1454,1449],[1440,1449,1444],[1440,1444,1442],[1440,1442,1446,1445],[1441,1642,1643,1450,1457,1447],[1441,1447,1448,1443,1641,1642],[1444,1449,1451],[1445,1447,1457,1454],[1445,1446,1448,1447],[1449,1454,1455,1451],[1450,1643,1644,1452,1458,1457],[1451,1455,1456,1468,1466,1453],[1452,1644,1645,1660,1481,1477,1459,1458],[1454,1457,1458,1455],[1455,1458,1459,1477,1468,1456],[1460,1461,1775,1469,1499,1500],[1462,1475,1776],[1462,1776,1775],[1462,1775,1464],[1462,1464,1467,1468],[1462,1468,1478,1475],[1463,1465,1467],[1463,1467,1464],[1465,1466,1468,1467],[1468,1477,1476,1478],[1469,1775,1776,1479,1497,1499],[1470,1496,1479,1776,1471],[1470,1471,1472,1663,1664,1679,1495,1496],[1471,1776,1473],[1471,1473,1662,1663,1472],[1473,1776,1475,1474],[1473,1474,1476,1480,1661,1662],[1474,1475,1478],[1474,1478,1476],[1476,1477,1481,1660,1661,1480],[1479,1496,1778],[1479,1778,1498,1497],[1482,1682,1683,1698,1505,1504,1484,1483],[1482,1483,1777,1493,1681,1682],[1483,1484,1504,1779,1485],[1483,1485,1777],[1485,1779,1780,1494,1489,1487],[1485,1487,1488,1777],[1486,1777,1488],[1486,1488,1491,1492],[1486,1492,1501,1498],[1486,1498,1778],[1486,1778,1777],[1487,1489,1491],[1487,1491,1488],[1489,1494,1780,1516,1503,1490],[1489,1490,1492,1491],[1492,1500,1499,1501],[1493,1777,1778,1502,1680,1681],[1495,1679,1680,1502,1778,1496],[1497,1498,1501],[1497,1501,1499],[1504,1505,1698,1699,1506],[1504,1506,1779],[1506,1699,1700,1515,1510,1508],[1506,1508,1509,1779],[1507,1509,1513,1514],[1507,1514,1522,1518],[1507,1518,1780],[1507,1780,1779],[1507,1779,1509],[1508,1510,1513],[1508,1513,1509],[1510,1515,1700,1701,1523,1511],[1510,1511,1514,1513],[1511,1523,1701,1702,1709,1529,1527,1512],[1511,1512,1527,1530,1524,1514],[1514,1524,1530,1539,1521,1520],[1514,1520,1519,1522],[1517,1518,1522],[1517,1522,1519],[1525,1531,1541,1537],[1525,1537,1530],[1525,1530,1527],[1525,1527,1532,1531],[1526,1538,1543,1534],[1526,1534,1535,1528],[1527,1529,1709,1725,1533,1532],[1528,1535,1536,1729,1730],[1530,1537,1539],[1531,1532,1535,1534],[1531,1534,1543,1541],[1532,1533,1725,1729,1536,1535],[1537,1541,1542,1539],[1538,1540,1544,1543],[1541,1543,1544,1542],[1545,1548,1561,1560],[1546,1563,1564,1579,1575,1547],[1546,1571,1572],[1546,1572,1563],[1548,1554,1562,1561],[1549,1550,1565,1571],[1549,1571,1560,1561],[1549,1561,1562],[1549,1562,1550],[1550,1552,1565],[1550,1562,1554,1551],[1550,1551,1553,1552],[1551,1781,1553],[1551,1554,1781],[1552,1566,1556],[1552,1556,1563,1565],[1552,1553,1555,1566],[1553,1781,1555],[1554,1558,1781],[1555,1557,1567,1566],[1555,1781,1559],[1556,1567,1568,1563],[1556,1566,1567],[1557,1570,1568,1567],[1563,1568,1569,1581,1579,1564],[1563,1572,1565],[1565,1572,1571],[1568,1570,1576,1581,1569],[1573,1578,1587,1582],[1573,1582,1577],[1573,1577,1575],[1573,1575,1579,1578],[1574,1583,1590,1580],[1574,1580,1581,1576],[1577,1582,1584],[1578,1580,1590,1587],[1578,1579,1581,1580],[1582,1587,1588,1584],[1583,1585,1591,1590],[1584,1588,1589,1607,1602,1586],[1585,1593,1595,1592,1591],[1587,1590,1591,1588],[1588,1591,1592,1595,1607,1589],[1593,1594,1596,1595],[1594,1601,1597,1596],[1595,1596,1608,1607],[1596,1597,1609,1608],[1597,1601,1600,1598],[1597,1598,1610,1609],[1598,1600,1615,1620,1599],[1598,1599,1620,1618,1611,1610],[1602,1607,1608,1603],[1603,1608,1609,1604],[1604,1609,1610,1605],[1605,1610,1611,1618,1614,1606],[1612,1617,1626,1621],[1612,1621,1616],[1612,1616,1614],[1612,1614,1618,1617],[1613,1622,1629,1619],[1613,1619,1620,1615],[1616,1621,1623],[1617,1619,1629,1626],[1617,1618,1620,1619],[1621,1626,1627,1623],[1622,1624,1630,1629],[1623,1627,1628,1646,1641,1625],[1624,1632,1634,1631,1630],[1626,1629,1630,1627],[1627,1630,1631,1634,1646,1628],[1632,1633,1635,1634],[1633,1640,1636,1635],[1634,1635,1647,1646],[1635,1636,1648,1647],[1636,1640,1639,1637],[1636,1637,1649,1648],[1637,1639,1651,1653,1638],[1637,1638,1653,1665,1650,1649],[1641,1646,1647,1642],[1642,1647,1648,1643],[1643,1648,1649,1644],[1644,1649,1650,1665,1660,1645],[1651,1652,1654,1653],[1652,1659,1655,1654],[1653,1654,1666,1665],[1654,1655,1667,1666],[1655,1659,1658,1656],[1655,1656,1668,1667],[1656,1658,1670,1672,1657],[1656,1657,1672,1684,1669,1668],[1660,1665,1666,1661],[1661,1666,1667,1662],[1662,1667,1668,1663],[1663,1668,1669,1684,1679,1664],[1670,1671,1673,1672],[1671,1678,1674,1673],[1672,1673,1685,1684],[1673,1674,1686,1685],[1674,1678,1677,1675],[1674,1675,1687,1686],[1675,1677,1689,1691,1676],[1675,1676,1691,1703,1688,1687],[1679,1684,1685,1680],[1680,1685,1686,1681],[1681,1686,1687,1682],[1682,1687,1688,1703,1698,1683],[1689,1690,1692,1691],[1690,1697,1693,1692],[1691,1692,1704,1703],[1692,1693,1705,1704],[1693,1697,1696,1694],[1693,1694,1706,1705],[1694,1696,1708,1722,1695],[1694,1695,1722,1731,1707,1706],[1698,1703,1704,1699],[1699,1704,1705,1700],[1700,1705,1706,1701],[1701,1706,1707,1731,1709,1702],[1708,1710,1723,1722],[1709,1731,1732],[1709,1732,1725],[1710,1716,1724,1723],[1711,1723,1724],[1711,1724,1712],[1711,1712,1726,1731],[1711,1731,1722,1723],[1712,1724,1716,1713],[1712,1713,1715,1714],[1712,1714,1726],[1713,1716,1782],[1713,1782,1715],[1714,1715,1717,1727],[1714,1727,1718],[1714,1718,1725,1726],[1715,1782,1717],[1716,1720,1782],[1717,1719,1728,1727],[1717,1782,1721],[1718,1727,1728],[1718,1728,1729,1725],[1719,1730,1729,1728],[1725,1732,1726],[1726,1732,1731]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\": [\"creasePattern\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0],[54,0],[54,54],[0,54],[0,5],[5,5],[6,5],[0,4],[2,4],[2,3],[1,2],[3,2],[2,1],[0,2],[2,0],[4,2],[4,0],[0,1],[1,0],[1,6],[1,5],[1,4],[1,3],[5,3],[6,3],[3,6],[3,3],[3,1],[4,1],[5,1],[6,1],[5,6],[5,0],[3,5],[4,4],[6,7],[0,7],[6,8],[0,8],[5,7],[5,8],[5,10],[5,11],[5,12],[6,11],[0,11],[6,10],[0,10],[1,7],[1,8],[1,10],[1,11],[1,12],[3,7],[3,8],[3,10],[3,11],[3,12],[6,13],[0,13],[6,14],[0,14],[5,13],[5,14],[5,16],[5,17],[5,18],[6,17],[0,17],[6,16],[0,16],[1,13],[1,14],[1,16],[1,17],[1,18],[3,13],[3,14],[3,16],[3,17],[3,18],[6,19],[0,19],[6,20],[0,20],[5,19],[5,20],[5,22],[5,23],[5,24],[6,23],[0,23],[6,22],[0,22],[1,19],[1,20],[1,22],[1,23],[1,24],[3,19],[3,20],[3,22],[3,23],[3,24],[6,25],[0,25],[6,26],[0,26],[5,25],[5,26],[5,28],[5,29],[5,30],[6,29],[0,29],[6,28],[0,28],[1,25],[1,26],[1,28],[1,29],[1,30],[3,25],[3,26],[3,28],[3,29],[3,30],[6,31],[0,31],[6,32],[0,32],[5,31],[5,32],[5,34],[5,35],[5,36],[6,35],[0,35],[6,34],[0,34],[1,31],[1,32],[1,34],[1,35],[1,36],[3,31],[3,32],[3,34],[3,35],[3,36],[6,37],[0,37],[6,38],[0,38],[5,37],[5,38],[5,40],[5,41],[5,42],[6,41],[0,41],[6,40],[0,40],[1,37],[1,38],[1,40],[1,41],[1,42],[3,37],[3,38],[3,40],[3,41],[3,42],[6,43],[0,43],[6,44],[0,44],[5,43],[5,44],[5,46],[5,47],[5,48],[6,47],[0,47],[6,46],[0,46],[1,43],[1,44],[1,46],[1,47],[1,48],[3,43],[3,44],[3,46],[3,47],[3,48],[5,54],[5,49],[4,54],[4,52],[3,52],[2,53],[2,51],[1,52],[2,54],[0,52],[2,50],[0,50],[1,54],[0,53],[6,53],[5,53],[4,53],[3,53],[3,49],[6,51],[3,51],[1,51],[1,50],[1,49],[6,49],[0,49],[5,51],[4,50],[8,4],[8,0],[7,5],[7,0],[7,6],[9,5],[8,3],[7,3],[8,1],[7,1],[10,4],[10,0],[11,5],[11,0],[11,6],[12,5],[10,3],[11,3],[12,3],[10,1],[11,1],[12,1],[9,6],[7,12],[7,11],[9,9],[7,9],[8,9],[7,8],[7,7],[8,8],[9,7],[8,12],[11,12],[11,11],[12,11],[12,10],[11,9],[10,9],[11,8],[11,7],[10,8],[10,12],[12,8],[12,7],[7,13],[9,15],[9,13],[9,14],[10,13],[11,13],[12,13],[10,14],[11,15],[7,17],[7,18],[8,18],[9,17],[9,16],[10,17],[11,17],[12,17],[10,16],[10,18],[11,18],[12,15],[11,19],[12,19],[12,20],[9,21],[11,21],[10,21],[11,22],[11,23],[11,24],[10,22],[9,23],[12,22],[7,19],[7,21],[8,21],[7,22],[7,23],[7,24],[8,22],[12,23],[9,24],[11,30],[11,25],[10,30],[10,28],[9,28],[8,29],[8,27],[7,28],[8,30],[8,26],[7,30],[12,29],[11,29],[10,29],[9,29],[9,25],[12,27],[9,27],[7,27],[7,26],[7,25],[12,25],[11,27],[10,26],[11,31],[12,31],[12,32],[9,33],[11,33],[10,33],[11,34],[11,35],[11,36],[10,34],[9,35],[12,34],[7,31],[7,33],[8,33],[7,34],[7,35],[7,36],[8,34],[12,35],[9,36],[12,37],[12,38],[11,37],[11,38],[11,40],[11,41],[11,42],[12,41],[12,40],[7,37],[7,38],[7,40],[7,41],[7,42],[9,37],[9,38],[9,40],[9,41],[9,42],[10,46],[11,47],[12,47],[11,48],[11,45],[9,46],[9,47],[9,48],[7,46],[7,47],[7,48],[10,44],[11,43],[12,43],[9,44],[9,43],[7,44],[7,43],[12,45],[10,50],[10,54],[11,49],[11,54],[12,49],[9,49],[10,51],[11,51],[12,51],[10,53],[11,53],[12,53],[8,50],[8,54],[7,49],[7,54],[8,51],[7,51],[8,53],[7,53],[13,0],[13,6],[14,0],[14,6],[13,1],[14,1],[16,1],[17,1],[18,1],[17,0],[17,6],[16,0],[16,6],[13,5],[14,5],[16,5],[17,5],[18,5],[13,3],[14,3],[16,3],[17,3],[18,3],[17,11],[18,11],[14,10],[14,9],[13,8],[15,8],[14,7],[16,8],[13,12],[13,11],[13,10],[13,9],[17,9],[18,9],[15,12],[15,9],[15,7],[16,7],[17,7],[18,7],[17,12],[15,11],[16,10],[14,14],[18,14],[13,13],[18,13],[13,15],[15,14],[15,13],[17,14],[17,13],[14,16],[18,16],[13,17],[18,17],[13,18],[15,16],[15,17],[15,18],[17,16],[17,17],[17,18],[13,24],[13,23],[15,21],[13,21],[14,21],[13,20],[13,19],[14,20],[15,19],[14,24],[17,24],[17,23],[18,23],[18,22],[17,21],[16,21],[17,20],[17,19],[16,20],[16,24],[18,20],[18,19],[14,28],[13,29],[13,30],[15,29],[14,27],[13,27],[14,25],[13,25],[16,28],[17,29],[17,30],[18,29],[16,27],[17,27],[18,27],[16,25],[17,25],[18,25],[15,30],[13,36],[13,35],[15,33],[13,33],[14,33],[13,32],[13,31],[14,32],[15,31],[14,36],[17,36],[17,35],[18,35],[18,34],[17,33],[16,33],[17,32],[17,31],[16,32],[16,36],[18,32],[18,31],[13,37],[15,39],[15,37],[15,38],[16,37],[17,37],[18,37],[16,38],[17,39],[13,41],[13,42],[14,42],[15,41],[15,40],[16,41],[17,41],[18,41],[16,40],[16,42],[17,42],[18,39],[18,47],[17,47],[17,48],[16,48],[15,45],[15,47],[15,46],[14,47],[13,47],[14,46],[13,45],[18,46],[14,48],[18,43],[17,43],[15,43],[15,44],[14,43],[13,43],[14,44],[18,44],[13,48],[13,54],[14,54],[13,49],[14,49],[16,49],[17,49],[18,49],[17,54],[16,54],[13,53],[14,53],[16,53],[17,53],[18,53],[13,51],[14,51],[16,51],[17,51],[18,51],[20,4],[20,0],[19,5],[19,0],[19,6],[21,5],[20,3],[19,3],[20,1],[19,1],[22,4],[22,0],[23,5],[23,0],[23,6],[24,5],[22,3],[23,3],[24,3],[22,1],[23,1],[24,1],[21,6],[24,7],[19,7],[24,8],[22,8],[22,9],[23,10],[21,10],[22,11],[24,10],[22,12],[20,10],[20,12],[24,11],[23,12],[23,7],[23,8],[23,9],[19,9],[21,9],[21,11],[20,11],[19,11],[19,12],[21,7],[20,8],[23,17],[24,17],[20,16],[20,15],[19,14],[21,14],[20,13],[22,14],[19,18],[19,17],[19,16],[19,15],[23,15],[24,15],[21,18],[21,15],[21,13],[22,13],[23,13],[24,13],[23,18],[21,17],[22,16],[19,24],[19,23],[21,21],[19,21],[20,21],[19,20],[19,19],[20,20],[21,19],[20,24],[23,24],[23,23],[24,23],[24,22],[23,21],[22,21],[23,20],[23,19],[22,20],[22,24],[24,20],[24,19],[19,30],[20,30],[19,25],[20,25],[22,25],[23,25],[24,25],[23,30],[22,30],[19,29],[20,29],[22,29],[23,29],[24,29],[19,27],[20,27],[22,27],[23,27],[24,27],[19,31],[21,33],[21,31],[21,32],[22,31],[23,31],[24,31],[22,32],[23,33],[19,35],[19,36],[20,36],[21,35],[21,34],[22,35],[23,35],[24,35],[22,34],[22,36],[23,36],[24,33],[19,41],[19,42],[20,38],[21,38],[22,37],[22,39],[23,38],[24,38],[22,40],[24,40],[24,37],[19,37],[20,37],[21,37],[21,41],[21,42],[21,39],[23,39],[23,40],[23,41],[23,42],[24,41],[19,39],[20,40],[24,43],[24,44],[23,43],[23,44],[23,46],[23,47],[23,48],[24,47],[24,46],[19,43],[19,44],[19,46],[19,47],[19,48],[21,43],[21,44],[21,46],[21,47],[21,48],[22,50],[22,54],[23,49],[23,54],[24,49],[21,49],[22,51],[23,51],[24,51],[22,53],[23,53],[24,53],[20,50],[20,54],[19,49],[19,54],[20,51],[19,51],[20,53],[19,53],[25,0],[25,6],[26,0],[26,6],[25,1],[26,1],[28,1],[29,1],[30,1],[29,0],[29,6],[28,0],[28,6],[25,5],[26,5],[28,5],[29,5],[30,5],[25,3],[26,3],[28,3],[29,3],[30,3],[25,7],[27,9],[27,7],[27,8],[28,7],[29,7],[30,7],[28,8],[29,9],[25,11],[25,12],[26,12],[27,11],[27,10],[28,11],[29,11],[30,11],[28,10],[28,12],[29,12],[30,9],[25,18],[26,18],[25,13],[26,13],[28,13],[29,13],[30,13],[29,18],[28,18],[25,17],[26,17],[28,17],[29,17],[30,17],[25,15],[26,15],[28,15],[29,15],[30,15],[25,19],[27,21],[27,19],[27,20],[28,19],[29,19],[30,19],[28,20],[29,21],[25,23],[25,24],[26,24],[27,23],[27,22],[28,23],[29,23],[30,23],[28,22],[28,24],[29,24],[30,21],[25,29],[25,30],[26,26],[27,26],[28,25],[28,27],[29,26],[30,26],[28,28],[30,28],[30,25],[25,25],[26,25],[27,25],[27,29],[27,30],[27,27],[29,27],[29,28],[29,29],[29,30],[30,29],[25,27],[26,28],[26,32],[30,32],[25,31],[30,31],[25,33],[27,32],[27,31],[29,32],[29,31],[26,34],[30,34],[25,35],[30,35],[25,36],[27,34],[27,35],[27,36],[29,34],[29,35],[29,36],[29,42],[29,37],[28,42],[28,40],[27,40],[26,41],[26,39],[25,40],[26,42],[26,38],[25,42],[30,41],[29,41],[28,41],[27,41],[27,37],[30,39],[27,39],[25,39],[25,38],[25,37],[30,37],[29,39],[28,38],[25,43],[27,45],[27,43],[27,44],[28,43],[29,43],[30,43],[28,44],[29,45],[25,47],[25,48],[26,48],[27,47],[27,46],[28,47],[29,47],[30,47],[28,46],[28,48],[29,48],[30,45],[25,54],[26,54],[25,49],[26,49],[28,49],[29,49],[30,49],[29,54],[28,54],[25,53],[26,53],[28,53],[29,53],[30,53],[25,51],[26,51],[28,51],[29,51],[30,51],[31,0],[31,6],[32,0],[32,6],[31,1],[32,1],[34,1],[35,1],[36,1],[35,0],[35,6],[34,0],[34,6],[31,5],[32,5],[34,5],[35,5],[36,5],[31,3],[32,3],[34,3],[35,3],[36,3],[31,12],[32,12],[31,7],[32,7],[34,7],[35,7],[36,7],[35,12],[34,12],[31,11],[32,11],[34,11],[35,11],[36,11],[31,9],[32,9],[34,9],[35,9],[36,9],[31,18],[32,18],[31,13],[32,13],[34,13],[35,13],[36,13],[35,18],[34,18],[31,17],[32,17],[34,17],[35,17],[36,17],[31,15],[32,15],[34,15],[35,15],[36,15],[31,24],[32,24],[31,19],[32,19],[34,19],[35,19],[36,19],[35,24],[34,24],[31,23],[32,23],[34,23],[35,23],[36,23],[31,21],[32,21],[34,21],[35,21],[36,21],[31,25],[33,27],[33,25],[33,26],[34,25],[35,25],[36,25],[34,26],[35,27],[31,29],[31,30],[32,30],[33,29],[33,28],[34,29],[35,29],[36,29],[34,28],[34,30],[35,30],[36,27],[31,31],[33,33],[33,31],[33,32],[34,31],[35,31],[36,31],[34,32],[35,33],[31,35],[31,36],[32,36],[33,35],[33,34],[34,35],[35,35],[36,35],[34,34],[34,36],[35,36],[36,33],[32,40],[31,41],[31,42],[33,41],[32,39],[31,39],[32,37],[31,37],[34,40],[35,41],[35,42],[36,41],[34,39],[35,39],[36,39],[34,37],[35,37],[36,37],[33,42],[36,43],[31,43],[36,44],[34,44],[34,45],[35,46],[33,46],[34,47],[36,46],[34,48],[32,46],[32,48],[36,47],[35,48],[35,43],[35,44],[35,45],[31,45],[33,45],[33,47],[32,47],[31,47],[31,48],[33,43],[32,44],[31,54],[32,54],[31,49],[32,49],[34,49],[35,49],[36,49],[35,54],[34,54],[31,53],[32,53],[34,53],[35,53],[36,53],[31,51],[32,51],[34,51],[35,51],[36,51],[37,0],[37,6],[38,0],[38,6],[37,1],[38,1],[40,1],[41,1],[42,1],[41,0],[41,6],[40,0],[40,6],[37,5],[38,5],[40,5],[41,5],[42,5],[37,3],[38,3],[40,3],[41,3],[42,3],[37,12],[38,12],[37,7],[38,7],[40,7],[41,7],[42,7],[41,12],[40,12],[37,11],[38,11],[40,11],[41,11],[42,11],[37,9],[38,9],[40,9],[41,9],[42,9],[37,18],[38,18],[37,13],[38,13],[40,13],[41,13],[42,13],[41,18],[40,18],[37,17],[38,17],[40,17],[41,17],[42,17],[37,15],[38,15],[40,15],[41,15],[42,15],[37,24],[38,24],[37,19],[38,19],[40,19],[41,19],[42,19],[41,24],[40,24],[37,23],[38,23],[40,23],[41,23],[42,23],[37,21],[38,21],[40,21],[41,21],[42,21],[37,30],[38,30],[37,25],[38,25],[40,25],[41,25],[42,25],[41,30],[40,30],[37,29],[38,29],[40,29],[41,29],[42,29],[37,27],[38,27],[40,27],[41,27],[42,27],[37,35],[37,36],[38,32],[39,32],[40,31],[40,33],[41,32],[42,32],[40,34],[42,34],[42,31],[37,31],[38,31],[39,31],[39,35],[39,36],[39,33],[41,33],[41,34],[41,35],[41,36],[42,35],[37,33],[38,34],[39,37],[41,37],[42,37],[37,37],[41,41],[39,41],[42,41],[37,42],[37,41],[41,42],[39,42],[37,39],[39,39],[42,39],[42,43],[42,44],[41,43],[41,44],[41,46],[41,47],[41,48],[42,47],[42,46],[37,43],[37,44],[37,46],[37,47],[37,48],[39,43],[39,44],[39,46],[39,47],[39,48],[40,50],[40,54],[41,49],[41,54],[42,49],[39,49],[40,51],[41,51],[42,51],[40,53],[41,53],[42,53],[38,50],[38,54],[37,49],[37,54],[38,51],[37,51],[38,53],[37,53],[43,0],[43,6],[44,0],[44,6],[43,1],[44,1],[46,1],[47,1],[48,1],[47,0],[47,6],[46,0],[46,6],[43,5],[44,5],[46,5],[47,5],[48,5],[43,3],[44,3],[46,3],[47,3],[48,3],[44,10],[43,11],[43,12],[45,11],[44,9],[43,9],[44,7],[43,7],[46,10],[47,11],[47,12],[48,11],[46,9],[47,9],[48,9],[46,7],[47,7],[48,7],[45,12],[48,13],[43,13],[48,14],[46,14],[46,15],[47,16],[45,16],[46,17],[48,16],[46,18],[44,16],[44,18],[48,17],[47,18],[47,13],[47,14],[47,15],[43,15],[45,15],[45,17],[44,17],[43,17],[43,18],[45,13],[44,14],[44,22],[43,23],[43,24],[45,23],[44,21],[43,21],[44,19],[43,19],[46,22],[47,23],[47,24],[48,23],[46,21],[47,21],[48,21],[46,19],[47,19],[48,19],[45,24],[44,26],[48,26],[43,25],[48,25],[43,27],[45,26],[45,25],[47,26],[47,25],[44,28],[48,28],[43,29],[48,29],[43,30],[45,28],[45,29],[45,30],[47,28],[47,29],[47,30],[43,36],[43,35],[45,33],[43,33],[44,33],[43,32],[43,31],[44,32],[45,31],[44,36],[47,36],[47,35],[48,35],[48,34],[47,33],[46,33],[47,32],[47,31],[46,32],[46,36],[48,32],[48,31],[48,41],[47,41],[47,42],[46,42],[45,39],[45,41],[45,40],[44,41],[43,41],[44,40],[43,39],[48,40],[44,42],[48,37],[47,37],[45,37],[45,38],[44,37],[43,37],[44,38],[48,38],[43,42],[47,43],[48,43],[48,44],[45,45],[47,45],[46,45],[47,46],[47,47],[47,48],[46,46],[45,47],[48,46],[43,43],[43,45],[44,45],[43,46],[43,47],[43,48],[44,46],[48,47],[45,48],[46,50],[46,54],[47,49],[47,54],[48,49],[45,49],[46,51],[47,51],[48,51],[46,53],[47,53],[48,53],[44,50],[44,54],[43,49],[43,54],[44,51],[43,51],[44,53],[43,53],[49,0],[49,5],[49,6],[50,0],[50,2],[51,2],[52,1],[52,3],[53,2],[52,0],[54,2],[52,4],[54,4],[53,0],[54,1],[49,1],[50,1],[51,1],[51,5],[51,6],[51,3],[53,3],[53,4],[53,5],[53,6],[54,5],[49,3],[50,4],[50,8],[54,8],[49,7],[54,7],[49,9],[51,8],[51,7],[53,8],[53,7],[50,10],[54,10],[49,11],[54,11],[49,12],[51,10],[51,11],[51,12],[53,10],[53,11],[53,12],[54,13],[54,14],[53,13],[53,14],[53,16],[53,17],[53,18],[54,17],[54,16],[49,13],[49,14],[49,16],[49,17],[49,18],[51,13],[51,14],[51,16],[51,17],[51,18],[50,20],[54,20],[49,19],[54,19],[49,21],[51,20],[51,19],[53,20],[53,19],[50,22],[54,22],[49,23],[54,23],[49,24],[51,22],[51,23],[51,24],[53,22],[53,23],[53,24],[54,25],[54,26],[53,25],[53,26],[53,28],[53,29],[53,30],[54,29],[54,28],[49,25],[49,26],[49,28],[49,29],[49,30],[51,25],[51,26],[51,28],[51,29],[51,30],[54,31],[54,32],[53,31],[53,32],[53,34],[53,35],[53,36],[54,35],[54,34],[49,31],[49,32],[49,34],[49,35],[49,36],[51,31],[51,32],[51,34],[51,35],[51,36],[54,37],[54,38],[53,37],[53,38],[53,40],[53,41],[53,42],[54,41],[54,40],[49,37],[49,38],[49,40],[49,41],[49,42],[51,37],[51,38],[51,40],[51,41],[51,42],[54,43],[54,44],[53,43],[53,44],[53,46],[53,47],[53,48],[54,47],[54,46],[49,43],[49,44],[49,46],[49,47],[49,48],[51,43],[51,44],[51,46],[51,47],[51,48],[54,49],[49,49],[54,50],[52,50],[52,51],[53,52],[51,52],[52,53],[54,52],[52,54],[50,52],[50,54],[54,53],[53,54],[53,49],[53,50],[53,51],[49,51],[51,51],[51,53],[50,53],[49,53],[49,54],[51,49],[50,50],[1,1],[1,53],[8,10],[10,10],[8,14],[8,16],[10,20],[8,20],[7,29],[10,32],[8,32],[13,7],[14,22],[16,22],[14,34],[16,34],[14,38],[14,40],[16,46],[16,44],[23,11],[19,13],[20,22],[22,22],[20,32],[20,34],[23,37],[26,8],[26,10],[26,20],[26,22],[29,25],[25,41],[26,44],[26,46],[32,26],[32,28],[32,32],[32,34],[35,47],[41,31],[47,17],[44,34],[46,34],[46,40],[46,38],[46,44],[44,44],[53,1],[53,53]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/maze-s.fold",
    "content": "{\n\t\"file_spec\":1.1,\n\t\"file_creator\":\"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"file_title\": \"Maze Folding\",\n\t\"file_author\": \"Demaine, Demaine, Ku\",\n\t\"vertices_coords\":[\n\t\t[0,0],[18,0],[18,12],[0,12],[5,0],[5,1],[6,1],[6,2],[3,3],[5,3],[4,3],[5,4],[5,5],[5,6],[4,4],[3,5],[4,0],[6,4],[1,0],[1,1],[0,1],[0,2],[1,3],[2,3],[1,4],[1,5],[1,6],[2,4],[2,0],[0,4],[6,5],[0,5],[3,6],[5,12],[5,7],[4,12],[4,10],[3,10],[2,11],[2,9],[1,10],[2,12],[0,10],[2,8],[0,8],[1,12],[0,11],[6,11],[5,11],[4,11],[3,11],[3,7],[6,9],[3,9],[1,9],[1,8],[1,7],[6,7],[0,7],[5,9],[4,8],[11,5],[12,5],[8,4],[8,3],[7,2],[9,2],[8,1],[8,0],[10,2],[10,0],[7,0],[7,6],[7,5],[7,4],[7,3],[11,3],[12,3],[9,6],[9,3],[9,1],[10,1],[11,1],[12,1],[11,6],[11,0],[9,5],[10,4],[12,7],[7,7],[12,8],[10,8],[10,9],[11,10],[9,10],[10,11],[12,10],[10,12],[8,10],[8,12],[12,11],[11,12],[11,7],[11,8],[11,9],[7,9],[9,9],[9,11],[8,11],[7,11],[7,12],[9,7],[8,8],[13,0],[13,5],[13,6],[14,0],[14,2],[15,2],[16,1],[16,3],[17,2],[16,0],[18,2],[16,4],[18,4],[17,0],[18,1],[13,1],[14,1],[15,1],[15,5],[15,6],[15,3],[17,3],[17,4],[17,5],[17,6],[18,5],[13,3],[14,4],[13,12],[13,11],[15,9],[13,9],[14,9],[13,8],[13,7],[14,8],[15,7],[14,12],[17,12],[17,11],[18,11],[18,10],[17,9],[16,9],[17,8],[17,7],[16,8],[16,12],[18,8],[18,7],[4,2],[2,2],[1,11],[7,1],[11,11],[17,1],[14,10],[16,10]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,18],[18,28],[28,16],[16,4],[4,71],[71,68],[68,70],[70,85],[85,113],[113,116],[116,122],[122,126],[126,1],[1,127],[127,123],[123,125],[125,138],[138,162],[162,161],[161,154],[154,153],[153,2],[2,151],[151,160],[160,150],[150,141],[141,101],[101,97],[97,99],[99,110],[110,33],[33,35],[35,41],[41,45],[45,3],[3,46],[46,42],[42,44],[44,58],[58,31],[31,29],[29,21],[21,20],[20,0],[4,5],[6,5],[5,7],[5,163],[163,8],[9,10],[9,11],[11,12],[12,13],[7,9],[9,14],[14,15],[16,163],[163,10],[10,14],[17,11],[11,14],[18,19],[20,19],[19,21],[19,164],[164,8],[22,23],[22,24],[24,25],[25,26],[21,22],[22,27],[27,15],[28,164],[164,23],[23,27],[29,24],[24,27],[7,163],[163,164],[164,21],[10,8],[8,23],[30,12],[12,15],[15,25],[25,31],[8,15],[15,32],[33,48],[48,59],[59,34],[34,13],[35,49],[49,36],[36,37],[37,38],[37,39],[38,40],[39,40],[41,38],[40,42],[41,165],[165,42],[39,43],[43,55],[55,44],[45,165],[165,40],[38,165],[165,46],[47,48],[48,49],[49,50],[50,37],[37,53],[53,51],[51,32],[52,59],[59,53],[53,39],[39,54],[54,55],[55,56],[56,26],[57,34],[34,51],[51,56],[56,58],[34,60],[60,53],[59,60],[60,51],[51,43],[43,54],[54,42],[59,36],[36,50],[50,41],[30,73],[73,86],[86,61],[61,62],[17,74],[74,63],[63,64],[64,65],[64,66],[65,67],[66,67],[7,65],[67,68],[7,166],[166,68],[66,69],[69,81],[81,70],[6,166],[166,67],[65,166],[166,71],[72,73],[73,74],[74,75],[75,64],[64,79],[79,76],[76,77],[78,86],[86,79],[79,66],[66,80],[80,81],[81,82],[82,83],[84,61],[61,76],[76,82],[82,85],[61,87],[87,79],[86,87],[87,76],[76,69],[69,80],[80,68],[86,63],[63,75],[75,7],[88,102],[102,111],[111,89],[89,57],[90,103],[103,91],[91,92],[92,93],[92,94],[93,95],[94,95],[96,93],[95,97],[96,167],[167,97],[94,98],[98,108],[108,99],[100,167],[167,95],[93,167],[167,101],[84,102],[102,103],[103,104],[104,92],[92,106],[106,105],[105,52],[78,111],[111,106],[106,94],[94,107],[107,108],[108,109],[109,47],[72,89],[89,105],[105,109],[109,110],[89,112],[112,106],[111,112],[112,105],[105,98],[98,107],[107,97],[111,91],[91,104],[104,96],[113,128],[128,139],[139,114],[114,115],[116,129],[129,117],[117,118],[118,119],[118,120],[119,121],[120,121],[122,119],[121,123],[122,168],[168,123],[120,124],[124,135],[135,125],[126,168],[168,121],[119,168],[168,127],[83,128],[128,129],[129,130],[130,118],[118,133],[133,131],[131,132],[77,139],[139,133],[133,120],[120,134],[134,135],[135,136],[136,137],[62,114],[114,131],[131,136],[136,138],[114,140],[140,133],[139,140],[140,131],[131,124],[124,134],[134,123],[139,117],[117,130],[130,122],[141,142],[100,142],[142,96],[142,169],[169,143],[144,145],[144,146],[146,147],[147,115],[96,144],[144,148],[148,149],[150,169],[169,145],[145,148],[90,146],[146,148],[151,152],[153,152],[152,154],[152,170],[170,143],[155,156],[155,157],[157,158],[158,137],[154,155],[155,159],[159,149],[160,170],[170,156],[156,159],[161,157],[157,159],[96,169],[169,170],[170,154],[145,143],[143,156],[88,147],[147,149],[149,158],[158,162],[143,149],[149,132]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180\n\t],\n\t\"faces_vertices\":[\n\t\t[0,18,19,20],[1,127,168,126],[2,151,152,153],[3,46,165,45],[4,71,166,6,5],[4,5,163,16],[5,6,166,7],[5,7,163],[7,65,64,75],[7,75,74,17,11,9],[7,9,10,163],[7,166,65],[8,10,14,15],[8,15,27,23],[8,23,164],[8,164,163],[8,163,10],[9,11,14],[9,14,10],[11,17,74,73,30,12],[11,12,15,14],[12,30,73,72,89,57,34,13],[12,13,34,51,32,15],[15,32,51,56,26,25],[15,25,24,27],[16,163,164,28],[18,28,164,19],[19,164,21],[19,21,20],[21,164,23,22],[21,22,24,29],[22,23,27],[22,27,24],[24,25,31,29],[25,26,56,58,31],[33,35,49,48],[33,48,47,109,110],[34,57,89,105,52,59],[34,59,60],[34,60,51],[35,41,50,49],[36,49,50],[36,50,37],[36,37,53,59],[36,59,48,49],[37,50,41,38],[37,38,40,39],[37,39,53],[38,41,165],[38,165,40],[39,40,42,54],[39,54,43],[39,43,51,53],[40,165,42],[41,45,165],[42,165,46],[42,44,55,54],[43,54,55],[43,55,56,51],[44,58,56,55],[47,48,59,52,105,109],[51,60,53],[53,60,59],[61,62,114,115,147,88,102,84],[61,84,102,111,78,86],[61,86,87],[61,87,76],[61,76,77,139,114,62],[63,86,73,74],[63,74,75],[63,75,64],[63,64,79,86],[64,65,67,66],[64,66,79],[65,166,67],[66,69,76,79],[66,67,68,80],[66,80,69],[67,166,68],[68,70,81,80],[68,166,71],[69,80,81],[69,81,82,76],[70,85,82,81],[72,73,86,78,111,89],[76,87,79],[76,82,83,128,139,77],[79,87,86],[82,85,113,128,83],[88,147,146,90,103,102],[89,111,112],[89,112,105],[90,146,144,96,104,103],[91,103,104],[91,104,92],[91,92,106,111],[91,111,102,103],[92,104,96,93],[92,93,95,94],[92,94,106],[93,96,167],[93,167,95],[94,95,97,107],[94,107,98],[94,98,105,106],[95,167,97],[96,169,142],[96,142,100,167],[96,144,145,169],[97,99,108,107],[97,167,101],[98,107,108],[98,108,109,105],[99,110,109,108],[100,142,141,101,167],[105,112,106],[106,112,111],[113,116,129,128],[114,131,132,149,147,115],[114,139,140],[114,140,131],[116,122,130,129],[117,118,133,139],[117,139,128,129],[117,129,130],[117,130,118],[118,120,133],[118,130,122,119],[118,119,121,120],[119,168,121],[119,122,168],[120,134,124],[120,124,131,133],[120,121,123,134],[121,168,123],[122,126,168],[123,125,135,134],[123,168,127],[124,135,136,131],[124,134,135],[125,138,136,135],[131,136,137,158,149,132],[131,140,133],[133,140,139],[136,138,162,158,137],[141,142,169,150],[143,156,170],[143,170,169],[143,169,145],[143,145,148,149],[143,149,159,156],[144,146,148],[144,148,145],[146,147,149,148],[149,158,157,159],[150,169,170,160],[151,160,170,152],[152,170,154],[152,154,153],[154,170,156,155],[154,155,157,161],[155,156,159],[155,159,157],[157,158,162,161]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0,0],[6,0,0],[6,4,0],[0,4,0],[1,0,0],[1,1,0],[2,1,0],[2,0,0],[1,1,0],[1,1,0],[1,2,0],[1,2,0],[1,1,0],[1,2,0],[1,2,1],[1,1,2],[2,0,0],[2,2,0],[1,0,0],[1,1,0],[0,1,0],[0,0,0],[1,1,0],[1,2,0],[1,2,0],[1,1,0],[1,2,0],[1,2,1],[0,0,0],[0,2,0],[2,1,0],[0,1,0],[1,2,2],[1,4,0],[1,3,0],[2,4,0],[2,3,1],[2,3,0],[1,4,0],[1,2,0],[0,3,0],[0,4,0],[0,4,0],[1,2,1],[0,2,0],[1,4,0],[0,3,0],[2,3,0],[1,3,0],[2,3,0],[1,3,0],[1,3,2],[2,3,2],[1,3,0],[1,3,0],[1,2,0],[1,3,0],[2,3,0],[0,3,0],[1,3,2],[2,3,1],[3,1,0],[4,1,0],[3,2,1],[3,2,0],[2,1,0],[4,1,0],[3,0,0],[2,0,0],[4,1,1],[4,0,0],[3,0,0],[3,2,0],[3,1,0],[3,2,0],[3,1,0],[3,1,2],[4,1,2],[3,2,2],[3,1,0],[3,1,0],[4,1,0],[3,1,0],[4,1,0],[3,2,0],[3,0,0],[3,1,2],[3,2,1],[4,3,0],[3,3,0],[4,2,0],[3,2,1],[3,2,0],[4,3,0],[2,3,0],[3,4,0],[4,4,0],[4,4,0],[2,3,1],[2,4,0],[4,3,0],[3,4,0],[3,3,0],[3,2,0],[3,3,0],[3,3,2],[3,3,0],[3,3,0],[2,3,0],[3,3,0],[3,4,0],[3,3,2],[3,2,1],[5,0,0],[5,1,0],[5,2,0],[4,0,0],[4,1,1],[4,1,0],[5,0,0],[5,2,0],[6,1,0],[6,0,0],[6,0,0],[5,2,1],[6,2,0],[5,0,0],[6,1,0],[5,1,0],[4,1,0],[5,1,0],[5,1,2],[5,2,2],[5,1,0],[5,1,0],[5,2,0],[5,1,0],[5,2,0],[6,1,0],[5,1,2],[4,1,1],[5,4,0],[5,3,0],[5,3,0],[5,3,0],[5,2,0],[5,2,0],[5,3,0],[5,2,1],[5,3,2],[4,4,0],[5,4,0],[5,3,0],[6,3,0],[6,4,0],[5,3,0],[5,2,0],[5,2,0],[5,3,0],[5,2,1],[6,4,0],[6,2,0],[6,3,0],[2,2,0],[0,2,0],[1,3,0],[3,1,0],[3,3,0],[5,1,0],[4,2,0],[6,2,0]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/maze-u.fold",
    "content": "{\n\t\"file_spec\":1.1,\n\t\"file_creator\":\"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"file_title\": \"Maze Folding\",\n\t\"file_author\": \"Demaine, Demaine, Ku\",\n\t\"vertices_coords\":[\n\t\t[0,0],[12,0],[12,12],[0,12],[0,5],[5,5],[6,5],[0,4],[2,4],[2,3],[1,2],[3,2],[2,1],[0,2],[2,0],[4,2],[4,0],[0,1],[1,0],[1,6],[1,5],[1,4],[1,3],[5,3],[6,3],[3,6],[3,3],[3,1],[4,1],[5,1],[6,1],[5,6],[5,0],[3,5],[4,4],[1,12],[1,11],[0,11],[0,10],[3,9],[1,9],[2,9],[1,8],[1,7],[2,8],[3,7],[2,12],[0,8],[5,12],[5,11],[6,11],[6,10],[5,9],[4,9],[5,8],[5,7],[4,8],[4,12],[6,8],[0,7],[6,7],[7,0],[7,5],[7,6],[8,0],[8,2],[9,2],[10,1],[10,3],[11,2],[10,0],[12,2],[10,4],[12,4],[11,0],[12,1],[7,1],[8,1],[9,1],[9,5],[9,6],[9,3],[11,3],[11,4],[11,5],[11,6],[12,5],[7,3],[8,4],[7,12],[7,11],[9,9],[7,9],[8,9],[7,8],[7,7],[8,8],[9,7],[8,12],[11,12],[11,11],[12,11],[12,10],[11,9],[10,9],[11,8],[11,7],[10,8],[10,12],[12,8],[12,7],[1,1],[2,10],[4,10],[11,1],[8,10],[10,10]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,18],[18,14],[14,16],[16,32],[32,61],[61,64],[64,70],[70,74],[74,1],[1,75],[75,71],[71,73],[73,86],[86,110],[110,109],[109,102],[102,101],[101,2],[2,99],[99,108],[108,98],[98,89],[89,48],[48,57],[57,46],[46,35],[35,3],[3,37],[37,38],[38,47],[47,59],[59,4],[4,7],[7,13],[13,17],[17,0],[4,20],[20,33],[33,5],[5,6],[7,21],[21,8],[8,9],[9,10],[9,11],[10,12],[11,12],[13,10],[12,14],[13,111],[111,14],[11,15],[15,28],[28,16],[17,111],[111,12],[10,111],[111,18],[19,20],[20,21],[21,22],[22,9],[9,26],[26,23],[23,24],[25,33],[33,26],[26,11],[11,27],[27,28],[28,29],[29,30],[31,5],[5,23],[23,29],[29,32],[5,34],[34,26],[33,34],[34,23],[23,15],[15,27],[27,14],[33,8],[8,22],[22,13],[35,36],[37,36],[36,38],[36,112],[112,39],[40,41],[40,42],[42,43],[43,19],[38,40],[40,44],[44,45],[46,112],[112,41],[41,44],[47,42],[42,44],[48,49],[50,49],[49,51],[49,113],[113,39],[52,53],[52,54],[54,55],[55,31],[51,52],[52,56],[56,45],[57,113],[113,53],[53,56],[58,54],[54,56],[38,112],[112,113],[113,51],[41,39],[39,53],[59,43],[43,45],[45,55],[55,60],[39,45],[45,25],[61,76],[76,87],[87,62],[62,63],[64,77],[77,65],[65,66],[66,67],[66,68],[67,69],[68,69],[70,67],[69,71],[70,114],[114,71],[68,72],[72,83],[83,73],[74,114],[114,69],[67,114],[114,75],[30,76],[76,77],[77,78],[78,66],[66,81],[81,79],[79,80],[24,87],[87,81],[81,68],[68,82],[82,83],[83,84],[84,85],[6,62],[62,79],[79,84],[84,86],[62,88],[88,81],[87,88],[88,79],[79,72],[72,82],[82,71],[87,65],[65,78],[78,70],[89,90],[50,90],[90,51],[90,115],[115,91],[92,93],[92,94],[94,95],[95,63],[51,92],[92,96],[96,97],[98,115],[115,93],[93,96],[58,94],[94,96],[99,100],[101,100],[100,102],[100,116],[116,91],[103,104],[103,105],[105,106],[106,85],[102,103],[103,107],[107,97],[108,116],[116,104],[104,107],[109,105],[105,107],[51,115],[115,116],[116,102],[93,91],[91,104],[60,95],[95,97],[97,106],[106,110],[91,97],[97,80]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,-180,90,-90,90,-90,-90,-90,-180,-180,-180,-90,-90,90,-90,90,90,90,-180,-180,180,180,180,-180,180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-180,-180,180,180,180,180,180,180,180,180,180,90,90,-180,-180,-180,-180,-180,-180\n\t],\n\t\"faces_vertices\":[\n\t\t[0,18,111,17],[1,75,114,74],[2,99,100,101],[3,37,36,35],[4,20,19,43,59],[4,7,21,20],[5,6,62,63,95,60,55,31],[5,31,55,45,25,33],[5,33,34],[5,34,23],[5,23,24,87,62,6],[7,13,22,21],[8,33,20,21],[8,21,22],[8,22,9],[8,9,26,33],[9,22,13,10],[9,10,12,11],[9,11,26],[10,13,111],[10,111,12],[11,15,23,26],[11,12,14,27],[11,27,15],[12,111,14],[13,17,111],[14,16,28,27],[14,111,18],[15,27,28],[15,28,29,23],[16,32,29,28],[19,20,33,25,45,43],[23,34,26],[23,29,30,76,87,24],[26,34,33],[29,32,61,76,30],[35,36,112,46],[36,37,38],[36,38,112],[38,47,42,40],[38,40,41,112],[39,53,113],[39,113,112],[39,112,41],[39,41,44,45],[39,45,56,53],[40,42,44],[40,44,41],[42,47,59,43],[42,43,45,44],[45,55,54,56],[46,112,113,57],[48,57,113,49],[48,49,50,90,89],[49,113,51],[49,51,90,50],[51,115,90],[51,113,53,52],[51,52,54,58,94,92],[51,92,93,115],[52,53,56],[52,56,54],[54,55,60,95,94,58],[61,64,77,76],[62,79,80,97,95,63],[62,87,88],[62,88,79],[64,70,78,77],[65,66,81,87],[65,87,76,77],[65,77,78],[65,78,66],[66,68,81],[66,78,70,67],[66,67,69,68],[67,114,69],[67,70,114],[68,82,72],[68,72,79,81],[68,69,71,82],[69,114,71],[70,74,114],[71,73,83,82],[71,114,75],[72,83,84,79],[72,82,83],[73,86,84,83],[79,84,85,106,97,80],[79,88,81],[81,88,87],[84,86,110,106,85],[89,90,115,98],[91,104,116],[91,116,115],[91,115,93],[91,93,96,97],[91,97,107,104],[92,94,96],[92,96,93],[94,95,97,96],[97,106,105,107],[98,115,116,108],[99,108,116,100],[100,116,102],[100,102,101],[102,116,104,103],[102,103,105,109],[103,104,107],[103,107,105],[105,106,110,109]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0,0],[4,0,0],[4,4,0],[0,4,0],[0,1,0],[1,1,0],[2,1,0],[0,2,0],[1,2,1],[1,2,0],[0,1,0],[2,1,0],[1,0,0],[0,0,0],[0,0,0],[2,1,1],[2,0,0],[0,1,0],[1,0,0],[1,2,0],[1,1,0],[1,2,0],[1,1,0],[1,1,2],[2,1,2],[1,2,2],[1,1,0],[1,1,0],[2,1,0],[1,1,0],[2,1,0],[1,2,0],[1,0,0],[1,1,2],[1,2,1],[1,4,0],[1,3,0],[0,3,0],[0,4,0],[1,3,0],[1,3,0],[1,2,0],[1,2,0],[1,3,0],[1,2,1],[1,3,2],[0,4,0],[0,2,0],[1,4,0],[1,3,0],[2,3,0],[2,4,0],[1,3,0],[1,2,0],[1,2,0],[1,3,0],[1,2,1],[2,4,0],[2,2,0],[0,3,0],[2,3,0],[3,0,0],[3,1,0],[3,2,0],[2,0,0],[2,1,1],[2,1,0],[3,0,0],[3,2,0],[4,1,0],[4,0,0],[4,0,0],[3,2,1],[4,2,0],[3,0,0],[4,1,0],[3,1,0],[2,1,0],[3,1,0],[3,1,2],[3,2,2],[3,1,0],[3,1,0],[3,2,0],[3,1,0],[3,2,0],[4,1,0],[3,1,2],[2,1,1],[3,4,0],[3,3,0],[3,3,0],[3,3,0],[3,2,0],[3,2,0],[3,3,0],[3,2,1],[3,3,2],[2,4,0],[3,4,0],[3,3,0],[4,3,0],[4,4,0],[3,3,0],[3,2,0],[3,2,0],[3,3,0],[3,2,1],[4,4,0],[4,2,0],[4,3,0],[1,1,0],[0,2,0],[2,2,0],[3,1,0],[2,2,0],[4,2,0]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/moosers-train-carriage-fourth.fold",
    "content": "{\"file_spec\":1.2,\"file_creator\":\"Rabbit Ear\",\"file_author\":\"Kraft\",\"file_title\":\"Mooser's Train\",\"file_description\":\"Emmanuel Mooser, modified by William Gardner\",\"file_classes\":[\"singleModel\"],\"vertices_coords\":[[20,0],[16,0],[15,0],[13,0],[11,0],[9,0],[8,0],[4,0],[2,0],[0,0],[0,4],[0,8],[0,10],[0,12],[0,14],[0,16],[0,20],[13,1],[11,1],[2,4],[2,8],[2,10],[2,12],[2,14],[2,16],[2,20],[8,12],[12,16],[8,4],[12,8],[15,3],[13,3],[11,3],[9,3],[4,20],[4,16],[4,12],[4,8],[4,4],[12,4],[16,4],[20,4],[20,8],[8,8],[12,20],[20,16],[20,20]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[17,18],[8,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24,25],[26,27],[28,29],[30,31],[31,32],[32,33],[34,35],[35,36],[36,37],[37,38],[38,7],[10,19],[19,38],[38,28],[28,39],[40,41],[21,36],[32,39],[42,29],[29,43],[43,37],[37,20],[20,11],[6,28],[28,43],[43,26],[40,30],[30,17],[23,35],[21,37],[28,33],[33,18],[5,33],[12,21],[4,18],[18,32],[31,39],[36,23],[26,36],[36,22],[22,13],[44,27],[27,29],[29,39],[3,17],[17,31],[14,23],[35,26],[29,40],[2,30],[15,24],[24,35],[35,27],[27,45],[1,40],[46,45],[45,42],[42,41],[41,0],[16,25],[25,34],[34,44],[44,46]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,90,-90,90,-90,-90,90,90,-90,180,-180,-90,90,-90,180,90,-90,-90,90,180,180,180,180,180,180,-180,-90,90,-90,-180,-180,-180,180,180,180,180,-180,180,180,180,90,180,90,-90,-180,-180,-180,-180,-180,-90,-180,-180,90,-90,-180,180,180,90,180,180,-90,-90,-180,0,0,0,0,0,0,0,0],\"vertices_edges\":[[81,0],[0,77,1],[1,72,2],[2,67,3],[3,57,4],[4,55,5],[5,46,6],[6,33,7],[7,17,8],[8,9],[34,10,9],[45,11,10],[56,12,11],[63,13,12],[69,14,13],[73,15,14],[82,15],[50,68,16,67],[16,58,54,57],[35,18,34,17],[44,19,45,18],[39,20,56,19,52],[62,21,63,20],[51,22,69,21,60],[74,23,73,22],[83,82,23],[24,70,61,48],[76,64,75,24,65],[37,25,47,36,46,53],[41,65,42,25,66,71],[49,26,50,72],[26,59,27,68],[27,40,28,58],[28,53,55,54],[84,83,29],[75,29,74,51,30,70],[61,30,60,62,39,31],[43,31,52,44,32],[36,32,35,33],[66,37,40,59],[38,71,49,77],[80,38,81],[79,41,80],[42,48,43,47],[85,84,64],[78,76,79],[85,78]],\"vertices_vertices\":[[41,1],[0,40,2],[1,30,3],[2,17,4],[3,18,5],[4,33,6],[5,28,7],[6,38,8],[7,19,9],[8,10],[19,11,9],[20,12,10],[21,13,11],[22,14,12],[23,15,13],[24,16,14],[25,15],[30,31,18,3],[17,32,33,4],[38,20,10,8],[37,21,11,19],[36,22,12,20,37],[36,23,13,21],[35,24,14,22,36],[35,25,15,23],[34,16,24],[27,35,36,43],[45,44,35,26,29],[39,29,43,38,6,33],[42,27,43,28,39,40],[40,31,17,2],[30,39,32,17],[31,39,33,18],[32,28,5,18],[44,25,35],[27,34,24,23,36,26],[26,35,23,22,21,37],[43,36,21,20,38],[28,37,19,7],[29,28,32,31],[41,29,30,1],[42,40,0],[45,29,41],[29,26,37,28],[46,34,27],[46,27,42],[44,45]],\"faces_vertices\":[[0,41,40,1],[1,40,30,2],[2,30,17,3],[3,17,18,4],[4,18,33,5],[5,33,28,6],[6,28,38,7],[7,38,19,8],[8,19,10,9],[10,19,20,11],[11,20,21,12],[12,21,22,13],[13,22,23,14],[14,23,24,15],[15,24,25,16],[17,30,31],[17,31,32,18],[18,32,33],[19,38,37,20],[20,37,21],[21,36,22],[21,37,36],[22,36,23],[23,35,24],[23,36,35],[24,35,34,25],[26,27,35],[26,35,36],[26,36,37,43],[26,43,29,27],[27,45,46,44],[27,44,34,35],[27,29,42,45],[28,39,29],[28,29,43],[28,43,37,38],[28,33,32,39],[29,39,31,30,40],[29,40,41,42],[31,39,32]],\"faces_edges\":[[81,38,77,0],[77,49,72,1],[72,50,67,2],[67,16,57,3],[57,54,55,4],[55,53,46,5],[46,36,33,6],[33,35,17,7],[17,34,9,8],[34,18,45,10],[45,19,56,11],[56,20,63,12],[63,21,69,13],[69,22,73,14],[73,23,82,15],[50,26,68],[68,27,58,16],[58,28,54],[35,32,44,18],[44,52,19],[39,62,20],[52,31,39],[62,60,21],[51,74,22],[60,30,51],[74,29,83,23],[24,75,70],[70,30,61],[61,31,43,48],[48,42,65,24],[76,78,85,64],[64,84,29,75],[65,41,79,76],[37,66,25],[25,42,47],[47,43,32,36],[53,28,40,37],[66,59,26,49,71],[71,38,80,41],[59,40,27]],\"vertices_faces\":[[0,null],[0,1,null],[1,2,null],[2,3,null],[3,4,null],[4,5,null],[5,6,null],[6,7,null],[7,8,null],[8,null],[9,null,8],[10,null,9],[11,null,10],[12,null,11],[13,null,12],[14,null,13],[null,14],[15,16,3,2],[16,17,4,3],[18,9,8,7],[19,10,9,18],[20,11,10,19,21],[22,12,11,20],[23,13,12,22,24],[25,14,13,23],[null,14,25],[26,27,28,29],[30,31,26,29,32],[33,34,35,6,5,36],[32,29,34,33,37,38],[37,15,2,1],[37,39,16,15],[39,36,17,16],[36,5,4,17],[null,25,31],[31,25,23,24,27,26],[27,24,22,20,21,28],[28,21,19,18,35],[35,18,7,6],[33,36,39,37],[38,37,1,0],[38,0,null],[32,38,null],[29,28,35,34],[null,31,30],[30,32,null],[30,null]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[6],[7],[8],[8],[9],[10],[11],[12],[13],[14],[3,16],[7,8],[9,18],[10,19],[11,20],[12,22],[13,23],[14,25],[26,29],[33,34],[15,37],[16,39],[17,36],[25,31],[24,27],[21,28],[18,35],[6,7],[8,9],[7,18],[6,35],[33,36],[0,38],[20,21],[36,39],[32,38],[29,34],[28,35],[18,19],[9,10],[5,6],[34,35],[28,29],[1,37],[2,15],[23,24],[19,21],[5,36],[4,17],[4,5],[10,11],[3,4],[16,17],[37,39],[22,24],[27,28],[20,22],[11,12],[30,31],[29,32],[33,37],[2,3],[15,16],[12,13],[26,27],[37,38],[1,2],[13,14],[23,25],[26,31],[30,32],[0,1],[30],[32],[38],[0],[14],[25],[31],[30]],\"faces_faces\":[[null,38,1,null],[0,37,2,null],[1,15,3,null],[2,16,4,null],[3,17,5,null],[4,36,6,null],[5,35,7,null],[6,18,8,null],[7,9,null,null],[8,18,10,null],[9,19,11,null],[10,20,12,null],[11,22,13,null],[12,23,14,null],[13,25,null,null],[2,37,16],[15,39,17,3],[16,36,4],[7,35,19,9],[18,21,10],[21,22,11],[19,28,20],[20,24,12],[24,25,13],[22,27,23],[23,31,null,14],[29,31,27],[26,24,28],[27,21,35,29],[28,34,32,26],[32,null,null,31],[30,null,25,26],[29,38,null,30],[36,37,34],[33,29,35],[34,28,18,6],[5,17,39,33],[33,39,15,1,38],[37,0,null,32],[37,36,16]]}"
  },
  {
    "path": "tests/files/fold/moosers-train-carriage.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"file_title\": \"Mooser's Train\",\n\t\"file_description\": \"Emmanuel Mooser, modified by William Gardner\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[40,0],[38,0],[36,0],[32,0],[31,0],[29,0],[27,0],[25,0],[24,0],[16,0],[15,0],[13,0],[11,0],[9,0],[8,0],[4,0],[2,0],[0,0],[0,4],[0,8],[0,10],[0,12],[0,14],[0,16],[0,24],[0,26],[0,28],[0,30],[0,32],[0,36],[0,40],[29,1],[27,1],[13,1],[11,1],[2,4],[2,8],[2,10],[2,12],[2,14],[2,16],[2,24],[2,26],[2,28],[2,30],[2,32],[2,36],[2,40],[8,12],[12,16],[28,32],[32,36],[8,4],[12,8],[28,24],[32,28],[31,3],[29,3],[27,3],[25,3],[15,3],[13,3],[11,3],[9,3],[4,40],[4,36],[4,32],[4,28],[4,24],[4,16],[4,12],[4,8],[4,4],[12,4],[16,4],[24,4],[28,4],[32,4],[36,4],[38,4],[40,4],[28,36],[29,37],[36,28],[38,30],[40,8],[38,8],[36,8],[32,8],[28,8],[8,8],[8,28],[8,32],[8,36],[8,40],[38,26],[36,24],[24,36],[25,37],[27,39],[9,37],[9,40],[38,10],[40,10],[11,37],[11,39],[11,40],[40,12],[38,12],[36,12],[32,12],[12,36],[12,32],[12,24],[13,37],[13,39],[13,40],[38,14],[40,14],[36,16],[16,36],[15,37],[15,40],[28,16],[38,16],[40,16],[16,40],[38,24],[40,24],[24,40],[25,40],[40,26],[27,37],[27,40],[40,28],[38,28],[29,39],[29,40],[40,30],[31,37],[31,40],[40,32],[38,32],[36,32],[32,32],[32,40],[36,40],[36,36],[38,36],[40,36],[38,40],[40,40]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24,25],[25,26],[26,27],[27,28],[28,29],[29,30],[31,32],[33,34],[16,35],[35,36],[36,37],[37,38],[38,39],[39,40],[40,41],[41,42],[42,43],[43,44],[44,45],[45,46],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[57,58],[58,59],[60,61],[61,62],[62,63],[64,65],[65,66],[66,67],[67,68],[68,69],[69,70],[70,71],[71,72],[72,15],[18,35],[35,72],[72,52],[52,73],[74,75],[76,77],[77,78],[78,79],[79,80],[37,70],[81,82],[62,73],[83,84],[85,86],[86,87],[87,88],[88,89],[89,53],[53,90],[90,71],[71,36],[36,19],[14,52],[52,90],[90,48],[91,92],[92,93],[93,94],[95,96],[74,60],[60,33],[39,69],[97,98],[98,99],[37,71],[52,63],[63,34],[13,63],[100,101],[20,37],[102,103],[12,34],[34,62],[104,105],[105,106],[61,73],[70,39],[107,108],[108,109],[109,110],[48,70],[70,38],[38,21],[111,112],[112,113],[113,49],[49,53],[53,73],[11,33],[33,61],[114,115],[115,116],[22,39],[117,118],[119,110],[68,91],[89,75],[112,120],[69,48],[53,74],[10,60],[121,122],[23,40],[40,69],[69,49],[49,123],[123,119],[119,124],[124,125],[9,74],[120,126],[58,76],[114,111],[117,109],[42,67],[42,68],[75,59],[59,32],[77,56],[56,31],[93,100],[100,105],[102,87],[44,66],[44,67],[76,57],[24,41],[41,68],[68,113],[113,54],[54,96],[96,127],[127,128],[8,75],[97,129],[7,59],[98,130],[77,89],[113,91],[25,42],[95,131],[6,32],[32,58],[132,99],[99,133],[134,135],[135,83],[83,55],[91,67],[67,43],[43,26],[81,50],[50,54],[54,123],[123,89],[89,76],[5,31],[31,57],[82,136],[136,137],[27,44],[84,138],[4,56],[139,140],[93,112],[123,110],[141,142],[142,143],[143,144],[144,50],[50,112],[112,92],[92,66],[66,45],[45,28],[3,77],[77,88],[88,110],[55,144],[144,51],[51,145],[104,111],[109,102],[146,147],[147,143],[143,83],[83,96],[96,119],[119,109],[109,87],[87,78],[78,2],[29,46],[46,65],[65,93],[93,111],[120,97],[81,51],[51,147],[147,148],[148,149],[117,119],[120,121],[121,115],[139,82],[82,132],[132,98],[121,114],[114,104],[104,100],[1,79],[79,86],[86,102],[102,108],[108,117],[117,124],[124,127],[127,95],[95,135],[135,84],[84,142],[142,148],[148,150],[136,99],[115,105],[151,150],[150,146],[146,145],[145,140],[140,137],[137,133],[133,130],[130,129],[129,126],[126,122],[122,116],[116,106],[106,101],[101,94],[94,64],[64,47],[47,30],[151,149],[149,141],[141,138],[138,134],[134,131],[131,128],[128,125],[125,118],[118,107],[107,103],[103,85],[85,80],[80,0],[96,55],[50,97],[132,81],[83,95],[84,143],[51,139],[139,136]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,90,90,-90,90,-90,-90,90,90,-90,90,90,-90,-90,90,-90,180,-180,-180,180,-90,90,-90,-90,90,-90,90,-90,-90,90,180,90,-90,-90,90,180,180,180,180,180,180,180,180,180,180,-180,-180,180,-180,-180,-90,90,-90,90,-90,-180,-180,-180,180,180,180,180,-180,-180,180,180,-180,180,180,180,180,180,90,90,180,180,90,-90,-90,90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-180,-180,90,-90,-90,90,-180,-180,180,180,180,180,180,180,90,90,180,180,-90,-90,-90,180,180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,180,180,180,180,180,-180,180,180,-90,-90,-90,180,180,-180,-180,90,90,-180,180,-180,-180,90,-90,-90,90,-180,-180,-180,-180,-180,-180,-180,-180,-90,-180,-180,90,-90,-90,90,180,180,90,90,-180,180,-180,-180,-90,90,-90,90,-90,-180,-180,-180,180,180,180,180,-180,-180,180,90,-90,-90,90,180,90,-90,-90,90,180,180,180,180,180,180,180,180,180,-180,180,180,-90,90,-90,-90,90,-90,-90,90,-90,-90,90,90,-90,90,90,-90,-90,90,-90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,180,-180,-180,180,180,180\n\t],\n\t\"faces_vertices\": [\n\t\t[0,80,79,1],[1,79,78,2],[2,78,77,3],[3,77,56,4],[4,56,31,5],[5,31,32,6],[6,32,59,7],[7,59,75,8],[8,75,74,9],[9,74,60,10],[10,60,33,11],[11,33,34,12],[12,34,63,13],[13,63,52,14],[14,52,72,15],[15,72,35,16],[16,35,18,17],[18,35,36,19],[19,36,37,20],[20,37,38,21],[21,38,39,22],[22,39,40,23],[23,40,41,24],[24,41,42,25],[25,42,43,26],[26,43,44,27],[27,44,45,28],[28,45,46,29],[29,46,47,30],[31,56,57],[31,57,58,32],[32,58,59],[33,60,61],[33,61,62,34],[34,62,63],[35,72,71,36],[36,71,37],[37,70,38],[37,71,70],[38,70,39],[39,69,40],[39,70,69],[40,69,68,41],[41,68,42],[42,67,43],[42,68,67],[43,67,44],[44,66,45],[44,67,66],[45,66,65,46],[46,65,64,47],[48,49,69],[48,69,70],[48,70,71,90],[48,90,53,49],[49,123,54,113],[49,113,68,69],[49,53,89,123],[50,144,51],[50,51,81],[50,81,132,98,97],[50,97,120,112],[50,112,113,54],[50,54,55,144],[51,147,146,145],[51,145,140,139],[51,139,82,81],[51,144,143,147],[52,73,53],[52,53,90],[52,90,71,72],[52,63,62,73],[53,73,61,60,74],[53,74,75,89],[54,96,55],[54,123,119,96],[55,83,143,144],[55,96,83],[56,77,76,57],[57,76,58],[58,76,89,75,59],[61,73,62],[64,65,93,94],[65,66,92,93],[66,67,91,92],[67,68,91],[68,113,91],[76,77,89],[77,78,87,88],[77,88,89],[78,79,86,87],[79,80,85,86],[81,82,132],[82,139,136],[82,136,99,132],[83,135,84],[83,84,143],[83,96,95],[83,95,135],[84,138,141,142],[84,142,143],[84,135,134,138],[85,103,102,86],[86,102,87],[87,102,109],[87,109,110,88],[88,110,123,89],[91,113,112,92],[92,112,93],[93,111,104,100],[93,100,101,94],[93,112,111],[95,131,134,135],[95,96,127],[95,127,128,131],[96,119,124,127],[97,98,130,129],[97,129,126,120],[98,132,99],[98,99,133,130],[99,136,137,133],[100,104,105],[100,105,106,101],[102,103,107,108],[102,108,109],[104,114,115,105],[104,111,114],[105,115,116,106],[107,118,117,108],[108,117,109],[109,117,119],[109,119,110],[110,119,123],[111,112,120,121,114],[114,121,115],[115,121,122,116],[117,118,125,124],[117,124,119],[120,126,122,121],[124,125,128,127],[136,139,140,137],[141,149,148,142],[142,148,147,143],[146,147,148,150],[148,149,151,150]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/moosers-train-engine.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"file_title\": \"Mooser's Train\",\n\t\"file_description\": \"Emmanuel Mooser, modified by William Gardner\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[48,0],[44,0],[40,0],[39,0],[37,0],[35,0],[33,0],[32,0],[28,0],[27,0],[25,0],[23,0],[21,0],[20,0],[16,0],[15,0],[13,0],[11,0],[9,0],[8,0],[4,0],[2,0],[0,0],[0,40],[0,36],[0,32],[0,30],[0,28],[0,26],[0,24],[0,16],[0,14],[0,12],[0,10],[0,8],[0,4],[37,37],[36,36],[24,24],[22.828427124746156,22.828427124746202],[37,1],[35,1],[25,1],[23,1],[13,1],[11,1],[2,4],[2,8],[2,10],[2,12],[2,14],[2,16],[2,24],[2,26],[2,28],[2,30],[2,32],[2,36],[2,40],[24,20],[21.171572875253787,17.171572875253794],[20,16],[12,8],[8,4],[35,39],[33,37],[32,36],[24,28],[20,24],[12,16],[8,12],[39,3],[37,3],[35,3],[33,3],[27,3],[25,3],[23,3],[21,3],[15,3],[13,3],[11,3],[9,3],[4,4],[12,4],[16,4],[20,4],[24,4],[28,4],[32,4],[36,4],[40,4],[44,4],[48,4],[4,40],[4,36],[4,32],[4,28],[4,24],[4,16],[4,12],[4,8],[28,36],[24,32],[20,28],[16,24],[42,6],[44,6],[46,6],[48,8],[46,8],[44,8],[42,8],[40,8],[36,8],[24,8],[20,8],[16,8],[8,8],[8,28],[8,32],[8,36],[8,40],[25,37],[24,36],[48,36],[46,34],[44,32],[38,26],[37.41421356237312,25.41421356237312],[36,24],[9,37],[9,40],[36.82842712474621,22],[37.41421356237312,22.58578643762688],[38.82842712474631,24],[11,37],[11,39],[11,40],[23,39],[21,37],[20,36],[16,32],[36,20],[38,22],[22.343145750507574,24],[20,12],[24,12],[36,12],[40,12],[12,36],[12,32],[12,24],[13,37],[13,39],[13,40],[42,14],[38,14],[36.82842712474621,14],[16,36],[36,16],[37.41421356237312,17.41421356237312],[38,18],[22.343145750507574,16],[22.828427124746156,17.171572875253794],[15,37],[15,40],[44,16],[38.82842712474631,16],[24,16],[16,16],[16,40],[37.41421356237312,14.58578643762688],[46,18],[38.82842712474631,18],[36.82842712474621,18],[48,20],[20,32],[20,40],[21,40],[21.17157287525379,22.828427124746202],[46,22],[38.82842712474631,22],[23,37],[23,40],[44,24],[36.82842712474621,26],[25,39],[25,40],[42,26],[27,37],[27,40],[36,28],[40,28],[28,40],[48,32],[46,32],[42,32],[40,32],[36,32],[32,40],[33,40],[42,34],[44,34],[35,37],[35,40],[40,36],[44,36],[39,37],[37,39],[37,40],[39,40],[48,40],[44,40],[40,40]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[23,24],[24,25],[25,26],[26,27],[27,28],[28,29],[29,30],[30,31],[31,32],[32,33],[33,34],[34,35],[35,22],[36,37],[38,39],[40,41],[42,43],[44,45],[21,46],[46,47],[47,48],[48,49],[49,50],[50,51],[51,52],[52,53],[53,54],[54,55],[55,56],[56,57],[57,58],[59,60],[60,61],[62,63],[64,65],[65,66],[67,68],[69,70],[71,72],[72,73],[73,74],[75,76],[76,77],[77,78],[79,80],[80,81],[81,82],[35,46],[46,83],[83,63],[63,84],[85,86],[86,87],[88,89],[90,91],[91,92],[92,93],[94,95],[95,96],[96,97],[97,98],[98,99],[99,100],[100,101],[101,83],[83,20],[102,103],[103,104],[104,105],[100,48],[81,84],[106,107],[107,108],[109,110],[110,111],[111,112],[112,113],[113,114],[114,115],[115,116],[116,117],[117,62],[62,118],[118,101],[101,47],[47,34],[19,63],[63,118],[118,70],[119,120],[120,121],[121,122],[123,124],[99,50],[125,126],[126,127],[128,129],[129,130],[85,79],[79,44],[45,82],[82,63],[101,48],[18,82],[131,132],[33,48],[133,134],[134,135],[17,45],[45,81],[136,137],[137,138],[80,84],[100,50],[139,140],[140,141],[141,142],[143,144],[67,145],[32,49],[49,100],[100,70],[146,147],[147,148],[148,149],[150,151],[151,152],[152,69],[69,62],[62,84],[16,44],[44,80],[153,154],[154,155],[156,157],[157,158],[50,31],[159,151],[119,98],[77,87],[160,161],[161,162],[85,62],[70,99],[163,164],[164,59],[15,79],[165,166],[167,168],[160,169],[169,163],[163,61],[61,170],[170,69],[69,99],[99,51],[51,30],[14,85],[117,170],[170,105],[105,142],[159,171],[158,172],[172,168],[153,150],[97,53],[157,148],[88,75],[75,42],[43,78],[78,86],[86,117],[164,60],[173,174],[174,162],[162,175],[137,131],[131,121],[96,55],[176,173],[173,167],[167,156],[156,149],[149,114],[114,89],[76,87],[98,53],[143,59],[13,86],[86,116],[116,146],[104,177],[177,141],[141,178],[12,78],[140,179],[60,180],[181,182],[182,144],[144,133],[73,90],[88,115],[115,146],[146,170],[97,55],[39,180],[11,43],[43,77],[183,139],[139,184],[185,135],[130,38],[38,145],[145,68],[68,105],[105,152],[152,98],[98,52],[52,29],[87,115],[115,147],[147,169],[169,59],[59,38],[38,67],[67,103],[103,124],[186,130],[10,42],[42,76],[123,187],[187,188],[111,106],[106,91],[91,71],[71,40],[41,74],[74,89],[147,61],[152,119],[133,143],[189,128],[128,186],[53,28],[147,163],[9,75],[190,191],[160,175],[27,54],[54,97],[97,119],[104,67],[67,192],[192,193],[8,88],[102,194],[72,90],[169,164],[148,158],[145,39],[39,59],[26,55],[59,180],[180,68],[151,121],[93,1],[195,196],[196,127],[127,197],[197,198],[198,199],[199,103],[103,177],[177,142],[142,151],[151,120],[120,96],[96,56],[56,25],[7,89],[66,200],[6,74],[65,201],[136,150],[202,203],[203,126],[5,41],[41,73],[204,64],[64,205],[24,57],[57,95],[95,121],[121,150],[159,141],[141,124],[102,66],[37,206],[206,207],[207,125],[90,114],[114,148],[148,160],[160,143],[143,130],[130,192],[192,199],[199,37],[93,108],[108,111],[157,172],[172,160],[159,165],[165,154],[208,36],[36,204],[204,65],[190,123],[123,183],[183,140],[165,153],[153,136],[136,131],[4,40],[40,72],[36,209],[209,210],[175,161],[161,168],[168,174],[174,182],[182,135],[209,64],[187,139],[154,137],[3,71],[208,211],[158,160],[143,162],[212,213],[213,214],[214,211],[211,210],[210,205],[205,201],[201,200],[200,194],[194,191],[191,188],[188,184],[184,179],[179,178],[178,171],[171,166],[166,155],[155,138],[138,132],[132,122],[122,94],[94,58],[58,23],[2,91],[91,113],[113,149],[193,198],[198,206],[206,214],[175,143],[202,197],[197,189],[156,112],[112,106],[183,124],[130,134],[134,144],[130,133],[192,186],[1,92],[92,107],[107,111],[111,167],[185,127],[127,203],[203,207],[207,213],[186,129],[129,135],[128,192],[102,190],[190,187],[126,196],[196,181],[173,110],[110,108],[212,125],[125,195],[195,176],[176,109],[109,93],[93,0],[176,181],[181,185],[185,189],[189,193],[193,199],[199,66],[204,37],[127,202],[202,206],[206,208],[208,209],[125,213]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,180,90,90,90,-90,90,-90,-90,90,90,-90,90,90,-90,-90,90,-90,-180,-180,-180,180,180,-180,180,-90,90,-90,-90,90,-90,-90,90,-90,180,180,180,180,180,180,180,180,180,180,90,-90,-90,90,180,90,-90,-90,90,180,-180,180,180,-180,-180,-180,60,-60,-90,90,-90,-180,-90,90,-90,90,-90,-180,-180,-180,180,180,180,180,-180,-180,-180,-180,180,-180,-180,180,180,180,180,180,90,90,180,-180,180,90,-90,-90,90,-180,-180,180,180,-180,-180,180,-180,-180,-180,180,135,180,-180,-180,-90,-180,-180,90,-90,-90,90,-180,180,-180,180,180,-180,-180,-180,180,180,180,180,90,90,180,-90,-180,180,-180,-90,-90,180,180,-180,-90,-90,-90,-180,-180,180,-180,-180,-180,180,180,180,180,-180,-180,-180,-180,180,180,180,180,-180,180,-180,180,-180,180,-180,-180,-90,-180,180,180,180,180,-180,90,90,180,-180,-180,180,-180,180,-180,180,180,-180,90,-90,-90,90,180,-90,-180,180,-180,-90,-90,180,180,-180,90,-90,-90,-90,-90,90,-180,180,90,-90,-90,90,180,-180,180,180,180,180,-180,180,180,-180,180,-180,180,90,90,180,-180,-180,-180,180,135,180,-180,-180,-180,180,180,180,180,180,-180,-180,-180,180,60,-60,-90,90,-90,-180,-90,90,-90,90,-90,-180,-180,-180,-180,90,90,-180,-180,-180,90,-90,-90,90,180,180,180,180,180,180,180,180,180,180,-180,90,-90,-90,-90,-90,90,-180,-180,180,-180,-180,180,180,-90,90,-90,-90,90,-90,-90,90,-90,90,-90,-90,90,-180,180,-180,180,-180,90,90,90,90,90,180,-180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,180,180,180,180,-180,180,-180,-180,-180,-180,-180,-180,-180,180,180,-30,30,-30,180,180,-30,30,-30,-180,180,-180,180,180,-180,-180,-180,-180,0,0,0,0,0,0,-180,180,-180,180,-180,180,-180,180,-180,180,180,180\n\t],\n\t\"faces_vertices\": [\n\t\t[0,93,1],[1,93,92],[1,92,91,2],[2,91,71,3],[3,71,40,4],[4,40,41,5],[5,41,74,6],[6,74,89,7],[7,89,88,8],[8,88,75,9],[9,75,42,10],[10,42,43,11],[11,43,78,12],[12,78,86,13],[13,86,85,14],[14,85,79,15],[15,79,44,16],[16,44,45,17],[17,45,82,18],[18,82,63,19],[19,63,83,20],[20,83,46,21],[21,46,35,22],[23,24,57,58],[24,25,56,57],[25,26,55,56],[26,27,54,55],[27,28,53,54],[28,29,52,53],[29,30,51,52],[30,31,50,51],[31,32,49,50],[32,33,48,49],[33,34,47,48],[34,35,46,47],[36,208,209],[36,209,64,204],[36,204,37],[36,37,206,208],[37,204,65,66,199],[37,199,198,206],[38,130,192,67],[38,67,145],[38,145,39],[38,39,59],[38,59,143,130],[39,145,68,180],[39,180,59],[40,71,72],[40,72,73,41],[41,73,74],[42,75,76],[42,76,77,43],[43,77,78],[44,79,80],[44,80,81,45],[45,81,82],[46,83,101,47],[47,101,48],[48,100,49],[48,101,100],[49,100,50],[50,99,51],[50,100,99],[51,99,98,52],[52,98,53],[53,97,54],[53,98,97],[54,97,55],[55,96,56],[55,97,96],[56,96,95,57],[57,95,94,58],[59,180,60],[59,60,164],[59,164,169],[59,169,160,143],[60,180,68,105,170,61],[60,61,163,164],[61,170,146,147],[61,147,163],[62,117,170,69],[62,69,70,118],[62,118,63],[62,63,84],[62,84,80,79,85],[62,85,86,117],[63,118,101,83],[63,82,81,84],[64,209,210,205],[64,205,201,65],[64,65,204],[65,201,200,66],[66,200,194,102],[66,102,103,199],[67,192,199,103],[67,103,104],[67,104,105,68],[67,68,145],[69,170,105,152],[69,152,98,99],[69,99,70],[70,99,100],[70,100,101,118],[71,91,90,72],[72,90,73],[73,90,114,89,74],[75,88,115,87,76],[76,87,77],[77,87,86,78],[80,84,81],[86,87,115,116],[86,116,117],[88,89,114,115],[90,91,113,114],[91,92,107,106],[91,106,112,113],[92,93,108,107],[93,109,110,108],[94,95,121,122],[95,96,120,121],[96,97,119,120],[97,98,119],[98,152,119],[102,194,191,190],[102,190,123,124,103],[103,124,141,177],[103,177,104],[104,177,142,105],[105,142,151,152],[106,107,111],[106,111,112],[107,108,111],[108,110,111],[109,176,173,110],[110,173,167,111],[111,167,156,112],[112,156,149,113],[113,149,114],[114,149,148],[114,148,147,115],[115,147,146],[115,146,116],[116,146,170,117],[119,152,151,120],[120,151,121],[121,150,136,131],[121,131,132,122],[121,151,150],[123,190,187],[123,187,139,183],[123,183,124],[124,183,140,141],[125,212,213],[125,213,207],[125,207,203,126],[125,126,196,195],[126,203,127],[126,127,196],[127,203,202],[127,202,197],[127,197,189,185],[127,185,181,196],[128,189,193,192],[128,192,186],[128,186,129],[128,129,135,185,189],[129,186,130],[129,130,134,135],[130,186,192],[130,143,133],[130,133,134],[131,136,137],[131,137,138,132],[133,144,134],[133,143,144],[134,144,182,135],[135,182,181,185],[136,153,154,137],[136,150,153],[137,154,155,138],[139,187,188,184],[139,184,179,140],[139,140,183],[140,179,178,141],[141,178,171,159],[141,159,151,142],[141,142,177],[143,160,175],[143,175,162],[143,162,174,182,144],[147,148,160,169],[147,169,163],[148,149,156,157],[148,157,158],[148,158,160],[150,151,159,165,153],[153,165,154],[154,165,166,155],[156,167,168,172,157],[157,172,158],[158,172,160],[159,171,166,165],[160,161,175],[160,172,168,161],[161,162,175],[161,168,174,162],[163,169,164],[167,173,174,168],[173,176,181,182,174],[176,195,196,181],[187,190,191,188],[189,197,198,193],[192,193,199],[193,198,199],[197,202,206,198],[202,203,207,206],[206,207,213,214],[206,214,211,208],[208,211,210,209]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/moosers-train.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"Mooser's Train\",\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"file_description\": \"Emmanuel Mooser, modified by William Gardner\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"frame_attributes\": [\"3D\"],\n\t\"vertices_coords\":[\n\t\t[14,2,4],[6,2,4],[13,2,4],[7,2,4],[13,0,4],[7,0,4],[15,0,4],[15,2,4],[14,2,4],[18,2,4],[5,0,4],[5,2,4],[6,2,4],[2,2,4],[22,4,4],[18,4,4],[22,2,4],[34,2,4],[26,2,4],[33,2,4],[27,2,4],[33,0,4],[27,0,4],[35,0,4],[35,2,4],[34,2,4],[38,2,4],[25,0,4],[25,2,4],[26,2,4],[42,4,4],[38,4,4],[42,2,4],[45,2,4],[45,0,4],[46,2,4],[47,0,4],[47,2,4],[49,2,4],[49,0,4],[50,2,4],[46,2,4],[51,0,4],[51,2,4],[53,2,4],[53,0,4],[54,2,4],[50,2,4],[55,0,4],[55,2,4],[54,2,4],[58,2,4],[58,2,0],[2,4,4],[0,4,4],[14,2,-4],[6,2,-4],[13,2,-4],[7,2,-4],[13,0,-4],[7,0,-4],[15,0,-4],[15,2,-4],[14,2,-4],[18,2,-4],[5,0,-4],[5,2,-4],[6,2,-4],[2,2,-4],[22,4,-4],[18,4,-4],[22,2,-4],[34,2,-4],[26,2,-4],[33,2,-4],[27,2,-4],[33,0,-4],[27,0,-4],[35,0,-4],[35,2,-4],[34,2,-4],[38,2,-4],[25,0,-4],[25,2,-4],[26,2,-4],[42,4,-4],[38,4,-4],[42,2,-4],[45,2,-4],[45,0,-4],[46,2,-4],[47,0,-4],[47,2,-4],[49,2,-4],[49,0,-4],[50,2,-4],[46,2,-4],[51,0,-4],[51,2,-4],[53,2,-4],[53,0,-4],[54,2,-4],[50,2,-4],[55,0,-4],[55,2,-4],[54,2,-4],[58,2,-4],[58,2,0],[2,4,-4],[0,4,-4],[58,14,0],[58,2,0],[58,2,0],[61.46410161513776,0,0],[61.46410161513772,0,0],[0,4,4],[0,4,-4],[0,4,2],[0,4,4],[0,4,2],[0,4,-2],[0,4,-4],[0,4,-2],[0,4,4],[0,4,0],[0,4,-4],[0,4,0],[58,2,2],[59.73205080756887,1,2],[56,2,2],[58,2,-2],[59.73205080756887,1,-2],[56,2,-2],[58,2,2],[58,2,0],[58,2,-2],[58,2,0],[58,2,2],[58,2,-2],[58,2,0],[58,2,4],[58,2,0],[58,2,-4],[58,8,2],[58,4,2],[58,8,-2],[58,4,-2],[58,12,2],[58,12,-2],[58,2,0],[58,2,0],[54,2,0],[54,2,0],[54,2,4],[54,2,-4],[58,4.828427124746156,2.828427124746254],[58,2,0],[58,7.65685424949254,0],[58,4.828427124746298,-2.828427124746227],[58,2,0],[54,2,0],[54,2,0],[58,10,0],[58,10,0],[58,6,0],[58,6,0],[55,2,1],[55,2,3],[55,2,-1],[55,2,-3],[53,2,3],[53,2,1],[53,2,-3],[53,2,-1],[55,0,3],[55,0,-3],[53,0,3],[53,0,-3],[46,2,4],[46,2,0],[46,2,-4],[46,2,0],[46,2,4],[42,2,4],[46,2,-4],[42,2,-4],[46,2,4],[42,2,4],[46,2,-4],[42,2,-4],[42,4,4],[42,4,-4],[42,6,4],[46,6,4],[42,4,4],[42,6,-4],[46,6,-4],[42,4,-4],[46,6,4],[46,10,4],[46,6,-4],[46,10,-4],[42,10,4],[42,2,4],[42,10,-4],[42,2,-4],[50,2,0],[50,2,0],[50,2,4],[50,2,-4],[46,4.828427124746131,2.8284271247462307],[46,2,0],[46,7.656854249492534,0],[46,4.828427124746291,-2.82842712474622],[46,2,0],[50,2,0],[50,2,0],[46,2,0],[46,2,0],[46,2,4],[46,2,-4],[46,2,0],[46,2,0],[46,6,0],[46,6,0],[51,2,1],[51,2,3],[51,2,-1],[51,2,-3],[49,2,3],[49,2,1],[49,2,-3],[49,2,-1],[51,0,3],[51,0,-3],[49,0,3],[49,0,-3],[47,2,1],[47,2,3],[47,2,-1],[47,2,-3],[45,2,3],[45,2,1],[45,2,-3],[45,2,-1],[47,0,3],[47,0,-3],[45,0,3],[45,0,-3],[42,4,0],[42,4,0],[42,4,2],[42,4,-2],[42,2,0],[42,2,0],[42,4,2],[42,4,-2],[38,4,4],[38,2,4],[38,4,-4],[38,2,-4],[22,4,4],[22,2,4],[22,4,-4],[22,2,-4],[18,4,4],[18,2,4],[18,4,-4],[18,2,-4],[2,4,4],[2,2,4],[2,4,-4],[2,2,-4],[34,2,4],[34,2,-4],[26,2,4],[26,2,-4],[14,2,4],[14,2,-4],[6,2,4],[6,2,-4],[38,4,4],[38,6,4],[34,6,4],[38,4,-4],[38,6,-4],[34,6,-4],[22,4,4],[22,6,4],[26,6,4],[22,4,-4],[22,6,-4],[26,6,-4],[18,4,4],[18,6,4],[14,6,4],[18,4,-4],[18,6,-4],[14,6,-4],[2,4,4],[2,6,4],[6,6,4],[2,4,-4],[2,6,-4],[6,6,-4],[38,10,4],[38,2,4],[38,10,-4],[38,2,-4],[22,10,4],[22,2,4],[22,10,-4],[22,2,-4],[18,10,4],[18,2,4],[18,10,-4],[18,2,-4],[2,10,4],[2,2,4],[2,10,-4],[2,2,-4],[34,2,0],[34,2,0],[26,2,0],[26,2,0],[14,2,0],[14,2,0],[6,2,0],[6,2,0],[38,2,4],[34,2,4],[38,2,-4],[34,2,-4],[22,2,4],[26,2,4],[22,2,-4],[26,2,-4],[18,2,4],[14,2,4],[18,2,-4],[14,2,-4],[2,2,4],[6,2,4],[2,2,-4],[6,2,-4],[34,2,0],[34,2,0],[26,2,0],[26,2,0],[14,2,0],[14,2,0],[6,2,0],[6,2,0],[35,2,1],[35,2,3],[35,2,-1],[35,2,-3],[25,2,1],[25,2,3],[25,2,-1],[25,2,-3],[15,2,1],[15,2,3],[15,2,-1],[15,2,-3],[5,2,1],[5,2,3],[5,2,-1],[5,2,-3],[33,2,3],[33,2,1],[33,2,-3],[33,2,-1],[27,2,3],[27,2,1],[27,2,-3],[27,2,-1],[13,2,3],[13,2,1],[13,2,-3],[13,2,-1],[7,2,3],[7,2,1],[7,2,-3],[7,2,-1],[35,0,3],[35,0,-3],[25,0,3],[25,0,-3],[15,0,3],[15,0,-3],[5,0,3],[5,0,-3],[33,0,3],[33,0,-3],[27,0,3],[27,0,-3],[13,0,3],[13,0,-3],[7,0,3],[7,0,-3],[38,4,0],[38,4,0],[22,4,0],[22,4,0],[18,4,0],[18,4,0],[2,4,0],[2,4,0],[38,4,2],[38,4,-2],[22,4,2],[22,4,-2],[18,4,2],[18,4,-2],[2,4,2],[2,4,-2],[38,2,0],[38,2,0],[22,2,0],[22,2,0],[18,2,0],[18,2,0],[2,2,0],[2,2,0],[2,4,2],[2,4,-2],[38,4,2],[38,4,-2],[22,4,2],[22,4,-2],[18,4,2],[18,4,-2],[42,4,4],[42,4,-4],[38,4,4],[38,4,-4],[22,4,4],[22,4,-4],[18,4,4],[18,4,-4],[2,4,4],[2,4,-4],[58,5.656854249492405,-2],[58,5.656854249492341,2],[46,6,-1.6568542494924117],[46,6,1.6568542494923975],[58,4,0.8284271247461561],[58,5.656854249492483,0.8284271247463018],[58,5.656854249492408,-0.8284271247461845],[58,4,-0.8284271247462129],[58,3.414213562373135,-1.4142135623731171],[58,4.828427124746327,0],[58,3.414213562373135,1.4142135623731207],[58,4.828427124746327,0],[58,4.828427124746014,2],[58,4.8284271247463835,-2],[58,6.242640687119362,-1.4142135623730745],[58,6.242640687119348,1.4142135623732095],[46,4.8284271247462485,-1.1715728752538297],[46,4.828427124746238,1.1715728752538155],[46,4.828427124746227,-2.8284271247462023],[46,4.828427124746227,2.8284271247461845]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[0,2],[1,3],[2,4],[3,5],[6,4],[7,6],[8,7],[8,9],[10,5],[11,10],[12,11],[12,13],[14,15],[14,16],[15,9],[17,18],[17,19],[18,20],[19,21],[20,22],[23,21],[24,23],[25,24],[25,26],[27,22],[28,27],[29,28],[29,16],[30,31],[30,32],[31,26],[33,34],[35,33],[35,32],[36,34],[37,36],[38,39],[40,41],[40,38],[41,37],[42,39],[43,42],[44,45],[46,47],[46,44],[47,43],[48,45],[49,48],[50,51],[50,49],[51,52],[53,13],[53,54],[55,56],[55,57],[56,58],[57,59],[58,60],[61,59],[62,61],[63,62],[63,64],[65,60],[66,65],[67,66],[67,68],[69,70],[69,71],[70,64],[72,73],[72,74],[73,75],[74,76],[75,77],[78,76],[79,78],[80,79],[80,81],[82,77],[83,82],[84,83],[84,71],[85,86],[85,87],[86,81],[88,89],[90,88],[90,87],[91,89],[92,91],[93,94],[95,96],[95,93],[96,92],[97,94],[98,97],[99,100],[101,102],[101,99],[102,98],[103,100],[104,103],[105,106],[105,104],[106,107],[108,68],[108,109],[110,111],[110,112],[111,113],[112,114],[113,52],[114,107],[115,116],[117,115],[118,117],[118,119],[120,116],[121,120],[121,122],[123,124],[123,119],[124,54],[125,126],[125,122],[126,109],[127,128],[127,129],[130,131],[130,132],[133,134],[135,136],[134,137],[136,138],[139,140],[141,142],[143,144],[145,146],[133,128],[133,147],[135,131],[135,148],[51,149],[106,150],[127,134],[130,136],[137,129],[137,143],[138,132],[138,145],[50,151],[105,152],[153,140],[154,142],[46,160],[101,161],[129,151],[132,152],[110,147],[110,148],[162,143],[163,145],[164,140],[165,142],[144,156],[146,159],[113,128],[114,131],[166,167],[168,169],[170,171],[172,173],[174,167],[175,169],[176,170],[177,172],[167,153],[169,154],[170,153],[172,154],[178,179],[180,181],[182,183],[184,185],[186,187],[188,189],[190,187],[191,189],[192,193],[194,192],[195,196],[197,195],[198,199],[200,201],[202,199],[202,203],[204,201],[204,205],[47,206],[102,207],[208,178],[209,180],[210,211],[210,212],[213,214],[213,212],[40,215],[95,216],[41,217],[96,218],[199,182],[201,184],[183,219],[183,202],[185,220],[185,204],[35,221],[90,222],[221,183],[222,185],[178,223],[180,224],[211,198],[214,200],[215,182],[216,184],[225,226],[227,228],[229,230],[231,232],[233,226],[234,228],[235,229],[236,231],[226,208],[228,209],[229,208],[231,209],[237,238],[239,240],[241,242],[243,244],[245,238],[246,240],[247,241],[248,243],[238,219],[240,220],[241,219],[243,220],[30,249],[85,250],[251,190],[251,194],[252,191],[252,197],[187,253],[187,192],[189,254],[189,195],[255,192],[256,195],[255,203],[256,205],[257,258],[259,260],[261,262],[263,264],[265,266],[267,268],[269,123],[269,270],[271,125],[271,272],[258,273],[260,274],[262,275],[264,276],[266,277],[268,278],[270,279],[272,280],[281,282],[282,283],[284,285],[285,286],[287,288],[288,289],[290,291],[291,292],[293,294],[294,295],[296,297],[297,298],[299,300],[299,118],[300,301],[302,303],[302,121],[303,304],[305,306],[307,308],[309,310],[311,312],[313,314],[315,316],[317,318],[319,320],[25,321],[80,322],[29,323],[84,324],[8,325],[63,326],[12,327],[67,328],[329,330],[329,305],[331,332],[331,307],[333,334],[333,309],[335,336],[335,311],[337,338],[337,313],[339,340],[339,315],[341,342],[341,317],[343,344],[343,319],[17,345],[72,346],[18,347],[73,348],[0,349],[55,350],[1,351],[56,352],[321,329],[322,331],[323,333],[324,335],[325,337],[326,339],[327,341],[328,343],[353,354],[355,356],[357,358],[359,360],[361,362],[363,364],[365,366],[367,368],[369,370],[371,372],[373,374],[375,376],[377,378],[379,380],[381,382],[383,384],[385,354],[386,356],[387,358],[388,360],[389,362],[390,364],[391,366],[392,368],[393,369],[394,371],[395,373],[396,375],[397,377],[398,379],[399,381],[400,383],[354,330],[356,332],[358,334],[360,336],[362,338],[364,340],[366,342],[368,344],[369,330],[371,332],[373,334],[375,336],[377,338],[379,340],[381,342],[383,344],[31,401],[86,402],[14,403],[69,404],[15,405],[70,406],[53,407],[108,408],[409,257],[409,281],[410,259],[410,284],[411,261],[411,287],[412,263],[412,290],[413,265],[413,293],[414,267],[414,296],[415,269],[415,299],[416,271],[416,302],[258,417],[258,282],[260,418],[260,285],[262,419],[262,288],[264,420],[264,291],[266,421],[266,294],[268,422],[268,297],[270,423],[270,300],[272,424],[272,303],[117,425],[120,426],[427,282],[428,285],[429,288],[430,291],[431,294],[432,297],[425,300],[426,303],[427,306],[428,308],[429,310],[430,312],[431,314],[432,316],[425,318],[426,320],[149,113],[149,151],[150,114],[150,152],[111,133],[112,135],[137,139],[138,141],[151,153],[152,154],[164,156],[165,159],[149,127],[150,130],[134,162],[136,163],[139,151],[139,164],[141,152],[141,165],[140,156],[142,159],[113,51],[114,106],[134,129],[136,132],[143,164],[145,165],[147,162],[148,163],[140,160],[142,161],[128,134],[131,136],[174,176],[175,177],[167,170],[169,172],[49,166],[104,168],[44,171],[99,173],[48,174],[103,175],[45,176],[100,177],[166,174],[166,151],[168,175],[168,152],[171,176],[171,160],[173,177],[173,161],[208,215],[209,216],[215,217],[216,218],[219,221],[220,222],[253,249],[253,221],[254,250],[254,222],[179,182],[181,184],[183,186],[185,188],[211,223],[214,224],[433,203],[434,205],[179,215],[179,223],[181,216],[181,224],[186,221],[186,193],[188,222],[188,196],[178,211],[180,214],[202,193],[204,196],[206,178],[207,180],[223,199],[224,201],[217,183],[218,185],[193,203],[196,205],[233,235],[234,236],[226,229],[228,231],[43,225],[98,227],[38,230],[93,232],[42,233],[97,234],[39,235],[94,236],[225,233],[225,206],[227,234],[227,207],[230,235],[230,215],[232,236],[232,216],[245,247],[246,248],[238,241],[240,243],[37,237],[92,239],[33,242],[88,244],[36,245],[91,246],[34,247],[89,248],[237,245],[237,217],[239,246],[239,218],[242,247],[242,221],[244,248],[244,222],[32,253],[87,254],[192,203],[195,205],[249,190],[250,191],[255,194],[255,433],[256,197],[256,434],[251,187],[252,189],[251,192],[252,195],[417,401],[417,321],[418,402],[418,322],[419,403],[419,323],[420,404],[420,324],[421,405],[421,325],[422,406],[422,326],[407,124],[423,407],[423,327],[408,126],[424,408],[424,328],[321,330],[322,332],[323,334],[324,336],[325,338],[326,340],[327,342],[328,344],[273,329],[274,331],[275,333],[276,335],[277,337],[278,339],[279,341],[280,343],[435,306],[436,308],[437,310],[438,312],[439,314],[440,316],[441,115],[441,318],[442,116],[442,320],[273,321],[273,283],[274,322],[274,286],[275,323],[275,289],[276,324],[276,292],[277,325],[277,295],[278,326],[278,298],[279,327],[279,301],[280,328],[280,304],[306,283],[308,286],[310,289],[312,292],[314,295],[316,298],[318,301],[320,304],[329,345],[331,346],[333,347],[335,348],[337,349],[339,350],[341,351],[343,352],[283,305],[286,307],[289,309],[292,311],[295,313],[298,315],[301,317],[304,319],[385,393],[386,394],[387,395],[388,396],[389,397],[390,398],[391,399],[392,400],[354,369],[356,371],[358,373],[360,375],[362,377],[364,379],[366,381],[368,383],[24,353],[79,355],[28,357],[83,359],[7,361],[62,363],[11,365],[66,367],[19,370],[74,372],[20,374],[75,376],[2,378],[57,380],[3,382],[58,384],[23,385],[78,386],[27,387],[82,388],[6,389],[61,390],[10,391],[65,392],[21,393],[76,394],[22,395],[77,396],[4,397],[59,398],[5,399],[60,400],[353,385],[353,321],[355,386],[355,322],[357,387],[357,323],[359,388],[359,324],[361,389],[361,325],[363,390],[363,326],[365,391],[365,327],[367,392],[367,328],[370,393],[370,345],[372,394],[372,346],[374,395],[374,347],[376,396],[376,348],[378,397],[378,349],[380,398],[380,350],[382,399],[382,351],[384,400],[384,352],[26,417],[81,418],[16,419],[71,420],[9,421],[64,422],[13,423],[68,424],[282,306],[285,308],[288,310],[291,312],[294,314],[297,316],[300,318],[303,320],[401,257],[402,259],[403,261],[404,263],[405,265],[406,267],[407,269],[408,271],[427,281],[427,435],[428,284],[428,436],[429,287],[429,437],[430,290],[430,438],[431,293],[431,439],[432,296],[432,440],[425,299],[425,441],[426,302],[426,442],[119,415],[122,416],[409,258],[410,260],[411,262],[412,264],[413,266],[414,268],[415,270],[416,272],[409,282],[410,285],[411,288],[412,291],[413,294],[414,297],[415,300],[416,303],[140,178],[142,180],[155,210],[158,213],[157,212],[190,257],[191,259],[194,281],[197,284],[199,201],[202,204],[433,434],[255,427],[256,428],[261,265],[263,267],[329,333],[331,335],[337,341],[339,343],[287,293],[290,296],[305,309],[307,311],[313,317],[315,319],[305,307],[309,311],[313,315],[317,319],[435,436],[437,438],[439,440],[441,442],[429,431],[430,432],[160,206],[161,207],[156,211],[159,214],[249,401],[250,402],[433,435],[434,436],[203,205],[251,409],[252,410],[403,405],[404,406],[345,347],[346,348],[349,351],[350,352],[437,439],[438,440],[306,308],[310,312],[314,316],[318,320],[411,413],[412,414],[157,443],[157,444],[213,445],[445,200],[214,445],[198,446],[446,210],[211,446],[159,158],[158,157],[157,155],[155,156],[144,447],[156,447],[444,448],[155,448],[443,449],[158,449],[146,450],[159,450],[450,158],[449,157],[448,157],[447,155],[146,451],[451,158],[450,451],[452,163],[451,452],[144,453],[453,155],[447,453],[454,162],[453,454],[444,455],[455,147],[443,456],[456,148],[454,455],[456,455],[456,452],[158,457],[457,443],[449,457],[457,452],[155,458],[458,444],[448,458],[458,454],[445,459],[459,212],[213,459],[446,460],[460,212],[210,460],[212,461],[461,200],[459,461],[212,462],[462,198],[460,462],[462,461]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-180,-180,-180,-180,-60,-60,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-30,-30,-30,-30,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-90,-180,-180,-180,-180,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,60,60,90,90,180,180,180,180,30,30,180,180,180,180,180,180,90,90,180,180,180,180,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,90,90,180,180,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-180,-180,-180,-180,-90,-90,-90,-90,-180,-180,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-90,-180,-180,180,180,135,135,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,180,180,180,-180,180,-90,-90,-90,-90,180,180,180,180,180,180,180,180,180,180,180,180,-180,-180,-180,180,180,-180,-180,-180,180,180,-180,-180,-180,-180,-180,180,-180,-180,-180,-180,180,-180,-180,-180,180,180,180,180,180,180,180,-180,-180,-180,-180,-180,-180,180\n\t],\n\t\"faces_vertices\": [\n\t\t[0,2,378,349],[0,349,351,1],[1,351,382,3],[2,4,397,378],[3,382,399,5],[4,6,389,397],[5,399,391,10],[6,7,361,389],[7,8,325,361],[8,9,421,325],[9,15,405,421],[10,391,365,11],[11,365,327,12],[12,327,423,13],[13,423,407,53],[14,16,419,403],[14,403,405,15],[16,29,323,419],[17,19,370,345],[17,345,347,18],[18,347,374,20],[19,21,393,370],[20,374,395,22],[21,23,385,393],[22,395,387,27],[23,24,353,385],[24,25,321,353],[25,26,417,321],[26,31,401,417],[27,387,357,28],[28,357,323,29],[30,32,253,249],[30,249,401,31],[32,35,221,253],[33,34,247,242],[33,242,221,35],[34,36,245,247],[36,37,237,245],[37,41,217,237],[38,39,235,230],[38,230,215,40],[39,42,233,235],[40,215,217,41],[42,43,225,233],[43,47,206,225],[44,45,176,171],[44,171,160,46],[45,48,174,176],[46,160,206,47],[48,49,166,174],[49,50,151,166],[50,51,149,151],[51,52,113],[51,113,149],[53,407,124,54],[55,56,352,350],[55,350,380,57],[56,58,384,352],[57,380,398,59],[58,60,400,384],[59,398,390,61],[60,65,392,400],[61,390,363,62],[62,363,326,63],[63,326,422,64],[64,422,406,70],[65,66,367,392],[66,67,328,367],[67,68,424,328],[68,108,408,424],[69,70,406,404],[69,404,420,71],[71,420,324,84],[72,73,348,346],[72,346,372,74],[73,75,376,348],[74,372,394,76],[75,77,396,376],[76,394,386,78],[77,82,388,396],[78,386,355,79],[79,355,322,80],[80,322,418,81],[81,418,402,86],[82,83,359,388],[83,84,324,359],[85,86,402,250],[85,250,254,87],[87,254,222,90],[88,90,222,244],[88,244,248,89],[89,248,246,91],[91,246,239,92],[92,239,218,96],[93,95,216,232],[93,232,236,94],[94,236,234,97],[95,96,218,216],[97,234,227,98],[98,227,207,102],[99,101,161,173],[99,173,177,100],[100,177,175,103],[101,102,207,161],[103,175,168,104],[104,168,152,105],[105,152,150,106],[106,150,114],[106,114,107],[108,109,126,408],[110,112,135,148],[110,148,456,455,147],[110,147,133,111],[111,133,128,113],[112,114,131,135],[113,128,127,149],[114,150,130,131],[115,441,442,116],[115,117,425,441],[116,442,426,120],[117,118,299,425],[118,119,415,299],[119,123,269,415],[120,426,302,121],[121,302,416,122],[122,416,271,125],[123,124,407,269],[125,271,408,126],[127,128,134],[127,134,129],[127,129,151,149],[128,133,134],[129,134,137],[129,137,139,151],[130,150,152,132],[130,132,136],[130,136,131],[131,136,135],[132,152,141,138],[132,138,136],[133,147,162,134],[134,162,143,137],[135,136,163,148],[136,138,145,163],[137,143,164,139],[138,141,165,145],[139,164,140],[139,140,153,151],[140,164,156],[140,156,211,178],[140,178,206,160],[140,160,171,170,153],[141,152,154,142],[141,142,165],[142,154,172,173,161],[142,161,207,180],[142,180,214,159],[142,159,165],[143,162,454,453,144],[143,144,156,164],[144,453,447],[144,447,156],[145,165,159,146],[145,146,451,452,163],[146,159,450],[146,450,451],[147,455,454,162],[148,163,452,456],[151,153,167,166],[152,168,169,154],[153,170,167],[154,169,172],[155,458,448],[155,448,157],[155,157,212,210],[155,210,211,156],[155,156,447],[155,447,453],[155,453,454,458],[157,443,449],[157,449,158],[157,158,213,212],[157,448,444],[157,444,455,456,443],[158,451,450],[158,450,159],[158,159,214,213],[158,449,457],[158,457,452,451],[166,167,174],[167,170,176,174],[168,175,169],[169,175,177,172],[170,171,176],[172,177,173],[178,211,223],[178,223,179],[178,179,215,208],[178,208,226,225,206],[179,223,199,182],[179,182,215],[180,207,227,228,209],[180,209,216,181],[180,181,224],[180,224,214],[181,216,184],[181,184,201,224],[182,199,202,183],[182,183,217,215],[183,202,193,186],[183,186,221],[183,221,219],[183,219,238,237,217],[184,216,218,185],[184,185,204,201],[185,218,239,240,220],[185,220,222],[185,222,188],[185,188,196,204],[186,193,192,187],[186,187,253,221],[187,192,251],[187,251,190],[187,190,249,253],[188,222,254,189],[188,189,195,196],[189,254,250,191],[189,191,252],[189,252,195],[190,251,409,257],[190,257,401,249],[191,250,402,259],[191,259,410,252],[192,193,203],[192,203,255],[192,255,194],[192,194,251],[193,202,203],[194,255,427,281],[194,281,409,251],[195,252,197],[195,197,256],[195,256,205],[195,205,196],[196,205,204],[197,252,410,284],[197,284,428,256],[198,446,460,462],[198,462,461,200,201,199],[198,199,223,211],[198,211,446],[199,201,204,202],[200,445,214],[200,214,224,201],[200,461,459,445],[202,204,205,203],[203,205,434,433],[203,433,255],[205,256,434],[208,215,230,229],[208,229,226],[209,228,231],[209,231,232,216],[210,212,460],[210,460,446],[210,446,211],[212,213,459],[212,459,461],[212,461,462],[212,462,460],[213,214,445],[213,445,459],[219,221,242,241],[219,241,238],[220,240,243],[220,243,244,222],[225,226,233],[226,229,235,233],[227,234,228],[228,234,236,231],[229,230,235],[231,236,232],[237,238,245],[238,241,247,245],[239,246,240],[240,246,248,243],[241,242,247],[243,248,244],[255,433,435,427],[256,428,436,434],[257,409,258],[257,258,417,401],[258,409,282],[258,282,283,273],[258,273,321,417],[259,402,418,260],[259,260,410],[260,418,322,274],[260,274,286,285],[260,285,410],[261,262,411],[261,411,413,265],[261,265,405,403],[261,403,419,262],[262,275,289,288],[262,288,411],[262,419,323,275],[263,264,420,404],[263,404,406,267],[263,267,414,412],[263,412,264],[264,276,324,420],[264,412,291],[264,291,292,276],[265,413,266],[265,266,421,405],[266,413,294],[266,294,295,277],[266,277,325,421],[267,406,422,268],[267,268,414],[268,422,326,278],[268,278,298,297],[268,297,414],[269,270,415],[269,407,423,270],[270,279,301,300],[270,300,415],[270,423,327,279],[271,272,424,408],[271,416,272],[272,280,328,424],[272,416,303],[272,303,304,280],[273,283,305,329],[273,329,321],[274,322,331],[274,331,307,286],[275,333,309,289],[275,323,333],[276,335,324],[276,292,311,335],[277,295,313,337],[277,337,325],[278,326,339],[278,339,315,298],[279,341,317,301],[279,327,341],[280,343,328],[280,304,319,343],[281,427,282],[281,282,409],[282,427,306],[282,306,283],[283,306,305],[284,410,285],[284,285,428],[285,286,308],[285,308,428],[286,307,308],[287,288,429],[287,429,431,293],[287,293,413,411],[287,411,288],[288,289,310],[288,310,429],[289,309,310],[290,291,412],[290,412,414,296],[290,296,432,430],[290,430,291],[291,430,312],[291,312,292],[292,312,311],[293,431,294],[293,294,413],[294,431,314],[294,314,295],[295,314,313],[296,414,297],[296,297,432],[297,298,316],[297,316,432],[298,315,316],[299,300,425],[299,415,300],[300,301,318],[300,318,425],[301,317,318],[302,303,416],[302,426,303],[303,426,320],[303,320,304],[304,320,319],[305,306,308,307],[305,307,311,309],[305,309,333,329],[306,435,436,308],[306,427,435],[307,331,335,311],[308,436,428],[309,311,312,310],[310,312,438,437],[310,437,429],[312,430,438],[313,314,316,315],[313,315,319,317],[313,317,341,337],[314,439,440,316],[314,431,439],[315,339,343,319],[316,440,432],[317,319,320,318],[318,320,442,441],[318,441,425],[320,426,442],[321,329,330],[321,330,354,353],[322,355,356,332],[322,332,331],[323,334,333],[323,357,358,334],[324,336,360,359],[324,335,336],[325,337,338],[325,338,362,361],[326,363,364,340],[326,340,339],[327,342,341],[327,365,366,342],[328,344,368,367],[328,343,344],[329,333,347,345],[329,345,370,369,330],[330,369,354],[331,332,371,372,346],[331,346,348,335],[332,356,371],[333,334,373,374,347],[334,358,373],[335,348,376,375,336],[336,375,360],[337,341,351,349],[337,349,378,377,338],[338,377,362],[339,340,379,380,350],[339,350,352,343],[340,364,379],[341,342,381,382,351],[342,366,381],[343,352,384,383,344],[344,383,368],[353,354,385],[354,369,393,385],[355,386,356],[356,386,394,371],[357,387,358],[358,387,395,373],[359,360,388],[360,375,396,388],[361,362,389],[362,377,397,389],[363,390,364],[364,390,398,379],[365,391,366],[366,391,399,381],[367,368,392],[368,383,400,392],[369,370,393],[371,394,372],[373,395,374],[375,376,396],[377,378,397],[379,398,380],[381,399,382],[383,384,400],[429,437,439,431],[430,432,440,438],[433,434,436,435],[437,438,440,439],[443,456,452,457],[443,457,449],[444,448,458],[444,458,454,455]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"creasePattern\"],\n\t\t\"frame_attributes\": [\"2D\"],\n\t\t\"vertices_coords\": [\n\t\t\t[24,0],[16,0],[25,0],[15,0],[27,0],[13,0],[29,0],[31,0],[32,0],[36,0],[11,0],[9,0],[8,0],[4,0],[42,0],[38,0],[44,0],[64,0],[56,0],[65,0],[55,0],[67,0],[53,0],[69,0],[71,0],[72,0],[76,0],[51,0],[49,0],[48,0],[82,0],[78,0],[84,0],[89,0],[91,0],[88,0],[93,0],[95,0],[101,0],[103,0],[100,0],[96,0],[105,0],[107,0],[113,0],[115,0],[112,0],[108,0],[117,0],[119,0],[120,0],[124,0],[128,0],[2,0],[0,0],[24,40],[16,40],[25,40],[15,40],[27,40],[13,40],[29,40],[31,40],[32,40],[36,40],[11,40],[9,40],[8,40],[4,40],[42,40],[38,40],[44,40],[64,40],[56,40],[65,40],[55,40],[67,40],[53,40],[69,40],[71,40],[72,40],[76,40],[51,40],[49,40],[48,40],[82,40],[78,40],[84,40],[89,40],[91,40],[88,40],[93,40],[95,40],[101,40],[103,40],[100,40],[96,40],[105,40],[107,40],[113,40],[115,40],[112,40],[108,40],[117,40],[119,40],[120,40],[124,40],[128,40],[2,40],[0,40],[128,20],[128,8],[128,32],[128,4],[128,36],[0,16],[0,24],[0,14],[0,12],[0,10],[0,26],[0,28],[0,30],[0,8],[0,4],[0,32],[0,36],[124,6],[126,6],[122,6],[124,34],[126,34],[122,34],[126,8],[124,8],[126,32],[124,32],[122,8],[122,32],[120,8],[116,8],[120,32],[116,32],[122,14],[118,14],[122,26],[118,26],[126,18],[126,22],[124,4],[124,36],[120,4],[120,36],[116,4],[116,36],[116,16],[116,12],[116,20],[116,24],[116,28],[112,4],[112,36],[124,16],[124,24],[120,12],[120,28],[119,3],[117,3],[119,37],[117,37],[115,3],[113,3],[115,37],[113,37],[117,1],[117,39],[115,1],[115,39],[104,8],[100,8],[104,32],[100,32],[96,8],[92,8],[96,32],[92,32],[88,8],[84,8],[88,32],[84,32],[82,8],[82,32],[84,12],[88,12],[82,12],[84,28],[88,28],[82,28],[100,16],[96,16],[100,24],[96,24],[92,16],[84,16],[92,24],[84,24],[108,4],[108,36],[104,4],[104,36],[104,16],[104,12],[104,20],[104,24],[104,28],[100,4],[100,36],[96,4],[96,36],[92,4],[92,36],[88,4],[88,36],[100,12],[100,28],[107,3],[105,3],[107,37],[105,37],[103,3],[101,3],[103,37],[101,37],[105,1],[105,39],[103,1],[103,39],[95,3],[93,3],[95,37],[93,37],[91,3],[89,3],[91,37],[89,37],[93,1],[93,39],[91,1],[91,39],[82,4],[82,36],[82,10],[82,30],[84,4],[84,36],[82,14],[82,26],[78,8],[76,8],[78,32],[76,32],[42,8],[44,8],[42,32],[44,32],[38,8],[36,8],[38,32],[36,32],[2,8],[4,8],[2,32],[4,32],[72,8],[72,32],[48,8],[48,32],[32,8],[32,32],[8,8],[8,32],[78,12],[76,12],[72,12],[78,28],[76,28],[72,28],[42,12],[44,12],[48,12],[42,28],[44,28],[48,28],[38,12],[36,12],[32,12],[38,28],[36,28],[32,28],[2,12],[4,12],[8,12],[2,28],[4,28],[8,28],[68,16],[76,16],[68,24],[76,24],[52,16],[44,16],[52,24],[44,24],[28,16],[36,16],[28,24],[36,24],[12,16],[4,16],[12,24],[4,24],[72,4],[72,36],[48,4],[48,36],[32,4],[32,36],[8,4],[8,36],[68,8],[68,4],[68,32],[68,36],[52,8],[52,4],[52,32],[52,36],[28,8],[28,4],[28,32],[28,36],[12,8],[12,4],[12,32],[12,36],[64,4],[64,36],[56,4],[56,36],[24,4],[24,36],[16,4],[16,36],[71,3],[69,3],[71,37],[69,37],[49,3],[51,3],[49,37],[51,37],[31,3],[29,3],[31,37],[29,37],[9,3],[11,3],[9,37],[11,37],[67,3],[65,3],[67,37],[65,37],[53,3],[55,3],[53,37],[55,37],[27,3],[25,3],[27,37],[25,37],[13,3],[15,3],[13,37],[15,37],[69,1],[69,39],[51,1],[51,39],[29,1],[29,39],[11,1],[11,39],[67,1],[67,39],[53,1],[53,39],[27,1],[27,39],[13,1],[13,39],[78,4],[78,36],[42,4],[42,36],[38,4],[38,36],[2,4],[2,36],[78,10],[78,30],[42,10],[42,30],[38,10],[38,30],[2,10],[2,30],[76,4],[76,36],[44,4],[44,36],[36,4],[36,36],[4,4],[4,36],[2,14],[2,26],[78,14],[78,26],[42,14],[42,26],[38,14],[38,26],[82,16],[82,24],[78,16],[78,24],[42,16],[42,24],[38,16],[38,24],[2,16],[2,24],[118,22],[118,18],[102.34314575050757,24],[102.34314575050757,16],[116.82842712474621,14],[116.82842712474621,18],[116.82842712474621,22],[116.82842712474621,26],[117.41421356237312,25.41421356237312],[118.82842712474631,24],[117.41421356237312,14.58578643762688],[118.82842712474631,16],[118.82842712474631,18],[118.82842712474631,22],[117.41421356237312,22.58578643762688],[117.41421356237312,17.41421356237312],[102.82842712474616,22.828427124746206],[102.82842712474616,17.171572875253794],[101.17157287525379,22.828427124746206],[101.17157287525379,17.171572875253794]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/nested-frames.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"nested frame inheritance example\",\n\t\"file_author\": \"Kraft\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_attributes\": [\"3D\"],\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [[0,0,0], [1,0,0], [1,1,0], [0,1,0]],\n\t\"edges_vertices\": [[0,1], [1,2], [2,3], [3,0]],\n\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\": [[0,1,2,3]],\n\t\"file_frames\": [\n\t\t{\n\t\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\t\"frame_parent\": 0,\n\t\t\t\"frame_inherit\": true,\n\t\t\t\"edges_vertices\": [[0,1], [1,2], [2,3], [3,0], [0,2]],\n\t\t\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\",\"U\"],\n\t\t\t\"faces_vertices\": [[0,1,2], [2,3,0]]\n\t\t},\n\t\t{\n\t\t\t\"frame_parent\": 1,\n\t\t\t\"frame_inherit\": true,\n\t\t\t\"vertices_coords\": [[0,0,0], [1,0,0], [1,1,0], [0,1,0], [0.5,0.5,0]],\n\t\t\t\"edges_vertices\": [[0,1], [1,2], [2,3], [3,0], [0,4], [4,2], [1,4], [4,3]],\n\t\t\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\",\"U\",\"U\",\"U\",\"U\"],\n\t\t\t\"faces_vertices\": [[0,1,4], [1,2,4], [2,3,4], [3,0,4]]\n\t\t},\n\t\t{\n\t\t\t\"frame_parent\": 2,\n\t\t\t\"frame_inherit\": true,\n\t\t\t\"vertices_coords\": [[0,0,0], [1,0,0], [0.5,0.5,1], [0,1,0], [0.5,0.5,0]]\n\t\t},\n\t\t{\n\t\t\t\"frame_parent\": 2,\n\t\t\t\"frame_inherit\": true,\n\t\t\t\"vertices_coords\": [[0,0,0], [0.5,0.5,1], [1,1,0], [0,1,0], [0.5,0.5,0]]\n\t\t},\n\t\t{\n\t\t\t\"frame_parent\": 3,\n\t\t\t\"frame_inherit\": true,\n\t\t\t\"vertices_coords\": [[0,0,0], [0,1,0], [0.5,0.5,1], [0,1,0], [0.5,0.5,0]]\n\t\t}\n\t]\n}"
  },
  {
    "path": "tests/files/fold/no-faces.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_title\": \"3D graph with no faces\",\n\t\"vertices_coords\": [[0,0,0], [1,0,1], [1,1,0], [0,1,1], [0.5,0.5,0.5]],\n\t\"edges_vertices\": [[0,1], [1,2], [2,3], [3,0], [0,4], [4,3], [4,2]]\n}\n"
  },
  {
    "path": "tests/files/fold/non-flat-paper.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_title\": \"non-flat paper\",\n\t\"file_author\": \"Kraft\",\n\t\"file_classes\": [\"singleModel\"],\n\t\"frame_attributes\": [\"3D\"],\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"vertices_coords\": [\n\t\t[0, 0, 0],\n\t\t[1, 0, 0],\n\t\t[0, 1, 0],\n\t\t[0, 0, 1],\n\t\t[0.5, 0.5, 0]\n\t],\n\t\"edges_vertices\": [\n\t\t[1, 4],\n\t\t[4, 2],\n\t\t[2, 3],\n\t\t[3, 1],\n\t\t[0, 1],\n\t\t[0, 2],\n\t\t[0, 3],\n\t\t[0, 4]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"V\"\n\t],\n\t\"faces_vertices\": [\n\t\t[0, 4, 1],\n\t\t[0, 2, 4],\n\t\t[0, 3, 2],\n\t\t[0, 1, 3]\n\t],\n\t\"faceOrders\": [\n\t\t[0, 1, 1],\n\t\t[2, 1, -1],\n\t\t[3, 2, -1],\n\t\t[0, 3, -1]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0, 0, 0],\n\t\t\t[1, 0, 0],\n\t\t\t[1, 0, 0],\n\t\t\t[0, 0, 1],\n\t\t\t[0.5, 0, 0.5]\n\t\t]\n\t}]\n}"
  },
  {
    "path": "tests/files/fold/non-planar-100-chaotic.fold",
    "content": "{\"vertices_coords\":[[0.4532722787234369,0.20369640581495285],[0.9054969138857212,0.254724570954062],[0.7947834048204345,0.9963538000920522],[0.8487117202654977,0.909535027348253],[0.31314198752881195,0.4456879845340369],[0.7184702855869565,0.5459109908943058],[0.37578098295177553,0.7118504484103416],[0.4028199543675177,0.9479943679385419],[0.773587861901258,0.7285988992725236],[0.7621361909401305,0.5538656657230479],[0.9740423818794333,0.6594325137564232],[0.7264884600614032,0.05142165534611043],[0.5756040845039068,0.4607884482211273],[0.24565587579820392,0.3944636806531976],[0.0532064845429141,0.8385140596902632],[0.1475495095138193,0.357633550128756],[0.41219009077286506,0.2570750060388498],[0.7510337718000184,0.49209976031564095],[0.8137971674675284,0.46141896941280747],[0.9941789325246111,0.6871603911076052],[0.2902574665590998,0.1983343173662111],[0.6680756600387983,0.14726480676178055],[0.4284421070951956,0.5956748502272051],[0.27051302071852334,0.10526904827012906],[0.4481103629910692,0.4463410498700131],[0.797767615321018,0.46480259695821635],[0.3516629788827763,0.8229321233527838],[0.14909788152404957,0.6020333089951051],[0.869759009505781,0.1516683559853127],[0.46291703683343965,0.4152925284628537],[0.43994825167712115,0.9127802789936663],[0.915357166608497,0.9176674974504455],[0.7554013439683076,0.25081186542339706],[0.22884627978991,0.5254912358472339],[0.640659820319645,0.3511725046605414],[0.833897847969769,0.14926453431384012],[0.07627020813094565,0.4913517574193722],[0.12343674238752933,0.7039558693776722],[0.2568138735680874,0.180529458331258],[0.8613295196728414,0.059738635078732116],[0.15243553593878234,0.7782002610423566],[0.4805619247350219,0.1909011066318409],[0.4352948836597612,0.6103074269134159],[0.17503975313861964,0.8582696222104758],[0.24935355551611726,0.6016323092352154],[0.36345863009589485,0.08364258073373021],[0.5110094299082131,0.42352711960134726],[0.14345202316500183,0.33943058466280873],[0.8624699928755446,0.1807934811332006],[0.4110267887515022,0.38177289384938407],[0.988337869525614,0.2042052340674343],[0.9568106223388242,0.2305296639476404],[0.24521624353006355,0.22099700446188209],[0.4903082232084037,0.45596315789900976],[0.7936255643541696,0.37262521728742004],[0.9042374432083773,0.44931528489957273],[0.23940773736796928,0.3344344030244446],[0.717334374537363,0.9242633649313392],[0.8152591639868736,0.032404566365366705],[0.6214218474535012,0.7000616958418897],[0.7009388950283704,0.32664875120239745],[0.3595969364414522,0.14291145420443963],[0.2870956891646659,0.3185769600763042],[0.17483916329199256,0.9369350162283439],[0.7097117261572525,0.68012690004563],[0.7465408615210103,0.34360412087697867],[0.1944418964807828,0.31920054160504385],[0.5560352719216024,0.052741029502525905],[0.7464306637674121,0.5544979867512918],[0.5861492924895522,0.6292448914178521],[0.10624851852629824,0.06745167606633662],[0.3176482148246045,0.778817716733776],[0.25489280719387764,0.7811745711868747],[0.4199329527242781,0.0344587139146042],[0.7014959830703205,0.4379427187853575],[0.8573352214626613,0.3088589778460993],[0.3696574958663219,0.49908594641800264],[0.8706535098734938,0.7017796125741835],[0.8134710478978218,0.032757213329208223],[0.4109738699200767,0.4052360090043907],[0.4944658801492694,0.05308927071668479],[0.8611915928579923,0.8844622613618882],[0.6360561385802099,0.8592082985228309],[0.6834424547117308,0.8505805343094821],[0.16046278102588363,0.3256818819575711],[0.959918208403876,0.048434456120391545],[0.861646192758861,0.5822120038671688],[0.22343700410430345,0.6271673500135533],[0.7460526410725539,0.45460884742071217],[0.12201068812284444,0.2728479125148662],[0.06514993788877632,0.4736705032239865],[0.25969745757980744,0.2052034212717586],[0.9187579889553206,0.6082535106932048],[0.29477265288592225,0.30419047042913183],[0.6054914731797338,0.9391031445546432],[0.7967723163621778,0.6073405299883041],[0.16412287694437966,0.716265204428389],[0.5803673312992033,0.6144894056503789],[0.5515684508396437,0.41553407633320627],[0.0006409769295423473,0.04606370697832207],[0.481188826641074,0.9366889741195723],[0.653060460367413,0.31013894430589795],[0.17574581723310567,0.8176818726556201],[0.8943003534197105,0.4480436179176279],[0.2215520958230488,0.053803596535376474],[0.8373156651723168,0.6696183015305075],[0.6752680325446945,0.6961707722120696],[0.23142792556735858,0.24954017573232612],[0.2583318561374768,0.05203098755344393],[0.6863758418578116,0.2943003073753965],[0.7674166115830885,0.3017369220557362],[0.593887337138876,0.259653877145682],[0.14250504879411952,0.4149308261608251],[0.3101943736748811,0.6432756790848915],[0.015023697221658194,0.013021664752273843],[0.04830241800022428,0.02620721316893615],[0.10692537655901435,0.6455644036928567],[0.0027850562075359964,0.02903171005162397],[0.9459855509703787,0.9450234196555145],[0.21369602785464048,0.8068714187657364],[0.9593694273547178,0.2294027813116324],[0.33503098548664445,0.25544335604758395],[0.29224662410079505,0.9524650538235935],[0.2807428831484631,0.8708743355159128],[0.8711312143957812,0.08523727400818415],[0.1111287878063818,0.48281734540272003],[0.2218797649951092,0.43096139143684997],[0.5358968822670394,0.28764172585469194],[0.05368611829968817,0.514840576710478],[0.7908555586019297,0.7503051206782556],[0.01700032420258779,0.07434688101394116],[0.6324619905704725,0.5661304155598248],[0.5071173537699203,0.07706689951963308],[0.0030393091433285058,0.0513695619793042],[0.7376091113298211,0.2264736332272761],[0.19035717359409077,0.9204206331082294],[0.8650620013637043,0.717048824423753],[0.022301487307334833,0.4114776679558607],[0.9895735465535305,0.01038379532634437],[0.9399195981147834,0.4903069211625921],[0.43209719575584726,0.9305329524569963],[0.6027104979766587,0.7944670613476248],[0.4871669028068317,0.05892154237137692],[0.6556339061137728,0.3544604715413828],[0.8198234051050739,0.29274842216514485],[0.21663268485836085,0.4885129931130625],[0.5015202509703862,0.9434536755629856],[0.12860743004760788,0.8464734600134993],[0.5015909039469217,0.8732190762263743],[0.22190766370810122,0.9492378329242275],[0.6936748597572218,0.7576678411047106],[0.014427954250301855,0.9940297884165028],[0.3261643269630603,0.9962375546129398],[0.1365185447582098,0.10330877059018029],[0.7653198220858934,0.17753397902424273],[0.6471073977618416,0.4462418532469512],[0.8270264883401148,0.34786271701377536],[0.19479688788844207,0.610088343261346],[0.11747881206209687,0.7513482879834688],[0.649740990306384,0.9454272482735635],[0.3892373107224616,0.5891004835385267],[0.05872249341758362,0.8921162475141093],[0.2495835817191303,0.28260929317269556],[0.5083091210636941,0.041556924978993326],[0.2811852467575,0.657775628022857],[0.6978157000780885,0.08457661391214888],[0.6898034920822698,0.6738486335110321],[0.7840438879224627,0.7516346743165261],[0.044847251685728784,0.971465611658691],[0.7833106793942888,0.30163496593217154],[0.996846971338305,0.6651905888267677],[0.18536430320194075,0.7559581122984358],[0.6244982825689787,0.2027660600843415],[0.4044949765007779,0.3528505629815466],[0.2742590289223692,0.65122040866086],[0.3477322727107761,0.051535123267073546],[0.7684101539256407,0.7353159737434598],[0.6307270349866632,0.11998009867593717],[0.26400077172278924,0.8660506608864045],[0.7418933081426473,0.0749271453671243],[0.000028691609959663467,0.09570754118577596],[0.9218636692947293,0.13824746762723672],[0.870101223768857,0.2258804757328281],[0.8127700348786138,0.4607487452179415],[0.05923136081316405,0.49590385154052563],[0.3594545339570483,0.31372564409500003],[0.4996754470045657,0.6625058083500674],[0.3375429511545318,0.8246643752455023],[0.9148831885568784,0.7980593393540243],[0.934208844468299,0.24835218346880894],[0.4073497918220037,0.12367442644081561],[0.9199755920027226,0.06770096975019357],[0.6518835258738642,0.09738536894187066],[0.6340878141857249,0.49002513653203716],[0.9370331991246583,0.20152707445265494],[0.40762862012360634,0.17533081359449354],[0.5286051947235415,0.9348577581737494],[0.007051874793318369,0.48163194480136595],[0.5342119868150608,0.25489183646056035],[0.08966241752014459,0.30103446293691993]],\"edges_vertices\":[[0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95],[96,97],[98,99],[100,101],[102,103],[104,105],[106,107],[108,109],[110,111],[112,113],[114,115],[116,117],[118,119],[120,121],[122,123],[124,125],[126,127],[128,129],[130,131],[132,133],[134,135],[136,137],[138,139],[140,141],[142,143],[144,145],[146,147],[148,149],[150,151],[152,153],[154,155],[156,157],[158,159],[160,161],[162,163],[164,165],[166,167],[168,169],[170,171],[172,173],[174,175],[176,177],[178,179],[180,181],[182,183],[184,185],[186,187],[188,189],[190,191],[192,193],[194,195],[196,197],[198,199]],\"edges_assignment\":[\"U\",\"F\",\"U\",\"M\",\"M\",\"U\",\"V\",\"F\",\"F\",\"U\",\"V\",\"V\",\"F\",\"F\",\"F\",\"V\",\"U\",\"M\",\"V\",\"M\",\"V\",\"F\",\"M\",\"U\",\"V\",\"M\",\"U\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"U\",\"V\",\"M\",\"F\",\"M\",\"M\",\"U\",\"M\",\"V\",\"F\",\"F\",\"U\",\"V\",\"M\",\"U\",\"F\",\"V\",\"U\",\"U\",\"F\",\"U\",\"M\",\"F\",\"V\",\"F\",\"M\",\"M\",\"U\",\"M\",\"V\",\"M\",\"F\",\"U\",\"F\",\"M\",\"V\",\"M\",\"M\",\"U\",\"U\",\"M\",\"F\",\"F\",\"M\",\"M\",\"U\",\"V\",\"M\",\"V\",\"U\",\"F\",\"V\",\"F\",\"V\",\"M\",\"M\",\"U\",\"V\",\"M\",\"V\",\"M\",\"U\",\"M\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-100-lines.fold",
    "content": "{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.5,0],[0.5,1],[0,0.5],[1,0.4999999999999998],[0,0.75],[1,0.75],[0.7500000000000001,0],[0.7500000000000002,1],[1,0.4999999999999999],[0.5,1],[0.4999999999999999,1],[1,0.4999999999999998],[0.25,0],[0.25,1],[0,0.25],[1,0.24999999999999978],[0,1.0000000000000002],[0.9999999999999998,0],[1.0000000000000004,-1.1102230246251565e-16],[8.326672684688674e-17,1],[0.875,0],[0.875,1],[0,0.875],[1,0.8749999999999999],[0.7196699141100893,1],[1,0.32322330470336297],[1,0.7196699141100891],[0.3232233047033631,1],[0.5625,0],[0.5625,1],[0,0.5625],[1,0.5624999999999998],[0,0.6875],[1,0.6874999999999998],[0.6875,0],[0.6875000000000001,1],[0.37499999999999967,1],[1,0.3749999999999998],[1,0.37499999999999967],[0.37499999999999994,1],[0,0.9142135623730949],[1,0.4999999999999998],[0.9142135623730948,-1.1102230246251565e-16],[0.49999999999999994,1],[0.625,0],[0.625,1],[0,0.625],[1,0.6249999999999999],[0,0.25],[0.25000000000000006,0],[0.24999999999999994,0],[0,0.25000000000000006],[0,0.8125],[1,0.8125],[0.8125,0],[0.8125000000000002,1],[0,0.59375],[1,0.5937499999999998],[0.59375,0],[0.5937500000000001,1],[0.78125,0],[0.7812500000000002,1],[0,0.78125],[1,0.78125],[0.3535533905932738,1],[0.7677669529663689,0],[1,0.35355339059327356],[0,0.7677669529663691],[1,0.90625],[0,0.90625],[0.9062500000000002,1],[0.90625,0],[0.640625,0],[0.6406250000000001,1],[0,0.640625],[1,0.6406249999999999],[0,0.6875000000000003],[0.6875,0],[0.6875000000000002,0],[0,0.6875000000000001],[0,0.45136408793424854],[1,0.0371505255611535],[0.45136408793424854,0],[0.037150525561153835,0.9999999999999999],[0.39062499999999994,0],[0.39062500000000006,1],[0,0.39062499999999994],[1,0.39062499999999983],[0,0.8437500000000002],[0.8437500000000001,0],[0.84375,0],[0,0.8437500000000003],[0,0.43121843353822914],[1,0.017004871165134022],[0.4312184335382291,0],[0.017004871165134217,1],[0.6875000000000002,1],[1,0.6875],[1,0.6875000000000002],[0.6875,1],[0,0.1250000000000001],[0.875,1],[0.12500000000000022,0],[1,0.8749999999999996],[0.421875,0],[0.42187500000000006,1],[0,0.421875],[1,0.42187499999999983],[1,0.9062500000000002],[0.9062500000000002,1],[0.9062500000000002,1],[1,0.9062500000000002],[0.18749999999999997,1],[0,0.8125],[1,0.18749999999999972],[0.8125000000000002,-2.465190328815662e-32],[0,0.11611652351681578],[1,0.5303300858899109],[0.11611652351681578,0],[0.5303300858899112,1],[0.359375,1],[0.3593750000000001,0],[1,0.3593749999999998],[0,0.35937500000000006],[0.5308689590993831,1],[0.9450825214724777,0],[1,0.5308689590993831],[0,0.9450825214724778],[0,0.7109375],[1,0.7109375],[0.7109375,0],[0.7109375000000002,1],[1,0.3982864376269048],[0,0.8125],[0.398286437626905,1],[0.8124999999999999,0],[0,0.1375631329235415],[1,0.5517766952966365],[0.1375631329235415,0],[0.5517766952966369,1],[-1.1102230246251565e-16,0.24999999999999978],[0.7500000000000002,1],[0.2500000000000001,0],[1,0.7499999999999998],[0.6294417382415926,1],[1.3877787807814457e-17,0.7392766952966369],[1,0.6294417382415918],[0.739276695296637,0],[0.48046875,0],[0.4804687500000001,1],[0,0.48046875],[1,0.4804687499999999],[0.7895946032737123,2.220446049250313e-16],[0.37538104090061747,1],[0,0.7895946032737126],[1,0.37538104090061725],[0.12499999999999994,1],[1,0.1250000000000001],[1,0.12499999999999983],[0.12500000000000022,1],[0.10546874999999989,1],[1,0.10546875],[1,0.10546874999999978],[0.10546875000000017,1],[0.9222281398266475,1],[0,0.6180005968816916],[1,0.9222281398266468],[0.6180005968816917,0],[0.71875,0],[0.71875,1],[0,0.71875],[1,0.7187499999999998],[0.2500000000000002,1],[1,0.24999999999999983],[1,0.25000000000000006],[0.24999999999999994,1],[0.5000000000000001,1],[0,0.5],[1,0.4999999999999999],[0.5000000000000001,0],[0.5795048711651342,0],[0.9937184335382293,1],[0,0.579504871165134],[1,0.993718433538229],[0,0.05269660940672727],[0.9473033905932728,1],[0.05269660940672727,0],[1,0.9473033905932726],[1,0.9686965913858006],[0.598752734023413,0],[0.9686965913858014,1],[0,0.5987527340234129]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95],[96,97],[98,99],[100,101],[102,103],[104,105],[106,107],[108,109],[110,111],[112,113],[114,115],[116,117],[118,119],[120,121],[122,123],[124,125],[126,127],[128,129],[130,131],[132,133],[134,135],[136,137],[138,139],[140,141],[142,143],[144,145],[146,147],[148,149],[150,151],[152,153],[154,155],[156,157],[158,159],[160,161],[162,163],[164,165],[166,167],[168,169],[170,171],[172,173],[174,175],[176,177],[178,179],[180,181],[182,183],[184,185],[186,187],[188,189],[190,191],[192,193],[194,195]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"F\",\"F\",\"U\",\"U\",\"F\",\"F\",\"M\",\"M\",\"V\",\"V\",\"U\",\"U\",\"M\",\"M\",\"U\",\"U\",\"V\",\"V\",\"U\",\"U\",\"F\",\"F\",\"M\",\"M\",\"U\",\"U\",\"U\",\"U\",\"V\",\"V\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"U\",\"U\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"U\",\"U\",\"F\",\"F\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"U\",\"U\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-25-lines.fold",
    "content": "{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.5,1],[0.5,0],[1,0.4999999999999998],[0,0.5],[0,0.4999999999999999],[0.5,0],[0.4999999999999999,0],[0,0.5000000000000001],[0.75,1],[0.75,0],[1,0.7499999999999998],[0,0.75],[1.0000000000000004,1],[0,1.1102230246251565e-16],[1,1],[2.220446049250313e-16,1.1102230246251565e-16],[0.25000000000000006,1],[0,0.75],[1,0.24999999999999983],[0.7500000000000002,0],[1,0.4999999999999997],[0.49999999999999983,1],[0.49999999999999994,1],[1,0.49999999999999983],[1,0.3964466094067261],[0.04289321881345254,0],[0.39644660940672627,1],[0,0.04289321881345254],[1,0.6249999999999998],[0,0.625],[0.625,1],[0.625,0],[1,0.8124999999999999],[0,0.8125],[0.8125000000000001,1],[0.8125,0],[0.40625000000000006,1],[0.40625,0],[1,0.40624999999999983],[0,0.40625],[0.20312500000000003,1],[0.203125,0],[1,0.2031249999999998],[0,0.203125]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"U\",\"U\",\"F\",\"F\",\"U\",\"U\",\"M\",\"M\",\"V\",\"V\",\"U\",\"U\",\"V\",\"V\",\"F\",\"F\",\"U\",\"U\",\"M\",\"M\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-50-chaotic.fold",
    "content": "{\"vertices_coords\":[[0.4797034538075833,0.892598920466604],[0.5507158128157077,0.16509297593681782],[0.8521735056136139,0.051873563403997736],[0.42245313305846643,0.7312934223233296],[0.31770330959718085,0.43884224919844894],[0.8159139090518952,0.28044651786930364],[0.9560007320993793,0.48629090890420246],[0.010179703288700548,0.04404264205540942],[0.4313375240624697,0.1714978585027338],[0.7650696664656178,0.6559343709813499],[0.7074794551256645,0.23197783975933972],[0.6168263049761071,0.7560866358763139],[0.9251219573192779,0.3261050787945792],[0.35601280841488636,0.23274555234282546],[0.1298770017624482,0.7773140502770843],[0.6685531497490476,0.6093790465241966],[0.6054796782093743,0.6170798069652375],[0.5534278803192345,0.4325961465566821],[0.40745848946368857,0.6033221019952157],[0.8750021542587034,0.9241492258847983],[0.014830238823857478,0.6955764728188152],[0.32718037166507674,0.9015842709287762],[0.03653463550678304,0.9941757996972924],[0.2327079532167251,0.9009591507307382],[0.7545954119900191,0.2343571318609725],[0.5521902402685266,0.6799240227216612],[0.01872517522300443,0.030317863926144994],[0.4423946674378558,0.032953679062658336],[0.977607430619454,0.9128500351711102],[0.2534158617111495,0.9797553213979637],[0.7153862776125459,0.4595480227551867],[0.25118416677745037,0.4197123491234518],[0.2653348934022042,0.4841649920791764],[0.12634111581650354,0.2956992462696306],[0.20646659994938887,0.976470890720152],[0.5779122555712322,0.24745206037687395],[0.6342243025862495,0.3786698570280427],[0.3261115037902125,0.4140272561656084],[0.9379909982117975,0.6341057100963887],[0.3433605458462734,0.6540000824576002],[0.4226053490516788,0.18825685765965128],[0.13284426839617103,0.8273327061899038],[0.2927741684202434,0.8837746954530881],[0.35299479335975703,0.22106832310487468],[0.9527251401184356,0.6340483075259356],[0.3654345330869613,0.37677617355435933],[0.49769023180447824,0.011801375122800994],[0.3346003488507765,0.28771723762253787],[0.6328316619888525,0.5880343304880302],[0.20338308838992614,0.36407768711410604],[0.4410674692137724,0.21349514810291503],[0.9267177188070155,0.11117515337415607],[0.6782305673265991,0.07051511287933954],[0.381008609264168,0.18899443927018655],[0.8197862506887983,0.4346922109761979],[0.9715489475880741,0.44828139487487983],[0.5070604140547377,0.20034143341364063],[0.19736438989825333,0.8053774609631854],[0.8336286739277103,0.9023718816540454],[0.3292456148775387,0.6771832844590102],[0.823552390456328,0.7028200386040027],[0.08503776276017705,0.8120615974951071],[0.8439519415462449,0.3324823691289365],[0.3656369226710845,0.6884010698358338],[0.4351732147744738,0.688110403463505],[0.9477081512212353,0.32622457352105805],[0.18865651699513641,0.7156786718373471],[0.19407614638435033,0.8083350995857548],[0.4649500289369646,0.4031772459260936],[0.9417856788072456,0.659290462225351],[0.6785728877462174,0.7021584169434931],[0.5645827720839274,0.26477114879664887],[0.08709153767948319,0.03901915088983676],[0.9522560743817734,0.23985359415624274],[0.6676537265506948,0.22774430259853728],[0.5506848228975509,0.07645531238464764],[0.7763901501637243,0.023360591583946988],[0.5153397623623417,0.3090314609204905],[0.5756416766321859,0.3116294832569937],[0.12525987464114663,0.1637842982200346],[0.4132959098515503,0.9750831992117468],[0.15810212128399437,0.05465100524383537],[0.488142055201086,0.27807500499454796],[0.6988946838315697,0.9918496646797168],[0.5138888119491805,0.25525203312263667],[0.7967631859624513,0.1868803079555068],[0.5195265267103473,0.6207853806032255],[0.3898915619858587,0.8371805154987175],[0.5710076278543004,0.8986556094361138],[0.5225508147379738,0.3651342683932135],[0.16705810393737996,0.4256250985360912],[0.47419419853665556,0.29306177729287786],[0.046013858935762375,0.25635087554404845],[0.3502552601527742,0.42306714703646864],[0.23085368377602022,0.16267162512651834],[0.1096592017844078,0.7253662999864412],[0.9959501453376964,0.31610811858871624],[0.7184828640689154,0.0174985991192389],[0.1097108942975451,0.8765671216141679],[0.39755506520831996,0.114552330668795]],\"edges_vertices\":[[0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95],[96,97],[98,99]],\"edges_assignment\":[\"V\",\"U\",\"M\",\"M\",\"M\",\"V\",\"U\",\"F\",\"V\",\"M\",\"V\",\"F\",\"V\",\"F\",\"F\",\"U\",\"U\",\"U\",\"M\",\"F\",\"V\",\"U\",\"F\",\"V\",\"U\",\"V\",\"F\",\"F\",\"M\",\"F\",\"U\",\"U\",\"M\",\"U\",\"V\",\"U\",\"V\",\"V\",\"M\",\"V\",\"U\",\"U\",\"U\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-50-lines.fold",
    "content": "{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0,0.5],[1,0.5],[0.5,0],[0.5000000000000002,1],[0,0.25],[1,0.25],[0.25,0],[0.2500000000000002,1],[0,0.7500000000000003],[0.7499999999999998,0],[0.7500000000000002,0],[0,0.7499999999999999],[0,0.603553390593274],[1,0.18933982822017875],[0.603553390593274,0],[0.18933982822017908,1],[1,0.75],[0,0.75],[0.7500000000000002,1],[0.75,0],[0,0.3106601717798214],[0.7500000000000002,0],[0.3106601717798214,0],[0,0.7500000000000007],[0.48223304703363123,1],[0.8964466094067263,2.7755575615628914e-17],[1,0.4822330470336309],[0,0.8964466094067263],[0,0.4999999999999995],[0.5000000000000006,1],[0.49999999999999967,0],[1,0.5000000000000002],[1,0.10355339059327405],[0.7499999999999992,0],[0.1035533905932743,1],[0,0.7499999999999988],[1,0.375],[0,0.375],[0.3750000000000002,1],[0.375,0],[0,0.6464466094067266],[0.6464466094067263,0],[0.6464466094067265,0],[0,0.6464466094067263],[0.646446609406726,0],[1,0.853553390593274],[0,0.646446609406726],[0.8535533905932746,1],[1,0.6875],[0,0.6875],[0.6875000000000002,1],[0.6875,0],[0,0.12500000000000028],[1,0.12500000000000017],[0.12500000000000028,0],[0.1250000000000004,1],[5.551115123125783e-17,0.20710678118654807],[0.5000000000000008,0],[0.20710678118654807,0],[0,0.5000000000000011],[0,0.08578643762690508],[1,0.5000000000000001],[0.08578643762690502,0],[0.5000000000000003,1],[0,0.18750000000000014],[1,0.18750000000000008],[0.18750000000000014,0],[0.1875000000000003,1],[0,0.4375000000000001],[1,0.4375000000000001],[0.43750000000000017,0],[0.43750000000000033,1],[0.84375,0],[0.8437500000000002,1],[0,0.8437500000000001],[1,0.84375],[0.07322330470336358,1],[0.48743686707645806,0],[1,0.07322330470336333],[5.551115123125783e-17,0.48743686707645817],[0,0.6945436482630056],[1,0.2803300858899106],[0.6945436482630056,0],[0.2803300858899108,1],[0,0.5625],[1,0.5625],[0.5625,0],[0.5625000000000002,1],[0,0.25],[0.25000000000000006,0],[0.24999999999999994,0],[0,0.25000000000000006]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"U\",\"U\",\"V\",\"V\",\"U\",\"U\",\"F\",\"F\",\"U\",\"U\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"U\",\"U\",\"M\",\"M\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"U\",\"U\",\"F\",\"F\",\"M\",\"M\",\"U\",\"U\",\"F\",\"F\",\"M\",\"M\",\"U\",\"U\",\"M\",\"M\",\"U\",\"U\",\"F\",\"F\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-500-chaotic.fold",
    "content": "{\"vertices_coords\":[[0.512694,0.398956],[0.238333,0.370397],[0.748054,0.55539],[0.760943,0.376841],[0.949606,0.825831],[0.842599,0.529679],[0.562668,0.669865],[0.239843,0.405129],[0.555449,0.143502],[0.930579,0.808069],[0.973468,0.235352],[0.561911,0.19191],[0.52671,0.043164],[0.350421,0.897127],[0.851784,0.444897],[0.093213,0.889866],[0.849592,0.832352],[0.468484,0.579777],[0.737362,0.288816],[0.747657,0.528824],[0.252539,0.7587],[0.62912,0.867218],[0.216431,0.428364],[0.291385,0.612967],[0.226732,0.045538],[0.022031,0.926358],[0.373301,0.078096],[0.097664,0.651131],[0.635669,0.903866],[0.111237,0.418943],[0.454562,0.444668],[0.434024,0.901093],[0.074272,0.921796],[0.540075,0.868472],[0.794639,0.606525],[0.126619,0.743137],[0.651637,0.116919],[0.268097,0.594054],[0.761442,0.036784],[0.19189,0.326461],[0.302569,0.814952],[0.940719,0.902177],[0.113605,0.745853],[0.653095,0.82703],[0.15266,0.802321],[0.946776,0.415617],[0.377818,0.808851],[0.458251,0.712719],[0.400906,0.616562],[0.974219,0.268686],[0.957515,0.00353],[0.817082,0.618721],[0.36947,0.235229],[0.823004,0.214095],[0.360172,0.381394],[0.541188,0.309863],[0.938521,0.919843],[0.25728,0.804681],[0.25716,0.098503],[0.280951,0.8777],[0.115171,0.786909],[0.299788,0.303758],[0.821613,0.71494],[0.514728,0.615462],[0.480848,0.477008],[0.802234,0.179567],[0.571734,0.489394],[0.375015,0.641014],[0.781833,0.310228],[0.546147,0.691989],[0.955458,0.595245],[0.749746,0.857629],[0.916009,0.76401],[0.826499,0.089101],[0.884962,0.15808],[0.134753,0.934549],[0.645255,0.003976],[0.847188,0.523102],[0.407066,0.943741],[0.378014,0.142527],[0.847354,0.748683],[0.143477,0.225946],[0.482175,0.372056],[0.333942,0.226871],[0.487696,0.463052],[0.68623,0.000257],[0.352817,0.429188],[0.748132,0.418467],[0.395491,0.054679],[0.017883,0.000365],[0.776768,0.652693],[0.988814,0.729038],[0.38915,0.9551],[0.675479,0.396219],[0.693112,0.236088],[0.316549,0.007826],[0.614024,0.573268],[0.355831,0.955539],[0.673841,0.88987],[0.760054,0.956909],[0.563996,0.540437],[0.26131,0.945212],[0.888731,0.194771],[0.143548,0.23212],[0.971367,0.961154],[0.70657,0.386889],[0.227914,0.649947],[0.733544,0.794033],[0.059804,0.80624],[0.936926,0.500705],[0.841194,0.273727],[0.330286,0.563159],[0.707579,0.198117],[0.627574,0.694701],[0.491982,0.026221],[0.40978,0.563323],[0.171226,0.112087],[0.469204,0.975118],[0.478223,0.588691],[0.377303,0.458934],[0.803651,0.553682],[0.729938,0.143263],[0.751498,0.666484],[0.944326,0.339597],[0.224169,0.04164],[0.997972,0.874629],[0.902795,0.92762],[0.579655,0.647044],[0.420032,0.64638],[0.026791,0.285215],[0.213725,0.118097],[0.482801,0.39765],[0.194189,0.478588],[0.4236,0.377348],[0.807793,0.764864],[0.173998,0.279335],[0.052084,0.229994],[0.370489,0.004968],[0.518414,0.891691],[0.506435,0.039424],[0.940099,0.620295],[0.327459,0.724229],[0.326572,0.837069],[0.964113,0.735838],[0.122494,0.743131],[0.320438,0.639331],[0.104105,0.381681],[0.349705,0.232909],[0.182833,0.517423],[0.363991,0.068283],[0.025445,0.448938],[0.486941,0.111551],[0.809268,0.062721],[0.734165,0.142434],[0.099502,0.990251],[0.827839,0.988452],[0.965247,0.705474],[0.327614,0.892802],[0.116845,0.368825],[0.766737,0.879974],[0.316922,0.888028],[0.4946,0.983929],[0.409208,0.082746],[0.649461,0.097009],[0.514746,0.951388],[0.926773,0.72858],[0.178412,0.467483],[0.545144,0.391645],[0.644394,0.89375],[0.013914,0.816133],[0.151653,0.742353],[0.362933,0.884922],[0.889154,0.097994],[0.547316,0.214972],[0.061334,0.134444],[0.542516,0.913289],[0.678033,0.488022],[0.068082,0.943952],[0.579137,0.28319],[0.85517,0.518301],[0.482813,0.562932],[0.326272,0.052358],[0.840101,0.744211],[0.159611,0.827881],[0.809823,0.935862],[0.535355,0.759707],[0.208338,0.582174],[0.214066,0.689409],[0.533613,0.690571],[0.723003,0.135033],[0.842261,0.452714],[0.266605,0.671725],[0.729621,0.598459],[0.318351,0.311719],[0.364278,0.684161],[0.981152,0.042476],[0.614499,0.735749],[0.727897,0.962137],[0.832839,0.712004],[0.006429,0.568377],[0.269253,0.037385],[0.407008,0.8889],[0.088205,0.370008],[0.388683,0.811242],[0.747143,0.140955],[0.15201,0.374251],[0.476493,0.290786],[0.644514,0.98573],[0.806518,0.800901],[0.284667,0.783941],[0.274468,0.967669],[0.05807,0.080687],[0.384595,0.755238],[0.073849,0.968466],[0.473372,0.158632],[0.214167,0.946197],[0.554407,0.237657],[0.092202,0.500234],[0.879966,0.385716],[0.444229,0.384221],[0.937893,0.367026],[0.861924,0.975818],[0.854184,0.1801],[0.752521,0.104386],[0.131131,0.137712],[0.852208,0.836807],[0.085432,0.993336],[0.276364,0.948602],[0.339725,0.480001],[0.886391,0.509775],[0.602947,0.048123],[0.577045,0.500714],[0.834913,0.31359],[0.605954,0.398307],[0.521532,0.020279],[0.208464,0.255478],[0.709524,0.305136],[0.136903,0.502741],[0.098454,0.622649],[0.000247,0.803409],[0.981355,0.594243],[0.960367,0.190541],[0.969368,0.378202],[0.01767,0.078379],[0.390671,0.305433],[0.702192,0.912669],[0.499729,0.556278],[0.117357,0.863282],[0.089249,0.618715],[0.20319,0.445432],[0.017635,0.105143],[0.899172,0.713298],[0.205522,0.247141],[0.461275,0.780688],[0.875627,0.591111],[0.260773,0.914281],[0.615044,0.550442],[0.447853,0.235586],[0.896884,0.986998],[0.162497,0.064307],[0.816671,0.870176],[0.789921,0.649002],[0.846416,0.320257],[0.368177,0.91872],[0.454116,0.184577],[0.615678,0.72177],[0.167208,0.652123],[0.047566,0.985369],[0.768114,0.156652],[0.468896,0.856558],[0.921653,0.449834],[0.66457,0.067229],[0.128126,0.555638],[0.977607,0.942164],[0.438312,0.090449],[0.106691,0.244637],[0.746899,0.736527],[0.24165,0.096248],[0.446657,0.146773],[0.498672,0.071061],[0.99649,0.727191],[0.253418,0.211451],[0.036092,0.637544],[0.847352,0.05993],[0.747491,0.780768],[0.251797,0.349101],[0.833126,0.480286],[0.051879,0.464166],[0.277126,0.828138],[0.344184,0.905026],[0.546768,0.297479],[0.285169,0.57373],[0.633556,0.763622],[0.056359,0.684885],[0.601273,0.813928],[0.967618,0.35334],[0.910502,0.402441],[0.268122,0.571223],[0.06108,0.708594],[0.93609,0.765277],[0.55266,0.895849],[0.390038,0.371264],[0.225458,0.825823],[0.314349,0.920905],[0.91423,0.233341],[0.071676,0.121057],[0.894752,0.153188],[0.897103,0.496941],[0.976277,0.504928],[0.181638,0.191246],[0.649619,0.971724],[0.926906,0.852575],[0.188156,0.529226],[0.140852,0.746231],[0.555327,0.617873],[0.2115,0.180102],[0.280753,0.598531],[0.915378,0.762673],[0.569117,0.686339],[0.541655,0.362293],[0.080797,0.417701],[0.037118,0.346382],[0.174755,0.127137],[0.059632,0.227647],[0.9129,0.203217],[0.221931,0.249753],[0.852415,0.396004],[0.372462,0.252181],[0.327777,0.668449],[0.180084,0.983697],[0.691488,0.167234],[0.701079,0.608888],[0.720665,0.773038],[0.932591,0.850087],[0.974244,0.880741],[0.040474,0.881675],[0.386308,0.864215],[0.165543,0.581375],[0.382193,0.152798],[0.483929,0.405514],[0.842164,0.729763],[0.700798,0.902961],[0.896394,0.419437],[0.486753,0.292095],[0.827631,0.75936],[0.230127,0.380196],[0.922515,0.111973],[0.326346,0.800335],[0.959228,0.82356],[0.418429,0.388395],[0.53979,0.285638],[0.126238,0.354294],[0.547913,0.567883],[0.686453,0.674923],[0.173916,0.835465],[0.904085,0.1197],[0.068051,0.796427],[0.762182,0.592928],[0.739506,0.374495],[0.757081,0.818699],[0.953672,0.880994],[0.306135,0.4399],[0.995503,0.029231],[0.443041,0.88188],[0.793,0.065428],[0.91394,0.732395],[0.278113,0.55678],[0.286039,0.842641],[0.230405,0.591082],[0.60042,0.042168],[0.719358,0.80235],[0.825637,0.486124],[0.633481,0.377009],[0.048699,0.94626],[0.696885,0.382375],[0.361901,0.668027],[0.057459,0.899189],[0.89675,0.847142],[0.609605,0.168273],[0.906276,0.82823],[0.592788,0.153636],[0.119153,0.596752],[0.525207,0.359538],[0.655012,0.149798],[0.717117,0.334096],[0.649151,0.769411],[0.723631,0.847124],[0.968022,0.590277],[0.20644,0.183894],[0.685407,0.364778],[0.57739,0.229421],[0.147057,0.712275],[0.036851,0.897816],[0.092498,0.566073],[0.720792,0.167253],[0.408879,0.563301],[0.888607,0.159617],[0.330514,0.485923],[0.558188,0.60147],[0.156584,0.245802],[0.045559,0.611038],[0.882371,0.407799],[0.13543,0.062399],[0.13038,0.293344],[0.336209,0.225351],[0.648635,0.556928],[0.169254,0.129517],[0.539446,0.601922],[0.544115,0.963678],[0.971813,0.668595],[0.044179,0.344386],[0.76296,0.091941],[0.267905,0.251847],[0.878064,0.254633],[0.936663,0.434574],[0.760341,0.383684],[0.319697,0.851905],[0.063388,0.061997],[0.598551,0.807452],[0.523635,0.732951],[0.946954,0.377888],[0.252987,0.084809],[0.603645,0.897773],[0.059404,0.23929],[0.339155,0.172001],[0.779925,0.966061],[0.861697,0.670017],[0.220636,0.069023],[0.564441,0.451395],[0.197563,0.460395],[0.71429,0.498258],[0.454426,0.657824],[0.07629,0.4514],[0.742754,0.184666],[0.293316,0.977275],[0.252932,0.808372],[0.250561,0.477098],[0.906788,0.710293],[0.758571,0.56957],[0.113267,0.919851],[0.371528,0.856591],[0.5104,0.023958],[0.780776,0.899894],[0.06039,0.379116],[0.406734,0.024122],[0.133378,0.059884],[0.5211,0.643395],[0.456158,0.380994],[0.147456,0.889887],[0.231248,0.852295],[0.544817,0.612712],[0.740824,0.886102],[0.221869,0.045398],[0.088757,0.523355],[0.249967,0.739519],[0.786173,0.849752],[0.143235,0.189755],[0.264963,0.607646],[0.636468,0.96963],[0.965995,0.565614],[0.71398,0.01245],[0.483022,0.57427],[0.027019,0.466485],[0.103003,0.04581],[0.675429,0.799553],[0.649894,0.348454],[0.475204,0.248703],[0.024096,0.653391],[0.264746,0.203041],[0.753145,0.32252],[0.603575,0.10154],[0.152191,0.941419],[0.597867,0.551661],[0.815879,0.998298],[0.410348,0.323777],[0.336281,0.270492],[0.723035,0.955901],[0.119029,0.013433],[0.437791,0.14577],[0.006877,0.922018],[0.180057,0.873799],[0.164968,0.337412],[0.921821,0.330254],[0.678665,0.306737],[0.623851,0.98134],[0.03321,0.349301],[0.698403,0.682248],[0.322892,0.173064],[0.489341,0.520987],[0.34771,0.846721],[0.946858,0.086333],[0.165419,0.40279],[0.029757,0.157374],[0.441089,0.278884],[0.101199,0.382199],[0.108272,0.88707],[0.879177,0.278884],[0.589452,0.213864],[0.876603,0.234776],[0.499253,0.150434],[0.817611,0.839816],[0.586752,0.882787],[0.948396,0.500485],[0.598987,0.929865],[0.251337,0.180537],[0.128847,0.775588],[0.251409,0.012025],[0.46018,0.322829],[0.283138,0.926835],[0.473826,0.195777],[0.649883,0.495665],[0.426326,0.325181],[0.957991,0.245555],[0.12585,0.092507],[0.226021,0.744627],[0.795286,0.314947],[0.136765,0.543431],[0.828866,0.80222],[0.980278,0.718949],[0.094587,0.390913],[0.149341,0.391406],[0.507569,0.360792],[0.55404,0.698389],[0.843926,0.94351],[0.230563,0.885968],[0.18262,0.667928],[0.621029,0.662112],[0.307556,0.701808],[0.754545,0.839087],[0.497114,0.676379],[0.461978,0.496863],[0.595376,0.047183],[0.185001,0.592296],[0.88312,0.437698],[0.730435,0.099218],[0.644819,0.298355],[0.743142,0.62041],[0.363136,0.793545],[0.116843,0.965202],[0.761119,0.341234],[0.941656,0.121633],[0.422533,0.932082],[0.415989,0.862621],[0.688604,0.650342],[0.511982,0.340697],[0.446177,0.347775],[0.902774,0.073157],[0.907587,0.701725],[0.04687,0.349],[0.924604,0.066524],[0.165054,0.475553],[0.780116,0.154001],[0.548265,0.187028],[0.12715,0.033724],[0.434082,0.761286],[0.993219,0.307414],[0.692215,0.462891],[0.578489,0.613025],[0.329131,0.634023],[0.402909,0.416854],[0.414994,0.045449],[0.815485,0.803188],[0.152342,0.888365],[0.697861,0.211068],[0.392794,0.19173],[0.600448,0.345283],[0.626181,0.746962],[0.950667,0.762131],[0.354525,0.816477],[0.039656,0.050517],[0.805764,0.793472],[0.824217,0.261484],[0.782689,0.578074],[0.40118,0.834945],[0.541813,0.979395],[0.362164,0.334218],[0.908993,0.76194],[0.769401,0.741183],[0.925134,0.041344],[0.805867,0.611207],[0.478814,0.312217],[0.141641,0.519937],[0.828943,0.403849],[0.223,0.430053],[0.304058,0.617263],[0.069116,0.738709],[0.002773,0.91214],[0.408817,0.477713],[0.86323,0.041367],[0.249974,0.356649],[0.753275,0.367721],[0.63957,0.804639],[0.223485,0.150656],[0.198188,0.326868],[0.975089,0.35422],[0.947088,0.175798],[0.935734,0.469019],[0.425574,0.064383],[0.529066,0.429176],[0.913126,0.61536],[0.927501,0.330665],[0.185976,0.860617],[0.179967,0.714953],[0.222467,0.899242],[0.518744,0.40186],[0.169995,0.085965],[0.559347,0.569878],[0.891392,0.219852],[0.841815,0.126505],[0.230293,0.054555],[0.511484,0.106317],[0.681738,0.848121],[0.492725,0.995535],[0.535266,0.053049],[0.216451,0.247811],[0.079344,0.50626],[0.906308,0.488636],[0.316286,0.763004],[0.706518,0.179753],[0.926947,0.104891],[0.370146,0.719029],[0.521856,0.62375],[0.927861,0.816904],[0.803022,0.957658],[0.796717,0.502403],[0.384506,0.974708],[0.129313,0.493512],[0.230433,0.354685],[0.722989,0.00004],[0.831757,0.507562],[0.584584,0.883211],[0.373626,0.480492],[0.740914,0.008321],[0.531122,0.825106],[0.461132,0.167985],[0.150026,0.591893],[0.332029,0.059854],[0.412089,0.856915],[0.235865,0.016728],[0.723419,0.670859],[0.964239,0.560223],[0.054416,0.886436],[0.234007,0.35826],[0.80803,0.234365],[0.696402,0.813212],[0.960605,0.957059],[0.027488,0.809632],[0.699513,0.993708],[0.805419,0.980657],[0.937092,0.95147],[0.834829,0.419799],[0.474743,0.251315],[0.568121,0.839393],[0.679303,0.374394],[0.232298,0.113077],[0.210004,0.123641],[0.55883,0.733864],[0.138095,0.563549],[0.558421,0.291755],[0.943604,0.038111],[0.888905,0.473827],[0.895901,0.640956],[0.380582,0.390709],[0.872647,0.596],[0.063156,0.283032],[0.159971,0.836313],[0.101766,0.189933],[0.099395,0.756469],[0.520649,0.697699],[0.745682,0.075282],[0.424446,0.385954],[0.023849,0.392396],[0.64122,0.459083],[0.616157,0.218502],[0.277095,0.310514],[0.07829,0.911096],[0.343741,0.905479],[0.614304,0.246652],[0.750747,0.514009],[0.612866,0.547157],[0.777316,0.582188],[0.443266,0.728439],[0.344692,0.964614],[0.494767,0.679282],[0.788424,0.955755],[0.16739,0.396028],[0.520628,0.292679],[0.068767,0.003431],[0.140806,0.141186],[0.105685,0.199214],[0.037909,0.153482],[0.131772,0.179905],[0.037586,0.307263],[0.005431,0.209484],[0.916449,0.95643],[0.424901,0.267501],[0.550815,0.613631],[0.510276,0.857705],[0.555778,0.046404],[0.86918,0.274239],[0.251173,0.309688],[0.014686,0.215746],[0.300247,0.194099],[0.934108,0.049362],[0.874819,0.873247],[0.758495,0.168838],[0.426676,0.198647],[0.812091,0.238255],[0.342491,0.20697],[0.234178,0.750496],[0.470036,0.639812],[0.093254,0.405588],[0.860919,0.183135],[0.812164,0.871902],[0.532697,0.745472],[0.353041,0.473999],[0.89342,0.245835],[0.577998,0.468221],[0.55379,0.407798],[0.648018,0.824952],[0.053454,0.919572],[0.138452,0.341629],[0.267733,0.8144],[0.798378,0.425544],[0.666474,0.376296],[0.199447,0.747596],[0.033294,0.890839],[0.413915,0.235813],[0.209706,0.342046],[0.784607,0.71825],[0.484308,0.662942],[0.654858,0.60847],[0.58371,0.619377],[0.706151,0.102913],[0.507863,0.819902],[0.16282,0.074887],[0.129626,0.461121],[0.161627,0.000257],[0.02372,0.904234],[0.33297,0.775018],[0.371142,0.229174],[0.043228,0.635254],[0.080882,0.547462],[0.137093,0.955311],[0.436435,0.454843],[0.19433,0.932516],[0.573313,0.948633],[0.504662,0.866833],[0.363845,0.297155],[0.43698,0.138122],[0.331519,0.339895],[0.806337,0.353452],[0.168732,0.079113],[0.39802,0.101953],[0.604803,0.640621],[0.273393,0.672421],[0.982067,0.937588],[0.322951,0.300274],[0.564764,0.242775],[0.87519,0.727273],[0.780089,0.701493],[0.5875,0.545421],[0.573069,0.928334],[0.633801,0.988527],[0.165253,0.518352],[0.301758,0.309206],[0.673649,0.71547],[0.858177,0.906799],[0.977245,0.878893],[0.417628,0.77344],[0.100497,0.291336],[0.929545,0.568941],[0.944157,0.920938],[0.603717,0.879726],[0.079002,0.891673],[0.403615,0.386819],[0.21978,0.581745],[0.005662,0.466511],[0.491856,0.123643],[0.560565,0.364056],[0.837685,0.117814],[0.000004,0.188671],[0.98329,0.753881],[0.707967,0.171696],[0.965213,0.840476],[0.170816,0.30296],[0.68048,0.986187],[0.766079,0.530853],[0.605537,0.638876],[0.727176,0.615442],[0.040722,0.932497],[0.705309,0.071333],[0.064604,0.152364],[0.387049,0.749513],[0.495726,0.188912],[0.80577,0.023443],[0.087885,0.962023],[0.626404,0.726897],[0.793909,0.516854],[0.798063,0.767605],[0.122366,0.715249],[0.367071,0.181832],[0.420128,0.300153],[0.09168,0.528387],[0.395163,0.287847],[0.462996,0.588254],[0.304771,0.152758],[0.137754,0.155683],[0.680415,0.592998],[0.96782,0.101945],[0.372607,0.104358],[0.435263,0.052855],[0.556357,0.32122],[0.795192,0.368082],[0.570258,0.717099],[0.321704,0.131445],[0.233081,0.552447],[0.022781,0.42482],[0.343658,0.899586],[0.50274,0.329766],[0.993485,0.609191],[0.927579,0.123421],[0.120637,0.404825],[0.670864,0.66445],[0.133865,0.779296],[0.474652,0.679509],[0.671011,0.338825],[0.180309,0.464055],[0.559868,0.026344],[0.423944,0.928272],[0.190771,0.075357],[0.878531,0.521766],[0.413026,0.971633],[0.312737,0.723286],[0.381925,0.357974],[0.566774,0.455651],[0.735741,0.813015],[0.972396,0.478716],[0.376522,0.992874],[0.432082,0.338919],[0.769369,0.564124],[0.04404,0.898243],[0.584726,0.221841],[0.291177,0.449375],[0.871289,0.657991],[0.796302,0.294858],[0.620546,0.9522],[0.89741,0.662765],[0.313277,0.15467],[0.208088,0.676856],[0.611658,0.171288],[0.170757,0.670787],[0.378227,0.153434],[0.510221,0.846467],[0.772052,0.202853],[0.390678,0.451736],[0.872226,0.085507],[0.024661,0.385361],[0.643164,0.982293],[0.955262,0.248398],[0.20831,0.748262],[0.355801,0.013768],[0.818017,0.066753],[0.770177,0.443031],[0.08151,0.363148],[0.881869,0.113244],[0.808859,0.355405],[0.841206,0.951932],[0.752385,0.847731],[0.864588,0.308145],[0.199788,0.855151],[0.234264,0.176083],[0.858507,0.286314],[0.686062,0.181678],[0.10337,0.443782],[0.11769,0.641802],[0.861394,0.019669],[0.629145,0.320418],[0.445051,0.045952],[0.035145,0.46435],[0.584345,0.709629],[0.175196,0.25459],[0.089263,0.622169],[0.974811,0.301282],[0.799501,0.679714],[0.89407,0.170412],[0.204876,0.10896],[0.421537,0.719026],[0.088155,0.405283],[0.946857,0.478118],[0.956735,0.61319],[0.102973,0.294433],[0.367891,0.025301],[0.841942,0.673472],[0.286999,0.881261],[0.580621,0.592136],[0.294383,0.805717],[0.85834,0.428222],[0.579897,0.915939],[0.497911,0.124967],[0.947076,0.147503],[0.618296,0.435709],[0.695619,0.622884],[0.285781,0.835401],[0.075053,0.124688],[0.541133,0.859883],[0.951934,0.537907],[0.179552,0.704989],[0.819809,0.52049],[0.288307,0.185094],[0.282384,0.798805],[0.946981,0.832823],[0.86209,0.434443],[0.650227,0.622597],[0.620564,0.329861],[0.207167,0.483434],[0.500312,0.937884],[0.086109,0.877412],[0.684461,0.084943],[0.674692,0.637259],[0.271696,0.145478],[0.658986,0.50043],[0.489787,0.586255],[0.779755,0.646184],[0.590694,0.789493],[0.807737,0.251165],[0.714339,0.820101],[0.676049,0.054922],[0.413936,0.189647],[0.061421,0.100969],[0.383752,0.199388],[0.257157,0.816517],[0.491577,0.029831],[0.1609,0.791294],[0.650024,0.560622],[0.575076,0.557675],[0.91432,0.591292],[0.665257,0.895889],[0.201769,0.279229],[0.062088,0.417547],[0.827917,0.638952],[0.499505,0.619405],[0.209522,0.524048],[0.532447,0.645044],[0.832367,0.605479],[0.88559,0.772889],[0.360559,0.194414],[0.73581,0.1788],[0.797185,0.836289],[0.951648,0.798233],[0.796012,0.002664],[0.9184,0.565268],[0.735559,0.735662],[0.349161,0.926358],[0.565121,0.636899],[0.213617,0.58465],[0.536949,0.77877],[0.598522,0.947132],[0.494816,0.990385],[0.438941,0.051423],[0.714703,0.082636],[0.129687,0.227666],[0.467537,0.999281],[0.349201,0.292068],[0.143815,0.294472],[0.783223,0.836729],[0.457107,0.560003],[0.382233,0.143796],[0.702474,0.514622],[0.188479,0.924715],[0.913439,0.689234],[0.145405,0.988209],[0.210637,0.503725],[0.904773,0.228163],[0.590963,0.343261],[0.664963,0.263694],[0.33511,0.465042],[0.869946,0.022338],[0.26061,0.338972],[0.246396,0.745708],[0.71857,0.469017],[0.435142,0.772174],[0.938697,0.3821],[0.071493,0.397784],[0.392154,0.941518],[0.249293,0.498052],[0.222497,0.746389],[0.520384,0.187515],[0.358323,0.346377],[0.349755,0.186998],[0.714579,0.674017],[0.170089,0.434059],[0.900904,0.483404],[0.920118,0.350316],[0.030655,0.052578],[0.180614,0.995071],[0.711654,0.835128],[0.9224,0.314441],[0.773998,0.18384],[0.786739,0.375631]],\"edges_vertices\":[[0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95],[96,97],[98,99],[100,101],[102,103],[104,105],[106,107],[108,109],[110,111],[112,113],[114,115],[116,117],[118,119],[120,121],[122,123],[124,125],[126,127],[128,129],[130,131],[132,133],[134,135],[136,137],[138,139],[140,141],[142,143],[144,145],[146,147],[148,149],[150,151],[152,153],[154,155],[156,157],[158,159],[160,161],[162,163],[164,165],[166,167],[168,169],[170,171],[172,173],[174,175],[176,177],[178,179],[180,181],[182,183],[184,185],[186,187],[188,189],[190,191],[192,193],[194,195],[196,197],[198,199],[200,201],[202,203],[204,205],[206,207],[208,209],[210,211],[212,213],[214,215],[216,217],[218,219],[220,221],[222,223],[224,225],[226,227],[228,229],[230,231],[232,233],[234,235],[236,237],[238,239],[240,241],[242,243],[244,245],[246,247],[248,249],[250,251],[252,253],[254,255],[256,257],[258,259],[260,261],[262,263],[264,265],[266,267],[268,269],[270,271],[272,273],[274,275],[276,277],[278,279],[280,281],[282,283],[284,285],[286,287],[288,289],[290,291],[292,293],[294,295],[296,297],[298,299],[300,301],[302,303],[304,305],[306,307],[308,309],[310,311],[312,313],[314,315],[316,317],[318,319],[320,321],[322,323],[324,325],[326,327],[328,329],[330,331],[332,333],[334,335],[336,337],[338,339],[340,341],[342,343],[344,345],[346,347],[348,349],[350,351],[352,353],[354,355],[356,357],[358,359],[360,361],[362,363],[364,365],[366,367],[368,369],[370,371],[372,373],[374,375],[376,377],[378,379],[380,381],[382,383],[384,385],[386,387],[388,389],[390,391],[392,393],[394,395],[396,397],[398,399],[400,401],[402,403],[404,405],[406,407],[408,409],[410,411],[412,413],[414,415],[416,417],[418,419],[420,421],[422,423],[424,425],[426,427],[428,429],[430,431],[432,433],[434,435],[436,437],[438,439],[440,441],[442,443],[444,445],[446,447],[448,449],[450,451],[452,453],[454,455],[456,457],[458,459],[460,461],[462,463],[464,465],[466,467],[468,469],[470,471],[472,473],[474,475],[476,477],[478,479],[480,481],[482,483],[484,485],[486,487],[488,489],[490,491],[492,493],[494,495],[496,497],[498,499],[500,501],[502,503],[504,505],[506,507],[508,509],[510,511],[512,513],[514,515],[516,517],[518,519],[520,521],[522,523],[524,525],[526,527],[528,529],[530,531],[532,533],[534,535],[536,537],[538,539],[540,541],[542,543],[544,545],[546,547],[548,549],[550,551],[552,553],[554,555],[556,557],[558,559],[560,561],[562,563],[564,565],[566,567],[568,569],[570,571],[572,573],[574,575],[576,577],[578,579],[580,581],[582,583],[584,585],[586,587],[588,589],[590,591],[592,593],[594,595],[596,597],[598,599],[600,601],[602,603],[604,605],[606,607],[608,609],[610,611],[612,613],[614,615],[616,617],[618,619],[620,621],[622,623],[624,625],[626,627],[628,629],[630,631],[632,633],[634,635],[636,637],[638,639],[640,641],[642,643],[644,645],[646,647],[648,649],[650,651],[652,653],[654,655],[656,657],[658,659],[660,661],[662,663],[664,665],[666,667],[668,669],[670,671],[672,673],[674,675],[676,677],[678,679],[680,681],[682,683],[684,685],[686,687],[688,689],[690,691],[692,693],[694,695],[696,697],[698,699],[700,701],[702,703],[704,705],[706,707],[708,709],[710,711],[712,713],[714,715],[716,717],[718,719],[720,721],[722,723],[724,725],[726,727],[728,729],[730,731],[732,733],[734,735],[736,737],[738,739],[740,741],[742,743],[744,745],[746,747],[748,749],[750,751],[752,753],[754,755],[756,757],[758,759],[760,761],[762,763],[764,765],[766,767],[768,769],[770,771],[772,773],[774,775],[776,777],[778,779],[780,781],[782,783],[784,785],[786,787],[788,789],[790,791],[792,793],[794,795],[796,797],[798,799],[800,801],[802,803],[804,805],[806,807],[808,809],[810,811],[812,813],[814,815],[816,817],[818,819],[820,821],[822,823],[824,825],[826,827],[828,829],[830,831],[832,833],[834,835],[836,837],[838,839],[840,841],[842,843],[844,845],[846,847],[848,849],[850,851],[852,853],[854,855],[856,857],[858,859],[860,861],[862,863],[864,865],[866,867],[868,869],[870,871],[872,873],[874,875],[876,877],[878,879],[880,881],[882,883],[884,885],[886,887],[888,889],[890,891],[892,893],[894,895],[896,897],[898,899],[900,901],[902,903],[904,905],[906,907],[908,909],[910,911],[912,913],[914,915],[916,917],[918,919],[920,921],[922,923],[924,925],[926,927],[928,929],[930,931],[932,933],[934,935],[936,937],[938,939],[940,941],[942,943],[944,945],[946,947],[948,949],[950,951],[952,953],[954,955],[956,957],[958,959],[960,961],[962,963],[964,965],[966,967],[968,969],[970,971],[972,973],[974,975],[976,977],[978,979],[980,981],[982,983],[984,985],[986,987],[988,989],[990,991],[992,993],[994,995],[996,997],[998,999]],\"edges_assignment\":[\"M\",\"U\",\"F\",\"F\",\"V\",\"U\",\"F\",\"F\",\"F\",\"U\",\"F\",\"U\",\"M\",\"U\",\"M\",\"V\",\"U\",\"U\",\"M\",\"F\",\"M\",\"U\",\"F\",\"F\",\"F\",\"U\",\"V\",\"M\",\"M\",\"F\",\"U\",\"V\",\"F\",\"U\",\"U\",\"F\",\"V\",\"M\",\"M\",\"M\",\"V\",\"F\",\"U\",\"U\",\"U\",\"U\",\"U\",\"F\",\"F\",\"V\",\"F\",\"F\",\"U\",\"V\",\"M\",\"U\",\"F\",\"M\",\"V\",\"F\",\"M\",\"M\",\"F\",\"M\",\"F\",\"U\",\"F\",\"F\",\"F\",\"V\",\"V\",\"M\",\"M\",\"F\",\"V\",\"F\",\"M\",\"U\",\"U\",\"M\",\"M\",\"V\",\"U\",\"V\",\"V\",\"M\",\"U\",\"F\",\"U\",\"V\",\"M\",\"U\",\"V\",\"V\",\"F\",\"U\",\"U\",\"V\",\"U\",\"U\",\"U\",\"F\",\"F\",\"U\",\"V\",\"F\",\"U\",\"M\",\"V\",\"U\",\"F\",\"V\",\"M\",\"M\",\"U\",\"M\",\"M\",\"V\",\"U\",\"M\",\"U\",\"V\",\"M\",\"M\",\"V\",\"U\",\"U\",\"M\",\"V\",\"F\",\"V\",\"V\",\"M\",\"M\",\"U\",\"F\",\"F\",\"V\",\"V\",\"M\",\"U\",\"F\",\"M\",\"M\",\"U\",\"U\",\"F\",\"U\",\"V\",\"V\",\"F\",\"U\",\"F\",\"F\",\"V\",\"M\",\"V\",\"U\",\"U\",\"U\",\"F\",\"M\",\"M\",\"U\",\"F\",\"F\",\"M\",\"V\",\"M\",\"M\",\"V\",\"M\",\"V\",\"F\",\"U\",\"U\",\"U\",\"F\",\"V\",\"F\",\"U\",\"M\",\"V\",\"F\",\"V\",\"M\",\"F\",\"V\",\"U\",\"V\",\"M\",\"M\",\"F\",\"U\",\"V\",\"F\",\"F\",\"V\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"U\",\"M\",\"M\",\"F\",\"U\",\"F\",\"U\",\"M\",\"V\",\"F\",\"V\",\"F\",\"M\",\"V\",\"F\",\"U\",\"M\",\"F\",\"F\",\"M\",\"V\",\"M\",\"V\",\"V\",\"M\",\"F\",\"M\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"F\",\"M\",\"M\",\"F\",\"V\",\"V\",\"M\",\"F\",\"V\",\"F\",\"V\",\"V\",\"U\",\"M\",\"M\",\"U\",\"U\",\"U\",\"F\",\"F\",\"M\",\"F\",\"V\",\"F\",\"U\",\"V\",\"V\",\"F\",\"U\",\"V\",\"U\",\"F\",\"V\",\"M\",\"F\",\"U\",\"V\",\"U\",\"F\",\"F\",\"M\",\"V\",\"V\",\"F\",\"V\",\"M\",\"V\",\"M\",\"U\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"U\",\"U\",\"V\",\"U\",\"M\",\"M\",\"M\",\"V\",\"U\",\"V\",\"U\",\"U\",\"F\",\"U\",\"F\",\"F\",\"U\",\"V\",\"M\",\"U\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"U\",\"M\",\"U\",\"M\",\"V\",\"U\",\"M\",\"M\",\"U\",\"F\",\"V\",\"U\",\"V\",\"M\",\"F\",\"F\",\"F\",\"U\",\"F\",\"V\",\"M\",\"V\",\"U\",\"F\",\"U\",\"U\",\"M\",\"M\",\"M\",\"U\",\"U\",\"M\",\"M\",\"U\",\"U\",\"F\",\"F\",\"M\",\"M\",\"F\",\"U\",\"M\",\"F\",\"F\",\"M\",\"V\",\"F\",\"U\",\"V\",\"U\",\"U\",\"U\",\"M\",\"M\",\"M\",\"V\",\"V\",\"U\",\"U\",\"U\",\"M\",\"U\",\"M\",\"V\",\"F\",\"F\",\"U\",\"F\",\"M\",\"U\",\"V\",\"V\",\"V\",\"F\",\"U\",\"M\",\"F\",\"F\",\"V\",\"F\",\"F\",\"M\",\"U\",\"M\",\"F\",\"U\",\"U\",\"M\",\"V\",\"F\",\"U\",\"F\",\"U\",\"F\",\"U\",\"F\",\"F\",\"V\",\"U\",\"M\",\"M\",\"F\",\"V\",\"V\",\"M\",\"U\",\"F\",\"M\",\"V\",\"F\",\"V\",\"M\",\"V\",\"F\",\"U\",\"U\",\"U\",\"V\",\"M\",\"U\",\"F\",\"U\",\"V\",\"M\",\"F\",\"M\",\"V\",\"F\",\"U\",\"V\",\"M\",\"M\",\"F\",\"M\",\"M\",\"U\",\"F\",\"F\",\"V\",\"F\",\"F\",\"M\",\"V\",\"M\",\"M\",\"M\",\"F\",\"U\",\"F\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"U\",\"F\",\"M\",\"F\",\"U\",\"M\",\"U\",\"M\",\"V\",\"F\",\"V\",\"M\",\"U\",\"V\",\"U\",\"V\",\"V\",\"F\",\"V\",\"U\",\"V\",\"F\"]}\n"
  },
  {
    "path": "tests/files/fold/non-planar-75-lines.fold",
    "content": "{\"frame_classes\":[\"creasePattern\"],\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0.5,1],[0.5,0],[1,0.4999999999999998],[0,0.5],[1.0000000000000004,1],[0,5.551115123125783e-17],[1,1],[1.1102230246251565e-16,0],[0.5857864376269051,-1.1102230246251565e-16],[1,0.9999999999999996],[0,0.5857864376269051],[1,0.9999999999999999],[1,-2.220446049250313e-16],[0,1],[-2.220446049250313e-16,1],[0.9999999999999998,0],[1,0.2928932188134522],[0,0.7071067811865475],[0.2928932188134525,1],[0.7071067811865474,0],[0.5857864376269049,1],[0.9999999999999999,0],[0.9999999999999999,0.5857864376269046],[4.930380657631324e-32,1],[0,0.24999999999999994],[1,0.24999999999999983],[0.24999999999999994,0],[0.25000000000000006,1],[0,0.625],[1,0.625],[0.625,0],[0.6250000000000002,1],[1,0.4999999999999997],[0.5000000000000001,0],[0.5,1],[0,0.49999999999999994],[1,0.20710678118654724],[0.20710678118654743,1],[0.20710678118654746,1],[1,0.20710678118654732],[0,0.3125],[1,0.3125],[0.3125,0],[0.3125000000000002,0.9999999999999999],[0,0.5],[1,0.08578643762690484],[0.49999999999999994,0],[0.08578643762690505,1],[0,0.11611652351681528],[1,0.5303300858899103],[0.11611652351681528,0],[0.5303300858899107,1],[0,0.28125],[1,0.28124999999999994],[0.28125,0],[0.28125000000000017,0.9999999999999999],[0.18933982822017875,1],[0.6035533905932737,0],[1,0.18933982822017856],[0,0.6035533905932737],[1,0.5732233047033629],[0,0.1590097423302677],[0.5732233047033632,1],[0.1590097423302677,0],[-9.197388681172373e-17,1],[0.41421356237309503,0],[1,-3.1401849173675503e-16],[1.1102230246251565e-16,0.4142135623730951],[0,0.8409902576697321],[1,0.4267766952966366],[0.8409902576697321,0],[0.4267766952966368,1],[0.20710678118654752,0],[0,0.5],[5.551115123125783e-17,0.20710678118654752],[0.4999999999999997,0],[0,0.5625],[1,0.5624999999999998],[0.5625,0],[0.5625000000000001,1],[0,0.9375000000000006],[0.9374999999999999,0],[0.9375000000000004,0],[0,0.9375],[0,0.8125000000000001],[0.8124999999999998,0],[0.8124999999999999,0],[0,0.8124999999999998],[0.59375,0],[0.5937500000000002,1],[0,0.59375],[1,0.59375],[0.21338834764831838,1],[0.6276019100214136,0],[1,0.21338834764831818],[0,0.6276019100214137],[2.7755575615628914e-17,0.06250000000000008],[0.9375000000000001,1],[0.06250000000000014,0],[1,0.9374999999999998],[0,0.625],[0.6250000000000001,0],[0.6249999999999999,0],[0,0.6250000000000002],[1,0.8535533905932736],[0,0.8535533905932737],[0.853553390593274,1],[0.8535533905932738,0],[0,0.9204951288348661],[1,0.5062815664617706],[0.9204951288348661,0],[0.5062815664617708,1],[0,0.31249999999999994],[0.6875000000000002,1],[0.31250000000000006,0],[1,0.6874999999999998],[0.42677669529663687,1],[0.012563132923541831,0],[1,0.42677669529663653],[0,0.012563132923541831],[1,0.9142135623730949],[0,0.5],[0.9142135623730951,0.9999999999999999],[0.5,0],[0.12055826175840775,0],[0.5347718241315029,1],[0,0.12055826175840775],[1,0.5347718241315026],[0.9785533905932733,0],[0,0.4053300858899106],[-2.7755575615628914e-17,0.978553390593274],[0.4053300858899106,-5.551115123125783e-17],[0.6464466094067262,0],[0,0.6464466094067263],[0,0.6464466094067263],[0.6464466094067262,-1.3877787807814457e-17],[0.3478106973409748,1],[0.76202425971407,0],[1,0.3478106973409746],[0,0.7620242597140701],[0,0.5883883476483185],[0.9999999999999999,0.17417478527522345],[0.5883883476483185,0],[0.17417478527522368,0.9999999999999999]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61],[62,63],[64,65],[66,67],[68,69],[70,71],[72,73],[74,75],[76,77],[78,79],[80,81],[82,83],[84,85],[86,87],[88,89],[90,91],[92,93],[94,95],[96,97],[98,99],[100,101],[102,103],[104,105],[106,107],[108,109],[110,111],[112,113],[114,115],[116,117],[118,119],[120,121],[122,123],[124,125],[126,127],[128,129],[130,131],[132,133],[134,135],[136,137],[138,139],[140,141],[142,143],[144,145],[146,147]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"U\",\"U\",\"M\",\"M\",\"V\",\"V\",\"U\",\"U\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"U\",\"U\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"F\",\"F\",\"U\",\"U\",\"V\",\"V\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"U\",\"U\",\"U\",\"U\",\"U\",\"U\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"U\",\"U\",\"U\",\"U\",\"U\",\"U\",\"F\",\"F\",\"U\",\"U\",\"F\",\"F\"]}"
  },
  {
    "path": "tests/files/fold/non-planar-bird-base.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[1,0],[0,1],[0.5,1],[0.5,0],[0,0.5],[1,0.5],[0,0],[1,0.4142135623730951],[0,0],[0.41421356237309503,1],[1,1],[0,0.5857864376269049],[1,1],[0.5857864376269049,0],[1,0],[0,0.4142135623730951],[0,1],[0.41421356237309503,0],[1,0],[0.585786437626905,1],[0,1],[1,0.5857864376269049],[0,0],[0.5,0.20710678118654754],[0,0],[0.20710678118654757,0.5],[0,1],[0.20710678118654763,0.5],[0,1],[0.5,0.7928932188134523],[1,1],[0.5,0.7928932188134525],[1,1],[0.7928932188134523,0.5],[1,0],[0.7928932188134525,0.5],[1,0],[0.5,0.20710678118654754],[-2.7755575615628914e-17,0.7071067811865475],[0.7071067811865476,0],[0.29289321881345143,1],[1,0.292893218813453],[0.29289321881345304,0],[1,0.7071067811865486],[0,0.29289321881345265],[0.7071067811865475,1],[0.20710678118654763,0.5],[0.5,0.20710678118654754],[0.5,0.7928932188134525],[0.7928932188134523,0.5]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"U\",\"U\",\"U\",\"U\",\"U\",\"U\",\"U\",\"U\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\"],\"faces_vertices\":[[0,1,2,3]],\"faces_edges\":[[0,1,2,3]],\"edges_foldAngle\":[0,0,0,0,180,180,-180,-180,0,0,0,0,0,0,0,0,-180,-180,-180,-180,-180,-180,-180,-180,0,0,0,0,180,180]}\n"
  },
  {
    "path": "tests/files/fold/non-planar-nonconvex.fold",
    "content": "{\"vertices_coords\":[[0,4],[-3,3],[-3,3],[-1,2],[-1,2],[-2,-2],[0,4],[1,0],[1,0],[3,2],[3,2],[4,-1],[4,-1],[-2,-2],[-3,3],[0.30878706414438284,2.764851743422468],[0,4],[-1.0768096208105948,2.0384048104052974],[-1,2],[0.34531110975864365,2.618755560965426],[-2,-2],[0.8436818559566444,0.625272576173423],[1,0],[0.5644157435051745,-1.572597376082471],[3,2],[2.2218347553393896,-1.296360874110102],[4,-1],[1.5032479862952566,0.5032479862952572],[0.7627240835523401,0.9491036657906402],[-2.770615218501403,3.0764615938328665],[-2,-2],[0.4618561912232768,2.152575235106892],[-2,-2],[3.444705864925636,0.6658824052230927],[4,-1],[-1.488394181826104,0.046423272695580886],[4,-1],[2.2210920596263923,1.2210920596263923],[3,2],[3.141419552995303,-1.1430967411674495],[3,2],[1.0536031465002451,-1.4910661422499594],[1,0],[0.031436314982777436,-1.6614272808362038],[1,0],[0.9810358047317906,-1.5031606992113684],[0,4],[-0.7484816087263996,-1.7914136014543995],[0,4],[-1.8737650607496554,2.4368825303748274],[-3,3],[0.1507036048449404,3.3971855806202376],[-3,3],[0.48885711521620534,2.0445715391351786],[-1,2],[0.5061309808772132,1.9754760764911468],[-1,2],[0.15254810313743827,3.389807587450247],[2.4951463825708453,1.4951463825708444],[3.2359036844628086,1.2922889466115741],[2.9359125994015236,1.9359125994015232],[3.02182878353107,1.93451364940679]],\"edges_vertices\":[[0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[40,41],[42,43],[44,45],[46,47],[48,49],[50,51],[52,53],[54,55],[56,57],[58,59],[60,61]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,-180,-180,-180,-180,-180,-180,-180,-180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180,180]}\n"
  },
  {
    "path": "tests/files/fold/non-planar-polygons.fold",
    "content": "{\"frame_description\":\"pentagon\",\"vertices_coords\":[[1,0],[0.30901699437494745,0.9510565162951535],[-0.8090169943749473,0.5877852522924732],[-0.8090169943749475,-0.587785252292473],[0.30901699437494723,-0.9510565162951536],[0.30901699437494745,0.9510565162951535],[0,0],[0,0],[-0.8090169943749475,-0.587785252292473],[0.30901699437494723,-0.9510565162951536],[0,0],[0,0],[-0.8090169943749473,0.5877852522924732],[1,0],[0,0]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,0],[5,6],[7,8],[9,10],[11,12],[13,14]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"M\",\"M\",\"U\"],\"faces_vertices\":[[0,1,2,3,4]],\"faces_edges\":[[0,1,2,3,4]],\"file_frames\":[{\"frame_description\":\"hexagon\",\"vertices_coords\":[[1,0],[0.5,0.8660254037844386],[-0.5,0.8660254037844387],[-1,0],[-0.5,-0.8660254037844384],[0.5,-0.8660254037844386],[0.5,0.8660254037844386],[0,0],[-1,1.2246467991473532e-16],[0,0],[0.5,-0.8660254037844386],[0,0],[-0.5,0.8660254037844387],[0,0],[-0.5,-0.8660254037844384],[0,0],[0,0],[1,0],[-1,0],[1,0],[0.5,0.8660254037844386],[-0.5,-0.8660254037844384],[0.5,-0.8660254037844386],[-0.5,0.8660254037844387]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,0],[18,19],[20,21],[22,23]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"U\"],\"faces_vertices\":[[0,1,2,3,4,5]],\"faces_edges\":[[0,1,2,3,4,5]]}]}\n"
  },
  {
    "path": "tests/files/fold/non-planar-square-fish.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[1,1],[0,1],[0,0],[1,1],[0,0],[0.41421356237309515,1],[0,0],[1,0.4142135623730951],[0.41421356237309515,1],[1,0.4142135623730951],[0.7071067811865476,1],[0.7071067811865476,0],[1,0.7071067811865476],[0,0.7071067811865476],[0,1],[1,0],[1,1],[0.5857864376269042,0],[1,1],[0,0.5857864376269051],[0.41421356237309515,1],[0.8284271247461905,0],[1,0.4142135623730951],[0,0.8284271247461903],[0.585786437626905,1],[0,0.41421356237309515],[1,0.5857864376269055],[0.41421356237309465,0],[0.41421356237309515,1],[0.5857864376269052,0.5857864376269051],[1,0.4142135623730951],[0.5857864376269052,0.5857864376269051],[1,1],[0.5857864376269052,0.5857864376269051],[1,0],[0.7071067811865471,0.2928932188134529],[0,1],[0.2928932188134519,0.7071067811865479],[0.2928932188134519,0.7071067811865479],[0.7071067811865471,0.2928932188134529],[0.2928932188134519,0.707106781186546],[0.5,0.5],[0.7071067811865471,0.29289321881345426],[0.5,0.5],[0,0],[0.4,0.4],[0.4,0.4],[0.2,0.2]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,0],[4,5],[6,7],[8,9],[10,11],[12,13],[14,15],[16,17],[18,19],[20,21],[22,23],[24,25],[26,27],[28,29],[30,31],[32,33],[34,35],[36,37],[38,39],[42,43],[44,45],[46,47],[48,49]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"F\",\"V\",\"V\",\"M\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"U\",\"U\",\"U\",\"U\",\"U\",\"C\",\"C\",\"U\",\"C\"],\"faces_vertices\":[[0,1,2,3]],\"faces_edges\":[[0,1,2,3]],\"edges_foldAngle\":[0,0,0,0,0,180,180,-180,180,180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}\n"
  },
  {
    "path": "tests/files/fold/overlapping-assignments.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"non-planar overlapping assignments\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[0, -1], [2, -1], [2, 1], [0, 1],\n\t\t[0, -2], [4, -2], [4, 2], [0, 2],\n\t\t[0, -3], [6, -3], [6, 3], [0, 3],\n\t\t[0, -4], [8, -4], [8, 4], [0, 4],\n\t\t[0, 5], [0, 3.5],\n\t\t[0, -5], [0, -1.5]\n\t],\n\t\"edges_vertices\": [\n\t\t[0, 1], [1, 2], [2, 3], [3, 0],\n\t\t[4, 5], [5, 6], [6, 7], [7, 4],\n\t\t[8, 9], [9, 10], [10, 11], [11, 8],\n\t\t[12, 13], [13, 14], [14, 15], [15, 12],\n\t\t[16, 17],\n\t\t[18, 19]\n\t],\n\t\"edges_assignment\": [\n\t\t\"V\", \"V\", \"V\", \"V\",\n\t\t\"C\", \"C\", \"C\", \"C\",\n\t\t\"M\", \"M\", \"M\", \"M\",\n\t\t\"U\", \"U\", \"U\", \"U\",\n\t\t\"B\", \"B\"\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-3x3-invalid.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[0,3],[0,2],[0,1],[3,1],[2,1],[1,1],[1,3],[1,2],[2,2],[3,2],[2,3],[3,3]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[4,5],[5,6],[6,0],[7,8],[8,9],[9,6],[10,11],[11,9],[9,1],[5,11],[11,12],[12,13],[14,12],[12,8],[8,2],[3,7],[7,13],[13,15],[15,14],[14,10],[10,4]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"V\",\"V\",\"M\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,9,6],[1,2,8,9],[2,3,7,8],[4,5,11,10],[5,6,9,11],[7,13,12,8],[8,12,11,9],[10,11,12,14],[12,13,15,14]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[2, 2], [1, 2], [2, 2], [1, 2], [2, 1], [2, 2], [2, 1], [1, 1],\n\t\t\t[2, 1], [1, 1], [1, 1], [1, 2], [2, 2], [1, 2], [2, 1], [1, 1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-3x3.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[0,3],[0,2],[0,1],[3,1],[2,1],[1,1],[1,3],[1,2],[2,2],[3,2],[2,3],[3,3]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[4,5],[5,6],[6,0],[7,8],[8,9],[9,6],[10,11],[11,9],[9,1],[5,11],[11,12],[12,13],[14,12],[12,8],[8,2],[3,7],[7,13],[13,15],[15,14],[14,10],[10,4]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,9,6],[1,2,8,9],[2,3,7,8],[4,5,11,10],[5,6,9,11],[7,13,12,8],[8,12,11,9],[10,11,12,14],[12,13,15,14]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[2, 2], [1, 2], [2, 2], [1, 2], [2, 1], [2, 2], [2, 1], [1, 1],\n\t\t\t[2, 1], [1, 1], [1, 1], [1, 2], [2, 2], [1, 2], [2, 1], [1, 1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-4x2.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[2,0],[3,0],[5,0],[0,4],[0,2],[1,2],[1,4],[2,2],[3,2],[5,2],[2,4],[3,4],[5,4]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[5,6],[6,0],[1,7],[7,8],[6,7],[7,9],[9,10],[10,11],[2,9],[9,12],[3,10],[10,13],[4,11],[11,14],[14,13],[13,12],[12,8],[8,5]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,7,6],[1,2,9,7],[2,3,10,9],[3,4,11,10],[5,6,7,8],[7,9,12,8],[9,10,13,12],[10,11,14,13]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0, 0],\n\t\t\t[1, 0],\n\t\t\t[0, 0],\n\t\t\t[1, 0],\n\t\t\t[-1, 0],\n\t\t\t[0, 4],\n\t\t\t[0, 2],\n\t\t\t[1, 2],\n\t\t\t[1, 4],\n\t\t\t[0, 2],\n\t\t\t[1, 2],\n\t\t\t[-1, 2],\n\t\t\t[0, 4],\n\t\t\t[1, 4],\n\t\t\t[-1, 4]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-5.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[5,0],[4,0],[3,0],[2,0],[1,0],[0,0],[0,1],[1,1],[2,1],[3,1],[4,1],[5,1]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[6,5],[6,7],[7,8],[8,9],[9,10],[10,11],[7,4],[8,3],[9,2],[10,1],[11,0]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\",\"V\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,11,10,1],[1,10,9,2],[2,9,8,3],[3,8,7,4],[4,7,6,5]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[5,0],[4,0],[5,0],[4,0],[5,0],[4,0],[4,1],[5,1],[4,1],[5,1],[4,1],[5,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-6x2-90deg.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[2,0],[0,6],[0,5],[0,4],[0,3],[0,2],[0,1],[1,1],[2,1],[1,2],[1,3],[1,4],[1,5],[1,6],[2,6],[2,5],[2,4],[2,3],[2,2]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[3,4],[4,5],[5,6],[6,7],[7,8],[8,0],[8,9],[9,10],[1,9],[9,11],[11,12],[12,13],[13,14],[14,15],[16,17],[17,18],[18,19],[19,20],[20,10],[10,2],[20,11],[11,7],[6,12],[12,19],[18,13],[13,5],[4,14],[14,17],[16,15],[15,3]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,180,180,90,-90,90,-90,90,-90,0,0,0,0,0,0,-180,-180,180,180,180,180,-180,-180,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,9,8],[7,8,9,11],[6,7,11,12],[5,6,12,13],[4,5,13,14],[3,4,14,15],[1,2,10,9],[9,10,20,11],[11,20,19,12],[12,19,18,13],[13,18,17,14],[14,17,16,15]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[0,0,0],[1,0,0],[1,0,1],[0,0,0],[0,1,0],[0,0,0],[0,1,0],[0,0,0],[0,1,0],[1,1,0],[1,1,1],[1,0,0],[1,1,0],[1,0,0],[1,1,0],[1,0,0],[1,0,1],[1,1,1],[1,0,1],[1,1,1],[1,0,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/panels-simple.fold",
    "content": "{\"vertices_coords\":[[0,0],[1,0],[2,0],[0,1],[1,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[3,0],[1,4],[5,4],[4,3],[2,5]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[0,0,0,180,0,0,0],\"vertices_edges\":[[0,2],[1,3,0],[6,1],[5,2],[4,5,3],[4,6]],\"vertices_vertices\":[[1,3],[2,4,0],[5,1],[4,0],[5,3,1],[4,2]],\"faces_vertices\":[[0,1,4,3],[1,2,5,4]],\"faces_edges\":[[0,3,5,2],[1,6,4,3]],\"vertices_faces\":[[0,null],[1,0,null],[1,null],[null,0],[null,0,1],[1,null]],\"edges_faces\":[[0],[1],[0],[0,1],[1],[0],[1]],\"faces_faces\":[[null,1,null,null],[null,null,null,0]],\"file_frames\":[{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[0,1],[1,1],[3,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[4,0],[1,5],[6,7],[7,5],[5,4],[2,7],[6,3]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,180,0,0,0,-180,0],\"vertices_edges\":[[0,3],[1,4,0],[2,8,1],[9,2],[7,3],[6,7,4],[5,9],[5,6,8]],\"vertices_vertices\":[[1,4],[2,5,0],[3,7,1],[6,2],[5,0],[7,4,1],[7,3],[6,5,2]],\"faces_vertices\":[[0,1,5,4],[1,2,7,5],[2,3,6,7]],\"faces_edges\":[[0,4,7,3],[1,8,6,4],[2,9,5,8]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[2,null],[null,0],[null,0,1],[2,null],[null,1,2]],\"edges_faces\":[[0],[1],[2],[0],[0,1],[2],[1],[0],[1,2],[2]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,null,null,1]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[0,1],[1,1],[4,1],[3,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[5,0],[1,6],[7,8],[8,9],[9,6],[6,5],[2,9],[8,3],[7,4]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,180,0,0,0,0,-180,180,0],\"vertices_edges\":[[0,4],[1,5,0],[2,10,1],[3,11,2],[12,3],[9,4],[8,9,5],[6,12],[6,7,11],[7,8,10]],\"vertices_vertices\":[[1,5],[2,6,0],[3,9,1],[4,8,2],[7,3],[6,0],[9,5,1],[8,4],[7,9,3],[8,6,2]],\"faces_vertices\":[[0,1,6,5],[1,2,9,6],[2,3,8,9],[3,4,7,8]],\"faces_edges\":[[0,5,9,4],[1,10,8,5],[2,11,7,10],[3,12,6,11]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[3,null],[null,0],[null,0,1],[3,null],[null,2,3],[null,1,2]],\"edges_faces\":[[0],[1],[2],[3],[0],[0,1],[3],[2],[1],[0],[1,2],[2,3],[3]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,null,null,2]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,1],[1,1],[5,1],[4,1],[3,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[6,0],[1,7],[8,9],[9,10],[10,11],[11,7],[7,6],[2,11],[10,3],[9,4],[8,5]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"M\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,180,0,0,0,0,0,-180,180,-180,0],\"vertices_edges\":[[0,5],[1,6,0],[2,12,1],[3,13,2],[4,14,3],[15,4],[11,5],[10,11,6],[7,15],[7,8,14],[8,9,13],[9,10,12]],\"vertices_vertices\":[[1,6],[2,7,0],[3,11,1],[4,10,2],[5,9,3],[8,4],[7,0],[11,6,1],[9,5],[8,10,4],[9,11,3],[10,7,2]],\"faces_vertices\":[[0,1,7,6],[1,2,11,7],[2,3,10,11],[3,4,9,10],[4,5,8,9]],\"faces_edges\":[[0,6,11,5],[1,12,10,6],[2,13,9,12],[3,14,8,13],[4,15,7,14]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[4,null],[null,0],[null,0,1],[4,null],[null,3,4],[null,2,3],[null,1,2]],\"edges_faces\":[[0],[1],[2],[3],[4],[0],[0,1],[4],[3],[2],[1],[0],[1,2],[2,3],[3,4],[4]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,null,null,3]]},{\"vertices_coords\":[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[0,1],[1,1],[6,1],[5,1],[4,1],[3,1],[2,1]],\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[1,8],[9,10],[10,11],[11,12],[12,13],[13,8],[8,7],[2,13],[12,3],[11,4],[10,5],[6,9]],\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"B\"],\"edges_foldAngle\":[0,0,0,0,0,0,0,180,0,0,0,0,0,0,-180,180,-180,180,0],\"vertices_edges\":[[0,6],[1,7,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[13,6],[12,13,7],[8,18],[8,9,17],[9,10,16],[10,11,15],[11,12,14]],\"vertices_vertices\":[[1,7],[2,8,0],[3,13,1],[4,12,2],[5,11,3],[6,10,4],[9,5],[8,0],[13,7,1],[10,6],[9,11,5],[10,12,4],[11,13,3],[12,8,2]],\"faces_vertices\":[[0,1,8,7],[1,2,13,8],[2,3,12,13],[3,4,11,12],[4,5,10,11],[5,6,9,10]],\"faces_edges\":[[0,7,13,6],[1,14,12,7],[2,15,11,14],[3,16,10,15],[4,17,9,16],[5,18,8,17]],\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[null,0,1],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2]],\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[0,1],[5],[4],[3],[2],[1],[0],[1,2],[2,3],[3,4],[4,5],[5]],\"faces_faces\":[[null,1,null,null],[null,2,null,0],[null,3,null,1],[null,4,null,2],[null,5,null,3],[null,null,null,4]]}]}"
  },
  {
    "path": "tests/files/fold/panels-zig-zag.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[0,1],[7,0],[5,0],[4,0],[3,0],[2,0],[2,1],[3,1],[4,1],[5,1],[7,1]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[2,3],[3,4],[4,5],[5,6],[6,0],[1,7],[7,8],[8,9],[9,10],[10,11],[7,6],[8,5],[9,4],[10,3],[11,2]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,6,7,1],[2,11,10,3],[3,10,9,4],[4,9,8,5],[5,8,7,6]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0, 0],\n\t\t\t[0, 1],\n\t\t\t[3, 0],\n\t\t\t[1, 0],\n\t\t\t[2, 0],\n\t\t\t[1, 0],\n\t\t\t[2, 0],\n\t\t\t[2, 1],\n\t\t\t[1, 1],\n\t\t\t[2, 1],\n\t\t\t[1, 1],\n\t\t\t[3, 1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/pleats-angle-3d.fold",
    "content": "{\n  \"vertices_coords\": [\n    [0.906138613861386, 0.65, 0.6886138613861386],\n    [1.1635643564356442, 0.65, 0.5856435643564357],\n    [0.9061386138613865, 1.3386138613861387, 0],\n    [1.1635643564356437, 1.2356435643564354, 0],\n    [0.65, 1.3, 0],\n    [0.65, 0.65, 0.65],\n    [0.5238613861386138, 0.65, 0.6113861386138613],\n    [0.7812871287128714, 1.3128712871287127, 0],\n    [0.7812871287128715, 0.65, 0.662871287128713],\n    [0.5238613861386141, 1.2613861386138616, 0],\n    [0.715, 1.3, 0],\n    [0.845, 0.65, 0.65],\n    [0.845, 1.3, 0],\n    [0.715, 0.65, 0.65],\n    [0.9087128712871289, 1.287128712871287, 0],\n    [0.6512871287128712, 0.6499999999999999, 0.637128712871287],\n    [0.6512871287128716, 1.2871287128712872, 0],\n    [0.9087128712871293, 0.65, 0.6371287128712873],\n    [0.7787128712871286, 0.65, 0.6628712871287128],\n    [1.0361386138613864, 1.2613861386138612, 0],\n    [1.0361386138613868, 0.65, 0.6113861386138615],\n    [0.7787128712871292, 1.312871287128713, 0],\n    [0.78, 0.65, 0],\n    [0.78, 0.65, 0],\n    [0.65, 0.65, 0],\n    [0.6473737373737376, 0.65, 0],\n    [0.6473737373737374, 0.65, 0],\n    [1.0452525252525255, 0.65, 0],\n    [0.78, 0.65, 0],\n    [0.9126262626262631, 0.65, 0],\n    [1.045252525252525, 0.65, 0],\n    [0.78, 0.65, 0],\n    [0.9126262626262627, 0.65, 0]\n  ],\n  \"edges_vertices\": [\n    [9, 10],\n    [10, 4],\n    [5, 11],\n    [11, 8],\n    [4, 12],\n    [12, 7],\n    [6, 13],\n    [13, 5],\n    [7, 14],\n    [15, 6],\n    [16, 9],\n    [8, 17],\n    [0, 18],\n    [18, 15],\n    [14, 19],\n    [19, 3],\n    [17, 20],\n    [20, 1],\n    [2, 21],\n    [21, 16],\n    [12, 22],\n    [22, 13],\n    [10, 23],\n    [23, 11],\n    [4, 24],\n    [24, 5],\n    [23, 24],\n    [24, 22],\n    [9, 25],\n    [25, 8],\n    [23, 25],\n    [7, 26],\n    [26, 6],\n    [22, 26],\n    [2, 27],\n    [27, 1],\n    [16, 28],\n    [28, 17],\n    [21, 29],\n    [29, 20],\n    [25, 28],\n    [28, 29],\n    [29, 27],\n    [3, 30],\n    [30, 0],\n    [14, 31],\n    [31, 15],\n    [19, 32],\n    [32, 18],\n    [26, 31],\n    [31, 32],\n    [32, 30]\n  ],\n  \"edges_assignment\": [\n    \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"V\", \"V\", \"M\", \"M\", \"V\", \"M\", \"M\", \"M\", \"M\", \"M\", \"M\", \"V\", \"B\", \"B\", \"F\", \"F\", \"F\", \"F\", \"V\", \"V\", \"V\", \"B\", \"B\", \"F\", \"F\", \"F\", \"F\", \"M\", \"M\", \"M\"\n  ],\n  \"edges_foldAngle\": [\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 180, 180, 180, -180, -180, 90, -90, -180, -180, -90, -180, -180, 90, 0, 0, 0, 0, 0, 0, 90, 90, 90, 0, 0, 0, 0, 0, 0, -90, -90, -90\n  ],\n  \"faces_vertices\": [\n    [23, 10, 4, 24],\n    [23, 24, 5, 11],\n    [22, 24, 4, 12],\n    [24, 22, 13, 5],\n    [9, 25, 28, 16],\n    [25, 23, 11, 8],\n    [25, 9, 10, 23],\n    [25, 8, 17, 28],\n    [16, 28, 29, 21],\n    [28, 17, 20, 29],\n    [21, 29, 27, 2],\n    [29, 20, 1, 27],\n    [26, 22, 12, 7],\n    [22, 26, 6, 13],\n    [26, 7, 14, 31],\n    [26, 31, 15, 6],\n    [31, 14, 19, 32],\n    [31, 32, 18, 15],\n    [32, 19, 3, 30],\n    [32, 30, 0, 18]\n  ],\n  \"file_spec\": 1.1,\n  \"file_creator\": \"Rabbit Ear\",\n  \"vertices_edges\": [\n    [12, 44],\n    [35, 17],\n    [18, 34],\n    [15, 43],\n    [1, 4, 24],\n    [2, 25, 7],\n    [6, 32, 9],\n    [5, 8, 31],\n    [11, 29, 3],\n    [10, 0, 28],\n    [0, 1, 22],\n    [3, 23, 2],\n    [4, 5, 20],\n    [7, 21, 6],\n    [8, 14, 45],\n    [9, 46, 13],\n    [19, 10, 36],\n    [16, 37, 11],\n    [13, 48, 12],\n    [14, 15, 47],\n    [17, 39, 16],\n    [18, 19, 38],\n    [27, 20, 33, 21],\n    [30, 22, 26, 23],\n    [26, 24, 27, 25],\n    [28, 30, 29, 40],\n    [33, 31, 49, 32],\n    [34, 42, 35],\n    [36, 40, 37, 41],\n    [38, 41, 39, 42],\n    [43, 44, 51],\n    [45, 50, 46, 49],\n    [47, 51, 48, 50]\n  ],\n  \"vertices_vertices\": [\n    [18, 30],\n    [27, 20],\n    [21, 27],\n    [19, 30],\n    [10, 12, 24],\n    [11, 24, 13],\n    [13, 26, 15],\n    [12, 14, 26],\n    [17, 25, 11],\n    [16, 10, 25],\n    [9, 4, 23],\n    [8, 23, 5],\n    [4, 7, 22],\n    [5, 22, 6],\n    [7, 19, 31],\n    [6, 31, 18],\n    [21, 9, 28],\n    [20, 28, 8],\n    [15, 32, 0],\n    [14, 3, 32],\n    [1, 29, 17],\n    [2, 16, 29],\n    [24, 12, 26, 13],\n    [25, 10, 24, 11],\n    [23, 4, 22, 5],\n    [9, 23, 8, 28],\n    [22, 7, 31, 6],\n    [2, 29, 1],\n    [16, 25, 17, 29],\n    [21, 28, 20, 27],\n    [3, 0, 32],\n    [14, 32, 15, 26],\n    [19, 30, 18, 31]\n  ],\n  \"faces_edges\": [\n    [22, 1, 24, 26],\n    [26, 25, 2, 23],\n    [27, 24, 4, 20],\n    [27, 21, 7, 25],\n    [28, 40, 36, 10],\n    [30, 23, 3, 29],\n    [28, 0, 22, 30],\n    [29, 11, 37, 40],\n    [36, 41, 38, 19],\n    [37, 16, 39, 41],\n    [38, 42, 34, 18],\n    [39, 17, 35, 42],\n    [33, 20, 5, 31],\n    [33, 32, 6, 21],\n    [31, 8, 45, 49],\n    [49, 46, 9, 32],\n    [45, 14, 47, 50],\n    [50, 48, 13, 46],\n    [47, 15, 43, 51],\n    [51, 44, 12, 48]\n  ],\n  \"vertices_faces\": [\n    [19, null],\n    [11, null],\n    [10, null],\n    [null, 18],\n    [null, 2, 0],\n    [1, 3, null],\n    [13, 15, null],\n    [null, 14, 12],\n    [7, 5, null],\n    [null, 6, 4],\n    [null, 0, 6],\n    [5, 1, null],\n    [null, 12, 2],\n    [3, 13, null],\n    [null, 16, 14],\n    [15, 17, null],\n    [null, 4, 8],\n    [9, 7, null],\n    [17, 19, null],\n    [null, 18, 16],\n    [11, 9, null],\n    [null, 8, 10],\n    [2, 12, 13, 3],\n    [6, 0, 1, 5],\n    [0, 2, 3, 1],\n    [6, 5, 7, 4],\n    [12, 14, 15, 13],\n    [10, 11, null],\n    [4, 7, 9, 8],\n    [8, 9, 11, 10],\n    [null, 19, 18],\n    [16, 17, 15, 14],\n    [18, 19, 17, 16]\n  ],\n  \"edges_faces\": [\n    [6],\n    [0],\n    [1],\n    [5],\n    [2],\n    [12],\n    [13],\n    [3],\n    [14],\n    [15],\n    [4],\n    [7],\n    [19],\n    [17],\n    [16],\n    [18],\n    [9],\n    [11],\n    [10],\n    [8],\n    [2, 12],\n    [3, 13],\n    [0, 6],\n    [1, 5],\n    [0, 2],\n    [1, 3],\n    [0, 1],\n    [2, 3],\n    [4, 6],\n    [5, 7],\n    [6, 5],\n    [12, 14],\n    [13, 15],\n    [12, 13],\n    [10],\n    [11],\n    [4, 8],\n    [7, 9],\n    [8, 10],\n    [9, 11],\n    [4, 7],\n    [8, 9],\n    [10, 11],\n    [18],\n    [19],\n    [14, 16],\n    [15, 17],\n    [16, 18],\n    [17, 19],\n    [14, 15],\n    [16, 17],\n    [18, 19]\n  ],\n  \"faces_faces\": [\n    [6, 2, 1],\n    [0, 3, 5],\n    [0, 3, 12],\n    [1, 2, 13],\n    [6, 7, 8],\n    [1, 6, 7],\n    [0, 4, 5],\n    [4, 5, 9],\n    [4, 9, 10],\n    [7, 8, 11],\n    [8, 11],\n    [9, 10],\n    [2, 13, 14],\n    [3, 12, 15],\n    [12, 16, 15],\n    [13, 14, 17],\n    [14, 18, 17],\n    [15, 16, 19],\n    [16, 19],\n    [17, 18]\n  ],\n  \"frame_classes\": [\n    \"foldedForm\"\n  ],\n  \"faceOrders\": [\n    [2, 12, 1],\n    [0, 6, 1],\n    [0, 2, -1],\n    [4, 6, -1],\n    [12, 14, -1],\n    [3, 13, 1],\n    [1, 5, 1],\n    [1, 3, -1],\n    [5, 7, -1],\n    [13, 15, -1],\n    [2, 14, -1],\n    [0, 12, 1],\n    [2, 6, 1],\n    [6, 12, 1],\n    [0, 14, -1],\n    [6, 14, -1],\n    [1, 13, 1],\n    [1, 7, -1],\n    [3, 5, 1],\n    [3, 7, -1],\n    [6, 8, -1],\n    [13, 17, -1],\n    [0, 4, -1],\n    [2, 4, -1],\n    [4, 12, 1],\n    [4, 14, -1],\n    [3, 15, -1],\n    [5, 13, 1],\n    [1, 15, -1],\n    [5, 15, -1],\n    [7, 13, 1],\n    [0, 8, -1],\n    [3, 17, -1],\n    [7, 15, -1],\n    [2, 8, -1],\n    [8, 12, 1],\n    [8, 14, -1],\n    [1, 17, -1],\n    [5, 17, -1],\n    [7, 17, -1],\n    [2, 10, -1],\n    [10, 12, 1],\n    [10, 14, -1],\n    [8, 16, -1],\n    [1, 19, -1],\n    [5, 19, -1],\n    [9, 17, -1],\n    [7, 19, -1],\n    [10, 16, -1],\n    [9, 19, -1],\n    [10, 18, -1],\n    [11, 19, -1]\n  ]\n}"
  },
  {
    "path": "tests/files/fold/preliminary-offset-cp.fold",
    "content": "{\n\t\"vertices_coords\": [\n\t\t[0,0],[0.7071067811865469,0],[1,0],[0,1],[0,0.7071067811865472],\n\t\t[0.707106781186547,1],[0.707106781186547,0.7071067811865471],\n\t\t[1,0.7071067811865471],[1,0.41421356237309415],[0.41421356237309415,1],[1,1]\n\t],\n\t\"edges_vertices\": [\n\t\t[0,1],[1,2],[3,4],[4,0],[5,6],[6,1],[4,6],[6,7],\n\t\t[8,6],[6,9],[2,8],[8,7],[7,10],[10,5],[5,9],[9,3]\n\t],\n\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\": [0,0,0,0,180,180,180,180,-180,-180,0,0,0,0,0,0],\n\t\"faces_vertices\": [[0,1,6,4],[1,2,8,6],[3,4,6,9],[6,8,7],[5,9,6],[5,6,7,10]]\n}"
  },
  {
    "path": "tests/files/fold/randlett-flapping-bird.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[0.2928932188134523,0],[0.5,0],[1,0],[0,1],[0,0.5],[0,0.29289321881345254],[0.5,0.5],[0.7442991054080123,0.7442991054080123],[0.9075345489044407,0.9075345489044407],[1,1],[0.2928932188134524,0.7071067811865476],[0.2071067811865476,0.5],[0.7071067811865474,0.29289321881345276],[0.5,0.20710678118654757],[0.7928932188134524,0.5],[1,0.6383843269570775],[0.5,0.7928932188134525],[0.6383843269570775,1],[0.7767686539141546,0.9075345489044407],[0.8692341050097139,1],[0.9075345489044407,0.776768653914155],[1,0.8692341050097143],[0.5,1],[1,0.5]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[4,5],[5,6],[6,0],[0,7],[7,8],[8,9],[9,10],[11,12],[13,14],[15,16],[17,18],[19,20],[21,22],[14,1],[12,6],[3,14],[4,12],[23,17],[17,7],[7,14],[14,2],[24,15],[15,7],[7,12],[12,5],[10,21],[21,15],[15,13],[10,19],[19,17],[17,11],[3,13],[13,7],[7,11],[11,4],[15,8],[17,8],[21,9],[9,19],[3,24],[24,16],[16,22],[22,10],[10,20],[20,18],[18,23],[23,4]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"V\",\"F\",\"F\",\"F\",\"V\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,14,7],[0,7,12,6],[1,2,14],[2,3,14],[3,24,15,13],[3,13,14],[4,5,12],[4,12,11],[4,11,17,23],[5,6,12],[7,15,8],[7,8,17],[7,17,11],[7,11,12],[7,14,13],[7,13,15],[8,9,19,17],[8,15,21,9],[9,10,19],[9,21,10],[10,20,19],[10,21,22],[15,24,16],[15,16,22,21],[17,19,20,18],[17,18,23]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0,0],[0.2928932188134523,0],[0.2928932188134524,0.20710678118654768],[0.2928932188134527,0.7071067811865477],[0.2928932188134521,0.7071067811865475],[0.2928932188134523,0.2071067811865474],[0.2928932188134525,0],[0.5,0.5],[0.7442991054080121,0.25570089459198764],[0.8326414908339146,0.46897827962],[0.95345342476596,0.41893633808841557],[0.29289321881345237,0.2928932188134525],[0.5,0.20710678118654754],[0.29289321881345265,0.29289321881345276],[0.5,0.20710678118654757],[0.5,0.20710678118654757],[0.744299105408012,0.2557008945919879],[0.5,0.20710678118654743],[0.744299105408012,0.25570089459198775],[0.7118295569018689,0.41893633808841596],[0.8326414908339144,0.46897827962],[0.7118295569018692,0.4189363380884161],[0.832641490833914,0.46897827962],[0.6464466094067263,0.3535533905932736],[0.6464466094067263,0.3535533905932738]\n\t\t],\n\t\t\"faceOrders\":[\n\t  \t[0,1,-1],[10,11,-1],[16,17,1],[18,19,-1],[7,13,-1],[5,14,-1],[22,23,-1],[24,25,-1],[20,24,1],[21,23,1],[0,2,1],[1,9,1],[3,5,1],[6,7,1],[0,14,1],[1,13,1],[19,21,1],[17,23,1],[4,15,1],[18,20,1],[16,24,1],[8,12,1],[4,5,-1],[14,15,-1],[12,13,-1],[7,8,-1],[10,17,1],[11,16,1],[17,19,-1],[16,18,-1],[1,14,1],[1,12,-1],[1,2,1],[0,13,1],[11,17,1],[16,19,-1],[18,21,1],[8,13,-1],[4,14,-1],[17,22,-1],[16,25,-1],[16,20,1],[17,21,1],[10,16,1],[17,18,-1],[17,20,1],[16,21,1],[0,7,-1],[1,7,-1],[6,13,-1],[7,12,1],[0,5,-1],[1,5,-1],[3,14,-1],[5,15,1],[18,24,1],[19,23,1],[0,3,1],[1,3,1],[3,4,-1],[3,15,1],[0,6,1],[1,6,1],[6,8,-1],[6,12,1],[0,15,-1],[0,4,1],[1,8,1],[10,23,-1],[11,23,-1],[10,24,-1],[11,24,-1],[12,15,-1],[4,10,1],[8,11,1],[4,11,-1],[8,10,-1],[6,14,1],[7,14,1],[3,13,1],[5,13,1],[1,15,-1],[13,14,1],[0,12,-1],[0,9,1],[19,20,1],[10,22,1],[11,25,1],[16,23,-1],[17,24,-1],[8,15,-1],[12,14,1],[5,7,-1],[1,4,1],[0,8,1],[5,6,1],[6,15,-1],[7,15,-1],[3,7,-1],[3,12,-1],[5,12,-1],[4,12,-1],[11,22,1],[10,25,1],[8,14,1],[4,13,1],[13,15,-1],[17,25,1],[3,8,1],[4,8,1],[5,8,1],[4,6,1],[4,7,-1],[20,21,1],[20,23,-1],[21,24,-1],[16,22,1],[3,6,1],[23,24,-1],[23,25,1],[22,24,-1],[2,9,1],[19,24,-1],[18,23,-1],[22,25,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/random-triangles-3d.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_author\": \"Kraft\",\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"file_description\": \"random disjoint triangles in 3D\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"vertices_coords\": [[0.3629426670075462,0.09346830358739466,0.6084810275370549],[0.030799797349832403,0.47390066818501997,0.1435636703160621],[0.40718128796332387,0.38206579405716123,0.9858188581065634],[0.9536614910228534,0.1966392072704457,0.42095442754739154],[0.24748497028185446,0.12724249998128712,0.7300450378933085],[0.36308476632552256,0.3400489916569194,0.7794683931666995],[0.09598845619750018,0.6854147242533477,0.8072183696792965],[0.2628044051045644,0.17718048427388355,0.7347378516777043],[0.046382260777202644,0.8561238557500108,0.5025754349153075],[0.25242663692372713,0.9533747677487479,0.4498131012842326],[0.2052865865321607,0.034808672619431436,0.20426726722203514],[0.9129032888886106,0.6270716774623786,0.6608798355383394],[0.1219717369260842,0.6930140291804385,0.65169740119955],[0.28684978132160177,0.7820283358599707,0.3852332698936298],[0.05634887737883454,0.5220734967532019,0.1497078750280565],[0.7631174776145266,0.871154335348147,0.4080205504657517],[0.6010866382120765,0.37517947779158667,0.4836336980297671],[0.7903152488119962,0.11348005224167679,0.9079790409562432],[0.35360952465642703,0.6037000606215208,0.8204675996684829],[0.05486098236956516,0.8752540629679744,0.34584149279293297],[0.7983928271470662,0.1474715104169677,0.7051581578098471],[0.5574247985907974,0.2580245101046563,0.86127436941499],[0.8965010028055584,0.9040878319443584,0.9026336050122921],[0.269616636165968,0.9139446373727789,0.4599350510466558],[0.6445235278101464,0.6218696005880973,0.916878917001956],[0.1891513196283745,0.4844772400437378,0.17623500594175678],[0.23503439742502374,0.8626179461753414,0.319168531006244],[0.8131210046293864,0.14014585432127813,0.4740083862867479],[0.75709707821298,0.23454004736241063,0.6237053137457742],[0.16645175648809496,0.1495369167530809,0.7297075642755091]],\n\t\"edges_vertices\": [[0,1],[1,2],[2,0],[3,4],[4,5],[5,3],[6,7],[7,8],[8,6],[9,10],[10,11],[11,9],[12,13],[13,14],[14,12],[15,16],[16,17],[17,15],[18,19],[19,20],[20,18],[21,22],[22,23],[23,21],[24,25],[25,26],[26,24],[27,28],[28,29],[29,27]],\n\t\"edges_assignment\": [\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\": [[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13,14],[15,16,17],[18,19,20],[21,22,23],[24,25,26],[27,28,29]]\n}\n"
  },
  {
    "path": "tests/files/fold/resch-tess.fold",
    "content": "{\n\t\"file_spec\": 1.1,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"vertices_coords\": [\n\t\t[0, 0.25],\n\t\t[0, 0.5],\n\t\t[0.25, 0.5],\n\t\t[0, 0.75],\n\t\t[0, 1],\n\t\t[0.25, 0.75],\n\t\t[0.25, 1],\n\t\t[0.5, 1],\n\t\t[0, 0],\n\t\t[0.25, 0.25],\n\t\t[0.5, 0.5],\n\t\t[0.5, 0],\n\t\t[0.25, 0],\n\t\t[0.5, 0.25],\n\t\t[0.75, 0.25],\n\t\t[0.5, 0.75],\n\t\t[0.75, 1],\n\t\t[0.75, 0.5],\n\t\t[0.75, 0.75],\n\t\t[1, 0.75],\n\t\t[1, 1],\n\t\t[0.75, 0],\n\t\t[1, 0.25],\n\t\t[1, 0],\n\t\t[1, 0.5]\n\t],\n\t\"edges_vertices\": [\n\t\t[1, 2],\n\t\t[4, 5],\n\t\t[3, 5],\n\t\t[5, 6],\n\t\t[8, 9],\n\t\t[9, 10],\n\t\t[1, 9],\n\t\t[9, 11],\n\t\t[8, 12],\n\t\t[11, 12],\n\t\t[9, 12],\n\t\t[2, 9],\n\t\t[11, 13],\n\t\t[0, 9],\n\t\t[9, 13],\n\t\t[13, 14],\n\t\t[10, 13],\n\t\t[2, 15],\n\t\t[15, 16],\n\t\t[6, 15],\n\t\t[15, 17],\n\t\t[2, 10],\n\t\t[10, 17],\n\t\t[2, 5],\n\t\t[10, 15],\n\t\t[7, 15],\n\t\t[17, 18],\n\t\t[5, 15],\n\t\t[15, 18],\n\t\t[18, 19],\n\t\t[16, 18],\n\t\t[21, 22],\n\t\t[13, 21],\n\t\t[14, 21],\n\t\t[11, 21],\n\t\t[21, 23],\n\t\t[22, 23],\n\t\t[14, 24],\n\t\t[18, 24],\n\t\t[14, 22],\n\t\t[14, 17],\n\t\t[22, 24],\n\t\t[19, 24],\n\t\t[17, 24],\n\t\t[19, 20],\n\t\t[0, 8],\n\t\t[0, 1],\n\t\t[1, 3],\n\t\t[3, 4],\n\t\t[4, 6],\n\t\t[6, 7],\n\t\t[7, 16],\n\t\t[16, 20]\n\t],\n\t\"edges_assignment\": [\n\t\t\"V\",\n\t\t\"M\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"V\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"M\",\n\t\t\"M\",\n\t\t\"V\",\n\t\t\"V\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"V\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\",\n\t\t\"B\"\n\t],\n\t\"edges_foldAngle\": [\n\t\t90,\n\t\t-180,\n\t\t90,\n\t\t180,\n\t\t-180,\n\t\t-180,\n\t\t-180,\n\t\t-180,\n\t\t0,\n\t\t0,\n\t\t90,\n\t\t90,\n\t\t180,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t-180,\n\t\t-180,\n\t\t-180,\n\t\t-180,\n\t\t180,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t180,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t90,\n\t\t-180,\n\t\t-180,\n\t\t90,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t-180,\n\t\t-180,\n\t\t180,\n\t\t90,\n\t\t0,\n\t\t0,\n\t\t90,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0\n\t],\n\t\"vertices_vertices\": [\n\t\t[8, 9, 1],\n\t\t[0, 9, 2, 3],\n\t\t[9, 10, 15, 5, 1],\n\t\t[1, 5, 4],\n\t\t[3, 5, 6],\n\t\t[2, 15, 6, 4, 3],\n\t\t[5, 15, 7, 4],\n\t\t[15, 16, 6],\n\t\t[12, 9, 0],\n\t\t[8, 12, 11, 13, 10, 2, 1, 0],\n\t\t[9, 13, 17, 15, 2],\n\t\t[21, 13, 9, 12],\n\t\t[11, 9, 8],\n\t\t[11, 21, 14, 10, 9],\n\t\t[21, 22, 24, 17, 13],\n\t\t[2, 10, 17, 18, 16, 7, 6, 5],\n\t\t[15, 18, 20, 7],\n\t\t[14, 24, 18, 15, 10],\n\t\t[17, 24, 19, 16, 15],\n\t\t[24, 20, 18],\n\t\t[19, 16],\n\t\t[23, 22, 14, 13, 11],\n\t\t[21, 23, 24, 14],\n\t\t[22, 21],\n\t\t[14, 22, 19, 18, 17]\n\t],\n\t\"faces_vertices\": [\n\t\t[9, 0, 8],\n\t\t[1, 0, 9],\n\t\t[2, 1, 9],\n\t\t[3, 1, 2, 5],\n\t\t[10, 2, 9],\n\t\t[15, 2, 10],\n\t\t[5, 2, 15],\n\t\t[4, 3, 5],\n\t\t[6, 4, 5],\n\t\t[6, 5, 15],\n\t\t[7, 6, 15],\n\t\t[16, 7, 15],\n\t\t[9, 8, 12],\n\t\t[11, 9, 12],\n\t\t[13, 9, 11],\n\t\t[10, 9, 13],\n\t\t[17, 10, 13, 14],\n\t\t[15, 10, 17],\n\t\t[13, 11, 21],\n\t\t[14, 13, 21],\n\t\t[22, 14, 21],\n\t\t[24, 14, 22],\n\t\t[17, 14, 24],\n\t\t[18, 15, 17],\n\t\t[16, 15, 18],\n\t\t[20, 16, 18, 19],\n\t\t[18, 17, 24],\n\t\t[19, 18, 24],\n\t\t[22, 21, 23]\n\t],\n\t\"faces_edges\": [\n\t\t[13, 45, 4],\n\t\t[46, 13, 6],\n\t\t[0, 6, 11],\n\t\t[47, 0, 23, 2],\n\t\t[21, 11, 5],\n\t\t[17, 21, 24],\n\t\t[23, 17, 27],\n\t\t[48, 2, 1],\n\t\t[49, 1, 3],\n\t\t[3, 27, 19],\n\t\t[50, 19, 25],\n\t\t[51, 25, 18],\n\t\t[4, 8, 10],\n\t\t[7, 10, 9],\n\t\t[14, 7, 12],\n\t\t[5, 14, 16],\n\t\t[22, 16, 15, 40],\n\t\t[24, 22, 20],\n\t\t[12, 34, 32],\n\t\t[15, 32, 33],\n\t\t[39, 33, 31],\n\t\t[37, 39, 41],\n\t\t[40, 37, 43],\n\t\t[28, 20, 26],\n\t\t[18, 28, 30],\n\t\t[52, 30, 29, 44],\n\t\t[26, 43, 38],\n\t\t[29, 38, 42],\n\t\t[31, 35, 36]\n\t],\n\t\"edges_faces\": [\n\t\t[2, 3],\n\t\t[7, 8],\n\t\t[3, 7],\n\t\t[8, 9],\n\t\t[0, 12],\n\t\t[4, 15],\n\t\t[1, 2],\n\t\t[13, 14],\n\t\t[12],\n\t\t[13],\n\t\t[12, 13],\n\t\t[2, 4],\n\t\t[14, 18],\n\t\t[0, 1],\n\t\t[14, 15],\n\t\t[16, 19],\n\t\t[15, 16],\n\t\t[5, 6],\n\t\t[11, 24],\n\t\t[9, 10],\n\t\t[17, 23],\n\t\t[4, 5],\n\t\t[16, 17],\n\t\t[3, 6],\n\t\t[5, 17],\n\t\t[10, 11],\n\t\t[23, 26],\n\t\t[6, 9],\n\t\t[23, 24],\n\t\t[25, 27],\n\t\t[24, 25],\n\t\t[20, 28],\n\t\t[18, 19],\n\t\t[19, 20],\n\t\t[18],\n\t\t[28],\n\t\t[28],\n\t\t[21, 22],\n\t\t[26, 27],\n\t\t[20, 21],\n\t\t[16, 22],\n\t\t[21],\n\t\t[27],\n\t\t[22, 26],\n\t\t[25],\n\t\t[0],\n\t\t[1],\n\t\t[3],\n\t\t[7],\n\t\t[8],\n\t\t[10],\n\t\t[11],\n\t\t[25]\n\t],\n\t\"vertices_faces\": [\n\t\t[0, 1],\n\t\t[1, 2, 3],\n\t\t[2, 3, 4, 5, 6],\n\t\t[3, 7],\n\t\t[7, 8],\n\t\t[3, 6, 7, 8, 9],\n\t\t[8, 9, 10],\n\t\t[10, 11],\n\t\t[0, 12],\n\t\t[0, 1, 2, 4, 12, 13, 14, 15],\n\t\t[4, 5, 15, 16, 17],\n\t\t[13, 14, 18],\n\t\t[12, 13],\n\t\t[14, 15, 16, 18, 19],\n\t\t[16, 19, 20, 21, 22],\n\t\t[5, 6, 9, 10, 11, 17, 23, 24],\n\t\t[11, 24, 25],\n\t\t[16, 17, 22, 23, 26],\n\t\t[23, 24, 25, 26, 27],\n\t\t[25, 27],\n\t\t[25],\n\t\t[18, 19, 20, 28],\n\t\t[20, 21, 28],\n\t\t[28],\n\t\t[21, 22, 26, 27]\n\t],\n\t\"edges_length\": [\n\t\t0.25,\n\t\t0.3535533905932738,\n\t\t0.25,\n\t\t0.25,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.3535533905932738,\n\t\t0.3535533905932738,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25,\n\t\t0.25\n\t],\n\t\"faces_faces\": [\n\t\t[1, 12],\n\t\t[0, 2],\n\t\t[1, 3, 4],\n\t\t[2, 6, 7],\n\t\t[2, 5, 15],\n\t\t[4, 6, 17],\n\t\t[3, 5, 9],\n\t\t[3, 8],\n\t\t[7, 9],\n\t\t[8, 6, 10],\n\t\t[9, 11],\n\t\t[10, 24],\n\t\t[0, 13],\n\t\t[12, 14],\n\t\t[13, 15, 18],\n\t\t[4, 14, 16],\n\t\t[15, 17, 19, 22],\n\t\t[5, 16, 23],\n\t\t[14, 19],\n\t\t[16, 18, 20],\n\t\t[19, 21, 28],\n\t\t[20, 22],\n\t\t[16, 21, 26],\n\t\t[17, 24, 26],\n\t\t[11, 23, 25],\n\t\t[24, 27],\n\t\t[23, 22, 27],\n\t\t[25, 26],\n\t\t[20]\n\t],\n\t\"vertices_edges\": [\n\t\t[13, 45, 46],\n\t\t[0, 6, 46, 47],\n\t\t[0, 11, 17, 21, 23],\n\t\t[2, 47, 48],\n\t\t[1, 48, 49],\n\t\t[1, 2, 3, 23, 27],\n\t\t[3, 19, 49, 50],\n\t\t[25, 50, 51],\n\t\t[4, 8, 45],\n\t\t[4, 5, 6, 7, 10, 11, 13, 14],\n\t\t[5, 16, 21, 22, 24],\n\t\t[7, 9, 12, 34],\n\t\t[8, 9, 10],\n\t\t[12, 14, 15, 16, 32],\n\t\t[15, 33, 37, 39, 40],\n\t\t[17, 18, 19, 20, 24, 25, 27, 28],\n\t\t[18, 30, 51, 52],\n\t\t[20, 22, 26, 40, 43],\n\t\t[26, 28, 29, 30, 38],\n\t\t[29, 42, 44],\n\t\t[44, 52],\n\t\t[31, 32, 33, 34, 35],\n\t\t[31, 36, 39, 41],\n\t\t[35, 36],\n\t\t[37, 38, 41, 42, 43]\n\t],\n\t\"edges_edges\": [\n\t\t[6, 46, 47, 11, 17, 21, 23],\n\t\t[48, 49, 2, 3, 23, 27],\n\t\t[47, 48, 1, 3, 23, 27],\n\t\t[1, 2, 23, 27, 19, 49, 50],\n\t\t[8, 45, 5, 6, 7, 10, 11, 13, 14],\n\t\t[4, 6, 7, 10, 11, 13, 14, 16, 21, 22, 24],\n\t\t[0, 46, 47, 4, 5, 7, 10, 11, 13, 14],\n\t\t[4, 5, 6, 10, 11, 13, 14, 9, 12, 34],\n\t\t[4, 45, 9, 10],\n\t\t[8, 10, 7, 12, 34],\n\t\t[8, 9, 4, 5, 6, 7, 11, 13, 14],\n\t\t[4, 5, 6, 7, 10, 13, 14, 0, 17, 21, 23],\n\t\t[7, 9, 34, 14, 15, 16, 32],\n\t\t[45, 46, 4, 5, 6, 7, 10, 11, 14],\n\t\t[4, 5, 6, 7, 10, 11, 13, 12, 15, 16, 32],\n\t\t[12, 14, 16, 32, 33, 37, 39, 40],\n\t\t[12, 14, 15, 32, 5, 21, 22, 24],\n\t\t[0, 11, 21, 23, 18, 19, 20, 24, 25, 27, 28],\n\t\t[17, 19, 20, 24, 25, 27, 28, 30, 51, 52],\n\t\t[3, 49, 50, 17, 18, 20, 24, 25, 27, 28],\n\t\t[17, 18, 19, 24, 25, 27, 28, 22, 26, 40, 43],\n\t\t[0, 11, 17, 23, 5, 16, 22, 24],\n\t\t[5, 16, 21, 24, 20, 26, 40, 43],\n\t\t[0, 11, 17, 21, 1, 2, 3, 27],\n\t\t[5, 16, 21, 22, 17, 18, 19, 20, 25, 27, 28],\n\t\t[17, 18, 19, 20, 24, 27, 28, 50, 51],\n\t\t[20, 22, 40, 43, 28, 29, 30, 38],\n\t\t[1, 2, 3, 23, 17, 18, 19, 20, 24, 25, 28],\n\t\t[17, 18, 19, 20, 24, 25, 27, 26, 29, 30, 38],\n\t\t[26, 28, 30, 38, 42, 44],\n\t\t[26, 28, 29, 38, 18, 51, 52],\n\t\t[32, 33, 34, 35, 36, 39, 41],\n\t\t[12, 14, 15, 16, 31, 33, 34, 35],\n\t\t[31, 32, 34, 35, 15, 37, 39, 40],\n\t\t[7, 9, 12, 31, 32, 33, 35],\n\t\t[31, 32, 33, 34, 36],\n\t\t[35, 31, 39, 41],\n\t\t[15, 33, 39, 40, 38, 41, 42, 43],\n\t\t[26, 28, 29, 30, 37, 41, 42, 43],\n\t\t[15, 33, 37, 40, 31, 36, 41],\n\t\t[15, 33, 37, 39, 20, 22, 26, 43],\n\t\t[31, 36, 39, 37, 38, 42, 43],\n\t\t[37, 38, 41, 43, 29, 44],\n\t\t[20, 22, 26, 40, 37, 38, 41, 42],\n\t\t[29, 42, 52],\n\t\t[4, 8, 13, 46],\n\t\t[13, 45, 0, 6, 47],\n\t\t[0, 6, 46, 2, 48],\n\t\t[2, 47, 1, 49],\n\t\t[1, 48, 3, 19, 50],\n\t\t[3, 19, 49, 25, 51],\n\t\t[25, 50, 18, 30, 52],\n\t\t[18, 30, 51, 44]\n\t],\n\t\"re:code\": \"// cp.load(RabbitEar.bases.unit_square);\\n\\nconst CNT = 12;\\nconst W = 3/CNT;\\n\\nconst u = ear.vector(W, 2*W);\\nconst v = ear.vector(2*W, -W);\\n\\nconst tile = (x, y, w = W*3, h = W*3) => {\\n  cp.mountain(x, y, x+w/3*2, y+h/3*2);\\n  cp.mountain(x, y+h/3*2, x+w/3*2, y);\\n  cp.valley(x, y, x+w/3, y);\\n  cp.valley(x+w/3, y, x+w/3*2, y).foldAngle = 90;\\n  cp.valley(x, y, x, y+h/3).foldAngle = 90;\\n  cp.valley(x, y+h/3, x+w, y+h/3).foldAngle = 90;\\n  cp.valley(x+w/3, y, x+w/3, y+h/3*2).foldAngle = 90;\\n  cp.valley(x+w/3*2, y, x+w/3*2, y+h/3);\\n  cp.valley(x+w/3*2, y+h/3, x+w/3*2, y+h/3*2).foldAngle = 90;\\n};\\n\\nfor (let j = -2; j < 5; j += 1) {\\n  for (let i = 0; i < 7; i += 1) {\\n    tile(u.x*i + v.x*j, u.y*i + v.y*j);\\n  }\\n}\\ncp.clean();\\nconst pad = 0.01;\\ncp.crop(ear.rect(1+pad*2, 1+pad*2, -pad, -pad));\\n\\ncp.valley(0,0,0,1);\\ncp.valley(1,1,0,1);\\ncp.makeBoundary();\\n\\n\\n\"\n}"
  },
  {
    "path": "tests/files/fold/separated-parallel-edges.fold",
    "content": "{\"vertices_coords\":[[2,0],[1,0],[-1,0],[-2,0],[0,2],[0,-2],[-1,-1],[-0.5,-0.5],[0.5,0.5],[1,1]],\"edges_vertices\":[[0,1],[2,3],[4,5],[6,7],[8,9],[4,9],[9,0],[5,6],[6,3],[0,5],[3,4]],\"edges_assignment\":[\"V\",\"V\",\"V\",\"M\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\"edges_foldAngle\":[180,180,180,-180,-180,0,0,0,0,0,0],\"vertices_edges\":[[6,0,9],[0],[1],[1,10,8],[10,2,5],[9,2,7],[3,8,7],[3],[4],[5,4,6]],\"vertices_vertices\":[[9,1,5],[0],[3],[2,4,6],[3,5,9],[0,4,6],[7,3,5],[6],[9],[4,8,0]],\"faces_vertices\":[[0,9,8,9,4,5,0,1],[2,3,6,7,6,5,4,3]],\"faces_edges\":[[6,4,4,5,2,9,0,0],[1,8,3,3,7,2,10,1]],\"vertices_faces\":[[0,0,null],[0],[1],[1,null,1],[1,0,null],[0,1,null],[1,null,1],[1],[0],[0,0,null]],\"edges_faces\":[[0],[1],[0,1],[1],[0],[0],[0],[1],[1],[0],[1]],\"faces_faces\":[[null,null,null,null,1,null,null,null],[null,null,null,null,null,0,null,null]]}\n"
  },
  {
    "path": "tests/files/fold/square-fish-3d.fold",
    "content": "{\n\t\"file_spec\":1.1,\n\t\"file_creator\":\"Rabbit Ear\",\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[0.5857864376269049,0],[1,0],[0,1],[0,0.5857864376269049],[0.5,0.5],[0.7071067811865476,0.7071067811865476],[1,1],[0.7071067811865476,0.2928932188134524],[1,0.41421356237309515],[0.2928932188134524,0.7071067811865476],[0.41421356237309515,1],[0.7071067811865476,1],[1,0.7071067811865476]],\n\t\"edges_vertices\":[[0,1],[1,2],[3,4],[4,0],[0,5],[5,6],[6,7],[0,8],[8,9],[0,10],[10,11],[8,1],[10,4],[8,6],[6,12],[3,10],[10,5],[5,8],[8,2],[10,6],[6,13],[7,12],[12,11],[11,3],[11,6],[6,9],[2,9],[9,13],[13,7]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\"V\",\"V\",\"M\",\"V\",\"M\",\"M\",\"M\",\"V\",\"F\",\"V\",\"F\",\"F\",\"V\",\"V\",\"F\",\"B\",\"B\",\"B\",\"M\",\"M\",\"B\",\"B\",\"B\"],\n\t\"edges_foldAngle\":[0,0,0,0,0,0,180,180,-90,180,-90,-90,-90,180,0,180,0,0,180,180,0,0,0,0,-90,-90,0,0,0],\n\t\"faces_vertices\":[[0,1,8],[0,8,5],[0,5,10],[0,10,4],[1,2,8],[2,9,8],[3,4,10],[3,10,11],[5,6,10],[5,8,6],[6,13,7],[6,7,12],[6,12,11],[6,11,10],[6,8,9],[6,9,13]]\n}\n"
  },
  {
    "path": "tests/files/fold/square-tube-with-overlap.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[0,2],[0,1],[5,1],[4,1],[3,1],[2,1],[1,1],[1,2],[2,2],[3,2],[4,2],[5,2]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[6,7],[7,0],[8,9],[9,10],[10,11],[11,12],[12,7],[13,12],[12,1],[14,11],[11,2],[6,13],[13,14],[14,15],[15,16],[16,17],[15,10],[10,3],[16,9],[9,4],[5,8],[8,17]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,180,180,180,180,180,-90,90,-90,90,0,0,0,0,0,-90,90,-90,90,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,12,7],[1,2,11,12],[2,3,10,11],[3,4,9,10],[4,5,8,9],[6,7,12,13],[8,17,16,9],[9,16,15,10],[10,15,14,11],[11,14,13,12]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\":[\n\t\t\t[0,0,0],[1,0,0],[1,0,1],[0,0,1],[0,0,0],[1,0,0],[0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,1],[1,1,1],[1,1,0],[1,0,0],[1,0,1],[0,0,1],[0,0,0],[1,0,0]\n\t\t],\n\t\t\"faceOrders\":[\n\t\t\t[4,6,1],[0,5,1],[2,8,1],[1,9,1],[3,7,1],[4,5,1],[0,4,1],[0,6,-1],[5,6,-1]\n\t\t],\n\t\t\"frame_classes\": [\"foldedForm\"]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/square-twist.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[3,0],[4,0],[6,0],[0,6],[0,3],[0,2],[4,3],[3,2],[2,3],[3,4],[2,6],[3,6],[6,3],[6,4],[6,6]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[4,5],[5,6],[6,0],[7,8],[9,10],[9,11],[8,6],[1,8],[10,12],[5,9],[7,13],[8,9],[10,14],[7,2],[10,7],[3,13],[13,14],[14,15],[15,12],[12,11],[11,4]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"M\",\"M\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"M\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,8,6],[1,2,7,8],[2,3,13,7],[4,5,9,11],[5,6,8,9],[7,13,14,10],[7,10,9,8],[9,10,12,11],[10,14,15,12]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[1,5],[1,2],[1,3],[1,1],[5,5],[2,5],[3,5],[4,3],[3,2],[2,3],[3,4],[5,3],[5,4],[4,1],[3,1],[5,1]\n\t\t],\n\t\t\"faceOrders\":[\n\t\t\t[1,6,-1],[6,7,-1],[3,7,1],[0,4,1],[0,1,-1],[7,8,-1],[3,4,-1],[2,5,-1],[4,6,-1],[5,8,1],[1,2,1],[5,6,-1],[2,6,-1],[1,5,-1],[1,4,1],[3,6,-1],[4,7,1],[5,7,-1],[3,5,1],[3,8,-1],[0,3,-1],[0,6,-1],[0,7,1],[0,2,1],[2,8,1],[2,4,1],[1,8,1],[6,8,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/strip-weave-concave.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[0,1],[0,3],[0,5],[0,7],[1,0],[3,0],[5,0],[7,0],[2,1],[1,2],[7,1],[6,1],[4,1],[1,1],[1,7],[1,6],[1,4]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[0,5],[5,6],[6,7],[7,8],[5,9],[1,10],[11,12],[12,13],[13,9],[9,14],[15,16],[16,17],[17,10],[10,14],[6,13],[2,17],[3,16],[7,12],[4,15],[11,8]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,5,9,14,10,1],[1,10,17,2],[2,17,16,3],[3,16,15,4],[5,6,13,9],[6,7,12,13],[7,8,11,12]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[0,0],[0,1],[2,1],[2,3],[4,3],[1,0],[1,2],[3,2],[3,4],[2,1],[1,2],[4,4],[4,3],[2,3],[1,1],[4,4],[3,4],[3,2]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/strip-weave.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[0,1],[0,3],[0,5],[0,7],[1,0],[3,0],[5,0],[7,0],[2,1],[1,2],[7,1],[6,1],[4,1],[1,7],[1,6],[1,4]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[0,5],[5,6],[6,7],[7,8],[5,9],[1,10],[11,12],[12,13],[13,9],[14,15],[15,16],[16,10],[6,13],[2,16],[10,9],[3,15],[7,12],[4,14],[11,8]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"B\",\"V\",\"V\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,5,9,10,1],[1,10,16,2],[2,16,15,3],[3,15,14,4],[5,6,13,9],[6,7,12,13],[7,8,11,12]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[[0,0],[0,1],[2,1],[2,3],[4,3],[1,0],[1,2],[3,2],[3,4],[2,1],[1,2],[4,4],[4,3],[2,3],[4,4],[3,4],[3,2]]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/strip-with-angle.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[2,0],[3,0],[5,0],[6,0],[7,0],[9,0],[0,1],[9,1],[7,1],[6,1],[4,1],[3,1],[2,1]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[7,0],[8,9],[9,10],[10,11],[11,12],[12,13],[13,7],[13,1],[12,2],[11,3],[10,4],[9,5],[6,8]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"M\",\"M\",\"B\"],\n\t\"edges_foldAngle\":[0,0,0,0,0,0,0,0,0,0,0,0,0,90,90,180,-90,-90,0],\n\t\"vertices_edges\":[[0,6],[1,13,0],[2,14,1],[3,15,2],[4,16,3],[5,17,4],[18,5],[12,6],[7,18],[7,8,17],[8,9,16],[9,10,15],[10,11,14],[11,12,13]],\n\t\"vertices_vertices\":[[1,7],[2,13,0],[3,12,1],[4,11,2],[5,10,3],[6,9,4],[8,5],[13,0],[9,6],[8,10,5],[9,11,4],[10,12,3],[11,13,2],[12,7,1]],\n\t\"faces_vertices\":[[0,1,13,7],[1,2,12,13],[2,3,11,12],[3,4,10,11],[4,5,9,10],[5,6,8,9]],\n\t\"faces_edges\":[[0,13,12,6],[1,14,11,13],[2,15,10,14],[3,16,9,15],[4,17,8,16],[5,18,7,17]],\n\t\"vertices_faces\":[[0,null],[1,0,null],[2,1,null],[3,2,null],[4,3,null],[5,4,null],[5,null],[null,0],[5,null],[null,4,5],[null,3,4],[null,2,3],[null,1,2],[null,0,1]],\n\t\"edges_faces\":[[0],[1],[2],[3],[4],[5],[0],[5],[4],[3],[2],[1],[0],[0,1],[1,2],[2,3],[3,4],[4,5],[5]],\n\t\"faces_faces\":[[1],[2,0],[3,1],[4,2],[5,3],[4]],\n\t\"file_frames\": [{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[5,0,1],[3,0,1],[3,0,0],[5,0,0],[5,-1,0],[5,-1,1],[5,1,1],[5,1,1],[4,1,1],[4,-1,1],[4,-1,0],[4,1,0],[3,1,0],[3,1,1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/surrounded-square.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_title\": \"inset square with nice graph numbering\",\n\t\"file_author\": \"Kraft\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],\n\t\t[1,0],\n\t\t[1,1],\n\t\t[0,1],\n\t\t[0.29289321881345265,0.2928932188134526],\n\t\t[0.7071067811865475,0.29289321881345254],\n\t\t[0.7071067811865476,0.7071067811865476],\n\t\t[0.2928932188134527,0.7071067811865475]\n\t],\n\t\"vertices_vertices\":[\n\t\t[1,4,3],[2,5,0],[3,6,1],[0,7,2],[5,7,0],[6,4,1],[7,5,2],[4,6,3]\n\t],\n\t\"vertices_edges\":[\n\t\t[0,8,3],[1,9,0],[2,10,1],[3,11,2],[4,7,8],[5,4,9],[6,5,10],[7,6,11]\n\t],\n\t\"vertices_faces\":[\n\t\t[1,4,null],[2,1,null],[3,2,null],[4,3,null],[0,4,1],[0,1,2],[0,2,3],[0,3,4]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"F\",\"F\",\"F\",\"F\",\"J\",\"J\",\"J\",\"J\"\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,0,0,0,0\n\t],\n\t\"edges_faces\":[\n\t\t[1],[2],[3],[4],[0,1],[0,2],[0,3],[0,4],[4,1],[1,2],[2,3],[3,4]\n\t],\n\t\"faces_vertices\":[\n\t\t[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]\n\t],\n\t\"faces_edges\":[\n\t\t[4,5,6,7],[0,9,4,8],[1,10,5,9],[2,11,6,10],[3,8,7,11]\n\t],\n\t\"faces_faces\":[\n\t\t[1,2,3,4],[null,2,0,4],[null,3,0,1],[null,4,0,2],[null,1,0,3]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/triangle-strip-2.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[[0,0],[1.154700538379252,0],[2.309401076758504,0],[3.4641016151377557,0],[4.618802153517009,0],[5.773502691896262,0],[6.928203230275514,0],[7.50555349946514,0],[0,2],[0,1],[0.5773502691896265,1],[1.1547005383792528,2],[7.50555349946514,1],[6.350852961085888,1],[5.196152422706636,1],[4.041451884327383,0.9999999999999999],[2.886751345948129,1.0000000000000002],[1.7320508075688783,1.0000000000000002],[2.309401076758504,2],[3.464101615137755,2],[4.618802153517009,2],[5.773502691896262,2],[6.928203230275514,2],[7.50555349946514,2]],\n\t\"edges_vertices\":[[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[8,9],[9,0],[0,10],[10,11],[12,13],[13,14],[14,15],[15,16],[16,17],[17,10],[10,9],[1,17],[17,18],[8,10],[10,1],[8,11],[11,18],[18,19],[19,20],[20,21],[21,22],[22,23],[2,16],[16,19],[11,17],[17,2],[3,15],[15,20],[18,16],[16,3],[4,15],[15,19],[4,14],[14,21],[20,14],[14,5],[5,13],[13,22],[21,13],[13,6],[6,12],[12,22],[23,12],[12,7]],\n\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"V\",\"V\",\"F\",\"F\",\"M\",\"M\",\"F\",\"F\",\"V\",\"V\",\"B\",\"B\"],\n\t\"faces_vertices\":[[0,1,10],[0,10,9],[1,2,17],[1,17,10],[2,3,16],[2,16,17],[3,4,15],[3,15,16],[4,5,14],[4,14,15],[5,6,13],[5,13,14],[6,7,12],[6,12,13],[8,9,10],[8,10,11],[10,17,11],[11,17,18],[12,23,22],[12,22,13],[13,22,21],[13,21,14],[14,21,20],[14,20,15],[15,20,19],[15,19,16],[16,19,18],[16,18,17]],\n\t\"file_frames\":[{\n\t\t\"frame_parent\":0,\n\t\t\"frame_inherit\":true,\n\t\t\"frame_classes\":[\"foldedForm\"],\n\t\t\"vertices_coords\":[\n\t\t\t[0, 0], [1.154700538379252, 0], [0.57735026919, 1], [1.7320508075688785, 1], [1.1547005383792561, 2], [2.3094010767585087, 2], [1.732050807568887, 3], [2.309401076758513, 3], [1.7320508075688785, 1], [0.8660254037844392, 0.5], [0.57735026919, 1], [1.1547005383792526, 2], [2.309401076758517, 4], [2.886751345948139, 3], [1.7320508075688863, 3], [2.309401076758508, 2], [1.1547005383792537, 2], [1.732050807568878, 1], [2.309401076758504, 2], [1.7320508075688785, 3], [2.8867513459481344, 3], [2.309401076758511, 4], [3.4641016151377633, 4], [3.1754264805429555, 3.5]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/triangle-strip.fold",
    "content": "{\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0, 0],\n\t\t[1.154700538379252, 0],\n\t\t[2.309401076758504, 0],\n\t\t[3.4641016151377557, 0],\n\t\t[4.618802153517009, 0],\n\t\t[5.773502691896262, 0],\n\t\t[6.928203230275514, 0],\n\t\t[7.50555349946514, 0],\n\t\t[0, 1],\n\t\t[0.5773502691896264, 1],\n\t\t[7.50555349946514, 1],\n\t\t[6.350852961085888, 1],\n\t\t[5.196152422706636, 1],\n\t\t[4.041451884327383, 1],\n\t\t[2.886751345948129, 1],\n\t\t[1.7320508075688779, 1]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[8,0],[0,9],[10,11],[11,12],[12,13],[13,14],[14,15],[15,9],[9,8],[9,1],[1,15],[15,2],[2,14],[3,13],[14,3],[13,4],[4,12],[12,5],[5,11],[11,6],[6,10],[10,7]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"F\",\"M\",\"F\",\"V\",\"M\",\"F\",\"F\",\"V\",\"F\",\"M\",\"F\",\"V\",\"B\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,9],[0,9,8],[1,2,15],[1,15,9],[2,3,14],[2,14,15],[3,4,13],[3,13,14],[4,5,12],[4,12,13],[5,6,11],[5,11,12],[6,7,10],[6,10,11]\n\t],\n\t\"file_frames\":[{\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"vertices_coords\": [\n\t\t\t[0, 0],\n\t\t\t[1.154700538379252, 0],\n\t\t\t[0.5773502691896261, 1],\n\t\t\t[1.7320508075688779, 1],\n\t\t\t[1.1547005383792552, 2],\n\t\t\t[2.3094010767585074, 2],\n\t\t\t[1.7320508075688856, 3],\n\t\t\t[2.309401076758512, 3],\n\t\t\t[0.8660254037844393, 0.5],\n\t\t\t[0.5773502691896264, 1],\n\t\t\t[2.309401076758515, 4],\n\t\t\t[2.886751345948137, 3],\n\t\t\t[1.7320508075688847, 3],\n\t\t\t[2.309401076758507, 2],\n\t\t\t[1.1547005383792524, 2],\n\t\t\t[1.7320508075688776, 1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/fold/two-bird-cp.fold",
    "content": "{\n\t\"vertices_coords\":[\n\t\t[0,0],[0.5,0],[1,0],[1,0.5],[1,1],[0.5,1],[0,1],[0,0.5],[0.5,0.5],[0.5,0.20710678118654757],[0.7928932188134524,0.5],[0.5,0.7928932188134524],[0.20710678118654757,0.5],[0.3535533905932738,0.3535533905932738],[0.6464466094067263,0.6464466094067263],[2,2],[2.5,2],[3,2],[3,2.5],[3,3],[2.5,3],[2,3],[2,2.5],[2.5,2.5],[2.5,2.2071067811865475],[2.7928932188134525,2.5],[2.5,2.7928932188134525],[2.2071067811865475,2.5],[2.353553390593274,2.353553390593274],[2.646446609406726,2.646446609406726]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],[0,9],[9,2],[2,10],[10,4],[4,11],[11,6],[6,12],[12,0],[1,9],[9,8],[3,10],[10,8],[5,11],[11,8],[7,12],[12,8],[2,8],[6,8],[0,13],[13,8],[13,9],[13,12],[4,14],[14,8],[14,10],[14,11],[15,16],[16,17],[17,18],[18,19],[19,20],[20,21],[21,22],[22,15],[15,24],[24,17],[17,25],[25,19],[19,26],[26,21],[21,27],[27,15],[16,24],[24,23],[18,25],[25,23],[20,26],[26,23],[22,27],[27,23],[17,23],[21,23],[15,28],[28,23],[28,24],[28,27],[19,29],[29,23],[29,25],[29,26]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\"\n\t],\n\t\"vertices_edges\":[\n\t\t[0,8,26,15,7],[1,16,0],[2,10,24,9,1],[3,18,2],[4,12,30,11,3],[4,5,20],[5,6,14,25,13],[22,6,7],[19,31,21,25,23,27,17,24],[17,28,8,16,9],[18,11,32,19,10],[12,20,13,21,33],[23,14,22,15,29],[27,29,26,28],[30,33,31,32],[34,42,60,49,41],[35,50,34],[36,44,58,43,35],[37,52,36],[38,46,64,45,37],[38,39,54],[39,40,48,59,47],[56,40,41],[53,65,55,59,57,61,51,58],[51,62,42,50,43],[52,45,66,53,44],[46,54,47,55,67],[57,48,56,49,63],[61,63,60,62],[64,67,65,66]\n\t],\n\t\"vertices_vertices\":[\n\t\t[1,9,13,12,7],[2,9,0],[3,10,8,9,1],[4,10,2],[5,11,14,10,3],[4,6,11],[5,7,12,8,11],[12,6,0],[10,14,11,6,12,13,9,2],[8,13,0,1,2],[3,4,14,8,2],[4,5,6,8,14],[8,6,7,0,13],[8,12,0,9],[4,11,8,10],[16,24,28,27,22],[17,24,15],[18,25,23,24,16],[19,25,17],[20,26,29,25,18],[19,21,26],[20,22,27,23,26],[27,21,15],[25,29,26,21,27,28,24,17],[23,28,15,16,17],[18,19,29,23,17],[19,20,21,23,29],[23,21,22,15,28],[23,27,15,24],[19,26,23,25]\n\t],\n\t\"edges_foldAngle\":[\n\t\t0,0,0,0,0,0,0,0,180,180,180,180,180,180,180,180,-180,180,-180,180,-180,180,-180,180,-180,-180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,180,180,180,180,180,180,180,-180,180,-180,180,-180,180,-180,180,-180,-180,0,0,0,0,0,0,0,0\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,9],[0,9,13],[0,13,12],[0,12,7],[1,2,9],[2,3,10],[2,10,8],[2,8,9],[3,4,10],[4,5,11],[4,11,14],[4,14,10],[5,6,11],[6,7,12],[6,12,8],[6,8,11],[8,10,14],[8,14,11],[8,12,13],[8,13,9],[15,16,24],[15,24,28],[15,28,27],[15,27,22],[16,17,24],[17,18,25],[17,25,23],[17,23,24],[18,19,25],[19,20,26],[19,26,29],[19,29,25],[20,21,26],[21,22,27],[21,27,23],[21,23,26],[23,25,29],[23,29,26],[23,27,28],[23,28,24]\n\t]\n}"
  },
  {
    "path": "tests/files/fold/wavy-miura-no-faces.fold",
    "content": "{\n  \"frame_classes\":[\"creasePattern\"],\n  \"vertices_coords\":[[0.333,0],[1.333,0.4],[2.333,0],[3.333,0.4],[4.333,0],[5.333,0.4],[6.333,0],[7.333,0.4],[8.333,0],[0.17992066785409055,1],[1.1799206678540906,1.4],[2.1799206678540903,1],[3.1799206678540903,1.4],[4.17992066785409,1],[5.17992066785409,1.4],[6.17992066785409,1],[7.17992066785409,1.4],[8.179920667854091,1],[-0.13857689657019842,2],[0.8614231034298016,2.4],[1.8614231034298017,2],[2.8614231034298014,2.4],[3.8614231034298014,2],[4.861423103429802,2.4],[5.861423103429802,2],[6.861423103429802,2.4],[7.861423103429802,2],[-0.32966750136794837,3],[0.6703324986320516,3.4],[1.6703324986320516,3],[2.670332498632052,3.4],[3.670332498632052,3],[4.670332498632051,3.4],[5.670332498632051,3],[6.670332498632051,3.4],[7.670332498632051,3],[-0.2176633257475828,4],[0.7823366742524172,4.4],[1.7823366742524172,4],[2.7823366742524174,4.4],[3.7823366742524174,4],[4.782336674252417,4.4],[5.782336674252417,4],[6.782336674252417,4.4],[7.782336674252417,4],[0.09445950775925435,5],[1.0944595077592543,5.4],[2.0944595077592543,5],[3.0944595077592543,5.4],[4.094459507759255,5],[5.094459507759255,5.4],[6.094459507759255,5],[7.094459507759255,5.4],[8.094459507759254,5],[0.3197367054545719,6],[1.319736705454572,6.4],[2.319736705454572,6],[3.319736705454572,6.4],[4.319736705454572,6],[5.319736705454572,6.4],[6.319736705454572,6],[7.319736705454572,6.4],[8.319736705454572,6],[0.25104945069632045,7],[1.2510494506963203,7.4],[2.2510494506963203,7],[3.2510494506963203,7.4],[4.25104945069632,7],[5.25104945069632,7.4],[6.25104945069632,7],[7.25104945069632,7.4],[8.251049450696321,7],[-0.04845151125826831,8],[0.9515484887417317,8.4],[1.9515484887417316,8],[2.951548488741732,8.4],[3.951548488741732,8],[4.951548488741731,8.4],[5.951548488741731,8],[6.951548488741731,8.4],[7.951548488741731,8]],\n  \"edges_vertices\":[[0,9],[1,10],[2,11],[3,12],[4,13],[5,14],[6,15],[7,16],[8,17],[9,18],[10,19],[11,20],[12,21],[13,22],[14,23],[15,24],[16,25],[17,26],[18,27],[19,28],[20,29],[21,30],[22,31],[23,32],[24,33],[25,34],[26,35],[27,36],[28,37],[29,38],[30,39],[31,40],[32,41],[33,42],[34,43],[35,44],[36,45],[37,46],[38,47],[39,48],[40,49],[41,50],[42,51],[43,52],[44,53],[45,54],[46,55],[47,56],[48,57],[49,58],[50,59],[51,60],[52,61],[53,62],[54,63],[55,64],[56,65],[57,66],[58,67],[59,68],[60,69],[61,70],[62,71],[63,72],[64,73],[65,74],[66,75],[67,76],[68,77],[69,78],[70,79],[71,80],[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[9,10],[10,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[18,19],[19,20],[20,21],[21,22],[22,23],[23,24],[24,25],[25,26],[27,28],[28,29],[29,30],[30,31],[31,32],[32,33],[33,34],[34,35],[36,37],[37,38],[38,39],[39,40],[40,41],[41,42],[42,43],[43,44],[45,46],[46,47],[47,48],[48,49],[49,50],[50,51],[51,52],[52,53],[54,55],[55,56],[56,57],[57,58],[58,59],[59,60],[60,61],[61,62],[63,64],[64,65],[65,66],[66,67],[67,68],[68,69],[69,70],[70,71],[72,73],[73,74],[74,75],[75,76],[76,77],[77,78],[78,79],[79,80]],\n  \"edges_assignment\":[\"B\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"B\",\"B\",\"V\",\"M\",\"V\",\"M\",\"V\",\"M\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"]\n}"
  },
  {
    "path": "tests/files/fold/windmill-no-edges.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\": [\"foldedForm\"],\n\t\"vertices_coords\": [\n\t\t[0, 2], [2, 2], [2, 0], [2, 4], [2, 2], [1, 1], [3, 3], [4, 2], [1, 3], [3, 1], [2, 2], [2, 2]\n\t],\n\t\"faces_vertices\": [\n\t\t[0, 1, 9, 5], [0, 5, 4], [1, 2, 9], [2, 11, 6, 9], [3, 4, 5, 8],\n\t\t[3, 8, 10], [5, 9, 6, 8], [6, 7, 10, 8], [6, 11, 7]\n\t],\n\t\"faceOrders\": [\n\t\t[0, 1, 1], [7, 8, 1], [4, 6, 1], [0, 6, 1], [5, 7, -1], [0, 2, -1], [1, 4, -1],\n\t\t[4, 5, 1], [2, 3, 1], [3, 6, 1], [6, 7, 1], [3, 8, -1], [0, 4, -1], [3, 7, 1],\n\t\t[1, 6, 1], [4, 7, -1], [2, 6, 1], [0, 3, 1], [5, 6, 1], [6, 8, -1]\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/windmill-variations.fold",
    "content": "{\n\t\"frame_classes\":[\"creasePattern\"],\n\t\"vertices_coords\":[\n\t\t[0,0],[2,0],[4,0],[0,4],[0,2],[1,1],[2,2],[3,3],[4,4],[1,3],[1,2],[2,1],[3,1],[2,4],[4,2],[2,3],[3,2]\n\t],\n\t\"edges_vertices\":[\n\t\t[0,1],[1,2],[3,4],[4,0],[0,5],[5,6],[6,7],[7,8],[9,10],[10,5],[5,11],[11,12],[4,9],[9,13],[14,12],[12,1],[1,5],[5,4],[1,11],[11,6],[6,15],[15,13],[4,10],[10,6],[6,16],[16,14],[3,9],[9,6],[6,12],[12,2],[12,16],[16,7],[7,15],[15,9],[2,14],[14,8],[8,13],[13,3],[13,7],[7,14]\n\t],\n\t\"edges_assignment\":[\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"M\",\"F\",\"M\",\"F\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"F\",\"M\"\n\t],\n\t\"faces_vertices\":[\n\t\t[0,1,5],[0,5,4],[1,2,12],[1,12,11],[1,11,5],[2,14,12],[3,4,9],[3,9,13],[4,10,9],[4,5,10],[5,11,6],[5,6,10],[6,16,7],[6,7,15],[6,15,9],[6,9,10],[6,11,12],[6,12,16],[7,8,13],[7,13,15],[7,16,14],[7,14,8],[9,15,13],[12,14,16]\n\t],\n\t\"file_frames\":[\n\t\t{\n\t\t\t\"frame_parent\":0,\n\t\t\t\"frame_inherit\":true,\n\t\t\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"F\",\"F\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"M\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"F\",\"M\"]\n\t\t},\n\t\t{\n\t\t\t\"frame_parent\":0,\n\t\t\t\"frame_inherit\":true,\n\t\t\t\"edges_assignment\":[\"B\",\"B\",\"B\",\"B\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"F\",\"F\",\"F\",\"F\",\"F\",\"F\",\"M\",\"V\",\"V\",\"M\",\"F\",\"F\",\"F\",\"F\",\"V\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"F\",\"F\"]\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "tests/files/fold/windmill.fold",
    "content": "{\n\t\"file_spec\": 1.2,\n\t\"file_creator\": \"Rabbit Ear\",\n\t\"frame_classes\": [\"creasePattern\"],\n\t\"vertices_coords\": [\n\t\t[0, 0], [2, 0], [4, 0], [0, 4], [0, 2], [1, 1], [3, 3], [4, 4], [1, 3], [3, 1], [2, 4], [4, 2]\n\t],\n\t\"edges_vertices\": [\n\t\t[0, 1], [1, 2], [3, 4], [4, 0], [0, 5], [6, 7], [8, 5], [5, 9], [8, 10], [9, 1],\n\t\t[5, 4], [3, 8], [9, 2], [9, 6], [6, 8], [2, 11], [11, 7], [7, 10], [10, 3], [6, 11]\n\t],\n\t\"edges_assignment\": [\n\t\t\"B\",\"B\",\"B\",\"B\",\"V\",\"V\",\"V\",\"V\",\"M\",\"M\",\"M\",\"V\",\"V\",\"V\",\"V\",\"B\",\"B\",\"B\",\"B\",\"M\"\n\t],\n\t\"faces_vertices\": [\n\t\t[0, 1, 9, 5], [0, 5, 4], [1, 2, 9], [2, 11, 6, 9], [3, 4, 5, 8],\n\t\t[3, 8, 10], [5, 9, 6, 8], [6, 7, 10, 8], [6, 11, 7]\n\t],\n\t\"file_frames\": [{\n\t\t\"frame_classes\": [\"foldedForm\"],\n\t\t\"frame_parent\": 0,\n\t\t\"frame_inherit\": true,\n\t\t\"vertices_coords\": [\n\t\t\t[0, 2], [2, 2], [2, 0], [2, 4], [2, 2], [1, 1], [3, 3], [4, 2], [1, 3], [3, 1], [2, 2], [2, 2]\n\t\t],\n\t\t\"faceOrders\": [\n\t\t\t[0, 1, 1], [7, 8, 1], [4, 6, 1], [0, 6, 1], [5, 7, -1], [0, 2, -1], [1, 4, -1],\n\t\t\t[4, 5, 1], [2, 3, 1], [3, 6, 1], [6, 7, 1], [3, 8, -1], [0, 4, -1], [3, 7, 1],\n\t\t\t[1, 6, 1], [4, 7, -1], [2, 6, 1], [0, 3, 1], [5, 6, 1], [6, 8, -1]\n\t\t]\n\t}]\n}\n"
  },
  {
    "path": "tests/files/json/crane-faces-edges-overlap.json",
    "content": "[\n\t[0,1,4,6,7,8,9,12,14,16,18,21,27,29,31,39,40,41,46,47,50,51,52,58,59,60,61,62,63,66,67,68,69,70,71,72,73,74,75,78,84,85,88,97,102,104,108],\n\t[0,1,2,3,4,5,6,8,10,12,13,14,16,17,19,20,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,54,55,57,58,62,63,64,67,68,69,70,72,73,77,78,79,80,83,84,85,86,88,89,90,97,100,102,104,106,108,110],\n\t[8,9,12,18,22,55,58,59,60,61,62,63,66,84,85,88,97,102,104,108],\n\t[0,1,4,6,7,14,21,22,39,41,47,51,52,55,59,71,72,73,74,75],\n\t[0,1,4,6,8,12,14,16,27,29,31,39,40,41,46,47,50,51,52,58,59,60,61,62,63,66,67,68,69,70,71,72,73,74,75,78,84,85,88,97,102,104,108],\n\t[0,1,4,6,8,12,14,16,27,29,31,39,40,41,46,47,50,51,52,58,59,60,61,62,63,66,67,68,69,70,71,72,73,74,75,78,84,85,88,97,102,104,108],\n\t[8,10,12,13,17,22,23,24,25,26,28,30,55,57,58,62,63,64,79,80,83,84,85,86,88,89,90,97,100,102,104,106,108,110],\n\t[0,1,2,3,4,5,6,14,19,20,22,32,33,34,35,36,37,38,39,41,42,43,44,45,47,48,49,51,52,54,55,72,73,77],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[0,1,2,3,4,5,6,7,21,22,42,44,48,52,53,54,55,59,71,72,73,74,75,76,77,82],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[8,9,10,12,13,18,22,28,30,55,56,57,58,59,60,61,62,63,64,65,66,80,81,84,85,86,87,98,101,103,107],\n\t[0,1,4,6,8,12,14,16,27,29,31,39,40,41,46,47,50,51,52,58,62,63,67,68,69,70,72,73,78,84,85,88,97,102,104,108],\n\t[8,12,22,55,58,59,60,61,62,63,66,84,85,88,97,102,104,108],\n\t[0,1,4,6,14,22,39,41,47,51,52,55,59,71,72,73,74,75],\n\t[8,12,22,55,58,59,60,61,62,63,66,84,85,88,97,102,104,108],\n\t[0,1,4,6,14,22,39,41,47,51,52,55,59,71,72,73,74,75],\n\t[1,4,6,7,14,21,22,39,41,47,51,52,55,59,71,72,74,75],\n\t[9,12,18,22,55,58,59,60,61,62,66,84,85,88,97,102,104,108],\n\t[0,1,4,6,14,22,39,41,47,51,52,55,72,73],\n\t[8,12,22,55,58,62,63,84,85,88,97,102,104,108],\n\t[12,22,55,58,62,84,85,88,97,102,104,108],\n\t[1,4,6,14,22,39,41,47,51,52,55,72],\n\t[1,6,7,14,21,39,41,47,51,59,71,72,74,75],\n\t[9,12,18,59,60,61,62,66,84,88,97,102,104,108],\n\t[22,55,58,62,85,88,97,102,104,108],\n\t[22,55,58,62,85,88,97,102,104,108],\n\t[4,14,22,39,41,47,51,52,55,72],\n\t[4,14,22,39,41,47,51,52,55,72],\n\t[12,62,84,88,97,102,104,108],\n\t[1,6,14,39,41,47,51,72],\n\t[14,39,41,47,51,72],\n\t[62,88,97,102,104,108],\n\t[14,39,41,47,51,72],\n\t[62,88,97,102,104,108],\n\t[1,2,3,5,6,20,42,43,44,45,48,49,54,77],\n\t[10,12,13,28,30,57,64,80,84,86,90,100,106,110],\n\t[10,12,13,28,30,57,64,80,84,86,90,100,106,110],\n\t[1,2,3,5,6,20,42,43,44,45,48,49,54,77],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110],\n\t[90,100,106,110]\n]\n"
  },
  {
    "path": "tests/files/json/crane-faces-faces-overlap.json",
    "content": "[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],[0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],[0,1,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49],[0,1,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],[0,1,2,3,4,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],[0,1,2,4,5,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49],[0,1,3,4,5,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,15,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,14,24,26,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,2,4,5,6,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,18,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,18,19,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,18,19,20,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,18,19,20,21,23,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,4,5,6,16,17,18,19,20,21,22,24,25,27,30,32,33,36,37,38,41,44,46,48,49,51,52,53,54,55,56,57,58],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,27,30,32,33,36,37,38,41,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,28,29,31,34,35,39,40,42,43,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,30,32,33,36,37,38,41,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,29,31,34,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,31,34,35,39,40,42,43,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,32,33,36,37,38,41,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,34,35,39,40,42,43,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,33,36,37,38,41,44,46,48,49],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,36,37,38,41,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,35,39,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,39,40,42,43,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,37,38,41,44,46,48,49],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,38,41,44,46,48,49],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,41,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,40,42,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,42,43,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,44,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,43,45,47,50],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,45,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,46,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,47,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,48,49],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,50],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,49],[0,1,2,4,5,6,16,17,18,19,20,21,22,23,24,25,27,30,32,33,36,37,38,41,44,46,48],[0,1,3,4,5,7,8,9,10,11,12,13,14,15,24,26,28,29,31,34,35,39,40,42,43,45,47],[16,17,18,19,20,21,22,23,52,53,54,55,56,57,58],[16,17,18,19,20,21,22,23,51,53,54,55,56,57,58],[16,17,18,19,20,21,22,23,51,52,54,55,56,57,58],[16,17,18,19,20,21,22,23,51,52,53,55,56,57,58],[16,17,18,19,20,21,22,23,51,52,53,54,56,57,58],[16,17,18,19,20,21,22,23,51,52,53,54,55,57,58],[16,17,18,19,20,21,22,23,51,52,53,54,55,56,58],[16,17,18,19,20,21,22,23,51,52,53,54,55,56,57]]\n"
  },
  {
    "path": "tests/files/json/crane-layer-solver-no-depth.json",
    "content": "{\n\t\"root\":{\n\t\t\"29 35\":1,\"39 43\":2,\"4 43\":1,\"0 35\":1,\"35 43\":1,\"5 45\":2,\"40 45\":1,\"5 26\":1,\"30 36\":1,\"4 27\":2,\"4 46\":1,\"1 6\":1,\"38 44\":1,\"5 44\":2,\"1 7\":1,\"2 16\":2,\"5 25\":1,\"8 31\":2,\"8 13\":1,\"4 28\":2,\"0 4\":2,\"22 27\":1,\"23 25\":2,\"18 38\":2,\"17 32\":2,\"33 38\":2,\"25 38\":2,\"25 27\":1,\"27 37\":1,\"30 37\":1,\"13 34\":2,\"10 40\":2,\"14 26\":2,\"15 28\":1,\"11 39\":1,\"12 29\":1,\"3 9\":2,\"29 39\":1,\"11 12\":1,\"3 29\":1,\"9 12\":2,\"28 39\":1,\"11 15\":1,\"34 40\":2,\"10 13\":2,\"26 40\":2,\"10 14\":2,\"26 28\":1,\"14 15\":1,\"42 45\":2,\"1 47\":1,\"24 42\":2,\"5 24\":2,\"1 48\":1,\"24 41\":2,\"41 44\":2,\"1 24\":1,\"41 48\":2,\"33 49\":2,\"48 49\":2,\"33 41\":2,\"32 33\":2,\"6 49\":1,\"6 32\":1,\"42 47\":2,\"47 50\":2,\"34 42\":2,\"7 31\":1,\"34 50\":2,\"7 50\":1,\"31 34\":2,\"20 30\":1,\"2 30\":1,\"0 2\":1,\"0 3\":1,\"19 37\":1,\"37 46\":2,\"36 46\":1,\"0 36\":1,\"21 33\":2,\"16 20\":2,\"19 55\":2,\"22 58\":2,\"23 57\":1,\"18 56\":1,\"21 53\":1,\"17 52\":1,\"52 53\":2,\"17 21\":1,\"54 55\":1,\"19 20\":1,\"53 56\":2,\"18 21\":2,\"56 57\":1,\"18 23\":2,\"57 58\":1,\"22 23\":2,\"55 58\":2,\"19 22\":1,\"16 51\":2,\"20 54\":2,\"51 54\":1,\"3 35\":1,\"28 43\":1,\"0 43\":1,\"26 45\":2,\"24 45\":2,\"4 26\":2,\"0 30\":1,\"5 27\":1,\"27 46\":1,\"0 46\":1,\"6 48\":1,\"1 32\":1,\"25 44\":2,\"27 44\":2,\"24 44\":2,\"7 47\":1,\"1 31\":1,\"2 20\":2,\"13 31\":2,\"8 10\":1,\"23 27\":1,\"19 27\":1,\"18 25\":2,\"21 38\":2,\"21 32\":2,\"27 38\":2,\"32 38\":2,\"25 37\":1,\"2 37\":1,\"19 30\":1,\"10 34\":2,\"14 40\":2,\"15 26\":2,\"11 28\":1,\"12 39\":1,\"3 12\":2,\"26 39\":1,\"3 39\":1,\"9 11\":2,\"28 40\":2,\"31 40\":2,\"1 42\":1,\"31 42\":2,\"1 41\":1,\"32 41\":2,\"33 48\":2,\"6 41\":1,\"6 33\":1,\"7 42\":1,\"34 47\":2,\"7 34\":1,\"16 19\":2,\"20 51\":2,\"22 55\":2,\"16 55\":2,\"20 55\":2,\"23 58\":1,\"18 57\":1,\"21 56\":1,\"17 53\":1,\"52 56\":2,\"17 18\":1,\"55 57\":2,\"51 55\":1,\"56 58\":1,\"55 56\":2,\"0 29\":1,\"12 35\":1,\"29 43\":1,\"0 39\":1,\"3 43\":1,\"4 39\":1,\"11 43\":1,\"12 43\":1,\"26 43\":1,\"35 39\":1,\"4 11\":2,\"4 12\":2,\"4 35\":1,\"0 12\":2,\"11 35\":1,\"26 35\":1,\"28 35\":1,\"5 40\":2,\"5 42\":2,\"10 45\":2,\"14 45\":2,\"24 40\":2,\"28 45\":2,\"31 45\":2,\"34 45\":2,\"40 42\":1,\"5 14\":1,\"5 15\":1,\"5 35\":1,\"5 28\":1,\"24 26\":1,\"2 36\":1,\"19 36\":1,\"20 36\":1,\"30 46\":1,\"0 27\":2,\"4 19\":2,\"4 22\":2,\"4 23\":2,\"4 25\":2,\"19 46\":1,\"22 46\":1,\"23 46\":1,\"4 37\":1,\"4 36\":1,\"5 38\":2,\"18 44\":2,\"21 44\":2,\"24 38\":2,\"32 44\":2,\"33 44\":2,\"38 41\":1,\"5 18\":1,\"5 21\":1,\"5 41\":2,\"16 30\":1,\"0 16\":2,\"21 25\":2,\"5 23\":1,\"24 25\":1,\"1 8\":1,\"7 8\":1,\"8 34\":2,\"1 13\":1,\"7 13\":1,\"0 28\":2,\"12 28\":1,\"4 15\":2,\"2 4\":2,\"3 4\":2,\"0 11\":2,\"0 15\":2,\"4 16\":2,\"0 19\":2,\"0 22\":2,\"0 23\":2,\"0 25\":2,\"0 26\":2,\"4 29\":1,\"4 30\":1,\"0 37\":1,\"22 37\":1,\"23 38\":2,\"22 25\":2,\"1 17\":1,\"6 17\":1,\"17 33\":2,\"5 33\":2,\"24 33\":2,\"22 38\":2,\"24 27\":1,\"23 37\":1,\"1 34\":1,\"13 42\":2,\"13 50\":2,\"13 40\":2,\"10 26\":2,\"15 39\":1,\"14 28\":1,\"11 29\":1,\"9 29\":1,\"0 9\":2,\"3 11\":2,\"14 39\":1,\"1 40\":1,\"5 34\":2,\"24 34\":2,\"1 10\":1,\"7 10\":1,\"13 26\":2,\"10 31\":2,\"10 42\":2,\"13 45\":2,\"1 26\":1,\"15 40\":2,\"1 14\":1,\"7 14\":1,\"1 28\":1,\"24 28\":1,\"0 14\":2,\"1 15\":1,\"4 14\":2,\"7 15\":1,\"15 45\":2,\"1 45\":1,\"7 45\":1,\"14 42\":2,\"15 42\":2,\"26 42\":2,\"28 42\":2,\"8 47\":2,\"10 47\":2,\"13 47\":2,\"14 47\":2,\"15 47\":2,\"1 50\":1,\"7 24\":1,\"10 24\":2,\"13 24\":2,\"14 24\":2,\"15 24\":2,\"24 47\":2,\"1 5\":1,\"5 7\":2,\"5 10\":1,\"5 13\":1,\"18 24\":2,\"21 24\":2,\"23 24\":2,\"17 48\":2,\"1 49\":1,\"6 24\":1,\"18 41\":2,\"21 41\":2,\"23 41\":2,\"24 48\":2,\"1 44\":1,\"6 44\":1,\"23 44\":2,\"25 41\":2,\"27 41\":2,\"8 24\":2,\"17 24\":2,\"1 18\":1,\"1 21\":1,\"1 23\":1,\"1 25\":1,\"1 27\":1,\"5 48\":2,\"17 41\":2,\"18 48\":2,\"21 48\":2,\"23 48\":2,\"25 48\":2,\"27 48\":2,\"41 49\":2,\"5 49\":2,\"17 49\":2,\"21 49\":2,\"32 49\":2,\"24 49\":2,\"25 49\":2,\"27 49\":2,\"32 48\":2,\"1 33\":1,\"25 33\":2,\"27 33\":2,\"6 21\":1,\"5 47\":2,\"8 42\":2,\"26 47\":2,\"28 47\":2,\"42 50\":2,\"5 50\":2,\"8 50\":2,\"10 50\":2,\"14 50\":2,\"15 50\":2,\"24 50\":2,\"26 50\":2,\"28 50\":2,\"14 34\":2,\"15 34\":2,\"26 34\":2,\"28 34\":2,\"5 31\":2,\"14 31\":2,\"15 31\":2,\"31 50\":2,\"31 47\":2,\"2 19\":2,\"0 20\":2,\"2 22\":2,\"2 23\":2,\"3 14\":2,\"3 15\":2,\"20 37\":1,\"2 46\":1,\"20 46\":1,\"25 46\":1,\"36 37\":1,\"22 36\":1,\"23 36\":1,\"25 36\":1,\"27 36\":1,\"16 36\":1,\"18 33\":2,\"16 54\":2,\"19 54\":2,\"22 57\":2,\"19 58\":2,\"18 53\":1,\"23 56\":1,\"21 52\":1,\"17 56\":1,\"22 54\":2,\"4 20\":2,\"20 27\":1,\"23 53\":1,\"6 18\":1,\"18 32\":2,\"6 23\":1,\"18 58\":1,\"19 57\":2,\"1 22\":1,\"5 22\":1,\"6 22\":1,\"22 24\":2,\"22 41\":2,\"22 44\":2,\"1 19\":1,\"6 19\":1,\"7 26\":1,\"26 31\":2,\"5 39\":1,\"5 43\":1,\"1 4\":1,\"4 24\":2,\"4 33\":2,\"4 38\":2,\"4 41\":2,\"4 44\":2,\"4 48\":2,\"4 49\":2,\"16 46\":1,\"5 37\":1,\"5 46\":1,\"8 45\":2,\"4 34\":2,\"4 40\":2,\"4 42\":2,\"4 45\":2,\"4 47\":2,\"4 50\":2,\"0 41\":2,\"0 42\":2,\"0 44\":2,\"0 45\":2,\"0 47\":2,\"0 48\":2,\"0 49\":2,\"0 50\":2,\"23 49\":2,\"17 44\":2,\"17 38\":2,\"16 37\":1,\"15 35\":1,\"15 43\":1,\"9 35\":1,\"9 43\":1,\"9 39\":1,\"8 40\":2,\"7 28\":1,\"28 31\":2,\"0 1\":2,\"24 35\":1,\"24 43\":1,\"24 46\":1,\"1 43\":1,\"1 46\":1,\"18 49\":2,\"0 6\":2,\"4 6\":2,\"5 6\":2,\"6 25\":1,\"6 27\":1,\"0 7\":2,\"4 7\":2,\"1 2\":1,\"2 6\":2,\"2 25\":2,\"2 27\":2,\"2 41\":2,\"2 44\":2,\"2 48\":2,\"2 49\":2,\"1 3\":1,\"3 7\":2,\"3 26\":2,\"3 28\":2,\"3 42\":2,\"3 45\":2,\"3 47\":2,\"3 50\":2,\"18 52\":1,\"23 52\":1,\"20 57\":2,\"20 58\":2,\"21 57\":1,\"21 58\":1,\"17 57\":1,\"19 56\":2,\"20 56\":2,\"22 56\":2,\"22 48\":2,\"22 49\":2,\"16 58\":2,\"18 55\":1,\"21 55\":1,\"23 55\":1,\"19 51\":2,\"22 51\":2,\"40 43\":1,\"4 5\":2,\"37 44\":2,\"24 32\":2,\"24 31\":2,\"37 38\":2,\"25 32\":2,\"27 30\":1,\"39 40\":2,\"28 29\":1,\"19 23\":2,\"18 22\":1,\"18 19\":1,\"0 40\":2,\"39 45\":2,\"0 5\":2,\"38 46\":1,\"5 32\":2,\"23 33\":2,\"15 29\":1,\"0 38\":2,\"24 37\":1,\"13 14\":2,\"24 39\":1,\"33 37\":1,\"31 39\":1,\"22 30\":1,\"43 45\":2,\"42 43\":1,\"41 46\":1,\"0 24\":2,\"33 46\":1,\"44 46\":1,\"34 43\":1,\"24 36\":1,\"35 42\":2,\"5 36\":1,\"2 38\":2,\"30 41\":2,\"30 44\":2,\"26 29\":1,\"29 42\":2,\"29 45\":2,\"20 23\":2,\"21 22\":1,\"18 20\":1,\"19 21\":2,\"5 29\":1,\"16 27\":1,\"4 9\":2,\"14 43\":1,\"7 40\":1,\"1 39\":1,\"14 35\":1,\"1 11\":1,\"7 11\":1,\"11 47\":2,\"1 38\":1,\"6 38\":1,\"5 8\":1,\"5 17\":1,\"1 37\":1,\"44 48\":2,\"38 48\":2,\"37 48\":2,\"44 49\":2,\"38 49\":2,\"37 49\":2,\"45 47\":2,\"40 47\":2,\"39 47\":2,\"45 50\":2,\"11 50\":2,\"40 50\":2,\"39 50\":2,\"23 30\":1,\"14 29\":1,\"25 30\":1,\"23 32\":2,\"22 32\":2,\"19 48\":2,\"19 24\":2,\"1 20\":1,\"19 49\":2,\"19 32\":2,\"6 37\":1,\"6 20\":1,\"0 33\":2,\"46 48\":2,\"46 49\":2,\"5 30\":1,\"0 34\":2,\"43 47\":2,\"43 50\":2,\"35 47\":2,\"36 48\":2,\"36 49\":2,\"35 50\":2,\"17 25\":2,\"9 28\":1,\"8 26\":2,\"7 39\":1,\"1 35\":1,\"1 36\":1,\"24 29\":1,\"0 32\":2,\"6 36\":1,\"6 46\":1,\"4 32\":2,\"27 32\":2,\"7 35\":1,\"0 31\":2,\"7 43\":1,\"4 31\":2,\"1 16\":1,\"2 24\":2,\"1 30\":1,\"6 16\":1,\"2 32\":2,\"6 30\":1,\"2 5\":2,\"2 33\":2,\"30 48\":2,\"30 49\":2,\"1 9\":1,\"1 29\":1,\"3 24\":2,\"7 9\":1,\"7 29\":1,\"3 31\":2,\"3 5\":2,\"3 34\":2,\"3 40\":2,\"29 47\":2,\"29 50\":2,\"16 57\":2,\"17 58\":1,\"19 53\":2,\"16 56\":2,\"20 53\":2,\"22 53\":2,\"22 33\":2,\"18 54\":1,\"17 55\":1,\"21 54\":1,\"23 54\":1,\"31 35\":1,\"34 35\":1,\"35 40\":2,\"35 45\":2,\"31 43\":1,\"24 30\":1,\"30 32\":2,\"30 33\":2,\"30 38\":2,\"32 46\":1,\"36 44\":2,\"19 44\":2,\"2 18\":2,\"18 27\":1,\"21 27\":1,\"19 25\":2,\"20 25\":2,\"19 38\":2,\"36 38\":2,\"32 37\":1,\"37 41\":2,\"18 37\":1,\"18 30\":1,\"21 30\":1,\"29 40\":2,\"34 39\":1,\"39 42\":2,\"9 47\":2,\"9 50\":2,\"19 41\":2,\"16 18\":2,\"16 21\":2,\"16 23\":2,\"16 24\":2,\"16 25\":2,\"16 32\":2,\"16 38\":2,\"16 41\":2,\"16 44\":2,\"16 48\":2,\"16 49\":2,\"16 53\":2,\"17 22\":1,\"16 17\":2,\"17 20\":1,\"20 21\":2,\"17 19\":1,\"2 17\":2,\"17 27\":1,\"17 30\":1,\"17 37\":1,\"17 54\":1,\"29 31\":2,\"29 34\":2,\"17 36\":1,\"18 36\":1,\"32 36\":1,\"33 36\":1,\"36 41\":2,\"21 36\":1,\"17 46\":1,\"18 46\":1,\"21 46\":1,\"0 17\":2,\"0 18\":2,\"0 21\":2,\"4 17\":2,\"4 18\":2,\"4 21\":2,\"21 37\":1,\"20 44\":2,\"5 16\":1,\"5 19\":1,\"5 20\":1,\"20 41\":2,\"20 24\":2,\"2 21\":2,\"20 38\":2,\"16 33\":2,\"19 33\":2,\"20 33\":2,\"20 48\":2,\"20 49\":2,\"20 32\":2,\"17 23\":2,\"20 22\":1,\"12 15\":1,\"21 23\":2,\"8 14\":2,\"54 57\":2,\"53 58\":1,\"54 56\":2,\"53 55\":1,\"16 22\":1,\"9 15\":1,\"53 54\":1,\"1 12\":1,\"11 24\":2,\"7 12\":1,\"11 31\":2,\"11 42\":2,\"11 34\":2,\"9 24\":2,\"9 31\":2,\"12 47\":2,\"9 42\":2,\"12 50\":2,\"9 34\":2,\"52 57\":1,\"54 58\":2,\"53 57\":1,\"51 58\":2,\"12 24\":2,\"5 11\":1,\"12 31\":2,\"12 42\":2,\"11 45\":2,\"12 34\":2,\"5 9\":1,\"9 45\":2,\"52 54\":1,\"51 53\":2,\"11 40\":2,\"9 40\":2,\"20 52\":2,\"51 52\":2,\"21 51\":1,\"5 12\":1,\"11 26\":2,\"12 45\":2,\"9 26\":2,\"12 40\":2,\"16 52\":2,\"19 52\":2,\"22 52\":2,\"52 55\":1,\"52 58\":1,\"17 51\":1,\"18 51\":1,\"23 51\":1,\"51 56\":2,\"51 57\":2,\"11 14\":2,\"9 14\":2,\"12 26\":2,\"12 14\":2\n\t},\n\t\"branches\":[\n\t\t[\n\t\t\t{\n\t\t\t\t\"8 35\":2,\"4 8\":1,\"3 8\":1,\"8 9\":2,\"0 10\":1,\"8 11\":2,\"8 12\":2,\"0 13\":1,\"8 15\":2,\"8 28\":2,\"9 13\":1,\"8 39\":2,\"11 13\":1,\"8 29\":2,\"8 43\":2,\"10 35\":2,\"4 10\":1,\"3 10\":1,\"13 35\":2,\"4 13\":1,\"3 13\":1,\"9 10\":1,\"10 11\":2,\"10 12\":2,\"10 15\":2,\"10 28\":2,\"12 13\":1,\"13 15\":2,\"13 28\":2,\"13 39\":2,\"10 39\":2,\"10 29\":2,\"13 29\":2,\"10 43\":2,\"13 43\":2,\"0 8\":1\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"8 35\":1,\"4 8\":2,\"3 8\":2,\"8 29\":1,\"8 39\":1,\"8 43\":1,\"8 9\":1,\"8 12\":1,\"8 11\":1,\"8 28\":1,\"8 15\":1,\"0 8\":2,\"13 39\":1,\"4 13\":2,\"13 35\":1,\"10 43\":1,\"11 13\":2,\"13 28\":1,\"10 39\":1,\"0 13\":2,\"4 10\":2,\"13 29\":1,\"10 35\":1,\"3 13\":2,\"13 15\":1,\"12 13\":2,\"10 11\":1,\"9 13\":2,\"10 28\":1,\"0 10\":2,\"10 29\":1,\"3 10\":2,\"10 15\":1,\"10 12\":1,\"9 10\":2,\"13 43\":1\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"8 35\":1,\"4 8\":2,\"3 8\":2,\"8 29\":1,\"8 39\":1,\"8 43\":1,\"8 9\":1,\"8 12\":1,\"8 11\":1,\"8 28\":1,\"8 15\":1,\"0 8\":2,\"13 39\":2,\"4 13\":1,\"13 35\":2,\"10 43\":2,\"0 13\":1,\"3 13\":1,\"9 13\":1,\"11 13\":1,\"12 13\":1,\"13 15\":2,\"13 28\":2,\"13 29\":2,\"10 12\":2,\"10 15\":2,\"10 39\":2,\"4 10\":1,\"10 35\":2,\"0 10\":1,\"3 10\":1,\"10 28\":2,\"10 29\":2,\"9 10\":1,\"10 11\":2,\"13 43\":2\n\t\t\t}\n\t\t]\n\t]\n}\n"
  },
  {
    "path": "tests/files/json/crane-layer-solver.json",
    "content": "{\n\t\"orders\": { \"29 35\":1,\"39 43\":2,\"4 43\":1,\"0 35\":1,\"35 43\":1,\"5 45\":2,\"40 45\":1,\"5 26\":1,\"30 36\":1,\"4 27\":2,\"4 46\":1,\"1 6\":1,\"38 44\":1,\"5 44\":2,\"1 7\":1,\"2 16\":2,\"5 25\":1,\"8 31\":2,\"8 13\":1,\"4 28\":2,\"0 4\":2,\"22 27\":1,\"23 25\":2,\"18 38\":2,\"17 32\":2,\"33 38\":2,\"25 38\":2,\"25 27\":1,\"27 37\":1,\"30 37\":1,\"13 34\":2,\"10 40\":2,\"14 26\":2,\"15 28\":1,\"11 39\":1,\"12 29\":1,\"3 9\":2,\"29 39\":1,\"11 12\":1,\"3 29\":1,\"9 12\":2,\"28 39\":1,\"11 15\":1,\"34 40\":2,\"10 13\":2,\"26 40\":2,\"10 14\":2,\"26 28\":1,\"14 15\":1,\"42 45\":2,\"1 47\":1,\"24 42\":2,\"5 24\":2,\"1 48\":1,\"24 41\":2,\"41 44\":2,\"1 24\":1,\"41 48\":2,\"33 49\":2,\"48 49\":2,\"33 41\":2,\"32 33\":2,\"6 49\":1,\"6 32\":1,\"42 47\":2,\"47 50\":2,\"34 42\":2,\"7 31\":1,\"34 50\":2,\"7 50\":1,\"31 34\":2,\"20 30\":1,\"2 30\":1,\"0 2\":1,\"0 3\":1,\"19 37\":1,\"37 46\":2,\"36 46\":1,\"0 36\":1,\"21 33\":2,\"16 20\":2,\"19 55\":2,\"22 58\":2,\"23 57\":1,\"18 56\":1,\"21 53\":1,\"17 52\":1,\"52 53\":2,\"17 21\":1,\"54 55\":1,\"19 20\":1,\"53 56\":2,\"18 21\":2,\"56 57\":1,\"18 23\":2,\"57 58\":1,\"22 23\":2,\"55 58\":2,\"19 22\":1,\"16 51\":2,\"20 54\":2,\"51 54\":1,\"3 35\":1,\"28 43\":1,\"0 43\":1,\"26 45\":2,\"24 45\":2,\"4 26\":2,\"0 30\":1,\"5 27\":1,\"27 46\":1,\"0 46\":1,\"6 48\":1,\"1 32\":1,\"25 44\":2,\"27 44\":2,\"24 44\":2,\"7 47\":1,\"1 31\":1,\"2 20\":2,\"13 31\":2,\"8 10\":1,\"23 27\":1,\"19 27\":1,\"18 25\":2,\"21 38\":2,\"21 32\":2,\"27 38\":2,\"32 38\":2,\"25 37\":1,\"2 37\":1,\"19 30\":1,\"10 34\":2,\"14 40\":2,\"15 26\":2,\"11 28\":1,\"12 39\":1,\"3 12\":2,\"26 39\":1,\"3 39\":1,\"9 11\":2,\"28 40\":2,\"31 40\":2,\"1 42\":1,\"31 42\":2,\"1 41\":1,\"32 41\":2,\"33 48\":2,\"6 41\":1,\"6 33\":1,\"7 42\":1,\"34 47\":2,\"7 34\":1,\"16 19\":2,\"20 51\":2,\"22 55\":2,\"16 55\":2,\"20 55\":2,\"23 58\":1,\"18 57\":1,\"21 56\":1,\"17 53\":1,\"52 56\":2,\"17 18\":1,\"55 57\":2,\"51 55\":1,\"56 58\":1,\"55 56\":2,\"0 29\":1,\"12 35\":1,\"29 43\":1,\"0 39\":1,\"3 43\":1,\"4 39\":1,\"11 43\":1,\"12 43\":1,\"26 43\":1,\"35 39\":1,\"4 11\":2,\"4 12\":2,\"4 35\":1,\"0 12\":2,\"11 35\":1,\"26 35\":1,\"28 35\":1,\"5 40\":2,\"5 42\":2,\"10 45\":2,\"14 45\":2,\"24 40\":2,\"28 45\":2,\"31 45\":2,\"34 45\":2,\"40 42\":1,\"5 14\":1,\"5 15\":1,\"5 35\":1,\"5 28\":1,\"24 26\":1,\"2 36\":1,\"19 36\":1,\"20 36\":1,\"30 46\":1,\"0 27\":2,\"4 19\":2,\"4 22\":2,\"4 23\":2,\"4 25\":2,\"19 46\":1,\"22 46\":1,\"23 46\":1,\"4 37\":1,\"4 36\":1,\"5 38\":2,\"18 44\":2,\"21 44\":2,\"24 38\":2,\"32 44\":2,\"33 44\":2,\"38 41\":1,\"5 18\":1,\"5 21\":1,\"5 41\":2,\"16 30\":1,\"0 16\":2,\"21 25\":2,\"5 23\":1,\"24 25\":1,\"1 8\":1,\"7 8\":1,\"8 34\":2,\"1 13\":1,\"7 13\":1,\"0 28\":2,\"12 28\":1,\"4 15\":2,\"2 4\":2,\"3 4\":2,\"0 11\":2,\"0 15\":2,\"4 16\":2,\"0 19\":2,\"0 22\":2,\"0 23\":2,\"0 25\":2,\"0 26\":2,\"4 29\":1,\"4 30\":1,\"0 37\":1,\"22 37\":1,\"23 38\":2,\"22 25\":2,\"1 17\":1,\"6 17\":1,\"17 33\":2,\"5 33\":2,\"24 33\":2,\"22 38\":2,\"24 27\":1,\"23 37\":1,\"1 34\":1,\"13 42\":2,\"13 50\":2,\"13 40\":2,\"10 26\":2,\"15 39\":1,\"14 28\":1,\"11 29\":1,\"9 29\":1,\"0 9\":2,\"3 11\":2,\"14 39\":1,\"1 40\":1,\"5 34\":2,\"24 34\":2,\"1 10\":1,\"7 10\":1,\"13 26\":2,\"10 31\":2,\"10 42\":2,\"13 45\":2,\"1 26\":1,\"15 40\":2,\"1 14\":1,\"7 14\":1,\"1 28\":1,\"24 28\":1,\"0 14\":2,\"1 15\":1,\"4 14\":2,\"7 15\":1,\"15 45\":2,\"1 45\":1,\"7 45\":1,\"14 42\":2,\"15 42\":2,\"26 42\":2,\"28 42\":2,\"8 47\":2,\"10 47\":2,\"13 47\":2,\"14 47\":2,\"15 47\":2,\"1 50\":1,\"7 24\":1,\"10 24\":2,\"13 24\":2,\"14 24\":2,\"15 24\":2,\"24 47\":2,\"1 5\":1,\"5 7\":2,\"5 10\":1,\"5 13\":1,\"18 24\":2,\"21 24\":2,\"23 24\":2,\"17 48\":2,\"1 49\":1,\"6 24\":1,\"18 41\":2,\"21 41\":2,\"23 41\":2,\"24 48\":2,\"1 44\":1,\"6 44\":1,\"23 44\":2,\"25 41\":2,\"27 41\":2,\"8 24\":2,\"17 24\":2,\"1 18\":1,\"1 21\":1,\"1 23\":1,\"1 25\":1,\"1 27\":1,\"5 48\":2,\"17 41\":2,\"18 48\":2,\"21 48\":2,\"23 48\":2,\"25 48\":2,\"27 48\":2,\"41 49\":2,\"5 49\":2,\"17 49\":2,\"21 49\":2,\"32 49\":2,\"24 49\":2,\"25 49\":2,\"27 49\":2,\"32 48\":2,\"1 33\":1,\"25 33\":2,\"27 33\":2,\"6 21\":1,\"5 47\":2,\"8 42\":2,\"26 47\":2,\"28 47\":2,\"42 50\":2,\"5 50\":2,\"8 50\":2,\"10 50\":2,\"14 50\":2,\"15 50\":2,\"24 50\":2,\"26 50\":2,\"28 50\":2,\"14 34\":2,\"15 34\":2,\"26 34\":2,\"28 34\":2,\"5 31\":2,\"14 31\":2,\"15 31\":2,\"31 50\":2,\"31 47\":2,\"2 19\":2,\"0 20\":2,\"2 22\":2,\"2 23\":2,\"3 14\":2,\"3 15\":2,\"20 37\":1,\"2 46\":1,\"20 46\":1,\"25 46\":1,\"36 37\":1,\"22 36\":1,\"23 36\":1,\"25 36\":1,\"27 36\":1,\"16 36\":1,\"18 33\":2,\"16 54\":2,\"19 54\":2,\"22 57\":2,\"19 58\":2,\"18 53\":1,\"23 56\":1,\"21 52\":1,\"17 56\":1,\"22 54\":2,\"4 20\":2,\"20 27\":1,\"23 53\":1,\"6 18\":1,\"18 32\":2,\"6 23\":1,\"18 58\":1,\"19 57\":2,\"1 22\":1,\"5 22\":1,\"6 22\":1,\"22 24\":2,\"22 41\":2,\"22 44\":2,\"1 19\":1,\"6 19\":1,\"7 26\":1,\"26 31\":2,\"5 39\":1,\"5 43\":1,\"1 4\":1,\"4 24\":2,\"4 33\":2,\"4 38\":2,\"4 41\":2,\"4 44\":2,\"4 48\":2,\"4 49\":2,\"16 46\":1,\"5 37\":1,\"5 46\":1,\"8 45\":2,\"4 34\":2,\"4 40\":2,\"4 42\":2,\"4 45\":2,\"4 47\":2,\"4 50\":2,\"0 41\":2,\"0 42\":2,\"0 44\":2,\"0 45\":2,\"0 47\":2,\"0 48\":2,\"0 49\":2,\"0 50\":2,\"23 49\":2,\"17 44\":2,\"17 38\":2,\"16 37\":1,\"15 35\":1,\"15 43\":1,\"9 35\":1,\"9 43\":1,\"9 39\":1,\"8 40\":2,\"7 28\":1,\"28 31\":2,\"0 1\":2,\"24 35\":1,\"24 43\":1,\"24 46\":1,\"1 43\":1,\"1 46\":1,\"18 49\":2,\"0 6\":2,\"4 6\":2,\"5 6\":2,\"6 25\":1,\"6 27\":1,\"0 7\":2,\"4 7\":2,\"1 2\":1,\"2 6\":2,\"2 25\":2,\"2 27\":2,\"2 41\":2,\"2 44\":2,\"2 48\":2,\"2 49\":2,\"1 3\":1,\"3 7\":2,\"3 26\":2,\"3 28\":2,\"3 42\":2,\"3 45\":2,\"3 47\":2,\"3 50\":2,\"18 52\":1,\"23 52\":1,\"20 57\":2,\"20 58\":2,\"21 57\":1,\"21 58\":1,\"17 57\":1,\"19 56\":2,\"20 56\":2,\"22 56\":2,\"22 48\":2,\"22 49\":2,\"16 58\":2,\"18 55\":1,\"21 55\":1,\"23 55\":1,\"19 51\":2,\"22 51\":2,\"40 43\":1,\"4 5\":2,\"37 44\":2,\"24 32\":2,\"24 31\":2,\"37 38\":2,\"25 32\":2,\"27 30\":1,\"39 40\":2,\"28 29\":1,\"19 23\":2,\"18 22\":1,\"18 19\":1,\"0 40\":2,\"39 45\":2,\"0 5\":2,\"38 46\":1,\"5 32\":2,\"23 33\":2,\"15 29\":1,\"0 38\":2,\"24 37\":1,\"13 14\":2,\"24 39\":1,\"33 37\":1,\"31 39\":1,\"22 30\":1,\"43 45\":2,\"42 43\":1,\"41 46\":1,\"0 24\":2,\"33 46\":1,\"44 46\":1,\"34 43\":1,\"24 36\":1,\"35 42\":2,\"5 36\":1,\"2 38\":2,\"30 41\":2,\"30 44\":2,\"26 29\":1,\"29 42\":2,\"29 45\":2,\"20 23\":2,\"21 22\":1,\"18 20\":1,\"19 21\":2,\"5 29\":1,\"16 27\":1,\"4 9\":2,\"14 43\":1,\"7 40\":1,\"1 39\":1,\"14 35\":1,\"1 11\":1,\"7 11\":1,\"11 47\":2,\"1 38\":1,\"6 38\":1,\"5 8\":1,\"5 17\":1,\"1 37\":1,\"44 48\":2,\"38 48\":2,\"37 48\":2,\"44 49\":2,\"38 49\":2,\"37 49\":2,\"45 47\":2,\"40 47\":2,\"39 47\":2,\"45 50\":2,\"11 50\":2,\"40 50\":2,\"39 50\":2,\"23 30\":1,\"14 29\":1,\"25 30\":1,\"23 32\":2,\"22 32\":2,\"19 48\":2,\"19 24\":2,\"1 20\":1,\"19 49\":2,\"19 32\":2,\"6 37\":1,\"6 20\":1,\"0 33\":2,\"46 48\":2,\"46 49\":2,\"5 30\":1,\"0 34\":2,\"43 47\":2,\"43 50\":2,\"35 47\":2,\"36 48\":2,\"36 49\":2,\"35 50\":2,\"17 25\":2,\"9 28\":1,\"8 26\":2,\"7 39\":1,\"1 35\":1,\"1 36\":1,\"24 29\":1,\"0 32\":2,\"6 36\":1,\"6 46\":1,\"4 32\":2,\"27 32\":2,\"7 35\":1,\"0 31\":2,\"7 43\":1,\"4 31\":2,\"1 16\":1,\"2 24\":2,\"1 30\":1,\"6 16\":1,\"2 32\":2,\"6 30\":1,\"2 5\":2,\"2 33\":2,\"30 48\":2,\"30 49\":2,\"1 9\":1,\"1 29\":1,\"3 24\":2,\"7 9\":1,\"7 29\":1,\"3 31\":2,\"3 5\":2,\"3 34\":2,\"3 40\":2,\"29 47\":2,\"29 50\":2,\"16 57\":2,\"17 58\":1,\"19 53\":2,\"16 56\":2,\"20 53\":2,\"22 53\":2,\"22 33\":2,\"18 54\":1,\"17 55\":1,\"21 54\":1,\"23 54\":1,\"31 35\":1,\"34 35\":1,\"35 40\":2,\"35 45\":2,\"31 43\":1,\"24 30\":1,\"30 32\":2,\"30 33\":2,\"30 38\":2,\"32 46\":1,\"36 44\":2,\"19 44\":2,\"2 18\":2,\"18 27\":1,\"21 27\":1,\"19 25\":2,\"20 25\":2,\"19 38\":2,\"36 38\":2,\"32 37\":1,\"37 41\":2,\"18 37\":1,\"18 30\":1,\"21 30\":1,\"29 40\":2,\"34 39\":1,\"39 42\":2,\"9 47\":2,\"9 50\":2,\"19 41\":2,\"16 18\":2,\"16 21\":2,\"16 23\":2,\"16 24\":2,\"16 25\":2,\"16 32\":2,\"16 38\":2,\"16 41\":2,\"16 44\":2,\"16 48\":2,\"16 49\":2,\"16 53\":2,\"17 22\":1,\"16 17\":2,\"17 20\":1,\"20 21\":2,\"17 19\":1,\"2 17\":2,\"17 27\":1,\"17 30\":1,\"17 37\":1,\"17 54\":1,\"29 31\":2,\"29 34\":2,\"17 36\":1,\"18 36\":1,\"32 36\":1,\"33 36\":1,\"36 41\":2,\"21 36\":1,\"17 46\":1,\"18 46\":1,\"21 46\":1,\"0 17\":2,\"0 18\":2,\"0 21\":2,\"4 17\":2,\"4 18\":2,\"4 21\":2,\"21 37\":1,\"20 44\":2,\"5 16\":1,\"5 19\":1,\"5 20\":1,\"20 41\":2,\"20 24\":2,\"2 21\":2,\"20 38\":2,\"16 33\":2,\"19 33\":2,\"20 33\":2,\"20 48\":2,\"20 49\":2,\"20 32\":2,\"17 23\":2,\"20 22\":1,\"12 15\":1,\"21 23\":2,\"8 14\":2,\"54 57\":2,\"53 58\":1,\"54 56\":2,\"53 55\":1,\"16 22\":1,\"9 15\":1,\"53 54\":1,\"1 12\":1,\"11 24\":2,\"7 12\":1,\"11 31\":2,\"11 42\":2,\"11 34\":2,\"9 24\":2,\"9 31\":2,\"12 47\":2,\"9 42\":2,\"12 50\":2,\"9 34\":2,\"52 57\":1,\"54 58\":2,\"53 57\":1,\"51 58\":2,\"12 24\":2,\"5 11\":1,\"12 31\":2,\"12 42\":2,\"11 45\":2,\"12 34\":2,\"5 9\":1,\"9 45\":2,\"52 54\":1,\"51 53\":2,\"11 40\":2,\"9 40\":2,\"20 52\":2,\"51 52\":2,\"21 51\":1,\"5 12\":1,\"11 26\":2,\"12 45\":2,\"9 26\":2,\"12 40\":2,\"16 52\":2,\"19 52\":2,\"22 52\":2,\"52 55\":1,\"52 58\":1,\"17 51\":1,\"18 51\":1,\"23 51\":1,\"51 56\":2,\"51 57\":2,\"11 14\":2,\"9 14\":2,\"12 26\":2,\"12 14\":2 },\n\t\"branches\": [\n\t\t[\n\t\t\t{\n\t\t\t\t\"orders\": { \"8 35\":2,\"4 8\":1,\"3 8\":1,\"8 9\":2,\"0 10\":1,\"8 11\":2,\"8 12\":2,\"0 13\":1,\"8 15\":2,\"8 28\":2,\"9 13\":1,\"8 39\":2,\"11 13\":1,\"8 29\":2,\"8 43\":2,\"10 35\":2,\"4 10\":1,\"3 10\":1,\"13 35\":2,\"4 13\":1,\"3 13\":1,\"9 10\":1,\"10 11\":2,\"10 12\":2,\"10 15\":2,\"10 28\":2,\"12 13\":1,\"13 15\":2,\"13 28\":2,\"13 39\":2,\"10 39\":2,\"10 29\":2,\"13 29\":2,\"10 43\":2,\"13 43\":2,\"0 8\":1 }\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"orders\": { \"8 35\":1,\"4 8\":2,\"3 8\":2,\"8 29\":1,\"8 39\":1,\"8 43\":1,\"8 9\":1,\"8 12\":1,\"8 11\":1,\"8 28\":1,\"8 15\":1,\"0 8\":2 },\n\t\t\t\t\"branches\": [\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"13 39\":1,\"4 13\":2,\"13 35\":1,\"10 43\":1,\"11 13\":2,\"13 28\":1,\"10 39\":1,\"0 13\":2,\"4 10\":2,\"13 29\":1,\"10 35\":1,\"3 13\":2,\"13 15\":1,\"12 13\":2,\"10 11\":1,\"9 13\":2,\"10 28\":1,\"0 10\":2,\"10 29\":1,\"3 10\":2,\"10 15\":1,\"10 12\":1,\"9 10\":2,\"13 43\":1 }\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"13 39\":2,\"4 13\":1,\"13 35\":2,\"10 43\":2,\"0 13\":1,\"3 13\":1,\"9 13\":1,\"11 13\":1,\"12 13\":1,\"13 15\":2,\"13 28\":2,\"13 29\":2,\"10 12\":2,\"10 15\":2,\"10 39\":2,\"4 10\":1,\"10 35\":2,\"0 10\":1,\"3 10\":1,\"10 28\":2,\"10 29\":2,\"9 10\":1,\"10 11\":2,\"13 43\":2 }\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t]\n}\n"
  },
  {
    "path": "tests/files/json/cube-octagon-constraints.json",
    "content": "{\n\t\"constraints\":{\n\t\t\"taco_taco\":[\n\t\t\t[30,0,31,1],[18,22,19,23],[16,15,33,17],[24,25,34,36],[15,17,16,33],[15,2,16,4],[17,2,33,4],[12,37,13,39],[12,38,13,48],[37,38,39,48],[44,8,45,43],[44,5,45,6],[8,5,43,6],[6,5,43,8],[39,37,48,38],[20,24,21,25],[20,34,21,36],[24,34,25,36],[40,10,41,11],[7,42,9,46]\n\t\t],\n\t\t\"taco_tortilla\":[\n\t\t\t[16,2,33],[16,4,33],[15,2,17],[15,4,17],[24,20,34],[24,21,34],[25,20,36],[25,21,36],[6,44,43],[6,45,43],[5,44,8],[5,45,8],[39,12,48],[39,13,48],[37,12,38],[37,13,38]\n\t\t],\n\t\t\"tortilla_tortilla\":[\n\t\t\t[11,37,40,13],[11,37,10,12],[11,37,41,39],[13,40,12,10],[13,40,39,41],[30,33,0,2],[30,33,1,17],[30,33,31,4],[0,2,1,17],[0,2,31,4],[1,17,31,4],[42,44,9,43],[42,45,46,43],[42,8,7,43],[9,45,46,44],[9,8,7,44],[18,24,23,20],[18,21,22,20],[18,25,19,20],[23,21,22,24],[23,25,19,24],[45,7,8,46],[10,12,41,39],[21,19,25,22]\n\t\t],\n\t\t\"transitivity\":[\n\t\t\t[2,15,33],[2,16,17],[4,15,33],[4,16,17],[5,43,44],[5,43,45],[6,8,44],[6,8,45],[12,37,48],[12,38,39],[13,37,48],[13,38,39],[20,24,36],[20,25,34],[21,24,36],[21,25,34]\n\t\t]\n\t},\n\t\"lookup\":{\n\t\t\"taco_taco\":{\n\t\t\t\"30 31\":[0],\"0 1\":[0],\"0 31\":[0],\"1 30\":[0],\"0 30\":[0],\"1 31\":[0],\"18 19\":[1],\"22 23\":[1],\"19 22\":[1],\"18 23\":[1],\"18 22\":[1],\"19 23\":[1],\"16 33\":[2,4],\"15 17\":[2,4],\"15 33\":[2,4],\"16 17\":[2,4],\"15 16\":[2,4,5],\"17 33\":[2,4,6],\"24 34\":[3,17],\"25 36\":[3,17],\"25 34\":[3,17],\"24 36\":[3,17],\"24 25\":[3,15,17],\"34 36\":[3,16,17],\"2 4\":[5,6],\"2 16\":[5],\"4 15\":[5],\"2 15\":[5],\"4 16\":[5],\"2 33\":[6],\"4 17\":[6],\"2 17\":[6],\"4 33\":[6],\"12 13\":[7,8],\"37 39\":[7,9,14],\"13 37\":[7],\"12 39\":[7],\"12 37\":[7],\"13 39\":[7],\"38 48\":[8,9,14],\"13 38\":[8],\"12 48\":[8],\"12 38\":[8],\"13 48\":[8],\"38 39\":[9,14],\"37 48\":[9,14],\"37 38\":[9,14],\"39 48\":[9,14],\"44 45\":[10,11],\"8 43\":[10,12,13],\"8 45\":[10],\"43 44\":[10],\"8 44\":[10],\"43 45\":[10],\"5 6\":[11,12,13],\"5 45\":[11],\"6 44\":[11],\"5 44\":[11],\"6 45\":[11],\"5 43\":[12,13],\"6 8\":[12,13],\"5 8\":[12,13],\"6 43\":[12,13],\"20 21\":[15,16],\"21 24\":[15],\"20 25\":[15],\"20 24\":[15],\"21 25\":[15],\"21 34\":[16],\"20 36\":[16],\"20 34\":[16],\"21 36\":[16],\"40 41\":[18],\"10 11\":[18],\"10 41\":[18],\"11 40\":[18],\"10 40\":[18],\"11 41\":[18],\"7 9\":[19],\"42 46\":[19],\"9 42\":[19],\"7 46\":[19],\"7 42\":[19],\"9 46\":[19]\n\t\t},\n\t\t\"taco_tortilla\":{\n\t\t\t\"16 33\":[0,1],\"2 16\":[0],\"2 33\":[0],\"4 16\":[1],\"4 33\":[1],\"15 17\":[2,3],\"2 15\":[2],\"2 17\":[2],\"4 15\":[3],\"4 17\":[3],\"24 34\":[4,5],\"20 24\":[4],\"20 34\":[4],\"21 24\":[5],\"21 34\":[5],\"25 36\":[6,7],\"20 25\":[6],\"20 36\":[6],\"21 25\":[7],\"21 36\":[7],\"6 43\":[8,9],\"6 44\":[8],\"43 44\":[8],\"6 45\":[9],\"43 45\":[9],\"5 8\":[10,11],\"5 44\":[10],\"8 44\":[10],\"5 45\":[11],\"8 45\":[11],\"39 48\":[12,13],\"12 39\":[12],\"12 48\":[12],\"13 39\":[13],\"13 48\":[13],\"37 38\":[14,15],\"12 37\":[14],\"12 38\":[14],\"13 37\":[15],\"13 38\":[15]\n\t\t},\n\t\t\"tortilla_tortilla\":{\n\t\t\t\"11 40\":[0],\"13 37\":[0],\"10 11\":[1],\"12 37\":[1],\"11 41\":[2],\"37 39\":[2],\"12 13\":[3],\"10 40\":[3],\"13 39\":[4],\"40 41\":[4],\"0 30\":[5],\"2 33\":[5],\"1 30\":[6],\"17 33\":[6],\"30 31\":[7],\"4 33\":[7],\"0 1\":[8],\"2 17\":[8],\"0 31\":[9],\"2 4\":[9],\"1 31\":[10],\"4 17\":[10],\"9 42\":[11],\"43 44\":[11],\"42 46\":[12],\"43 45\":[12],\"7 42\":[13],\"8 43\":[13],\"9 46\":[14],\"44 45\":[14],\"7 9\":[15],\"8 44\":[15],\"18 23\":[16],\"20 24\":[16],\"18 22\":[17],\"20 21\":[17],\"18 19\":[18],\"20 25\":[18],\"22 23\":[19],\"21 24\":[19],\"19 23\":[20],\"24 25\":[20],\"8 45\":[21],\"7 46\":[21],\"10 41\":[22],\"12 39\":[22],\"21 25\":[23],\"19 22\":[23]\n\t\t},\n\t\t\"transitivity\":{\n\t\t\t\"2 15\":[0],\"15 33\":[0,2],\"2 33\":[0],\"2 16\":[1],\"16 17\":[1,3],\"2 17\":[1],\"4 15\":[2],\"4 33\":[2],\"4 16\":[3],\"4 17\":[3],\"5 43\":[4,5],\"43 44\":[4],\"5 44\":[4],\"43 45\":[5],\"5 45\":[5],\"6 8\":[6,7],\"8 44\":[6],\"6 44\":[6],\"8 45\":[7],\"6 45\":[7],\"12 37\":[8],\"37 48\":[8,10],\"12 48\":[8],\"12 38\":[9],\"38 39\":[9,11],\"12 39\":[9],\"13 37\":[10],\"13 48\":[10],\"13 38\":[11],\"13 39\":[11],\"20 24\":[12],\"24 36\":[12,14],\"20 36\":[12],\"20 25\":[13],\"25 34\":[13,15],\"20 34\":[13],\"21 24\":[14],\"21 36\":[14],\"21 25\":[15],\"21 34\":[15]\n\t\t}\n\t},\n\t\"facePairs\":[\n\t\t\"0 1\",\"0 30\",\"0 31\",\"1 30\",\"1 31\",\"2 15\",\"2 16\",\"2 4\",\"2 17\",\"2 33\",\"4 15\",\"4 16\",\"4 17\",\"4 33\",\"5 6\",\"5 8\",\"5 43\",\"5 44\",\"5 45\",\"6 8\",\"6 43\",\"6 44\",\"6 45\",\"7 9\",\"7 42\",\"7 46\",\"8 43\",\"8 44\",\"8 45\",\"9 42\",\"9 46\",\"10 11\",\"10 40\",\"10 41\",\"11 40\",\"11 41\",\"12 13\",\"12 37\",\"12 38\",\"12 39\",\"12 48\",\"13 37\",\"13 38\",\"13 39\",\"13 48\",\"15 16\",\"15 17\",\"15 33\",\"16 17\",\"16 33\",\"17 33\",\"18 22\",\"18 23\",\"18 19\",\"19 22\",\"19 23\",\"20 24\",\"20 34\",\"20 25\",\"20 36\",\"20 21\",\"21 24\",\"21 34\",\"21 25\",\"21 36\",\"22 23\",\"24 34\",\"24 25\",\"24 36\",\"25 34\",\"25 36\",\"30 31\",\"34 36\",\"37 38\",\"37 39\",\"37 48\",\"38 39\",\"38 48\",\"39 48\",\"40 41\",\"42 46\",\"43 44\",\"43 45\",\"44 45\"\n\t],\n\t\"faces_winding\":[\n\t\ttrue,false,true,false,false,true,false,true,false,false,true,false,true,false,false,true,false,false,false,true,true,false,true,false,true,false,true,false,true,false,true,false,false,true,false,false,true,false,true,true,false,true,false,true,true,false,true,false,false\n\t],\n\t\"orders\":{\n\t\t\"30 31\":1,\"0 1\":1,\"18 19\":2,\"22 23\":1,\"16 33\":1,\"15 17\":2,\"24 34\":2,\"25 36\":1,\"15 16\":2,\"17 33\":2,\"2 4\":2,\"12 13\":2,\"37 39\":2,\"38 48\":2,\"44 45\":2,\"8 43\":2,\"5 6\":2,\"6 43\":1,\"5 8\":2,\"39 48\":2,\"37 38\":1,\"20 21\":2,\"24 25\":1,\"34 36\":1,\"40 41\":2,\"10 11\":1,\"7 9\":1,\"42 46\":2\n\t}\n}\n"
  },
  {
    "path": "tests/files/json/kabuto-constraints.json",
    "content": "{\n\t\"taco_taco\":[\n\t\t[0,1,8,10],[0,1,9,11],[10,0,12,3],[10,1,12,2],[10,6,12,8],[10,4,12,5],[0,11,3,13],[0,1,3,2],[0,6,3,8],[0,7,3,9],[0,4,3,5],[11,1,13,2],[11,7,13,9],[11,4,13,5],[1,6,2,8],[1,7,2,9],[1,4,2,5],[6,12,16,14],[7,13,17,15],[6,4,8,5],[7,4,9,5]\n\t],\n\t\"taco_tortilla\":[\n\t\t[0,14,8],[0,16,8],[0,15,9],[0,17,9],[6,0,16],[6,1,16],[6,2,16],[6,3,16],[6,4,16],[6,5,16],[6,8,16],[6,10,16],[7,0,17],[7,1,17],[7,2,17],[7,3,17],[7,4,17],[7,5,17],[7,9,17],[7,11,17],[3,0,4],[3,1,4],[3,2,4],[3,5,4],[3,6,4],[3,7,4],[3,8,4],[3,9,4],[3,10,4],[3,11,4],[3,12,4],[3,13,4],[3,14,4],[3,15,4],[3,16,4],[3,17,4],[1,15,11],[1,17,11],[1,14,10],[1,16,10],[14,0,16],[14,1,16],[14,2,16],[14,3,16],[14,4,16],[14,5,16],[14,6,16],[14,8,16],[14,10,16],[14,12,16],[8,0,10],[8,1,10],[8,2,10],[8,3,10],[8,4,10],[8,5,10],[9,0,11],[9,1,11],[9,2,11],[9,3,11],[9,4,11],[9,5,11],[15,0,17],[15,1,17],[15,2,17],[15,3,17],[15,4,17],[15,5,17],[15,7,17],[15,9,17],[15,11,17],[15,13,17],[12,0,14],[12,1,14],[12,2,14],[12,3,14],[12,4,14],[12,5,14],[12,8,14],[12,10,14],[13,0,15],[13,1,15],[13,2,15],[13,3,15],[13,4,15],[13,5,15],[13,9,15],[13,11,15]\n\t],\n\t\"tortilla_tortilla\":[],\n\t\"transitivity\":[\n\t\t[0,1,4],[0,1,5],[0,1,6],[0,1,7],[0,1,12],[0,1,13],[0,1,14],[0,1,15],[0,1,16],[0,1,17],[0,2,4],[0,2,5],[0,2,6],[0,2,7],[0,2,8],[0,2,9],[0,2,10],[0,2,11],[0,2,12],[0,2,13],[0,2,14],[0,2,15],[0,2,16],[0,2,17],[0,3,14],[0,3,15],[0,3,16],[0,3,17],[0,4,6],[0,4,7],[0,4,8],[0,4,9],[0,4,10],[0,4,11],[0,4,12],[0,4,13],[0,4,14],[0,4,15],[0,4,16],[0,4,17],[0,5,6],[0,5,7],[0,5,8],[0,5,9],[0,5,10],[0,5,11],[0,5,12],[0,5,13],[0,5,14],[0,5,15],[0,5,16],[0,5,17],[0,6,10],[0,6,12],[0,6,14],[0,7,11],[0,7,13],[0,7,15],[0,8,12],[0,9,13],[0,10,14],[0,10,16],[0,11,15],[0,11,17],[0,12,16],[0,13,17],[1,2,14],[1,2,15],[1,2,16],[1,2,17],[1,3,5],[1,3,6],[1,3,7],[1,3,8],[1,3,9],[1,3,10],[1,3,11],[1,3,12],[1,3,13],[1,3,14],[1,3,15],[1,3,16],[1,3,17],[1,4,6],[1,4,7],[1,4,8],[1,4,9],[1,4,10],[1,4,11],[1,4,12],[1,4,13],[1,4,14],[1,4,15],[1,4,16],[1,4,17],[1,5,6],[1,5,7],[1,5,8],[1,5,9],[1,5,10],[1,5,11],[1,5,12],[1,5,13],[1,5,14],[1,5,15],[1,5,16],[1,5,17],[1,6,10],[1,6,12],[1,6,14],[1,7,11],[1,7,13],[1,7,15],[1,8,12],[1,8,14],[1,8,16],[1,9,13],[1,9,15],[1,9,17],[1,12,16],[1,13,17],[2,3,5],[2,3,6],[2,3,7],[2,3,8],[2,3,9],[2,3,10],[2,3,11],[2,3,12],[2,3,13],[2,3,14],[2,3,15],[2,3,16],[2,3,17],[2,4,6],[2,4,7],[2,4,8],[2,4,9],[2,4,10],[2,4,11],[2,4,12],[2,4,13],[2,4,14],[2,4,15],[2,4,16],[2,4,17],[2,5,6],[2,5,7],[2,5,8],[2,5,9],[2,5,10],[2,5,11],[2,5,12],[2,5,13],[2,5,14],[2,5,15],[2,5,16],[2,5,17],[2,6,10],[2,6,12],[2,6,14],[2,7,11],[2,7,13],[2,7,15],[2,8,12],[2,8,14],[2,8,16],[2,9,13],[2,9,15],[2,9,17],[2,10,14],[2,10,16],[2,11,15],[2,11,17],[2,12,16],[2,13,17],[3,5,6],[3,5,7],[3,5,8],[3,5,9],[3,5,10],[3,5,11],[3,5,12],[3,5,13],[3,5,14],[3,5,15],[3,5,16],[3,5,17],[3,6,10],[3,6,12],[3,6,14],[3,7,11],[3,7,13],[3,7,15],[3,8,12],[3,8,14],[3,8,16],[3,9,13],[3,9,15],[3,9,17],[3,10,14],[3,10,16],[3,11,15],[3,11,17],[3,12,16],[3,13,17],[4,5,14],[4,5,15],[4,5,16],[4,5,17],[4,6,10],[4,6,12],[4,6,14],[4,7,11],[4,7,13],[4,7,15],[4,8,12],[4,8,14],[4,8,16],[4,9,13],[4,9,15],[4,9,17],[4,10,14],[4,10,16],[4,11,15],[4,11,17],[4,12,16],[4,13,17],[5,6,10],[5,6,12],[5,6,14],[5,7,11],[5,7,13],[5,7,15],[5,8,12],[5,8,14],[5,8,16],[5,9,13],[5,9,15],[5,9,17],[5,10,14],[5,10,16],[5,11,15],[5,11,17],[5,12,16],[5,13,17],[6,8,14],[6,10,14],[7,9,15],[7,11,15],[8,10,14],[8,10,16],[8,12,16],[9,11,15],[9,11,17],[9,13,17],[10,12,16],[11,13,17]\n\t]\n}\n"
  },
  {
    "path": "tests/files/json/kabuto-faces-faces-overlap.json",
    "content": "[\n\t[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,1,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,1,2,3,4,6,7,8,9,10,11,12,13,14,15,16,17],\n\t[0,1,2,3,4,5,8,10,12,14,16],\n\t[0,1,2,3,4,5,9,11,13,15,17],\n\t[0,1,2,3,4,5,6,10,12,14,16],\n\t[0,1,2,3,4,5,7,11,13,15,17],\n\t[0,1,2,3,4,5,6,8,12,14,16],\n\t[0,1,2,3,4,5,7,9,13,15,17],\n\t[0,1,2,3,4,5,6,8,10,14,16],\n\t[0,1,2,3,4,5,7,9,11,15,17],\n\t[0,1,2,3,4,5,6,8,10,12,16],\n\t[0,1,2,3,4,5,7,9,11,13,17],\n\t[0,1,2,3,4,5,6,8,10,12,14],\n\t[0,1,2,3,4,5,7,9,11,13,15]\n]\n"
  },
  {
    "path": "tests/files/json/kabuto-layer-solver-no-depth.json",
    "content": "{\n\t\"root\":{\n\t\t\"0 8\":2,\"0 9\":2,\"10 12\":2,\"0 3\":2,\"11 13\":2,\"1 2\":1,\"6 16\":2,\"7 17\":2,\"3 4\":1,\"1 11\":2,\"1 10\":2,\"6 8\":1,\"14 16\":1,\"8 10\":2,\"7 9\":1,\"9 11\":2,\"15 17\":1,\"4 5\":1,\"12 14\":2,\"13 15\":2,\"0 10\":2,\"0 6\":2,\"0 11\":2,\"0 7\":2,\"0 12\":2,\"1 12\":2,\"8 12\":2,\"0 13\":2,\"3 5\":1,\"1 13\":2,\"9 13\":2,\"6 14\":2,\"7 15\":2,\"10 14\":2,\"0 4\":2,\"11 15\":2,\"0 16\":2,\"8 16\":2,\"0 17\":2,\"9 17\":2,\"1 15\":2,\"1 9\":2,\"1 14\":2,\"1 8\":2,\"0 14\":2,\"1 16\":2,\"8 14\":2,\"10 16\":2,\"12 16\":2,\"0 15\":2,\"1 17\":2,\"9 15\":2,\"11 17\":2,\"13 17\":2,\"2 14\":2,\"2 15\":2,\"2 16\":2,\"2 17\":2,\"0 1\":1,\"2 10\":2,\"0 5\":2,\"2 11\":2,\"6 12\":1,\"7 13\":1,\"2 7\":2,\"2 6\":2,\"1 6\":2,\"6 10\":1,\"1 7\":2,\"7 11\":1,\"2 12\":2,\"2 13\":2,\"1 4\":2,\"1 3\":2,\"2 9\":2,\"2 8\":2,\"2 4\":2,\"1 5\":2,\"0 2\":1,\"2 5\":2,\"2 3\":2\n\t},\"branches\":[\n\t\t[\n\t\t\t{\"3 8\":2,\"3 16\":2,\"4 6\":2,\"5 6\":2,\"3 14\":2,\"4 8\":2,\"3 10\":2,\"4 16\":2,\"5 16\":2,\"4 14\":2,\"3 12\":2,\"5 8\":2,\"5 14\":2,\"4 10\":2,\"4 12\":2,\"5 10\":2,\"5 12\":2,\"3 6\":2},\n\t\t\t{\"3 8\":1,\"3 16\":1,\"4 6\":1,\"3 10\":1,\"3 12\":1,\"4 8\":1,\"4 16\":1,\"3 14\":1,\"4 10\":1,\"4 12\":1,\"4 14\":1,\"3 6\":1,\"5 16\":1,\"5 12\":1,\"5 6\":1,\"5 8\":1,\"5 10\":1,\"5 14\":1},\n\t\t\t{\"3 8\":1,\"3 16\":1,\"4 6\":1,\"3 10\":1,\"3 12\":1,\"4 8\":1,\"4 16\":1,\"3 14\":1,\"4 10\":1,\"4 12\":1,\"4 14\":1,\"3 6\":1,\"5 16\":2,\"5 12\":2,\"5 10\":2,\"5 6\":2,\"5 8\":2,\"5 14\":2}\n\t\t],\n\t\t[\n\t\t\t{\"3 9\":2,\"3 17\":2,\"4 7\":2,\"5 7\":2,\"3 15\":2,\"4 9\":2,\"3 11\":2,\"4 17\":2,\"5 17\":2,\"4 15\":2,\"3 13\":2,\"5 9\":2,\"5 15\":2,\"4 11\":2,\"4 13\":2,\"5 11\":2,\"5 13\":2,\"3 7\":2},\n\t\t\t{\"3 9\":1,\"3 17\":1,\"4 7\":1,\"3 11\":1,\"3 13\":1,\"4 9\":1,\"4 17\":1,\"3 15\":1,\"4 11\":1,\"4 13\":1,\"4 15\":1,\"3 7\":1,\"5 17\":1,\"5 13\":1,\"5 7\":1,\"5 9\":1,\"5 11\":1,\"5 15\":1},\n\t\t\t{\"3 9\":1,\"3 17\":1,\"4 7\":1,\"3 11\":1,\"3 13\":1,\"4 9\":1,\"4 17\":1,\"3 15\":1,\"4 11\":1,\"4 13\":1,\"4 15\":1,\"3 7\":1,\"5 17\":2,\"5 13\":2,\"5 11\":2,\"5 7\":2,\"5 9\":2,\"5 15\":2}\n\t\t]\n\t]\n}\n"
  },
  {
    "path": "tests/files/json/kabuto-layer-solver.json",
    "content": "{\n\t\"orders\": { \"0 8\":2,\"0 9\":2,\"10 12\":2,\"0 3\":2,\"11 13\":2,\"1 2\":1,\"6 16\":2,\"7 17\":2,\"3 4\":1,\"1 11\":2,\"1 10\":2,\"6 8\":1,\"14 16\":1,\"8 10\":2,\"7 9\":1,\"9 11\":2,\"15 17\":1,\"4 5\":1,\"12 14\":2,\"13 15\":2,\"0 10\":2,\"0 6\":2,\"0 11\":2,\"0 7\":2,\"0 12\":2,\"1 12\":2,\"8 12\":2,\"0 13\":2,\"3 5\":1,\"1 13\":2,\"9 13\":2,\"6 14\":2,\"7 15\":2,\"10 14\":2,\"0 4\":2,\"11 15\":2,\"0 16\":2,\"8 16\":2,\"0 17\":2,\"9 17\":2,\"1 15\":2,\"1 9\":2,\"1 14\":2,\"1 8\":2,\"0 14\":2,\"1 16\":2,\"8 14\":2,\"10 16\":2,\"12 16\":2,\"0 15\":2,\"1 17\":2,\"9 15\":2,\"11 17\":2,\"13 17\":2,\"2 14\":2,\"2 15\":2,\"2 16\":2,\"2 17\":2,\"0 1\":1,\"2 10\":2,\"0 5\":2,\"2 11\":2,\"6 12\":1,\"7 13\":1,\"2 7\":2,\"2 6\":2,\"1 6\":2,\"6 10\":1,\"1 7\":2,\"7 11\":1,\"2 12\":2,\"2 13\":2,\"1 4\":2,\"1 3\":2,\"2 9\":2,\"2 8\":2,\"2 4\":2,\"1 5\":2,\"0 2\":1,\"2 5\":2,\"2 3\":2 },\n\t\"branches\": [\n\t\t[\n\t\t\t{\n\t\t\t\t\"orders\": { \"3 8\":1,\"3 16\":1,\"4 6\":1,\"3 10\":1,\"3 12\":1,\"4 8\":1,\"4 16\":1,\"3 14\":1,\"4 10\":1,\"4 12\":1,\"4 14\":1,\"3 6\":1 },\n\t\t\t\t\"branches\":[\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"5 16\":1,\"5 12\":1,\"5 6\":1,\"5 8\":1,\"5 10\":1,\"5 14\":1 }\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"5 16\":2,\"5 12\":2,\"5 10\":2,\"5 6\":2,\"5 8\":2,\"5 14\":2 }\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"orders\": { \"3 8\":2,\"3 16\":2,\"4 6\":2,\"5 6\":2,\"3 14\":2,\"4 8\":2,\"3 10\":2,\"4 16\":2,\"5 16\":2,\"4 14\":2,\"3 12\":2,\"5 8\":2,\"5 14\":2,\"4 10\":2,\"4 12\":2,\"5 10\":2,\"5 12\":2,\"3 6\":2 }\n\t\t\t}\n\t\t],\n\t\t[\n\t\t\t{\n\t\t\t\t\"orders\": { \"3 9\":1,\"3 17\":1,\"4 7\":1,\"3 11\":1,\"3 13\":1,\"4 9\":1,\"4 17\":1,\"3 15\":1,\"4 11\":1,\"4 13\":1,\"4 15\":1,\"3 7\":1 },\n\t\t\t\t\"branches\": [\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"5 17\":1,\"5 13\":1,\"5 7\":1,\"5 9\":1,\"5 11\":1,\"5 15\":1 }\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"5 17\":2,\"5 13\":2,\"5 11\":2,\"5 7\":2,\"5 9\":2,\"5 15\":2 }\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"orders\": { \"3 9\":2,\"3 17\":2,\"4 7\":2,\"5 7\":2,\"3 15\":2,\"4 9\":2,\"3 11\":2,\"4 17\":2,\"5 17\":2,\"4 15\":2,\"3 13\":2,\"5 9\":2,\"5 15\":2,\"4 11\":2,\"4 13\":2,\"5 11\":2,\"5 13\":2,\"3 7\":2 }\n\t\t\t}\n\t\t]\n\t]\n}\n"
  },
  {
    "path": "tests/files/json/kraft-bird-faces-faces-overlap.json",
    "content": "[[1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[3,90,91,92,94,96,97,98,101,123,124,125,127,128,129,130,131],[2,90,91,92,94,96,97,98,101,123,124,125,127,128,129,130,131],[5,6,7,8,9,10,12,13,14,18,19,20,21,22,25,26,28,44,47,90,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,162,163,277,278,279,280,281,282,284,285,286,287,288,289,302,303,304,305,306,309,310,311,312,313,314,315],[4,6,7,10,18,55,56,68,69,90,99,101,125,131,133,286,288,289,292,312,314,315,319],[4,5,7,10,18,55,56,68,69,90,99,101,125,131,133,286,288,289,292,312,314,315,319],[4,5,6,8,9,10,12,13,14,18,19,20,21,22,25,26,28,44,47,90,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,162,163,277,278,279,280,281,282,284,285,286,287,288,289,302,303,304,305,306,309,310,311,312,313,314,315],[4,7,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,7,8,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,5,6,7,8,9,11,12,13,17,18,19,20,21,22,25,26,30,33,34,35,36,37,44,45,47,48,55,56,57,60,68,69,72,78,90,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,286,287,288,289,292,293,294,295,296,297,303,304,310,311,312,313,314,315,318,319,320,321,322,323],[10,17,18,30,33,36,37,57,60,72,78,104,109,140,141,295,296,297,320,321,323],[4,7,10,18,20,21,22,25,26,55,56,68,69,90,99,100,101,110,125,131,132,133,146,147,148,286,288,289,292,294,312,314,315,319,322],[4,7,10,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,8,9,16,28,29,115,116,118,119,121,156,157,159,160,163,278,279,280,281,284,285,302,303,304,305,310,311],[27],[14,21,25,28,29],[10,11,18,30,33,36,37,57,60,72,78,104,109,140,141,295,296,297,320,321,323],[4,5,6,7,8,9,10,11,12,13,17,19,20,21,22,25,26,30,33,34,35,36,37,44,45,47,48,55,56,57,60,68,69,72,78,90,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,286,287,288,289,292,293,294,295,296,297,303,304,310,311,312,313,314,315,318,319,320,321,322,323],[4,7,10,13,18,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,10,12,18,21,22,25,26,55,56,68,69,90,99,100,101,110,125,131,132,133,146,147,148,286,288,289,292,294,312,314,315,319,322],[4,7,8,9,10,12,13,16,18,19,20,25,29,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,163,280,281,284,285,286,287,288,289,294,302,303,310,311,312,313,314,315,322],[4,7,10,12,18,20,26,55,56,68,69,90,99,100,101,110,125,131,132,133,146,147,148,286,288,289,292,294,312,314,315,319,322],[24],[23],[4,7,8,9,10,12,13,16,18,19,20,21,29,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,163,280,281,284,285,286,287,288,289,294,302,303,310,311,312,313,314,315,322],[4,7,10,12,18,20,22,55,56,68,69,90,99,100,101,110,125,131,132,133,146,147,148,286,288,289,292,294,312,314,315,319,322],[15],[4,7,8,9,14,16,29,115,116,118,119,121,156,157,159,160,163,278,279,280,281,284,285,302,303,304,305,310,311],[14,16,21,25,28],[10,11,17,18,31,32,33,34,35,36,37,38,39,61,76,77,81,291,293,295,316,318,321],[30,32,33,34,35,36,37,38,39,57,60,61,72,76,77,78,81,295,321],[30,31,33,34,35,36,37,38,39,57,60,61,72,76,77,78,81,290,291,292,293,295,316,317,318,319,321],[10,11,17,18,30,31,32,34,35,36,37,38,39,61,76,77,81,291,293,295,316,318,321],[10,18,30,31,32,33,35,36,37,38,39,288,289,290,291,292,293,295,312,315,316,317,318,319,321],[10,18,30,31,32,33,34,36,37,38,39,288,289,290,291,292,293,295,312,315,316,317,318,319,321],[10,11,17,18,30,31,32,33,34,35,37,38,39,61,76,77,81,291,293,295,316,318,321],[10,11,17,18,30,31,32,33,34,35,36,38,39,61,76,77,81,291,293,295,316,318,321],[30,31,32,33,34,35,36,37,39,57,60,61,72,76,77,78,81,290,291,292,293,295,316,317,318,319,321],[30,31,32,33,34,35,36,37,38,57,60,61,72,76,77,78,81,295,321],[41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,216,217,218,219,222,223,226,227,251,252,267,268],[0,1,40,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[40,41,42,169,171,178,182,184,185,196,197,198,208,209,210,212,213,216,217,218,219,222,223,226,227,251,252,267,268],[4,7,10,13,18,19,21,25,45,47,48,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[10,18,44,46,47,48,49,56,68,90,104,105,106,108,110,125,136,138,139,140,142,146,155,288,294,312,322],[45,48,49,56,68,294,322],[4,7,10,13,18,19,21,25,44,45,48,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[10,18,44,45,46,47,49,56,68,90,104,105,106,108,110,125,136,138,139,140,142,146,155,288,294,312,322],[45,46,48,56,68,294,322],[52,246,259],[53,62,84,245,246,259,260],[50,246,259],[51,62,84,245,246,259,260],[55,69,70,90,91,98,99,101,124,125,130,131,133,292,319],[5,6,10,12,18,20,22,26,54,69,70,90,91,98,99,110,124,125,130,133,146,147,148,292,319],[5,6,10,12,13,18,19,20,21,22,25,26,44,45,46,47,48,49,68,90,99,100,102,103,104,105,106,108,109,110,111,112,113,125,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,292,294,310,311,312,313,314,315,319,322],[10,11,17,18,31,32,38,39,58,59,60,61,62,63,66,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,87,104,109,140,141,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,231,232,233,234,237,238,239,242,243,244,245,248,249,254,260,261,262,263,270,271,275,295,296,301,320,321,326],[57,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,176,186,187,188,189,192,200,201,202,203,205,296,301,320,326],[57,58,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,176,192,205,296,320],[10,11,17,18,31,32,38,39,57,61,62,72,76,77,78,79,81,82,84,104,109,140,141,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[30,31,32,33,36,37,38,39,57,60,62,72,76,77,78,79,81,82,84,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[51,53,57,60,61,72,75,76,77,78,79,80,81,82,84,228,229,236,237,240,241,242,243,244,245,246,247,248,249,250,253,254,255,256,257,258,259,260,261,262,263,264,265,266,269,270,271,272,273,274,275,295,321],[57,58,59,66,67,71,72,73,85,87,89,164,165,166,167,173,174,175,176,186,189,190,191,192,199,200,203,205,206,296,301,320,326],[65,86,88,190,199,301,326],[64,86,88,190,199,301,326],[57,58,59,63,67,71,72,73,74,75,80,83,85,87,89,164,165,166,167,168,172,173,174,175,176,186,189,190,191,192,193,194,199,200,203,204,205,206,207,230,232,235,239,242,263,295,296,301,320,321,326],[63,66,85,87,89,191,194,206,207,232,239],[5,6,10,12,13,18,19,20,21,22,25,26,44,45,46,47,48,49,56,90,99,100,102,103,104,105,106,108,109,110,111,112,113,125,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,292,294,310,311,312,313,314,315,319,322],[5,6,10,12,18,20,22,26,54,55,70,90,91,98,99,110,124,125,130,133,146,147,148,292,319],[54,55,69,90,91,98,99,101,124,125,130,131,133,292,319],[57,58,59,63,66,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,176,186,187,188,189,192,200,201,202,203,205,296,301,320,326],[10,11,17,18,31,32,38,39,57,58,59,60,61,62,63,66,71,73,74,75,76,77,78,79,80,81,82,83,84,85,87,104,109,140,141,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,231,232,233,234,237,238,239,242,243,244,245,248,249,254,260,261,262,263,270,271,275,295,296,301,320,321,326],[57,58,59,63,66,71,72,74,75,80,83,85,87,164,165,166,167,173,174,175,176,192,205,296,320],[57,58,59,66,71,72,73,75,80,83,87,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,229,231,232,233,234,236,237,238,239,240,241,242,243,248,249,250,254,262,263,264,265,269,270,271,275,295,296,301,320,321,326],[57,58,59,62,66,71,72,73,74,79,80,82,83,84,87,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,229,231,232,233,234,236,237,238,239,240,241,242,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,271,275,295,296,301,320,321,326],[30,31,32,33,36,37,38,39,57,60,61,62,72,77,78,79,81,82,84,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[30,31,32,33,36,37,38,39,57,60,61,62,72,76,78,79,81,82,84,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[10,11,17,18,31,32,38,39,57,60,61,62,72,76,77,79,81,82,84,104,109,140,141,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[57,60,61,62,72,75,76,77,78,80,81,82,84,228,229,236,237,240,241,242,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,271,275,295,321],[57,58,59,62,66,71,72,73,74,75,79,82,83,84,87,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,229,231,232,233,234,236,237,238,239,240,241,242,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,271,275,295,296,301,320,321,326],[30,31,32,33,36,37,38,39,57,60,61,62,72,76,77,78,79,82,84,228,231,232,233,234,237,238,239,248,249,254,270,271,275,295,296,301,320,321,326],[57,60,61,62,72,75,76,77,78,79,80,81,84,228,229,236,237,240,241,242,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,271,275,295,321],[57,58,59,66,71,72,73,74,75,80,87,164,165,166,167,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,229,231,232,233,234,236,237,238,239,240,241,242,243,248,249,250,254,262,263,264,265,269,270,271,275,295,296,301,320,321,326],[51,53,57,60,61,62,72,75,76,77,78,79,80,81,82,228,229,236,237,240,241,242,243,244,245,246,247,248,249,250,253,254,255,256,257,258,259,260,261,262,263,264,265,266,269,270,271,272,273,274,275,295,321],[57,58,59,63,66,67,71,72,73,87,89,164,165,166,167,173,174,175,176,186,189,190,191,192,199,200,203,205,206,296,301,320,326],[64,65,88,190,199,301,326],[57,58,59,63,66,67,71,72,73,74,75,80,83,85,89,164,165,166,167,168,172,173,174,175,176,186,189,190,191,192,193,194,199,200,203,204,205,206,207,230,232,235,239,242,263,295,296,301,320,321,326],[64,65,86,190,199,301,326],[63,66,67,85,87,191,194,206,207,232,239],[2,3,4,5,6,7,10,12,13,18,19,20,21,22,25,26,44,45,47,48,54,55,56,68,69,70,91,92,94,95,96,97,98,99,100,101,102,103,104,105,106,108,109,110,111,112,113,123,124,125,126,127,128,129,130,131,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,292,294,310,311,312,313,314,315,319,322],[2,3,54,55,69,70,90,92,94,95,96,97,98,99,101,123,124,125,126,127,128,129,130,131,133,292,319],[2,3,90,91,94,95,96,97,98,123,124,125,126,127,128,129,130],[95,97,122,126,128],[2,3,90,91,92,96,97,98,123,124,125,127,128,129,130],[90,91,92,93,97,122,123,124,125,126,128],[2,3,90,91,92,94,98,123,124,125,127,129,130],[2,3,90,91,92,93,94,95,98,122,123,124,125,126,127,128,130],[2,3,54,55,69,70,90,91,92,94,96,97,101,123,124,125,127,128,129,130,131,292,319],[4,5,6,7,10,12,18,20,22,26,54,55,56,68,69,70,90,91,101,110,124,125,131,133,144,146,147,148,149,286,288,289,292,312,314,315,319],[4,7,10,12,13,18,19,20,21,22,25,26,44,47,56,68,90,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,294,310,311,312,313,314,315,322],[2,3,4,5,6,7,10,12,13,18,19,20,21,22,25,26,44,47,54,70,90,91,98,99,100,102,103,104,105,106,108,109,110,111,112,113,124,125,130,131,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,310,311,312,313,314,315],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,103,104,105,106,108,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,8,9,10,13,18,19,21,25,44,47,56,68,90,100,101,102,104,105,106,107,108,110,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,10,11,13,17,18,19,21,25,44,45,47,48,56,57,60,68,72,78,90,100,101,102,103,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,295,296,297,303,304,310,311,312,313,320,321,322,323],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,106,107,108,110,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,107,108,110,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,10,18,21,25,103,104,105,106,110,112,114,115,117,118,120,121,134,137,138,139,140,142,146,154,155,157,158,160,161,162,163,279,280,284,285,303,304,310,311],[4,7,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,10,11,13,17,18,19,21,25,56,57,60,68,72,78,90,100,101,104,110,111,112,113,125,131,132,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,288,294,295,296,297,311,312,320,321,322,323],[4,7,8,9,10,12,13,18,19,20,21,22,25,26,44,45,47,48,55,56,68,69,90,99,100,101,102,103,104,105,106,107,108,109,111,112,113,114,115,117,118,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,286,287,288,289,292,294,303,304,310,311,312,313,314,315,319,322],[4,7,10,13,18,19,21,25,56,68,90,100,101,104,109,110,112,113,125,131,132,140,141,142,146,147,148,151,153,154,155,284,288,294,311,312,322],[4,7,8,9,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,113,114,115,117,118,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,157,158,160,161,163,280,284,285,287,288,294,303,310,311,312,313,322],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,109,110,111,112,125,131,132,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,8,9,10,18,21,25,103,104,105,106,107,110,112,115,117,118,120,121,134,137,138,139,140,142,146,154,155,157,158,160,161,162,163,279,280,284,285,303,304,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,116,117,118,119,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,163,280,281,284,285,302,303,310,311],[4,7,8,9,14,21,25,28,115,118,119,121,156,157,159,160,163,280,281,284,285,302,303,310,311],[4,7,8,9,10,18,21,25,103,104,105,106,107,110,112,114,115,118,121,134,137,138,139,140,142,146,154,155,157,158,160,161,163,280,284,285,303,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,7,8,9,14,21,25,28,115,116,118,121,156,157,159,160,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,7,8,9,10,18,103,104,105,106,107,110,114,118,121,134,137,138,139,140,142,146,155,157,161,162,163,279,280,284,285,303,304,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,285,302,303,304,305,310,311],[93,95,97,126,128],[2,3,90,91,92,94,95,96,97,98,124,125,126,127,128,129,130],[2,3,54,55,69,70,90,91,92,94,95,96,97,98,99,101,123,125,126,127,128,129,130,131,133,292,319],[2,3,4,5,6,7,10,12,13,18,19,20,21,22,25,26,44,45,47,48,54,55,56,68,69,70,90,91,92,94,95,96,97,98,99,100,101,102,103,104,105,106,108,109,110,111,112,113,123,124,126,127,128,129,130,131,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,292,294,310,311,312,313,314,315,319,322],[90,91,92,93,95,97,122,123,124,125,128],[2,3,90,91,92,94,96,97,98,123,124,125,128,129,130],[2,3,90,91,92,93,94,95,97,98,122,123,124,125,126,127,130],[2,3,90,91,92,94,96,98,123,124,125,127,130],[2,3,54,55,69,70,90,91,92,94,96,97,98,101,123,124,125,127,128,129,131,292,319],[2,3,4,5,6,7,10,12,13,18,19,20,21,22,25,26,44,47,54,70,90,91,98,99,100,101,102,103,104,105,106,108,109,110,111,112,113,124,125,130,132,133,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,310,311,312,313,314,315],[4,7,10,12,13,18,19,20,21,22,25,26,44,47,56,68,90,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,284,285,286,287,288,289,294,310,311,312,313,314,315,322],[4,5,6,7,10,12,18,20,22,26,54,55,56,68,69,70,90,91,99,101,110,124,125,131,144,146,147,148,149,286,288,289,292,312,314,315,319],[4,7,8,9,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,107,108,110,112,113,114,115,117,118,120,121,125,131,132,135,136,137,138,139,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,135,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,8,9,10,18,21,25,103,104,105,106,107,110,112,114,115,117,118,120,121,134,138,139,140,142,146,154,155,157,158,160,161,162,163,279,280,284,285,303,304,310,311],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,107,108,110,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,139,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,107,108,110,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,140,142,146,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,10,11,13,17,18,19,21,25,44,45,47,48,56,57,60,68,72,78,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,295,296,297,303,304,310,311,312,313,320,321,322,323],[4,7,10,11,13,17,18,19,21,25,56,57,60,68,72,78,90,100,101,104,109,110,111,112,113,125,131,132,140,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,288,294,295,296,297,311,312,320,321,322,323],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,141,143,144,146,147,148,149,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,10,13,18,19,21,25,56,68,90,100,101,104,109,110,112,113,125,131,132,140,141,142,144,146,149,152,153,154,155,284,288,294,311,312,322],[4,7,10,13,18,19,21,25,56,68,90,99,100,101,104,109,110,112,113,125,131,132,133,140,141,142,143,146,149,152,153,154,155,284,286,288,292,294,311,312,314,319,322],[10,18,104,109,110,140,141,146,150,288,292,294,312,319,322],[4,7,8,9,10,12,13,18,19,20,21,22,25,26,44,45,47,48,55,56,68,69,90,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,147,148,149,150,151,152,153,154,155,157,158,160,161,162,163,279,280,284,285,286,287,288,289,292,294,303,304,310,311,312,313,314,315,319,322],[4,7,10,12,13,18,19,20,21,22,25,26,55,56,68,69,90,99,100,101,104,109,110,111,112,113,125,131,132,133,140,141,142,146,148,151,153,154,155,284,286,288,289,292,294,311,312,314,315,319,322],[4,7,10,12,13,18,19,20,21,22,25,26,55,56,68,69,90,99,100,101,104,109,110,111,112,113,125,131,132,133,140,141,142,146,147,151,153,154,155,284,286,288,289,292,294,311,312,314,315,319,322],[4,7,10,13,18,19,21,25,56,68,90,99,100,101,104,109,110,112,113,125,131,132,133,140,141,142,143,144,146,152,153,154,155,284,286,288,292,294,311,312,314,319,322],[10,18,104,109,110,140,141,145,146,288,292,294,312,319,322],[4,7,10,13,18,19,21,25,56,68,90,100,101,104,109,110,111,112,113,125,131,132,140,141,142,146,147,148,153,154,155,284,288,294,311,312,322],[4,7,10,13,18,19,21,25,56,68,90,100,101,104,109,110,112,113,125,131,132,140,141,142,143,144,146,149,153,154,155,284,288,294,311,312,322],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,134,135,136,138,139,140,141,142,143,144,146,147,148,149,151,152,154,155,284,285,287,288,294,310,311,312,313,322],[4,7,8,9,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,155,157,158,160,161,163,280,284,285,287,288,294,303,310,311,312,313,322],[4,7,8,9,10,13,18,19,21,25,44,45,47,48,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,120,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,157,158,160,161,162,163,279,280,284,285,287,288,294,303,304,310,311,312,313,322],[4,7,8,9,14,21,25,28,115,116,118,119,121,157,159,160,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,158,159,160,161,162,163,278,279,280,281,284,285,302,303,304,305,310,311],[4,7,8,9,10,18,21,25,103,104,105,106,107,110,112,114,115,117,118,121,134,137,138,139,140,142,146,154,155,157,160,161,163,280,284,285,303,310,311],[4,7,8,9,14,21,25,28,115,116,118,119,121,156,157,160,163,280,281,284,285,302,303,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,121,134,137,138,139,140,142,146,154,155,156,157,158,159,161,163,280,281,284,285,302,303,310,311],[4,7,8,9,10,18,21,25,103,104,105,106,107,110,112,114,115,117,118,120,121,134,137,138,139,140,142,146,154,155,157,158,160,162,163,279,280,284,285,303,304,310,311],[4,7,8,9,10,18,103,104,105,106,107,110,114,118,120,121,134,137,138,139,140,142,146,155,157,161,163,279,280,284,285,303,304,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,278,279,280,281,284,285,302,303,304,305,310,311],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,165,166,167,173,174,175,176,186,189,192,200,203,205,296,301,320,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,166,167,173,174,175,176,186,189,192,200,203,205,296,301,320,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,167,173,174,175,176,186,187,188,189,192,200,201,202,203,205,296,301,320,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,168,172,173,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,231,232,233,234,237,238,239,242,243,248,249,254,262,263,270,271,275,295,296,301,320,321,326],[57,66,72,74,75,80,83,87,167,172,173,193,204,228,231,232,233,234,237,238,239,242,248,249,263,270,271,295,321],[40,41,42,43,171,178,182,184,185,195,196,197,198,208,209,210,211,212,213,216,217,218,219,222,223,226,227,251,252,267,268],[171,183,185,251,253,255,266,268,274],[0,1,40,41,42,43,169,170,178,182,183,184,185,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,253,255,266,267,268,274],[57,66,72,74,75,80,83,87,167,168,173,193,204,228,231,232,233,234,237,238,239,242,248,249,263,270,271,295,321],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,168,172,174,175,176,186,187,188,189,192,193,200,201,202,203,204,205,228,231,232,233,234,237,238,239,242,243,248,249,254,262,263,270,271,275,295,296,301,320,321,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,175,176,186,187,188,189,192,200,201,202,203,205,296,301,320,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,176,186,189,192,200,203,205,296,301,320,326],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,186,189,192,200,203,205,296,301,320,326],[179,180,181],[0,1,40,41,42,43,169,171,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[177,180,181],[177,179,181],[177,179,180],[0,1,40,41,42,43,169,171,178,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[170,171,185,251,253,255,266,268,274],[40,41,42,43,169,171,178,182,185,195,196,197,198,208,209,210,211,212,213,216,217,218,219,222,223,226,227,251,252,267,268],[0,1,40,41,42,43,169,170,171,178,182,183,184,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,253,255,266,267,268,274],[57,58,63,66,71,72,74,75,80,83,85,87,164,165,166,167,173,174,175,176,187,188,189,200,201,202,203,296,301,320,326],[57,58,71,72,74,75,80,83,166,167,173,174,186,188,189,200,201,202,203,296,301,320,326],[57,58,71,72,74,75,80,83,166,167,173,174,186,187,189,200,201,202,203,296,301,320,326],[57,58,63,66,71,72,74,75,80,83,85,87,164,165,166,167,173,174,175,176,186,187,188,200,201,202,203,296,301,320,326],[63,64,65,66,85,86,87,88,199,301,326],[63,66,67,85,87,89,206],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,176,205,296,320],[57,66,72,74,75,80,83,87,167,168,172,173,204,231,232,233,234,238,239,295,321],[66,67,87,89,207,232,239],[169,171,184,185,211,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[63,64,65,66,85,86,87,88,190,301,326],[57,58,63,66,71,72,74,75,80,83,85,87,164,165,166,167,173,174,175,176,186,187,188,189,201,202,203,296,301,320,326],[57,58,71,72,74,75,80,83,166,167,173,174,186,187,188,189,200,202,203,296,301,320,326],[57,58,71,72,74,75,80,83,166,167,173,174,186,187,188,189,200,201,203,296,301,320,326],[57,58,63,66,71,72,74,75,80,83,85,87,164,165,166,167,173,174,175,176,186,187,188,189,200,201,202,296,301,320,326],[57,66,72,74,75,80,83,87,167,168,172,173,193,231,232,233,234,238,239,295,321],[57,58,59,63,66,71,72,73,74,75,80,83,85,87,164,165,166,167,173,174,175,176,192,296,320],[63,66,67,85,87,89,191],[66,67,87,89,194,232,239],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[169,171,184,185,195,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,216,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,217,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,218,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,219,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,220,221,222,223,224,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,222,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,223,224,225,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,224,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,225,226,227,251,252,267,268],[0,1,41,42,171,178,182,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,226,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,227,251,252,267,268],[0,1,40,41,42,43,169,171,178,182,184,185,196,197,198,208,209,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,251,252,267,268],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,229,236,237,240,241,242,243,245,247,248,249,250,253,258,260,262,263,264,265,266,269,270,271,295,321],[62,74,75,79,80,82,83,84,228,236,237,240,241,242,245,247,248,249,250,253,258,260,263,264,265,266,269,270,271],[66,87,231,232,234,235,239],[57,60,61,72,74,75,76,77,78,80,81,83,167,168,172,173,193,204,230,234,235,242,263,295,321],[57,60,61,66,67,72,74,75,76,77,78,80,81,83,87,89,167,168,172,173,193,194,204,207,230,233,235,238,239,242,263,295,321],[57,60,61,72,74,75,76,77,78,80,81,83,167,168,172,173,193,204,232,238,239,295,321],[57,60,61,72,74,75,76,77,78,80,81,83,167,168,172,173,193,204,230,231,235,242,263,295,321],[66,87,230,231,232,234,239],[62,74,75,79,80,82,83,84,228,229,237,240,241,242,245,247,248,249,250,253,258,260,263,264,265,266,269,270,271],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,228,229,236,240,241,242,243,245,247,248,249,250,253,258,260,262,263,264,265,266,269,270,271,295,321],[57,60,61,72,74,75,76,77,78,80,81,83,167,168,172,173,193,204,232,233,239,295,321],[57,60,61,66,67,72,74,75,76,77,78,80,81,83,87,89,167,168,172,173,193,194,204,207,230,232,233,235,238,242,263,295,321],[62,74,75,79,80,82,83,84,228,229,236,237,241,242,245,248,249,250,254,260,263,264,265,269,270,271,275],[62,74,75,79,80,82,83,84,228,229,236,237,240,242,245,248,249,250,254,260,263,264,265,269,270,271,275],[57,62,66,72,74,75,79,80,82,83,84,87,167,168,172,173,228,229,231,232,234,236,237,239,240,241,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,271,275,295,321],[57,62,72,74,75,79,80,82,83,84,167,173,228,237,242,244,245,248,249,254,260,261,262,263,270,271,275,295,321],[57,62,72,75,79,80,82,84,242,243,245,248,254,260,261,262,263,271,275,295,321],[51,53,57,62,72,75,79,80,82,84,228,229,236,237,240,241,242,243,244,246,247,248,249,250,253,254,255,256,257,258,259,260,261,262,263,264,265,266,269,270,271,272,273,274,275,295,321],[50,51,52,53,62,84,245,259,260],[62,84,228,229,236,237,245,249,250,253,255,256,257,258,260,266,269,270,272,273,274],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,228,229,236,237,240,241,242,243,244,245,249,250,254,260,261,262,263,264,265,269,270,271,275,295,321],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,228,229,236,237,240,241,242,243,245,247,248,250,253,258,260,262,263,264,265,266,269,270,271,295,321],[62,74,75,79,80,82,83,84,228,229,236,237,240,241,242,245,247,248,249,253,258,260,263,264,265,266,269,270,271],[0,1,40,41,42,43,169,170,171,178,182,183,184,185,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,252,253,255,256,257,266,267,268,272,273,274],[0,1,40,41,42,43,169,171,178,182,184,185,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,267,268],[62,84,170,171,183,185,228,229,236,237,245,247,249,250,251,255,256,257,258,260,266,268,269,270,272,273,274],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,173,240,241,242,243,244,245,248,260,261,262,263,264,265,271,275,295,321],[62,84,170,171,183,185,245,247,251,253,256,257,258,260,266,268,272,273,274],[62,84,245,247,251,253,255,257,258,260,266,268,272,273,274],[62,84,245,247,251,253,255,256,258,260,266,268,272,273,274],[62,84,228,229,236,237,245,247,249,250,253,255,256,257,260,266,269,270,272,273,274],[50,51,52,53,62,84,245,246,260],[51,53,57,62,72,75,79,80,82,84,228,229,236,237,240,241,242,243,244,245,246,247,248,249,250,253,254,255,256,257,258,259,261,262,263,264,265,266,269,270,271,272,273,274,275,295,321],[57,62,72,75,79,80,82,84,242,243,244,245,248,254,260,262,263,271,275,295,321],[57,62,72,74,75,79,80,82,83,84,167,173,228,237,242,243,244,245,248,249,254,260,261,263,270,271,275,295,321],[57,62,66,72,74,75,79,80,82,83,84,87,167,168,172,173,228,229,231,232,234,236,237,239,240,241,242,243,244,245,248,249,250,254,260,261,262,264,265,269,270,271,275,295,321],[62,74,75,79,80,82,83,84,228,229,236,237,240,241,242,245,248,249,250,254,260,263,265,269,270,271,275],[62,74,75,79,80,82,83,84,228,229,236,237,240,241,242,245,248,249,250,254,260,263,264,269,270,271,275],[62,84,170,171,183,185,228,229,236,237,245,247,249,250,251,253,255,256,257,258,260,268,269,270,272,273,274],[0,1,40,41,42,43,169,171,178,182,184,185,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,268],[0,1,40,41,42,43,169,170,171,178,182,183,184,185,195,196,197,198,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,251,252,253,255,256,257,266,267,272,273,274],[62,74,75,79,80,82,83,84,228,229,236,237,240,241,242,245,247,248,249,250,253,258,260,263,264,265,266,270,271],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,228,229,236,237,240,241,242,243,245,247,248,249,250,253,258,260,262,263,264,265,266,269,271,295,321],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,168,172,173,228,229,236,237,240,241,242,243,244,245,248,249,250,254,260,261,262,263,264,265,269,270,275,295,321],[62,84,245,247,251,253,255,256,257,258,260,266,268,273,274],[62,84,245,247,251,253,255,256,257,258,260,266,268,272,274],[62,84,170,171,183,185,245,247,251,253,255,256,257,258,260,266,268,272,273],[57,60,61,62,72,74,75,76,77,78,79,80,81,82,83,84,167,173,240,241,242,243,244,245,248,254,260,261,262,263,264,265,271,295,321],[277,278,279,280,281,282,283,302,303,304,305,306,307,308,309],[4,7,276,278,279,280,281,282,283,284,302,303,304,305,306,307,308,309,311],[4,7,8,9,14,28,118,119,121,156,157,163,276,277,279,280,281,282,283,284,285,302,303,304,305,306,307,308,309,310,311],[4,7,8,9,10,14,18,28,103,104,105,106,107,110,114,118,119,120,121,134,137,138,139,140,142,146,155,156,157,161,162,163,276,277,278,280,281,282,283,284,285,302,303,304,305,306,307,308,309,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,276,277,278,279,281,282,283,284,285,302,303,304,305,306,307,308,309,310,311],[4,7,8,9,14,21,25,28,115,116,118,119,121,156,157,159,160,163,276,277,278,279,280,282,283,284,285,302,303,304,305,306,307,308,309,310,311],[4,7,276,277,278,279,280,281,283,284,302,303,304,305,306,307,308,309,311],[276,277,278,279,280,281,282,302,303,304,305,306,307,308,309],[4,7,8,9,10,13,14,18,19,21,25,28,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,162,163,277,278,279,280,281,282,285,287,288,294,302,303,304,305,306,309,310,311,312,313,322],[4,7,8,9,10,13,14,18,19,21,25,28,44,47,56,68,90,100,101,102,103,104,105,106,107,108,110,112,113,114,115,116,117,118,119,120,121,125,131,132,134,135,136,137,138,139,140,142,146,153,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,287,288,294,302,303,304,305,310,311,312,313,322],[4,5,6,7,10,12,18,20,21,22,25,26,56,68,90,99,100,101,110,125,131,132,133,144,146,147,148,149,288,289,292,294,312,314,315,319,322],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,288,294,310,311,312,313,322],[4,5,6,7,10,12,13,18,19,20,21,22,25,26,34,35,44,45,47,48,56,68,90,99,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,133,134,135,136,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,285,286,287,292,293,294,310,311,312,313,314,318,319,322],[4,5,6,7,10,12,18,20,21,22,25,26,34,35,56,68,90,99,100,101,110,125,131,132,133,146,147,148,286,292,294,314,315,319,322],[32,34,35,38,292,317,319],[30,32,33,34,35,36,37,38,292,293,316,318,319],[5,6,10,12,18,20,22,26,32,34,35,38,54,55,56,68,69,70,90,91,98,99,110,124,125,130,133,144,145,146,147,148,149,150,286,288,289,290,291,312,314,315,316,317,319],[10,18,30,32,33,34,35,36,37,38,288,291,312,316,318],[10,12,13,18,19,20,21,22,25,26,44,45,46,47,48,49,56,68,90,100,102,103,104,105,106,108,109,110,111,112,113,125,132,134,135,136,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,285,286,287,288,289,310,311,312,313,314,315,322],[10,11,17,18,30,31,32,33,34,35,36,37,38,39,57,60,61,62,66,72,74,75,76,77,78,79,80,81,82,83,84,87,104,109,140,141,167,168,172,173,193,204,228,231,232,233,234,237,238,239,242,243,244,245,248,249,254,260,261,262,263,270,271,275,321],[10,11,17,18,57,58,59,60,61,63,66,71,72,73,74,75,76,77,78,80,81,83,85,87,104,109,140,141,164,165,166,167,173,174,175,176,186,187,188,189,192,200,201,202,203,205,298,299,300,301,320,324,325,326,327],[10,11,17,18,104,109,140,141,298,323,325],[296,297,320,323,325],[296,320,324],[296,320,327],[57,58,60,61,63,64,65,66,71,72,74,75,76,77,78,80,81,83,85,86,87,88,164,165,166,167,173,174,175,176,186,187,188,189,190,199,200,201,202,203,296,320,326],[4,7,8,9,14,21,25,28,115,116,118,119,121,156,157,159,160,163,276,277,278,279,280,281,282,283,284,285,303,304,305,306,307,308,309,310,311],[4,7,8,9,10,14,18,21,25,28,103,104,105,106,107,110,112,114,115,116,117,118,119,120,121,134,137,138,139,140,142,146,154,155,156,157,158,159,160,161,162,163,276,277,278,279,280,281,282,283,284,285,302,304,305,306,307,308,309,310,311],[4,7,8,9,10,14,18,28,103,104,105,106,107,110,114,118,119,120,121,134,137,138,139,140,142,146,155,156,157,161,162,163,276,277,278,279,280,281,282,283,284,285,302,303,305,306,307,308,309,310,311],[4,7,8,9,14,28,118,119,121,156,157,163,276,277,278,279,280,281,282,283,284,285,302,303,304,306,307,308,309,310,311],[4,7,276,277,278,279,280,281,282,283,284,302,303,304,305,307,308,309,311],[276,277,278,279,280,281,282,283,302,303,304,305,306,308,309],[276,277,278,279,280,281,282,283,302,303,304,305,306,307,309],[4,7,276,277,278,279,280,281,282,283,284,302,303,304,305,306,307,308,311],[4,7,8,9,10,13,14,18,19,21,25,28,44,47,56,68,90,100,101,102,103,104,105,106,107,108,110,112,113,114,115,116,117,118,119,120,121,125,131,132,134,135,136,137,138,139,140,142,146,153,154,155,156,157,158,159,160,161,162,163,278,279,280,281,284,285,287,288,294,302,303,304,305,311,312,313,322],[4,7,8,9,10,13,14,18,19,21,25,28,44,47,56,68,90,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,125,131,132,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,151,152,153,154,155,156,157,158,159,160,161,162,163,277,278,279,280,281,282,284,285,287,288,294,302,303,304,305,306,309,310,312,313,322],[4,5,6,7,10,12,13,18,19,20,21,22,25,26,34,35,44,45,47,48,56,68,90,99,100,101,102,103,104,105,106,108,109,110,111,112,113,125,131,132,133,134,135,136,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,285,286,287,288,292,293,294,310,311,313,314,318,319,322],[4,7,10,13,18,19,21,25,44,47,56,68,90,100,101,102,103,104,105,106,108,110,112,113,125,131,132,134,135,136,138,139,140,142,146,153,154,155,284,285,287,288,294,310,311,312,322],[4,5,6,7,10,12,18,20,21,22,25,26,56,68,90,99,100,101,110,125,131,132,133,144,146,147,148,149,286,288,289,292,294,312,315,319,322],[4,5,6,7,10,12,18,20,21,22,25,26,34,35,56,68,90,99,100,101,110,125,131,132,133,146,147,148,286,289,292,294,314,319,322],[30,32,33,34,35,36,37,38,291,292,293,318,319],[32,34,35,38,290,292,319],[10,18,30,32,33,34,35,36,37,38,288,291,293,312,316],[5,6,10,12,18,20,22,26,32,34,35,38,54,55,56,68,69,70,90,91,98,99,110,124,125,130,133,144,145,146,147,148,149,150,286,288,289,290,291,292,312,314,315,316,317],[10,11,17,18,57,58,59,60,61,63,66,71,72,73,74,75,76,77,78,80,81,83,85,87,104,109,140,141,164,165,166,167,173,174,175,176,186,187,188,189,192,200,201,202,203,205,296,298,299,300,301,324,325,326,327],[10,11,17,18,30,31,32,33,34,35,36,37,38,39,57,60,61,62,66,72,74,75,76,77,78,79,80,81,82,83,84,87,104,109,140,141,167,168,172,173,193,204,228,231,232,233,234,237,238,239,242,243,244,245,248,249,254,260,261,262,263,270,271,275,295],[10,12,13,18,19,20,21,22,25,26,44,45,46,47,48,49,56,68,90,100,102,103,104,105,106,108,109,110,111,112,113,125,132,134,135,136,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,284,285,286,287,288,289,294,310,311,312,313,314,315],[10,11,17,18,104,109,140,141,297,298,325],[296,299,320],[296,297,298,320,323],[57,58,60,61,63,64,65,66,71,72,74,75,76,77,78,80,81,83,85,86,87,88,164,165,166,167,173,174,175,176,186,187,188,189,190,199,200,201,202,203,296,301,320],[296,300,320]]\n"
  },
  {
    "path": "tests/files/json/layer-table.json",
    "content": "{\"taco_taco\":{\"100000\":true,\"100001\":[3,1],\"100002\":true,\"100010\":true,\"100011\":[2,2],\"100012\":true,\"100020\":[2,1],\"100021\":[1,1],\"100022\":[2,1],\"100100\":true,\"100101\":true,\"100102\":[2,1],\"100110\":true,\"100111\":[2,2],\"100112\":[2,1],\"100120\":[1,1],\"100121\":[1,1],\"100122\":false,\"100200\":[5,2],\"100201\":false,\"100202\":true,\"100210\":[1,2],\"100211\":false,\"100212\":[1,2],\"100220\":[2,1],\"100221\":false,\"100222\":[2,1],\"101000\":true,\"101001\":[1,1],\"101002\":true,\"101010\":[3,1],\"101011\":false,\"101012\":[3,1],\"101020\":true,\"101021\":[1,1],\"101022\":[3,2],\"101100\":true,\"101101\":[1,1],\"101102\":[4,1],\"101110\":[5,2],\"101111\":false,\"101112\":true,\"101120\":[1,1],\"101121\":[1,1],\"101122\":false,\"101200\":[4,2],\"101201\":false,\"101202\":[4,2],\"101210\":false,\"101211\":false,\"101212\":false,\"101220\":[5,2],\"101221\":false,\"101222\":true,\"102000\":[4,1],\"102001\":[3,1],\"102002\":[1,2],\"102010\":true,\"102011\":[3,1],\"102012\":[1,2],\"102020\":false,\"102021\":false,\"102022\":false,\"102100\":[4,1],\"102101\":[4,1],\"102102\":false,\"102110\":[5,1],\"102111\":true,\"102112\":false,\"102120\":false,\"102121\":false,\"102122\":false,\"102200\":[1,2],\"102201\":false,\"102202\":[1,2],\"102210\":[1,2],\"102211\":false,\"102212\":[1,2],\"102220\":false,\"102221\":false,\"102222\":false,\"110000\":true,\"110001\":[3,1],\"110002\":[2,1],\"110010\":[3,1],\"110011\":[2,2],\"110012\":[2,1],\"110020\":[2,1],\"110021\":[2,1],\"110022\":[2,1],\"110100\":true,\"110101\":true,\"110102\":[2,1],\"110110\":true,\"110111\":[2,2],\"110112\":[2,1],\"110120\":[2,1],\"110121\":[2,1],\"110122\":false,\"110200\":[2,1],\"110201\":false,\"110202\":[2,1],\"110210\":false,\"110211\":false,\"110212\":false,\"110220\":[2,1],\"110221\":false,\"110222\":[2,1],\"111000\":true,\"111001\":[3,1],\"111002\":true,\"111010\":[3,1],\"111011\":false,\"111012\":[3,1],\"111020\":true,\"111021\":[3,1],\"111022\":[3,2],\"111100\":true,\"111101\":[4,2],\"111102\":[4,1],\"111110\":[5,2],\"111111\":false,\"111112\":true,\"111120\":[5,1],\"111121\":true,\"111122\":false,\"111200\":[4,2],\"111201\":false,\"111202\":[4,2],\"111210\":false,\"111211\":false,\"111212\":false,\"111220\":[5,2],\"111221\":false,\"111222\":true,\"112000\":[3,1],\"112001\":[3,1],\"112002\":false,\"112010\":[3,1],\"112011\":[3,1],\"112012\":false,\"112020\":false,\"112021\":false,\"112022\":false,\"112100\":[4,1],\"112101\":[4,1],\"112102\":false,\"112110\":[5,1],\"112111\":true,\"112112\":false,\"112120\":false,\"112121\":false,\"112122\":false,\"112200\":false,\"112201\":false,\"112202\":false,\"112210\":false,\"112211\":false,\"112212\":false,\"112220\":false,\"112221\":false,\"112222\":false,\"120000\":true,\"120001\":[2,2],\"120002\":true,\"120010\":true,\"120011\":[2,2],\"120012\":true,\"120020\":[2,1],\"120021\":false,\"120022\":[2,1],\"120100\":[4,1],\"120101\":[2,2],\"120102\":[2,1],\"120110\":true,\"120111\":[2,2],\"120112\":[2,1],\"120120\":false,\"120121\":false,\"120122\":false,\"120200\":[5,2],\"120201\":false,\"120202\":true,\"120210\":[2,2],\"120211\":false,\"120212\":[2,2],\"120220\":[2,1],\"120221\":false,\"120222\":[2,1],\"121000\":[5,2],\"121001\":false,\"121002\":true,\"121010\":[3,1],\"121011\":false,\"121012\":[3,1],\"121020\":[3,2],\"121021\":false,\"121022\":[3,2],\"121100\":[4,1],\"121101\":false,\"121102\":[4,1],\"121110\":[5,2],\"121111\":false,\"121112\":true,\"121120\":false,\"121121\":false,\"121122\":false,\"121200\":[4,2],\"121201\":false,\"121202\":[4,2],\"121210\":false,\"121211\":false,\"121212\":false,\"121220\":[5,2],\"121221\":false,\"121222\":true,\"122000\":[4,1],\"122001\":[3,1],\"122002\":[3,2],\"122010\":true,\"122011\":[3,1],\"122012\":[3,2],\"122020\":false,\"122021\":false,\"122022\":false,\"122100\":[4,1],\"122101\":[4,1],\"122102\":false,\"122110\":[5,1],\"122111\":true,\"122112\":false,\"122120\":false,\"122121\":false,\"122122\":false,\"122200\":[4,1],\"122201\":false,\"122202\":[4,1],\"122210\":[5,2],\"122211\":false,\"122212\":true,\"122220\":false,\"122221\":false,\"122222\":false,\"200000\":true,\"200001\":true,\"200002\":[3,2],\"200010\":[2,2],\"200011\":[2,2],\"200012\":[1,2],\"200020\":true,\"200021\":true,\"200022\":[2,1],\"200100\":[5,1],\"200101\":true,\"200102\":false,\"200110\":[2,2],\"200111\":[2,2],\"200112\":false,\"200120\":[1,1],\"200121\":[1,1],\"200122\":false,\"200200\":true,\"200201\":[2,2],\"200202\":true,\"200210\":[1,2],\"200211\":false,\"200212\":[1,2],\"200220\":true,\"200221\":[2,2],\"200222\":[2,1],\"201000\":[4,2],\"201001\":[1,1],\"201002\":[3,2],\"201010\":false,\"201011\":false,\"201012\":false,\"201020\":true,\"201021\":[1,1],\"201022\":[3,2],\"201100\":[1,1],\"201101\":[1,1],\"201102\":false,\"201110\":false,\"201111\":false,\"201112\":false,\"201120\":[1,1],\"201121\":[1,1],\"201122\":false,\"201200\":[4,2],\"201201\":false,\"201202\":[4,2],\"201210\":false,\"201211\":false,\"201212\":false,\"201220\":[5,2],\"201221\":false,\"201222\":true,\"202000\":true,\"202001\":true,\"202002\":[1,2],\"202010\":true,\"202011\":[3,1],\"202012\":[1,2],\"202020\":[3,2],\"202021\":[3,2],\"202022\":false,\"202100\":[4,1],\"202101\":[4,1],\"202102\":false,\"202110\":[5,1],\"202111\":true,\"202112\":false,\"202120\":false,\"202121\":false,\"202122\":false,\"202200\":true,\"202201\":[4,2],\"202202\":[1,2],\"202210\":[1,2],\"202211\":false,\"202212\":[1,2],\"202220\":[5,1],\"202221\":true,\"202222\":false,\"210000\":true,\"210001\":true,\"210002\":[2,1],\"210010\":[2,2],\"210011\":[2,2],\"210012\":false,\"210020\":true,\"210021\":true,\"210022\":[2,1],\"210100\":[5,1],\"210101\":true,\"210102\":false,\"210110\":[2,2],\"210111\":[2,2],\"210112\":false,\"210120\":[2,1],\"210121\":[2,1],\"210122\":false,\"210200\":[4,2],\"210201\":[2,2],\"210202\":[2,1],\"210210\":false,\"210211\":false,\"210212\":false,\"210220\":true,\"210221\":[2,2],\"210222\":[2,1],\"211000\":[4,2],\"211001\":[3,1],\"211002\":[3,2],\"211010\":false,\"211011\":false,\"211012\":false,\"211020\":true,\"211021\":[3,1],\"211022\":[3,2],\"211100\":[4,2],\"211101\":[4,2],\"211102\":false,\"211110\":false,\"211111\":false,\"211112\":false,\"211120\":[5,1],\"211121\":true,\"211122\":false,\"211200\":[4,2],\"211201\":false,\"211202\":[4,2],\"211210\":false,\"211211\":false,\"211212\":false,\"211220\":[5,2],\"211221\":false,\"211222\":true,\"212000\":[5,1],\"212001\":true,\"212002\":false,\"212010\":[3,1],\"212011\":[3,1],\"212012\":false,\"212020\":[3,2],\"212021\":[3,2],\"212022\":false,\"212100\":[4,1],\"212101\":[4,1],\"212102\":false,\"212110\":[5,1],\"212111\":true,\"212112\":false,\"212120\":false,\"212121\":false,\"212122\":false,\"212200\":[4,2],\"212201\":[4,2],\"212202\":false,\"212210\":false,\"212211\":false,\"212212\":false,\"212220\":[5,1],\"212221\":true,\"212222\":false,\"220000\":true,\"220001\":[2,2],\"220002\":[3,2],\"220010\":[2,2],\"220011\":[2,2],\"220012\":[2,2],\"220020\":[3,2],\"220021\":[2,2],\"220022\":[2,1],\"220100\":[2,2],\"220101\":[2,2],\"220102\":false,\"220110\":[2,2],\"220111\":[2,2],\"220112\":false,\"220120\":false,\"220121\":false,\"220122\":false,\"220200\":true,\"220201\":[2,2],\"220202\":true,\"220210\":[2,2],\"220211\":false,\"220212\":[2,2],\"220220\":true,\"220221\":[2,2],\"220222\":[2,1],\"221000\":[3,2],\"221001\":false,\"221002\":[3,2],\"221010\":false,\"221011\":false,\"221012\":false,\"221020\":[3,2],\"221021\":false,\"221022\":[3,2],\"221100\":false,\"221101\":false,\"221102\":false,\"221110\":false,\"221111\":false,\"221112\":false,\"221120\":false,\"221121\":false,\"221122\":false,\"221200\":[4,2],\"221201\":false,\"221202\":[4,2],\"221210\":false,\"221211\":false,\"221212\":false,\"221220\":[5,2],\"221221\":false,\"221222\":true,\"222000\":true,\"222001\":true,\"222002\":[3,2],\"222010\":true,\"222011\":[3,1],\"222012\":[3,2],\"222020\":[3,2],\"222021\":[3,2],\"222022\":false,\"222100\":[4,1],\"222101\":[4,1],\"222102\":false,\"222110\":[5,1],\"222111\":true,\"222112\":false,\"222120\":false,\"222121\":false,\"222122\":false,\"222200\":true,\"222201\":[4,2],\"222202\":[4,1],\"222210\":[5,2],\"222211\":false,\"222212\":true,\"222220\":[5,1],\"222221\":true,\"222222\":false,\"000000\":true,\"000001\":true,\"000002\":true,\"000010\":true,\"000011\":[2,2],\"000012\":true,\"000020\":true,\"000021\":true,\"000022\":[2,1],\"000100\":true,\"000101\":true,\"000102\":[0,1],\"000110\":true,\"000111\":[2,2],\"000112\":[0,1],\"000120\":[1,1],\"000121\":[1,1],\"000122\":false,\"000200\":true,\"000201\":[0,2],\"000202\":true,\"000210\":[1,2],\"000211\":false,\"000212\":[1,2],\"000220\":true,\"000221\":[0,2],\"000222\":[2,1],\"001000\":true,\"001001\":[1,1],\"001002\":true,\"001010\":[0,1],\"001011\":false,\"001012\":[0,1],\"001020\":true,\"001021\":[1,1],\"001022\":[3,2],\"001100\":true,\"001101\":[1,1],\"001102\":[0,1],\"001110\":[0,1],\"001111\":false,\"001112\":[0,1],\"001120\":[1,1],\"001121\":[1,1],\"001122\":false,\"001200\":[4,2],\"001201\":false,\"001202\":[4,2],\"001210\":false,\"001211\":false,\"001212\":false,\"001220\":[5,2],\"001221\":false,\"001222\":true,\"002000\":true,\"002001\":true,\"002002\":[1,2],\"002010\":true,\"002011\":[3,1],\"002012\":[1,2],\"002020\":[0,2],\"002021\":[0,2],\"002022\":false,\"002100\":[4,1],\"002101\":[4,1],\"002102\":false,\"002110\":[5,1],\"002111\":true,\"002112\":false,\"002120\":false,\"002121\":false,\"002122\":false,\"002200\":true,\"002201\":[0,2],\"002202\":[1,2],\"002210\":[1,2],\"002211\":false,\"002212\":[1,2],\"002220\":[0,2],\"002221\":[0,2],\"002222\":false,\"010000\":true,\"010001\":true,\"010002\":[2,1],\"010010\":[3,1],\"010011\":[2,2],\"010012\":[0,1],\"010020\":true,\"010021\":true,\"010022\":[2,1],\"010100\":true,\"010101\":true,\"010102\":[0,1],\"010110\":true,\"010111\":[2,2],\"010112\":[0,1],\"010120\":[2,1],\"010121\":[2,1],\"010122\":false,\"010200\":[4,2],\"010201\":[0,2],\"010202\":[2,1],\"010210\":false,\"010211\":false,\"010212\":false,\"010220\":true,\"010221\":[0,2],\"010222\":[2,1],\"011000\":true,\"011001\":[3,1],\"011002\":true,\"011010\":[0,1],\"011011\":false,\"011012\":[0,1],\"011020\":true,\"011021\":[3,1],\"011022\":[3,2],\"011100\":true,\"011101\":[4,2],\"011102\":[0,1],\"011110\":[0,1],\"011111\":false,\"011112\":[0,1],\"011120\":[5,1],\"011121\":true,\"011122\":false,\"011200\":[4,2],\"011201\":false,\"011202\":[4,2],\"011210\":false,\"011211\":false,\"011212\":false,\"011220\":[5,2],\"011221\":false,\"011222\":true,\"012000\":[5,1],\"012001\":true,\"012002\":false,\"012010\":[3,1],\"012011\":[3,1],\"012012\":false,\"012020\":[0,2],\"012021\":[0,2],\"012022\":false,\"012100\":[4,1],\"012101\":[4,1],\"012102\":false,\"012110\":[5,1],\"012111\":true,\"012112\":false,\"012120\":false,\"012121\":false,\"012122\":false,\"012200\":[0,2],\"012201\":[0,2],\"012202\":false,\"012210\":false,\"012211\":false,\"012212\":false,\"012220\":[0,2],\"012221\":[0,2],\"012222\":false,\"020000\":true,\"020001\":[2,2],\"020002\":true,\"020010\":true,\"020011\":[2,2],\"020012\":true,\"020020\":[3,2],\"020021\":[0,2],\"020022\":[2,1],\"020100\":[4,1],\"020101\":[2,2],\"020102\":[0,1],\"020110\":true,\"020111\":[2,2],\"020112\":[0,1],\"020120\":false,\"020121\":false,\"020122\":false,\"020200\":true,\"020201\":[0,2],\"020202\":true,\"020210\":[2,2],\"020211\":false,\"020212\":[2,2],\"020220\":true,\"020221\":[0,2],\"020222\":[2,1],\"021000\":[5,2],\"021001\":false,\"021002\":true,\"021010\":[0,1],\"021011\":false,\"021012\":[0,1],\"021020\":[3,2],\"021021\":false,\"021022\":[3,2],\"021100\":[0,1],\"021101\":false,\"021102\":[0,1],\"021110\":[0,1],\"021111\":false,\"021112\":[0,1],\"021120\":false,\"021121\":false,\"021122\":false,\"021200\":[4,2],\"021201\":false,\"021202\":[4,2],\"021210\":false,\"021211\":false,\"021212\":false,\"021220\":[5,2],\"021221\":false,\"021222\":true,\"022000\":true,\"022001\":true,\"022002\":[3,2],\"022010\":true,\"022011\":[3,1],\"022012\":[3,2],\"022020\":[0,2],\"022021\":[0,2],\"022022\":false,\"022100\":[4,1],\"022101\":[4,1],\"022102\":false,\"022110\":[5,1],\"022111\":true,\"022112\":false,\"022120\":false,\"022121\":false,\"022122\":false,\"022200\":true,\"022201\":[0,2],\"022202\":[4,1],\"022210\":[5,2],\"022211\":false,\"022212\":true,\"022220\":[0,2],\"022221\":[0,2],\"022222\":false},\"taco_tortilla\":{\"100\":true,\"101\":[1,2],\"102\":[1,1],\"110\":[2,2],\"111\":false,\"112\":true,\"120\":[2,1],\"121\":true,\"122\":false,\"200\":true,\"201\":[1,2],\"202\":[1,1],\"210\":[2,2],\"211\":false,\"212\":true,\"220\":[2,1],\"221\":true,\"222\":false,\"000\":true,\"001\":[1,2],\"002\":[1,1],\"010\":[2,2],\"011\":false,\"012\":true,\"020\":[2,1],\"021\":true,\"022\":false},\"tortilla_tortilla\":{\"10\":[1,1],\"11\":true,\"12\":false,\"20\":[1,2],\"21\":false,\"22\":true,\"00\":true,\"01\":[0,1],\"02\":[0,2]},\"transitivity\":{\"100\":true,\"101\":[1,2],\"102\":true,\"110\":[2,2],\"111\":false,\"112\":true,\"120\":true,\"121\":true,\"122\":true,\"200\":true,\"201\":true,\"202\":[1,1],\"210\":true,\"211\":true,\"212\":true,\"220\":[2,1],\"221\":true,\"222\":false,\"000\":true,\"001\":true,\"002\":true,\"010\":true,\"011\":[0,2],\"012\":true,\"020\":true,\"021\":true,\"022\":[0,1]}}\n"
  },
  {
    "path": "tests/files/json/maze-u-constraints.json",
    "content": "{\n\t\"constraints\":{\n\t\t\"taco_taco\":[\n\t\t\t[19,11,25,16],[24,22,27,26],[0,19,25,20],[20,0,24,27],[37,39,38,40],[36,38,51,40],[40,39,43,48],[40,42,43,51],[39,42,48,51],[54,57,55,58],[51,54,52,57],[41,58,57,62],[41,42,57,51],[58,42,62,51],[76,67,81,73],[80,79,83,82],[1,75,81,76],[75,1,80,83],[55,58,56,59],[91,56,101,59],[59,58,94,62],[59,93,94,101],[58,93,62,101],[103,105,104,106],[101,103,102,105],[92,106,105,109],[92,93,105,101],[106,93,109,101],[12,7,31,8],[12,15,31,34],[7,15,8,34],[12,14,13,15],[8,32,9,34],[8,9,34,32],[8,12,34,15],[9,12,32,15],[46,60,47,61],[44,45,49,50],[44,46,47,49],[44,45,47,60],[44,50,47,61],[46,45,49,60],[46,50,49,61],[45,50,60,61],[31,7,49,50],[31,44,49,45],[7,44,50,45],[77,84,78,85],[97,107,98,108],[95,96,99,100],[95,97,98,99],[95,96,98,107],[95,100,98,108],[97,96,99,107],[97,100,99,108],[96,100,107,108],[64,87,99,100],[64,95,99,96],[87,95,100,96],[21,28,23,29],[33,10,69,65],[33,68,69,89],[10,68,65,89],[69,68,70,71],[65,88,66,89],[65,66,89,88],[65,68,89,69],[66,68,88,69]\n\t\t],\n\t\t\"taco_tortilla\":[\n\t\t\t[4,11,5],[4,16,5],[4,17,5],[5,4,11],[16,4,17],[16,5,17],[16,11,17],[17,6,18],[17,0,20],[17,11,20],[17,16,20],[17,19,20],[17,22,20],[17,24,20],[17,25,20],[17,26,20],[17,27,20],[17,26,22],[17,30,22],[17,35,22],[19,0,25],[19,17,25],[19,20,25],[24,0,27],[24,17,27],[24,20,27],[26,35,30],[0,11,25],[0,16,25],[0,17,25],[20,17,24],[20,22,24],[20,26,24],[19,11,20],[19,16,20],[19,17,20],[0,17,27],[0,22,27],[0,26,27],[30,17,35],[30,22,35],[30,26,35],[22,0,26],[22,17,26],[22,20,26],[11,0,16],[11,17,16],[11,20,16],[3,51,36],[3,36,37],[3,38,37],[3,39,37],[3,40,37],[3,51,37],[37,3,38],[37,36,38],[37,51,38],[36,4,38],[36,39,38],[36,40,38],[36,48,38],[36,51,38],[42,4,43],[42,39,43],[42,40,43],[42,48,43],[42,51,43],[39,3,40],[39,36,40],[39,51,40],[40,4,43],[39,4,48],[52,51,53],[53,51,55],[53,52,55],[53,54,55],[53,57,55],[53,58,55],[54,51,55],[54,52,55],[54,53,55],[52,6,54],[52,51,54],[52,57,54],[52,58,54],[52,62,54],[41,6,42],[41,51,42],[41,57,42],[41,58,42],[41,62,42],[57,51,58],[57,52,58],[57,53,58],[51,6,52],[51,53,52],[51,55,52],[51,58,52],[51,62,52],[41,6,57],[58,6,62],[42,4,51],[42,6,51],[54,6,57],[54,53,57],[54,55,57],[54,58,57],[54,62,57],[4,36,48],[4,38,48],[4,39,48],[4,40,48],[4,51,48],[6,51,62],[6,52,62],[6,54,62],[6,57,62],[6,58,62],[35,67,63],[35,73,63],[35,74,63],[63,35,67],[73,35,74],[73,63,74],[73,67,74],[72,6,74],[74,1,75],[74,67,75],[74,73,75],[74,76,75],[74,79,75],[74,80,75],[74,81,75],[74,82,75],[74,83,75],[74,82,79],[74,86,79],[74,90,79],[76,1,81],[76,74,81],[76,75,81],[80,1,83],[80,74,83],[80,75,83],[82,90,86],[1,67,81],[1,73,81],[1,74,81],[75,74,80],[75,79,80],[75,82,80],[75,67,76],[75,73,76],[75,74,76],[1,74,83],[1,79,83],[1,82,83],[86,74,90],[86,79,90],[86,82,90],[79,1,82],[79,74,82],[79,75,82],[67,1,73],[67,74,73],[67,75,73],[53,101,91],[53,56,55],[53,58,55],[53,59,55],[53,91,55],[53,101,55],[55,53,56],[55,91,56],[55,101,56],[56,6,91],[56,58,91],[56,59,91],[56,62,91],[56,101,91],[93,6,94],[93,58,94],[93,59,94],[93,62,94],[93,101,94],[58,53,59],[58,91,59],[58,101,59],[91,6,101],[91,53,101],[91,55,101],[91,58,101],[91,62,101],[59,6,94],[58,6,62],[2,101,102],[2,101,104],[2,102,104],[2,103,104],[2,105,104],[2,106,104],[103,2,104],[103,101,104],[103,102,104],[102,90,103],[102,101,103],[102,105,103],[102,106,103],[102,109,103],[92,90,93],[92,101,93],[92,105,93],[92,106,93],[92,109,93],[105,2,106],[105,101,106],[105,102,106],[92,90,105],[106,90,109],[56,6,59],[56,53,59],[56,55,59],[56,58,59],[56,62,59],[93,6,101],[93,90,101],[6,56,62],[6,58,62],[6,59,62],[6,91,62],[6,101,62],[90,101,109],[90,102,109],[90,103,109],[90,105,109],[90,106,109],[12,7,13],[12,31,13],[14,7,15],[14,31,15],[8,7,9],[8,12,9],[8,15,9],[8,31,9],[32,7,34],[32,12,34],[32,15,34],[32,31,34],[8,7,34],[8,31,34],[9,7,32],[9,31,32],[12,7,15],[12,31,15],[13,7,14],[13,12,14],[13,15,14],[13,31,14],[46,7,47],[46,31,47],[46,44,47],[46,45,47],[46,49,47],[46,50,47],[44,7,49],[44,31,49],[44,7,47],[44,31,47],[46,7,49],[46,31,49],[60,7,61],[60,31,61],[60,44,61],[60,45,61],[60,49,61],[60,50,61],[45,7,50],[45,31,50],[45,7,60],[45,31,60],[50,7,61],[50,31,61],[77,64,78],[77,87,78],[84,64,85],[84,87,85],[78,64,84],[78,87,84],[77,64,85],[77,78,85],[77,84,85],[77,87,85],[97,64,98],[97,87,98],[97,95,98],[97,96,98],[97,99,98],[97,100,98],[95,64,99],[95,87,99],[95,64,98],[95,87,98],[97,64,99],[97,87,99],[107,64,108],[107,87,108],[107,95,108],[107,96,108],[107,99,108],[107,100,108],[96,64,100],[96,87,100],[96,64,107],[96,87,107],[100,64,108],[100,87,108],[21,10,23],[21,33,23],[28,10,29],[28,33,29],[21,10,29],[21,33,29],[23,10,28],[23,21,28],[23,29,28],[23,33,28],[69,10,70],[69,33,70],[68,10,71],[68,33,71],[65,10,66],[65,33,66],[65,68,66],[65,69,66],[88,10,89],[88,33,89],[88,68,89],[88,69,89],[65,10,89],[65,33,89],[66,10,88],[66,33,88],[68,10,69],[68,33,69],[70,10,71],[70,33,71],[70,68,71],[70,69,71],[12,32,31],[12,9,31],[7,32,8],[7,9,8],[15,32,34],[29,21,33],[15,9,34],[29,10,33],[33,88,69],[33,66,69],[10,88,65],[10,66,65],[68,88,89],[84,78,87],[68,66,89],[84,64,87]\n\t\t],\n\t\t\"tortilla_tortilla\":[\n\t\t\t[6,10,18,21],[4,12,5,31],[4,13,11,31],[4,14,16,31],[5,13,11,12],[5,14,16,12],[11,14,16,13],[15,18,7,6],[21,32,10,9],[22,28,26,23],[22,29,30,23],[22,33,35,23],[26,29,30,28],[26,33,35,28],[29,35,33,30],[40,46,39,47],[40,49,48,47],[40,31,4,47],[40,44,43,47],[39,49,48,46],[39,31,4,46],[39,44,43,46],[48,31,4,49],[48,44,43,49],[4,44,43,31],[57,60,58,61],[57,60,62,50],[57,60,6,7],[57,60,41,45],[58,61,62,50],[58,61,6,7],[58,61,41,45],[50,62,7,6],[50,62,45,41],[6,7,41,45],[6,78,72,64],[33,63,69,35],[33,67,70,35],[33,73,71,35],[63,70,67,69],[63,71,73,69],[67,71,73,70],[68,72,10,6],[78,66,64,88],[77,79,85,82],[77,79,84,86],[77,79,87,90],[82,85,86,84],[82,85,90,87],[84,86,87,90],[59,97,58,98],[59,99,62,98],[59,64,6,98],[59,95,94,98],[58,99,62,97],[58,64,6,97],[58,95,94,97],[62,64,6,99],[62,95,94,99],[6,95,94,64],[105,107,106,108],[105,107,109,100],[105,107,90,87],[105,107,92,96],[106,108,109,100],[106,108,90,87],[106,108,92,96],[100,109,87,90],[100,109,96,92],[87,90,96,92]\n\t\t],\n\t\t\"transitivity\":[\n\t\t\t[0,11,17],[0,11,19],[0,11,20],[0,16,17],[0,16,19],[0,16,20],[0,17,19],[0,17,22],[0,17,24],[0,17,26],[0,20,22],[0,20,26],[0,22,24],[0,24,26],[1,67,74],[1,67,75],[1,67,76],[1,73,74],[1,73,75],[1,73,76],[1,74,76],[1,74,79],[1,74,80],[1,74,82],[1,75,79],[1,75,82],[1,79,80],[1,80,82],[2,101,103],[2,101,105],[2,101,106],[2,102,103],[2,102,105],[2,102,106],[2,103,105],[2,103,106],[3,36,38],[3,36,39],[3,36,40],[3,38,39],[3,38,40],[3,38,51],[3,39,51],[3,40,51],[4,11,16],[4,11,17],[4,36,39],[4,36,40],[4,36,51],[4,38,39],[4,38,40],[4,38,51],[4,39,40],[4,39,42],[4,39,43],[4,39,51],[4,40,42],[4,40,51],[4,42,48],[4,43,48],[4,43,51],[5,11,16],[5,11,17],[6,41,51],[6,41,58],[6,41,62],[6,42,57],[6,42,58],[6,42,62],[6,51,54],[6,51,57],[6,51,58],[6,52,57],[6,52,58],[6,54,58],[6,56,58],[6,56,101],[6,57,58],[6,58,59],[6,58,91],[6,58,93],[6,58,94],[6,58,101],[6,59,91],[6,59,93],[6,59,101],[6,62,93],[6,62,94],[6,94,101],[7,8,32],[7,9,12],[7,9,15],[7,9,31],[7,9,34],[7,12,14],[7,12,32],[7,12,34],[7,13,15],[7,13,31],[7,14,31],[7,15,31],[7,15,32],[7,31,32],[7,31,34],[7,31,44],[7,31,45],[7,31,46],[7,31,47],[7,31,60],[7,31,61],[7,44,46],[7,44,60],[7,44,61],[7,45,46],[7,45,47],[7,45,49],[7,45,61],[7,46,50],[7,46,60],[7,46,61],[7,47,49],[7,47,50],[7,47,60],[7,47,61],[7,49,60],[7,49,61],[7,50,60],[8,12,32],[8,15,31],[8,15,32],[8,31,32],[9,12,31],[9,12,34],[9,15,31],[9,15,34],[9,31,34],[10,21,28],[10,21,33],[10,23,29],[10,23,33],[10,28,33],[10,29,33],[10,33,66],[10,33,68],[10,33,70],[10,33,71],[10,33,88],[10,33,89],[10,65,88],[10,66,68],[10,66,69],[10,66,89],[10,68,70],[10,68,88],[10,69,71],[10,69,88],[10,69,89],[11,17,19],[11,17,25],[11,20,25],[12,14,31],[12,31,32],[13,15,31],[15,31,32],[16,17,19],[16,17,25],[16,20,25],[17,22,24],[17,22,27],[17,24,26],[17,26,27],[17,26,30],[17,26,35],[20,22,27],[20,26,27],[21,28,33],[22,26,30],[22,26,35],[23,29,33],[31,44,46],[31,44,50],[31,44,60],[31,44,61],[31,45,46],[31,45,47],[31,45,61],[31,46,50],[31,46,60],[31,46,61],[31,47,49],[31,47,50],[31,47,60],[31,47,61],[31,49,60],[31,49,61],[31,50,60],[33,65,68],[33,65,88],[33,66,68],[33,66,69],[33,66,89],[33,68,70],[33,68,88],[33,69,71],[33,69,88],[35,67,73],[35,67,74],[36,37,39],[36,37,40],[36,37,51],[36,39,48],[36,39,51],[36,40,48],[36,48,51],[37,39,51],[37,40,51],[38,39,48],[38,39,51],[38,40,48],[38,48,51],[39,40,42],[39,43,51],[40,42,48],[40,48,51],[41,51,58],[41,51,62],[42,57,58],[42,57,62],[43,48,51],[44,45,46],[44,45,61],[44,46,50],[44,46,60],[44,46,61],[44,49,60],[44,49,61],[44,50,60],[45,46,50],[45,46,61],[45,47,49],[45,47,50],[45,47,61],[45,49,61],[46,50,60],[47,49,50],[47,49,60],[47,49,61],[47,50,60],[49,50,60],[51,53,54],[51,53,57],[51,53,58],[51,54,58],[51,54,62],[51,55,57],[51,55,58],[51,57,62],[52,53,54],[52,53,57],[52,53,58],[52,55,57],[52,55,58],[52,57,62],[52,58,62],[53,54,58],[53,56,58],[53,56,91],[53,56,101],[53,58,91],[53,58,101],[53,59,91],[53,59,101],[54,58,62],[55,58,91],[55,58,101],[55,59,91],[55,59,101],[56,58,62],[56,58,101],[56,62,101],[58,59,93],[58,62,91],[58,94,101],[59,62,91],[59,62,93],[59,62,101],[62,94,101],[63,67,73],[63,67,74],[64,77,84],[64,77,87],[64,78,85],[64,78,87],[64,84,87],[64,85,87],[64,87,95],[64,87,96],[64,87,97],[64,87,98],[64,87,107],[64,87,108],[64,95,97],[64,95,100],[64,95,107],[64,95,108],[64,96,97],[64,96,98],[64,96,108],[64,97,100],[64,97,107],[64,97,108],[64,98,99],[64,98,100],[64,98,107],[64,98,108],[64,99,107],[64,99,108],[64,100,107],[65,68,88],[65,69,88],[66,68,89],[66,69,89],[67,74,76],[67,74,81],[67,75,81],[73,74,76],[73,74,81],[73,75,81],[74,79,80],[74,79,83],[74,80,82],[74,82,83],[74,82,86],[74,82,90],[75,79,83],[75,82,83],[77,84,87],[78,85,87],[79,82,86],[79,82,90],[87,95,97],[87,95,107],[87,95,108],[87,96,97],[87,96,98],[87,96,99],[87,96,108],[87,97,100],[87,97,107],[87,97,108],[87,98,99],[87,98,100],[87,98,107],[87,98,108],[87,99,107],[87,99,108],[87,100,107],[90,92,101],[90,92,106],[90,92,109],[90,93,105],[90,93,106],[90,93,109],[90,101,102],[90,101,103],[90,101,105],[90,101,106],[90,102,105],[90,102,106],[90,103,105],[90,103,106],[90,105,106],[92,101,106],[92,101,109],[93,105,106],[93,105,109],[95,96,97],[95,96,108],[95,97,100],[95,97,107],[95,97,108],[95,99,107],[95,99,108],[95,100,107],[96,97,100],[96,97,108],[96,98,99],[96,98,100],[96,98,108],[96,99,108],[97,100,107],[98,99,100],[98,99,107],[98,99,108],[98,100,107],[99,100,107],[101,102,104],[101,102,106],[101,102,109],[101,103,106],[101,103,109],[101,104,105],[101,104,106],[101,105,109],[102,104,105],[102,104,106],[102,105,109],[102,106,109],[103,105,109],[103,106,109]\n\t\t]\n\t},\n\t\"facePairs\":[\n\t\t\"0 16\",\"0 17\",\"0 20\",\"0 11\",\"0 19\",\"0 22\",\"0 24\",\"0 26\",\"0 27\",\"0 25\",\"1 79\",\"1 82\",\"1 75\",\"1 76\",\"1 80\",\"1 81\",\"1 83\",\"1 67\",\"1 73\",\"1 74\",\"2 102\",\"2 103\",\"2 104\",\"2 105\",\"2 106\",\"2 101\",\"3 37\",\"3 36\",\"3 39\",\"3 38\",\"3 40\",\"3 51\",\"4 5\",\"4 16\",\"4 17\",\"4 11\",\"4 36\",\"4 39\",\"4 48\",\"4 38\",\"4 40\",\"4 42\",\"4 43\",\"4 51\",\"5 16\",\"5 17\",\"5 11\",\"6 18\",\"6 52\",\"6 54\",\"6 57\",\"6 41\",\"6 58\",\"6 62\",\"6 17\",\"6 42\",\"6 51\",\"6 72\",\"6 74\",\"6 91\",\"6 56\",\"6 59\",\"6 93\",\"6 94\",\"6 101\",\"7 9\",\"7 32\",\"7 8\",\"7 12\",\"7 15\",\"7 34\",\"7 31\",\"7 44\",\"7 45\",\"7 49\",\"7 50\",\"7 13\",\"7 14\",\"7 46\",\"7 47\",\"7 60\",\"7 61\",\"8 9\",\"8 32\",\"8 12\",\"8 15\",\"8 34\",\"8 31\",\"9 32\",\"9 12\",\"9 15\",\"9 34\",\"9 31\",\"10 29\",\"10 21\",\"10 23\",\"10 28\",\"10 33\",\"10 68\",\"10 71\",\"10 69\",\"10 70\",\"10 65\",\"10 66\",\"10 88\",\"10 89\",\"11 16\",\"11 17\",\"11 20\",\"11 19\",\"11 25\",\"12 32\",\"12 15\",\"12 34\",\"12 31\",\"12 13\",\"12 14\",\"13 14\",\"13 15\",\"13 31\",\"14 15\",\"14 31\",\"15 32\",\"15 34\",\"15 31\",\"16 17\",\"16 20\",\"16 19\",\"16 25\",\"17 20\",\"17 19\",\"17 22\",\"17 24\",\"17 26\",\"17 27\",\"17 25\",\"17 18\",\"17 30\",\"17 35\",\"19 20\",\"19 25\",\"20 22\",\"20 24\",\"20 26\",\"20 27\",\"20 25\",\"21 29\",\"21 23\",\"21 28\",\"21 33\",\"22 24\",\"22 26\",\"22 27\",\"22 30\",\"22 35\",\"23 29\",\"23 28\",\"23 33\",\"24 26\",\"24 27\",\"26 27\",\"26 30\",\"26 35\",\"28 29\",\"28 33\",\"29 33\",\"30 35\",\"31 32\",\"31 34\",\"31 44\",\"31 45\",\"31 49\",\"31 50\",\"31 46\",\"31 47\",\"31 60\",\"31 61\",\"32 34\",\"33 68\",\"33 71\",\"33 69\",\"33 70\",\"33 65\",\"33 66\",\"33 88\",\"33 89\",\"35 73\",\"35 74\",\"35 63\",\"35 67\",\"36 37\",\"36 39\",\"36 48\",\"36 38\",\"36 40\",\"36 51\",\"37 39\",\"37 38\",\"37 40\",\"37 51\",\"38 39\",\"38 48\",\"38 40\",\"38 51\",\"39 48\",\"39 40\",\"39 42\",\"39 43\",\"39 51\",\"40 48\",\"40 42\",\"40 43\",\"40 51\",\"41 57\",\"41 58\",\"41 62\",\"41 42\",\"41 51\",\"42 48\",\"42 43\",\"42 51\",\"42 57\",\"42 58\",\"42 62\",\"43 48\",\"43 51\",\"44 45\",\"44 49\",\"44 50\",\"44 46\",\"44 47\",\"44 60\",\"44 61\",\"45 49\",\"45 50\",\"45 46\",\"45 47\",\"45 60\",\"45 61\",\"46 47\",\"46 60\",\"46 61\",\"46 49\",\"46 50\",\"47 60\",\"47 61\",\"47 49\",\"47 50\",\"48 51\",\"49 50\",\"49 60\",\"49 61\",\"50 60\",\"50 61\",\"51 52\",\"51 53\",\"51 54\",\"51 55\",\"51 57\",\"51 58\",\"51 62\",\"52 53\",\"52 54\",\"52 55\",\"52 57\",\"52 58\",\"52 62\",\"53 54\",\"53 55\",\"53 57\",\"53 58\",\"53 91\",\"53 56\",\"53 59\",\"53 101\",\"54 55\",\"54 57\",\"54 58\",\"54 62\",\"55 57\",\"55 58\",\"55 91\",\"55 56\",\"55 59\",\"55 101\",\"56 91\",\"56 59\",\"56 101\",\"56 58\",\"56 62\",\"57 58\",\"57 62\",\"58 62\",\"58 91\",\"58 59\",\"58 93\",\"58 94\",\"58 101\",\"59 91\",\"59 93\",\"59 94\",\"59 101\",\"59 62\",\"60 61\",\"62 91\",\"62 93\",\"62 94\",\"62 101\",\"63 73\",\"63 74\",\"63 67\",\"64 78\",\"64 84\",\"64 87\",\"64 95\",\"64 96\",\"64 99\",\"64 100\",\"64 77\",\"64 85\",\"64 97\",\"64 98\",\"64 107\",\"64 108\",\"65 68\",\"65 69\",\"65 66\",\"65 88\",\"65 89\",\"66 68\",\"66 69\",\"66 88\",\"66 89\",\"67 73\",\"67 74\",\"67 75\",\"67 76\",\"67 81\",\"68 71\",\"68 69\",\"68 70\",\"68 88\",\"68 89\",\"69 71\",\"69 70\",\"69 88\",\"69 89\",\"70 71\",\"72 74\",\"73 74\",\"73 75\",\"73 76\",\"73 81\",\"74 79\",\"74 82\",\"74 86\",\"74 90\",\"74 75\",\"74 76\",\"74 80\",\"74 81\",\"74 83\",\"75 79\",\"75 82\",\"75 76\",\"75 80\",\"75 81\",\"75 83\",\"76 81\",\"77 85\",\"77 78\",\"77 84\",\"77 87\",\"78 84\",\"78 87\",\"78 85\",\"79 82\",\"79 86\",\"79 90\",\"79 80\",\"79 83\",\"80 82\",\"80 83\",\"82 86\",\"82 90\",\"82 83\",\"84 87\",\"84 85\",\"85 87\",\"86 90\",\"87 95\",\"87 96\",\"87 99\",\"87 100\",\"87 97\",\"87 98\",\"87 107\",\"87 108\",\"88 89\",\"90 102\",\"90 103\",\"90 105\",\"90 92\",\"90 106\",\"90 109\",\"90 93\",\"90 101\",\"91 101\",\"92 105\",\"92 106\",\"92 109\",\"92 93\",\"92 101\",\"93 94\",\"93 101\",\"93 105\",\"93 106\",\"93 109\",\"94 101\",\"95 96\",\"95 99\",\"95 100\",\"95 97\",\"95 98\",\"95 107\",\"95 108\",\"96 99\",\"96 100\",\"96 97\",\"96 98\",\"96 107\",\"96 108\",\"97 98\",\"97 107\",\"97 108\",\"97 99\",\"97 100\",\"98 107\",\"98 108\",\"98 99\",\"98 100\",\"99 100\",\"99 107\",\"99 108\",\"100 107\",\"100 108\",\"101 102\",\"101 103\",\"101 104\",\"101 105\",\"101 106\",\"101 109\",\"102 103\",\"102 104\",\"102 105\",\"102 106\",\"102 109\",\"103 104\",\"103 105\",\"103 106\",\"103 109\",\"104 105\",\"104 106\",\"105 106\",\"105 109\",\"106 109\",\"107 108\"\n\t],\n\t\"faces_winding\":[\n\t\ttrue,true,true,true,true,false,true,true,false,true,true,true,true,false,true,false,false,true,false,true,false,false,false,true,true,false,true,false,false,true,false,false,false,false,true,true,false,false,true,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,true,false,true,false,true,false,false,true,false,false,false,false,true,true,false,true,false,true,false,false,true,false,true,false,true,false,true,false,true,false,false,true,false,true,false,true,true,false,true,false,true,false,true,false,true,true,false,true,false,true,false,false,true,false,true,false\n\t],\n\t\"orders\":{\n\t\t\"10 23\":1,\"10 28\":1,\"10 29\":1,\"10 33\":1,\"15 31\":1,\"7 31\":1,\"12 15\":2,\"7 12\":1,\"13 15\":2,\"7 13\":1,\"14 15\":2,\"7 14\":1,\"21 23\":1,\"21 28\":1,\"21 29\":1,\"21 33\":1,\"47 60\":2,\"47 61\":2,\"47 50\":2,\"7 47\":1,\"45 47\":1,\"46 60\":2,\"46 61\":2,\"46 50\":2,\"7 46\":1,\"45 46\":1,\"49 60\":2,\"49 61\":2,\"49 50\":2,\"7 49\":1,\"45 49\":1,\"31 60\":2,\"31 61\":2,\"31 50\":2,\"31 45\":2,\"44 60\":2,\"44 61\":2,\"44 50\":2,\"7 44\":1,\"44 45\":2,\"64 77\":2,\"64 85\":2,\"64 84\":2,\"64 87\":2,\"33 68\":2,\"68 69\":1,\"10 69\":1,\"68 70\":1,\"10 70\":1,\"68 71\":1,\"10 71\":1,\"77 78\":1,\"78 85\":2,\"78 84\":2,\"78 87\":2,\"98 107\":2,\"98 108\":2,\"98 100\":2,\"87 98\":1,\"96 98\":1,\"97 107\":2,\"97 108\":2,\"97 100\":2,\"87 97\":1,\"96 97\":1,\"99 107\":2,\"99 108\":2,\"99 100\":2,\"87 99\":1,\"96 99\":1,\"64 107\":2,\"64 108\":2,\"64 100\":2,\"64 96\":2,\"95 107\":2,\"95 108\":2,\"95 100\":2,\"87 95\":1,\"95 96\":2,\"4 5\":1,\"5 11\":1,\"16 17\":1,\"17 18\":2,\"17 20\":2,\"17 22\":2,\"16 19\":1,\"22 24\":1,\"19 25\":2,\"24 27\":2,\"26 30\":2,\"0 25\":1,\"20 24\":2,\"19 20\":1,\"0 27\":1,\"30 35\":2,\"22 26\":2,\"11 16\":1,\"3 36\":1,\"3 37\":1,\"37 38\":1,\"36 38\":2,\"42 43\":2,\"39 40\":1,\"36 51\":1,\"40 43\":1,\"39 48\":2,\"52 53\":2,\"53 55\":1,\"54 55\":2,\"52 54\":2,\"41 42\":1,\"57 58\":2,\"51 52\":2,\"41 57\":2,\"58 62\":2,\"38 40\":2,\"42 51\":1,\"54 57\":2,\"4 48\":1,\"6 62\":1,\"35 63\":1,\"63 67\":1,\"73 74\":1,\"72 74\":1,\"74 75\":2,\"74 79\":2,\"73 76\":1,\"79 80\":1,\"76 81\":2,\"80 83\":2,\"82 86\":2,\"1 81\":1,\"75 80\":2,\"75 76\":2,\"1 83\":1,\"86 90\":2,\"79 82\":2,\"67 73\":1,\"53 91\":1,\"55 56\":1,\"56 91\":1,\"93 94\":2,\"58 59\":1,\"91 101\":1,\"59 94\":1,\"2 102\":1,\"2 104\":1,\"103 104\":2,\"102 103\":2,\"92 93\":1,\"105 106\":2,\"101 102\":2,\"92 105\":2,\"106 109\":2,\"56 59\":2,\"93 101\":1,\"103 105\":2,\"90 109\":1,\"12 31\":1,\"7 8\":1,\"12 13\":2,\"15 34\":2,\"8 9\":1,\"32 34\":1,\"8 34\":1,\"9 32\":1,\"13 14\":2,\"46 47\":2,\"44 49\":1,\"44 47\":1,\"46 49\":1,\"60 61\":2,\"45 50\":2,\"45 60\":2,\"50 61\":1,\"31 49\":2,\"7 50\":1,\"84 85\":1,\"84 87\":2,\"77 85\":2,\"97 98\":2,\"95 99\":1,\"95 98\":1,\"97 99\":1,\"107 108\":2,\"96 100\":2,\"96 107\":2,\"100 108\":1,\"64 99\":2,\"87 100\":1,\"28 29\":1,\"29 33\":1,\"23 28\":1,\"33 69\":2,\"10 65\":1,\"69 70\":2,\"68 89\":2,\"65 66\":1,\"88 89\":1,\"65 89\":1,\"66 88\":1,\"70 71\":2\n\t}\n}\n"
  },
  {
    "path": "tests/files/json/panels-3x3-layer-solver-no-depth.json",
    "content": "{\n\t\"root\": {\n\t\t\"2 5\": 1,\n\t\t\"1 6\": 1,\n\t\t\"0 4\": 1,\n\t\t\"3 7\": 2,\n\t\t\"4 6\": 1,\n\t\t\"0 1\": 2,\n\t\t\"3 4\": 1,\n\t\t\"6 7\": 2,\n\t\t\"5 8\": 2,\n\t\t\"7 8\": 1,\n\t\t\"5 6\": 1,\n\t\t\"1 2\": 1,\n\t\t\"1 5\": 1,\n\t\t\"2 6\": 1,\n\t\t\"0 6\": 1,\n\t\t\"1 4\": 1,\n\t\t\"4 7\": 2,\n\t\t\"3 6\": 1,\n\t\t\"5 7\": 2,\n\t\t\"6 8\": 2\n\t},\n\t\"branches\":[[\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 5\":1,\"2 4\":2,\"3 8\":1,\"3 5\":1,\"2 3\":2,\"2 7\":2,\"4 8\":1,\"0 7\":2,\"0 3\":2,\"1 7\":2,\"2 8\":1,\"1 3\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 5\":1,\"2 4\":2,\"3 8\":1,\"3 5\":1,\"2 3\":2,\"2 7\":2,\"4 8\":1,\"1 3\":1,\"0 7\":2,\"2 8\":1,\"0 3\":1,\"1 7\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 5\":1,\"2 4\":2,\"3 8\":1,\"3 5\":1,\"2 3\":2,\"2 7\":2,\"4 8\":1,\"1 3\":1,\"2 8\":2,\"1 7\":1,\"0 3\":1,\"0 7\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 5\":1,\"2 4\":2,\"3 8\":1,\"3 5\":1,\"2 3\":2,\"2 7\":2,\"4 8\":1,\"1 3\":1,\"2 8\":2,\"1 7\":1,\"0 3\":2,\"0 7\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"0 7\":2,\"2 3\":2,\"3 8\":1,\"0 3\":2,\"4 5\":2,\"2 7\":2,\"1 7\":2,\"2 4\":1,\"2 8\":1,\"1 3\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"1 7\":1,\"4 5\":1,\"0 3\":1,\"2 4\":2,\"0 7\":1,\"2 3\":2,\"2 7\":2,\"2 8\":2,\"3 8\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"1 7\":1,\"4 5\":2,\"2 4\":1,\"3 8\":1,\"2 7\":1,\"0 7\":1,\"2 3\":1,\"0 3\":1,\"2 8\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"1 7\":1,\"4 5\":2,\"2 4\":1,\"3 8\":1,\"2 7\":2,\"2 3\":2,\"2 8\":2,\"0 3\":1,\"0 7\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"1 7\":1,\"4 5\":2,\"2 4\":1,\"3 8\":1,\"2 7\":2,\"2 3\":2,\"2 8\":2,\"0 3\":2,\"0 7\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"0 7\":2,\"2 7\":2,\"0 3\":1,\"2 8\":1,\"2 4\":1,\"4 5\":2,\"3 8\":1,\"1 7\":2,\"2 3\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"3 5\":1,\"1 3\":1,\"0 7\":2,\"2 7\":2,\"0 3\":1,\"2 8\":1,\"2 4\":1,\"4 5\":2,\"3 8\":1,\"1 7\":2,\"2 3\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"4 5\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"2 4\":1,\"3 8\":2,\"3 5\":2,\"0 7\":2,\"2 7\":2,\"2 8\":1,\"1 7\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"4 5\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"2 4\":1,\"3 8\":2,\"3 5\":2,\"0 7\":1,\"1 7\":1,\"2 7\":1,\"2 8\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"1 8\":1,\"0 8\":1,\"4 8\":2,\"4 5\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"2 4\":1,\"3 8\":2,\"3 5\":2,\"0 7\":1,\"1 7\":1,\"2 7\":2,\"2 8\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"4 5\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"2 4\":1,\"3 8\":2,\"1 7\":2,\"1 8\":2,\"3 5\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":1,\"1 8\":2,\"3 8\":2,\"4 5\":1,\"2 4\":2,\"2 3\":2,\"1 7\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"2 3\":2,\"1 7\":1,\"4 5\":2,\"2 4\":1,\"3 8\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"2 3\":2,\"1 7\":1,\"4 5\":1,\"2 4\":2,\"3 8\":2},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"3 5\":1,\"0 3\":2,\"2 3\":2,\"1 7\":2,\"1 8\":2,\"1 3\":2,\"4 5\":2,\"2 4\":1,\"3 8\":1},\n\t\t{\"0 5\":1,\"0 2\":1,\"2 8\":2,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"0 8\":2,\"3 5\":1,\"0 3\":2,\"2 3\":2,\"1 7\":2,\"1 8\":2,\"1 3\":2,\"4 5\":1,\"2 4\":2,\"3 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":1,\"4 8\":1,\"0 7\":2,\"2 3\":2,\"0 3\":2,\"2 7\":2,\"1 7\":2,\"1 3\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":1,\"4 8\":1,\"1 3\":1,\"0 7\":2,\"2 7\":2,\"0 3\":1,\"2 3\":1,\"1 7\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":1,\"4 8\":1,\"1 3\":1,\"2 7\":1,\"2 3\":1,\"1 7\":1,\"0 3\":1,\"0 7\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":1,\"4 8\":1,\"1 3\":1,\"2 7\":1,\"2 3\":1,\"1 7\":1,\"0 3\":2,\"0 7\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"4 8\":2,\"0 7\":1,\"2 7\":1,\"1 7\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"1 8\":1,\"2 8\":1,\"0 5\":1,\"4 5\":1,\"3 5\":1,\"0 8\":1,\"3 8\":2,\"0 3\":1,\"1 3\":1,\"2 3\":1,\"4 8\":2,\"0 7\":2,\"2 7\":2,\"1 7\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"4 5\":2,\"0 5\":2,\"3 8\":1,\"2 7\":1,\"2 3\":1,\"2 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"4 5\":2,\"0 5\":2,\"3 8\":1,\"2 7\":2,\"2 3\":2,\"2 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"4 5\":1,\"0 5\":1,\"3 8\":2,\"2 7\":1,\"2 3\":1,\"2 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"4 5\":1,\"0 5\":1,\"3 8\":2,\"2 7\":2,\"2 8\":2,\"2 3\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"4 5\":1,\"0 5\":1,\"3 8\":2,\"2 7\":2,\"2 8\":2,\"2 3\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":1,\"2 7\":2,\"2 3\":1,\"0 5\":1,\"3 8\":2,\"4 5\":1,\"1 7\":2,\"2 8\":1,\"1 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"1 3\":1,\"0 3\":1,\"2 7\":2,\"2 3\":1,\"0 5\":1,\"3 8\":2,\"4 5\":1,\"1 7\":2,\"2 8\":2,\"1 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"0 3\":2,\"2 3\":2,\"1 7\":2,\"2 7\":2,\"1 3\":2,\"4 5\":1,\"1 8\":2,\"2 8\":2,\"0 5\":1,\"3 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"0 3\":2,\"2 3\":2,\"1 7\":2,\"2 7\":2,\"1 3\":2,\"4 5\":2,\"0 5\":2,\"3 8\":1,\"2 8\":1,\"1 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"3 5\":1,\"0 3\":2,\"2 3\":2,\"1 7\":2,\"2 7\":2,\"1 3\":2,\"4 5\":2,\"0 5\":2,\"3 8\":1,\"2 8\":2,\"1 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"4 5\":2,\"1 3\":1,\"2 3\":1,\"0 5\":2,\"3 8\":2,\"3 5\":2,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"2 7\":1,\"2 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"4 5\":2,\"1 3\":1,\"2 3\":1,\"0 5\":2,\"3 8\":2,\"3 5\":2,\"0 3\":2,\"1 8\":1,\"1 7\":1,\"2 7\":2,\"2 8\":2},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"4 5\":2,\"1 3\":1,\"2 3\":1,\"0 5\":2,\"3 8\":2,\"3 5\":2,\"0 3\":1,\"2 7\":2,\"1 7\":2,\"2 8\":1,\"1 8\":1},\n\t\t{\"2 4\":1,\"0 2\":2,\"4 8\":2,\"0 7\":2,\"0 8\":2,\"4 5\":2,\"1 3\":1,\"2 3\":1,\"0 5\":2,\"3 8\":2,\"3 5\":2,\"0 3\":1,\"2 7\":2,\"1 7\":2,\"2 8\":2,\"1 8\":2}\n\t]]\n}"
  },
  {
    "path": "tests/files/json/panels-3x3-layer-solver.json",
    "content": "{\n\t\"orders\": {\n\t\t\"2 5\": 1,\n\t\t\"1 6\": 1,\n\t\t\"0 4\": 1,\n\t\t\"3 7\": 2,\n\t\t\"4 6\": 1,\n\t\t\"0 1\": 2,\n\t\t\"3 4\": 1,\n\t\t\"6 7\": 2,\n\t\t\"5 8\": 2,\n\t\t\"7 8\": 1,\n\t\t\"5 6\": 1,\n\t\t\"1 2\": 1,\n\t\t\"1 5\": 1,\n\t\t\"2 6\": 1,\n\t\t\"0 6\": 1,\n\t\t\"1 4\": 1,\n\t\t\"4 7\": 2,\n\t\t\"3 6\": 1,\n\t\t\"5 7\": 2,\n\t\t\"6 8\": 2\n\t},\n\t\"branches\":[[\n\t\t{\n\t\t\t\"orders\": { \"0 5\": 1, \"0 2\": 1 },\n\t\t\t\"branches\": [[\n\t\t\t\t{\n\t\t\t\t\t\"orders\": { \"1 8\": 1, \"0 8\": 1 },\n\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"1 3\": 1, \"0 3\": 1, \"1 7\": 1, \"0 7\": 1 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"2 8\": 1, \"2 3\": 1, \"2 4\": 1, \"4 5\": 2, \"4 8\": 2, \"2 7\": 1 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 1, \"3 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 2, \"3 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"2 8\": 2, \"2 7\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"4 8\": 2, \"2 4\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 1, \"2 3\": 2, \"3 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 2, \"2 3\": 1, \"3 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"2 3\": 2, \"3 5\": 1, \"2 4\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 1, \"4 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 2, \"4 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"2 7\": 2, \"0 7\": 2 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"4 8\": 2, \"2 4\": 1 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"2 8\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 5\": 1, \"3 8\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 1, \"1 3\": 1, \"2 3\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"2 3\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 1, \"1 3\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 2, \"1 3\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 5\": 2, \"0 3\": 1, \"1 3\": 1, \"2 3\": 1, \"3 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"0 3\": 2, \"2 3\": 2, \"3 5\": 1, \"3 8\": 1, \"2 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"2 3\": 2, \"3 5\": 1, \"2 4\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"3 8\": 1, \"4 8\": 1, \"2 8\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 1, \"1 3\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 2, \"1 3\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"0 3\": 2, \"3 8\": 1, \"4 8\": 1, \"2 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t}\n\t\t\t\t\t]]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"orders\": { \"2 8\": 2, \"4 8\": 2, \"0 7\": 2, \"2 7\": 2, \"0 8\": 2 },\n\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"2 4\": 1 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 1, \"1 3\": 1, \"3 8\": 2, \"1 7\": 2, \"3 5\": 2, \"1 8\": 2, \"2 3\": 1 }\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"3 5\": 1, \"3 8\": 1, \"0 3\": 2, \"2 3\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"1 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"1 3\": 2, \"1 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"2 3\": 2, \"3 8\": 2, \"3 5\": 1, \"2 4\": 2 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"0 3\": 2, \"1 8\": 1 }\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"1 8\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 1, \"1 3\": 1 }\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 2, \"1 3\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t}\n\t\t\t\t\t]]\n\t\t\t\t}\n\t\t\t]]\n\t\t},\n\t\t{\n\t\t\t\"orders\": { \"2 4\": 1, \"0 2\": 2 },\n\t\t\t\"branches\": [[\n\t\t\t\t{\n\t\t\t\t\t\"orders\": { \"1 8\": 1, \"2 8\": 1, \"0 5\": 1, \"4 5\": 1, \"3 5\": 1, \"0 8\": 1 },\n\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"1 3\": 1, \"2 7\": 1, \"0 3\": 1, \"1 7\": 1, \"2 3\": 1, \"0 7\": 1 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"4 8\": 1, \"3 8\": 1 }\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"4 8\": 2, \"3 8\": 2 }\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"0 7\": 2 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"2 3\": 1, \"1 3\": 1, \"0 3\": 2, \"3 8\": 1, \"4 8\": 1, \"2 7\": 1 }\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"2 7\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 3\": 1, \"0 3\": 1, \"2 3\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 8\": 1, \"3 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 8\": 2, \"3 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 2, \"3 8\": 1, \"1 3\": 2, \"4 8\": 1, \"2 3\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t}\n\t\t\t\t\t]]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"orders\": { \"4 8\": 2, \"0 7\": 2, \"0 8\": 2 },\n\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"1 8\": 1, \"2 3\": 1, \"1 7\": 1, \"1 3\": 1, \"0 3\": 2, \"2 8\": 1, \"2 7\": 1 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"0 5\": 2, \"3 5\": 1, \"3 8\": 1 }\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"0 5\": 1, \"3 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"0 5\": 2, \"3 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"orders\": { \"2 7\": 2 },\n\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"1 3\": 1, \"2 3\": 1 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 8\": 1, \"1 7\": 2, \"0 3\": 1, \"3 8\": 2, \"2 8\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"3 5\": 1, \"0 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"3 5\": 2, \"0 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"3 8\": 2, \"2 8\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"1 8\": 2, \"0 3\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"3 5\": 1, \"0 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"3 5\": 2, \"0 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 8\": 1, \"0 3\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"3 5\": 1, \"0 5\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"3 5\": 2, \"0 5\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"orders\": { \"0 3\": 2, \"3 5\": 1, \"2 3\": 2 },\n\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 8\": 1, \"3 8\": 1, \"1 7\": 2, \"4 5\": 2, \"1 3\": 2, \"0 5\": 2, \"2 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"2 8\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 2, \"0 5\": 2, \"3 8\": 1 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"1 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"1 3\": 2, \"1 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"4 5\": 1, \"0 5\": 1, \"3 8\": 2 },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"branches\": [[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 1, \"1 3\": 1, \"1 8\": 1 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"orders\": { \"1 7\": 2, \"1 3\": 2, \"1 8\": 2 }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]]\n\t\t\t\t\t\t}\n\t\t\t\t\t]]\n\t\t\t\t}\n\t\t\t]]\n\t\t}\n\t]]\n}\n"
  },
  {
    "path": "tests/files/json/randlett-flapping-bird-layer-solver.json",
    "content": "{\"orders\":{\"0 1\":1,\"10 11\":1,\"16 17\":2,\"18 19\":2,\"7 13\":2,\"5 14\":1,\"22 23\":2,\"24 25\":2,\"20 24\":2,\"21 23\":1,\"0 2\":2,\"1 9\":1,\"3 5\":1,\"6 7\":2,\"0 14\":2,\"1 13\":1,\"19 21\":2,\"17 23\":1,\"4 15\":1,\"18 20\":1,\"16 24\":2,\"8 12\":2,\"4 5\":2,\"14 15\":2,\"12 13\":2,\"7 8\":2,\"10 17\":2,\"11 16\":1,\"17 19\":2,\"16 18\":1,\"1 14\":2,\"1 12\":1,\"1 2\":2,\"0 13\":1,\"11 17\":2,\"16 19\":2,\"18 21\":2,\"8 13\":2,\"4 14\":1,\"17 22\":1,\"16 25\":2,\"16 20\":1,\"17 21\":2,\"10 16\":1,\"17 18\":1,\"17 20\":1,\"16 21\":2,\"0 7\":1,\"1 7\":1,\"6 13\":2,\"7 12\":2,\"0 5\":2,\"1 5\":2,\"3 14\":1,\"5 15\":1,\"18 24\":2,\"19 23\":1,\"0 3\":2,\"1 3\":2,\"3 4\":1,\"3 15\":1,\"0 6\":1,\"1 6\":1,\"6 8\":2,\"6 12\":2,\"0 15\":2,\"0 4\":2,\"1 8\":1,\"10 23\":2,\"11 23\":2,\"10 24\":1,\"11 24\":1,\"12 15\":2,\"4 10\":1,\"8 11\":2,\"4 11\":1,\"8 10\":2,\"6 14\":2,\"7 14\":2,\"3 13\":1,\"5 13\":1,\"1 15\":2,\"13 14\":2,\"0 12\":1,\"0 9\":1,\"19 20\":1,\"10 22\":2,\"11 25\":1,\"16 23\":2,\"17 24\":1,\"8 15\":2,\"12 14\":2,\"5 7\":1,\"1 4\":2,\"0 8\":1,\"5 6\":1,\"6 15\":2,\"7 15\":2,\"3 7\":1,\"3 12\":1,\"5 12\":1,\"4 12\":1,\"11 22\":2,\"10 25\":1,\"8 14\":2,\"4 13\":1,\"13 15\":2,\"17 25\":1,\"3 8\":1,\"4 8\":1,\"5 8\":1,\"4 6\":1,\"4 7\":1,\"20 21\":2,\"20 23\":2,\"21 24\":1,\"16 22\":2,\"3 6\":1,\"23 24\":1,\"23 25\":1,\"22 24\":1,\"2 9\":1,\"19 24\":1,\"18 23\":2,\"22 25\":1}}\n"
  },
  {
    "path": "tests/files/obj/sphere-with-holes.obj",
    "content": "v 0.000000 -1.000000 0.000000\nv 0.723607 -0.447220 0.525725\nv -0.276388 -0.447220 0.850649\nv -0.894426 -0.447216 0.000000\nv -0.276388 -0.447220 -0.850649\nv 0.723607 -0.447220 -0.525725\nv 0.276388 0.447220 0.850649\nv -0.723607 0.447220 0.525725\nv -0.723607 0.447220 -0.525725\nv 0.276388 0.447220 -0.850649\nv 0.894426 0.447216 0.000000\nv 0.000000 1.000000 0.000000\nv -0.162456 -0.850654 0.499995\nv 0.425323 -0.850654 0.309011\nv 0.850648 -0.525736 0.000000\nv 0.425323 -0.850654 -0.309011\nv -0.525730 -0.850652 0.000000\nv -0.688189 -0.525736 0.499997\nv -0.162456 -0.850654 -0.499995\nv 0.262869 -0.525738 -0.809012\nv 0.951058 0.000000 0.309013\nv 0.951058 0.000000 -0.309013\nv 0.000000 0.000000 1.000000\nv 0.587786 0.000000 0.809017\nv -0.951058 0.000000 0.309013\nv -0.587786 0.000000 0.809017\nv -0.587786 0.000000 -0.809017\nv -0.951058 0.000000 -0.309013\nv 0.587786 0.000000 -0.809017\nv 0.000000 0.000000 -1.000000\nv 0.688189 0.525736 0.499997\nv -0.262869 0.525738 0.809012\nv -0.850648 0.525736 0.000000\nv -0.262869 0.525738 -0.809012\nv 0.162456 0.850654 0.499995\nv 0.525730 0.850652 0.000000\nv -0.425323 0.850654 0.309011\nv -0.425323 0.850654 -0.309011\nv 0.162456 0.850654 -0.499995\nf 1/1/1 14/2/1 13/3/1\nf 2/4/2 14/5/2 15/6/2\nf 1/7/3 13/8/3 17/9/3\nf 1/10/4 17/11/4 19/12/4\nf 1/13/5 19/14/5 16/15/5\nf 2/4/6 15/6/6 21/16/6\nf 4/17/7 18/18/7 25/19/7\nf 6/20/8 20/21/8 29/22/8\nf 2/4/9 21/16/9 24/23/9\nf 3/24/10 23/25/10 26/26/10\nf 4/17/11 25/19/11 28/27/11\nf 5/28/12 27/29/12 30/30/12\nf 6/20/13 29/22/13 22/31/13\nf 7/32/14 31/33/14 35/34/14\nf 8/35/15 32/36/15 37/37/15\nf 9/38/16 33/39/16 38/40/16\nf 10/41/17 34/42/17 39/43/17\nf 36/44/18 39/45/18 12/46/18\nf 39/43/19 38/47/19 12/48/19\nf 39/43/20 34/42/20 38/47/20\nf 34/42/21 9/38/21 38/47/21\nf 38/40/22 37/49/22 12/50/22\nf 38/40/23 33/39/23 37/49/23\nf 33/39/24 8/51/24 37/49/24\nf 37/37/25 35/52/25 12/53/25\nf 37/37/26 32/36/26 35/52/26\nf 32/36/27 7/32/27 35/52/27\nf 35/34/28 36/54/28 12/55/28\nf 35/34/29 31/33/29 36/54/29\nf 31/33/30 11/56/30 36/54/30\nf 30/30/31 34/42/31 10/41/31\nf 30/30/32 27/29/32 34/42/32\nf 27/29/33 9/38/33 34/42/33\nf 28/27/34 33/39/34 9/38/34\nf 28/27/35 25/19/35 33/39/35\nf 25/19/36 8/51/36 33/39/36\nf 26/26/37 32/36/37 8/35/37\nf 26/26/38 23/25/38 32/36/38\nf 23/25/39 7/32/39 32/36/39\nf 24/23/40 31/33/40 7/32/40\nf 24/23/41 21/16/41 31/33/41\nf 21/16/42 11/56/42 31/33/42\nf 29/22/43 30/30/43 10/41/43\nf 29/22/44 20/21/44 30/30/44\nf 20/21/45 5/28/45 30/30/45\nf 27/29/46 28/27/46 9/38/46\nf 25/19/47 26/57/47 8/51/47\nf 25/19/48 18/18/48 26/57/48\nf 18/18/49 3/58/49 26/57/49\nf 23/25/50 24/23/50 7/32/50\nf 21/16/51 22/31/51 11/56/51\nf 21/16/52 15/6/52 22/31/52\nf 15/6/53 6/20/53 22/31/53\nf 16/15/54 20/21/54 6/20/54\nf 16/15/55 19/14/55 20/21/55\nf 19/14/56 5/28/56 20/21/56\nf 17/9/57 18/18/57 4/17/57\nf 17/9/58 13/8/58 18/18/58\nf 13/8/59 3/58/59 18/18/59\nf 15/6/60 16/59/60 6/20/60\nf 15/6/61 14/5/61 16/59/61\nf 14/5/62 1/60/62 16/59/62\n"
  },
  {
    "path": "tests/files/obj/stanford-bunny.obj",
    "content": "# title: Stanford Bunny, simplified\nv -4.498931 3.147773 0.181835\nv -4.557349 2.082472 0.697259\nv -3.07032 4.23874 1.762023\nv -4.076093 0.001936 1.107375\nv 2.950079 0.055147 1.00281\nv -1.096427 5.830371 -0.717677\nv -3.10043 5.387782 -3.086266\nv -1.223708 2.815631 1.220183\nv 0.390108 3.181022 0.185816\nv -4.59804 2.288467 2.178839\nv -1.62618 -0.868822 -0.503767\nv -3.666046 2.271722 -0.431766\nv -4.536206 4.066438 1.026011\nv 1.604964 1.188187 -1.01477\nv -4.551212 1.175283 0.850787\nv -2.612268 4.195515 0.645511\nv -3.967972 3.17974 2.683493\nv -3.48215 2.303492 2.657752\nv -0.309384 1.009445 -1.917948\nv -0.451567 5.346025 -1.469755\nv 2.298579 1.40475 0.79984\nv 2.844157 -0.726053 -0.136003\nv -3.268838 3.561421 -0.457568\nv 0.158494 1.630549 2.274441\nv -2.466235 3.663707 0.075377\nv -2.957971 1.112161 2.327206\nv -3.557516 1.856693 2.064218\nv 0.124327 -0.516452 2.85667\nv 2.618295 -1.359569 0.829077\nv 2.484568 0.28933 0.525522\nv -3.179761 -1.738689 1.017074\nv 0.882077 -1.594115 2.132988\nv 1.332302 2.501323 1.580588\nv -2.412865 0.196147 2.15975\nv 2.034804 -0.723001 1.678599\nv 0.732387 -1.696151 -0.493074\nv -3.446745 4.104529 -2.54327\nv -3.782777 -1.6673 0.026168\nv -2.259767 -1.720254 -0.76965\nv -2.16445 -1.034784 1.980421\nv -3.291215 -1.641876 2.183311\nv 1.268213 0.881688 2.464054\nv -2.292629 2.96786 1.140089\nv -1.62214 1.243433 2.21375\nv -1.29347 -0.931538 2.032796\nv 1.696119 2.143237 -0.295267\nv -1.433818 -1.563849 2.80097\nv -2.515121 -0.896432 0.796816\nv -1.682867 1.188515 -1.291449\nv -2.083575 -1.768751 -1.559177\nv -1.057133 1.008989 2.896109\nv -0.816674 4.533923 -0.349209\nv -1.516611 -1.611888 2.131003\nv 1.889283 -0.710724 -0.436824\nv -1.31993 -1.073235 -1.36827\nv 0.648431 -1.696023 -1.071017\nv -3.876009 5.385763 -2.466716\nv 0.340278 -0.649736 -1.640465\nv -1.412777 0.309231 -1.838236\nv -2.515807 -0.599013 -0.459537\nv -2.483166 2.873362 -0.305461\nv -2.571918 1.588931 -1.136505\nv 0.473236 2.338757 -0.898994\nv -3.294838 4.221361 0.222431\nv 1.139079 -1.658086 0.570052\nv -3.869911 4.027547 -0.212799\nv -3.887556 0.554465 -0.784717\nv 1.025244 -1.62313 1.275895\nf 64 13 3\nf 1 13 66\nf 51 42 24\nf 42 51 28\nf 24 42 33\nf 45 32 28\nf 34 44 26\nf 26 8 27\nf 8 43 27\nf 40 44 34\nf 45 44 40\nf 44 8 26\nf 30 35 5\nf 33 9 8\nf 41 40 48\nf 8 24 33\nf 47 32 45\nf 24 8 44\nf 24 44 51\nf 22 30 5\nf 14 21 30\nf 18 43 17\nf 17 43 3\nf 53 40 41\nf 18 27 43\nf 30 21 35\nf 42 21 33\nf 6 16 52\nf 29 5 35\nf 35 21 42\nf 22 29 36\nf 52 16 25\nf 35 28 32\nf 35 42 28\nf 64 57 66\nf 39 11 50\nf 4 15 67\nf 7 64 23\nf 11 60 49\nf 32 68 35\nf 56 54 36\nf 41 48 31\nf 14 46 21\nf 14 30 54\nf 43 16 3\nf 29 22 5\nf 43 25 16\nf 13 64 66\nf 2 13 1\nf 64 3 16\nf 21 46 33\nf 2 67 15\nf 61 43 8\nf 46 9 33\nf 7 57 64\nf 45 51 44\nf 20 52 25\nf 28 51 45\nf 13 17 3\nf 46 63 9\nf 18 17 10\nf 47 40 53\nf 26 27 4\nf 6 52 20\nf 48 40 34\nf 4 27 15\nf 47 45 40\nf 7 23 37\nf 16 6 64\nf 17 13 10\nf 63 62 9\nf 37 66 57\nf 59 55 11\nf 37 23 66\nf 59 11 49\nf 63 46 14\nf 10 13 2\nf 63 49 62\nf 27 2 15\nf 31 48 38\nf 49 14 19\nf 1 66 23\nf 2 12 67\nf 48 60 38\nf 12 2 1\nf 11 55 50\nf 30 22 54\nf 64 25 23\nf 8 9 61\nf 14 49 63\nf 23 25 61\nf 61 9 62\nf 61 62 12\nf 43 61 25\nf 67 12 62\nf 1 23 12\nf 39 38 60\nf 12 23 61\nf 36 54 22\nf 54 58 14\nf 54 56 58\nf 39 60 11\nf 58 19 14\nf 25 64 20\nf 60 62 49\nf 55 58 56\nf 56 50 55\nf 20 64 6\nf 57 7 37\nf 55 59 58\nf 58 59 19\nf 67 62 60\nf 59 49 19\nf 29 65 36\nf 29 68 65\nf 29 35 68\nf 2 27 10\nf 27 18 10\nf 60 48 4\nf 4 34 26\nf 34 4 48\nf 4 67 60 "
  },
  {
    "path": "tests/files/opx/bird-base-2012.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n<java version=\"1.5.0_02\" class=\"java.beans.XMLDecoder\"> \n <object class=\"oripa.DataSet\"> \n  <void property=\"lines\"> \n   <array class=\"oripa.OriLineProxy\" length=\"20\"> \n    <void index=\"0\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>1</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-200.0</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"1\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>3</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>3.6415315207705135E-14</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-3.6415315207705135E-14</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"2\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>1</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>200.0</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"3\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>3</int> \n      </void> \n      <void property=\"x0\"> \n       <double>3.6415315207705135E-14</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-3.6415315207705135E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>200.0</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"4\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>1</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-200.0</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"5\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>1</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-200.0</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"6\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-117.157287525381</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"7\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"y0\"> \n       <double>-3.641531520770513E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-117.157287525381</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"8\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-2.4731605323635646E-16</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-117.15728752538098</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"9\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>3.6415315207705135E-14</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-117.15728752538098</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-3.6415315207705135E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-2.2603502422849217E-14</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"10\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-117.15728752538101</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>2.180541856215705E-14</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"11\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-117.15728752538098</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>2.1805418562157038E-14</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"12\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>3</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-117.15728752538098</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-2.2603502422849217E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-117.157287525381</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"13\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-1.0048591735576161E-14</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>117.15728752538098</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"14\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>117.157287525381</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-5.886328755950317E-15</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"15\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x1\"> \n       <double>117.157287525381</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-1.4210854715202007E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-5.886328755950317E-15</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"16\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>117.157287525381</double> \n      </void> \n      <void property=\"y0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-5.886328755950317E-15</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"17\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"y0\"> \n       <double>-4.022499833786352E-14</double> \n      </void> \n      <void property=\"y1\"> \n       <double>117.15728752538098</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"18\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>2</int> \n      </void> \n      <void property=\"x0\"> \n       <double>-200.0</double> \n      </void> \n      <void property=\"x1\"> \n       <double>-2.425944645077817E-14</double> \n      </void> \n      <void property=\"y0\"> \n       <double>200.0</double> \n      </void> \n      <void property=\"y1\"> \n       <double>117.15728752538101</double> \n      </void> \n     </object> \n    </void> \n    <void index=\"19\"> \n     <object class=\"oripa.OriLineProxy\"> \n      <void property=\"type\"> \n       <int>3</int> \n      </void> \n      <void property=\"x1\"> \n       <double>117.157287525381</double> \n      </void> \n      <void property=\"y0\"> \n       <double>117.15728752538098</double> \n      </void> \n      <void property=\"y1\"> \n       <double>-5.886328755950317E-15</double> \n      </void> \n     </object> \n    </void> \n   </array> \n  </void> \n  <void property=\"mainVersion\"> \n   <int>1</int> \n  </void> \n  <void property=\"paperSize\"> \n   <double>400.0</double> \n  </void> \n </object> \n</java> \n"
  },
  {
    "path": "tests/files/opx/bird-base.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"21.0.2\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\" id=\"DataSet0\">\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>lines</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <array class=\"oripa.OriLineProxy\" length=\"28\">\n     <void index=\"0\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-6.866836743578443E-14</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>117.15728752538102</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"1\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-117.15728752538104</double>\n       </void>\n       <void property=\"y0\">\n        <double>2.1805418562157054E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>-117.157287525381</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"2\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-5.445751272058243E-14</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>117.15728752538097</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"3\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-2.4731605323635646E-16</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-117.15728752538098</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"4\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-117.15728752538101</double>\n       </void>\n       <void property=\"y1\">\n        <double>2.180541856215705E-14</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"5\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"6\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"7\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"8\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"9\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>117.15728752538102</double>\n       </void>\n       <void property=\"x1\">\n        <double>-2.425944645077817E-14</double>\n       </void>\n       <void property=\"y0\">\n        <double>-3.1900472378611834E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>117.15728752538101</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"10\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"y0\">\n        <double>-4.022499833786352E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>117.15728752538098</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"11\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-117.157287525381</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-2.260350242284923E-14</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"12\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>3.6415315207705135E-14</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-3.6415315207705135E-14</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"13\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>3.6415315207705135E-14</double>\n       </void>\n       <void property=\"x1\">\n        <double>-117.15728752538098</double>\n       </void>\n       <void property=\"y0\">\n        <double>-3.6415315207705135E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>-2.2603502422849217E-14</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"14\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"15\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"16\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"17\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"18\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>117.157287525381</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-5.886328755950317E-15</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"19\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-117.15728752538101</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>2.180541856215705E-14</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"20\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x1\">\n        <double>117.157287525381</double>\n       </void>\n       <void property=\"y0\">\n        <double>-1.4210854715202007E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>-5.886328755950317E-15</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"21\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>117.157287525381</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-5.886328755950317E-15</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"22\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>117.157287525381</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-5.886328755950317E-15</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"23\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-2.425944645077817E-14</double>\n       </void>\n       <void property=\"y0\">\n        <double>117.15728752538101</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"24\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"y0\">\n        <double>-3.641531520770513E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>-117.157287525381</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"25\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>3.6415315207705135E-14</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-3.6415315207705135E-14</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"26\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-117.157287525381</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"27\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-117.157287525381</double>\n       </void>\n      </object>\n     </void>\n    </array>\n   </void>\n  </void>\n  <void property=\"mainVersion\">\n   <int>2</int>\n  </void>\n  <void property=\"paperSize\">\n   <double>400.0</double>\n  </void>\n  <void property=\"subVersion\">\n   <int>1</int>\n  </void>\n </object>\n</java>\n"
  },
  {
    "path": "tests/files/opx/one-crease.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"21.0.2\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\" id=\"DataSet0\">\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>lines</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <array class=\"oripa.OriLineProxy\" length=\"5\">\n     <void index=\"0\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"1\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"2\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"3\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"4\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n    </array>\n   </void>\n  </void>\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>title</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <string>one single crease</string>\n   </void>\n  </void>\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>editorName</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <string>Kraft</string>\n   </void>\n  </void>\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>originalAuthorName</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <string>traditional</string>\n   </void>\n  </void>\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>reference</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <string>no references</string>\n   </void>\n  </void>\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>memo</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <string>this is a square with one diagonal crease</string>\n   </void>\n  </void>\n  <void property=\"mainVersion\">\n   <int>2</int>\n  </void>\n  <void property=\"paperSize\">\n   <double>400.0</double>\n  </void>\n  <void property=\"subVersion\">\n   <int>1</int>\n  </void>\n </object>\n</java>\n"
  },
  {
    "path": "tests/files/opx/square-fish.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"21.0.2\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\" id=\"DataSet0\">\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>lines</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <array class=\"oripa.OriLineProxy\" length=\"33\">\n     <void index=\"0\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.84271247461493</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"1\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461493</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"2\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461493</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"3\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-34.314575050753774</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"4\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.84271247461493</double>\n       </void>\n       <void property=\"y1\">\n        <double>-34.314575050757206</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"5\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-34.314575050757206</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"6\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"7\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>34.314575050753774</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"8\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.84271247461493</double>\n       </void>\n       <void property=\"y1\">\n        <double>34.314575050753774</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"9\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"10\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"11\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>34.314575050753774</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"12\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-34.314575050753774</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"13\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>82.84271247462311</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247462311</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"14\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"x1\">\n        <double>34.314575050757206</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"15\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.8427124746313</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"16\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"17\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.8427124746313</double>\n       </void>\n       <void property=\"y1\">\n        <double>34.314575050753774</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"18\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"19\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"20\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"21\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.8427124746313</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"22\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247463128</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461493</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"23\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247463136</double>\n       </void>\n       <void property=\"x1\">\n        <double>-82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>-82.84271247461493</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"24\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-34.314575050757206</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"25\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>34.314575050757206</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"26\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"27\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247462311</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.84271247462311</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"28\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-34.314575050753774</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-82.8427124746313</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"29\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"x1\">\n        <double>34.314575050757206</double>\n       </void>\n       <void property=\"y0\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"30\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"x1\">\n        <double>-34.314575050753774</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"31\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>82.84271247461493</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"32\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>82.84271247461491</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>82.84271247461491</double>\n       </void>\n      </object>\n     </void>\n    </array>\n   </void>\n  </void>\n  <void property=\"mainVersion\">\n   <int>2</int>\n  </void>\n  <void property=\"paperSize\">\n   <double>400.0</double>\n  </void>\n  <void property=\"subVersion\">\n   <int>1</int>\n  </void>\n </object>\n</java>\n"
  },
  {
    "path": "tests/files/opx/test.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"1.5.0_06\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\">\n  <void property=\"lines\">\n   <array class=\"oripa.OriLineProxy\" length=\"10\">\n    <void index=\"0\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>200.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"1\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>-200.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"2\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x1\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>-200.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"3\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>3</int>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"4\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>3</int>\n      </void>\n      <void property=\"x0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"5\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>100.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"6\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>100.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"7\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>100.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"8\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>1</int>\n      </void>\n      <void property=\"x0\">\n       <double>200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>100.0</double>\n      </void>\n     </object>\n    </void>\n    <void index=\"9\">\n     <object class=\"oripa.OriLineProxy\">\n      <void property=\"type\">\n       <int>3</int>\n      </void>\n      <void property=\"x0\">\n       <double>-200.0</double>\n      </void>\n      <void property=\"x1\">\n       <double>200.0</double>\n      </void>\n      <void property=\"y0\">\n       <double>100.0</double>\n      </void>\n      <void property=\"y1\">\n       <double>100.0</double>\n      </void>\n     </object>\n    </void>\n   </array>\n  </void>\n  <void property=\"mainVersion\">\n   <int>1</int>\n  </void>\n  <void property=\"paperSize\">\n   <double>400.0</double>\n  </void>\n  <void property=\"subVersion\">\n   <int>1</int>\n  </void>\n </object>\n</java>\n"
  },
  {
    "path": "tests/files/opx/windmill.opx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<java version=\"21.0.2\" class=\"java.beans.XMLDecoder\">\n <object class=\"oripa.DataSet\" id=\"DataSet0\">\n  <void class=\"oripa.DataSet\" method=\"getField\">\n   <string>lines</string>\n   <void method=\"set\">\n    <object idref=\"DataSet0\"/>\n    <array class=\"oripa.OriLineProxy\" length=\"28\">\n     <void index=\"0\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"1\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"2\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"3\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"4\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"5\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"6\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"7\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>100.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"8\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>100.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"9\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"10\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"11\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>3</int>\n       </void>\n       <void property=\"x0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"12\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"13\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"14\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"15\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"16\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"17\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"18\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>-100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"19\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"20\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"21\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>2</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"22\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"23\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"24\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"25\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"type\">\n        <int>1</int>\n       </void>\n       <void property=\"x0\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>-200.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>-200.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"26\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>-100.0</double>\n       </void>\n      </object>\n     </void>\n     <void index=\"27\">\n      <object class=\"oripa.OriLineProxy\">\n       <void property=\"x1\">\n        <double>100.0</double>\n       </void>\n       <void property=\"y0\">\n        <double>200.0</double>\n       </void>\n       <void property=\"y1\">\n        <double>100.0</double>\n       </void>\n      </object>\n     </void>\n    </array>\n   </void>\n  </void>\n  <void property=\"mainVersion\">\n   <int>2</int>\n  </void>\n  <void property=\"paperSize\">\n   <double>400.0</double>\n  </void>\n  <void property=\"subVersion\">\n   <int>1</int>\n  </void>\n </object>\n</java>\n"
  },
  {
    "path": "tests/fold.bases.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"bases, all are valid\", () => {\n\texpect(ear.graph.validate(ear.graph.blintz())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.waterbomb())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.kite())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.fish())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.bird())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.frog())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.windmill())).toHaveLength(0);\n\texpect(ear.graph.validate(ear.graph.squareFish())).toHaveLength(0);\n});\n\ntest(\"square\", () => {\n\tconst res1 = ear.graph.square();\n\tconst res2 = ear.graph.square(3);\n\texpect(res1.vertices_coords[0][0]).toBe(0);\n\texpect(res1.vertices_coords[0][1]).toBe(0);\n\texpect(res1.vertices_coords[2][0]).toBe(1);\n\texpect(res1.vertices_coords[2][1]).toBe(1);\n\texpect(res2.vertices_coords[0][0]).toBe(0);\n\texpect(res2.vertices_coords[0][1]).toBe(0);\n\texpect(res2.vertices_coords[2][0]).toBe(3);\n\texpect(res2.vertices_coords[2][1]).toBe(3);\n});\n\ntest(\"rectangle\", () => {\n\tconst res1 = ear.graph.rectangle();\n\tconst res2 = ear.graph.rectangle(2);\n\tconst res3 = ear.graph.rectangle(2, 1);\n\texpect(res1.vertices_coords[0][0]).toBe(0);\n\texpect(res1.vertices_coords[0][1]).toBe(0);\n\texpect(res2.vertices_coords[1][0]).toBe(2);\n\texpect(res2.vertices_coords[1][1]).toBe(0);\n\texpect(res3.vertices_coords[2][0]).toBe(2);\n\texpect(res3.vertices_coords[2][1]).toBe(1);\n});\n\ntest(\"polygon\", () => {\n\texpect(ear.graph.polygon().vertices_coords.length).toBe(3);\n\texpect(ear.graph.polygon(1).vertices_coords.length).toBe(1);\n\texpect(ear.graph.polygon(2).vertices_coords.length).toBe(2);\n\tconst res1 = ear.graph.polygon(12);\n\tconst res2 = ear.graph.polygon(1024);\n\tconst res3 = ear.graph.polygon(12, 100);\n\texpect(res1.vertices_coords[0][0]).toBeCloseTo(1);\n\texpect(res1.vertices_coords[0][1]).toBeCloseTo(0);\n\texpect(res1.vertices_coords[3][0]).toBeCloseTo(0);\n\texpect(res1.vertices_coords[3][1]).toBeCloseTo(1);\n\texpect(res2.vertices_coords[0][0]).toBeCloseTo(1);\n\texpect(res2.vertices_coords[0][1]).toBeCloseTo(0);\n\texpect(res2.vertices_coords[256][0]).toBeCloseTo(0);\n\texpect(res2.vertices_coords[256][1]).toBeCloseTo(1);\n\texpect(res3.vertices_coords[0][0]).toBeCloseTo(100);\n\texpect(res3.vertices_coords[0][1]).toBeCloseTo(0);\n});\n\ntest(\"kite\", () => {\n\tconst kite = ear.graph.kite();\n\tArray.from(Array(6))\n\t\t.forEach((_, i) => expect(kite.edges_assignment[i]).toBe(\"B\"));\n\texpect(kite.edges_assignment.includes(\"M\")).toBe(false);\n\texpect(kite.edges_assignment.includes(\"V\")).toBe(true);\n\texpect(kite.edges_assignment.includes(\"F\")).toBe(true);\n});\n\ntest(\"fish\", () => {\n\tconst fish = ear.graph.fish();\n\texpect(fish.vertices_coords.length).toBe(11);\n\tArray.from(Array(8))\n\t\t.forEach((_, i) => expect(fish.edges_assignment[i]).toBe(\"B\"));\n\texpect(fish.edges_assignment.includes(\"M\")).toBe(true);\n\texpect(fish.edges_assignment.includes(\"V\")).toBe(true);\n\texpect(fish.edges_assignment.includes(\"F\")).toBe(true);\n});\n\ntest(\"bird\", () => {\n\tconst bird = ear.graph.bird();\n\tArray.from(Array(8))\n\t\t.forEach((_, i) => expect(bird.edges_assignment[i]).toBe(\"B\"));\n\texpect(bird.edges_assignment.includes(\"M\")).toBe(true);\n\texpect(bird.edges_assignment.includes(\"V\")).toBe(true);\n\texpect(bird.edges_assignment.includes(\"F\")).toBe(true);\n});\n\ntest(\"frog\", () => {\n\tconst frog = ear.graph.frog();\n\texpect(frog.edges_assignment.includes(\"B\")).toBe(true);\n\texpect(frog.edges_assignment.includes(\"M\")).toBe(true);\n\texpect(frog.edges_assignment.includes(\"V\")).toBe(true);\n\texpect(frog.edges_assignment.includes(\"F\")).toBe(true);\n});\n\ntest(\"windmill\", () => {\n\tconst windmill = ear.graph.windmill();\n\texpect(windmill.edges_assignment.includes(\"B\")).toBe(true);\n\texpect(windmill.edges_assignment.includes(\"M\")).toBe(true);\n\texpect(windmill.edges_assignment.includes(\"V\")).toBe(true);\n\texpect(windmill.edges_assignment.includes(\"F\")).toBe(true);\n});\n"
  },
  {
    "path": "tests/fold.colors.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"rgbToAssignment, all assignments\", () => {\n\texpect(ear.graph.rgbToAssignment(255, 0, 0)).toBe(\"M\");\n\texpect(ear.graph.rgbToAssignment(0, 255, 0)).toBe(\"C\");\n\texpect(ear.graph.rgbToAssignment(0, 0, 255)).toBe(\"V\");\n\texpect(ear.graph.rgbToAssignment(255, 255, 0)).toBe(\"J\");\n\texpect(ear.graph.rgbToAssignment(255, 255, 255)).toBe(\"F\");\n\texpect(ear.graph.rgbToAssignment(127, 127, 127)).toBe(\"F\");\n\texpect(ear.graph.rgbToAssignment(0, 0, 0)).toBe(\"B\");\n});\n\ntest(\"rgbToAssignment red to gray\", () => {\n\t// red to gray\n\texpect(ear.graph.rgbToAssignment(255, 0, 0)).toBe(\"M\");\n\texpect(ear.graph.rgbToAssignment(200, 127, 60)).toBe(\"M\");\n\texpect(ear.graph.rgbToAssignment(192, 127, 127)).toBe(\"M\");\n\t// too gray, now it is \"F\"\n\texpect(ear.graph.rgbToAssignment(152, 127, 127)).toBe(\"F\");\n});\n"
  },
  {
    "path": "tests/fold.frames.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"countFrames\", () => {\n\tconst graph1 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/blintz-frames.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.countFrames(graph1)).toBe(33);\n\tconst graph2 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/nested-frames.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.countFrames(graph2)).toBe(6);\n\tconst graph3 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.countFrames(graph3)).toBe(2);\n});\n\ntest(\"flattenFrame\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/nested-frames.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tconst flat = ear.graph.flattenFrame(FOLD, 3);\n\texpect(flat.frame_classes.length).toBe(1);\n\texpect(flat.frame_classes[0]).toBe(\"foldedForm\");\n});\n\ntest(\"flattenFrame, frame 0\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/nested-frames.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tconst flat = ear.graph.flattenFrame(FOLD, 0);\n\texpect(flat.frame_classes.length).toBe(1);\n\texpect(flat.frame_classes[0]).toBe(\"creasePattern\");\n});\n\ntest(\"getFileFramesAsArray\", () => {\n\tconst graph1 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/blintz-frames.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.getFileFramesAsArray(graph1).length).toBe(33);\n\n\tconst graph2 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/nested-frames.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.getFileFramesAsArray(graph2).length).toBe(6);\n});\n\ntest(\"getFramesByClassName\", () => {\n\tconst graph1 = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/blintz-frames.fold\",\n\t\t\"utf-8\",\n\t));\n\texpect(ear.graph.getFramesByClassName(graph1, \"creasePattern\").length).toBe(1);\n\texpect(ear.graph.getFramesByClassName(graph1, \"foldedForm\").length).toBe(32);\n});\n"
  },
  {
    "path": "tests/fold.spec.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"keys\", () => {\n\tconst graph = ear.graph();\n\texpect(graph.file_spec).toBe(1.2);\n\texpect(graph.file_creator).toBe(\"Rabbit Ear\");\n});\n\ntest(\"edgeAssignmentToFoldAngle\", () => {\n\texpect(ear.graph.edgeAssignmentToFoldAngle(\"\")).toBe(0);\n\t// every letter besides \"m\", \"v\"\n\t\"abcdefghijklnopqrstuwxyzABCDEFGHIJKLNOPQRSTUWXYZ.!@#$%^&*(){}[]-=_+\"\n\t\t.split(\"\")\n\t\t.forEach(ch => expect(ear.graph.edgeAssignmentToFoldAngle(ch))\n\t\t\t.toBe(0));\n\texpect(ear.graph.edgeAssignmentToFoldAngle(\"v\")).toBe(180);\n\texpect(ear.graph.edgeAssignmentToFoldAngle(\"V\")).toBe(180);\n\texpect(ear.graph.edgeAssignmentToFoldAngle(\"m\")).toBe(-180);\n\texpect(ear.graph.edgeAssignmentToFoldAngle(\"M\")).toBe(-180);\n});\n\nconst key_test = {\n\ttest: 0,\n\ttesttest: 0,\n\ttest_: 0,\n\t_test: 0,\n\taaatestaaa: 0,\n\taaatest: 0,\n\ttestaaa: 0,\n\ttest_test_test: 0,\n\ttest_aaa: 0,\n\taaa_test: 0,\n\t_test_test_test_: 0,\n};\n\n// test(\"filterKeysWithSuffix\", () => {\n// \tconst result = ear.graph.filterKeysWithSuffix(key_test, \"test\");\n// \texpect(result.length).toBe(6);\n// });\n\n// test(\"filterKeysWithPrefix\", () => {\n// \tconst result = ear.graph.filterKeysWithPrefix(key_test, \"test\");\n// \texpect(result.length).toBe(6);\n// });\n\ntest(\"filterKeysWithPrefix\", () => {\n\tconst result = ear.graph.filterKeysWithPrefix(key_test, \"test\");\n\texpect(result.length).toBe(3);\n});\n\ntest(\"filterKeysWithSuffix\", () => {\n\tconst result = ear.graph.filterKeysWithSuffix(key_test, \"test\");\n\texpect(result.length).toBe(3);\n});\n\ntest(\"transposeGraphArrays\", () => {\n\tconst craneString = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst crane = JSON.parse(craneString);\n\tconst result = ear.graph.transposeGraphArrays(crane, \"edges\");\n\texpect(result.length).toBe(crane.edges_vertices.length);\n\texpect(result[0].edges_vertices.length).toBe(2);\n\texpect(result[0].edges_assignment.length).toBe(1); // string, \"M\" or \"V\" or something\n\t// no key\n\texpect(ear.graph.transposeGraphArrays(crane, \"nokey\").length)\n\t\t.toBe(0);\n});\n\ntest(\"transposeGraphArrayAtIndex\", () => {\n\tconst craneString = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst crane = JSON.parse(craneString);\n\tconst result = ear.graph.transposeGraphArrayAtIndex(crane, \"edges\", 10);\n\texpect(result.edges_vertices.length).toBe(2);\n\texpect(result.edges_assignment.length).toBe(1); // string, \"M\" or \"V\" or something\n\t// no key\n\texpect(ear.graph.transposeGraphArrayAtIndex(crane, \"nokey\", 0))\n\t\t.toBe(undefined);\n});\n\ntest(\"edge angle assinments\", () => {\n\tconst assignments = [\"B\", \"b\", \"M\", \"m\", \"V\", \"v\", \"F\", \"f\", \"U\", \"u\"];\n\tconst angles = [0, 0, -180, -180, 180, 180, 0, 0, 0, 0];\n\tconst calculated = assignments.map(a => ear.graph.edgeAssignmentToFoldAngle(a));\n\texpect(ear.math.epsilonEqualVectors(calculated, angles)).toBe(true);\n});\n\ntest(\"prefix key search test\", () => {\n\tconst graph = {\n\t\tvertices: null,\n\t\tvertices_: null,\n\t\tvertices_coords: null,\n\t\tvertices_vertices: null,\n\t\tvertices_vertices_vertices: null,\n\t\tvertices_vertices_vertices_coords: null,\n\t\t_vertices: null,\n\t\t_vertices_: null,\n\t\t_vertices_coords: null,\n\t\t_vertices_vertices: null,\n\t\tVertices: null,\n\t\tVertices_: null,\n\t\t_Vertices: null,\n\t\t_Vertices_: null,\n\t};\n\n\tconst expected = [\n\t\t\"vertices_\",\n\t\t\"vertices_coords\",\n\t\t\"vertices_vertices\",\n\t\t\"vertices_vertices_vertices\",\n\t\t\"vertices_vertices_vertices_coords\",\n\t];\n\tconst calculated = ear.graph.filterKeysWithPrefix(graph, \"vertices\");\n\n\texpect(expected).toEqual(\n\t\texpect.arrayContaining(calculated),\n\t);\n});\n\ntest(\"suffix key search test\", () => {\n\tconst graph = {\n\t\tvertices: null,\n\t\tvertices_: null,\n\t\tvertices_coords: null,\n\t\tvertices_vertices: null,\n\t\tvertices_vertices_vertices: null,\n\t\tvertices_vertices_vertices_coords: null,\n\t\t_vertices: null,\n\t\t_vertices_: null,\n\t\t_vertices_coords: null,\n\t\t_vertices_vertices: null,\n\t\tVertices: null,\n\t\tVertices_: null,\n\t\t_Vertices: null,\n\t\t_Vertices_: null,\n\t};\n\n\tconst expected = [\n\t\t\"vertices_vertices\",\n\t\t\"vertices_vertices_vertices\",\n\t\t\"_vertices\",\n\t\t\"_vertices_vertices\",\n\t];\n\n\tconst calculated = ear.graph.filterKeysWithSuffix(graph, \"vertices\");\n\n\texpect(expected).toEqual(\n\t\texpect.arrayContaining(calculated),\n\t);\n});\n\ntest(\"prefix key with extensions search test\", () => {\n\tconst graph = {\n\t\t\"vertices_re:\": null,\n\t\t\"vertices_re:coords\": null,\n\t\t\"vertices_re:vertices\": null,\n\t\t\"vertices_re:vertices_vertices\": null,\n\t\t\"vertices_vertices_re:vertices\": null,\n\t\t\"vertices_vertices_vertices_re:coords\": null,\n\t\t\"_vertices_re:\": null,\n\t\t\"_vertices_re:coords\": null,\n\t\t\"_vertices_re:vertices\": null,\n\t\t\"Vertices_re:\": null,\n\t\t\"_Vertices_re:\": null,\n\t\t\"re:vertices\": null,\n\t\t\"re:vertices_\": null,\n\t\t\"re:vertices_coords\": null,\n\t\t\"re:vertices_vertices\": null,\n\t\t\"re:vertices_vertices_vertices\": null,\n\t\t\"re:vertices_vertices_vertices_coords\": null,\n\t\t\"re:_vertices\": null,\n\t\t\"re:_vertices_\": null,\n\t\t\"re:_vertices_coords\": null,\n\t\t\"re:_vertices_vertices\": null,\n\t\t\"_re:vertices\": null,\n\t\t\"_re:vertices_\": null,\n\t\t\"_re:vertices_coords\": null,\n\t\t\"_re:vertices_vertices\": null,\n\t\t\"re:Vertices\": null,\n\t\t\"re:Vertices_\": null,\n\t\t\"re:_Vertices\": null,\n\t\t\"re:_Vertices_\": null,\n\t\t\"_re:Vertices\": null,\n\t\t\"_re:Vertices_\": null,\n\t};\n\n\tconst expected = [\n\t\t\"vertices_re:\",\n\t\t\"vertices_re:coords\",\n\t\t\"vertices_re:vertices\",\n\t\t\"vertices_re:vertices_vertices\",\n\t\t\"vertices_vertices_re:vertices\",\n\t\t\"vertices_vertices_vertices_re:coords\",\n\t];\n\n\tconst calculated = ear.graph.filterKeysWithPrefix(graph, \"vertices\");\n\n\texpect(expected).toEqual(\n\t\texpect.arrayContaining(calculated),\n\t);\n});\n\ntest(\"transpose geometry arrays\", () => {\n\tconst graph = {\n\t\tfile_spec: 1.1,\n\t\tfile_creator: \"\",\n\t\tfile_author: \"\",\n\t\tfile_classes: [\"singleModel\"],\n\t\tframe_title: \"\",\n\t\tframe_attributes: [\"2D\"],\n\t\tframe_classes: [\"creasePattern\"],\n\t\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 1], [0.5, 1], [0, 1]],\n\t\tvertices_vertices: [[1, 5], [2, 4, 0], [3, 1], [4, 2], [5, 1, 3], [0, 4]],\n\t\tvertices_faces: [[0], [0, 1], [1], [1], [1, 0], [0]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4]],\n\t\tedges_faces: [[0], [1], [1], [1], [0], [0], [0, 1]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\"],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 180],\n\t\tedges_length: [0.5, 0.5, 1, 0.5, 0.5, 1, 1],\n\t\tfaces_vertices: [[1, 4, 5, 0], [4, 1, 2, 3]],\n\t\tfaces_edges: [[6, 4, 5, 0], [6, 1, 2, 3]],\n\t};\n\tconst transposedVertices = ear.graph.transposeGraphArrays(graph, \"vertices\");\n\n\tconst expectedVertex1 = {\n\t\tvertices_coords: [0.5, 0],\n\t\tvertices_faces: [0, 1],\n\t\tvertices_vertices: [2, 4, 0],\n\t};\n\n\texpect(transposedVertices.length).toEqual(6);\n\texpect(Object.keys(expectedVertex1)).toEqual(\n\t\texpect.arrayContaining(Object.keys(transposedVertices[0])),\n\t);\n\texpect(expectedVertex1.vertices_coords).toEqual(\n\t\texpect.arrayContaining(transposedVertices[1].vertices_coords),\n\t);\n\texpect(expectedVertex1.vertices_faces).toEqual(\n\t\texpect.arrayContaining(transposedVertices[1].vertices_faces),\n\t);\n\texpect(expectedVertex1.vertices_vertices).toEqual(\n\t\texpect.arrayContaining(transposedVertices[1].vertices_vertices),\n\t);\n});\n\ntest(\"get keys with ending\", () => {\n\t// ear.graph.get_keys_with_ending();\n\texpect(true).toBe(true);\n});\n\ntest(\"invertAssignments assignments\", () => {\n\texpect(ear.graph.invertAssignments({ edges_assignment: Array.from(\"MBVUFJC\") })\n\t\t.edges_assignment\n\t\t.join(\"\")).toBe(\"VBMUFJC\");\n\texpect(ear.graph\n\t\t.invertAssignments({ edges_foldAngle: [0, -180, 180] })\n\t\t.edges_foldAngle[0])\n\t\t.toBe(-0);\n\texpect(ear.graph\n\t\t.invertAssignments({ edges_foldAngle: [0, -180, 180] })\n\t\t.edges_foldAngle[1])\n\t\t.toBe(180);\n\texpect(ear.graph\n\t\t.invertAssignments({ edges_foldAngle: [0, -180, 180] })\n\t\t.edges_foldAngle[2])\n\t\t.toBe(-180);\n});\n\ntest(\"getFileMetadata\", () => {\n\tconst graph = JSON.parse(fs.readFileSync(\"./tests/files/fold/crane-cp-bmvfcj.fold\", \"utf-8\"));\n\tconst metadata = ear.graph.getFileMetadata(graph);\n\texpect(Object.keys(metadata).length).toBe(5);\n\texpect(metadata.file_author).toBe(\"Kraft\");\n\texpect(metadata.file_creator).toBe(\"Rabbit Ear\");\n\texpect(metadata.file_spec).toBe(1.1);\n\texpect(metadata.file_title).toBe(\"crane\");\n\texpect(metadata.file_classes[0]).toBe(\"singleModel\");\n});\n"
  },
  {
    "path": "tests/general.array.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst arraysMatch = (a, b) => a.forEach((_, i) => expect(a[i]).toBe(b[i]));\n\ntest(\"uniqueElements\", () => {\n\t// Test case 1: Array with duplicate elements\n\tconst testArray1 = [1, 2, 3, 4, 4, 5, 5];\n\tconst result1 = ear.general.uniqueElements(testArray1);\n\texpect(JSON.stringify(result1)).toBe(JSON.stringify([1, 2, 3, 4, 5]));\n\n\t// Test case 2: Array with all unique elements\n\tconst testArray2 = [6, 7, 8, 9, 10];\n\tconst result2 = ear.general.uniqueElements(testArray2);\n\texpect(JSON.stringify(result2)).toBe(JSON.stringify([6, 7, 8, 9, 10]));\n\n\t// Test case 3: Array with all duplicate elements\n\tconst testArray3 = [11, 11, 11, 11, 11];\n\tconst result3 = ear.general.uniqueElements(testArray3);\n\texpect(JSON.stringify(result3)).toBe(JSON.stringify([11]));\n\n\t// Test case 4: Array with mixed data types\n\tconst testArray4 = [\"a\", \"b\", \"c\", \"b\", \"d\", \"a\", 1, 2, 3, 2];\n\tconst result4 = ear.general.uniqueElements(testArray4);\n\texpect(JSON.stringify(result4)).toBe(JSON.stringify([\"a\", \"b\", \"c\", \"d\", 1, 2, 3]));\n\n\t// Test case 5: Array with objects\n\tconst testArray5 = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 1 }];\n\tconst result5 = ear.general.uniqueElements(testArray5);\n\texpect(JSON.stringify(result5))\n\t\t.toBe(JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 1 }]));\n});\n\ntest(\"nonUniqueElements\", () => {\n\n});\n\ntest(\"uniqueSortedNumbers\", () => {\n\tconst array = Array.from(Array(10000)).map(() => Math.floor(Math.random() * 1000));\n\tconst result = ear.general.uniqueSortedNumbers(array);\n\texpect(result.length).toBeLessThan(array.length);\n});\n\ntest(\"epsilonUniqueSortedNumbers\", () => {\n\n});\n\ntest(\"array intersection\", () => {\n\tarraysMatch([1, 7, 9, 15], ear.general.arrayIntersection(\n\t\t[1, 2, 4, 5, 7, 9, 11, 15, 18],\n\t\t[19, 15, 14, 12, 9, 7, 3, 1],\n\t));\n\tarraysMatch([1, 7, 9, 15, 18], ear.general.arrayIntersection(\n\t\t[1, 2, 4, 5, 7, 9, 11, 15, 18],\n\t\t[18, 15, 14, 12, 9, 7, 3, 1],\n\t));\n\tarraysMatch([7, 9, 15, 18], ear.general.arrayIntersection(\n\t\t[0, 1, 2, 4, 5, 7, 9, 11, 15, 18],\n\t\t[18, 15, 14, 12, 9, 7, 3],\n\t));\n\tarraysMatch([6, 9], ear.general.arrayIntersection(\n\t\t[1, 5, 6, 9, 13],\n\t\t[16, 9, 6, 4, 2],\n\t));\n\tarraysMatch([3, 5, 7, 9], ear.general.arrayIntersection(\n\t\t[3, 5, 7, 9],\n\t\t[9, 7, 5, 3],\n\t));\n\tarraysMatch([3, 5, 7, 9], ear.general.arrayIntersection(\n\t\t[3, 5, 7, 9],\n\t\t[9, 7, 5, 3, 2],\n\t));\n\tarraysMatch([3, 5, 7, 9], ear.general.arrayIntersection(\n\t\t[3, 5, 7, 9],\n\t\t[11, 9, 7, 5, 3],\n\t));\n\tarraysMatch([3, 5, 7, 9], ear.general.arrayIntersection(\n\t\t[3, 5, 7, 9, 11],\n\t\t[9, 7, 5, 3],\n\t));\n\tarraysMatch([3, 5, 7, 9], ear.general.arrayIntersection(\n\t\t[2, 3, 5, 7, 9],\n\t\t[9, 7, 5, 3],\n\t));\n\tarraysMatch([1, 2], ear.general.arrayIntersection(\n\t\t[1, 2, 3, 4],\n\t\t[0, 1, 2, 6, 7],\n\t));\n\tarraysMatch([1, 2, 7], ear.general.arrayIntersection(\n\t\t[1, 2, 3, 4, 7],\n\t\t[0, 1, 2, 6, 7],\n\t));\n});\n\ntest(\"rotateCircularArray\", () => {\n\texpect(ear.general.rotateCircularArray([0, 1, 2, 3, 4, 5], 2))\n\t\t.toMatchObject([2, 3, 4, 5, 0, 1]);\n\n\texpect(ear.general.rotateCircularArray([0, 1, 2, 3, 4, 5], -1))\n\t\t.toMatchObject([0, 1, 2, 3, 4, 5]);\n\n\texpect(ear.general.rotateCircularArray([0, 1, 2, 3, 4, 5], 8))\n\t\t.toMatchObject([0, 1, 2, 3, 4, 5]);\n\n\texpect(ear.general.rotateCircularArray([0, 1, , , 4, 5], 0))\n\t\t.toMatchObject([0, 1, , , 4, 5]);\n\n\texpect(ear.general.rotateCircularArray([0, 1, , , 4, 5], 2))\n\t\t.toMatchObject([, , 4, 5, 0, 1]);\n\n\texpect(ear.general.rotateCircularArray([0, 1, , , 4, 5], 5))\n\t\t.toMatchObject([5, 0, 1, , , 4]);\n});\n\ntest(\"chooseTwoPairs\", () => {\n\n});\n\ntest(\"setDifferenceSortedNumbers\", () => {\n\n});\n\ntest(\"setDifferenceSortedEpsilonNumbers\", () => {\n\n});\n\ntest(\"arrayMinimumIndex\", () => {\n\tconst array1 = [99, 0, 1, , , , 5, 6, 7];\n\texpect(ear.general.arrayMinimumIndex(array1)).toBe(1);\n\n\tconst array2 = [99, , , , , , 5, 6, 7];\n\texpect(ear.general.arrayMinimumIndex(array2)).toBe(6);\n\n\tconst array3 = [97, 98, 99, , , , , , 5, 6, 7];\n\texpect(ear.general.arrayMinimumIndex(array3, n => -n)).toBe(2);\n\n\tconst array4 = [0, 1, 2, , , , , , 99, 98, 97];\n\texpect(ear.general.arrayMinimumIndex(array4, n => -n)).toBe(8);\n});\n\ntest(\"arrayMaximumIndex\", () => {\n\n});\n\ntest(\"mergeArraysWithHoles\", () => {\n\texpect(ear.general.mergeArraysWithHoles(\n\t\t[0, 1, , , 4, 5],\n\t\t[, , 2, 3, , , 6, 7],\n\t)).toMatchObject([0, 1, 2, 3, 4, 5, 6, 7]);\n\n\texpect(ear.general.mergeArraysWithHoles(\n\t\t[0, 1, , , 4, 5],\n\t\t[, , 2, 3],\n\t)).toMatchObject([0, 1, 2, 3, 4, 5]);\n\n\texpect(ear.general.mergeArraysWithHoles(\n\t\t[0, 1, , 4, 5],\n\t\t[, 2, 3, , 6, 7],\n\t)).toMatchObject([0, 2, 3, 4, 6, 7]);\n\n\texpect(ear.general.mergeArraysWithHoles(\n\t\t[, \"b\", , , , \"f\", \"g\", , \"i\"],\n\t\t[, , 3, , 5, , , 8],\n\t)).toMatchObject([, \"b\", 3, , 5, \"f\", \"g\", 8, \"i\"]);\n});\n\ntest(\"clustersToReflexiveArrays\", () => {\n\tconst result = ear.general.clustersToReflexiveArrays([[0, 2], [1, 3, 4]]);\n\texpect(result).toMatchObject([\n\t\t[2], [3, 4], [0], [1, 4], [1, 3]\n\t]);\n});\n\ntest(\"clustersToReflexiveArrays\", () => {\n\tconst example = [[6, 0, 2, 4], [5, 14, 1, 7, 13, 3], [10, 9], [11, 12], [8]];\n\tconst result = ear.general.clustersToReflexiveArrays(example);\n\texpect(result).toMatchObject([\n\t\t[6, 2, 4], [5, 14, 7, 13, 3], [6, 0, 4], [5, 14, 1, 7, 13],\n\t\t[6, 0, 2], [14, 1, 7, 13, 3], [0, 2, 4], [5, 14, 1, 13, 3],\n\t\t[], [10], [9], [12], [11], [5, 14, 1, 7, 3], [5, 1, 7, 13, 3],\n\t]);\n});\n"
  },
  {
    "path": "tests/general.cluster.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"clusterSortedGeneric\", () => {\n\n});\n\ntest(\"clusterScalars\", () => {\n\texpect(ear.general.clusterScalars([1, 2, 3, 6, 9, 13, 14, 15, 19, 21, 25, 26, 27], 1.5))\n\t\t.toMatchObject([[0, 1, 2], [3], [4], [5, 6, 7], [8], [9], [10, 11, 12]]);\n\n\texpect(ear.general.clusterScalars([, , 3, 8, 9, 13, 14, , 19, 21, 25, 26, 27], 1.5))\n\t\t.toMatchObject([[2], [3, 4], [5, 6], [8], [9], [10, 11, 12]]);\n});\n\ntest(\"clusterRanges\", () => {\n\texpect(ear.general.clusterRanges([[0, 1], [1, 2], [2, 3], [3, 4]]))\n\t\t.toMatchObject([[0], [1], [2], [3]]);\n\texpect(ear.general.clusterRanges([[0, 2], [1, 3], [5, 7], [2, 6]]))\n\t\t.toMatchObject([[0, 1, 3, 2]]);\n\texpect(ear.general.clusterRanges([[0, 2], [1, 3], [5, 7]]))\n\t\t.toMatchObject([[0, 1], [2]]);\n\texpect(ear.general.clusterRanges([[0, 2], [1, 3], [5, 7]]))\n\t\t.toMatchObject([[0, 1], [2]]);\n\texpect(ear.general.clusterRanges([[4, 6], [1, 3], [0, 1]]))\n\t\t.toMatchObject([[2], [1], [0]]);\n\texpect(ear.general.clusterRanges([[4, 6], [1, 3], [0, 2]]))\n\t\t.toMatchObject([[2, 1], [0]]);\n\texpect(ear.general.clusterRanges([[0, 6], [2, 3], [1, 2]]))\n\t\t.toMatchObject([[0, 2, 1]]);\n});\n\ntest(\"clusterRanges, some ranges inverted\", () => {\n\texpect(ear.general.clusterRanges([[1, 0], [1, 2], [3, 2], [3, 4]]))\n\t\t.toMatchObject([[0], [1], [2], [3]]);\n\texpect(ear.general.clusterRanges([[2, 0], [1, 3], [7, 5], [2, 6]]))\n\t\t.toMatchObject([[0, 1, 3, 2]]);\n\texpect(ear.general.clusterRanges([[2, 0], [1, 3], [7, 5]]))\n\t\t.toMatchObject([[0, 1], [2]]);\n\texpect(ear.general.clusterRanges([[2, 0], [1, 3], [7, 5]]))\n\t\t.toMatchObject([[0, 1], [2]]);\n\texpect(ear.general.clusterRanges([[6, 4], [1, 3], [1, 0]]))\n\t\t.toMatchObject([[2], [1], [0]]);\n\texpect(ear.general.clusterRanges([[6, 4], [1, 3], [2, 0]]))\n\t\t.toMatchObject([[2, 1], [0]]);\n\t// this would have failed if we didn't sort the ranges so that [0] is smaller\n\texpect(ear.general.clusterRanges([[6, 0], [3, 2], [2, 1]]))\n\t\t.toMatchObject([[0, 2, 1]]);\n});\n\ntest(\"clusterParallelVectors\", () => {\n\n});\n"
  },
  {
    "path": "tests/general.get.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"getVector\", () => {\n\n});\ntest(\"getArrayOfVectors\", () => {\n\n});\ntest(\"getSegment\", () => {\n\n});\ntest(\"getLine\", () => {\n\n});\n"
  },
  {
    "path": "tests/general.hashCode.test.js",
    "content": "import { expect, test } from \"vitest\";\n\ntest(\"hashCode is not exposed in API\", () => expect(true).toBe(true));\n"
  },
  {
    "path": "tests/general.number.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst equalTest = (a, b) => expect(JSON.stringify(a))\n\t.toBe(JSON.stringify(b));\n\ntest(\"cleanNumber\", () => {\n\t// this is the most decimal places javascript uses\n\tequalTest(ear.general.cleanNumber(0.12345678912345678), 0.12345678912345678);\n\tequalTest(ear.general.cleanNumber(0.12345678912345678, 5), 0.12345678912345678);\n\tequalTest(ear.general.cleanNumber(0.00000678912345678, 5), 0.00000678912345678);\n\tequalTest(ear.general.cleanNumber(0.00000078912345678, 5), 0);\n\tequalTest(ear.general.cleanNumber(0.00000000000000001), 0);\n\tequalTest(ear.general.cleanNumber(0.0000000000000001), 0);\n\tequalTest(ear.general.cleanNumber(0.000000000000001), 0.000000000000001);\n\tequalTest(ear.general.cleanNumber(0.00000000001, 9), 0);\n\tequalTest(ear.general.cleanNumber(0.0000000001, 9), 0);\n\tequalTest(ear.general.cleanNumber(0.000000001, 9), 0.000000001);\n\tequalTest(ear.general.cleanNumber(NaN), NaN);\n\tequalTest(ear.general.cleanNumber(NaN, 10), NaN);\n\tequalTest(ear.general.cleanNumber(3), 3);\n\tequalTest(ear.general.cleanNumber(33), 33);\n\tequalTest(ear.general.cleanNumber(33, 10), 33);\n\tequalTest(ear.general.cleanNumber(33, 100), 33);\n});\n\ntest(\"cleanNumber invalid input\", () => {\n\t// this is the most decimal places javascript uses\n\texpect(ear.general.cleanNumber(\"50.00000000001\")).toBe(50.00000000001);\n\texpect(ear.general.cleanNumber(undefined)).toBe(undefined);\n\texpect(ear.general.cleanNumber(true)).toBe(true);\n\texpect(ear.general.cleanNumber(false)).toBe(false);\n\tconst arr = [];\n\texpect(ear.general.cleanNumber(arr)).toBe(arr);\n});\n"
  },
  {
    "path": "tests/general.sort.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"sortPointsAlongVector empty\", () => {\n\texpect(ear.general.sortPointsAlongVector([], [1, 0])).toMatchObject([]);\n\texpect(ear.general.sortPointsAlongVector([,,,,,], [1, 0])).toMatchObject([,,,,,]);\n});\n\ntest(\"sortPointsAlongVector\", () => {\n\tconst points = [\n\t\t[4, -0.0001],\n\t\t[3, 0.0001],\n\t\t[2, -100],\n\t\t[1, 99],\n\t\t[0, 0],\n\t];\n\texpect(ear.general.sortPointsAlongVector(points, [1, 0])).toMatchObject([4, 3, 2, 1, 0]);\n\texpect(ear.general.sortPointsAlongVector(points, [-1, 0])).toMatchObject([0, 1, 2, 3, 4]);\n\texpect(ear.general.sortPointsAlongVector(points, [0, 1])).toMatchObject([2, 0, 4, 1, 3]);\n\texpect(ear.general.sortPointsAlongVector(points, [0, -1])).toMatchObject([3, 1, 4, 0, 2]);\n});\n\ntest(\"radialSortUnitVectors2\", () => {\n\tconst vectors = [\n\t\t[1, 0.5],\n\t\t[1, 1.5],\n\t\t[1, -1.5],\n\t\t[-1, -1.5],\n\t\t[-2, -5],\n\t\t[-2, 5],\n\t\t[-2, 2.2],\n\t\t[-2, 1],\n\t\t[2, -1.2],\n\t].map(ear.math.normalize);\n\tconst result = ear.general.radialSortUnitVectors2(vectors);\n\texpect(JSON.stringify(result))\n\t\t.toBe(JSON.stringify([0, 1, 5, 6, 7, 3, 4, 2, 8]));\n});\n"
  },
  {
    "path": "tests/general.string.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"toCamel\", () => {\n\n});\n\ntest(\"toKebab\", () => {\n\n});\n\ntest(\"capitalized\", () => {\n\n});\n"
  },
  {
    "path": "tests/generate.test.js",
    "content": "import fs from \"fs\";\nimport { test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"test can remain empty\", () => {});\n\n// test(\"write folded vertices\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-cycle-nonconvex.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \t// const graph = ear.graph.flattenFrame(fold, 1);\n// \tconst folded = ear.graph.makeVerticesCoordsFolded(fold, [0]);\n// \t// const folded = ear.graph.makeVerticesCoordsFlatFolded(fold);\n// \tconst foldedVertices = folded.map(p => p.map(n => ear.general.cleanNumber(n, 12)));\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/folded-vertices.json`,\n// \t\tJSON.stringify(foldedVertices, null, 2),\n// \t);\n// });\n\n// test(\"write layer solution to file\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n// \tconst solution = ear.layer.solveLayerOrders(folded);\n\n// \t// optional: delete faces_winding\n// \tdelete solution.faces_winding;\n\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/layer-solution.json`,\n// \t\tJSON.stringify(solution, null, 2),\n// \t);\n// });\n\n// test(\"write faceOrders\", () => {\n// \tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-twist.fold\", \"utf-8\");\n// \tconst graph = JSON.parse(FOLD);\n// \tconst foldedFrame = ear.graph.getFramesByClassName(graph, \"foldedForm\")[0];\n// \tconst faceOrders = ear.layer.layer3D(foldedFrame).faceOrders();\n// \tfs.writeFileSync(`./tests/tmp/faceOrders.fold`, JSON.stringify(faceOrders));\n// });\n\n// test(\"generate a graph with random edges\", () => {\n// \tconst COUNT = 500;\n// \tconst graph = {\n// \t\tvertices_coords: Array.from(Array(COUNT * 2))\n// \t\t\t.map(() => [Math.random(), Math.random()]),\n// \t\tedges_vertices: Array.from(Array(COUNT))\n// \t\t\t.map((_, i) => [i * 2, i * 2 + 1]),\n// \t\tedges_assignment: Array.from(Array(COUNT))\n// \t\t\t.map(() => [\"V\", \"M\", \"F\", \"U\"][Math.floor(Math.random() * 4)]),\n// \t};\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/non-planar-${COUNT}-random-lines.fold`,\n// \t\tJSON.stringify(graph),\n// \t);\n// });\n\n// test(\"generate an un-planarized set of random fold lines\", () => {\n// \tconst {\n// \t\tvertices_coords,\n// \t\tedges_vertices,\n// \t\tedges_assignment,\n// \t} = ear.graph.square();\n// \tconst graph = {\n// \t\tframe_classes: [\"creasePattern\"],\n// \t\tvertices_coords,\n// \t\tedges_vertices,\n// \t\tedges_assignment,\n// \t};\n\n// \tconst COUNT = 100;\n\n// \tconst boundary = ear.graph.boundaryPolygon(graph);\n\n// \tconst matrix = ear.math.makeMatrix2Reflect([1, 1], [0, 0]);\n\n// \tArray.from(Array(COUNT * 2)).forEach(() => {\n// \t\tif (graph.edges_vertices.length >= COUNT) { return; }\n\n// \t\tconst { lines } = ear.graph.getEdgesLine(graph);\n// \t\tconst lineA = lines[Math.floor(Math.random() * lines.length)];\n// \t\tconst lineB = lines[Math.floor(Math.random() * lines.length)];\n// \t\tconst results = ear.axiom.axiom3InPolygon(boundary, lineA, lineB)\n// \t\t\t.filter(a => a !== undefined);\n\n// \t\tif (!results.length) { return; }\n// \t\tconst result = results[Math.floor(Math.random() * results.length)];\n\n// \t\tconst degrees = Math.atan2(result.vector[1], result.vector[0]) * (180 / Math.PI);\n// \t\tif (!ear.math.epsilonEqual(Math.abs(degrees) % 22.5, 0)) { return; }\n\n// \t\tif (lines.some(line => ear.math.collinearLines2(line, result))) { return; }\n\n// \t\tconst assignment = [\"M\", \"V\", \"F\", \"U\"][Math.floor(Math.random() * 4)];\n// \t\t[result, ear.math.multiplyMatrix2Line2(matrix, result)].forEach(line => {\n// \t\t\tconst clip = ear.math.clipLineConvexPolygon(boundary, line);\n// \t\t\tconst vertices = ear.graph.addVertices(graph, clip);\n// \t\t\tear.graph.addEdge(graph, vertices, [], assignment);\n// \t\t});\n// \t});\n\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/non-planar-${COUNT}-lines.fold`,\n// \t\tJSON.stringify(graph),\n// \t);\n// });\n"
  },
  {
    "path": "tests/graph.add.edge.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"deprecated\", () => expect(true).toBe(true));\n\n// test(\"add edges\", () => {\n//   const graph = {};\n\n//   ear.graph.addEdges(graph, {\n//     edges_vertices: [\n//       ear.graph.addVertices(graph, [[0, 0], [1, 1]])\n//     ]\n//   });\n\n//   ear.graph.addEdges(graph, {\n//     edges_vertices: [\n//       ear.graph.addVertices(graph, [[2, 2], [3, 3]])\n//     ]\n//   });\n\n//   ear.graph.addEdges(graph, {\n//     edges_vertices: [\n//       ear.graph.addVertices(graph, [[1, 1], [2, 2]])\n//     ]\n//   });\n\n//   expect(JSON.stringify(graph.edges_vertices))\n//     .toBe(JSON.stringify([ [0, 1], [2, 3], [1, 2] ]));\n\n//   expect(JSON.stringify(graph.vertices_coords))\n//     .toBe(JSON.stringify([ [0, 0], [1, 1], [2, 2], [3, 3] ]));\n// });\n"
  },
  {
    "path": "tests/graph.add.vertex.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"deprecated\", () => expect(true).toBe(true));\n\n// test(\"add vertices simple\", () => {\n// \tconst graph = {\n// \t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n// \t\tedges_foldAngle: [0, 0, 0, 0, 90],\n// \t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"V\"],\n// \t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n// \t\tfaces_edges: [[0, 4, 3], [1, 4, 2]],\n// \t};\n\n// \tear.graph.addVertices(graph, [[0.33, 0.33], [0.5, 0.5]]);\n\n// \texpect(graph.vertices_coords[4][0]).toBe(0.33);\n// \texpect(graph.vertices_coords[5][0]).toBe(0.5);\n// \texpect(JSON.stringify(graph.edges_vertices))\n// \t\t.toBe(JSON.stringify([[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]]));\n// \texpect(JSON.stringify(graph.faces_vertices))\n// \t\t.toBe(JSON.stringify([[0, 1, 3], [2, 3, 1]]));\n// });\n\n// // test(\"add vertices duplicate vertices\", () => {\n// //   // this method will still add vertices, even if it's a duplicate\n// //   const graph = {\n// //     vertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n// //   };\n// //   ear.graph.addVertices(graph, [[0.33, 0.33], [1, 1]]);\n\n// //   expect(graph.vertices_coords[4][0]).toBe(0.33);\n// //   expect(graph.vertices_coords[4][1]).toBe(0.33);\n// //   expect(graph.vertices_coords[5][0]).toBe(1);\n// //   expect(graph.vertices_coords[5][1]).toBe(1);\n// // });\n\n// test(\"add vertices no vertices_coords\", () => {\n// \tconst graph = {\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n// \t\tedges_foldAngle: [0, 0, 0, 0, 90],\n// \t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"V\"],\n// \t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n// \t\tfaces_edges: [[0, 4, 3], [1, 4, 2]],\n// \t};\n// \tear.graph.addVertices(graph, [[0.33, 0.33], [0.5, 0.5]]);\n// \texpect(graph.vertices_coords[0][0]).toBe(0.33);\n// \texpect(graph.vertices_coords[1][0]).toBe(0.5);\n// });\n\n// test(\"add vertices no graph\", () => {\n// \tconst graph = {};\n// \tear.graph.addVertices(graph, [[0.33, 0.33], [0.5, 0.5]]);\n// \texpect(graph.vertices_coords[0][0]).toBe(0.33);\n// \texpect(graph.vertices_coords[1][0]).toBe(0.5);\n// });\n\n// test(\"add vertices duplicate\", () => {\n// \tconst graph = {\n// \t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n// \t};\n// \tconst result = ear.graph.addVertices(graph, [[0.5, 0.5], [1, 1]]);\n// \texpect(result[0]).toBe(4);\n// \texpect(result[1]).toBe(2);\n// });\n\n// test(\"add vertices unique\", () => {\n// \tconst graph = {\n// \t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n// \t\tedges_foldAngle: [0, 0, 0, 0, 90],\n// \t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"V\"],\n// \t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n// \t\tfaces_edges: [[0, 4, 3], [1, 4, 2]],\n// \t};\n\n// \tear.graph.addVertices(graph, [[0.33, 0.33], [0.5, 0.5]]);\n// });\n"
  },
  {
    "path": "tests/graph.boundary.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"boundaries\", () => {\n\tconst foldString = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldString);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst result = ear.graph.boundaries(graph);\n\texpect(result[0].vertices.length).toBe(6);\n\texpect(result[1].vertices.length).toBe(6);\n\texpect(result[2].vertices.length).toBe(4);\n\texpect(result[3].vertices.length).toBe(4);\n\texpect(result[4].vertices.length).toBe(4);\n\tresult.forEach(({ vertices, edges }) => {\n\t\texpect(vertices.length).toBe(edges.length);\n\t});\n});\n\ntest(\"bounding box\", () => {\n\tconst graph = ear.graph.square();\n\tconst box2d = ear.graph.boundingBox(graph);\n\t// make 3D\n\tgraph.vertices_coords.forEach((coord, i) => {\n\t\tgraph.vertices_coords[i] = [...coord, 0];\n\t});\n\tconst box3d = ear.graph.boundingBox(graph);\n\texpect(box2d.min.length).toBe(2);\n\texpect(box2d.max.length).toBe(2);\n\texpect(box2d.span.length).toBe(2);\n\texpect(box3d.min.length).toBe(3);\n\texpect(box3d.max.length).toBe(3);\n\texpect(box3d.span.length).toBe(3);\n\texpect(box3d.min[2]).toBeCloseTo(0);\n\texpect(box3d.max[2]).toBeCloseTo(0);\n\texpect(box3d.span[2]).toBeCloseTo(0);\n\tgraph.vertices_coords.push([1, 1, 1]);\n\tconst box3dUpdate = ear.graph.boundingBox(graph);\n\texpect(box3dUpdate.min[2]).toBeCloseTo(0);\n\texpect(box3dUpdate.max[2]).toBeCloseTo(1);\n\texpect(box3dUpdate.span[2]).toBeCloseTo(1);\n});\n\ntest(\"bounding vertices\", () => {\n\tconst fold = fs.readFileSync(\"./tests/files/fold/bird-disjoint-edges.fold\", \"utf-8\");\n\tconst graph = JSON.parse(fold);\n\tconst boundaryVertices = ear.graph.boundaryVertices(graph);\n\texpect(JSON.stringify(boundaryVertices))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]));\n\tgraph.edges_assignment = graph.edges_assignment.map(() => \"U\");\n\tconst boundaryVertices2 = ear.graph.boundaryVertices(graph);\n\texpect(JSON.stringify(boundaryVertices2)).toBe(JSON.stringify([]));\n\tdelete graph.edges_assignment;\n\tconst boundaryVertices3 = ear.graph.boundaryVertices(graph);\n\texpect(JSON.stringify(boundaryVertices3)).toBe(JSON.stringify([]));\n});\n\ntest(\"planar boundary\", () => {\n\tconst fish = ear.graph.fish();\n\tdelete fish.edges_assignment;\n\tdelete fish.edges_foldAngle;\n\tconst result = ear.graph.planarBoundary(fish);\n\texpect(JSON.stringify(result.vertices))\n\t\t.toBe(JSON.stringify([2, 3, 4, 5, 6, 7, 0, 1]));\n\texpect(JSON.stringify(result.edges))\n\t\t.toBe(JSON.stringify([2, 3, 4, 5, 6, 7, 0, 1]));\n});\n\ntest(\"planar boundaries, multiple boundaries\", () => {\n\tconst foldString = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldString);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst singleBoundary = ear.graph.planarBoundary(graph);\n\tconst boundaries = ear.graph.planarBoundaries(graph);\n\tconst allEdges = boundaries.flatMap(el => el.edges);\n\texpect(singleBoundary.edges.length < allEdges.length)\n\t\t.toBe(true);\n\texpect(allEdges.length).toBe(24);\n});\n"
  },
  {
    "path": "tests/graph.clean.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"duplicate edge\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [2, 1]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\"],\n\t});\n\texpect(graph.edges_vertices.length).toBe(5);\n\tgraph.clean();\n\texpect(graph.edges_vertices.length).toBe(4);\n});\n\ntest(\"circular edge\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [3, 3], [2, 3], [3, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\"],\n\t});\n\texpect(graph.edges_vertices.length).toBe(5);\n\tgraph.clean();\n\texpect(graph.edges_vertices.length).toBe(4);\n});\n\ntest(\"circular, duplicate edges and isolated, duplicate vertices\", () => {\n\t// these are all permutations of the same graph.\n\tconst graph1 = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 1], [1, 0], [0.5, 0.5], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 2], [2, 1], [2, 4], [3, 3], [4, 5], [5, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"M\", \"B\", \"B\"],\n\t});\n\tconst res1 = graph1.clean();\n\texpect(JSON.stringify(res1.vertices.map))\n\t\t.toBe(JSON.stringify([0, 1, 2, null, 1, 3]));\n\texpect(JSON.stringify(res1.vertices.remove))\n\t\t.toBe(JSON.stringify([4, 3]));\n\texpect(JSON.stringify(res1.edges.map))\n\t\t.toBe(JSON.stringify([0, 1, 1, null, 2, 3]));\n\texpect(JSON.stringify(res1.edges.remove))\n\t\t.toBe(JSON.stringify([3, 2]));\n\n\tconst graph2 = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 1], [1, 0], [0.5, 0.5], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 2], [2, 1], [3, 3], [4, 5], [5, 0], [2, 4]],\n\t\tedges_assignment: [\"B\", \"B\", \"M\", \"B\", \"B\", \"B\"],\n\t});\n\tconst res2 = graph2.clean();\n\texpect(JSON.stringify(res2.vertices.map))\n\t\t.toBe(JSON.stringify([0, 1, 2, null, 1, 3]));\n\texpect(JSON.stringify(res2.vertices.remove))\n\t\t.toBe(JSON.stringify([4, 3]));\n\texpect(JSON.stringify(res2.edges.map))\n\t\t.toBe(JSON.stringify([0, 1, null, 2, 3, 1]));\n\texpect(JSON.stringify(res2.edges.remove))\n\t\t.toBe(JSON.stringify([2, 5]));\n});\n"
  },
  {
    "path": "tests/graph.connectedComponents.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"connectedComponents\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst faceOrders = ear.layer.layer3D(graph).faceOrders();\n\tconst faces_set = ear.graph.connectedComponents(ear.graph.makeVerticesVerticesUnsorted({\n\t\tedges_vertices: faceOrders.map(ord => [ord[0], ord[1]]),\n\t}));\n\tfs.writeFileSync(\n\t\t`./tests/tmp/connectedFaces.json`,\n\t\tJSON.stringify(faces_set, null, 2),\n\t);\n});\n\ntest(\"connectedComponents on disjoint graphs\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst vertices_vertices = ear.graph.makeVerticesVertices(graph);\n\tconst vertices_group = ear.graph.connectedComponents(vertices_vertices);\n\tconst expected = [\n\t\t0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,\n\t];\n\tvertices_group.forEach((n, i) => expect(n).toBe(expected[i]));\n});\n"
  },
  {
    "path": "tests/graph.count.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"count, empty, invalid\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.countVertices();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"general function\", () => {\n\texpect(ear.graph.count({ robby_vertices: [\"a\", \"b\", \"c\", \"d\"] }, \"robby\"))\n\t\t.toBe(4);\n\texpect(ear.graph.count({ vertices_robby: [\"a\", \"b\", \"c\", \"d\"] }, \"robby\"))\n\t\t.toBe(0);\n});\n\ntest(\"count, empty\", () => {\n\texpect(ear.graph.countVertices({})).toBe(0);\n\texpect(ear.graph.countEdges({})).toBe(0);\n\texpect(ear.graph.countFaces({})).toBe(0);\n});\n\ntest(\"irrelevant arrays\", () => {\n\texpect(ear.graph.countVertices({\n\t\tfaces_edges: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t\tedges_vertices: [[4, 6], [11, 9], [14, 12], [11, 6]],\n\t})).toBe(0);\n\texpect(ear.graph.countEdges({\n\t\tvertices_coords: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t\tfaces_vertices: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(0);\n\texpect(ear.graph.countFaces({\n\t\tvertices_coords: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t\tedges_vertices: [[4, 6], [11, 9], [14, 12], [11, 6]],\n\t})).toBe(0);\n});\n\ntest(\"relevant arrays\", () => {\n\texpect(ear.graph.countVertices({\n\t\tvertices_coords: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t\tfaces_vertices: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(5);\n\texpect(ear.graph.countEdges({\n\t\tvertices_coords: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t\tedges_vertices: [[4, 6], [11, 9], [14, 12], [11, 6]],\n\t})).toBe(4);\n\texpect(ear.graph.countFaces({\n\t\tfaces_edges: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t\tedges_vertices: [[4, 6], [11, 9], [14, 12], [11, 6]],\n\t})).toBe(4);\n});\n\ntest(\"vertices count\", () => {\n\texpect(ear.graph.countVertices({\n\t\tvertices_coords: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t})).toBe(5);\n\texpect(ear.graph.countVertices({\n\t\tvertices_faces: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t})).toBe(5);\n});\n\ntest(\"non standard geometry\", () => {\n\texpect(ear.graph.countVertices({\n\t\tvertices_fakeGeometry: [[1, 0], [1, 1], [0, 1], [0.5, 0.5], [0, 0]],\n\t})).toBe(0);\n});\n\ntest(\"edges count\", () => {\n\texpect(ear.graph.countEdges({\n\t\tedges_vertices: [[4, 6], [11, 9], [14, 12], [11, 6]],\n\t})).toBe(4);\n});\n\ntest(\"edges count edgeOrders\", () => {\n\texpect(ear.graph.countEdges({\n\t\tedgeOrders: [[4, 6, 0], [11, 9, -1], [14, 12, 0], [11, 6, 1]],\n\t})).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.countImplied.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"implied count, empty, invalid\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.countImpliedVertices();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"implied count, empty\", () => {\n\texpect(ear.graph.countImpliedVertices({})).toBe(0);\n\texpect(ear.graph.countImpliedEdges({})).toBe(0);\n\texpect(ear.graph.countImpliedFaces({})).toBe(0);\n});\n\ntest(\"irrelevant arrays\", () => {\n\texpect(ear.graph.countImpliedEdges({\n\t\tfaces_vertices: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(0);\n\texpect(ear.graph.countImpliedFaces({\n\t\tfaces_vertices: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(0);\n\texpect(ear.graph.countImpliedVertices({\n\t\tvertices_edges: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(0);\n});\n\ntest(\"implied vertices\", () => {\n\texpect(ear.graph.countImpliedVertices({\n\t\tfaces_vertices: [[4, 6, 7], [11, 9, 6], [14, 12, 5], [11, 6, 9]],\n\t})).toBe(15);\n});\n\ntest(\"implied edgeOrders\", () => {\n\texpect(ear.graph.countImpliedEdges({\n\t\tedgeOrders: [[4, 6, 0], [11, 9, -1], [14, 12, 0], [11, 6, 1]],\n\t})).toBe(15);\n});\n"
  },
  {
    "path": "tests/graph.cycles.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"fixCycles, windmill\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst result = ear.graph.fixCycles(folded);\n\n\tfs.writeFileSync(\"./tests/tmp/fixCycles-windmill.fold\", JSON.stringify(result));\n});\n\ntest(\"fixCycles, cycle with non convex face in the center\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/layers-cycle-nonconvex.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst result = ear.graph.fixCycles(folded);\n\n\tfs.writeFileSync(\"./tests/tmp/fixCycles-nonconvex.fold\", JSON.stringify(result));\n});\n\ntest(\"fixCycles, no cycles, crane\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst result = ear.graph.fixCycles(folded);\n\n\tfs.writeFileSync(\"./tests/tmp/fixCycles-crane.fold\", JSON.stringify(result));\n});\n\ntest(\"fixCycles, no cycles, kraft-bird\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\tconst result = ear.graph.fixCycles(folded);\n\n\tfs.writeFileSync(\"./tests/tmp/fixCycles-kraft-bird-base.fold\", JSON.stringify(result));\n});\n"
  },
  {
    "path": "tests/graph.directedGraph.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"topologicalSort, Empty graph\", () => {\n\tconst directedEdges = [];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([]);\n});\n\ntest(\"topologicalSort, Single node graph\", () => {\n\tconst directedEdges = [[0, 0]];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0]);\n});\n\ntest(\"topologicalSort, Linear graph\", () => {\n\tconst directedEdges = [[0, 1], [1, 2], [2, 3]];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort, Graph with cycles\", () => {\n\tconst directedEdges = [[0, 1], [1, 2], [2, 0]];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\t// Since the graph contains a cycle, the result should be undefined\n\texpect(result).toMatchObject(undefined);\n});\n\ntest(\"topologicalSort, Graph with disconnected components\", () => {\n\tconst directedEdges = [[0, 1], [1, 2], [3, 4], [4, 5]];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0, 1, 2, 3, 4, 5]);\n});\n\ntest(\"topologicalSort, Graph with multiple possible orders\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [0, 2], [1, 3], [2, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort, Large acyclic graph\", () => {\n\tconst directedEdges = [];\n\tfor (let i = 0; i < 1000; i++) {\n\t\tdirectedEdges.push([i, i + 1]);\n\t}\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toHaveLength(1001);\n\texpect(result[0]).toBe(0);\n\texpect(result[1000]).toBe(1000);\n});\n\ntest(\"topologicalSort, Graph with self-referencing nodes\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 2], [2, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort, Graph with duplicate edges\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 3], [0, 1], [2, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject([0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort, Graph with multiple disconnected components\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject(undefined);\n});\n\ntest(\"topologicalSort, Graph with loops\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 3], [3, 1]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toMatchObject(undefined);\n});\n\ntest(\"topologicalSort, Graph with negative vertex indices\", () => {\n\tconst directedEdges = [\n\t\t[-1, 0], [0, 1], [1, 2], [2, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toEqual([-1, 0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort, Graph with negative and positive vertex indices\", () => {\n\tconst directedEdges = [\n\t\t[-2, -1], [-1, 0], [0, 1], [1, 2], [2, 3]\n\t];\n\tconst result = ear.graph.topologicalSort(directedEdges);\n\texpect(result).toEqual([-2, -1, 0, 1, 2, 3]);\n});\n\ntest(\"topologicalSort\", () => {\n\tconst ordering = ear.graph.topologicalSort([[5, 2], [2, 1], [5, 1], [1, 0], [1, 3]]);\n\texpect(ordering).toMatchObject([5, 2, 1, 0, 3]);\n});\n\ntest(\"topologicalSort, with cycle\", () => {\n\tconst ordering = ear.graph.topologicalSort([[5, 2], [2, 1], [1, 5]]);\n\texpect(ordering).toMatchObject(undefined);\n});\n\ntest(\"topologicalSort, crane\", () => {\n\tconst json = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(json);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tgraph.faces_normal = ear.graph.makeFacesNormal(graph);\n\tconst ordering = ear.graph.linearizeFaceOrders(graph);\n\tconst expected = [\n\t\t37, 46, 36, 30, 2, 39, 43, 35, 29, 3, 0, 4, 28, 15, 9, 27, 22, 16, 12, 20,\n\t\t11, 19, 10, 13, 8, 14, 26, 55, 54, 51, 58, 57, 52, 53, 56, 18, 21, 17, 23,\n\t\t25, 5, 24, 31, 34, 42, 45, 40, 32, 33, 41, 44, 38, 47, 50, 7, 48, 49, 6, 1,\n\t];\n\texpect(ordering).toMatchObject(expected);\n});\n\ntest(\"topologicalSort, subset of crane faces\", () => {\n\t// only contain orderings for a subset of the faces involved.\n\tconst json = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(json);\n\tconst crane = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst graph = {\n\t\tvertices_coords: crane.vertices_coords,\n\t\tfaces_vertices: crane.faces_vertices,\n\t\tfaceOrders: [\n\t\t\t[14, 12, -1],\n\t\t\t[14, 15, -1],\n\t\t\t[14, 11, 1],\n\t\t\t[14, 13, 1],\n\t\t\t[12, 15, -1],\n\t\t\t[11, 15, -1],\n\t\t\t[11, 12, -1],\n\t\t\t[20, 16, 1],\n\t\t\t[19, 20, -1],\n\t\t\t[19, 16, 1],\n\t\t\t[17, 18, -1],\n\t\t\t[18, 16, 1],\n\t\t\t[18, 19, 1],\n\t\t\t[18, 20, -1],\n\t\t\t[17, 16, 1],\n\t\t\t[17, 19, 1],\n\t\t\t[17, 20, -1],\n\t\t\t[13, 12, -1],\n\t\t\t[13, 15, -1],\n\t\t\t[13, 11, 1],\n\t\t],\n\t};\n\tgraph.faces_normal = ear.graph.makeFacesNormal(graph);\n\tconst ordering = ear.graph.linearizeFaceOrders(graph);\n\tconst expected = [15, 12, 11, 13, 14, 16, 20, 19, 18, 17];\n\texpect(ordering).toMatchObject(expected);\n});\n\ntest(\"topologicalSort, subset of crane faces. again\", () => {\n\t// only contain orderings for a subset of the faces involved.\n\tconst json = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(json);\n\tconst crane = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst graph = {\n\t\tvertices_coords: crane.vertices_coords,\n\t\tfaces_vertices: crane.faces_vertices,\n\t\tfaceOrders: [\n\t\t\t[32, 30, 1],\n\t\t\t[31, 35, -1],\n\t\t\t[32, 36, -1],\n\t\t\t[31, 39, -1],\n\t\t\t[32, 37, -1],\n\t\t\t[35, 39, -1],\n\t\t\t[36, 37, -1],\n\t\t\t[38, 30, 1],\n\t\t\t[33, 30, 1],\n\t\t\t[33, 36, -1],\n\t\t\t[38, 36, -1],\n\t\t\t[33, 37, -1],\n\t\t\t[38, 37, -1],\n\t\t\t[30, 36, -1],\n\t\t\t[30, 37, -1],\n\t\t\t[34, 35, -1],\n\t\t\t[34, 39, -1],\n\t\t\t[34, 31, 1],\n\t\t\t[33, 32, 1],\n\t\t\t[38, 32, 1],\n\t\t\t[38, 33, -1],\n\t\t],\n\t};\n\tgraph.faces_normal = ear.graph.makeFacesNormal(graph);\n\tconst ordering = ear.graph.linearizeFaceOrders(graph);\n\tconst expected = [37, 36, 30, 39, 35, 31, 32, 33, 34, 38];\n\texpect(ordering).toMatchObject(expected);\n});\n\ntest(\"topologicalSortQuick, Graph with cycles\", () => {\n\tconst directedEdges = [[0, 1], [1, 2], [2, 0]];\n\tconst result = ear.graph.topologicalSortQuick(directedEdges);\n\t// Since the graph contains a cycle, the result should be undefined\n\texpect(result).toMatchObject([1, 2, 0]);\n});\n\ntest(\"topologicalSortQuick, Graph with cycles\", () => {\n\tconst directedEdges = [[11, 12], [12, 13], [13, 14], [14, 15], [15, 11]];\n\tconst result = ear.graph.topologicalSortQuick(directedEdges);\n\t// Since the graph contains a cycle, the result should be undefined\n\texpect(result).toMatchObject([12, 13, 14, 15, 11]);\n});\n\ntest(\"topologicalSortQuick, Graph with multiple disconnected components\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]\n\t];\n\tconst result = ear.graph.topologicalSortQuick(directedEdges);\n\t// two separate cycles, two elements in this result\n\texpect(result).toMatchObject([1, 2, 0, 4, 5, 3]);\n});\n\ntest(\"topologicalSortQuick, Graph with loops\", () => {\n\tconst directedEdges = [\n\t\t[0, 1], [1, 2], [2, 3], [3, 1]\n\t];\n\tconst result = ear.graph.topologicalSortQuick(directedEdges);\n\texpect(result).toMatchObject([0, 2, 3, 1]);\n});\n"
  },
  {
    "path": "tests/graph.disjoint.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"disjointGraphsIndices\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldFile);\n\tconst indices = ear.graph.disjointGraphsIndices(graph);\n\texpect(indices.length).toBe(5);\n});\n\ntest(\"disjointGraphs\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldFile);\n\tconst indices = ear.graph.disjointGraphs(graph);\n\texpect(indices.length).toBe(5);\n});\n\ntest(\"disjointGraphsIndices, connected graph\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldFile);\n\tconst indices = ear.graph.disjointGraphsIndices(graph);\n\texpect(indices.length).toBe(1);\n});\n"
  },
  {
    "path": "tests/graph.edges.circular.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst arraysMatch = (a, b) => a.forEach((_, i) => expect(a[i]).toBe(b[i]));\n\ntest(\"circular edges\", () => {\n\tconst graph = {\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 3], [2, 2], [4, 1], [3, 0],\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[0, 1, 2, 3], [0, 3, 4],\n\t\t],\n\t};\n\tconst res = ear.graph.removeCircularEdges(graph);\n\texpect(res[2]).toBe(undefined);\n\tarraysMatch(graph.faces_edges[0], [0, 1, 2]);\n\tarraysMatch(graph.faces_edges[1], [0, 2, 3]);\n});\n"
  },
  {
    "path": "tests/graph.edges.duplicate.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"duplicate edges\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 4], [4, 2], [2, 3], [3, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\t\tfaces_vertices: [[0, 1, 2, 4, 2, 3]],\n\t\tfaces_edges: [[0, 1, 2, 3, 4, 5]],\n\t};\n\tear.graph.populate(graph);\n\n\tconst duplicates = ear.graph.duplicateEdges(graph);\n\texpect(JSON.stringify(duplicates)).toBe(JSON.stringify([,,, 2]));\n\n\tear.graph.removeDuplicateEdges(graph);\n\n\t// removeDuplicateEdges now automatically fixes the vertices\n\t// we don't need to run populate\n\t// expect(graph.vertices_vertices[2].length).toBe(4);\n\t// expect(graph.vertices_edges[2].length).toBe(4);\n\t// ear.graph.populate(graph);\n\n\texpect(graph.vertices_vertices[2].length).toBe(3);\n\texpect(graph.vertices_edges[2].length).toBe(3);\n});\n\ntest(\"duplicate edges\", () => {\n\tconst graph = {\n\t\tedges_vertices: [\n\t\t\t[0, 1],\n\t\t\t[1, 2],\n\t\t\t[2, 3],\n\t\t\t[3, 0],\n\t\t\t[0, 3],\n\t\t\t[0, 2],\n\t\t\t[1, 3],\n\t\t\t[0, 2],\n\t\t\t[0, 4],\n\t\t\t[1, 4],\n\t\t\t[2, 4],\n\t\t\t[3, 4],\n\t\t\t[4, 0],\n\t\t],\n\t};\n\tconst result = ear.graph.duplicateEdges(graph);\n\texpect(result[4]).toBe(3);\n\texpect(result[7]).toBe(5);\n\texpect(result[12]).toBe(8);\n\texpect(JSON.stringify(result)).toBe(JSON.stringify([,,,, 3,,, 5,,,,, 8]));\n});\n\ntest(\"invalid edges\", () => {\n\tconst graph1 = {\n\t\tedges_vertices: [\n\t\t\t[0, 1, 2],\n\t\t\t[3, 4, 5],\n\t\t\t[2, 1, 0],\n\t\t],\n\t};\n\tconst result1 = ear.graph.duplicateEdges(graph1);\n\texpect(result1[0]).toBe(undefined);\n\texpect(result1[1]).toBe(undefined);\n\texpect(result1[2]).toBe(0);\n\n\tconst graph2 = {\n\t\tedges_vertices: [\n\t\t\t[0, 1, 2],\n\t\t\t[3, 4, 5],\n\t\t\t[0, 1, 2],\n\t\t],\n\t};\n\tconst result2 = ear.graph.duplicateEdges(graph2);\n\texpect(result2[0]).toBe(undefined);\n\texpect(result2[1]).toBe(undefined);\n\texpect(result2[2]).toBe(0);\n});\n\ntest(\"duplicate edges, invalid input 1\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.duplicateEdges();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"duplicate edges, invalid input 2\", () => {\n\tconst result = ear.graph.duplicateEdges({});\n\texpect(result.length).toBe(0);\n});\n\ntest(\"duplicate edges, with undefined\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.duplicateEdges({\n\t\t\tedges_vertices: [\n\t\t\t\t[0, 1],\n\t\t\t\tundefined,\n\t\t\t\t[1, 0],\n\t\t\t],\n\t\t});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"duplicate edges\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 4], [4, 2], [2, 3], [3, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\t\tfaces_vertices: [[0, 1, 2, 4, 2, 3]],\n\t\tfaces_edges: [[0, 1, 2, 3, 4, 5]],\n\t};\n\tear.graph.populate(graph);\n\texpect(graph.faces_faces).toMatchObject([[\n\t\tundefined, undefined, undefined, undefined, undefined, undefined\n\t]]);\n});\n\ntest(\"similar edges bird base\", () => {\n\tconst cp = ear.graph.bird();\n\tconst graph = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\texpect(ear.graph.getSimilarEdges(graph)).toMatchObject([\n\t\t[5, 6, 0, 26, 7, 2, 1, 4, 30, 3],\n\t\t[14, 13, 15, 12],\n\t\t[25, 24],\n\t\t[8, 10, 9, 11],\n\t\t[20, 33, 22, 29],\n\t\t[31, 27],\n\t\t[32, 18, 16, 28],\n\t\t[19, 17],\n\t\t[21, 23],\n\t]);\n});\n\ntest(\"similar edges crane\", () => {\n\tconst json = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(json);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\texpect(ear.graph.getSimilarEdges(graph)).toMatchObject([\n\t\t[11],\n\t\t[15],\n\t\t[67, 69],\n\t\t[56, 65],\n\t\t[62],\n\t\t[17, 79, 23, 83, 24, 25, 26, 89],\n\t\t[80],\n\t\t[90, 110, 106, 100],\n\t\t[30, 28, 64],\n\t\t[87, 98, 101, 103, 107],\n\t\t[99, 105, 109, 113],\n\t\t[10, 13, 57],\n\t\t[22, 55],\n\t\t[85, 58],\n\t\t[86],\n\t\t[61, 60],\n\t\t[66],\n\t\t[59],\n\t\t[12, 84],\n\t\t[18, 9],\n\t\t[31, 40, 46, 27],\n\t\t[102, 88, 108, 104, 97],\n\t\t[16, 78],\n\t\t[36, 37, 34, 35, 38, 19, 32, 33],\n\t\t[41, 51, 39, 14, 47],\n\t\t[29, 50],\n\t\t[70, 68],\n\t\t[0],\n\t\t[1, 6],\n\t\t[4, 52],\n\t\t[8],\n\t\t[63],\n\t\t[81],\n\t\t[73],\n\t\t[20, 45, 49, 43],\n\t\t[77, 44, 48],\n\t\t[42],\n\t\t[74],\n\t\t[2, 5, 54],\n\t\t[3],\n\t\t[95, 96, 111, 112, 91, 92, 93, 94],\n\t\t[53, 76],\n\t\t[72],\n\t\t[71, 75],\n\t\t[7, 21],\n\t\t[82]\n\t])\n});\n"
  },
  {
    "path": "tests/graph.edges.lines.test.js",
    "content": "import fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"edgesToLines and edgesToLines3\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst folded = JSON.parse(FOLD);\n\n\tconst edgesLine = ear.graph.edgesToLines(folded);\n\tconst edgesLine3 = ear.graph.edgesToLines3(folded);\n\n\tedgesLine.forEach((_, i) => [0, 1, 2].forEach(n => {\n\t\texpect(edgesLine[i].vector[n]).toBeCloseTo(edgesLine3[i].vector[n]);\n\t\texpect(edgesLine[i].origin[n]).toBeCloseTo(edgesLine3[i].origin[n]);\n\t}));\n});\n\ntest(\"edgesLine with empty arrays\", () => {\n\texpect(ear.graph.getEdgesLine({\n\t\tvertices_coords: [,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,],\n\t\tedges_vertices: [,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]\n\t})).toMatchObject({ lines: [], edges_line: [] });\n})\n\ntest(\"fish base\", () => {\n\tconst graph = ear.graph.fish();\n\tconst { lines, edges_line } = ear.graph.getEdgesLine(graph);\n\tconst n029 = 1 - Math.SQRT1_2;\n\tconst n070 = Math.SQRT1_2;\n\tconst expected = [\n\t\t{ vector: [1, 0], origin: [0, 0] }, // bottom\n\t\t{ vector: [0, -1], origin: [0, 1] }, // left\n\t\t{ vector: [-n070, -n029], origin: [n070, n029] },\n\t\t{ vector: [-n029, -n070], origin: [n029, n070] },\n\t\t{ vector: [-1, -1], origin: [1, 1] }, // diagonal\n\t\t{ vector: [n029, 0], origin: [n070, n029] },\n\t\t{ vector: [0, n029], origin: [n029, n070] },\n\t\t{ vector: [n029, n070], origin: [n070, n029] },\n\t\t{ vector: [n070, n029], origin: [n029, n070] },\n\t\t{ vector: [1, -1], origin: [0, 1] }, // diagonal\n\t\t{ vector: [0, -n029], origin: [n070, n029] },\n\t\t{ vector: [-n029, 0], origin: [n029, n070] },\n\t\t{ vector: [0, 1], origin: [1, 0] }, // right\n\t\t{ vector: [-1, 0], origin: [1, 1] }, // top\n\t];\n\tlines.forEach((line, i) => expect(\n\t\tear.math.epsilonEqualVectors(line.vector, expected[i].vector),\n\t).toBe(true));\n\tlines.forEach((line, i) => expect(\n\t\tear.math.epsilonEqualVectors(line.origin, expected[i].origin),\n\t).toBe(true));\n});\n\ntest(\"maze folding\", () => {\n\tconst svg = fs.readFileSync(\"./tests/files/svg/maze-8x8.svg\", \"utf-8\");\n\tconst graph = ear.convert.svgEdgeGraph(svg);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeEdgeGraph.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n\tconst { lines, edges_line } = ear.graph.getEdgesLine(graph);\n\tconst edgesValid = graph.edges_vertices\n\t\t.map(ev => ev.map(v => graph.vertices_coords[v]))\n\t\t.map((coords, i) => coords\n\t\t\t.map(coord => ear.math.overlapLinePoint(lines[edges_line[i]], coord)))\n\t\t.map(pair => pair[0] && pair[1])\n\t\t.map((valid, i) => (!valid ? i : undefined))\n\t\t.filter(a => a !== undefined);\n\texpect(edgesValid.length).toBe(0);\n});\n\ntest(\"parallel same distance 3d\", () => {\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[1, 2, 3], [7, 2, 6], // random values\n\t\t\t[-1, 5, 5], [1, 5, 5],\n\t\t\t[8, 3, 5], [1, 3, 2], // random values\n\t\t\t[-1, -5, 5], [1, -5, 5],\n\t\t\t[9, 2, 7], [2, 8, 4], // random values\n\t\t\t[-1, -5, -5], [1, -5, -5],\n\t\t\t[8, 5, 9], [4, 5, 1], // random values\n\t\t\t[-1, 5, -5], [1, 5, -5],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13], [14, 15],\n\t\t],\n\t};\n\tconst result = ear.graph.getEdgesLine(graph);\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(result.edges_line);\n\tlines_edges.forEach(el => expect(el.length).toBe(1));\n\texpect(result.lines.length).toBe(8);\n});\n\ntest(\"parallel same distance 3d\", () => {\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[-1, 5, 5], [1, 5, 5],\n\t\t\t[-1, -5, 5], [1, -5, 5],\n\t\t\t[-1, -5, -5], [1, -5, -5],\n\t\t\t[-1, 5, -5], [1, 5, -5],\n\t\t],\n\t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n\t};\n\tconst result = ear.graph.getEdgesLine(graph);\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(result.edges_line);\n\tlines_edges.forEach(el => expect(el.length).toBe(1));\n\texpect(result.lines.length).toBe(4);\n});\n\ntest(\"getEdgesLine, parallel lines at same distance in 3D, circle XY\", () => {\n\tconst graph = {\n\t\tvertices_coords: Array.from(Array(16))\n\t\t\t.map((_, i) => (i / 16) * (Math.PI * 2))\n\t\t\t.flatMap(a => [\n\t\t\t\t[Math.cos(a), Math.sin(a), -1],\n\t\t\t\t[Math.cos(a), Math.sin(a), 1],\n\t\t\t]),\n\t\tedges_vertices: Array.from(Array(16))\n\t\t\t.map((_, i) => [i * 2, i * 2 + 1]),\n\t};\n\tconst result = ear.graph.getEdgesLine(graph);\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(result.edges_line);\n\tlines_edges.forEach(el => expect(el.length).toBe(1));\n\texpect(result.lines.length).toBe(16);\n});\n\ntest(\"getEdgesLine, parallel lines at same distance in 3D, circle XZ\", () => {\n\tconst graph = {\n\t\tvertices_coords: Array.from(Array(16))\n\t\t\t.map((_, i) => (i / 16) * (Math.PI * 2))\n\t\t\t.flatMap(a => [\n\t\t\t\t[Math.cos(a), -1, Math.sin(a)],\n\t\t\t\t[Math.cos(a), 1, Math.sin(a)],\n\t\t\t]),\n\t\tedges_vertices: Array.from(Array(16))\n\t\t\t.map((_, i) => [i * 2, i * 2 + 1]),\n\t};\n\tconst result = ear.graph.getEdgesLine(graph);\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(result.edges_line);\n\tlines_edges.forEach(el => expect(el.length).toBe(1));\n\texpect(result.lines.length).toBe(16);\n});\n\ntest(\"getEdgesLine, parallel lines at same distance in 3D, circle YZ\", () => {\n\tconst graph = {\n\t\tvertices_coords: Array.from(Array(16))\n\t\t\t.map((_, i) => (i / 16) * (Math.PI * 2))\n\t\t\t.flatMap(a => [\n\t\t\t\t[-1, Math.cos(a), Math.sin(a)],\n\t\t\t\t[1, Math.cos(a), Math.sin(a)],\n\t\t\t]),\n\t\tedges_vertices: Array.from(Array(16))\n\t\t\t.map((_, i) => [i * 2, i * 2 + 1]),\n\t};\n\tconst result = ear.graph.getEdgesLine(graph);\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(result.edges_line);\n\tlines_edges.forEach(el => expect(el.length).toBe(1));\n\texpect(result.lines.length).toBe(16);\n});\n\ntest(\"getEdgesLine, Mooser's train, one fourth of carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage-fourth.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t};\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tlines,\n\t\tedges_line,\n\t} = ear.graph.getEdgesLine(folded);\n\n\tconst bruteForce = ear.graph.getEdgesLineBruteForce(folded);\n\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(edges_line);\n\n\t// console.log(JSON.stringify(lines.map(({ vector, origin }) => ({\n\t// \tvector: vector.map(n => ear.general.cleanNumber(n, 12)),\n\t// \torigin: origin.map(n => ear.general.cleanNumber(n, 12)),\n\t// }))));\n\n\t// console.log(JSON.stringify(lines.map(({ vector, origin }) => ({\n\t// \tvector: vector.map(n => ear.general.cleanNumber(n, 12)),\n\t// \torigin: origin.map(n => ear.general.cleanNumber(n, 12)),\n\t// }))));\n\t// console.log(JSON.stringify(edges_line));\n\t// console.log(JSON.stringify(lines_edges));\n\n\tconst expectedLines = [\n\t\t{ vector: [-8, 0, 0], origin: [20, 0, 0] },\n\t\t{ vector: [-2, 0, 0], origin: [17, 1, 0] },\n\t\t{ vector: [-2, 0, 0], origin: [12, 0, 2] },\n\t\t{ vector: [-2, 0, 0], origin: [17, 0, -2] },\n\t\t{ vector: [-2, 0, 0], origin: [17, 1, -2] },\n\t\t{ vector: [2, 0, 0], origin: [10, 2, 2] },\n\t\t{ vector: [8, 0, 0], origin: [12, 4, 0] },\n\t\t{ vector: [-4, 0, 0], origin: [16, 0, 4] },\n\t\t{ vector: [2, 0, 0], origin: [10, 4, 2] },\n\t\t{ vector: [8, 0, 0], origin: [12, 0, 8] },\n\t\t{ vector: [-4, -4, 0], origin: [16, 4, 0] },\n\t\t{ vector: [4, 0, 4], origin: [12, 0, 0] },\n\t\t{ vector: [8, 0, 0], origin: [12, 4, 8] },\n\t\t{ vector: [0, -4, 0], origin: [10, 4, 2] },\n\t\t{ vector: [1, -1, 0], origin: [15, 1, 0] },\n\t\t{ vector: [-1, -1, 0], origin: [17, 1, 0] },\n\t\t{ vector: [0, -4, 0], origin: [12, 4, 0] },\n\t\t{ vector: [0, 0, 8], origin: [12, 0, 0] },\n\t\t{ vector: [0, -2, -2], origin: [12, 2, 2] },\n\t\t{ vector: [0, -4, 0], origin: [12, 4, 2] },\n\t\t{ vector: [0, -2, 2], origin: [12, 2, 2] },\n\t\t{ vector: [0, 0, -8], origin: [12, 4, 8] },\n\t\t{ vector: [1, -1, 0], origin: [16, 4, 0] },\n\t\t{ vector: [-4, 0, 4], origin: [16, 0, 4] },\n\t\t{ vector: [0, -4, 0], origin: [12, 4, 8] },\n\t\t{ vector: [0, 0, 2], origin: [15, 0, -2] },\n\t\t{ vector: [0, 3, 0], origin: [15, 0, 0] },\n\t\t{ vector: [0, 0, 2], origin: [15, 1, -2] },\n\t\t{ vector: [0, 1, 0], origin: [15, 0, -2] },\n\t\t{ vector: [0, -2, -2], origin: [15, 3, 0] },\n\t\t{ vector: [0, 4, 0], origin: [16, 0, 0] },\n\t\t{ vector: [0, 0, 4], origin: [16, 0, 0] },\n\t\t{ vector: [0, 0, -2], origin: [17, 0, 0] },\n\t\t{ vector: [0, 3, 0], origin: [17, 0, 0] },\n\t\t{ vector: [0, 0, 2], origin: [17, 1, -2] },\n\t\t{ vector: [0, 1, 0], origin: [17, 0, -2] },\n\t\t{ vector: [0, -2, -2], origin: [17, 3, 0] },\n\t\t{ vector: [0, 0, -8], origin: [20, 0, 8] },\n\t\t{ vector: [0, 4, 0], origin: [20, 0, 0] },\n\t\t{ vector: [0, -4, 0], origin: [20, 4, 8] },\n\t];\n\n\texpect(expectedLines.length).toBe(bruteForce.lines.length);\n\n\tlines.forEach((_, i) => [0, 1, 2].forEach(d => {\n\t\texpect(lines[i].vector[d]).toBeCloseTo(expectedLines[i].vector[d]);\n\t\texpect(lines[i].origin[d]).toBeCloseTo(expectedLines[i].origin[d]);\n\t}));\n\n\texpect(lines_edges).toMatchObject([\n\t\t// outside edge\n\t\t[0, 1, 5, 6, 41, 42, 43, 66],\n\t\t[27],\n\t\t// front-outside edge of hitch joint\n\t\t[8, 45, 63, 73],\n\t\t// one of three faces for the wheel\n\t\t[3],\n\t\t// bottom of wheel\n\t\t[16],\n\t\t[56, 69],\n\t\t[36, 38],\n\t\t[61],\n\t\t// back of hitch\n\t\t[34, 82],\n\t\t[76],\n\t\t// 45 degree connection to the wheel part\n\t\t[25, 53, 71],\n\t\t[70],\n\t\t[85],\n\t\t// back of hitch joint\n\t\t[9, 10, 11, 12, 13, 14, 15],\n\t\t[40],\n\t\t[59],\n\t\t[29, 32, 33],\n\t\t// back-bottom of carriage, below the hitch joint\n\t\t[7, 30, 31, 44, 62, 65, 74, 75],\n\t\t// small 45 deg to hitch joint\n\t\t[51, 52],\n\t\t// front of hitch joint, alongside carriage\n\t\t[17, 18, 19, 20, 21, 22, 23],\n\t\t// small 45 deg to hitch joint\n\t\t[39, 60],\n\t\t// back edge of the back face of the carriage\n\t\t[35, 83, 84],\n\t\t[49],\n\t\t// big side carriage panel, 45 degree\n\t\t[24],\n\t\t[64],\n\t\t// one of three faces for the wheel\n\t\t[4],\n\t\t[28, 55],\n\t\t[58],\n\t\t[57],\n\t\t[54],\n\t\t[37, 46, 47, 77],\n\t\t[48],\n\t\t// one of three faces for the wheel\n\t\t[2],\n\t\t[26, 72],\n\t\t[68],\n\t\t[67],\n\t\t[50],\n\t\t[79],\n\t\t[80, 81],\n\t\t[78]\n\t]);\n});\n\ntest(\"getEdgesLine, Mooser's train, carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t}\n\n\tconst {\n\t\tlines,\n\t\tedges_line,\n\t} = ear.graph.getEdgesLine(folded);\n\n\tconst lines_edges = ear.graph.invertFlatToArrayMap(edges_line);\n\n\tconst expectedLines = [\n\t\t{ vector: [-20, 0, 0], origin: [40, 0, 0] },\n\t\t{ vector: [16, 0, 0], origin: [22, 0, -2] },\n\t\t{ vector: [20, 0, 0], origin: [20, 2, 0] },\n\t\t{ vector: [-16, 0, 0], origin: [38, 0, 2] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 1, -2] },\n\t\t{ vector: [20, 0, 0], origin: [20, 4, 0] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 0, -4] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 1, -4] },\n\t\t{ vector: [16, 0, 0], origin: [22, 4, -2] },\n\t\t{ vector: [20, 0, 0], origin: [20, 6, 0] },\n\t\t{ vector: [16, 0, 0], origin: [22, 0, 6] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 7, -2] },\n\t\t{ vector: [-20, 0, 0], origin: [40, 8, 0] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 7, -4] },\n\t\t{ vector: [-16, 0, 0], origin: [38, 8, -2] },\n\t\t{ vector: [-16, 0, 0], origin: [38, 8, 2] },\n\t\t{ vector: [-10, 0, 0], origin: [35, 8, -4] },\n\t\t{ vector: [16, 0, 0], origin: [22, 8, 6] },\n\t\t{ vector: [1, 1, 0], origin: [25, 7, -2] },\n\t\t{ vector: [5, 5, 0], origin: [22, 0, -2] },\n\t\t{ vector: [4, 0, 4], origin: [22, 0, -2] },\n\t\t{ vector: [-8, -8, 0], origin: [34, 8, -2] },\n\t\t{ vector: [1, -1, 0], origin: [25, 1, -2] },\n\t\t{ vector: [4, 0, 4], origin: [22, 8, -2] },\n\t\t{ vector: [-4, 0, 4], origin: [26, 0, 2] },\n\t\t{ vector: [0, 8, 0], origin: [20, 0, 0] },\n\t\t{ vector: [5, -5, 0], origin: [22, 8, -2] },\n\t\t{ vector: [-5, -5, 0], origin: [38, 8, -2] },\n\t\t{ vector: [4, 0, -4], origin: [22, 8, 6] },\n\t\t{ vector: [0, 0, 8], origin: [22, 0, -2] },\n\t\t{ vector: [0, 8, 0], origin: [22, 0, 0] },\n\t\t{ vector: [0, -2, 2], origin: [22, 2, 0] },\n\t\t{ vector: [0, -2, -2], origin: [22, 2, 0] },\n\t\t{ vector: [0, -8, 0], origin: [22, 8, -2] },\n\t\t{ vector: [0, 0, -2], origin: [22, 4, 0] },\n\t\t{ vector: [0, 2, 2], origin: [22, 6, 0] },\n\t\t{ vector: [0, 2, -2], origin: [22, 6, 0] },\n\t\t{ vector: [-4, 0, -4], origin: [38, 0, 6] },\n\t\t{ vector: [0, -8, 0], origin: [22, 8, 6] },\n\t\t{ vector: [0, 0, -8], origin: [22, 8, 6] },\n\t\t{ vector: [-4, 0, -4], origin: [38, 8, 6] },\n\t\t{ vector: [1, 1, 0], origin: [34, 0, -2] },\n\t\t{ vector: [-8, 8, 0], origin: [34, 0, -2] },\n\t\t{ vector: [0, 0, 2], origin: [25, 0, -4] },\n\t\t{ vector: [0, 0, 2], origin: [25, 1, -4] },\n\t\t{ vector: [0, 8, 0], origin: [25, 0, -2] },\n\t\t{ vector: [0, 2, -2], origin: [25, 5, -2] },\n\t\t{ vector: [0, -2, -2], origin: [25, 3, -2] },\n\t\t{ vector: [0, 8, 0], origin: [25, 0, -4] },\n\t\t{ vector: [-4, 0, 4], origin: [38, 0, -2] },\n\t\t{ vector: [0, 0, -2], origin: [25, 7, -2] },\n\t\t{ vector: [0, 0, 4], origin: [26, 0, -2] },\n\t\t{ vector: [0, -8, 0], origin: [26, 8, -2] },\n\t\t{ vector: [0, 0, 2], origin: [25, 8, -4] },\n\t\t{ vector: [-4, 0, 4], origin: [38, 8, -2] },\n\t\t{ vector: [-5, 5, 0], origin: [38, 0, -2] },\n\t\t{ vector: [0, 0, -2], origin: [27, 0, -2] },\n\t\t{ vector: [0, 0, 2], origin: [27, 1, -4] },\n\t\t{ vector: [0, 8, 0], origin: [27, 0, -2] },\n\t\t{ vector: [0, 2, -2], origin: [27, 5, -2] },\n\t\t{ vector: [0, 0, -4], origin: [26, 8, 2] },\n\t\t{ vector: [0, -2, -2], origin: [27, 3, -2] },\n\t\t{ vector: [0, 8, 0], origin: [27, 0, -4] },\n\t\t{ vector: [0, 0, -2], origin: [27, 7, -2] },\n\t\t{ vector: [0, 0, -2], origin: [27, 8, -2] },\n\t\t{ vector: [1, -1, 0], origin: [34, 8, -2] },\n\t\t{ vector: [0, 0, 2], origin: [33, 0, -4] },\n\t\t{ vector: [0, 0, 2], origin: [33, 1, -4] },\n\t\t{ vector: [0, 8, 0], origin: [33, 0, -2] },\n\t\t{ vector: [0, 2, -2], origin: [33, 5, -2] },\n\t\t{ vector: [0, -2, -2], origin: [33, 3, -2] },\n\t\t{ vector: [0, 8, 0], origin: [33, 0, -4] },\n\t\t{ vector: [0, 0, -2], origin: [33, 7, -2] },\n\t\t{ vector: [0, 0, 2], origin: [33, 8, -4] },\n\t\t{ vector: [0, 0, 4], origin: [34, 0, -2] },\n\t\t{ vector: [0, 8, 0], origin: [34, 0, -2] },\n\t\t{ vector: [0, 0, -4], origin: [34, 8, 2] },\n\t\t{ vector: [0, 0, -2], origin: [35, 0, -2] },\n\t\t{ vector: [0, 0, 2], origin: [35, 1, -4] },\n\t\t{ vector: [0, -8, 0], origin: [35, 8, -2] },\n\t\t{ vector: [0, 2, -2], origin: [35, 5, -2] },\n\t\t{ vector: [0, -2, -2], origin: [35, 3, -2] },\n\t\t{ vector: [0, 8, 0], origin: [35, 0, -4] },\n\t\t{ vector: [0, 0, -2], origin: [35, 7, -2] },\n\t\t{ vector: [0, 0, -2], origin: [35, 8, -2] },\n\t\t{ vector: [0, 0, -8], origin: [38, 0, 6] },\n\t\t{ vector: [0, -8, 0], origin: [38, 8, 0] },\n\t\t{ vector: [0, 2, -2], origin: [38, 0, 2] },\n\t\t{ vector: [0, -2, -2], origin: [38, 2, 0] },\n\t\t{ vector: [0, -8, 0], origin: [38, 8, -2] },\n\t\t{ vector: [0, 0, 2], origin: [38, 4, -2] },\n\t\t{ vector: [0, -2, -2], origin: [38, 8, 2] },\n\t\t{ vector: [0, 2, -2], origin: [38, 6, 0] },\n\t\t{ vector: [0, -8, 0], origin: [38, 8, 6] },\n\t\t{ vector: [0, 0, 8], origin: [38, 8, -2] },\n\t\t{ vector: [0, -8, 0], origin: [40, 8, 0] },\n\t];\n\n\tconst expectedLinesEdges = [\n\t\t[0, 16, 77, 85, 111, 116, 136, 142],\n\t\t[2, 3, 7, 8, 9, 13, 14, 79, 80, 81, 82, 83, 121, 189],\n\t\t[103, 104, 126, 127],\n\t\t[113, 114],\n\t\t[50, 53],\n\t\t[64, 72, 226, 234],\n\t\t[5, 11],\n\t\t[30, 31],\n\t\t[66, 68, 70, 228, 230, 232],\n\t\t[173, 174, 194, 195],\n\t\t[139],\n\t\t[239, 242],\n\t\t[160, 166, 179, 184, 200, 208, 259, 275],\n\t\t[257, 258],\n\t\t[117, 185, 202, 203, 204, 205, 206, 261, 262, 266, 267, 268, 272, 273],\n\t\t[181, 182],\n\t\t[264, 270],\n\t\t[163],\n\t\t[215],\n\t\t[47, 99, 133, 236],\n\t\t[132],\n\t\t[109, 291],\n\t\t[75],\n\t\t[129],\n\t\t[45],\n\t\t[17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],\n\t\t[93, 131, 154, 198],\n\t\t[46, 150, 290, 294],\n\t\t[172],\n\t\t[15, 60, 61, 84, 115, 120, 137, 138],\n\t\t[32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44],\n\t\t[73, 110],\n\t\t[95, 98],\n\t\t[55, 56, 59, 62, 63],\n\t\t[65, 227],\n\t\t[148, 158],\n\t\t[149, 157],\n\t\t[199],\n\t\t[119],\n\t\t[57, 58, 118, 161, 162, 183, 207, 274],\n\t\t[48],\n\t\t[159],\n\t\t[145, 146],\n\t\t[12],\n\t\t[106],\n\t\t[54, 101, 102, 243],\n\t\t[155],\n\t\t[100],\n\t\t[105, 108],\n\t\t[128],\n\t\t[107],\n\t\t[88],\n\t\t[67, 86, 87, 90, 91, 143, 144, 229],\n\t\t[271],\n\t\t[289],\n\t\t[96, 130, 152, 171],\n\t\t[10],\n\t\t[123],\n\t\t[52, 134, 135, 241],\n\t\t[237],\n\t\t[89],\n\t\t[94],\n\t\t[122, 125],\n\t\t[124],\n\t\t[269],\n\t\t[74],\n\t\t[6],\n\t\t[176],\n\t\t[51, 169, 170, 240],\n\t\t[97],\n\t\t[151],\n\t\t[175, 178],\n\t\t[177],\n\t\t[265],\n\t\t[211],\n\t\t[69, 167, 168, 209, 210, 213, 214, 231],\n\t\t[212],\n\t\t[4],\n\t\t[191],\n\t\t[49, 196, 197, 238],\n\t\t[295],\n\t\t[153],\n\t\t[190, 193],\n\t\t[192],\n\t\t[263],\n\t\t[1, 78, 112, 140, 141, 188, 222, 223],\n\t\t[244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256],\n\t\t[147, 216],\n\t\t[156, 235],\n\t\t[217, 218, 221, 224, 225],\n\t\t[71, 233],\n\t\t[76, 292],\n\t\t[92, 293],\n\t\t[187],\n\t\t[164, 165, 180, 186, 201, 219, 220, 260],\n\t\t[276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288]\n\t];\n\n\t// console.log(JSON.stringify(lines.map(({ vector, origin }) => ({\n\t// \tvector: vector.map(n => ear.general.cleanNumber(n, 12)),\n\t// \torigin: origin.map(n => ear.general.cleanNumber(n, 12)),\n\t// }))));\n\t// console.log(JSON.stringify(edges_line));\n\t// console.log(JSON.stringify(lines_edges));\n\n\tlines.forEach((_, i) => [0, 1, 2].forEach(d => {\n\t\texpect(lines[i].vector[d]).toBeCloseTo(expectedLines[i].vector[d]);\n\t\texpect(lines[i].origin[d]).toBeCloseTo(expectedLines[i].origin[d]);\n\t}));\n\n\texpect(lines_edges).toMatchObject(expectedLinesEdges);\n});\n\ntest(\"Mooser's train 3d edges_line\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { lines, edges_line } = ear.graph.getEdgesLine(graph);\n\n\texpect(lines.length).toBe(301);\n\texpect(edges_line).toMatchObject([\n\t\t19,19,19,76,59,13,91,19,19,13,42,19,19,36,115,99,19,19,19,163,144,13,174,19,19,13,131,19,19,36,198,184,210,19,19,13,237,247,19,19,19,13,256,264,19,19,19,13,273,19,19,279,24,36,22,22,22,75,58,15,90,22,22,15,41,22,22,37,114,98,22,22,22,162,143,15,173,22,22,15,130,22,22,37,197,183,209,22,22,15,236,246,22,22,22,15,255,263,22,22,22,15,272,22,22,279,23,37,274,274,151,151,151,151,16,16,16,16,16,16,16,16,16,16,16,16,16,152,6,153,7,279,279,279,279,279,279,281,280,152,281,153,280,279,279,279,279,6,281,7,280,265,265,19,22,265,265,186,187,299,300,297,298,289,290,277,278,238,239,267,267,258,258,270,271,261,262,191,192,175,176,216,216,19,22,19,22,198,197,47,198,46,197,219,220,66,198,65,197,248,248,19,22,214,224,215,225,248,248,216,216,219,220,19,198,22,197,216,216,154,155,221,222,214,215,176,175,250,250,241,241,253,254,244,245,187,186,155,154,230,230,204,204,234,235,207,208,176,175,149,150,199,199,199,199,199,199,196,198,196,197,194,195,200,201,184,183,115,114,99,98,36,24,37,23,19,22,19,22,19,22,19,22,184,47,183,46,115,47,114,46,99,47,98,46,24,36,47,23,37,46,184,183,115,114,99,98,24,23,164,164,132,132,77,77,40,40,19,184,22,183,19,115,22,114,19,99,22,98,19,24,22,23,164,164,132,132,77,77,40,40,121,122,101,102,62,63,26,27,168,168,125,125,85,85,31,31,157,157,138,138,70,70,50,50,171,172,128,129,88,89,38,39,160,161,141,142,73,74,55,56,135,136,92,93,67,68,2,3,108,109,109,108,53,54,54,53,185,185,116,116,100,100,25,25,185,185,185,185,116,116,116,116,100,100,100,100,25,25,25,25,182,184,182,183,113,115,113,114,97,99,97,98,8,24,8,23,20,21,180,181,111,112,95,96,4,5,188,189,117,118,103,104,28,29,151,0,151,0,279,279,279,279,265,265,274,274,279,279,274,274,0,274,0,274,279,279,238,239,191,192,290,289,298,297,186,187,227,228,9,10,11,12,267,267,258,258,266,266,257,257,268,186,269,187,259,187,260,186,248,248,0,0,216,216,193,0,193,0,216,216,19,22,211,211,198,197,0,211,0,211,216,219,216,220,216,216,177,178,176,175,222,221,154,155,145,146,9,10,11,12,250,250,241,241,249,249,240,240,251,175,252,176,242,176,243,175,9,10,11,12,230,230,204,204,229,229,203,203,231,155,232,154,205,154,206,155,196,196,198,197,199,199,199,199,199,199,200,201,194,195,179,0,179,0,110,0,110,0,94,0,94,0,14,1,0,14,1,0,164,164,132,132,77,77,40,40,19,22,19,22,19,22,19,22,184,183,115,114,99,98,36,24,37,23,164,165,164,166,132,133,132,134,77,78,77,79,40,48,40,49,147,148,80,81,82,83,17,18,121,122,101,102,62,63,26,27,105,106,119,120,43,44,60,61,9,10,9,10,9,10,9,10,11,12,11,12,11,12,11,12,168,168,125,125,85,85,31,31,157,157,138,138,70,70,50,50,167,167,124,124,84,84,30,30,156,156,137,137,69,69,45,45,169,121,170,122,126,101,127,102,86,62,87,63,32,26,33,27,158,122,159,121,139,102,140,101,71,63,72,62,51,27,52,26,182,182,113,113,97,97,8,8,184,183,115,114,99,98,24,23,185,185,116,116,100,100,25,25,185,185,185,185,116,116,116,116,100,100,100,100,25,25,25,25,20,21,188,189,117,118,103,104,28,29,180,181,111,112,95,96,4,5,19,22,34,35,57,36,37,36,37,233,202,199,20,21,36,37,19,22,19,22,36,37,66,65,66,65,190,123,107,64,185,116,100,25,20,21,0,0,0,0,14,14,36,37,196,20,21,14,14,0,0,0,0,36,37,182,113,97,8,20,21,293,292,225,226,212,226,224,213,278,293,292,277,288,275,296,294,296,295,288,276,284,282,283,285,278,278,287,274,287,277,277,286,274,286,281,281,280,280,291,291,291,293,293,286,286,292,292,287,287,212,218,223,213,217,223,225,215,223,224,214,223,223\n\t]);\n});\n\n// test(\"getCollinearOverlappingEdges\", () => {\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[-2, -2], [-1, -2],\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5]],\n// \t}).clusters_edges).toMatchObject([[1], [2], [0]]);\n\n// \t// diagonal\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3]],\n// \t}).clusters_edges).toMatchObject([[0], [1]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[0.99, 0.99], [2, 2],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3]],\n// \t}).clusters_edges).toMatchObject([[0, 1]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [100, 100],\n// \t\t\t[99, 99], [200, 200],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3]],\n// \t}).clusters_edges).toMatchObject([[0, 1]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[-100, -100], [-50, -50],\n// \t\t\t[-50.001, -50.001], [0, 0],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3]],\n// \t}).clusters_edges).toMatchObject([[0, 1]]);\n// \t// horizontal, does not pass through origin\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[100, 100], [200, 100],\n// \t\t\t[199, 100], [300, 100],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3]],\n// \t}).clusters_edges).toMatchObject([[0, 1]]);\n\n// });\n\n// test(\"getCollinearOverlappingEdges\", () => {\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[3, 3], [4, 4],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t}).clusters_edges).toMatchObject([[0], [1], [2], [3]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1.001, 1.001],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[2.99, 2.99], [4, 4],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t}).clusters_edges).toMatchObject([[0, 1], [2, 3]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1.001, 1.001],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[2.99, 2.99], [4, 4],\n// \t\t\t[-1, -1], [0.01, 0.01],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],\n// \t}).clusters_edges).toMatchObject([[4, 0, 1], [2, 3]]);\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[-1, -1], [10, 10],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t}).clusters_edges).toMatchObject([[3, 0, 1, 2]]);\n// });\n\n// test(\"getCollinearOverlappingEdges\", () => {\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[3, 3], [4, 4],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t})).toMatchObject({\n// \t\tedges_cluster: [0, 1, 2, 3]\n// \t});\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1.001, 1.001],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[2.99, 2.99], [4, 4],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t})).toMatchObject({\n// \t\tedges_cluster: [0, 0, 1, 1]\n// \t});\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1.001, 1.001],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[2.99, 2.99], [4, 4],\n// \t\t\t[-1, -1], [0.01, 0.01],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],\n// \t})).toMatchObject({\n// \t\tedges_cluster: [0, 0, 1, 1, 0]\n// \t});\n\n// \texpect(ear.graph.getCollinearOverlappingEdges({\n// \t\tvertices_coords: [\n// \t\t\t[0, 0], [1, 1],\n// \t\t\t[1, 1], [2, 2],\n// \t\t\t[2, 2], [3, 3],\n// \t\t\t[-1, -1], [10, 10],\n// \t\t],\n// \t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n// \t})).toMatchObject({\n// \t\tedges_cluster: [0, 0, 0, 0]\n// \t});\n// });\n"
  },
  {
    "path": "tests/graph.edges.overlap.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"doEdgesOverlap, X overlapping\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 2], [1, 3]],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(true);\n});\n\ntest(\"doEdgesOverlap, + overlapping\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0.5], [1, 0.5], [0.5, 0], [0.5, 1]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(true);\n});\n\ntest(\"doEdgesOverlap, + diagonal overlapping\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 1], [0.5, 0], [0.5, 1]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(true);\n});\n\ntest(\"doEdgesOverlap, square\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0]],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph, 0.1)).toBe(false);\n});\n\ntest(\"doEdgesOverlap, square with X\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0],\n\t\t\t[0, 4], [1, 4], [2, 4], [3, 4],\n\t\t],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph, 0.1)).toBe(false);\n});\n\ntest(\"doEdgesOverlap, square with X, overlapping\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0],\n\t\t\t[0, 2], [1, 3],\n\t\t],\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(true);\n});\n\ntest(\"doEdgesOverlap, random lines, un-planarized\", () => {\n\tconst graph = {\n\t\tvertices_coords: Array.from(Array(100)).map(() => [Math.random(), Math.random()]),\n\t\tedges_vertices: Array.from(Array(50)).map((_, i) => [i * 2, i * 2 + 1]),\n\t};\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(true);\n});\n\ntest(\"doEdgesOverlap, random lines, planarized\", () => {\n\tconst graph = ear.graph.planarize({\n\t\tvertices_coords: Array.from(Array(20)).map(() => [Math.random(), Math.random()]),\n\t\tedges_vertices: Array.from(Array(10)).map((_, i) => [i * 2, i * 2 + 1]),\n\t});\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(false);\n});\n\ntest(\"doEdgesOverlap, crease pattern\", () => {\n\tconst file = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(file);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(false);\n});\n\ntest(\"doEdgesOverlap, crease pattern\", () => {\n\tconst file = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(file);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\texpect(ear.graph.doEdgesOverlap(graph)).toBe(false);\n});\n"
  },
  {
    "path": "tests/graph.explode.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"explodeFaces and explodeEdges\", () => {\n\tconst bird = ear.graph.bird();\n\tconst explodeEdges = ear.graph.explodeEdges(bird);\n\tconst explodeFaces = ear.graph.explodeFaces(bird);\n\texpect(explodeEdges.edges_vertices.length).toBe(bird.edges_vertices.length);\n\texpect(explodeFaces.faces_vertices.length).toBe(bird.faces_vertices.length);\n\texplodeFaces.faces_vertices.flat().forEach((v, i) => expect(v).toBe(i));\n});\n\ntest(\"empty graphs\", () => {\n\texpect(ear.graph.explodeEdges({})).toMatchObject({});\n\texpect(ear.graph.explodeFaces({})).toMatchObject({});\n});\n\ntest(\"simple graphs\", () => {\n\tconst edgesOnly = ear.graph.explodeEdges({\n\t\tedges_vertices: [[0, 1], [1, 2]],\n\t});\n\tconst facesOnly = ear.graph.explodeFaces({\n\t\tfaces_vertices: [[0, 1, 2], [1, 3, 2]],\n\t});\n\texpect(JSON.stringify(edgesOnly.edges_vertices.flat()))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3]));\n\texpect(JSON.stringify(facesOnly.faces_vertices.flat()))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3, 4, 5]));\n});\n\ntest(\"explodeFaces 3D\", () => {\n\tconst graph = JSON.parse(fs.readFileSync(\"./tests/files/fold/bird-base-3d.fold\", \"utf-8\"));\n\tear.graph.explodeEdges(graph);\n\tear.graph.explodeFaces(graph);\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/graph.faces.facePoint.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"facesContainingPoint\", () => { });\n\ntest(\"faceContainingPoint\", () => { });\n"
  },
  {
    "path": "tests/graph.faces.matrix.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeFacesMatrix 3d sphere\", () => {\n\tconst sphere = fs.readFileSync(\"./tests/files/obj/sphere-with-holes.obj\", \"utf-8\");\n\tconst graph = ear.convert.objToFold(sphere);\n\tconst res1 = ear.graph.makeFacesMatrix(graph);\n\tres1[0].forEach((n, i) => expect(n).toBeCloseTo(ear.math.identity3x4[i]));\n\tres1[1].forEach((n, i) => expect(n).not.toBeCloseTo(ear.math.identity3x4[i]));\n\tconst res2 = ear.graph.makeFacesMatrix(graph, [1]);\n\tres2[0].forEach((n, i) => expect(n).not.toBeCloseTo(ear.math.identity3x4[i]));\n\tres2[1].forEach((n, i) => expect(n).toBeCloseTo(ear.math.identity3x4[i]));\n});\n\ntest(\"makeFacesMatrix2 and makeFacesMatrix similarity\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-step.fold\", \"utf-8\");\n\tconst cp = JSON.parse(foldfile);\n\tconst res1 = ear.graph.makeFacesMatrix(cp);\n\tconst res2 = ear.graph.makeFacesMatrix2(cp);\n\tres1.forEach((mat, f) => {\n\t\texpect(mat[0]).toBeCloseTo(res2[f][0]);\n\t\texpect(mat[1]).toBeCloseTo(res2[f][1]);\n\t\texpect(mat[3]).toBeCloseTo(res2[f][2]);\n\t\texpect(mat[4]).toBeCloseTo(res2[f][3]);\n\t\texpect(mat[9]).toBeCloseTo(res2[f][4]);\n\t\texpect(mat[10]).toBeCloseTo(res2[f][5]);\n\t});\n});\n\ntest(\"makeFacesMatrix no folded creases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/fan-flat-cp.fold\", \"utf-8\");\n\tconst cp = JSON.parse(foldfile);\n\tear.graph.makeFacesMatrix(cp)\n\t\t.forEach(mat => mat\n\t\t\t.forEach((n, i) => expect(n).toBeCloseTo(ear.math.identity3x4[i])));\n});\n"
  },
  {
    "path": "tests/graph.faces.planes.getFacesPlane.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// const createRandomTriangleGraph = (NUM_TRIS = 10) => ({\n// \tvertices_coords: Array.from(Array(3 * NUM_TRIS))\n// \t\t.map(() => [Math.random(), Math.random(), Math.random()]),\n// \tedges_vertices: Array.from(Array(NUM_TRIS))\n// \t\t.flatMap((_, i) => [\n// \t\t\t[i * 3 + 0, i * 3 + 1],\n// \t\t\t[i * 3 + 1, i * 3 + 2],\n// \t\t\t[i * 3 + 2, i * 3 + 0],\n// \t\t]),\n// \tedges_assignment: Array.from(Array(NUM_TRIS))\n// \t\t.flatMap(() => [\"B\", \"B\", \"B\"]),\n// \tfaces_vertices: Array.from(Array(NUM_TRIS))\n// \t\t.map((_, i) => [i * 3, i * 3 + 1, i * 3 + 2]),\n// });\n\ntest(\"getFacesPlane, random planes\", () => {\n\t// randomly place a bunch of triangles in a 3D -1...+1 bounding box\n\t// disjoint triangles in 3D\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/random-triangles-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldFile);\n\n\tconst {\n\t\t// planes,\n\t\t// planes_faces,\n\t\tplanes_transform,\n\t\tfaces_plane,\n\t\t// faces_winding,\n\t} = ear.graph.getFacesPlane(graph);\n\n\t// one point for every face which lies in the plane\n\tconst faces_center = ear.graph.makeFacesCenterQuick(graph);\n\tconst faces_polygon = ear.graph.makeFacesPolygonQuick(graph);\n\n\tconst faces_centerXY = faces_center\n\t\t.map((point, f) => ear.math.multiplyMatrix4Vector3(\n\t\t\tplanes_transform[faces_plane[f]],\n\t\t\tpoint,\n\t\t));\n\tconst faces_polygonXY = faces_polygon\n\t\t.map((points, f) => points\n\t\t\t.map(point => ear.math.multiplyMatrix4Vector3(\n\t\t\t\tplanes_transform[faces_plane[f]],\n\t\t\t\tpoint,\n\t\t\t)));\n\n\texpect(faces_plane).toHaveLength(10);\n\texpect(faces_center).toHaveLength(10);\n\texpect(faces_polygon).toHaveLength(10);\n\n\tfaces_centerXY\n\t\t.map(([,, z]) => z)\n\t\t.forEach(n => expect(n).toBeCloseTo(0.0));\n\tfaces_polygonXY\n\t\t.flatMap(points => points.map(([,, z]) => z))\n\t\t.forEach(n => expect(n).toBeCloseTo(0.0));\n});\n\ntest(\"getFacesPlane, upside-down planes\", () => {\n\t// disjoint triangles in 3D\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [\n\t\t\t// counter\n\t\t\t[-1, -1, 5],\n\t\t\t[1, 0, 5],\n\t\t\t[-1, 1, 5],\n\t\t\t// counter\n\t\t\t[-1, -1, -6],\n\t\t\t[1, 0, -6],\n\t\t\t[-1, 1, -6],\n\t\t\t// clockwise\n\t\t\t[-1, -1, 7],\n\t\t\t[-1, 1, 7],\n\t\t\t[1, 0, 7],\n\t\t\t// clockwise\n\t\t\t[-1, -1, -8],\n\t\t\t[-1, 1, -8],\n\t\t\t[1, 0, -8],\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[0, 1, 2],\n\t\t\t[3, 4, 5],\n\t\t\t[6, 7, 8],\n\t\t\t[9, 10, 11],\n\t\t],\n\t});\n\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t} = ear.graph.getFacesPlane(graph);\n\n\tconst faces_center = ear.graph.makeFacesCenterQuick(graph);\n\n\tconst faces_polygon = ear.graph.makeFacesPolygonQuick(graph);\n\n\tconst faces_centerXY = faces_center\n\t\t.map((point, f) => ear.math.multiplyMatrix4Vector3(\n\t\t\tplanes_transform[faces_plane[f]],\n\t\t\tpoint,\n\t\t));\n\n\tconst faces_polygonXY = faces_polygon\n\t\t.map((points, f) => points\n\t\t\t.map(point => ear.math.multiplyMatrix4Vector3(\n\t\t\t\tplanes_transform[faces_plane[f]],\n\t\t\t\tpoint,\n\t\t\t)));\n\n\texpect(planes).toMatchObject([\n\t\t{ normal: [0, 0, 1], origin: [-0, -0, -8] },\n\t\t{ normal: [0, 0, 1], origin: [-0, -0, -6] },\n\t\t{ normal: [0, 0, 1], origin: [0, 0, 5] },\n\t\t{ normal: [0, 0, 1], origin: [0, 0, 7] },\n\t]);\n\n\texpect(planes_faces).toMatchObject([[3], [1], [0], [2]]);\n\n\texpect(planes_transform).toMatchObject([\n\t\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 8, 1],\n\t\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 6, 1],\n\t\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -5, 1],\n\t\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -7, 1],\n\t]);\n\n\texpect(faces_plane).toMatchObject([2, 1, 3, 0]);\n\n\texpect(faces_winding).toMatchObject([true, true, false, false]);\n\n\tfaces_centerXY\n\t\t.map(([,, z]) => z)\n\t\t.forEach(n => expect(n).toBeCloseTo(0.0));\n\tfaces_polygonXY\n\t\t.flatMap(points => points.map(([,, z]) => z))\n\t\t.forEach(n => expect(n).toBeCloseTo(0.0));\n});\n\ntest(\"getFacesPlane, disjoint\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t} = ear.graph.getFacesPlane(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-planes-disjoint.json`, JSON.stringify({\n\t\tplanes,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t}, null, 2));\n\n\texpect(planes.length).toBe(3);\n\n\t// plane 0\n\texpect(JSON.stringify(planes_faces[0]))\n\t\t.toBe(JSON.stringify([0, 1, 4, 5, 8, 11]));\n\texpect(JSON.stringify(planes_faces[0].map(face => faces_winding[face])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, false]));\n\texpect(planes[0].origin[0]).toBeCloseTo(0);\n\texpect(planes[0].origin[1]).toBeCloseTo(0);\n\texpect(planes[0].origin[2]).toBeCloseTo(0);\n\texpect(planes[0].normal[0]).toBeCloseTo(0);\n\texpect(planes[0].normal[1]).toBeCloseTo(0);\n\texpect(planes[0].normal[2]).toBeCloseTo(1);\n\n\t// plane 1\n\texpect(JSON.stringify(planes_faces[1]))\n\t\t.toBe(JSON.stringify([13]));\n\texpect(JSON.stringify(planes_faces[1].map(face => faces_winding[face])))\n\t\t.toBe(JSON.stringify([true]));\n\texpect(planes[1].origin[0]).toBeCloseTo(0);\n\texpect(planes[1].origin[1]).toBeCloseTo(0);\n\texpect(planes[1].origin[2]).toBeCloseTo(0.5);\n\texpect(planes[1].normal[0]).toBeCloseTo(0);\n\texpect(planes[1].normal[1]).toBeCloseTo(0);\n\texpect(planes[1].normal[2]).toBeCloseTo(1);\n\n\t// plane 2\n\texpect(JSON.stringify(planes_faces[2]))\n\t\t.toBe(JSON.stringify([2, 3, 6, 7, 9, 10, 12]));\n\texpect(JSON.stringify(planes_faces[2].map(face => faces_winding[face])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, true, false]));\n\texpect(planes[2].origin[0]).toBeCloseTo(0);\n\texpect(planes[2].origin[1]).toBeCloseTo(0);\n\texpect(planes[2].origin[2]).toBeCloseTo(0);\n\texpect(planes[2].normal[0]).toBeCloseTo(0);\n\texpect(planes[2].normal[1]).toBeCloseTo(1);\n\texpect(planes[2].normal[2]).toBeCloseTo(0);\n});\n\ntest(\"getFacesPlane, disjoint and separated\", () => {\n\t// fold this from the crease pattern, a lot fewer faces are now overlapping\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst creasePattern = ear.graph.getFramesByClassName(foldObject, \"creasePattern\")[0];\n\tconst foldedForm = {\n\t\t...creasePattern,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(creasePattern),\n\t};\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t} = ear.graph.getFacesPlane(foldedForm);\n\tfs.writeFileSync(`./tests/tmp/coplanar-planes-disjoint-separated.json`, JSON.stringify({\n\t\tplanes,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t}, null, 2));\n\n\texpect(planes.length).toBe(2);\n\n\t// plane 0\n\texpect(JSON.stringify(planes_faces[0]))\n\t\t.toBe(JSON.stringify([0, 1, 4, 5, 8, 10, 12]));\n\texpect(JSON.stringify(planes_faces[0].map(face => faces_winding[face])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, true, true]));\n\texpect(planes[0].origin[0]).toBeCloseTo(0);\n\texpect(planes[0].origin[1]).toBeCloseTo(0);\n\texpect(planes[0].origin[2]).toBeCloseTo(0);\n\texpect(planes[0].normal[0]).toBeCloseTo(0);\n\texpect(planes[0].normal[1]).toBeCloseTo(0);\n\texpect(planes[0].normal[2]).toBeCloseTo(1);\n\n\t// plane 1\n\texpect(JSON.stringify(planes_faces[1]))\n\t\t.toBe(JSON.stringify([2, 3, 6, 7, 9, 11, 13]));\n\texpect(JSON.stringify(planes_faces[1].map(face => faces_winding[face])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, true, true]));\n\texpect(planes[1].origin[0]).toBeCloseTo(0);\n\texpect(planes[1].origin[1]).toBeCloseTo(0);\n\texpect(planes[1].origin[2]).toBeCloseTo(0);\n\texpect(planes[1].normal[0]).toBeCloseTo(0);\n\texpect(planes[1].normal[1]).toBeCloseTo(1);\n\texpect(planes[1].normal[2]).toBeCloseTo(0);\n});\n\ntest(\"getFacesPlane, maze\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getFacesPlane(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-planes-maze-u.json`, JSON.stringify(result, null, 2));\n\n\t// ensure all faces are accounted for.\n\tconst totalFaceCount = foldedFrame.faces_vertices.length;\n\tconst faceFound = [];\n\tresult.planes_faces.forEach(el => el.forEach(f => { faceFound[f] = true; }));\n\texpect(faceFound.filter(a => a !== undefined).length).toBe(totalFaceCount);\n\n\t// ensure face normals directions match.\n\tconst facesNormal = ear.graph.makeFacesNormal(foldedFrame);\n\tresult.faces_plane.forEach((plane, f) => {\n\t\tconst planeFacesDot = ear.math.dot(result.planes[plane].normal, facesNormal[f]);\n\t\t// if aligned is true, dot product should be 1. if false, should be -1.\n\t\texpect(planeFacesDot).toBeCloseTo(result.faces_winding[f] ? 1 : -1);\n\t});\n});\n\ntest(\"coplanar and overlapping faces, Mooser's train, carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t}\n\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\t// faces_plane,\n\t\t// faces_winding,\n\t} = ear.graph.getFacesPlane(folded);\n\n\tconst expectedPlanes = [\n\t\t{ normal: [0, 0, 1], origin: [0, 0, -4] },\n\t\t{ normal: [0, 0, 1], origin: [0, 0, -2] },\n\t\t{ normal: [0, 0, 1], origin: [0, 0, 0] },\n\t\t{ normal: [0, 0, 1], origin: [0, 0, 6] },\n\t\t{ normal: [-1, 0, 0], origin: [38, 0, 0] },\n\t\t{ normal: [-1, 0, 0], origin: [35, 0, 0] },\n\t\t{ normal: [-1, 0, 0], origin: [33, 0, 0] },\n\t\t{ normal: [-1, 0, 0], origin: [27, 0, 0] },\n\t\t{ normal: [-1, 0, 0], origin: [25, 0, 0] },\n\t\t{ normal: [-1, 0, 0], origin: [22, 0, 0] },\n\t\t{ normal: [0, -1, 0], origin: [0, 8, 0] },\n\t\t{ normal: [0, -1, 0], origin: [0, 7, 0] },\n\t\t{ normal: [0, -1, 0], origin: [0, 1, 0] },\n\t\t{ normal: [0, -1, 0], origin: [0, 0, 0] },\n\t];\n\n\tconst expectedPlanesFaces = [\n\t\t// the bottom of all four wheels\n\t\t[5, 11, 120, 127],\n\t\t// the underside of the carriage\n\t\t[13, 9, 14, 68, 69, 70, 71, 79, 80, 81, 87, 2, 3, 7, 72, 73, 78, 88, 89, 8, 64, 65,\n\t\t\t66, 67, 92, 109, 110, 111, 116, 117, 138, 126, 58, 59, 60, 61, 83, 108, 133, 82],\n\t\t// the hitch join plane\n\t\t[26, 19, 17, 16, 18, 144, 22, 0, 23, 91, 99, 101, 139, 24,\n\t\t\t25, 141, 102, 123, 128, 136, 20, 27, 21, 112, 114, 28],\n\t\t// the top of the carriage\n\t\t[55],\n\t\t// the front of the carriage\n\t\t[1, 75, 90, 95, 96, 97, 98, 100, 103, 104, 113, 115, 124, 129, 130, 137, 142, 143],\n\t\t// side A of wheel set 1\n\t\t[4, 29, 93, 140],\n\t\t// side B of wheel set 1\n\t\t[6, 31, 118, 119],\n\t\t// side A of wheel set 2\n\t\t[10, 32, 134, 135],\n\t\t// side B of wheel set 2\n\t\t[12, 34, 121, 122],\n\t\t// the backside of the carriage\n\t\t[50, 15, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 56],\n\t\t// one side of the carriage\n\t\t[74, 62, 63, 76, 77, 85, 107, 86, 84],\n\t\t// back of one set of wheels\n\t\t[94, 125],\n\t\t// back of one set of wheels\n\t\t[30, 33],\n\t\t// one side of the carriage\n\t\t[105, 131, 132, 106, 51, 52, 53, 54, 57],\n\t];\n\n\texpect(planes).toHaveLength(14);\n\n\t[4, 40, 26, 1, 18, 4, 4, 4, 4, 18, 9, 2, 2, 9]\n\t\t.forEach((len, i) => expect(planes_faces[i]).toHaveLength(len));\n\n\tplanes.forEach((_, i) => [0, 1, 2].forEach(d => {\n\t\texpect(planes[i].normal[d]).toBeCloseTo(expectedPlanes[i].normal[d]);\n\t\texpect(planes[i].origin[d]).toBeCloseTo(expectedPlanes[i].origin[d]);\n\t}));\n\n\texpect(planes_faces).toMatchObject(expectedPlanesFaces);\n});\n\ntest(\"coplanar and overlapping faces, Mooser's train, engine only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-engine.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t}\n\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\t// faces_plane,\n\t\t// faces_winding,\n\t} = ear.graph.getFacesPlane(folded);\n\n\tconst expectedPlanes = [\n\t\t{ normal: [0, 0, 1], origin: [0, 0, 0] },\n\t\t{ normal: [0, -0.5, -0.8660254037844389], origin: [0, 1, 1.7320508075688767] },\n\t\t{ normal: [0, -0.5, -0.8660254037844389], origin: [0, 0, 0] },\n\t\t{ normal: [0, -0.5, -0.8660254037844389], origin: [0, -1, -1.7320508075688694] },\n\t\t{ normal: [0, -0.5, -0.8660254037844389], origin: [0, -4, -6.928203230275504] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, 0, 0] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -2.598076211353321, 1.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -4.330127018922196, 2.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -6.062177826491074, 3.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -7.79422863405995, 4.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -9.526279441628816, 5.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -10.392304845413257, 6] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -11.25833024919769, 6.5] },\n\t\t{ normal: [0, -0.8660254037844387, 0.5], origin: [0, -13.85640646055101, 8] },\n\t\t{ normal: [1, 0, 0], origin: [44, 0, 0] },\n\t\t{ normal: [1, 0, 0], origin: [45, 0, 0] },\n\t\t{ normal: [1, 0, 0], origin: [51, 0, 0] },\n\t\t{ normal: [1, 0, 0], origin: [52, 0, 0] },\n\t\t{\n\t\t\tnormal: [0.7071067811865611, 0.3535533905932669, 0.6123724356957827],\n\t\t\torigin: [21.171572875254608, 10.585786437626895, 18.335119948],\n\t\t},\n\t\t{\n\t\t\tnormal: [0.7071067811865611, 0.3535533905932669, 0.6123724356957827],\n\t\t\torigin: [24, 12, 20.784609690826453],\n\t\t},\n\t\t{\n\t\t\tnormal: [0.7071067811865346, -0.3535533905932799, -0.6123724356958058],\n\t\t\torigin: [24, -12, -20.784609690826525],\n\t\t},\n\t\t{\n\t\t\tnormal: [0.7071067811865346, -0.3535533905932799, -0.6123724356958058],\n\t\t\torigin: [26.82842712474543, -13.414213562373194, -23.234099433609902],\n\t\t},\n\t];\n\n\tconst expectedPlanesFaces = [\n\t\t// cowcatcher\n\t\t[153, 154, 155, 156, 133, 0, 1, 118, 132, 117, 157, 158],\n\t\t// bottom of wheels, 6 wheels\n\t\t[5, 89, 11, 180, 17, 181],\n\t\t// below boiler base plane and undercarriage\n\t\t[83, 84, 85, 86, 120, 146, 147, 148, 106, 119, 145, 196, 202, 20, 87, 88,\n\t\t\t110, 179, 39, 40, 92, 126, 217, 218, 124, 125, 185, 186, 187, 13, 15, 152,\n\t\t\t9, 111, 112, 109, 113, 14, 107, 108, 216, 105, 114, 104, 184, 3, 8, 159,\n\t\t\t160, 93, 94, 115, 116, 2, 7, 130, 131, 19, 151, 140, 37, 38, 215, 95],\n\t\t// hitch join plane\n\t\t[32, 30, 34, 22, 33, 26, 31, 27, 28, 29, 24, 23, 25],\n\t\t// top of carriage\n\t\t[99],\n\t\t// smoke stack\n\t\t[161, 162, 169, 170, 171, 135, 213, 134, 199, 139, 137, 138, 209,\n\t\t\t205, 206, 193, 194, 195, 208, 212, 201, 203, 204, 163, 164, 165,\n\t\t\t166, 136, 188, 189, 190, 200, 210, 174, 175, 214, 177, 176, 167, 168],\n\t\t// front wheel row, front side plane\n\t\t[219, 4, 48, 35],\n\t\t// front wheel row, back side plane\n\t\t[6, 50, 90, 91],\n\t\t// middle wheel row, front side plane\n\t\t[211, 10, 51, 149],\n\t\t// middle wheel row, back side plane\n\t\t[12, 53, 182, 183],\n\t\t// back wheel row, front side plane\n\t\t[54, 16, 198, 197],\n\t\t// front of carriage, joint plane with back of boiler\n\t\t[141, 142, 143, 192, 79, 80, 42, 43, 44, 46, 47, 73, 74, 75, 96, 97, 98, 207, 77, 78, 127, 128],\n\t\t// back wheel row, back side plane\n\t\t[172, 173, 18, 56],\n\t\t// back of carriage\n\t\t[64, 68, 61, 62, 63, 65, 66, 67, 71, 72, 21, 69, 70, 100, 58, 59, 60, 57],\n\t\t// right side of carriage\n\t\t[101, 81, 82, 102, 103],\n\t\t// right three wheels, back faces\n\t\t[55, 52, 49],\n\t\t// left three wheels, back faces\n\t\t[178, 150, 36],\n\t\t// left side of carriage\n\t\t[122, 129, 121, 123, 144],\n\t\t// boiler, top right\n\t\t[76],\n\t\t// boiler, bottom left\n\t\t[41],\n\t\t// boiler, bottom right\n\t\t[191],\n\t\t// boiler, top left\n\t\t[45],\n\t];\n\n\t// I counted 22 planes\n\texpect(planes).toHaveLength(22);\n\n\t[12, 6, 64, 13, 1, 40, 4, 4, 4, 4, 4, 22, 4, 18, 5, 3, 3, 5, 1, 1, 1, 1]\n\t\t.forEach((len, i) => expect(planes_faces[i]).toHaveLength(len));\n\n\tplanes.forEach((_, i) => [0, 1, 2].forEach(d => {\n\t\texpect(planes[i].normal[d]).toBeCloseTo(expectedPlanes[i].normal[d]);\n\t\texpect(planes[i].origin[d]).toBeCloseTo(expectedPlanes[i].origin[d]);\n\t}));\n\n\texpect(planes_faces).toMatchObject(expectedPlanesFaces);\n});\n"
  },
  {
    "path": "tests/graph.faces.planes.overlapping.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"getCoplanarAdjacentOverlappingFaces\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(\n\t\t`./tests/tmp/coplanar-overlapping-faces-disjoint-triangles.json`,\n\t\tJSON.stringify(result, null, 2),\n\t);\n\n\t// ensure all faces are accounted for.\n\tconst totalFaceCount = foldedFrame.faces_vertices.length;\n\tconst faceFound = [];\n\tresult.clusters_faces.forEach(set => set.forEach(f => { faceFound[f] = true; }));\n\texpect(faceFound.filter(a => a !== undefined).length).toBe(totalFaceCount);\n});\n\ntest(\"getCoplanarAdjacentOverlappingFaces\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst {\n\t\tclusters_faces,\n\t\tclusters_plane,\n\t\tplanes_transform,\n\t\tfaces_cluster,\n\t} = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\n\texpect(faces_cluster).toMatchObject([\n\t\t0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 3, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 3, 0, 3,\n\t\t0, 0, 0, 0, 3, 3, 0, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,\n\t\t0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 3, 3, 0, 3, 3, 3, 3,\n\t\t0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 0, 2, 3, 3, 0, 0, 0, 0, 0, 2,\n\t\t2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0,\n\t]);\n\texpect(clusters_plane).toMatchObject([0, 1, 2, 3]);\n\texpect(planes_transform).toHaveLength(4);\n\n\t// ensure all faces are accounted for.\n\tconst totalFaceCount = foldedFrame.faces_vertices.length;\n\tconst faceFound = [];\n\tclusters_faces.forEach(set => set.forEach(f => { faceFound[f] = true; }));\n\texpect(faceFound.filter(a => a !== undefined).length).toBe(totalFaceCount);\n});\n\ntest(\"coplanar and overlapping disjoint faces\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-planes-disjoint.json`, JSON.stringify(result, null, 2));\n\n\tconst {\n\t\tplanes,\n\t\t// planes_transform,\n\t\tfaces_winding,\n\t\tplanes_faces,\n\t\t// faces_plane,\n\t\t// planes_clusters,\n\t\t// clusters_plane,\n\t\t// clusters_faces,\n\t\t// faces_cluster,\n\t} = result;\n\n\texpect(planes.length).toBe(3);\n\n\t// plane 0\n\texpect(JSON.stringify(planes_faces[0]))\n\t\t.toBe(JSON.stringify([0, 1, 4, 5, 8, 11]));\n\texpect(JSON.stringify(planes_faces[0].map(f => faces_winding[f])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, false]));\n\texpect(planes[0].origin[0]).toBeCloseTo(0);\n\texpect(planes[0].origin[1]).toBeCloseTo(0);\n\texpect(planes[0].origin[2]).toBeCloseTo(0);\n\texpect(planes[0].normal[0]).toBeCloseTo(0);\n\texpect(planes[0].normal[1]).toBeCloseTo(0);\n\texpect(planes[0].normal[2]).toBeCloseTo(1);\n\n\t// plane 1\n\texpect(JSON.stringify(planes_faces[1]))\n\t\t.toBe(JSON.stringify([13]));\n\texpect(JSON.stringify(planes_faces[1].map(f => faces_winding[f])))\n\t\t.toBe(JSON.stringify([true]));\n\texpect(planes[1].origin[0]).toBeCloseTo(0);\n\texpect(planes[1].origin[1]).toBeCloseTo(0);\n\texpect(planes[1].origin[2]).toBeCloseTo(0.5);\n\texpect(planes[1].normal[0]).toBeCloseTo(0);\n\texpect(planes[1].normal[1]).toBeCloseTo(0);\n\texpect(planes[1].normal[2]).toBeCloseTo(1);\n\n\t// plane 2\n\texpect(JSON.stringify(planes_faces[2]))\n\t\t.toBe(JSON.stringify([2, 3, 6, 7, 9, 10, 12]));\n\texpect(JSON.stringify(planes_faces[2].map(f => faces_winding[f])))\n\t\t.toBe(JSON.stringify([true, true, true, true, true, true, false]));\n\texpect(planes[2].origin[0]).toBeCloseTo(0);\n\texpect(planes[2].origin[1]).toBeCloseTo(0);\n\texpect(planes[2].origin[2]).toBeCloseTo(0);\n\texpect(planes[2].normal[0]).toBeCloseTo(0);\n\texpect(planes[2].normal[1]).toBeCloseTo(1);\n\texpect(planes[2].normal[2]).toBeCloseTo(0);\n});\n\ntest(\"coplanar and overlapping faces, command strip\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/command-strip.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-command-strip.json`, JSON.stringify(result, null, 2));\n\n\tconst {\n\t\tplanes,\n\t\tplanes_transform,\n\t\tfaces_winding,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tplanes_clusters,\n\t\tclusters_plane,\n\t\tclusters_faces,\n\t\tfaces_cluster,\n\t} = result;\n\n\texpect(planes.length).toBe(8);\n\texpect(planes_transform.length).toBe(8);\n\texpect(faces_winding.length).toBe(foldedFrame.faces_vertices.length);\n\n\t// 4 planes contain 2 faces, 4 planes contain 3 faces\n\texpect(planes_faces.filter(arr => arr.length === 2).length).toBe(4);\n\texpect(planes_faces.filter(arr => arr.length === 3).length).toBe(4);\n\n\texpect(JSON.stringify(faces_plane))\n\t\t.toBe(JSON.stringify([0, 6, 1, 5, 0, 4, 1, 5, 2, 4, 3, 5, 2, 6, 3, 7, 2, 6, 1, 7]));\n\n\t// all clusters have one face only, so here, clusters should match faces\n\t// 4 planes contain 2 clusters, 4 planes contain 3 clusters\n\texpect(planes_clusters.filter(arr => arr.length === 2).length).toBe(4);\n\texpect(planes_clusters.filter(arr => arr.length === 3).length).toBe(4);\n\n\texpect(JSON.stringify(clusters_plane))\n\t\t.toBe(JSON.stringify([0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7]));\n\n\t// every cluster should only contain one face\n\tclusters_faces.forEach(faces => expect(faces.length).toBe(1));\n\n\t// faces and clusters should be 1:1. 20 unique clusters\n\texpect(Array.from(new Set(faces_cluster)).length).toBe(20);\n});\n\ntest(\"coplanar and overlapping faces, command strip with back\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/command-strip-with-back.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-command-strip-with-back.json`, JSON.stringify(result, null, 2));\n\n\tconst {\n\t\tplanes,\n\t\tplanes_transform,\n\t\tfaces_winding,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tplanes_clusters,\n\t\tclusters_plane,\n\t\t// clusters_faces,\n\t\t// faces_cluster,\n\t} = result;\n\n\texpect(planes.length).toBe(9);\n\texpect(planes_transform.length).toBe(9);\n\texpect(faces_winding.length).toBe(54);\n\n\t// 4 planes contain 2 faces, 4 planes contain 3 faces\n\t// 54 (total) - (2*4) - (3*4)\n\texpect(planes_faces.filter(arr => arr.length === 2).length).toBe(4);\n\texpect(planes_faces.filter(arr => arr.length === 3).length).toBe(4);\n\texpect(planes_faces.filter(arr => arr.length === 34).length).toBe(1);\n\n\texpect(JSON.stringify(faces_plane))\n\t\t.toBe(JSON.stringify([0, 6, 1, 5, 0, 4, 1, 5, 2, 4, 3, 5, 2, 6, 3, 7, 2, 6, 1, 7,\n\t\t\t8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,\n\t\t\t8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]));\n\t// all clusters have one face only, so here, clusters should match faces\n\t// 4 planes contain 2 clusters, 4 planes contain 3 clusters\n\texpect(planes_clusters.filter(arr => arr.length === 2).length).toBe(4);\n\texpect(planes_clusters.filter(arr => arr.length === 3).length).toBe(4);\n\texpect(planes_clusters.filter(arr => arr.length === 1).length).toBe(1);\n\n\texpect(JSON.stringify(clusters_plane))\n\t\t.toBe(JSON.stringify([0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8]));\n\n\t// // every cluster should only contain one face\n\t// clusters_faces.forEach(faces => expect(faces.length).toBe(1));\n\n\t// // faces and clusters should be 1:1. 20 unique clusters\n\t// expect(Array.from(new Set(faces_cluster)).length).toBe(20);\n});\n\ntest(\"coplanar and overlapping faces, square tube with overlap\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/square-tube-with-overlap.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-square-tube-with-overlap.json`, JSON.stringify(result, null, 2));\n\n\tconst {\n\t\tplanes,\n\t\tplanes_transform,\n\t\tfaces_winding,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tplanes_clusters,\n\t\tclusters_plane,\n\t\tclusters_faces,\n\t\tfaces_cluster,\n\t} = result;\n\n\texpect(planes.length).toBe(4);\n\texpect(planes_transform.length).toBe(4);\n\texpect(faces_winding.length).toBe(foldedFrame.faces_vertices.length);\n\n\texpect(JSON.stringify(planes_faces))\n\t\t.toBe(JSON.stringify([[0, 4, 5, 6], [2, 8], [1, 9], [3, 7]]));\n\texpect(JSON.stringify(faces_plane))\n\t\t.toBe(JSON.stringify([0, 2, 1, 3, 0, 0, 0, 3, 1, 2]));\n\texpect(JSON.stringify(planes_clusters))\n\t\t.toBe(JSON.stringify([[0], [1], [2], [3]]));\n\texpect(JSON.stringify(clusters_plane))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3]));\n\texpect(JSON.stringify(clusters_faces))\n\t\t.toBe(JSON.stringify([[0, 5, 4, 6], [2, 8], [1, 9], [3, 7]]));\n\texpect(JSON.stringify(faces_cluster))\n\t\t.toBe(JSON.stringify([0, 2, 1, 3, 0, 0, 0, 3, 1, 2]));\n});\n\ntest(\"coplanar and overlapping faces, strip with angle\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/strip-with-angle.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(\n\t\t`./tests/tmp/coplanar-overlapping-strip-with-angle.json`,\n\t\tJSON.stringify(result, null, 2),\n\t);\n\n\tconst {\n\t\tplanes,\n\t\tplanes_transform,\n\t\tfaces_winding,\n\t\tplanes_faces,\n\t\tfaces_plane,\n\t\tplanes_clusters,\n\t\tclusters_plane,\n\t\tclusters_faces,\n\t\tfaces_cluster,\n\t} = result;\n\n\texpect(planes.length).toBe(4);\n\texpect(planes_transform.length).toBe(4);\n\texpect(faces_winding.length).toBe(foldedFrame.faces_vertices.length);\n\n\texpect(JSON.stringify(planes_faces))\n\t\t.toBe(JSON.stringify([[0, 5], [2, 3], [1], [4]]));\n\texpect(JSON.stringify(faces_plane))\n\t\t.toBe(JSON.stringify([0, 2, 1, 1, 3, 0]));\n\texpect(JSON.stringify(planes_clusters))\n\t\t.toBe(JSON.stringify([[0], [1], [2], [3]]));\n\texpect(JSON.stringify(clusters_plane))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3]));\n\texpect(JSON.stringify(clusters_faces))\n\t\t.toBe(JSON.stringify([[0, 5], [2, 3], [1], [4]]));\n\texpect(JSON.stringify(faces_cluster))\n\t\t.toBe(JSON.stringify([0, 2, 1, 1, 3, 0]));\n});\n\ntest(\"coplanar and overlapping faces, Mooser's train, carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t}\n\n\t// planes data is tested in another file testing getFacesPlane()\n\tconst {\n\t\t// planes,\n\t\t// planes_transform,\n\t\t// faces_winding,\n\t\t// planes_faces,\n\t\t// faces_plane,\n\t\tplanes_clusters,\n\t\t// clusters_plane,\n\t\tclusters_faces,\n\t\t// faces_cluster,\n\t} = ear.graph.getCoplanarAdjacentOverlappingFaces(folded);\n\n\tconst expectedPlanesClusters = [\n\t\t// the bottom of all four wheels\n\t\t[0, 1, 2, 3],\n\t\t// the underside of the carriage\n\t\t[4, 5],\n\t\t// the hitch join plane\n\t\t[6, 7],\n\t\t// the top of the carriage\n\t\t[8],\n\t\t// the front of the carriage\n\t\t[9],\n\t\t// side A of wheel set 1\n\t\t[10, 11],\n\t\t// side B of wheel set 1\n\t\t[12, 13],\n\t\t// side A of wheel set 2\n\t\t[14, 15],\n\t\t// side B of wheel set 2\n\t\t[16, 17],\n\t\t// the backside of the carriage\n\t\t[18],\n\t\t// one side of the carriage\n\t\t[19],\n\t\t// back of one set of wheels\n\t\t[20, 21],\n\t\t// back of one set of wheels\n\t\t[22, 23],\n\t\t// one side of the carriage\n\t\t[24],\n\t];\n\n\texpect(planes_clusters).toMatchObject(expectedPlanesClusters);\n\n\texpect(clusters_faces).toMatchObject([\n\t\t// the bottom of all four wheels\n\t\t[5], [11], [120], [127],\n\t\t// the underside of the carriage\n\t\t[2, 3, 7, 8, 9, 13, 14, 68, 69, 70, 71, 72, 73, 78, 79, 80, 81, 87, 88, 89],\n\t\t[58, 59, 60, 61, 64, 65, 66, 67, 82, 83, 92, 108, 109, 110, 111, 116, 117, 126, 133, 138],\n\t\t// the hitch join plane\n\t\t[0, 91, 99, 101, 102, 112, 114, 123, 128, 136, 139, 141, 144],\n\t\t[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28],\n\t\t// the top of the carriage\n\t\t[55],\n\t\t// the front of the carriage\n\t\t[1, 75, 90, 95, 96, 97, 98, 100, 103, 104, 113, 115, 124, 129, 130, 137, 142, 143],\n\t\t// side A of wheel set 1\n\t\t[4, 29], [93, 140],\n\t\t// side B of wheel set 1\n\t\t[6, 31], [118, 119],\n\t\t// side A of wheel set 2\n\t\t[10, 32], [134, 135],\n\t\t// side B of wheel set 2\n\t\t[12, 34], [121, 122],\n\t\t// the backside of the carriage\n\t\t[15, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 56],\n\t\t// one side of the carriage\n\t\t[62, 63, 74, 76, 77, 84, 85, 86, 107],\n\t\t// back of one set of wheels\n\t\t[94], [125],\n\t\t// back of one set of wheels\n\t\t[30], [33],\n\t\t// one side of the carriage\n\t\t[51, 52, 53, 54, 57, 105, 106, 131, 132]]);\n});\n\ntest(\"coplanar and overlapping faces, Mooser's train\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst foldedFrame = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tconst result = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedFrame);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-faces-mooser.json`, JSON.stringify(result, null, 2));\n\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\tplanes_transform,\n\t\tplanes_clusters,\n\t\t// faces_winding,\n\t\tfaces_plane,\n\t\tfaces_cluster,\n\t\tclusters_plane,\n\t\t// clusters_faces,\n\t} = result;\n\n\texpect(planes.length).toBe(34);\n\texpect(planes_faces.length).toBe(34);\n\texpect(planes_transform.length).toBe(34);\n\texpect(planes_clusters.length).toBe(34);\n\n\texpect(planes_faces).toMatchObject([\n\t\t[251,395,406],[16,32,54,70,86,109,117,118,119,120,121,122,123,124,125,126,127,229,230,231,232,238,239,245,246,288,289,301,302,308,309,361,362,368,369,476,477,478,479],[0,1,2,8,9,12,13,17,18,19,20,26,27,30,33,35,38,40,42,44,46,48,50,51,55,56,57,63,64,67,68,72,73,74,75,81,82,85,88,89,93,94,97,99,100,103,105,106,129,130,132,133,134,135,138,139,147,149,150,151,152,154,155,156,168,169,170,171,197,198,200,201,202,205,208,210,211,212,213,215,216,217,220,224,259,260,261,262,272,273,274,275,294,297,306,311,318,321,328,331,335,336,339,340,343,344,347,348,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451],[5,6,23,24,36,41,47,60,61,78,79,91,96,102],[14,69,324,325,327,329,330,332,384,385,387,389,390,391,412,413,414,415],[11,66,464,466],[4,59,474,475],[3,58,472,473],[7,62,460,462],[10,65,314,315,316,319,320,323,374,375,376,379,380,382,405,408,409,411],[15,71,300,303,305,307,310,312,360,363,365,367,370,371,401,402,403,404],[29,84,456,458],[22,77,470,471],[21,76,468,469],[25,80,452,454],[28,83,290,291,292,295,296,299,350,351,352,355,356,358,394,397,398,400],[31,87,221,222,223,226,227,228,234,235,236,240,241,242,255,256,257,258],[34,90,286,287],[195,196,199,203,204,206,247,248,249,250,252,253,254,263,264,265,266,267,268,269,270,271],[37,92,282,284],[39,95,280,281],[43,98,276,278],[45,101,193,194],[49,104,189,191],[110,111,112,140,141,142,143,144,145,146,148,153,157,158,159,160,161,162,163,164,165,166,167,172,173,176,177,178,179,180,182,183,184,185,187,188,480,481,482,483],[52,53,107,108,113,114,115,128,136,131,137,116],[186],[174],[181],[175],[214,218,225,243,244,298,313,322,333,337,341,345,349,357,359,372,373,381,383,392,393,399,410],[192,279,285,455,459,463,467],[190,277,283,453,457,461,465],[207,209,219,233,237,293,304,317,326,334,338,342,346,353,354,364,366,377,378,386,388,396,407],\n\t]);\n\n\texpect(planes_clusters).toMatchObject([\n\t\t[0,1,2],[3,4,5],[6,7,8,9,10,11],[12,13,14,15,16,17,18,19,20,21,22,23,24,25],[26],[27,28],[29,30],[31,32],[33,34],[35],[36],[37,38],[39,40],[41,42],[43,44],[45],[46],[47,48],[49],[50,51],[52,53],[54,55],[56,57],[58,59],[60],[61,62],[63],[64],[65],[66],[67,68,69],[70,71,72,73,74,75,76],[77,78,79,80,81,82,83],[84,85,86]\n\t]);\n\n\texpect(faces_plane).toMatchObject([2,2,2,7,6,3,3,8,2,2,9,5,2,2,4,10,1,2,2,2,2,13,12,3,3,14,2,2,15,11,2,16,1,2,17,2,3,19,2,20,2,3,2,21,2,22,2,3,2,23,2,2,25,25,1,2,2,2,7,6,3,3,8,2,2,9,5,2,2,4,1,10,2,2,2,2,13,12,3,3,14,2,2,15,11,2,1,16,2,2,17,3,19,2,2,20,3,2,21,2,2,22,3,2,23,2,2,25,25,1,24,24,24,25,25,25,25,1,1,1,1,1,1,1,1,1,1,1,25,2,2,25,2,2,2,2,25,25,2,2,24,24,24,24,24,24,24,2,24,2,2,2,2,24,2,2,2,24,24,24,24,24,24,24,24,24,24,24,2,2,2,2,24,24,27,29,24,24,24,24,24,28,24,24,24,24,26,24,24,23,32,23,31,22,22,18,18,2,2,18,2,2,2,18,18,2,18,33,2,33,2,2,2,2,30,2,2,2,30,33,2,16,16,16,2,30,16,16,16,1,1,1,1,33,16,16,16,33,1,1,16,16,16,30,30,1,1,18,18,18,18,0,18,18,18,16,16,16,16,2,2,2,2,18,18,18,18,18,18,18,18,18,2,2,2,2,21,32,21,31,20,20,19,32,19,31,17,17,1,1,15,15,15,33,2,15,15,2,30,15,10,1,1,10,33,10,2,10,1,1,10,2,10,30,9,9,9,33,2,9,9,2,30,9,4,4,33,4,2,4,4,2,4,30,33,2,2,30,33,2,2,30,33,2,2,30,33,2,2,30,15,15,15,33,33,15,15,30,15,30,10,1,1,10,33,10,33,10,1,1,10,10,30,30,9,9,9,33,33,9,9,30,9,30,4,4,33,4,33,4,4,4,30,30,15,0,33,15,15,30,15,10,10,10,10,9,0,33,9,9,30,9,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,14,32,14,31,11,32,11,31,8,32,8,31,5,32,5,31,13,13,12,12,7,7,6,6,1,1,1,1,24,24,24,24]);\n\n\texpect(faces_cluster).toMatchObject([6,6,6,31,29,12,13,33,6,6,35,27,6,6,26,36,3,7,7,7,7,41,39,14,15,43,7,7,45,37,7,46,4,8,47,8,16,50,8,52,8,17,8,54,8,56,8,18,8,58,8,8,61,61,5,9,9,9,32,30,19,20,34,9,9,35,28,9,9,26,3,36,10,10,10,10,42,40,21,22,44,10,10,45,38,10,4,46,11,11,48,23,51,11,11,53,24,11,55,11,11,57,25,11,59,11,11,62,62,5,60,60,60,61,62,61,62,5,5,5,5,5,5,5,5,5,5,5,61,8,8,61,8,8,11,11,62,62,11,11,60,60,60,60,60,60,60,8,60,8,8,8,11,60,11,11,11,60,60,60,60,60,60,60,60,60,60,60,8,11,8,11,60,60,64,66,60,60,60,60,60,65,60,60,60,60,63,60,60,58,77,59,70,56,57,49,49,8,8,49,8,11,11,49,49,11,49,84,8,84,8,8,8,11,67,11,11,11,67,84,8,46,46,46,11,67,46,46,46,4,4,4,4,84,46,46,46,84,4,4,46,46,46,67,67,4,4,49,49,49,49,0,49,49,49,46,46,46,46,8,8,11,11,49,49,49,49,49,49,49,49,49,8,8,11,11,54,78,55,71,52,53,50,79,51,72,47,48,4,4,45,45,45,85,7,45,45,10,68,45,36,3,3,36,85,36,7,36,3,3,36,10,36,68,35,35,35,86,6,35,35,9,69,35,26,26,86,26,6,26,26,9,26,69,85,7,10,68,85,7,10,68,86,6,9,69,86,6,9,69,45,45,45,85,85,45,45,68,45,68,36,3,3,36,85,36,85,36,3,3,36,36,68,68,35,35,35,86,86,35,35,69,35,69,26,26,86,26,86,26,26,26,69,69,45,1,85,45,45,68,45,36,36,36,36,35,2,86,35,35,69,35,26,26,26,26,7,7,10,10,7,7,10,10,6,6,9,9,6,6,9,9,7,7,7,10,10,10,7,7,10,10,6,6,6,9,9,9,6,6,9,9,43,80,44,73,37,81,38,74,33,82,34,75,27,83,28,76,41,42,39,40,31,32,29,30,3,3,4,3,60,60,60,60]);\n\n\texpect(clusters_plane).toMatchObject([0,0,0,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,5,5,6,6,7,7,8,8,9,10,11,11,12,12,13,13,14,14,15,16,17,17,18,19,19,20,20,21,21,22,22,23,23,24,25,25,26,27,28,29,30,30,30,31,31,31,31,31,31,31,32,32,32,32,32,32,32,33,33,33]);\n});\n\ntest(\"coplanar and overlapping disjoint faces and separated\", () => {\n\t// fold this from the crease pattern, a lot fewer faces are now overlapping\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/disjoint-triangles-3d.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst creasePattern = ear.graph.getFramesByClassName(foldObject, \"creasePattern\")[0];\n\tconst foldedForm = {\n\t\t...creasePattern,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(creasePattern),\n\t};\n\tconst planes = ear.graph.getCoplanarAdjacentOverlappingFaces(foldedForm);\n\tfs.writeFileSync(`./tests/tmp/coplanar-overlapping-planes-disjoint-separated.json`, JSON.stringify(planes, null, 2));\n});\n"
  },
  {
    "path": "tests/graph.faces.winding.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeFacesWinding\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords,\n\t};\n\tear.graph.makeFacesWinding(graph)\n\t\t.forEach(side => expect(side).toBe(true));\n\tconst foldedWinding = ear.graph.makeFacesWinding(folded);\n\t// difference between up and flipped should be 1. (at least less than 5)\n\tconst up = foldedWinding.filter(a => a).length;\n\tconst down = foldedWinding.filter(a => !a).length;\n\texpect(Math.abs(up - down) < 5).toBe(true);\n});\n\ntest(\"makeFacesWindingFromMatrix and makeFacesWindingFromMatrix2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords,\n\t};\n\tconst foldedWinding = ear.graph.makeFacesWinding(folded);\n\tconst cpMatrices = ear.graph.makeFacesMatrix(graph);\n\tconst cpMatrices2 = ear.graph.makeFacesMatrix2(graph);\n\tconst foldedMatrices = ear.graph.makeFacesMatrix(folded);\n\tconst foldedMatrices2 = ear.graph.makeFacesMatrix2(folded);\n\tconst cpMatrixWinding = ear.graph.makeFacesWindingFromMatrix(cpMatrices);\n\tconst cpMatrix2Winding = ear.graph.makeFacesWindingFromMatrix2(cpMatrices2);\n\tconst foldedMatrixWinding = ear.graph.makeFacesWindingFromMatrix(foldedMatrices);\n\tconst foldedMatrix2Winding = ear.graph.makeFacesWindingFromMatrix2(foldedMatrices2);\n\tfoldedWinding.forEach((side, i) => expect(side).toBe(cpMatrixWinding[i]));\n\tfoldedWinding.forEach((side, i) => expect(side).toBe(cpMatrix2Winding[i]));\n\tfoldedWinding.forEach((side, i) => expect(side).toBe(foldedMatrixWinding[i]));\n\tfoldedWinding.forEach((side, i) => expect(side).toBe(foldedMatrix2Winding[i]));\n});\n\ntest(\"makeFacesWinding\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kissing-squares.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords,\n\t};\n\tear.graph.makeFacesWinding(graph)\n\t\t.forEach(side => expect(side).toBe(true));\n\tconst foldedWinding = ear.graph.makeFacesWinding(folded);\n\texpect(foldedWinding[0]).toBe(true);\n\texpect(foldedWinding[1]).toBe(false);\n\texpect(foldedWinding[2]).toBe(true);\n\texpect(foldedWinding[3]).toBe(false);\n});\n"
  },
  {
    "path": "tests/graph.flaps.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"flaps\", () => expect(true).toBe(true));\n\ntest(\"flaps, waterbomb\", () => {\n\tconst graph = ear.graph.waterbomb();\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n});\n\n/*\ntest(\"getEdgesSide\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane-step.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst result0 = ear.graph.getEdgesSide(graph, { vector: [1, 1], origin: [0, 0] });\n\tconst result1 = ear.graph.getEdgesSide(graph, { vector: [1, -1], origin: [0, 1] });\n\tconst positiveCount0 = result0.filter(a => a === 1).length;\n\tconst negativeCount0 = result0.filter(a => a === -1).length;\n\tconst intersectCount0 = result0.filter(a => a === 0).length;\n\tconst collinearCount0 = result0.filter(a => a === 2).length;\n\tconst positiveCount1 = result1.filter(a => a === 1).length;\n\tconst negativeCount1 = result1.filter(a => a === -1).length;\n\tconst intersectCount1 = result1.filter(a => a === 0).length;\n\tconst collinearCount1 = result1.filter(a => a === 2).length;\n\texpect(positiveCount0).toBe(negativeCount0);\n\texpect(positiveCount1).toBe(negativeCount1);\n\texpect(intersectCount0).toBe(2);\n\texpect(collinearCount0).toBe(0);\n\texpect(intersectCount1).toBe(0);\n\texpect(collinearCount1).toBe(4);\n});\n\ntest(\"getFacesSide\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane-step.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst result0 = ear.graph.getFacesSide(graph, { vector: [1, 1], origin: [0, 0] });\n\tconst result1 = ear.graph.getFacesSide(graph, { vector: [1, -1], origin: [0, 1] });\n\tconst positiveCount0 = result0.filter(a => a === 1).length;\n\tconst negativeCount0 = result0.filter(a => a === -1).length;\n\tconst intersectCount0 = result0.filter(a => a === 0).length;\n\tconst positiveCount1 = result1.filter(a => a === 1).length;\n\tconst negativeCount1 = result1.filter(a => a === -1).length;\n\tconst intersectCount1 = result1.filter(a => a === 0).length;\n\texpect(positiveCount0).toBe(negativeCount0);\n\texpect(positiveCount1).toBe(negativeCount1);\n\texpect(intersectCount0).toBe(4);\n\texpect(intersectCount1).toBe(0);\n});\n\ntest(\"crane flaps\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst origin = graph.vertices_coords[39];\n\tconst vector = ear.math.subtract2(...graph.edges_vertices[59].map(v => graph.vertices_coords[v]));\n\t// const vector = [1, -1];\n\tconsole.log(origin, vector);\n\tconst result = ear.graph.getFlapsThroughLine(graph, { vector, origin });\n\t// console.log(graph);\n\texpect(true).toBe(true);\n});\n\n*/\n"
  },
  {
    "path": "tests/graph.fold.flatFold.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"flatFold, square, valley fold\", () => {\n\tconst graph = ear.graph.square();\n\tear.graph.flatFold(graph, { vector: [1, 0.1], origin: [0.5, 0.5] });\n\texpect(ear.graph.countVertices(graph)).toBe(6);\n\texpect(ear.graph.countEdges(graph)).toBe(7);\n\texpect(ear.graph.countFaces(graph)).toBe(2);\n\tear.graph.flatFold(graph, { vector: [0.1, 1], origin: [0.5, 0.5] });\n\texpect(ear.graph.countVertices(graph)).toBe(9);\n\texpect(ear.graph.countEdges(graph)).toBe(12);\n\texpect(ear.graph.countFaces(graph)).toBe(4);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/flatFold-square.fold\",\n\t\tJSON.stringify(graph),\n\t);\n});\n\ntest(\"flatFold, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\n\tear.graph.flatFold(graph, { vector: [1, -1], origin: [0.45, 0.45] });\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\texpect(ear.graph.countVertices(folded)).toBe(88);\n\texpect(ear.graph.countEdges(folded)).toBe(175);\n\texpect(ear.graph.countFaces(folded)).toBe(88);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/flatFold-crane-cp.fold\",\n\t\tJSON.stringify(graph),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/flatFold-crane-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n"
  },
  {
    "path": "tests/graph.fold.foldGraph.layers.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\nconst FOLD_ANGLE = 90;\n\ntest(\"foldGraph, faceOrders, square folding, valley, valley\", () => {\n\tconst graph = ear.graph.square();\n\tear.graph.foldGraph(graph, { vector: [3, 1], origin: [0.5, 0.5] });\n\tgraph.faceOrders = [[0, 1, 1]];\n\texpect(graph.faceOrders).toMatchObject([[0, 1, 1]]);\n\tear.graph.foldGraph(graph, { vector: [-1, 5], origin: [0.5, 0.5] });\n\n\t// splitFace (first): 0 becomes 2, 3\n\t// [0, 1, 1] becomes two rules: [2, 1, 1], [3, 1, 1].\n\t// remap: 0->X 1->0 2->1 3->2\n\t// rules become: [1, 0, 1] and [2, 0, 1]\n\t// splitFace (second): 0 becomes 3, 4\n\t// [1, 0, 1] becomes [1, 3, 1] and [1, 4, 1]\n\t// [2, 0, 1] becomes [2, 3, 1] and [2, 4, 1], the order should be:\n\t// [1, 3, 1], [2, 3, 1], [1, 4, 1], [2, 4, 1]\n\t// faces get remapped, 0->X, 1->0, 2->1, 3->2, 4->3\n\t// [0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1]\n\t// finally, we deal with the faces which no longer overlap\n\t// 0, 2 no longer overlap, 1, 3 no longer overlap\n\t// split line separated by a \"valley\"\n\t// windings: 0 true, 1 true, 2 false, 3 false\n\t// 0-2 face 2 is second one, false on valley results in -1\n\t// 1-3 face 3 is second one, false on valley, results in -1. becomes:\n\t// [0, 2, -1], [1, 2, 1], [0, 3, 1], [1, 3, -1]\n\t// two new orders:\n\t// - 0-1 (true winding) valley 1.\n\t// - 2-3 (false winding) valley -1.\n\texpect(graph.faceOrders).toMatchObject([\n\t\t[0, 2, -1], [1, 2, 1], [0, 3, 1], [1, 3, -1], [0, 1, 1], [2, 3, -1],\n\t]);\n\n\tear.graph.foldGraph(graph, { vector: [4, -1], origin: [0.25, 0.25] });\n\t// console.log(JSON.stringify(graph.faceOrders));\n\t// did not check this yet.\n\texpect(graph.faceOrders).toMatchObject([\n\t\t[0, 4, -1], [2, 4, -1], [0, 6, 1], [2, 6, -1], [0, 2, 1], [4, 6, 1],\n\t\t[1, 4, -1], [1, 6, 1], [1, 2, 1], [3, 4, 1], [3, 6, 1], [0, 3, 1],\n\t\t[1, 3, 1], [0, 5, -1], [2, 5, 1], [5, 6, -1], [1, 5, -1], [3, 5, -1],\n\t\t[0, 7, 1], [2, 7, 1], [4, 7, -1], [1, 7, 1], [3, 7, -1], [5, 7, 1],\n\t\t[0, 1, -1], [2, 3, 1], [4, 5, -1], [6, 7, 1],\n\t]);\n});\n\ntest(\"foldGraph, faceOrders, square folding, valley, mountain\", () => {\n\tconst graph = ear.graph.square();\n\tear.graph.foldGraph(graph, { vector: [3, 1], origin: [0.5, 0.5] });\n\tgraph.faceOrders = [[0, 1, 1]];\n\texpect(graph.faceOrders).toMatchObject([[0, 1, 1]]);\n\tear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [-1, 5], origin: [0.5, 0.5] },\n\t\tear.math.includeL,\n\t\t[],\n\t\t\"M\",\n\t);\n\n\t// splitFace (first): 0 becomes 2, 3\n\t// [0, 1, 1] becomes two rules: [2, 1, 1], [3, 1, 1].\n\t// remap: 0->X 1->0 2->1 3->2\n\t// rules become: [1, 0, 1] and [2, 0, 1]\n\t// splitFace (second): 0 becomes 3, 4\n\t// [1, 0, 1] becomes [1, 3, 1] and [1, 4, 1]\n\t// [2, 0, 1] becomes [2, 3, 1] and [2, 4, 1], the order should be:\n\t// [1, 3, 1], [2, 3, 1], [1, 4, 1], [2, 4, 1]\n\t// faces get remapped, 0->X, 1->0, 2->1, 3->2, 4->3\n\t// [0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1]\n\t// finally, we deal with the faces which no longer overlap\n\t// 0, 2 no longer overlap, 1, 3 no longer overlap\n\t// split line separated by a \"mountain\"\n\t// windings: 0 true, 1 true, 2 false, 3 false\n\t// 0-2 face 2 is second one, false on mountain results in 1\n\t// 1-3 face 3 is second one, false on mountain, results in 1. becomes:\n\t// [0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1]\n\t// two new orders:\n\t// - 0-1 (true winding) mountain -1.\n\t// - 2-3 (false winding) mountain 1.\n\texpect(graph.faceOrders).toMatchObject([\n\t\t[0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1], [0, 1, -1], [2, 3, 1],\n\t]);\n});\n\ntest(\"foldGraph, faceOrders, square folding\", () => {\n\tconst graph = ear.graph.square();\n\n\tconst sequence = {\n\t\t...structuredClone(graph),\n\t\tfile_frames: [],\n\t};\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [3, 1], origin: [0.5, 0.5] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\texpect(graph.faceOrders).toMatchObject([[0, 1, 1]]);\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [-1, 5], origin: [0.5, 0.5] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// splitFace (first): 0 becomes 2, 3\n\t// [0, 1, 1] becomes two rules: [2, 1, 1], [3, 1, 1].\n\t// remap: 0->X 1->0 2->1 3->2\n\t// rules become: [1, 0, 1] and [2, 0, 1]\n\t// splitFace (second): 0 becomes 3, 4\n\t// [1, 0, 1] becomes [1, 3, 1] and [1, 4, 1]\n\t// [2, 0, 1] becomes [2, 3, 1] and [2, 4, 1], the order should be:\n\t// [1, 3, 1], [2, 3, 1], [1, 4, 1], [2, 4, 1]\n\t// faces get remapped, 0->X, 1->0, 2->1, 3->2, 4->3\n\t// [0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1]\n\t// expect(graph.faceOrders).toMatchObject([\n\t// \t[0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1], [0, 1, 1], [2, 3, -1],\n\t// ]);\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [4, -1], origin: [0.25, 0.25] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [1, 4], origin: [0.25, 0.25] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [1, 0], origin: [0.125, 0.125] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [0, 1], origin: [0.125, 0.125] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [1, 1], origin: [0.05, 0.05] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\t// fold step\n\tear.graph.foldGraph(graph, { vector: [-1, 1], origin: [0.05, 0.05] });\n\tsequence.file_frames.push(structuredClone(graph));\n\tsequence.file_frames.push({\n\t\t...structuredClone(graph),\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t\tframe_classes: [\"foldedForm\"],\n\t});\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-fold-sequence.fold\",\n\t\tJSON.stringify(sequence),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, faceOrders, panels\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/panels-simple.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\n\t// no faceOrders exist at this point\n\tear.graph.foldLine(graph, { vector: [1, 0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[0], { vector: [1, -0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[1], { vector: [1, 0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[2], { vector: [1, -0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[3], { vector: [1, 0.1], origin: [0.5, 0.5] });\n\n\texpect(graph.faceOrders)\n\t\t.toMatchObject([[0, 1, 1], [2, 3, -1]]);\n\texpect(graph.file_frames[0].faceOrders)\n\t\t.toMatchObject([[0, 1, 1], [2, 3, -1], [4, 5, 1]]);\n\texpect(graph.file_frames[1].faceOrders)\n\t\t.toMatchObject([[0, 1, 1], [2, 3, -1], [4, 5, 1], [6, 7, -1]]);\n\texpect(graph.file_frames[2].faceOrders)\n\t\t.toMatchObject([[0, 1, 1], [2, 3, -1], [4, 5, 1], [6, 7, -1], [8, 9, 1]]);\n\texpect(graph.file_frames[3].faceOrders)\n\t\t.toMatchObject([[0, 1, 1], [2, 3, -1], [4, 5, 1], [6, 7, -1], [8, 9, 1], [10, 11, -1]]);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-panels.fold`,\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, faceOrders, panels\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/panels-simple.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tgraph.faceOrders = [[0, 1, 1]];\n\tgraph.file_frames[0].faceOrders = [[0, 1, 1], [1, 2, -1]];\n\tgraph.file_frames[1].faceOrders = [[0, 1, 1], [1, 2, -1], [2, 3, 1]];\n\tgraph.file_frames[2].faceOrders = [[0, 1, 1], [1, 2, -1], [2, 3, 1], [3, 4, -1]];\n\tgraph.file_frames[3].faceOrders = [[0, 1, 1], [1, 2, -1], [2, 3, 1], [3, 4, -1], [4, 5, 1]];\n\n\tear.graph.foldLine(graph, { vector: [1, 0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[0], { vector: [1, -0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[1], { vector: [1, 0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[2], { vector: [1, -0.1], origin: [0.5, 0.5] });\n\tear.graph.foldLine(graph.file_frames[3], { vector: [1, 0.1], origin: [0.5, 0.5] });\n\n\t// console.log(JSON.stringify(graph.faceOrders));\n\n\t// in splitFace (first): face 0 turns into face 2 and 3.\n\t// [0, 1, 1] becomes two rules: [2, 1, 1], [3, 1, 1].\n\t// faces get remapped, 0->X, 1->0, 2->1, 3->2.\n\t// rules become: [1, 0, 1] and [2, 0, 1]\n\t// then, splitFace (second): face 0 turns into face 3 and 4\n\t// [1, 0, 1] becomes [1, 3, 1] and [1, 4, 1]\n\t// [2, 0, 1] becomes [2, 3, 1] and [2, 4, 1], the order should be:\n\t// [1, 3, 1], [2, 3, 1], [1, 4, 1], [2, 4, 1]\n\t// faces get remapped, 0->X, 1->0, 2->1, 3->2, 4->3\n\t// [0, 2, 1], [1, 2, 1], [0, 3, 1], [1, 3, 1]\n\t// newly non overlapping faces need to be dealt with:\n\t// 0-3 no longer overlap. 1-2 no longer overlap\n\t// - \"valley\" for 0-3 (3 is false winding) results in -1.\n\t// - \"valley\" for 1-2 (2 is false winding) results in -1.\n\t// finally,\n\t// then [0, 1, 1], [2, 3, -1] get calculated and added at the end\n\texpect(graph.faceOrders).toMatchObject([\n\t\t[0, 2, 1], [1, 2, -1], [0, 3, -1], [1, 3, 1], [0, 1, 1], [2, 3, -1]\n\t]);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-panels.fold`,\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, 3D simple\", () => {\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[0, 0], [1, 0], [2, 0], [3, 0], [0, 3], [0, 2], [0, 1], [1, 3],\n\t\t\t[1, 2], [1, 1], [2, 1], [3, 1], [2, 2], [2, 3], [3, 2], [3, 3],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 0], [7, 8], [8, 9], [9, 1],\n\t\t\t[6, 9], [9, 10], [10, 11], [2, 10], [10, 12], [12, 13], [14, 12], [12, 8],\n\t\t\t[8, 5], [3, 11], [11, 14], [14, 15], [15, 13], [13, 7], [7, 4],\n\t\t],\n\t\tedges_assignment: [\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"F\", \"F\", \"F\",\n\t\t\t\"V\", \"V\", \"V\", \"M\", \"V\", \"V\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\",\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[0, 1, 9, 6], [1, 2, 10, 9], [2, 3, 11, 10], [4, 5, 8, 7], [5, 6, 9, 8],\n\t\t\t[7, 8, 12, 13], [8, 9, 10, 12], [10, 11, 14, 12], [12, 14, 15, 13],\n\t\t],\n\t};\n\n\tear.graph.populate(graph);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t}\n\tgraph.faceOrders = ear.layer(folded).faceOrders();\n\n\t// all of this checks out.\n\t// adjacent faces, valley: 1-2, 3-4, 5-6, 6-7, 5-8\n\t// adjacent faces, mountain: 7-8\n\t// solved layers: 5-7 (away), 6-8 (away)\n\texpect(graph.faceOrders).toMatchObject([\n\t\t[1, 2, 1], [6, 7, 1], [5, 8, 1], [7, 8, -1],\n\t\t[5, 6, 1], [3, 4, 1], [5, 7, -1], [6, 8, -1],\n\t]);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-3D-simple-before.fold`,\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n\n\tear.graph.foldLine(graph, { vector: [-1, 1], origin: [1.75, 1.75] }, \"V\", FOLD_ANGLE);\n\n\tconst newFolded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t};\n\n\tconst { faces_plane } = ear.graph.getFacesPlane(newFolded);\n\tconst badFaceOrders = graph.faceOrders\n\t\t.filter(([a, b]) => faces_plane[a] !== faces_plane[b]);\n\texpect(badFaceOrders).toHaveLength(0);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-3D-simple.fold`,\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-3D-simple-folded.fold`,\n\t\tJSON.stringify(newFolded),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, 3D Kabuto\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(graph);\n\tgraph.faceOrders = ear.layer(folded).faceOrders();\n\tear.graph.foldLine(graph, { vector: [1, 0], origin: [0, 0.25] }, \"V\", FOLD_ANGLE);\n\t// ear.graph.foldLine(graph, { vector: [1, 0], origin: [0, 0.25] }, \"F\", 0);\n\tconst newFoldedGraph = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t};\n\n\tconst { faces_plane } = ear.graph.getFacesPlane(newFoldedGraph);\n\tconst badFaceOrders = graph.faceOrders\n\t\t.filter(([a, b]) => faces_plane[a] !== faces_plane[b]);\n\texpect(badFaceOrders).toHaveLength(0);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-3D-kabuto.fold`,\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n\n\tfs.writeFileSync(\n\t\t`./tests/tmp/foldGraph-3D-kabuto-folded.fold`,\n\t\tJSON.stringify(newFoldedGraph),\n\t\t\"utf8\",\n\t);\n});\n"
  },
  {
    "path": "tests/graph.fold.foldGraph.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\nconst FOLD_ANGLE = 90;\n\ntest(\"foldGraph, segment, along a collinear edge\", () => {\n\tconst graph = ear.graph.squareFish();\n\tconst vertices_coordsFolded = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\n\tconst flatEdgeCount = graph.edges_assignment\n\t\t.filter(a => a === \"F\" || a === \"f\")\n\t\t.length;\n\n\t// a line that runs the entire length of the folded form. diagonally\n\t// along one of the collinear edges along one of the flaps,\n\t// through the other flap\n\tconst line = {\n\t\tvector: [-0.24264068711928527, 0.5857864376269053],\n\t\torigin: [0.7071067811865475, -0.2928932188134526],\n\t};\n\tconst includePoints = [\n\t\t[0.7071067811865475, -0.2928932188134526],\n\t\t[0.4644660940672622, 0.2928932188134527],\n\t];\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\tline,\n\t\tear.math.includeS,\n\t\tincludePoints,\n\t\t\"V\",\n\t\tundefined,\n\t\tvertices_coordsFolded,\n\t);\n\n\tconst newFlatEdgeCount = graph.edges_assignment\n\t\t.filter(a => a === \"F\" || a === \"f\")\n\t\t.length;\n\n\t// one flat edge has been crossed collinearly and has been converted,\n\t// also, one flat edge was crossed and has been split into two,\n\t// thus the total number of flat assignments remains the same.\n\texpect(newFlatEdgeCount).toBe(flatEdgeCount);\n\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-collinear-flat-segment-cp.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-collinear-flat-segment-folded.fold\",\n\t\tJSON.stringify(folded),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, sparse graph\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coordsFolded = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.75, 0.5] },\n\t\tear.math.includeL,\n\t\t[],\n\t\t\"V\",\n\t\tundefined,\n\t\tvertices_coordsFolded,\n\t);\n\t// console.log(\"result\", result);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-crane-line.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, sparse graph, segment\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coordsFolded = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.75, 0.45] },\n\t\tear.math.includeS,\n\t\t[[0.75, 0.45], [0.75, 1.45]],\n\t\t\"V\",\n\t\tundefined,\n\t\tvertices_coordsFolded,\n\t);\n\t// console.log(\"result\", result);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-crane-segment.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, populated graph\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = ear.graph.populate(JSON.parse(FOLD));\n\t// graph.faceOrders = ear.layer(graph).faceOrders();\n\tconst vertices_coordsFolded = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.75, 0.5] },\n\t\tear.math.includeL,\n\t\t[],\n\t\t\"V\",\n\t\tundefined,\n\t\tvertices_coordsFolded,\n\t);\n\t// console.log(\"result\", result);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-populated-crane-line.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, populated graph, segment\", () => {\n\tconst FOLD = fs.readFileSync(\"tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst graph = ear.graph.populate(JSON.parse(FOLD));\n\tconst vertices_coordsFolded = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.75, 0.45] },\n\t\tear.math.includeS,\n\t\t[[0.75, 0.45], [0.75, 1.45]],\n\t\t\"V\",\n\t\tundefined,\n\t\tvertices_coordsFolded,\n\t);\n\t// console.log(\"result\", result);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-populated-crane-segment.fold\",\n\t\tJSON.stringify(graph),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"foldGraph, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tgraph.faceOrders = ear.layer({\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t}).faceOrders();\n\n\tconst result = ear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [1, -1], origin: [0.45, 0.45] },\n\t);\n\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\texpect(result.edges.new).toMatchObject([\n\t\t146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160,\n\t\t161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174,\n\t]);\n\n\texpect(result.faces.map).toMatchObject([\n\t\t[30, 31], [0], [32, 33], [34, 35], [36, 37], [38, 39], [1], [2], [40, 41],\n\t\t[42, 43], [44, 45], [46, 47], [48, 49], [50, 51], [52, 53], [54, 55],\n\t\t[56, 57], [58, 59], [60, 61], [62, 63], [64, 65], [66, 67], [68, 69],\n\t\t[70, 71], [3], [72, 73], [74, 75], [76, 77], [78, 79], [80, 81], [82, 83],\n\t\t[4], [5], [6], [7], [84, 85], [86, 87], [8], [9], [10], [11], [12], [13],\n\t\t[14], [15], [16], [17], [18], [19], [20], [21], [22], [23], [24], [25],\n\t\t[26], [27], [28], [29],\n\t]);\n\n\texpect(ear.graph.countVertices(folded)).toBe(88);\n\texpect(ear.graph.countEdges(folded)).toBe(175);\n\texpect(ear.graph.countFaces(folded)).toBe(88);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-crane-cp.fold\",\n\t\tJSON.stringify(graph),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-crane-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"foldGraph through vertex, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tgraph.faceOrders = ear.layer({\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t}).faceOrders();\n\n\tear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.6464466094063558, 0.6464466094063558] },\n\t\tear.math.includeL,\n\t\t[],\n\t\t\"V\",\n\t\t// 90,\n\t);\n\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-through-vertex-crane-cp.fold\",\n\t\tJSON.stringify(graph),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-through-vertex-crane-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"foldGraph 3D folded through vertex, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tgraph.faceOrders = ear.layer({\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t}).faceOrders();\n\n\tear.graph.foldGraph(\n\t\tgraph,\n\t\t{ vector: [0, 1], origin: [0.6464466094063558, 0.6464466094063558] },\n\t\tear.math.includeL,\n\t\t[],\n\t\t\"V\",\n\t\tFOLD_ANGLE,\n\t);\n\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph), // not flat\n\t};\n\n\tconst { faces_plane } = ear.graph.getFacesPlane(folded);\n\tconst badFaceOrders = graph.faceOrders\n\t\t.filter(([a, b]) => faces_plane[a] !== faces_plane[b]);\n\texpect(badFaceOrders).toHaveLength(0);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-3D-through-vertex-crane-cp.fold\",\n\t\tJSON.stringify(graph),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/foldGraph-3D-through-vertex-crane-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"foldGraph 3D folded edge collinear, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst crane = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tcrane.faceOrders = ear.layer({\n\t\t...crane,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(crane),\n\t}).faceOrders(1);\n\n\t[\n\t\t{ vector: [-1, 1], origin: [0.6464466094063558, 0.6464466094063558] },\n\t\t{\n\t\t\tvector: [0.7419209286103429 - 0.6464466094063558, 0.9510254852310431 - 0.6464466094063558],\n\t\t\torigin: [0.6464466094063558, 0.6464466094063558],\n\t\t},\n\t\t{ vector: [-1, 1], origin: [0.7099378782951967, 0.7099378782951967] },\n\t\t{ vector: [0, 1], origin: [0.6755766511796801, 0.6755766511796801] },\n\t].forEach((line, i) => {\n\t\tconst graph = structuredClone(crane);\n\t\tear.graph.foldGraph(\n\t\t\tgraph,\n\t\t\tline,\n\t\t\tear.math.includeL,\n\t\t\t[],\n\t\t\t\"V\",\n\t\t\tFOLD_ANGLE,\n\t\t);\n\t\tconst folded = {\n\t\t\t...graph,\n\t\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph), // not flat\n\t\t\tframe_classes: [\"foldedForm\"],\n\t\t};\n\n\t\tconst { faces_plane } = ear.graph.getFacesPlane(folded);\n\t\tconst badFaceOrders = graph.faceOrders\n\t\t\t.filter(([a, b]) => faces_plane[a] !== faces_plane[b]);\n\t\texpect(badFaceOrders).toHaveLength(0);\n\n\t\tfs.writeFileSync(\n\t\t\t`./tests/tmp/foldGraph-3D-edge-collinear-crane-cp-${i}.fold`,\n\t\t\tJSON.stringify(graph),\n\t\t);\n\t\tfs.writeFileSync(\n\t\t\t`./tests/tmp/foldGraph-3D-edge-collinear-crane-folded-${i}.fold`,\n\t\t\tJSON.stringify(folded),\n\t\t);\n\t});\n});\n"
  },
  {
    "path": "tests/graph.fold.foldGraphIntoSegments.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"foldGraphIntoSegments, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\n\tconst result = ear.graph.foldGraphIntoSegments(\n\t\tgraph,\n\t\t{ vector: [1, -1], origin: [0.45, 0.45] },\n\t);\n\n\t// these faces were intersected\n\texpect(Object.keys(result).map(parseFloat)).toMatchObject([\n\t\t0, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,\n\t\t19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 35, 36,\n\t]);\n\n\t// expect(ear.graph.countVertices(folded)).toBe(88);\n\t// expect(ear.graph.countEdges(folded)).toBe(175);\n\t// expect(ear.graph.countFaces(folded)).toBe(88);\n\n\t// fs.writeFileSync(\n\t// \t\"./tests/tmp/foldGraphIntoSegments-crane-cp.fold\",\n\t// \tJSON.stringify(graph),\n\t// );\n\t// fs.writeFileSync(\n\t// \t\"./tests/tmp/foldGraphIntoSegments-crane-folded.fold\",\n\t// \tJSON.stringify(folded),\n\t// );\n});\n"
  },
  {
    "path": "tests/graph.fold.foldGraphIntoSubgraph.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"foldGraphIntoSubgraph\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.intersect.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"intersectLineVertices through vertices\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 0.5], origin: [0.1, 0.1] };\n\tconst result = ear.graph.intersectLineVertices(graph, line);\n\n\texpect(result.length).toBe(4);\n\texpect(result[0]).toBe(-0.2);\n\texpect(result[2]).toBe(1.8);\n});\n\ntest(\"intersectLineVerticesEdges through vertices\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 0.5], origin: [0.1, 0.1] };\n\tconst result = ear.graph.intersectLineVerticesEdges(graph, line);\n\n\texpect(result.vertices.length).toBe(4);\n\texpect(result.vertices[0]).toBeCloseTo(-0.2);\n\texpect(result.vertices[2]).toBeCloseTo(1.8);\n\n\texpect(result.edges.length).toBe(4);\n\texpect(result.edges[0]).toBe(undefined);\n\texpect(result.edges[1]).toBe(undefined);\n\texpect(result.edges[2]).toBe(undefined);\n\texpect(result.edges[3]).toBe(undefined);\n});\n\ntest(\"intersectLine through vertices\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 0.5], origin: [0.1, 0.1] };\n\tconst result = ear.graph.intersectLine(graph, line);\n\n\texpect(result.vertices.length).toBe(4);\n\texpect(result.vertices[0]).toBe(-0.2);\n\texpect(result.vertices[2]).toBe(1.8);\n\n\texpect(result.edges.length).toBe(4);\n\texpect(result.edges[0]).toBe(undefined);\n\texpect(result.edges[1]).toBe(undefined);\n\texpect(result.edges[2]).toBe(undefined);\n\texpect(result.edges[3]).toBe(undefined);\n\n\texpect(result.faces.length).toBe(1);\n\texpect(result.faces[0].length).toBe(2);\n\texpect(result.faces[0][0].a).toBeCloseTo(-0.2);\n\texpect(result.faces[0][0].b).toBe(undefined);\n\texpect(result.faces[0][0].vertex).toBe(0);\n\texpect(result.faces[0][0].edge).toBe(undefined);\n\n\texpect(result.faces[0][1].a).toBeCloseTo(1.8);\n\texpect(result.faces[0][1].b).toBe(undefined);\n\texpect(result.faces[0][1].vertex).toBe(2);\n\texpect(result.faces[0][1].edge).toBe(undefined);\n});\n\ntest(\"intersectLineVertices through vertex and edge\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 1], origin: [0.1, 0.2] };\n\tconst result = ear.graph.intersectLineVertices(graph, line);\n\n\texpect(result.length).toBe(4);\n\n\texpect(result[0]).toBe(-0.2);\n\texpect(result[1]).toBe(undefined);\n\texpect(result[2]).toBe(undefined);\n\texpect(result[3]).toBe(undefined);\n});\n\ntest(\"intersectLineVerticesEdges through vertex and edge\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 1], origin: [0.1, 0.2] };\n\tconst result = ear.graph.intersectLineVerticesEdges(graph, line);\n\n\texpect(result.vertices.length).toBe(4);\n\texpect(result.edges.length).toBe(4);\n\n\texpect(result.edges[0]).toBe(undefined);\n\texpect(result.edges[1]).toBe(undefined);\n\texpect(result.edges[2]).not.toBe(undefined);\n\texpect(result.edges[3]).toBe(undefined);\n\n\texpect(result.edges[2].a).toBeCloseTo(0.8);\n\texpect(result.edges[2].b).toBeCloseTo(0.5);\n\texpect(result.edges[2].vertex).toBe(undefined);\n});\n\ntest(\"intersectLine through vertex and edge\", () => {\n\tconst graph = ear.graph.square();\n\tconst line = { vector: [0.5, 1], origin: [0.1, 0.2] };\n\tconst result = ear.graph.intersectLine(graph, line);\n\n\texpect(result.vertices.length).toBe(4);\n\texpect(result.edges.length).toBe(4);\n\texpect(result.faces.length).toBe(1);\n\n\texpect(result.faces[0].length).toBe(2);\n\n\texpect(result.faces[0][0].a).toBeCloseTo(-0.2);\n\texpect(result.faces[0][0].b).toBe(undefined);\n\texpect(result.faces[0][0].vertex).toBe(0);\n\texpect(result.faces[0][0].edge).toBe(undefined);\n\n\texpect(result.faces[0][1].a).toBeCloseTo(0.8);\n\texpect(result.faces[0][1].b).toBe(0.5);\n\texpect(result.faces[0][1].point[0]).toBe(0.5);\n\texpect(result.faces[0][1].point[1]).toBe(1);\n\texpect(result.faces[0][1].vertex).toBe(undefined);\n\texpect(result.faces[0][1].edge).toBe(2);\n});\n\ntest(\"intersectLineAndPoints, randlett-flapping-bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\n\tconst line = { vector: [-1, 1], origin: [0.25, 0.25] };\n\n\tconst {\n\t\tvertices,\n\t\tedges,\n\t\tfaces,\n\t} = ear.graph.intersectLineAndPoints(graph, line);\n\n\texpect(vertices.filter(a => a)).toHaveLength(2);\n\texpect(edges.filter(a => a)).toHaveLength(3);\n\t// everything checks out here.\n\texpect(faces).toMatchObject([\n\t\t{\n\t\t\tedges: [{ edge: 16 }, { edge: 6 }], // a:, b:, point:,\n\t\t\tvertices: [],\n\t\t\tpoints: [],\n\t\t},\n\t\t{\n\t\t\tedges: [{ edge: 6 }, { edge: 17 }], // a:, b:, point:,\n\t\t\tvertices: [],\n\t\t\tpoints: [],\n\t\t},\n\t\t{\n\t\t\tedges: [{ edge: 16 }], // a:, b:, point:,\n\t\t\tvertices: [{ a: -0.25, vertex: 2 }],\n\t\t\tpoints: [],\n\t\t},\n\t\t{ edges: [], vertices: [{ a: -0.25, vertex: 2 }], points: [] }, // does not cross\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [{ a: 0.25, vertex: 5 }], points: [] }, // does not cross\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{\n\t\t\tedges: [{ edge: 17 }], // a:, b:, point:,\n\t\t\tvertices: [{ a: 0.25, vertex: 5 }],\n\t\t\tpoints: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t\t{ edges: [], vertices: [], points: [] },\n\t]);\n});\n\ntest(\"filterCollinearFacesData, randlett-flapping-bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst line = { vector: [1, 1], origin: [0, 0] };\n\tconst intersect = ear.graph.intersectLineAndPoints(graph, line);\n\n\texpect(intersect.vertices.filter(a => a)).toHaveLength(4);\n\texpect(intersect.edges.filter(a => a)).toHaveLength(0);\n\texpect(intersect.faces.filter(({ edges, vertices, points }) => !(\n\t\tedges.length === 0 && vertices.length === 0 && points.length === 0\n\t))).toHaveLength(14);\n\n\tear.graph.filterCollinearFacesData(graph, intersect);\n\n\t// vertices and edges remain unchanged.\n\n\t// 14 faces becomes 6 faces:\n\t// 3 on either side. these are faces which have one vertex collinear\n\t// to the fold line (not a pair of vertices). no face has a pair of vertices.\n\texpect(intersect.vertices.filter(a => a)).toHaveLength(4);\n\texpect(intersect.edges.filter(a => a)).toHaveLength(0);\n\texpect(intersect.faces.filter(({ edges, vertices, points }) => !(\n\t\tedges.length === 0 && vertices.length === 0 && points.length === 0\n\t))).toHaveLength(6);\n});\n"
  },
  {
    "path": "tests/graph.intersect.vertices.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"depricated\", () => {});\n\n// test(\"getVerticesCollinearToLine\", () => {\n// \tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane-step.fold\", \"utf-8\");\n// \tconst graph = JSON.parse(FOLD);\n// \tconst result0 = ear.graph.getVerticesCollinearToLine(\n// \t\tgraph,\n// \t\t{ vector: [1, 1], origin: [0, 0] },\n// \t);\n// \tconst result1 = ear.graph.getVerticesCollinearToLine(\n// \t\tgraph,\n// \t\t{ vector: [1, -1], origin: [0, 1] },\n// \t);\n// \texpect(result0.length).toBe(3);\n// \texpect(result1.length).toBe(5);\n// });\n\n// test(\"getVerticesCollinearToLine. same line different origin\", () => {\n// \tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane-step.fold\", \"utf-8\");\n// \tconst graph = JSON.parse(FOLD);\n// \tconst result0 = ear.graph.getVerticesCollinearToLine(\n// \t\tgraph,\n// \t\t{ vector: [1, 1], origin: [0, 0] },\n// \t);\n// \tconst result1 = ear.graph.getVerticesCollinearToLine(\n// \t\tgraph,\n// \t\t{ vector: [1, 1], origin: [0, 0] },\n// \t);\n// \tconst result2 = ear.graph.getVerticesCollinearToLine(\n// \t\tgraph,\n// \t\t{ vector: [1, 1], origin: [0, 0] },\n// \t);\n// \texpect(result0.length).toBe(result1.length);\n// \texpect(result1.length).toBe(result2.length);\n// });\n"
  },
  {
    "path": "tests/graph.join.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst graph = ear.graph({\n\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 1], [0.5, 1], [0, 1]],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4]],\n\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"F\"],\n\tfaces_vertices: [[0, 1, 4, 5], [3, 4, 1, 2]],\n});\n\ntest(\"join graphs\", () => {\n\tconst cp1 = ear.graph.fish();\n\tconst cp2 = ear.graph.fish().translate([0.5, 0]);\n\tear.graph.join(cp1, cp2);\n});\n\ntest(\"join graphs with 2D and 3D vertices\", () => {\n\t// all vertices will be converted into 3D, duplicate vertices\n\t// check will be correct and as expected.\n\t// see: \"removeDuplicateVertices with 2D and 3D vertices\"\n\t// in vertices.duplicate.test for more context.\n\n\t// vertices: [[0,0],[0.5,0],[1,0],[1,1],[0.5,1],[0,1]]\n\tconst cp1 = graph.clone();\n\t// vertices: [[0.5,0],[0.5,0],[0.5,0],[0.5,1],[0.5,1],[0.5,1]]\n\tconst cp2 = graph.clone().rotate(Math.PI / 2, [0, 1, 0], [0.5, 0, 0]);\n\tear.graph.join(cp1, cp2);\n\texpect(cp1.vertices_coords.length).toBe(12);\n\tear.graph.removeDuplicateVertices(cp1);\n\texpect(cp1.vertices_coords.length).toBe(10);\n\texpect(cp1.edges_vertices.length).toBe(14);\n\texpect(cp1.faces_vertices.length).toBe(4);\n});\n"
  },
  {
    "path": "tests/graph.make.edges.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"make edges_foldAngle\", () => {\n\tconst result = ear.graph.makeEdgesFoldAngle({\n\t\tedges_assignment: [\"M\", \"m\", \"f\", \"F\", \"V\", \"v\"],\n\t});\n\texpect(result).toEqual(expect.arrayContaining([-180, -180, 0, 0, 180, 180]));\n});\n\ntest(\"make edges_foldAngle undefineds\", () => {\n\tconst result = ear.graph.makeEdgesFoldAngle({\n\t\tedges_assignment: [\"M\", undefined, undefined, \"F\", \"V\"],\n\t});\n\texpect(result).toEqual(expect.arrayContaining([-180, 0, 0, 0, 180]));\n});\n\ntest(\"make edges_assignment\", () => {\n\tconst result = ear.graph.makeEdgesAssignmentSimple({\n\t\tedges_foldAngle: [-180, -180, 0, 0, 180, 180],\n\t});\n\texpect(result).toEqual(expect.arrayContaining([\"M\", \"M\", \"F\", \"F\", \"V\", \"V\"]));\n});\n\ntest(\"make edges_assignment\", () => {\n\tconst result = ear.graph.makeEdgesAssignmentSimple({\n\t\tedges_foldAngle: [-Infinity, -1, -0.5, -1e-10, 0, 1e-10, 0.5, 1, Infinity],\n\t});\n\texpect(result).toEqual(expect.arrayContaining([\"M\", \"M\", \"M\", \"M\", \"F\", \"V\", \"V\", \"V\", \"V\"]));\n});\n\ntest(\"make edges_length\", () => {\n\tconst result = ear.graph.makeEdgesLength({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t});\n\texpect(result[4]).toBeCloseTo(Math.sqrt(2));\n\texpect(result).toEqual(expect.arrayContaining([1, 1, 1, 1, result[4]]));\n});\n"
  },
  {
    "path": "tests/graph.make.edgesEdges.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"edges_edges square\", () => {\n\tconst graph = { edges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0]] };\n\tgraph.vertices_edges = ear.graph.makeVerticesEdgesUnsorted(graph);\n\tconst result = ear.graph.makeEdgesEdges(graph);\n\texpect(result[0]).toEqual(expect.arrayContaining([3, 1]));\n\texpect(result[1]).toEqual(expect.arrayContaining([0, 2]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1, 3]));\n\texpect(result[3]).toEqual(expect.arrayContaining([2, 0]));\n});\n\ntest(\"edges_edges line\", () => {\n\tconst graph = { edges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]] };\n\tgraph.vertices_edges = ear.graph.makeVerticesEdgesUnsorted(graph);\n\tconst result = ear.graph.makeEdgesEdges(graph);\n\texpect(result[0]).toEqual(expect.arrayContaining([1]));\n\texpect(result[1]).toEqual(expect.arrayContaining([0, 2]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1, 3]));\n\texpect(result[3]).toEqual(expect.arrayContaining([2, 4]));\n\texpect(result[4]).toEqual(expect.arrayContaining([3, 5]));\n\texpect(result[5]).toEqual(expect.arrayContaining([4, 6]));\n\texpect(result[6]).toEqual(expect.arrayContaining([5]));\n});\n\ntest(\"edges_edges, no edge adjacency\", () => {\n\tconst graph = { edges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]] };\n\tgraph.vertices_edges = ear.graph.makeVerticesEdgesUnsorted(graph);\n\tconst result = ear.graph.makeEdgesEdges(graph);\n\texpect(result[0].length).toBe(0);\n\texpect(result[1].length).toBe(0);\n\texpect(result[2].length).toBe(0);\n\texpect(result[3].length).toBe(0);\n});\n\ntest(\"edges_edges, bad edge construction\", () => {\n\tconst graph = { edges_vertices: [[0, 1, 2], [2, 3, 4], [4, 5, 6]] };\n\tgraph.vertices_edges = ear.graph.makeVerticesEdgesUnsorted(graph);\n\tconst result = ear.graph.makeEdgesEdges(graph);\n\texpect(result[0].length).toBe(0);\n\texpect(result[1]).toEqual(expect.arrayContaining([0]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1]));\n});\n\n// test(\"make edges_edges 1\", () => {\n//   const result = ear.graph.makeEdgesEdges({\n//     edges_vertices: [[0, 1], [1,2], [2,3], [3,0]],\n//   });\n//   expect(result[0]).toEqual(expect.arrayContaining([0, 3]));\n//   expect(result[1]).toEqual(expect.arrayContaining([0, 1]));\n//   expect(result[2]).toEqual(expect.arrayContaining([1, 2]));\n//   expect(result[3]).toEqual(expect.arrayContaining([2, 3]));\n// });\n\n// test(\"make edges_edges, invalid faces\", () => {\n//   const res1 = ear.graph.makeEdgesEdges({\n//     edges_vertices: [[undefined], [undefined]]\n//   });\n//   expect(res1[0]).toEqual(expect.arrayContaining([1]));\n//   expect(res1[1]).toEqual(expect.arrayContaining([0]));\n//   // lol. look, it will match anything including strings\n//   const res2 = ear.graph.makeEdgesEdges({ edges_vertices: [[\"hi\"], [\"hi\"]] });\n//   expect(res2[0]).toEqual(expect.arrayContaining([1]));\n//   expect(res2[1]).toEqual(expect.arrayContaining([0]));\n//   const res3 = ear.graph.makeEdgesEdges({ edges_vertices: [[\"hi\"], [\"bye\"]] });\n//   expect(res3[0].length).toBe(0);\n//   expect(res3[1].length).toBe(0);\n// });\n\n// test(\"make edges_edges 3\", () => {\n//   // technically these edges are invalid\n//   const result = ear.graph.makeEdgesEdges({\n//     edges_vertices: [[0, 1, 2, 3, 4], [5, 6]],\n//   });\n//   [[0], [0], [0], [0], [0], [1], [1]].forEach((arr, i) => {\n//     expect(result[i]).toEqual(expect.arrayContaining(arr));\n//   })\n// });\n\n// test(\"make edges_edges 4\", () => new Promise(done => {\n//   try {\n//     const result = ear.graph.makeEdgesEdges({\n//       edges_vertices: [[0], [1], undefined, [2]],\n//     });\n//   } catch (error) {\n//     expect(error).not.toBe(undefined);\n//     done();\n//   }\n// }));\n\ntest(\"make edges_edges 5\", () => new Promise(done => {\n\ttry {\n\t\tconst result = ear.graph.makeEdgesEdges();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make edges_edges 6\", () => new Promise(done => {\n\ttry {\n\t\tconst result = ear.graph.makeEdgesEdges({});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make edges_edges 7\", () => {\n\tconst result = ear.graph.makeEdgesEdges({ edges_vertices: [] });\n\texpect(result.length).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.make.edgesFaces.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// because there are no faces to build edges_faces, the question is:\n// should edges_faces be empty, or contain empty arrays one for each edge\ntest(\"no face graph\", () => {\n\tconst origami = ear.graph({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [1, 0]],\n\t\tedges_vertices: [[0, 1], [1, 2]],\n\t\tedges_assignment: [\"B\", \"B\"],\n\t});\n\n\texpect(origami.edges_faces.length).toBe(2);\n\texpect(origami.edges_faces[0].length).toBe(0);\n\texpect(origami.edges_faces[1].length).toBe(0);\n});\n\ntest(\"edges_faces square\", () => {\n\tconst result = ear.graph.makeEdgesFaces({\n\t\tfaces_edges: [[0, 4, 3], [1, 2, 4]],\n\t});\n\texpect(result[0]).toEqual(expect.arrayContaining([0]));\n\texpect(result[1]).toEqual(expect.arrayContaining([1]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1]));\n\texpect(result[3]).toEqual(expect.arrayContaining([0]));\n\texpect(result[4]).toEqual(expect.arrayContaining([0, 1]));\n});\n\ntest(\"edges_faces\", () => {\n\tconst result = ear.graph.makeEdgesFaces({\n\t\tfaces_edges: [[8, 7, 6], [5, 4, 3]],\n\t});\n\texpect(result[0].length).toBe(0);\n\texpect(result[1].length).toBe(0);\n\texpect(result[2].length).toBe(0);\n\texpect(result[3]).toEqual(expect.arrayContaining([1]));\n\texpect(result[4]).toEqual(expect.arrayContaining([1]));\n\texpect(result[5]).toEqual(expect.arrayContaining([1]));\n\texpect(result[6]).toEqual(expect.arrayContaining([0]));\n\texpect(result[7]).toEqual(expect.arrayContaining([0]));\n\texpect(result[8]).toEqual(expect.arrayContaining([0]));\n});\n\ntest(\"edges faces direction\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [2, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"M\"],\n\t};\n\t// prepare\n\tconst planar_faces = ear.graph.makePlanarFaces(graph);\n\tgraph.faces_vertices = planar_faces.faces_vertices;\n\tgraph.faces_edges = planar_faces.faces_edges;\n\n\tconst edges_faces1 = ear.graph.makeEdgesFaces(graph);\n\texpect(edges_faces1[4][0]).toBe(0);\n\texpect(edges_faces1[4][1]).toBe(1);\n\n\tgraph.edges_vertices = [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]];\n\n\tconst edges_faces2 = ear.graph.makeEdgesFaces(graph);\n\texpect(edges_faces2[4][0]).toBe(1);\n\texpect(edges_faces2[4][1]).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.make.facesFaces.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"make faces_faces, square\", () => {\n\tconst result = ear.graph.makeFacesFaces({\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t});\n\texpect(result.length).toBe(2);\n\texpect(result[0]).toEqual(expect.arrayContaining([1]));\n\texpect(result[1]).toEqual(expect.arrayContaining([0]));\n});\n\ntest(\"make faces_faces, invalid faces\", () => {\n\tconst res1 = ear.graph.makeFacesFaces({\n\t\tfaces_vertices: [[undefined], [undefined]],\n\t});\n\texpect(res1[0]).toEqual(expect.arrayContaining([1]));\n\texpect(res1[1]).toEqual(expect.arrayContaining([0]));\n\n\t// lol. look, it will match anything including strings\n\texpect(ear.graph.makeFacesFaces({ faces_vertices: [[\"hi\"], [\"hi\"]] }))\n\t\t.toMatchObject([[1], [0]]);\n\texpect(ear.graph.makeFacesFaces({ faces_vertices: [[\"hi\"], [\"bye\"]] }))\n\t\t.toMatchObject([[undefined], [undefined]]);\n});\n\ntest(\"make faces_faces, degenerate\", () => {\n\t// technically these edges are invalid\n\t// should be one empty face_face array\n\texpect(ear.graph.makeFacesFaces({ faces_vertices: [[0, 0]] }))\n\t\t.toMatchObject([[undefined, undefined]]);\n});\n\ntest(\"make faces_faces 4\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.makeFacesFaces({\n\t\t\tfaces_vertices: [[0], [1], undefined, [2]],\n\t\t});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make faces_faces 5\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.makeFacesFaces();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make faces_faces 6\", () => new Promise(done => {\n\ttry {\n\t\tear.graph.makeFacesFaces({});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make faces_faces 7\", () => {\n\tconst result = ear.graph.makeFacesFaces({ faces_vertices: [] });\n\texpect(result.length).toBe(0);\n});\n\ntest(\"kabuto\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst kabuto = JSON.parse(FOLD);\n\tconst faces_faces = ear.graph.makeFacesFaces(kabuto);\n\n\texpect(faces_faces).toMatchObject([\n\t\t[8, 3, 9],\n\t\t[2, 10, 11],\n\t\t[1, undefined, undefined],\n\t\t[0, undefined, 4, undefined],\n\t\t[3, undefined, 5, undefined],\n\t\t[4, undefined, undefined],\n\t\t[16, undefined, 8],\n\t\t[17, 9, undefined],\n\t\t[0, 10, 6],\n\t\t[0, 7, 11],\n\t\t[12, 8, 1],\n\t\t[13, 1, 9],\n\t\t[10, undefined, 14],\n\t\t[11, 15, undefined],\n\t\t[16, 12, undefined],\n\t\t[17, undefined, 13],\n\t\t[6, 14, undefined],\n\t\t[7, undefined, 15],\n\t]);\n});\n\ntest(\"face that repeats an edge\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 4], [4, 2], [2, 3], [3, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\t\tfaces_vertices: [[0, 1, 2, 4, 2, 3]],\n\t\tfaces_edges: [[0, 1, 2, 3, 4, 5]],\n\t};\n\tear.graph.populate(graph);\n\n\t// 6 entries of undefined\n\texpect(graph.faces_faces).toMatchObject([[\n\t\tundefined, undefined, undefined, undefined, undefined, undefined\n\t]]);\n});\n"
  },
  {
    "path": "tests/graph.make.facesMatrix.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"faces matrix, diagonal fold\", () => {\n\tconst result = ear.graph.makeFacesMatrix({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n\t\tedges_foldAngle: [0, 0, 0, 0, 90],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t});\n\tconst _707 = Math.sqrt(2) / 2;\n\t[0.5, -0.5, _707, -0.5, 0.5, _707, -_707, -_707, 0, 0.5, 0.5, -_707]\n\t\t.forEach((v, i) => expect(result[1][i]).toBeCloseTo(v));\n});\n\ntest(\"faces matrix, book fold\", () => {\n\tconst result = ear.graph.makeFacesMatrix({\n\t\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 1], [0.5, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4]],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 180],\n\t\tfaces_vertices: [[0, 1, 4, 5], [1, 2, 3, 4]],\n\t});\n\t[-1, 0, 0, 0, 1, 0, 0, 0, -1, 1, 0, 0]\n\t\t.forEach((v, i) => expect(result[1][i]).toBeCloseTo(v));\n});\n\ntest(\"faces matrix, book fold, rectangle\", () => {\n\tconst result = ear.graph.makeFacesMatrix({\n\t\tvertices_coords: [[0, 0], [1, 0], [2, 0], [2, 1], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4]],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 180],\n\t\tfaces_vertices: [[0, 1, 4, 5], [1, 2, 3, 4]],\n\t});\n\t[-1, 0, 0, 0, 1, 0, 0, 0, -1, 2, 0, 0]\n\t\t.forEach((v, i) => expect(result[1][i]).toBeCloseTo(v));\n});\n\ntest(\"faces matrix, assignment, no foldAngle\", () => {\n\tconst result = ear.graph.makeFacesMatrix({\n\t\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 1], [0.5, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\"],\n\t\tfaces_vertices: [[0, 1, 4, 5], [1, 2, 3, 4]],\n\t});\n\t[-1, 0, 0, 0, 1, 0, 0, 0, -1, 1, 0, 0]\n\t\t.forEach((v, i) => expect(result[1][i]).toBeCloseTo(v));\n});\n"
  },
  {
    "path": "tests/graph.make.facesVertices.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// we do want this method. bring this back (idk when it was even here)\ntest(\"faces_vertices\", () => {\n\texpect(true).toBe(true);\n\t// const craneJSON = fs.readFileSync(\"./tests/files/crane.fold\");\n\t// const crane = JSON.parse(craneJSON);\n\t// delete crane.faces_vertices;\n\t// // crane.vertices_vertices = ear.graph.makeVerticesVertices(crane);\n\t// crane.faces_vertices = ear.graph.make_faces_vertices(crane);\n\t// crane.faces_edges = ear.graph.make_faces_edges(crane);\n\t// fs.writeFileSync(\"./tests/files/crane-faces-rebuilt.fold\", JSON.stringify(crane), \"utf8\");\n\n\t// // console.log(ear.graph.makeVerticesVerticesVector(crane));\n\t// // console.log(ear.graph.make_vertex_pair_to_edge_map_directional(crane));\n\t// // console.log(crane.vertices_sectors);\n});\n"
  },
  {
    "path": "tests/graph.make.facesWinding.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"faces winding, flat cp\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\");\n\tconst crane = JSON.parse(foldfile);\n\tconst windings = ear.graph.makeFacesWinding(crane);\n\t// all must be true (counter-clockwise)\n\texpect(windings.reduce((a, b) => a && b, true)).toBe(true);\n});\n\ntest(\"faces winding, folded\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\");\n\tconst crane = JSON.parse(foldfile);\n\tconst folded = ear.graph(crane).folded();\n\tconst windings = ear.graph.makeFacesWinding(folded);\n\tconst up = windings.filter(a => a === true).length;\n\tconst down = windings.filter(a => a === false).length;\n\t// about half of the faces (within 5%) should be flipped\n\texpect(Math.abs(up - down) < crane.faces_vertices.length * 0.05).toBe(true);\n});\n\ntest(\"faces_coloring\", () => {\n\t// const foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\");\n\t// const crane = JSON.parse(foldfile);\n\texpect(true).toBe(true);\n\t// todo bring this back\n\n\t// const tree = ear.graph.makeFaceSpanningTree(crane);\n\t// const winding = ear.graph.makeFacesWinding(crane);\n\t// crane.faces_matrix = ear.graph.makeFacesMatrix(crane);\n\t// const coloring2 = ear.graph.makeFacesWindingFromMatrix(crane.faces_matrix);\n\t// expect(winding.length).toBe(coloring2.length);\n\t// winding.forEach((color, i) => expect(color).toBe(coloring2[i]));\n});\n"
  },
  {
    "path": "tests/graph.make.lookup.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeVerticesToEdge\", () => {\n\tconst graph = ear.graph.blintz();\n\texpect(ear.graph.makeVerticesToEdge(graph)).toMatchObject({\n\t\t\"0 1\": 0,\n\t\t\"1 2\": 1,\n\t\t\"2 3\": 2,\n\t\t\"3 4\": 3,\n\t\t\"4 5\": 4,\n\t\t\"5 6\": 5,\n\t\t\"6 7\": 6,\n\t\t\"7 0\": 7,\n\t\t\"7 1\": 8,\n\t\t\"1 3\": 9,\n\t\t\"3 5\": 10,\n\t\t\"5 7\": 11,\n\t\t\"1 0\": 0,\n\t\t\"2 1\": 1,\n\t\t\"3 2\": 2,\n\t\t\"4 3\": 3,\n\t\t\"5 4\": 4,\n\t\t\"6 5\": 5,\n\t\t\"7 6\": 6,\n\t\t\"0 7\": 7,\n\t\t\"1 7\": 8,\n\t\t\"3 1\": 9,\n\t\t\"5 3\": 10,\n\t\t\"7 5\": 11,\n\t});\n});\n\ntest(\"makeVerticesToFace\", () => {\n\tconst graph = ear.graph.blintz();\n\tconst faceMap = ear.graph.makeVerticesToFace(graph);\n\texpect(faceMap).toMatchObject({\n\t\t\"1 3\": 0,\n\t\t\"3 5\": 0,\n\t\t\"5 7\": 0,\n\t\t\"7 1\": 0,\n\t\t\"1 7\": 1,\n\t\t\"0 1\": 1,\n\t\t\"7 0\": 1,\n\t\t\"1 2\": 2,\n\t\t\"2 3\": 2,\n\t\t\"3 1\": 2,\n\t\t\"3 4\": 3,\n\t\t\"4 5\": 3,\n\t\t\"5 3\": 3,\n\t\t\"5 6\": 4,\n\t\t\"6 7\": 4,\n\t\t\"7 5\": 4,\n\t});\n});\n\ntest(\"makeEdgesToFace\", () => {\n\tconst graph = ear.graph.blintz();\n\tconst faceMap = ear.graph.makeEdgesToFace(graph);\n\texpect(faceMap).toMatchObject({\n\t\t\"8 9\": 0,\n\t\t\"9 10\": 0,\n\t\t\"10 11\": 0,\n\t\t\"11 8\": 0,\n\t\t\"0 8\": 1,\n\t\t\"7 0\": 1,\n\t\t\"8 7\": 1,\n\t\t\"9 1\": 2,\n\t\t\"1 2\": 2,\n\t\t\"2 9\": 2,\n\t\t\"10 3\": 3,\n\t\t\"3 4\": 3,\n\t\t\"4 10\": 3,\n\t\t\"11 5\": 4,\n\t\t\"5 6\": 4,\n\t\t\"6 11\": 4,\n\t});\n});\n"
  },
  {
    "path": "tests/graph.make.verticesEdges.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"make vertices_edges 1\", () => {\n\tconst result = ear.graph.makeVerticesEdgesUnsorted({\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0]],\n\t});\n\texpect(result[0]).toEqual(expect.arrayContaining([0, 3]));\n\texpect(result[1]).toEqual(expect.arrayContaining([0, 1]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1, 2]));\n\texpect(result[3]).toEqual(expect.arrayContaining([2, 3]));\n});\n\ntest(\"make vertices_edges 2\", () => {\n\tconst result = ear.graph.makeVerticesEdgesUnsorted({\n\t\tedges_vertices: [[0, 1], [0, 2], [0, 3], [0, 4]],\n\t});\n\texpect(result[0]).toEqual(expect.arrayContaining([0, 1, 2, 3]));\n\texpect(result[1]).toEqual(expect.arrayContaining([0]));\n\texpect(result[2]).toEqual(expect.arrayContaining([1]));\n\texpect(result[3]).toEqual(expect.arrayContaining([2]));\n\texpect(result[4]).toEqual(expect.arrayContaining([3]));\n});\n\ntest(\"make vertices_edges 3\", () => {\n\t// technically these edges are invalid\n\tconst result = ear.graph.makeVerticesEdgesUnsorted({\n\t\tedges_vertices: [[0, 1, 2, 3, 4], [5, 6]],\n\t});\n\t[[0], [0], [0], [0], [0], [1], [1]].forEach((arr, i) => {\n\t\texpect(result[i]).toEqual(expect.arrayContaining(arr));\n\t});\n});\n\ntest(\"make vertices_edges 4\", () => new Promise(done => {\n\ttry {\n\t\tconst result = ear.graph.makeVerticesEdgesUnsorted({\n\t\t\tedges_vertices: [[0], [1], undefined, [2]],\n\t\t});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make vertices_edges 5\", () => new Promise(done => {\n\ttry {\n\t\tconst result = ear.graph.makeVerticesEdgesUnsorted();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make vertices_edges 6\", () => new Promise(done => {\n\ttry {\n\t\tconst result = ear.graph.makeVerticesEdgesUnsorted({});\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"make vertices_edges 7\", () => {\n\tconst result = ear.graph.makeVerticesEdgesUnsorted({ edges_vertices: [] });\n\texpect(result.length).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.make.verticesFaces.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeVerticesFacesUnsorted, with vertices, isolated\", () => {\n\tconst result = ear.graph.makeVerticesFacesUnsorted({\n\t\tvertices_coords: [\n\t\t\t[0, 0], [1, 1], [0, 1], [1, 0], [0.5, 1], [0.5, 0], [1, 0.5],\n\t\t],\n\t\tfaces_vertices: [[3, 4, 5], [3, 6, 4]],\n\t});\n\texpect(JSON.stringify(result)).toBe(\"[[],[],[],[0,1],[0,1],[0],[1]]\");\n});\n\ntest(\"makeVerticesFacesUnsorted, with vertices, no vertices\", () => {\n\tconst result = ear.graph.makeVerticesFacesUnsorted({\n\t\tfaces_vertices: [[3, 4, 5], [3, 6, 4]],\n\t});\n\texpect(JSON.stringify(result)).toBe(\"[[],[],[],[0,1],[0,1],[0],[1]]\");\n});\n\ntest(\"makeVerticesFacesUnsorted, with vertices, not enough vertices\", () => {\n\tlet error;\n\ttry {\n\t\tear.graph.makeVerticesFacesUnsorted({\n\t\t\tvertices_coords: [],\n\t\t\tfaces_vertices: [[3, 4, 5], [3, 6, 4]],\n\t\t});\n\t} catch (e) {\n\t\terror = e;\n\t}\n\texpect(error).not.toBe(undefined);\n});\n\ntest(\"makeVerticesFacesUnsorted, with vertices_edges\", () => {\n\tconst result = ear.graph.makeVerticesFacesUnsorted({\n\t\tvertices_edges: [[0, 1, 3], [2, 3], [0, 4], [0], [0, 1], [2, 3], [3]],\n\t\tfaces_vertices: [[3, 4, 5], [3, 6, 4]],\n\t});\n\texpect(JSON.stringify(result)).toBe(\"[[],[],[],[0,1],[0,1],[0],[1]]\");\n});\n\ntest(\"vertices faces\", () => {\n\tconst result = ear.graph.makeVerticesFaces({\n\t\tvertices_coords: [[0, 0], [1, 1], [0, 3]],\n\t\tvertices_vertices: [[1, 2], [0, 2], [0, 1]],\n\t\tfaces_vertices: [[0, 1, 2]],\n\t});\n\texpect(JSON.stringify(result)).toBe(\"[[0,null],[null,0],[0,null]]\");\n});\n"
  },
  {
    "path": "tests/graph.make.verticesVertices.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"vertices vertices\", () => {\n\tconst result = ear.graph.makeVerticesVertices({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t});\n\texpect(result[0]).toEqual(expect.arrayContaining([1, 2, 3]));\n\texpect(result[1]).toEqual(expect.arrayContaining([2, 0]));\n\texpect(result[2]).toEqual(expect.arrayContaining([3, 0, 1]));\n\texpect(result[3]).toEqual(expect.arrayContaining([2, 0]));\n});\n\ntest(\"vertices vertices, circle, starting at +X\", () => {\n\tconst result = ear.graph.makeVerticesVertices({\n\t\tvertices_coords: Array.from(Array(12))\n\t\t\t.map((_, i) => i / 12)\n\t\t\t.map(t => [\n\t\t\t\tMath.cos(t * Math.PI * 2),\n\t\t\t\tMath.sin(t * Math.PI * 2),\n\t\t\t]).concat([[0, 0]]),\n\t\tedges_vertices: Array.from(Array(12))\n\t\t\t.map((_, i) => [i, 12]),\n\t});\n\tresult[12].forEach((n, i) => expect(n).toBe(i));\n});\n\ntest(\"vertices vertices, circle, starting at -X\", () => {\n\tconst result = ear.graph.makeVerticesVertices({\n\t\tvertices_coords: Array.from(Array(12))\n\t\t\t.map((_, i) => i / 12)\n\t\t\t.map(t => [\n\t\t\t\tMath.cos(t * Math.PI * 2 + Math.PI),\n\t\t\t\tMath.sin(t * Math.PI * 2 + Math.PI),\n\t\t\t]).concat([[0, 0]]),\n\t\tedges_vertices: Array.from(Array(12))\n\t\t\t.map((_, i) => [i, 12]),\n\t});\n\tresult[12].forEach((n, i) => expect(n).toBe((i + 6) % 12));\n});\n"
  },
  {
    "path": "tests/graph.maps.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst objMatch = (a, b) => expect(JSON.stringify(a)).toBe(JSON.stringify(b));\n\nconst arrMatch = (a, b) => {\n\ta.forEach((_, i) => expect(a[i] === b[i]).toBe(true));\n\texpect(a.length).toBe(b.length);\n};\n\ntest(\"merge simple next map\", () => {\n\tconst map1 = [0, 1, 2, 2, 3, 1, 4, 5];\n\tconst map2 = [0, 1, 1, 2, 0, 3];\n\tconst res = ear.graph.mergeFlatNextmaps(map1, map2);\n\tobjMatch(res, [0, 1, 1, 1, 2, 1, 0, 3]);\n});\n\ntest(\"merge nextmap\", () => {\n\tconst map1 = [[0, 1, 2], [3, 4, 5, 6], [7, 8], [9, 10]];\n\tconst map2 = [0, 1, 2, 3, [4, 5, 6, 7], 8, 9, 10, 11, 12, 13];\n\tconst res = ear.graph.mergeNextmaps(map1, map2);\n\tobjMatch(res, [[0, 1, 2], [3, 4, 5, 6, 7, 8, 9], [10, 11], [12, 13]]);\n});\n\ntest(\"merge simple backmap\", () => {\n\tconst map1 = [0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3];\n\tconst map2 = [0, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 8, 9, 10];\n\tconst res = ear.graph.mergeFlatBackmaps(map1, map2);\n\tobjMatch(res, [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3]);\n});\n\ntest(\"merge backmap\", () => {\n\tconst map1 = [0, [1, 5], [2, 3], 4, 6, 7];\n\tconst map2 = [[0, 4], 1, [2, 5], 3];\n\tconst res = ear.graph.mergeBackmaps(map1, map2);\n\tobjMatch(res, [[0, 6], [1, 5], [2, 3, 7], [4]]);\n});\n\ntest(\"merge backmap no nested\", () => {\n\tconst map1 = [0, 1, 2, 3, 4, 5];\n\tconst map2 = [0, 1, 2, 3, 4, 5];\n\tconst res = ear.graph.mergeBackmaps(map1, map2);\n\texpect(res).toMatchObject([[0], [1], [2], [3], [4], [5]]);\n});\n\ntest(\"merge nextmap no nested\", () => {\n\tconst map1 = [0, 1, 2, 3, 4, 5];\n\tconst map2 = [0, 1, 2, 3, 4, 5];\n\tconst res = ear.graph.mergeNextmaps(map1, map2);\n\texpect(res).toMatchObject([[0], [1], [2], [3], [4], [5]]);\n});\n\ntest(\"merge with undefineds\", () => {\n\t// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n\tconst map1 = [0, 1, 2, null, 3, 4, null, null, 5, 6];\n\tconst map2 = [0, null, 1, 2, 3, 4, null];\n\t// const expected = [0, null, 1, null, 2, 3, null, null, 4, null];\n\tconst expected = [0, null, 1, undefined, 2, 3, undefined, undefined, 4, null];\n\tconst res = ear.graph.mergeFlatNextmaps(map1, map2);\n\tarrMatch(res, expected);\n});\n\ntest(\"inverse map\", () => {\n\t// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n\tconst map1 = [0, 1, 2, null, 3, 4, null, null, 5, 6];\n\tconst map2 = [0, null, 1, 2, 3, 4, null];\n\tconst res = ear.graph.invertFlatMap(ear.graph.mergeFlatNextmaps(map1, map2));\n\tconst expected = [0, 2, 4, 5, 8];\n\tarrMatch(res, expected);\n});\n"
  },
  {
    "path": "tests/graph.nearest.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"facesContainingPoint\", () => {\n\n});\n\ntest(\"faceContainingPoint\", () => {\n\n});\n\ntest(\"nearestVertex\", () => {\n\n});\n\ntest(\"nearestEdge\", () => {\n\n});\n\ntest(\"nearestFace\", () => {\n\n});\n\ntest(\"nearest\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.normalize.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"normalize\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.normals.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeFacesNormal crease pattern\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tconst faceNormals = ear.graph.makeFacesNormal(FOLD);\n\tfaceNormals\n\t\t.map(normal => ear.math.dot(normal, [0, 0, 1]))\n\t\t.forEach(dot => expect(dot).toBeCloseTo(1));\n});\n\ntest(\"makeVerticesNormal crease pattern\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n\tconst FOLD = JSON.parse(foldfile);\n\tconst verticesNormal = ear.graph.makeVerticesNormal(FOLD);\n\tverticesNormal\n\t\t.map(normal => ear.math.dot(normal, [0, 0, 1]))\n\t\t.forEach(dot => expect(dot).toBeCloseTo(1));\n});\n\ntest(\"makeFacesNormal flat folded form\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/preliminary-offset-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faceNormals = ear.graph.makeFacesNormal(folded);\n\texpect(ear.math.dot(faceNormals[0], [0, 0, 1])).toBeCloseTo(1);\n\texpect(ear.math.dot(faceNormals[1], [0, 0, -1])).toBeCloseTo(1);\n\texpect(ear.math.dot(faceNormals[2], [0, 0, -1])).toBeCloseTo(1);\n\texpect(ear.math.dot(faceNormals[3], [0, 0, 1])).toBeCloseTo(1);\n\texpect(ear.math.dot(faceNormals[4], [0, 0, 1])).toBeCloseTo(1);\n\texpect(ear.math.dot(faceNormals[5], [0, 0, -1])).toBeCloseTo(1);\n});\n\ntest(\"makeFacesNormal 3d form\", () => {\n\tconst sphere = fs.readFileSync(\"./tests/files/obj/sphere-with-holes.obj\", \"utf-8\");\n\tconst FOLD = ear.convert.objToFold(sphere);\n\tconst facesNormals = ear.graph.makeFacesNormal(FOLD);\n\tconst m1 = facesNormals.filter(normal => ear.math.dot(normal, [1, 0, 0]) > 0.999);\n\tconst m2 = facesNormals.filter(normal => ear.math.dot(normal, [-1, 0, 0]) > 0.999);\n\tconst m3 = facesNormals.filter(normal => ear.math.dot(normal, [0, 1, 0]) > 0.999);\n\tconst m4 = facesNormals.filter(normal => ear.math.dot(normal, [0, -1, 0]) > 0.999);\n\tconst m5 = facesNormals.filter(normal => ear.math.dot(normal, [0, 0, 1]) > 0.999);\n\tconst m6 = facesNormals.filter(normal => ear.math.dot(normal, [0, 0, -1]) > 0.999);\n\texpect(m1.length).toBe(0);\n\texpect(m2.length).toBe(0);\n\texpect(m3.length).toBe(0);\n\texpect(m4.length).toBe(0);\n\texpect(m5.length).toBe(0);\n\texpect(m6.length).toBe(0);\n});\n\ntest(\"makeVerticesNormal 3d form\", () => {\n\tconst sphere = fs.readFileSync(\"./tests/files/obj/sphere-with-holes.obj\", \"utf-8\");\n\tconst FOLD = ear.convert.objToFold(sphere);\n\tconst verticesNormal = ear.graph.makeVerticesNormal(FOLD);\n\n\tconst m1 = verticesNormal.filter(normal => ear.math.dot(normal, [0, 1, 0]) > 0.999);\n\tconst m2 = verticesNormal.filter(normal => ear.math.dot(normal, [0, -1, 0]) > 0.999);\n\tconst m3 = verticesNormal.filter(normal => ear.math.dot(normal, [0, 0, -1]) > 0.999);\n\texpect(m1.length).toBe(1);\n\texpect(m2.length).toBe(1);\n\texpect(m3.length).toBe(1);\n});\n\ntest(\"normals 3D many planes\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faces_normal = ear.graph.makeFacesNormal(folded);\n\tconst normals = [\n\t\t[0, 0, 1],\n\t\t[0, 0, -1],\n\t\t[0, 0, -1],\n\t\t[0, 0, 1],\n\t\t[0.9238795325112866, -0.38268343236509034, 0],\n\t\t[-0.9238795325112866, 0.38268343236509034, 0],\n\t\t[0.9238795325112863, 0.3826834323650909, 0],\n\t\t[-0.923879532511287, -0.38268343236508917, 0],\n\t\t[0, 0, -1],\n\t\t[0, 0, -1],\n\t\t[0, -1, 0],\n\t\t[0, 1, 0],\n\t\t[0, 1, 0],\n\t\t[0, 0, 1],\n\t\t[0, 0, 1],\n\t\t[0, -1, 0],\n\t];\n\tnormals\n\t\t.forEach((normal, i) => normal\n\t\t\t.forEach((n, j) => expect(faces_normal[i][j]).toBeCloseTo(n)));\n});\n"
  },
  {
    "path": "tests/graph.orders.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"coplanar faces groups\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst foldObject = JSON.parse(foldFile);\n\tconst graph = ear.graph.getFramesByClassName(foldObject, \"foldedForm\")[0];\n\tgraph.faceOrders = ear.layer.layer3D(graph).faceOrders();\n\tconst faces_nudge = ear.graph.nudgeFacesWithFaceOrders(graph);\n\tfs.writeFileSync(`./tests/tmp/faces_nudge.json`, JSON.stringify(faces_nudge, null, 2));\n});\n\ntest(\"linear order face orders\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faces_normal = ear.graph.makeFacesNormal(folded);\n\tconst set1 = ear.graph.linearizeFaceOrders({\n\t\tfaceOrders: [[0, 1, 1], [9, 14, 1], [1, 14, 1]],\n\t\tfaces_normal,\n\t});\n\tconst set2 = ear.graph.linearizeFaceOrders({\n\t\tfaceOrders: [[2, 3, 1], [2, 13, 1], [8, 13, 1]],\n\t\tfaces_normal,\n\t});\n\texpect(JSON.stringify(set1)).toBe(JSON.stringify([0, 14, 1, 9]));\n\texpect(JSON.stringify(set2)).toBe(JSON.stringify([2, 3, 8, 13]));\n});\n\ntest(\"testing the inside of the linearizeFaceOrders method\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faces_normal = ear.graph.makeFacesNormal(folded);\n\tconst faceOrders1 = [[0, 1, 1], [9, 14, 1], [1, 14, 1]];\n\tconst faceOrders2 = [[2, 3, 1], [2, 13, 1], [8, 13, 1]];\n\tconst faces1 = ear.general.uniqueSortedNumbers(faceOrders1\n\t\t.flatMap(order => [order[0], order[1]]));\n\tconst faces2 = ear.general.uniqueSortedNumbers(faceOrders2\n\t\t.flatMap(order => [order[0], order[1]]));\n\t// the first face's normal will determine the linearization direction.\n\tconst normal1 = faces_normal[faces1[0]];\n\tconst normal2 = faces_normal[faces2[0]];\n\tconst facesNormalMatch1 = [];\n\tfaces1.forEach(f => {\n\t\tfacesNormalMatch1[f] = ear.math.dot(faces_normal[f], normal1) > 0;\n\t});\n\tconst facesNormalMatch2 = [];\n\tfaces2.forEach(f => {\n\t\tfacesNormalMatch2[f] = ear.math.dot(faces_normal[f], normal2) > 0;\n\t});\n\t// this pair states face [0] is above face [1]. according to the +1 -1 order,\n\t// and whether or not the reference face [1] normal is flipped. (xor either)\n\tconst directedEdges1 = faceOrders1\n\t\t.map(order => ((order[2] === -1) ^ (!facesNormalMatch1[order[1]])\n\t\t\t? [order[0], order[1]]\n\t\t\t: [order[1], order[0]]));\n\tconst directedEdges2 = faceOrders2\n\t\t.map(order => ((order[2] === -1) ^ (!facesNormalMatch2[order[1]])\n\t\t\t? [order[0], order[1]]\n\t\t\t: [order[1], order[0]]));\n\texpect(facesNormalMatch1[0]).toBe(true);\n\texpect(facesNormalMatch1[1]).toBe(false);\n\texpect(facesNormalMatch1[9]).toBe(false);\n\texpect(facesNormalMatch1[14]).toBe(true);\n\texpect(facesNormalMatch2[2]).toBe(true);\n\texpect(facesNormalMatch2[3]).toBe(false);\n\texpect(facesNormalMatch2[8]).toBe(true);\n\texpect(facesNormalMatch2[13]).toBe(false);\n\texpect(JSON.stringify(directedEdges1))\n\t\t.toBe(JSON.stringify([[0, 1], [14, 9], [14, 1]]));\n\texpect(JSON.stringify(directedEdges2))\n\t\t.toBe(JSON.stringify([[2, 3], [2, 13], [8, 13]]));\n});\n\ntest(\"inside of nudgeFacesWithFaceOrders\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faceOrders = [\n\t\t[0, 1, 1], [2, 3, 1], [4, 5, 1], [6, 7, 1], [10, 11, 1],\n\t\t[2, 13, 1], [8, 13, 1], [9, 14, 1], [1, 14, 1], [12, 15, 1],\n\t];\n\tconst faces_set = ear.graph.connectedComponents(ear.graph.makeVerticesVerticesUnsorted({\n\t\tedges_vertices: faceOrders.map(ord => [ord[0], ord[1]]),\n\t}));\n\tconst sets_faces = ear.graph.invertFlatToArrayMap(faces_set);\n\tconst faces_normal = ear.graph.makeFacesNormal(folded);\n\tconst sets_layers_face = sets_faces\n\t\t.map(faces => ear.graph.faceOrdersSubset(faceOrders, faces))\n\t\t.map(orders => ear.graph.linearizeFaceOrders({ faceOrders: orders, faces_normal }));\n\n\texpect(JSON.stringify(faces_set)).toBe(JSON.stringify([\n\t\t0, 0, 1, 1, 2, 2,\n\t\t3, 3, 1, 0, 4, 4,\n\t\t5, 1, 0, 5,\n\t]));\n\tsets_faces.forEach((faces, i) => expect(JSON.stringify(faces))\n\t\t.toBe(JSON.stringify([\n\t\t\t[0, 1, 9, 14], [2, 3, 8, 13], [4, 5], [6, 7], [10, 11], [12, 15],\n\t\t][i])));\n\texpect(JSON.stringify(sets_layers_face)).toBe(JSON.stringify([\n\t\t[0, 14, 1, 9],\n\t\t[2, 3, 8, 13],\n\t\t[4, 5],\n\t\t[6, 7],\n\t\t[10, 11],\n\t\t[12, 15],\n\t]));\n});\n\ntest(\"linear order face orders with cycle\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst faces_normal = ear.graph.makeFacesNormal(folded);\n\tconst linear = ear.graph.linearizeFaceOrders({\n\t\t...folded,\n\t\tfaces_normal,\n\t});\n\t// console.log(linear);\n\t// const set2 = ear.graph.linearizeFaceOrders({\n\t// \tfaceOrders: [[2, 3, 1], [2, 13, 1], [8, 13, 1]],\n\t// \tfaces_normal,\n\t// });\n\t// expect(JSON.stringify(set1)).toBe(JSON.stringify([0, 14, 1, 9]));\n\t// expect(JSON.stringify(set2)).toBe(JSON.stringify([2, 3, 8, 13]));\n});\n"
  },
  {
    "path": "tests/graph.overlap.components.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"edges-faces all data, triangle cut by edge\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [-1, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [1, 3]],\n\t\tedges_assignment: Array.from(\"BBBB\"),\n\t\tfaces_vertices: [[0, 1, 2]],\n\t});\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(graph);\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [], [3], [2],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n\t\t[],\n\t]);\n});\n\ntest(\"edges-faces all data, square cut by edge through vertex\", () => {\n\t// square cut by edge between two of its vertices\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t\tedges_assignment: Array.from(\"BBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2, 3]],\n\t}));\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n\t\t[],\n\t]);\n});\n\ntest(\"edges-faces all data, square cut by edge through vertex\", () => {\n\t// square cut by edge between one of its vertices, through another vertex\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [2, 2]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 4]],\n\t\tedges_assignment: Array.from(\"BBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2, 3]],\n\t}));\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3], [4],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [], [4], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n\t\t[],\n\t]);\n});\n\ntest(\"edges-faces all data, square cut by edge through two vertices\", () => {\n\t// square cut by edge between one of its vertices, through another vertex\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [-1, -1], [2, 2]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5]],\n\t\tedges_assignment: Array.from(\"BBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2, 3]],\n\t}));\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3], [4], [5],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[4], [], [4], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n\t\t[],\n\t]);\n});\n\ntest(\"overlapping components, two identical edges\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 0], [0, 0]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t\tfaces_vertices: [],\n\t});\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(graph);\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0, 3], [1, 2], [1, 2], [0, 3],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([]);\n});\n\ntest(\"overlapping components, two crossing edges\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [0.5, 1], [0.5, -1]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t\tfaces_vertices: [],\n\t});\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(graph);\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [], [], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[1], [0],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([]);\n});\n\ntest(\"overlapping components, two collinear overlapping edges\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [2, 0], [1, 0], [3, 0]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t\tfaces_vertices: [],\n\t});\n\tconst {\n\t\tverticesVertices,\n\t\tverticesEdges,\n\t\tedgesEdges,\n\t\tfacesVertices,\n\t} = ear.graph.getOverlappingComponents(graph);\n\texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n\t\t[0], [1], [2], [3],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n\t\t[], [1], [0], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n\t\t[], [],\n\t]);\n\texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([]);\n});\n\ntest(\"overlapping components, two adjacent faces one point overlap\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [0, 1]],\n\t\tedges_vertices: [[0, 3], [3, 1], [1, 2], [2, 0], [0, 1]],\n\t\tedges_assignment: Array.from(\"BBBBV\"),\n\t\tfaces_vertices: [[0, 2, 1], [0, 1, 3]],\n\t});\n\tconst components = ear.graph.getOverlappingComponents(graph);\n\n\t// console.log(components);\n\t// vertex 2 and 3 are mutually overlapping\n\texpect(components).toMatchObject({\n\t\tverticesVertices: [[true], [, true], [,, true, true], [,, true, true]],\n\t\tverticesEdges: [[], [], [], []],\n\t\tedgesEdges: [[], [], [], [], []],\n\t\tfacesVertices: [[], []],\n\t});\n});\n\ntest(\"overlapping components, two adjacent faces no overlap points, two crossing edges\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [-0.1, 0.9]],\n\t\tedges_vertices: [[0, 3], [3, 1], [1, 2], [2, 0], [0, 1]],\n\t\tedges_assignment: Array.from(\"BBBBV\"),\n\t\tfaces_vertices: [[0, 2, 1], [0, 1, 3]],\n\t});\n\tconst components = ear.graph.getOverlappingComponents(graph);\n\n\t// edge 1 and 3 are mutually overlapping\n\texpect(components).toMatchObject({\n\t\tverticesVertices: [[true], [, true], [,, true], [,,, true]],\n\t\tverticesEdges: [[], [], [], []],\n\t\tedgesEdges: [[], [,,, true], [], [, true], []],\n\t\tfacesVertices: [[], []],\n\t});\n});\n\ntest(\"overlapping components, two separate faces, identical\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [0, 0], [0.5, 0.5], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]],\n\t\tedges_assignment: Array.from(\"BBBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2], [3, 4, 5]],\n\t});\n\tconst components = ear.graph.getOverlappingComponents(graph);\n\n\texpect(components).toMatchObject({\n\t\tverticesVertices: [\n\t\t\t[true,,, true], [, true,,, true], [,, true,,, true],\n\t\t\t[true,,, true], [, true,,, true], [,, true,,, true],\n\t\t],\n\t\tverticesEdges: [[], [], [], [], [], []],\n\t\tedgesEdges: [[], [], [], [], [], []],\n\t\tfacesVertices: [[], []],\n\t});\n});\n\n// test(\"overlapping components, two separate faces, one point in common\", () => {\n// \t// one upside down triangle (point at origin)\n// \t// copy of other triangle, scaled shorter in the Y axis (point at origin)\n// \t// the second triangle's top line cuts through the other triangle's sides\n// \tconst graph = ear.graph.populate({\n// \t\tvertices_coords: [[0, 0], [1, 1], [-1, 1], [0, 0], [1, 0.5], [-1, 0.5]],\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]],\n// \t\tedges_assignment: Array.from(\"BBBBBB\"),\n// \t\tfaces_vertices: [[0, 1, 2], [3, 4, 5]],\n// \t});\n// \tconst components = ear.graph.getOverlappingComponents(graph);\n\n// \t// vertices 0 and 3 are mutually overlapping\n// \t// edge 4 overlaps with both edges 0 and 2\n// \texpect(components).toMatchObject({\n// \t\tverticesVertices: [[,,, true], [], [], [true], [], []],\n// \t\tverticesEdges: [[], [], [], [], [], []],\n// \t\tedgesEdges: [[,,,, true], [], [,,,, true], [], [true,, true], []],\n// \t\tfacesVertices: [[], []],\n// \t});\n// });\n\n// test(\"overlapping components, points inside faces\", () => {\n// \t// one upside down triangle (point at origin)\n// \t// one right side up triangle\n// \tconst graph = ear.graph.populate({\n// \t\tvertices_coords: [[0, 0], [1, 1], [-1, 1], [-1, -1], [1, -1], [0, 0.5]],\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]],\n// \t\tedges_assignment: Array.from(\"BBBBBB\"),\n// \t\tfaces_vertices: [[0, 1, 2], [3, 4, 5]],\n// \t});\n// \tconst components = ear.graph.getOverlappingComponents(graph);\n\n// \t// vertices 0 and 3 are mutually overlapping\n// \t// edge 4 overlaps with both edges 0 and 2\n// \texpect(components).toMatchObject({\n// \t\tverticesVertices: [[], [], [], [], [], []],\n// \t\tverticesEdges: [[], [], [], [], [], []],\n// \t\tedgesEdges: [[,,,, true], [], [,,,,, true], [], [true], [,, true]],\n// \t\tfacesVertices: [[,,,,, true], [true]],\n// \t});\n// });\n\n// test(\"overlapping components kite base\", () => {\n// \tconst cp = ear.graph.kite();\n// \tconst folded = {\n// \t\t...cp,\n// \t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n// \t};\n// \tear.graph.populate(folded);\n\n// \tconst components = ear.graph.getOverlappingComponents(folded);\n\n// \t// vertex 1 and 5 mutually overlap\n// \t// vertex 1 and 5 both overlap edge 8\n// \t// edge 1 overlaps face 1\n// \t// edge 4 overlaps face 2\n// \texpect(components).toMatchObject({\n// \t\tverticesVertices: [\n// \t\t\t[], [,,,,, true], [], [], [], [, true],\n// \t\t],\n// \t\tverticesEdges: [\n// \t\t\t[], [,,,,,,,, true], [], [], [], [,,,,,,,, true],\n// \t\t],\n// \t\tedgesEdges: [\n// \t\t\t[], [], [], [], [], [], [], [], [],\n// \t\t],\n// \t\tfacesVertices: [\n// \t\t\t[], [], [], [],\n// \t\t],\n// \t});\n// });\n\n// test(\"overlapping components bird base\", () => {\n// \tconst cp = ear.graph.bird();\n// \tconst folded = {\n// \t\t...cp,\n// \t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n// \t};\n// \tear.graph.populate(folded);\n\n// \tconst {\n// \t\tverticesVertices,\n// \t\tverticesEdges,\n// \t\tedgesEdges,\n// \t\tfacesVertices,\n// \t} = ear.graph.getOverlappingComponents(folded);\n\n// \t// hopefully this is all okay, I did not manually check these.\n// \t// tentatively it looks okay.\n// \t// vertex 8 is isolated.\n// \t// edges 24 and 25 are the two which get vertex-overlaps inside them\n// \texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n// \t\t[2, 4, 6],\n// \t\t[3, 5, 7, 13, 14],\n// \t\t[0, 4, 6],\n// \t\t[1, 5, 7, 13, 14],\n// \t\t[0, 2, 6],\n// \t\t[1, 3, 7, 13, 14],\n// \t\t[0, 2, 4],\n// \t\t[1, 3, 5, 13, 14],\n// \t\t[],\n// \t\t[10],\n// \t\t[9],\n// \t\t[12],\n// \t\t[11],\n// \t\t[1, 3, 5, 7, 14],\n// \t\t[1, 3, 5, 7, 13],\n// \t]);\n// \texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n// \t\t[], [24, 25], [], [24, 25], [], [24, 25], [],\n// \t\t[24, 25], [], [], [], [], [], [24, 25], [24, 25],\n// \t]);\n// \texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n// \t\t[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [],\n// \t\t[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [],\n// \t]);\n// \texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n// \t\t[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [],\n// \t]);\n// });\n\n// test(\"overlapping components crane\", () => {\n// \tconst json = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(json);\n// \tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n// \tear.graph.populate(folded);\n\n// \tconst {\n// \t\tverticesVertices,\n// \t\tverticesEdges,\n// \t\tedgesEdges,\n// \t\tfacesVertices,\n// \t} = ear.graph.getOverlappingComponents(folded);\n\n// \t// 5, 6, 7, 8 mutually overlap\n// \t// 47, 49, 51, 53, 55 mutually overlap (crane head)\n// \t// 48, 50, 52, 54 mutually overlap (crane head)\n// \texpect(ear.general.lookupArrayToArrayArray(verticesVertices)).toMatchObject([\n// \t\t[],[],[],[],[],[6,7,8],[5,7,8],[5,6,8],[5,6,7],[12,39,45],[13],[],[9,39,45],[10],[],[18,40,42],[19],[],[15,40,42],[16],[],[22,25,27,29,31,32,34,36,38],[21,25,27,29,31,32,34,36,38],[26,28,30],[33,35,37],[21,22,27,29,31,32,34,36,38],[23,28,30],[21,22,25,29,31,32,34,36,38],[23,26,30],[21,22,25,27,31,32,34,36,38],[23,26,28],[21,22,25,27,29,32,34,36,38],[21,22,25,27,29,31,34,36,38],[24,35,37],[21,22,25,27,29,31,32,36,38],[24,33,37],[21,22,25,27,29,31,32,34,38],[24,33,35],[21,22,25,27,29,31,32,34,36],[9,12,45],[15,18,42],[],[15,18,40],[46],[],[9,12,39],[43],[49,51,53,55],[50,52,54],[47,51,53,55],[48,52,54],[47,49,53,55],[48,50,54],[47,49,51,55],[48,50,52],[47,49,51,53]\n// \t]);\n// \t// vertex 4 overlaps edges 16, 78\n// \texpect(ear.general.lookupArrayToArrayArray(verticesEdges)).toMatchObject([\n// \t\t[],[],[],[],[16,78],[16,29,50,67,68,69,70,78],[16,29,50,67,68,69,70,78],[16,29,50,67,68,69,70,78],[16,29,50,67,68,69,70,78],[3,42],[42,44,48,77],[],[3,42],[42,44,48,77],[],[80,86],[28,30,64,80],[],[80,86],[28,30,64,80],[],[22,55,67,69],[22,55,67,69],[],[],[22,55,67,69],[],[22,55,67,69],[],[22,55,67,69],[],[22,55,67,69],[22,55,67,69],[],[22,55,67,69],[],[22,55,67,69],[],[22,55,67,69],[3,42],[80,86],[8,59,66],[80,86],[16,29,50,59,78],[0,59,74],[3,42],[16,29,50,59,78],[],[],[],[],[],[],[],[],[]\n// \t]);\n// \t// edges 14, 39, 41, 47, 51 overlap both 7, 21\n// \texpect(ear.general.lookupArrayToArrayArray(edgesEdges)).toMatchObject([\n// \t\t[7,14,21,39,41,47,51,59,74],[14,39,41,47,51,72],[72],[72],[14,39,41,47,51],[72],[14,39,41,47,51,72],[0,14,39,41,47,51],[9,18,59,66,88,97,102,104,108],[8,88,97,102,104,108],[62],[],[62,88,97,102,104,108],[62],[0,1,4,6,7,21,52,59,73,74,82],[],[59],[],[8,88,97,102,104,108],[],[53,72,76],[0,14,39,41,47,51],[67,69],[],[],[],[],[],[62],[59],[62],[],[],[],[],[],[],[],[],[0,1,4,6,7,21,52,59,73,74,82],[],[0,1,4,6,7,21,52,59,73,74,82],[72],[53,72,76],[72],[53,72,76],[],[0,1,4,6,7,21,52,59,73,74,82],[72],[53,72,76],[59],[0,1,4,6,7,21,52,59,73,74,82],[14,39,41,47,51],[20,43,45,49],[72],[67,69],[90,100,106,110],[62],[88,97,102,104,108],[0,8,14,16,29,39,41,47,50,51,78,88,97,102,104,108],[],[],[10,12,13,28,30,57,64,80,84,86,90,100,106,110],[88,97,102,104,108],[62],[90,100,106,110],[8,88,97,102,104,108],[22,55],[],[22,55],[],[],[1,2,3,5,6,20,42,43,44,45,48,49,54,77],[14,39,41,47,51],[0,14,39,41,47,51],[],[20,43,45,49],[72],[59],[],[62],[88,97,102,104,108],[14,39,41,47,51],[],[62,88,97,102,104,108],[88,97,102,104,108],[62],[90,100,106,110],[8,9,12,18,58,59,63,66,81,84,85],[],[56,62,65,87,98,101,103,107],[],[],[],[],[],[],[8,9,12,18,58,59,63,66,81,84,85],[90,100,106,110],[],[56,62,65,87,98,101,103,107],[90,100,106,110],[8,9,12,18,58,59,63,66,81,84,85],[90,100,106,110],[8,9,12,18,58,59,63,66,81,84,85],[],[56,62,65,87,98,101,103,107],[90,100,106,110],[8,9,12,18,58,59,63,66,81,84,85],[],[56,62,65,87,98,101,103,107],[],[],[]\n// \t]);\n// \t// amazingly this does seem to be correct.\n// \t// faces 29-58 apparently have no vertex overlaps.\n// \t// faces 51-58 are the crane head. no overlaps.\n// \t// face 26, 28 overlap vertex 44, and faces 25, 27 overlap vertex 41\n// \texpect(ear.general.lookupArrayToArrayArray(facesVertices)).toMatchObject([\n// \t\t[4,5,6,7,8,41,43,44,46],[5,6,7,8,10,13,16,19,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],[41],[44],[5,6,7,8,41,43,44,46],[5,6,7,8,41,43,44,46],[16,19,23,26,28,30],[10,13,24,33,35,37],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[9,10,11,12,13,39,44,45],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[15,16,17,18,19,40,41,42],[5,6,7,8],[41],[44],[41],[44],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]\n// \t]);\n// });\n"
  },
  {
    "path": "tests/graph.overlap.edgesEdges.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"edges-edges\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(554);\n});\n"
  },
  {
    "path": "tests/graph.overlap.facesEdges.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"edges-faces, triangle cut by edge\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [-1, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [1, 3]],\n\t\tedges_assignment: Array.from(\"BBBB\"),\n\t\tfaces_vertices: [[0, 1, 2]],\n\t});\n\t// const facesEdges = ear.graph.getFacesEdgesOverlapAllData(graph);\n\t// expect(facesEdges).toMatchObject([\n\t// \t[\n\t// \t\t, // edge 0, already a part of face 0\n\t// \t\t, // edge 1, already a part of face 0\n\t// \t\t, // edge 2, already a part of face 0\n\t// \t\t{ v: [1], e: [2], p: [] }, // edge 3, overlaps vert 1 and crosses edge 2\n\t// \t],\n\t// ]);\n\texpect(ear.graph.getFacesEdgesOverlap(graph)).toMatchObject([[3]]);\n});\n\ntest(\"edges-faces, square cut by edge through vertex\", () => {\n\t// square cut by edge between two of its vertices\n\tconst graph1 = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t\tedges_assignment: Array.from(\"BBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2, 3]],\n\t});\n\t// square cut by edge between one of its vertices, through another vertex\n\tconst graph2 = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [2, 2]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 4]],\n\t\tedges_assignment: Array.from(\"BBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2, 3]],\n\t});\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph1)).toMatchObject([\n\t// \t[\n\t// \t\t,\n\t// \t\t,\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [0, 2], e: [], p: [] },\n\t// \t],\n\t// ]);\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph2)).toMatchObject([\n\t// \t[\n\t// \t\t,\n\t// \t\t,\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [0, 2], e: [], p: [] },\n\t// \t],\n\t// ]);\n\n\texpect(ear.graph.getFacesEdgesOverlap(graph1)).toMatchObject([[4]]);\n\texpect(ear.graph.getFacesEdgesOverlap(graph2)).toMatchObject([[4]]);\n});\n\ntest(\"edges-faces all data, two adjacent faces one point overlap\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [0, 1]],\n\t\tedges_vertices: [[0, 3], [3, 1], [1, 2], [2, 0], [0, 1]],\n\t\tedges_assignment: Array.from(\"BBBBV\"),\n\t\tfaces_vertices: [[0, 2, 1], [0, 1, 3]],\n\t});\n\n\texpect(ear.graph.getFacesEdgesOverlap(graph)).toMatchObject([[], []]);\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph))\n\t// \t.toMatchObject([[], []]);\n});\n\ntest(\"edges-faces all data, two adjacent faces no overlap points, two crossing edges\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [-0.1, 0.9]],\n\t\tedges_vertices: [[0, 3], [3, 1], [1, 2], [2, 0], [0, 1]],\n\t\tedges_assignment: Array.from(\"BBBBV\"),\n\t\tfaces_vertices: [[0, 2, 1], [0, 1, 3]],\n\t});\n\n\texpect(ear.graph.getFacesEdgesOverlap(graph)).toMatchObject([[1], [3]]);\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph)).toMatchObject([\n\t// \t[\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t\t{ v: [1], e: [3], p: [] },\n\t// \t], [\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [1], e: [], p: [] },\n\t// \t\t{ v: [0], e: [1], p: [] },\n\t// \t],\n\t// ]);\n});\n\ntest(\"edges-faces all data, two separate faces, identical\", () => {\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [0, 1], [0, 0], [0.5, 0.5], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]],\n\t\tedges_assignment: Array.from(\"BBBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2], [3, 4, 5]],\n\t});\n\n\texpect(ear.graph.getFacesEdgesOverlap(graph)).toMatchObject([[], []]);\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph))\n\t// \t.toMatchObject([[], []]);\n});\n\ntest(\"edges-faces all data, two separate faces, one point in common\", () => {\n\t// one upside down triangle (point at origin)\n\t// copy of other triangle, scaled shorter in the Y axis (point at origin)\n\t// the second triangle's top line cuts through the other triangle's sides\n\tconst graph = ear.graph.populate({\n\t\tvertices_coords: [[0, 0], [1, 1], [-1, 1], [0, 0], [1, 0.5], [-1, 0.5]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]],\n\t\tedges_assignment: Array.from(\"BBBBBB\"),\n\t\tfaces_vertices: [[0, 1, 2], [3, 4, 5]],\n\t});\n\n\texpect(ear.graph.getFacesEdgesOverlap(graph)).toMatchObject([[4], [0, 2]]);\n\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(graph)).toMatchObject([\n\t// \t[\n\t// \t\t,\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t\t{ v: [], e: [0, 2], p: [] },\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t], [\n\t// \t\t{ v: [3], e: [4], p: [] },\n\t// \t\t,\n\t// \t\t{ v: [3], e: [4], p: [] },\n\t// \t],\n\t// ]);\n});\n\ntest(\"edges-faces kite base\", () => {\n\tconst cp = ear.graph.kite();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getFacesEdgesOverlap(folded))\n\t\t.toMatchObject([[], [1], [4], []]);\n\n\t// // edge 1 overlaps face 1\n\t// // edge 4 overlaps face 2\n\t// expect(ear.graph.getFacesEdgesOverlapAllData(folded)).toMatchObject([\n\t// \t[\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [2], e: [], p: [] },\n\t// \t\t,\n\t// \t\t{ v: [1], e: [], p: [] },\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t],\n\t// \t[\n\t// \t\t{ v: [0], e: [8], p: [] }, // edge 8 contains vertex 0. will not overlap.\n\t// \t\t{ v: [2], e: [8], p: [] }, // face 1 and edge 1 should overlap\n\t// \t\t,\n\t// \t\t{ v: [3], e: [], p: [] },\n\t// \t\t{ v: [], e: [8], p: [] },\n\t// \t\t{ v: [0], e: [8], p: [] },\n\t// \t\t,\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t],\n\t// \t[\n\t// \t\t{ v: [0], e: [8], p: [] },\n\t// \t\t{ v: [], e: [8], p: [] },\n\t// \t\t{ v: [3], e: [], p: [] },\n\t// \t\t,\n\t// \t\t{ v: [4], e: [8], p: [] }, // face 2 and edge 4 should overlap\n\t// \t\t{ v: [0], e: [8], p: [] },\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t],\n\t// \t[\n\t// \t\t,\n\t// \t\t{ v: [5], e: [], p: [] },\n\t// \t\t,\n\t// \t\t{ v: [4], e: [], p: [] },\n\t// \t\t,\n\t// \t\t,\n\t// \t\t{ v: [0], e: [], p: [] },\n\t// \t],\n\t// ]);\n});\n\ntest(\"edges-faces, bird base\", () => {\n\tconst cp = ear.graph.bird();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getFacesEdgesOverlap(folded)).toMatchObject([\n\t\t[], [], [], [], [], [], [16, 18, 28, 32], [16, 18, 28, 32],\n\t\t[], [], [], [], [], [], [20, 22, 29, 33], [20, 22, 29, 33],\n\t\t[], [], [], []\n\t]);\n});\n\ntest(\"edges-faces kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getFacesEdgesOverlap(folded)).toMatchObject([\n\t\t[6, 7, 8, 12, 13, 15, 16, 18, 19, 22, 23, 31, 32],\n\t\t[6, 7, 8, 12, 13, 15, 16, 18, 19, 22, 23, 31, 32],\n\t\t[6, 7, 8, 12, 13, 15, 16, 18, 19, 22, 23, 31, 32],\n\t\t[6, 7, 12, 13, 15, 16, 18, 19, 22, 23, 31, 32],\n\t\t[6, 7, 12, 13, 15, 16, 18, 19, 22, 23],\n\t\t[6, 7, 8, 12, 13, 15, 16, 18, 19],\n\t\t[8, 12, 22, 31],\n\t\t[8, 16, 23, 32],\n\t\t[6, 8, 12, 18, 22, 31],\n\t\t[7, 8, 16, 19, 23, 32],\n\t\t[6, 8, 12, 18, 22, 31],\n\t\t[7, 8, 16, 19, 23, 32],\n\t\t[8, 12, 22, 31],\n\t\t[8, 16, 23, 32],\n\t\t[0, 8, 10, 21, 22, 25, 28],\n\t\t[1, 8, 9, 20, 23, 26, 35],\n\t\t[0, 8, 10, 21, 22, 25, 28],\n\t\t[1, 8, 9, 20, 23, 26, 35]\n\t]);\n});\n\ntest(\"edges-faces four flaps\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layer-4-flaps.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getFacesEdgesOverlap(folded)).toMatchObject([\n\t\t[2, 3], // vertical face, overlapped by two horizontal edges\n\t\t[0, 1, 2, 3], // all overlap the center face\n\t\t[2, 3], // vertical face, overlapped by two horizontal edges\n\t\t[0, 1], // horizontal face, overlapped by two vertical edges\n\t\t[0, 1], // horizontal face, overlapped by two vertical edges\n\t]);\n});\n\ntest(\"edges-faces randlett flapping bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\t// manually checked the faces with no overlaps, they seem correct\n\t// they are the four small triangles which have no overlapping edges.\n\texpect(ear.graph.getFacesEdgesOverlap(folded)).toMatchObject([\n\t\t[1, 2, 3, 4, 10, 11, 18, 19, 23, 27, 30, 33],\n\t\t[1, 2, 3, 4, 10, 11, 18, 19, 23, 27, 30, 33],\n\t\t[],\n\t\t[6, 10, 11, 30, 33, 35, 36],\n\t\t[6, 18, 19, 21, 22, 25, 26, 35, 36],\n\t\t[6, 35, 36],\n\t\t[6, 10, 11, 30, 33, 35, 36],\n\t\t[6, 35, 36],\n\t\t[6, 18, 19, 21, 22, 25, 26, 35, 36],\n\t\t[],\n\t\t[20, 24, 29, 32],\n\t\t[20, 24, 29, 32],\n\t\t[18, 19],\n\t\t[18, 19],\n\t\t[18, 19],\n\t\t[18, 19],\n\t\t[7, 28, 31, 43, 48],\n\t\t[7, 28, 31, 43, 48],\n\t\t[8, 44, 47],\n\t\t[8, 44, 47],\n\t\t[8, 44, 47],\n\t\t[8, 44, 47],\n\t\t[],\n\t\t[7, 28, 31, 43, 48],\n\t\t[7, 28, 31, 43, 48],\n\t\t[]\n\t]);\n});\n\ntest(\"edges-faces crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/crane-faces-edges-overlap.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\n\tconst facesEdges = ear.graph.getFacesEdgesOverlap(folded);\n\n\texpect(facesEdges.flat().length).toBe(1167);\n\texpect(facesEdges).toMatchObject(expected);\n});\n\ntest(\"edges-faces kraft bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t}\n\tear.graph.populate(folded);\n\n\tconst start = performance.now();\n\tconst facesEdges = ear.graph.getFacesEdgesOverlap(folded);\n\tconst end = performance.now();\n\tconst elapsed = end - start;\n\t// console.log(`${elapsed} ms. Kraft bird faces-edges overlap`);\n\n\texpect(facesEdges.flat().length).toBe(7544);\n});\n"
  },
  {
    "path": "tests/graph.overlap.facesFaces.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"getFacesFacesOverlap strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject([\n\t\t[1, 4],\n\t\t[0, 2, 4],\n\t\t[1, 3, 5],\n\t\t[2, 6],\n\t\t[0, 1, 5],\n\t\t[2, 4, 6],\n\t\t[3, 5],\n\t]);\n});\n\ntest(\"getFacesFacesOverlap four panel square\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject([\n\t\t[1, 2, 3],\n\t\t[0, 2, 3],\n\t\t[0, 1, 3],\n\t\t[0, 1, 2],\n\t\t[5, 6, 7],\n\t\t[4, 6, 7],\n\t\t[4, 5, 7],\n\t\t[4, 5, 6],\n\t]);\n});\n\ntest(\"getFacesFacesOverlap zig-zag panels\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-zig-zag.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject([\n\t\t[1, 2, 3, 4],\n\t\t[0, 2, 3, 4],\n\t\t[0, 1, 3, 4],\n\t\t[0, 1, 2, 4],\n\t\t[0, 1, 2, 3],\n\t]);\n});\n\ntest(\"getFacesFacesOverlap bird base, faces-faces\", () => {\n\tconst cp = ear.graph.bird();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject([\n\t\t[1,4,5,6,7,8,11],[0,4,5,6,7,8,11],[3,9,10,12,13,14,15],[2,9,10,12,13,14,15],\n\t\t[0,1,5,6,7,8,11],[0,1,4,6,7,8,11],[0,1,4,5,7,8,11,16,19],[0,1,4,5,6,8,11,16,19],\n\t\t[0,1,4,5,6,7,11],[2,3,10,12,13,14,15],[2,3,9,12,13,14,15],[0,1,4,5,6,7,8],\n\t\t[2,3,9,10,13,14,15],[2,3,9,10,12,14,15],[2,3,9,10,12,13,15,17,18],\n\t\t[2,3,9,10,12,13,14,17,18],[6,7,19],[14,15,18],[14,15,17],[6,7,16]\n\t]);\n});\n\ntest(\"edges-faces bird base, edges-edges\", () => {\n\tconst cp = ear.graph.bird();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(194);\n});\n\ntest(\"getFacesFacesOverlap kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/kabuto-faces-faces-overlap.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject(expected);\n});\n\ntest(\"getFacesFacesOverlap crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/crane-faces-faces-overlap.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject(expected);\n});\n\ntest(\"getFacesFacesOverlap Kraft bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst cp = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/kraft-bird-faces-faces-overlap.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\n\tconst result = ear.graph.getFacesFacesOverlap(folded);\n\tresult.forEach(arr => arr.sort((a, b) => a - b));\n\texpect(result).toMatchObject(expected);\n});\n"
  },
  {
    "path": "tests/graph.planarize.collinearEdges.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"planarize, overlapping assignments\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/overlapping-assignments.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t\t// edges_line,\n\t\t\t// lines,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// # of vertices does not change\n\texpect(cp.vertices_coords).toHaveLength(20);\n\texpect(result.vertices_coords).toHaveLength(20);\n\n\t// # of edges increases\n\texpect(cp.edges_vertices).toHaveLength(18);\n\texpect(result.edges_vertices).toHaveLength(23);\n\n\texpect(vertices.map).toMatchObject([\n\t\t6, 13, 12, 5, 8, 15, 14, 4, 9, 17, 16, 3, 10, 19, 18, 1, 0, 2, 11, 7,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[12], [13], [11], [5], [15], [18], [14], [4, 5, 6, 7], [17], [21], [16],\n\t\t[3, 4, 5, 6, 7, 8], [20], [22], [19], [1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1],\n\t\t[7, 8, 9, 10],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-overlapping-assignments.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, crane\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t\t// edges_line,\n\t\t\t// lines,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// this graph is already planar. neither # of vertices nor edges change\n\texpect(cp.vertices_coords).toHaveLength(56);\n\texpect(result.vertices_coords).toHaveLength(56);\n\texpect(cp.edges_vertices).toHaveLength(114);\n\texpect(result.edges_vertices).toHaveLength(114);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 4, 54, 13, 50, 51, 3, 9, 49, 43, 22, 23, 14, 15, 7, 44, 24, 25, 16,\n\t\t17, 1, 39, 5, 40, 34, 27, 20, 21, 45, 46, 31, 11, 47, 48, 18, 19, 26,\n\t\t37, 38, 35, 36, 2, 30, 10, 8, 33, 6, 55, 52, 53, 28, 29, 41, 42, 32, 12,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[72], [61], [36], [17], [96], [13], [65], [66], [74], [67], [37], [0], [64],\n\t\t[14], [3], [6], [109], [34], [63], [57], [28], [62], [97], [16], [46], [38],\n\t\t[56], [48], [76], [85], [92], [101], [69], [39], [47], [15], [19], [44],\n\t\t[33], [112], [103], [102], [104], [105], [94], [93], [52], [51], [79], [80],\n\t\t[86], [87], [58], [30], [29], [59], [32], [31], [60], [55], [42], [40], [1],\n\t\t[2], [23], [22], [53], [9], [10], [5], [4], [43], [7], [8], [54], [41], [26],\n\t\t[27], [113], [45], [106], [75], [73], [20], [68], [98], [18], [111], [110],\n\t\t[70], [107], [89], [21], [95], [35], [88], [82], [11], [12], [25], [24],\n\t\t[99], [100], [50], [49], [78], [77], [83], [84], [90], [91], [81], [71], [108]\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-crane.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, non-planar square fish\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-square-fish.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// # of vertices decreases\n\texpect(cp.vertices_coords).toHaveLength(50);\n\texpect(result.vertices_coords).toHaveLength(38);\n\n\t// # of edges decreases\n\texpect(cp.edges_vertices).toHaveLength(26);\n\texpect(result.edges_vertices).toHaveLength(25);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 35, 2, 3, 7, 8, 9, 10, 11, 36, 37, 25, 26, 27, 28, 20, 24, 16,\n\t\t17, 18, 19, 29, 31, 32, 34, 14, 15, 12, 13, 29, 30, 32, 33, 7, 6, 24,\n\t\t23, 20, 21, undefined, undefined, 21, 22, 23, 22, 3, 5, 5, 4,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [22], [23], [1], [2, 3, 4, 5], [6], [7], [24], [16], [17],\n\t\t[12, 13, 14, 15], [10], [11], [18, 19], [20, 21], [9], [8], [18],\n\t\t[20], [5], [15], [12], [13], [14], [2, 3], [3],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-non-planar-square-fish.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, non-planar bird base\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-bird-base.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// # of vertices decreases\n\texpect(cp.vertices_coords).toHaveLength(56);\n\texpect(result.vertices_coords).toHaveLength(48);\n\n\t// # of edges increases\n\texpect(cp.edges_vertices).toHaveLength(30);\n\texpect(result.edges_vertices).toHaveLength(32);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 47, 2, 3, 4, 35, 36, 21, 22, 23, 24, 5, 7, 8, 10, 32, 34, 29, 31,\n\t\t18, 20, 15, 17, 44, 46, 41, 43, 5, 6, 8, 9, 15, 16, 41, 42, 32, 33, 29,\n\t\t30, 44, 45, 18, 19, 25, 28, 37, 40, 13, 14, 11, 12, 26, 27, 38, 39,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [30], [31], [1], [2], [22], [13], [14], [3, 4], [5, 6], [20, 21],\n\t\t[18, 19], [11, 12], [9, 10], [28, 29], [26, 27], [3], [5], [9], [26],\n\t\t[20], [18], [28], [11], [15, 16, 17], [23, 24, 25], [8], [7], [16], [24],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-non-planar-bird-base.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, separated\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/separated-parallel-edges.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// neither the number of vertices nor edges changes\n\texpect(cp.vertices_coords).toHaveLength(10);\n\texpect(result.vertices_coords).toHaveLength(10);\n\texpect(cp.edges_vertices).toHaveLength(11);\n\texpect(result.edges_vertices).toHaveLength(11);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 2, 3, 4, 5, 6, 7, 8, 9,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-separated.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, non-planar-polygons\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-polygons.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t\t// edges_line,\n\t\t\t// lines,\n\t\t},\n\t} = ear.graph.planarizeCollinearEdges(cp);\n\n\t// neither the number of vertices nor edges changes\n\texpect(cp.vertices_coords).toHaveLength(15);\n\texpect(result.vertices_coords).toHaveLength(15);\n\texpect(cp.edges_vertices).toHaveLength(10);\n\texpect(result.edges_vertices).toHaveLength(10);\n\n\texpect(vertices.map).toMatchObject([\n\t\t11, 12, 13, 14, 10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[6], [7], [8], [9], [5], [0], [1], [2], [3], [4],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-non-planar-polygons.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, foldedForm, kabuto\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst { result, changes } = ear.graph.planarizeCollinearEdges(folded);\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-kabuto.fold\", JSON.stringify(result));\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-kabuto.json\", JSON.stringify(changes));\n});\n\ntest(\"planarize, foldedForm, kraft bird\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\tconst { result, changes } = ear.graph.planarizeCollinearEdges(folded);\n\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-kraft-bird.fold\", JSON.stringify(result));\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-kraft-bird.json\", JSON.stringify(changes));\n});\n\ntest(\"planarize, foldedForm, windmill\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst { result, changes } = ear.graph.planarizeCollinearEdges(folded);\n\n\texpect(folded.vertices_coords).toHaveLength(12);\n\texpect(result.vertices_coords).toHaveLength(9);\n\texpect(folded.edges_vertices).toHaveLength(20);\n\texpect(result.edges_vertices).toHaveLength(16);\n\n\texpect(result.vertices_coords).toMatchObject([\n\t\t[1, 1], // square corner 1\n\t\t[3, 3], // square corner 2\n\t\t[1, 3], // square corner 3\n\t\t[3, 1], // square corner 4\n\t\t[0, 2], // tip 1\n\t\t[2, 0], // tip 2\n\t\t[2, 4], // tip 3\n\t\t[2, 2], // center (appears twice)\n\t\t[4, 2], // tip 4\n\t]);\n\n\t// console.log(JSON.stringify(changes.edges.map));\n\n\texpect(result.edges_vertices).toMatchObject([\n\t\t[0, 7], [7, 1], [2, 0], [0, 3], [4, 0], [3, 5], [6, 2], [4, 7],\n\t\t[7, 8], [6, 7], [7, 5], [2, 7], [7, 3], [3, 1], [1, 2], [1, 8],\n\t]);\n\n\texpect(changes.vertices.map).toMatchObject([\n\t\t4, 7, 5, 6, 7, 0, 1, 8, 2, 3, 7, 7,\n\t]);\n\n\texpect(changes.edges.map).toMatchObject([\n\t\t[7], [10], [9], [7], [4], [15], [2], [3], [11], [12],\n\t\t[0], [6], [5], [13], [14], [10], [8], [8], [9], [1],\n\t]);\n\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-windmill.fold\", JSON.stringify(result));\n\tfs.writeFileSync(\"./tests/tmp/planarizeCollinearEdges-windmill.json\", JSON.stringify(changes));\n});\n"
  },
  {
    "path": "tests/graph.planarize.faceMap.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n/**\n * @description Create a face-map relationship between the a graph\n * and the planarized copy of the same graph.\n * IMPORTANT! all faces in the newGraph (planarized) have to be convex\n * otherwise the face center point might not lie inside the polygon.\n * @param {FOLD} oldGraph the input folded form\n * @param {FOLD} newGraph the planarized graph\n * @param {number[][]} nextmap\n * @returns {{ backmap: number[][], bruteForceBackmap: number[][] }}\n */\nconst makeBruteForceFaceMap = (oldGraph, newGraph, nextmap) => {\n\tconst backmap = ear.graph.invertArrayMap(nextmap);\n\tconst faces_centerPlanar = ear.graph.makeFacesCenterQuick(newGraph);\n\tconst faces_polygonFolded = ear.graph.makeFacesPolygon(oldGraph);\n\tconst bruteForceBackmap = faces_centerPlanar\n\t\t.map(center => faces_polygonFolded\n\t\t\t.map((poly, f2) => (ear.math.overlapConvexPolygonPoint(poly, center).overlap\n\t\t\t\t? f2\n\t\t\t\t: undefined))\n\t\t\t.filter(a => a !== undefined));\n\treturn {\n\t\tbackmap,\n\t\tbruteForceBackmap,\n\t}\n}\n\ntest(\"planarize face map, brute force, flapping-bird\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\tresult,\n\t\tchanges: { faces: { map } },\n\t} = ear.graph.planarizeVerbose(folded);\n\n\tconst {\n\t\tbackmap,\n\t\tbruteForceBackmap,\n\t} = makeBruteForceFaceMap(folded, result, map);\n\n\texpect(backmap).toMatchObject(bruteForceBackmap);\n});\n\ntest(\"planarize face map, brute force, kabuto\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\tresult,\n\t\tchanges: { faces: { map } },\n\t} = ear.graph.planarizeVerbose(folded);\n\n\tconst {\n\t\tbackmap,\n\t\tbruteForceBackmap,\n\t} = makeBruteForceFaceMap(folded, result, map);\n\n\texpect(backmap).toMatchObject(bruteForceBackmap);\n});\n\ntest(\"planarize face map, brute force, crane\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\tconst {\n\t\tresult,\n\t\tchanges: { faces: { map } },\n\t} = ear.graph.planarizeVerbose(folded);\n\n\tconst {\n\t\tbackmap,\n\t\tbruteForceBackmap,\n\t} = makeBruteForceFaceMap(folded, result, map);\n\n\texpect(backmap).toMatchObject(bruteForceBackmap);\n});\n\ntest(\"planarize face map, brute force, kraft-bird\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\tconst {\n\t\tresult,\n\t\tchanges: { faces: { map } },\n\t} = ear.graph.planarizeVerbose(folded);\n\n\tconst {\n\t\tbackmap,\n\t\tbruteForceBackmap,\n\t} = makeBruteForceFaceMap(folded, result, map);\n\n\texpect(backmap).toMatchObject(bruteForceBackmap);\n});\n"
  },
  {
    "path": "tests/graph.planarize.intersect.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"intersectAllEdges, a bunch of random lines\", () => {\n\tconst FOLD25 = fs.readFileSync(\"./tests/files/fold/non-planar-25-lines.fold\", \"utf-8\");\n\tconst FOLD50 = fs.readFileSync(\"./tests/files/fold/non-planar-50-lines.fold\", \"utf-8\");\n\tconst FOLD75 = fs.readFileSync(\"./tests/files/fold/non-planar-75-lines.fold\", \"utf-8\");\n\tconst FOLD100 = fs.readFileSync(\"./tests/files/fold/non-planar-100-lines.fold\", \"utf-8\");\n\tconst FOLD50c = fs.readFileSync(\"./tests/files/fold/non-planar-50-chaotic.fold\", \"utf-8\");\n\tconst FOLD100c = fs.readFileSync(\"./tests/files/fold/non-planar-100-chaotic.fold\", \"utf-8\");\n\tconst FOLD500c = fs.readFileSync(\"./tests/files/fold/non-planar-500-chaotic.fold\", \"utf-8\");\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD25))).toHaveLength(150);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD50))).toHaveLength(635);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD75))).toHaveLength(1947);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD100))).toHaveLength(2957);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD50c))).toHaveLength(296);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD100c))).toHaveLength(1032);\n\texpect(ear.graph.intersectAllEdges(JSON.parse(FOLD500c))).toHaveLength(29033);\n});\n"
  },
  {
    "path": "tests/graph.planarize.new.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// test(\"planarize, time test\", () => {\n// \tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-500-random-lines.fold\", \"utf-8\");\n// \tconst graph = JSON.parse(FOLD);\n// \tconsole.time(\"new\");\n// \tear.graph.planarizeVerbose(graph);\n// \tconsole.timeEnd(\"new\");\n// \tconsole.time(\"old\");\n// \tear.graph.planarize(graph);\n// \tconsole.timeEnd(\"old\");\n// });\n\ntest(\"planarize, faces but no edges\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill-no-edges.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { result, changes } = ear.graph.planarizeVerbose(graph);\n\n\texpect(result.vertices_coords).toHaveLength(13);\n\texpect(result.edges_vertices).toHaveLength(24);\n\texpect(result.faces_vertices).toHaveLength(12);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-windmill-no-edges.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-windmill-no-edges.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, flapping bird with line through\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tcp.vertices_coords.push([0.1, 1], [1, 0.1]);\n\tcp.edges_vertices.push([\n\t\tcp.vertices_coords.length - 2,\n\t\tcp.vertices_coords.length - 1,\n\t]);\n\tcp.edges_assignment.push(\"F\");\n\tconst { result, changes } = ear.graph.planarizeVerbose(cp);\n\n\texpect(result.vertices_coords).toHaveLength(32);\n\texpect(result.edges_vertices).toHaveLength(63);\n\texpect(result.faces_vertices).toHaveLength(32);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-flapping-bird.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-flapping-bird.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, non-planar bird base\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-bird-base.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst { result, changes } = ear.graph.planarizeVerbose(cp);\n\n\texpect(result.vertices_coords).toHaveLength(53);\n\texpect(result.edges_vertices).toHaveLength(116);\n\texpect(result.faces_vertices).toHaveLength(64);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-non-planar-bird-base.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-non-planar-bird-base.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, non-planar square fish base\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-square-fish.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst { result, changes } = ear.graph.planarizeVerbose(cp);\n\n\texpect(result.vertices_coords).toHaveLength(35);\n\texpect(result.edges_vertices).toHaveLength(76);\n\texpect(result.faces_vertices).toHaveLength(42);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-non-planar-square-fish.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-non-planar-square-fish.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, non-planar 50 random lines\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-50-chaotic.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { result } = ear.graph.planarizeVerbose(graph);\n\n\texpect(result.vertices_coords).toHaveLength(396);\n\texpect(result.edges_vertices).toHaveLength(642);\n\texpect(result.faces_vertices).toHaveLength(249);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-new-non-planar-50-random-chaotic.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, non-planar 100 random lines\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-100-chaotic.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { result } = ear.graph.planarizeVerbose(graph, 1e-4);\n\n\texpect(result.vertices_coords).toHaveLength(1224);\n\texpect(result.edges_vertices).toHaveLength(2151);\n\texpect(result.faces_vertices).toHaveLength(929);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-new-non-planar-100-random-chaotic.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, kraft bird base\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { result, changes } = ear.graph.planarizeVerbose(graph);\n\n\texpect(result.vertices_coords).toHaveLength(245);\n\texpect(result.edges_vertices).toHaveLength(572);\n\texpect(result.faces_vertices).toHaveLength(328);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-kraft-bird-base.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-kraft-bird-base.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, crane already planar\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst { result, changes } = ear.graph.planarizeVerbose(graph);\n\n\texpect(result.vertices_coords).toHaveLength(56);\n\texpect(result.edges_vertices).toHaveLength(114);\n\texpect(result.faces_vertices).toHaveLength(59);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-crane.fold\",\n\t\tJSON.stringify(result),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-crane.json\",\n\t\tJSON.stringify(changes),\n\t);\n});\n\ntest(\"planarize, foldedForm, windmill\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst { result, changes } = ear.graph.planarizeVerbose(folded);\n\n\texpect(changes.vertices.map).toMatchObject([\n\t\t[4], [7], [5], [6], [7], [0], [1], [8], [2], [3], [7], [7],\n\t]);\n\n\t// 8 edges with just one mapping. checks out.\n\texpect(changes.edges.map).toMatchObject([\n\t\t[9, 10], [15, 16], [13, 14], [9, 10],\n\t\t[6], [23],\n\t\t[2, 3], [4, 5],\n\t\t[17], [18], [0], [8], [7],\n\t\t[19, 20], [21, 22], [15, 16], [11, 12], [11, 12], [13, 14],\n\t\t[1],\n\t]);\n\n\t// in the new graph, faces:\n\t// 2, 5, 6, 11 are the outer triangles\n\t// 0, 1, 3, 4, 7, 8, 9, 10 are the eight triangles in the inner square\n\t// 1, 2, 5, 8 are the small triangles (5 top, 1 left, 2 bottom, 8 right)\n\t// 0, 3, 4, 7 are the parallelograms (0 bottom, 4 left, 7 top, 3 right)\n\n\t// in the old graph, faces:\n\t// 6 is the center square\n\t// 4, 5 go to the top triangle\n\t// 2, 3 bottom triangle\n\t// 0, 1 left triangle\n\t// 7, 8 right triangle\n\t// additionally, all faces other than 6 go into the center square somewhere\n\n\t// did this by hand\n\texpect(changes.faces.map).toMatchObject([\n\t\t[0, 1, 2, 10],\n\t\t[1, 2],\n\t\t[10, 11],\n\t\t[4, 9, 10, 11],\n\t\t[1, 6, 7, 8],\n\t\t[6, 8],\n\t\t[0, 1, 3, 4, 7, 8, 9, 10],\n\t\t[3, 4, 5, 8],\n\t\t[4, 5],\n\t])\n\n\tfs.writeFileSync(\"./tests/tmp/planarize-windmill.fold\", JSON.stringify(result));\n\tfs.writeFileSync(\"./tests/tmp/planarize-windmill.json\", JSON.stringify(changes));\n});\n"
  },
  {
    "path": "tests/graph.planarize.test.js",
    "content": "import fs from \"fs\";\nimport xmldom from \"@xmldom/xmldom\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"planarizeEdges, empty graph\", () => {\n\tconst graph = {\n\t\tvertices_coords: [],\n\t\tedges_vertices: [],\n\t\tedges_assignment: [],\n\t\tedges_foldAngle: [],\n\t};\n\tconst result = ear.graph.planarizeEdges(graph);\n\texpect(JSON.stringify(graph)).toBe(JSON.stringify(result));\n});\n\ntest(\"planarize, empty graph\", () => {\n\tconst graph = {\n\t\tvertices_coords: [],\n\t\tedges_vertices: [],\n\t\tedges_assignment: [],\n\t\tedges_foldAngle: [],\n\t};\n\tconst result = ear.graph.planarize(graph);\n\tconst expected = {\n\t\tvertices_coords: [],\n\t\tedges_vertices: [],\n\t\tedges_assignment: [],\n\t\tedges_foldAngle: [],\n\t\tfaces_vertices: [],\n\t\tfaces_edges: [],\n\t}\n\texpect(JSON.stringify(expected)).toBe(JSON.stringify(result));\n});\n\ntest(\"planarize random lines\", () => {\n\tconst EDGES = 400;\n\tconst graph = {\n\t\tvertices_coords: Array.from(Array(EDGES * 2))\n\t\t\t.map(() => [Math.random(), Math.random()]),\n\t\tedges_vertices: Array.from(Array(EDGES))\n\t\t\t.map((_, i) => [i * 2, i * 2 + 1]),\n\t};\n\tear.graph.planarize(JSON.parse(JSON.stringify(graph)));\n});\n\ntest(\"planarize, random lines with collinear end points\", () => {\n\tconst graph = ear.graph.square();\n\tfor (let i = 0; i < 4; i += 1) {\n\t\tgraph.vertices_coords.push([Math.random(), 0], [Math.random(), 1]);\n\t\tgraph.edges_vertices.push([\n\t\t\tgraph.vertices_coords.length - 2,\n\t\t\tgraph.vertices_coords.length - 1,\n\t\t]);\n\t\tgraph.edges_assignment.push(\"V\");\n\t\tgraph.edges_foldAngle.push(90);\n\t}\n\tfor (let i = 0; i < 4; i += 1) {\n\t\tgraph.vertices_coords.push([0, Math.random()], [1, Math.random()]);\n\t\tgraph.edges_vertices.push([\n\t\t\tgraph.vertices_coords.length - 2,\n\t\t\tgraph.vertices_coords.length - 1,\n\t\t]);\n\t\tgraph.edges_assignment.push(\"M\");\n\t\tgraph.edges_foldAngle.push(-90);\n\t}\n\tconst planar = ear.graph.planarize(graph);\n\tfs.writeFileSync(\"./tests/tmp/planar-square.fold\", JSON.stringify(planar, null, 2), \"utf8\");\n});\n\ntest(\"planarize, bird base with duplicate vertices\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/bird-disjoint-edges.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst result = ear.graph.planarize(graph);\n\tfs.writeFileSync(\"./tests/tmp/bird-base-planarized.fold\", JSON.stringify(result, null, 2), \"utf8\");\n\texpect(true).toBe(true);\n});\n\ntest(\"planarize, svg import\", () => {\n\tconst svg = fs.readFileSync(\"./tests/files/svg/maze-8x8.svg\", \"utf-8\");\n\tconst graph = ear.convert.svgEdgeGraph(svg);\n\tconst result = ear.graph.planarize(graph);\n\tfs.writeFileSync(\"./tests/tmp/svg-to-fold-maze.fold\", JSON.stringify(result, null, 2), \"utf8\");\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/svg-to-fold-to-svg-maze-8x8.svg\",\n\t\tear.convert.foldToSvg(result, { string: true }),\n\t\t\"utf8\",\n\t);\n\texpect(true).toBe(true);\n});\n\ntest(\"planarize, overlapping edge assignments\", () => {\n\tconst svg = `<svg>\n\t\t<line x1=\"0\" y1=\"9\" x2=\"0\" y2=\"7.5\" stroke=\"black\" />\n\t\t<rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" stroke=\"purple\" />\n\t\t<rect x=\"0\" y=\"1\" width=\"6\" height=\"6\" stroke=\"red\" />\n\t\t<rect x=\"0\" y=\"2\" width=\"4\" height=\"4\" stroke=\"green\" />\n\t\t<rect x=\"0\" y=\"3\" width=\"2\" height=\"2\" stroke=\"blue\" />\n\t\t<line x1=\"0\" y1=\"-1\" x2=\"0\" y2=\"2.5\" stroke=\"black\" />\n\t</svg>`;\n\tconst graph = ear.convert.svgEdgeGraph(svg);\n\tconst result = ear.graph.planarize(graph);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarize-overlapping-edges.fold\",\n\t\tJSON.stringify(result, null, 2),\n\t\t\"utf8\",\n\t);\n});\n\ntest(\"planarize 2 lines, x formation\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[1, 1], [9, 2], [2, 9], [11, 10]],\n\t\tedges_vertices: [[0, 3], [1, 2]],\n\t\tedges_assignment: [\"M\", \"V\"],\n\t};\n\tconst planar = ear.graph.planarize(graph);\n\texpect(planar.vertices_coords.length).toBe(5);\n});\n\ntest(\"planarize two loops 'x-' x with a horizontal dash from its center\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [0, 1], [1, 1], [0.5, 0.5], [2, 0.5]],\n\t\tedges_vertices: [[0, 3], [1, 2], [4, 5]],\n\t\tedges_assignment: [\"M\", \"V\", \"F\"],\n\t};\n\tconst planar = ear.graph.planarize(graph);\n\texpect(JSON.stringify(planar.edges_assignment))\n\t\t.toBe(JSON.stringify([\"M\", \"M\", \"F\", \"V\", \"V\"]));\n\texpect(planar.vertices_coords.length).toBe(6);\n});\n\ntest(\"planarize dup verts\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [0, 0], [0, 1], [0, 0], [-1, 0], [0, 0], [0, -1]],\n\t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7]],\n\t\tedges_assignment: [\"M\", \"V\", \"F\", \"B\"],\n\t};\n\tconst planar = ear.graph.planarize(graph);\n\texpect(planar.vertices_coords.length).toBe(5);\n});\n\ntest(\"planarize, one edges crossing boundary, more assignments than fold angles\", () => {\n\tconst graph = ear.graph.square();\n\tgraph.vertices_coords.push([-0.1, 0.3], [1.1, 0.9]);\n\tgraph.edges_vertices.push([4, 5]);\n\tgraph.edges_assignment.push(\"V\");\n\n\tconst planar = ear.graph.planarize(graph);\n\n\texpect(planar.vertices_coords.length).toBe(8);\n\texpect(planar.edges_vertices.length).toBe(9);\n\texpect(planar.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(6);\n\texpect(planar.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(3);\n\texpect(planar.edges_foldAngle.filter(a => a === 0).length).toBe(6);\n\texpect(planar.edges_foldAngle.filter(a => a === 180).length).toBe(0);\n\texpect(planar.edges_foldAngle.filter(a => a === undefined).length).toBe(3);\n});\n\ntest(\"planarize, graphs with bad arrays\", () => {\n\tconst {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t} = ear.graph.square();\n\tconst graph1 = {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t\tedges_assignment,\n\t\tedges_foldAngle,\n\t\tfaces_vertices,\n\t};\n\tconst graph2 = ear.graph.square();\n\tgraph1.vertices_coords.push([-0.1, 0.3], [1.1, 0.9]);\n\tgraph1.edges_vertices.push([4, 5]);\n\tgraph1.edges_assignment.push(\"V\");\n\tgraph2.vertices_coords.push([-0.1, 0.3], [1.1, 0.9]);\n\tgraph2.edges_vertices.push([4, 5]);\n\tgraph2.edges_assignment.push(\"V\");\n\n\texpect(ear.graph.planarize(graph1)).toMatchObject(ear.graph.planarize(graph2));\n})\n\ntest(\"planarize, two crossing edges, more assignments than fold angles\", () => {\n\tconst graph = ear.graph.square();\n\tgraph.vertices_coords.push([-0.1, 0.3], [1.1, 0.9]);\n\tgraph.vertices_coords.push([0.2, -0.1], [0.8, 1.1]);\n\tgraph.edges_vertices.push([4, 5]);\n\tgraph.edges_vertices.push([6, 7]);\n\tgraph.edges_assignment.push(\"V\");\n\tgraph.edges_assignment.push(\"M\");\n\n\tconst planar = ear.graph.planarize(graph);\n\n\texpect(planar.vertices_coords.length).toBe(13);\n\texpect(planar.edges_vertices.length).toBe(16);\n\texpect(planar.edges_assignment.filter(a => a === \"B\" || a === \"b\").length).toBe(8);\n\texpect(planar.edges_assignment.filter(a => a === \"V\" || a === \"v\").length).toBe(4);\n\texpect(planar.edges_assignment.filter(a => a === \"M\" || a === \"m\").length).toBe(4);\n\texpect(planar.edges_foldAngle.filter(a => a === 0).length).toBe(8);\n\texpect(planar.edges_foldAngle.filter(a => a === 180).length).toBe(0);\n\texpect(planar.edges_foldAngle.filter(a => a === -180).length).toBe(0);\n\texpect(planar.edges_foldAngle.filter(a => a === undefined).length).toBe(8);\n});\n\ntest(\"planarize 2 lines, collinear\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[1, 1], [3, 3], [5, 5], [7, 7]],\n\t\tedges_vertices: [[0, 3], [1, 2]],\n\t\tedges_assignment: [\"M\", \"V\"],\n\t};\n\tconst planar = ear.graph.planarize(graph);\n\texpect(planar.vertices_coords.length).toBe(2);\n\texpect(planar.edges_vertices.length).toBe(1);\n\texpect(planar.vertices_coords[0][0]).toBeCloseTo(1);\n\texpect(planar.vertices_coords[0][1]).toBeCloseTo(1);\n\texpect(planar.vertices_coords[1][0]).toBeCloseTo(7);\n\texpect(planar.vertices_coords[1][1]).toBeCloseTo(7);\n});\n"
  },
  {
    "path": "tests/graph.planarizeOverlap.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"planarizeOverlaps, crossing lines\", () => {\n\tconst cp = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 2], [1, 3]],\n\t}\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\texpect(cp.vertices_coords).toHaveLength(4);\n\texpect(result.vertices_coords).toHaveLength(5);\n\texpect(cp.edges_vertices).toHaveLength(2);\n\texpect(result.edges_vertices).toHaveLength(4);\n\texpect(vertices.map).toMatchObject([0, 1, 2, 3]);\n\texpect(edges.map).toMatchObject([[0, 1], [2, 3]]);\n});\n\ntest(\"planarizeOverlaps, endpoints touching\", () => {\n\tconst cp = {\n\t\tvertices_coords: [[0, 0], [1, 1], [1, 1], [1, 0]],\n\t\tedges_vertices: [[0, 1], [2, 3]],\n\t}\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\texpect(cp.vertices_coords).toHaveLength(4);\n\texpect(result.vertices_coords).toHaveLength(3);\n\texpect(cp.edges_vertices).toHaveLength(2);\n\texpect(result.edges_vertices).toHaveLength(2);\n\texpect(vertices.map).toMatchObject([0, 1, 1, 2]);\n\texpect(edges.map).toMatchObject([[0], [1]]);\n});\n\ntest(\"planarizeOverlaps, overlapping assignments\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/overlapping-assignments.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\t// # of vertices does not change\n\texpect(cp.vertices_coords).toHaveLength(20);\n\texpect(result.vertices_coords).toHaveLength(20);\n\n\t// # of edges increases\n\texpect(cp.edges_vertices).toHaveLength(18);\n\texpect(result.edges_vertices).toHaveLength(34);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [1], [2], [3], [4], [5], [6], [7, 8, 9], [10], [11], [12],\n\t\t[13, 14, 15, 16, 17], [18], [19], [20], [21, 22, 23, 24, 25, 26, 27],\n\t\t[28, 29], [30, 31, 32, 33],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeOverlaps-overlapping-assignments.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarizeOverlaps, crane\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t\t// edges_line,\n\t\t\t// lines,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\t// this result is already planar. neither # of vertices nor edges change\n\texpect(cp.vertices_coords).toHaveLength(56);\n\texpect(result.vertices_coords).toHaveLength(56);\n\texpect(cp.edges_vertices).toHaveLength(114);\n\texpect(result.edges_vertices).toHaveLength(114);\n\texpect(vertices.map).toHaveLength(56);\n\tedges.map.forEach(val => expect(val).toHaveLength(1));\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeOverlaps-crane.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarizeOverlaps, non-planar-polygons pentagon\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-polygons.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD);\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\texpect(cp.vertices_coords).toHaveLength(15);\n\texpect(result.vertices_coords).toHaveLength(6);\n\texpect(cp.edges_vertices).toHaveLength(10);\n\texpect(result.edges_vertices).toHaveLength(10);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 2, 3, 4, 1, 5, 5, 3, 4, 5, 5, 2, 0, 5,\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [1], [2], [3], [4], [5], [6], [7], [8], [9],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeOverlaps-non-planar-polygons.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarizeOverlaps, non-planar-polygons hexagon\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/non-planar-polygons.fold\", \"utf-8\");\n\tconst cp = JSON.parse(FOLD).file_frames[0];\n\tconst {\n\t\tresult,\n\t\tchanges: {\n\t\t\tvertices,\n\t\t\tedges,\n\t\t},\n\t} = ear.graph.planarizeOverlaps(cp);\n\n\texpect(cp.vertices_coords).toHaveLength(24);\n\texpect(result.vertices_coords).toHaveLength(7);\n\texpect(cp.edges_vertices).toHaveLength(9);\n\texpect(result.edges_vertices).toHaveLength(12);\n\n\texpect(vertices.map).toMatchObject([\n\t\t0, 1, 2, 3, 4, 5, 1, 6, 3, 6, 5, 6, 2, 6, 4, 6, 6, 0, 3, 0, 1, 4, 5, 2,\n\t]);\n\texpect(ear.graph.invertFlatToArrayMap(vertices.map)).toMatchObject([\n\t\t[0, 17, 19], [1, 6, 20], [2, 12, 23], [3, 8, 18], [4, 14, 21], [5, 10, 22],\n\t\t[7, 9, 11, 13, 15, 16],\n\t]);\n\texpect(edges.map).toMatchObject([\n\t\t[0], [1], [2], [3], [4], [5], [6, 7], [8, 9], [10, 11],\n\t]);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/planarizeCollinearEdges-non-planar-polygons.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n\ntest(\"planarize, foldedForm, windmill\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst { result, changes } = ear.graph.planarizeOverlaps(folded);\n\n\texpect(folded.vertices_coords).toHaveLength(12);\n\texpect(result.vertices_coords).toHaveLength(13);\n\texpect(folded.edges_vertices).toHaveLength(20);\n\texpect(result.edges_vertices).toHaveLength(32);\n\n\texpect(result.vertices_coords).toMatchObject([\n\t\t[0, 2], // bottom point\n\t\t[2, 2], // center\n\t\t[2, 0], // left point\n\t\t[2, 4], // top point\n\t\t[1, 1], // square corner 1\n\t\t[3, 3], // square corner 2\n\t\t[4, 2], // right point\n\t\t[1, 3], // square corner 3\n\t\t[3, 1], // square corner 4\n\t\t[1, 2], // square edge 1\n\t\t[2, 1], // square edge 2\n\t\t[2, 3], // square edge 3\n\t\t[3, 2], // square edge 4\n\t]);\n\n\texpect(result.edges_vertices).toMatchObject([\n\t\t[0, 9], [9, 1], [1, 10], [10, 2], [3, 11], [11, 1], [1, 9], [9, 0], [0, 4],\n\t\t[5, 6], [7, 9], [9, 4], [4, 10], [10, 8], [7, 1], [8, 1], [4, 1], [3, 7],\n\t\t[8, 2], [8, 12], [12, 5], [5, 11], [11, 7], [2, 10], [10, 1], [1, 12],\n\t\t[12, 6], [6, 12], [12, 1], [1, 11], [11, 3], [5, 1],\n\t]);\n\n\texpect(changes.vertices.map).toMatchObject([\n\t\t0, 1, 2, 3, 1, 4, 5, 6, 7, 8, 1, 1,\n\t]);\n\n\texpect(changes.edges.map).toMatchObject([\n\t\t[0, 1], [2, 3], [4, 5], [6, 7], [8], [9], [10, 11], [12, 13],\n\t\t[14], [15], [16], [17], [18], [19, 20], [21, 22], [23, 24],\n\t\t[25, 26], [27, 28], [29, 30], [31],\n\t]);\n\n\tfs.writeFileSync(\"./tests/tmp/planarizeOverlaps-windmill.fold\", JSON.stringify(result));\n\tfs.writeFileSync(\"./tests/tmp/planarizeOverlaps-windmill.json\", JSON.stringify(changes));\n});\n"
  },
  {
    "path": "tests/graph.pleat.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"pleat\", () => {\n\n});\n\ntest(\"pleatEdges\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.populate.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\n// export const makeEmptyGraph = () => populate({\n// \tvertices_coords: [],\n// \tedges_vertices: [],\n// \tedges_assignment: [],\n// \tedges_foldAngle: [],\n// \tfaces_vertices: [],\n// });\n\ntest(\"populate with isolated vertex\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [2, 0], [3, 0]],\n\t\tedges_vertices: [[0, 1], [1, 2]],\n\t\tedges_foldAngle: [0, 0],\n\t\tedges_assignment: [\"U\", \"U\"],\n\t});\n\tgraph.populate();\n\texpect(graph.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_edges.length).toBe(3);\n\texpect(graph.vertices_vertices.length).toBe(3);\n\t// expect(graph.vertices_sectors.length).toBe(3);\n\n\texpect(graph.edges_vertices.length).toBe(2);\n\t// expect(graph.edges_vector.length).toBe(2);\n\texpect(graph.edges_assignment.length).toBe(2);\n\texpect(graph.edges_foldAngle.length).toBe(2);\n\n\texpect(graph.faces_vertices.length).toBe(0);\n\texpect(graph.faces_edges.length).toBe(0);\n\texpect(graph.faces_faces.length).toBe(0);\n\t// expect(graph.faces_sectors.length).toBe(0);\n\t// expect(graph.faces_matrix.length).toBe(0);\n});\n\ntest(\"populate with assignment and fold angle\", () => {\n\tconst graph1 = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0]],\n\t\tedges_vertices: [[0, 1]],\n\t\tedges_assignment: [\"M\", \"V\", \"U\", \"F\", \"B\", \"X\"],\n\t});\n\tgraph1.populate();\n\texpect(graph1.edges_foldAngle.length).toBe(6);\n\texpect(graph1.edges_foldAngle[0]).toBe(-180);\n\texpect(graph1.edges_foldAngle[1]).toBe(180);\n\texpect(graph1.edges_foldAngle[5]).toBe(0);\n\n\tconst graph2 = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0]],\n\t\tedges_vertices: [[0, 1]],\n\t\tedges_foldAngle: [-180, 180, 0, 0, 0, 0],\n\t});\n\tgraph2.populate();\n\texpect(graph2.edges_assignment.length).toBe(6);\n\texpect(graph2.edges_assignment[0]).toBe(\"M\");\n\texpect(graph2.edges_assignment[1]).toBe(\"V\");\n\texpect(graph2.edges_assignment[5]).toBe(\"U\");\n});\n\ntest(\"component edges with no vertex data\", () => {\n\tconst graph = ear.graph({\n\t\tedges_faces: [[0, 1], [3, 0]],\n\t\tedges_foldAngle: [0, 0],\n\t});\n\n\ttry {\n\t\tgraph.populate();\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t}\n});\n\ntest(\"one-fold populate, no faces rebuilt\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n\t});\n\tgraph.populate();\n\texpect(graph.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_vertices.length).toBe(4);\n\texpect(graph.vertices_faces.length).toBe(4);\n\texpect(graph.vertices_edges.length).toBe(4);\n\texpect(graph.edges_vertices.length).toBe(5);\n\texpect(graph.edges_edges).toBe(undefined);\n\texpect(graph.edges_faces.length).toBe(5);\n\texpect(graph.edges_length).toBe(undefined);\n\texpect(graph.edges_assignment.length).toBe(5);\n\texpect(graph.edges_foldAngle.length).toBe(5);\n\texpect(graph.faces_faces.length).toBe(0);\n\texpect(graph.faces_vertices.length).toBe(0);\n\texpect(graph.faces_edges.length).toBe(0);\n});\n\ntest(\"one-fold populate, rebuild faces\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n\t});\n\tgraph.populate({ faces: true });\n\texpect(graph.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_vertices.length).toBe(4);\n\texpect(graph.vertices_faces.length).toBe(4);\n\texpect(graph.vertices_edges.length).toBe(4);\n\texpect(graph.edges_vertices.length).toBe(5);\n\texpect(graph.edges_edges).toBe(undefined);\n\texpect(graph.edges_faces.length).toBe(5);\n\texpect(graph.edges_length).toBe(undefined);\n\texpect(graph.edges_assignment.length).toBe(5);\n\texpect(graph.edges_foldAngle.length).toBe(5);\n\texpect(graph.faces_faces.length).toBe(2);\n\texpect(graph.faces_vertices.length).toBe(2);\n\texpect(graph.faces_edges.length).toBe(2);\n});\n\ntest(\"one-fold populate, with assignments\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 3]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"V\"],\n\t});\n\tgraph.populate({ faces: true });\n\texpect(graph.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_vertices.length).toBe(4);\n\texpect(graph.vertices_faces.length).toBe(4);\n\texpect(graph.vertices_edges.length).toBe(4);\n\texpect(graph.edges_vertices.length).toBe(5);\n\texpect(graph.edges_edges).toBe(undefined);\n\texpect(graph.edges_faces.length).toBe(5);\n\texpect(graph.edges_length).toBe(undefined);\n\texpect(graph.edges_assignment.length).toBe(5);\n\texpect(graph.edges_foldAngle.length).toBe(5);\n\texpect(graph.faces_faces.length).toBe(2);\n\texpect(graph.faces_vertices.length).toBe(2);\n\texpect(graph.faces_edges.length).toBe(2);\n});\n\ntest(\"populate kite base\", () => {\n\tconst kite = {\n\t\tfile_spec: 1.1,\n\t\tvertices_coords: [[0, 0], [0.414, 0], [1, 0], [1, 0.586], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [5, 1], [3, 5], [5, 2]],\n\t\tfaces_vertices: [[0, 1, 5], [1, 2, 5], [2, 3, 5], [3, 4, 5]],\n\t};\n\n\tconst graph = ear.graph(kite);\n\tgraph.populate();\n\texpect(graph.vertices_coords.length).toBe(6);\n\texpect(graph.vertices_vertices.length).toBe(6);\n\texpect(graph.vertices_edges.length).toBe(6);\n\texpect(graph.vertices_faces.length).toBe(6);\n\texpect(graph.edges_vertices.length).toBe(9);\n\texpect(graph.edges_faces.length).toBe(9);\n\texpect(graph.edges_length).toBe(undefined);\n\texpect(graph.edges_edges).toBe(undefined);\n\texpect(graph.faces_vertices.length).toBe(4);\n\texpect(graph.faces_edges.length).toBe(4);\n\texpect(graph.faces_faces.length).toBe(4);\n});\n\ntest(\"FOLD core populate, no assignments\", () => {\n\tconst blintz = {\n\t\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5]],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6],\n\t\t\t[6, 7], [7, 0], [1, 3], [3, 5], [5, 7], [7, 1],\n\t\t],\n\t};\n\tear.graph.populate(blintz);\n\n\texpect(blintz.edges_faces !== undefined).toBe(true);\n\texpect(blintz.edges_vertices !== undefined).toBe(true);\n\texpect(blintz.faces_edges !== undefined).toBe(true);\n\texpect(blintz.faces_faces !== undefined).toBe(true);\n\texpect(blintz.faces_vertices !== undefined).toBe(true);\n\texpect(blintz.vertices_coords !== undefined).toBe(true);\n\texpect(blintz.vertices_edges !== undefined).toBe(true);\n\texpect(blintz.vertices_faces !== undefined).toBe(true);\n\texpect(blintz.vertices_vertices !== undefined).toBe(true);\n\texpect(blintz.edges_assignment !== undefined).toBe(true);\n\texpect(blintz.edges_foldAngle !== undefined).toBe(true);\n\texpect(blintz.edges_length === undefined).toBe(true);\n});\n\n\ntest(\"FOLD core populate\", () => {\n\tconst blintz = {\n\t\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5]],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6],\n\t\t\t[6, 7], [7, 0], [1, 3], [3, 5], [5, 7], [7, 1],\n\t\t],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"V\", \"V\"],\n\t};\n\tear.graph.populate(blintz);\n\n\texpect(blintz.edges_faces !== undefined).toBe(true);\n\texpect(blintz.edges_vertices !== undefined).toBe(true);\n\texpect(blintz.faces_edges !== undefined).toBe(true);\n\texpect(blintz.faces_faces !== undefined).toBe(true);\n\texpect(blintz.faces_vertices !== undefined).toBe(true);\n\texpect(blintz.vertices_coords !== undefined).toBe(true);\n\texpect(blintz.vertices_edges !== undefined).toBe(true);\n\texpect(blintz.vertices_faces !== undefined).toBe(true);\n\texpect(blintz.vertices_vertices !== undefined).toBe(true);\n\texpect(blintz.edges_assignment !== undefined).toBe(true);\n\texpect(blintz.edges_foldAngle !== undefined).toBe(true);\n\n\texpect(blintz.edges_length === undefined).toBe(true);\n});\n\ntest(\"populate a complete graph\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/surrounded-square.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst clone = structuredClone(graph);\n\n\t// clone turned all \"undefined\" into \"null\". change these back\n\tObject.keys(clone)\n\t\t.filter(arr => clone[arr][0] != null && clone[arr][0].constructor === Array)\n\t\t.forEach(key => clone[key].forEach((arr, i) => arr.forEach((value, j) => {\n\t\t\tif (value === null) { clone[key][i][j] = undefined; }\n\t\t})))\n\n\t// delete all fields, leaving behind:\n\t// vertices_coords, vertices_vertices, edges_vertices, faces_vertices\n\tdelete graph.faces_faces;\n\tdelete graph.faces_edges;\n\tdelete graph.edges_faces;\n\tdelete graph.faces_faces;\n\tdelete graph.vertices_faces;\n\tdelete graph.vertices_edges;\n\n\t// rebuild using populate, expecting the same result as before\n\tear.graph.populate(graph);\n\n\t// currently this library does not follow the\n\t// spec's recommendation for edges_faces ordering.\n\tdelete graph.edges_faces;\n\tdelete clone.edges_faces;\n\texpect(graph).toMatchObject(clone);\n});\n"
  },
  {
    "path": "tests/graph.remove.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"array with undefined\", () => {\n\tconst graph = {\n\t\tfaces_faces: [\n\t\t\t[1, 2, 3, 4],\n\t\t\t[undefined, 2, 0, 4],\n\t\t\t[undefined, 3, 0, 1],\n\t\t\t[undefined, 4, 0, 2],\n\t\t\t[undefined, 1, 0, 3],\n\t\t]\n\t};\n\n\t// replacing face 0 with two new faces (added to the end)\n\tgraph.faces_faces.push([3, 4, 6]);\n\tgraph.faces_faces.push([1, 2, 5]);\n\n\t// replace instance of '0'\n\tgraph.faces_faces[1][2] = 6;\n\tgraph.faces_faces[2][2] = 6;\n\tgraph.faces_faces[3][2] = 5;\n\tgraph.faces_faces[4][2] = 5;\n\n\tear.graph.remove(graph, \"faces\", [0]);\n\n\texpect(graph.faces_faces).toMatchObject([\n\t\t[undefined, 1, 5, 3],\n\t\t[undefined, 2, 5, 0],\n\t\t[undefined, 3, 4, 1],\n\t\t[undefined, 0, 4, 2],\n\t\t[2, 3, 5],\n\t\t[0, 1, 4],\n\t]);\n});\n\ntest(\"populated graph, order of remove\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.fish();\n\tconst graph3 = ear.graph.fish();\n\tconst graph4 = ear.graph.fish();\n\tconst graph5 = ear.graph.fish();\n\tconst graph6 = ear.graph.fish();\n\tconst remove = {\n\t\tvertices: [0],\n\t\tedges: [0, 4, 7, 16, 20],\n\t\tfaces: [0, 1, 2, 3],\n\t};\n\tear.graph.remove(graph1, \"vertices\", remove.vertices);\n\tear.graph.remove(graph1, \"edges\", remove.edges);\n\tear.graph.remove(graph1, \"faces\", remove.faces);\n\n\tear.graph.remove(graph2, \"vertices\", remove.vertices);\n\tear.graph.remove(graph2, \"faces\", remove.faces);\n\tear.graph.remove(graph2, \"edges\", remove.edges);\n\n\tear.graph.remove(graph3, \"edges\", remove.edges);\n\tear.graph.remove(graph3, \"vertices\", remove.vertices);\n\tear.graph.remove(graph3, \"faces\", remove.faces);\n\n\tear.graph.remove(graph4, \"edges\", remove.edges);\n\tear.graph.remove(graph4, \"faces\", remove.faces);\n\tear.graph.remove(graph4, \"vertices\", remove.vertices);\n\n\tear.graph.remove(graph5, \"faces\", remove.faces);\n\tear.graph.remove(graph5, \"vertices\", remove.vertices);\n\tear.graph.remove(graph5, \"edges\", remove.edges);\n\n\tear.graph.remove(graph6, \"faces\", remove.faces);\n\tear.graph.remove(graph6, \"edges\", remove.edges);\n\tear.graph.remove(graph6, \"vertices\", remove.vertices);\n\n\texpect(JSON.stringify(graph1)).toBe(JSON.stringify(graph2));\n\texpect(JSON.stringify(graph2)).toBe(JSON.stringify(graph3));\n\texpect(JSON.stringify(graph3)).toBe(JSON.stringify(graph4));\n\texpect(JSON.stringify(graph4)).toBe(JSON.stringify(graph5));\n\texpect(JSON.stringify(graph5)).toBe(JSON.stringify(graph6));\n});\n\ntest(\"populated graph, order of remove, no zero indexed elements\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.fish();\n\tconst graph3 = ear.graph.fish();\n\tconst graph4 = ear.graph.fish();\n\tconst graph5 = ear.graph.fish();\n\tconst graph6 = ear.graph.fish();\n\tconst remove = {\n\t\tvertices: [2],\n\t\tedges: [1, 8, 9, 11, 13],\n\t\tfaces: [6, 7, 8, 9],\n\t};\n\n\tear.graph.remove(graph1, \"vertices\", remove.vertices);\n\tear.graph.remove(graph1, \"edges\", remove.edges);\n\tear.graph.remove(graph1, \"faces\", remove.faces);\n\n\tear.graph.remove(graph2, \"vertices\", remove.vertices);\n\tear.graph.remove(graph2, \"faces\", remove.faces);\n\tear.graph.remove(graph2, \"edges\", remove.edges);\n\n\tear.graph.remove(graph3, \"edges\", remove.edges);\n\tear.graph.remove(graph3, \"vertices\", remove.vertices);\n\tear.graph.remove(graph3, \"faces\", remove.faces);\n\n\tear.graph.remove(graph4, \"edges\", remove.edges);\n\tear.graph.remove(graph4, \"faces\", remove.faces);\n\tear.graph.remove(graph4, \"vertices\", remove.vertices);\n\n\tear.graph.remove(graph5, \"faces\", remove.faces);\n\tear.graph.remove(graph5, \"vertices\", remove.vertices);\n\tear.graph.remove(graph5, \"edges\", remove.edges);\n\n\tear.graph.remove(graph6, \"faces\", remove.faces);\n\tear.graph.remove(graph6, \"edges\", remove.edges);\n\tear.graph.remove(graph6, \"vertices\", remove.vertices);\n\n\texpect(JSON.stringify(graph1)).toBe(JSON.stringify(graph2));\n\texpect(JSON.stringify(graph2)).toBe(JSON.stringify(graph3));\n\texpect(JSON.stringify(graph3)).toBe(JSON.stringify(graph4));\n\texpect(JSON.stringify(graph4)).toBe(JSON.stringify(graph5));\n\texpect(JSON.stringify(graph5)).toBe(JSON.stringify(graph6));\n});\n\ntest(\"populated graph, removing components, null entry in edges_vertices\", () => {\n\tconst expected = {\n\t\tvertices_coords: [\n\t\t\t[0, 0],\n\t\t\t[0.7071067811865476, 0],\n\t\t\t[1, 0.2928932188134524],\n\t\t\t[1, 1],\n\t\t\t[0.2928932188134524, 1],\n\t\t\t[0, 1],\n\t\t\t[0, 0.7071067811865476],\n\t\t\t[0.5, 0.5],\n\t\t\t[0.7071067811865476, 0.2928932188134524],\n\t\t\t[0.2928932188134524, 0.7071067811865476],\n\t\t],\n\t\tvertices_vertices: [\n\t\t\t[1, 8, 7, 9, 6],\n\t\t\t[undefined, 8, 0],\n\t\t\t[3, 8, undefined],\n\t\t\t[4, 9, 7, 8, 2],\n\t\t\t[3, 5, 9],\n\t\t\t[4, 6, 9],\n\t\t\t[9, 5, 0],\n\t\t\t[3, 9, 0, 8],\n\t\t\t[2, 3, 7, 0, 1, undefined],\n\t\t\t[3, 4, 5, 6, 0, 7],\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[0, undefined, 13, undefined, 6],\n\t\t\t[undefined, 9, 0],\n\t\t\t[2, 11, 1],\n\t\t\t[3, undefined, 15, 7, 2],\n\t\t\t[3, 4, 12],\n\t\t\t[4, 5, 8],\n\t\t\t[10, 5, 6],\n\t\t\t[15, 16, 13, 14],\n\t\t\t[11, 7, 14, undefined, 9, undefined],\n\t\t\t[undefined, 12, 8, 10, undefined, 16]\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[0, 1, 2, 3, undefined],\n\t\t\t[4, 0, undefined],\n\t\t\t[undefined, 5, undefined],\n\t\t\t[undefined, undefined, undefined, undefined, undefined],\n\t\t\t[undefined, 6, undefined],\n\t\t\t[undefined, 7, 6],\n\t\t\t[7, undefined, 3],\n\t\t\t[undefined, 2, 1, undefined],\n\t\t\t[undefined, undefined, 1, 0, 4, 5],\n\t\t\t[undefined, 6, 7, 3, 2, undefined],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [undefined, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 0], [8, 3],\n\t\t\t[9, 5], [8, 1], [9, 6], [8, 2], [9, 4], [7, 0], [7, 8], [7, 3], [7, 9]\n\t\t],\n\t\tedges_faces: [\n\t\t\t[0],\n\t\t\t[5],\n\t\t\t[undefined],\n\t\t\t[undefined],\n\t\t\t[6],\n\t\t\t[7],\n\t\t\t[3],\n\t\t\t[undefined, undefined],\n\t\t\t[6, 7],\n\t\t\t[0, 4],\n\t\t\t[3, 7],\n\t\t\t[5, undefined],\n\t\t\t[undefined, 6],\n\t\t\t[1, 2],\n\t\t\t[1, undefined],\n\t\t\t[undefined, undefined],\n\t\t\t[2, undefined],\n\t\t],\n\t\tedges_assignment: [\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"M\", \"M\", \"F\", \"F\", \"F\", \"F\", \"F\", \"F\",\n\t\t],\n\t\tedges_foldAngle: [\n\t\t\t0, 0, 0, 0, 0, 0, 0, 180, 180, -180, -180, 0, 0, 0, 0, 0, 0,\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[0, 1, 8],\n\t\t\t[0, 8, 7],\n\t\t\t[0, 7, 9],\n\t\t\t[0, 9, 6],\n\t\t\t[1, undefined, 8],\n\t\t\t[undefined, 2, 8],\n\t\t\t[4, 5, 9],\n\t\t\t[5, 6, 9],\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[0, 9, undefined],\n\t\t\t[undefined, 14, 13],\n\t\t\t[13, 16, undefined],\n\t\t\t[undefined, 10, 6],\n\t\t\t[undefined, undefined, 9],\n\t\t\t[1, 11, undefined],\n\t\t\t[4, 8, 12],\n\t\t\t[5, 10, 8],\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[undefined, 4, 1],\n\t\t\t[0, undefined, 2],\n\t\t\t[1, undefined, 3],\n\t\t\t[2, 7, undefined],\n\t\t\t[undefined, 5, 0],\n\t\t\t[undefined, undefined, 4],\n\t\t\t[undefined, 7, undefined],\n\t\t\t[undefined, 3, 6],\n\t\t],\n\t};\n\tconst graph = ear.graph.fish();\n\tconst remove = {\n\t\tvertices: [2],\n\t\tedges: [1, 8, 9, 11, 13],\n\t\tfaces: [6, 7, 8, 9],\n\t};\n\tear.graph.remove(graph, \"vertices\", remove.vertices);\n\tear.graph.remove(graph, \"edges\", remove.edges);\n\tear.graph.remove(graph, \"faces\", remove.faces);\n\texpect(graph).toMatchObject(expected);\n});\n\nconst testGraph = () => ({\n\tvertices_coords: [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 0]],\n\tedges_string: [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\"],\n\tfaces_vertices: [[0, 1, 2, 3, 4, 5, 6]],\n});\n\ntest(\"remove graph\", () => {\n\tconst graph = testGraph();\n\tconst res = ear.graph.remove(graph, \"vertices\", [2, 3]);\n\t[0, 1, undefined, undefined, 2, 3, 4].forEach((el, i) => expect(el).toBe(res[i]));\n});\n\ntest(\"few params\", () => {\n\tconst res1 = ear.graph.remove({ abc_def: [[1, 2, 3]] }, \"abc\", [0]);\n\texpect(res1.length).toBe(1);\n\texpect(res1[0]).toBe(undefined);\n\n\tconst res2 = ear.graph.remove({ abc_def: [[1, 2, 3], [4, 5, 6]] }, \"abc\", [0]);\n\texpect(res2.length).toBe(2);\n\texpect(res2[0]).toBe(undefined);\n\texpect(res2[1]).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.rendering.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"prepareForRendering, flapping-bird\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst backup = structuredClone(fold);\n\tconst result = ear.graph.prepareForRendering(folded);\n\texpect(fold).toMatchObject(backup);\n\n\texpect(result.edges_assignment).toMatchObject([\n\t\t\"B\", \"V\", \"J\", \"J\", \"V\", \"M\", \"M\", \"V\", \"J\", \"J\", \"V\", \"B\", \"B\", \"F\", \"V\", \"B\", \"V\", \"F\", \"B\", \"F\", \"J\", \"J\", \"V\", \"M\", \"M\", \"M\", \"V\", \"B\", \"F\", \"V\", \"V\", \"M\", \"M\", \"M\", \"V\", \"J\", \"J\", \"F\", \"B\", \"B\", \"V\", \"F\", \"F\", \"V\", \"M\", \"M\", \"V\", \"F\", \"F\", \"V\", \"M\", \"M\", \"M\", \"V\", \"V\", \"M\", \"M\", \"M\", \"V\", \"F\", \"V\", \"M\", \"J\", \"J\", \"V\", \"V\", \"V\", \"V\", \"J\", \"J\", \"M\", \"V\", \"M\", \"V\", \"M\", \"M\", \"V\", \"M\", \"B\", \"V\", \"V\", \"V\", \"V\", \"B\", \"F\", \"B\", \"M\", \"M\", \"B\", \"J\", \"J\", \"V\", \"V\", \"V\", \"V\", \"J\", \"J\", \"B\", \"M\", \"M\", \"B\", \"F\",\n\t]);\n\texpect(result.vertices_coords).toHaveLength(102);\n\texpect(result.edges_vertices).toHaveLength(102);\n\texpect(result.faces_vertices).toHaveLength(34);\n});\n\ntest(\"prepareForRendering, windmill\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst backup = structuredClone(fold);\n\tconst result = ear.graph.prepareForRendering(folded);\n\texpect(fold).toMatchObject(backup);\n\n\texpect(result.edges_assignment).toMatchObject([\n\t\t\"V\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"V\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"V\", \"B\", \"J\", \"V\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"B\", \"V\", \"V\", \"J\", \"V\", \"J\", \"J\", \"J\", \"V\", \"B\", \"V\", \"V\", \"J\", \"V\", \"J\", \"J\", \"J\", \"V\", \"J\", \"V\", \"J\", \"J\", \"J\", \"V\", \"B\", \"V\", \"V\", \"B\", \"V\", \"V\", \"J\", \"V\", \"J\", \"J\", \"J\", \"V\",\n\t]);\n\texpect(result.vertices_coords).toHaveLength(72);\n\texpect(result.edges_vertices).toHaveLength(72);\n\texpect(result.faces_vertices).toHaveLength(24);\n});\n\ntest(\"prepareForRendering, square-twist\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-twist.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst backup = structuredClone(fold);\n\tconst result = ear.graph.prepareForRendering(folded);\n\texpect(fold).toMatchObject(backup);\n\n\texpect(result.edges_assignment).toMatchObject([\n\t\t\"V\", \"B\", \"J\", \"J\", \"J\", \"J\", \"J\", \"V\", \"V\", \"V\", \"V\", \"J\", \"J\", \"V\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"V\", \"V\", \"J\", \"J\", \"V\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"V\", \"V\", \"J\", \"J\", \"V\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"J\", \"B\", \"J\", \"J\", \"B\", \"V\", \"J\", \"B\", \"J\", \"J\", \"M\", \"M\", \"M\", \"J\", \"J\", \"J\", \"J\", \"M\", \"M\", \"J\", \"J\", \"J\", \"B\", \"M\", \"M\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"J\", \"M\", \"M\", \"J\", \"J\", \"J\", \"B\", \"M\", \"M\", \"B\", \"J\", \"J\", \"B\", \"J\", \"J\", \"J\", \"M\", \"M\", \"J\", \"J\", \"J\", \"B\", \"M\", \"M\", \"B\", \"J\", \"J\", \"B\", \"J\", \"M\", \"B\", \"J\", \"J\", \"B\", \"J\",\n\t])\n\texpect(result.vertices_coords).toHaveLength(120);\n\texpect(result.edges_vertices).toHaveLength(120);\n\texpect(result.faces_vertices).toHaveLength(40);\n});\n\ntest(\"prepareForRendering, no-faces-3d\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/no-faces.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst backup = structuredClone(fold);\n\tconst result = ear.graph.prepareForRendering(fold);\n\texpect(fold).toMatchObject(backup);\n\t// expect(result).toMatchObject(fold);\n\t// console.log(result);\n\n\t// expect(result.edges_assignment).toMatchObject([])\n\t// expect(result.vertices_coords).toHaveLength(120);\n\t// expect(result.edges_vertices).toHaveLength(120);\n\t// expect(result.faces_vertices).toHaveLength(40);\n});\n\ntest(\"prepareForRendering, cycles-3d\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/cycles-3d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(FOLD);\n\tconst backup = structuredClone(fold);\n\tconst result = ear.graph.prepareForRendering(fold);\n\texpect(fold).toMatchObject(backup);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/prepareForRendering-cycles-3d.fold\",\n\t\tJSON.stringify(result),\n\t);\n});\n"
  },
  {
    "path": "tests/graph.replace.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst graph = {\n\tvertices_coords: [\n\t\t[-0.001909307190396963, 0.21764344783120085],\n\t\t[0.24517501802668604, 0.9517802390855862],\n\t\t[0.006039349947071344, 0.20829299033896528],\n\t\t[0.2440059511590459, 0.9524697151219929],\n\t\t[0.6404285180020232, 0.6435864234941955],\n\t\t[0.7087180310059188, 0.4552124487148278],\n\t\t[0.005510613107705598, 0.21655320394605676],\n\t\t[0.7139660994755092, 0.4582948218446743],\n\t\t[0.005994386393809087, 0.21693481270269366],\n\t\t[0.6400373309040567, 0.644372981988839],\n\t\t[0.7163456906111709, 0.45874165960911734],\n\t\t[0.6414904984293182, 0.6533241744381391],\n\t\t[0.23975422698301938, 0.9429776466811459],\n\t\t[0.64310572075461, 0.6536288833909647],\n\t\t[0.7110837306511976, 0.45563597629605307],\n\t\t[0.6361936508708128, 0.6510278534131151],\n\t\t[-0.0001625821399819996, 0.21607779650853517],\n\t\t[0.6415339486224014, 0.6423248287700194],\n\t\t[0.23939653286603954, 0.939979137755126],\n\t\t[0.005631780506698969, 0.20823677978344754],\n\t\t[0.709342623465554, 0.45452006521439414],\n\t\t[0.6448711429320774, 0.6517715251287783],\n\t\t[0.7052727618919836, 0.4602003868880367],\n\t\t[0.23677251582363706, 0.9508151096912241],\n\t\t[0.23691633726315126, 0.9516669576591591],\n\t\t[0.7136296763895639, 0.4591714060738851],\n\t\t[0.7068958585231394, 0.45300794316449405],\n\t\t[0.24013080994722208, 0.9460298467540476],\n\t\t[0.644542165978088, 0.6559301559145365],\n\t\t[0.24841608291067824, 0.9402426211906101],\n\t],\n\tedges_vertices: [\n\t\t[0, 1],\n\t\t[2, 3],\n\t\t[4, 5],\n\t\t[6, 7],\n\t\t[8, 9],\n\t\t[10, 11],\n\t\t[12, 13],\n\t\t[14, 15],\n\t\t[16, 17],\n\t\t[18, 19],\n\t\t[20, 21],\n\t\t[22, 23],\n\t\t[24, 25],\n\t\t[26, 27],\n\t\t[28, 29],\n\t],\n};\n\ntest(\"\", () => {\n\t// {3:1, 8:6, 9:4, 10:7, 13:11, 14:7, 15:11, 16:0, 17:4,\n\t// 18:12, 19:2, 20:5, 21:11, 22:5, 24:23, 25:5, 26:5, 27:12, 28:11}\n\tconst arr = [\n\t\tnull, null, null, 1, null, null, null, null, 6, 4, 7, null,\n\t\tnull, 11, 7, 11, 0, 4, 12, 2, 5, 11, 5, null, 23, 5, 5, 12, 11,\n\t];\n\tarr.forEach((el, i) => { if (el === null) { delete arr[i]; } });\n\n\tear.graph.replace(graph, \"vertices\", arr);\n\t// console.log(graph);\n\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/graph.split.splitEdge.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\n// this feature is now deprecated\n// test(\"split edge similar endpoint\", () => {\n// \t// kite graph has 6 boundary edges, indices 0-5\n// \t// then 3 edges, \"V\", \"V\", \"F\", edges 6, 7, 8.\n// \tconst graph = ear.graph.kite();\n\n// \t// split an edge but provide the edge's endpoint\n// \t// this should not split the edge but just return the existing vertex\n// \tconst res = ear.graph.splitEdge(graph, 6, [1, 0.41421356237309515]);\n\n// \t// the point we provided is the same as vertex 2\n// \t// and edge 6 is an edge which includes vertex 2.\n// \texpect(res.vertex).toBe(2);\n// });\n\ntest(\"split multiple edges, edge maps, approach 1: reverse order\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t};\n\tconst line = { vector: [0, 1], origin: [0.5, 0.75] };\n\n\t// this will intersect edges: [0, 2, 4]\n\tconst { edges } = ear.graph\n\t\t.intersectLineVerticesEdges(graph, line);\n\n\t// very important!\n\t// this sorts the edges in reverse order, this ensure that when we modify\n\t// the graph, the edge indices that are untouched are smaller than\n\t// the current edge, this way we always iterate next to an untouched index.\n\tconst splitEdgeMaps = edges\n\t\t.map((intersection, edge) => ({ intersection, edge }))\n\t\t.filter(a => a.intersection !== undefined)\n\t\t.reverse()\n\t\t.map(({ intersection: { point }, edge }) => {\n\t\t\tconst { edges: { map } } = ear.graph.splitEdge(graph, edge, point);\n\t\t\treturn map;\n\t\t});\n\n\tconst merge = ear.graph.mergeNextmaps(...splitEdgeMaps);\n\n\texpect(edges[0]).not.toBe(undefined);\n\texpect(edges[1]).toBe(undefined);\n\texpect(edges[2]).not.toBe(undefined);\n\texpect(edges[3]).toBe(undefined);\n\texpect(edges[4]).not.toBe(undefined);\n\n\texpect(merge).toMatchObject([[6, 7], [0], [4, 5], [1], [2, 3]]);\n\n\t// the corner vertices remained 0-3\n\t// the new vertices, in reverse order (of edge indices)\n\t// the diagonal first, then the top boundary, then the bottom boundary.\n\texpect(graph.vertices_coords).toMatchObject([\n\t\t[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5], [0.5, 1], [0.5, 0],\n\t]);\n\n\t// these are the only two edges remaining untouched from the original graph\n\t// they bubbled up to the first two indices\n\texpect(graph.edges_vertices[0]).toMatchObject([1, 2]);\n\texpect(graph.edges_vertices[1]).toMatchObject([3, 0]);\n});\n\ntest(\"split multiple edges, edge maps, approach 2: update inside loop\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t};\n\tconst line = { vector: [0, 1], origin: [0.5, 0.75] };\n\n\t// this will intersect edges: [0, 2, 4]\n\tconst { edges } = ear.graph\n\t\t.intersectLineVerticesEdges(graph, line);\n\n\t// this approach does not bother with any edge sorting, it can work\n\t// in any order. however, each time we run \"splitEdge\", because the graph\n\t// has changed since the last loop, we have to update the intended edge index\n\t// using the current state of the edgeMap, get the current index of the edge.\n\tlet edgeMap = graph.edges_vertices.map((_, i) => i);\n\tedges\n\t\t.map((intersection, edge) => ({ intersection, edge }))\n\t\t.filter(a => a.intersection !== undefined)\n\t\t.forEach(({ intersection: { point }, edge }) => {\n\t\t\tconst newEdge = edgeMap[edge];\n\t\t\tconst { edges: { map } } = ear.graph.splitEdge(graph, newEdge, point);\n\t\t\tedgeMap = ear.graph.mergeNextmaps(edgeMap, map);\n\t\t});\n\n\texpect(edges[0]).not.toBe(undefined);\n\texpect(edges[1]).toBe(undefined);\n\texpect(edges[2]).not.toBe(undefined);\n\texpect(edges[3]).toBe(undefined);\n\texpect(edges[4]).not.toBe(undefined);\n\n\texpect(edgeMap).toMatchObject([[2, 3], [0], [4, 5], [1], [6, 7]]);\n\n\t// the corner vertices remained 0-3\n\t// the new vertices, in correct order (of edge indices)\n\t// the bottom boundary first, then the top boundary, then the diagonal\n\texpect(graph.vertices_coords).toMatchObject([\n\t\t[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0], [0.5, 1], [0.5, 0.5],\n\t]);\n\n\t// these are the only two edges remaining untouched from the original graph\n\t// they bubbled up to the first two indices\n\texpect(graph.edges_vertices[0]).toMatchObject([1, 2]);\n\texpect(graph.edges_vertices[1]).toMatchObject([3, 0]);\n});\n\ntest(\"splitEdge, interior edge\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst result = ear.graph.splitEdge(\n\t\tgraph,\n\t\t4,\n\t\t[0.5, 0.2928932188134526],\n\t);\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n\n\texpect(result).toMatchObject({\n\t\tvertex: 8,\n\t\tedges: {\n\t\t\tadd: [11, 12],\n\t\t\tremove: 4,\n\t\t\tmap: [0, 1, 2, 3, [11, 12], 4, 5, 6, 7, 8, 9, 10],\n\t\t},\n\t});\n\n\texpect(graph).toMatchObject({\n\t\tvertices_vertices: [\n\t\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t\t[8, 7, 0], [6, 8, 1], [7, 5, 2], [4, 6, 3], [4, 5],\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[0, 7, 3], [1, 8, 0], [2, 9, 1], [3, 10, 2],\n\t\t\t[11, 6, 7], [4, 12, 8], [5, 4, 9], [6, 5, 10], [11, 12],\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[1, 4, null], [2, 1, null], [3, 2, null], [4, 3, null],\n\t\t\t[0, 4, 1], [0, 1, 2], [0, 2, 3], [0, 3, 4], [1, 0],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0], [5, 6], [6, 7], [7, 4],\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], [4, 8], [8, 5],\n\t\t],\n\t\tedges_assignment: [\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"F\", \"F\"\n\t\t],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\t\tedges_faces: [\n\t\t\t[1], [2], [3], [4], [0, 2], [0, 3], [0, 4],\n\t\t\t[4, 1], [1, 2], [2, 3], [3, 4], [0, 1], [0, 1],\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[4, 8, 5, 6, 7], [0, 1, 5, 8, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[11, 12, 4, 5, 6], [0, 8, 12, 11, 7], [1, 9, 4, 8], [2, 10, 5, 9], [3, 7, 6, 10]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[1, 1, 2, 3, 4], [null, 2, 0, 0, 4], [null, 3, 0, 1], [null, 4, 0, 2], [null, 1, 0, 3]\n\t\t],\n\t});\n});\n\ntest(\"splitEdge, exterior edge\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst result = ear.graph.splitEdge(graph, 0, [0.5, 0]);\n\n\texpect(result).toMatchObject({\n\t\tvertex: 8,\n\t\tedges: {\n\t\t\tadd: [11, 12],\n\t\t\tremove: 0,\n\t\t\tmap: [[11, 12], 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n\t\t},\n\t});\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n\n\texpect(graph).toMatchObject({\n\t\tvertices_vertices: [\n\t\t\t[8, 4, 3], [2, 5, 8], [3, 6, 1], [0, 7, 2],\n\t\t\t[5, 7, 0], [6, 4, 1], [7, 5, 2], [4, 6, 3], [0, 1]\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[11, 7, 2], [0, 8, 12], [1, 9, 0], [2, 10, 1],\n\t\t\t[3, 6, 7], [4, 3, 8], [5, 4, 9], [6, 5, 10], [11, 12]\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[1, 4, null], [2, 1, null], [3, 2, null], [4, 3, null],\n\t\t\t[0, 4, 1], [0, 1, 2], [0, 2, 3], [0, 3, 4], [undefined, 1],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4],\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], [0, 8], [8, 1],\n\t\t],\n\t\tedges_assignment: [\n\t\t\t\"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"B\", \"B\",\n\t\t],\n\t\tedges_foldAngle: [\n\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n\t\t],\n\t\tedges_faces: [\n\t\t\t[2], [3], [4], [0, 1], [0, 2], [0, 3], [0, 4], [4, 1], [1, 2], [2, 3], [3, 4], [1], [1],\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[4, 5, 6, 7], [0, 8, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[3, 4, 5, 6], [11, 12, 8, 3, 7], [0, 9, 4, 8], [1, 10, 5, 9], [2, 7, 6, 10]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[1, 2, 3, 4], [null, null, 2, 0, 4], [null, 3, 0, 1], [null, 4, 0, 2], [null, 1, 0, 3]\n\t\t]\n\t});\n});\n\ntest(\"splitEdge ensure winding order around vertices_faces\", () => {\n\t// in this exaple, faces_faces[0] had to duplicate the last index\n\t// but splice it into the first index to maintain winding order\n\t// with faces_vertices\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\n\tconst graph = JSON.parse(FOLD);\n\n\tear.graph.splitEdge(graph, 7, [0.2928932188134526, 0.5]);\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n\n\texpect(graph).toMatchObject({\n\t\tfaces_vertices: [\n\t\t\t[8, 4, 5, 6, 7],\n\t\t\t[0, 1, 5, 4],\n\t\t\t[1, 2, 6, 5],\n\t\t\t[2, 3, 7, 6],\n\t\t\t[3, 0, 4, 8, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[12, 4, 5, 6, 11],\n\t\t\t[0, 8, 4, 7],\n\t\t\t[1, 9, 5, 8],\n\t\t\t[2, 10, 6, 9],\n\t\t\t[3, 7, 12, 11, 10]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[4, 1, 2, 3, 4],\n\t\t\t[null, 2, 0, 4],\n\t\t\t[null, 3, 0, 1],\n\t\t\t[null, 4, 0, 2],\n\t\t\t[null, 1, 0, 0, 3],\n\t\t],\n\t});\n});\n\ntest(\"splitEdge along every edge, validate\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\n\tconst graph = JSON.parse(FOLD);\n\tconst graphs = graph.edges_vertices.map(() => structuredClone(graph));\n\tgraphs.forEach((g, i) => ear.graph.splitEdge(g, i));\n\texpect(graphs.flatMap(g => ear.graph.validate(g)).length).toBe(0);\n});\n\ntest(\"splitEdge twice, consecutively\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\n\tconst graph = JSON.parse(FOLD);\n\n\tear.graph.splitEdge(graph, 7);\n\n\texpect(graph).toMatchObject({\n\t\tvertices_vertices: [\n\t\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t\t[5, 8, 0], [6, 4, 1], [7, 5, 2], [8, 6, 3], [7, 4]\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[0, 7, 3], [1, 8, 0], [2, 9, 1], [3, 10, 2],\n\t\t\t[4, 12, 7], [5, 4, 8], [6, 5, 9], [11, 6, 10], [11, 12]\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[1, 4, null], [2, 1, null], [3, 2, null], [4, 3, null],\n\t\t\t[0, 4, 1], [0, 1, 2], [0, 2, 3], [0, 3, 4], [4, 0]\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7],\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], [7, 8], [8, 4]\n\t\t],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"F\", \"F\"],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\t\tedges_faces: [\n\t\t\t[1], [2], [3], [4], [0, 1], [0, 2], [0, 3], [4, 1], [1, 2], [2, 3], [3, 4], [0, 4], [0, 4]\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[8, 4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 8, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[12, 4, 5, 6, 11], [0, 8, 4, 7], [1, 9, 5, 8], [2, 10, 6, 9], [3, 7, 12, 11, 10]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[4, 1, 2, 3, 4], [null, 2, 0, 4], [null, 3, 0, 1], [null, 4, 0, 2], [null, 1, 0, 0, 3]\n\t\t]\n\t});\n\n\tear.graph.splitEdge(graph, 11);\n\n\texpect(graph).toMatchObject({\n\t\tvertices_vertices: [\n\t\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t\t[5, 8, 0], [6, 4, 1], [7, 5, 2], [9, 6, 3], [9, 4], [7, 8]\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[0, 7, 3], [1, 8, 0], [2, 9, 1], [3, 10, 2], [4, 11, 7],\n\t\t\t[5, 4, 8], [6, 5, 9], [12, 6, 10], [13, 11], [12, 13]\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[1, 4, null], [2, 1, null], [3, 2, null], [4, 3, null],\n\t\t\t[0, 4, 1], [0, 1, 2], [0, 2, 3], [0, 3, 4], [4, 0], [4, 0]\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7],\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], [8, 4], [7, 9], [9, 8]\n\t\t],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"F\", \"F\", \"F\"],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\t\tedges_faces: [\n\t\t\t[1], [2], [3], [4], [0, 1], [0, 2], [0, 3], [4, 1],\n\t\t\t[1, 2], [2, 3], [3, 4], [0, 4], [0, 4], [0, 4]\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[9, 8, 4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 8, 9, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[13, 11, 4, 5, 6, 12], [0, 8, 4, 7], [1, 9, 5, 8], [2, 10, 6, 9], [3, 7, 11, 13, 12, 10]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[4, 4, 1, 2, 3, 4], [null, 2, 0, 4], [null, 3, 0, 1], [null, 4, 0, 2], [null, 1, 0, 0, 0, 3]\n\t\t]\n\t});\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n});\n\ntest(\"splitEdge this will be used in splitFace tests\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\n\tconst graph = JSON.parse(FOLD);\n\n\t// split edges 4 and 6\n\tear.graph.splitEdge(graph, 4);\n\tear.graph.splitEdge(graph, 5); // used to be 6\n\n\texpect(graph).toMatchObject({\n\t\tvertices_vertices: [\n\t\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t\t[8, 7, 0], [6, 8, 1], [9, 5, 2], [4, 9, 3], [4, 5], [6, 7]\n\t\t],\n\t\tvertices_edges: [\n\t\t\t[0, 6, 3], [1, 7, 0], [2, 8, 1], [3, 9, 2],\n\t\t\t[10, 5, 6], [4, 11, 7], [12, 4, 8], [5, 13, 9], [10, 11], [12, 13]\n\t\t],\n\t\tvertices_faces: [\n\t\t\t[1, 4, null], [2, 1, null], [3, 2, null], [4, 3, null],\n\t\t\t[0, 4, 1], [0, 1, 2], [0, 2, 3], [0, 3, 4], [1, 0], [3, 0]\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0], [5, 6], [7, 4],\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], [4, 8], [8, 5], [6, 9], [9, 7]\n\t\t],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"F\", \"F\", \"F\", \"F\"],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\t\tedges_faces: [\n\t\t\t[1], [2], [3], [4], [0, 2], [0, 4], [4, 1],\n\t\t\t[1, 2], [2, 3], [3, 4], [0, 1], [0, 1], [0, 3], [0, 3]\n\t\t],\n\t\tfaces_vertices: [\n\t\t\t[4, 8, 5, 6, 9, 7], [0, 1, 5, 8, 4], [1, 2, 6, 5], [2, 3, 7, 9, 6], [3, 0, 4, 7]\n\t\t],\n\t\tfaces_edges: [\n\t\t\t[10, 11, 4, 12, 13, 5], [0, 7, 11, 10, 6], [1, 8, 4, 7], [2, 9, 13, 12, 8], [3, 6, 5, 9]\n\t\t],\n\t\tfaces_faces: [\n\t\t\t[1, 1, 2, 3, 3, 4], [null, 2, 0, 0, 4], [null, 3, 0, 1], [null, 4, 0, 0, 2], [null, 1, 0, 3]\n\t\t],\n\t});\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.split.splitFace.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"splitFaceWithEdge no change\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst clone = structuredClone(graph);\n\tconst result = ear.graph.splitFaceWithEdge(graph, 0, [4, 5]);\n\texpect(graph).toMatchObject(clone);\n\texpect(result).toMatchObject({});\n});\n\ntest(\"splitFaceWithEdge between existing vertices\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst result = ear.graph.splitFaceWithEdge(graph, 0, [4, 6]);\n\n\t// diagonal cut from bottom left to top right.\n\t// face 4 is on top\n\t// face 5 is on bottom\n\texpect(result).toMatchObject({\n\t\tedge: 12,\n\t\tfaces: { map: [[4, 5], 0, 1, 2, 3], new: [4, 5], remove: 0 },\n\t});\n\n\t// 4: vertex 6 inserted between 5 and 7\n\t// 6: vertex 4 inserted between 7 and 5\n\texpect(graph.vertices_vertices).toMatchObject([\n\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t[5, 6, 7, 0], [6, 4, 1], [7, 4, 5, 2], [4, 6, 3]\n\t]);\n\t// at 4 and 6, new edge 12 matches vertices_vertices\n\texpect(graph.vertices_edges).toMatchObject([\n\t\t[0, 8, 3], [1, 9, 0], [2, 10, 1], [3, 11, 2],\n\t\t[4, 12, 7, 8], [5, 4, 9], [6, 12, 5, 10], [7, 6, 11],\n\t]);\n\t// aligns with vertices_vertices/vertices_edges\n\t// 4: face 5 matches with vertices 5,6 and edges 4,12. should go 5 then 4\n\t// 6: face 4 matches with vertices 7,4 and edges 6,12. should go 4 then 5\n\texpect(graph.vertices_faces).toMatchObject([\n\t\t[0, 3, undefined], [1, 0, undefined], [2, 1, undefined], [3, 2, undefined],\n\t\t[5, 4, 3, 0], [5, 0, 1], [4, 5, 1, 2], [4, 2, 3],\n\t]);\n\texpect(graph.edges_vertices).toMatchObject([\n\t\t[0, 1], [1, 2], [2, 3], [3, 0], // outer\n\t\t[4, 5], [5, 6], [6, 7], [7, 4], // inner\n\t\t[0, 4], [1, 5], [2, 6], [3, 7], // diagonal connectors\n\t\t[4, 6], // new edge\n\t]);\n\texpect(graph.edges_assignment).toMatchObject([\n\t\t\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"U\"\n\t]);\n\texpect(graph.edges_foldAngle).toMatchObject([\n\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n\t]);\n\texpect(graph.edges_faces).toMatchObject([\n\t\t[0], [1], [2], [3],\n\t\t[5, 0], [5, 1], [4, 2], [4, 3],\n\t\t[3, 0], [0, 1], [1, 2], [2, 3],\n\t\t[4, 5], // new edge\n\t]);\n\texpect(graph.faces_vertices).toMatchObject([\n\t\t[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7],\n\t\t[6, 7, 4], [4, 5, 6], // new faces\n\t]);\n\t// 4: edge 6 matches vertices 6,7 and should come first\n\t// 5: edge 4 matches vertices 4,5 and should come first\n\texpect(graph.faces_edges).toMatchObject([\n\t\t[0, 9, 4, 8], [1, 10, 5, 9], [2, 11, 6, 10], [3, 8, 7, 11],\n\t\t[6, 7, 12], [4, 5, 12], // new faces\n\t]);\n\t// faces_faces matches faces_edges in winding order.\n\texpect(graph.faces_faces).toMatchObject([\n\t\t[undefined, 1, 5, 3],\n\t\t[undefined, 2, 5, 0],\n\t\t[undefined, 3, 4, 1],\n\t\t[undefined, 0, 4, 2],\n\t\t[2, 3, 5],\n\t\t[0, 1, 4],\n\t]);\n});\n\ntest(\"splitFace after two calls to splitEdge\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/surrounded-square.fold\",\n\t\t\"utf-8\",\n\t);\n\n\tconst graph = JSON.parse(FOLD);\n\n\t// split edges 4 and 6\n\tear.graph.splitEdge(graph, 4);\n\tear.graph.splitEdge(graph, 5); // used to be 6\n\t// the result of these two splitEdge calls are tested in splitEdge.test file\n\n\tconst result = ear.graph.splitFace(graph, 0, [8, 9], \"U\", 0);\n\n\texpect(result).toMatchObject({\n\t\tedge: 14,\n\t\tfaces: { map: [[4, 5], 0, 1, 2, 3], new: [4, 5], remove: 0 },\n\t});\n\n\t// vertex 8 is on the bottom, vertex 9 is on the top\n\t// face 4 is on the left, face 5 is on the right\n\t// edges 10, 11 are on the bottom, edges 12, 13 are on the top\n\t// edge 14 is the new edge from splitFace\n\n\texpect(graph).toMatchObject({\n\t\t// the two new vertices are counter-clockwise\n\t\tvertices_vertices: [\n\t\t\t[1, 4, 3], [2, 5, 0], [3, 6, 1], [0, 7, 2],\n\t\t\t[8, 7, 0], [6, 8, 1], [9, 5, 2], [4, 9, 3], [9, 4, 5], [8, 6, 7]\n\t\t],\n\t\t// two new vertices's edges are counter-clockwise\n\t\tvertices_edges: [\n\t\t\t[0, 6, 3], [1, 7, 0], [2, 8, 1], [3, 9, 2],\n\t\t\t[10, 5, 6], [4, 11, 7], [12, 4, 8], [5, 13, 9], [14, 10, 11], [14, 12, 13]\n\t\t],\n\t\t// two new vertices's faces are counter-clockwise\n\t\tvertices_faces: [\n\t\t\t[0, 3, undefined], [1, 0, undefined], [2, 1, undefined], [3, 2, undefined],\n\t\t\t[4, 3, 0], [5, 0, 1], [5, 1, 2], [4, 2, 3], [4, 0, 5], [5, 2, 4]\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 0], // boundary\n\t\t\t[5, 6], [7, 4], // inner square, sides\n\t\t\t[0, 4], [1, 5], [2, 6], [3, 7], // diagonals\n\t\t\t[4, 8], [8, 5], [6, 9], [9, 7], // split edges new edges\n\t\t\t[8, 9], // split face new edge\n\t\t],\n\t\tedges_assignment: [\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"F\", \"F\", \"J\", \"J\", \"J\", \"J\", \"F\", \"F\", \"F\", \"F\", \"U\",\n\t\t],\n\t\tedges_foldAngle: [\n\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t],\n\t\t// correct faces, in no particular order\n\t\tedges_faces: [\n\t\t\t[0], [1], [2], [3], // boundary edges\n\t\t\t[5, 1], [4, 3], // inner square, sides\n\t\t\t[3, 0], [0, 1], [1, 2], [2, 3], // diagonals\n\t\t\t[4, 0], [5, 0], [5, 2], [4, 2], // split edges new edges\n\t\t\t[4, 5], // split face new edge\n\t\t],\n\t\t// new faces have correct winding\n\t\tfaces_vertices: [\n\t\t\t[0, 1, 5, 8, 4], [1, 2, 6, 5], [2, 3, 7, 9, 6], [3, 0, 4, 7],\n\t\t\t[9, 7, 4, 8], // new faces\n\t\t\t[8, 5, 6, 9], // new faces\n\t\t],\n\t\t// new faces match winding for faces_vertices\n\t\tfaces_edges: [\n\t\t\t[0, 7, 11, 10, 6], [1, 8, 4, 7], [2, 9, 13, 12, 8], [3, 6, 5, 9],\n\t\t\t[13, 5, 10, 14], // new faces\n\t\t\t[11, 4, 12, 14], // new faces\n\t\t],\n\t\t// face 0: goes 5 then 4\n\t\t// face 2: goes 4 then 5\n\t\t// new faces match winding with faces_edges\n\t\tfaces_faces: [\n\t\t\t[undefined, 1, 5, 4, 3],\n\t\t\t[undefined, 2, 5, 0],\n\t\t\t[undefined, 3, 4, 5, 1],\n\t\t\t[undefined, 0, 4, 2],\n\t\t\t[2, 3, 0, 5],\n\t\t\t[0, 1, 2, 4],\n\t\t],\n\t});\n\n\texpect(ear.graph.validate(graph).length).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.split.splitGraph.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"splitGraphWithLineAndPoints, bird base\", () => {\n\tconst graph = ear.graph.bird();\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tconst {\n\t\tvertices,\n\t\tedges,\n\t\tfaces,\n\t} = ear.graph.splitGraphWithLineAndPoints(\n\t\tfolded,\n\t\t{ vector: [-1, 1], origin: [0.25, 0] },\n\t);\n\n\t// create an unfolded graph, reassign all \"U\" to be \"F\" so that\n\t// makeVerticesCoordsFlatFolded works\n\t// this is extremely not precise don't do this outside of basic testing\n\tfolded.edges_assignment = folded.edges_assignment\n\t\t.map(a => (a === \"U\" || a === \"u\" ? \"F\" : a));\n\tconst unfolded = {\n\t\t...folded,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(folded),\n\t};\n\n\texpect(vertices.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(0);\n\texpect(edges.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(20);\n\texpect(faces.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(20);\n\texpect(ear.graph.countVertices(folded)).toBe(35);\n\texpect(ear.graph.countEdges(folded)).toBe(70);\n\texpect(ear.graph.countFaces(folded)).toBe(36);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-bird-base-cp.fold\",\n\t\tJSON.stringify(unfolded),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-bird-base-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"splitGraphWithLineAndPoints, square fish base\", () => {\n\tconst graph = ear.graph.squareFish();\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tconst {\n\t\tvertices,\n\t\tedges,\n\t\tfaces,\n\t} = ear.graph.splitGraphWithLineAndPoints(\n\t\tfolded,\n\t\t{ vector: [-1, 1], origin: [Math.SQRT1_2, 0] },\n\t);\n\n\t// create an unfolded graph, reassign all \"U\" to be \"F\" so that\n\t// makeVerticesCoordsFlatFolded works\n\t// this is extremely not precise don't do this outside of basic testing\n\tfolded.edges_assignment = folded.edges_assignment\n\t\t.map(a => (a === \"U\" || a === \"u\" ? \"F\" : a));\n\tconst unfolded = {\n\t\t...folded,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(folded),\n\t};\n\n\texpect(vertices.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(1);\n\texpect(edges.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(11);\n\texpect(faces.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(16);\n\texpect(ear.graph.countVertices(folded)).toBe(25);\n\texpect(ear.graph.countEdges(folded)).toBe(51);\n\texpect(ear.graph.countFaces(folded)).toBe(27);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-square-fish-base-cp.fold\",\n\t\tJSON.stringify(unfolded),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-square-fish-base-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"splitGraphWithLineAndPoints, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst graph = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tconst {\n\t\tvertices,\n\t\tedges,\n\t\tfaces,\n\t} = ear.graph.splitGraphWithLineAndPoints(\n\t\tfolded,\n\t\t{ vector: [0, 1], origin: [0.6, 0.6] },\n\t);\n\n\t// create an unfolded graph, reassign all \"U\" to be \"F\" so that\n\t// makeVerticesCoordsFlatFolded works\n\t// this is extremely not precise don't do this outside of basic testing\n\tfolded.edges_assignment = folded.edges_assignment\n\t\t.map(a => (a === \"U\" || a === \"u\" ? \"F\" : a));\n\tconst unfolded = {\n\t\t...folded,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(folded),\n\t};\n\n\texpect(vertices.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(0);\n\texpect(edges.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(39);\n\texpect(faces.intersect.filter(el => el !== undefined))\n\t\t.toHaveLength(59);\n\texpect(ear.graph.countVertices(folded)).toBe(95);\n\texpect(ear.graph.countEdges(folded)).toBe(190);\n\texpect(ear.graph.countFaces(folded)).toBe(96);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-crane-cp.fold\",\n\t\tJSON.stringify(unfolded),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithLineAndPoints-crane-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"splitGraphWithSegment, windmill\", () => {\n\tconst graph = ear.graph.windmill();\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tconst segment = [[0, 1], [1, -1]];\n\tear.graph.splitGraphWithSegment(folded, segment);\n\n\t// create an unfolded graph, reassign all \"U\" to be \"F\" so that\n\t// makeVerticesCoordsFlatFolded works\n\t// this is extremely not precise don't do this outside of basic testing\n\tfolded.edges_assignment = folded.edges_assignment\n\t\t.map(a => (a === \"U\" || a === \"u\" ? \"F\" : a));\n\tconst unfolded = {\n\t\t...folded,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(folded),\n\t};\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithSegment-windmill-cp.fold\",\n\t\tJSON.stringify(unfolded),\n\t);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithSegment-windmill-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n\ntest(\"splitGraphWithSegment with leaf edges, windmill\", () => {\n\tconst graph = ear.graph.windmill();\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(graph),\n\t};\n\n\tconst segment = [[0.6, -0.2], [0.4, 0.2]];\n\tear.graph.splitGraphWithSegment(folded, segment);\n\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/splitGraphWithSegment-leafedges-windmill-folded.fold\",\n\t\tJSON.stringify(folded),\n\t);\n});\n"
  },
  {
    "path": "tests/graph.split.splitLine.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"splitLineToSegments\", () => {\n\n});\n\ntest(\"splitLineIntoEdges\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.subgraph.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"subgraphWithFaces\", () => {\n\tconst graph = ear.graph.fish();\n\tconst subgraph = ear.graph.subgraphWithFaces(graph, [6]);\n\texpect(subgraph.vertices_coords.filter(a => a).length).toBe(3);\n\texpect(subgraph.edges_vertices.filter(a => a).length).toBe(3);\n\texpect(subgraph.faces_vertices.filter(a => a).length).toBe(1);\n});\n\ntest(\"subgraph of subgraph\", () => {\n\tconst graph = ear.graph.fish();\n\tconst subgraph = ear.graph.subgraphWithFaces(graph, [3, 4, 5, 6, 7]);\n\tconst subsubgraph = ear.graph.subgraphWithFaces(subgraph, [6]);\n\texpect(JSON.stringify(ear.graph.subgraphWithFaces(graph, [6])))\n\t\t.toBe(JSON.stringify(subsubgraph));\n});\n"
  },
  {
    "path": "tests/graph.sweep.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"sweepVertices\", () => {\n\tconst vertices_coords = [\n\t\t[0, 0],\n\t\t[1e-3, 10],\n\t\t[2e-3, 4],\n\t\t[3e-3, -10],\n\t\t[4e-3, -4],\n\t\t[5e-3, 0],\n\t];\n\tconst res = ear.graph.sweepVertices({ vertices_coords }, 0, 1e-2);\n\texpect(res[0].t).toBeCloseTo(0.0025);\n\texpect(res[0].vertices.length).toBe(6);\n});\n\ntest(\"sweepVertices exactly at the epsilon\", () => {\n\tconst res = ear.graph.sweepVertices({ vertices_coords: [\n\t\t[0, 0],\n\t\t[1, 10],\n\t\t[2, 4],\n\t\t[3, -10],\n\t\t[4, -4],\n\t\t[5, 0],\n\t] }, 0, 1);\n\texpect(res.length).toBe(6);\n\texpect(res[0].t).toBeCloseTo(0);\n\texpect(res[5].t).toBeCloseTo(5);\n});\n\ntest(\"sweepVertices shuffled\", () => {\n\tconst res = ear.graph.sweepVertices({ vertices_coords: [\n\t\t[5, 0],\n\t\t[3, -10],\n\t\t[0, 0],\n\t\t[2, 4],\n\t\t[4, -4],\n\t\t[1, 10],\n\t] }, 0, 1.5);\n\texpect(res.length).toBe(1);\n\texpect(JSON.stringify(res[0].vertices))\n\t\t.toBe(JSON.stringify([2, 5, 3, 1, 4, 0]));\n});\n\ntest(\"sweepVertices shuffled, spaced\", () => {\n\tconst res = ear.graph.sweepVertices({ vertices_coords: [\n\t\t[5, 0],\n\t\t[3, -10],\n\t\t[0, 0],\n\t\t[4, -4],\n\t\t[1, 10],\n\t] }, 0, 1.5);\n\texpect(res.length).toBe(2);\n\texpect(JSON.stringify(res[0].vertices))\n\t\t.toBe(JSON.stringify([2, 4]));\n});\n\ntest(\"sweepVertices large values\", () => {\n\tconst res1 = ear.graph.sweepVertices({ vertices_coords: [\n\t\t[1e11 + 0, 0],\n\t\t[1e11 + 1e-3, 10],\n\t\t[1e11 + 2e-3, 4],\n\t\t[1e11 + 3e-3, -10],\n\t\t[1e11 + 4e-3, -4],\n\t\t[1e11 + 5e-3, 0],\n\t] }, 0, 1e-2);\n\texpect(res1[0].t).toBeCloseTo(1e11 + 0.0025);\n\texpect(res1[0].vertices.length).toBe(6);\n\tconst res2 = ear.graph.sweepVertices({ vertices_coords: [\n\t\t[1e14 + 0, 0],\n\t\t[1e14 + 1, 10],\n\t\t[1e14 + 2, 4],\n\t\t[1e14 + 3, -10],\n\t\t[1e14 + 4, -4],\n\t\t[1e14 + 5, 0],\n\t] }, 0, 1e-2);\n\texpect(res2[0].t).toBeCloseTo(1e14);\n\texpect(res2.length).toBe(6);\n\texpect(res2[0].vertices.length).toBe(1);\n});\n\n// test(\"sweepValues\", () => {\n\n// });\n\ntest(\"sweepEdges\", () => {\n\tconst res = ear.graph.sweepEdges({\n\t\tvertices_coords: [\n\t\t\t[0.01, 0], [2, 0],\n\t\t\t[1.01, 0], [3, 0],\n\t\t\t[2.01, 0], [4, 0],\n\t\t\t[3.01, 0], [5, 0],\n\t\t\t[4.01, 0], [6, 0],\n\t\t\t[5.01, 0], [7, 0],\n\t\t],\n\t\tedges_vertices: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]],\n\t});\n\tfor (let i = 1; i < 11; i += 1) {\n\t\tif (i % 2) {\n\t\t\texpect(res[i].start.length).toBe(1);\n\t\t\texpect(res[i].end.length).toBe(0);\n\t\t} else {\n\t\t\texpect(res[i].start.length).toBe(0);\n\t\t\texpect(res[i].end.length).toBe(1);\n\t\t}\n\t}\n});\n\ntest(\"sweepFaces\", () => {\n\tconst res = ear.graph.sweepFaces({\n\t\tvertices_coords: [\n\t\t\t[0.01, 0], [2, 0],\n\t\t\t[1.01, 0], [3, 0],\n\t\t\t[2.01, 0], [4, 0],\n\t\t\t[3.01, 0], [5, 0],\n\t\t\t[4.01, 0], [6, 0],\n\t\t\t[5.01, 0], [7, 0],\n\t\t],\n\t\tfaces_vertices: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]],\n\t});\n\tfor (let i = 1; i < 11; i += 1) {\n\t\tif (i % 2) {\n\t\t\texpect(res[i].start.length).toBe(1);\n\t\t\texpect(res[i].end.length).toBe(0);\n\t\t} else {\n\t\t\texpect(res[i].start.length).toBe(0);\n\t\t\texpect(res[i].end.length).toBe(1);\n\t\t}\n\t}\n});\n\ntest(\"sweep\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.symmetry.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"currently not included\", () => {});\n\n// test(\"symmetry lines, crane\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst lines = ear.graph.findSymmetryLines(fold);\n// \tconst matches = lines.filter(el => el.error < 0.25);\n// \t// should be around 65, but the exact number doesn't really matter here.\n// \texpect(lines.length > 10).toBe(true);\n// \texpect(matches.length).toBe(1);\n// \texpect(ear.math.parallel([1, 1], matches[0].line.vector)).toBe(true);\n\n// \t// no error values should be below 0\n// \texpect(lines.filter(el => el.error < 0).length).toBe(0);\n// });\n\n// test(\"symmetry lines, book-symmetry, no collinear edge\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst lines = ear.graph.findSymmetryLines(fold);\n\n// \tconst matches = lines.filter(el => el.error < 0.25);\n// \t// should be around 65, but the exact number doesn't really matter here.\n\n// \t// no error values should be below 0\n// \texpect(lines.filter(el => el.error < 0).length).toBe(0);\n// });\n"
  },
  {
    "path": "tests/graph.transfer.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"transferPointInFaceBetweenGraphs\", () => {\n\n});\n"
  },
  {
    "path": "tests/graph.transform.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"transform, uniform scale\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.scaleUniform(\n\t\tstructuredClone(graph1),\n\t\t2,\n\t);\n\tgraph2.vertices_coords.forEach((v, i) => {\n\t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] * 2);\n\t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] * 2);\n\t});\n});\n\ntest(\"transform, non-uniform scale\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.scale(\n\t\tstructuredClone(graph1),\n\t\t[2, 1],\n\t);\n\tgraph2.vertices_coords.forEach((v, i) => {\n\t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] * 2);\n\t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] * 1);\n\t});\n});\n\ntest(\"transform, translate\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.translate(\n\t\tstructuredClone(graph1),\n\t\t[1, 2],\n\t);\n\tgraph2.vertices_coords.forEach((v, i) => {\n\t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] + 1);\n\t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] + 2);\n\t\texpect(v[2]).toBe(undefined);\n\t});\n});\n\ntest(\"transform, translate, 3D with 2D vertices\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.translate(\n\t\tstructuredClone(graph1),\n\t\t[1, 2, 3],\n\t);\n\tgraph2.vertices_coords.forEach((v, i) => {\n\t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] + 1);\n\t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] + 2);\n\t\texpect(v[2]).toBe(undefined);\n\t});\n});\n\ntest(\"transform, translate3, 3D with 2D vertices\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.translate3(\n\t\tstructuredClone(graph1),\n\t\t[1, 2, 3],\n\t);\n\tgraph2.vertices_coords.forEach((v, i) => {\n\t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] + 1);\n\t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] + 2);\n\t\texpect(v[2]).toBeCloseTo(3);\n\t});\n});\n\ntest(\"transform, rotate z\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.rotateZ(\n\t\tstructuredClone(graph1),\n\t\tMath.PI,\n\t\t[4, 4],\n\t);\n\tconst bounds = ear.graph.boundingBox(graph2);\n\texpect(bounds.min[0]).toBeCloseTo(7);\n\texpect(bounds.min[1]).toBeCloseTo(7);\n\texpect(bounds.max[0]).toBeCloseTo(8);\n\texpect(bounds.max[1]).toBeCloseTo(8);\n});\n\ntest(\"transform, rotate 3D\", () => {\n\tconst graph1 = ear.graph.fish();\n\tconst graph2 = ear.graph.rotate(\n\t\tstructuredClone(graph1),\n\t\tMath.PI,\n\t\t[0, 0, 1],\n\t\t[4, 4],\n\t);\n\tconst bounds = ear.graph.boundingBox(graph2);\n\texpect(bounds.min[0]).toBeCloseTo(7);\n\texpect(bounds.min[1]).toBeCloseTo(7);\n\texpect(bounds.max[0]).toBeCloseTo(8);\n\texpect(bounds.max[1]).toBeCloseTo(8);\n});\n\ntest(\"transform, unitize 2D\", () => {\n\tconst graph1 = { vertices_coords: [[5, 3], [7, 4]] };\n\tconst graph2 = ear.graph.unitize(structuredClone(graph1));\n\tconst bounds = ear.graph.boundingBox(graph2);\n\texpect(bounds.min[0]).toBeCloseTo(0);\n\texpect(bounds.min[1]).toBeCloseTo(0);\n\texpect(bounds.max[0]).toBeCloseTo(1);\n\texpect(bounds.max[1]).toBeCloseTo(0.5);\n});\n\ntest(\"transform, unitize 3D\", () => {\n\tconst graph1 = { vertices_coords: [[5, 3, 0], [7, 4, 3]] };\n\tconst graph2 = ear.graph.unitize(structuredClone(graph1));\n\tconst bounds = ear.graph.boundingBox(graph2);\n\texpect(bounds.min[0]).toBeCloseTo(0);\n\texpect(bounds.min[1]).toBeCloseTo(0);\n\texpect(bounds.min[2]).toBeCloseTo(0);\n\texpect(bounds.max[0]).toBeCloseTo(2 / 3);\n\texpect(bounds.max[1]).toBeCloseTo(1 / 3);\n\texpect(bounds.max[2]).toBeCloseTo(1);\n});\n\n// these call the transform matrix method which is now deprecated\n\n// test(\"transform, matrix translate\", () => {\n// \tconst graph1 = ear.graph.fish();\n// \tconst graph2 = ear.graph.transform(\n// \t\tstructuredClone(graph1),\n// \t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 10, 0],\n// \t);\n// \tgraph2.vertices_coords.forEach((v, i) => {\n// \t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] + 10);\n// \t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] + 10);\n// \t});\n// });\n\n// test(\"transform, matrix scale and translate\", () => {\n// \tconst graph1 = ear.graph.fish();\n// \tconst graph2 = ear.graph.transform(\n// \t\tstructuredClone(graph1),\n// \t\t[2, 0, 0, 0, 2, 0, 0, 0, 1, 10, 10, 0],\n// \t);\n// \tgraph2.vertices_coords.forEach((v, i) => {\n// \t\texpect(v[0]).toBeCloseTo(graph1.vertices_coords[i][0] * 2 + 10);\n// \t\texpect(v[1]).toBeCloseTo(graph1.vertices_coords[i][1] * 2 + 10);\n// \t});\n// });\n"
  },
  {
    "path": "tests/graph.trees.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"minimumSpanningTrees, faces\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/disjoint-triangles-3d.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(foldfile);\n\tconst faces_faces = ear.graph.makeFacesFaces(graph);\n\tconst trees = ear.graph.minimumSpanningTrees(faces_faces);\n\texpect(trees.flat(2).length).toBe(faces_faces.length);\n\texpect(trees.length).toBe(5);\n\ttrees.forEach(tree => expect(tree[0].length).toBe(1));\n\texpect(ear.general.uniqueElements(trees.flat(2).map(el => el.index)).length)\n\t\t.toBe(faces_faces.length);\n});\n\ntest(\"minimumSpanningTrees, vertices\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/disjoint-triangles-3d.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_vertices = ear.graph.makeFacesFaces(graph);\n\tconst trees = ear.graph.minimumSpanningTrees(vertices_vertices);\n\texpect(trees.flat(2).length).toBe(vertices_vertices.length);\n\texpect(trees.length).toBe(5);\n\ttrees.forEach(tree => expect(tree[0].length).toBe(1));\n\texpect(ear.general.uniqueElements(trees.flat(2).map(el => el.index)).length)\n\t\t.toBe(vertices_vertices.length);\n});\n\n// tree object is an array inside an array, looks like:\n// [\n//   [ { face: 0 } ],\n//   [\n//     { face: 1, parent: 0, edge_vertices: [Array] },\n//     { face: 2, parent: 0, edge_vertices: [Array] },\n//     { face: 5, parent: 0, edge_vertices: [Array] }\n//   ],\n//   [\n//     { face: 3, parent: 1, edge_vertices: [Array] },\n//     { face: 38, parent: 1, edge_vertices: [Array] },\n//     { face: 39, parent: 1, edge_vertices: [Array] },\n//     { face: 4, parent: 2, edge_vertices: [Array] },\n//     { face: 11, parent: 5, edge_vertices: [Array] },\n//     { face: 20, parent: 5, edge_vertices: [Array] }\n//   ],\n//   ...\n// ]\n\ntest(\"face walk tree, crane\", () => {\n\tconst craneJSON = fs.readFileSync(\"./tests/files/fold/crane-cp.fold\");\n\tconst crane = JSON.parse(craneJSON);\n\tconst faces_faces = ear.graph.makeFacesFaces(crane);\n\n\tconst startingFace = 0;\n\tconst tree = ear.graph.minimumSpanningTrees(faces_faces, [startingFace])[0];\n\n\texpect(tree[0].length).toBe(1);\n\texpect(tree[1].length).toBe(3);\n\texpect(tree[2].length).toBe(6);\n\texpect(tree[3].length).toBe(7);\n\texpect(tree[4].length).toBe(7);\n\texpect(tree[5].length).toBe(8);\n\texpect(tree[6].length).toBe(9);\n\texpect(tree[7].length).toBe(10);\n\texpect(tree[8].length).toBe(7);\n\texpect(tree[9].length).toBe(1);\n\n\t// test that every face and edge was only ever visited once\n\tconst uniqueFace = {};\n\tuniqueFace[startingFace] = true;\n\ttree.slice(1).forEach(level => level\n\t\t.forEach(el => {\n\t\t\texpect(uniqueFace[el.parent]).toBe(true);\n\t\t\texpect(uniqueFace[el.index]).toBe(undefined);\n\t\t\tuniqueFace[el.index] = true;\n\t\t}));\n\n\t// try to check edges vertices data\n\t// tree.slice(1)\n\t// \t.forEach(level => level\n\t// \t\t.forEach(el => expect(el.edge_vertices.length).toBe(2)));\n});\n\ntest(\"face walk tree, no param\", () => {\n\texpect(ear.graph.minimumSpanningTrees()).toStrictEqual([]);\n});\n\ntest(\"face walk tree, empty graph\", () => {\n\texpect(ear.graph.minimumSpanningTrees([])).toStrictEqual([]);\n});\n\ntest(\"face walk tree, empty graph\", () => {\n\tconst result = ear.graph.minimumSpanningTrees([[]]);\n\texpect(result.length).toBe(1);\n\texpect(result[0].length).toBe(1);\n\texpect(result[0][0][0].index).toBe(0);\n});\n\ntest(\"face walk tree, no edges_vertices\", () => {\n\tconst faces_faces = ear.graph.makeFacesFaces({\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t});\n\tconst result = ear.graph.minimumSpanningTrees(faces_faces)[0];\n\texpect(result.length).toBe(2);\n\texpect(result[1].length).toBe(1);\n\texpect(result[1][0].index).toBe(1);\n\texpect(result[1][0].parent).toBe(0);\n});\n\ntest(\"face walk tree, empty edges_vertices. same as test above\", () => {\n\tconst result = ear.graph.minimumSpanningTrees(ear.graph.makeFacesFaces({\n\t\tedges_vertices: [],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t}))[0];\n\texpect(result.length).toBe(2);\n\texpect(result[1].length).toBe(1);\n\texpect(result[1][0].index).toBe(1);\n\texpect(result[1][0].parent).toBe(0);\n\t// this edge order: [3, 1] needs to match order of these indices in\n\t// the second face. 3, then 1.\n\t// expect(result[1][0].edge_vertices[0]).toBe(3);\n\t// expect(result[1][0].edge_vertices[1]).toBe(1);\n});\n\ntest(\"face walk tree, empty edges_vertices. same as test above\", () => {\n\tconst result = ear.graph.minimumSpanningTrees(ear.graph.makeFacesFaces({\n\t\tedges_vertices: [],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t}))[0];\n\texpect(result.length).toBe(2);\n\texpect(result[1].length).toBe(1);\n\texpect(result[1][0].index).toBe(1);\n\texpect(result[1][0].parent).toBe(0);\n\t// this edge order: [3, 1] needs to match order of these indices in\n\t// the second face. 3, then 1.\n\t// expect(result[1][0].edge_vertices[0]).toBe(3);\n\t// expect(result[1][0].edge_vertices[1]).toBe(1);\n});\n\ntest(\"face walk tree, finding an edge match. bad edge formations\", () => {\n\t// good edge\n\tconst result0 = ear.graph.minimumSpanningTrees(ear.graph.makeFacesFaces({\n\t\tedges_vertices: [[1, 3]],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t}))[0];\n\t// expect(result0[1][0].edge_vertices[0]).toBe(3);\n\t// expect(result0[1][0].edge_vertices[1]).toBe(1);\n\n\t// bad edge formation\n\tconst result1 = ear.graph.minimumSpanningTrees(ear.graph.makeFacesFaces({\n\t\tedges_vertices: [[3, 1, 0]],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t}))[0];\n\t// expect(result1[1][0].edge_vertices[0]).toBe(3);\n\t// expect(result1[1][0].edge_vertices[1]).toBe(1);\n\tconst result2 = ear.graph.minimumSpanningTrees(ear.graph.makeFacesFaces({\n\t\tedges_vertices: [[3, 0, 1]],\n\t\tfaces_vertices: [[0, 1, 3], [2, 3, 1]],\n\t}))[0];\n\t// expect(result2[1][0].edge_vertices[0]).toBe(3);\n\t// expect(result2[1][0].edge_vertices[1]).toBe(1);\n});\n\ntest(\"face walk tree, two graphs, joined at a single vertex\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kissing-squares.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst faces_faces = ear.graph.makeFacesFaces(graph);\n\tconst trees1 = ear.graph.minimumSpanningTrees(faces_faces, [0]);\n\tfs.writeFileSync(\n\t\t\"./tests/tmp/disjoint-spanning-trees.fold\",\n\t\tJSON.stringify(trees1),\n\t\t\"utf8\",\n\t);\n\t// all 4 face indices are covered, even if the faces are (edge-) disjoint.\n\texpect(trees1[0][0][0].index).toBe(0);\n\texpect(trees1[0][1][0].index).toBe(1);\n\texpect(trees1[0][1][0].parent).toBe(0);\n\texpect(trees1[1][0][0].index).toBe(2);\n\texpect(trees1[1][1][0].index).toBe(3);\n\texpect(trees1[1][1][0].parent).toBe(2);\n\n\tconst trees2 = ear.graph.minimumSpanningTrees(faces_faces, [3]);\n\t// all 4 face indices are covered, even if the faces are (edge-) disjoint.\n\texpect(trees2[0][0][0].index).toBe(3);\n\texpect(trees2[0][1][0].index).toBe(2);\n\texpect(trees2[0][1][0].parent).toBe(3);\n\texpect(trees2[1][0][0].index).toBe(0);\n\texpect(trees2[1][1][0].index).toBe(1);\n\texpect(trees2[1][1][0].parent).toBe(0);\n});\n"
  },
  {
    "path": "tests/graph.triangulate.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport earcut from \"earcut\";\nimport ear from \"../src/index.js\";\n\nconst convexHexagon = {\n\tvertices_coords: [[0, 0], [1, 1], [1, 2], [0, 3], [-1, 2], [-1, 1]],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0]],\n\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\tfaces_vertices: [[0, 1, 2, 3, 4, 5]],\n};\n\nconst blintz = {\n\tvertices_coords: [[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1], [0.5, 1], [0, 1], [0, 0.5]],\n\tedges_vertices: [\n\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0], [1, 3], [3, 5], [5, 7], [7, 1],\n\t],\n\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"V\", \"V\"],\n\tfaces_vertices: [[1, 3, 5, 7], [0, 1, 7], [2, 3, 1], [4, 5, 3], [6, 7, 5]],\n};\n\ntest(\"triangulate\", () => {\n\tconst fold = JSON.parse(JSON.stringify(convexHexagon));\n\tconst {\n\t\tresult,\n\t\tchanges,\n\t} = ear.graph.triangulate(fold);\n\texpect(result.edges_assignment.length).toBe(9);\n\texpect(result.edges_assignment[6]).toBe(\"J\");\n\texpect(result.edges_assignment[7]).toBe(\"J\");\n\texpect(result.edges_assignment[8]).toBe(\"J\");\n\texpect(result.faces_vertices.length).toBe(4);\n\texpect(result.faces_edges.length).toBe(4);\n\t// all indices in the map will be 0. all came from face 0.\n\texpect(JSON.stringify(changes.faces.map))\n\t\t.toBe(JSON.stringify([[0, 1, 2, 3]]));\n\texpect(JSON.stringify(changes.edges.new))\n\t\t.toBe(JSON.stringify([6, 7, 8]));\n});\n\ntest(\"triangulate with and without earcut 1\", () => {\n\tconst fold1 = JSON.parse(JSON.stringify(blintz));\n\tconst fold2 = JSON.parse(JSON.stringify(blintz));\n\tconst { changes: changes1 } = ear.graph.triangulate(fold1);\n\tconst { changes: changes2 } = ear.graph.triangulate(fold2, earcut);\n\texpect(JSON.stringify(changes1.faces.map))\n\t\t.toBe(JSON.stringify(changes2.faces.map));\n\texpect(changes1.edges.new).toHaveLength(1);\n\texpect(JSON.stringify(changes1.edges.new))\n\t\t.toBe(JSON.stringify(changes2.edges.new));\n});\n\ntest(\"triangulate with and without earcut 2\", () => {\n\tconst bird = JSON.parse(fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\"));\n\tconst fold1 = JSON.parse(JSON.stringify(bird));\n\tconst fold2 = JSON.parse(JSON.stringify(bird));\n\tconst {\n\t\tresult: result1,\n\t\tchanges: changes1,\n\t} = ear.graph.triangulate(fold1);\n\tconst {\n\t\tresult: result2,\n\t\tchanges: changes2,\n\t} = ear.graph.triangulate(fold2, earcut);\n\texpect(JSON.stringify(changes1.faces.map))\n\t\t.toBe(JSON.stringify(changes2.faces.map));\n\texpect(JSON.stringify(changes1.edges.new))\n\t\t.toBe(JSON.stringify(changes2.edges.new));\n\texpect(result1.edges_assignment.filter(a => a === \"J\").length)\n\t\t.toBe(112);\n\texpect(result1.edges_assignment.filter(a => a === \"J\").length)\n\t\t.toBe(result2.edges_assignment.filter(a => a === \"J\").length);\n});\n\ntest(\"triangulate with only faces_vertices\", () => {\n\tconst graph = {\n\t\tfaces_vertices: [[1, 3, 5, 7], [0, 1, 7], [2, 3, 1], [4, 5, 3], [6, 7, 5]],\n\t};\n\tconst { result } = ear.graph.triangulate(graph);\n\texpect(result.faces_edges.length).toBe(result.faces_vertices.length);\n\texpect(result.edges_vertices.length).not.toBe(0);\n});\n\ntest(\"triangulate with only faces_vertices, with earcut\", () => {\n\tconst graph = {\n\t\tfaces_vertices: [[1, 3, 5, 7], [0, 1, 7], [2, 3, 1], [4, 5, 3], [6, 7, 5]],\n\t};\n\tlet err;\n\ttry {\n\t\tear.graph.triangulate(graph, earcut);\n\t} catch (error) {\n\t\terr = error;\n\t}\n\texpect(err).not.toBe(undefined);\n});\n"
  },
  {
    "path": "tests/graph.validate.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"validate, valid\", () => {\n\tconst graphsViolations = [\n\t\t\"disjoint-triangles-3d.fold\",\n\t\t\"two-bird-cp.fold\",\n\t].map(filename => `./tests/files/fold/${filename}`)\n\t\t.map(path => fs.readFileSync(path, \"utf-8\"))\n\t\t.map(contents => JSON.parse(contents))\n\t\t.map(graph => ear.graph.validate(graph));\n\n\tgraphsViolations\n\t\t.forEach(violations => expect(violations.length).toBe(0));\n});\n\ntest(\"validate, contains null at top level\", () => {\n\tconst graph = {\n\t\tvertices_vertices: [[2], undefined, [0]],\n\t}\n\tconst violations = ear.graph.validate(graph);\n\texpect(violations.length).toBe(4);\n});\n\ntest(\"validate, contains null inside arrays\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 1, 0], [1, 1, null], [1, 0, 0]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 0], [undefined, 0]],\n\t\tvertices_faces: [[0, 1, 2]],\n\t}\n\tconst violations = ear.graph.validate(graph);\n\texpect(violations.length).toBe(2);\n});\n\ntest(\"validate, contains null inside arrays, but its okay\", () => {\n\tconst graph = {\n\t\tfaces_faces: [[1, null], [undefined, 0]],\n\t}\n\texpect(ear.graph.validate(graph).length).toBe(0);\n})\n\ntest(\"validate, winding order violations\", () => {\n\tconst graphsViolations = [\n\t\t\"invalid-box-pleat-3d.fold\",\n\t\t\"kissing-squares.fold\",\n\t].map(filename => `./tests/files/fold/${filename}`)\n\t\t.map(path => fs.readFileSync(path, \"utf-8\"))\n\t\t.map(contents => JSON.parse(contents))\n\t\t.map(graph => ear.graph.validate(graph));\n\n\tconst graphsNonWindingViolations = graphsViolations\n\t\t.map(violations => violations\n\t\t\t.filter(str => str.substring(0, 8) !== \"windings\"));\n\n\tgraphsViolations\n\t\t.forEach(violations => expect(violations.length).not.toBe(0));\n\tgraphsNonWindingViolations\n\t\t.forEach(violations => expect(violations.length).toBe(0));\n});\n\ntest(\"validate, invalid array lengths\", () => {\n\tconst fold = fs.readFileSync(\"./tests/files/fold/invalid-mismatch-length.fold\", \"utf-8\");\n\tconst graph = JSON.parse(fold);\n\tconst result = ear.graph.validate(graph);\n\texpect(result.length).toBe(2);\n});\n\ntest(\"validate, invalid references\", () => {\n\tconst fold = fs.readFileSync(\"./tests/files/fold/invalid-mismatch-references.fold\", \"utf-8\");\n\tconst graph = JSON.parse(fold);\n\tconst result = ear.graph.validate(graph);\n\texpect(result.length).toBe(1);\n});\n\ntest(\"validate, contains duplicate faceOrders\", () => {\n\tconst graph = {\n\t\tfaceOrders: [[0, 1, 1], [0, 2, 1], [1, 2, 1], [1, 0, -1]]\n\t}\n\tconst violations = ear.graph.validate(graph);\n\texpect(violations.length).toBe(1);\n});\n\ntest(\"validate, contains poorly formed faceOrders\", () => {\n\tconst graph = {\n\t\tfaceOrders: [[0, 1, 1], [2, 2, 1], [1, 2, 1], [1, 1, -1]]\n\t}\n\tconst violations = ear.graph.validate(graph);\n\texpect(violations.length).toBe(2);\n});\n"
  },
  {
    "path": "tests/graph.vertices.clusters.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"getVerticesClusters no clusters\", () => {\n\tconst graph = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/fan-cp.fold\",\n\t\t\"utf-8\",\n\t));\n\tconst clusters = ear.graph.getVerticesClusters(graph);\n\tclusters.forEach(cluster => expect(cluster.length).toBe(1));\n});\n\ntest(\"getVerticesClusters bird base. clusters\", () => {\n\tconst graph = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/bird-disjoint-edges.fold\",\n\t\t\"utf-8\",\n\t));\n\tconst clusters = ear.graph.getVerticesClusters(graph);\n\t// console.log(clusters.map(c => c.length));\n\t// clusters vary in length, anywhere between 3 to 10\n\tclusters.forEach(cluster => expect(cluster.length).not.toBe(1));\n\tclusters.forEach(cluster => expect(cluster.length > 2).toBe(true));\n});\n\ntest(\"getVerticesClusters bird base\", () => {\n\tconst cp = ear.graph.bird();\n\tconst graph = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tconst clusters_vertices = ear.graph.getVerticesClusters(graph);\n\texpect(clusters_vertices).toMatchObject([\n\t\t[6, 0, 2, 4], // bottom in folded form\n\t\t[5, 14, 1, 7, 13, 3], // 6 points coming together in the inside center axis.\n\t\t[10, 9], // side point in folded form\n\t\t[11, 12], // side point in folded form\n\t\t[8], // center point in cp\n\t]);\n});\n\ntest(\"vertices clusters graphs with holes\", () => {\n\t// both of these have 14 vertices\n\texpect(ear.graph.getVerticesClusters({\n\t\tvertices_coords: [\n\t\t\t[0, 0, 0], [1, 0, 0],, [0, 0, 0], [0, 1, 0], [0, 0, 0], [0, 1, 0], [0, 0, 0],\n\t\t\t[0, 1, 0], [1, 1, 0],, [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 1, 0], [1, 0, 0]\n\t\t],\n\t})).toMatchObject([[0, 3, 5, 7], [4, 6, 8], [1, 11, 13, 15], [9, 12, 14]]);\n\n\texpect(ear.graph.getVerticesClusters({\n\t\tvertices_coords: [, [1, 0, 0], [1, 0, 1],,,,,,, [1, 1, 0], [1, 1, 1], [1, 0, 0], [1, 1, 0],\n\t\t\t[1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1]\n\t\t]\n\t})).toMatchObject([[1, 11, 13, 15], [2, 16, 18, 20], [9, 12, 14], [10, 17, 19]]);\n});\n"
  },
  {
    "path": "tests/graph.vertices.collinear.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// this graph looks like this: (except, square)\n//\n//  o--o--o--o--o--o--o--o--o-----o\n//  |                             |\n//  o-----------------------------o\n//\nconst collinearSquare = {\n\tvertices_coords: [[0, 0], [0.1, 0], [0.2, 0], [0.3, 0], [0.4, 0], [0.5, 0],\n\t\t[0.6, 0], [0.7, 0], [0.8, 0], [1, 0], [1, 1], [0, 1]],\n\tvertices_vertices: [[11, 1], [0, 2], [1, 3], [2, 4], [3, 5], [4, 6],\n\t\t[5, 7], [6, 8], [7, 9], [8, 10], [9, 11], [10, 0]],\n\tvertices_faces: [[0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0]],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7],\n\t\t[7, 8], [8, 9], [9, 10], [10, 11], [11, 0]],\n\tedges_faces: [[0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0]],\n\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\tfaces_vertices: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]],\n\tfaces_edges: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]],\n};\n\ntest(\"isVertexCollinear\", () => {\n\tconst expected = [\n\t\tfalse, true, true, true, true, true, true, true, true, false, false, false,\n\t];\n\tconst isCollinear = collinearSquare.vertices_coords\n\t\t.map((_, i) => ear.graph.isVertexCollinear(collinearSquare, i));\n\texpect(JSON.stringify(expected)).toBe(JSON.stringify(isCollinear));\n});\n\n// test(\"remove collinear vertices\", () => {\n// \tconst graph = {\n// \t\tvertices_coords: [\n// \t\t\t[-1, 1], [-1, 0], [0, 0], [1, 1], [2, 2], [3, 2], [-1, 5], [-1, 4], [-1, 3], [-1, 2],\n// \t\t],\n// \t\tedges_vertices: [\n// \t\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 0],\n// \t\t],\n// \t\tedges_assignment: [\"M\", \"M\", \"M\", \"M\", \"M\", \"M\", \"M\", \"M\", \"M\", \"M\"],\n// \t};\n// \tgraph.vertices_edges = ear.graph.makeVerticesEdgesUnsorted(graph);\n// \t// ear.graph.populate(graph);\n\n// \tconst collinearVertices = graph.vertices_coords\n// \t\t.map((_, i) => (ear.graph.isVertexCollinear(graph, i) ? i : undefined))\n// \t\t.filter(a => a !== undefined)\n// \t\t.sort((a, b) => b - a);\n// \texpect(collinearVertices.length).toBe(5);\n// \texpect(JSON.stringify(collinearVertices)).toBe(JSON.stringify([9, 8, 7, 3, 0]));\n\n// \tcollinearVertices.forEach(v => ear.graph.removeCollinearVertex(graph, v));\n// \tconsole.log(graph);\n// \t// this should have removed the vertex [1, 1]\n// \t// the next vertex should have shifted up by 1\n// \texpect(graph.vertices_coords.length).toBe(5);\n// \texpect(graph.vertices_coords[2][0]).toBe(2);\n// \texpect(graph.vertices_coords[2][1]).toBe(2);\n// });\n"
  },
  {
    "path": "tests/graph.vertices.duplicate.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"duplicateVertices\", () => {\n\tconst graph = JSON.parse(fs.readFileSync(\n\t\t\"./tests/files/fold/bird-disjoint-edges.fold\",\n\t\t\"utf-8\",\n\t));\n\tconst duplicates = ear.graph.duplicateVertices(graph);\n\t// every point in this graph should be duplicate.\n\texpect(duplicates.length).toBe(ear.graph.bird().vertices_coords.length);\n\texpect(duplicates.flat().length).toBe(graph.vertices_coords.length);\n});\n\ntest(\"clusters\", () => {\n\tconst clusters = ear.graph.getVerticesClusters({\n\t\tvertices_coords: [\n\t\t\t[0, 0],\n\t\t\t[1, 1],\n\t\t\t[0.00000000001, 0],\n\t\t\t[0.2, 0.2],\n\t\t\t[0.20000000001, 0.2000000001],\n\t\t\t[0.20000000001, 0.4],\n\t\t],\n\t});\n\texpect(clusters[0][0]).toBe(0);\n\texpect(clusters[0][1]).toBe(2);\n\texpect(clusters[1][0]).toBe(3);\n\texpect(clusters[1][1]).toBe(4);\n\texpect(clusters[2][0]).toBe(5);\n\texpect(clusters[3][0]).toBe(1);\n});\n\ntest(\"clusters, inside epsilon\", () => {\n\tconst epsilon = ear.math.EPSILON * 0.9;\n\tconst clusters = ear.graph.getVerticesClusters({\n\t\tvertices_coords: [\n\t\t\t[0.5, 0.5],\n\t\t\t[0.5, 0.5 + epsilon],\n\t\t\t[0.5, 0.5 + epsilon * 2],\n\t\t\t[0.5, 0.5 + epsilon * 3],\n\t\t\t[0.5, 0.5 + epsilon * 4],\n\t\t\t[0.5, 0.5 + epsilon * 5],\n\t\t\t[0.5, 0.5 + epsilon * 6],\n\t\t],\n\t});\n\texpect(clusters.length).toBe(1);\n\texpect(clusters[0].length).toBe(7);\n});\n\ntest(\"clusters, outside epsilon\", () => {\n\tconst epsilon = ear.math.EPSILON * 1.1;\n\tconst clusters = ear.graph.getVerticesClusters({\n\t\tvertices_coords: [\n\t\t\t[0.5, 0.5],\n\t\t\t[0.5, 0.5 + epsilon],\n\t\t\t[0.5, 0.5 + epsilon * 2],\n\t\t\t[0.5, 0.5 + epsilon * 3],\n\t\t\t[0.5, 0.5 + epsilon * 4],\n\t\t\t[0.5, 0.5 + epsilon * 5],\n\t\t\t[0.5, 0.5 + epsilon * 6],\n\t\t],\n\t});\n\texpect(clusters.length).toBe(7);\n});\n\ntest(\"merge duplicate vertices\", () => {\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[0, 0],\n\t\t\t[1, 1],\n\t\t\t[0.00000000001, 0],\n\t\t\t[0.2, 0.2],\n\t\t\t[0.20000000001, 0.2000000001],\n\t\t\t[0.20000000001, 0.4],\n\t\t],\n\t};\n\tear.graph.removeDuplicateVertices(graph);\n\texpect(graph.vertices_coords.length).toBe(4);\n});\n\ntest(\"merge duplicate vertices, inside epsilon, point averaging\", () => {\n\tconst epsilon = ear.math.EPSILON * 0.9;\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[0.5, 0.5],\n\t\t\t[0.5, 0.5 + epsilon],\n\t\t\t[0.5, 0.5 + epsilon * 2],\n\t\t\t[0.5, 0.5 + epsilon * 3],\n\t\t\t[0.5, 0.5 + epsilon * 4],\n\t\t\t[0.5, 0.5 + epsilon * 5],\n\t\t\t[0.5, 0.5 + epsilon * 6],\n\t\t],\n\t};\n\tear.graph.removeDuplicateVertices(graph);\n\texpect(graph.vertices_coords.length).toBe(1);\n\t// vertices should be averaged together, the y value should be halfway\n\t// between 0 and 6 epsilons\n\texpect(graph.vertices_coords[0][1] > 0.5 + epsilon * 2).toBe(true);\n\texpect(graph.vertices_coords[0][1] < 0.5 + epsilon * 4).toBe(true);\n});\n\ntest(\"merge duplicate vertices, outside epsilon\", () => {\n\tconst epsilon = ear.math.EPSILON * 1.1;\n\tconst graph = {\n\t\tvertices_coords: [\n\t\t\t[0.5, 0.5],\n\t\t\t[0.5, 0.5 + epsilon],\n\t\t\t[0.5, 0.5 + epsilon * 2],\n\t\t\t[0.5, 0.5 + epsilon * 3],\n\t\t\t[0.5, 0.5 + epsilon * 4],\n\t\t\t[0.5, 0.5 + epsilon * 5],\n\t\t\t[0.5, 0.5 + epsilon * 6],\n\t\t],\n\t};\n\tear.graph.removeDuplicateVertices(graph);\n\texpect(graph.vertices_coords.length).toBe(7);\n});\n\n// merge duplicate vertices, graph with both 2D and 3D vertices\n// where the first vertex is a 2D vertex.\n// this graph is considered poorly formed.\n// all vertices will be treated as 2D. and this will remove vertices\n// that aren't actually duplicate. not good but technically correct.\ntest(\"removeDuplicateVertices with 2D and 3D vertices\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [2, 0], [1, 0, 1], [2, 0, -2], [0, 0, 3]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],\n\t};\n\texpect(graph.vertices_coords.length).toBe(6);\n\texpect(graph.edges_vertices.length).toBe(5);\n\tear.graph.removeDuplicateVertices(graph);\n\texpect(graph.vertices_coords.length).toBe(3);\n\texpect(graph.edges_vertices.length).toBe(5);\n});\n"
  },
  {
    "path": "tests/graph.vertices.folded.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst arraysTest = (a, b) => a\n\t.forEach((_, i) => expect(a[i]).toBeCloseTo(b[i]));\n\ntest(\"fold a 3D model\", () => {\n\tconst file = \"bird-base-3d.fold\";\n\t// const file = \"panels.fold\";\n\tconst FOLD = JSON.parse(fs.readFileSync(`./tests/files/fold/${file}`));\n\tconst cp = ear.graph.getFramesByClassName(FOLD, \"creasePattern\")[0];\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(cp);\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords,\n\t};\n\t// console.log(\"folded\", folded);\n\n\texpect(true).toBe(true);\n});\n\ntest(\"fold in 3d\", () => {\n\tconst file = \"bird-base-3d.fold\";\n\t// const file = \"panels.fold\";\n\n\tconst FOLD = JSON.parse(fs.readFileSync(`./tests/files/fold/${file}`));\n\tconst cp = ear.graph.getFramesByClassName(FOLD, \"creasePattern\")[0];\n\n\t// make edge-adjacent faces for every face\n\t// cp.faces_faces = ear.graph.makeFacesFaces(cp);\n\t// pick a starting face, breadth-first walk to every face\n\t// each of these results will relate a face to:\n\t// 1. each parent face in the walk\n\t// 2. which edge it crossed to move from the parent to this face\n\t// const walk = ear.graph.make_face_walk_tree(cp);\n\t// console.log(walk);\n\n\t// everything commented out above is done inside these next few methods\n\n\t// give each face a global transform matrix based on it and its connected face's matrices\n\tconst faces_matrix = ear.graph.makeFacesMatrix(cp);\n\t// console.log(\"faces_matrix\", faces_matrix);\n\n\t// most vertices are a part of a few different faces, it doesn't matter\n\t// which face, just assign each vertex to one of its face's matrices, and\n\t// apply that transform.\n\tconst vertices_faces = ear.graph.makeVerticesFaces(cp);\n\tconst new_vertices_coords = cp.vertices_coords\n\t\t.map((coords, i) => ear.math.multiplyMatrix3Vector3(\n\t\t\tfaces_matrix[vertices_faces[i][0]],\n\t\t\tear.math.resize(3, coords),\n\t\t));\n\n\t// modify the original graph, replace the vertices_coords with the new set.\n\tcp.vertices_coords = new_vertices_coords;\n\n\t// fs.writeFileSync(`./tests/files/folded-${file}`, JSON.stringify(cp));\n\texpect(true).toBe(true);\n});\n\ntest(\"fold a disjoint graph\", () => {\n\tconst file = \"disjoint-triangles-3d.fold\";\n\tconst FOLD = JSON.parse(fs.readFileSync(`./tests/files/fold/${file}`));\n\tconst cp = ear.graph.getFramesByClassName(FOLD, \"creasePattern\")[0];\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(cp);\n\tconst expected = [\n\t\t[0, 0, 0],\n\t\t[1, 0, 1],\n\t\t[1, 1, 0],\n\t\t[1, 0, 0],\n\t\t[0.5, 0, 0],\n\t\t[0.5, 0.5, 0],\n\t\t[0.5, 0, 0.5],\n\t\t[-0.5, 0, 0],\n\t\t[-1.5, 1, 0],\n\t\t[-1.5, 0, 1],\n\t\t[-1.5, 0, 0],\n\t\t[-1, 0, 0],\n\t\t[-1, 0, 0.5],\n\t\t[-1, 0.5, 0],\n\t\t[1.5, 0, 0],\n\t\t[2.5, 0, 1],\n\t\t[2.5, 1, 0],\n\t\t[2.5, 0, 0],\n\t\t[3, 0, 0],\n\t\t[4, 0, 1],\n\t\t[4, 1, 0],\n\t\t[4, 0, 0],\n\t\t[4.5, 0, 0],\n\t\t[5, 0, 0.5],\n\t\t[5, 0.5, 0],\n\t\t[5, 0, 0],\n\t];\n\tvertices_coords\n\t\t.forEach((coords, i) => coords\n\t\t\t.forEach((n, j) => expect(n).toBeCloseTo(expected[i][j])));\n});\n\ntest(\"two graphs joined at a single vertex, folded\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kissing-squares.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoords3DFolded(graph);\n\tconst expected = [\n\t\t[0, 0, 0],\n\t\t[1, 0, 0],\n\t\t[0, 2, 0],\n\t\t[0, 1, 0],\n\t\t[-1, 2, 0],\n\t\t[0, 0, 0],\n\t\t[0, 2, 0],\n\t];\n\texpected\n\t\t.forEach((coords, i) => coords\n\t\t\t.forEach((_, j) => expect(vertices_coords[i][j]).toBeCloseTo(expected[i][j])));\n});\n\ntest(\"two graphs joined at a single vertex, flat-folded\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kissing-squares.fold\", \"utf-8\");\n\tconst graph = JSON.parse(foldfile);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst expected = [\n\t\t[0, 0],\n\t\t[1, 0],\n\t\t[0, 2],\n\t\t[0, 1],\n\t\t[-1, 2],\n\t\t[0, 0],\n\t\t[0, 2],\n\t];\n\texpected\n\t\t.forEach((coords, i) => coords\n\t\t\t.forEach((_, j) => expect(vertices_coords[i][j]).toBeCloseTo(expected[i][j])));\n});\n\nconst fourPanel = {\n\tvertices_coords: [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [3, 1], [2, 1], [1, 1], [0, 1]],\n\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6],\n\t\t[6, 7], [7, 8], [8, 9], [9, 0], [1, 8], [2, 7], [3, 6]],\n\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 90, 90],\n\tfaces_vertices: [[0, 1, 8, 9], [1, 2, 7, 8], [2, 3, 6, 7], [3, 4, 5, 6]],\n};\n\ntest(\"folded vertices_coords\", () => {\n\tconst result = ear.graph.makeVerticesCoordsFolded(fourPanel);\n\t[\n\t\t[0, 0, 0],\n\t\t[1, 0, 0],\n\t\t[1, 0, 1],\n\t\t[0, 0, 1],\n\t\t[0, 0, 0],\n\t\t[0, 1, 0],\n\t\t[0, 1, 1],\n\t\t[1, 1, 1],\n\t\t[1, 1, 0],\n\t\t[0, 1, 0],\n\t].forEach((point, i) => arraysTest(point, result[i]));\n});\n\ntest(\"folded vertices_coords. starting face 1\", () => {\n\tconst result = ear.graph.makeVerticesCoordsFolded(fourPanel, [1]);\n\t[\n\t\t[1, 0, 1],\n\t\t[1, 0, 0],\n\t\t[2, 0, 0],\n\t\t[2, 0, 1],\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t\t[2, 1, 1],\n\t\t[2, 1, 0],\n\t\t[1, 1, 0],\n\t\t[1, 1, 1],\n\t].forEach((point, i) => arraysTest(point, result[i]));\n});\n\ntest(\"folded vertices_coords. starting face 3\", () => {\n\tconst result = ear.graph.makeVerticesCoordsFolded(fourPanel, [3]);\n\t[\n\t\t[4, 0, 0],\n\t\t[4, 0, 1],\n\t\t[3, 0, 1],\n\t\t[3, 0, 0],\n\t\t[4, 0, 0],\n\t\t[4, 1, 0],\n\t\t[3, 1, 0],\n\t\t[3, 1, 1],\n\t\t[4, 1, 1],\n\t\t[4, 1, 0],\n\t].forEach((point, i) => arraysTest(point, result[i]));\n});\n"
  },
  {
    "path": "tests/graph.vertices.isolated.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"isolated vertex\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [0.5, 0.5], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 2], [2, 3], [3, 4], [4, 0]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\"],\n\t\tfaces_vertices: [[0, 2, 3, 4]],\n\t};\n\tear.graph.populate(graph);\n\texpect(graph.vertices_coords.length).toBe(5);\n\texpect(graph.vertices_edges.length).toBe(5);\n\texpect(graph.vertices_faces.length).toBe(5);\n\texpect(graph.vertices_vertices.length).toBe(5);\n\texpect(graph.faces_vertices.length).toBe(1);\n\texpect(graph.faces_edges.length).toBe(1);\n\texpect(graph.faces_faces.length).toBe(1);\n\texpect(JSON.stringify(graph.faces_vertices[0]))\n\t\t.toBe(JSON.stringify([0, 2, 3, 4]));\n\n\tconst res = ear.graph.removeIsolatedVertices(graph);\n\n\texpect(graph.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_edges.length).toBe(4);\n\texpect(graph.vertices_faces.length).toBe(4);\n\texpect(graph.vertices_vertices.length).toBe(4);\n\texpect(JSON.stringify(graph.faces_vertices[0]))\n\t\t.toBe(JSON.stringify([0, 1, 2, 3]));\n\n\texpect(res.remove[0]).toBe(1);\n\texpect(JSON.stringify(res.map))\n\t\t.toBe(JSON.stringify([0, undefined, 1, 2, 3]));\n});\n\ntest(\"isolated vertices\", () => {\n\tconst graph = {\n\t\tfile_spec: 1.1,\n\t\tvertices_coords: [\n\t\t\t[0, 1], [1, 0], [0.2928932188134524, 0.2928932188134526], [0, 0],\n\t\t\t[0.7071067811865475, 0.7071067811865476], [1, 1],\n\t\t],\n\t\tedges_vertices: [[1, 3], [0, 3], [1, 5], [0, 5]],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\"],\n\t\tedges_foldAngle: [0, 0, 0, 0],\n\t\tedges_length: [1, 1, 1, 1],\n\t\tvertices_vertices: [[3, 5], [5, 3], [], [1, 0], [], [1, 0]],\n\t\tfaces_vertices: [[5, 0, 3, 1]],\n\t\tfaces_edges: [[3, 1, 0, 2]],\n\t\tedges_faces: [[0], [0], [0], [0]],\n\t\tvertices_faces: [[0], [0], [], [0], [], [0]],\n\t\tfaces_faces: [[]],\n\t\tvertices_edges: [[1, 3], [0, 2], null, [0, 1], null, [2, 3]],\n\t};\n\n\tconst isolated = ear.graph.isolatedVertices(graph);\n\texpect(isolated.length).toBe(2);\n\texpect(isolated[0]).toBe(2);\n\texpect(isolated[1]).toBe(4);\n});\n"
  },
  {
    "path": "tests/graph.vertices.sort.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst arraysMatch = (a, b) => a.forEach((_, i) => expect(a[i]).toBe(b[i]));\n\ntest(\"sortVerticesCounterClockwise\", () => {\n\tarraysMatch([0, 1, 2], ear.graph.sortVerticesCounterClockwise(\n\t\t{ vertices_coords: [[1, 0], [1, 0.1], [1, -0.1], [0, 0]] },\n\t\t[0, 2, 1],\n\t\t3,\n\t));\n\tarraysMatch([0, 1, 2], ear.graph.sortVerticesCounterClockwise(\n\t\t{ vertices_coords: [[1, 0], [1, 0.1], [1, -0.1], [0, 0]] },\n\t\t[0, 1, 2],\n\t\t3,\n\t));\n\tarraysMatch([0, 2, 1], ear.graph.sortVerticesCounterClockwise(\n\t\t{ vertices_coords: [[1, 0], [1, -0.1], [1, 0.1], [0, 0]] },\n\t\t[0, 1, 2],\n\t\t3,\n\t));\n});\n\ntest(\"sortVerticesAlongVector\", () => {\n\tarraysMatch([2, 1, 0], ear.graph.sortVerticesAlongVector(\n\t\t{ vertices_coords: [[3, 100], [2, -90], [1, 0.01]] },\n\t\t[0, 1, 2],\n\t\t[1, 0],\n\t));\n\tarraysMatch([1, 2, 0], ear.graph.sortVerticesAlongVector(\n\t\t{ vertices_coords: [[3, 100], [-2, 1000000], [2, -90]] },\n\t\t[0, 1, 2],\n\t\t[1, 0],\n\t));\n\tarraysMatch([0, 2, 1], ear.graph.sortVerticesAlongVector(\n\t\t{ vertices_coords: [[3, 100], [-2, 1000000], [2, -90]] },\n\t\t[0, 1, 2],\n\t\t[-1, 0],\n\t));\n\tarraysMatch([2, 0, 1], ear.graph.sortVerticesAlongVector(\n\t\t{ vertices_coords: [[3, 100], [-2, 1000000], [2, -90]] },\n\t\t[0, 1, 2],\n\t\t[0, 1],\n\t));\n});\n"
  },
  {
    "path": "tests/graph.walk.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"simple polygon\", () => {\n\tconst graph = {\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0]],\n\t\tvertices_vertices: [[1, 3], [2, 0], [3, 1], [2, 0]],\n\t\tvertices_sectors: [\n\t\t\t[1.5707963267948966, 4.71238898038469],\n\t\t\t[1.5707963267948966, 4.71238898038469],\n\t\t\t[1.5707963267948966, 4.71238898038469],\n\t\t\t[4.71238898038469, 1.5707963267948966],\n\t\t],\n\t};\n\n\tconst result = ear.graph.walkPlanarFaces(graph);\n\n\t// {\n\t// \tvertices: [0, 1, 2, 3],\n\t// \tedges: [\"0 1\", \"1 2\", \"2 3\", \"3 0\"],\n\t// \tangles: [\n\t// \t\t1.5707963267948966,\n\t// \t\t1.5707963267948966,\n\t// \t\t1.5707963267948966,\n\t// \t\t1.5707963267948966\n\t// \t]\n\t// }\n\texpect(result.length).toBe(2);\n\texpect(JSON.stringify(result[0].vertices)).toBe(JSON.stringify([0, 1, 2, 3]));\n\texpect(JSON.stringify(result[0].edges))\n\t\t.toBe(JSON.stringify([\"0 1\", \"1 2\", \"2 3\", \"3 0\"]));\n});\n\ntest(\"populate building faces (by walking)\", () => {\n\tconst vertices_coords = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]];\n\n\t// graph 1, manually build faces\n\tconst graph1 = {\n\t\tvertices_coords,\n\t\tedges_vertices: vertices_coords\n\t\t\t.map((_, i, arr) => [i, (i + 1) % arr.length]),\n\t\tedges_assignment: Array(vertices_coords.length).fill(\"B\"),\n\t\tfaces_vertices: [vertices_coords.map((_, i) => i)],\n\t\tfaces_edges: [vertices_coords.map((_, i) => i)],\n\t};\n\n\t// graph 2, build faces using populate() which uses walkPlanarFaces\n\tconst graph2 = ear.graph.populate({\n\t\tvertices_coords,\n\t\tedges_vertices: vertices_coords\n\t\t\t.map((_, i, arr) => [i, (i + 1) % arr.length]),\n\t\tedges_assignment: Array(vertices_coords.length).fill(\"B\"),\n\t}, { faces: true });\n\n\texpect(JSON.stringify(graph1.faces_vertices))\n\t\t.toBe(JSON.stringify(graph2.faces_vertices));\n\texpect(JSON.stringify(graph1.faces_edges))\n\t\t.toBe(JSON.stringify(graph2.faces_edges));\n});\n\ntest(\"walkSingleFace\", () => {\n\tconst vertices_vertices = [\n\t\t[1, 4, 3],\n\t\t[2, 4, 0],\n\t\t[3, 4, 1],\n\t\t[0, 4, 2],\n\t\t[0, 1, 2, 3],\n\t];\n\tear.graph.walkSingleFace({ vertices_vertices }, 0, 1)\n\t\t.vertices.forEach((v, i) => expect(v).toBe([0, 1, 4][i]));\n\tear.graph.walkSingleFace({ vertices_vertices }, 1, 2)\n\t\t.vertices.forEach((v, i) => expect(v).toBe([1, 2, 4][i]));\n\tear.graph.walkSingleFace({ vertices_vertices }, 2, 3)\n\t\t.vertices.forEach((v, i) => expect(v).toBe([2, 3, 4][i]));\n\tear.graph.walkSingleFace({ vertices_vertices }, 3, 0)\n\t\t.vertices.forEach((v, i) => expect(v).toBe([3, 0, 4][i]));\n\tear.graph.walkSingleFace({ vertices_vertices }, 0, 3)\n\t\t.vertices.forEach((v, i) => expect(v).toBe([0, 3, 2, 1][i]));\n});\n\ntest(\"walkSingleFace incomplete vertices_vertices\", () => new Promise(done => {\n\tconst vertices_vertices = [\n\t\t[1, 4, 3],\n\t\t[2, 4, 0],\n\t\t[3, 4, 1],\n\t\t[0, 4, 2],\n\t];\n\ttry {\n\t\tear.graph.walkSingleFace({ vertices_vertices }, 0, 1);\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t\tdone();\n\t}\n}));\n\ntest(\"walkSingleFace with weird circular paths\", () => {\n\tconst vertices_vertices = [\n\t\t[1, 2, 3],\n\t\t[2, 3, 0],\n\t\t[3, 0, 1],\n\t\t[0, 1, 2],\n\t];\n\tconst result = ear.graph.walkSingleFace({ vertices_vertices }, 0, 1);\n\tconst expected = [0, 1, 3, 0, 2, 3, 1, 2];\n\tresult.vertices.forEach((v, i) => expect(v).toBe(expected[i]));\n});\n"
  },
  {
    "path": "tests/index.html",
    "content": "<!DOCTYPE html>\n<title> </title>\n<meta charset=\"utf-8\" />\n<style>\n\tbody { background-color: gray; }\n</style>\n<body></body>\n\n<script type=\"module\">\n\timport ear from \"../src/index.js\";\n\twindow.ear = ear;\n</script>\n\n<!-- <script type=\"text/javascript\" src=\"../rabbit-ear.js\"></script> -->\n"
  },
  {
    "path": "tests/layer.constraints3DEdges.edgesFaces.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst solveOverlapFacesWith3DEdge = (graph, epsilon) => {\n\tconst {\n\t\tfaces_plane,\n\t\tfaces_winding,\n\t\tclusters_graph,\n\t} = ear.layer.constraints3DFaceClusters(graph, epsilon);\n\treturn ear.layer.solveOverlapFacesWith3DEdge(\n\t\tgraph,\n\t\tear.layer.getOverlapFacesWith3DEdge(\n\t\t\tgraph,\n\t\t\t{ clusters_graph, faces_plane },\n\t\t\tepsilon,\n\t\t),\n\t\tfaces_winding,\n\t);\n};\n\ntest(\"getOverlapFacesWith3DEdge, layers edge-face\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/layers-3d-edge-face.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst graphsEdgeFace3DOverlaps = foldedForms\n\t\t.map(folded => ear.layer.getOverlapFacesWith3DEdge(\n\t\t\tfolded,\n\t\t\tear.layer.constraints3DFaceClusters(folded),\n\t\t));\n\n\tconst orders = foldedForms\n\t\t.map(folded => solveOverlapFacesWith3DEdge(folded));\n\n\texpect(graphsEdgeFace3DOverlaps).toMatchObject([\n\t\t[{ edge: 6, tortilla: 1, coplanar: 3, angled: 2 }],\n\t\t[{ edge: 6, tortilla: 1, coplanar: 3, angled: 2 }],\n\t\t[],\n\t\t[{ edge: 11, tortilla: 0, coplanar: 3, angled: 2 }],\n\t\t[{ edge: 11, tortilla: 0, coplanar: 3, angled: 2 }],\n\t\t[{ edge: 11, tortilla: 0, coplanar: 3, angled: 2 }],\n\t]);\n\n\texpect(orders).toMatchObject([\n\t\t{ \"1 3\": 2 },\n\t\t{ \"1 3\": 2 },\n\t\t{},\n\t\t// in each of these next cases, face 0 is below face 3, locally,\n\t\t// and because the plane normal is [0, 0, 1], globally as well.\n\t\t{ \"0 3\": 2 },\n\t\t{ \"0 3\": 2 },\n\t\t{ \"0 3\": 2 },\n\t]);\n});\n\ntest(\"getOverlapFacesWith3DEdge, layers edge-edge\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/layers-3d-edge-edge.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst graphsEdgeFace3DOverlaps = foldedForms\n\t\t.map(folded => ear.layer.getOverlapFacesWith3DEdge(\n\t\t\tfolded,\n\t\t\tear.layer.constraints3DFaceClusters(folded),\n\t\t));\n\n\t// all frames overlap edges at edges, none will have a result\n\texpect(graphsEdgeFace3DOverlaps).toMatchObject([\n\t\t[], [], [], [], [], [], [], [], [], [], [],\n\t]);\n});\n\ntest(\"getOverlapFacesWith3DEdge, cube-octagon\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/cube-octagon.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\t// the second folded frame is the one without the flat assignments\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[1];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tplanes,\n\t\t// planes_faces,\n\t\t// planes_transform,\n\t\t// planes_clusters,\n\t\tfaces_winding,\n\t\tfaces_plane,\n\t\t// faces_cluster,\n\t\t// clusters_plane,\n\t\t// clusters_faces,\n\t} = ear.graph.getCoplanarAdjacentOverlappingFaces(folded);\n\n\tconst edgeFace3DOverlaps = ear.layer.getOverlapFacesWith3DEdge(\n\t\tfolded,\n\t\tear.layer.constraints3DFaceClusters(folded),\n\t);\n\tconst orders = solveOverlapFacesWith3DEdge(folded);\n\n\texpect(edgeFace3DOverlaps).toHaveLength(20);\n\texpect(Object.keys(orders)).toHaveLength(20);\n\n\t// inside tortillas: 18, 23, 24, 26\n\t// these all have only one 3D tortilla constraint, with\n\t// 18: (19-23), 23: (34-26) ...\n\t// then four tortillas: 3, 11, 17, 28 all have four instances\n\t// three of which are bundled up in the corner, 3 non flat edges,\n\t// and the last is the big face from the neighboring counter-clockwise group\n\t// everything here checks out.\n\texpect(edgeFace3DOverlaps).toMatchObject([\n\t\t{ edge: 32, tortilla: 26, coplanar: 25, angled: 24 },\n\t\t{ edge: 42, tortilla: 18, coplanar: 19, angled: 23 },\n\t\t{ edge: 24, tortilla: 3, coplanar: 35, angled: 34 },\n\t\t{ edge: 25, tortilla: 3, coplanar: 36, angled: 7 },\n\t\t{ edge: 51, tortilla: 3, coplanar: 28, angled: 23 },\n\t\t{ edge: 52, tortilla: 3, coplanar: 6, angled: 5 },\n\t\t{ edge: 20, tortilla: 11, coplanar: 27, angled: 25 },\n\t\t{ edge: 21, tortilla: 11, coplanar: 2, angled: 0 },\n\t\t{ edge: 22, tortilla: 11, coplanar: 13, angled: 1 },\n\t\t{ edge: 23, tortilla: 11, coplanar: 3, angled: 26 },\n\t\t{ edge: 18, tortilla: 17, coplanar: 30, angled: 9 },\n\t\t{ edge: 19, tortilla: 17, coplanar: 11, angled: 24 },\n\t\t{ edge: 53, tortilla: 17, coplanar: 10, angled: 8 },\n\t\t{ edge: 54, tortilla: 17, coplanar: 32, angled: 33 },\n\t\t{ edge: 49, tortilla: 28, coplanar: 16, angled: 14 },\n\t\t{ edge: 50, tortilla: 28, coplanar: 20, angled: 19 },\n\t\t{ edge: 55, tortilla: 28, coplanar: 17, angled: 18 },\n\t\t{ edge: 56, tortilla: 28, coplanar: 21, angled: 15 },\n\t\t{ edge: 30, tortilla: 24, coplanar: 33, angled: 18 },\n\t\t{ edge: 44, tortilla: 23, coplanar: 34, angled: 26 },\n\t]);\n\n\texpect(planes).toMatchObject([\n\t\t{ normal: [0, 1, 0], origin: [0, 2, 0] },\n\t\t{ normal: [0, 1, 0], origin: [0, 3, 0] },\n\t\t{ normal: [0, 0, -1], origin: [0, 0, -0] },\n\t\t{ normal: [0, 0, -1], origin: [0, 0, -1] },\n\t\t{ normal: [1, 0, 0], origin: [2, 0, 0] },\n\t\t{ normal: [1, 0, 0], origin: [3, 0, 0] },\n\t]);\n\n\texpect(faces_plane[3]).toBe(3);\n\texpect(faces_plane[11]).toBe(3);\n\texpect(faces_plane[17]).toBe(3);\n\texpect(faces_plane[28]).toBe(3);\n\n\t// to ensure that the orders (1, 2) are correct.\n\texpect(faces_winding[3]).toBe(false);\n\texpect(faces_winding[11]).toBe(false);\n\texpect(faces_winding[17]).toBe(false);\n\texpect(faces_winding[28]).toBe(false);\n\n\texpect(orders).toMatchObject({\n\t\t// the four inner most layer orders\n\t\t\"25 26\": 1,\n\t\t\"18 19\": 1,\n\t\t\"24 33\": 2,\n\t\t\"23 34\": 1,\n\n\t\t// the four tortillas (3, 11, 17, 28) paired with themselves\n\t\t// apparently, 3 appears to be below 28 and both have \"true\" winding\n\t\t// however, the plane's normal is [0, 0, -1], all data is flipped, so,\n\t\t// in reality, 3 is above 28, and both have \"false\" winding\n\t\t\"3 28\": 1, // 3 is above 28, even though from above, 3 appears to be below 28\n\t\t\"17 28\": 2, // 17 is below 28\n\t\t\"11 17\": 2, // 11 is below 17\n\t\t\"3 11\": 2, // 3 is below 11\n\n\t\t// tortilla 3\n\t\t\"3 35\": 1,\n\t\t\"3 36\": 1,\n\t\t\"3 6\": 1,\n\n\t\t// tortilla 11\n\t\t\"11 27\": 1,\n\t\t\"2 11\": 2,\n\t\t\"11 13\": 1,\n\n\t\t// tortilla 17\n\t\t\"17 30\": 1,\n\t\t\"10 17\": 2,\n\t\t\"17 32\": 1,\n\n\t\t// tortilla 28\n\t\t\"16 28\": 2,\n\t\t\"20 28\": 2,\n\t\t\"21 28\": 2,\n\t});\n});\n\ntest(\"getOverlapFacesWith3DEdge, maze-u\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/maze-u.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst edgeFace3DOverlaps = ear.layer.getOverlapFacesWith3DEdge(\n\t\tfolded,\n\t\tear.layer.constraints3DFaceClusters(folded),\n\t);\n\n\tconst orders = solveOverlapFacesWith3DEdge(folded);\n\n\texpect(edgeFace3DOverlaps).toHaveLength(64);\n\texpect(Object.keys(orders)).toHaveLength(60);\n\texpect(edgeFace3DOverlaps).toMatchObject([\n\t\t{ edge: 39, tortilla: 17, coplanar: 6, angled: 10 }, // dup A\n\t\t{ edge: 58, tortilla: 17, coplanar: 4, angled: 31 },\n\t\t{ edge: 59, tortilla: 17, coplanar: 5, angled: 12 },\n\t\t{ edge: 60, tortilla: 17, coplanar: 11, angled: 13 },\n\t\t{ edge: 61, tortilla: 17, coplanar: 16, angled: 14 },\n\t\t{ edge: 62, tortilla: 17, coplanar: 18, angled: 15 },\n\t\t{ edge: 67, tortilla: 17, coplanar: 18, angled: 21 },\n\t\t{ edge: 68, tortilla: 17, coplanar: 22, angled: 23 },\n\t\t{ edge: 69, tortilla: 17, coplanar: 26, angled: 28 },\n\t\t{ edge: 70, tortilla: 17, coplanar: 30, angled: 29 },\n\t\t{ edge: 71, tortilla: 17, coplanar: 35, angled: 33 },\n\t\t{ edge: 72, tortilla: 17, coplanar: 6, angled: 7 }, // dup A\n\t\t{ edge: 91, tortilla: 42, coplanar: 40, angled: 47 },\n\t\t{ edge: 92, tortilla: 42, coplanar: 39, angled: 46 },\n\t\t{ edge: 93, tortilla: 42, coplanar: 48, angled: 49 },\n\t\t{ edge: 94, tortilla: 42, coplanar: 4, angled: 31 },\n\t\t{ edge: 108, tortilla: 42, coplanar: 57, angled: 60 },\n\t\t{ edge: 109, tortilla: 42, coplanar: 58, angled: 61 },\n\t\t{ edge: 110, tortilla: 42, coplanar: 62, angled: 50 },\n\t\t{ edge: 111, tortilla: 42, coplanar: 6, angled: 7 },\n\t\t{ edge: 123, tortilla: 42, coplanar: 43, angled: 44 },\n\t\t{ edge: 124, tortilla: 42, coplanar: 41, angled: 45 },\n\t\t{ edge: 91, tortilla: 51, coplanar: 40, angled: 47 },\n\t\t{ edge: 92, tortilla: 51, coplanar: 39, angled: 46 },\n\t\t{ edge: 93, tortilla: 51, coplanar: 48, angled: 49 },\n\t\t{ edge: 94, tortilla: 51, coplanar: 4, angled: 31 },\n\t\t{ edge: 108, tortilla: 51, coplanar: 57, angled: 60 },\n\t\t{ edge: 109, tortilla: 51, coplanar: 58, angled: 61 },\n\t\t{ edge: 110, tortilla: 51, coplanar: 62, angled: 50 },\n\t\t{ edge: 111, tortilla: 51, coplanar: 6, angled: 7 },\n\t\t{ edge: 123, tortilla: 51, coplanar: 43, angled: 44 },\n\t\t{ edge: 124, tortilla: 51, coplanar: 41, angled: 45 },\n\t\t{ edge: 134, tortilla: 74, coplanar: 6, angled: 64 }, // dup B\n\t\t{ edge: 153, tortilla: 74, coplanar: 35, angled: 33 },\n\t\t{ edge: 154, tortilla: 74, coplanar: 63, angled: 69 },\n\t\t{ edge: 155, tortilla: 74, coplanar: 67, angled: 70 },\n\t\t{ edge: 156, tortilla: 74, coplanar: 73, angled: 71 },\n\t\t{ edge: 157, tortilla: 74, coplanar: 72, angled: 68 },\n\t\t{ edge: 162, tortilla: 74, coplanar: 72, angled: 78 },\n\t\t{ edge: 163, tortilla: 74, coplanar: 79, angled: 77 },\n\t\t{ edge: 164, tortilla: 74, coplanar: 82, angled: 85 },\n\t\t{ edge: 165, tortilla: 74, coplanar: 86, angled: 84 },\n\t\t{ edge: 166, tortilla: 74, coplanar: 90, angled: 87 },\n\t\t{ edge: 167, tortilla: 74, coplanar: 6, angled: 10 }, // dup B\n\t\t{ edge: 186, tortilla: 93, coplanar: 59, angled: 98 },\n\t\t{ edge: 187, tortilla: 93, coplanar: 58, angled: 97 },\n\t\t{ edge: 188, tortilla: 93, coplanar: 62, angled: 99 },\n\t\t{ edge: 189, tortilla: 93, coplanar: 6, angled: 64 },\n\t\t{ edge: 203, tortilla: 93, coplanar: 105, angled: 107 },\n\t\t{ edge: 204, tortilla: 93, coplanar: 106, angled: 108 },\n\t\t{ edge: 205, tortilla: 93, coplanar: 109, angled: 100 },\n\t\t{ edge: 206, tortilla: 93, coplanar: 90, angled: 87 },\n\t\t{ edge: 218, tortilla: 93, coplanar: 94, angled: 95 },\n\t\t{ edge: 219, tortilla: 93, coplanar: 92, angled: 96 },\n\t\t{ edge: 186, tortilla: 101, coplanar: 59, angled: 98 },\n\t\t{ edge: 187, tortilla: 101, coplanar: 58, angled: 97 },\n\t\t{ edge: 188, tortilla: 101, coplanar: 62, angled: 99 },\n\t\t{ edge: 189, tortilla: 101, coplanar: 6, angled: 64 },\n\t\t{ edge: 203, tortilla: 101, coplanar: 105, angled: 107 },\n\t\t{ edge: 204, tortilla: 101, coplanar: 106, angled: 108 },\n\t\t{ edge: 205, tortilla: 101, coplanar: 109, angled: 100 },\n\t\t{ edge: 206, tortilla: 101, coplanar: 90, angled: 87 },\n\t\t{ edge: 218, tortilla: 101, coplanar: 94, angled: 95 },\n\t\t{ edge: 219, tortilla: 101, coplanar: 92, angled: 96 },\n\t]);\n\texpect(orders).toMatchObject({\n\t\t\"6 17\":1,\"4 17\":1,\"5 17\":1,\"11 17\":1,\"16 17\":1,\"17 18\":2,\"17 22\":2,\"17 26\":2,\"17 30\":2,\"17 35\":2,\"40 42\":1,\"39 42\":1,\"42 48\":2,\"4 42\":1,\"42 57\":2,\"42 58\":2,\"42 62\":2,\"6 42\":1,\"42 43\":2,\"41 42\":1,\"40 51\":1,\"39 51\":1,\"48 51\":1,\"4 51\":1,\"51 57\":2,\"51 58\":2,\"51 62\":2,\"6 51\":1,\"43 51\":1,\"41 51\":1,\"6 74\":1,\"35 74\":1,\"63 74\":1,\"67 74\":1,\"73 74\":1,\"72 74\":1,\"74 79\":2,\"74 82\":2,\"74 86\":2,\"74 90\":2,\"59 93\":1,\"58 93\":1,\"62 93\":1,\"6 93\":1,\"93 105\":2,\"93 106\":2,\"93 109\":2,\"90 93\":1,\"93 94\":2,\"92 93\":1,\"59 101\":1,\"58 101\":1,\"62 101\":1,\"6 101\":1,\"101 105\":2,\"101 106\":2,\"101 109\":2,\"90 101\":1,\"94 101\":1,\"92 101\":1\n\t});\n});\n\ntest(\"getOverlapFacesWith3DEdge, Mooser's train\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst folded = JSON.parse(FOLD);\n\tear.graph.populate(folded);\n\n\tconst epsilon = 1e-3;\n\n\tconst facesEdges3D = ear.layer.getOverlapFacesWith3DEdge(\n\t\tfolded,\n\t\tear.layer.constraints3DFaceClusters(folded, epsilon),\n\t\tepsilon,\n\t);\n\tconst orders = solveOverlapFacesWith3DEdge(folded, epsilon);\n\n\texpect(facesEdges3D).toHaveLength(677);\n\texpect(Object.keys(orders)).toHaveLength(665);\n});\n"
  },
  {
    "path": "tests/layer.constraints3DEdges.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// call ear.layer.getSolvable3DEdgePairs. prepare all necessary data to do so\nconst getSolvable3DEdgePairs = (graph) => {\n\tconst {\n\t\tfaces_plane,\n\t\tfacesFacesOverlap,\n\t} = ear.layer.constraints3DFaceClusters(graph);\n\n\tconst edges_vertices2 = graph.edges_vertices.slice();\n\tgraph.edges_faces\n\t\t.map((_, e) => e)\n\t\t.filter(e => graph.edges_faces[e].length !== 2)\n\t\t.forEach(e => delete edges_vertices2[e]);\n\n\tconst edgesEdgesOverlap = ear.graph.getEdgesEdgesCollinearOverlap({\n\t\tvertices_coords: graph.vertices_coords, edges_vertices: edges_vertices2,\n\t});\n\tconst edgePairs = ear.graph.connectedComponentsPairs(edgesEdgesOverlap);\n\tconst facesFacesLookup = ear.general.arrayArrayToLookupArray(facesFacesOverlap);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = ear.layer.getSolvable3DEdgePairs({\n\t\tedges_faces: graph.edges_faces,\n\t\tfaces_plane,\n\t\tedgePairs,\n\t\tfacesFacesLookup,\n\t});\n\n\treturn {\n\t\ttJunctions: tJunctions.map(i => edgePairs[i]),\n\t\tyJunctions: yJunctions.map(i => edgePairs[i]),\n\t\tbentFlatTortillas: bentFlatTortillas.map(i => edgePairs[i]),\n\t\tbentTortillas: bentTortillas.map(i => edgePairs[i]),\n\t\tbentTortillasFlatTaco: bentTortillasFlatTaco.map(i => edgePairs[i]),\n\t};\n};\n\n// call ear.layer.constraints3DEdges. prepare all necessary data to do so\nconst constraints3DEdges = (graph) => {\n\ttry {\n\t\tconst faceClusters = ear.layer.constraints3DFaceClusters(graph);\n\t\treturn {\n\t\t\t...faceClusters,\n\t\t\t...ear.layer.constraints3DEdges(graph, {\n\t\t\t\t...faceClusters,\n\t\t\t\tedgesEdgesOverlap: ear.graph.getEdgesEdgesCollinearOverlap(graph),\n\t\t\t}),\n\t\t};\n\t} catch (error) {\n\t\treturn \"error\";\n\t}\n};\n\ntest(\"getSolvable3DEdgePairs, layer 3D special cases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-edge.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\tconst results = foldedForms.map(getSolvable3DEdgePairs);\n\n\texpect(results[0]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [[11, 14]],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[1]).toMatchObject({\n\t\ttJunctions: [[13, 17]],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[2]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [[13, 17]],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[3]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [[13, 17]],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[4]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [[13, 17]],\n\t});\n\n\texpect(results[5]).toMatchObject({\n\t\ttJunctions: [[23, 32]],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [[26, 29]],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[6]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [[25, 35]],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [[27, 33]],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[7]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [[15, 20], [16, 19]],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[8]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[9]).toMatchObject({\n\t\ttJunctions: [[13, 17]],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\texpect(results[10]).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [[13, 17]],\n\t\tbentFlatTortillas: [],\n\t\tbentTortillas: [],\n\t\tbentTortillasFlatTaco: [],\n\t});\n});\n\ntest(\"constraints3DEdges, layer 3D special cases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-edge.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst results = foldedForms.map(constraints3DEdges);\n\n\t// one flat tortilla-tortilla on top of a bent tortilla-tortilla\n\t// this solves faces 0-4\n\texpect(results[0]).toMatchObject({\n\t\tfaces_winding: [true, true, true, true, false],\n\t\tfacesFacesOverlap: [[4], [], [], [], [0]],\n\t\tfacePairs: [\"0 4\"],\n\t\torders: { \"0 4\": 2 }, // 1 or 2?\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// a T-junction where the top T faces are coplanar\n\t// this solves faces 1-4\n\texpect(results[1]).toMatchObject({\n\t\tfaces_winding: [true, true, true, false, false, true],\n\t\tfacesFacesOverlap: [[], [4], [3], [2], [1], []],\n\t\tfacePairs: [\"1 4\", \"2 3\"],\n\t\torders: { \"1 4\": 2 },\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// an Y-junction where the top two faces are not coplanar\n\t// this solves faces 1-4\n\texpect(results[2]).toMatchObject({\n\t\tfaces_winding: [true, true, true, false, false, true],\n\t\tfacesFacesOverlap: [[], [4], [3], [2], [1], []],\n\t\tfacePairs: [\"1 4\", \"2 3\"],\n\t\torders: { \"1 4\": 2 },\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// a bent-tortilla-tortilla (next to a flat tortilla-tortilla)\n\t// no orders are solved, but bentTortillaTortillas condition is generated.\n\texpect(results[3]).toMatchObject({\n\t\tfaces_winding: [true, true, true, false, false, false],\n\t\tfacesFacesOverlap: [[5], [4], [3], [2], [1], [0]],\n\t\tfacePairs: [\"0 5\", \"1 4\", \"2 3\"],\n\t\torders: {},\n\t\ttortilla_tortilla: [[0, 1, 5, 4]],\n\t\ttaco_tortilla: [],\n\t});\n\n\texpect(results[4]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, false, false, true, true, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[4, 1], [0, 4], [3], [2], [0, 1], [],\n\t\t],\n\t\tfacePairs: [\"0 4\", \"0 1\", \"1 4\", \"2 3\"],\n\t\torders: {},\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [[0, 4, 1]],\n\t});\n\n\t// contains two: an bent-tortilla-tortilla and a T-junction.\n\texpect(results[5]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, true, true, true, true, true, true, false, false, false, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[], [9], [8], [7], [], [], [], [3], [2], [1], [],\n\t\t],\n\t\tfacePairs: [\"1 9\", \"2 8\", \"3 7\"],\n\t\torders: { \"1 9\": 1, \"3 7\": 1 },\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// contains two: a bent-flat-tortilla and an Y-junction\n\texpect(results[6]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, true, true, true, true, false, true, false, false, false, false, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[], [10], [9], [8], [7, 5, 6], [4, 7, 6], [4, 5, 7], [4, 5, 6], [3], [2], [1], [],\n\t\t],\n\t\tfacePairs: [\"1 10\", \"2 9\", \"3 8\", \"4 7\", \"4 5\", \"4 6\", \"5 7\", \"5 6\", \"6 7\"],\n\t\torders: { \"1 10\": 1 },\n\t\ttortilla_tortilla: [[2, 3, 9, 8]],\n\t\ttaco_tortilla: [],\n\t});\n\n\texpect(results[7]).toMatchObject(\"error\");\n\n\t// expect(results[8]).toMatchObject(\"error\");\n\texpect(results[8]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, true, true, true, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[], [], [], [], [],\n\t\t],\n\t\tfacePairs: [],\n\t\torders: {},\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// expect(results[9]).toMatchObject(\"error\");\n\texpect(results[9]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, true, true, false, false, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[], [4], [3], [2], [1], [],\n\t\t],\n\t\tfacePairs: [\"1 4\", \"2 3\"],\n\t\torders: { \"1 4\": 1 },\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n\n\t// expect(results[10]).toMatchObject(\"error\");\n\texpect(results[10]).toMatchObject({\n\t\tfaces_winding: [\n\t\t\ttrue, true, true, false, false, true,\n\t\t],\n\t\tfacesFacesOverlap: [\n\t\t\t[], [4], [3], [2], [1], [],\n\t\t],\n\t\tfacePairs: [\"1 4\", \"2 3\"],\n\t\torders: { \"1 4\": 1 },\n\t\ttortilla_tortilla: [],\n\t\ttaco_tortilla: [],\n\t});\n});\n\ntest(\"constraints3DEdges, maze-u\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\tclusters_faces,\n\t\tclusters_graph,\n\t\tclusters_transform,\n\t\t// facesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs(folded);\n\n\texpect(facePairs).toHaveLength(467);\n\t// expect(pairs_data).toHaveLength(152);\n\texpect(clusters_faces).toHaveLength(4);\n\texpect(clusters_graph).toHaveLength(4);\n\texpect(clusters_transform).toHaveLength(4);\n\texpect(yJunctions).toHaveLength(0);\n\texpect(tJunctions).toHaveLength(82);\n\texpect(bentFlatTortillas).toHaveLength(0);\n\texpect(bentTortillas).toHaveLength(70);\n\texpect(bentTortillasFlatTaco).toHaveLength(16);\n\n\texpect(yJunctions).toMatchObject([]);\n\n\texpect(tJunctions).toMatchObject([\n\t\t[39, 68], [39, 69], [39, 70], [39, 71], [58, 62], [58, 72], [59, 62], [59, 72], [60, 62], [60, 72], [61, 62], [61, 72], [67, 68], [67, 69], [67, 70], [67, 71], [91, 108], [91, 109], [91, 110], [91, 111], [91, 124], [92, 108], [92, 109], [92, 110], [92, 111], [92, 124], [93, 108], [93, 109], [93, 110], [93, 111], [93, 124], [94, 108], [94, 109], [94, 110], [94, 111], [94, 124], [108, 123], [109, 123], [110, 123], [111, 123], [123, 124], [134, 163], [134, 164], [134, 165], [134, 166], [153, 157], [153, 167], [154, 157], [154, 167], [155, 157], [155, 167], [156, 157], [156, 167], [162, 163], [162, 164], [162, 165], [162, 166], [186, 203], [186, 204], [186, 205], [186, 206], [186, 219], [187, 203], [187, 204], [187, 205], [187, 206], [187, 219], [188, 203], [188, 204], [188, 205], [188, 206], [188, 219], [189, 203], [189, 204], [189, 205], [189, 206], [189, 219], [203, 218], [204, 218], [205, 218], [206, 218], [218, 219],\n\t]);\n\n\texpect(bentTortillas).toMatchObject([\n\t\t[39, 67], [58, 59], [58, 60], [58, 61], [59, 60], [59, 61], [60, 61], [62, 72], [63, 73], [68, 69], [68, 70], [68, 71], [69, 70], [69, 71], [70, 71], [91, 92], [91, 93], [91, 94], [91, 123], [92, 93], [92, 94], [92, 123], [93, 94], [93, 123], [94, 123], [108, 109], [108, 110], [108, 111], [108, 124], [109, 110], [109, 111], [109, 124], [110, 111], [110, 124], [111, 124], [134, 162], [153, 154], [153, 155], [153, 156], [154, 155], [154, 156], [155, 156], [157, 167], [158, 168], [163, 164], [163, 165], [163, 166], [164, 165], [164, 166], [165, 166], [186, 187], [186, 188], [186, 189], [186, 218], [187, 188], [187, 189], [187, 218], [188, 189], [188, 218], [189, 218], [203, 204], [203, 205], [203, 206], [203, 219], [204, 205], [204, 206], [204, 219], [205, 206], [205, 219], [206, 219],\n\t]);\n\n\texpect(orders).toMatchObject({\n\t\t\"10 23\": 1, \"10 28\": 1, \"10 29\": 1, \"10 33\": 1, \"15 31\": 1, \"7 31\": 1,\n\t\t\"12 15\": 2, \"7 12\": 1, \"13 15\": 2, \"7 13\": 1, \"14 15\": 2, \"7 14\": 1,\n\t\t\"21 23\": 1, \"21 28\": 1, \"21 29\": 1, \"21 33\": 1, \"47 60\": 2, \"47 61\": 2,\n\t\t\"47 50\": 2, \"7 47\": 1, \"45 47\": 1, \"46 60\": 2, \"46 61\": 2, \"46 50\": 2,\n\t\t\"7 46\": 1, \"45 46\": 1, \"49 60\": 2, \"49 61\": 2, \"49 50\": 2, \"7 49\": 1,\n\t\t\"45 49\": 1, \"31 60\": 2, \"31 61\": 2, \"31 50\": 2, \"31 45\": 2, \"44 60\": 2,\n\t\t\"44 61\": 2, \"44 50\": 2, \"7 44\": 1, \"44 45\": 2, \"64 77\": 2, \"64 85\": 2,\n\t\t\"64 84\": 2, \"64 87\": 2, \"33 68\": 2, \"68 69\": 1, \"10 69\": 1, \"68 70\": 1,\n\t\t\"10 70\": 1, \"68 71\": 1, \"10 71\": 1, \"77 78\": 1, \"78 85\": 2, \"78 84\": 2,\n\t\t\"78 87\": 2, \"98 107\": 2, \"98 108\": 2, \"98 100\": 2, \"87 98\": 1, \"96 98\": 1,\n\t\t\"97 107\": 2, \"97 108\": 2, \"97 100\": 2, \"87 97\": 1, \"96 97\": 1, \"99 107\": 2,\n\t\t\"99 108\": 2, \"99 100\": 2, \"87 99\": 1, \"96 99\": 1, \"64 107\": 2, \"64 108\": 2,\n\t\t\"64 100\": 2, \"64 96\": 2, \"95 107\": 2, \"95 108\": 2, \"95 100\": 2, \"87 95\": 1,\n\t\t\"95 96\": 2,\n\t});\n\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[6,10,18,21],[4,12,5,31],[4,13,11,31],[4,14,16,31],[5,13,11,12],[5,14,16,12],[11,14,16,13],[15,18,7,6],[21,32,10,9],[22,28,26,23],[22,29,30,23],[22,33,35,23],[26,29,30,28],[26,33,35,28],[29,35,33,30],[40,46,39,47],[40,49,48,47],[40,31,4,47],[40,44,43,47],[39,49,48,46],[39,31,4,46],[39,44,43,46],[48,31,4,49],[48,44,43,49],[4,44,43,31],[57,60,58,61],[57,60,62,50],[57,60,6,7],[57,60,41,45],[58,61,62,50],[58,61,6,7],[58,61,41,45],[50,62,7,6],[50,62,45,41],[6,7,41,45],[6,78,72,64],[33,63,69,35],[33,67,70,35],[33,73,71,35],[63,70,67,69],[63,71,73,69],[67,71,73,70],[68,72,10,6],[78,66,64,88],[77,79,85,82],[77,79,84,86],[77,79,87,90],[82,85,86,84],[82,85,90,87],[84,86,87,90],[59,97,58,98],[59,99,62,98],[59,64,6,98],[59,95,94,98],[58,99,62,97],[58,64,6,97],[58,95,94,97],[62,64,6,99],[62,95,94,99],[6,95,94,64],[105,107,106,108],[105,107,109,100],[105,107,90,87],[105,107,92,96],[106,108,109,100],[106,108,90,87],[106,108,92,96],[100,109,87,90],[100,109,96,92],[87,90,96,92]\n\t]);\n\n\texpect(taco_tortilla).toMatchObject([\n\t\t[12, 32, 31], [12, 9, 31], [7, 32, 8], [7, 9, 8], [15, 32, 34],\n\t\t[29, 21, 33], [15, 9, 34], [29, 10, 33], [33, 88, 69],\n\t\t[33, 66, 69], [10, 88, 65], [10, 66, 65], [68, 88, 89],\n\t\t[84, 78, 87], [68, 66, 89], [84, 64, 87],\n\t]);\n});\n\ntest(\"constraints3DEdges, Mooser's train, carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t};\n\tear.graph.populate(folded);\n\n\t// face cluster stuff is test in other files: graph.faces.planes\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\t// clusters_faces,\n\t\t// clusters_graph,\n\t\t// clusters_transform,\n\t\t// facesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs(folded);\n\n\texpect(facePairs).toHaveLength(440);\n\texpect(yJunctions).toHaveLength(0);\n\texpect(tJunctions).toHaveLength(40);\n\texpect(bentFlatTortillas).toHaveLength(0);\n\texpect(bentTortillas).toHaveLength(80);\n\texpect(bentTortillasFlatTaco).toHaveLength(68);\n\texpect(tortilla_tortilla).toHaveLength(80);\n\texpect(taco_tortilla).toHaveLength(68);\n\texpect(Object.keys(orders)).toHaveLength(40);\n\n\texpect(tJunctions).toMatchObject([\n\t\t[32, 35], [32, 36], [33, 35], [33, 36], [34, 35], [34, 36], [35, 37],\n\t\t[35, 38], [36, 37], [36, 38], [38, 40], [38, 41], [39, 40], [39, 41],\n\t\t[40, 42], [40, 43], [40, 44], [41, 42], [41, 43], [41, 44], [244, 247],\n\t\t[244, 248], [245, 247], [245, 248], [246, 247], [246, 248], [247, 249],\n\t\t[247, 250], [248, 249], [248, 250], [250, 252], [250, 253], [251, 252],\n\t\t[251, 253], [252, 254], [252, 255], [252, 256], [253, 254], [253, 255],\n\t\t[253, 256],\n\t]);\n\n\texpect(bentTortillas).toMatchObject([\n\t\t[32, 33], [32, 34], [32, 37], [32, 38], [33, 34], [33, 37], [33, 38],\n\t\t[34, 37], [34, 38], [35, 36], [37, 38], [38, 39], [38, 42], [38, 43],\n\t\t[38, 44], [39, 42], [39, 43], [39, 44], [40, 41], [42, 43], [42, 44],\n\t\t[43, 44], [49, 196], [51, 169], [52, 134], [54, 101], [55, 56], [57, 58],\n\t\t[57, 162], [58, 162], [60, 61], [60, 138], [61, 138], [62, 63], [79, 80],\n\t\t[79, 81], [80, 81], [81, 82], [81, 83], [82, 83], [102, 243], [135, 241],\n\t\t[140, 222], [140, 223], [164, 219], [164, 220], [170, 240], [197, 238],\n\t\t[202, 203], [202, 204], [203, 204], [204, 205], [204, 206], [205, 206],\n\t\t[217, 218], [219, 220], [222, 223], [224, 225], [244, 245], [244, 246],\n\t\t[244, 249], [244, 250], [245, 246], [245, 249], [245, 250], [246, 249],\n\t\t[246, 250], [247, 248], [249, 250], [250, 251], [250, 254], [250, 255],\n\t\t[250, 256], [251, 254], [251, 255], [251, 256], [252, 253], [254, 255],\n\t\t[254, 256], [255, 256],\n\t]);\n\n\texpect(bentTortillasFlatTaco).toMatchObject([\n\t\t[55, 59], [56, 59], [57, 118], [57, 161], [57, 183], [57, 207], [58, 118],\n\t\t[58, 161], [58, 183], [58, 207], [59, 62], [59, 63], [60, 84], [60, 115],\n\t\t[60, 120], [60, 137], [61, 84], [61, 115], [61, 120], [61, 137], [78, 140],\n\t\t[78, 222], [78, 223], [79, 189], [80, 189], [81, 121], [81, 189], [82, 121],\n\t\t[83, 121], [84, 138], [112, 140], [112, 222], [112, 223], [115, 138],\n\t\t[117, 204], [117, 205], [117, 206], [118, 162], [120, 138], [137, 138],\n\t\t[140, 141], [140, 188], [141, 222], [141, 223], [161, 162], [162, 183],\n\t\t[162, 207], [164, 165], [164, 180], [164, 186], [164, 201], [165, 219],\n\t\t[165, 220], [180, 219], [180, 220], [185, 202], [185, 203], [185, 204],\n\t\t[186, 219], [186, 220], [188, 222], [188, 223], [201, 219], [201, 220],\n\t\t[217, 221], [218, 221], [221, 224], [221, 225],\n\t]);\n\n\texpect(orders).toMatchObject({\n\t\t\"16 19\": 2, \"16 20\": 2, \"17 19\": 2, \"17 20\": 2, \"18 19\": 2, \"18 20\": 2, \"19 21\": 1, \"19 22\": 1, \"20 21\": 1, \"20 22\": 1, \"22 24\": 2, \"22 25\": 2, \"23 24\": 2, \"23 25\": 2, \"24 26\": 1, \"24 27\": 1, \"24 28\": 1, \"25 26\": 1, \"25 27\": 1, \"25 28\": 1, \"0 123\": 2, \"0 128\": 2, \"91 123\": 2, \"91 128\": 2, \"102 123\": 2, \"102 128\": 2, \"123 136\": 1, \"123 139\": 1, \"128 136\": 1, \"128 139\": 1, \"112 139\": 1, \"101 139\": 1, \"112 114\": 1, \"101 114\": 1, \"99 112\": 2, \"112 141\": 1, \"112 144\": 1, \"99 101\": 2, \"101 141\": 1, \"101 144\": 1,\n\t});\n});\n\ntest(\"constraints3DEdges, mooser's train\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\tclusters_faces,\n\t\tclusters_graph,\n\t\tclusters_transform,\n\t\t// facesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs(folded);\n\n\texpect(facePairs).toHaveLength(1721);\n\texpect(clusters_faces).toHaveLength(87);\n\texpect(clusters_graph).toHaveLength(87);\n\texpect(clusters_transform).toHaveLength(87);\n\n\texpect(yJunctions).toHaveLength(52);\n\texpect(tJunctions).toHaveLength(100);\n\texpect(bentFlatTortillas).toHaveLength(0);\n\texpect(bentTortillas).toHaveLength(234);\n\texpect(bentTortillasFlatTaco).toHaveLength(208);\n\texpect(tortilla_tortilla).toHaveLength(234);\n\texpect(taco_tortilla).toHaveLength(208);\n\texpect(Object.keys(orders)).toHaveLength(112);\n});\n\ntest(\"constraints3DEdges and getSolvable3DEdgePairs, cube octagon\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/cube-octagon.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\tclusters_faces,\n\t\tclusters_graph,\n\t\tclusters_transform,\n\t\t// facesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs(folded);\n\n\texpect(facePairs).toHaveLength(84);\n\texpect(clusters_faces).toHaveLength(6);\n\texpect(clusters_graph).toHaveLength(6);\n\texpect(clusters_transform).toHaveLength(6);\n\n\texpect(tJunctions).toMatchObject([]);\n\texpect(yJunctions).toMatchObject([]);\n\texpect(bentFlatTortillas).toMatchObject([\n\t\t[18, 35], [19, 35], [21, 40], [22, 40], [23, 40], [24, 40], [26, 58],\n\t\t[27, 58], [32, 39], [34, 50], [35, 68], [35, 69], [37, 57], [52, 55],\n\t\t[53, 63], [53, 64], [53, 71], [53, 72], [58, 66], [58, 67],\n\t]);\n\texpect(bentTortillas).toMatchObject([\n\t\t[18, 19], [18, 68], [18, 69], [19, 68], [19, 69], [21, 22],\n\t\t[21, 23], [21, 24], [22, 23], [22, 24], [23, 24], [26, 27],\n\t\t[26, 66], [26, 67], [27, 66], [27, 67], [63, 64], [63, 71],\n\t\t[63, 72], [64, 71], [64, 72], [66, 67], [68, 69], [71, 72],\n\t]);\n\texpect(bentTortillasFlatTaco).toMatchObject([]);\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[11, 37, 40, 13], [11, 37, 10, 12], [11, 37, 41, 39], [13, 40, 12, 10],\n\t\t[13, 40, 39, 41], [30, 33, 0, 2], [30, 33, 1, 17], [30, 33, 31, 4],\n\t\t[0, 2, 1, 17], [0, 2, 31, 4], [1, 17, 31, 4], [42, 44, 9, 43],\n\t\t[42, 45, 46, 43], [42, 8, 7, 43], [9, 45, 46, 44], [9, 8, 7, 44],\n\t\t[18, 24, 23, 20], [18, 21, 22, 20], [18, 25, 19, 20], [23, 21, 22, 24],\n\t\t[23, 25, 19, 24], [45, 7, 8, 46], [10, 12, 41, 39], [21, 19, 25, 22],\n\t]);\n\texpect(taco_tortilla).toMatchObject([]);\n\texpect(Object.keys(orders)).toHaveLength(20);\n});\n\ntest(\"constraints3DEdges and getSolvable3DEdgePairs, coplanar angles\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-face.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(fold),\n\t};\n\tear.graph.populate(folded);\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\t// clusters_faces,\n\t\t// clusters_graph,\n\t\t// clusters_transform,\n\t\tfacesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n\n\tconst {\n\t\ttJunctions,\n\t\tyJunctions,\n\t\tbentFlatTortillas,\n\t\tbentTortillas,\n\t\tbentTortillasFlatTaco,\n\t} = getSolvable3DEdgePairs(folded);\n\n\texpect(facePairs).toMatchObject([\"1 3\", \"1 4\", \"1 5\", \"3 4\", \"3 5\", \"4 5\"]);\n\texpect(facesFacesOverlap).toMatchObject([\n\t\t[], [3, 4, 5], [], [1, 4, 5], [1, 3, 5], [1, 3, 4],\n\t]);\n\n\texpect(yJunctions).toHaveLength(0);\n\texpect(tJunctions).toHaveLength(0);\n\texpect(bentFlatTortillas).toHaveLength(0);\n\texpect(bentTortillas).toHaveLength(0);\n\texpect(bentTortillasFlatTaco).toHaveLength(0);\n\texpect(tortilla_tortilla).toHaveLength(0);\n\texpect(taco_tortilla).toHaveLength(0);\n\texpect(Object.keys(orders)).toHaveLength(0);\n});\n\ntest(\"constraints3DEdges and getSolvable3DEdgePairs, square tube\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/square-tube-with-overlap.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(fold),\n\t};\n\tear.graph.populate(folded);\n\n\texpect(getSolvable3DEdgePairs(folded)).toMatchObject({\n\t\ttJunctions: [],\n\t\tyJunctions: [],\n\t\tbentFlatTortillas: [],\n\t\t// four bent-tortillas\n\t\tbentTortillas: [[12, 13], [14, 15], [21, 22], [23, 24]],\n\t\tbentTortillasFlatTaco: [],\n\t});\n\n\t// console.log(JSON.stringify(constraints3DEdges(folded)));\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\t// clusters_faces,\n\t\t// clusters_graph,\n\t\t// clusters_transform,\n\t\tfacesFacesOverlap,\n\t\tfacePairs,\n\t\torders,\n\t\ttortilla_tortilla,\n\t\ttaco_tortilla,\n\t} = constraints3DEdges(folded);\n});\n"
  },
  {
    "path": "tests/layer.constraints3DFaces.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeSolverConstraints3D, subgraph components\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/layers-3d-edge-face.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst results = foldedForms\n\t\t.map(folded => ear.layer.constraints3DFaceClusters(folded));\n\n\texpect(results[3].clusters_graph).toMatchObject([{\n\t\t// vertices 0, 1, 5, 9 and 3, 4, 6, 7\n\t\t// vertices_coords: [[0, 0], [2, 0],, [1, 0], [0, 0], [0, 1], [0, 1], [1, 1],, [2, 1]],\n\t\t// edges 0, 4, 8, 9 and 3, 5, 11, 12\n\t\tedges_vertices: [[0, 1],,, [3, 4], [5, 0], [6, 7],,, [9, 5], [9, 1],, [7, 3], [4, 6]],\n\t\tedges_faces: [[0],,, [3], [0], [3],,, [0], [0],, [3], [3]],\n\t\tedges_assignment: [\"B\",,, \"B\", \"B\", \"B\",,, \"B\", \"V\",, \"M\", \"B\"],\n\t\tedges_foldAngle: [0,,, 0, 0, 0,,, 0, 120,, -60, 0],\n\t\t// faces 0, 3\n\t\tfaces_vertices: [[0, 1, 9, 5],,, [3, 4, 6, 7]],\n\t\tfaces_edges: [[0, 9, 8, 4],,, [3, 12, 5, 11]],\n\t\tfaces_faces: [[],,, []],\n\t\t// faces_center: [[1, 0.5],,, [0.5, 0.5]],\n\t}, {\n\t\t// vertices_coords: [, [-1, 0], [0, 0],,,,,, [0, 1], [-1, 1]],\n\t\t// edges 1, 7, 9, 10\n\t\tedges_vertices: [, [1, 2],,,,,, [8, 9],, [9, 1], [8, 2]],\n\t\tedges_faces: [, [1],,,,,, [1],, [1], [1]],\n\t\tedges_assignment: [, \"B\",,,,,, \"B\",, \"V\", \"V\"],\n\t\tedges_foldAngle: [, 0,,,,,, 0,, 120, 120],\n\t\t// faces 1\n\t\tfaces_vertices: [, [1, 2, 8, 9]],\n\t\tfaces_edges: [, [1, 10, 7, 9]],\n\t\tfaces_faces: [, []],\n\t\t// faces_center: [, [-0.5, 0.5]],\n\t}, {\n\t\t// vertices_coords: [,,  [-1.5, 0], [-0.5, 0],,,, [-0.5, 1] [-1.5, 1]],\n\t\t// edges 2, 6, 10, 11\n\t\tedges_vertices: [,, [2, 3],,,, [7, 8],,,, [8, 2], [7, 3]],\n\t\tedges_faces: [,, [2],,,, [2],,,, [2], [2]],\n\t\tedges_assignment: [,, \"B\",,,, \"B\",,,, \"V\", \"M\"],\n\t\tedges_foldAngle: [,, 0,,,, 0,,,, 120, -60],\n\t\t// faces 2\n\t\tfaces_vertices: [,, [2, 3, 7, 8]],\n\t\tfaces_edges: [,, [2, 11, 6, 10]],\n\t\tfaces_faces: [,, []],\n\t\t// faces_center: [,, [-1, 0.5]],\n\t}]);\n});\n\ntest(\"makeSolverConstraints3D, Mooser's train, one fourth of carriage only\", () => {\n\tconst FOLD = fs.readFileSync(\n\t\t\"./tests/files/fold/moosers-train-carriage-fourth.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst graph = JSON.parse(FOLD);\n\tconst folded = {\n\t\t...graph,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(graph),\n\t};\n\tear.graph.populate(folded);\n\n\t// face cluster stuff is test in other files: graph.faces.planes\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\t// faces_cluster,\n\t\t// faces_winding,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\tclusters_faces,\n\t\t// clusters_graph,\n\t\t// clusters_transform,\n\t\t// facesFacesOverlap,\n\t\tfacePairs,\n\t} = ear.layer.constraints3DFaceClusters(folded);\n\n\texpect(facePairs).toMatchObject([\n\t\t// bottom carriage, in front of wheel\n\t\t\"0 1\", \"0 37\", \"0 38\", \"0 39\", \"1 37\", \"1 38\", \"1 39\",\n\t\t// front of wheel\n\t\t\"2 15\",\n\t\t// back of wheel\n\t\t\"4 17\",\n\t\t// bottom carriage, all over\n\t\t\"5 36\", \"5 39\", \"5 6\", \"5 33\", \"5 34\", \"5 35\", \"5 37\", \"5 38\", \"6 33\", \"6 34\", \"6 37\", \"6 38\", \"6 35\", \"6 36\", \"6 39\",\n\t\t// back of carriage, mostly below hitch plane\n\t\t\"7 18\", \"7 25\", \"7 23\", \"7 24\", \"7 31\", \"7 19\", \"7 21\",\n\t\t// hitch joint plane\n\t\t\"8 9\", \"8 10\", \"8 11\", \"8 12\", \"8 13\", \"8 14\", \"9 10\", \"9 11\", \"9 12\", \"9 13\", \"9 14\", \"10 11\", \"10 12\", \"10 13\", \"10 14\", \"11 12\", \"11 13\", \"11 14\", \"12 13\", \"12 14\", \"13 14\",\n\t\t// back of carriage\n\t\t\"18 25\", \"18 23\", \"18 24\", \"18 31\", \"18 19\", \"18 21\", \"19 25\", \"19 23\", \"19 24\", \"19 31\", \"19 21\", \"20 22\", \"20 21\", \"20 24\", \"20 31\", \"21 25\", \"21 23\", \"21 24\", \"21 31\", \"21 22\", \"22 24\", \"22 31\", \"23 25\", \"23 24\", \"23 31\", \"24 25\", \"24 31\", \"25 31\",\n\t\t// large side of carriage\n\t\t\"26 29\", \"26 32\", \"26 27\", \"26 28\", \"27 29\", \"27 32\", \"27 28\", \"28 29\", \"28 32\", \"29 32\",\n\t\t// bottom of carriage\n\t\t\"33 34\", \"33 37\", \"33 38\", \"33 35\", \"33 36\", \"33 39\", \"34 37\", \"34 38\", \"34 35\", \"34 36\", \"34 39\", \"35 37\", \"35 38\", \"35 36\", \"35 39\", \"36 39\", \"36 37\", \"36 38\", \"37 38\", \"37 39\", \"38 39\"\n\t]);\n\n\texpect(clusters_faces).toMatchObject([\n\t\t// bottom of wheel\n\t\t[3],\n\t\t// bottom of carriage\n\t\t[0, 1, 5, 6, 33, 34, 35, 36, 37, 38, 39],\n\t\t// hitch joint plane\n\t\t[8, 9, 10, 11, 12, 13, 14],\n\t\t// top of carriage\n\t\t[30],\n\t\t// front side of wheel\n\t\t[2, 15],\n\t\t// back side of wheel\n\t\t[4, 17],\n\t\t// back of carriage\n\t\t[7, 18, 19, 20, 21, 22, 23, 24, 25, 31],\n\t\t// back of wheel\n\t\t[16],\n\t\t// big side of carriage\n\t\t[26, 27, 28, 29, 32],\n\t]);\n});\n"
  },
  {
    "path": "tests/layer.constraints3d.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeSolverConstraints, 2D and 3D comparison, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst flat = ear.layer.makeSolverConstraintsFlat(folded);\n\tconst three = ear.layer.makeSolverConstraints3D(folded);\n\texpect(flat.constraints).toMatchObject(three.constraints);\n\texpect(flat.facePairs).toMatchObject(three.facePairs);\n\texpect(flat.faces_winding).toMatchObject(three.faces_winding);\n\texpect(flat.orders).toMatchObject(three.orders);\n});\n\ntest(\"makeSolverConstraints, 2D and 3D comparison, kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst flat = ear.layer.makeSolverConstraintsFlat(folded);\n\tconst three = ear.layer.makeSolverConstraints3D(folded);\n\texpect(flat.constraints).toMatchObject(three.constraints);\n\texpect(flat.facePairs).toMatchObject(three.facePairs);\n\texpect(flat.faces_winding).toMatchObject(three.faces_winding);\n\texpect(flat.orders).toMatchObject(three.orders);\n});\n\ntest(\"makeSolverConstraints, 2D and 3D comparison, kraft bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\tear.graph.populate(folded);\n\n\tconst flat = ear.layer.makeSolverConstraintsFlat(folded);\n\tconst three = ear.layer.makeSolverConstraints3D(folded);\n\texpect(flat.constraints).toMatchObject(three.constraints);\n\texpect(flat.facePairs).toMatchObject(three.facePairs);\n\texpect(flat.faces_winding).toMatchObject(three.faces_winding);\n\texpect(flat.orders).toMatchObject(three.orders);\n});\n\ntest(\"makeSolverConstraints3D cube octagon\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/cube-octagon.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/cube-octagon-constraints.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\n\tconst solverConstraints = ear.layer.makeSolverConstraints3D(folded);\n\texpect(solverConstraints).toMatchObject(expected);\n});\n\n// this tests all of the cases which were built on top of the 2D solver,\n// things which examine the overlapping geometry in 3D and generate\n// either additional solutions (orders) or conditions (tacos/tortillas/transitivity)\ntest(\"makeSolverConstraints3D layer 3D test cases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-edge.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst results = foldedForms.map(folded => {\n\t\ttry {\n\t\t\treturn ear.layer.makeSolverConstraints3D(folded);\n\t\t} catch (error) {\n\t\t\treturn \"error\";\n\t\t}\n\t});\n\n\texpect(results[0].constraints).toMatchObject({\n\t\ttaco_taco: [], taco_tortilla: [], tortilla_tortilla: [], transitivity: [],\n\t});\n\texpect(results[0].facePairs).toMatchObject([\"0 4\"]);\n\texpect(results[0].orders).toMatchObject({ \"0 4\": 2 });\n\t// face 0 is below face 4,\n\texpect(results[0].faces_winding[0]).toBe(true);\n\texpect(results[0].faces_winding[4]).toBe(false);\n\n\texpect(results[1].constraints).toMatchObject({\n\t\ttaco_taco: [],\n\t\ttaco_tortilla: [],\n\t\ttortilla_tortilla: [[1, 2, 4, 3]],\n\t\ttransitivity: [],\n\t});\n\texpect(results[1].facePairs).toMatchObject([\"1 4\", \"2 3\"]);\n\texpect(results[1].orders).toMatchObject({ \"1 4\": 2, \"2 3\": 2 });\n\n\texpect(results[2].constraints).toMatchObject({\n\t\ttaco_taco: [],\n\t\ttaco_tortilla: [],\n\t\ttortilla_tortilla: [[1, 2, 4, 3]],\n\t\ttransitivity: [],\n\t});\n\texpect(results[2].facePairs).toMatchObject([\"1 4\", \"2 3\"]);\n\texpect(results[2].orders).toMatchObject({ \"1 4\": 2, \"2 3\": 2 });\n\n\texpect(results[3].constraints).toMatchObject({\n\t\ttaco_taco: [],\n\t\ttaco_tortilla: [],\n\t\ttortilla_tortilla: [[1, 2, 4, 3], [0, 1, 5, 4]],\n\t\ttransitivity: [],\n\t});\n\texpect(results[3].facePairs).toMatchObject([\"0 5\", \"1 4\", \"2 3\"]);\n\texpect(results[3].orders).toMatchObject({ \"2 3\": 2 });\n\n\t// only 3 pairs of faces overlap each other: 1-9, 2-8, 3-7\n\t// - 2 tortilla_tortilla between the overlapping 3 sets.\n\texpect(results[5].constraints).toMatchObject({\n\t\ttaco_taco: [],\n\t\ttaco_tortilla: [],\n\t\ttortilla_tortilla: [[1, 2, 9, 8], [2, 3, 8, 7]],\n\t\ttransitivity: [],\n\t});\n\texpect(results[5].facePairs).toMatchObject([\"1 9\", \"2 8\", \"3 7\"]);\n\t// 1-9 via the 3d overlapping edges algorithm\n\t// 3-7 via the 3d overlapping edges algorithm\n\texpect(results[5].orders).toMatchObject({ \"1 9\": 1, \"3 7\": 1 });\n\n\t// 3 pairs of faces overlap each other: 1-10, 2-9, 3-8\n\t// then another group of 4 faces all overlap each other: 4, 5, 6, 7\n\t// - 1 taco_taco in the group of overlapping 4 faces\n\t// - 2 taco_tortilla faces 4 and 7 have a \"F\" edge\n\t// - 3 tortilla_tortilla where 1-2-10-9 and 3-4-8-7 are \"F\" tortillas\n\t//   and 2-3-9-8 is a 3D-bent-tortilla at 90 degrees\n\texpect(results[6].constraints).toMatchObject({\n\t\ttaco_taco: [[4, 6, 5, 7]],\n\t\ttaco_tortilla: [[5, 4, 6], [5, 7, 6]],\n\t\ttortilla_tortilla: [[1, 2, 10, 9], [3, 4, 8, 7], [2, 3, 9, 8]],\n\t\ttransitivity: [],\n\t});\n\texpect(results[6].facePairs).toMatchObject([\n\t\t\"1 10\", \"2 9\", \"3 8\", \"4 7\", \"4 5\", \"4 6\", \"5 7\", \"5 6\", \"6 7\",\n\t]);\n\t// 1-10 is known via the 3d overlapping edges algorithm\n\t// 4-5, 5-6, 6-7 are simply flat adjacent faces\n\texpect(results[6].orders).toMatchObject({\n\t\t\"1 10\": 1, \"4 5\": 2, \"5 6\": 2, \"6 7\": 1,\n\t});\n});\n\ntest(\"makeSolverConstraints3D coplanar angles 3D\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-face.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frame2 = ear.graph.flattenFrame(fold, 1);\n\tconst folded1 = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(fold),\n\t};\n\tear.graph.populate(folded1);\n\n\tconst folded2 = {\n\t\t...frame2,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame2),\n\t};\n\tear.graph.populate(folded1);\n\tear.graph.populate(folded2);\n\n\t// frame 2 has a longer side length that crosses one of the faces,\n\t// but everything should remain consistent\n\texpect(ear.layer.makeSolverConstraints3D(folded1))\n\t\t.toMatchObject(ear.layer.makeSolverConstraints3D(folded2));\n\n\tconst {\n\t\tconstraints: { taco_taco, taco_tortilla, tortilla_tortilla, transitivity },\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t} = ear.layer.makeSolverConstraints3D(folded1);\n\n\t// faces 1, 3, 4, 5 are all coplanar.\n\t// face 0 is base plane\n\t// face 1 is angled plane\n\texpect(facePairs).toMatchObject([\"1 3\", \"1 4\", \"1 5\", \"3 4\", \"3 5\", \"4 5\"]);\n\texpect(faces_winding).toMatchObject([true, true, true, false, true, false]);\n\texpect(orders).toMatchObject({ \"3 4\": 1, \"4 5\": 2, \"1 5\": 1 });\n\texpect(taco_taco).toMatchObject([[3, 1, 4, 5]]);\n\texpect(taco_tortilla).toMatchObject([[4, 1, 5], [4, 3, 5]]);\n\texpect(tortilla_tortilla).toMatchObject([]);\n\texpect(transitivity).toMatchObject([]);\n\n\t// const result = ear.layer.getOverlappingParallelEdgePairs()\n});\n\ntest(\"makeSolverConstraints3D panels 6x2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-6x2-90deg.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tconstraints: { taco_taco, taco_tortilla, tortilla_tortilla, transitivity },\n\t\tfacePairs,\n\t\tfaces_winding,\n\t\torders,\n\t} = ear.layer.makeSolverConstraints3D(folded);\n\n\t[\n\t\t// every permutation of pairs of these:\n\t\t// 0, 1, 2, 3, 4, 5\n\t\t\"0 1\", \"0 2\", \"0 3\", \"0 4\", \"0 5\", \"1 2\", \"1 3\", \"1 4\", \"1 5\",\n\t\t\"2 3\", \"2 4\", \"2 5\", \"3 4\", \"3 5\", \"4 5\",\n\t\t// every permutation of pairs of these:\n\t\t// 6, 7, 8, 9, 10, 11\n\t\t\"6 7\", \"6 8\", \"6 9\", \"6 10\", \"6 11\", \"7 8\", \"7 9\", \"7 10\", \"7 11\",\n\t\t\"8 9\", \"8 10\", \"8 11\", \"9 10\", \"9 11\", \"10 11\",\n\t].forEach(key => expect(facePairs).toContain(key));\n\texpect(faces_winding).toMatchObject([\n\t\ttrue, false, true, false, true, false, true, false, true, false, true, false,\n\t]);\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 2, \"1 2\": 2, \"2 3\": 2, \"3 4\": 1, \"4 5\": 1, \"6 7\": 2, \"7 8\": 2, \"8 9\": 2, \"9 10\": 1, \"10 11\": 1,\n\t});\n\n\texpect(taco_taco).toMatchObject([\n\t\t[0, 2, 1, 3],\n\t\t[0, 4, 1, 5],\n\t\t[1, 3, 2, 4],\n\t\t[2, 4, 3, 5],\n\t\t[6, 8, 7, 9],\n\t\t[6, 10, 7, 11],\n\t\t[7, 9, 8, 10],\n\t\t[8, 10, 9, 11],\n\t]);\n\n\texpect(taco_tortilla).toMatchObject([]);\n\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[0, 6, 1, 7],\n\t\t[0, 6, 2, 8],\n\t\t[0, 6, 3, 9],\n\t\t[0, 6, 4, 10],\n\t\t[0, 6, 5, 11],\n\t\t[1, 7, 2, 8],\n\t\t[1, 7, 3, 9],\n\t\t[1, 7, 4, 10],\n\t\t[1, 7, 5, 11],\n\t\t[2, 8, 3, 9],\n\t\t[2, 8, 4, 10],\n\t\t[2, 8, 5, 11],\n\t\t[3, 9, 4, 10],\n\t\t[3, 9, 5, 11],\n\t\t[4, 10, 5, 11],\n\t]);\n\n\texpect(transitivity).toMatchObject([\n\t\t[0, 2, 4],\n\t\t[0, 2, 5],\n\t\t[0, 3, 4],\n\t\t[0, 3, 5],\n\t\t[1, 2, 5],\n\t\t[1, 3, 5],\n\t\t[6, 8, 10],\n\t\t[6, 8, 11],\n\t\t[6, 9, 10],\n\t\t[6, 9, 11],\n\t\t[7, 8, 11],\n\t\t[7, 9, 11],\n\t]);\n});\n\ntest(\"makeSolverConstraints3D maze-u\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tconst expectedJSON = fs.readFileSync(\"./tests/files/json/maze-u-constraints.json\", \"utf-8\");\n\tconst expected = JSON.parse(expectedJSON);\n\tconst solverConstraints = ear.layer.makeSolverConstraints3D(folded);\n\texpect(solverConstraints).toMatchObject(expected);\n});\n"
  },
  {
    "path": "tests/layer.constraintsFlat.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeSolverConstraintsFlat, four panel square\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\t// only two taco-tacos, one on the top and one bottom\n\texpect(taco_taco).toMatchObject([\n\t\t[0, 2, 1, 3],\n\t\t[4, 6, 5, 7],\n\t]);\n\n\texpect(taco_tortilla).toMatchObject([\n\t\t[1, 3, 2],\n\t\t[5, 7, 6],\n\t]);\n\n\t// tortilla-tortilla between 4 sets of pairs, 0-4, 1-5, 2-6, 3-7\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[0, 4, 1, 5],\n\t\t[0, 4, 2, 6],\n\t\t[0, 4, 3, 7],\n\t\t[1, 5, 2, 6],\n\t\t[1, 5, 3, 7],\n\t\t[2, 6, 3, 7],\n\t]);\n\n\texpect(transitivity).toMatchObject([]);\n});\n\n\ntest(\"makeSolverConstraintsFlat, strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tfaces_winding,\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\t// interestingly, this model, which obviously has various different layer\n\t// solutions, results in no information for the solver.\n\texpect(taco_taco).toMatchObject([]);\n\texpect(taco_tortilla).toMatchObject([]);\n\texpect(tortilla_tortilla).toMatchObject([]);\n\texpect(transitivity).toMatchObject([[0, 1, 4]]);\n\n\texpect(faces_winding).toMatchObject([true, false, true, false, false, true, false]);\n});\n\ntest(\"makeSolverConstraintsFlat, zig-zag panels\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-zig-zag.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\texpect(taco_taco).toMatchObject([\n\t\t[0, 2, 4, 3],\n\t\t[3, 1, 4, 2],\n\t]);\n\n\texpect(taco_tortilla).toMatchObject([\n\t\t[0, 1, 4],\n\t\t[3, 0, 4],\n\t\t[2, 1, 3],\n\t\t[1, 0, 2],\n\t]);\n\n\texpect(tortilla_tortilla).toMatchObject([]);\n\n\texpect(transitivity).toMatchObject([[0, 1, 3]]);\n});\n\n// test(\"makeSolverConstraintsFlat, flat grid\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-flat-grid.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst folded = {\n// \t\t...fold,\n// \t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n// \t}\n// \tear.graph.populate(folded);\n\n// \tconst result = ear.layer.makeSolverConstraintsFlat(folded);\n\n// \texpect(result.constraints.taco_taco).toHaveLength(0);\n// \texpect(result.constraints.taco_tortilla).toHaveLength(0);\n// \texpect(result.constraints.tortilla_tortilla).toHaveLength(30);\n// \texpect(result.constraints.transitivity).toHaveLength(0);\n// \texpect(result).toMatchObject({\n// \t\tfacePairs: [\n// \t\t\t\"2 3\", \"4 23\", \"5 51\", \"6 50\", \"7 49\", \"8 48\", \"24 25\", \"26 32\", \"27 54\", \"28 55\", \"29 56\", \"30 31\", \"33 34\", \"35 36\", \"37 38\", \"39 40\", \"41 42\", \"43 44\", \"52 63\", \"53 62\", \"59 66\",\n// \t\t],\n// \t\torders: {\n// \t\t\t\"2 3\": 1, \"24 25\": 1, \"30 31\": 1, \"33 34\": 1, \"37 38\": 1, \"41 42\": 1,\n// \t\t}\n// \t});\n// });\n\ntest(\"makeSolverConstraintsFlat, triangle strip\", () => {\n\tconst fileStrip1 = fs.readFileSync(\"./tests/files/fold/triangle-strip.fold\", \"utf-8\");\n\tconst fileStrip2 = fs.readFileSync(\"./tests/files/fold/triangle-strip-2.fold\", \"utf-8\");\n\tconst foldStrip1 = JSON.parse(fileStrip1);\n\tconst foldStrip2 = JSON.parse(fileStrip2);\n\tconst folded1 = ear.graph.getFramesByClassName(foldStrip1, \"foldedForm\")[0];\n\tconst folded2 = ear.graph.getFramesByClassName(foldStrip2, \"foldedForm\")[0];\n\tear.graph.populate(folded1);\n\tear.graph.populate(folded2);\n\n\tconst res1 = ear.layer.makeSolverConstraintsFlat(folded1);\n\texpect(res1.constraints.taco_taco.length).toBe(0);\n\texpect(res1.constraints.taco_tortilla.length).toBe(0);\n\texpect(res1.constraints.tortilla_tortilla.length).toBe(0);\n\texpect(res1.constraints.transitivity.length).toBe(0);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t} = ear.layer.makeSolverConstraintsFlat(folded2);\n\n\texpect(taco_taco).toMatchObject([\n\t\t[15, 4, 16, 5],\n\t\t[17, 6, 27, 7],\n\t\t[25, 8, 26, 9],\n\t\t[23, 10, 24, 11],\n\t\t[21, 12, 22, 13],\n\t]);\n\n\texpect(taco_tortilla.length).toBe(0);\n\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[13, 19, 21, 20],\n\t\t[11, 21, 23, 22],\n\t\t[11, 21, 10, 13],\n\t\t[9, 23, 25, 24],\n\t\t[9, 23, 8, 11],\n\t\t[7, 25, 27, 26],\n\t\t[7, 25, 6, 9],\n\t\t[5, 27, 16, 17],\n\t\t[5, 27, 4, 7],\n\t\t[3, 16, 14, 15],\n\t\t[3, 16, 2, 5],\n\t\t[1, 14, 0, 3],\n\t\t[14, 15, 2, 5],\n\t\t[16, 17, 4, 7],\n\t\t[26, 27, 9, 6],\n\t\t[24, 25, 11, 8],\n\t\t[22, 23, 13, 10],\n\t]);\n\n\texpect(transitivity).toMatchObject([[2, 3, 14], [18, 19, 20]]);\n});\n\ntest(\"makeSolverConstraintsFlat, bird base\", () => {\n\tconst cp = ear.graph.bird();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(194);\n\texpect(ear.graph.getEdgesFacesOverlap(folded).flat().length).toBe(16);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\texpect(taco_taco).toMatchObject([\n\t\t// long 22.5 edges, one side, 8 faces\n\t\t// faces: 0-1, 5-6, 4-7, 8-11\n\t\t[0, 4, 1, 7],\n\t\t[0, 5, 1, 6],\n\t\t[0, 8, 1, 11],\n\t\t[4, 5, 7, 6],\n\t\t[4, 8, 7, 11],\n\t\t[5, 8, 6, 11],\n\n\t\t// long 22.5 edges, the other side, 8 faces\n\t\t// faces: 2-3, 13-14, 9-10, 12-15\n\t\t[9, 12, 10, 15],\n\t\t[9, 13, 10, 14],\n\t\t[9, 2, 10, 3],\n\t\t[12, 13, 15, 14],\n\t\t[12, 2, 15, 3],\n\t\t[13, 2, 14, 3],\n\n\t\t// inside reverse fold (under armpit), one side: 0-4, 5-8\n\t\t[0, 5, 4, 8],\n\t\t// top 45 triangle hypotenuse, one side: 6-16, 7-19\n\t\t[7, 6, 19, 16],\n\t\t// inside reverse fold (under armpit), the other side: 3-13, 9-12\n\t\t[9, 3, 12, 13],\n\t\t// top 45 triangle hypotenuse, the other side: 14-18, 15-17\n\t\t[15, 14, 17, 18],\n\t]);\n\n\texpect(taco_tortilla).toMatchObject([\n\t\t[0, 1, 4],\n\t\t[0, 11, 4],\n\t\t[5, 1, 8],\n\t\t[5, 11, 8],\n\t\t[9, 2, 12],\n\t\t[9, 10, 12],\n\t\t[3, 2, 13],\n\t\t[3, 10, 13],\n\t\t[6, 1, 7],\n\t\t[6, 19, 7],\n\t\t[6, 11, 7],\n\t\t[6, 16, 7],\n\t\t[14, 2, 15],\n\t\t[14, 18, 15],\n\t\t[14, 10, 15],\n\t\t[14, 17, 15],\n\t\t[0, 6, 4],\n\t\t[0, 7, 4],\n\t\t[5, 6, 8],\n\t\t[5, 7, 8],\n\t\t[9, 14, 12],\n\t\t[9, 15, 12],\n\t\t[3, 14, 13],\n\t\t[3, 15, 13],\n\t]);\n\n\texpect(tortilla_tortilla).toMatchObject([\n\t\t[1, 2, 11, 10],\n\t\t[18, 19, 17, 16],\n\t\t[1, 19, 11, 16],\n\t\t[2, 18, 10, 17],\n\t\t[1, 19, 6, 6],\n\t\t[1, 19, 7, 7],\n\t\t[2, 18, 14, 14],\n\t\t[2, 18, 15, 15],\n\t\t[11, 16, 6, 6],\n\t\t[11, 16, 7, 7],\n\t\t[10, 17, 14, 14],\n\t\t[10, 17, 15, 15],\n\t]);\n\n\texpect(transitivity).toMatchObject([\n\t\t[0, 5, 7], [0, 5, 11], [0, 6, 7], [0, 6, 8], [0, 6, 11], [0, 7, 8],\n\t\t[0, 7, 11], [1, 4, 5], [1, 4, 6], [1, 4, 8], [1, 4, 11], [1, 5, 7],\n\t\t[1, 5, 11], [1, 6, 8], [1, 6, 11], [1, 7, 8], [1, 7, 11], [2, 9, 13],\n\t\t[2, 9, 14], [2, 9, 15], [2, 10, 12], [2, 10, 13], [2, 10, 14], [2, 10, 15],\n\t\t[2, 12, 13], [2, 12, 14], [2, 13, 15], [3, 9, 14], [3, 9, 15], [3, 10, 12],\n\t\t[3, 10, 14], [3, 10, 15], [3, 12, 14], [3, 14, 15], [4, 5, 11], [4, 6, 8],\n\t\t[4, 6, 11], [5, 7, 11], [6, 7, 8], [9, 13, 15], [9, 14, 15], [10, 12, 13],\n\t\t[10, 12, 14], [10, 13, 15],\n\t]);\n});\n\ntest(\"makeSolverConstraintsFlat, kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tconstraints,\n\t\tfacePairs,\n\t\torders,\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\tconst expectedJSON = fs.readFileSync(\n\t\t\"./tests/files/json/kabuto-constraints.json\",\n\t\t\"utf-8\",\n\t);\n\tconst expected = JSON.parse(expectedJSON);\n\n\texpect(constraints).toMatchObject(expected);\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(104);\n\texpect(ear.graph.getEdgesFacesOverlap(folded).flat().length).toBe(138);\n\texpect(constraints.taco_taco.length).toMatchObject(21);\n\texpect(constraints.taco_tortilla.length).toMatchObject(88);\n\texpect(constraints.tortilla_tortilla.length).toMatchObject(0);\n\texpect(constraints.transitivity.length).toMatchObject(258);\n\texpect(Object.keys(facePairs).length).toBe(117);\n\texpect(Object.keys(orders).length).toBe(20);\n});\n\ntest(\"makeSolverConstraintsFlat, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\t// const cp = ear.graph.getFramesByClassName(fold, \"creasePattern\")[0];\n\t// console.log(JSON.stringify(cp));\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst epsilon = 1e-4;\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tfacePairs,\n\t\torders,\n\t} = ear.layer.makeSolverConstraintsFlat(folded, epsilon);\n\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded, epsilon).flat().length).toBe(554);\n\texpect(ear.graph.getFacesEdgesOverlap(folded, epsilon).flat().length).toBe(1167);\n\texpect(taco_taco.length).toMatchObject(196);\n\texpect(taco_tortilla.length).toMatchObject(1049);\n\texpect(tortilla_tortilla.length).toMatchObject(0);\n\texpect(transitivity.length).toMatchObject(5328);\n\texpect(Object.keys(facePairs).length).toBe(838);\n\texpect(Object.keys(orders).length).toBe(102);\n});\n\ntest(\"makeSolverConstraintsFlat, flapping bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(114);\n\texpect(ear.graph.getEdgesFacesOverlap(folded).flat().length).toBe(110);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tfacePairs,\n\t\torders,\n\t} = ear.layer.makeSolverConstraintsFlat(folded);\n\n\texpect(taco_taco.length).toMatchObject(27);\n\texpect(taco_tortilla.length).toMatchObject(82);\n\texpect(tortilla_tortilla.length).toMatchObject(15);\n\texpect(transitivity.length).toMatchObject(168);\n\texpect(Object.keys(facePairs).length).toBe(125);\n\texpect(Object.keys(orders).length).toBe(30);\n});\n\ntest(\"layer bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold, [6]),\n\t};\n\tear.graph.populate(folded);\n\n\texpect(ear.graph.getEdgesEdgesCollinearOverlap(folded).flat().length).toBe(3404);\n\texpect(ear.graph.getEdgesFacesOverlap(folded).flat().length).toBe(7544);\n\n\tconst {\n\t\tconstraints: {\n\t\t\ttaco_taco,\n\t\t\ttaco_tortilla,\n\t\t\ttortilla_tortilla,\n\t\t\ttransitivity,\n\t\t},\n\t\tfacePairs,\n\t\torders,\n\t} = ear.layer.makeSolverConstraintsFlat(folded, 1e-6);\n\n\texpect(taco_taco.length).toBe(764);\n\texpect(taco_tortilla.length).toBe(4860);\n\texpect(tortilla_tortilla.length).toBe(2613);\n\texpect(transitivity.length).toBe(54856);\n\n\texpect(Object.keys(facePairs).length).toBe(5848);\n\texpect(Object.keys(orders).length).toBe(338);\n});\n"
  },
  {
    "path": "tests/layer.facesSide.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"overlappingParallelEdgePairs panel 4x2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\n\t// 8, 9, 10, 11 all overlap each other\n\t// 6 and 14, and 7 and 15 overlap each other\n\texpect(edgePairs).toMatchObject([\n\t\t[6, 14], [7, 15],\n\t\t[8, 9], [8, 10],\n\t\t[8, 11], [9, 10],\n\t\t[9, 11], [10, 11],\n\t]);\n});\n\ntest(\"overlappingParallelEdgePairs strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\n\texpect(edgePairs.length).toBe(0);\n});\n\ntest(\"overlappingParallelEdgePairs triangle strip\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/triangle-strip.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\n\texpect(edgePairs.length).toBe(0);\n});\n\ntest(\"overlappingParallelEdgePairs triangle strip-2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/triangle-strip-2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\n\texpect(edgePairs).toMatchObject([\n\t\t[10, 29], [11, 45],\n\t\t[12, 41], [12, 46],\n\t\t[13, 38], [13, 42],\n\t\t[14, 35], [14, 37],\n\t\t[15, 31], [15, 36],\n\t\t[16, 20], [16, 32],\n\t\t[17, 21], [19, 33],\n\t\t[20, 32], [30, 39],\n\t\t[31, 36], [34, 43],\n\t\t[35, 37], [38, 42],\n\t\t[40, 47], [41, 46],\n\t]);\n});\n\ntest(\"edgesFacesSide 2D and 3D\", () => {\n\tconst {\n\t\tvertices_coords,\n\t\tedges_vertices,\n\t} = ear.graph.square();\n\n\tconst graph1 = ear.graph.populate({\n\t\tvertices_coords,\n\t\tedges_vertices: [...edges_vertices, [0, 2]],\n\t\tfaces_vertices: [[0, 1, 2], [2, 3, 0]],\n\t});\n\tconst graph2 = ear.graph.populate({\n\t\tvertices_coords,\n\t\tedges_vertices: [[1, 0], [2, 1], [3, 2], [0, 3], [2, 0]],\n\t\tfaces_vertices: [[0, 1, 2], [2, 3, 0]],\n\t});\n\tgraph1.faces_center = ear.graph.makeFacesCenterQuick(graph1);\n\tgraph2.faces_center = ear.graph.makeFacesCenterQuick(graph2);\n\n\texpect(ear.layer.makeEdgesFacesSide(graph1))\n\t\t.toMatchObject([[-1], [-1], [-1], [-1], [1, -1]]);\n\n\texpect(ear.layer.makeEdgesFacesSide(graph2))\n\t\t.toMatchObject([[1], [1], [1], [1], [-1, 1]]);\n\n\texpect(ear.layer.makeEdgesFacesSide3D(graph1, {\n\t\t...ear.graph.getEdgesLine(graph1),\n\t\t...ear.graph.getFacesPlane(graph1),\n\t})).toMatchObject([[-1], [-1], [-1], [-1], [1, -1]]);\n\n\texpect(ear.layer.makeEdgesFacesSide3D(graph2, {\n\t\t...ear.graph.getEdgesLine(graph2),\n\t\t...ear.graph.getFacesPlane(graph2),\n\t})).toMatchObject([[1], [1], [1], [1], [-1, 1]]);\n});\n\ntest(\"edgesFacesSide strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tfolded.faces_center = ear.graph.makeFacesCenterQuick(folded);\n\t// all non boundary are taco-taco (same side)\n\texpect(ear.layer.makeEdgesFacesSide(folded)).toMatchObject([\n\t\t[1], [-1], [1], [-1], [-1], [1], [-1], [1], [-1, -1], [1, 1], [1], [-1],\n\t\t[1], [-1], [1], [-1], [1, 1], [-1, -1], [1], [1, 1], [-1, -1], [-1], [-1],\n\t]);\n});\n\ntest(\"facesSide panels zig zag\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-zig-zag.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tfolded.faces_center = ear.graph.makeFacesCenterQuick(folded);\n\n\t// only valley/mountains are 11, 12, 13, 14, all are tacos (same sign)\n\tconst edgesFacesSide = ear.layer.makeEdgesFacesSide(folded);\n\texpect(edgesFacesSide).toMatchObject([\n\t\t[1], [1], [-1], [1], [-1], [1], [1], [-1], [1], [-1], [1],\n\t\t[1, 1], [-1, -1], [1, 1], [-1, -1],\n\t\t[1],\n\t]);\n\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\texpect(edgePairs).toMatchObject([\n\t\t[11, 13], [12, 14],\n\t]);\n\n\t// const tacos_faces = edgePairs.map(pair => pair.map(edge => folded.edges_faces[edge]));\n\tconst tacosFacesSide = ear.layer.makeEdgePairsFacesSide(folded, edgePairs);\n\texpect(tacosFacesSide).toMatchObject([\n\t\t[[1, 1], [1, 1]], [[-1, -1], [-1, -1]],\n\t]);\n});\n\ntest(\"facesSide panel 4x2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\tfolded.faces_center = ear.graph.makeFacesCenterQuick(folded);\n\n\tconst edgesFacesSide2D = ear.layer.makeEdgesFacesSide(folded);\n\tconst edgesFacesSide3D = ear.layer.makeEdgesFacesSide3D(folded, {\n\t\t...ear.graph.getEdgesLine(folded),\n\t\t...ear.graph.getFacesPlane(folded),\n\t});\n\n\texpect(folded.edges_assignment)\n\t\t.toMatchObject([\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\",\n\t\t\t\"F\", \"F\", \"F\", \"F\", \"V\", \"V\", \"M\", \"M\",\n\t\t\t\"B\", \"B\", \"B\", \"B\", \"B\", \"B\",\n\t\t]);\n\texpect(edgesFacesSide2D).toMatchObject([\n\t\t// 6 boundary\n\t\t[-1], [1], [-1], [1], [-1], [-1],\n\t\t// 2 valley\n\t\t[-1, -1], [-1, -1],\n\t\t// 4 flat\n\t\t[1, -1], [-1, 1], [1, -1], [-1, 1],\n\t\t// 2 valley 2 mountain\n\t\t[1, 1], [1, 1], [-1, -1], [-1, -1],\n\t\t// 5 boundary\n\t\t[1], [1], [1], [-1], [1], [-1],\n\t]);\n\n\texpect(edgesFacesSide3D).toMatchObject([\n\t\t// 6 boundary\n\t\t[-1], [-1], [-1], [-1], [-1], [-1],\n\t\t// 2 valley\n\t\t[-1, -1], [-1, -1],\n\t\t// 4 flat\n\t\t[1, -1], [1, -1], [1, -1], [1, -1],\n\t\t// 2 valley 2 mountain\n\t\t[-1, -1], [-1, -1], [-1, -1], [-1, -1],\n\t\t// 5 boundary\n\t\t[1], [1], [1], [1], [1], [1],\n\t]);\n\n\tconst edgePairs = ear.graph.connectedComponentsPairs(\n\t\tear.graph.getEdgesEdgesCollinearOverlap(folded),\n\t).filter(pair => pair.every(edge => folded.edges_faces[edge].length === 2));\n\n\tconst tacos_faces = edgePairs.map(pair => pair.map(edge => folded.edges_faces[edge]));\n\tconst tacosFacesSide = ear.layer.makeEdgePairsFacesSide(folded, edgePairs, tacos_faces);\n\texpect(tacosFacesSide).toMatchObject([\n\t\t[[-1, -1], [-1, -1]],\n\t\t[[-1, -1], [-1, -1]],\n\t\t[[1, -1], [1, -1]],\n\t\t[[1, -1], [1, -1]],\n\t\t[[1, -1], [1, -1]],\n\t\t[[-1, 1], [-1, 1]],\n\t\t[[-1, 1], [-1, 1]],\n\t\t[[1, -1], [1, -1]],\n\t]);\n});\n\ntest(\"makeEdgesFacesSide3D, layer 3D cases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-edge.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\tconst folded = foldedForms[1];\n\n\t// const ignore = ear.layer.constraints3DFaceClusters(folded);\n\tconst edges_facesSides = ear.layer.makeEdgesFacesSide3D(folded, {\n\t\t...ear.graph.getEdgesLine(folded),\n\t\t...ear.graph.getFacesPlane(folded),\n\t});\n\n\texpect(edges_facesSides).toMatchObject([\n\t\t[-1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], [1], [1], [1], [1], [-1],\n\t\t[1, -1],\n\t\t[1, -1],\n\t\t[1, 1],\n\t\t[-1, 1],\n\t\t[-1, -1],\n\t\t[-1],\n\t]);\n});\n"
  },
  {
    "path": "tests/layer.general.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"overlappingParallelEdgePairs\", () => {});\n\ntest(\"constraintToFacePairs\", () => {});\n\ntest(\"constraintToFacePairsStrings\", () => {});\n\ntest(\"solverSolutionToFaceOrders\", () => {});\n"
  },
  {
    "path": "tests/layer.initialSolution.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"solveFlatAdjacentEdges, different axes\", () => {\n\t// shortcut methods\n\tconst solveFlatAdjacentEdges = (graph) => (\n\t\tear.layer.solveFlatAdjacentEdges(\n\t\t\tgraph,\n\t\t\tear.graph.makeFacesWinding(graph),\n\t\t));\n\tconst solverSolutionToFaceOrders = (graph) => (\n\t\tear.layer.solverSolutionToFaceOrders(\n\t\t\tear.layer.solveFlatAdjacentEdges(\n\t\t\t\tgraph,\n\t\t\t\tear.graph.makeFacesWinding(graph),\n\t\t\t),\n\t\t\tear.graph.makeFacesWinding(graph),\n\t\t)\n\t);\n\n\t// two squares joined at an edge, folded to lie exactly on top of each other\n\t// A: 0, 1, 2, 3 (counter)\n\t// B: 5, 4, 1, 0 (clock)\n\tconst base = {\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [1, 4], [4, 5], [5, 0]],\n\t\tedges_assignment: [\"V\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\"],\n\t\tfaces_vertices: [[0, 1, 2, 3], [5, 4, 1, 0]],\n\t}\n\tconst graphXYUp = ear.graph.populate({\n\t\t...base,\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1], [1, 1], [0, 1]],\n\t});\n\tconst graphXYDown = ear.graph.populate({\n\t\t...base,\n\t\tvertices_coords: [[0, 0], [0, 1], [1, 1], [1, 0], [1, 1], [1, 0]],\n\t});\n\n\t// XY up\n\t// face 0 is upright, plane normal will be [0, 0, 1]\n\texpect(ear.graph.makeFacesWinding(graphXYUp)).toMatchObject([true, false]);\n\t// in plane [0, 0, 1], face 0 is below face 1\n\texpect(solveFlatAdjacentEdges(graphXYUp)).toMatchObject({ \"0 1\": 2 });\n\t// face 0 is above face 1's normal\n\texpect(solverSolutionToFaceOrders(graphXYUp)).toMatchObject([[0, 1, 1]]);\n\n\t// face 0 is flipped, plane normal will be [0, 0, -1]\n\texpect(ear.graph.makeFacesWinding(graphXYDown)).toMatchObject([false, true]);\n\t// in plane [0, 0, -1], face 0 is above face 1\n\texpect(solveFlatAdjacentEdges(graphXYDown)).toMatchObject({ \"0 1\": 1 });\n\t// face 0 is above face 1's normal\n\texpect(solverSolutionToFaceOrders(graphXYDown)).toMatchObject([[0, 1, 1]]);\n});\n\ntest(\"solveFlatAdjacentEdges, four panel square\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\t// only two taco-tacos, one on the top and one bottom\n\texpect(faces_winding).toMatchObject([\n\t\ttrue, false, true, false, true, false, true, false,\n\t]);\n\t// 0 is below 1 (V)\n\t// 1 is above 2 (V)\n\t// 2 is above 3 (M)\n\t// same with every face N+4\n\texpect(adjacentOrders).toMatchObject({\n\t\t\"0 1\": 2, \"1 2\": 1, \"2 3\": 1, \"4 5\": 2, \"5 6\": 1, \"6 7\": 1,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding).toMatchObject([\n\t\ttrue, false, true, false, false, true, false,\n\t]);\n\texpect(adjacentOrders).toMatchObject({\n\t\t\"0 1\": 2, \"0 4\": 2, \"1 2\": 1, \"2 3\": 2, \"4 5\": 1, \"5 6\": 2,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, zig-zag panels\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-zig-zag.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding).toMatchObject([true, true, false, true, false]);\n\texpect(adjacentOrders).toMatchObject({\n\t\t\"0 4\": 2, \"1 2\": 1, \"2 3\": 1, \"3 4\": 1,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, triangle strip\", () => {\n\tconst fileStrip1 = fs.readFileSync(\"./tests/files/fold/triangle-strip.fold\", \"utf-8\");\n\tconst fileStrip2 = fs.readFileSync(\"./tests/files/fold/triangle-strip-2.fold\", \"utf-8\");\n\tconst foldStrip1 = JSON.parse(fileStrip1);\n\tconst foldStrip2 = JSON.parse(fileStrip2);\n\tconst folded1 = ear.graph.getFramesByClassName(foldStrip1, \"foldedForm\")[0];\n\tconst folded2 = ear.graph.getFramesByClassName(foldStrip2, \"foldedForm\")[0];\n\tear.graph.populate(folded1);\n\tear.graph.populate(folded2);\n\n\tconst faces_winding1 = ear.graph.makeFacesWinding(folded1);\n\tconst adjacentOrders1 = ear.layer.solveFlatAdjacentEdges(folded1, faces_winding1);\n\tconst faces_winding2 = ear.graph.makeFacesWinding(folded2);\n\tconst adjacentOrders2 = ear.layer.solveFlatAdjacentEdges(folded2, faces_winding2);\n\n\texpect(faces_winding1).toMatchObject([\n\t\ttrue, false, false, true, true, false, false, true, true, false, false, true, true, false,\n\t]);\n\texpect(adjacentOrders1).toMatchObject({\n\t\t\"0 1\": 2, \"10 11\": 2, \"12 13\": 2, \"2 3\": 2, \"4 5\": 2, \"6 7\": 2, \"8 9\": 2,\n\t});\n\texpect(faces_winding2).toMatchObject([\n\t\ttrue, false, false, true, true, false, false, true, true, false,\n\t\tfalse, true, true, false, false, false, true, true, true, false,\n\t\ttrue, true, false, false, true, true, false, false,\n\t]);\n\texpect(adjacentOrders2).toMatchObject({\n\t\t\"0 1\": 2, \"10 11\": 2, \"12 13\": 2, \"15 16\": 1, \"17 27\": 1, \"18 19\": 2, \"19 20\": 2, \"2 3\": 2, \"21 22\": 2, \"23 24\": 2, \"25 26\": 2, \"4 5\": 2, \"6 7\": 2, \"8 9\": 2,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, bird base\", () => {\n\tconst cp = ear.graph.bird();\n\tconst folded = {\n\t\t...cp,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(cp),\n\t};\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding).toMatchObject([\n\t\ttrue, false, false, true, false, true, false, true, false, false,\n\t\ttrue, true, true, false, true, false, true, true, false, false,\n\t]);\n\texpect(adjacentOrders).toMatchObject({\n\t\t\"0 1\": 2, \"0 4\": 1, \"12 15\": 2, \"13 14\": 1, \"14 15\": 1, \"14 18\": 2, \"15 17\": 1, \"2 3\": 1, \"3 13\": 1, \"4 7\": 1, \"5 6\": 2, \"5 8\": 1, \"6 16\": 1, \"6 7\": 2, \"7 19\": 2, \"8 11\": 1, \"9 10\": 1, \"9 12\": 2,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding).toMatchObject([\n\t\ttrue, false, true, false, true, false, true, true, false,\n\t\tfalse, true, true, false, false, true, true, false, false,\n\t]);\n\texpect(adjacentOrders).toMatchObject({\n\t\t\"0 3\": 2,\n\t\t\"0 8\": 2,\n\t\t\"0 9\": 2,\n\t\t\"1 10\": 2,\n\t\t\"1 11\": 2,\n\t\t\"1 2\": 1,\n\t\t\"10 12\": 2,\n\t\t\"11 13\": 2,\n\t\t\"12 14\": 2,\n\t\t\"13 15\": 2,\n\t\t\"14 16\": 1,\n\t\t\"15 17\": 1,\n\t\t\"3 4\": 1,\n\t\t\"4 5\": 1,\n\t\t\"6 16\": 2,\n\t\t\"6 8\": 1,\n\t\t\"7 17\": 2,\n\t\t\"7 9\": 1,\n\t\t\"8 10\": 2,\n\t\t\"9 11\": 2,\n\t});\n});\n\ntest(\"solveFlatAdjacentEdges, crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding[0]).toBe(true);\n\texpect(Object.keys(adjacentOrders)).toHaveLength(102);\n});\n\ntest(\"solveFlatAdjacentEdges, flapping bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding[0]).toBe(true);\n\texpect(Object.keys(adjacentOrders)).toHaveLength(30);\n});\n\ntest(\"solveFlatAdjacentEdges, bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold, [6]),\n\t};\n\tear.graph.populate(folded);\n\n\tconst faces_winding = ear.graph.makeFacesWinding(folded);\n\tconst adjacentOrders = ear.layer.solveFlatAdjacentEdges(folded, faces_winding);\n\n\texpect(faces_winding[6]).toBe(true);\n\texpect(Object.keys(adjacentOrders)).toHaveLength(338);\n});\n\ntest(\"solveFlatAdjacentEdges, 3D cube-octagon\", () => {\n\tconst foldfile = fs.readFileSync(\n\t\t\"./tests/files/fold/cube-octagon.fold\",\n\t\t\"utf-8\",\n\t);\n\tconst fold = JSON.parse(foldfile);\n\t// the second folded frame is the one without the flat assignments\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[1];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\tplanes,\n\t\tplanes_faces,\n\t\t// planes_transform,\n\t\t// planes_clusters,\n\t\tfaces_winding,\n\t\tfaces_plane,\n\t\t// faces_cluster,\n\t\t// clusters_plane,\n\t\t// clusters_faces,\n\t} = ear.graph.getCoplanarAdjacentOverlappingFaces(folded);\n\n\tconst {\n\t\t// planes_transform,\n\t\t// faces_plane,\n\t\tfaces_cluster,\n\t\tfaces_winding: faces_winding2,\n\t\t// faces_polygon,\n\t\t// faces_center,\n\t\t// clusters_faces,\n\t\tclusters_graph,\n\t\t// clusters_transform,\n\t\t// facesFacesOverlap,\n\t\t// facePairs,\n\t} = ear.layer.constraints3DFaceClusters(folded);\n\n\texpect(planes).toMatchObject([\n\t\t{ normal: [0, 1, 0], origin: [0, 2, 0] },\n\t\t{ normal: [0, 1, 0], origin: [0, 3, 0] },\n\t\t{ normal: [0, 0, -1], origin: [0, 0, -0] },\n\t\t{ normal: [0, 0, -1], origin: [0, 0, -1] },\n\t\t{ normal: [1, 0, 0], origin: [2, 0, 0] },\n\t\t{ normal: [1, 0, 0], origin: [3, 0, 0] },\n\t]);\n\n\t// ensure faces_winding is correct within its plane.\n\t// noTransformWinding is performed on the input folded form which appears\n\t// to have a flat plane in the XY, which when done proper is given the\n\t// normal (0, 0, -1), so, when done proper, all windings will be flipped\n\t// at least from the faces in planes_faces[3] (other planes will be corrupt\n\t// as we are simply projecting 3D down into 2D creating degenerate faces)\n\texpect(faces_winding).toMatchObject(faces_winding2);\n\tconst noTransformWinding = ear.graph.makeFacesWinding(folded);\n\tplanes_faces[3]\n\t\t.forEach(f => expect(faces_winding[f]).not.toEqual(noTransformWinding[f]));\n\n\t// it just so happens that in this example all planes match clusters\n\t// making planes and clusters synonymous\n\texpect(faces_plane).toMatchObject(faces_cluster);\n\n\t// the vertices coords of the folded form\n\texpect(folded.vertices_coords).toMatchObject([\n\t\t[2, 2, 0], [2, 2, -1], [2, 1, -1], [3, 1, -1], [3, 2, -1], [3, 2, 0],\n\t\t[2, 3, 0], [2, 3, -1], [1, 3, -1], [1, 2, -1], [2, 2, -1], [3, 3, 0],\n\t\t[4, 3, -1], [3, 3, 0], [2, 2, 0], [1, 2, -1], [2, 2, -1], [1.5, 1.5, -1],\n\t\t[3, 3, -1], [3.5, 3.5, -1], [3, 3, -1], [2, 3, -1], [2, 4, -1], [3, 2, -1],\n\t\t[3, 1, -1], [3, 2, -1], [2, 3, 0], [2, 3, -1], [3, 2, 0], [1.5, 3.5, -1],\n\t\t[2, 4, -1], [4, 2, -1], [3.5, 1.5, -1], [3, 3, -1], [4, 3, -1], [3, 4, -1],\n\t]);\n\n\t// the vertices coords from the big flat plane (3), this plane has been flipped,\n\t// the normal is [0, 0, -1] so we should see the vertices appearing flipped.\n\texpect(clusters_graph[3].vertices_coords).toMatchObject([\n\t\t, [2, -2], [2, -1], [3, -1], [3, -2], ,\n\t\t, [2, -3], [1, -3], [1, -2], [2, -2], ,\n\t\t[4, -3], , , [1, -2], [2, -2], [1.5, -1.5],\n\t\t[3, -3], [3.5, -3.5], [3, -3], [2, -3], [2, -4], [3, -2],\n\t\t[3, -1], [3, -2], , [2, -3], , [1.5, -3.5],\n\t\t[2, -4], [4, -2], [3.5, -1.5], [3, -3], [4, -3], [3, -4],\n\t]);\n\n\t// four tortillas in the big flat plane (#3): 3, 11, 17, 28\n\t// have 1 instace each of overlaping one of the others\n\t// plane #3 normal is [0, 0, -1], so looking at the folded form from above,\n\t// all layer results should be sorted in flipped order.\n\texpect(faces_plane[3]).toBe(3);\n\texpect(faces_plane[11]).toBe(3);\n\texpect(faces_plane[17]).toBe(3);\n\texpect(faces_plane[28]).toBe(3);\n\n\t// let's focus on the relationship between faces 2 and 3 for a moment\n\n\t// cluster 3, faces 2 and 3 as polygons\n\tconst foldedPolygon2 = folded.faces_vertices[2]\n\t\t.map(v => folded.vertices_coords[v]);\n\tconst foldedPolygon3 = folded.faces_vertices[3]\n\t\t.map(v => folded.vertices_coords[v]);\n\tconst clusterGraphPolygon2 = clusters_graph[3].faces_vertices[2]\n\t\t.map(v => clusters_graph[3].vertices_coords[v]);\n\tconst clusterGraphPolygon3 = clusters_graph[3].faces_vertices[3]\n\t\t.map(v => clusters_graph[3].vertices_coords[v]);\n\n\t// folded face 2 appears to wind clockwise\n\texpect(foldedPolygon2).toMatchObject([[2, 2, -1], [2, 1, -1], [1, 2, -1]]);\n\t// folded face 3 appears to wind counter-clockwise\n\texpect(foldedPolygon3)\n\t\t.toMatchObject([[2, 1, -1], [3, 1, -1], [3.5, 1.5, -1], [3, 2, -1], [1, 2, -1]]);\n\n\t// folded face 2 appears to wind counter-clockwise\n\texpect(clusterGraphPolygon2).toMatchObject([[2, -2], [2, -1], [1, -2]]);\n\t// folded face 3 appears to wind clockwise\n\texpect(clusterGraphPolygon3)\n\t\t.toMatchObject([[2, -1], [3, -1], [3.5, -1.5], [3, -2], [1, -2]]);\n\n\t// the cluster graph's winding direction is the one reflected\n\t// in the faces_winding array.\n\t// face 3 is flipped because, despite it appearing to wind counter-clockwise,\n\t// the faces_winding is taken with respect to the plane's normal, [0, 0, -1],\n\t// creating a winding which will appear flipped from the apparent view.\n\texpect(faces_winding[2]).toBe(true);\n\texpect(faces_winding[3]).toBe(false);\n\n\t// solveFlatAdjacentEdges does not bother with vertices_coords, these two graphs\n\t// will have the same result\n\texpect(ear.layer.solveFlatAdjacentEdges(folded, faces_winding))\n\t\t.toMatchObject(ear.layer.solveFlatAdjacentEdges(clusters_graph[3], faces_winding));\n\n\t// face 2 is upright, face 3 is flipped\n\t// if faces 3 is flipped, a valley fold from face 3 to 2 places 2 BELOW face 3\n\t// note: this is just a subset of the total result.\n\texpect(ear.layer.solveFlatAdjacentEdges(folded, faces_winding)).toMatchObject({\n\t\t\"2 3\": 2, // 2 is below 3, even though from above, 2 appears to be above 3\n\t\t\"10 11\": 2, // 10 is below 11\n\t\t\"16 17\": 2, // 16 is below 17\n\t\t\"28 36\": 1, // 28 is above 36\n\t});\n\n\t// here's the rest\n\texpect(ear.layer.solveFlatAdjacentEdges(folded, faces_winding)).toMatchObject({\n\t\t\"14 15\": 2,\n\t\t\"18 19\": 1,\n\t\t\"25 26\": 1,\n\t\t\"0 1\": 1,\n\t\t\"11 27\": 1,\n\t\t\"12 13\": 2,\n\t\t\"20 28\": 2,\n\t\t\"21 29\": 1,\n\t\t\"9 30\": 1,\n\t\t\"11 24\": 1,\n\t\t\"25 27\": 1,\n\t\t\"0 2\": 1,\n\t\t\"1 13\": 1,\n\t\t\"3 26\": 1,\n\t\t\"34 35\": 2,\n\t\t\"7 36\": 2,\n\t\t\"11 12\": 1,\n\t\t\"13 27\": 2,\n\t\t\"22 24\": 2,\n\t\t\"18 33\": 2,\n\t\t\"22 26\": 2,\n\t\t\"24 25\": 1,\n\t\t\"30 32\": 2,\n\t\t\"17 31\": 1,\n\t\t\"6 35\": 2,\n\t\t\"3 4\": 1,\n\t\t\"3 35\": 1,\n\t\t\"4 6\": 2,\n\t\t\"18 22\": 1,\n\t\t\"19 23\": 1,\n\t\t\"22 23\": 2,\n\t\t\"26 34\": 1,\n\t\t\"5 7\": 1,\n\t\t\"23 34\": 1,\n\t\t\"24 33\": 2,\n\t\t\"8 9\": 1,\n\t\t\"14 16\": 2,\n\t\t\"19 20\": 2,\n\t\t\"23 28\": 2,\n\t\t\"5 6\": 2,\n\t\t\"8 10\": 1,\n\t\t\"32 33\": 1,\n\t\t\"17 18\": 1,\n\t\t\"15 21\": 2,\n\t\t\"17 32\": 1,\n\t\t\"30 31\": 1,\n\t\t\"20 21\": 1,\n\t\t\"28 29\": 1,\n\t});\n});\n"
  },
  {
    "path": "tests/layer.layer.solveLayerOrders.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"layer strip weave\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/strip-weave.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\t// these are all adjacent faces\n\t// the first call to propagate() resulted in nothing\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 2,\n\t\t\"4 5\": 1,\n\t\t\"1 2\": 1,\n\t\t\"2 3\": 2,\n\t\t\"5 6\": 2,\n\t});\n\n\t// three branches, only one pair in each\n\texpect(branches).toMatchObject([\n\t\t[{ orders: { \"1 4\": 1 } }, { orders: { \"1 4\": 2 } }],\n\t\t[{ orders: { \"2 5\": 1 } }, { orders: { \"2 5\": 2 } }],\n\t\t[{ orders: { \"3 6\": 1 } }, { orders: { \"3 6\": 2 } }],\n\t]);\n});\n\ntest(\"layer panels 4x2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-4x2.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 2,\n\t\t\"4 5\": 2,\n\t\t\"1 2\": 1,\n\t\t\"5 6\": 1,\n\t\t\"2 3\": 1,\n\t\t\"6 7\": 1,\n\t\t\"1 3\": 1,\n\t\t\"5 7\": 1,\n\t});\n\n\t// only one branch\n\t// the one branch has two solutions\n\texpect(branches).toMatchObject([[\n\t\t{ orders: { \"0 3\": 1, \"4 6\": 1, \"4 7\": 1, \"0 2\": 1 } },\n\t\t{ orders: { \"0 3\": 2, \"4 6\": 2, \"4 7\": 2, \"0 2\": 2 } },\n\t]]);\n});\n\ntest(\"layer panels 3x3\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-3x3.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\n\tconst expectedJSON = fs.readFileSync(\n\t\t\"./tests/files/json/panels-3x3-layer-solver.json\",\n\t\t\"utf-8\",\n\t);\n\tconst expected = JSON.parse(expectedJSON);\n\tdelete expected.faces_winding;\n\n\tconst {\n\t\tfaces_winding,\n\t\t...solution\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(solution).toMatchObject(expected);\n});\n\ntest(\"layer panels 3x3 no solution\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-3x3-invalid.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\n\ttry {\n\t\tear.layer.solveLayerOrders(folded);\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t}\n});\n\ntest(\"layer panels 5\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-5.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"3 4\": 1,\n\t\t\"2 3\": 1,\n\t\t\"1 2\": 1,\n\t\t\"0 1\": 2,\n\t\t\"1 4\": 1,\n\t\t\"1 3\": 1,\n\t\t\"2 4\": 1,\n\t});\n\n\t// two top-level possibilities\n\t// - one complete solution\n\t// - another with branches\n\texpect(branches).toMatchObject([[\n\t\t{ orders: { \"0 3\": 1, \"0 4\": 1, \"0 2\": 1 } },\n\t\t{\n\t\t\torders: { \"0 3\": 2, \"0 2\": 2 },\n\t\t\tbranches: [[\n\t\t\t\t{ orders: { \"0 4\": 1 } },\n\t\t\t\t{ orders: { \"0 4\": 2 } },\n\t\t\t]],\n\t\t},\n\t]]);\n});\n\ntest(\"layer windmill faces with a cycle\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/windmill.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 1,\n\t\t\"7 8\": 1,\n\t\t\"4 6\": 1,\n\t\t\"0 6\": 1,\n\t\t\"5 7\": 1,\n\t\t\"0 2\": 2,\n\t\t\"1 4\": 1,\n\t\t\"4 5\": 1,\n\t\t\"2 3\": 2,\n\t\t\"3 6\": 1,\n\t\t\"6 7\": 2,\n\t\t\"3 8\": 2,\n\t\t\"0 4\": 1,\n\t\t\"3 7\": 2,\n\t\t\"1 6\": 1,\n\t\t\"4 7\": 1,\n\t\t\"2 6\": 1,\n\t\t\"0 3\": 2,\n\t\t\"5 6\": 1,\n\t\t\"6 8\": 2,\n\t});\n\n\texpect(branches).toBe(undefined);\n});\n\ntest(\"layer panels zig-zag\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-zig-zag.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"0 4\": 2,\n\t\t\"3 4\": 1,\n\t\t\"2 3\": 1,\n\t\t\"1 2\": 1,\n\t\t\"2 4\": 1,\n\t\t\"1 4\": 1,\n\t\t\"0 1\": 2,\n\t\t\"0 3\": 2,\n\t\t\"1 3\": 1,\n\t\t\"0 2\": 2,\n\t});\n\texpect(branches).toBe(undefined);\n});\n\ntest(\"layer four flaps\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layer-4-flaps.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 2,\n\t\t\"1 2\": 1,\n\t\t\"1 3\": 1,\n\t\t\"1 4\": 1,\n\t});\n\n\t// four branches, each branch has 2 solutions\n\texpect(branches.length).toBe(4);\n\texpect(branches).toMatchObject([\n\t\t[{ orders: { \"0 4\": 1 } }, { orders: { \"0 4\": 2 } }],\n\t\t[{ orders: { \"0 3\": 1 } }, { orders: { \"0 3\": 2 } }],\n\t\t[{ orders: { \"2 3\": 1 } }, { orders: { \"2 3\": 2 } }],\n\t\t[{ orders: { \"2 4\": 1 } }, { orders: { \"2 4\": 2 } }],\n\t]);\n});\n\ntest(\"layer Kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst solution = ear.layer.solveLayerOrders(folded);\n\n\tconst expectedJSON = fs.readFileSync(\n\t\t\"./tests/files/json/kabuto-layer-solver.json\",\n\t\t\"utf-8\",\n\t);\n\tconst expected = JSON.parse(expectedJSON);\n\tdelete expected.faces_winding;\n\n\texpect(solution).toMatchObject(expected)\n});\n\ntest(\"layer Randlett flapping bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst solution = ear.layer.solveLayerOrders(folded);\n\n\tconst expectedJSON = fs.readFileSync(\n\t\t\"./tests/files/json/randlett-flapping-bird-layer-solver.json\",\n\t\t\"utf-8\",\n\t);\n\tconst expected = JSON.parse(expectedJSON);\n\tdelete expected.faces_winding;\n\n\texpect(solution).toMatchObject(expected);\n});\n\ntest(\"layer crane\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/crane.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst solution = ear.layer.solveLayerOrders(folded);\n\n\tconst expectedJSON = fs.readFileSync(\n\t\t\"./tests/files/json/crane-layer-solver.json\",\n\t\t\"utf-8\",\n\t);\n\tconst expected = JSON.parse(expectedJSON);\n\texpect(solution).toMatchObject(expected);\n});\n\n// test(\"layer bird\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst folded = {\n// \t\t...fold,\n// \t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n// \t}\n// \tconst solution = ear.layer.solveLayerOrders(folded);\n\n// \t// const expectedJSON = fs.readFileSync(\n// \t// \t\"./tests/files/json/crane-layer-solver.json\",\n// \t// \t\"utf-8\",\n// \t// );\n// \t// const expected = JSON.parse(expectedJSON);\n// \t// expect(solution).toMatchObject(expected);\n// });\n"
  },
  {
    "path": "tests/layer.layer.solveLayerOrders3D.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeSolverConstraints3D panels 6x2\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/panels-6x2-90deg.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tear.graph.populate(folded);\n\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.solveLayerOrders3D(folded);\n\n\texpect(orders).toMatchObject({\n\t\t\"0 1\": 2, \"1 2\": 2, \"2 3\": 2, \"3 4\": 1, \"4 5\": 1, \"6 7\": 2, \"7 8\": 2, \"8 9\": 2, \"9 10\": 1, \"10 11\": 1, \"0 3\": 2, \"1 3\": 2, \"3 5\": 1, \"6 9\": 2, \"7 9\": 2, \"9 11\": 1, \"0 2\": 2, \"6 8\": 2,\n\t})\n\n\texpect(branches).toMatchObject([\n\t\t[\n\t\t\t{\n\t\t\t\torders: { \"1 4\": 1, \"6 10\": 1, \"2 4\": 1, \"0 5\": 1, \"7 10\": 1, \"2 5\": 1, \"8 10\": 1, \"1 5\": 1, \"6 11\": 1, \"8 11\": 1, \"7 11\": 1, \"0 4\": 1 },\n\t\t\t},\n\t\t\t{\n\t\t\t\torders: { \"6 10\": 2, \"0 5\": 2, \"6 11\": 2, \"0 4\": 2 },\n\t\t\t\t// interesting that \"0 5\": 2, and \"6 11\": 2, aren't inside the \"orders\" section.\n\t\t\t\t// is this okay?\n\t\t\t\tbranches: [\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\torders: { \"1 4\": 1, \"2 5\": 1, \"8 10\": 1, \"7 10\": 1, \"8 11\": 1, \"1 5\": 1, \"7 11\": 1, \"2 4\": 1 },\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\torders: { \"1 4\": 2, \"2 5\": 2, \"8 10\": 2, \"7 10\": 2, \"8 11\": 2, \"1 5\": 2, \"7 11\": 2, \"2 4\": 2 },\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t},\n\t\t]\n\t]);\n});\n"
  },
  {
    "path": "tests/layer.propagate.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"propagate\", () => {});\n"
  },
  {
    "path": "tests/layer.prototype.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// ear.layer.faceOrdersToMatrix()\n// ear.layer.facesLayerToEdgesAssignments()\n// ear.layer.flipFacesLayer()\n// ear.layer.makeEpsilon()\n// ear.layer.constraintToFacePairs()\n// ear.layer.constraintToFacePairsStrings()\n\ntest(\"no depth\", () => {\n\tconst layerSolution = {\n\t\torders: { \"1 2\": 1 },\n\t};\n\tconst result = ear.layer.gatherAll(layerSolution);\n\n\t// only one solution\n\texpect(result.length).toBe(1);\n\n\t// the one solution only has one object\n\texpect(result[0].length).toBe(1);\n\texpect(result).toMatchObject([[{ \"1 2\": 1 }]]);\n});\n\ntest(\"one depth with one 'and' and two 'or' branches\", () => {\n\tconst layerSolution = {\n\t\torders: { \"1 2\": 1 },\n\t\tbranches: [\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 1 }\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 2 }\n\t\t\t\t}\n\t\t\t]\n\t\t]\n\t};\n\tconst result = ear.layer.gatherAll(layerSolution);\n\n\t// two solutions\n\texpect(result.length).toBe(2);\n\n\t// each of the two solutions contains two objects\n\texpect(result[0].length).toBe(2);\n\texpect(result[1].length).toBe(2);\n\n\texpect(result).toMatchObject([\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2 }],\n\t]);\n});\n\ntest(\"one depth with two 'and' and two 'or' branches\", () => {\n\tconst layerSolution = {\n\t\torders: { \"1 2\": 1 },\n\t\tbranches: [\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 1 }\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 2 }\n\t\t\t\t}\n\t\t\t],\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\torders: { \"5 6\": 1 }\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\torders: { \"5 6\": 2 }\n\t\t\t\t}\n\t\t\t]\n\t\t]\n\t};\n\tconst result = ear.layer.gatherAll(layerSolution);\n\n\texpect(result.length).toBe(4);\n\texpect(result[0].length).toBe(3);\n\texpect(result[1].length).toBe(3);\n\texpect(result[2].length).toBe(3);\n\texpect(result[3].length).toBe(3);\n\n\texpect(result).toMatchObject([\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 2 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2 }, { \"5 6\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2 }, { \"5 6\": 2 }],\n\t]);\n});\n\ntest(\"single-entry structure that mimics the Kabuto result\", () => {\n\tconst layerSolution = {\n\t\torders: { \"1 2\": 1 },\n\t\tbranches: [\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 1 },\n\t\t\t\t\tbranches: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\torders: { \"5 6\": 1 }\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\torders: { \"5 6\": 2 }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\torders: { \"3 4\": 2, \"5 6\": 1 }\n\t\t\t\t}\n\t\t\t],\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\torders: { \"7 8\": 1 },\n\t\t\t\t\tbranches: [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\torders: { \"9 10\": 1 }\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\torders: { \"9 10\": 2 }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\torders: { \"7 8\": 2, \"9 10\": 1 }\n\t\t\t\t}\n\t\t\t]\n\t\t]\n\t};\n\tconst result = ear.layer.gatherAll(layerSolution);\n\n\texpect(result).toMatchObject([\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 1 }, { \"7 8\": 1 }, { \"9 10\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 1 }, { \"7 8\": 1 }, { \"9 10\": 2 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 1 }, { \"7 8\": 2, \"9 10\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 2 }, { \"7 8\": 1 }, { \"9 10\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 2 }, { \"7 8\": 1 }, { \"9 10\": 2 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 1 }, { \"5 6\": 2 }, { \"7 8\": 2, \"9 10\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2, \"5 6\": 1 }, { \"7 8\": 1 }, { \"9 10\": 1 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2, \"5 6\": 1 }, { \"7 8\": 1 }, { \"9 10\": 2 }],\n\t\t[{ \"1 2\": 1 }, { \"3 4\": 2, \"5 6\": 1 }, { \"7 8\": 2, \"9 10\": 1 }],\n\t]);\n});\n"
  },
  {
    "path": "tests/layer.solver.2D.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// mostly 2D.\n// there is some overlap between 2D and 3D, for comparison sake.\n\ntest(\"2D layer solver, Randlett flapping bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/randlett-flapping-bird.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer(folded);\n\texpect(orders).toMatchObject(folded.faceOrders);\n\texpect(branches).toBe(undefined);\n});\n\ntest(\"2D layer solver, preliminary fold\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/preliminary-offset-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst solution = ear.layer(folded);\n\tconst faceOrders = solution.faceOrders();\n\t// going bottom up, two paths on either side\n\t// 0 to 2 to 4 to 5\n\t// 0 to 1 to 3 to 5\n\tconst faceOrdersStrings = faceOrders.map(order => JSON.stringify(order));\n\t// 0 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 0, 1]))).toBe(true);\n\t// 0 and 1, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 1, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([1, 0, 1]))).toBe(true);\n\t// 0 and 3, both facing same direction, 3 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 3, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([3, 0, 1]))).toBe(true);\n\t// 1 and 3, both facing away from each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([1, 3, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([3, 1, -1]))).toBe(true);\n\t// 1 and 5, both facing same direction, 1 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([1, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 1, -1]))).toBe(true);\n\t// 3 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([3, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 3, 1]))).toBe(true);\n\t// 0 and 2, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 2, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([2, 0, 1]))).toBe(true);\n\t// 0 and 4, both facing same direction, 3 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 4, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([4, 0, 1]))).toBe(true);\n\t// 2 and 4, both facing away from each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 4, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([4, 2, -1]))).toBe(true);\n\t// 2 and 5, both facing same direction, 1 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 2, -1]))).toBe(true);\n\t// 4 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([4, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 4, 1]))).toBe(true);\n});\n\ntest(\"3D layer solver, preliminary fold\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/preliminary-offset-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst solution = ear.layer.layer3D(folded);\n\tconst faceOrders = solution.faceOrders();\n\t// going bottom up, two paths on either side\n\t// 0 to 2 to 4 to 5\n\t// 0 to 1 to 3 to 5\n\tconst faceOrdersStrings = faceOrders.map(order => JSON.stringify(order));\n\t// 0 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 0, 1]))).toBe(true);\n\t// 0 and 1, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 1, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([1, 0, 1]))).toBe(true);\n\t// 0 and 3, both facing same direction, 3 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 3, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([3, 0, 1]))).toBe(true);\n\t// 1 and 3, both facing away from each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([1, 3, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([3, 1, -1]))).toBe(true);\n\t// 1 and 5, both facing same direction, 1 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([1, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 1, -1]))).toBe(true);\n\t// 3 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([3, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 3, 1]))).toBe(true);\n\t// 0 and 2, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 2, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([2, 0, 1]))).toBe(true);\n\t// 0 and 4, both facing same direction, 3 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 4, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([4, 0, 1]))).toBe(true);\n\t// 2 and 4, both facing away from each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 4, -1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([4, 2, -1]))).toBe(true);\n\t// 2 and 5, both facing same direction, 1 on top\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 2, -1]))).toBe(true);\n\t// 4 and 5, both facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([4, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 4, 1]))).toBe(true);\n});\n\ntest(\"2D layer solver, preliminary fold, layers linearized\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/preliminary-offset-cp.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFlatFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst solution = ear.layer(folded);\n\tconst faceOrders = solution.faceOrders();\n\t// going bottom up, two paths on either side\n\t// 0 to 2 to 4 to 5\n\t// 0 to 1 to 3 to 5\n\t// this just happens to generate 0, 1, 2, 3, 4, 5\n\t// but a few others would still be valid.\n\tconst linearize = ear.graph.linearize2DFaces({ ...folded, faceOrders });\n\texpect(linearize).toMatchObject([0, 1, 2, 3, 4, 5]);\n});\n\ntest(\"2D layer solver, kabuto\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kabuto.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst solution = ear.layer(folded);\n\texpect(solution.structure()).toMatchObject([\n\t\t[[[[], []]], []],\n\t\t[[[[], []]], []],\n\t]);\n\tconst layerSolutions = solution.compileAll();\n\texpect(layerSolutions).toHaveLength(9);\n\tlayerSolutions.forEach(orders => expect(orders).toHaveLength(117));\n});\n\ntest(\"2D layer solver, kraft-bird\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/kraft-bird-base.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded = {\n\t\t...fold,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n\t};\n\n\tconst faceOrders = ear.layer(folded).faceOrders();\n\texpect(faceOrders).toHaveLength(5848);\n\n\tfs.writeFileSync(`./tests/tmp/kraft-bird-base-solved.fold`, JSON.stringify({\n\t\t...folded,\n\t\tfaceOrders,\n\t}));\n});\n\n// // takes about 2 seconds\n// test(\"2D layer solver, zipper\", () => {\n// \tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-zipper.fold\", \"utf-8\");\n// \tconst fold = JSON.parse(foldfile);\n// \tconst folded = {\n// \t\t...fold,\n// \t\tvertices_coords: ear.graph.makeVerticesCoordsFlatFolded(fold),\n// \t}\n// \tconst result = ear.layer(folded);\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/layers-zipper-solution.fold`,\n// \t\tJSON.stringify(result),\n// \t);\n// \tconst {\n// \t\tfaces_winding,\n// \t\t...resultFlat\n// \t} = ear.layer.solveLayerOrdersSingleBranches(folded);\n// \tfs.writeFileSync(\n// \t\t`./tests/tmp/layers-zipper-solution-flat.fold`,\n// \t\tJSON.stringify(resultFlat),\n// \t);\n// });\n"
  },
  {
    "path": "tests/layer.solver.3D.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"3D layer solver, all 3D special cases\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/layers-3d-edge-edge.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst frames = ear.graph.getFileFramesAsArray(fold);\n\tconst foldedForms = frames.map(frame => ({\n\t\t...frame,\n\t\tvertices_coords: ear.graph.makeVerticesCoordsFolded(frame),\n\t}));\n\tfoldedForms.forEach(folded => ear.graph.populate(folded));\n\n\t// the final\n\tconst results = foldedForms.map(graph => {\n\t\ttry {\n\t\t\treturn ear.layer.layer3D(graph);\n\t\t} catch (error) {\n\t\t\treturn \"error\";\n\t\t}\n\t});\n\n\texpect(results).toMatchObject([\n\t\t// 0 and 4 face each other's normals\n\t\t{ orders: [[0, 4, 1]] },\n\t\t// both pairs of faces face each other's normals\n\t\t{ orders: [[1, 4, 1], [2, 3, 1]] },\n\t\t// same as above\n\t\t{ orders: [[1, 4, 1], [2, 3, 1]] },\n\t\t// all 3 pairs face into each other's normals\n\t\t{ orders: [[2, 3, 1], [1, 4, 1], [0, 5, 1]] },\n\t\t// 0-1 normals point away from each other\n\t\t// 2-3 normals point towards each other\n\t\t// 1-4 normals point towards each other\n\t\t// 0-4 0 is above 4's normal\n\t\t{ orders: [[0, 1, -1], [2, 3, 1], [1, 4, 1], [0, 4, 1]] },\n\t\t// todo\n\t\t{ orders: [[1, 9, -1], [3, 7, -1], [2, 8, -1]] },\n\t\t// todo\n\t\t{ orders: [\n\t\t\t[1, 10, -1], [4, 5, 1], [5, 6, -1], [6, 7, -1], [4, 6, -1],\n\t\t\t[5, 7, -1], [2, 9, -1], [4, 7, -1], [3, 8, -1]\n\t\t] },\n\t\t\"error\",\n\t\t{ orders: [] }, // this is a layer-crossing error. undetected for now\n\t\t\"error\",\n\t\t\"error\",\n\t]);\n});\n\ntest(\"3D layer solver, cube octagon\", () => {\n\tconst foldfile = fs.readFileSync(\"./tests/files/fold/cube-octagon.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldfile);\n\tconst folded0 = ear.graph.getFramesByClassName(fold, \"foldedForm\")[0];\n\tconst folded1 = ear.graph.getFramesByClassName(fold, \"foldedForm\")[1];\n\tconst result0 = ear.layer.layer3D(folded0);\n\tconst result1 = ear.layer.layer3D(folded1);\n\texpect(result0.orders).toMatchObject(folded0.faceOrders);\n\texpect(result1.orders).toMatchObject(folded1.faceOrders);\n\texpect(result0.branches).toBe(undefined);\n\texpect(result1.branches).toBe(undefined);\n});\n\ntest(\"3D layer solver, square-fish base\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst solution = ear.layer.layer3D(folded);\n\tconst faceOrders = solution.faceOrders();\n\tconst faceOrdersStrings = faceOrders.map(order => JSON.stringify(order));\n\n\texpect(faceOrders.length).toBe(10);\n\n\t// 0 and 1, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([0, 1, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([1, 0, 1]))).toBe(true);\n\t// 2 and 3, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 3, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([3, 2, 1]))).toBe(true);\n\t// 4 and 5, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([4, 5, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([5, 4, 1]))).toBe(true);\n\t// 6 and 7, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([6, 7, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([7, 6, 1]))).toBe(true);\n\t// 10 and 11, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([10, 11, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([11, 10, 1]))).toBe(true);\n\t// 2 and 13, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([2, 13, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([13, 2, 1]))).toBe(true);\n\t// 8 and 13, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([8, 13, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([13, 8, 1]))).toBe(true);\n\t// 9 and 14, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([9, 14, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([14, 9, 1]))).toBe(true);\n\t// 1 and 14, adjacent, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([1, 14, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([14, 1, 1]))).toBe(true);\n\t// 12 and 15, facing each other\n\texpect(faceOrdersStrings.includes(JSON.stringify([12, 15, 1]))\n\t\t|| faceOrdersStrings.includes(JSON.stringify([15, 12, 1]))).toBe(true);\n});\n\ntest(\"3D layer solver, square-fish base, faces sets\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/square-fish-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst vertices_coords = ear.graph.makeVerticesCoordsFolded(graph);\n\tconst folded = { ...graph, vertices_coords };\n\tconst faceOrders = [\n\t\t[0, 1, 1], [2, 3, 1], [4, 5, 1], [6, 7, 1], [10, 11, 1],\n\t\t[2, 13, 1], [8, 13, 1], [9, 14, 1], [1, 14, 1], [12, 15, 1],\n\t];\n\tconst nudge = ear.graph.nudgeFacesWithFaceOrders({ ...folded, faceOrders });\n\n\t// faces\n\t[0, 14, 1, 9].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n).toBeCloseTo([0, 0, 1][i])));\n\t[2, 3, 8, 13].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n).toBeCloseTo([0, 0, -1][i])));\n\t[4, 5].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n)\n\t\t\t.toBeCloseTo([0.9238795325112866, -0.38268343236509034, 0][i])));\n\t[6, 7].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n)\n\t\t\t.toBeCloseTo([0.9238795325112863, 0.3826834323650909, 0][i])));\n\t[10, 11].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n).toBeCloseTo([0, -1, 0][i])));\n\t[12, 15].forEach(f => nudge[f].vector\n\t\t.forEach((n, i) => expect(n).toBeCloseTo([0, 1, 0][i])));\n\n\t[0, 14, 1, 9].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n\t[2, 3, 8, 13].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n\t[4, 5].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n\t[6, 7].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n\t[10, 11].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n\t[12, 15].forEach((f, i) => expect(nudge[f].layer).toBe(i));\n});\n\ntest(\"3D layer solver, Mooser's train\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/moosers-train.fold\", \"utf-8\");\n\tconst folded = JSON.parse(FOLD);\n\tconst {\n\t\torders,\n\t\tbranches,\n\t} = ear.layer.layer3D(folded);\n\n\t// something so confusing.\n\t// in the source code for \"getEdgesLine\", the line:\n\t// const edgesLine = edgesToLines3({ vertices_coords, edges_vertices });\n\t// switch it out for \"edgesToLines\" and 1633 becomes 1581.\n\n\texpect(JSON.stringify(folded.vertices_coords.map(ear.math.resize3)))\n\t\t.toMatchObject(JSON.stringify(folded.vertices_coords));\n\n\texpect(JSON.stringify(ear.graph.edgesToLines(folded)))\n\t\t.toMatchObject(JSON.stringify(ear.graph.edgesToLines3(folded)));\n\n\t// expect(Object.keys(orders).length).toBe(1713);\n\texpect(Object.keys(orders).length).toBe(1633);\n\t// expect(Object.keys(orders).length).toBe(1581);\n\texpect(ear.layer.getBranchStructure({ branches })).toMatchObject([\n\t\t[[], []], [[], []],\n\t\t[[], []], [[], []],\n\t\t[[], []], [[], []],\n\t\t[[], []], [[], []],\n\t\t[[], []], [[], []],\n\t\t[[], []], [[], []],\n\t\t// [[], []], [[], []],\n\t\t// [[], []], [[], []],\n\t\t// [[], []], [[], []],\n\t]);\n\n\t// expect(branches).toMatchObject([\n\t// \t[\n\t// \t\t{ orders: [[52, 115, 1], [52, 131, 1], [52, 128, -1], [52, 113, -1]]},\n\t// \t\t{ orders: [[52, 115, -1], [52, 131, -1], [52, 128, 1], [52, 113, 1]]},\n\t// \t], [\n\t// \t\t{ orders: [[108, 114, -1], [108, 136, -1], [108, 137, 1], [108, 116, 1]]},\n\t// \t\t{ orders: [[108, 114, 1], [108, 136, 1], [108, 137, -1], [108, 116, -1]]},\n\t// \t]\n\t// ]);\n\tfolded.faceOrders = ear.layer.compile({ orders, branches });\n\tfs.writeFileSync(`./tests/tmp/moosers-train-layer-solved.fold`, JSON.stringify(folded));\n});\n\ntest(\"3D layer solver, maze-u\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/maze-u.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst foldedFrame = ear.graph.getFramesByClassName(graph, \"foldedForm\")[0];\n\tfoldedFrame.faceOrders = ear.layer.layer3D(foldedFrame).faceOrders();\n\tfs.writeFileSync(`./tests/tmp/maze-u-layer-solved.fold`, JSON.stringify(foldedFrame));\n\n\t// every face has some order associated with it\n\tconst facesFound = [];\n\tfoldedFrame.faceOrders.forEach(order => {\n\t\tfacesFound[order[0]] = true;\n\t\tfacesFound[order[1]] = true;\n\t});\n\texpect(facesFound.filter(a => a !== undefined).length)\n\t\t.toBe(foldedFrame.faces_vertices.length);\n});\n\ntest(\"3D layer solver, maze-s\", () => {\n\tconst FOLD = fs.readFileSync(\"./tests/files/fold/maze-s.fold\", \"utf-8\");\n\tconst graph = JSON.parse(FOLD);\n\tconst foldedFrame = ear.graph.getFramesByClassName(graph, \"foldedForm\")[0];\n\tconst solution = ear.layer.layer3D(foldedFrame);\n\tconst solutionCounts = solution.count();\n\tfoldedFrame.faceOrders = solution.faceOrders();\n\tfoldedFrame.file_frames = solutionCounts\n\t\t.flatMap((count, i) => Array.from(Array(count))\n\t\t\t.map((_, j) => {\n\t\t\t\tconst solutionIndices = solutionCounts.map(() => 0);\n\t\t\t\tsolutionIndices[i] = j;\n\t\t\t\treturn solutionIndices;\n\t\t\t})\n\t\t\t.map(solutionIndices => ({\n\t\t\t\tframe_parent: 0,\n\t\t\t\tframe_inherit: true,\n\t\t\t\tfaceOrders: solution.faceOrders(...solutionIndices),\n\t\t\t})));\n\tfs.writeFileSync(`./tests/tmp/maze-s-layer-solved.fold`, JSON.stringify(foldedFrame));\n\texpect(true).toBe(true);\n});\n\n// test(\"3D layer solver, 8x8 maze\", () => {\n// \tconst FOLD = fs.readFileSync(\"./tests/files/fold/maze-8x8.fold\", \"utf-8\");\n// \tconst graph = JSON.parse(FOLD);\n// \tconst foldedFrame = ear.graph.getFramesByClassName(graph, \"foldedForm\")[0];\n// \tfoldedFrame.faceOrders = ear.layer.layer3D(foldedFrame).faceOrders();\n// \tfs.writeFileSync(`./tests/tmp/maze-8x8-layer-solved.fold`, JSON.stringify(foldedFrame));\n// \texpect(true).toBe(true);\n// });\n"
  },
  {
    "path": "tests/layer.table.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"layer table\", () => {\n\tconst layerTableJSON = fs.readFileSync(\n\t\t\"./tests/files/json/layer-table.json\",\n\t\t\"utf-8\",\n\t);\n\tconst table = JSON.parse(layerTableJSON);\n\texpect(ear.layer.table).toMatchObject(table);\n});\n\ntest(\"immutability test\", () => {\n\t// taco_taco at 000011 is a const array entry,\n\t// attempt to overwrite one of its values.\n\ttry {\n\t\tear.layer.table.taco_taco[\"000011\"][0] = 99;\n\t} catch (error) {\n\t\texpect(error).not.toBe(undefined);\n\t}\n\n\t// the object should have been recursively frozen,\n\t// it should not be possible to change a value even inside an array.\n\texpect(ear.layer.table.taco_taco[\"000011\"][0]).not.toBe(99);\n\texpect(ear.layer.table.taco_taco[\"000011\"][0]).toBe(2);\n});\n\ntest(\"layer table structure\", () => {\n\tconst keys = [\"taco_taco\", \"taco_tortilla\", \"tortilla_tortilla\", \"transitivity\"];\n\tObject.keys(ear.layer.table)\n\t\t.forEach((key, i) => expect(key).toBe(keys[i]));\n\tkeys.forEach(key => expect(typeof ear.layer.table[key]).toBe(\"object\"));\n\tconst keysLength = [729, 27, 9, 27];\n\tkeys.forEach((key, i) => expect(Object.keys(ear.layer.table[key]).length).toBe(keysLength[i]));\n});\n\ntest(\"layer table elements\", () => {\n\texpect(ear.layer.table.taco_taco[\"000000\"]).toBe(true);\n\texpect(ear.layer.table.taco_taco[\"000122\"]).toBe(false);\n\texpect(JSON.stringify(ear.layer.table.taco_taco[\"000011\"]))\n\t\t.toBe(JSON.stringify([2, 2]));\n\n\texpect(ear.layer.table.taco_tortilla[\"000\"]).toBe(true);\n\texpect(ear.layer.table.taco_tortilla[\"011\"]).toBe(false);\n\texpect(JSON.stringify(ear.layer.table.taco_tortilla[\"110\"]))\n\t\t.toBe(JSON.stringify([2, 2]));\n\n\texpect(ear.layer.table.tortilla_tortilla[\"00\"]).toBe(true);\n\texpect(ear.layer.table.tortilla_tortilla[\"12\"]).toBe(false);\n\texpect(JSON.stringify(ear.layer.table.tortilla_tortilla[\"10\"]))\n\t\t.toBe(JSON.stringify([1, 1]));\n\n\texpect(ear.layer.table.transitivity[\"000\"]).toBe(true);\n\texpect(ear.layer.table.transitivity[\"111\"]).toBe(false);\n\texpect(JSON.stringify(ear.layer.table.transitivity[\"110\"]))\n\t\t.toBe(JSON.stringify([2, 2]));\n});\n"
  },
  {
    "path": "tests/layer.transitivity.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"makeTransitivity\", () => {});\n\ntest(\"getTransitivityTriosFromTacos\", () => {});\n"
  },
  {
    "path": "tests/lineno.js",
    "content": "import fs from \"fs\";\n\nconst repoName = \"Origami\";\n\nconst regex = /\\*(\\s*)\\@linkcode.*/g;\n\nconst processFile = (path) => {\n\t// console.log(\"checking file\", path);\n\tconst contents = fs.readFileSync(path, \"utf8\");\n\tconst matches = contents.match(regex);\n\tif (!matches) { return; }\n\tconst lineNumbers = contents\n\t\t.split(/\\r?\\n/)\n\t\t.map((line, i) => (line.match(regex) !== null ? i : undefined))\n\t\t.filter(a => a !== undefined)\n\t\t.map(num => num + 1); // line numbers start with 1\n\tif (matches.length !== lineNumbers.length) {\n\t\tconsole.log(\"unexpected error! the lengths of matches don't equal\"); return;\n\t}\n\tconst values = lineNumbers.map(num => `${repoName} ${path} ${num}`);\n\tconst lines = values.map(value => `* @linkcode ${value}`);\n\n\tlet count = 0;\n\tconst fn = () => lines[count++];\n\tconst modified = contents.replace(regex, fn);\n\tfs.writeFileSync(path, modified);\n\n\t// console.log(`modified ${path} with ${matches.length} line number entries`);\n};\n\nconst searchDir = (path) => {\n\tconst contents = fs.readdirSync(path, { withFileTypes: true });\n\tconst files = contents.filter(el => el.isFile());\n\tconst directories = contents.filter(el => el.isDirectory());\n\t// console.log(\"checking dir\", path);\n\t// console.log(\"files\", files);\n\t// console.log(\"directories\", directories);\n\tfiles.forEach(el => processFile(`${path}/${el.name}`));\n\tdirectories.forEach(el => searchDir(`${path}/${el.name}`));\n};\n\nsearchDir(\"./src\");\n"
  },
  {
    "path": "tests/math.algebra.matrix2.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectors = function (...args) {\n\texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n};\n\ntest(\"matrix 2 core\", () => {\n\texpect(ear.math.invertMatrix2([1, 0, 0, 1, 0, 0])).not.toBe(undefined);\n\tconst r1 = ear.math.makeMatrix2Translate();\n\texpect(r1[0]).toBe(1);\n\texpect(r1[4]).toBe(0);\n\texpect(r1[5]).toBe(0);\n\tconst r2 = ear.math.makeMatrix2Scale([2, -1]);\n\texpect(r2[0]).toBe(2);\n\texpect(r2[3]).toBe(-1);\n});\n\ntest(\"matrix 2 transform line\", () => {\n\tconst result = ear.math.multiplyMatrix2Line2(\n\t\tear.math.makeMatrix2Scale([0.5, 0.5, 0.5]),\n\t\t{ vector: [1, 1], origin: [1, 0] },\n\t);\n\texpect(result.vector[0]).toBeCloseTo(0.5);\n\texpect(result.vector[1]).toBeCloseTo(0.5);\n\texpect(result.origin[0]).toBeCloseTo(0.5);\n\texpect(result.origin[1]).toBeCloseTo(0);\n});\n\ntest(\"makeMatrix2Scale\", () => {\n\ttestEqualVectors(\n\t\tear.math.makeMatrix2Scale(),\n\t\t[1, 0, 0, 1, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix2Scale([0.5, 0.5]),\n\t\t[0.5, 0, 0, 0.5, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix2UniformScale(),\n\t\t[1, 0, 0, 1, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix2UniformScale(0.5),\n\t\t[0.5, 0, 0, 0.5, 0, 0],\n\t);\n});\n\ntest(\"matrix 2\", () => {\n\t// top level types\n\ttestEqualVectors([1, 0, 0, 1, 6, 7], ear.math.makeMatrix2Translate(6, 7));\n\ttestEqualVectors([3, 0, 0, 3, -2, 0], ear.math.makeMatrix2Scale([3, 3], [1, 0]));\n\ttestEqualVectors([0, 1, 1, -0, -8, 8], ear.math.makeMatrix2Reflect([1, 1], [-5, 3]));\n\ttestEqualVectors(\n\t\t[Math.SQRT1_2, Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2, 1, 1],\n\t\tear.math.makeMatrix2Rotate(Math.PI / 4, [1, 1]),\n\t);\n\ttestEqualVectors(\n\t\t[Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2,\n\t\t\tMath.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2],\n\t\tear.math.invertMatrix2([Math.SQRT1_2, Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2, 1, 0]),\n\t);\n\ttestEqualVectors(\n\t\t[Math.sqrt(4.5), Math.SQRT1_2, -Math.SQRT1_2, Math.sqrt(4.5), Math.sqrt(4.5), Math.SQRT1_2],\n\t\tear.math.multiplyMatrices2(\n\t\t\t[Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2, Math.SQRT1_2, 0, 0],\n\t\t\t[1, 2, -2, 1, 1, 2],\n\t\t),\n\t);\n\ttestEqualVectors(\n\t\t[0, 3],\n\t\tear.math.multiplyMatrix2Vector2([2, 1, -1, 2, -1, 0], [1, 1]),\n\t);\n\ttestEqualVectors(\n\t\t[-2, 3],\n\t\tear.math.multiplyMatrix2Vector2(\n\t\t\tear.math.makeMatrix2Scale([3, 3], [1, 0]),\n\t\t\t[0, 1],\n\t\t),\n\t);\n\ttestEqualVectors(\n\t\t[-1, 2],\n\t\tear.math.multiplyMatrix2Vector2(\n\t\t\tear.math.makeMatrix2Scale([3, 3], [0.5, 0.5]),\n\t\t\t[0, 1],\n\t\t),\n\t);\n\ttestEqualVectors(\n\t\t[1, 1],\n\t\tear.math.multiplyMatrix2Vector2(\n\t\t\tear.math.makeMatrix2Scale([0.5, 0.5], [1, 1]),\n\t\t\t[1, 1],\n\t\t),\n\t);\n\ttestEqualVectors(\n\t\t[0.75, 0.75],\n\t\tear.math.multiplyMatrix2Vector2(\n\t\t\tear.math.makeMatrix2Scale([0.5, 0.5], [0.5, 0.5]),\n\t\t\t[1, 1],\n\t\t),\n\t);\n});\n"
  },
  {
    "path": "tests/math.algebra.matrix3.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectors = function (...args) {\n\texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n};\n\ntest(\"isIdentity\", () => {\n\texpect(ear.math.isIdentity3x4([1, 2, 3, 4, 5, 6, 7, 8, 9])).toBe(false);\n\texpect(ear.math.isIdentity3x4([1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 5, 6])).toBe(false);\n\texpect(ear.math.isIdentity3x4([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0])).toBe(true);\n});\n\ntest(\"multiply\", () => {\n\tconst m = ear.math.multiplyMatrices3(\n\t\tear.math.makeMatrix3RotateX(Math.PI / 4),\n\t\tear.math.makeMatrix3RotateZ(Math.PI / 4),\n\t);\n\tconst sq = Math.SQRT1_2;\n\t[sq, 0.5, 0.5, -sq, 0.5, 0.5, 0, -sq, sq, 0, 0, 0]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"determinant\", () => {\n\t// randomly made matrices, made using these affine transforms,\n\t// even their inverted forms, should all have a determinant of 1\n\texpect(ear.math.determinant3([...ear.math.identity3x4])).toBeCloseTo(1);\n\texpect(ear.math.determinant3(ear.math.makeMatrix3RotateX(Math.random() * Math.PI * 2)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant3(ear.math.makeMatrix3RotateZ(Math.random() * Math.PI * 2)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant3(ear.math.makeMatrix3Translate(1, 2, 3)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant3(ear.math.invertMatrix3(\n\t\tear.math.makeMatrix3Rotate(Math.random() * Math.PI * 2, [1, 5, 8]),\n\t))).toBeCloseTo(1);\n});\n\ntest(\"inverse\", () => {\n\tconst m = ear.math.invertMatrix3([0, -1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]);\n\t[0, 1, -0, -1, 0, 0, 0, -0, 1, 0, 0, 0]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"translate\", () => {\n\tconst m = ear.math.makeMatrix3Translate(4, 5, 6);\n\texpect(m[9]).toBe(4);\n\texpect(m[10]).toBe(5);\n\texpect(m[11]).toBe(6);\n});\n\ntest(\"rotateX\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix3RotateX(Math.PI / 4);\n\t[1, 0, 0, 0, sq, sq, 0, -sq, sq, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotateY\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix3RotateY(Math.PI / 4);\n\t[sq, 0, -sq, 0, 1, 0, sq, 0, sq, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotateZ\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix3RotateZ(Math.PI / 4);\n\t[sq, sq, 0, -sq, sq, 0, 0, 0, 1, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotate\", () => {\n\tconst m = ear.math.makeMatrix3Rotate(Math.PI / 2, [1, 1, 1], [0, 0, 0]);\n\texpect(m[2]).toBeCloseTo(m[3]);\n\texpect(m[0]).toBeCloseTo(m[4]);\n\texpect(m[1]).toBeCloseTo(m[5]);\n});\n\ntest(\"scale\", () => {\n\tconst m = ear.math.makeMatrix3Scale([0.5, 0.5, 0.5]);\n\t[0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"makeMatrix3Scale\", () => {\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3Scale(),\n\t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3Scale([0.5, 0.5, 0.5]),\n\t\t[0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3UniformScale(),\n\t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3UniformScale(0.5),\n\t\t[0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0],\n\t);\n});\n\ntest(\"combine operations\", () => {\n\tconst result = ear.math.multiplyMatrices3(\n\t\tear.math.makeMatrix3RotateX(Math.PI / 2),\n\t\tear.math.makeMatrix3Translate(40, 20, 10),\n\t);\n\t[1, 0, 0, 0, 0, 1, 0, -1, 0, 40, -10, 20]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(result[i]));\n});\n\ntest(\"reflectZ\", () => {\n\tconst m = ear.math.makeMatrix3ReflectZ([1, 1, 1], [0, 0, 0]);\n\texpect(m[1]).toBeCloseTo(m[3]);\n\texpect(m[0]).toBeCloseTo(m[4]);\n\texpect(m[2]).toBeCloseTo(m[5]);\n\texpect(m[5]).toBeCloseTo(m[6]);\n});\n\ntest(\"transform\", () => {\n\tconst matrix = ear.math.multiplyMatrices3(\n\t\tear.math.makeMatrix3RotateZ(Math.PI / 2),\n\t\tear.math.makeMatrix3Translate(4, 5, 6),\n\t);\n\tconst segment = [[-1, 0, 0], [1, 0, 0]];\n\tconst result = segment\n\t\t.map(p => ear.math.multiplyMatrix3Vector3(matrix, p));\n\t[[-5, 3, 6], [-5, 5, 6]]\n\t\t.forEach((p, i) => p\n\t\t\t.forEach((n, j) => expect(n).toBe(result[i][j])));\n});\n\ntest(\"transformVector\", () => {\n\tconst vector = [1, 2, 3];\n\tconst result = ear.math.multiplyMatrix3Vector3(\n\t\tear.math.makeMatrix3Scale([0.5, 0.5, 0.5]),\n\t\tvector,\n\t);\n\texpect(result[0]).toBeCloseTo(0.5);\n\texpect(result[1]).toBeCloseTo(1);\n\texpect(result[2]).toBeCloseTo(1.5);\n});\n\ntest(\"transformLine\", () => {\n\tconst result = ear.math.multiplyMatrix3Line3(\n\t\tear.math.makeMatrix3Scale([0.5, 0.5, 0.5]),\n\t\t[0.707, 0.707, 0],\n\t\t[1, 0, 0],\n\t);\n\texpect(result.vector[0]).toBeCloseTo(0.3535);\n\texpect(result.vector[1]).toBeCloseTo(0.3535);\n\texpect(result.vector[2]).toBeCloseTo(0);\n\texpect(result.origin[0]).toBeCloseTo(0.5);\n\texpect(result.origin[1]).toBeCloseTo(0);\n\texpect(result.origin[2]).toBeCloseTo(0);\n});\n\ntest(\"matrix core invert\", () => {\n\texpect(ear.math.invertMatrix2([1, 0, 0, 1, 0, 0])).not.toBe(undefined);\n\texpect(ear.math.invertMatrix3([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0])).not.toBe(undefined);\n\texpect(ear.math.invertMatrix2([5, 5, 4, 4, 3, 3])).toBe(undefined);\n\texpect(ear.math.invertMatrix3([0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1])).toBe(undefined);\n});\n\n// todo: test matrix3 methods (invert) with the translation component to make sure it carries over\ntest(\"matrix 3 core, transforms\", () => {\n\tconst result1 = ear.math.makeMatrix3Translate();\n\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0].forEach((n, i) => expect(n).toBe(result1[i]));\n\t// rotate 360 degrees about an arbitrary axis and origin\n\ttestEqualVectors(\n\t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n\t\tear.math.makeMatrix3Rotate(\n\t\t\tMath.PI * 2,\n\t\t\t[Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5],\n\t\t\t[Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5],\n\t\t),\n\t);\n\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3RotateX(Math.PI / 6),\n\t\tear.math.makeMatrix3Rotate(Math.PI / 6, [1, 0, 0]),\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3RotateY(Math.PI / 6),\n\t\tear.math.makeMatrix3Rotate(Math.PI / 6, [0, 1, 0]),\n\t);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix3RotateZ(Math.PI / 6),\n\t\tear.math.makeMatrix3Rotate(Math.PI / 6, [0, 0, 1]),\n\t);\n\t// source wikipedia https://en.wikipedia.org/wiki/Rotation_matrix#Examples\n\tconst exampleMat = [\n\t\t0.35612209405955486, -0.8018106071106572, 0.47987165414043453,\n\t\t0.47987165414043464, 0.5975763087872217, 0.6423595182829954,\n\t\t-0.8018106071106572, 0.0015183876574496047, 0.5975763087872216,\n\t\t0, 0, 0,\n\t];\n\ttestEqualVectors(\n\t\texampleMat,\n\t\tear.math.makeMatrix3Rotate((-74 / 180) * Math.PI, [-1 / 3, 2 / 3, 2 / 3]),\n\t);\n\ttestEqualVectors(\n\t\texampleMat,\n\t\tear.math.makeMatrix3Rotate((-74 / 180) * Math.PI, [-0.5, 1, 1]),\n\t);\n\n\ttestEqualVectors(\n\t\t[1, 0, 0, 0, 0.8660254, 0.5, 0, -0.5, 0.8660254, 0, 0, 0],\n\t\tear.math.makeMatrix3Rotate(Math.PI / 6, [1, 0, 0]),\n\t);\n});\n\ntest(\"matrix 3 core\", () => {\n\texpect(ear.math.determinant3([1, 2, 3, 2, 4, 8, 7, 8, 9])).toBe(12);\n\texpect(ear.math.determinant3([3, 2, 0, 0, 0, 1, 2, -2, 1, 0, 0, 0])).toBe(10);\n\ttestEqualVectors(\n\t\t[4, 5, -8, -5, -6, 9, -2, -2, 3, 0, 0, 0],\n\t\tear.math.invertMatrix3([0, 1, -3, -3, -4, 4, -2, -2, 1, 0, 0, 0]),\n\t);\n\ttestEqualVectors(\n\t\t[0.2, -0.2, 0.2, 0.2, 0.3, -0.3, 0, 1, 0, 0, 0, 0],\n\t\tear.math.invertMatrix3([3, 2, 0, 0, 0, 1, 2, -2, 1, 0, 0, 0]),\n\t);\n\tconst mat_3d_ref = ear.math.makeMatrix3ReflectZ([1, -2], [12, 13]);\n\ttestEqualVectors(\n\t\tear.math.makeMatrix2Reflect([1, -2], [12, 13]),\n\t\t[mat_3d_ref[0], mat_3d_ref[1], mat_3d_ref[3], mat_3d_ref[4], mat_3d_ref[9], mat_3d_ref[10]],\n\t);\n\t// source wolfram alpha\n\ttestEqualVectors(\n\t\t[-682, 3737, -5545, 2154, -549, -1951, 953, -3256, 4401, 0, 0, 0],\n\t\tear.math.multiplyMatrices3(\n\t\t\t[5, -52, 85, 15, -9, -2, 32, 2, -50, 0, 0, 0],\n\t\t\t[-77, 25, -21, 3, 53, 42, 63, 2, 19, 0, 0, 0],\n\t\t),\n\t);\n});\n"
  },
  {
    "path": "tests/math.algebra.matrix4.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"isIdentity\", () => {\n\texpect(ear.math.isIdentity4x4([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]))\n\t\t.toBe(false);\n\texpect(ear.math.isIdentity4x4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4, 5, 6, 1]))\n\t\t.toBe(false);\n\texpect(ear.math.isIdentity4x4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]))\n\t\t.toBe(true);\n});\n\ntest(\"multiply\", () => {\n\tconst m = ear.math.multiplyMatrices4(\n\t\tear.math.makeMatrix4RotateX(Math.PI / 4),\n\t\tear.math.makeMatrix4RotateZ(Math.PI / 4),\n\t);\n\tconst sq = Math.SQRT1_2;\n\t[sq, 0.5, 0.5, 0, -sq, 0.5, 0.5, 0, 0, -sq, sq, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"determinant\", () => {\n\t// randomly made matrices, made using these affine transforms,\n\t// even their inverted forms, should all have a determinant of 1\n\texpect(ear.math.determinant4([...ear.math.identity4x4])).toBeCloseTo(1);\n\texpect(ear.math.determinant4(ear.math.makeMatrix4RotateX(Math.random() * Math.PI * 2)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant4(ear.math.makeMatrix4RotateZ(Math.random() * Math.PI * 2)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant4(ear.math.makeMatrix4Translate(1, 2, 3)))\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.determinant4(ear.math.invertMatrix4(\n\t\tear.math.makeMatrix4Rotate(Math.random() * Math.PI * 2, [1, 5, 8]),\n\t))).toBeCloseTo(1);\n});\n\ntest(\"inverse\", () => {\n\tconst m = ear.math.invertMatrix4([0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);\n\t[0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n\n\tconst bad = ear.math.invertMatrix4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, Infinity, 0, 1]);\n\texpect(bad).toBe(undefined);\n});\n\ntest(\"translate\", () => {\n\tconst m = ear.math.makeMatrix4Translate(4, 5, 6);\n\texpect(m[12]).toBe(4);\n\texpect(m[13]).toBe(5);\n\texpect(m[14]).toBe(6);\n});\n\ntest(\"rotateX\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix4RotateX(Math.PI / 4);\n\t[1, 0, 0, 0, 0, sq, sq, 0, 0, -sq, sq, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotateY\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix4RotateY(Math.PI / 4);\n\t[sq, 0, -sq, 0, 0, 1, 0, 0, sq, 0, sq, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotateZ\", () => {\n\tconst sq = Math.SQRT1_2;\n\tconst m = ear.math.makeMatrix4RotateZ(Math.PI / 4);\n\t[sq, sq, 0, 0, -sq, sq, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"rotate\", () => {\n\tconst m = ear.math.makeMatrix4Rotate(Math.PI / 2, [1, 1, 1], [0, 0, 0]);\n\texpect(m[2]).toBeCloseTo(m[4]);\n\texpect(m[4]).toBeCloseTo(m[9]);\n\texpect(m[0]).toBeCloseTo(m[5]);\n\texpect(m[5]).toBeCloseTo(m[10]);\n\texpect(m[1]).toBeCloseTo(m[6]);\n\texpect(m[6]).toBeCloseTo(m[8]);\n});\n\ntest(\"scale\", () => {\n\tconst m = ear.math.makeMatrix4Scale([0.5, 0.5, 0.5]);\n\t[0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1]\n\t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n});\n\ntest(\"makeMatrix4Scale\", () => {\n\tconst m0 = ear.math.makeMatrix4Scale();\n\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(m0[i]));\n\tconst m1 = ear.math.makeMatrix4Scale([0.5, 0.5, 0.5]);\n\t[0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(m1[i]));\n\tconst m2 = ear.math.makeMatrix4UniformScale();\n\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(m2[i]));\n\tconst m3 = ear.math.makeMatrix4UniformScale(0.5);\n\t[0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(m3[i]));\n});\n\ntest(\"combine operations\", () => {\n\tconst result = ear.math.multiplyMatrices4(\n\t\tear.math.makeMatrix4RotateX(Math.PI / 2),\n\t\tear.math.makeMatrix4Translate(40, 20, 10),\n\t);\n\t[1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 40, -10, 20, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(result[i]));\n});\n\ntest(\"reflectZ\", () => {\n\tconst m = ear.math.makeMatrix4ReflectZ([1, 1, 1], [0, 0, 0]);\n\t[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(n).toBeCloseTo(m[i]));\n});\n\ntest(\"transform\", () => {\n\tconst matrix = ear.math.multiplyMatrices4(\n\t\tear.math.makeMatrix4RotateZ(Math.PI / 2),\n\t\tear.math.makeMatrix4Translate(4, 5, 6),\n\t);\n\tconst segment = [[-1, 0, 0], [1, 0, 0]];\n\tconst result = segment\n\t\t.map(p => ear.math.multiplyMatrix4Vector3(matrix, p));\n\t[[-5, 3, 6], [-5, 5, 6]]\n\t\t.forEach((p, i) => p\n\t\t\t.forEach((n, j) => expect(n).toBe(result[i][j])));\n});\n\ntest(\"transformVector\", () => {\n\tconst vector = [1, 2, 3];\n\tconst result = ear.math.multiplyMatrix4Vector3(\n\t\tear.math.makeMatrix4Scale([0.5, 0.5, 0.5]),\n\t\tvector,\n\t);\n\texpect(result[0]).toBeCloseTo(0.5);\n\texpect(result[1]).toBeCloseTo(1);\n\texpect(result[2]).toBeCloseTo(1.5);\n});\n\ntest(\"transformLine\", () => {\n\tconst result = ear.math.multiplyMatrix4Line3(\n\t\tear.math.makeMatrix4Scale([0.5, 0.5, 0.5]),\n\t\t[0.707, 0.707, 0],\n\t\t[1, 0, 0],\n\t);\n\texpect(result.vector[0]).toBeCloseTo(0.3535);\n\texpect(result.vector[1]).toBeCloseTo(0.3535);\n\texpect(result.vector[2]).toBeCloseTo(0);\n\texpect(result.origin[0]).toBeCloseTo(0.5);\n\texpect(result.origin[1]).toBeCloseTo(0);\n\texpect(result.origin[2]).toBeCloseTo(0);\n});\n/**\n * WebGL Matrices\n */\ntest(\"perspective matrix\", () => {\n\tconst res1 = ear.math.makePerspectiveMatrix4(Math.PI / 2, 1, 1, 10);\n\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -(11 / 9), -1, 0, 0, -(20 / 9), 0]\n\t\t.forEach((n, i) => expect(res1[i]).toBeCloseTo(n));\n\n\t// fov 180deg results in 0 in x and y diagonal\n\tconst res2 = ear.math.makePerspectiveMatrix4(Math.PI, 1, 1, 10);\n\texpect(res2[0]).toBeCloseTo(0);\n\texpect(res2[5]).toBeCloseTo(0);\n});\n\ntest(\"orthographic matrix\", () => {\n\tconst res1 = ear.math.makeOrthographicMatrix4(1, 1, -1, -1, 1, 10);\n\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -(2 / 9), 0, 0, 0, -(11 / 9), 1]\n\t\t.forEach((n, i) => expect(res1[i]).toBeCloseTo(n));\n});\n\ntest(\"lookat matrix\", () => {\n\tconst res1 = ear.math.makeLookAtMatrix4([0, 0, 1], [0, 0, 0], [0, 1, 0]);\n\t[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1]\n\t\t.forEach((n, i) => expect(res1[i]).toBeCloseTo(n));\n\n\tconst res2 = ear.math.makeLookAtMatrix4([1, 0, 0], [0, 0, 0], [0, 1, 0]);\n\t[0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]\n\t\t.forEach((n, i) => expect(res2[i]).toBeCloseTo(n));\n\n\tconst res3 = ear.math.makeLookAtMatrix4([0, 0, -1], [0, 0, 0], [0, 1, 0]);\n\t[-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 1]\n\t\t.forEach((n, i) => expect(res3[i]).toBeCloseTo(n));\n\n\tconst res4 = ear.math.makeLookAtMatrix4([0, 0, 1], [0, 0, 0], [0, -1, 0]);\n\t[-1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1]\n\t\t.forEach((n, i) => expect(res4[i]).toBeCloseTo(n));\n});\n"
  },
  {
    "path": "tests/math.algebra.quaternion.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"quaternionFromTwoVectors\", () => {\n\tconst res1 = ear.math.quaternionFromTwoVectors([1, 0, 0], [0, 1, 0]);\n\t[0, 0, Math.SQRT1_2, Math.SQRT1_2]\n\t\t.forEach((n, i) => expect(res1[i]).toBeCloseTo(n));\n\n\tconst res2 = ear.math.quaternionFromTwoVectors([1, 0, 0], [0, 0, 1]);\n\t[0, -Math.SQRT1_2, 0, Math.SQRT1_2]\n\t\t.forEach((n, i) => expect(res2[i]).toBeCloseTo(n));\n\n\tconst res3 = ear.math.quaternionFromTwoVectors([0, 1, 0], [0, 0, 1]);\n\t[Math.SQRT1_2, 0, 0, Math.SQRT1_2]\n\t\t.forEach((n, i) => expect(res3[i]).toBeCloseTo(n));\n});\n\ntest(\"matrix4FromQuaternion\", () => {\n\tconst res1 = ear.math.matrix4FromQuaternion([0, 0, 0, 1]);\n\tear.math.identity4x4\n\t\t.forEach((n, i) => expect(res1[i]).toBeCloseTo(n));\n\n\tconst res2 = ear.math.matrix4FromQuaternion([1, 0, 0, 0]);\n\t[1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(res2[i]).toBeCloseTo(n));\n\n\tconst res3 = ear.math.matrix4FromQuaternion([0, 1, 0, 0]);\n\t[-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(res3[i]).toBeCloseTo(n));\n\n\tconst res4 = ear.math.matrix4FromQuaternion([0, 0, 1, 0]);\n\t[-1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\t\t.forEach((n, i) => expect(res4[i]).toBeCloseTo(n));\n});\n"
  },
  {
    "path": "tests/math.algebra.vector.resize.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst equalTest = (a, b) => expect(JSON.stringify(a))\n\t.toBe(JSON.stringify(b));\n\ntest(\"resize\", () => {\n\tconst a = [1, 2, 3];\n\tconst _a = ear.math.resize(5, a);\n\tequalTest(_a, [1, 2, 3, 0, 0]);\n});\n\ntest(\"resize empty\", () => {\n\tconst res = ear.math.resize(3, []);\n\texpect(ear.math.epsilonEqualVectors([0, 0, 0], res)).toBe(true);\n});\n\ntest(\"resize undefined\", () => {\n\ttry {\n\t\tear.math.resize(3);\n\t} catch (err) {\n\t\texpect(err).not.toBe(undefined);\n\t}\n});\n\ntest(\"resizeUp\", () => {\n\tconst a = [1, 2, 3];\n\tconst b = [4, 5];\n\texpect(a.length).toBe(3);\n\texpect(b.length).toBe(2);\n\tconst [_a, _b] = ear.math.resizeUp(a, b);\n\texpect(_a.length).toBe(3);\n\texpect(_b.length).toBe(3);\n});\n\n// function is not included.\n// since implementation it has never been used\n// test(\"resizeDown\", () => {\n// \tconst a = [1, 2, 3];\n// \tconst b = [4, 5];\n// \texpect(a.length).toBe(3);\n// \texpect(b.length).toBe(2);\n// \tconst [_a, _b] = ear.math.resizeDown(a, b);\n// \texpect(_a.length).toBe(2);\n// \texpect(_b.length).toBe(2);\n// });\n"
  },
  {
    "path": "tests/math.algebra.vector.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqual = function (...args) {\n\texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n};\n/**\n * algebra core\n */\ntest(\"magnitude\", () => {\n\texpect(ear.math.magnitude([0, 0, 0, 0, 0, 1])).toBe(1);\n\texpect(ear.math.magnitude([1, 1])).toBeCloseTo(Math.sqrt(2));\n\texpect(ear.math.magnitude([0, 0, 0, 0, 0, 0])).toBe(0);\n\texpect(ear.math.magnitude([])).toBe(0);\n\n\texpect(ear.math.magnitude2([1, 0, 10])).toBe(1);\n\texpect(ear.math.magnitude2([1, 0])).toBe(1);\n\texpect(ear.math.magnitude2([0, 0, 1])).toBe(0);\n\n\texpect(ear.math.magnitude3([0, 0, 10])).toBe(10);\n\texpect(ear.math.magnitude3([1, 0, 0])).toBe(1);\n\texpect(ear.math.magnitude3([0, 0, 1])).toBe(1);\n\texpect(ear.math.magnitude3([0, 0, 0, 1])).toBe(0);\n});\n\ntest(\"mag sq\", () => {\n\texpect(ear.math.magSquared([1, 1, 1, 1])).toBe(4);\n\texpect(ear.math.magSquared([])).toBe(0);\n\texpect(ear.math.magSquared([1, -2, 3]))\n\t\t.toBe((1 ** 2) + (2 ** 2) + (3 ** 2));\n\texpect(ear.math.magSquared([-100])).toBe(100 * 100);\n});\n\ntest(\"normalize\", () => {\n\texpect(ear.math.normalize([]).length).toBe(0);\n\texpect(ear.math.normalize([1, 1])[0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize([1, 1])[1]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize([1, -1, 1])[0]).toBeCloseTo(Math.sqrt(3) / 3);\n\n\texpect(ear.math.normalize2([]).length).toBe(2);\n\texpect(ear.math.normalize2([1, 1])[0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize2([1, 1])[1]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize2([1, 1])[2]).toBe(undefined);\n\texpect(ear.math.normalize2([1, 1, 10])[0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize2([1, 1, 10])[1]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.normalize2([1, 1, 10])[2]).toBe(undefined);\n\n\texpect(ear.math.normalize3([]).length).toBe(3);\n\texpect(ear.math.normalize3([0, 0, 0])[0]).toBeCloseTo(0);\n\texpect(ear.math.normalize3([0, 0, 0])[1]).toBeCloseTo(0);\n\texpect(ear.math.normalize3([0, 0, 0])[2]).toBeCloseTo(0);\n\texpect(ear.math.normalize3([1, 1, 1])[0]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1])[1]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1])[2]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1])[3]).toBe(undefined);\n\texpect(ear.math.normalize3([1, 1, 1, 10])[0]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1, 10])[1]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1, 10])[2]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(ear.math.normalize3([1, 1, 1, 10])[3]).toBe(undefined);\n});\n\ntest(\"scale\", () => {\n\texpect(ear.math.scale([]).length).toBe(0);\n\texpect(ear.math.scale([1])[0]).toBe(NaN);\n\texpect(ear.math.scale([1], 2)[0]).toBe(2);\n\texpect(ear.math.scale([1], -2)[0]).toBe(-2);\n\n\texpect(ear.math.scale2([]).length).toBe(2);\n\texpect(ear.math.scale2([1])[0]).toBe(NaN);\n\texpect(ear.math.scale2([1], 2)[1]).toBe(NaN);\n\texpect(ear.math.scale2([1], 2)[2]).toBe(undefined);\n\texpect(ear.math.scale2([1, 1], 2)[1]).toBe(2);\n\texpect(ear.math.scale2([1, 1], 2)[2]).toBe(undefined);\n\texpect(ear.math.scale2([1, 1, 1], 2)[2]).toBe(undefined);\n\n\texpect(ear.math.scale3([]).length).toBe(3);\n\texpect(ear.math.scale3([1])[0]).toBe(NaN);\n\texpect(ear.math.scale3([1], 2)[1]).toBe(NaN);\n\texpect(ear.math.scale3([1], 2)[2]).toBe(NaN);\n\texpect(ear.math.scale3([1, 1, 1], 2)[1]).toBe(2);\n\texpect(ear.math.scale3([1, 1, 1], 2)[2]).toBe(2);\n\texpect(ear.math.scale3([1, 1, 1], 2)[3]).toBe(undefined);\n});\n\ntest(\"add\", () => {\n\texpect(ear.math.add([1], [1, 2, 3]).length).toBe(1);\n\texpect(ear.math.add([1], [1, 2, 3])[0]).toBe(2);\n\texpect(ear.math.add([1, 2, 3], [1, 2])[0]).toBe(2);\n\texpect(ear.math.add([1, 2, 3], [1, 2])[1]).toBe(4);\n\texpect(ear.math.add([1, 2, 3], [1, 2])[2]).toBe(3);\n\texpect(ear.math.add([1, 2, 3], [])[0]).toBe(1);\n\n\texpect(ear.math.add2([1], [1, 2, 3]).length).toBe(2);\n\texpect(ear.math.add2([1], [1, 2, 3])[0]).toBe(2);\n\texpect(ear.math.add2([1], [1, 2, 3])[1]).toBe(NaN);\n\texpect(ear.math.add2([1, 2, 3], [1, 2])[0]).toBe(2);\n\texpect(ear.math.add2([1, 2, 3], [1, 2])[1]).toBe(4);\n\texpect(ear.math.add2([1, 2, 3], [1, 2])[2]).toBe(undefined);\n\texpect(ear.math.add2([1, 2, 3], [])[0]).toBe(NaN);\n\n\texpect(ear.math.add3([1], [1, 2, 3]).length).toBe(3);\n\texpect(ear.math.add3([1], [1, 2, 3])[0]).toBe(2);\n\texpect(ear.math.add3([1], [1, 2, 3])[1]).toBe(NaN);\n\texpect(ear.math.add3([1, 2, 3], [1, 2])[0]).toBe(2);\n\texpect(ear.math.add3([1, 2, 3], [1, 2])[1]).toBe(4);\n\texpect(ear.math.add3([1, 2, 3], [1, 2])[2]).toBe(NaN);\n\texpect(ear.math.add3([1, 2, 3], [])[0]).toBe(NaN);\n});\n\ntest(\"subtract\", () => {\n\texpect(ear.math.subtract([1], [2, 3, 4]).length).toBe(1);\n\texpect(ear.math.subtract([1], [2, 3, 4])[0]).toBe(-1);\n\texpect(ear.math.subtract([1, 2, 3], [1, 2])[0]).toBe(0);\n\texpect(ear.math.subtract([1, 2, 3], [1, 2])[1]).toBe(0);\n\texpect(ear.math.subtract([1, 2, 3], [1, 2])[2]).toBe(3);\n\texpect(ear.math.subtract([1, 2, 3], [])[0]).toBe(1);\n\n\texpect(ear.math.subtract2([1], [2, 3, 4]).length).toBe(2);\n\texpect(ear.math.subtract2([1], [2, 3, 4])[0]).toBe(-1);\n\texpect(ear.math.subtract2([1, 2, 3], [1, 2])[0]).toBe(0);\n\texpect(ear.math.subtract2([1, 2, 3], [1, 2])[1]).toBe(0);\n\texpect(ear.math.subtract2([1, 2, 3], [1, 2])[2]).toBe(undefined);\n\texpect(ear.math.subtract2([1, 2, 3], [])[0]).toBe(NaN);\n\n\texpect(ear.math.subtract3([1], [2, 3, 4]).length).toBe(3);\n\texpect(ear.math.subtract3([1], [2, 3, 4])[0]).toBe(-1);\n\texpect(ear.math.subtract3([1, 2, 3], [1, 2])[0]).toBe(0);\n\texpect(ear.math.subtract3([1, 2, 3], [1, 2])[1]).toBe(0);\n\texpect(ear.math.subtract3([1, 2, 3], [1, 2])[2]).toBe(NaN);\n\texpect(ear.math.subtract3([1, 2, 3], [])[0]).toBe(NaN);\n});\n\ntest(\"dot\", () => {\n\texpect(ear.math.dot([3, 1000], [1, 0])).toBe(3);\n\texpect(ear.math.dot([3, 1000], [1, 0])).toBe(3);\n\texpect(ear.math.dot([3, 1000], [0, 1])).toBe(1000);\n\texpect(ear.math.dot([1, 1000], [400])).toBe(400);\n\texpect(ear.math.dot([1, 1000], [400, 0])).toBe(400);\n\texpect(ear.math.dot([1, 1000], [400, 1])).toBe(1400);\n\texpect(ear.math.dot([1, 1000], [])).toBe(0);\n\n\texpect(ear.math.dot2([3, 1000], [1, 0])).toBe(3);\n\texpect(ear.math.dot2([3, 1000], [1, 0])).toBe(3);\n\texpect(ear.math.dot2([3, 1000], [0, 1])).toBe(1000);\n\texpect(ear.math.dot2([1, 1000], [400])).toBe(NaN);\n\texpect(ear.math.dot2([1, 1000], [400, 0])).toBe(400);\n\texpect(ear.math.dot2([1, 1000], [400, 1])).toBe(1400);\n\texpect(ear.math.dot2([1, 1000], [])).toBe(NaN);\n\n\texpect(ear.math.dot3([3, 1000], [1, 0])).toBe(NaN);\n\texpect(ear.math.dot3([3, 1000], [1, 0])).toBe(NaN);\n\texpect(ear.math.dot3([3, 1000], [0, 1])).toBe(NaN);\n\texpect(ear.math.dot3([1, 1000], [400])).toBe(NaN);\n\texpect(ear.math.dot3([1, 1000], [400, 0])).toBe(NaN);\n\texpect(ear.math.dot3([1, 1000], [400, 1])).toBe(NaN);\n\texpect(ear.math.dot3([1, 1000], [])).toBe(NaN);\n\texpect(ear.math.dot3([3, 1000, 0], [1, 0, 0])).toBe(3);\n\texpect(ear.math.dot3([3, 1000, 0], [0, 1, 0])).toBe(1000);\n\texpect(ear.math.dot3([3, 1000, 200], [1, 1, 1])).toBe(1203);\n\texpect(ear.math.dot3([1, 1000, 0], [400])).toBe(NaN);\n\texpect(ear.math.dot3([1, 1000, 0], [400, 0, 0])).toBe(400);\n\texpect(ear.math.dot3([1, 1000, 0], [400, 1, 0])).toBe(1400);\n\texpect(ear.math.dot3([1, 1000, 0], [])).toBe(NaN);\n});\n\ntest(\"midpoint\", () => {\n\texpect(ear.math.midpoint([1, 2], [5, 6, 7]).length).toBe(2);\n\texpect(ear.math.midpoint([1, 2], [5, 6, 7])[0]).toBe(3);\n\texpect(ear.math.midpoint([1, 2], [5, 6, 7])[1]).toBe(4);\n\texpect(ear.math.midpoint([], [5, 6, 7]).length).toBe(0);\n\n\texpect(ear.math.midpoint2([1, 2], [5, 6, 7]).length).toBe(2);\n\texpect(ear.math.midpoint2([1, 2], [5, 6, 7])[0]).toBe(3);\n\texpect(ear.math.midpoint2([1, 2], [5, 6, 7])[1]).toBe(4);\n\texpect(ear.math.midpoint2([1, 2], [5, 6, 7])[2]).toBe(undefined);\n\texpect(ear.math.midpoint2([], [5, 6, 7]).length).toBe(2);\n\texpect(ear.math.midpoint2([], [5, 6, 7])[0]).toBe(NaN);\n\texpect(ear.math.midpoint2([], [5, 6, 7])[1]).toBe(NaN);\n\n\texpect(ear.math.midpoint3([1, 2], [5, 6, 7]).length).toBe(3);\n\texpect(ear.math.midpoint3([1, 2], [5, 6, 7])[0]).toBe(3);\n\texpect(ear.math.midpoint3([1, 2], [5, 6, 7])[1]).toBe(4);\n\texpect(ear.math.midpoint3([1, 2], [5, 6, 7])[2]).toBe(NaN);\n\texpect(ear.math.midpoint3([], [5, 6, 7]).length).toBe(3);\n\texpect(ear.math.midpoint3([], [5, 6, 7])[0]).toBe(NaN);\n\texpect(ear.math.midpoint3([], [5, 6, 7])[1]).toBe(NaN);\n});\n\ntest(\"average function\", () => {\n\t// improper use\n\texpect(ear.math.average()).toBe(undefined);\n\texpect(ear.math.average(0, 1, 2).length).toBe(0);\n\texpect(ear.math.average([], [], []).length).toBe(0);\n\t// correct\n\ttestEqual(\n\t\t[3.75, 4.75],\n\t\tear.math.average([4, 1], [5, 6], [4, 6], [2, 6]),\n\t);\n\ttestEqual(\n\t\t[4, 5, 3],\n\t\tear.math.average([1, 2, 3], [4, 5, 6], [7, 8]),\n\t);\n\ttestEqual(\n\t\t[4, 5, 6],\n\t\tear.math.average([1, 2, 3], [4, 5, 6], [7, 8, 9]),\n\t);\n});\n\ntest(\"average2\", () => {\n\texpect(ear.math.average2()).toBe(undefined);\n\texpect(ear.math.average2([0, 1], [2, 3])[0]).toBe(1);\n\texpect(ear.math.average2([0, 1], [2, 3])[1]).toBe(2);\n\texpect(ear.math.average2([], [], []).length).toBe(2);\n\texpect(ear.math.average2([], [], [])[0]).toBe(NaN);\n\texpect(ear.math.average2([], [], [])[1]).toBe(NaN);\n});\n\ntest(\"lerp\", () => {\n\texpect(ear.math.lerp([0, 1], [2, 0], 0)[0]).toBe(0);\n\texpect(ear.math.lerp([0, 1], [2, 0], 0)[1]).toBe(1);\n\texpect(ear.math.lerp([0, 1], [2, 0], 1)[0]).toBe(2);\n\texpect(ear.math.lerp([0, 1], [2, 0], 1)[1]).toBe(0);\n\texpect(ear.math.lerp([0, 1], [2, 0], 0.5)[0]).toBe(1);\n\texpect(ear.math.lerp([0, 1], [2, 0], 0.5)[1]).toBe(0.5);\n});\n\ntest(\"cross2\", () => {\n\texpect(ear.math.cross2([1, 0], [-4, 3])).toBe(3);\n\texpect(ear.math.cross2([2, -1], [1, 3])).toBe(7);\n});\n\ntest(\"cross3\", () => {\n\texpect(ear.math.cross3([-3, 0, -2], [5, -1, 2])[0]).toBe(-2);\n\texpect(ear.math.cross3([-3, 0, -2], [5, -1, 2])[1]).toBe(-4);\n\texpect(ear.math.cross3([-3, 0, -2], [5, -1, 2])[2]).toBe(3);\n\texpect(Number.isNaN(ear.math.cross3([-3, 0], [5, -1, 2])[0])).toBe(true);\n\texpect(Number.isNaN(ear.math.cross3([-3, 0], [5, -1, 2])[1])).toBe(true);\n\texpect(Number.isNaN(ear.math.cross3([-3, 0], [5, -1, 2])[2])).toBe(false);\n});\n\ntest(\"distance3\", () => {\n\tconst r1 = ear.math.distance3([1, 2, 3], [4, 5, 6]);\n\tconst r2 = ear.math.distance3([1, 2, 3], [4, 5]);\n\texpect(r1).toBeCloseTo(5.196152422706632);\n\texpect(Number.isNaN(r2)).toBe(true);\n});\n\ntest(\"rotate90, rotate270\", () => {\n\texpect(ear.math.rotate90([-3, 2])[0]).toBe(-2);\n\texpect(ear.math.rotate90([-3, 2])[1]).toBe(-3);\n\texpect(ear.math.rotate270([-3, 2])[0]).toBe(2);\n\texpect(ear.math.rotate270([-3, 2])[1]).toBe(3);\n});\n\ntest(\"flip\", () => {\n\texpect(ear.math.flip([-2, -1])[0]).toBe(2);\n\texpect(ear.math.flip([-2, -1])[1]).toBe(1);\n});\n\ntest(\"degenerate\", () => {\n\texpect(ear.math.degenerate([1])).toBe(false);\n\texpect(ear.math.degenerate([1], 1)).toBe(false);\n\texpect(ear.math.degenerate([1], 1 + ear.math.EPSILON)).toBe(true);\n\texpect(ear.math.degenerate([1, 1], 2)).toBe(false);\n\texpect(ear.math.degenerate([1, 1], 2 + ear.math.EPSILON)).toBe(true);\n});\n\ntest(\"parallel\", () => {\n\texpect(ear.math.parallel([1, 0], [0, 1])).toBe(false);\n\texpect(ear.math.parallel([1, 0], [-1, 0])).toBe(true);\n\t// this is where the parallel test breaks down when it uses dot product\n\texpect(ear.math.parallel([1, 0], [1, 0.0014142])).toBe(true);\n\texpect(ear.math.parallel([1, 0], [1, 0.0014143])).toBe(false);\n\t// this is the parallel test using cross product\n\texpect(ear.math.parallel2([1, 0], [1, 0.0000009])).toBe(true);\n\texpect(ear.math.parallel2([1, 0], [1, 0.0000010])).toBe(false);\n});\n\ntest(\"parallelNormalized\", () => {\n\texpect(ear.math.parallelNormalized([1, 0], [0, 1])).toBe(false);\n\texpect(ear.math.parallelNormalized([1, 0], [1, 0])).toBe(true);\n\texpect(ear.math.parallelNormalized([1, 0], [-1, 0])).toBe(true);\n\t// unintended usage\n\texpect(ear.math.parallelNormalized([3, 0], [3, 0])).toBe(true);\n\texpect(ear.math.parallelNormalized([2, 0], [1, 0])).toBe(true);\n\texpect(ear.math.parallelNormalized([-2, 0], [1, 0])).toBe(true);\n\texpect(ear.math.parallelNormalized([1, 0], [2, 0])).toBe(true);\n\texpect(ear.math.parallelNormalized([1, 0], [-2, 0])).toBe(true);\n});\n\ntest(\"basisVectors2\", () => {\n\texpect(ear.math.basisVectors2().length).toBe(2);\n\tconst basis1 = ear.math.basisVectors2([1, 0]);\n\t// [-v[1], v[0]]\n\texpect(basis1[0][0]).toBeCloseTo(1);\n\texpect(basis1[0][1]).toBeCloseTo(0);\n\texpect(basis1[1][0]).toBeCloseTo(0);\n\texpect(basis1[1][1]).toBeCloseTo(1);\n\n\tconst basis2 = ear.math.basisVectors2([500, 0]);\n\texpect(basis2[0][0]).toBeCloseTo(1);\n\texpect(basis2[0][1]).toBeCloseTo(0);\n\texpect(basis2[1][0]).toBeCloseTo(0);\n\texpect(basis2[1][1]).toBeCloseTo(1);\n\n\tconst basis3 = ear.math.basisVectors2([0, 500]);\n\texpect(basis3[0][0]).toBeCloseTo(0);\n\texpect(basis3[0][1]).toBeCloseTo(1);\n\texpect(basis3[1][0]).toBeCloseTo(-1);\n\texpect(basis3[1][1]).toBeCloseTo(0);\n\n\tconst basis4 = ear.math.basisVectors2([20, 20]);\n\texpect(basis4[0][0]).toBeCloseTo(Math.SQRT1_2);\n\texpect(basis4[0][1]).toBeCloseTo(Math.SQRT1_2);\n\texpect(basis4[1][0]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(basis4[1][1]).toBeCloseTo(Math.SQRT1_2);\n\n\tconst basis5 = ear.math.basisVectors2([0, 0]);\n\texpect(basis5[0][0]).toBeCloseTo(0);\n\texpect(basis5[0][1]).toBeCloseTo(0);\n\texpect(basis5[1][0]).toBeCloseTo(0);\n\texpect(basis5[1][1]).toBeCloseTo(0);\n});\n\ntest(\"basisVectors3\", () => {\n\texpect(ear.math.basisVectors3().length).toBe(3);\n\tconst basis1 = ear.math.basisVectors3([1, 0, 0]);\n\texpect(basis1[0][0]).toBeCloseTo(1);\n\texpect(basis1[0][1]).toBeCloseTo(0);\n\texpect(basis1[0][2]).toBeCloseTo(0);\n\texpect(basis1[1][0]).toBeCloseTo(0);\n\texpect(basis1[1][1]).toBeCloseTo(0);\n\texpect(basis1[1][2]).toBeCloseTo(-1);\n\texpect(basis1[2][0]).toBeCloseTo(0);\n\texpect(basis1[2][1]).toBeCloseTo(1);\n\texpect(basis1[2][2]).toBeCloseTo(0);\n\n\tconst basis2 = ear.math.basisVectors3([0, 0, 500]);\n\texpect(basis2[0][0]).toBeCloseTo(0);\n\texpect(basis2[0][1]).toBeCloseTo(0);\n\texpect(basis2[0][2]).toBeCloseTo(1);\n\texpect(basis2[1][0]).toBeCloseTo(0);\n\texpect(basis2[1][1]).toBeCloseTo(-1);\n\texpect(basis2[1][2]).toBeCloseTo(0);\n\texpect(basis2[2][0]).toBeCloseTo(1);\n\texpect(basis2[2][1]).toBeCloseTo(0);\n\texpect(basis2[2][2]).toBeCloseTo(0);\n\n\tconst basis3 = ear.math.basisVectors3([1, 1, 1]);\n\texpect(basis3[0][0]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(basis3[0][1]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(basis3[0][2]).toBeCloseTo(Math.sqrt(3) / 3);\n\texpect(basis3[1][0]).toBeCloseTo(0);\n\texpect(basis3[1][1]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(basis3[1][2]).toBeCloseTo(Math.SQRT1_2);\n\texpect(basis3[2][0]).toBeCloseTo((Math.sqrt(3) / 3) * Math.SQRT2);\n\texpect(basis3[2][1]).toBeCloseTo(-((Math.sqrt(3) / 3) * Math.SQRT2) / 2);\n\texpect(basis3[2][2]).toBeCloseTo(-((Math.sqrt(3) / 3) * Math.SQRT2) / 2);\n\n\tconst basis4 = ear.math.basisVectors3([0, 0, 0]);\n\texpect(basis4[0][0]).toBeCloseTo(0);\n\texpect(basis4[0][1]).toBeCloseTo(0);\n\texpect(basis4[0][2]).toBeCloseTo(0);\n\texpect(basis4[1][0]).toBeCloseTo(0);\n\texpect(basis4[1][1]).toBeCloseTo(0);\n\texpect(basis4[1][2]).toBeCloseTo(0);\n\texpect(basis4[2][0]).toBeCloseTo(0);\n\texpect(basis4[2][1]).toBeCloseTo(0);\n\texpect(basis4[2][2]).toBeCloseTo(0);\n});\n"
  },
  {
    "path": "tests/math.debug.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"single test for debugging\", () => {\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/math.general.array.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"removed\", () => expect(true).toBe(true));\n\n// const equalTest = (a, b) => expect(JSON.stringify(a))\n// \t.toBe(JSON.stringify(b));\n\n// /**\n//  * inputs and argument inference\n//  */\n// test(\"semiFlattenArrays\", () => {\n// \tequalTest(\n// \t\t[[0, 1, 2], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([0, 1, 2], [2, 3, 4]),\n// \t);\n// \tequalTest(\n// \t\t[[0, 1, 2], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([[0, 1, 2]], [[2, 3, 4]]),\n// \t);\n// \tequalTest(\n// \t\t[[0, 1, 2], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([[[0, 1, 2]], [[2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[0, 1, 2], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([[[[0, 1, 2]], [[2, 3, 4]]]]),\n// \t);\n// \tequalTest(\n// \t\t[[[0], [1], [2]], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([[[[0], [1], [2]]], [[2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[[0], [1], [2]], [2, 3, 4]],\n// \t\tear.math.semiFlattenArrays([[[[[[0]]], [[[1]]], [2]]], [[2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[{ x: 5, y: 3 }],\n// \t\tear.math.semiFlattenArrays({ x: 5, y: 3 }),\n// \t);\n// \tequalTest(\n// \t\t[{ x: 5, y: 3 }],\n// \t\tear.math.semiFlattenArrays([[[{ x: 5, y: 3 }]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.semiFlattenArrays([[[5, 3]]]),\n// \t);\n// \tequalTest(\n// \t\t[[5], [3]],\n// \t\tear.math.semiFlattenArrays([[[5], [3]]]),\n// \t);\n// \tequalTest(\n// \t\t[[5], [3]],\n// \t\tear.math.semiFlattenArrays([[[5]], [[3]]]),\n// \t);\n// \tequalTest(\n// \t\t[[5], [3]],\n// \t\tear.math.semiFlattenArrays([[[5]]], [[[3]]]),\n// \t);\n// });\n\n// test(\"flattenArrays\", () => {\n// \tequalTest(\n// \t\t[1],\n// \t\tear.math.flattenArrays([[[1]], []]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4],\n// \t\tear.math.flattenArrays([[[1, 2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4],\n// \t\tear.math.flattenArrays(1, 2, 3, 4),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 2, 4],\n// \t\tear.math.flattenArrays([1, 2, 3, 4], [2, 4]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 6, 7, 6],\n// \t\tear.math.flattenArrays([1, 2, 3, 4], [6, 7], 6),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 6, 7, 6, 2, 4, 5],\n// \t\tear.math.flattenArrays([1, 2, 3, 4], [6, 7], 6, 2, 4, 5),\n// \t);\n// \tequalTest(\n// \t\t[{ x: 5, y: 3 }],\n// \t\tear.math.flattenArrays({ x: 5, y: 3 }),\n// \t);\n// \tequalTest(\n// \t\t[{ x: 5, y: 3 }],\n// \t\tear.math.flattenArrays([[{ x: 5, y: 3 }]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 5, 6],\n// \t\tear.math.flattenArrays([[[1], [2, 3]]], 4, [5, 6]),\n// \t);\n// \tequalTest(\n// \t\t[undefined, undefined],\n// \t\tear.math.flattenArrays([[[undefined, [[undefined]]]]]),\n// \t);\n// });\n"
  },
  {
    "path": "tests/math.general.constant.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"constants\", () => {\n\texpect(typeof ear.math.EPSILON).toBe(\"number\");\n\texpect(typeof ear.math.TWO_PI).toBe(\"number\");\n\texpect(typeof ear.math.D2R).toBe(\"number\");\n\texpect(typeof ear.math.R2D).toBe(\"number\");\n});\n"
  },
  {
    "path": "tests/math.general.convert.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst t = 0.38268343236509;\nconst n = 0.92387953251129;\nconst s = Math.SQRT1_2;\n\n// [\n// \t{u:[ 0, 1], d:0},\n// \t{u:[-t, n], d:0},\n// \t{u:[-s, s], d:0},\n// \t{u:[-n, t], d:0},\n// \t{u:[-1, 0], d:0},\n// \t{u:[-n,-t], d:0},\n// \t{u:[-s,-s], d:0},\n// \t{u:[-t,-n], d:0},\n// \t{u:[ 0,-1], d:0},\n// \t{u:[ t,-n], d:0},\n// \t{u:[ s,-s], d:0},\n// \t{u:[ n,-t], d:0},\n// \t{u:[ 1, 0], d:0},\n// \t{u:[ n, t], d:0},\n// \t{u:[ s, s], d:0},\n// \t{u:[ t, n], d:0},\n// ]\ntest(\"16 angles of lines, through the origin\", () => {\n\tconst angles = Array.from(Array(16))\n\t\t.map((_, i) => ((Math.PI * 2) / 16) * i);\n\tconst vectors = angles.map(a => [Math.cos(a), Math.sin(a)]);\n\tconst origins = angles.map(() => [0, 0]);\n\tvectors\n\t\t.map((vector, i) => ({ vector, origin: origins[i] }))\n\t\t.map(vec_or => ear.math.vecLineToUniqueLine(vec_or))\n\t\t.map(norm_dist => ear.math.uniqueLineToVecLine(norm_dist))\n\t\t.forEach((el, i) => {\n\t\t\texpect(el.vector[0]).toBeCloseTo(vectors[i][0]);\n\t\t\texpect(el.vector[1]).toBeCloseTo(vectors[i][1]);\n\t\t\texpect(el.origin[0]).toBeCloseTo(origins[i][0]);\n\t\t\texpect(el.origin[1]).toBeCloseTo(origins[i][1]);\n\t\t});\n});\n\n// [\n// \t{u:[-0, 1], d:1},\n// \t{u:[-t, n], d:1},\n// \t{u:[-s, s], d:1},\n// \t{u:[-n, t], d:1},\n// \t{u:[-1, 0], d:1},\n// \t{u:[-n,-t], d:1},\n// \t{u:[-s,-s], d:1},\n// \t{u:[-t,-n], d:1},\n// \t{u:[-0,-1], d:1},\n// \t{u:[ t,-n], d:1},\n// \t{u:[ s,-s], d:1},\n// \t{u:[ n,-t], d:1},\n// \t{u:[ 1,-0], d:1},\n// \t{u:[ n, t], d:1},\n// \t{u:[ s, s], d:1},\n// \t{u:[ t, n], d:1},\n// ];\ntest(\"16 angles of lines, not through the origin, dir 1\", () => {\n\tconst angles = Array.from(Array(16))\n\t\t.map((_, i) => ((Math.PI * 2) / 16) * i);\n\tconst vectors = angles.map(a => [Math.cos(a), Math.sin(a)]);\n\tconst origins = angles\n\t\t.map(a => [Math.cos(a + Math.PI / 2), Math.sin(a + Math.PI / 2)]);\n\tvectors\n\t\t.map((vector, i) => ({ vector, origin: origins[i] }))\n\t\t.map(vec_or => ear.math.vecLineToUniqueLine(vec_or))\n\t\t.map(norm_dist => ear.math.uniqueLineToVecLine(norm_dist))\n\t\t.forEach((el, i) => {\n\t\t\texpect(el.vector[0]).toBeCloseTo(vectors[i][0]);\n\t\t\texpect(el.vector[1]).toBeCloseTo(vectors[i][1]);\n\t\t\texpect(el.origin[0]).toBeCloseTo(origins[i][0]);\n\t\t\texpect(el.origin[1]).toBeCloseTo(origins[i][1]);\n\t\t});\n});\n\ntest(\"16 angles of lines, not through the origin, dir 2\", () => {\n\tconst angles = Array.from(Array(16))\n\t\t.map((_, i) => ((Math.PI * 2) / 16) * i);\n\tconst vectors = angles.map(a => [Math.cos(a), Math.sin(a)]);\n\tconst origins = angles\n\t\t.map(a => [Math.cos(a - Math.PI / 2), Math.sin(a - Math.PI / 2)]);\n\tvectors\n\t\t.map((vector, i) => ({ vector, origin: origins[i] }))\n\t\t.map(vec_or => ear.math.vecLineToUniqueLine(vec_or))\n\t\t.map(norm_dist => ear.math.uniqueLineToVecLine(norm_dist))\n\t\t.forEach((el, i) => {\n\t\t\texpect(el.vector[0]).toBeCloseTo(vectors[i][0]);\n\t\t\texpect(el.vector[1]).toBeCloseTo(vectors[i][1]);\n\t\t\texpect(el.origin[0]).toBeCloseTo(origins[i][0]);\n\t\t\texpect(el.origin[1]).toBeCloseTo(origins[i][1]);\n\t\t});\n});\n"
  },
  {
    "path": "tests/math.general.function.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"clamp functions\", () => {\n\texpect(ear.math.clampLine(0)).toBe(0);\n\texpect(ear.math.clampLine(-Infinity)).toBe(-Infinity);\n\texpect(ear.math.clampLine(Infinity)).toBe(Infinity);\n\texpect(ear.math.clampLine(NaN)).toBe(NaN);\n\n\texpect(ear.math.clampRay(0)).toBe(0);\n\texpect(ear.math.clampRay(-1e-10)).toBe(-1e-10);\n\texpect(ear.math.clampRay(-1e-1)).toBe(0);\n\texpect(ear.math.clampRay(Infinity)).toBe(Infinity);\n\texpect(ear.math.clampRay(-Infinity)).toBe(0);\n\n\texpect(ear.math.clampSegment(0)).toBe(0);\n\texpect(ear.math.clampSegment(-1e-10)).toBe(-1e-10);\n\texpect(ear.math.clampSegment(-1e-1)).toBe(0);\n\texpect(ear.math.clampSegment(Infinity)).toBe(1);\n\texpect(ear.math.clampSegment(-Infinity)).toBe(0);\n});\n\ntest(\"epsilonEqual\", () => {\n\tconst smEp = ear.math.EPSILON / 10; // smaller than epsilon\n\tconst bgEp = ear.math.EPSILON * 10; // larger than epsilon\n\texpect(ear.math.epsilonEqual(0, 0)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, smEp)).toBe(true);\n\texpect(ear.math.epsilonEqual(10, 10)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, 1)).toBe(false);\n\texpect(ear.math.epsilonEqual(1, 0)).toBe(false);\n\texpect(ear.math.epsilonEqual(0, 0, smEp)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, 0, 1)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, 1, smEp)).toBe(false);\n\texpect(ear.math.epsilonEqual(1, 0, smEp)).toBe(false);\n\texpect(ear.math.epsilonEqual(0, 1, 10)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, smEp, bgEp)).toBe(true);\n\texpect(ear.math.epsilonEqual(0, bgEp, smEp)).toBe(false);\n\texpect(ear.math.epsilonEqual(bgEp, 0, smEp)).toBe(false);\n});\n\ntest(\"epsilonCompare\", () => {\n\tconst smEp = ear.math.EPSILON / 10; // smaller than epsilon\n\tconst bgEp = ear.math.EPSILON * 10; // larger than epsilon\n\texpect(ear.math.epsilonCompare(0, 0)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, smEp)).toBe(0);\n\texpect(ear.math.epsilonCompare(10, 10)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, 1)).toBe(-1);\n\texpect(ear.math.epsilonCompare(1, 0)).toBe(1);\n\texpect(ear.math.epsilonCompare(0, 0, smEp)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, 0, 1)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, 1, smEp)).toBe(-1);\n\texpect(ear.math.epsilonCompare(1, 0, smEp)).toBe(1);\n\texpect(ear.math.epsilonCompare(0, 1, 10)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, smEp, bgEp)).toBe(0);\n\texpect(ear.math.epsilonCompare(0, bgEp, smEp)).toBe(-1);\n\texpect(ear.math.epsilonCompare(bgEp, 0, smEp)).toBe(1);\n});\n\ntest(\"epsilonCompare to sort\", () => {\n\tconst smEp = ear.math.EPSILON / 10; // smaller than epsilon\n\tconst array = [0, 0 + smEp, 1, 3 + smEp * 2, 3, 2, 1.1];\n\t// sort increasing (or leave pairs untouched)\n\tarray.sort(ear.math.epsilonCompare);\n\t// the result is an increasing array, except that values within an epsilon\n\t// are allowed to be unsorted with respect to each other.\n\t// hence, we test for equality. will be either \"less than\" or \"equal\".\n\tfor (let i = 0; i < array.length - 1; i += 1) {\n\t\tconst equal = ear.math.epsilonEqual(array[i], array[i + 1]);\n\t\tconst lessThan = array[i] < array[i + 1];\n\t\texpect(equal || lessThan).toBe(true);\n\t}\n});\n\ntest(\"equivalent vectors\", () => {\n\tconst smEp = ear.math.EPSILON / 10; // smaller than epsilon\n\tconst bgEp = ear.math.EPSILON * 10; // larger than epsilon\n\texpect(ear.math.epsilonEqualVectors([1, 2, 3], [1, 2, 3])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1, 2 + smEp], [1, 2 - smEp])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1, 2 + bgEp], [1, 2 - bgEp])).toBe(false);\n\texpect(ear.math.epsilonEqualVectors([1, 2], [1, 2.0000000001])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1, 2, 3, 4], [1, 2])).toBe(false);\n\texpect(ear.math.epsilonEqualVectors([], [])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1.000000001, -1], [1, -1])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1.000000001, 0], [1])).toBe(true);\n\texpect(ear.math.epsilonEqualVectors([1.000000001, 0], [1, 0])).toBe(true);\n});\n"
  },
  {
    "path": "tests/math.general.get.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"removed\", () => expect(true).toBe(true));\n\n// const equalTest = (a, b) => expect(JSON.stringify(a))\n// \t.toBe(JSON.stringify(b));\n\n// test(\"getVector\", () => {\n// \tequalTest(\n// \t\t[1, 2, 3, 4],\n// \t\tear.math.getVector([[[1, 2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4],\n// \t\tear.math.getVector(1, 2, 3, 4),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 2, 4],\n// \t\tear.math.getVector([1, 2, 3, 4], [2, 4]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 6, 7, 6],\n// \t\tear.math.getVector([1, 2, 3, 4], [6, 7], 6),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 6, 7, 6, 2, 4, 5],\n// \t\tear.math.getVector([1, 2, 3, 4], [6, 7], 6, 2, 4, 5),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector({ x: 5, y: 3 }),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[{ x: 5, y: 3 }]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[5, 3]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[5], [3]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[5]], [[3]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[5]]], [[[3]]]),\n// \t);\n// \tequalTest(\n// \t\t[5, 3],\n// \t\tear.math.getVector([[[5]]], 3),\n// \t);\n// });\n\n// test(\"getLine\", () => {\n// \tequalTest(ear.math.getLine(1), { vector: [1], origin: [] });\n// \tequalTest(ear.math.getLine(1, 2), { vector: [1, 2], origin: [] });\n// \tequalTest(ear.math.getLine(1, 2, 3), { vector: [1, 2, 3], origin: [] });\n// \tequalTest(ear.math.getLine([1], [2]), { vector: [1], origin: [2] });\n// \tequalTest(ear.math.getLine([1, 2], [2, 3]), { vector: [1, 2], origin: [2, 3] });\n// \tequalTest(ear.math.getLine(), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine({}), { vector: [], origin: [] });\n// \tequalTest(\n// \t\tear.math.getLine({ vector: [1], origin: [2] }),\n// \t\t{ vector: [1], origin: [2] },\n// \t);\n// \tequalTest(\n// \t\tear.math.getLine({ vector: [1, 2], origin: [2, 3] }),\n// \t\t{ vector: [1, 2], origin: [2, 3] },\n// \t);\n// \tequalTest(\n// \t\tear.math.getLine({ vector: [1] }),\n// \t\t{ vector: [1], origin: [] },\n// \t);\n// \t// \"getLine\" only looks for a \"vector\" key,\n// \t// this will result in an empty object\n// \tequalTest(\n// \t\tear.math.getLine({ origin: [1] }),\n// \t\t{ vector: [], origin: [] },\n// \t);\n// \tequalTest(ear.math.getLine(), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine({}), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine([]), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine([{}]), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine(undefined), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine([undefined]), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine(null), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine([null]), { vector: [], origin: [] });\n// \tequalTest(ear.math.getLine(NaN), { vector: [NaN], origin: [] });\n// \tequalTest(ear.math.getLine([NaN]), { vector: [NaN], origin: [] });\n// });\n\n// test(\"getArrayOfVectors\", () => {\n// \tequalTest(\n// \t\t[[1, 2], [3, 4]],\n// \t\tear.math.getArrayOfVectors({ x: 1, y: 2 }, { x: 3, y: 4 }),\n// \t);\n// \tequalTest(\n// \t\t[[1, 2], [3, 4]],\n// \t\tear.math.getArrayOfVectors([[[{ x: 1, y: 2 }, { x: 3, y: 4 }]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1, 2], [3, 4]],\n// \t\tear.math.getArrayOfVectors([[[1, 2], [3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1, 2], [3, 4]],\n// \t\tear.math.getArrayOfVectors([[[1, 2]], [[3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1, 2], [3, 4]],\n// \t\tear.math.getArrayOfVectors([[[1, 2]]], [[[3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1], [2], [3], [4]],\n// \t\tear.math.getArrayOfVectors([[[1], [2], [3], [4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1], [2], [3], [4]],\n// \t\tear.math.getArrayOfVectors([[[1]], [[2]], [[3]], [[4]]]),\n// \t);\n// \tequalTest(\n// \t\t[[1], [2], [3], [4]],\n// \t\tear.math.getArrayOfVectors([[[1]]], 2, 3, 4),\n// \t);\n// \tequalTest(\n// \t\t[[1], [2], [3], [4]],\n// \t\tear.math.getArrayOfVectors([[[1, 2, 3, 4]]]),\n// \t);\n// });\n\n// test(\"getSegment\", () => {\n// \tequalTest([[1, 2], [3, 4]], ear.math.getSegment(1, 2, 3, 4));\n// \tequalTest([[1, 2], [3, 4]], ear.math.getSegment([1, 2], [3, 4]));\n// \tequalTest([[1, 2], [3, 4]], ear.math.getSegment([1, 2, 3, 4]));\n// \tequalTest([[1, 2], [3, 4]], ear.math.getSegment([[1, 2], [3, 4]]));\n// });\n\n// test(\"get_matrix2\", () => {\n//   equalTest(\n//     [1, 2, 3, 4, 5, 6],\n//     ear.math.get_matrix2([[[1, 2, 3, 4, 5, 6]]])\n//   );\n//   equalTest(\n//     [1, 2, 3, 4, 0, 0],\n//     ear.math.get_matrix2([[1, 2, 3, 4]])\n//   );\n//   equalTest(\n//     [1, 2, 3, 1, 0, 0],\n//     ear.math.get_matrix2(1, 2, 3)\n//   );\n//   equalTest(\n//     [1, 2, 3, 1, 0, 0],\n//     ear.math.get_matrix2(1, 2, 3, 1)\n//   );\n// });\n\n// test(\"getMatrix3x4\", () => {\n// \tequalTest(\n// \t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n// \t\tear.math.getMatrix3x4([[[]]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 0, 3, 4, 0, 0, 0, 1, 0, 0, 0],\n// \t\tear.math.getMatrix3x4([[[1, 2, 3, 4]]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 0, 3, 4, 0, 0, 0, 1, 5, 6, 0],\n// \t\tear.math.getMatrix3x4([[[1, 2, 3, 4, 5, 6]]]),\n// \t);\n// \tequalTest(\n// \t\t[1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0],\n// \t\tear.math.getMatrix3x4([[[1, 2, 3, 4, 5, 6, 7, 8, 9]]]),\n// \t);\n// });\n"
  },
  {
    "path": "tests/math.general.sort.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst equalTest = (a, b) => expect(JSON.stringify(a))\n\t.toBe(JSON.stringify(b));\n\ntest(\"sortPointsAlongVector\", () => {\n\tconst points = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\tconst result = ear.general.sortPointsAlongVector(points, [1, 0]);\n\texpect(result[0]).toBe(2);\n\texpect([result[1], result[2]].sort((a, b) => a - b).join(\" \")).toBe(\"1 3\");\n\texpect(result[3]).toBe(0);\n});\n\n// no longer included in API\n// test(\"clusterIndicesOfSortedNumbers\", () => {\n// \tconst result = ear.general.clusterIndicesOfSortedNumbers([0, 1, 2, 3, 4, 5]);\n// \tequalTest(result, [[0], [1], [2], [3], [4], [5]]);\n// \tconst result1 = ear.general.clusterIndicesOfSortedNumbers([0, 1, 2, 3, 4, 5], 1);\n// \tequalTest(result1, [[0], [1], [2], [3], [4], [5]]);\n//\tconst result2 = ear.general\n//\t.clusterIndicesOfSortedNumbers([0, 1, 2, 3, 4, 5], 1 + ear.math.EPSILON * 2);\n// \tequalTest(result2, [[0, 1, 2, 3, 4, 5]]);\n// });\n\ntest(\"convexHullRadialSortPoints\", () => {\n\tequalTest(ear.math.convexHullRadialSortPoints(), []);\n\n\tconst result0 = ear.math.convexHullRadialSortPoints([[1, 0], [0, 1], [-1, 0]]);\n\tequalTest(result0, [[2], [0], [1]]);\n\tconst result1 = ear.math.convexHullRadialSortPoints([[0, 1], [-1, 0], [1, 0]]);\n\tequalTest(result1, [[1], [2], [0]]);\n\tconst result2 = ear.math.convexHullRadialSortPoints([[-1, 0], [1, 0], [0, 1]]);\n\tequalTest(result2, [[0], [1], [2]]);\n\n\tconst result3 = ear.math.convexHullRadialSortPoints([[1, 0], [0, 1], [-1, 0]], 2);\n\tequalTest(result3, [[2], [1, 0]]);\n\tconst result4 = ear.math.convexHullRadialSortPoints([[0, 1], [-1, 0], [1, 0]], 2);\n\tequalTest(result4, [[1], [0, 2]]);\n\tconst result5 = ear.math.convexHullRadialSortPoints([[-1, 0], [1, 0], [0, 1]], 2);\n\tequalTest(result5, [[0], [2, 1]]);\n});\n\ntest(\"radialSortVectors3\", () => {\n\tconst points = Array.from(Array(24))\n\t\t.map(() => [Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1]);\n\tpoints.push([0, 0, 0]);\n\tconst result = ear.general.radialSortVectors3(points, [1, 0, 0]);\n\texpect(result.length).toBe(25);\n});\n\ntest(\"radialSortVectors3\", () => {\n\tconst points = [\n\t\t[1, 1, 1],\n\t\t[-1, -1, -1],\n\t\t[-1, -1, 1],\n\t\t[1, 1, -1],\n\t];\n\tconst resultX = ear.general.radialSortVectors3(points, [1, 0, 0]);\n\tconst resultY = ear.general.radialSortVectors3(points, [0, 1, 0]);\n\tconst resultZ = ear.general.radialSortVectors3(points, [0, 0, 1]);\n\tconst resultXn = ear.general.radialSortVectors3(points, [-1, 0, 0]);\n\tconst resultYn = ear.general.radialSortVectors3(points, [0, -1, 0]);\n\tconst resultZn = ear.general.radialSortVectors3(points, [0, 0, -1]);\n\texpect(JSON.stringify(resultX)).toBe(JSON.stringify([3, 0, 2, 1]));\n\texpect(JSON.stringify(resultY)).toBe(JSON.stringify([0, 3, 1, 2]));\n\texpect(JSON.stringify(resultZ)).toBe(JSON.stringify([0, 3, 1, 2]));\n\texpect(JSON.stringify(resultXn)).toBe(JSON.stringify([0, 3, 1, 2]));\n\texpect(JSON.stringify(resultYn)).toBe(JSON.stringify([3, 0, 2, 1]));\n\texpect(JSON.stringify(resultZn)).toBe(JSON.stringify([0, 3, 1, 2]));\n});\n\n// test(\"smallestVector2 easy\", () => {\n// \texpect(ear.math.smallestVector2()).toBe(undefined);\n// \texpect(ear.math.smallestVector2([])).toBe(undefined);\n// \texpect(ear.math.smallestVector2([[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]])).toBe(0);\n// \texpect(ear.math.smallestVector2([[4, 0], [3, 0], [2, 0], [1, 0], [0, 0]])).toBe(4);\n// \texpect(ear.math.smallestVector2([[2, 0], [1, 0], [0, 0], [4, 0], [3, 0]])).toBe(2);\n// });\n\n// test(\"smallestVector2 vertically aligned\", () => {\n// \texpect(ear.math.smallestVector2([[3, 0], [3, 1], [3, 2], [3, 3], [3, 4]])).toBe(0);\n// \texpect(ear.math.smallestVector2([[3, 1], [3, 2], [3, 3], [3, 4], [3, 0]])).toBe(4);\n// \texpect(ear.math.smallestVector2([[3, 2], [3, 3], [3, 4], [3, 0], [3, 1]])).toBe(3);\n// \texpect(ear.math.smallestVector2([[3, 3], [3, 4], [3, 0], [3, 1], [3, 2]])).toBe(2);\n// });\n"
  },
  {
    "path": "tests/math.general.typeof.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"removed\", () => expect(true).toBe(true));\n\n// test(\"type guessing\", () => {\n// \tconst vector1 = [1, 2, 3];\n// \tconst vector2 = [1, 2, 3, 4, 5, 6, 7, 8, 9];\n// \tconst line = { vector: [1, 1], origin: [0.5, 0.5] };\n// \tconst segment1 = [[1, 2], [4, 5]];\n// \tconst polygon1 = [[1, 2]];\n// \tconst polygon2 = [[1, 2], [4, 5], [6, 7]];\n// \tconst polygon3 = [[1], [2], [3], [4]];\n// \tconst circle = { radius: 1, origin: [1, 2] };\n// \tconst boundingBox1 = { min: [1, 2], max: [6, 8], span: [5, 6] };\n// \tconst boundingBox2 = { min: [3], max: [5], span: [2] };\n\n// \texpect(ear.math.typeof(vector1)).toBe(\"vector\");\n// \texpect(ear.math.typeof(vector2)).toBe(\"vector\");\n// \texpect(ear.math.typeof(line)).toBe(\"line\");\n// \texpect(ear.math.typeof(segment1)).toBe(\"segment\");\n// \texpect(ear.math.typeof(polygon1)).toBe(\"polygon\");\n// \texpect(ear.math.typeof(polygon2)).toBe(\"polygon\");\n// \texpect(ear.math.typeof(polygon3)).toBe(\"polygon\");\n// \texpect(ear.math.typeof(circle)).toBe(\"circle\");\n// \texpect(ear.math.typeof(boundingBox1)).toBe(\"box\");\n// \texpect(ear.math.typeof(boundingBox2)).toBe(\"box\");\n// \t// Javascript primitives\n// \texpect(ear.math.typeof({})).toBe(\"object\");\n// \texpect(ear.math.typeof([])).toBe(\"object\");\n// \texpect(ear.math.typeof(() => {})).toBe(\"function\");\n// \texpect(ear.math.typeof(4)).toBe(\"number\");\n// \texpect(ear.math.typeof(true)).toBe(\"boolean\");\n// \texpect(ear.math.typeof(\"s\")).toBe(\"string\");\n// });\n\n// test(\"speed of type guessing\", () => {\n// \tconst objects = [\n// \t\t[1, 2, 3],\n// \t\t[1, 2, 3, 4, 5, 6, 7, 8, 9],\n// \t\t{ vector: [1, 1], origin: [0.5, 0.5] },\n// \t\t[[1, 2], [4, 5]],\n// \t\t[[1, 2]],\n// \t\t[[1, 2], [4, 5], [6, 7]],\n// \t\t[[1], [2], [3], [4]],\n// \t\t{ radius: 1, origin: [1, 2] },\n// \t];\n// \t// one million takes 54 milliseconds\n// \tconsole.time(\"type-speed\");\n// \tfor (let i = 0; i < 1000000; i += 1) {\n// \t\tear.math.typeof(objects[i % objects.length]);\n// \t}\n// \tconsole.timeEnd(\"type-speed\");\n// });\n"
  },
  {
    "path": "tests/math.geometry.convexHull.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"convexHull\", () => {\n\tconst rect = [\n\t\t[1, 0],\n\t\t[0, 0],\n\t\t[1, 1],\n\t\t[0, 1],\n\t];\n\tconst res0 = ear.math.convexHull(rect)\n\t\t.map(v => rect[v]);\n\tconst res1 = ear.math.convexHull(rect, true)\n\t\t.map(v => rect[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(4);\n});\n\ntest(\"convexHull collinear\", () => {\n\tconst rect_collinear = [\n\t\t[1, 0],\n\t\t[0, 0],\n\t\t[1, 1],\n\t\t[0, 1],\n\t\t[0.5, 0],\n\t\t[0, 0.5],\n\t\t[1, 0.5],\n\t\t[0.5, 1],\n\t];\n\tconst res0 = ear.math.convexHull(rect_collinear)\n\t\t.map(v => rect_collinear[v]);\n\tconst res1 = ear.math.convexHull(rect_collinear, true)\n\t\t.map(v => rect_collinear[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(8);\n});\n\ntest(\"convexHull collinear\", () => {\n\tconst rect_collinear = [\n\t\t[3, 0],\n\t\t[0, 0],\n\t\t[3, 3],\n\t\t[0, 3],\n\t\t// collinear points\n\t\t[1, 0],\n\t\t[0, 1],\n\t\t[3, 1],\n\t\t[1, 3],\n\t\t[2, 0],\n\t\t[0, 2],\n\t\t[3, 2],\n\t\t[2, 3],\n\t];\n\tconst res0 = ear.math.convexHull(rect_collinear)\n\t\t.map(v => rect_collinear[v]);\n\tconst res1 = ear.math.convexHull(rect_collinear, true)\n\t\t.map(v => rect_collinear[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(12);\n});\n\ntest(\"convexHull axisaligned\", () => {\n\tconst rect = [\n\t\t[1, 0],\n\t\t[-1, 0],\n\t\t[0, 1],\n\t\t[0, -1],\n\t];\n\tconst res0 = ear.math.convexHull(rect)\n\t\t.map(v => rect[v]);\n\tconst res1 = ear.math.convexHull(rect, true)\n\t\t.map(v => rect[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(4);\n});\n\ntest(\"convexHull collinear axisaligned\", () => {\n\tconst rect = [\n\t\t[1, 0],\n\t\t[-1, 0],\n\t\t[0, 1],\n\t\t[0, -1],\n\t\t[0.5, 0.5],\n\t\t[0.5, -0.5],\n\t\t[-0.5, -0.5],\n\t\t[-0.5, 0.5],\n\t];\n\tconst res0 = ear.math.convexHull(rect)\n\t\t.map(v => rect[v]);\n\tconst res1 = ear.math.convexHull(rect, true)\n\t\t.map(v => rect[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(8);\n});\n\ntest(\"convexHull collinear axisaligned\", () => {\n\tconst rect = [\n\t\t[3, 0],\n\t\t[-3, 0],\n\t\t[0, 3],\n\t\t[0, -3],\n\t\t// collinear points\n\t\t[1, 2],\n\t\t[2, 1],\n\t\t[1, -2],\n\t\t[2, -1],\n\t\t[-1, -2],\n\t\t[-2, -1],\n\t\t[-1, 2],\n\t\t[-2, 1],\n\t];\n\tconst res0 = ear.math.convexHull(rect)\n\t\t.map(v => rect[v]);\n\tconst res1 = ear.math.convexHull(rect, true)\n\t\t.map(v => rect[v]);\n\texpect(res0.length).toBe(4);\n\texpect(res1.length).toBe(12);\n});\n"
  },
  {
    "path": "tests/math.geometry.line.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"collinearBetween\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\tconst p1 = [0.5, 0];\n\texpect(ear.math.collinearBetween(p0, p1, p2, false)).toBe(true);\n});\n\ntest(\"collinearBetween on endpoint, inclusive\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\tconst p1 = [1e-12, 0];\n\texpect(ear.math.collinearBetween(p0, p1, p2, true)).toBe(true);\n});\n\ntest(\"collinearBetween on endpoint, exclusive\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\tconst p1 = [1e-12, 0];\n\texpect(ear.math.collinearBetween(p0, p1, p2)).toBe(false);\n});\n\ntest(\"collinearBetween almost near endpoint, exclusive\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\tconst p1 = [1e-4, 0];\n\texpect(ear.math.collinearBetween(p0, p1, p2)).toBe(true);\n});\n\ntest(\"collinearBetween perpendicularly away too far\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\texpect(ear.math.collinearBetween(p0, [0.5, 1e-2], p2)).toBe(false);\n\texpect(ear.math.collinearBetween(p0, [0.5, 1e-3], p2)).toBe(false);\n\texpect(ear.math.collinearBetween(p0, [0.5, 1e-4], p2)).toBe(true);\n\texpect(ear.math.collinearBetween(p0, [0.5, 1e-5], p2)).toBe(true);\n});\n\ntest(\"collinearBetween perpendicularly away near\", () => {\n\tconst [p0, p2] = [[0, 0], [1, 0]];\n\tconst p1 = [0.5, 1e-12];\n\texpect(ear.math.collinearBetween(p0, p1, p2)).toBe(true);\n});\n\ntest(\"pleats\", () => {\n\tconst a = { vector: [1, 0], origin: [0, 0] };\n\tconst b = { vector: [1, 0], origin: [10, 0] };\n\tconst pleats = ear.math.pleat(a, b, 10);\n\tpleats[0].forEach((line, i) => {\n\t\texpect(line.origin[0]).toBeCloseTo(i + 1);\n\t});\n\tpleats[0].forEach(line => {\n\t\texpect(line.vector[0]).toBeCloseTo(1);\n\t\texpect(line.vector[1]).toBeCloseTo(0);\n\t});\n});\n\ntest(\"pleats, opposite vector\", () => {\n\tconst a = { vector: [1, 0], origin: [0, 0] };\n\tconst b = { vector: [-1, 0], origin: [1, 0] };\n\tconst pleats = ear.math.pleat(a, b, 4);\n\texpect(pleats[0].length).toBe(0);\n\tpleats[1].forEach(line => {\n\t\texpect(line.vector[0]).toBeCloseTo(1);\n\t\texpect(line.vector[1]).toBeCloseTo(0);\n\t});\n});\n\n// test(\"lerp lines, opposite vectors\", () => {\n// \tconst a = { vector: [1, 0], origin: [0, 0] };\n// \tconst b = { vector: [-1, 0], origin: [1, 0] };\n// \texpect(ear.math.lerpLines(a, b, 0.25).origin[0]).toBeCloseTo(0.25);\n// \texpect(ear.math.lerpLines(a, b, 0.5).origin[0]).toBeCloseTo(0.5);\n// \texpect(ear.math.lerpLines(a, b, 0.75).origin[0]).toBeCloseTo(0.75);\n// \texpect(ear.math.lerpLines(a, b, 0.25).origin[1]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.5).origin[1]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.75).origin[1]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.25).vector[0]).toBeCloseTo(0.5);\n// \texpect(ear.math.lerpLines(a, b, 0.5).vector[0]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.75).vector[0]).toBeCloseTo(-0.5);\n// \texpect(ear.math.lerpLines(a, b, 0.25).vector[1]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.5).vector[1]).toBeCloseTo(0);\n// \texpect(ear.math.lerpLines(a, b, 0.75).vector[1]).toBeCloseTo(0);\n// });\n"
  },
  {
    "path": "tests/math.geometry.nearest.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectors = function (...args) {\n\texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n};\n\ntest(\"nearest point\", () => {\n\ttestEqualVectors([5, 5], ear.math.nearestPoint2(\n\t\t[[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9]],\n\t\t[10, 0],\n\t));\n\ttestEqualVectors([6, 6, 0], ear.math.nearestPoint(\n\t\t[[0, 0, 0], [1, 1, 0], [2, 2, 0], [3, 3, 0], [4, 4, 1],\n\t\t\t[5, 5, 10], [6, 6, 0], [7, 7, 0], [8, 8, 0], [9, 9, 0]],\n\t\t[10, 0, 0],\n\t));\n});\n\ntest(\"nearestPointOnPolygon\", () => {\n\tconst polygon = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\tconst result = ear.math.nearestPointOnPolygon(polygon, [10, 10]);\n\t// result { point: [ 0.5, 0.5 ], edge: 0, distance: 13.435028842544403 }\n\texpect(result.point[0]).toBe(0.5);\n\texpect(result.point[1]).toBe(0.5);\n\texpect(result.distance).toBeCloseTo(13.435028842544403);\n\texpect(result.edge).toBe(0);\n\texpect(polygon[result.edge][0]).toBe(1);\n\texpect(polygon[result.edge][1]).toBe(0);\n\n\texpect(ear.math.nearestPointOnPolygon(polygon, [-10, 10]).edge).toBe(1);\n\texpect(ear.math.nearestPointOnPolygon(polygon, [-10, -10]).edge).toBe(2);\n\texpect(ear.math.nearestPointOnPolygon(polygon, [10, -10]).edge).toBe(3);\n});\n\ntest(\"nearestPointOnPolygon nearest to vertex\", () => {\n\tconst polygon = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\n\tconst result1 = ear.math.nearestPointOnPolygon(polygon, [10, 0]);\n\tconst result2 = ear.math.nearestPointOnPolygon(polygon, [0, 10]);\n\tconst result3 = ear.math.nearestPointOnPolygon(polygon, [-10, 0]);\n\tconst result4 = ear.math.nearestPointOnPolygon(polygon, [0, -10]);\n\n\texpect(result1.point[0]).toBe(1);\n\texpect(result1.point[1]).toBe(0);\n\texpect(result2.point[0]).toBe(0);\n\texpect(result2.point[1]).toBe(1);\n\texpect(result3.point[0]).toBe(-1);\n\texpect(result3.point[1]).toBe(0);\n\texpect(result4.point[0]).toBe(0);\n\texpect(result4.point[1]).toBe(-1);\n\n\texpect(result1.edge).toBe(0);\n\texpect(result2.edge).toBe(0);\n\texpect(result3.edge).toBe(1);\n\texpect(result4.edge).toBe(2);\n});\n\ntest(\"nearestPointOnCircle\", () => {\n\tconst circle = { radius: 1, origin: [0, 0] };\n\n\tconst result1 = ear.math.nearestPointOnCircle(circle, [10, 0]);\n\tconst result2 = ear.math.nearestPointOnCircle(circle, [0, 10]);\n\tconst result3 = ear.math.nearestPointOnCircle(circle, [-10, 0]);\n\tconst result4 = ear.math.nearestPointOnCircle(circle, [0, -10]);\n\n\tconst result5 = ear.math.nearestPointOnCircle(circle, [10, 10]);\n\tconst result6 = ear.math.nearestPointOnCircle(circle, [-10, 10]);\n\tconst result7 = ear.math.nearestPointOnCircle(circle, [-10, -10]);\n\tconst result8 = ear.math.nearestPointOnCircle(circle, [10, -10]);\n\n\texpect(result1[0]).toBeCloseTo(1);\n\texpect(result1[1]).toBeCloseTo(0);\n\texpect(result2[0]).toBeCloseTo(0);\n\texpect(result2[1]).toBeCloseTo(1);\n\texpect(result3[0]).toBeCloseTo(-1);\n\texpect(result3[1]).toBeCloseTo(0);\n\texpect(result4[0]).toBeCloseTo(0);\n\texpect(result4[1]).toBeCloseTo(-1);\n\n\texpect(result5[0]).toBeCloseTo(Math.SQRT1_2);\n\texpect(result5[1]).toBeCloseTo(Math.SQRT1_2);\n\texpect(result6[0]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(result6[1]).toBeCloseTo(Math.SQRT1_2);\n\texpect(result7[0]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(result7[1]).toBeCloseTo(-Math.SQRT1_2);\n\texpect(result8[0]).toBeCloseTo(Math.SQRT1_2);\n\texpect(result8[1]).toBeCloseTo(-Math.SQRT1_2);\n});\n"
  },
  {
    "path": "tests/math.geometry.polygon.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectorVectors = function (a, b) {\n\texpect(a.length).toBe(b.length);\n\ta.forEach((_, i) => expect(ear.math.epsilonEqualVectors(a[i], b[i]))\n\t\t.toBe(true));\n};\n\ntest(\"signedArea\", () => {\n\texpect(ear.math.signedArea([[1, 0], [0, 1], [-1, 0], [0, -1]])).toBeCloseTo(2);\n\texpect(ear.math.signedArea([[1, 0], [0, 1], [-1, 0]])).toBeCloseTo(1);\n});\n\ntest(\"centroid\", () => {\n\texpect(ear.math.centroid([[1, 0], [0, 1], [-1, 0], [0, -1]])[0]).toBeCloseTo(0);\n\texpect(ear.math.centroid([[1, 0], [0, 1], [-1, 0], [0, -1]])[1]).toBeCloseTo(0);\n\texpect(ear.math.centroid([[1, 0], [0, 1], [-1, 0]])[0]).toBeCloseTo(0);\n\texpect(ear.math.centroid([[1, 0], [0, 1], [-1, 0]])[1]).toBeCloseTo(1 / 3);\n});\n\ntest(\"boundingBox\", () => {\n\tconst box = ear.math.boundingBox([[1, 0], [0, 1], [-1, 0], [0, -1]]);\n\texpect(box.min[0]).toBe(-1);\n\texpect(box.min[1]).toBe(-1);\n\texpect(box.span[0]).toBe(2);\n\texpect(box.span[1]).toBe(2);\n\tconst badBox = ear.math.boundingBox();\n\texpect(badBox).toBe(undefined);\n});\n\ntest(\"makePolygonCircumradius\", () => {\n\texpect(ear.math.makePolygonCircumradius().length).toBe(3);\n\tconst vert_square = ear.math.makePolygonCircumradius(4);\n\texpect(vert_square[0][0]).toBe(1);\n\texpect(vert_square[0][1]).toBe(0);\n\tconst vert_square_2 = ear.math.makePolygonCircumradius(4, 2);\n\texpect(vert_square_2[0][0]).toBe(2);\n\texpect(vert_square_2[0][1]).toBe(0);\n\n\tconst tri1 = ear.math.makePolygonCircumradius(3);\n\tconst tri2 = ear.math.makePolygonCircumradius(3, 2);\n\t// first coord (1,0)\n\texpect(tri1[0][0]).toBeCloseTo(1);\n\texpect(tri1[0][1]).toBeCloseTo(0);\n\texpect(tri1[1][0]).toBeCloseTo(-0.5);\n\texpect(tri1[1][1]).toBeCloseTo(Math.sqrt(3) / 2);\n\texpect(tri1[2][0]).toBeCloseTo(-0.5);\n\texpect(tri1[2][1]).toBeCloseTo(-Math.sqrt(3) / 2);\n\t// 2\n\texpect(tri2[0][0]).toBeCloseTo(2);\n\texpect(tri2[1][0]).toBeCloseTo(-1);\n});\n\ntest(\"make regular polygon side aligned\", () => {\n\tconst tri = ear.math.makePolygonCircumradiusSide();\n\texpect(tri.length).toBe(3);\n\tconst square = ear.math.makePolygonCircumradiusSide(4);\n\texpect(square[0][0]).toBeCloseTo(Math.sqrt(2) / 2);\n\tconst square2 = ear.math.makePolygonCircumradiusSide(4, 2);\n\texpect(square2[0][0]).toBeCloseTo(Math.sqrt(2));\n});\n\ntest(\"make regular polygon inradius\", () => {\n\tconst tri = ear.math.makePolygonInradius();\n\texpect(tri.length).toBe(3);\n\tconst square = ear.math.makePolygonInradius(4);\n\texpect(square[0][0]).toBeCloseTo(Math.sqrt(2));\n\texpect(square[0][1]).toBeCloseTo(0);\n});\n\ntest(\"make_polygon_inradius_s\", () => {\n\tconst tri = ear.math.makePolygonInradiusSide();\n\texpect(tri.length).toBe(3);\n\tconst square = ear.math.makePolygonInradiusSide(4);\n\texpect(square[0][0]).toBeCloseTo(1);\n\tconst square2 = ear.math.makePolygonInradiusSide(4, 2);\n\texpect(square2[0][0]).toBeCloseTo(2);\n});\n\ntest(\"make_polygon_side_length\", () => {\n\tconst tri = ear.math.makePolygonSideLength();\n\texpect(tri.length).toBe(3);\n\tconst square = ear.math.makePolygonSideLength(4);\n\texpect(square[0][0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(square[0][1]).toBeCloseTo(0);\n\tconst square2 = ear.math.makePolygonSideLength(4, 2);\n\texpect(square2[0][0]).toBeCloseTo(Math.sqrt(2));\n\texpect(square2[0][1]).toBeCloseTo(0);\n});\n\ntest(\"make_polygon_side_length_s\", () => {\n\tconst tri = ear.math.makePolygonSideLengthSide();\n\texpect(tri.length).toBe(3);\n\tconst square = ear.math.makePolygonSideLengthSide(4);\n\texpect(square[0][0]).toBeCloseTo(0.5);\n\tconst square2 = ear.math.makePolygonSideLengthSide(4, 2);\n\texpect(square2[0][0]).toBeCloseTo(1);\n});\n\ntest(\"makePolygonNonCollinear\", () => {\n\tconst polygon = [[0, 0], [1, 0], [2, 0], [2, 2], [0, 2]];\n\tconst result = ear.math.makePolygonNonCollinear(polygon);\n\ttestEqualVectorVectors(\n\t\t[[0, 0], [2, 0], [2, 2], [0, 2]],\n\t\tresult,\n\t);\n});\n"
  },
  {
    "path": "tests/math.geometry.radial.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectors = function (...args) {\n\texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n};\n\ntest(\"isCounterClockwiseBetween\", () => {\n\texpect(ear.math.isCounterClockwiseBetween(0.5, 0, 1)).toBe(true);\n\texpect(ear.math.isCounterClockwiseBetween(0.5, 1, 0)).toBe(false);\n\texpect(ear.math.isCounterClockwiseBetween(11, 10, 12)).toBe(true);\n\texpect(ear.math.isCounterClockwiseBetween(11, 12, 10)).toBe(false);\n\texpect(ear.math.isCounterClockwiseBetween(\n\t\tMath.PI * (2 * 4) + Math.PI / 2,\n\t\t0,\n\t\tMath.PI,\n\t)).toBe(true);\n\texpect(ear.math.isCounterClockwiseBetween(\n\t\tMath.PI * (2 * 4) + Math.PI / 2,\n\t\tMath.PI,\n\t\t0,\n\t)).toBe(false);\n});\n\ntest(\"interior angles\", () => {\n\ttestEqualVectors(\n\t\t[Math.PI / 2, Math.PI / 2, Math.PI / 2, Math.PI / 2],\n\t\t[[1, 0], [0, 1], [-1, 0], [0, -1]]\n\t\t\t.map((v, i, ar) => ear.math.counterClockwiseAngle2(v, ar[(i + 1) % ar.length])),\n\t);\n\ttestEqualVectors(\n\t\t[Math.PI / 2, Math.PI / 2, Math.PI / 2, Math.PI / 2],\n\t\t[[1, 1], [-1, 1], [-1, -1], [1, -1]]\n\t\t\t.map((v, i, ar) => ear.math.counterClockwiseAngle2(v, ar[(i + 1) % ar.length])),\n\t);\n});\n\ntest(\"counter-clockwise vector sorting\", () => {\n\ttestEqualVectors(\n\t\t[0, 1, 2, 3],\n\t\tear.math.counterClockwiseOrder2([[1, 1], [-1, 1], [-1, -1], [1, -1]]),\n\t);\n\ttestEqualVectors(\n\t\t[0, 3, 2, 1],\n\t\tear.math.counterClockwiseOrder2([[1, -1], [-1, -1], [-1, 1], [1, 1]]),\n\t);\n});\n\n// test(\"sectors\", () => {\n//   testEqual(Math.PI / 2, ear.math.sector.fromVectors([1, 0], [0, 1]).angle);\n//   testEqual(true, ear.math.sector.fromVectors([1, 0], [0, 1]).contains([1, 1]));\n//   testEqual(false, ear.math.sector.fromVectors([1, 0], [0, 1]).contains([-1, 1]));\n//   testEqual(false, ear.math.sector.fromVectors([1, 0], [0, 1]).contains([-1, -1]));\n//   testEqual(false, ear.math.sector.fromVectors([1, 0], [0, 1]).contains([1, -1]));\n// });\n\n// test(\"junctions\", () => {\n//   testEqual([[1, 1], [1, -1], [-1, 1], [-1, -1]],\n//     ear.math.junction([1, 1], [1, -1], [-1, 1], [-1, -1]).vectors);\n//   testEqual([0, 2, 3, 1],\n//     ear.math.junction([1, 1], [1, -1], [-1, 1], [-1, -1]).vectorOrder);\n//   testEqual([Math.PI / 2, Math.PI / 2, Math.PI / 2, Math.PI / 2],\n//     ear.math.junction([1, 1], [1, -1], [-1, 1], [-1, -1]).angles());\n// });\n\ntest(\"clockwiseAngleRadians\", () => {\n\texpect(ear.math.clockwiseAngleRadians(Math.PI, Math.PI / 2))\n\t\t.toBeCloseTo(Math.PI * (1 / 2));\n\texpect(ear.math.clockwiseAngleRadians(Math.PI / 2, Math.PI))\n\t\t.toBeCloseTo(Math.PI * (3 / 2));\n\t// same as above with negative numbers\n\texpect(ear.math.clockwiseAngleRadians(\n\t\tMath.PI + Math.PI * (2 * 4),\n\t\tMath.PI / 2 - Math.PI * (2 * 8),\n\t)).toBeCloseTo(Math.PI * (1 / 2));\n\texpect(ear.math.clockwiseAngleRadians(\n\t\tMath.PI / 2 - Math.PI * (2 * 3),\n\t\tMath.PI + Math.PI * (2 * 4),\n\t)).toBeCloseTo(Math.PI * (3 / 2));\n\texpect(ear.math.clockwiseAngleRadians(\n\t\tMath.PI - Math.PI * (2 * 4),\n\t\tMath.PI / 2 - Math.PI * (2 * 8),\n\t)).toBeCloseTo(Math.PI * (1 / 2));\n\texpect(ear.math.clockwiseAngleRadians(\n\t\tMath.PI / 2 - Math.PI * (2 * 3),\n\t\tMath.PI - Math.PI * (2 * 4),\n\t)).toBeCloseTo(Math.PI * (3 / 2));\n});\n\ntest(\"counterClockwiseAngleRadians\", () => {\n\texpect(ear.math.counterClockwiseAngleRadians(Math.PI, Math.PI / 2))\n\t\t.toBeCloseTo(Math.PI * (3 / 2));\n\texpect(ear.math.counterClockwiseAngleRadians(Math.PI / 2, Math.PI))\n\t\t.toBeCloseTo(Math.PI * (1 / 2));\n\t// same as above with negative numbers\n\texpect(ear.math.counterClockwiseAngleRadians(\n\t\tMath.PI - Math.PI * (2 * 4),\n\t\tMath.PI / 2 - Math.PI * (2 * 5),\n\t)).toBeCloseTo(Math.PI * (3 / 2));\n\texpect(ear.math.counterClockwiseAngleRadians(\n\t\tMath.PI + Math.PI * (2 * 4),\n\t\tMath.PI / 2 + Math.PI * (2 * 5),\n\t)).toBeCloseTo(Math.PI * (3 / 2));\n\texpect(ear.math.counterClockwiseAngleRadians(\n\t\tMath.PI / 2 - Math.PI * (2 * 7),\n\t\tMath.PI - Math.PI * (2 * 3),\n\t)).toBeCloseTo(Math.PI * (1 / 2));\n});\n\ntest(\"clockwiseAngle2\", () => {\n\texpect(ear.math.clockwiseAngle2([1, 0], [0, 1])).toBeCloseTo(Math.PI * (3 / 2));\n\texpect(ear.math.clockwiseAngle2([0, 1], [1, 0])).toBeCloseTo(Math.PI * (1 / 2));\n});\n\ntest(\"counterClockwiseAngle2\", () => {\n\texpect(ear.math.counterClockwiseAngle2([1, 0], [0, 1]))\n\t\t.toBeCloseTo(Math.PI * (1 / 2));\n\texpect(ear.math.counterClockwiseAngle2([0, 1], [1, 0]))\n\t\t.toBeCloseTo(Math.PI * (3 / 2));\n});\n\n// test(\"counter_clockwise_vector_order\", () => {\n//   ear.math.counter_clockwise_vector_order(...vectors)\n// });\n\ntest(\"interior sector angles\", () => {\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [0, 1], [-1, 0]])[0])\n\t\t.toBeCloseTo(Math.PI / 2);\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [0, 1], [-1, 0]])[1])\n\t\t.toBeCloseTo(Math.PI / 2);\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [0, 1], [-1, 0]])[2])\n\t\t.toBeCloseTo(Math.PI);\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [-1, 0], [0, -1]])[0])\n\t\t.toBeCloseTo(Math.PI);\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [-1, 0], [0, -1]])[1])\n\t\t.toBeCloseTo(Math.PI / 2);\n\texpect(ear.math.counterClockwiseSectors2([[1, 0], [-1, 0], [0, -1]])[2])\n\t\t.toBeCloseTo(Math.PI / 2);\n});\n\ntest(\"clockwise bisect\", () => {\n\texpect(ear.math.clockwiseBisect2([1, 0], [0, -1])[0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseBisect2([1, 0], [0, -1])[1]).toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseBisect2([1, 0], [-1, 0])[0]).toBeCloseTo(0);\n\texpect(ear.math.clockwiseBisect2([1, 0], [-1, 0])[1]).toBeCloseTo(-1);\n\texpect(ear.math.clockwiseBisect2([1, 0], [0, 1])[0]).toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseBisect2([1, 0], [0, 1])[1]).toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseBisect2([1, 0], [1, 0])[0]).toBeCloseTo(1);\n\texpect(ear.math.clockwiseBisect2([1, 0], [1, 0])[1]).toBeCloseTo(0);\n});\n\ntest(\"counter-clockwise bisect\", () => {\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, 1])[0]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, 1])[1]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [-1, 0])[0]).toBeCloseTo(0);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [-1, 0])[1]).toBeCloseTo(1);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, -1])[0]).toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, -1])[1]).toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [1, 0])[0]).toBeCloseTo(1);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [1, 0])[1]).toBeCloseTo(0);\n});\n\ntest(\"counterClockwiseBisect2\", () => {\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, 1])[0])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [0, 1])[1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([0, 1], [-1, 0])[0])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseBisect2([0, 1], [-1, 0])[1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\t// flipped vectors\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [-1, 0])[0]).toBeCloseTo(0);\n\texpect(ear.math.counterClockwiseBisect2([1, 0], [-1, 0])[1]).toBeCloseTo(1);\n});\n\ntest(\"bisectLines2\", () => {\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t)[1])\n\t\t.toBe(undefined);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t)[0].vector[0])\n\t\t.toBeCloseTo(0);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t)[0].vector[1])\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t)[0].origin[0])\n\t\t.toBeCloseTo(0.5);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t)[0].origin[1])\n\t\t.toBeCloseTo(0);\n\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [1, 1], origin: [1, 0] },\n\t)[0].vector[0])\n\t\t.toBeCloseTo(0.3826834323650897);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [1, 1], origin: [1, 0] },\n\t)[0].vector[1])\n\t\t.toBeCloseTo(0.9238795325112867);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [1, 1], origin: [1, 0] },\n\t)[0].origin[0])\n\t\t.toBeCloseTo(0);\n\texpect(ear.math.bisectLines2(\n\t\t{ vector: [0, 1], origin: [0, 0] },\n\t\t{ vector: [1, 1], origin: [1, 0] },\n\t)[0].origin[1])\n\t\t.toBeCloseTo(-1);\n});\n\ntest(\"counterClockwiseSubsectRadians\", () => {\n\ttestEqualVectors(\n\t\tear.math.counterClockwiseSubsectRadians(0, 3, 3),\n\t\t[1, 2],\n\t);\n\ttestEqualVectors(\n\t\tear.math.counterClockwiseSubsectRadians(-1, 2, 3),\n\t\t[0, 1],\n\t);\n\texpect(ear.math.counterClockwiseSubsectRadians(0, -Math.PI, 4)[0])\n\t\t.toBeCloseTo(Math.PI * (1 / 4));\n\texpect(ear.math.counterClockwiseSubsectRadians(0, -Math.PI, 4)[1])\n\t\t.toBeCloseTo(Math.PI * (2 / 4));\n\texpect(ear.math.counterClockwiseSubsectRadians(0, -Math.PI, 4)[2])\n\t\t.toBeCloseTo(Math.PI * (3 / 4));\n\texpect(ear.math.counterClockwiseSubsectRadians(0, -Math.PI, 2)[0])\n\t\t.toBeCloseTo(Math.PI / 2);\n\texpect(ear.math.counterClockwiseSubsectRadians(0, -Math.PI, 1).length)\n\t\t.toBe(0);\n});\n\ntest(\"counterClockwiseSubsect2\", () => {\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [0, 1], 2)[0][0])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [0, 1], 2)[0][1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[0][0])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[0][1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[1][0])\n\t\t.toBeCloseTo(0);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[1][1])\n\t\t.toBeCloseTo(1);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[2][0])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.counterClockwiseSubsect2([1, 0], [-1, 0], 4)[2][1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n});\n\ntest(\"clockwiseSubsectRadians\", () => {\n\ttestEqualVectors(ear.math.clockwiseSubsectRadians(3, 0, 3), [4, 5]);\n\ttestEqualVectors(ear.math.clockwiseSubsectRadians(2, -1, 3), [3, 4]);\n\ttestEqualVectors(ear.math.clockwiseSubsectRadians(2, -2, 4), [3, 4, 5]);\n\n\texpect(ear.math.clockwiseSubsectRadians(-Math.PI, 0, 4)[0])\n\t\t.toBeCloseTo(-Math.PI * (3 / 4));\n\texpect(ear.math.clockwiseSubsectRadians(-Math.PI, 0, 4)[1])\n\t\t.toBeCloseTo(-Math.PI * (2 / 4));\n\texpect(ear.math.clockwiseSubsectRadians(-Math.PI, 0, 4)[2])\n\t\t.toBeCloseTo(-Math.PI * (1 / 4));\n\n\texpect(ear.math.clockwiseSubsectRadians(-Math.PI, 0, 2)[0])\n\t\t.toBeCloseTo(-Math.PI / 2);\n\texpect(ear.math.clockwiseSubsectRadians(-Math.PI, 0, 1).length)\n\t\t.toBe(0);\n});\n\ntest(\"clockwiseSubsect2\", () => {\n\texpect(ear.math.clockwiseSubsect2([0, 1], [1, 0], 2)[0][0])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseSubsect2([0, 1], [1, 0], 2)[0][1])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[0][0])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[0][1])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[1][0])\n\t\t.toBeCloseTo(0);\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[1][1])\n\t\t.toBeCloseTo(-1);\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[2][0])\n\t\t.toBeCloseTo(Math.sqrt(2) / 2);\n\texpect(ear.math.clockwiseSubsect2([-1, 0], [1, 0], 4)[2][1])\n\t\t.toBeCloseTo(-Math.sqrt(2) / 2);\n});\n\ntest(\"threePointTurnDirection\", () => {\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, 0])).toBe(0);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, 1])).toBe(1);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, -1])).toBe(-1);\n\t// with epsilon\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, 0.000001], 0.001)).toBe(0);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, 0.001], 0.000001)).toBe(1);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, -0.000001], 0.001)).toBe(0);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [2, -0.001], 0.000001)).toBe(-1);\n\t// 180 degree turn\n\texpect(ear.math.threePointTurnDirection([0, 0], [2, 0], [1, 0])).toBe(undefined);\n\texpect(ear.math.threePointTurnDirection([0, 0], [5, 5], [2, 2])).toBe(undefined);\n\texpect(ear.math.threePointTurnDirection([0, 0], [5, 0], [0, 0])).toBe(undefined);\n\texpect(ear.math.threePointTurnDirection([0, 0], [1, 0], [-1, 0])).toBe(undefined);\n});\n"
  },
  {
    "path": "tests/math.geometry.straightSkeleton.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"straight skeleton triangle\", () => {\n\tconst f1f = Math.sqrt(2) - 1;\n\tconst skeleton = ear.math.straightSkeleton([[1, 0], [0, 1], [-1, 0]]);\n\texpect(skeleton.length).toBe(4);\n\t[\"skeleton\", \"skeleton\", \"skeleton\", \"perpendicular\"]\n\t\t.forEach((key, i) => expect(skeleton[i].type).toBe(key));\n\t[[1, 0], [0, f1f]].forEach((pt, i) => ear.math.epsilonEqualVectors(\n\t\tpt,\n\t\tskeleton[0].points[i],\n\t));\n\t[[0, 1], [0, f1f]].forEach((pt, i) => ear.math.epsilonEqualVectors(\n\t\tpt,\n\t\tskeleton[1].points[i],\n\t));\n\t[[-1, 0], [0, f1f]].forEach((pt, i) => ear.math.epsilonEqualVectors(\n\t\tpt,\n\t\tskeleton[2].points[i],\n\t));\n});\n\ntest(\"straight skeleton quad\", () => {\n\tconst skeleton = ear.math.straightSkeleton([[0, 0], [2, 0], [2, 1], [0, 1]]);\n\texpect(skeleton.length).toBe(7);\n\t// const points = skeleton.map(el => el.points);\n\tconst keys = [\"skeleton\", \"perpendicular\"];\n\t[0, 0, 1, 0, 0, 0, 1].forEach((n, i) => expect(skeleton[i].type).toBe(keys[n]));\n});\n"
  },
  {
    "path": "tests/math.geometry.triangle.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"circumcircle\", () => {\n\tconst circle = ear.math.circumcircle([1, 0], [0, 1], [-1, 0]);\n\texpect(circle.origin[0]).toBeCloseTo(0);\n\texpect(circle.origin[1]).toBeCloseTo(0);\n\texpect(circle.radius).toBeCloseTo(1);\n\t// todo, this is the degenerate case. not sure why the result is such\n\tconst circle2 = ear.math.circumcircle([1, 0], [0, 0], [-1, 0]);\n\texpect(circle2.origin[0]).toBeCloseTo(0);\n\texpect(circle2.origin[1]).toBeCloseTo(0);\n\texpect(circle2.radius).toBeCloseTo(1);\n});\n"
  },
  {
    "path": "tests/math.intersect.clip.line.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// const {\n// \texclude,\n// \tinclude,\n// \tincludeL,\n// \texcludeL,\n// \tincludeR,\n// \texcludeR,\n// \tincludeS,\n// \texcludeS,\n// } = math;\n\nconst clip_line_in_convex_poly_inclusive = function () {\n\treturn ear.math.clipLineConvexPolygon(\n\t\t...arguments,\n\t\tear.math.include,\n\t\tear.math.includeL,\n\t);\n};\nconst clip_line_in_convex_poly_exclusive = function () {\n\treturn ear.math.clipLineConvexPolygon(\n\t\t...arguments,\n\t\tear.math.exclude,\n\t\tear.math.excludeL,\n\t);\n};\nconst clip_ray_in_convex_poly_inclusive = function () {\n\treturn ear.math.clipLineConvexPolygon(\n\t\t...arguments,\n\t\tear.math.include,\n\t\tear.math.includeR,\n\t);\n};\nconst clip_ray_in_convex_poly_exclusive = function () {\n\treturn ear.math.clipLineConvexPolygon(\n\t\t...arguments,\n\t\tear.math.exclude,\n\t\tear.math.excludeR,\n\t);\n};\nconst clip_segment_in_convex_poly_inclusive = function (poly, s0, s1) {\n\tconst vector = [s1[0] - s0[0], s1[1] - s0[1]];\n\treturn ear.math.clipLineConvexPolygon(\n\t\tpoly,\n\t\t{ vector, origin: s0 },\n\t\tear.math.include,\n\t\tear.math.includeS,\n\t);\n};\nconst clip_segment_in_convex_poly_exclusive = function (poly, s0, s1) {\n\tconst vector = [s1[0] - s0[0], s1[1] - s0[1]];\n\treturn ear.math.clipLineConvexPolygon(\n\t\tpoly,\n\t\t{ vector, origin: s0 },\n\t\tear.math.exclude,\n\t\tear.math.excludeS,\n\t);\n};\n\ntest(\"collinear line\", () => {\n\t// all inclusive cases will return a segment with unique endpoints\n\t// all exclusive cases will return undefined\n\tconst rect = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst lineHoriz1 = { vector: [1, 0], origin: [0.5, 0] };\n\tconst lineHoriz2 = { vector: [1, 0], origin: [0.5, 1] };\n\tconst lineVert1 = { vector: [0, 1], origin: [0, 0.5] };\n\tconst lineVert2 = { vector: [0, 1], origin: [1, 0.5] };\n\tconst result1 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineHoriz1,\n\t\tear.math.include,\n\t\tear.math.includeL,\n\t);\n\tconst result2 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineHoriz2,\n\t\tear.math.include,\n\t\tear.math.includeL,\n\t);\n\tconst result3 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineVert1,\n\t\tear.math.include,\n\t\tear.math.includeL,\n\t);\n\tconst result4 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineVert2,\n\t\tear.math.include,\n\t\tear.math.includeL,\n\t);\n\tconst result5 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineHoriz1,\n\t\tear.math.exclude,\n\t\tear.math.excludeL,\n\t);\n\tconst result6 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineHoriz2,\n\t\tear.math.exclude,\n\t\tear.math.excludeL,\n\t);\n\tconst result7 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineVert1,\n\t\tear.math.exclude,\n\t\tear.math.excludeL,\n\t);\n\tconst result8 = ear.math.clipLineConvexPolygon(\n\t\trect,\n\t\tlineVert2,\n\t\tear.math.exclude,\n\t\tear.math.excludeL,\n\t);\n\texpect(result1.length).toBe(2);\n\texpect(result2.length).toBe(2);\n\texpect(result3.length).toBe(2);\n\texpect(result4.length).toBe(2);\n\texpect(result5).toBe(undefined);\n\texpect(result6).toBe(undefined);\n\texpect(result7).toBe(undefined);\n\texpect(result8).toBe(undefined);\n\texpect(JSON.stringify(result1[0])).not.toBe(JSON.stringify(result1[1]));\n\texpect(JSON.stringify(result2[0])).not.toBe(JSON.stringify(result2[1]));\n\texpect(JSON.stringify(result3[0])).not.toBe(JSON.stringify(result3[1]));\n\texpect(JSON.stringify(result4[0])).not.toBe(JSON.stringify(result4[1]));\n});\n\ntest(\"vertex-incident line\", () => {\n\t// all cases will return undefined\n\tconst quad = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\tconst lineHoriz1 = { vector: [1, 0], origin: [-1, 1] };\n\tconst lineHoriz2 = { vector: [1, 0], origin: [-1, -1] };\n\tconst lineVert1 = { vector: [0, 1], origin: [-1, -1] };\n\tconst lineVert2 = { vector: [0, 1], origin: [1, -1] };\n\tconst results = [\n\t\tear.math.clipLineConvexPolygon(quad, lineHoriz1, ear.math.include, ear.math.includeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineHoriz2, ear.math.include, ear.math.includeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineVert1, ear.math.include, ear.math.includeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineVert2, ear.math.include, ear.math.includeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineHoriz1, ear.math.exclude, ear.math.excludeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineHoriz2, ear.math.exclude, ear.math.excludeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineVert1, ear.math.exclude, ear.math.excludeL),\n\t\tear.math.clipLineConvexPolygon(quad, lineVert2, ear.math.exclude, ear.math.excludeL),\n\t];\n\tresults.forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear core, segment\", () => {\n\tconst rect = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst segHoriz1 = { vector: [1, 0], origin: [0.5, 0] };\n\tconst segHoriz2 = { vector: [1, 0], origin: [-0.5, 0] };\n\tconst segVert1 = { vector: [0, 1], origin: [0, 0.5] };\n\tconst segVert2 = { vector: [0, 1], origin: [1, 0.5] };\n\tconst result1 = ear.math.clipLineConvexPolygon(rect, segHoriz1, ear.math.include, ear.math.includeS);\n\tconst result2 = ear.math.clipLineConvexPolygon(rect, segHoriz2, ear.math.include, ear.math.includeS);\n\tconst result3 = ear.math.clipLineConvexPolygon(rect, segVert1, ear.math.include, ear.math.includeS);\n\tconst result4 = ear.math.clipLineConvexPolygon(rect, segVert2, ear.math.include, ear.math.includeS);\n\tconst result5 = ear.math.clipLineConvexPolygon(rect, segHoriz1, ear.math.exclude, ear.math.excludeS);\n\tconst result6 = ear.math.clipLineConvexPolygon(rect, segHoriz2, ear.math.exclude, ear.math.excludeS);\n\tconst result7 = ear.math.clipLineConvexPolygon(rect, segVert1, ear.math.exclude, ear.math.excludeS);\n\tconst result8 = ear.math.clipLineConvexPolygon(rect, segVert2, ear.math.exclude, ear.math.excludeS);\n\texpect(result1.length).toBe(2);\n\texpect(result2.length).toBe(2);\n\texpect(result3.length).toBe(2);\n\texpect(result4.length).toBe(2);\n\texpect(result5).toBe(undefined);\n\texpect(result6).toBe(undefined);\n\texpect(result7).toBe(undefined);\n\texpect(result8).toBe(undefined);\n\texpect(JSON.stringify(result1[0])).not.toBe(JSON.stringify(result1[1]));\n\texpect(JSON.stringify(result2[0])).not.toBe(JSON.stringify(result2[1]));\n\texpect(JSON.stringify(result3[0])).not.toBe(JSON.stringify(result3[1]));\n\texpect(JSON.stringify(result4[0])).not.toBe(JSON.stringify(result4[1]));\n\texpect(result1[0][0]).toBe(0.5);\n\texpect(result1[0][1]).toBe(0);\n\texpect(result1[1][0]).toBe(1);\n\texpect(result1[1][1]).toBe(0);\n\texpect(result2[0][0]).toBe(0);\n\texpect(result2[0][1]).toBe(0);\n\texpect(result2[1][0]).toBe(0.5);\n\texpect(result2[1][1]).toBe(0);\n\t// remember these are VECTORS, ORIGIN\n\tconst segHoriz3 = { vector: [0.5, 0], origin: [0.25, 0] };\n\tconst segVert3 = { vector: [0, 2], origin: [0, -0.5] };\n\tconst result9 = ear.math.clipLineConvexPolygon(rect, segHoriz3, ear.math.include, ear.math.includeS);\n\tconst result10 = ear.math.clipLineConvexPolygon(rect, segVert3, ear.math.include, ear.math.includeS);\n\tconst result11 = ear.math.clipLineConvexPolygon(rect, segHoriz3, ear.math.exclude, ear.math.excludeS);\n\tconst result12 = ear.math.clipLineConvexPolygon(rect, segVert3, ear.math.exclude, ear.math.excludeS);\n\texpect(result9[0][0]).toBe(0.25);\n\texpect(result9[0][1]).toBe(0);\n\texpect(result9[1][0]).toBe(0.75);\n\texpect(result9[1][1]).toBe(0);\n\texpect(result10[0][0]).toBe(0);\n\texpect(result10[0][1]).toBe(0);\n\texpect(result10[1][0]).toBe(0);\n\texpect(result10[1][1]).toBe(1);\n});\n\ntest(\"vertex-incident segment\", () => {\n\t// all cases will return undefined\n\tconst quad = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\tconst horiz1 = { vector: [1, 0], origin: [-1, 1] };\n\tconst horiz2 = { vector: [1, 0], origin: [-1, -1] };\n\tconst vert1 = { vector: [0, 1], origin: [-1, -1] };\n\tconst vert2 = { vector: [0, 1], origin: [1, -1] };\n\tconst results = [\n\t\tear.math.clipLineConvexPolygon(quad, horiz1, ear.math.include, ear.math.includeS),\n\t\tear.math.clipLineConvexPolygon(quad, horiz2, ear.math.include, ear.math.includeS),\n\t\tear.math.clipLineConvexPolygon(quad, vert1, ear.math.include, ear.math.includeS),\n\t\tear.math.clipLineConvexPolygon(quad, vert2, ear.math.include, ear.math.includeS),\n\t\tear.math.clipLineConvexPolygon(quad, horiz1, ear.math.exclude, ear.math.excludeS),\n\t\tear.math.clipLineConvexPolygon(quad, horiz2, ear.math.exclude, ear.math.excludeS),\n\t\tear.math.clipLineConvexPolygon(quad, vert1, ear.math.exclude, ear.math.excludeS),\n\t\tear.math.clipLineConvexPolygon(quad, vert2, ear.math.exclude, ear.math.excludeS),\n\t];\n\tresults.forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear core, ray\", () => {\n\tconst rect = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst rayHoriz1 = { vector: [1, 0], origin: [0.5, 0] };\n\tconst rayHoriz2 = { vector: [1, 0], origin: [0.5, 1] };\n\tconst rayVert1 = { vector: [0, 1], origin: [0, 0.5] };\n\tconst rayVert2 = { vector: [0, 1], origin: [1, 0.5] };\n\tconst result1 = ear.math.clipLineConvexPolygon(rect, rayHoriz1, ear.math.include, ear.math.includeR);\n\tconst result2 = ear.math.clipLineConvexPolygon(rect, rayHoriz2, ear.math.include, ear.math.includeR);\n\tconst result3 = ear.math.clipLineConvexPolygon(rect, rayVert1, ear.math.include, ear.math.includeR);\n\tconst result4 = ear.math.clipLineConvexPolygon(rect, rayVert2, ear.math.include, ear.math.includeR);\n\tconst result5 = ear.math.clipLineConvexPolygon(rect, rayHoriz1, ear.math.exclude, ear.math.excludeR);\n\tconst result6 = ear.math.clipLineConvexPolygon(rect, rayHoriz2, ear.math.exclude, ear.math.excludeR);\n\tconst result7 = ear.math.clipLineConvexPolygon(rect, rayVert1, ear.math.exclude, ear.math.excludeR);\n\tconst result8 = ear.math.clipLineConvexPolygon(rect, rayVert2, ear.math.exclude, ear.math.excludeR);\n\texpect(result1.length).toBe(2);\n\texpect(result2.length).toBe(2);\n\texpect(result3.length).toBe(2);\n\texpect(result4.length).toBe(2);\n\texpect(result5).toBe(undefined);\n\texpect(result6).toBe(undefined);\n\texpect(result7).toBe(undefined);\n\texpect(result8).toBe(undefined);\n\texpect(JSON.stringify(result1[0])).not.toBe(JSON.stringify(result1[1]));\n\texpect(JSON.stringify(result2[0])).not.toBe(JSON.stringify(result2[1]));\n\texpect(JSON.stringify(result3[0])).not.toBe(JSON.stringify(result3[1]));\n\texpect(JSON.stringify(result4[0])).not.toBe(JSON.stringify(result4[1]));\n});\n\ntest(\"vertex-incident ray\", () => {\n\t// all cases will return undefined\n\tconst quad = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\tconst horiz1 = { vector: [1, 0], origin: [-1, 1] };\n\tconst horiz2 = { vector: [1, 0], origin: [-1, -1] };\n\tconst vert1 = { vector: [0, 1], origin: [-1, -1] };\n\tconst vert2 = { vector: [0, 1], origin: [1, -1] };\n\tconst results = [\n\t\tear.math.clipLineConvexPolygon(quad, horiz1, ear.math.include, ear.math.includeR),\n\t\tear.math.clipLineConvexPolygon(quad, horiz2, ear.math.include, ear.math.includeR),\n\t\tear.math.clipLineConvexPolygon(quad, vert1, ear.math.include, ear.math.includeR),\n\t\tear.math.clipLineConvexPolygon(quad, vert2, ear.math.include, ear.math.includeR),\n\t\tear.math.clipLineConvexPolygon(quad, horiz1, ear.math.exclude, ear.math.excludeR),\n\t\tear.math.clipLineConvexPolygon(quad, horiz2, ear.math.exclude, ear.math.excludeR),\n\t\tear.math.clipLineConvexPolygon(quad, vert1, ear.math.exclude, ear.math.excludeR),\n\t\tear.math.clipLineConvexPolygon(quad, vert2, ear.math.exclude, ear.math.excludeR),\n\t];\n\tresults.forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear core, segment\", () => {\n\tconst rect = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst segHoriz1 = [[1, 0], [0.5, 0]];\n\tconst segHoriz2 = [[1, 0], [0.5, 1]];\n\tconst segVert1 = [[0, 1], [0, 0.5]];\n\tconst segVert2 = [[0, 1], [1, 0.5]];\n\tconst result1 = clip_segment_in_convex_poly_exclusive(rect, ...segHoriz1);\n\tconst result2 = clip_segment_in_convex_poly_exclusive(rect, ...segHoriz2);\n\tconst result3 = clip_segment_in_convex_poly_exclusive(rect, ...segVert1);\n\tconst result4 = clip_segment_in_convex_poly_exclusive(rect, ...segVert2);\n});\n\ntest(\"collinear core, segment, spanning multiple points\", () => {\n\tconst poly = [\n\t\t[0, 0], [5, 0], [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [0, 5],\n\t];\n\tconst seg = [[5, -1], [5, 6]];\n\tconst res = ear.math.clipLineConvexPolygon(\n\t\tpoly,\n\t\t{ vector: ear.math.subtract(seg[1], seg[0]), origin: seg[0] },\n\t\tear.math.include,\n\t\tear.math.includeS,\n\t);\n\texpect(res[0][0]).toBe(5);\n\texpect(res[0][1]).toBe(0);\n\texpect(res[1][0]).toBe(5);\n\texpect(res[1][1]).toBe(5);\n});\n\ntest(\"collinear core, segment, spanning multiple points, inside\", () => {\n\tconst poly = [\n\t\t[0, 0], [5, 0], [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [0, 5],\n\t];\n\tconst seg = [[5, 0.5], [5, 4.5]];\n\tconst res = clip_segment_in_convex_poly_inclusive(poly, ...seg);\n\texpect(res[0][0]).toBe(5);\n\texpect(res[0][1]).toBe(0.5);\n\texpect(res[1][0]).toBe(5);\n\texpect(res[1][1]).toBe(4.5);\n});\n"
  },
  {
    "path": "tests/math.intersect.clip.polygon.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testEqualVectorVectors = function (a, b) {\n\texpect(a.length).toBe(b.length);\n\ta.forEach((_, i) => expect(ear.math.epsilonEqualVectors(a[i], b[i]))\n\t\t.toBe(true));\n};\n\ntest(\"clipPolygonPolygon edge adjacent non intersecting\", () => {\n\tconst poly1 = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst poly2 = [[1, 0], [2, 0], [2, 1], [1, 1]];\n\tconst res1 = ear.math.clipPolygonPolygon(poly1, poly2);\n\tconst res2 = ear.math.clipPolygonPolygon(poly1, poly2, 0.1);\n\tconst res3 = ear.math.clipPolygonPolygon(poly1, poly2, -0.1);\n\texpect(res1).toBe(undefined);\n\texpect(res2).toBe(undefined);\n\texpect(res3).not.toBe(undefined);\n});\n\ntest(\"clipPolygonPolygon overlapping collinear edges, axis-aligned\", () => {\n\tconst poly1 = [[0, 0], [2, 0], [2, 1], [0, 1]];\n\tconst poly2 = [[1, 0], [3, 0], [3, 1], [1, 1]];\n\tconst res1 = ear.math.clipPolygonPolygon(poly1, poly2);\n\ttestEqualVectorVectors(\n\t\tres1,\n\t\t[[1, 1], [1, 0], [2, 0], [2, 1]],\n\t);\n});\n\ntest(\"clipPolygonPolygon overlapping collinear edges, angled edges\", () => {\n\tconst poly1 = [[2, 0], [0, 2], [-2, 0], [0, -2]];\n\tconst poly2 = [[1, -1], [3, 1], [1, 3], [-1, 1]];\n\tconst res1 = ear.math.clipPolygonPolygon(poly1, poly2);\n\ttestEqualVectorVectors(\n\t\tres1,\n\t\t[[-1, 1], [1, -1], [2, 0], [0, 2]],\n\t);\n});\n\ntest(\"clipPolygonPolygon enclosing polygons\", () => {\n\tconst poly1 = [[0, 0], [10, 0], [10, 10], [0, 10]];\n\tconst poly2 = [[4, 4], [5, 4], [5, 5], [4, 5]];\n\tconst res1 = ear.math.clipPolygonPolygon(poly1, poly2);\n\tconst res2 = ear.math.clipPolygonPolygon(poly2, poly1);\n\ttestEqualVectorVectors([[4, 5], [4, 4], [5, 4], [5, 5]], res1);\n\ttestEqualVectorVectors([[4, 4], [5, 4], [5, 5], [4, 5]], res2);\n});\n\ntest(\"clipPolygonPolygon same vertex, edge on vertex\", () => {\n\t// all vertices exist on top of each other\n\tconst poly1 = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst poly2 = [[1, 0], [1, 1], [0, 1]];\n\tconst res1 = ear.math.clipPolygonPolygon(poly1, poly2);\n\ttestEqualVectorVectors(\n\t\tres1,\n\t\t[[0, 1], [1, 0], [1, 1]],\n\t);\n\n\tconst poly3 = [[3, -2], [3, 3], [-2, 3]];\n\tconst res2 = ear.math.clipPolygonPolygon(poly1, poly3);\n\ttestEqualVectorVectors(\n\t\tres2,\n\t\t[[1, 0], [1, 1], [0, 1]],\n\t);\n});\n\ntest(\"clipPolygonPolygon ensure input parameters did not modify\", () => {\n\tconst poly1 = [[2, 0], [0, 2], [-2, 0], [0, -2]];\n\tconst poly2 = [[1, -1], [3, 1], [1, 3], [-1, 1]];\n\tear.math.clipPolygonPolygon(poly1, poly2);\n\tear.math.clipPolygonPolygon(poly2, poly1);\n\ttestEqualVectorVectors(\n\t\tpoly1,\n\t\t[[2, 0], [0, 2], [-2, 0], [0, -2]],\n\t);\n\ttestEqualVectorVectors(\n\t\tpoly2,\n\t\t[[1, -1], [3, 1], [1, 3], [-1, 1]],\n\t);\n});\n"
  },
  {
    "path": "tests/math.intersect.encloses.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"enclosingBoundingBoxes fully enclosed\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [0.25, 0.25], max: [0.75, 0.75] };\n\texpect(ear.math.enclosingBoundingBoxes(box1, box2)).toBe(true);\n});\n\ntest(\"enclosingBoundingBoxes edge collinear\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [0, 0], max: [0.5, 0.5] };\n\texpect(ear.math.enclosingBoundingBoxes(box1, box2)).toBe(true);\n});\n\ntest(\"enclosingBoundingBoxes edge collinear epsilon\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [0, 0], max: [0.5, 0.5] };\n\texpect(ear.math.enclosingBoundingBoxes(box1, box2, -1e-4)).toBe(false);\n\texpect(ear.math.enclosingBoundingBoxes(box1, box2, 1e-4)).toBe(true);\n\tconst box3 = { min: [-1e-3, -1e-3], max: [0.5, 0.5] };\n\texpect(ear.math.enclosingBoundingBoxes(box1, box3, -1e-4)).toBe(false);\n\texpect(ear.math.enclosingBoundingBoxes(box1, box3, 1e-4)).toBe(false);\n\texpect(ear.math.enclosingBoundingBoxes(box1, box3, 1e-2)).toBe(true);\n});\n\n// enclosing polygon polygon is never used anywhere here or in\n// Rabbit Ear so it's no longer included in the build.\n// test(\"enclosingPolygonPolygon\", () => {\n// \tconst poly1 = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n// \tconst poly2 = [[10, 0], [0, 10], [-10, 0], [0, -10]];\n// \tconst poly3 = [[8, 8], [-8, 8], [-8, -8], [8, -8]];\n// \texpect(ear.math.enclosingPolygonPolygon(poly2, poly1)).toBe(true);\n// \texpect(ear.math.enclosingPolygonPolygon(poly3, poly1)).toBe(true);\n// \t// todo, this should be false i think\n// \t// expect(ear.math.enclosingPolygonPolygon(poly2, poly3)).toBe(false);\n// \texpect(ear.math.enclosingPolygonPolygon(poly1, poly2)).toBe(false);\n// \texpect(ear.math.enclosingPolygonPolygon(poly1, poly3)).toBe(false);\n// });\n"
  },
  {
    "path": "tests/math.intersect.method.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"intersection method has been removed\", () => expect(true).toBe(true));\n\n// test(\"intersections\", () => {\n// \tconst polygon = [[0, 1.15], [-1, -0.577], [1, -0.577]];\n// \tconst circle = { radius: 1, origin: [0, 0] };\n// \tconst line = { vector: [1, 2], origin: [0.5, 0] };\n// \tconst ray = { vector: [-1, 2], origin: [0.5, -0.1], domain: ear.math.excludeR };\n// \tconst segment = { vector: [4, 0], origin: [-2, 0.5], domain: ear.math.excludeS };\n\n// \tconst polygon2 = [[0, -1.15], [1, 0.577], [-1, 0.577]];\n// \tconst circle2 = { radius: 1, origin: [0.5, 0] };\n// \tconst line2 = { vector: [-1, 2], origin: [0.5, 0] };\n// \tconst ray2 = { vector: [1, 2], origin: [-0.5, 0], domain: ear.math.excludeR };\n// \tconst segment2 = { vector: [0, 4], origin: [0.5, -2], domain: ear.math.excludeS };\n\n// \t[\n// \t\tear.math.intersect(polygon, line),\n// \t\tear.math.intersect(polygon, ray),\n// \t\tear.math.intersect(polygon, segment),\n// \t\tear.math.intersect(circle, circle2),\n// \t\tear.math.intersect(circle, line),\n// \t\tear.math.intersect(circle, ray),\n// \t\tear.math.intersect(circle, segment),\n// \t\tear.math.intersect(line, polygon),\n// \t\tear.math.intersect(line, circle),\n// \t\tear.math.intersect(line, line2),\n// \t\tear.math.intersect(line, ray),\n// \t\tear.math.intersect(line, segment),\n// \t\tear.math.intersect(ray, polygon),\n// \t\tear.math.intersect(ray, circle),\n// \t\tear.math.intersect(ray, line),\n// \t\tear.math.intersect(ray, ray2),\n// \t\tear.math.intersect(ray, segment),\n// \t\tear.math.intersect(segment, polygon),\n// \t\tear.math.intersect(segment, circle),\n// \t\tear.math.intersect(segment, line),\n// \t\tear.math.intersect(segment, ray),\n// \t\tear.math.intersect(segment, segment2),\n// \t].forEach(intersect => expect(intersect).not.toBeUndefined());\n\n// \t// intersection between these types is not yet implemented\n// \t[\n// \t\tear.math.intersect(polygon, polygon2),\n// \t\tear.math.intersect(polygon, circle),\n// \t\tear.math.intersect(circle, polygon),\n// \t].forEach(intersect => expect(intersect).toBeUndefined());\n// });\n\n// test(\"collinear segment intersections, types not core\", () => {\n// \t// horizontal\n// \tconst seg01 = ear.math.pointsToLine([0, 2], [2, 2]);\n// \tconst seg02 = ear.math.pointsToLine([-1, 2], [10, 2]);\n// \tconst seg03 = ear.math.pointsToLine([0, 2], [2, 2]);\n// \tconst seg04 = ear.math.pointsToLine([10, 2], [-1, 2]);\n// \t// vertical\n// \tconst seg05 = ear.math.pointsToLine([2, 0], [2, 2]);\n// \tconst seg06 = ear.math.pointsToLine([2, -1], [2, 10]);\n// \tconst seg07 = ear.math.pointsToLine([2, 0], [2, 2]);\n// \tconst seg08 = ear.math.pointsToLine([2, 10], [2, -1]);\n// \t// diagonal\n// \tconst seg09 = ear.math.pointsToLine([0, 0], [2, 2]);\n// \tconst seg10 = ear.math.pointsToLine([-1, -1], [5, 5]);\n// \tconst seg11 = ear.math.pointsToLine([0, 0], [2, 2]);\n// \tconst seg12 = ear.math.pointsToLine([5, 5], [-1, -1]);\n// \t[seg01, seg02, seg03, seg04, seg05, seg06, seg07, seg08, seg09, seg10, seg11, seg12]\n// \t\t.forEach(seg => { seg.domain = ear.math.excludeS; });\n// \t[[seg01, seg02],\n// \t\t[seg03, seg04],\n// \t\t[seg05, seg06],\n// \t\t[seg07, seg08],\n// \t\t[seg09, seg10],\n// \t\t[seg11, seg12],\n// \t].map(pair => ear.math.intersect(...pair))\n// \t\t.forEach(res => expect(res.point).toBeUndefined());\n// });\n"
  },
  {
    "path": "tests/math.intersect.overlap.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"overlapBoundingBoxes, point overlap\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [0.9, 0.9], max: [2, 2] };\n\texpect(ear.math.overlapBoundingBoxes(box1, box2)).toBe(true);\n});\n\ntest(\"overlapBoundingBoxes, edge overlap\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [1, 0], max: [2, 1] };\n\texpect(ear.math.overlapBoundingBoxes(box1, box2)).toBe(true);\n});\n\ntest(\"overlapBoundingBoxes, point overlap, epsilon away\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [1 + 1e-2, 1 + 1e-2], max: [2, 2] };\n\texpect(ear.math.overlapBoundingBoxes(box1, box2)).toBe(false);\n});\n\ntest(\"overlapBoundingBoxes, edge overlap\", () => {\n\tconst box1 = { min: [0, 0], max: [1, 1] };\n\tconst box2 = { min: [1 + 1e-2, 0], max: [2, 1] };\n\texpect(ear.math.overlapBoundingBoxes(box1, box2)).toBe(false);\n});\n\n// test(\"overlap on member types\", () => {\n// \tconst polygon = ear.math.polygon([0, 1.15], [-1, -0.577], [1, -0.577]);\n// \tconst circle = ear.math.circle(1);\n// \tconst line = ear.math.line([1, 2], [0.5, 0]);\n// \tconst ray = ear.math.ray([-1, 2], [0.5, -0.1]);\n// \tconst segment = ear.math.segment([-2, 0.5], [2, 0.5]);\n// \tconst vector = ear.math.vector(0.75, 0.5);\n\n// \tconst polygon2 = ear.math.polygon([0, -1.15], [1, 0.577], [-1, 0.577]);\n// \tconst circle2 = ear.math.circle(1, [0.5, 0]);\n// \tconst line2 = ear.math.line([-1, 2], [0.5, 0]);\n// \tconst ray2 = ear.math.ray([1, 2], [-0.5, 0]);\n// \tconst segment2 = ear.math.segment([0.5, -2], [0.5, 2]);\n// \tconst vector2 = ear.math.vector(0, 1);\n// \tconst vector3 = ear.math.vector(0, 1, 0);\n\n// \t[\n// \t\tpolygon.overlap(polygon2),\n// \t\t// polygon.overlap(circle),\n// \t\t// polygon.overlap(line),\n// \t\t// polygon.overlap(ray),\n// \t\t// polygon.overlap(segment),\n// \t\tpolygon.overlap(vector2),\n// \t\t// circle.overlap(polygon),\n// \t\t// circle.overlap(circle2),\n// \t\t// circle.overlap(line),\n// \t\t// circle.overlap(ray),\n// \t\t// circle.overlap(segment),\n// \t\tcircle.overlap(vector),\n// \t\t// line.overlap(polygon),\n// \t\t// line.overlap(circle),\n// \t\tline.overlap(line2),\n// \t\tline.overlap(ray),\n// \t\tline.overlap(segment),\n// \t\tline.overlap(vector),\n// \t\t// ray.overlap(polygon),\n// \t\t// ray.overlap(circle),\n// \t\tray.overlap(line),\n// \t\tray.overlap(ray2),\n// \t\tray.overlap(segment),\n// \t\tray2.overlap(vector2),\n// \t\t// segment.overlap(polygon),\n// \t\t// segment.overlap(circle),\n// \t\tsegment.overlap(line),\n// \t\tsegment.overlap(ray),\n// \t\tsegment.overlap(segment2),\n// \t\tsegment.overlap(vector),\n// \t\tvector2.overlap(polygon),\n// \t\tvector.overlap(circle),\n// \t\tvector.overlap(line),\n// \t\tvector2.overlap(ray2),\n// \t\tvector.overlap(segment),\n// \t\tvector2.overlap(vector3),\n// \t].forEach(overlap => expect(overlap).toBe(true));\n// });\n\ntest(\"point on line, point at line origin\", () => {\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[0, 0],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[5, 5],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[Math.SQRT1_2, Math.SQRT1_2],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[0, 0],\n\t\tear.math.excludeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[5, 5],\n\t\tear.math.excludeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[Math.SQRT1_2, Math.SQRT1_2],\n\t\tear.math.excludeS,\n\t)).toBe(true);\n});\n\ntest(\"point on line\", () => {\n\texpect(ear.math.overlapLinePoint({ vector: [5, 5], origin: [0, 0] }, [2, 2])).toBe(true);\n\texpect(ear.math.overlapLinePoint({ vector: [1, 1], origin: [0, 0] }, [2, 2])).toBe(true);\n\texpect(ear.math.overlapLinePoint({ vector: [2, 2], origin: [0, 0] }, [2.1, 2.1])).toBe(true);\n\texpect(ear.math.overlapLinePoint({ vector: [2, 2], origin: [0, 0] }, [2.000000001, 2.000000001]))\n\t\t.toBe(true);\n\texpect(ear.math.overlapLinePoint({ vector: [2, 2], origin: [0, 0] }, [-1, -1])).toBe(true);\n\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeR,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeR,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [0, 0] },\n\t\t[2.1, 2.1],\n\t\tear.math.includeR,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [0, 0] },\n\t\t[2.000000001, 2.000000001],\n\t\tear.math.includeR,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [-1, -1], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeR,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[-0.1, -0.1],\n\t\tear.math.includeR,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[-0.000000001, -0.000000001],\n\t\tear.math.includeR,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[-0.000000001, -0.000000001],\n\t\tear.math.excludeR,\n\t)).toBe(false);\n\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [5, 5], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [1, 1], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [0, 0] },\n\t\t[2.1, 2.1],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [0, 0] },\n\t\t[2.000000001, 2.000000001],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [-1, -1], origin: [0, 0] },\n\t\t[2, 2],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [0, 0] },\n\t\t[2.000000001, 2.000000001],\n\t\tear.math.excludeS,\n\t)).toBe(false);\n});\n\ntest(\"overlap.point_on_segment_inclusive\", () => {\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[4, 3],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[3, 3],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[2.9, 3],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[2.9999999999, 3],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[6.1, 3],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [3, 0], origin: [3, 3] },\n\t\t[6.0000000001, 3],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [2, 2] },\n\t\t[3.5, 3.5],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [2, 2] },\n\t\t[2.9, 3.1],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [2, 2], origin: [2, 2] },\n\t\t[2.99999999, 3.000000001],\n\t\tear.math.includeS,\n\t)).toBe(true);\n\t// degenerate edge returns false\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [0, 0], origin: [2, 2] },\n\t\t[2, 2],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [0, 0], origin: [2, 2] },\n\t\t[2.1, 2.1],\n\t\tear.math.includeS,\n\t)).toBe(false);\n\texpect(ear.math.overlapLinePoint(\n\t\t{ vector: [0, 0], origin: [2, 2] },\n\t\t[2.000000001, 2.00000001],\n\t\tear.math.includeS,\n\t)).toBe(false);\n});\n\ntest(\"point on line epsilon\", () => {\n\n});\n\nconst overlapMethod = (...args) => (\n\tear.math.overlapConvexPolygonPoint(...args).overlap\n);\n\ntest(\"point in poly\", () => {\n\tconst poly = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\texpect(overlapMethod(poly, [0.0, 0.0])).toBe(true);\n\texpect(overlapMethod(poly, [0.999, 0.0])).toBe(true);\n\texpect(overlapMethod(poly, [0.9999999999, 0.0])).toBe(false);\n\t// edge collinear\n\texpect(overlapMethod(poly, [0.5, 0.5])).toBe(false);\n\texpect(overlapMethod(poly, [0.49, 0.49])).toBe(true);\n\texpect(overlapMethod(poly, [0.51, 0.51])).toBe(false);\n\texpect(overlapMethod(poly, [0.500000001, 0.500000001])).toBe(false);\n\texpect(overlapMethod(poly, [0.5, -0.5])).toBe(false);\n\t// expect(overlapMethod(poly, [-0.5, 0.5])).toBe(false);\n\t// expect(overlapMethod(poly, [-0.5, -0.5])).toBe(false);\n\t// polygon points\n\texpect(overlapMethod(poly, [1.0, 0.0])).toBe(false);\n\texpect(overlapMethod(poly, [0.0, 1.0])).toBe(false);\n\t// expect(overlapMethod(poly, [-1.0, 0.0])).toBe(false);\n\texpect(overlapMethod(poly, [0.0, -1.0])).toBe(false);\n});\n\ntest(\"convex point in poly inclusive\", () => {\n\tconst poly = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\texpect(overlapMethod(poly, [0.0, 0.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.999, 0.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.9999999999, 0.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [1.1, 0.0], ear.math.include))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [1.000000001, 0.0], ear.math.include))\n\t\t.toBe(true);\n\t// edge collinear\n\texpect(overlapMethod(poly, [0.5, 0.5], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.49, 0.49], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.499999999, 0.499999999], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.51, 0.51], ear.math.include))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.500000001, 0.500000001], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.5, -0.5], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [-0.5, 0.5], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [-0.5, -0.5], ear.math.include))\n\t\t.toBe(true);\n\t// polygon points\n\texpect(overlapMethod(poly, [1.0, 0.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.0, 1.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [-1.0, 0.0], ear.math.include))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.0, -1.0], ear.math.include))\n\t\t.toBe(true);\n});\n\ntest(\"convex point in poly exclusive\", () => {\n\tconst poly = [[1, 0], [0, 1], [-1, 0], [0, -1]];\n\texpect(overlapMethod(poly, [0.0, 0.0], ear.math.exclude))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.999, 0.0], ear.math.exclude))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.9999999999, 0.0], ear.math.exclude))\n\t\t.toBe(false);\n\t// edge collinear\n\texpect(overlapMethod(poly, [0.5, 0.5], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.49, 0.49], ear.math.exclude))\n\t\t.toBe(true);\n\texpect(overlapMethod(poly, [0.499999999, 0.499999999], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.51, 0.51], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.5, -0.5], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [-0.5, 0.5], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [-0.5, -0.5], ear.math.exclude))\n\t\t.toBe(false);\n\t// polygon points\n\texpect(overlapMethod(poly, [1.0, 0.0], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.0, 1.0], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [-1.0, 0.0], ear.math.exclude))\n\t\t.toBe(false);\n\texpect(overlapMethod(poly, [0.0, -1.0], ear.math.exclude))\n\t\t.toBe(false);\n});\n"
  },
  {
    "path": "tests/math.intersect.polyLine.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"intersectPolygonLine, edge collinear\", () => {\n\t// all cases will have length of 2\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst lineHoriz1 = { vector: [1, 0], origin: [0, 0] };\n\tconst lineHoriz2 = { vector: [1, 0], origin: [1, 1] };\n\tconst lineVert1 = { vector: [0, 1], origin: [0, 0] };\n\tconst lineVert2 = { vector: [0, 1], origin: [1, 1] };\n\tconst results = [\n\t\tear.math.intersectPolygonLine(square, lineHoriz1, ear.math.includeL),\n\t\tear.math.intersectPolygonLine(square, lineHoriz2, ear.math.includeL),\n\t\tear.math.intersectPolygonLine(square, lineVert1, ear.math.includeL),\n\t\tear.math.intersectPolygonLine(square, lineVert2, ear.math.includeL),\n\t];\n\tresults.forEach(res => expect(res).toHaveLength(2));\n});\n\ntest(\"intersectPolygonLine, through two vertices\", () => {\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst lineDiag1 = { vector: [1, 1], origin: [0, 0] };\n\tconst lineDiag2 = { vector: [-1, 1], origin: [1, 0] };\n\texpect(ear.math.intersectPolygonLine(square, lineDiag1, ear.math.includeL))\n\t\t.toHaveLength(2);\n\texpect(ear.math.intersectPolygonLine(square, lineDiag2, ear.math.includeL))\n\t\t.toHaveLength(2);\n\n\texpect(ear.math.intersectPolygonLine(square, lineDiag1, ear.math.includeS))\n\t\t.toHaveLength(2);\n\texpect(ear.math.intersectPolygonLine(square, lineDiag2, ear.math.includeS))\n\t\t.toHaveLength(2);\n\n\texpect(ear.math.intersectPolygonLine(square, lineDiag1, ear.math.excludeS))\n\t\t.toHaveLength(0);\n\texpect(ear.math.intersectPolygonLine(square, lineDiag2, ear.math.excludeS))\n\t\t.toHaveLength(0);\n});\n"
  },
  {
    "path": "tests/math.intersect.split.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"deprecated\", () => expect(true).toBe(true));\n\n// test(\"splitConvexPolygon\", () => {\n// \tconst rect_counter = [\n// \t\t[-1, -1],\n// \t\t[+1, -1],\n// \t\t[+1, +1],\n// \t\t[-1, +1],\n// \t];\n// \tconst rect_clock = [\n// \t\t[-1, -1],\n// \t\t[-1, +1],\n// \t\t[+1, +1],\n// \t\t[+1, -1],\n// \t];\n// \tconst res0 = ear.math.splitConvexPolygon(rect_counter, { vector: [1, 2], origin: [0, 0] });\n// \t[[-1, 1], [-1, -1], [-0.5, -1], [0.5, 1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[0][i]));\n// \t});\n// \t[[1, -1], [1, 1], [0.5, 1], [-0.5, -1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[1][i]));\n// \t});\n// });\n\n// test(\"splitConvexPolygon no overlap\", () => {\n// \tconst rect_counter = [\n// \t\t[-1, -1],\n// \t\t[+1, -1],\n// \t\t[+1, +1],\n// \t\t[-1, +1],\n// \t];\n// \tconst result = ear.math.splitConvexPolygon(rect_counter, { vector: [1, 2], origin: [10, 0] });\n// \trect_counter.forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(result[0][i]));\n// \t});\n// });\n\n// test(\"splitConvexPolygon vertex collinear\", () => {\n// \tconst rect_counter = [\n// \t\t[-1, -1],\n// \t\t[+1, -1],\n// \t\t[+1, +1],\n// \t\t[-1, +1],\n// \t];\n// \tconst res0 = ear.math.splitConvexPolygon(rect_counter, { vector: [1, 1], origin: [0, 0] });\n// \t[[1, 1], [-1, 1], [-1, -1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[0][i]));\n// \t});\n// \t[[-1, -1], [1, -1], [1, 1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[1][i]));\n// \t});\n// });\n\n// test(\"splitConvexPolygon 1 edge and 1 vertex collinear\", () => {\n// \tconst rect_counter = [\n// \t\t[-1, -1],\n// \t\t[+1, -1],\n// \t\t[+1, +1],\n// \t\t[-1, +1],\n// \t];\n// \tconst res0 = ear.math.splitConvexPolygon(rect_counter, { vector: [1, 2], origin: [-1, -1] });\n// \t[[-1, 1], [-1, -1], [0, 1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[0][i]));\n// \t});\n// \t[[1, -1], [1, 1], [0, 1], [-1, -1]].forEach((expected, i) => {\n// \t\texpect(JSON.stringify(expected)).toBe(JSON.stringify(res0[1][i]));\n// \t});\n// });\n"
  },
  {
    "path": "tests/math.intersect.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"intersectLineLine include exclude\", () => {\n\tconst res0 = ear.math.intersectLineLine(\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t\t{ vector: [1, 0], origin: [0, 1] },\n\t).point;\n\tconst res1 = ear.math.intersectLineLine(\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t\t{ vector: [1, 0], origin: [0, 1] },\n\t\tear.math.includeS,\n\t\tear.math.includeS,\n\t).point;\n\tconst res2 = ear.math.intersectLineLine(\n\t\t{ vector: [0, 1], origin: [1, 0] },\n\t\t{ vector: [1, 0], origin: [0, 1] },\n\t\tear.math.excludeS,\n\t\tear.math.excludeS,\n\t).point;\n\texpect(res0).not.toBe(undefined);\n\texpect(res1).not.toBe(undefined);\n\texpect(res2).toBe(undefined);\n});\n\ntest(\"collinear line intersections\", () => {\n\tconst intersect = (a, b, c, d, ...args) => ear.math.intersectLineLine(\n\t\t{ vector: a, origin: b },\n\t\t{ vector: c, origin: d },\n\t\t...args,\n\t).point;\n\t[\n\t\t// INCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeL, ear.math.includeL),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.includeL, ear.math.includeL),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeL, ear.math.includeL),\n\t\t// INCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.includeL, ear.math.includeL),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.includeL, ear.math.includeL),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.includeL, ear.math.includeL),\n\t\t// INCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.includeL, ear.math.includeL),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.includeL, ear.math.includeL),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.includeL, ear.math.includeL),\n\t\t// EXCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeL, ear.math.excludeL),\n\t\t// EXCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.excludeL, ear.math.excludeL),\n\t\t// EXCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.excludeL, ear.math.excludeL),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.excludeL, ear.math.excludeL),\n\t].forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear ray intersections\", () => {\n\tconst intersect = (a, b, c, d, ...args) => ear.math.intersectLineLine(\n\t\t{ vector: a, origin: b },\n\t\t{ vector: c, origin: d },\n\t\t...args,\n\t).point;\n\t[\n\t\t// INCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeR, ear.math.includeR),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.includeR, ear.math.includeR),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeR, ear.math.includeR),\n\t\t// INCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.includeR, ear.math.includeR),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.includeR, ear.math.includeR),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.includeR, ear.math.includeR),\n\t\t// INCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.includeR, ear.math.includeR),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.includeR, ear.math.includeR),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.includeR, ear.math.includeR),\n\t\t// EXCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeR, ear.math.excludeR),\n\t\t// EXCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.excludeR, ear.math.excludeR),\n\t\t// EXCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.excludeR, ear.math.excludeR),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.excludeR, ear.math.excludeR),\n\t].forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear segment intersections\", () => {\n\tconst intersect = (a, b, c, d, ...args) => ear.math.intersectLineLine(\n\t\t{ vector: a, origin: b },\n\t\t{ vector: c, origin: d },\n\t\t...args,\n\t).point;\n\t[\n\t\t// INCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeS, ear.math.includeS),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.includeS, ear.math.includeS),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.includeS, ear.math.includeS),\n\t\t// INCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.includeS, ear.math.includeS),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.includeS, ear.math.includeS),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.includeS, ear.math.includeS),\n\t\t// INCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.includeS, ear.math.includeS),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.includeS, ear.math.includeS),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.includeS, ear.math.includeS),\n\t\t// EXCLUDE horizontal\n\t\tintersect([1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([1, 0], [2, 2], [-1, 0], [-1, 2], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([-1, 0], [2, 2], [1, 0], [-1, 2], ear.math.excludeS, ear.math.excludeS),\n\t\t// EXCLUDE vertical\n\t\tintersect([0, 1], [3, 0], [0, 1], [3, 3], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([0, 1], [3, 0], [0, -1], [3, 3], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([0, -1], [3, 0], [0, 1], [3, 3], ear.math.excludeS, ear.math.excludeS),\n\t\t// EXCLUDE diagonal\n\t\tintersect([1, 1], [2, 2], [1, 1], [-1, -1], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([-1, -1], [2, 2], [1, 1], [-1, -1], ear.math.excludeS, ear.math.excludeS),\n\t\tintersect([1, 1], [2, 2], [-1, -1], [-1, -1], ear.math.excludeS, ear.math.excludeS),\n\t].forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"collinear segment intersections, types not core\", () => {\n\tconst intersect = (a, b) => ear.math.intersectLineLine(a, b).point;\n\t[\n\t\t// horizontal\n\t\tintersect(\n\t\t\tear.math.pointsToLine([0, 2], [2, 2]),\n\t\t\tear.math.pointsToLine([-1, 2], [10, 2]),\n\t\t),\n\t\tintersect(\n\t\t\tear.math.pointsToLine([0, 2], [2, 2]),\n\t\t\tear.math.pointsToLine([10, 2], [-1, 2]),\n\t\t),\n\t\t// vertical\n\t\tintersect(\n\t\t\tear.math.pointsToLine([2, 0], [2, 2]),\n\t\t\tear.math.pointsToLine([2, -1], [2, 10]),\n\t\t),\n\t\tintersect(\n\t\t\tear.math.pointsToLine([2, 0], [2, 2]),\n\t\t\tear.math.pointsToLine([2, 10], [2, -1]),\n\t\t),\n\t\t// diagonal\n\t\tintersect(\n\t\t\tear.math.pointsToLine([0, 0], [2, 2]),\n\t\t\tear.math.pointsToLine([-1, -1], [5, 5]),\n\t\t),\n\t\tintersect(\n\t\t\tear.math.pointsToLine([0, 0], [2, 2]),\n\t\t\tear.math.pointsToLine([5, 5], [-1, -1]),\n\t\t),\n\t].forEach(res => expect(res).toBe(undefined));\n});\n\ntest(\"clip polygon polygon, same polygon\", () => {\n\t// all of the \"b\" cases are flipped clockwise and should return no solution\n\t// same polygon\n\tconst res1 = ear.math.clipPolygonPolygon(\n\t\t[[60, 10], [50, 50], [20, 20]],\n\t\t[[50, 50], [20, 20], [60, 10]],\n\t);\n\texpect(res1.length).toBe(3);\n\n\tconst res2 = ear.math.clipPolygonPolygon(\n\t\t[[50, 50], [25, 25], [50, 0]],\n\t\t[[50, 50], [25, 25], [50, 0]],\n\t);\n\texpect(res2.length).toBe(3);\n\n\tconst res2b = ear.math.clipPolygonPolygon(\n\t\t[[50, 0], [25, 25], [50, 50]],\n\t\t[[50, 0], [25, 25], [50, 50]],\n\t);\n\texpect(res2b).toBe(undefined);\n\n\t// same polygon, array rotated\n\tconst res3 = ear.math.clipPolygonPolygon(\n\t\t[[50, 50], [25, 25], [50, 0]],\n\t\t[[25, 25], [50, 0], [50, 50]],\n\t);\n\texpect(res3.length).toBe(3);\n\n\tconst res3b = ear.math.clipPolygonPolygon(\n\t\t[[50, 0], [25, 25], [50, 50]],\n\t\t[[50, 50], [50, 0], [25, 25]],\n\t);\n\texpect(res3b).toBe(undefined);\n});\n\ntest(\"polygon polygon, edge aligned\", () => {\n\t// edge aligned\n\n\tconst poly3 = [[40, 40], [100, 40], [80, 80]];\n\tconst poly4 = [[100, 40], [40, 40], [80, 0]];\n\tconst res2 = ear.math.clipPolygonPolygon(poly3, poly4);\n\texpect(res2).toBe(undefined);\n\n\tconst poly5 = [[40, 40], [100, 40], [80, 80]];\n\tconst poly6 = [[90, 40], [50, 40], [80, 0]];\n\tconst res3 = ear.math.clipPolygonPolygon(poly5, poly6);\n\texpect(res3).toBe(undefined);\n\n\tconst poly7 = [[40, 40], [100, 40], [80, 80]];\n\tconst poly8 = [[200, 40], [50, 40], [80, 0]];\n\tconst res4 = ear.math.clipPolygonPolygon(poly7, poly8);\n\texpect(res4).toBe(undefined);\n\n\tconst poly9 = [[40, 40], [100, 40], [80, 80]];\n\tconst poly10 = [[200, 40], [20, 40], [80, 0]];\n\tconst res5 = ear.math.clipPolygonPolygon(poly9, poly10);\n\texpect(res5).toBe(undefined);\n});\n\ntest(\"polygon polygon, epsilon\", () => {\n\t// now with epsilon\n\tconst ep = 1e-10;\n\tconst poly11 = [[40, 40 - ep], [100, 40 - ep], [80, 80]];\n\tconst poly12 = [[100, 40], [40, 40], [80, 0]];\n\tconst res6 = ear.math.clipPolygonPolygon(poly11, poly12);\n\texpect(res6).toBe(undefined);\n\tconst res7 = ear.math.clipPolygonPolygon(poly12, poly11);\n\texpect(res7).toBe(undefined);\n\n\tconst poly13 = [[60, 10], [50, 50], [20, 20]];\n\tconst poly14 = [[50 + ep, 50 + ep], [20, 20], [60, 10]];\n\tconst res8 = ear.math.clipPolygonPolygon(poly13, poly14);\n\texpect(res8.length).toBe(3);\n\tconst res9 = ear.math.clipPolygonPolygon(poly14, poly13);\n\texpect(res9.length).toBe(3);\n\n\tconst poly15 = [[60, 10], [50, 50], [20, 20]];\n\tconst poly16 = [[50 - ep, 50 - ep], [20, 20], [60, 10]];\n\tconst res10 = ear.math.clipPolygonPolygon(poly15, poly16);\n\texpect(res10.length).toBe(3);\n\tconst res11 = ear.math.clipPolygonPolygon(poly16, poly15);\n\texpect(res11.length).toBe(3);\n});\n\ntest(\"polygon polygon collinear edge\", () => {\n\t// these two polygons overlap and have 2 overlapping edges\n\tconst poly1clock = [[0, 0], [-1, 1], [0, 2], [2, 0]];\n\tconst poly2clock = [[0, 2], [1, 1], [1, -1], [-1, 1]];\n\tconst poly1counter = poly1clock.slice().reverse();\n\tconst poly2counter = poly2clock.slice().reverse();\n\n\t// the only one guaranteed to work\n\texpect(ear.math.clipPolygonPolygon(poly1counter, poly2counter)).not.toBeUndefined();\n\texpect(ear.math.clipPolygonPolygon(poly2counter, poly1counter)).not.toBeUndefined();\n\n\t// all of these have undefined behavior\n\texpect(ear.math.clipPolygonPolygon(poly1clock, poly2clock)).toBeUndefined();\n\texpect(ear.math.clipPolygonPolygon(poly2clock, poly1clock)).toBeUndefined();\n\n\texpect(ear.math.clipPolygonPolygon(poly1clock, poly2counter)).not.toBeUndefined();\n\texpect(ear.math.clipPolygonPolygon(poly2counter, poly1clock)).toBeUndefined();\n\n\texpect(ear.math.clipPolygonPolygon(poly1counter, poly2clock)).toBeUndefined();\n\texpect(ear.math.clipPolygonPolygon(poly2clock, poly1counter)).not.toBeUndefined();\n});\n\ntest(\"intersect lines\", () => {\n\tconst clipLine = ear.math.intersectCircleLine(\n\t\t{ radius: 1, origin: [0, 0] },\n\t\t{ vector: [0, 1], origin: [0.5, 0] },\n\t);\n\tconst shouldBeLine = [[0.5, -Math.sqrt(3) / 2], [0.5, Math.sqrt(3) / 2]];\n\tear.math.epsilonEqualVectors(clipLine[0], shouldBeLine[0]);\n\tear.math.epsilonEqualVectors(clipLine[1], shouldBeLine[1]);\n\t// no intersect\n\texpect(ear.math.intersectCircleLine(\n\t\t{ radius: 1, origin: [2, 2] },\n\t\t{ vector: [0, 1], origin: [10, 0] },\n\t)).toBe(undefined);\n\t// tangent\n\tconst tangent = ear.math.intersectCircleLine(\n\t\t{ radius: 1, origin: [2, 0] },\n\t\t{ vector: [0, 1], origin: [3, 0] },\n\t);\n\texpect(tangent[0][0]).toBe(3);\n\texpect(tangent[0][1]).toBe(0);\n\n\tconst shouldBeRay = [Math.SQRT1_2, Math.SQRT1_2];\n\tconst clipRay = ear.math.intersectCircleLine(\n\t\t{ radius: 1, origin: [0, 0] },\n\t\t{ vector: [0.1, 0.1], origin: [0, 0] },\n\t\tear.math.include,\n\t\tear.math.includeR,\n\t);\n\tear.math.epsilonEqualVectors(shouldBeRay, clipRay[0]);\n\n\tconst shouldBeSeg = [Math.SQRT1_2, Math.SQRT1_2];\n\tconst clipSeg = ear.math.intersectCircleLine(\n\t\t{ radius: 1, origin: [0, 0] },\n\t\t{ vector: [10, 10], origin: [0, 0] },\n\t\tear.math.include,\n\t\tear.math.includeS,\n\t);\n\tear.math.epsilonEqualVectors(shouldBeSeg, clipSeg[0]);\n});\n"
  },
  {
    "path": "tests/math.overlap.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"overlapConvexPolygonPoint bad inputs\", () => {\n\t// invalid point\n\texpect(ear.math.overlapConvexPolygonPoint(\n\t\t[[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\t[],\n\t\tear.math.exclude,\n\t).overlap).toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint counter-clockwise point on boundary\", () => {\n\t// counter-clockwise\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint clockwise point on boundary\", () => {\n\t// clockwise\n\tconst square = [[0, 0], [0, 1], [1, 1], [1, 0]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint counter-clockwise point on vertex\", () => {\n\t// counter-clockwise\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint clockwise point on vertex\", () => {\n\t// clockwise\n\tconst square = [[0, 0], [0, 1], [1, 1], [1, 0]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 1], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 0], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [1, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0, 1], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint counter-clockwise point inside\", () => {\n\t// counter-clockwise\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint clockwise point inside\", () => {\n\t// clockwise\n\tconst square = [[0, 0], [0, 1], [1, 1], [1, 0]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(true);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 0.5], ear.math.include).overlap)\n\t\t.toBe(true);\n});\n\ntest(\"overlapConvexPolygonPoint counter-clockwise point outside\", () => {\n\t// counter-clockwise\n\tconst square = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [2, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 2], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [-2, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, -2], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [2, 0.5], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 2], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [-2, 0.5], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, -2], ear.math.include).overlap)\n\t\t.toBe(false);\n});\n\ntest(\"overlapConvexPolygonPoint clockwise point outside\", () => {\n\t// clockwise\n\tconst square = [[0, 0], [0, 1], [1, 1], [1, 0]];\n\texpect(ear.math.overlapConvexPolygonPoint(square, [2, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 2], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [-2, 0.5], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, -2], ear.math.exclude).overlap)\n\t\t.toBe(false);\n\n\texpect(ear.math.overlapConvexPolygonPoint(square, [2, 0.5], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, 2], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [-2, 0.5], ear.math.include).overlap)\n\t\t.toBe(false);\n\texpect(ear.math.overlapConvexPolygonPoint(square, [0.5, -2], ear.math.include).overlap)\n\t\t.toBe(false);\n});\n\ntest(\"overlapConvexPolygons\", () => {\n\tconst square1 = [[0, 0], [1, 0], [1, 1], [0, 1]];\n\tconst square2 = [[1, 0], [2, 0], [2, 1], [1, 1]];\n\t// nudge squares by an amount larger than the epsilon\n\tconst square2Plus = [[1, 0], [2, 0], [2, 1], [1, 1]].map(p => [p[0] + 1e-2, p[1]]);\n\tconst square2Minus = [[1, 0], [2, 0], [2, 1], [1, 1]].map(p => [p[0] - 1e-2, p[1]]);\n\n\texpect(ear.math.overlapConvexPolygons(square1, square2)).toBe(false);\n\texpect(ear.math.overlapConvexPolygons(square2, square1)).toBe(false);\n\n\texpect(ear.math.overlapConvexPolygons(square1, square2Plus)).toBe(false);\n\texpect(ear.math.overlapConvexPolygons(square2Plus, square1)).toBe(false);\n\n\texpect(ear.math.overlapConvexPolygons(square1, square2Minus)).toBe(true);\n\texpect(ear.math.overlapConvexPolygons(square2Minus, square1)).toBe(true);\n\n\t// increase epsilon until they overlap again\n\t// this is not possible, you cannot adjust the epsilon to convert a\n\t// non-overlapping to overlapping or visa-versa.\n\t// expect(ear.math.overlapConvexPolygons(square1, square2Minus), 1e-3).toBe(false);\n\t// expect(ear.math.overlapConvexPolygons(square2Minus, square1), 1e-3).toBe(false);\n});\n"
  },
  {
    "path": "tests/math.primitives.circle.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"arguments\", () => {\n// \texpect(ear.math.circle(1, [4, 5]).radius).toBe(1);\n// \texpect(ear.math.circle(1, [4, 5]).origin.x).toBe(4);\n// \texpect(ear.math.circle(1, [4, 5]).origin.y).toBe(5);\n// \texpect(ear.math.circle([4, 5], 1).radius).toBe(1);\n// \texpect(ear.math.circle([4, 5], 1).origin.x).toBe(4);\n// \texpect(ear.math.circle([4, 5], 1).origin.y).toBe(5);\n// \texpect(ear.math.circle(1, 2).radius).toBe(2);\n// \texpect(ear.math.circle(1, 2).origin.x).toBe(1);\n// \texpect(ear.math.circle(1, 2, 3).radius).toBe(3);\n// \texpect(ear.math.circle(1, 2, 3).origin.x).toBe(1);\n// \texpect(ear.math.circle(1, 2, 3).origin.y).toBe(2);\n// \texpect(ear.math.circle(1, 2, 3, 4).radius).toBe(4);\n// \texpect(ear.math.circle(1, 2, 3, 4).origin.x).toBe(1);\n// \texpect(ear.math.circle(1, 2, 3, 4).origin.y).toBe(2);\n// \texpect(ear.math.circle(1, 2, 3, 4).origin.z).toBe(3);\n// \texpect(ear.math.circle([1, 2], [3, 4]).radius).toBe(ear.math.distance2([1, 2], [3, 4]));\n// \texpect(ear.math.circle([1, 2], [3, 4]).origin.x).toBe(1);\n// \texpect(ear.math.circle([1, 2], [3, 4]).origin.y).toBe(2);\n// \t// expect(ear.math.circle([1,2], [3,4], [5,6]) circumcenter between 3 points\n// \t// expect(ear.math.circle([1,2], [3,4], [5,6]) circumcenter between 3 points\n// });\n\n// test(\"x, y\", () => {\n// \tconst result = ear.math.circle(1, [2, 3]);\n// \texpect(result.x).toBe(2);\n// \texpect(result.y).toBe(3);\n// \texpect(result.z).toBe(undefined);\n\n// \tconst result1 = ear.math.circle(1);\n// \texpect(result1.x).toBe(0);\n// \texpect(result1.y).toBe(0);\n// \texpect(result1.z).toBe(0);\n// });\n\n// test(\"circle nearest point\", () => {\n// \tconst result1 = ear.math.circle(1).nearestPoint([5, 0]);\n// \texpect(result1.x).toBeCloseTo(1);\n// \texpect(result1.y).toBeCloseTo(0);\n// \tconst result2 = ear.math.circle(2, [4, 4]).nearestPoint([0, 0]);\n// \texpect(result2.x).toBeCloseTo(4 - Math.sqrt(2));\n// \texpect(result2.y).toBeCloseTo(4 - Math.sqrt(2));\n// });\n\n// test(\"points\", () => {\n// \tconst result = ear.math.circle(1, [1, 2]).points();\n// \texpect(result.length).toBe(128);\n// \texpect(result[0][0]).toBeCloseTo(2);\n// \texpect(result[0][1]).toBeCloseTo(2);\n// });\n\n// test(\"points param\", () => {\n// \tconst result1 = ear.math.circle(1).points(64);\n// \texpect(result1.length).toBe(64);\n// \tconst result2 = ear.math.circle(1).points(1);\n// \texpect(result2.length).toBe(1);\n// \tconst result3 = ear.math.circle(1).points(3);\n// \texpect(result3.length).toBe(3);\n// });\n\n// test(\"polygon\", () => {\n// \tconst result = ear.math.circle(1, [1, 2]).polygon();\n// \texpect(result.points.length).toBe(128);\n// \texpect(result.points[0][0]).toBeCloseTo(2);\n// \texpect(result.points[0][1]).toBeCloseTo(2);\n// });\n\n// test(\"segments\", () => {\n// \tconst result = ear.math.circle(1, [1, 2]).segments();\n// \texpect(result.length).toBe(128);\n// \texpect(result[0][0][0]).toBeCloseTo(2);\n// \texpect(result[0][0][1]).toBeCloseTo(2);\n// });\n\n// test(\"circle fromPoints\", () => {\n// \tconst result1 = ear.math.circle.fromPoints([1, 2], [0, 3], [-1, 2]);\n// \texpect(result1.radius).toBeCloseTo(1);\n// \texpect(result1.origin.x).toBeCloseTo(0);\n// \texpect(result1.origin.y).toBeCloseTo(2);\n\n// \tconst result2 = ear.math.circle.fromPoints([1, 2], [0, 3]);\n// \texpect(result2.radius).toBeCloseTo(Math.sqrt(2));\n// \texpect(result2.origin.x).toBeCloseTo(1);\n// \texpect(result2.origin.y).toBeCloseTo(2);\n// });\n\n// test(\"circle fromThreePoints\", () => {\n// \tconst result = ear.math.circle.fromThreePoints([1, 2], [0, 3], [-1, 2]);\n// \texpect(result.origin.x).toBeCloseTo(0);\n// \texpect(result.origin.y).toBeCloseTo(2);\n// });\n\n// test(\"intersect lines\", () => {\n// \tconst clipLine = ear.math.circle(1).intersect(ear.math.line([0, 1], [0.5, 0]));\n// \tconst shouldBeLine = [[0.5, -Math.sqrt(3) / 2], [0.5, Math.sqrt(3) / 2]];\n// \tear.math.epsilonEqualVectors(clipLine[0], shouldBeLine[0]);\n// \tear.math.epsilonEqualVectors(clipLine[1], shouldBeLine[1]);\n// \t// no intersect\n// \texpect(ear.math.circle(1, [2, 2]).intersect(ear.math.line([0, 1], [10, 0]))).toBe(undefined);\n// \t// tangent\n// \tconst tangent = ear.math.circle(1, [2, 0]).intersect(ear.math.line([0, 1], [3, 0]));\n// \texpect(tangent[0][0]).toBe(3);\n// \texpect(tangent[0][1]).toBe(0);\n\n// \tconst shouldBeRay = [Math.sqrt(2) / 2, Math.sqrt(2) / 2];\n// \tconst clipRay = ear.math.circle(1).intersect(ear.math.ray(0.1, 0.1));\n// \tear.math.epsilonEqualVectors(shouldBeRay, clipRay[0]);\n\n// \tconst shouldBeSeg = [Math.sqrt(2) / 2, Math.sqrt(2) / 2];\n// \tconst clipSeg = ear.math.circle(1).intersect(ear.math.segment(0, 0, 10, 10));\n// \tear.math.epsilonEqualVectors(shouldBeSeg, clipSeg[0]);\n// });\n\n// test(\"circle circle intersect\", () => {\n// \t// same origin\n// \texpect(ear.math.circle(1).intersect(ear.math.circle(2))).toBe(undefined);\n// \t// kissing circles\n// \tconst result1 = ear.math.circle(1).intersect(ear.math.circle(1, [2, 0]));\n// \texpect(result1[0][0]).toBe(1);\n// \texpect(result1[0][1]).toBe(0);\n// \tconst result2 = ear.math.circle(1).intersect(ear.math.circle(1, [Math.sqrt(2), Math.sqrt(2)]));\n// \texpect(result2[0][0]).toBeCloseTo(Math.sqrt(2) / 2);\n// \texpect(result2[0][1]).toBeCloseTo(Math.sqrt(2) / 2);\n// \t// circles are contained\n// \texpect(ear.math.circle(10).intersect(ear.math.circle(1, [2, 0]))).toBe(undefined);\n// });\n"
  },
  {
    "path": "tests/math.primitives.clip.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"math types, clip line in rect\", () => {\n// \t// const rect = [[-1, -1], [1, -1], [1, 1], [-1, 1]];\n// \tconst rect = ear.math.rect(-1, -1, 2, 2);\n// \trect.exclusive();\n// \texpect(rect.clip(ear.math.line(1, 1))).not.toBe(undefined);\n// \texpect(rect.clip(ear.math.line([1, 0], [0, 1]))).toBe(undefined);\n// \texpect(rect.clip(ear.math.line(1, -1))).not.toBe(undefined);\n// \trect.inclusive();\n// \texpect(rect.clip(ear.math.line([1, 0], [0, 1]))).not.toBe(undefined);\n\n// \t// same as above, but inclusive test.\n// \tconst result1 = clip_line_in_convex_poly_inclusive(\n// \t\trect,\n// \t\tear.math.line(1, 1).vector,\n// \t\tear.math.line(1, 1).origin,\n// \t);\n// \texpect(result1[0][0]).toBe(-1);\n// \texpect(result1[0][1]).toBe(-1);\n// \texpect(result1[1][0]).toBe(1);\n// \texpect(result1[1][1]).toBe(1);\n// \tconst result2 = clip_line_in_convex_poly_inclusive(\n// \t\trect,\n// \t\tear.math.line([1, 0], [0, 1]).vector,\n// \t\tear.math.line([1, 0], [0, 1]).origin,\n// \t);\n// \texpect(result2[0][0]).toBe(-1);\n// \texpect(result2[0][1]).toBe(1);\n// \texpect(result2[1][0]).toBe(1);\n// \texpect(result2[1][1]).toBe(1);\n// \tconst result3 = clip_line_in_convex_poly_inclusive(\n// \t\trect,\n// \t\tear.math.line(1, -1).vector,\n// \t\tear.math.line(1, -1).origin,\n// \t);\n// \texpect(result3[0][0]).toBe(-1);\n// \texpect(result3[0][1]).toBe(1);\n// \texpect(result3[1][0]).toBe(1);\n// \texpect(result3[1][1]).toBe(-1);\n// });\n\n// test(\"math types, clip ray in rect\", () => {\n// \tconst rect = ear.math.rect(-1, -1, 2, 2);\n// \tconst result1 = rect.clip(ear.math.ray(1, 1));\n// \texpect(result1[0][0]).toBe(0);\n// \texpect(result1[0][1]).toBe(0);\n// \texpect(result1[1][0]).toBe(1);\n// \texpect(result1[1][1]).toBe(1);\n// \trect.inclusive();\n// \texpect(rect.clip(ear.math.ray([1, 0], [0, 1]))).not.toBe(undefined);\n// \trect.exclusive();\n// \texpect(rect.clip(ear.math.ray([1, 0], [0, 1]))).toBe(undefined);\n// \tconst result3 = rect.clip(ear.math.ray(1, -1));\n// \texpect(result3[0][0]).toBe(0);\n// \texpect(result3[0][1]).toBe(0);\n// \texpect(result3[1][0]).toBe(1);\n// \texpect(result3[1][1]).toBe(-1);\n// });\n\n// test(\"math types, clip segment in rect\", () => {\n// \tconst rect = ear.math.rect(-1, -1, 2, 2);\n// \tconst result1 = rect.clip(ear.math.segment([0, 0], [1, 1]));\n// \texpect(result1[0][0]).toBe(0);\n// \texpect(result1[0][1]).toBe(0);\n// \texpect(result1[1][0]).toBe(1);\n// \texpect(result1[1][1]).toBe(1);\n// \tconst result2 = rect.clip(ear.math.segment([0, 0], [2, 2]));\n// \texpect(result2[0][0]).toBe(0);\n// \texpect(result2[0][1]).toBe(0);\n// \texpect(result2[1][0]).toBe(1);\n// \texpect(result2[1][1]).toBe(1);\n// \tconst result3 = rect.clip(ear.math.segment([0, 0], [1, -1]));\n// \texpect(result3[0][0]).toBe(0);\n// \texpect(result3[0][1]).toBe(0);\n// \texpect(result3[1][0]).toBe(1);\n// \texpect(result3[1][1]).toBe(-1);\n// });\n\n// test(\"no clips\", () => {\n// \tconst rect = ear.math.rect(-1, -1, 2, 2);\n// \tconst result1 = rect.clip(ear.math.line([-0.707, 0.707], [2, 0]));\n// \texpect(result1).toBe(undefined);\n// });\n\n// test(\"core clip\", () => {\n// \tconst poly = [...ear.math.rect(-1, -1, 2, 2)];\n// \tconst vector = [1, 1];\n// \tconst origin = [0, 0];\n// \t[\n// \t\tclip_line_in_convex_poly_inclusive(poly, vector, origin),\n// \t\tclip_ray_in_convex_poly_exclusive(poly, vector, origin),\n// \t\tclip_ray_in_convex_poly_inclusive(poly, vector, origin),\n// \t\tclip_segment_in_convex_poly_exclusive(poly, vector, origin),\n// \t\tclip_segment_in_convex_poly_inclusive(poly, vector, origin),\n// \t].forEach(res => expect(res).not.toBe(undefined));\n// });\n\n// test(\"core no clip\", () => {\n// \tconst poly = [...ear.math.rect(-1, -1, 2, 2)];\n// \tconst vector = [1, 1];\n// \tconst origin = [10, 0];\n// \tconst seg0 = [10, 0];\n// \tconst seg1 = [0, 10];\n// \t[\n// \t\tclip_line_in_convex_poly_inclusive(poly, vector, origin),\n// \t\tclip_ray_in_convex_poly_exclusive(poly, vector, origin),\n// \t\tclip_ray_in_convex_poly_inclusive(poly, vector, origin),\n// \t\tclip_segment_in_convex_poly_exclusive(poly, seg0, seg1),\n// \t\tclip_segment_in_convex_poly_inclusive(poly, seg0, seg1),\n// \t].forEach(res => expect(res).toBe(undefined));\n// });\n\n// test(\"core clip segments exclusive\", () => {\n// \tconst poly = [...ear.math.rect(-1, -1, 2, 2)];\n// \t// all inside\n// \tconst seg0 = [[0, 0], [0.2, 0.2]];\n// \tconst result0 = clip_segment_in_convex_poly_exclusive(poly, ...seg0);\n// \texpect(ear.math.epsilonEqualVectors(seg0[0], result0[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg0[1], result0[1])).toBe(true);\n// \t// all outside\n// \tconst seg1 = [[10, 10], [10.2, 10.2]];\n// \t// const result1 = clip_segment_in_convex_poly_exclusive(poly, ...seg1);\n// \tconst result1 = ear.math.clipLineConvexPolygon(\n// \t\t[[-1, -1], [1, -1], [1, 1], [-1, 1]],\n// \t\t[0.2, 0.2],\n// \t\t[10, 10],\n// \t\tear.math.include,\n// \t\tear.math.includeS,\n// \t);\n// \texpect(result1).toBe(undefined);\n// \t// inside and collinear\n// \tconst seg2 = [[0, 0], [1, 0]];\n// \tconst result2 = ear.math.clipLineConvexPolygon(\n// \t\tpoly,\n// \t\t[1, 0],\n// \t\t[0, 0],\n// \t\tear.math.include,\n// \t\tear.math.includeS,\n// \t);\n// \texpect(ear.math.epsilonEqualVectors(seg2[0], result2[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg2[1], result2[1])).toBe(true);\n// \t// outside and collinear\n// \tconst seg3 = [[5, 0], [1, 0]];\n// \t// const result3 = clip_segment_in_convex_poly_exclusive(poly, ...seg3);\n// \tconst result3 = ear.math.clipLineConvexPolygon(\n// \t\tpoly,\n// \t\t[5, 0],\n// \t\t[1, 0],\n// \t\tear.math.exclude,\n// \t\tear.math.excludeS,\n// \t);\n// \texpect(result3).toBe(undefined);\n\n// \t// inside and collinear\n// \tconst seg4 = [[-1, 0], [1, 0]];\n// \tconst result4 = clip_segment_in_convex_poly_exclusive(poly, ...seg4);\n// \texpect(ear.math.epsilonEqualVectors(seg4[0], result4[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg4[1], result4[1])).toBe(true);\n// });\n\n// test(\"core clip segments inclusive\", () => {\n// \tconst poly = [...ear.math.rect(-1, -1, 2, 2)];\n// \t// all inside\n// \tconst seg0 = [[0, 0], [0.2, 0.2]];\n// \tconst result0 = clip_segment_in_convex_poly_inclusive(poly, ...seg0);\n// \texpect(ear.math.epsilonEqualVectors(seg0[0], result0[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg0[1], result0[1])).toBe(true);\n// \t// all outside\n// \tconst seg1 = [[10, 10], [10.2, 10.2]];\n// \tconst result1 = clip_segment_in_convex_poly_inclusive(poly, ...seg1);\n// \texpect(result1).toBe(undefined);\n// \t// inside and collinear\n// \tconst seg2 = [[0, 0], [1, 0]];\n// \tconst result2 = clip_segment_in_convex_poly_inclusive(poly, ...seg2);\n// \texpect(ear.math.epsilonEqualVectors(seg2[0], result2[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg2[1], result2[1])).toBe(true);\n// \t// outside and collinear\n// \t// const seg3 = [[5, 0], [1, 0]];\n// \t// const result3 = clip_segment_in_convex_poly_inclusive(poly, ...seg3);\n// \tconst result3 = ear.math.clipLineConvexPolygon(\n// \t\tpoly,\n// \t\t[5, 0],\n// \t\t[1, 0],\n// \t\tear.math.include,\n// \t\tear.math.includeS,\n// \t);\n// \texpect(result3).toBe(undefined);\n// \t// inside and collinear\n// \tconst seg4 = [[-1, 0], [1, 0]];\n// \tconst result4 = clip_segment_in_convex_poly_inclusive(poly, ...seg4);\n// \texpect(ear.math.epsilonEqualVectors(seg4[0], result4[0])).toBe(true);\n// \texpect(ear.math.epsilonEqualVectors(seg4[1], result4[1])).toBe(true);\n// });\n"
  },
  {
    "path": "tests/math.primitives.ellipse.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"x, y\", () => {\n// \tconst ellipse0 = ear.math.ellipse(2, 1);\n// \texpect(ellipse0.x).toBe(0);\n// \texpect(ellipse0.y).toBe(0);\n// \t// expect(ellipse0.z).toBe(0);\n// \tconst ellipse1 = ear.math.ellipse(2, 1, 5, 6);\n// \texpect(ellipse1.x).toBe(5);\n// \texpect(ellipse1.y).toBe(6);\n// \tconst ellipse2 = ear.math.ellipse(2, 1, [5, 6], 9);\n// \texpect(ellipse2.x).toBe(5);\n// \texpect(ellipse2.y).toBe(6);\n// \texpect(ellipse2.spin).toBe(9);\n// });\n\n// test(\"foci\", () => {\n// \tconst result = ear.math.ellipse(1, 0.5).foci;\n// \t// one of these is negative\n// \texpect(Math.abs(result[0].x)).toBeCloseTo(Math.sqrt(3) / 2);\n// \texpect(result[0].y).toBeCloseTo(0);\n// \texpect(Math.abs(result[1].x)).toBeCloseTo(Math.sqrt(3) / 2);\n// \texpect(result[1].y).toBeCloseTo(0);\n// });\n// // test(\"nearestPoint\", () => {\n// //   const result = ear.math.ellipse(2, 1).nearestPoint(0, 0);\n// // };\n// // test(\"intersect\", () => {\n// //   const result = ear.math.ellipse(2, 1).intersect(object);\n// // };\n// test(\"svgPath\", () => {\n// \texpect(ear.math.ellipse(2, 1).svgPath()).toBe(\"M2 0A2 1 0 0 1 -2 0A2 1 0 0 1 2 0\");\n// \texpect(ear.math.ellipse(2, 1, 5, 6).svgPath()).toBe(\"M7 6A2 1 0 0 1 3 6A2 1 0 0 1 7 6\");\n// });\n// test(\"points\", () => {\n// \texpect(ear.math.ellipse(2, 1).points().length).toBe(128);\n// \texpect(ear.math.ellipse(2, 1).points(10).length).toBe(10);\n// });\n// test(\"polygon\", () => {\n// \tconst result = ear.math.ellipse(2, 1).polygon();\n// \texpect(result.length).toBe(128);\n// \texpect(result[0][0]).toBeCloseTo(2);\n// \texpect(result[0][1]).toBeCloseTo(0);\n// });\n// test(\"segments\", () => {\n// \tconst result = ear.math.ellipse(2, 1).segments();\n// \texpect(result.length).toBe(128);\n// \texpect(result[0][0][0]).toBeCloseTo(2);\n// \texpect(result[0][0][1]).toBeCloseTo(0);\n// });\n"
  },
  {
    "path": "tests/math.primitives.json.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"vector\", () => {\n// \tconst v = JSON.stringify(ear.math.vector(1, 2, 3));\n// \texpect(v).toBe(`{\"0\":1,\"1\":2,\"2\":3,\"length\":3}`);\n// });\n\n// test(\"circle\", () => {\n// \tconst c = JSON.stringify(ear.math.circle(1, 2, 3));\n// \texpect(c).toBe(`{\"radius\":3,\"origin\":{\"0\":1,\"1\":2,\"length\":2}}`);\n// });\n"
  },
  {
    "path": "tests/math.primitives.junction.test.js",
    "content": "import { expect, test } from \"vitest\";\n// import ear from \"../src/index.js\";\n\ntest(\"empty\", () => { expect(true).toBe(true); });\n\n/*\ntest(\"getters\", () => {\n\tconst junction = ear.math.junction([1, 0], [0, 1], [-1, 0]);\n\texpect(junction.order.length).toBe(3);\n\texpect(junction.radians.length).toBe(3);\n\texpect(junction.vectors.length).toBe(3);\n\texpect(junction.sectors.length).toBe(3);\n});\n\ntest(\"alternating angle sum\", () => {\n\tconst junction = ear.math.junction([1, 0], [0, 1], [-1, 0], [0, -1]);\n\tconst res = junction.alternatingAngleSum();\n\texpect(res[0]).toBe(Math.PI);\n\texpect(res[1]).toBe(Math.PI);\n});\n\ntest(\"alternating angle 3\", () => {\n\tconst junction = ear.math.junction([1, 0], [0, 1], [-1, 0]);\n\tconst res = junction.alternatingAngleSum();\n\texpect(res[0]).toBe(Math.PI / 2 * 3);\n\texpect(res[1]).toBe(Math.PI / 2);\n});\n\ntest(\"static fromRadians 1\", () => {\n\tconst junction = ear.math.junction.fromRadians(0, 1, 2);\n\texpect(ear.math.equivalent_vectors(junction.radians, [0, 1, 2])).toBe(true);\n});\n\ntest(\"static fromRadians 2\", () => {\n\tconst junction = ear.math.junction.fromRadians(Math.PI/2, Math.PI, Math.PI/2*3);\n\texpect(junction.vectors[0].x).toBeCloseTo(0);\n\texpect(junction.vectors[0].y).toBeCloseTo(1);\n\n\texpect(junction.vectors[1].x).toBeCloseTo(-1);\n\texpect(junction.vectors[1].y).toBeCloseTo(0);\n\n\texpect(junction.vectors[2].x).toBeCloseTo(0);\n\texpect(junction.vectors[2].y).toBeCloseTo(-1);\n});\n*/\n"
  },
  {
    "path": "tests/math.primitives.line.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// const testEqual = function (...args) {\n// \texpect(ear.math.equivalent(...args)).toBe(true);\n// };\n\n// test(\"arguments\", () => {\n// \tconst l1 = ear.math.line(1);\n// \texpect(l1.origin[0]).toBe(0);\n// \texpect(l1.origin[1]).toBe(undefined);\n// \texpect(l1.origin[2]).toBe(undefined);\n// \tconst l2 = ear.math.line(1, 2);\n// \texpect(l2.origin[0]).toBe(0);\n// \texpect(l2.origin[1]).toBe(0);\n// \texpect(l2.origin[2]).toBe(undefined);\n// \tconst l3 = ear.math.line(1, 2, 3);\n// \texpect(l3.origin[0]).toBe(0);\n// \texpect(l3.origin[1]).toBe(0);\n// \texpect(l3.origin[2]).toBe(0);\n// });\n\n// test(\"u d form\", () => {\n// \tconst l1 = ear.math.line.fromNormalDistance({ normal: [1, 0], distance: 3 });\n// \texpect(l1.vector.x).toBeCloseTo(0);\n// \texpect(l1.vector.y).toBeCloseTo(-1);\n// \texpect(l1.origin.x).toBeCloseTo(3);\n// \texpect(l1.origin.y).toBeCloseTo(0);\n// });\n\n// // from the prototype\n// test(\"isParallel\", () => {\n// \tconst l = ear.math.line([0, 1], [2, 3]);\n// \texpect(l.isParallel([0, -1], [7, 8])).toBe(true);\n// });\n// test(\"isDegenerate\", () => {\n// \tconst l = ear.math.line([0, 0], [2, 3]);\n// \texpect(l.isDegenerate()).toBe(true);\n// });\n// test(\"reflection\", () => {\n// \tconst result = ear.math.line([0, 1], [2, 3]).reflectionMatrix();\n// \texpect(result[0]).toBeCloseTo(-1);\n// \texpect(result[1]).toBeCloseTo(0);\n// \texpect(result[2]).toBeCloseTo(0);\n// \texpect(result[3]).toBeCloseTo(0);\n// \texpect(result[4]).toBeCloseTo(1);\n// \texpect(result[9]).toBeCloseTo(4);\n// \texpect(result[10]).toBeCloseTo(0);\n// \t// expect(l.reflection().origin).toBe();\n// });\n// test(\"nearestPoint\", () => {\n// \tconst res = ear.math.line([1, 1], [2, 3]).nearestPoint(0, 0);\n// \texpect(res[0]).toBe(-0.5);\n// \texpect(res[1]).toBe(0.5);\n// \t// expect(l.nearestPoint(0,0)).toBe(true);\n// });\n// test(\"transform\", () => {\n// \tconst res = ear.math.line([0, 1], [2, 3]).transform([2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0]);\n// \texpect(res.vector[0]).toBeCloseTo(0);\n// \texpect(res.vector[1]).toBeCloseTo(2);\n// \texpect(res.origin[0]).toBeCloseTo(4);\n// \texpect(res.origin[1]).toBeCloseTo(6);\n// });\n// test(\"intersect\", () => {\n// \tconst polygon = ear.math.polygon([0, 1.15], [-1, -0.577], [1, -0.577]);\n// \tconst circle = ear.math.circle(1);\n// \tconst line = ear.math.line([1, 2], [-0.5, 0]);\n// \tconst line2 = ear.math.line([-1, 2], [0.5, 0]);\n// \tconst ray = ear.math.ray([-1, 2], [0.5, 0]);\n// \tconst segment = ear.math.segment([-2, 0.5], [2, 0.5]);\n// \t[\n// \t\tline.intersect(polygon),\n// \t\tline.intersect(circle),\n// \t\tline.intersect(line2),\n// \t\tline.intersect(ray),\n// \t\tline.intersect(segment),\n// \t].forEach(intersect => expect(intersect).not.toBe(undefined));\n// });\n\n// // todo problems\n// test(\"bisect\", () => {\n// \texpect(true).toBe(true);\n// \tconst l0 = ear.math.line([0, 1], [0, 0]);\n// \tconst l1 = ear.math.line([0, 1], [1, 0]);\n// \tconst res = l0.bisect(l1);\n// \t// console.log(\"Bisec\", res);\n// \t// expect(l.bisectLine()).toBe(true);\n// });\n\n// // line\n// test(\"fromPoints\", () => {\n// \tconst result = ear.math.line.fromPoints([1, 2], [3, 4]);\n// \texpect(result.origin.x).toBe(1);\n// \texpect(result.origin.y).toBe(2);\n// \texpect(result.vector.x).toBe(2);\n// \texpect(result.vector.y).toBe(2);\n// });\n// test(\"perpendicularBisector\", () => {\n// \tconst result = ear.math.line.perpendicularBisector([0, 1], [2, 3]);\n// \texpect(result.origin.x).toBe(1);\n// \texpect(result.origin.y).toBe(2);\n// \texpect(result.vector.x).toBe(-2);\n// \texpect(result.vector.y).toBe(2);\n// });\n// // this is no longer a property\n// // test(\"length infinity\", () => {\n// //   expect(ear.math.line().length).toBe(Infinity);\n// //   expect(ear.math.line([1,2],[3,4]).length).toBe(Infinity);\n// //   expect(ear.math.line.fromPoints([1,2],[3,4]).length).toBe(Infinity);\n\n// //   expect(ear.math.ray().length).toBe(Infinity);\n// //   expect(ear.math.ray([1,2],[3,4]).length).toBe(Infinity);\n// //   expect(ear.math.ray.fromPoints([1,2],[3,4]).length).toBe(Infinity);\n// // });\n// // // ray\n// // test(\"transform\", () => {\n// //   const r = ear.math.ray(0,1,2,3);\n// //   expect(r.transform()).toBe(true);\n// // });\n// test(\"ray\", () => {\n// \tconst result = ear.math.ray([1, 2], [3, 3]);\n// \texpect(result.origin.x).toBe(3);\n// \texpect(result.origin.y).toBe(3);\n// \texpect(result.vector.x).toBe(1);\n// \texpect(result.vector.y).toBe(2);\n// });\n// test(\"flip\", () => {\n// \tconst result = ear.math.ray([1, 2], [3, 3]).flip();\n// \texpect(result.origin.x).toBe(3);\n// \texpect(result.origin.y).toBe(3);\n// \texpect(result.vector.x).toBe(-1);\n// \texpect(result.vector.y).toBe(-2);\n// });\n// test(\"scale\", () => {\n// \tconst ray = ear.math.ray([6, 2], [3, 4]);\n// \tconst res = ray.scale(1 / 2);\n// \texpect(res.vector.x).toBe(3);\n// \texpect(res.vector.y).toBe(1);\n// });\n// test(\"normalize\", () => {\n// \tconst ray = ear.math.ray([4, 4], [2, 3]);\n// \tconst res = ray.normalize();\n// \texpect(res.vector.x).toBeCloseTo(Math.SQRT1_2);\n// \texpect(res.vector.y).toBeCloseTo(Math.SQRT1_2);\n// });\n// test(\"fromPoints\", () => {\n// \tconst result = ear.math.ray.fromPoints([1, 2], [3, 4]);\n// \texpect(result.origin.x).toBe(1);\n// \texpect(result.origin.y).toBe(2);\n// \texpect(result.vector.x).toBe(2);\n// \texpect(result.vector.y).toBe(2);\n// });\n// test(\"nearestPoint\", () => {\n// \tconst result = ear.math.ray([1, 1], [2, 3]).nearestPoint(0, 0);\n// \texpect(result[0]).toBe(2);\n// \texpect(result[1]).toBe(3);\n// \tconst result2 = ear.math.ray([0, 1], [5, -5]).nearestPoint(0, 0);\n// \texpect(result2[0]).toBe(5);\n// \texpect(result2[1]).toBe(0);\n// });\n\n// // segment\n// test(\"[0], [1]\", () => {\n// \tconst result = ear.math.segment([1, 2], [3, 4]);\n// \texpect(result[0][0]).toBe(1);\n// \texpect(result[0][1]).toBe(2);\n// \texpect(result[1][0]).toBe(3);\n// \texpect(result[1][1]).toBe(4);\n// });\n// test(\"length\", () => {\n// \tconst result = ear.math.segment([1, 2], [3, 4]);\n// \t// the Array.prototype length property\n// \texpect(result.length).toBe(2);\n// \t// geometric length\n// \texpect(result.magnitude).toBeCloseTo(Math.sqrt(2) * 2);\n// });\n// test(\"transform\", () => {\n// \tconst result1 = ear.math.segment([1, 2], [3, 4]).transform(ear.math.matrix().scale([0.5, 0.5, 0.5]));\n// \texpect(result1[0].x).toBe(0.5);\n// \texpect(result1[0].y).toBe(1);\n// \texpect(result1[1].x).toBe(1.5);\n// \texpect(result1[1].y).toBe(2);\n// });\n// test(\"midpoint\", () => {\n// \tconst result = ear.math.segment([1, 2], [3, 4]).midpoint();\n// \texpect(result.x).toBe(2);\n// \texpect(result.y).toBe(3);\n// });\n// test(\"fromPoints\", () => {\n// \tconst result = ear.math.segment.fromPoints([1, 2], [3, 4]);\n// \texpect(result[0].x).toBe(1);\n// \texpect(result[0].y).toBe(2);\n// \texpect(result[1].x).toBe(3);\n// \texpect(result[1].y).toBe(4);\n// });\n// test(\"nearestPoint\", () => {\n// \tconst res = ear.math.segment([1, 1], [2, 3]).nearestPoint(0, 0);\n// \texpect(res[0]).toBe(1);\n// \texpect(res[1]).toBe(1);\n// });\n\n// /**\n//  * lines, rays, segments\n//  */\n\n// // test(\"line ray segment intersections\", () => {\n// //   testEqual([5, 5], ear.math.line(0, 0, 1, 1).intersect(ear.math.line(10, 0, -1, 1)));\n// //   testEqual([5, 5], ear.math.line(0, 0, 1, 1).intersect(ear.math.ray(10, 0, -1, 1)));\n// //   testEqual([5, 5], ear.math.line(0, 0, 1, 1).intersect(ear.math.segment(10, 0, 0, 10)));\n// // });\n\n// // test(\"line ray segment parallel\", () => {\n// //   testEqual(true, ear.math.line(0, 0, 1, 1).isParallel(ear.math.ray(10, 0, 1, 1)));\n// //   testEqual(true, ear.math.line(0, 0, -1, 1).isParallel(ear.math.segment(0, 0, -2, 2)));\n// //   testEqual(false, ear.math.line(0, 0, -1, 1).isParallel(ear.math.segment(10, 0, 1, 1)));\n// // });\n\n// // test(\"line ray segment reflection matrices\", () => {\n// //   testEqual(\n// //     ear.math.line(10, 0, -1, 1).reflection(),\n// //     ear.math.ray(10, 0, -1, 1).reflection()\n// //   );\n// //   testEqual(\n// //     ear.math.segment(10, 0, 0, 10).reflection(),\n// //     ear.math.ray(10, 0, -1, 1).reflection()\n// //   );\n// // });\n\n// test(\"line ray segment nearest points\", () => {\n// \t// testEqual([20, -10], ear.math.line(10, 0, -1, 1).nearestPoint([20, -10]));\n// \t// testEqual([-50, 60], ear.math.line(10, 0, -1, 1).nearestPoint([-10, 100]));\n// \t// testEqual([10, 0], ear.math.ray(10, 0, -1, 1).nearestPoint([20, -10]));\n// \t// testEqual([-50, 60], ear.math.ray(10, 0, -1, 1).nearestPoint([-10, 100]));\n// \t// testEqual([10, 0], ear.math.segment(10, 0, 0, 10).nearestPoint([20, -10]));\n// \t// testEqual([0, 10], ear.math.segment(10, 0, 0, 10).nearestPoint([-10, 100]));\n// \t// testEqual(\n// \t//   ear.math.ray(10, 0, -1, 1).nearestPoint([0, 0]),\n// \t//   ear.math.line(10, 0, -1, 1).nearestPoint([0, 0])\n// \t// );\n// \t// testEqual(\n// \t//   ear.math.segment(10, 0, 0, 10).nearestPoint([0, 0]),\n// \t//   ear.math.ray(10, 0, -1, 1).nearestPoint([0, 0])\n// \t// );\n// });\n"
  },
  {
    "path": "tests/math.primitives.matrix.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"copy\", () => {\n// \tconst matrix = ear.math.matrix(1, 2, 3, 4, 5, 6, 7, 8, 9);\n// \tconst result = matrix.copy();\n// \tmatrix.set(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0);\n// \texpect(result[3]).toBe(4);\n// });\n// test(\"set\", () => {\n// \tconst matrix = ear.math.matrix(1, 2, 3, 4, 5, 6, 7, 8, 9);\n// \tmatrix.set(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0);\n// \texpect(matrix[3]).toBe(0);\n// });\n// test(\"isIdentity\", () => {\n// \texpect(ear.math.matrix(1, 2, 3, 4, 5, 6, 7, 8, 9).isIdentity()).toBe(false);\n// \texpect(ear.math.matrix().isIdentity()).toBe(true);\n// \texpect(ear.math.matrix(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 5, 6).isIdentity()).toBe(false);\n// \texpect(ear.math.matrix(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0).isIdentity()).toBe(true);\n// });\n// test(\"multiply\", () => {\n// \tconst matrix = ear.math.matrix();\n// \tconst m = ear.math.matrix().multiply(ear.math.matrix().rotateX(Math.PI / 4).rotateZ(Math.PI / 4));\n// \tconst sq = Math.sqrt(2) / 2;\n// \t[sq, 0.5, 0.5, -sq, 0.5, 0.5, 0, -sq, sq, 0, 0, 0]\n// \t\t.forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n// });\n// test(\"determinant\", () => {\n// \texpect(ear.math.matrix().determinant()).toBe(1);\n// });\n// test(\"inverse\", () => {\n// \tconst m = ear.math.matrix(0, -1, 0, 1, 0, 0, 0, 0, 1).inverse();\n// \t[0, 1, -0, -1, 0, 0, 0, -0, 1, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n// });\n// test(\"translate\", () => {\n// \texpect(ear.math.matrix().translate(4, 5, 6)[9]).toBe(4);\n// \texpect(ear.math.matrix().translate(4, 5, 6)[10]).toBe(5);\n// \texpect(ear.math.matrix().translate(4, 5, 6)[11]).toBe(6);\n// });\n// test(\"rotateX\", () => {\n// \tconst sq = Math.sqrt(2) / 2;\n// \tconst m = ear.math.matrix().rotateX(Math.PI / 4);\n// \t[1, 0, 0, 0, sq, sq, 0, -sq, sq, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n// });\n// test(\"rotateY\", () => {\n// \tconst sq = Math.sqrt(2) / 2;\n// \tconst m = ear.math.matrix().rotateY(Math.PI / 4);\n// \t[sq, 0, -sq, 0, 1, 0, sq, 0, sq, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n// });\n// test(\"rotateZ\", () => {\n// \tconst sq = Math.sqrt(2) / 2;\n// \tconst m = ear.math.matrix().rotateZ(Math.PI / 4);\n// \t[sq, sq, 0, -sq, sq, 0, 0, 0, 1, 0, 0, 0].forEach((a, i) => expect(a).toBeCloseTo(m[i]));\n// });\n// test(\"rotate\", () => {\n// \tconst m = ear.math.matrix().rotate(Math.PI / 2, [1, 1, 1], [0, 0, 0]);\n// \texpect(m[2]).toBeCloseTo(m[3]);\n// \texpect(m[0]).toBeCloseTo(m[4]);\n// \texpect(m[1]).toBeCloseTo(m[5]);\n// });\n// test(\"scale\", () => {\n// \texpect(ear.math.matrix().scale([0.5, 0.5, 0.5])[0]).toBe(0.5);\n// });\n// test(\"combine operations\", () => {\n// \tconst ident = ear.math.matrix();\n// \tconst result = ident.rotateX(Math.PI / 2).translate(40, 20, 10);\n// \t[1, 0, 0, 0, 0, 1, 0, -1, 0, 40, -10, 20]\n// \t\t.forEach((n, i) => expect(n).toBeCloseTo(result[i]));\n// });\n// test(\"reflectZ\", () => {\n// \tconst m = ear.math.matrix().reflectZ([1, 1, 1], [0, 0, 0]);\n// \texpect(m[1]).toBeCloseTo(m[3]);\n// \texpect(m[0]).toBeCloseTo(m[4]);\n// \texpect(m[2]).toBeCloseTo(m[5]);\n// \texpect(m[5]).toBeCloseTo(m[6]);\n// });\n// test(\"transform\", () => {\n// \tconst matrix = ear.math.matrix().rotateZ(Math.PI / 2).translate(4, 5, 6);\n// \tconst result = matrix.transform(ear.math.segment([-1, 0], [1, 0]));\n// });\n// test(\"transformVector\", () => {\n// \tconst vector = ear.math.vector(1, 2, 3);\n// \texpect(ear.math.matrix().scale([0.5, 0.5, 0.5]).transformVector(vector).x).toBeCloseTo(0.5);\n// \texpect(ear.math.matrix().scale([0.5, 0.5, 0.5]).transformVector(vector).y).toBeCloseTo(1);\n// \texpect(ear.math.matrix().scale([0.5, 0.5, 0.5]).transformVector(vector).z).toBeCloseTo(1.5);\n// });\n// test(\"transformLine\", () => {\n// \tconst line = ear.math.line([0.707, 0.707, 0], [1, 0, 0]);\n// \tconst result = ear.math.matrix().scale([0.5, 0.5, 0.5]).transformLine(line);\n// \texpect(result.vector.x).toBeCloseTo(0.3535);\n// \texpect(result.vector.y).toBeCloseTo(0.3535);\n// \texpect(result.vector.z).toBeCloseTo(0);\n// \texpect(result.origin.x).toBeCloseTo(0.5);\n// \texpect(result.origin.y).toBeCloseTo(0);\n// \texpect(result.origin.z).toBeCloseTo(0);\n// });\n// // test(\"transformLine with 2D vectors\", () => {\n// //   const vector = ear.math.line([0.707, 0.707], [1, 0]);\n// //   console.log(ear.math.matrix().scale(0.5).transformLine(vector));\n// // });\n\n// const testEqualVectors = function (...args) {\n// \texpect(ear.math.epsilonEqualVectors(...args)).toBe(true);\n// };\n\n// const sqrt05 = Math.sqrt(0.5);\n\n// /**\n//  * matrices\n//  */\n\n// test(\"matrix 2 core\", () => {\n// \texpect(ear.math.invertMatrix2([1, 0, 0, 1, 0, 0])).not.toBe(undefined);\n// \tconst r1 = ear.math.makeMatrix2Translate();\n// \texpect(r1[0]).toBe(1);\n// \texpect(r1[4]).toBe(0);\n// \texpect(r1[5]).toBe(0);\n// \tconst r2 = ear.math.makeMatrix2Scale([2, -1]);\n// \texpect(r2[0]).toBe(2);\n// \texpect(r2[3]).toBe(-1);\n// });\n\n// test(\"matrix core invert\", () => {\n// \texpect(ear.math.invertMatrix2([1, 0, 0, 1, 0, 0])).not.toBe(undefined);\n// \texpect(ear.math.invertMatrix3([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0])).not.toBe(undefined);\n// \texpect(ear.math.invertMatrix2([5, 5, 4, 4, 3, 3])).toBe(undefined);\n// \texpect(ear.math.invertMatrix3([0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1])).toBe(undefined);\n// });\n\n// test(\"matrix 3, init with parameters\", () => {\n// \tconst result1 = ear.math.matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0);\n// \ttestEqualVectors(result1, [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]);\n// \tconst result2 = ear.math.matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);\n// \ttestEqualVectors(result2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);\n// \tconst result3 = ear.math.matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);\n// \ttestEqualVectors(result3, [1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15]);\n// });\n\n// // todo: test matrix3 methods (invert) with the translation component to make sure it carries over\n// test(\"matrix 3 core, transforms\", () => {\n// \tconst result1 = ear.math.makeMatrix3Translate();\n// \t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0].forEach((n, i) => expect(n).toBe(result1[i]));\n// \t// rotate 360 degrees about an arbitrary axis and origin\n// \ttestEqualVectors(\n// \t\t[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n// \t\tear.math.makeMatrix3Rotate(\n// \t\t\tMath.PI * 2,\n// \t\t\t[Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5],\n// \t\t\t[Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5],\n// \t\t),\n// \t);\n\n// \ttestEqualVectors(\n// \t\tear.math.makeMatrix3RotateX(Math.PI / 6),\n// \t\tear.math.makeMatrix3Rotate(Math.PI / 6, [1, 0, 0]),\n// \t);\n// \ttestEqualVectors(\n// \t\tear.math.makeMatrix3RotateY(Math.PI / 6),\n// \t\tear.math.makeMatrix3Rotate(Math.PI / 6, [0, 1, 0]),\n// \t);\n// \ttestEqualVectors(\n// \t\tear.math.makeMatrix3RotateZ(Math.PI / 6),\n// \t\tear.math.makeMatrix3Rotate(Math.PI / 6, [0, 0, 1]),\n// \t);\n// \t// source wikipedia https://en.wikipedia.org/wiki/Rotation_matrix#Examples\n// \tconst exampleMat = [\n// \t\t0.35612209405955486, -0.8018106071106572, 0.47987165414043453,\n// \t\t0.47987165414043464, 0.5975763087872217, 0.6423595182829954,\n// \t\t-0.8018106071106572, 0.0015183876574496047, 0.5975763087872216,\n// \t\t0, 0, 0,\n// \t];\n// \ttestEqualVectors(\n// \t\texampleMat,\n// \t\tear.math.makeMatrix3Rotate((-74 / 180) * Math.PI, [-1 / 3, 2 / 3, 2 / 3]),\n// \t);\n// \ttestEqualVectors(\n// \t\texampleMat,\n// \t\tear.math.makeMatrix3Rotate((-74 / 180) * Math.PI, [-0.5, 1, 1]),\n// \t);\n\n// \ttestEqualVectors(\n// \t\t[1, 0, 0, 0, 0.8660254, 0.5, 0, -0.5, 0.8660254, 0, 0, 0],\n// \t\tear.math.makeMatrix3Rotate(Math.PI / 6, [1, 0, 0]),\n// \t);\n// });\n\n// test(\"matrix 3 core\", () => {\n// \texpect(ear.math.determinant3([1, 2, 3, 2, 4, 8, 7, 8, 9])).toBe(12);\n// \texpect(ear.math.determinant3([3, 2, 0, 0, 0, 1, 2, -2, 1, 0, 0, 0])).toBe(10);\n// \ttestEqualVectors(\n// \t\t[4, 5, -8, -5, -6, 9, -2, -2, 3, 0, 0, 0],\n// \t\tear.math.invertMatrix3([0, 1, -3, -3, -4, 4, -2, -2, 1, 0, 0, 0]),\n// \t);\n// \ttestEqualVectors(\n// \t\t[0.2, -0.2, 0.2, 0.2, 0.3, -0.3, 0, 1, 0, 0, 0, 0],\n// \t\tear.math.invertMatrix3([3, 2, 0, 0, 0, 1, 2, -2, 1, 0, 0, 0]),\n// \t);\n// \tconst mat_3d_ref = ear.math.makeMatrix3ReflectZ([1, -2], [12, 13]);\n// \ttestEqualVectors(\n// \t\tear.math.makeMatrix2Reflect([1, -2], [12, 13]),\n// \t\t[mat_3d_ref[0], mat_3d_ref[1], mat_3d_ref[3], mat_3d_ref[4], mat_3d_ref[9], mat_3d_ref[10]],\n// \t);\n// \t// source wolfram alpha\n// \ttestEqualVectors(\n// \t\t[-682, 3737, -5545, 2154, -549, -1951, 953, -3256, 4401, 0, 0, 0],\n// \t\tear.math.multiplyMatrices3(\n// \t\t\t[5, -52, 85, 15, -9, -2, 32, 2, -50, 0, 0, 0],\n// \t\t\t[-77, 25, -21, 3, 53, 42, 63, 2, 19, 0, 0, 0],\n// \t\t),\n// \t);\n// });\n\n// // matrix 2 has been depricated as a top-level object\n// //\n// // test(\"matrix 2\", () => {\n// //   // top level types\n// //   testEqualVectors([1, 2, 3, 4, 5, 6], ear.math.matrix2(1, 2, 3, 4, 5, 6));\n// //   testEqualVectors([1, 0, 0, 1, 6, 7], ear.math.matrix2.makeTranslation(6, 7));\n// //   testEqualVectors([3, 0, 0, 3, -2, 0], ear.math.matrix2.makeScale(3, 3, [1, 0]));\n// //   testEqualVectors([0, 1, 1, -0, -8, 8], ear.math.matrix2.makeReflection([1, 1], [-5, 3]));\n// //   testEqualVectors(\n// //     [sqrt05, sqrt05, -sqrt05, sqrt05, 1, 1],\n// //     ear.math.matrix2.makeRotation(Math.PI/4, [1, 1])\n// //   );\n// //   testEqualVectors(\n// //     [sqrt05, -sqrt05, sqrt05, sqrt05, -sqrt05, sqrt05],\n// //     ear.math.matrix2(sqrt05, sqrt05, -sqrt05, sqrt05, 1, 0).inverse()\n// //   );\n// //   testEqualVectors(\n// //     [Math.sqrt(4.5), sqrt05, -sqrt05, Math.sqrt(4.5), Math.sqrt(4.5), sqrt05],\n// //     ear.math.matrix2(sqrt05, -sqrt05, sqrt05, sqrt05, 0, 0)\n// //       .multiply(ear.math.matrix2(1, 2, -2, 1, 1, 2))\n// //   );\n// //   testEqualVectors([0, 3], ear.math.matrix2(2, 1, -1, 2, -1, 0).transform(1, 1));\n// //   testEqualVectors([-2, 3], ear.math.matrix2.makeScale(3, 3, [1, 0]).transform([0, 1]));\n// //   testEqualVectors([-1, 2], ear.math.matrix2.makeScale(3, 3, [0.5, 0.5]).transform([0, 1]));\n// //   testEqualVectors([1, 1], ear.math.matrix2.makeScale(0.5, 0.5, [1, 1]).transform([1, 1]));\n// //   testEqualVectors([0.75, 0.75], ear.math.matrix2.makeScale(0.5, 0.5, [0.5, 0.5]).transform([1, 1]));\n// // });\n"
  },
  {
    "path": "tests/math.primitives.paths.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"segment\", () => {\n// \texpect(ear.math.segment(1, 2, 3, 4).svgPath()).toBe(\"M1 2L3 4\");\n// });\n\n// test(\"line\", () => {\n// \texpect(ear.math.line(1, 2).svgPath()).toBe(\"M-10000 -20000l20000 40000\");\n// \texpect(ear.math.line(1, 2).svgPath(20)).toBe(\"M-10 -20l20 40\");\n// });\n\n// test(\"ray\", () => {\n// \texpect(ear.math.ray(1, 2).svgPath()).toBe(\"M0 0l10000 20000\");\n// \texpect(ear.math.ray(1, 2).svgPath(10)).toBe(\"M0 0l10 20\");\n// });\n\n// test(\"rect\", () => {\n// \texpect(ear.math.rect(1, 2).svgPath()).toBe(\"M0 0h1v2h-1Z\");\n// });\n\n// test(\"circle\", () => {\n// \texpect(ear.math.circle(4).svgPath()).toBe(\"M4 0A4 4 0 0 1 -4 0A4 4 0 0 1 4 0\");\n// });\n\n// test(\"ellipse\", () => {\n// \texpect(ear.math.ellipse(1, 2).svgPath()).toBe(\"M1 0A1 2 0 0 1 -1 0A1 2 0 0 1 1 0\");\n// \texpect(ear.math.ellipse(1, 2).svgPath(-Math.PI).slice(0, 4)).toBe(\"M-1 \");\n// \texpect(ear.math.ellipse(1, 2).svgPath(-Math.PI * 2)).toBe(\"M1 0A1 2 0 0 1 -1 0A1 2 0 0 1 1 0\");\n// });\n\n// test(\"polygon\", () => {\n// \texpect(ear.math.polygon([1, 0], [0, 1], [-1, 0], [0, -1]).svgPath())\n// \t\t.toBe(\"M1 0L0 1L-1 0L0 -1z\");\n// });\n"
  },
  {
    "path": "tests/math.primitives.polygon.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// const equalTest = (a, b) => expect(JSON.stringify(a))\n// \t.toBe(JSON.stringify(b));\n\n// test(\"prototype member variables accessing 'this'\", () => {\n// \texpect(ear.math.makePolygonCircumradius(4).sides.length).toBe(4);\n// \texpect(ear.math.makePolygonCircumradius(4).area()).toBeCloseTo(2);\n// });\n\n// // todo: convex\n// test(\"isConvex\", () => {\n// \texpect(ear.math.makePolygonCircumradius(4).isConvex).toBe(undefined);\n// });\n// test(\".segments\", () => {\n// \tconst polygon = ear.math.makePolygonCircumradius(4);\n// \tconst segments = polygon.segments();\n// \texpect(segments.length).toBe(4);\n// \texpect(polygon.sides[0]).toBe(polygon.segments()[0]);\n// });\n// test(\"intersect\", () => {\n// \tconst polygon = ear.math.makePolygonCircumradius(4);\n// \tconst segment = ear.math.intersectLineLine(polygon, [1, 1], [0, 0]).point;\n// \texpect(Math.abs(segment[0][0])).toBe(0.5);\n// \texpect(Math.abs(segment[0][1])).toBe(0.5);\n// \texpect(Math.abs(segment[1][0])).toBe(0.5);\n// \texpect(Math.abs(segment[1][1])).toBe(0.5);\n// });\n// test(\"area\", () => {\n// \texpect(ear.math.polygon([-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]).area()).toBeCloseTo(1);\n// });\n// test(\"convex Hull\", () => {\n// \tconst result = ear.math.polygon.convexHull([[1, 0], [0.5, 0], [0, 1], [0, -1]]);\n// \texpect(result.points.length).toBe(3);\n// });\n// test(\"skeleton\", () => {\n// \tconst hull = ear.math.polygon.convexHull(Array.from(Array(10))\n// \t\t.map(() => [Math.random(), Math.random()]));\n// \tconst skeleton = hull.straightSkeleton();\n// \tconst skeletons = skeleton.filter(el => el.type === \"skeleton\");\n// \tconst perpendiculars = skeleton.filter(el => el.type === \"perpendicular\");\n// \texpect(skeletons.length).toBe((hull.length - 3) * 2 + 3);\n// \texpect(perpendiculars.length).toBe(hull.length - 2);\n// });\n// // test(\"midpoint\", () => {\n// //   const result = ear.math.polygon([-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]).midpoint();\n// //   expect(result[0]).toBeCloseTo(0);\n// //   expect(result[1]).toBeCloseTo(0);\n// // });\n// test(\"centroid\", () => {\n// \tconst result = ear.math.polygon([1, 0], [0, 1], [-1, 0]).centroid();\n// \texpect(result[0]).toBeCloseTo(0);\n// \texpect(result[1]).toBeCloseTo(1 / 3);\n// });\n// test(\"boundingBox\", () => {\n// \tconst box = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).boundingBox();\n// \texpect(box.min[0]).toBe(-1);\n// \texpect(box.min[1]).toBe(-1);\n// \texpect(box.span[0]).toBe(2);\n// \texpect(box.span[1]).toBe(2);\n// });\n// test(\"contains\", () => {\n// \texpect(ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).overlap(ear.math.vector(0.49, 0.49)))\n// \t\t.toBe(true);\n// \texpect(ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).overlap(ear.math.vector(0.5, 0.5)))\n// \t\t.toBe(true);\n// \texpect(ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).overlap(ear.math.vector(0.51, 0.51)))\n// \t\t.toBe(false);\n// });\n// test(\"scale\", () => {\n// \tconst result = ear.math.polygon([-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]).scale(2);\n// \texpect(result.points[0][0]).toBeCloseTo(-1);\n// \texpect(result.points[0][1]).toBeCloseTo(-1);\n// });\n// test(\"rotate\", () => {\n// \tconst sq = Math.sqrt(2) / 2;\n// \tconst result = ear.math.polygon([-sq, -sq], [sq, -sq], [sq, sq], [-sq, sq]).rotate(Math.PI / 4);\n// \texpect(result.points[0][0]).toBeCloseTo(0);\n// \texpect(result.points[0][1]).toBeCloseTo(-1);\n// });\n// test(\"translate\", () => {\n// \tconst result = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).translate(Math.PI / 2);\n// });\n// test(\"transform\", () => {\n// \tconst matrix = ear.math.matrix(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 5, 0);\n// \tconst result = ear.math.polygon([-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]).transform(matrix);\n// \texpect(result.points[0][0]).toBeCloseTo(3.5);\n// \texpect(result.points[0][1]).toBeCloseTo(4.5);\n// });\n// // test(\"sectors\", () => {\n// //   const result = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).sectors();\n// //   console.log(\"sectors\", result);\n// // });\n// test(\"nearest\", () => {\n// \tconst result = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]).nearest(10, 10);\n// \texpect(result.point[0]).toBe(0.5);\n// \texpect(result.point[1]).toBe(0.5);\n// \texpect(result.distance).toBeCloseTo(13.435028842544403);\n// \texpect(result.edge[0][0]).toBeCloseTo(1);\n// \texpect(result.edge[0][1]).toBeCloseTo(0);\n// });\n\n// test(\"overlap\", () => {\n// \tconst poly1 = ear.math.polygon([[1, 0], [0, 1], [-1, 0]]); // top\n// \tconst poly2 = ear.math.polygon([[0, 1], [-1, 0], [0, -1]]); // left\n// \tconst poly3 = ear.math.polygon([[1, 0], [0, 1], [0, -1]]); // right\n// \tconst poly4 = ear.math.polygon([[1, 0], [-1, 0], [0, -1]]); // bottom\n// \texpect(poly1.overlap(poly2)).toBe(true);\n// \texpect(poly1.overlap(poly3)).toBe(true);\n// \texpect(poly4.overlap(poly2)).toBe(true);\n// \texpect(poly4.overlap(poly3)).toBe(true);\n\n// \texpect(poly2.overlap(poly3)).toBe(false);\n// \texpect(poly1.overlap(poly4)).toBe(false);\n// });\n// test(\"split\", () => {\n// \tconst poly = ear.math.polygon([[1, 0], [0, 1], [-1, 0]]);\n// \tconst line1 = ear.math.line([1, 0], [0, 0.5]);\n// \tconst line2 = ear.math.line([1, 0], [0, -0.5]);\n// \tconst result1 = poly.split(line1);\n\n// \texpect(result1[0][0][0]).toBe(-1);\n// \texpect(result1[0][0][1]).toBe(0);\n\n// \texpect(result1[0][1][0]).toBe(1);\n// \texpect(result1[0][1][1]).toBe(0);\n\n// \texpect(result1[0][2][0]).toBe(0.5);\n// \texpect(result1[0][2][1]).toBe(0.5);\n\n// \texpect(result1[0][3][0]).toBe(-0.5);\n// \texpect(result1[0][3][1]).toBe(0.5);\n\n// \texpect(result1[1][0][0]).toBe(0);\n// \texpect(result1[1][0][1]).toBe(1);\n\n// \texpect(result1[1][1][0]).toBe(-0.5);\n// \texpect(result1[1][1][1]).toBe(0.5);\n\n// \texpect(result1[1][2][0]).toBe(0.5);\n// \texpect(result1[1][2][1]).toBe(0.5);\n// });\n\n// test(\"intersectLine\", () => {\n// \tconst poly = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]);\n// \tconst line = ear.math.line([1, 0], [0, 0.5]);\n// \tconst result = poly.intersect(line);\n// \texpect(result[0][0]).toBeCloseTo(0.5);\n// \texpect(result[0][1]).toBeCloseTo(0.5);\n// });\n// test(\"intersectRay\", () => {\n// \tconst poly = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]);\n// \tconst ray = ear.math.ray([1, 0], [0, 0.5]);\n// \tconst result = poly.intersect(ray);\n// });\n// test(\"intersectSegment\", () => {\n// \tconst poly = ear.math.polygon([[1, 0], [0, 1], [-1, 0], [0, -1]]);\n// \tconst segment = ear.math.segment([-2, 0.5], [2, 0.5]);\n// \tconst result = poly.intersect(segment);\n// \texpect(result[0][0]).toBeCloseTo(0.5);\n// \texpect(result[0][1]).toBeCloseTo(0.5);\n// \texpect(result[1][0]).toBeCloseTo(-0.5);\n// \texpect(result[1][1]).toBeCloseTo(0.5);\n// });\n// // test(\"svgPath\", () => {\n// //   svgPath: function ()\n// // });\n"
  },
  {
    "path": "tests/math.primitives.rect.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// // static\n// test(\"static fromPoints\", () => {\n// \tconst r = ear.math.rect.fromPoints([1, 1], [3, 2]);\n// \texpect(r.width).toBe(2);\n// \texpect(r.height).toBe(1);\n// });\n\n// // native\n// test(\"area\", () => {\n// \tconst r = ear.math.rect(2, 3, 4, 5);\n// \texpect(r.area()).toBe(4 * 5);\n// });\n\n// test(\"scale\", () => {\n// \tconst r = ear.math.rect(2, 3, 4, 5);\n// \texpect(r.scale(2).area()).toBe((4 * 2) * (5 * 2));\n// });\n\n// test(\"segments\", () => {\n// \tconst r = ear.math.rect(2, 3, 4, 5);\n// \tconst seg = r.segments();\n// \texpect(seg.length).toBe(4);\n// });\n\n// test(\"center\", () => {\n// \tconst r = ear.math.rect(2, 3, 4, 5);\n// \texpect(r.center.x).toBe(2 + 4 / 2);\n// \texpect(r.center.y).toBe(3 + 5 / 2);\n// });\n\n// test(\"centroid\", () => {\n// \tconst r = ear.math.rect(1, 2, 3, 4);\n// \tconst centroid = r.centroid();\n// \texpect(centroid.x).toBe(1 + 3 / 2);\n// \texpect(centroid.y).toBe(2 + 4 / 2);\n// });\n\n// test(\"boundingBox\", () => {\n// \tconst r = ear.math.rect(1, 2, 3, 4);\n// \tconst bounds = r.boundingBox();\n// \texpect(bounds.min[0]).toBe(1);\n// \texpect(bounds.min[1]).toBe(2);\n// \texpect(bounds.span[0]).toBe(3);\n// \texpect(bounds.span[1]).toBe(4);\n// });\n\n// test(\"contains\", () => {\n// \tconst r = ear.math.rect(1, 2, 3, 4);\n// \texpect(r.overlap(ear.math.vector(0, 0))).toBe(false);\n// \texpect(r.overlap(ear.math.vector(1.5, 3))).toBe(true);\n// });\n\n// test(\"svg\", () => {\n// \tconst r = ear.math.rect(1, 2, 3, 4);\n// \texpect(r.svgPath()).toBe(\"M1 2h3v4h-3Z\");\n// });\n\n// // test(\"rotate\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.rotate();\n// // });\n\n// // test(\"translate\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.translate();\n// // });\n\n// // test(\"transform\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.transform();\n// // });\n\n// // test(\"sectors\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.sectors();\n// // });\n\n// // test(\"nearest\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.nearest();\n// // });\n\n// // test(\"clipSegment\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.clipSegment();\n// // });\n\n// // test(\"clipLine\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.clipLine();\n// // });\n\n// // test(\"clipRay\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.clipRay();\n// // });\n\n// // test(\"split\", () => {\n// //   const r = ear.math.rect(1, 2, 3, 4);\n// //   r.split();\n// // });\n"
  },
  {
    "path": "tests/math.primitives.types.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// const names = [\n// \t\"vector\",\n// \t\"matrix\",\n// \t\"segment\",\n// \t\"ray\",\n// \t\"line\",\n// \t\"rect\",\n// \t\"circle\",\n// \t\"ellipse\",\n// \t\"polygon\",\n// \t// \"junction\",\n// ];\n\n// const primitives = names\n// \t.map(key => ear.math[key]());\n\n// test(\"primitive constructor function names\", () => primitives\n// \t.forEach((primitive, i) => expect(primitive.constructor.name)\n// \t\t.toBe(names[i])));\n\n// test(\"primitives Typeof\", () => primitives\n// \t.forEach((primitive, i) => expect(ear.math.typeof(primitive))\n// \t\t.toBe(names[i])));\n\n// test(\"primitives instanceof\", () => primitives\n// \t.forEach((primitive, i) => expect(primitive instanceof ear.math[names[i]])\n// \t\t.toBe(true)));\n\n// test(\"primitives constructor\", () => primitives\n// \t.forEach((primitive, i) => expect(primitive.constructor === ear.math[names[i]])\n// \t\t.toBe(true)));\n\n// test(\"interchangeability with get_\", () => {\n// \tconst vector = ear.math.vector(1, 2, 3);\n// \tconst matrix = ear.math.matrix(1, 2, 3, 4);\n// \tconst line = ear.math.line(1, 2);\n// \tconst ray = ear.math.ray(1, 2);\n// \tconst segment = ear.math.segment([1, 2], [3, 4]);\n// \tconst circle = ear.math.circle(1);\n// \tconst rect = ear.math.rect(2, 4);\n// \tconst ellipse = ear.math.ellipse(1, 2);\n// \texpect(ear.math.getVector(vector)[2]).toBe(3);\n// \t// expect(ear.math.getVectorOfVectors(segment)[0]).toBe(1);\n// \texpect(ear.math.getSegment(segment)[0][1]).toBe(2);\n// \texpect(ear.math.getLine(line).vector[1]).toBe(2);\n// \texpect(ear.math.getRect(rect).height).toBe(4);\n// \texpect(ear.math.getMatrix3x4(matrix)[4]).toBe(4);\n// //  expect(ear.math.get_matrix2(matrix)[1]).toBe(2);\n// });\n"
  },
  {
    "path": "tests/math.primitives.vector.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"excluding primitives\", () => expect(true).toBe(true));\n\n// test(\"arguments\", () => {\n// \texpect(ear.math.vector(1, 2, 3)[2]).toBe(3);\n// \texpect(ear.math.vector([1, 2, 3])[2]).toBe(3);\n// \texpect(ear.math.vector([[1, 2, 3]])[2]).toBe(3);\n// \texpect(ear.math.vector([[], [1, 2, 3]])[2]).toBe(3);\n// \texpect(ear.math.vector([[1], [2], [3]])[2]).toBe(3);\n// \texpect(ear.math.vector({ x: 1, y: 2, z: 3 })[2]).toBe(3);\n// \texpect(ear.math.vector([{ x: 1, y: 2, z: 3 }])[2]).toBe(3);\n// \texpect(ear.math.vector([[{ x: 1, y: 2, z: 3 }]])[2]).toBe(3);\n// \texpect(ear.math.vector([[], { x: 1, y: 2, z: 3 }])[2]).toBe(3);\n// });\n\n// // static\n\n// test(\"static\", () => {\n// \texpect(ear.math.vector.fromAngle(Math.PI).x).toBeCloseTo(-1);\n// \texpect(ear.math.vector.fromAngle(Math.PI).y).toBeCloseTo(0);\n// \texpect(ear.math.vector.fromAngle(Math.PI).z).toBe(undefined);\n// \texpect(ear.math.vector.fromAngleDegrees(90).x).toBeCloseTo(0);\n// \texpect(ear.math.vector.fromAngleDegrees(90).y).toBeCloseTo(1);\n// \texpect(ear.math.vector.fromAngleDegrees(90).z).toBe(undefined);\n// });\n\n// // methods\n\n// test(\"magnitude\", () => {\n// \tconst v = ear.math.vector(1, 2, 3).normalize();\n// \texpect(v.magnitude()).toBe(1);\n// });\n\n// test(\"normalize\", () => {\n// \tconst v = ear.math.vector(0).normalize();\n// \texpect(v.magnitude()).toBe(0);\n// \t// normalize appears so many places in these tests...\n// });\n\n// test(\"isEquivalent\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.isEquivalent([1, 2, 2.99999999])).toBe(true);\n// \texpect(v.isEquivalent([1, 2, 2.9999999])).toBe(true);\n// \t// this is where the current epsilon is\n// \texpect(v.isEquivalent([1, 2, 2.999999])).toBe(false);\n// \texpect(v.isEquivalent([1, 2])).toBe(false);\n// \texpect(v.isEquivalent([1, 2, 0])).toBe(false);\n// \texpect(v.isEquivalent([1, 2, 3, 4])).toBe(false);\n// });\n\n// test(\"isParallel\", () => {\n// \tconst v = ear.math.vector(1, 2, 3).normalize();\n// \texpect(v.isParallel([-1, -2, -3])).toBe(true);\n// });\n\n// test(\"dot\", () => {\n// \tconst v = ear.math.vector(1, 2);\n// \texpect(v.dot([-2, 1])).toBe(0);\n// });\n\n// test(\"distanceTo\", () => {\n// \tconst v = ear.math.vector(3, 0);\n// \texpect(v.distanceTo([-3, 0])).toBe(6);\n// });\n\n// test(\"bisect\", () => {\n// \texpect(ear.math.vector(1, 0).bisect(ear.math.vector(0, 1)).x)\n// \t\t.toBeCloseTo(Math.sqrt(2) / 2);\n// \texpect(ear.math.vector(1, 0).bisect(ear.math.vector(0, 1)).y)\n// \t\t.toBeCloseTo(Math.sqrt(2) / 2);\n// });\n\n// test(\"copy\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.copy().z).toBe(3);\n// });\n\n// test(\"scale\", () => {\n// \tconst v = ear.math.vector(2, -3);\n// \texpect(v.scale(2).x).toBe(4);\n// \texpect(v.scale(2).y).toBe(-6);\n// \texpect(v.scale(-2).x).toBe(-4);\n// \texpect(v.scale(-2).y).toBe(6);\n// \texpect(v.scale(0).x).toBeCloseTo(0);\n// \texpect(v.scale(0).y).toBeCloseTo(0);\n// });\n\n// test(\"cross\", () => {\n// \tconst v = ear.math.vector(1, 2, 3).normalize();\n// \tlet w = ear.math.vector(3, 4).normalize();\n// \t// [0, 0, 0.8]\n// \texpect(0.8 - w.cross(2, 4)[2]).toBeLessThan(1e-6);\n// \texpect(w.cross(2, -4, 5)[0]).toBe(4);\n// \texpect(w.cross(2, -4, 5)[1]).toBe(-3);\n// \texpect(w.cross(2, -4, 5)[2]).toBe(-4);\n// \texpect(w.cross(2, -4)[2]).toBe(-4);\n// });\n\n// test(\"add\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \tfor (let i = 0; i < v.length; i += 1) {\n// \t\texpect(v.add([-1, 2, 3])[i]).toBe([0, 4, 6][i]);\n// \t}\n// \tfor (let i = 0; i < v.length; i += 1) {\n// \t\texpect(v.add([-1, 2])[i]).toBe([0, 4, 3][i]);\n// \t}\n// });\n\n// test(\"subtract\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \tfor (let i = 0; i < v.length; i += 1) {\n// \t\texpect(v.subtract([-1, 2, 3])[i]).toBe([2, 0, 0][i]);\n// \t}\n// \tfor (let i = 0; i < v.length; i += 1) {\n// \t\texpect(v.subtract([-1, 2])[i]).toBe([2, 0, 3][i]);\n// \t}\n// });\n\n// test(\"rotate90\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.rotate90().x).toBe(-2);\n// \texpect(v.rotate90().y).toBe(1);\n// \texpect(v.rotate90().z).toBe(undefined);\n// });\n\n// test(\"rotate270\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.rotate270().x).toBe(2);\n// \texpect(v.rotate270().y).toBe(-1);\n// \texpect(v.rotate270().z).toBe(undefined);\n// });\n\n// test(\"flip\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.flip().x).toBe(-1);\n// \texpect(v.flip().y).toBe(-2);\n// \texpect(v.flip().z).toBe(-3);\n// });\n\n// test(\"lerp\", () => {\n// \tconst v = ear.math.vector(2, 0);\n// \texpect(v.lerp([-2, 0], 0.5)[0]).toBe(0);\n// \texpect(v.lerp([-2, 0], 0.25)[0]).toBe(1);\n// \texpect(v.lerp([-2, 0], 0.75)[0]).toBe(-1);\n// \texpect(v.lerp([-2], 0.25)[0]).toBe(1);\n// \texpect(v.lerp([-2], 0.75)[0]).toBe(-1);\n// });\n\n// test(\"midpoint\", () => {\n// \tconst v = ear.math.vector(1, 2, 3);\n// \texpect(v.midpoint([1, 2])[2]).toBe(1.5);\n// \texpect(v.midpoint([1, 2, 10]).x).toBe(1);\n// \texpect(v.midpoint([1, 2, 10]).y).toBe(2);\n// \texpect(v.midpoint([1, 2, 10]).z).toBe(6.5);\n// \texpect(v.midpoint([1, 2, 10, 20])[3]).toBe(10);\n// \texpect(v.midpoint([1]).y).toBe(1);\n// \texpect(v.midpoint([]).z).toBe(1.5);\n// });\n\n// test(\"transform\", () => {\n// \tconst v = ear.math.vector(1, 2);\n// \texpect(v.transform(1, 0, 0, 0, 1, 0, 0, 0, 1).x).toBe(1);\n// \texpect(v.transform(1, 0, 0, 0, 1, 0, 0, 0, 1).y).toBe(2);\n// \t// rotate around x\n// \texpect(v.transform(1, 0, 0, 0, 0, -1, 0, 1, 0).x).toBe(1);\n// \texpect(v.transform(1, 0, 0, 0, 0, -1, 0, 1, 0).y).toBe(0);\n// \texpect(v.transform(1, 0, 0, 0, 0, -1, 0, 1, 0).z).toBe(-2);\n// \t// rotate around z\n// \texpect(v.transform(0, -1, 0, 1, 0, 0, 0, 0, 1).x).toBe(2);\n// \texpect(v.transform(0, -1, 0, 1, 0, 0, 0, 0, 1).y).toBe(-1);\n// \texpect(v.transform(0, -1, 0, 1, 0, 0, 0, 0, 1).z).toBe(0);\n// \t// rotate 2D\n// \texpect(v.transform(0, -1, 1, 0).x).toBe(2);\n// \texpect(v.transform(0, -1, 1, 0).y).toBe(-1);\n// });\n\n// test(\"rotateZ\", () => {\n// \tconst v = ear.math.vector(1);\n// \texpect(v.rotateZ(Math.PI / 2).x).toBeCloseTo(0);\n// \texpect(v.rotateZ(Math.PI / 2).y).toBeCloseTo(1);\n// \texpect(v.rotateZ(-Math.PI / 2).x).toBeCloseTo(0);\n// \texpect(v.rotateZ(-Math.PI / 2).y).toBeCloseTo(-1);\n// });\n"
  },
  {
    "path": "tests/math.range.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"rangesOverlapExclusive\", () => {\n\t// default is exclusive\n\texpect(ear.math.doRangesOverlap([0, 1], [1, 2])).toBe(false);\n\t// negative epsilon is inclusive\n\texpect(ear.math.doRangesOverlap([0, 1], [1, 2], -1e-6)).toBe(true);\n\n\t// order does not matter\n\texpect(ear.math.doRangesOverlap([1, 0], [1, 2], 1e-6)).toBe(false);\n\texpect(ear.math.doRangesOverlap([1, 0], [2, 1], 1e-6)).toBe(false);\n\texpect(ear.math.doRangesOverlap([1, 0], [1, 2], -1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([1, 0], [2, 1], -1e-6)).toBe(true);\n\n\t// negative numbers\n\texpect(ear.math.doRangesOverlap([-10, -9], [-9, -8], 1e-6)).toBe(false);\n\texpect(ear.math.doRangesOverlap([-10, -9], [-9, -8], -1e-6)).toBe(true);\n\n\t// same ranges\n\texpect(ear.math.doRangesOverlap([2, 3], [2, 3], 1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([2, 3], [2, 3], -1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([2, 3], [3, 2], 1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([2, 3], [3, 2], -1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([-2, -3], [-2, -3], 1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([-2, -3], [-2, -3], -1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([-2, -3], [-3, -2], 1e-6)).toBe(true);\n\texpect(ear.math.doRangesOverlap([-2, -3], [-3, -2], -1e-6)).toBe(true);\n});\n"
  },
  {
    "path": "tests/prototypes.cp.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"deprecated\", () => expect(true).toBe(true));\n\n// test(\"methods\", () => {\n// \tconst cp = ear.cp();\n// \tcp.ray([Math.random(), Math.random()]);\n// \tconst two = cp.clone();\n// \texpect(two.edges_vertices.length).toBe(cp.edges_vertices.length);\n// \tcp.segment([Math.random(), Math.random()], [Math.random(), Math.random()]);\n// \texpect(two.edges_vertices.length).not.toBe(cp.edges_vertices.length);\n// });\n\n// test(\"cp draw methods\", () => {\n// \tconst cp = ear.cp();\n// \tcp.line([Math.random() - 0.5, Math.random() - 0.5], [0.5, 0.5]);\n// \tcp.segment([Math.random(), Math.random()], [Math.random(), Math.random()]);\n// \tcp.ray([Math.random(), Math.random()]);\n// \t// cp.circle(Math.random());\n// \t// cp.rect(Math.random(), Math.random(), Math.random(), Math.random());\n// });\n\n// test(\"cp draw methods\", () => {\n// \tconst cp = ear.cp();\n// \tconst l = cp.line([Math.random() - 0.5, Math.random() - 0.5], [0.5, 0.5]);\n// \texpect(cp.edges_assignment[cp.edges_assignment.length - 1]).toBe(\"U\");\n// \tl.mountain();\n// \texpect(cp.edges_assignment[cp.edges_assignment.length - 1]).toBe(\"M\");\n// \tl.valley();\n// \texpect(cp.edges_assignment[cp.edges_assignment.length - 1]).toBe(\"V\");\n// \tl.flat();\n// \texpect(cp.edges_assignment[cp.edges_assignment.length - 1]).toBe(\"F\");\n// });\n\n// test(\"cp.polygon\", () => {\n// \tconst cp = ear.cp.fish();\n// \tconst res = cp.polygon([[0, 0], [4, 1], [1, 4]]);\n// \tres.cut();\n// \texpect(cp.edges_assignment.reduce((a, b) => a + (b === \"C\" ? 1 : 0), 0))\n// \t\t.toBe(6);\n// });\n"
  },
  {
    "path": "tests/prototypes.graph.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"all methods that return the graph itself\", () => {\n\tconst graph = ear.graph.fish();\n\tconst result = graph\n\t\t.populate()\n\t\t.invertAssignments()\n\t\t// .transform([2, 0, 0, 0, 2, 0, 0, 0, 2, 1, 2, 3])\n\t\t.scaleUniform(2)\n\t\t.scale([1, 2, 3])\n\t\t.translate([1, 2])\n\t\t.rotate(Math.PI / 2, [0, 0, 1], [0.5, 0.5])\n\t\t.rotateZ(Math.PI / 2, [0.5, 0.5])\n\t\t.unitize();\n\texpect(result instanceof ear.graph).toBe(true);\n});\n\ntest(\"all methods that return something else\", () => {\n\tconst graph = ear.graph.fish();\n\n\tgraph.clean();\n\tgraph.subgraph({ vertices: [0, 1, 2, 3] });\n\tgraph.boundary();\n\tgraph.boundaries();\n\tgraph.planarBoundary();\n\tgraph.planarBoundaries();\n\tgraph.boundingBox();\n\tgraph.nearestVertex([0.5, 0.5]);\n\tgraph.nearestEdge([0.5, 0.5]);\n\tgraph.nearestFace([0.5, 0.5]);\n\tgraph.splitEdge(0, [0, 0.1]);\n\tgraph.svg();\n\tgraph.obj();\n\tgraph.explodeFaces();\n\tgraph.explodeEdges();\n\tgraph.validate();\n\n\texpect(true).toBe(true);\n});\n\ntest(\"copy\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n\t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n\t\tfaces_vertices: [[0, 1, 2], [2, 3, 0]],\n\t\tfaces_edges: [[0, 1, 4], [2, 3, 4]],\n\t});\n\tconst copy = graph.clone();\n\tgraph.vertices_coords = [];\n\texpect(copy.vertices_coords.length).toBe(4);\n\texpect(graph.vertices_coords.length).toBe(0);\n});\n\n// \"load\" has been removed for now.\n// test(\"load\", () => {\n//   const graph = ear.graph();\n//   // this shouldn't do anything, but also won't throw an error\n//   graph.load();\n//   // now, load.\n//   graph.load({\n//     vertices_coords: [[0,0], [1,0], [1,1], [0,1]],\n//     edges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n//     faces_vertices: [[0, 1, 2], [2, 3, 0]],\n//     faces_edges: [[0, 1, 4], [2, 3, 4]],\n//   });\n//   expect(graph.vertices_coords.length).toBe(4);\n//   expect(graph.vertices_coords[2][0]).toBe(1);\n//   expect(graph.vertices_coords[2][1]).toBe(1);\n// });\n\n// test(\"load, rewrite, and append\", () => {\n//   const graph = ear.graph();\n//   graph.load({\n//     edges_assignment: [\"V\", \"M\", \"U\", \"F\"],\n//   });\n//   expect(graph.edges_assignment[3]).toBe(\"F\");\n//   graph.load({\n//     edges_assignment: [\"U\", \"U\", \"U\", \"U\"],\n//   });\n//   expect(graph.edges_assignment[3]).toBe(\"U\");\n//   graph.load({\n//     vertices_coords: [[0.5, 0.5], [0.25, 0.25]],\n//   });\n//   // edges_assignment has been overwritten\n//   expect(graph.edges_assignment).toBe(undefined);\n//   expect(graph.vertices_coords[0][0]).toBe(0.5);\n//   // now load a graph but don't overwrite\n//   graph.load({\n//     edges_assignment: [\"U\", \"U\", \"U\", \"U\"],\n//   }, { append: true });\n//   expect(graph.edges_assignment[3]).toBe(\"U\");\n//   expect(graph.vertices_coords[0][0]).toBe(0.5);\n// });\n\n// test(\"clear\", () => {\n// \tconst graph = ear.graph({\n// \t\tvertices_coords: [[0, 0], [1, 0], [1, 1], [0, 1]],\n// \t\tedges_vertices: [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],\n// \t\tfaces_vertices: [[0, 1, 2], [2, 3, 0]],\n// \t\tfaces_edges: [[0, 1, 4], [2, 3, 4]],\n// \t});\n// \tgraph.clear();\n// \texpect(graph.vertices_coords).toBe(undefined);\n// \texpect(graph.edges_vertices).toBe(undefined);\n// \texpect(graph.faces_vertices).toBe(undefined);\n// \texpect(graph.faces_edges).toBe(undefined);\n// });\n\ntest(\"populate\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [\n\t\t\t[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [3, 1], [2, 1], [1, 1], [0, 1],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7],\n\t\t\t[7, 8], [8, 9], [9, 0], [1, 8], [2, 7], [3, 6],\n\t\t],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 90, 90],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"V\", \"V\"],\n\t});\n\n\tconst extra_keys = [\n\t\t\"vertices_edges\", \"vertices_faces\", \"vertices_vertices\",\n\t\t\"edges_vertices\", \"edges_faces\",\n\t\t\"faces_vertices\", \"faces_edges\", \"faces_faces\",\n\t];\n\t// calling graph() initializer now automatically runs populate();\n\t// extra_keys.forEach(key => expect(graph.vertices_vertices).toBe(undefined));\n\tgraph.populate();\n\textra_keys.forEach(key => expect(graph[key]).not.toBe(undefined));\n});\n\ntest(\"affine transforms\", () => {\n\tconst graph = ear.graph({\n\t\tvertices_coords: [\n\t\t\t[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [3, 1], [2, 1], [1, 1], [0, 1],\n\t\t],\n\t\tedges_vertices: [\n\t\t\t[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7],\n\t\t\t[7, 8], [8, 9], [9, 0], [1, 8], [2, 7], [3, 6],\n\t\t],\n\t\tedges_foldAngle: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 90, 90],\n\t\tedges_assignment: [\"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"B\", \"V\", \"V\", \"V\", \"V\"],\n\t});\n\tgraph.translate([4, 8]);\n\texpect(graph.vertices_coords[5][0]).toBe(8);\n\texpect(graph.vertices_coords[5][1]).toBe(9);\n\tgraph.scaleUniform(0.5);\n\texpect(graph.vertices_coords[5][0]).toBe(4);\n\texpect(graph.vertices_coords[5][1]).toBe(4.5);\n});\n"
  },
  {
    "path": "tests/prototypes.static.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// test(\"regular polygon bases\", () => {\n// \tconst names = [\n// \t\"triangle\", \"square\", \"pentagon\", \"hexagon\", \"heptagon\",\n// \t\"octagon\", \"nonagon\", \"decagon\", \"hendecagon\", \"dodecagon\",\n// ];\n// \tnames.forEach((name, i) => {\n// \t\tlet g = ear.graph[name]();\n// \t\texpect(g.vertices_coords.length).toBe(i + 3);\n// \t\texpect(g.edges_vertices.length).toBe(i + 3);\n// \t\texpect(g.faces_vertices.length).toBe(1);\n// \t});\n// });\n\n// test(\"other shapes\", () => {\n// \tconst circle = ear.graph.circle();\n// \texpect(circle.vertices_coords.length).toBe(90);\n// \tconst circle64 = ear.graph.circle(64);\n// \texpect(circle64.vertices_coords.length).toBe(64);\n// });\n\ntest(\"bases\", () => {\n\tconst kite = ear.graph.kite();\n\texpect(kite.vertices_coords.length).toBe(6);\n\texpect(kite.edges_vertices.length).toBe(9);\n\n\t// const bird = ear.graph.bird();\n});\n"
  },
  {
    "path": "tests/singleVertex.degree4.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"foldDegree4\", () => {\n\tconst foldAngle = Math.PI / 2;\n\tconst sixth = Math.PI / 3;\n\tconst result = ear.singleVertex.foldDegree4(\n\t\t[sixth * 1, sixth * 2, sixth * 2, sixth * 1],\n\t\t[\"M\", \"M\", \"V\", \"M\"],\n\t\tfoldAngle,\n\t);\n\tconst expected = [\n\t\t0.9272952180016123,\n\t\tfoldAngle,\n\t\t-0.9272952180016123,\n\t\tfoldAngle,\n\t];\n\tresult.forEach((res, i) => expect(res).toBeCloseTo(expected[i]));\n});\n\ntest(\"foldDegree4, does not worry about self intersection\", () => {\n\tconst seventh = (Math.PI * 2) / 7;\n\tconst assignments = [\n\t\t[\"V\", \"M\", \"M\", \"M\"],\n\t\t[\"M\", \"V\", \"M\", \"M\"],\n\t\t[\"M\", \"M\", \"V\", \"M\"],\n\t\t[\"M\", \"M\", \"M\", \"V\"],\n\t];\n\tconst result = assignments.map(assign => ear.singleVertex.foldDegree4(\n\t\t[seventh * 1, seventh * 2, seventh * 2, seventh * 2],\n\t\tassign,\n\t\tMath.PI / 2,\n\t));\n\tresult.forEach(res => {\n\t\texpect(res).not.toBe(undefined);\n\t\texpect(res.length).toBe(4);\n\t});\n});\n"
  },
  {
    "path": "tests/singleVertex.flatFoldable.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport fs from \"fs\";\nimport ear from \"../src/index.js\";\n\ntest(\"verticesFlatFoldabilityMaekawa\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityMaekawa(graphs[0]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 0, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityMaekawa(graphs[1]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, -2, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityMaekawa(graphs[2]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 0, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityMaekawa(graphs[3]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, -2, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityMaekawa(graphs[4]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 0, 0, 0, 0]);\n});\n\ntest(\"verticesFlatFoldabilityKawasaki\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityKawasaki(graphs[0])[5])\n\t\t.toBeCloseTo(0);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityKawasaki(graphs[1])[5])\n\t\t.toBeCloseTo(0);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityKawasaki(graphs[2])[5])\n\t\t.toBeCloseTo(-0.04060135259522912);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityKawasaki(graphs[3])[5])\n\t\t.toBeCloseTo(-0.04060135259522912);\n\n\texpect(ear.singleVertex.verticesFlatFoldabilityKawasaki(graphs[4])[5])\n\t\t.toBeCloseTo(0);\n});\n\ntest(\"verticesFlatFoldableMaekawa\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldableMaekawa(graphs[0]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableMaekawa(graphs[1]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableMaekawa(graphs[2]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableMaekawa(graphs[3]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableMaekawa(graphs[4]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n});\n\ntest(\"verticesFlatFoldableKawasaki\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldableKawasaki(graphs[0]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableKawasaki(graphs[1]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableKawasaki(graphs[2]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableKawasaki(graphs[3]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldableKawasaki(graphs[4]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n});\n\ntest(\"verticesFlatFoldability\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldability(graphs[0]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 0, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldability(graphs[1]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 1, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldability(graphs[2]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 2, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldability(graphs[3]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 3, 0, 0, 0]);\n\n\texpect(ear.singleVertex.verticesFlatFoldability(graphs[4]))\n\t\t.toMatchObject([0, 0, 0, 0, 0, 0, 0, 0, 0]);\n});\n\ntest(\"verticesFlatFoldable\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\texpect(ear.singleVertex.verticesFlatFoldable(graphs[0]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldable(graphs[1]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldable(graphs[2]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldable(graphs[3]))\n\t\t.toMatchObject([true, true, true, true, true, false, true, true, true]);\n\n\texpect(ear.singleVertex.verticesFlatFoldable(graphs[4]))\n\t\t.toMatchObject([true, true, true, true, true, true, true, true, true]);\n});\n"
  },
  {
    "path": "tests/singleVertex.foldable.test.js",
    "content": "import fs from \"fs\";\nimport { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"verticesFoldability\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\tear.singleVertex.verticesFoldability(graphs[0])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([0, 0, 0, 0, 0, 0, 0, 0, 0][i]));\n\n\tear.singleVertex.verticesFoldability(graphs[1])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([0, 0, 0, 0, 0, 0, 0, 0, 0][i]));\n\n\tear.singleVertex.verticesFoldability(graphs[2])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([0, 0, 0, 0, 0, 0.082828, 0, 0, 0][i]));\n\n\tear.singleVertex.verticesFoldability(graphs[3])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([0, 0, 0, 0, 0, 0.082828, 0, 0, 0][i]));\n\n\tear.singleVertex.verticesFoldability(graphs[4])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([0, 0, 0, 0, 0, 0, 0, 0, 0][i]));\n});\n\n\ntest(\"verticesFoldable\", () => {\n\tconst foldFile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-2d.fold\", \"utf-8\");\n\tconst fold = JSON.parse(foldFile);\n\tconst graphs = ear.graph.getFileFramesAsArray(fold);\n\n\tear.singleVertex.verticesFoldable(graphs[0])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([true, true, true, true, true, true, true, true, true][i]));\n\n\tear.singleVertex.verticesFoldable(graphs[1])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([true, true, true, true, true, true, true, true, true][i]));\n\n\tear.singleVertex.verticesFoldable(graphs[2])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([true, true, true, true, true, false, true, true, true][i]));\n\n\tear.singleVertex.verticesFoldable(graphs[3])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([true, true, true, true, true, false, true, true, true][i]));\n\n\tear.singleVertex.verticesFoldable(graphs[4])\n\t\t.forEach((value, i) => expect(value)\n\t\t\t.toBeCloseTo([true, true, true, true, true, true, true, true, true][i]));\n});\n\ntest(\"verticesFoldability\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/fold/invalid-box-pleat-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(testfile);\n\tconst verts = ear.singleVertex.verticesFoldability(graph);\n\n\t// one vertex is not foldable\n\t[0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0]\n\t\t.forEach((value, i) => expect(verts[i]).toBeCloseTo(value));\n});\n\ntest(\"verticesFoldable\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/fold/invalid-box-pleat-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(testfile);\n\tconst verts = ear.singleVertex.verticesFoldable(graph);\n\n\t// one vertex is not foldable\n\tconst expected = [\n\t\ttrue, true, true, true, true, true, true, false, true, true, true, true,\n\t];\n\texpected.forEach((valid, i) => expect(verts[i]).toBe(valid));\n});\n\ntest(\"verticesFoldable\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/fold/invalid-single-vertex-3d.fold\", \"utf-8\");\n\tconst graph = JSON.parse(testfile);\n\tconst verts = ear.singleVertex.verticesFoldable(graph);\n\n\t// expect(verts).toMatchObject([true, true, true, false, true, true]);\n\texpect(verts).toMatchObject([true, true, true, true, true, true]);\n});\n\ntest(\"verticesFoldable\", () => {\n\tconst testfile = fs.readFileSync(\"./tests/files/fold/invalid-self-intersect.fold\", \"utf-8\");\n\tconst graph = JSON.parse(testfile);\n\tconst verts = ear.singleVertex.verticesFoldable(graph);\n\n\t// expect(verts).toMatchObject([\n\t// \ttrue, true, true, true, true, false, true, true, true, true, true\n\t// ]);\n\texpect(verts).toMatchObject([\n\t\ttrue, true, true, true, true, true, true, true, true, true, true\n\t]);\n});\n"
  },
  {
    "path": "tests/singleVertex.kawasaki.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\nconst testArrays = (a, b) => {\n\ta.forEach((_, i) => {\n\t\tif (typeof a[i] === \"number\") {\n\t\t\texpect(a[i]).toBeCloseTo(b[i]);\n\t\t} else if (typeof a[i] === \"object\" && a[i].constructor === Array) {\n\t\t\texpect(ear.math.epsilonEqualVectors(a[i], b[i])).toBe(true);\n\t\t} else {\n\t\t\texpect(a[i]).toBe(b[i]);\n\t\t}\n\t});\n\texpect(a.length).toBe(b.length);\n};\n\ntest(\"alternating sum\", () => {\n\ttestArrays([0, 0], ear.singleVertex.alternatingSum([]));\n\ttestArrays([1, 0], ear.singleVertex.alternatingSum([1]));\n\ttestArrays([16, 20], ear.singleVertex.alternatingSum([1, 2, 3, 4, 5, 6, 7, 8]));\n});\n\ntest(\"alternating deviation, equal pairs\", () => {\n\tconst equal2 = [Math.PI, Math.PI];\n\tconst equal4 = Array(4).fill(Math.PI / 2);\n\ttestArrays([0, 0], ear.singleVertex.alternatingSumDifference(equal2));\n\ttestArrays([0, 0], ear.singleVertex.alternatingSumDifference(equal4));\n\ttestArrays(\n\t\t[0, 0],\n\t\tear.singleVertex.alternatingSumDifference(\n\t\t\tear.math.counterClockwiseSectors2([[1, 0], [0, 1], [-1, 0], [0, -1]]),\n\t\t),\n\t);\n});\n\ntest(\"alternating deviation, non-equal pairs\", () => {\n\t// two Math.PI +/- 1\n\tconst arr2 = Array(2).fill(Math.PI).map((v, i) => v + (i % 2 ? 1 : -1));\n\t// four Math.PI/2 +/- 1/2\n\tconst arr4 = Array(4).fill(Math.PI / 2).map((v, i) => v + (i % 2 ? 1 / 2 : -1 / 2));\n\ttestArrays([-1, 1], ear.singleVertex.alternatingSumDifference(arr2));\n\ttestArrays([-1, 1], ear.singleVertex.alternatingSumDifference(arr4));\n});\n\ntest(\"kawasaki solutions radians\", () => {\n\ttestArrays(\n\t\t[undefined, undefined, 1.25 * Math.PI],\n\t\tear.singleVertex.kawasakiSolutionsRadians([\n\t\t\t0, Math.PI / 2, (Math.PI / 4) * 3,\n\t\t]),\n\t);\n});\n\ntest(\"kawasaki solutions\", () => {\n\ttestArrays(\n\t\t[\n\t\t\t[Math.cos(Math.PI * (1 / 3)), Math.sin(Math.PI * (1 / 3))],\n\t\t\t[Math.cos(Math.PI * (3 / 3)), Math.sin(Math.PI * (3 / 3))],\n\t\t\t[Math.cos(Math.PI * (5 / 3)), Math.sin(Math.PI * (5 / 3))],\n\t\t],\n\t\tear.singleVertex.kawasakiSolutionsVectors([\n\t\t\t[Math.cos(0), Math.sin(0)],\n\t\t\t[Math.cos(Math.PI * (2 / 3)), Math.sin(Math.PI * (2 / 3))],\n\t\t\t[Math.cos(Math.PI * (4 / 3)), Math.sin(Math.PI * (4 / 3))],\n\t\t]),\n\t);\n\n\tconst sqrt05 = Math.sqrt(1 / 2);\n\ttestArrays(\n\t\t[undefined, undefined, [-sqrt05, -sqrt05]],\n\t\tear.singleVertex.kawasakiSolutionsVectors([\n\t\t\t[Math.cos(0), Math.sin(0)],\n\t\t\t[Math.cos(Math.PI / 4), Math.sin(Math.PI / 4)],\n\t\t\t[Math.cos(Math.PI / 2), Math.sin(Math.PI / 2)],\n\t\t]),\n\t);\n});\n"
  },
  {
    "path": "tests/singleVertex.maekawa.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"maekawaSolver, no U\", () => {\n\tconst result = ear.singleVertex.maekawaSolver(Array.from(\"MMVM\"));\n\texpect(result.length).toBe(1);\n\texpect(result[0].join(\"\")).toBe(\"MMVM\");\n});\n\ntest(\"maekawaSolver one solution\", () => {\n\tconst result = ear.singleVertex.maekawaSolver(Array.from(\"MMVMUVMV\"));\n\texpect(result.length).toBe(1);\n\texpect(result[0].join(\"\")).toBe(\"MMVMMVMV\");\n});\n\ntest(\"maekawaSolver two solutions\", () => {\n\tconst result = ear.singleVertex.maekawaSolver(Array.from(\"MMVUUVMV\"));\n\texpect(result.length).toBe(2);\n\texpect(result[0].join(\"\")).toBe(\"MMVVVVMV\");\n\texpect(result[1].join(\"\")).toBe(\"MMVMMVMV\");\n});\n\ntest(\"maekawaSolver with boundary\", () => {\n\tconst result = ear.singleVertex.maekawaSolver(Array.from(\"UUBV\"));\n\texpect(result.length).toBe(4);\n\texpect(result[0].join(\"\")).toBe(\"VVBV\");\n\texpect(result[3].join(\"\")).toBe(\"MMBV\");\n});\n\ntest(\"maekawaSolver with cut\", () => {\n\tconst result = ear.singleVertex.maekawaSolver(Array.from(\"UUCV\"));\n\texpect(result.length).toBe(4);\n\texpect(result[0].join(\"\")).toBe(\"VVCV\");\n\texpect(result[3].join(\"\")).toBe(\"MMCV\");\n});\n\ntest(\"maekawaSolver various\", () => {\n\t// if the set contains a boundary (\"B\" or \"C\"), then it returns all possible\n\t// permutations, including just the one. if it doesn't incude a boundary it\n\t// runs maekawa and if M-V=+/-2 is not satisfied it returns an empty array.\n\texpect(ear.singleVertex.maekawaSolver(Array.from(\"BBBB\"))[0].join(\"\")).toBe(\"BBBB\");\n\texpect(ear.singleVertex.maekawaSolver(Array.from(\"CCCC\"))[0].join(\"\")).toBe(\"CCCC\");\n\texpect(ear.singleVertex.maekawaSolver(Array.from(\"JJJJ\")).length).toBe(0);\n\texpect(ear.singleVertex.maekawaSolver(Array.from(\"FFFF\")).length).toBe(0);\n\texpect(ear.singleVertex.maekawaSolver(Array.from(\"BCJF\"))[0].join(\"\")).toBe(\"BCJF\");\n});\n"
  },
  {
    "path": "tests/svg.arrow.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\ntest(\"arrow\", () => {\n\t// svg.size(2.5, 1)\n\t//   .strokeWidth(0.01)\n\t//   .padding(0.25);\n\n\t// const options = {\n\t//   bend: 0.3,\n\t//   head: {\n\t//     width: 0.1,\n\t//     height: 0.13,\n\t//   },\n\t//   fill: \"red\",\n\t// };\n\n\t// const layer = svg.g();\n\n\t// svg.controls(2)\n\t//   .svg(() => svg.circle(0.04).fill(\"blue\"))\n\t//   .position(() => [Math.random(), Math.random()])\n\t//   .onChange((p, i, pts) => {\n\t//     layer.removeChildren();\n\t//     const arrow = layer.arrow(options)\n\t//       .points(...pts)\n\t//     arrow.getLine()\n\t//       .strokeWidth(0.03)\n\t//       .stroke(\"black\");\n\n\t//   }, true);\n\texpect(true).toBe(true);\n});\n\ntest(\"arrow object\", () => {\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/svg.attributes.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\nconst toCamel = s => s\n\t.replace(/([-_][a-z])/ig, $1 => $1\n\t\t.toUpperCase()\n\t\t.replace(\"-\", \"\")\n\t\t.replace(\"_\", \"\"));\n\nconst Attributes = {\n\tsvg: [\"viewBox\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tline: [\"x1\", \"y1\", \"x2\", \"y2\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\trect: [\"x\", \"y\", \"width\", \"height\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tcircle: [\"cx\", \"cy\", \"r\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tellipse: [\"cx\", \"cy\", \"rx\", \"ry\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tpolygon: [\"points\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tpolyline: [\"points\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tpath: [\"d\", \"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tmask: [\"id\"],\n\tsymbol: [\"id\"],\n\tclipPath: [\"id\", \"clip-rule\"],\n\tmarker: [\"id\", \"markerHeight\", \"markerUnits\", \"markerWidth\", \"orient\", \"refX\", \"refY\"],\n\tlinearGradient: [\"x1\", \"x2\", \"y1\", \"y2\"],\n\tradialGradient: [\"cx\", \"cy\", \"r\", \"fr\", \"fx\", \"fy\"],\n\tstop: [\"offset\", \"stop-color\", \"stop-opacity\", \"gradientTransform\", \"gradientUnits\", \"spreadMethod\"],\n\tpattern: [\"patternContentUnits\", \"patternTransform\", \"patternUnits\"],\n\tdefs: [\"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\tdesc: [],\n\tfilter: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tmetadata: [],\n\tstyle: [],\n\tscript: [],\n\ttitle: [],\n\tview: [],\n\tcdata: [],\n\tg: [\"color\", \"color-interpolation\", \"cursor\", \"direction\", \"display\", \"fill\", \"fill-opacity\", \"fill-rule\", \"font-family\", \"font-size\", \"font-size-adjust\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\", \"image-rendering\", \"letter-spacing\", \"opacity\", \"overflow\", \"paint-order\", \"pointer-events\", \"preserveAspectRatio\", \"shape-rendering\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"tabindex\", \"transform-origin\", \"user-select\", \"vector-effect\", \"visibility\"],\n\ttext: [\"x\", \"y\", \"dx\", \"dy\", \"alignment-baseline\", \"baseline-shift\", \"dominant-baseline\", \"lengthAdjust\", \"method\", \"overline-position\", \"overline-thickness\", \"rotate\", \"spacing\", \"startOffset\", \"strikethrough-position\", \"strikethrough-thickness\", \"text-anchor\", \"text-decoration\", \"text-rendering\", \"textLength\", \"underline-position\", \"underline-thickness\", \"word-spacing\", \"writing-mode\"],\n\ttextPath: [\"dx\", \"dy\", \"alignment-baseline\", \"baseline-shift\", \"dominant-baseline\", \"lengthAdjust\", \"method\", \"overline-position\", \"overline-thickness\", \"rotate\", \"spacing\", \"startOffset\", \"strikethrough-position\", \"strikethrough-thickness\", \"text-anchor\", \"text-decoration\", \"text-rendering\", \"textLength\", \"underline-position\", \"underline-thickness\", \"word-spacing\", \"writing-mode\"],\n\ttspan: [\"dx\", \"dy\", \"alignment-baseline\", \"baseline-shift\", \"dominant-baseline\", \"lengthAdjust\", \"method\", \"overline-position\", \"overline-thickness\", \"rotate\", \"spacing\", \"startOffset\", \"strikethrough-position\", \"strikethrough-thickness\", \"text-anchor\", \"text-decoration\", \"text-rendering\", \"textLength\", \"underline-position\", \"underline-thickness\", \"word-spacing\", \"writing-mode\"],\n\tfeBlend: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeColorMatrix: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeComponentTransfer: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeComposite: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeConvolveMatrix: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeDiffuseLighting: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeDisplacementMap: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeDistantLight: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeDropShadow: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeFlood: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeFuncA: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeFuncB: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeFuncG: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeFuncR: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeGaussianBlur: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeImage: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeMerge: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeMergeNode: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeMorphology: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeOffset: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfePointLight: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeSpecularLighting: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeSpotLight: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeTile: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n\tfeTurbulence: [\"azimuth\", \"baseFrequency\", \"bias\", \"color-interpolation-filters\", \"diffuseConstant\", \"divisor\", \"edgeMode\", \"elevation\", \"exponent\", \"filter\", \"filterRes\", \"filterUnits\", \"flood-color\", \"flood-opacity\", \"in\", \"in2\", \"intercept\", \"k1\", \"k2\", \"k3\", \"k4\", \"kernelMatrix\", \"lighting-color\", \"limitingConeAngle\", \"mode\", \"numOctaves\", \"operator\", \"order\", \"pointsAtX\", \"pointsAtY\", \"pointsAtZ\", \"preserveAlpha\", \"primitiveUnits\", \"radius\", \"result\", \"seed\", \"specularConstant\", \"specularExponent\", \"stdDeviation\", \"stitchTiles\", \"surfaceScale\", \"targetX\", \"targetY\", \"type\", \"xChannelSelector\", \"yChannelSelector\"],\n};\n\ntest(\"attributes test\", () => {\n\tObject.keys(Attributes).forEach(nodeName => {\n\t\tconst element = ear.svg[nodeName]();\n\n\t\tAttributes[nodeName]\n\t\t\t// .filter(attr => typeof element[attr] === \"function\")\n\t\t\t.forEach((attr, i) => element[toCamel(attr)](String(i)));\n\t\t// some attributes are being set without the user.\n\t\t// svg: \"xmlns\" and \"version\"\n\t\t// style: \"type\" (text/css)\n\t\tlet countMod = 0;\n\t\tswitch (nodeName) {\n\t\tcase \"svg\": countMod = 2; break;\n\t\tcase \"style\": countMod = 1; break;\n\t\tdefault: countMod = 0; break;\n\t\t}\n\t\texpect(element.attributes.length)\n\t\t\t.toBe(Attributes[nodeName].length + countMod);\n\t});\n});\n"
  },
  {
    "path": "tests/svg.circle.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"circle arguments\", () => {\n\texpect(ear.svg.circle(1).getAttribute(\"r\")).toBe(\"1\");\n\texpect(ear.svg.circle(5).getAttribute(\"r\")).toBe(\"5\");\n\texpect(ear.svg.circle(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle(2, 3).getAttribute(\"r\") === null\n\t\t|| ear.svg.circle(2, 3).getAttribute(\"r\") === \"\").toBe(true);\n\n\texpect(ear.svg.circle(1, 2, 3).getAttribute(\"cx\")).toBe(\"1\");\n\texpect(ear.svg.circle(1, 2, 3).getAttribute(\"cy\")).toBe(\"2\");\n\texpect(ear.svg.circle(1, 2, 3).getAttribute(\"r\")).toBe(\"3\");\n\n\texpect(parseFloat(ear.svg.circle(1, 2, 3, 4).getAttribute(\"r\")))\n\t\t.toBeCloseTo(2 * Math.sqrt(2));\n\texpect(ear.svg.circle(1, 2, 3, 4).getAttribute(\"cx\")).toBe(\"1\");\n\texpect(ear.svg.circle(1, 2, 3, 4).getAttribute(\"cy\")).toBe(\"2\");\n});\n\ntest(\"circle setters\", () => {\n\texpect(ear.svg.circle().radius(5).getAttribute(\"r\")).toBe(\"5\");\n\texpect(ear.svg.circle().setRadius(5).getAttribute(\"r\")).toBe(\"5\");\n\texpect(ear.svg.circle().origin(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().origin(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle().setOrigin(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().setOrigin(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle().center(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().center(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle().setCenter(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().setCenter(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle().position(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().position(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.circle().setPosition(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.circle().setPosition(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\n\tconst attrs = [\"cx\", \"cy\"];\n\tlet c = ear.svg.circle();\n\tc.setCenter(1, 2, 3, 4);\n\tattrs.forEach((attr, i) => expect(c.getAttribute(attr)).toBe(String([1, 2][i])));\n\texpect(c.attributes.length).toBe(2);\n\n\tc.setRadius(10);\n\tattrs.forEach((attr, i) => expect(c.getAttribute(attr)).toBe(String([1, 2][i])));\n\texpect(c.getAttribute(\"r\")).toBe(\"10\");\n\texpect(c.attributes.length).toBe(3);\n});\n"
  },
  {
    "path": "tests/svg.class.id.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"class\", () => {\n\tconst svg = ear.svg();\n\tif (typeof svg.classList === \"undefined\") {\n\t\texpect(true).toBe(true);\n\t\treturn;\n\t}\n\tsvg.classList.add(\"big\");\n\texpect(svg.getAttribute(\"class\")).toBe(\"big\");\n\tsvg.classList.add(\"medium\");\n\texpect(svg.getAttribute(\"class\")).toBe(\"big medium\");\n\tsvg.classList.remove(\"big\");\n\texpect(svg.getAttribute(\"class\")).toBe(\"medium\");\n\tsvg.className = \"small\";\n\texpect(svg.getAttribute(\"class\")).toBe(\"small\");\n\n\tconst line = ear.svg.line();\n\tline.id = \"five\";\n\texpect(line.getAttribute(\"id\")).toBe(\"five\");\n\n\tconst circle = ear.svg.circle();\n\tcircle.classList.remove(\"apple\");\n\texpect(circle.getAttribute(\"class\")).toBe(\"\");\n});\n"
  },
  {
    "path": "tests/svg.colors.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport ear from \"../src/index.js\";\n\n// anything involving hsl is within 1 unit\n\ntest(\"all conversions with alpha\", () => {\n\tconst rgbaValues = [17, 85, 136, 0.25];\n\tconst hslaValues = [206, 77.777, 30, 0.25];\n\tconst hex = \"#11558840\";\n\tconst rgb = \"rgba(17, 85, 136)\";\n\tconst rgba = \"rgba(17, 85, 136, 0.25)\";\n\tconst hsl = \"hsla(206, 77.777%, 30%)\";\n\tconst hsla = \"hsla(206, 77.777%, 30%, 0.25)\";\n\n\t// core conversion methods\n\texpect(ear.svg.rgbToHex(...rgbaValues)).toBe(hex);\n\tear.svg.hexToRgb(hex)\n\t\t.forEach((n, i) => expect(n).toBe(rgbaValues[i]));\n\tear.svg.hslToRgb(...hslaValues)\n\t\t.map((n, i) => Math.abs(n - rgbaValues[i]))\n\t\t.forEach(diff => expect(diff < 1).toBe(true));\n\n\t// parser methods\n\tear.svg.parseColorToRgb(hex)\n\t\t.forEach((n, i) => expect(n).toBe(rgbaValues[i]));\n\tear.svg.parseColorToRgb(rgb)\n\t\t.forEach((n, i) => expect(n).toBe(rgbaValues[i]));\n\tear.svg.parseColorToRgb(rgba)\n\t\t.forEach((n, i) => expect(n).toBe(rgbaValues[i]));\n\tear.svg.parseColorToRgb(hsl)\n\t\t.map((n, i) => Math.abs(n - rgbaValues[i]))\n\t\t.forEach(diff => expect(diff < 1).toBe(true));\n\tear.svg.parseColorToRgb(hsla)\n\t\t.map((n, i) => Math.abs(n - rgbaValues[i]))\n\t\t.forEach(diff => expect(diff < 1).toBe(true));\n\n\texpect(ear.svg.parseColorToHex(hex)).toBe(hex);\n\texpect(ear.svg.parseColorToHex(rgb)).toBe(\"#115588\");\n\texpect(ear.svg.parseColorToHex(rgba)).toBe(hex);\n\t// these are off by one. 54 instead of 55. unfortunately,\n\t// I think we just have to deal with this error in conversion.\n\texpect(ear.svg.parseColorToHex(hsl)).toBe(\"#115488\");\n\texpect(ear.svg.parseColorToHex(hsla)).toBe(\"#11548840\");\n\texpect(ear.svg.parseColorToHex(\"red\")).toBe(\"#FF0000\");\n\texpect(ear.svg.parseColorToHex(\"blue\")).toBe(\"#0000FF\");\n\texpect(ear.svg.parseColorToHex(\"\")).toBe(undefined);\n});\n\ntest(\"colors hexToRgb\", () => {\n\tconst rgb1 = ear.svg.hexToRgb(\"#158\");\n\tconst rgb2 = ear.svg.hexToRgb(\"#115588\");\n\texpect(JSON.stringify(rgb1)).toBe(JSON.stringify(rgb2));\n\t[17, 85, 136]\n\t\t.forEach((value, i) => expect(value).toBeCloseTo(rgb1[i]));\n});\n\ntest(\"colors hslToRgb\", () => {\n\tconst rgb1 = ear.svg.hslToRgb(0, 0, 100);\n\tconst rgb2 = ear.svg.hslToRgb(0, 100, 100);\n\texpect(JSON.stringify(rgb1)).toBe(JSON.stringify(rgb2));\n});\n\ntest(\"colors hslToRgb 2\", () => {\n\tconst colorR = ear.svg.hslToRgb(0, 100, 50);\n\t[255, 0, 0].forEach((value, i) => expect(value).toBeCloseTo(colorR[i]));\n\tconst colorG = ear.svg.hslToRgb(120, 100, 50);\n\t[0, 255, 0].forEach((value, i) => expect(value).toBeCloseTo(colorG[i]));\n\tconst colorB = ear.svg.hslToRgb(240, 100, 50);\n\t[0, 0, 255].forEach((value, i) => expect(value).toBeCloseTo(colorB[i]));\n});\n\ntest(\"parseColorToRgb\", () => {\n\tconst color1 = ear.svg.parseColorToRgb(\"red\");\n\tconst color2 = ear.svg.parseColorToRgb(\"#f00\");\n\tconst color3 = ear.svg.parseColorToRgb(\"#ff0000\");\n\tconst color4 = ear.svg.parseColorToRgb(\"rgb(255, 0, 0)\");\n\tconst color5 = ear.svg.parseColorToRgb(\"hsl(0, 100%, 50%)\");\n\tconst colorAlpha1 = ear.svg.parseColorToRgb(\"rgba(255, 0, 0, 1)\");\n\tconst colorAlpha2 = ear.svg.parseColorToRgb(\"hsla(0, 100%, 50%, 1)\");\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(color2));\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(color3));\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(color4));\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(color5));\n\texpect(JSON.stringify(colorAlpha1)).toBe(JSON.stringify(colorAlpha2));\n\texpect(JSON.stringify(color1)).not.toBe(JSON.stringify(colorAlpha1));\n});\n\ntest(\"empty parseColorToRgb\", () => {\n\tconst color1 = ear.svg.parseColorToRgb(\"rgb()\");\n\tconst color2 = ear.svg.parseColorToRgb(\"hsl()\");\n\tconst colorAlpha1 = ear.svg.parseColorToRgb(\"rgba()\");\n\tconst colorAlpha2 = ear.svg.parseColorToRgb(\"hsla()\");\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(color2));\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(colorAlpha1));\n\texpect(JSON.stringify(color1)).toBe(JSON.stringify(colorAlpha2));\n});\n\ntest(\"invalid parseColorToRgb\", () => {\n\tconst notacolor = ear.svg.parseColorToRgb(\"notacolor\");\n\texpect(notacolor).toBe(undefined);\n});\n"
  },
  {
    "path": "tests/svg.coordinates.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"\", () => expect(true).toBe(true));\n\n// test(\"coordinates, two points\", () => {\n// \tear.svg.makeCoordinates(1, 2, 3, 4)\n// \t\t.forEach((n, i) => expect(n).toBe([1, 2, 3, 4][i]));\n// \tear.svg.makeCoordinates([1, 2], [3, 4])\n// \t\t.forEach((n, i) => expect(n).toBe([1, 2, 3, 4][i]));\n// \tear.svg.makeCoordinates([1, 2, 3], [4, 5, 6])\n// \t\t.forEach((n, i) => expect(n).toBe([1, 2, 4, 5][i]));\n// \tear.svg.makeCoordinates([1], [2])\n// \t\t.forEach((n, i) => expect(n).toBe([1, undefined, 2, undefined][i]));\n// });\n\n// test(\"coordinates, not two points\", () => {\n// \texpect(ear.svg.makeCoordinates().length).toBe(0);\n// \texpect(ear.svg.makeCoordinates([]).length).toBe(0);\n// \texpect(ear.svg.makeCoordinates([[]]).length).toBe(0);\n// \texpect(ear.svg.makeCoordinates([], []).length).toBe(0);\n\n// \tear.svg.makeCoordinates([1, 2], [3, 4], [5, 6])\n// \t\t.forEach((n, i) => expect(n).toBe([1, 2, 3, 4, 5, 6][i]));\n// });\n"
  },
  {
    "path": "tests/svg.dom.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"removeChildren()\", () => {\n\tconst svg = ear.svg();\n\tsvg.line(1, 2, 3, 4);\n\texpect(svg.childNodes.length).toBe(1);\n\tsvg.removeChildren();\n\texpect(svg.childNodes.length).toBe(0);\n});\n\n// test(\"appendTo()\", () => {\n// \tconst svg = ear.svg();\n// \texpect(svg.childNodes.length).toBe(0);\n// \tconst line = ear.svg.line();\n// \tconst circle = ear.svg.circle();\n// \tconst ellipse = ear.svg.ellipse();\n// \tconst rect = ear.svg.rect();\n// \tconst path = ear.svg.path();\n// \tconst polygon = ear.svg.polygon();\n// \tconst polyline = ear.svg.polyline();\n// \tconst group = ear.svg.g();\n// \t[line, circle, ellipse, rect, path, polygon, polyline, group]\n// \t\t.forEach(primitive => primitive.appendTo(svg));\n// \texpect(svg.childNodes.length).toBe(8);\n// });\n\n// test(\"setAttributes()\", () => {\n// \tconst props = { a: \"10\", display: \"block\", style: \"color:red\" };\n// \tconst line = ear.svg.line();\n// \tline.setAttributes(props);\n// \texpect(line.getAttribute(\"display\")).toBe(\"block\");\n// \tconst group = ear.svg.g();\n// \tgroup.setAttributes(props);\n// \texpect(group.getAttribute(\"style\")).toBe(\"color:red\");\n// \tconst svg = ear.svg();\n// \tsvg.setAttributes(props);\n// \texpect(svg.getAttribute(\"display\")).toBe(\"block\");\n// });\n"
  },
  {
    "path": "tests/svg.ellipse.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"ellipse setters\", () => {\n\texpect(ear.svg.ellipse().radius(5, 6).getAttribute(\"rx\")).toBe(\"5\");\n\texpect(ear.svg.ellipse().radius(5, 6).getAttribute(\"ry\")).toBe(\"6\");\n\texpect(ear.svg.ellipse().setRadius(5, 6).getAttribute(\"rx\")).toBe(\"5\");\n\texpect(ear.svg.ellipse().setRadius(5, 6).getAttribute(\"ry\")).toBe(\"6\");\n\texpect(ear.svg.ellipse().origin(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().origin(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.ellipse().setOrigin(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().setOrigin(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.ellipse().center(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().center(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.ellipse().setCenter(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().setCenter(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.ellipse().position(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().position(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\texpect(ear.svg.ellipse().setPosition(2, 3).getAttribute(\"cx\")).toBe(\"2\");\n\texpect(ear.svg.ellipse().setPosition(2, 3).getAttribute(\"cy\")).toBe(\"3\");\n\t// incomplete info\n\texpect(ear.svg.ellipse().setRadius(5).getAttribute(\"rx\")).toBe(\"5\");\n\t// expect(ear.svg.ellipse().setRadius(5).getAttribute(\"ry\")).toBe(\"\");\n});\n"
  },
  {
    "path": "tests/svg.environment.test.js",
    "content": "import { expect, test } from \"vitest\";\n\ntest(\"environment\", () => {\n\texpect(true).toBe(true);\n\t// console.log(\"window\", window);\n\t// console.log(\"window.document\", window.document);\n});\n"
  },
  {
    "path": "tests/svg.line.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"argument parsing, line\", () => {\n\tconst lines = [\n\t\tear.svg.line(1, 2, 3, 4),\n\t\tear.svg.line([1, 2, 3, 4]),\n\t\tear.svg.line([[1, 2, 3, 4]]),\n\t\tear.svg.line([1, 2], [3, 4]),\n\t\tear.svg.line({ x: 1, y: 2 }, { x: 3, y: 4 }),\n\t\tear.svg.line([{ x: 1, y: 2 }, { x: 3, y: 4 }]),\n\t\tear.svg.line([1, 2, 9], [3, 4, 9]),\n\t\tear.svg.line([[1, 2, 9], [3, 4, 9]]),\n\t\tear.svg.line({ x: 1, y: 2, z: 9 }, { x: 3, y: 4, z: 9 }),\n\t];\n\t// ear.svg.line([1], [2], [3], [4]),\n\t// ear.svg.line([{x:1, y:2}], [{x:3, y:4}]),\n\t// ear.svg.line([[{x:1, y:2}], [{x:3, y:4}]]),\n\tconst result = lines\n\t\t.map(el => [\"x1\", \"y1\", \"x2\", \"y2\"]\n\t\t\t.map(attr => el.getAttribute(attr))\n\t\t\t.map((value, i) => value === String(i + 1))\n\t\t\t.reduce((a, b) => a && b, true))\n\t\t.reduce((a, b) => a && b, true);\n\texpect(result).toBe(true);\n});\n\ntest(\"line arguments, missing arguments\", () => {\n\tconst result1 = ear.svg.line(1, 2, 3);\n\texpect(result1.getAttribute(\"x2\")).toBe(\"3\");\n\texpect(result1.getAttribute(\"y2\")).toBe(\"\");\n\n\tconst result2 = ear.svg.line({ x: 1, y: 2 }, { x: 3 });\n\texpect(result2.getAttribute(\"x2\")).toBe(\"3\");\n\t// expect(result2.getAttribute(\"y2\")).toBe(\"\");\n});\n\ntest(\"line setters\", () => {\n\tconst attrs = [\"x1\", \"y1\", \"x2\", \"y2\"];\n\n\tlet l = ear.svg.line();\n\tl.setPoints(1, 2, 3, 4);\n\tattrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([1, 2, 3, 4][i])));\n\n\tl.setPoints([[1, 2, 3, 4]]);\n\tattrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([1, 2, 3, 4][i])));\n\n\tl.setPoints([[1, 2], [3, 4]]);\n\tattrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([1, 2, 3, 4][i])));\n\texpect(l.attributes.length).toBe(4);\n\n\t// this will not work\n\tl.setPoints([[1, 2], [3, 4], 5, [6, 7]]);\n\t// attrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([1, 2, 3, 4][i])));\n\t// expect(l.attributes.length).toBe(4);\n\n\t// this will not work\n\tl.setPoints(\"9\", \"8\", \"7\", \"6\");\n\t// attrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([1, 2, 3, 4][i])));\n\n\tl.setPoints({ x: 5, y: 6 }, { x: 7, y: 8 }, { x: 9, y: 10 });\n\tattrs.forEach((attr, i) => expect(l.getAttribute(attr)).toBe(String([5, 6, 7, 8][i])));\n\texpect(l.attributes.length).toBe(4);\n});\n"
  },
  {
    "path": "tests/svg.nodes.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\nconst NodeAndChildren = {\n\tsvg: [\"svg\", \"defs\", \"desc\", \"filter\", \"metadata\", \"style\", \"script\", \"title\", \"view\", \"linearGradient\", \"radialGradient\", \"pattern\", \"marker\", \"symbol\", \"clipPath\", \"mask\", \"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n\tg: [\"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n\t// text: [\"textPath\", \"tspan\"], // text has another \"text\" child node that doesn't match\n\tlinearGradient: [\"stop\"],\n\tradialGradient: [\"stop\"],\n\tdefs: [\"desc\", \"filter\", \"metadata\", \"style\", \"script\", \"title\", \"view\", \"linearGradient\", \"radialGradient\", \"pattern\", \"marker\", \"symbol\", \"clipPath\", \"mask\"],\n\tfilter: [\"feBlend\", \"feColorMatrix\", \"feComponentTransfer\", \"feComposite\", \"feConvolveMatrix\", \"feDiffuseLighting\", \"feDisplacementMap\", \"feDistantLight\", \"feDropShadow\", \"feFlood\", \"feFuncA\", \"feFuncB\", \"feFuncG\", \"feFuncR\", \"feGaussianBlur\", \"feImage\", \"feMerge\", \"feMergeNode\", \"feMorphology\", \"feOffset\", \"fePointLight\", \"feSpecularLighting\", \"feSpotLight\", \"feTile\", \"feTurbulence\"],\n\tmarker: [\"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n\tsymbol: [\"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n\tclipPath: [\"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n\tmask: [\"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\", \"text\"],\n};\n\nconst NodesNames = [\n\t\"svg\",\n\t\"line\",\n\t\"rect\",\n\t\"circle\",\n\t\"ellipse\",\n\t\"polygon\",\n\t\"polyline\",\n\t\"path\",\n\t\"mask\",\n\t\"symbol\",\n\t\"clipPath\",\n\t\"marker\",\n\t\"linearGradient\",\n\t\"radialGradient\",\n\t\"stop\",\n\t\"pattern\",\n\t\"defs\",\n\t\"desc\",\n\t\"filter\",\n\t\"metadata\",\n\t\"style\",\n\t\"script\",\n\t\"title\",\n\t\"view\",\n\t\"cdata\",\n\t\"g\",\n\t\"text\",\n\t\"textPath\",\n\t\"tspan\",\n\t\"feBlend\",\n\t\"feColorMatrix\",\n\t\"feComponentTransfer\",\n\t\"feComposite\",\n\t\"feConvolveMatrix\",\n\t\"feDiffuseLighting\",\n\t\"feDisplacementMap\",\n\t\"feDistantLight\",\n\t\"feDropShadow\",\n\t\"feFlood\",\n\t\"feFuncA\",\n\t\"feFuncB\",\n\t\"feFuncG\",\n\t\"feFuncR\",\n\t\"feGaussianBlur\",\n\t\"feImage\",\n\t\"feMerge\",\n\t\"feMergeNode\",\n\t\"feMorphology\",\n\t\"feOffset\",\n\t\"fePointLight\",\n\t\"feSpecularLighting\",\n\t\"feSpotLight\",\n\t\"feTile\",\n\t\"feTurbulence\",\n];\n\ntest(\"node test\", () => {\n\tconst svg = ear.svg();\n\tObject.keys(NodeAndChildren).forEach(parent => {\n\t\tconst node = svg[parent]();\n\t\tNodeAndChildren[parent].forEach(child => node[child]());\n\t\texpect(node.childNodes.length).toBe(NodeAndChildren[parent].length);\n\t\tNodeAndChildren[parent].forEach((child, i) => expect(node.childNodes[i].nodeName)\n\t\t\t.toBe(child));\n\t});\n\texpect(svg.childNodes.length).toBe(Object.keys(NodeAndChildren).length);\n\tObject.keys(NodeAndChildren).forEach((child, i) => expect(svg.childNodes[i].nodeName)\n\t\t.toBe(child));\n});\n\ntest(\"node names\", () => {\n\tconst createElements = NodesNames.map(nodeName => ear.svg[nodeName]())\n\t\t.map((node, i) => node.nodeName === NodesNames[i])\n\t\t.reduce((a, b) => a && b, true);\n\texpect(createElements).toBe(true);\n});\n"
  },
  {
    "path": "tests/svg.object.assign.test.js",
    "content": "import { expect, test } from \"vitest\";\n\nconst test1 = {\n\ta: {\n\t\tone: {\n\t\t\twhat: true,\n\t\t},\n\t\ttwo: {\n\t\t\twhatagain: true,\n\t\t},\n\t},\n\tb: {\n\t\ta: {\n\t\t\twhat: true,\n\t\t},\n\t},\n};\n\nconst test2 = {\n\ta: {\n\t\tthree: {\n\t\t\thi: true,\n\t\t},\n\t\tfour: {\n\t\t\twhatagain: true,\n\t\t},\n\t},\n\tb: {\n\t\tb: {\n\t\t\tokay: true,\n\t\t},\n\t},\n};\n\ntest(\"object assign\", () => {\n\tObject.assign(test1, test2);\n\n\texpect(test1.a.one == null).toBe(true);\n\texpect(test1.a.two == null).toBe(true);\n\texpect(test1.a.three != null).toBe(true);\n\texpect(test1.a.four != null).toBe(true);\n\texpect(test1.b.a == null).toBe(true);\n\texpect(test1.b.b != null).toBe(true);\n});\n"
  },
  {
    "path": "tests/svg.path.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"path\", () => {\n\tconst p = ear.svg.path();\n\tp.Move(20, 20);\n\texpect(p.getAttribute(\"d\")).toBe(\"M20 20\");\n\tp.line(50, 50);\n\texpect(p.getAttribute(\"d\")).toBe(\"M20 20l50 50\");\n\tp.vertical(30);\n\texpect(p.getAttribute(\"d\")).toBe(\"M20 20l50 50v30\");\n\tp.Curve(50, 0, 0, 50, 10, 10);\n\texpect(p.getAttribute(\"d\")).toBe(\"M20 20l50 50v30C50 0 0 50 10 10\");\n\tp.clear();\n\t// specification around getAttribute when it doesn't exist is \"\" or null\n\texpect(p.getAttribute(\"d\") === \"\" || p.getAttribute(\"d\") === null).toBe(true);\n});\n\nconst path_commands = [\n\t\"m\", // move\n\t\"l\", // line\n\t\"v\", // vertical\n\t\"h\", // horizontal\n\t\"a\", // ellipse\n\t\"c\", // curve\n\t\"s\", // smoothCurve\n\t\"q\", // quadCurve\n\t\"t\", // smoothQuadCurve\n\t\"z\", // close\n];\n\ntest(\"path init from args\", () => {\n\tconst pathString = \"M20 40V60l10 10\";\n\texpect(ear.svg.path(pathString).getAttribute(\"d\")).toBe(pathString);\n//  expect(ear.svg.path({command:\"M\", values: [20, 40]}).getAttribute(\"d\")).toBe(\"M20 40\");\n});\n\ntest(\"path commands\", () => {\n\tconst pathString = \"M20 40V60l10 10\";\n\tconst path = ear.svg.path(pathString);\n\tpath.Line(50, 50);\n\texpect(path.getAttribute(\"d\")).toBe(`${pathString}L50 50`);\n\n\tconst commands = path.getCommands();\n\tconst expected = [\n\t\t{ command: \"M\", values: [20, 40] },\n\t\t{ command: \"V\", values: [60] },\n\t\t{ command: \"l\", values: [10, 10] },\n\t\t{ command: \"L\", values: [50, 50] },\n\t];\n\texpected.forEach((el, i) => {\n\t\texpect(commands[i].command).toBe(expected[i].command);\n\t\texpect(JSON.stringify(commands[i].values))\n\t\t\t.toBe(JSON.stringify(expected[i].values));\n\t});\n\n\t// path.add(\"H20V60\");\n\t// expect(path.getAttribute(\"d\")).toBe(pathString + \"L50 50\" + \"H20V60\");\n\n\t// path.set(\"H20V60\");\n\t// expect(path.getAttribute(\"d\")).toBe(\"H20V60\");\n});\n\ntest(\"path commands\", () => {\n\tconst path = ear.svg.path(\"M50 50h200\");\n\texpect(path.getD()).toBe(\"M50 50h200\");\n\tconst path2 = ear.svg.path();\n\texpect(path2.getD()).toBe(\"\");\n});\n\ntest(\"path commands\", () => {\n\tconst path = ear.svg.path(\"M50 50\");\n\tpath.addCommand(\"L\", 100, 100);\n\texpect(path.getAttribute(\"d\")).toBe(\"M50 50L100 100\");\n\tpath.appendCommand(\"V\", 66);\n\texpect(path.getAttribute(\"d\")).toBe(\"M50 50L100 100V66\");\n\tpath.clear();\n\texpect(path.getAttribute(\"d\") === null || path.getAttribute(\"d\") === \"\").toBe(true);\n});\n\n// test(\"bezier\", () => {\n//   let bez = ear.svg.bezier(0, 0, 25, 75, 75, 25, 100, 100);\n//   const d = Array.from(bez.attributes).filter(a => a.nodeName === \"d\").shift();\n//   expect(d.nodeValue).toBe(\"M 0,0 C 25,75 75,25 100,100\");\n//   bez.setBezier(0, 0, 100, 0, 100, 100, 0, 100);\n//   const d2 = Array.from(bez.attributes).filter(a => a.nodeName === \"d\").shift();\n//   expect(d2.nodeValue).toBe(\"M 0,0 C 100,0 100,100 0,100\");\n//   expect(true).toBe(true);\n// });\n"
  },
  {
    "path": "tests/svg.polygon.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"argument parsing, polygon\", () => {\n\tlet poly = ear.svg.polygon([0, 0, 0, 1, 1, 1, 1, 0]);\n\texpect(poly.getAttribute(\"points\")).toBe(\"0,0 0,1 1,1 1,0\");\n\tconst polygons = [\n\t\tear.svg.polygon(0, 1, 2, 3, 4, 5),\n\t\tear.svg.polygon([0, 1], [2, 3], [4, 5]),\n\t\tear.svg.polygon([[0, 1], [2, 3], [4, 5]]),\n\t\tear.svg.polygon([[0, 1]], [[2, 3]], [[4, 5]]),\n\t\tear.svg.polygon({ x: 0, y: 1 }, { x: 2, y: 3 }, { x: 4, y: 5 }),\n\t\tear.svg.polygon([{ x: 0, y: 1 }, { x: 2, y: 3 }, { x: 4, y: 5 }]),\n\t\tear.svg.polygon([0, 1, 9], [2, 3, 9], [4, 5, 9]),\n\t\tear.svg.polygon([[0, 1, 9], [2, 3, 9], [4, 5, 9]]),\n\t\tear.svg.polygon([[0, 1, 9]], [[2, 3, 9]], [[4, 5, 9]]),\n\t\tear.svg.polygon({ x: 0, y: 1, z: 9 }, { x: 2, y: 3, z: 9 }, { x: 4, y: 5, z: 9 }),\n\t\t// ear.svg.polygon([{x:0, y:1}], [{x:2, y:3}], [{x:4, y:5}]),\n\t\t// ear.svg.polygon([[{x:0, y:1}], [{x:2, y:3}], [{x:4, y:5}]]),\n\t];\n\tconst result = polygons\n\t\t.map(p => p.getAttribute(\"points\"))\n\t\t.map(points => points === \"0,1 2,3 4,5\")\n\t\t.reduce((a, b) => a && b, true);\n\texpect(result).toBe(true);\n});\n\ntest(\"polygon point methods\", () => {\n\tconst poly = ear.svg.polygon([0, 0, 0, 1, 1, 1, 1, 0]);\n\texpect(poly.getAttribute(\"points\")).toBe(\"0,0 0,1 1,1 1,0\");\n\tpoly.addPoint(6, 7);\n\texpect(poly.getAttribute(\"points\")).toBe(\"0,0 0,1 1,1 1,0 6,7\");\n\tpoly.addPoint([3, 4]);\n\texpect(poly.getAttribute(\"points\")).toBe(\"0,0 0,1 1,1 1,0 6,7 3,4\");\n});\n"
  },
  {
    "path": "tests/svg.primitives.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"svg and group\", () => {\n\tconst svg = ear.svg();\n\tsvg.appendChild(ear.svg.g());\n\texpect(svg.childNodes.length).toBe(1);\n});\n\ntest(\"all primitives\", () => {\n\tconst group = ear.svg.g();\n\tgroup.appendChild(ear.svg.line(0, 1, 2, 3));\n\tgroup.appendChild(ear.svg.rect(0, 1, 2, 3));\n\tgroup.appendChild(ear.svg.circle(0, 1, 2));\n\tgroup.appendChild(ear.svg.ellipse(0, 1, 2));\n\tgroup.appendChild(ear.svg.polygon([[0, 1], [2, 3], [4, 5]]));\n\tgroup.appendChild(ear.svg.polyline([[0, 1], [2, 3], [4, 5]]));\n\tgroup.appendChild(ear.svg.text(\"abc\", 0, 1));\n\tconst nodeNames = [\"line\", \"rect\", \"circle\", \"ellipse\", \"polygon\", \"polyline\", \"text\"];\n\tArray.from(group.childNodes).forEach((el, i) => {\n\t\texpect(el.nodeName).toBe(nodeNames[i]);\n\t});\n\texpect(group.childNodes.length).toBe(7);\n});\n\ntest(\"init from SVG\", () => {\n\tconst svg = ear.svg();\n\tsvg.line(0, 1, 2, 3);\n\tsvg.rect(0, 1, 2, 3);\n\tsvg.circle(0, 1, 2);\n\tsvg.ellipse(0, 1, 2);\n\tsvg.polygon([[0, 1], [2, 3], [4, 5]]);\n\tsvg.polyline([[0, 1], [2, 3], [4, 5]]);\n\tsvg.text(\"abc\", 0, 1);\n\tconst nodeNames = [\"line\", \"rect\", \"circle\", \"ellipse\", \"polygon\", \"polyline\", \"text\"];\n\tArray.from(svg.childNodes).forEach((el, i) => {\n\t\texpect(el.nodeName).toBe(nodeNames[i]);\n\t});\n\texpect(svg.childNodes.length).toBe(7);\n});\n\ntest(\"method chaining\", () => {\n\tconst svg = ear.svg();\n\texpect(svg.line().setPoints(1, 2, 3, 4).getAttribute(\"x1\")).toBe(\"1\");\n\texpect(svg.rect().setSize(3, 4).setOrigin(1, 2).getAttribute(\"width\")).toBe(\"3\");\n\texpect(svg.circle().setCenter(3, 4).getAttribute(\"cx\")).toBe(\"3\");\n\texpect(svg.ellipse().setRadius(1, 2).getAttribute(\"rx\")).toBe(\"1\");\n\texpect(svg.polygon().setPoints(1, 2, 3, 4).getAttribute(\"points\")).toBe(\"1,2 3,4\");\n\texpect(svg.polyline().setPoints(1, 2, 3, 4).getAttribute(\"points\")).toBe(\"1,2 3,4\");\n\t// expect(svg.text().setPoints(1, 2, 3, 4).getAttribute(\"x\")).toBe(\"1\")\n});\n\ntest(\"primitives, argument order\", () => {\n\tconst line0 = ear.svg.line();\n\tconst line1 = ear.svg.line(1);\n\tconst line2 = ear.svg.line(1, 2);\n\tconst line3 = ear.svg.line(1, 2, 3);\n\tconst line4 = ear.svg.line(1, 2, 3, 4);\n\texpect(line0.getAttribute(\"x1\") === null || line0.getAttribute(\"x1\") === \"\")\n\t\t.toBe(true);\n\texpect(line1.getAttribute(\"x1\") === null || line1.getAttribute(\"x1\") === \"\")\n\t\t.toBe(false);\n\texpect(line1.getAttribute(\"y1\") === null || line1.getAttribute(\"y1\") === \"\")\n\t\t.toBe(true);\n\texpect(line2.getAttribute(\"y1\") === null || line2.getAttribute(\"y1\") === \"\")\n\t\t.toBe(false);\n\texpect(line2.getAttribute(\"x2\") === null || line2.getAttribute(\"x2\") === \"\")\n\t\t.toBe(true);\n\texpect(line3.getAttribute(\"x2\") === null || line3.getAttribute(\"x2\") === \"\")\n\t\t.toBe(false);\n\texpect(line3.getAttribute(\"y2\") === null || line3.getAttribute(\"y2\") === \"\")\n\t\t.toBe(true);\n\texpect(line4.getAttribute(\"y2\") === null || line4.getAttribute(\"y2\") === \"\")\n\t\t.toBe(false);\n\n\tconst rect0 = ear.svg.rect();\n\tconst rect1 = ear.svg.rect(1);\n\tconst rect2 = ear.svg.rect(1, 2);\n\tconst rect3 = ear.svg.rect(1, 2, 3);\n\tconst rect4 = ear.svg.rect(1, 2, 3, 4);\n\texpect(rect0.getAttribute(\"x\") === null || rect0.getAttribute(\"x\") === \"\")\n\t\t.toBe(true);\n\texpect(rect0.getAttribute(\"width\") === null || rect0.getAttribute(\"width\") === \"\")\n\t\t.toBe(true);\n\texpect(rect1.getAttribute(\"x\") === null || rect1.getAttribute(\"x\") === \"\")\n\t\t.toBe(true);\n\texpect(rect1.getAttribute(\"y\") === null || rect1.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect1.getAttribute(\"width\") === null || rect1.getAttribute(\"width\") === \"\")\n\t\t.toBe(false);\n\texpect(rect1.getAttribute(\"height\") === null || rect1.getAttribute(\"height\") === \"\")\n\t\t.toBe(true);\n\texpect(rect2.getAttribute(\"x\") === null || rect2.getAttribute(\"x\") === \"\")\n\t\t.toBe(true);\n\texpect(rect2.getAttribute(\"y\") === null || rect2.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect2.getAttribute(\"width\") === null || rect2.getAttribute(\"width\") === \"\")\n\t\t.toBe(false);\n\texpect(rect2.getAttribute(\"height\") === null || rect2.getAttribute(\"height\") === \"\")\n\t\t.toBe(false);\n\texpect(rect3.getAttribute(\"x\") === null || rect3.getAttribute(\"x\") === \"\")\n\t\t.toBe(true);\n\texpect(rect3.getAttribute(\"y\") === null || rect3.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect3.getAttribute(\"width\") === null || rect3.getAttribute(\"width\") === \"\")\n\t\t.toBe(false);\n\texpect(rect3.getAttribute(\"height\") === null || rect3.getAttribute(\"height\") === \"\")\n\t\t.toBe(false);\n\texpect(rect4.getAttribute(\"x\") === null || rect4.getAttribute(\"x\") === \"\")\n\t\t.toBe(false);\n\texpect(rect4.getAttribute(\"y\") === null || rect4.getAttribute(\"y\") === \"\")\n\t\t.toBe(false);\n\texpect(rect4.getAttribute(\"width\") === null || rect4.getAttribute(\"width\") === \"\")\n\t\t.toBe(false);\n\texpect(rect4.getAttribute(\"height\") === null || rect4.getAttribute(\"height\") === \"\")\n\t\t.toBe(false);\n\n\tconst circle0 = ear.svg.circle();\n\tconst circle1 = ear.svg.circle(1);\n\tconst circle2 = ear.svg.circle(1, 2);\n\tconst circle3 = ear.svg.circle(1, 2, 3);\n\texpect(circle0.getAttribute(\"cx\") === null || circle0.getAttribute(\"cx\") === \"\")\n\t\t.toBe(true);\n\texpect(circle0.getAttribute(\"cy\") === null || circle0.getAttribute(\"cy\") === \"\")\n\t\t.toBe(true);\n\texpect(circle0.getAttribute(\"r\") === null || circle0.getAttribute(\"r\") === \"\")\n\t\t.toBe(true);\n\texpect(circle1.getAttribute(\"cx\") === null || circle1.getAttribute(\"cx\") === \"\")\n\t\t.toBe(true);\n\texpect(circle1.getAttribute(\"cy\") === null || circle1.getAttribute(\"cy\") === \"\")\n\t\t.toBe(true);\n\texpect(circle1.getAttribute(\"r\") === null || circle1.getAttribute(\"r\") === \"\")\n\t\t.toBe(false);\n\texpect(circle2.getAttribute(\"cx\") === null || circle2.getAttribute(\"cx\") === \"\")\n\t\t.toBe(false);\n\texpect(circle2.getAttribute(\"cy\") === null || circle2.getAttribute(\"cy\") === \"\")\n\t\t.toBe(false);\n\texpect(circle2.getAttribute(\"r\") === null || circle2.getAttribute(\"r\") === \"\")\n\t\t.toBe(true);\n\texpect(circle3.getAttribute(\"cx\") === null || circle3.getAttribute(\"cx\") === \"\")\n\t\t.toBe(false);\n\texpect(circle3.getAttribute(\"cy\") === null || circle3.getAttribute(\"cy\") === \"\")\n\t\t.toBe(false);\n\texpect(circle3.getAttribute(\"r\") === null || circle3.getAttribute(\"r\") === \"\")\n\t\t.toBe(false);\n\n\tconst ellipse1 = ear.svg.ellipse(1);\n\tconst ellipse2 = ear.svg.ellipse(1, 2);\n\tconst ellipse3 = ear.svg.ellipse(1, 2, 3);\n\tconst ellipse4 = ear.svg.ellipse(1, 2, 3, 4);\n\tconst text1s = ear.svg.text(\"abc\", 0, 1);\n});\n\n// test(\"custom primitives\", () => {\n//   const group = ear.svg.g();\n//   group.appendChild(ear.svg.bezier(0, 1, 2, 3, 4, 5, 6, 7));\n//   group.appendChild(ear.svg.arc(0, 1, 2, 3, 4));\n//   group.appendChild(ear.svg.wedge(0, 1, 2, 3, 4));\n//   group.appendChild(ear.svg.arcEllipse(0, 1, 2, 3, 4, 5));\n//   group.appendChild(ear.svg.wedgeEllipse(0, 1, 2, 3, 4, 5));\n//   group.appendChild(ear.svg.arrow([0, 1], [2, 3]));\n//   const nodeNames = [\"path\", \"path\", \"path\", \"path\", \"path\", \"g\"];\n//   Array.from(group.childNodes).forEach((el, i) => {\n//     expect(el.nodeName).toBe(nodeNames[i]);\n//   });\n//   expect(group.childNodes.length).toBe(6);\n// });\n"
  },
  {
    "path": "tests/svg.rect.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"rect\", () => {\n\tconst rect = ear.svg.rect(1, 2, 3, 4);\n\texpect(rect.getAttribute(\"x\")).toBe(\"1\");\n\texpect(rect.getAttribute(\"y\")).toBe(\"2\");\n\texpect(rect.getAttribute(\"width\")).toBe(\"3\");\n\texpect(rect.getAttribute(\"height\")).toBe(\"4\");\n\n\tconst rect2 = ear.svg.rect(3, 4);\n\texpect(rect2.getAttribute(\"y\") === null || rect2.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect2.getAttribute(\"y\") === null || rect2.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect2.getAttribute(\"width\")).toBe(\"3\");\n\texpect(rect2.getAttribute(\"height\")).toBe(\"4\");\n});\n\ntest(\"rect, args, negative width height\", () => {\n\tconst rect = ear.svg.rect(300, 200, -300, -200);\n\texpect(rect.getAttribute(\"x\")).toBe(\"0\");\n\texpect(rect.getAttribute(\"y\")).toBe(\"0\");\n\texpect(rect.getAttribute(\"width\")).toBe(\"300\");\n\texpect(rect.getAttribute(\"height\")).toBe(\"200\");\n\tconst rect2 = ear.svg.rect(300, 200, -320, -220);\n\texpect(rect2.getAttribute(\"x\")).toBe(\"-20\");\n\texpect(rect2.getAttribute(\"y\")).toBe(\"-20\");\n\texpect(rect2.getAttribute(\"width\")).toBe(\"320\");\n\texpect(rect2.getAttribute(\"height\")).toBe(\"220\");\n\tconst rect3 = ear.svg.rect(300, 200, 320, -220);\n\texpect(rect3.getAttribute(\"x\")).toBe(\"300\");\n\texpect(rect3.getAttribute(\"y\")).toBe(\"-20\");\n\texpect(rect3.getAttribute(\"width\")).toBe(\"320\");\n\texpect(rect3.getAttribute(\"height\")).toBe(\"220\");\n\tconst rect4 = ear.svg.rect(300, 200, -320, 220);\n\texpect(rect4.getAttribute(\"x\")).toBe(\"-20\");\n\texpect(rect4.getAttribute(\"y\")).toBe(\"200\");\n\texpect(rect4.getAttribute(\"width\")).toBe(\"320\");\n\texpect(rect4.getAttribute(\"height\")).toBe(\"220\");\n\tconst rect5 = ear.svg.rect(-320, -220);\n\texpect(rect5.getAttribute(\"x\")).toBe(\"-320\");\n\texpect(rect5.getAttribute(\"y\")).toBe(\"-220\");\n\texpect(rect5.getAttribute(\"width\")).toBe(\"320\");\n\texpect(rect5.getAttribute(\"height\")).toBe(\"220\");\n\tconst rect6 = ear.svg.rect(-320, 220);\n\texpect(rect6.getAttribute(\"x\")).toBe(\"-320\");\n\texpect(rect6.getAttribute(\"y\") === null || rect6.getAttribute(\"y\") === \"\")\n\t\t.toBe(true);\n\texpect(rect6.getAttribute(\"width\")).toBe(\"320\");\n\texpect(rect6.getAttribute(\"height\")).toBe(\"220\");\n});\n"
  },
  {
    "path": "tests/svg.stylesheet.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"style\", () => {\n\tconst styleString = \"line{stroke:purple};\";\n\tconst svg = ear.svg();\n\tsvg.stylesheet(styleString);\n\tconst style = Array.from(svg.childNodes).filter(a => a.nodeName === \"style\").shift();\n\texpect(style.childNodes[0].textContent).toBe(styleString);\n});\n\ntest(\"style setTextContent\", () => {\n\tconst styleString = \"line{stroke:purple};\";\n\tconst styleString2 = \"circle { fill: '#000' }\";\n\tconst svg = ear.svg();\n\tconst stylesheet = svg.stylesheet(styleString);\n\tstylesheet.setTextContent(styleString2);\n\tconst style = Array.from(svg.childNodes).filter(a => a.nodeName === \"style\").shift();\n\texpect(style.childNodes[0].textContent).toBe(styleString2);\n});\n"
  },
  {
    "path": "tests/svg.svg.animation.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"animation\", () => {\n\tconst svg = ear.svg();\n\tsvg.play = e => {};\n\tsvg.stop();\n});\n"
  },
  {
    "path": "tests/svg.svg.args.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\nconst { DOMParser } = xmldom;\n\ntest(\"argument parsing, svg\", () => {\n\tconst svg0 = ear.svg();\n\tconst vb0 = svg0.getAttribute(\"viewBox\");\n\texpect(vb0 == null || vb0 === \"\").toBe(true);\n\n\tconst svg1 = ear.svg(400, 500);\n\texpect(svg1.getAttribute(\"viewBox\")).toBe(\"0 0 400 500\");\n\n\tconst svg2 = ear.svg(1, 2, 400, 500);\n\texpect(svg2.getAttribute(\"viewBox\")).toBe(\"1 2 400 500\");\n\n\tconst svg3 = ear.svg(1, 400, 500);\n\tconst vb3 = svg3.getAttribute(\"viewBox\");\n\texpect(vb3 == null || vb3 === \"\").toBe(true);\n});\n\ntest(\"no parent\", () => {\n\tconst svg = ear.svg(600, 600);\n\texpect(svg.parentNode).toBe(null);\n});\n\ntest(\"parent element\", () => {\n\tconst parent = (new DOMParser()).parseFromString(\"<div></div>\", \"text/xml\").documentElement;\n\tconst svg = ear.svg(600, 600, parent);\n\texpect(svg.parentNode.nodeName).toBe(\"div\");\n});\n\n// test(\"svg embed as string\", () => {\n// \t// this string contains \\n newlines, the load method targets and removes them\n// \t// by testing the <line> element at [0] this is also testing the newline removal\n// \tconst svgString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n// \t<line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/>\n// </svg>`;\n// \tconst svg = ear.svg(svgString);\n// \texpect(svg.childNodes.length).toBe(1);\n// \texpect(svg.childNodes[0].nodeName).toBe(\"line\");\n// });\n"
  },
  {
    "path": "tests/svg.svg.background.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"set background\", () => {\n\tconst svg = ear.svg();\n\tsvg.background(\"black\", true);\n\tsvg.background(\"#332698\", false);\n\texpect(svg.childNodes.length).toBe(1);\n});\n"
  },
  {
    "path": "tests/svg.svg.controls.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"deprecated\", () => expect(true).toBe(true));\n\n// test(\"controls\", () => {\n// \tconst svg = ear.svg();\n// \tconst controlLayer = svg.g();\n// \tsvg.controls(3)\n// \t\t.svg(() => ear.svg.circle(svg.getWidth() * 0.1).fill(\"gray\"))\n// \t\t.position(() => [svg.getWidth() * 0.5, svg.getHeight() * 0.5])\n// \t\t.parent(controlLayer)\n// \t\t.onChange((point) => {\n// \t\t\tpoint.svg.setRadius(Math.random() * 10);\n// \t\t}, true);\n// });\n"
  },
  {
    "path": "tests/svg.svg.load.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"\", () => expect(true).toBe(true));\n\n// const path = \"./tests/dragon.svg\";\n\n// const testSVG = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 300 150\">\n// <circle cx=\"150\" cy=\"75\" r=\"75\"/>\n// </svg>`;\n\n// test(\"loading callback\", done => {\n// \tear.svg(400, 400, () => {\n// \t\tdone();\n// \t});\n// });\n\n// // test(\"async attempt promises\", () => {\n// //   const svg = ear.svg();\n// //   svg.load(path)\n// //     .then(() => {});\n// //   expect(svg.childNodes.length).toBe(0);\n// // });\n\n// test(\"async using fs\", (done) => {\n// \tconst svg = ear.svg();\n// \tfs.readFile(path, { encoding: \"utf8\" }, (err, data) => {\n// \t\tsvg.load(data);\n// \t\tconst polyline = Array.from(svg.childNodes)\n// \t\t\t.filter(a => a.nodeName === \"polyline\")\n// \t\t\t.shift();\n// \t\texpect(polyline !== undefined).toBe(true);\n// \t\tdone();\n// \t});\n// });\n\n// test(\"async attempt with node\", () => {\n// \tconst svg = ear.svg();\n// \tsvg.load(path);\n// \texpect(svg.childNodes.length).toBe(0);\n// });\n\n// test(\"sync\", () => {\n// \tconst svg = ear.svg();\n// \tsvg.load(testSVG);\n// \tconst circle = Array.from(svg.childNodes)\n// \t\t.filter(a => a.nodeName === \"circle\")\n// \t\t.shift();\n\n// \texpect(circle !== undefined).toBe(true);\n// });\n\n// test(\"import load()\", () => {\n// \tconst svgString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"><line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/></svg>`;\n\n// \tconst svgFromString = ear.svg();\n// \texpect(svgFromString.childNodes.length).toBe(0);\n// \tsvgFromString.load(svgString);\n// \texpect(svgFromString.childNodes.length).toBe(1);\n\n// \t// console.log(\"svgFromString.childNodes\", svgFromString.childNodes);\n// \t// console.log(\"window\", window);\n// \t// console.log(\"window.document\", window.document);\n// });\n\n// test(\"import string in argument\", () => {\n// \tconst svgString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"><line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/></svg>`;\n\n// \tconst svg = ear.svg(svgString);\n// \texpect(svg.childNodes.length).toBe(1);\n\n// \t// console.log(\"svg.childNodes\", svg.childNodes);\n// \t// console.log(\"window\", window);\n// \t// console.log(\"window.document\", window.document);\n// });\n\n// test(\"import string in argument with attributes\", () => {\n// \tconst svgString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"10 20 800 600\" display=\"none\"></svg>`;\n// \tconst svg = ear.svg(svgString);\n// \texpect(svg.getAttribute(\"viewBox\")).toBe(\"10 20 800 600\");\n// \texpect(svg.getAttribute(\"display\")).toBe(\"none\");\n// });\n"
  },
  {
    "path": "tests/svg.svg.save.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"\", () => expect(true).toBe(true));\n\n// test(\"export options\", () => {\n// \tconst svg = ear.svg();\n// \tconst save0 = svg.save();\n// \tconst save1 = svg.save({ output: \"string\" });\n// \tconst save2 = svg.save({ output: \"svg\" });\n// \tconst save3 = svg.save({ windowStyle: true });\n// \texpect(typeof save0).toBe(\"string\");\n// \texpect(typeof save1).toBe(\"string\");\n// \texpect(typeof save2).toBe(\"object\");\n// \texpect(typeof save3).toBe(\"string\");\n// });\n\n// test(\"svg export\", () => {\n// \tconst svg = ear.svg();\n// \tsvg.line(0, 0, 300, 150).stroke(\"black\").strokeWidth(5);\n// \tconst asString = svg.save();\n// \tconst asSvg = svg.save({ output: \"svg\" });\n// \tconst expectedString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n// \t<line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/>\n// </svg>`;\n// \t// const expectedString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"><line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/></svg>`;\n// \texpect(asString).toBe(expectedString);\n// \texpect(asSvg.childNodes.length).toBe(1);\n// \texpect(asSvg.childNodes[0].nodeName).toBe(\"line\");\n// });\n\n// test(\"svg export with comments\", () => {\n// \tconst svgString = `<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n// \t<!-- this is a comment-->\n// \t\t<line x1=\"0\" y1=\"0\" x2=\"300\" y2=\"150\" stroke=\"black\" stroke-width=\"5\"/>\n// \t<!--a comment with <xml> things <inside/> </inside>< ></ >< / > it-->\n// </svg>`;\n// \tconst svg = ear.svg(svgString);\n// \tconst asSvg = svg.save({ output: \"svg\" });\n// \texpect(asSvg.childNodes.length).toBe(3);\n// \texpect(asSvg.childNodes[0].nodeName).toBe(\"#comment\");\n// \texpect(asSvg.childNodes[1].nodeName).toBe(\"line\");\n// \texpect(asSvg.childNodes[2].nodeName).toBe(\"#comment\");\n// });\n"
  },
  {
    "path": "tests/svg.svg.viewbox.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"viewBox get and set\", () => {\n\tconst svg = ear.svg();\n\tsvg.setViewBox(1, 2, 3, 4);\n\texpect(svg.getAttribute(\"viewBox\")).toBe(\"1 2 3 4\");\n\tsvg.setViewBox(\"-10 -10 400 500\");\n\texpect(svg.getAttribute(\"viewBox\")).toBe(\"-10 -10 400 500\");\n\tsvg.setViewBox(300, 200);\n\texpect(svg.getAttribute(\"viewBox\")).toBe(\"0 0 300 200\");\n});\n\ntest(\"get width and height\", () => {\n\tconst svg = ear.svg();\n\texpect(svg.getWidth()).toBe(undefined);\n\texpect(svg.getHeight()).toBe(undefined);\n\tconst svg2 = ear.svg(400, 300);\n\texpect(svg2.getWidth()).toBe(400);\n\texpect(svg2.getHeight()).toBe(300);\n\tconst svg3 = ear.svg(0, 0, 800, 600);\n\texpect(svg3.getWidth()).toBe(800);\n\texpect(svg3.getHeight()).toBe(600);\n});\n"
  },
  {
    "path": "tests/svg.transforms.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"parseTransform\", () => {\n\tconst transformString = \"translate(20 100) rotate(45) scale(2 1) matrix(0 -1 1 0 0 0)\";\n\tconst result = ear.svg.parseTransform(transformString);\n\t[\"translate\", \"rotate\", \"scale\", \"matrix\"]\n\t\t.forEach((name, i) => expect(result[i].transform).toBe(name));\n});\n\ntest(\"transformStringToMatrix\", () => {\n\tconst transformString = \"translate(20 100) rotate(45) scale(2 1) matrix(0 -1 1 0 0 0)\";\n\texpect(ear.svg.transformStringToMatrix(transformString).length).toBe(6);\n\n\tconst transformString2 = \"translate(20) skewX(4) rotate(45 2 3) skewY(2) scale(2)\";\n\texpect(ear.svg.transformStringToMatrix(transformString2).length).toBe(6);\n});\n\ntest(\"transformStringToMatrix bad input\", () => {\n\tconst transformString = \"translate() rotate() scale() skewX() skewY()\";\n\texpect(ear.svg.transformStringToMatrix(transformString).length).toBe(6);\n});\n\ntest(\"transforms\", () => {\n\tconst svg = ear.svg();\n\n\tconst transformString = \"translate(20 100) rotate(45) translate(50 50) matrix(0 -1 1 0 0 0)\";\n\n\t[\"svg\", \"g\", \"circle\", \"ellipse\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\"]\n\t// , \"text\"\n\t\t.map(node => svg[node]()\n\t\t\t.translate(\"20 100\")\n\t\t\t.rotate(45)\n\t\t\t.translate(50, 50)\n\t\t\t.matrix(0, -1, 1, 0, 0, 0))\n\t\t.forEach(p => expect(p.getAttribute(\"transform\"))\n\t\t\t.toBe(transformString));\n});\n\ntest(\"clear transform\", () => {\n\tconst svg = ear.svg();\n\tconst l = svg.line(0, 0, 400, 400)\n\t\t.rotate(45)\n\t\t.translate(50, 50)\n\t\t.clearTransform();\n\tconst transform = l.getAttribute(\"transform\");\n\texpect(transform == null || transform === \"\").toBe(true);\n});\n"
  },
  {
    "path": "tests/svg.types.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\nconst primitives = [\n\t\"line\",\n\t\"circle\",\n\t\"ellipse\",\n\t\"rect\",\n\t\"polygon\",\n\t\"polyline\",\n\t\"text\",\n];\n\nconst groupLevel = [\"g\"];\n\nconst defsLevel = [\n\t\"style\",\n\t\"clipPath\",\n\t\"mask\",\n\t\"script\",\n];\n\nconst rootLevel = [\n\t\"defs\",\n\t\"style\",\n\t// \"clipPath\", // conflict, clipPath is a constructor AND an assigner\n\t// \"mask\",\n];\n\nconst customPrimitives = [\n\t\"bezier\",\n\t\"wedge\",\n\t\"arc\",\n\t\"parabola\",\n\t\"regularPolygon\",\n\t\"arrow\",\n];\n\ntest(\"svg and group\", () => {\n\tconst svg = ear.svg();\n\tprimitives.forEach(p => expect(typeof svg[p]).toBe(\"function\"));\n\tgroupLevel.forEach(g => expect(typeof svg[g]).toBe(\"function\"));\n\trootLevel.forEach(r => expect(typeof svg[r]).toBe(\"function\"));\n\n\tconst group = ear.svg.g();\n\tprimitives.forEach(p => expect(typeof group[p]).toBe(\"function\"));\n\tgroupLevel.forEach(g => expect(typeof group[g]).toBe(\"function\"));\n\trootLevel.forEach(r => expect(typeof group[r]).not.toBe(\"function\"));\n\n\tconst defs = ear.svg.defs();\n\t// primitives.forEach(p => expect(typeof defs[p]).toBe(\"function\"));\n\t// groupLevel.forEach(g => expect(typeof defs[g]).toBe(\"function\"));\n\t// defsLevel.forEach(r => expect(typeof defs[r]).toBe(\"function\"));\n});\n"
  },
  {
    "path": "tests/svg.urls.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"custom id names\", () => {\n\t[\"clipPath\", \"symbol\", \"mask\", \"marker\"].forEach(nodeName => {\n\t\tconst svg = ear.svg();\n\t\tconst test1 = svg[nodeName]();\n\t\tconst test2 = svg[nodeName](\"what is\");\n\t\texpect(test1.getAttribute(\"id\").length).toBe(5);\n\t\texpect(test2.getAttribute(\"id\")).toBe(\"what is\");\n\t});\n});\n\ntest(\"assign to types\", () => {\n\tconst svg = ear.svg();\n\tconst things = [\"clipPath\", \"symbol\", \"mask\", \"marker\", \"marker\", \"marker\"]\n\t\t.map(nodeName => svg[nodeName]());\n\tconst line = svg.line(1, 2, 3, 4);\n\t[\"clipPath\", \"mask\", \"symbol\", \"markerEnd\", \"markerMid\", \"markerStart\"]\n\t\t.forEach((method, i) => line[method](things[i]));\n\n\t// these should be the attributes\n\t[\"x1\", \"y1\", \"x2\", \"y2\", \"clip-path\", \"mask\", \"symbol\", \"marker-end\", \"marker-mid\", \"marker-start\"]\n\t\t.forEach((attr, i) => expect(line.attributes[i].name).toBe(attr));\n});\n"
  },
  {
    "path": "tests/svg.use.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n// const ear = require(\"./ear\");\n\ntest(\"\", () => expect(true).toBe(true));\n\n// test(\"use with rabbit ear\", () => {\n// \tear.use(SVG);\n// \texpect(typeof ear.segment.svg).toBe(\"function\");\n// \texpect(typeof ear.segment.svg).toBe(\"function\");\n// \texpect(typeof ear.circle.svg).toBe(\"function\");\n// \texpect(typeof ear.ellipse.svg).toBe(\"function\");\n// \texpect(typeof ear.rect.svg).toBe(\"function\");\n// \texpect(typeof ear.polygon.svg).toBe(\"function\");\n// \tear.segment.svg();\n// \tear.circle.svg();\n// \tear.ellipse.svg();\n// \tear.rect.svg();\n// \tear.polygon.svg();\n// });\n"
  },
  {
    "path": "tests/svg.window.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.svg.window = xmldom;\n\ntest(\"environment\", () => {\n\texpect(true).toBe(true);\n});\n"
  },
  {
    "path": "tests/webgl.initialize.test.js",
    "content": "import { expect, test } from \"vitest\";\nimport xmldom from \"@xmldom/xmldom\";\nimport ear from \"../src/index.js\";\n\near.window = xmldom;\n\ntest(\"webgl cannot be tested server side\", () => expect(true).toBe(true));\n\n// here is a test that can be done in the browser.\n\n// test(\"initialize webgl\", () => {\n// \tconst FOLD = ear.graph.bird();\n\n// \tconst canvas = window.document.createElement(\"canvas\");\n// \tdocument.body.appendChild(canvas);\n\n// \t// gl is the WebGL context. version is either 1 or 2.\n// \tconst { gl, version } = ear.webgl.initializeWebGL(canvas);\n\n// \t// Initialize a WebGL viewport based on the dimensions of the canvas\n// \tear.webgl.rebuildViewport(gl, canvas);\n\n// \t// draw creasePattern style, or foldedForm style\n// \tconst models = ear.webgl.creasePattern(gl, version, FOLD);\n// \t// program = ear.webgl.foldedForm(gl, version, FOLD);\n\n// \tconst projectionMatrix = ear.webgl.makeProjectionMatrix(canvas, \"orthographic\");\n// \tconst modelViewMatrix = ear.webgl.makeModelMatrix(FOLD);\n\n// \tgl.enable(gl.BLEND);\n// \tgl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n// \tgl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n// \tmodels.forEach(model => {\n// \t\tear.webgl.drawModel(gl, version, model, model.makeUniforms(gl, {\n// \t\t\tprojectionMatrix,\n// \t\t\tmodelViewMatrix\n// \t\t}));\n// \t});\n\n// \tmodels.forEach(model => ear.webgl.deallocModel(gl, model));\n// });\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"include\": [\"src/**/*\"],\n\t\"compilerOptions\": {\n\t\t// changes the first line of index.d.ts to declare a module name\n\t\t\"esModuleInterop\": true,\n\t\t\"target\": \"ES2022\", // \"ES2015\",\n\t\t\"module\": \"nodenext\", // \"es2015\",\n\t\t\"lib\": [\"ES2022\", \"DOM\"], // [\"es2015\", \"DOM\"],\n\t\t\"moduleResolution\": \"nodenext\", // \"node\",\n\t\t// Tells TypeScript to read JS files, as\n\t\t// normally they are ignored as source files\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t// Generate d.ts files\n\t\t\"declaration\": true,\n\t\t// Types should go into this directory.\n\t\t// Removing this would place the .d.ts files\n\t\t// next to the .js files\n\t\t\"declarationDir\": \"./types\", // \"./module\"\n\t\t\"outDir\": \"./types\", // \"./module\"\n\t\t\"rootDir\": \"./src\",\n\t\t// This compiler run should\n\t\t// only output d.ts files\n\t\t\"emitDeclarationOnly\": true,\n\t\t// go to js file when using IDE functions like\n\t\t// \"Go to Definition\" in VSCode\n\t\t\"declarationMap\": true\n\t}\n}\n"
  },
  {
    "path": "typedoc.config.json",
    "content": "{\n\t\"entryPoints\": [\"./src\"],\n\t\"out\": \"docs\",\n\t\"name\": \"Rabbit Ear\",\n\t\"includeVersion\": true,\n\t\"entryPointStrategy\": \"expand\",\n\t\"exclude\": [\"**/index.js\"], // \"**/*.d.ts\"\n\t\"customCss\": \"docs-style.css\"\n}\n"
  }
]