灵符lingfu

lingmodeler

↓ 0 downloads

LingModeler: box modeling with quads and tris, rigging + weight painting, 2D vector illustration and animation, procedural textures and game assets; .lmesh / .ling native formats, OBJ, glTF and SVG import / export

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodeler
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (67)

ling/meshops.ling
# ━━━ meshops.ling — box modeling operations on the active object ━━━
# Conventions: faces with 2 corners are loose edges (no fill); every op keeps uv per corner where it can,
# calls me_edges_dirty and leaves a sensible selection. Callers push undo first.

# ── helpers ──────────────────────────────────────────────────────────────────
# edge -> adjacent faces (up to 2): returns [ef0, ef1]
fn mo_edge_faces(o) {
    me_rebuild_edges(o)
    bind ce = st_get(mk(o, "ce"), [])
    bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind ne = len(st_get(mk(o, "ea"), []))
    bind ef0 = me_fill(ne, 0.0 - 1.0)
    bind ef1 = me_fill(ne, 0.0 - 1.0)
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        if c >= 3.0 {
            bind k = 0.0
            while k < c {
                bind e = list_get(ce, s + k)
                if list_get(ef0, e) < 0.0 { bind ef0 = list_set(ef0, e, f) }
                else if list_get(ef1, e) < 0.0 { bind ef1 = list_set(ef1, e, f) }
                bind k = k + 1.0
            }
        }
        bind f = f + 1.0
    }
    return [ef0, ef1]
}
# rebuild an object from new face lists and a vertex remap (old index -> new index or -1 to drop).
# nfaces = flat list of corner lists terminated by -1 each:  a b c -1 d e f g -1 ...
# corner uv/fcol are reset for faces that are new (uvsrc = -1); existing faces may pass their old corner start via uvsrc list.
fn mo_compact(o, nvx, nvy, nvz, cfi, cfs, cfc, cuv, cfcol, cwb) {
    st_set(mk(o, "vx"), nvx) st_set(mk(o, "vy"), nvy) st_set(mk(o, "vz"), nvz)
    st_set(mk(o, "fi"), cfi) st_set(mk(o, "fs"), cfs) st_set(mk(o, "fc"), cfc)
    st_set(mk(o, "uv"), cuv)
    st_set(mk(o, "fcol"), cfcol)
    st_set(mk(o, "wb"), cwb)
    st_set(mk(o, "vsel"), me_zeros(len(nvx)))
    st_set(mk(o, "fsel"), me_zeros(len(cfc)))
    me_edges_dirty(o)
    me_rebuild_edges(o)
    st_set("dirty", 1.0)
}
# remove vertices that no face references (after a face deletion). keeps everything else.
fn mo_drop_unused(o, keep_isolated) {
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind nv = len(vx)
    bind used = me_zeros(nv)
    bind i = 0.0
    while i < len(fi) { bind used = list_set(used, list_get(fi, i), 1.0) bind i = i + 1.0 }
    bind remap = me_fill(nv, 0.0 - 1.0)
    bind nvx = list_new() bind nvy = list_new() bind nvz = list_new() bind nwb = list_new()
    bind i = 0.0
    while i < nv {
        bind keep = list_get(used, i) > 0.5
        if len(keep_isolated) > i { if list_get(keep_isolated, i) > 0.5 { bind keep = true } }
        if keep {
            bind remap = list_set(remap, i, len(nvx))
            bind nvx = list_push(nvx, list_get(vx, i)) bind nvy = list_push(nvy, list_get(vy, i)) bind nvz = list_push(nvz, list_get(vz, i))
            if len(wb) > 0.5 { bind k = 0.0 while k < 8.0 { bind nwb = list_push(nwb, list_get(wb, i * 8.0 + k)) bind k = k + 1.0 } }
        }
        bind i = i + 1.0
    }
    bind nfi = list_new()
    bind i = 0.0
    while i < len(fi) { bind nfi = list_push(nfi, list_get(remap, list_get(fi, i))) bind i = i + 1.0 }
    mo_compact(o, nvx, nvy, nvz, nfi, fs, fc, uv, fcol, nwb)
}
fn mo_avg_normal_faces(o, faces) {
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind n = [0.0, 0.0, 0.0]
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind n = v3_add(n, me_face_normal(vx, vy, vz, fi, list_get(fs, f), list_get(fc, f)))
        bind i = i + 1.0
    }
    bind l = v3_len(n)
    if l < 0.000001 { return [0.0, 1.0, 0.0] }
    return v3_scale(n, 1.0 / l)
}
# normal at a vertex (average of adjacent face normals), zero when none
fn mo_vertex_normal(o, v) {
    bind vf = me_vert_faces(o)
    bind starts = list_get(vf, 0) bind fl = list_get(vf, 1)
    bind faces = list_new()
    bind k = list_get(starts, v)
    while k < list_get(starts, v + 1.0) { bind faces = list_push(faces, list_get(fl, k)) bind k = k + 1.0 }
    if len(faces) < 0.5 { return [0.0, 0.0, 0.0] }
    return mo_avg_normal_faces(o, faces)
}
fn mo_push_face(fi, fs, fc, corners) {
    bind fs2 = list_push(fs, len(fi))
    bind fc2 = list_push(fc, len(corners))
    bind fi2 = fi
    bind k = 0.0
    while k < len(corners) { bind fi2 = list_push(fi2, list_get(corners, k)) bind k = k + 1.0 }
    return [fi2, fs2, fc2]
}
fn mo_uv_pad(uv, n) {
    bind out = uv
    bind k = 0.0
    while k < n * 2.0 { bind out = list_push(out, 0.0) bind k = k + 1.0 }
    return out
}
fn mo_wb_dup(wb, v) {
    # append a copy of vertex v's weights (or an empty slot when the mesh has no weights)
    if len(wb) < 0.5 { return wb }
    bind out = wb
    bind k = 0.0
    while k < 8.0 { bind out = list_push(out, list_get(wb, v * 8.0 + k)) bind k = k + 1.0 }
    return out
}
fn mo_wb_avg(wb, a, b) {
    if len(wb) < 0.5 { return wb }
    bind out = wb
    bind k = 0.0
    while k < 8.0 {
        bind va = list_get(wb, a * 8.0 + k) bind vb = list_get(wb, b * 8.0 + k)
        if k % 2.0 < 0.5 { bind out = list_push(out, va) } else { bind out = list_push(out, (va + vb) * 0.5) }
        bind k = k + 1.0
    }
    return out
}

# ── delete ───────────────────────────────────────────────────────────────────
# what: 0 vertices, 1 edges, 2 faces (selected elements)
fn mo_delete(o, what) {
    bind vsel = st_get(mk(o, "vsel"), [])
    bind fsel = st_get(mk(o, "fsel"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind esel = []
    bind ce = []
    if what == 1.0 { me_rebuild_edges(o) bind esel = st_get(mk(o, "esel"), []) bind ce = st_get(mk(o, "ce"), []) }
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nuv = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind drop = false
        if what == 2.0 { if f < len(fsel) { if list_get(fsel, f) > 0.5 { bind drop = true } } }
        bind k = 0.0
        while k < c {
            bind v = list_get(fi, s + k)
            if what == 0.0 { if list_get(vsel, v) > 0.5 { bind drop = true } }
            if what == 1.0 { if list_get(esel, list_get(ce, s + k)) > 0.5 { bind drop = true } }
            bind k = k + 1.0
        }
        if !drop {
            bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, c)
            bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
            bind k = 0.0
            while k < c {
                bind nfi = list_push(nfi, list_get(fi, s + k))
                bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0)) bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0 + 1.0))
                bind k = k + 1.0
            }
        }
        bind f = f + 1.0
    }
    st_set(mk(o, "fi"), nfi) st_set(mk(o, "fs"), nfs) st_set(mk(o, "fc"), nfc)
    st_set(mk(o, "uv"), nuv) st_set(mk(o, "fcol"), nfcol)
    # vertices: in vertex mode the selected ones go; otherwise only vertices that lost all their faces
    bind keep = me_zeros(len(vsel))
    if what != 0.0 {
        bind i = 0.0
        while i < len(vsel) { bind keep = list_set(keep, i, sel(list_get(vsel, i) > 0.5, 0.0, 1.0)) bind i = i + 1.0 }
    }
    mo_drop_unused(o, keep)
    st_set("status", "Deleted")
}

# ── extrude ──────────────────────────────────────────────────────────────────
fn mo_extrude(o, dist) {
    me_rebuild_edges(o)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind vsel = st_get(mk(o, "vsel"), []) bind fsel = st_get(mk(o, "fsel"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind ce = st_get(mk(o, "ce"), []) bind esel = st_get(mk(o, "esel"), [])
    bind faces = me_selected_faces(o)
    bind nv = len(vx)
    bind newv = me_fill(nv, 0.0 - 1.0)
    bind sm = st_get("sel_mode", 0.0)
    if len(faces) > 0.5 && sm != 0.0 {
        # region extrude: copy the region's vertices, re-point selected faces, add side quads on boundary edges
        bind nrm = mo_avg_normal_faces(o, faces)
        bind fsel_cnt = me_zeros(len(ea))
        bind i = 0.0
        while i < len(faces) {
            bind f = list_get(faces, i)
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind k = 0.0
            while k < c {
                bind e = list_get(ce, s + k)
                bind fsel_cnt = list_set(fsel_cnt, e, list_get(fsel_cnt, e) + 1.0)
                bind v = list_get(fi, s + k)
                if list_get(newv, v) < 0.0 {
                    bind newv = list_set(newv, v, len(vx))
                    bind vx = list_push(vx, list_get(vx, v) + list_get(nrm, 0) * dist)
                    bind vy = list_push(vy, list_get(vy, v) + list_get(nrm, 1) * dist)
                    bind vz = list_push(vz, list_get(vz, v) + list_get(nrm, 2) * dist)
                    bind wb = mo_wb_dup(wb, v)
                }
                bind k = k + 1.0
            }
            bind i = i + 1.0
        }
        # does a boundary edge also border an unselected face? then the region is "open" there -> side quad
        bind i = 0.0
        while i < len(faces) {
            bind f = list_get(faces, i)
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind k = 0.0
            while k < c {
                bind e = list_get(ce, s + k)
                if list_get(fsel_cnt, e) == 1.0 {
                    bind a = list_get(fi, s + k) bind b = list_get(fi, s + (k + 1.0) % c)
                    bind r = mo_push_face(fi, fs, fc, [b, a, list_get(newv, a), list_get(newv, b)])
                    bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                    bind uv = mo_uv_pad(uv, 4.0)
                    bind fcol = list_push(fcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
                }
                bind k = k + 1.0
            }
            bind i = i + 1.0
        }
        # re-point the selected faces to the copies
        bind i = 0.0
        while i < len(faces) {
            bind f = list_get(faces, i)
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind k = 0.0
            while k < c { bind fi = list_set(fi, s + k, list_get(newv, list_get(fi, s + k))) bind k = k + 1.0 }
            bind i = i + 1.0
        }
        bind nvsel = me_zeros(len(vx))
        bind i = 0.0
        while i < nv { if list_get(newv, i) >= 0.0 { bind nvsel = list_set(nvsel, list_get(newv, i), 1.0) } bind i = i + 1.0 }
        mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
        st_set(mk(o, "vsel"), nvsel)
        me_sel_sync_from_verts(o)
        bind nfsel = st_get(mk(o, "fsel"), [])
        bind i = 0.0
        while i < len(faces) { bind nfsel = list_set(nfsel, list_get(faces, i), 1.0) bind i = i + 1.0 }
        st_set(mk(o, "fsel"), nfsel)
        v3_begin_modal("g")
        st_set("tr_axis", 0.0 - 1.0)
        st_set("status", "Extruded " + num_str(len(faces)) + " faces along the normal - move the mouse, click to confirm")
        return 0.0
    }
    bind sedges = me_selected_edges(o)
    if len(sedges) > 0.5 && sm != 0.0 {
        # edge extrude: quads from each selected edge, direction = average normal of adjacent faces
        bind efa = mo_edge_faces(o)
        bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
        bind i = 0.0
        while i < len(sedges) {
            bind e = list_get(sedges, i)
            bind a = list_get(ea, e) bind b = list_get(eb, e)
            bind adj = list_new()
            if list_get(ef0, e) >= 0.0 { bind adj = list_push(adj, list_get(ef0, e)) }
            if list_get(ef1, e) >= 0.0 { bind adj = list_push(adj, list_get(ef1, e)) }
            bind nrm = sel(len(adj) > 0.5, mo_avg_normal_faces(o, adj), v3_cam_up())
            bind k = 0.0
            while k < 2.0 {
                bind v = sel(k < 0.5, a, b)
                if list_get(newv, v) < 0.0 {
                    bind newv = list_set(newv, v, len(vx))
                    bind vx = list_push(vx, list_get(vx, v) + list_get(nrm, 0) * dist)
                    bind vy = list_push(vy, list_get(vy, v) + list_get(nrm, 1) * dist)
                    bind vz = list_push(vz, list_get(vz, v) + list_get(nrm, 2) * dist)
                    bind wb = mo_wb_dup(wb, v)
                }
                bind k = k + 1.0
            }
            # winding: opposite to the single adjacent face, if any
            bind flipq = false
            if len(adj) == 1.0 {
                bind f = list_get(adj, 0)
                bind s = list_get(fs, f) bind c = list_get(fc, f)
                bind k = 0.0
                while k < c { if list_get(fi, s + k) == a && list_get(fi, s + (k + 1.0) % c) == b { bind flipq = true } bind k = k + 1.0 }
            }
            bind quad = [a, b, list_get(newv, b), list_get(newv, a)]
            if flipq { bind quad = [b, a, list_get(newv, a), list_get(newv, b)] }
            bind r = mo_push_face(fi, fs, fc, quad)
            bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
            bind uv = mo_uv_pad(uv, 4.0)
            bind fcol = list_push(fcol, 0.0 - 1.0)
            bind i = i + 1.0
        }
        bind nvsel = me_zeros(len(vx))
        bind i = 0.0
        while i < nv { if list_get(newv, i) >= 0.0 { bind nvsel = list_set(nvsel, list_get(newv, i), 1.0) } bind i = i + 1.0 }
        mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
        st_set(mk(o, "vsel"), nvsel)
        me_sel_sync_from_verts(o)
        v3_begin_modal("g")
        st_set("status", "Extruded " + num_str(len(sedges)) + " edges - move the mouse, click to confirm")
        return 0.0
    }
    # vertex extrude: each selected vertex grows a loose edge to a copy
    bind verts = me_selected_verts(o)
    if len(verts) < 0.5 { st_set("status", "Nothing selected to extrude") return 0.0 }
    bind i = 0.0
    while i < len(verts) {
        bind v = list_get(verts, i)
        bind nrm = mo_vertex_normal(o, v)
        bind nid = len(vx)
        bind newv = list_set(newv, v, nid)
        bind vx = list_push(vx, list_get(vx, v) + list_get(nrm, 0) * dist)
        bind vy = list_push(vy, list_get(vy, v) + list_get(nrm, 1) * dist)
        bind vz = list_push(vz, list_get(vz, v) + list_get(nrm, 2) * dist)
        bind wb = mo_wb_dup(wb, v)
        bind r = mo_push_face(fi, fs, fc, [v, nid])
        bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
        bind uv = mo_uv_pad(uv, 2.0)
        bind fcol = list_push(fcol, 0.0 - 1.0)
        bind i = i + 1.0
    }
    bind nvsel = me_zeros(len(vx))
    bind i = 0.0
    while i < nv { if list_get(newv, i) >= 0.0 { bind nvsel = list_set(nvsel, list_get(newv, i), 1.0) } bind i = i + 1.0 }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    v3_begin_modal("g")
    st_set("status", "Extruded " + num_str(len(verts)) + " vertices - move the mouse, click to confirm")
}

# ── inset (individual faces) ─────────────────────────────────────────────────
fn mo_inset(o, t) {
    bind faces = me_selected_faces(o)
    if len(faces) < 0.5 { st_set("status", "Select faces to inset") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind nvsel = me_zeros(len(vx))
    bind inner = list_new()
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        if c >= 3.0 {
            bind cx = 0.0 bind cy = 0.0 bind cz = 0.0
            bind k = 0.0
            while k < c { bind v = list_get(fi, s + k) bind cx = cx + list_get(vx, v) bind cy = cy + list_get(vy, v) bind cz = cz + list_get(vz, v) bind k = k + 1.0 }
            bind cx = cx / c bind cy = cy / c bind cz = cz / c
            bind newc = list_new()
            bind k = 0.0
            while k < c {
                bind v = list_get(fi, s + k)
                bind dx = cx - list_get(vx, v) bind dy = cy - list_get(vy, v) bind dz = cz - list_get(vz, v)
                bind l = max(0.000001, sqrt(dx * dx + dy * dy + dz * dz))
                bind tt = min(t, l * 0.95)
                bind nid = len(vx)
                bind vx = list_push(vx, list_get(vx, v) + dx / l * tt)
                bind vy = list_push(vy, list_get(vy, v) + dy / l * tt)
                bind vz = list_push(vz, list_get(vz, v) + dz / l * tt)
                bind wb = mo_wb_dup(wb, v)
                bind nvsel = list_push(nvsel, 1.0)
                bind newc = list_push(newc, nid)
                bind k = k + 1.0
            }
            # ring quads
            bind k = 0.0
            while k < c {
                bind a = list_get(fi, s + k) bind b = list_get(fi, s + (k + 1.0) % c)
                bind r = mo_push_face(fi, fs, fc, [a, b, list_get(newc, (k + 1.0) % c), list_get(newc, k)])
                bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                bind uv = mo_uv_pad(uv, 4.0)
                bind fcol = list_push(fcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
                bind k = k + 1.0
            }
            # the face itself becomes the inner polygon
            bind k = 0.0
            while k < c { bind fi = list_set(fi, s + k, list_get(newc, k)) bind k = k + 1.0 }
            bind inner = list_push(inner, f)
        }
        bind i = i + 1.0
    }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    bind nfsel = st_get(mk(o, "fsel"), [])
    bind i = 0.0
    while i < len(inner) { bind nfsel = list_set(nfsel, list_get(inner, i), 1.0) bind i = i + 1.0 }
    st_set(mk(o, "fsel"), nfsel)
    st_set("status", "Inset " + num_str(len(inner)) + " faces by " + fmt2(t))
}

# ── bevel (chamfer of selected edges) ────────────────────────────────────────
fn mo_bevel(o, w, segs) {
    me_rebuild_edges(o)
    bind sedges = me_selected_edges(o)
    if len(sedges) < 0.5 { st_set("status", "Select edges to bevel (edge mode, or faces: their edges)") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind ce = st_get(mk(o, "ce"), []) bind esel = st_get(mk(o, "esel"), [])
    bind nv = len(vx)
    bind nf = len(fc)
    # new corner vertex per (vertex, other-edge) for single-selected corners: key = v * ne + e, stored in st map
    st_set("bv_n", st_get("bv_n", 0.0) + 1.0)
    bind keyp = "bv" + num_str(st_get("bv_n", 0.0)) + "_"
    # per face corner: the replacement vertex id (or the original)
    bind rep = me_zeros(len(fi))
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 0.0
        while k < c {
            bind v = list_get(fi, s + k)
            bind prev = list_get(fi, s + (k + c - 1.0) % c)
            bind nxt = list_get(fi, s + (k + 1.0) % c)
            bind e_in = list_get(ce, s + (k + c - 1.0) % c)
            bind e_out = list_get(ce, s + k)
            bind sin_ = list_get(esel, e_in) > 0.5
            bind sout = list_get(esel, e_out) > 0.5
            bind r = v
            if c >= 3.0 && (sin_ || sout) {
                bind din = v3_norm([list_get(vx, prev) - list_get(vx, v), list_get(vy, prev) - list_get(vy, v), list_get(vz, prev) - list_get(vz, v)])
                bind dout = v3_norm([list_get(vx, nxt) - list_get(vx, v), list_get(vy, nxt) - list_get(vy, v), list_get(vz, nxt) - list_get(vz, v)])
                bind lin = v3_dist([list_get(vx, prev), list_get(vy, prev), list_get(vz, prev)], [list_get(vx, v), list_get(vy, v), list_get(vz, v)])
                bind lout = v3_dist([list_get(vx, nxt), list_get(vy, nxt), list_get(vz, nxt)], [list_get(vx, v), list_get(vy, v), list_get(vz, v)])
                bind ww = min(w, min(lin, lout) * 0.45)
                if sin_ && sout {
                    bind r = len(vx)
                    bind vx = list_push(vx, list_get(vx, v) + (list_get(din, 0) + list_get(dout, 0)) * ww)
                    bind vy = list_push(vy, list_get(vy, v) + (list_get(din, 1) + list_get(dout, 1)) * ww)
                    bind vz = list_push(vz, list_get(vz, v) + (list_get(din, 2) + list_get(dout, 2)) * ww)
                    bind wb = mo_wb_dup(wb, v)
                } else {
                    # move along the unselected edge; share the vertex with the face across that edge
                    bind oe = sel(sin_, e_out, e_in)
                    bind d = sel(sin_, dout, din)
                    bind key = keyp + num_str(v) + "_" + num_str(oe)
                    if st_has(key) > 0.5 { bind r = st_get(key, 0.0) }
                    else {
                        bind r = len(vx)
                        st_set(key, r)
                        bind vx = list_push(vx, list_get(vx, v) + list_get(d, 0) * ww)
                        bind vy = list_push(vy, list_get(vy, v) + list_get(d, 1) * ww)
                        bind vz = list_push(vz, list_get(vz, v) + list_get(d, 2) * ww)
                        bind wb = mo_wb_dup(wb, v)
                    }
                }
            }
            bind rep = list_set(rep, s + k, r)
            bind k = k + 1.0
        }
        bind f = f + 1.0
    }
    # bevel quads between the two faces of each selected edge
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    bind i = 0.0
    while i < len(sedges) {
        bind e = list_get(sedges, i)
        bind f1 = list_get(ef0, e) bind f2 = list_get(ef1, e)
        if f1 >= 0.0 && f2 >= 0.0 {
            bind a = list_get(ea, e) bind b = list_get(eb, e)
            # corner slots of a and b in f1 (a->b direction) and f2
            bind a1 = 0.0 - 1.0 bind b1 = 0.0 - 1.0 bind a2 = 0.0 - 1.0 bind b2 = 0.0 - 1.0
            bind s = list_get(fs, f1) bind c = list_get(fc, f1)
            bind k = 0.0
            while k < c {
                if list_get(ce, s + k) == e {
                    bind p = list_get(fi, s + k) bind q = list_get(fi, s + (k + 1.0) % c)
                    bind rp = list_get(rep, s + k) bind rq = list_get(rep, s + (k + 1.0) % c)
                    if p == a { bind a1 = rp bind b1 = rq } else { bind a1 = rq bind b1 = rp }
                }
                bind k = k + 1.0
            }
            bind s = list_get(fs, f2) bind c = list_get(fc, f2)
            bind k = 0.0
            while k < c {
                if list_get(ce, s + k) == e {
                    bind p = list_get(fi, s + k) bind q = list_get(fi, s + (k + 1.0) % c)
                    bind rp = list_get(rep, s + k) bind rq = list_get(rep, s + (k + 1.0) % c)
                    if p == a { bind a2 = rp bind b2 = rq } else { bind a2 = rq bind b2 = rp }
                }
                bind k = k + 1.0
            }
            # f1 traverses a->b when its stored direction is a->b; the quad must traverse the shared edges backwards
            bind dir1 = false
            bind s = list_get(fs, f1) bind c = list_get(fc, f1)
            bind k = 0.0
            while k < c { if list_get(fi, s + k) == a && list_get(fi, s + (k + 1.0) % c) == b { bind dir1 = true } bind k = k + 1.0 }
            bind quad = [b1, a1, a2, b2]
            if !dir1 { bind quad = [a1, b1, b2, a2] }
            bind r = mo_push_face(fi, fs, fc, quad)
            bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
            bind uv = mo_uv_pad(uv, 4.0)
            bind fcol = list_push(fcol, sel(f1 < len(fcol), list_get(fcol, f1), 0.0 - 1.0))
        }
        bind i = i + 1.0
    }
    # corner caps: vertices whose surrounding corners were all replaced by 3+ distinct points
    bind vf = me_vert_faces(o)
    bind vstarts = list_get(vf, 0) bind vfl = list_get(vf, 1)
    bind v = 0.0
    while v < nv {
        bind cnt = list_get(vstarts, v + 1.0) - list_get(vstarts, v)
        if cnt >= 3.0 {
            # walk the fan: from face f, leave through the edge (v -> next), enter the face across it
            bind f0 = list_get(vfl, list_get(vstarts, v))
            bind ring = list_new()
            bind cur = f0
            bind guard = 0.0
            bind go = true
            bind closed = false
            while go && guard < 64.0 {
                bind s = list_get(fs, cur) bind c = list_get(fc, cur)
                bind pos = 0.0 - 1.0
                bind k = 0.0
                while k < c { if list_get(fi, s + k) == v { bind pos = k } bind k = k + 1.0 }
                if pos < 0.0 || c < 3.0 { bind go = false }
                else {
                    bind ring = list_push(ring, list_get(rep, s + pos))
                    bind e_out = list_get(ce, s + pos)
                    bind nxtf = sel(list_get(ef0, e_out) == cur, list_get(ef1, e_out), list_get(ef0, e_out))
                    if nxtf < 0.0 { bind go = false }
                    else if nxtf == f0 { bind go = false bind closed = true }
                    else { bind cur = nxtf }
                }
                bind guard = guard + 1.0
            }
            # distinct points
            bind pts = list_new()
            bind k = 0.0
            while k < len(ring) {
                bind p = list_get(ring, k)
                bind dup = false
                bind q = 0.0
                while q < len(pts) { if list_get(pts, q) == p { bind dup = true } bind q = q + 1.0 }
                if !dup { bind pts = list_push(pts, p) }
                bind k = k + 1.0
            }
            bind changed = false
            bind k = 0.0
            while k < len(pts) { if list_get(pts, k) != v { bind changed = true } bind k = k + 1.0 }
            if closed && changed && len(pts) >= 3.0 {
                # reverse so the cap faces outward like its neighbours
                bind cap = list_new()
                bind k = len(pts) - 1.0
                while k >= 0.0 { bind cap = list_push(cap, list_get(pts, k)) bind k = k - 1.0 }
                bind r = mo_push_face(fi, fs, fc, cap)
                bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                bind uv = mo_uv_pad(uv, len(cap))
                bind fcol = list_push(fcol, 0.0 - 1.0)
            }
        }
        bind v = v + 1.0
    }
    # apply the corner replacements to the original faces
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 0.0
        while k < c { bind fi = list_set(fi, s + k, list_get(rep, s + k)) bind k = k + 1.0 }
        bind f = f + 1.0
    }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    mo_drop_unused(o, [])
    me_sel_all(o, false)
    st_set("status", "Bevelled " + num_str(len(sedges)) + " edges (width " + fmt2(w) + ")")
}

# ── loop cut ─────────────────────────────────────────────────────────────────
fn mo_loopcut(o, cuts) {
    me_rebuild_edges(o)
    bind sedges = me_selected_edges(o)
    if len(sedges) < 0.5 { st_set("status", "Select one edge (edge mode): the cut runs across its ring of quads") return 0.0 }
    bind e0 = list_get(sedges, 0)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind ce = st_get(mk(o, "ce"), [])
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    bind ne = len(ea)
    bind cuts = max(1.0, floor(cuts))
    # ring walk in both directions. per ring edge: oriented pair (sideA, sideB); per crossed quad: entry edge, exit edge
    bind esideA = me_fill(ne, 0.0 - 1.0)
    bind esideB = me_fill(ne, 0.0 - 1.0)
    bind quads = list_new()
    bind qin = list_new()
    bind qout = list_new()
    bind esideA = list_set(esideA, e0, list_get(ea, e0))
    bind esideB = list_set(esideB, e0, list_get(eb, e0))
    bind dir = 0.0
    while dir < 2.0 {
        bind cur = e0
        bind prevf = sel(dir < 0.5, list_get(ef1, e0), list_get(ef0, e0))
        bind go = true
        bind guard = 0.0
        while go && guard < 4000.0 {
            bind fa = list_get(ef0, cur) bind fb = list_get(ef1, cur)
            bind nf = sel(fa == prevf, fb, fa)
            bind isquad = false
            if nf >= 0.0 { if list_get(fc, nf) == 4.0 { bind isquad = true } }
            bind seen = false
            bind q = 0.0
            while q < len(quads) { if list_get(quads, q) == nf { bind seen = true } bind q = q + 1.0 }
            if !isquad || seen { bind go = false }
            else {
                bind s = list_get(fs, nf)
                bind pos = 0.0 - 1.0
                bind k = 0.0
                while k < 4.0 { if list_get(ce, s + k) == cur { bind pos = k } bind k = k + 1.0 }
                bind opp = list_get(ce, s + (pos + 2.0) % 4.0)
                # orientation: in this quad the entry edge runs p->q (corners pos, pos+1); the rail from p goes to corner pos+3
                bind p = list_get(fi, s + pos)
                bind A = list_get(esideA, cur)
                bind railA = sel(p == A, list_get(fi, s + (pos + 3.0) % 4.0), list_get(fi, s + (pos + 2.0) % 4.0))
                bind railB = sel(p == A, list_get(fi, s + (pos + 2.0) % 4.0), list_get(fi, s + (pos + 3.0) % 4.0))
                if list_get(esideA, opp) < 0.0 { bind esideA = list_set(esideA, opp, railA) bind esideB = list_set(esideB, opp, railB) }
                bind quads = list_push(quads, nf) bind qin = list_push(qin, cur) bind qout = list_push(qout, opp)
                bind prevf = nf
                bind cur = opp
                if cur == e0 { bind go = false }
            }
            bind guard = guard + 1.0
        }
        bind dir = dir + 1.0
    }
    if len(quads) < 0.5 { st_set("status", "No quad ring through that edge") return 0.0 }
    # new vertices on every ring edge (cuts per edge), keyed by edge: index of first new vertex
    bind efirst = me_fill(ne, 0.0 - 1.0)
    bind ring_edges = list_new()
    bind i = 0.0
    while i < len(quads) {
        bind k = 0.0
        while k < 2.0 {
            bind e = sel(k < 0.5, list_get(qin, i), list_get(qout, i))
            if list_get(efirst, e) < 0.0 {
                bind efirst = list_set(efirst, e, len(vx))
                bind ring_edges = list_push(ring_edges, e)
                bind A = list_get(esideA, e) bind B = list_get(esideB, e)
                bind j = 1.0
                while j <= cuts {
                    bind t = j / (cuts + 1.0)
                    bind vx = list_push(vx, list_get(vx, A) + (list_get(vx, B) - list_get(vx, A)) * t)
                    bind vy = list_push(vy, list_get(vy, A) + (list_get(vy, B) - list_get(vy, A)) * t)
                    bind vz = list_push(vz, list_get(vz, A) + (list_get(vz, B) - list_get(vz, A)) * t)
                    bind wb = mo_wb_avg(wb, A, B)
                    bind j = j + 1.0
                }
            }
            bind k = k + 1.0
        }
        bind i = i + 1.0
    }
    # rebuild faces: crossed quads become cuts+1 quads; other faces touching a ring edge get the new verts inserted
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nuv = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind qi = 0.0 - 1.0
        bind q = 0.0
        while q < len(quads) { if list_get(quads, q) == f { bind qi = q } bind q = q + 1.0 }
        bind col = sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0)
        if qi >= 0.0 {
            bind ein = list_get(qin, qi) bind eout = list_get(qout, qi)
            # corner order so the quad reads (A_in, B_in, B_out, A_out) consistent with the face winding
            bind pos = 0.0 - 1.0
            bind k = 0.0
            while k < 4.0 { if list_get(ce, s + k) == ein { bind pos = k } bind k = k + 1.0 }
            bind p0 = list_get(fi, s + pos) bind p1 = list_get(fi, s + (pos + 1.0) % 4.0)
            bind p2 = list_get(fi, s + (pos + 2.0) % 4.0) bind p3 = list_get(fi, s + (pos + 3.0) % 4.0)
            # sequence of points along the entry edge from p0 to p1 and along the exit edge from p3 to p2
            bind in_pts = mo_edge_chain(ein, p0, p1, esideA, efirst, cuts)
            bind out_pts = mo_edge_chain(eout, p3, p2, esideA, efirst, cuts)
            bind j = 0.0
            while j <= cuts {
                bind corners = [list_get(in_pts, j), list_get(in_pts, j + 1.0), list_get(out_pts, j + 1.0), list_get(out_pts, j)]
                bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, 4.0) bind nfcol = list_push(nfcol, col)
                bind k = 0.0
                while k < 4.0 { bind nfi = list_push(nfi, list_get(corners, k)) bind nuv = list_push(nuv, 0.0) bind nuv = list_push(nuv, 0.0) bind k = k + 1.0 }
                bind j = j + 1.0
            }
        } else {
            bind corners = list_new()
            bind k = 0.0
            while k < c {
                bind a = list_get(fi, s + k) bind b = list_get(fi, s + (k + 1.0) % c)
                bind e = list_get(ce, s + k)
                bind corners = list_push(corners, a)
                if c >= 3.0 && list_get(efirst, e) >= 0.0 {
                    bind chain = mo_edge_chain(e, a, b, esideA, efirst, cuts)
                    bind j = 1.0
                    while j <= cuts { bind corners = list_push(corners, list_get(chain, j)) bind j = j + 1.0 }
                }
                bind k = k + 1.0
            }
            bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, len(corners)) bind nfcol = list_push(nfcol, col)
            bind k = 0.0
            while k < len(corners) { bind nfi = list_push(nfi, list_get(corners, k)) bind nuv = list_push(nuv, 0.0) bind nuv = list_push(nuv, 0.0) bind k = k + 1.0 }
        }
        bind f = f + 1.0
    }
    bind nvsel = me_zeros(len(vx))
    bind i = 0.0
    while i < len(ring_edges) {
        bind first = list_get(efirst, list_get(ring_edges, i))
        bind j = 0.0
        while j < cuts { bind nvsel = list_set(nvsel, first + j, 1.0) bind j = j + 1.0 }
        bind i = i + 1.0
    }
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, nuv, nfcol, wb)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    st_set("sel_mode", 0.0)
    st_set("status", "Loop cut: " + num_str(len(quads)) + " quads, " + num_str(cuts) + " cut(s) - G slides the new loop")
}
# points along edge e from vertex a to vertex b including both ends: [a, n1.., b]
fn mo_edge_chain(e, a, b, esideA, efirst, cuts) {
    bind out = [a]
    bind first = list_get(efirst, e)
    bind forward = list_get(esideA, e) == a
    bind j = 0.0
    while j < cuts {
        bind idx = sel(forward, first + j, first + cuts - 1.0 - j)
        bind out = list_push(out, idx)
        bind j = j + 1.0
    }
    return list_push(out, b)
}

# ── subdivide (selected faces, or all) ───────────────────────────────────────
fn mo_subdivide(o) {
    me_rebuild_edges(o)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind ce = st_get(mk(o, "ce"), [])
    bind fsel = st_get(mk(o, "fsel"), [])
    bind sel_faces = me_selected_faces(o)
    bind all = len(sel_faces) < 0.5
    bind ne = len(ea)
    bind emid = me_fill(ne, 0.0 - 1.0)
    bind use = me_zeros(len(fc))
    bind f = 0.0
    while f < len(fc) {
        bind on = all
        if !on { if f < len(fsel) { if list_get(fsel, f) > 0.5 { bind on = true } } }
        if list_get(fc, f) < 3.0 { bind on = false }
        if on {
            bind use = list_set(use, f, 1.0)
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind k = 0.0
            while k < c {
                bind e = list_get(ce, s + k)
                if list_get(emid, e) < 0.0 {
                    bind a = list_get(ea, e) bind b = list_get(eb, e)
                    bind emid = list_set(emid, e, len(vx))
                    bind vx = list_push(vx, (list_get(vx, a) + list_get(vx, b)) * 0.5)
                    bind vy = list_push(vy, (list_get(vy, a) + list_get(vy, b)) * 0.5)
                    bind vz = list_push(vz, (list_get(vz, a) + list_get(vz, b)) * 0.5)
                    bind wb = mo_wb_avg(wb, a, b)
                }
                bind k = k + 1.0
            }
        }
        bind f = f + 1.0
    }
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind nvsel = me_zeros(len(vx))
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind col = sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0)
        if list_get(use, f) > 0.5 {
            bind cx = 0.0 bind cy = 0.0 bind cz = 0.0
            bind k = 0.0
            while k < c { bind v = list_get(fi, s + k) bind cx = cx + list_get(vx, v) bind cy = cy + list_get(vy, v) bind cz = cz + list_get(vz, v) bind k = k + 1.0 }
            bind centre = len(vx)
            bind vx = list_push(vx, cx / c) bind vy = list_push(vy, cy / c) bind vz = list_push(vz, cz / c)
            bind wb = mo_wb_dup(wb, list_get(fi, s))
            bind nvsel = list_push(nvsel, 1.0)
            bind k = 0.0
            while k < c {
                bind v = list_get(fi, s + k)
                bind m_prev = list_get(emid, list_get(ce, s + (k + c - 1.0) % c))
                bind m_next = list_get(emid, list_get(ce, s + k))
                bind r = mo_push_face(nfi, nfs, nfc, [v, m_next, centre, m_prev])
                bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                bind nfcol = list_push(nfcol, col)
                bind nvsel = list_set(nvsel, v, 1.0)
                bind nvsel = list_set(nvsel, m_next, 1.0)
                bind k = k + 1.0
            }
        } else {
            bind corners = list_new()
            bind k = 0.0
            while k < c {
                bind corners = list_push(corners, list_get(fi, s + k))
                bind m = list_get(emid, list_get(ce, s + k))
                if c >= 3.0 && m >= 0.0 { bind corners = list_push(corners, m) }
                bind k = k + 1.0
            }
            bind r = mo_push_face(nfi, nfs, nfc, corners)
            bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
            bind nfcol = list_push(nfcol, col)
        }
        bind f = f + 1.0
    }
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, wb)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    st_set("status", "Subdivided")
}

# ── Catmull-Clark (whole mesh) ───────────────────────────────────────────────
fn mo_subsurf(o) {
    me_rebuild_edges(o)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind ce = st_get(mk(o, "ce"), [])
    bind nv = len(vx) bind ne = len(ea) bind nf = len(fc)
    if nf < 0.5 { return 0.0 }
    # face points
    bind fpx = me_zeros(nf) bind fpy = me_zeros(nf) bind fpz = me_zeros(nf)
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind cx = 0.0 bind cy = 0.0 bind cz = 0.0
        bind k = 0.0
        while k < c { bind v = list_get(fi, s + k) bind cx = cx + list_get(vx, v) bind cy = cy + list_get(vy, v) bind cz = cz + list_get(vz, v) bind k = k + 1.0 }
        bind fpx = list_set(fpx, f, cx / c) bind fpy = list_set(fpy, f, cy / c) bind fpz = list_set(fpz, f, cz / c)
        bind f = f + 1.0
    }
    # edge -> faces
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    # edge points
    bind epx = me_zeros(ne) bind epy = me_zeros(ne) bind epz = me_zeros(ne)
    bind e = 0.0
    while e < ne {
        bind a = list_get(ea, e) bind b = list_get(eb, e)
        bind mx2 = (list_get(vx, a) + list_get(vx, b)) * 0.5
        bind my2 = (list_get(vy, a) + list_get(vy, b)) * 0.5
        bind mz2 = (list_get(vz, a) + list_get(vz, b)) * 0.5
        bind f1 = list_get(ef0, e) bind f2 = list_get(ef1, e)
        if f1 >= 0.0 && f2 >= 0.0 {
            bind epx = list_set(epx, e, (mx2 * 2.0 + list_get(fpx, f1) + list_get(fpx, f2)) * 0.25)
            bind epy = list_set(epy, e, (my2 * 2.0 + list_get(fpy, f1) + list_get(fpy, f2)) * 0.25)
            bind epz = list_set(epz, e, (mz2 * 2.0 + list_get(fpz, f1) + list_get(fpz, f2)) * 0.25)
        } else {
            bind epx = list_set(epx, e, mx2) bind epy = list_set(epy, e, my2) bind epz = list_set(epz, e, mz2)
        }
        bind e = e + 1.0
    }
    # vertex points: (F + 2R + (n-3)P) / n, boundary: (P*6 + M1 + M2)/8 with boundary edge midpoints
    bind sfx = me_zeros(nv) bind sfy = me_zeros(nv) bind sfz = me_zeros(nv) bind vnf = me_zeros(nv)
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 0.0
        while k < c {
            bind v = list_get(fi, s + k)
            bind sfx = list_set(sfx, v, list_get(sfx, v) + list_get(fpx, f)) bind sfy = list_set(sfy, v, list_get(sfy, v) + list_get(fpy, f)) bind sfz = list_set(sfz, v, list_get(sfz, v) + list_get(fpz, f))
            bind vnf = list_set(vnf, v, list_get(vnf, v) + 1.0)
            bind k = k + 1.0
        }
        bind f = f + 1.0
    }
    bind srx = me_zeros(nv) bind sry = me_zeros(nv) bind srz = me_zeros(nv) bind vne = me_zeros(nv)
    bind bx = me_zeros(nv) bind by = me_zeros(nv) bind bz = me_zeros(nv) bind vnb = me_zeros(nv)
    bind e = 0.0
    while e < ne {
        bind a = list_get(ea, e) bind b = list_get(eb, e)
        bind mx2 = (list_get(vx, a) + list_get(vx, b)) * 0.5 bind my2 = (list_get(vy, a) + list_get(vy, b)) * 0.5 bind mz2 = (list_get(vz, a) + list_get(vz, b)) * 0.5
        bind k = 0.0
        while k < 2.0 {
            bind v = sel(k < 0.5, a, b)
            bind srx = list_set(srx, v, list_get(srx, v) + mx2) bind sry = list_set(sry, v, list_get(sry, v) + my2) bind srz = list_set(srz, v, list_get(srz, v) + mz2)
            bind vne = list_set(vne, v, list_get(vne, v) + 1.0)
            if list_get(ef1, e) < 0.0 {
                bind bx = list_set(bx, v, list_get(bx, v) + mx2) bind by = list_set(by, v, list_get(by, v) + my2) bind bz = list_set(bz, v, list_get(bz, v) + mz2)
                bind vnb = list_set(vnb, v, list_get(vnb, v) + 1.0)
            }
            bind k = k + 1.0
        }
        bind e = e + 1.0
    }
    bind nvx = me_zeros(nv) bind nvy = me_zeros(nv) bind nvz = me_zeros(nv)
    bind v = 0.0
    while v < nv {
        bind n = list_get(vne, v)
        if list_get(vnb, v) > 0.5 {
            bind nvx = list_set(nvx, v, (list_get(vx, v) * 6.0 + list_get(bx, v)) / (6.0 + list_get(vnb, v)))
            bind nvy = list_set(nvy, v, (list_get(vy, v) * 6.0 + list_get(by, v)) / (6.0 + list_get(vnb, v)))
            bind nvz = list_set(nvz, v, (list_get(vz, v) * 6.0 + list_get(bz, v)) / (6.0 + list_get(vnb, v)))
        } else if n >= 3.0 && list_get(vnf, v) > 0.5 {
            bind F = [list_get(sfx, v) / list_get(vnf, v), list_get(sfy, v) / list_get(vnf, v), list_get(sfz, v) / list_get(vnf, v)]
            bind R = [list_get(srx, v) / n, list_get(sry, v) / n, list_get(srz, v) / n]
            bind nvx = list_set(nvx, v, (list_get(F, 0) + 2.0 * list_get(R, 0) + (n - 3.0) * list_get(vx, v)) / n)
            bind nvy = list_set(nvy, v, (list_get(F, 1) + 2.0 * list_get(R, 1) + (n - 3.0) * list_get(vy, v)) / n)
            bind nvz = list_set(nvz, v, (list_get(F, 2) + 2.0 * list_get(R, 2) + (n - 3.0) * list_get(vz, v)) / n)
        } else {
            bind nvx = list_set(nvx, v, list_get(vx, v)) bind nvy = list_set(nvy, v, list_get(vy, v)) bind nvz = list_set(nvz, v, list_get(vz, v))
        }
        bind v = v + 1.0
    }
    # assemble: original verts, then edge points, then face points
    bind e = 0.0
    while e < ne { bind nvx = list_push(nvx, list_get(epx, e)) bind nvy = list_push(nvy, list_get(epy, e)) bind nvz = list_push(nvz, list_get(epz, e)) bind wb = mo_wb_avg(wb, list_get(ea, e), list_get(eb, e)) bind e = e + 1.0 }
    bind f = 0.0
    while f < nf { bind nvx = list_push(nvx, list_get(fpx, f)) bind nvy = list_push(nvy, list_get(fpy, f)) bind nvz = list_push(nvz, list_get(fpz, f)) bind wb = mo_wb_dup(wb, list_get(fi, list_get(fs, f))) bind f = f + 1.0 }
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind col = sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0)
        if c >= 3.0 {
            bind k = 0.0
            while k < c {
                bind v = list_get(fi, s + k)
                bind e_prev = list_get(ce, s + (k + c - 1.0) % c)
                bind e_next = list_get(ce, s + k)
                bind r = mo_push_face(nfi, nfs, nfc, [v, nv + e_next, nv + ne + f, nv + e_prev])
                bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                bind nfcol = list_push(nfcol, col)
                bind k = k + 1.0
            }
        } else {
            bind r = mo_push_face(nfi, nfs, nfc, [list_get(fi, s), list_get(fi, s + 1.0)])
            bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
            bind nfcol = list_push(nfcol, col)
        }
        bind f = f + 1.0
    }
    mo_compact(o, nvx, nvy, nvz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, wb)
    mo_drop_unused(o, [])
    uv_box(o)
    st_set("status", "Catmull-Clark: " + num_str(me_nfaces(o)) + " faces")
}

# ── merge / weld ─────────────────────────────────────────────────────────────
fn mo_apply_remap(o, remap, nvx, nvy, nvz, nwb) {
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nuv = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind corners = list_new() bind cuv = list_new()
        bind k = 0.0
        while k < c {
            bind v = list_get(remap, list_get(fi, s + k))
            bind addc = true
            if len(corners) > 0.5 { if list_get(corners, len(corners) - 1.0) == v { bind addc = false } }
            if addc {
                bind corners = list_push(corners, v)
                bind cuv = list_push(cuv, list_get(uv, (s + k) * 2.0)) bind cuv = list_push(cuv, list_get(uv, (s + k) * 2.0 + 1.0))
            }
            bind k = k + 1.0
        }
        if len(corners) > 1.5 { if list_get(corners, 0) == list_get(corners, len(corners) - 1.0) {
            bind tmp = list_new() bind tuv = list_new()
            bind k = 0.0
            while k < len(corners) - 1.0 { bind tmp = list_push(tmp, list_get(corners, k)) bind tuv = list_push(tuv, list_get(cuv, k * 2.0)) bind tuv = list_push(tuv, list_get(cuv, k * 2.0 + 1.0)) bind k = k + 1.0 }
            bind corners = tmp bind cuv = tuv
        } }
        bind keepf = len(corners) >= 3.0 || (len(corners) == 2.0 && c == 2.0)
        if keepf {
            bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, len(corners))
            bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
            bind k = 0.0
            while k < len(corners) { bind nfi = list_push(nfi, list_get(corners, k)) bind nuv = list_push(nuv, list_get(cuv, k * 2.0)) bind nuv = list_push(nuv, list_get(cuv, k * 2.0 + 1.0)) bind k = k + 1.0 }
        }
        bind f = f + 1.0
    }
    mo_compact(o, nvx, nvy, nvz, nfi, nfs, nfc, nuv, nfcol, nwb)
}
fn mo_merge(o) {
    bind verts = me_selected_verts(o)
    if len(verts) < 2.0 { st_set("status", "Select at least two vertices to merge") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind wb = st_get(mk(o, "wb"), [])
    bind cx = 0.0 bind cy = 0.0 bind cz = 0.0
    bind i = 0.0
    while i < len(verts) { bind v = list_get(verts, i) bind cx = cx + list_get(vx, v) bind cy = cy + list_get(vy, v) bind cz = cz + list_get(vz, v) bind i = i + 1.0 }
    bind n = len(verts)
    bind target = list_get(verts, 0)
    bind vx = list_set(vx, target, cx / n) bind vy = list_set(vy, target, cy / n) bind vz = list_set(vz, target, cz / n)
    bind vsel = st_get(mk(o, "vsel"), [])
    bind remap = me_zeros(len(vx))
    bind nvx = list_new() bind nvy = list_new() bind nvz = list_new() bind nwb = list_new()
    bind i = 0.0
    while i < len(vx) {
        bind keep = list_get(vsel, i) < 0.5 || i == target
        if keep {
            bind remap = list_set(remap, i, len(nvx))
            bind nvx = list_push(nvx, list_get(vx, i)) bind nvy = list_push(nvy, list_get(vy, i)) bind nvz = list_push(nvz, list_get(vz, i))
            bind nwb = mo_wb_dup(wb, i)
        }
        bind i = i + 1.0
    }
    bind i = 0.0
    while i < len(vx) { if list_get(vsel, i) > 0.5 { bind remap = list_set(remap, i, list_get(remap, target)) } bind i = i + 1.0 }
    mo_apply_remap(o, remap, nvx, nvy, nvz, nwb)
    bind nvsel = st_get(mk(o, "vsel"), [])
    bind nvsel = list_set(nvsel, list_get(remap, target), 1.0)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    st_set("status", "Merged " + num_str(n) + " vertices")
}
fn mo_merge_dist(o, d) {
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind wb = st_get(mk(o, "wb"), [])
    bind n = len(vx)
    bind order = mx_argsort(vx)
    bind remap = me_fill(n, 0.0 - 1.0)
    bind nvx = list_new() bind nvy = list_new() bind nvz = list_new() bind nwb = list_new()
    bind i = 0.0
    while i < n {
        bind vi = list_get(order, i)
        if list_get(remap, vi) < 0.0 {
            bind id = len(nvx)
            bind nvx = list_push(nvx, list_get(vx, vi)) bind nvy = list_push(nvy, list_get(vy, vi)) bind nvz = list_push(nvz, list_get(vz, vi))
            bind nwb = mo_wb_dup(wb, vi)
            bind remap = list_set(remap, vi, id)
            bind j = i + 1.0
            bind go = true
            while go {
                if j >= n { bind go = false }
                else {
                    bind vj = list_get(order, j)
                    if list_get(vx, vj) - list_get(vx, vi) > d { bind go = false }
                    else {
                        if list_get(remap, vj) < 0.0 && abs(list_get(vy, vj) - list_get(vy, vi)) <= d && abs(list_get(vz, vj) - list_get(vz, vi)) <= d { bind remap = list_set(remap, vj, id) }
                        bind j = j + 1.0
                    }
                }
            }
        }
        bind i = i + 1.0
    }
    bind before = n
    mo_apply_remap(o, remap, nvx, nvy, nvz, nwb)
    st_set("status", "Merged " + num_str(before - len(nvx)) + " vertices by distance")
}

# ── dissolve (selected edges merge their faces; 2-edge vertices disappear) ───
fn mo_dissolve(o) {
    me_rebuild_edges(o)
    bind sedges = me_selected_edges(o)
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind ce = st_get(mk(o, "ce"), []) bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), [])
    bind fcol = st_get(mk(o, "fcol"), [])
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    bind vsel = st_get(mk(o, "vsel"), [])
    # union-find over faces joined by dissolved edges
    bind parent = list_new()
    bind f = 0.0
    while f < len(fc) { bind parent = list_push(parent, f) bind f = f + 1.0 }
    bind dis = me_zeros(len(ea))
    bind i = 0.0
    while i < len(sedges) {
        bind e = list_get(sedges, i)
        bind a = list_get(ef0, e) bind b = list_get(ef1, e)
        if a >= 0.0 && b >= 0.0 && a != b {
            bind dis = list_set(dis, e, 1.0)
            bind ra = mo_find(parent, a) bind rb = mo_find(parent, b)
            if ra != rb { bind parent = list_set(parent, ra, rb) }
        }
        bind i = i + 1.0
    }
    # for each group: walk the boundary (edges not dissolved) to build one polygon
    bind done = me_zeros(len(fc))
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind root = mo_find(parent, f)
        if list_get(done, root) < 0.5 {
            bind done = list_set(done, root, 1.0)
            bind single = true
            bind g = 0.0
            while g < len(fc) { if g != f && mo_find(parent, g) == root { bind single = false } bind g = g + 1.0 }
            bind col = sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0)
            if single {
                bind corners = list_new()
                bind s = list_get(fs, f) bind c = list_get(fc, f)
                bind k = 0.0
                while k < c { bind corners = list_push(corners, list_get(fi, s + k)) bind k = k + 1.0 }
                bind r = mo_push_face(nfi, nfs, nfc, corners)
                bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                bind nfcol = list_push(nfcol, col)
            } else {
                # directed boundary half-edges of the group: (from, to) for corners whose edge is not dissolved
                bind hfrom = list_new() bind hto = list_new()
                bind g = 0.0
                while g < len(fc) {
                    if mo_find(parent, g) == root {
                        bind s = list_get(fs, g) bind c = list_get(fc, g)
                        bind k = 0.0
                        while k < c {
                            if list_get(dis, list_get(ce, s + k)) < 0.5 {
                                bind hfrom = list_push(hfrom, list_get(fi, s + k))
                                bind hto = list_push(hto, list_get(fi, s + (k + 1.0) % c))
                            }
                            bind k = k + 1.0
                        }
                    }
                    bind g = g + 1.0
                }
                if len(hfrom) >= 3.0 {
                    bind used = me_zeros(len(hfrom))
                    bind corners = [list_get(hfrom, 0)]
                    bind cur = list_get(hto, 0)
                    bind used = list_set(used, 0, 1.0)
                    bind guard = 0.0
                    bind go = true
                    while go && guard < len(hfrom) {
                        bind corners = list_push(corners, cur)
                        bind nxt = 0.0 - 1.0
                        bind k = 0.0
                        while k < len(hfrom) { if nxt < 0.0 && list_get(used, k) < 0.5 && list_get(hfrom, k) == cur { bind nxt = k } bind k = k + 1.0 }
                        if nxt < 0.0 { bind go = false }
                        else { bind used = list_set(used, nxt, 1.0) bind cur = list_get(hto, nxt) if cur == list_get(corners, 0) { bind go = false } }
                        bind guard = guard + 1.0
                    }
                    bind r = mo_push_face(nfi, nfs, nfc, corners)
                    bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                    bind nfcol = list_push(nfcol, col)
                }
            }
        }
        bind f = f + 1.0
    }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, st_get(mk(o, "wb"), []))
    # dissolve selected vertices that now sit on a straight run (exactly two edges)
    mo_dissolve_verts(o, vsel)
    mo_drop_unused(o, [])
    st_set("status", "Dissolved")
}
fn mo_find(parent, i) {
    bind r = i
    bind guard = 0.0
    while list_get(parent, r) != r && guard < 100000.0 { bind r = list_get(parent, r) bind guard = guard + 1.0 }
    return r
}
fn mo_dissolve_verts(o, vsel) {
    bind nb = me_vert_neighbours(o)
    bind starts = list_get(nb, 0)
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind fcol = st_get(mk(o, "fcol"), [])
    bind drop = me_zeros(me_nverts(o))
    bind any = false
    bind v = 0.0
    while v < len(drop) {
        if v < len(vsel) { if list_get(vsel, v) > 0.5 && list_get(starts, v + 1.0) - list_get(starts, v) == 2.0 { bind drop = list_set(drop, v, 1.0) bind any = true } }
        bind v = v + 1.0
    }
    if !any { return 0.0 }
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind corners = list_new()
        bind k = 0.0
        while k < c { bind v = list_get(fi, s + k) if list_get(drop, v) < 0.5 { bind corners = list_push(corners, v) } bind k = k + 1.0 }
        if len(corners) >= 3.0 || (len(corners) == 2.0 && c == 2.0) {
            bind r = mo_push_face(nfi, nfs, nfc, corners)
            bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
            bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
        }
        bind f = f + 1.0
    }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, st_get(mk(o, "wb"), []))
}

# ── fill: a face from the selected vertices (ordered around their centre) ────
fn mo_fill(o) {
    bind verts = me_selected_verts(o)
    if len(verts) < 2.0 { st_set("status", "Select 2 vertices for an edge or 3+ for a face") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind corners = verts
    if len(verts) >= 3.0 {
        # best-fit plane: u = longest chord, n = u x (farthest point from that line)
        bind c = [0.0, 0.0, 0.0]
        bind i = 0.0
        while i < len(verts) { bind v = list_get(verts, i) bind c = v3_add(c, [list_get(vx, v), list_get(vy, v), list_get(vz, v)]) bind i = i + 1.0 }
        bind c = v3_scale(c, 1.0 / len(verts))
        bind far = 0.0 bind fd = 0.0 - 1.0
        bind i = 0.0
        while i < len(verts) {
            bind v = list_get(verts, i)
            bind d = v3_dist(c, [list_get(vx, v), list_get(vy, v), list_get(vz, v)])
            if d > fd { bind fd = d bind far = v }
            bind i = i + 1.0
        }
        bind u = v3_norm(v3_sub([list_get(vx, far), list_get(vy, far), list_get(vz, far)], c))
        bind n = [0.0, 0.0, 0.0] bind nd = 0.0 - 1.0
        bind i = 0.0
        while i < len(verts) {
            bind v = list_get(verts, i)
            bind p = v3_sub([list_get(vx, v), list_get(vy, v), list_get(vz, v)], c)
            bind cr = v3_cross(u, p)
            if v3_len(cr) > nd { bind nd = v3_len(cr) bind n = cr }
            bind i = i + 1.0
        }
        bind n = v3_norm(n)
        if v3_len(n) < 0.5 { bind n = v3_perp(u) }
        bind w = v3_cross(n, u)
        bind angs = list_new()
        bind i = 0.0
        while i < len(verts) {
            bind v = list_get(verts, i)
            bind p = v3_sub([list_get(vx, v), list_get(vy, v), list_get(vz, v)], c)
            bind angs = list_push(angs, mx_atan2(v3_dot(p, w), v3_dot(p, u)))
            bind i = i + 1.0
        }
        bind order = mx_argsort(angs)
        bind corners = list_new()
        bind i = 0.0
        while i < len(order) { bind corners = list_push(corners, list_get(verts, list_get(order, i))) bind i = i + 1.0 }
        # face the camera
        bind nrm = me_face_normal(vx, vy, vz, corners, 0.0, len(corners))
        bind cam = m_apply(m_inv(me_matrix(o)), v3_cam_pos())
        if v3_dot(nrm, v3_sub(cam, c)) < 0.0 {
            bind rev = list_new()
            bind i = len(corners) - 1.0
            while i >= 0.0 { bind rev = list_push(rev, list_get(corners, i)) bind i = i - 1.0 }
            bind corners = rev
        }
    }
    bind r = mo_push_face(fi, fs, fc, corners)
    st_set(mk(o, "fi"), list_get(r, 0)) st_set(mk(o, "fs"), list_get(r, 1)) st_set(mk(o, "fc"), list_get(r, 2))
    st_set(mk(o, "uv"), mo_uv_pad(st_get(mk(o, "uv"), []), len(corners)))
    st_set(mk(o, "fcol"), list_push(st_get(mk(o, "fcol"), []), 0.0 - 1.0))
    me_edges_dirty(o)
    me_rebuild_edges(o)
    me_sel_sync_from_verts(o)
    st_set("dirty", 1.0)
    st_set("status", sel(len(corners) == 2.0, "Added an edge", "Filled a face with " + num_str(len(corners)) + " corners"))
}

# ── connect two selected vertices across their shared face ───────────────────
fn mo_connect(o) {
    bind verts = me_selected_verts(o)
    if len(verts) != 2.0 { st_set("status", "Select exactly two vertices of one face to connect") return 0.0 }
    bind a = list_get(verts, 0) bind b = list_get(verts, 1)
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind f = 0.0
    bind hit = 0.0 - 1.0 bind pa = 0.0 - 1.0 bind pb = 0.0 - 1.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind ia = 0.0 - 1.0 bind ib = 0.0 - 1.0
        bind k = 0.0
        while k < c { bind v = list_get(fi, s + k) if v == a { bind ia = k } if v == b { bind ib = k } bind k = k + 1.0 }
        if hit < 0.0 && ia >= 0.0 && ib >= 0.0 && c >= 4.0 {
            # must not already be adjacent
            if (ia + 1.0) % c != ib && (ib + 1.0) % c != ia { bind hit = f bind pa = ia bind pb = ib }
        }
        bind f = f + 1.0
    }
    if hit < 0.0 { st_set("status", "Those vertices do not share a face (or are already connected)") return 0.0 }
    bind s = list_get(fs, hit) bind c = list_get(fc, hit)
    bind col = sel(hit < len(fcol), list_get(fcol, hit), 0.0 - 1.0)
    bind c1 = list_new() bind c2 = list_new()
    bind k = pa
    bind go = true
    while go { bind c1 = list_push(c1, list_get(fi, s + k)) if k == pb { bind go = false } else { bind k = (k + 1.0) % c } }
    bind k = pb
    bind go = true
    while go { bind c2 = list_push(c2, list_get(fi, s + k)) if k == pa { bind go = false } else { bind k = (k + 1.0) % c } }
    # rebuild faces replacing hit by c1 and appending c2
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s2 = list_get(fs, f) bind cc = list_get(fc, f)
        if f == hit {
            bind r = mo_push_face(nfi, nfs, nfc, c1) bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2) bind nfcol = list_push(nfcol, col)
        } else {
            bind corners = list_new()
            bind k = 0.0
            while k < cc { bind corners = list_push(corners, list_get(fi, s2 + k)) bind k = k + 1.0 }
            bind r = mo_push_face(nfi, nfs, nfc, corners) bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2) bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
        }
        bind f = f + 1.0
    }
    bind r = mo_push_face(nfi, nfs, nfc, c2) bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2) bind nfcol = list_push(nfcol, col)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, st_get(mk(o, "wb"), []))
    bind vsel = st_get(mk(o, "vsel"), [])
    bind vsel = list_set(vsel, a, 1.0) bind vsel = list_set(vsel, b, 1.0)
    st_set(mk(o, "vsel"), vsel)
    me_sel_sync_from_verts(o)
    st_set("status", "Connected")
}

# ── split (rip selected faces off their neighbours) ──────────────────────────
fn mo_split(o) {
    bind faces = me_selected_faces(o)
    if len(faces) < 0.5 { st_set("status", "Select faces to split off") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind wb = st_get(mk(o, "wb"), [])
    bind fsel = st_get(mk(o, "fsel"), [])
    bind nv = len(vx)
    bind in_sel = me_zeros(nv) bind in_other = me_zeros(nv)
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind on = false
        if f < len(fsel) { if list_get(fsel, f) > 0.5 { bind on = true } }
        bind k = 0.0
        while k < c { bind v = list_get(fi, s + k) if on { bind in_sel = list_set(in_sel, v, 1.0) } else { bind in_other = list_set(in_other, v, 1.0) } bind k = k + 1.0 }
        bind f = f + 1.0
    }
    bind newv = me_fill(nv, 0.0 - 1.0)
    bind v = 0.0
    while v < nv {
        if list_get(in_sel, v) > 0.5 && list_get(in_other, v) > 0.5 {
            bind newv = list_set(newv, v, len(vx))
            bind vx = list_push(vx, list_get(vx, v)) bind vy = list_push(vy, list_get(vy, v)) bind vz = list_push(vz, list_get(vz, v))
            bind wb = mo_wb_dup(wb, v)
        }
        bind v = v + 1.0
    }
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 0.0
        while k < c { bind v = list_get(fi, s + k) if list_get(newv, v) >= 0.0 { bind fi = list_set(fi, s + k, list_get(newv, v)) } bind k = k + 1.0 }
        bind i = i + 1.0
    }
    mo_compact(o, vx, vy, vz, fi, fs, fc, st_get(mk(o, "uv"), []), st_get(mk(o, "fcol"), []), wb)
    bind nfsel = st_get(mk(o, "fsel"), [])
    bind i = 0.0
    while i < len(faces) { bind nfsel = list_set(nfsel, list_get(faces, i), 1.0) bind i = i + 1.0 }
    st_set(mk(o, "fsel"), nfsel)
    bind nvsel = st_get(mk(o, "vsel"), [])
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 0.0
        while k < c { bind nvsel = list_set(nvsel, list_get(fi, s + k), 1.0) bind k = k + 1.0 }
        bind i = i + 1.0
    }
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
    st_set("status", "Split off " + num_str(len(faces)) + " faces")
}

# ── separate selected faces into a new object ────────────────────────────────
fn mo_separate(o) {
    bind faces = me_selected_faces(o)
    if len(faces) < 0.5 { st_set("status", "Select faces to separate") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind remap = me_fill(len(vx), 0.0 - 1.0)
    bind nvx = list_new() bind nvy = list_new() bind nvz = list_new()
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nuv = list_new() bind nfcol = list_new()
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, c) bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
        bind k = 0.0
        while k < c {
            bind v = list_get(fi, s + k)
            if list_get(remap, v) < 0.0 {
                bind remap = list_set(remap, v, len(nvx))
                bind nvx = list_push(nvx, list_get(vx, v)) bind nvy = list_push(nvy, list_get(vy, v)) bind nvz = list_push(nvz, list_get(vz, v))
            }
            bind nfi = list_push(nfi, list_get(remap, v))
            bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0)) bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0 + 1.0))
            bind k = k + 1.0
        }
        bind i = i + 1.0
    }
    mo_delete(o, 2.0)
    bind n = me_add_object(me_name(o) + "_part", nvx, nvy, nvz, nfi, nfs, nfc)
    st_set(mk(n, "uv"), nuv) st_set(mk(n, "fcol"), nfcol)
    bind keys = ["px", "py", "pz", "rx", "ry", "rz", "sx", "sy", "sz", "col", "tex", "smooth"]
    bind k = 0.0
    while k < len(keys) { st_set(mk(n, list_get(keys, k)), st_get(mk(o, list_get(keys, k)), 0.0)) bind k = k + 1.0 }
    st_set("sel_o", o)
    st_set("status", "Separated into " + me_name(n))
    return n
}

# ── join objects into the active one ─────────────────────────────────────────
fn mo_join(ids) {
    bind o = me_sel_o()
    if o < 0.0 || len(ids) < 2.0 { st_set("status", "Select two or more objects (the active one receives the others)") return 0.0 }
    me_undo_push()
    bind inv = m_inv(me_matrix(o))
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind i = 0.0
    while i < len(ids) {
        bind q = list_get(ids, i)
        if q != o {
            bind m = m_mul(inv, me_matrix(q))
            bind base = len(vx)
            bind qvx = st_get(mk(q, "vx"), []) bind qvy = st_get(mk(q, "vy"), []) bind qvz = st_get(mk(q, "vz"), [])
            bind k = 0.0
            while k < len(qvx) {
                bind p = m_apply(m, [list_get(qvx, k), list_get(qvy, k), list_get(qvz, k)])
                bind vx = list_push(vx, list_get(p, 0)) bind vy = list_push(vy, list_get(p, 1)) bind vz = list_push(vz, list_get(p, 2))
                bind k = k + 1.0
            }
            bind qfi = st_get(mk(q, "fi"), []) bind qfs = st_get(mk(q, "fs"), []) bind qfc = st_get(mk(q, "fc"), [])
            bind quv = st_get(mk(q, "uv"), []) bind qfcol = st_get(mk(q, "fcol"), [])
            bind qcol = st_get(mk(q, "col"), C_BLUE)
            bind f = 0.0
            while f < len(qfc) {
                bind s = list_get(qfs, f) bind c = list_get(qfc, f)
                bind fs = list_push(fs, len(fi)) bind fc = list_push(fc, c)
                bind fcv = sel(f < len(qfcol), list_get(qfcol, f), 0.0 - 1.0)
                bind fcol = list_push(fcol, sel(fcv >= 0.0, fcv, qcol))
                bind k = 0.0
                while k < c {
                    bind fi = list_push(fi, base + list_get(qfi, s + k))
                    bind uv = list_push(uv, list_get(quv, (s + k) * 2.0)) bind uv = list_push(uv, list_get(quv, (s + k) * 2.0 + 1.0))
                    bind k = k + 1.0
                }
                bind f = f + 1.0
            }
            me_remove(q)
        }
        bind i = i + 1.0
    }
    st_set(mk(o, "wb"), [])
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, [])
    me_select_object(o, false)
    st_set("status", "Joined into " + me_name(o))
}

# ── normals ──────────────────────────────────────────────────────────────────
fn mo_flip(o) {
    bind faces = me_selected_faces(o)
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), [])
    bind all = len(faces) < 0.5 || st_get("mode", 0.0) != 1.0
    bind f = 0.0
    while f < len(fc) {
        bind on = all
        if !on { bind i = 0.0 while i < len(faces) { if list_get(faces, i) == f { bind on = true } bind i = i + 1.0 } }
        if on {
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind k = 0.0
            while k < floor(c / 2.0) {
                bind a = s + k bind b = s + c - 1.0 - k
                bind t = list_get(fi, a) bind fi = list_set(fi, a, list_get(fi, b)) bind fi = list_set(fi, b, t)
                bind tu = list_get(uv, a * 2.0) bind tv = list_get(uv, a * 2.0 + 1.0)
                bind uv = list_set(uv, a * 2.0, list_get(uv, b * 2.0)) bind uv = list_set(uv, a * 2.0 + 1.0, list_get(uv, b * 2.0 + 1.0))
                bind uv = list_set(uv, b * 2.0, tu) bind uv = list_set(uv, b * 2.0 + 1.0, tv)
                bind k = k + 1.0
            }
        }
        bind f = f + 1.0
    }
    st_set(mk(o, "fi"), fi) st_set(mk(o, "uv"), uv)
    me_edges_dirty(o)
    st_set("dirty", 1.0)
    st_set("status", "Flipped normals")
}
# make windings consistent across edges, then outward (positive signed volume)
fn mo_recalc(o) {
    me_rebuild_edges(o)
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind ce = st_get(mk(o, "ce"), [])
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    bind nf = len(fc)
    bind visited = me_zeros(nf)
    bind flipf = me_zeros(nf)
    bind f0 = 0.0
    while f0 < nf {
        if list_get(visited, f0) < 0.5 && list_get(fc, f0) >= 3.0 {
            bind stack = [f0]
            bind visited = list_set(visited, f0, 1.0)
            while len(stack) > 0.5 {
                bind f = list_get(stack, len(stack) - 1.0)
                bind ns = list_new()
                bind i = 0.0
                while i < len(stack) - 1.0 { bind ns = list_push(ns, list_get(stack, i)) bind i = i + 1.0 }
                bind stack = ns
                bind s = list_get(fs, f) bind c = list_get(fc, f)
                bind k = 0.0
                while k < c {
                    bind e = list_get(ce, s + k)
                    bind g = sel(list_get(ef0, e) == f, list_get(ef1, e), list_get(ef0, e))
                    if g >= 0.0 { if list_get(visited, g) < 0.5 {
                        # directions of the shared edge in f and g (after their current flips)
                        bind a = list_get(fi, s + k) bind b = list_get(fi, s + (k + 1.0) % c)
                        bind fwd_f = list_get(flipf, f) < 0.5
                        bind gs = list_get(fs, g) bind gc = list_get(fc, g)
                        bind same = false
                        bind q = 0.0
                        while q < gc { if list_get(fi, gs + q) == a && list_get(fi, gs + (q + 1.0) % gc) == b { bind same = true } bind q = q + 1.0 }
                        bind fwd_g = list_get(flipf, g) < 0.5
                        # consistent when g traverses the edge opposite to f
                        bind opp = sel(same, false, true)
                        if fwd_f != fwd_g { bind opp = !opp }
                        if !opp { bind flipf = list_set(flipf, g, sel(list_get(flipf, g) > 0.5, 0.0, 1.0)) }
                        bind visited = list_set(visited, g, 1.0)
                        bind stack = list_push(stack, g)
                    } }
                    bind k = k + 1.0
                }
            }
        }
        bind f0 = f0 + 1.0
    }
    bind fsel_bak = st_get(mk(o, "fsel"), [])
    st_set(mk(o, "fsel"), flipf)
    st_set("mode_bak", st_get("mode", 0.0))
    st_set("mode", 1.0)
    bind any = false
    bind i = 0.0
    while i < nf { if list_get(flipf, i) > 0.5 { bind any = true } bind i = i + 1.0 }
    if any { mo_flip(o) }
    st_set("mode", st_get("mode_bak", 0.0))
    st_set(mk(o, "fsel"), fsel_bak)
    # outward: signed volume
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), [])
    bind vol = 0.0
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind k = 1.0
        while k + 1.0 < c {
            bind a = list_get(fi, s) bind b = list_get(fi, s + k) bind cc = list_get(fi, s + k + 1.0)
            bind vol = vol + v3_dot([list_get(vx, a), list_get(vy, a), list_get(vz, a)], v3_cross([list_get(vx, b), list_get(vy, b), list_get(vz, b)], [list_get(vx, cc), list_get(vy, cc), list_get(vz, cc)]))
            bind k = k + 1.0
        }
        bind f = f + 1.0
    }
    if vol < 0.0 {
        st_set("mode_bak", st_get("mode", 0.0)) st_set("mode", 0.0)
        mo_flip(o)
        st_set("mode", st_get("mode_bak", 0.0))
    }
    st_set("status", "Normals recalculated")
}

# ── triangulate / quads ──────────────────────────────────────────────────────
fn mo_triangulate(o) {
    bind faces = me_selected_faces(o)
    bind all = len(faces) < 0.5 || st_get("mode", 0.0) != 1.0
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind fsel = st_get(mk(o, "fsel"), [])
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nuv = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind on = all
        if !on { if f < len(fsel) { if list_get(fsel, f) > 0.5 { bind on = true } } }
        bind col = sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0)
        if on && c > 3.0 {
            bind k = 1.0
            while k + 1.0 < c {
                bind idx = [s, s + k, s + k + 1.0]
                bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, 3.0) bind nfcol = list_push(nfcol, col)
                bind q = 0.0
                while q < 3.0 { bind ci = list_get(idx, q) bind nfi = list_push(nfi, list_get(fi, ci)) bind nuv = list_push(nuv, list_get(uv, ci * 2.0)) bind nuv = list_push(nuv, list_get(uv, ci * 2.0 + 1.0)) bind q = q + 1.0 }
                bind k = k + 1.0
            }
        } else {
            bind nfs = list_push(nfs, len(nfi)) bind nfc = list_push(nfc, c) bind nfcol = list_push(nfcol, col)
            bind k = 0.0
            while k < c { bind nfi = list_push(nfi, list_get(fi, s + k)) bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0)) bind nuv = list_push(nuv, list_get(uv, (s + k) * 2.0 + 1.0)) bind k = k + 1.0 }
        }
        bind f = f + 1.0
    }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, nuv, nfcol, st_get(mk(o, "wb"), []))
    st_set("status", "Triangulated: " + num_str(len(nfc)) + " faces")
}
fn mo_quads(o) {
    me_rebuild_edges(o)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind ce = st_get(mk(o, "ce"), []) bind fcol = st_get(mk(o, "fcol"), [])
    bind efa = mo_edge_faces(o)
    bind ef0 = list_get(efa, 0) bind ef1 = list_get(efa, 1)
    bind nf = len(fc)
    bind used = me_zeros(nf)
    bind nfi = list_new() bind nfs = list_new() bind nfc = list_new() bind nfcol = list_new()
    bind f = 0.0
    while f < nf {
        if list_get(used, f) < 0.5 {
            bind s = list_get(fs, f) bind c = list_get(fc, f)
            bind merged = false
            if c == 3.0 {
                bind n1 = v3_norm(me_face_normal(vx, vy, vz, fi, s, 3.0))
                bind best = 0.0 - 1.0 bind bestk = 0.0 bind bd = 0.0 - 2.0
                bind k = 0.0
                while k < 3.0 {
                    bind e = list_get(ce, s + k)
                    bind g = sel(list_get(ef0, e) == f, list_get(ef1, e), list_get(ef0, e))
                    if g >= 0.0 { if list_get(used, g) < 0.5 && list_get(fc, g) == 3.0 {
                        bind n2 = v3_norm(me_face_normal(vx, vy, vz, fi, list_get(fs, g), 3.0))
                        bind d = v3_dot(n1, n2)
                        if d > bd && d > 0.85 { bind bd = d bind best = g bind bestk = k }
                    } }
                    bind k = k + 1.0
                }
                if best >= 0.0 {
                    bind a = list_get(fi, s + bestk) bind b = list_get(fi, s + (bestk + 1.0) % 3.0) bind cc = list_get(fi, s + (bestk + 2.0) % 3.0)
                    bind gs = list_get(fs, best)
                    bind d = 0.0 - 1.0
                    bind q = 0.0
                    while q < 3.0 { bind v = list_get(fi, gs + q) if v != a && v != b { bind d = v } bind q = q + 1.0 }
                    bind r = mo_push_face(nfi, nfs, nfc, [a, d, b, cc])
                    bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                    bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
                    bind used = list_set(used, best, 1.0)
                    bind merged = true
                }
            }
            if !merged {
                bind corners = list_new()
                bind k = 0.0
                while k < c { bind corners = list_push(corners, list_get(fi, s + k)) bind k = k + 1.0 }
                bind r = mo_push_face(nfi, nfs, nfc, corners)
                bind nfi = list_get(r, 0) bind nfs = list_get(r, 1) bind nfc = list_get(r, 2)
                bind nfcol = list_push(nfcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
            }
            bind used = list_set(used, f, 1.0)
        }
        bind f = f + 1.0
    }
    mo_compact(o, vx, vy, vz, nfi, nfs, nfc, me_zeros(len(nfi) * 2.0), nfcol, st_get(mk(o, "wb"), []))
    st_set("status", "Tris to quads: " + num_str(len(nfc)) + " faces")
}

# ── smooth ───────────────────────────────────────────────────────────────────
fn mo_smooth(o, strength) {
    bind nb = me_vert_neighbours(o)
    bind starts = list_get(nb, 0) bind vl = list_get(nb, 1)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind vsel = st_get(mk(o, "vsel"), [])
    bind all = me_sel_count(o) < 0.5 || st_get("mode", 0.0) != 1.0
    bind iters = max(1.0, round(strength))
    bind it = 0.0
    while it < iters {
        bind nx = vx bind ny = vy bind nz = vz
        bind v = 0.0
        while v < len(vx) {
            bind on = all
            if !on { if list_get(vsel, v) > 0.5 { bind on = true } }
            bind n = list_get(starts, v + 1.0) - list_get(starts, v)
            if on && n > 0.5 {
                bind sx = 0.0 bind sy = 0.0 bind sz = 0.0
                bind k = list_get(starts, v)
                while k < list_get(starts, v + 1.0) { bind w = list_get(vl, k) bind sx = sx + list_get(vx, w) bind sy = sy + list_get(vy, w) bind sz = sz + list_get(vz, w) bind k = k + 1.0 }
                bind nx = list_set(nx, v, list_get(vx, v) * 0.5 + sx / n * 0.5)
                bind ny = list_set(ny, v, list_get(vy, v) * 0.5 + sy / n * 0.5)
                bind nz = list_set(nz, v, list_get(vz, v) * 0.5 + sz / n * 0.5)
            }
            bind v = v + 1.0
        }
        bind vx = nx bind vy = ny bind vz = nz
        bind it = it + 1.0
    }
    st_set(mk(o, "vx"), vx) st_set(mk(o, "vy"), vy) st_set(mk(o, "vz"), vz)
    st_set("dirty", 1.0)
    st_set("status", "Smoothed")
}

# ── mirror / symmetrize (X axis, object space) ───────────────────────────────
fn mo_mirror_x(o) {
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind nv = len(vx)
    bind i = 0.0
    while i < nv { bind vx = list_push(vx, 0.0 - list_get(vx, i)) bind vy = list_push(vy, list_get(vy, i)) bind vz = list_push(vz, list_get(vz, i)) bind wb = mo_wb_dup(wb, i) bind i = i + 1.0 }
    bind nf = len(fc)
    bind f = 0.0
    while f < nf {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind fs = list_push(fs, len(fi)) bind fc = list_push(fc, c) bind fcol = list_push(fcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
        bind k = c - 1.0
        while k >= 0.0 { bind fi = list_push(fi, nv + list_get(fi, s + k)) bind uv = list_push(uv, list_get(uv, (s + k) * 2.0)) bind uv = list_push(uv, list_get(uv, (s + k) * 2.0 + 1.0)) bind k = k - 1.0 }
        bind f = f + 1.0
    }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    mo_merge_dist(o, 0.0001)
    st_set("status", "Mirrored across X")
}
fn mo_symmetrize(o) {
    # drop everything with x < 0 (faces whose centre is negative), then mirror
    bind vx = st_get(mk(o, "vx"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind fsel = me_zeros(len(fc))
    bind f = 0.0
    while f < len(fc) {
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind cx = 0.0
        bind k = 0.0
        while k < c { bind cx = cx + list_get(vx, list_get(fi, s + k)) bind k = k + 1.0 }
        if cx / c < 0.0 - 0.00001 { bind fsel = list_set(fsel, f, 1.0) }
        bind f = f + 1.0
    }
    st_set(mk(o, "fsel"), fsel)
    mo_delete(o, 2.0)
    # snap near-zero x to zero so the seam welds
    bind vx = st_get(mk(o, "vx"), [])
    bind i = 0.0
    while i < len(vx) { if abs(list_get(vx, i)) < 0.001 { bind vx = list_set(vx, i, 0.0) } bind i = i + 1.0 }
    st_set(mk(o, "vx"), vx)
    mo_mirror_x(o)
    st_set("status", "Symmetrized (+X half kept)")
}

# ── spin / lathe the selection around the Y axis through the 3D cursor ───────
fn mo_spin(o, steps, angle_deg) {
    me_rebuild_edges(o)
    bind verts = me_selected_verts(o)
    if len(verts) < 0.5 { st_set("status", "Select a profile (vertices / edges) to spin") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), []) bind esel = st_get(mk(o, "esel"), [])
    bind cur = m_apply(m_inv(me_matrix(o)), [st_get("cursor_x", 0.0), st_get("cursor_y", 0.0), st_get("cursor_z", 0.0)])
    bind cx = list_get(cur, 0) bind cz = list_get(cur, 2)
    bind steps = max(1.0, floor(steps))
    bind full = abs(angle_deg - 360.0) < 0.001
    bind nv = len(vx)
    bind vsel = st_get(mk(o, "vsel"), [])
    # ring index of each selected vertex
    bind ridx = me_fill(nv, 0.0 - 1.0)
    bind i = 0.0
    while i < len(verts) { bind ridx = list_set(ridx, list_get(verts, i), i) bind i = i + 1.0 }
    bind nsel = len(verts)
    bind copies = sel(full, steps, steps + 1.0)
    # vertex of ring r, profile i: r == 0 -> original, else nv + (r-1)*nsel + i
    bind r = 1.0
    while r < copies {
        bind a = mx_rad(angle_deg) * r / steps
        bind ca = cos(a) bind sa = sin(a)
        bind i = 0.0
        while i < nsel {
            bind v = list_get(verts, i)
            bind dx = list_get(vx, v) - cx bind dz = list_get(vz, v) - cz
            bind vx = list_push(vx, cx + dx * ca + dz * sa)
            bind vy = list_push(vy, list_get(vy, v))
            bind vz = list_push(vz, cz - dx * sa + dz * ca)
            bind wb = mo_wb_dup(wb, v)
            bind i = i + 1.0
        }
        bind r = r + 1.0
    }
    # quads for every selected edge
    bind e = 0.0
    while e < len(ea) {
        if list_get(esel, e) > 0.5 {
            bind ia = list_get(ridx, list_get(ea, e)) bind ib = list_get(ridx, list_get(eb, e))
            bind r = 0.0
            while r < steps {
                bind r2 = sel(full && r == steps - 1.0, 0.0, r + 1.0)
                bind a0 = sel(r == 0.0, list_get(ea, e), nv + (r - 1.0) * nsel + ia)
                bind b0 = sel(r == 0.0, list_get(eb, e), nv + (r - 1.0) * nsel + ib)
                bind a1 = sel(r2 == 0.0, list_get(ea, e), nv + (r2 - 1.0) * nsel + ia)
                bind b1 = sel(r2 == 0.0, list_get(eb, e), nv + (r2 - 1.0) * nsel + ib)
                bind q = mo_push_face(fi, fs, fc, [a0, a1, b1, b0])
                bind fi = list_get(q, 0) bind fs = list_get(q, 1) bind fc = list_get(q, 2)
                bind uv = mo_uv_pad(uv, 4.0)
                bind fcol = list_push(fcol, 0.0 - 1.0)
                bind r = r + 1.0
            }
        }
        bind e = e + 1.0
    }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    mo_merge_dist(o, 0.0001)
    mo_recalc(o)
    st_set("status", "Spun " + num_str(steps) + " steps over " + fmt0(angle_deg) + " degrees")
}

# ── transforms ───────────────────────────────────────────────────────────────
fn mo_apply_xf(o) {
    bind m = me_matrix(o)
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind i = 0.0
    while i < len(vx) {
        bind p = m_apply(m, [list_get(vx, i), list_get(vy, i), list_get(vz, i)])
        bind vx = list_set(vx, i, list_get(p, 0)) bind vy = list_set(vy, i, list_get(p, 1)) bind vz = list_set(vz, i, list_get(p, 2))
        bind i = i + 1.0
    }
    st_set(mk(o, "vx"), vx) st_set(mk(o, "vy"), vy) st_set(mk(o, "vz"), vz)
    rg_transform_bones(o, m)
    st_set(mk(o, "px"), 0.0) st_set(mk(o, "py"), 0.0) st_set(mk(o, "pz"), 0.0)
    st_set(mk(o, "rx"), 0.0) st_set(mk(o, "ry"), 0.0) st_set(mk(o, "rz"), 0.0)
    st_set(mk(o, "sx"), 1.0) st_set(mk(o, "sy"), 1.0) st_set(mk(o, "sz"), 1.0)
    st_set("dirty", 1.0)
    st_set("status", "Transform applied")
}
fn mo_center(o) {
    bind b = me_sel_bounds_local(o, false)
    if len(b) < 0.5 { return 0.0 }
    bind cx = (list_get(b, 0) + list_get(b, 3)) * 0.5 bind cy = (list_get(b, 1) + list_get(b, 4)) * 0.5 bind cz = (list_get(b, 2) + list_get(b, 5)) * 0.5
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind i = 0.0
    while i < len(vx) { bind vx = list_set(vx, i, list_get(vx, i) - cx) bind vy = list_set(vy, i, list_get(vy, i) - cy) bind vz = list_set(vz, i, list_get(vz, i) - cz) bind i = i + 1.0 }
    st_set(mk(o, "vx"), vx) st_set(mk(o, "vy"), vy) st_set(mk(o, "vz"), vz)
    rg_transform_bones(o, m_trans(0.0 - cx, 0.0 - cy, 0.0 - cz))
    bind m = me_matrix(o)
    bind p = m_apply(m, [cx, cy, cz])
    st_set(mk(o, "px"), list_get(p, 0)) st_set(mk(o, "py"), list_get(p, 1)) st_set(mk(o, "pz"), list_get(p, 2))
    st_set("dirty", 1.0)
}

# ── duplicate selected faces inside the object ───────────────────────────────
fn mo_dup_sel(o) {
    bind faces = me_selected_faces(o)
    bind verts = me_selected_verts(o)
    if len(verts) < 0.5 { st_set("status", "Nothing selected") return 0.0 }
    bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
    bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
    bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), []) bind wb = st_get(mk(o, "wb"), [])
    bind nv = len(vx)
    bind newv = me_fill(nv, 0.0 - 1.0)
    bind i = 0.0
    while i < len(verts) {
        bind v = list_get(verts, i)
        bind newv = list_set(newv, v, len(vx))
        bind vx = list_push(vx, list_get(vx, v)) bind vy = list_push(vy, list_get(vy, v)) bind vz = list_push(vz, list_get(vz, v))
        bind wb = mo_wb_dup(wb, v)
        bind i = i + 1.0
    }
    bind i = 0.0
    while i < len(faces) {
        bind f = list_get(faces, i)
        bind s = list_get(fs, f) bind c = list_get(fc, f)
        bind fs = list_push(fs, len(fi)) bind fc = list_push(fc, c) bind fcol = list_push(fcol, sel(f < len(fcol), list_get(fcol, f), 0.0 - 1.0))
        bind k = 0.0
        while k < c { bind fi = list_push(fi, list_get(newv, list_get(fi, s + k))) bind uv = list_push(uv, list_get(uv, (s + k) * 2.0)) bind uv = list_push(uv, list_get(uv, (s + k) * 2.0 + 1.0)) bind k = k + 1.0 }
        bind i = i + 1.0
    }
    bind nvsel = me_zeros(len(vx))
    bind i = 0.0
    while i < nv { if list_get(newv, i) >= 0.0 { bind nvsel = list_set(nvsel, list_get(newv, i), 1.0) } bind i = i + 1.0 }
    mo_compact(o, vx, vy, vz, fi, fs, fc, uv, fcol, wb)
    st_set(mk(o, "vsel"), nvsel)
    me_sel_sync_from_verts(o)
}

fn mo_select_by_color(o) {
    bind faces = me_selected_faces(o)
    if len(faces) < 0.5 { return 0.0 }
    bind fcol = st_get(mk(o, "fcol"), [])
    bind target = list_get(fcol, list_get(faces, 0))
    bind f = 0.0
    while f < len(fcol) { if list_get(fcol, f) == target { me_sel_face(o, f, true) } bind f = f + 1.0 }
}

← Back to index