[
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: GitHub Pages Deployment\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: 'latest'\n\n    - name: Install TypeScript\n      run: npm install -g typescript\n\n    - name: Compile TypeScript\n      run: tsc\n\n    - name: Deploy to GitHub Pages\n      uses: peaceiris/actions-gh-pages@v3\n      with:\n        github_token: ${{ secrets.GITHUB_TOKEN }}\n        publish_dir: .\n        publish_branch: gh-pages\n        exclude_assets: .github,src,.gitignore,app.js.map,LICENSE,README.md,tsconfig.json,app.ts\n        full_commit_message: Deploy to Github Pages"
  },
  {
    "path": ".gitignore",
    "content": "*.js\n*.js.map\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Marian Kleineberg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![](https://i.imgur.com/L7moBQT.png)\n\n# Part Designer\n\nThis is a free online CAD tool to create custom LEGO® Technic compatible construction parts for 3D printing.\n\nFeatures\n- Assemble a custom part from basic blocks: Pin Hole, Axle Hole, Pin, Axle, Solid\n- Save your model as an STL file\n- Catalog of existing LEGO® parts\n- Customize measurements to get a perfect fit\n- Create a sharable link of your part\n\n# Local setup and development\n\nYou need to have [TypeScript](https://www.typescriptlang.org/) installed.\nIn the project root, run `tsc`.\nThis should run without errors and create the file `app.js`.\n\nYou need a webserver that locally serves the files from the project directory.\nIf you have python installed, you can call `python3 -m http.server`.\nIt will tell you the port, for example 8000, and you can visit http://localhost:8000 in your browser.\nAlternatively, you can install [http-server](https://www.npmjs.com/package/http-server), which will also create a server in port 8000.\n\nIf you work on the code, run `tsc --watch`, which will recompile everytime you change a source file.\n"
  },
  {
    "path": "app.css",
    "content": "html, body {\n    font-family: 'Segoe UI', sans-serif;\n    margin: 0px;\n    height: 100%;\n    overflow: hidden;\n    font-size: 14px;\n}\n\n.canvas-container canvas {\n    width: 100%;\n    height: 100%;\n}\n\n.canvas-container canvas:focus {\n    outline: none;\n}\n\n.canvas-container {\n    padding-left: 420px;\n    box-sizing: border-box;\n    width: 100%;\n    height: 100%;\n}\n\n.sidebar {\n    background-color: rgb(219, 219, 219);\n    width: 420px;\n    height: 100%;\n    position: fixed;\n    overflow: auto;\n}\n\ndetails {\n    padding: 10px;\n    overflow: hidden;\n}\n\nsummary {\n    font-weight: bold;\n    box-sizing: border-box;\n    background-color: rgb(167, 167, 167);\n    margin: -10px;\n    padding: 7px;\n    user-select: none;\n    -moz-user-select: none;\n    border-bottom: 1px solid rgb(219, 219, 219);\n    transition: background-color 0.2s;\n    outline: none;\n}\n\nsummary:hover {\n    background-color: rgb(184, 184, 184);\n}\n\ndetails[open] summary {    \n    margin-bottom: 10px;\n}\n\n.cell {    \n    display: table-cell;\n    width: 100%;\n    padding-right: 10px;\n}\n\n.cell:last-of-type {\n    padding-right: 0;\n}\n\n.group {\n    margin-bottom: 10px;\n    display: table;\n    table-layout: fixed;\n    width: 100%;\n}\n\nbutton, label.radiolabel {\n    transition: 0.1s;\n    border: 1px solid rgb(21, 98, 212);\n}\n\nbutton {\n    position: relative;\n    background-color: rgb(21, 98, 212);\n    border-radius: 5px;\n    color: white;\n    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);\n    padding: 8px 16px;\n}\n\nlabel.radiolabel {\n    text-align: center;\n    background-color: rgb(219, 219, 219);\n    border-right-style: none;\n    margin-right: -6px;\n    color: rgb(21, 98, 212);\n    display: table-cell;\n    vertical-align: middle;\n    user-select: none;\n    -moz-user-select: none;\n    width: 100%;    \n    padding: 8px;\n}\n\nlabel.radiolabel:active, button:active {\n    box-shadow: inset 2px 2px 6px 0px rgba(0,0,0,0.35);\n}\n\ninput[type=radio]:checked + label.radiolabel {\n    background-color: rgb(21, 98, 212);\n    color: white;\n    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);\n}\n\n.radiolabel:hover, input[type=radio]:checked + label.radiolabel:hover, button:hover {\n    border: 1px solid rgb(47, 130, 255);\n    background-color: rgb(47, 130, 255);\n    color: white;\n    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);\n}\n\n.group input[type=radio] {\n    display: none;\n}\n\nlabel.radiolabel:first-of-type {\n    border-top-left-radius: 5px;\n    border-bottom-left-radius: 5px;\n}\n\nlabel.radiolabel:last-of-type {\n    border-top-right-radius: 5px;\n    border-bottom-right-radius: 5px;\n    border-right-style: solid;\n}\n\nlabel.radiolabel.x {\n    color: #FF0000;\n}\n\ninput[type=radio]:checked + label.radiolabel.x {\n    color: white;\n    background-color: #B30000;\n    border-color: #B30000;\n}\n\ninput[type=radio]:checked + label.radiolabel.x:hover, label.radiolabel.x:hover {\n    color: white;\n    background-color: red;\n    border-color: red;\n}\n\nlabel.radiolabel.y {\n    color: #009A05;\n}\n\ninput[type=radio]:checked + label.radiolabel.y {\n    color: white;\n    background-color: #009A05;\n    border-color: #009A05;\n}\n\ninput[type=radio]:checked + label.radiolabel.y:hover, label.radiolabel.y:hover {\n    color: white;\n    background-color: #00DF07;\n    border-color: #00DF07;\n}\n\nlabel.radiolabel.z {\n    color: #0000FF;\n}\n\ninput[type=radio]:checked + label.radiolabel.z {\n    color: white;\n    background-color: #0011A7;\n    border-color: #0011A7;\n}\n\ninput[type=radio]:checked + label.radiolabel.z:hover, label.radiolabel.z:hover {\n    color: white;\n    background-color: #001AFF;\n    border-color: #001AFF;\n}\n\n.editorhint {\n    display: table-cell;\n    width: 84px;\n    vertical-align: middle;\n}\n\n.catalogItem {\n    margin: 2px;\n    padding: 2px;\n    display: inline-block;\n    border: 2px solid rgba(0,0,0,0);\n    border-radius: 5px;\n    transition: 0.2s;\n}\n\n.catalogItem:hover {\n    border-color: rgb(47, 130, 255);\n}\n\n.group select, .group select:focus {\n    display: table-cell;\n    width: 100%;\n    border-radius: 5px;\n    padding: 6px;\n    border: 1px solid rgb(21, 98, 212);\n    background-color:  rgb(219, 219, 219);\n    transition: 0.1s;\n    color: rgb(21, 98, 212);\n}\n\n.group select:hover, .group select:active {\n    border: 1px solid rgb(47, 130, 255);\n    background-color: rgb(47, 130, 255);\n    color: white;\n    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);\n}\n\n.group .measurementhint {\n    display: table-cell;\n    width: 180px;\n    vertical-align: middle;\n}\n.group .reset {\n    display: table-cell;\n    vertical-align: middle;\n    text-align: right;\n    padding-right: 10px;\n    width: 50px;\n    color: rgb(21, 98, 212);\n}\n\n.group .reset:visited {\n    color: rgb(21, 98, 212);\n}\n\n.group input[type=text] {\n    padding: 7px;\n    border-radius: 5px 0px 0px 5px;\n    border: 1px solid rgb(47, 130, 255);\n    border-right: none;\n    background-color: rgb(219, 219, 219);\n    display: table-cell;\n    width: 100%;\n    box-sizing: border-box;\n}\n\n.group .measurement {\n    text-align: right;\n}\n\n.group input[type=text]:last-child {\n    border-radius: 5px;\n    border-right: none;\n    border: 1px solid rgb(47, 130, 255);\n}\n\n.unit {\n    border-radius: 5px;\n    border: 1px solid rgb(47, 130, 255);\n    background-color: rgb(219, 219, 219);\n    border-radius: 0px 5px 5px 0px;\n    border-left: none;\n    display: table-cell;\n    width: 32px;\n    box-sizing: border-box;\n    color: rgb(78, 78, 78);\n    user-select: none;\n}\n\n.fineprint, .fineprint a, .fineprint a:visited {\n    font-size: 13px;\n    color: rgb(78, 78, 78);\n}\n\n.part-buttons {\n    margin-bottom: 10px;\n}\n\n.key {\n    display: inline-block;\n    padding: 3px 5px;\n    line-height: 10px;\n    font: 10px monospace;\n    color: #555;\n    vertical-align: middle;\n    background-color: #eee;\n    border: solid 1px #ccc;\n    border-radius: 3px;\n    box-shadow: inset 0 -1px 0 #bbb;\n}\n"
  },
  {
    "path": "app.ts",
    "content": "﻿let gl: WebGLRenderingContext;\n\nvar editor: Editor;\nvar catalog: Catalog;\n\nwindow.onload = () => {\n\tcatalog = new Catalog();\n\teditor = new Editor();\n};\n\nwindow.onpopstate = function(event: PopStateEvent){\n    if (event.state) {\n\t\tvar url = new URL(document.URL);\n\t\tif (url.searchParams.has(\"part\")) {\n\t\t\teditor.part = Part.fromString(url.searchParams.get(\"part\"));\n\t\t\teditor.updateMesh(true);\n\t\t}\n    }\n};"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\" />\n    <title>Part Designer</title>\n    <meta name=\"author\" content=\"Marian Kleineberg\">\n    <meta name=\"description\" content=\"A free online CAD tool to create custom LEGO&reg; Technic compatible construction parts for 3D printing.\">\n    <link rel=\"stylesheet\" href=\"app.css\" type=\"text/css\" />\n    <link rel=\"icon\" href=\"favicon.png\" type=\"image/png\" sizes=\"16x16\"/>\n</head>\n<body>\n    <div class=\"sidebar\">\n        <details open>\n            <summary>Part</summary>\n            <div class=\"part-buttons\">\n                <button id=\"clear\">Clear</button>\n                <button id=\"share\">Share</button>\n                <button id=\"save-stl\">Save STL</button>\n                <button id=\"save-studio\">Save Studio part</button>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Display style</span>\n                <select id=\"style\">\n                    <option value=\"0\">Contour</option>\n                    <option value=\"1\">Solid</option>\n                    <option value=\"2\">Wireframe</option>\n                    <option value=\"3\">Solid Wireframe</option>\n                </select>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Part name</span>\n                <input type=\"text\" id=\"partName\" placeholder=\"Unnamed part\">\n            </div>\n        </details>\n        <details id=\"blockeditor\" open>\n            <summary>Edit Block</summary>\n            <div class=\"group\" id=\"type\">\n                <span class=\"editorhint\">Type</span>\n                <input type=\"radio\" id=\"pinhole\" name=\"type\" value=\"pinhole\" checked>\n                <label class=\"radiolabel\" for=\"pinhole\">Pin Hole</label>\n\n                <input type=\"radio\" id=\"axlehole\" name=\"type\" value=\"axlehole\">\n                <label class=\"radiolabel\" for=\"axlehole\">Axle Hole</label>\n\n                <input type=\"radio\" id=\"pin\" name=\"type\" value=\"pin\">\n                <label class=\"radiolabel\" for=\"pin\">Pin</label>\n\n                <input type=\"radio\" id=\"axle\" name=\"type\" value=\"axle\">\n                <label class=\"radiolabel\" for=\"axle\">Axle</label>\n\n                <input type=\"radio\" id=\"solid\" name=\"type\" value=\"solid\">\n                <label class=\"radiolabel\" for=\"solid\">Solid</label>\n\n                <input type=\"radio\" id=\"balljoint\" name=\"type\" value=\"balljoint\">\n                <label class=\"radiolabel\" for=\"balljoint\">Ball Joint</label>\n            </div>\n            <div class=\"group\" id=\"orientation\">\n                <span class=\"editorhint\">Orientation</span>\n                <input type=\"radio\" id=\"x\" name=\"orientation\" value=\"x\" checked>\n                <label class=\"radiolabel x\" for=\"x\">X</label>\n            \n                <input type=\"radio\" id=\"y\" name=\"orientation\" value=\"y\">\n                <label class=\"radiolabel y\" for=\"y\">Y</label>\n            \n                <input type=\"radio\" id=\"z\" name=\"orientation\" value=\"z\">\n                <label class=\"radiolabel z\" for=\"z\">Z</label>\n            </div>\n            <div class=\"group\" id=\"size\">\n                <span class=\"editorhint\">Size</span>            \n                <input type=\"radio\" id=\"full\" name=\"blocksize\" value=\"full\" checked>\n                <label class=\"radiolabel\" for=\"full\">Full Block</label>\n\n                <input type=\"radio\" id=\"half\" name=\"blocksize\" value=\"half\">\n                <label class=\"radiolabel\" for=\"half\">Half Block</label>\n            </div>\n            <div class=\"group\" id=\"rounded\">\n                <span class=\"editorhint\">Edges</span>\n                <input type=\"radio\" id=\"true\" name=\"rounded\" value=\"true\" checked>\n                <label class=\"radiolabel\" for=\"true\">Rounded</label>\n            \n                <input type=\"radio\" id=\"false\" name=\"rounded\" value=\"false\">\n                <label class=\"radiolabel\" for=\"false\">Not Rounded</label>\n            </div>\n            <div class=\"group\">\n                <span class=\"editorhint\"></span>\n                <button id=\"remove\">Remove block</button>\n            </div>\n        </details>\n        <details id=\"catalog\">\n            <summary>Catalog</summary>\n        </details>\n        <details>\n            <summary>Measurements</summary>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Technic Unit</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"technicUnit\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Subdivisions Per Quarter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"subdivisionsPerQuarter\">\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Edge Margin</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"edgeMargin\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Interior Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"interiorRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Pin Hole Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"pinHoleRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Pin Hole Offset</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"pinHoleOffset\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Axle Hole Size</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"axleHoleSize\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Pin Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"pinRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Pin Lip Size</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"pinLipRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Lip Subdivisions</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"lipSubdivisions\">\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Axle Size Inner</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"axleSizeInner\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Axle Size Outer</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"axleSizeOuter\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Attachment Adapter Size</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"attachmentAdapterSize\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Attachment Adapter Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"attachmentAdapterRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Interior End Margin</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"interiorEndMargin\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Ball Joint Ball Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"ballRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <span class=\"measurementhint\">Ball Joint Base Diameter</span>\n                <a href=\"#\" class=\"reset\">Reset</a>\n                <input class=\"measurement\" type=\"text\" id=\"ballBaseRadius\">\n                <span class=\"unit\">mm</span>\n            </div>\n            <div class=\"group\">\n                <div class=\"cell\"><button id=\"resetmeasurements\">Reset</button></div>\n                <div class=\"cell\"><button id=\"applymeasurements\">Apply</button></div>\n            </div>\n        </details>\n\n        <details open>\n            <summary>About</summary>\n            <p>This is a free online CAD tool to create custom LEGO&reg; Technic compatible construction parts for 3D printing.</p>\n            <p>\n            <table>\n                <tr>\n                    <td><span class=\"key\">1</span> <span class=\"key\">2</span> <span class=\"key\">3</span>\n                        <span class=\"key\">4</span> <span class=\"key\">5</span> <span class=\"key\">6</span>\n                    </td>\n                    <td>set type</td>\n                </tr>\n                <tr>\n                    <td><span class=\"key\">x</span> <span class=\"key\">y</span> <span class=\"key\">z</span></td>\n                    <td>set orientation</td>\n                </tr>\n                <tr>\n                    <td><span class=\"key\">←</span> <span class=\"key\">→</span> <span class=\"key\">↑</span>\n                        <span class=\"key\">↓</span> <span class=\"key\">PgUp</span> <span class=\"key\">PgDown</span>\n                    </td>\n                    <td>\n                        move cursor\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <span class=\"key\">Del</span>, <span class=\"key\">BkSp</span>\n                    </td>\n                    <td>\n                        remove block\n                    </td>\n                </tr>\n            </table>\n            </p>\n            <p>You can find the source code for this project on <a href=\"https://github.com/marian42/partdesigner\" target=\"_blank\">Github</a>.</p>\n            <p class=\"fineprint\">\n                <a href=\"https://marian42.de/\" target=\"_blank\">marian42.de</a>&nbsp;&middot;&nbsp;\n                <a href=\"mailto:mail@marian42.de\">Contact</a>\n            </p>\n        </details>\n    </div>\n    <div class=\"canvas-container\">\n        <canvas id=\"canvas\"></canvas>\n    </div>\n    \n    <script src=\"app.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "src/MeshGenerator.ts",
    "content": "class MeshGenerator {\n    protected triangles: Triangle[] = [];\n    protected measurements: Measurements;\n\n    constructor(measurements: Measurements) {\n        this.measurements = measurements;\n    }\n\n    public getMesh(): Mesh {\n        return new Mesh(this.triangles);\n    }\n\n    protected createQuad(v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3, flipped = false) {\n        if (!flipped) {\n            this.triangles.push(new Triangle(v1, v2, v4));\n            this.triangles.push(new Triangle(v2, v3, v4));\n        } else {\n            this.triangles.push(new Triangle(v4, v2, v1));\n            this.triangles.push(new Triangle(v4, v3, v2));\n        }\n    }\n\n    protected createQuadWithNormals(v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3, n1: Vector3, n2: Vector3, n3: Vector3, n4: Vector3, flipped = false) {\n        if (!flipped) {\n            this.triangles.push(new TriangleWithNormals(v1, v2, v4, n1, n2, n4));\n            this.triangles.push(new TriangleWithNormals(v2, v3, v4, n2, n3, n4));\n        } else {\n            this.triangles.push(new TriangleWithNormals(v4, v2, v1, n4.times(-1), n2.times(-1), n1.times(-1)));\n            this.triangles.push(new TriangleWithNormals(v4, v3, v2, n4.times(-1), n3.times(-1), n2.times(-1)));\n        }\n    }\n\n    protected createCircleWithHole(block: TinyBlock, innerRadius: number, outerRadius: number, offset: number, inverted = false, square = false) {\n        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));\n\n        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n            let i1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter);\n            let i2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter);\n            var o1 = i1;\n            var o2 = i2;\n\n            if (square) {\n                if (Math.abs(o1.dot(block.right)) > Math.abs(o1.dot(block.up))) {\n                    o1 = o1.times(1 / Math.abs(o1.dot(block.right)));\n                } else {\n                    o1 = o1.times(1 / Math.abs(o1.dot(block.up)));\n                }\n                if (Math.abs(o2.dot(block.right)) > Math.abs(o2.dot(block.up))) {\n                    o2 = o2.times(1 / Math.abs(o2.dot(block.right)));\n                } else {\n                    o2 = o2.times(1 / Math.abs(o2.dot(block.up)));\n                }\n            }\n\n            this.createQuad(\n                i1.times(innerRadius).plus(center),\n                i2.times(innerRadius).plus(center),\n                o2.times(outerRadius).plus(center),\n                o1.times(outerRadius).plus(center),\n                inverted);\n        }\n    }\n\n    protected createCircle(block: TinyBlock, radius: number, offset: number, inverted = false) {\n        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));\n\n        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n            let p1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter, radius);\n            let p2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter, radius);\n\n            if (inverted) {\n                this.triangles.push(new Triangle(center.plus(p1), center, center.plus(p2)));\n            } else {\n                this.triangles.push(new Triangle(center, center.plus(p1), center.plus(p2)));\n            }            \n        }\n    }\n\n    protected createCylinder(block: TinyBlock, offset: number, radius: number, distance: number, inverted = false) {\n        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));\n\n        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n            let v1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter);\n            let v2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter);\n            this.createQuadWithNormals(\n                center.plus(v1.times(radius)),\n                center.plus(v2.times(radius)),\n                center.plus(v2.times(radius)).plus(block.forward.times(distance)),\n                center.plus(v1.times(radius)).plus(block.forward.times(distance)),\n                v1, v2, v2, v1,\n                !inverted);\n        }\n    }\n\n    public tinyIndexToWorld(p: number): number {\n        let i = Math.floor((p + 1) / 3);\n        let j = p - i * 3;\n    \n        var f = 0.5 * i;\n        if (j == 0) {\n            f += this.measurements.edgeMargin;\n        } else if (j == 1) {\n            f += 0.5 - this.measurements.edgeMargin;\n        }\n    \n        return f;\n    }\n    \n    public tinyBlockToWorld(position: Vector3): Vector3 {\n        return new Vector3(this.tinyIndexToWorld(position.x), this.tinyIndexToWorld(position.y), this.tinyIndexToWorld(position.z));\n    }\n}"
  },
  {
    "path": "src/PartMeshGenerator.ts",
    "content": "class PartMeshGenerator extends MeshGenerator {\n    private smallBlocks: VectorDictionary<SmallBlock>;\n\tprivate tinyBlocks: VectorDictionary<TinyBlock>;\n\n\tconstructor(part: Part, measurements: Measurements) {\n        super(measurements);\n        this.smallBlocks = part.createSmallBlocks();\n\t\tthis.createDummyBlocks();\n        this.updateRounded();\n        this.createTinyBlocks();\n        this.processTinyBlocks();\n        this.checkInteriors();\n\t\tthis.mergeSimilarBlocks();\n\t\tthis.renderPerpendicularRoundedAdapters();\n\t\tthis.renderRoundedExteriors();\n\t\tthis.renderInteriors();\n        this.renderAttachments();\n        this.renderTinyBlockFaces();\n    }\n\n    private updateRounded() {\n\t\tvar perpendicularRoundedAdapters: SmallBlock[] = [];\n\n        for (var block of this.smallBlocks.values()) {\n\t\t\tif (block.isAttachment) {\n\t\t\t\tblock.rounded = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!block.rounded) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar next = this.smallBlocks.getOrNull(block.position.plus(block.forward));\n\t\t\tif (next != null && next.orientation == block.orientation && next.quadrant != block.quadrant) {\n\t\t\t\tblock.rounded = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar previous = this.smallBlocks.getOrNull(block.position.minus(block.forward));\n\t\t\tif (previous != null && previous.orientation == block.orientation && previous.quadrant != block.quadrant) {\n\t\t\t\tblock.rounded = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar neighbor1 = this.smallBlocks.getOrNull(block.position.plus(block.horizontal));\n\t\t\tvar neighbor2 = this.smallBlocks.getOrNull(block.position.plus(block.vertical));\n\t\t\tif ((neighbor1 == null || (neighbor1.isAttachment && neighbor1.forward.dot(block.right) == 0))\n\t\t\t\t&& (neighbor2 == null || (neighbor2.isAttachment && neighbor2.forward.dot(block.up) == 0))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (this.createPerpendicularRoundedAdapterIfPossible(block)) {\n\t\t\t\tperpendicularRoundedAdapters.push(block);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tblock.rounded = false;\n\t\t}\n\n\t\t// Remove adapters where the neighbor was later changed from rounded to not rounded\n\t\tvar anythingChanged: boolean;\n\t\tdo {\n\t\t\tanythingChanged = false;\n\t\t\tfor (var block of perpendicularRoundedAdapters) {\n\t\t\t\tif (block.perpendicularRoundedAdapter != null && !block.perpendicularRoundedAdapter.neighbor.rounded) {\n\t\t\t\t\tblock.perpendicularRoundedAdapter = null;\n\t\t\t\t\tblock.rounded = false;\n\t\t\t\t\tanythingChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} while (anythingChanged);\n    }\n\n    private createDummyBlocks() {\n        var addedAnything = false;\n\t\tfor (var block of this.smallBlocks.values()) {\n\t\t\tif (!block.isAttachment) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar affectedPositions = [\n\t\t\t\tblock.position,\n\t\t\t\tblock.position.minus(block.horizontal),\n\t\t\t\tblock.position.minus(block.vertical),\n\t\t\t\tblock.position.minus(block.horizontal).minus(block.vertical)\n            ];\n\t\t\tfor (var forwardDirection = -1; forwardDirection <= 1; forwardDirection += 2) {\n\t\t\t\tvar count = countInArray(affectedPositions, (p) => this.smallBlocks.containsKey(p.plus(block.forward.times(forwardDirection))));\n\t\t\t\tif (count != 0 && count != 4) {\n\t\t\t\t\tvar source = new Block(block.orientation, BlockType.Solid, true);\n\t\t\t\t\tfor (var position of affectedPositions) {\n\t\t\t\t\t\tvar targetPosition = position.plus(block.forward.times(forwardDirection));\n\t\t\t\t\t\tif (!this.smallBlocks.containsKey(targetPosition)) {\n\t\t\t\t\t\t\tthis.smallBlocks.set(targetPosition, new SmallBlock(this.smallBlocks.get(position).quadrant, targetPosition, source));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\taddedAnything = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (addedAnything) {\n\t\t\tthis.createDummyBlocks();\n\t\t}\n\t}\n\t\n\tprivate createPerpendicularRoundedAdapterIfPossible(block: SmallBlock): boolean {\n\t\tvar neighbor1 = this.smallBlocks.getOrNull(block.position.plus(block.horizontal));\n\t\tvar neighbor2 = this.smallBlocks.getOrNull(block.position.plus(block.vertical));\n\t\t\n\t\tvar hasHorizontalNeighbor = neighbor2 == null && neighbor1 != null && neighbor1.forward.dot(block.horizontal) != 0 && neighbor1.rounded;\n\t\tvar hasVerticalNeighbor = neighbor1 == null && neighbor2 != null && neighbor2.forward.dot(block.vertical) != 0 && neighbor2.rounded;\n\t\t\n\t\tif (hasHorizontalNeighbor == hasVerticalNeighbor) {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar adapter = new PerpendicularRoundedAdapter();\n\t\tadapter.directionToNeighbor = hasVerticalNeighbor ? block.vertical : block.horizontal;\n\t\tadapter.isVertical = hasVerticalNeighbor;\n\t\tadapter.neighbor = hasHorizontalNeighbor ? neighbor1 : neighbor2;\n\t\tadapter.facesForward = block.forward.dot(adapter.neighbor.horizontal.plus(adapter.neighbor.vertical)) < 0;\n\t\tadapter.sourceBlock = block;\n\t\t\n\t\tif (!this.smallBlocks.containsKey(block.position.plus(block.forward.times(adapter.facesForward ? 1 : -1)))) {\n\t\t\treturn false;\n\t\t}\n\n\t\tblock.perpendicularRoundedAdapter = adapter;\n\t\treturn true;\n\t}\n\n    private createTinyBlocks() {\n        this.tinyBlocks = new VectorDictionary<TinyBlock>();\n\n        for (let block of this.smallBlocks.values()) {\n            if (block.isAttachment) {\n                continue;\n            }\n\n            let pos = block.position;\n            for (var a = -1; a <= 1; a++) {\n                for (var b = -1; b <= 1; b++) {\n                    for (var c = -1; c <= 1; c++) {\n                        if (this.isSmallBlock(pos.plus(new Vector3(a, 0, 0)))\n                            && this.isSmallBlock(pos.plus(new Vector3(0, b, 0)))\n                            && this.isSmallBlock(pos.plus(new Vector3(0, 0, c)))\n                            && this.isSmallBlock(pos.plus(new Vector3(a, b, c)))\n                            && this.isSmallBlock(pos.plus(new Vector3(a, b, 0)))\n                            && this.isSmallBlock(pos.plus(new Vector3(a, 0, c)))\n                            && this.isSmallBlock(pos.plus(new Vector3(0, b, c)))) {\n                            this.createTinyBlock(pos.times(3).plus(new Vector3(a, b, c)), block);\n                        }\n                    }\n                }\n            }\n        }\n\n        for (let block of this.smallBlocks.values()) {\n            if (!block.isAttachment) {\n                continue;\n            }\n            for (var a = -2; a <= 2; a++) {\n                var neighbor = block.position.plus(block.forward.times(sign(a)));\n                if (!this.smallBlocks.containsKey(neighbor) || (Math.abs(a) >= 2 && this.smallBlocks.get(neighbor).isAttachment)) {\n                    continue;\n                }\n\n                for (var b = -1; b <= 0; b++) {\n                    for (var c = -1; c <= 0; c++) {\n                        this.createTinyBlock(block.position.times(3).plus(block.forward.times(a)).plus(block.horizontal.times(b)).plus(block.vertical.times(c)), block);\n                    }\n                }\n            }\n        }\n    }\n\n    private isTinyBlock(position: Vector3): boolean {\n        return this.tinyBlocks.containsKey(position) && !this.tinyBlocks.get(position).isAttachment;\n\t}\n\t\n\tprivate pushBlock(smallBlock: SmallBlock, forwardFactor: number) {\n\t\tvar nextBlock = this.smallBlocks.getOrNull(smallBlock.position.plus(smallBlock.forward.times(forwardFactor)));\n\t\t\t\n\t\tfor (var a = -2; a <= 2; a++) {\n\t\t\tfor (var b = -2; b <= 2; b++) {\n\t\t\t\tvar from = smallBlock.position.times(3)\n\t\t\t\t\t.plus(smallBlock.right.times(a))\n\t\t\t\t\t.plus(smallBlock.up.times(b))\n\t\t\t\t\t.plus(smallBlock.forward.times(forwardFactor));\n\t\t\t\tvar to = from.plus(smallBlock.forward.times(forwardFactor));\n\t\t\t\tif (!this.tinyBlocks.containsKey(to)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!this.tinyBlocks.containsKey(from)) {\n\t\t\t\t\tthis.tinyBlocks.remove(to);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (smallBlock.orientation == nextBlock.orientation) {\n\t\t\t\t\tif (Math.abs(a) < 2 && Math.abs(b) < 2) {\n\t\t\t\t\t\tthis.tinyBlocks.get(to).rounded = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.createTinyBlock(to, this.tinyBlocks.get(from));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n    private processTinyBlocks() {\n\t\t// Disable interiors when adjacent quadrants are missing\n\t\tfor (var block of this.tinyBlocks.values()) {\n\t\t\tif (block.isCenter\n\t\t\t\t&& !block.isAttachment\n\t\t\t\t&& (block.hasInterior || block.rounded)\n\t\t\t\t&& (!this.isTinyBlock(block.position.minus(block.horizontal.times(3))) || !this.isTinyBlock(block.position.minus(block.vertical.times(3))))) {\n\t\t\t\tfor (var a = -1; a <= 1; a++) {\n\t\t\t\t\tfor (var b = -1; b <= 1; b++) {\n\t\t\t\t\t\tvar position = block.position.plus(block.right.times(a)).plus(block.up.times(b));\n\t\t\t\t\t\tif (this.tinyBlocks.containsKey(position)) {\n\t\t\t\t\t\t\tthis.tinyBlocks.get(position).rounded = false;\n\t\t\t\t\t\t\tthis.tinyBlocks.get(position).hasInterior = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (var smallBlock of this.smallBlocks.values()) {\n\t\t\tvar nextBlock = this.smallBlocks.getOrNull(smallBlock.position.plus(smallBlock.forward));\n\t\t\t// Offset rounded to non rounded transitions to make them flush\n\t\t\tif (smallBlock.rounded && nextBlock != null && !nextBlock.rounded && smallBlock.perpendicularRoundedAdapter == null) {\n\t\t\t\tthis.pushBlock(smallBlock, 1);\n\t\t\t}\n\t\t\tvar previousBlock = this.smallBlocks.getOrNull(smallBlock.position.minus(smallBlock.forward));\n\t\t\t// Offset rounded to non rounded transitions to make them flush\n\t\t\tif (smallBlock.rounded && previousBlock != null && !previousBlock.rounded && smallBlock.perpendicularRoundedAdapter == null) {\n\t\t\t\tthis.pushBlock(smallBlock, -1);\n\t\t\t}\n\n\t\t\tif (smallBlock.rounded && nextBlock != null && nextBlock.rounded && smallBlock.orientation != nextBlock.orientation) {\n\t\t\t\tthis.pushBlock(smallBlock, 1);\n\t\t\t}\n\t\t\t\n\t\t\tif (smallBlock.rounded && previousBlock != null && previousBlock.rounded && smallBlock.orientation != previousBlock.orientation) {\n\t\t\t\tthis.pushBlock(smallBlock, -1);\n\t\t\t}\n\t\t}\n    }\n\n    // Sets HasInterior to false for all tiny blocks that do not form coherent blocks with their neighbors\n\tprivate checkInteriors() {\n\t\tfor (var block of this.tinyBlocks.values()) {\n\t\t\tif (!block.isCenter || !block.hasInterior) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (var a = 0; a <= 1; a++) {\n\t\t\t\tfor (var b = 1 - a; b <= 1; b++) {\n\t\t\t\t\tvar neighborPos = block.position.minus(block.horizontal.times(3 * a)).minus(block.vertical.times(3 * b));\n\t\t\t\t\tif (!this.tinyBlocks.containsKey(neighborPos)) {\n\t\t\t\t\t\tblock.hasInterior = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar neighbor = this.tinyBlocks.get(neighborPos);\n\t\t\t\t\t\tif (block.orientation != neighbor.orientation\n\t\t\t\t\t\t\t|| block.type != neighbor.type\n\t\t\t\t\t\t\t|| neighbor.localX != block.localX - a * block.directionX\n\t\t\t\t\t\t\t|| neighbor.localY != block.localY - b * block.directionY) {\n\t\t\t\t\t\t\tblock.hasInterior = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getPerpendicularRoundedNeighborOrNull(block: TinyBlock): SmallBlock {\n\t\tvar verticalNeighbor = this.smallBlocks.getOrNull(block.smallBlockPosition.plus(block.vertical));\n\t\tvar horizontalNeighbor = this.smallBlocks.getOrNull(block.smallBlockPosition.plus(block.horizontal));\n\t\tvar neighbor = verticalNeighbor != null ? verticalNeighbor : horizontalNeighbor;\n\t\tvar verticalOrHorizontal = verticalNeighbor != null ? block.vertical : block.horizontal;\n\t\tif (neighbor != null && neighbor.rounded && neighbor.forward.dot(verticalOrHorizontal) != 0) {\n\t\t\treturn neighbor;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate getPerpendicularRoundedNeighborOrNull2(block: TinyBlock): SmallBlock {\n\t\tvar smallBlock = this.smallBlocks.get(block.smallBlockPosition);\n\t\tif (smallBlock.perpendicularRoundedAdapter != null) {\n\t\t\treturn smallBlock.perpendicularRoundedAdapter.neighbor;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\t\n\tprivate preventMergingForPerpendicularRoundedBlock(block1: TinyBlock, block2: TinyBlock): boolean {\n\t\tif (!block1.rounded || !block2.rounded || !block1.isCenter) {\n\t\t\treturn false;\n\t\t}\n\t\tvar neighbor1 = this.getPerpendicularRoundedNeighborOrNull(block1);\n\t\tvar neighbor2 = this.getPerpendicularRoundedNeighborOrNull(block2);\n\n\t\tvar inside1 = neighbor1 != null && block1.position.minus(neighbor1.position.times(3)).dot(neighbor1.vertical.plus(neighbor1.horizontal)) <= 0;\n\t\tvar inside2 = neighbor2 != null && block2.position.minus(neighbor2.position.times(3)).dot(neighbor2.vertical.plus(neighbor2.horizontal)) <= 0;\n\t\t\n\t\treturn inside1 != inside2 || (inside1 && inside2 && !neighbor1.position.equals(neighbor2.position));\n\t}\n\n    private mergeSimilarBlocks() {\n        for (var block of this.tinyBlocks.values()) {\n\t\t\tif (block.isExteriorMerged) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar amount = 0;\n\t\t\twhile (true) {\n\t\t\t\tvar pos = block.position.plus(block.forward.times(amount + 1));\n\t\t\t\tif (!this.tinyBlocks.containsKey(pos)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tvar nextBlock = this.tinyBlocks.get(pos);\n\t\t\t\tif (nextBlock.orientation != block.orientation\n\t\t\t\t\t|| nextBlock.quadrant != block.quadrant\n\t\t\t\t\t|| nextBlock.isAttachment != block.isAttachment\n\t\t\t\t\t|| nextBlock.hasInterior != block.hasInterior\n\t\t\t\t\t|| (nextBlock.isAttachment && (nextBlock.type != block.type))\n\t\t\t\t\t|| nextBlock.rounded != block.rounded\n\t\t\t\t\t|| this.isTinyBlock(block.position.plus(block.right)) != this.isTinyBlock(nextBlock.position.plus(block.right))\n\t\t\t\t\t|| this.isTinyBlock(block.position.minus(block.right)) != this.isTinyBlock(nextBlock.position.minus(block.right))\n\t\t\t\t\t|| this.isTinyBlock(block.position.plus(block.up)) != this.isTinyBlock(nextBlock.position.plus(block.up))\n\t\t\t\t\t|| this.isTinyBlock(block.position.minus(block.up)) != this.isTinyBlock(nextBlock.position.minus(block.up))\n\t\t\t\t\t|| this.preventMergingForPerpendicularRoundedBlock(this.tinyBlocks.get(block.position.plus(block.forward.times(amount))), nextBlock)) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tamount += nextBlock.exteriorMergedBlocks;\n\t\t\t\tnextBlock.isExteriorMerged = true;\n\t\t\t\tif (nextBlock.exteriorMergedBlocks != 1) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tblock.exteriorMergedBlocks += amount;\n\t\t}\n\n\t\tfor (var block of this.tinyBlocks.values()) {\n\t\t\tif (block.isInteriorMerged || !block.hasInterior) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar amount = 0;\n\t\t\twhile (true) {\n\t\t\t\tvar pos = block.position.plus(block.forward.times(amount + 1));\n\t\t\t\tif (!this.tinyBlocks.containsKey(pos)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tvar nextBlock = this.tinyBlocks.get(pos);\n\t\t\t\tif (!nextBlock.hasInterior\n\t\t\t\t\t|| nextBlock.orientation != block.orientation\n\t\t\t\t\t|| nextBlock.quadrant != block.quadrant\n\t\t\t\t\t|| nextBlock.type != block.type) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tamount += nextBlock.interiorMergedBlocks;\n\t\t\t\tnextBlock.isInteriorMerged = true;\n\t\t\t\tif (nextBlock.interiorMergedBlocks != 1) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tblock.interiorMergedBlocks += amount;\n\t\t}\n    }\n\n    private isSmallBlock(position: Vector3): boolean {\n        return this.smallBlocks.containsKey(position) && !this.smallBlocks.get(position).isAttachment;\n    }\n\n    private createTinyBlock(position: Vector3, source: SmallBlock) {\n        this.tinyBlocks.set(position, new TinyBlock(position, source));\n    }\n\t\n    private getNextBlock(block: TinyBlock, interior: boolean): TinyBlock {\n\t\tvar mergedAmount = interior ? block.interiorMergedBlocks : block.exteriorMergedBlocks;\n        return this.tinyBlocks.getOrNull(block.position.plus(block.forward.times(mergedAmount)));\n    }\n\n    private getPreviousBlock(block: TinyBlock): TinyBlock {\n        return this.tinyBlocks.getOrNull(block.position.minus(block.forward));\n    }\n\n    private hasOpenEnd(block: TinyBlock, interior: boolean): boolean {\n\t\tvar pos = block.position;\n\t\tvar mergedAmount = interior ? block.interiorMergedBlocks : block.exteriorMergedBlocks;\n        \n        return !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)))\n            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.horizontal.times(3)))\n            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.vertical.times(3)))\n            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.horizontal.times(3)).minus(block.vertical.times(3)));\n    }\n\n    private hasOpenStart(block: TinyBlock): boolean {\n        var pos = block.position;\n        return !this.tinyBlocks.containsKey(pos.minus(block.forward))\n            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.horizontal.times(3)))\n            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.vertical.times(3)))\n            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.horizontal.times(3)).minus(block.vertical.times(3)));\n\t}\n\t\n\tprivate hideStartEndFaces(position: Vector3, block: TinyBlock, forward: boolean) {\n\t\tvar direction = forward ? block.forward : block.forward.times(-1);\n\t\tthis.hideFaceIfExists(position, direction);\n\t\tthis.hideFaceIfExists(position.minus(block.horizontal), direction);\n\t\tthis.hideFaceIfExists(position.minus(block.vertical), direction);\n\t\tthis.hideFaceIfExists(position.minus(block.vertical).minus(block.horizontal), direction);\n\t}\n\n\tprivate hideFaceIfExists(position: Vector3, direction: Vector3) {\n\t\tif (this.tinyBlocks.containsKey(position)) {\n\t\t\tthis.tinyBlocks.get(position).hideFace(direction);\n\t\t}\n\t}\n\n\tprivate hideOutsideFaces(centerBlock: TinyBlock) {\n\t\tvar vertical = centerBlock.vertical;\n\t\tvar horizontal = centerBlock.horizontal;\n\t\tcenterBlock.hideFace(vertical);\n\t\tcenterBlock.hideFace(horizontal);\n\t\tthis.tinyBlocks.get(centerBlock.position.minus(vertical)).hideFace(horizontal);\n\t\tthis.tinyBlocks.get(centerBlock.position.minus(horizontal)).hideFace(vertical);\n\t}\n\t\n\tprivate renderPerpendicularRoundedAdapters() {\n\t\tfor (var block of this.smallBlocks.values()) {\n\t\t\tif (block.perpendicularRoundedAdapter == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\tvar adapter = block.perpendicularRoundedAdapter;\n\t\t\tvar center = block.forward.times(this.tinyIndexToWorld(block.forward.dot(block.position) * 3 - (adapter.facesForward ? 0 : 1)))\n\t\t\t\t.plus(block.right.times((block.position.dot(block.right) + (1 - block.localX)) * 0.5))\n\t\t\t\t.plus(block.up.times((block.position.dot(block.up) + (1 - block.localY)) * 0.5));\n\t\t\tvar radius = 0.5 - this.measurements.edgeMargin;\n\t\t\tvar forward = block.forward;\n\t\t\t\t\t\t\t\t\n\t\t\tfor (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n\t\t\t\tvar angle1 = Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter;\n\t\t\t\tvar angle2 = Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter;\n\t\t\t\tvar sincos1 = 1 - (block.odd() == adapter.isVertical ? Math.sin(angle1) : Math.cos(angle1));\n\t\t\t\tvar sincos2 = 1 - (block.odd() == adapter.isVertical ? Math.sin(angle2) : Math.cos(angle2));\n\t\t\t\t\n\t\t\t\tlet vertex1 = center.plus(block.getOnCircle(angle1).times(radius)).plus(forward.times(adapter.facesForward ? 0 : radius));\n\t\t\t\tlet vertex2 = center.plus(block.getOnCircle(angle2).times(radius)).plus(forward.times(adapter.facesForward ? 0 : radius));\n\t\t\t\tvar vertex3 = vertex2.plus(forward.times(sincos2 * (adapter.facesForward ? 1 : -1) * radius));\n\t\t\t\tvar vertex4 = vertex1.plus(forward.times(sincos1 * (adapter.facesForward ? 1 : -1) * radius));\n\n\t\t\t\tvar normal1 = block.getOnCircle(angle1).times(adapter.facesForward ? 1 : -1);\n\t\t\t\tvar normal2 = block.getOnCircle(angle2).times(adapter.facesForward ? 1 : -1);\n\n\t\t\t\tthis.createQuadWithNormals(\n\t\t\t\t\tvertex1, vertex2, vertex3, vertex4,\n\t\t\t\t\tnormal1, normal2, normal2, normal1, adapter.facesForward);\n\n\t\t\t\tvar invertAngle = ((adapter.isVertical ? block.localY : block.localX) != 1) != adapter.facesForward;\n\t\t\t\tvar vertex5 = vertex4.plus(adapter.directionToNeighbor.times(radius * sincos1));\n\t\t\t\tvar vertex6 = vertex3.plus(adapter.directionToNeighbor.times(radius * sincos2));\n\t\t\t\tvar normal3 = adapter.neighbor.getOnCircle(invertAngle ? angle1 : Math.PI / 2 - angle1).times(adapter.facesForward ? -1 : 1);\n\t\t\t\tvar normal4 = adapter.neighbor.getOnCircle(invertAngle ? angle2 : Math.PI / 2 - angle2).times(adapter.facesForward ? -1 : 1);\n\n\t\t\t\tthis.createQuadWithNormals(\n\t\t\t\t\tvertex5, vertex6, vertex3, vertex4,\n\t\t\t\t\tnormal3, normal4, normal4, normal3, !adapter.facesForward);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate isPerpendicularRoundedAdapter(block: TinyBlock) {\n\t\tif (block.perpendicularRoundedAdapter == null) {\n\t\t\treturn false;\n\t\t}\n\t\tvar localForward = block.position.minus(block.perpendicularRoundedAdapter.sourceBlock.position.times(3)).dot(block.forward);\n\t\treturn localForward == 0 || (localForward > 0) == block.perpendicularRoundedAdapter.facesForward;\n\t}\n\n    private renderRoundedExteriors() {\n\t\tvar blockSizeWithoutMargin = 0.5 - this.measurements.edgeMargin;\n\t\t\n        for (let block of this.tinyBlocks.values()) {\n            if (block.isExteriorMerged || !block.isCenter || block.isAttachment) {\n                continue;\n            }\n\n            var nextBlock = this.getNextBlock(block, false);\n            var previousBlock = this.getPreviousBlock(block);\n            var distance = block.getExteriorDepth(this);\n\n            var hasOpenEnd = this.hasOpenEnd(block, false);\n            var hasOpenStart = this.hasOpenStart(block);\n\n            // Back cap\n            if (nextBlock == null && (block.rounded || block.hasInterior)) {\n\t\t\t\tthis.createCircleWithHole(block, block.hasInterior && hasOpenEnd ? this.measurements.interiorRadius : 0, blockSizeWithoutMargin, distance, false, !block.rounded);\n\t\t\t\tthis.hideStartEndFaces(block.position.plus(block.forward.times(block.exteriorMergedBlocks - 1)), block, true);\n            }\n\n            // Front cap\n            if (previousBlock == null && (block.rounded || block.hasInterior)) {\n\t\t\t\tthis.createCircleWithHole(block, block.hasInterior && hasOpenStart ? this.measurements.interiorRadius : 0, blockSizeWithoutMargin, 0, true, !block.rounded);\n\t\t\t\tthis.hideStartEndFaces(block.position, block, false);\n            }\n\n            if (block.rounded) {\n\t\t\t\tif (!this.isPerpendicularRoundedAdapter(block)) {\n\t\t\t\t\tthis.createCylinder(block, 0, blockSizeWithoutMargin, distance);\n\n\t\t\t\t\t// Rounded to non rounded adapter\n\t\t\t\t\tif (nextBlock != null && !nextBlock.rounded) {\n\t\t\t\t\t\tthis.createCircleWithHole(block, blockSizeWithoutMargin, blockSizeWithoutMargin, distance, true, true);\n\t\t\t\t\t}\n\t\t\t\t\tif (previousBlock != null && !previousBlock.rounded) {\n\t\t\t\t\t\tthis.createCircleWithHole(block, blockSizeWithoutMargin, blockSizeWithoutMargin, 0, false, true);\n\t\t\t\t\t}\n\t\t\t\t}\n                // Rounded corners\n\t\t\t\tfor (var i = 0; i < block.exteriorMergedBlocks; i++) {\n\t\t\t\t\tthis.hideOutsideFaces(this.tinyBlocks.get(block.position.plus(block.forward.times(i))));\n\t\t\t\t}\n            }\n        }\n\t}\n\t\n\tprivate renderInteriors() {\n\t\tfor (let block of this.tinyBlocks.values()) {\n            if (block.isInteriorMerged || !block.isCenter || !block.hasInterior) {\n                continue;\n\t\t\t}\n\t\t\t\n\t\t\tif (block.type == BlockType.PinHole) {\n\t\t\t\tthis.renderPinHoleInterior(block);\n\t\t\t} else if (block.type == BlockType.AxleHole) {\n\t\t\t\tthis.renderAxleHoleInterior(block);\n\t\t\t}\n        }\n\t}\n\n    private renderAttachments() {\n        for (var block of this.tinyBlocks.values()) {\n            if (block.isExteriorMerged || !block.isCenter) {\n                continue;\n            }\n\n            switch (block.type) {\n                case BlockType.Pin:\n                    this.renderPin(block);\n                    break;\n                case BlockType.Axle:\n\t\t\t\t\tthis.renderAxle(block);\n\t\t\t\t\tbreak;\n\t\t\t\tcase BlockType.BallJoint:\n\t\t\t\t\tthis.renderBallJoint(block);\n\t\t\t\t\tbreak;\n            }\n        }\n\t}\n\n\tprivate renderLip(block: TinyBlock, zOffset: number) {\t\t\n\t\tvar center = block.getCylinderOrigin(this).plus(block.forward.times(zOffset));\n\t\t\n\t\tfor (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n\t\t\tvar out1 = block.getOnCircle(i / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);\n\t\t\tvar out2 = block.getOnCircle((i + 1) / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);\n\n\t\t\tfor (var j = 0; j < this.measurements.lipSubdivisions; j++) {\n\t\t\t\tvar angleJ = j * Math.PI / this.measurements.lipSubdivisions;\n\t\t\t\tvar angleJ2 = (j + 1) * Math.PI / this.measurements.lipSubdivisions;\n\t\t\t\tthis.createQuadWithNormals(\n\t\t\t\t\tcenter.plus(out1.times(this.measurements.pinRadius)).plus(out1.times(Math.sin(angleJ) * this.measurements.pinLipRadius).plus(block.forward.times(Math.cos(angleJ) * this.measurements.pinLipRadius))),\n\t\t\t\t\tcenter.plus(out2.times(this.measurements.pinRadius)).plus(out2.times(Math.sin(angleJ) * this.measurements.pinLipRadius).plus(block.forward.times(Math.cos(angleJ) * this.measurements.pinLipRadius))),\n\t\t\t\t\tcenter.plus(out2.times(this.measurements.pinRadius)).plus(out2.times(Math.sin(angleJ2) * this.measurements.pinLipRadius).plus(block.forward.times(Math.cos(angleJ2) * this.measurements.pinLipRadius))),\n\t\t\t\t\tcenter.plus(out1.times(this.measurements.pinRadius)).plus(out1.times(Math.sin(angleJ2) * this.measurements.pinLipRadius).plus(block.forward.times(Math.cos(angleJ2) * this.measurements.pinLipRadius))),\n\t\t\t\t\tout1.times(-Math.sin(angleJ)).plus(block.forward.times(-Math.cos(angleJ))),\n\t\t\t\t\tout2.times(-Math.sin(angleJ)).plus(block.forward.times(-Math.cos(angleJ))),\n\t\t\t\t\tout2.times(-Math.sin(angleJ2)).plus(block.forward.times(-Math.cos(angleJ2))),\n\t\t\t\t\tout1.times(-Math.sin(angleJ2)).plus(block.forward.times(-Math.cos(angleJ2))));\n\t\t\t}\n\t\t}\n\t}\n\n    private renderPin(block: TinyBlock) {\n\t\tvar nextBlock = this.getNextBlock(block, false);\n\t\tvar previousBlock = this.getPreviousBlock(block);\n\n\t\tvar distance = block.getExteriorDepth(this);\n\n\t\tvar startOffset = (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Pin) ? this.measurements.attachmentAdapterSize : 0;\n\t\tif (previousBlock == null) {\n\t\t\tstartOffset += 2 * this.measurements.pinLipRadius;\n\t\t}\n\t\tvar endOffset = (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Pin) ? this.measurements.attachmentAdapterSize : 0;\n\t\tif (nextBlock == null) {\n\t\t\tendOffset += 2 * this.measurements.pinLipRadius;\n\t\t}\n\n\t\tthis.createCylinder(block, startOffset, this.measurements.pinRadius, distance - startOffset - endOffset);\n\n\t\tif (nextBlock == null) {\n\t\t\tthis.createCircle(block, this.measurements.pinRadius, distance, true);\n\t\t\tthis.renderLip(block, distance - this.measurements.pinLipRadius);\n\t\t}\n\t\tif (previousBlock == null) {\n\t\t\tthis.createCircle(block, this.measurements.pinRadius, 0);\n\t\t\tthis.renderLip(block, this.measurements.pinLipRadius);\n\t\t}\n\t\tif (nextBlock != null && !nextBlock.isAttachment) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinRadius, 0.5 - this.measurements.edgeMargin, distance, true, !nextBlock.rounded);\n\t\t\tthis.hideStartEndFaces(nextBlock.position, block, false);\n\t\t}\n\t\tif (previousBlock != null && !previousBlock.isAttachment) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinRadius, 0.5 - this.measurements.edgeMargin, 0, false, !previousBlock.rounded);\n\t\t\tthis.hideStartEndFaces(previousBlock.position, block, true);\n\t\t}\n\t\tif (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Pin) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinRadius, this.measurements.attachmentAdapterRadius, distance - this.measurements.attachmentAdapterSize, true);\n\t\t}\n\t\tif (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Pin) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinRadius, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize);\n\t\t\tthis.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);\n\t\t}\n\t}\n\n    private renderAxle(block: TinyBlock) {\n\t\tvar nextBlock = this.getNextBlock(block, false);\n\t\tvar previousBlock = this.getPreviousBlock(block);\n\n\t\tvar start = block.getCylinderOrigin(this);\n\t\tvar end = start.plus(block.forward.times(block.getExteriorDepth(this)));\n\n\t\tif (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Axle) {\n\t\t\tstart = start.plus(block.forward.times(this.measurements.attachmentAdapterSize));\n\t\t}\n\t\tif (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Axle) {\n\t\t\tend = end.minus(block.forward.times(this.measurements.attachmentAdapterSize));\n\t\t}\n\n\t\tvar horizontalInner = block.horizontal.times(this.measurements.axleSizeInner);\n\t\tvar horizontalOuter = block.horizontal.times(this.measurements.axleSizeOuter);\n\t\tvar verticalInner = block.vertical.times(this.measurements.axleSizeInner);\n\t\tvar verticalOuter = block.vertical.times(this.measurements.axleSizeOuter);\n\n\t\tvar odd = block.odd();\n\t\tthis.createQuad(\n            start.plus(horizontalInner).plus(verticalInner),\n            start.plus(horizontalInner).plus(verticalOuter),\n            end.plus(horizontalInner).plus(verticalOuter),\n            end.plus(horizontalInner).plus(verticalInner), odd);\n\t\tthis.createQuad(\n\t\t\tstart.plus(horizontalInner).plus(verticalInner),\n\t\t\tstart.plus(horizontalOuter).plus(verticalInner),\n\t\t\tend.plus(horizontalOuter).plus(verticalInner),\n\t\t\tend.plus(horizontalInner).plus(verticalInner), !odd);\n\t\tthis.createQuad(\n\t\t\tend.plus(horizontalOuter),\n\t\t\tstart.plus(horizontalOuter),\n\t\t\tstart.plus(horizontalOuter).plus(verticalInner),\n\t\t\tend.plus(horizontalOuter).plus(verticalInner), odd);\n\t\tthis.createQuad(\n\t\t\tend.plus(verticalOuter),\n\t\t\tstart.plus(verticalOuter),\n\t\t\tstart.plus(verticalOuter).plus(horizontalInner),\n\t\t\tend.plus(verticalOuter).plus(horizontalInner), !odd);\n\n\t\tif (nextBlock == null) {\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(horizontalInner).plus(verticalInner),\n\t\t\t\tend.plus(verticalInner),\n\t\t\t\tend,\n\t\t\t\tend.plus(horizontalInner), odd);\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(horizontalInner),\n\t\t\t\tend.plus(horizontalOuter),\n\t\t\t\tend.plus(horizontalOuter).plus(verticalInner),\n\t\t\t\tend.plus(horizontalInner).plus(verticalInner), odd);\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(verticalInner),\n\t\t\t\tend.plus(verticalOuter),\n\t\t\t\tend.plus(verticalOuter).plus(horizontalInner),\n\t\t\t\tend.plus(verticalInner).plus(horizontalInner), !odd);\n\t\t}\n\t\tif (previousBlock == null) {\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(horizontalInner).plus(verticalInner),\n\t\t\t\tstart.plus(verticalInner),\n\t\t\t\tstart,\n\t\t\t\tstart.plus(horizontalInner), !odd);\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(horizontalInner),\n\t\t\t\tstart.plus(horizontalOuter),\n\t\t\t\tstart.plus(horizontalOuter).plus(verticalInner),\n\t\t\t\tstart.plus(horizontalInner).plus(verticalInner), !odd);\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(verticalInner),\n\t\t\t\tstart.plus(verticalOuter),\n\t\t\t\tstart.plus(verticalOuter).plus(horizontalInner),\n\t\t\t\tstart.plus(verticalInner).plus(horizontalInner), odd);\n\t\t}\n\n\t\tvar blockSizeWithoutMargin = 0.5 - this.measurements.edgeMargin;\n\t\tif (nextBlock != null && nextBlock.type != block.type && !nextBlock.rounded) {\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(block.horizontal.times(blockSizeWithoutMargin)),\n\t\t\t\tend.plus(horizontalOuter),\n\t\t\t\tend.plus(horizontalOuter).plus(verticalInner),\n\t\t\t\tend.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner), odd);\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(block.vertical.times(blockSizeWithoutMargin)),\n\t\t\t\tend.plus(verticalOuter),\n\t\t\t\tend.plus(verticalOuter).plus(horizontalInner),\n\t\t\t\tend.plus(block.vertical.times(blockSizeWithoutMargin)).plus(horizontalInner), !odd);\n\t\t\tthis.createQuad(\n\t\t\t\tend.plus(horizontalInner).plus(verticalInner),\n\t\t\t\tend.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner),\n\t\t\t\tend.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(block.vertical.times(blockSizeWithoutMargin)),\n\t\t\t\tend.plus(horizontalInner).plus(block.vertical.times(blockSizeWithoutMargin)), !odd);\n\t\t}\n\t\tif (previousBlock != null && previousBlock.type != block.type && !previousBlock.rounded) {\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(block.horizontal.times(blockSizeWithoutMargin)),\n\t\t\t\tstart.plus(horizontalOuter),\n\t\t\t\tstart.plus(horizontalOuter).plus(verticalInner),\n\t\t\t\tstart.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner), !odd);\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(block.vertical.times(blockSizeWithoutMargin)),\n\t\t\t\tstart.plus(verticalOuter),\n\t\t\t\tstart.plus(verticalOuter).plus(horizontalInner),\n\t\t\t\tstart.plus(block.vertical.times(blockSizeWithoutMargin)).plus(horizontalInner), odd);\n\t\t\tthis.createQuad(\n\t\t\t\tstart.plus(horizontalInner).plus(verticalInner),\n\t\t\t\tstart.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner),\n\t\t\t\tstart.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(block.vertical.times(blockSizeWithoutMargin)),\n\t\t\t\tstart.plus(horizontalInner).plus(block.vertical.times(blockSizeWithoutMargin)), odd);\n\t\t}\n\t\tif (nextBlock != null && nextBlock.type != block.type && nextBlock.rounded) {\n\t\t\tthis.createAxleToCircleAdapter(end, block, nextBlock.isAttachment ? this.measurements.attachmentAdapterRadius : blockSizeWithoutMargin);\n\t\t}\n\t\tif (previousBlock != null && previousBlock.type != block.type && previousBlock.rounded) {\n\t\t\tthis.createAxleToCircleAdapter(start, block, previousBlock.isAttachment ? this.measurements.attachmentAdapterRadius : blockSizeWithoutMargin, true);\n\t\t}\n\t\tif (nextBlock != null && !nextBlock.isAttachment) {\n\t\t\tthis.hideStartEndFaces(nextBlock.position, block, false);\n\t\t}\n\t\tif (previousBlock != null && !previousBlock.isAttachment) {\n\t\t\tthis.hideStartEndFaces(previousBlock.position, block, true);\n\t\t}\n\t\t\n\t\tif (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Axle) {\n\t\t\tthis.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);\n\t\t}\n    }\n\t\n\tprivate renderBallJoint(block: TinyBlock) {\n\t\tvar nextBlock = this.getNextBlock(block, false);\n\t\tvar previousBlock = this.getPreviousBlock(block);\n\n\t\tvar distance = block.getExteriorDepth(this);\n\n\t\tvar startOffset = (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.BallJoint) ? this.measurements.attachmentAdapterSize : 0;\n\t\tif (previousBlock == null) {\n\t\t\tstartOffset += 2 * this.measurements.pinLipRadius;\n\t\t}\n\t\tvar endOffset = (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.BallJoint) ? this.measurements.attachmentAdapterSize : 0;\n\t\tif (nextBlock == null) {\n\t\t\tendOffset += 2 * this.measurements.pinLipRadius;\n\t\t}\n\t\t\n\t\tvar ballCenterDistance: number;\n\t\tif (nextBlock == null) {\n\t\t\tvar offset = mod(block.position.dot(block.forward) - 1, 3) - 1;\n\t\t\tballCenterDistance = 0.5 - offset * this.measurements.edgeMargin;\n\t\t} else {\n\t\t\tvar offset = mod(block.position.dot(block.forward) + block.exteriorMergedBlocks - 1, 3) - 1;\n\t\t\tballCenterDistance = distance - 0.5 - offset * this.measurements.edgeMargin;\n\t\t}\n\n\t\tvar ballCenter = block.getCylinderOrigin(this).plus(block.forward.times(ballCenterDistance));\n\t\tvar angle = Math.acos(this.measurements.ballBaseRadius / this.measurements.ballRadius);\n\n\t\tfor (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n\t\t\tvar angleStart = lerp(-angle, +angle, i / this.measurements.subdivisionsPerQuarter);\n\t\t\tvar angleEnd = lerp(-angle, +angle, (i+1) / this.measurements.subdivisionsPerQuarter);\n\n\t\t\tvar ballCenterStart = ballCenter.plus(block.forward.times(Math.sin(angleStart) * this.measurements.ballRadius));\n\t\t\tvar ballCenterEnd = ballCenter.plus(block.forward.times(Math.sin(angleEnd) * this.measurements.ballRadius));\n\t\t\tvar radiusStart = this.measurements.ballRadius * Math.cos(angleStart);\n\t\t\tvar radiusEnd = this.measurements.ballRadius * Math.cos(angleEnd);\n\n\t\t\tfor (var j = 0; j < this.measurements.subdivisionsPerQuarter; j++) {\n\t\t\t\tvar out1 = block.getOnCircle(j / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);\n\t\t\t\tvar out2 = block.getOnCircle((j + 1) / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);\n\t\n\t\t\t\tthis.createQuadWithNormals(\n\t\t\t\t\tballCenterStart.plus(out2.times(radiusStart)),\n\t\t\t\t\tballCenterStart.plus(out1.times(radiusStart)),\n\t\t\t\t\tballCenterEnd.plus(out1.times(radiusEnd)),\n\t\t\t\t\tballCenterEnd.plus(out2.times(radiusEnd)),\n\t\t\t\t\tout2.times(-Math.cos(angleStart)).minus(block.forward.times(Math.sin(angleStart))),\n\t\t\t\t\tout1.times(-Math.cos(angleStart)).minus(block.forward.times(Math.sin(angleStart))),\n\t\t\t\t\tout1.times(-Math.cos(angleEnd)).minus(block.forward.times(Math.sin(angleEnd))),\n\t\t\t\t\tout2.times(-Math.cos(angleEnd)).minus(block.forward.times(Math.sin(angleEnd)))\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\t\n\t\tvar ballStart = ballCenterDistance - Math.sin(angle) * this.measurements.ballRadius;\n\t\tvar ballEnd = ballCenterDistance + Math.sin(angle) * this.measurements.ballRadius;\n\n\t\tif (nextBlock == null) {\n\t\t\tthis.createCircle(block, this.measurements.ballBaseRadius, ballEnd, true);\n\t\t} else {\n\t\t\tthis.createCylinder(block, ballEnd, this.measurements.ballBaseRadius, distance - endOffset - ballEnd);\n\t\t}\n\n\t\tif (previousBlock == null) {\n\t\t\tthis.createCircle(block, this.measurements.ballBaseRadius, ballStart);\n\t\t} else {\n\t\t\tthis.createCylinder(block, startOffset, this.measurements.ballBaseRadius, ballStart - startOffset);\n\t\t}\n\t\t\n\t\tif (nextBlock != null && !nextBlock.isAttachment) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.ballBaseRadius, 0.5 - this.measurements.edgeMargin, distance, true, !nextBlock.rounded);\n\t\t\tthis.hideStartEndFaces(nextBlock.position, block, false);\n\t\t}\n\t\tif (previousBlock != null && !previousBlock.isAttachment) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.ballBaseRadius, 0.5 - this.measurements.edgeMargin, 0, false, !previousBlock.rounded);\n\t\t\tthis.hideStartEndFaces(previousBlock.position, block, true);\n\t\t}\n\t\tif (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.BallJoint) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.ballBaseRadius, this.measurements.attachmentAdapterRadius, distance - this.measurements.attachmentAdapterSize, true);\n\t\t}\n\t\tif (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.BallJoint) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.ballBaseRadius, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize);\n\t\t\tthis.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);\n\t\t}\n    }\n\t\n\tprivate createAxleToCircleAdapter(center: Vector3, block: SmallBlock, radius: number, flipped = false) {\n\t\tvar horizontalInner = block.horizontal.times(this.measurements.axleSizeInner);\n\t\tvar horizontalOuter = block.horizontal.times(this.measurements.axleSizeOuter);\n\t\tvar verticalInner = block.vertical.times(this.measurements.axleSizeInner);\n\t\tvar verticalOuter = block.vertical.times(this.measurements.axleSizeOuter);\n\t\tvar odd = block.odd();\n\n\t\tfor (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n\t\t\tvar focus = center.copy();\n\t\t\tif (i < this.measurements.subdivisionsPerQuarter / 2 == !odd) {\n\t\t\t\tfocus = focus.plus(horizontalInner).plus(verticalOuter);\n\t\t\t} else {\n\t\t\t\tfocus = focus.plus(horizontalOuter).plus(verticalInner);\n\t\t\t}\n\n\t\t\tthis.triangles.push(new Triangle(focus,\n\t\t\t\tcenter.plus(block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter, radius)),\n\t\t\t\tcenter.plus(block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter, radius)), flipped));\n\t\t}\n\t\tthis.triangles.push(new Triangle(\n\t\t\tcenter.plus(horizontalInner).plus(verticalOuter),\n\t\t\tcenter.plus(verticalOuter),\n\t\t\tcenter.plus(block.vertical.times(radius)), odd != flipped));\n\t\tthis.triangles.push(new Triangle(\n\t\t\tcenter.plus(verticalInner).plus(horizontalOuter),\n\t\t\tcenter.plus(horizontalOuter),\n\t\t\tcenter.plus(block.horizontal.times(radius)), odd == flipped));\n\t\tthis.createQuad(\n\t\t\tcenter.plus(verticalInner).plus(horizontalInner),\n\t\t\tcenter.plus(verticalOuter).plus(horizontalInner),\n\t\t\tcenter.plus(block.getOnCircle(45 * DEG_TO_RAD, radius)),\n\t\t\tcenter.plus(verticalInner).plus(horizontalOuter), odd != flipped);\n\t}\n\n    private showInteriorCap(currentBlock: SmallBlock, neighbor: SmallBlock): boolean {\n        if (neighbor == null) {\n            return false;\n        }\n        if (neighbor.orientation != currentBlock.orientation\n            || neighbor.quadrant != currentBlock.quadrant\n            || !neighbor.hasInterior) {\n            return true;\n        }\n        \n        if (currentBlock.type == BlockType.AxleHole && neighbor.type == BlockType.PinHole\n            || neighbor.type == BlockType.AxleHole && currentBlock.type == BlockType.PinHole) {\n            // Pin hole to axle hole adapter\n            return false;\n        }\n\n        return currentBlock.type != neighbor.type;\n    }\n\n    private renderPinHoleInterior(block: TinyBlock) {\n\t\tvar nextBlock = this.getNextBlock(block, true);\n        var previousBlock = this.getPreviousBlock(block);\n        var distance = block.getInteriorDepth(this);\n\n        var hasOpenEnd = this.hasOpenEnd(block, true);\n        var hasOpenStart = this.hasOpenStart(block);\n        var showInteriorEndCap = this.showInteriorCap(block, nextBlock) || (nextBlock == null && !hasOpenEnd);\n\t\tvar showInteriorStartCap = this.showInteriorCap(block, previousBlock) || (previousBlock == null && !hasOpenStart);\n\t\t\n\t\tvar offset = this.measurements.pinHoleOffset;\n\t\tvar endMargin = showInteriorEndCap ? this.measurements.interiorEndMargin : 0;\n\t\tvar startMargin = showInteriorStartCap ? this.measurements.interiorEndMargin : 0;\n        var offsetStart = (hasOpenStart || showInteriorStartCap ? offset : 0) + startMargin;\n\t\tvar offsetEnd = (hasOpenEnd || showInteriorEndCap ? offset : 0) + endMargin;\n\t\tvar interiorRadius = this.measurements.interiorRadius;\n\n\t\tthis.createCylinder(block, offsetStart, this.measurements.pinHoleRadius, distance - offsetStart - offsetEnd, true);\n\n        if (hasOpenStart || showInteriorStartCap) {\n            this.createCylinder(block, startMargin, interiorRadius, offset, true);\n            this.createCircleWithHole(block, this.measurements.pinHoleRadius, interiorRadius, offset + startMargin, true);\n        }\n\n        if (hasOpenEnd || showInteriorEndCap) {\n            this.createCylinder(block, distance - offset - endMargin, interiorRadius, offset, true);\n            this.createCircleWithHole(block, this.measurements.pinHoleRadius, interiorRadius, distance - offset - endMargin, false);\n        }\n\n        if (showInteriorEndCap) {\n            this.createCircle(block, interiorRadius, distance - endMargin, false);\n        }\n        if (showInteriorStartCap) {\n            this.createCircle(block, interiorRadius, startMargin, true);\n        }\n    }\n\n    private renderAxleHoleInterior(block: TinyBlock) {\n        var nextBlock = this.getNextBlock(block, true);\n        var previousBlock = this.getPreviousBlock(block);\n\n        var hasOpenEnd = this.hasOpenEnd(block, true);\n        var hasOpenStart = this.hasOpenStart(block);\n        var showInteriorEndCap = this.showInteriorCap(block, nextBlock) || (nextBlock == null && !hasOpenEnd);\n        var showInteriorStartCap = this.showInteriorCap(block, previousBlock) || (previousBlock == null && !hasOpenStart);\n        \n\t\tvar distance = block.getInteriorDepth(this);\n\t\tvar holeSize = this.measurements.axleHoleSize;\n        \n        var start = block.getCylinderOrigin(this).plus(showInteriorStartCap ? block.forward.times(this.measurements.interiorEndMargin) : Vector3.zero());\n        var end = start.plus(block.forward.times(distance - (showInteriorStartCap ? this.measurements.interiorEndMargin : 0) - (showInteriorEndCap ? this.measurements.interiorEndMargin : 0)));\n\t\t\n\t\tvar axleWingAngle = Math.asin(holeSize / this.measurements.pinHoleRadius);\n\t\tvar axleWingAngle2 = 90 * DEG_TO_RAD - axleWingAngle;\n\t\tvar subdivAngle = 90 / this.measurements.subdivisionsPerQuarter * DEG_TO_RAD;\n\t\tvar adjustedRadius = this.measurements.pinHoleRadius * Math.cos(subdivAngle / 2) / Math.cos(subdivAngle / 2 - (axleWingAngle - Math.floor(axleWingAngle / subdivAngle) * subdivAngle));\n\t\tthis.createQuad(\n\t\t\tstart.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\tstart.plus(block.getOnCircle(axleWingAngle, adjustedRadius)),\n\t\t\tend.plus(block.getOnCircle(axleWingAngle, adjustedRadius)),\n\t\t\tend.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\ttrue);\n\t\tthis.createQuad(\n\t\t\tstart.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\tstart.plus(block.getOnCircle(axleWingAngle2, adjustedRadius)),\n\t\t\tend.plus(block.getOnCircle(axleWingAngle2, adjustedRadius)),\n\t\t\tend.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\tfalse);\n\n\t\tfor (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {\n\t\t\tvar angle1 = lerp(0, 90, i / this.measurements.subdivisionsPerQuarter) * DEG_TO_RAD;\n\t\t\tvar angle2 = lerp(0, 90, (i + 1) / this.measurements.subdivisionsPerQuarter) * DEG_TO_RAD;\n\t\t\tvar startAngleInside = angle1;\n\t\t\tvar endAngleInside = angle2;\n\t\t\tvar startAngleOutside = angle1;\n\t\t\tvar endAngleOutside = angle2;\n\t\t\tvar radius1Inside = this.measurements.pinHoleRadius;\n\t\t\tvar radius2Inside = this.measurements.pinHoleRadius;\n\t\t\tvar radius1Outside = this.measurements.pinHoleRadius;\n\t\t\tvar radius2Outside = this.measurements.pinHoleRadius;\n\t\t\tif (angle1 < axleWingAngle && angle2 > axleWingAngle) {\n\t\t\t\tendAngleInside = axleWingAngle;\n\t\t\t\tstartAngleOutside = axleWingAngle;\n\t\t\t\tradius1Outside = adjustedRadius;\n\t\t\t\tradius2Inside = adjustedRadius;\n\t\t\t}\n\t\t\tif (angle1 < axleWingAngle2 && angle2 > axleWingAngle2) {\n\t\t\t\tstartAngleInside = axleWingAngle2;\n\t\t\t\tendAngleOutside = axleWingAngle2;\n\t\t\t\tradius2Outside = adjustedRadius;\n\t\t\t\tradius1Inside = adjustedRadius;\n\t\t\t}\n\n\t\t\t// Walls\n\t\t\tif (angle1 < axleWingAngle || angle2 > axleWingAngle2) {\n\t\t\t\tvar v1 = block.getOnCircle(startAngleInside);\n\t\t\t\tvar v2 = block.getOnCircle(endAngleInside);\n\t\t\t\tthis.createQuadWithNormals(\n\t\t\t\t\tstart.plus(v1.times(radius1Inside)),\n\t\t\t\t\tstart.plus(v2.times(radius2Inside)),\n\t\t\t\t\tend.plus(v2.times(radius2Inside)),\n                    end.plus(v1.times(radius1Inside)),\n\t\t\t\t\tv1, v2, v2, v1, false);\n\t\t\t}\n\n\t\t\t// Outside caps\n\t\t\tif (hasOpenStart || (previousBlock != null && previousBlock.type == BlockType.PinHole && !showInteriorStartCap)) {\n\t\t\t\tif (angle2 > axleWingAngle && angle1 < axleWingAngle2) {\n\t\t\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\t\t\tstart.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\t\t\tstart.plus(block.getOnCircle(startAngleOutside, radius1Outside)),\n\t\t\t\t\t\tstart.plus(block.getOnCircle(endAngleOutside, radius2Outside))));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (hasOpenEnd || (nextBlock != null && nextBlock.type == BlockType.PinHole && !showInteriorEndCap)) {\n\t\t\t\tif (angle2 > axleWingAngle && angle1 < axleWingAngle2) {\n\t\t\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\t\t\tend.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\t\t\tend.plus(block.getOnCircle(endAngleOutside, radius2Outside)),\n\t\t\t\t\t\tend.plus(block.getOnCircle(startAngleOutside, radius1Outside))));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Inside caps\n\t\t\tif (showInteriorEndCap && (angle1 < axleWingAngle || angle2 > axleWingAngle2)) {\n\t\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\t\tend,\n\t\t\t\t\tend.plus(block.getOnCircle(startAngleInside, radius1Outside)),\n\t\t\t\t\tend.plus(block.getOnCircle(endAngleInside, radius2Outside))));\n\t\t\t}\n\t\t\tif (showInteriorStartCap && (angle1 < axleWingAngle || angle2 > axleWingAngle2)) {\n\t\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\t\tstart,\n\t\t\t\t\tstart.plus(block.getOnCircle(endAngleInside, radius2Outside)),\n\t\t\t\t\tstart.plus(block.getOnCircle(startAngleInside, radius1Outside))));\n\t\t\t}\n\t\t}\n\t\tif (hasOpenEnd) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinHoleRadius, this.measurements.interiorRadius, distance, false);\n\t\t}\n\n\t\tif (hasOpenStart) {\n\t\t\tthis.createCircleWithHole(block, this.measurements.pinHoleRadius, this.measurements.interiorRadius, 0, true);\n\t\t}\n\n\t\tif (showInteriorEndCap) {\n\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\tend.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\tend,\n\t\t\t\tend.plus(block.getOnCircle(axleWingAngle, adjustedRadius))));\n\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\tend,\n\t\t\t\tend.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\tend.plus(block.getOnCircle(axleWingAngle2, adjustedRadius))));\n\t\t}\n\t\tif (showInteriorStartCap) {\n\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\tstart,\n\t\t\t\tstart.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\tstart.plus(block.getOnCircle(axleWingAngle, adjustedRadius))));\n\t\t\tthis.triangles.push(new Triangle(\n\t\t\t\tstart.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),\n\t\t\t\tstart,\n\t\t\t\tstart.plus(block.getOnCircle(axleWingAngle2, adjustedRadius))));\n\t\t}\n\t}\n\n    private isFaceVisible(position: Vector3, direction: Vector3): boolean {\n\t\tvar block = this.tinyBlocks.getOrNull(position);\n\t\treturn block != null\n\t\t\t&& !this.isTinyBlock(block.position.plus(direction))\n\t\t\t&& !block.isAttachment\n\t\t\t&& block.isFaceVisible(direction);\n\t}\n\n    private createTinyFace(position: Vector3, size: Vector3, direction: Vector3) {\n\t\tvar vertices: Vector3[] = null;\n\n\t\tif (direction.x > 0) {\n\t\t\tvertices = RIGHT_FACE_VERTICES;\n\t\t} else if (direction.x < 0) {\n\t\t\tvertices = LEFT_FACE_VERTICES;\n\t\t} else if (direction.y > 0) {\n\t\t\tvertices = UP_FACE_VERTICES;\n\t\t} else if (direction.y < 0) {\n\t\t\tvertices = DOWN_FACE_VERTICES;\n\t\t} else if (direction.z > 0) {\n\t\t\tvertices = FORWARD_FACE_VERTICES;\n\t\t} else if (direction.z < 0) {\n\t\t\tvertices = BACK_FACE_VERTICES;\n\t\t} else {\n\t\t\tthrow new Error(\"Invalid direction: \" + direction.toString());\n\t\t}\n        \n        this.createQuad(\n            this.tinyBlockToWorld(position.plus(vertices[0].elementwiseMultiply(size))),\n            this.tinyBlockToWorld(position.plus(vertices[1].elementwiseMultiply(size))),\n            this.tinyBlockToWorld(position.plus(vertices[2].elementwiseMultiply(size))),\n            this.tinyBlockToWorld(position.plus(vertices[3].elementwiseMultiply(size))));\n\t}\n\n\tprivate isRowOfVisibleFaces(position: Vector3, rowDirection: Vector3, faceDirection: Vector3, count: number): boolean {\n\t\tfor (var i = 0; i < count; i++) {\n\t\t\tif (!this.isFaceVisible(position.plus(rowDirection.times(i)), faceDirection)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/* Finds a connected rectangle of visible faces in the given direction by starting with\n\tthe supplied position and a rectangle of size 1x1 and expanding it in the 4 directions\n\tthat are tangential to the supplied face direction, until it is no longer possible to\n\texpand in any direction. \n\tReturns the lower left corner of the rectangle and its size.\n\tThe component of the size vector of the direction supplied by the direction parameter is\n\talways 1. The component of the position vector in the direction supplied by the direction\n\tparameter remains unchanged. */\n\tprivate findConnectedFaces(position: Vector3, direction: Vector3): [Vector3, Vector3] {\n\t\tvar tangent1 = new Vector3(direction.x == 0 ? 1 : 0, direction.x == 0 ? 0 : 1, 0);\n\t\tvar tangent2 = new Vector3(0, direction.z == 0 ? 0 : 1, direction.z == 0 ? 1 : 0);\n\n\t\tvar size = Vector3.one();\n\t\twhile (true) {\n\t\t\tvar hasChanged = false;\n\n\t\t\tif (this.isRowOfVisibleFaces(position.minus(tangent2), tangent1, direction, size.dot(tangent1))) {\n\t\t\t\tposition = position.minus(tangent2);\n\t\t\t\tsize = size.plus(tangent2);\n\t\t\t\thasChanged = true;\n\t\t\t}\n\t\t\tif (this.isRowOfVisibleFaces(position.minus(tangent1), tangent2, direction, size.dot(tangent2))) {\n\t\t\t\tposition = position.minus(tangent1);\n\t\t\t\tsize = size.plus(tangent1);\n\t\t\t\thasChanged = true;\n\t\t\t}\n\t\t\tif (this.isRowOfVisibleFaces(position.plus(tangent2.times(size.dot(tangent2))), tangent1, direction, size.dot(tangent1))) {\n\t\t\t\tsize = size.plus(tangent2);\n\t\t\t\thasChanged = true;\n\t\t\t}\n\t\t\tif (this.isRowOfVisibleFaces(position.plus(tangent1.times(size.dot(tangent1))), tangent2, direction, size.dot(tangent2))) {\n\t\t\t\tsize = size.plus(tangent1);\n\t\t\t\thasChanged = true;\n\t\t\t}\n\n\t\t\tif (!hasChanged) {\n\t\t\t\treturn [position, size];\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hideFaces(position: Vector3, size: Vector3, direction: Vector3) {\n\t\tfor (var x = 0; x < size.x; x++) {\n\t\t\tfor (var y = 0; y < size.y; y++) {\n\t\t\t\tfor (var z = 0; z < size.z; z++) {\n\t\t\t\t\tthis.hideFaceIfExists(new Vector3(position.x + x, position.y + y, position.z + z), direction);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n    private renderTinyBlockFaces() {\n        for (let block of this.tinyBlocks.values()) {\n\t\t\tfor (let direction of FACE_DIRECTIONS) {\n\t\t\t\tif (!this.isFaceVisible(block.position, direction)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tvar expanded = this.findConnectedFaces(block.position, direction);\n\t\t\t\tvar position = expanded[0];\n\t\t\t\tvar size = expanded[1];\n\t\t\t\tthis.createTinyFace(position, size, direction);\n\t\t\t\tthis.hideFaces(position, size, direction);\n\t\t\t}\n\t\t}\n    }\n}"
  },
  {
    "path": "src/editor/Catalog.ts",
    "content": "class Catalog {\n\tprivate container: HTMLElement;\n\n\tprivate initialized: boolean = false;\n\tpublic items: CatalogItem[];\n\n\tconstructor() {\n\t\tthis.container = document.getElementById(\"catalog\");\n\t\tthis.createCatalogItems();\n\t\tdocument.getElementById(\"catalog\").addEventListener(\"toggle\", (event: MouseEvent) => this.onToggleCatalog(event));\n\t}\n\n\tprivate onToggleCatalog(event: MouseEvent) {\n\t\tif ((event.srcElement as HTMLDetailsElement).open && !this.initialized) {\n\t\t\tthis.createCatalogUI();\n\t\t}\n\t}\n\n\tprivate createCatalogUI() {\n\t\tvar oldRenderingContext = gl;\n\t\tvar canvas = document.createElement(\"canvas\");\n\t\tcanvas.style.height = \"64px\";\n\t\tcanvas.style.width = \"64px\";\n\t\tthis.container.appendChild(canvas);\n\n\t\tvar camera = new Camera(canvas, 2);\n\t\tcamera.clearColor = new Vector3(0.859, 0.859, 0.859);\n\t\tvar partRenderer = new MeshRenderer();\n\t\tpartRenderer.color = new Vector3(0.67, 0.7, 0.71);\n\t\tvar partNormalDepthRenderer = new NormalDepthRenderer();\n\t\tcamera.renderers.push(partRenderer);\n\t\tcamera.renderers.push(partNormalDepthRenderer);\n\t\tcamera.renderers.push(new ContourPostEffect());\n\t\tvar measurements = new Measurements();\n\t\t\n\t\tfor (var item of this.items) {\n\t\t\tvar catalogLink: HTMLAnchorElement = document.createElement(\"a\");\n\t\t\tcatalogLink.className = \"catalogItem\";\n\t\t\tcatalogLink.href = \"?part=\" + item.string + \"&name=\" + encodeURIComponent(item.name);\n\t\t\tcatalogLink.title = item.name;\n\t\t\tthis.container.appendChild(catalogLink);\n\t\t\tvar itemCanvas = document.createElement(\"canvas\");\n\t\t\tcatalogLink.appendChild(itemCanvas);\n\t\t\titemCanvas.style.height = \"64px\";\n\t\t\titemCanvas.style.width = \"64px\";\n\t\t\tvar mesh = new PartMeshGenerator(item.part, measurements).getMesh();\n\t\t\tpartRenderer.setMesh(mesh);\n\t\t\tpartNormalDepthRenderer.setMesh(mesh);\n\t\t\tcamera.size = (item.part.getSize() + 2) * 0.41;\n\t\t\tcamera.transform = Matrix4.getTranslation(item.part.getCenter().times(-0.5))\n\t\t\t\t.times(Matrix4.getRotation(new Vector3(0, 45, -30))\n\t\t\t\t.times(Matrix4.getTranslation(new Vector3(-0.1, 0, 0))));\n\t\t\tcamera.render();\n\t\t\tvar context = itemCanvas.getContext(\"2d\");\n\t\t\tcontext.canvas.width = gl.canvas.width;\n\t\t\tcontext.canvas.height = gl.canvas.height;\n\t\t\tcontext.drawImage(canvas, 0, 0);\n\n\t\t\tlet itemCopy = item;\n\t\t\tcatalogLink.addEventListener(\"click\", (event: MouseEvent) => this.onSelectPart(itemCopy, event));\n\t\t}\n\t\tgl = oldRenderingContext;\n\t\tthis.initialized = true;\n\t\tthis.container.removeChild(canvas);\n\t}\n\n\tprivate createCatalogItems() {\t\t\n\t\tthis.items = [\n\t\t\tnew CatalogItem(3713, \"Bushing\", \"0z22z2\"),\n\t\t\tnew CatalogItem(32123, \"Half Bushing\", \"0z2\"),\n\t\t\tnew CatalogItem(43093, \"Axle to Pin Connector\", \"0z32z37z410z4\"),\n\t\t\tnew CatalogItem(6682, \"Pin with Ball\", \"7z50z32z3\"),\n\t\t\tnew CatalogItem(2736, \"Axle with Ball\", \"0z42z47z5\"),\n\t\t\tnew CatalogItem(6553, \"Axle 1.5 with Perpendicular Axle Connector\", \"1ex210z07z42z40z433x2\"),\n\t\t\tnew CatalogItem(18651, \"Axle 2m with Pin\", \"1ez432z47z410z40z32z3\"),\n\t\t\tnew CatalogItem(2853, \"Crankshaft\", \"8z411z40z2\"),\n\t\t\tnew CatalogItem(32054, \"Long Pin with Bushing Attached\", \"0z32z37z310z31ez232z2\"),\n\t\t\tnew CatalogItem(32138, \"Double Pin With Perpendicular Axle Hole\", \"11y21by29z312z34fz372z30z32z31ez332z3\"),\n\t\t\tnew CatalogItem(40147, \"Beam 1 x 2 with Axle Hole and Pin Hole\", \"7y10y2dy11y2\"),\n\t\t\tnew CatalogItem(43857, \"Beam 2\", \"0y17y11y1dy1\"),\n\t\t\tnew CatalogItem(17141, \"Beam 3\", \"0y17y11ey11y1dy12dy1\"),\n\t\t\tnew CatalogItem(32316, \"Beam 5\", \"9cy14dy11ey17y10y1c9y169y12dy1dy11y1\"),\n\t\t\tnew CatalogItem(16615, \"Beam 7\", \"1bay1113y19cy14dy11ey17y10y1215y1155y1c9y169y12dy1dy11y1\"),\n\t\t\tnew CatalogItem(41677, \"Beam 2 x 0.5 with Axle Holes\", \"0y27y2\"),\n\t\t\tnew CatalogItem(6632, \"Beam 3 x 0.5 with Axle Hole each end\", \"7y11ey20y2\"),\n\t\t\tnew CatalogItem(32449, \"Beam 4 x 0.5 with Axle Hole each end\", \"7y11ey14dy20y2\"),\n\t\t\tnew CatalogItem(11478, \"Beam 5 x 0.5 with Axle Holes each end\", \"7y11ey14dy19cy20y2\"),\n\t\t\tnew CatalogItem(32017, \"Beam 5 x 0.5\", \"1ey14dy19cy17y10y1\"),\n\t\t\tnew CatalogItem(3704, \"Axle 2\", \"0z42z47z410z4\"),\n\t\t\tnew CatalogItem(4519, \"Axle 3\", \"7z410z41ez432z40z42z4\"),\n\t\t\tnew CatalogItem(2825, \"Beam 1 x 4 x 0.5 with Boss\", \"7y11ey14dy20y21y2\"),\n\t\t\tnew CatalogItem(33299, \"Half Beam 3 with Knob and Pin\", \"2dy342y04y217y2ay21ey3\"),\n\t\t\tnew CatalogItem(60484, \"Beam 3 x 3 T-Shaped\", \"17x13bx11ex17x10x12ax15bx133x111x13x1\"),\n\t\t\tnew CatalogItem(6538, \"Angle Connector\", \"7z210z20y11y1\"),\n\t\t\tnew CatalogItem(59443, \"Axle Connector\", \"0z22z27z210z2\"),\n\t\t\tnew CatalogItem(15555, \"Pin Joiner\", \"0z12z17z110z1\"),\n\t\t\tnew CatalogItem(36536, \"Cross Block \", \"9y2fy20z12z1\"),\n\t\t\tnew CatalogItem(42003, \"Cross Block 1 x 3\", \"0z12z19z112z122y231y2\"),\n\t\t\tnew CatalogItem(32184, \"Cross Block 1 x 3 with Two Axle holes\", \"0y21y29z112z122y231y2\"),\n\t\t\tnew CatalogItem(41678, \"Cross Block 2 x 2 Split\", \"4z1bz10x219z12bz113x2\"),\n\t\t\tnew CatalogItem(32291, \"Cross Block With Two Pinholes\", \"4z1bz13x219z12bz19x2\"),\n\t\t\tnew CatalogItem(32034, \"Angle Connector #2\", \"0z22z27y11ez232z2dy1\"),\n\t\t\tnew CatalogItem(32039, \"Through Axle Connector with Bushing\", \"0y21y29x213x2\"),\n\t\t\tnew CatalogItem(32014, \"Angle Connector #6\", \"9y120z234z2fy10x23x2\"),\n\t\t\tnew CatalogItem(32126, \"Toggle Joint Connector\", \"7z210z20x1\"),\n\t\t\tnew CatalogItem(44809, \"Cross Block Bent 90 Degrees with Three Pinholes\", \"17z129z17x10y11y111x1\"),\n\t\t\tnew CatalogItem(55615, \"Cross Block beam Bent 90 Degrees with 4 Pins\", \"17y142x18dz3c1z34x126y182z1b4z1e6x1181z31e3z31ey30y32dy31y364x1cx112ex1\"),\n\t\t\tnew CatalogItem(48989, \"Cross Block Beam 3 with Four Pins\", \"0y31ey31y32dy34x117y142x126y114y382y323y3afy3cx164x1\"),\n\t\t\tnew CatalogItem(63869, \"Cross Block 3 x 2\", \"1ex17x10x117z229z233x111x13x1\"),\n\t\t\tnew CatalogItem(92907, \"Cross Block 2 x 2 x 2 Bent 90 Split\", \"2az143z166x035x213x217x07x20x2\"),\n\t\t\tnew CatalogItem(32557, \"Cross Block 2 x 3 with Four Pinholes\", \"9z112z119x13dx10z12z1cx125x1\"),\n\t\t\tnew CatalogItem(10197, \"Beam 1m with 2 Axles 90°\", \"7x10z42z417y426y411x1\"),\n\t\t\tnew CatalogItem(22961, \"Beam 1 with Axle\", \"0z42z47x111x1\"),\n\t\t\tnew CatalogItem(15100, \"Hole With pin\", \"0z32z37x111x1\"),\n\t\t\tnew CatalogItem(98989, \"Cross Block 2 x 4\", \"7x10x117z1bz13bz124z17bz255z211x13x1\"),\n\t\t\tnew CatalogItem(27940, \"Beam 1 Hole with 2 Axles 180\", \"7x11ez432z40z42z411x1\"),\n\t\t\tnew CatalogItem(87082, \"Long Pin with Center Hole\", \"0z32z37x11ez332z311x1\"),\n\t\t\tnew CatalogItem(11272, \"Cross Block 2 x 3\", \"7x211x233x23x220x24fx29x235x2\"),\n\t\t\tnew CatalogItem(32140, \"Beam 2 x 4 Bent 90 Degrees, 2 and 4 holes\", \"a2y153y1cfy16fy151y16dy120y12fy17y2dy2\"),\n\t\t\tnew CatalogItem(32526, \"Beam Bent 90 Degrees, 3 and 5 Holes\", \"1ey12dy14fy16by1a0y1cdy1119y115by11c2y111by1a4y121dy115dy1d1y1\"),\n\t\t\tnew CatalogItem(32056, \"Beam 3 x 3 x 0.5 Bent 90\", \"0y27y11ey24fy1a0y2\"),\n\t\t\tnew CatalogItem(64179, \"Beam Frame 5 x 7\", \"3bcz1466z122z136z1525z15f6z153z176z16dey1527x13c0y12a1x11c2y111bx1a4y17c5y1459y121dy1d1y15f9x1329x1169x129bz1322z19z112z11bay10y17x11ey14dx19cy1113x1215y11y12dy1c9y111x171x1161x1\"),\n\t\t\tnew CatalogItem(14720, \"Beam I Frame\", \"115y19ex14fx120x19y1157y1fy1d5x173x135x11bey122y1219y131y19cy10y1c9y11y1\"),\n\t\t\tnew CatalogItem(53533, \"Half Beam 3 with Fork\", \"11y13z38z31by135x073x1d5x17x01ex14dx1\"),\n\t\t\tnew CatalogItem(4261, \"Steering Arm with Two Half Pins\", \"31z14bz162y322y319y04y2\"),\n\t\t\tnew CatalogItem(6572, \"Half Beam Fork with Ball Joint\", \"7z010x232x170x23z58z320z050x29fx1116x2\"),\n\t\t\tnew CatalogItem(15460, \"Hole with 3 Ball Joints\", \"0z52z57x11ez532z517y526y511x1\")\n\t\t];\n\t}\n\n\tprivate onSelectPart(item: CatalogItem, event: MouseEvent) {\n\t\teditor.part = Part.fromString(item.string);\n\t\teditor.updateMesh(true);\n\t\twindow.history.pushState({}, document.title, \"?part=\" + item.string + \"&name=\" + encodeURIComponent(item.name));\n\t\tevent.preventDefault();\n\t\teditor.setName(item.name);\n\t}\n}"
  },
  {
    "path": "src/editor/CatalogItem.ts",
    "content": "class CatalogItem {\n\tpart: Part = null;\n\tid: number;\n\tstring: string;\n\tname: string;\n\n\tconstructor(id: number, name: string, string: string) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.string = string;\n\t\tthis.part = Part.fromString(string);\n\t}\n}"
  },
  {
    "path": "src/editor/Editor.ts",
    "content": "enum MouseMode {\n\tNone,\n\tManipulate,\n\tTranslate,\n\tRotate\n}\n\nclass Editor {\n\tcamera: Camera;\n\tpartRenderer: MeshRenderer;\n\tpartNormalDepthRenderer: NormalDepthRenderer;\n\tcontourEffect: ContourPostEffect;\n\twireframeRenderer: WireframeRenderer;\n\tpart: Part;\n\tcanvas: HTMLCanvasElement;\n\n\ttranslation: Vector3 = new Vector3(0, 0, 0);\n\tcenter: Vector3;\n\trotationX: number = 45;\n\trotationY: number = -20;\n\tzoom: number = 5;\n\tzoomStep = 0.9;\n\n\tmouseMode = MouseMode.None;\n\tlastMousePosition: [number, number];\n\n\thandles: Handles;\n\n\teditorState: EditorState;\n\n\tstyle: RenderStyle = RenderStyle.Contour;\n\n\tmeasurements: Measurements = new Measurements();\n\tpreviousMousePostion: [number, number];\n\n\tconstructor() {\n\t\tvar url = new URL(document.URL);\n\t\tif (url.searchParams.has(\"part\")) {\n\t\t\tthis.part = Part.fromString(url.searchParams.get(\"part\"));\n\t\t\tif (url.searchParams.has(\"name\")) {\n\t\t\t\tthis.setName(url.searchParams.get(\"name\"));\n\t\t\t}\n\t\t} else {\n\t\t\tthis.part = Part.fromString(catalog.items[Math.floor(Math.random() * catalog.items.length)].string);\n\t\t}\n\n\t\tthis.displayMeasurements();\n\t\t\n\t\tthis.editorState = new EditorState();\n\n\t\tthis.canvas = document.getElementById('canvas') as HTMLCanvasElement;\n\t\tthis.canvas.tabIndex = 0;\n\t\tthis.camera = new Camera(this.canvas);\n\t\t\n\t\tthis.partRenderer = new MeshRenderer();\n\t\tthis.partRenderer.color = new Vector3(0.67, 0.7, 0.71);\n\t\tthis.camera.renderers.push(this.partRenderer);\n\n\t\tthis.wireframeRenderer = new WireframeRenderer();\n\t\tthis.wireframeRenderer.enabled = false;\n\t\tthis.camera.renderers.push(this.wireframeRenderer);\n\n\t\tthis.partNormalDepthRenderer = new NormalDepthRenderer();\n\t\tthis.camera.renderers.push(this.partNormalDepthRenderer);\n\n\t\tthis.contourEffect = new ContourPostEffect();\n\t\tthis.camera.renderers.push(this.contourEffect);\n\n\t\tthis.handles = new Handles(this.camera);\n\t\tthis.camera.renderers.push(this.handles);\n\n\t\tthis.center = Vector3.zero();\n\t\tthis.updateMesh(true);\n\t\tthis.camera.size = this.zoom;\n\t\tthis.camera.render();\n\n\t\tthis.canvas.addEventListener(\"mousedown\", (event: MouseEvent) => this.onMouseDown(event));\n\t\tthis.canvas.addEventListener(\"mouseup\", (event: MouseEvent) => this.onMouseUp(event));\n\t\tthis.canvas.addEventListener(\"mousemove\", (event: MouseEvent) => this.onMouseMove(event));\n\t\tthis.canvas.addEventListener(\"contextmenu\", (event: Event) => event.preventDefault());\n\t\tthis.canvas.addEventListener(\"wheel\", (event: WheelEvent) => this.onScroll(event));\n\t\twindow.addEventListener(\"keydown\", (event: KeyboardEvent) => this.onKeydown(event));\n\t\tdocument.getElementById(\"clear\").addEventListener(\"click\", (event: MouseEvent) => this.clear());\n\t\tdocument.getElementById(\"share\").addEventListener(\"click\", (event: MouseEvent) => this.share());\n\t\tdocument.getElementById(\"save-stl\").addEventListener(\"click\", (event: MouseEvent) => this.saveSTL());\n\t\tdocument.getElementById(\"save-studio\").addEventListener(\"click\", (event: MouseEvent) => this.saveStudioPart());\n\t\tdocument.getElementById(\"remove\").addEventListener(\"click\", (event: MouseEvent) => this.remove());\n\t\tdocument.getElementById(\"style\").addEventListener(\"change\", (event: MouseEvent) => this.setRenderStyle(parseInt((event.srcElement as HTMLSelectElement).value)));\n        window.addEventListener(\"resize\", (e: Event) => this.camera.onResize());\n\t\tdocument.getElementById(\"applymeasurements\").addEventListener(\"click\", (event: MouseEvent) => this.applyMeasurements());\n\t\tdocument.getElementById(\"resetmeasurements\").addEventListener(\"click\", (event: MouseEvent) => this.resetMeasurements());\n\n\t\tthis.initializeEditor(\"type\", (typeName: string) => this.setType(typeName));\n\t\tthis.initializeEditor(\"orientation\", (orientationName: string) => this.setOrientation(orientationName));\n\t\tthis.initializeEditor(\"size\", (sizeName: string) => this.setSize(sizeName));\n\t\tthis.initializeEditor(\"rounded\", (roundedName: string) => this.setRounded(roundedName));\n\n\t\tdocument.getElementById(\"blockeditor\").addEventListener(\"toggle\", (event: MouseEvent) => this.onNodeEditorClick(event));\n\n\t\tthis.getNameTextbox().addEventListener(\"change\", (event: Event) => this.onPartNameChange(event));\n\t\tthis.getNameTextbox().addEventListener(\"keyup\", (event: Event) => this.onPartNameChange(event));\n\t}\n\n\tprivate onNodeEditorClick(event: MouseEvent) {\n\t\tthis.handles.visible = (event.srcElement as HTMLDetailsElement).open;\n\t\tthis.camera.render();\n\t}\n\n\tprivate saveSTL() {\n\t\tSTLExporter.saveSTLFile(this.part, this.measurements, this.getName());\n\t}\n\n\tprivate saveStudioPart() {\n\t\tStudioPartExporter.savePartFile(this.part, this.measurements, this.getName());\n\t}\n\n\tprivate initializeEditor(elementId: string, onchange: (value: string) => void) {\n\t\tvar element = document.getElementById(elementId);\n\t\tfor (var i = 0; i < element.children.length; i++) {\n\t\t\tvar child = element.children[i];\n\t\t\tif (child.tagName.toLowerCase() == \"label\") {\t\t\t\t\n\t\t\t\tchild.addEventListener(\"click\", (event: Event) => onchange(((event.target as HTMLElement).previousElementSibling as HTMLInputElement).value));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate clear() {\n\t\tthis.part.blocks.clear();\n\t\tthis.updateMesh();\n\t}\n\n\tprivate share() {\n\t\tvar name = this.getName();\n\t\tvar url = \"?part=\" + this.part.toString();\n\t\tif (name.length != 0) {\n\t\t\turl += '&name=' + encodeURIComponent(name);\n\t\t}\n\t\twindow.history.pushState({}, document.title, url);\n\t}\n\n\tprivate remove() {\n\t\tthis.part.clearBlock(this.handles.getSelectedBlock(), this.editorState.orientation);\n\t\tif (this.editorState.fullSize) {\n\t\t\tthis.part.clearBlock(this.handles.getSelectedBlock().plus(FORWARD[this.editorState.orientation]), this.editorState.orientation);\n\t\t}\n\t\tthis.updateMesh();\n\t}\n\n\tprivate setType(typeName: string) {\n\t\tthis.editorState.type = BLOCK_TYPE[typeName];\n\t\tthis.updateBlock();\n\t}\n\n\tprivate setOrientation(orientatioName: string) {\n\t\tthis.editorState.orientation = ORIENTATION[orientatioName];\n\t\tthis.handles.setMode(this.editorState.fullSize, this.editorState.orientation);\n\t\tthis.updateBlock();\n\t}\n\n\tprivate setSize(sizeName: string) {\n\t\tthis.editorState.fullSize = sizeName == \"full\";\n\t\tthis.handles.setMode(this.editorState.fullSize, this.editorState.orientation);\n\t\tthis.camera.render();\n\t}\n\n\tprivate setRounded(roundedName: string) {\n\t\tthis.editorState.rounded = roundedName == \"true\";\n\t\tthis.updateBlock();\n\t}\n\n\tprivate setRenderStyle(style: RenderStyle) {\n\t\tthis.style = style;\n\t\tthis.partNormalDepthRenderer.enabled = style == RenderStyle.Contour;\n\t\tthis.contourEffect.enabled = style == RenderStyle.Contour;\n\t\tthis.partRenderer.enabled = style != RenderStyle.Wireframe;\n\t\tthis.wireframeRenderer.enabled = style == RenderStyle.SolidWireframe || style == RenderStyle.Wireframe;\n\t\tthis.updateMesh();\n\t}\n\n\tprivate updateBlock() {\n\t\tthis.part.placeBlockForced(this.handles.getSelectedBlock(), new Block(this.editorState.orientation, this.editorState.type, this.editorState.rounded));\n\t\tif (this.editorState.fullSize) {\n\t\t\tthis.part.placeBlockForced(this.handles.getSelectedBlock().plus(FORWARD[this.editorState.orientation]),\n\t\t\t\tnew Block(this.editorState.orientation, this.editorState.type, this.editorState.rounded));\n\t\t}\n\t\tthis.updateMesh();\n\t}\n\n\tpublic updateMesh(center = false) {\n\t\tlet mesh = new PartMeshGenerator(this.part, this.measurements).getMesh();\n\t\tif (this.partRenderer.enabled) {\n\t\t\tthis.partRenderer.setMesh(mesh);\n\t\t}\n\t\tif (this.partNormalDepthRenderer.enabled) {\n\t\t\tthis.partNormalDepthRenderer.setMesh(mesh);\n\t\t}\n\t\tif (this.wireframeRenderer.enabled) {\n\t\t\tthis.wireframeRenderer.setMesh(mesh);\n\t\t}\n\n\t\tvar newCenter = this.part.getCenter().times(-0.5);\n\t\tif (center) {\n\t\t\tthis.translation = Vector3.zero();\n\t\t} else {\n\t\t\tthis.translation = this.translation.plus(this.getRotation().transformDirection(this.center.minus(newCenter)));\n\t\t}\n\t\tthis.center = newCenter;\n\t\tthis.updateTransform();\n\t\tthis.handles.updateTransforms();\n\t\tthis.camera.render();\n\t}\n\n\tprivate getRotation(): Matrix4 {\n\t\treturn Matrix4.getRotation(new Vector3(0, this.rotationX, this.rotationY));\n\t}\n\n\tprivate updateTransform() {\n\t\tthis.camera.transform = \n\t\t\tMatrix4.getTranslation(this.center)\n\t\t\t.times(this.getRotation())\n\t\t\t.times(Matrix4.getTranslation(this.translation.plus(new Vector3(0, 0, -15))));\n\t}\n\n\tprivate onMouseDown(event: MouseEvent) {\n\t\tthis.canvas.focus();\n\t\tconst {ctrlKey, shiftKey} = event;\n\t\tif (event.button === 0 && !ctrlKey && !shiftKey) {\n\t\t\tif (this.handles.onMouseDown(event)) {\n\t\t\t\tthis.mouseMode = MouseMode.Manipulate;\n\t\t\t}\n\t\t} else if (event.button === 1 || shiftKey) {\n\t\t\tthis.mouseMode = MouseMode.Translate;\n\t\t\tthis.previousMousePostion = [event.clientX, event.clientY];\n\t\t} else if (event.button === 2 || ctrlKey) {\n\t\t\tthis.mouseMode = MouseMode.Rotate;\n\t\t}\n\t\tevent.preventDefault();\n\t}\n\n\tprivate onMouseUp(event: MouseEvent) {\n\t\tthis.mouseMode = MouseMode.None;\n\t\tthis.handles.onMouseUp();\n\t\tevent.preventDefault();\n\t}\n\n\tprivate onMouseMove(event: MouseEvent) {\n\t\tswitch (this.mouseMode) {\n\t\t\tcase MouseMode.None:\n\t\t\tcase MouseMode.Manipulate:\n\t\t\t\tthis.handles.onMouseMove(event);\n\t\t\t\tbreak;\n\t\t\tcase MouseMode.Translate:\n\t\t\t\tthis.translation = this.translation.plus(new Vector3(event.clientX - this.previousMousePostion[0], -(event.clientY - this.previousMousePostion[1]), 0).times(this.camera.size / this.canvas.clientHeight));\n\t\t\t\tthis.previousMousePostion = [event.clientX, event.clientY];\n\t\t\t\tthis.updateTransform();\n\t\t\t\tthis.camera.render();\n\t\t\t\tbreak;\n\t\t\tcase MouseMode.Rotate:\n\t\t\t\tthis.rotationX -= event.movementX * 0.6;\n\t\t\t\tthis.rotationY = clamp(-90, 90, this.rotationY - event.movementY * 0.6);\n\t\t\t\t\n\t\t\t\tthis.updateTransform();\n\t\t\t\tthis.camera.render();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate onScroll(event: WheelEvent) {\n\t\tthis.zoom *= event.deltaY < 0 ? this.zoomStep : 1 / this.zoomStep;\n\t\tthis.camera.size = this.zoom;\n\t\tthis.camera.render();\n\t}\n\n\tprivate onKeydown(event: KeyboardEvent) {\n\t\tconst keyActions: { [key: string]: () => void } = {\n\t\t\t'1': () => this.setType('pinhole'),\n\t\t\t'2': () => this.setType('axlehole'),\n\t\t\t'3': () => this.setType('pin'),\n\t\t\t'4': () => this.setType('axle'),\n\t\t\t'5': () => this.setType('solid'),\n\t\t\t'6': () => this.setType('balljoint'),\n\t\t\t'y': () => this.setOrientation('y'),\n\t\t\t'z': () => this.setOrientation('z'),\n\t\t\t'x': () => this.setOrientation('x'),\n\t\t\t'PageUp': () => this.handles.move(new Vector3(0, 1, 0)),\n\t\t\t'PageDown': () => this.handles.move(new Vector3(0, -1, 0)),\n\t\t\t'ArrowLeft': () => this.handles.move(new Vector3(0, 0, 1)),\n\t\t\t'ArrowRight': () => this.handles.move(new Vector3(0, 0, -1)),\n\t\t\t'ArrowUp': () => this.handles.move(new Vector3(-1, 0, 0)),\n\t\t\t'ArrowDown': () => this.handles.move(new Vector3(1, 0, 0)),\n\t\t\t'Backspace': () => this.remove(),\n\t\t\t'Delete': () => this.remove(),\n\t\t};\n\n\t\tif (event.key in keyActions && document.activeElement == this.canvas) {\n\t\t\tkeyActions[event.key]();\n\t\t}\n\t}\n\tprivate displayMeasurements() {\n\t\tfor (var namedMeasurement of NAMED_MEASUREMENTS) {\n\t\t\tnamedMeasurement.applyToDom(this.measurements);\n\t\t}\n\t}\n\n\tpublic applyMeasurements() {\n\t\tfor (var namedMeasurement of NAMED_MEASUREMENTS) {\n\t\t\tnamedMeasurement.readFromDOM(this.measurements);\n\t\t}\n\t\tthis.measurements.enforceConstraints();\n\t\tthis.displayMeasurements();\n\t\tthis.updateMesh();\n\t}\n\n\tprivate resetMeasurements() {\n\t\tthis.measurements = new Measurements();\n\t\tthis.displayMeasurements();\n\t\tthis.updateMesh();\n\t}\n\n\tpublic getNameTextbox(): HTMLInputElement {\n\t\treturn document.getElementById('partName') as HTMLInputElement;\n\t}\n\n\tpublic getName(): string {\n\t\tvar name = this.getNameTextbox().value.trim();\n\t\tif (name.length == 0) {\n\t\t\tname = 'Part';\n\t\t}\n\t\treturn name;\n\t}\n\n\tprivate onPartNameChange(event: Event) {\n\t\tvar name = this.getNameTextbox().value.trim();\n\t\tif (name.length == 0) {\n\t\t\tdocument.title = 'Part Designer';\n\t\t} else {\n\t\t\tdocument.title = name + ' ⋅ Part Designer';\n\t\t}\n\t}\n\n\tpublic setName(name: string) {\n\t\tdocument.title = name + ' ⋅ Part Designer';\n\t\tthis.getNameTextbox().value = name;\n\t}\n}\n"
  },
  {
    "path": "src/editor/EditorState.ts",
    "content": "class EditorState {\n\tpublic orientation: Orientation = Orientation.X;\n\tpublic type: BlockType = BlockType.PinHole;\n\tpublic fullSize: boolean = true;\n\tpublic rounded: boolean = true;\n}"
  },
  {
    "path": "src/editor/Handles.ts",
    "content": "const ARROW_RADIUS_INNER = 0.05;\nconst ARROW_RADIUS_OUTER = 0.15;\nconst ARROW_LENGTH = 0.35;\nconst ARROW_TIP = 0.15;\n\nconst HANDLE_DISTANCE = 0.5;\n\nconst GRAB_RADIUS = 0.1;\nconst GRAB_START = 0.4;\nconst GRAB_END = 1.1;\n\nconst UNSELECTED_ALPHA = 0.5;\n\nenum Axis {\n\tNone,\n\tX,\n\tY,\n\tZ\n}\n\nclass Handles implements Renderer {\n\tprivate xNegative: MeshRenderer;\n\tprivate xPositive: MeshRenderer;\n\tprivate yNegative: MeshRenderer;\n\tprivate yPositive: MeshRenderer;\n\tprivate zNegative: MeshRenderer;\n\tprivate zPositive: MeshRenderer;\n\tprivate meshRenderers: MeshRenderer[] = [];\n\n\tprivate position: Vector3;\n\tprivate block: Vector3;\n\tprivate camera: Camera;\n\n\tprivate handleAlpha: Vector3 = Vector3.one().times(UNSELECTED_ALPHA);\n\n\tprivate grabbedAxis: Axis = Axis.None;\n\tprivate grabbedPosition: number;\n\n\tvisible: boolean = true;\n\t\n\tprivate box: WireframeBox;\n\n\tprivate fullSize: boolean = true;\n\tprivate orientation: Orientation = Orientation.X;\n\tprivate size: Vector3;\n\n\tprivate createRenderer(mesh: Mesh, color: Vector3): MeshRenderer {\n\t\tlet renderer = new MeshRenderer();\n\t\trenderer.setMesh(mesh);\n\t\trenderer.color = color;\n\t\tthis.meshRenderers.push(renderer);\n\t\treturn renderer;\n\t}\n\n\tprivate getBlockCenter(block: Vector3): Vector3 {\n\t\tif (this.fullSize) {\n\t\t\treturn this.block.plus(Vector3.one()).times(0.5);\n\t\t} else {\n\t\t\treturn this.block.plus(Vector3.one()).times(0.5).minus(FORWARD[this.orientation].times(0.25));\n\t\t}\n\t}\n\n\tprivate getBlock(worldPosition: Vector3): Vector3 {\n\t\tif (this.fullSize) {\n\t\t\treturn worldPosition.times(2).minus(Vector3.one().times(0.5)).floor();\n\t\t} else {\n\t\t\treturn worldPosition.times(2).minus(Vector3.one().minus(FORWARD[this.orientation]).times(0.5)).floor();\n\t\t}\n\t}\n\n\tconstructor(camera: Camera) {\n\t\tthis.box = new WireframeBox();\n\t\tlet mesh = Handles.getArrowMesh(20);\n\n\t\tthis.xNegative = this.createRenderer(mesh, new Vector3(1, 0, 0));\n\t\tthis.xPositive = this.createRenderer(mesh, new Vector3(1, 0, 0));\n\t\tthis.yNegative = this.createRenderer(mesh, new Vector3(0, 1, 0));\n\t\tthis.yPositive = this.createRenderer(mesh, new Vector3(0, 1, 0));\n\t\tthis.zNegative = this.createRenderer(mesh, new Vector3(0, 0, 1));\n\t\tthis.zPositive = this.createRenderer(mesh, new Vector3(0, 0, 1));\n\t\t\n\t\tthis.block = Vector3.zero();\n\t\tthis.setMode(true, Orientation.X, false);\n\t\tthis.camera = camera;\n\t}\n\n\tpublic render(camera: Camera) {\n\t\tif (!this.visible) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.box.render(camera);\n\n\t\tthis.xPositive.alpha = this.handleAlpha.x;\n\t\tthis.xNegative.alpha = this.handleAlpha.x;\n\t\tthis.yPositive.alpha = this.handleAlpha.y;\n\t\tthis.yNegative.alpha = this.handleAlpha.y;\n\t\tthis.zPositive.alpha = this.handleAlpha.z;\n\t\tthis.zNegative.alpha = this.handleAlpha.z;\n\n\t\tgl.colorMask(false, false, false, false);\n\t\tgl.depthFunc(gl.ALWAYS);\n\t\tfor (let renderer of this.meshRenderers) {\n\t\t\trenderer.render(camera);\n\t\t}\n\t\tgl.depthFunc(gl.LEQUAL);\n\t\tfor (let renderer of this.meshRenderers) {\n\t\t\trenderer.render(camera);\n\t\t}\n\t\tgl.colorMask(true, true, true, true);\n\t\tfor (let renderer of this.meshRenderers) {\n\t\t\trenderer.render(camera);\n\t\t}\n\t}\n\n\tpublic updateTransforms() {\n\t\tthis.xPositive.transform = Quaternion.euler(new Vector3(0, -90, 0)).toMatrix()\n\t\t\t.times(Matrix4.getTranslation(this.position.plus(new Vector3(this.size.x * HANDLE_DISTANCE, 0, 0))));\n\t\tthis.xNegative.transform = Quaternion.euler(new Vector3(0, 90, 0)).toMatrix()\n\t\t\t.times(Matrix4.getTranslation(this.position.plus(new Vector3(this.size.x * -HANDLE_DISTANCE, 0, 0))));\n\t\tthis.yPositive.transform = Quaternion.euler(new Vector3(90, 0, 0)).toMatrix()\n\t\t\t.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, this.size.y * HANDLE_DISTANCE, 0))));\n\t\tthis.yNegative.transform = Quaternion.euler(new Vector3(-90, 0, 0)).toMatrix()\n\t\t\t.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, this.size.y * -HANDLE_DISTANCE, 0))));\n\t\tthis.zPositive.transform = Matrix4.getTranslation(this.position.plus(new Vector3(0, 0, this.size.z * HANDLE_DISTANCE)));\n\t\tthis.zNegative.transform = Quaternion.euler(new Vector3(180, 0, 0)).toMatrix()\n\t\t\t.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, 0, this.size.z * -HANDLE_DISTANCE))));\n\t\t\t\n\t\tthis.box.transform = Matrix4.getTranslation(this.getBlockCenter(this.block));\n\t\tthis.box.scale = this.size.times(0.5);\n\t}\n\n\tprivate static getVector(angle: number, radius: number, z: number): Vector3 {\n\t\treturn new Vector3(radius * Math.cos(angle), radius * Math.sin(angle), z);\n\t}\n\t\n\tpublic static getArrowMesh(subdivisions: number): Mesh {\n\t\tlet triangles: Triangle[] = [];\n\n\t\tfor (let i = 0; i < subdivisions; i++) {\n\t\t\tlet angle1 = i / subdivisions * 2 * Math.PI;\n\t\t\tlet angle2 = (i + 1) / subdivisions * 2 * Math.PI;\n\n\t\t\t// Base\n\t\t\ttriangles.push(new Triangle(Handles.getVector(angle1, ARROW_RADIUS_INNER, 0), Vector3.zero(), Handles.getVector(angle2, ARROW_RADIUS_INNER, 0)));\n\t\t\t// Side\n\t\t\ttriangles.push(new TriangleWithNormals(\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_INNER, 0),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_INNER, 0),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle1, 1, 0).times(-1),\n\t\t\t\tHandles.getVector(angle2, 1, 0).times(-1),\n\t\t\t\tHandles.getVector(angle2, 1, 0).times(-1)));\n\t\t\ttriangles.push(new TriangleWithNormals(\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_INNER, 0),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle1, 1, 0).times(-1),\n\t\t\t\tHandles.getVector(angle1, 1, 0).times(-1),\n\t\t\t\tHandles.getVector(angle2, 1, 0).times(-1)));\n\t\t\t// Tip base\n\t\t\ttriangles.push(new Triangle(\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH)));\n\t\t\ttriangles.push(new Triangle(\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_OUTER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH)));\n\t\t\t// Tip\n\t\t\tlet alpha = Math.tan(ARROW_TIP / ARROW_RADIUS_OUTER);\n\n\t\t\ttriangles.push(new TriangleWithNormals(\n\t\t\t\tnew Vector3(0, 0, ARROW_LENGTH + ARROW_TIP),\n\t\t\t\tHandles.getVector(angle1, ARROW_RADIUS_OUTER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH),\n\t\t\t\tHandles.getVector(angle1, -Math.sin(alpha), -Math.cos(alpha)),\n\t\t\t\tHandles.getVector(angle1, -Math.sin(alpha), -Math.cos(alpha)),\n\t\t\t\tHandles.getVector(angle2, -Math.sin(alpha), -Math.cos(alpha))));\n\t\t}\n\n\t\treturn new Mesh(triangles);\n\t}\n\n\tprivate getRay(axis: Axis): Ray {\n\t\tswitch (axis) {\n\t\t\tcase Axis.X:\n\t\t\t\treturn new Ray(this.position, new Vector3(1, 0, 0));\n\t\t\tcase Axis.Y:\n\t\t\t\treturn new Ray(this.position, new Vector3(0, 1, 0));\n\t\t\tcase Axis.Z:\n\t\t\t\treturn new Ray(this.position, new Vector3(0, 0, 1));\n\t\t}\n\t\tthrow new Error(\"Unknown axis: \" + axis);\n\t}\n\n\tprivate getMouseHandle(event: MouseEvent): [Axis, number] {\n\t\tvar mouseRay = this.camera.getScreenToWorldRay(event);\n\t\tfor (let axis of [Axis.X, Axis.Y, Axis.Z]) {\n\t\t\tvar axisRay = this.getRay(axis);\n\t\t\tif (mouseRay.getDistanceToRay(axisRay) < GRAB_RADIUS) {\n\t\t\t\tvar position = axisRay.getClosestToRay(mouseRay);\n\t\t\t\tif (Math.abs(position) > GRAB_START && Math.abs(position) < GRAB_END) {\n\t\t\t\t\treturn [axis, position];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn [Axis.None, 0];\n\t}\n\n\tpublic onMouseDown(event: MouseEvent): boolean {\n\t\tvar handleData = this.getMouseHandle(event);\n\t\tthis.grabbedAxis = handleData[0];\n\t\tthis.grabbedPosition = handleData[1];\t\t\n\t\treturn this.grabbedAxis != Axis.None;\n\t}\n\n\tpublic onMouseMove(event: MouseEvent) {\n\t\tif (this.grabbedAxis != Axis.None) {\n\t\t\tvar mouseRay = this.camera.getScreenToWorldRay(event);\n\t\t\tvar axisRay = this.getRay(this.grabbedAxis);\n\t\t\tvar mousePosition = axisRay.getClosestToRay(mouseRay);\n\n\t\t\tthis.position = this.position.plus(axisRay.direction.times(mousePosition - this.grabbedPosition));\t\t\t\n\t\t\tthis.block = this.getBlock(this.position);\n\t\t\tthis.updateTransforms();\n\t\t\tthis.camera.render();\n\t\t} else {\n\t\t\tvar axis = this.getMouseHandle(event)[0];\n\t\t\tvar newAlpha = new Vector3(axis == Axis.X ? 1 : UNSELECTED_ALPHA, axis == Axis.Y ? 1 : UNSELECTED_ALPHA, axis == Axis.Z ? 1 : UNSELECTED_ALPHA);\n\t\t\tif (!newAlpha.equals(this.handleAlpha)) {\n\t\t\t\tthis.handleAlpha = newAlpha;\n\t\t\t\tthis.camera.render();\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic onMouseUp() {\n\t\tif (this.grabbedAxis != Axis.None) {\n\t\t\tthis.grabbedAxis = Axis.None;\n\t\t\tthis.animatePositionAndSize(this.getBlockCenter(this.block), this.size, false, 100);\n\t\t}\n\t}\n\n\tpublic move(direction: Vector3) {\n\t\tthis.position = this.position.plus(direction);\n\t\tthis.block = this.getBlock(this.position);\n\t\tthis.updateTransforms();\n\t\tthis.camera.render();\n\t}\n\n\tpublic getSelectedBlock(): Vector3 {\n\t\treturn this.block;\n\t}\n\n\tpublic setMode(fullSize: boolean, orientation: Orientation, animate: boolean = true) {\n\t\tif (this.fullSize == fullSize && this.orientation == orientation && animate) {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (orientation) {\n\t\t\tcase Orientation.X:\n\t\t\t\tthis.box.color = new Vector3(1.0, 0.0, 0.0);\n\t\t\t\tbreak;\n\t\t\tcase Orientation.Y:\n\t\t\t\tthis.box.color = new Vector3(0.0, 0.8, 0.0);\n\t\t\t\tbreak;\n\t\t\tcase Orientation.Z:\n\t\t\t\tthis.box.color = new Vector3(0.0, 0.0, 1.0);\n\t\t\t\tbreak;\n\t\t}\n\t\t\n\t\tthis.fullSize = fullSize;\n\t\tthis.orientation = orientation;\n\n\t\tvar targetPosition = this.getBlockCenter(this.block);\n\t\tvar targetSize = Vector3.one();\n\t\tif (!this.fullSize) {\n\t\t\ttargetSize = targetSize.minus(FORWARD[this.orientation].times(0.5));\n\t\t}\n\t\t\n\t\tif (!animate) {\n\t\t\tthis.position = targetPosition;\n\t\t\tthis.size = Vector3.one();\n\t\t\tthis.updateTransforms();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.animatePositionAndSize(targetPosition, targetSize);\n\t}\n\n\tprivate animatePositionAndSize(targetPosition: Vector3, targetSize: Vector3, animateBox: boolean = true, time = 300) {\n\t\tvar startPosition = this.position;\n\t\tvar startSize = this.size;\n\n\t\tvar start = new Date().getTime();\n\t\tvar end = start + time;\n\t\tvar handles = this;\n\n\t\tfunction callback() {\n\t\t\tvar progress = ease(Math.min(1.0, (new Date().getTime() - start) / (end - start)));\n\t\t\thandles.position = Vector3.lerp(startPosition, targetPosition, progress);\n\t\t\thandles.size = Vector3.lerp(startSize, targetSize, progress);\n\t\t\thandles.updateTransforms();\n\t\t\tif (animateBox) {\n\t\t\t\thandles.box.transform = Matrix4.getTranslation(handles.position);\n\t\t\t}\n\t\t\thandles.camera.render();\n\t\t\tif (progress < 1.0) {\n\t\t\t\twindow.requestAnimationFrame(callback);\n\t\t\t}\n\t\t}\n\t\twindow.requestAnimationFrame(callback)\n\t}\n}"
  },
  {
    "path": "src/editor/NamedMeasurement.ts",
    "content": "class NamedMeasurement {\n\tprivate name: string;\n\tprivate relative: boolean;\n\tprivate displayDouble: boolean;\n\tprivate domElement: HTMLInputElement;\n\tprivate resetElement: HTMLAnchorElement;\n\n\tconstructor(name: string, relative: boolean, displayDouble: boolean) {\n\t\tthis.name = name;\n\t\tthis.relative = relative;\n\t\tthis.displayDouble = displayDouble;\n\t\tthis.domElement = document.getElementById(name) as HTMLInputElement;\n\t\tthis.resetElement = this.domElement.previousElementSibling as HTMLAnchorElement;\n\t\tif (this.domElement == null) {\n\t\t\tthrow new Error(\"DOM Element \" + this.name + \" not found.\");\n\t\t}\n\t\tthis.resetElement.addEventListener(\"click\", (event: MouseEvent) => this.reset(event));\n\t}\n\n\tpublic readFromDOM(measurements: Measurements) {\n\t\tvar value = parseFloat(this.domElement.value);\n\t\tif (!isFinite(value) || value < 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.relative) {\n\t\t\tvalue /= measurements.technicUnit;\n\t\t}\n\t\tif (this.displayDouble) {\n\t\t\tvalue /= 2;\n\t\t}\n\t\tmeasurements[this.name] = value;\n\t}\n\n\tpublic applyToDom(measurements: Measurements) {\n\t\tvar value: number = measurements[this.name];\n\t\tif (this.relative) {\n\t\t\tvalue *= measurements.technicUnit;\n\t\t}\n\t\tif (this.displayDouble) {\n\t\t\tvalue *= 2;\n\t\t}\n\t\tvalue = Math.round(value * 1000) / 1000;\n\t\tthis.domElement.value = value.toString();\n\t\tthis.resetElement.style.visibility = measurements[this.name] == DEFAULT_MEASUREMENTS[this.name] ? \"hidden\" : \"visible\";\n\t}\n\n\tprivate reset(event: MouseEvent) {\n\t\teditor.measurements[this.name] = DEFAULT_MEASUREMENTS[this.name];\n\t\tthis.applyToDom(DEFAULT_MEASUREMENTS);\n\t\teditor.updateMesh();\n\t\tevent.preventDefault();\n\t}\n}\n\nconst NAMED_MEASUREMENTS : NamedMeasurement[] = [\n\tnew NamedMeasurement(\"technicUnit\", false, false),\n\tnew NamedMeasurement(\"edgeMargin\", true, false),\n\tnew NamedMeasurement(\"interiorRadius\", true, true),\n\tnew NamedMeasurement(\"pinHoleRadius\", true, true),\n\tnew NamedMeasurement(\"pinHoleOffset\", true, false),\n\tnew NamedMeasurement(\"axleHoleSize\", true, true),\n\tnew NamedMeasurement(\"pinRadius\", true, true),\n\tnew NamedMeasurement(\"pinLipRadius\", true, true),\n\tnew NamedMeasurement(\"axleSizeInner\", true, false),\n\tnew NamedMeasurement(\"axleSizeOuter\", true, false),\n\tnew NamedMeasurement(\"attachmentAdapterSize\", true, true),\n\tnew NamedMeasurement(\"attachmentAdapterRadius\", true, true),\n\tnew NamedMeasurement(\"interiorEndMargin\", true, false),\n\tnew NamedMeasurement(\"lipSubdivisions\", false, false),\n\tnew NamedMeasurement(\"subdivisionsPerQuarter\", false, false),\n\tnew NamedMeasurement(\"ballRadius\", true, true),\n\tnew NamedMeasurement(\"ballBaseRadius\", true, true)\n]"
  },
  {
    "path": "src/editor/RenderStyle.ts",
    "content": "enum RenderStyle {\n\tContour,\n\tSolid,\n\tWireframe,\n\tSolidWireframe\n}"
  },
  {
    "path": "src/export/STLExporter.ts",
    "content": "class STLExporter {\n    private readonly buffer: ArrayBuffer;\n    private readonly view: DataView;\n\n    constructor(size: number) {\n        this.buffer = new ArrayBuffer(size);\n        this.view = new DataView(this.buffer, 0, size);\n    }\n\n    private writeVector(offset: number, vector: Vector3) {\n        this.view.setFloat32(offset, vector.z, true);\n        this.view.setFloat32(offset + 4, vector.x, true);\n        this.view.setFloat32(offset + 8, vector.y, true);\n    }\n\n    private writeTriangle(offset: number, triangle: Triangle, scalingFactor: number) {\n        this.writeVector(offset, triangle.normal().times(-1));\n        this.writeVector(offset + 12, triangle.v1.times(scalingFactor));\n        this.writeVector(offset + 24, triangle.v2.times(scalingFactor));\n        this.writeVector(offset + 36, triangle.v3.times(scalingFactor));\n        this.view.setInt16(offset + 48, 0, true);\n    }\n\n    private static fixOpenEdges(triangles: Triangle[]): Triangle[] {\n        var points: Vector3[] = [];\n\n        for (var triangle of triangles) {\n            if (!containsPoint(points, triangle.v1)) {\n                points.push(triangle.v1);\n            }\n            if (!containsPoint(points, triangle.v2)) {\n                points.push(triangle.v2);\n            }\n            if (!containsPoint(points, triangle.v3)) {\n                points.push(triangle.v3);\n            }\n        }\n\n        var result: Triangle[] = [];\n\n        for (var triangle of triangles) {\n            var edge1Hits: number[] = [0];\n            var edge2Hits: number[] = [0];\n            var edge3Hits: number[] = [0];\n\n            var edge1Direction = triangle.v2.minus(triangle.v1);\n            var edge2Direction = triangle.v3.minus(triangle.v2);\n            var edge3Direction = triangle.v1.minus(triangle.v3);\n\n            let edge1LengthSquared = Math.pow(edge1Direction.magnitude(), 2);\n            let edge2LengthSquared = Math.pow(edge2Direction.magnitude(), 2);\n            let edge3LengthSquared = Math.pow(edge3Direction.magnitude(), 2);\n\n            for (var point of points) {\n                var vertex1Relative = point.minus(triangle.v1);\n                var vertex2Relative = point.minus(triangle.v2);\n                var vertex3Relative = point.minus(triangle.v3);\n\n                if (Vector3.isCollinear(edge1Direction, vertex1Relative)) {\n                    let progress = vertex1Relative.dot(edge1Direction) / edge1LengthSquared;\n                    if (progress > 0.0001 && progress < 0.999) {\n                        edge1Hits.push(progress);\n                        continue;\n                    }\n                    continue;\n                }\n\n                if (Vector3.isCollinear(edge2Direction, vertex2Relative)) {\n                    let progress = vertex2Relative.dot(edge2Direction) / edge2LengthSquared;\n                    if (progress > 0.0001 && progress < 0.999) {\n                        edge2Hits.push(progress);\n                        continue;\n                    }\n                    continue;\n                }\n\n                if (Vector3.isCollinear(edge3Direction, vertex3Relative)) {\n                    let progress = vertex3Relative.dot(edge3Direction) / edge3LengthSquared;\n                    if (progress > 0.0001 && progress < 0.999) {\n                        edge3Hits.push(progress);\n                        continue;\n                    }\n                    continue;\n                }\n            }\n\n            if (edge1Hits.length == 1 && edge2Hits.length == 1 && edge3Hits.length == 1) {\n                result.push(triangle);\n                continue;\n            }\n\n            edge1Hits.sort();\n            edge2Hits.sort();\n            edge3Hits.sort();\n\n            for (var i = 0; i < edge1Hits.length - 1; i++) {\n                result.push(new Triangle(\n                    triangle.getOnEdge1(edge1Hits[i]),\n                    triangle.getOnEdge1(edge1Hits[i + 1]),\n                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1])\n                ));\n            }\n            for (var i = 0; i < edge2Hits.length - 1; i++) {\n                result.push(new Triangle(\n                    triangle.getOnEdge2(edge2Hits[i]),\n                    triangle.getOnEdge2(edge2Hits[i + 1]),\n                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1])\n                ));\n            }\n            for (var i = 0; i < edge3Hits.length - 1; i++) {\n                result.push(new Triangle(\n                    triangle.getOnEdge3(edge3Hits[i]),\n                    triangle.getOnEdge3(edge3Hits[i + 1]),\n                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1])\n                ));\n            }\n            if (edge1Hits.length > 1 && edge2Hits.length == 1) {\n                result.push(new Triangle(\n                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1]),\n                    triangle.getOnEdge2(edge2Hits[0]),\n                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1])\n                ));\n            }\n            else if (edge2Hits.length > 1 && edge3Hits.length == 1) {\n                result.push(new Triangle(\n                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1]),\n                    triangle.getOnEdge3(edge3Hits[0]),\n                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1])\n                ));\n            }\n            else if (edge3Hits.length > 1 && edge1Hits.length == 1) {\n                result.push(new Triangle(\n                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1]),\n                    triangle.getOnEdge1(edge1Hits[0]),\n                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1])\n                ));\n            }\n        }\n\n        return result;\n    }\n\n    private static createBuffer(part: Part, measurements: Measurements) {\n        let mesh = new PartMeshGenerator(part, measurements).getMesh();\n        let triangles = STLExporter.fixOpenEdges(mesh.triangles);\n\n        let exporter = new STLExporter(84 + 50 * triangles.length);\n        \n        for (var i = 0; i < 80; i++) {\n            exporter.view.setInt8(i, 0);\n        }\n        \n        var p = 80;\n        exporter.view.setInt32(p, triangles.length, true);\n        p += 4;\n\n        for (let triangle of triangles) {\n            exporter.writeTriangle(p, triangle, measurements.technicUnit);\n            p += 50;\n        }\n\n        return exporter.buffer;\n    }\n    \n    public static saveSTLFile(part: Part, measurements: Measurements, name=\"part\") {\n        let filename = name.toLowerCase().replaceAll(\" \", \"_\") + \".stl\";\n        let blob = new Blob([STLExporter.createBuffer(part, measurements)], { type: \"application/octet-stream\" });\n        let link = document.createElement('a');\n        link.href = window.URL.createObjectURL(blob);\n        link.download = filename;\n        link.click();\n    }\n}"
  },
  {
    "path": "src/export/StudioPartExporter.ts",
    "content": "class StudioPartExporter {\n    private static formatPoint(vector: Vector3): string {\n        return (vector.x * 20).toFixed(4) + \" \" + (-vector.y * 20).toFixed(4) + \" \" + (-vector.z * 20).toFixed(4);\n    }\n\n    private static formatVector(vector: Vector3): string {\n        return (vector.x).toFixed(4) + \" \" + (-vector.y).toFixed(4) + \" \" + (-vector.z).toFixed(4);\n    }\n\n    private static formatConnector(position: Vector3, block: Block, facesForward: boolean): string {\n        let result = \"0 PE_CONN \";\n\n        switch (block.type) {\n            case BlockType.PinHole: result += \"0 2\"; break;\n            case BlockType.AxleHole: result += \"0 6\"; break;\n            case BlockType.Axle: result += \"0 7\"; break;\n            case BlockType.Pin: result += \"0 3\"; break;\n            case BlockType.BallJoint: result += \"1 5\"; break;\n            default: throw new Error(\"Unknown block type: \" + block.type);\n        }\n\n        if (facesForward) {\n            result += \" \"\n                + StudioPartExporter.formatVector(block.right) + \" \"\n                + StudioPartExporter.formatVector(block.forward) + \" \"\n                + StudioPartExporter.formatVector(block.up) + \" \"\n                + StudioPartExporter.formatPoint(position.plus(new Vector3(1, 1, 1).plus(block.forward)).times(0.5));\n        } else {\n            result += \" \"\n                + StudioPartExporter.formatVector(block.right.times(-1)) + \" \"\n                + StudioPartExporter.formatVector(block.forward.times(-1)) + \" \"\n                + StudioPartExporter.formatVector(block.up) + \" \"\n                + StudioPartExporter.formatPoint(position.plus(new Vector3(1, 1, 1).minus(block.forward)).times(0.5));\n        }\n\n         \n        result += \" 0 0 0.8 0 0\\n\";\n        return result;\n    }\n\n    private static createFileContent(part: Part, measurements: Measurements, name: string, filename: string): string {\n        let smallBlocks = part.createSmallBlocks();\n        let mesh = new PartMeshGenerator(part, measurements).getMesh();\n\n        var result: string = `0 FILE ` + filename + `\n0 Description: part\n0 Name: ` + name + `\n0 Author: \n0 BFC CERTIFY CCW\n1 16 0.0000 -0.5000 0.0000 1.0000 0.0000 0.0000 0.0000 1.0000 0.0000 0.0000 0.0000 1.0000 part.obj_grouped\n0 NOFILE\n0 FILE part.obj_grouped\n0 Description: part.obj_grouped\n0 Name: \n0 Author: \n0 ModelType: Part\n0 BFC CERTIFY CCW\n1 16 0.0000 0.0000 0.0000 1.0000 0.0000 0.0000 0.0000 1.0000 0.0000 0.0000 0.0000 1.0000 part.obj\n`;\n\n        for (let position of part.blocks.keys()) {\n            let startBlock = part.blocks.get(position);\n\n            if (startBlock.type == BlockType.Solid) {\n                continue;\n            }\n\n            let previousBlock = part.blocks.getOrNull(position.minus(startBlock.forward));\n            let isFirstInRow = previousBlock == null || previousBlock.orientation != startBlock.orientation || previousBlock.type != startBlock.type;\n\n            if (!isFirstInRow) {\n                continue;\n            }\n\n            let facesForward = false;\n\n            if (startBlock.isAttachment) {\n                for (let x = 0; x <= 1; x++) {\n                    for (let y = 0; y <= 1; y++) {\n                        let supportBlockPosition = position.minus(startBlock.forward).plus(startBlock.right.times(x)).plus(startBlock.up.times(y));\n                        let supportBlock = smallBlocks.getOrNull(supportBlockPosition);\n                        if (supportBlock != null && !supportBlock.isAttachment) {\n                            facesForward = true;\n                            break;\n                        }\n                    }\n                    if (facesForward) {\n                        break;\n                    }\n                }\n            }\n\n            let block = startBlock;\n            let offset = 0;\n            while (true) {\n                let nextBlock = part.blocks.getOrNull(position.plus(startBlock.forward));\n                let isLastInRow = nextBlock == null || nextBlock.orientation != startBlock.orientation || nextBlock.type != startBlock.type;\n\n                if (isLastInRow && offset % 2 == 0 && offset > 0) {\n                    result += StudioPartExporter.formatConnector(position.minus(startBlock.forward), block, facesForward);\n                } else if (offset % 2 == 0) {\n                    result += StudioPartExporter.formatConnector(position, block, facesForward);\n                }\n\n                if (isLastInRow) {\n                    break;\n                }\n\n                offset += 1;\n                position = position.plus(startBlock.forward);\n                block = nextBlock;\n            }\n        }\n\n        result += `\n0 NOFILE\n0 FILE part.obj\n0 Description: part.obj\n0 Name: \n0 Author: \n0 BFC CERTIFY CCW\n`;\n\n        for (let triangle of mesh.triangles) {\n            result += \"3 16 \" + this.formatPoint(triangle.v1) + \" \" + this.formatPoint(triangle.v2) + \" \" + this.formatPoint(triangle.v3) + \"\\n\";\n        }\n\n        result += \"0 NOFILE\\n\";\n        return result;\n    }   \n\n    public static savePartFile(part: Part, measurements: Measurements, name = \"part\") {\n        let filename = name.toLowerCase().replaceAll(\" \", \"_\") + \".part\";\n        let content = StudioPartExporter.createFileContent(part, measurements, name, filename);\n        let link = document.createElement('a');\n        link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);\n        link.download = filename;\n        link.click();\n    }\n}"
  },
  {
    "path": "src/functions.ts",
    "content": "﻿function triangularNumber(n: number): number {\n\treturn n * (n + 1) / 2;\n}\n\nfunction inverseTriangularNumber(s: number): number {\n\treturn Math.floor((Math.floor(Math.sqrt(8 * s + 1)) - 1) / 2);\n}\n\nfunction tetrahedralNumber(n: number): number {\n\treturn n * (n + 1) * (n + 2) / 6;\n}\n\nfunction inverseTetrahedralNumber(s: number): number {\n\tif (s == 0) {\n\t\treturn 0;\n\t}\n\tlet f = Math.pow(1.73205080757 * Math.sqrt(243 * Math.pow(s, 2) - 1) + 27 * s, 1 / 3);\n\treturn Math.floor(f / 2.08008382305 + 0.69336127435 / f - 1);\n}\n\nlet DEG_TO_RAD = Math.PI / 180;\n\nfunction min<T>(iterable: Iterable<T>, selector: (item: T) => number): number {\n\tvar initialized = false;\n\tvar minValue: number;\n\n\tfor (let item of iterable) {\n\t\tlet currentValue = selector(item);\n\t\tif (!initialized || currentValue < minValue) {\n\t\t\tinitialized = true;\n\t\t\tminValue = currentValue;\n\t\t}\n\t}\n\treturn minValue;\n}\n\nfunction sign(a: number): number {\n\tif (a == 0) {\n\t\treturn 0;\n\t} else if (a < 0) {\n\t\treturn -1;\n\t} else {\n\t\treturn 1;\n\t}\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n\treturn a + t * (b - a);\n}\n\nfunction clamp(lower: number, upper: number, value: number) {\n\tif (value > upper) {\n\t\treturn upper;\n\t} else if (value < lower) {\n\t\treturn lower;\n\t} else {\n\t\treturn value;\n\t}\n}\n\nfunction countInArray<T>(items: T[], selector: (item: T) => boolean): number {\n\tvar result = 0;\n\tfor (var item of items) {\n\t\tif (selector(item)) {\n\t\t\tresult++;\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction ease(value: number): number {\n\treturn value < 0.5 ? 2 * value * value : -1 + (4 - 2 * value) * value;\n}\n\nfunction mod(a: number, b: number): number {\n\treturn ((a % b) + b) % b;\n}\n\nfunction containsPoint(list: Vector3[], query: Vector3): boolean {\n\tfor (var item of list) {\n\t\tif (query.equals(item)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}"
  },
  {
    "path": "src/geometry/Matrix4.ts",
    "content": "type NumberArray16 = [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];\n\nclass Matrix4 {\n\telements: NumberArray16;\n\n\tconstructor(elements: NumberArray16) {\n\t\tthis.elements = elements;\n\t}\n\n\tget(i: number, j: number): number {\n\t\treturn this.elements[4 * i + j];\n\t}\n\n\tpublic times(other: Matrix4): Matrix4 {\n\t\tlet result: number[] = [];\n\n\t\tfor (var i = 0; i < 4; i++) {\n\t\t\tfor (var j = 0; j < 4; j++) {\n\t\t\t\tlet element = 0;\n\t\t\t\tfor (var k = 0; k < 4; k++) {\n\t\t\t\t\telement += this.get(i, k) * other.get(k, j);\n\t\t\t\t}\n\t\t\t\tresult.push(element);\n\t\t\t}\n\t\t}\n\n\t\treturn new Matrix4(result as NumberArray16);\n\t}\n\n\tpublic transpose() {\n\t\treturn new Matrix4([\n\t\t\tthis.elements[0], this.elements[4], this.elements[8], this.elements[12],\n\t\t\tthis.elements[1], this.elements[5], this.elements[9], this.elements[13],\n\t\t\tthis.elements[2], this.elements[6], this.elements[10], this.elements[14],\n\t\t\tthis.elements[3], this.elements[7], this.elements[10], this.elements[15]\n\t\t]);\n\t}\n\n\tpublic invert(): Matrix4 {\n\t\t// based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm\n\t\t// via https://github.com/mrdoob/three.js/blob/dev/src/math/Matrix4.js\n\t\tvar el: NumberArray16 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n\n\t\tn11 = this.elements[ 0 ], n21 = this.elements[ 1 ], n31 = this.elements[ 2 ], n41 = this.elements[ 3 ],\n\t\tn12 = this.elements[ 4 ], n22 = this.elements[ 5 ], n32 = this.elements[ 6 ], n42 = this.elements[ 7 ],\n\t\tn13 = this.elements[ 8 ], n23 = this.elements[ 9 ], n33 = this.elements[ 10 ], n43 = this.elements[ 11 ],\n\t\tn14 = this.elements[ 12 ], n24 = this.elements[ 13 ], n34 = this.elements[ 14 ], n44 = this.elements[ 15 ],\n\n\t\tt11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,\n\t\tt12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,\n\t\tt13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,\n\t\tt14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;\n\n\t\tvar det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;\n\n\t\tif (det == 0) {\n\t\t\tthrow new Error(\"Warning: Trying to invert matrix with determinant zero.\");\n\t\t}\n\n\t\tvar detInv = 1 / det;\n\n\t\tel[ 0 ] = t11 * detInv;\n\t\tel[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv;\n\t\tel[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv;\n\t\tel[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv;\n\n\t\tel[ 4 ] = t12 * detInv;\n\t\tel[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv;\n\t\tel[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv;\n\t\tel[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv;\n\n\t\tel[ 8 ] = t13 * detInv;\n\t\tel[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv;\n\t\tel[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv;\n\t\tel[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv;\n\n\t\tel[ 12 ] = t14 * detInv;\n\t\tel[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv;\n\t\tel[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv;\n\t\tel[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv;\n\n\t\treturn new Matrix4(el);\n\t}\n\n\tpublic transformPoint(point: Vector3): Vector3 {\n\t\treturn new Vector3(\n\t\t\tpoint.x * this.elements[0] + point.y * this.elements[4] + point.z * this.elements[8] + this.elements[12],\n\t\t\tpoint.x * this.elements[1] + point.y * this.elements[5] + point.z * this.elements[9] + this.elements[13],\n\t\t\tpoint.x * this.elements[2] + point.y * this.elements[6] + point.z * this.elements[10] + this.elements[14]);\n\t}\n\n\tpublic transformDirection(point: Vector3): Vector3 {\n\t\treturn new Vector3(\n\t\t\tpoint.x * this.elements[0] + point.y * this.elements[4] + point.z * this.elements[8],\n\t\t\tpoint.x * this.elements[1] + point.y * this.elements[5] + point.z * this.elements[9],\n\t\t\tpoint.x * this.elements[2] + point.y * this.elements[6] + point.z * this.elements[10]);\n\t}\n\n\tpublic static getProjection(near = 0.1, far = 1000, fov = 25): Matrix4 {\n\t\tlet aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;\n        return new Matrix4([\n            1 / (Math.tan(fov * DEG_TO_RAD / 2) * aspectRatio), 0, 0, 0,\n            0, 1 / Math.tan(fov * DEG_TO_RAD / 2), 0, 0,\n            0, 0, -(far + near)/(far - near), -1,\n            0, 0, -0.2, 0\n        ]);\n\t}\n\n\tpublic static getOrthographicProjection(far = 1000, size = 5): Matrix4 {\n\t\tlet aspectRatio = gl.canvas.width / gl.canvas.height;\n        return new Matrix4([\n            2 / size / aspectRatio, 0, 0, 0,\n            0, 2 / size, 0, 0,\n            0, 0, -1 / far, 0,\n            0, 0, 0, 1\n        ]);\n\t}\n\n\tpublic static getIdentity(): Matrix4 {\n\t\treturn new Matrix4([\n\t\t\t1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1\n\t\t]);\n\t}\n\t\n\tpublic static getTranslation(vector: Vector3): Matrix4 {\n\t\treturn new Matrix4([\n\t\t\t1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\tvector.x, vector.y, vector.z, 1\n\t\t]);\n\t}\n\n\tpublic static getRotation(euler: Vector3): Matrix4 {\n\t\tlet phi = euler.x * DEG_TO_RAD;\n\t\tlet theta = euler.y * DEG_TO_RAD;\n\t\tlet psi = euler.z * DEG_TO_RAD;\n\t\tlet sin = Math.sin;\n\t\tlet cos = Math.cos;\n\t\treturn new Matrix4([\n\t\t\tcos(theta) * cos(phi), -cos(psi) * sin(phi) + sin(psi) * sin(theta) * cos(phi), sin(psi) * sin(phi) + cos(psi) * sin(theta) * cos(phi), 0,\n\t\t\tcos(theta) * sin(phi), cos(psi)*cos(phi) + sin(psi) * sin(theta) * sin(phi), -sin(psi) * cos(phi) + cos(psi) * sin(theta) * sin(phi), 0,\n\t\t\t-sin(theta), sin(psi) * cos(theta), cos(psi) * cos(theta), 0,\n\t\t\t0, 0, 0, 1\n\t\t]);\n\t}\n}"
  },
  {
    "path": "src/geometry/Mesh.ts",
    "content": "class Mesh {\n    public readonly triangles: Triangle[];\n\n    private vertexBuffer: WebGLBuffer = null;\n    private normalBuffer: WebGLBuffer = null;\n\n    constructor(triangles: Triangle[]) {\n        this.triangles = triangles;\n    }\n\n    public createVertexBuffer(): WebGLBuffer {\n        if (this.vertexBuffer != null) {\n            return this.vertexBuffer;\n        }\n\n        let vertexBuffer = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n        var positions: number[] = [];\n\n        for (let triangle of this.triangles) {\n            this.pushVector(positions, triangle.v1);\n            this.pushVector(positions, triangle.v2);\n            this.pushVector(positions, triangle.v3);\n        }\n\n        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);\n\n        this.vertexBuffer = vertexBuffer;\n        return vertexBuffer;\n    }\n\n    public createNormalBuffer(): WebGLBuffer {\n        if (this.normalBuffer != null) {\n            return this.normalBuffer;\n        }\n\n        let normalBuffer = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);\n        var normals: number[] = [];\n\n        for (let triangle of this.triangles) {\n            if (triangle instanceof TriangleWithNormals) {\n                this.pushVector(normals, triangle.n1);\n                this.pushVector(normals, triangle.n2);\n                this.pushVector(normals, triangle.n3);\n            } else {\n                for (var i = 0; i < 3; i++) {\n                    this.pushVector(normals, triangle.normal());\n                }\n            }\n        }\n\n        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);\n        this.normalBuffer = normalBuffer;\n        return normalBuffer;\n    }\n\n    public createWireframeVertexBuffer(): WebGLBuffer {        \n        let vertexBuffer = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);\n        var positions: number[] = [];\n\n        for (let triangle of this.triangles) {\n            this.pushVector(positions, triangle.v1);\n            this.pushVector(positions, triangle.v2);\n            this.pushVector(positions, triangle.v2);\n            this.pushVector(positions, triangle.v3);\n            this.pushVector(positions, triangle.v3);\n            this.pushVector(positions, triangle.v1);\n        }\n\n        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);\n\n        return vertexBuffer;\n    }\n\n    private pushVector(array: number[], vector: Vector3) {\n        array.push(vector.x);\n        array.push(vector.y);\n        array.push(vector.z);\n    }\n\n    public getVertexCount(): number {\n        return this.triangles.length * 3;\n    }\n}"
  },
  {
    "path": "src/geometry/Quaternion.ts",
    "content": "class Quaternion {\n\tx: number;\n\ty: number;\n\tz: number;\n\tw: number;\n\n\tconstructor(x: number, y: number, z: number, w: number) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t\tthis.w = w;\n\t}\n\n\ttimes(other: Quaternion): Quaternion {\n\t\treturn new Quaternion(this.x * other.x - this.y * other.y - this.z * other.z - this.w * other.w,\n\t\t\tthis.x * other.y + other.x * this.y + this.z * other.w - other.z * this.w,\n\t\t\tthis.x * other.z + other.x * this.z + this.w * other.y - other.w * this.y,\n\t\t\tthis.x * other.w + other.x * this.w + this.y * other.z - other.y * this.z);\n\t}\n\n\ttoMatrix(): Matrix4 {\n\t\treturn new Matrix4([\n\t\t\t1 - 2 * Math.pow(this.z, 2) - 2 * Math.pow(this.w, 2), 2 * this.y * this.z - 2 * this.w * this.x, 2 * this.y * this.w + 2 * this.z * this.x, 0,\n\t\t\t2 * this.y * this.z + 2 * this.w * this.x, 1 - 2 * Math.pow(this.y, 2) - 2 * Math.pow(this.w, 2), 2 * this.z * this.w - 2 * this.y * this.x, 0,\n\t\t\t2 * this.y * this.w - 2 * this.z * this.x, 2 * this.z * this.w + 2 * this.y * this.x, 1 - 2 * Math.pow(this.y, 2) - 2 * Math.pow(this.z, 2), 0,\n\t\t\t0, 0, 0, 1\n\t\t]);\n\t}\n\n\tstatic euler(angles: Vector3): Quaternion {\n\t\treturn Quaternion.angleAxis(angles.z, new Vector3(0, 0, 1))\n\t\t\t.times(Quaternion.angleAxis(angles.y, new Vector3(0, 1, 0)))\n\t\t\t.times(Quaternion.angleAxis(angles.x, new Vector3(1, 0, 0)));\n\t}\n\n\tstatic angleAxis(angle: number, axis: Vector3): Quaternion {\n\t\tlet theta_half = angle * DEG_TO_RAD * 0.5;\n\t\treturn new Quaternion(Math.cos(theta_half), axis.x * Math.sin(theta_half), axis.y * Math.sin(theta_half), axis.z * Math.sin(theta_half));\n\t}\n\n\tstatic identity(): Quaternion {\n\t\treturn new Quaternion(1, 0, 0, 0);\n\t}\n}"
  },
  {
    "path": "src/geometry/Ray.ts",
    "content": "class Ray {\n\tpoint: Vector3;\n\tdirection: Vector3;\n\n\tconstructor(point: Vector3, direction: Vector3) {\n\t\tthis.point = point;\n\t\tthis.direction = direction;\n\t}\n\n\tget(t: number): Vector3 {\n\t\treturn this.point.plus(this.direction.times(t));\n\t}\n\n\tgetDistanceToRay(other: Ray): number {\n\t\tvar normal = this.direction.cross(other.direction).normalized();\n\n\t\tvar d1 = normal.dot(this.point);\n\t\tvar d2 = normal.dot(other.point);\n\n\t\treturn Math.abs(d1 - d2);\n\t}\n\n\tgetClosestToPoint(point: Vector3): number {\n\t\treturn this.direction.dot(this.point.minus(point));\n\t}\n\n\tgetClosestToRay(other: Ray): number {\n\t\tvar connection = this.direction.cross(other.direction).normalized();\n\t\tvar planeNormal = connection.cross(other.direction).normalized();\n\n\t\tvar planeToOrigin = other.point.dot(planeNormal);\n\t\tvar result = (-this.point.dot(planeNormal) + planeToOrigin) / this.direction.dot(planeNormal);\n\t\treturn result;\n\t}\n}"
  },
  {
    "path": "src/geometry/Triangle.ts",
    "content": "class Triangle {\n    public readonly v1: Vector3;\n    public readonly v2: Vector3;\n    public readonly v3: Vector3;\n\n    constructor(v1: Vector3, v2: Vector3, v3: Vector3, flipped = false) {\n        if (flipped) {\n            this.v1 = v2;\n            this.v2 = v1;\n            this.v3 = v3;\n        } else {\n            this.v1 = v1;\n            this.v2 = v2;\n            this.v3 = v3;\n        }\n    }\n\n    public normal(): Vector3 {\n        return this.v3.minus(this.v1).cross(this.v2.minus(this.v1)).normalized();\n    }\n\n    public getOnEdge1(progress: number): Vector3 {\n        return Vector3.interpolate(this.v1, this.v2, progress);\n    }\n\n    public getOnEdge2(progress: number): Vector3 {\n        return Vector3.interpolate(this.v2, this.v3, progress);\n    }\n    \n    public getOnEdge3(progress: number): Vector3 {\n        return Vector3.interpolate(this.v3, this.v1, progress);\n    }\n}"
  },
  {
    "path": "src/geometry/TriangleWithNormals.ts",
    "content": "class TriangleWithNormals extends Triangle {\n\tn1: Vector3;\n\tn2: Vector3;\n\tn3: Vector3;\n\n\tconstructor(v1: Vector3, v2: Vector3, v3: Vector3, n1: Vector3, n2: Vector3, n3: Vector3) {\n\t\tsuper(v1, v2, v3);\n\t\tthis.n1 = n1;\n\t\tthis.n2 = n2;\n\t\tthis.n3 = n3;\n\t}\n}"
  },
  {
    "path": "src/geometry/Vector3.ts",
    "content": "﻿class Vector3 {\n\tpublic readonly x: number;\n\tpublic readonly y: number;\n\tpublic readonly z: number;\n\n\tconstructor(x: number, y: number, z: number) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t}\n\n\tpublic times(factor: number): Vector3 {\n\t\treturn new Vector3(this.x * factor, this.y * factor, this.z * factor);\n\t}\n\n\tpublic plus(other: Vector3): Vector3 {\n\t\treturn new Vector3(this.x + other.x, this.y + other.y, this.z + other.z);\n\t}\n\n\tpublic minus(other: Vector3): Vector3 {\n\t\treturn new Vector3(this.x - other.x, this.y - other.y, this.z - other.z);\n\t}\n\n\tpublic dot(other: Vector3): number {\n\t\treturn this.x * other.x + this.y * other.y + this.z * other.z;\n\t}\n\n\tpublic cross(other: Vector3): Vector3 {\n\t\treturn new Vector3(this.y * other.z - this.z * other.y, this.z * other.x - this.x * other.z, this.x * other.y - this.y * other.x);\n\t}\n\n\tpublic elementwiseMultiply(other: Vector3) {\n\t\treturn new Vector3(this.x * other.x, this.y * other.y, this.z * other.z);\n\t}\n\n\tpublic magnitude(): number {\n\t\treturn Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));\n\t}\n\n\tpublic normalized(): Vector3 {\n\t\treturn this.times(1 / this.magnitude());\n\t}\n\n\tpublic toString(): string {\n\t\treturn \"(\" + this.x + \", \" + this.y + \", \" + this.z + \")\";\n\t}\n\n\tpublic copy(): Vector3 {\n\t\treturn new Vector3(this.x, this.y, this.z);\n\t}\n\n\tpublic equals(other: Vector3): boolean {\n\t\treturn this.x == other.x && this.y == other.y && this.z == other.z;\n\t}\n\n\tpublic floor(): Vector3 {\n\t\treturn new Vector3(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));\n\t}\n\n\tpublic toNumber(): number {\n\t\tlet layer3D = this.x + this.y + this.z;\n\t\tlet layer2D = layer3D - this.y;\n\t\n\t\treturn tetrahedralNumber(layer3D) + triangularNumber(layer2D) + this.x;\n\t}\n\n\tpublic static fromNumber(value: number): Vector3 {\n\t\tlet layer3D = inverseTetrahedralNumber(value);\n\t\tvalue -= tetrahedralNumber(layer3D);\n\t\tlet layer2D = inverseTriangularNumber(value);\n\t\n\t\tlet x = value - triangularNumber(layer2D);\n\t\tlet y = layer3D - layer2D;\n\t\tlet z = layer3D - x - y;\n\t\n\t\treturn new Vector3(x, y, z);\n\t}\n\n\tpublic static zero(): Vector3 {\n\t\treturn new Vector3(0, 0, 0);\n\t}\n\n\tpublic static one(): Vector3 {\n\t\treturn new Vector3(1, 1, 1);\n\t}\n\n\tpublic static lerp(a: Vector3, b: Vector3, progress: number): Vector3 {\n\t\treturn a.plus(b.minus(a).times(progress));\n\t}\n\n\tpublic static isCollinear(a: Vector3, b: Vector3) {\n\t\tvar factor: number | null = null;\n\t\tif (a.x == 0 || b.x == 0) {\n\t\t\tif (Math.abs(a.x + b.x) > 0.001) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tfactor = a.x / b.x;\n\t\t}\n\n\t\tif (a.y == 0 || b.y == 0) {\n\t\t\tif (Math.abs(a.y + b.y) > 0.001) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tif (factor == null) {\n\t\t\t\tfactor = a.y / b.y;\n\t\t\t} else if (Math.abs(factor - a.y / b.y) > 0.001) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tif (a.z == 0 || b.z == 0) {\n\t\t\tif (Math.abs(a.z + b.z) > 0.001) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (factor != null && Math.abs(factor - a.z / b.z) > 0.001) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tpublic static interpolate(a: Vector3, b: Vector3, t: number) {\n\t\treturn a.times(1.0 - t).plus(b.times(t));\n\t}\n}\n\nconst RIGHT_FACE_VERTICES = [\n\tnew Vector3(1, 1, 0),\n\tnew Vector3(1, 1, 1),\n\tnew Vector3(1, 0, 1),\n\tnew Vector3(1, 0, 0)\n];\n\nconst LEFT_FACE_VERTICES = [\n\tnew Vector3(0, 0, 0),\n\tnew Vector3(0, 0, 1),\n\tnew Vector3(0, 1, 1),\n\tnew Vector3(0, 1, 0)\n];\n\nconst UP_FACE_VERTICES = [\n\tnew Vector3(0, 1, 0),\n\tnew Vector3(0, 1, 1),\n\tnew Vector3(1, 1, 1),\n\tnew Vector3(1, 1, 0)\n];\n\nconst DOWN_FACE_VERTICES = [\n\tnew Vector3(1, 0, 0),\n\tnew Vector3(1, 0, 1),\n\tnew Vector3(0, 0, 1),\n\tnew Vector3(0, 0, 0)\n];\n\nconst FORWARD_FACE_VERTICES = [\n\tnew Vector3(1, 0, 1),\n\tnew Vector3(1, 1, 1),\n\tnew Vector3(0, 1, 1),\n\tnew Vector3(0, 0, 1)\n];\n\nconst BACK_FACE_VERTICES = [\n\tnew Vector3(0, 0, 0),\n\tnew Vector3(0, 1, 0),\n\tnew Vector3(1, 1, 0),\n\tnew Vector3(1, 0, 0)\n];\n\nconst FACE_DIRECTIONS = [\n\tnew Vector3(1, 0, 0),\n\tnew Vector3(-1, 0, 0),\n\tnew Vector3(0, 1, 0),\n\tnew Vector3(0, -1, 0),\n\tnew Vector3(0, 0, 1),\n\tnew Vector3(0, 0, -1)\n];"
  },
  {
    "path": "src/geometry/VectorDictionary.ts",
    "content": "﻿class VectorDictionary<T> {\n\tprivate data: {\n\t\t[id: number]: {\n\t\t\t[id: number]: {\n\t\t\t\t[id: number]: T;\n\t\t\t};\n\t\t};\n\t} = {};\n\n\tcontainsKey(key: Vector3): boolean {\n\t\treturn key.x in this.data && key.y in this.data[key.x] && key.z in this.data[key.x][key.y];\n\t}\n\n\tget(key: Vector3): T {\n\t\tif (!this.containsKey(key)) {\n\t\t\tthrow new Error(\"Dictionary does not contain key: \" + key.toString());\n\t\t}\n\t\treturn this.data[key.x][key.y][key.z];\n\t}\n\n\tgetOrNull(key: Vector3): T {\n\t\tif (!this.containsKey(key)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.data[key.x][key.y][key.z];\n\t}\n\n\tset(key: Vector3, value: T) {\n\t\tif (!(key.x in this.data)) {\n\t\t\tthis.data[key.x] = {};\n\t\t}\n\t\tif (!(key.y in this.data[key.x])) {\n\t\t\tthis.data[key.x][key.y] = {};\n\t\t}\n\t\tthis.data[key.x][key.y][key.z] = value;\n\t}\n\n\tremove(key: Vector3) {\n\t\tif (key.x in this.data && key.y in this.data[key.x] && key.z in this.data[key.x][key.y]) {\n\t\t\tdelete this.data[key.x][key.y][key.z];\n\t\t}\n\t}\n\n\tclear() {\n\t\tthis.data = {};\n\t}\n\n\t*keys(): IterableIterator<Vector3> {\n\t\tfor (let x in this.data) {\n\t\t\tfor (let y in this.data[x]) {\n\t\t\t\tfor (let z in this.data[x][y]) {\n\t\t\t\t\tyield new Vector3(parseInt(x), parseInt(y), parseInt(z));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t*values(): IterableIterator<T> {\n\t\tfor (let x in this.data) {\n\t\t\tfor (let y in this.data[x]) {\n\t\t\t\tfor (let z in this.data[x][y]) {\n\t\t\t\t\tyield this.data[x][y][z];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tany(): boolean {\n\t\tfor (let x in this.data) {\n\t\t\tfor (let y in this.data[x]) {\n\t\t\t\tfor (let z in this.data[x][y]) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}"
  },
  {
    "path": "src/measurements.ts",
    "content": "class Measurements {\n\ttechnicUnit = 8;\n\n\tedgeMargin = 0.2 / this.technicUnit;\n\tinteriorRadius = 3.2 / this.technicUnit;\n\tpinHoleRadius = 2.475 / this.technicUnit;\n\tpinHoleOffset = 0.89 / this.technicUnit;\n\taxleHoleSize = 1.01 / this.technicUnit;\n\tpinRadius = 2.315 / this.technicUnit;\n\tballBaseRadius = 1.6 / this.technicUnit;\n\tballRadius = 3.0 / this.technicUnit;\n\tpinLipRadius = 0.17 / this.technicUnit;\n\taxleSizeInner = 0.86 / this.technicUnit;\n\taxleSizeOuter = 2.15 / this.technicUnit;\n\tattachmentAdapterSize = 0.4 / this.technicUnit;\n\tattachmentAdapterRadius = 3 / this.technicUnit;\n\tinteriorEndMargin = 0.2 / this.technicUnit;\n\n\tlipSubdivisions = 6;\n\n\tsubdivisionsPerQuarter = 8;\n\n\tpublic enforceConstraints() {\n\t\tthis.lipSubdivisions = Math.max(2, Math.ceil(this.lipSubdivisions));\n\t\tthis.subdivisionsPerQuarter = Math.max(2, Math.ceil(this.subdivisionsPerQuarter / 2) * 2);\n\t\tthis.edgeMargin = Math.min(0.49, this.edgeMargin);\n\t\tthis.interiorRadius = Math.min(0.5 - this.edgeMargin, this.interiorRadius);\n\t\tthis.interiorEndMargin = Math.min(0.49, this.interiorEndMargin);\n\t\tthis.pinHoleRadius = Math.min(this.interiorRadius, this.pinHoleRadius);\n\t\tthis.pinHoleOffset = Math.min(0.5 - this.edgeMargin, this.pinHoleOffset);\n\t\tthis.axleHoleSize = Math.min(this.interiorRadius / 2, this.axleHoleSize);\n\t\tthis.pinRadius = Math.min(0.5 - this.edgeMargin, this.pinRadius);\n\t\tthis.axleSizeOuter = Math.min(Math.sqrt(Math.pow(Math.min(0.5 - this.edgeMargin, this.attachmentAdapterRadius), 2.0) - Math.pow(this.axleSizeInner, 2.0)), this.axleSizeOuter);\n\t\tthis.axleSizeInner = Math.min(this.axleSizeOuter, this.axleSizeInner);\n\t\tthis.attachmentAdapterSize = Math.min((0.5 - this.edgeMargin) / 2, this.attachmentAdapterSize);\n\t\tthis.ballBaseRadius = Math.min(this.ballBaseRadius, this.interiorRadius);\n\t\tthis.ballRadius = Math.max(Math.min(this.ballRadius, 0.5 - this.attachmentAdapterSize), this.ballBaseRadius);\n\t}\n}\n\nconst DEFAULT_MEASUREMENTS = new Measurements();"
  },
  {
    "path": "src/model/Block.ts",
    "content": "﻿class Block {\n\tpublic readonly orientation: Orientation;\n\tpublic readonly type: BlockType;\n\tpublic rounded: boolean;\n\n\tpublic readonly right: Vector3;\n\tpublic readonly up: Vector3;\n\tpublic readonly forward: Vector3;\n\tpublic readonly isAttachment: boolean;\n\n\tconstructor(orientation: Orientation, type: BlockType, rounded: boolean) {\n\t\tthis.orientation = orientation;\n\t\tthis.type = type;\n\t\tthis.rounded = rounded;\n\n\t\tthis.right = RIGHT[this.orientation];\n\t\tthis.up = UP[this.orientation];\n\t\tthis.forward = FORWARD[this.orientation];\n\t\tthis.isAttachment = this.type == BlockType.Pin || this.type == BlockType.Axle || this.type == BlockType.BallJoint;\n\t}\n}\n"
  },
  {
    "path": "src/model/Part.ts",
    "content": "///<reference path=\"../geometry/Vector3.ts\" />\n\nlet CUBE = [\n\tnew Vector3(0, 0, 0),\n\tnew Vector3(0, 0, 1),\n\tnew Vector3(0, 1, 0),\n\tnew Vector3(0, 1, 1),\n\tnew Vector3(1, 0, 0),\n\tnew Vector3(1, 0, 1),\n\tnew Vector3(1, 1, 0),\n\tnew Vector3(1, 1, 1)\n];\n\nclass Part {\n\tpublic blocks: VectorDictionary<Block> = new VectorDictionary<Block>();\n\n\tpublic createSmallBlocks(): VectorDictionary<SmallBlock> {\n\t\tvar result = new VectorDictionary<SmallBlock>();\n\n\t\tfor (let position of this.blocks.keys()) {\n\t\t\tlet block = this.blocks.get(position);\n\t\t\tfor (let local of CUBE) {\n\t\t\t\tif (block.forward.dot(local) == 1) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tresult.set(position.plus(local), SmallBlock.createFromLocalCoordinates(block.right.dot(local), block.up.dot(local), position.plus(local), block));\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tpublic isSmallBlockFree(position: Vector3): boolean {\n\t\tfor (let local of CUBE) {\n\t\t\tif (!this.blocks.containsKey(position.minus(local))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar block = this.blocks.get(position.minus(local));\n\t\t\tif (block.forward.dot(local) == 1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tpublic clearSingle(position: Vector3) {\n\t\tfor (let local of CUBE) {\n\t\t\tif (!this.blocks.containsKey(position.minus(local))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar block = this.blocks.get(position.minus(local));\n\t\t\tif (block.forward.dot(local) != 1) {\n\t\t\t\tthis.blocks.remove(position.minus(local));\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic clearBlock(position: Vector3, orientation: Orientation) {\n\t\tfor (let local of CUBE) {\n\t\t\tif (FORWARD[orientation].dot(local) != 1) {\n\t\t\t\tthis.clearSingle(position.plus(local));\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic isBlockPlaceable(position: Vector3, orientation: Orientation, doubleSize: boolean): boolean {\n\t\tfor (let local of CUBE) {\n\t\t\tif (!doubleSize && FORWARD[orientation].dot(local) == 1) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!this.isSmallBlockFree(position.plus(local))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tpublic placeBlockForced(position: Vector3, block: Block) {\n\t\tthis.clearBlock(position, block.orientation);\n\t\tthis.blocks.set(position, block);\n\t}\n\n\tpublic toString(): string {\n\t\tvar result = \"\";\n\n\t\tif (!this.blocks.any()) {\n\t\t\treturn result;\n\t\t}\n\n\t\tvar origin = new Vector3(min(this.blocks.keys(), p => p.x), min(this.blocks.keys(), p => p.y), min(this.blocks.keys(), p => p.z));\n\n\t\tfor (let position of this.blocks.keys()) {\n\t\t\tresult += position.minus(origin).toNumber().toString(16).toLowerCase();\n\n\t\t\tlet block = this.blocks.get(position);\n\t\t\tlet orientationAndRounded = block.orientation == Orientation.X ? \"x\" : (block.orientation == Orientation.Y ? \"y\" : \"z\");\n\t\t\tif (!block.rounded) {\n\t\t\t\torientationAndRounded = orientationAndRounded.toUpperCase();\n\t\t\t}\n\t\t\tresult += orientationAndRounded;\n\t\t\tresult += block.type.toString();\n\t\t}\n\t\treturn result;\n\t}\n\n\tpublic static fromString(s: string): Part {\n\t\tlet XYZ = \"xyz\";\n\n\t\tlet part = new Part();\n\n\t\tvar p = 0;\n\t\twhile (p < s.length) {\n\t\t\tvar chars = 1;\n\t\t\twhile (XYZ.indexOf(s[p + chars].toLowerCase()) == -1) {\n\t\t\t\tchars++;\n\t\t\t}\n\t\t\t\n\t\t\tlet position = Vector3.fromNumber(parseInt(s.substr(p, chars), 16));\n\t\t\tp += chars;\n\t\t\tlet orientationString = s[p].toString().toLowerCase();\n\t\t\tlet orientation = orientationString == \"x\" ? Orientation.X : (orientationString == \"y\" ? Orientation.Y : Orientation.Z);\n\t\t\tlet rounded = s[p].toLowerCase() == s[p];\n\t\t\tlet type = parseInt(s[p + 1]) as BlockType;\n\n\t\t\tpart.blocks.set(position, new Block(orientation, type, rounded));\n\t\t\tp += 2;\n\t\t}\n\t\treturn part;\n\t}\n\n\tprivate getBoundingBox(): [Vector3, Vector3] {\n\t\tlet defaultPosition = this.blocks.keys().next().value;\n\t\tvar minX = defaultPosition.x;\n\t\tvar minY = defaultPosition.y;\n\t\tvar minZ = defaultPosition.z;\n\t\tvar maxX = defaultPosition.x;\n\t\tvar maxY = defaultPosition.y;\n\t\tvar maxZ = defaultPosition.z;\n\n\t\tfor (var position of this.blocks.keys()) {\n\t\t\tvar forward = this.blocks.get(position).forward;\n\t\t\tif (position.x < minX) {\n\t\t\t\tminX = position.x;\n\t\t\t}\n\t\t\tif (position.y < minY) {\n\t\t\t\tminY = position.y;\n\t\t\t}\n\t\t\tif (position.z < minZ) {\n\t\t\t\tminZ = position.z;\n\t\t\t}\n\t\t\tif (position.x + (1.0 - forward.x) > maxX) {\n\t\t\t\tmaxX = position.x + (1.0 - forward.x);\n\t\t\t}\n\t\t\tif (position.y + (1.0 - forward.y) > maxY) {\n\t\t\t\tmaxY = position.y + (1.0 - forward.y);\n\t\t\t}\n\t\t\tif (position.z + (1.0 - forward.z) > maxZ) {\n\t\t\t\tmaxZ = position.z + (1.0 - forward.z);\n\t\t\t}\n\t\t}\n\t\treturn [new Vector3(minX, minY, minZ), new Vector3(maxX, maxY, maxZ)];\n\t}\n\n\tpublic getCenter(): Vector3 {\n\t\tif (!this.blocks.any()) {\n\t\t\treturn Vector3.zero();\n\t\t}\n\n\t\tvar boundingBox = this.getBoundingBox();\n\t\tvar min = boundingBox[0];\n\t\tvar max = boundingBox[1];\n\t\t\n\t\treturn min.plus(max).plus(Vector3.one()).times(0.5);\n\t}\n\n\tpublic getSize() {\n\t\tvar boundingBox = this.getBoundingBox();\n\t\tvar min = boundingBox[0];\n\t\tvar max = boundingBox[1];\n\t\treturn Math.max(max.x - min.x, Math.max(max.y - min.y, max.z - min.z)) + 1;\n\t}\n}"
  },
  {
    "path": "src/model/PerpendicularRoundedAdaper.ts",
    "content": "class PerpendicularRoundedAdapter {\n\tpublic isVertical: boolean;\n\tpublic neighbor: SmallBlock;\n\tpublic directionToNeighbor: Vector3;\n\tpublic facesForward: boolean;\n\tpublic sourceBlock: SmallBlock;\n}"
  },
  {
    "path": "src/model/SmallBlock.ts",
    "content": "﻿class SmallBlock extends Block {\n\tpublic readonly quadrant: Quadrant;\n\tpublic readonly position: Vector3;\n\tpublic hasInterior: boolean;\n\n\tpublic perpendicularRoundedAdapter: PerpendicularRoundedAdapter = null;\n\n\tpublic readonly localX: number;\n\tpublic readonly localY: number;\n\tpublic readonly directionX: number;\n\tpublic readonly directionY: number;\n\tpublic readonly horizontal: Vector3;\n\tpublic readonly vertical: Vector3;\n\n\tconstructor(quadrant: Quadrant, positon: Vector3, source: Block) {\n\t\tsuper(source.orientation, source.type, source.rounded);\n\t\tthis.quadrant = quadrant;\n\t\tthis.position = positon;\n\n\t\tthis.hasInterior = source.type != BlockType.Solid;\n\n\t\tthis.localX = localX(this.quadrant);\n\t\tthis.localY = localY(this.quadrant);\n\t\tthis.directionX = this.localX == 1 ? 1 : -1;\n\t\tthis.directionY = this.localY == 1 ? 1 : -1;\n\t\tthis.horizontal = this.localX == 1 ? RIGHT[this.orientation] : LEFT[this.orientation];\n\t\tthis.vertical = this.localY == 1 ? UP[this.orientation] : DOWN[this.orientation];\n\t}\n\n\tpublic static createFromLocalCoordinates(localX: number, localY: number, position: Vector3, source: Block) {\n\t\treturn new SmallBlock(SmallBlock.getQuadrantFromLocal(localX, localY), position, source);\n\t}\n\t\n\tpublic odd(): boolean {\n\t\treturn this.quadrant == Quadrant.BottomRight || this.quadrant == Quadrant.TopLeft;\n\t}\n\n\tprivate static getQuadrantFromLocal(x: number, y: number): Quadrant {\n\t\tif (x == 0) {\n\t\t\tif (y == 0) {\n\t\t\t\treturn Quadrant.BottomLeft;\n\t\t\t} else {\n\t\t\t\treturn Quadrant.TopLeft;\n\t\t\t}\n\t\t} else {\n\t\t\tif (y == 0) {\n\t\t\t\treturn Quadrant.BottomRight;\n\t\t\t} else {\n\t\t\t\treturn Quadrant.TopRight;\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic getOnCircle(angle: number, radius = 1): Vector3 {\n\t\treturn this.right.times(Math.sin(angle + getAngle(this.quadrant)) * radius).plus(\n\t\t\tthis.up.times(Math.cos(angle + getAngle(this.quadrant)) * radius));\n\t}\n}\n "
  },
  {
    "path": "src/model/TinyBlock.ts",
    "content": "﻿class TinyBlock extends SmallBlock {\n\tpublic exteriorMergedBlocks = 1;\n\tpublic isExteriorMerged = false;\n\t\n\tpublic interiorMergedBlocks = 1;\n\tpublic isInteriorMerged = false;\n\n\tprivate readonly visibleFaces: [boolean, boolean, boolean, boolean, boolean, boolean] = null;\n\n\tpublic readonly angle: number;\n\tpublic readonly isCenter: boolean;\n\tpublic readonly smallBlockPosition: Vector3;\n\n\tconstructor(position: Vector3, source: SmallBlock) {\n\t\tsuper(source.quadrant, position, source);\n\t\tthis.visibleFaces = [true, true, true, true, true, true];\n\t\tthis.perpendicularRoundedAdapter = source.perpendicularRoundedAdapter;\n\n\t\tthis.angle = getAngle(this.quadrant);\n\t\tthis.smallBlockPosition = new Vector3(\n\t\t\tMath.floor((position.x + 1) / 3),\n\t\t\tMath.floor((position.y + 1) / 3),\n\t\t\tMath.floor((position.z + 1) / 3));\n\t\tvar localPosition = position.minus(this.smallBlockPosition.times(3));\n\t\tthis.isCenter = localPosition.dot(this.up) == 0 && localPosition.dot(this.right) == 0;\n\t}\n\n\tpublic getCylinderOrigin(meshGenerator: MeshGenerator): Vector3 {\n\t\treturn this.forward.times(meshGenerator.tinyIndexToWorld(this.forward.dot(this.position)))\n\t\t\t.plus(this.right.times((this.smallBlockPosition.dot(this.right) + (1 - this.localX)) * 0.5))\n\t\t\t.plus(this.up.times((this.smallBlockPosition.dot(this.up) + (1 - this.localY)) * 0.5));\n\t}\n\n\tpublic getExteriorDepth(meshGenerator: MeshGenerator): number {\n\t\treturn meshGenerator.tinyIndexToWorld(this.forward.dot(this.position) + this.exteriorMergedBlocks) - meshGenerator.tinyIndexToWorld(this.forward.dot(this.position));\n\t}\n\t\n\tpublic getInteriorDepth(meshGenerator: MeshGenerator): number {\n\t\treturn meshGenerator.tinyIndexToWorld(this.forward.dot(this.position) + this.interiorMergedBlocks) - meshGenerator.tinyIndexToWorld(this.forward.dot(this.position));\n\t}\n\n\tpublic isFaceVisible(direction: Vector3): boolean {\n\t\tif (direction.x > 0 && direction.y == 0 && direction.z == 0) {\n\t\t\treturn this.visibleFaces[0];\n\t\t} else if (direction.x < 0 && direction.y == 0 && direction.z == 0) {\n\t\t\treturn this.visibleFaces[1];\n\t\t} else if (direction.x == 0 && direction.y > 0 && direction.z == 0) {\n\t\t\treturn this.visibleFaces[2];\n\t\t} else if (direction.x == 0 && direction.y < 0 && direction.z == 0) {\n\t\t\treturn this.visibleFaces[3];\n\t\t} else if (direction.x == 0 && direction.y == 0 && direction.z > 0) {\n\t\t\treturn this.visibleFaces[4];\n\t\t} else if (direction.x == 0 && direction.y == 0 && direction.z < 0) {\n\t\t\treturn this.visibleFaces[5];\n\t\t} else {\n\t\t\tthrow new Error(\"Invalid direction vector.\");\n\t\t}\n\t}\n\n\tpublic hideFace(direction: Vector3) {\n\t\tif (direction.x > 0 && direction.y == 0 && direction.z == 0) {\n\t\t\tthis.visibleFaces[0] = false;\n\t\t} else if (direction.x < 0 && direction.y == 0 && direction.z == 0) {\n\t\t\tthis.visibleFaces[1] = false;\n\t\t} else if (direction.x == 0 && direction.y > 0 && direction.z == 0) {\n\t\t\tthis.visibleFaces[2] = false;\n\t\t} else if (direction.x == 0 && direction.y < 0 && direction.z == 0) {\n\t\t\tthis.visibleFaces[3] = false;\n\t\t} else if (direction.x == 0 && direction.y == 0 && direction.z > 0) {\n\t\t\tthis.visibleFaces[4] = false;\n\t\t} else if (direction.x == 0 && direction.y == 0 && direction.z < 0) {\n\t\t\tthis.visibleFaces[5] = false;\n\t\t} else {\n\t\t\tthrow new Error(\"Invalid direction vector.\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/model/enums/BlockType.ts",
    "content": "﻿enum BlockType {\n\tSolid,\n\tPinHole,\n\tAxleHole,\n\tPin,\n\tAxle,\n\tBallJoint,\n\tBallSocket\n}\n\nconst BLOCK_TYPE = {\n\t\"solid\": BlockType.Solid,\n\t\"pinhole\": BlockType.PinHole,\n\t\"axlehole\": BlockType.AxleHole,\n\t\"pin\": BlockType.Pin,\n\t\"axle\": BlockType.Axle,\n\t\"balljoint\": BlockType.BallJoint,\n\t\"ballsocket\": BlockType.BallSocket\n}"
  },
  {
    "path": "src/model/enums/Orientation.ts",
    "content": "﻿enum Orientation {\n\tX = 0,\n\tY = 1,\n\tZ = 2\n}\n\nconst ORIENTATION = {\n\t\"x\": Orientation.X,\n\t\"y\": Orientation.Y,\n\t\"z\": Orientation.Z\n};\n\nconst FORWARD = {\n\t0: new Vector3(1, 0, 0),\n\t1: new Vector3(0, 1, 0),\n\t2: new Vector3(0, 0, 1)\n};\n\nconst RIGHT = {\n\t0: new Vector3(0, 1, 0),\n\t1: new Vector3(0, 0, 1),\n\t2: new Vector3(1, 0, 0)\n};\n\nconst UP = {\n\t0: new Vector3(0, 0, 1),\n\t1: new Vector3(1, 0, 0),\n\t2: new Vector3(0, 1, 0)\n}\n\nconst LEFT = {\n\t0: new Vector3(0, -1, 0),\n\t1: new Vector3(0, 0, -1),\n\t2: new Vector3(-1, 0, 0)\n};\n\nconst DOWN = {\n\t0: new Vector3(0, 0, -1),\n\t1: new Vector3(-1, 0, 0),\n\t2: new Vector3(0, -1, 0)\n}"
  },
  {
    "path": "src/model/enums/Quadrant.ts",
    "content": "﻿enum Quadrant {\n\tTopLeft,\n\tTopRight,\n\tBottomLeft,\n\tBottomRight\n}\n\nfunction localX(quadrant: Quadrant): number {\n\treturn (quadrant == Quadrant.TopRight || quadrant == Quadrant.BottomRight) ? 1 : 0;\n}\n\nfunction localY(quadrant: Quadrant): number {\n\treturn (quadrant == Quadrant.TopRight || quadrant == Quadrant.TopLeft) ? 1 : 0;\n}\n\nfunction getAngle(quadrant: Quadrant): number {\n\tswitch (quadrant) {\n\t\tcase Quadrant.TopRight:\n\t\t\treturn 0;\n\t\tcase Quadrant.BottomRight:\n\t\t\treturn 90 * DEG_TO_RAD;\n\t\tcase Quadrant.BottomLeft:\n\t\t\treturn 180 * DEG_TO_RAD;\n\t\tcase Quadrant.TopLeft:\n\t\t\treturn 270 * DEG_TO_RAD;\n\t}\n\tthrow new Error(\"Unknown quadrant: \" + quadrant);\n}"
  },
  {
    "path": "src/rendering/Camera.ts",
    "content": "class Camera {\n    public renderers: Renderer[] = [];\n\n    public transform: Matrix4 = Matrix4.getIdentity();\n\n    public size = 5;\n\n    public frameBuffer: WebGLFramebuffer;\n    public normalTexture: WebGLTexture;\n    public depthTexture: WebGLTexture;\n\n    public clearColor: Vector3 = new Vector3(0.95, 0.95, 0.95);\n\n    public supersample: number = 1;\n\n    constructor(canvas: HTMLCanvasElement, supersample = 1) {\n        gl = canvas.getContext(\"webgl\") as WebGLRenderingContext;\n\n\t\tif (gl == null) {\n\t\t\tthrow new Error(\"WebGL is not supported.\");\n        }\n        gl.getExtension('WEBGL_depth_texture');\n\n        this.supersample = supersample;        \n        canvas.width = Math.round(canvas.clientWidth * window.devicePixelRatio) * this.supersample;\n        canvas.height = Math.round(canvas.clientHeight * window.devicePixelRatio) * this.supersample;\n        this.createBuffers();\n    }\n\n    private createBuffers() {\n        this.normalTexture = gl.createTexture();\n        gl.bindTexture(gl.TEXTURE_2D, this.normalTexture);\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n        \n        this.depthTexture = gl.createTexture();\n        gl.bindTexture(gl.TEXTURE_2D, this.depthTexture);\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, gl.canvas.width, gl.canvas.height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n        \n        this.frameBuffer = gl.createFramebuffer();\n        gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);\n        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.normalTexture, 0);\n        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);\n\n        gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n    }\n\n    public getProjectionMatrix(): Matrix4 {\n       return Matrix4.getOrthographicProjection(30, this.size);\n    }\n\n    public render() {\n        gl.clearColor(this.clearColor.x, this.clearColor.y, this.clearColor.z, 1.0);\n        gl.colorMask(true, true, true, true);\n        gl.clearDepth(1.0);\n        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n        gl.enable(gl.DEPTH_TEST);\n        gl.depthFunc(gl.LEQUAL);\n        gl.enable(gl.CULL_FACE);\n        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n        gl.enable(gl.BLEND);\n        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\t\tfor (var renderer of this.renderers) {\n\t\t\trenderer.render(this);\n        }\n\n        gl.colorMask(false, false, false, true);\n        gl.clearColor(0, 0, 0, 1);\n        gl.clear(gl.COLOR_BUFFER_BIT);\n    }\n    \n    public onResize() {\n        gl.canvas.width = Math.round((gl.canvas as HTMLCanvasElement).clientWidth * window.devicePixelRatio) * this.supersample;\n        gl.canvas.height = Math.round((gl.canvas as HTMLCanvasElement).clientHeight * window.devicePixelRatio) * this.supersample;\n        this.createBuffers();\n        this.render();\n    }\n\n    public getScreenToWorldRay(event: MouseEvent): Ray {\n        var rect = (gl.canvas as HTMLCanvasElement).getBoundingClientRect();\n        var x = event.clientX - rect.left;\n        var y = event.clientY - rect.top;\n\n        x = x / (gl.canvas as HTMLCanvasElement).clientWidth * 2 - 1;\n        y = y / (gl.canvas as HTMLCanvasElement).clientHeight * -2 + 1;\n\n        let viewSpacePoint = new Vector3(x * this.size / 2 * gl.drawingBufferWidth / gl.drawingBufferHeight, y * this.size / 2, 0);\n        let viewSpaceDirection = new Vector3(0, 0, -1);\n        let inverseCameraTransform = this.transform.invert();\n\n        return new Ray(inverseCameraTransform.transformPoint(viewSpacePoint), inverseCameraTransform.transformDirection(viewSpaceDirection));\n    }\n}"
  },
  {
    "path": "src/rendering/ContourPostEffect.ts",
    "content": "class ContourPostEffect implements Renderer {\n\n\tprivate shader: Shader;\n\tprivate vertices: WebGLBuffer;\n\t\n\tpublic enabled: boolean = true;\n    \n    constructor() {\n        this.shader = new Shader(COUNTOUR_VERTEX, CONTOUR_FRAGMENT);\n\n\t\tthis.shader.setAttribute(\"vertexPosition\");\n\t\tthis.shader.setUniform(\"normalTexture\");\n\t\tthis.shader.setUniform(\"depthTexture\");\n\t\tthis.shader.setUniform(\"resolution\");\n\t\t\n\t\tthis.vertices = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);\n\t\tvar positions: number[] = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];\n\t\tgl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);\n    }\n\n    public render(camera: Camera) {\n\t\tif (!this.enabled) {\n\t\t\treturn;\n\t\t}\n\n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);\n        gl.vertexAttribPointer(this.shader.attributes[\"vertexPosition\"], 2, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.shader.attributes[\"vertexPosition\"]);\n      \n        gl.useProgram(this.shader.program);\n\t\t\n\t\tgl.depthFunc(gl.ALWAYS);\n\t\tgl.depthMask(false);\n\n\t\tgl.activeTexture(gl.TEXTURE0);\n\t\tgl.bindTexture(gl.TEXTURE_2D, camera.normalTexture);\n\t\tgl.uniform1i(this.shader.attributes[\"normalTexture\"], 0);\t\t\n\t\tgl.activeTexture(gl.TEXTURE1);\n\t\tgl.bindTexture(gl.TEXTURE_2D, camera.depthTexture);\n\t\tgl.uniform1i(this.shader.attributes[\"depthTexture\"], 1);\n\t\tgl.uniform2f(this.shader.attributes[\"resolution\"], gl.drawingBufferWidth, gl.drawingBufferHeight);\n\t\t\n        gl.drawArrays(gl.TRIANGLES, 0, 6);\n\t\tgl.depthFunc(gl.LEQUAL);\n\t\tgl.depthMask(true);\n    }\n}"
  },
  {
    "path": "src/rendering/MeshRenderer.ts",
    "content": "class MeshRenderer implements Renderer {\n    private shader: Shader;\n\n    private vertices: WebGLBuffer;\n    private normals: WebGLBuffer;\n\n    private vertexCount: number;\n    public transform: Matrix4;\n    public color: Vector3 = new Vector3(1, 0, 0);\n    public alpha: number = 1;\n\n    public enabled: boolean = true;\n\n    constructor() {\n        this.shader = new Shader(VERTEX_SHADER, FRAGMENT_SHADER);\n\n        this.shader.setAttribute(\"vertexPosition\");\n        this.shader.setAttribute(\"normal\");\n        this.shader.setUniform(\"projectionMatrix\");\n        this.shader.setUniform(\"modelViewMatrix\");\n        this.shader.setUniform(\"albedo\");\n        this.shader.setUniform(\"alpha\");\n\n        this.transform = Matrix4.getIdentity();\n    }\n\n    public setMesh(mesh: Mesh) {\n        this.vertexCount = mesh.getVertexCount();\n        this.vertices = mesh.createVertexBuffer();\n        this.normals = mesh.createNormalBuffer();\n    }\n\n    public render(camera: Camera) {\n        if (!this.enabled) {\n            return;\n        }\n\n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);\n        gl.vertexAttribPointer(this.shader.attributes[\"vertexPosition\"], 3, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.shader.attributes[\"vertexPosition\"]);\n        \n        gl.bindBuffer(gl.ARRAY_BUFFER, this.normals);\n        gl.vertexAttribPointer(this.shader.attributes[\"normal\"], 3, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.shader.attributes[\"normal\"]);\n      \n        gl.useProgram(this.shader.program);\n      \n        gl.uniformMatrix4fv(this.shader.attributes[\"projectionMatrix\"], false, camera.getProjectionMatrix().elements);\n        gl.uniformMatrix4fv(this.shader.attributes[\"modelViewMatrix\"], false, this.transform.times(camera.transform).elements);\n        gl.uniform3f(this.shader.attributes[\"albedo\"], this.color.x, this.color.y, this.color.z);\n        gl.uniform1f(this.shader.attributes[\"alpha\"], this.alpha);\n      \n        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);\n    }\n}"
  },
  {
    "path": "src/rendering/NormalDepthRenderer.ts",
    "content": "class NormalDepthRenderer implements Renderer {\n    private shader: Shader;\n\n    private vertices: WebGLBuffer;\n    private normals: WebGLBuffer;\n\n    public transform: Matrix4;\n\n    private vertexCount: number;\n\n    public enabled: boolean = true;\n\n    constructor() {\n        this.prepareShaders();\n        this.transform = Matrix4.getIdentity();\n    }\n\n    private prepareShaders() {\n        this.shader = new Shader(VERTEX_SHADER, NORMAL_FRAGMENT_SHADER);\n        this.shader.setAttribute(\"vertexPosition\");\n        this.shader.setAttribute(\"normal\");\n        this.shader.setUniform(\"projectionMatrix\");\n        this.shader.setUniform(\"modelViewMatrix\");\n    }\n\n    public setMesh(mesh: Mesh) {\n        this.vertexCount = mesh.getVertexCount();\n        this.vertices = mesh.createVertexBuffer();\n        this.normals = mesh.createNormalBuffer();\n    }\n\n    public render(camera: Camera) {\n        if (!this.enabled) {\n            return;\n        }\n\n        gl.bindFramebuffer(gl.FRAMEBUFFER, camera.frameBuffer);\n        gl.bindTexture(gl.TEXTURE_2D, null);\n\n        gl.clearColor(0.5, 0.5, -1.0, 1.0);\n        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n        \n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);\n        gl.vertexAttribPointer(this.shader.attributes[\"vertexPosition\"], 3, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.shader.attributes[\"vertexPosition\"]);\n        \n        gl.bindBuffer(gl.ARRAY_BUFFER, this.normals);\n        gl.vertexAttribPointer(this.shader.attributes[\"normal\"], 3, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.shader.attributes[\"normal\"]);\n      \n        gl.useProgram(this.shader.program);\n      \n        gl.uniformMatrix4fv(this.shader.attributes[\"projectionMatrix\"], false, camera.getProjectionMatrix().elements);\n        gl.uniformMatrix4fv(this.shader.attributes[\"modelViewMatrix\"], false, this.transform.times(camera.transform).elements);\n      \n        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);\n        gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n    }\n}"
  },
  {
    "path": "src/rendering/Renderer.ts",
    "content": "interface Renderer {\n\trender(camera: Camera);\n}"
  },
  {
    "path": "src/rendering/Shader.ts",
    "content": "class Shader {\n\tpublic program: WebGLShader;\n\tpublic attributes: {[id: string]: number } = {};\n\n\tprivate loadShader(type: number, source: string): WebGLShader {\n        let shader = gl.createShader(type);      \n        gl.shaderSource(shader, source);      \n        gl.compileShader(shader);\n\n        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n            var lines = source.split(\"\\n\");\n            for (var index = 0; index < lines.length; index++) {\n                console.log((index + 1) + \": \" + lines[index]);\n            }\n            throw new Error('An error occurred compiling the shaders: ' +  gl.getShaderInfoLog(shader));\n        }\n\n        return shader;\n    }\n\n    constructor(vertexSource: string, fragmentSource: string) {\n        const vertexShader = this.loadShader(gl.VERTEX_SHADER, vertexSource);\n        const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fragmentSource);\n      \n        this.program = gl.createProgram();\n        gl.attachShader(this.program, vertexShader);\n        gl.attachShader(this.program, fragmentShader);\n        gl.linkProgram(this.program);\n      \n        if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {\n            throw new Error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(this.program));\n\t\t}\n\t}\n\n\tpublic setAttribute( name: string) {\n\t\tthis.attributes[name] = gl.getAttribLocation(this.program, name);\n\t}\n\n\tpublic setUniform(name: string) {\t\t\n\t\tthis.attributes[name] = gl.getUniformLocation(this.program, name) as number;\n\t}\n}"
  },
  {
    "path": "src/rendering/WireframeBox.ts",
    "content": "class WireframeBox implements Renderer {\n\tprivate shader: Shader;\n\tprivate positions: WebGLBuffer;\n\n\tpublic transform: Matrix4;\n\t\n\tpublic visible: boolean = true;\n\n\tpublic color: Vector3 = new Vector3(0.0, 0.0, 1.0);\n\tpublic alpha: number = 0.8;\t\n\tpublic colorOccluded: Vector3 = new Vector3(0.0, 0.0, 0.0);\n\tpublic alphaOccluded: number = 0.15;\n\t\n\tpublic scale: Vector3 = Vector3.one();\n\n\tconstructor() {\n\t\tthis.shader = new Shader(SIMPLE_VERTEX_SHADER, COLOR_FRAGMENT_SHADER);\n\n\t\tthis.shader.setAttribute(\"vertexPosition\");\n        this.shader.setUniform(\"projectionMatrix\");\n        this.shader.setUniform(\"modelViewMatrix\");\n        this.shader.setUniform(\"color\");\n        this.shader.setUniform(\"scale\");\n\t\t\n\t\tthis.positions = gl.createBuffer();\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.positions);\n\t\tvar positions: number[] = [\n\t\t\t-1, -1, -1,  -1, -1, +1,\n\t\t\t+1, -1, -1,  +1, -1, +1,\n\t\t\t-1, +1, -1,  -1, +1, +1,\n\t\t\t+1, +1, -1,  +1, +1, +1,\n\n\t\t\t-1, -1, -1,  -1, +1, -1,\n\t\t\t-1, -1, +1,  -1, +1, +1,\n\t\t\t+1, -1, -1,  +1, +1, -1,\n\t\t\t+1, -1, +1,  +1, +1, +1,\n\n\t\t\t-1, -1, -1,  +1, -1, -1,\n\t\t\t-1, +1, -1,  +1, +1, -1,\n\t\t\t-1, -1, +1,  +1, -1, +1,\n\t\t\t-1, +1, +1,  +1, +1, +1\n\t\t];\n\t\tgl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);\n\t}\n\n\tpublic render(camera: Camera) {\n\t\tif (!this.visible) {\n\t\t\treturn;\n\t\t}\n\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.positions);\n\t\tgl.vertexAttribPointer(this.shader.attributes[\"vertexPosition\"], 3, gl.FLOAT, false, 0, 0);\n\t\tgl.enableVertexAttribArray(this.shader.attributes[\"vertexPosition\"]);\n\t\t\n\t\tgl.useProgram(this.shader.program);\n\n\t\tgl.uniformMatrix4fv(this.shader.attributes[\"projectionMatrix\"], false, camera.getProjectionMatrix().elements);\n\t\tgl.uniformMatrix4fv(this.shader.attributes[\"modelViewMatrix\"], false, this.transform.times(camera.transform).elements);\n\t\tgl.uniform3f(this.shader.attributes[\"scale\"], this.scale.x, this.scale.y, this.scale.z);\n\t\t\n\t\tgl.depthFunc(gl.GREATER);\n\t\tgl.depthMask(false);\n        gl.uniform4f(this.shader.attributes[\"color\"], this.colorOccluded.x, this.colorOccluded.y, this.colorOccluded.z, this.alphaOccluded);\n\t\tgl.drawArrays(gl.LINES, 0, 24);\n\t\t\n\t\tgl.depthFunc(gl.LEQUAL);\n\t\tgl.depthMask(true);\n        gl.uniform4f(this.shader.attributes[\"color\"], this.color.x, this.color.y, this.color.z, this.alpha);\t\t\n\t\tgl.drawArrays(gl.LINES, 0, 24);\n\t}\n}"
  },
  {
    "path": "src/rendering/WireframeRenderer.ts",
    "content": "class WireframeRenderer implements Renderer {\n\tprivate shader: Shader;\n\tprivate vertices: WebGLBuffer;\n\tprivate vertexCount: number;\n\n\tpublic transform: Matrix4;\n\t\n\tpublic enabled: boolean = true;\n\n\tpublic color: Vector3 = new Vector3(0.0, 0.0, 0.0);\n\tpublic alpha: number = 0.5;\n\n    constructor() {\n\t\tthis.shader = new Shader(SIMPLE_VERTEX_SHADER, COLOR_FRAGMENT_SHADER);\n\n\t\tthis.shader.setAttribute(\"vertexPosition\");\n        this.shader.setUniform(\"projectionMatrix\");\n        this.shader.setUniform(\"modelViewMatrix\");\n        this.shader.setUniform(\"color\");\n        this.shader.setUniform(\"scale\");\t\t\n\n        this.transform = Matrix4.getIdentity();\n    }\n\n    public setMesh(mesh: Mesh) {\n        this.vertexCount = mesh.getVertexCount() * 2;\n        this.vertices = mesh.createWireframeVertexBuffer();\n    }\n\n    public render(camera: Camera) {\n\t\tif (!this.enabled) {\n\t\t\treturn;\n\t\t}\n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);\n\t\tgl.vertexAttribPointer(this.shader.attributes[\"vertexPosition\"], 3, gl.FLOAT, false, 0, 0);\n\t\tgl.enableVertexAttribArray(this.shader.attributes[\"vertexPosition\"]);\n\t\t\n\t\tgl.useProgram(this.shader.program);\n\n\t\tgl.uniformMatrix4fv(this.shader.attributes[\"projectionMatrix\"], false, camera.getProjectionMatrix().elements);\n\t\tgl.uniformMatrix4fv(this.shader.attributes[\"modelViewMatrix\"], false, this.transform.times(camera.transform).elements);\n\t\tgl.uniform3f(this.shader.attributes[\"scale\"], 1, 1, 1);\n\t\tgl.uniform4f(this.shader.attributes[\"color\"], this.color.x, this.color.y, this.color.z, this.alpha);\n\t\t\n      \n        gl.drawArrays(gl.LINES, 0, this.vertexCount);\n    }\n}"
  },
  {
    "path": "src/rendering/shaders.ts",
    "content": "const VERTEX_SHADER = `\n    attribute vec4 vertexPosition;\n    attribute vec4 normal;\n\n    uniform mat4 modelViewMatrix;\n    uniform mat4 projectionMatrix;\n\n    varying vec3 v2fNormal;\n\n    void main() {\n        v2fNormal = (modelViewMatrix * vec4(normal.xyz, 0.0)).xyz;\n        gl_Position = projectionMatrix * modelViewMatrix * vertexPosition;\n    }\n`;\n\n\nconst FRAGMENT_SHADER = `\n    precision mediump float;\n\n    const vec3 lightDirection = vec3(-0.7, -0.7, 0.14);\n    const float ambient = 0.2;\n    const float diffuse = 0.8;\n    const float specular = 0.3;\n    const vec3 viewDirection = vec3(0.0, 0.0, 1.0);\n\n    varying vec3 v2fNormal;\n\n    uniform vec3 albedo;\n    uniform float alpha;\n\n    void main() {\n        vec3 color = albedo * (ambient\n             + diffuse * (0.5 + 0.5 * dot(lightDirection, v2fNormal))\n             + specular * pow(max(0.0, dot(reflect(-lightDirection, v2fNormal), viewDirection)), 2.0));\n\n        gl_FragColor = vec4(color.r, color.g, color.b, alpha);\n    }\n`;\n\nconst NORMAL_FRAGMENT_SHADER = `\n    precision mediump float;\n\n    varying vec3 v2fNormal;\n\n    void main() {\n        vec3 normal = vec3(0.5) + 0.5 * normalize(v2fNormal);\n        gl_FragColor = vec4(normal, 1.0);\n    }\n`;\n\nconst COUNTOUR_VERTEX = `\n    attribute vec2 vertexPosition;\n\n    varying vec2 uv;\n\n    void main() {\n        uv = vertexPosition / 2.0 + vec2(0.5);\n        gl_Position = vec4(vertexPosition, 0.0, 1.0);\n    }\n`;\n\nconst CONTOUR_FRAGMENT = `\n    precision mediump float;\n\n    uniform sampler2D normalTexture;\n    uniform sampler2D depthTexture;\n    uniform vec2 resolution;\n\n    varying vec2 uv;\n    \n    const float NORMAL_THRESHOLD = 0.5;\n\n    vec3 getNormal(vec2 uv) {\n        vec4 sample = texture2D(normalTexture, uv);\n        return 2.0 * sample.xyz - vec3(1.0);\n    }\n\n    float getDepth(vec2 uv) {\n        return texture2D(depthTexture, uv).r;\n    }\n\n    bool isContour(vec2 uv, float referenceDepth, vec3 referenceNormal) {\n        float depth = getDepth(uv);\n        vec3 normal = getNormal(uv);\n        float angle = abs(referenceNormal.z);\n        \n        float threshold = mix(0.005, 0.0001, pow(-referenceNormal.z, 0.5));\n\n        if (abs(depth - referenceDepth) > threshold) {\n            return true;\n        }\n\n        if (abs(dot(normal, referenceNormal)) < NORMAL_THRESHOLD) {\n            return true;\n        }\n\n        return false;\n    }\n\n    void main() {\n        vec2 pixelSize = vec2(1.0 / resolution.x, 1.0 / resolution.y);\n\n        float depth = getDepth(uv);\n        vec3 normal = getNormal(uv);\n\n        float count = 0.0;\n\n        for (float x = -1.0; x <= 1.0; x++) {\n            for (float y = -1.0; y <= 1.0; y++) {\n                if ((x != 0.0 || y != 0.0) && isContour(uv + pixelSize * vec2(x, y), depth, normal)) {\n                    count++;\n                }\n            }\n        }\n        float contour = count == 1.0 ? 0.0 : (count - 0.2) / 5.0;\n        \n        gl_FragColor = vec4(vec3(0.0), contour);\n    }\n`\n\nconst SIMPLE_VERTEX_SHADER = `\n    attribute vec4 vertexPosition;\n\n    uniform mat4 modelViewMatrix;\n    uniform mat4 projectionMatrix;\n\n    uniform vec3 scale;\n\n    void main() {\n        gl_Position = projectionMatrix * modelViewMatrix * vec4((vertexPosition.xyz * scale), vertexPosition.a);\n    }\n`;\n\nconst COLOR_FRAGMENT_SHADER = `\n    precision mediump float;\n\n    uniform vec4 color;\n\n    void main() {\n        gl_FragColor = color;\n    }\n`;"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"outFile\": \"app.js\",\n        \"sourceMap\": true,\n        \"target\": \"esnext\",\n    },\n\t\"compileOnSave\": true,\n    \"include\": [\n        \"**/*.ts\"\n    ]\n}"
  }
]