[
  {
    "path": ".devcontainer/Containerfile",
    "content": "ARG VARIANT=bookworm\nFROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}\n\nENV DEBIAN_FRONTEND=noninteractive\nRUN sudo apt-get update \\\n && sudo apt-get -y install --no-install-recommends \\\n      clang \\\n      python3-venv \\\n      udev\n\n## Set up udev rules for PlatformIO\nRUN curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules\nRUN service udev restart \nRUN usermod -a -G dialout vscode\nRUN usermod -a -G plugdev vscode\n\nARG FEDORA_COMPAT=0\n### Set up compatibility with Fedora host (if needed)\n### Since Fedora uses `18` as the group for dialout, we need to add it to the container\nRUN if [ \"$FEDORA_COMPAT\" = \"1\" ]; then \\\n  sudo groupadd -g 18 compat_dialout; \\\n  sudo usermod -a -G compat_dialout vscode; \\\nfi\n\n\n# Install PlatformIO CLI\nUSER vscode\nRUN curl -fsSL -o /tmp/get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py\nRUN python3 /tmp/get-platformio.py\nRUN echo 'export PATH=\"$PATH:$HOME/.platformio/penv/bin\"' | tee -a /home/vscode/.bashrc /home/vscode/.zshrc \nRUN echo 'export PATH=\"$PATH:$HOME/.platformio/penv/bin\"' | sudo tee -a /root/.bashrc /root/.zshrc\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n\t\"name\": \"PlatformIO (Community)\",\n\t\"build\": {\n\t\t\"dockerfile\": \"Containerfile\",\n\t\t\"context\": \".\",\n\t\t\"args\": {\n\t\t\t\"FEDORA_COMPAT\" : \"1\", // enables fedora compatibility mode (extra dialout group with gid 18)\n\t\t\t\"VARIANT\": \"bookworm\"\n\t\t}\n\t},\n\t\"customizations\": {\n\t\t\"vscode\": {\n\t\t\t\"settings\": {\n\t\t\t\t\"terminal.integrated.defaultProfile.linux\": \"zsh\"\n\t\t\t},\n\t\t\t\"extensions\": [\n\t\t\t\t\"ms-vscode.cpptools\",\n\t\t\t\t\"platformio.platformio-ide\"\n\t\t\t]\n\t\t}\n\t},\n\t\"forwardPorts\": [\n\t\t8008\n\t],\n\t\"mounts\": [\n\t\t\"source=/dev/,target=/dev/,type=bind,consistency=consistent\"\n\t],\n\t\"runArgs\": [\n\t\t\"--privileged\"\n\t],\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/features/node:1\": {\n    \t\t\"version\": \"lts\"\n    \t},\n\t\t\"ghcr.io/devcontainers/features/git-lfs:1\": \"latest\"\n\t},\n\t\"postAttachCommand\": \"sudo service udev restart\"\n}"
  },
  {
    "path": ".gitignore",
    "content": ".pio\ndata/www/worker/*\ntsc/node_modules\ntsc/package-lock.json\ntsc/dist\ntsc/dist_packed\ntsc/svgs/*\n.DS_Store\n.vscode/c_cpp_properties.json"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    // See http://go.microsoft.com/fwlink/?LinkId=827846\n    // for the documentation about the extensions.json format\n    \"recommendations\": [\n        \"platformio.platformio-ide\"\n    ],\n    \"unwantedRecommendations\": [\n        \"ms-vscode.cpptools-extension-pack\"\n    ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "// AUTOMATICALLY GENERATED FILE. PLEASE DO NOT MODIFY IT MANUALLY\n//\n// PlatformIO Debugging Solution\n//\n// Documentation: https://docs.platformio.org/en/latest/plus/debugging.html\n// Configuration: https://docs.platformio.org/en/latest/projectconf/sections/env/options/debug/index.html\n\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Launch SVG Tester\",\n            \"program\": \"${workspaceFolder}/tsc/dist/tester.js\",\n            \"request\": \"launch\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"type\": \"node\",\n            \"preLaunchTask\": \"tsc: build - tsc/tsconfig.json\",\n            \"env\": {\n                \"server\": \"true\"\n            }\n        },\n        {\n            \"type\": \"platformio-debug\",\n            \"request\": \"launch\",\n            \"name\": \"PIO Debug\",\n            \"executable\": \"/workspaces/mural/.pio/build/esp32dev/firmware.elf\",\n            \"projectEnvName\": \"esp32dev\",\n            \"toolchainBinDir\": \"/home/vscode/.platformio/packages/toolchain-xtensa-esp32/bin\",\n            \"internalConsoleOptions\": \"openOnSessionStart\",\n            \"preLaunchTask\": {\n                \"type\": \"PlatformIO\",\n                \"task\": \"Pre-Debug\"\n            }\n        },\n        {\n            \"type\": \"platformio-debug\",\n            \"request\": \"launch\",\n            \"name\": \"PIO Debug (skip Pre-Debug)\",\n            \"executable\": \"/workspaces/mural/.pio/build/esp32dev/firmware.elf\",\n            \"projectEnvName\": \"esp32dev\",\n            \"toolchainBinDir\": \"/home/vscode/.platformio/packages/toolchain-xtensa-esp32/bin\",\n            \"internalConsoleOptions\": \"openOnSessionStart\"\n        },\n        {\n            \"type\": \"platformio-debug\",\n            \"request\": \"launch\",\n            \"name\": \"PIO Debug (without uploading)\",\n            \"executable\": \"/workspaces/mural/.pio/build/esp32dev/firmware.elf\",\n            \"projectEnvName\": \"esp32dev\",\n            \"toolchainBinDir\": \"/home/vscode/.platformio/packages/toolchain-xtensa-esp32/bin\",\n            \"internalConsoleOptions\": \"openOnSessionStart\",\n            \"loadMode\": \"manual\"\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"files.associations\": {\n        \"*.tcc\": \"cpp\",\n        \"array\": \"cpp\",\n        \"atomic\": \"cpp\",\n        \"bitset\": \"cpp\",\n        \"cctype\": \"cpp\",\n        \"clocale\": \"cpp\",\n        \"cmath\": \"cpp\",\n        \"cstdarg\": \"cpp\",\n        \"cstddef\": \"cpp\",\n        \"cstdint\": \"cpp\",\n        \"cstdio\": \"cpp\",\n        \"cstdlib\": \"cpp\",\n        \"cstring\": \"cpp\",\n        \"ctime\": \"cpp\",\n        \"cwchar\": \"cpp\",\n        \"cwctype\": \"cpp\",\n        \"deque\": \"cpp\",\n        \"unordered_map\": \"cpp\",\n        \"unordered_set\": \"cpp\",\n        \"vector\": \"cpp\",\n        \"exception\": \"cpp\",\n        \"algorithm\": \"cpp\",\n        \"functional\": \"cpp\",\n        \"iterator\": \"cpp\",\n        \"map\": \"cpp\",\n        \"memory\": \"cpp\",\n        \"memory_resource\": \"cpp\",\n        \"numeric\": \"cpp\",\n        \"optional\": \"cpp\",\n        \"random\": \"cpp\",\n        \"regex\": \"cpp\",\n        \"string\": \"cpp\",\n        \"string_view\": \"cpp\",\n        \"system_error\": \"cpp\",\n        \"tuple\": \"cpp\",\n        \"type_traits\": \"cpp\",\n        \"utility\": \"cpp\",\n        \"fstream\": \"cpp\",\n        \"initializer_list\": \"cpp\",\n        \"iomanip\": \"cpp\",\n        \"iosfwd\": \"cpp\",\n        \"istream\": \"cpp\",\n        \"limits\": \"cpp\",\n        \"new\": \"cpp\",\n        \"ostream\": \"cpp\",\n        \"sstream\": \"cpp\",\n        \"stdexcept\": \"cpp\",\n        \"streambuf\": \"cpp\",\n        \"cinttypes\": \"cpp\",\n        \"typeinfo\": \"cpp\"\n    }\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": [\n\t\t{\n\t\t\t\"type\": \"typescript\",\n\t\t\t\"tsconfig\": \"tsc/tsconfig.json\",\n\t\t\t\"problemMatcher\": [\n\t\t\t\t\"$tsc\"\n\t\t\t],\n\t\t\t\"group\": \"build\",\n\t\t\t\"label\": \"tsc: build - tsc/tsconfig.json\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "BOM.md",
    "content": "# Bill of Materials\nAll parts can be found on Amazon, AliExpress, Ebay etc. Make sure the items you're ordering match the pictures. Listed prices are the lowest I could find on AliExpress.\n\n| Part| Reference Photo | Price | Notes\n|---|---|---|---|\n|NodeMCU ESP32 ESP-WROOM-32|![esp32](/images/bom/esp32.jpg)| $4 | 30 pin version\n| 2x STEPPERONLINE Pancake Nema 17 motors |![nema17](/images/bom/nema17.jpg) ![nema17-dims](/images/bom/nema17-dims.jpg) | $18 | \n| 2x Stepper Motor Driver control extension boards |![driverboard](/images/bom/driverboard.jpg) | $2 |\n| 2x BigTreeTech TMC2209 Stepper motor drivers |![drivers](/images/bom/tmc2209.jpg) | $14 |\n| 2x GT2 Pulley 20 teeth 5mm bore 6mm shaft | ![pulley](/images/bom/pulley.jpg) ![pulley-dims](/images/bom/pulley_dims.jpg) | $2 | Make sure to get the pulley with those exact dimensions\n| GT2 Timing belt 2mm pitch 6mm width | ![belt](/images/bom/belt.jpg) | $6 |\n| MG90s metal gear servo | ![servo](/images/bom/mg90s.jpg) | $2 |\n| USB-C PD trigger module | ![display](/images/bom/pd.jpg) | $1 |\n| LM2596 step down voltage regulator | ![buck](/images/bom/buck.jpg) | $1 |\n| 30W USB-C power delivery adapter | ![power](/images/bom/power.jpg) | $3 | You can use any PD adapter with at least 30W and can supply 12V \n| USB-C male-to-male 10ft cable | ![cable](/images/bom/cable.jpg) | $1 |\n| M-F and F-F breadboard jumper wire 10cm and 20cm | ![wire](/images/bom/wire.jpg) | $2 |\n| M3 hex bolt set | ![m3](/images/bom/m3.jpg) | $2 | You'll need 6/8/12/25 mm bolt lengths \n| 2.54m pitch male headers | ![headers](/images/bom/header.jpg) | $1 |\n| M3 heat inserts | ![inserts](/images/bom/inserts.jpg) | $1 | Optional - only one is used for the pen, and you can use an M3 bolt without it\n| 12mm M3 thumbscrew | ![thumbscrews](/images/bom/thumbscrews.jpg) | $1 |Optional, but sure is nicer. You only need 1 \n| 10cm stepper motor cable | ![steppercable](/images/bom/steppercable.webp) | $2 | These are surprisingly hard to find, I bought mine on AliExpress. You can also simply cut and solder together the long cables that came with your steppers"
  },
  {
    "path": "KinematicModel.md",
    "content": "# Mural Kinematic Model\n\nThe Mural bot is suspended on two belts. As it moves across the wall it rotates slightly,\nin particular it tilts towards the center as it moves to the edges of the drawing region.\n\nIn order to achieve precise drawing abilities it is essential that Mural keeps track of its\nposition in space. In particular, it needs to be aware of its inclination angle as it\nmoves around.\n\n## Basic Model\n\nHere, we describe the kinematic model of Mural, which is used to derive its exact location \nand orientation in space. The model is implemented in ``movement.cpp``, in particular the function ``getBeltLengths``.\n\nThe bot is modeled as a rigid body in 2D and all features are assumed to be projected onto the wall plane.\nIn this representation it can be modeled as two lines: One connecting the pulley tangent points\nand orthogonal to it the line which goes through the pen center and the bot's center of mass $m$. The \ntwo lines coincide in a reference point called $Q$. \nThe distance of the two tangent points is calles $s$.\nThe bot's mass is assumed to be concentrated into a single point (its center of gravity) which is\nlocated in distance $d_m$ from $Q$. The pen center is located in distance $d_p$ from $Q$ .\n\nThe relevant forces in this model are: $F_L$ applied by the left pulley, $F_R$ applied by the right pulley, \nand the gravitational force $F_G$ affecting the center of mass.\n\n![kinematic_model1](/images/doc/kinematic_model1.drawio.svg)\n\nAssumptions:\n\n- all mass is concentrated in a single point\n- the belts masses are negligible\n- the pin distance is much larger than the bot width $d_{pins} >> width_{bot}$ \n\n### Tangent points\n\nIn our model the belt connects to the bot in the tangent point of its pulley. Technically, this point \nis not fully stable: It rotates slightly around the pulley center as the bot tilts sideways.\n\nIn the following it is assumed that this rotation can be ignored, and the tangent point are fixed at\na $45^\\circ$ belt angle.\n\n![tangent_point](/images/doc/tangent_point.drawio.svg)\n\nThe tangent point of the left pulley is located $q$ shifted to the right and $q$ shifted up relative to the pulley center. Likewise, the right tangent point is located $q$ left of the right pulley center and $q$ above the line connecting both pulley centers.\n\n$q$ can be calculated from the pulley diameter $d_{pulley}$ as\n\n$q=\\frac{d_{pulley}}{2\\cdot \\sqrt2}$\n\nSo, for a typical pulley diameter of $d_{pulley}=12.69$ mm we get $q=4.4866$ mm .\n\nThe lenght of the line connecting the tangent point is given as the distance of the pulley axes minus $2*q$ .\n\n## Solving for the Equilibrium State\n\nWith forces $F$ affecting the Mural bot they are moving it (translation) and rotating it by generating torques. \nWe are looking for the static state of the bot, in which the forces as well as the torques cancel out.\nWe'll find this state by updating the values describing the bot's location, the forces and the torques \nin a consecutive and decoupled manner. I.e. while computing the forces we assume there's no torque, and while \ncomputing the torque there's no translating force. Updating these values repeatedly and in a loop\nwill lead to convergence of all quantities towards their true equilibrium states.\n\nOn a top level, we run the following steps in a loop until the quantities converge:\n- compute belt angles $\\varphi_L$ and $\\varphi_R$\n- compute forces on both belts\n- compute torque on mural, solve for mural inclination $\\gamma$\n\nWith the result: mural inclination $\\gamma$, length of both belts in wall plane, and belt forces $F_L$ and $F_R$ .\n\nIn a subsequent step, these quantities are used to compute the belt lengths in 3D. Finally, a dilation correction is applied to account for non-rigidity of the belts under force.\n\n## Forces\n\nIn the equilibrium state the overall torque on the bot is zero and can be ignored. In this case\nall forces can be assumed to be applied to a single point:\n\n![kinematic_model_forces](/images/doc/kinematic_model_forces.jpg)\n\nIntroducing the angles $\\rho = 90^\\circ-\\varphi_R$ and $\\delta = 90^\\circ-\\varphi_L$ we can apply the [Law of Sines](https://en.wikipedia.org/wiki/Law_of_sines) and get:\n\n$\\frac{F_R}{F_G}=\\frac{\\sin(\\delta)}{\\sin\\left( \\varphi_R + \\varphi_L \\right)}$\n\n$\\Leftrightarrow F_R=\\frac{F_G\\cdot\\sin(\\delta)}{\\sin\\left( \\varphi_L + \\varphi_R \\right)} =\n\\frac{F_G\\cdot\\cos(\\varphi_L)}{\\sin\\left( \\varphi_L + \\varphi_R \\right)}$\n\nand likewise\n\n$\\frac{F_L}{F_G}=\\frac{\\sin(\\rho)}{\\sin\\left( \\varphi_L + \\varphi_R \\right)}$\n\n$\\Leftrightarrow F_L=\\frac{F_G\\cdot\\sin(\\rho)}{\\sin\\left( \\varphi_L + \\varphi_R \\right)} =\n\\frac{F_G\\cdot\\cos(\\varphi_R)}{\\sin\\left( \\varphi_L + \\varphi_R \\right)}$\n\n\n## Torques\n\n![kinematic_model_torques](/images/doc/kinematic_model_torques.jpg)\n\nGiven the forces we can compute the torque values $T_L$ ,  $T_R$ and  $T_m$ they induce around the reference point $Q$.\nWhat we are interested in is the bot inclination angle $\\gamma$. A positive $\\gamma$ means the bot tilts to the right,\nwhile a negative $\\gamma$ represents a tilt to the left.\n\nLet's introduce the auxilliary angles $\\alpha$ and $\\beta$ representing the direction of the belts relative to the\nline connecting the tangent points:\n$\\varphi_L = \\alpha + \\gamma$\nand \n$\\varphi_R = \\beta - \\gamma$ .\n\nAs $Q$ is located in the center between the tangent points we get $s_L = 0.5\\cdot s$ and $s_R = 0.5\\cdot s$.\n\nThe torque induced on the left tangent point is \n\n$T_L = s_L \\cdot \\sin(\\alpha)\\cdot F_L$ \n\n, and it is pushing the bot clockwise.\n\nAnalogously, $T_R$ is affecting the right tangent point and rotating the bot counter-clockwise around $Q$:\n\n$T_R = s_R \\cdot \\sin(\\beta)\\cdot F_R$ \n\nThe gravity force $F_G$ of the bot results in torque $T_m$ which is rotating it counter-clockwise (for $\\gamma>0$):\n\n$T_m = s_m \\cdot F_m$ , with\n\n$s_m = d_m \\cdot \\tan(\\gamma)$ and\n\n$F_m = F_g \\cdot \\cos(\\gamma)$ .\n\nSo we get\n\n$T_m = d_m \\cdot \\tan(\\gamma) \\cdot F_G \\cdot \\cos(\\gamma)$\n\nIn the static state the resulting torque is zero, so\n\n$T_R - T_L + T_m \\stackrel{!}{=} 0$\n\n, and the implementation searches numerically for a $\\gamma$ which fulfills this condition.\n\n## Tangent Points given Pen Location\n\nAs our goal is to compute the precise belt lenths, we have to compute the exact tangent point location given the outcome\nof the optimization operation (see ``Movement::getLeftTangentPoint``). For the left tangent point we calculate the distance from the \npen center in $x$ and $y$ as\n\n$P_{LX} = s_L \\cdot \\cos(\\gamma) - d_p \\cdot \\sin(\\gamma)$\n\n$P_{LY} = s_L \\cdot \\sin(\\gamma) + d_p \\cdot \\cos(\\gamma)$\n\nand with these the left tangent point coordinates as\n\n$x_{PL} = x_{pen} - P_{LX}$\n\n$y_{PL} = y_{pen} - P_{LY}$\n\nIn the same way we get for the right pulley:\n\n\n$P_{RX} = s_R \\cdot \\cos(\\gamma) + d_p \\cdot \\sin(\\gamma)$\n\n$P_{RY} = s_R \\cdot \\sin(\\gamma) - d_p \\cdot \\cos(\\gamma)$\n\nand\n\n$x_{PR} = x_{pen} + P_{RX}$\n\n$y_{PR} = y_{pen} + P_{RY}$\n\n"
  },
  {
    "path": "LICENSE.md",
    "content": "## Creative Commons Attribution-NonCommercial 4.0 International Public License\n\nBy exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License (\"Public License\"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.\n\n### Section 1 – Definitions.\n\na. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.\n\nb. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.\n\nc. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.\n\nd. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.\n\ne. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.\n\nf. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License.\n\ng. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.\n\nh. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License.\n\ni. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.\n\nj. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.\n\nk. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.\n\nl. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.\n\n### Section 2 – Scope.\n\na. ___License grant.___\n\n   1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:\n\n       A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and\n\n       B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only.\n\n   2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.\n\n   3. __Term.__ The term of this Public License is specified in Section 6(a).\n\n   4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.\n\n   5. __Downstream recipients.__\n\n        A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.\n\n        B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.\n\n   6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).\n\nb. ___Other rights.___\n\n   1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.\n\n   2. Patent and trademark rights are not licensed under this Public License.\n\n   3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.\n\n### Section 3 – License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the following conditions.\n\na. ___Attribution.___\n\n   1. If You Share the Licensed Material (including in modified form), You must:\n\n       A. retain the following if it is supplied by the Licensor with the Licensed Material:\n\n         i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);\n\n         ii. a copyright notice;\n\n         iii. a notice that refers to this Public License;\n\n         iv. a notice that refers to the disclaimer of warranties;\n\n         v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;\n\n       B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and\n\n       C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.\n\n   2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.\n\n   3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.\n\n   4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License.\n\n### Section 4 – Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:\n\na. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;\n\nb. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and\n\nc. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.\n\n### Section 5 – Disclaimer of Warranties and Limitation of Liability.\n\na. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__\n\nb. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__\n\nc. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.\n\n### Section 6 – Term and Termination.\n\na. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.\n\nb. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:\n\n   1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or\n\n   2. upon express reinstatement by the Licensor.\n\n   For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.\n\nc. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.\n\nd. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.\n\n### Section 7 – Other Terms and Conditions.\n\na. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.\n\nb. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.\n\n### Section 8 – Interpretation.\n\na. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.\n\nb. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.\n\nc. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.\n\nd. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.\n"
  },
  {
    "path": "README.md",
    "content": "# [getmural.me](https://getmural.me)\n\nPlease find the main documentation on https://getmural.me. \n\n# Additional Information\n\n## Positioning of the Drawing on the Wall\n\nHere's how the image is prepared and drawn:\n\n- The user defines the pin distance as part of the setup in the UI. For example 1 meter (or 1000mm). (This is d_pins in the image below.)\n- The top margin is 20% of that distance, so the top of the image will be 200mm below the line between the two pins.\n- Each side also has a 20% margin, so you'll get total of 60% of the horizontal distance, or 600mm.\n- Now that we have the max width (600mm). The SVG is resized so its width is 600 and the height gets resized proportionally.\n- Then a processing step is performed on the SVG to figure out what to actually draw, with each SVG unit being treated as millimeter.\n- Finally it's converted into a simple format for Mural to draw, containing mostly its coordinate movement commands and pen up/down. This file is then uploaded to the microcontroller and executed line by line.\n\n![image_positioning](/images/doc/muralbot_image_positioning.svg)\n\n## Mural's Kinematic Model\n\nPlease find the kinematic model [here](KinematicModel.md).\n"
  },
  {
    "path": "build.py",
    "content": "import os\nImport(\"env\")\n\nprint(\"Transpiling TS code\")\nenv.Execute(\"rm data/www/worker/* || true\")\ncurrentPath = os.getcwd()\n\nos.chdir('./tsc')\nenv.Execute(\"npm run build\")\nif not os.path.exists(\"../data/www/worker/\"):\n    os.makedirs(\"../data/www/worker/\")\nenv.Execute(\"cp dist_packed/main.js ../data/www/worker/worker.js\")\nos.chdir(currentPath)"
  },
  {
    "path": "data/www/client.js",
    "content": "export async function leftRetractDown() {\n    await postCommand(\"l-ret\");\n}\n\nexport async function leftExtendDown() {\n    await postCommand(\"l-ext\");\n}\n\nexport async function rightRetractDown() {\n    await postCommand(\"r-ret\");\n}\n\nexport async function rightExtendDown() {\n    await postCommand(\"r-ext\");\n}\n\nexport async function leftRetractUp() {\n    await postCommand(\"l-0\");\n}\n\nexport async function leftExtendUp() {\n    await postCommand(\"l-0\");\n}\n\nexport async function rightRetractUp() {\n    await postCommand(\"r-0\");\n}\n\nexport async function rightExtendUp() {\n    await postCommand(\"r-0\");\n}\n\nasync function postCommand(command) {\n    $.post(\"/command\", {command}).fail(function() {\n        alert(\"Command failed\");\n        location.reload();\n    });\n}"
  },
  {
    "path": "data/www/dpad.less",
    "content": ".set {\n    overflow: hidden;\n    text-align: center;\n   .d-pad { margin-right: 40px; }\n   .d-pad, .o-pad {\n      display: inline-block;\n      // transform: scale(.7);\n    }\n  }\n  .set.setbg { background: #222; }\n  .set.setbg2 { background: #5f9837; }\n  \n  \n  @dpad-radius: 17%;\n  @dpad-radius-in: 20%;\n  @dpad-fg: #ddd;\n  @dpad-fg-hover: #eee;\n  @dpad-bg: #fff;\n  @arrowcolor: #aaa;\n  @tri-sml-a: 13px;\n  @tri-sml-b: 19px;\n  @tri-lrg-a: 13px;\n  @tri-lrg-b: 19px;\n  @dpad-arrow-shift: 5px;\n  @dpad-arrow-move: 35%;\n  .d-pad {\n    position: relative;\n    width: 200px;\n    height: 200px;\n    border-radius: 48%;\n    overflow: hidden;\n    &:before {\n      content: '';\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      border-radius: 5%;       \n      transform: translate(-50%, -50%);\n      width: 66.6%;\n      height: 66.6%;\n      background: @dpad-fg;\n    }\n    &:after {\n      content: '';\n      position: absolute;\n      display: none;\n      z-index:2;\n      width: 20%;\n      height: 20%;\n      top: 50%;\n      left: 50%;\n      background: @dpad-fg;\n      border-radius: 50%;   \n      transform: translate(-50%, -50%);\n      transition: all .25s;\n      cursor: pointer;\n    }\n    &:hover:after {\n      width: 30%;\n      height: 30%;\n    }  \n    a {\n      display:block;\n      position: absolute;\n      -webkit-tap-highlight-color:  rgba(255, 255, 255, 0);\n      width: 33.3%;\n      height: 43%;\n      line-height: 40%;\n      color: #fff;\n      background: @dpad-fg;\n      text-align: center;  \n      &:hover {\n        background: @dpad-fg-hover;\n      }\n      &:before {\n        content: '';\n        position: absolute;\n        width: 0;\n        height: 0;\n        border-radius: 5px;\n        border-style: solid;     \n        transition: all .25s;\n      }\n      &:after {\n        content: '';\n        position: absolute;\n        width: 102%;\n        height: 78%;\n        background: @dpad-bg;\n        border-radius: @dpad-radius-in;\n      }    \n    }\n    a.left, a.right {\n      width: 43%;\n      height: 33%;\n      &:after {\n        width: 78%;\n        height: 102%;\n      }    \n    }\n    \n    a.up {\n      top: 0;\n      left: 50%;\n      transform: translate(-50%, 0);\n      border-radius: @dpad-radius @dpad-radius 50% 50%;\n      &:hover {\n        background: linear-gradient(0deg, @dpad-fg 0%, @dpad-fg-hover 50%);\n      }\n      &:after {\n        left: 0;\n        top: 0;\n        transform: translate(-100%, 0);\n        border-top-left-radius: 50%;\n        pointer-events: none;\n      }\n      &:before {\n        top: 40%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        border-width: 0 @tri-sml-a @tri-sml-b @tri-sml-a;\n        border-color: transparent transparent @arrowcolor transparent;\n      }\n      &:active:before {\n        border-bottom-color: #333;\n      }\n    }\n    a.up:hover:before { top: @dpad-arrow-move; }\n    \n    a.down {\n      bottom: 0;\n      left: 50%;    \n      transform: translate(-50%, 0);\n      border-radius: 50% 50% @dpad-radius @dpad-radius; \n      &:hover {\n        background: linear-gradient(180deg, @dpad-fg 0%, @dpad-fg-hover 50%);\n      }\n      &:after {\n        right: 0;\n        bottom: 0;\n        transform: translate(100%, 0);\n        border-bottom-right-radius: 50%;\n        pointer-events: none;\n      }\n      &:before {\n        bottom: 40%;\n        left: 50%;\n        transform: translate(-50%, 50%);\n        border-width: @tri-sml-b @tri-sml-a 0px @tri-sml-a;\n        border-color: @arrowcolor transparent transparent transparent;\n      }\n      &:active:before {\n        border-top-color: #333;\n      }\n    }\n    a.down:hover:before { bottom: @dpad-arrow-move; }\n    \n    a.left {\n      top: 50%;\n      left: 0;    \n      transform: translate(0, -50%);\n      border-radius: @dpad-radius 50% 50% @dpad-radius;\n      &:hover {\n        background: linear-gradient(-90deg, @dpad-fg 0%, @dpad-fg-hover 50%);\n      }\n      &:after {\n        left: 0;\n        bottom: 0;\n        transform: translate(0, 100%);\n        border-bottom-left-radius: 50%;\n        pointer-events: none;\n      }\n      &:before {\n        left: 40%;\n        top: 50%;\n        transform: translate(-50%, -50%);\n        border-width: @tri-sml-a @tri-sml-b @tri-sml-a 0;\n        border-color: transparent @arrowcolor transparent transparent;\n      }\n      &:active:before {\n        border-right-color: #333;\n      }\n    }\n    a.left:hover:before { left: @dpad-arrow-move; }\n    \n    a.right {\n      top: 50%;\n      right: 0;    \n      transform: translate(0, -50%);  \n      border-radius: 50% @dpad-radius @dpad-radius 50%;\n      &:hover {\n        background: linear-gradient(90deg, @dpad-fg 0%, @dpad-fg-hover 50%);\n      }\n      &:after {\n        right: 0;\n        top: 0;\n        transform: translate(0, -100%);\n        border-top-right-radius: 50%;      \n        pointer-events: none;\n      }\n      &:before {\n        right: 40%;\n        top: 50%;\n        transform: translate(50%, -50%);\n        border-width: @tri-sml-a 0 @tri-sml-a @tri-sml-b;\n        border-color: transparent transparent transparent @arrowcolor;\n      }\n      &:active:before {\n        border-left-color: #333;\n      }\n    }\n    a.right:hover:before { right: @dpad-arrow-move; }  \n  }\n  .d-pad.up a.up:before { border-bottom-color: #333; }\n  .d-pad.down a.down:before { border-top-color: #333; }\n  .d-pad.left a.left:before { border-right-color: #333; }\n  .d-pad.right a.right:before { border-left-color: #333; }\n  \n  .blue {\n    @c: #1843ca;\n    @c-h: #143cb9;\n    @c-t: #ccc;\n    @c-t-a: rgba(255,255,255,1);\n    .d-pad {    \n      &:before, a { background: @c; }\n      &:after { display: block; background: @c-t; } \n      a:after { border-radius: 10%; }\n      a.up:hover { background: linear-gradient(0deg, @c 0%, @c-h 50%); }\n      a.right:hover { background: linear-gradient(90deg, @c 0%, @c-h 50%); }\n      a.down:hover { background: linear-gradient(180deg, @c 0%, @c-h 50%); }\n      a.left:hover { background: linear-gradient(-90deg, @c 0%, @c-h 50%); }\n      a.up:before { border-bottom-color: @c-t; }\n      a.right:before { border-left-color: @c-t; }    \n      a.down:before { border-top-color: @c-t; }\n      a.left:before { border-right-color: @c-t; }\n      a.up:active:before { border-bottom-color: @c-t-a; }\n      a.right:active:before { border-left-color: @c-t-a; }    \n      a.down:active:before { border-top-color: @c-t-a; }\n      a.left:active:before { border-right-color: @c-t-a; }\n    }\n  }\n  \n  // set direction active state\n  \n  .d-pad.up a.up:before { border-bottom-color: #333; }\n  .d-pad.down a.down:before { border-top-color: #333; }\n  .d-pad.left a.left:before { border-right-color: #333; }\n  .d-pad.right a.right:before { border-left-color: #333; }\n  \n  .o-pad.up a.up:after { border-bottom-color: #333; }\n  .o-pad.down a.down:after { border-top-color: #333; }\n  .o-pad.left a.left:after { border-right-color: #333; }\n  .o-pad.right a.right:after { border-left-color: #333; }"
  },
  {
    "path": "data/www/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Mural</title>\n  <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\"\n    integrity=\"sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx\" crossorigin=\"anonymous\">\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css\">\n  <link rel=\"stylesheet/less\" type=\"text/css\" href=\"dpad.less\" />\n  <link href=\"main.css\" rel=\"stylesheet\">\n  <style>\n    .bd-placeholder-img {\n      font-size: 1.125rem;\n      text-anchor: middle;\n      -webkit-user-select: none;\n      -moz-user-select: none;\n      user-select: none;\n    }\n\n    @media (min-width: 768px) {\n      .bd-placeholder-img-lg {\n        font-size: 3.5rem;\n      }\n    }\n\n    .b-example-divider {\n      height: 3rem;\n      background-color: rgba(0, 0, 0, .1);\n      border: solid rgba(0, 0, 0, .15);\n      border-width: 1px 0;\n      box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);\n    }\n\n    .b-example-vr {\n      flex-shrink: 0;\n      width: 1.5rem;\n      height: 100vh;\n    }\n\n    .bi {\n      vertical-align: -.125em;\n      fill: currentColor;\n    }\n\n    .nav-scroller {\n      position: relative;\n      z-index: 2;\n      height: 2.75rem;\n      overflow-y: hidden;\n    }\n\n    .nav-scroller .nav {\n      display: flex;\n      flex-wrap: nowrap;\n      padding-bottom: 1rem;\n      margin-top: -1px;\n      overflow-x: auto;\n      text-align: center;\n      white-space: nowrap;\n      -webkit-overflow-scrolling: touch;\n    }\n  </style>\n</head>\n\n<body class=\"text-center\">\n  <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js\"\n    integrity=\"sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa\"\n    crossorigin=\"anonymous\"></script>\n  <script src=\"https://code.jquery.com/jquery-3.6.0.min.js\" crossorigin=\"anonymous\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js\" crossorigin=\"anonymous\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/less\" ></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js\" integrity=\"sha512-NApOOz1j2Dz1PKsIvg1hrXLzDFd62+J0qOPIhm8wueAnk4fQdSclq6XvfzvejDs6zibSoDC+ipl1dC66m+EoSQ==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n  <script src=\"main.js\" type=\"module\"></script>\n  <main class=\"form-signin w-100 m-auto\">\n      <div id=\"loadingSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <div class=\"spinner-border mb-5 loading\" role=\"status\"></div>\n      </div>\n      <div id=\"retractBeltsSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Retract belts</h3>\n        <h6 class=\"mb-5\"><small class=\"text-muted\">Place the belt loops on the homing screws and retract the belts until the motors stall. Don't let the motors stall for too long.</small></h6>\n        <div class=\"form-check form-switch mb-5\">\n          <input class=\"form-check-input\" style=\"transform: scale(2);\" type=\"checkbox\" role=\"switch\" id=\"leftMotorToggle\">\n          <label class=\"form-check-label\" for=\"leftMotorToggle\">Left motor</label>\n        </div>\n        <div class=\"form-check form-switch mb-5\">\n          <input class=\"form-check-input\" style=\"transform: scale(2);\" type=\"checkbox\" role=\"switch\" id=\"rightMotorToggle\">\n          <label class=\"form-check-label\" for=\"rightMotorToggle\">Right motor</label>\n        </div>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"beltsRetracted\">Belts are retracted</button>\n      </div>\n      <div id=\"distanceBetweenAnchorsSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Distance between hangers</h3>\n        <h6 class=\"mb-5\"><small class=\"text-muted\">Distance between the two nails on which you are hanging Mural, in millimeters</small></h6>\n        <input type=\"number\" class=\"form-control mb-5\" id=\"distanceInput\">\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"setDistance\">Set distance</button>\n        <div style=\"position:fixed; top: 10px; right: 10px;\" data-bs-toggle=\"modal\" data-bs-target=\"#toolsModal\"><i class=\"bi-gear fs-2\"></i></div>\n      </div>\n      <div id=\"extendToHomeSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Extend belts</h3>\n        <h6 class=\"mb-5\"><small class=\"text-muted\">The belts will extend to their home position. Make sure the belts are unobstructed and the motors do not skip, or the drawing accuracy may be affected</small></h6>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"extendToHome\">Extend to home position</button>\n        <div class=\"spinner-border mb-5 loading\" id=\"extendingSpinner\" role=\"status\" style=\"visibility:hidden;\"></div>\n      </div>\n      <div id=\"penCalibrationSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Pen calibration</h3>\n        <h6 class=\"mb-5\"><small class=\"text-muted\">Insert the pen so it's close to, but not touching the wall and secure it with a bolt. Then, adjust the pen position so the pen is touching the wall</small></h6>\n        <input type=\"range\" class=\"form-range mb-5 text-center\" min=\"0\" max=\"90\" id=\"servoRange\" value=\"0\">\n        <div class=\"btn-group mb-3 btn-group-lg\" role=\"group\">\n          <div class=\"btn-group me-3\" role=\"group\">\n            <button type=\"button\" class=\"btn btn-primary fs-4\" id=\"penMinus\">-</button>\n          </div>\n          <div class=\"btn-group me-3\" role=\"group\">\n            <button type=\"button\" class=\"btn btn-primary fs-4\" id=\"penPlus\">+</button>\n          </div>\n        </div>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"setPenDistance\">Pen is touching the wall</button>\n      </div>\n      <div id=\"svgUploadSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Select SVG Image</h3>\n        <h6 class=\"mb-2\"><small class=\"text-muted\">Use black and white line art SVGs for now</small></h6>\n        <img id=\"sourceSvg\" class=\"img-thumbnail mb-2 svg-control\" style=\"display:none;width:100%;\">\n        <div class=\"container svg-control mb-2\" style=\"display: none;\">\n          <div class=\"row\">\n            <div class=\"col-10 align-self-center\">\n              <div class=\"set blue\">\n                <nav class=\"d-pad\">\n                  <a class=\"up\" href=\"#\"></a>\n                  <a class=\"right\" href=\"#\"></a>\n                  <a class=\"down\" href=\"#\"></a>\n                  <a class=\"left\" href=\"#\"></a>  \n                </nav>\n              </div>\n            </div>\n            <div class=\"col-2 align-self-center\">\n              <button type=\"button\" class=\"btn btn-primary mb-2\" id=\"zoomIn\"><i class=\"bi-zoom-in fs-2\"></i></button>\n              <button type=\"button\" class=\"btn btn-primary mb-2\" id=\"zoomOut\"><i class=\"bi-zoom-out fs-2\"></i></button>\n              <button type=\"button\" class=\"btn btn-primary\" id=\"resetTransform\"><i class=\"bi-arrow-counterclockwise fs-2\"></i></button>\n            </div>\n          </div>\n          <div class=\"row\">\n            <h6><small class=\"text-muted\" id=\"transformText\"></small></h6>\n          </div>\n        </div>\n        <input class=\"form-control form-control-lg mb-2\" id=\"uploadSvg\" type=\"file\" accept=\".svg\">\n        <button class=\"w-100 btn btn-lg btn-primary mb-6\" id=\"preview\" disabled>Preview drawing</button>\n      </div>\n      <div id=\"chooseRendererSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Choose render type</h3>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5 mt-5\" id=\"pathTracing\">Path Tracing<br/><sub>Works well for most drawings</sub></button>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"vectorRasterVector\">Vector → Raster → Vector<br/><sub>Preserves stroke width</sub></button>\n        <button class=\"w-100 btn btn-lg btn-secondary mt-3 backToSvgSelect\">Back</button>\n      </div>\n      <div id=\"drawingPreviewSlide\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Drawing preview</h3>\n        <img id=\"previewSvg\" class=\"img-thumbnail mb-2 svg-preview\" style=\"display:none;\">\n        <div class=\"progress mb-2\" style=\"height: 20px;\">\n          <div class=\"progress-bar progress-bar-striped progress-bar-animated\" role=\"progressbar\" id=\"progressBar\" style=\"width: 100%;\" aria-valuenow=\"100\" aria-valuemin=\"0\" aria-valuemax=\"100\"></div>\n        </div>\n        <h6 class=\"mb-2 fw-normal svg-preview\" style=\"display: none;\" id=\"distances\"></h6>\n        <label for=\"infillDensity\" class=\"form-label mb-0\">Infill Density</label>\n        <input type=\"range\" class=\"form-range mb-2 text-center\" min=\"0\" max=\"4\" step=\"1\" id=\"infillDensity\" value=\"0\">\n        <label for=\"turdSize\" class=\"form-label mb-0\">Despeckle</label>\n        <input type=\"range\" class=\"form-range mb-2 text-center\" min=\"2\" max=\"200\" step=\"11\" id=\"turdSize\" value=\"2\">\n        <input class=\"form-check-input mb-2\" type=\"checkbox\" id=\"flattenPathsCheckbox\">\n        <label class=\"form-check-label\" for=\"flattenPathsCheckbox\">Flatten paths</label>\n        <button class=\"w-100 btn btn-lg btn-primary mb-2\" id=\"acceptSvg\" disabled>Accept</button>\n        <button class=\"w-100 btn btn-lg btn-secondary mb-5 backToSvgSelect\">Back</button>\n      </div>\n      <div id=\"uploadProgress\" class=\"muralSlide\" style=\"display:none;\">\n        <h3 class=\"mb-2 fw-normal\">Uploading...</h3>\n        <div class=\"progress mb-2\" id=\"uploadProgress\" role=\"progressbar\" aria-label=\"Upload progress\" aria-valuenow=\"0\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n          <div class=\"progress-bar\" style=\"width: 0%\"></div>\n        </div>\n        <div class=\"progress mb-2\" id=\"verificationProgress\" role=\"progressbar\" aria-label=\"Verification progress\" aria-valuenow=\"0\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n          <div class=\"progress-bar bg-success\" style=\"width: 0%\"></div>\n        </div>\n      </div>\n      <div id=\"beginDrawingSlide\" class=\"muralSlide\" style=\"display: none;\">\n        <h3 class=\"mb-5 fw-normal\">Mural is ready!</h3>\n        <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"beginDrawing\">Begin Drawing</button>\n        <button class=\"w-100 btn btn-lg btn-secondary mb-5\" id=\"reset\">Reset</button>\n      </div>\n      <div id=\"drawingBegan\" class=\"muralSlide\" style=\"display: none;\">\n        <h3 class=\"mb-2 fw-normal\">Drawing Started</h3>\n        <h6 class=\"mb-5\"><small class=\"text-muted\">Mural will not be accessible via your browser while the drawing is in progress. Please refresh the page once the drawing is finished.</small></h6>\n      </div>\n      <div class=\"mt-5\"><a href=\"https://github.com/nikivanov/mural\" class=\"link-primary\">Mural</a></div>\n  </main>\n  <div class=\"modal fade\" id=\"toolsModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"toolsModalLabel\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\" role=\"document\">\n      <div class=\"modal-content\">\n        <div class=\"modal-header\">\n          <h5 class=\"modal-title\" id=\"toolsModalLabel\">Tools</h5>\n          <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n        </div>\n        <div class=\"modal-body\">\n          <label for=\"leftMotorTool\" class=\"form-label\">Left Motor</label>\n          <input type=\"range\" class=\"form-range mb-5 text-center\" min=\"-1\" max=\"1\" id=\"leftMotorTool\" value=\"0\">\n          <label for=\"rightMotorTool\" class=\"form-label\">Right Motor</label>\n          <input type=\"range\" class=\"form-range mb-5 text-center\" min=\"-1\" max=\"1\" id=\"rightMotorTool\" value=\"0\">\n          <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"parkServoTool\">Park Servo</button>\n          <button class=\"w-100 btn btn-lg btn-primary mb-5\" id=\"estepsTool\">Extend 1000mm (E-steps calibration)</button>\n        </div>\n        <div class=\"modal-footer\">\n          <button type=\"button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">Close</button>\n        </div>\n      </div>\n    </div>\n  </div>\n  \n</body>\n\n</html>"
  },
  {
    "path": "data/www/main.css",
    "content": "html,\nbody {\n  height: 100%;\n}\n\nbody {\n  display: flex;\n  align-items: center;\n  padding-top: 40px;\n  padding-bottom: 40px;\n  background-color: #f5f5f5;\n}\n\n.form-signin {\n  max-width: 330px;\n  padding: 15px;\n}\n"
  },
  {
    "path": "data/www/main.js",
    "content": "import * as svgControl from './svgControl.js';\nimport * as client from './client.js';\n\nlet currentState = null;\n\nlet currentWorker = null;\n\nwindow.onload = function () {\n    init();\n};\n\nlet uploadConvertedCommands = null;\n\nasync function checkIfExtendedToHome(extendToHomeTime) {\n    await new Promise(r => setTimeout(r, extendToHomeTime * 1000));\n\n    const waitPeriod = 2000;\n    let done = false;\n    while (!done) {\n        try {\n            const state = await $.get(\"/getState\");\n            if (state.phase !== 'ExtendToHome') {\n                adaptToState(state);\n                done = true;\n            } else {\n                await new Promise(r => setTimeout(r, waitPeriod));\n            }\n        } catch (err) {\n            alert(\"Failed to get current phase: \" + err);\n            location.reload();\n        }\n    }\n}\n\nfunction init() {\n    function doneWithPhase(custom) {\n        $(\".muralSlide\").hide();\n        $(\"#loadingSlide\").show();\n        if (!custom) {\n            custom = {\n                url: \"/doneWithPhase\",\n                data: {},\n                commandName: \"Done With Phase\",\n            };\n        }\n\n        $.post(custom.url, custom.data || {}, function(state) {\n            adaptToState(state);\n        }).fail(function() {\n            alert(`${custom.commandName} command failed`);\n            location.reload();\n        });\n    }\n\n    $(\"#beltsRetracted\").click(async function() { \n        await client.leftRetractUp();\n        await client.rightRetractUp();\n        doneWithPhase();\n    });\n\n    $(\"#setDistance\").click(function() {\n        const inputValue = parseInt($(\"#distanceInput\").val());\n        if (isNaN(inputValue)) {\n            throw new Error(\"input value is not a number\");\n        }\n\n        doneWithPhase({\n            url: \"/setTopDistance\",\n            data: {distance: inputValue},\n            commandName: \"Set Top Distance\",\n        });\n    });\n\n    $(\"#leftMotorToggle\").change(function() {\n        if (this.checked) {\n            client.leftRetractDown(); \n        } else {\n            client.leftRetractUp();\n        }\n    });\n\n    $(\"#rightMotorToggle\").change(function() {\n        if (this.checked) {\n            client.rightRetractDown(); \n        } else {\n            client.rightRetractUp();\n        }\n    });\n\n    $(\"#extendToHome\").click(function() {\n        $(this).prop( \"disabled\", true);\n        $(\"#extendingSpinner\").css('visibility', 'visible');\n        $.post(\"/extendToHome\", {})\n        .always(async function(res) {\n            const extendToHomeTime = parseInt(res);\n            await checkIfExtendedToHome(extendToHomeTime);\n        });\n    });\n    \n    function getServoValueFromInputValue() {\n        const inputValue = parseInt($(\"#servoRange\").val());\n        const value = 90 - inputValue;\n        let normalizedValue;\n        if (value < 0) {\n            normalizedValue = 0;\n        } else if (value > 90) {\n            normalizedValue = 90;\n        } else {\n            normalizedValue = value;\n        }\n\n        return normalizedValue;\n    }\n\n    $(\"#servoRange\").on('input', $.throttle(250, function (e) {\n        const servoValue = getServoValueFromInputValue();\n        $.post(\"/setServo\", {angle: servoValue});\n    }));\n\n    const stepVaule = 5;\n    $(\"#penMinus\").click(function() {\n        $(\"#servoRange\")[0].stepDown(stepVaule);\n        $(\"#servoRange\").trigger('input');\n    });\n\n    $(\"#penPlus\").click(function() {\n        $(\"#servoRange\")[0].stepUp(stepVaule);\n        $(\"#servoRange\").trigger('input');\n    });\n\n    $(\"#setPenDistance\").click(function () {\n        const inputValue = getServoValueFromInputValue();\n        doneWithPhase({\n            url: \"/setPenDistance\",\n            data: {angle: inputValue},\n            commandName: \"Set Pen Distance\",\n        });\n    });\n\n    async function getUploadedSvgString() {\n        const [file] = $(\"#uploadSvg\")[0].files;\n        if (file) {\n            return await file.text();\n        } else {\n            return null;\n        }\n    }\n\n    $(\"#uploadSvg\").change(async function() {\n        const svgString = await getUploadedSvgString();\n        if (svgString) {\n            svgControl.setSvgString(svgString, currentState);\n\n            $(\".svg-control\").show();\n            $(\"#preview\").removeAttr(\"disabled\");\n        } else {\n            $(\"#preview\").attr(\"disabled\", \"disabled\");\n            $(\".svg-control\").hide();\n            $(\"#infillDensity\").val(0);\n            $(\"#turdSize\").val(2);\n        }\n    });\n\n    \n    let currentPreviewId = 0;\n    let rendererFn = null;\n\n    async function render_VectorRasterVector() {\n        if (currentWorker) {\n            console.log(\"Terminating previous worker\");\n            currentWorker.terminate();\n        }\n        currentPreviewId++;\n        const thisPreviewId = currentPreviewId;\n\n        const svgString = await getUploadedSvgString();\n        if (!svgString) {\n            throw new Error('No SVG string');\n        }\n\n        $(\"#progressBar\").text(\"Rasterizing\");\n        const raster = await svgControl.getCurrentSvgImageData();\n\n        const vectorizeRequest = {\n            type: 'vectorize',\n            raster,\n            turdSize: getTurdSize(),\n        };\n\n        if (currentPreviewId == thisPreviewId) {\n            currentWorker = new Worker(`./worker/worker.js?v=${Date.now()}`);\n\n            currentWorker.onmessage = (e) => {\n                if (e.data.type === 'status') {\n                    $(\"#progressBar\").text(e.data.payload);\n                } else if (e.data.type === 'vectorizer') {\n                    const vectorizedSvg = e.data.payload.svg;\n                    const scale = svgControl.getRenderScale();\n                    renderSvgInWorker(\n                        currentWorker,\n                        vectorizedSvg,\n                        svgControl.getTargetWidth() * scale,\n                        svgControl.getTargetHeight() * scale,\n                    );\n                }\n                else if (e.data.type === 'log') {\n                    console.log(`Worker: ${e.data.payload}`);\n                }\n            }\n\n            currentWorker.postMessage(vectorizeRequest);\n        }\n    }\n\n    async function render_PathTracing() {\n        if (currentWorker) {\n            console.log(\"Terminating previous worker\");\n            currentWorker.terminate();\n        }\n        currentPreviewId++;\n        const thisPreviewId = currentPreviewId;\n\n        const svgString = await getUploadedSvgString();\n        if (!svgString) {\n            throw new Error('No SVG string');\n        }\n\n        if (currentPreviewId == thisPreviewId) {\n            currentWorker = new Worker(`./worker/worker.js?v=${Date.now()}`);\n            currentWorker.onmessage = (e) => {\n                if (e.data.type === 'status') {\n                    $(\"#progressBar\").text(e.data.payload);\n                }\n                else if (e.data.type === 'log') {\n                    console.log(`Worker: ${e.data.payload}`);\n                }\n            }\n\n            const renderSvg = svgControl.getRenderSvg();\n            const renderSvgString = new XMLSerializer().serializeToString(renderSvg);\n            renderSvgInWorker(currentWorker, renderSvgString, svgControl.getTargetWidth(), svgControl.getTargetHeight());\n        }\n    }\n\n    function renderSvgInWorker(worker, svg, svgWidth, svgHeight) {\n        const svgJson = svgControl.getSvgJson(svg);\n       \n        const renderRequest = {\n            type: \"renderSvg\",\n            svgJson,\n            width: svgControl.getTargetWidth(),\n            height: svgControl.getTargetHeight(),\n            svgWidth,\n            svgHeight,\n            homeX: currentState.homeX,\n            homeY: currentState.homeY,\n            infillDensity: getInfillDensity(),\n            flattenPaths: getFlattenPaths(),\n        }\n\n        worker.onmessage = (e) => {\n            if (e.data.type === 'status') {\n                $(\"#progressBar\").text(e.data.payload);\n            } else if (e.data.type === 'renderer') {\n                console.log(\"Worker finished!\");\n\n                uploadConvertedCommands = e.data.payload.commands.join('\\n');\n                const resultSvgJson = e.data.payload.svgJson;\n                const resultDataUrl = svgControl.convertJsonToDataURL(resultSvgJson, svgControl.getTargetWidth(), svgControl.getTargetHeight());\n\n                const totalDistanceM = +(e.data.payload.distance / 1000).toFixed(1);\n                const drawDistanceM = +(e.data.payload.drawDistance / 1000).toFixed(1);\n                \n                deactivateProgressBar();\n                $(\"#previewSvg\").attr(\"src\", resultDataUrl);\n                $(\"#distances\").text(`Total: ${totalDistanceM}m / Draw: ${drawDistanceM}m`);\n                $(\".svg-preview\").show();\n                $(\"#acceptSvg\").removeAttr(\"disabled\");\n            }\n        };\n\n        worker.postMessage(renderRequest);\n    }\n\n    function activateProgressBar() {\n        const bar = $(\"#progressBar\");\n        bar.addClass(\"progress-bar-striped\");\n        bar.addClass(\"progress-bar-animated\");\n        bar.removeClass(\"bg-success\");\n        bar.text(\"\");\n    }\n\n    function deactivateProgressBar() {\n        const bar = $(\"#progressBar\");\n        bar.removeClass(\"progress-bar-striped\");\n        bar.removeClass(\"progress-bar-animated\");\n        bar.addClass(\"bg-success\");\n        bar.text(\"Success\");\n    }\n\n\n    $(\"#infillDensity,#turdSize,#flattenPathsCheckbox\").on('input change', async function() {\n        activateProgressBar();\n        $(\"#acceptSvg\").attr(\"disabled\", \"disabled\");\n        await rendererFn();\n    });\n\n    $(\"#preview\").click(async function() {\n        $(\"#svgUploadSlide\").hide();\n        $(\"#chooseRendererSlide\").show();\n    });\n\n    $(\"#pathTracing\").click(async function() {\n        $(\"label[for='turdSize'],#turdSize\").hide();\n        $(\"label[for='flattenPathsCheckbox'],#flattenPathsCheckbox\").show();\n\n        $(\"#chooseRendererSlide\").hide();\n        $(\"#drawingPreviewSlide\").show();\n        rendererFn = render_PathTracing;\n        await rendererFn();\n    });\n\n    $(\"#vectorRasterVector\").click(async function() {\n        $(\"#flattenPathsCheckbox\").prop(\"checked\", false);\n        $(\"label[for='turdSize'],#turdSize\").show();\n        $(\"label[for='flattenPathsCheckbox'],#flattenPathsCheckbox\").hide();\n\n        $(\"#chooseRendererSlide\").hide();\n        $(\"#drawingPreviewSlide\").show();\n        rendererFn = render_VectorRasterVector;\n        await rendererFn();\n    });\n\n    $(\".backToSvgSelect\").click(function() {\n        uploadConvertedCommands = null;\n\n        $(\".loading\").show();\n        activateProgressBar();\n        $(\"#previewSvg\").removeAttr(\"src\");\n        $(\".svg-preview\").hide();\n        $(\"#acceptSvg\").attr(\"disabled\", \"disabled\");\n\n        $(\"#svgUploadSlide\").show();\n        $(\"#drawingPreviewSlide\").hide();\n        $(\"#chooseRendererSlide\").hide();\n    });\n    \n    $(\"#acceptSvg\").click(function() {\n        if (!uploadConvertedCommands) {\n            throw new Error('Commands are empty');\n        }\n        $(\"#acceptSvg\").attr(\"disabled\", \"disabled\");\n\n        const commandsBlob = new Blob([uploadConvertedCommands], {\n            type: \"text/plain\"\n        });\n\n        $(\".muralSlide\").hide();\n        $(\"#uploadProgress\").show();\n\n        const formData = new FormData();\n        formData.append(\"commands\", commandsBlob);\n\n        $.ajax({\n            url: \"/uploadCommands\",\n            data: formData,\n            processData: false,\n            contentType: false,\n            type: 'POST',\n            success: function(data) {\n                verifyUpload(data);\n            },\n            error: function(err) {\n                alert('Upload to Mural failed! ' + err);\n                window.location.reload();\n            },\n            xhr: function () {\n                var xhr = new window.XMLHttpRequest();\n\n                xhr.upload.addEventListener(\"progress\", function (evt) {\n                    if (evt.lengthComputable) {\n                        var percentComplete = evt.loaded / evt.total;\n                        percentComplete = parseInt(percentComplete * 100);\n                        $(\"#uploadProgress\").attr(\"aria-valuemax\", evt.total.toString());\n                        $(\"#uploadProgress\").attr(\"aria-valuenow\", evt.loaded.toString());\n                        $(\"#uploadProgress > .progress-bar\").attr(\"style\", `width: ${percentComplete}%`);\n                    }\n                }, false);\n\n                return xhr;\n            },\n        });\n    });\n\n\n\n    $(\"#beginDrawing\").click(function() {\n        $(\".muralSlide\").hide();\n        $(\"#drawingBegan\").show();\n        $.post(\"/run\", {});\n    });\n\n    $(\"#reset\").click(function() {\n        doneWithPhase();\n        location.reload();\n    });\n\n    $(\"#leftMotorTool\").on('input', function() {\n        const leftMotorDir = parseInt($(\"#leftMotorTool\").val());\n        if (leftMotorDir <= -1) {\n            client.leftRetractDown(); \n        } else if (leftMotorDir >= 1) {\n            client.leftExtendDown();\n        } else {\n            client.leftRetractUp();\n        }\n    });\n\n    $(\"#rightMotorTool\").on('input', function() {\n        const rightMotorDir = parseInt($(\"#rightMotorTool\").val());\n        if (rightMotorDir <= -1) {\n            client.rightRetractDown(); \n        } else if (rightMotorDir >= 1) {\n            client.rightExtendDown();\n        } else {\n            client.rightRetractUp();\n        }\n    });\n\n    $(\"#parkServoTool\").click(function() {\n        $.post(\"/setServo\", {angle: 0});\n    });\n\n    $(\"#estepsTool\").click(function() {\n        $.post(\"/estepsCalibration\", {});\n    });\n\n    const toolsModal = $(\"#toolsModal\")[0];\n\n    toolsModal.addEventListener('hidden.bs.modal', function (event) {\n        client.rightRetractUp();\n        client.leftRetractUp();\n    });\n\n    svgControl.initSvgControl();\n\n    $(\"#loadingSlide\").show();\n\n    // adaptToState({\n    //     phase: \"BeginDrawing\",\n    //     topDistance: 1727,\n    //     safeWidth: 1000,\n    //     homeX: 0,\n    //     homeY: 0,\n    // });\n\n    $.get(\"/getState\", function(data) {\n        adaptToState(data);\n    }).fail(function() {\n        alert(\"Failed to retrieve state\");\n    });\n}\n\nfunction verifyUpload(state) {\n    $.ajax({\n            url: \"/downloadCommands\",\n            processData: false,\n            contentType: false,\n            type: 'GET',\n            success: function(data) {\n                const receivedData = data.split('\\n');\n                const sentData = uploadConvertedCommands.split('\\n');\n                if (receivedData.length !== sentData.length) {\n                    alert(\"Data verification failed\");\n                    window.location.reload();\n                    return;\n                }\n                for (let i = 0; i < receivedData.length; i++) {\n                    if (receivedData[i] !== sentData[i]) {\n                        alert(\"Data verification failed\");\n                        window.location.reload();\n                        return;\n                    }\n                }\n                setTimeout(function() {\n                    adaptToState(state);\n                }, 1000);\n            },\n            error: function(err) {\n                alert('Failed to download commands from Mural! ' + err);\n                window.location.reload();\n            },\n            xhr: function () {\n                var xhr = new window.XMLHttpRequest();\n                xhr.addEventListener(\"progress\", function (evt) {\n                    if (evt.lengthComputable) {\n                        var percentComplete = evt.loaded / evt.total;\n                        percentComplete = parseInt(percentComplete * 100);\n                        $(\"#verificationProgress\").attr(\"aria-valuemax\", evt.total.toString());\n                        $(\"#verificationProgress\").attr(\"aria-valuenow\", evt.loaded.toString());\n                        $(\"#verificationProgress > .progress-bar\").attr(\"style\", `width: ${percentComplete}%`);\n                    }\n                }, false);\n\n                return xhr;\n            },\n        });\n    \n}\n\nfunction adaptToState(state) {\n    $(\".muralSlide\").hide();\n    currentState = state;\n    switch(state.phase) {\n        case \"RetractBelts\":\n            $(\"#retractBeltsSlide\").show();\n            break;\n        case \"SetTopDistance\":\n            $(\"#distanceBetweenAnchorsSlide\").show();\n            break;\n        case \"ExtendToHome\":\n            $(\"#extendToHomeSlide\").show();\n            if (state.moving || state.startedHoming) {\n                $(\"#extendToHome\").prop( \"disabled\", true);\n                $(\"#extendingSpinner\").css('visibility', 'visible');\n                checkIfExtendedToHome();\n            }\n            break;\n        case \"PenCalibration\":\n            $.post(\"/setServo\", {angle: 90});\n            $(\"#penCalibrationSlide\").show();\n            break;\n        case \"SvgSelect\":\n            $(\"#svgUploadSlide\").show();\n            break;\n        case \"BeginDrawing\":\n            $(\"#beginDrawingSlide\").show();\n            break;\n        default:\n            alert(\"Unrecognized phase\");\n    }\n}\n\nfunction getInfillDensity() {\n    const density = parseInt($(\"#infillDensity\").val());\n    if ([0, 1, 2, 3, 4].includes(density)) {\n        return density;\n    } else {\n        throw new Error('Invalid density');\n    }\n}\n\nfunction getTurdSize() {\n    return parseInt($(\"#turdSize\").val());\n}\n\nfunction getFlattenPaths() {\n    return $(\"#flattenPathsCheckbox\").is(\":checked\");\n}"
  },
  {
    "path": "data/www/svgControl.js",
    "content": "document.body.addEventListener(\"click\", function(e) {\n\tif(e.target && e.target.nodeName == \"A\" && e.target.parentElement.className == 'd-pad') {\n        const validDirection = [\"up\", \"down\", \"left\", \"right\"];\n        if (validDirection.includes(e.target.className)) {\n            requestChangeInTransform(e.target.className);\n        }\n\t}\n});\n\nexport const renderScale = 2;\n\nexport function initSvgControl() {\n    $(\"#zoomIn\").click(function() {\n        requestChangeInTransform(\"in\");\n    });\n    \n    $(\"#zoomOut\").click(function() {\n        requestChangeInTransform(\"out\");\n    });\n    \n    $(\"#resetTransform\").click(function() {\n        requestChangeInTransform(\"reset\");\n    });\n}\n\nconst affineTransform = [1, 0, 0, 1, 0, 0];\n// nudge by this fraction of the viewport's width and height\nconst nudgeByFactor = 0.025;\nconst zoomByFactor = 0.05;\nfunction requestChangeInTransform(direction) {\n    switch (direction) {\n        case \"up\":\n            affineTransform[5] = affineTransform[5] - nudgeByFactor;\n            break;\n        case \"down\":\n            affineTransform[5] = affineTransform[5] + nudgeByFactor;\n            break;\n        case \"left\":\n            affineTransform[4] = affineTransform[4] - nudgeByFactor;\n            break;\n        case \"right\":\n            affineTransform[4] = affineTransform[4] + nudgeByFactor;\n            break;\n        case \"in\":\n            affineTransform[0] = affineTransform[0] + zoomByFactor;\n            affineTransform[3] = affineTransform[3] + zoomByFactor;\n            break;\n        case \"out\":\n            affineTransform[0] = affineTransform[0] - zoomByFactor;\n            affineTransform[3] = affineTransform[3] - zoomByFactor;\n            break;\n        case \"reset\":\n            resetTransform();\n            break;\n        default:\n            console.log(\"Unrecognized transform direction\");\n            return;\n    }\n    applyTransform();\n}\n\nfunction resetTransform() {\n    affineTransform[0] = 1;\n    affineTransform[3] = 1;\n    affineTransform[4] = 0;\n    affineTransform[5] = 0;\n}\n\nlet originalSvg;\nlet transformedSvg;\nlet currentWidth;\nlet currentHeight;\nexport function setSvgString(svgString, currentState) {\n    resetTransform();\n\n    originalSvg = new DOMParser().parseFromString(svgString, 'image/svg+xml');\n    currentWidth = currentState.safeWidth;\n    normalizeSvg();\n    applyTransform();\n}\n\nconst transformGroupID = \"muralTransformGroup\";\nfunction normalizeSvg() {\n    const svgElement = originalSvg.documentElement;\n    let width, height;\n\n    if (svgElement.hasAttribute(\"width\") && svgElement.hasAttribute(\"height\")) {\n        width = convertUnitsToPx(svgElement.getAttribute(\"width\"));\n        height = convertUnitsToPx(svgElement.getAttribute(\"height\"));\n    }\n    \n    if (svgElement.hasAttribute(\"viewBox\")) {\n        if (!width || !height) {\n            const viewBox = svgElement.getAttribute(\"viewBox\").split(/[\\s,]/).filter(s => s != \"\");;\n            width = parseFloat(viewBox[2]);\n            height = parseFloat(viewBox[3]);\n        }\n    } else {\n        svgElement.setAttribute(\"viewBox\", `0, 0, ${width}, ${height}`);\n    }\n    \n    if (!width || !height) {\n        throw new Error(\"Invalid SVG\");\n    }\n\n    currentHeight = currentWidth / width * height;\n\n    svgElement.setAttribute(\"width\", currentWidth);\n    svgElement.setAttribute(\"height\", currentHeight);\n\n    const transformGroup = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n    transformGroup.id = transformGroupID;\n    while (svgElement.firstChild) {\n        transformGroup.appendChild(svgElement.firstChild);\n    }\n    svgElement.appendChild(transformGroup);\n}\n\nfunction convertUnitsToPx(dimension) {\n    const unitConversionFactors = {\n        pt: 1.3333,    // Points to pixels\n        pc: 16,        // Picas to pixels\n        in: 96,        // Inches to pixels\n        cm: 37.795,    // Centimeters to pixels\n        mm: 3.7795,    // Millimeters to pixels\n        px: 1,         // Pixels to pixels\n    };\n\n    const match = dimension.match(/([\\d.]+)([a-z%]*)/i);\n    if (!match) {\n        alert(\"Invalid SVG\");\n        throw new Error(`Invalid dimension: \"${dimension}\"`);\n    }\n    const value = parseFloat(match[1]);\n    const unit = match[2] || \"px\"; // Default to pixels if no unit is provided\n    const conversionFactor = unitConversionFactors[unit] || 1;\n    return value * conversionFactor; // Convert to pixels\n}\n\nexport function getTargetWidth() {\n    return currentWidth;\n}\n\nexport function getTargetHeight() {\n    return currentHeight;\n}\n\nexport function getRenderScale() {\n    return renderScale;\n}\n\nexport function getRenderSvg() {\n    return makeTransformedSvgWithHeight()[0];\n}\n\nfunction applyTransform() {\n    updateTransformText();\n\n    const [clonedSvg, newHeight] = makeTransformedSvgWithHeight();\n    currentHeight = newHeight;\n    \n    const svgString = new XMLSerializer().serializeToString(clonedSvg);\n    const svgDataURL = `data:image/svg+xml;base64,${btoa(svgString)}`;\n    $(\"#sourceSvg\")[0].src = svgDataURL;\n\n    transformedSvg = clonedSvg;\n}\n\nfunction makeTransformedSvgWithHeight() {\n    const clonedSvg = originalSvg.cloneNode(true);\n    const svgElement = clonedSvg.documentElement;\n\n    const viewBox = svgElement.getAttribute(\"viewBox\").split(/[\\s,]/).filter(s => s != \"\");\n    const vbWidth = parseFloat(viewBox[2]);\n    const vbHeight = parseFloat(viewBox[3]);\n\n    const scaledAffine = [...affineTransform];\n    scaledAffine[4] = scaledAffine[4] * vbWidth;\n\n    let newHeight = parseFloat(svgElement.getAttribute(\"height\"));\n    if (scaledAffine[5] > 0) {\n        // when shifting down increase height\n        const heightOffset = scaledAffine[5] * newHeight;\n        newHeight = newHeight + heightOffset;\n        svgElement.setAttribute(\"height\", newHeight);\n\n        scaledAffine[5] = scaledAffine[5] * vbHeight;\n        viewBox[3] = vbHeight + scaledAffine[5];\n        svgElement.setAttribute(\"viewBox\", viewBox.join(\", \"));\n    } else {\n        scaledAffine[5] = scaledAffine[5] * vbHeight;\n    }\n\n    const transfromGroup = clonedSvg.getElementById(transformGroupID);\n    transfromGroup.setAttribute(\"transform\", `matrix(${scaledAffine.join(\", \")})`);\n\n    return [clonedSvg, newHeight];\n}\n\nfunction updateTransformText() {\n    function normalizeNumber(num) {\n        return +num.toFixed(2);\n    }\n    $(\"#transformText\").text(`(${normalizeNumber(affineTransform[4] * 100)}, ${normalizeNumber(affineTransform[5] * 100)}) ${normalizeNumber(affineTransform[0])}x`);\n}\n\nexport async function getCurrentSvgImageData() {\n    const scaledHeight = currentHeight * renderScale;\n    const scaledWidth = currentWidth * renderScale;\n    \n    const svgString = new XMLSerializer().serializeToString(transformedSvg);\n\n    const canvas = new OffscreenCanvas(scaledWidth, scaledHeight);\n    const canvasContext = canvas.getContext(\"2d\",);\n    const img = await loadImage(`data:image/svg+xml;base64,${btoa(svgString)}`);\n\n    const bitmap = await createImageBitmap(img, {resizeHeight: scaledHeight, resizeWidth: scaledWidth});\n\n    canvasContext.drawImage(bitmap, 0, 0, scaledWidth, scaledHeight);\n    \n    const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);\n    \n    return imageData;\n}\n\nasync function loadImage(src) {\n    return new Promise((resolve, reject) => {\n        const img = new Image();\n        img.onload = () => resolve(img);\n        img.onerror = reject;\n        img.src = src;\n    });\n}\n\nexport function getSvgJson(svgString) {\n    const size = new paper.Size(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);\n    paper.setup(size);\n    const svg = paper.project.importSVG(svgString, {\n        expandShapes: true,\n        applyMatrix: true,\n    });\n    const json = svg.exportJSON();\n    paper.project.remove();\n\n    return json;\n}\n\nexport function convertJsonToDataURL(json, width, height) {\n    $(\"#previewCanvas\").remove();\n    $(document.body).append(`<canvas id=\"previewCanvas\" width=\"${width}\" height=\"${height}\" style=\"display: none;\"></canvas>`);\n    \n    paper.setup($(\"#previewCanvas\")[0]);\n    paper.project.importJSON(json);\n    paper.view.draw();\n\n    const dataURL = $(\"#previewCanvas\")[0].toDataURL();\n    \n    paper.project.remove();\n    $(\"#previewCanvas\").remove();\n\n    return dataURL;\n}\n\n\n\n"
  },
  {
    "path": "images/doc/kinematic_model1.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" agent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36\" version=\"27.0.9\">\n  <diagram name=\"Page-1\" id=\"uI_OCheiNeMay8VbvTcP\">\n    <mxGraphModel grid=\"1\" page=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-5\" value=\"\" style=\"shape=sumEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"60\" y=\"150\" width=\"30\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-6\" value=\"\" style=\"shape=sumEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"790\" y=\"150\" width=\"30\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-8\" value=\"\" style=\"endArrow=none;html=1;rounded=0;entryX=0.533;entryY=0.567;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"m5EwUfMTjH4Hw6mmDvRn-1\" target=\"m5EwUfMTjH4Hw6mmDvRn-5\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"400\" y=\"450\" as=\"sourcePoint\" />\n            <mxPoint x=\"450\" y=\"400\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-9\" value=\"\" style=\"endArrow=none;html=1;rounded=0;entryX=0.5;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"m5EwUfMTjH4Hw6mmDvRn-2\" target=\"m5EwUfMTjH4Hw6mmDvRn-6\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"400\" y=\"450\" as=\"sourcePoint\" />\n            <mxPoint x=\"450\" y=\"400\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-24\" value=\"Q\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"330\" y=\"360\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-30\" value=\"\" style=\"group;rotation=10;\" vertex=\"1\" connectable=\"0\" parent=\"1\">\n          <mxGeometry x=\"160\" y=\"380\" width=\"360\" height=\"185\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-10\" value=\"\" style=\"endArrow=none;html=1;rounded=0;\" edge=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\" source=\"m5EwUfMTjH4Hw6mmDvRn-12\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"192\" y=\"11\" as=\"sourcePoint\" />\n            <mxPoint x=\"168\" y=\"149\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-1\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;rotation=10;opacity=50;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"11\" y=\"-25\" width=\"60\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-2\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;rotation=10;opacity=50;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"306\" y=\"27\" width=\"60\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-3\" value=\"\" style=\"shape=orEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;rotation=55;opacity=50;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"177\" y=\"32\" width=\"20\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-11\" value=\"\" style=\"endArrow=none;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\" source=\"m5EwUfMTjH4Hw6mmDvRn-1\" target=\"m5EwUfMTjH4Hw6mmDvRn-2\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"241\" y=\"81\" as=\"sourcePoint\" />\n            <mxPoint x=\"299\" y=\"40\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-13\" value=\"\" style=\"endArrow=none;html=1;rounded=0;\" edge=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\" target=\"m5EwUfMTjH4Hw6mmDvRn-12\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"192\" y=\"11\" as=\"sourcePoint\" />\n            <mxPoint x=\"168\" y=\"149\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-12\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;rotation=10;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"162\" y=\"149\" width=\"10\" height=\"10\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-19\" value=\"\" style=\"endArrow=none;html=1;rounded=0;\" edge=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"184\" y=\"-1\" as=\"sourcePoint\" />\n            <mxPoint x=\"200\" y=\"23\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-20\" value=\"\" style=\"endArrow=none;html=1;rounded=0;\" edge=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"181\" y=\"19\" as=\"sourcePoint\" />\n            <mxPoint x=\"204\" y=\"3\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-21\" value=\"(pulley)\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=10;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"3\" y=\"34\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-22\" value=\"(pulley)\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=10;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"299\" y=\"86\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-29\" value=\"(pen center)\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=10;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"128\" y=\"47\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-23\" value=\"m\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"150\" y=\"149\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-36\" value=\"\" style=\"shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;labelPosition=left;verticalLabelPosition=middle;align=right;verticalAlign=middle;rotation=10;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"153\" y=\"5\" width=\"10\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-38\" value=\"d_p\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"112\" y=\"2\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-35\" value=\"\" style=\"shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;flipH=1;labelPosition=right;verticalLabelPosition=middle;align=left;verticalAlign=middle;rotation=11;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"203.88\" y=\"15.670000000000002\" width=\"20\" height=\"150\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"m5EwUfMTjH4Hw6mmDvRn-37\" value=\"d_m\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"208.88\" y=\"81\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-27\" value=\"F_G\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"m5EwUfMTjH4Hw6mmDvRn-30\">\n          <mxGeometry x=\"102\" y=\"155\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-1\" value=\"\" style=\"verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.arc;startAngle=0.24823168451121255;endAngle=0.40955433894124044;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"7.25\" y=\"107.25\" width=\"135.5\" height=\"115.5\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-2\" value=\"\" style=\"endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;entryX=0.495;entryY=0.49;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.443;exitY=0.51;exitDx=0;exitDy=0;exitPerimeter=0;\" edge=\"1\" parent=\"1\" source=\"m5EwUfMTjH4Hw6mmDvRn-5\" target=\"m5EwUfMTjH4Hw6mmDvRn-6\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"120\" y=\"180\" as=\"sourcePoint\" />\n            <mxPoint x=\"330\" y=\"310\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-3\" value=\"\" style=\"verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.arc;startAngle=0.6435868456459786;endAngle=0.7516075710155771;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"731.13\" y=\"91.13\" width=\"147.75\" height=\"147.75\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-22\" value=\"phi_L\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"82.75\" y=\"170\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-23\" value=\"phi_R\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"730\" y=\"160\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-24\" value=\"\" style=\"endArrow=classic;html=1;rounded=0;\" edge=\"1\" parent=\"1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"244\" y=\"354\" as=\"sourcePoint\" />\n            <mxPoint x=\"213\" y=\"312\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-25\" value=\"F_L\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"211\" y=\"310\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-26\" value=\"F_R\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"444\" y=\"360\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-28\" value=\"\" style=\"endArrow=classic;html=1;rounded=0;\" edge=\"1\" parent=\"1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"467\" y=\"395\" as=\"sourcePoint\" />\n            <mxPoint x=\"507\" y=\"365\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-29\" value=\"\" style=\"endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.25;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"m5EwUfMTjH4Hw6mmDvRn-23\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"110\" y=\"330\" as=\"sourcePoint\" />\n            <mxPoint x=\"310\" y=\"570\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-30\" value=\"\" style=\"shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;flipH=1;labelPosition=right;verticalLabelPosition=middle;align=left;verticalAlign=middle;rotation=-80;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"358\" y=\"194\" width=\"20\" height=\"256.32\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-31\" value=\"\" style=\"endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"m5EwUfMTjH4Hw6mmDvRn-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"280\" y=\"350\" as=\"sourcePoint\" />\n            <mxPoint x=\"244\" y=\"354\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-32\" value=\"\" style=\"endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" target=\"m5EwUfMTjH4Hw6mmDvRn-2\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"467\" y=\"395\" as=\"sourcePoint\" />\n            <mxPoint x=\"248\" y=\"360\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"OY2j0D1znraPUxaiwNrB-34\" value=\"s\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"340\" y=\"291\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "images/doc/tangent_point.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" agent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36\" version=\"27.0.9\">\n  <diagram name=\"Page-1\" id=\"JntVAdGJVKhrkGMVg9V-\">\n    <mxGraphModel grid=\"1\" page=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-1\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"320\" y=\"320\" width=\"160\" height=\"160\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-3\" value=\"\" style=\"shape=sumEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"160\" y=\"70\" width=\"30\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-2\" value=\"\" style=\"endArrow=none;html=1;rounded=0;entryX=1;entryY=0;entryDx=0;entryDy=0;exitX=0.502;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;\" edge=\"1\" parent=\"1\" source=\"W8-vzGq-EH7mcyE8-KKW-3\" target=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"180\" y=\"80\" as=\"sourcePoint\" />\n            <mxPoint x=\"450\" y=\"400\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-4\" value=\"\" style=\"endArrow=none;html=1;strokeWidth=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"550\" y=\"280\" as=\"sourcePoint\" />\n            <mxPoint x=\"400\" y=\"400\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-6\" value=\"\" style=\"endArrow=none;html=1;strokeWidth=1;rounded=0;entryX=1;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" target=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"399\" y=\"345\" as=\"sourcePoint\" />\n            <mxPoint x=\"490\" y=\"320\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-9\" value=\"\" style=\"endArrow=none;html=1;strokeWidth=1;rounded=0;entryX=1;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" target=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"457\" y=\"400\" as=\"sourcePoint\" />\n            <mxPoint x=\"457\" y=\"343\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-10\" value=\"\" style=\"endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;\" edge=\"1\" parent=\"1\" source=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"410\" y=\"290\" as=\"sourcePoint\" />\n            <mxPoint x=\"400\" y=\"480\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-11\" value=\"\" style=\"endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"W8-vzGq-EH7mcyE8-KKW-1\" target=\"W8-vzGq-EH7mcyE8-KKW-1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"250\" y=\"340\" as=\"sourcePoint\" />\n            <mxPoint x=\"250\" y=\"500\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-12\" value=\"r\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"395\" y=\"350\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-13\" value=\"q\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"390\" y=\"320\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-14\" value=\"q\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"433\" y=\"360\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"W8-vzGq-EH7mcyE8-KKW-15\" value=\"(pulley)\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"340\" y=\"430\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "include/README",
    "content": "\nThis directory is intended for project header files.\n\nA header file is a file containing C declarations and macro definitions\nto be shared between several project source files. You request the use of a\nheader file in your project source file (C, C++, etc) located in `src` folder\nby including it, with the C preprocessing directive `#include'.\n\n```src/main.c\n\n#include \"header.h\"\n\nint main (void)\n{\n ...\n}\n```\n\nIncluding a header file produces the same results as copying the header file\ninto each source file that needs it. Such copying would be time-consuming\nand error-prone. With a header file, the related declarations appear\nin only one place. If they need to be changed, they can be changed in one\nplace, and programs that include the header file will automatically use the\nnew version when next recompiled. The header file eliminates the labor of\nfinding and changing all the copies as well as the risk that a failure to\nfind one copy will result in inconsistencies within a program.\n\nIn C, the usual convention is to give header files names that end with `.h'.\nIt is most portable to use only letters, digits, dashes, and underscores in\nheader file names, and at most one dot.\n\nRead more about using header files in official GCC documentation:\n\n* Include Syntax\n* Include Operation\n* Once-Only Headers\n* Computed Includes\n\nhttps://gcc.gnu.org/onlinedocs/cpp/Header-Files.html\n"
  },
  {
    "path": "lib/README",
    "content": "\nThis directory is intended for project specific (private) libraries.\nPlatformIO will compile them to static libraries and link into executable file.\n\nThe source code of each library should be placed in a an own separate directory\n(\"lib/your_library_name/[here are source files]\").\n\nFor example, see a structure of the following two libraries `Foo` and `Bar`:\n\n|--lib\n|  |\n|  |--Bar\n|  |  |--docs\n|  |  |--examples\n|  |  |--src\n|  |     |- Bar.c\n|  |     |- Bar.h\n|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html\n|  |\n|  |--Foo\n|  |  |- Foo.c\n|  |  |- Foo.h\n|  |\n|  |- README --> THIS FILE\n|\n|- platformio.ini\n|--src\n   |- main.c\n\nand a contents of `src/main.c`:\n```\n#include <Foo.h>\n#include <Bar.h>\n\nint main (void)\n{\n  ...\n}\n\n```\n\nPlatformIO Library Dependency Finder will find automatically dependent\nlibraries scanning project source files.\n\nMore information about PlatformIO Library Dependency Finder\n- https://docs.platformio.org/page/librarymanager/ldf.html\n"
  },
  {
    "path": "partitions.csv",
    "content": "# ESP-IDF Partition Table\n# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x6000,\nphy_init, data, phy,     0xf000,  0x1000,\nfactory,  app,  factory, 0x10000, 1200K,\nspiffs, data, spiffs, 0x13C000, 2800K,"
  },
  {
    "path": "platformio.ini",
    "content": "; PlatformIO Project Configuration File\n;\n;   Build options: build flags, source filter\n;   Upload options: custom upload port, speed and extra flags\n;   Library options: dependencies, extra library storages\n;   Advanced options: extra scripting\n;\n; Please visit documentation for the other options and examples\n; https://docs.platformio.org/page/projectconf.html\n\n[env:esp32dev]\nplatform = espressif32\nboard = esp32dev\nboard_build.filesystem = littlefs\nboard_build.partitions = partitions.csv\nmonitor_filters = esp32_exception_decoder\nextra_scripts = build.py\nframework = arduino\n\nlib_deps = \n\thttps://github.com/tzapu/WiFiManager.git@2.0.17\n\thttps://github.com/ESP32Async/ESPAsyncWebServer.git@^3.7.4\n\thttps://github.com/madhephaestus/ESP32Servo.git@^3.0.9\n\thttps://github.com/adafruit/Adafruit_SSD1306.git@^2.5.13\n\twaspinator/AccelStepper@^1.64\n\tbblanchon/ArduinoJson@^5.13.4\n"
  },
  {
    "path": "src/display.cpp",
    "content": "#include \"display.h\"\n#include <Adafruit_SSD1306.h>\n\n#define SCREEN_WIDTH 128 // OLED display width, in pixels\n#define SCREEN_HEIGHT 64 // OLED display height, in pixels\nDisplay::Display() {\n    display = new Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);\n    if (!display->begin(SSD1306_SWITCHCAPVCC, 0x3C))\n    { // Address 0x3D for 128x64\n        Serial.println(F(\"SSD1306 allocation failed\"));\n        throw std::invalid_argument(\"not ready\");\n    }\n    delay(2000);\n    display->setRotation(2);\n    display->clearDisplay();\n    display->setTextColor(WHITE);\n    display->setTextSize(1);\n    display->display();\n}\n\nvoid Display::displayText(String text)\n{\n    int16_t x1;\n    int16_t y1;\n    uint16_t width;\n    uint16_t height;\n\n    display->getTextBounds(text, 0, 0, &x1, &y1, &width, &height);\n\n    // display on horizontal and vertical center\n    display->clearDisplay(); // clear display\n    display->setCursor((SCREEN_WIDTH - width) / 2, (SCREEN_HEIGHT - height) / 2);\n    display->println(text); // text to display\n    display->display();\n    Serial.println(\"Displayed \" + text);\n}\n\nvoid Display::displayHomeScreen(String ipLine, String orLine, String mdnsLine) {\n    display->clearDisplay();\n\n    int16_t x1;\n    int16_t y1;\n    uint16_t width;\n    uint16_t height;\n\n    display->getTextBounds(ipLine, 0, 0, &x1, &y1, &width, &height);\n    display->setCursor((SCREEN_WIDTH - width) / 2, 10);\n    display->println(ipLine);\n\n    display->getTextBounds(orLine, 0, 0, &x1, &y1, &width, &height);\n    display->setCursor((SCREEN_WIDTH - width) / 2, 10 + SCREEN_HEIGHT / 3);\n    display->println(orLine);\n\n    display->getTextBounds(mdnsLine, 0, 0, &x1, &y1, &width, &height);\n    display->setCursor((SCREEN_WIDTH - width) / 2, 10 + SCREEN_HEIGHT / 3 * 2);\n    display->println(mdnsLine);\n\n    display->display();\n}"
  },
  {
    "path": "src/display.h",
    "content": "#ifndef Display_h\n#define Display_h\n#include <Adafruit_SSD1306.h>\n\nclass Display {\n    private:\n    Adafruit_SSD1306 *display;\n    public:\n    Display();\n    void displayText(String text);\n    void displayHomeScreen(String ipLine, String orLine, String mdnsLine);\n};\n#endif"
  },
  {
    "path": "src/main.cpp",
    "content": "#include <Arduino.h>\n#include <WiFiManager.h>\n#include <AsyncTCP.h>\n#include <ESPAsyncWebServer.h>\n#include <FS.h>\n#include <LittleFS.h>\n#include <Wire.h>\n#include <ESPmDNS.h>\n#include \"movement.h\"\n#include \"runner.h\"\n#include \"pen.h\"\n#include \"display.h\"\n#include \"phases/phasemanager.h\"\n\nAsyncWebServer server(80);\n\nMovement *movement;\nRunner *runner;\nPen *pen;\nDisplay *display;\n\nPhaseManager* phaseManager;\n\nvoid notFound(AsyncWebServerRequest *request)\n{\n    request->send(404, \"text/plain\", \"Not found\");\n}\n\nvoid handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {\n    phaseManager->getCurrentPhase()->handleUpload(request, filename, index, data, len, final);\n}\n\nvoid handleGetState(AsyncWebServerRequest *request) {\n    phaseManager->respondWithState(request);\n}\n\nstd::vector<const char *> menu = {\"wifi\", \"sep\"};\nvoid setup()\n{\n    delay(10);\n    Serial.begin(9600);\n\n    if (!LittleFS.begin(true)) {\n        Serial.println(\"An Error has occurred while mounting LittleFS\");\n        return;\n    }\n\n    display = new Display();\n    Serial.println(\"Initialized display\");\n\n    // initialize movement right away or the motors can start creeping due to floating output\n    movement = new Movement(display);\n    Serial.println(\"Initialized steppers\");\n\n    bool resetAfterConnect = false;\n    std::function<void()> serverCallback = [&] () {\n        resetAfterConnect = true;\n    };\n    \n    WiFiManager wifiManager;\n    \n    wifiManager.setConnectTimeout(20);\n    wifiManager.setTitle(\"Connect to WiFi\");\n    wifiManager.setMenu(menu);\n    wifiManager.setWebServerCallback(serverCallback);\n    wifiManager.autoConnect(\"Mural\");\n\n    if (resetAfterConnect) {\n        Serial.println(\"Connected to WiFi through captive portal, restarting...\");\n        ESP.restart();\n    }\n    \n    Serial.println(\"Connected to wifi\");\n\n    MDNS.begin(\"mural\");\n\n    Serial.println(\"Started mDNS for mural\");\n\n    pen = new Pen();\n    Serial.println(\"Initialized servo\");\n\n    runner = new Runner(movement, pen, display);\n    Serial.println(\"Initialized runner\");\n\n    server.serveStatic(\"/\", LittleFS, \"/www/\").setDefaultFile(\"index.html\").setCacheControl(\"no-cache\");\n\n    server.on(\"/command\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->handleCommand(request); });\n\n    server.on(\"/setTopDistance\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->setTopDistance(request); });\n\n    server.on(\"/extendToHome\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->extendToHome(request); });\n\n    server.on(\"/setServo\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->setServo(request); });\n\n    server.on(\"/setPenDistance\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->setPenDistance(request); });\n\n    server.on(\"/estepsCalibration\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->estepsCalibration(request); });\n\n    server.on(\"/doneWithPhase\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->doneWithPhase(request); });\n\n    server.on(\"/run\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->run(request); });\n\n    server.on(\"/resume\", HTTP_POST, [](AsyncWebServerRequest *request)\n              { phaseManager->getCurrentPhase()->resumeTopDistance(request); });\n\n    server.on(\"/getState\", HTTP_GET, [](AsyncWebServerRequest *request)\n              { handleGetState(request); });\n\n    server.on(\n        \"/uploadCommands\", HTTP_POST,\n        [](AsyncWebServerRequest *request) {\n            handleGetState(request);\n        }, \n        handleUpload\n    );\n\n    server.on(\n        \"/downloadCommands\", HTTP_GET,\n        [](AsyncWebServerRequest *request) {\n            request->send(LittleFS, \"/commands\", \"text/plain\");\n        }\n    );\n\n    server.onNotFound(notFound);\n\n    Serial.println(\"Finished setting up the server\");\n\n    phaseManager = new PhaseManager(movement, pen, runner, &server);\n\n    server.begin();\n    Serial.println(\"Server started\");\n\n    display->displayHomeScreen(\"http://\" + WiFi.localIP().toString(), \"or\", \"http://mural.local\");\n    \n}\n\nvoid loop()\n{\n    movement->runSteppers();\n    runner->run();\n    phaseManager->getCurrentPhase()->loopPhase();\n}\n"
  },
  {
    "path": "src/movement.cpp",
    "content": "#include \"movement.h\"\n#include \"display.h\"\n#include <stdexcept>\n\nMovement::Movement(Display *display)\n{\n    this->display = display;\n   \n    leftMotor = new AccelStepper(AccelStepper::DRIVER, LEFT_STEP_PIN, LEFT_DIR_PIN);\n    leftMotor->setEnablePin(LEFT_ENABLE_PIN);\n    leftMotor->setMaxSpeed(moveSpeedSteps);\n    leftMotor->setPinsInverted(true);\n    leftMotor->disableOutputs();\n\n    rightMotor = new AccelStepper(AccelStepper::DRIVER, RIGHT_STEP_PIN, RIGHT_DIR_PIN);\n    rightMotor->setEnablePin(RIGHT_ENABLE_PIN);\n    rightMotor->setMaxSpeed(moveSpeedSteps);\n    rightMotor->disableOutputs();\n\n    topDistance = -1;\n   \n    moving = false;\n    homed = false;\n    startedHoming = false;\n};\n\nvoid Movement::setTopDistance(const int distance) {\n    Serial.printf(\"Top distance set to %s\\n\", String(distance));\n    topDistance = distance;                         // = d_pins [mm]\n\n    minSafeY = safeYFraction * topDistance;         // = top_margin * d_pins [mm]\n    minSafeXOffset = safeXFraction * topDistance;   // = side_margin * d_pins [mm]\n    width = topDistance - 2 * minSafeXOffset;       // width of the drawing area [mm]\n};\n\nvoid Movement::resumeTopDistance(int distance /* = d_pin in mm */) {\n    setTopDistance(distance);\n    homed = true;\n\n    const Point homeCoordinates = getHomeCoordinates();\n    X = homeCoordinates.x;\n    Y = homeCoordinates.y;\n\n    const Lengths lengths = getBeltLengths(homeCoordinates.x, homeCoordinates.y);\n    leftMotor->setCurrentPosition(lengths.left);\n    rightMotor->setCurrentPosition(lengths.right);\n\n    moving = false;\n}\n\nvoid Movement::setOrigin()\n{\n    leftMotor->setCurrentPosition(homedStepsOffset);\n    rightMotor->setCurrentPosition(homedStepsOffset);\n    homed = true;\n};\n\nvoid Movement::leftStepper(const int dir)\n{\n    if (dir > 0)\n    {\n        leftMotor->move(INFINITE_STEPS);\n        leftMotor->setSpeed(printSpeedSteps);\n    }\n    else if (dir < 0)\n    {\n        leftMotor->move(-INFINITE_STEPS);\n        leftMotor->setSpeed(printSpeedSteps);\n    }\n    else\n    {\n        leftMotor->setAcceleration(acceleration);\n        leftMotor->stop();\n    }\n\n    moving = true;\n};\n\nvoid Movement::rightStepper(const int dir)\n{\n    if (dir > 0)\n    {\n        rightMotor->move(INFINITE_STEPS);\n        rightMotor->setSpeed(printSpeedSteps);\n    }\n    else if (dir < 0)\n    {\n        rightMotor->move(-INFINITE_STEPS);\n        rightMotor->setSpeed(printSpeedSteps);\n    }\n    else\n    {\n        rightMotor->setAcceleration(acceleration);\n        rightMotor->stop();\n    }\n\n    moving = true;\n};\n\nMovement::Point Movement::getHomeCoordinates() {\n    if (topDistance == -1) {\n        return Point(0, 0);\n    }\n\n    return Point(width / 2, HOME_Y_OFFSET_MM);\n}\n\nint Movement::extendToHome()\n{\n    setOrigin();\n\n    auto homeCoordinates = getHomeCoordinates();\n    startedHoming = true;\n    auto moveTime = beginLinearTravel(homeCoordinates.x, homeCoordinates.y, moveSpeedSteps);\n    return int(ceil(moveTime));\n};\n\nvoid Movement::runSteppers()\n{\n    if (moving)\n    {\n        leftMotor->runSpeedToPosition();\n        rightMotor->runSpeedToPosition();\n\n        if (leftMotor->distanceToGo() == 0 && rightMotor->distanceToGo() == 0)\n        {\n            moving = false;\n            //Serial.printf(\"Motion complete. Left steps: %ld, Right steps: %ld\\n\", leftMotor->currentPosition(), rightMotor->currentPosition());\n        }\n    }\n};\n\ninline void Movement::getLeftTangentPoint(const double frameX, const double frameY, const double gamma, double& x_PL, double& y_PL) const {\n    // Input frameX and frameY are the coordinates of the pen center.\n    const double s_L = d_t / 2.0;   // Distance of left and right tangent point from point Q. [mm]\n    const double P_LX = s_L * cos(gamma) - d_p * sin(gamma); // [mm] distance from pen center in x\n    const double P_LY = s_L * sin(gamma) + d_p * cos(gamma); // [mm] .. and y\n    x_PL = frameX - P_LX;    // [mm] Left pulley tangent point in frame coordinate system.\n    y_PL = frameY - P_LY;    // [mm]\n}\n\ninline void Movement::getRightTangentPoint(const double frameX, const double frameY, const double gamma, double& x_PR, double& y_PR) const {\n    // Coordinates of right pulley tangent point:\n    const double s_R = d_t / 2.0;\n    const double P_RX = s_R * cos(gamma) + d_p * sin(gamma); // [mm]\n    const double P_RY = s_R * sin(gamma) - d_p * cos(gamma); // [mm]\n    x_PR = frameX + P_RX;    // [mm] Right pulley tangent point in frame coordinate system.\n    y_PR = frameY + P_RY;    // [mm]\n}\n\n// Compute angles of the belts and the forces on them.\n// Input: - Mural coordinates X and Y in frame coordinate system [mm].\n//        - Mural inclination gamma [rad].\n// Output: - belt angles phi_L, phi_R [rad], measured against the line connecting the pins.\nvoid Movement::getBeltAngles(const double frameX, const double frameY, const double gamma, double& phi_L, double& phi_R) const {\n    double x_PL;\n    double y_PL;\n    getLeftTangentPoint(frameX, frameY, gamma, x_PL, y_PL);\n    phi_L = atan2(y_PL, x_PL);     // Angle of left belt, measured from line connecting the pins. [rad]\n\n    double x_PR;\n    double y_PR;\n    getRightTangentPoint(frameX, frameY, gamma, x_PR, y_PR);\n    phi_R = atan2(y_PR, topDistance - x_PR);     // Angle of left belt, measured from line connecting the pins. [rad]\n}\n\nvoid Movement::getBeltForces(const double phi_L, const double phi_R, double& F_L, double&F_R) const {\n    // Computing the Forces. \n    // Force vectors are parallel to their belts, so the direction is given by phi_R and phi_L.\n    // We assume that the bot is in a stable state (no torque), which allows us for having\n    // the force vectors of left (L) and right (R) pulley meet in a single point. \n    // In this stable state the pulley forces cancel out the gravity force in x and y.\n    // Note this is an approximation which is refined due to iteratively updating the values (torque, angles, forces). \n    const double F_G = mass_bot * g_constant;       // [N] Gravity force is pulling bot down. No x component.\n    F_R = F_G * cos(phi_L) / sin(phi_L + phi_R);    // [N] magnitude of the force vector\n    F_L = F_G * cos(phi_R) / sin(phi_L + phi_R);    // [N]\n    // double F_Ly = F_L * sin(phi_L);                         // [N] components in y and x\n    // double F_Lx = F_L * sin(phi_L);                         // [N] ...\n    // double F_Ry = F_R * sin(phi_R);                         // [N]\n    // double F_Rx = F_R * sin(phi_R);                         // [N]\n}\n\ndouble Movement::solveTorqueEquilibrium(const double phi_L, const double phi_R, const double F_L, const double F_R, const double gamma_init) const {\n    // Solve for torque equilibrium: As the belts are pulling on two distinct point, there's a torque rotating the\n    // bot around a reference point. Here, we assume this reference point corresponds to Q, where tangent line d_t\n    // and mass line d_m meet.\n    // In the static case the residual torque is zero, which occurs at a certain inclination gamma. The goal here is\n    // to find this gamma.\n    const double s_L = d_t / 2.0;   // [mm] Lenght of the effective arm for the left pulley.\n    const double s_R = d_t / 2.0;   // [mm]\n\n    double gamma_best = 99999999;\n    double T_delta_best = 99999999;\n\n    // Solver parameters.\n    constexpr double gamma_step = 0.20 * PI / 180.0;   // [rad] solver step width.\n    constexpr double gamma_min = -90.0 * PI / 180.0;   // [rad] Solver search range: max and min values.\n    constexpr double gamma_max = 90.0 * PI / 180.0;    // [rad]\n    constexpr double gamma_search_window = 2.0 * PI / 180.0;    // [rad] Solver will focus on gamma_init +- gamma_search_window.\n    \n    // Simple solver: finding the minimum T_delta by searching over the range specified above:\n    for (double gamma = gamma_init - gamma_search_window;\n            gamma > gamma_min &&\n            gamma < gamma_max &&\n            gamma <= gamma_init + gamma_search_window;\n            gamma += gamma_step){\n        const double alpha = phi_L - gamma;   // [rad] Angle between left belt and line connecting tangent points (of pulleys and belts).\n        const double beta = phi_R + gamma;    // [rad] Angle between right belt and line connecting tangent points.\n    \n        double T_L = /* s_L * F_L = */ s_L * sin(alpha) * F_L;  // [N * mm]\n        double T_R = s_R * sin(beta) * F_R;                     // [N * mm]\n\n        // The center of mass sits under the center of line connecting the tangent points.\n        double s_m = d_m * tan(gamma);                          // [mm]\n        const double F_G = mass_bot * g_constant;               // [N] Gravity force is pulling bot down. No x component.\n        double F_m = F_G * cos(gamma);\n        double T_m = s_m * F_m;                                 // [N * mm]\n\n        // Left pulley tries to turn the bot clockwise. Right pulley ccw. Gravity ccw if gamma is positive (i.e. the bot inclined to the right).\n        double T_delta = T_R - T_L + T_m;                       // [N * mm]\n        // Solve gamma for T_delta = 0.0 .\n        if (abs(T_delta) < abs(T_delta_best)){\n            T_delta_best = T_delta;\n            gamma_best = gamma;\n            // Serial.printf(\"  solveTorqueEquilibrium: T_delta=%1.4f @ gamma=%1.4f, T_delta_best=%1.4f @ gamma_best=%1.4f\\n\",\n            //     T_delta, gamma, T_delta_best, gamma_best);\n        } else {\n            // There is only one zero crossing: terminate early if T_delta gets worse than best one so far.\n\n            // Serial.printf(\"  solveTorqueEquilibrium: T_delta=%1.4f @ gamma=%1.4f, T_delta_best=%1.4f @ gamma_best=%1.4f Exit function.\\n\",\n            //     T_delta, gamma, T_delta_best, gamma_best);\n            return gamma_best;\n        }\n    }\n\n    return gamma_best;\n}\n\ninline double Movement::getDilationCorrectedBeltLength(double belt_length, double F_belt) const {\n    // Apply belt length correction: The belts stretch because of Mural's mass. \n    // This function returns a (shorter) length of the belt, such that with gravity the belt\n    // exactly as long as required.\n    const double elongation_factor = 1 + belt_elongation_coefficient * F_belt;\n    const double belth_length_corrected = belt_length / elongation_factor;\n    return belth_length_corrected;\n}\n\n// Calculate the lengths of the left and right belt in mm based on the input coordinates.\n// input: x [mm], y [mm] ; both in image coordinate system\n// output: Struct containing the target stepper position for each motor to move.\nMovement::Lengths Movement::getBeltLengths(const double x, const double y) {\n    // Mural rotates as it moves towards the sides. As this happens, Mural's coordinate\n    // system rotates as well, which would mean straight lines become curved. Therefore, \n    // a compensation in this rotated system is computed and applied.\n    // !!!! Please see KinematicModel.md for a more detailed explanation !!!!\n    //\n    // This function works as follows:\n    // 1 Compute the belt length in the wall plane first:\n    //   {\n    //      compute belt angles phi_L and phi_R\n    //      compute forces on both belts\n    //      compute torque on mural, solve for mural inclination gamma\n    //      loop (if needed)\n    //      result: mural inclination, x and y correction, and belt forces\n    //   }\n    // 2 Compute 3D belt length: Euclidean distance due to Pulleys not being in same (wall) plane\n    //   as belt anchors (pins).\n    // 3 Apply dilation correction to account for non-rigid belts.\n\n\n    // Coordinate systems:\n    // Frame coordinate system: Outer frame defined by the belt pins. Origin is the center of the left pin.\n    //      x-axis points right towards the right pin. y-axis is perpendicular to x, pointing down.\n    // Image coordinate system:\n    //      This coordinate system defines the actual drawing area. The origin is in the top left corner \n    //      of the image to be drawn. It is shifted by safeYFraction * d_pins down from the line connecting the pins.\n    //      Additionally, it's shifted safeXFraction to the right from the y-axis of the frame coordinate system.\n    //      So, in frame coordinates the origin of the image coordinate system is \n    //      (safeYFraction * d_pins, safeXFraction * d_pins).\n    //      See also /images/doc/muralbot_image_positioning.svg . \n\n    // Pen coordinates in frame coordinate system.\n    const double frameX = x + minSafeXOffset;\n    const double frameY = y + minSafeY;\n\n    double gamma = gamma_last_position;              // Inclination of the bot [rad]. 0: Bot is horizontal. gamma>0: Bot tilts to the right.\n    double phi_L = 0.0;\n    double phi_R = 0.0;\n    double F_L = 0.0;                               // [N] magnitude of the force vector (left belt)\n    double F_R = 0.0;                               // [N] magnitude of the force vector (right belt)\n    constexpr int solver_max_iterations = 20;       // Maximum number of outer loop iterations of the solver.\n    constexpr double gamma_delta_termination = 0.25 / 180.0 * PI; // [rad] Outer loop of solver will stop if last update is smaller than this. \n                                                                  // Value should be greater than gamma step size in solveTorqueEquilibrium.\n\n    // Solve for belt angles phi and bot inclination gamma by running a few rounds.\n    int debug_step_count = 0;\n    for (int i = 0; i < solver_max_iterations; i++){\n        getBeltAngles(frameX, frameY, gamma, phi_L, phi_R);\n\n        getBeltForces(phi_L, phi_R, F_L, F_R);\n\n        const double gamma_last = gamma;\n        gamma = solveTorqueEquilibrium(phi_L, phi_R, F_L, F_R, gamma);\n        // Serial.printf(\" Solver loop: i=%d, frameX=%1.2f, frameY=%1.2f, phi_L=%1.4f, phi_R=%1.4f, F_L=%1.2f, F_R=%1.2f, gamma=%1.4f\\n\", \n        //     i, frameX, frameY, phi_L, phi_R, F_L, F_R, gamma);\n        debug_step_count = i;\n        if (abs(gamma_last - gamma) < gamma_delta_termination) break;\n    }\n    gamma_last_position = gamma;\n    // Serial.printf(\"Solver found: frameX=%1.2f, frameY=%1.2f, phi_L=%1.4f, phi_R=%1.4f, F_L=%1.2f, F_R=%1.2f, debug_step_count=%d, gamma=%1.4f\\n\", \n    //     frameX, frameY, phi_L, phi_R, F_L, F_R, debug_step_count, gamma);\n\n    double leftX, leftY;\n    double rightX, rightY;\n    getLeftTangentPoint(frameX, frameY, gamma, leftX, leftY);\n    getRightTangentPoint(frameX, frameY, gamma, rightX, rightY);\n\n    // Left and right leg distances flush to the wall.\n    const double leftLegFlat = sqrt(pow(leftX, 2) + pow(leftY, 2));\n    const double rightLegFlat = sqrt(pow(topDistance - rightX, 2) + pow(rightY, 2));\n\n    // Left and right leg distances including the standoff length.\n    double leftLeg = sqrt(pow(leftLegFlat, 2) + pow(midPulleyToWall, 2));\n    double rightLeg = sqrt(pow(rightLegFlat, 2) + pow(midPulleyToWall, 2));\n\n    leftLeg = getDilationCorrectedBeltLength(leftLeg, F_L);\n    rightLeg = getDilationCorrectedBeltLength(rightLeg, F_R);\n    \n    const double leftLegSteps = int((leftLeg / circumference) * stepsPerRotation);\n    const double rightLegSteps = int((rightLeg / circumference) * stepsPerRotation);\n\n    return Lengths(leftLegSteps, rightLegSteps);\n}\n\nfloat Movement::beginLinearTravel(double x, double y, int speed)\n{\n    X = x;\n    Y = y;\n    if (topDistance == -1 || !homed) {\n        Serial.println(\"Not ready\");\n        throw std::invalid_argument(\"not ready\");\n    }\n\n    if (x < 0 || (x - 1) > width)\n    {\n        Serial.println(\"Invalid x\");\n        throw std::invalid_argument(\"Invalid x\");\n    }\n\n    if (y < 0)\n    {\n        Serial.println(\"Invalid y\");\n        throw std::invalid_argument(\"Invalid y\");\n    }\n\n    auto lengths = getBeltLengths(x, y);\n    auto leftLegSteps = lengths.left;\n    auto rightLegSteps = lengths.right;\n\n    auto deltaLeft = int(abs(abs(leftMotor->currentPosition()) - leftLegSteps));\n    auto deltaRight = int(abs(abs(rightMotor->currentPosition()) - rightLegSteps));\n\n    float leftSpeed, rightSpeed, moveTime;\n    if (deltaLeft >= deltaRight)\n    {\n        leftSpeed = speed;\n        moveTime = deltaLeft / leftSpeed;\n        rightSpeed = deltaRight / moveTime;\n    }\n    else\n    {\n        rightSpeed = speed;\n        moveTime = deltaRight / rightSpeed;\n        leftSpeed = deltaLeft / moveTime;\n    }\n\n    //Serial.printf(\"Begin movement: X(%s) Y(%s) UnsafeX(%s) UnsafeY(%s) leftLeg(%s) rightLeg(%s) deltaLeft(%s) deltaRight(%s) leftSpeed(%s) rightSpeed(%s) \\n\", String(x), String(y), String(unsafeX), String(unsafeY), String(leftLeg), String(rightLeg), String(deltaLeft), String(deltaRight), String(leftSpeed), String(rightSpeed));\n    leftMotor->moveTo(leftLegSteps);\n    leftMotor->setSpeed(leftSpeed);\n    \n    rightMotor->moveTo(rightLegSteps);\n    rightMotor->setSpeed(rightSpeed);\n\n    //display->displayText(String(X) + \", \" + String(Y));\n    // delay(sleepDurationAfterMove_ms);\n\n    moving = true;\n    return moveTime;\n};\n\ndouble Movement::getWidth() {\n    if (topDistance == -1) {\n        throw std::invalid_argument(\"not ready\");\n    }\n    return width;\n}\n\nMovement::Point Movement::getCoordinates() {\n    if (X == -1 || Y == -1) {\n        Serial.println(\"Not ready to get coordinates\");\n        throw std::invalid_argument(\"not ready\");\n    }\n\n    if (moving) {\n        Serial.println(\"Can't get coordinates while moving\");\n        throw std::invalid_argument(\"not ready\");\n    }\n    return Movement::Point(X, Y);\n}\n\nvoid Movement::extend1000mm() {\n    const int steps = int((1000 / circumference) * stepsPerRotation);   \n\n    leftMotor->move(steps);\n    leftMotor->setSpeed(moveSpeedSteps);\n\n    rightMotor->move(steps);\n    rightMotor->setSpeed(moveSpeedSteps);\n\n    moving = true;\n}\n\nvoid Movement::disableMotors() {\n    leftMotor->disableOutputs();\n    rightMotor->disableOutputs();\n}\n\nbool Movement::isMoving() {\n    return moving;\n}\n\nbool Movement::hasStartedHoming() {\n    return startedHoming;\n}\n\nint Movement::getTopDistance() {\n    return topDistance;\n}\n"
  },
  {
    "path": "src/movement.h",
    "content": "#ifndef Movement_h\n#define Movement_h\n\n#include \"AccelStepper.h\"\n#include \"Arduino.h\" \n#include \"display.h\"\n\n// Motor driver parameters.\nconstexpr int printSpeedSteps = 500;\nconstexpr int  moveSpeedSteps = 1500;\nconstexpr long INFINITE_STEPS = 999999999;\nconstexpr long acceleration = 999999999;  // Essentially infinite, causing instant stop / start\nconstexpr int stepsPerRotation = 200 * 8; // 1/8 microstepping\n\n// Geometry parameters:\n// Effective diameter of the pulley+belts. Use EStep calibration to refine this value.\nconstexpr double diameter = 12.69;          // [mm]\nconst double circumference = diameter * PI; // [mm]\nconstexpr double midPulleyToWall = 41.0;    // (Height) distance from mid of pulley to wall [mm].\nconstexpr float homedStepOffsetMM = 40.0;   // Length of fully retracted belt hitting stop screw.\n                                            // Measured from outer edge of screw to the point\n                                            // of tangency between belt and pulley. [mm]\nconst int homedStepsOffset = int((homedStepOffsetMM / circumference) * stepsPerRotation);\nconstexpr double mass_bot = 0.55;   // Mass of the mural bot [kg].\nconstexpr double g_constant = 9.81; // Earth's gravitational acceleration constant [m/s^2]. Please adjust when running Mural on other planets!\nconstexpr double d_t = 76.027;      // [mm] Distance of tangent points, where belts touch the pulleys.\n                                    // Calculated as (axis distance) 85.00 - (diameter) 12.69/sqrt(2).\nconstexpr double d_p = 4.4866;      // [mm] distance from Q to center of pen. Calculated as diameter/(2 * sqrt(2)).\nconstexpr double d_m = 10.0 + d_p;  // [mm] Distance from line connecting tangent points to center of mass of bot (projected onto wall plane).\n                                    // The point where d_m and d_t meet shall be called Q.\n                                    // The center of mass sits roughly at the bottom of the pen opening.\nconstexpr double belt_elongation_coefficient = 5e-5; // [m/N] elongation of the belts under force.\nconst int HOME_Y_OFFSET_MM = 350;   // Y coordinate of mural home position in image coordinate system [mm].\n\n\n// Margins used for transformations of the coordinate systems:\nconstexpr double safeYFraction = 0.2;           // Top Margin: Image top to topDistance line.\nconstexpr double safeXFraction = 0.2;           // Left and right margin: from draw area boundaries to line from each pin straight down.\n\n// Variables used for debugging:\n// constexpr int sleepDurationAfterMove_ms = 0;    // Delay after linear movement [ms], e.g. 50.\n\n// ESP setup:\nconstexpr int LEFT_STEP_PIN = 13;\nconstexpr int LEFT_DIR_PIN = 12;\nconstexpr int LEFT_ENABLE_PIN = 14;\n\nconstexpr int RIGHT_STEP_PIN = 27;\nconstexpr int RIGHT_DIR_PIN = 26;\nconstexpr int RIGHT_ENABLE_PIN = 25;\n\nclass Movement{\nprivate:\n    int topDistance;            // Distance between pins (d_pins) [mm].\n    double minSafeY;\n    double minSafeXOffset;\n    double width;               // width of the drawing area [mm]\n    volatile bool moving;\n    bool homed;\n    double X = -1;              // Location of Pen in x [mm].\n    double Y = -1;              // Location of Pen in y [mm].\n    bool startedHoming;\n    AccelStepper *leftMotor;\n    AccelStepper *rightMotor;\n    Display *display;\n    void setOrigin();\n\n    struct Lengths {\n        int left;\n        int right;\n        Lengths(int left, int right) {\n            this->left = left;\n            this->right = right;\n        }\n        Lengths() {\n\n        }\n    };\n\n    Lengths getBeltLengths(double x, double y);\n\n    double gamma_last_position = 0.0;   // [rad] The last known inclination of the mural bot. As the angle changes only slowly \n                                        // with position we can compute updates faster by keeping track of the last solution.\n    inline void getLeftTangentPoint(const double frameX, const double frameY, const double gamma, double& x_PL, double& y_PL) const;\n    inline void getRightTangentPoint(const double frameX, const double frameY, const double gamma, double& x_PR, double& y_PR) const;\n    void getBeltAngles(const double frameX, const double frameY, const double gamma, double& phi_L, double& phi_R) const;\n    void getBeltForces(const double phi_L, const double phi_R, double& F_L, double&F_R) const;\n    double solveTorqueEquilibrium(const double phi_L, const double phi_R, const double F_L, const double F_R, const double gamma_start) const;\n    double getDilationCorrectedBeltLength(double belt_length, double F_belt) const;\n    \npublic:\n    Movement(Display *display);\n    struct Point {\n        double x;\n        double y;\n        Point(double x, double y) {\n            this->x = x;\n            this->y = y;\n        }\n        Point() {\n        }\n    };\n\n    static double distanceBetweenPoints(Point point1, Point point2) {\n        return sqrt(pow(point2.x - point1.x, 2) + pow(point2.y - point1.y, 2));\n    }\n\n    bool isMoving();\n    bool hasStartedHoming();\n    double getWidth();\n    Point getCoordinates();\n    void setTopDistance(const int distance);\n    void resumeTopDistance(const int distance);\n    int getTopDistance();\n    void leftStepper(const int dir);\n    void rightStepper(const int dir);\n    int extendToHome();\n    void runSteppers();\n    float beginLinearTravel(double x, double y, int speed);\n\n    // Used for calibration of the esteps.\n    void extend1000mm(); \n\n    Point getHomeCoordinates();\n    void disableMotors();\n};\n\n#endif"
  },
  {
    "path": "src/pen.cpp",
    "content": "#include \"pen.h\"\n\nbool shouldStop(int currentDegree, int targetDegree, bool positive) {\n    if (positive) {\n        return currentDegree > targetDegree;\n    } else {\n        return currentDegree < targetDegree;\n    }\n}\n\nvoid doSlowMove(Pen* pen, int startDegree, int targetDegree, int speedDegPerSec) {\n    if (startDegree == targetDegree) {\n        return;\n    }\n\n    auto startTime = millis();\n\n    bool positive;\n    if (targetDegree > startDegree) {\n        positive = true;\n    } else {\n        positive = false;\n    }\n\n    auto currentDegree = startDegree;\n\n    while (!(shouldStop(currentDegree, targetDegree, positive))) {\n        pen->setRawValue(currentDegree);\n        delay(10);\n\n        auto currentTime = millis();\n        auto deltaTime = currentTime - startTime;\n        auto progressDegrees = int(double(deltaTime) / 1000 * speedDegPerSec);\n\n        if (!positive) {\n            progressDegrees = progressDegrees * -1;\n        }\n\n        currentDegree = startDegree + progressDegrees;\n    }\n    pen->setRawValue(targetDegree);\n    delay(200);\n}\n\n\nPen::Pen()\n{\n    servo = new Servo();\n    servo->attach(2);\n    servo->write(90);\n    currentPosition = 90;\n}\n\nvoid Pen::setRawValue(int rawValue) {\n    this->servo->write(rawValue);\n    currentPosition = rawValue;\n}\n\nvoid Pen::setPenDistance(int value) {\n    Serial.println(\"Pen distance angle set to \" + String(value));\n    this->penDistance = value;\n}\n\nvoid Pen::slowUp() {\n    if (penDistance == -1) {\n        throw std::invalid_argument(\"not ready\");\n    }\n\n    doSlowMove(this, currentPosition, 90, slowSpeedDegPerSec);\n    currentPosition = 90;\n}\n\nvoid Pen::slowDown() {\n    if (penDistance == -1) {\n        throw std::invalid_argument(\"not ready\");\n    }\n\n    doSlowMove(this, currentPosition, penDistance, slowSpeedDegPerSec);\n    currentPosition = penDistance;\n}\n\nbool Pen::isDown() {\n    return currentPosition == penDistance;\n}"
  },
  {
    "path": "src/pen.h",
    "content": "#ifndef Pen_h\n#define Pen_h\n#include <ESP32Servo.h>\nconst int RETRACT_DISTANCE = 20;\nclass Pen {\n    private:\n    Servo *servo;\n    int penDistance = -1;\n    int slowSpeedDegPerSec = 90;\n    int currentPosition = 90;\n    public:\n    Pen();\n    void setRawValue(int rawValue);\n    void setPenDistance(int value);\n    void slowUp();\n    void slowDown();\n    bool isDown();\n};\n#endif"
  },
  {
    "path": "src/phases/begindrawingphase.cpp",
    "content": "#include \"begindrawingphase.h\"\nBeginDrawingPhase::BeginDrawingPhase(PhaseManager* manager, Runner* runner, AsyncWebServer* server) {\n    this->manager = manager;\n    this->runner = runner;\n    this->server = server;\n}\n\nvoid BeginDrawingPhase::run(AsyncWebServerRequest *request) {\n    runner->start();\n    request->send(200, \"text/plain\", \"OK\"); \n    server->end();\n}\n\nvoid BeginDrawingPhase::doneWithPhase(AsyncWebServerRequest *request) {\n    manager->reset();\n    manager->respondWithState(request);\n}\n\nconst char* BeginDrawingPhase::getName() {\n    return \"BeginDrawing\";\n}"
  },
  {
    "path": "src/phases/begindrawingphase.h",
    "content": "#ifndef BeginDrawingPhase_h\n#define BeginDrawingPhase_h\n#include \"notsupportedphase.h\"\n#include \"phasemanager.h\"\nclass BeginDrawingPhase : public NotSupportedPhase {\n    private:\n    PhaseManager* manager;\n    Runner* runner;\n    AsyncWebServer* server;\n    public:\n    BeginDrawingPhase(PhaseManager* manager, Runner* runner, AsyncWebServer* server);\n    const char* getName();\n    void run(AsyncWebServerRequest *request);\n    void doneWithPhase(AsyncWebServerRequest *request);\n};\n#endif"
  },
  {
    "path": "src/phases/commandhandlingphase.cpp",
    "content": "#include \"commandhandlingphase.h\"\n\nCommandHandlingPhase::CommandHandlingPhase(Movement* movement) {\n    this->movement = movement;\n}\n\nvoid CommandHandlingPhase::handleCommand(AsyncWebServerRequest *request) {\n    auto command = request->arg(\"command\");\n    if (command == \"l-ret\")\n    {\n        movement->leftStepper(-1);\n    }\n    else if (command == \"l-ext\")\n    {\n        movement->leftStepper(1);\n    }\n    else if (command == \"l-0\")\n    {\n        movement->leftStepper(0);\n    }\n    else if (command == \"r-ret\")\n    {\n        movement->rightStepper(-1);\n    }\n    else if (command == \"r-ext\")\n    {\n        movement->rightStepper(1);\n    }\n    else if (command == \"r-0\")\n    {\n        movement->rightStepper(0);\n    }\n    else {\n        request->send(400, \"text/plain\", \"Unsupported command\");    \n        return;\n    }\n\n    request->send(200, \"text/plain\", \"OK\");\n}"
  },
  {
    "path": "src/phases/commandhandlingphase.h",
    "content": "#ifndef CommandHandlingPhase_h\n#define CommandHandlingPhase_h\n#include \"notsupportedphase.h\"\n#include \"phasemanager.h\"\nclass CommandHandlingPhase : public NotSupportedPhase {\n    private:\n    Movement* movement;\n    public:\n    CommandHandlingPhase(Movement* movement);\n    void handleCommand(AsyncWebServerRequest *request);\n};\n#endif"
  },
  {
    "path": "src/phases/extendtohomephase.cpp",
    "content": "#include \"extendtohomephase.h\"\nvoid ExtendToHomePhase::extendToHome(AsyncWebServerRequest *request) {\n    auto moveTime = movement->extendToHome() + 1; // extra second of waiting for good measure\n    request->send(200, \"text/plain\", String(moveTime));\n}\n\nExtendToHomePhase::ExtendToHomePhase(PhaseManager* manager, Movement* movement) {\n    this->manager = manager;\n    this->movement = movement;\n}\n\nconst char* ExtendToHomePhase::getName() {\n    return \"ExtendToHome\";\n}\n\nvoid ExtendToHomePhase::loopPhase() {\n    if (movement->hasStartedHoming() && !movement->isMoving()) {\n        manager->setPhase(PhaseManager::PenCalibration);\n    }\n}"
  },
  {
    "path": "src/phases/extendtohomephase.h",
    "content": "#ifndef ExtendToHomePhase_h\n#define ExtendToHomePhase_h\n#include \"notsupportedphase.h\"\n#include \"phasemanager.h\"\n#include \"movement.h\"\nclass ExtendToHomePhase : public NotSupportedPhase {\n    private:\n    PhaseManager* manager;\n    Movement* movement;\n    public:\n    ExtendToHomePhase(PhaseManager* manager, Movement* movement);\n    void extendToHome(AsyncWebServerRequest *request);\n    const char* getName();\n    void loopPhase();\n};\n#endif"
  },
  {
    "path": "src/phases/notsupportedphase.cpp",
    "content": "#include \"notsupportedphase.h\"\nvoid NotSupportedPhase::handleCommand(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::setTopDistance(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::extendToHome(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::setServo(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::setPenDistance(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::resumeTopDistance(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::run(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::doneWithPhase(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nvoid NotSupportedPhase::estepsCalibration(AsyncWebServerRequest *request) {\n    handleNotSupported(request);\n}\n\nconst char* NotSupportedPhase::getName() {\n    throw std::invalid_argument(\"should be overridden\");\n}\n\nvoid NotSupportedPhase::handleNotSupported(AsyncWebServerRequest *request) {\n    request->send(400, \"Request is not supported by the current server phase\");\n}\n\nvoid NotSupportedPhase::loopPhase() {\n    // don't throw here - most phases dont need to do anything on loop()\n}"
  },
  {
    "path": "src/phases/notsupportedphase.h",
    "content": "#ifndef NotSupportedPhase_h\n#define NotSupportedPhase_h\n#include \"phase.h\"\nclass NotSupportedPhase : public Phase {\n    private:\n    void handleNotSupported(AsyncWebServerRequest *request);\n    public:\n    void handleCommand(AsyncWebServerRequest *request);\n    void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);\n    void setTopDistance(AsyncWebServerRequest *request);\n    void extendToHome(AsyncWebServerRequest *request);\n    void setServo(AsyncWebServerRequest *request);\n    void setPenDistance(AsyncWebServerRequest *request);\n    void resumeTopDistance(AsyncWebServerRequest *request);\n    void run(AsyncWebServerRequest *request);\n    void doneWithPhase(AsyncWebServerRequest *request);\n    void estepsCalibration(AsyncWebServerRequest *request);\n    const char* getName();\n    void loopPhase();\n};\n#endif"
  },
  {
    "path": "src/phases/pencalibrationphase.cpp",
    "content": "#include \"pencalibrationphase.h\"\nPenCalibrationPhase::PenCalibrationPhase(PhaseManager* manager, Pen* pen) {\n    this->manager = manager;\n    this->pen = pen;\n    this->runner = runner;\n}\n\nvoid PenCalibrationPhase::setServo(AsyncWebServerRequest *request) {\n    const AsyncWebParameter* p = request->getParam(0);\n    int angle = p->value().toInt();\n    pen->setRawValue(angle);\n    request->send(200, \"text/plain\", \"OK\"); \n}\n\nvoid PenCalibrationPhase::setPenDistance(AsyncWebServerRequest *request) {\n    const AsyncWebParameter* p = request->getParam(0);\n    int angle = p->value().toInt();\n    pen->setPenDistance(angle);\n    pen->slowUp();\n    manager->setPhase(PhaseManager::BeginDrawing);\n    manager->respondWithState(request);\n}\n\nconst char* PenCalibrationPhase::getName() {\n    return \"PenCalibration\";\n}"
  },
  {
    "path": "src/phases/pencalibrationphase.h",
    "content": "#ifndef PenCalibrationPhase_h\n#define PenCalibrationPhase_h\n#include \"notsupportedphase.h\"\n#include \"phasemanager.h\"\n#include \"pen.h\"\nclass PenCalibrationPhase : public NotSupportedPhase {\n    private:\n    PhaseManager* manager;\n    Pen* pen;\n    Runner* runner;\n    public:\n    PenCalibrationPhase(PhaseManager* manager, Pen* pen);\n    void setServo(AsyncWebServerRequest *request);\n    void setPenDistance(AsyncWebServerRequest *request);\n    const char* getName();\n    \n};\n#endif"
  },
  {
    "path": "src/phases/phase.h",
    "content": "#ifndef Phase_h\n#define Phase_h\n#include <ESPAsyncWebServer.h>\nclass Phase {\n    public:\n    virtual void handleCommand(AsyncWebServerRequest *request) = 0;\n    virtual void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) = 0;\n    virtual void setTopDistance(AsyncWebServerRequest *request) = 0;\n    virtual void extendToHome(AsyncWebServerRequest *request) = 0;\n    virtual void setServo(AsyncWebServerRequest *request) = 0;\n    virtual void setPenDistance(AsyncWebServerRequest *request) = 0;\n    virtual void resumeTopDistance(AsyncWebServerRequest *request) = 0;\n    virtual void run(AsyncWebServerRequest *request) = 0;\n    virtual void doneWithPhase(AsyncWebServerRequest *request) = 0;\n    virtual void estepsCalibration(AsyncWebServerRequest *request) = 0;\n    virtual const char* getName() = 0;\n    virtual void loopPhase() = 0;\n};\n#endif"
  },
  {
    "path": "src/phases/phasemanager.cpp",
    "content": "#include \"phasemanager.h\"\n#include \"retractbeltsphase.h\"\n#include \"settopdistancephase.h\"\n#include \"extendtohomephase.h\"\n#include \"pencalibrationphase.h\"\n#include \"svgselectphase.h\"\n#include \"begindrawingphase.h\"\n#include \"AsyncJson.h\"\n#include \"ArduinoJson.h\"\n#include <stdexcept>\n\nPhaseManager::PhaseManager(Movement* movement, Pen* pen, Runner* runner, AsyncWebServer* server) {\n    retractBeltsPhase = new RetractBeltsPhase(this, movement);\n    setTopDistancePhase = new SetTopDistancePhase(this, movement, pen);\n    extendToHomePhase = new ExtendToHomePhase(this, movement);\n    penCalibrationPhase = new PenCalibrationPhase(this, pen);\n    svgSelectPhase = new SvgSelectPhase(this);\n    beginDrawingPhase = new BeginDrawingPhase(this, runner, server);\n\n    this->movement = movement;\n    reset();\n}\n\nPhase* PhaseManager::getCurrentPhase() {\n    return currentPhase;\n}\n\nvoid PhaseManager::setPhase(PhaseNames name) {\n    Serial.print(\"Switching current phase to \");\n    switch (name) {\n        case PhaseNames::RetractBelts:\n            Serial.println(\"RetractBelts\");\n            currentPhase = retractBeltsPhase;\n            break;\n        case PhaseNames::SetTopDistance:\n            Serial.println(\"SetTopDistance\");\n            currentPhase = setTopDistancePhase;\n            break;\n        case PhaseNames::ExtendToHome:\n            Serial.println(\"ExtendToHome\");\n            currentPhase = extendToHomePhase;\n            break;\n        case PhaseNames::PenCalibration:\n            Serial.println(\"PenCalibration\");\n            currentPhase = penCalibrationPhase;\n            break;\n        case PhaseNames::SvgSelect:\n            Serial.println(\"SvgSelect\");\n            currentPhase = svgSelectPhase;\n            break;\n        case PhaseNames::BeginDrawing:\n            Serial.println(\"BeginDrawing\");\n            currentPhase = beginDrawingPhase;\n            break;\n        default:\n            throw std::invalid_argument(\"Invalid Phase\");\n    }\n}\n\nvoid PhaseManager::respondWithState(AsyncWebServerRequest *request) {\n    auto currentPhase = getCurrentPhase()->getName();\n    auto moving = movement->isMoving();\n    auto startedHoming = movement->hasStartedHoming();\n    auto homePosition = movement->getHomeCoordinates();\n\n    auto topDistance = movement->getTopDistance();\n    auto safeWidth = topDistance != -1 ? movement->getWidth() : -1;\n\n    AsyncResponseStream *response = request->beginResponseStream(\"application/json\");\n    DynamicJsonBuffer jsonBuffer;\n    JsonObject &root = jsonBuffer.createObject();\n\n    root[\"phase\"] = currentPhase;\n    root[\"moving\"] = moving;\n    root[\"topDistance\"] = topDistance;\n    root[\"safeWidth\"] = safeWidth;\n    root[\"homeX\"] = homePosition.x;\n    root[\"homeY\"] = homePosition.y;\n\n    root.printTo(*response);\n    request->send(response);\n}\n\nvoid PhaseManager::reset() {\n    setPhase(PhaseManager::SetTopDistance);\n}"
  },
  {
    "path": "src/phases/phasemanager.h",
    "content": "#ifndef PhaseManager_H\n#define PhaseManager_H\n#include \"phase.h\"\n#include \"movement.h\"\n#include \"pen.h\"\n#include \"runner.h\"\n#include <ESPAsyncWebServer.h>\nclass PhaseManager {\n    private:\n    Phase* currentPhase;\n    Phase* retractBeltsPhase;\n    Phase* setTopDistancePhase;\n    Phase* extendToHomePhase;\n    Phase* penCalibrationPhase;\n    Phase* svgSelectPhase;\n    Phase* beginDrawingPhase;\n    Movement* movement;\n    public:\n    enum PhaseNames {RetractBelts, SetTopDistance, ExtendToHome, PenCalibration, SvgSelect, BeginDrawing};\n    PhaseManager(Movement* movement, Pen* pen, Runner* runner, AsyncWebServer* server);\n    Phase* getCurrentPhase();\n    void setPhase(PhaseNames name);\n    void respondWithState(AsyncWebServerRequest *request);\n    void reset();\n};\n#endif"
  },
  {
    "path": "src/phases/retractbeltsphase.cpp",
    "content": "#include \"retractbeltsphase.h\"\n#include \"commandhandlingphase.h\"\nRetractBeltsPhase::RetractBeltsPhase(PhaseManager* manager, Movement* movement) : CommandHandlingPhase(movement) {\n    this->manager = manager;\n    this->movement = movement;\n}\n\nvoid RetractBeltsPhase::doneWithPhase(AsyncWebServerRequest *request) {\n    manager->setPhase(PhaseManager::ExtendToHome);\n    manager->respondWithState(request);\n}\n\nconst char* RetractBeltsPhase::getName() {\n    return \"RetractBelts\";\n}"
  },
  {
    "path": "src/phases/retractbeltsphase.h",
    "content": "#ifndef RetractBelts_h\n#define RetractBelts_h\n#include \"commandhandlingphase.h\"\n#include \"phasemanager.h\"\n#include \"movement.h\"\n#include \"pen.h\"\nclass RetractBeltsPhase : public CommandHandlingPhase {\n    private:\n    PhaseManager* manager;\n    Movement* movement;\n    public:\n    RetractBeltsPhase(PhaseManager* manager, Movement* movement);\n    void doneWithPhase(AsyncWebServerRequest *request);\n    const char* getName();\n};\n#endif"
  },
  {
    "path": "src/phases/settopdistancephase.cpp",
    "content": "#include \"settopdistancephase.h\"\n#include \"commandhandlingphase.h\"\nSetTopDistancePhase::SetTopDistancePhase(PhaseManager* manager, Movement* movement, Pen* pen) : CommandHandlingPhase(movement) {\n    this->manager = manager;\n    this->movement = movement;\n    this->pen = pen;\n}\n\nvoid SetTopDistancePhase::setTopDistance(AsyncWebServerRequest *request) {\n    const AsyncWebParameter* p = request->getParam(0);\n    int distance = p->value().toInt();\n    Serial.println(\"Setting distance\");\n    movement->setTopDistance(distance); \n    manager->setPhase(PhaseManager::SvgSelect);\n    manager->respondWithState(request);\n}\n\nvoid SetTopDistancePhase::setServo(AsyncWebServerRequest *request) {\n    const AsyncWebParameter* p = request->getParam(0);\n    int angle = p->value().toInt();\n    pen->setRawValue(angle);\n    request->send(200, \"text/plain\", \"OK\"); \n}\n\nvoid SetTopDistancePhase::estepsCalibration(AsyncWebServerRequest* request) {\n    Serial.println(\"Extending 1000mm\");\n    movement->extend1000mm();\n    request->send(200, \"text/plain\", \"OK\");\n}\n\nconst char* SetTopDistancePhase::getName() {\n    return \"SetTopDistance\";\n}"
  },
  {
    "path": "src/phases/settopdistancephase.h",
    "content": "#ifndef SetDistancePhase_h\n#define SetDistancePhase_h\n#include \"commandhandlingphase.h\"\n#include \"phasemanager.h\"\n#include \"movement.h\"\nclass SetTopDistancePhase : public CommandHandlingPhase {\n    private:\n    PhaseManager* manager;\n    Movement* movement;\n    Pen* pen;\n    public:\n    SetTopDistancePhase(PhaseManager* manager, Movement* movement, Pen* pen);\n    void setTopDistance(AsyncWebServerRequest *request);\n    void setServo(AsyncWebServerRequest *request);\n    void estepsCalibration(AsyncWebServerRequest *request);\n    const char* getName();\n};\n#endif"
  },
  {
    "path": "src/phases/svgselectphase.cpp",
    "content": "#include \"svgselectphase.h\"\n#include \"LittleFS.h\"\n\nSvgSelectPhase::SvgSelectPhase(PhaseManager* manager) {\n    this->manager = manager;\n}\n\nvoid SvgSelectPhase::handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)\n{\n    if (!index)\n    {\n        if (LittleFS.exists(\"/commands\")) {\n            LittleFS.remove(\"/commands\");\n        }\n\n        Serial.printf(\"%d bytes total, %d bytes free\\n\",  LittleFS.totalBytes(), LittleFS.totalBytes() - LittleFS.usedBytes());\n        Serial.printf(\"Upload size: %d bytes\\n\", request->contentLength());\n\n        if (LittleFS.totalBytes() -  LittleFS.usedBytes() < request->contentLength()) {\n            Serial.println(\"Not enough space on LittleFS\");\n            request->send(400, \"text/plain\", \"Not enough space for upload\");\n            return;\n        }\n            \n        request->_tempFile = LittleFS.open(\"/commands\", \"w\");\n        Serial.println(\"Upload started\");\n    }\n\n    if (len)\n    {\n        // stream the incoming chunk to the opened file\n        request->_tempFile.write(data, len);\n    }\n\n    if (final)\n    {\n        request->_tempFile.close();\n        Serial.println(\"Upload finished\");\n        manager->setPhase(PhaseManager::RetractBelts);\n    }\n}\n\nconst char* SvgSelectPhase::getName() {\n    return \"SvgSelect\";\n}"
  },
  {
    "path": "src/phases/svgselectphase.h",
    "content": "#ifndef SvgSelectPhase_h\n#define SvgSelectPhase_h\n#include \"notsupportedphase.h\"\n#include \"phasemanager.h\"\nclass SvgSelectPhase : public NotSupportedPhase {\n    private:\n    PhaseManager* manager;\n    public:\n    SvgSelectPhase(PhaseManager* manager);\n    void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);\n    const char* getName();\n};\n#endif"
  },
  {
    "path": "src/runner.cpp",
    "content": "#include \"runner.h\"\n#include \"tasks/movementtask.h\"\n#include \"tasks/interpolatingmovementtask.h\"\n#include \"tasks/pentask.h\"\n#include \"pen.h\"\n#include \"display.h\"\n#include \"LittleFS.h\"\nusing namespace std;\n\nRunner::Runner(Movement *movement, Pen *pen, Display *display) {\n    stopped = true;\n    this->movement = movement;\n    this->pen = pen;\n    this->display = display;\n}\n\nvoid Runner::initTaskProvider() {\n    openedFile = LittleFS.open(\"/commands\");\n    if (!openedFile || !openedFile.available()) {\n        Serial.println(\"Failed to open file\");\n        throw std::invalid_argument(\"No File\");\n    }\n\n    auto line = openedFile.readStringUntil('\\n');\n    if (line.charAt(0) == 'd') {\n        totalDistance = line.substring(1, line.length() - 1).toDouble();\n    } else {\n        Serial.println(\"Bad file - no distance\");\n        throw std::invalid_argument(\"bad file\");\n    }\n\n    auto heightLine = openedFile.readStringUntil('\\n');\n    if (heightLine.charAt(0) == 'h') {\n        auto height = heightLine.substring(1, heightLine.length() - 1).toDouble();\n        // we actually dont need it, just validating\n    } else {\n        Serial.println(\"Bad file - no height\");\n        throw std::invalid_argument(\"bad file\");\n    }\n\n    Serial.println(\"Total distance to travel: \" + String(totalDistance));\n\n    distanceSoFar = 0;\n    progress = -1; // so 0% appears right away\n    startPosition = movement->getCoordinates();\n\n    auto homeCoordinates = movement->getHomeCoordinates();\n    finishingSequence[0] = new InterpolatingMovementTask(movement, homeCoordinates);\n}\n\nvoid Runner::start() {\n    initTaskProvider();\n    currentTask = getNextTask();\n    currentTask->startRunning();\n    stopped = false;\n}\n\nTask *Runner::getNextTask()\n{\n    if (openedFile.available())\n    {\n        auto line = openedFile.readStringUntil('\\n');\n        if (line.charAt(0) == 'p')\n        {\n            if (line.charAt(1) == '1')\n            {\n                //Serial.println(\"Pen down\");\n                return new PenTask(false, pen);\n            }\n            else\n            {\n                //Serial.println(\"Pen up\");\n                return new PenTask(true, pen);\n            }\n        }\n        else\n        {\n            auto x = line.substring(0, line.indexOf(\" \")).toDouble();\n            auto y = line.substring(line.indexOf(\" \") + 1).toDouble();\n            targetPosition = Movement::Point(x, y);\n            return new InterpolatingMovementTask(movement, targetPosition);\n        }\n    }\n    else\n    {\n        if (sequenceIx < (end(finishingSequence) - begin(finishingSequence))) {\n            auto currentIx = sequenceIx;\n            sequenceIx = sequenceIx + 1;\n            return finishingSequence[currentIx];\n        } else {\n            // DistanceState::storeDistance(movement->getTopDistance());\n            delay(200);\n            ESP.restart();\n            // unreachable\n            return NULL;\n        }\n    }\n}\n\nvoid Runner::run()\n{\n    if (stopped)\n    {\n        return;\n    }\n\n    if (currentTask->isDone())\n    {\n        if (currentTask->name() == InterpolatingMovementTask::NAME) {\n            auto distanceCovered = Movement::distanceBetweenPoints(startPosition, targetPosition);\n            distanceSoFar += distanceCovered;\n            startPosition = targetPosition;\n            auto newProgress = int(floor(distanceSoFar / totalDistance * 100));\n            if (newProgress > 100) {\n                newProgress = 100;\n            }\n            if (progress != newProgress) {\n                Serial.println(\"Progress: \" + String(newProgress));\n                progress = newProgress;\n                display->displayText(String(progress) + \"%\");\n            }\n\n        }\n        delete currentTask;\n        currentTask = getNextTask();\n        if (currentTask != NULL)\n        {\n            currentTask->startRunning();\n        }\n        else\n        {\n            stopped = true;\n        }\n    }\n}\n\nvoid Runner::dryRun() {\n    initTaskProvider();\n    auto task = getNextTask();\n    auto index = 1;\n    while (task != NULL) {\n        //Serial.println(String(index));\n        index = index + 1;\n        delete task;\n        task = getNextTask();\n    }\n    Serial.println(\"All done\");\n}"
  },
  {
    "path": "src/runner.h",
    "content": "#ifndef Runner_h\n#define Runner_h\n#include \"movement.h\"\n#include \"tasks/task.h\"\n#include \"pen.h\"\n#include \"display.h\"\n#include \"LittleFS.h\"\nclass Runner {\n    private:\n    Movement *movement;\n    Pen *pen;\n    Display *display;\n    void initTaskProvider();\n    Task* getNextTask();\n    Task* currentTask;\n    bool stopped;\n    File openedFile;\n    double totalDistance;\n    double distanceSoFar;\n    Movement::Point startPosition;\n    Movement::Point targetPosition;\n    int progress;\n    Task *finishingSequence[1];\n    int sequenceIx = 0;\n    public:\n    Runner(Movement *movement, Pen *pen, Display *display);\n    void start();\n    void run();\n    void dryRun();\n};\n#endif"
  },
  {
    "path": "src/tasks/interpolatingmovementtask.cpp",
    "content": "#include \"movement.h\"\n#include \"interpolatingmovementtask.h\"\nconst char* InterpolatingMovementTask::NAME = \"InterpolatingMovementTask\";\n\nMovement::Point getNextIncrement(Movement::Point currentPosition, Movement::Point target) {\n    auto distanceBetween = Movement::distanceBetweenPoints(currentPosition, target);\n    if (distanceBetween <= INCREMENT) {\n        return target;\n    }\n\n    auto nextX = currentPosition.x + (INCREMENT / distanceBetween) * (target.x - currentPosition.x);\n    auto nextY = currentPosition.y + (INCREMENT / distanceBetween) * (target.y - currentPosition.y);\n\n    return Movement::Point(nextX, nextY);\n}\n\nbool arePointsEqual(Movement::Point point1, Movement::Point point2) {\n    return point1.x == point2.x && point1.y == point2.y;\n}\n\nInterpolatingMovementTask::InterpolatingMovementTask(Movement *movement, Movement::Point target) {\n    this->target = target;\n    this->movement = movement;\n}\n\nvoid InterpolatingMovementTask::startRunning() {\n    Serial.printf(\"Starting the move to %.1f, %.1f\\n\", target.x, target.y);\n    auto currentCoordinates = movement->getCoordinates();\n    auto incrementPoint = getNextIncrement(currentCoordinates, target);\n    movement->beginLinearTravel(incrementPoint.x, incrementPoint.y, printSpeedSteps);\n}\n\nbool InterpolatingMovementTask::isDone() {\n    if (movement->isMoving()) {\n        return false;\n    }\n\n    auto currentPosition = movement->getCoordinates();\n    if (arePointsEqual(currentPosition, target)) {\n        return true;\n    }\n\n    auto incrementPoint = getNextIncrement(movement->getCoordinates(), target);\n    movement->beginLinearTravel(incrementPoint.x, incrementPoint.y, printSpeedSteps);\n    \n    return false;\n}\n\n"
  },
  {
    "path": "src/tasks/interpolatingmovementtask.h",
    "content": "#ifndef InterpolatingMovementTask_h\n#define InterpolatingMovementTask_h\n#include \"movement.h\"\n#include \"task.h\"\nconst double INCREMENT = 1;\nclass InterpolatingMovementTask : public Task {\n    private:\n    Movement *movement;\n    Movement::Point target;\n    Movement::Point position;\n    public:\n    const static char* NAME;\n    InterpolatingMovementTask(Movement *movement, Movement::Point target);\n    bool isDone();\n    void startRunning();\n    const char* name() {\n        return NAME;\n    }\n};\n#endif"
  },
  {
    "path": "src/tasks/movementtask.cpp",
    "content": "#include \"movementtask.h\"\nMovementTask::MovementTask(int x, int y, Movement *movement) {\n    this->x = x;\n    this->y = y;\n    this->movement = movement;\n}\n\nvoid MovementTask::startRunning() {\n    movement->beginLinearTravel(x, y, printSpeedSteps);\n}\n\nbool MovementTask::isDone() {\n    return !(movement->isMoving());\n}"
  },
  {
    "path": "src/tasks/movementtask.h",
    "content": "#ifndef MovementTask_h\n#define MovementTask_h\n#include \"movement.h\"\n#include \"task.h\"\nclass MovementTask : public Task {\n    private:\n    const char* NAME = \"MovementTask\";\n    Movement *movement;\n    int x;\n    int y;\n    public:\n    MovementTask(int x, int y, Movement *movement);\n    bool isDone();\n    void startRunning();\n    const char* name() {\n        return NAME;\n    }\n};\n#endif"
  },
  {
    "path": "src/tasks/pentask.cpp",
    "content": "#include \"pentask.h\"\nPenTask::PenTask(bool up, Pen *pen) {\n    this->up = up;\n    this->pen = pen;\n}\n\nvoid PenTask::startRunning() {\n    Serial.println(\"Starting pen task \" + String(up));\n    if (up) {\n        Serial.println(\"Pen is going up\");\n        pen->slowUp();\n    } else {\n        Serial.println(\"Pen is going down\");\n        pen->slowDown();\n    }\n    Serial.println(\"Pen task ran\");\n}\n\nbool PenTask::isDone() {\n    Serial.println(\"Pen task is done\");\n    return true;\n}"
  },
  {
    "path": "src/tasks/pentask.h",
    "content": "#ifndef PenTask_h\n#define PenTask_h\n#include \"pen.h\"\n#include \"task.h\"\nclass PenTask : public Task {\n    private:\n    const char* NAME = \"PenTask\";\n    Pen *pen;\n    bool up;\n    public:\n    PenTask(bool up, Pen *pen);\n    bool isDone();\n    void startRunning();\n    const char* name() {\n        return NAME;\n    }\n};\n#endif"
  },
  {
    "path": "src/tasks/task.h",
    "content": "#ifndef Task_h\n#define Task_h\nclass Task {\n    public:\n    virtual void startRunning() = 0;\n    virtual bool isDone() = 0;\n    virtual const char* name() = 0;\n};\n#endif"
  },
  {
    "path": "test/README",
    "content": "\nThis directory is intended for PlatformIO Unit Testing and project tests.\n\nUnit Testing is a software testing method by which individual units of\nsource code, sets of one or more MCU program modules together with associated\ncontrol data, usage procedures, and operating procedures, are tested to\ndetermine whether they are fit for use. Unit testing finds problems early\nin the development cycle.\n\nMore information about PlatformIO Unit Testing:\n- https://docs.platformio.org/page/plus/unit-testing.html\n"
  },
  {
    "path": "tsc/package.json",
    "content": "{\n  \"name\": \"mural\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"build\": \"webpack --mode=production --node-env=production\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"Nik Ivanov\",\n  \"license\": \"GPL\",\n  \"devDependencies\": {\n    \"@types/jsdom\": \"^21.1.7\",\n    \"@types/node\": \"^20.10.1\",\n    \"@types/webpack\": \"^5.28.5\",\n    \"@webpack-cli/generators\": \"^3.0.7\",\n    \"clean-webpack-plugin\": \"^4.0.0\",\n    \"copy-webpack-plugin\": \"^11.0.0\",\n    \"declaration-bundler-webpack-plugin\": \"^1.0.3\",\n    \"ts-loader\": \"^9.5.1\",\n    \"typescript\": \"5.0.4\",\n    \"webpack\": \"^5.89.0\",\n    \"webpack-cli\": \"^5.1.4\"\n  },\n  \"dependencies\": {\n    \"canvas\": \"^2.11.2\",\n    \"jsdom\": \"^24.1.1\",\n    \"paper\": \"0.12.17\",\n    \"paper-jsdom\": \"0.12.17\"\n  }\n}\n"
  },
  {
    "path": "tsc/src/deduplicator.ts",
    "content": "import { Command } from \"./types\";\nimport { getLastPoint } from \"./utils\";\n\nexport function dedupeCommands(commands: Command[]): Command[] {\n    const dedupedCommands: Command[] = [];\n    for (const command of commands) {\n        if (typeof command === 'string') {\n            if (dedupedCommands.length === 0 || dedupedCommands[dedupedCommands.length - 1] !== command) {\n                dedupedCommands.push(command);\n            }\n        } else {\n            const lastCommand = getLastPoint(dedupedCommands);\n            if (lastCommand) {\n                if (command.x !== lastCommand.x || command.y !== lastCommand.y) {\n                    dedupedCommands.push(command);\n                } \n            } else {\n                dedupedCommands.push(command);\n            }\n        }\n    }\n\n    const filteredCommands: Command[] = [];\n    for (let i = 0; i < dedupedCommands.length; i++) {\n        if (i == 0) {\n            filteredCommands.push(dedupedCommands[0]);\n        } else {\n            const currentCommand = dedupedCommands[i];\n            const previousCommand = filteredCommands[filteredCommands.length - 1];\n            if (previousCommand === 'p0' && currentCommand === 'p1') {\n                //verify that we were p1 before that last p0\n                for (let i = filteredCommands.length - 2; i >= 0; i--) {\n                    const command = filteredCommands[i];\n                    if (typeof command === 'string' && command.charAt(0) === 'p') {\n                        if (command.charAt(1) === '1') {\n                            // all is well\n                            break;\n                        } else {\n                            throw new Error('Inconsistent pen movement');\n                        }\n                    }\n                }\n                filteredCommands.pop();\n            } else {\n                filteredCommands.push(currentCommand);\n            }\n        }\n    }\n\n\n    return filteredCommands;\n}"
  },
  {
    "path": "tsc/src/flattener.ts",
    "content": "import {loadPaper} from './paperLoader';\nimport { updateStatusFn } from './types';\n\nconst paper = loadPaper();\n\nexport function flattenPaths(paths: paper.PathItem[], updateStatusFn: updateStatusFn) {\n    updateStatusFn(\"Sorting paths\");\n    paths.sort((a, b) => a.isAbove(b) ? -1 : 1);\n\n    const count = paths.length;\n    for (let currentPathIx = 0; currentPathIx < paths.length - 1; currentPathIx++) {\n        updateStatusFn(`Flattening paths: ${currentPathIx + 1} / ${count}`)\n        const currentPath = paths[currentPathIx];\n        for (let modifiedPathIx = currentPathIx + 1; modifiedPathIx < paths.length; modifiedPathIx++) {\n            const pathToModify = paths[modifiedPathIx];\n            const modifiedPath = pathToModify.subtract(currentPath, {\n                insert: false,\n            });\n            paths[modifiedPathIx] = modifiedPath;\n        }\n    }\n}"
  },
  {
    "path": "tsc/src/generator.ts",
    "content": "import { loadPaper } from './paperLoader';\n\nconst paper = loadPaper();\n\nexport function generatePaths(svg: paper.Item): paper.PathItem[] {\n    return generatePathsRecursive(svg);\n}\n\nfunction generatePathsRecursive(item: paper.Item): paper.PathItem[] {\n    const paths: paper.PathItem[] = [];\n    for (const child of item.children) {\n        if (child instanceof paper.Group) {\n            const innerPaths = generatePathsRecursive(child);\n            paths.push(...innerPaths);\n        } else if (child instanceof paper.Path || child instanceof paper.CompoundPath) {\n            \n            paths.push(child);\n        }\n    }\n\n    return paths;\n}\n\n"
  },
  {
    "path": "tsc/src/infill.ts",
    "content": "import { loadPaper } from './paperLoader';\nimport { InfillDensity, InfilledPath } from './types';\n\nconst paper = loadPaper();\n\nconst infillDensityToSpacingMap = new Map<Exclude<InfillDensity, 0>, number>([\n    [1, 20],\n    [2, 15],\n    [3, 10],\n    [4, 7],\n]);\n\nconst infillAngle = Math.PI / 4;\n\nexport function generateInfills(pathsToInfill: paper.PathItem[], infillDensity: InfillDensity): InfilledPath[] {\n    const view = paper.project.view;\n    const xOffset = view.size.height * Math.tan(infillAngle);\n    const lines: paper.Path.Line[] = [];\n\n    let minInfillLength = 1000;\n    if (infillDensity != 0) {\n        const infillSpacing = infillDensityToSpacingMap.get(infillDensity)!;\n        minInfillLength = Math.floor(infillSpacing);\n        const infillXSpacing = infillSpacing * Math.sqrt(2);\n        for (let currentX = -xOffset; currentX < view.size.width; currentX = currentX + infillXSpacing) {\n            lines.push(new paper.Path.Line({x: currentX, y: 0}, {x: currentX + xOffset, y: view.size.height}));\n            lines.push(new paper.Path.Line({x: currentX, y: view.size.height}, {x: currentX + xOffset, y: 0}));\n        }\n    }\n\n    const boundsPath = new paper.Path.Rectangle(view.bounds);\n    \n    const infilledPaths = pathsToInfill.map(path => {\n        if (path.fillColor && path.fillColor.toCSS(true) === '#ffffff' && !path.strokeColor) {\n            return null;\n        }\n\n        const outlinePaths: paper.Path[] = [];\n        \n        if (path instanceof paper.Path) {\n            if (path.firstSegment && path.lastSegment) {\n                outlinePaths.push(path);\n            }\n            \n        } else if (path instanceof paper.CompoundPath) {\n            const unwoundPaths = unwrapCompoundPath(path).filter(p => p.firstSegment && p.lastSegment);\n            outlinePaths.push(...unwoundPaths);\n        } else {\n            throw new Error(\"Path item is neither a Path or CompoundPath\");\n        }\n\n        const infillPaths: paper.Path[] = [];\n\n        if (!path.fillColor || path.fillColor.toCSS(true) !== '#ffffff') {\n            for (const line of lines) {\n                const intersections = [...path.getIntersections(line), ...boundsPath.getIntersections(line)].filter(i => i.point.isInside(boundsPath.bounds));\n\n                intersections.sort((a, b) => a.point.x - b.point.x);\n\n                let currentLineGroup: paper.Point[] = [];\n                function saveCurrentLineAsPath() {\n                    if (currentLineGroup.length > 1) {\n                        const infillLine = new paper.Path.Line(currentLineGroup[0], currentLineGroup[currentLineGroup.length - 1]);\n                        if (infillLine.length > minInfillLength) {\n                            infillPaths.push(infillLine);\n                        }\n                    }\n                }\n\n                for (const intersection of intersections) {\n                    if (currentLineGroup.length === 0) {\n                        currentLineGroup.push(intersection.point);\n                    } else {\n                        const previousPoint = currentLineGroup[currentLineGroup.length - 1];\n                        const thisPoint = intersection.point;\n                        const midPoint = getMidPoint(previousPoint, thisPoint);\n                        if (path.contains(midPoint)) {\n                            currentLineGroup.push(thisPoint);\n                        } else {\n                            saveCurrentLineAsPath();\n                            currentLineGroup = [thisPoint];\n                        }\n                    }\n                }\n                saveCurrentLineAsPath();\n            }\n        }\n\n        const infilledPath: InfilledPath = {\n            originalPath: path,\n            infillPaths,\n            outlinePaths,\n        };\n\n        return infilledPath;\n    }).filter((ip) => !!ip) as InfilledPath[];\n\n    return infilledPaths;\n}\n\nfunction getMidPoint(point1: paper.Point, point2: paper.Point): paper.Point {\n    return new paper.Point(\n        point1.x + (point2.x - point1.x) / 2,\n        point1.y + (point2.y - point1.y) / 2,\n    );\n}\n\nfunction unwrapCompoundPath(path: paper.CompoundPath) {\n    const paths: paper.Path[] = [];\n    for (const child of path.children) {\n        if (child instanceof paper.Path) {\n            paths.push(child);\n        } else if (child instanceof paper.CompoundPath) {\n            paths.push(...unwrapCompoundPath(child));\n        }\n    }\n\n    return paths;\n}\n"
  },
  {
    "path": "tsc/src/main.ts",
    "content": "import { renderCommandsToSvgJson } from \"./toSvgJson\";\nimport { renderSvgJsonToCommands } from \"./toCommands\";\nimport { vectorizeImageData } from './vectorizer';\nimport { InfillDensities, RequestTypes } from \"./types\";\n\nconst updateStatusFn = (status: string) => {\n    self.postMessage({\n        type: \"status\",\n        payload: status,\n    });\n};\n\nself.onmessage = async (e: MessageEvent<any>) => {\n    if (isVectorizeRequest(e.data)) {\n        vectorize(e.data);\n    } else if (isRenderSvgRequest(e.data)) {\n        await render(e.data);\n    } else {\n        throw new Error(\"Bad request\");\n    }\n};\n\nfunction vectorize(request: RequestTypes.VectorizeRequest) {\n    updateStatusFn(\"Vectorizing\");\n    const svgString = vectorizeImageData(request.raster, request.turdSize);\n    self.postMessage({\n        type: \"vectorizer\",\n        payload: {\n            svg: svgString,\n        }\n    });\n}\n\nasync function render(request: RequestTypes.RenderSVGRequest) {\n    const renderResult = await renderSvgJsonToCommands(\n        request,\n        updateStatusFn,\n    )\n    const resultSvgJson = renderCommandsToSvgJson(renderResult.commands, request.width, request.height, updateStatusFn);\n    self.postMessage({\n        type: \"renderer\",\n        payload: {\n            commands: renderResult.commands,\n            svgJson: resultSvgJson,\n            distance: renderResult.distance,\n            drawDistance: renderResult.drawDistance,\n        }\n    });\n}\n\nfunction isVectorizeRequest(obj: any): obj is RequestTypes.VectorizeRequest {\n    if (!('type' in obj) || obj.type !== 'vectorize') {\n        return false;\n    }\n\n    if (!('raster' in obj) || typeof obj.raster !== 'object') {\n        return false;\n    }\n\n    if (!('turdSize' in obj) || typeof obj.turdSize !== 'number') {\n        return false;\n    }\n\n    return true;\n}\n\n\nfunction isRenderSvgRequest(obj: any): obj is RequestTypes.RenderSVGRequest {\n    if (!('type' in obj) || obj.type !== 'renderSvg') {\n        return false;\n    }\n\n    if (!('svgJson' in obj) || typeof obj.svgJson !== 'string') {\n        return false;\n    }\n\n    if (!('width' in obj) || typeof obj.width !== 'number') {\n        return false;\n    }\n\n    if (!('height' in obj) || typeof obj.height !== 'number') {\n        return false;\n    }\n\n    if (!('svgWidth' in obj) || typeof obj.svgWidth !== 'number') {\n        return false;\n    }\n\n    if (!('svgHeight' in obj) || typeof obj.svgHeight !== 'number') {\n        return false;\n    }\n\n    if (!('homeX' in obj) || typeof obj.homeX !== 'number') {\n        return false;\n    }\n\n    if (!('homeY' in obj) || typeof obj.homeY !== 'number') {\n        return false;\n    }\n\n    if (!('infillDensity' in obj) || typeof obj.infillDensity !== 'number' || !InfillDensities.includes(obj.infillDensity)) {\n        return false;\n    }\n\n    if (!('flattenPaths' in obj) || typeof obj.flattenPaths !== 'boolean') {\n        return false;\n    }\n\n    return true;\n}\n\n"
  },
  {
    "path": "tsc/src/measurer.ts",
    "content": "import { Command } from \"./types\";\nimport { distanceBetweenPoints, getLastPoint } from \"./utils\";\n\nexport function measureDistance(dedupedCommands: Command[]) {\n    let totalDistance = 0;\n    let drawDistance = 0;\n    let penUp = true;\n\n    for (let i = 1; i < dedupedCommands.length; i++) {\n        const command = dedupedCommands[i];\n\n        if (typeof command !== 'string') {\n            const lastCommand = getLastPoint(dedupedCommands.slice(0, i));\n            if (lastCommand) {\n                if (command.x !== lastCommand.x || command.y !== lastCommand.y) {\n                    const distance = distanceBetweenPoints(lastCommand, command);\n                    totalDistance += distance;\n\n                    if (!penUp) {\n                        drawDistance += distance;\n                    }\n                }\n            }\n        } else {\n            if (command === 'p0') {\n                penUp = true;\n            } else if (command === 'p1') {\n                penUp = false;\n            }\n        }\n    }\n\n    return {\n        totalDistance,\n        drawDistance,\n    };\n}"
  },
  {
    "path": "tsc/src/optimizer.ts",
    "content": "import { loadPaper } from \"./paperLoader\";\nimport { InfilledPath } from \"./types\";\n\nconst paper = loadPaper();\n\nexport function optimizePaths(infilledPaths: InfilledPath[], start_x: number, start_y: number): paper.Path[] {\n    const paths: paper.Path[] = [];\n\n    function getLastPoint() {\n        if (paths.length === 0) {\n            throw new Error('no points found');\n        }\n\n        const lastPath = paths[paths.length - 1];\n        return lastPath.closed ? lastPath.firstSegment.point : lastPath.lastSegment.point;\n    }\n    \n    const infilledPathsCopy = [...infilledPaths];\n\n    while (infilledPathsCopy.length > 0) {\n        \n        const infilledPathToProcess = getClosestInfilledPath(infilledPathsCopy, paths.length > 0 ? getLastPoint() : new paper.Point(start_x, start_y));\n        const infilledPathIndex = infilledPathToProcess.infilledPathIndex;\n        let outlinePathIndex = infilledPathToProcess.index;\n\n        const infilledPath = infilledPathsCopy[infilledPathIndex];\n        const outlinePathsCopy = [...infilledPath.outlinePaths];\n\n        while (outlinePathsCopy.length > 0)\n        {\n            const currentOutlinePath = outlinePathsCopy[outlinePathIndex];\n            paths.push(currentOutlinePath);\n\n            outlinePathsCopy.splice(outlinePathIndex, 1);\n\n            const nextPath = getClosestPath(outlinePathsCopy, getLastPoint(), false);\n            if (nextPath) {\n                outlinePathIndex = nextPath.index;\n            }\n        }\n\n        const infillsCopy = [...infilledPath.infillPaths];\n        while (infillsCopy.length > 0) {\n            const nextInfill = getClosestPath(infillsCopy, getLastPoint(), true);\n            \n            if (nextInfill.reverse) {\n                nextInfill.path.reverse();\n            } \n\n            paths.push(nextInfill.path);\n\n            infillsCopy.splice(nextInfill.index, 1);\n        }\n\n        infilledPathsCopy.splice(infilledPathIndex, 1);\n    }\n    return paths;\n}\n\nfunction getClosestInfilledPath(infilledPaths: InfilledPath[], lastPoint: paper.Point) {\n    const infilledPathsCost = infilledPaths.map((ip, index) => {\n        // this could be optimized by considering all segments (and potentially drawing segments in reverse)\n        // at the expense of compute\n        \n        const closestOutlinePath = getClosestPath(ip.outlinePaths, lastPoint, false);\n        return {\n            infilledPath: ip,\n            infilledPathIndex: index,\n            ...closestOutlinePath,\n        }\n    });\n\n    return infilledPathsCost.sort((a, b) => a.cost - b.cost)[0];\n}\n\nfunction getClosestPath(paths: paper.Path[], lastPoint: paper.Point, canReverse: boolean) {\n    const pathCosts = paths.map((p, index) => {\n        const startPoint = p.firstSegment.point;\n        // cheaper to keep it squared\n        const startPointCost = startPoint.getDistance(lastPoint, true);\n\n        if (canReverse) {\n            const endPoint = p.lastSegment.point;\n            const endPointCost = endPoint.getDistance(lastPoint, true);\n\n            if (endPointCost >= startPointCost) {\n                return {path: p, cost: startPointCost, index, reverse: false};\n            } else {\n                return {path: p, cost: endPointCost, index, reverse: true};\n            }\n        } else {\n            return {path: p, cost: startPointCost, index, reverse: false};\n        }\n    });\n\n    return pathCosts.sort((a, b) => a.cost - b.cost)[0];\n}\n\n"
  },
  {
    "path": "tsc/src/paperLoader.ts",
    "content": "import paper from 'paper';\nimport { env } from 'process';\n\nlet loaded = false;\nexport function loadPaper(): paper.PaperScope {\n    if (env && env[\"server\"]) {\n        const paperModule = require(\"paper\");\n        return paperModule;\n    } else {\n        if (!loaded) {\n            importScripts(\"https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js\");\n            (self.paper as any as paper.PaperScope).install(self);\n            loaded = true;\n        }\n    \n        return self.paper as any as paper.PaperScope;\n    }\n    \n}"
  },
  {
    "path": "tsc/src/renderer.ts",
    "content": "import { Command } from './types';\nimport { loadPaper } from './paperLoader';\n\nconst paper = loadPaper();\n\nexport function renderPathsToCommands(paths: paper.Path[], width: number, height: number): Command[] {\n    const viewRectangle = new paper.Rectangle(0, 0, width, height);\n    return paths.flatMap(p => {\n        if (p.segments.length < 2) {\n            return [];\n        }\n\n        const commands: Command[] = ['p0'];\n        let started = false;\n        let firstSegment: paper.Segment | null = null;\n        for (const segment of p.segments) {\n            if (viewRectangle.contains(segment.point)) {\n                commands.push({\n                    x: segment.point.x,\n                    y: segment.point.y,\n                }); \n                if (!started) {\n                    firstSegment = segment;\n                    commands.push('p1');\n                    started = true;\n                }\n            }\n            \n        }\n        \n        if (firstSegment && p.closed) {\n            commands.push({\n                x: firstSegment.point.x,\n                y: firstSegment.point.y,\n            }); \n        }\n\n        return commands;\n    });\n}\n\n"
  },
  {
    "path": "tsc/src/tester.ts",
    "content": "import { renderCommandsToSvgJson } from \"./toSvgJson\";\nimport { vectorizeImageData } from './vectorizer';\nimport { renderSvgJsonToCommands } from \"./toCommands\";\nimport path from 'path';\nimport * as fs from 'fs';\nimport {loadImage, createCanvas} from 'canvas';\nimport { loadPaper } from './paperLoader';\nimport { RequestTypes } from \"./types\";\n\nconst paper = loadPaper();\n\nconst width = 1000;\nconst renderScaleFactor = 2;\n\nfunction updater(status: string) {\n    console.log(status);\n}\n\nasync function main_vectorRasterVector() {\n    const dirPath = path.join(__dirname, '../svgs');\n    const inDir = fs.opendirSync(dirPath);\n\n    const outDirPath = path.join(__dirname, '../svgs/out/');\n    \n\n    let dirEntry = inDir.readSync();\n    while (dirEntry) {\n        if (dirEntry.isFile() && dirEntry.name.endsWith(\".svg\")) {\n            if (dirEntry.name == \"finitecurve.svg\") {\n                console.log(`processing ${dirEntry.name}`);\n\n                const file = fs.readFileSync(path.join(dirEntry.path, dirEntry.name));\n                const svgString = file.toString();\n                const [imageData, svgWidth, svgHeight] = await getImageData(svgString, renderScaleFactor);\n\n                const vectorizedSvg = vectorizeImageData(imageData, 2);\n                const vectorizedJson = convertSvgToSvgJson(vectorizedSvg);\n\n                const height = Math.floor(svgHeight * (width / svgWidth));\n                \n                const request: RequestTypes.RenderSVGRequest = {\n                    svgJson: vectorizedJson,\n                    height,\n                    width,\n                    svgWidth: width * renderScaleFactor,\n                    svgHeight: height * renderScaleFactor,\n                    homeX: 0,\n                    homeY: 0,\n                    infillDensity: 4,\n                    type: 'renderSvg',\n                    flattenPaths: false,\n                };\n                const result = await renderSvgJsonToCommands(request, updater);\n                const resultSvgJsonString = renderCommandsToSvgJson(result.commands, width, height, updater);\n                const resultSvg = convertSvgJsonToSvg(resultSvgJsonString, width, height);\n                const fullResultPath = path.join(outDirPath, dirEntry.name);\n                fs.writeFileSync(fullResultPath, resultSvg);\n            }\n            \n        }\n        dirEntry = inDir.readSync();\n    }\n};\n\nasync function getImageData(svgString: string, renderScaleFactor: number): Promise<[ImageData, number, number]> {\n    const jsdom = require(\"jsdom\");\n    const window = new jsdom.JSDOM().window;\n    const parser = new window.DOMParser();\n    const serializer = new window.XMLSerializer();\n\n    const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');\n    const svgElement = svgDoc.documentElement;\n    const svgWidth = parseFloat(svgElement.getAttribute('width')!);\n    const svgHeight = parseFloat(svgElement.getAttribute('height')!);\n    \n    const scale = Math.min(width / svgWidth) * renderScaleFactor;\n    const scaledHeight = svgHeight * scale;\n    const scaledWidth = svgWidth * scale;\n\n    svgElement.setAttribute('width', scaledWidth.toString());\n    svgElement.setAttribute('height', scaledHeight.toString());\n\n    const scaledSvgString = serializer.serializeToString(svgElement);\n\n    const image = await loadImage(`data:image/svg+xml;base64,${btoa(scaledSvgString)}`);\n    \n    const canvas = createCanvas(scaledWidth, scaledHeight);\n    const ctx = canvas.getContext('2d');\n    \n    // Draw the image onto the canvas\n    ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);\n    \n    // Get the ImageData from the canvas\n    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    const dataMap = new Map();\n    for (const val of imageData.data) {\n        if (!dataMap.has(val)) {\n            dataMap.set(val, 1);\n        } else {\n            dataMap.set(val, dataMap.get(val) + 1);\n        }\n    }\n    const kvps = Array.from(dataMap);\n    kvps.sort((a, b) => b[1] - a[1]);\n    const fullImageData: ImageData = { ...imageData, colorSpace: \"srgb\", height: canvas.height, width: canvas.width};\n\n    return [fullImageData, svgWidth, svgHeight];\n}\n\nasync function main_pathTracer() {\n    const dirPath = path.join(__dirname, '../svgs');\n    const inDir = fs.opendirSync(dirPath);\n\n    const outDirPath = path.join(__dirname, '../svgs/out/');\n    \n\n    let dirEntry = inDir.readSync();\n    while (dirEntry) {\n        if (dirEntry.isFile() && dirEntry.name.endsWith(\".svg\")) {\n            if (dirEntry.name == \"finitecurve.svg\") {\n                console.log(`processing ${dirEntry.name}`);\n\n                const file = fs.readFileSync(path.join(dirEntry.path, dirEntry.name));\n                const svgString = file.toString();\n\n                const jsdom = require(\"jsdom\");\n                const window = new jsdom.JSDOM().window;\n                const parser = new window.DOMParser();\n\n                const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');\n                const svgElement = svgDoc.documentElement;\n                const svgWidth = parseFloat(svgElement.getAttribute('width')!);\n                const svgHeight = parseFloat(svgElement.getAttribute('height')!);\n\n                const height = Math.floor(svgHeight * (width / svgWidth));\n\n                const svgJson = convertSvgToSvgJson(svgString);\n                const request: RequestTypes.RenderSVGRequest = {\n                    svgJson: svgJson,\n                    height,\n                    width,\n                    svgWidth,\n                    svgHeight,\n                    homeX: 0,\n                    homeY: 0,\n                    infillDensity: 0,\n                    type: 'renderSvg',\n                    flattenPaths: false,\n                };\n                const result = await renderSvgJsonToCommands(request, updater);\n                fs.writeFileSync(path.join(__dirname, '../svgs/out/commands.txt'), result.commands.join('\\n'));\n                const resultSvgJsonString = renderCommandsToSvgJson(result.commands, width, height, updater);\n                const resultSvg = convertSvgJsonToSvg(resultSvgJsonString, width, height);\n                const fullResultPath = path.join(outDirPath, dirEntry.name);\n                fs.writeFileSync(fullResultPath, resultSvg);\n            }\n            \n        }\n        dirEntry = inDir.readSync();\n    }\n}\n\nfunction convertSvgToSvgJson(svgString: string) {\n    const size = new paper.Size(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);\n    paper.setup(size);\n    const svg = paper.project.importSVG(svgString, {\n        expandShapes: true,\n        applyMatrix: true,\n    });\n    const json = svg.exportJSON();\n    paper.project.remove();\n\n    return json;\n}\n\nfunction convertSvgJsonToSvg(svgJson: string, width: number, height: number): string {\n    const size = new paper.Size(width, height);\n    paper.setup(size);\n    paper.project.importJSON(svgJson);\n    const svg = paper.project.exportSVG({\n        asString: true,\n    }) as string;\n    paper.project.remove();\n    return svg;\n}\n\nmain_pathTracer();\n\n"
  },
  {
    "path": "tsc/src/toCommands.ts",
    "content": "import { Command, RequestTypes, updateStatusFn } from './types';\nimport { generatePaths } from './generator';\nimport { generateInfills } from './infill';\nimport { optimizePaths } from './optimizer';\nimport { renderPathsToCommands } from './renderer';\nimport { trimCommands } from './trimmer';\nimport { dedupeCommands } from './deduplicator';\nimport { measureDistance } from './measurer';\nimport { loadPaper } from './paperLoader';\nimport { flattenPaths } from './flattener';\n\nconst paper = loadPaper();\n\nexport async function renderSvgJsonToCommands(\n    request: RequestTypes.RenderSVGRequest,\n    updateStatusFn: updateStatusFn,\n) {\n    paper.setup({width: request.width, height: request.height});\n\n    updateStatusFn(\"Importing\");\n    const svg = paper.project.importJSON(request.svgJson);\n\n    // scale the document so its coordinates match the world 1:1, in mm\n    const projectToViewRatio = request.width / request.svgWidth;\n\n    console.log(`Scaling by ${projectToViewRatio}`);\n    svg.scale(projectToViewRatio, {x: 0, y: 0});\n    svg.applyMatrix = true;\n\n    updateStatusFn(\"Generating paths\");\n    const paths = generatePaths(svg);\n\n    paths.forEach(p => p.flatten(0.5));\n\n    if (request.flattenPaths) {\n        flattenPaths(paths, updateStatusFn);\n    }\n\n    updateStatusFn(\"Generating infill\");\n    const pathsWithInfills = generateInfills(paths, request.infillDensity);\n\n    updateStatusFn(\"Optimizing paths\");\n    const optimizedPaths = optimizePaths(pathsWithInfills, request.homeX, request.homeY);\n\n    updateStatusFn(\"Generating commands\");\n    const commands = renderPathsToCommands(optimizedPaths, request.width, request.height);\n    commands.push('p0');\n\n    const trimmedCommands = trimCommands(commands);\n\n    updateStatusFn(\"Simplifying commands\");\n\n    const dedupedCommands = dedupeCommands(trimmedCommands);\n\n    updateStatusFn(\"Measuring total distance\");\n    dedupedCommands.unshift(`h${request.height}`);\n    const distances = measureDistance(dedupedCommands);\n    const totalDistance = +distances.totalDistance.toFixed(1);\n    dedupedCommands.unshift(`d${totalDistance}`);\n\n    const commandStrings = dedupedCommands.map(stringifyCommand);\n    return {\n        commands: commandStrings,\n        distance: totalDistance,\n        drawDistance: +distances.drawDistance.toFixed(1),\n    };\n}\n\nfunction stringifyCommand(cmd: Command): string {\n    if (typeof cmd === 'string') {\n        return cmd;\n    } else {\n        return `${cmd.x} ${cmd.y}`;\n    }\n}\n"
  },
  {
    "path": "tsc/src/toSvgJson.ts",
    "content": "import { loadPaper } from './paperLoader';\nimport {updateStatusFn} from './types';\n\nconst paper = loadPaper();\n\nexport function renderCommandsToSvgJson(commands: string[], width: number, height: number, updateStatusFn: updateStatusFn): string {\n    updateStatusFn(\"Rendering result\");\n    const size = new paper.Size(width, height);\n    paper.setup(size);\n\n    const layer = paper.project.activeLayer;\n\n    let pathPoints: paper.Point[] = [];\n\n    let penUp = true;\n    function handlePenChange(newPenUp: boolean) {\n        if (penUp === newPenUp) {\n            // no change in state, nothing to do\n            return;\n        }\n\n        if (penUp) {\n            // the pen was up, now it's down\n            // discard whatever points we've accumulated while not drawing except the last one, which\n            // is our starting\n\n            penUp = false;\n            if (pathPoints.length > 1) {\n                pathPoints = [pathPoints[pathPoints.length - 1]];\n            }\n        } else {\n            penUp = true;\n            // then pen was down, now it's up\n            // create a path out of path points we've traveled so far\n            if (pathPoints.length > 1) {\n                const segments = pathPoints.map(p => new paper.Segment(p));\n                const path = new paper.Path(segments);\n                path.fillColor = new paper.Color('transparent');\n                path.strokeColor = new paper.Color('black');\n                layer.addChild(path);\n\n                pathPoints = [pathPoints[pathPoints.length - 1]];\n            }\n        }\n    }\n    \n    for (const command of commands) {\n        const firstChar = command.charAt(0);\n        if (firstChar === 'd') {\n            console.log(`Total distance ${command.slice(1)}`);\n            continue;\n        } else if (firstChar === 'h') {\n            console.log(`Drawing height is ${command.slice(1)}`);\n            continue;\n        } else if (firstChar === 'p') {\n            const secondChar = command.charAt(1);\n            if (secondChar === '1') {\n                handlePenChange(false);\n            } else if (secondChar === '0') {\n                handlePenChange(true);\n            }\n        }\n        else {\n            const coords = command.split(' ');\n            const x = parseFloat(coords[0]);\n            const y = parseFloat(coords[1]);\n            pathPoints.push(new paper.Point(x, y));\n        }\n    }\n\n    handlePenChange(true);\n\n    const backgroundPath = new paper.Path([\n        new paper.Segment({x: 0, y: 0}),\n        new paper.Segment({x: width, y: 0}),\n        new paper.Segment({x: width, y: height}),\n        new paper.Segment({x: 0, y: height}),\n        new paper.Segment({x: 0, y: 0}),\n    ]);\n    backgroundPath.fillColor = new paper.Color('#ffffff');\n    backgroundPath.strokeColor = new paper.Color('transparent');\n    layer.addChild(backgroundPath);\n    backgroundPath.sendToBack();\n    \n    return paper.project.exportJSON({\n        asString: true,\n    });\n}"
  },
  {
    "path": "tsc/src/tracer.js",
    "content": "/**\n * Ported from POTrace: https://github.com/kilobtye/potrace\n */\n\n/* Copyright (C) 2001-2013 Peter Selinger.\n *\n * A javascript port of Potrace (http://potrace.sourceforge.net).\n * \n * Licensed under the GPL\n * \n * Usage\n *   loadImageFromFile(file) : load image from File API\n *   loadImageFromUrl(url): load image from URL\n *     because of the same-origin policy, can not load image from another domain.\n *     input color/grayscale image is simply converted to binary image. no pre-\n *     process is performed.\n * \n *   setParameter({para1: value, ...}) : set parameters\n *     parameters:\n *        turnpolicy (\"black\" / \"white\" / \"left\" / \"right\" / \"minority\" / \"majority\")\n *          how to resolve ambiguities in path decomposition. (default: \"minority\")       \n *        turdsize\n *          suppress speckles of up to this size (default: 2)\n *        optcurve (true / false)\n *          turn on/off curve optimization (default: true)\n *        alphamax\n *          corner threshold parameter (default: 1)\n *        opttolerance \n *          curve optimization tolerance (default: 0.2)\n *       \n *   process(callback) : wait for the image be loaded, then run potrace algorithm,\n *                       then call callback function.\n * \n *   getSVG(size, opt_type) : return a string of generated SVG image.\n *                                    result_image_size = original_image_size * size\n *                                    optional parameter opt_type can be \"curve\"\n */\n\nexport var Potrace = (function () {\n\n    function Point(x, y) {\n        this.x = x;\n        this.y = y;\n    }\n\n    Point.prototype.copy = function () {\n        return new Point(this.x, this.y);\n    };\n\n    function Bitmap(w, h) {\n        this.w = w;\n        this.h = h;\n        this.size = w * h;\n        this.arraybuffer = new ArrayBuffer(this.size);\n        this.data = new Int8Array(this.arraybuffer);\n    }\n\n    Bitmap.prototype.at = function (x, y) {\n        return (x >= 0 && x < this.w && y >= 0 && y < this.h) &&\n            this.data[this.w * y + x] === 1;\n    };\n\n    Bitmap.prototype.index = function (i) {\n        var point = new Point();\n        point.y = Math.floor(i / this.w);\n        point.x = i - point.y * this.w;\n        return point;\n    };\n\n    Bitmap.prototype.flip = function (x, y) {\n        if (this.at(x, y)) {\n            this.data[this.w * y + x] = 0;\n        } else {\n            this.data[this.w * y + x] = 1;\n        }\n    };\n\n    Bitmap.prototype.copy = function () {\n        var bm = new Bitmap(this.w, this.h), i;\n        for (i = 0; i < this.size; i++) {\n            bm.data[i] = this.data[i];\n        }\n        return bm;\n    };\n\n    function Path() {\n        this.area = 0;\n        this.len = 0;\n        this.curve = {};\n        this.pt = [];\n        this.minX = 100000;\n        this.minY = 100000;\n        this.maxX = -1;\n        this.maxY = -1;\n    }\n\n    function Curve(n) {\n        this.n = n;\n        this.tag = new Array(n);\n        this.c = new Array(n * 3);\n        this.alphaCurve = 0;\n        this.vertex = new Array(n);\n        this.alpha = new Array(n);\n        this.alpha0 = new Array(n);\n        this.beta = new Array(n);\n    }\n\n    var bm = null,\n        pathlist = [],\n        callback,\n        info = {\n            isReady: false,\n            turnpolicy: \"minority\",\n            turdsize: 2,\n            optcurve: true,\n            alphamax: 1,\n            opttolerance: 0.2\n        };\n\n    function setParameter(obj) {\n        var key;\n        for (key in obj) {\n            if (obj.hasOwnProperty(key)) {\n                info[key] = obj[key];\n            }\n        }\n    }\n\n    function setBitmap(width, height, data) {\n        clear();\n        bm = new Bitmap(width, height);\n        bm.data = data;\n        info.isReady = true;\n        bmToPathlist();\n        processPath();\n    }\n\n    function bmToPathlist() {\n        var bm1 = bm.copy(),\n            currentPoint = new Point(0, 0),\n            path;\n\n        function findNext(point) {\n            var i = bm1.w * point.y + point.x;\n            while (i < bm1.size && bm1.data[i] !== 1) {\n                i++;\n            }\n            return i < bm1.size && bm1.index(i);\n        }\n\n        function majority(x, y) {\n            var i, a, ct;\n            for (i = 2; i < 5; i++) {\n                ct = 0;\n                for (a = -i + 1; a <= i - 1; a++) {\n                    ct += bm1.at(x + a, y + i - 1) ? 1 : -1;\n                    ct += bm1.at(x + i - 1, y + a - 1) ? 1 : -1;\n                    ct += bm1.at(x + a - 1, y - i) ? 1 : -1;\n                    ct += bm1.at(x - i, y + a) ? 1 : -1;\n                }\n                if (ct > 0) {\n                    return 1;\n                } else if (ct < 0) {\n                    return 0;\n                }\n            }\n            return 0;\n        }\n\n        function findPath(point) {\n            var path = new Path(),\n                x = point.x, y = point.y,\n                dirx = 0, diry = 1, tmp;\n\n            path.sign = bm.at(point.x, point.y) ? \"+\" : \"-\";\n\n            while (1) {\n                path.pt.push(new Point(x, y));\n                if (x > path.maxX)\n                    path.maxX = x;\n                if (x < path.minX)\n                    path.minX = x;\n                if (y > path.maxY)\n                    path.maxY = y;\n                if (y < path.minY)\n                    path.minY = y;\n                path.len++;\n\n                x += dirx;\n                y += diry;\n                path.area -= x * diry;\n\n                if (x === point.x && y === point.y)\n                    break;\n\n                var l = bm1.at(x + (dirx + diry - 1) / 2, y + (diry - dirx - 1) / 2);\n                var r = bm1.at(x + (dirx - diry - 1) / 2, y + (diry + dirx - 1) / 2);\n\n                if (r && !l) {\n                    if (info.turnpolicy === \"right\" ||\n                        (info.turnpolicy === \"black\" && path.sign === '+') ||\n                        (info.turnpolicy === \"white\" && path.sign === '-') ||\n                        (info.turnpolicy === \"majority\" && majority(x, y)) ||\n                        (info.turnpolicy === \"minority\" && !majority(x, y))) {\n                        tmp = dirx;\n                        dirx = -diry;\n                        diry = tmp;\n                    } else {\n                        tmp = dirx;\n                        dirx = diry;\n                        diry = -tmp;\n                    }\n                } else if (r) {\n                    tmp = dirx;\n                    dirx = -diry;\n                    diry = tmp;\n                } else if (!l) {\n                    tmp = dirx;\n                    dirx = diry;\n                    diry = -tmp;\n                }\n            }\n            return path;\n        }\n\n        function xorPath(path) {\n            var y1 = path.pt[0].y,\n                len = path.len,\n                x, y, maxX, minY, i, j;\n            for (i = 1; i < len; i++) {\n                x = path.pt[i].x;\n                y = path.pt[i].y;\n\n                if (y !== y1) {\n                    minY = y1 < y ? y1 : y;\n                    maxX = path.maxX;\n                    for (j = x; j < maxX; j++) {\n                        bm1.flip(j, minY);\n                    }\n                    y1 = y;\n                }\n            }\n\n        }\n\n        while (currentPoint = findNext(currentPoint)) {\n\n            path = findPath(currentPoint);\n\n            xorPath(path);\n\n            if (path.area > info.turdsize) {\n                pathlist.push(path);\n            }\n        }\n\n    }\n\n\n    function processPath() {\n\n        function Quad() {\n            this.data = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n        }\n\n        Quad.prototype.at = function (x, y) {\n            return this.data[x * 3 + y];\n        };\n\n        function Sum(x, y, xy, x2, y2) {\n            this.x = x;\n            this.y = y;\n            this.xy = xy;\n            this.x2 = x2;\n            this.y2 = y2;\n        }\n\n        function mod(a, n) {\n            return a >= n ? a % n : a >= 0 ? a : n - 1 - (-1 - a) % n;\n        }\n\n        function xprod(p1, p2) {\n            return p1.x * p2.y - p1.y * p2.x;\n        }\n\n        function cyclic(a, b, c) {\n            if (a <= c) {\n                return (a <= b && b < c);\n            } else {\n                return (a <= b || b < c);\n            }\n        }\n\n        function sign(i) {\n            return i > 0 ? 1 : i < 0 ? -1 : 0;\n        }\n\n        function quadform(Q, w) {\n            var v = new Array(3), i, j, sum;\n\n            v[0] = w.x;\n            v[1] = w.y;\n            v[2] = 1;\n            sum = 0.0;\n\n            for (i = 0; i < 3; i++) {\n                for (j = 0; j < 3; j++) {\n                    sum += v[i] * Q.at(i, j) * v[j];\n                }\n            }\n            return sum;\n        }\n\n        function interval(lambda, a, b) {\n            var res = new Point();\n\n            res.x = a.x + lambda * (b.x - a.x);\n            res.y = a.y + lambda * (b.y - a.y);\n            return res;\n        }\n\n        function dorth_infty(p0, p2) {\n            var r = new Point();\n\n            r.y = sign(p2.x - p0.x);\n            r.x = -sign(p2.y - p0.y);\n\n            return r;\n        }\n\n        function ddenom(p0, p2) {\n            var r = dorth_infty(p0, p2);\n\n            return r.y * (p2.x - p0.x) - r.x * (p2.y - p0.y);\n        }\n\n        function dpara(p0, p1, p2) {\n            var x1, y1, x2, y2;\n\n            x1 = p1.x - p0.x;\n            y1 = p1.y - p0.y;\n            x2 = p2.x - p0.x;\n            y2 = p2.y - p0.y;\n\n            return x1 * y2 - x2 * y1;\n        }\n\n        function cprod(p0, p1, p2, p3) {\n            var x1, y1, x2, y2;\n\n            x1 = p1.x - p0.x;\n            y1 = p1.y - p0.y;\n            x2 = p3.x - p2.x;\n            y2 = p3.y - p2.y;\n\n            return x1 * y2 - x2 * y1;\n        }\n\n        function iprod(p0, p1, p2) {\n            var x1, y1, x2, y2;\n\n            x1 = p1.x - p0.x;\n            y1 = p1.y - p0.y;\n            x2 = p2.x - p0.x;\n            y2 = p2.y - p0.y;\n\n            return x1 * x2 + y1 * y2;\n        }\n\n        function iprod1(p0, p1, p2, p3) {\n            var x1, y1, x2, y2;\n\n            x1 = p1.x - p0.x;\n            y1 = p1.y - p0.y;\n            x2 = p3.x - p2.x;\n            y2 = p3.y - p2.y;\n\n            return x1 * x2 + y1 * y2;\n        }\n\n        function ddist(p, q) {\n            return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));\n        }\n\n        function bezier(t, p0, p1, p2, p3) {\n            var s = 1 - t, res = new Point();\n\n            res.x = s * s * s * p0.x + 3 * (s * s * t) * p1.x + 3 * (t * t * s) * p2.x + t * t * t * p3.x;\n            res.y = s * s * s * p0.y + 3 * (s * s * t) * p1.y + 3 * (t * t * s) * p2.y + t * t * t * p3.y;\n\n            return res;\n        }\n\n        function tangent(p0, p1, p2, p3, q0, q1) {\n            var A, B, C, a, b, c, d, s, r1, r2;\n\n            A = cprod(p0, p1, q0, q1);\n            B = cprod(p1, p2, q0, q1);\n            C = cprod(p2, p3, q0, q1);\n\n            a = A - 2 * B + C;\n            b = -2 * A + 2 * B;\n            c = A;\n\n            d = b * b - 4 * a * c;\n\n            if (a === 0 || d < 0) {\n                return -1.0;\n            }\n\n            s = Math.sqrt(d);\n\n            r1 = (-b + s) / (2 * a);\n            r2 = (-b - s) / (2 * a);\n\n            if (r1 >= 0 && r1 <= 1) {\n                return r1;\n            } else if (r2 >= 0 && r2 <= 1) {\n                return r2;\n            } else {\n                return -1.0;\n            }\n        }\n\n        function calcSums(path) {\n            var i, x, y;\n            path.x0 = path.pt[0].x;\n            path.y0 = path.pt[0].y;\n\n            path.sums = [];\n            var s = path.sums;\n            s.push(new Sum(0, 0, 0, 0, 0));\n            for (i = 0; i < path.len; i++) {\n                x = path.pt[i].x - path.x0;\n                y = path.pt[i].y - path.y0;\n                s.push(new Sum(s[i].x + x, s[i].y + y, s[i].xy + x * y,\n                    s[i].x2 + x * x, s[i].y2 + y * y));\n            }\n        }\n\n        function calcLon(path) {\n\n            var n = path.len, pt = path.pt, dir,\n                pivk = new Array(n),\n                nc = new Array(n),\n                ct = new Array(4);\n            path.lon = new Array(n);\n\n            var constraint = [new Point(), new Point()],\n                cur = new Point(),\n                off = new Point(),\n                dk = new Point(),\n                foundk;\n\n            var i, j, k1, a, b, c, d, k = 0;\n            for (i = n - 1; i >= 0; i--) {\n                if (pt[i].x != pt[k].x && pt[i].y != pt[k].y) {\n                    k = i + 1;\n                }\n                nc[i] = k;\n            }\n\n            for (i = n - 1; i >= 0; i--) {\n                ct[0] = ct[1] = ct[2] = ct[3] = 0;\n                dir = (3 + 3 * (pt[mod(i + 1, n)].x - pt[i].x) +\n                    (pt[mod(i + 1, n)].y - pt[i].y)) / 2;\n                ct[dir]++;\n\n                constraint[0].x = 0;\n                constraint[0].y = 0;\n                constraint[1].x = 0;\n                constraint[1].y = 0;\n\n                k = nc[i];\n                k1 = i;\n                while (1) {\n                    foundk = 0;\n                    dir = (3 + 3 * sign(pt[k].x - pt[k1].x) +\n                        sign(pt[k].y - pt[k1].y)) / 2;\n                    ct[dir]++;\n\n                    if (ct[0] && ct[1] && ct[2] && ct[3]) {\n                        pivk[i] = k1;\n                        foundk = 1;\n                        break;\n                    }\n\n                    cur.x = pt[k].x - pt[i].x;\n                    cur.y = pt[k].y - pt[i].y;\n\n                    if (xprod(constraint[0], cur) < 0 || xprod(constraint[1], cur) > 0) {\n                        break;\n                    }\n\n                    if (Math.abs(cur.x) <= 1 && Math.abs(cur.y) <= 1) {\n\n                    } else {\n                        off.x = cur.x + ((cur.y >= 0 && (cur.y > 0 || cur.x < 0)) ? 1 : -1);\n                        off.y = cur.y + ((cur.x <= 0 && (cur.x < 0 || cur.y < 0)) ? 1 : -1);\n                        if (xprod(constraint[0], off) >= 0) {\n                            constraint[0].x = off.x;\n                            constraint[0].y = off.y;\n                        }\n                        off.x = cur.x + ((cur.y <= 0 && (cur.y < 0 || cur.x < 0)) ? 1 : -1);\n                        off.y = cur.y + ((cur.x >= 0 && (cur.x > 0 || cur.y < 0)) ? 1 : -1);\n                        if (xprod(constraint[1], off) <= 0) {\n                            constraint[1].x = off.x;\n                            constraint[1].y = off.y;\n                        }\n                    }\n                    k1 = k;\n                    k = nc[k1];\n                    if (!cyclic(k, i, k1)) {\n                        break;\n                    }\n                }\n                if (foundk === 0) {\n                    dk.x = sign(pt[k].x - pt[k1].x);\n                    dk.y = sign(pt[k].y - pt[k1].y);\n                    cur.x = pt[k1].x - pt[i].x;\n                    cur.y = pt[k1].y - pt[i].y;\n\n                    a = xprod(constraint[0], cur);\n                    b = xprod(constraint[0], dk);\n                    c = xprod(constraint[1], cur);\n                    d = xprod(constraint[1], dk);\n\n                    j = 10000000;\n                    if (b < 0) {\n                        j = Math.floor(a / -b);\n                    }\n                    if (d > 0) {\n                        j = Math.min(j, Math.floor(-c / d));\n                    }\n                    pivk[i] = mod(k1 + j, n);\n                }\n            }\n\n            j = pivk[n - 1];\n            path.lon[n - 1] = j;\n            for (i = n - 2; i >= 0; i--) {\n                if (cyclic(i + 1, pivk[i], j)) {\n                    j = pivk[i];\n                }\n                path.lon[i] = j;\n            }\n\n            for (i = n - 1; cyclic(mod(i + 1, n), j, path.lon[i]); i--) {\n                path.lon[i] = j;\n            }\n        }\n\n        function bestPolygon(path) {\n\n            function penalty3(path, i, j) {\n\n                var n = path.len, pt = path.pt, sums = path.sums;\n                var x, y, xy, x2, y2,\n                    k, a, b, c, s,\n                    px, py, ex, ey,\n                    r = 0;\n                if (j >= n) {\n                    j -= n;\n                    r = 1;\n                }\n\n                if (r === 0) {\n                    x = sums[j + 1].x - sums[i].x;\n                    y = sums[j + 1].y - sums[i].y;\n                    x2 = sums[j + 1].x2 - sums[i].x2;\n                    xy = sums[j + 1].xy - sums[i].xy;\n                    y2 = sums[j + 1].y2 - sums[i].y2;\n                    k = j + 1 - i;\n                } else {\n                    x = sums[j + 1].x - sums[i].x + sums[n].x;\n                    y = sums[j + 1].y - sums[i].y + sums[n].y;\n                    x2 = sums[j + 1].x2 - sums[i].x2 + sums[n].x2;\n                    xy = sums[j + 1].xy - sums[i].xy + sums[n].xy;\n                    y2 = sums[j + 1].y2 - sums[i].y2 + sums[n].y2;\n                    k = j + 1 - i + n;\n                }\n\n                px = (pt[i].x + pt[j].x) / 2.0 - pt[0].x;\n                py = (pt[i].y + pt[j].y) / 2.0 - pt[0].y;\n                ey = (pt[j].x - pt[i].x);\n                ex = -(pt[j].y - pt[i].y);\n\n                a = ((x2 - 2 * x * px) / k + px * px);\n                b = ((xy - x * py - y * px) / k + px * py);\n                c = ((y2 - 2 * y * py) / k + py * py);\n\n                s = ex * ex * a + 2 * ex * ey * b + ey * ey * c;\n\n                return Math.sqrt(s);\n            }\n\n            var i, j, m, k,\n                n = path.len,\n                pen = new Array(n + 1),\n                prev = new Array(n + 1),\n                clip0 = new Array(n),\n                clip1 = new Array(n + 1),\n                seg0 = new Array(n + 1),\n                seg1 = new Array(n + 1),\n                thispen, best, c;\n\n            for (i = 0; i < n; i++) {\n                c = mod(path.lon[mod(i - 1, n)] - 1, n);\n                if (c == i) {\n                    c = mod(i + 1, n);\n                }\n                if (c < i) {\n                    clip0[i] = n;\n                } else {\n                    clip0[i] = c;\n                }\n            }\n\n            j = 1;\n            for (i = 0; i < n; i++) {\n                while (j <= clip0[i]) {\n                    clip1[j] = i;\n                    j++;\n                }\n            }\n\n            i = 0;\n            for (j = 0; i < n; j++) {\n                seg0[j] = i;\n                i = clip0[i];\n            }\n            seg0[j] = n;\n            m = j;\n\n            i = n;\n            for (j = m; j > 0; j--) {\n                seg1[j] = i;\n                i = clip1[i];\n            }\n            seg1[0] = 0;\n\n            pen[0] = 0;\n            for (j = 1; j <= m; j++) {\n                for (i = seg1[j]; i <= seg0[j]; i++) {\n                    best = -1;\n                    for (k = seg0[j - 1]; k >= clip1[i]; k--) {\n                        thispen = penalty3(path, k, i) + pen[k];\n                        if (best < 0 || thispen < best) {\n                            prev[i] = k;\n                            best = thispen;\n                        }\n                    }\n                    pen[i] = best;\n                }\n            }\n            path.m = m;\n            path.po = new Array(m);\n\n            for (i = n, j = m - 1; i > 0; j--) {\n                i = prev[i];\n                path.po[j] = i;\n            }\n        }\n\n        function adjustVertices(path) {\n\n            function pointslope(path, i, j, ctr, dir) {\n\n                var n = path.len, sums = path.sums,\n                    x, y, x2, xy, y2,\n                    k, a, b, c, lambda2, l, r = 0;\n\n                while (j >= n) {\n                    j -= n;\n                    r += 1;\n                }\n                while (i >= n) {\n                    i -= n;\n                    r -= 1;\n                }\n                while (j < 0) {\n                    j += n;\n                    r -= 1;\n                }\n                while (i < 0) {\n                    i += n;\n                    r += 1;\n                }\n\n                x = sums[j + 1].x - sums[i].x + r * sums[n].x;\n                y = sums[j + 1].y - sums[i].y + r * sums[n].y;\n                x2 = sums[j + 1].x2 - sums[i].x2 + r * sums[n].x2;\n                xy = sums[j + 1].xy - sums[i].xy + r * sums[n].xy;\n                y2 = sums[j + 1].y2 - sums[i].y2 + r * sums[n].y2;\n                k = j + 1 - i + r * n;\n\n                ctr.x = x / k;\n                ctr.y = y / k;\n\n                a = (x2 - x * x / k) / k;\n                b = (xy - x * y / k) / k;\n                c = (y2 - y * y / k) / k;\n\n                lambda2 = (a + c + Math.sqrt((a - c) * (a - c) + 4 * b * b)) / 2;\n\n                a -= lambda2;\n                c -= lambda2;\n\n                if (Math.abs(a) >= Math.abs(c)) {\n                    l = Math.sqrt(a * a + b * b);\n                    if (l !== 0) {\n                        dir.x = -b / l;\n                        dir.y = a / l;\n                    }\n                } else {\n                    l = Math.sqrt(c * c + b * b);\n                    if (l !== 0) {\n                        dir.x = -c / l;\n                        dir.y = b / l;\n                    }\n                }\n                if (l === 0) {\n                    dir.x = dir.y = 0;\n                }\n            }\n\n            var m = path.m, po = path.po, n = path.len, pt = path.pt,\n                x0 = path.x0, y0 = path.y0,\n                ctr = new Array(m), dir = new Array(m),\n                q = new Array(m),\n                v = new Array(3), d, i, j, k, l,\n                s = new Point();\n\n            path.curve = new Curve(m);\n\n            for (i = 0; i < m; i++) {\n                j = po[mod(i + 1, m)];\n                j = mod(j - po[i], n) + po[i];\n                ctr[i] = new Point();\n                dir[i] = new Point();\n                pointslope(path, po[i], j, ctr[i], dir[i]);\n            }\n\n            for (i = 0; i < m; i++) {\n                q[i] = new Quad();\n                d = dir[i].x * dir[i].x + dir[i].y * dir[i].y;\n                if (d === 0.0) {\n                    for (j = 0; j < 3; j++) {\n                        for (k = 0; k < 3; k++) {\n                            q[i].data[j * 3 + k] = 0;\n                        }\n                    }\n                } else {\n                    v[0] = dir[i].y;\n                    v[1] = -dir[i].x;\n                    v[2] = - v[1] * ctr[i].y - v[0] * ctr[i].x;\n                    for (l = 0; l < 3; l++) {\n                        for (k = 0; k < 3; k++) {\n                            q[i].data[l * 3 + k] = v[l] * v[k] / d;\n                        }\n                    }\n                }\n            }\n\n            var Q, w, dx, dy, det, min, cand, xmin, ymin, z;\n            for (i = 0; i < m; i++) {\n                Q = new Quad();\n                w = new Point();\n\n                s.x = pt[po[i]].x - x0;\n                s.y = pt[po[i]].y - y0;\n\n                j = mod(i - 1, m);\n\n                for (l = 0; l < 3; l++) {\n                    for (k = 0; k < 3; k++) {\n                        Q.data[l * 3 + k] = q[j].at(l, k) + q[i].at(l, k);\n                    }\n                }\n\n                while (1) {\n\n                    det = Q.at(0, 0) * Q.at(1, 1) - Q.at(0, 1) * Q.at(1, 0);\n                    if (det !== 0.0) {\n                        w.x = (-Q.at(0, 2) * Q.at(1, 1) + Q.at(1, 2) * Q.at(0, 1)) / det;\n                        w.y = (Q.at(0, 2) * Q.at(1, 0) - Q.at(1, 2) * Q.at(0, 0)) / det;\n                        break;\n                    }\n\n                    if (Q.at(0, 0) > Q.at(1, 1)) {\n                        v[0] = -Q.at(0, 1);\n                        v[1] = Q.at(0, 0);\n                    } else if (Q.at(1, 1)) {\n                        v[0] = -Q.at(1, 1);\n                        v[1] = Q.at(1, 0);\n                    } else {\n                        v[0] = 1;\n                        v[1] = 0;\n                    }\n                    d = v[0] * v[0] + v[1] * v[1];\n                    v[2] = - v[1] * s.y - v[0] * s.x;\n                    for (l = 0; l < 3; l++) {\n                        for (k = 0; k < 3; k++) {\n                            Q.data[l * 3 + k] += v[l] * v[k] / d;\n                        }\n                    }\n                }\n                dx = Math.abs(w.x - s.x);\n                dy = Math.abs(w.y - s.y);\n                if (dx <= 0.5 && dy <= 0.5) {\n                    path.curve.vertex[i] = new Point(w.x + x0, w.y + y0);\n                    continue;\n                }\n\n                min = quadform(Q, s);\n                xmin = s.x;\n                ymin = s.y;\n\n                if (Q.at(0, 0) !== 0.0) {\n                    for (z = 0; z < 2; z++) {\n                        w.y = s.y - 0.5 + z;\n                        w.x = - (Q.at(0, 1) * w.y + Q.at(0, 2)) / Q.at(0, 0);\n                        dx = Math.abs(w.x - s.x);\n                        cand = quadform(Q, w);\n                        if (dx <= 0.5 && cand < min) {\n                            min = cand;\n                            xmin = w.x;\n                            ymin = w.y;\n                        }\n                    }\n                }\n\n                if (Q.at(1, 1) !== 0.0) {\n                    for (z = 0; z < 2; z++) {\n                        w.x = s.x - 0.5 + z;\n                        w.y = - (Q.at(1, 0) * w.x + Q.at(1, 2)) / Q.at(1, 1);\n                        dy = Math.abs(w.y - s.y);\n                        cand = quadform(Q, w);\n                        if (dy <= 0.5 && cand < min) {\n                            min = cand;\n                            xmin = w.x;\n                            ymin = w.y;\n                        }\n                    }\n                }\n\n                for (l = 0; l < 2; l++) {\n                    for (k = 0; k < 2; k++) {\n                        w.x = s.x - 0.5 + l;\n                        w.y = s.y - 0.5 + k;\n                        cand = quadform(Q, w);\n                        if (cand < min) {\n                            min = cand;\n                            xmin = w.x;\n                            ymin = w.y;\n                        }\n                    }\n                }\n\n                path.curve.vertex[i] = new Point(xmin + x0, ymin + y0);\n            }\n        }\n\n        function reverse(path) {\n            var curve = path.curve, m = curve.n, v = curve.vertex, i, j, tmp;\n\n            for (i = 0, j = m - 1; i < j; i++, j--) {\n                tmp = v[i];\n                v[i] = v[j];\n                v[j] = tmp;\n            }\n        }\n\n        function smooth(path) {\n            var m = path.curve.n, curve = path.curve;\n\n            var i, j, k, dd, denom, alpha,\n                p2, p3, p4;\n\n            for (i = 0; i < m; i++) {\n                j = mod(i + 1, m);\n                k = mod(i + 2, m);\n                p4 = interval(1 / 2.0, curve.vertex[k], curve.vertex[j]);\n\n                denom = ddenom(curve.vertex[i], curve.vertex[k]);\n                if (denom !== 0.0) {\n                    dd = dpara(curve.vertex[i], curve.vertex[j], curve.vertex[k]) / denom;\n                    dd = Math.abs(dd);\n                    alpha = dd > 1 ? (1 - 1.0 / dd) : 0;\n                    alpha = alpha / 0.75;\n                } else {\n                    alpha = 4 / 3.0;\n                }\n                curve.alpha0[j] = alpha;\n\n                if (alpha >= info.alphamax) {\n                    curve.tag[j] = \"CORNER\";\n                    curve.c[3 * j + 1] = curve.vertex[j];\n                    curve.c[3 * j + 2] = p4;\n                } else {\n                    if (alpha < 0.55) {\n                        alpha = 0.55;\n                    } else if (alpha > 1) {\n                        alpha = 1;\n                    }\n                    p2 = interval(0.5 + 0.5 * alpha, curve.vertex[i], curve.vertex[j]);\n                    p3 = interval(0.5 + 0.5 * alpha, curve.vertex[k], curve.vertex[j]);\n                    curve.tag[j] = \"CURVE\";\n                    curve.c[3 * j + 0] = p2;\n                    curve.c[3 * j + 1] = p3;\n                    curve.c[3 * j + 2] = p4;\n                }\n                curve.alpha[j] = alpha;\n                curve.beta[j] = 0.5;\n            }\n            curve.alphacurve = 1;\n        }\n\n        function optiCurve(path) {\n            function Opti() {\n                this.pen = 0;\n                this.c = [new Point(), new Point()];\n                this.t = 0;\n                this.s = 0;\n                this.alpha = 0;\n            }\n\n            function opti_penalty(path, i, j, res, opttolerance, convc, areac) {\n                var m = path.curve.n, curve = path.curve, vertex = curve.vertex,\n                    k, k1, k2, conv, i1,\n                    area, alpha, d, d1, d2,\n                    p0, p1, p2, p3, pt,\n                    A, R, A1, A2, A3, A4,\n                    s, t;\n\n                if (i == j) {\n                    return 1;\n                }\n\n                k = i;\n                i1 = mod(i + 1, m);\n                k1 = mod(k + 1, m);\n                conv = convc[k1];\n                if (conv === 0) {\n                    return 1;\n                }\n                d = ddist(vertex[i], vertex[i1]);\n                for (k = k1; k != j; k = k1) {\n                    k1 = mod(k + 1, m);\n                    k2 = mod(k + 2, m);\n                    if (convc[k1] != conv) {\n                        return 1;\n                    }\n                    if (sign(cprod(vertex[i], vertex[i1], vertex[k1], vertex[k2])) !=\n                        conv) {\n                        return 1;\n                    }\n                    if (iprod1(vertex[i], vertex[i1], vertex[k1], vertex[k2]) <\n                        d * ddist(vertex[k1], vertex[k2]) * -0.999847695156) {\n                        return 1;\n                    }\n                }\n\n                p0 = curve.c[mod(i, m) * 3 + 2].copy();\n                p1 = vertex[mod(i + 1, m)].copy();\n                p2 = vertex[mod(j, m)].copy();\n                p3 = curve.c[mod(j, m) * 3 + 2].copy();\n\n                area = areac[j] - areac[i];\n                area -= dpara(vertex[0], curve.c[i * 3 + 2], curve.c[j * 3 + 2]) / 2;\n                if (i >= j) {\n                    area += areac[m];\n                }\n\n                A1 = dpara(p0, p1, p2);\n                A2 = dpara(p0, p1, p3);\n                A3 = dpara(p0, p2, p3);\n\n                A4 = A1 + A3 - A2;\n\n                if (A2 == A1) {\n                    return 1;\n                }\n\n                t = A3 / (A3 - A4);\n                s = A2 / (A2 - A1);\n                A = A2 * t / 2.0;\n\n                if (A === 0.0) {\n                    return 1;\n                }\n\n                R = area / A;\n                alpha = 2 - Math.sqrt(4 - R / 0.3);\n\n                res.c[0] = interval(t * alpha, p0, p1);\n                res.c[1] = interval(s * alpha, p3, p2);\n                res.alpha = alpha;\n                res.t = t;\n                res.s = s;\n\n                p1 = res.c[0].copy();\n                p2 = res.c[1].copy();\n\n                res.pen = 0;\n\n                for (k = mod(i + 1, m); k != j; k = k1) {\n                    k1 = mod(k + 1, m);\n                    t = tangent(p0, p1, p2, p3, vertex[k], vertex[k1]);\n                    if (t < -0.5) {\n                        return 1;\n                    }\n                    pt = bezier(t, p0, p1, p2, p3);\n                    d = ddist(vertex[k], vertex[k1]);\n                    if (d === 0.0) {\n                        return 1;\n                    }\n                    d1 = dpara(vertex[k], vertex[k1], pt) / d;\n                    if (Math.abs(d1) > opttolerance) {\n                        return 1;\n                    }\n                    if (iprod(vertex[k], vertex[k1], pt) < 0 ||\n                        iprod(vertex[k1], vertex[k], pt) < 0) {\n                        return 1;\n                    }\n                    res.pen += d1 * d1;\n                }\n\n                for (k = i; k != j; k = k1) {\n                    k1 = mod(k + 1, m);\n                    t = tangent(p0, p1, p2, p3, curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]);\n                    if (t < -0.5) {\n                        return 1;\n                    }\n                    pt = bezier(t, p0, p1, p2, p3);\n                    d = ddist(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]);\n                    if (d === 0.0) {\n                        return 1;\n                    }\n                    d1 = dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], pt) / d;\n                    d2 = dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], vertex[k1]) / d;\n                    d2 *= 0.75 * curve.alpha[k1];\n                    if (d2 < 0) {\n                        d1 = -d1;\n                        d2 = -d2;\n                    }\n                    if (d1 < d2 - opttolerance) {\n                        return 1;\n                    }\n                    if (d1 < d2) {\n                        res.pen += (d1 - d2) * (d1 - d2);\n                    }\n                }\n\n                return 0;\n            }\n\n            var curve = path.curve, m = curve.n, vert = curve.vertex,\n                pt = new Array(m + 1),\n                pen = new Array(m + 1),\n                len = new Array(m + 1),\n                opt = new Array(m + 1),\n                om, i, j, r,\n                o = new Opti(), p0,\n                i1, area, alpha, ocurve,\n                s, t;\n\n            var convc = new Array(m), areac = new Array(m + 1);\n\n            for (i = 0; i < m; i++) {\n                if (curve.tag[i] == \"CURVE\") {\n                    convc[i] = sign(dpara(vert[mod(i - 1, m)], vert[i], vert[mod(i + 1, m)]));\n                } else {\n                    convc[i] = 0;\n                }\n            }\n\n            area = 0.0;\n            areac[0] = 0.0;\n            p0 = curve.vertex[0];\n            for (i = 0; i < m; i++) {\n                i1 = mod(i + 1, m);\n                if (curve.tag[i1] == \"CURVE\") {\n                    alpha = curve.alpha[i1];\n                    area += 0.3 * alpha * (4 - alpha) *\n                        dpara(curve.c[i * 3 + 2], vert[i1], curve.c[i1 * 3 + 2]) / 2;\n                    area += dpara(p0, curve.c[i * 3 + 2], curve.c[i1 * 3 + 2]) / 2;\n                }\n                areac[i + 1] = area;\n            }\n\n            pt[0] = -1;\n            pen[0] = 0;\n            len[0] = 0;\n\n\n            for (j = 1; j <= m; j++) {\n                pt[j] = j - 1;\n                pen[j] = pen[j - 1];\n                len[j] = len[j - 1] + 1;\n\n                for (i = j - 2; i >= 0; i--) {\n                    r = opti_penalty(path, i, mod(j, m), o, info.opttolerance, convc,\n                        areac);\n                    if (r) {\n                        break;\n                    }\n                    if (len[j] > len[i] + 1 ||\n                        (len[j] == len[i] + 1 && pen[j] > pen[i] + o.pen)) {\n                        pt[j] = i;\n                        pen[j] = pen[i] + o.pen;\n                        len[j] = len[i] + 1;\n                        opt[j] = o;\n                        o = new Opti();\n                    }\n                }\n            }\n            om = len[m];\n            ocurve = new Curve(om);\n            s = new Array(om);\n            t = new Array(om);\n\n            j = m;\n            for (i = om - 1; i >= 0; i--) {\n                if (pt[j] == j - 1) {\n                    ocurve.tag[i] = curve.tag[mod(j, m)];\n                    ocurve.c[i * 3 + 0] = curve.c[mod(j, m) * 3 + 0];\n                    ocurve.c[i * 3 + 1] = curve.c[mod(j, m) * 3 + 1];\n                    ocurve.c[i * 3 + 2] = curve.c[mod(j, m) * 3 + 2];\n                    ocurve.vertex[i] = curve.vertex[mod(j, m)];\n                    ocurve.alpha[i] = curve.alpha[mod(j, m)];\n                    ocurve.alpha0[i] = curve.alpha0[mod(j, m)];\n                    ocurve.beta[i] = curve.beta[mod(j, m)];\n                    s[i] = t[i] = 1.0;\n                } else {\n                    ocurve.tag[i] = \"CURVE\";\n                    ocurve.c[i * 3 + 0] = opt[j].c[0];\n                    ocurve.c[i * 3 + 1] = opt[j].c[1];\n                    ocurve.c[i * 3 + 2] = curve.c[mod(j, m) * 3 + 2];\n                    ocurve.vertex[i] = interval(opt[j].s, curve.c[mod(j, m) * 3 + 2],\n                        vert[mod(j, m)]);\n                    ocurve.alpha[i] = opt[j].alpha;\n                    ocurve.alpha0[i] = opt[j].alpha;\n                    s[i] = opt[j].s;\n                    t[i] = opt[j].t;\n                }\n                j = pt[j];\n            }\n\n            for (i = 0; i < om; i++) {\n                i1 = mod(i + 1, om);\n                ocurve.beta[i] = s[i] / (s[i] + t[i1]);\n            }\n            ocurve.alphacurve = 1;\n            path.curve = ocurve;\n        }\n\n        for (var i = 0; i < pathlist.length; i++) {\n            var path = pathlist[i];\n            calcSums(path);\n            calcLon(path);\n            bestPolygon(path);\n            adjustVertices(path);\n\n            if (path.sign === \"-\") {\n                reverse(path);\n            }\n\n            smooth(path);\n\n            if (info.optcurve) {\n                optiCurve(path);\n            }\n        }\n\n    }\n\n    function process(c) {\n        if (c) {\n            callback = c;\n        }\n        if (!info.isReady) {\n            setTimeout(process, 100);\n            return;\n        }\n        bmToPathlist();\n        processPath();\n        callback();\n        callback = null;\n    }\n\n    function clear() {\n        bm = null;\n        pathlist = [];\n        callback = null;\n        info.isReady = false;\n    }\n\n    function getSVG(size, opt_type) {\n\n        function path(curve) {\n\n            function bezier(i) {\n                var b = 'C ' + (curve.c[i * 3 + 0].x * size).toFixed(3) + ' ' +\n                    (curve.c[i * 3 + 0].y * size).toFixed(3) + ',';\n                b += (curve.c[i * 3 + 1].x * size).toFixed(3) + ' ' +\n                    (curve.c[i * 3 + 1].y * size).toFixed(3) + ',';\n                b += (curve.c[i * 3 + 2].x * size).toFixed(3) + ' ' +\n                    (curve.c[i * 3 + 2].y * size).toFixed(3) + ' ';\n                return b;\n            }\n\n            function segment(i) {\n                var s = 'L ' + (curve.c[i * 3 + 1].x * size).toFixed(3) + ' ' +\n                    (curve.c[i * 3 + 1].y * size).toFixed(3) + ' ';\n                s += (curve.c[i * 3 + 2].x * size).toFixed(3) + ' ' +\n                    (curve.c[i * 3 + 2].y * size).toFixed(3) + ' ';\n                return s;\n            }\n\n            var n = curve.n, i;\n            var p = 'M' + (curve.c[(n - 1) * 3 + 2].x * size).toFixed(3) +\n                ' ' + (curve.c[(n - 1) * 3 + 2].y * size).toFixed(3) + ' ';\n            for (i = 0; i < n; i++) {\n                if (curve.tag[i] === \"CURVE\") {\n                    p += bezier(i);\n                } else if (curve.tag[i] === \"CORNER\") {\n                    p += segment(i);\n                }\n            }\n            //p += \n            return p;\n        }\n\n        var w = bm.w * size, h = bm.h * size,\n            len = pathlist.length, c, i, strokec, fillc, fillrule;\n\n        var svg = '<svg id=\"svg\" version=\"1.1\" width=\"' + w + '\" height=\"' + h +\n            '\" xmlns=\"http://www.w3.org/2000/svg\">';\n        svg += '<path d=\"';\n        for (i = 0; i < len; i++) {\n            c = pathlist[i].curve;\n            svg += path(c);\n        }\n        if (opt_type === \"curve\") {\n            strokec = \"black\";\n            fillc = \"none\";\n            fillrule = '';\n        } else {\n            strokec = \"none\";\n            fillc = \"black\";\n            fillrule = ' fill-rule=\"evenodd\"';\n        }\n        svg += '\" stroke=\"' + strokec + '\" fill=\"' + fillc + '\"' + fillrule + '/></svg>';\n        return svg;\n    }\n\n    return {\n        setBitmap: setBitmap,\n        setParameter: setParameter,\n        process: process,\n        getSVG: getSVG,\n    };\n});"
  },
  {
    "path": "tsc/src/trimmer.ts",
    "content": "import { Command } from \"./types\";\n\nexport function trimCommands<T extends Command>(commands: T[], precision = 1): Command[] {\n    return commands.map(cmd => {\n        if (typeof cmd === 'string') {\n            return cmd;\n        } else {\n            return {\n                x: +cmd.x.toFixed(precision),\n                y: +cmd.y.toFixed(precision),\n            }\n        }\n    });\n}"
  },
  {
    "path": "tsc/src/types.ts",
    "content": "\nexport type updateStatusFn = (status: string) => void;\n\nexport type CoordinateCommand = {\n    x: number;\n    y: number;\n}\n\nexport type PenUpCommand = 'p0';\nexport type PenDownCommand = 'p1';\nexport type DistanceCommand = `d${number}`\nexport type HeightCommand = `h${number}`;\n\nexport type Command = CoordinateCommand | PenUpCommand | PenDownCommand | DistanceCommand | HeightCommand;\n\nexport type InfilledPath = {\n    outlinePaths: paper.Path[],\n    infillPaths: paper.Path[],\n    originalPath: paper.PathItem,\n}\n\nexport type InfillDensity = 0 | 1 | 2 | 3 | 4;\nexport const InfillDensities: InfillDensity[] = [0, 1, 2, 3, 4];\n\nexport namespace RequestTypes {\n    export type RenderSVGRequest = {\n        type: 'renderSvg',\n        svgJson: string,\n        width: number,\n        height: number,\n        svgWidth: number,\n        svgHeight: number,\n        homeX: number,\n        homeY: number,\n        infillDensity: InfillDensity,\n        flattenPaths: boolean,\n    };\n\n    export type VectorizeRequest = {\n        type: 'vectorize',\n        raster: ImageData,\n        turdSize: number,\n    }\n}"
  },
  {
    "path": "tsc/src/utils.ts",
    "content": "import { Command, CoordinateCommand } from \"./types\";\n//import path from 'path';\n//import * as fs from 'fs';\nimport {loadPaper} from './paperLoader';\n\nconst paper = loadPaper();\n\nexport function getLastPoint(commandList: Command[]) : CoordinateCommand | undefined {\n    for (let i = commandList.length - 1; i >= 0; i--) {\n        const command = commandList[i];\n        if (typeof command === 'string') {\n            continue;\n        } else {\n            return command;\n        }\n    }\n\n    return undefined;\n}\n\nexport function distanceBetweenPoints(cmd1: CoordinateCommand, cmd2: CoordinateCommand): number {\n    return Math.sqrt(Math.pow(cmd2.x - cmd1.x, 2) + Math.pow(cmd2.y - cmd1.y, 2));\n}\n\nexport function distanceBetweenPointsSquared(cmd1: CoordinateCommand, cmd2: CoordinateCommand): number {\n    return Math.pow(cmd2.x - cmd1.x, 2) + Math.pow(cmd2.y - cmd1.y, 2);\n}\n\nexport function isPathWhiteOnly(path: paper.PathItem): boolean {\n    return !!(path.fillColor && path.fillColor.toCSS(true) === '#ffffff' && !path.strokeColor);\n}\n\n// export function dumpSVG(svg: paper.Item) {\n//     const svgString = svg.exportSVG({\n//         asString: true,\n//     }) as string;\n//     return dumpStringAsSvg(svgString);\n// }\n\n// export async function dumpCanvas(canvas: Canvas) {\n//     const fullPath = path.join(__dirname, '../svgs/out.png');\n//     fs.writeFileSync(fullPath, canvas.toBuffer());\n// }\n\n// export async function dumpStringAsSvg(svgString: string) {\n//     const fullPath = path.join(__dirname, '../svgs/out.svg');\n//     fs.writeFileSync(fullPath, svgString);\n// }"
  },
  {
    "path": "tsc/src/vectorizer.ts",
    "content": "import { loadPaper } from './paperLoader';\nimport {Potrace} from './tracer';\n\n\nconst paper = loadPaper();\n\nconst WHITE_COLOR = new paper.Color(\"#FFFFFF\");\n\nexport function vectorizeImageData(imageData: ImageData, turdSize: number): string {\n    const colorMatrix: paper.Color[][] = []\n\n    for (let row = 0; row < imageData.height; row++) {\n        for (let column = 0; column < imageData.width; column++) {\n            if (!colorMatrix[row]) {\n                colorMatrix[row] = [];\n            }\n            const address = (row * imageData.width + column) * 4;\n            const r = imageData.data[address];\n            const g = imageData.data[address + 1];\n            const b = imageData.data[address + 2];\n            const a = imageData.data[address + 3];\n            const color = new paper.Color(r / 255, g / 255, b / 255, a / 255);\n            colorMatrix[row][column] = color;\n        }\n    }\n\n    return createPathsFromColorMatrix(colorMatrix, turdSize);\n}\n\n\nfunction createPathsFromColorMatrix(colorMatrix: paper.Color[][], turdSize: number): string {\n    const width = colorMatrix[0].length;\n    const height = colorMatrix.length;\n\n    const data: (1|0)[] = [];\n    for (let row = 0; row < height; row++) {\n        for (let column = 0; column < width; column++) {\n            let bmColor: (1|0) = 0;\n            const currentColor = colorMatrix[row][column];\n            \n            if (currentColor.alpha > 0 && !currentColor.equals(WHITE_COLOR)) {\n                bmColor = 1;\n            }\n\n            data.push(bmColor);\n        }\n    }\n\n    const tracer = Potrace();\n    tracer.setParameter({\"turdsize\": turdSize});\n    tracer.setBitmap(width, height, data);\n\n    const svgString: string = tracer.getSVG(1);\n\n    return svgString;\n}\n\nfunction colorDistance(color1: paper.Color, color2: paper.Color) {\n    return (color2.red - color1.red) ** 2 + (color2.green - color1.green) ** 2 + (color2.blue - color1.blue) ** 2;\n}\n\n"
  },
  {
    "path": "tsc/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig to read more about this file */\n\n    /* Projects */\n    // \"incremental\": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */\n    // \"composite\": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */\n    // \"tsBuildInfoFile\": \"./.tsbuildinfo\",              /* Specify the path to .tsbuildinfo incremental compilation file. */\n    // \"disableSourceOfProjectReferenceRedirect\": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */\n    // \"disableSolutionSearching\": true,                 /* Opt a project out of multi-project reference checking when editing. */\n    // \"disableReferencedProjectLoad\": true,             /* Reduce the number of projects loaded automatically by TypeScript. */\n\n    /* Language and Environment */\n    \"target\": \"ES2020\",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */\n    // \"lib\": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */\n    // \"jsx\": \"preserve\",                                /* Specify what JSX code is generated. */\n    // \"experimentalDecorators\": true,                   /* Enable experimental support for legacy experimental decorators. */\n    // \"emitDecoratorMetadata\": true,                    /* Emit design-type metadata for decorated declarations in source files. */\n    // \"jsxFactory\": \"\",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */\n    // \"jsxFragmentFactory\": \"\",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */\n    // \"jsxImportSource\": \"\",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */\n    // \"reactNamespace\": \"\",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */\n    // \"noLib\": true,                                    /* Disable including any library files, including the default lib.d.ts. */\n    // \"useDefineForClassFields\": true,                  /* Emit ECMAScript-standard-compliant class fields. */\n    // \"moduleDetection\": \"auto\",                        /* Control what method is used to detect module-format JS files. */\n\n    /* Modules */\n    \"module\": \"CommonJS\",                                /* Specify what module code is generated. */\n    \"rootDir\": \"src\",                                  /* Specify the root folder within your source files. */\n    \"moduleResolution\": \"Node\",                     /* Specify how TypeScript looks up a file from a given module specifier. */\n    // \"baseUrl\": \"./\",                                  /* Specify the base directory to resolve non-relative module names. */\n    // \"paths\": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */\n    // \"rootDirs\": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */\n    // \"typeRoots\": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */\n    // \"types\": [],                                      /* Specify type package names to be included without being referenced in a source file. */\n    // \"allowUmdGlobalAccess\": true,                     /* Allow accessing UMD globals from modules. */\n    // \"moduleSuffixes\": [],                             /* List of file name suffixes to search when resolving a module. */\n    // \"allowImportingTsExtensions\": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */\n    // \"resolvePackageJsonExports\": true,                /* Use the package.json 'exports' field when resolving package imports. */\n    // \"resolvePackageJsonImports\": true,                /* Use the package.json 'imports' field when resolving imports. */\n    // \"customConditions\": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */\n    // \"resolveJsonModule\": true,                        /* Enable importing .json files. */\n    // \"allowArbitraryExtensions\": true,                 /* Enable importing files with any extension, provided a declaration file is present. */\n    // \"noResolve\": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */\n\n    /* JavaScript Support */\n    \"allowJs\": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */\n    // \"checkJs\": true,                                  /* Enable error reporting in type-checked JavaScript files. */\n    // \"maxNodeModuleJsDepth\": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */\n\n    /* Emit */\n    // \"declaration\": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */\n    // \"declarationMap\": true,                           /* Create sourcemaps for d.ts files. */\n    // \"emitDeclarationOnly\": true,                      /* Only output d.ts files and not JavaScript files. */\n    \"sourceMap\": true,                                /* Create source map files for emitted JavaScript files. */\n    // \"inlineSourceMap\": true,                          /* Include sourcemap files inside the emitted JavaScript. */\n    // \"outFile\": \"./\",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */\n    \"outDir\": \"dist\",                                   /* Specify an output folder for all emitted files. */\n    // \"removeComments\": true,                           /* Disable emitting comments. */\n    // \"noEmit\": true,                                   /* Disable emitting files from a compilation. */\n    // \"importHelpers\": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */\n    // \"importsNotUsedAsValues\": \"remove\",               /* Specify emit/checking behavior for imports that are only used for types. */\n    // \"downlevelIteration\": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */\n    // \"sourceRoot\": \"\",                                 /* Specify the root path for debuggers to find the reference source code. */\n    // \"mapRoot\": \"\",                                    /* Specify the location where debugger should locate map files instead of generated locations. */\n    // \"inlineSources\": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */\n    // \"emitBOM\": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */\n    // \"newLine\": \"crlf\",                                /* Set the newline character for emitting files. */\n    // \"stripInternal\": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */\n    // \"noEmitHelpers\": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */\n    // \"noEmitOnError\": true,                            /* Disable emitting files if any type checking errors are reported. */\n    // \"preserveConstEnums\": true,                       /* Disable erasing 'const enum' declarations in generated code. */\n    // \"declarationDir\": \"./\",                           /* Specify the output directory for generated declaration files. */\n    // \"preserveValueImports\": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */\n\n    /* Interop Constraints */\n    // \"isolatedModules\": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */\n    // \"verbatimModuleSyntax\": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */\n    // \"allowSyntheticDefaultImports\": true,             /* Allow 'import x from y' when a module doesn't have a default export. */\n    \"esModuleInterop\": true,  \n    // \"preserveSymlinks\": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */\n    \"forceConsistentCasingInFileNames\": true,            /* Ensure that casing is correct in imports. */\n\n    /* Type Checking */\n    \"strict\": true,                                      /* Enable all strict type-checking options. */\n    \"noImplicitAny\": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */\n    \"strictNullChecks\": true,                         /* When type checking, take into account 'null' and 'undefined'. */\n    \"strictFunctionTypes\": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */\n    \"strictBindCallApply\": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */\n    // \"strictPropertyInitialization\": true,             /* Check for class properties that are declared but not set in the constructor. */\n    // \"noImplicitThis\": true,                           /* Enable error reporting when 'this' is given the type 'any'. */\n    // \"useUnknownInCatchVariables\": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */\n    // \"alwaysStrict\": true,                             /* Ensure 'use strict' is always emitted. */\n    // \"noUnusedLocals\": true,                           /* Enable error reporting when local variables aren't read. */\n    // \"noUnusedParameters\": true,                       /* Raise an error when a function parameter isn't read. */\n    // \"exactOptionalPropertyTypes\": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */\n    // \"noImplicitReturns\": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */\n    // \"noFallthroughCasesInSwitch\": true,               /* Enable error reporting for fallthrough cases in switch statements. */\n    // \"noUncheckedIndexedAccess\": true,                 /* Add 'undefined' to a type when accessed using an index. */\n    // \"noImplicitOverride\": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */\n    // \"noPropertyAccessFromIndexSignature\": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */\n    // \"allowUnusedLabels\": true,                        /* Disable error reporting for unused labels. */\n    // \"allowUnreachableCode\": true,                     /* Disable error reporting for unreachable code. */\n\n    /* Completeness */\n    // \"skipDefaultLibCheck\": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */\n    \"skipLibCheck\": true                                 /* Skip type checking all .d.ts files. */\n  },\n  \"files\": [\"src/main.ts\", \"src/tester.ts\"]\n}\n"
  },
  {
    "path": "tsc/webpack.config.js",
    "content": "// Generated using webpack-cli https://github.com/webpack/webpack-cli\n\nconst path = require('path');\n\nconst isProduction = process.env.NODE_ENV == 'production';\n\n\nconst config = {\n    entry: './src/main.ts',\n    output: {\n        path: path.resolve(__dirname, 'dist_packed'),\n    },\n    plugins: [\n        // Add your plugins here\n        // Learn more about plugins from https://webpack.js.org/configuration/plugins/\n    ],\n    module: {\n        rules: [\n            {\n                test: /\\.(ts|tsx)$/i,\n                loader: 'ts-loader',\n                exclude: ['/node_modules/'],\n                options: {\n                    transpileOnly: true\n                }\n            },\n            {\n                test: /\\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,\n                type: 'asset',\n            },\n\n            // Add your rules for custom modules here\n            // Learn more about loaders from https://webpack.js.org/loaders/\n        ]\n    },\n    resolve: {\n        extensions: ['.tsx', '.ts', '.jsx', '.js', '...'],\n    },\n    externals: {\n        'paper': 'paper',\n        'jsdom': 'jsdom',\n        'canvas': 'canvas',\n    }\n};\n\nmodule.exports = () => {\n    if (isProduction) {\n        config.mode = 'production';\n        \n        \n    } else {\n        config.mode = 'development';\n    }\n    return config;\n};\n"
  }
]