lingmodeler
↓ 0 downloadsLingModeler: 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
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodelerCode / Files (67)
ling/rig.ling
# ━━━ rig.ling — armatures (bones), skin binding, adjustable auto-weights, weight painting + editing ━━━
# Bones belong to the mesh object they deform (prefix m<o>_bn_*), in object space:
# bn_name bn_par bn_hx/hy/hz (head) bn_tx/ty/tz (tail) bn_sel
# Weights: wb = 8 numbers per vertex: (bone, weight) x 4, bone -1 = empty slot. "bound" = 1 once the mesh is bound.
# The active bone index is st "act_bone".
fn rg_count(o) { return len(st_get(mk(o, "bn_name"), [])) }
fn rg_active(o) {
bind a = st_get("act_bone", 0.0 - 1.0)
if a >= rg_count(o) { return 0.0 - 1.0 }
return a
}
fn rg_bone_name(o, b) { if b < 0.0 || b >= rg_count(o) { return "" } return list_get(st_get(mk(o, "bn_name"), []), b) }
fn rg_rename(o, b, name) {
if b < 0.0 || b >= rg_count(o) { return 0.0 }
bind names = st_get(mk(o, "bn_name"), [])
bind names = list_set(names, b, name)
st_set(mk(o, "bn_name"), names)
st_set("dirty", 1.0)
}
fn rg_sel_count(o) {
bind s = st_get(mk(o, "bn_sel"), [])
bind n = 0.0
bind i = 0.0
while i < len(s) { if list_get(s, i) > 0.5 { bind n = n + 1.0 } bind i = i + 1.0 }
return n
}
fn rg_selected(o) {
bind s = st_get(mk(o, "bn_sel"), [])
bind out = list_new()
bind i = 0.0
while i < len(s) { if list_get(s, i) > 0.5 { bind out = list_push(out, i) } bind i = i + 1.0 }
return out
}
fn rg_sel_all(o, on) {
st_set(mk(o, "bn_sel"), me_fill(rg_count(o), sel(on, 1.0, 0.0)))
if !on { st_set("act_bone", 0.0 - 1.0) }
}
fn rg_select(o, b, extend) {
bind s = st_get(mk(o, "bn_sel"), [])
if !extend { bind s = me_zeros(len(s)) }
if b >= 0.0 && b < len(s) {
bind on = list_get(s, b) > 0.5
bind s = list_set(s, b, sel(extend && on, 0.0, 1.0))
if !(extend && on) { st_set("act_bone", b) }
}
st_set(mk(o, "bn_sel"), s)
}
fn rg_head(o, b) { return [list_get(st_get(mk(o, "bn_hx"), []), b), list_get(st_get(mk(o, "bn_hy"), []), b), list_get(st_get(mk(o, "bn_hz"), []), b)] }
fn rg_tail(o, b) { return [list_get(st_get(mk(o, "bn_tx"), []), b), list_get(st_get(mk(o, "bn_ty"), []), b), list_get(st_get(mk(o, "bn_tz"), []), b)] }
fn rg_set_head(o, b, p) {
st_lset(mk(o, "bn_hx"), b, list_get(p, 0)) st_lset(mk(o, "bn_hy"), b, list_get(p, 1)) st_lset(mk(o, "bn_hz"), b, list_get(p, 2))
}
fn rg_set_tail(o, b, p) {
st_lset(mk(o, "bn_tx"), b, list_get(p, 0)) st_lset(mk(o, "bn_ty"), b, list_get(p, 1)) st_lset(mk(o, "bn_tz"), b, list_get(p, 2))
}
fn rg_len(o, b) { return v3_dist(rg_head(o, b), rg_tail(o, b)) }
fn rg_depth(o, b) {
bind par = st_get(mk(o, "bn_par"), [])
bind d = 0.0
bind p = list_get(par, b)
bind guard = 0.0
while p >= 0.0 && guard < 256.0 { bind d = d + 1.0 bind p = list_get(par, p) bind guard = guard + 1.0 }
return d
}
fn rg_unique_name(o, base) {
bind names = st_get(mk(o, "bn_name"), [])
bind nm = base
bind k = 1.0
bind taken = true
while taken {
bind taken = false
bind i = 0.0
while i < len(names) { if list_get(names, i) == nm { bind taken = true } bind i = i + 1.0 }
if taken { bind k = k + 1.0 bind nm = base + "." + sel(k < 10.0, "00", sel(k < 100.0, "0", "")) + num_str(k) }
}
return nm
}
# ── creation / editing ───────────────────────────────────────────────────────
fn rg_push_bone(o, name, parent, h, t) {
st_lpush(mk(o, "bn_name"), rg_unique_name(o, name))
st_lpush(mk(o, "bn_par"), parent)
st_lpush(mk(o, "bn_hx"), list_get(h, 0)) st_lpush(mk(o, "bn_hy"), list_get(h, 1)) st_lpush(mk(o, "bn_hz"), list_get(h, 2))
st_lpush(mk(o, "bn_tx"), list_get(t, 0)) st_lpush(mk(o, "bn_ty"), list_get(t, 1)) st_lpush(mk(o, "bn_tz"), list_get(t, 2))
st_lpush(mk(o, "bn_sel"), 0.0)
bind keys = ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz"]
bind i = 0.0
while i < 6.0 { st_lpush(mk(o, list_get(keys, i)), 0.0) bind i = i + 1.0 }
st_lpush(mk(o, "bp_sx"), 1.0) st_lpush(mk(o, "bp_sy"), 1.0) st_lpush(mk(o, "bp_sz"), 1.0)
st_set("dirty", 1.0)
return rg_count(o) - 1.0
}
fn rg_add_bone() {
bind o = me_sel_o()
if o < 0.0 {
me_undo_push()
bind o = me_add_object("Armature", [], [], [], [], [], [])
} else { me_undo_push() }
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 b = me_sel_bounds_local(o, false)
bind size = 1.0
if len(b) > 0.5 { bind size = max(0.1, (list_get(b, 4) - list_get(b, 1)) * 0.25) }
bind nb = rg_push_bone(o, "Bone", 0.0 - 1.0, cur, [list_get(cur, 0), list_get(cur, 1) + size, list_get(cur, 2)])
rg_select(o, nb, false)
st_set("status", "Added " + rg_bone_name(o, nb) + " at the 3D cursor (E extrudes a child, G moves, T edits tails)")
}
fn rg_extrude() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind sel_b = rg_selected(o)
if len(sel_b) < 0.5 { st_set("status", "Select a bone to extrude from its tail") return 0.0 }
me_undo_push()
bind news = list_new()
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
bind h = rg_tail(o, b)
bind d = v3_sub(rg_tail(o, b), rg_head(o, b))
bind t = v3_add(h, v3_scale(d, 0.8))
bind nb = rg_push_bone(o, rg_bone_name(o, b), b, h, t)
bind news = list_push(news, nb)
bind i = i + 1.0
}
rg_sel_all(o, false)
bind i = 0.0
while i < len(news) { rg_select(o, list_get(news, i), true) bind i = i + 1.0 }
st_set("rg_tipmode", 1.0)
v3_begin_modal("g")
st_set("status", "Extruded bone - move its tip, click to confirm")
}
fn rg_parent() {
bind o = me_sel_o()
bind a = rg_active(o)
if o < 0.0 || a < 0.0 { st_set("status", "Click the parent bone last (active), with the children also selected") return 0.0 }
me_undo_push()
bind sel_b = rg_selected(o)
bind par = st_get(mk(o, "bn_par"), [])
bind n = 0.0
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
if b != a {
# avoid cycles: the active bone must not descend from b
bind p = a
bind cyc = false
bind guard = 0.0
while p >= 0.0 && guard < 256.0 { if p == b { bind cyc = true } bind p = list_get(par, p) bind guard = guard + 1.0 }
if !cyc { bind par = list_set(par, b, a) bind n = n + 1.0 }
}
bind i = i + 1.0
}
st_set(mk(o, "bn_par"), par)
st_set("status", "Parented " + num_str(n) + " bones to " + rg_bone_name(o, a))
}
fn rg_unparent() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind sel_b = rg_selected(o)
bind par = st_get(mk(o, "bn_par"), [])
bind i = 0.0
while i < len(sel_b) { bind par = list_set(par, list_get(sel_b, i), 0.0 - 1.0) bind i = i + 1.0 }
st_set(mk(o, "bn_par"), par)
st_set("status", "Cleared parents")
}
fn rg_delete() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind sel_b = rg_selected(o)
if len(sel_b) < 0.5 { st_set("status", "No bone selected") return 0.0 }
me_undo_push()
bind n = rg_count(o)
bind s = st_get(mk(o, "bn_sel"), [])
bind par = st_get(mk(o, "bn_par"), [])
bind remap = me_fill(n, 0.0 - 1.0)
bind k = 0.0
bind i = 0.0
while i < n { if list_get(s, i) < 0.5 { bind remap = list_set(remap, i, k) bind k = k + 1.0 } bind i = i + 1.0 }
# children of deleted bones go to the nearest surviving ancestor
bind keys = ["bn_name", "bn_par", "bn_hx", "bn_hy", "bn_hz", "bn_tx", "bn_ty", "bn_tz", "bn_sel", "bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"]
bind ki = 0.0
while ki < len(keys) {
bind key = list_get(keys, ki)
bind old = st_get(mk(o, key), [])
bind nw = list_new()
bind i = 0.0
while i < n {
if list_get(remap, i) >= 0.0 {
bind v = list_get(old, i)
if key == "bn_par" {
bind p = v
bind guard = 0.0
while p >= 0.0 && guard < 256.0 { if list_get(remap, p) >= 0.0 { bind guard = 1000.0 } else { bind p = list_get(par, p) } bind guard = guard + 1.0 }
bind v = sel(p >= 0.0, list_get(remap, p), 0.0 - 1.0)
}
if key == "bn_sel" { bind v = 0.0 }
bind nw = list_push(nw, v)
}
bind i = i + 1.0
}
st_set(mk(o, key), nw)
bind ki = ki + 1.0
}
# weights: remap bone indices, drop deleted
bind wb = st_get(mk(o, "wb"), [])
bind i = 0.0
while i < len(wb) {
bind b = list_get(wb, i)
if b >= 0.0 {
bind nb = sel(b < n, list_get(remap, b), 0.0 - 1.0)
bind wb = list_set(wb, i, nb)
if nb < 0.0 { bind wb = list_set(wb, i + 1.0, 0.0) }
}
bind i = i + 2.0
}
st_set(mk(o, "wb"), wb)
# actions: drop keys of deleted bones, remap the rest
bind acts = st_get(mk(o, "act_data"), [])
bind ai = 0.0
while ai < len(acts) {
bind keysl = list_get(acts, ai)
bind nk = list_new()
bind j = 0.0
while j + 3.0 < len(keysl) {
bind b = list_get(keysl, j)
if b < n { if list_get(remap, b) >= 0.0 {
bind nk = list_push(nk, list_get(remap, b)) bind nk = list_push(nk, list_get(keysl, j + 1.0)) bind nk = list_push(nk, list_get(keysl, j + 2.0)) bind nk = list_push(nk, list_get(keysl, j + 3.0))
} }
bind j = j + 4.0
}
bind acts = list_set(acts, ai, nk)
bind ai = ai + 1.0
}
st_set(mk(o, "act_data"), acts)
st_set("act_bone", 0.0 - 1.0)
st_set("status", "Deleted " + num_str(len(sel_b)) + " bones")
}
fn rg_dup() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind sel_b = rg_selected(o)
if len(sel_b) < 0.5 { return 0.0 }
me_undo_push()
bind par = st_get(mk(o, "bn_par"), [])
bind map = me_fill(rg_count(o), 0.0 - 1.0)
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
bind nb = rg_push_bone(o, rg_bone_name(o, b), list_get(par, b), rg_head(o, b), rg_tail(o, b))
bind map = list_set(map, b, nb)
bind i = i + 1.0
}
# parents inside the copied set point to the copies
bind par = st_get(mk(o, "bn_par"), [])
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
bind p = list_get(par, b)
if p >= 0.0 { if list_get(map, p) >= 0.0 { bind par = list_set(par, list_get(map, b), list_get(map, p)) } }
bind i = i + 1.0
}
st_set(mk(o, "bn_par"), par)
rg_sel_all(o, false)
bind i = 0.0
while i < len(sel_b) { rg_select(o, list_get(map, list_get(sel_b, i)), true) bind i = i + 1.0 }
st_set("rg_tipmode", 0.0)
v3_begin_modal("g")
}
# names ending in .L / _L / .l get mirrored .R copies across X (existing .R bones are updated)
fn rg_symmetrize() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind names = st_get(mk(o, "bn_name"), [])
bind n = len(names)
bind made = 0.0
bind map = me_fill(n, 0.0 - 1.0)
bind i = 0.0
while i < n {
bind nm = list_get(names, i)
bind suf = str_slice(nm, str_len(nm) - 2.0, 2.0)
if suf == ".L" || suf == "_L" || suf == ".l" {
bind rn = str_slice(nm, 0.0, str_len(nm) - 2.0) + sel(suf == ".l", ".r", sel(suf == "_L", "_R", ".R"))
bind h = rg_head(o, i) bind t = rg_tail(o, i)
bind mh = [0.0 - list_get(h, 0), list_get(h, 1), list_get(h, 2)]
bind mt = [0.0 - list_get(t, 0), list_get(t, 1), list_get(t, 2)]
bind existing = 0.0 - 1.0
bind j = 0.0
while j < len(names) { if list_get(names, j) == rn { bind existing = j } bind j = j + 1.0 }
if existing >= 0.0 { rg_set_head(o, existing, mh) rg_set_tail(o, existing, mt) bind map = list_set(map, i, existing) }
else {
bind nb = rg_push_bone(o, rn, list_get(st_get(mk(o, "bn_par"), []), i), mh, mt)
bind map = list_set(map, i, nb)
bind made = made + 1.0
}
}
bind i = i + 1.0
}
# mirrored bones parent to the mirrored parent when it exists
bind par = st_get(mk(o, "bn_par"), [])
bind i = 0.0
while i < n {
bind m = list_get(map, i)
if m >= 0.0 {
bind p = list_get(par, i)
if p >= 0.0 { if list_get(map, p) >= 0.0 { bind par = list_set(par, m, list_get(map, p)) } }
}
bind i = i + 1.0
}
st_set(mk(o, "bn_par"), par)
st_set("status", "Symmetrized: " + num_str(made) + " new .R bones (name bones *.L first)")
}
fn rg_transform_bones(o, m) {
bind n = rg_count(o)
bind i = 0.0
while i < n {
rg_set_head(o, i, m_apply(m, rg_head(o, i)))
rg_set_tail(o, i, m_apply(m, rg_tail(o, i)))
bind i = i + 1.0
}
}
fn rg_bind() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
if rg_count(o) < 0.5 { st_set("status", "Add bones first (Shift+A in Rig mode)") return 0.0 }
me_undo_push()
st_set(mk(o, "bound"), 1.0)
bind wb = st_get(mk(o, "wb"), [])
if len(wb) != me_nverts(o) * 8.0 { st_set(mk(o, "wb"), me_wb_resize([], me_nverts(o))) }
st_set("status", "Mesh bound to its armature - run Auto Weights or paint weights")
}
# ── rest / pose matrices ─────────────────────────────────────────────────────
# object-space rest frame per bone (origin head, Y along the bone): flat 12 per bone
fn rg_rest_matrices(o) {
bind n = rg_count(o)
bind out = list_new()
bind b = 0.0
while b < n {
bind h = rg_head(o, b) bind t = rg_tail(o, b)
bind d = v3_sub(t, h)
if v3_len(d) < 0.000001 { bind d = [0.0, 1.0, 0.0] }
bind m = m_frame_y(h, d)
bind k = 0.0
while k < 12.0 { bind out = list_push(out, list_get(m, k)) bind k = k + 1.0 }
bind b = b + 1.0
}
return out
}
fn rg_mat_at(flat, b) {
bind out = list_new()
bind k = 0.0
while k < 12.0 { bind out = list_push(out, list_get(flat, b * 12.0 + k)) bind k = k + 1.0 }
return out
}
# ── drawing ──────────────────────────────────────────────────────────────────
fn rg_draw_bone(o, b, h, t, col, thick) {
bind om = me_matrix(o)
bind hw = m_apply(om, h) bind tw = m_apply(om, t)
bind hs = v3_w2s(list_get(hw, 0), list_get(hw, 1), list_get(hw, 2))
bind ts = v3_w2s(list_get(tw, 0), list_get(tw, 1), list_get(tw, 2))
if list_get(hs, 2) < 0.05 || list_get(ts, 2) < 0.05 { return 0.0 }
bind hx = list_get(hs, 0) bind hy = list_get(hs, 1) bind tx = list_get(ts, 0) bind ty = list_get(ts, 1)
bind dx = tx - hx bind dy = ty - hy
bind l = sqrt(dx * dx + dy * dy)
if l < 0.5 { px_ring(hx, hy, 4.0, col, 0.9) return 0.0 }
bind nx = 0.0 - dy / l bind ny = dx / l
bind w = clampf(l * 0.12, 2.0, 9.0)
bind jx = hx + dx * 0.18 bind jy = hy + dy * 0.18
# octahedral bone outline
px_line(hx, hy, jx + nx * w, jy + ny * w, col, 0.95)
px_line(hx, hy, jx - nx * w, jy - ny * w, col, 0.95)
px_line(jx + nx * w, jy + ny * w, tx, ty, col, 0.95)
px_line(jx - nx * w, jy - ny * w, tx, ty, col, 0.95)
px_line(jx + nx * w, jy + ny * w, jx - nx * w, jy - ny * w, col, 0.6)
if thick { ds_hatch(min(hx, tx) - 1.0, min(hy, ty) - 1.0, abs(dx) + 2.0, abs(dy) + 2.0, col, 45.0, 4.0, 0.0) px_line(hx, hy, tx, ty, col, 0.9) }
px_disc(hx, hy, 3.0, col)
px_ring(tx, ty, 3.0, col, 0.9)
}
fn rg_draw(o) {
bind n = rg_count(o)
if n < 0.5 { return 0.0 }
bind mode = st_get("mode", 0.0)
bind s = st_get(mk(o, "bn_sel"), [])
bind act = rg_active(o)
bind posed = mode == 4.0 || tl_playing()
bind pm = []
if posed { bind pm = ps_pose_matrices(o) }
bind b = 0.0
while b < n {
bind h = rg_head(o, b) bind t = rg_tail(o, b)
if posed {
bind m = rg_mat_at(pm, b)
bind ln = rg_len(o, b)
bind h = m_apply(m, [0.0, 0.0, 0.0])
bind t = m_apply(m, [0.0, ln, 0.0])
}
bind col = C_BONE
bind on = false
if b < len(s) { if list_get(s, b) > 0.5 { bind on = true } }
if on { bind col = C_BONE_SEL }
if b == act { bind col = C_ACT_EL }
if mode == 3.0 && b == act { bind col = C_RED }
rg_draw_bone(o, b, h, t, col, on)
if st_get("rg_names", 1.0) > 0.5 {
bind om = me_matrix(o)
bind hw = m_apply(om, h)
bind hs = v3_w2s(list_get(hw, 0), list_get(hw, 1), list_get(hw, 2))
if list_get(hs, 2) > 0.05 { g_text(list_get(hs, 0) + 6.0, list_get(hs, 1) - 14.0, 10.0, col, rg_bone_name(o, b)) }
}
bind b = b + 1.0
}
}
fn rg_draw_faint(o) {
bind n = rg_count(o)
bind b = 0.0
while b < n { rg_draw_bone(o, b, rg_head(o, b), rg_tail(o, b), C_WIRE_DIM, false) bind b = b + 1.0 }
}
# ── picking / selection ──────────────────────────────────────────────────────
# nearest bone to the pointer (screen space); returns [bone, nearTail(0/1)] or [-1, 0]
fn rg_pick(o, mx, my, radius) {
bind n = rg_count(o)
bind om = me_matrix(o)
bind posed = st_get("mode", 0.0) == 4.0
bind pm = []
if posed { bind pm = ps_pose_matrices(o) }
bind best = 0.0 - 1.0 bind bd = radius bind tip = 0.0
bind b = 0.0
while b < n {
bind h = rg_head(o, b) bind t = rg_tail(o, b)
if posed { bind m = rg_mat_at(pm, b) bind h = m_apply(m, [0.0, 0.0, 0.0]) bind t = m_apply(m, [0.0, rg_len(o, b), 0.0]) }
bind hw = m_apply(om, h) bind tw = m_apply(om, t)
bind hs = v3_w2s(list_get(hw, 0), list_get(hw, 1), list_get(hw, 2))
bind ts = v3_w2s(list_get(tw, 0), list_get(tw, 1), list_get(tw, 2))
if list_get(hs, 2) > 0.05 && list_get(ts, 2) > 0.05 {
bind d = mx_pt_seg2(mx, my, list_get(hs, 0), list_get(hs, 1), list_get(ts, 0), list_get(ts, 1))
if d < bd {
bind bd = d bind best = b
bind dt = sqrt((mx - list_get(ts, 0)) * (mx - list_get(ts, 0)) + (my - list_get(ts, 1)) * (my - list_get(ts, 1)))
bind tip = sel(dt < 10.0, 1.0, 0.0)
}
}
bind b = b + 1.0
}
return [best, tip]
}
fn rg_click(mx, my, extend) {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind r = rg_pick(o, mx, my, 12.0)
bind b = list_get(r, 0)
if b < 0.0 { if !extend { rg_sel_all(o, false) } return 0.0 }
rg_select(o, b, extend)
if st_get("mode", 0.0) == 2.0 { st_set("rg_tipmode", list_get(r, 1)) }
}
fn rg_box_select(o, xa, ya, xb, yb, extend) {
bind n = rg_count(o)
bind om = me_matrix(o)
bind s = st_get(mk(o, "bn_sel"), [])
if !extend { bind s = me_zeros(n) }
bind b = 0.0
while b < n {
bind hw = m_apply(om, rg_head(o, b)) bind tw = m_apply(om, rg_tail(o, b))
bind hs = v3_w2s(list_get(hw, 0), list_get(hw, 1), list_get(hw, 2))
bind ts = v3_w2s(list_get(tw, 0), list_get(tw, 1), list_get(tw, 2))
bind inh = list_get(hs, 0) >= xa && list_get(hs, 0) <= xb && list_get(hs, 1) >= ya && list_get(hs, 1) <= yb
bind int = list_get(ts, 0) >= xa && list_get(ts, 0) <= xb && list_get(ts, 1) >= ya && list_get(ts, 1) <= yb
if inh || int { bind s = list_set(s, b, 1.0) st_set("act_bone", b) }
bind b = b + 1.0
}
st_set(mk(o, "bn_sel"), s)
}
# ── modal transforms in rig (2) and pose (4) modes ───────────────────────────
fn rg_pivot(o, mode) {
bind sel_b = rg_selected(o)
bind om = me_matrix(o)
bind c = [0.0, 0.0, 0.0]
bind n = 0.0
bind pm = []
if mode == 4.0 { bind pm = ps_pose_matrices(o) }
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
if mode == 4.0 {
bind c = v3_add(c, m_apply(om, m_apply(rg_mat_at(pm, b), [0.0, 0.0, 0.0]))) bind n = n + 1.0
} else if st_get("rg_tipmode", 0.0) > 0.5 {
bind c = v3_add(c, m_apply(om, rg_head(o, b))) bind n = n + 1.0
} else {
bind c = v3_add(c, m_apply(om, rg_head(o, b))) bind c = v3_add(c, m_apply(om, rg_tail(o, b))) bind n = n + 2.0
}
bind i = i + 1.0
}
if n < 0.5 { return [0.0, 0.0, 0.0] }
return v3_scale(c, 1.0 / n)
}
fn rg_modal_snapshot(o, mode) {
bind keys = sel(mode == 4.0, ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"], ["bn_hx", "bn_hy", "bn_hz", "bn_tx", "bn_ty", "bn_tz"])
bind i = 0.0
while i < len(keys) { st_set("trs_" + list_get(keys, i), st_get(mk(o, list_get(keys, i)), [])) bind i = i + 1.0 }
if mode == 4.0 { st_set("trs_pm", ps_pose_matrices(o)) }
}
fn rg_modal_restore(o, mode) {
bind keys = sel(mode == 4.0, ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"], ["bn_hx", "bn_hy", "bn_hz", "bn_tx", "bn_ty", "bn_tz"])
bind i = 0.0
while i < len(keys) { st_set(mk(o, list_get(keys, i)), st_get("trs_" + list_get(keys, i), [])) bind i = i + 1.0 }
ps_mark(o)
}
fn rg_modal_apply(o, mode, kind, M, axis) {
bind sel_b = rg_selected(o)
bind om = me_matrix(o)
bind T = m_mul(m_inv(om), m_mul(M, om))
if mode == 2.0 {
bind tip = st_get("rg_tipmode", 0.0) > 0.5
bind hx = st_get("trs_bn_hx", []) bind hy = st_get("trs_bn_hy", []) bind hz = st_get("trs_bn_hz", [])
bind tx = st_get("trs_bn_tx", []) bind ty = st_get("trs_bn_ty", []) bind tz = st_get("trs_bn_tz", [])
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
bind t = m_apply(T, [list_get(tx, b), list_get(ty, b), list_get(tz, b)])
rg_set_tail(o, b, t)
if !tip { rg_set_head(o, b, m_apply(T, [list_get(hx, b), list_get(hy, b), list_get(hz, b)])) }
bind i = i + 1.0
}
# children heads follow a moved parent tail when connected (same point)
bind par = st_get(mk(o, "bn_par"), [])
bind n = rg_count(o)
bind c = 0.0
while c < n {
bind p = list_get(par, c)
if p >= 0.0 {
bind on = false
bind i = 0.0
while i < len(sel_b) { if list_get(sel_b, i) == p { bind on = true } bind i = i + 1.0 }
if on && v3_dist([list_get(hx, c), list_get(hy, c), list_get(hz, c)], [list_get(tx, p), list_get(ty, p), list_get(tz, p)]) < 0.0001 {
bind nh = rg_tail(o, p)
bind d = v3_sub(nh, rg_head(o, c))
rg_set_head(o, c, nh)
bind own = false
bind i = 0.0
while i < len(sel_b) { if list_get(sel_b, i) == c { bind own = true } bind i = i + 1.0 }
if !own { rg_set_tail(o, c, v3_add(rg_tail(o, c), d)) }
}
}
bind c = c + 1.0
}
return 0.0
}
# pose: apply the delta to each selected bone's pose matrix, re-express locally
bind pm = st_get("trs_pm", [])
bind rest = rg_rest_matrices(o)
bind par = st_get(mk(o, "bn_par"), [])
bind keys = ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"]
bind i = 0.0
while i < len(sel_b) {
bind b = list_get(sel_b, i)
bind P = rg_mat_at(pm, b)
bind Pn = m_mul(T, P)
bind p = list_get(par, b)
bind Pp = m_ident()
if p >= 0.0 { bind Pp = rg_mat_at(pm, p) }
# local = inv(L) * inv(Pp) * Pn, L = inv(Rp) * Rb
bind Rb = rg_mat_at(rest, b)
bind L = Rb
if p >= 0.0 { bind L = m_mul(m_inv(rg_mat_at(rest, p)), Rb) }
bind X = m_mul(m_inv(L), m_mul(m_inv(Pp), Pn))
bind sx = v3_len([list_get(X, 0), list_get(X, 4), list_get(X, 8)])
bind sy = v3_len([list_get(X, 1), list_get(X, 5), list_get(X, 9)])
bind sz = v3_len([list_get(X, 2), list_get(X, 6), list_get(X, 10)])
bind e = m_to_euler(X)
if kind == "r" || kind == "g" || kind == "s" {
st_lset(mk(o, "bp_tx"), b, list_get(X, 3)) st_lset(mk(o, "bp_ty"), b, list_get(X, 7)) st_lset(mk(o, "bp_tz"), b, list_get(X, 11))
st_lset(mk(o, "bp_rx"), b, list_get(e, 0)) st_lset(mk(o, "bp_ry"), b, list_get(e, 1)) st_lset(mk(o, "bp_rz"), b, list_get(e, 2))
st_lset(mk(o, "bp_sx"), b, sx) st_lset(mk(o, "bp_sy"), b, sy) st_lset(mk(o, "bp_sz"), b, sz)
}
bind i = i + 1.0
}
ps_mark(o)
}
# ── auto weights (adjustable; see modal_autoweights) ─────────────────────────
fn rg_auto_weights() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind n = rg_count(o)
bind nv = me_nverts(o)
if n < 0.5 || nv < 0.5 { st_set("status", "Need a mesh with bones") return 0.0 }
me_undo_push()
st_set(mk(o, "bound"), 1.0)
bind pw = st_get("aw_pow", 4.0)
bind env = st_get("aw_env", 0.15)
bind maxk = st_get("aw_max", 4.0)
bind iters = st_get("aw_smooth", 2.0)
bind cut = st_get("aw_cut", 0.02)
bind selonly = st_get("aw_selonly", 0.0) > 0.5
bind heat = st_get("aw_heat", 1.0) > 0.5
bind bsel = st_get(mk(o, "bn_sel"), [])
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
# dense nv x n weights
bind W = me_zeros(nv * n)
bind heads = list_new() bind tails = list_new() bind lens = list_new()
bind b = 0.0
while b < n { bind heads = list_push(heads, rg_head(o, b)) bind tails = list_push(tails, rg_tail(o, b)) bind lens = list_push(lens, rg_len(o, b)) bind b = b + 1.0 }
bind v = 0.0
while v < nv {
bind p = [list_get(vx, v), list_get(vy, v), list_get(vz, v)]
bind b = 0.0
while b < n {
bind use = true
if selonly { bind use = false if b < len(bsel) { if list_get(bsel, b) > 0.5 { bind use = true } } }
if use {
bind r = v3_seg_dist(p, list_get(heads, b), list_get(tails, b))
bind d = max(0.0, list_get(r, 0) - env * list_get(lens, b))
bind w = 1.0 / pow(d + 0.02, pw)
bind W = list_set(W, v * n + b, w)
}
bind b = b + 1.0
}
bind v = v + 1.0
}
rg_normalize_dense(W, nv, n)
bind W = st_get("aw_W", [])
# smoothing across mesh edges (surface-aware doubles the passes)
bind passes = iters
if heat { bind passes = iters * 2.0 }
if passes > 0.5 {
bind nb = me_vert_neighbours(o)
bind starts = list_get(nb, 0) bind vl = list_get(nb, 1)
bind it = 0.0
while it < passes {
bind W2 = W
bind v = 0.0
while v < nv {
bind k0 = list_get(starts, v) bind k1 = list_get(starts, v + 1.0)
bind cnt = k1 - k0
if cnt > 0.5 {
bind b = 0.0
while b < n {
bind acc = 0.0
bind k = k0
while k < k1 { bind acc = acc + list_get(W, list_get(vl, k) * n + b) bind k = k + 1.0 }
bind W2 = list_set(W2, v * n + b, list_get(W, v * n + b) * 0.5 + acc / cnt * 0.5)
bind b = b + 1.0
}
}
bind v = v + 1.0
}
bind W = W2
bind it = it + 1.0
}
}
# sparsify: top maxk per vertex above cut, normalised
bind wb = list_new()
bind v = 0.0
while v < nv {
bind chosen = list_new() bind cw = list_new()
bind k = 0.0
while k < maxk {
bind best = 0.0 - 1.0 bind bw = 0.0
bind b = 0.0
while b < n {
bind w = list_get(W, v * n + b)
bind taken = false
bind q = 0.0
while q < len(chosen) { if list_get(chosen, q) == b { bind taken = true } bind q = q + 1.0 }
if !taken && w > bw { bind bw = w bind best = b }
bind b = b + 1.0
}
if best >= 0.0 && bw >= cut { bind chosen = list_push(chosen, best) bind cw = list_push(cw, bw) }
bind k = k + 1.0
}
bind tot = 0.0
bind q = 0.0
while q < len(cw) { bind tot = tot + list_get(cw, q) bind q = q + 1.0 }
bind q = 0.0
while q < 4.0 {
if q < len(chosen) { bind wb = list_push(wb, list_get(chosen, q)) bind wb = list_push(wb, sel(tot > 0.0, list_get(cw, q) / tot, 0.0)) }
else { bind wb = list_push(wb, 0.0 - 1.0) bind wb = list_push(wb, 0.0) }
bind q = q + 1.0
}
bind v = v + 1.0
}
st_set(mk(o, "wb"), wb)
st_set("aw_W", [])
ps_mark(o)
st_set("dirty", 1.0)
}
fn rg_normalize_dense(W, nv, n) {
bind v = 0.0
while v < nv {
bind tot = 0.0
bind b = 0.0
while b < n { bind tot = tot + list_get(W, v * n + b) bind b = b + 1.0 }
if tot > 0.0 { bind b = 0.0 while b < n { bind W = list_set(W, v * n + b, list_get(W, v * n + b) / tot) bind b = b + 1.0 } }
bind v = v + 1.0
}
st_set("aw_W", W)
}
# ── weights: access ──────────────────────────────────────────────────────────
fn wp_get(wb, v, b) {
bind k = 0.0
while k < 4.0 { if list_get(wb, v * 8.0 + k * 2.0) == b { return list_get(wb, v * 8.0 + k * 2.0 + 1.0) } bind k = k + 1.0 }
return 0.0
}
# set weight of bone b on vertex v; returns the updated list
fn wp_set(wb, v, b, w) {
bind k = 0.0
while k < 4.0 { if list_get(wb, v * 8.0 + k * 2.0) == b { return list_set(wb, v * 8.0 + k * 2.0 + 1.0, w) } bind k = k + 1.0 }
if w <= 0.0 { return wb }
# free slot, else the smallest
bind slot = 0.0 - 1.0
bind k = 0.0
while k < 4.0 { if slot < 0.0 { if list_get(wb, v * 8.0 + k * 2.0) < 0.0 || list_get(wb, v * 8.0 + k * 2.0 + 1.0) <= 0.0 { bind slot = k } } bind k = k + 1.0 }
if slot < 0.0 {
bind sw = 2.0
bind k = 0.0
while k < 4.0 { if list_get(wb, v * 8.0 + k * 2.0 + 1.0) < sw { bind sw = list_get(wb, v * 8.0 + k * 2.0 + 1.0) bind slot = k } bind k = k + 1.0 }
}
bind wb = list_set(wb, v * 8.0 + slot * 2.0, b)
return list_set(wb, v * 8.0 + slot * 2.0 + 1.0, w)
}
# scale the other bones so the vertex sums to 1 (keeps bone b as is)
fn wp_normalize_vertex(wb, v, b) {
bind wbv = wp_get(wb, v, b)
bind others = 0.0
bind k = 0.0
while k < 4.0 { bind bb = list_get(wb, v * 8.0 + k * 2.0) if bb >= 0.0 && bb != b { bind others = others + list_get(wb, v * 8.0 + k * 2.0 + 1.0) } bind k = k + 1.0 }
bind target = max(0.0, 1.0 - wbv)
bind k = 0.0
while k < 4.0 {
bind bb = list_get(wb, v * 8.0 + k * 2.0)
if bb >= 0.0 && bb != b {
bind cur = list_get(wb, v * 8.0 + k * 2.0 + 1.0)
bind nw = sel(others > 0.000001, cur / others * target, sel(k == 0.0 && wbv < 1.0, target, 0.0))
bind wb = list_set(wb, v * 8.0 + k * 2.0 + 1.0, nw)
}
bind k = k + 1.0
}
return wb
}
fn wp_ensure(o) {
bind wb = st_get(mk(o, "wb"), [])
if len(wb) != me_nverts(o) * 8.0 { bind wb = me_wb_resize(wb, me_nverts(o)) st_set(mk(o, "wb"), wb) }
return wb
}
# heat colour for a weight 0..1
fn wp_heat(w) {
if w <= 0.0 { return c_rgb(40.0, 40.0, 140.0) }
if w < 0.25 { return c_mix(c_rgb(40.0, 40.0, 200.0), c_rgb(0.0, 200.0, 220.0), w / 0.25) }
if w < 0.5 { return c_mix(c_rgb(0.0, 200.0, 220.0), c_rgb(60.0, 220.0, 80.0), (w - 0.25) / 0.25) }
if w < 0.75 { return c_mix(c_rgb(60.0, 220.0, 80.0), c_rgb(240.0, 220.0, 60.0), (w - 0.5) / 0.25) }
return c_mix(c_rgb(240.0, 220.0, 60.0), c_rgb(240.0, 50.0, 40.0), (w - 0.75) / 0.25)
}
fn wp_face_color(o, f) {
bind b = rg_active(o)
bind wb = st_get(mk(o, "wb"), [])
if b < 0.0 || len(wb) < 0.5 { return c_rgb(40.0, 40.0, 140.0) }
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind s = list_get(fs, f) bind c = list_get(fc, f)
bind acc = 0.0
bind k = 0.0
while k < c { bind acc = acc + wp_get(wb, list_get(fi, s + k), b) bind k = k + 1.0 }
return wp_heat(acc / c)
}
# ── weight painting ──────────────────────────────────────────────────────────
fn wp_init_defaults() {
if st_has("wp_radius") < 0.5 { st_set("wp_radius", 40.0) }
if st_has("wp_strength") < 0.5 { st_set("wp_strength", 0.5) }
if st_has("wp_mode") < 0.5 { st_set("wp_mode", 0.0) }
if st_has("wp_falloff") < 0.5 { st_set("wp_falloff", 0.0) }
if st_has("wp_front") < 0.5 { st_set("wp_front", 1.0) }
if st_has("wp_autonorm") < 0.5 { st_set("wp_autonorm", 1.0) }
}
fn wp_input(x, y, w, h, inside) {
wp_init_defaults()
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind b = rg_active(o)
if u_pressed() && inside && !u_alt() {
if b < 0.0 { st_set("status", "Pick a bone in the outliner first") return 0.0 }
me_undo_push()
st_set("wp_painting", 1.0)
}
if st_get("wp_painting", 0.0) < 0.5 { return 0.0 }
if !u_down() { st_set("wp_painting", 0.0) ps_mark(o) return 0.0 }
if b < 0.0 { return 0.0 }
bind wb = wp_ensure(o)
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind radius = st_get("wp_radius", 40.0)
bind strength = st_get("wp_strength", 0.5)
bind mode = st_get("wp_mode", 0.0)
if u_ctrl() { bind mode = sel(mode == 0.0, 1.0, 0.0) }
bind fall = st_get("wp_falloff", 0.0)
bind front = st_get("wp_front", 1.0) > 0.5
bind autonorm = st_get("wp_autonorm", 1.0) > 0.5
bind mx = u_mx() bind my = u_my()
bind nb = []
if mode == 3.0 { bind nb = me_vert_neighbours(o) }
bind vnx = [] bind vny = [] bind vnz = []
bind cam = [0.0, 0.0, 0.0]
if front {
bind nrm = me_vertex_normals(o)
bind vnx = list_get(nrm, 0) bind vny = list_get(nrm, 1) bind vnz = list_get(nrm, 2)
bind cam = m_apply(m_inv(me_matrix(o)), v3_cam_pos())
}
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind rate = clampf(st_get("dt", 0.016) * 6.0, 0.05, 0.4)
bind v = 0.0
while v < len(px) {
if list_get(pd, v) > 0.05 {
bind dx = list_get(px, v) - mx bind dy = list_get(py, v) - my
bind d = sqrt(dx * dx + dy * dy)
if d < radius {
bind okv = true
if front {
bind toc = v3_sub(cam, [list_get(vx, v), list_get(vy, v), list_get(vz, v)])
if list_get(vnx, v) * list_get(toc, 0) + list_get(vny, v) * list_get(toc, 1) + list_get(vnz, v) * list_get(toc, 2) < 0.0 { bind okv = false }
}
if okv {
bind t = d / radius
bind f = 1.0
if fall == 0.0 { bind f = 1.0 - t * t * (3.0 - 2.0 * t) }
if fall == 1.0 { bind f = 1.0 - t }
bind inf = f * strength * rate
bind cur = wp_get(wb, v, b)
bind nw = cur
if mode == 0.0 { bind nw = min(1.0, cur + inf) }
if mode == 1.0 { bind nw = max(0.0, cur - inf) }
if mode == 2.0 { bind nw = cur + (strength - cur) * f * rate * 2.0 }
if mode == 3.0 {
bind starts = list_get(nb, 0) bind vl = list_get(nb, 1)
bind k0 = list_get(starts, v) bind k1 = list_get(starts, v + 1.0)
if k1 > k0 {
bind acc = 0.0
bind k = k0
while k < k1 { bind acc = acc + wp_get(wb, list_get(vl, k), b) bind k = k + 1.0 }
bind nw = cur + (acc / (k1 - k0) - cur) * f * strength * rate * 3.0
}
}
bind wb = wp_set(wb, v, b, nw)
if autonorm { bind wb = wp_normalize_vertex(wb, v, b) }
}
}
}
bind v = v + 1.0
}
st_set(mk(o, "wb"), wb)
st_set("dirty", 1.0)
}
fn wp_draw_brush() {
wp_init_defaults()
bind r = st_get("wp_radius", 40.0)
px_ring(u_mx(), u_my(), r, C_TEXT_STRONG, 0.7)
px_ring(u_mx(), u_my(), r * 0.5, C_TEXT_DIM, 0.4)
}
fn wp_toolbar(x, by) {
wp_init_defaults()
bind xx = x
bind names = ["Add", "Sub", "Set", "Blur"]
bind i = 0.0
while i < 4.0 {
if u_chip("wp_m" + num_str(i), xx + i * 46.0, by + 2.0, 42.0, 20.0, list_get(names, i), st_get("wp_mode", 0.0) == i, C_ORANGE) { st_set("wp_mode", i) }
bind i = i + 1.0
}
bind xx = xx + 192.0
bind nv = u_dragval("wp_rad", xx, by, 92.0, 24.0, st_get("wp_radius", 40.0), 2.0, 400.0, 0.5, "Radius " + fmt0(st_get("wp_radius", 40.0)))
if u_has(nv) { st_set("wp_radius", nv) }
bind xx = xx + 96.0
bind nv = u_dragval("wp_str", xx, by, 100.0, 24.0, st_get("wp_strength", 0.5), 0.0, 1.0, 0.005, "Strength " + fmt2(st_get("wp_strength", 0.5)))
if u_has(nv) { st_set("wp_strength", nv) }
bind xx = xx + 108.0
if u_tool("wp_norm", xx, by, 92.0, 24.0, "weight", "Normalize", false, C_GREEN, "Make every vertex sum to 1") { wp_normalize_all() }
if u_tool("wp_aw", xx + 94.0, by, 108.0, 24.0, "weight", "Auto weights", false, C_GREEN, "Recompute from bone distance (adjustable)") { do_action("rig.autoweights") }
return xx + 206.0
}
fn wp_props(x, y, w) {
wp_init_defaults()
bind o = me_sel_o()
bind cy = y
u_header(x, cy, w, "Brush", ds_col("lit"))
bind cy = cy + 20.0
if o < 0.0 { g_text(x, cy, 11.0, C_TEXT_DIM, "Select a mesh with bones.") return cy + 20.0 }
bind b = rg_active(o)
g_text(x, cy, 11.0, C_TEXT, "Bone: " + sel(b >= 0.0, rg_bone_name(o, b), "(none - pick one in the outliner)"))
bind cy = cy + 20.0
bind nv = u_param("wpp_rad", x, cy, w, "Radius (px) [ ]", st_get("wp_radius", 40.0), 2.0, 400.0, C_ORANGE, 0.0)
if u_has(nv) { st_set("wp_radius", nv) }
bind cy = cy + 34.0
bind nv = u_param("wpp_str", x, cy, w, "Strength", st_get("wp_strength", 0.5), 0.0, 1.0, C_ORANGE, 2.0)
if u_has(nv) { st_set("wp_strength", nv) }
bind cy = cy + 34.0
g_text(x, cy, 11.0, C_TEXT_DIM, "Falloff")
bind r = ds_selector("wpp_fall", x + 50.0, cy - 1.0, 11.0, ["SMOOTH", "LINEAR", "FLAT"], st_get("wp_falloff", 0.0), ds_col("lit"), "Brush falloff")
if r >= 0.0 { st_set("wp_falloff", r) }
bind cy = cy + 22.0
if u_check("wpp_front", x, cy, "Front faces only", st_get("wp_front", 1.0) > 0.5) { st_set("wp_front", 1.0 - st_get("wp_front", 1.0)) }
bind cy = cy + 22.0
if u_check("wpp_auto", x, cy, "Auto-normalize", st_get("wp_autonorm", 1.0) > 0.5) { st_set("wp_autonorm", 1.0 - st_get("wp_autonorm", 1.0)) }
bind cy = cy + 22.0
g_text(x, cy, 11.0, C_TEXT_DIM, "Ctrl while painting subtracts.")
bind cy = cy + 26.0
u_header(x, cy, w, "Weights", ds_col("ctl"))
bind cy = cy + 20.0
bind bw = floor((w - 6.0) / 2.0)
if u_button("wpp_aw", x, cy, bw, 22.0, "Auto weights...", C_BLUE) { do_action("rig.autoweights") }
if u_button("wpp_norm", x + bw + 6.0, cy, bw, 22.0, "Normalize all", 0.0) { wp_normalize_all() }
bind cy = cy + 26.0
if u_button("wpp_smooth", x, cy, bw, 22.0, "Smooth all", 0.0) { wp_smooth_all() }
if u_button("wpp_mirror", x + bw + 6.0, cy, bw, 22.0, "Mirror X", 0.0) { wp_mirror_x() }
bind cy = cy + 26.0
if u_button("wpp_clear", x, cy, bw, 22.0, "Clear bone", 0.0) { wp_clear_bone() }
if u_button("wpp_clearall", x + bw + 6.0, cy, bw, 22.0, "Clear all", C_RED) { wp_clear_all() }
bind cy = cy + 34.0
u_header(x, cy, w, "Assign", ds_col("func"))
bind cy = cy + 20.0
g_text_wrap(x, cy, w, 11.0, C_TEXT_DIM, "Set the active bone's weight on the vertices selected in Edit mode (" + num_str(me_sel_count(o)) + " selected).")
bind cy = cy + 34.0
bind nv = u_param("wpp_assign", x, cy, w, "Weight to assign", st_get("wp_assign", 1.0), 0.0, 1.0, C_GREEN, 2.0)
if u_has(nv) { st_set("wp_assign", nv) }
bind cy = cy + 34.0
if u_button("wpp_do_assign", x, cy, bw, 22.0, "Assign", C_BLUE) { wp_assign_selected(st_get("wp_assign", 1.0)) }
if u_button("wpp_do_remove", x + bw + 6.0, cy, bw, 22.0, "Remove", 0.0) { wp_assign_selected(0.0) }
bind cy = cy + 34.0
u_header(x, cy, w, "Vertex", ds_col("mod"))
bind cy = cy + 20.0
bind av = st_get("act_v", 0.0 - 1.0)
bind wb = st_get(mk(o, "wb"), [])
if av >= 0.0 && av < me_nverts(o) && len(wb) >= me_nverts(o) * 8.0 {
g_text(x, cy, 11.0, C_TEXT, "Vertex #" + num_str(av))
bind cy = cy + 18.0
bind k = 0.0
while k < 4.0 {
bind bb = list_get(wb, av * 8.0 + k * 2.0)
bind ww = list_get(wb, av * 8.0 + k * 2.0 + 1.0)
bind label = sel(bb >= 0.0, rg_bone_name(o, bb), "(empty)")
g_text_fit(x, cy + 4.0, w - 100.0, 11.0, sel(bb >= 0.0, C_TEXT, C_TEXT_DIM), label)
if bb >= 0.0 {
bind nv = u_dragval("wpp_vw" + num_str(k), x + w - 90.0, cy, 90.0, 20.0, ww, 0.0, 1.0, 0.005, fmt3(ww))
if u_has(nv) { if u_pressed() { me_undo_push() } bind wb = list_set(wb, av * 8.0 + k * 2.0 + 1.0, nv) st_set(mk(o, "wb"), wb) ps_mark(o) st_set("dirty", 1.0) }
} else if b >= 0.0 {
if u_button("wpp_vadd" + num_str(k), x + w - 90.0, cy, 90.0, 20.0, "+ " + rg_bone_name(o, b), 0.0) { me_undo_push() bind wb = wp_set(wb, av, b, 0.5) st_set(mk(o, "wb"), wb) ps_mark(o) }
}
bind cy = cy + 24.0
bind k = k + 1.0
}
} else { g_text_wrap(x, cy, w, 11.0, C_TEXT_DIM, "Click a vertex in Edit mode to inspect and type its weights here.") bind cy = cy + 30.0 }
return cy + 10.0
}
fn wp_keys(shift, ctrl, alt) {
if key_pressed("lbracket") || key_pressed("[") { st_set("wp_radius", max(2.0, st_get("wp_radius", 40.0) * 0.8)) return true }
if key_pressed("rbracket") || key_pressed("]") { st_set("wp_radius", min(400.0, st_get("wp_radius", 40.0) * 1.25)) return true }
if key_pressed("1") { st_set("wp_mode", 0.0) return true }
if key_pressed("2") { st_set("wp_mode", 1.0) return true }
if key_pressed("3") { st_set("wp_mode", 2.0) return true }
if key_pressed("4") { st_set("wp_mode", 3.0) return true }
return false
}
fn wp_assign_selected(w) {
bind o = me_sel_o()
bind b = rg_active(o)
if o < 0.0 || b < 0.0 { st_set("status", "Pick a bone first") return 0.0 }
me_undo_push()
bind wb = wp_ensure(o)
bind verts = me_selected_verts(o)
bind i = 0.0
while i < len(verts) {
bind v = list_get(verts, i)
bind wb = wp_set(wb, v, b, w)
bind wb = wp_normalize_vertex(wb, v, b)
bind i = i + 1.0
}
st_set(mk(o, "wb"), wb)
st_set(mk(o, "bound"), 1.0)
ps_mark(o)
st_set("dirty", 1.0)
st_set("status", "Assigned " + fmt2(w) + " to " + num_str(len(verts)) + " vertices for " + rg_bone_name(o, b))
}
fn wp_normalize_all() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind wb = wp_ensure(o)
bind nv = me_nverts(o)
bind v = 0.0
while v < nv {
bind tot = 0.0
bind k = 0.0
while k < 4.0 { if list_get(wb, v * 8.0 + k * 2.0) >= 0.0 { bind tot = tot + list_get(wb, v * 8.0 + k * 2.0 + 1.0) } bind k = k + 1.0 }
if tot > 0.000001 { bind k = 0.0 while k < 4.0 { if list_get(wb, v * 8.0 + k * 2.0) >= 0.0 { bind wb = list_set(wb, v * 8.0 + k * 2.0 + 1.0, list_get(wb, v * 8.0 + k * 2.0 + 1.0) / tot) } bind k = k + 1.0 } }
bind v = v + 1.0
}
st_set(mk(o, "wb"), wb)
ps_mark(o)
st_set("status", "Weights normalized")
}
fn wp_clear_all() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
st_set(mk(o, "wb"), me_wb_resize([], me_nverts(o)))
ps_mark(o)
st_set("status", "Weights cleared")
}
fn wp_clear_bone() {
bind o = me_sel_o()
bind b = rg_active(o)
if o < 0.0 || b < 0.0 { return 0.0 }
me_undo_push()
bind wb = wp_ensure(o)
bind nv = me_nverts(o)
bind v = 0.0
while v < nv { bind wb = wp_set(wb, v, b, 0.0) bind v = v + 1.0 }
st_set(mk(o, "wb"), wb)
ps_mark(o)
}
fn wp_smooth_all() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind wb = wp_ensure(o)
bind n = rg_count(o)
bind nb = me_vert_neighbours(o)
bind starts = list_get(nb, 0) bind vl = list_get(nb, 1)
bind nv = me_nverts(o)
bind out = wb
bind v = 0.0
while v < nv {
bind k0 = list_get(starts, v) bind k1 = list_get(starts, v + 1.0)
if k1 > k0 {
bind b = 0.0
while b < n {
bind acc = 0.0
bind k = k0
while k < k1 { bind acc = acc + wp_get(wb, list_get(vl, k), b) bind k = k + 1.0 }
bind cur = wp_get(wb, v, b)
bind nw = cur * 0.5 + acc / (k1 - k0) * 0.5
if nw > 0.001 || cur > 0.0 { bind out = wp_set(out, v, b, nw) }
bind b = b + 1.0
}
}
bind v = v + 1.0
}
st_set(mk(o, "wb"), out)
wp_normalize_all()
st_set("status", "Weights smoothed")
}
# copy weights from +X vertices to their -X mirrors, swapping .L/.R bone names
fn wp_mirror_x() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind wb = wp_ensure(o)
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind names = st_get(mk(o, "bn_name"), [])
bind n = len(names)
bind mirror_b = list_new()
bind b = 0.0
while b < n {
bind nm = list_get(names, b)
bind suf = str_slice(nm, str_len(nm) - 2.0, 2.0)
bind target = b
bind want = ""
if suf == ".L" { bind want = str_slice(nm, 0.0, str_len(nm) - 2.0) + ".R" }
if suf == ".R" { bind want = str_slice(nm, 0.0, str_len(nm) - 2.0) + ".L" }
if suf == "_L" { bind want = str_slice(nm, 0.0, str_len(nm) - 2.0) + "_R" }
if suf == "_R" { bind want = str_slice(nm, 0.0, str_len(nm) - 2.0) + "_L" }
if want != "" { bind j = 0.0 while j < n { if list_get(names, j) == want { bind target = j } bind j = j + 1.0 } }
bind mirror_b = list_push(mirror_b, target)
bind b = b + 1.0
}
bind nv = len(vx)
bind count = 0.0
bind v = 0.0
while v < nv {
if list_get(vx, v) > 0.0001 {
# find the mirrored vertex
bind best = 0.0 - 1.0 bind bd = 0.001
bind u = 0.0
while u < nv {
bind d = abs(list_get(vx, u) + list_get(vx, v)) + abs(list_get(vy, u) - list_get(vy, v)) + abs(list_get(vz, u) - list_get(vz, v))
if d < bd { bind bd = d bind best = u }
bind u = u + 1.0
}
if best >= 0.0 {
bind k = 0.0
while k < 4.0 {
bind bb = list_get(wb, v * 8.0 + k * 2.0)
bind ww = list_get(wb, v * 8.0 + k * 2.0 + 1.0)
bind wb = list_set(wb, best * 8.0 + k * 2.0, sel(bb >= 0.0, list_get(mirror_b, bb), 0.0 - 1.0))
bind wb = list_set(wb, best * 8.0 + k * 2.0 + 1.0, ww)
bind k = k + 1.0
}
bind count = count + 1.0
}
}
bind v = v + 1.0
}
st_set(mk(o, "wb"), wb)
ps_mark(o)
st_set("status", "Mirrored weights onto " + num_str(count) + " vertices")
}
# ── outliner bone list + properties ──────────────────────────────────────────
fn rg_bone_list(x, y, w, h) {
bind o = me_sel_o()
u_header(x + 8.0, y, w - 16.0, "Bones", ds_col("mod"))
if o < 0.0 { return 0.0 }
bind n = rg_count(o)
bind rowh = 20.0
bind off = u_scroll_y("bl_scroll", x, y + 18.0, w, h - 18.0, n * rowh + 4.0, st_get("bl_off", 0.0))
st_set("bl_off", off)
u_clip(x, y + 18.0, w, h - 18.0)
bind s = st_get(mk(o, "bn_sel"), [])
bind act = rg_active(o)
bind b = 0.0
while b < n {
bind ry = y + 20.0 + b * rowh - off
bind d = rg_depth(o, b)
bind on = false
if b < len(s) { if list_get(s, b) > 0.5 { bind on = true } }
bind r = u_row("bl_row" + num_str(b), x + 2.0 + d * 10.0, ry, w - 4.0 - d * 10.0, rowh, rg_bone_name(o, b), on || b == act, sel(b == act, C_ACT_EL, C_BONE), "bone")
if r > 1.5 { st_set("act_bone", b) modal_text("Rename bone", "bone", rg_bone_name(o, b)) }
else if r > 0.5 { rg_select(o, b, u_shift()) }
bind b = b + 1.0
}
u_unclip()
if n < 0.5 { g_text_wrap(x + 10.0, y + 24.0, w - 20.0, 11.0, C_TEXT_DIM, "No bones yet. Shift+A adds one at the 3D cursor; E extrudes a child.") }
}
fn rg_props(x, y, w) {
bind o = me_sel_o()
bind cy = y
u_header(x, cy, w, "Armature", ds_col("mod"))
bind cy = cy + 20.0
if o < 0.0 { g_text(x, cy, 11.0, C_TEXT_DIM, "Select an object (or Shift+A to make one).") return cy + 20.0 }
g_text(x, cy, 11.0, C_TEXT, me_name(o) + ": " + num_str(rg_count(o)) + " bones" + sel(st_get(mk(o, "bound"), 0.0) > 0.5, ", bound", ""))
bind cy = cy + 20.0
bind bw = floor((w - 6.0) / 2.0)
if u_button("rgp_add", x, cy, bw, 22.0, "Add bone", C_BLUE) { rg_add_bone() }
if u_button("rgp_ext", x + bw + 6.0, cy, bw, 22.0, "Extrude (E)", 0.0) { rg_extrude() }
bind cy = cy + 26.0
if u_button("rgp_par", x, cy, bw, 22.0, "Parent (Ctrl+P)", 0.0) { rg_parent() }
if u_button("rgp_unpar", x + bw + 6.0, cy, bw, 22.0, "Unparent", 0.0) { rg_unparent() }
bind cy = cy + 26.0
if u_button("rgp_symm", x, cy, bw, 22.0, "Symmetrize L/R", 0.0) { rg_symmetrize() }
if u_button("rgp_del", x + bw + 6.0, cy, bw, 22.0, "Delete (X)", C_RED) { rg_delete() }
bind cy = cy + 30.0
if u_check("rgp_tip", x, cy, "Transform tails only (T)", st_get("rg_tipmode", 0.0) > 0.5) { st_set("rg_tipmode", 1.0 - st_get("rg_tipmode", 0.0)) }
bind cy = cy + 22.0
if u_check("rgp_names", x, cy, "Show bone names", st_get("rg_names", 1.0) > 0.5) { st_set("rg_names", 1.0 - st_get("rg_names", 1.0)) }
bind cy = cy + 30.0
u_header(x, cy, w, "Bone", ds_col("bind"))
bind cy = cy + 20.0
bind b = rg_active(o)
if b < 0.0 { g_text(x, cy, 11.0, C_TEXT_DIM, "No active bone - click one.") bind cy = cy + 24.0 }
else {
bind nm = u_textfield("rgp_name", x, cy, w, 22.0, sel(st_get("focus", "") == "rgp_name", st_get("rgp_name_t", rg_bone_name(o, b)), rg_bone_name(o, b)))
st_set("rgp_name_t", nm)
if st_get("focus", "") != "rgp_name" && nm != rg_bone_name(o, b) && str_trim(nm) != "" { rg_rename(o, b, str_trim(nm)) }
bind cy = cy + 28.0
bind names = st_get(mk(o, "bn_name"), [])
bind pnames = ["(none)"]
bind i = 0.0
while i < len(names) { bind pnames = list_push(pnames, list_get(names, i)) bind i = i + 1.0 }
g_text(x, cy + 5.0, 11.0, C_TEXT_DIM, "Parent")
bind cur = list_get(st_get(mk(o, "bn_par"), []), b)
bind nv = u_combo("rgp_parent", x + 50.0, cy, w - 50.0, 22.0, pnames, cur + 1.0)
if nv >= 0.0 && nv - 1.0 != b { me_undo_push() st_lset(mk(o, "bn_par"), b, nv - 1.0) }
bind cy = cy + 28.0
bind h = rg_head(o, b)
g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Head")
bind r = u_vec3("rgp_head", x + 40.0, cy, w - 40.0, list_get(h, 0), list_get(h, 1), list_get(h, 2), 0.0 - 10000.0, 10000.0, 0.01)
if list_get(r, 0) >= 0.0 { if u_pressed() { me_undo_push() } bind h = list_set(h, list_get(r, 0), list_get(r, 1)) rg_set_head(o, b, h) st_set("dirty", 1.0) }
bind cy = cy + 26.0
bind t = rg_tail(o, b)
g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Tail")
bind r = u_vec3("rgp_tail", x + 40.0, cy, w - 40.0, list_get(t, 0), list_get(t, 1), list_get(t, 2), 0.0 - 10000.0, 10000.0, 0.01)
if list_get(r, 0) >= 0.0 { if u_pressed() { me_undo_push() } bind t = list_set(t, list_get(r, 0), list_get(r, 1)) rg_set_tail(o, b, t) st_set("dirty", 1.0) }
bind cy = cy + 26.0
g_text(x, cy, 11.0, C_TEXT_DIM, "Length " + fmt3(rg_len(o, b)) + " depth " + num_str(rg_depth(o, b)))
bind cy = cy + 24.0
}
u_header(x, cy, w, "Skin", ds_col("ctl"))
bind cy = cy + 20.0
if u_button("rgp_bind", x, cy, bw, 22.0, sel(st_get(mk(o, "bound"), 0.0) > 0.5, "Bound", "Bind mesh"), sel(st_get(mk(o, "bound"), 0.0) > 0.5, 0.0, C_BLUE)) { rg_bind() }
if u_button("rgp_aw", x + bw + 6.0, cy, bw, 22.0, "Auto weights...", C_BLUE) { do_action("rig.autoweights") }
bind cy = cy + 26.0
if u_button("rgp_paint", x, cy, bw, 22.0, "Paint weights", 0.0) { set_mode(3.0) }
if u_button("rgp_pose", x + bw + 6.0, cy, bw, 22.0, "Pose", 0.0) { set_mode(4.0) }
return cy + 30.0
}
fn rg_keys(shift, ctrl, alt) {
if key_pressed("a") && shift { rg_add_bone() return true }
if key_pressed("e") && !ctrl { rg_extrude() return true }
if key_pressed("p") && ctrl { rg_parent() return true }
if key_pressed("p") && alt { rg_unparent() return true }
if key_pressed("f2") { do_action("rig.rename") return true }
if key_pressed("t") && !ctrl { st_set("rg_tipmode", 1.0 - st_get("rg_tipmode", 0.0)) return true }
if key_pressed("m") && shift { rg_symmetrize() return true }
return false
}