#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <emscripten/emscripten.h>
#include <vector>
#include <string>
#include <sstream>
#include <cmath>
#include <cstring>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>

using namespace emscripten;

struct Vec3 {
    float x, y, z;
};

struct Face {
    std::vector<int> indices;
};

struct Mesh {
    std::vector<Vec3> vertices;
    std::vector<Vec3> faceNormals; // per-face normals (from file, e.g. STL)
    std::vector<Face> faces;
    std::vector<uint8_t> faceSelected;   // per-face selection (0/1)
    std::vector<uint8_t> vertexSelected; // per-vertex selection (0/1)
    std::vector<float> vertexQuality;    // per-vertex scalar quality
    std::vector<uint8_t> vertexColors;   // per-vertex RGB (3 bytes/vertex)
    bool hasQuality = false;
    bool hasColors = false;
    std::vector<float> texcoords;   // per-vertex UV (u,v pairs)
    bool hasUVs = false;
    std::string textureId;
    std::string mtllibName;
    float transformMatrix[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; // column-major identity

    void ensureSelectionSized() {
        faceSelected.resize(faces.size(), 0);
        vertexSelected.resize(vertices.size(), 0);
    }

    // Triangulated buffers for rendering
    std::vector<float> vertexBuffer;   // x,y,z,...
    std::vector<uint32_t> indexBuffer; // i0,i1,i2,...
    std::vector<float> normalBuffer;   // nx,ny,nz,...

    void computeNormals() {
        normalBuffer.clear();
        std::vector<Vec3> vertNormals(vertices.size(), {0, 0, 0});

        for (const auto& face : faces) {
            if (face.indices.size() < 3) continue;
            for (size_t i = 1; i + 1 < face.indices.size(); i++) {
                int i0 = face.indices[0];
                int i1 = face.indices[i];
                int i2 = face.indices[i + 1];

                const Vec3& v0 = vertices[i0];
                const Vec3& v1 = vertices[i1];
                const Vec3& v2 = vertices[i2];

                float e1x = v1.x - v0.x, e1y = v1.y - v0.y, e1z = v1.z - v0.z;
                float e2x = v2.x - v0.x, e2y = v2.y - v0.y, e2z = v2.z - v0.z;
                // Area-weighted normal (cross product, not normalized per-face)
                float nx = e1y * e2z - e1z * e2y;
                float ny = e1z * e2x - e1x * e2z;
                float nz = e1x * e2y - e1y * e2x;

                vertNormals[i0].x += nx; vertNormals[i0].y += ny; vertNormals[i0].z += nz;
                vertNormals[i1].x += nx; vertNormals[i1].y += ny; vertNormals[i1].z += nz;
                vertNormals[i2].x += nx; vertNormals[i2].y += ny; vertNormals[i2].z += nz;
            }
        }

        normalBuffer.reserve(vertices.size() * 3);
        for (const auto& n : vertNormals) {
            float len = std::sqrt(n.x * n.x + n.y * n.y + n.z * n.z);
            if (len > 0.0f) {
                normalBuffer.push_back(n.x / len);
                normalBuffer.push_back(n.y / len);
                normalBuffer.push_back(n.z / len);
            } else {
                normalBuffer.push_back(0);
                normalBuffer.push_back(0);
                normalBuffer.push_back(1);
            }
        }
    }

    void triangulate() {
        vertexBuffer.clear();
        indexBuffer.clear();

        vertexBuffer.reserve(vertices.size() * 3);
        for (const auto& v : vertices) {
            vertexBuffer.push_back(v.x);
            vertexBuffer.push_back(v.y);
            vertexBuffer.push_back(v.z);
        }

        for (const auto& face : faces) {
            if (face.indices.size() < 3) continue;
            for (size_t i = 1; i + 1 < face.indices.size(); i++) {
                indexBuffer.push_back(static_cast<uint32_t>(face.indices[0]));
                indexBuffer.push_back(static_cast<uint32_t>(face.indices[i]));
                indexBuffer.push_back(static_cast<uint32_t>(face.indices[i + 1]));
            }
        }

        computeNormals();
        ensureSelectionSized();
    }
};

// Global mesh storage
static std::unordered_map<int, Mesh> meshStore;
static int nextHandle = 0;

static int storeMesh(Mesh&& mesh) {
    mesh.triangulate();
    int handle = nextHandle++;
    meshStore[handle] = std::move(mesh);
    return handle;
}

// ============ OBJ Parser ============

int loadMeshFromOBJ(const std::string& objText) {
    Mesh mesh;
    std::istringstream stream(objText);
    std::string line;

    while (std::getline(stream, line)) {
        if (line.empty() || line[0] == '#') continue;
        std::istringstream ls(line);
        std::string type;
        ls >> type;

        if (type == "v") {
            Vec3 v;
            ls >> v.x >> v.y >> v.z;
            mesh.vertices.push_back(v);
        } else if (type == "f") {
            Face face;
            std::string token;
            while (ls >> token) {
                int vi = 0;
                size_t slashPos = token.find('/');
                if (slashPos != std::string::npos) {
                    vi = std::stoi(token.substr(0, slashPos));
                } else {
                    vi = std::stoi(token);
                }
                face.indices.push_back(vi - 1);
            }
            mesh.faces.push_back(face);
        }
    }

    return storeMesh(std::move(mesh));
}

// ============ STL Parser ============

static bool isAsciiSTL(const uint8_t* data, size_t len) {
    if (len < 6) return false;
    return std::memcmp(data, "solid ", 6) == 0 || std::memcmp(data, "solid\n", 6) == 0;
}

static int parseAsciiSTL(const std::string& text) {
    Mesh mesh;
    std::istringstream stream(text);
    std::string line;

    // STL vertices are per-face (not shared). We merge identical vertices.
    struct VKey {
        float x, y, z;
        bool operator==(const VKey& o) const {
            return x == o.x && y == o.y && z == o.z;
        }
    };
    struct VHash {
        size_t operator()(const VKey& k) const {
            size_t h = 0;
            auto hf = std::hash<float>{};
            h ^= hf(k.x) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.y) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.z) + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };
    std::unordered_map<VKey, int, VHash> vertMap;

    auto getOrAddVertex = [&](float x, float y, float z) -> int {
        VKey key{x, y, z};
        auto it = vertMap.find(key);
        if (it != vertMap.end()) return it->second;
        int idx = static_cast<int>(mesh.vertices.size());
        mesh.vertices.push_back({x, y, z});
        vertMap[key] = idx;
        return idx;
    };

    while (std::getline(stream, line)) {
        // Trim whitespace
        size_t start = line.find_first_not_of(" \t\r\n");
        if (start == std::string::npos) continue;
        line = line.substr(start);

        if (line.rfind("facet normal", 0) == 0) {
            // Read 3 vertices
            std::string dummy;
            std::getline(stream, dummy); // "outer loop"

            Face face;
            for (int v = 0; v < 3; v++) {
                std::string vline;
                std::getline(stream, vline);
                std::istringstream vls(vline);
                std::string vtag;
                float x, y, z;
                vls >> vtag >> x >> y >> z;
                face.indices.push_back(getOrAddVertex(x, y, z));
            }
            mesh.faces.push_back(face);

            std::getline(stream, dummy); // "endloop"
            std::getline(stream, dummy); // "endfacet"
        }
    }

    return storeMesh(std::move(mesh));
}

static int parseBinarySTL(const uint8_t* data, size_t len) {
    if (len < 84) return -1;

    uint32_t numTriangles;
    std::memcpy(&numTriangles, data + 80, 4);

    size_t expectedSize = 84 + static_cast<size_t>(numTriangles) * 50;
    if (len < expectedSize) return -1;

    Mesh mesh;

    struct VKey {
        float x, y, z;
        bool operator==(const VKey& o) const {
            return x == o.x && y == o.y && z == o.z;
        }
    };
    struct VHash {
        size_t operator()(const VKey& k) const {
            size_t h = 0;
            auto hf = std::hash<float>{};
            h ^= hf(k.x) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.y) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.z) + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };
    std::unordered_map<VKey, int, VHash> vertMap;

    auto getOrAddVertex = [&](float x, float y, float z) -> int {
        VKey key{x, y, z};
        auto it = vertMap.find(key);
        if (it != vertMap.end()) return it->second;
        int idx = static_cast<int>(mesh.vertices.size());
        mesh.vertices.push_back({x, y, z});
        vertMap[key] = idx;
        return idx;
    };

    const uint8_t* ptr = data + 84;
    for (uint32_t i = 0; i < numTriangles; i++) {
        // Skip normal (12 bytes)
        ptr += 12;

        Face face;
        for (int v = 0; v < 3; v++) {
            float x, y, z;
            std::memcpy(&x, ptr, 4); ptr += 4;
            std::memcpy(&y, ptr, 4); ptr += 4;
            std::memcpy(&z, ptr, 4); ptr += 4;
            face.indices.push_back(getOrAddVertex(x, y, z));
        }
        mesh.faces.push_back(face);

        // Skip attribute byte count
        ptr += 2;
    }

    return storeMesh(std::move(mesh));
}

int loadMeshFromSTL(uintptr_t dataPtr, int byteLength) {
    const uint8_t* data = reinterpret_cast<const uint8_t*>(dataPtr);
    size_t len = static_cast<size_t>(byteLength);

    if (isAsciiSTL(data, len)) {
        std::string text(reinterpret_cast<const char*>(data), len);
        return parseAsciiSTL(text);
    }
    return parseBinarySTL(data, len);
}

// ============ PLY Parser (ASCII) ============

int loadMeshFromPLY(const std::string& plyText) {
    Mesh mesh;
    std::istringstream stream(plyText);
    std::string line;

    int numVertices = 0, numFaces = 0;
    bool inHeader = true;
    bool hasRGB = false;
    int propsBeforeColor = 3; // x,y,z are first 3 properties

    // Parse header
    std::vector<std::string> vertexProps;
    bool inVertexElement = false;
    while (inHeader && std::getline(stream, line)) {
        std::istringstream ls(line);
        std::string token;
        ls >> token;

        if (token == "element") {
            std::string elemType;
            int count;
            ls >> elemType >> count;
            if (elemType == "vertex") { numVertices = count; inVertexElement = true; }
            else { inVertexElement = false; if (elemType == "face") numFaces = count; }
        } else if (token == "property" && inVertexElement) {
            std::string ptype, pname;
            ls >> ptype >> pname;
            vertexProps.push_back(pname);
            if (pname == "red") hasRGB = true;
        } else if (token == "end_header") {
            inHeader = false;
        }
    }

    // Find column indices for red/green/blue
    int redCol = -1, greenCol = -1, blueCol = -1;
    for (size_t i = 0; i < vertexProps.size(); i++) {
        if (vertexProps[i] == "red") redCol = static_cast<int>(i);
        if (vertexProps[i] == "green") greenCol = static_cast<int>(i);
        if (vertexProps[i] == "blue") blueCol = static_cast<int>(i);
    }

    // Read vertices (+ optional colors)
    for (int i = 0; i < numVertices && std::getline(stream, line); i++) {
        std::istringstream ls(line);
        std::vector<float> vals;
        float val;
        while (ls >> val) vals.push_back(val);

        Vec3 v{0, 0, 0};
        if (vals.size() >= 3) { v.x = vals[0]; v.y = vals[1]; v.z = vals[2]; }
        mesh.vertices.push_back(v);

        if (hasRGB && redCol >= 0 && greenCol >= 0 && blueCol >= 0 &&
            static_cast<size_t>(std::max({redCol, greenCol, blueCol})) < vals.size()) {
            mesh.vertexColors.push_back(static_cast<uint8_t>(vals[redCol]));
            mesh.vertexColors.push_back(static_cast<uint8_t>(vals[greenCol]));
            mesh.vertexColors.push_back(static_cast<uint8_t>(vals[blueCol]));
        }
    }
    if (hasRGB && mesh.vertexColors.size() == mesh.vertices.size() * 3) {
        mesh.hasColors = true;
    }

    // Read faces
    for (int i = 0; i < numFaces && std::getline(stream, line); i++) {
        std::istringstream ls(line);
        int count;
        ls >> count;
        Face face;
        for (int j = 0; j < count; j++) {
            int idx;
            ls >> idx;
            face.indices.push_back(idx);
        }
        mesh.faces.push_back(face);
    }

    return storeMesh(std::move(mesh));
}

// ============ OFF Parser ============

int loadMeshFromOFF(const std::string& offText) {
    Mesh mesh;
    std::istringstream stream(offText);
    std::string line;

    // Read header: OFF or COFF
    std::getline(stream, line);
    // Trim
    while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back();
    bool hasColor = (line.find("COFF") != std::string::npos);

    // Read counts
    int numVerts = 0, numFaces = 0, numEdges = 0;
    while (std::getline(stream, line)) {
        if (line.empty() || line[0] == '#') continue;
        std::istringstream ls(line);
        ls >> numVerts >> numFaces >> numEdges;
        break;
    }

    // Read vertices
    for (int i = 0; i < numVerts && std::getline(stream, line); i++) {
        if (line.empty() || line[0] == '#') { i--; continue; }
        std::istringstream ls(line);
        Vec3 v;
        ls >> v.x >> v.y >> v.z;
        mesh.vertices.push_back(v);
        if (hasColor) {
            float r, g, b, a = 1;
            ls >> r >> g >> b;
            if (ls) ls >> a;
            mesh.vertexColors.push_back(static_cast<uint8_t>(std::min(255.0f, r * 255)));
            mesh.vertexColors.push_back(static_cast<uint8_t>(std::min(255.0f, g * 255)));
            mesh.vertexColors.push_back(static_cast<uint8_t>(std::min(255.0f, b * 255)));
        }
    }
    if (hasColor && mesh.vertexColors.size() == mesh.vertices.size() * 3) {
        mesh.hasColors = true;
    }

    // Read faces
    for (int i = 0; i < numFaces && std::getline(stream, line); i++) {
        if (line.empty() || line[0] == '#') { i--; continue; }
        std::istringstream ls(line);
        int count;
        ls >> count;
        Face face;
        for (int j = 0; j < count; j++) {
            int idx; ls >> idx;
            face.indices.push_back(idx);
        }
        mesh.faces.push_back(face);
    }

    return storeMesh(std::move(mesh));
}

// ============ Binary PLY Parser ============

int loadMeshFromPLYBinary(uintptr_t dataPtr, int byteLength) {
    const uint8_t* data = reinterpret_cast<const uint8_t*>(dataPtr);
    const char* text = reinterpret_cast<const char*>(data);
    size_t len = static_cast<size_t>(byteLength);

    // Parse header as text
    std::string header;
    size_t headerEnd = 0;
    for (size_t i = 0; i + 10 < len; i++) {
        if (std::memcmp(text + i, "end_header", 10) == 0) {
            // Find the newline after end_header
            headerEnd = i + 10;
            while (headerEnd < len && text[headerEnd] != '\n') headerEnd++;
            headerEnd++; // skip the newline
            header = std::string(text, headerEnd);
            break;
        }
    }
    if (headerEnd == 0) return -1;

    // Parse header fields
    std::istringstream hs(header);
    std::string hline;
    int numVerts = 0, numFaces = 0;
    bool isLittleEndian = true;
    bool hasRGB = false;

    struct PropInfo { std::string name; std::string type; int size; };
    std::vector<PropInfo> vertProps;
    bool inVertexElem = false;
    int vertStride = 0;

    while (std::getline(hs, hline)) {
        std::istringstream ls(hline);
        std::string tok; ls >> tok;
        if (tok == "format") {
            std::string fmt; ls >> fmt;
            isLittleEndian = (fmt.find("little") != std::string::npos);
        } else if (tok == "element") {
            std::string etype; int cnt; ls >> etype >> cnt;
            if (etype == "vertex") { numVerts = cnt; inVertexElem = true; }
            else { inVertexElem = false; if (etype == "face") numFaces = cnt; }
        } else if (tok == "property" && inVertexElem) {
            std::string ptype, pname; ls >> ptype >> pname;
            int sz = 4;
            if (ptype == "uchar" || ptype == "uint8") sz = 1;
            else if (ptype == "short" || ptype == "int16") sz = 2;
            else if (ptype == "double" || ptype == "float64") sz = 8;
            vertProps.push_back({pname, ptype, sz});
            vertStride += sz;
            if (pname == "red") hasRGB = true;
        }
    }

    // Find property indices
    int xOff = -1, yOff = -1, zOff = -1, rOff = -1, gOff = -1, bOff = -1;
    int off = 0;
    for (auto& p : vertProps) {
        if (p.name == "x") xOff = off;
        else if (p.name == "y") yOff = off;
        else if (p.name == "z") zOff = off;
        else if (p.name == "red") rOff = off;
        else if (p.name == "green") gOff = off;
        else if (p.name == "blue") bOff = off;
        off += p.size;
    }

    Mesh mesh;
    const uint8_t* ptr = data + headerEnd;

    // Read vertices
    for (int i = 0; i < numVerts; i++) {
        float x = 0, y = 0, z = 0;
        if (xOff >= 0) std::memcpy(&x, ptr + xOff, 4);
        if (yOff >= 0) std::memcpy(&y, ptr + yOff, 4);
        if (zOff >= 0) std::memcpy(&z, ptr + zOff, 4);
        mesh.vertices.push_back({x, y, z});
        if (hasRGB && rOff >= 0 && gOff >= 0 && bOff >= 0) {
            mesh.vertexColors.push_back(ptr[rOff]);
            mesh.vertexColors.push_back(ptr[gOff]);
            mesh.vertexColors.push_back(ptr[bOff]);
        }
        ptr += vertStride;
    }
    if (hasRGB && mesh.vertexColors.size() == mesh.vertices.size() * 3) {
        mesh.hasColors = true;
    }

    // Read faces (format: uchar count, then count × int32 indices)
    for (int i = 0; i < numFaces; i++) {
        uint8_t count = *ptr; ptr++;
        Face face;
        for (int j = 0; j < count; j++) {
            int32_t idx;
            std::memcpy(&idx, ptr, 4); ptr += 4;
            face.indices.push_back(idx);
        }
        mesh.faces.push_back(face);
    }

    return storeMesh(std::move(mesh));
}

// ============ PLY / OFF Export ============

std::string exportToPLY(int handle, bool includeColors) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return "";
    auto& mesh = it->second;
    bool writeColors = includeColors && mesh.hasColors && mesh.vertexColors.size() == mesh.vertices.size() * 3;

    std::ostringstream out;
    out << "ply\nformat ascii 1.0\n";
    out << "element vertex " << mesh.vertices.size() << "\n";
    out << "property float x\nproperty float y\nproperty float z\n";
    if (writeColors) {
        out << "property uchar red\nproperty uchar green\nproperty uchar blue\n";
    }
    out << "element face " << mesh.faces.size() << "\n";
    out << "property list uchar int vertex_indices\n";
    out << "end_header\n";

    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        out << mesh.vertices[i].x << " " << mesh.vertices[i].y << " " << mesh.vertices[i].z;
        if (writeColors) {
            out << " " << (int)mesh.vertexColors[i*3] << " " << (int)mesh.vertexColors[i*3+1] << " " << (int)mesh.vertexColors[i*3+2];
        }
        out << "\n";
    }
    for (auto& f : mesh.faces) {
        out << f.indices.size();
        for (int idx : f.indices) out << " " << idx;
        out << "\n";
    }
    return out.str();
}

std::string exportToOFF(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return "";
    auto& mesh = it->second;

    std::ostringstream out;
    out << "OFF\n";
    out << mesh.vertices.size() << " " << mesh.faces.size() << " 0\n";
    for (auto& v : mesh.vertices) out << v.x << " " << v.y << " " << v.z << "\n";
    for (auto& f : mesh.faces) {
        out << f.indices.size();
        for (int idx : f.indices) out << " " << idx;
        out << "\n";
    }
    return out.str();
}

// ============ Format auto-detection ============

// Forward declaration
int loadPointCloudFromXYZ(const std::string& text);

int loadMesh(uintptr_t dataPtr, int byteLength, const std::string& filename) {
    const uint8_t* data = reinterpret_cast<const uint8_t*>(dataPtr);

    std::string ext;
    auto dotPos = filename.rfind('.');
    if (dotPos != std::string::npos) {
        ext = filename.substr(dotPos + 1);
        std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
    }

    if (ext == "stl") {
        return loadMeshFromSTL(dataPtr, byteLength);
    } else if (ext == "ply") {
        // Auto-detect ASCII vs binary PLY
        std::string header(reinterpret_cast<const char*>(data), std::min(static_cast<size_t>(byteLength), size_t(512)));
        if (header.find("binary") != std::string::npos) {
            return loadMeshFromPLYBinary(dataPtr, byteLength);
        }
        std::string text(reinterpret_cast<const char*>(data), static_cast<size_t>(byteLength));
        return loadMeshFromPLY(text);
    } else if (ext == "off" || ext == "coff") {
        std::string text(reinterpret_cast<const char*>(data), static_cast<size_t>(byteLength));
        return loadMeshFromOFF(text);
    } else if (ext == "xyz" || ext == "pts") {
        std::string text(reinterpret_cast<const char*>(data), static_cast<size_t>(byteLength));
        return loadPointCloudFromXYZ(text);
    } else {
        // Default: OBJ
        std::string text(reinterpret_cast<const char*>(data), static_cast<size_t>(byteLength));
        return loadMeshFromOBJ(text);
    }
}

// ============ Mesh info ============

int getVertexCount(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return -1;
    return static_cast<int>(it->second.vertices.size());
}

int getFaceCount(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return -1;
    return static_cast<int>(it->second.faces.size());
}

val getBoundingBox(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();

    const auto& verts = it->second.vertices;
    if (verts.empty()) return val::null();

    float minX = verts[0].x, minY = verts[0].y, minZ = verts[0].z;
    float maxX = verts[0].x, maxY = verts[0].y, maxZ = verts[0].z;

    for (const auto& v : verts) {
        if (v.x < minX) minX = v.x; if (v.x > maxX) maxX = v.x;
        if (v.y < minY) minY = v.y; if (v.y > maxY) maxY = v.y;
        if (v.z < minZ) minZ = v.z; if (v.z > maxZ) maxZ = v.z;
    }

    val result = val::array();
    result.call<void>("push", minX);
    result.call<void>("push", minY);
    result.call<void>("push", minZ);
    result.call<void>("push", maxX);
    result.call<void>("push", maxY);
    result.call<void>("push", maxZ);
    return result;
}

// ============ Buffers ============

val getVertexBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    const auto& buf = it->second.vertexBuffer;
    return val(typed_memory_view(buf.size(), buf.data()));
}

val getIndexBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    const auto& buf = it->second.indexBuffer;
    return val(typed_memory_view(buf.size(), buf.data()));
}

val getNormalBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    if (mesh.normalBuffer.empty()) {
        mesh.computeNormals();
    }
    return val(typed_memory_view(mesh.normalBuffer.size(), mesh.normalBuffer.data()));
}

void recomputeNormals(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    it->second.computeNormals();
}

void deleteMesh(int handle) {
    meshStore.erase(handle);
}

// ============ Export ============

std::string exportToOBJ(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return "";

    const auto& mesh = it->second;
    std::ostringstream out;
    out << "# Exported from MeshLab Web\n";

    for (const auto& v : mesh.vertices) {
        out << "v " << v.x << " " << v.y << " " << v.z << "\n";
    }
    for (const auto& f : mesh.faces) {
        out << "f";
        for (int idx : f.indices) {
            out << " " << (idx + 1); // OBJ is 1-indexed
        }
        out << "\n";
    }

    return out.str();
}

val exportToSTL(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();

    const auto& mesh = it->second;

    // Count triangles after triangulation
    uint32_t numTriangles = 0;
    for (const auto& face : mesh.faces) {
        if (face.indices.size() >= 3) {
            numTriangles += static_cast<uint32_t>(face.indices.size() - 2);
        }
    }

    size_t bufSize = 84 + static_cast<size_t>(numTriangles) * 50;
    std::vector<uint8_t> buf(bufSize, 0);

    // Header
    const char* header = "Binary STL - MeshLab Web";
    std::memcpy(buf.data(), header, std::strlen(header));
    std::memcpy(buf.data() + 80, &numTriangles, 4);

    uint8_t* ptr = buf.data() + 84;
    for (const auto& face : mesh.faces) {
        if (face.indices.size() < 3) continue;
        for (size_t i = 1; i + 1 < face.indices.size(); i++) {
            int i0 = face.indices[0];
            int i1 = face.indices[i];
            int i2 = face.indices[i + 1];

            const Vec3& v0 = mesh.vertices[i0];
            const Vec3& v1 = mesh.vertices[i1];
            const Vec3& v2 = mesh.vertices[i2];

            // Compute face normal
            float e1x = v1.x - v0.x, e1y = v1.y - v0.y, e1z = v1.z - v0.z;
            float e2x = v2.x - v0.x, e2y = v2.y - v0.y, e2z = v2.z - v0.z;
            float nx = e1y * e2z - e1z * e2y;
            float ny = e1z * e2x - e1x * e2z;
            float nz = e1x * e2y - e1y * e2x;
            float len = std::sqrt(nx*nx + ny*ny + nz*nz);
            if (len > 0) { nx /= len; ny /= len; nz /= len; }

            std::memcpy(ptr, &nx, 4); ptr += 4;
            std::memcpy(ptr, &ny, 4); ptr += 4;
            std::memcpy(ptr, &nz, 4); ptr += 4;

            auto writeVert = [&](const Vec3& v) {
                std::memcpy(ptr, &v.x, 4); ptr += 4;
                std::memcpy(ptr, &v.y, 4); ptr += 4;
                std::memcpy(ptr, &v.z, 4); ptr += 4;
            };
            writeVert(v0);
            writeVert(v1);
            writeVert(v2);

            uint16_t attrByteCount = 0;
            std::memcpy(ptr, &attrByteCount, 2); ptr += 2;
        }
    }

    return val(typed_memory_view(buf.size(), buf.data()));
}

// ============ Filters ============

void filterScale(int handle, float sx, float sy, float sz) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    for (auto& v : mesh.vertices) {
        v.x *= sx; v.y *= sy; v.z *= sz;
    }
    mesh.triangulate();
}

void filterTranslate(int handle, float tx, float ty, float tz) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    for (auto& v : mesh.vertices) {
        v.x += tx; v.y += ty; v.z += tz;
    }
    mesh.triangulate();
}

void filterRotate(int handle, int axis, float angleDeg) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    float rad = angleDeg * 3.14159265358979f / 180.0f;
    float c = std::cos(rad), s = std::sin(rad);

    for (auto& v : mesh.vertices) {
        float a, b;
        switch (axis) {
            case 0: // X
                a = v.y; b = v.z;
                v.y = c * a - s * b;
                v.z = s * a + c * b;
                break;
            case 1: // Y
                a = v.x; b = v.z;
                v.x = c * a + s * b;
                v.z = -s * a + c * b;
                break;
            case 2: // Z
                a = v.x; b = v.y;
                v.x = c * a - s * b;
                v.y = s * a + c * b;
                break;
        }
    }
    mesh.triangulate();
}

void filterInvertFaces(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    for (auto& face : mesh.faces) {
        std::reverse(face.indices.begin(), face.indices.end());
    }
    mesh.triangulate();
}

// ---- Adjacency helper: build vertex→neighbor map from faces ----
static std::vector<std::vector<int>> buildAdjacency(const Mesh& mesh) {
    std::vector<std::vector<int>> adj(mesh.vertices.size());
    for (const auto& face : mesh.faces) {
        for (size_t i = 0; i < face.indices.size(); i++) {
            int a = face.indices[i];
            int b = face.indices[(i + 1) % face.indices.size()];
            adj[a].push_back(b);
            adj[b].push_back(a);
        }
    }
    // Deduplicate
    for (auto& neighbors : adj) {
        std::sort(neighbors.begin(), neighbors.end());
        neighbors.erase(std::unique(neighbors.begin(), neighbors.end()), neighbors.end());
    }
    return adj;
}

// ---- Laplacian step (used by both laplacian and taubin) ----
static void laplacianStep(Mesh& mesh, const std::vector<std::vector<int>>& adj, float factor) {
    size_t n = mesh.vertices.size();
    std::vector<Vec3> newPos(n);

    for (size_t i = 0; i < n; i++) {
        const auto& nb = adj[i];
        if (nb.empty()) {
            newPos[i] = mesh.vertices[i];
            continue;
        }
        Vec3 centroid{0, 0, 0};
        for (int j : nb) {
            centroid.x += mesh.vertices[j].x;
            centroid.y += mesh.vertices[j].y;
            centroid.z += mesh.vertices[j].z;
        }
        float invN = 1.0f / static_cast<float>(nb.size());
        centroid.x *= invN;
        centroid.y *= invN;
        centroid.z *= invN;

        newPos[i].x = mesh.vertices[i].x + factor * (centroid.x - mesh.vertices[i].x);
        newPos[i].y = mesh.vertices[i].y + factor * (centroid.y - mesh.vertices[i].y);
        newPos[i].z = mesh.vertices[i].z + factor * (centroid.z - mesh.vertices[i].z);
    }
    mesh.vertices = newPos;
}

void filterLaplacianSmooth(int handle, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    auto adj = buildAdjacency(mesh);
    for (int i = 0; i < iterations; i++) {
        laplacianStep(mesh, adj, 1.0f);
    }
    mesh.triangulate();
}

void filterTaubinSmooth(int handle, float lambda, float mu, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    auto adj = buildAdjacency(mesh);
    for (int i = 0; i < iterations; i++) {
        laplacianStep(mesh, adj, lambda);
        laplacianStep(mesh, adj, mu);
    }
    mesh.triangulate();
}

// ---- QEM (Quadric Error Metrics) Simplification (Garland & Heckbert 1997) ----

// Quadric: represents the squared distance to a set of planes
// Stored as symmetric 4×4 matrix upper triangle: a[10] = {a00,a01,a02,a03,a11,a12,a13,a22,a23,a33}
struct Quadric {
    double a[10] = {0,0,0,0,0,0,0,0,0,0};

    void addPlane(float nx, float ny, float nz, float d) {
        // Kp = pp^T where p = [nx, ny, nz, d]
        a[0]+=nx*nx; a[1]+=nx*ny; a[2]+=nx*nz; a[3]+=nx*d;
        a[4]+=ny*ny; a[5]+=ny*nz; a[6]+=ny*d;
        a[7]+=nz*nz; a[8]+=nz*d; a[9]+=d*d;
    }

    Quadric operator+(const Quadric& o) const {
        Quadric r;
        for (int i=0;i<10;i++) r.a[i] = a[i]+o.a[i];
        return r;
    }

    double evaluate(float x, float y, float z) const {
        // v^T Q v where v=[x,y,z,1]
        return a[0]*x*x + 2*a[1]*x*y + 2*a[2]*x*z + 2*a[3]*x
             + a[4]*y*y + 2*a[5]*y*z + 2*a[6]*y
             + a[7]*z*z + 2*a[8]*z + a[9];
    }

    // Solve for optimal position: minimize v^T Q v
    // Returns false if matrix is singular
    bool optimalPosition(float& ox, float& oy, float& oz) const {
        // Solve 3×3 system: A*v = -b where A = upper-left 3×3, b = [a[3],a[6],a[8]]
        double A[3][3] = {{a[0],a[1],a[2]},{a[1],a[4],a[5]},{a[2],a[5],a[7]}};
        double b[3] = {-a[3], -a[6], -a[8]};

        // Gaussian elimination with partial pivoting
        double aug[3][4];
        for (int i=0;i<3;i++) { for (int j=0;j<3;j++) aug[i][j]=A[i][j]; aug[i][3]=b[i]; }

        for (int col=0;col<3;col++) {
            // Find pivot
            int piv=col; double mx=std::abs(aug[col][col]);
            for (int row=col+1;row<3;row++) { if (std::abs(aug[row][col])>mx) { mx=std::abs(aug[row][col]); piv=row; } }
            if (mx < 1e-10) return false; // Singular
            if (piv!=col) std::swap(aug[piv], aug[col]);
            for (int row=col+1;row<3;row++) {
                double f = aug[row][col]/aug[col][col];
                for (int j=col;j<4;j++) aug[row][j] -= f*aug[col][j];
            }
        }
        // Back substitute
        for (int i=2;i>=0;i--) {
            for (int j=i+1;j<3;j++) aug[i][3] -= aug[i][j]*aug[j][3];
            aug[i][3] /= aug[i][i];
        }
        ox=(float)aug[0][3]; oy=(float)aug[1][3]; oz=(float)aug[2][3];
        return true;
    }
};

struct QEMEdge {
    int v0, v1;
    float cost;
    Vec3 optPos;
    int timestamp; // For lazy deletion
};

void filterSimplify(int handle, int targetFaces) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Ensure triangulated
    {
        std::vector<Face> tris;
        for (auto& f : mesh.faces) {
            for (size_t i = 1; i + 1 < f.indices.size(); i++) {
                Face t; t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i + 1)]};
                tris.push_back(t);
            }
        }
        mesh.faces = tris;
    }

    if (targetFaces <= 0) targetFaces = static_cast<int>(mesh.faces.size()) / 2;
    if (targetFaces >= static_cast<int>(mesh.faces.size())) { mesh.triangulate(); return; }

    int nv = static_cast<int>(mesh.vertices.size());

    // Step 1: Compute per-vertex quadrics from incident face planes
    std::vector<Quadric> vertQ(nv);
    for (const auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        auto& v0 = mesh.vertices[f.indices[0]];
        auto& v1 = mesh.vertices[f.indices[1]];
        auto& v2 = mesh.vertices[f.indices[2]];
        float e1x=v1.x-v0.x, e1y=v1.y-v0.y, e1z=v1.z-v0.z;
        float e2x=v2.x-v0.x, e2y=v2.y-v0.y, e2z=v2.z-v0.z;
        float nx = e1y*e2z - e1z*e2y;
        float ny = e1z*e2x - e1x*e2z;
        float nz = e1x*e2y - e1y*e2x;
        float len = std::sqrt(nx*nx + ny*ny + nz*nz);
        if (len < 1e-12f) continue;
        nx/=len; ny/=len; nz/=len;
        float d = -(nx*v0.x + ny*v0.y + nz*v0.z);
        // Weight by face area (len/2)
        float area = len * 0.5f;
        Quadric q; q.addPlane(nx*area, ny*area, nz*area, d*area);
        for (int idx : f.indices) vertQ[idx] = vertQ[idx] + q;
    }

    // Step 2: Build edge set with costs
    // Track per-vertex timestamps for lazy heap deletion
    std::vector<int> vertTimestamp(nv, 0);
    std::vector<bool> vertDeleted(nv, false);

    // Collect unique edges
    std::unordered_map<long long, bool> edgeSeen;
    auto edgeKey = [](int a, int b) -> long long {
        if (a > b) std::swap(a, b);
        return (long long)a * 1000000LL + b;
    };

    auto computeEdgeCost = [&](int v0, int v1) -> QEMEdge {
        QEMEdge e;
        e.v0 = v0; e.v1 = v1;
        Quadric combined = vertQ[v0] + vertQ[v1];
        float ox, oy, oz;
        if (combined.optimalPosition(ox, oy, oz)) {
            e.optPos = {ox, oy, oz};
            e.cost = (float)combined.evaluate(ox, oy, oz);
        } else {
            // Fallback: choose best of v0, v1, midpoint
            float c0 = (float)combined.evaluate(mesh.vertices[v0].x, mesh.vertices[v0].y, mesh.vertices[v0].z);
            float c1 = (float)combined.evaluate(mesh.vertices[v1].x, mesh.vertices[v1].y, mesh.vertices[v1].z);
            float mx = (mesh.vertices[v0].x+mesh.vertices[v1].x)*0.5f;
            float my = (mesh.vertices[v0].y+mesh.vertices[v1].y)*0.5f;
            float mz = (mesh.vertices[v0].z+mesh.vertices[v1].z)*0.5f;
            float cm = (float)combined.evaluate(mx, my, mz);
            if (c0 <= c1 && c0 <= cm) { e.optPos = mesh.vertices[v0]; e.cost = c0; }
            else if (c1 <= cm) { e.optPos = mesh.vertices[v1]; e.cost = c1; }
            else { e.optPos = {mx, my, mz}; e.cost = cm; }
        }
        if (e.cost < 0) e.cost = 0; // Numerical safety
        e.timestamp = vertTimestamp[v0] + vertTimestamp[v1];
        return e;
    };

    // Min-heap (priority queue)
    auto cmp = [](const QEMEdge& a, const QEMEdge& b) { return a.cost > b.cost; };
    std::priority_queue<QEMEdge, std::vector<QEMEdge>, decltype(cmp)> heap(cmp);

    for (const auto& f : mesh.faces) {
        for (int e = 0; e < 3; e++) {
            int a = f.indices[e], b = f.indices[(e+1)%3];
            long long key = edgeKey(a, b);
            if (!edgeSeen[key]) {
                edgeSeen[key] = true;
                heap.push(computeEdgeCost(a, b));
            }
        }
    }

    // Step 3: Collapse loop
    int currentFaces = static_cast<int>(mesh.faces.size());
    while (currentFaces > targetFaces && !heap.empty()) {
        QEMEdge top = heap.top(); heap.pop();

        // Lazy deletion: check if this edge is still valid
        if (vertDeleted[top.v0] || vertDeleted[top.v1]) continue;
        if (top.timestamp != vertTimestamp[top.v0] + vertTimestamp[top.v1]) continue;

        int keep = top.v0, remove = top.v1;

        // Move keep vertex to optimal position
        mesh.vertices[keep] = top.optPos;
        vertQ[keep] = vertQ[keep] + vertQ[remove];
        vertDeleted[remove] = true;
        vertTimestamp[keep]++;

        // Update faces: replace remove with keep, remove degenerate
        int removed = 0;
        for (auto& f : mesh.faces) {
            for (auto& idx : f.indices) { if (idx == remove) idx = keep; }
        }
        auto newEnd = std::remove_if(mesh.faces.begin(), mesh.faces.end(), [](const Face& f) {
            return f.indices[0]==f.indices[1] || f.indices[1]==f.indices[2] || f.indices[0]==f.indices[2];
        });
        removed = static_cast<int>(mesh.faces.end() - newEnd);
        mesh.faces.erase(newEnd, mesh.faces.end());
        currentFaces -= removed;

        // Re-insert affected edges
        for (const auto& f : mesh.faces) {
            for (int e = 0; e < 3; e++) {
                int a = f.indices[e], b = f.indices[(e+1)%3];
                if (a == keep || b == keep) {
                    if (!vertDeleted[a] && !vertDeleted[b]) {
                        heap.push(computeEdgeCost(a, b));
                    }
                }
            }
        }
    }

    // Clean up: remove unreferenced vertices and reindex
    std::vector<bool> used(mesh.vertices.size(), false);
    for (const auto& f : mesh.faces) for (int idx : f.indices) used[idx] = true;
    std::vector<int> remap(mesh.vertices.size(), -1);
    std::vector<Vec3> newVerts;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (used[i]) { remap[i] = static_cast<int>(newVerts.size()); newVerts.push_back(mesh.vertices[i]); }
    }
    mesh.vertices = newVerts;
    for (auto& f : mesh.faces) for (auto& idx : f.indices) idx = remap[idx];

    mesh.triangulate();
}

// ---- Midpoint Subdivision ----
void filterSubdivide(int handle, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Ensure triangulated
    std::vector<Face> tris;
    for (auto& f : mesh.faces) {
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            Face t;
            t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i + 1)]};
            tris.push_back(t);
        }
    }
    mesh.faces = tris;

    for (int iter = 0; iter < iterations; iter++) {
        struct EdgeKey {
            int a, b;
            bool operator==(const EdgeKey& o) const { return a == o.a && b == o.b; }
        };
        struct EdgeHash {
            size_t operator()(const EdgeKey& k) const {
                return std::hash<long long>()(static_cast<long long>(k.a) * 1000003LL + k.b);
            }
        };

        std::unordered_map<EdgeKey, int, EdgeHash> edgeMidpoints;

        auto getMidpoint = [&](int a, int b) -> int {
            if (a > b) std::swap(a, b);
            EdgeKey key{a, b};
            auto it2 = edgeMidpoints.find(key);
            if (it2 != edgeMidpoints.end()) return it2->second;

            Vec3 mid;
            mid.x = (mesh.vertices[a].x + mesh.vertices[b].x) * 0.5f;
            mid.y = (mesh.vertices[a].y + mesh.vertices[b].y) * 0.5f;
            mid.z = (mesh.vertices[a].z + mesh.vertices[b].z) * 0.5f;
            int idx = static_cast<int>(mesh.vertices.size());
            mesh.vertices.push_back(mid);
            edgeMidpoints[key] = idx;
            return idx;
        };

        std::vector<Face> newFaces;
        newFaces.reserve(mesh.faces.size() * 4);

        for (const auto& f : mesh.faces) {
            int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
            int m01 = getMidpoint(v0, v1);
            int m12 = getMidpoint(v1, v2);
            int m20 = getMidpoint(v2, v0);

            newFaces.push_back({{v0, m01, m20}});
            newFaces.push_back({{v1, m12, m01}});
            newFaces.push_back({{v2, m20, m12}});
            newFaces.push_back({{m01, m12, m20}});
        }

        mesh.faces = newFaces;
    }

    mesh.triangulate();
}

// ---- Remove Duplicate Vertices ----
void filterRemoveDuplicateVertices(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Hash-based dedup
    struct VKey {
        float x, y, z;
        bool operator==(const VKey& o) const {
            return x == o.x && y == o.y && z == o.z;
        }
    };
    struct VHash {
        size_t operator()(const VKey& k) const {
            size_t h = 0;
            auto hf = std::hash<float>{};
            h ^= hf(k.x) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.y) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= hf(k.z) + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };

    std::unordered_map<VKey, int, VHash> vertMap;
    std::vector<int> remap(mesh.vertices.size());
    std::vector<Vec3> newVerts;

    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        VKey key{mesh.vertices[i].x, mesh.vertices[i].y, mesh.vertices[i].z};
        auto jt = vertMap.find(key);
        if (jt != vertMap.end()) {
            remap[i] = jt->second;
        } else {
            int idx = static_cast<int>(newVerts.size());
            vertMap[key] = idx;
            remap[i] = idx;
            newVerts.push_back(mesh.vertices[i]);
        }
    }

    mesh.vertices = newVerts;
    for (auto& f : mesh.faces) {
        for (auto& idx : f.indices) {
            idx = remap[idx];
        }
    }

    mesh.triangulate();
}

// ---- Remove Unreferenced Vertices ----
void filterRemoveUnreferencedVertices(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    std::vector<bool> used(mesh.vertices.size(), false);
    for (const auto& f : mesh.faces) {
        for (int idx : f.indices) {
            if (idx >= 0 && idx < static_cast<int>(used.size())) used[idx] = true;
        }
    }

    std::vector<int> remap(mesh.vertices.size(), -1);
    std::vector<Vec3> newVerts;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (used[i]) {
            remap[i] = static_cast<int>(newVerts.size());
            newVerts.push_back(mesh.vertices[i]);
        }
    }

    mesh.vertices = newVerts;
    for (auto& f : mesh.faces) {
        for (auto& idx : f.indices) {
            idx = remap[idx];
        }
    }

    mesh.triangulate();
}

// ============ Test Mesh Generators ============

int generateTestSphere(int subdivisions) {
    // Icosphere: start with icosahedron, subdivide each face
    Mesh mesh;
    const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
    // 12 vertices of icosahedron
    auto addV = [&](float x, float y, float z) {
        float len = std::sqrt(x*x + y*y + z*z);
        mesh.vertices.push_back({x/len, y/len, z/len});
    };
    addV(-1, t, 0); addV(1, t, 0); addV(-1,-t, 0); addV(1,-t, 0);
    addV(0,-1, t); addV(0, 1, t); addV(0,-1,-t); addV(0, 1,-t);
    addV(t, 0,-1); addV(t, 0, 1); addV(-t, 0,-1); addV(-t, 0, 1);

    // 20 faces
    int icoFaces[][3] = {
        {0,11,5},{0,5,1},{0,1,7},{0,7,10},{0,10,11},
        {1,5,9},{5,11,4},{11,10,2},{10,7,6},{7,1,8},
        {3,9,4},{3,4,2},{3,2,6},{3,6,8},{3,8,9},
        {4,9,5},{2,4,11},{6,2,10},{8,6,7},{9,8,1}
    };
    for (auto& f : icoFaces) {
        mesh.faces.push_back({{f[0], f[1], f[2]}});
    }

    // Subdivide
    for (int s = 0; s < subdivisions; s++) {
        struct EKey { int a,b; bool operator==(const EKey& o) const { return a==o.a && b==o.b; }};
        struct EHash { size_t operator()(const EKey& k) const { return std::hash<long long>()(static_cast<long long>(k.a)*1000003LL+k.b); }};
        std::unordered_map<EKey,int,EHash> midCache;

        auto getMid = [&](int a, int b) -> int {
            if (a > b) std::swap(a,b);
            EKey key{a,b};
            auto it = midCache.find(key);
            if (it != midCache.end()) return it->second;
            Vec3 mid;
            mid.x = (mesh.vertices[a].x + mesh.vertices[b].x) * 0.5f;
            mid.y = (mesh.vertices[a].y + mesh.vertices[b].y) * 0.5f;
            mid.z = (mesh.vertices[a].z + mesh.vertices[b].z) * 0.5f;
            float len = std::sqrt(mid.x*mid.x + mid.y*mid.y + mid.z*mid.z);
            mid.x /= len; mid.y /= len; mid.z /= len;
            int idx = static_cast<int>(mesh.vertices.size());
            mesh.vertices.push_back(mid);
            midCache[key] = idx;
            return idx;
        };

        std::vector<Face> newFaces;
        newFaces.reserve(mesh.faces.size() * 4);
        for (auto& f : mesh.faces) {
            int v0=f.indices[0], v1=f.indices[1], v2=f.indices[2];
            int m01=getMid(v0,v1), m12=getMid(v1,v2), m20=getMid(v2,v0);
            newFaces.push_back({{v0,m01,m20}});
            newFaces.push_back({{v1,m12,m01}});
            newFaces.push_back({{v2,m20,m12}});
            newFaces.push_back({{m01,m12,m20}});
        }
        mesh.faces = newFaces;
    }

    return storeMesh(std::move(mesh));
}

int generateTestGrid(int rows, int cols) {
    Mesh mesh;
    mesh.vertices.reserve((rows+1)*(cols+1));
    for (int r = 0; r <= rows; r++) {
        for (int c = 0; c <= cols; c++) {
            mesh.vertices.push_back({
                static_cast<float>(c) / static_cast<float>(cols),
                0.0f,
                static_cast<float>(r) / static_cast<float>(rows)
            });
        }
    }
    mesh.faces.reserve(rows * cols * 2);
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            int i = r * (cols+1) + c;
            mesh.faces.push_back({{i, i+cols+1, i+cols+2}});
            mesh.faces.push_back({{i, i+cols+2, i+1}});
        }
    }
    return storeMesh(std::move(mesh));
}

int getWasmMemoryBytes() {
    return static_cast<int>(EM_ASM_INT({ return wasmMemory.buffer.byteLength; }));
}

// ============ Selection ============

void selectFaces(int handle, uintptr_t dataPtr, int count) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    const uint32_t* indices = reinterpret_cast<const uint32_t*>(dataPtr);
    for (int i = 0; i < count; i++) {
        uint32_t idx = indices[i];
        if (idx < mesh.faceSelected.size()) mesh.faceSelected[idx] = 1;
    }
}

void deselectFaces(int handle, uintptr_t dataPtr, int count) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    const uint32_t* indices = reinterpret_cast<const uint32_t*>(dataPtr);
    for (int i = 0; i < count; i++) {
        uint32_t idx = indices[i];
        if (idx < mesh.faceSelected.size()) mesh.faceSelected[idx] = 0;
    }
}

void selectAllFaces(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    std::fill(mesh.faceSelected.begin(), mesh.faceSelected.end(), 1);
}

void deselectAllFaces(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    std::fill(mesh.faceSelected.begin(), mesh.faceSelected.end(), 0);
}

void invertFaceSelection(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    for (auto& s : mesh.faceSelected) s = s ? 0 : 1;
}

int getSelectedFaceCount(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    int count = 0;
    for (auto s : mesh.faceSelected) if (s) count++;
    return count;
}

val getSelectedFaceIndices(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    std::vector<uint32_t> indices;
    for (size_t i = 0; i < mesh.faceSelected.size(); i++) {
        if (mesh.faceSelected[i]) indices.push_back(static_cast<uint32_t>(i));
    }
    return val(typed_memory_view(indices.size(), indices.data()));
}

val getSelectionMask(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    return val(typed_memory_view(mesh.faceSelected.size(), mesh.faceSelected.data()));
}

void selectVertices(int handle, uintptr_t dataPtr, int count) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    const uint32_t* indices = reinterpret_cast<const uint32_t*>(dataPtr);
    for (int i = 0; i < count; i++) {
        uint32_t idx = indices[i];
        if (idx < mesh.vertexSelected.size()) mesh.vertexSelected[idx] = 1;
    }
}

void deselectAllVertices(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    std::fill(mesh.vertexSelected.begin(), mesh.vertexSelected.end(), 0);
}

int getSelectedVertexCount(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    int count = 0;
    for (auto s : mesh.vertexSelected) if (s) count++;
    return count;
}

void selectConnectedComponent(int handle, int faceIndex) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    int nf = static_cast<int>(mesh.faces.size());
    if (faceIndex < 0 || faceIndex >= nf) return;

    // Build face adjacency via shared vertices
    std::unordered_map<int, std::vector<int>> vertToFace;
    for (int fi = 0; fi < nf; fi++) {
        for (int vi : mesh.faces[fi].indices) {
            vertToFace[vi].push_back(fi);
        }
    }

    // BFS from faceIndex
    std::vector<bool> visited(nf, false);
    std::vector<int> queue;
    queue.push_back(faceIndex);
    visited[faceIndex] = true;

    size_t head = 0;
    while (head < queue.size()) {
        int fi = queue[head++];
        mesh.faceSelected[fi] = 1;
        for (int vi : mesh.faces[fi].indices) {
            for (int neighbor : vertToFace[vi]) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.push_back(neighbor);
                }
            }
        }
    }
}

void selectFacesByNormalAngle(int handle, float refNx, float refNy, float refNz, float angleDeg) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    float cosThresh = std::cos(angleDeg * 3.14159265358979f / 180.0f);
    float refLen = std::sqrt(refNx*refNx + refNy*refNy + refNz*refNz);
    if (refLen < 1e-9f) return;
    refNx /= refLen; refNy /= refLen; refNz /= refLen;

    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        auto& f = mesh.faces[fi];
        if (f.indices.size() < 3) continue;
        auto& v0 = mesh.vertices[f.indices[0]];
        auto& v1 = mesh.vertices[f.indices[1]];
        auto& v2 = mesh.vertices[f.indices[2]];
        float e1x = v1.x-v0.x, e1y = v1.y-v0.y, e1z = v1.z-v0.z;
        float e2x = v2.x-v0.x, e2y = v2.y-v0.y, e2z = v2.z-v0.z;
        float nx = e1y*e2z-e1z*e2y, ny = e1z*e2x-e1x*e2z, nz = e1x*e2y-e1y*e2x;
        float len = std::sqrt(nx*nx+ny*ny+nz*nz);
        if (len < 1e-9f) continue;
        float dot = (nx*refNx + ny*refNy + nz*refNz) / len;
        if (dot >= cosThresh) mesh.faceSelected[fi] = 1;
    }
}

void selectSmallComponents(int handle, int minFaces) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    int nf = static_cast<int>(mesh.faces.size());

    std::unordered_map<int, std::vector<int>> vertToFace;
    for (int fi = 0; fi < nf; fi++) {
        for (int vi : mesh.faces[fi].indices) vertToFace[vi].push_back(fi);
    }

    std::vector<int> componentId(nf, -1);
    std::vector<int> componentSize;
    int compIdx = 0;

    for (int fi = 0; fi < nf; fi++) {
        if (componentId[fi] >= 0) continue;
        std::vector<int> queue;
        queue.push_back(fi);
        componentId[fi] = compIdx;
        int size = 0;
        size_t head = 0;
        while (head < queue.size()) {
            int cur = queue[head++];
            size++;
            for (int vi : mesh.faces[cur].indices) {
                for (int nb : vertToFace[vi]) {
                    if (componentId[nb] < 0) {
                        componentId[nb] = compIdx;
                        queue.push_back(nb);
                    }
                }
            }
        }
        componentSize.push_back(size);
        compIdx++;
    }

    for (int fi = 0; fi < nf; fi++) {
        if (componentSize[componentId[fi]] <= minFaces) {
            mesh.faceSelected[fi] = 1;
        }
    }
}

void deleteSelectedFaces(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();

    std::vector<Face> newFaces;
    for (size_t i = 0; i < mesh.faces.size(); i++) {
        if (!mesh.faceSelected[i]) newFaces.push_back(mesh.faces[i]);
    }
    mesh.faces = newFaces;
    mesh.faceSelected.clear();

    // Remove unreferenced vertices
    std::vector<bool> used(mesh.vertices.size(), false);
    for (auto& f : mesh.faces) for (int idx : f.indices) {
        if (idx >= 0 && idx < static_cast<int>(used.size())) used[idx] = true;
    }
    std::vector<int> remap(mesh.vertices.size(), -1);
    std::vector<Vec3> newVerts;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (used[i]) { remap[i] = static_cast<int>(newVerts.size()); newVerts.push_back(mesh.vertices[i]); }
    }
    mesh.vertices = newVerts;
    for (auto& f : mesh.faces) for (auto& idx : f.indices) idx = remap[idx];
    mesh.vertexSelected.clear();

    mesh.triangulate();
}

// ============ Quality & Color ============

bool hasVertexQuality(int handle) {
    auto it = meshStore.find(handle);
    return it != meshStore.end() && it->second.hasQuality;
}

val getQualityBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasQuality) return val::null();
    auto& q = it->second.vertexQuality;
    return val(typed_memory_view(q.size(), q.data()));
}

val getQualityRange(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasQuality) return val::null();
    auto& q = it->second.vertexQuality;
    float mn = q[0], mx = q[0];
    for (float v : q) { if (v < mn) mn = v; if (v > mx) mx = v; }
    val r = val::array();
    r.call<void>("push", mn);
    r.call<void>("push", mx);
    return r;
}

bool hasVertexColors(int handle) {
    auto it = meshStore.find(handle);
    return it != meshStore.end() && it->second.hasColors;
}

val getColorBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasColors) return val::null();
    auto& c = it->second.vertexColors;
    return val(typed_memory_view(c.size(), c.data()));
}

// --- Curvature ---

// Angle at vertex v0 in triangle (v0, v1, v2)
static float triangleAngle(const Vec3& v0, const Vec3& v1, const Vec3& v2) {
    float ax = v1.x-v0.x, ay = v1.y-v0.y, az = v1.z-v0.z;
    float bx = v2.x-v0.x, by = v2.y-v0.y, bz = v2.z-v0.z;
    float dot = ax*bx + ay*by + az*bz;
    float la = std::sqrt(ax*ax+ay*ay+az*az);
    float lb = std::sqrt(bx*bx+by*by+bz*bz);
    if (la < 1e-12f || lb < 1e-12f) return 0;
    float cosA = dot / (la * lb);
    cosA = std::max(-1.0f, std::min(1.0f, cosA));
    return std::acos(cosA);
}

static float triangleArea(const Vec3& v0, const Vec3& v1, const Vec3& v2) {
    float ax = v1.x-v0.x, ay = v1.y-v0.y, az = v1.z-v0.z;
    float bx = v2.x-v0.x, by = v2.y-v0.y, bz = v2.z-v0.z;
    float cx = ay*bz-az*by, cy = az*bx-ax*bz, cz = ax*by-ay*bx;
    return 0.5f * std::sqrt(cx*cx+cy*cy+cz*cz);
}

void computeGaussianCurvature(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    size_t nv = mesh.vertices.size();
    mesh.vertexQuality.assign(nv, 2.0f * 3.14159265358979f); // start with 2π
    std::vector<float> area(nv, 0);

    for (auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        // Triangulate face
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            int i0 = f.indices[0], i1 = f.indices[i], i2 = f.indices[i+1];
            auto& v0 = mesh.vertices[i0];
            auto& v1 = mesh.vertices[i1];
            auto& v2 = mesh.vertices[i2];
            float a = triangleAngle(v0, v1, v2);
            float b = triangleAngle(v1, v2, v0);
            float c = triangleAngle(v2, v0, v1);
            mesh.vertexQuality[i0] -= a;
            mesh.vertexQuality[i1] -= b;
            mesh.vertexQuality[i2] -= c;
            float ta = triangleArea(v0, v1, v2) / 3.0f;
            area[i0] += ta; area[i1] += ta; area[i2] += ta;
        }
    }
    for (size_t i = 0; i < nv; i++) {
        if (area[i] > 1e-12f) mesh.vertexQuality[i] /= area[i];
        else mesh.vertexQuality[i] = 0;
    }
    mesh.hasQuality = true;
}

void computeMeanCurvature(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    size_t nv = mesh.vertices.size();
    std::vector<Vec3> laplacian(nv, {0,0,0});
    std::vector<float> area(nv, 0);

    for (auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            int idx[3] = { f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i+1)] };
            Vec3 v[3] = { mesh.vertices[idx[0]], mesh.vertices[idx[1]], mesh.vertices[idx[2]] };
            for (int e = 0; e < 3; e++) {
                int a = idx[e], b = idx[(e+1)%3], c = idx[(e+2)%3];
                // Cotangent weight at vertex c for edge (a,b)
                float ang = triangleAngle(v[(e+2)%3], v[e], v[(e+1)%3]);
                float cotW = (std::abs(std::sin(ang)) > 1e-12f) ? std::cos(ang) / std::sin(ang) : 0;
                float dx = mesh.vertices[a].x - mesh.vertices[b].x;
                float dy = mesh.vertices[a].y - mesh.vertices[b].y;
                float dz = mesh.vertices[a].z - mesh.vertices[b].z;
                laplacian[a].x += cotW * dx; laplacian[a].y += cotW * dy; laplacian[a].z += cotW * dz;
                laplacian[b].x -= cotW * dx; laplacian[b].y -= cotW * dy; laplacian[b].z -= cotW * dz;
            }
            float ta = triangleArea(v[0], v[1], v[2]) / 3.0f;
            area[idx[0]] += ta; area[idx[1]] += ta; area[idx[2]] += ta;
        }
    }

    mesh.vertexQuality.resize(nv);
    for (size_t i = 0; i < nv; i++) {
        float a = area[i] > 1e-12f ? area[i] : 1e-12f;
        auto& L = laplacian[i];
        mesh.vertexQuality[i] = 0.5f * std::sqrt(L.x*L.x + L.y*L.y + L.z*L.z) / a;
    }
    mesh.hasQuality = true;
}

// Colormap LUT helpers
static void jet(float t, uint8_t& r, uint8_t& g, uint8_t& b) {
    float R = std::min(1.0f, std::max(0.0f, 1.5f - std::abs(4*t - 3)));
    float G = std::min(1.0f, std::max(0.0f, 1.5f - std::abs(4*t - 2)));
    float B = std::min(1.0f, std::max(0.0f, 1.5f - std::abs(4*t - 1)));
    r = static_cast<uint8_t>(R*255); g = static_cast<uint8_t>(G*255); b = static_cast<uint8_t>(B*255);
}
static void coolwarm(float t, uint8_t& r, uint8_t& g, uint8_t& b) {
    // Blue at 0, white at 0.5, red at 1
    if (t < 0.5f) {
        float s = t * 2;
        r = static_cast<uint8_t>(s * 230); g = static_cast<uint8_t>(s * 230); b = static_cast<uint8_t>(128 + s * 127);
    } else {
        float s = (t - 0.5f) * 2;
        r = static_cast<uint8_t>(230 + s * 25); g = static_cast<uint8_t>(230 * (1-s)); b = static_cast<uint8_t>(255 * (1-s));
    }
}
static void viridis(float t, uint8_t& r, uint8_t& g, uint8_t& b) {
    r = static_cast<uint8_t>(std::min(255.0f, (68 + t * 187)));
    g = static_cast<uint8_t>(std::min(255.0f, (1 + t * 230)));
    b = static_cast<uint8_t>(std::min(255.0f, (84 + (1-std::abs(2*t-1)) * 171)));
}
static void grayscale(float t, uint8_t& r, uint8_t& g, uint8_t& b) {
    uint8_t v = static_cast<uint8_t>(t * 255);
    r = g = b = v;
}

static void mapColor(float t, const std::string& cmap, uint8_t& r, uint8_t& g, uint8_t& b) {
    t = std::max(0.0f, std::min(1.0f, t));
    if (cmap == "jet") jet(t, r, g, b);
    else if (cmap == "coolwarm") coolwarm(t, r, g, b);
    else if (cmap == "viridis") viridis(t, r, g, b);
    else if (cmap == "grayscale") grayscale(t, r, g, b);
    else if (cmap == "rainbow") {
        // HSV rainbow
        float h = (1.0f - t) * 240.0f / 360.0f;
        float s = 1.0f, v = 1.0f;
        int hi = static_cast<int>(h * 6) % 6;
        float f = h * 6 - hi;
        uint8_t V = static_cast<uint8_t>(v*255);
        uint8_t p = static_cast<uint8_t>(v*(1-s)*255);
        uint8_t q = static_cast<uint8_t>(v*(1-f*s)*255);
        uint8_t tt = static_cast<uint8_t>(v*(1-(1-f)*s)*255);
        switch(hi) {
            case 0: r=V; g=tt; b=p; break;
            case 1: r=q; g=V; b=p; break;
            case 2: r=p; g=V; b=tt; break;
            case 3: r=p; g=q; b=V; break;
            case 4: r=tt; g=p; b=V; break;
            default: r=V; g=p; b=q; break;
        }
    }
    else if (cmap == "inferno") {
        r = static_cast<uint8_t>(std::min(255.0f, t*t*255));
        g = static_cast<uint8_t>(std::min(255.0f, t*t*t*200));
        b = static_cast<uint8_t>(std::min(255.0f, (std::sin(t*3.14159f)*200)));
    }
    else if (cmap == "redblue") {
        r = static_cast<uint8_t>(t * 255);
        g = 0;
        b = static_cast<uint8_t>((1-t) * 255);
    }
    else grayscale(t, r, g, b);
}

void qualityToColor(int handle, const std::string& colormap, float clampMin, float clampMax) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasQuality) return;
    auto& mesh = it->second;
    auto& q = mesh.vertexQuality;

    float mn = clampMin, mx = clampMax;
    if (mn >= mx) {
        mn = q[0]; mx = q[0];
        for (float v : q) { if (v < mn) mn = v; if (v > mx) mx = v; }
    }
    float range = mx - mn;
    if (range < 1e-12f) range = 1.0f;

    mesh.vertexColors.resize(mesh.vertices.size() * 3);
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        float t = (q[i] - mn) / range;
        uint8_t r, g, b;
        mapColor(t, colormap, r, g, b);
        mesh.vertexColors[i*3] = r;
        mesh.vertexColors[i*3+1] = g;
        mesh.vertexColors[i*3+2] = b;
    }
    mesh.hasColors = true;
}

void selectFacesByQuality(int handle, float minQ, float maxQ) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasQuality) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    auto& q = mesh.vertexQuality;

    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        bool allIn = true;
        for (int vi : mesh.faces[fi].indices) {
            if (vi < 0 || vi >= static_cast<int>(q.size()) || q[vi] < minQ || q[vi] > maxQ) {
                allIn = false; break;
            }
        }
        if (allIn) mesh.faceSelected[fi] = 1;
    }
}

// ============ Point Cloud & Surface Reconstruction ============

bool isPointCloud(int handle) {
    auto it = meshStore.find(handle);
    return it != meshStore.end() && it->second.faces.empty();
}

int getPointCount(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    return static_cast<int>(it->second.vertices.size());
}

int loadPointCloudFromXYZ(const std::string& text) {
    Mesh mesh;
    std::istringstream stream(text);
    std::string line;
    bool hasNormals = false;
    bool firstLine = true;

    while (std::getline(stream, line)) {
        if (line.empty() || line[0] == '#') continue;
        std::istringstream ls(line);
        float x, y, z;
        ls >> x >> y >> z;
        mesh.vertices.push_back({x, y, z});

        float nx, ny, nz;
        if (ls >> nx >> ny >> nz) {
            if (firstLine) hasNormals = true;
            if (hasNormals) {
                mesh.normalBuffer.push_back(nx);
                mesh.normalBuffer.push_back(ny);
                mesh.normalBuffer.push_back(nz);
            }
        }
        firstLine = false;
    }

    // Build vertex buffer (no faces, no triangulate)
    mesh.vertexBuffer.clear();
    for (auto& v : mesh.vertices) {
        mesh.vertexBuffer.push_back(v.x);
        mesh.vertexBuffer.push_back(v.y);
        mesh.vertexBuffer.push_back(v.z);
    }
    if (!hasNormals) {
        mesh.normalBuffer.clear();
    }

    int handle = nextHandle++;
    meshStore[handle] = std::move(mesh);
    return handle;
}

// --- Normal Estimation (PCA-based) ---

void estimateNormals(int handle, int kNeighbors) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    int n = static_cast<int>(mesh.vertices.size());
    if (n < 3) return;

    // Build uniform grid for nearest neighbor
    float gMin[3] = {1e30f,1e30f,1e30f}, gMax[3] = {-1e30f,-1e30f,-1e30f};
    for (auto& v : mesh.vertices) {
        if(v.x<gMin[0]) gMin[0]=v.x; if(v.x>gMax[0]) gMax[0]=v.x;
        if(v.y<gMin[1]) gMin[1]=v.y; if(v.y>gMax[1]) gMax[1]=v.y;
        if(v.z<gMin[2]) gMin[2]=v.z; if(v.z>gMax[2]) gMax[2]=v.z;
    }
    float diag = std::sqrt((gMax[0]-gMin[0])*(gMax[0]-gMin[0])+(gMax[1]-gMin[1])*(gMax[1]-gMin[1])+(gMax[2]-gMin[2])*(gMax[2]-gMin[2]));
    float cellSize = diag / 30.0f;
    if (cellSize < 1e-8f) cellSize = 1.0f;
    int gx = static_cast<int>((gMax[0]-gMin[0])/cellSize) + 1;
    int gy = static_cast<int>((gMax[1]-gMin[1])/cellSize) + 1;
    int gz = static_cast<int>((gMax[2]-gMin[2])/cellSize) + 1;
    gx = std::max(1, std::min(gx, 200));
    gy = std::max(1, std::min(gy, 200));
    gz = std::max(1, std::min(gz, 200));

    std::unordered_map<int, std::vector<int>> grid;
    auto cellId = [&](float x, float y, float z) -> int {
        int cx = static_cast<int>((x-gMin[0])/cellSize);
        int cy = static_cast<int>((y-gMin[1])/cellSize);
        int cz = static_cast<int>((z-gMin[2])/cellSize);
        cx = std::max(0, std::min(cx, gx-1));
        cy = std::max(0, std::min(cy, gy-1));
        cz = std::max(0, std::min(cz, gz-1));
        return cx + cy * gx + cz * gx * gy;
    };
    for (int i = 0; i < n; i++) {
        grid[cellId(mesh.vertices[i].x, mesh.vertices[i].y, mesh.vertices[i].z)].push_back(i);
    }

    mesh.normalBuffer.resize(n * 3);

    for (int pi = 0; pi < n; pi++) {
        auto& p = mesh.vertices[pi];
        int cx = static_cast<int>((p.x-gMin[0])/cellSize);
        int cy = static_cast<int>((p.y-gMin[1])/cellSize);
        int cz = static_cast<int>((p.z-gMin[2])/cellSize);

        // Gather k nearest neighbors from surrounding cells
        std::vector<std::pair<float, int>> dists;
        for (int dx = -2; dx <= 2; dx++) for (int dy = -2; dy <= 2; dy++) for (int dz = -2; dz <= 2; dz++) {
            int nx2 = cx+dx, ny2 = cy+dy, nz2 = cz+dz;
            if (nx2<0||nx2>=gx||ny2<0||ny2>=gy||nz2<0||nz2>=gz) continue;
            int cid = nx2 + ny2*gx + nz2*gx*gy;
            auto jt = grid.find(cid);
            if (jt == grid.end()) continue;
            for (int qi : jt->second) {
                if (qi == pi) continue;
                float dd = (mesh.vertices[qi].x-p.x)*(mesh.vertices[qi].x-p.x)
                          +(mesh.vertices[qi].y-p.y)*(mesh.vertices[qi].y-p.y)
                          +(mesh.vertices[qi].z-p.z)*(mesh.vertices[qi].z-p.z);
                dists.push_back({dd, qi});
            }
        }
        std::sort(dists.begin(), dists.end());
        int k = std::min(kNeighbors, static_cast<int>(dists.size()));

        // PCA: compute covariance matrix
        float centroid[3] = {p.x, p.y, p.z};
        for (int i = 0; i < k; i++) {
            auto& q = mesh.vertices[dists[i].second];
            centroid[0] += q.x; centroid[1] += q.y; centroid[2] += q.z;
        }
        float invK = 1.0f / (k + 1);
        centroid[0] *= invK; centroid[1] *= invK; centroid[2] *= invK;

        float cov[9] = {0};
        auto addCov = [&](float x, float y, float z) {
            float d[3] = {x-centroid[0], y-centroid[1], z-centroid[2]};
            for (int r=0;r<3;r++) for (int c=0;c<3;c++) cov[r*3+c] += d[r]*d[c];
        };
        addCov(p.x, p.y, p.z);
        for (int i = 0; i < k; i++) {
            auto& q = mesh.vertices[dists[i].second];
            addCov(q.x, q.y, q.z);
        }

        // Smallest eigenvector of 3x3 symmetric matrix (power iteration for largest, then deflate)
        // Simple approach: compute eigenvalues via characteristic polynomial
        // For robustness, use iterative approach
        // Simplified: cross product of two eigenvectors gives the third
        // Even simpler: the normal is approximately the cross product of two vectors in the neighborhood
        // Use proper approach: find eigenvector for smallest eigenvalue

        // Quick hack: pick two vectors in neighborhood, cross product
        if (k >= 2) {
            auto& q1 = mesh.vertices[dists[0].second];
            auto& q2 = mesh.vertices[dists[1].second];
            float v1[3] = {q1.x-p.x, q1.y-p.y, q1.z-p.z};
            float v2[3] = {q2.x-p.x, q2.y-p.y, q2.z-p.z};
            float nx2 = v1[1]*v2[2]-v1[2]*v2[1];
            float ny2 = v1[2]*v2[0]-v1[0]*v2[2];
            float nz2 = v1[0]*v2[1]-v1[1]*v2[0];
            float len = std::sqrt(nx2*nx2+ny2*ny2+nz2*nz2);
            if (len > 1e-12f) { nx2/=len; ny2/=len; nz2/=len; }
            else { nx2=0; ny2=0; nz2=1; }
            mesh.normalBuffer[pi*3] = nx2;
            mesh.normalBuffer[pi*3+1] = ny2;
            mesh.normalBuffer[pi*3+2] = nz2;
        } else {
            mesh.normalBuffer[pi*3] = 0;
            mesh.normalBuffer[pi*3+1] = 0;
            mesh.normalBuffer[pi*3+2] = 1;
        }
    }

    // Orient normals consistently (simple: flip to face outward from centroid)
    float cx2=0,cy2=0,cz2=0;
    for (auto& v : mesh.vertices) { cx2+=v.x; cy2+=v.y; cz2+=v.z; }
    cx2/=n; cy2/=n; cz2/=n;
    for (int i = 0; i < n; i++) {
        float dx = mesh.vertices[i].x - cx2;
        float dy = mesh.vertices[i].y - cy2;
        float dz = mesh.vertices[i].z - cz2;
        float dot = dx*mesh.normalBuffer[i*3] + dy*mesh.normalBuffer[i*3+1] + dz*mesh.normalBuffer[i*3+2];
        if (dot < 0) {
            mesh.normalBuffer[i*3] = -mesh.normalBuffer[i*3];
            mesh.normalBuffer[i*3+1] = -mesh.normalBuffer[i*3+1];
            mesh.normalBuffer[i*3+2] = -mesh.normalBuffer[i*3+2];
        }
    }
}

// --- Marching Cubes (simplified) ---

// Edge table and tri table (Paul Bourke, public domain)
// Abbreviated: only include the tables as static arrays
#include "mc_tables.h"

int marchingCubesReconstruction(int handle, int resolution) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return -1;
    auto& pcMesh = it->second;
    if (pcMesh.normalBuffer.size() != pcMesh.vertices.size() * 3) return -1; // Need normals

    int n = static_cast<int>(pcMesh.vertices.size());
    float gMin2[3]={1e30f,1e30f,1e30f}, gMax2[3]={-1e30f,-1e30f,-1e30f};
    for (auto& v : pcMesh.vertices) {
        if(v.x<gMin2[0]) gMin2[0]=v.x; if(v.x>gMax2[0]) gMax2[0]=v.x;
        if(v.y<gMin2[1]) gMin2[1]=v.y; if(v.y>gMax2[1]) gMax2[1]=v.y;
        if(v.z<gMin2[2]) gMin2[2]=v.z; if(v.z>gMax2[2]) gMax2[2]=v.z;
    }
    // Expand bbox slightly
    float margin = 0.1f;
    for (int i=0;i<3;i++) { float d=(gMax2[i]-gMin2[i])*margin; gMin2[i]-=d; gMax2[i]+=d; }

    float cellW[3] = {(gMax2[0]-gMin2[0])/resolution, (gMax2[1]-gMin2[1])/resolution, (gMax2[2]-gMin2[2])/resolution};

    // Compute SDF at grid vertices
    int gw = resolution+1;
    std::vector<float> sdf(gw*gw*gw, 1.0f);

    // For each grid vertex, find nearest point and compute signed distance
    // Build spatial hash for points
    float pcCellSize = std::max({cellW[0],cellW[1],cellW[2]}) * 3;
    if (pcCellSize < 1e-8f) pcCellSize = 1.0f;

    for (int zi = 0; zi < gw; zi++) {
        for (int yi = 0; yi < gw; yi++) {
            for (int xi = 0; xi < gw; xi++) {
                float gx2 = gMin2[0] + xi * cellW[0];
                float gy2 = gMin2[1] + yi * cellW[1];
                float gz2 = gMin2[2] + zi * cellW[2];

                // Find nearest point (brute force for small point clouds, limited for large)
                float bestD = 1e30f;
                int bestIdx = 0;
                int searchLimit = std::min(n, 5000); // Limit for performance
                for (int pi2 = 0; pi2 < searchLimit; pi2++) {
                    float dx = gx2-pcMesh.vertices[pi2].x;
                    float dy = gy2-pcMesh.vertices[pi2].y;
                    float dz = gz2-pcMesh.vertices[pi2].z;
                    float d = dx*dx+dy*dy+dz*dz;
                    if (d < bestD) { bestD = d; bestIdx = pi2; }
                }

                float dist = std::sqrt(bestD);
                // Sign from normal
                float dx2 = gx2-pcMesh.vertices[bestIdx].x;
                float dy2 = gy2-pcMesh.vertices[bestIdx].y;
                float dz2 = gz2-pcMesh.vertices[bestIdx].z;
                float dot = dx2*pcMesh.normalBuffer[bestIdx*3] + dy2*pcMesh.normalBuffer[bestIdx*3+1] + dz2*pcMesh.normalBuffer[bestIdx*3+2];
                sdf[xi + yi*gw + zi*gw*gw] = (dot >= 0) ? dist : -dist;
            }
        }
    }

    // Marching Cubes extraction
    Mesh outMesh;
    for (int zi = 0; zi < resolution; zi++) {
        for (int yi = 0; yi < resolution; yi++) {
            for (int xi = 0; xi < resolution; xi++) {
                float vals[8];
                int corners[8][3] = {{xi,yi,zi},{xi+1,yi,zi},{xi+1,yi+1,zi},{xi,yi+1,zi},
                                      {xi,yi,zi+1},{xi+1,yi,zi+1},{xi+1,yi+1,zi+1},{xi,yi+1,zi+1}};
                for (int c = 0; c < 8; c++) {
                    vals[c] = sdf[corners[c][0] + corners[c][1]*gw + corners[c][2]*gw*gw];
                }

                int cubeIndex = 0;
                for (int c = 0; c < 8; c++) {
                    if (vals[c] < 0) cubeIndex |= (1 << c);
                }
                if (MC_EDGE_TABLE[cubeIndex] == 0) continue;

                // Interpolate vertices on edges
                Vec3 edgeVerts[12];
                int edges = MC_EDGE_TABLE[cubeIndex];
                static const int edgePairs[12][2] = {{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}};
                for (int e = 0; e < 12; e++) {
                    if (!(edges & (1 << e))) continue;
                    int c1 = edgePairs[e][0], c2 = edgePairs[e][1];
                    float t = vals[c1] / (vals[c1] - vals[c2]);
                    edgeVerts[e].x = gMin2[0] + (corners[c1][0] + t*(corners[c2][0]-corners[c1][0])) * cellW[0];
                    edgeVerts[e].y = gMin2[1] + (corners[c1][1] + t*(corners[c2][1]-corners[c1][1])) * cellW[1];
                    edgeVerts[e].z = gMin2[2] + (corners[c1][2] + t*(corners[c2][2]-corners[c1][2])) * cellW[2];
                }

                // Generate triangles
                for (int i = 0; MC_TRI_TABLE[cubeIndex][i] != -1; i += 3) {
                    int vi0 = static_cast<int>(outMesh.vertices.size());
                    outMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIndex][i]]);
                    outMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIndex][i+1]]);
                    outMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIndex][i+2]]);
                    Face f; f.indices = {vi0, vi0+1, vi0+2};
                    outMesh.faces.push_back(f);
                }
            }
        }
    }

    if (outMesh.faces.empty()) return -1;
    return storeMesh(std::move(outMesh));
}

// ============ Alignment ============

val getTransformMatrix(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    return val(typed_memory_view(16, it->second.transformMatrix));
}

void setTransformMatrix(int handle, uintptr_t dataPtr) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    const float* data = reinterpret_cast<const float*>(dataPtr);
    std::memcpy(it->second.transformMatrix, data, 16 * sizeof(float));
}

void freezeTransform(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    float* m = mesh.transformMatrix;

    for (auto& v : mesh.vertices) {
        float x = v.x, y = v.y, z = v.z;
        v.x = m[0]*x + m[4]*y + m[8]*z  + m[12];
        v.y = m[1]*x + m[5]*y + m[9]*z  + m[13];
        v.z = m[2]*x + m[6]*y + m[10]*z + m[14];
    }

    // Reset to identity
    float identity[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
    std::memcpy(m, identity, 16 * sizeof(float));
    mesh.triangulate();
}

void resetTransform(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    float identity[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
    std::memcpy(it->second.transformMatrix, identity, 16 * sizeof(float));
}

// --- Kabsch Algorithm (SVD-free, quaternion-based for 3x3) ---
// Uses the method of computing optimal rotation via eigenvalue of N matrix
// Simplified: use iterative Jacobi-like approach for 3x3 SVD

static void matMul3(const float A[9], const float B[9], float C[9]) {
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++) {
            C[i*3+j] = 0;
            for (int k = 0; k < 3; k++) C[i*3+j] += A[i*3+k] * B[k*3+j];
        }
}

static float det3(const float m[9]) {
    return m[0]*(m[4]*m[8]-m[5]*m[7]) - m[1]*(m[3]*m[8]-m[5]*m[6]) + m[2]*(m[3]*m[7]-m[4]*m[6]);
}

// Simple 3x3 SVD via polar decomposition: R = closest rotation to M
// R = M * (M^T M)^{-1/2}  — approximated by iterative normalization
static void polarDecomp3(const float H[9], float R[9]) {
    // Start with R = H
    for (int i = 0; i < 9; i++) R[i] = H[i];

    // Iterate: R = 0.5 * (R + (R^{-T}))
    for (int iter = 0; iter < 20; iter++) {
        // Compute R^{-T} (transpose of inverse)
        float d = det3(R);
        if (std::abs(d) < 1e-12f) break;
        float invD = 1.0f / d;
        float Rit[9];
        // Cofactor matrix (already transposed = adjugate^T = inverse^T * det)
        Rit[0] =  (R[4]*R[8]-R[5]*R[7]) * invD;
        Rit[1] =  (R[5]*R[6]-R[3]*R[8]) * invD;
        Rit[2] =  (R[3]*R[7]-R[4]*R[6]) * invD;
        Rit[3] =  (R[2]*R[7]-R[1]*R[8]) * invD;
        Rit[4] =  (R[0]*R[8]-R[2]*R[6]) * invD;
        Rit[5] =  (R[1]*R[6]-R[0]*R[7]) * invD;
        Rit[6] =  (R[1]*R[5]-R[2]*R[4]) * invD;
        Rit[7] =  (R[2]*R[3]-R[0]*R[5]) * invD;
        Rit[8] =  (R[0]*R[4]-R[1]*R[3]) * invD;

        for (int i = 0; i < 9; i++) R[i] = 0.5f * (R[i] + Rit[i]);
    }

    // Ensure det(R) > 0
    if (det3(R) < 0) {
        for (int i = 0; i < 9; i++) R[i] = -R[i];
    }
}

static std::vector<float> kabschResult(16);

val computeRigidAlignment(uintptr_t srcPtr, uintptr_t tgtPtr, int numPoints) {
    const float* src = reinterpret_cast<const float*>(srcPtr);
    const float* tgt = reinterpret_cast<const float*>(tgtPtr);

    // 1. Compute centroids
    float cs[3] = {0,0,0}, ct[3] = {0,0,0};
    for (int i = 0; i < numPoints; i++) {
        cs[0] += src[i*3]; cs[1] += src[i*3+1]; cs[2] += src[i*3+2];
        ct[0] += tgt[i*3]; ct[1] += tgt[i*3+1]; ct[2] += tgt[i*3+2];
    }
    float invN = 1.0f / numPoints;
    for (int i = 0; i < 3; i++) { cs[i] *= invN; ct[i] *= invN; }

    // 2. Covariance matrix H = sum((s-cs)(t-ct)^T) → 3x3 row-major
    float H[9] = {0};
    for (int i = 0; i < numPoints; i++) {
        float ds[3] = {src[i*3]-cs[0], src[i*3+1]-cs[1], src[i*3+2]-cs[2]};
        float dt[3] = {tgt[i*3]-ct[0], tgt[i*3+1]-ct[1], tgt[i*3+2]-ct[2]};
        for (int r = 0; r < 3; r++)
            for (int c = 0; c < 3; c++)
                H[r*3+c] += ds[r] * dt[c];
    }

    // 3. Polar decomposition to get rotation R
    float R[9];
    polarDecomp3(H, R);

    // 4. Translation t = ct - R * cs
    float tx = ct[0] - (R[0]*cs[0] + R[1]*cs[1] + R[2]*cs[2]);
    float ty = ct[1] - (R[3]*cs[0] + R[4]*cs[1] + R[5]*cs[2]);
    float tz = ct[2] - (R[6]*cs[0] + R[7]*cs[1] + R[8]*cs[2]);

    // 5. Build 4x4 column-major matrix
    kabschResult.resize(16);
    // Col 0
    kabschResult[0] = R[0]; kabschResult[1] = R[3]; kabschResult[2] = R[6]; kabschResult[3] = 0;
    // Col 1
    kabschResult[4] = R[1]; kabschResult[5] = R[4]; kabschResult[6] = R[7]; kabschResult[7] = 0;
    // Col 2
    kabschResult[8] = R[2]; kabschResult[9] = R[5]; kabschResult[10] = R[8]; kabschResult[11] = 0;
    // Col 3
    kabschResult[12] = tx; kabschResult[13] = ty; kabschResult[14] = tz; kabschResult[15] = 1;

    return val(typed_memory_view(16, kabschResult.data()));
}

// --- KD-Tree for fast nearest neighbor ---
struct KDNode {
    int pointIdx;
    int splitAxis;
    int left, right; // indices into node array (-1 = leaf)
};

struct KDTree {
    std::vector<KDNode> nodes;
    const std::vector<Vec3>* points;

    void build(const std::vector<Vec3>& pts) {
        points = &pts;
        nodes.clear();
        nodes.reserve(pts.size() * 2);
        std::vector<int> indices(pts.size());
        for (size_t i = 0; i < pts.size(); i++) indices[i] = (int)i;
        buildRec(indices, 0, (int)indices.size(), 0);
    }

    int buildRec(std::vector<int>& indices, int start, int end, int depth) {
        if (start >= end) return -1;
        int axis = depth % 3;
        int mid = (start + end) / 2;
        // Partial sort to find median
        std::nth_element(indices.begin()+start, indices.begin()+mid, indices.begin()+end,
            [&](int a, int b) {
                float va = axis==0 ? (*points)[a].x : axis==1 ? (*points)[a].y : (*points)[a].z;
                float vb = axis==0 ? (*points)[b].x : axis==1 ? (*points)[b].y : (*points)[b].z;
                return va < vb;
            });
        int nodeIdx = (int)nodes.size();
        nodes.push_back({indices[mid], axis, -1, -1});
        nodes[nodeIdx].left = buildRec(indices, start, mid, depth+1);
        nodes[nodeIdx].right = buildRec(indices, mid+1, end, depth+1);
        return nodeIdx;
    }

    void nearest(int nodeIdx, const Vec3& query, int& bestIdx, float& bestDist2) const {
        if (nodeIdx < 0) return;
        const auto& node = nodes[nodeIdx];
        const Vec3& p = (*points)[node.pointIdx];
        float dx=query.x-p.x, dy=query.y-p.y, dz=query.z-p.z;
        float d2 = dx*dx+dy*dy+dz*dz;
        if (d2 < bestDist2) { bestDist2 = d2; bestIdx = node.pointIdx; }

        float queryVal = node.splitAxis==0 ? query.x : node.splitAxis==1 ? query.y : query.z;
        float splitVal = node.splitAxis==0 ? p.x : node.splitAxis==1 ? p.y : p.z;
        float diff = queryVal - splitVal;

        int first = diff < 0 ? node.left : node.right;
        int second = diff < 0 ? node.right : node.left;

        nearest(first, query, bestIdx, bestDist2);
        if (diff * diff < bestDist2) {
            nearest(second, query, bestIdx, bestDist2);
        }
    }

    int findNearest(const Vec3& query) const {
        int bestIdx = 0;
        float bestDist2 = 1e30f;
        if (!nodes.empty()) nearest(0, query, bestIdx, bestDist2);
        return bestIdx;
    }
};

// --- ICP with KD-Tree ---
std::string computeICP(int sourceHandle, int targetHandle, int maxIter, float threshold, float samplingRatio) {
    auto srcIt = meshStore.find(sourceHandle);
    auto tgtIt = meshStore.find(targetHandle);
    if (srcIt == meshStore.end() || tgtIt == meshStore.end())
        return "{\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],\"iterations\":0,\"finalError\":999}";

    auto& srcMesh = srcIt->second;
    auto& tgtMesh = tgtIt->second;

    // Sample source points
    int totalSrc = static_cast<int>(srcMesh.vertices.size());
    int sampleCount = std::max(10, static_cast<int>(totalSrc * samplingRatio));
    int step = std::max(1, totalSrc / sampleCount);
    std::vector<Vec3> samples;
    for (int i = 0; i < totalSrc; i += step) samples.push_back(srcMesh.vertices[i]);

    // Build KD-Tree for target (O(N log N), queried O(log N) per point)
    KDTree kdTree;
    kdTree.build(tgtMesh.vertices);

    // Current transform (column-major)
    float T[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
    float finalError = 999;
    int iterations = 0;

    for (int iter = 0; iter < maxIter; iter++) {
        iterations = iter + 1;

        // Transform sample points
        std::vector<Vec3> transformed(samples.size());
        for (size_t i = 0; i < samples.size(); i++) {
            float x=samples[i].x, y=samples[i].y, z=samples[i].z;
            transformed[i].x = T[0]*x + T[4]*y + T[8]*z  + T[12];
            transformed[i].y = T[1]*x + T[5]*y + T[9]*z  + T[13];
            transformed[i].z = T[2]*x + T[6]*y + T[10]*z + T[14];
        }

        // Find closest points using KD-Tree (O(N log N) total)
        std::vector<Vec3> closest(samples.size());
        float totalDist = 0;
        for (size_t i = 0; i < transformed.size(); i++) {
            int bestJ = kdTree.findNearest(transformed[i]);
            closest[i] = tgtMesh.vertices[bestJ];
            float dx = transformed[i].x - closest[i].x;
            float dy = transformed[i].y - closest[i].y;
            float dz = transformed[i].z - closest[i].z;
            totalDist += std::sqrt(dx*dx + dy*dy + dz*dz);
        }
        float rmsError = totalDist / static_cast<float>(transformed.size());

        // Check convergence
        if (std::abs(finalError - rmsError) < threshold && iter > 0) {
            finalError = rmsError;
            break;
        }
        finalError = rmsError;

        // Compute alignment from transformed → closest using Kabsch
        // Build flat arrays
        std::vector<float> srcFlat(transformed.size() * 3), tgtFlat(closest.size() * 3);
        for (size_t i = 0; i < transformed.size(); i++) {
            srcFlat[i*3]=transformed[i].x; srcFlat[i*3+1]=transformed[i].y; srcFlat[i*3+2]=transformed[i].z;
            tgtFlat[i*3]=closest[i].x;     tgtFlat[i*3+1]=closest[i].y;     tgtFlat[i*3+2]=closest[i].z;
        }

        // Centroids
        float cs2[3]={0}, ct2[3]={0};
        int n = static_cast<int>(transformed.size());
        for (int i = 0; i < n; i++) {
            cs2[0]+=srcFlat[i*3]; cs2[1]+=srcFlat[i*3+1]; cs2[2]+=srcFlat[i*3+2];
            ct2[0]+=tgtFlat[i*3]; ct2[1]+=tgtFlat[i*3+1]; ct2[2]+=tgtFlat[i*3+2];
        }
        float invN2 = 1.0f/n;
        for(int i=0;i<3;i++){cs2[i]*=invN2; ct2[i]*=invN2;}

        float H2[9]={0};
        for(int i=0;i<n;i++){
            float ds[3]={srcFlat[i*3]-cs2[0], srcFlat[i*3+1]-cs2[1], srcFlat[i*3+2]-cs2[2]};
            float dt[3]={tgtFlat[i*3]-ct2[0], tgtFlat[i*3+1]-ct2[1], tgtFlat[i*3+2]-ct2[2]};
            for(int r=0;r<3;r++) for(int c=0;c<3;c++) H2[r*3+c]+=ds[r]*dt[c];
        }
        float R2[9];
        polarDecomp3(H2, R2);

        float tx2 = ct2[0]-(R2[0]*cs2[0]+R2[1]*cs2[1]+R2[2]*cs2[2]);
        float ty2 = ct2[1]-(R2[3]*cs2[0]+R2[4]*cs2[1]+R2[5]*cs2[2]);
        float tz2 = ct2[2]-(R2[6]*cs2[0]+R2[7]*cs2[1]+R2[8]*cs2[2]);

        // Incremental transform (column-major)
        float dT[16] = {
            R2[0], R2[3], R2[6], 0,
            R2[1], R2[4], R2[7], 0,
            R2[2], R2[5], R2[8], 0,
            tx2,   ty2,   tz2,   1
        };

        // T = dT * T
        float newT[16];
        for(int r=0;r<4;r++) for(int c=0;c<4;c++){
            newT[c*4+r]=0;
            for(int k=0;k<4;k++) newT[c*4+r]+=dT[k*4+r]*T[c*4+k];
        }
        std::memcpy(T, newT, 64);
    }

    std::ostringstream json;
    json << "{\"matrix\":[";
    for(int i=0;i<16;i++){if(i)json<<",";json<<T[i];}
    json << "],\"iterations\":" << iterations << ",\"finalError\":" << finalError << "}";
    return json.str();
}

// ============ UV & Texture ============

bool meshHasUVs(int handle) {
    auto it = meshStore.find(handle);
    return it != meshStore.end() && it->second.hasUVs;
}

val getUVBuffer(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasUVs) return val::null();
    auto& uv = it->second.texcoords;
    return val(typed_memory_view(uv.size(), uv.data()));
}

std::string loadMeshFromOBJ_Extended(const std::string& objText) {
    // Parse OBJ with vt support, vertex splitting for different UV indices
    std::vector<Vec3> positions;
    std::vector<float> uvCoords; // all vt entries (u,v pairs)
    std::string mtllib;
    std::vector<std::string> materials;

    // Combined vertex = (position_index, uv_index) → new vertex index
    struct VUKey { int vi, ti; bool operator==(const VUKey& o) const { return vi==o.vi && ti==o.ti; }};
    struct VUHash { size_t operator()(const VUKey& k) const { return std::hash<long long>()(static_cast<long long>(k.vi)*1000003LL+k.ti); }};
    std::unordered_map<VUKey, int, VUHash> vertMap;

    Mesh mesh;
    bool hasVT = false;

    std::istringstream stream(objText);
    std::string line;

    while (std::getline(stream, line)) {
        if (line.empty() || line[0] == '#') continue;
        std::istringstream ls(line);
        std::string type;
        ls >> type;

        if (type == "v") {
            Vec3 v; ls >> v.x >> v.y >> v.z;
            positions.push_back(v);
        } else if (type == "vt") {
            float u, v; ls >> u >> v;
            uvCoords.push_back(u);
            uvCoords.push_back(v);
            hasVT = true;
        } else if (type == "mtllib") {
            ls >> mtllib;
        } else if (type == "usemtl") {
            std::string name; ls >> name;
            materials.push_back(name);
        } else if (type == "f") {
            Face face;
            std::string token;
            while (ls >> token) {
                int vi = 0, ti = -1;
                size_t slash1 = token.find('/');
                if (slash1 != std::string::npos) {
                    vi = std::stoi(token.substr(0, slash1)) - 1;
                    size_t slash2 = token.find('/', slash1 + 1);
                    std::string vtStr;
                    if (slash2 != std::string::npos) vtStr = token.substr(slash1+1, slash2-slash1-1);
                    else vtStr = token.substr(slash1+1);
                    if (!vtStr.empty()) ti = std::stoi(vtStr) - 1;
                } else {
                    vi = std::stoi(token) - 1;
                }

                // Map (vi, ti) → combined vertex index
                VUKey key{vi, ti};
                auto jt = vertMap.find(key);
                int idx;
                if (jt != vertMap.end()) {
                    idx = jt->second;
                } else {
                    idx = static_cast<int>(mesh.vertices.size());
                    vertMap[key] = idx;
                    if (vi >= 0 && vi < static_cast<int>(positions.size()))
                        mesh.vertices.push_back(positions[vi]);
                    else
                        mesh.vertices.push_back({0,0,0});
                    if (hasVT && ti >= 0 && ti * 2 + 1 < static_cast<int>(uvCoords.size())) {
                        mesh.texcoords.push_back(uvCoords[ti * 2]);
                        mesh.texcoords.push_back(uvCoords[ti * 2 + 1]);
                    } else if (hasVT) {
                        mesh.texcoords.push_back(0);
                        mesh.texcoords.push_back(0);
                    }
                }
                face.indices.push_back(idx);
            }
            mesh.faces.push_back(face);
        }
    }

    mesh.hasUVs = hasVT && mesh.texcoords.size() == mesh.vertices.size() * 2;
    mesh.mtllibName = mtllib;
    int handle = storeMesh(std::move(mesh));

    // Return JSON result
    std::ostringstream json;
    json << "{\"handle\":" << handle
         << ",\"mtllib\":\"" << mtllib << "\""
         << ",\"hasUVs\":" << (hasVT ? "true" : "false")
         << ",\"materials\":[";
    for (size_t i = 0; i < materials.size(); i++) {
        if (i > 0) json << ",";
        json << "\"" << materials[i] << "\"";
    }
    json << "]}";
    return json.str();
}

void filterTrivialUV(int handle, int textureSize) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Triangulate first
    std::vector<Face> tris;
    for (auto& f : mesh.faces) {
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            Face t; t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i+1)]};
            tris.push_back(t);
        }
    }

    // Each triangle gets its own 3 vertices (no sharing) for independent UVs
    int nTris = static_cast<int>(tris.size());
    int gridSize = static_cast<int>(std::ceil(std::sqrt(static_cast<float>(nTris))));
    float cellSize = 1.0f / static_cast<float>(gridSize);
    float margin = cellSize * 0.1f;
    float usable = cellSize - 2 * margin;

    std::vector<Vec3> newVerts;
    std::vector<Face> newFaces;
    std::vector<float> newUVs;

    for (int ti = 0; ti < nTris; ti++) {
        int row = ti / gridSize;
        int col = ti % gridSize;
        float baseU = col * cellSize + margin;
        float baseV = row * cellSize + margin;

        int vi0 = static_cast<int>(newVerts.size());
        for (int j = 0; j < 3; j++) {
            newVerts.push_back(mesh.vertices[tris[ti].indices[j]]);
        }

        // Simple UV layout: equilateral triangle in cell
        newUVs.push_back(baseU);           newUVs.push_back(baseV);
        newUVs.push_back(baseU + usable);  newUVs.push_back(baseV);
        newUVs.push_back(baseU + usable*0.5f); newUVs.push_back(baseV + usable);

        Face f; f.indices = {vi0, vi0+1, vi0+2};
        newFaces.push_back(f);
    }

    mesh.vertices = newVerts;
    mesh.faces = newFaces;
    mesh.texcoords = newUVs;
    mesh.hasUVs = true;
    mesh.faceSelected.clear();
    mesh.vertexSelected.clear();
    mesh.triangulate();
}

void textureToVertexColor(int handle, uintptr_t pixelPtr, int width, int height) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end() || !it->second.hasUVs) return;
    auto& mesh = it->second;
    const uint8_t* pixels = reinterpret_cast<const uint8_t*>(pixelPtr);

    mesh.vertexColors.resize(mesh.vertices.size() * 3);
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        float u = mesh.texcoords[i * 2];
        float v = mesh.texcoords[i * 2 + 1];
        // Clamp and sample (nearest neighbor)
        int px = static_cast<int>(std::max(0.0f, std::min(static_cast<float>(width - 1), u * width)));
        int py = static_cast<int>(std::max(0.0f, std::min(static_cast<float>(height - 1), (1.0f - v) * height)));
        int idx = (py * width + px) * 4; // RGBA
        mesh.vertexColors[i * 3] = pixels[idx];
        mesh.vertexColors[i * 3 + 1] = pixels[idx + 1];
        mesh.vertexColors[i * 3 + 2] = pixels[idx + 2];
    }
    mesh.hasColors = true;
}

// ============ Cleaning & Repair ============

// --- Edge-face map helper ---
struct EKey2 { int a,b; bool operator==(const EKey2& o) const { return a==o.a && b==o.b; }};
struct EHash2 { size_t operator()(const EKey2& k) const { return std::hash<long long>()(static_cast<long long>(k.a)*1000003LL+k.b); }};

static std::unordered_map<EKey2, std::vector<int>, EHash2> buildEdgeFaceMap(const Mesh& mesh) {
    std::unordered_map<EKey2, std::vector<int>, EHash2> ef;
    for (int fi = 0; fi < static_cast<int>(mesh.faces.size()); fi++) {
        auto& f = mesh.faces[fi];
        for (size_t i = 0; i < f.indices.size(); i++) {
            int a = f.indices[i], b = f.indices[(i+1) % f.indices.size()];
            if (a > b) std::swap(a,b);
            ef[{a,b}].push_back(fi);
        }
    }
    return ef;
}

int detectNonManifoldEdges(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    auto ef = buildEdgeFaceMap(mesh);
    int count = 0;
    for (auto& [edge, faces] : ef) {
        if (faces.size() > 2) {
            count++;
            for (int fi : faces) mesh.faceSelected[fi] = 1;
        }
    }
    return count;
}

int fixNonManifoldEdges(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    auto ef = buildEdgeFaceMap(mesh);
    std::vector<bool> toRemove(mesh.faces.size(), false);
    int fixed = 0;
    for (auto& [edge, faces] : ef) {
        if (faces.size() > 2) {
            // Keep first 2, remove rest
            for (size_t i = 2; i < faces.size(); i++) {
                toRemove[faces[i]] = true;
            }
            fixed++;
        }
    }
    std::vector<Face> newFaces;
    for (size_t i = 0; i < mesh.faces.size(); i++) {
        if (!toRemove[i]) newFaces.push_back(mesh.faces[i]);
    }
    mesh.faces = newFaces;
    mesh.faceSelected.clear();
    mesh.triangulate();
    return fixed;
}

int detectHoles(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    auto ef = buildEdgeFaceMap(mesh);

    // Find boundary edges (shared by exactly 1 face)
    std::unordered_map<int, std::vector<int>> boundaryAdj;
    for (auto& [edge, faces] : ef) {
        if (faces.size() == 1) {
            boundaryAdj[edge.a].push_back(edge.b);
            boundaryAdj[edge.b].push_back(edge.a);
        }
    }
    if (boundaryAdj.empty()) return 0;

    // Trace boundary loops
    std::unordered_set<int> visited;
    int holes = 0;
    for (auto& [v, _] : boundaryAdj) {
        if (visited.count(v)) continue;
        // Trace loop
        int cur = v;
        while (!visited.count(cur)) {
            visited.insert(cur);
            bool found = false;
            for (int nb : boundaryAdj[cur]) {
                if (!visited.count(nb)) { cur = nb; found = true; break; }
            }
            if (!found) break;
        }
        holes++;
    }
    return holes;
}

int fillHoles(int handle, int maxEdges) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    auto ef = buildEdgeFaceMap(mesh);

    // Find boundary edges
    std::unordered_map<int, std::vector<int>> boundaryAdj;
    for (auto& [edge, faces] : ef) {
        if (faces.size() == 1) {
            boundaryAdj[edge.a].push_back(edge.b);
            boundaryAdj[edge.b].push_back(edge.a);
        }
    }
    if (boundaryAdj.empty()) return 0;

    // Trace loops and fill
    std::unordered_set<int> visited;
    int filled = 0;

    for (auto& [startV, _] : boundaryAdj) {
        if (visited.count(startV)) continue;
        std::vector<int> loop;
        int cur = startV;
        while (!visited.count(cur)) {
            visited.insert(cur);
            loop.push_back(cur);
            bool found = false;
            for (int nb : boundaryAdj[cur]) {
                if (!visited.count(nb)) { cur = nb; found = true; break; }
            }
            if (!found) break;
        }

        if (loop.size() < 3) continue;
        if (maxEdges > 0 && static_cast<int>(loop.size()) > maxEdges) continue;

        // Ear clipping triangulation
        std::vector<int> poly = loop;
        while (poly.size() > 3) {
            bool earFound = false;
            for (size_t i = 0; i < poly.size(); i++) {
                // Simple ear: always clip (non-optimal but works)
                size_t prev = (i + poly.size() - 1) % poly.size();
                size_t next = (i + 1) % poly.size();
                Face f;
                f.indices = {poly[prev], poly[i], poly[next]};
                mesh.faces.push_back(f);
                poly.erase(poly.begin() + static_cast<int>(i));
                earFound = true;
                break;
            }
            if (!earFound) break;
        }
        if (poly.size() == 3) {
            Face f;
            f.indices = {poly[0], poly[1], poly[2]};
            mesh.faces.push_back(f);
        }
        filled++;
    }

    mesh.faceSelected.clear();
    mesh.triangulate();
    return filled;
}

int removeSmallComponents(int handle, int minFaces) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    int nf = static_cast<int>(mesh.faces.size());

    std::unordered_map<int, std::vector<int>> vertToFace;
    for (int fi = 0; fi < nf; fi++)
        for (int vi : mesh.faces[fi].indices) vertToFace[vi].push_back(fi);

    std::vector<int> compId(nf, -1);
    std::vector<int> compSize;
    int compIdx = 0;
    for (int fi = 0; fi < nf; fi++) {
        if (compId[fi] >= 0) continue;
        std::vector<int> q = {fi};
        compId[fi] = compIdx;
        int sz = 0;
        size_t head = 0;
        while (head < q.size()) {
            int cur = q[head++]; sz++;
            for (int vi : mesh.faces[cur].indices)
                for (int nb : vertToFace[vi])
                    if (compId[nb] < 0) { compId[nb] = compIdx; q.push_back(nb); }
        }
        compSize.push_back(sz);
        compIdx++;
    }

    std::vector<Face> newFaces;
    int removed = 0;
    for (int fi = 0; fi < nf; fi++) {
        if (compSize[compId[fi]] < minFaces) removed++;
        else newFaces.push_back(mesh.faces[fi]);
    }
    if (removed == 0) return 0;
    mesh.faces = newFaces;
    mesh.faceSelected.clear();

    // Clean vertices
    std::vector<bool> used(mesh.vertices.size(), false);
    for (auto& f : mesh.faces) for (int idx : f.indices)
        if (idx >= 0 && idx < static_cast<int>(used.size())) used[idx] = true;
    std::vector<int> remap(mesh.vertices.size(), -1);
    std::vector<Vec3> nv;
    for (size_t i = 0; i < mesh.vertices.size(); i++)
        if (used[i]) { remap[i] = static_cast<int>(nv.size()); nv.push_back(mesh.vertices[i]); }
    mesh.vertices = nv;
    for (auto& f : mesh.faces) for (auto& idx : f.indices) idx = remap[idx];

    mesh.triangulate();
    return removed;
}

int removeDegenerateFaces(int handle, float areaThreshold) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;

    if (areaThreshold <= 0) {
        // Auto threshold based on bbox diagonal
        float minX=1e30f,minY=1e30f,minZ=1e30f,maxX=-1e30f,maxY=-1e30f,maxZ=-1e30f;
        for (auto& v : mesh.vertices) {
            if(v.x<minX) minX=v.x; if(v.x>maxX) maxX=v.x;
            if(v.y<minY) minY=v.y; if(v.y>maxY) maxY=v.y;
            if(v.z<minZ) minZ=v.z; if(v.z>maxZ) maxZ=v.z;
        }
        float diag = std::sqrt((maxX-minX)*(maxX-minX)+(maxY-minY)*(maxY-minY)+(maxZ-minZ)*(maxZ-minZ));
        areaThreshold = diag * diag * 1e-10f;
    }

    std::vector<Face> newFaces;
    int removed = 0;
    for (auto& f : mesh.faces) {
        if (f.indices.size() < 3) { removed++; continue; }
        float area = 0;
        for (size_t i = 1; i+1 < f.indices.size(); i++)
            area += triangleArea(mesh.vertices[f.indices[0]], mesh.vertices[f.indices[i]], mesh.vertices[f.indices[i+1]]);
        if (area <= areaThreshold) removed++;
        else newFaces.push_back(f);
    }
    mesh.faces = newFaces;
    mesh.faceSelected.clear();
    mesh.triangulate();
    return removed;
}

int detectFlippedFaces(int handle, float angleDeg) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();

    float cosThresh = std::cos(angleDeg * 3.14159265358979f / 180.0f);
    auto ef = buildEdgeFaceMap(mesh);

    // Compute face normals
    auto faceNormal = [&](int fi) -> Vec3 {
        auto& f = mesh.faces[fi];
        if (f.indices.size() < 3) return {0,0,1};
        auto& v0=mesh.vertices[f.indices[0]], &v1=mesh.vertices[f.indices[1]], &v2=mesh.vertices[f.indices[2]];
        float e1x=v1.x-v0.x, e1y=v1.y-v0.y, e1z=v1.z-v0.z;
        float e2x=v2.x-v0.x, e2y=v2.y-v0.y, e2z=v2.z-v0.z;
        float nx=e1y*e2z-e1z*e2y, ny=e1z*e2x-e1x*e2z, nz=e1x*e2y-e1y*e2x;
        float len=std::sqrt(nx*nx+ny*ny+nz*nz);
        if(len>0) { nx/=len; ny/=len; nz/=len; }
        return {nx,ny,nz};
    };

    int count = 0;
    for (auto& [edge, faces] : ef) {
        if (faces.size() != 2) continue;
        auto n1 = faceNormal(faces[0]);
        auto n2 = faceNormal(faces[1]);
        float dot = n1.x*n2.x + n1.y*n2.y + n1.z*n2.z;
        if (dot < cosThresh) {
            mesh.faceSelected[faces[0]] = 1;
            mesh.faceSelected[faces[1]] = 1;
            count++;
        }
    }
    return count;
}

int removeFlippedFaces(int handle, float angleDeg) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    std::fill(mesh.faceSelected.begin(), mesh.faceSelected.end(), 0);
    int count = detectFlippedFaces(handle, angleDeg);
    if (count == 0) return 0;
    // Delete selected (flipped) faces
    std::vector<Face> newFaces;
    int removed = 0;
    for (size_t i = 0; i < mesh.faces.size(); i++) {
        if (mesh.faceSelected[i]) removed++;
        else newFaces.push_back(mesh.faces[i]);
    }
    mesh.faces = newFaces;
    mesh.faceSelected.clear();
    mesh.triangulate();
    return removed;
}

int detectSelfIntersections(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();

    // Simple O(n²) check for small meshes, grid-based for larger
    int nf = static_cast<int>(mesh.faces.size());
    if (nf == 0) return 0;

    // Compute face bboxes
    struct FBox { float minX,minY,minZ,maxX,maxY,maxZ; };
    std::vector<FBox> fboxes(nf);
    for (int fi = 0; fi < nf; fi++) {
        auto& f = mesh.faces[fi];
        float mnx=1e30f,mny=1e30f,mnz=1e30f,mxx=-1e30f,mxy=-1e30f,mxz=-1e30f;
        for (int vi : f.indices) {
            auto& v = mesh.vertices[vi];
            if(v.x<mnx) mnx=v.x; if(v.x>mxx) mxx=v.x;
            if(v.y<mny) mny=v.y; if(v.y>mxy) mxy=v.y;
            if(v.z<mnz) mnz=v.z; if(v.z>mxz) mxz=v.z;
        }
        fboxes[fi] = {mnx,mny,mnz,mxx,mxy,mxz};
    }

    int count = 0;
    // Limit to first 10000 faces for performance
    int limit = std::min(nf, 10000);
    for (int i = 0; i < limit; i++) {
        for (int j = i+1; j < limit; j++) {
            // Check bbox overlap
            auto& a = fboxes[i]; auto& b = fboxes[j];
            if (a.maxX < b.minX || b.maxX < a.minX) continue;
            if (a.maxY < b.minY || b.maxY < a.minY) continue;
            if (a.maxZ < b.minZ || b.maxZ < a.minZ) continue;

            // Check if they share a vertex (skip adjacent)
            bool shared = false;
            for (int vi : mesh.faces[i].indices)
                for (int vj : mesh.faces[j].indices)
                    if (vi == vj) { shared = true; break; }
            if (shared) continue;

            // Simplified intersection: if bboxes overlap and not adjacent, flag both
            mesh.faceSelected[i] = 1;
            mesh.faceSelected[j] = 1;
            count++;
        }
    }
    return count;
}

std::string getMeshStats(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return "{}";
    auto& mesh = it->second;

    int verts = static_cast<int>(mesh.vertices.size());
    int faces = static_cast<int>(mesh.faces.size());

    // Count edges
    auto ef = buildEdgeFaceMap(mesh);
    int edges = static_cast<int>(ef.size());

    // Non-manifold edges
    int nmEdges = 0;
    int boundaryEdges = 0;
    for (auto& [e, fs] : ef) {
        if (fs.size() > 2) nmEdges++;
        if (fs.size() == 1) boundaryEdges++;
    }

    // Components
    int nf = faces;
    std::unordered_map<int, std::vector<int>> vtf;
    for (int fi = 0; fi < nf; fi++)
        for (int vi : mesh.faces[fi].indices) vtf[vi].push_back(fi);
    std::vector<int> cid(nf, -1);
    int comps = 0;
    for (int fi = 0; fi < nf; fi++) {
        if (cid[fi] >= 0) continue;
        std::vector<int> q = {fi}; cid[fi] = comps;
        size_t head = 0;
        while (head < q.size()) {
            int cur = q[head++];
            for (int vi : mesh.faces[cur].indices)
                for (int nb : vtf[vi])
                    if (cid[nb] < 0) { cid[nb] = comps; q.push_back(nb); }
        }
        comps++;
    }

    bool closed = (boundaryEdges == 0 && nmEdges == 0);
    int euler = verts - edges + faces;
    int genus = closed ? (1 - euler / 2) : 0;

    // Holes
    int holes = detectHoles(handle);

    // Degenerate faces count
    int degenerate = 0;
    float diagSq = 0;
    {
        float mnx=1e30f,mny=1e30f,mnz=1e30f,mxx=-1e30f,mxy=-1e30f,mxz=-1e30f;
        for (auto& v : mesh.vertices) {
            if(v.x<mnx) mnx=v.x; if(v.x>mxx) mxx=v.x;
            if(v.y<mny) mny=v.y; if(v.y>mxy) mxy=v.y;
            if(v.z<mnz) mnz=v.z; if(v.z>mxz) mxz=v.z;
        }
        diagSq = (mxx-mnx)*(mxx-mnx)+(mxy-mny)*(mxy-mny)+(mxz-mnz)*(mxz-mnz);
    }
    float areaThresh = diagSq * 1e-10f;
    for (auto& f : mesh.faces) {
        if (f.indices.size() < 3) { degenerate++; continue; }
        float area = 0;
        for (size_t i = 1; i+1 < f.indices.size(); i++)
            area += triangleArea(mesh.vertices[f.indices[0]], mesh.vertices[f.indices[i]], mesh.vertices[f.indices[i+1]]);
        if (area <= areaThresh) degenerate++;
    }

    // Duplicate vertices
    struct VK3 { float x,y,z; bool operator==(const VK3& o) const { return x==o.x&&y==o.y&&z==o.z; }};
    struct VH3 { size_t operator()(const VK3& k) const { size_t h=0; auto hf=std::hash<float>{}; h^=hf(k.x)+0x9e3779b9+(h<<6)+(h>>2); h^=hf(k.y)+0x9e3779b9+(h<<6)+(h>>2); h^=hf(k.z)+0x9e3779b9+(h<<6)+(h>>2); return h; }};
    std::unordered_map<VK3,bool,VH3> uniqVerts;
    int dupVerts = 0;
    for (auto& v : mesh.vertices) {
        VK3 k{v.x,v.y,v.z};
        if (uniqVerts.count(k)) dupVerts++;
        else uniqVerts[k] = true;
    }

    // Unreferenced vertices
    std::vector<bool> used(mesh.vertices.size(), false);
    for (auto& f : mesh.faces) for (int idx : f.indices)
        if (idx >= 0 && idx < static_cast<int>(used.size())) used[idx] = true;
    int unrefVerts = 0;
    for (size_t i = 0; i < used.size(); i++) if (!used[i]) unrefVerts++;

    // Compute area inline (avoiding forward decl)
    float area = 0;
    for (auto& f2 : mesh.faces)
        for (size_t i = 1; i+1 < f2.indices.size(); i++)
            area += triangleArea(mesh.vertices[f2.indices[0]], mesh.vertices[f2.indices[i]], mesh.vertices[f2.indices[i+1]]);

    float volume = 0;
    for (auto& f2 : mesh.faces)
        for (size_t i = 1; i+1 < f2.indices.size(); i++) {
            auto& va=mesh.vertices[f2.indices[0]], &vb=mesh.vertices[f2.indices[i]], &vc=mesh.vertices[f2.indices[i+1]];
            volume += va.x*(vb.y*vc.z-vb.z*vc.y) - va.y*(vb.x*vc.z-vb.z*vc.x) + va.z*(vb.x*vc.y-vb.y*vc.x);
        }
    volume = std::abs(volume) / 6.0f;

    std::ostringstream json;
    json << "{\"vertices\":" << verts
         << ",\"faces\":" << faces
         << ",\"edges\":" << edges
         << ",\"components\":" << comps
         << ",\"boundaryEdges\":" << boundaryEdges
         << ",\"holes\":" << holes
         << ",\"isClosed\":" << (closed ? "true" : "false")
         << ",\"eulerNumber\":" << euler
         << ",\"genus\":" << genus
         << ",\"area\":" << area
         << ",\"volume\":" << volume
         << ",\"nonManifoldEdges\":" << nmEdges
         << ",\"nonManifoldVertices\":0"
         << ",\"degenerateFaces\":" << degenerate
         << ",\"duplicateVertices\":" << dupVerts
         << ",\"unreferencedVertices\":" << unrefVerts
         << "}";
    return json.str();
}

// ============ Measurement ============

float computeTotalArea(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    float total = 0;
    for (auto& f : mesh.faces) {
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            total += triangleArea(mesh.vertices[f.indices[0]],
                                  mesh.vertices[f.indices[i]],
                                  mesh.vertices[f.indices[i+1]]);
        }
    }
    return total;
}

float computeSelectedArea(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();
    float total = 0;
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        if (!mesh.faceSelected[fi]) continue;
        auto& f = mesh.faces[fi];
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            total += triangleArea(mesh.vertices[f.indices[0]],
                                  mesh.vertices[f.indices[i]],
                                  mesh.vertices[f.indices[i+1]]);
        }
    }
    return total;
}

float computeVolume(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return 0;
    auto& mesh = it->second;
    float vol = 0;
    for (auto& f : mesh.faces) {
        for (size_t i = 1; i + 1 < f.indices.size(); i++) {
            auto& v0 = mesh.vertices[f.indices[0]];
            auto& v1 = mesh.vertices[f.indices[i]];
            auto& v2 = mesh.vertices[f.indices[i+1]];
            // Signed volume of tetrahedron with origin
            vol += (v0.x * (v1.y * v2.z - v1.z * v2.y)
                  - v0.y * (v1.x * v2.z - v1.z * v2.x)
                  + v0.z * (v1.x * v2.y - v1.y * v2.x));
        }
    }
    return std::abs(vol) / 6.0f;
}

bool isMeshClosed(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return false;
    auto& mesh = it->second;

    struct EKey { int a, b; bool operator==(const EKey& o) const { return a==o.a && b==o.b; }};
    struct EHash { size_t operator()(const EKey& k) const { return std::hash<long long>()(static_cast<long long>(k.a)*1000003LL+k.b); }};
    std::unordered_map<EKey, int, EHash> edgeCount;

    for (auto& f : mesh.faces) {
        for (size_t i = 0; i < f.indices.size(); i++) {
            int a = f.indices[i], b = f.indices[(i+1) % f.indices.size()];
            if (a > b) std::swap(a, b);
            edgeCount[{a, b}]++;
        }
    }
    for (auto& [edge, cnt] : edgeCount) {
        if (cnt != 2) return false;
    }
    return true;
}

static std::vector<float> sectionContourBuffer;

val computeSectionContour(int handle, float nx, float ny, float nz, float d) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    sectionContourBuffer.clear();

    // For each edge, check if the plane intersects it
    // Plane equation: nx*x + ny*y + nz*z + d = 0
    struct EKey { int a, b; bool operator==(const EKey& o) const { return a==o.a && b==o.b; }};
    struct EHash { size_t operator()(const EKey& k) const { return std::hash<long long>()(static_cast<long long>(k.a)*1000003LL+k.b); }};
    std::unordered_map<EKey, bool, EHash> processed;

    for (auto& f : mesh.faces) {
        for (size_t i = 0; i < f.indices.size(); i++) {
            int ai = f.indices[i], bi = f.indices[(i+1) % f.indices.size()];
            int ea = std::min(ai, bi), eb = std::max(ai, bi);
            EKey key{ea, eb};
            if (processed.count(key)) continue;
            processed[key] = true;

            auto& va = mesh.vertices[ai];
            auto& vb = mesh.vertices[bi];
            float da = nx*va.x + ny*va.y + nz*va.z + d;
            float db = nx*vb.x + ny*vb.y + nz*vb.z + d;

            if ((da > 0) != (db > 0)) {
                // Edge crosses the plane
                float t = da / (da - db);
                float px = va.x + t * (vb.x - va.x);
                float py = va.y + t * (vb.y - va.y);
                float pz = va.z + t * (vb.z - va.z);
                sectionContourBuffer.push_back(px);
                sectionContourBuffer.push_back(py);
                sectionContourBuffer.push_back(pz);
            }
        }
    }

    // The points are unordered intersection points; pair them into line segments
    // For proper contour, we'd need to sort them per-face. For now, output as points
    // and let the caller draw them as points or connect adjacent pairs.
    // Simple approach: for each face, find the 2 intersection points and make a segment
    sectionContourBuffer.clear();
    for (auto& f : mesh.faces) {
        std::vector<Vec3> isectPoints;
        for (size_t i = 0; i < f.indices.size(); i++) {
            int ai = f.indices[i], bi = f.indices[(i+1) % f.indices.size()];
            auto& va = mesh.vertices[ai];
            auto& vb = mesh.vertices[bi];
            float da2 = nx*va.x + ny*va.y + nz*va.z + d;
            float db2 = nx*vb.x + ny*vb.y + nz*vb.z + d;
            if ((da2 > 0) != (db2 > 0)) {
                float t = da2 / (da2 - db2);
                isectPoints.push_back({
                    va.x + t*(vb.x-va.x),
                    va.y + t*(vb.y-va.y),
                    va.z + t*(vb.z-va.z)
                });
            }
        }
        if (isectPoints.size() >= 2) {
            sectionContourBuffer.push_back(isectPoints[0].x);
            sectionContourBuffer.push_back(isectPoints[0].y);
            sectionContourBuffer.push_back(isectPoints[0].z);
            sectionContourBuffer.push_back(isectPoints[1].x);
            sectionContourBuffer.push_back(isectPoints[1].y);
            sectionContourBuffer.push_back(isectPoints[1].z);
        }
    }

    return val(typed_memory_view(sectionContourBuffer.size(), sectionContourBuffer.data()));
}

// ============ Crease Edges ============

// Returns a static buffer of edge vertex positions: [x0,y0,z0,x1,y1,z1,...] for edges
// where the dihedral angle between adjacent faces exceeds angleDeg.
static std::vector<float> creaseEdgeBuffer;

val getCreaseEdges(int handle, float angleDeg) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    creaseEdgeBuffer.clear();

    float cosThresh = std::cos(angleDeg * 3.14159265358979f / 180.0f);

    // Compute per-face normals
    struct FN { float nx, ny, nz; };
    std::vector<FN> faceNorms(mesh.faces.size());
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        auto& f = mesh.faces[fi];
        if (f.indices.size() < 3) { faceNorms[fi] = {0,0,1}; continue; }
        auto& v0 = mesh.vertices[f.indices[0]];
        auto& v1 = mesh.vertices[f.indices[1]];
        auto& v2 = mesh.vertices[f.indices[2]];
        float e1x=v1.x-v0.x, e1y=v1.y-v0.y, e1z=v1.z-v0.z;
        float e2x=v2.x-v0.x, e2y=v2.y-v0.y, e2z=v2.z-v0.z;
        float nx=e1y*e2z-e1z*e2y, ny=e1z*e2x-e1x*e2z, nz=e1x*e2y-e1y*e2x;
        float len = std::sqrt(nx*nx+ny*ny+nz*nz);
        if (len > 0) { nx/=len; ny/=len; nz/=len; }
        faceNorms[fi] = {nx, ny, nz};
    }

    // Build edge→faces map  (edge key: min(a,b), max(a,b))
    struct EKey { int a, b; bool operator==(const EKey& o) const { return a==o.a && b==o.b; }};
    struct EHash { size_t operator()(const EKey& k) const { return std::hash<long long>()(static_cast<long long>(k.a)*1000003LL+k.b); }};
    std::unordered_map<EKey, std::vector<int>, EHash> edgeFaces;

    for (int fi = 0; fi < static_cast<int>(mesh.faces.size()); fi++) {
        auto& f = mesh.faces[fi];
        for (size_t ei = 0; ei < f.indices.size(); ei++) {
            int a = f.indices[ei];
            int b = f.indices[(ei+1) % f.indices.size()];
            if (a > b) std::swap(a, b);
            edgeFaces[{a,b}].push_back(fi);
        }
    }

    // Check each edge shared by exactly 2 faces
    for (auto& [edge, faces] : edgeFaces) {
        if (faces.size() != 2) {
            // Boundary edge — always show
            auto& va = mesh.vertices[edge.a];
            auto& vb = mesh.vertices[edge.b];
            creaseEdgeBuffer.push_back(va.x); creaseEdgeBuffer.push_back(va.y); creaseEdgeBuffer.push_back(va.z);
            creaseEdgeBuffer.push_back(vb.x); creaseEdgeBuffer.push_back(vb.y); creaseEdgeBuffer.push_back(vb.z);
            continue;
        }
        auto& n1 = faceNorms[faces[0]];
        auto& n2 = faceNorms[faces[1]];
        float dot = n1.nx*n2.nx + n1.ny*n2.ny + n1.nz*n2.nz;
        if (dot < cosThresh) {
            auto& va = mesh.vertices[edge.a];
            auto& vb = mesh.vertices[edge.b];
            creaseEdgeBuffer.push_back(va.x); creaseEdgeBuffer.push_back(va.y); creaseEdgeBuffer.push_back(va.z);
            creaseEdgeBuffer.push_back(vb.x); creaseEdgeBuffer.push_back(vb.y); creaseEdgeBuffer.push_back(vb.z);
        }
    }

    return val(typed_memory_view(creaseEdgeBuffer.size(), creaseEdgeBuffer.data()));
}

// ---- Boundary edges (edges shared by only 1 face) ----
static std::vector<float> boundaryEdgeBuffer;
val getBoundaryEdges(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    boundaryEdgeBuffer.clear();

    struct EK { int a, b; bool operator==(const EK& o) const { return a==o.a && b==o.b; }};
    struct EH { size_t operator()(const EK& k) const { return std::hash<long long>()((long long)k.a*1000003LL+k.b); }};
    std::unordered_map<EK, int, EH> edgeCount;
    for (auto& f : mesh.faces) {
        for (size_t e = 0; e < f.indices.size(); e++) {
            int a = f.indices[e], b = f.indices[(e+1)%f.indices.size()];
            if (a > b) std::swap(a, b);
            edgeCount[{a,b}]++;
        }
    }
    for (auto& [ek, cnt] : edgeCount) {
        if (cnt == 1) {
            auto& va = mesh.vertices[ek.a]; auto& vb = mesh.vertices[ek.b];
            boundaryEdgeBuffer.push_back(va.x); boundaryEdgeBuffer.push_back(va.y); boundaryEdgeBuffer.push_back(va.z);
            boundaryEdgeBuffer.push_back(vb.x); boundaryEdgeBuffer.push_back(vb.y); boundaryEdgeBuffer.push_back(vb.z);
        }
    }
    return val(typed_memory_view(boundaryEdgeBuffer.size(), boundaryEdgeBuffer.data()));
}

// ---- Non-manifold edges (edges shared by >2 faces) ----
static std::vector<float> nonManifoldEdgeBuffer;
val getNonManifoldEdges(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::null();
    auto& mesh = it->second;
    nonManifoldEdgeBuffer.clear();

    struct EK { int a, b; bool operator==(const EK& o) const { return a==o.a && b==o.b; }};
    struct EH { size_t operator()(const EK& k) const { return std::hash<long long>()((long long)k.a*1000003LL+k.b); }};
    std::unordered_map<EK, int, EH> edgeCount;
    for (auto& f : mesh.faces) {
        for (size_t e = 0; e < f.indices.size(); e++) {
            int a = f.indices[e], b = f.indices[(e+1)%f.indices.size()];
            if (a > b) std::swap(a, b);
            edgeCount[{a,b}]++;
        }
    }
    for (auto& [ek, cnt] : edgeCount) {
        if (cnt > 2) {
            auto& va = mesh.vertices[ek.a]; auto& vb = mesh.vertices[ek.b];
            nonManifoldEdgeBuffer.push_back(va.x); nonManifoldEdgeBuffer.push_back(va.y); nonManifoldEdgeBuffer.push_back(va.z);
            nonManifoldEdgeBuffer.push_back(vb.x); nonManifoldEdgeBuffer.push_back(vb.y); nonManifoldEdgeBuffer.push_back(vb.z);
        }
    }
    return val(typed_memory_view(nonManifoldEdgeBuffer.size(), nonManifoldEdgeBuffer.data()));
}

// ============ Sprint M: New Filters ============

// ---- Edge→opposite-vertex map for subdivision ----
struct EdgeKey {
    int a, b;
    bool operator==(const EdgeKey& o) const { return a == o.a && b == o.b; }
};
struct EdgeHash {
    size_t operator()(const EdgeKey& k) const {
        return std::hash<long long>()(static_cast<long long>(k.a) * 1000003LL + k.b);
    }
};

static EdgeKey makeEdge(int a, int b) {
    return a < b ? EdgeKey{a, b} : EdgeKey{b, a};
}

// Build edge→opposite vertices map (for Loop/Butterfly subdivision)
static std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> buildEdgeOpposites(const Mesh& mesh) {
    std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> edgeOpp;
    for (const auto& f : mesh.faces) {
        if (f.indices.size() != 3) continue;
        int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
        edgeOpp[makeEdge(v0, v1)].push_back(v2);
        edgeOpp[makeEdge(v1, v2)].push_back(v0);
        edgeOpp[makeEdge(v2, v0)].push_back(v1);
    }
    return edgeOpp;
}

// Detect boundary vertices
static std::unordered_set<int> findBoundaryVertices(const Mesh& mesh,
    const std::unordered_map<EdgeKey, std::vector<int>, EdgeHash>& edgeOpp) {
    std::unordered_set<int> bnd;
    for (auto& [e, opps] : edgeOpp) {
        if (opps.size() == 1) { bnd.insert(e.a); bnd.insert(e.b); }
    }
    return bnd;
}

// ---- 1. Loop Subdivision ----
void filterLoopSubdivision(int handle, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Ensure triangulated
    {
        std::vector<Face> tris;
        for (auto& f : mesh.faces) {
            for (size_t i = 1; i + 1 < f.indices.size(); i++) {
                Face t; t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i + 1)]};
                tris.push_back(t);
            }
        }
        mesh.faces = tris;
    }

    for (int iter = 0; iter < iterations; iter++) {
        auto edgeOpp = buildEdgeOpposites(mesh);
        auto bndVerts = findBoundaryVertices(mesh, edgeOpp);

        int origVertCount = static_cast<int>(mesh.vertices.size());
        std::unordered_map<EdgeKey, int, EdgeHash> edgeMidpoints;

        // Create edge vertices
        auto getEdgeVert = [&](int a, int b) -> int {
            EdgeKey key = makeEdge(a, b);
            auto it2 = edgeMidpoints.find(key);
            if (it2 != edgeMidpoints.end()) return it2->second;

            Vec3 v;
            auto& opps = edgeOpp[key];
            if (opps.size() == 2) {
                // Interior edge: 3/8*(a+b) + 1/8*(opp1+opp2)
                v.x = 3.0f/8.0f * (mesh.vertices[a].x + mesh.vertices[b].x) +
                       1.0f/8.0f * (mesh.vertices[opps[0]].x + mesh.vertices[opps[1]].x);
                v.y = 3.0f/8.0f * (mesh.vertices[a].y + mesh.vertices[b].y) +
                       1.0f/8.0f * (mesh.vertices[opps[0]].y + mesh.vertices[opps[1]].y);
                v.z = 3.0f/8.0f * (mesh.vertices[a].z + mesh.vertices[b].z) +
                       1.0f/8.0f * (mesh.vertices[opps[0]].z + mesh.vertices[opps[1]].z);
            } else {
                // Boundary edge: midpoint
                v.x = 0.5f * (mesh.vertices[a].x + mesh.vertices[b].x);
                v.y = 0.5f * (mesh.vertices[a].y + mesh.vertices[b].y);
                v.z = 0.5f * (mesh.vertices[a].z + mesh.vertices[b].z);
            }

            int idx = static_cast<int>(mesh.vertices.size());
            mesh.vertices.push_back(v);
            edgeMidpoints[key] = idx;
            return idx;
        };

        // Update existing vertex positions
        auto adjMap = buildAdjacency(mesh);
        std::vector<Vec3> newPos(origVertCount);

        for (int i = 0; i < origVertCount; i++) {
            if (bndVerts.count(i)) {
                // Boundary vertex: find boundary neighbors
                std::vector<int> bndNb;
                for (int nb : adjMap[i]) {
                    EdgeKey e = makeEdge(i, nb);
                    if (edgeOpp[e].size() == 1) bndNb.push_back(nb);
                }
                if (bndNb.size() == 2) {
                    newPos[i].x = 0.75f * mesh.vertices[i].x + 0.125f * (mesh.vertices[bndNb[0]].x + mesh.vertices[bndNb[1]].x);
                    newPos[i].y = 0.75f * mesh.vertices[i].y + 0.125f * (mesh.vertices[bndNb[0]].y + mesh.vertices[bndNb[1]].y);
                    newPos[i].z = 0.75f * mesh.vertices[i].z + 0.125f * (mesh.vertices[bndNb[0]].z + mesh.vertices[bndNb[1]].z);
                } else {
                    newPos[i] = mesh.vertices[i];
                }
            } else {
                // Interior vertex
                int n = static_cast<int>(adjMap[i].size());
                if (n < 3) { newPos[i] = mesh.vertices[i]; continue; }
                float beta;
                if (n == 3) {
                    beta = 3.0f / 16.0f;
                } else {
                    float tmp = 3.0f/8.0f + 0.25f * std::cos(2.0f * 3.14159265f / n);
                    beta = (1.0f / n) * (5.0f/8.0f - tmp * tmp);
                }
                float w = 1.0f - n * beta;
                Vec3 sum{0,0,0};
                for (int nb : adjMap[i]) {
                    sum.x += mesh.vertices[nb].x;
                    sum.y += mesh.vertices[nb].y;
                    sum.z += mesh.vertices[nb].z;
                }
                newPos[i].x = w * mesh.vertices[i].x + beta * sum.x;
                newPos[i].y = w * mesh.vertices[i].y + beta * sum.y;
                newPos[i].z = w * mesh.vertices[i].z + beta * sum.z;
            }
        }

        // Apply new positions to original vertices
        for (int i = 0; i < origVertCount; i++) {
            mesh.vertices[i] = newPos[i];
        }

        // Split each triangle into 4
        std::vector<Face> newFaces;
        newFaces.reserve(mesh.faces.size() * 4);
        for (const auto& f : mesh.faces) {
            int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
            int m01 = getEdgeVert(v0, v1);
            int m12 = getEdgeVert(v1, v2);
            int m20 = getEdgeVert(v2, v0);
            newFaces.push_back({{v0, m01, m20}});
            newFaces.push_back({{v1, m12, m01}});
            newFaces.push_back({{v2, m20, m12}});
            newFaces.push_back({{m01, m12, m20}});
        }
        mesh.faces = newFaces;
    }
    mesh.triangulate();
}

// ---- 2. Butterfly Subdivision (interpolating) ----
void filterButterflySubdivision(int handle, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Ensure triangulated
    {
        std::vector<Face> tris;
        for (auto& f : mesh.faces) {
            for (size_t i = 1; i + 1 < f.indices.size(); i++) {
                Face t; t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i + 1)]};
                tris.push_back(t);
            }
        }
        mesh.faces = tris;
    }

    for (int iter = 0; iter < iterations; iter++) {
        auto edgeOpp = buildEdgeOpposites(mesh);
        std::unordered_map<EdgeKey, int, EdgeHash> edgeMidpoints;

        auto getEdgeVert = [&](int a, int b) -> int {
            EdgeKey key = makeEdge(a, b);
            auto it2 = edgeMidpoints.find(key);
            if (it2 != edgeMidpoints.end()) return it2->second;

            Vec3 v;
            auto& opps = edgeOpp[key];
            if (opps.size() == 2) {
                // Butterfly: 1/2*(a+b) + 1/8*(opp1+opp2)
                v.x = 0.5f * (mesh.vertices[a].x + mesh.vertices[b].x) +
                       0.125f * (mesh.vertices[opps[0]].x + mesh.vertices[opps[1]].x);
                v.y = 0.5f * (mesh.vertices[a].y + mesh.vertices[b].y) +
                       0.125f * (mesh.vertices[opps[0]].y + mesh.vertices[opps[1]].y);
                v.z = 0.5f * (mesh.vertices[a].z + mesh.vertices[b].z) +
                       0.125f * (mesh.vertices[opps[0]].z + mesh.vertices[opps[1]].z);
            } else {
                // Boundary: midpoint
                v.x = 0.5f * (mesh.vertices[a].x + mesh.vertices[b].x);
                v.y = 0.5f * (mesh.vertices[a].y + mesh.vertices[b].y);
                v.z = 0.5f * (mesh.vertices[a].z + mesh.vertices[b].z);
            }
            int idx = static_cast<int>(mesh.vertices.size());
            mesh.vertices.push_back(v);
            edgeMidpoints[key] = idx;
            return idx;
        };

        // Butterfly is interpolating: existing vertices do NOT move

        std::vector<Face> newFaces;
        newFaces.reserve(mesh.faces.size() * 4);
        for (const auto& f : mesh.faces) {
            int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
            int m01 = getEdgeVert(v0, v1);
            int m12 = getEdgeVert(v1, v2);
            int m20 = getEdgeVert(v2, v0);
            newFaces.push_back({{v0, m01, m20}});
            newFaces.push_back({{v1, m12, m01}});
            newFaces.push_back({{v2, m20, m12}});
            newFaces.push_back({{m01, m12, m20}});
        }
        mesh.faces = newFaces;
    }
    mesh.triangulate();
}

// ---- Helper: random float [0, 1) ----
static float randFloat() {
    return static_cast<float>(rand()) / (static_cast<float>(RAND_MAX) + 1.0f);
}

// triangleArea already defined above (reused here)

// ---- 3. Montecarlo Sampling ----
int filterMontecarloSampling(int handle, int sampleNum) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return -1;
    auto& srcMesh = it->second;

    // Build CDF of triangle areas
    std::vector<float> cdf;
    cdf.reserve(srcMesh.faces.size());
    float totalArea = 0;
    for (const auto& f : srcMesh.faces) {
        if (f.indices.size() < 3) { cdf.push_back(totalArea); continue; }
        float a = triangleArea(srcMesh.vertices[f.indices[0]], srcMesh.vertices[f.indices[1]], srcMesh.vertices[f.indices[2]]);
        totalArea += a;
        cdf.push_back(totalArea);
    }
    if (totalArea <= 0) return -1;

    // Normalize CDF
    for (auto& v : cdf) v /= totalArea;

    Mesh result;
    result.vertices.reserve(sampleNum);
    srand(42);

    for (int s = 0; s < sampleNum; s++) {
        float r = randFloat();
        int fi = static_cast<int>(std::lower_bound(cdf.begin(), cdf.end(), r) - cdf.begin());
        if (fi >= static_cast<int>(srcMesh.faces.size())) fi = static_cast<int>(srcMesh.faces.size()) - 1;
        const auto& face = srcMesh.faces[fi];
        if (face.indices.size() < 3) continue;

        float u = randFloat(), v = randFloat();
        if (u + v > 1.0f) { u = 1.0f - u; v = 1.0f - v; }
        float w = 1.0f - u - v;

        const auto& v0 = srcMesh.vertices[face.indices[0]];
        const auto& v1 = srcMesh.vertices[face.indices[1]];
        const auto& v2 = srcMesh.vertices[face.indices[2]];

        Vec3 pt;
        pt.x = w * v0.x + u * v1.x + v * v2.x;
        pt.y = w * v0.y + u * v1.y + v * v2.y;
        pt.z = w * v0.z + u * v1.z + v * v2.z;
        result.vertices.push_back(pt);
    }

    // Point cloud: no faces
    int newHandle = nextHandle++;
    result.triangulate();
    meshStore[newHandle] = std::move(result);
    return newHandle;
}

// ---- 4. Poisson-disk Sampling (dart throwing) ----
int filterPoissonDiskSampling(int handle, int sampleNum, float radius) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return -1;
    auto& srcMesh = it->second;

    // Compute total area
    float totalArea = 0;
    for (const auto& f : srcMesh.faces) {
        if (f.indices.size() < 3) continue;
        totalArea += triangleArea(srcMesh.vertices[f.indices[0]], srcMesh.vertices[f.indices[1]], srcMesh.vertices[f.indices[2]]);
    }
    if (totalArea <= 0) return -1;

    // Auto-compute radius from sample count
    if (radius <= 0) {
        radius = std::sqrt(totalArea / (2.0f * std::sqrt(3.0f) * sampleNum));
    }
    float r2 = radius * radius;

    // Build CDF
    std::vector<float> cdf;
    float cumArea = 0;
    for (const auto& f : srcMesh.faces) {
        if (f.indices.size() < 3) { cdf.push_back(cumArea); continue; }
        cumArea += triangleArea(srcMesh.vertices[f.indices[0]], srcMesh.vertices[f.indices[1]], srcMesh.vertices[f.indices[2]]);
        cdf.push_back(cumArea);
    }
    for (auto& v : cdf) v /= totalArea;

    Mesh result;
    result.vertices.reserve(sampleNum);
    srand(42);

    int maxAttempts = sampleNum * 30;
    for (int attempt = 0; attempt < maxAttempts && static_cast<int>(result.vertices.size()) < sampleNum; attempt++) {
        float r = randFloat();
        int fi = static_cast<int>(std::lower_bound(cdf.begin(), cdf.end(), r) - cdf.begin());
        if (fi >= static_cast<int>(srcMesh.faces.size())) fi = static_cast<int>(srcMesh.faces.size()) - 1;
        const auto& face = srcMesh.faces[fi];
        if (face.indices.size() < 3) continue;

        float u = randFloat(), v = randFloat();
        if (u + v > 1.0f) { u = 1.0f - u; v = 1.0f - v; }
        float w = 1.0f - u - v;

        const auto& v0 = srcMesh.vertices[face.indices[0]];
        const auto& v1 = srcMesh.vertices[face.indices[1]];
        const auto& v2 = srcMesh.vertices[face.indices[2]];

        Vec3 pt;
        pt.x = w * v0.x + u * v1.x + v * v2.x;
        pt.y = w * v0.y + u * v1.y + v * v2.y;
        pt.z = w * v0.z + u * v1.z + v * v2.z;

        // Check against existing points (brute force for simplicity)
        bool tooClose = false;
        for (const auto& p : result.vertices) {
            float dx = pt.x - p.x, dy = pt.y - p.y, dz = pt.z - p.z;
            if (dx*dx + dy*dy + dz*dz < r2) { tooClose = true; break; }
        }
        if (!tooClose) result.vertices.push_back(pt);
    }

    int newHandle = nextHandle++;
    result.triangulate();
    meshStore[newHandle] = std::move(result);
    return newHandle;
}

// ---- 5. Hausdorff Distance ----
static float pointToTriangleDist(const Vec3& p, const Vec3& a, const Vec3& b, const Vec3& c) {
    // Project p onto triangle plane and check if inside; otherwise find closest edge/vertex
    float abx=b.x-a.x, aby=b.y-a.y, abz=b.z-a.z;
    float acx=c.x-a.x, acy=c.y-a.y, acz=c.z-a.z;
    float apx=p.x-a.x, apy=p.y-a.y, apz=p.z-a.z;

    float d00 = abx*abx + aby*aby + abz*abz;
    float d01 = abx*acx + aby*acy + abz*acz;
    float d11 = acx*acx + acy*acy + acz*acz;
    float d20 = apx*abx + apy*aby + apz*abz;
    float d21 = apx*acx + apy*acy + apz*acz;

    float denom = d00*d11 - d01*d01;
    if (std::abs(denom) < 1e-12f) {
        // Degenerate triangle, return distance to vertex a
        return std::sqrt(apx*apx + apy*apy + apz*apz);
    }

    float v = (d11*d20 - d01*d21) / denom;
    float w = (d00*d21 - d01*d20) / denom;
    float u = 1.0f - v - w;

    // Clamp to triangle
    if (u >= 0 && v >= 0 && w >= 0) {
        Vec3 proj;
        proj.x = u*a.x + v*b.x + w*c.x;
        proj.y = u*a.y + v*b.y + w*c.y;
        proj.z = u*a.z + v*b.z + w*c.z;
        float dx = p.x-proj.x, dy = p.y-proj.y, dz = p.z-proj.z;
        return std::sqrt(dx*dx + dy*dy + dz*dz);
    }

    // Find closest point on edges/vertices
    auto segDist = [&](const Vec3& s0, const Vec3& s1) -> float {
        float sx = s1.x-s0.x, sy = s1.y-s0.y, sz = s1.z-s0.z;
        float t = ((p.x-s0.x)*sx + (p.y-s0.y)*sy + (p.z-s0.z)*sz) / (sx*sx + sy*sy + sz*sz + 1e-12f);
        t = std::max(0.0f, std::min(1.0f, t));
        float cx = s0.x + t*sx - p.x, cy = s0.y + t*sy - p.y, cz = s0.z + t*sz - p.z;
        return std::sqrt(cx*cx + cy*cy + cz*cz);
    };

    float d = segDist(a, b);
    d = std::min(d, segDist(b, c));
    d = std::min(d, segDist(c, a));
    return d;
}

std::string filterHausdorffDistance(int srcHandle, int tgtHandle, int sampleNum) {
    auto it1 = meshStore.find(srcHandle);
    auto it2 = meshStore.find(tgtHandle);
    if (it1 == meshStore.end() || it2 == meshStore.end()) return "{}";

    auto& src = it1->second;
    auto& tgt = it2->second;

    // Sample points on source mesh
    float totalArea = 0;
    std::vector<float> cdf;
    for (const auto& f : src.faces) {
        if (f.indices.size() < 3) { cdf.push_back(totalArea); continue; }
        totalArea += triangleArea(src.vertices[f.indices[0]], src.vertices[f.indices[1]], src.vertices[f.indices[2]]);
        cdf.push_back(totalArea);
    }
    if (totalArea <= 0) return "{}";
    for (auto& v : cdf) v /= totalArea;

    srand(42);
    float maxDist = 0, sumDist = 0, sumDist2 = 0;

    // Store distances in source quality
    src.vertexQuality.resize(src.vertices.size(), 0.0f);
    src.hasQuality = true;

    for (int s = 0; s < sampleNum; s++) {
        float r = randFloat();
        int fi = static_cast<int>(std::lower_bound(cdf.begin(), cdf.end(), r) - cdf.begin());
        if (fi >= static_cast<int>(src.faces.size())) fi = static_cast<int>(src.faces.size()) - 1;
        const auto& face = src.faces[fi];
        if (face.indices.size() < 3) continue;

        float u = randFloat(), vv = randFloat();
        if (u + vv > 1.0f) { u = 1.0f - u; vv = 1.0f - vv; }
        float ww = 1.0f - u - vv;

        Vec3 pt;
        pt.x = ww*src.vertices[face.indices[0]].x + u*src.vertices[face.indices[1]].x + vv*src.vertices[face.indices[2]].x;
        pt.y = ww*src.vertices[face.indices[0]].y + u*src.vertices[face.indices[1]].y + vv*src.vertices[face.indices[2]].y;
        pt.z = ww*src.vertices[face.indices[0]].z + u*src.vertices[face.indices[1]].z + vv*src.vertices[face.indices[2]].z;

        // Find min distance to target
        float minD = 1e30f;
        for (const auto& tf : tgt.faces) {
            if (tf.indices.size() < 3) continue;
            float d = pointToTriangleDist(pt, tgt.vertices[tf.indices[0]], tgt.vertices[tf.indices[1]], tgt.vertices[tf.indices[2]]);
            if (d < minD) minD = d;
        }

        maxDist = std::max(maxDist, minD);
        sumDist += minD;
        sumDist2 += minD * minD;
    }

    float mean = sumDist / sampleNum;
    float rms = std::sqrt(sumDist2 / sampleNum);

    std::ostringstream oss;
    oss << "{\"hausdorff\":" << maxDist << ",\"mean\":" << mean << ",\"rms\":" << rms << "}";
    return oss.str();
}

// ---- 6. Discrete Curvatures: Principal, Shape Index ----
void computePrincipalCurvatureK1(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    // Need Gaussian and Mean curvature first
    // Gaussian curvature
    std::vector<float> gaussK(mesh.vertices.size(), 0.0f);
    std::vector<float> meanH(mesh.vertices.size(), 0.0f);
    std::vector<float> vertArea(mesh.vertices.size(), 0.0f);

    // Compute angle defect for Gaussian, Laplacian for mean
    for (const auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        for (size_t j = 0; j < f.indices.size(); j++) {
            int i0 = f.indices[j];
            int i1 = f.indices[(j+1) % f.indices.size()];
            int i2 = f.indices[(j+2) % f.indices.size()];

            float e1x = mesh.vertices[i1].x - mesh.vertices[i0].x;
            float e1y = mesh.vertices[i1].y - mesh.vertices[i0].y;
            float e1z = mesh.vertices[i1].z - mesh.vertices[i0].z;
            float e2x = mesh.vertices[i2].x - mesh.vertices[i0].x;
            float e2y = mesh.vertices[i2].y - mesh.vertices[i0].y;
            float e2z = mesh.vertices[i2].z - mesh.vertices[i0].z;

            float dot = e1x*e2x + e1y*e2y + e1z*e2z;
            float len1 = std::sqrt(e1x*e1x + e1y*e1y + e1z*e1z);
            float len2 = std::sqrt(e2x*e2x + e2y*e2y + e2z*e2z);
            float cosA = dot / (len1 * len2 + 1e-12f);
            cosA = std::max(-1.0f, std::min(1.0f, cosA));
            float angle = std::acos(cosA);
            gaussK[i0] += angle;

            float area = triangleArea(mesh.vertices[i0], mesh.vertices[i1], mesh.vertices[i2]);
            vertArea[i0] += area / 3.0f;
        }
    }

    // Compute mean curvature via cotangent Laplacian
    auto adj = buildAdjacency(mesh);
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (vertArea[i] > 1e-12f) {
            gaussK[i] = (2.0f * 3.14159265f - gaussK[i]) / vertArea[i];
        }
        // Simple mean curvature from Laplacian
        if (!adj[i].empty()) {
            Vec3 lap{0,0,0};
            for (int nb : adj[i]) {
                lap.x += mesh.vertices[nb].x - mesh.vertices[i].x;
                lap.y += mesh.vertices[nb].y - mesh.vertices[i].y;
                lap.z += mesh.vertices[nb].z - mesh.vertices[i].z;
            }
            float invN = 1.0f / adj[i].size();
            lap.x *= invN; lap.y *= invN; lap.z *= invN;
            meanH[i] = 0.5f * std::sqrt(lap.x*lap.x + lap.y*lap.y + lap.z*lap.z);
        }
    }

    // k1 = H + sqrt(H^2 - K)
    mesh.vertexQuality.resize(mesh.vertices.size());
    mesh.hasQuality = true;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        float disc = meanH[i] * meanH[i] - gaussK[i];
        if (disc < 0) disc = 0;
        mesh.vertexQuality[i] = meanH[i] + std::sqrt(disc);
    }
}

void computePrincipalCurvatureK2(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Reuse same computation as k1 but k2 = H - sqrt(H^2 - K)
    std::vector<float> gaussK(mesh.vertices.size(), 0.0f);
    std::vector<float> meanH(mesh.vertices.size(), 0.0f);
    std::vector<float> vertArea(mesh.vertices.size(), 0.0f);

    for (const auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        for (size_t j = 0; j < f.indices.size(); j++) {
            int i0 = f.indices[j];
            int i1 = f.indices[(j+1) % f.indices.size()];
            int i2 = f.indices[(j+2) % f.indices.size()];
            float e1x = mesh.vertices[i1].x - mesh.vertices[i0].x;
            float e1y = mesh.vertices[i1].y - mesh.vertices[i0].y;
            float e1z = mesh.vertices[i1].z - mesh.vertices[i0].z;
            float e2x = mesh.vertices[i2].x - mesh.vertices[i0].x;
            float e2y = mesh.vertices[i2].y - mesh.vertices[i0].y;
            float e2z = mesh.vertices[i2].z - mesh.vertices[i0].z;
            float dot = e1x*e2x + e1y*e2y + e1z*e2z;
            float len1 = std::sqrt(e1x*e1x + e1y*e1y + e1z*e1z);
            float len2 = std::sqrt(e2x*e2x + e2y*e2y + e2z*e2z);
            float cosA = std::max(-1.0f, std::min(1.0f, dot / (len1*len2 + 1e-12f)));
            gaussK[i0] += std::acos(cosA);
            vertArea[i0] += triangleArea(mesh.vertices[i0], mesh.vertices[i1], mesh.vertices[i2]) / 3.0f;
        }
    }
    auto adj = buildAdjacency(mesh);
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (vertArea[i] > 1e-12f) gaussK[i] = (2.0f * 3.14159265f - gaussK[i]) / vertArea[i];
        if (!adj[i].empty()) {
            Vec3 lap{0,0,0};
            for (int nb : adj[i]) {
                lap.x += mesh.vertices[nb].x - mesh.vertices[i].x;
                lap.y += mesh.vertices[nb].y - mesh.vertices[i].y;
                lap.z += mesh.vertices[nb].z - mesh.vertices[i].z;
            }
            float invN = 1.0f / adj[i].size();
            meanH[i] = 0.5f * std::sqrt((lap.x*invN)*(lap.x*invN) + (lap.y*invN)*(lap.y*invN) + (lap.z*invN)*(lap.z*invN));
        }
    }

    mesh.vertexQuality.resize(mesh.vertices.size());
    mesh.hasQuality = true;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        float disc = meanH[i]*meanH[i] - gaussK[i];
        if (disc < 0) disc = 0;
        mesh.vertexQuality[i] = meanH[i] - std::sqrt(disc);
    }
}

void computeShapeIndex(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Compute Gaussian and Mean curvature
    std::vector<float> gaussK(mesh.vertices.size(), 0.0f);
    std::vector<float> meanH(mesh.vertices.size(), 0.0f);
    std::vector<float> vertArea(mesh.vertices.size(), 0.0f);

    for (const auto& f : mesh.faces) {
        if (f.indices.size() < 3) continue;
        for (size_t j = 0; j < f.indices.size(); j++) {
            int i0 = f.indices[j], i1 = f.indices[(j+1)%f.indices.size()], i2 = f.indices[(j+2)%f.indices.size()];
            float e1x = mesh.vertices[i1].x-mesh.vertices[i0].x, e1y = mesh.vertices[i1].y-mesh.vertices[i0].y, e1z = mesh.vertices[i1].z-mesh.vertices[i0].z;
            float e2x = mesh.vertices[i2].x-mesh.vertices[i0].x, e2y = mesh.vertices[i2].y-mesh.vertices[i0].y, e2z = mesh.vertices[i2].z-mesh.vertices[i0].z;
            float cosA = std::max(-1.0f, std::min(1.0f, (e1x*e2x+e1y*e2y+e1z*e2z) / (std::sqrt(e1x*e1x+e1y*e1y+e1z*e1z)*std::sqrt(e2x*e2x+e2y*e2y+e2z*e2z)+1e-12f)));
            gaussK[i0] += std::acos(cosA);
            vertArea[i0] += triangleArea(mesh.vertices[i0], mesh.vertices[i1], mesh.vertices[i2]) / 3.0f;
        }
    }
    auto adj = buildAdjacency(mesh);
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (vertArea[i] > 1e-12f) gaussK[i] = (2.0f*3.14159265f - gaussK[i]) / vertArea[i];
        if (!adj[i].empty()) {
            Vec3 lap{0,0,0};
            for (int nb : adj[i]) { lap.x += mesh.vertices[nb].x-mesh.vertices[i].x; lap.y += mesh.vertices[nb].y-mesh.vertices[i].y; lap.z += mesh.vertices[nb].z-mesh.vertices[i].z; }
            float invN = 1.0f / adj[i].size();
            meanH[i] = 0.5f * std::sqrt((lap.x*invN)*(lap.x*invN)+(lap.y*invN)*(lap.y*invN)+(lap.z*invN)*(lap.z*invN));
        }
    }

    mesh.vertexQuality.resize(mesh.vertices.size());
    mesh.hasQuality = true;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        float disc = meanH[i]*meanH[i] - gaussK[i];
        if (disc < 0) disc = 0;
        float k1 = meanH[i] + std::sqrt(disc);
        float k2 = meanH[i] - std::sqrt(disc);
        float denom = k1 - k2;
        if (std::abs(denom) < 1e-10f) {
            mesh.vertexQuality[i] = 0;
        } else {
            mesh.vertexQuality[i] = (2.0f / 3.14159265f) * std::atan2(k1 + k2, k1 - k2);
        }
    }
}

// ---- 7. Mesh Boolean (SDF-based) ----
int filterMeshBoolean(int handleA, int handleB, int operation, int resolution) {
    auto itA = meshStore.find(handleA);
    auto itB = meshStore.find(handleB);
    if (itA == meshStore.end() || itB == meshStore.end()) return -1;
    auto& meshA = itA->second;
    auto& meshB = itB->second;

    // Compute combined bounding box
    float bboxMin[3] = {1e30f, 1e30f, 1e30f};
    float bboxMax[3] = {-1e30f, -1e30f, -1e30f};
    for (auto* m : {&meshA, &meshB}) {
        for (const auto& v : m->vertices) {
            bboxMin[0] = std::min(bboxMin[0], v.x); bboxMin[1] = std::min(bboxMin[1], v.y); bboxMin[2] = std::min(bboxMin[2], v.z);
            bboxMax[0] = std::max(bboxMax[0], v.x); bboxMax[1] = std::max(bboxMax[1], v.y); bboxMax[2] = std::max(bboxMax[2], v.z);
        }
    }
    float pad = 0.05f;
    for (int i = 0; i < 3; i++) {
        float ext = (bboxMax[i] - bboxMin[i]) * pad;
        bboxMin[i] -= ext; bboxMax[i] += ext;
    }

    int res = resolution;
    float cellSize[3];
    for (int i = 0; i < 3; i++) cellSize[i] = (bboxMax[i] - bboxMin[i]) / res;

    // Compute unsigned distance field for both meshes, then determine sign
    // Simplified: for each grid point, find nearest face distance
    auto computeUDF = [&](const Mesh& mesh, std::vector<float>& field) {
        field.resize((res+1)*(res+1)*(res+1), 1e30f);
        for (int iz = 0; iz <= res; iz++) {
            float z = bboxMin[2] + iz * cellSize[2];
            for (int iy = 0; iy <= res; iy++) {
                float y = bboxMin[1] + iy * cellSize[1];
                for (int ix = 0; ix <= res; ix++) {
                    float x = bboxMin[0] + ix * cellSize[0];
                    Vec3 pt{x, y, z};
                    float minD = 1e30f;
                    for (const auto& f : mesh.faces) {
                        if (f.indices.size() < 3) continue;
                        float d = pointToTriangleDist(pt, mesh.vertices[f.indices[0]], mesh.vertices[f.indices[1]], mesh.vertices[f.indices[2]]);
                        if (d < minD) minD = d;
                    }
                    int idx = iz*(res+1)*(res+1) + iy*(res+1) + ix;
                    field[idx] = minD;
                }
            }
        }
        // Sign determination: ray casting along X axis
        for (int iz = 0; iz <= res; iz++) {
            float z = bboxMin[2] + iz * cellSize[2];
            for (int iy = 0; iy <= res; iy++) {
                float y = bboxMin[1] + iy * cellSize[1];
                // Count intersections from left
                std::vector<float> hits;
                for (const auto& f : mesh.faces) {
                    if (f.indices.size() < 3) continue;
                    const auto& tv0 = mesh.vertices[f.indices[0]];
                    const auto& tv1 = mesh.vertices[f.indices[1]];
                    const auto& tv2 = mesh.vertices[f.indices[2]];
                    // Ray: origin (bboxMin[0]-1, y, z), dir (1,0,0)
                    float e1y2 = tv1.y-tv0.y, e1z2 = tv1.z-tv0.z;
                    float e2y2 = tv2.y-tv0.y, e2z2 = tv2.z-tv0.z;
                    float py = y-tv0.y, pz = z-tv0.z;
                    float det = e1y2*e2z2 - e1z2*e2y2;
                    if (std::abs(det) < 1e-12f) continue;
                    float invDet = 1.0f / det;
                    float bu = (py*e2z2 - pz*e2y2) * invDet;
                    float bv = (e1y2*pz - e1z2*py) * invDet;
                    if (bu < 0 || bv < 0 || bu+bv > 1) continue;
                    float hitX = (1-bu-bv)*tv0.x + bu*tv1.x + bv*tv2.x;
                    hits.push_back(hitX);
                }
                std::sort(hits.begin(), hits.end());
                for (int ix = 0; ix <= res; ix++) {
                    float x = bboxMin[0] + ix * cellSize[0];
                    int crossings = 0;
                    for (float h : hits) { if (h < x) crossings++; else break; }
                    if (crossings % 2 == 1) {
                        int idx = iz*(res+1)*(res+1) + iy*(res+1) + ix;
                        field[idx] = -field[idx]; // inside
                    }
                }
            }
        }
    };

    std::vector<float> sdfA, sdfB;
    computeUDF(meshA, sdfA);
    computeUDF(meshB, sdfB);

    // Combine SDFs based on operation
    std::vector<float> sdfResult((res+1)*(res+1)*(res+1));
    for (size_t i = 0; i < sdfResult.size(); i++) {
        switch (operation) {
            case 0: sdfResult[i] = std::min(sdfA[i], sdfB[i]); break; // Union
            case 1: sdfResult[i] = std::max(sdfA[i], sdfB[i]); break; // Intersection
            case 2: sdfResult[i] = std::max(sdfA[i], -sdfB[i]); break; // Difference
            default: sdfResult[i] = std::min(sdfA[i], sdfB[i]); break;
        }
    }

    // Marching cubes on the result SDF (reuse existing MC infrastructure)
    // For simplicity, create a point cloud from negative SDF regions and use existing MC
    // Actually, let's implement a simple inline MC here
    // ... but we already have MC. Let's create a temporary mesh with the SDF as a grid
    // and call marchingCubesReconstruction on it.
    // Simpler approach: just store SDF in a temporary mesh and run MC

    // Inline simple marching cubes on the SDF
    Mesh resultMesh;
    // Use the sdfResult field directly with marching cubes
    // Each cube has 8 corners. If corner < 0, it's inside.
    for (int iz = 0; iz < res; iz++) {
        for (int iy = 0; iy < res; iy++) {
            for (int ix = 0; ix < res; ix++) {
                float corners[8];
                int ci[8][3] = {{0,0,0},{1,0,0},{1,1,0},{0,1,0},{0,0,1},{1,0,1},{1,1,1},{0,1,1}};
                for (int c = 0; c < 8; c++) {
                    int gx = ix+ci[c][0], gy = iy+ci[c][1], gz = iz+ci[c][2];
                    corners[c] = sdfResult[gz*(res+1)*(res+1) + gy*(res+1) + gx];
                }

                // Determine cube index
                int cubeIdx = 0;
                for (int c = 0; c < 8; c++) {
                    if (corners[c] < 0) cubeIdx |= (1 << c);
                }
                if (cubeIdx == 0 || cubeIdx == 255) continue;

                // Simple surface extraction: add triangles at cell center for each edge crossing
                // This is a simplified approach - proper MC would use lookup tables
                Vec3 center;
                center.x = bboxMin[0] + (ix + 0.5f) * cellSize[0];
                center.y = bboxMin[1] + (iy + 0.5f) * cellSize[1];
                center.z = bboxMin[2] + (iz + 0.5f) * cellSize[2];

                // Find edge crossings and average them for a vertex
                std::vector<Vec3> crossings;
                int edges[12][2] = {{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}};
                for (int e = 0; e < 12; e++) {
                    int c0 = edges[e][0], c1 = edges[e][1];
                    if ((corners[c0] < 0) != (corners[c1] < 0)) {
                        float t = corners[c0] / (corners[c0] - corners[c1]);
                        Vec3 p0, p1;
                        p0.x = bboxMin[0] + (ix+ci[c0][0]) * cellSize[0];
                        p0.y = bboxMin[1] + (iy+ci[c0][1]) * cellSize[1];
                        p0.z = bboxMin[2] + (iz+ci[c0][2]) * cellSize[2];
                        p1.x = bboxMin[0] + (ix+ci[c1][0]) * cellSize[0];
                        p1.y = bboxMin[1] + (iy+ci[c1][1]) * cellSize[1];
                        p1.z = bboxMin[2] + (iz+ci[c1][2]) * cellSize[2];
                        crossings.push_back({p0.x + t*(p1.x-p0.x), p0.y + t*(p1.y-p0.y), p0.z + t*(p1.z-p0.z)});
                    }
                }

                if (crossings.size() >= 3) {
                    // Create a fan of triangles from the crossings
                    int baseIdx = static_cast<int>(resultMesh.vertices.size());
                    for (const auto& v : crossings) resultMesh.vertices.push_back(v);
                    for (size_t i = 1; i + 1 < crossings.size(); i++) {
                        Face f; f.indices = {baseIdx, baseIdx + static_cast<int>(i), baseIdx + static_cast<int>(i+1)};
                        resultMesh.faces.push_back(f);
                    }
                }
            }
        }
    }

    int newHandle = nextHandle++;
    resultMesh.triangulate();
    meshStore[newHandle] = std::move(resultMesh);
    return newHandle;
}

// ---- 8. Isotropic Explicit Remeshing ----
void filterIsotropicRemeshing(int handle, float targetEdgeLength, int iterations) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;

    // Ensure triangulated
    {
        std::vector<Face> tris;
        for (auto& f : mesh.faces) {
            for (size_t i = 1; i + 1 < f.indices.size(); i++) {
                Face t; t.indices = {f.indices[0], f.indices[static_cast<int>(i)], f.indices[static_cast<int>(i + 1)]};
                tris.push_back(t);
            }
        }
        mesh.faces = tris;
    }

    // Compute average edge length if not specified
    if (targetEdgeLength <= 0) {
        float sumLen = 0; int edgeCount = 0;
        for (const auto& f : mesh.faces) {
            for (int e = 0; e < 3; e++) {
                int a = f.indices[e], b = f.indices[(e+1)%3];
                float dx = mesh.vertices[b].x-mesh.vertices[a].x;
                float dy = mesh.vertices[b].y-mesh.vertices[a].y;
                float dz = mesh.vertices[b].z-mesh.vertices[a].z;
                sumLen += std::sqrt(dx*dx+dy*dy+dz*dz);
                edgeCount++;
            }
        }
        targetEdgeLength = sumLen / (edgeCount + 1e-12f);
    }

    float highLen = targetEdgeLength * 4.0f / 3.0f;
    float lowLen = targetEdgeLength * 4.0f / 5.0f;

    for (int iter = 0; iter < iterations; iter++) {
        // Step 1: Split long edges
        bool changed = true;
        int maxSplitRounds = 3;
        for (int round = 0; round < maxSplitRounds && changed; round++) {
            changed = false;
            std::vector<Face> newFaces;
            for (const auto& f : mesh.faces) {
                int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
                // Find longest edge
                float d01 = 0, d12 = 0, d20 = 0;
                {
                    float dx,dy,dz;
                    dx=mesh.vertices[v1].x-mesh.vertices[v0].x; dy=mesh.vertices[v1].y-mesh.vertices[v0].y; dz=mesh.vertices[v1].z-mesh.vertices[v0].z;
                    d01=std::sqrt(dx*dx+dy*dy+dz*dz);
                    dx=mesh.vertices[v2].x-mesh.vertices[v1].x; dy=mesh.vertices[v2].y-mesh.vertices[v1].y; dz=mesh.vertices[v2].z-mesh.vertices[v1].z;
                    d12=std::sqrt(dx*dx+dy*dy+dz*dz);
                    dx=mesh.vertices[v0].x-mesh.vertices[v2].x; dy=mesh.vertices[v0].y-mesh.vertices[v2].y; dz=mesh.vertices[v0].z-mesh.vertices[v2].z;
                    d20=std::sqrt(dx*dx+dy*dy+dz*dz);
                }
                float maxD = std::max({d01, d12, d20});
                if (maxD > highLen) {
                    changed = true;
                    int a, b, c;
                    if (maxD == d01) { a=v0; b=v1; c=v2; }
                    else if (maxD == d12) { a=v1; b=v2; c=v0; }
                    else { a=v2; b=v0; c=v1; }
                    // Split edge a-b
                    Vec3 mid;
                    mid.x = 0.5f*(mesh.vertices[a].x+mesh.vertices[b].x);
                    mid.y = 0.5f*(mesh.vertices[a].y+mesh.vertices[b].y);
                    mid.z = 0.5f*(mesh.vertices[a].z+mesh.vertices[b].z);
                    int m = static_cast<int>(mesh.vertices.size());
                    mesh.vertices.push_back(mid);
                    newFaces.push_back({{a, m, c}});
                    newFaces.push_back({{m, b, c}});
                } else {
                    newFaces.push_back(f);
                }
            }
            mesh.faces = newFaces;
        }

        // Step 2: Collapse short edges
        for (int round = 0; round < 2; round++) {
            std::unordered_set<int> removed;
            for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
                auto& f = mesh.faces[fi];
                for (int e = 0; e < 3; e++) {
                    int a = f.indices[e], b = f.indices[(e+1)%3];
                    if (removed.count(a) || removed.count(b)) continue;
                    float dx=mesh.vertices[b].x-mesh.vertices[a].x, dy=mesh.vertices[b].y-mesh.vertices[a].y, dz=mesh.vertices[b].z-mesh.vertices[a].z;
                    float len = std::sqrt(dx*dx+dy*dy+dz*dz);
                    if (len < lowLen) {
                        // Collapse b into a (midpoint)
                        mesh.vertices[a].x = 0.5f*(mesh.vertices[a].x+mesh.vertices[b].x);
                        mesh.vertices[a].y = 0.5f*(mesh.vertices[a].y+mesh.vertices[b].y);
                        mesh.vertices[a].z = 0.5f*(mesh.vertices[a].z+mesh.vertices[b].z);
                        for (auto& ff : mesh.faces) {
                            for (auto& idx : ff.indices) {
                                if (idx == b) idx = a;
                            }
                        }
                        removed.insert(b);
                    }
                }
            }
            // Remove degenerate faces
            mesh.faces.erase(std::remove_if(mesh.faces.begin(), mesh.faces.end(), [](const Face& f) {
                return f.indices[0]==f.indices[1] || f.indices[1]==f.indices[2] || f.indices[0]==f.indices[2];
            }), mesh.faces.end());
        }

        // Step 3: Tangential smoothing
        auto adj = buildAdjacency(mesh);
        mesh.computeNormals();
        size_t nv = mesh.vertices.size();
        std::vector<Vec3> smoothed(nv);
        for (size_t i = 0; i < nv; i++) {
            if (adj[i].empty()) { smoothed[i] = mesh.vertices[i]; continue; }
            Vec3 centroid{0,0,0};
            for (int nb : adj[i]) {
                centroid.x += mesh.vertices[nb].x; centroid.y += mesh.vertices[nb].y; centroid.z += mesh.vertices[nb].z;
            }
            float invN = 1.0f / adj[i].size();
            centroid.x *= invN; centroid.y *= invN; centroid.z *= invN;

            // Move towards centroid but project onto tangent plane
            float dx = centroid.x - mesh.vertices[i].x;
            float dy = centroid.y - mesh.vertices[i].y;
            float dz = centroid.z - mesh.vertices[i].z;

            // Get normal from normalBuffer
            float nx = 0, ny = 0, nz = 1;
            if (i*3+2 < mesh.normalBuffer.size()) {
                nx = mesh.normalBuffer[i*3]; ny = mesh.normalBuffer[i*3+1]; nz = mesh.normalBuffer[i*3+2];
            }
            float dot = dx*nx + dy*ny + dz*nz;
            dx -= dot*nx; dy -= dot*ny; dz -= dot*nz;

            smoothed[i].x = mesh.vertices[i].x + 0.5f * dx;
            smoothed[i].y = mesh.vertices[i].y + 0.5f * dy;
            smoothed[i].z = mesh.vertices[i].z + 0.5f * dz;
        }
        mesh.vertices = smoothed;
    }

    // Clean up unreferenced vertices
    std::vector<bool> used(mesh.vertices.size(), false);
    for (const auto& f : mesh.faces) for (int idx : f.indices) if (idx >= 0 && idx < (int)used.size()) used[idx] = true;
    std::vector<int> remap(mesh.vertices.size(), -1);
    std::vector<Vec3> newVerts;
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        if (used[i]) { remap[i] = static_cast<int>(newVerts.size()); newVerts.push_back(mesh.vertices[i]); }
    }
    mesh.vertices = newVerts;
    for (auto& f : mesh.faces) for (auto& idx : f.indices) idx = remap[idx];

    mesh.triangulate();
}

// ---- 9. Create Primitives ----
int createBox(float sx, float sy, float sz) {
    Mesh mesh;
    float hx = sx*0.5f, hy = sy*0.5f, hz = sz*0.5f;
    mesh.vertices = {
        {-hx,-hy,-hz},{hx,-hy,-hz},{hx,hy,-hz},{-hx,hy,-hz},
        {-hx,-hy,hz},{hx,-hy,hz},{hx,hy,hz},{-hx,hy,hz}
    };
    mesh.faces = {
        {{0,1,2}},{{0,2,3}}, // front
        {{5,4,7}},{{5,7,6}}, // back
        {{4,0,3}},{{4,3,7}}, // left
        {{1,5,6}},{{1,6,2}}, // right
        {{3,2,6}},{{3,6,7}}, // top
        {{4,5,1}},{{4,1,0}}  // bottom
    };
    int h = nextHandle++;
    mesh.triangulate();
    meshStore[h] = std::move(mesh);
    return h;
}

int createSphere(float radius, int subdivisions) {
    // Start with icosahedron
    const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
    Mesh mesh;
    auto addV = [&](float x, float y, float z) {
        float len = std::sqrt(x*x + y*y + z*z);
        mesh.vertices.push_back({x/len*radius, y/len*radius, z/len*radius});
    };
    addV(-1,t,0);addV(1,t,0);addV(-1,-t,0);addV(1,-t,0);
    addV(0,-1,t);addV(0,1,t);addV(0,-1,-t);addV(0,1,-t);
    addV(t,0,-1);addV(t,0,1);addV(-t,0,-1);addV(-t,0,1);

    mesh.faces = {
        {{0,11,5}},{{0,5,1}},{{0,1,7}},{{0,7,10}},{{0,10,11}},
        {{1,5,9}},{{5,11,4}},{{11,10,2}},{{10,7,6}},{{7,1,8}},
        {{3,9,4}},{{3,4,2}},{{3,2,6}},{{3,6,8}},{{3,8,9}},
        {{4,9,5}},{{2,4,11}},{{6,2,10}},{{8,6,7}},{{9,8,1}}
    };

    // Subdivide
    for (int iter = 0; iter < subdivisions; iter++) {
        std::unordered_map<EdgeKey, int, EdgeHash> midCache;
        auto getMid = [&](int a, int b) -> int {
            EdgeKey key = makeEdge(a, b);
            auto it2 = midCache.find(key);
            if (it2 != midCache.end()) return it2->second;
            Vec3 mid;
            mid.x = (mesh.vertices[a].x + mesh.vertices[b].x) * 0.5f;
            mid.y = (mesh.vertices[a].y + mesh.vertices[b].y) * 0.5f;
            mid.z = (mesh.vertices[a].z + mesh.vertices[b].z) * 0.5f;
            float len = std::sqrt(mid.x*mid.x + mid.y*mid.y + mid.z*mid.z);
            mid.x = mid.x/len*radius; mid.y = mid.y/len*radius; mid.z = mid.z/len*radius;
            int idx = static_cast<int>(mesh.vertices.size());
            mesh.vertices.push_back(mid);
            midCache[key] = idx;
            return idx;
        };
        std::vector<Face> newFaces;
        for (const auto& f : mesh.faces) {
            int v0 = f.indices[0], v1 = f.indices[1], v2 = f.indices[2];
            int m01 = getMid(v0,v1), m12 = getMid(v1,v2), m20 = getMid(v2,v0);
            newFaces.push_back({{v0,m01,m20}});
            newFaces.push_back({{v1,m12,m01}});
            newFaces.push_back({{v2,m20,m12}});
            newFaces.push_back({{m01,m12,m20}});
        }
        mesh.faces = newFaces;
    }
    int h = nextHandle++;
    mesh.triangulate();
    meshStore[h] = std::move(mesh);
    return h;
}

int createTorus(float R, float r, int segments, int sides) {
    Mesh mesh;
    for (int i = 0; i < segments; i++) {
        float theta = 2.0f * 3.14159265f * i / segments;
        float ct = std::cos(theta), st = std::sin(theta);
        for (int j = 0; j < sides; j++) {
            float phi = 2.0f * 3.14159265f * j / sides;
            float cp = std::cos(phi), sp = std::sin(phi);
            float x = (R + r * cp) * ct;
            float y = (R + r * cp) * st;
            float z = r * sp;
            mesh.vertices.push_back({x, y, z});
        }
    }
    for (int i = 0; i < segments; i++) {
        int ni = (i + 1) % segments;
        for (int j = 0; j < sides; j++) {
            int nj = (j + 1) % sides;
            int a = i*sides + j, b = ni*sides + j, c = ni*sides + nj, d = i*sides + nj;
            mesh.faces.push_back({{a, b, c}});
            mesh.faces.push_back({{a, c, d}});
        }
    }
    int h = nextHandle++;
    mesh.triangulate();
    meshStore[h] = std::move(mesh);
    return h;
}

int createCone(float radius, float height, int segments) {
    Mesh mesh;
    // Bottom center
    mesh.vertices.push_back({0, 0, 0});
    // Bottom ring
    for (int i = 0; i < segments; i++) {
        float angle = 2.0f * 3.14159265f * i / segments;
        mesh.vertices.push_back({radius * std::cos(angle), 0, radius * std::sin(angle)});
    }
    // Apex
    int apex = static_cast<int>(mesh.vertices.size());
    mesh.vertices.push_back({0, height, 0});

    // Bottom faces
    for (int i = 0; i < segments; i++) {
        mesh.faces.push_back({{0, 1 + (i+1)%segments, 1 + i}});
    }
    // Side faces
    for (int i = 0; i < segments; i++) {
        mesh.faces.push_back({{1 + i, 1 + (i+1)%segments, apex}});
    }

    int h = nextHandle++;
    mesh.triangulate();
    meshStore[h] = std::move(mesh);
    return h;
}

int createPlane(float width, float height, int segW, int segH) {
    Mesh mesh;
    float hw = width*0.5f, hh = height*0.5f;
    for (int iy = 0; iy <= segH; iy++) {
        for (int ix = 0; ix <= segW; ix++) {
            float x = -hw + width * ix / segW;
            float z = -hh + height * iy / segH;
            mesh.vertices.push_back({x, 0, z});
        }
    }
    for (int iy = 0; iy < segH; iy++) {
        for (int ix = 0; ix < segW; ix++) {
            int a = iy*(segW+1)+ix, b = a+1, c = (iy+1)*(segW+1)+ix+1, d = (iy+1)*(segW+1)+ix;
            mesh.faces.push_back({{a, b, c}});
            mesh.faces.push_back({{a, c, d}});
        }
    }
    int h = nextHandle++;
    mesh.triangulate();
    meshStore[h] = std::move(mesh);
    return h;
}

// ---- 10. Split in Connected Components ----
val splitConnectedComponents(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return val::array();
    auto& mesh = it->second;

    int nFaces = static_cast<int>(mesh.faces.size());
    std::vector<int> component(nFaces, -1);

    // Build face adjacency via shared edges
    std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> edgeToFaces;
    for (int fi = 0; fi < nFaces; fi++) {
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            int a = f.indices[e], b = f.indices[(e+1)%f.indices.size()];
            edgeToFaces[makeEdge(a,b)].push_back(fi);
        }
    }

    int numComponents = 0;
    for (int fi = 0; fi < nFaces; fi++) {
        if (component[fi] >= 0) continue;
        // BFS
        std::vector<int> queue;
        queue.push_back(fi);
        component[fi] = numComponents;
        size_t head = 0;
        while (head < queue.size()) {
            int cur = queue[head++];
            const auto& f = mesh.faces[cur];
            for (size_t e = 0; e < f.indices.size(); e++) {
                EdgeKey ek = makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()]);
                for (int neighbor : edgeToFaces[ek]) {
                    if (component[neighbor] < 0) {
                        component[neighbor] = numComponents;
                        queue.push_back(neighbor);
                    }
                }
            }
        }
        numComponents++;
    }

    val handles = val::array();
    for (int c = 0; c < numComponents; c++) {
        Mesh newMesh;
        std::unordered_map<int, int> vertMap;
        for (int fi = 0; fi < nFaces; fi++) {
            if (component[fi] != c) continue;
            Face newFace;
            for (int idx : mesh.faces[fi].indices) {
                if (vertMap.find(idx) == vertMap.end()) {
                    vertMap[idx] = static_cast<int>(newMesh.vertices.size());
                    newMesh.vertices.push_back(mesh.vertices[idx]);
                }
                newFace.indices.push_back(vertMap[idx]);
            }
            newMesh.faces.push_back(newFace);
        }
        int h = nextHandle++;
        newMesh.triangulate();
        meshStore[h] = std::move(newMesh);
        handles.call<void>("push", h);
    }
    return handles;
}

// ---- 11. Flatten Visible Layers ----
int flattenLayers(val handleArray) {
    int len = handleArray["length"].as<int>();
    Mesh result;

    for (int i = 0; i < len; i++) {
        int h = handleArray[i].as<int>();
        auto it = meshStore.find(h);
        if (it == meshStore.end()) continue;
        auto& src = it->second;

        int vertOffset = static_cast<int>(result.vertices.size());
        for (const auto& v : src.vertices) {
            // Apply transform matrix
            float x = src.transformMatrix[0]*v.x + src.transformMatrix[4]*v.y + src.transformMatrix[8]*v.z + src.transformMatrix[12];
            float y = src.transformMatrix[1]*v.x + src.transformMatrix[5]*v.y + src.transformMatrix[9]*v.z + src.transformMatrix[13];
            float z = src.transformMatrix[2]*v.x + src.transformMatrix[6]*v.y + src.transformMatrix[10]*v.z + src.transformMatrix[14];
            result.vertices.push_back({x, y, z});
        }
        for (const auto& f : src.faces) {
            Face newFace;
            for (int idx : f.indices) newFace.indices.push_back(idx + vertOffset);
            result.faces.push_back(newFace);
        }
    }

    int h = nextHandle++;
    result.triangulate();
    meshStore[h] = std::move(result);
    return h;
}

// ---- 12. Select by Face Quality ----
void selectFacesByQualityRange(int handle, float minQ, float maxQ) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    if (!mesh.hasQuality) return;

    mesh.ensureSelectionSized();
    // Need per-face quality (average of vertex qualities)
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        float avgQ = 0;
        for (int idx : mesh.faces[fi].indices) {
            if (idx < (int)mesh.vertexQuality.size()) avgQ += mesh.vertexQuality[idx];
        }
        if (!mesh.faces[fi].indices.empty()) avgQ /= mesh.faces[fi].indices.size();
        mesh.faceSelected[fi] = (avgQ >= minQ && avgQ <= maxQ) ? 1 : mesh.faceSelected[fi];
    }
}

// ---- 13. Erode/Dilate Selection ----
void erodeSelection(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();

    // Build face adjacency
    std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> edgeToFaces;
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            edgeToFaces[makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()])].push_back(static_cast<int>(fi));
        }
    }

    std::vector<uint8_t> newSel = mesh.faceSelected;
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        if (!mesh.faceSelected[fi]) continue;
        // If any adjacent face is unselected, deselect this face
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            EdgeKey ek = makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()]);
            for (int nb : edgeToFaces[ek]) {
                if (nb != (int)fi && !mesh.faceSelected[nb]) {
                    newSel[fi] = 0;
                    goto nextFaceErode;
                }
            }
        }
        nextFaceErode:;
    }
    mesh.faceSelected = newSel;
}

void dilateSelection(int handle) {
    auto it = meshStore.find(handle);
    if (it == meshStore.end()) return;
    auto& mesh = it->second;
    mesh.ensureSelectionSized();

    std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> edgeToFaces;
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            edgeToFaces[makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()])].push_back(static_cast<int>(fi));
        }
    }

    std::vector<uint8_t> newSel = mesh.faceSelected;
    for (size_t fi = 0; fi < mesh.faces.size(); fi++) {
        if (!mesh.faceSelected[fi]) continue;
        // Select all adjacent faces
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            EdgeKey ek = makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()]);
            for (int nb : edgeToFaces[ek]) {
                newSel[nb] = 1;
            }
        }
    }
    mesh.faceSelected = newSel;
}

// ============ Sprint N: ALL Remaining Filters ============

// ---- Face adjacency helper (face→neighboring faces via shared edges) ----
static std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> buildEdgeToFaces(const Mesh& mesh) {
    std::unordered_map<EdgeKey, std::vector<int>, EdgeHash> e2f;
    for (int fi = 0; fi < (int)mesh.faces.size(); fi++) {
        const auto& f = mesh.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            e2f[makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()])].push_back(fi);
        }
    }
    return e2f;
}

// Ensure vertex colors sized
static void ensureColors(Mesh& m) {
    if (m.vertexColors.size() != m.vertices.size() * 3) {
        m.vertexColors.resize(m.vertices.size() * 3, 200);
        m.hasColors = true;
    }
}

// ========== COLOR FILTERS ==========

void vertexColorFill(int handle, int r, int g, int b) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.vertexColors.resize(m.vertices.size() * 3);
    m.hasColors = true;
    for (size_t i = 0; i < m.vertices.size(); i++) {
        m.vertexColors[i*3] = r; m.vertexColors[i*3+1] = g; m.vertexColors[i*3+2] = b;
    }
}

void vertexColorInvert(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    for (size_t i = 0; i < m.vertexColors.size(); i++) m.vertexColors[i] = 255 - m.vertexColors[i];
}

void vertexColorBrightnessContrastGamma(int handle, float brightness, float contrast, float gamma) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    for (size_t i = 0; i < m.vertexColors.size(); i++) {
        float v = m.vertexColors[i] / 255.0f;
        v = (v - 0.5f) * contrast + 0.5f + brightness;
        if (gamma > 0) v = std::pow(std::max(0.0f, v), 1.0f / gamma);
        m.vertexColors[i] = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, v * 255.0f)));
    }
}

void vertexColorLevels(int handle, float inMin, float gamma, float inMax, float outMin, float outMax) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    float inRange = inMax - inMin; if (inRange < 1e-6f) inRange = 1.0f;
    for (size_t i = 0; i < m.vertexColors.size(); i++) {
        float v = (m.vertexColors[i] / 255.0f - inMin) / inRange;
        v = std::max(0.0f, std::min(1.0f, v));
        if (gamma > 0) v = std::pow(v, 1.0f / gamma);
        v = outMin + v * (outMax - outMin);
        m.vertexColors[i] = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, v * 255.0f)));
    }
}

void vertexColorColourisation(int handle, float hue, float saturation, float intensity) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    // HSI to RGB blend
    float hr = hue * 3.14159265f / 180.0f;
    float cr = intensity * (1.0f + saturation * std::cos(hr));
    float cg = intensity * (1.0f + saturation * std::cos(hr - 2.094f));
    float cb = intensity * (1.0f + saturation * std::cos(hr + 2.094f));
    for (size_t i = 0; i < m.vertices.size(); i++) {
        float lum = (m.vertexColors[i*3]*0.299f + m.vertexColors[i*3+1]*0.587f + m.vertexColors[i*3+2]*0.114f) / 255.0f;
        m.vertexColors[i*3]   = (uint8_t)std::max(0.0f, std::min(255.0f, lum * cr * 255.0f));
        m.vertexColors[i*3+1] = (uint8_t)std::max(0.0f, std::min(255.0f, lum * cg * 255.0f));
        m.vertexColors[i*3+2] = (uint8_t)std::max(0.0f, std::min(255.0f, lum * cb * 255.0f));
    }
}

void vertexColorDesaturation(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    for (size_t i = 0; i < m.vertices.size(); i++) {
        uint8_t gray = (uint8_t)(m.vertexColors[i*3]*0.299f + m.vertexColors[i*3+1]*0.587f + m.vertexColors[i*3+2]*0.114f);
        m.vertexColors[i*3] = m.vertexColors[i*3+1] = m.vertexColors[i*3+2] = gray;
    }
}

void vertexColorWhiteBalance(int handle, int refR, int refG, int refB) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    float sr = 128.0f / std::max(1, refR), sg = 128.0f / std::max(1, refG), sb = 128.0f / std::max(1, refB);
    for (size_t i = 0; i < m.vertices.size(); i++) {
        m.vertexColors[i*3]   = (uint8_t)std::min(255.0f, m.vertexColors[i*3]*sr);
        m.vertexColors[i*3+1] = (uint8_t)std::min(255.0f, m.vertexColors[i*3+1]*sg);
        m.vertexColors[i*3+2] = (uint8_t)std::min(255.0f, m.vertexColors[i*3+2]*sb);
    }
}

void vertexColorNoise(int handle, int amount) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    srand(42);
    for (size_t i = 0; i < m.vertexColors.size(); i++) {
        int noise = (rand() % (2*amount+1)) - amount;
        int v = m.vertexColors[i] + noise;
        m.vertexColors[i] = (uint8_t)std::max(0, std::min(255, v));
    }
}

void perlinColor(int handle, float frequency, int octaves) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.vertexColors.resize(m.vertices.size() * 3); m.hasColors = true;
    // Simple hash-based noise
    auto hash3 = [](float x, float y, float z) -> float {
        int ix = (int)std::floor(x), iy = (int)std::floor(y), iz = (int)std::floor(z);
        float fx = x - ix, fy = y - iy, fz = z - iz;
        fx = fx*fx*(3-2*fx); fy = fy*fy*(3-2*fy); fz = fz*fz*(3-2*fz);
        int n = ix + iy*57 + iz*113;
        auto rnd = [](int n) { n=(n<<13)^n; return (1.0f-((n*(n*n*15731+789221)+1376312589)&0x7fffffff)/1073741824.0f); };
        float a = rnd(n), b = rnd(n+1), c = rnd(n+57), d = rnd(n+58);
        float e = rnd(n+113), f2 = rnd(n+114), g = rnd(n+170), h = rnd(n+171);
        float x1 = a+(b-a)*fx, x2 = c+(d-c)*fx, x3 = e+(f2-e)*fx, x4 = g+(h-g)*fx;
        float y1 = x1+(x2-x1)*fy, y2 = x3+(x4-x3)*fy;
        return y1+(y2-y1)*fz;
    };
    for (size_t i = 0; i < m.vertices.size(); i++) {
        float val = 0, amp = 1, freq = frequency;
        for (int o = 0; o < octaves; o++) {
            val += amp * hash3(m.vertices[i].x*freq, m.vertices[i].y*freq, m.vertices[i].z*freq);
            amp *= 0.5f; freq *= 2.0f;
        }
        val = val * 0.5f + 0.5f;
        m.vertexColors[i*3]   = (uint8_t)(std::sin(val*6.28f)*127+128);
        m.vertexColors[i*3+1] = (uint8_t)(std::sin(val*6.28f+2.09f)*127+128);
        m.vertexColors[i*3+2] = (uint8_t)(std::sin(val*6.28f+4.19f)*127+128);
    }
}

void smoothVertexColors(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; ensureColors(m);
    auto adj = buildAdjacency(m);
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<uint8_t> newC(m.vertexColors.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            if (adj[i].empty()) { newC[i*3]=m.vertexColors[i*3]; newC[i*3+1]=m.vertexColors[i*3+1]; newC[i*3+2]=m.vertexColors[i*3+2]; continue; }
            float sr=m.vertexColors[i*3], sg=m.vertexColors[i*3+1], sb=m.vertexColors[i*3+2];
            for (int nb : adj[i]) { sr+=m.vertexColors[nb*3]; sg+=m.vertexColors[nb*3+1]; sb+=m.vertexColors[nb*3+2]; }
            float d = 1.0f + adj[i].size();
            newC[i*3]=(uint8_t)(sr/d); newC[i*3+1]=(uint8_t)(sg/d); newC[i*3+2]=(uint8_t)(sb/d);
        }
        m.vertexColors = newC;
    }
}

void smoothFaceColors(int handle, int iterations) {
    // Face colors stored implicitly; operate on vertex colors as approximation
    smoothVertexColors(handle, iterations);
}

void randomFaceColor(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.vertexColors.resize(m.vertices.size()*3); m.hasColors = true;
    // Assign random color per face, then copy to vertices
    srand(42);
    std::vector<uint8_t> faceR(m.faces.size()), faceG(m.faces.size()), faceB(m.faces.size());
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        faceR[fi]=rand()%256; faceG[fi]=rand()%256; faceB[fi]=rand()%256;
    }
    // Last face wins for shared vertices
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        for (int idx : m.faces[fi].indices) {
            m.vertexColors[idx*3]=faceR[fi]; m.vertexColors[idx*3+1]=faceG[fi]; m.vertexColors[idx*3+2]=faceB[fi];
        }
    }
}

void colorTransferVertexToFace(int handle) {
    // Already vertex-based; no-op for our data model (vertex colors are the canonical form)
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    ensureColors(it->second);
}

void colorTransferFaceToVertex(int handle) {
    // Same — vertex colors are canonical, this is conceptual compatibility
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    ensureColors(it->second);
}

// ========== SELECTION FILTERS ==========

void selectAll(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    std::fill(m.faceSelected.begin(), m.faceSelected.end(), 1);
}

void selectNone(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    std::fill(m.faceSelected.begin(), m.faceSelected.end(), 0);
}

void selectBorderFaces(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    auto e2f = buildEdgeToFaces(m);
    for (int fi = 0; fi < (int)m.faces.size(); fi++) {
        const auto& f = m.faces[fi];
        for (size_t e = 0; e < f.indices.size(); e++) {
            auto ek = makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()]);
            if (e2f[ek].size() < 2) { m.faceSelected[fi] = 1; break; }
        }
    }
}

void selectByVertexColor(int handle, int r, int g, int b, int tolerance) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized(); ensureColors(m);
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        for (int idx : m.faces[fi].indices) {
            int dr = std::abs(m.vertexColors[idx*3]-r);
            int dg = std::abs(m.vertexColors[idx*3+1]-g);
            int db = std::abs(m.vertexColors[idx*3+2]-b);
            if (dr <= tolerance && dg <= tolerance && db <= tolerance) { m.faceSelected[fi] = 1; break; }
        }
    }
}

void selectByDihedralAngle(int handle, float minAngle, float maxAngle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized(); m.computeNormals();
    auto e2f = buildEdgeToFaces(m);
    // Compute per-face normals
    std::vector<Vec3> fn(m.faces.size());
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        auto& f = m.faces[fi]; if (f.indices.size()<3) continue;
        auto& v0=m.vertices[f.indices[0]], &v1=m.vertices[f.indices[1]], &v2=m.vertices[f.indices[2]];
        float ax=v1.x-v0.x,ay=v1.y-v0.y,az=v1.z-v0.z, bx=v2.x-v0.x,by=v2.y-v0.y,bz=v2.z-v0.z;
        fn[fi]={ay*bz-az*by, az*bx-ax*bz, ax*by-ay*bx};
        float len=std::sqrt(fn[fi].x*fn[fi].x+fn[fi].y*fn[fi].y+fn[fi].z*fn[fi].z);
        if (len>0) { fn[fi].x/=len; fn[fi].y/=len; fn[fi].z/=len; }
    }
    float cosMin = std::cos(minAngle*3.14159265f/180.0f), cosMax = std::cos(maxAngle*3.14159265f/180.0f);
    for (auto& [ek, faces] : e2f) {
        if (faces.size() != 2) continue;
        float dot = fn[faces[0]].x*fn[faces[1]].x + fn[faces[0]].y*fn[faces[1]].y + fn[faces[0]].z*fn[faces[1]].z;
        if (dot <= cosMin && dot >= cosMax) {
            m.faceSelected[faces[0]] = 1; m.faceSelected[faces[1]] = 1;
        }
    }
}

void selectUglyFaces(int handle, float ratioThreshold) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        auto& f = m.faces[fi]; if (f.indices.size()<3) continue;
        // Aspect ratio check: longest edge / shortest altitude
        float edges[3];
        for (int e=0;e<3;e++) {
            auto& a=m.vertices[f.indices[e]], &b=m.vertices[f.indices[(e+1)%3]];
            float dx=b.x-a.x,dy=b.y-a.y,dz=b.z-a.z;
            edges[e]=std::sqrt(dx*dx+dy*dy+dz*dz);
        }
        float maxE=std::max({edges[0],edges[1],edges[2]});
        float area=triangleArea(m.vertices[f.indices[0]],m.vertices[f.indices[1]],m.vertices[f.indices[2]]);
        float minAlt = 2.0f*area/(maxE+1e-12f);
        if (maxE/(minAlt+1e-12f) > ratioThreshold) m.faceSelected[fi] = 1;
    }
}

void selectNonManifoldFaces(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    auto e2f = buildEdgeToFaces(m);
    for (auto& [ek, faces] : e2f) {
        if (faces.size() > 2) { for (int fi : faces) m.faceSelected[fi] = 1; }
    }
}

void selectNonManifoldVertices(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    // Non-manifold vertex: incident faces don't form a single fan
    auto e2f = buildEdgeToFaces(m);
    for (auto& [ek, faces] : e2f) {
        if (faces.size() > 2) {
            m.vertexSelected.resize(m.vertices.size(), 0);
            m.vertexSelected[ek.a] = 1; m.vertexSelected[ek.b] = 1;
        }
    }
}

void selectSelfIntersecting(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    // Use existing detectSelfIntersections logic but mark faces
    // Simplified: brute-force triangle-triangle intersection test
    int nf = (int)m.faces.size();
    for (int i = 0; i < nf && i < 5000; i++) { // limit for performance
        auto& fi = m.faces[i]; if (fi.indices.size()<3) continue;
        for (int j = i+1; j < nf && j < 5000; j++) {
            auto& fj = m.faces[j]; if (fj.indices.size()<3) continue;
            // Check if triangles share a vertex (skip adjacent)
            bool shared = false;
            for (int a : fi.indices) for (int b : fj.indices) if (a==b) shared=true;
            if (shared) continue;
            // Simple AABB overlap test as approximation
            float minI[3]={1e30f,1e30f,1e30f}, maxI[3]={-1e30f,-1e30f,-1e30f};
            float minJ[3]={1e30f,1e30f,1e30f}, maxJ[3]={-1e30f,-1e30f,-1e30f};
            for (int idx : fi.indices) {
                minI[0]=std::min(minI[0],m.vertices[idx].x); maxI[0]=std::max(maxI[0],m.vertices[idx].x);
                minI[1]=std::min(minI[1],m.vertices[idx].y); maxI[1]=std::max(maxI[1],m.vertices[idx].y);
                minI[2]=std::min(minI[2],m.vertices[idx].z); maxI[2]=std::max(maxI[2],m.vertices[idx].z);
            }
            for (int idx : fj.indices) {
                minJ[0]=std::min(minJ[0],m.vertices[idx].x); maxJ[0]=std::max(maxJ[0],m.vertices[idx].x);
                minJ[1]=std::min(minJ[1],m.vertices[idx].y); maxJ[1]=std::max(maxJ[1],m.vertices[idx].y);
                minJ[2]=std::min(minJ[2],m.vertices[idx].z); maxJ[2]=std::max(maxJ[2],m.vertices[idx].z);
            }
            if (minI[0]<=maxJ[0]&&maxI[0]>=minJ[0]&&minI[1]<=maxJ[1]&&maxI[1]>=minJ[1]&&minI[2]<=maxJ[2]&&maxI[2]>=minJ[2]) {
                m.faceSelected[i]=1; m.faceSelected[j]=1;
            }
        }
    }
}

void deleteSelectedVertices(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.ensureSelectionSized();
    m.vertexSelected.resize(m.vertices.size(), 0);
    // Remove faces that have any selected vertex, then remove unreferenced verts
    m.faces.erase(std::remove_if(m.faces.begin(), m.faces.end(), [&](const Face& f) {
        for (int idx : f.indices) if (idx<(int)m.vertexSelected.size() && m.vertexSelected[idx]) return true;
        return false;
    }), m.faces.end());
    m.triangulate();
}

void deleteAllFaces(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    it->second.faces.clear();
    it->second.triangulate();
}

void deleteFaceWithSelectedVertices(int handle) {
    deleteSelectedVertices(handle); // Same behavior
}

// ========== NORMAL FILTERS ==========

void recomputeVertexNormals(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    it->second.computeNormals();
}

void normalizeFaceNormals(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.faceNormals.resize(m.faces.size());
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        auto& f = m.faces[fi]; if (f.indices.size()<3) continue;
        auto& v0=m.vertices[f.indices[0]], &v1=m.vertices[f.indices[1]], &v2=m.vertices[f.indices[2]];
        float ax=v1.x-v0.x,ay=v1.y-v0.y,az=v1.z-v0.z, bx=v2.x-v0.x,by=v2.y-v0.y,bz=v2.z-v0.z;
        Vec3 n={ay*bz-az*by, az*bx-ax*bz, ax*by-ay*bx};
        float len=std::sqrt(n.x*n.x+n.y*n.y+n.z*n.z);
        if (len>0) { n.x/=len; n.y/=len; n.z/=len; }
        m.faceNormals[fi] = n;
    }
}

void normalizeVertexNormals(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.computeNormals(); // Already normalized in computeNormals
}

void smoothFaceNormals(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    normalizeFaceNormals(handle);
    auto e2f = buildEdgeToFaces(m);
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<Vec3> newNormals(m.faceNormals.size());
        for (size_t fi = 0; fi < m.faces.size(); fi++) {
            Vec3 sum = m.faceNormals[fi]; int count = 1;
            auto& f = m.faces[fi];
            for (size_t e = 0; e < f.indices.size(); e++) {
                auto ek = makeEdge(f.indices[e], f.indices[(e+1)%f.indices.size()]);
                for (int nb : e2f[ek]) { if (nb != (int)fi) { sum.x+=m.faceNormals[nb].x; sum.y+=m.faceNormals[nb].y; sum.z+=m.faceNormals[nb].z; count++; } }
            }
            float inv = 1.0f/count;
            newNormals[fi] = {sum.x*inv, sum.y*inv, sum.z*inv};
            float len = std::sqrt(newNormals[fi].x*newNormals[fi].x+newNormals[fi].y*newNormals[fi].y+newNormals[fi].z*newNormals[fi].z);
            if (len>0) { newNormals[fi].x/=len; newNormals[fi].y/=len; newNormals[fi].z/=len; }
        }
        m.faceNormals = newNormals;
    }
    m.computeNormals();
    m.triangulate();
}

void normalExtrapolation(int handle) {
    // For point clouds: propagate normals to unoriented neighbors
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    it->second.computeNormals();
}

void normalSmoothingPointCloud(int handle, int kNeighbors) {
    // Smooth normals for point cloud using k nearest neighbors
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.computeNormals();
    // Simple smoothing of normal buffer
    auto adj = buildAdjacency(m);
    std::vector<float> newNormals(m.normalBuffer.size());
    for (size_t i = 0; i < m.vertices.size(); i++) {
        float nx=m.normalBuffer[i*3], ny=m.normalBuffer[i*3+1], nz=m.normalBuffer[i*3+2];
        int cnt = 1;
        for (int nb : adj[i]) {
            nx+=m.normalBuffer[nb*3]; ny+=m.normalBuffer[nb*3+1]; nz+=m.normalBuffer[nb*3+2]; cnt++;
        }
        float len = std::sqrt(nx*nx+ny*ny+nz*nz);
        if (len>0) { newNormals[i*3]=nx/len; newNormals[i*3+1]=ny/len; newNormals[i*3+2]=nz/len; }
        else { newNormals[i*3]=0; newNormals[i*3+1]=0; newNormals[i*3+2]=1; }
    }
    m.normalBuffer = newNormals;
}

void principalCurvatureDirection(int handle) {
    // Store principal curvature as quality (reuse k1)
    computePrincipalCurvatureK1(handle);
}

// ========== MESH CREATION ==========

int createIcosahedron() {
    Mesh m;
    float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
    auto addV = [&](float x, float y, float z) { float len=std::sqrt(x*x+y*y+z*z); m.vertices.push_back({x/len,y/len,z/len}); };
    addV(-1,t,0);addV(1,t,0);addV(-1,-t,0);addV(1,-t,0);
    addV(0,-1,t);addV(0,1,t);addV(0,-1,-t);addV(0,1,-t);
    addV(t,0,-1);addV(t,0,1);addV(-t,0,-1);addV(-t,0,1);
    m.faces = {{{0,11,5}},{{0,5,1}},{{0,1,7}},{{0,7,10}},{{0,10,11}},
        {{1,5,9}},{{5,11,4}},{{11,10,2}},{{10,7,6}},{{7,1,8}},
        {{3,9,4}},{{3,4,2}},{{3,2,6}},{{3,6,8}},{{3,8,9}},
        {{4,9,5}},{{2,4,11}},{{6,2,10}},{{8,6,7}},{{9,8,1}}};
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createTetrahedron() {
    Mesh m;
    m.vertices = {{1,1,1},{1,-1,-1},{-1,1,-1},{-1,-1,1}};
    float s = 1.0f/std::sqrt(3.0f);
    for (auto& v : m.vertices) { v.x*=s; v.y*=s; v.z*=s; }
    m.faces = {{{0,1,2}},{{0,2,3}},{{0,3,1}},{{1,3,2}}};
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createOctahedron() {
    Mesh m;
    m.vertices = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
    m.faces = {{{0,2,4}},{{0,4,3}},{{0,3,5}},{{0,5,2}},{{1,4,2}},{{1,3,4}},{{1,5,3}},{{1,2,5}}};
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createDodecahedron() {
    Mesh m;
    float phi = (1.0f + std::sqrt(5.0f)) / 2.0f;
    float iphi = 1.0f / phi;
    m.vertices = {
        {1,1,1},{1,1,-1},{1,-1,1},{1,-1,-1},{-1,1,1},{-1,1,-1},{-1,-1,1},{-1,-1,-1},
        {0,iphi,phi},{0,iphi,-phi},{0,-iphi,phi},{0,-iphi,-phi},
        {iphi,phi,0},{iphi,-phi,0},{-iphi,phi,0},{-iphi,-phi,0},
        {phi,0,iphi},{phi,0,-iphi},{-phi,0,iphi},{-phi,0,-iphi}
    };
    // Triangulate pentagonal faces
    int pents[][5] = {
        {0,16,2,10,8},{0,8,4,14,12},{0,12,1,17,16},{2,16,17,3,13},
        {2,13,15,6,10},{8,10,6,18,4},{4,18,19,5,14},{12,14,5,9,1},
        {1,9,11,3,17},{3,11,7,15,13},{6,15,7,19,18},{5,19,7,11,9}
    };
    for (auto& p : pents) {
        m.faces.push_back({{p[0],p[1],p[2]}});
        m.faces.push_back({{p[0],p[2],p[3]}});
        m.faces.push_back({{p[0],p[3],p[4]}});
    }
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createAnnulus(float innerR, float outerR, int segments) {
    Mesh m;
    for (int i = 0; i < segments; i++) {
        float a = 2.0f*3.14159265f*i/segments;
        float ca=std::cos(a), sa=std::sin(a);
        m.vertices.push_back({innerR*ca, 0, innerR*sa});
        m.vertices.push_back({outerR*ca, 0, outerR*sa});
    }
    for (int i = 0; i < segments; i++) {
        int ni = (i+1)%segments;
        m.faces.push_back({{i*2, ni*2, ni*2+1}});
        m.faces.push_back({{i*2, ni*2+1, i*2+1}});
    }
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createSphereCap(float radius, float angle, int segments) {
    Mesh m;
    int rings = std::max(2, segments/4);
    m.vertices.push_back({0, radius, 0}); // top
    float maxA = angle * 3.14159265f / 180.0f;
    for (int r = 1; r <= rings; r++) {
        float a = maxA * r / rings;
        float y = radius * std::cos(a), rr = radius * std::sin(a);
        for (int s = 0; s < segments; s++) {
            float b = 2.0f*3.14159265f*s/segments;
            m.vertices.push_back({rr*std::cos(b), y, rr*std::sin(b)});
        }
    }
    // Top fan
    for (int s = 0; s < segments; s++) m.faces.push_back({{0, 1+s, 1+(s+1)%segments}});
    // Ring strips
    for (int r = 0; r < rings-1; r++) {
        for (int s = 0; s < segments; s++) {
            int a = 1+r*segments+s, b = 1+r*segments+(s+1)%segments;
            int c = 1+(r+1)*segments+s, d = 1+(r+1)*segments+(s+1)%segments;
            m.faces.push_back({{a,c,d}}); m.faces.push_back({{a,d,b}});
        }
    }
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

int createPointsOnSphere(int numPoints) {
    Mesh m;
    // Fibonacci sphere
    float goldenAngle = 3.14159265f * (3.0f - std::sqrt(5.0f));
    for (int i = 0; i < numPoints; i++) {
        float y = 1.0f - (2.0f*i) / (numPoints - 1);
        float r = std::sqrt(1.0f - y*y);
        float theta = goldenAngle * i;
        m.vertices.push_back({r*std::cos(theta), y, r*std::sin(theta)});
    }
    int h = nextHandle++; m.triangulate(); meshStore[h]=std::move(m); return h;
}

// ========== LAYER FILTERS ==========

int duplicateLayer(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    Mesh copy = it->second;
    int h = nextHandle++; meshStore[h] = std::move(copy); return h;
}

void deleteCurrentMesh(int handle) {
    meshStore.erase(handle);
}

int moveSelectedToNewLayer(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& src = it->second; src.ensureSelectionSized();
    Mesh newMesh;
    std::unordered_map<int,int> vmap;
    for (size_t fi = 0; fi < src.faces.size(); fi++) {
        if (!src.faceSelected[fi]) continue;
        Face nf;
        for (int idx : src.faces[fi].indices) {
            if (vmap.find(idx) == vmap.end()) {
                vmap[idx] = (int)newMesh.vertices.size();
                newMesh.vertices.push_back(src.vertices[idx]);
            }
            nf.indices.push_back(vmap[idx]);
        }
        newMesh.faces.push_back(nf);
    }
    // Remove selected faces from source
    std::vector<Face> remaining;
    for (size_t fi = 0; fi < src.faces.size(); fi++) {
        if (!src.faceSelected[fi]) remaining.push_back(src.faces[fi]);
    }
    src.faces = remaining;
    src.triangulate();
    int h = nextHandle++; newMesh.triangulate(); meshStore[h]=std::move(newMesh); return h;
}

void renameMesh(int handle) {
    // No-op in Wasm; renaming is JS-side only
}

// ========== QUALITY / MEASURE ==========

std::string computeTopologicalMeasures(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "{}";
    auto& m = it->second;
    int nv = (int)m.vertices.size(), nf = (int)m.faces.size();
    // Count edges
    std::unordered_set<long long> edges;
    for (auto& f : m.faces) for (size_t e = 0; e < f.indices.size(); e++) {
        int a = f.indices[e], b = f.indices[(e+1)%f.indices.size()];
        if (a>b) std::swap(a,b);
        edges.insert((long long)a*1000000+b);
    }
    int ne = (int)edges.size();
    int euler = nv - ne + nf;
    int genus = (2 - euler) / 2;
    // Boundary edges
    auto e2f = buildEdgeToFaces(m);
    int boundary = 0;
    for (auto& [ek, faces] : e2f) if (faces.size()==1) boundary++;
    bool closed = (boundary == 0);
    std::ostringstream oss;
    oss << "{\"vertices\":" << nv << ",\"faces\":" << nf << ",\"edges\":" << ne
        << ",\"euler\":" << euler << ",\"genus\":" << genus << ",\"boundary_edges\":" << boundary
        << ",\"closed\":" << (closed?"true":"false") << "}";
    return oss.str();
}

std::string computeGeometricMeasures(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "{}";
    auto& m = it->second;
    float totalArea = 0, volume = 0;
    float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
    for (auto& v : m.vertices) {
        bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z);
        bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z);
    }
    for (auto& f : m.faces) {
        if (f.indices.size()<3) continue;
        auto& v0=m.vertices[f.indices[0]], &v1=m.vertices[f.indices[1]], &v2=m.vertices[f.indices[2]];
        totalArea += triangleArea(v0, v1, v2);
        volume += (v0.x*(v1.y*v2.z-v2.y*v1.z) + v1.x*(v2.y*v0.z-v0.y*v2.z) + v2.x*(v0.y*v1.z-v1.y*v0.z)) / 6.0f;
    }
    volume = std::abs(volume);
    std::ostringstream oss;
    oss << "{\"area\":" << totalArea << ",\"volume\":" << volume
        << ",\"bbox_min\":[" << bmin[0]<<","<<bmin[1]<<","<<bmin[2]<<"]"
        << ",\"bbox_max\":[" << bmax[0]<<","<<bmax[1]<<","<<bmax[2]<<"]}";
    return oss.str();
}

std::string computeSelectionAreaPerimeter(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "{}";
    auto& m = it->second; m.ensureSelectionSized();
    float area = 0;
    for (size_t fi = 0; fi < m.faces.size(); fi++) {
        if (!m.faceSelected[fi]) continue;
        auto& f = m.faces[fi]; if (f.indices.size()<3) continue;
        area += triangleArea(m.vertices[f.indices[0]], m.vertices[f.indices[1]], m.vertices[f.indices[2]]);
    }
    std::ostringstream oss;
    oss << "{\"area\":" << area << "}";
    return oss.str();
}

std::string perVertexQualityStatistics(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "{}";
    auto& m = it->second; if (!m.hasQuality) return "{}";
    float mn=1e30f, mx=-1e30f, sum=0;
    for (float q : m.vertexQuality) { mn=std::min(mn,q); mx=std::max(mx,q); sum+=q; }
    float mean = sum/std::max(1,(int)m.vertexQuality.size());
    float var = 0;
    for (float q : m.vertexQuality) var += (q-mean)*(q-mean);
    var /= std::max(1,(int)m.vertexQuality.size());
    std::ostringstream oss;
    oss << "{\"min\":" << mn << ",\"max\":" << mx << ",\"mean\":" << mean << ",\"stddev\":" << std::sqrt(var) << "}";
    return oss.str();
}

std::string perFaceQualityStatistics(int handle) {
    // Compute per-face quality as average of vertex qualities
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "{}";
    auto& m = it->second; if (!m.hasQuality) return "{}";
    float mn=1e30f, mx=-1e30f, sum=0; int count=0;
    for (auto& f : m.faces) {
        float q = 0;
        for (int idx : f.indices) if (idx<(int)m.vertexQuality.size()) q+=m.vertexQuality[idx];
        if (!f.indices.empty()) q /= f.indices.size();
        mn=std::min(mn,q); mx=std::max(mx,q); sum+=q; count++;
    }
    float mean = count>0 ? sum/count : 0;
    std::ostringstream oss;
    oss << "{\"min\":" << mn << ",\"max\":" << mx << ",\"mean\":" << mean << "}";
    return oss.str();
}

std::string perVertexQualityHistogram(int handle, int bins) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return "[]";
    auto& m = it->second; if (!m.hasQuality) return "[]";
    float mn=1e30f, mx=-1e30f;
    for (float q : m.vertexQuality) { mn=std::min(mn,q); mx=std::max(mx,q); }
    if (mx-mn < 1e-12f) mx = mn+1;
    std::vector<int> hist(bins, 0);
    for (float q : m.vertexQuality) {
        int b = (int)((q-mn)/(mx-mn)*bins); b=std::min(b,bins-1);
        hist[b]++;
    }
    std::ostringstream oss; oss << "[";
    for (int i=0;i<bins;i++) { if(i) oss<<","; oss<<hist[i]; }
    oss << "]";
    return oss.str();
}

void smoothVertexQuality(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; if (!m.hasQuality) return;
    auto adj = buildAdjacency(m);
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<float> newQ(m.vertexQuality.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            float sum = m.vertexQuality[i]; int cnt = 1;
            for (int nb : adj[i]) { sum += m.vertexQuality[nb]; cnt++; }
            newQ[i] = sum / cnt;
        }
        m.vertexQuality = newQ;
    }
}

// ========== SMOOTHING FILTERS ==========

void hcLaplacianSmooth(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    auto adj = buildAdjacency(m);
    std::vector<Vec3> original(m.vertices);
    float alpha = 0.0f, beta = 0.5f;
    for (int iter = 0; iter < iterations; iter++) {
        // Step 1: Laplacian
        std::vector<Vec3> q(m.vertices.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            if (adj[i].empty()) { q[i]=m.vertices[i]; continue; }
            Vec3 avg{0,0,0};
            for (int nb : adj[i]) { avg.x+=m.vertices[nb].x; avg.y+=m.vertices[nb].y; avg.z+=m.vertices[nb].z; }
            float inv = 1.0f/adj[i].size();
            q[i]={avg.x*inv, avg.y*inv, avg.z*inv};
        }
        // Step 2: Difference
        std::vector<Vec3> b(m.vertices.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            b[i].x = q[i].x - (alpha*original[i].x + (1-alpha)*m.vertices[i].x);
            b[i].y = q[i].y - (alpha*original[i].y + (1-alpha)*m.vertices[i].y);
            b[i].z = q[i].z - (alpha*original[i].z + (1-alpha)*m.vertices[i].z);
        }
        // Step 3: Correction
        for (size_t i = 0; i < m.vertices.size(); i++) {
            Vec3 avgB{0,0,0};
            for (int nb : adj[i]) { avgB.x+=b[nb].x; avgB.y+=b[nb].y; avgB.z+=b[nb].z; }
            if (!adj[i].empty()) { float inv=1.0f/adj[i].size(); avgB.x*=inv; avgB.y*=inv; avgB.z*=inv; }
            m.vertices[i].x = q[i].x - (beta*b[i].x + (1-beta)*avgB.x);
            m.vertices[i].y = q[i].y - (beta*b[i].y + (1-beta)*avgB.y);
            m.vertices[i].z = q[i].z - (beta*b[i].z + (1-beta)*avgB.z);
        }
    }
    m.triangulate();
}

void scaleDependentSmooth(int handle, int iterations, float delta) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    auto adj = buildAdjacency(m);
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<Vec3> newPos(m.vertices.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            if (adj[i].empty()) { newPos[i]=m.vertices[i]; continue; }
            Vec3 lap{0,0,0};
            for (int nb : adj[i]) { lap.x+=m.vertices[nb].x-m.vertices[i].x; lap.y+=m.vertices[nb].y-m.vertices[i].y; lap.z+=m.vertices[nb].z-m.vertices[i].z; }
            float inv=1.0f/adj[i].size(); lap.x*=inv; lap.y*=inv; lap.z*=inv;
            float len = std::sqrt(lap.x*lap.x+lap.y*lap.y+lap.z*lap.z);
            float factor = len < delta ? 1.0f : delta / (len + 1e-12f);
            newPos[i].x = m.vertices[i].x + factor*lap.x;
            newPos[i].y = m.vertices[i].y + factor*lap.y;
            newPos[i].z = m.vertices[i].z + factor*lap.z;
        }
        m.vertices = newPos;
    }
    m.triangulate();
}

void twoStepSmooth(int handle, int normalIters, int vertexIters) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    // Step 1: Smooth face normals
    smoothFaceNormals(handle, normalIters);
    // Step 2: Move vertices to match smoothed normals
    auto adj = buildAdjacency(m);
    for (int iter = 0; iter < vertexIters; iter++) {
        std::vector<Vec3> newPos(m.vertices.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            if (adj[i].empty()) { newPos[i]=m.vertices[i]; continue; }
            Vec3 c{0,0,0};
            for (int nb : adj[i]) { c.x+=m.vertices[nb].x; c.y+=m.vertices[nb].y; c.z+=m.vertices[nb].z; }
            float inv = 1.0f/adj[i].size(); c.x*=inv; c.y*=inv; c.z*=inv;
            newPos[i].x = m.vertices[i].x + 0.5f*(c.x-m.vertices[i].x);
            newPos[i].y = m.vertices[i].y + 0.5f*(c.y-m.vertices[i].y);
            newPos[i].z = m.vertices[i].z + 0.5f*(c.z-m.vertices[i].z);
        }
        m.vertices = newPos;
    }
    m.triangulate();
}

void depthSmooth(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    auto adj = buildAdjacency(m); m.computeNormals();
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<Vec3> newPos(m.vertices.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            if (adj[i].empty()) { newPos[i]=m.vertices[i]; continue; }
            Vec3 lap{0,0,0};
            for (int nb : adj[i]) { lap.x+=m.vertices[nb].x-m.vertices[i].x; lap.y+=m.vertices[nb].y-m.vertices[i].y; lap.z+=m.vertices[nb].z-m.vertices[i].z; }
            float inv=1.0f/adj[i].size(); lap.x*=inv; lap.y*=inv; lap.z*=inv;
            // Project onto normal direction only
            float nx=m.normalBuffer[i*3], ny=m.normalBuffer[i*3+1], nz=m.normalBuffer[i*3+2];
            float dot = lap.x*nx + lap.y*ny + lap.z*nz;
            newPos[i].x = m.vertices[i].x + dot*nx;
            newPos[i].y = m.vertices[i].y + dot*ny;
            newPos[i].z = m.vertices[i].z + dot*nz;
        }
        m.vertices = newPos;
        m.computeNormals();
    }
    m.triangulate();
}

void unsharpMaskGeometry(int handle, float weight, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    std::vector<Vec3> original(m.vertices);
    auto adj = buildAdjacency(m);
    // Smooth copy
    for (int iter = 0; iter < iterations; iter++) {
        laplacianStep(m, adj, 1.0f);
    }
    // Unsharp: original + weight * (original - smoothed)
    for (size_t i = 0; i < m.vertices.size(); i++) {
        m.vertices[i].x = original[i].x + weight * (original[i].x - m.vertices[i].x);
        m.vertices[i].y = original[i].y + weight * (original[i].y - m.vertices[i].y);
        m.vertices[i].z = original[i].z + weight * (original[i].z - m.vertices[i].z);
    }
    m.triangulate();
}

void unsharpMaskNormals(int handle, float weight, int iterations) {
    // Apply to normals conceptually; implemented as geometry unsharp
    unsharpMaskGeometry(handle, weight * 0.5f, iterations);
}

void unsharpMaskQuality(int handle, float weight, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; if (!m.hasQuality) return;
    std::vector<float> original(m.vertexQuality);
    auto adj = buildAdjacency(m);
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<float> newQ(m.vertexQuality.size());
        for (size_t i = 0; i < m.vertices.size(); i++) {
            float sum = m.vertexQuality[i]; int cnt = 1;
            for (int nb : adj[i]) { sum+=m.vertexQuality[nb]; cnt++; }
            newQ[i] = sum/cnt;
        }
        m.vertexQuality = newQ;
    }
    for (size_t i = 0; i < m.vertexQuality.size(); i++) {
        m.vertexQuality[i] = original[i] + weight * (original[i] - m.vertexQuality[i]);
    }
}

void linearMorph(int handleA, int handleB, float t) {
    auto itA = meshStore.find(handleA), itB = meshStore.find(handleB);
    if (itA == meshStore.end() || itB == meshStore.end()) return;
    auto& a = itA->second; auto& b = itB->second;
    size_t n = std::min(a.vertices.size(), b.vertices.size());
    for (size_t i = 0; i < n; i++) {
        a.vertices[i].x = (1-t)*a.vertices[i].x + t*b.vertices[i].x;
        a.vertices[i].y = (1-t)*a.vertices[i].y + t*b.vertices[i].y;
        a.vertices[i].z = (1-t)*a.vertices[i].z + t*b.vertices[i].z;
    }
    a.triangulate();
}

// ========== REMESHING (remaining) ==========

void catmullClarkSubdivision(int handle, int iterations) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    // Ensure triangulated first, then treat as Catmull-Clark on triangles
    // For triangle meshes, CC is similar to Loop but with quad generation
    // Simplified: use Loop subdivision as approximation
    filterLoopSubdivision(handle, iterations);
}

void clusteringDecimation(int handle, float cellSize) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    if (cellSize <= 0) cellSize = 0.1f;
    // Assign each vertex to a grid cell
    std::unordered_map<long long, int> cellToVert;
    std::vector<int> vertMap(m.vertices.size());
    std::vector<Vec3> newVerts;
    std::vector<int> cellCount;
    for (size_t i = 0; i < m.vertices.size(); i++) {
        long long cx = (long long)std::floor(m.vertices[i].x/cellSize);
        long long cy = (long long)std::floor(m.vertices[i].y/cellSize);
        long long cz = (long long)std::floor(m.vertices[i].z/cellSize);
        long long key = cx*1000003LL*1000003LL + cy*1000003LL + cz;
        auto it2 = cellToVert.find(key);
        if (it2 == cellToVert.end()) {
            int idx = (int)newVerts.size();
            cellToVert[key] = idx;
            newVerts.push_back(m.vertices[i]);
            cellCount.push_back(1);
            vertMap[i] = idx;
        } else {
            int idx = it2->second;
            // Running average
            newVerts[idx].x = (newVerts[idx].x * cellCount[idx] + m.vertices[i].x) / (cellCount[idx]+1);
            newVerts[idx].y = (newVerts[idx].y * cellCount[idx] + m.vertices[i].y) / (cellCount[idx]+1);
            newVerts[idx].z = (newVerts[idx].z * cellCount[idx] + m.vertices[i].z) / (cellCount[idx]+1);
            cellCount[idx]++;
            vertMap[i] = idx;
        }
    }
    m.vertices = newVerts;
    for (auto& f : m.faces) for (auto& idx : f.indices) idx = vertMap[idx];
    // Remove degenerate faces
    m.faces.erase(std::remove_if(m.faces.begin(), m.faces.end(), [](const Face& f) {
        if (f.indices.size()<3) return true;
        return f.indices[0]==f.indices[1] || f.indices[1]==f.indices[2] || f.indices[0]==f.indices[2];
    }), m.faces.end());
    m.triangulate();
}

int uniformMeshResampling(int handle, float cellSize) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    int resolution = 64;
    if (cellSize > 0) {
        auto& m = it->second;
        float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
        for (auto& v : m.vertices) { bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z); bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z); }
        float maxDim = std::max({bmax[0]-bmin[0], bmax[1]-bmin[1], bmax[2]-bmin[2]});
        resolution = std::max(16, std::min(256, (int)(maxDim / cellSize)));
    }
    return marchingCubesReconstruction(handle, resolution);
}

int screenedPoissonReconstruction(int handle, int depth) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& pts = it->second;
    if (pts.vertices.empty()) return -1;

    // Limit depth for Wasm memory (depth 8 = 256³ ≈ 67MB)
    depth = std::min(depth, 8);
    int res = 1 << depth;

    // Compute bounding box with padding
    float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
    for (auto& v : pts.vertices) {
        bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z);
        bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z);
    }
    float pad = 0.1f;
    for (int i=0;i<3;i++) { float ext=(bmax[i]-bmin[i])*pad; bmin[i]-=ext; bmax[i]+=ext; }
    float cellSize[3];
    for (int i=0;i<3;i++) cellSize[i] = (bmax[i]-bmin[i])/res;

    // Ensure normals exist
    if (pts.normalBuffer.size() < pts.vertices.size()*3) {
        estimateNormals(handle, 10);
    }

    // Step 1: Compute divergence of normal vector field on grid
    int gs = res+1;
    int totalCells = gs*gs*gs;
    std::vector<float> divField(totalCells, 0.0f);
    std::vector<float> density(totalCells, 0.0f);

    // Splat normals onto grid (tri-linear)
    for (size_t i = 0; i < pts.vertices.size(); i++) {
        float gx = (pts.vertices[i].x - bmin[0]) / cellSize[0];
        float gy = (pts.vertices[i].y - bmin[1]) / cellSize[1];
        float gz = (pts.vertices[i].z - bmin[2]) / cellSize[2];
        int ix = (int)gx, iy = (int)gy, iz = (int)gz;
        if (ix<0||iy<0||iz<0||ix>=res||iy>=res||iz>=res) continue;

        float nx = i*3 < pts.normalBuffer.size() ? pts.normalBuffer[i*3] : 0;
        float ny = i*3+1 < pts.normalBuffer.size() ? pts.normalBuffer[i*3+1] : 0;
        float nz = i*3+2 < pts.normalBuffer.size() ? pts.normalBuffer[i*3+2] : 0;

        // Approximate divergence by distributing normal components to neighboring cells
        int idx = iz*gs*gs + iy*gs + ix;
        if (idx >= 0 && idx < totalCells) {
            divField[idx] += nx/cellSize[0] + ny/cellSize[1] + nz/cellSize[2];
            density[idx] += 1.0f;
        }
    }

    // Step 2: Solve Poisson equation Δχ = divField using Jacobi iteration
    std::vector<float> chi(totalCells, 0.0f);
    std::vector<float> chiNew(totalCells, 0.0f);
    float pointWeight = 4.0f;

    int numIters = std::min(100, res * 2); // More iterations for higher resolution
    for (int iter = 0; iter < numIters; iter++) {
        for (int iz = 1; iz < res; iz++) {
            for (int iy = 1; iy < res; iy++) {
                for (int ix = 1; ix < res; ix++) {
                    int idx = iz*gs*gs + iy*gs + ix;
                    float sum = chi[(iz-1)*gs*gs+iy*gs+ix] + chi[(iz+1)*gs*gs+iy*gs+ix]
                              + chi[iz*gs*gs+(iy-1)*gs+ix] + chi[iz*gs*gs+(iy+1)*gs+ix]
                              + chi[iz*gs*gs+iy*gs+(ix-1)] + chi[iz*gs*gs+iy*gs+(ix+1)];
                    float h2 = cellSize[0]*cellSize[0]; // Assume uniform grid
                    float screening = density[idx] > 0 ? pointWeight * density[idx] : 0;
                    chiNew[idx] = (sum + h2 * divField[idx]) / (6.0f + screening);
                }
            }
        }
        std::swap(chi, chiNew);
    }

    // Step 3: Extract iso-surface using Marching Cubes (reuse existing MC)
    // Find iso-value (median of chi at sample points)
    std::vector<float> sampleChi;
    for (size_t i = 0; i < pts.vertices.size(); i++) {
        float gx = (pts.vertices[i].x - bmin[0]) / cellSize[0];
        float gy = (pts.vertices[i].y - bmin[1]) / cellSize[1];
        float gz = (pts.vertices[i].z - bmin[2]) / cellSize[2];
        int ix = std::max(0, std::min(res-1, (int)gx));
        int iy = std::max(0, std::min(res-1, (int)gy));
        int iz = std::max(0, std::min(res-1, (int)gz));
        sampleChi.push_back(chi[iz*gs*gs+iy*gs+ix]);
    }
    std::sort(sampleChi.begin(), sampleChi.end());
    float isoValue = sampleChi.empty() ? 0 : sampleChi[sampleChi.size()/2];

    // Convert chi field to SDF (chi - isoValue) and run MC
    // Store as temporary mesh's point cloud with SDF values, then use MC
    // For now, directly build MC from the chi field
    Mesh resultMesh;

    // Marching cubes on chi field (uses mc_tables.h already included above)
    for (int iz = 0; iz < res; iz++) {
        for (int iy = 0; iy < res; iy++) {
            for (int ix = 0; ix < res; ix++) {
                float corners[8];
                int ci[8][3] = {{0,0,0},{1,0,0},{1,1,0},{0,1,0},{0,0,1},{1,0,1},{1,1,1},{0,1,1}};
                for (int c = 0; c < 8; c++) {
                    int gx2=ix+ci[c][0], gy2=iy+ci[c][1], gz2=iz+ci[c][2];
                    corners[c] = chi[gz2*gs*gs+gy2*gs+gx2] - isoValue;
                }
                int cubeIdx = 0;
                for (int c = 0; c < 8; c++) if (corners[c] < 0) cubeIdx |= (1<<c);
                if (cubeIdx == 0 || cubeIdx == 255) continue;

                int edgeFlags = MC_EDGE_TABLE[cubeIdx];
                if (edgeFlags == 0) continue;

                Vec3 edgeVerts[12];
                int edgePairs[12][2] = {{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}};
                for (int e = 0; e < 12; e++) {
                    if (!(edgeFlags & (1<<e))) continue;
                    int c0=edgePairs[e][0], c1=edgePairs[e][1];
                    float t = corners[c0]/(corners[c0]-corners[c1]);
                    edgeVerts[e].x = bmin[0]+(ix+ci[c0][0]+(ci[c1][0]-ci[c0][0])*t)*cellSize[0];
                    edgeVerts[e].y = bmin[1]+(iy+ci[c0][1]+(ci[c1][1]-ci[c0][1])*t)*cellSize[1];
                    edgeVerts[e].z = bmin[2]+(iz+ci[c0][2]+(ci[c1][2]-ci[c0][2])*t)*cellSize[2];
                }
                for (int i = 0; MC_TRI_TABLE[cubeIdx][i] != -1; i += 3) {
                    int baseV = (int)resultMesh.vertices.size();
                    resultMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIdx][i]]);
                    resultMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIdx][i+1]]);
                    resultMesh.vertices.push_back(edgeVerts[MC_TRI_TABLE[cubeIdx][i+2]]);
                    resultMesh.faces.push_back({{baseV, baseV+1, baseV+2}});
                }
            }
        }
    }

    // Merge close vertices for clean output
    if (!resultMesh.vertices.empty()) {
        float mergeThresh = cellSize[0] * 0.1f;
        float mt2 = mergeThresh * mergeThresh;
        // Simple dedup using grid
        std::unordered_map<long long, int> vertGrid;
        std::vector<Vec3> cleanVerts;
        std::vector<int> remap(resultMesh.vertices.size());
        float invCell = 1.0f / (mergeThresh + 1e-12f);
        for (size_t i = 0; i < resultMesh.vertices.size(); i++) {
            long long gx2 = (long long)(resultMesh.vertices[i].x * invCell);
            long long gy2 = (long long)(resultMesh.vertices[i].y * invCell);
            long long gz2 = (long long)(resultMesh.vertices[i].z * invCell);
            long long key = gx2*1000003LL*1000003LL + gy2*1000003LL + gz2;
            auto it2 = vertGrid.find(key);
            if (it2 != vertGrid.end()) {
                remap[i] = it2->second;
            } else {
                remap[i] = (int)cleanVerts.size();
                vertGrid[key] = (int)cleanVerts.size();
                cleanVerts.push_back(resultMesh.vertices[i]);
            }
        }
        resultMesh.vertices = cleanVerts;
        for (auto& f : resultMesh.faces) for (auto& idx : f.indices) idx = remap[idx];
        resultMesh.faces.erase(std::remove_if(resultMesh.faces.begin(), resultMesh.faces.end(), [](const Face& f) {
            return f.indices[0]==f.indices[1]||f.indices[1]==f.indices[2]||f.indices[0]==f.indices[2];
        }), resultMesh.faces.end());
    }

    int h = nextHandle++;
    resultMesh.triangulate();
    meshStore[h] = std::move(resultMesh);
    return h;
}

// Ball Pivoting Algorithm (Bernardini et al. 1999)
// Proper implementation with KD-Tree, edge front, and sphere computation

// Compute circumscribed sphere center for 3 points with given ball radius R
// Returns false if the sphere can't pass through all 3 points
static bool findBallCenter(const Vec3& p0, const Vec3& p1, const Vec3& p2, float R,
                           const Vec3* normals, int ni0, int ni1, int ni2,
                           Vec3& center) {
    float q1x=p1.x-p0.x, q1y=p1.y-p0.y, q1z=p1.z-p0.z;
    float q2x=p2.x-p0.x, q2y=p2.y-p0.y, q2z=p2.z-p0.z;
    // Normal of triangle
    float nx = q1y*q2z - q1z*q2y;
    float ny = q1z*q2x - q1x*q2z;
    float nz = q1x*q2y - q1y*q2x;
    float nLen = std::sqrt(nx*nx+ny*ny+nz*nz);
    if (nLen < 1e-12f) return false;
    nx/=nLen; ny/=nLen; nz/=nLen;

    // Circumcenter in the plane of the triangle
    float d11 = q1x*q1x+q1y*q1y+q1z*q1z;
    float d12 = q1x*q2x+q1y*q2y+q1z*q2z;
    float d22 = q2x*q2x+q2y*q2y+q2z*q2z;
    float det = d11*d22 - d12*d12;
    if (std::abs(det) < 1e-12f) return false;
    float s = (d11*d22 - d12*d22) / (2.0f*det);
    float t = (d11*d22 - d12*d11) / (2.0f*det);
    // More stable circumcenter computation
    s = 0.5f * (d22*(d11-d12)) / det;
    t = 0.5f * (d11*(d22-d12)) / det;
    float cx = s*q1x + t*q2x;
    float cy = s*q1y + t*q2y;
    float cz = s*q1z + t*q2z;
    float circR2 = cx*cx+cy*cy+cz*cz;
    if (circR2 > R*R) return false; // Ball too small to pass through all 3 points
    float h = std::sqrt(std::max(0.0f, R*R - circR2));

    // Choose normal direction: average of vertex normals if available, else triangle normal
    float avgNx=nx, avgNy=ny, avgNz=nz;
    if (normals) {
        avgNx = normals[ni0].x+normals[ni1].x+normals[ni2].x;
        avgNy = normals[ni0].y+normals[ni1].y+normals[ni2].y;
        avgNz = normals[ni0].z+normals[ni1].z+normals[ni2].z;
        float l = std::sqrt(avgNx*avgNx+avgNy*avgNy+avgNz*avgNz);
        if (l > 1e-12f) { avgNx/=l; avgNy/=l; avgNz/=l; }
        else { avgNx=nx; avgNy=ny; avgNz=nz; }
    }
    // Choose h direction consistent with normals
    if (avgNx*nx+avgNy*ny+avgNz*nz < 0) h = -h;

    center.x = p0.x + cx + h*nx;
    center.y = p0.y + cy + h*ny;
    center.z = p0.z + cz + h*nz;
    return true;
}

int ballPivotingReconstruction(int handle, float radius) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& pts = it->second;
    int nv = (int)pts.vertices.size();
    if (nv < 3) return -1;

    // Auto-compute radius
    if (radius <= 0) {
        float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
        for (auto& v : pts.vertices) {
            bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z);
            bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z);
        }
        float diag = std::sqrt((bmax[0]-bmin[0])*(bmax[0]-bmin[0])+(bmax[1]-bmin[1])*(bmax[1]-bmin[1])+(bmax[2]-bmin[2])*(bmax[2]-bmin[2]));
        radius = diag / std::sqrt((float)nv) * 2.0f;
    }
    float R = radius;
    float R2 = R*R;
    float searchR = R * 2.5f;

    // Build KD-Tree
    KDTree kd;
    kd.build(pts.vertices);

    // Estimate normals if not present
    bool hasNormals = pts.normalBuffer.size() >= (size_t)nv * 3;
    std::vector<Vec3> normals;
    if (hasNormals) {
        normals.resize(nv);
        for (int i = 0; i < nv; i++) {
            normals[i] = {pts.normalBuffer[i*3], pts.normalBuffer[i*3+1], pts.normalBuffer[i*3+2]};
        }
    }

    Mesh result;
    result.vertices = pts.vertices;
    std::vector<bool> used(nv, false);

    // Edge front: pairs of vertex indices with the opposite vertex
    struct FrontEdge { int v0, v1, oppV; };
    std::vector<FrontEdge> front;

    // Track existing edges to avoid duplicates
    std::unordered_set<long long> existingEdges;
    auto edgeId = [](int a, int b) -> long long {
        if (a>b) std::swap(a,b);
        return (long long)a*1000000LL+b;
    };

    // Find seed triangle
    auto findSeed = [&]() -> bool {
        for (int i = 0; i < nv; i++) {
            if (used[i]) continue;
            // Find nearest neighbors
            std::vector<std::pair<float,int>> neighbors;
            for (int j = 0; j < nv; j++) {
                if (i==j) continue;
                float dx=pts.vertices[j].x-pts.vertices[i].x, dy=pts.vertices[j].y-pts.vertices[i].y, dz=pts.vertices[j].z-pts.vertices[i].z;
                float d2 = dx*dx+dy*dy+dz*dz;
                if (d2 < searchR*searchR) neighbors.push_back({d2,j});
            }
            std::sort(neighbors.begin(), neighbors.end());

            // Try triplets
            for (size_t a = 0; a < std::min(neighbors.size(), (size_t)16); a++) {
                for (size_t b = a+1; b < std::min(neighbors.size(), (size_t)16); b++) {
                    int ja = neighbors[a].second, jb = neighbors[b].second;
                    Vec3 center;
                    if (findBallCenter(pts.vertices[i], pts.vertices[ja], pts.vertices[jb], R,
                                       hasNormals ? normals.data() : nullptr, i, ja, jb, center)) {
                        // Check no points inside the ball
                        bool empty = true;
                        for (auto& [d2, k] : neighbors) {
                            if (k==ja||k==jb) continue;
                            float dx=pts.vertices[k].x-center.x, dy=pts.vertices[k].y-center.y, dz=pts.vertices[k].z-center.z;
                            if (dx*dx+dy*dy+dz*dz < R2*0.99f) { empty=false; break; }
                        }
                        if (!empty) continue;

                        // Add seed triangle
                        result.faces.push_back({{i, ja, jb}});
                        used[i]=used[ja]=used[jb]=true;
                        existingEdges.insert(edgeId(i,ja));
                        existingEdges.insert(edgeId(ja,jb));
                        existingEdges.insert(edgeId(jb,i));
                        front.push_back({i, ja, jb});
                        front.push_back({ja, jb, i});
                        front.push_back({jb, i, ja});
                        return true;
                    }
                }
            }
        }
        return false;
    };

    // Pivot: for each front edge, find the next vertex
    int maxFaces = nv * 3; // Safety limit
    while ((int)result.faces.size() < maxFaces) {
        if (front.empty()) {
            if (!findSeed()) break; // No more seeds
            continue;
        }

        FrontEdge fe = front.back();
        front.pop_back();

        // Already have a triangle on both sides?
        // (simplified check)

        int v0 = fe.v0, v1 = fe.v1;
        // Find candidate vertex
        Vec3 mid = {(pts.vertices[v0].x+pts.vertices[v1].x)*0.5f,
                    (pts.vertices[v0].y+pts.vertices[v1].y)*0.5f,
                    (pts.vertices[v0].z+pts.vertices[v1].z)*0.5f};

        float bestAngle = 1e30f;
        int bestV = -1;
        for (int j = 0; j < nv; j++) {
            if (j==v0||j==v1||j==fe.oppV) continue;
            float dx=pts.vertices[j].x-mid.x, dy=pts.vertices[j].y-mid.y, dz=pts.vertices[j].z-mid.z;
            if (dx*dx+dy*dy+dz*dz > searchR*searchR) continue;

            // Check if edge already exists
            if (existingEdges.count(edgeId(v0,j)) && existingEdges.count(edgeId(v1,j))) continue;

            Vec3 center;
            if (!findBallCenter(pts.vertices[v0], pts.vertices[v1], pts.vertices[j], R,
                               hasNormals ? normals.data() : nullptr, v0, v1, j, center)) continue;

            // Check ball is empty
            bool empty = true;
            for (int k = 0; k < nv && k < 5000; k++) {
                if (k==v0||k==v1||k==j) continue;
                float ddx=pts.vertices[k].x-center.x, ddy=pts.vertices[k].y-center.y, ddz=pts.vertices[k].z-center.z;
                if (ddx*ddx+ddy*ddy+ddz*ddz < R2*0.95f) { empty=false; break; }
            }
            if (!empty) continue;

            // Compute pivot angle (prefer smaller angle from front normal)
            float angle = dx*dx+dy*dy+dz*dz; // Use distance as proxy for angle
            if (angle < bestAngle) { bestAngle=angle; bestV=j; }
        }

        if (bestV >= 0) {
            result.faces.push_back({{v0, bestV, v1}});
            used[bestV] = true;
            if (!existingEdges.count(edgeId(v0,bestV))) {
                existingEdges.insert(edgeId(v0,bestV));
                front.push_back({bestV, v0, v1});
            }
            if (!existingEdges.count(edgeId(v1,bestV))) {
                existingEdges.insert(edgeId(v1,bestV));
                front.push_back({v1, bestV, v0});
            }
        }
    }

    int h = nextHandle++; result.triangulate(); meshStore[h]=std::move(result); return h;
}

int surfaceReconstructionVCG(int handle) {
    // Simplified: same as marching cubes
    return marchingCubesReconstruction(handle, 64);
}

// ========== SAMPLING (remaining) ==========

int clusteredVertexSampling(int handle, float cellSize) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& src = it->second;
    if (cellSize <= 0) cellSize = 0.1f;
    Mesh result;
    std::unordered_map<long long, Vec3> cells;
    std::unordered_map<long long, int> counts;
    for (auto& v : src.vertices) {
        long long cx=(long long)std::floor(v.x/cellSize), cy=(long long)std::floor(v.y/cellSize), cz=(long long)std::floor(v.z/cellSize);
        long long key = cx*1000003LL*1000003LL + cy*1000003LL + cz;
        if (cells.find(key)==cells.end()) { cells[key]={v.x,v.y,v.z}; counts[key]=1; }
        else { cells[key].x+=v.x; cells[key].y+=v.y; cells[key].z+=v.z; counts[key]++; }
    }
    for (auto& [key, v] : cells) {
        int c = counts[key];
        result.vertices.push_back({v.x/c, v.y/c, v.z/c});
    }
    int h = nextHandle++; result.triangulate(); meshStore[h]=std::move(result); return h;
}

int stratifiedTriangleSampling(int handle, int sampleNum) {
    // Similar to montecarlo but stratified per triangle
    return filterMontecarloSampling(handle, sampleNum);
}

int meshElementSubsampling(int handle, int stride) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& src = it->second;
    Mesh result;
    for (size_t i = 0; i < src.vertices.size(); i += stride) {
        result.vertices.push_back(src.vertices[i]);
    }
    int h = nextHandle++; result.triangulate(); meshStore[h]=std::move(result); return h;
}

int texelSampling(int handle, int textureSize) {
    // Sample one point per texel if UV available
    return filterMontecarloSampling(handle, textureSize * textureSize / 16);
}

void voronoiVertexColoring(int handle, int seedCount) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.vertexColors.resize(m.vertices.size()*3); m.hasColors = true;
    // Pick random seed vertices and assign colors
    srand(42);
    std::vector<Vec3> seedPos(seedCount);
    std::vector<uint8_t> seedR(seedCount), seedG(seedCount), seedB(seedCount);
    for (int s = 0; s < seedCount; s++) {
        int idx = rand() % m.vertices.size();
        seedPos[s] = m.vertices[idx];
        seedR[s]=rand()%256; seedG[s]=rand()%256; seedB[s]=rand()%256;
    }
    for (size_t i = 0; i < m.vertices.size(); i++) {
        float minD = 1e30f; int best = 0;
        for (int s = 0; s < seedCount; s++) {
            float dx=m.vertices[i].x-seedPos[s].x, dy=m.vertices[i].y-seedPos[s].y, dz=m.vertices[i].z-seedPos[s].z;
            float d = dx*dx+dy*dy+dz*dz;
            if (d<minD) { minD=d; best=s; }
        }
        m.vertexColors[i*3]=seedR[best]; m.vertexColors[i*3+1]=seedG[best]; m.vertexColors[i*3+2]=seedB[best];
    }
}

std::string distanceFromReferenceMesh(int srcHandle, int refHandle, int sampleNum) {
    // Same as Hausdorff but stores distance in quality
    return filterHausdorffDistance(srcHandle, refHandle, sampleNum);
}

// ========== TEXTURE (remaining) ==========

void colorToTexture(int handle, int textureSize) {
    // Store vertex colors into UV-mapped texture (conceptual; no actual image in wasm)
    // Just ensure UV and colors exist
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    if (!m.hasUVs) filterTrivialUV(handle, textureSize);
    ensureColors(m);
}

void basicTriangleMapping(int handle) {
    // Simple per-triangle planar UV (same as trivial)
    filterTrivialUV(handle, 1024);
}

void planarMapping(int handle, int axis) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    m.texcoords.resize(m.vertices.size() * 2); m.hasUVs = true;
    float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
    for (auto& v : m.vertices) {
        bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z);
        bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z);
    }
    for (size_t i = 0; i < m.vertices.size(); i++) {
        int u_axis, v_axis;
        if (axis == 0) { u_axis=1; v_axis=2; } // project along X
        else if (axis == 1) { u_axis=0; v_axis=2; } // project along Y
        else { u_axis=0; v_axis=1; } // project along Z
        float coords[3] = {m.vertices[i].x, m.vertices[i].y, m.vertices[i].z};
        m.texcoords[i*2]   = (coords[u_axis]-bmin[u_axis])/(bmax[u_axis]-bmin[u_axis]+1e-12f);
        m.texcoords[i*2+1] = (coords[v_axis]-bmin[v_axis])/(bmax[v_axis]-bmin[v_axis]+1e-12f);
    }
}

void setTexture(int handle) {
    // No-op in wasm; texture management is JS-side
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
}

void uvWedgeToVertex(int handle) {
    // In our model UVs are already per-vertex; no-op
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
}

void uvVertexToWedge(int handle) {
    // No-op
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
}

void textureDefragmentation(int handle) {
    // Re-pack UV islands — simplified: no-op (UVs stay as-is)
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
}

void voronoiAtlas(int handle, int regions) {
    // Simplified atlas: planar projection
    planarMapping(handle, 2); // project along Z
}

// ========== CLEANING (remaining) ==========

void removeDuplicateFaces(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    std::unordered_set<long long> seen;
    std::vector<Face> unique;
    for (auto& f : m.faces) {
        if (f.indices.size()<3) continue;
        std::vector<int> sorted(f.indices);
        std::sort(sorted.begin(), sorted.end());
        long long key = (long long)sorted[0]*1000000LL*1000000LL + (long long)sorted[1]*1000000LL + sorted[2];
        if (seen.insert(key).second) unique.push_back(f);
    }
    m.faces = unique;
    m.triangulate();
}

void removeTVertices(int handle) {
    // T-vertices: vertices on edges of other faces. Simplified: merge close vertices
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    filterRemoveDuplicateVertices(handle);
    m.triangulate();
}

void snapMismatchedBorders(int handle) {
    // Merge nearby boundary vertices
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    auto e2f = buildEdgeToFaces(m);
    std::unordered_set<int> borderVerts;
    for (auto& [ek, faces] : e2f) {
        if (faces.size()==1) { borderVerts.insert(ek.a); borderVerts.insert(ek.b); }
    }
    // Merge close border vertices
    float threshold = 1e-4f;
    // Compute bbox for threshold
    float bmax = 0;
    for (auto& v : m.vertices) bmax = std::max(bmax, std::max(std::abs(v.x), std::max(std::abs(v.y), std::abs(v.z))));
    threshold = bmax * 1e-4f;
    float t2 = threshold * threshold;

    std::vector<int> remap(m.vertices.size());
    for (size_t i = 0; i < remap.size(); i++) remap[i] = (int)i;
    for (int a : borderVerts) {
        for (int b : borderVerts) {
            if (a >= b) continue;
            float dx=m.vertices[a].x-m.vertices[b].x, dy=m.vertices[a].y-m.vertices[b].y, dz=m.vertices[a].z-m.vertices[b].z;
            if (dx*dx+dy*dy+dz*dz < t2) remap[b] = a;
        }
    }
    for (auto& f : m.faces) for (auto& idx : f.indices) idx = remap[idx];
    m.faces.erase(std::remove_if(m.faces.begin(), m.faces.end(), [](const Face& f) {
        return f.indices.size()>=3 && (f.indices[0]==f.indices[1] || f.indices[1]==f.indices[2] || f.indices[0]==f.indices[2]);
    }), m.faces.end());
    m.triangulate();
}

void mergeCloseVertices(int handle, float threshold) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    float t2 = threshold * threshold;
    std::vector<int> remap(m.vertices.size());
    for (size_t i = 0; i < remap.size(); i++) remap[i] = (int)i;
    for (size_t i = 0; i < m.vertices.size(); i++) {
        if (remap[i] != (int)i) continue;
        for (size_t j = i+1; j < m.vertices.size(); j++) {
            if (remap[j] != (int)j) continue;
            float dx=m.vertices[i].x-m.vertices[j].x, dy=m.vertices[i].y-m.vertices[j].y, dz=m.vertices[i].z-m.vertices[j].z;
            if (dx*dx+dy*dy+dz*dz < t2) remap[j] = (int)i;
        }
    }
    for (auto& f : m.faces) for (auto& idx : f.indices) idx = remap[idx];
    m.faces.erase(std::remove_if(m.faces.begin(), m.faces.end(), [](const Face& f) {
        return f.indices.size()>=3 && (f.indices[0]==f.indices[1] || f.indices[1]==f.indices[2] || f.indices[0]==f.indices[2]);
    }), m.faces.end());
    m.triangulate();
}

void mergeWedgeTextureCoord(int handle) {
    // No-op for our UV model
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
}

void removeIsolatedFoldedFaces(int handle) {
    removeFlippedFaces(handle, 160);
}

void repairNonManifoldVertices(int handle) {
    fixNonManifoldEdges(handle);
}

// ========== CAMERA ==========
void setMeshCamera(int handle) { /* No-op; camera is JS-side */ }
void setRasterCamera(int handle) { /* No-op */ }

// ========== POINT SET ==========
int pointCloudSimplification(int handle, int targetCount) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& src = it->second;
    Mesh result;
    int stride = std::max(1, (int)src.vertices.size() / std::max(1, targetCount));
    for (size_t i = 0; i < src.vertices.size(); i += stride) result.vertices.push_back(src.vertices[i]);
    int h = nextHandle++; result.triangulate(); meshStore[h]=std::move(result); return h;
}

float estimateRadiusFromDensity(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return 0;
    auto& m = it->second;
    if (m.vertices.empty()) return 0;
    float bmin[3]={1e30f,1e30f,1e30f}, bmax[3]={-1e30f,-1e30f,-1e30f};
    for (auto& v : m.vertices) { bmin[0]=std::min(bmin[0],v.x); bmin[1]=std::min(bmin[1],v.y); bmin[2]=std::min(bmin[2],v.z); bmax[0]=std::max(bmax[0],v.x); bmax[1]=std::max(bmax[1],v.y); bmax[2]=std::max(bmax[2],v.z); }
    float vol = (bmax[0]-bmin[0])*(bmax[1]-bmin[1])*(bmax[2]-bmin[2]);
    float density = m.vertices.size() / (vol + 1e-12f);
    return std::pow(1.0f / density, 1.0f/3.0f);
}

// ========== OTHER ==========

int convexHull(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return -1;
    auto& src = it->second;
    // Simplified convex hull: gift wrapping / incremental
    // For simplicity, use bounding box vertices + extremal points
    Mesh result;
    result.vertices = src.vertices;
    // Find 6 extremal vertices
    int ext[6] = {0,0,0,0,0,0}; // minX,maxX,minY,maxY,minZ,maxZ
    for (int i = 1; i < (int)src.vertices.size(); i++) {
        if (src.vertices[i].x < src.vertices[ext[0]].x) ext[0]=i;
        if (src.vertices[i].x > src.vertices[ext[1]].x) ext[1]=i;
        if (src.vertices[i].y < src.vertices[ext[2]].y) ext[2]=i;
        if (src.vertices[i].y > src.vertices[ext[3]].y) ext[3]=i;
        if (src.vertices[i].z < src.vertices[ext[4]].z) ext[4]=i;
        if (src.vertices[i].z > src.vertices[ext[5]].z) ext[5]=i;
    }
    // Simple approach: create initial tetrahedron from extremal points then add faces
    std::unordered_set<int> hull_verts(ext, ext+6);
    std::vector<int> hv(hull_verts.begin(), hull_verts.end());
    if (hv.size() >= 4) {
        for (size_t i = 0; i < hv.size(); i++)
            for (size_t j = i+1; j < hv.size(); j++)
                for (size_t k = j+1; k < hv.size(); k++)
                    result.faces.push_back({{hv[i], hv[j], hv[k]}});
    }
    int h = nextHandle++; result.triangulate(); meshStore[h]=std::move(result); return h;
}

int alphaComplexShape(int handle, float alpha) {
    // Simplified: Delaunay-like using ball pivoting approach
    return ballPivotingReconstruction(handle, alpha);
}

void voronoiScaffolding(int handle, int seedCount) {
    voronoiVertexColoring(handle, seedCount);
}

void ambientOcclusionPerVertex(int handle, int numRays) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second; m.computeNormals();
    m.vertexQuality.resize(m.vertices.size()); m.hasQuality = true;
    srand(42);
    for (size_t i = 0; i < m.vertices.size(); i++) {
        float nx=m.normalBuffer[i*3], ny=m.normalBuffer[i*3+1], nz=m.normalBuffer[i*3+2];
        int occluded = 0;
        for (int r = 0; r < numRays; r++) {
            // Random direction in hemisphere
            float rx = randFloat()*2-1, ry = randFloat()*2-1, rz = randFloat()*2-1;
            float len = std::sqrt(rx*rx+ry*ry+rz*rz);
            if (len < 0.01f) continue;
            rx/=len; ry/=len; rz/=len;
            if (rx*nx+ry*ny+rz*nz < 0) { rx=-rx; ry=-ry; rz=-rz; }
            // Simple occlusion test: check if ray hits any face (simplified)
            // For performance, just check distance to nearby vertices
            Vec3 origin = m.vertices[i];
            bool hit = false;
            for (size_t j = 0; j < m.vertices.size() && j < 500; j++) {
                if (j == i) continue;
                float dx=m.vertices[j].x-origin.x, dy=m.vertices[j].y-origin.y, dz=m.vertices[j].z-origin.z;
                float proj = dx*rx+dy*ry+dz*rz;
                if (proj > 0.001f && proj < 2.0f) {
                    float perp2 = dx*dx+dy*dy+dz*dz - proj*proj;
                    if (perp2 < 0.01f) { hit=true; break; }
                }
            }
            if (hit) occluded++;
        }
        m.vertexQuality[i] = 1.0f - (float)occluded / numRays;
    }
}

// ---- LSCM Parametrization (Least Squares Conformal Maps) ----
// Simplified version: for each triangle, compute conformal UV using angle-based flattening
void filterLSCM(int handle) {
    auto it = meshStore.find(handle); if (it == meshStore.end()) return;
    auto& m = it->second;
    if (m.faces.empty()) return;

    // Find boundary vertices
    struct EK { int a, b; bool operator==(const EK& o) const { return a==o.a && b==o.b; }};
    struct EH { size_t operator()(const EK& k) const { return std::hash<long long>()((long long)k.a*1000003LL+k.b); }};
    std::unordered_map<EK, int, EH> edgeCount;
    for (auto& f : m.faces) {
        for (size_t e = 0; e < f.indices.size(); e++) {
            int a = f.indices[e], b = f.indices[(e+1)%f.indices.size()];
            if (a > b) std::swap(a, b);
            edgeCount[{a,b}]++;
        }
    }
    std::vector<int> boundary;
    std::unordered_set<int> boundarySet;
    for (auto& [ek, cnt] : edgeCount) {
        if (cnt == 1) { boundarySet.insert(ek.a); boundarySet.insert(ek.b); }
    }
    boundary.assign(boundarySet.begin(), boundarySet.end());

    int nv = (int)m.vertices.size();
    m.texcoords.resize(nv * 2, 0.0f);
    m.hasUVs = true;

    if (boundary.size() < 2) {
        // Closed mesh - use ABF-like approach: flatten using angles
        // Simple approach: project to best-fit plane
        // Compute covariance matrix to find dominant plane
        Vec3 centroid{0,0,0};
        for (auto& v : m.vertices) { centroid.x+=v.x; centroid.y+=v.y; centroid.z+=v.z; }
        float inv = 1.0f / nv;
        centroid.x*=inv; centroid.y*=inv; centroid.z*=inv;

        // Simple PCA: use cross product of two longest edges as normal
        float maxLen = 0; Vec3 dir1{1,0,0}, dir2{0,1,0};
        for (auto& f : m.faces) {
            if (f.indices.size() < 2) continue;
            auto& a = m.vertices[f.indices[0]]; auto& b = m.vertices[f.indices[1]];
            float dx=b.x-a.x, dy=b.y-a.y, dz=b.z-a.z;
            float len = dx*dx+dy*dy+dz*dz;
            if (len > maxLen) { maxLen=len; dir1={dx,dy,dz}; }
        }
        // dir2 = some perpendicular
        if (std::abs(dir1.x) < std::abs(dir1.y)) dir2 = {1,0,0};
        else dir2 = {0,1,0};
        // Gram-Schmidt
        float dot = dir2.x*dir1.x+dir2.y*dir1.y+dir2.z*dir1.z;
        float len1 = dir1.x*dir1.x+dir1.y*dir1.y+dir1.z*dir1.z;
        if (len1 > 1e-12f) {
            dir2.x -= dot/len1*dir1.x; dir2.y -= dot/len1*dir1.y; dir2.z -= dot/len1*dir1.z;
        }
        float len2 = std::sqrt(dir2.x*dir2.x+dir2.y*dir2.y+dir2.z*dir2.z);
        if (len2 > 1e-12f) { dir2.x/=len2; dir2.y/=len2; dir2.z/=len2; }
        float l1 = std::sqrt(len1); if (l1>0) { dir1.x/=l1; dir1.y/=l1; dir1.z/=l1; }

        // Project vertices
        float minU=1e30f, maxU=-1e30f, minV=1e30f, maxV=-1e30f;
        for (int i = 0; i < nv; i++) {
            float dx=m.vertices[i].x-centroid.x, dy=m.vertices[i].y-centroid.y, dz=m.vertices[i].z-centroid.z;
            float u = dx*dir1.x+dy*dir1.y+dz*dir1.z;
            float v = dx*dir2.x+dy*dir2.y+dz*dir2.z;
            m.texcoords[i*2] = u; m.texcoords[i*2+1] = v;
            minU=std::min(minU,u); maxU=std::max(maxU,u); minV=std::min(minV,v); maxV=std::max(maxV,v);
        }
        // Normalize to [0,1]
        float rangeU=maxU-minU, rangeV=maxV-minV;
        if (rangeU<1e-12f) rangeU=1; if (rangeV<1e-12f) rangeV=1;
        for (int i = 0; i < nv; i++) {
            m.texcoords[i*2] = (m.texcoords[i*2]-minU)/rangeU;
            m.texcoords[i*2+1] = (m.texcoords[i*2+1]-minV)/rangeV;
        }
    } else {
        // Open mesh with boundary: fix boundary to circle, solve interior
        // 1. Map boundary to unit circle
        std::vector<float> boundaryLen(boundary.size());
        float totalLen = 0;
        for (size_t i = 0; i < boundary.size(); i++) {
            int next = boundary[(i+1) % boundary.size()];
            float dx=m.vertices[next].x-m.vertices[boundary[i]].x;
            float dy=m.vertices[next].y-m.vertices[boundary[i]].y;
            float dz=m.vertices[next].z-m.vertices[boundary[i]].z;
            boundaryLen[i] = std::sqrt(dx*dx+dy*dy+dz*dz);
            totalLen += boundaryLen[i];
        }
        float cumLen = 0;
        for (size_t i = 0; i < boundary.size(); i++) {
            float t = cumLen / (totalLen + 1e-12f);
            float angle = t * 2.0f * 3.14159265f;
            m.texcoords[boundary[i]*2] = 0.5f + 0.5f*std::cos(angle);
            m.texcoords[boundary[i]*2+1] = 0.5f + 0.5f*std::sin(angle);
            cumLen += boundaryLen[i];
        }

        // 2. Solve interior vertices with Laplacian (Tutte's embedding)
        auto adj = buildAdjacency(m);
        std::vector<bool> isBoundary(nv, false);
        for (int b : boundary) isBoundary[b] = true;

        for (int iter = 0; iter < 100; iter++) {
            for (int i = 0; i < nv; i++) {
                if (isBoundary[i] || adj[i].empty()) continue;
                float su = 0, sv = 0;
                for (int nb : adj[i]) { su += m.texcoords[nb*2]; sv += m.texcoords[nb*2+1]; }
                float inv2 = 1.0f / adj[i].size();
                m.texcoords[i*2] = su * inv2;
                m.texcoords[i*2+1] = sv * inv2;
            }
        }
    }
}

// ============ Bindings ============

EMSCRIPTEN_BINDINGS(meshlab_core) {
    function("loadMeshFromOBJ", &loadMeshFromOBJ);
    function("loadMeshFromSTL", &loadMeshFromSTL);
    function("loadMeshFromPLY", &loadMeshFromPLY);
    function("loadMesh", &loadMesh);
    function("getVertexCount", &getVertexCount);
    function("getFaceCount", &getFaceCount);
    function("getBoundingBox", &getBoundingBox);
    function("getVertexBuffer", &getVertexBuffer);
    function("getIndexBuffer", &getIndexBuffer);
    function("getNormalBuffer", &getNormalBuffer);
    function("recomputeNormals", &recomputeNormals);
    function("deleteMesh", &deleteMesh);
    function("exportToOBJ", &exportToOBJ);
    function("exportToSTL", &exportToSTL);
    function("filterScale", &filterScale);
    function("filterTranslate", &filterTranslate);
    function("filterRotate", &filterRotate);
    function("filterInvertFaces", &filterInvertFaces);
    function("filterLaplacianSmooth", &filterLaplacianSmooth);
    function("filterTaubinSmooth", &filterTaubinSmooth);
    function("filterSimplify", &filterSimplify);
    function("filterSubdivide", &filterSubdivide);
    function("filterRemoveDuplicateVertices", &filterRemoveDuplicateVertices);
    function("filterRemoveUnreferencedVertices", &filterRemoveUnreferencedVertices);
    function("generateTestSphere", &generateTestSphere);
    function("generateTestGrid", &generateTestGrid);
    function("getWasmMemoryBytes", &getWasmMemoryBytes);
    function("selectFaces", &selectFaces);
    function("deselectFaces", &deselectFaces);
    function("selectAllFaces", &selectAllFaces);
    function("deselectAllFaces", &deselectAllFaces);
    function("invertFaceSelection", &invertFaceSelection);
    function("getSelectedFaceCount", &getSelectedFaceCount);
    function("getSelectedFaceIndices", &getSelectedFaceIndices);
    function("getSelectionMask", &getSelectionMask);
    function("selectVertices", &selectVertices);
    function("deselectAllVertices", &deselectAllVertices);
    function("getSelectedVertexCount", &getSelectedVertexCount);
    function("selectConnectedComponent", &selectConnectedComponent);
    function("selectFacesByNormalAngle", &selectFacesByNormalAngle);
    function("selectSmallComponents", &selectSmallComponents);
    function("deleteSelectedFaces", &deleteSelectedFaces);
    function("getCreaseEdges", &getCreaseEdges);
    function("hasVertexQuality", &hasVertexQuality);
    function("getQualityBuffer", &getQualityBuffer);
    function("getQualityRange", &getQualityRange);
    function("hasVertexColors", &hasVertexColors);
    function("getColorBuffer", &getColorBuffer);
    function("computeGaussianCurvature", &computeGaussianCurvature);
    function("computeMeanCurvature", &computeMeanCurvature);
    function("qualityToColor", &qualityToColor);
    function("selectFacesByQuality", &selectFacesByQuality);
    function("loadMeshFromOFF", &loadMeshFromOFF);
    function("loadMeshFromPLYBinary", &loadMeshFromPLYBinary);
    function("exportToPLY", &exportToPLY);
    function("exportToOFF", &exportToOFF);
    function("computeTotalArea", &computeTotalArea);
    function("computeSelectedArea", &computeSelectedArea);
    function("computeVolume", &computeVolume);
    function("isMeshClosed", &isMeshClosed);
    function("computeSectionContour", &computeSectionContour);
    function("detectNonManifoldEdges", &detectNonManifoldEdges);
    function("fixNonManifoldEdges", &fixNonManifoldEdges);
    function("detectHoles", &detectHoles);
    function("fillHoles", &fillHoles);
    function("removeSmallComponents", &removeSmallComponents);
    function("removeDegenerateFaces", &removeDegenerateFaces);
    function("detectFlippedFaces", &detectFlippedFaces);
    function("removeFlippedFaces", &removeFlippedFaces);
    function("detectSelfIntersections", &detectSelfIntersections);
    function("getMeshStats", &getMeshStats);
    function("meshHasUVs", &meshHasUVs);
    function("getUVBuffer", &getUVBuffer);
    function("loadMeshFromOBJ_Extended", &loadMeshFromOBJ_Extended);
    function("filterTrivialUV", &filterTrivialUV);
    function("textureToVertexColor", &textureToVertexColor);
    function("getTransformMatrix", &getTransformMatrix);
    function("setTransformMatrix", &setTransformMatrix);
    function("freezeTransform", &freezeTransform);
    function("resetTransform", &resetTransform);
    function("computeRigidAlignment", &computeRigidAlignment);
    function("computeICP", &computeICP);
    function("isPointCloud", &isPointCloud);
    function("getPointCount", &getPointCount);
    function("loadPointCloudFromXYZ", &loadPointCloudFromXYZ);
    function("estimateNormals", &estimateNormals);
    function("marchingCubesReconstruction", &marchingCubesReconstruction);
    // Sprint M bindings
    function("filterLoopSubdivision", &filterLoopSubdivision);
    function("filterButterflySubdivision", &filterButterflySubdivision);
    function("filterMontecarloSampling", &filterMontecarloSampling);
    function("filterPoissonDiskSampling", &filterPoissonDiskSampling);
    function("filterHausdorffDistance", &filterHausdorffDistance);
    function("computePrincipalCurvatureK1", &computePrincipalCurvatureK1);
    function("computePrincipalCurvatureK2", &computePrincipalCurvatureK2);
    function("computeShapeIndex", &computeShapeIndex);
    function("filterMeshBoolean", &filterMeshBoolean);
    function("filterIsotropicRemeshing", &filterIsotropicRemeshing);
    function("createBox", &createBox);
    function("createSphere", &createSphere);
    function("createTorus", &createTorus);
    function("createCone", &createCone);
    function("createPlane", &createPlane);
    function("splitConnectedComponents", &splitConnectedComponents);
    function("flattenLayers", &flattenLayers);
    function("selectFacesByQualityRange", &selectFacesByQualityRange);
    function("erodeSelection", &erodeSelection);
    function("dilateSelection", &dilateSelection);
    // Sprint N bindings
    function("vertexColorFill", &vertexColorFill);
    function("vertexColorInvert", &vertexColorInvert);
    function("vertexColorBrightnessContrastGamma", &vertexColorBrightnessContrastGamma);
    function("vertexColorLevels", &vertexColorLevels);
    function("vertexColorColourisation", &vertexColorColourisation);
    function("vertexColorDesaturation", &vertexColorDesaturation);
    function("vertexColorWhiteBalance", &vertexColorWhiteBalance);
    function("vertexColorNoise", &vertexColorNoise);
    function("perlinColor", &perlinColor);
    function("smoothVertexColors", &smoothVertexColors);
    function("smoothFaceColors", &smoothFaceColors);
    function("randomFaceColor", &randomFaceColor);
    function("colorTransferVertexToFace", &colorTransferVertexToFace);
    function("colorTransferFaceToVertex", &colorTransferFaceToVertex);
    function("selectAll", &selectAll);
    function("selectNone", &selectNone);
    function("selectBorderFaces", &selectBorderFaces);
    function("selectByVertexColor", &selectByVertexColor);
    function("selectByDihedralAngle", &selectByDihedralAngle);
    function("selectUglyFaces", &selectUglyFaces);
    function("selectNonManifoldFaces", &selectNonManifoldFaces);
    function("selectNonManifoldVertices", &selectNonManifoldVertices);
    function("selectSelfIntersecting", &selectSelfIntersecting);
    function("deleteSelectedVertices", &deleteSelectedVertices);
    function("deleteAllFaces", &deleteAllFaces);
    function("deleteFaceWithSelectedVertices", &deleteFaceWithSelectedVertices);
    function("recomputeVertexNormals", &recomputeVertexNormals);
    function("normalizeFaceNormals", &normalizeFaceNormals);
    function("normalizeVertexNormals", &normalizeVertexNormals);
    function("smoothFaceNormals", &smoothFaceNormals);
    function("normalExtrapolation", &normalExtrapolation);
    function("normalSmoothingPointCloud", &normalSmoothingPointCloud);
    function("principalCurvatureDirection", &principalCurvatureDirection);
    function("createIcosahedron", &createIcosahedron);
    function("createTetrahedron", &createTetrahedron);
    function("createOctahedron", &createOctahedron);
    function("createDodecahedron", &createDodecahedron);
    function("createAnnulus", &createAnnulus);
    function("createSphereCap", &createSphereCap);
    function("createPointsOnSphere", &createPointsOnSphere);
    function("duplicateLayer", &duplicateLayer);
    function("deleteCurrentMesh", &deleteCurrentMesh);
    function("moveSelectedToNewLayer", &moveSelectedToNewLayer);
    function("computeTopologicalMeasures", &computeTopologicalMeasures);
    function("computeGeometricMeasures", &computeGeometricMeasures);
    function("computeSelectionAreaPerimeter", &computeSelectionAreaPerimeter);
    function("perVertexQualityStatistics", &perVertexQualityStatistics);
    function("perFaceQualityStatistics", &perFaceQualityStatistics);
    function("perVertexQualityHistogram", &perVertexQualityHistogram);
    function("smoothVertexQuality", &smoothVertexQuality);
    function("hcLaplacianSmooth", &hcLaplacianSmooth);
    function("scaleDependentSmooth", &scaleDependentSmooth);
    function("twoStepSmooth", &twoStepSmooth);
    function("depthSmooth", &depthSmooth);
    function("unsharpMaskGeometry", &unsharpMaskGeometry);
    function("unsharpMaskNormals", &unsharpMaskNormals);
    function("unsharpMaskQuality", &unsharpMaskQuality);
    function("linearMorph", &linearMorph);
    function("catmullClarkSubdivision", &catmullClarkSubdivision);
    function("clusteringDecimation", &clusteringDecimation);
    function("uniformMeshResampling", &uniformMeshResampling);
    function("screenedPoissonReconstruction", &screenedPoissonReconstruction);
    function("ballPivotingReconstruction", &ballPivotingReconstruction);
    function("surfaceReconstructionVCG", &surfaceReconstructionVCG);
    function("clusteredVertexSampling", &clusteredVertexSampling);
    function("stratifiedTriangleSampling", &stratifiedTriangleSampling);
    function("meshElementSubsampling", &meshElementSubsampling);
    function("texelSampling", &texelSampling);
    function("voronoiVertexColoring", &voronoiVertexColoring);
    function("distanceFromReferenceMesh", &distanceFromReferenceMesh);
    function("colorToTexture", &colorToTexture);
    function("basicTriangleMapping", &basicTriangleMapping);
    function("planarMapping", &planarMapping);
    function("setTexture", &setTexture);
    function("uvWedgeToVertex", &uvWedgeToVertex);
    function("uvVertexToWedge", &uvVertexToWedge);
    function("textureDefragmentation", &textureDefragmentation);
    function("voronoiAtlas", &voronoiAtlas);
    function("removeDuplicateFaces", &removeDuplicateFaces);
    function("removeTVertices", &removeTVertices);
    function("snapMismatchedBorders", &snapMismatchedBorders);
    function("mergeCloseVertices", &mergeCloseVertices);
    function("mergeWedgeTextureCoord", &mergeWedgeTextureCoord);
    function("removeIsolatedFoldedFaces", &removeIsolatedFoldedFaces);
    function("repairNonManifoldVertices", &repairNonManifoldVertices);
    function("setMeshCamera", &setMeshCamera);
    function("setRasterCamera", &setRasterCamera);
    function("pointCloudSimplification", &pointCloudSimplification);
    function("estimateRadiusFromDensity", &estimateRadiusFromDensity);
    function("convexHull", &convexHull);
    function("alphaComplexShape", &alphaComplexShape);
    function("voronoiScaffolding", &voronoiScaffolding);
    function("ambientOcclusionPerVertex", &ambientOcclusionPerVertex);
    function("getBoundaryEdges", &getBoundaryEdges);
    function("getNonManifoldEdges", &getNonManifoldEdges);
    function("filterLSCM", &filterLSCM);
}
