Full Code of marian42/partdesigner for AI

master 2a07b5aae2f7 cached
46 files
172.7 KB
48.3k tokens
316 symbols
1 requests
Download .txt
Repository: marian42/partdesigner
Branch: master
Commit: 2a07b5aae2f7
Files: 46
Total size: 172.7 KB

Directory structure:
gitextract_gf1jn8a0/

├── .github/
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── LICENSE
├── README.md
├── app.css
├── app.ts
├── index.html
├── src/
│   ├── MeshGenerator.ts
│   ├── PartMeshGenerator.ts
│   ├── editor/
│   │   ├── Catalog.ts
│   │   ├── CatalogItem.ts
│   │   ├── Editor.ts
│   │   ├── EditorState.ts
│   │   ├── Handles.ts
│   │   ├── NamedMeasurement.ts
│   │   └── RenderStyle.ts
│   ├── export/
│   │   ├── STLExporter.ts
│   │   └── StudioPartExporter.ts
│   ├── functions.ts
│   ├── geometry/
│   │   ├── Matrix4.ts
│   │   ├── Mesh.ts
│   │   ├── Quaternion.ts
│   │   ├── Ray.ts
│   │   ├── Triangle.ts
│   │   ├── TriangleWithNormals.ts
│   │   ├── Vector3.ts
│   │   └── VectorDictionary.ts
│   ├── measurements.ts
│   ├── model/
│   │   ├── Block.ts
│   │   ├── Part.ts
│   │   ├── PerpendicularRoundedAdaper.ts
│   │   ├── SmallBlock.ts
│   │   ├── TinyBlock.ts
│   │   └── enums/
│   │       ├── BlockType.ts
│   │       ├── Orientation.ts
│   │       └── Quadrant.ts
│   └── rendering/
│       ├── Camera.ts
│       ├── ContourPostEffect.ts
│       ├── MeshRenderer.ts
│       ├── NormalDepthRenderer.ts
│       ├── Renderer.ts
│       ├── Shader.ts
│       ├── WireframeBox.ts
│       ├── WireframeRenderer.ts
│       └── shaders.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/deploy.yml
================================================
name: GitHub Pages Deployment

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 'latest'

    - name: Install TypeScript
      run: npm install -g typescript

    - name: Compile TypeScript
      run: tsc

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: .
        publish_branch: gh-pages
        exclude_assets: .github,src,.gitignore,app.js.map,LICENSE,README.md,tsconfig.json,app.ts
        full_commit_message: Deploy to Github Pages

================================================
FILE: .gitignore
================================================
*.js
*.js.map


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Marian Kleineberg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![](https://i.imgur.com/L7moBQT.png)

# Part Designer

This is a free online CAD tool to create custom LEGO® Technic compatible construction parts for 3D printing.

Features
- Assemble a custom part from basic blocks: Pin Hole, Axle Hole, Pin, Axle, Solid
- Save your model as an STL file
- Catalog of existing LEGO® parts
- Customize measurements to get a perfect fit
- Create a sharable link of your part

# Local setup and development

You need to have [TypeScript](https://www.typescriptlang.org/) installed.
In the project root, run `tsc`.
This should run without errors and create the file `app.js`.

You need a webserver that locally serves the files from the project directory.
If you have python installed, you can call `python3 -m http.server`.
It will tell you the port, for example 8000, and you can visit http://localhost:8000 in your browser.
Alternatively, you can install [http-server](https://www.npmjs.com/package/http-server), which will also create a server in port 8000.

If you work on the code, run `tsc --watch`, which will recompile everytime you change a source file.


================================================
FILE: app.css
================================================
html, body {
    font-family: 'Segoe UI', sans-serif;
    margin: 0px;
    height: 100%;
    overflow: hidden;
    font-size: 14px;
}

.canvas-container canvas {
    width: 100%;
    height: 100%;
}

.canvas-container canvas:focus {
    outline: none;
}

.canvas-container {
    padding-left: 420px;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
}

.sidebar {
    background-color: rgb(219, 219, 219);
    width: 420px;
    height: 100%;
    position: fixed;
    overflow: auto;
}

details {
    padding: 10px;
    overflow: hidden;
}

summary {
    font-weight: bold;
    box-sizing: border-box;
    background-color: rgb(167, 167, 167);
    margin: -10px;
    padding: 7px;
    user-select: none;
    -moz-user-select: none;
    border-bottom: 1px solid rgb(219, 219, 219);
    transition: background-color 0.2s;
    outline: none;
}

summary:hover {
    background-color: rgb(184, 184, 184);
}

details[open] summary {    
    margin-bottom: 10px;
}

.cell {    
    display: table-cell;
    width: 100%;
    padding-right: 10px;
}

.cell:last-of-type {
    padding-right: 0;
}

.group {
    margin-bottom: 10px;
    display: table;
    table-layout: fixed;
    width: 100%;
}

button, label.radiolabel {
    transition: 0.1s;
    border: 1px solid rgb(21, 98, 212);
}

button {
    position: relative;
    background-color: rgb(21, 98, 212);
    border-radius: 5px;
    color: white;
    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);
    padding: 8px 16px;
}

label.radiolabel {
    text-align: center;
    background-color: rgb(219, 219, 219);
    border-right-style: none;
    margin-right: -6px;
    color: rgb(21, 98, 212);
    display: table-cell;
    vertical-align: middle;
    user-select: none;
    -moz-user-select: none;
    width: 100%;    
    padding: 8px;
}

label.radiolabel:active, button:active {
    box-shadow: inset 2px 2px 6px 0px rgba(0,0,0,0.35);
}

input[type=radio]:checked + label.radiolabel {
    background-color: rgb(21, 98, 212);
    color: white;
    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);
}

.radiolabel:hover, input[type=radio]:checked + label.radiolabel:hover, button:hover {
    border: 1px solid rgb(47, 130, 255);
    background-color: rgb(47, 130, 255);
    color: white;
    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);
}

.group input[type=radio] {
    display: none;
}

label.radiolabel:first-of-type {
    border-top-left-radius: 5px;
    border-bottom-left-radius: 5px;
}

label.radiolabel:last-of-type {
    border-top-right-radius: 5px;
    border-bottom-right-radius: 5px;
    border-right-style: solid;
}

label.radiolabel.x {
    color: #FF0000;
}

input[type=radio]:checked + label.radiolabel.x {
    color: white;
    background-color: #B30000;
    border-color: #B30000;
}

input[type=radio]:checked + label.radiolabel.x:hover, label.radiolabel.x:hover {
    color: white;
    background-color: red;
    border-color: red;
}

label.radiolabel.y {
    color: #009A05;
}

input[type=radio]:checked + label.radiolabel.y {
    color: white;
    background-color: #009A05;
    border-color: #009A05;
}

input[type=radio]:checked + label.radiolabel.y:hover, label.radiolabel.y:hover {
    color: white;
    background-color: #00DF07;
    border-color: #00DF07;
}

label.radiolabel.z {
    color: #0000FF;
}

input[type=radio]:checked + label.radiolabel.z {
    color: white;
    background-color: #0011A7;
    border-color: #0011A7;
}

input[type=radio]:checked + label.radiolabel.z:hover, label.radiolabel.z:hover {
    color: white;
    background-color: #001AFF;
    border-color: #001AFF;
}

.editorhint {
    display: table-cell;
    width: 84px;
    vertical-align: middle;
}

.catalogItem {
    margin: 2px;
    padding: 2px;
    display: inline-block;
    border: 2px solid rgba(0,0,0,0);
    border-radius: 5px;
    transition: 0.2s;
}

.catalogItem:hover {
    border-color: rgb(47, 130, 255);
}

.group select, .group select:focus {
    display: table-cell;
    width: 100%;
    border-radius: 5px;
    padding: 6px;
    border: 1px solid rgb(21, 98, 212);
    background-color:  rgb(219, 219, 219);
    transition: 0.1s;
    color: rgb(21, 98, 212);
}

.group select:hover, .group select:active {
    border: 1px solid rgb(47, 130, 255);
    background-color: rgb(47, 130, 255);
    color: white;
    text-shadow: 0px 1px 6px rgba(0,0,0,0.63);
}

.group .measurementhint {
    display: table-cell;
    width: 180px;
    vertical-align: middle;
}
.group .reset {
    display: table-cell;
    vertical-align: middle;
    text-align: right;
    padding-right: 10px;
    width: 50px;
    color: rgb(21, 98, 212);
}

.group .reset:visited {
    color: rgb(21, 98, 212);
}

.group input[type=text] {
    padding: 7px;
    border-radius: 5px 0px 0px 5px;
    border: 1px solid rgb(47, 130, 255);
    border-right: none;
    background-color: rgb(219, 219, 219);
    display: table-cell;
    width: 100%;
    box-sizing: border-box;
}

.group .measurement {
    text-align: right;
}

.group input[type=text]:last-child {
    border-radius: 5px;
    border-right: none;
    border: 1px solid rgb(47, 130, 255);
}

.unit {
    border-radius: 5px;
    border: 1px solid rgb(47, 130, 255);
    background-color: rgb(219, 219, 219);
    border-radius: 0px 5px 5px 0px;
    border-left: none;
    display: table-cell;
    width: 32px;
    box-sizing: border-box;
    color: rgb(78, 78, 78);
    user-select: none;
}

.fineprint, .fineprint a, .fineprint a:visited {
    font-size: 13px;
    color: rgb(78, 78, 78);
}

.part-buttons {
    margin-bottom: 10px;
}

.key {
    display: inline-block;
    padding: 3px 5px;
    line-height: 10px;
    font: 10px monospace;
    color: #555;
    vertical-align: middle;
    background-color: #eee;
    border: solid 1px #ccc;
    border-radius: 3px;
    box-shadow: inset 0 -1px 0 #bbb;
}


================================================
FILE: app.ts
================================================
let gl: WebGLRenderingContext;

var editor: Editor;
var catalog: Catalog;

window.onload = () => {
	catalog = new Catalog();
	editor = new Editor();
};

window.onpopstate = function(event: PopStateEvent){
    if (event.state) {
		var url = new URL(document.URL);
		if (url.searchParams.has("part")) {
			editor.part = Part.fromString(url.searchParams.get("part"));
			editor.updateMesh(true);
		}
    }
};

================================================
FILE: index.html
================================================
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Part Designer</title>
    <meta name="author" content="Marian Kleineberg">
    <meta name="description" content="A free online CAD tool to create custom LEGO&reg; Technic compatible construction parts for 3D printing.">
    <link rel="stylesheet" href="app.css" type="text/css" />
    <link rel="icon" href="favicon.png" type="image/png" sizes="16x16"/>
</head>
<body>
    <div class="sidebar">
        <details open>
            <summary>Part</summary>
            <div class="part-buttons">
                <button id="clear">Clear</button>
                <button id="share">Share</button>
                <button id="save-stl">Save STL</button>
                <button id="save-studio">Save Studio part</button>
            </div>
            <div class="group">
                <span class="measurementhint">Display style</span>
                <select id="style">
                    <option value="0">Contour</option>
                    <option value="1">Solid</option>
                    <option value="2">Wireframe</option>
                    <option value="3">Solid Wireframe</option>
                </select>
            </div>
            <div class="group">
                <span class="measurementhint">Part name</span>
                <input type="text" id="partName" placeholder="Unnamed part">
            </div>
        </details>
        <details id="blockeditor" open>
            <summary>Edit Block</summary>
            <div class="group" id="type">
                <span class="editorhint">Type</span>
                <input type="radio" id="pinhole" name="type" value="pinhole" checked>
                <label class="radiolabel" for="pinhole">Pin Hole</label>

                <input type="radio" id="axlehole" name="type" value="axlehole">
                <label class="radiolabel" for="axlehole">Axle Hole</label>

                <input type="radio" id="pin" name="type" value="pin">
                <label class="radiolabel" for="pin">Pin</label>

                <input type="radio" id="axle" name="type" value="axle">
                <label class="radiolabel" for="axle">Axle</label>

                <input type="radio" id="solid" name="type" value="solid">
                <label class="radiolabel" for="solid">Solid</label>

                <input type="radio" id="balljoint" name="type" value="balljoint">
                <label class="radiolabel" for="balljoint">Ball Joint</label>
            </div>
            <div class="group" id="orientation">
                <span class="editorhint">Orientation</span>
                <input type="radio" id="x" name="orientation" value="x" checked>
                <label class="radiolabel x" for="x">X</label>
            
                <input type="radio" id="y" name="orientation" value="y">
                <label class="radiolabel y" for="y">Y</label>
            
                <input type="radio" id="z" name="orientation" value="z">
                <label class="radiolabel z" for="z">Z</label>
            </div>
            <div class="group" id="size">
                <span class="editorhint">Size</span>            
                <input type="radio" id="full" name="blocksize" value="full" checked>
                <label class="radiolabel" for="full">Full Block</label>

                <input type="radio" id="half" name="blocksize" value="half">
                <label class="radiolabel" for="half">Half Block</label>
            </div>
            <div class="group" id="rounded">
                <span class="editorhint">Edges</span>
                <input type="radio" id="true" name="rounded" value="true" checked>
                <label class="radiolabel" for="true">Rounded</label>
            
                <input type="radio" id="false" name="rounded" value="false">
                <label class="radiolabel" for="false">Not Rounded</label>
            </div>
            <div class="group">
                <span class="editorhint"></span>
                <button id="remove">Remove block</button>
            </div>
        </details>
        <details id="catalog">
            <summary>Catalog</summary>
        </details>
        <details>
            <summary>Measurements</summary>
            <div class="group">
                <span class="measurementhint">Technic Unit</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="technicUnit">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Subdivisions Per Quarter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="subdivisionsPerQuarter">
            </div>
            <div class="group">
                <span class="measurementhint">Edge Margin</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="edgeMargin">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Interior Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="interiorRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Pin Hole Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="pinHoleRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Pin Hole Offset</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="pinHoleOffset">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Axle Hole Size</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="axleHoleSize">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Pin Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="pinRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Pin Lip Size</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="pinLipRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Lip Subdivisions</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="lipSubdivisions">
            </div>
            <div class="group">
                <span class="measurementhint">Axle Size Inner</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="axleSizeInner">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Axle Size Outer</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="axleSizeOuter">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Attachment Adapter Size</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="attachmentAdapterSize">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Attachment Adapter Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="attachmentAdapterRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Interior End Margin</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="interiorEndMargin">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Ball Joint Ball Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="ballRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <span class="measurementhint">Ball Joint Base Diameter</span>
                <a href="#" class="reset">Reset</a>
                <input class="measurement" type="text" id="ballBaseRadius">
                <span class="unit">mm</span>
            </div>
            <div class="group">
                <div class="cell"><button id="resetmeasurements">Reset</button></div>
                <div class="cell"><button id="applymeasurements">Apply</button></div>
            </div>
        </details>

        <details open>
            <summary>About</summary>
            <p>This is a free online CAD tool to create custom LEGO&reg; Technic compatible construction parts for 3D printing.</p>
            <p>
            <table>
                <tr>
                    <td><span class="key">1</span> <span class="key">2</span> <span class="key">3</span>
                        <span class="key">4</span> <span class="key">5</span> <span class="key">6</span>
                    </td>
                    <td>set type</td>
                </tr>
                <tr>
                    <td><span class="key">x</span> <span class="key">y</span> <span class="key">z</span></td>
                    <td>set orientation</td>
                </tr>
                <tr>
                    <td><span class="key">←</span> <span class="key">→</span> <span class="key">↑</span>
                        <span class="key">↓</span> <span class="key">PgUp</span> <span class="key">PgDown</span>
                    </td>
                    <td>
                        move cursor
                    </td>
                </tr>
                <tr>
                    <td>
                        <span class="key">Del</span>, <span class="key">BkSp</span>
                    </td>
                    <td>
                        remove block
                    </td>
                </tr>
            </table>
            </p>
            <p>You can find the source code for this project on <a href="https://github.com/marian42/partdesigner" target="_blank">Github</a>.</p>
            <p class="fineprint">
                <a href="https://marian42.de/" target="_blank">marian42.de</a>&nbsp;&middot;&nbsp;
                <a href="mailto:mail@marian42.de">Contact</a>
            </p>
        </details>
    </div>
    <div class="canvas-container">
        <canvas id="canvas"></canvas>
    </div>
    
    <script src="app.js"></script>
</body>
</html>


================================================
FILE: src/MeshGenerator.ts
================================================
class MeshGenerator {
    protected triangles: Triangle[] = [];
    protected measurements: Measurements;

    constructor(measurements: Measurements) {
        this.measurements = measurements;
    }

    public getMesh(): Mesh {
        return new Mesh(this.triangles);
    }

    protected createQuad(v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3, flipped = false) {
        if (!flipped) {
            this.triangles.push(new Triangle(v1, v2, v4));
            this.triangles.push(new Triangle(v2, v3, v4));
        } else {
            this.triangles.push(new Triangle(v4, v2, v1));
            this.triangles.push(new Triangle(v4, v3, v2));
        }
    }

    protected createQuadWithNormals(v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3, n1: Vector3, n2: Vector3, n3: Vector3, n4: Vector3, flipped = false) {
        if (!flipped) {
            this.triangles.push(new TriangleWithNormals(v1, v2, v4, n1, n2, n4));
            this.triangles.push(new TriangleWithNormals(v2, v3, v4, n2, n3, n4));
        } else {
            this.triangles.push(new TriangleWithNormals(v4, v2, v1, n4.times(-1), n2.times(-1), n1.times(-1)));
            this.triangles.push(new TriangleWithNormals(v4, v3, v2, n4.times(-1), n3.times(-1), n2.times(-1)));
        }
    }

    protected createCircleWithHole(block: TinyBlock, innerRadius: number, outerRadius: number, offset: number, inverted = false, square = false) {
        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));

        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
            let i1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter);
            let i2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter);
            var o1 = i1;
            var o2 = i2;

            if (square) {
                if (Math.abs(o1.dot(block.right)) > Math.abs(o1.dot(block.up))) {
                    o1 = o1.times(1 / Math.abs(o1.dot(block.right)));
                } else {
                    o1 = o1.times(1 / Math.abs(o1.dot(block.up)));
                }
                if (Math.abs(o2.dot(block.right)) > Math.abs(o2.dot(block.up))) {
                    o2 = o2.times(1 / Math.abs(o2.dot(block.right)));
                } else {
                    o2 = o2.times(1 / Math.abs(o2.dot(block.up)));
                }
            }

            this.createQuad(
                i1.times(innerRadius).plus(center),
                i2.times(innerRadius).plus(center),
                o2.times(outerRadius).plus(center),
                o1.times(outerRadius).plus(center),
                inverted);
        }
    }

    protected createCircle(block: TinyBlock, radius: number, offset: number, inverted = false) {
        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));

        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
            let p1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter, radius);
            let p2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter, radius);

            if (inverted) {
                this.triangles.push(new Triangle(center.plus(p1), center, center.plus(p2)));
            } else {
                this.triangles.push(new Triangle(center, center.plus(p1), center.plus(p2)));
            }            
        }
    }

    protected createCylinder(block: TinyBlock, offset: number, radius: number, distance: number, inverted = false) {
        let center = block.getCylinderOrigin(this).plus(block.forward.times(offset));

        for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
            let v1 = block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter);
            let v2 = block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter);
            this.createQuadWithNormals(
                center.plus(v1.times(radius)),
                center.plus(v2.times(radius)),
                center.plus(v2.times(radius)).plus(block.forward.times(distance)),
                center.plus(v1.times(radius)).plus(block.forward.times(distance)),
                v1, v2, v2, v1,
                !inverted);
        }
    }

    public tinyIndexToWorld(p: number): number {
        let i = Math.floor((p + 1) / 3);
        let j = p - i * 3;
    
        var f = 0.5 * i;
        if (j == 0) {
            f += this.measurements.edgeMargin;
        } else if (j == 1) {
            f += 0.5 - this.measurements.edgeMargin;
        }
    
        return f;
    }
    
    public tinyBlockToWorld(position: Vector3): Vector3 {
        return new Vector3(this.tinyIndexToWorld(position.x), this.tinyIndexToWorld(position.y), this.tinyIndexToWorld(position.z));
    }
}

================================================
FILE: src/PartMeshGenerator.ts
================================================
class PartMeshGenerator extends MeshGenerator {
    private smallBlocks: VectorDictionary<SmallBlock>;
	private tinyBlocks: VectorDictionary<TinyBlock>;

	constructor(part: Part, measurements: Measurements) {
        super(measurements);
        this.smallBlocks = part.createSmallBlocks();
		this.createDummyBlocks();
        this.updateRounded();
        this.createTinyBlocks();
        this.processTinyBlocks();
        this.checkInteriors();
		this.mergeSimilarBlocks();
		this.renderPerpendicularRoundedAdapters();
		this.renderRoundedExteriors();
		this.renderInteriors();
        this.renderAttachments();
        this.renderTinyBlockFaces();
    }

    private updateRounded() {
		var perpendicularRoundedAdapters: SmallBlock[] = [];

        for (var block of this.smallBlocks.values()) {
			if (block.isAttachment) {
				block.rounded = true;
				continue;
			}
			if (!block.rounded) {
				continue;
			}

			var next = this.smallBlocks.getOrNull(block.position.plus(block.forward));
			if (next != null && next.orientation == block.orientation && next.quadrant != block.quadrant) {
				block.rounded = false;
				continue;
			}
			var previous = this.smallBlocks.getOrNull(block.position.minus(block.forward));
			if (previous != null && previous.orientation == block.orientation && previous.quadrant != block.quadrant) {
				block.rounded = false;
				continue;
			}

			var neighbor1 = this.smallBlocks.getOrNull(block.position.plus(block.horizontal));
			var neighbor2 = this.smallBlocks.getOrNull(block.position.plus(block.vertical));
			if ((neighbor1 == null || (neighbor1.isAttachment && neighbor1.forward.dot(block.right) == 0))
				&& (neighbor2 == null || (neighbor2.isAttachment && neighbor2.forward.dot(block.up) == 0))) {
				continue;
			}

			if (this.createPerpendicularRoundedAdapterIfPossible(block)) {
				perpendicularRoundedAdapters.push(block);
				continue;
			}

			block.rounded = false;
		}

		// Remove adapters where the neighbor was later changed from rounded to not rounded
		var anythingChanged: boolean;
		do {
			anythingChanged = false;
			for (var block of perpendicularRoundedAdapters) {
				if (block.perpendicularRoundedAdapter != null && !block.perpendicularRoundedAdapter.neighbor.rounded) {
					block.perpendicularRoundedAdapter = null;
					block.rounded = false;
					anythingChanged = true;
				}
			}
		} while (anythingChanged);
    }

    private createDummyBlocks() {
        var addedAnything = false;
		for (var block of this.smallBlocks.values()) {
			if (!block.isAttachment) {
				continue;
			}
			var affectedPositions = [
				block.position,
				block.position.minus(block.horizontal),
				block.position.minus(block.vertical),
				block.position.minus(block.horizontal).minus(block.vertical)
            ];
			for (var forwardDirection = -1; forwardDirection <= 1; forwardDirection += 2) {
				var count = countInArray(affectedPositions, (p) => this.smallBlocks.containsKey(p.plus(block.forward.times(forwardDirection))));
				if (count != 0 && count != 4) {
					var source = new Block(block.orientation, BlockType.Solid, true);
					for (var position of affectedPositions) {
						var targetPosition = position.plus(block.forward.times(forwardDirection));
						if (!this.smallBlocks.containsKey(targetPosition)) {
							this.smallBlocks.set(targetPosition, new SmallBlock(this.smallBlocks.get(position).quadrant, targetPosition, source));
						}
					}
					addedAnything = true;
				}
			}
		}
		if (addedAnything) {
			this.createDummyBlocks();
		}
	}
	
	private createPerpendicularRoundedAdapterIfPossible(block: SmallBlock): boolean {
		var neighbor1 = this.smallBlocks.getOrNull(block.position.plus(block.horizontal));
		var neighbor2 = this.smallBlocks.getOrNull(block.position.plus(block.vertical));
		
		var hasHorizontalNeighbor = neighbor2 == null && neighbor1 != null && neighbor1.forward.dot(block.horizontal) != 0 && neighbor1.rounded;
		var hasVerticalNeighbor = neighbor1 == null && neighbor2 != null && neighbor2.forward.dot(block.vertical) != 0 && neighbor2.rounded;
		
		if (hasHorizontalNeighbor == hasVerticalNeighbor) {
			return false;
		}

		var adapter = new PerpendicularRoundedAdapter();
		adapter.directionToNeighbor = hasVerticalNeighbor ? block.vertical : block.horizontal;
		adapter.isVertical = hasVerticalNeighbor;
		adapter.neighbor = hasHorizontalNeighbor ? neighbor1 : neighbor2;
		adapter.facesForward = block.forward.dot(adapter.neighbor.horizontal.plus(adapter.neighbor.vertical)) < 0;
		adapter.sourceBlock = block;
		
		if (!this.smallBlocks.containsKey(block.position.plus(block.forward.times(adapter.facesForward ? 1 : -1)))) {
			return false;
		}

		block.perpendicularRoundedAdapter = adapter;
		return true;
	}

    private createTinyBlocks() {
        this.tinyBlocks = new VectorDictionary<TinyBlock>();

        for (let block of this.smallBlocks.values()) {
            if (block.isAttachment) {
                continue;
            }

            let pos = block.position;
            for (var a = -1; a <= 1; a++) {
                for (var b = -1; b <= 1; b++) {
                    for (var c = -1; c <= 1; c++) {
                        if (this.isSmallBlock(pos.plus(new Vector3(a, 0, 0)))
                            && this.isSmallBlock(pos.plus(new Vector3(0, b, 0)))
                            && this.isSmallBlock(pos.plus(new Vector3(0, 0, c)))
                            && this.isSmallBlock(pos.plus(new Vector3(a, b, c)))
                            && this.isSmallBlock(pos.plus(new Vector3(a, b, 0)))
                            && this.isSmallBlock(pos.plus(new Vector3(a, 0, c)))
                            && this.isSmallBlock(pos.plus(new Vector3(0, b, c)))) {
                            this.createTinyBlock(pos.times(3).plus(new Vector3(a, b, c)), block);
                        }
                    }
                }
            }
        }

        for (let block of this.smallBlocks.values()) {
            if (!block.isAttachment) {
                continue;
            }
            for (var a = -2; a <= 2; a++) {
                var neighbor = block.position.plus(block.forward.times(sign(a)));
                if (!this.smallBlocks.containsKey(neighbor) || (Math.abs(a) >= 2 && this.smallBlocks.get(neighbor).isAttachment)) {
                    continue;
                }

                for (var b = -1; b <= 0; b++) {
                    for (var c = -1; c <= 0; c++) {
                        this.createTinyBlock(block.position.times(3).plus(block.forward.times(a)).plus(block.horizontal.times(b)).plus(block.vertical.times(c)), block);
                    }
                }
            }
        }
    }

    private isTinyBlock(position: Vector3): boolean {
        return this.tinyBlocks.containsKey(position) && !this.tinyBlocks.get(position).isAttachment;
	}
	
	private pushBlock(smallBlock: SmallBlock, forwardFactor: number) {
		var nextBlock = this.smallBlocks.getOrNull(smallBlock.position.plus(smallBlock.forward.times(forwardFactor)));
			
		for (var a = -2; a <= 2; a++) {
			for (var b = -2; b <= 2; b++) {
				var from = smallBlock.position.times(3)
					.plus(smallBlock.right.times(a))
					.plus(smallBlock.up.times(b))
					.plus(smallBlock.forward.times(forwardFactor));
				var to = from.plus(smallBlock.forward.times(forwardFactor));
				if (!this.tinyBlocks.containsKey(to)) {
					continue;
				}
				if (!this.tinyBlocks.containsKey(from)) {
					this.tinyBlocks.remove(to);
					continue;
				}
				if (smallBlock.orientation == nextBlock.orientation) {
					if (Math.abs(a) < 2 && Math.abs(b) < 2) {
						this.tinyBlocks.get(to).rounded = true;
					}
				} else {
					this.createTinyBlock(to, this.tinyBlocks.get(from));
				}
			}
		}
	}

    private processTinyBlocks() {
		// Disable interiors when adjacent quadrants are missing
		for (var block of this.tinyBlocks.values()) {
			if (block.isCenter
				&& !block.isAttachment
				&& (block.hasInterior || block.rounded)
				&& (!this.isTinyBlock(block.position.minus(block.horizontal.times(3))) || !this.isTinyBlock(block.position.minus(block.vertical.times(3))))) {
				for (var a = -1; a <= 1; a++) {
					for (var b = -1; b <= 1; b++) {
						var position = block.position.plus(block.right.times(a)).plus(block.up.times(b));
						if (this.tinyBlocks.containsKey(position)) {
							this.tinyBlocks.get(position).rounded = false;
							this.tinyBlocks.get(position).hasInterior = false;
						}
					}
				}
			}
		}

		for (var smallBlock of this.smallBlocks.values()) {
			var nextBlock = this.smallBlocks.getOrNull(smallBlock.position.plus(smallBlock.forward));
			// Offset rounded to non rounded transitions to make them flush
			if (smallBlock.rounded && nextBlock != null && !nextBlock.rounded && smallBlock.perpendicularRoundedAdapter == null) {
				this.pushBlock(smallBlock, 1);
			}
			var previousBlock = this.smallBlocks.getOrNull(smallBlock.position.minus(smallBlock.forward));
			// Offset rounded to non rounded transitions to make them flush
			if (smallBlock.rounded && previousBlock != null && !previousBlock.rounded && smallBlock.perpendicularRoundedAdapter == null) {
				this.pushBlock(smallBlock, -1);
			}

			if (smallBlock.rounded && nextBlock != null && nextBlock.rounded && smallBlock.orientation != nextBlock.orientation) {
				this.pushBlock(smallBlock, 1);
			}
			
			if (smallBlock.rounded && previousBlock != null && previousBlock.rounded && smallBlock.orientation != previousBlock.orientation) {
				this.pushBlock(smallBlock, -1);
			}
		}
    }

    // Sets HasInterior to false for all tiny blocks that do not form coherent blocks with their neighbors
	private checkInteriors() {
		for (var block of this.tinyBlocks.values()) {
			if (!block.isCenter || !block.hasInterior) {
				continue;
			}
			for (var a = 0; a <= 1; a++) {
				for (var b = 1 - a; b <= 1; b++) {
					var neighborPos = block.position.minus(block.horizontal.times(3 * a)).minus(block.vertical.times(3 * b));
					if (!this.tinyBlocks.containsKey(neighborPos)) {
						block.hasInterior = false;
					} else {
						var neighbor = this.tinyBlocks.get(neighborPos);
						if (block.orientation != neighbor.orientation
							|| block.type != neighbor.type
							|| neighbor.localX != block.localX - a * block.directionX
							|| neighbor.localY != block.localY - b * block.directionY) {
							block.hasInterior = false;
						}
					}
				}
			}
		}
	}

	private getPerpendicularRoundedNeighborOrNull(block: TinyBlock): SmallBlock {
		var verticalNeighbor = this.smallBlocks.getOrNull(block.smallBlockPosition.plus(block.vertical));
		var horizontalNeighbor = this.smallBlocks.getOrNull(block.smallBlockPosition.plus(block.horizontal));
		var neighbor = verticalNeighbor != null ? verticalNeighbor : horizontalNeighbor;
		var verticalOrHorizontal = verticalNeighbor != null ? block.vertical : block.horizontal;
		if (neighbor != null && neighbor.rounded && neighbor.forward.dot(verticalOrHorizontal) != 0) {
			return neighbor;
		} else {
			return null;
		}
	}

	private getPerpendicularRoundedNeighborOrNull2(block: TinyBlock): SmallBlock {
		var smallBlock = this.smallBlocks.get(block.smallBlockPosition);
		if (smallBlock.perpendicularRoundedAdapter != null) {
			return smallBlock.perpendicularRoundedAdapter.neighbor;
		} else {
			return null;
		}
	}
	
	private preventMergingForPerpendicularRoundedBlock(block1: TinyBlock, block2: TinyBlock): boolean {
		if (!block1.rounded || !block2.rounded || !block1.isCenter) {
			return false;
		}
		var neighbor1 = this.getPerpendicularRoundedNeighborOrNull(block1);
		var neighbor2 = this.getPerpendicularRoundedNeighborOrNull(block2);

		var inside1 = neighbor1 != null && block1.position.minus(neighbor1.position.times(3)).dot(neighbor1.vertical.plus(neighbor1.horizontal)) <= 0;
		var inside2 = neighbor2 != null && block2.position.minus(neighbor2.position.times(3)).dot(neighbor2.vertical.plus(neighbor2.horizontal)) <= 0;
		
		return inside1 != inside2 || (inside1 && inside2 && !neighbor1.position.equals(neighbor2.position));
	}

    private mergeSimilarBlocks() {
        for (var block of this.tinyBlocks.values()) {
			if (block.isExteriorMerged) {
				continue;
			}
			var amount = 0;
			while (true) {
				var pos = block.position.plus(block.forward.times(amount + 1));
				if (!this.tinyBlocks.containsKey(pos)) {
					break;
				}
				var nextBlock = this.tinyBlocks.get(pos);
				if (nextBlock.orientation != block.orientation
					|| nextBlock.quadrant != block.quadrant
					|| nextBlock.isAttachment != block.isAttachment
					|| nextBlock.hasInterior != block.hasInterior
					|| (nextBlock.isAttachment && (nextBlock.type != block.type))
					|| nextBlock.rounded != block.rounded
					|| this.isTinyBlock(block.position.plus(block.right)) != this.isTinyBlock(nextBlock.position.plus(block.right))
					|| this.isTinyBlock(block.position.minus(block.right)) != this.isTinyBlock(nextBlock.position.minus(block.right))
					|| this.isTinyBlock(block.position.plus(block.up)) != this.isTinyBlock(nextBlock.position.plus(block.up))
					|| this.isTinyBlock(block.position.minus(block.up)) != this.isTinyBlock(nextBlock.position.minus(block.up))
					|| this.preventMergingForPerpendicularRoundedBlock(this.tinyBlocks.get(block.position.plus(block.forward.times(amount))), nextBlock)) {
						break;
				}
				amount += nextBlock.exteriorMergedBlocks;
				nextBlock.isExteriorMerged = true;
				if (nextBlock.exteriorMergedBlocks != 1) {
					break;
				}
			}
			block.exteriorMergedBlocks += amount;
		}

		for (var block of this.tinyBlocks.values()) {
			if (block.isInteriorMerged || !block.hasInterior) {
				continue;
			}
			var amount = 0;
			while (true) {
				var pos = block.position.plus(block.forward.times(amount + 1));
				if (!this.tinyBlocks.containsKey(pos)) {
					break;
				}
				var nextBlock = this.tinyBlocks.get(pos);
				if (!nextBlock.hasInterior
					|| nextBlock.orientation != block.orientation
					|| nextBlock.quadrant != block.quadrant
					|| nextBlock.type != block.type) {
						break;
				}
				amount += nextBlock.interiorMergedBlocks;
				nextBlock.isInteriorMerged = true;
				if (nextBlock.interiorMergedBlocks != 1) {
					break;
				}
			}
			block.interiorMergedBlocks += amount;
		}
    }

    private isSmallBlock(position: Vector3): boolean {
        return this.smallBlocks.containsKey(position) && !this.smallBlocks.get(position).isAttachment;
    }

    private createTinyBlock(position: Vector3, source: SmallBlock) {
        this.tinyBlocks.set(position, new TinyBlock(position, source));
    }
	
    private getNextBlock(block: TinyBlock, interior: boolean): TinyBlock {
		var mergedAmount = interior ? block.interiorMergedBlocks : block.exteriorMergedBlocks;
        return this.tinyBlocks.getOrNull(block.position.plus(block.forward.times(mergedAmount)));
    }

    private getPreviousBlock(block: TinyBlock): TinyBlock {
        return this.tinyBlocks.getOrNull(block.position.minus(block.forward));
    }

    private hasOpenEnd(block: TinyBlock, interior: boolean): boolean {
		var pos = block.position;
		var mergedAmount = interior ? block.interiorMergedBlocks : block.exteriorMergedBlocks;
        
        return !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)))
            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.horizontal.times(3)))
            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.vertical.times(3)))
            && !this.tinyBlocks.containsKey(pos.plus(block.forward.times(mergedAmount)).minus(block.horizontal.times(3)).minus(block.vertical.times(3)));
    }

    private hasOpenStart(block: TinyBlock): boolean {
        var pos = block.position;
        return !this.tinyBlocks.containsKey(pos.minus(block.forward))
            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.horizontal.times(3)))
            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.vertical.times(3)))
            && !this.tinyBlocks.containsKey(pos.minus(block.forward).minus(block.horizontal.times(3)).minus(block.vertical.times(3)));
	}
	
	private hideStartEndFaces(position: Vector3, block: TinyBlock, forward: boolean) {
		var direction = forward ? block.forward : block.forward.times(-1);
		this.hideFaceIfExists(position, direction);
		this.hideFaceIfExists(position.minus(block.horizontal), direction);
		this.hideFaceIfExists(position.minus(block.vertical), direction);
		this.hideFaceIfExists(position.minus(block.vertical).minus(block.horizontal), direction);
	}

	private hideFaceIfExists(position: Vector3, direction: Vector3) {
		if (this.tinyBlocks.containsKey(position)) {
			this.tinyBlocks.get(position).hideFace(direction);
		}
	}

	private hideOutsideFaces(centerBlock: TinyBlock) {
		var vertical = centerBlock.vertical;
		var horizontal = centerBlock.horizontal;
		centerBlock.hideFace(vertical);
		centerBlock.hideFace(horizontal);
		this.tinyBlocks.get(centerBlock.position.minus(vertical)).hideFace(horizontal);
		this.tinyBlocks.get(centerBlock.position.minus(horizontal)).hideFace(vertical);
	}
	
	private renderPerpendicularRoundedAdapters() {
		for (var block of this.smallBlocks.values()) {
			if (block.perpendicularRoundedAdapter == null) {
				continue;
			}
			
			var adapter = block.perpendicularRoundedAdapter;
			var center = block.forward.times(this.tinyIndexToWorld(block.forward.dot(block.position) * 3 - (adapter.facesForward ? 0 : 1)))
				.plus(block.right.times((block.position.dot(block.right) + (1 - block.localX)) * 0.5))
				.plus(block.up.times((block.position.dot(block.up) + (1 - block.localY)) * 0.5));
			var radius = 0.5 - this.measurements.edgeMargin;
			var forward = block.forward;
								
			for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
				var angle1 = Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter;
				var angle2 = Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter;
				var sincos1 = 1 - (block.odd() == adapter.isVertical ? Math.sin(angle1) : Math.cos(angle1));
				var sincos2 = 1 - (block.odd() == adapter.isVertical ? Math.sin(angle2) : Math.cos(angle2));
				
				let vertex1 = center.plus(block.getOnCircle(angle1).times(radius)).plus(forward.times(adapter.facesForward ? 0 : radius));
				let vertex2 = center.plus(block.getOnCircle(angle2).times(radius)).plus(forward.times(adapter.facesForward ? 0 : radius));
				var vertex3 = vertex2.plus(forward.times(sincos2 * (adapter.facesForward ? 1 : -1) * radius));
				var vertex4 = vertex1.plus(forward.times(sincos1 * (adapter.facesForward ? 1 : -1) * radius));

				var normal1 = block.getOnCircle(angle1).times(adapter.facesForward ? 1 : -1);
				var normal2 = block.getOnCircle(angle2).times(adapter.facesForward ? 1 : -1);

				this.createQuadWithNormals(
					vertex1, vertex2, vertex3, vertex4,
					normal1, normal2, normal2, normal1, adapter.facesForward);

				var invertAngle = ((adapter.isVertical ? block.localY : block.localX) != 1) != adapter.facesForward;
				var vertex5 = vertex4.plus(adapter.directionToNeighbor.times(radius * sincos1));
				var vertex6 = vertex3.plus(adapter.directionToNeighbor.times(radius * sincos2));
				var normal3 = adapter.neighbor.getOnCircle(invertAngle ? angle1 : Math.PI / 2 - angle1).times(adapter.facesForward ? -1 : 1);
				var normal4 = adapter.neighbor.getOnCircle(invertAngle ? angle2 : Math.PI / 2 - angle2).times(adapter.facesForward ? -1 : 1);

				this.createQuadWithNormals(
					vertex5, vertex6, vertex3, vertex4,
					normal3, normal4, normal4, normal3, !adapter.facesForward);
			}
		}
	}

	private isPerpendicularRoundedAdapter(block: TinyBlock) {
		if (block.perpendicularRoundedAdapter == null) {
			return false;
		}
		var localForward = block.position.minus(block.perpendicularRoundedAdapter.sourceBlock.position.times(3)).dot(block.forward);
		return localForward == 0 || (localForward > 0) == block.perpendicularRoundedAdapter.facesForward;
	}

    private renderRoundedExteriors() {
		var blockSizeWithoutMargin = 0.5 - this.measurements.edgeMargin;
		
        for (let block of this.tinyBlocks.values()) {
            if (block.isExteriorMerged || !block.isCenter || block.isAttachment) {
                continue;
            }

            var nextBlock = this.getNextBlock(block, false);
            var previousBlock = this.getPreviousBlock(block);
            var distance = block.getExteriorDepth(this);

            var hasOpenEnd = this.hasOpenEnd(block, false);
            var hasOpenStart = this.hasOpenStart(block);

            // Back cap
            if (nextBlock == null && (block.rounded || block.hasInterior)) {
				this.createCircleWithHole(block, block.hasInterior && hasOpenEnd ? this.measurements.interiorRadius : 0, blockSizeWithoutMargin, distance, false, !block.rounded);
				this.hideStartEndFaces(block.position.plus(block.forward.times(block.exteriorMergedBlocks - 1)), block, true);
            }

            // Front cap
            if (previousBlock == null && (block.rounded || block.hasInterior)) {
				this.createCircleWithHole(block, block.hasInterior && hasOpenStart ? this.measurements.interiorRadius : 0, blockSizeWithoutMargin, 0, true, !block.rounded);
				this.hideStartEndFaces(block.position, block, false);
            }

            if (block.rounded) {
				if (!this.isPerpendicularRoundedAdapter(block)) {
					this.createCylinder(block, 0, blockSizeWithoutMargin, distance);

					// Rounded to non rounded adapter
					if (nextBlock != null && !nextBlock.rounded) {
						this.createCircleWithHole(block, blockSizeWithoutMargin, blockSizeWithoutMargin, distance, true, true);
					}
					if (previousBlock != null && !previousBlock.rounded) {
						this.createCircleWithHole(block, blockSizeWithoutMargin, blockSizeWithoutMargin, 0, false, true);
					}
				}
                // Rounded corners
				for (var i = 0; i < block.exteriorMergedBlocks; i++) {
					this.hideOutsideFaces(this.tinyBlocks.get(block.position.plus(block.forward.times(i))));
				}
            }
        }
	}
	
	private renderInteriors() {
		for (let block of this.tinyBlocks.values()) {
            if (block.isInteriorMerged || !block.isCenter || !block.hasInterior) {
                continue;
			}
			
			if (block.type == BlockType.PinHole) {
				this.renderPinHoleInterior(block);
			} else if (block.type == BlockType.AxleHole) {
				this.renderAxleHoleInterior(block);
			}
        }
	}

    private renderAttachments() {
        for (var block of this.tinyBlocks.values()) {
            if (block.isExteriorMerged || !block.isCenter) {
                continue;
            }

            switch (block.type) {
                case BlockType.Pin:
                    this.renderPin(block);
                    break;
                case BlockType.Axle:
					this.renderAxle(block);
					break;
				case BlockType.BallJoint:
					this.renderBallJoint(block);
					break;
            }
        }
	}

	private renderLip(block: TinyBlock, zOffset: number) {		
		var center = block.getCylinderOrigin(this).plus(block.forward.times(zOffset));
		
		for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
			var out1 = block.getOnCircle(i / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);
			var out2 = block.getOnCircle((i + 1) / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);

			for (var j = 0; j < this.measurements.lipSubdivisions; j++) {
				var angleJ = j * Math.PI / this.measurements.lipSubdivisions;
				var angleJ2 = (j + 1) * Math.PI / this.measurements.lipSubdivisions;
				this.createQuadWithNormals(
					center.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))),
					center.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))),
					center.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))),
					center.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))),
					out1.times(-Math.sin(angleJ)).plus(block.forward.times(-Math.cos(angleJ))),
					out2.times(-Math.sin(angleJ)).plus(block.forward.times(-Math.cos(angleJ))),
					out2.times(-Math.sin(angleJ2)).plus(block.forward.times(-Math.cos(angleJ2))),
					out1.times(-Math.sin(angleJ2)).plus(block.forward.times(-Math.cos(angleJ2))));
			}
		}
	}

    private renderPin(block: TinyBlock) {
		var nextBlock = this.getNextBlock(block, false);
		var previousBlock = this.getPreviousBlock(block);

		var distance = block.getExteriorDepth(this);

		var startOffset = (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Pin) ? this.measurements.attachmentAdapterSize : 0;
		if (previousBlock == null) {
			startOffset += 2 * this.measurements.pinLipRadius;
		}
		var endOffset = (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Pin) ? this.measurements.attachmentAdapterSize : 0;
		if (nextBlock == null) {
			endOffset += 2 * this.measurements.pinLipRadius;
		}

		this.createCylinder(block, startOffset, this.measurements.pinRadius, distance - startOffset - endOffset);

		if (nextBlock == null) {
			this.createCircle(block, this.measurements.pinRadius, distance, true);
			this.renderLip(block, distance - this.measurements.pinLipRadius);
		}
		if (previousBlock == null) {
			this.createCircle(block, this.measurements.pinRadius, 0);
			this.renderLip(block, this.measurements.pinLipRadius);
		}
		if (nextBlock != null && !nextBlock.isAttachment) {
			this.createCircleWithHole(block, this.measurements.pinRadius, 0.5 - this.measurements.edgeMargin, distance, true, !nextBlock.rounded);
			this.hideStartEndFaces(nextBlock.position, block, false);
		}
		if (previousBlock != null && !previousBlock.isAttachment) {
			this.createCircleWithHole(block, this.measurements.pinRadius, 0.5 - this.measurements.edgeMargin, 0, false, !previousBlock.rounded);
			this.hideStartEndFaces(previousBlock.position, block, true);
		}
		if (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Pin) {
			this.createCircleWithHole(block, this.measurements.pinRadius, this.measurements.attachmentAdapterRadius, distance - this.measurements.attachmentAdapterSize, true);
		}
		if (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Pin) {
			this.createCircleWithHole(block, this.measurements.pinRadius, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize);
			this.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);
		}
	}

    private renderAxle(block: TinyBlock) {
		var nextBlock = this.getNextBlock(block, false);
		var previousBlock = this.getPreviousBlock(block);

		var start = block.getCylinderOrigin(this);
		var end = start.plus(block.forward.times(block.getExteriorDepth(this)));

		if (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Axle) {
			start = start.plus(block.forward.times(this.measurements.attachmentAdapterSize));
		}
		if (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.Axle) {
			end = end.minus(block.forward.times(this.measurements.attachmentAdapterSize));
		}

		var horizontalInner = block.horizontal.times(this.measurements.axleSizeInner);
		var horizontalOuter = block.horizontal.times(this.measurements.axleSizeOuter);
		var verticalInner = block.vertical.times(this.measurements.axleSizeInner);
		var verticalOuter = block.vertical.times(this.measurements.axleSizeOuter);

		var odd = block.odd();
		this.createQuad(
            start.plus(horizontalInner).plus(verticalInner),
            start.plus(horizontalInner).plus(verticalOuter),
            end.plus(horizontalInner).plus(verticalOuter),
            end.plus(horizontalInner).plus(verticalInner), odd);
		this.createQuad(
			start.plus(horizontalInner).plus(verticalInner),
			start.plus(horizontalOuter).plus(verticalInner),
			end.plus(horizontalOuter).plus(verticalInner),
			end.plus(horizontalInner).plus(verticalInner), !odd);
		this.createQuad(
			end.plus(horizontalOuter),
			start.plus(horizontalOuter),
			start.plus(horizontalOuter).plus(verticalInner),
			end.plus(horizontalOuter).plus(verticalInner), odd);
		this.createQuad(
			end.plus(verticalOuter),
			start.plus(verticalOuter),
			start.plus(verticalOuter).plus(horizontalInner),
			end.plus(verticalOuter).plus(horizontalInner), !odd);

		if (nextBlock == null) {
			this.createQuad(
				end.plus(horizontalInner).plus(verticalInner),
				end.plus(verticalInner),
				end,
				end.plus(horizontalInner), odd);
			this.createQuad(
				end.plus(horizontalInner),
				end.plus(horizontalOuter),
				end.plus(horizontalOuter).plus(verticalInner),
				end.plus(horizontalInner).plus(verticalInner), odd);
			this.createQuad(
				end.plus(verticalInner),
				end.plus(verticalOuter),
				end.plus(verticalOuter).plus(horizontalInner),
				end.plus(verticalInner).plus(horizontalInner), !odd);
		}
		if (previousBlock == null) {
			this.createQuad(
				start.plus(horizontalInner).plus(verticalInner),
				start.plus(verticalInner),
				start,
				start.plus(horizontalInner), !odd);
			this.createQuad(
				start.plus(horizontalInner),
				start.plus(horizontalOuter),
				start.plus(horizontalOuter).plus(verticalInner),
				start.plus(horizontalInner).plus(verticalInner), !odd);
			this.createQuad(
				start.plus(verticalInner),
				start.plus(verticalOuter),
				start.plus(verticalOuter).plus(horizontalInner),
				start.plus(verticalInner).plus(horizontalInner), odd);
		}

		var blockSizeWithoutMargin = 0.5 - this.measurements.edgeMargin;
		if (nextBlock != null && nextBlock.type != block.type && !nextBlock.rounded) {
			this.createQuad(
				end.plus(block.horizontal.times(blockSizeWithoutMargin)),
				end.plus(horizontalOuter),
				end.plus(horizontalOuter).plus(verticalInner),
				end.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner), odd);
			this.createQuad(
				end.plus(block.vertical.times(blockSizeWithoutMargin)),
				end.plus(verticalOuter),
				end.plus(verticalOuter).plus(horizontalInner),
				end.plus(block.vertical.times(blockSizeWithoutMargin)).plus(horizontalInner), !odd);
			this.createQuad(
				end.plus(horizontalInner).plus(verticalInner),
				end.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner),
				end.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(block.vertical.times(blockSizeWithoutMargin)),
				end.plus(horizontalInner).plus(block.vertical.times(blockSizeWithoutMargin)), !odd);
		}
		if (previousBlock != null && previousBlock.type != block.type && !previousBlock.rounded) {
			this.createQuad(
				start.plus(block.horizontal.times(blockSizeWithoutMargin)),
				start.plus(horizontalOuter),
				start.plus(horizontalOuter).plus(verticalInner),
				start.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner), !odd);
			this.createQuad(
				start.plus(block.vertical.times(blockSizeWithoutMargin)),
				start.plus(verticalOuter),
				start.plus(verticalOuter).plus(horizontalInner),
				start.plus(block.vertical.times(blockSizeWithoutMargin)).plus(horizontalInner), odd);
			this.createQuad(
				start.plus(horizontalInner).plus(verticalInner),
				start.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(verticalInner),
				start.plus(block.horizontal.times(blockSizeWithoutMargin)).plus(block.vertical.times(blockSizeWithoutMargin)),
				start.plus(horizontalInner).plus(block.vertical.times(blockSizeWithoutMargin)), odd);
		}
		if (nextBlock != null && nextBlock.type != block.type && nextBlock.rounded) {
			this.createAxleToCircleAdapter(end, block, nextBlock.isAttachment ? this.measurements.attachmentAdapterRadius : blockSizeWithoutMargin);
		}
		if (previousBlock != null && previousBlock.type != block.type && previousBlock.rounded) {
			this.createAxleToCircleAdapter(start, block, previousBlock.isAttachment ? this.measurements.attachmentAdapterRadius : blockSizeWithoutMargin, true);
		}
		if (nextBlock != null && !nextBlock.isAttachment) {
			this.hideStartEndFaces(nextBlock.position, block, false);
		}
		if (previousBlock != null && !previousBlock.isAttachment) {
			this.hideStartEndFaces(previousBlock.position, block, true);
		}
		
		if (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.Axle) {
			this.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);
		}
    }
	
	private renderBallJoint(block: TinyBlock) {
		var nextBlock = this.getNextBlock(block, false);
		var previousBlock = this.getPreviousBlock(block);

		var distance = block.getExteriorDepth(this);

		var startOffset = (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.BallJoint) ? this.measurements.attachmentAdapterSize : 0;
		if (previousBlock == null) {
			startOffset += 2 * this.measurements.pinLipRadius;
		}
		var endOffset = (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.BallJoint) ? this.measurements.attachmentAdapterSize : 0;
		if (nextBlock == null) {
			endOffset += 2 * this.measurements.pinLipRadius;
		}
		
		var ballCenterDistance: number;
		if (nextBlock == null) {
			var offset = mod(block.position.dot(block.forward) - 1, 3) - 1;
			ballCenterDistance = 0.5 - offset * this.measurements.edgeMargin;
		} else {
			var offset = mod(block.position.dot(block.forward) + block.exteriorMergedBlocks - 1, 3) - 1;
			ballCenterDistance = distance - 0.5 - offset * this.measurements.edgeMargin;
		}

		var ballCenter = block.getCylinderOrigin(this).plus(block.forward.times(ballCenterDistance));
		var angle = Math.acos(this.measurements.ballBaseRadius / this.measurements.ballRadius);

		for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
			var angleStart = lerp(-angle, +angle, i / this.measurements.subdivisionsPerQuarter);
			var angleEnd = lerp(-angle, +angle, (i+1) / this.measurements.subdivisionsPerQuarter);

			var ballCenterStart = ballCenter.plus(block.forward.times(Math.sin(angleStart) * this.measurements.ballRadius));
			var ballCenterEnd = ballCenter.plus(block.forward.times(Math.sin(angleEnd) * this.measurements.ballRadius));
			var radiusStart = this.measurements.ballRadius * Math.cos(angleStart);
			var radiusEnd = this.measurements.ballRadius * Math.cos(angleEnd);

			for (var j = 0; j < this.measurements.subdivisionsPerQuarter; j++) {
				var out1 = block.getOnCircle(j / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);
				var out2 = block.getOnCircle((j + 1) / 2 * Math.PI / this.measurements.subdivisionsPerQuarter);
	
				this.createQuadWithNormals(
					ballCenterStart.plus(out2.times(radiusStart)),
					ballCenterStart.plus(out1.times(radiusStart)),
					ballCenterEnd.plus(out1.times(radiusEnd)),
					ballCenterEnd.plus(out2.times(radiusEnd)),
					out2.times(-Math.cos(angleStart)).minus(block.forward.times(Math.sin(angleStart))),
					out1.times(-Math.cos(angleStart)).minus(block.forward.times(Math.sin(angleStart))),
					out1.times(-Math.cos(angleEnd)).minus(block.forward.times(Math.sin(angleEnd))),
					out2.times(-Math.cos(angleEnd)).minus(block.forward.times(Math.sin(angleEnd)))
				);
			}
		}
		
		var ballStart = ballCenterDistance - Math.sin(angle) * this.measurements.ballRadius;
		var ballEnd = ballCenterDistance + Math.sin(angle) * this.measurements.ballRadius;

		if (nextBlock == null) {
			this.createCircle(block, this.measurements.ballBaseRadius, ballEnd, true);
		} else {
			this.createCylinder(block, ballEnd, this.measurements.ballBaseRadius, distance - endOffset - ballEnd);
		}

		if (previousBlock == null) {
			this.createCircle(block, this.measurements.ballBaseRadius, ballStart);
		} else {
			this.createCylinder(block, startOffset, this.measurements.ballBaseRadius, ballStart - startOffset);
		}
		
		if (nextBlock != null && !nextBlock.isAttachment) {
			this.createCircleWithHole(block, this.measurements.ballBaseRadius, 0.5 - this.measurements.edgeMargin, distance, true, !nextBlock.rounded);
			this.hideStartEndFaces(nextBlock.position, block, false);
		}
		if (previousBlock != null && !previousBlock.isAttachment) {
			this.createCircleWithHole(block, this.measurements.ballBaseRadius, 0.5 - this.measurements.edgeMargin, 0, false, !previousBlock.rounded);
			this.hideStartEndFaces(previousBlock.position, block, true);
		}
		if (nextBlock != null && nextBlock.isAttachment && nextBlock.type != BlockType.BallJoint) {
			this.createCircleWithHole(block, this.measurements.ballBaseRadius, this.measurements.attachmentAdapterRadius, distance - this.measurements.attachmentAdapterSize, true);
		}
		if (previousBlock != null && previousBlock.isAttachment && previousBlock.type != BlockType.BallJoint) {
			this.createCircleWithHole(block, this.measurements.ballBaseRadius, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize);
			this.createCylinder(block, -this.measurements.attachmentAdapterSize, this.measurements.attachmentAdapterRadius, this.measurements.attachmentAdapterSize * 2);
		}
    }
	
	private createAxleToCircleAdapter(center: Vector3, block: SmallBlock, radius: number, flipped = false) {
		var horizontalInner = block.horizontal.times(this.measurements.axleSizeInner);
		var horizontalOuter = block.horizontal.times(this.measurements.axleSizeOuter);
		var verticalInner = block.vertical.times(this.measurements.axleSizeInner);
		var verticalOuter = block.vertical.times(this.measurements.axleSizeOuter);
		var odd = block.odd();

		for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
			var focus = center.copy();
			if (i < this.measurements.subdivisionsPerQuarter / 2 == !odd) {
				focus = focus.plus(horizontalInner).plus(verticalOuter);
			} else {
				focus = focus.plus(horizontalOuter).plus(verticalInner);
			}

			this.triangles.push(new Triangle(focus,
				center.plus(block.getOnCircle(Math.PI / 2 * i / this.measurements.subdivisionsPerQuarter, radius)),
				center.plus(block.getOnCircle(Math.PI / 2 * (i + 1) / this.measurements.subdivisionsPerQuarter, radius)), flipped));
		}
		this.triangles.push(new Triangle(
			center.plus(horizontalInner).plus(verticalOuter),
			center.plus(verticalOuter),
			center.plus(block.vertical.times(radius)), odd != flipped));
		this.triangles.push(new Triangle(
			center.plus(verticalInner).plus(horizontalOuter),
			center.plus(horizontalOuter),
			center.plus(block.horizontal.times(radius)), odd == flipped));
		this.createQuad(
			center.plus(verticalInner).plus(horizontalInner),
			center.plus(verticalOuter).plus(horizontalInner),
			center.plus(block.getOnCircle(45 * DEG_TO_RAD, radius)),
			center.plus(verticalInner).plus(horizontalOuter), odd != flipped);
	}

    private showInteriorCap(currentBlock: SmallBlock, neighbor: SmallBlock): boolean {
        if (neighbor == null) {
            return false;
        }
        if (neighbor.orientation != currentBlock.orientation
            || neighbor.quadrant != currentBlock.quadrant
            || !neighbor.hasInterior) {
            return true;
        }
        
        if (currentBlock.type == BlockType.AxleHole && neighbor.type == BlockType.PinHole
            || neighbor.type == BlockType.AxleHole && currentBlock.type == BlockType.PinHole) {
            // Pin hole to axle hole adapter
            return false;
        }

        return currentBlock.type != neighbor.type;
    }

    private renderPinHoleInterior(block: TinyBlock) {
		var nextBlock = this.getNextBlock(block, true);
        var previousBlock = this.getPreviousBlock(block);
        var distance = block.getInteriorDepth(this);

        var hasOpenEnd = this.hasOpenEnd(block, true);
        var hasOpenStart = this.hasOpenStart(block);
        var showInteriorEndCap = this.showInteriorCap(block, nextBlock) || (nextBlock == null && !hasOpenEnd);
		var showInteriorStartCap = this.showInteriorCap(block, previousBlock) || (previousBlock == null && !hasOpenStart);
		
		var offset = this.measurements.pinHoleOffset;
		var endMargin = showInteriorEndCap ? this.measurements.interiorEndMargin : 0;
		var startMargin = showInteriorStartCap ? this.measurements.interiorEndMargin : 0;
        var offsetStart = (hasOpenStart || showInteriorStartCap ? offset : 0) + startMargin;
		var offsetEnd = (hasOpenEnd || showInteriorEndCap ? offset : 0) + endMargin;
		var interiorRadius = this.measurements.interiorRadius;

		this.createCylinder(block, offsetStart, this.measurements.pinHoleRadius, distance - offsetStart - offsetEnd, true);

        if (hasOpenStart || showInteriorStartCap) {
            this.createCylinder(block, startMargin, interiorRadius, offset, true);
            this.createCircleWithHole(block, this.measurements.pinHoleRadius, interiorRadius, offset + startMargin, true);
        }

        if (hasOpenEnd || showInteriorEndCap) {
            this.createCylinder(block, distance - offset - endMargin, interiorRadius, offset, true);
            this.createCircleWithHole(block, this.measurements.pinHoleRadius, interiorRadius, distance - offset - endMargin, false);
        }

        if (showInteriorEndCap) {
            this.createCircle(block, interiorRadius, distance - endMargin, false);
        }
        if (showInteriorStartCap) {
            this.createCircle(block, interiorRadius, startMargin, true);
        }
    }

    private renderAxleHoleInterior(block: TinyBlock) {
        var nextBlock = this.getNextBlock(block, true);
        var previousBlock = this.getPreviousBlock(block);

        var hasOpenEnd = this.hasOpenEnd(block, true);
        var hasOpenStart = this.hasOpenStart(block);
        var showInteriorEndCap = this.showInteriorCap(block, nextBlock) || (nextBlock == null && !hasOpenEnd);
        var showInteriorStartCap = this.showInteriorCap(block, previousBlock) || (previousBlock == null && !hasOpenStart);
        
		var distance = block.getInteriorDepth(this);
		var holeSize = this.measurements.axleHoleSize;
        
        var start = block.getCylinderOrigin(this).plus(showInteriorStartCap ? block.forward.times(this.measurements.interiorEndMargin) : Vector3.zero());
        var end = start.plus(block.forward.times(distance - (showInteriorStartCap ? this.measurements.interiorEndMargin : 0) - (showInteriorEndCap ? this.measurements.interiorEndMargin : 0)));
		
		var axleWingAngle = Math.asin(holeSize / this.measurements.pinHoleRadius);
		var axleWingAngle2 = 90 * DEG_TO_RAD - axleWingAngle;
		var subdivAngle = 90 / this.measurements.subdivisionsPerQuarter * DEG_TO_RAD;
		var adjustedRadius = this.measurements.pinHoleRadius * Math.cos(subdivAngle / 2) / Math.cos(subdivAngle / 2 - (axleWingAngle - Math.floor(axleWingAngle / subdivAngle) * subdivAngle));
		this.createQuad(
			start.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
			start.plus(block.getOnCircle(axleWingAngle, adjustedRadius)),
			end.plus(block.getOnCircle(axleWingAngle, adjustedRadius)),
			end.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
			true);
		this.createQuad(
			start.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
			start.plus(block.getOnCircle(axleWingAngle2, adjustedRadius)),
			end.plus(block.getOnCircle(axleWingAngle2, adjustedRadius)),
			end.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
			false);

		for (var i = 0; i < this.measurements.subdivisionsPerQuarter; i++) {
			var angle1 = lerp(0, 90, i / this.measurements.subdivisionsPerQuarter) * DEG_TO_RAD;
			var angle2 = lerp(0, 90, (i + 1) / this.measurements.subdivisionsPerQuarter) * DEG_TO_RAD;
			var startAngleInside = angle1;
			var endAngleInside = angle2;
			var startAngleOutside = angle1;
			var endAngleOutside = angle2;
			var radius1Inside = this.measurements.pinHoleRadius;
			var radius2Inside = this.measurements.pinHoleRadius;
			var radius1Outside = this.measurements.pinHoleRadius;
			var radius2Outside = this.measurements.pinHoleRadius;
			if (angle1 < axleWingAngle && angle2 > axleWingAngle) {
				endAngleInside = axleWingAngle;
				startAngleOutside = axleWingAngle;
				radius1Outside = adjustedRadius;
				radius2Inside = adjustedRadius;
			}
			if (angle1 < axleWingAngle2 && angle2 > axleWingAngle2) {
				startAngleInside = axleWingAngle2;
				endAngleOutside = axleWingAngle2;
				radius2Outside = adjustedRadius;
				radius1Inside = adjustedRadius;
			}

			// Walls
			if (angle1 < axleWingAngle || angle2 > axleWingAngle2) {
				var v1 = block.getOnCircle(startAngleInside);
				var v2 = block.getOnCircle(endAngleInside);
				this.createQuadWithNormals(
					start.plus(v1.times(radius1Inside)),
					start.plus(v2.times(radius2Inside)),
					end.plus(v2.times(radius2Inside)),
                    end.plus(v1.times(radius1Inside)),
					v1, v2, v2, v1, false);
			}

			// Outside caps
			if (hasOpenStart || (previousBlock != null && previousBlock.type == BlockType.PinHole && !showInteriorStartCap)) {
				if (angle2 > axleWingAngle && angle1 < axleWingAngle2) {
					this.triangles.push(new Triangle(
						start.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
						start.plus(block.getOnCircle(startAngleOutside, radius1Outside)),
						start.plus(block.getOnCircle(endAngleOutside, radius2Outside))));
				}
			}
			if (hasOpenEnd || (nextBlock != null && nextBlock.type == BlockType.PinHole && !showInteriorEndCap)) {
				if (angle2 > axleWingAngle && angle1 < axleWingAngle2) {
					this.triangles.push(new Triangle(
						end.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
						end.plus(block.getOnCircle(endAngleOutside, radius2Outside)),
						end.plus(block.getOnCircle(startAngleOutside, radius1Outside))));
				}
			}

			// Inside caps
			if (showInteriorEndCap && (angle1 < axleWingAngle || angle2 > axleWingAngle2)) {
				this.triangles.push(new Triangle(
					end,
					end.plus(block.getOnCircle(startAngleInside, radius1Outside)),
					end.plus(block.getOnCircle(endAngleInside, radius2Outside))));
			}
			if (showInteriorStartCap && (angle1 < axleWingAngle || angle2 > axleWingAngle2)) {
				this.triangles.push(new Triangle(
					start,
					start.plus(block.getOnCircle(endAngleInside, radius2Outside)),
					start.plus(block.getOnCircle(startAngleInside, radius1Outside))));
			}
		}
		if (hasOpenEnd) {
			this.createCircleWithHole(block, this.measurements.pinHoleRadius, this.measurements.interiorRadius, distance, false);
		}

		if (hasOpenStart) {
			this.createCircleWithHole(block, this.measurements.pinHoleRadius, this.measurements.interiorRadius, 0, true);
		}

		if (showInteriorEndCap) {
			this.triangles.push(new Triangle(
				end.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
				end,
				end.plus(block.getOnCircle(axleWingAngle, adjustedRadius))));
			this.triangles.push(new Triangle(
				end,
				end.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
				end.plus(block.getOnCircle(axleWingAngle2, adjustedRadius))));
		}
		if (showInteriorStartCap) {
			this.triangles.push(new Triangle(
				start,
				start.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
				start.plus(block.getOnCircle(axleWingAngle, adjustedRadius))));
			this.triangles.push(new Triangle(
				start.plus(block.horizontal.times(holeSize)).plus(block.vertical.times(holeSize)),
				start,
				start.plus(block.getOnCircle(axleWingAngle2, adjustedRadius))));
		}
	}

    private isFaceVisible(position: Vector3, direction: Vector3): boolean {
		var block = this.tinyBlocks.getOrNull(position);
		return block != null
			&& !this.isTinyBlock(block.position.plus(direction))
			&& !block.isAttachment
			&& block.isFaceVisible(direction);
	}

    private createTinyFace(position: Vector3, size: Vector3, direction: Vector3) {
		var vertices: Vector3[] = null;

		if (direction.x > 0) {
			vertices = RIGHT_FACE_VERTICES;
		} else if (direction.x < 0) {
			vertices = LEFT_FACE_VERTICES;
		} else if (direction.y > 0) {
			vertices = UP_FACE_VERTICES;
		} else if (direction.y < 0) {
			vertices = DOWN_FACE_VERTICES;
		} else if (direction.z > 0) {
			vertices = FORWARD_FACE_VERTICES;
		} else if (direction.z < 0) {
			vertices = BACK_FACE_VERTICES;
		} else {
			throw new Error("Invalid direction: " + direction.toString());
		}
        
        this.createQuad(
            this.tinyBlockToWorld(position.plus(vertices[0].elementwiseMultiply(size))),
            this.tinyBlockToWorld(position.plus(vertices[1].elementwiseMultiply(size))),
            this.tinyBlockToWorld(position.plus(vertices[2].elementwiseMultiply(size))),
            this.tinyBlockToWorld(position.plus(vertices[3].elementwiseMultiply(size))));
	}

	private isRowOfVisibleFaces(position: Vector3, rowDirection: Vector3, faceDirection: Vector3, count: number): boolean {
		for (var i = 0; i < count; i++) {
			if (!this.isFaceVisible(position.plus(rowDirection.times(i)), faceDirection)) {
				return false;
			}
		}
		return true;
	}

	/* Finds a connected rectangle of visible faces in the given direction by starting with
	the supplied position and a rectangle of size 1x1 and expanding it in the 4 directions
	that are tangential to the supplied face direction, until it is no longer possible to
	expand in any direction. 
	Returns the lower left corner of the rectangle and its size.
	The component of the size vector of the direction supplied by the direction parameter is
	always 1. The component of the position vector in the direction supplied by the direction
	parameter remains unchanged. */
	private findConnectedFaces(position: Vector3, direction: Vector3): [Vector3, Vector3] {
		var tangent1 = new Vector3(direction.x == 0 ? 1 : 0, direction.x == 0 ? 0 : 1, 0);
		var tangent2 = new Vector3(0, direction.z == 0 ? 0 : 1, direction.z == 0 ? 1 : 0);

		var size = Vector3.one();
		while (true) {
			var hasChanged = false;

			if (this.isRowOfVisibleFaces(position.minus(tangent2), tangent1, direction, size.dot(tangent1))) {
				position = position.minus(tangent2);
				size = size.plus(tangent2);
				hasChanged = true;
			}
			if (this.isRowOfVisibleFaces(position.minus(tangent1), tangent2, direction, size.dot(tangent2))) {
				position = position.minus(tangent1);
				size = size.plus(tangent1);
				hasChanged = true;
			}
			if (this.isRowOfVisibleFaces(position.plus(tangent2.times(size.dot(tangent2))), tangent1, direction, size.dot(tangent1))) {
				size = size.plus(tangent2);
				hasChanged = true;
			}
			if (this.isRowOfVisibleFaces(position.plus(tangent1.times(size.dot(tangent1))), tangent2, direction, size.dot(tangent2))) {
				size = size.plus(tangent1);
				hasChanged = true;
			}

			if (!hasChanged) {
				return [position, size];
			}
		}
	}

	private hideFaces(position: Vector3, size: Vector3, direction: Vector3) {
		for (var x = 0; x < size.x; x++) {
			for (var y = 0; y < size.y; y++) {
				for (var z = 0; z < size.z; z++) {
					this.hideFaceIfExists(new Vector3(position.x + x, position.y + y, position.z + z), direction);
				}
			}
		}
	}

    private renderTinyBlockFaces() {
        for (let block of this.tinyBlocks.values()) {
			for (let direction of FACE_DIRECTIONS) {
				if (!this.isFaceVisible(block.position, direction)) {
					continue;
				}
				var expanded = this.findConnectedFaces(block.position, direction);
				var position = expanded[0];
				var size = expanded[1];
				this.createTinyFace(position, size, direction);
				this.hideFaces(position, size, direction);
			}
		}
    }
}

================================================
FILE: src/editor/Catalog.ts
================================================
class Catalog {
	private container: HTMLElement;

	private initialized: boolean = false;
	public items: CatalogItem[];

	constructor() {
		this.container = document.getElementById("catalog");
		this.createCatalogItems();
		document.getElementById("catalog").addEventListener("toggle", (event: MouseEvent) => this.onToggleCatalog(event));
	}

	private onToggleCatalog(event: MouseEvent) {
		if ((event.srcElement as HTMLDetailsElement).open && !this.initialized) {
			this.createCatalogUI();
		}
	}

	private createCatalogUI() {
		var oldRenderingContext = gl;
		var canvas = document.createElement("canvas");
		canvas.style.height = "64px";
		canvas.style.width = "64px";
		this.container.appendChild(canvas);

		var camera = new Camera(canvas, 2);
		camera.clearColor = new Vector3(0.859, 0.859, 0.859);
		var partRenderer = new MeshRenderer();
		partRenderer.color = new Vector3(0.67, 0.7, 0.71);
		var partNormalDepthRenderer = new NormalDepthRenderer();
		camera.renderers.push(partRenderer);
		camera.renderers.push(partNormalDepthRenderer);
		camera.renderers.push(new ContourPostEffect());
		var measurements = new Measurements();
		
		for (var item of this.items) {
			var catalogLink: HTMLAnchorElement = document.createElement("a");
			catalogLink.className = "catalogItem";
			catalogLink.href = "?part=" + item.string + "&name=" + encodeURIComponent(item.name);
			catalogLink.title = item.name;
			this.container.appendChild(catalogLink);
			var itemCanvas = document.createElement("canvas");
			catalogLink.appendChild(itemCanvas);
			itemCanvas.style.height = "64px";
			itemCanvas.style.width = "64px";
			var mesh = new PartMeshGenerator(item.part, measurements).getMesh();
			partRenderer.setMesh(mesh);
			partNormalDepthRenderer.setMesh(mesh);
			camera.size = (item.part.getSize() + 2) * 0.41;
			camera.transform = Matrix4.getTranslation(item.part.getCenter().times(-0.5))
				.times(Matrix4.getRotation(new Vector3(0, 45, -30))
				.times(Matrix4.getTranslation(new Vector3(-0.1, 0, 0))));
			camera.render();
			var context = itemCanvas.getContext("2d");
			context.canvas.width = gl.canvas.width;
			context.canvas.height = gl.canvas.height;
			context.drawImage(canvas, 0, 0);

			let itemCopy = item;
			catalogLink.addEventListener("click", (event: MouseEvent) => this.onSelectPart(itemCopy, event));
		}
		gl = oldRenderingContext;
		this.initialized = true;
		this.container.removeChild(canvas);
	}

	private createCatalogItems() {		
		this.items = [
			new CatalogItem(3713, "Bushing", "0z22z2"),
			new CatalogItem(32123, "Half Bushing", "0z2"),
			new CatalogItem(43093, "Axle to Pin Connector", "0z32z37z410z4"),
			new CatalogItem(6682, "Pin with Ball", "7z50z32z3"),
			new CatalogItem(2736, "Axle with Ball", "0z42z47z5"),
			new CatalogItem(6553, "Axle 1.5 with Perpendicular Axle Connector", "1ex210z07z42z40z433x2"),
			new CatalogItem(18651, "Axle 2m with Pin", "1ez432z47z410z40z32z3"),
			new CatalogItem(2853, "Crankshaft", "8z411z40z2"),
			new CatalogItem(32054, "Long Pin with Bushing Attached", "0z32z37z310z31ez232z2"),
			new CatalogItem(32138, "Double Pin With Perpendicular Axle Hole", "11y21by29z312z34fz372z30z32z31ez332z3"),
			new CatalogItem(40147, "Beam 1 x 2 with Axle Hole and Pin Hole", "7y10y2dy11y2"),
			new CatalogItem(43857, "Beam 2", "0y17y11y1dy1"),
			new CatalogItem(17141, "Beam 3", "0y17y11ey11y1dy12dy1"),
			new CatalogItem(32316, "Beam 5", "9cy14dy11ey17y10y1c9y169y12dy1dy11y1"),
			new CatalogItem(16615, "Beam 7", "1bay1113y19cy14dy11ey17y10y1215y1155y1c9y169y12dy1dy11y1"),
			new CatalogItem(41677, "Beam 2 x 0.5 with Axle Holes", "0y27y2"),
			new CatalogItem(6632, "Beam 3 x 0.5 with Axle Hole each end", "7y11ey20y2"),
			new CatalogItem(32449, "Beam 4 x 0.5 with Axle Hole each end", "7y11ey14dy20y2"),
			new CatalogItem(11478, "Beam 5 x 0.5 with Axle Holes each end", "7y11ey14dy19cy20y2"),
			new CatalogItem(32017, "Beam 5 x 0.5", "1ey14dy19cy17y10y1"),
			new CatalogItem(3704, "Axle 2", "0z42z47z410z4"),
			new CatalogItem(4519, "Axle 3", "7z410z41ez432z40z42z4"),
			new CatalogItem(2825, "Beam 1 x 4 x 0.5 with Boss", "7y11ey14dy20y21y2"),
			new CatalogItem(33299, "Half Beam 3 with Knob and Pin", "2dy342y04y217y2ay21ey3"),
			new CatalogItem(60484, "Beam 3 x 3 T-Shaped", "17x13bx11ex17x10x12ax15bx133x111x13x1"),
			new CatalogItem(6538, "Angle Connector", "7z210z20y11y1"),
			new CatalogItem(59443, "Axle Connector", "0z22z27z210z2"),
			new CatalogItem(15555, "Pin Joiner", "0z12z17z110z1"),
			new CatalogItem(36536, "Cross Block ", "9y2fy20z12z1"),
			new CatalogItem(42003, "Cross Block 1 x 3", "0z12z19z112z122y231y2"),
			new CatalogItem(32184, "Cross Block 1 x 3 with Two Axle holes", "0y21y29z112z122y231y2"),
			new CatalogItem(41678, "Cross Block 2 x 2 Split", "4z1bz10x219z12bz113x2"),
			new CatalogItem(32291, "Cross Block With Two Pinholes", "4z1bz13x219z12bz19x2"),
			new CatalogItem(32034, "Angle Connector #2", "0z22z27y11ez232z2dy1"),
			new CatalogItem(32039, "Through Axle Connector with Bushing", "0y21y29x213x2"),
			new CatalogItem(32014, "Angle Connector #6", "9y120z234z2fy10x23x2"),
			new CatalogItem(32126, "Toggle Joint Connector", "7z210z20x1"),
			new CatalogItem(44809, "Cross Block Bent 90 Degrees with Three Pinholes", "17z129z17x10y11y111x1"),
			new CatalogItem(55615, "Cross Block beam Bent 90 Degrees with 4 Pins", "17y142x18dz3c1z34x126y182z1b4z1e6x1181z31e3z31ey30y32dy31y364x1cx112ex1"),
			new CatalogItem(48989, "Cross Block Beam 3 with Four Pins", "0y31ey31y32dy34x117y142x126y114y382y323y3afy3cx164x1"),
			new CatalogItem(63869, "Cross Block 3 x 2", "1ex17x10x117z229z233x111x13x1"),
			new CatalogItem(92907, "Cross Block 2 x 2 x 2 Bent 90 Split", "2az143z166x035x213x217x07x20x2"),
			new CatalogItem(32557, "Cross Block 2 x 3 with Four Pinholes", "9z112z119x13dx10z12z1cx125x1"),
			new CatalogItem(10197, "Beam 1m with 2 Axles 90°", "7x10z42z417y426y411x1"),
			new CatalogItem(22961, "Beam 1 with Axle", "0z42z47x111x1"),
			new CatalogItem(15100, "Hole With pin", "0z32z37x111x1"),
			new CatalogItem(98989, "Cross Block 2 x 4", "7x10x117z1bz13bz124z17bz255z211x13x1"),
			new CatalogItem(27940, "Beam 1 Hole with 2 Axles 180", "7x11ez432z40z42z411x1"),
			new CatalogItem(87082, "Long Pin with Center Hole", "0z32z37x11ez332z311x1"),
			new CatalogItem(11272, "Cross Block 2 x 3", "7x211x233x23x220x24fx29x235x2"),
			new CatalogItem(32140, "Beam 2 x 4 Bent 90 Degrees, 2 and 4 holes", "a2y153y1cfy16fy151y16dy120y12fy17y2dy2"),
			new CatalogItem(32526, "Beam Bent 90 Degrees, 3 and 5 Holes", "1ey12dy14fy16by1a0y1cdy1119y115by11c2y111by1a4y121dy115dy1d1y1"),
			new CatalogItem(32056, "Beam 3 x 3 x 0.5 Bent 90", "0y27y11ey24fy1a0y2"),
			new CatalogItem(64179, "Beam Frame 5 x 7", "3bcz1466z122z136z1525z15f6z153z176z16dey1527x13c0y12a1x11c2y111bx1a4y17c5y1459y121dy1d1y15f9x1329x1169x129bz1322z19z112z11bay10y17x11ey14dx19cy1113x1215y11y12dy1c9y111x171x1161x1"),
			new CatalogItem(14720, "Beam I Frame", "115y19ex14fx120x19y1157y1fy1d5x173x135x11bey122y1219y131y19cy10y1c9y11y1"),
			new CatalogItem(53533, "Half Beam 3 with Fork", "11y13z38z31by135x073x1d5x17x01ex14dx1"),
			new CatalogItem(4261, "Steering Arm with Two Half Pins", "31z14bz162y322y319y04y2"),
			new CatalogItem(6572, "Half Beam Fork with Ball Joint", "7z010x232x170x23z58z320z050x29fx1116x2"),
			new CatalogItem(15460, "Hole with 3 Ball Joints", "0z52z57x11ez532z517y526y511x1")
		];
	}

	private onSelectPart(item: CatalogItem, event: MouseEvent) {
		editor.part = Part.fromString(item.string);
		editor.updateMesh(true);
		window.history.pushState({}, document.title, "?part=" + item.string + "&name=" + encodeURIComponent(item.name));
		event.preventDefault();
		editor.setName(item.name);
	}
}

================================================
FILE: src/editor/CatalogItem.ts
================================================
class CatalogItem {
	part: Part = null;
	id: number;
	string: string;
	name: string;

	constructor(id: number, name: string, string: string) {
		this.id = id;
		this.name = name;
		this.string = string;
		this.part = Part.fromString(string);
	}
}

================================================
FILE: src/editor/Editor.ts
================================================
enum MouseMode {
	None,
	Manipulate,
	Translate,
	Rotate
}

class Editor {
	camera: Camera;
	partRenderer: MeshRenderer;
	partNormalDepthRenderer: NormalDepthRenderer;
	contourEffect: ContourPostEffect;
	wireframeRenderer: WireframeRenderer;
	part: Part;
	canvas: HTMLCanvasElement;

	translation: Vector3 = new Vector3(0, 0, 0);
	center: Vector3;
	rotationX: number = 45;
	rotationY: number = -20;
	zoom: number = 5;
	zoomStep = 0.9;

	mouseMode = MouseMode.None;
	lastMousePosition: [number, number];

	handles: Handles;

	editorState: EditorState;

	style: RenderStyle = RenderStyle.Contour;

	measurements: Measurements = new Measurements();
	previousMousePostion: [number, number];

	constructor() {
		var url = new URL(document.URL);
		if (url.searchParams.has("part")) {
			this.part = Part.fromString(url.searchParams.get("part"));
			if (url.searchParams.has("name")) {
				this.setName(url.searchParams.get("name"));
			}
		} else {
			this.part = Part.fromString(catalog.items[Math.floor(Math.random() * catalog.items.length)].string);
		}

		this.displayMeasurements();
		
		this.editorState = new EditorState();

		this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
		this.canvas.tabIndex = 0;
		this.camera = new Camera(this.canvas);
		
		this.partRenderer = new MeshRenderer();
		this.partRenderer.color = new Vector3(0.67, 0.7, 0.71);
		this.camera.renderers.push(this.partRenderer);

		this.wireframeRenderer = new WireframeRenderer();
		this.wireframeRenderer.enabled = false;
		this.camera.renderers.push(this.wireframeRenderer);

		this.partNormalDepthRenderer = new NormalDepthRenderer();
		this.camera.renderers.push(this.partNormalDepthRenderer);

		this.contourEffect = new ContourPostEffect();
		this.camera.renderers.push(this.contourEffect);

		this.handles = new Handles(this.camera);
		this.camera.renderers.push(this.handles);

		this.center = Vector3.zero();
		this.updateMesh(true);
		this.camera.size = this.zoom;
		this.camera.render();

		this.canvas.addEventListener("mousedown", (event: MouseEvent) => this.onMouseDown(event));
		this.canvas.addEventListener("mouseup", (event: MouseEvent) => this.onMouseUp(event));
		this.canvas.addEventListener("mousemove", (event: MouseEvent) => this.onMouseMove(event));
		this.canvas.addEventListener("contextmenu", (event: Event) => event.preventDefault());
		this.canvas.addEventListener("wheel", (event: WheelEvent) => this.onScroll(event));
		window.addEventListener("keydown", (event: KeyboardEvent) => this.onKeydown(event));
		document.getElementById("clear").addEventListener("click", (event: MouseEvent) => this.clear());
		document.getElementById("share").addEventListener("click", (event: MouseEvent) => this.share());
		document.getElementById("save-stl").addEventListener("click", (event: MouseEvent) => this.saveSTL());
		document.getElementById("save-studio").addEventListener("click", (event: MouseEvent) => this.saveStudioPart());
		document.getElementById("remove").addEventListener("click", (event: MouseEvent) => this.remove());
		document.getElementById("style").addEventListener("change", (event: MouseEvent) => this.setRenderStyle(parseInt((event.srcElement as HTMLSelectElement).value)));
        window.addEventListener("resize", (e: Event) => this.camera.onResize());
		document.getElementById("applymeasurements").addEventListener("click", (event: MouseEvent) => this.applyMeasurements());
		document.getElementById("resetmeasurements").addEventListener("click", (event: MouseEvent) => this.resetMeasurements());

		this.initializeEditor("type", (typeName: string) => this.setType(typeName));
		this.initializeEditor("orientation", (orientationName: string) => this.setOrientation(orientationName));
		this.initializeEditor("size", (sizeName: string) => this.setSize(sizeName));
		this.initializeEditor("rounded", (roundedName: string) => this.setRounded(roundedName));

		document.getElementById("blockeditor").addEventListener("toggle", (event: MouseEvent) => this.onNodeEditorClick(event));

		this.getNameTextbox().addEventListener("change", (event: Event) => this.onPartNameChange(event));
		this.getNameTextbox().addEventListener("keyup", (event: Event) => this.onPartNameChange(event));
	}

	private onNodeEditorClick(event: MouseEvent) {
		this.handles.visible = (event.srcElement as HTMLDetailsElement).open;
		this.camera.render();
	}

	private saveSTL() {
		STLExporter.saveSTLFile(this.part, this.measurements, this.getName());
	}

	private saveStudioPart() {
		StudioPartExporter.savePartFile(this.part, this.measurements, this.getName());
	}

	private initializeEditor(elementId: string, onchange: (value: string) => void) {
		var element = document.getElementById(elementId);
		for (var i = 0; i < element.children.length; i++) {
			var child = element.children[i];
			if (child.tagName.toLowerCase() == "label") {				
				child.addEventListener("click", (event: Event) => onchange(((event.target as HTMLElement).previousElementSibling as HTMLInputElement).value));
			}
		}
	}

	private clear() {
		this.part.blocks.clear();
		this.updateMesh();
	}

	private share() {
		var name = this.getName();
		var url = "?part=" + this.part.toString();
		if (name.length != 0) {
			url += '&name=' + encodeURIComponent(name);
		}
		window.history.pushState({}, document.title, url);
	}

	private remove() {
		this.part.clearBlock(this.handles.getSelectedBlock(), this.editorState.orientation);
		if (this.editorState.fullSize) {
			this.part.clearBlock(this.handles.getSelectedBlock().plus(FORWARD[this.editorState.orientation]), this.editorState.orientation);
		}
		this.updateMesh();
	}

	private setType(typeName: string) {
		this.editorState.type = BLOCK_TYPE[typeName];
		this.updateBlock();
	}

	private setOrientation(orientatioName: string) {
		this.editorState.orientation = ORIENTATION[orientatioName];
		this.handles.setMode(this.editorState.fullSize, this.editorState.orientation);
		this.updateBlock();
	}

	private setSize(sizeName: string) {
		this.editorState.fullSize = sizeName == "full";
		this.handles.setMode(this.editorState.fullSize, this.editorState.orientation);
		this.camera.render();
	}

	private setRounded(roundedName: string) {
		this.editorState.rounded = roundedName == "true";
		this.updateBlock();
	}

	private setRenderStyle(style: RenderStyle) {
		this.style = style;
		this.partNormalDepthRenderer.enabled = style == RenderStyle.Contour;
		this.contourEffect.enabled = style == RenderStyle.Contour;
		this.partRenderer.enabled = style != RenderStyle.Wireframe;
		this.wireframeRenderer.enabled = style == RenderStyle.SolidWireframe || style == RenderStyle.Wireframe;
		this.updateMesh();
	}

	private updateBlock() {
		this.part.placeBlockForced(this.handles.getSelectedBlock(), new Block(this.editorState.orientation, this.editorState.type, this.editorState.rounded));
		if (this.editorState.fullSize) {
			this.part.placeBlockForced(this.handles.getSelectedBlock().plus(FORWARD[this.editorState.orientation]),
				new Block(this.editorState.orientation, this.editorState.type, this.editorState.rounded));
		}
		this.updateMesh();
	}

	public updateMesh(center = false) {
		let mesh = new PartMeshGenerator(this.part, this.measurements).getMesh();
		if (this.partRenderer.enabled) {
			this.partRenderer.setMesh(mesh);
		}
		if (this.partNormalDepthRenderer.enabled) {
			this.partNormalDepthRenderer.setMesh(mesh);
		}
		if (this.wireframeRenderer.enabled) {
			this.wireframeRenderer.setMesh(mesh);
		}

		var newCenter = this.part.getCenter().times(-0.5);
		if (center) {
			this.translation = Vector3.zero();
		} else {
			this.translation = this.translation.plus(this.getRotation().transformDirection(this.center.minus(newCenter)));
		}
		this.center = newCenter;
		this.updateTransform();
		this.handles.updateTransforms();
		this.camera.render();
	}

	private getRotation(): Matrix4 {
		return Matrix4.getRotation(new Vector3(0, this.rotationX, this.rotationY));
	}

	private updateTransform() {
		this.camera.transform = 
			Matrix4.getTranslation(this.center)
			.times(this.getRotation())
			.times(Matrix4.getTranslation(this.translation.plus(new Vector3(0, 0, -15))));
	}

	private onMouseDown(event: MouseEvent) {
		this.canvas.focus();
		const {ctrlKey, shiftKey} = event;
		if (event.button === 0 && !ctrlKey && !shiftKey) {
			if (this.handles.onMouseDown(event)) {
				this.mouseMode = MouseMode.Manipulate;
			}
		} else if (event.button === 1 || shiftKey) {
			this.mouseMode = MouseMode.Translate;
			this.previousMousePostion = [event.clientX, event.clientY];
		} else if (event.button === 2 || ctrlKey) {
			this.mouseMode = MouseMode.Rotate;
		}
		event.preventDefault();
	}

	private onMouseUp(event: MouseEvent) {
		this.mouseMode = MouseMode.None;
		this.handles.onMouseUp();
		event.preventDefault();
	}

	private onMouseMove(event: MouseEvent) {
		switch (this.mouseMode) {
			case MouseMode.None:
			case MouseMode.Manipulate:
				this.handles.onMouseMove(event);
				break;
			case MouseMode.Translate:
				this.translation = this.translation.plus(new Vector3(event.clientX - this.previousMousePostion[0], -(event.clientY - this.previousMousePostion[1]), 0).times(this.camera.size / this.canvas.clientHeight));
				this.previousMousePostion = [event.clientX, event.clientY];
				this.updateTransform();
				this.camera.render();
				break;
			case MouseMode.Rotate:
				this.rotationX -= event.movementX * 0.6;
				this.rotationY = clamp(-90, 90, this.rotationY - event.movementY * 0.6);
				
				this.updateTransform();
				this.camera.render();
				break;
		}
	}

	private onScroll(event: WheelEvent) {
		this.zoom *= event.deltaY < 0 ? this.zoomStep : 1 / this.zoomStep;
		this.camera.size = this.zoom;
		this.camera.render();
	}

	private onKeydown(event: KeyboardEvent) {
		const keyActions: { [key: string]: () => void } = {
			'1': () => this.setType('pinhole'),
			'2': () => this.setType('axlehole'),
			'3': () => this.setType('pin'),
			'4': () => this.setType('axle'),
			'5': () => this.setType('solid'),
			'6': () => this.setType('balljoint'),
			'y': () => this.setOrientation('y'),
			'z': () => this.setOrientation('z'),
			'x': () => this.setOrientation('x'),
			'PageUp': () => this.handles.move(new Vector3(0, 1, 0)),
			'PageDown': () => this.handles.move(new Vector3(0, -1, 0)),
			'ArrowLeft': () => this.handles.move(new Vector3(0, 0, 1)),
			'ArrowRight': () => this.handles.move(new Vector3(0, 0, -1)),
			'ArrowUp': () => this.handles.move(new Vector3(-1, 0, 0)),
			'ArrowDown': () => this.handles.move(new Vector3(1, 0, 0)),
			'Backspace': () => this.remove(),
			'Delete': () => this.remove(),
		};

		if (event.key in keyActions && document.activeElement == this.canvas) {
			keyActions[event.key]();
		}
	}
	private displayMeasurements() {
		for (var namedMeasurement of NAMED_MEASUREMENTS) {
			namedMeasurement.applyToDom(this.measurements);
		}
	}

	public applyMeasurements() {
		for (var namedMeasurement of NAMED_MEASUREMENTS) {
			namedMeasurement.readFromDOM(this.measurements);
		}
		this.measurements.enforceConstraints();
		this.displayMeasurements();
		this.updateMesh();
	}

	private resetMeasurements() {
		this.measurements = new Measurements();
		this.displayMeasurements();
		this.updateMesh();
	}

	public getNameTextbox(): HTMLInputElement {
		return document.getElementById('partName') as HTMLInputElement;
	}

	public getName(): string {
		var name = this.getNameTextbox().value.trim();
		if (name.length == 0) {
			name = 'Part';
		}
		return name;
	}

	private onPartNameChange(event: Event) {
		var name = this.getNameTextbox().value.trim();
		if (name.length == 0) {
			document.title = 'Part Designer';
		} else {
			document.title = name + ' ⋅ Part Designer';
		}
	}

	public setName(name: string) {
		document.title = name + ' ⋅ Part Designer';
		this.getNameTextbox().value = name;
	}
}


================================================
FILE: src/editor/EditorState.ts
================================================
class EditorState {
	public orientation: Orientation = Orientation.X;
	public type: BlockType = BlockType.PinHole;
	public fullSize: boolean = true;
	public rounded: boolean = true;
}

================================================
FILE: src/editor/Handles.ts
================================================
const ARROW_RADIUS_INNER = 0.05;
const ARROW_RADIUS_OUTER = 0.15;
const ARROW_LENGTH = 0.35;
const ARROW_TIP = 0.15;

const HANDLE_DISTANCE = 0.5;

const GRAB_RADIUS = 0.1;
const GRAB_START = 0.4;
const GRAB_END = 1.1;

const UNSELECTED_ALPHA = 0.5;

enum Axis {
	None,
	X,
	Y,
	Z
}

class Handles implements Renderer {
	private xNegative: MeshRenderer;
	private xPositive: MeshRenderer;
	private yNegative: MeshRenderer;
	private yPositive: MeshRenderer;
	private zNegative: MeshRenderer;
	private zPositive: MeshRenderer;
	private meshRenderers: MeshRenderer[] = [];

	private position: Vector3;
	private block: Vector3;
	private camera: Camera;

	private handleAlpha: Vector3 = Vector3.one().times(UNSELECTED_ALPHA);

	private grabbedAxis: Axis = Axis.None;
	private grabbedPosition: number;

	visible: boolean = true;
	
	private box: WireframeBox;

	private fullSize: boolean = true;
	private orientation: Orientation = Orientation.X;
	private size: Vector3;

	private createRenderer(mesh: Mesh, color: Vector3): MeshRenderer {
		let renderer = new MeshRenderer();
		renderer.setMesh(mesh);
		renderer.color = color;
		this.meshRenderers.push(renderer);
		return renderer;
	}

	private getBlockCenter(block: Vector3): Vector3 {
		if (this.fullSize) {
			return this.block.plus(Vector3.one()).times(0.5);
		} else {
			return this.block.plus(Vector3.one()).times(0.5).minus(FORWARD[this.orientation].times(0.25));
		}
	}

	private getBlock(worldPosition: Vector3): Vector3 {
		if (this.fullSize) {
			return worldPosition.times(2).minus(Vector3.one().times(0.5)).floor();
		} else {
			return worldPosition.times(2).minus(Vector3.one().minus(FORWARD[this.orientation]).times(0.5)).floor();
		}
	}

	constructor(camera: Camera) {
		this.box = new WireframeBox();
		let mesh = Handles.getArrowMesh(20);

		this.xNegative = this.createRenderer(mesh, new Vector3(1, 0, 0));
		this.xPositive = this.createRenderer(mesh, new Vector3(1, 0, 0));
		this.yNegative = this.createRenderer(mesh, new Vector3(0, 1, 0));
		this.yPositive = this.createRenderer(mesh, new Vector3(0, 1, 0));
		this.zNegative = this.createRenderer(mesh, new Vector3(0, 0, 1));
		this.zPositive = this.createRenderer(mesh, new Vector3(0, 0, 1));
		
		this.block = Vector3.zero();
		this.setMode(true, Orientation.X, false);
		this.camera = camera;
	}

	public render(camera: Camera) {
		if (!this.visible) {
			return;
		}

		this.box.render(camera);

		this.xPositive.alpha = this.handleAlpha.x;
		this.xNegative.alpha = this.handleAlpha.x;
		this.yPositive.alpha = this.handleAlpha.y;
		this.yNegative.alpha = this.handleAlpha.y;
		this.zPositive.alpha = this.handleAlpha.z;
		this.zNegative.alpha = this.handleAlpha.z;

		gl.colorMask(false, false, false, false);
		gl.depthFunc(gl.ALWAYS);
		for (let renderer of this.meshRenderers) {
			renderer.render(camera);
		}
		gl.depthFunc(gl.LEQUAL);
		for (let renderer of this.meshRenderers) {
			renderer.render(camera);
		}
		gl.colorMask(true, true, true, true);
		for (let renderer of this.meshRenderers) {
			renderer.render(camera);
		}
	}

	public updateTransforms() {
		this.xPositive.transform = Quaternion.euler(new Vector3(0, -90, 0)).toMatrix()
			.times(Matrix4.getTranslation(this.position.plus(new Vector3(this.size.x * HANDLE_DISTANCE, 0, 0))));
		this.xNegative.transform = Quaternion.euler(new Vector3(0, 90, 0)).toMatrix()
			.times(Matrix4.getTranslation(this.position.plus(new Vector3(this.size.x * -HANDLE_DISTANCE, 0, 0))));
		this.yPositive.transform = Quaternion.euler(new Vector3(90, 0, 0)).toMatrix()
			.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, this.size.y * HANDLE_DISTANCE, 0))));
		this.yNegative.transform = Quaternion.euler(new Vector3(-90, 0, 0)).toMatrix()
			.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, this.size.y * -HANDLE_DISTANCE, 0))));
		this.zPositive.transform = Matrix4.getTranslation(this.position.plus(new Vector3(0, 0, this.size.z * HANDLE_DISTANCE)));
		this.zNegative.transform = Quaternion.euler(new Vector3(180, 0, 0)).toMatrix()
			.times(Matrix4.getTranslation(this.position.plus(new Vector3(0, 0, this.size.z * -HANDLE_DISTANCE))));
			
		this.box.transform = Matrix4.getTranslation(this.getBlockCenter(this.block));
		this.box.scale = this.size.times(0.5);
	}

	private static getVector(angle: number, radius: number, z: number): Vector3 {
		return new Vector3(radius * Math.cos(angle), radius * Math.sin(angle), z);
	}
	
	public static getArrowMesh(subdivisions: number): Mesh {
		let triangles: Triangle[] = [];

		for (let i = 0; i < subdivisions; i++) {
			let angle1 = i / subdivisions * 2 * Math.PI;
			let angle2 = (i + 1) / subdivisions * 2 * Math.PI;

			// Base
			triangles.push(new Triangle(Handles.getVector(angle1, ARROW_RADIUS_INNER, 0), Vector3.zero(), Handles.getVector(angle2, ARROW_RADIUS_INNER, 0)));
			// Side
			triangles.push(new TriangleWithNormals(
				Handles.getVector(angle1, ARROW_RADIUS_INNER, 0),
				Handles.getVector(angle2, ARROW_RADIUS_INNER, 0),
				Handles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle1, 1, 0).times(-1),
				Handles.getVector(angle2, 1, 0).times(-1),
				Handles.getVector(angle2, 1, 0).times(-1)));
			triangles.push(new TriangleWithNormals(
				Handles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle1, ARROW_RADIUS_INNER, 0),
				Handles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle1, 1, 0).times(-1),
				Handles.getVector(angle1, 1, 0).times(-1),
				Handles.getVector(angle2, 1, 0).times(-1)));
			// Tip base
			triangles.push(new Triangle(
				Handles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle2, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH)));
			triangles.push(new Triangle(
				Handles.getVector(angle1, ARROW_RADIUS_OUTER, ARROW_LENGTH),
				Handles.getVector(angle1, ARROW_RADIUS_INNER, ARROW_LENGTH),
				Handles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH)));
			// Tip
			let alpha = Math.tan(ARROW_TIP / ARROW_RADIUS_OUTER);

			triangles.push(new TriangleWithNormals(
				new Vector3(0, 0, ARROW_LENGTH + ARROW_TIP),
				Handles.getVector(angle1, ARROW_RADIUS_OUTER, ARROW_LENGTH),
				Handles.getVector(angle2, ARROW_RADIUS_OUTER, ARROW_LENGTH),
				Handles.getVector(angle1, -Math.sin(alpha), -Math.cos(alpha)),
				Handles.getVector(angle1, -Math.sin(alpha), -Math.cos(alpha)),
				Handles.getVector(angle2, -Math.sin(alpha), -Math.cos(alpha))));
		}

		return new Mesh(triangles);
	}

	private getRay(axis: Axis): Ray {
		switch (axis) {
			case Axis.X:
				return new Ray(this.position, new Vector3(1, 0, 0));
			case Axis.Y:
				return new Ray(this.position, new Vector3(0, 1, 0));
			case Axis.Z:
				return new Ray(this.position, new Vector3(0, 0, 1));
		}
		throw new Error("Unknown axis: " + axis);
	}

	private getMouseHandle(event: MouseEvent): [Axis, number] {
		var mouseRay = this.camera.getScreenToWorldRay(event);
		for (let axis of [Axis.X, Axis.Y, Axis.Z]) {
			var axisRay = this.getRay(axis);
			if (mouseRay.getDistanceToRay(axisRay) < GRAB_RADIUS) {
				var position = axisRay.getClosestToRay(mouseRay);
				if (Math.abs(position) > GRAB_START && Math.abs(position) < GRAB_END) {
					return [axis, position];
				}
			}
		}
		return [Axis.None, 0];
	}

	public onMouseDown(event: MouseEvent): boolean {
		var handleData = this.getMouseHandle(event);
		this.grabbedAxis = handleData[0];
		this.grabbedPosition = handleData[1];		
		return this.grabbedAxis != Axis.None;
	}

	public onMouseMove(event: MouseEvent) {
		if (this.grabbedAxis != Axis.None) {
			var mouseRay = this.camera.getScreenToWorldRay(event);
			var axisRay = this.getRay(this.grabbedAxis);
			var mousePosition = axisRay.getClosestToRay(mouseRay);

			this.position = this.position.plus(axisRay.direction.times(mousePosition - this.grabbedPosition));			
			this.block = this.getBlock(this.position);
			this.updateTransforms();
			this.camera.render();
		} else {
			var axis = this.getMouseHandle(event)[0];
			var newAlpha = new Vector3(axis == Axis.X ? 1 : UNSELECTED_ALPHA, axis == Axis.Y ? 1 : UNSELECTED_ALPHA, axis == Axis.Z ? 1 : UNSELECTED_ALPHA);
			if (!newAlpha.equals(this.handleAlpha)) {
				this.handleAlpha = newAlpha;
				this.camera.render();
			}
		}
	}

	public onMouseUp() {
		if (this.grabbedAxis != Axis.None) {
			this.grabbedAxis = Axis.None;
			this.animatePositionAndSize(this.getBlockCenter(this.block), this.size, false, 100);
		}
	}

	public move(direction: Vector3) {
		this.position = this.position.plus(direction);
		this.block = this.getBlock(this.position);
		this.updateTransforms();
		this.camera.render();
	}

	public getSelectedBlock(): Vector3 {
		return this.block;
	}

	public setMode(fullSize: boolean, orientation: Orientation, animate: boolean = true) {
		if (this.fullSize == fullSize && this.orientation == orientation && animate) {
			return;
		}

		switch (orientation) {
			case Orientation.X:
				this.box.color = new Vector3(1.0, 0.0, 0.0);
				break;
			case Orientation.Y:
				this.box.color = new Vector3(0.0, 0.8, 0.0);
				break;
			case Orientation.Z:
				this.box.color = new Vector3(0.0, 0.0, 1.0);
				break;
		}
		
		this.fullSize = fullSize;
		this.orientation = orientation;

		var targetPosition = this.getBlockCenter(this.block);
		var targetSize = Vector3.one();
		if (!this.fullSize) {
			targetSize = targetSize.minus(FORWARD[this.orientation].times(0.5));
		}
		
		if (!animate) {
			this.position = targetPosition;
			this.size = Vector3.one();
			this.updateTransforms();
			return;
		}

		this.animatePositionAndSize(targetPosition, targetSize);
	}

	private animatePositionAndSize(targetPosition: Vector3, targetSize: Vector3, animateBox: boolean = true, time = 300) {
		var startPosition = this.position;
		var startSize = this.size;

		var start = new Date().getTime();
		var end = start + time;
		var handles = this;

		function callback() {
			var progress = ease(Math.min(1.0, (new Date().getTime() - start) / (end - start)));
			handles.position = Vector3.lerp(startPosition, targetPosition, progress);
			handles.size = Vector3.lerp(startSize, targetSize, progress);
			handles.updateTransforms();
			if (animateBox) {
				handles.box.transform = Matrix4.getTranslation(handles.position);
			}
			handles.camera.render();
			if (progress < 1.0) {
				window.requestAnimationFrame(callback);
			}
		}
		window.requestAnimationFrame(callback)
	}
}

================================================
FILE: src/editor/NamedMeasurement.ts
================================================
class NamedMeasurement {
	private name: string;
	private relative: boolean;
	private displayDouble: boolean;
	private domElement: HTMLInputElement;
	private resetElement: HTMLAnchorElement;

	constructor(name: string, relative: boolean, displayDouble: boolean) {
		this.name = name;
		this.relative = relative;
		this.displayDouble = displayDouble;
		this.domElement = document.getElementById(name) as HTMLInputElement;
		this.resetElement = this.domElement.previousElementSibling as HTMLAnchorElement;
		if (this.domElement == null) {
			throw new Error("DOM Element " + this.name + " not found.");
		}
		this.resetElement.addEventListener("click", (event: MouseEvent) => this.reset(event));
	}

	public readFromDOM(measurements: Measurements) {
		var value = parseFloat(this.domElement.value);
		if (!isFinite(value) || value < 0) {
			return;
		}
		if (this.relative) {
			value /= measurements.technicUnit;
		}
		if (this.displayDouble) {
			value /= 2;
		}
		measurements[this.name] = value;
	}

	public applyToDom(measurements: Measurements) {
		var value: number = measurements[this.name];
		if (this.relative) {
			value *= measurements.technicUnit;
		}
		if (this.displayDouble) {
			value *= 2;
		}
		value = Math.round(value * 1000) / 1000;
		this.domElement.value = value.toString();
		this.resetElement.style.visibility = measurements[this.name] == DEFAULT_MEASUREMENTS[this.name] ? "hidden" : "visible";
	}

	private reset(event: MouseEvent) {
		editor.measurements[this.name] = DEFAULT_MEASUREMENTS[this.name];
		this.applyToDom(DEFAULT_MEASUREMENTS);
		editor.updateMesh();
		event.preventDefault();
	}
}

const NAMED_MEASUREMENTS : NamedMeasurement[] = [
	new NamedMeasurement("technicUnit", false, false),
	new NamedMeasurement("edgeMargin", true, false),
	new NamedMeasurement("interiorRadius", true, true),
	new NamedMeasurement("pinHoleRadius", true, true),
	new NamedMeasurement("pinHoleOffset", true, false),
	new NamedMeasurement("axleHoleSize", true, true),
	new NamedMeasurement("pinRadius", true, true),
	new NamedMeasurement("pinLipRadius", true, true),
	new NamedMeasurement("axleSizeInner", true, false),
	new NamedMeasurement("axleSizeOuter", true, false),
	new NamedMeasurement("attachmentAdapterSize", true, true),
	new NamedMeasurement("attachmentAdapterRadius", true, true),
	new NamedMeasurement("interiorEndMargin", true, false),
	new NamedMeasurement("lipSubdivisions", false, false),
	new NamedMeasurement("subdivisionsPerQuarter", false, false),
	new NamedMeasurement("ballRadius", true, true),
	new NamedMeasurement("ballBaseRadius", true, true)
]

================================================
FILE: src/editor/RenderStyle.ts
================================================
enum RenderStyle {
	Contour,
	Solid,
	Wireframe,
	SolidWireframe
}

================================================
FILE: src/export/STLExporter.ts
================================================
class STLExporter {
    private readonly buffer: ArrayBuffer;
    private readonly view: DataView;

    constructor(size: number) {
        this.buffer = new ArrayBuffer(size);
        this.view = new DataView(this.buffer, 0, size);
    }

    private writeVector(offset: number, vector: Vector3) {
        this.view.setFloat32(offset, vector.z, true);
        this.view.setFloat32(offset + 4, vector.x, true);
        this.view.setFloat32(offset + 8, vector.y, true);
    }

    private writeTriangle(offset: number, triangle: Triangle, scalingFactor: number) {
        this.writeVector(offset, triangle.normal().times(-1));
        this.writeVector(offset + 12, triangle.v1.times(scalingFactor));
        this.writeVector(offset + 24, triangle.v2.times(scalingFactor));
        this.writeVector(offset + 36, triangle.v3.times(scalingFactor));
        this.view.setInt16(offset + 48, 0, true);
    }

    private static fixOpenEdges(triangles: Triangle[]): Triangle[] {
        var points: Vector3[] = [];

        for (var triangle of triangles) {
            if (!containsPoint(points, triangle.v1)) {
                points.push(triangle.v1);
            }
            if (!containsPoint(points, triangle.v2)) {
                points.push(triangle.v2);
            }
            if (!containsPoint(points, triangle.v3)) {
                points.push(triangle.v3);
            }
        }

        var result: Triangle[] = [];

        for (var triangle of triangles) {
            var edge1Hits: number[] = [0];
            var edge2Hits: number[] = [0];
            var edge3Hits: number[] = [0];

            var edge1Direction = triangle.v2.minus(triangle.v1);
            var edge2Direction = triangle.v3.minus(triangle.v2);
            var edge3Direction = triangle.v1.minus(triangle.v3);

            let edge1LengthSquared = Math.pow(edge1Direction.magnitude(), 2);
            let edge2LengthSquared = Math.pow(edge2Direction.magnitude(), 2);
            let edge3LengthSquared = Math.pow(edge3Direction.magnitude(), 2);

            for (var point of points) {
                var vertex1Relative = point.minus(triangle.v1);
                var vertex2Relative = point.minus(triangle.v2);
                var vertex3Relative = point.minus(triangle.v3);

                if (Vector3.isCollinear(edge1Direction, vertex1Relative)) {
                    let progress = vertex1Relative.dot(edge1Direction) / edge1LengthSquared;
                    if (progress > 0.0001 && progress < 0.999) {
                        edge1Hits.push(progress);
                        continue;
                    }
                    continue;
                }

                if (Vector3.isCollinear(edge2Direction, vertex2Relative)) {
                    let progress = vertex2Relative.dot(edge2Direction) / edge2LengthSquared;
                    if (progress > 0.0001 && progress < 0.999) {
                        edge2Hits.push(progress);
                        continue;
                    }
                    continue;
                }

                if (Vector3.isCollinear(edge3Direction, vertex3Relative)) {
                    let progress = vertex3Relative.dot(edge3Direction) / edge3LengthSquared;
                    if (progress > 0.0001 && progress < 0.999) {
                        edge3Hits.push(progress);
                        continue;
                    }
                    continue;
                }
            }

            if (edge1Hits.length == 1 && edge2Hits.length == 1 && edge3Hits.length == 1) {
                result.push(triangle);
                continue;
            }

            edge1Hits.sort();
            edge2Hits.sort();
            edge3Hits.sort();

            for (var i = 0; i < edge1Hits.length - 1; i++) {
                result.push(new Triangle(
                    triangle.getOnEdge1(edge1Hits[i]),
                    triangle.getOnEdge1(edge1Hits[i + 1]),
                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1])
                ));
            }
            for (var i = 0; i < edge2Hits.length - 1; i++) {
                result.push(new Triangle(
                    triangle.getOnEdge2(edge2Hits[i]),
                    triangle.getOnEdge2(edge2Hits[i + 1]),
                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1])
                ));
            }
            for (var i = 0; i < edge3Hits.length - 1; i++) {
                result.push(new Triangle(
                    triangle.getOnEdge3(edge3Hits[i]),
                    triangle.getOnEdge3(edge3Hits[i + 1]),
                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1])
                ));
            }
            if (edge1Hits.length > 1 && edge2Hits.length == 1) {
                result.push(new Triangle(
                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1]),
                    triangle.getOnEdge2(edge2Hits[0]),
                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1])
                ));
            }
            else if (edge2Hits.length > 1 && edge3Hits.length == 1) {
                result.push(new Triangle(
                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1]),
                    triangle.getOnEdge3(edge3Hits[0]),
                    triangle.getOnEdge1(edge1Hits[edge1Hits.length - 1])
                ));
            }
            else if (edge3Hits.length > 1 && edge1Hits.length == 1) {
                result.push(new Triangle(
                    triangle.getOnEdge3(edge3Hits[edge3Hits.length - 1]),
                    triangle.getOnEdge1(edge1Hits[0]),
                    triangle.getOnEdge2(edge2Hits[edge2Hits.length - 1])
                ));
            }
        }

        return result;
    }

    private static createBuffer(part: Part, measurements: Measurements) {
        let mesh = new PartMeshGenerator(part, measurements).getMesh();
        let triangles = STLExporter.fixOpenEdges(mesh.triangles);

        let exporter = new STLExporter(84 + 50 * triangles.length);
        
        for (var i = 0; i < 80; i++) {
            exporter.view.setInt8(i, 0);
        }
        
        var p = 80;
        exporter.view.setInt32(p, triangles.length, true);
        p += 4;

        for (let triangle of triangles) {
            exporter.writeTriangle(p, triangle, measurements.technicUnit);
            p += 50;
        }

        return exporter.buffer;
    }
    
    public static saveSTLFile(part: Part, measurements: Measurements, name="part") {
        let filename = name.toLowerCase().replaceAll(" ", "_") + ".stl";
        let blob = new Blob([STLExporter.createBuffer(part, measurements)], { type: "application/octet-stream" });
        let link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename;
        link.click();
    }
}

================================================
FILE: src/export/StudioPartExporter.ts
================================================
class StudioPartExporter {
    private static formatPoint(vector: Vector3): string {
        return (vector.x * 20).toFixed(4) + " " + (-vector.y * 20).toFixed(4) + " " + (-vector.z * 20).toFixed(4);
    }

    private static formatVector(vector: Vector3): string {
        return (vector.x).toFixed(4) + " " + (-vector.y).toFixed(4) + " " + (-vector.z).toFixed(4);
    }

    private static formatConnector(position: Vector3, block: Block, facesForward: boolean): string {
        let result = "0 PE_CONN ";

        switch (block.type) {
            case BlockType.PinHole: result += "0 2"; break;
            case BlockType.AxleHole: result += "0 6"; break;
            case BlockType.Axle: result += "0 7"; break;
            case BlockType.Pin: result += "0 3"; break;
            case BlockType.BallJoint: result += "1 5"; break;
            default: throw new Error("Unknown block type: " + block.type);
        }

        if (facesForward) {
            result += " "
                + StudioPartExporter.formatVector(block.right) + " "
                + StudioPartExporter.formatVector(block.forward) + " "
                + StudioPartExporter.formatVector(block.up) + " "
                + StudioPartExporter.formatPoint(position.plus(new Vector3(1, 1, 1).plus(block.forward)).times(0.5));
        } else {
            result += " "
                + StudioPartExporter.formatVector(block.right.times(-1)) + " "
                + StudioPartExporter.formatVector(block.forward.times(-1)) + " "
                + StudioPartExporter.formatVector(block.up) + " "
                + StudioPartExporter.formatPoint(position.plus(new Vector3(1, 1, 1).minus(block.forward)).times(0.5));
        }

         
        result += " 0 0 0.8 0 0\n";
        return result;
    }

    private static createFileContent(part: Part, measurements: Measurements, name: string, filename: string): string {
        let smallBlocks = part.createSmallBlocks();
        let mesh = new PartMeshGenerator(part, measurements).getMesh();

        var result: string = `0 FILE ` + filename + `
0 Description: part
0 Name: ` + name + `
0 Author: 
0 BFC CERTIFY CCW
1 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
0 NOFILE
0 FILE part.obj_grouped
0 Description: part.obj_grouped
0 Name: 
0 Author: 
0 ModelType: Part
0 BFC CERTIFY CCW
1 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
`;

        for (let position of part.blocks.keys()) {
            let startBlock = part.blocks.get(position);

            if (startBlock.type == BlockType.Solid) {
                continue;
            }

            let previousBlock = part.blocks.getOrNull(position.minus(startBlock.forward));
            let isFirstInRow = previousBlock == null || previousBlock.orientation != startBlock.orientation || previousBlock.type != startBlock.type;

            if (!isFirstInRow) {
                continue;
            }

            let facesForward = false;

            if (startBlock.isAttachment) {
                for (let x = 0; x <= 1; x++) {
                    for (let y = 0; y <= 1; y++) {
                        let supportBlockPosition = position.minus(startBlock.forward).plus(startBlock.right.times(x)).plus(startBlock.up.times(y));
                        let supportBlock = smallBlocks.getOrNull(supportBlockPosition);
                        if (supportBlock != null && !supportBlock.isAttachment) {
                            facesForward = true;
                            break;
                        }
                    }
                    if (facesForward) {
                        break;
                    }
                }
            }

            let block = startBlock;
            let offset = 0;
            while (true) {
                let nextBlock = part.blocks.getOrNull(position.plus(startBlock.forward));
                let isLastInRow = nextBlock == null || nextBlock.orientation != startBlock.orientation || nextBlock.type != startBlock.type;

                if (isLastInRow && offset % 2 == 0 && offset > 0) {
                    result += StudioPartExporter.formatConnector(position.minus(startBlock.forward), block, facesForward);
                } else if (offset % 2 == 0) {
                    result += StudioPartExporter.formatConnector(position, block, facesForward);
                }

                if (isLastInRow) {
                    break;
                }

                offset += 1;
                position = position.plus(startBlock.forward);
                block = nextBlock;
            }
        }

        result += `
0 NOFILE
0 FILE part.obj
0 Description: part.obj
0 Name: 
0 Author: 
0 BFC CERTIFY CCW
`;

        for (let triangle of mesh.triangles) {
            result += "3 16 " + this.formatPoint(triangle.v1) + " " + this.formatPoint(triangle.v2) + " " + this.formatPoint(triangle.v3) + "\n";
        }

        result += "0 NOFILE\n";
        return result;
    }   

    public static savePartFile(part: Part, measurements: Measurements, name = "part") {
        let filename = name.toLowerCase().replaceAll(" ", "_") + ".part";
        let content = StudioPartExporter.createFileContent(part, measurements, name, filename);
        let link = document.createElement('a');
        link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
        link.download = filename;
        link.click();
    }
}

================================================
FILE: src/functions.ts
================================================
function triangularNumber(n: number): number {
	return n * (n + 1) / 2;
}

function inverseTriangularNumber(s: number): number {
	return Math.floor((Math.floor(Math.sqrt(8 * s + 1)) - 1) / 2);
}

function tetrahedralNumber(n: number): number {
	return n * (n + 1) * (n + 2) / 6;
}

function inverseTetrahedralNumber(s: number): number {
	if (s == 0) {
		return 0;
	}
	let f = Math.pow(1.73205080757 * Math.sqrt(243 * Math.pow(s, 2) - 1) + 27 * s, 1 / 3);
	return Math.floor(f / 2.08008382305 + 0.69336127435 / f - 1);
}

let DEG_TO_RAD = Math.PI / 180;

function min<T>(iterable: Iterable<T>, selector: (item: T) => number): number {
	var initialized = false;
	var minValue: number;

	for (let item of iterable) {
		let currentValue = selector(item);
		if (!initialized || currentValue < minValue) {
			initialized = true;
			minValue = currentValue;
		}
	}
	return minValue;
}

function sign(a: number): number {
	if (a == 0) {
		return 0;
	} else if (a < 0) {
		return -1;
	} else {
		return 1;
	}
}

function lerp(a: number, b: number, t: number): number {
	return a + t * (b - a);
}

function clamp(lower: number, upper: number, value: number) {
	if (value > upper) {
		return upper;
	} else if (value < lower) {
		return lower;
	} else {
		return value;
	}
}

function countInArray<T>(items: T[], selector: (item: T) => boolean): number {
	var result = 0;
	for (var item of items) {
		if (selector(item)) {
			result++;
		}
	}
	return result;
}

function ease(value: number): number {
	return value < 0.5 ? 2 * value * value : -1 + (4 - 2 * value) * value;
}

function mod(a: number, b: number): number {
	return ((a % b) + b) % b;
}

function containsPoint(list: Vector3[], query: Vector3): boolean {
	for (var item of list) {
		if (query.equals(item)) {
			return true;
		}
	}
	return false;
}

================================================
FILE: src/geometry/Matrix4.ts
================================================
type NumberArray16 = [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];

class Matrix4 {
	elements: NumberArray16;

	constructor(elements: NumberArray16) {
		this.elements = elements;
	}

	get(i: number, j: number): number {
		return this.elements[4 * i + j];
	}

	public times(other: Matrix4): Matrix4 {
		let result: number[] = [];

		for (var i = 0; i < 4; i++) {
			for (var j = 0; j < 4; j++) {
				let element = 0;
				for (var k = 0; k < 4; k++) {
					element += this.get(i, k) * other.get(k, j);
				}
				result.push(element);
			}
		}

		return new Matrix4(result as NumberArray16);
	}

	public transpose() {
		return new Matrix4([
			this.elements[0], this.elements[4], this.elements[8], this.elements[12],
			this.elements[1], this.elements[5], this.elements[9], this.elements[13],
			this.elements[2], this.elements[6], this.elements[10], this.elements[14],
			this.elements[3], this.elements[7], this.elements[10], this.elements[15]
		]);
	}

	public invert(): Matrix4 {
		// based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
		// via https://github.com/mrdoob/three.js/blob/dev/src/math/Matrix4.js
		var el: NumberArray16 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

		n11 = this.elements[ 0 ], n21 = this.elements[ 1 ], n31 = this.elements[ 2 ], n41 = this.elements[ 3 ],
		n12 = this.elements[ 4 ], n22 = this.elements[ 5 ], n32 = this.elements[ 6 ], n42 = this.elements[ 7 ],
		n13 = this.elements[ 8 ], n23 = this.elements[ 9 ], n33 = this.elements[ 10 ], n43 = this.elements[ 11 ],
		n14 = this.elements[ 12 ], n24 = this.elements[ 13 ], n34 = this.elements[ 14 ], n44 = this.elements[ 15 ],

		t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,
		t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,
		t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,
		t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;

		var det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;

		if (det == 0) {
			throw new Error("Warning: Trying to invert matrix with determinant zero.");
		}

		var detInv = 1 / det;

		el[ 0 ] = t11 * detInv;
		el[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv;
		el[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv;
		el[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv;

		el[ 4 ] = t12 * detInv;
		el[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv;
		el[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv;
		el[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv;

		el[ 8 ] = t13 * detInv;
		el[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv;
		el[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv;
		el[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv;

		el[ 12 ] = t14 * detInv;
		el[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv;
		el[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv;
		el[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv;

		return new Matrix4(el);
	}

	public transformPoint(point: Vector3): Vector3 {
		return new Vector3(
			point.x * this.elements[0] + point.y * this.elements[4] + point.z * this.elements[8] + this.elements[12],
			point.x * this.elements[1] + point.y * this.elements[5] + point.z * this.elements[9] + this.elements[13],
			point.x * this.elements[2] + point.y * this.elements[6] + point.z * this.elements[10] + this.elements[14]);
	}

	public transformDirection(point: Vector3): Vector3 {
		return new Vector3(
			point.x * this.elements[0] + point.y * this.elements[4] + point.z * this.elements[8],
			point.x * this.elements[1] + point.y * this.elements[5] + point.z * this.elements[9],
			point.x * this.elements[2] + point.y * this.elements[6] + point.z * this.elements[10]);
	}

	public static getProjection(near = 0.1, far = 1000, fov = 25): Matrix4 {
		let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
        return new Matrix4([
            1 / (Math.tan(fov * DEG_TO_RAD / 2) * aspectRatio), 0, 0, 0,
            0, 1 / Math.tan(fov * DEG_TO_RAD / 2), 0, 0,
            0, 0, -(far + near)/(far - near), -1,
            0, 0, -0.2, 0
        ]);
	}

	public static getOrthographicProjection(far = 1000, size = 5): Matrix4 {
		let aspectRatio = gl.canvas.width / gl.canvas.height;
        return new Matrix4([
            2 / size / aspectRatio, 0, 0, 0,
            0, 2 / size, 0, 0,
            0, 0, -1 / far, 0,
            0, 0, 0, 1
        ]);
	}

	public static getIdentity(): Matrix4 {
		return new Matrix4([
			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 0, 1, 0,
			0, 0, 0, 1
		]);
	}
	
	public static getTranslation(vector: Vector3): Matrix4 {
		return new Matrix4([
			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 0, 1, 0,
			vector.x, vector.y, vector.z, 1
		]);
	}

	public static getRotation(euler: Vector3): Matrix4 {
		let phi = euler.x * DEG_TO_RAD;
		let theta = euler.y * DEG_TO_RAD;
		let psi = euler.z * DEG_TO_RAD;
		let sin = Math.sin;
		let cos = Math.cos;
		return new Matrix4([
			cos(theta) * cos(phi), -cos(psi) * sin(phi) + sin(psi) * sin(theta) * cos(phi), sin(psi) * sin(phi) + cos(psi) * sin(theta) * cos(phi), 0,
			cos(theta) * sin(phi), cos(psi)*cos(phi) + sin(psi) * sin(theta) * sin(phi), -sin(psi) * cos(phi) + cos(psi) * sin(theta) * sin(phi), 0,
			-sin(theta), sin(psi) * cos(theta), cos(psi) * cos(theta), 0,
			0, 0, 0, 1
		]);
	}
}

================================================
FILE: src/geometry/Mesh.ts
================================================
class Mesh {
    public readonly triangles: Triangle[];

    private vertexBuffer: WebGLBuffer = null;
    private normalBuffer: WebGLBuffer = null;

    constructor(triangles: Triangle[]) {
        this.triangles = triangles;
    }

    public createVertexBuffer(): WebGLBuffer {
        if (this.vertexBuffer != null) {
            return this.vertexBuffer;
        }

        let vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        var positions: number[] = [];

        for (let triangle of this.triangles) {
            this.pushVector(positions, triangle.v1);
            this.pushVector(positions, triangle.v2);
            this.pushVector(positions, triangle.v3);
        }

        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

        this.vertexBuffer = vertexBuffer;
        return vertexBuffer;
    }

    public createNormalBuffer(): WebGLBuffer {
        if (this.normalBuffer != null) {
            return this.normalBuffer;
        }

        let normalBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
        var normals: number[] = [];

        for (let triangle of this.triangles) {
            if (triangle instanceof TriangleWithNormals) {
                this.pushVector(normals, triangle.n1);
                this.pushVector(normals, triangle.n2);
                this.pushVector(normals, triangle.n3);
            } else {
                for (var i = 0; i < 3; i++) {
                    this.pushVector(normals, triangle.normal());
                }
            }
        }

        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
        this.normalBuffer = normalBuffer;
        return normalBuffer;
    }

    public createWireframeVertexBuffer(): WebGLBuffer {        
        let vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        var positions: number[] = [];

        for (let triangle of this.triangles) {
            this.pushVector(positions, triangle.v1);
            this.pushVector(positions, triangle.v2);
            this.pushVector(positions, triangle.v2);
            this.pushVector(positions, triangle.v3);
            this.pushVector(positions, triangle.v3);
            this.pushVector(positions, triangle.v1);
        }

        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

        return vertexBuffer;
    }

    private pushVector(array: number[], vector: Vector3) {
        array.push(vector.x);
        array.push(vector.y);
        array.push(vector.z);
    }

    public getVertexCount(): number {
        return this.triangles.length * 3;
    }
}

================================================
FILE: src/geometry/Quaternion.ts
================================================
class Quaternion {
	x: number;
	y: number;
	z: number;
	w: number;

	constructor(x: number, y: number, z: number, w: number) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;
	}

	times(other: Quaternion): Quaternion {
		return new Quaternion(this.x * other.x - this.y * other.y - this.z * other.z - this.w * other.w,
			this.x * other.y + other.x * this.y + this.z * other.w - other.z * this.w,
			this.x * other.z + other.x * this.z + this.w * other.y - other.w * this.y,
			this.x * other.w + other.x * this.w + this.y * other.z - other.y * this.z);
	}

	toMatrix(): Matrix4 {
		return new Matrix4([
			1 - 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,
			2 * 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,
			2 * 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,
			0, 0, 0, 1
		]);
	}

	static euler(angles: Vector3): Quaternion {
		return Quaternion.angleAxis(angles.z, new Vector3(0, 0, 1))
			.times(Quaternion.angleAxis(angles.y, new Vector3(0, 1, 0)))
			.times(Quaternion.angleAxis(angles.x, new Vector3(1, 0, 0)));
	}

	static angleAxis(angle: number, axis: Vector3): Quaternion {
		let theta_half = angle * DEG_TO_RAD * 0.5;
		return new Quaternion(Math.cos(theta_half), axis.x * Math.sin(theta_half), axis.y * Math.sin(theta_half), axis.z * Math.sin(theta_half));
	}

	static identity(): Quaternion {
		return new Quaternion(1, 0, 0, 0);
	}
}

================================================
FILE: src/geometry/Ray.ts
================================================
class Ray {
	point: Vector3;
	direction: Vector3;

	constructor(point: Vector3, direction: Vector3) {
		this.point = point;
		this.direction = direction;
	}

	get(t: number): Vector3 {
		return this.point.plus(this.direction.times(t));
	}

	getDistanceToRay(other: Ray): number {
		var normal = this.direction.cross(other.direction).normalized();

		var d1 = normal.dot(this.point);
		var d2 = normal.dot(other.point);

		return Math.abs(d1 - d2);
	}

	getClosestToPoint(point: Vector3): number {
		return this.direction.dot(this.point.minus(point));
	}

	getClosestToRay(other: Ray): number {
		var connection = this.direction.cross(other.direction).normalized();
		var planeNormal = connection.cross(other.direction).normalized();

		var planeToOrigin = other.point.dot(planeNormal);
		var result = (-this.point.dot(planeNormal) + planeToOrigin) / this.direction.dot(planeNormal);
		return result;
	}
}

================================================
FILE: src/geometry/Triangle.ts
================================================
class Triangle {
    public readonly v1: Vector3;
    public readonly v2: Vector3;
    public readonly v3: Vector3;

    constructor(v1: Vector3, v2: Vector3, v3: Vector3, flipped = false) {
        if (flipped) {
            this.v1 = v2;
            this.v2 = v1;
            this.v3 = v3;
        } else {
            this.v1 = v1;
            this.v2 = v2;
            this.v3 = v3;
        }
    }

    public normal(): Vector3 {
        return this.v3.minus(this.v1).cross(this.v2.minus(this.v1)).normalized();
    }

    public getOnEdge1(progress: number): Vector3 {
        return Vector3.interpolate(this.v1, this.v2, progress);
    }

    public getOnEdge2(progress: number): Vector3 {
        return Vector3.interpolate(this.v2, this.v3, progress);
    }
    
    public getOnEdge3(progress: number): Vector3 {
        return Vector3.interpolate(this.v3, this.v1, progress);
    }
}

================================================
FILE: src/geometry/TriangleWithNormals.ts
================================================
class TriangleWithNormals extends Triangle {
	n1: Vector3;
	n2: Vector3;
	n3: Vector3;

	constructor(v1: Vector3, v2: Vector3, v3: Vector3, n1: Vector3, n2: Vector3, n3: Vector3) {
		super(v1, v2, v3);
		this.n1 = n1;
		this.n2 = n2;
		this.n3 = n3;
	}
}

================================================
FILE: src/geometry/Vector3.ts
================================================
class Vector3 {
	public readonly x: number;
	public readonly y: number;
	public readonly z: number;

	constructor(x: number, y: number, z: number) {
		this.x = x;
		this.y = y;
		this.z = z;
	}

	public times(factor: number): Vector3 {
		return new Vector3(this.x * factor, this.y * factor, this.z * factor);
	}

	public plus(other: Vector3): Vector3 {
		return new Vector3(this.x + other.x, this.y + other.y, this.z + other.z);
	}

	public minus(other: Vector3): Vector3 {
		return new Vector3(this.x - other.x, this.y - other.y, this.z - other.z);
	}

	public dot(other: Vector3): number {
		return this.x * other.x + this.y * other.y + this.z * other.z;
	}

	public cross(other: Vector3): Vector3 {
		return 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);
	}

	public elementwiseMultiply(other: Vector3) {
		return new Vector3(this.x * other.x, this.y * other.y, this.z * other.z);
	}

	public magnitude(): number {
		return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
	}

	public normalized(): Vector3 {
		return this.times(1 / this.magnitude());
	}

	public toString(): string {
		return "(" + this.x + ", " + this.y + ", " + this.z + ")";
	}

	public copy(): Vector3 {
		return new Vector3(this.x, this.y, this.z);
	}

	public equals(other: Vector3): boolean {
		return this.x == other.x && this.y == other.y && this.z == other.z;
	}

	public floor(): Vector3 {
		return new Vector3(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));
	}

	public toNumber(): number {
		let layer3D = this.x + this.y + this.z;
		let layer2D = layer3D - this.y;
	
		return tetrahedralNumber(layer3D) + triangularNumber(layer2D) + this.x;
	}

	public static fromNumber(value: number): Vector3 {
		let layer3D = inverseTetrahedralNumber(value);
		value -= tetrahedralNumber(layer3D);
		let layer2D = inverseTriangularNumber(value);
	
		let x = value - triangularNumber(layer2D);
		let y = layer3D - layer2D;
		let z = layer3D - x - y;
	
		return new Vector3(x, y, z);
	}

	public static zero(): Vector3 {
		return new Vector3(0, 0, 0);
	}

	public static one(): Vector3 {
		return new Vector3(1, 1, 1);
	}

	public static lerp(a: Vector3, b: Vector3, progress: number): Vector3 {
		return a.plus(b.minus(a).times(progress));
	}

	public static isCollinear(a: Vector3, b: Vector3) {
		var factor: number | null = null;
		if (a.x == 0 || b.x == 0) {
			if (Math.abs(a.x + b.x) > 0.001) {
				return false;
			}
		} else {
			factor = a.x / b.x;
		}

		if (a.y == 0 || b.y == 0) {
			if (Math.abs(a.y + b.y) > 0.001) {
				return false;
			}
		} else {
			if (factor == null) {
				factor = a.y / b.y;
			} else if (Math.abs(factor - a.y / b.y) > 0.001) {
				return false;
			}
		}

		if (a.z == 0 || b.z == 0) {
			if (Math.abs(a.z + b.z) > 0.001) {
				return false;
			}
		} else if (factor != null && Math.abs(factor - a.z / b.z) > 0.001) {
			return false;
		}

		return true;
	}

	public static interpolate(a: Vector3, b: Vector3, t: number) {
		return a.times(1.0 - t).plus(b.times(t));
	}
}

const RIGHT_FACE_VERTICES = [
	new Vector3(1, 1, 0),
	new Vector3(1, 1, 1),
	new Vector3(1, 0, 1),
	new Vector3(1, 0, 0)
];

const LEFT_FACE_VERTICES = [
	new Vector3(0, 0, 0),
	new Vector3(0, 0, 1),
	new Vector3(0, 1, 1),
	new Vector3(0, 1, 0)
];

const UP_FACE_VERTICES = [
	new Vector3(0, 1, 0),
	new Vector3(0, 1, 1),
	new Vector3(1, 1, 1),
	new Vector3(1, 1, 0)
];

const DOWN_FACE_VERTICES = [
	new Vector3(1, 0, 0),
	new Vector3(1, 0, 1),
	new Vector3(0, 0, 1),
	new Vector3(0, 0, 0)
];

const FORWARD_FACE_VERTICES = [
	new Vector3(1, 0, 1),
	new Vector3(1, 1, 1),
	new Vector3(0, 1, 1),
	new Vector3(0, 0, 1)
];

const BACK_FACE_VERTICES = [
	new Vector3(0, 0, 0),
	new Vector3(0, 1, 0),
	new Vector3(1, 1, 0),
	new Vector3(1, 0, 0)
];

const FACE_DIRECTIONS = [
	new Vector3(1, 0, 0),
	new Vector3(-1, 0, 0),
	new Vector3(0, 1, 0),
	new Vector3(0, -1, 0),
	new Vector3(0, 0, 1),
	new Vector3(0, 0, -1)
];

================================================
FILE: src/geometry/VectorDictionary.ts
================================================
class VectorDictionary<T> {
	private data: {
		[id: number]: {
			[id: number]: {
				[id: number]: T;
			};
		};
	} = {};

	containsKey(key: Vector3): boolean {
		return key.x in this.data && key.y in this.data[key.x] && key.z in this.data[key.x][key.y];
	}

	get(key: Vector3): T {
		if (!this.containsKey(key)) {
			throw new Error("Dictionary does not contain key: " + key.toString());
		}
		return this.data[key.x][key.y][key.z];
	}

	getOrNull(key: Vector3): T {
		if (!this.containsKey(key)) {
			return null;
		}
		return this.data[key.x][key.y][key.z];
	}

	set(key: Vector3, value: T) {
		if (!(key.x in this.data)) {
			this.data[key.x] = {};
		}
		if (!(key.y in this.data[key.x])) {
			this.data[key.x][key.y] = {};
		}
		this.data[key.x][key.y][key.z] = value;
	}

	remove(key: Vector3) {
		if (key.x in this.data && key.y in this.data[key.x] && key.z in this.data[key.x][key.y]) {
			delete this.data[key.x][key.y][key.z];
		}
	}

	clear() {
		this.data = {};
	}

	*keys(): IterableIterator<Vector3> {
		for (let x in this.data) {
			for (let y in this.data[x]) {
				for (let z in this.data[x][y]) {
					yield new Vector3(parseInt(x), parseInt(y), parseInt(z));
				}
			}
		}
	}

	*values(): IterableIterator<T> {
		for (let x in this.data) {
			for (let y in this.data[x]) {
				for (let z in this.data[x][y]) {
					yield this.data[x][y][z];
				}
			}
		}
	}

	any(): boolean {
		for (let x in this.data) {
			for (let y in this.data[x]) {
				for (let z in this.data[x][y]) {
					return true;
				}
			}
		}
		return false;
	}
}

================================================
FILE: src/measurements.ts
================================================
class Measurements {
	technicUnit = 8;

	edgeMargin = 0.2 / this.technicUnit;
	interiorRadius = 3.2 / this.technicUnit;
	pinHoleRadius = 2.475 / this.technicUnit;
	pinHoleOffset = 0.89 / this.technicUnit;
	axleHoleSize = 1.01 / this.technicUnit;
	pinRadius = 2.315 / this.technicUnit;
	ballBaseRadius = 1.6 / this.technicUnit;
	ballRadius = 3.0 / this.technicUnit;
	pinLipRadius = 0.17 / this.technicUnit;
	axleSizeInner = 0.86 / this.technicUnit;
	axleSizeOuter = 2.15 / this.technicUnit;
	attachmentAdapterSize = 0.4 / this.technicUnit;
	attachmentAdapterRadius = 3 / this.technicUnit;
	interiorEndMargin = 0.2 / this.technicUnit;

	lipSubdivisions = 6;

	subdivisionsPerQuarter = 8;

	public enforceConstraints() {
		this.lipSubdivisions = Math.max(2, Math.ceil(this.lipSubdivisions));
		this.subdivisionsPerQuarter = Math.max(2, Math.ceil(this.subdivisionsPerQuarter / 2) * 2);
		this.edgeMargin = Math.min(0.49, this.edgeMargin);
		this.interiorRadius = Math.min(0.5 - this.edgeMargin, this.interiorRadius);
		this.interiorEndMargin = Math.min(0.49, this.interiorEndMargin);
		this.pinHoleRadius = Math.min(this.interiorRadius, this.pinHoleRadius);
		this.pinHoleOffset = Math.min(0.5 - this.edgeMargin, this.pinHoleOffset);
		this.axleHoleSize = Math.min(this.interiorRadius / 2, this.axleHoleSize);
		this.pinRadius = Math.min(0.5 - this.edgeMargin, this.pinRadius);
		this.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);
		this.axleSizeInner = Math.min(this.axleSizeOuter, this.axleSizeInner);
		this.attachmentAdapterSize = Math.min((0.5 - this.edgeMargin) / 2, this.attachmentAdapterSize);
		this.ballBaseRadius = Math.min(this.ballBaseRadius, this.interiorRadius);
		this.ballRadius = Math.max(Math.min(this.ballRadius, 0.5 - this.attachmentAdapterSize), this.ballBaseRadius);
	}
}

const DEFAULT_MEASUREMENTS = new Measurements();

================================================
FILE: src/model/Block.ts
================================================
class Block {
	public readonly orientation: Orientation;
	public readonly type: BlockType;
	public rounded: boolean;

	public readonly right: Vector3;
	public readonly up: Vector3;
	public readonly forward: Vector3;
	public readonly isAttachment: boolean;

	constructor(orientation: Orientation, type: BlockType, rounded: boolean) {
		this.orientation = orientation;
		this.type = type;
		this.rounded = rounded;

		this.right = RIGHT[this.orientation];
		this.up = UP[this.orientation];
		this.forward = FORWARD[this.orientation];
		this.isAttachment = this.type == BlockType.Pin || this.type == BlockType.Axle || this.type == BlockType.BallJoint;
	}
}


================================================
FILE: src/model/Part.ts
================================================
///<reference path="../geometry/Vector3.ts" />

let CUBE = [
	new Vector3(0, 0, 0),
	new Vector3(0, 0, 1),
	new Vector3(0, 1, 0),
	new Vector3(0, 1, 1),
	new Vector3(1, 0, 0),
	new Vector3(1, 0, 1),
	new Vector3(1, 1, 0),
	new Vector3(1, 1, 1)
];

class Part {
	public blocks: VectorDictionary<Block> = new VectorDictionary<Block>();

	public createSmallBlocks(): VectorDictionary<SmallBlock> {
		var result = new VectorDictionary<SmallBlock>();

		for (let position of this.blocks.keys()) {
			let block = this.blocks.get(position);
			for (let local of CUBE) {
				if (block.forward.dot(local) == 1) {
					continue;
				}
				result.set(position.plus(local), SmallBlock.createFromLocalCoordinates(block.right.dot(local), block.up.dot(local), position.plus(local), block));
			}
		}

		return result;
	}

	public isSmallBlockFree(position: Vector3): boolean {
		for (let local of CUBE) {
			if (!this.blocks.containsKey(position.minus(local))) {
				continue;
			}
			var block = this.blocks.get(position.minus(local));
			if (block.forward.dot(local) == 1) {
				return false;
			}
		}
		return true;
	}

	public clearSingle(position: Vector3) {
		for (let local of CUBE) {
			if (!this.blocks.containsKey(position.minus(local))) {
				continue;
			}
			var block = this.blocks.get(position.minus(local));
			if (block.forward.dot(local) != 1) {
				this.blocks.remove(position.minus(local));
			}
		}
	}

	public clearBlock(position: Vector3, orientation: Orientation) {
		for (let local of CUBE) {
			if (FORWARD[orientation].dot(local) != 1) {
				this.clearSingle(position.plus(local));
			}
		}
	}

	public isBlockPlaceable(position: Vector3, orientation: Orientation, doubleSize: boolean): boolean {
		for (let local of CUBE) {
			if (!doubleSize && FORWARD[orientation].dot(local) == 1) {
				continue;
			}
			if (!this.isSmallBlockFree(position.plus(local))) {
				return false;
			}
		}
		return true;
	}

	public placeBlockForced(position: Vector3, block: Block) {
		this.clearBlock(position, block.orientation);
		this.blocks.set(position, block);
	}

	public toString(): string {
		var result = "";

		if (!this.blocks.any()) {
			return result;
		}

		var origin = new Vector3(min(this.blocks.keys(), p => p.x), min(this.blocks.keys(), p => p.y), min(this.blocks.keys(), p => p.z));

		for (let position of this.blocks.keys()) {
			result += position.minus(origin).toNumber().toString(16).toLowerCase();

			let block = this.blocks.get(position);
			let orientationAndRounded = block.orientation == Orientation.X ? "x" : (block.orientation == Orientation.Y ? "y" : "z");
			if (!block.rounded) {
				orientationAndRounded = orientationAndRounded.toUpperCase();
			}
			result += orientationAndRounded;
			result += block.type.toString();
		}
		return result;
	}

	public static fromString(s: string): Part {
		let XYZ = "xyz";

		let part = new Part();

		var p = 0;
		while (p < s.length) {
			var chars = 1;
			while (XYZ.indexOf(s[p + chars].toLowerCase()) == -1) {
				chars++;
			}
			
			let position = Vector3.fromNumber(parseInt(s.substr(p, chars), 16));
			p += chars;
			let orientationString = s[p].toString().toLowerCase();
			let orientation = orientationString == "x" ? Orientation.X : (orientationString == "y" ? Orientation.Y : Orientation.Z);
			let rounded = s[p].toLowerCase() == s[p];
			let type = parseInt(s[p + 1]) as BlockType;

			part.blocks.set(position, new Block(orientation, type, rounded));
			p += 2;
		}
		return part;
	}

	private getBoundingBox(): [Vector3, Vector3] {
		let defaultPosition = this.blocks.keys().next().value;
		var minX = defaultPosition.x;
		var minY = defaultPosition.y;
		var minZ = defaultPosition.z;
		var maxX = defaultPosition.x;
		var maxY = defaultPosition.y;
		var maxZ = defaultPosition.z;

		for (var position of this.blocks.keys()) {
			var forward = this.blocks.get(position).forward;
			if (position.x < minX) {
				minX = position.x;
			}
			if (position.y < minY) {
				minY = position.y;
			}
			if (position.z < minZ) {
				minZ = position.z;
			}
			if (position.x + (1.0 - forward.x) > maxX) {
				maxX = position.x + (1.0 - forward.x);
			}
			if (position.y + (1.0 - forward.y) > maxY) {
				maxY = position.y + (1.0 - forward.y);
			}
			if (position.z + (1.0 - forward.z) > maxZ) {
				maxZ = position.z + (1.0 - forward.z);
			}
		}
		return [new Vector3(minX, minY, minZ), new Vector3(maxX, maxY, maxZ)];
	}

	public getCenter(): Vector3 {
		if (!this.blocks.any()) {
			return Vector3.zero();
		}

		var boundingBox = this.getBoundingBox();
		var min = boundingBox[0];
		var max = boundingBox[1];
		
		return min.plus(max).plus(Vector3.one()).times(0.5);
	}

	public getSize() {
		var boundingBox = this.getBoundingBox();
		var min = boundingBox[0];
		var max = boundingBox[1];
		return Math.max(max.x - min.x, Math.max(max.y - min.y, max.z - min.z)) + 1;
	}
}

================================================
FILE: src/model/PerpendicularRoundedAdaper.ts
================================================
class PerpendicularRoundedAdapter {
	public isVertical: boolean;
	public neighbor: SmallBlock;
	public directionToNeighbor: Vector3;
	public facesForward: boolean;
	public sourceBlock: SmallBlock;
}

================================================
FILE: src/model/SmallBlock.ts
================================================
class SmallBlock extends Block {
	public readonly quadrant: Quadrant;
	public readonly position: Vector3;
	public hasInterior: boolean;

	public perpendicularRoundedAdapter: PerpendicularRoundedAdapter = null;

	public readonly localX: number;
	public readonly localY: number;
	public readonly directionX: number;
	public readonly directionY: number;
	public readonly horizontal: Vector3;
	public readonly vertical: Vector3;

	constructor(quadrant: Quadrant, positon: Vector3, source: Block) {
		super(source.orientation, source.type, source.rounded);
		this.quadrant = quadrant;
		this.position = positon;

		this.hasInterior = source.type != BlockType.Solid;

		this.localX = localX(this.quadrant);
		this.localY = localY(this.quadrant);
		this.directionX = this.localX == 1 ? 1 : -1;
		this.directionY = this.localY == 1 ? 1 : -1;
		this.horizontal = this.localX == 1 ? RIGHT[this.orientation] : LEFT[this.orientation];
		this.vertical = this.localY == 1 ? UP[this.orientation] : DOWN[this.orientation];
	}

	public static createFromLocalCoordinates(localX: number, localY: number, position: Vector3, source: Block) {
		return new SmallBlock(SmallBlock.getQuadrantFromLocal(localX, localY), position, source);
	}
	
	public odd(): boolean {
		return this.quadrant == Quadrant.BottomRight || this.quadrant == Quadrant.TopLeft;
	}

	private static getQuadrantFromLocal(x: number, y: number): Quadrant {
		if (x == 0) {
			if (y == 0) {
				return Quadrant.BottomLeft;
			} else {
				return Quadrant.TopLeft;
			}
		} else {
			if (y == 0) {
				return Quadrant.BottomRight;
			} else {
				return Quadrant.TopRight;
			}
		}
	}

	public getOnCircle(angle: number, radius = 1): Vector3 {
		return this.right.times(Math.sin(angle + getAngle(this.quadrant)) * radius).plus(
			this.up.times(Math.cos(angle + getAngle(this.quadrant)) * radius));
	}
}
 

================================================
FILE: src/model/TinyBlock.ts
================================================
class TinyBlock extends SmallBlock {
	public exteriorMergedBlocks = 1;
	public isExteriorMerged = false;
	
	public interiorMergedBlocks = 1;
	public isInteriorMerged = false;

	private readonly visibleFaces: [boolean, boolean, boolean, boolean, boolean, boolean] = null;

	public readonly angle: number;
	public readonly isCenter: boolean;
	public readonly smallBlockPosition: Vector3;

	constructor(position: Vector3, source: SmallBlock) {
		super(source.quadrant, position, source);
		this.visibleFaces = [true, true, true, true, true, true];
		this.perpendicularRoundedAdapter = source.perpendicularRoundedAdapter;

		this.angle = getAngle(this.quadrant);
		this.smallBlockPosition = new Vector3(
			Math.floor((position.x + 1) / 3),
			Math.floor((position.y + 1) / 3),
			Math.floor((position.z + 1) / 3));
		var localPosition = position.minus(this.smallBlockPosition.times(3));
		this.isCenter = localPosition.dot(this.up) == 0 && localPosition.dot(this.right) == 0;
	}

	public getCylinderOrigin(meshGenerator: MeshGenerator): Vector3 {
		return this.forward.times(meshGenerator.tinyIndexToWorld(this.forward.dot(this.position)))
			.plus(this.right.times((this.smallBlockPosition.dot(this.right) + (1 - this.localX)) * 0.5))
			.plus(this.up.times((this.smallBlockPosition.dot(this.up) + (1 - this.localY)) * 0.5));
	}

	public getExteriorDepth(meshGenerator: MeshGenerator): number {
		return meshGenerator.tinyIndexToWorld(this.forward.dot(this.position) + this.exteriorMergedBlocks) - meshGenerator.tinyIndexToWorld(this.forward.dot(this.position));
	}
	
	public getInteriorDepth(meshGenerator: MeshGenerator): number {
		return meshGenerator.tinyIndexToWorld(this.forward.dot(this.position) + this.interiorMergedBlocks) - meshGenerator.tinyIndexToWorld(this.forward.dot(this.position));
	}

	public isFaceVisible(direction: Vector3): boolean {
		if (direction.x > 0 && direction.y == 0 && direction.z == 0) {
			return this.visibleFaces[0];
		} else if (direction.x < 0 && direction.y == 0 && direction.z == 0) {
			return this.visibleFaces[1];
		} else if (direction.x == 0 && direction.y > 0 && direction.z == 0) {
			return this.visibleFaces[2];
		} else if (direction.x == 0 && direction.y < 0 && direction.z == 0) {
			return this.visibleFaces[3];
		} else if (direction.x == 0 && direction.y == 0 && direction.z > 0) {
			return this.visibleFaces[4];
		} else if (direction.x == 0 && direction.y == 0 && direction.z < 0) {
			return this.visibleFaces[5];
		} else {
			throw new Error("Invalid direction vector.");
		}
	}

	public hideFace(direction: Vector3) {
		if (direction.x > 0 && direction.y == 0 && direction.z == 0) {
			this.visibleFaces[0] = false;
		} else if (direction.x < 0 && direction.y == 0 && direction.z == 0) {
			this.visibleFaces[1] = false;
		} else if (direction.x == 0 && direction.y > 0 && direction.z == 0) {
			this.visibleFaces[2] = false;
		} else if (direction.x == 0 && direction.y < 0 && direction.z == 0) {
			this.visibleFaces[3] = false;
		} else if (direction.x == 0 && direction.y == 0 && direction.z > 0) {
			this.visibleFaces[4] = false;
		} else if (direction.x == 0 && direction.y == 0 && direction.z < 0) {
			this.visibleFaces[5] = false;
		} else {
			throw new Error("Invalid direction vector.");
		}
	}
}


================================================
FILE: src/model/enums/BlockType.ts
================================================
enum BlockType {
	Solid,
	PinHole,
	AxleHole,
	Pin,
	Axle,
	BallJoint,
	BallSocket
}

const BLOCK_TYPE = {
	"solid": BlockType.Solid,
	"pinhole": BlockType.PinHole,
	"axlehole": BlockType.AxleHole,
	"pin": BlockType.Pin,
	"axle": BlockType.Axle,
	"balljoint": BlockType.BallJoint,
	"ballsocket": BlockType.BallSocket
}

================================================
FILE: src/model/enums/Orientation.ts
================================================
enum Orientation {
	X = 0,
	Y = 1,
	Z = 2
}

const ORIENTATION = {
	"x": Orientation.X,
	"y": Orientation.Y,
	"z": Orientation.Z
};

const FORWARD = {
	0: new Vector3(1, 0, 0),
	1: new Vector3(0, 1, 0),
	2: new Vector3(0, 0, 1)
};

const RIGHT = {
	0: new Vector3(0, 1, 0),
	1: new Vector3(0, 0, 1),
	2: new Vector3(1, 0, 0)
};

const UP = {
	0: new Vector3(0, 0, 1),
	1: new Vector3(1, 0, 0),
	2: new Vector3(0, 1, 0)
}

const LEFT = {
	0: new Vector3(0, -1, 0),
	1: new Vector3(0, 0, -1),
	2: new Vector3(-1, 0, 0)
};

const DOWN = {
	0: new Vector3(0, 0, -1),
	1: new Vector3(-1, 0, 0),
	2: new Vector3(0, -1, 0)
}

================================================
FILE: src/model/enums/Quadrant.ts
================================================
enum Quadrant {
	TopLeft,
	TopRight,
	BottomLeft,
	BottomRight
}

function localX(quadrant: Quadrant): number {
	return (quadrant == Quadrant.TopRight || quadrant == Quadrant.BottomRight) ? 1 : 0;
}

function localY(quadrant: Quadrant): number {
	return (quadrant == Quadrant.TopRight || quadrant == Quadrant.TopLeft) ? 1 : 0;
}

function getAngle(quadrant: Quadrant): number {
	switch (quadrant) {
		case Quadrant.TopRight:
			return 0;
		case Quadrant.BottomRight:
			return 90 * DEG_TO_RAD;
		case Quadrant.BottomLeft:
			return 180 * DEG_TO_RAD;
		case Quadrant.TopLeft:
			return 270 * DEG_TO_RAD;
	}
	throw new Error("Unknown quadrant: " + quadrant);
}

================================================
FILE: src/rendering/Camera.ts
================================================
class Camera {
    public renderers: Renderer[] = [];

    public transform: Matrix4 = Matrix4.getIdentity();

    public size = 5;

    public frameBuffer: WebGLFramebuffer;
    public normalTexture: WebGLTexture;
    public depthTexture: WebGLTexture;

    public clearColor: Vector3 = new Vector3(0.95, 0.95, 0.95);

    public supersample: number = 1;

    constructor(canvas: HTMLCanvasElement, supersample = 1) {
        gl = canvas.getContext("webgl") as WebGLRenderingContext;

		if (gl == null) {
			throw new Error("WebGL is not supported.");
        }
        gl.getExtension('WEBGL_depth_texture');

        this.supersample = supersample;        
        canvas.width = Math.round(canvas.clientWidth * window.devicePixelRatio) * this.supersample;
        canvas.height = Math.round(canvas.clientHeight * window.devicePixelRatio) * this.supersample;
        this.createBuffers();
    }

    private createBuffers() {
        this.normalTexture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.normalTexture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        
        this.depthTexture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.depthTexture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, gl.canvas.width, gl.canvas.height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        
        this.frameBuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.normalTexture, 0);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);

        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }

    public getProjectionMatrix(): Matrix4 {
       return Matrix4.getOrthographicProjection(30, this.size);
    }

    public render() {
        gl.clearColor(this.clearColor.x, this.clearColor.y, this.clearColor.z, 1.0);
        gl.colorMask(true, true, true, true);
        gl.clearDepth(1.0);
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
        gl.enable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);
        gl.enable(gl.CULL_FACE);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
		for (var renderer of this.renderers) {
			renderer.render(this);
        }

        gl.colorMask(false, false, false, true);
        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
    
    public onResize() {
        gl.canvas.width = Math.round((gl.canvas as HTMLCanvasElement).clientWidth * window.devicePixelRatio) * this.supersample;
        gl.canvas.height = Math.round((gl.canvas as HTMLCanvasElement).clientHeight * window.devicePixelRatio) * this.supersample;
        this.createBuffers();
        this.render();
    }

    public getScreenToWorldRay(event: MouseEvent): Ray {
        var rect = (gl.canvas as HTMLCanvasElement).getBoundingClientRect();
        var x = event.clientX - rect.left;
        var y = event.clientY - rect.top;

        x = x / (gl.canvas as HTMLCanvasElement).clientWidth * 2 - 1;
        y = y / (gl.canvas as HTMLCanvasElement).clientHeight * -2 + 1;

        let viewSpacePoint = new Vector3(x * this.size / 2 * gl.drawingBufferWidth / gl.drawingBufferHeight, y * this.size / 2, 0);
        let viewSpaceDirection = new Vector3(0, 0, -1);
        let inverseCameraTransform = this.transform.invert();

        return new Ray(inverseCameraTransform.transformPoint(viewSpacePoint), inverseCameraTransform.transformDirection(viewSpaceDirection));
    }
}

================================================
FILE: src/rendering/ContourPostEffect.ts
================================================
class ContourPostEffect implements Renderer {

	private shader: Shader;
	private vertices: WebGLBuffer;
	
	public enabled: boolean = true;
    
    constructor() {
        this.shader = new Shader(COUNTOUR_VERTEX, CONTOUR_FRAGMENT);

		this.shader.setAttribute("vertexPosition");
		this.shader.setUniform("normalTexture");
		this.shader.setUniform("depthTexture");
		this.shader.setUniform("resolution");
		
		this.vertices = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);
		var positions: number[] = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
		gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    }

    public render(camera: Camera) {
		if (!this.enabled) {
			return;
		}

        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);
        gl.vertexAttribPointer(this.shader.attributes["vertexPosition"], 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.shader.attributes["vertexPosition"]);
      
        gl.useProgram(this.shader.program);
		
		gl.depthFunc(gl.ALWAYS);
		gl.depthMask(false);

		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, camera.normalTexture);
		gl.uniform1i(this.shader.attributes["normalTexture"], 0);		
		gl.activeTexture(gl.TEXTURE1);
		gl.bindTexture(gl.TEXTURE_2D, camera.depthTexture);
		gl.uniform1i(this.shader.attributes["depthTexture"], 1);
		gl.uniform2f(this.shader.attributes["resolution"], gl.drawingBufferWidth, gl.drawingBufferHeight);
		
        gl.drawArrays(gl.TRIANGLES, 0, 6);
		gl.depthFunc(gl.LEQUAL);
		gl.depthMask(true);
    }
}

================================================
FILE: src/rendering/MeshRenderer.ts
================================================
class MeshRenderer implements Renderer {
    private shader: Shader;

    private vertices: WebGLBuffer;
    private normals: WebGLBuffer;

    private vertexCount: number;
    public transform: Matrix4;
    public color: Vector3 = new Vector3(1, 0, 0);
    public alpha: number = 1;

    public enabled: boolean = true;

    constructor() {
        this.shader = new Shader(VERTEX_SHADER, FRAGMENT_SHADER);

        this.shader.setAttribute("vertexPosition");
        this.shader.setAttribute("normal");
        this.shader.setUniform("projectionMatrix");
        this.shader.setUniform("modelViewMatrix");
        this.shader.setUniform("albedo");
        this.shader.setUniform("alpha");

        this.transform = Matrix4.getIdentity();
    }

    public setMesh(mesh: Mesh) {
        this.vertexCount = mesh.getVertexCount();
        this.vertices = mesh.createVertexBuffer();
        this.normals = mesh.createNormalBuffer();
    }

    public render(camera: Camera) {
        if (!this.enabled) {
            return;
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);
        gl.vertexAttribPointer(this.shader.attributes["vertexPosition"], 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.shader.attributes["vertexPosition"]);
        
        gl.bindBuffer(gl.ARRAY_BUFFER, this.normals);
        gl.vertexAttribPointer(this.shader.attributes["normal"], 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.shader.attributes["normal"]);
      
        gl.useProgram(this.shader.program);
      
        gl.uniformMatrix4fv(this.shader.attributes["projectionMatrix"], false, camera.getProjectionMatrix().elements);
        gl.uniformMatrix4fv(this.shader.attributes["modelViewMatrix"], false, this.transform.times(camera.transform).elements);
        gl.uniform3f(this.shader.attributes["albedo"], this.color.x, this.color.y, this.color.z);
        gl.uniform1f(this.shader.attributes["alpha"], this.alpha);
      
        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
    }
}

================================================
FILE: src/rendering/NormalDepthRenderer.ts
================================================
class NormalDepthRenderer implements Renderer {
    private shader: Shader;

    private vertices: WebGLBuffer;
    private normals: WebGLBuffer;

    public transform: Matrix4;

    private vertexCount: number;

    public enabled: boolean = true;

    constructor() {
        this.prepareShaders();
        this.transform = Matrix4.getIdentity();
    }

    private prepareShaders() {
        this.shader = new Shader(VERTEX_SHADER, NORMAL_FRAGMENT_SHADER);
        this.shader.setAttribute("vertexPosition");
        this.shader.setAttribute("normal");
        this.shader.setUniform("projectionMatrix");
        this.shader.setUniform("modelViewMatrix");
    }

    public setMesh(mesh: Mesh) {
        this.vertexCount = mesh.getVertexCount();
        this.vertices = mesh.createVertexBuffer();
        this.normals = mesh.createNormalBuffer();
    }

    public render(camera: Camera) {
        if (!this.enabled) {
            return;
        }

        gl.bindFramebuffer(gl.FRAMEBUFFER, camera.frameBuffer);
        gl.bindTexture(gl.TEXTURE_2D, null);

        gl.clearColor(0.5, 0.5, -1.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);
        gl.vertexAttribPointer(this.shader.attributes["vertexPosition"], 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.shader.attributes["vertexPosition"]);
        
        gl.bindBuffer(gl.ARRAY_BUFFER, this.normals);
        gl.vertexAttribPointer(this.shader.attributes["normal"], 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.shader.attributes["normal"]);
      
        gl.useProgram(this.shader.program);
      
        gl.uniformMatrix4fv(this.shader.attributes["projectionMatrix"], false, camera.getProjectionMatrix().elements);
        gl.uniformMatrix4fv(this.shader.attributes["modelViewMatrix"], false, this.transform.times(camera.transform).elements);
      
        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }
}

================================================
FILE: src/rendering/Renderer.ts
================================================
interface Renderer {
	render(camera: Camera);
}

================================================
FILE: src/rendering/Shader.ts
================================================
class Shader {
	public program: WebGLShader;
	public attributes: {[id: string]: number } = {};

	private loadShader(type: number, source: string): WebGLShader {
        let shader = gl.createShader(type);      
        gl.shaderSource(shader, source);      
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            var lines = source.split("\n");
            for (var index = 0; index < lines.length; index++) {
                console.log((index + 1) + ": " + lines[index]);
            }
            throw new Error('An error occurred compiling the shaders: ' +  gl.getShaderInfoLog(shader));
        }

        return shader;
    }

    constructor(vertexSource: string, fragmentSource: string) {
        const vertexShader = this.loadShader(gl.VERTEX_SHADER, vertexSource);
        const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fragmentSource);
      
        this.program = gl.createProgram();
        gl.attachShader(this.program, vertexShader);
        gl.attachShader(this.program, fragmentShader);
        gl.linkProgram(this.program);
      
        if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
            throw new Error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(this.program));
		}
	}

	public setAttribute( name: string) {
		this.attributes[name] = gl.getAttribLocation(this.program, name);
	}

	public setUniform(name: string) {		
		this.attributes[name] = gl.getUniformLocation(this.program, name) as number;
	}
}

================================================
FILE: src/rendering/WireframeBox.ts
================================================
class WireframeBox implements Renderer {
	private shader: Shader;
	private positions: WebGLBuffer;

	public transform: Matrix4;
	
	public visible: boolean = true;

	public color: Vector3 = new Vector3(0.0, 0.0, 1.0);
	public alpha: number = 0.8;	
	public colorOccluded: Vector3 = new Vector3(0.0, 0.0, 0.0);
	public alphaOccluded: number = 0.15;
	
	public scale: Vector3 = Vector3.one();

	constructor() {
		this.shader = new Shader(SIMPLE_VERTEX_SHADER, COLOR_FRAGMENT_SHADER);

		this.shader.setAttribute("vertexPosition");
        this.shader.setUniform("projectionMatrix");
        this.shader.setUniform("modelViewMatrix");
        this.shader.setUniform("color");
        this.shader.setUniform("scale");
		
		this.positions = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, this.positions);
		var positions: number[] = [
			-1, -1, -1,  -1, -1, +1,
			+1, -1, -1,  +1, -1, +1,
			-1, +1, -1,  -1, +1, +1,
			+1, +1, -1,  +1, +1, +1,

			-1, -1, -1,  -1, +1, -1,
			-1, -1, +1,  -1, +1, +1,
			+1, -1, -1,  +1, +1, -1,
			+1, -1, +1,  +1, +1, +1,

			-1, -1, -1,  +1, -1, -1,
			-1, +1, -1,  +1, +1, -1,
			-1, -1, +1,  +1, -1, +1,
			-1, +1, +1,  +1, +1, +1
		];
		gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
	}

	public render(camera: Camera) {
		if (!this.visible) {
			return;
		}

		gl.bindBuffer(gl.ARRAY_BUFFER, this.positions);
		gl.vertexAttribPointer(this.shader.attributes["vertexPosition"], 3, gl.FLOAT, false, 0, 0);
		gl.enableVertexAttribArray(this.shader.attributes["vertexPosition"]);
		
		gl.useProgram(this.shader.program);

		gl.uniformMatrix4fv(this.shader.attributes["projectionMatrix"], false, camera.getProjectionMatrix().elements);
		gl.uniformMatrix4fv(this.shader.attributes["modelViewMatrix"], false, this.transform.times(camera.transform).elements);
		gl.uniform3f(this.shader.attributes["scale"], this.scale.x, this.scale.y, this.scale.z);
		
		gl.depthFunc(gl.GREATER);
		gl.depthMask(false);
        gl.uniform4f(this.shader.attributes["color"], this.colorOccluded.x, this.colorOccluded.y, this.colorOccluded.z, this.alphaOccluded);
		gl.drawArrays(gl.LINES, 0, 24);
		
		gl.depthFunc(gl.LEQUAL);
		gl.depthMask(true);
        gl.uniform4f(this.shader.attributes["color"], this.color.x, this.color.y, this.color.z, this.alpha);		
		gl.drawArrays(gl.LINES, 0, 24);
	}
}

================================================
FILE: src/rendering/WireframeRenderer.ts
================================================
class WireframeRenderer implements Renderer {
	private shader: Shader;
	private vertices: WebGLBuffer;
	private vertexCount: number;

	public transform: Matrix4;
	
	public enabled: boolean = true;

	public color: Vector3 = new Vector3(0.0, 0.0, 0.0);
	public alpha: number = 0.5;

    constructor() {
		this.shader = new Shader(SIMPLE_VERTEX_SHADER, COLOR_FRAGMENT_SHADER);

		this.shader.setAttribute("vertexPosition");
        this.shader.setUniform("projectionMatrix");
        this.shader.setUniform("modelViewMatrix");
        this.shader.setUniform("color");
        this.shader.setUniform("scale");		

        this.transform = Matrix4.getIdentity();
    }

    public setMesh(mesh: Mesh) {
        this.vertexCount = mesh.getVertexCount() * 2;
        this.vertices = mesh.createWireframeVertexBuffer();
    }

    public render(camera: Camera) {
		if (!this.enabled) {
			return;
		}
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertices);
		gl.vertexAttribPointer(this.shader.attributes["vertexPosition"], 3, gl.FLOAT, false, 0, 0);
		gl.enableVertexAttribArray(this.shader.attributes["vertexPosition"]);
		
		gl.useProgram(this.shader.program);

		gl.uniformMatrix4fv(this.shader.attributes["projectionMatrix"], false, camera.getProjectionMatrix().elements);
		gl.uniformMatrix4fv(this.shader.attributes["modelViewMatrix"], false, this.transform.times(camera.transform).elements);
		gl.uniform3f(this.shader.attributes["scale"], 1, 1, 1);
		gl.uniform4f(this.shader.attributes["color"], this.color.x, this.color.y, this.color.z, this.alpha);
		
      
        gl.drawArrays(gl.LINES, 0, this.vertexCount);
    }
}

================================================
FILE: src/rendering/shaders.ts
================================================
const VERTEX_SHADER = `
    attribute vec4 vertexPosition;
    attribute vec4 normal;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    varying vec3 v2fNormal;

    void main() {
        v2fNormal = (modelViewMatrix * vec4(normal.xyz, 0.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vertexPosition;
    }
`;


const FRAGMENT_SHADER = `
    precision mediump float;

    const vec3 lightDirection = vec3(-0.7, -0.7, 0.14);
    const float ambient = 0.2;
    const float diffuse = 0.8;
    const float specular = 0.3;
    const vec3 viewDirection = vec3(0.0, 0.0, 1.0);

    varying vec3 v2fNormal;

    uniform vec3 albedo;
    uniform float alpha;

    void main() {
        vec3 color = albedo * (ambient
             + diffuse * (0.5 + 0.5 * dot(lightDirection, v2fNormal))
             + specular * pow(max(0.0, dot(reflect(-lightDirection, v2fNormal), viewDirection)), 2.0));

        gl_FragColor = vec4(color.r, color.g, color.b, alpha);
    }
`;

const NORMAL_FRAGMENT_SHADER = `
    precision mediump float;

    varying vec3 v2fNormal;

    void main() {
        vec3 normal = vec3(0.5) + 0.5 * normalize(v2fNormal);
        gl_FragColor = vec4(normal, 1.0);
    }
`;

const COUNTOUR_VERTEX = `
    attribute vec2 vertexPosition;

    varying vec2 uv;

    void main() {
        uv = vertexPosition / 2.0 + vec2(0.5);
        gl_Position = vec4(vertexPosition, 0.0, 1.0);
    }
`;

const CONTOUR_FRAGMENT = `
    precision mediump float;

    uniform sampler2D normalTexture;
    uniform sampler2D depthTexture;
    uniform vec2 resolution;

    varying vec2 uv;
    
    const float NORMAL_THRESHOLD = 0.5;

    vec3 getNormal(vec2 uv) {
        vec4 sample = texture2D(normalTexture, uv);
        return 2.0 * sample.xyz - vec3(1.0);
    }

    float getDepth(vec2 uv) {
        return texture2D(depthTexture, uv).r;
    }

    bool isContour(vec2 uv, float referenceDepth, vec3 referenceNormal) {
        float depth = getDepth(uv);
        vec3 normal = getNormal(uv);
        float angle = abs(referenceNormal.z);
        
        float threshold = mix(0.005, 0.0001, pow(-referenceNormal.z, 0.5));

        if (abs(depth - referenceDepth) > threshold) {
            return true;
        }

        if (abs(dot(normal, referenceNormal)) < NORMAL_THRESHOLD) {
            return true;
        }

        return false;
    }

    void main() {
        vec2 pixelSize = vec2(1.0 / resolution.x, 1.0 / resolution.y);

        float depth = getDepth(uv);
        vec3 normal = getNormal(uv);

        float count = 0.0;

        for (float x = -1.0; x <= 1.0; x++) {
            for (float y = -1.0; y <= 1.0; y++) {
                if ((x != 0.0 || y != 0.0) && isContour(uv + pixelSize * vec2(x, y), depth, normal)) {
                    count++;
                }
            }
        }
        float contour = count == 1.0 ? 0.0 : (count - 0.2) / 5.0;
        
        gl_FragColor = vec4(vec3(0.0), contour);
    }
`

const SIMPLE_VERTEX_SHADER = `
    attribute vec4 vertexPosition;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    uniform vec3 scale;

    void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4((vertexPosition.xyz * scale), vertexPosition.a);
    }
`;

const COLOR_FRAGMENT_SHADER = `
    precision mediump float;

    uniform vec4 color;

    void main() {
        gl_FragColor = color;
    }
`;

================================================
FILE: tsconfig.json
================================================
{
    "compilerOptions": {
        "outFile": "app.js",
        "sourceMap": true,
        "target": "esnext",
    },
	"compileOnSave": true,
    "include": [
        "**/*.ts"
    ]
}
Download .txt
gitextract_gf1jn8a0/

├── .github/
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── LICENSE
├── README.md
├── app.css
├── app.ts
├── index.html
├── src/
│   ├── MeshGenerator.ts
│   ├── PartMeshGenerator.ts
│   ├── editor/
│   │   ├── Catalog.ts
│   │   ├── CatalogItem.ts
│   │   ├── Editor.ts
│   │   ├── EditorState.ts
│   │   ├── Handles.ts
│   │   ├── NamedMeasurement.ts
│   │   └── RenderStyle.ts
│   ├── export/
│   │   ├── STLExporter.ts
│   │   └── StudioPartExporter.ts
│   ├── functions.ts
│   ├── geometry/
│   │   ├── Matrix4.ts
│   │   ├── Mesh.ts
│   │   ├── Quaternion.ts
│   │   ├── Ray.ts
│   │   ├── Triangle.ts
│   │   ├── TriangleWithNormals.ts
│   │   ├── Vector3.ts
│   │   └── VectorDictionary.ts
│   ├── measurements.ts
│   ├── model/
│   │   ├── Block.ts
│   │   ├── Part.ts
│   │   ├── PerpendicularRoundedAdaper.ts
│   │   ├── SmallBlock.ts
│   │   ├── TinyBlock.ts
│   │   └── enums/
│   │       ├── BlockType.ts
│   │       ├── Orientation.ts
│   │       └── Quadrant.ts
│   └── rendering/
│       ├── Camera.ts
│       ├── ContourPostEffect.ts
│       ├── MeshRenderer.ts
│       ├── NormalDepthRenderer.ts
│       ├── Renderer.ts
│       ├── Shader.ts
│       ├── WireframeBox.ts
│       ├── WireframeRenderer.ts
│       └── shaders.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (316 symbols across 38 files)

FILE: src/MeshGenerator.ts
  class MeshGenerator (line 1) | class MeshGenerator {
    method constructor (line 5) | constructor(measurements: Measurements) {
    method getMesh (line 9) | public getMesh(): Mesh {
    method createQuad (line 13) | protected createQuad(v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector...
    method createQuadWithNormals (line 23) | protected createQuadWithNormals(v1: Vector3, v2: Vector3, v3: Vector3,...
    method createCircleWithHole (line 33) | protected createCircleWithHole(block: TinyBlock, innerRadius: number, ...
    method createCircle (line 64) | protected createCircle(block: TinyBlock, radius: number, offset: numbe...
    method createCylinder (line 79) | protected createCylinder(block: TinyBlock, offset: number, radius: num...
    method tinyIndexToWorld (line 95) | public tinyIndexToWorld(p: number): number {
    method tinyBlockToWorld (line 109) | public tinyBlockToWorld(position: Vector3): Vector3 {

FILE: src/PartMeshGenerator.ts
  class PartMeshGenerator (line 1) | class PartMeshGenerator extends MeshGenerator {
    method constructor (line 5) | constructor(part: Part, measurements: Measurements) {
    method updateRounded (line 21) | private updateRounded() {
    method createDummyBlocks (line 73) | private createDummyBlocks() {
    method createPerpendicularRoundedAdapterIfPossible (line 104) | private createPerpendicularRoundedAdapterIfPossible(block: SmallBlock)...
    method createTinyBlocks (line 130) | private createTinyBlocks() {
    method isTinyBlock (line 175) | private isTinyBlock(position: Vector3): boolean {
    method pushBlock (line 179) | private pushBlock(smallBlock: SmallBlock, forwardFactor: number) {
    method processTinyBlocks (line 207) | private processTinyBlocks() {
    method checkInteriors (line 249) | private checkInteriors() {
    method getPerpendicularRoundedNeighborOrNull (line 273) | private getPerpendicularRoundedNeighborOrNull(block: TinyBlock): Small...
    method getPerpendicularRoundedNeighborOrNull2 (line 285) | private getPerpendicularRoundedNeighborOrNull2(block: TinyBlock): Smal...
    method preventMergingForPerpendicularRoundedBlock (line 294) | private preventMergingForPerpendicularRoundedBlock(block1: TinyBlock, ...
    method mergeSimilarBlocks (line 307) | private mergeSimilarBlocks() {
    method isSmallBlock (line 368) | private isSmallBlock(position: Vector3): boolean {
    method createTinyBlock (line 372) | private createTinyBlock(position: Vector3, source: SmallBlock) {
    method getNextBlock (line 376) | private getNextBlock(block: TinyBlock, interior: boolean): TinyBlock {
    method getPreviousBlock (line 381) | private getPreviousBlock(block: TinyBlock): TinyBlock {
    method hasOpenEnd (line 385) | private hasOpenEnd(block: TinyBlock, interior: boolean): boolean {
    method hasOpenStart (line 395) | private hasOpenStart(block: TinyBlock): boolean {
    method hideStartEndFaces (line 403) | private hideStartEndFaces(position: Vector3, block: TinyBlock, forward...
    method hideFaceIfExists (line 411) | private hideFaceIfExists(position: Vector3, direction: Vector3) {
    method hideOutsideFaces (line 417) | private hideOutsideFaces(centerBlock: TinyBlock) {
    method renderPerpendicularRoundedAdapters (line 426) | private renderPerpendicularRoundedAdapters() {
    method isPerpendicularRoundedAdapter (line 470) | private isPerpendicularRoundedAdapter(block: TinyBlock) {
    method renderRoundedExteriors (line 478) | private renderRoundedExteriors() {
    method renderInteriors (line 525) | private renderInteriors() {
    method renderAttachments (line 539) | private renderAttachments() {
    method renderLip (line 559) | private renderLip(block: TinyBlock, zOffset: number) {
    method renderPin (line 582) | private renderPin(block: TinyBlock) {
    method renderAxle (line 624) | private renderAxle(block: TinyBlock) {
    method renderBallJoint (line 753) | private renderBallJoint(block: TinyBlock) {
    method createAxleToCircleAdapter (line 838) | private createAxleToCircleAdapter(center: Vector3, block: SmallBlock, ...
    method showInteriorCap (line 872) | private showInteriorCap(currentBlock: SmallBlock, neighbor: SmallBlock...
    method renderPinHoleInterior (line 891) | private renderPinHoleInterior(block: TinyBlock) {
    method renderAxleHoleInterior (line 928) | private renderAxleHoleInterior(block: TinyBlock) {
    method isFaceVisible (line 1058) | private isFaceVisible(position: Vector3, direction: Vector3): boolean {
    method createTinyFace (line 1066) | private createTinyFace(position: Vector3, size: Vector3, direction: Ve...
    method isRowOfVisibleFaces (line 1092) | private isRowOfVisibleFaces(position: Vector3, rowDirection: Vector3, ...
    method findConnectedFaces (line 1109) | private findConnectedFaces(position: Vector3, direction: Vector3): [Ve...
    method hideFaces (line 1142) | private hideFaces(position: Vector3, size: Vector3, direction: Vector3) {
    method renderTinyBlockFaces (line 1152) | private renderTinyBlockFaces() {

FILE: src/editor/Catalog.ts
  class Catalog (line 1) | class Catalog {
    method constructor (line 7) | constructor() {
    method onToggleCatalog (line 13) | private onToggleCatalog(event: MouseEvent) {
    method createCatalogUI (line 19) | private createCatalogUI() {
    method createCatalogItems (line 67) | private createCatalogItems() {
    method onSelectPart (line 131) | private onSelectPart(item: CatalogItem, event: MouseEvent) {

FILE: src/editor/CatalogItem.ts
  class CatalogItem (line 1) | class CatalogItem {
    method constructor (line 7) | constructor(id: number, name: string, string: string) {

FILE: src/editor/Editor.ts
  type MouseMode (line 1) | enum MouseMode {
  class Editor (line 8) | class Editor {
    method constructor (line 36) | constructor() {
    method onNodeEditorClick (line 104) | private onNodeEditorClick(event: MouseEvent) {
    method saveSTL (line 109) | private saveSTL() {
    method saveStudioPart (line 113) | private saveStudioPart() {
    method initializeEditor (line 117) | private initializeEditor(elementId: string, onchange: (value: string) ...
    method clear (line 127) | private clear() {
    method share (line 132) | private share() {
    method remove (line 141) | private remove() {
    method setType (line 149) | private setType(typeName: string) {
    method setOrientation (line 154) | private setOrientation(orientatioName: string) {
    method setSize (line 160) | private setSize(sizeName: string) {
    method setRounded (line 166) | private setRounded(roundedName: string) {
    method setRenderStyle (line 171) | private setRenderStyle(style: RenderStyle) {
    method updateBlock (line 180) | private updateBlock() {
    method updateMesh (line 189) | public updateMesh(center = false) {
    method getRotation (line 213) | private getRotation(): Matrix4 {
    method updateTransform (line 217) | private updateTransform() {
    method onMouseDown (line 224) | private onMouseDown(event: MouseEvent) {
    method onMouseUp (line 240) | private onMouseUp(event: MouseEvent) {
    method onMouseMove (line 246) | private onMouseMove(event: MouseEvent) {
    method onScroll (line 268) | private onScroll(event: WheelEvent) {
    method onKeydown (line 274) | private onKeydown(event: KeyboardEvent) {
    method displayMeasurements (line 299) | private displayMeasurements() {
    method applyMeasurements (line 305) | public applyMeasurements() {
    method resetMeasurements (line 314) | private resetMeasurements() {
    method getNameTextbox (line 320) | public getNameTextbox(): HTMLInputElement {
    method getName (line 324) | public getName(): string {
    method onPartNameChange (line 332) | private onPartNameChange(event: Event) {
    method setName (line 341) | public setName(name: string) {

FILE: src/editor/EditorState.ts
  class EditorState (line 1) | class EditorState {

FILE: src/editor/Handles.ts
  constant ARROW_RADIUS_INNER (line 1) | const ARROW_RADIUS_INNER = 0.05;
  constant ARROW_RADIUS_OUTER (line 2) | const ARROW_RADIUS_OUTER = 0.15;
  constant ARROW_LENGTH (line 3) | const ARROW_LENGTH = 0.35;
  constant ARROW_TIP (line 4) | const ARROW_TIP = 0.15;
  constant HANDLE_DISTANCE (line 6) | const HANDLE_DISTANCE = 0.5;
  constant GRAB_RADIUS (line 8) | const GRAB_RADIUS = 0.1;
  constant GRAB_START (line 9) | const GRAB_START = 0.4;
  constant GRAB_END (line 10) | const GRAB_END = 1.1;
  constant UNSELECTED_ALPHA (line 12) | const UNSELECTED_ALPHA = 0.5;
  type Axis (line 14) | enum Axis {
  class Handles (line 21) | class Handles implements Renderer {
    method createRenderer (line 47) | private createRenderer(mesh: Mesh, color: Vector3): MeshRenderer {
    method getBlockCenter (line 55) | private getBlockCenter(block: Vector3): Vector3 {
    method getBlock (line 63) | private getBlock(worldPosition: Vector3): Vector3 {
    method constructor (line 71) | constructor(camera: Camera) {
    method render (line 87) | public render(camera: Camera) {
    method updateTransforms (line 116) | public updateTransforms() {
    method getVector (line 133) | private static getVector(angle: number, radius: number, z: number): Ve...
    method getArrowMesh (line 137) | public static getArrowMesh(subdivisions: number): Mesh {
    method getRay (line 185) | private getRay(axis: Axis): Ray {
    method getMouseHandle (line 197) | private getMouseHandle(event: MouseEvent): [Axis, number] {
    method onMouseDown (line 211) | public onMouseDown(event: MouseEvent): boolean {
    method onMouseMove (line 218) | public onMouseMove(event: MouseEvent) {
    method onMouseUp (line 238) | public onMouseUp() {
    method move (line 245) | public move(direction: Vector3) {
    method getSelectedBlock (line 252) | public getSelectedBlock(): Vector3 {
    method setMode (line 256) | public setMode(fullSize: boolean, orientation: Orientation, animate: b...
    method animatePositionAndSize (line 292) | private animatePositionAndSize(targetPosition: Vector3, targetSize: Ve...

FILE: src/editor/NamedMeasurement.ts
  class NamedMeasurement (line 1) | class NamedMeasurement {
    method constructor (line 8) | constructor(name: string, relative: boolean, displayDouble: boolean) {
    method readFromDOM (line 20) | public readFromDOM(measurements: Measurements) {
    method applyToDom (line 34) | public applyToDom(measurements: Measurements) {
    method reset (line 47) | private reset(event: MouseEvent) {
  constant NAMED_MEASUREMENTS (line 55) | const NAMED_MEASUREMENTS : NamedMeasurement[] = [

FILE: src/editor/RenderStyle.ts
  type RenderStyle (line 1) | enum RenderStyle {

FILE: src/export/STLExporter.ts
  class STLExporter (line 1) | class STLExporter {
    method constructor (line 5) | constructor(size: number) {
    method writeVector (line 10) | private writeVector(offset: number, vector: Vector3) {
    method writeTriangle (line 16) | private writeTriangle(offset: number, triangle: Triangle, scalingFacto...
    method fixOpenEdges (line 24) | private static fixOpenEdges(triangles: Triangle[]): Triangle[] {
    method createBuffer (line 143) | private static createBuffer(part: Part, measurements: Measurements) {
    method saveSTLFile (line 165) | public static saveSTLFile(part: Part, measurements: Measurements, name...

FILE: src/export/StudioPartExporter.ts
  class StudioPartExporter (line 1) | class StudioPartExporter {
    method formatPoint (line 2) | private static formatPoint(vector: Vector3): string {
    method formatVector (line 6) | private static formatVector(vector: Vector3): string {
    method formatConnector (line 10) | private static formatConnector(position: Vector3, block: Block, facesF...
    method createFileContent (line 41) | private static createFileContent(part: Part, measurements: Measurement...
    method savePartFile (line 132) | public static savePartFile(part: Part, measurements: Measurements, nam...

FILE: src/functions.ts
  function triangularNumber (line 1) | function triangularNumber(n: number): number {
  function inverseTriangularNumber (line 5) | function inverseTriangularNumber(s: number): number {
  function tetrahedralNumber (line 9) | function tetrahedralNumber(n: number): number {
  function inverseTetrahedralNumber (line 13) | function inverseTetrahedralNumber(s: number): number {
  constant DEG_TO_RAD (line 21) | let DEG_TO_RAD = Math.PI / 180;
  function min (line 23) | function min<T>(iterable: Iterable<T>, selector: (item: T) => number): n...
  function sign (line 37) | function sign(a: number): number {
  function lerp (line 47) | function lerp(a: number, b: number, t: number): number {
  function clamp (line 51) | function clamp(lower: number, upper: number, value: number) {
  function countInArray (line 61) | function countInArray<T>(items: T[], selector: (item: T) => boolean): nu...
  function ease (line 71) | function ease(value: number): number {
  function mod (line 75) | function mod(a: number, b: number): number {
  function containsPoint (line 79) | function containsPoint(list: Vector3[], query: Vector3): boolean {

FILE: src/geometry/Matrix4.ts
  type NumberArray16 (line 1) | type NumberArray16 = [number, number, number, number, number, number, nu...
  class Matrix4 (line 3) | class Matrix4 {
    method constructor (line 6) | constructor(elements: NumberArray16) {
    method get (line 10) | get(i: number, j: number): number {
    method times (line 14) | public times(other: Matrix4): Matrix4 {
    method transpose (line 30) | public transpose() {
    method invert (line 39) | public invert(): Matrix4 {
    method transformPoint (line 85) | public transformPoint(point: Vector3): Vector3 {
    method transformDirection (line 92) | public transformDirection(point: Vector3): Vector3 {
    method getProjection (line 99) | public static getProjection(near = 0.1, far = 1000, fov = 25): Matrix4 {
    method getOrthographicProjection (line 109) | public static getOrthographicProjection(far = 1000, size = 5): Matrix4 {
    method getIdentity (line 119) | public static getIdentity(): Matrix4 {
    method getTranslation (line 128) | public static getTranslation(vector: Vector3): Matrix4 {
    method getRotation (line 137) | public static getRotation(euler: Vector3): Matrix4 {

FILE: src/geometry/Mesh.ts
  class Mesh (line 1) | class Mesh {
    method constructor (line 7) | constructor(triangles: Triangle[]) {
    method createVertexBuffer (line 11) | public createVertexBuffer(): WebGLBuffer {
    method createNormalBuffer (line 32) | public createNormalBuffer(): WebGLBuffer {
    method createWireframeVertexBuffer (line 58) | public createWireframeVertexBuffer(): WebGLBuffer {
    method pushVector (line 77) | private pushVector(array: number[], vector: Vector3) {
    method getVertexCount (line 83) | public getVertexCount(): number {

FILE: src/geometry/Quaternion.ts
  class Quaternion (line 1) | class Quaternion {
    method constructor (line 7) | constructor(x: number, y: number, z: number, w: number) {
    method times (line 14) | times(other: Quaternion): Quaternion {
    method toMatrix (line 21) | toMatrix(): Matrix4 {
    method euler (line 30) | static euler(angles: Vector3): Quaternion {
    method angleAxis (line 36) | static angleAxis(angle: number, axis: Vector3): Quaternion {
    method identity (line 41) | static identity(): Quaternion {

FILE: src/geometry/Ray.ts
  class Ray (line 1) | class Ray {
    method constructor (line 5) | constructor(point: Vector3, direction: Vector3) {
    method get (line 10) | get(t: number): Vector3 {
    method getDistanceToRay (line 14) | getDistanceToRay(other: Ray): number {
    method getClosestToPoint (line 23) | getClosestToPoint(point: Vector3): number {
    method getClosestToRay (line 27) | getClosestToRay(other: Ray): number {

FILE: src/geometry/Triangle.ts
  class Triangle (line 1) | class Triangle {
    method constructor (line 6) | constructor(v1: Vector3, v2: Vector3, v3: Vector3, flipped = false) {
    method normal (line 18) | public normal(): Vector3 {
    method getOnEdge1 (line 22) | public getOnEdge1(progress: number): Vector3 {
    method getOnEdge2 (line 26) | public getOnEdge2(progress: number): Vector3 {
    method getOnEdge3 (line 30) | public getOnEdge3(progress: number): Vector3 {

FILE: src/geometry/TriangleWithNormals.ts
  class TriangleWithNormals (line 1) | class TriangleWithNormals extends Triangle {
    method constructor (line 6) | constructor(v1: Vector3, v2: Vector3, v3: Vector3, n1: Vector3, n2: Ve...

FILE: src/geometry/Vector3.ts
  class Vector3 (line 1) | class Vector3 {
    method constructor (line 6) | constructor(x: number, y: number, z: number) {
    method times (line 12) | public times(factor: number): Vector3 {
    method plus (line 16) | public plus(other: Vector3): Vector3 {
    method minus (line 20) | public minus(other: Vector3): Vector3 {
    method dot (line 24) | public dot(other: Vector3): number {
    method cross (line 28) | public cross(other: Vector3): Vector3 {
    method elementwiseMultiply (line 32) | public elementwiseMultiply(other: Vector3) {
    method magnitude (line 36) | public magnitude(): number {
    method normalized (line 40) | public normalized(): Vector3 {
    method toString (line 44) | public toString(): string {
    method copy (line 48) | public copy(): Vector3 {
    method equals (line 52) | public equals(other: Vector3): boolean {
    method floor (line 56) | public floor(): Vector3 {
    method toNumber (line 60) | public toNumber(): number {
    method fromNumber (line 67) | public static fromNumber(value: number): Vector3 {
    method zero (line 79) | public static zero(): Vector3 {
    method one (line 83) | public static one(): Vector3 {
    method lerp (line 87) | public static lerp(a: Vector3, b: Vector3, progress: number): Vector3 {
    method isCollinear (line 91) | public static isCollinear(a: Vector3, b: Vector3) {
    method interpolate (line 124) | public static interpolate(a: Vector3, b: Vector3, t: number) {
  constant RIGHT_FACE_VERTICES (line 129) | const RIGHT_FACE_VERTICES = [
  constant LEFT_FACE_VERTICES (line 136) | const LEFT_FACE_VERTICES = [
  constant UP_FACE_VERTICES (line 143) | const UP_FACE_VERTICES = [
  constant DOWN_FACE_VERTICES (line 150) | const DOWN_FACE_VERTICES = [
  constant FORWARD_FACE_VERTICES (line 157) | const FORWARD_FACE_VERTICES = [
  constant BACK_FACE_VERTICES (line 164) | const BACK_FACE_VERTICES = [
  constant FACE_DIRECTIONS (line 171) | const FACE_DIRECTIONS = [

FILE: src/geometry/VectorDictionary.ts
  class VectorDictionary (line 1) | class VectorDictionary<T> {
    method containsKey (line 10) | containsKey(key: Vector3): boolean {
    method get (line 14) | get(key: Vector3): T {
    method getOrNull (line 21) | getOrNull(key: Vector3): T {
    method set (line 28) | set(key: Vector3, value: T) {
    method remove (line 38) | remove(key: Vector3) {
    method clear (line 44) | clear() {
    method keys (line 48) | *keys(): IterableIterator<Vector3> {
    method values (line 58) | *values(): IterableIterator<T> {
    method any (line 68) | any(): boolean {

FILE: src/measurements.ts
  class Measurements (line 1) | class Measurements {
    method enforceConstraints (line 23) | public enforceConstraints() {
  constant DEFAULT_MEASUREMENTS (line 41) | const DEFAULT_MEASUREMENTS = new Measurements();

FILE: src/model/Block.ts
  class Block (line 1) | class Block {
    method constructor (line 11) | constructor(orientation: Orientation, type: BlockType, rounded: boolea...

FILE: src/model/Part.ts
  constant CUBE (line 3) | let CUBE = [
  class Part (line 14) | class Part {
    method createSmallBlocks (line 17) | public createSmallBlocks(): VectorDictionary<SmallBlock> {
    method isSmallBlockFree (line 33) | public isSmallBlockFree(position: Vector3): boolean {
    method clearSingle (line 46) | public clearSingle(position: Vector3) {
    method clearBlock (line 58) | public clearBlock(position: Vector3, orientation: Orientation) {
    method isBlockPlaceable (line 66) | public isBlockPlaceable(position: Vector3, orientation: Orientation, d...
    method placeBlockForced (line 78) | public placeBlockForced(position: Vector3, block: Block) {
    method toString (line 83) | public toString(): string {
    method fromString (line 106) | public static fromString(s: string): Part {
    method getBoundingBox (line 131) | private getBoundingBox(): [Vector3, Vector3] {
    method getCenter (line 164) | public getCenter(): Vector3 {
    method getSize (line 176) | public getSize() {

FILE: src/model/PerpendicularRoundedAdaper.ts
  class PerpendicularRoundedAdapter (line 1) | class PerpendicularRoundedAdapter {

FILE: src/model/SmallBlock.ts
  class SmallBlock (line 1) | class SmallBlock extends Block {
    method constructor (line 15) | constructor(quadrant: Quadrant, positon: Vector3, source: Block) {
    method createFromLocalCoordinates (line 30) | public static createFromLocalCoordinates(localX: number, localY: numbe...
    method odd (line 34) | public odd(): boolean {
    method getQuadrantFromLocal (line 38) | private static getQuadrantFromLocal(x: number, y: number): Quadrant {
    method getOnCircle (line 54) | public getOnCircle(angle: number, radius = 1): Vector3 {

FILE: src/model/TinyBlock.ts
  class TinyBlock (line 1) | class TinyBlock extends SmallBlock {
    method constructor (line 14) | constructor(position: Vector3, source: SmallBlock) {
    method getCylinderOrigin (line 28) | public getCylinderOrigin(meshGenerator: MeshGenerator): Vector3 {
    method getExteriorDepth (line 34) | public getExteriorDepth(meshGenerator: MeshGenerator): number {
    method getInteriorDepth (line 38) | public getInteriorDepth(meshGenerator: MeshGenerator): number {
    method isFaceVisible (line 42) | public isFaceVisible(direction: Vector3): boolean {
    method hideFace (line 60) | public hideFace(direction: Vector3) {

FILE: src/model/enums/BlockType.ts
  type BlockType (line 1) | enum BlockType {
  constant BLOCK_TYPE (line 11) | const BLOCK_TYPE = {

FILE: src/model/enums/Orientation.ts
  type Orientation (line 1) | enum Orientation {
  constant ORIENTATION (line 7) | const ORIENTATION = {
  constant FORWARD (line 13) | const FORWARD = {
  constant RIGHT (line 19) | const RIGHT = {
  constant LEFT (line 31) | const LEFT = {
  constant DOWN (line 37) | const DOWN = {

FILE: src/model/enums/Quadrant.ts
  type Quadrant (line 1) | enum Quadrant {
  function localX (line 8) | function localX(quadrant: Quadrant): number {
  function localY (line 12) | function localY(quadrant: Quadrant): number {
  function getAngle (line 16) | function getAngle(quadrant: Quadrant): number {

FILE: src/rendering/Camera.ts
  class Camera (line 1) | class Camera {
    method constructor (line 16) | constructor(canvas: HTMLCanvasElement, supersample = 1) {
    method createBuffers (line 30) | private createBuffers() {
    method getProjectionMatrix (line 53) | public getProjectionMatrix(): Matrix4 {
    method render (line 57) | public render() {
    method onResize (line 77) | public onResize() {
    method getScreenToWorldRay (line 84) | public getScreenToWorldRay(event: MouseEvent): Ray {

FILE: src/rendering/ContourPostEffect.ts
  class ContourPostEffect (line 1) | class ContourPostEffect implements Renderer {
    method constructor (line 8) | constructor() {
    method render (line 22) | public render(camera: Camera) {

FILE: src/rendering/MeshRenderer.ts
  class MeshRenderer (line 1) | class MeshRenderer implements Renderer {
    method constructor (line 14) | constructor() {
    method setMesh (line 27) | public setMesh(mesh: Mesh) {
    method render (line 33) | public render(camera: Camera) {

FILE: src/rendering/NormalDepthRenderer.ts
  class NormalDepthRenderer (line 1) | class NormalDepthRenderer implements Renderer {
    method constructor (line 13) | constructor() {
    method prepareShaders (line 18) | private prepareShaders() {
    method setMesh (line 26) | public setMesh(mesh: Mesh) {
    method render (line 32) | public render(camera: Camera) {

FILE: src/rendering/Renderer.ts
  type Renderer (line 1) | interface Renderer {

FILE: src/rendering/Shader.ts
  class Shader (line 1) | class Shader {
    method loadShader (line 5) | private loadShader(type: number, source: string): WebGLShader {
    method constructor (line 21) | constructor(vertexSource: string, fragmentSource: string) {
    method setAttribute (line 35) | public setAttribute( name: string) {
    method setUniform (line 39) | public setUniform(name: string) {

FILE: src/rendering/WireframeBox.ts
  class WireframeBox (line 1) | class WireframeBox implements Renderer {
    method constructor (line 16) | constructor() {
    method render (line 46) | public render(camera: Camera) {

FILE: src/rendering/WireframeRenderer.ts
  class WireframeRenderer (line 1) | class WireframeRenderer implements Renderer {
    method constructor (line 13) | constructor() {
    method setMesh (line 25) | public setMesh(mesh: Mesh) {
    method render (line 30) | public render(camera: Camera) {

FILE: src/rendering/shaders.ts
  constant VERTEX_SHADER (line 1) | const VERTEX_SHADER = `
  constant FRAGMENT_SHADER (line 17) | const FRAGMENT_SHADER = `
  constant NORMAL_FRAGMENT_SHADER (line 40) | const NORMAL_FRAGMENT_SHADER = `
  constant COUNTOUR_VERTEX (line 51) | const COUNTOUR_VERTEX = `
  constant CONTOUR_FRAGMENT (line 62) | const CONTOUR_FRAGMENT = `
  constant SIMPLE_VERTEX_SHADER (line 121) | const SIMPLE_VERTEX_SHADER = `
  constant COLOR_FRAGMENT_SHADER (line 134) | const COLOR_FRAGMENT_SHADER = `
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (191K chars).
[
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 756,
    "preview": "name: GitHub Pages Deployment\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n   "
  },
  {
    "path": ".gitignore",
    "chars": 14,
    "preview": "*.js\n*.js.map\n"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2019 Marian Kleineberg\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 1094,
    "preview": "![](https://i.imgur.com/L7moBQT.png)\n\n# Part Designer\n\nThis is a free online CAD tool to create custom LEGO® Technic com"
  },
  {
    "path": "app.css",
    "chars": 5795,
    "preview": "html, body {\n    font-family: 'Segoe UI', sans-serif;\n    margin: 0px;\n    height: 100%;\n    overflow: hidden;\n    font-"
  },
  {
    "path": "app.ts",
    "chars": 406,
    "preview": "let gl: WebGLRenderingContext;\n\nvar editor: Editor;\nvar catalog: Catalog;\n\nwindow.onload = () => {\n\tcatalog = new Catal"
  },
  {
    "path": "index.html",
    "chars": 11414,
    "preview": "<!DOCTYPE html>\n\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\" />\n    <title>Part Designer</title>\n    <meta name=\"a"
  },
  {
    "path": "src/MeshGenerator.ts",
    "chars": 4854,
    "preview": "class MeshGenerator {\n    protected triangles: Triangle[] = [];\n    protected measurements: Measurements;\n\n    construct"
  },
  {
    "path": "src/PartMeshGenerator.ts",
    "chars": 52209,
    "preview": "class PartMeshGenerator extends MeshGenerator {\n    private smallBlocks: VectorDictionary<SmallBlock>;\n\tprivate tinyBloc"
  },
  {
    "path": "src/editor/Catalog.ts",
    "chars": 7726,
    "preview": "class Catalog {\n\tprivate container: HTMLElement;\n\n\tprivate initialized: boolean = false;\n\tpublic items: CatalogItem[];\n\n"
  },
  {
    "path": "src/editor/CatalogItem.ts",
    "chars": 246,
    "preview": "class CatalogItem {\n\tpart: Part = null;\n\tid: number;\n\tstring: string;\n\tname: string;\n\n\tconstructor(id: number, name: str"
  },
  {
    "path": "src/editor/Editor.ts",
    "chars": 11848,
    "preview": "enum MouseMode {\n\tNone,\n\tManipulate,\n\tTranslate,\n\tRotate\n}\n\nclass Editor {\n\tcamera: Camera;\n\tpartRenderer: MeshRenderer;"
  },
  {
    "path": "src/editor/EditorState.ts",
    "chars": 183,
    "preview": "class EditorState {\n\tpublic orientation: Orientation = Orientation.X;\n\tpublic type: BlockType = BlockType.PinHole;\n\tpubl"
  },
  {
    "path": "src/editor/Handles.ts",
    "chars": 10509,
    "preview": "const ARROW_RADIUS_INNER = 0.05;\nconst ARROW_RADIUS_OUTER = 0.15;\nconst ARROW_LENGTH = 0.35;\nconst ARROW_TIP = 0.15;\n\nco"
  },
  {
    "path": "src/editor/NamedMeasurement.ts",
    "chars": 2588,
    "preview": "class NamedMeasurement {\n\tprivate name: string;\n\tprivate relative: boolean;\n\tprivate displayDouble: boolean;\n\tprivate do"
  },
  {
    "path": "src/editor/RenderStyle.ts",
    "chars": 66,
    "preview": "enum RenderStyle {\n\tContour,\n\tSolid,\n\tWireframe,\n\tSolidWireframe\n}"
  },
  {
    "path": "src/export/STLExporter.ts",
    "chars": 6878,
    "preview": "class STLExporter {\n    private readonly buffer: ArrayBuffer;\n    private readonly view: DataView;\n\n    constructor(size"
  },
  {
    "path": "src/export/StudioPartExporter.ts",
    "chars": 5489,
    "preview": "class StudioPartExporter {\n    private static formatPoint(vector: Vector3): string {\n        return (vector.x * 20).toFi"
  },
  {
    "path": "src/functions.ts",
    "chars": 1801,
    "preview": "function triangularNumber(n: number): number {\n\treturn n * (n + 1) / 2;\n}\n\nfunction inverseTriangularNumber(s: number):"
  },
  {
    "path": "src/geometry/Matrix4.ts",
    "chars": 6473,
    "preview": "type NumberArray16 = [number, number, number, number, number, number, number, number, number, number, number, number, nu"
  },
  {
    "path": "src/geometry/Mesh.ts",
    "chars": 2716,
    "preview": "class Mesh {\n    public readonly triangles: Triangle[];\n\n    private vertexBuffer: WebGLBuffer = null;\n    private norma"
  },
  {
    "path": "src/geometry/Quaternion.ts",
    "chars": 1641,
    "preview": "class Quaternion {\n\tx: number;\n\ty: number;\n\tz: number;\n\tw: number;\n\n\tconstructor(x: number, y: number, z: number, w: num"
  },
  {
    "path": "src/geometry/Ray.ts",
    "chars": 904,
    "preview": "class Ray {\n\tpoint: Vector3;\n\tdirection: Vector3;\n\n\tconstructor(point: Vector3, direction: Vector3) {\n\t\tthis.point = poi"
  },
  {
    "path": "src/geometry/Triangle.ts",
    "chars": 894,
    "preview": "class Triangle {\n    public readonly v1: Vector3;\n    public readonly v2: Vector3;\n    public readonly v3: Vector3;\n\n   "
  },
  {
    "path": "src/geometry/TriangleWithNormals.ts",
    "chars": 254,
    "preview": "class TriangleWithNormals extends Triangle {\n\tn1: Vector3;\n\tn2: Vector3;\n\tn3: Vector3;\n\n\tconstructor(v1: Vector3, v2: Ve"
  },
  {
    "path": "src/geometry/Vector3.ts",
    "chars": 4016,
    "preview": "class Vector3 {\n\tpublic readonly x: number;\n\tpublic readonly y: number;\n\tpublic readonly z: number;\n\n\tconstructor(x: nu"
  },
  {
    "path": "src/geometry/VectorDictionary.ts",
    "chars": 1552,
    "preview": "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} = "
  },
  {
    "path": "src/measurements.ts",
    "chars": 1965,
    "preview": "class Measurements {\n\ttechnicUnit = 8;\n\n\tedgeMargin = 0.2 / this.technicUnit;\n\tinteriorRadius = 3.2 / this.technicUnit;\n"
  },
  {
    "path": "src/model/Block.ts",
    "chars": 655,
    "preview": "class Block {\n\tpublic readonly orientation: Orientation;\n\tpublic readonly type: BlockType;\n\tpublic rounded: boolean;\n\n\t"
  },
  {
    "path": "src/model/Part.ts",
    "chars": 4857,
    "preview": "///<reference path=\"../geometry/Vector3.ts\" />\n\nlet CUBE = [\n\tnew Vector3(0, 0, 0),\n\tnew Vector3(0, 0, 1),\n\tnew Vector3("
  },
  {
    "path": "src/model/PerpendicularRoundedAdaper.ts",
    "chars": 198,
    "preview": "class PerpendicularRoundedAdapter {\n\tpublic isVertical: boolean;\n\tpublic neighbor: SmallBlock;\n\tpublic directionToNeighb"
  },
  {
    "path": "src/model/SmallBlock.ts",
    "chars": 1850,
    "preview": "class SmallBlock extends Block {\n\tpublic readonly quadrant: Quadrant;\n\tpublic readonly position: Vector3;\n\tpublic hasIn"
  },
  {
    "path": "src/model/TinyBlock.ts",
    "chars": 3273,
    "preview": "class TinyBlock extends SmallBlock {\n\tpublic exteriorMergedBlocks = 1;\n\tpublic isExteriorMerged = false;\n\t\n\tpublic inte"
  },
  {
    "path": "src/model/enums/BlockType.ts",
    "chars": 319,
    "preview": "enum BlockType {\n\tSolid,\n\tPinHole,\n\tAxleHole,\n\tPin,\n\tAxle,\n\tBallJoint,\n\tBallSocket\n}\n\nconst BLOCK_TYPE = {\n\t\"solid\": Bl"
  },
  {
    "path": "src/model/enums/Orientation.ts",
    "chars": 618,
    "preview": "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\": Orie"
  },
  {
    "path": "src/model/enums/Quadrant.ts",
    "chars": 659,
    "preview": "enum Quadrant {\n\tTopLeft,\n\tTopRight,\n\tBottomLeft,\n\tBottomRight\n}\n\nfunction localX(quadrant: Quadrant): number {\n\treturn"
  },
  {
    "path": "src/rendering/Camera.ts",
    "chars": 4227,
    "preview": "class Camera {\n    public renderers: Renderer[] = [];\n\n    public transform: Matrix4 = Matrix4.getIdentity();\n\n    publi"
  },
  {
    "path": "src/rendering/ContourPostEffect.ts",
    "chars": 1570,
    "preview": "class ContourPostEffect implements Renderer {\n\n\tprivate shader: Shader;\n\tprivate vertices: WebGLBuffer;\n\t\n\tpublic enable"
  },
  {
    "path": "src/rendering/MeshRenderer.ts",
    "chars": 2034,
    "preview": "class MeshRenderer implements Renderer {\n    private shader: Shader;\n\n    private vertices: WebGLBuffer;\n    private nor"
  },
  {
    "path": "src/rendering/NormalDepthRenderer.ts",
    "chars": 2062,
    "preview": "class NormalDepthRenderer implements Renderer {\n    private shader: Shader;\n\n    private vertices: WebGLBuffer;\n    priv"
  },
  {
    "path": "src/rendering/Renderer.ts",
    "chars": 47,
    "preview": "interface Renderer {\n\trender(camera: Camera);\n}"
  },
  {
    "path": "src/rendering/Shader.ts",
    "chars": 1538,
    "preview": "class Shader {\n\tpublic program: WebGLShader;\n\tpublic attributes: {[id: string]: number } = {};\n\n\tprivate loadShader(type"
  },
  {
    "path": "src/rendering/WireframeBox.ts",
    "chars": 2343,
    "preview": "class WireframeBox implements Renderer {\n\tprivate shader: Shader;\n\tprivate positions: WebGLBuffer;\n\n\tpublic transform: M"
  },
  {
    "path": "src/rendering/WireframeRenderer.ts",
    "chars": 1622,
    "preview": "class WireframeRenderer implements Renderer {\n\tprivate shader: Shader;\n\tprivate vertices: WebGLBuffer;\n\tprivate vertexCo"
  },
  {
    "path": "src/rendering/shaders.ts",
    "chars": 3426,
    "preview": "const VERTEX_SHADER = `\n    attribute vec4 vertexPosition;\n    attribute vec4 normal;\n\n    uniform mat4 modelViewMatrix;"
  },
  {
    "path": "tsconfig.json",
    "chars": 184,
    "preview": "{\n    \"compilerOptions\": {\n        \"outFile\": \"app.js\",\n        \"sourceMap\": true,\n        \"target\": \"esnext\",\n    },\n\t\""
  }
]

About this extraction

This page contains the full source code of the marian42/partdesigner GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (172.7 KB), approximately 48.3k tokens, and a symbol index with 316 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!