diff --git a/.gitignore b/.gitignore
index 82b4e8f..c1eca77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,15 @@
*.stl
!data/*.stl
__pycache__
+out/
+
+node_modules
+a.*
+
+openscad.wasm
+openscad.wasm.js
+worker.js
+
+# Added by cargo
+
+/target
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..17ba462
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "webapp/openscad-wasm"]
+ path = webapp/openscad-wasm
+ url = git@github.com:openscad/openscad-wasm.git
diff --git a/flake.lock b/flake.lock
index 786f446..dc9c41a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1774106199,
- "narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
+ "lastModified": 1783522502,
+ "narHash": "sha256-iffAls3iaNTyJC2faYcUXSI+Gp02cDjYl+MygxKl2GI=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
+ "rev": "0bb7ec54c8483066ec9d7720e780a5caa71f8612",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 9a89640..10147b9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -19,6 +19,13 @@
pkgs.cairo
pkgs.openscad
pkgs.python3
+ pkgs.nodejs
+
+ # openscad-wasm build dependencies
+ pkgs.gnumake
+ pkgs.git
+ pkgs.wget
+ pkgs.deno
];
nativeBuildInputs = with pkgs; [
diff --git a/webapp/app.ts b/webapp/app.ts
new file mode 100644
index 0000000..48201a0
--- /dev/null
+++ b/webapp/app.ts
@@ -0,0 +1,1126 @@
+import * as THREE from "three";
+import { STLLoader } from "three/addons/loaders/STLLoader.js";
+import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
+import { OrbitControls } from "three/addons/controls/OrbitControls.js";
+import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
+import { VertexPrintOutputs, VertexPrintParams, vertexPrint } from "./core"
+import Matrix from "ml-matrix";
+import JSZip from "jszip";
+
+const DATA_DIR = "data/";
+const DEFAULT_MODEL = "DisdyakisTriacontahedron.obj";
+
+const V_FG = cssVar("--color-v-fg");
+const V_BG = cssVar("--color-v-bg");
+const V_DARK = cssVar("--color-v-dark");
+const V_BORDER = cssVar("--color-v-border");
+const V_BLUE = cssVar("--color-v-blue");
+
+const PARAMS = new VertexPrintParams();
+
+// utils ---
+function clamp(v: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, v));
+}
+
+function cssVar(name: string): number {
+ const hex = getComputedStyle(document.documentElement)
+ .getPropertyValue(name)
+ .trim();
+ return parseInt(hex.replace("#", ""), 16);
+}
+
+function makeOrientationCube(renderer: THREE.WebGLRenderer):
+ [THREE.Mesh, THREE.Scene, THREE.OrthographicCamera] {
+ const scene = new THREE.Scene();
+ const cam = new THREE.OrthographicCamera();
+ cam.position.set(0, 0, 1);
+
+ const FACE_COLOR = '#' + V_BORDER.toString(16).padStart(6, '0');
+ const TEXT_COLOR = '#' + V_FG.toString(16).padStart(6, '0');
+ const TEXTURE_SIZE = 256;
+
+ const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
+ function makeFaceLabel(text: string): THREE.CanvasTexture {
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = TEXTURE_SIZE;
+ const context = canvas.getContext("2d")!;
+ context.fillStyle = FACE_COLOR;
+ context.fillRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
+ context.fillStyle = TEXT_COLOR;
+ context.font = `48px monospace`;
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+ context.fillText(text, TEXTURE_SIZE / 2, TEXTURE_SIZE / 2);
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.colorSpace = THREE.SRGBColorSpace;
+ texture.anisotropy = maxAnisotropy;
+ return texture;
+ }
+
+ const faceLabels: string[] = [
+ "Right", // +x
+ "Left", // -x
+ "Top", // +y
+ "Bottom", // -y
+ "Front", // +z
+ "Back" // -z
+ ];
+
+ const cubeMaterials = faceLabels.map(label => {
+ const texture = makeFaceLabel(label);
+ return new THREE.MeshPhongMaterial({
+ map: texture,
+ });
+ });
+
+ const cube = new THREE.Mesh(
+ new THREE.BoxGeometry(0.75, 0.75, 0.75),
+ cubeMaterials
+ );
+ scene.add(cube);
+ scene.add(new THREE.AmbientLight(0xffffff, 1));
+
+ const directionalLight = new THREE.DirectionalLight(V_FG, 2);
+ directionalLight.position.set(1, 1, 1);
+ scene.add(directionalLight);
+
+ return [cube, scene, cam]
+}
+
+// Canvas logic. Placeholder. ---
+
+class Canvas {
+ scene: THREE.Scene;
+ camera: THREE.OrthographicCamera;
+ controls: OrbitControls;
+ root: THREE.Group;
+ viewHalfHeight: number;
+
+ currentMesh: THREE.Mesh | null;
+ currentName: string;
+ currentData: ArrayBuffer;
+
+ currentMeshes: THREE.Mesh[];
+ vertexCount: number;
+ vertexMaterial: THREE.MeshStandardMaterial;
+ edgeMaterial: THREE.MeshStandardMaterial;
+ meshMaterial: THREE.MeshStandardMaterial;
+ highlightMaterial: THREE.MeshStandardMaterial;
+ highlighted: number[];
+
+ constructor() {
+ this.currentMesh = null;
+ this.currentMeshes = [];
+ this.vertexMaterial = new THREE.MeshStandardMaterial({
+ color: V_BLUE,
+ flatShading: false,
+ });
+ this.edgeMaterial = new THREE.MeshStandardMaterial({
+ color: 0xffffff,
+ flatShading: false,
+ });
+ this.meshMaterial = new THREE.MeshStandardMaterial({
+ color: V_BLUE,
+ flatShading: false,
+ });
+ this.highlightMaterial = new THREE.MeshStandardMaterial({
+ color: 0xff0000,
+ flatShading: false,
+ });
+ this.currentName = DEFAULT_MODEL;
+ this.currentData = new ArrayBuffer(0);
+ this.highlighted = [];
+ this.vertexCount = 0;
+
+ this.initCanvas();
+ }
+
+ async initCanvas() {
+ this.scene = new THREE.Scene();
+ this.scene.background = new THREE.Color(V_BG);
+
+ this.root = new THREE.Group();
+ this.root.rotation.x = -Math.PI / 2;
+ this.scene.add(this.root);
+
+ this.camera = new THREE.OrthographicCamera();
+ this.camera.position.set(20, 15, 20);
+ this.camera.near = 0.1;
+ this.camera.far = 1000;
+ this.viewHalfHeight = 15;
+ this.updateFrustum();
+
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.domElement.style.position = "fixed";
+ renderer.domElement.style.top = "0";
+ renderer.domElement.style.left = "0";
+ renderer.setPixelRatio(window.devicePixelRatio);
+ document.body.prepend(renderer.domElement);
+
+ this.controls = new OrbitControls(this.camera, renderer.domElement);
+ this.controls.update();
+
+ this.scene.add(new THREE.HemisphereLight(V_FG, V_BG, 1));
+
+ const keyLight = new THREE.DirectionalLight(V_FG, 2);
+ this.camera.add(keyLight);
+ this.camera.add(keyLight.target);
+ keyLight.position.set(0, 0, 0);
+ keyLight.target.position.set(0, 0, -1);
+
+ const fillLight = new THREE.DirectionalLight(V_FG, 0.8);
+ this.camera.add(fillLight);
+ this.camera.add(fillLight.target);
+ fillLight.position.set(1, -1, 0);
+ fillLight.target.position.set(0, 0, -1);
+
+ this.scene.add(this.camera);
+
+ const mesh = new THREE.Mesh(new THREE.BufferGeometry(), this.meshMaterial);
+ this.root.add(mesh);
+ this.currentMesh = mesh;
+
+ const [cube, cubeScene, cubeCamera] = makeOrientationCube(renderer);
+
+ const ORENT_CUBE_SIZE = 240;
+ renderer.autoClear = false;
+ renderer.setScissorTest(true);
+
+ const animate = () => {
+ requestAnimationFrame(animate);
+ this.controls.update();
+
+ const W = window.innerWidth;
+ const H = window.innerHeight;
+
+ // Full-screen main scene
+ renderer.setViewport(0, 0, W, H);
+ renderer.setScissor(0, 0, W, H);
+ renderer.clear();
+ renderer.render(this.scene, this.camera);
+
+ // Counter-rotate the orientation cube so its faces track the camera's
+ // orientation (ie the cube shows where the camera looks).
+ cube.quaternion.copy(this.camera.quaternion).invert();
+
+ const gx = W - ORENT_CUBE_SIZE; // right edge
+ const gy = 0; // bottom edge (GL y is measured from bottom)
+ renderer.setViewport(gx, gy, ORENT_CUBE_SIZE, ORENT_CUBE_SIZE);
+ renderer.setScissor(gx, gy, ORENT_CUBE_SIZE, ORENT_CUBE_SIZE);
+ renderer.clearDepth();
+ renderer.render(cubeScene, cubeCamera);
+ };
+ animate();
+
+ window.addEventListener("resize", () => {
+ this.updateFrustum();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ });
+ }
+
+ // Recompute the orthographic view frustum from the current half-height
+ // and window aspect ratio.
+ updateFrustum() {
+ const aspect = window.innerWidth / window.innerHeight;
+ const h = this.viewHalfHeight;
+ const w = h * aspect;
+ this.camera.left = -w;
+ this.camera.right = w;
+ this.camera.top = h;
+ this.camera.bottom = -h;
+ this.camera.updateProjectionMatrix();
+ }
+
+ // Reposition camera so every mesh currently in the scene is in frame,
+ // preserving camera orientation.
+ fitView() {
+ const box = new THREE.Box3().setFromObject(this.root);
+ if (box.isEmpty()) return;
+ const sphere = new THREE.Sphere();
+ box.getBoundingSphere(sphere);
+ const radius = sphere.radius || 1;
+ const center = sphere.center;
+
+ const dist = radius * 3;
+
+ const dir = new THREE.Vector3()
+ .copy(this.camera.position)
+ .sub(this.controls.target)
+ .normalize();
+ if (dir.lengthSq() === 0) dir.set(1, 0.75, 1).normalize();
+ this.camera.position.copy(center).add(dir.multiplyScalar(dist));
+ this.controls.target.copy(center);
+ this.controls.update();
+
+ const aspect = window.innerWidth / window.innerHeight;
+ this.viewHalfHeight = (radius / Math.min(1, aspect)) * 1.25;
+
+ this.camera.near = Math.max(0.1, radius / 100);
+ this.camera.far = dist + radius * 10;
+ this.updateFrustum();
+ }
+
+ // Load and display geometry. The file extension determines how `data` is
+ // parsed.
+ loadGeometry(name: string, data: ArrayBuffer) {
+ if (!this.currentMesh) return;
+ this.clear();
+ this.currentName = name;
+ this.currentData = data;
+ let geometry: THREE.BufferGeometry;
+ try {
+ const isObj = name.toLowerCase().endsWith(".obj");
+ geometry = isObj
+ ? geometryFromObj(new TextDecoder().decode(data))
+ : new STLLoader().parse(data);
+ } catch (e) {
+ // TODO: show user an error message
+ console.error("Failed to load", name, e);
+ return;
+ }
+
+ const count = geometry.attributes.position?.count ?? 0;
+ if (count === 0) {
+ console.error("Failed to load", name, "malformed file: no geometry parsed");
+ return;
+ }
+
+ geometry.center();
+ geometry.computeVertexNormals();
+ this.currentMesh.geometry = geometry;
+
+ disableDownloadButton();
+ resetInspector();
+ this.fitView();
+ }
+
+ loadVertexPrintOutputs(outputs: VertexPrintOutputs) {
+ this.clear()
+ const polyhedron = outputs.polyhedron;
+ this.vertexCount = polyhedron.vertexFigures.length;
+ for (let i = 0; i < polyhedron.vertexFigures.length; ++i) {
+ const geometry = new STLLoader().parse(outputs.stls[i]);
+ geometry.computeVertexNormals();
+ const mesh = new THREE.Mesh(geometry, this.vertexMaterial);
+ const position = polyhedron.vertices.getRow(i);
+ const rotation = polyhedron.vertexFigures[i].euler;
+ const x = PARAMS.scale * position[0];
+ const y = PARAMS.scale * position[1];
+ const z = PARAMS.scale * position[2];
+ mesh.position.set(x, y, z);
+ mesh.rotation.set(rotation[0], rotation[1], rotation[2], "ZYX");
+ this.root.add(mesh);
+ this.currentMeshes.push(mesh)
+ }
+
+ this.loadEdges(outputs)
+
+ this.fitView();
+ }
+
+ // Draw edges. Identical to solids.scad/hedron_edges
+ loadEdges(outputs: VertexPrintOutputs) {
+ const polyhedron = outputs.polyhedron;
+ const radius = polyhedron.options.edgeDiameter / 2;
+ for (const [key, value] of polyhedron.edges) {
+ const [v1, v2] = key.split(",").map(Number);
+ const a = polyhedron.vertices.getRowVector(v1);
+ const b = polyhedron.vertices.getRowVector(v2);
+ const [o1, o2] = value.offsets;
+
+ // v = b - a, as in the scad prototype
+ const v = Matrix.sub(b, a);
+ const dist = v.norm();
+ const length = PARAMS.scale * dist - o1 - o2;
+
+ if (length <= 0) {
+ continue;
+ }
+
+ const vx = v.get(0, 0);
+ const vy = v.get(0, 1);
+ const vz = v.get(0, 2);
+
+ const phi = Math.atan2(vy, vx);
+ const theta = Math.acos(vz / dist);
+
+ const geometry = new THREE.CylinderGeometry(radius, radius, length, 16);
+ geometry.translate(0, length / 2, 0);
+ geometry.rotateX(Math.PI / 2);
+
+ const mesh = new THREE.Mesh(geometry, this.edgeMaterial);
+
+ // Position at the scaled vertex a, inset by o1 along the edge
+ const ux = vx / dist;
+ const uy = vy / dist;
+ const uz = vz / dist;
+
+ mesh.position.set(
+ PARAMS.scale * a.get(0, 0) + o1 * ux,
+ PARAMS.scale * a.get(0, 1) + o1 * uy,
+ PARAMS.scale * a.get(0, 2) + o1 * uz,
+ );
+
+ mesh.rotation.set(0, theta, phi, "ZYX");
+
+ this.root.add(mesh);
+ this.currentMeshes.push(mesh);
+ }
+ }
+
+ // Clear scene of all meshes
+ clear() {
+ for (const mesh of this.currentMeshes) {
+ mesh.geometry.dispose();
+ this.root.remove(mesh);
+ }
+ this.currentMeshes = [];
+ if (this.currentMesh) {
+ this.currentMesh.geometry.dispose();
+ this.currentMesh.geometry = new THREE.BufferGeometry();
+ }
+ this.highlighted = [];
+ this.vertexCount = 0;
+ }
+
+ highlightVertex(index: number) {
+ this.currentMeshes[index].material = this.highlightMaterial;
+ this.highlighted.push(index)
+ }
+
+ highlightEdge(index: number) {
+ const meshIndex = this.vertexCount + index;
+ this.currentMeshes[meshIndex].material = this.highlightMaterial;
+ this.highlighted.push(meshIndex)
+ }
+
+ clearHighlights() {
+ for (const v of this.highlighted) {
+ const mat = v < this.vertexCount ? this.vertexMaterial : this.edgeMaterial;
+ this.currentMeshes[v].material = mat;
+ }
+ this.highlighted = [];
+ }
+}
+
+// Merge every mesh geometry contained in an OBJLoader-produced group.
+function geometryFromObj(text: string): THREE.BufferGeometry {
+ const group = new OBJLoader().parse(text);
+ const geometries: THREE.BufferGeometry[] = [];
+ group.traverse((o: THREE.Object3D) => {
+ const mesh = o as THREE.Mesh;
+ if (mesh.isMesh) geometries.push(mesh.geometry);
+ });
+ if (geometries.length === 0) return new THREE.BufferGeometry();
+ if (geometries.length === 1) return geometries[0];
+ return mergeGeometries(geometries, false) ?? geometries[0];
+}
+
+// Runtime page construction ---
+
+type NumberKeys = {
+ [K in keyof VertexPrintParams]: VertexPrintParams[K] extends number ? K : never
+}[keyof VertexPrintParams];
+type StringKeys = {
+ [K in keyof VertexPrintParams]: VertexPrintParams[K] extends string ? K : never
+}[keyof VertexPrintParams];
+
+type SliderParam = {
+ kind: 'slider';
+ name: NumberKeys;
+ label: string;
+ desc: string;
+ min: number;
+ max: number;
+ step: number;
+ value: number;
+ unit: string;
+};
+
+type SelectParam = {
+ kind: 'select';
+ name: StringKeys;
+ label: string;
+ desc: string;
+ value: string;
+ options: { value: string; label: string }[];
+ reveal?: (v: string) => string | null;
+};
+type Param = SliderParam | SelectParam;
+
+const OPTIONS: Param[] = [
+ {
+ kind: 'slider',
+ name: 'edgeDiameter',
+ label: 'Rod diameter',
+ desc: 'Diameter of your dowel rods.',
+ min: 0,
+ max: 100,
+ step: 0.05,
+ value: 3.0,
+ unit: 'mm',
+ },
+ {
+ kind: 'slider',
+ name: 'diameterTolerance',
+ label: 'Diameter tolerance',
+ desc: 'Additional tolerance added to the diameter.\nAdd about 12% of your diameter for wood dowel rods, and 5% of your diameter for metal dowel rods',
+ min: 0,
+ max: 1,
+ step: 0.01,
+ value: 0.35,
+ unit: 'mm',
+ },
+ {
+ kind: 'slider',
+ name: 'diameterTaper',
+ label: 'Diameter taper',
+ desc: 'The rod holder diameter decreases by this amount. A small taper is helpful to account for unevenness in the diameters of your dowel rods, particularly for wood dowel rods',
+ min: 0,
+ max: 1,
+ step: 0.01,
+ value: 0.1,
+ unit: 'mm',
+ },
+ {
+ kind: 'slider',
+ name: 'wallThickness',
+ label: 'Wall thickness',
+ desc: 'Thickness of the tube walls.',
+ min: 0,
+ max: 40,
+ step: 0.01,
+ value: 1.2,
+ unit: 'mm',
+ },
+ {
+ kind: 'slider',
+ name: 'scale',
+ label: 'Scale',
+ desc: 'Scale factor. Vertexprinted objects are typically larger than than the original object. Objects that are too small risk having their vertex pieces collide.',
+ min: 0,
+ max: 100,
+ step: 0.01,
+ value: 1.00,
+ unit: 'x',
+ },
+ {
+ kind: 'slider',
+ name: 'rodInset',
+ label: 'Tube length',
+ desc: 'Length of tube enveloping dowel rods.',
+ min: 0,
+ max: 100,
+ step: 0.1,
+ value: 6,
+ unit: 'mm',
+ },
+ {
+ kind: 'slider',
+ name: 'minPrinterOverhangAngle',
+ label: 'Overhang angle',
+ desc: 'The angle by which your vertex pieces can overhang. At 0°, any overhang angle is permitted. At 90°, vertexprint minimizes overhangs.',
+ min: 0,
+ max: 90,
+ step: 5,
+ value: 45,
+ unit: '°',
+ },
+ {
+ kind: 'select',
+ name: 'offsetType',
+ label: 'Offset type',
+ desc: 'Dowel rods are offset from the center of each vertex to avoid them from colliding with each other within a vertex piece.\n\
+ "Auto (per-edge)" allows a different offset for each edge at a given vertex. It is the most space-efficient.\n\
+ "Auto (per-vertex)" forces the same offset at each vertex, but allows different vertices to have different offsets.\n\
+ "Auto (global)" forces the entire solid to share an offset value.\n\
+ "Manual" allows you to select a global offset value.\n',
+ value: 'auto_per_edge',
+ options: [
+ { value: 'fixed', label: 'Manual' },
+ { value: 'auto_global', label: 'Auto (global)' },
+ { value: 'auto_per_vertex', label: 'Auto (per-vertex)' },
+ { value: 'auto_per_edge', label: 'Auto (per-edge)' },
+ ],
+ reveal: (v) => v === 'fixed' ? 'manualOffset' : null,
+ },
+ {
+ kind: 'slider',
+ name: 'manualOffset',
+ label: 'Offset',
+ desc: 'Manually set a global offset value.', // see comment above
+ min: 0,
+ max: 100,
+ step: 0.01,
+ value: 0,
+ unit: 'mm',
+ },
+ {
+ kind: 'select',
+ name: 'renderQuality',
+ label: 'Render quality',
+ desc: 'Quality of part. "Preview" renderes with fewer triangles. Always render with "Final" before printing.',
+ value: 'preview',
+ options: [
+ { value: 'preview', label: 'Preview' },
+ { value: 'final', label: 'Final' },
+ ],
+ },
+];
+
+// Build and position a tooltip element for `host`, returning it.
+function makeTooltip(host: HTMLElement, text: string): HTMLDivElement {
+ const tip = document.createElement('div');
+ tip.className = 'dh-tooltip';
+ const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
+ tip.innerHTML = lines.map(l => `
${l}
`).join('');
+ document.body.appendChild(tip);
+
+ const r = host.getBoundingClientRect();
+ const tw = tip.offsetWidth;
+ const th = tip.offsetHeight;
+ const gap = 6;
+
+ // Place tooltip below the host element. Place it above if it overflows the
+ // viewport.
+ let top = r.bottom + gap;
+ if (top + th > window.innerHeight) top = r.top - th - gap;
+ if (top < 0) top = gap;
+
+ // Left-align the tooltip with the host. Clamp to viewport if this is not possible.
+ let left = r.left;
+ if (left + tw > window.innerWidth - gap) left = window.innerWidth - tw - gap;
+ if (left < gap) left = gap;
+ tip.style.top = `${top}px`;
+ tip.style.left = `${left}px`;
+ return tip;
+}
+
+// Bind a tooltip to a html element
+function bindTooltip(element: HTMLElement, text: string) {
+ let tip: HTMLDivElement | null = null;
+ let timer: number | null = null;
+ const title = element.getAttribute('title');
+ const show = () => {
+ if (tip || timer) return;
+ // strip title while tooltip shows, keep title otherwise for screen
+ // readers
+ if (title !== null) element.removeAttribute('title');
+ // 1s delay before tooltip shows
+ timer = window.setTimeout(() => {
+ timer = null;
+ tip = makeTooltip(element, text);
+ }, 600);
+ };
+ const hide = () => {
+ if (timer !== null) {
+ window.clearTimeout(timer); timer = null;
+ }
+ if (title !== null) element.setAttribute('title', title);
+ if (tip) {
+ tip.remove(); tip = null;
+ }
+ };
+ element.addEventListener('mouseenter', show);
+ element.addEventListener('focus', show);
+ element.addEventListener('mouseleave', hide);
+ element.addEventListener('blur', hide);
+ element.addEventListener('keydown', (e) => { if (e.key === 'Escape') hide(); });
+}
+
+const TW_CLASS = {
+ row: 'flex flex-col gap-0.5 px-1.5 py-1',
+ top: 'flex items-center justify-between gap-1.5',
+ label: 'opt-label font-mono text-[11px] text-v-fg truncate',
+ num: 'w-16 min-w-16 font-mono text-[11px] leading-none text-v-fg bg-black border border-v-border py-0.5 pl-1 pr-5 text-left appearance-none focus:outline-none focus:border-v-blue',
+ unit: 'pointer-events-none absolute right-1 inset-y-0 flex items-center translate-y-px font-mono text-[11px] leading-none text-v-fg/70',
+ slider: 'dh-slider w-full h-3.5 cursor-pointer appearance-none',
+ select: 'w-full font-mono text-[11px] text-v-fg bg-black border border-v-border px-1 py-0.5 focus:outline-none focus:border-v-blue',
+};
+
+
+type SliderRow = {
+ row: HTMLElement;
+ slider: HTMLInputElement;
+ num: HTMLInputElement;
+ param: SliderParam
+};
+
+type SelectRow = {
+ row: HTMLElement;
+ sel: HTMLSelectElement;
+ param: SelectParam
+};
+
+class Sidebar {
+ rows: Map;
+
+ constructor() {
+ this.rows = new Map();
+ this.initSidebar();
+ }
+
+ makeSlider(param: SliderParam): HTMLElement {
+ const row = document.createElement('div');
+ row.className = TW_CLASS.row;
+ row.dataset.param = param.name;
+ row.innerHTML =
+ ``
+ + ``;
+ bindTooltip(row.querySelector('.opt-label')!, param.desc);
+ return row
+ }
+
+ makeSelect(param: SelectParam): HTMLElement {
+ const row = document.createElement('div');
+ row.className = TW_CLASS.row;
+ row.dataset.param = param.name;
+ const paramsHTML = param.options.map(o =>
+ ``).join('');
+ row.innerHTML =
+ `${param.label}
`
+ + ``;
+ bindTooltip(row.querySelector('.opt-label')!, param.desc);
+ return row
+ }
+
+ enforceSelects() {
+ for (const param of OPTIONS) {
+ if (param.kind === 'select' && param.reveal) {
+ const currentVal = String(PARAMS[param.name]);
+ const shown = param.reveal(currentVal);
+ if (shown)
+ this.rows.get(shown)!.style.display = '';
+ for (const o of param.options) {
+ const r = param.reveal(o.value);
+ if (r && o.value !== currentVal)
+ this.rows.get(r)!.style.display = 'none';
+ }
+ }
+ }
+ }
+
+ // Programmatically set a param value and update the corresponding UI
+ // element.
+ setParam(name: NumberKeys | StringKeys, value: number | string) {
+ const row = this.rows.get(name as string);
+ if (!row) return;
+ const param = OPTIONS.find(p => p.name === name);
+ if (!param) return;
+ if (param.kind === 'slider') {
+ const slider = row.querySelector('.dh-slider') as HTMLInputElement;
+ const num = row.querySelector('input[type="number"]') as HTMLInputElement;
+ const v = clamp(value as number, param.min, param.max);
+ slider.value = String(v);
+ num.value = String(v);
+ PARAMS[param.name] = v;
+ const pct = (v - param.min) / (param.max - param.min) * 100;
+ slider.style.setProperty('--fill', `${pct}%`);
+ } else {
+ const sel = row.querySelector('select') as HTMLSelectElement;
+ sel.value = String(value);
+ PARAMS[param.name] = value as any;
+ this.enforceSelects();
+ }
+ }
+
+ // Synchronize slider with corresponding num entry and vv
+ syncSlider(r: SliderRow) {
+ const setFill = () => {
+ const pct = (clamp(r.slider.valueAsNumber, r.param.min, r.param.max) - r.param.min)
+ / (r.param.max - r.param.min) * 100;
+ r.slider.style.setProperty('--fill', `${pct}%`);
+ };
+ const sync = (src: 'slider' | 'num') => {
+ const raw = parseFloat(src === 'slider' ? r.slider.value : r.num.value);
+ const v = clamp(isNaN(raw) ? 0 : raw, r.param.min, r.param.max);
+ r.slider.value = String(v);
+ r.num.value = String(v);
+ PARAMS[r.param.name] = v;
+ setFill();
+ };
+ r.slider.addEventListener('input', () => sync('slider'));
+ r.num.addEventListener('input', () => sync('num'));
+ r.num.addEventListener('change', () => sync('num'));
+ setFill();
+ }
+
+ syncSelect(r: SelectRow) {
+ r.sel.addEventListener('change', () => {
+ PARAMS[r.param.name] = r.sel.value;
+ this.enforceSelects();
+ });
+ }
+
+ initSidebar() {
+ const sidebar = document.getElementById('sidebar')!;
+ // Initialize sidebar input fields
+ const opts = document.getElementById('sidebar-opts')!;
+ for (const param of OPTIONS) {
+ let row: HTMLElement;
+ if (param.kind === 'slider') {
+ PARAMS[param.name] = param.value;
+ row = this.makeSlider(param);
+ this.syncSlider({
+ row,
+ slider: row.querySelector('.dh-slider')!,
+ num: row.querySelector('input[type="number"]')!,
+ param
+ });
+ } else {
+ PARAMS[param.name] = param.value;
+ row = this.makeSelect(param);
+ this.syncSelect({
+ row,
+ sel: row.querySelector('select')!,
+ param
+ });
+ }
+ this.rows.set(param.name, row);
+ opts.appendChild(row);
+ }
+ this.enforceSelects();
+
+ // Open/close sidebar
+ const sidebar_reopen = document.getElementById('sidebar-reopen')!;
+ const sidebar_close = document.getElementById('sidebar-close')!;
+ bindTooltip(sidebar_close, 'Close sidebar');
+ bindTooltip(sidebar_reopen, 'Show sidebar');
+ sidebar_close.addEventListener('click', () => {
+ sidebar.style.display = 'none';
+ sidebar_reopen.style.display = 'flex';
+ });
+ sidebar_reopen.addEventListener('click', () => {
+ sidebar.style.display = '';
+ sidebar_reopen.style.display = 'none';
+ });
+
+ // Resize sidebar
+ const sidebar_resizer = document.getElementById('sidebar-resizer')!;
+ let dragging = false, startX = 0, startW = 0;
+ sidebar_resizer.addEventListener('mousedown', (e) => {
+ dragging = true;
+ sidebar_resizer.classList.add('dragging');
+ startX = e.clientX;
+ startW = sidebar.offsetWidth;
+ document.body.style.userSelect = 'none';
+ e.preventDefault();
+ });
+ window.addEventListener('mousemove', (e) => {
+ if (!dragging) return;
+ const w = clamp(startW + (e.clientX - startX), 160, window.innerWidth * 0.5);
+ sidebar.style.width = w + 'px';
+ sidebar.style.maxWidth = 'none';
+ });
+ window.addEventListener('mouseup', () => {
+ dragging = false;
+ sidebar_resizer.classList.remove('dragging');
+ document.body.style.userSelect = '';
+ });
+ }
+}
+
+
+function initInspector() {
+ const inspector = document.getElementById('inspector')!;
+
+ // Switch tabs in inspector panel
+ document.querySelectorAll('#inspector-tabs .inspector-tab').forEach((tab) => {
+ tab.addEventListener('click', () => {
+ const name = tab.dataset.tab;
+ document.querySelectorAll('#inspector-tabs .inspector-tab').forEach((t) => {
+ const active = t.dataset.tab === name;
+ t.classList.toggle('text-v-blue', active);
+ t.classList.toggle('border-v-blue', active);
+ t.classList.toggle('bg-v-panel', active);
+ t.classList.toggle('text-v-fg', !active);
+ t.classList.toggle('border-transparent', !active);
+ });
+ document.querySelectorAll('.inspector-pane').forEach((p) => {
+ p.style.display = p.dataset.pane === name ? '' : 'none';
+ });
+ });
+ });
+
+
+ // Open/close inspector
+ const inspector_reopen = document.getElementById('inspector-reopen')!;
+ const inspector_close = document.getElementById('inspector-close')!;
+ bindTooltip(inspector_close, 'Close inspector');
+ bindTooltip(inspector_reopen, 'Show inspector');
+ inspector_close.addEventListener('click', () => {
+ inspector.style.display = 'none';
+ inspector_reopen.style.display = 'flex';
+ });
+
+ inspector_reopen.addEventListener('click', () => {
+ inspector.style.display = '';
+ inspector_reopen.style.display = 'none';
+ });
+}
+
+// Reset the inspector panes
+function resetInspector() {
+ const msg = 'This pane will populate after you generate artifacts';
+ document.querySelectorAll('.inspector-pane').forEach((p) => {
+ p.innerHTML = `${msg}
`;
+ });
+}
+
+// Fill the vertex inspector with vertex-edge adjacencies
+// Fill the edge inspector with edge-vertex adjacencies, and offset-adjusted edge length
+function populateInspector(outputs: VertexPrintOutputs, canvas: Canvas) {
+ const polyhedron = outputs.polyhedron;
+ const verticesPane = document.querySelector('.inspector-pane[data-pane="vertices"]')!;
+ const edgesPane = document.querySelector('.inspector-pane[data-pane="edges"]')!;
+
+ const vertexCount = polyhedron.vertexFigures.length;
+ let vhtml = `${vertexCount} vertices
`;
+ vhtml += `index, edges
`;
+ for (const vf of polyhedron.vertexFigures) {
+ vhtml += `v${vf.vertexIndex} [${vf.edges.join(", ")}]
`;
+ }
+ verticesPane.innerHTML = vhtml;
+
+ for (let i = 0; i < polyhedron.vertexFigures.length; ++i) {
+ const vi = document.getElementById(`v${i}`);
+ vi.addEventListener('click', () => {
+ canvas.highlightVertex(i);
+ });
+ }
+
+ const edges = [...polyhedron.edges.entries()]
+ .map(([key, field]) => {
+ const [v1, v2] = key.split(",").map(Number);
+ return { name: field.name, v1, v2, offsetLength: field.offsetLength };
+ })
+ .sort((a, b) => a.name - b.name);
+
+ let ehtml = `${edges.length} edges
`;
+ ehtml += `index, vertices, length (mm)
`;
+ for (const e of edges) {
+ ehtml += `e${e.name} [${e.v1} ${e.v2}] ${e.offsetLength.toFixed(2)}
`;
+ }
+ edgesPane.innerHTML = ehtml;
+
+ for (let i = 0; i < edges.length; ++i) {
+ const vi = document.getElementById(`e${i}`);
+ vi.addEventListener('click', () => {
+ canvas.highlightEdge(i);
+ });
+ }
+
+ document.addEventListener('click', (e) => {
+ const t = e.target as Node;
+ if (!verticesPane.contains(t) && !edgesPane.contains(t)) {
+ canvas.clearHighlights();
+ }
+ });
+}
+
+const PRESETS: Record> = {
+ 'Platonic solids': {
+ 'Tetrahedron': { file: 'Tetrahedron.obj', scale: 50 },
+ 'Cube': { file: 'Cube.obj', scale: 50 },
+ 'Octahedron': { file: 'Octahedron.obj', scale: 50 },
+ 'Dodecahedron': { file: 'Dodecahedron.obj', scale: 50 },
+ 'Icosahedron': { file: 'Icosahedron.obj', scale: 50 },
+ },
+ 'Archimedean solids': {
+ 'Truncated Tetrahedron': { file: 'TruncatedTetrahedron.obj', scale: 50 },
+ 'Cuboctahedron': { file: 'Cuboctahedron.obj', scale: 50 },
+ 'Truncated Cube': { file: 'TruncatedCube.obj', scale: 50 },
+ 'Truncated Octahedron': { file: 'TruncatedOctahedron.obj', scale: 50 },
+ 'Rhombicuboctahedron': { file: 'Rhombicuboctahedron.obj', scale: 50 },
+ 'Truncated Cuboctahedron': { file: 'TruncatedCuboctahedron.obj', scale: 50 },
+ 'Snub Cube (laevo)': { file: 'LsnubCube.obj', scale: 50 },
+ 'Icosidodecahedron': { file: 'Icosidodecahedron.obj', scale: 50 },
+ 'Truncated Dodecahedron': { file: 'TruncatedDodecahedron.obj', scale: 50 },
+ 'Truncated Icosahedron': { file: 'TruncatedIcosahedron.obj', scale: 50 },
+ 'Rhombicosidodecahedron': { file: 'Rhombicosidodecahedron.obj', scale: 50 },
+ 'Truncated Icosidodecahedron': { file: 'TruncatedIcosidodecahedron.obj', scale: 50 },
+ 'Snub Dodecahedron (laevo)': { file: 'LsnubDodecahedron.obj', scale: 50 },
+ },
+ 'Catalan solids': {
+ 'Triakis Tetrahedron': { file: 'TriakisTetrahedron.obj', scale: 50 },
+ 'Rhombic Dodecahedron': { file: 'RhombicDodecahedron.obj', scale: 50 },
+ 'Triakis Octahedron': { file: 'TriakisOctahedron.obj', scale: 50 },
+ 'Tetrakis Hexahedron': { file: 'TetrakisHexahedron.obj', scale: 50 },
+ 'Deltoidal Icositetrahedron': { file: 'DeltoidalIcositetrahedron.obj', scale: 50 },
+ 'Disdyakis Dodecahedron': { file: 'DisdyakisDodecahedron.obj', scale: 50 },
+ 'Pentagonal Icositetrahedron (laevo)': { file: 'LpentagonalIcositetrahedron.obj', scale: 50 },
+ 'Rhombic Triacontahedron': { file: 'RhombicTriacontahedron.obj', scale: 50 },
+ 'Triakis Icosahedron': { file: 'TriakisIcosahedron.obj', scale: 50 },
+ 'Pentakis Dodecahedron': { file: 'PentakisDodecahedron.obj', scale: 50 },
+ 'Deltoidal Hexecontahedron': { file: 'DeltoidalHexecontahedron.obj', scale: 50 },
+ 'Disdyakis Triacontahedron': { file: 'DisdyakisTriacontahedron.obj', scale: 50 },
+ 'Pentagonal Hexecontahedron (laevo)': { file: 'LpentagonalHexecontahedron.obj', scale: 50 },
+ },
+ 'Misc': {
+ 'Stanford Bunny': { file: 'bunny.stl', scale: 6 },
+ 'Rhombic Dodecahedron Tiling': { file: '13RhombicDodecahedra.obj', scale: 50 },
+ },
+};
+
+function initPresetsMenu(canvas: Canvas, sidebar: Sidebar) {
+ const presets_button = document.getElementById('presets-button')!;
+ const presets_menu = document.getElementById('presets-menu')!;
+ bindTooltip(presets_button, 'Open presets');
+
+ for (const [category, solids] of Object.entries(PRESETS)) {
+ const header = document.createElement('p');
+ header.textContent = category;
+ header.className = 'font-mono text-xs px-3 pt-2 pb-1 text-v-fg/60 border-b border-v-border min-w-full';
+ presets_menu.appendChild(header);
+ for (const [label, preset] of Object.entries(solids)) {
+ const { file, scale } = preset;
+ const item = document.createElement('button');
+ item.type = 'button';
+ item.textContent = label;
+ item.dataset.file = file;
+ item.className = 'block w-full cursor-pointer text-left font-mono text-xs px-3 py-1 text-v-fg hover:bg-v-blue hover:text-v-dark';
+ item.addEventListener('click', async () => {
+ sidebar.setParam('scale', scale);
+ const data = await fetch(DATA_DIR + file).then((r) => r.arrayBuffer())
+ canvas.loadGeometry(file, data);
+ });
+ presets_menu.appendChild(item);
+ }
+ }
+
+ presets_button.addEventListener('click', (e) => {
+ e.stopPropagation();
+ presets_menu.classList.toggle('hidden');
+ });
+
+ // Close the menu when clicking outside it or selecting an item.
+ document.addEventListener('click', (e) => {
+ if (!presets_menu.contains(e.target as Node) && e.target !== presets_button) {
+ presets_menu.classList.add('hidden');
+ }
+ });
+ presets_menu.addEventListener('click', (e) => {
+ const target = e.target as HTMLElement;
+ if (target.tagName === 'BUTTON') {
+ presets_menu.classList.add('hidden');
+ }
+ });
+}
+
+function initUploadButton(canvas: Canvas) {
+ const button = document.getElementById('upload-button')!;
+ bindTooltip(button, 'Upload files');
+ button.addEventListener('click', () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.stl,.obj';
+ input.addEventListener('change', async () => {
+ const file = input.files?.[0];
+ if (!file) return;
+ const data = await file.arrayBuffer();
+ const name = file.name;
+ void canvas.loadGeometry(name, data);
+ });
+ input.click();
+ });
+}
+
+
+function initConstructButton(canvas: Canvas) {
+ const button = document.getElementById('construct-button')!;
+ bindTooltip(button, 'Vertexprint your structure');
+ const label = button.querySelector('p')!;
+ const IDLE_TEXT = 'Construct';
+
+ button.addEventListener('click', async () => {
+ if (canvas.currentData.byteLength === 0) {
+ console.error('No model loaded to construct from');
+ return;
+ }
+
+ button.classList.add('opacity-70', 'pointer-events-none');
+ let dots = 0;
+ label.textContent = 'Generating';
+ const timer = window.setInterval(() => {
+ dots = (dots + 1) % 4;
+ label.textContent = 'Generating' + '.'.repeat(dots);
+ }, 400);
+
+ const outputs = await vertexPrint(canvas.currentName, canvas.currentData, PARAMS);
+ canvas.loadVertexPrintOutputs(outputs);
+ canvas.loadEdges(outputs);
+ window.clearInterval(timer);
+ label.textContent = IDLE_TEXT;
+ button.classList.remove('opacity-70', 'pointer-events-none');
+ enableDownloadButton(outputs);
+ populateInspector(outputs, canvas);
+ });
+}
+
+async function saveOutputs(outputs: VertexPrintOutputs) {
+ const zip = new JSZip();
+ const polyhedron = outputs.polyhedron;
+ const baseName = (polyhedron.name || "vertexprint").replace(/[\\/:*?"<>|]/g, "_");
+ for (let i = 0; i < outputs.stls.length; i++) {
+ const blob = new Blob([outputs.stls[i]], { type: "application/octet-stream" });
+ zip.file(`${baseName}_v${i}.stl`, blob);
+ }
+
+ return zip.generateAsync({ type: "blob" }).then((content) => {
+ const url = URL.createObjectURL(content);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${baseName}.zip`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(url);
+ });
+}
+
+function resetDownloadButton(): HTMLButtonElement {
+ const old = document.getElementById('download-button')!;
+ const button = old.cloneNode(true) as HTMLButtonElement;
+ old.replaceWith(button);
+ return button;
+}
+
+function enableDownloadButton(outputs: VertexPrintOutputs) {
+ const button = resetDownloadButton();
+ button.classList.remove('text-v-fg/70', 'cursor-not-allowed');
+ button.classList.add('text-v-fg', 'cursor-pointer', 'hover:bg-v-blue', 'hover:text-v-dark');
+ bindTooltip(button, 'Download generated artifacts.\nYou must generate artifacts before a download.');
+ button.addEventListener('click', () => { void saveOutputs(outputs); });
+}
+
+function disableDownloadButton() {
+ const button = resetDownloadButton();
+ button.classList.add('text-v-fg/70', 'cursor-not-allowed');
+ button.classList.remove('text-v-fg', 'cursor-pointer', 'hover:bg-v-blue', 'hover:text-v-dark');
+ bindTooltip(button, 'Download generated artifacts.\nYou must generate artifacts before a download.');
+}
+
+async function initMesh(canvas: Canvas, sidebar: Sidebar) {
+ sidebar.setParam('scale', 50);
+ const data = await fetch(DATA_DIR + DEFAULT_MODEL).then((r) => r.arrayBuffer());
+ canvas.loadGeometry(DEFAULT_MODEL, data);
+}
+
+async function init() {
+ const canvas = new Canvas();
+ const sidebar = new Sidebar();
+ initInspector();
+ initPresetsMenu(canvas, sidebar);
+ initUploadButton(canvas);
+ initConstructButton(canvas);
+ disableDownloadButton();
+ bindTooltip(document.getElementById('help-button')!, 'Help');
+ bindTooltip(document.getElementById('source-link')!, 'Source code');
+ initMesh(canvas, sidebar);
+}
+
+init();
diff --git a/webapp/core.ts b/webapp/core.ts
new file mode 100644
index 0000000..ff7a860
--- /dev/null
+++ b/webapp/core.ts
@@ -0,0 +1,787 @@
+import Matrix, { SingularValueDecomposition } from "ml-matrix";
+import { STLLoader } from "three/addons/loaders/STLLoader.js";
+import { mergeVertices } from "three/addons/utils/BufferGeometryUtils.js";
+
+let VERTEXPRINT_SOURCE: string | null = null;
+
+async function loadSource(): Promise {
+ if (VERTEXPRINT_SOURCE !== null) {
+ return VERTEXPRINT_SOURCE;
+ }
+ VERTEXPRINT_SOURCE = await fetch("vertexprint.scad").then((r) => r.text());
+ return VERTEXPRINT_SOURCE;
+}
+
+export class VertexPrintParams {
+ edgeDiameter: number;
+ diameterTolerance: number;
+ diameterTaper: number;
+ wallThickness: number;
+ scale: number;
+ rodInset: number;
+ minPrinterOverhangAngle: number;
+ offsetType: string;
+ manualOffset: number;
+ renderQuality: "preview" | "final";
+};
+
+export class VertexPrintOutputs {
+ polyhedron: Polyhedron;
+ stls: ArrayBuffer[];
+
+ constructor(polyhedron: Polyhedron, stls: ArrayBuffer[]) {
+ this.polyhedron = polyhedron;
+ this.stls = stls;
+ }
+}
+
+export async function vertexPrint(
+ name: string,
+ data: ArrayBuffer,
+ options: VertexPrintParams,
+): Promise {
+ const polyhedron = polyhedronFromFile(name, data, options);
+ if (!polyhedron) {
+ throw new Error(`Failed to parse polyhedron from ${name}`);
+ }
+ const scadSource = await loadSource();
+ const args = new OpenscadArgs(polyhedron, options);
+ const count = polyhedron.vertexFigures.length;
+ const cliArgs: string[][] = [];
+ for (let i = 0; i < count; i++) {
+ cliArgs.push(args.toOpenscadArgs(i))
+ }
+ const stls = await renderVertices(polyhedron.name, scadSource, cliArgs);
+ return new VertexPrintOutputs(polyhedron, stls);
+}
+
+// Render vertices in parallel by processing contiguous chunks of vertices
+// through a pool of web workers.
+async function renderVertices(
+ name: string,
+ scadSource: string,
+ cliArgs: string[][],
+): Promise {
+ const count = cliArgs.length;
+ const results: ArrayBuffer[] = new Array(count);
+ if (count === 0) return results;
+
+ const workerCount = Math.min(navigator.hardwareConcurrency || 1, count);
+ const chunkSize = Math.ceil(count / workerCount);
+
+ const workerUrl = new URL("./worker.js", import.meta.url);
+ const workerPromises: Promise[] = [];
+
+ for (let w = 0; w < workerCount; w++) {
+ const start = w * chunkSize;
+ const end = Math.min(start + chunkSize, count);
+ if (start >= end) break;
+
+ const worker = new Worker(workerUrl, { type: "module" });
+
+ workerPromises.push(new Promise((resolve, reject) => {
+ let pending = end - start;
+ const onMessage = (e: MessageEvent) => {
+ const m = e.data;
+ if (m.type === "error") {
+ // TODO: visual cue when an openscad render fails
+ worker.removeEventListener("message", onMessage);
+ worker.terminate();
+ reject(new Error(m.message));
+ return;
+ }
+ results[m.index] = m.buffer;
+ pending--;
+ if (pending === 0) {
+ worker.removeEventListener("message", onMessage);
+ worker.terminate();
+ resolve();
+ }
+ };
+ worker.addEventListener("message", onMessage);
+ for (let i = start; i < end; i++) {
+ worker.postMessage({
+ type: "render",
+ index: i,
+ cliArgs: cliArgs[i],
+ name,
+ source: scadSource,
+ });
+ }
+ }));
+ }
+ await Promise.all(workerPromises);
+ return results;
+}
+
+
+function polyhedronFromFile(name: string, data: ArrayBuffer, options: VertexPrintParams) {
+ let polyhedron: Polyhedron;
+ try {
+ const isObj = name.toLowerCase().endsWith(".obj");
+ polyhedron = isObj
+ ? parseObj(data, name, options)
+ : parseStl(data, name, options);
+ } catch (e) {
+ // TODO: show user an error message
+ console.error("Failed to load", name, e);
+ return;
+ }
+ return polyhedron;
+}
+
+// Parse OBJ files into Polyhedron objects.
+function parseObj(
+ data: ArrayBuffer,
+ filename: string,
+ options: VertexPrintParams,
+): Polyhedron {
+ const text = new TextDecoder().decode(data);
+ const vertices: number[][] = [];
+ const faces: string[][] = [];
+
+ const lines = text.split(/\r?\n/);
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+
+ const parts = line.split(/\s+/);
+ if (parts.length === 0) {
+ continue;
+ }
+
+ if (parts[0] === "v") {
+ const vertex = [parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])];
+ vertices.push(vertex);
+ } else if (parts[0] === "f") {
+ // OBJ uses 1-based indexing, we convert to 0-based
+ const face: string[] = [];
+ for (let i = 1; i < parts.length; i++) {
+ const vertDef = parts[i];
+ const indices = vertDef.split("/")[0];
+ face.push(String(parseInt(indices, 10) - 1));
+ }
+ faces.push(face);
+ }
+ }
+ if (vertices.length === 0) {
+ throw new Error(`No vertices found in OBJ file: ${filename}`);
+ }
+
+ // os.path.basename(filepath).replace(".obj", "")
+ const base = filename.split(/[\\/]/).pop() ?? filename;
+ const name = base.replace(/\.obj$/i, "");
+
+ return new Polyhedron(
+ name,
+ faces,
+ new Matrix(vertices),
+ options,
+ );
+}
+
+// Parse STL files into Polyhedron objects.
+function parseStl(data: ArrayBuffer, filename: string, options: VertexPrintParams): Polyhedron {
+ const parsed = new STLLoader().parse(data);
+ // Three.js's mergeVertices function deduplicates by all vertex attributes.
+ // Keep only the position attribute, to prevent vertices with per-face
+ // normals/colors/other junk from not merging.
+ for (const name of Object.keys(parsed.attributes)) {
+ if (name !== "position") {
+ parsed.deleteAttribute(name);
+ }
+ }
+ const geometry = mergeVertices(parsed);
+ const position = geometry.attributes.position;
+ const index = geometry.index;
+ if (!position || !index) {
+ throw new Error("No positions found in STL data");
+ }
+
+ const arr = position.array as ArrayLike;
+ const vertices: number[][] = [];
+ for (let i = 0; i < position.count; i++) {
+ vertices.push([arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2]]);
+ }
+
+ const faces: string[][] = [];
+ for (let i = 0; i < index.count; i += 3) {
+ faces.push([String(index.getX(i)), String(index.getX(i + 1)), String(index.getX(i + 2))]);
+ }
+
+ return new Polyhedron(filename, faces, new Matrix(vertices), options);
+}
+
+// For some godforsaken reason ml-matrix does not provide cross products.
+function cross(a: Matrix, b: Matrix): Matrix {
+ const [ax, ay, az] = a.to1DArray();
+ const [bx, by, bz] = b.to1DArray();
+ return Matrix.columnVector([
+ ay * bz - az * by,
+ az * bx - ax * bz,
+ ax * by - ay * bx,
+ ]);
+}
+
+
+class VertexFigure {
+ vertex: Matrix;
+ vertexIndex: number;
+ vecs: Matrix;
+ neighbors: number[];
+ std: Matrix;
+ euler: [number, number, number];
+ options: VertexPrintParams;
+
+ halfEdgeOffset: number[];
+ vertexOffset: number;
+
+ edges: number[] = [];
+ tag: number;
+
+ constructor(
+ vertex: Matrix,
+ vertexIndex: number,
+ vecs: Matrix,
+ neighbors: number[],
+ tag: number,
+ options: VertexPrintParams,
+ ) {
+ this.vertex = vertex;
+ this.vertexIndex = vertexIndex;
+ this.vecs = vecs;
+ this.neighbors = neighbors;
+ this.tag = tag;
+ this.options = options;
+
+ this.std = vecs;
+ this.euler = [0.0, 0.0, 0.0];
+
+ this.halfEdgeOffset = this.computeOffsets();
+ this.vertexOffset = this.largestOffset();
+
+ const planeNormal = this.planeNormal();
+ const normal = this.normal();
+ if (normal !== null && planeNormal !== null) {
+ const direction = planeNormal.dot(normal) > 0 ? 1 : -1;
+ const [rotated, euler] = this.reorientTo(
+ Matrix.mul(planeNormal, direction),
+ );
+ this.std = rotated;
+ this.euler = euler;
+ }
+
+ }
+
+ // flag
+ annotateEdgeNames(edges: Map): void {
+ this.edges = [];
+ for (const neighbor of this.neighbors) {
+ const edge: [number, number] =
+ this.vertexIndex < neighbor
+ ? [this.vertexIndex, neighbor]
+ : [neighbor, this.vertexIndex];
+ this.edges.push(edges.get(edge.join(","))!.name);
+ }
+ }
+
+ normalizable(): boolean {
+ return this.normal() !== null;
+ }
+
+ normal(): Matrix | null {
+ const n = Matrix.columnVector(this.vecs.sum("column"));
+ const norm = n.norm();
+ return norm > 1e-10 ? Matrix.mul(n, 1 / norm) : null;
+ }
+
+ // flag
+ planeNormal(): Matrix | null {
+ const v = this.vecs.clone();
+ if (this.options.offsetType === "auto_per_edge") {
+ // vecs *= (half_edge_offset[:, None] + rod_inset): scale each row.
+ const factors = Matrix.columnVector(
+ this.halfEdgeOffset.map((o) => o + this.options.rodInset),
+ ).repeat({ columns: v.columns });
+ v.mul(factors);
+ }
+ if (v.rows < 3) {
+ return null;
+ }
+ // centered = vecs - np.mean(vecs, axis=0)
+ v.center("column");
+ // _, _, vh = np.linalg.svd(centered); normal = vh[2]
+ const svd = new SingularValueDecomposition(v);
+ const diag = svd.diagonal;
+ const V = svd.rightSingularVectors;
+ let minIndex = 0;
+ let min = diag[0];
+ for (let i = 1; i < diag.length; i++) {
+ if (diag[i] < min) {
+ min = diag[i];
+ minIndex = i;
+ }
+ }
+ const normal = V.getColumnVector(minIndex);
+ return Matrix.mul(normal, -1 / normal.norm());
+ }
+
+ matrixToRotation(R: Matrix): [number, number, number] {
+ const sy = Math.sqrt(R.get(0, 0) ** 2 + R.get(1, 0) ** 2);
+ const singular = sy < 1e-6;
+ if (!singular) {
+ return [
+ Math.atan2(R.get(2, 1), R.get(2, 2)),
+ Math.atan2(-R.get(2, 0), sy),
+ Math.atan2(R.get(1, 0), R.get(0, 0)),
+ ];
+ } else if (R.get(2, 0) < 0) {
+ // y = 90 degrees
+ return [Math.atan2(-R.get(1, 2), R.get(1, 1)), Math.PI / 2, 0.0];
+ } else {
+ // y = -90 degrees
+ return [Math.atan2(-R.get(1, 2), R.get(1, 1)), -Math.PI / 2, 0.0];
+ }
+ }
+
+ // Orient normal to target, then apply this rotation to all vectors in the
+ // figure
+ reorientTo(
+ normal: Matrix,
+ target: Matrix = Matrix.columnVector([0.0, 0.0, 1.0]),
+ ): [Matrix, [number, number, number]] {
+ const nn = normal.norm();
+ if (nn < 1e-9) {
+ return [this.vecs, [0.0, 0.0, 0.0]];
+ }
+ const uMean = Matrix.mul(normal, 1 / nn);
+ const axis = cross(uMean, target);
+ const lenAxis = axis.norm();
+ const dotVal = uMean.dot(target);
+ if (lenAxis < 1e-6) {
+ if (dotVal > 0) {
+ return [this.vecs, [0.0, 0.0, 0.0]];
+ } else {
+ // Flip y and z of every row vector.
+ const sign = new Matrix([[1, -1, -1]]).repeat({
+ rows: this.vecs.rows,
+ });
+ const flipped = Matrix.mul(this.vecs, sign);
+ return [flipped, [Math.PI, 0.0, 0.0]];
+ }
+ }
+ const u = axis.mul(1 / lenAxis);
+ const u0 = u.get(0, 0);
+ const u1 = u.get(1, 0);
+ const u2 = u.get(2, 0);
+ const c = dotVal;
+ const s = lenAxis;
+ const C = 1 - c;
+ const R = new Matrix([
+ [c + u0 * u0 * C, u0 * u1 * C - u2 * s, u0 * u2 * C + u1 * s],
+ [u1 * u0 * C + u2 * s, c + u1 * u1 * C, u1 * u2 * C - u0 * s],
+ [u2 * u0 * C - u1 * s, u2 * u1 * C + u0 * s, c + u2 * u2 * C],
+ ]);
+ const RT = R.transpose();
+ const euler = this.matrixToRotation(RT);
+ const rotated = this.vecs.mmul(RT);
+ return [rotated, euler];
+ }
+
+ minCosDist(index: number): Matrix {
+ const scores: number[] = [];
+ const vIndex = this.vecs.getRowVector(index);
+ for (let i = 0; i < this.vecs.rows; i++) {
+ if (i === index) {
+ scores.push(-1000);
+ } else {
+ const vi = this.vecs.getRowVector(i);
+ scores.push(vIndex.dot(vi) / vi.norm());
+ }
+ }
+ let max = scores[0];
+ let maxIndex = 0;
+ for (let i = 1; i < scores.length; i++) {
+ if (scores[i] > max) {
+ max = scores[i];
+ maxIndex = i;
+ }
+ }
+ return this.vecs.getRowVector(maxIndex);
+ }
+
+ axisOffset(v0: Matrix, v1: Matrix): number {
+ const c = v0.dot(v1);
+ const s = cross(v0, v1).norm();
+ const outerTubeRadius =
+ this.options.edgeDiameter / 2 + this.options.wallThickness;
+ const lSide =
+ (outerTubeRadius * c + this.options.edgeDiameter / 2) / s;
+ const lBase = (this.options.edgeDiameter / 2 * (1 + c)) / s;
+ if (s < 1e-9) {
+ return c > 0 ? 1e9 : 0;
+ }
+ return Math.max(lSide, lBase);
+ }
+
+ offsetFromSingleVec(index: number): number {
+ const closest = this.minCosDist(index);
+ return this.axisOffset(
+ this.vecs.getRowVector(index),
+ closest,
+ );
+ }
+
+ computeOffsets(): number[] {
+ const out: number[] = [];
+ for (let i = 0; i < this.vecs.rows; i++) {
+ out.push(this.offsetFromSingleVec(i));
+ }
+ return out;
+ }
+
+ largestOffset(): number {
+ return Math.max(...this.halfEdgeOffset);
+ }
+}
+
+type EdgeField = { length: number; offsetLength: number; name: number, offsets: [number, number] };
+
+class Polyhedron {
+ name: string;
+ faces: string[][];
+ vertices: Matrix;
+ options: VertexPrintParams;
+
+ edges: Map;
+ vertexFigures: VertexFigure[];
+ solidOffset: number;
+
+ constructor(
+ name: string,
+ faces: string[][],
+ vertices: Matrix,
+ options: VertexPrintParams,
+ ) {
+ this.name = name;
+ this.faces = faces;
+ this.vertices = vertices;
+ this.options = options;
+
+ this.edges = this.makeEdgelist();
+ this.vertexFigures = this.annotateVertexFigures();
+ this.solidOffset = this.largestOffset();
+ this.computeEdgeLengths();
+ for (const vf of this.vertexFigures) {
+ vf.annotateEdgeNames(this.edges);
+ }
+ }
+
+ averageEdgeLength(): number {
+ if (this.edges.size === 0) {
+ return 0;
+ }
+ let total = 0;
+ for (const e of this.edges.values()) {
+ total += e.length;
+ }
+ return total / this.edges.size;
+ }
+
+ // Largest offset among all vertex figure max offsets
+ largestOffset(): number {
+ if (this.vertexFigures.length === 0) {
+ return 0;
+ }
+ let max = this.vertexFigures[0].vertexOffset;
+ for (let i = 1; i < this.vertexFigures.length; i++) {
+ if (this.vertexFigures[i].vertexOffset > max) {
+ max = this.vertexFigures[i].vertexOffset;
+ }
+ }
+ return max;
+ }
+
+ // Determine offsets to be subtracted from each end of a given edge
+ offsetForEdge(v1: number, v2: number): [number, number] {
+ const vf1 = this.vertexFigures[v1];
+ const vf2 = this.vertexFigures[v2];
+ switch (this.options.offsetType) {
+ case "fixed":
+ return [this.options.manualOffset, this.options.manualOffset];
+ case "auto_per_vertex":
+ return [vf1.vertexOffset, vf2.vertexOffset];
+ case "auto_per_edge": {
+ const i1 = vf1.neighbors.findIndex((n) => n === v2);
+ const i2 = vf2.neighbors.findIndex((n) => n === v1);
+ return [
+ vf1.halfEdgeOffset[i1],
+ vf2.halfEdgeOffset[i2],
+ ];
+ }
+ case "auto_global":
+ default:
+ return [this.solidOffset, this.solidOffset];
+ }
+ }
+
+ // rod length = scale * |v1 - v2| - offset(v1, v2) - offset(v2, v1)
+ // value appended to self.edges
+ computeEdgeLengths(): void {
+ type KLO = { key: string; length: number; offsetLength: number, offsets: [number, number] };
+ const klo: KLO[] = [];
+ for (const key of this.edges.keys()) {
+ const [v1, v2] = key.split(",").map(Number);
+ const [v1_offset, v2_offset] = this.offsetForEdge(v1, v2);
+ const v1_arr = this.vertices.getRowVector(v1);
+ const v2_arr = this.vertices.getRowVector(v2);
+ const length = Matrix.sub(v2_arr, v1_arr).norm();
+ const offsetLength = this.options.scale * length - v1_offset - v2_offset;
+ klo.push({ key, length, offsetLength, offsets: [v1_offset, v2_offset] });
+ }
+
+ klo.sort((x, y) => x.offsetLength - y.offsetLength);
+ const sorted = new Map();
+ for (let i = 0; i < klo.length; i++) {
+ const { key, length, offsetLength, offsets } = klo[i];
+ sorted.set(key, { length, offsetLength, name: i, offsets });
+ }
+ this.edges = sorted;
+ }
+
+ // Convert facelist into edgelist
+ makeEdgelist(): Map {
+ const edges = new Map();
+ for (const face of this.faces) {
+ for (let k = 0; k < face.length; k++) {
+ const v1 = parseInt(face[k], 10);
+ const v2 = parseInt(face[(k + 1) % face.length], 10);
+ if (Number.isNaN(v1) || Number.isNaN(v2)) {
+ continue;
+ }
+ if (v1 < this.vertices.rows && v2 < this.vertices.rows) {
+ const key =
+ v1 < v2 ? `${v1},${v2}` : `${v2},${v1}`;
+ if (!edges.has(key)) {
+ edges.set(key, { length: 0, offsetLength: 0, name: -1, offsets: [0, 0] });
+ }
+ }
+ }
+ }
+ return edges;
+ }
+
+ // Distinct vertex figures should have distinct signatures. Probably.
+ vertexFigureSignature(vecs: Matrix): string {
+ const precision = 100000;
+ const n = vecs.rows;
+ const rows: Matrix[] = [];
+ for (let i = 0; i < n; i++) {
+ rows.push(vecs.getRowVector(i));
+ }
+ const dots: number[] = [];
+ for (let i = 0; i < n; i++) {
+ for (let j = i; j < n; j++) {
+ dots.push(Math.round(rows[i].dot(rows[j]) * precision));
+ }
+ }
+ const triples: number[] = [];
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ for (let k = 0; k < n; k++) {
+ triples.push(
+ Math.round(cross(rows[i], rows[j]).dot(rows[k]) * precision),
+ );
+ }
+ }
+ }
+ dots.sort((a, b) => a - b);
+ triples.sort((a, b) => a - b);
+ return dots.join(",") + "|" + triples.join(",");
+ }
+
+ // Construct vertex figure list
+ annotateVertexFigures(): VertexFigure[] {
+ const tags = new Map();
+ let tag = 0;
+ const vertexFigures: VertexFigure[] = [];
+
+ for (let i = 0; i < this.vertices.rows; i++) {
+ const neighbors: number[] = [];
+ for (const key of this.edges.keys()) {
+ const [a, b] = key.split(",").map(Number);
+ if (a === i) {
+ neighbors.push(b);
+ } else if (b === i) {
+ neighbors.push(a);
+ }
+ }
+ const vertex = this.vertices.getRowVector(i);
+ // Direction vectors from this vertex to each neighbor, normalized.
+ const vecs = new Matrix(
+ neighbors.map((n) => {
+ const nb = this.vertices.getRowVector(n);
+ const diff = Matrix.sub(nb, vertex);
+ const norm = diff.norm();
+ const f = norm > 0 ? 1 / norm : 1;
+ return Matrix.mul(diff, f).to1DArray();
+ }),
+ );
+
+ const signature = this.vertexFigureSignature(vecs);
+ if (!tags.has(signature)) {
+ tags.set(signature, tag);
+ tag++;
+ }
+
+ const t = tags.get(signature)!;
+ vertexFigures.push(
+ new VertexFigure(
+ vertex,
+ i,
+ vecs,
+ neighbors,
+ t,
+ this.options,
+ ),
+ );
+ }
+
+ return vertexFigures;
+ }
+}
+
+class OpenscadArgs {
+ vertices: Matrix;
+ edges: number[][];
+ vertexFigures: Matrix[];
+ eulers: number[][];
+ tags: number[];
+ vertexFigureEdges: number[][];
+ offsets: number[][];
+ options: VertexPrintParams
+
+ constructor(polyhedron: Polyhedron, options: VertexPrintParams) {
+ const {
+ vertices,
+ edges,
+ vertexFigures,
+ eulers,
+ tags,
+ vertexFigureEdges,
+ } = this.polyhedronOptionsArray(polyhedron);
+
+ this.vertices = vertices;
+ this.edges = edges;
+ this.vertexFigures = vertexFigures;
+ this.eulers = eulers;
+ this.tags = tags;
+ this.vertexFigureEdges = vertexFigureEdges;
+ this.offsets = this.polyhedronOffsetArray(polyhedron);
+
+ this.options = options
+ }
+
+ polyhedronOptionsArray(polyhedron: Polyhedron): {
+ vertices: Matrix;
+ edges: number[][];
+ vertexFigures: Matrix[];
+ eulers: number[][];
+ tags: number[];
+ vertexFigureEdges: number[][];
+ } {
+ const vertices = polyhedron.vertices;
+ const edges: number[][] = [];
+ for (const key of polyhedron.edges.keys()) {
+ const [v1, v2] = key.split(",").map(Number);
+ edges.push([v1, v2]);
+ }
+ const vertexFigures: Matrix[] = [];
+ const eulers: number[][] = [];
+ const tags: number[] = [];
+ const vertexFigureEdges: number[][] = [];
+
+ for (const vf of polyhedron.vertexFigures) {
+ vertexFigures.push(vf.std);
+ eulers.push(vf.euler);
+ tags.push(vf.tag);
+ vertexFigureEdges.push(vf.edges);
+ }
+
+ return { vertices, edges, vertexFigures, eulers, tags, vertexFigureEdges };
+ }
+
+ polyhedronOffsetArray(polyhedron: Polyhedron): number[][] {
+ switch (polyhedron.options.offsetType) {
+ case "fixed": {
+ const value = polyhedron.options.manualOffset;
+ return polyhedron.vertexFigures.map((vf) =>
+ Array(vf.vecs.rows).fill(value),
+ );
+ }
+ case "auto_per_vertex": {
+ return polyhedron.vertexFigures.map((vf) =>
+ Array(vf.vecs.rows).fill(vf.vertexOffset),
+ );
+ }
+ case "auto_per_edge": {
+ return polyhedron.vertexFigures.map((vf) =>
+ [...vf.halfEdgeOffset],
+ );
+ }
+ case "auto_global":
+ default: {
+ const value = polyhedron.solidOffset;
+ return polyhedron.vertexFigures.map((vf) =>
+ Array(vf.vecs.rows).fill(value),
+ );
+ }
+ }
+ }
+
+ // Convert a vertex to an unholy argument list that is passed to an
+ // openscad call
+ toOpenscadArgs(vertex: number): string[] {
+ const args: string[] = [];
+ args.push(`-DEDGE_DIAMETER=${this.options.edgeDiameter}`);
+ args.push(`-DDIAMETER_TOLERANCE_FIT=${this.options.diameterTolerance}`);
+ args.push(`-DDIAMETER_TAPER_DECREASE=${this.options.diameterTaper}`);
+ args.push(`-DWALL_THICKNESS=${this.options.wallThickness}`);
+ args.push(`-DROD_INSET=${this.options.rodInset}`);
+ args.push(
+ `-DMIN_PRINTER_OVERHANG_ANGLE=${this.options.minPrinterOverhangAngle}`,
+ );
+
+ const fparams = {
+ // fa, fs
+ "preview": [12, 2],
+ "final": [30, 0.2],
+ }
+
+ args.push(`-DFA=${fparams[this.options.renderQuality][0]}`)
+ args.push(`-DFS=${fparams[this.options.renderQuality][1]}`)
+
+ args.push(`-Dindex=${vertex}`)
+ // vertexFigures (std)
+ const vertexFigure = this.vertexFigures[vertex]
+ const rows: string[] = [];
+ for (let i = 0; i < vertexFigure.rows; i++) {
+ rows.push(`[${vertexFigure.getRow(i).join(",")}]`);
+ }
+ const vfStr = `[${rows.join(",")}]`;
+ args.push(`-Dvertex_figure=${vfStr}`);
+
+ const tag = this.tags[vertex]
+ args.push(`-Dtag=${tag}`);
+
+ const offset = `[${this.offsets[vertex].join(",")}]`;
+ args.push(`-Doffsets=${offset}`);
+
+ const vertexFigureEdges = `[${this.vertexFigureEdges[vertex].join(",")}]`;
+ args.push(`-Dvertex_figure_edge=${vertexFigureEdges}`);
+
+ return args;
+ }
+}
diff --git a/webapp/data/13RhombicDodecahedra.obj b/webapp/data/13RhombicDodecahedra.obj
new file mode 100644
index 0000000..efbd62a
--- /dev/null
+++ b/webapp/data/13RhombicDodecahedra.obj
@@ -0,0 +1,263 @@
+v 0.530330 0.530330 0.530330
+v 0.000000 0.000000 1.060660
+v 0.530330 -0.530330 0.530330
+v 1.060660 0.000000 0.000000
+v 0.530330 0.530330 -0.530330
+v 0.000000 1.060660 0.000000
+v -0.530330 0.530330 0.530330
+v 0.530330 -0.530330 -0.530330
+v 0.000000 0.000000 -1.060660
+v 0.000000 -1.060660 0.000000
+v -0.530330 -0.530330 -0.530330
+v -0.530330 0.530330 -0.530330
+v -1.060660 0.000000 0.000000
+v -0.530330 -0.530330 0.530330
+v -1.060660 -1.060660 1.060660
+v -0.530330 -1.590990 0.530330
+v -1.590990 -0.530330 0.530330
+v -0.530330 -1.590990 -0.530330
+v -1.060660 -1.060660 -1.060660
+v -1.060660 -2.121320 0.000000
+v -1.590990 -1.590990 -0.530330
+v -1.590990 -0.530330 -0.530330
+v -2.121320 -1.060660 0.000000
+v -1.590990 -1.590990 0.530330
+v -0.530330 0.530330 -1.590990
+v -1.060660 1.060660 -1.060660
+v -1.590990 0.530330 -0.530330
+v -0.530330 -0.530330 -1.590990
+v -1.060660 0.000000 -2.121320
+v -1.590990 -0.530330 -1.590990
+v -1.590990 0.530330 -1.590990
+v -2.121320 0.000000 -1.060660
+v 0.530330 -1.590990 -0.530330
+v 1.060660 -1.060660 -1.060660
+v 0.530330 -0.530330 -1.590990
+v 0.530330 -1.590990 -1.590990
+v 0.000000 -1.060660 -2.121320
+v 0.000000 -2.121320 -1.060660
+v -0.530330 -1.590990 -1.590990
+v -0.530330 1.590990 0.530330
+v -1.060660 1.060660 1.060660
+v -0.530330 1.590990 -0.530330
+v -1.060660 2.121320 0.000000
+v -1.590990 1.590990 0.530330
+v -1.590990 1.590990 -0.530330
+v -2.121320 1.060660 0.000000
+v -1.590990 0.530330 0.530330
+v -0.530330 0.530330 1.590990
+v -1.060660 0.000000 2.121320
+v -0.530330 -0.530330 1.590990
+v -1.590990 0.530330 1.590990
+v -2.121320 0.000000 1.060660
+v -1.590990 -0.530330 1.590990
+v 0.530330 -0.530330 1.590990
+v 0.000000 -1.060660 2.121320
+v 0.530330 -1.590990 1.590990
+v 1.060660 -1.060660 1.060660
+v 0.530330 -1.590990 0.530330
+v 0.000000 -2.121320 1.060660
+v -0.530330 -1.590990 1.590990
+v 1.590990 -0.530330 0.530330
+v 1.590990 -1.590990 0.530330
+v 2.121320 -1.060660 0.000000
+v 1.590990 -0.530330 -0.530330
+v 1.590990 -1.590990 -0.530330
+v 1.060660 -2.121320 0.000000
+v 1.590990 0.530330 -0.530330
+v 2.121320 0.000000 -1.060660
+v 1.590990 0.530330 -1.590990
+v 1.060660 1.060660 -1.060660
+v 1.590990 -0.530330 -1.590990
+v 1.060660 0.000000 -2.121320
+v 0.530330 0.530330 -1.590990
+v 0.530330 1.590990 -0.530330
+v 0.530330 1.590990 -1.590990
+v 0.000000 2.121320 -1.060660
+v 0.000000 1.060660 -2.121320
+v -0.530330 1.590990 -1.590990
+v 1.590990 1.590990 0.530330
+v 1.060660 1.060660 1.060660
+v 1.590990 0.530330 0.530330
+v 2.121320 1.060660 0.000000
+v 1.590990 1.590990 -0.530330
+v 1.060660 2.121320 0.000000
+v 0.530330 1.590990 0.530330
+v 1.590990 0.530330 1.590990
+v 1.060660 0.000000 2.121320
+v 1.590990 -0.530330 1.590990
+v 2.121320 0.000000 1.060660
+v 0.530330 0.530330 1.590990
+v 0.530330 1.590990 1.590990
+v 0.000000 1.060660 2.121320
+v 0.000000 2.121320 1.060660
+v -0.530330 1.590990 1.590990
+g dodecahedron_0
+f 1 2 3 4
+f 1 4 5 6
+f 1 6 7 2
+f 8 9 5 4
+f 8 4 3 10
+f 8 10 11 9
+f 12 9 11 13
+f 12 13 7 6
+f 12 6 5 9
+f 14 2 7 13
+f 14 13 11 10
+f 14 10 3 2
+g dodecahedron_1
+f 14 15 16 10
+f 14 10 11 13
+f 14 13 17 15
+f 18 19 11 10
+f 18 10 16 20
+f 18 20 21 19
+f 22 19 21 23
+f 22 23 17 13
+f 22 13 11 19
+f 24 15 17 23
+f 24 23 21 20
+f 24 20 16 15
+g dodecahedron_2
+f 12 13 11 9
+f 12 9 25 26
+f 12 26 27 13
+f 28 29 25 9
+f 28 9 11 19
+f 28 19 30 29
+f 31 29 30 32
+f 31 32 27 26
+f 31 26 25 29
+f 22 13 27 32
+f 22 32 30 19
+f 22 19 11 13
+g dodecahedron_3
+f 8 10 33 34
+f 8 34 35 9
+f 8 9 11 10
+f 36 37 35 34
+f 36 34 33 38
+f 36 38 39 37
+f 28 37 39 19
+f 28 19 11 9
+f 28 9 35 37
+f 18 10 11 19
+f 18 19 39 38
+f 18 38 33 10
+g dodecahedron_4
+f 40 41 7 6
+f 40 6 42 43
+f 40 43 44 41
+f 12 26 42 6
+f 12 6 7 13
+f 12 13 27 26
+f 45 26 27 46
+f 45 46 44 43
+f 45 43 42 26
+f 47 41 44 46
+f 47 46 27 13
+f 47 13 7 41
+g dodecahedron_5
+f 48 49 50 2
+f 48 2 7 41
+f 48 41 51 49
+f 14 13 7 2
+f 14 2 50 15
+f 14 15 17 13
+f 47 13 17 52
+f 47 52 51 41
+f 47 41 7 13
+f 53 49 51 52
+f 53 52 17 15
+f 53 15 50 49
+g dodecahedron_6
+f 54 55 56 57
+f 54 57 3 2
+f 54 2 50 55
+f 58 10 3 57
+f 58 57 56 59
+f 58 59 16 10
+f 14 10 16 15
+f 14 15 50 2
+f 14 2 3 10
+f 60 55 50 15
+f 60 15 16 59
+f 60 59 56 55
+g dodecahedron_7
+f 61 57 62 63
+f 61 63 64 4
+f 61 4 3 57
+f 65 34 64 63
+f 65 63 62 66
+f 65 66 33 34
+f 8 34 33 10
+f 8 10 3 4
+f 8 4 64 34
+f 58 57 3 10
+f 58 10 33 66
+f 58 66 62 57
+g dodecahedron_8
+f 67 4 64 68
+f 67 68 69 70
+f 67 70 5 4
+f 71 72 69 68
+f 71 68 64 34
+f 71 34 35 72
+f 73 72 35 9
+f 73 9 5 70
+f 73 70 69 72
+f 8 4 5 9
+f 8 9 35 34
+f 8 34 64 4
+g dodecahedron_9
+f 74 6 5 70
+f 74 70 75 76
+f 74 76 42 6
+f 73 77 75 70
+f 73 70 5 9
+f 73 9 25 77
+f 78 77 25 26
+f 78 26 42 76
+f 78 76 75 77
+f 12 6 42 26
+f 12 26 25 9
+f 12 9 5 6
+g dodecahedron_10
+f 79 80 81 82
+f 79 82 83 84
+f 79 84 85 80
+f 67 70 83 82
+f 67 82 81 4
+f 67 4 5 70
+f 74 70 5 6
+f 74 6 85 84
+f 74 84 83 70
+f 1 80 85 6
+f 1 6 5 4
+f 1 4 81 80
+g dodecahedron_11
+f 86 87 88 89
+f 86 89 81 80
+f 86 80 90 87
+f 61 4 81 89
+f 61 89 88 57
+f 61 57 3 4
+f 1 4 3 2
+f 1 2 90 80
+f 1 80 81 4
+f 54 87 90 2
+f 54 2 3 57
+f 54 57 88 87
+g dodecahedron_12
+f 91 92 90 80
+f 91 80 85 93
+f 91 93 94 92
+f 1 6 85 80
+f 1 80 90 2
+f 1 2 7 6
+f 40 6 7 41
+f 40 41 94 93
+f 40 93 85 6
+f 48 92 94 41
+f 48 41 7 2
+f 48 2 90 92
diff --git a/webapp/data/Cube.obj b/webapp/data/Cube.obj
new file mode 100644
index 0000000..31f77d0
--- /dev/null
+++ b/webapp/data/Cube.obj
@@ -0,0 +1,14 @@
+v 0.5 0.5 0.5
+v 0.5 0.5 -0.5
+v 0.5 -0.5 0.5
+v 0.5 -0.5 -0.5
+v -0.5 0.5 0.5
+v -0.5 0.5 -0.5
+v -0.5 -0.5 0.5
+v -0.5 -0.5 -0.5
+f 1 2 6 5
+f 1 5 7 3
+f 1 3 4 2
+f 8 4 3 7
+f 8 7 5 6
+f 8 6 2 4
diff --git a/webapp/data/Cuboctahedron.obj b/webapp/data/Cuboctahedron.obj
new file mode 100644
index 0000000..39a7418
--- /dev/null
+++ b/webapp/data/Cuboctahedron.obj
@@ -0,0 +1,26 @@
+v 0.7071067811865476 0.0 0.7071067811865476
+v 0.7071067811865476 0.0 -0.7071067811865476
+v -0.7071067811865476 0.0 0.7071067811865476
+v -0.7071067811865476 0.0 -0.7071067811865476
+v 0.7071067811865476 0.7071067811865476 0.0
+v 0.7071067811865476 -0.7071067811865476 0.0
+v -0.7071067811865476 0.7071067811865476 0.0
+v -0.7071067811865476 -0.7071067811865476 0.0
+v 0.0 0.7071067811865476 0.7071067811865476
+v 0.0 0.7071067811865476 -0.7071067811865476
+v 0.0 -0.7071067811865476 0.7071067811865476
+v 0.0 -0.7071067811865476 -0.7071067811865476
+f 1 6 2 5
+f 1 9 3 11
+f 8 3 7 4
+f 8 12 6 11
+f 10 2 12 4
+f 10 7 9 5
+f 1 5 9
+f 2 6 12
+f 3 8 11
+f 4 7 10
+f 5 2 10
+f 6 1 11
+f 7 3 9
+f 8 4 12
diff --git a/webapp/data/DeltoidalHexecontahedron.obj b/webapp/data/DeltoidalHexecontahedron.obj
new file mode 100644
index 0000000..96682e5
--- /dev/null
+++ b/webapp/data/DeltoidalHexecontahedron.obj
@@ -0,0 +1,122 @@
+v 0.0 0.0 2.23606797749979
+v 0.0 0.0 -2.23606797749979
+v 2.23606797749979 0.0 0.0
+v -2.23606797749979 0.0 0.0
+v 0.0 2.23606797749979 0.0
+v 0.0 -2.23606797749979 0.0
+v 0.0 0.7834576353408995 2.051118718068096
+v 0.0 0.7834576353408995 -2.051118718068096
+v 0.0 -0.7834576353408995 2.051118718068096
+v 0.0 -0.7834576353408995 -2.051118718068096
+v 2.051118718068096 0.0 0.7834576353408995
+v 2.051118718068096 0.0 -0.7834576353408995
+v -2.051118718068096 0.0 0.7834576353408995
+v -2.051118718068096 0.0 -0.7834576353408995
+v 0.7834576353408995 2.051118718068096 0.0
+v 0.7834576353408995 -2.051118718068096 0.0
+v -0.7834576353408995 2.051118718068096 0.0
+v -0.7834576353408995 -2.051118718068096 0.0
+v 1.2060113295832984 0.0 1.9513673220832282
+v 1.2060113295832984 0.0 -1.9513673220832282
+v -1.2060113295832984 0.0 1.9513673220832282
+v -1.2060113295832984 0.0 -1.9513673220832282
+v 1.9513673220832282 1.2060113295832984 0.0
+v 1.9513673220832282 -1.2060113295832984 0.0
+v -1.9513673220832282 1.2060113295832984 0.0
+v -1.9513673220832282 -1.2060113295832984 0.0
+v 0.0 1.9513673220832282 1.2060113295832984
+v 0.0 1.9513673220832282 -1.2060113295832984
+v 0.0 -1.9513673220832282 1.2060113295832984
+v 0.0 -1.9513673220832282 -1.2060113295832984
+v 0.6909830056250525 1.118033988749895 1.8090169943749475
+v 0.6909830056250525 1.118033988749895 -1.8090169943749475
+v 0.6909830056250525 -1.118033988749895 1.8090169943749475
+v 0.6909830056250525 -1.118033988749895 -1.8090169943749475
+v -0.6909830056250525 1.118033988749895 1.8090169943749475
+v -0.6909830056250525 1.118033988749895 -1.8090169943749475
+v -0.6909830056250525 -1.118033988749895 1.8090169943749475
+v -0.6909830056250525 -1.118033988749895 -1.8090169943749475
+v 1.8090169943749475 0.6909830056250525 1.118033988749895
+v 1.8090169943749475 0.6909830056250525 -1.118033988749895
+v 1.8090169943749475 -0.6909830056250525 1.118033988749895
+v 1.8090169943749475 -0.6909830056250525 -1.118033988749895
+v -1.8090169943749475 0.6909830056250525 1.118033988749895
+v -1.8090169943749475 0.6909830056250525 -1.118033988749895
+v -1.8090169943749475 -0.6909830056250525 1.118033988749895
+v -1.8090169943749475 -0.6909830056250525 -1.118033988749895
+v 1.118033988749895 1.8090169943749475 0.6909830056250525
+v 1.118033988749895 1.8090169943749475 -0.6909830056250525
+v 1.118033988749895 -1.8090169943749475 0.6909830056250525
+v 1.118033988749895 -1.8090169943749475 -0.6909830056250525
+v -1.118033988749895 1.8090169943749475 0.6909830056250525
+v -1.118033988749895 1.8090169943749475 -0.6909830056250525
+v -1.118033988749895 -1.8090169943749475 0.6909830056250525
+v -1.118033988749895 -1.8090169943749475 -0.6909830056250525
+v 1.2676610827271964 1.2676610827271964 1.2676610827271964
+v 1.2676610827271964 1.2676610827271964 -1.2676610827271964
+v 1.2676610827271964 -1.2676610827271964 1.2676610827271964
+v 1.2676610827271964 -1.2676610827271964 -1.2676610827271964
+v -1.2676610827271964 1.2676610827271964 1.2676610827271964
+v -1.2676610827271964 1.2676610827271964 -1.2676610827271964
+v -1.2676610827271964 -1.2676610827271964 1.2676610827271964
+v -1.2676610827271964 -1.2676610827271964 -1.2676610827271964
+f 19 1 9 33
+f 19 33 57 41
+f 19 41 11 39
+f 19 39 55 31
+f 19 31 7 1
+f 20 2 8 32
+f 20 32 56 40
+f 20 40 12 42
+f 20 42 58 34
+f 20 34 10 2
+f 21 1 7 35
+f 21 35 59 43
+f 21 43 13 45
+f 21 45 61 37
+f 21 37 9 1
+f 22 2 10 38
+f 22 38 62 46
+f 22 46 14 44
+f 22 44 60 36
+f 22 36 8 2
+f 23 3 12 40
+f 23 40 56 48
+f 23 48 15 47
+f 23 47 55 39
+f 23 39 11 3
+f 24 3 11 41
+f 24 41 57 49
+f 24 49 16 50
+f 24 50 58 42
+f 24 42 12 3
+f 25 4 13 43
+f 25 43 59 51
+f 25 51 17 52
+f 25 52 60 44
+f 25 44 14 4
+f 26 4 14 46
+f 26 46 62 54
+f 26 54 18 53
+f 26 53 61 45
+f 26 45 13 4
+f 27 5 17 51
+f 27 51 59 35
+f 27 35 7 31
+f 27 31 55 47
+f 27 47 15 5
+f 28 5 15 48
+f 28 48 56 32
+f 28 32 8 36
+f 28 36 60 52
+f 28 52 17 5
+f 29 6 16 49
+f 29 49 57 33
+f 29 33 9 37
+f 29 37 61 53
+f 29 53 18 6
+f 30 6 18 54
+f 30 54 62 38
+f 30 38 10 34
+f 30 34 58 50
+f 30 50 16 6
diff --git a/webapp/data/DeltoidalIcositetrahedron.obj b/webapp/data/DeltoidalIcositetrahedron.obj
new file mode 100644
index 0000000..91388ca
--- /dev/null
+++ b/webapp/data/DeltoidalIcositetrahedron.obj
@@ -0,0 +1,50 @@
+v 0.0 0.0 1.4142135623730951
+v 0.0 0.0 -1.4142135623730951
+v 1.4142135623730951 0.0 0.0
+v -1.4142135623730951 0.0 0.0
+v 0.0 1.4142135623730951 0.0
+v 0.0 -1.4142135623730951 0.0
+v 1.0 0.0 1.0
+v 1.0 0.0 -1.0
+v -1.0 0.0 1.0
+v -1.0 0.0 -1.0
+v 1.0 1.0 0.0
+v 1.0 -1.0 0.0
+v -1.0 1.0 0.0
+v -1.0 -1.0 0.0
+v 0.0 1.0 1.0
+v 0.0 1.0 -1.0
+v 0.0 -1.0 1.0
+v 0.0 -1.0 -1.0
+v 0.7734590803390136 0.7734590803390136 0.7734590803390136
+v 0.7734590803390136 0.7734590803390136 -0.7734590803390136
+v 0.7734590803390136 -0.7734590803390136 0.7734590803390136
+v 0.7734590803390136 -0.7734590803390136 -0.7734590803390136
+v -0.7734590803390136 0.7734590803390136 0.7734590803390136
+v -0.7734590803390136 0.7734590803390136 -0.7734590803390136
+v -0.7734590803390136 -0.7734590803390136 0.7734590803390136
+v -0.7734590803390136 -0.7734590803390136 -0.7734590803390136
+f 1 7 19 15
+f 1 15 23 9
+f 1 9 25 17
+f 1 17 21 7
+f 2 10 24 16
+f 2 16 20 8
+f 2 8 22 18
+f 2 18 26 10
+f 3 8 20 11
+f 3 11 19 7
+f 3 7 21 12
+f 3 12 22 8
+f 4 9 23 13
+f 4 13 24 10
+f 4 10 26 14
+f 4 14 25 9
+f 5 11 20 16
+f 5 16 24 13
+f 5 13 23 15
+f 5 15 19 11
+f 6 12 21 17
+f 6 17 25 14
+f 6 14 26 18
+f 6 18 22 12
diff --git a/webapp/data/DisdyakisDodecahedron.obj b/webapp/data/DisdyakisDodecahedron.obj
new file mode 100644
index 0000000..63eb2e3
--- /dev/null
+++ b/webapp/data/DisdyakisDodecahedron.obj
@@ -0,0 +1,74 @@
+v 0.0 0.0 2.6754174373368365
+v 0.0 0.0 -2.6754174373368365
+v 2.6754174373368365 0.0 0.0
+v -2.6754174373368365 0.0 0.0
+v 0.0 2.6754174373368365 0.0
+v 0.0 -2.6754174373368365 0.0
+v 1.6407544820340814 0.0 1.6407544820340814
+v 1.6407544820340814 0.0 -1.6407544820340814
+v -1.6407544820340814 0.0 1.6407544820340814
+v -1.6407544820340814 0.0 -1.6407544820340814
+v 1.6407544820340814 1.6407544820340814 0.0
+v 1.6407544820340814 -1.6407544820340814 0.0
+v -1.6407544820340814 1.6407544820340814 0.0
+v -1.6407544820340814 -1.6407544820340814 0.0
+v 0.0 1.6407544820340814 1.6407544820340814
+v 0.0 1.6407544820340814 -1.6407544820340814
+v 0.0 -1.6407544820340814 1.6407544820340814
+v 0.0 -1.6407544820340814 -1.6407544820340814
+v 1.4142135623730951 1.4142135623730951 1.4142135623730951
+v 1.4142135623730951 1.4142135623730951 -1.4142135623730951
+v 1.4142135623730951 -1.4142135623730951 1.4142135623730951
+v 1.4142135623730951 -1.4142135623730951 -1.4142135623730951
+v -1.4142135623730951 1.4142135623730951 1.4142135623730951
+v -1.4142135623730951 1.4142135623730951 -1.4142135623730951
+v -1.4142135623730951 -1.4142135623730951 1.4142135623730951
+v -1.4142135623730951 -1.4142135623730951 -1.4142135623730951
+f 1 7 19
+f 1 19 15
+f 1 15 23
+f 1 23 9
+f 1 9 25
+f 1 25 17
+f 1 17 21
+f 1 21 7
+f 2 8 22
+f 2 22 18
+f 2 18 26
+f 2 26 10
+f 2 10 24
+f 2 24 16
+f 2 16 20
+f 2 20 8
+f 3 7 21
+f 3 21 12
+f 3 12 22
+f 3 22 8
+f 3 8 20
+f 3 20 11
+f 3 11 19
+f 3 19 7
+f 4 9 23
+f 4 23 13
+f 4 13 24
+f 4 24 10
+f 4 10 26
+f 4 26 14
+f 4 14 25
+f 4 25 9
+f 5 11 20
+f 5 20 16
+f 5 16 24
+f 5 24 13
+f 5 13 23
+f 5 23 15
+f 5 15 19
+f 5 19 11
+f 6 12 21
+f 6 21 17
+f 6 17 25
+f 6 25 14
+f 6 14 26
+f 6 26 18
+f 6 18 22
+f 6 22 12
diff --git a/webapp/data/DisdyakisTriacontahedron.obj b/webapp/data/DisdyakisTriacontahedron.obj
new file mode 100644
index 0000000..41e8658
--- /dev/null
+++ b/webapp/data/DisdyakisTriacontahedron.obj
@@ -0,0 +1,182 @@
+v 0.0 0.0 3.802983248181589
+v 0.0 0.0 -3.802983248181589
+v 3.802983248181589 0.0 0.0
+v -3.802983248181589 0.0 0.0
+v 0.0 3.802983248181589 0.0
+v 0.0 -3.802983248181589 0.0
+v 0.0 1.381966011250105 3.618033988749895
+v 0.0 1.381966011250105 -3.618033988749895
+v 0.0 -1.381966011250105 3.618033988749895
+v 0.0 -1.381966011250105 -3.618033988749895
+v 3.618033988749895 0.0 1.381966011250105
+v 3.618033988749895 0.0 -1.381966011250105
+v -3.618033988749895 0.0 1.381966011250105
+v -3.618033988749895 0.0 -1.381966011250105
+v 1.381966011250105 3.618033988749895 0.0
+v 1.381966011250105 -3.618033988749895 0.0
+v -1.381966011250105 3.618033988749895 0.0
+v -1.381966011250105 -3.618033988749895 0.0
+v 2.170820393249937 0.0 3.5124611797498106
+v 2.170820393249937 0.0 -3.5124611797498106
+v -2.170820393249937 0.0 3.5124611797498106
+v -2.170820393249937 0.0 -3.5124611797498106
+v 3.5124611797498106 2.170820393249937 0.0
+v 3.5124611797498106 -2.170820393249937 0.0
+v -3.5124611797498106 2.170820393249937 0.0
+v -3.5124611797498106 -2.170820393249937 0.0
+v 0.0 3.5124611797498106 2.170820393249937
+v 0.0 3.5124611797498106 -2.170820393249937
+v 0.0 -3.5124611797498106 2.170820393249937
+v 0.0 -3.5124611797498106 -2.170820393249937
+v 1.1751864530113494 1.9014916240907944 3.0766780771021436
+v 1.1751864530113494 1.9014916240907944 -3.0766780771021436
+v 1.1751864530113494 -1.9014916240907944 3.0766780771021436
+v 1.1751864530113494 -1.9014916240907944 -3.0766780771021436
+v -1.1751864530113494 1.9014916240907944 3.0766780771021436
+v -1.1751864530113494 1.9014916240907944 -3.0766780771021436
+v -1.1751864530113494 -1.9014916240907944 3.0766780771021436
+v -1.1751864530113494 -1.9014916240907944 -3.0766780771021436
+v 3.0766780771021436 1.1751864530113494 1.9014916240907944
+v 3.0766780771021436 1.1751864530113494 -1.9014916240907944
+v 3.0766780771021436 -1.1751864530113494 1.9014916240907944
+v 3.0766780771021436 -1.1751864530113494 -1.9014916240907944
+v -3.0766780771021436 1.1751864530113494 1.9014916240907944
+v -3.0766780771021436 1.1751864530113494 -1.9014916240907944
+v -3.0766780771021436 -1.1751864530113494 1.9014916240907944
+v -3.0766780771021436 -1.1751864530113494 -1.9014916240907944
+v 1.9014916240907944 3.0766780771021436 1.1751864530113494
+v 1.9014916240907944 3.0766780771021436 -1.1751864530113494
+v 1.9014916240907944 -3.0766780771021436 1.1751864530113494
+v 1.9014916240907944 -3.0766780771021436 -1.1751864530113494
+v -1.9014916240907944 3.0766780771021436 1.1751864530113494
+v -1.9014916240907944 3.0766780771021436 -1.1751864530113494
+v -1.9014916240907944 -3.0766780771021436 1.1751864530113494
+v -1.9014916240907944 -3.0766780771021436 -1.1751864530113494
+v 2.23606797749979 2.23606797749979 2.23606797749979
+v 2.23606797749979 2.23606797749979 -2.23606797749979
+v 2.23606797749979 -2.23606797749979 2.23606797749979
+v 2.23606797749979 -2.23606797749979 -2.23606797749979
+v -2.23606797749979 2.23606797749979 2.23606797749979
+v -2.23606797749979 2.23606797749979 -2.23606797749979
+v -2.23606797749979 -2.23606797749979 2.23606797749979
+v -2.23606797749979 -2.23606797749979 -2.23606797749979
+f 19 1 9
+f 19 9 33
+f 19 33 57
+f 19 57 41
+f 19 41 11
+f 19 11 39
+f 19 39 55
+f 19 55 31
+f 19 31 7
+f 19 7 1
+f 20 2 8
+f 20 8 32
+f 20 32 56
+f 20 56 40
+f 20 40 12
+f 20 12 42
+f 20 42 58
+f 20 58 34
+f 20 34 10
+f 20 10 2
+f 21 1 7
+f 21 7 35
+f 21 35 59
+f 21 59 43
+f 21 43 13
+f 21 13 45
+f 21 45 61
+f 21 61 37
+f 21 37 9
+f 21 9 1
+f 22 2 10
+f 22 10 38
+f 22 38 62
+f 22 62 46
+f 22 46 14
+f 22 14 44
+f 22 44 60
+f 22 60 36
+f 22 36 8
+f 22 8 2
+f 23 3 12
+f 23 12 40
+f 23 40 56
+f 23 56 48
+f 23 48 15
+f 23 15 47
+f 23 47 55
+f 23 55 39
+f 23 39 11
+f 23 11 3
+f 24 3 11
+f 24 11 41
+f 24 41 57
+f 24 57 49
+f 24 49 16
+f 24 16 50
+f 24 50 58
+f 24 58 42
+f 24 42 12
+f 24 12 3
+f 25 4 13
+f 25 13 43
+f 25 43 59
+f 25 59 51
+f 25 51 17
+f 25 17 52
+f 25 52 60
+f 25 60 44
+f 25 44 14
+f 25 14 4
+f 26 4 14
+f 26 14 46
+f 26 46 62
+f 26 62 54
+f 26 54 18
+f 26 18 53
+f 26 53 61
+f 26 61 45
+f 26 45 13
+f 26 13 4
+f 27 5 17
+f 27 17 51
+f 27 51 59
+f 27 59 35
+f 27 35 7
+f 27 7 31
+f 27 31 55
+f 27 55 47
+f 27 47 15
+f 27 15 5
+f 28 5 15
+f 28 15 48
+f 28 48 56
+f 28 56 32
+f 28 32 8
+f 28 8 36
+f 28 36 60
+f 28 60 52
+f 28 52 17
+f 28 17 5
+f 29 6 16
+f 29 16 49
+f 29 49 57
+f 29 57 33
+f 29 33 9
+f 29 9 37
+f 29 37 61
+f 29 61 53
+f 29 53 18
+f 29 18 6
+f 30 6 18
+f 30 18 54
+f 30 54 62
+f 30 62 38
+f 30 38 10
+f 30 10 34
+f 30 34 58
+f 30 58 50
+f 30 50 16
+f 30 16 6
diff --git a/webapp/data/Dodecahedron.obj b/webapp/data/Dodecahedron.obj
new file mode 100644
index 0000000..764a56d
--- /dev/null
+++ b/webapp/data/Dodecahedron.obj
@@ -0,0 +1,32 @@
+v 0.0 0.5 1.3090169943749475
+v 0.0 0.5 -1.3090169943749475
+v 0.0 -0.5 1.3090169943749475
+v 0.0 -0.5 -1.3090169943749475
+v 1.3090169943749475 0.0 0.5
+v 1.3090169943749475 0.0 -0.5
+v -1.3090169943749475 0.0 0.5
+v -1.3090169943749475 0.0 -0.5
+v 0.5 1.3090169943749475 0.0
+v 0.5 -1.3090169943749475 0.0
+v -0.5 1.3090169943749475 0.0
+v -0.5 -1.3090169943749475 0.0
+v 0.8090169943749475 0.8090169943749475 0.8090169943749475
+v 0.8090169943749475 0.8090169943749475 -0.8090169943749475
+v 0.8090169943749475 -0.8090169943749475 0.8090169943749475
+v 0.8090169943749475 -0.8090169943749475 -0.8090169943749475
+v -0.8090169943749475 0.8090169943749475 0.8090169943749475
+v -0.8090169943749475 0.8090169943749475 -0.8090169943749475
+v -0.8090169943749475 -0.8090169943749475 0.8090169943749475
+v -0.8090169943749475 -0.8090169943749475 -0.8090169943749475
+f 1 3 15 5 13
+f 1 13 9 11 17
+f 1 17 7 19 3
+f 8 7 17 11 18
+f 8 18 2 4 20
+f 8 20 12 19 7
+f 10 12 20 4 16
+f 10 16 6 5 15
+f 10 15 3 19 12
+f 14 2 18 11 9
+f 14 9 13 5 6
+f 14 6 16 4 2
diff --git a/webapp/data/GreatDodecahedron.obj b/webapp/data/GreatDodecahedron.obj
new file mode 100644
index 0000000..a9950a9
--- /dev/null
+++ b/webapp/data/GreatDodecahedron.obj
@@ -0,0 +1,24 @@
+v 0.5 0.0 0.8090169943749475
+v 0.5 0.0 -0.8090169943749475
+v -0.5 0.0 0.8090169943749475
+v -0.5 0.0 -0.8090169943749475
+v 0.8090169943749475 0.5 0.0
+v 0.8090169943749475 -0.5 0.0
+v -0.8090169943749475 0.5 0.0
+v -0.8090169943749475 -0.5 0.0
+v 0.0 0.8090169943749475 0.5
+v 0.0 0.8090169943749475 -0.5
+v 0.0 -0.8090169943749475 0.5
+v 0.0 -0.8090169943749475 -0.5
+f 1 3 8 12 6
+f 1 6 2 10 9
+f 1 9 7 8 11
+f 2 4 7 9 5
+f 2 5 1 11 12
+f 2 12 8 7 10
+f 3 1 5 10 7
+f 3 7 4 12 11
+f 3 11 6 5 9
+f 4 2 6 11 8
+f 4 8 3 9 10
+f 4 10 5 6 12
diff --git a/webapp/data/GreatIcosahedron.obj b/webapp/data/GreatIcosahedron.obj
new file mode 100644
index 0000000..78cf395
--- /dev/null
+++ b/webapp/data/GreatIcosahedron.obj
@@ -0,0 +1,32 @@
+v 0.0 -0.5 0.30901699437494745
+v 0.0 -0.5 -0.30901699437494745
+v 0.0 0.5 0.30901699437494745
+v 0.0 0.5 -0.30901699437494745
+v -0.5 0.30901699437494745 0.0
+v 0.5 0.30901699437494745 0.0
+v -0.5 -0.30901699437494745 0.0
+v 0.5 -0.30901699437494745 0.0
+v 0.30901699437494745 0.0 -0.5
+v 0.30901699437494745 0.0 0.5
+v -0.30901699437494745 0.0 -0.5
+v -0.30901699437494745 0.0 0.5
+f 1 3 11
+f 1 11 6
+f 1 6 5
+f 1 5 9
+f 1 9 3
+f 4 2 12
+f 4 12 8
+f 4 8 7
+f 4 7 10
+f 4 10 2
+f 3 7 8
+f 3 8 11
+f 11 8 12
+f 11 12 6
+f 6 12 2
+f 6 2 5
+f 5 2 10
+f 5 10 9
+f 9 10 7
+f 9 7 3
diff --git a/webapp/data/GreatStellatedDodecahedron.obj b/webapp/data/GreatStellatedDodecahedron.obj
new file mode 100644
index 0000000..1453218
--- /dev/null
+++ b/webapp/data/GreatStellatedDodecahedron.obj
@@ -0,0 +1,32 @@
+v 0.5 0.0 0.19098300562505258
+v 0.5 0.0 -0.19098300562505258
+v -0.5 0.0 0.19098300562505258
+v -0.5 0.0 -0.19098300562505258
+v 0.0 0.19098300562505258 0.5
+v 0.0 0.19098300562505258 -0.5
+v 0.0 -0.19098300562505258 0.5
+v 0.0 -0.19098300562505258 -0.5
+v 0.19098300562505258 0.5 0.0
+v -0.19098300562505258 0.5 0.0
+v 0.19098300562505258 -0.5 0.0
+v -0.19098300562505258 -0.5 0.0
+v -0.30901699437494745 -0.30901699437494745 -0.30901699437494745
+v -0.30901699437494745 -0.30901699437494745 0.30901699437494745
+v 0.30901699437494745 -0.30901699437494745 -0.30901699437494745
+v 0.30901699437494745 -0.30901699437494745 0.30901699437494745
+v -0.30901699437494745 0.30901699437494745 -0.30901699437494745
+v -0.30901699437494745 0.30901699437494745 0.30901699437494745
+v 0.30901699437494745 0.30901699437494745 -0.30901699437494745
+v 0.30901699437494745 0.30901699437494745 0.30901699437494745
+f 1 3 15 5 13
+f 1 13 9 11 17
+f 1 17 7 19 3
+f 8 7 17 11 18
+f 8 18 2 4 20
+f 8 20 12 19 7
+f 10 12 20 4 16
+f 10 16 6 5 15
+f 10 15 3 19 12
+f 14 2 18 11 9
+f 14 9 13 5 6
+f 14 6 16 4 2
diff --git a/webapp/data/Icosahedron.obj b/webapp/data/Icosahedron.obj
new file mode 100644
index 0000000..88d44f4
--- /dev/null
+++ b/webapp/data/Icosahedron.obj
@@ -0,0 +1,32 @@
+v 0.5 0.0 0.8090169943749475
+v 0.5 0.0 -0.8090169943749475
+v -0.5 0.0 0.8090169943749475
+v -0.5 0.0 -0.8090169943749475
+v 0.8090169943749475 0.5 0.0
+v 0.8090169943749475 -0.5 0.0
+v -0.8090169943749475 0.5 0.0
+v -0.8090169943749475 -0.5 0.0
+v 0.0 0.8090169943749475 0.5
+v 0.0 0.8090169943749475 -0.5
+v 0.0 -0.8090169943749475 0.5
+v 0.0 -0.8090169943749475 -0.5
+f 1 3 11
+f 1 11 6
+f 1 6 5
+f 1 5 9
+f 1 9 3
+f 4 2 12
+f 4 12 8
+f 4 8 7
+f 4 7 10
+f 4 10 2
+f 3 7 8
+f 3 8 11
+f 11 8 12
+f 11 12 6
+f 6 12 2
+f 6 2 5
+f 5 2 10
+f 5 10 9
+f 9 10 7
+f 9 7 3
diff --git a/webapp/data/Icosidodecahedron.obj b/webapp/data/Icosidodecahedron.obj
new file mode 100644
index 0000000..1669844
--- /dev/null
+++ b/webapp/data/Icosidodecahedron.obj
@@ -0,0 +1,62 @@
+v 0.0 0.0 1.618033988749895
+v 0.0 0.0 -1.618033988749895
+v 1.618033988749895 0.0 0.0
+v -1.618033988749895 0.0 0.0
+v 0.0 1.618033988749895 0.0
+v 0.0 -1.618033988749895 0.0
+v 0.5 0.8090169943749475 1.3090169943749475
+v 0.5 0.8090169943749475 -1.3090169943749475
+v 0.5 -0.8090169943749475 1.3090169943749475
+v 0.5 -0.8090169943749475 -1.3090169943749475
+v -0.5 0.8090169943749475 1.3090169943749475
+v -0.5 0.8090169943749475 -1.3090169943749475
+v -0.5 -0.8090169943749475 1.3090169943749475
+v -0.5 -0.8090169943749475 -1.3090169943749475
+v 1.3090169943749475 0.5 0.8090169943749475
+v 1.3090169943749475 0.5 -0.8090169943749475
+v 1.3090169943749475 -0.5 0.8090169943749475
+v 1.3090169943749475 -0.5 -0.8090169943749475
+v -1.3090169943749475 0.5 0.8090169943749475
+v -1.3090169943749475 0.5 -0.8090169943749475
+v -1.3090169943749475 -0.5 0.8090169943749475
+v -1.3090169943749475 -0.5 -0.8090169943749475
+v 0.8090169943749475 1.3090169943749475 0.5
+v 0.8090169943749475 1.3090169943749475 -0.5
+v 0.8090169943749475 -1.3090169943749475 0.5
+v 0.8090169943749475 -1.3090169943749475 -0.5
+v -0.8090169943749475 1.3090169943749475 0.5
+v -0.8090169943749475 1.3090169943749475 -0.5
+v -0.8090169943749475 -1.3090169943749475 0.5
+v -0.8090169943749475 -1.3090169943749475 -0.5
+f 1 9 17 15 7
+f 1 11 19 21 13
+f 2 8 16 18 10
+f 2 14 22 20 12
+f 3 16 24 23 15
+f 3 17 25 26 18
+f 4 19 27 28 20
+f 4 22 30 29 21
+f 5 24 8 12 28
+f 5 27 11 7 23
+f 6 25 9 13 29
+f 6 30 14 10 26
+f 1 7 11
+f 1 13 9
+f 2 10 14
+f 2 12 8
+f 3 15 17
+f 3 18 16
+f 4 20 22
+f 4 21 19
+f 5 23 24
+f 5 28 27
+f 6 26 25
+f 6 29 30
+f 7 15 23
+f 8 24 16
+f 9 25 17
+f 10 18 26
+f 11 27 19
+f 12 20 28
+f 13 21 29
+f 14 30 22
diff --git a/webapp/data/LpentagonalHexecontahedron.obj b/webapp/data/LpentagonalHexecontahedron.obj
new file mode 100644
index 0000000..6a6f82c
--- /dev/null
+++ b/webapp/data/LpentagonalHexecontahedron.obj
@@ -0,0 +1,152 @@
+v -0.19289371135235903 -0.21848337012732122 -2.0970538352520878
+v -0.19289371135235903 0.21848337012732122 2.0970538352520878
+v 0.19289371135235903 0.21848337012732122 -2.0970538352520878
+v 0.19289371135235903 -0.21848337012732122 2.0970538352520878
+v -2.0970538352520878 -0.19289371135235903 -0.21848337012732122
+v -2.0970538352520878 0.19289371135235903 0.21848337012732122
+v 2.0970538352520878 0.19289371135235903 -0.21848337012732122
+v 2.0970538352520878 -0.19289371135235903 0.21848337012732122
+v -0.21848337012732122 -2.0970538352520878 -0.19289371135235903
+v -0.21848337012732122 2.0970538352520878 0.19289371135235903
+v 0.21848337012732122 2.0970538352520878 -0.19289371135235903
+v 0.21848337012732122 -2.0970538352520878 0.19289371135235903
+v 0.0 -0.7554672605165956 -1.9778389654202186
+v 0.0 -0.7554672605165956 1.9778389654202186
+v 0.0 0.7554672605165956 -1.9778389654202186
+v 0.0 0.7554672605165956 1.9778389654202186
+v -1.9778389654202186 0.0 -0.7554672605165956
+v -1.9778389654202186 0.0 0.7554672605165956
+v 1.9778389654202186 0.0 -0.7554672605165956
+v 1.9778389654202186 0.0 0.7554672605165956
+v -0.7554672605165956 -1.9778389654202186 0.0
+v -0.7554672605165956 1.9778389654202186 0.0
+v 0.7554672605165956 -1.9778389654202186 0.0
+v 0.7554672605165956 1.9778389654202186 0.0
+v -1.167123436475334 0.0 -1.8884453892836692
+v -1.167123436475334 0.0 1.8884453892836692
+v 1.167123436475334 0.0 -1.8884453892836692
+v 1.167123436475334 0.0 1.8884453892836692
+v -1.8884453892836692 -1.167123436475334 0.0
+v -1.8884453892836692 1.167123436475334 0.0
+v 1.8884453892836692 -1.167123436475334 0.0
+v 1.8884453892836692 1.167123436475334 0.0
+v 0.0 -1.8884453892836692 -1.167123436475334
+v 0.0 -1.8884453892836692 1.167123436475334
+v 0.0 1.8884453892836692 -1.167123436475334
+v 0.0 1.8884453892836692 1.167123436475334
+v -0.5677153694669214 0.8249575526762758 -1.8654013108176957
+v -0.5677153694669214 -0.8249575526762758 1.8654013108176957
+v 0.5677153694669214 -0.8249575526762758 -1.8654013108176957
+v 0.5677153694669214 0.8249575526762758 1.8654013108176957
+v -1.8654013108176957 0.5677153694669214 -0.8249575526762758
+v -1.8654013108176957 -0.5677153694669214 0.8249575526762758
+v 1.8654013108176957 -0.5677153694669214 -0.8249575526762758
+v 1.8654013108176957 0.5677153694669214 0.8249575526762758
+v -0.8249575526762758 1.8654013108176957 -0.5677153694669214
+v -0.8249575526762758 -1.8654013108176957 0.5677153694669214
+v 0.8249575526762758 -1.8654013108176957 -0.5677153694669214
+v 0.8249575526762758 1.8654013108176957 0.5677153694669214
+v -0.3748216581145623 -1.1370661338605041 -1.7461864409858263
+v -0.3748216581145623 1.1370661338605041 1.7461864409858263
+v 0.3748216581145623 1.1370661338605041 -1.7461864409858263
+v 0.3748216581145623 -1.1370661338605041 1.7461864409858263
+v -1.7461864409858263 -0.3748216581145623 -1.1370661338605041
+v -1.7461864409858263 0.3748216581145623 1.1370661338605041
+v 1.7461864409858263 0.3748216581145623 -1.1370661338605041
+v 1.7461864409858263 -0.3748216581145623 1.1370661338605041
+v -1.1370661338605041 -1.7461864409858263 -0.3748216581145623
+v -1.1370661338605041 1.7461864409858263 0.3748216581145623
+v 1.1370661338605041 1.7461864409858263 -0.3748216581145623
+v 1.1370661338605041 -1.7461864409858263 0.3748216581145623
+v -0.9212288883095505 -0.9599877013915838 -1.6469179406903744
+v -0.9212288883095505 0.9599877013915838 1.6469179406903744
+v 0.9212288883095505 0.9599877013915838 -1.6469179406903744
+v 0.9212288883095505 -0.9599877013915838 1.6469179406903744
+v -1.6469179406903744 -0.9212288883095505 -0.9599877013915838
+v -1.6469179406903744 0.9212288883095505 0.9599877013915838
+v 1.6469179406903744 0.9212288883095505 -0.9599877013915838
+v 1.6469179406903744 -0.9212288883095505 0.9599877013915838
+v -0.9599877013915838 -1.6469179406903744 -0.9212288883095505
+v -0.9599877013915838 1.6469179406903744 0.9212288883095505
+v 0.9599877013915838 1.6469179406903744 -0.9212288883095505
+v 0.9599877013915838 -1.6469179406903744 0.9212288883095505
+v -0.7283351769571915 1.2720962825758122 -1.527703070858505
+v -0.7283351769571915 -1.2720962825758122 1.527703070858505
+v 0.7283351769571915 -1.2720962825758122 -1.527703070858505
+v 0.7283351769571915 1.2720962825758122 1.527703070858505
+v -1.527703070858505 0.7283351769571915 -1.2720962825758122
+v -1.527703070858505 -0.7283351769571915 1.2720962825758122
+v 1.527703070858505 -0.7283351769571915 -1.2720962825758122
+v 1.527703070858505 0.7283351769571915 1.2720962825758122
+v -1.2720962825758122 1.527703070858505 -0.7283351769571915
+v -1.2720962825758122 -1.527703070858505 0.7283351769571915
+v 1.2720962825758122 -1.527703070858505 -0.7283351769571915
+v 1.2720962825758122 1.527703070858505 0.7283351769571915
+v -1.2223717049036231 -1.2223717049036231 -1.2223717049036231
+v -1.2223717049036231 -1.2223717049036231 1.2223717049036231
+v -1.2223717049036231 1.2223717049036231 -1.2223717049036231
+v -1.2223717049036231 1.2223717049036231 1.2223717049036231
+v 1.2223717049036231 -1.2223717049036231 -1.2223717049036231
+v 1.2223717049036231 -1.2223717049036231 1.2223717049036231
+v 1.2223717049036231 1.2223717049036231 -1.2223717049036231
+v 1.2223717049036231 1.2223717049036231 1.2223717049036231
+f 25 37 15 3 1
+f 25 77 87 73 37
+f 25 53 17 41 77
+f 25 61 85 65 53
+f 25 1 13 49 61
+f 26 38 14 4 2
+f 26 78 86 74 38
+f 26 54 18 42 78
+f 26 62 88 66 54
+f 26 2 16 50 62
+f 27 39 13 1 3
+f 27 79 89 75 39
+f 27 55 19 43 79
+f 27 63 91 67 55
+f 27 3 15 51 63
+f 28 40 16 2 4
+f 28 80 92 76 40
+f 28 56 20 44 80
+f 28 64 90 68 56
+f 28 4 14 52 64
+f 29 42 18 6 5
+f 29 82 86 78 42
+f 29 57 21 46 82
+f 29 65 85 69 57
+f 29 5 17 53 65
+f 30 41 17 5 6
+f 30 81 87 77 41
+f 30 58 22 45 81
+f 30 66 88 70 58
+f 30 6 18 54 66
+f 31 43 19 7 8
+f 31 83 89 79 43
+f 31 60 23 47 83
+f 31 68 90 72 60
+f 31 8 20 56 68
+f 32 44 20 8 7
+f 32 84 92 80 44
+f 32 59 24 48 84
+f 32 67 91 71 59
+f 32 7 19 55 67
+f 33 47 23 12 9
+f 33 75 89 83 47
+f 33 49 13 39 75
+f 33 69 85 61 49
+f 33 9 21 57 69
+f 34 46 21 9 12
+f 34 74 86 82 46
+f 34 52 14 38 74
+f 34 72 90 64 52
+f 34 12 23 60 72
+f 35 45 22 10 11
+f 35 73 87 81 45
+f 35 51 15 37 73
+f 35 71 91 63 51
+f 35 11 24 59 71
+f 36 48 24 11 10
+f 36 76 92 84 48
+f 36 50 16 40 76
+f 36 70 88 62 50
+f 36 10 22 58 70
diff --git a/webapp/data/LpentagonalIcositetrahedron.obj b/webapp/data/LpentagonalIcositetrahedron.obj
new file mode 100644
index 0000000..fe0acc3
--- /dev/null
+++ b/webapp/data/LpentagonalIcositetrahedron.obj
@@ -0,0 +1,62 @@
+v 0.0 0.0 -1.3614101519264425
+v 0.0 0.0 1.3614101519264425
+v -1.3614101519264425 0.0 0.0
+v 1.3614101519264425 0.0 0.0
+v 0.0 -1.3614101519264425 0.0
+v 0.0 1.3614101519264425 0.0
+v -0.7401837413698572 0.21879664300048043 -1.0236561781126903
+v -0.7401837413698572 -0.21879664300048043 1.0236561781126903
+v 0.7401837413698572 -0.21879664300048043 -1.0236561781126903
+v 0.7401837413698572 0.21879664300048043 1.0236561781126903
+v -1.0236561781126903 0.7401837413698572 -0.21879664300048043
+v -1.0236561781126903 -0.7401837413698572 0.21879664300048043
+v 1.0236561781126903 -0.7401837413698572 -0.21879664300048043
+v 1.0236561781126903 0.7401837413698572 0.21879664300048043
+v -0.21879664300048043 1.0236561781126903 -0.7401837413698572
+v -0.21879664300048043 -1.0236561781126903 0.7401837413698572
+v 0.21879664300048043 -1.0236561781126903 -0.7401837413698572
+v 0.21879664300048043 1.0236561781126903 0.7401837413698572
+v -0.21879664300048043 -0.7401837413698572 -1.0236561781126903
+v -0.21879664300048043 0.7401837413698572 1.0236561781126903
+v 0.21879664300048043 0.7401837413698572 -1.0236561781126903
+v 0.21879664300048043 -0.7401837413698572 1.0236561781126903
+v -1.0236561781126903 -0.21879664300048043 -0.7401837413698572
+v -1.0236561781126903 0.21879664300048043 0.7401837413698572
+v 1.0236561781126903 0.21879664300048043 -0.7401837413698572
+v 1.0236561781126903 -0.21879664300048043 0.7401837413698572
+v -0.7401837413698572 -1.0236561781126903 -0.21879664300048043
+v -0.7401837413698572 1.0236561781126903 0.21879664300048043
+v 0.7401837413698572 1.0236561781126903 -0.21879664300048043
+v 0.7401837413698572 -1.0236561781126903 0.21879664300048043
+v -0.7401837413698572 -0.7401837413698572 -0.7401837413698572
+v -0.7401837413698572 -0.7401837413698572 0.7401837413698572
+v -0.7401837413698572 0.7401837413698572 -0.7401837413698572
+v -0.7401837413698572 0.7401837413698572 0.7401837413698572
+v 0.7401837413698572 -0.7401837413698572 -0.7401837413698572
+v 0.7401837413698572 -0.7401837413698572 0.7401837413698572
+v 0.7401837413698572 0.7401837413698572 -0.7401837413698572
+v 0.7401837413698572 0.7401837413698572 0.7401837413698572
+f 1 19 31 23 7
+f 1 9 35 17 19
+f 1 21 37 25 9
+f 1 7 33 15 21
+f 2 20 34 24 8
+f 2 10 38 18 20
+f 2 22 36 26 10
+f 2 8 32 16 22
+f 3 24 34 28 11
+f 3 12 32 8 24
+f 3 23 31 27 12
+f 3 11 33 7 23
+f 4 26 36 30 13
+f 4 14 38 10 26
+f 4 25 37 29 14
+f 4 13 35 9 25
+f 5 30 36 22 16
+f 5 17 35 13 30
+f 5 27 31 19 17
+f 5 16 32 12 27
+f 6 29 37 21 15
+f 6 18 38 14 29
+f 6 28 34 20 18
+f 6 15 33 11 28
diff --git a/webapp/data/LsnubCube.obj b/webapp/data/LsnubCube.obj
new file mode 100644
index 0000000..0569f49
--- /dev/null
+++ b/webapp/data/LsnubCube.obj
@@ -0,0 +1,62 @@
+v 0.6212264105565853 0.33775397381375233 1.1426135089259621
+v 0.6212264105565853 -0.33775397381375233 -1.1426135089259621
+v -0.6212264105565853 -0.33775397381375233 1.1426135089259621
+v -0.6212264105565853 0.33775397381375233 -1.1426135089259621
+v 1.1426135089259621 0.6212264105565853 0.33775397381375233
+v 1.1426135089259621 -0.6212264105565853 -0.33775397381375233
+v -1.1426135089259621 -0.6212264105565853 0.33775397381375233
+v -1.1426135089259621 0.6212264105565853 -0.33775397381375233
+v 0.33775397381375233 1.1426135089259621 0.6212264105565853
+v 0.33775397381375233 -1.1426135089259621 -0.6212264105565853
+v -0.33775397381375233 -1.1426135089259621 0.6212264105565853
+v -0.33775397381375233 1.1426135089259621 -0.6212264105565853
+v 0.33775397381375233 -0.6212264105565853 1.1426135089259621
+v 0.33775397381375233 0.6212264105565853 -1.1426135089259621
+v -0.33775397381375233 0.6212264105565853 1.1426135089259621
+v -0.33775397381375233 -0.6212264105565853 -1.1426135089259621
+v 1.1426135089259621 -0.33775397381375233 0.6212264105565853
+v 1.1426135089259621 0.33775397381375233 -0.6212264105565853
+v -1.1426135089259621 0.33775397381375233 0.6212264105565853
+v -1.1426135089259621 -0.33775397381375233 -0.6212264105565853
+v 0.6212264105565853 -1.1426135089259621 0.33775397381375233
+v 0.6212264105565853 1.1426135089259621 -0.33775397381375233
+v -0.6212264105565853 1.1426135089259621 0.33775397381375233
+v -0.6212264105565853 -1.1426135089259621 -0.33775397381375233
+f 3 13 1 15
+f 4 14 2 16
+f 5 17 6 18
+f 8 20 7 19
+f 9 22 12 23
+f 10 21 11 24
+f 1 9 15
+f 2 10 16
+f 3 11 13
+f 4 12 14
+f 5 1 17
+f 6 2 18
+f 7 3 19
+f 8 4 20
+f 9 5 22
+f 10 6 21
+f 11 7 24
+f 12 8 23
+f 13 17 1
+f 14 18 2
+f 15 19 3
+f 16 20 4
+f 17 21 6
+f 18 22 5
+f 19 23 8
+f 20 24 7
+f 21 13 11
+f 22 14 12
+f 23 15 9
+f 24 16 10
+f 9 1 5
+f 10 2 6
+f 11 3 7
+f 12 4 8
+f 13 21 17
+f 14 22 18
+f 15 23 19
+f 16 24 20
diff --git a/webapp/data/LsnubDodecahedron.obj b/webapp/data/LsnubDodecahedron.obj
new file mode 100644
index 0000000..c33da1b
--- /dev/null
+++ b/webapp/data/LsnubDodecahedron.obj
@@ -0,0 +1,152 @@
+v 0.3748216581145623 -0.33092102472984425 2.0970538352520878
+v 0.3748216581145623 0.33092102472984425 -2.0970538352520878
+v -0.3748216581145623 0.33092102472984425 2.0970538352520878
+v -0.3748216581145623 -0.33092102472984425 -2.0970538352520878
+v 2.0970538352520878 -0.3748216581145623 0.33092102472984425
+v 2.0970538352520878 0.3748216581145623 -0.33092102472984425
+v -2.0970538352520878 0.3748216581145623 0.33092102472984425
+v -2.0970538352520878 -0.3748216581145623 -0.33092102472984425
+v 0.33092102472984425 -2.0970538352520878 0.3748216581145623
+v 0.33092102472984425 2.0970538352520878 -0.3748216581145623
+v -0.33092102472984425 2.0970538352520878 0.3748216581145623
+v -0.33092102472984425 -2.0970538352520878 -0.3748216581145623
+v 0.5677153694669214 0.6430296059140725 1.9778389654202186
+v 0.5677153694669214 -0.6430296059140725 -1.9778389654202186
+v -0.5677153694669214 -0.6430296059140725 1.9778389654202186
+v -0.5677153694669214 0.6430296059140725 -1.9778389654202186
+v 1.9778389654202186 0.5677153694669214 0.6430296059140725
+v 1.9778389654202186 -0.5677153694669214 -0.6430296059140725
+v -1.9778389654202186 -0.5677153694669214 0.6430296059140725
+v -1.9778389654202186 0.5677153694669214 -0.6430296059140725
+v 0.6430296059140725 1.9778389654202186 0.5677153694669214
+v 0.6430296059140725 -1.9778389654202186 -0.5677153694669214
+v -0.6430296059140725 -1.9778389654202186 0.5677153694669214
+v -0.6430296059140725 1.9778389654202186 -0.5677153694669214
+v 0.19289371135235903 -1.2495037884630271 1.7461864409858263
+v 0.19289371135235903 1.2495037884630271 -1.7461864409858263
+v -0.19289371135235903 1.2495037884630271 1.7461864409858263
+v -0.19289371135235903 -1.2495037884630271 -1.7461864409858263
+v 1.7461864409858263 -0.19289371135235903 1.2495037884630271
+v 1.7461864409858263 0.19289371135235903 -1.2495037884630271
+v -1.7461864409858263 0.19289371135235903 1.2495037884630271
+v -1.7461864409858263 -0.19289371135235903 -1.2495037884630271
+v 1.2495037884630271 -1.7461864409858263 0.19289371135235903
+v 1.2495037884630271 1.7461864409858263 -0.19289371135235903
+v -1.2495037884630271 1.7461864409858263 0.19289371135235903
+v -1.2495037884630271 -1.7461864409858263 -0.19289371135235903
+v 1.1031568350717538 -0.8475500467890608 1.6469179406903744
+v 1.1031568350717538 0.8475500467890608 -1.6469179406903744
+v -1.1031568350717538 0.8475500467890608 1.6469179406903744
+v -1.1031568350717538 -0.8475500467890608 -1.6469179406903744
+v 1.6469179406903744 -1.1031568350717538 0.8475500467890608
+v 1.6469179406903744 1.1031568350717538 -0.8475500467890608
+v -1.6469179406903744 1.1031568350717538 0.8475500467890608
+v -1.6469179406903744 -1.1031568350717538 -0.8475500467890608
+v 0.8475500467890608 -1.6469179406903744 1.1031568350717538
+v 0.8475500467890608 1.6469179406903744 -1.1031568350717538
+v -0.8475500467890608 1.6469179406903744 1.1031568350717538
+v -0.8475500467890608 -1.6469179406903744 -1.1031568350717538
+v 1.4152654162559821 0.7283351769571915 1.4540242293380155
+v 1.4152654162559821 -0.7283351769571915 -1.4540242293380155
+v -1.4152654162559821 -0.7283351769571915 1.4540242293380155
+v -1.4152654162559821 0.7283351769571915 -1.4540242293380155
+v 1.4540242293380155 1.4152654162559821 0.7283351769571915
+v 1.4540242293380155 -1.4152654162559821 -0.7283351769571915
+v -1.4540242293380155 -1.4152654162559821 0.7283351769571915
+v -1.4540242293380155 1.4152654162559821 -0.7283351769571915
+v 0.7283351769571915 1.4540242293380155 1.4152654162559821
+v 0.7283351769571915 -1.4540242293380155 -1.4152654162559821
+v -0.7283351769571915 -1.4540242293380155 1.4152654162559821
+v -0.7283351769571915 1.4540242293380155 -1.4152654162559821
+f 1 37 29 49 13
+f 2 38 30 50 14
+f 3 39 31 51 15
+f 4 40 32 52 16
+f 5 41 33 54 18
+f 6 42 34 53 17
+f 7 43 35 56 20
+f 8 44 36 55 19
+f 9 45 25 59 23
+f 10 46 26 60 24
+f 11 47 27 57 21
+f 12 48 28 58 22
+f 1 3 15
+f 2 4 16
+f 3 1 13
+f 4 2 14
+f 5 6 17
+f 6 5 18
+f 7 8 19
+f 8 7 20
+f 9 12 22
+f 10 11 21
+f 11 10 24
+f 12 9 23
+f 13 49 57
+f 14 50 58
+f 15 51 59
+f 16 52 60
+f 17 53 49
+f 18 54 50
+f 19 55 51
+f 20 56 52
+f 21 57 53
+f 22 58 54
+f 23 59 55
+f 24 60 56
+f 25 45 37
+f 26 46 38
+f 27 47 39
+f 28 48 40
+f 29 37 41
+f 30 38 42
+f 31 39 43
+f 32 40 44
+f 33 41 45
+f 34 42 46
+f 35 43 47
+f 36 44 48
+f 37 1 25
+f 38 2 26
+f 39 3 27
+f 40 4 28
+f 41 5 29
+f 42 6 30
+f 43 7 31
+f 44 8 32
+f 45 9 33
+f 46 10 34
+f 47 11 35
+f 48 12 36
+f 49 29 17
+f 50 30 18
+f 51 31 19
+f 52 32 20
+f 53 34 21
+f 54 33 22
+f 55 36 23
+f 56 35 24
+f 57 27 13
+f 58 28 14
+f 59 25 15
+f 60 26 16
+f 25 1 15
+f 26 2 16
+f 27 3 13
+f 28 4 14
+f 29 5 17
+f 30 6 18
+f 31 7 19
+f 32 8 20
+f 33 9 22
+f 34 10 21
+f 35 11 24
+f 36 12 23
+f 37 45 41
+f 38 46 42
+f 39 47 43
+f 40 48 44
+f 49 53 57
+f 50 54 58
+f 51 55 59
+f 52 56 60
diff --git a/webapp/data/Octahedron.obj b/webapp/data/Octahedron.obj
new file mode 100644
index 0000000..72a451c
--- /dev/null
+++ b/webapp/data/Octahedron.obj
@@ -0,0 +1,14 @@
+v 0.0 0.0 0.7071067811865476
+v 0.0 0.0 -0.7071067811865476
+v 0.7071067811865476 0.0 0.0
+v -0.7071067811865476 0.0 0.0
+v 0.0 0.7071067811865476 0.0
+v 0.0 -0.7071067811865476 0.0
+f 1 3 5
+f 1 5 4
+f 1 4 6
+f 1 6 3
+f 2 3 6
+f 2 6 4
+f 2 4 5
+f 2 5 3
diff --git a/webapp/data/PentakisDodecahedron.obj b/webapp/data/PentakisDodecahedron.obj
new file mode 100644
index 0000000..c127642
--- /dev/null
+++ b/webapp/data/PentakisDodecahedron.obj
@@ -0,0 +1,92 @@
+v 0.0 0.9270509831248422 2.4270509831248424
+v 0.0 0.9270509831248422 -2.4270509831248424
+v 0.0 -0.9270509831248422 2.4270509831248424
+v 0.0 -0.9270509831248422 -2.4270509831248424
+v 2.4270509831248424 0.0 0.9270509831248422
+v 2.4270509831248424 0.0 -0.9270509831248422
+v -2.4270509831248424 0.0 0.9270509831248422
+v -2.4270509831248424 0.0 -0.9270509831248422
+v 0.9270509831248422 2.4270509831248424 0.0
+v 0.9270509831248422 -2.4270509831248424 0.0
+v -0.9270509831248422 2.4270509831248424 0.0
+v -0.9270509831248422 -2.4270509831248424 0.0
+v 1.3305869973355013 0.0 2.152934986677507
+v 1.3305869973355013 0.0 -2.152934986677507
+v -1.3305869973355013 0.0 2.152934986677507
+v -1.3305869973355013 0.0 -2.152934986677507
+v 2.152934986677507 1.3305869973355013 0.0
+v 2.152934986677507 -1.3305869973355013 0.0
+v -2.152934986677507 1.3305869973355013 0.0
+v -2.152934986677507 -1.3305869973355013 0.0
+v 0.0 2.152934986677507 1.3305869973355013
+v 0.0 2.152934986677507 -1.3305869973355013
+v 0.0 -2.152934986677507 1.3305869973355013
+v 0.0 -2.152934986677507 -1.3305869973355013
+v 1.5 1.5 1.5
+v 1.5 1.5 -1.5
+v 1.5 -1.5 1.5
+v 1.5 -1.5 -1.5
+v -1.5 1.5 1.5
+v -1.5 1.5 -1.5
+v -1.5 -1.5 1.5
+v -1.5 -1.5 -1.5
+f 13 1 3
+f 13 3 27
+f 13 27 5
+f 13 5 25
+f 13 25 1
+f 14 4 2
+f 14 2 26
+f 14 26 6
+f 14 6 28
+f 14 28 4
+f 15 3 1
+f 15 1 29
+f 15 29 7
+f 15 7 31
+f 15 31 3
+f 16 2 4
+f 16 4 32
+f 16 32 8
+f 16 8 30
+f 16 30 2
+f 17 5 6
+f 17 6 26
+f 17 26 9
+f 17 9 25
+f 17 25 5
+f 18 6 5
+f 18 5 27
+f 18 27 10
+f 18 10 28
+f 18 28 6
+f 19 8 7
+f 19 7 29
+f 19 29 11
+f 19 11 30
+f 19 30 8
+f 20 7 8
+f 20 8 32
+f 20 32 12
+f 20 12 31
+f 20 31 7
+f 21 9 11
+f 21 11 29
+f 21 29 1
+f 21 1 25
+f 21 25 9
+f 22 11 9
+f 22 9 26
+f 22 26 2
+f 22 2 30
+f 22 30 11
+f 23 12 10
+f 23 10 27
+f 23 27 3
+f 23 3 31
+f 23 31 12
+f 24 10 12
+f 24 12 32
+f 24 32 4
+f 24 4 28
+f 24 28 10
diff --git a/webapp/data/RhombicDodecahedron.obj b/webapp/data/RhombicDodecahedron.obj
new file mode 100644
index 0000000..a612edb
--- /dev/null
+++ b/webapp/data/RhombicDodecahedron.obj
@@ -0,0 +1,26 @@
+v 0.0 0.0 1.0606601717798212
+v 0.0 0.0 -1.0606601717798212
+v 1.0606601717798212 0.0 0.0
+v -1.0606601717798212 0.0 0.0
+v 0.0 1.0606601717798212 0.0
+v 0.0 -1.0606601717798212 0.0
+v 0.5303300858899106 0.5303300858899106 0.5303300858899106
+v 0.5303300858899106 0.5303300858899106 -0.5303300858899106
+v 0.5303300858899106 -0.5303300858899106 0.5303300858899106
+v 0.5303300858899106 -0.5303300858899106 -0.5303300858899106
+v -0.5303300858899106 0.5303300858899106 0.5303300858899106
+v -0.5303300858899106 0.5303300858899106 -0.5303300858899106
+v -0.5303300858899106 -0.5303300858899106 0.5303300858899106
+v -0.5303300858899106 -0.5303300858899106 -0.5303300858899106
+f 7 1 9 3
+f 7 3 8 5
+f 7 5 11 1
+f 10 2 8 3
+f 10 3 9 6
+f 10 6 14 2
+f 12 2 14 4
+f 12 4 11 5
+f 12 5 8 2
+f 13 1 11 4
+f 13 4 14 6
+f 13 6 9 1
diff --git a/webapp/data/RhombicTriacontahedron.obj b/webapp/data/RhombicTriacontahedron.obj
new file mode 100644
index 0000000..4a3c8b2
--- /dev/null
+++ b/webapp/data/RhombicTriacontahedron.obj
@@ -0,0 +1,62 @@
+v 0.9045084971874737 0.0 1.4635254915624212
+v 0.9045084971874737 0.0 -1.4635254915624212
+v -0.9045084971874737 0.0 1.4635254915624212
+v -0.9045084971874737 0.0 -1.4635254915624212
+v 1.4635254915624212 0.9045084971874737 0.0
+v 1.4635254915624212 -0.9045084971874737 0.0
+v -1.4635254915624212 0.9045084971874737 0.0
+v -1.4635254915624212 -0.9045084971874737 0.0
+v 0.0 1.4635254915624212 0.9045084971874737
+v 0.0 1.4635254915624212 -0.9045084971874737
+v 0.0 -1.4635254915624212 0.9045084971874737
+v 0.0 -1.4635254915624212 -0.9045084971874737
+v 0.0 0.5590169943749475 1.4635254915624212
+v 0.0 0.5590169943749475 -1.4635254915624212
+v 0.0 -0.5590169943749475 1.4635254915624212
+v 0.0 -0.5590169943749475 -1.4635254915624212
+v 1.4635254915624212 0.0 0.5590169943749475
+v 1.4635254915624212 0.0 -0.5590169943749475
+v -1.4635254915624212 0.0 0.5590169943749475
+v -1.4635254915624212 0.0 -0.5590169943749475
+v 0.5590169943749475 1.4635254915624212 0.0
+v 0.5590169943749475 -1.4635254915624212 0.0
+v -0.5590169943749475 1.4635254915624212 0.0
+v -0.5590169943749475 -1.4635254915624212 0.0
+v 0.9045084971874737 0.9045084971874737 0.9045084971874737
+v 0.9045084971874737 0.9045084971874737 -0.9045084971874737
+v 0.9045084971874737 -0.9045084971874737 0.9045084971874737
+v 0.9045084971874737 -0.9045084971874737 -0.9045084971874737
+v -0.9045084971874737 0.9045084971874737 0.9045084971874737
+v -0.9045084971874737 0.9045084971874737 -0.9045084971874737
+v -0.9045084971874737 -0.9045084971874737 0.9045084971874737
+v -0.9045084971874737 -0.9045084971874737 -0.9045084971874737
+f 1 13 3 15
+f 1 15 11 27
+f 1 27 6 17
+f 2 14 10 26
+f 2 26 5 18
+f 2 18 6 28
+f 3 29 7 19
+f 3 19 8 31
+f 3 31 11 15
+f 4 20 7 30
+f 4 30 10 14
+f 4 14 2 16
+f 5 21 9 25
+f 5 25 1 17
+f 5 17 6 18
+f 8 19 7 20
+f 8 20 4 32
+f 8 32 12 24
+f 9 23 7 29
+f 9 29 3 13
+f 9 13 1 25
+f 10 30 7 23
+f 10 23 9 21
+f 10 21 5 26
+f 11 31 8 24
+f 11 24 12 22
+f 11 22 6 27
+f 12 32 4 16
+f 12 16 2 28
+f 12 28 6 22
diff --git a/webapp/data/Rhombicosacron.obj b/webapp/data/Rhombicosacron.obj
new file mode 100644
index 0000000..4921cb0
--- /dev/null
+++ b/webapp/data/Rhombicosacron.obj
@@ -0,0 +1,110 @@
+v 0.0 0.6180339887498949 1.618033988749895
+v 0.0 0.6180339887498949 -1.618033988749895
+v 0.0 -0.6180339887498949 1.618033988749895
+v 0.0 -0.6180339887498949 -1.618033988749895
+v 1.618033988749895 0.0 0.6180339887498949
+v 1.618033988749895 0.0 -0.6180339887498949
+v -1.618033988749895 0.0 0.6180339887498949
+v -1.618033988749895 0.0 -0.6180339887498949
+v 0.6180339887498949 1.618033988749895 0.0
+v 0.6180339887498949 -1.618033988749895 0.0
+v -0.6180339887498949 1.618033988749895 0.0
+v -0.6180339887498949 -1.618033988749895 0.0
+v 0.0 0.0 1.3416407864998738
+v 0.0 0.0 -1.3416407864998738
+v 1.3416407864998738 0.0 0.0
+v -1.3416407864998738 0.0 0.0
+v 0.0 1.3416407864998738 0.0
+v 0.0 -1.3416407864998738 0.0
+v 0.41458980337503154 0.6708203932499369 1.0854101966249685
+v 0.41458980337503154 0.6708203932499369 -1.0854101966249685
+v 0.41458980337503154 -0.6708203932499369 1.0854101966249685
+v 0.41458980337503154 -0.6708203932499369 -1.0854101966249685
+v -0.41458980337503154 0.6708203932499369 1.0854101966249685
+v -0.41458980337503154 0.6708203932499369 -1.0854101966249685
+v -0.41458980337503154 -0.6708203932499369 1.0854101966249685
+v -0.41458980337503154 -0.6708203932499369 -1.0854101966249685
+v 1.0854101966249685 0.41458980337503154 0.6708203932499369
+v 1.0854101966249685 0.41458980337503154 -0.6708203932499369
+v 1.0854101966249685 -0.41458980337503154 0.6708203932499369
+v 1.0854101966249685 -0.41458980337503154 -0.6708203932499369
+v -1.0854101966249685 0.41458980337503154 0.6708203932499369
+v -1.0854101966249685 0.41458980337503154 -0.6708203932499369
+v -1.0854101966249685 -0.41458980337503154 0.6708203932499369
+v -1.0854101966249685 -0.41458980337503154 -0.6708203932499369
+v 0.6708203932499369 1.0854101966249685 0.41458980337503154
+v 0.6708203932499369 1.0854101966249685 -0.41458980337503154
+v 0.6708203932499369 -1.0854101966249685 0.41458980337503154
+v 0.6708203932499369 -1.0854101966249685 -0.41458980337503154
+v -0.6708203932499369 1.0854101966249685 0.41458980337503154
+v -0.6708203932499369 1.0854101966249685 -0.41458980337503154
+v -0.6708203932499369 -1.0854101966249685 0.41458980337503154
+v -0.6708203932499369 -1.0854101966249685 -0.41458980337503154
+v 1.0 1.0 1.0
+v 1.0 1.0 -1.0
+v 1.0 -1.0 1.0
+v 1.0 -1.0 -1.0
+v -1.0 1.0 1.0
+v -1.0 1.0 -1.0
+v -1.0 -1.0 1.0
+v -1.0 -1.0 -1.0
+f 1 17 9 23
+f 1 29 45 19
+f 1 33 7 13
+f 2 17 11 20
+f 2 34 50 24
+f 2 30 6 14
+f 3 18 12 21
+f 3 31 47 25
+f 3 27 5 13
+f 4 18 10 26
+f 4 28 44 22
+f 4 32 8 14
+f 5 13 1 29
+f 5 36 44 27
+f 5 38 10 15
+f 6 14 4 28
+f 6 37 45 30
+f 6 35 9 15
+f 7 13 3 31
+f 7 42 50 33
+f 7 40 11 16
+f 8 14 2 34
+f 8 39 47 32
+f 8 41 12 16
+f 9 15 5 36
+f 9 23 47 35
+f 9 24 2 17
+f 10 15 6 37
+f 10 26 50 38
+f 10 25 3 18
+f 11 16 8 39
+f 11 20 44 40
+f 11 19 1 17
+f 12 16 7 42
+f 12 21 45 41
+f 12 22 4 18
+f 43 21 3 27
+f 43 39 11 19
+f 43 28 6 35
+f 44 22 46 20
+f 44 27 43 28
+f 44 40 48 36
+f 45 19 43 21
+f 45 30 46 29
+f 45 41 49 37
+f 46 20 2 30
+f 46 42 12 22
+f 46 29 5 38
+f 47 25 49 23
+f 47 32 48 31
+f 47 35 43 39
+f 48 26 4 32
+f 48 36 9 24
+f 48 31 7 40
+f 49 23 1 33
+f 49 37 10 25
+f 49 34 8 41
+f 50 24 48 26
+f 50 33 49 34
+f 50 38 46 42
diff --git a/webapp/data/Rhombicosidodecahedron.obj b/webapp/data/Rhombicosidodecahedron.obj
new file mode 100644
index 0000000..c717b4f
--- /dev/null
+++ b/webapp/data/Rhombicosidodecahedron.obj
@@ -0,0 +1,122 @@
+v 0.5 0.5 2.118033988749895
+v 0.5 0.5 -2.118033988749895
+v 0.5 -0.5 2.118033988749895
+v 0.5 -0.5 -2.118033988749895
+v -0.5 0.5 2.118033988749895
+v -0.5 0.5 -2.118033988749895
+v -0.5 -0.5 2.118033988749895
+v -0.5 -0.5 -2.118033988749895
+v 2.118033988749895 0.5 0.5
+v 2.118033988749895 0.5 -0.5
+v 2.118033988749895 -0.5 0.5
+v 2.118033988749895 -0.5 -0.5
+v -2.118033988749895 0.5 0.5
+v -2.118033988749895 0.5 -0.5
+v -2.118033988749895 -0.5 0.5
+v -2.118033988749895 -0.5 -0.5
+v 0.5 2.118033988749895 0.5
+v 0.5 2.118033988749895 -0.5
+v 0.5 -2.118033988749895 0.5
+v 0.5 -2.118033988749895 -0.5
+v -0.5 2.118033988749895 0.5
+v -0.5 2.118033988749895 -0.5
+v -0.5 -2.118033988749895 0.5
+v -0.5 -2.118033988749895 -0.5
+v 0.0 1.3090169943749475 1.8090169943749475
+v 0.0 1.3090169943749475 -1.8090169943749475
+v 0.0 -1.3090169943749475 1.8090169943749475
+v 0.0 -1.3090169943749475 -1.8090169943749475
+v 1.8090169943749475 0.0 1.3090169943749475
+v 1.8090169943749475 0.0 -1.3090169943749475
+v -1.8090169943749475 0.0 1.3090169943749475
+v -1.8090169943749475 0.0 -1.3090169943749475
+v 1.3090169943749475 1.8090169943749475 0.0
+v 1.3090169943749475 -1.8090169943749475 0.0
+v -1.3090169943749475 1.8090169943749475 0.0
+v -1.3090169943749475 -1.8090169943749475 0.0
+v 1.3090169943749475 0.8090169943749475 1.618033988749895
+v 1.3090169943749475 0.8090169943749475 -1.618033988749895
+v 1.3090169943749475 -0.8090169943749475 1.618033988749895
+v 1.3090169943749475 -0.8090169943749475 -1.618033988749895
+v -1.3090169943749475 0.8090169943749475 1.618033988749895
+v -1.3090169943749475 0.8090169943749475 -1.618033988749895
+v -1.3090169943749475 -0.8090169943749475 1.618033988749895
+v -1.3090169943749475 -0.8090169943749475 -1.618033988749895
+v 1.618033988749895 1.3090169943749475 0.8090169943749475
+v 1.618033988749895 1.3090169943749475 -0.8090169943749475
+v 1.618033988749895 -1.3090169943749475 0.8090169943749475
+v 1.618033988749895 -1.3090169943749475 -0.8090169943749475
+v -1.618033988749895 1.3090169943749475 0.8090169943749475
+v -1.618033988749895 1.3090169943749475 -0.8090169943749475
+v -1.618033988749895 -1.3090169943749475 0.8090169943749475
+v -1.618033988749895 -1.3090169943749475 -0.8090169943749475
+v 0.8090169943749475 1.618033988749895 1.3090169943749475
+v 0.8090169943749475 1.618033988749895 -1.3090169943749475
+v 0.8090169943749475 -1.618033988749895 1.3090169943749475
+v 0.8090169943749475 -1.618033988749895 -1.3090169943749475
+v -0.8090169943749475 1.618033988749895 1.3090169943749475
+v -0.8090169943749475 1.618033988749895 -1.3090169943749475
+v -0.8090169943749475 -1.618033988749895 1.3090169943749475
+v -0.8090169943749475 -1.618033988749895 -1.3090169943749475
+f 25 53 17 21 57
+f 26 58 22 18 54
+f 27 59 23 19 55
+f 28 56 20 24 60
+f 29 37 1 3 39
+f 30 40 4 2 38
+f 31 43 7 5 41
+f 32 42 6 8 44
+f 33 45 9 10 46
+f 34 48 12 11 47
+f 35 50 14 13 49
+f 36 51 15 16 52
+f 1 37 53 25
+f 2 26 54 38
+f 3 27 55 39
+f 4 40 56 28
+f 5 25 57 41
+f 6 42 58 26
+f 7 43 59 27
+f 8 28 60 44
+f 9 45 37 29
+f 10 30 38 46
+f 11 29 39 47
+f 12 48 40 30
+f 13 31 41 49
+f 14 50 42 32
+f 15 51 43 31
+f 16 32 44 52
+f 17 53 45 33
+f 18 33 46 54
+f 19 34 47 55
+f 20 56 48 34
+f 21 35 49 57
+f 22 58 50 35
+f 23 59 51 36
+f 24 36 52 60
+f 1 5 7 3
+f 2 4 8 6
+f 9 11 12 10
+f 13 14 16 15
+f 17 18 22 21
+f 19 23 24 20
+f 25 5 1
+f 26 2 6
+f 27 3 7
+f 28 8 4
+f 29 11 9
+f 30 10 12
+f 31 13 15
+f 32 16 14
+f 33 18 17
+f 34 19 20
+f 35 21 22
+f 36 24 23
+f 37 45 53
+f 38 54 46
+f 39 55 47
+f 40 48 56
+f 41 57 49
+f 42 50 58
+f 43 51 59
+f 44 60 52
diff --git a/webapp/data/Rhombicuboctahedron.obj b/webapp/data/Rhombicuboctahedron.obj
new file mode 100644
index 0000000..23ed846
--- /dev/null
+++ b/webapp/data/Rhombicuboctahedron.obj
@@ -0,0 +1,50 @@
+v 0.5 0.5 1.2071067811865475
+v 0.5 0.5 -1.2071067811865475
+v 0.5 -0.5 1.2071067811865475
+v 0.5 -0.5 -1.2071067811865475
+v -0.5 0.5 1.2071067811865475
+v -0.5 0.5 -1.2071067811865475
+v -0.5 -0.5 1.2071067811865475
+v -0.5 -0.5 -1.2071067811865475
+v 1.2071067811865475 0.5 0.5
+v 1.2071067811865475 0.5 -0.5
+v 1.2071067811865475 -0.5 0.5
+v 1.2071067811865475 -0.5 -0.5
+v -1.2071067811865475 0.5 0.5
+v -1.2071067811865475 0.5 -0.5
+v -1.2071067811865475 -0.5 0.5
+v -1.2071067811865475 -0.5 -0.5
+v 0.5 1.2071067811865475 0.5
+v 0.5 1.2071067811865475 -0.5
+v 0.5 -1.2071067811865475 0.5
+v 0.5 -1.2071067811865475 -0.5
+v -0.5 1.2071067811865475 0.5
+v -0.5 1.2071067811865475 -0.5
+v -0.5 -1.2071067811865475 0.5
+v -0.5 -1.2071067811865475 -0.5
+f 1 5 7 3
+f 2 4 8 6
+f 9 11 12 10
+f 13 14 16 15
+f 17 18 22 21
+f 19 23 24 20
+f 1 3 11 9
+f 1 17 21 5
+f 8 4 20 24
+f 8 16 14 6
+f 12 4 2 10
+f 12 11 19 20
+f 13 15 7 5
+f 13 21 22 14
+f 18 2 6 22
+f 18 17 9 10
+f 23 15 16 24
+f 23 19 3 7
+f 1 9 17
+f 2 18 10
+f 3 19 11
+f 4 12 20
+f 5 21 13
+f 6 14 22
+f 7 15 23
+f 8 24 16
diff --git a/webapp/data/SmallStellatedDodecahedron.obj b/webapp/data/SmallStellatedDodecahedron.obj
new file mode 100644
index 0000000..54c7fe3
--- /dev/null
+++ b/webapp/data/SmallStellatedDodecahedron.obj
@@ -0,0 +1,24 @@
+v 0.0 0.5 -0.30901699437494745
+v 0.0 0.5 0.30901699437494745
+v 0.0 -0.5 -0.30901699437494745
+v 0.0 -0.5 0.30901699437494745
+v 0.5 -0.30901699437494745 0.0
+v -0.5 -0.30901699437494745 0.0
+v 0.5 0.30901699437494745 0.0
+v -0.5 0.30901699437494745 0.0
+v -0.30901699437494745 0.0 0.5
+v -0.30901699437494745 0.0 -0.5
+v 0.30901699437494745 0.0 0.5
+v 0.30901699437494745 0.0 -0.5
+f 1 3 8 12 6
+f 1 6 2 10 9
+f 1 9 7 8 11
+f 2 4 7 9 5
+f 2 5 1 11 12
+f 2 12 8 7 10
+f 3 1 5 10 7
+f 3 7 4 12 11
+f 3 11 6 5 9
+f 4 2 6 11 8
+f 4 8 3 9 10
+f 4 10 5 6 12
diff --git a/webapp/data/Tetrahedron.obj b/webapp/data/Tetrahedron.obj
new file mode 100644
index 0000000..ed56961
--- /dev/null
+++ b/webapp/data/Tetrahedron.obj
@@ -0,0 +1,8 @@
+v 0.3535533905932738 -0.3535533905932738 0.3535533905932738
+v 0.3535533905932738 0.3535533905932738 -0.3535533905932738
+v -0.3535533905932738 0.3535533905932738 0.3535533905932738
+v -0.3535533905932738 -0.3535533905932738 -0.3535533905932738
+f 1 2 3
+f 2 1 4
+f 3 4 1
+f 4 3 2
diff --git a/webapp/data/TetrakisHexahedron.obj b/webapp/data/TetrakisHexahedron.obj
new file mode 100644
index 0000000..2583006
--- /dev/null
+++ b/webapp/data/TetrakisHexahedron.obj
@@ -0,0 +1,38 @@
+v 0.0 0.0 1.590990257669732
+v 0.0 0.0 -1.590990257669732
+v 1.590990257669732 0.0 0.0
+v -1.590990257669732 0.0 0.0
+v 0.0 1.590990257669732 0.0
+v 0.0 -1.590990257669732 0.0
+v 1.0606601717798212 1.0606601717798212 1.0606601717798212
+v 1.0606601717798212 1.0606601717798212 -1.0606601717798212
+v 1.0606601717798212 -1.0606601717798212 1.0606601717798212
+v 1.0606601717798212 -1.0606601717798212 -1.0606601717798212
+v -1.0606601717798212 1.0606601717798212 1.0606601717798212
+v -1.0606601717798212 1.0606601717798212 -1.0606601717798212
+v -1.0606601717798212 -1.0606601717798212 1.0606601717798212
+v -1.0606601717798212 -1.0606601717798212 -1.0606601717798212
+f 1 7 11
+f 1 11 13
+f 1 13 9
+f 1 9 7
+f 2 8 10
+f 2 10 14
+f 2 14 12
+f 2 12 8
+f 3 7 9
+f 3 9 10
+f 3 10 8
+f 3 8 7
+f 4 11 12
+f 4 12 14
+f 4 14 13
+f 4 13 11
+f 5 7 8
+f 5 8 12
+f 5 12 11
+f 5 11 7
+f 6 9 13
+f 6 13 14
+f 6 14 10
+f 6 10 9
diff --git a/webapp/data/TriakisIcosahedron.obj b/webapp/data/TriakisIcosahedron.obj
new file mode 100644
index 0000000..c7a6e7e
--- /dev/null
+++ b/webapp/data/TriakisIcosahedron.obj
@@ -0,0 +1,92 @@
+v 1.8090169943749475 0.0 2.9270509831248424
+v 1.8090169943749475 0.0 -2.9270509831248424
+v -1.8090169943749475 0.0 2.9270509831248424
+v -1.8090169943749475 0.0 -2.9270509831248424
+v 2.9270509831248424 1.8090169943749475 0.0
+v 2.9270509831248424 -1.8090169943749475 0.0
+v -2.9270509831248424 1.8090169943749475 0.0
+v -2.9270509831248424 -1.8090169943749475 0.0
+v 0.0 2.9270509831248424 1.8090169943749475
+v 0.0 2.9270509831248424 -1.8090169943749475
+v 0.0 -2.9270509831248424 1.8090169943749475
+v 0.0 -2.9270509831248424 -1.8090169943749475
+v 0.0 1.0495531792613397 2.747765896306699
+v 0.0 1.0495531792613397 -2.747765896306699
+v 0.0 -1.0495531792613397 2.747765896306699
+v 0.0 -1.0495531792613397 -2.747765896306699
+v 2.747765896306699 0.0 1.0495531792613397
+v 2.747765896306699 0.0 -1.0495531792613397
+v -2.747765896306699 0.0 1.0495531792613397
+v -2.747765896306699 0.0 -1.0495531792613397
+v 1.0495531792613397 2.747765896306699 0.0
+v 1.0495531792613397 -2.747765896306699 0.0
+v -1.0495531792613397 2.747765896306699 0.0
+v -1.0495531792613397 -2.747765896306699 0.0
+v 1.698212717045359 1.698212717045359 1.698212717045359
+v 1.698212717045359 1.698212717045359 -1.698212717045359
+v 1.698212717045359 -1.698212717045359 1.698212717045359
+v 1.698212717045359 -1.698212717045359 -1.698212717045359
+v -1.698212717045359 1.698212717045359 1.698212717045359
+v -1.698212717045359 1.698212717045359 -1.698212717045359
+v -1.698212717045359 -1.698212717045359 1.698212717045359
+v -1.698212717045359 -1.698212717045359 -1.698212717045359
+f 13 1 9
+f 13 9 3
+f 13 3 1
+f 14 2 4
+f 14 4 10
+f 14 10 2
+f 15 1 3
+f 15 3 11
+f 15 11 1
+f 16 2 12
+f 16 12 4
+f 16 4 2
+f 17 1 6
+f 17 6 5
+f 17 5 1
+f 18 2 5
+f 18 5 6
+f 18 6 2
+f 19 3 7
+f 19 7 8
+f 19 8 3
+f 20 4 8
+f 20 8 7
+f 20 7 4
+f 21 5 10
+f 21 10 9
+f 21 9 5
+f 22 6 11
+f 22 11 12
+f 22 12 6
+f 23 7 9
+f 23 9 10
+f 23 10 7
+f 24 8 12
+f 24 12 11
+f 24 11 8
+f 25 1 5
+f 25 5 9
+f 25 9 1
+f 26 2 10
+f 26 10 5
+f 26 5 2
+f 27 1 11
+f 27 11 6
+f 27 6 1
+f 28 2 6
+f 28 6 12
+f 28 12 2
+f 29 3 9
+f 29 9 7
+f 29 7 3
+f 30 4 7
+f 30 7 10
+f 30 10 4
+f 31 3 8
+f 31 8 11
+f 31 11 3
+f 32 4 12
+f 32 12 8
+f 32 8 4
diff --git a/webapp/data/TriakisOctahedron.obj b/webapp/data/TriakisOctahedron.obj
new file mode 100644
index 0000000..7e8073d
--- /dev/null
+++ b/webapp/data/TriakisOctahedron.obj
@@ -0,0 +1,38 @@
+v 0.0 0.0 2.414213562373095
+v 0.0 0.0 -2.414213562373095
+v 2.414213562373095 0.0 0.0
+v -2.414213562373095 0.0 0.0
+v 0.0 2.414213562373095 0.0
+v 0.0 -2.414213562373095 0.0
+v 1.0 1.0 1.0
+v 1.0 1.0 -1.0
+v 1.0 -1.0 1.0
+v 1.0 -1.0 -1.0
+v -1.0 1.0 1.0
+v -1.0 1.0 -1.0
+v -1.0 -1.0 1.0
+v -1.0 -1.0 -1.0
+f 7 1 3
+f 7 3 5
+f 7 5 1
+f 8 2 5
+f 8 5 3
+f 8 3 2
+f 9 1 6
+f 9 6 3
+f 9 3 1
+f 10 2 3
+f 10 3 6
+f 10 6 2
+f 11 1 5
+f 11 5 4
+f 11 4 1
+f 12 2 4
+f 12 4 5
+f 12 5 2
+f 13 1 4
+f 13 4 6
+f 13 6 1
+f 14 2 6
+f 14 6 4
+f 14 4 2
diff --git a/webapp/data/TriakisTetrahedron.obj b/webapp/data/TriakisTetrahedron.obj
new file mode 100644
index 0000000..d169f71
--- /dev/null
+++ b/webapp/data/TriakisTetrahedron.obj
@@ -0,0 +1,20 @@
+v 1.0606601717798212 1.0606601717798212 1.0606601717798212
+v 1.0606601717798212 -1.0606601717798212 -1.0606601717798212
+v -1.0606601717798212 -1.0606601717798212 1.0606601717798212
+v -1.0606601717798212 1.0606601717798212 -1.0606601717798212
+v 0.6363961030678927 -0.6363961030678927 0.6363961030678927
+v 0.6363961030678927 0.6363961030678927 -0.6363961030678927
+v -0.6363961030678927 0.6363961030678927 0.6363961030678927
+v -0.6363961030678927 -0.6363961030678927 -0.6363961030678927
+f 5 1 3
+f 5 3 2
+f 5 2 1
+f 6 1 2
+f 6 2 4
+f 6 4 1
+f 7 1 4
+f 7 4 3
+f 7 3 1
+f 8 2 3
+f 8 3 4
+f 8 4 2
diff --git a/webapp/data/TruncatedCube.obj b/webapp/data/TruncatedCube.obj
new file mode 100644
index 0000000..e3a96a4
--- /dev/null
+++ b/webapp/data/TruncatedCube.obj
@@ -0,0 +1,38 @@
+v 1.2071067811865475 0.5 1.2071067811865475
+v 1.2071067811865475 0.5 -1.2071067811865475
+v 1.2071067811865475 -0.5 1.2071067811865475
+v 1.2071067811865475 -0.5 -1.2071067811865475
+v -1.2071067811865475 0.5 1.2071067811865475
+v -1.2071067811865475 0.5 -1.2071067811865475
+v -1.2071067811865475 -0.5 1.2071067811865475
+v -1.2071067811865475 -0.5 -1.2071067811865475
+v 1.2071067811865475 1.2071067811865475 0.5
+v 1.2071067811865475 1.2071067811865475 -0.5
+v 1.2071067811865475 -1.2071067811865475 0.5
+v 1.2071067811865475 -1.2071067811865475 -0.5
+v -1.2071067811865475 1.2071067811865475 0.5
+v -1.2071067811865475 1.2071067811865475 -0.5
+v -1.2071067811865475 -1.2071067811865475 0.5
+v -1.2071067811865475 -1.2071067811865475 -0.5
+v 0.5 1.2071067811865475 1.2071067811865475
+v 0.5 1.2071067811865475 -1.2071067811865475
+v 0.5 -1.2071067811865475 1.2071067811865475
+v 0.5 -1.2071067811865475 -1.2071067811865475
+v -0.5 1.2071067811865475 1.2071067811865475
+v -0.5 1.2071067811865475 -1.2071067811865475
+v -0.5 -1.2071067811865475 1.2071067811865475
+v -0.5 -1.2071067811865475 -1.2071067811865475
+f 1 3 11 12 4 2 10 9
+f 1 17 21 5 7 23 19 3
+f 13 14 6 8 16 15 7 5
+f 13 21 17 9 10 18 22 14
+f 20 24 8 6 22 18 2 4
+f 20 12 11 19 23 15 16 24
+f 1 9 17
+f 2 18 10
+f 3 19 11
+f 4 12 20
+f 5 21 13
+f 6 14 22
+f 7 15 23
+f 8 24 16
diff --git a/webapp/data/TruncatedCuboctahedron.obj b/webapp/data/TruncatedCuboctahedron.obj
new file mode 100644
index 0000000..8076157
--- /dev/null
+++ b/webapp/data/TruncatedCuboctahedron.obj
@@ -0,0 +1,74 @@
+v 1.2071067811865475 0.5 1.9142135623730951
+v 1.2071067811865475 0.5 -1.9142135623730951
+v 1.2071067811865475 -0.5 1.9142135623730951
+v 1.2071067811865475 -0.5 -1.9142135623730951
+v -1.2071067811865475 0.5 1.9142135623730951
+v -1.2071067811865475 0.5 -1.9142135623730951
+v -1.2071067811865475 -0.5 1.9142135623730951
+v -1.2071067811865475 -0.5 -1.9142135623730951
+v 1.9142135623730951 1.2071067811865475 0.5
+v 1.9142135623730951 1.2071067811865475 -0.5
+v 1.9142135623730951 -1.2071067811865475 0.5
+v 1.9142135623730951 -1.2071067811865475 -0.5
+v -1.9142135623730951 1.2071067811865475 0.5
+v -1.9142135623730951 1.2071067811865475 -0.5
+v -1.9142135623730951 -1.2071067811865475 0.5
+v -1.9142135623730951 -1.2071067811865475 -0.5
+v 0.5 1.9142135623730951 1.2071067811865475
+v 0.5 1.9142135623730951 -1.2071067811865475
+v 0.5 -1.9142135623730951 1.2071067811865475
+v 0.5 -1.9142135623730951 -1.2071067811865475
+v -0.5 1.9142135623730951 1.2071067811865475
+v -0.5 1.9142135623730951 -1.2071067811865475
+v -0.5 -1.9142135623730951 1.2071067811865475
+v -0.5 -1.9142135623730951 -1.2071067811865475
+v 0.5 1.2071067811865475 1.9142135623730951
+v 0.5 1.2071067811865475 -1.9142135623730951
+v 0.5 -1.2071067811865475 1.9142135623730951
+v 0.5 -1.2071067811865475 -1.9142135623730951
+v -0.5 1.2071067811865475 1.9142135623730951
+v -0.5 1.2071067811865475 -1.9142135623730951
+v -0.5 -1.2071067811865475 1.9142135623730951
+v -0.5 -1.2071067811865475 -1.9142135623730951
+v 1.9142135623730951 0.5 1.2071067811865475
+v 1.9142135623730951 0.5 -1.2071067811865475
+v 1.9142135623730951 -0.5 1.2071067811865475
+v 1.9142135623730951 -0.5 -1.2071067811865475
+v -1.9142135623730951 0.5 1.2071067811865475
+v -1.9142135623730951 0.5 -1.2071067811865475
+v -1.9142135623730951 -0.5 1.2071067811865475
+v -1.9142135623730951 -0.5 -1.2071067811865475
+v 1.2071067811865475 1.9142135623730951 0.5
+v 1.2071067811865475 1.9142135623730951 -0.5
+v 1.2071067811865475 -1.9142135623730951 0.5
+v 1.2071067811865475 -1.9142135623730951 -0.5
+v -1.2071067811865475 1.9142135623730951 0.5
+v -1.2071067811865475 1.9142135623730951 -0.5
+v -1.2071067811865475 -1.9142135623730951 0.5
+v -1.2071067811865475 -1.9142135623730951 -0.5
+f 1 25 29 5 7 31 27 3
+f 2 4 28 32 8 6 30 26
+f 9 33 35 11 12 36 34 10
+f 13 14 38 40 16 15 39 37
+f 17 41 42 18 22 46 45 21
+f 19 23 47 48 24 20 44 43
+f 1 33 9 41 17 25
+f 2 26 18 42 10 34
+f 3 27 19 43 11 35
+f 4 36 12 44 20 28
+f 5 29 21 45 13 37
+f 6 38 14 46 22 30
+f 7 39 15 47 23 31
+f 8 32 24 48 16 40
+f 1 3 35 33
+f 2 34 36 4
+f 5 37 39 7
+f 6 8 40 38
+f 9 10 42 41
+f 11 43 44 12
+f 13 45 46 14
+f 15 16 48 47
+f 17 21 29 25
+f 18 26 30 22
+f 19 27 31 23
+f 20 24 32 28
diff --git a/webapp/data/TruncatedDodecahedron.obj b/webapp/data/TruncatedDodecahedron.obj
new file mode 100644
index 0000000..fcef10b
--- /dev/null
+++ b/webapp/data/TruncatedDodecahedron.obj
@@ -0,0 +1,92 @@
+v 0.0 0.5 2.9270509831248424
+v 0.0 0.5 -2.9270509831248424
+v 0.0 -0.5 2.9270509831248424
+v 0.0 -0.5 -2.9270509831248424
+v 2.9270509831248424 0.0 0.5
+v 2.9270509831248424 0.0 -0.5
+v -2.9270509831248424 0.0 0.5
+v -2.9270509831248424 0.0 -0.5
+v 0.5 2.9270509831248424 0.0
+v 0.5 -2.9270509831248424 0.0
+v -0.5 2.9270509831248424 0.0
+v -0.5 -2.9270509831248424 0.0
+v 0.5 1.3090169943749475 2.618033988749895
+v 0.5 1.3090169943749475 -2.618033988749895
+v 0.5 -1.3090169943749475 2.618033988749895
+v 0.5 -1.3090169943749475 -2.618033988749895
+v -0.5 1.3090169943749475 2.618033988749895
+v -0.5 1.3090169943749475 -2.618033988749895
+v -0.5 -1.3090169943749475 2.618033988749895
+v -0.5 -1.3090169943749475 -2.618033988749895
+v 2.618033988749895 0.5 1.3090169943749475
+v 2.618033988749895 0.5 -1.3090169943749475
+v 2.618033988749895 -0.5 1.3090169943749475
+v 2.618033988749895 -0.5 -1.3090169943749475
+v -2.618033988749895 0.5 1.3090169943749475
+v -2.618033988749895 0.5 -1.3090169943749475
+v -2.618033988749895 -0.5 1.3090169943749475
+v -2.618033988749895 -0.5 -1.3090169943749475
+v 1.3090169943749475 2.618033988749895 0.5
+v 1.3090169943749475 2.618033988749895 -0.5
+v 1.3090169943749475 -2.618033988749895 0.5
+v 1.3090169943749475 -2.618033988749895 -0.5
+v -1.3090169943749475 2.618033988749895 0.5
+v -1.3090169943749475 2.618033988749895 -0.5
+v -1.3090169943749475 -2.618033988749895 0.5
+v -1.3090169943749475 -2.618033988749895 -0.5
+v 1.3090169943749475 1.618033988749895 2.118033988749895
+v 1.3090169943749475 1.618033988749895 -2.118033988749895
+v 1.3090169943749475 -1.618033988749895 2.118033988749895
+v 1.3090169943749475 -1.618033988749895 -2.118033988749895
+v -1.3090169943749475 1.618033988749895 2.118033988749895
+v -1.3090169943749475 1.618033988749895 -2.118033988749895
+v -1.3090169943749475 -1.618033988749895 2.118033988749895
+v -1.3090169943749475 -1.618033988749895 -2.118033988749895
+v 2.118033988749895 1.3090169943749475 1.618033988749895
+v 2.118033988749895 1.3090169943749475 -1.618033988749895
+v 2.118033988749895 -1.3090169943749475 1.618033988749895
+v 2.118033988749895 -1.3090169943749475 -1.618033988749895
+v -2.118033988749895 1.3090169943749475 1.618033988749895
+v -2.118033988749895 1.3090169943749475 -1.618033988749895
+v -2.118033988749895 -1.3090169943749475 1.618033988749895
+v -2.118033988749895 -1.3090169943749475 -1.618033988749895
+v 1.618033988749895 2.118033988749895 1.3090169943749475
+v 1.618033988749895 2.118033988749895 -1.3090169943749475
+v 1.618033988749895 -2.118033988749895 1.3090169943749475
+v 1.618033988749895 -2.118033988749895 -1.3090169943749475
+v -1.618033988749895 2.118033988749895 1.3090169943749475
+v -1.618033988749895 2.118033988749895 -1.3090169943749475
+v -1.618033988749895 -2.118033988749895 1.3090169943749475
+v -1.618033988749895 -2.118033988749895 -1.3090169943749475
+f 1 3 15 39 47 23 21 45 37 13
+f 2 4 20 44 52 28 26 50 42 18
+f 3 1 17 41 49 25 27 51 43 19
+f 4 2 14 38 46 22 24 48 40 16
+f 5 6 22 46 54 30 29 53 45 21
+f 6 5 23 47 55 31 32 56 48 24
+f 7 8 28 52 60 36 35 59 51 27
+f 8 7 25 49 57 33 34 58 50 26
+f 9 11 33 57 41 17 13 37 53 29
+f 10 12 36 60 44 20 16 40 56 32
+f 11 9 30 54 38 14 18 42 58 34
+f 12 10 31 55 39 15 19 43 59 35
+f 1 13 17
+f 2 18 14
+f 3 19 15
+f 4 16 20
+f 5 21 23
+f 6 24 22
+f 7 27 25
+f 8 26 28
+f 9 29 30
+f 10 32 31
+f 11 34 33
+f 12 35 36
+f 37 45 53
+f 38 54 46
+f 39 55 47
+f 40 48 56
+f 41 57 49
+f 42 50 58
+f 43 51 59
+f 44 60 52
diff --git a/webapp/data/TruncatedIcosahedron.obj b/webapp/data/TruncatedIcosahedron.obj
new file mode 100644
index 0000000..df0f59c
--- /dev/null
+++ b/webapp/data/TruncatedIcosahedron.obj
@@ -0,0 +1,92 @@
+v 0.5 0.0 2.4270509831248424
+v 0.5 0.0 -2.4270509831248424
+v -0.5 0.0 2.4270509831248424
+v -0.5 0.0 -2.4270509831248424
+v 2.4270509831248424 0.5 0.0
+v 2.4270509831248424 -0.5 0.0
+v -2.4270509831248424 0.5 0.0
+v -2.4270509831248424 -0.5 0.0
+v 0.0 2.4270509831248424 0.5
+v 0.0 2.4270509831248424 -0.5
+v 0.0 -2.4270509831248424 0.5
+v 0.0 -2.4270509831248424 -0.5
+v 1.0 0.8090169943749475 2.118033988749895
+v 1.0 0.8090169943749475 -2.118033988749895
+v 1.0 -0.8090169943749475 2.118033988749895
+v 1.0 -0.8090169943749475 -2.118033988749895
+v -1.0 0.8090169943749475 2.118033988749895
+v -1.0 0.8090169943749475 -2.118033988749895
+v -1.0 -0.8090169943749475 2.118033988749895
+v -1.0 -0.8090169943749475 -2.118033988749895
+v 2.118033988749895 1.0 0.8090169943749475
+v 2.118033988749895 1.0 -0.8090169943749475
+v 2.118033988749895 -1.0 0.8090169943749475
+v 2.118033988749895 -1.0 -0.8090169943749475
+v -2.118033988749895 1.0 0.8090169943749475
+v -2.118033988749895 1.0 -0.8090169943749475
+v -2.118033988749895 -1.0 0.8090169943749475
+v -2.118033988749895 -1.0 -0.8090169943749475
+v 0.8090169943749475 2.118033988749895 1.0
+v 0.8090169943749475 2.118033988749895 -1.0
+v 0.8090169943749475 -2.118033988749895 1.0
+v 0.8090169943749475 -2.118033988749895 -1.0
+v -0.8090169943749475 2.118033988749895 1.0
+v -0.8090169943749475 2.118033988749895 -1.0
+v -0.8090169943749475 -2.118033988749895 1.0
+v -0.8090169943749475 -2.118033988749895 -1.0
+v 0.5 1.618033988749895 1.8090169943749475
+v 0.5 1.618033988749895 -1.8090169943749475
+v 0.5 -1.618033988749895 1.8090169943749475
+v 0.5 -1.618033988749895 -1.8090169943749475
+v -0.5 1.618033988749895 1.8090169943749475
+v -0.5 1.618033988749895 -1.8090169943749475
+v -0.5 -1.618033988749895 1.8090169943749475
+v -0.5 -1.618033988749895 -1.8090169943749475
+v 1.8090169943749475 0.5 1.618033988749895
+v 1.8090169943749475 0.5 -1.618033988749895
+v 1.8090169943749475 -0.5 1.618033988749895
+v 1.8090169943749475 -0.5 -1.618033988749895
+v -1.8090169943749475 0.5 1.618033988749895
+v -1.8090169943749475 0.5 -1.618033988749895
+v -1.8090169943749475 -0.5 1.618033988749895
+v -1.8090169943749475 -0.5 -1.618033988749895
+v 1.618033988749895 1.8090169943749475 0.5
+v 1.618033988749895 1.8090169943749475 -0.5
+v 1.618033988749895 -1.8090169943749475 0.5
+v 1.618033988749895 -1.8090169943749475 -0.5
+v -1.618033988749895 1.8090169943749475 0.5
+v -1.618033988749895 1.8090169943749475 -0.5
+v -1.618033988749895 -1.8090169943749475 0.5
+v -1.618033988749895 -1.8090169943749475 -0.5
+f 1 3 19 43 39 15
+f 2 4 18 42 38 14
+f 3 1 13 37 41 17
+f 4 2 16 40 44 20
+f 5 6 24 48 46 22
+f 6 5 21 45 47 23
+f 7 8 27 51 49 25
+f 8 7 26 50 52 28
+f 9 10 34 58 57 33
+f 10 9 29 53 54 30
+f 11 12 32 56 55 31
+f 12 11 35 59 60 36
+f 13 45 21 53 29 37
+f 14 38 30 54 22 46
+f 15 39 31 55 23 47
+f 16 48 24 56 32 40
+f 17 41 33 57 25 49
+f 18 50 26 58 34 42
+f 19 51 27 59 35 43
+f 20 44 36 60 28 52
+f 1 15 47 45 13
+f 2 14 46 48 16
+f 3 17 49 51 19
+f 4 20 52 50 18
+f 5 22 54 53 21
+f 6 23 55 56 24
+f 7 25 57 58 26
+f 8 28 60 59 27
+f 9 33 41 37 29
+f 10 30 38 42 34
+f 11 31 39 43 35
+f 12 36 44 40 32
diff --git a/webapp/data/TruncatedIcosidodecahedron.obj b/webapp/data/TruncatedIcosidodecahedron.obj
new file mode 100644
index 0000000..a86675d
--- /dev/null
+++ b/webapp/data/TruncatedIcosidodecahedron.obj
@@ -0,0 +1,182 @@
+v 0.5 0.5 3.73606797749979
+v 0.5 0.5 -3.73606797749979
+v 0.5 -0.5 3.73606797749979
+v 0.5 -0.5 -3.73606797749979
+v -0.5 0.5 3.73606797749979
+v -0.5 0.5 -3.73606797749979
+v -0.5 -0.5 3.73606797749979
+v -0.5 -0.5 -3.73606797749979
+v 3.73606797749979 0.5 0.5
+v 3.73606797749979 0.5 -0.5
+v 3.73606797749979 -0.5 0.5
+v 3.73606797749979 -0.5 -0.5
+v -3.73606797749979 0.5 0.5
+v -3.73606797749979 0.5 -0.5
+v -3.73606797749979 -0.5 0.5
+v -3.73606797749979 -0.5 -0.5
+v 0.5 3.73606797749979 0.5
+v 0.5 3.73606797749979 -0.5
+v 0.5 -3.73606797749979 0.5
+v 0.5 -3.73606797749979 -0.5
+v -0.5 3.73606797749979 0.5
+v -0.5 3.73606797749979 -0.5
+v -0.5 -3.73606797749979 0.5
+v -0.5 -3.73606797749979 -0.5
+v 1.0 1.3090169943749475 3.4270509831248424
+v 1.0 1.3090169943749475 -3.4270509831248424
+v 1.0 -1.3090169943749475 3.4270509831248424
+v 1.0 -1.3090169943749475 -3.4270509831248424
+v -1.0 1.3090169943749475 3.4270509831248424
+v -1.0 1.3090169943749475 -3.4270509831248424
+v -1.0 -1.3090169943749475 3.4270509831248424
+v -1.0 -1.3090169943749475 -3.4270509831248424
+v 3.4270509831248424 1.0 1.3090169943749475
+v 3.4270509831248424 1.0 -1.3090169943749475
+v 3.4270509831248424 -1.0 1.3090169943749475
+v 3.4270509831248424 -1.0 -1.3090169943749475
+v -3.4270509831248424 1.0 1.3090169943749475
+v -3.4270509831248424 1.0 -1.3090169943749475
+v -3.4270509831248424 -1.0 1.3090169943749475
+v -3.4270509831248424 -1.0 -1.3090169943749475
+v 1.3090169943749475 3.4270509831248424 1.0
+v 1.3090169943749475 3.4270509831248424 -1.0
+v 1.3090169943749475 -3.4270509831248424 1.0
+v 1.3090169943749475 -3.4270509831248424 -1.0
+v -1.3090169943749475 3.4270509831248424 1.0
+v -1.3090169943749475 3.4270509831248424 -1.0
+v -1.3090169943749475 -3.4270509831248424 1.0
+v -1.3090169943749475 -3.4270509831248424 -1.0
+v 0.5 2.118033988749895 3.118033988749895
+v 0.5 2.118033988749895 -3.118033988749895
+v 0.5 -2.118033988749895 3.118033988749895
+v 0.5 -2.118033988749895 -3.118033988749895
+v -0.5 2.118033988749895 3.118033988749895
+v -0.5 2.118033988749895 -3.118033988749895
+v -0.5 -2.118033988749895 3.118033988749895
+v -0.5 -2.118033988749895 -3.118033988749895
+v 3.118033988749895 0.5 2.118033988749895
+v 3.118033988749895 0.5 -2.118033988749895
+v 3.118033988749895 -0.5 2.118033988749895
+v 3.118033988749895 -0.5 -2.118033988749895
+v -3.118033988749895 0.5 2.118033988749895
+v -3.118033988749895 0.5 -2.118033988749895
+v -3.118033988749895 -0.5 2.118033988749895
+v -3.118033988749895 -0.5 -2.118033988749895
+v 2.118033988749895 3.118033988749895 0.5
+v 2.118033988749895 3.118033988749895 -0.5
+v 2.118033988749895 -3.118033988749895 0.5
+v 2.118033988749895 -3.118033988749895 -0.5
+v -2.118033988749895 3.118033988749895 0.5
+v -2.118033988749895 3.118033988749895 -0.5
+v -2.118033988749895 -3.118033988749895 0.5
+v -2.118033988749895 -3.118033988749895 -0.5
+v 1.8090169943749475 1.618033988749895 2.9270509831248424
+v 1.8090169943749475 1.618033988749895 -2.9270509831248424
+v 1.8090169943749475 -1.618033988749895 2.9270509831248424
+v 1.8090169943749475 -1.618033988749895 -2.9270509831248424
+v -1.8090169943749475 1.618033988749895 2.9270509831248424
+v -1.8090169943749475 1.618033988749895 -2.9270509831248424
+v -1.8090169943749475 -1.618033988749895 2.9270509831248424
+v -1.8090169943749475 -1.618033988749895 -2.9270509831248424
+v 2.9270509831248424 1.8090169943749475 1.618033988749895
+v 2.9270509831248424 1.8090169943749475 -1.618033988749895
+v 2.9270509831248424 -1.8090169943749475 1.618033988749895
+v 2.9270509831248424 -1.8090169943749475 -1.618033988749895
+v -2.9270509831248424 1.8090169943749475 1.618033988749895
+v -2.9270509831248424 1.8090169943749475 -1.618033988749895
+v -2.9270509831248424 -1.8090169943749475 1.618033988749895
+v -2.9270509831248424 -1.8090169943749475 -1.618033988749895
+v 1.618033988749895 2.9270509831248424 1.8090169943749475
+v 1.618033988749895 2.9270509831248424 -1.8090169943749475
+v 1.618033988749895 -2.9270509831248424 1.8090169943749475
+v 1.618033988749895 -2.9270509831248424 -1.8090169943749475
+v -1.618033988749895 2.9270509831248424 1.8090169943749475
+v -1.618033988749895 2.9270509831248424 -1.8090169943749475
+v -1.618033988749895 -2.9270509831248424 1.8090169943749475
+v -1.618033988749895 -2.9270509831248424 -1.8090169943749475
+v 1.3090169943749475 2.4270509831248424 2.618033988749895
+v 1.3090169943749475 2.4270509831248424 -2.618033988749895
+v 1.3090169943749475 -2.4270509831248424 2.618033988749895
+v 1.3090169943749475 -2.4270509831248424 -2.618033988749895
+v -1.3090169943749475 2.4270509831248424 2.618033988749895
+v -1.3090169943749475 2.4270509831248424 -2.618033988749895
+v -1.3090169943749475 -2.4270509831248424 2.618033988749895
+v -1.3090169943749475 -2.4270509831248424 -2.618033988749895
+v 2.618033988749895 1.3090169943749475 2.4270509831248424
+v 2.618033988749895 1.3090169943749475 -2.4270509831248424
+v 2.618033988749895 -1.3090169943749475 2.4270509831248424
+v 2.618033988749895 -1.3090169943749475 -2.4270509831248424
+v -2.618033988749895 1.3090169943749475 2.4270509831248424
+v -2.618033988749895 1.3090169943749475 -2.4270509831248424
+v -2.618033988749895 -1.3090169943749475 2.4270509831248424
+v -2.618033988749895 -1.3090169943749475 -2.4270509831248424
+v 2.4270509831248424 2.618033988749895 1.3090169943749475
+v 2.4270509831248424 2.618033988749895 -1.3090169943749475
+v 2.4270509831248424 -2.618033988749895 1.3090169943749475
+v 2.4270509831248424 -2.618033988749895 -1.3090169943749475
+v -2.4270509831248424 2.618033988749895 1.3090169943749475
+v -2.4270509831248424 2.618033988749895 -1.3090169943749475
+v -2.4270509831248424 -2.618033988749895 1.3090169943749475
+v -2.4270509831248424 -2.618033988749895 -1.3090169943749475
+f 1 3 27 75 107 59 57 105 73 25
+f 2 26 74 106 58 60 108 76 28 4
+f 5 29 77 109 61 63 111 79 31 7
+f 6 8 32 80 112 64 62 110 78 30
+f 9 10 34 82 114 66 65 113 81 33
+f 11 35 83 115 67 68 116 84 36 12
+f 13 37 85 117 69 70 118 86 38 14
+f 15 16 40 88 120 72 71 119 87 39
+f 17 21 45 93 101 53 49 97 89 41
+f 18 42 90 98 50 54 102 94 46 22
+f 19 43 91 99 51 55 103 95 47 23
+f 20 24 48 96 104 56 52 100 92 44
+f 1 25 49 53 29 5
+f 2 6 30 54 50 26
+f 3 7 31 55 51 27
+f 4 28 52 56 32 8
+f 9 33 57 59 35 11
+f 10 12 36 60 58 34
+f 13 15 39 63 61 37
+f 14 38 62 64 40 16
+f 17 41 65 66 42 18
+f 19 20 44 68 67 43
+f 21 22 46 70 69 45
+f 23 47 71 72 48 24
+f 73 105 81 113 89 97
+f 74 98 90 114 82 106
+f 75 99 91 115 83 107
+f 76 108 84 116 92 100
+f 77 101 93 117 85 109
+f 78 110 86 118 94 102
+f 79 111 87 119 95 103
+f 80 104 96 120 88 112
+f 1 5 7 3
+f 2 4 8 6
+f 9 11 12 10
+f 13 14 16 15
+f 17 18 22 21
+f 19 23 24 20
+f 25 73 97 49
+f 26 50 98 74
+f 27 51 99 75
+f 28 76 100 52
+f 29 53 101 77
+f 30 78 102 54
+f 31 79 103 55
+f 32 56 104 80
+f 33 81 105 57
+f 34 58 106 82
+f 35 59 107 83
+f 36 84 108 60
+f 37 61 109 85
+f 38 86 110 62
+f 39 87 111 63
+f 40 64 112 88
+f 41 89 113 65
+f 42 66 114 90
+f 43 67 115 91
+f 44 92 116 68
+f 45 69 117 93
+f 46 94 118 70
+f 47 95 119 71
+f 48 72 120 96
diff --git a/webapp/data/TruncatedOctahedron.obj b/webapp/data/TruncatedOctahedron.obj
new file mode 100644
index 0000000..6e81783
--- /dev/null
+++ b/webapp/data/TruncatedOctahedron.obj
@@ -0,0 +1,38 @@
+v 0.7071067811865476 0.0 1.4142135623730951
+v 0.7071067811865476 0.0 -1.4142135623730951
+v -0.7071067811865476 0.0 1.4142135623730951
+v -0.7071067811865476 0.0 -1.4142135623730951
+v 1.4142135623730951 0.7071067811865476 0.0
+v 1.4142135623730951 -0.7071067811865476 0.0
+v -1.4142135623730951 0.7071067811865476 0.0
+v -1.4142135623730951 -0.7071067811865476 0.0
+v 0.0 1.4142135623730951 0.7071067811865476
+v 0.0 1.4142135623730951 -0.7071067811865476
+v 0.0 -1.4142135623730951 0.7071067811865476
+v 0.0 -1.4142135623730951 -0.7071067811865476
+v 0.0 0.7071067811865476 1.4142135623730951
+v 0.0 0.7071067811865476 -1.4142135623730951
+v 0.0 -0.7071067811865476 1.4142135623730951
+v 0.0 -0.7071067811865476 -1.4142135623730951
+v 1.4142135623730951 0.0 0.7071067811865476
+v 1.4142135623730951 0.0 -0.7071067811865476
+v -1.4142135623730951 0.0 0.7071067811865476
+v -1.4142135623730951 0.0 -0.7071067811865476
+v 0.7071067811865476 1.4142135623730951 0.0
+v 0.7071067811865476 -1.4142135623730951 0.0
+v -0.7071067811865476 1.4142135623730951 0.0
+v -0.7071067811865476 -1.4142135623730951 0.0
+f 1 15 11 22 6 17
+f 2 14 10 21 5 18
+f 3 13 9 23 7 19
+f 4 16 12 24 8 20
+f 5 21 9 13 1 17
+f 6 22 12 16 2 18
+f 8 24 11 15 3 19
+f 7 23 10 14 4 20
+f 1 13 3 15
+f 2 16 4 14
+f 5 17 6 18
+f 7 20 8 19
+f 9 21 10 23
+f 11 24 12 22
diff --git a/webapp/data/TruncatedTetrahedron.obj b/webapp/data/TruncatedTetrahedron.obj
new file mode 100644
index 0000000..a6ee75b
--- /dev/null
+++ b/webapp/data/TruncatedTetrahedron.obj
@@ -0,0 +1,20 @@
+v 0.3535533905932738 -0.3535533905932738 1.0606601717798212
+v 0.3535533905932738 0.3535533905932738 -1.0606601717798212
+v -0.3535533905932738 0.3535533905932738 1.0606601717798212
+v -0.3535533905932738 -0.3535533905932738 -1.0606601717798212
+v 1.0606601717798212 -0.3535533905932738 0.3535533905932738
+v 1.0606601717798212 0.3535533905932738 -0.3535533905932738
+v -1.0606601717798212 0.3535533905932738 0.3535533905932738
+v -1.0606601717798212 -0.3535533905932738 -0.3535533905932738
+v 0.3535533905932738 -1.0606601717798212 0.3535533905932738
+v 0.3535533905932738 1.0606601717798212 -0.3535533905932738
+v -0.3535533905932738 1.0606601717798212 0.3535533905932738
+v -0.3535533905932738 -1.0606601717798212 -0.3535533905932738
+f 1 5 6 10 11 3
+f 2 6 5 9 12 4
+f 3 7 8 12 9 1
+f 4 8 7 11 10 2
+f 1 9 5
+f 2 10 6
+f 3 11 7
+f 4 12 8
diff --git a/webapp/fonts/pixelifysans-latin.woff2 b/webapp/fonts/pixelifysans-latin.woff2
new file mode 100644
index 0000000..fe867e9
Binary files /dev/null and b/webapp/fonts/pixelifysans-latin.woff2 differ
diff --git a/webapp/index.html b/webapp/index.html
new file mode 100644
index 0000000..e6ecf52
--- /dev/null
+++ b/webapp/index.html
@@ -0,0 +1,86 @@
+
+
+
+ Vertexprint
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This pane will populate after you generate artifacts
+
+
+
This pane will populate after you generate artifacts
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/openscad-wasm b/webapp/openscad-wasm
new file mode 160000
index 0000000..54ae2e7
--- /dev/null
+++ b/webapp/openscad-wasm
@@ -0,0 +1 @@
+Subproject commit 54ae2e7333d426951950aa6e238da4cc90304807
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
new file mode 100644
index 0000000..586d678
--- /dev/null
+++ b/webapp/package-lock.json
@@ -0,0 +1,2148 @@
+{
+ "name": "webapp",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@tailwindcss/cli": "^4.3.1",
+ "esbuild": "^0.28.1",
+ "jszip": "^3.10.1",
+ "ml-matrix": "^6.13.0",
+ "tailwindcss": "^4.3.1",
+ "three": "^0.184.0",
+ "typescript": "^6.0.3"
+ },
+ "devDependencies": {
+ "@iconify/json": "^2.2.490",
+ "@iconify/tailwind4": "^1.2.3",
+ "@types/three": "^0.185.0"
+ }
+ },
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@cyberalien/svg-utils": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@cyberalien/svg-utils/-/svg-utils-1.2.15.tgz",
+ "integrity": "sha512-ZbKU6npzW5PNocdoLVJYfKzaP+c/RpT6JUkoaKrW1DOcw6lyXub8XtcNpI3xok6FnyNjS6ZbsrrtjTnS9yeZAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/types": "^2.0.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@iconify/json": {
+ "version": "2.2.490",
+ "resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.490.tgz",
+ "integrity": "sha512-4hhzvLDCJ+oqkr6Awf5rZvgrAnELX/Jt9yWaQBYpWmh7jCGR76inYOrcMFXZrsTfhsIlJ+d/ziG9Ss/XBRNgXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/types": "*",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/@iconify/tailwind4": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@iconify/tailwind4/-/tailwind4-1.2.3.tgz",
+ "integrity": "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/tools": "^5.0.5",
+ "@iconify/types": "^2.0.0",
+ "@iconify/utils": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/cyberalien"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">= 4.0.0"
+ }
+ },
+ "node_modules/@iconify/tools": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/@iconify/tools/-/tools-5.0.12.tgz",
+ "integrity": "sha512-aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cyberalien/svg-utils": "^1.2.15",
+ "@iconify/types": "^2.0.0",
+ "@iconify/utils": "^3.1.3",
+ "fflate": "^0.8.3",
+ "modern-tar": "^0.7.6",
+ "pathe": "^2.0.3",
+ "svgo": "^4.0.1"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz",
+ "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/cli": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.1.tgz",
+ "integrity": "sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@parcel/watcher": "2.5.1",
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "enhanced-resolve": "5.21.6",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.3.1"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.185.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz",
+ "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/csso": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
+ "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "~2.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/css-tree": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
+ "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.28",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/mdn-data": {
+ "version": "2.0.28",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
+ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-any-array": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz",
+ "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==",
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss/node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/ml-array-max": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz",
+ "integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^3.0.0"
+ }
+ },
+ "node_modules/ml-array-min": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz",
+ "integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^3.0.0"
+ }
+ },
+ "node_modules/ml-array-rescale": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz",
+ "integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^3.0.0",
+ "ml-array-max": "^2.0.0",
+ "ml-array-min": "^2.0.0"
+ }
+ },
+ "node_modules/ml-matrix": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.13.0.tgz",
+ "integrity": "sha512-QpV0UTUkglg6vPUgThKGBEtit2ac6habSoZ33bwI9rU0UHZLqw6G3ukTIE8zWiUF3sjK8YAlhx/o/b9layzH8A==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^3.0.0",
+ "ml-array-rescale": "^2.0.0"
+ }
+ },
+ "node_modules/modern-tar": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
+ "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/package-manager-detector": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
+ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+ "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/svgo": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",
+ "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^11.1.0",
+ "css-select": "^5.1.0",
+ "css-tree": "^3.0.1",
+ "css-what": "^6.1.0",
+ "csso": "^5.0.5",
+ "picocolors": "^1.1.1",
+ "sax": "^1.5.0"
+ },
+ "bin": {
+ "svgo": "bin/svgo.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/svgo"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.184.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
+ "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/webapp/package.json b/webapp/package.json
new file mode 100644
index 0000000..9ad91e1
--- /dev/null
+++ b/webapp/package.json
@@ -0,0 +1,20 @@
+{
+ "dependencies": {
+ "@tailwindcss/cli": "^4.3.1",
+ "esbuild": "^0.28.1",
+ "jszip": "^3.10.1",
+ "ml-matrix": "^6.13.0",
+ "tailwindcss": "^4.3.1",
+ "three": "^0.184.0",
+ "typescript": "^6.0.3"
+ },
+ "scripts": {
+ "build:openscad-wasm": "make -C openscad-wasm",
+ "build": "esbuild app.ts --bundle --outfile=a.js --format=esm --target=es2022 --minify --tree-shaking=true --loader:.stl=file && esbuild worker.ts --bundle --outfile=worker.js --format=esm --target=es2022 --minify --tree-shaking=true && tailwindcss -i style.css -o a.css --minify && cp openscad-wasm/build/openscad.wasm.js openscad-wasm/build/openscad.wasm ."
+ },
+ "devDependencies": {
+ "@iconify/json": "^2.2.490",
+ "@iconify/tailwind4": "^1.2.3",
+ "@types/three": "^0.185.0"
+ }
+}
diff --git a/webapp/style.css b/webapp/style.css
new file mode 100644
index 0000000..1fb0f6c
--- /dev/null
+++ b/webapp/style.css
@@ -0,0 +1,93 @@
+@import "tailwindcss";
+
+/* Icon tailwind plugin */
+@plugin "@iconify/tailwind4";
+
+/* Pixelify Sans, vendored from Google Fonts (no runtime fetch). */
+@font-face {
+ font-family: "Pixelify Sans";
+ font-style: normal;
+ font-weight: 400 700;
+ font-display: swap;
+ src: url("fonts/pixelifysans-latin.woff2") format("woff2");
+}
+
+@theme {
+ --color-v-fg: #B1B1B3;
+ --color-v-bg: #232327;
+ --color-v-dark: #0C0C0D;
+ --color-v-panel: #18181A;
+ --color-v-border: #38383D;
+ --color-v-blue: #75BAFF;
+ --color-v-green: #86DE74;
+ --font-pixel: "Pixelify Sans", monospace;
+}
+
+/* Tailwind has no utilities for sliders */
+.dh-slider {
+ --fill: 0%;
+ background: linear-gradient(to right,
+ var(--color-v-blue) 0%, var(--color-v-blue) var(--fill),
+ var(--color-v-border) var(--fill), var(--color-v-border) 100%);
+ background-size: 100% 4px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.dh-slider::-webkit-slider-runnable-track {
+ height: 4px;
+ background: transparent;
+ border-radius: 0px;
+}
+.dh-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ margin-top: -4px;
+ background: var(--color-v-blue);
+ border: 0px;
+ border-radius: 0px;
+}
+.dh-slider::-moz-range-track {
+ height: 4px;
+ background: transparent;
+ border-radius: 0px;
+}
+.dh-slider::-moz-range-progress {
+ height: 4px;
+ background: var(--color-v-blue);
+ border-radius: 0px;
+}
+.dh-slider::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ background: var(--color-v-blue);
+ border: 0px;
+ border-radius: 0px;
+}
+
+input[type="number"] { color-scheme: dark; }
+input[type="number"] { -moz-appearance: textfield; }
+
+/* Highlight while resizing. The cursor can slip off the 2px wide handle, so
+ * :hover fails */
+#sidebar-resizer.dragging { background-color: var(--color-v-blue); }
+
+/* Text on hover is underlined */
+.opt-label:hover { text-decoration: underline; }
+
+.dh-tooltip {
+ position: fixed;
+ z-index: 100;
+ max-width: 256px;
+ padding: 8px 8px;
+ background: var(--color-v-dark);
+ color: var(--color-v-fg);
+ border: 1px solid var(--color-v-border);
+ font-family: monospace;
+ font-size: 11px;
+ line-height: 1.4;
+ pointer-events: none;
+}
+.dh-tooltip p { margin: 0; }
+.dh-tooltip p + p { margin-top: 4px; }
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json
new file mode 100644
index 0000000..2a6cb0f
--- /dev/null
+++ b/webapp/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "noImplicitAny": true,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ // "sourceMap": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "outDir": ".",
+ "allowImportingTsExtensions": false
+ },
+ "include": ["**/*.ts"],
+ "exclude": ["**/*.spec.ts", "**/*.js", "node_modules", "openscad", "three"]
+}
diff --git a/webapp/vertexprint.scad b/webapp/vertexprint.scad
new file mode 100644
index 0000000..e73132a
--- /dev/null
+++ b/webapp/vertexprint.scad
@@ -0,0 +1,237 @@
+// The js script sets all of these global variables with the -D flag
+// Values here are defaults.
+EDGE_DIAMETER = 3.0;
+DIAMETER_TOLERANCE_FIT = 0.35;
+DIAMETER_TAPER_DECREASE = 0.10;
+WALL_THICKNESS = 1.2;
+RADIUS = 200;
+ROD_INSET = 8;
+MIN_PRINTER_OVERHANG_ANGLE = 30;
+
+LABEL_VERTICES=false;
+TUBULAR_SUPPORTS=true;
+
+TUBE_DEPTH = ROD_INSET+WALL_THICKNESS;
+OUTER_TUBE_RADIUS = EDGE_DIAMETER/2+WALL_THICKNESS;
+
+INDEX = 0;
+
+FA=12;
+FS=1;
+
+// Openscad hangs if you don't set these lists with a flag
+vertex_figure = [];
+vertex_figure_edge = [];
+tag = "";
+index = 0.0;
+offsets = [];
+
+
+// Locus of lowest points along a cylinder. v is axis, r is radius, l is parameter
+function lowest_line_on_cylinder(v, l, r) =
+ let(
+ uv = v / norm(v),
+ center = uv * l,
+ down = [0, 0, -1],
+ proj = down - (down * uv) * uv
+ )
+ (norm(proj) < 1e-9)
+ ? center + [r, 0, 0]
+ : center + r * (proj / norm(proj));
+
+// Convert a unit vector to euler angles
+function direction_to_euler(v) =
+ [
+ 0,
+ atan2(norm([v[0], v[1]]), v[2]),
+ atan2(v[1], v[0])
+ ];
+
+// Calculates the smallest translation length l such that the outer cylinders (radius R)
+// just touch the inner cylinders (radius r).
+function axis_offset(v0, v1, R, r) =
+ let(
+ c = v0 * v1,
+ s = norm(cross(v0, v1)),
+
+ // Mode 1: Rim of outer contacts side of inner (active for acute angles)
+ l_side = (R * c + r) / s,
+
+ // Mode 2: Rim of inner touches base of outer (active for obtuse angles)
+ // Uses identity cot(theta/2) = (1 + cos theta) / sin theta
+ l_base = (r * (1 + c)) / s
+ )
+ // Handle parallel case to avoid division by zero
+ s < 1e-9 ? (c > 0 ? 1e9 : 0) :
+ max(l_side, l_base);
+
+
+// Find the vector most aligned with -z (lowest vector)
+function lowest_vector(vecs) =
+ let(
+ z_components = [for(v=vecs) v[2]],
+ min_z = min(z_components),
+ min_ix = search(min_z, z_components)[0]
+ )
+ min_ix;
+
+// Return the vector with minimum cosine distance to t; assume that t = vecs[0]
+function min_cos_dist(index, vecs) =
+ let (
+ scores = [for(i=[1:len(vecs)-1])
+ i == index ?
+ -1000 :
+ (vecs[index] * vecs[i]) / norm(vecs[i])],
+ ix = search(max(scores), scores)[0]
+ )
+ vecs[ix+1];
+
+// Height to translate the vertex holder before making the xy cut
+function offset_from_single_vec(index, vecs) =
+ let ( closest = min_cos_dist(index, vecs) )
+ axis_offset(vecs[index], closest, OUTER_TUBE_RADIUS, EDGE_DIAMETER/2);
+
+// Concat two lists of vectors, but keep only their z-components
+function concat_z(l1, l2) = [for(l=[l1, l2], a=l) a.z];
+
+function cutoff_height(vecs, offsets) =
+ let (
+ lowest_bottom_points = [for (i=[0:len(vecs)-1])
+ lowest_line_on_cylinder(vecs[i], offsets[i], OUTER_TUBE_RADIUS)],
+ lowest_top_points = [for (i=[0:len(vecs)-1])
+ lowest_line_on_cylinder(vecs[i], offsets[i] + TUBE_DEPTH, OUTER_TUBE_RADIUS)],
+ z_components = concat_z(lowest_bottom_points, lowest_top_points),
+ lowest_point = min(z_components)
+ )
+ lowest_point;
+
+module tubular_vertex_holder(vecs, offsets=[], edge_list=[], index) {
+ offsets = len(offsets) == 0 ?
+ [for (i=[0:len(vecs)-1]) offset_from_single_vec(i, vecs)] :
+ offsets;
+
+ vertex_offset = max(offsets);
+
+ cutoff = cutoff_height(vecs, offsets);
+
+ difference() {
+ for(i=[0:len(vecs)-1]) {
+ v = vecs[i];
+ rotation = direction_to_euler(v);
+ half_edge_offset = offsets[i];
+
+ // Add support structure if v sits below the minimum overhang angle
+ if (rotation[1] > 90 - MIN_PRINTER_OVERHANG_ANGLE && TUBULAR_SUPPORTS) {
+ lowest_top_point = lowest_line_on_cylinder(
+ v,
+ half_edge_offset+TUBE_DEPTH,
+ OUTER_TUBE_RADIUS);
+
+ base_inset =
+ abs(lowest_top_point.z - cutoff)
+ / tan(MIN_PRINTER_OVERHANG_ANGLE);
+
+ clamped_base_position = min(
+ max(half_edge_offset + TUBE_DEPTH - base_inset, 0),
+ norm([lowest_top_point.x, lowest_top_point.y]));
+
+ tube_top_to_cutoff_plane =
+ -OUTER_TUBE_RADIUS
+ -lowest_top_point.z
+ +cutoff
+ +(half_edge_offset+TUBE_DEPTH)*v.z;
+
+ hull() {
+ // Move endpoint to base position along cutoff plane;
+ translate(clamped_base_position * [v.x, v.y, 0])
+ // Move endpoint downwards to cutoff plane
+ translate([0, 0, tube_top_to_cutoff_plane])
+ rotate(rotation)
+ cube([0.1, OUTER_TUBE_RADIUS, 0.1], center=true);
+
+ translate([0, 0, tube_top_to_cutoff_plane])
+ rotate(rotation)
+ cube([0.1, OUTER_TUBE_RADIUS, 0.1], center=true);
+
+
+ translate(half_edge_offset * v)
+ rotate(rotation)
+ difference() {
+ union() {
+ cylinder(r=OUTER_TUBE_RADIUS, h=TUBE_DEPTH);
+ translate([0, 0, -half_edge_offset])
+ cylinder(r=OUTER_TUBE_RADIUS, h=WALL_THICKNESS+half_edge_offset);
+ }
+ translate([-50+(EDGE_DIAMETER+DIAMETER_TOLERANCE_FIT)/2, 0, 0])
+ cube([100, 100, 100], center=true);
+ }
+ }
+ }
+
+ // Tubes
+ translate(half_edge_offset * v)
+ rotate(rotation)
+ difference() {
+ union() {
+ cylinder(r=OUTER_TUBE_RADIUS, h=TUBE_DEPTH);
+ translate([0, 0, -half_edge_offset])
+ cylinder(r=OUTER_TUBE_RADIUS, h=WALL_THICKNESS+half_edge_offset);
+ }
+ // Add text to tube holders
+ if (LABEL_VERTICES) {
+ rotate([0, 0, 90])
+ intersection() {
+ translate([0, 0, TUBE_DEPTH-WALL_THICKNESS])
+ rotate([0, 90, 0])
+ linear_extrude(20)
+ text(str(edge_list[i]), valign="center", size = OUTER_TUBE_RADIUS);
+ difference() {
+ cylinder(r=OUTER_TUBE_RADIUS, h=RADIUS, center=true);
+ cylinder(r=OUTER_TUBE_RADIUS-0.5, h=RADIUS, center=true);
+ };
+ };
+ rotate([0, 0, 270])
+ intersection() {
+ translate([0, 0, TUBE_DEPTH-WALL_THICKNESS])
+ rotate([0, 90, 0])
+ linear_extrude(20)
+ text(str(index), valign="center", size = OUTER_TUBE_RADIUS);
+ difference() {
+ cylinder(r=OUTER_TUBE_RADIUS, h=RADIUS, center=true);
+ cylinder(r=OUTER_TUBE_RADIUS-0.5, h=RADIUS, center=true);
+ };
+ };
+ }
+ }
+ }
+
+ for(i=[0:len(vecs)-1]) {
+ v = vecs[i];
+ rotation = direction_to_euler(v);
+ half_edge_offset = offsets[i];
+ translate(half_edge_offset * v)
+ rotate(rotation)
+ // A tiny offset is added to the length of the internal cylinder
+ // to prevent z-fighting on the top surface
+ union() {
+ cylinder(
+ d1=EDGE_DIAMETER+DIAMETER_TOLERANCE_FIT-DIAMETER_TAPER_DECREASE,
+ d2=EDGE_DIAMETER+DIAMETER_TOLERANCE_FIT,
+ h=TUBE_DEPTH);
+ cylinder(
+ d=EDGE_DIAMETER+DIAMETER_TOLERANCE_FIT,
+ h=RADIUS);
+ }
+ }
+
+ // Flat bottom plane
+ translate([0, 0, -50+cutoff])
+ cube([100, 100, 100], center=true);
+ }
+}
+
+module vertex_holder(index) {
+ tubular_vertex_holder(vertex_figure, offsets, vertex_figure_edge, index);
+}
+
+vertex_holder($fa=FA, $fs=FS);
diff --git a/webapp/worker.ts b/webapp/worker.ts
new file mode 100644
index 0000000..281ca8d
--- /dev/null
+++ b/webapp/worker.ts
@@ -0,0 +1,40 @@
+import OpenSCAD from "./openscad-wasm/build/openscad.wasm.js";
+
+let instancePromise: Promise | null = null;
+
+self.onmessage = async (e: MessageEvent) => {
+ const msg = e.data;
+ try {
+ if (msg.type === "render") {
+ if (!instancePromise) {
+ instancePromise = OpenSCAD({
+ noInitialRun: true,
+ noExitRuntime: true,
+ locateFile: (path: string) =>
+ new URL(`./${path}`, import.meta.url).href,
+ });
+ }
+ const inst = await instancePromise;
+ inst.FS.writeFile("./input.scad", msg.source);
+ const filename = `v_${msg.name}_${msg.index}.stl`;
+ inst.callMain([
+ "./input.scad",
+ ...msg.cliArgs,
+ "-o",
+ filename,
+ ]);
+ const out = inst.FS.readFile(`./${filename}`, { encoding: "binary" });
+ const buffer = out.slice().buffer;
+ (self as unknown as Worker).postMessage(
+ { type: "rendered", index: msg.index, buffer },
+ [buffer],
+ );
+ }
+ } catch (err: any) {
+ (self as unknown as Worker).postMessage({
+ type: "error",
+ index: msg.index,
+ message: String(err?.message ?? err),
+ });
+ }
+};