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/pose.ling
# ━━━ pose.ling — pose channels, linear blend skinning, actions + keyframes for bones ━━━
# Pose channels per bone (object prefix): bp_rx bp_ry bp_rz (radians, local XYZ), bp_tx/ty/tz, bp_sx/sy/sz.
# Actions: act_names (list), act_cur (index), act_data (list of key lists), act_len (end frame per action).
# A key is 4 numbers: bone, channel (0 rx .. 8 sz), frame, value.
fn ps_mark(o) { st_set("pv_" + num_str(o), st_get("pv_" + num_str(o), 0.0) + 1.0) }
fn ps_ver(o) { return st_get("pv_" + num_str(o), 0.0) }
fn ps_identity(o) {
bind n = rg_count(o)
bind keys = ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz"]
bind k = 0.0
while k < 6.0 {
bind l = st_get(mk(o, list_get(keys, k)), [])
bind i = 0.0
while i < len(l) { if abs(list_get(l, i)) > 0.000001 { return false } bind i = i + 1.0 }
bind k = k + 1.0
}
bind keys2 = ["bp_sx", "bp_sy", "bp_sz"]
bind k = 0.0
while k < 3.0 {
bind l = st_get(mk(o, list_get(keys2, k)), [])
bind i = 0.0
while i < len(l) { if abs(list_get(l, i) - 1.0) > 0.000001 { return false } bind i = i + 1.0 }
bind k = k + 1.0
}
return true
}
# object-space pose matrix per bone (flat 12 each): P = P_parent * L * T * R * S
fn ps_pose_matrices(o) {
bind n = rg_count(o)
bind rest = rg_rest_matrices(o)
bind par = st_get(mk(o, "bn_par"), [])
bind rx = st_get(mk(o, "bp_rx"), []) bind ry = st_get(mk(o, "bp_ry"), []) bind rz = st_get(mk(o, "bp_rz"), [])
bind tx = st_get(mk(o, "bp_tx"), []) bind ty = st_get(mk(o, "bp_ty"), []) bind tz = st_get(mk(o, "bp_tz"), [])
bind sx = st_get(mk(o, "bp_sx"), []) bind sy = st_get(mk(o, "bp_sy"), []) bind sz = st_get(mk(o, "bp_sz"), [])
bind out = me_zeros(n * 12.0)
bind done = me_zeros(n)
# bones may reference parents with larger indices: iterate until all are resolved
bind remaining = n
bind guard = 0.0
while remaining > 0.5 && guard < n + 2.0 {
bind b = 0.0
while b < n {
if list_get(done, b) < 0.5 {
bind p = list_get(par, b)
bind ready = p < 0.0
if p >= 0.0 { if list_get(done, p) > 0.5 { bind ready = true } }
if ready {
bind Rb = rg_mat_at(rest, b)
bind L = Rb
bind Pp = m_ident()
if p >= 0.0 { bind L = m_mul(m_inv(rg_mat_at(rest, p)), Rb) bind Pp = rg_mat_at(out, p) }
bind X = m_trs(sel(b < len(tx), list_get(tx, b), 0.0), sel(b < len(ty), list_get(ty, b), 0.0), sel(b < len(tz), list_get(tz, b), 0.0), sel(b < len(rx), list_get(rx, b), 0.0), sel(b < len(ry), list_get(ry, b), 0.0), sel(b < len(rz), list_get(rz, b), 0.0), sel(b < len(sx), list_get(sx, b), 1.0), sel(b < len(sy), list_get(sy, b), 1.0), sel(b < len(sz), list_get(sz, b), 1.0))
bind P = m_mul(Pp, m_mul(L, X))
bind k = 0.0
while k < 12.0 { bind out = list_set(out, b * 12.0 + k, list_get(P, k)) bind k = k + 1.0 }
bind done = list_set(done, b, 1.0)
bind remaining = remaining - 1.0
}
}
bind b = b + 1.0
}
bind guard = guard + 1.0
}
return out
}
# skinned vertex positions [vx, vy, vz] or [] when the rest pose should be shown
fn ps_deformed(o) {
if st_get(mk(o, "bound"), 0.0) < 0.5 { return [] }
bind n = rg_count(o)
if n < 0.5 { return [] }
bind mode = st_get("mode", 0.0)
if mode == 2.0 || mode == 1.0 { return [] }
if !(mode == 4.0 || tl_playing()) { if ps_identity(o) { return [] } }
bind ver = ps_ver(o)
if st_get(pj(o, "dver"), 0.0 - 1.0) == ver {
bind cdx = st_get(pj(o, "dx"), [])
if len(cdx) == me_nverts(o) { return [cdx, st_get(pj(o, "dy"), []), st_get(pj(o, "dz"), [])] }
}
bind rest = rg_rest_matrices(o)
bind pm = ps_pose_matrices(o)
# skin matrices K = P * inv(R)
bind K = me_zeros(n * 12.0)
bind b = 0.0
while b < n {
bind m = m_mul(rg_mat_at(pm, b), m_inv(rg_mat_at(rest, b)))
bind k = 0.0
while k < 12.0 { bind K = list_set(K, b * 12.0 + k, list_get(m, k)) bind k = k + 1.0 }
bind b = b + 1.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 nv = len(vx)
bind dx = list_new() bind dy = list_new() bind dz = list_new()
bind v = 0.0
while v < nv {
bind x = list_get(vx, v) bind y = list_get(vy, v) bind z = list_get(vz, v)
bind ax = 0.0 bind ay = 0.0 bind az = 0.0 bind tot = 0.0
if v * 8.0 + 7.0 < len(wb) {
bind k = 0.0
while k < 4.0 {
bind bb = list_get(wb, v * 8.0 + k * 2.0)
bind w = list_get(wb, v * 8.0 + k * 2.0 + 1.0)
if bb >= 0.0 && bb < n && w > 0.0 {
bind q = bb * 12.0
bind ax = ax + w * (list_get(K, q) * x + list_get(K, q + 1.0) * y + list_get(K, q + 2.0) * z + list_get(K, q + 3.0))
bind ay = ay + w * (list_get(K, q + 4.0) * x + list_get(K, q + 5.0) * y + list_get(K, q + 6.0) * z + list_get(K, q + 7.0))
bind az = az + w * (list_get(K, q + 8.0) * x + list_get(K, q + 9.0) * y + list_get(K, q + 10.0) * z + list_get(K, q + 11.0))
bind tot = tot + w
}
bind k = k + 1.0
}
}
if tot >= 0.999 { bind dx = list_push(dx, ax / tot) bind dy = list_push(dy, ay / tot) bind dz = list_push(dz, az / tot) }
else if tot > 0.000001 { bind dx = list_push(dx, ax + x * (1.0 - tot)) bind dy = list_push(dy, ay + y * (1.0 - tot)) bind dz = list_push(dz, az + z * (1.0 - tot)) }
else { bind dx = list_push(dx, x) bind dy = list_push(dy, y) bind dz = list_push(dz, z) }
bind v = v + 1.0
}
st_set(pj(o, "dx"), dx) st_set(pj(o, "dy"), dy) st_set(pj(o, "dz"), dz)
st_set(pj(o, "dver"), ver)
return [dx, dy, dz]
}
# ── actions / keyframes ──────────────────────────────────────────────────────
fn ps_channel_key(ch) { return list_get(["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"], ch) }
fn ps_action_count(o) { return len(st_get(mk(o, "act_names"), [])) }
fn ps_cur_action(o) {
bind c = st_get(mk(o, "act_cur"), 0.0 - 1.0)
if c >= ps_action_count(o) { return 0.0 - 1.0 }
return c
}
fn ps_new_action(o, name) {
if o < 0.0 { return 0.0 - 1.0 }
st_set(mk(o, "act_names"), list_push(st_get(mk(o, "act_names"), []), name))
st_set(mk(o, "act_data"), list_push(st_get(mk(o, "act_data"), []), []))
st_set(mk(o, "act_len"), list_push(st_get(mk(o, "act_len"), []), st_get("tl_end", 48.0)))
st_set(mk(o, "act_cur"), ps_action_count(o) - 1.0)
st_set("dirty", 1.0)
st_set("status", "New action: " + name)
return ps_action_count(o) - 1.0
}
fn ps_action_keys(o) {
bind a = ps_cur_action(o)
if a < 0.0 { return [] }
return list_get(st_get(mk(o, "act_data"), []), a)
}
fn ps_set_action_keys(o, keys) {
bind a = ps_cur_action(o)
if a < 0.0 { return 0.0 }
bind d = st_get(mk(o, "act_data"), [])
bind d = list_set(d, a, keys)
st_set(mk(o, "act_data"), d)
st_set("dirty", 1.0)
tl_mark_dirty()
}
# set/replace one key
fn ps_put_key(keys, b, ch, frame, v) {
bind i = 0.0
while i + 3.0 < len(keys) {
if list_get(keys, i) == b && list_get(keys, i + 1.0) == ch && abs(list_get(keys, i + 2.0) - frame) < 0.001 { return list_set(keys, i + 3.0, v) }
bind i = i + 4.0
}
bind keys = list_push(keys, b) bind keys = list_push(keys, ch) bind keys = list_push(keys, frame)
return list_push(keys, v)
}
fn ps_key() {
bind o = me_sel_o()
if o < 0.0 || rg_count(o) < 0.5 { st_set("status", "No bones to key") return 0.0 }
bind bones = rg_selected(o)
if len(bones) < 0.5 {
bind all = list_new()
bind b = 0.0
while b < rg_count(o) { bind all = list_push(all, b) bind b = b + 1.0 }
bind bones = all
}
if ps_cur_action(o) < 0.0 { ps_new_action(o, "Action") }
me_undo_push()
bind keys = ps_action_keys(o)
bind frame = tl_frame()
bind i = 0.0
while i < len(bones) {
bind b = list_get(bones, i)
bind ch = 0.0
while ch < 9.0 {
bind l = st_get(mk(o, ps_channel_key(ch)), [])
bind v = sel(b < len(l), list_get(l, b), sel(ch >= 6.0, 1.0, 0.0))
bind keys = ps_put_key(keys, b, ch, frame, v)
bind ch = ch + 1.0
}
bind i = i + 1.0
}
ps_set_action_keys(o, keys)
bind alen = st_get(mk(o, "act_len"), [])
bind a = ps_cur_action(o)
if frame > list_get(alen, a) { st_lset(mk(o, "act_len"), a, frame) }
st_set("status", "Keyed " + num_str(len(bones)) + " bones at frame " + fmt0(frame))
}
fn ps_unkey() {
bind o = me_sel_o()
if o < 0.0 || ps_cur_action(o) < 0.0 { return 0.0 }
me_undo_push()
bind bones = rg_selected(o)
bind keys = ps_action_keys(o)
bind frame = tl_frame()
bind out = list_new()
bind i = 0.0
while i + 3.0 < len(keys) {
bind b = list_get(keys, i)
bind on = false
bind q = 0.0
while q < len(bones) { if list_get(bones, q) == b { bind on = true } bind q = q + 1.0 }
if len(bones) < 0.5 { bind on = true }
if !(on && abs(list_get(keys, i + 2.0) - frame) < 0.001) {
bind out = list_push(out, b) bind out = list_push(out, list_get(keys, i + 1.0)) bind out = list_push(out, list_get(keys, i + 2.0)) bind out = list_push(out, list_get(keys, i + 3.0))
}
bind i = i + 4.0
}
ps_set_action_keys(o, out)
st_set("status", "Removed keys at frame " + fmt0(frame))
}
# evaluate the current action at a frame into the pose channels
fn ps_eval(o, frame) {
bind keys = ps_action_keys(o)
if len(keys) < 3.5 { return 0.0 }
bind n = rg_count(o)
# per bone/channel: prev key (frame <= f) and next key (frame > f)
bind pf = me_fill(n * 9.0, 0.0 - 1000000.0) bind pv = me_zeros(n * 9.0)
bind nf = me_fill(n * 9.0, 1000000.0) bind nv = me_zeros(n * 9.0)
bind has = me_zeros(n * 9.0)
bind i = 0.0
while i + 3.0 < len(keys) {
bind b = list_get(keys, i) bind ch = list_get(keys, i + 1.0) bind kf = list_get(keys, i + 2.0) bind kv = list_get(keys, i + 3.0)
if b < n {
bind idx = b * 9.0 + ch
bind has = list_set(has, idx, 1.0)
if kf <= frame && kf > list_get(pf, idx) { bind pf = list_set(pf, idx, kf) bind pv = list_set(pv, idx, kv) }
if kf > frame && kf < list_get(nf, idx) { bind nf = list_set(nf, idx, kf) bind nv = list_set(nv, idx, kv) }
}
bind i = i + 4.0
}
bind b = 0.0
while b < n {
bind ch = 0.0
while ch < 9.0 {
bind idx = b * 9.0 + ch
if list_get(has, idx) > 0.5 {
bind v = 0.0
bind p = list_get(pf, idx) bind q = list_get(nf, idx)
if p < 0.0 - 999999.0 { bind v = list_get(nv, idx) }
else if q > 999999.0 { bind v = list_get(pv, idx) }
else {
bind t = (frame - p) / max(0.000001, q - p)
bind t = t * t * (3.0 - 2.0 * t)
bind a = list_get(pv, idx) bind c = list_get(nv, idx)
if ch < 3.0 { bind c = a + mx_wrap_angle(c - a) }
bind v = a + (c - a) * t
}
st_lset(mk(o, ps_channel_key(ch)), b, v)
}
bind ch = ch + 1.0
}
bind b = b + 1.0
}
ps_mark(o)
}
fn ps_key_frames(o) {
bind keys = ps_action_keys(o)
bind out = list_new()
bind i = 0.0
while i + 3.0 < len(keys) {
bind f = list_get(keys, i + 2.0)
bind dup = false
bind q = 0.0
while q < len(out) { if abs(list_get(out, q) - f) < 0.001 { bind dup = true } bind q = q + 1.0 }
if !dup { bind out = list_push(out, f) }
bind i = i + 4.0
}
return out
}
fn ps_clear() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind bones = rg_selected(o)
bind n = rg_count(o)
bind b = 0.0
while b < n {
bind on = len(bones) < 0.5
bind q = 0.0
while q < len(bones) { if list_get(bones, q) == b { bind on = true } bind q = q + 1.0 }
if on {
bind ch = 0.0
while ch < 9.0 { st_lset(mk(o, ps_channel_key(ch)), b, sel(ch >= 6.0, 1.0, 0.0)) bind ch = ch + 1.0 }
}
bind b = b + 1.0
}
ps_mark(o)
st_set("status", "Pose cleared")
}
fn ps_apply_rest() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind pm = ps_pose_matrices(o)
bind n = rg_count(o)
bind b = 0.0
while b < n {
bind m = rg_mat_at(pm, b)
bind ln = rg_len(o, b)
rg_set_head(o, b, m_apply(m, [0.0, 0.0, 0.0]))
rg_set_tail(o, b, m_apply(m, [0.0, ln, 0.0]))
bind b = b + 1.0
}
# bake the deformation into the mesh so it matches the new rest
bind d = ps_deformed(o)
if len(d) > 2.5 { st_set(mk(o, "vx"), list_get(d, 0)) st_set(mk(o, "vy"), list_get(d, 1)) st_set(mk(o, "vz"), list_get(d, 2)) }
bind bones = rg_selected(o)
rg_sel_all(o, false)
ps_clear()
st_set("status", "Pose applied as the new rest pose")
}
fn ps_bake() {
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
me_undo_push()
bind d = ps_deformed(o)
if len(d) < 2.5 { st_set("status", "Nothing to bake (no deformation)") return 0.0 }
st_set(mk(o, "vx"), list_get(d, 0)) st_set(mk(o, "vy"), list_get(d, 1)) st_set(mk(o, "vz"), list_get(d, 2))
rg_sel_all(o, false)
ps_clear()
st_set("status", "Deformed mesh baked")
}
# ── UI ───────────────────────────────────────────────────────────────────────
fn ps_toolbar(x, by) {
bind o = me_sel_o()
bind xx = x
if o < 0.0 { return xx }
bind names = st_get(mk(o, "act_names"), [])
bind cn = list_new()
bind i = 0.0
while i < len(names) { bind cn = list_push(cn, list_get(names, i)) bind i = i + 1.0 }
bind cn = list_push(cn, "+ New action")
bind cur = ps_cur_action(o)
bind nv = u_combo("ps_act", xx, by, 150.0, 24.0, cn, sel(cur < 0.0, len(cn) - 1.0, cur))
if nv >= 0.0 { if nv >= len(names) { modal_text("New action", "action", "Action") } else { st_set(mk(o, "act_cur"), nv) tl_mark_dirty() } }
bind xx = xx + 156.0
if u_tool("ps_key", xx, by, 64.0, 24.0, "key", "Key (I)", false, C_GREEN, "Insert keyframes for the selected bones") { ps_key() }
if u_tool("ps_unkey", xx + 66.0, by, 64.0, 24.0, "key_off", "Unkey", false, C_RED, "Remove keyframes at this frame (Alt+I)") { ps_unkey() }
if u_chip("ps_autokey", xx + 136.0, by + 2.0, 70.0, 20.0, "Auto key", st_get("autokey", 0.0) > 0.5, C_RED) { st_set("autokey", 1.0 - st_get("autokey", 0.0)) }
if u_tool("ps_clear", xx + 212.0, by, 74.0, 24.0, "refresh", "Clear pose", false, 0.0, "Reset the selected bones (Alt+R)") { ps_clear() }
if u_tool("ps_g", xx + 290.0, by, 28.0, 24.0, "move", "", v3_modal_active() && st_get("tr_kind", "") == "g", C_BLUE, "Move bone (G)") { v3_begin_modal("g") }
if u_tool("ps_r", xx + 320.0, by, 28.0, 24.0, "rotate", "", v3_modal_active() && st_get("tr_kind", "") == "r", C_BLUE, "Rotate bone (R)") { v3_begin_modal("r") }
if u_tool("ps_s", xx + 350.0, by, 28.0, 24.0, "scale", "", v3_modal_active() && st_get("tr_kind", "") == "s", C_BLUE, "Scale bone (S)") { v3_begin_modal("s") }
return xx + 386.0
}
fn ps_props(x, y, w) {
bind o = me_sel_o()
bind cy = y
u_header(x, cy, w, "Pose", ds_col("bind"))
bind cy = cy + 20.0
if o < 0.0 || rg_count(o) < 0.5 { g_text_wrap(x, cy, w, 11.0, C_TEXT_DIM, "Select a rigged mesh. Bones come from the Rig tab.") return cy + 30.0 }
if st_get(mk(o, "bound"), 0.0) < 0.5 { g_text_wrap(x, cy, w, 11.0, C_RED, "Mesh is not bound - Rig > Bind / Auto Weights so the mesh follows the bones.") bind cy = cy + 30.0 }
bind b = rg_active(o)
if b < 0.0 { g_text(x, cy, 11.0, C_TEXT_DIM, "Click a bone to pose it (R rotates).") bind cy = cy + 22.0 }
else {
g_text(x, cy, 12.0, C_TEXT_STRONG, rg_bone_name(o, b))
bind cy = cy + 20.0
g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Rotate")
bind rx = st_get(mk(o, "bp_rx"), []) bind ry = st_get(mk(o, "bp_ry"), []) bind rz = st_get(mk(o, "bp_rz"), [])
bind r = u_vec3("psp_rot", x + 46.0, cy, w - 46.0, mx_deg(list_get(rx, b)), mx_deg(list_get(ry, b)), mx_deg(list_get(rz, b)), 0.0 - 100000.0, 100000.0, 0.5)
if list_get(r, 0) >= 0.0 { if u_pressed() { me_undo_push() } st_lset(mk(o, ps_channel_key(list_get(r, 0))), b, mx_rad(list_get(r, 1))) ps_mark(o) if st_get("autokey", 0.0) > 0.5 && u_released() { ps_key() } }
bind cy = cy + 26.0
g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Move")
bind tx = st_get(mk(o, "bp_tx"), []) bind ty = st_get(mk(o, "bp_ty"), []) bind tz = st_get(mk(o, "bp_tz"), [])
bind r = u_vec3("psp_loc", x + 46.0, cy, w - 46.0, list_get(tx, b), list_get(ty, b), list_get(tz, b), 0.0 - 10000.0, 10000.0, 0.01)
if list_get(r, 0) >= 0.0 { if u_pressed() { me_undo_push() } st_lset(mk(o, ps_channel_key(3.0 + list_get(r, 0))), b, list_get(r, 1)) ps_mark(o) }
bind cy = cy + 26.0
g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Scale")
bind sx = st_get(mk(o, "bp_sx"), []) bind sy = st_get(mk(o, "bp_sy"), []) bind sz = st_get(mk(o, "bp_sz"), [])
bind r = u_vec3("psp_scl", x + 46.0, cy, w - 46.0, list_get(sx, b), list_get(sy, b), list_get(sz, b), 0.001, 1000.0, 0.01)
if list_get(r, 0) >= 0.0 { if u_pressed() { me_undo_push() } st_lset(mk(o, ps_channel_key(6.0 + list_get(r, 0))), b, list_get(r, 1)) ps_mark(o) }
bind cy = cy + 30.0
}
bind bw = floor((w - 6.0) / 2.0)
if u_button("psp_key", x, cy, bw, 22.0, "Insert key (I)", C_BLUE) { ps_key() }
if u_button("psp_unkey", x + bw + 6.0, cy, bw, 22.0, "Delete key", 0.0) { ps_unkey() }
bind cy = cy + 26.0
if u_button("psp_clear", x, cy, bw, 22.0, "Clear pose", 0.0) { ps_clear() }
if u_button("psp_rest", x + bw + 6.0, cy, bw, 22.0, "Apply as rest", 0.0) { ps_apply_rest() }
bind cy = cy + 26.0
if u_button("psp_bake", x, cy, bw, 22.0, "Bake to mesh", 0.0) { ps_bake() }
if u_button("psp_newact", x + bw + 6.0, cy, bw, 22.0, "New action", 0.0) { modal_text("New action", "action", "Action") }
bind cy = cy + 34.0
u_header(x, cy, w, "Actions", ds_col("ctl"))
bind cy = cy + 20.0
bind names = st_get(mk(o, "act_names"), [])
bind cur = ps_cur_action(o)
bind i = 0.0
while i < len(names) {
bind r = u_row("psp_act" + num_str(i), x, cy, w - 28.0, 20.0, list_get(names, i) + " (" + num_str(floor(len(list_get(st_get(mk(o, "act_data"), []), i)) / 4.0)) + " keys)", i == cur, C_GREEN, "anim")
if r > 0.5 { st_set(mk(o, "act_cur"), i) tl_mark_dirty() }
if u_icon_button("psp_actdel" + num_str(i), x + w - 24.0, cy, 22.0, 20.0, "trash", 10.0, C_TEXT_DIM, false) {
me_undo_push()
bind nn = list_new() bind nd = list_new() bind nl = list_new()
bind q = 0.0
while q < len(names) { if q != i { bind nn = list_push(nn, list_get(names, q)) bind nd = list_push(nd, list_get(st_get(mk(o, "act_data"), []), q)) bind nl = list_push(nl, list_get(st_get(mk(o, "act_len"), []), q)) } bind q = q + 1.0 }
st_set(mk(o, "act_names"), nn) st_set(mk(o, "act_data"), nd) st_set(mk(o, "act_len"), nl)
st_set(mk(o, "act_cur"), sel(len(nn) > 0.5, 0.0, 0.0 - 1.0))
}
bind cy = cy + 22.0
bind i = i + 1.0
}
if len(names) < 0.5 { g_text(x, cy, 11.0, C_TEXT_DIM, "No actions - press I to key a pose.") bind cy = cy + 20.0 }
return cy + 10.0
}
fn ps_keys(shift, ctrl, alt) {
if key_pressed("i") && !ctrl { if alt { ps_unkey() } else { ps_key() } return true }
if alt && (key_pressed("r") || key_pressed("g") || key_pressed("s")) { ps_clear() return true }
if key_pressed("left") { tl_set_frame(tl_frame() - 1.0) return true }
if key_pressed("right") { tl_set_frame(tl_frame() + 1.0) return true }
return false
}