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/view3d.ling
# ━━━ view3d.ling — the 3D viewport: camera, projection, painter's renderer, picking, box select, modal G/R/S ━━━
# Camera: orbit (yaw around Y, pitch around X) about a target point at a distance; camera space looks down -Z.
# Projection cache per object (per frame): pj<o>_x / _y (screen px), pj<o>_d (depth > 0 in front), pj<o>_cx/_cy/_cz.
fn pj(o, key) { return "pj" + num_str(o) + "_" + key }
fn v3_init() {
st_set("cam_yaw", 0.0 - 0.6)
st_set("cam_pitch", 0.45)
st_set("cam_dist", 7.0)
st_set("cam_tx", 0.0) st_set("cam_ty", 0.5) st_set("cam_tz", 0.0)
st_set("cam_persp", 1.0)
if st_has("v3_shade") < 0.5 { st_set("v3_shade", 1.0) }
if st_has("v3_grid") < 0.5 { st_set("v3_grid", 1.0) }
if st_has("v3_cull") < 0.5 { st_set("v3_cull", 1.0) }
st_set("v3_xray", 0.0)
st_set("v3_normals", 0.0)
st_set("v3_weights", 0.0)
st_set("sel_mode", 0.0)
st_set("tr_kind", "")
st_set("bs_on", 0.0)
st_set("nav_kind", "")
st_set("op_extrude", 0.5)
st_set("op_inset", 0.2)
st_set("op_bevel", 0.1)
st_set("op_bevel_segs", 1.0)
st_set("op_cuts", 1.0)
st_set("op_merge_d", 0.001)
st_set("op_smooth", 1.0)
st_set("op_spin_steps", 12.0)
st_set("op_spin_angle", 360.0)
}
# ── camera ───────────────────────────────────────────────────────────────────
fn v3_update_camera(x, y, w, h) {
st_set("v3_x", x) st_set("v3_y", y) st_set("v3_w", w) st_set("v3_h", h)
bind yaw = st_get("cam_yaw", 0.0)
bind pitch = st_get("cam_pitch", 0.0)
bind dist = st_get("cam_dist", 7.0)
bind m = m_mul(m_trans(0.0, 0.0, 0.0 - dist), m_mul(m_rotx(pitch), m_mul(m_roty(yaw), m_trans(0.0 - st_get("cam_tx", 0.0), 0.0 - st_get("cam_ty", 0.0), 0.0 - st_get("cam_tz", 0.0)))))
st_set("cam_m", m)
st_set("cam_mi", m_inv(m))
bind fov = mx_rad(st_get("cam_fov", 50.0))
bind f = (h * 0.5) / mx_tan(fov * 0.5)
st_set("cam_f", f)
st_set("cam_s", f / max(0.01, dist))
st_set("cam_cx", x + w * 0.5)
st_set("cam_cy", y + h * 0.5)
}
fn v3_cam_right() { bind m = st_get("cam_m", m_ident()) return [list_get(m, 0), list_get(m, 1), list_get(m, 2)] }
fn v3_cam_up() { bind m = st_get("cam_m", m_ident()) return [list_get(m, 4), list_get(m, 5), list_get(m, 6)] }
fn v3_cam_fwd() { bind m = st_get("cam_m", m_ident()) return [0.0 - list_get(m, 8), 0.0 - list_get(m, 9), 0.0 - list_get(m, 10)] }
fn v3_cam_pos() { return m_apply(st_get("cam_mi", m_ident()), [0.0, 0.0, 0.0]) }
# world -> [sx, sy, depth]
fn v3_w2s(x, y, z) {
bind c = m_apply(st_get("cam_m", m_ident()), [x, y, z])
bind d = 0.0 - list_get(c, 2)
bind cx = st_get("cam_cx", 0.0) bind cy = st_get("cam_cy", 0.0)
if st_get("cam_persp", 1.0) > 0.5 {
bind dd = max(0.0001, d)
bind f = st_get("cam_f", 500.0)
return [cx + list_get(c, 0) * f / dd, cy - list_get(c, 1) * f / dd, d]
}
bind s = st_get("cam_s", 100.0)
return [cx + list_get(c, 0) * s, cy - list_get(c, 1) * s, d]
}
# screen + depth -> world
fn v3_s2w(sx, sy, d) {
bind cx = st_get("cam_cx", 0.0) bind cy = st_get("cam_cy", 0.0)
bind xc = 0.0 bind yc = 0.0
if st_get("cam_persp", 1.0) > 0.5 {
bind f = st_get("cam_f", 500.0)
bind xc = (sx - cx) * d / f
bind yc = (0.0 - (sy - cy)) * d / f
} else {
bind s = st_get("cam_s", 100.0)
bind xc = (sx - cx) / s
bind yc = (0.0 - (sy - cy)) / s
}
return m_apply(st_get("cam_mi", m_ident()), [xc, yc, 0.0 - d])
}
# world units per screen pixel at depth d
fn v3_px_world(d) {
if st_get("cam_persp", 1.0) > 0.5 { return max(0.0001, d) / st_get("cam_f", 500.0) }
return 1.0 / st_get("cam_s", 100.0)
}
fn v3_depth_of(x, y, z) {
bind c = m_apply(st_get("cam_m", m_ident()), [x, y, z])
return 0.0 - list_get(c, 2)
}
fn v3_set_view(which) {
if which == "front" { st_set("cam_yaw", 0.0) st_set("cam_pitch", 0.0) }
if which == "back" { st_set("cam_yaw", 3.14159265) st_set("cam_pitch", 0.0) }
if which == "right" { st_set("cam_yaw", 1.5707963) st_set("cam_pitch", 0.0) }
if which == "left" { st_set("cam_yaw", 0.0 - 1.5707963) st_set("cam_pitch", 0.0) }
if which == "top" { st_set("cam_yaw", 0.0) st_set("cam_pitch", 1.5707963) }
if which == "bottom" { st_set("cam_yaw", 0.0) st_set("cam_pitch", 0.0 - 1.5707963) }
if which == "persp" { st_set("cam_persp", 1.0 - st_get("cam_persp", 1.0)) }
st_set("status", "View: " + which)
}
# frame all / selected
fn v3_frame(all) {
bind ids = me_ids()
bind mn = [1000000.0, 1000000.0, 1000000.0]
bind mx = [0.0 - 1000000.0, 0.0 - 1000000.0, 0.0 - 1000000.0]
bind any = false
bind i = 0.0
while i < len(ids) {
bind o = list_get(ids, i)
bind use_it = all || st_get(mk(o, "osel"), 0.0) > 0.5
if st_get("mode", 0.0) == 1.0 && !all { bind use_it = o == me_sel_o() }
if use_it && st_get(mk(o, "vis"), 1.0) > 0.5 {
bind b = []
if st_get("mode", 0.0) == 1.0 && !all && me_sel_count(o) > 0.5 {
bind lb = me_sel_bounds_local(o, true)
if len(lb) > 0.5 {
bind m = me_matrix(o)
bind p0 = m_apply(m, [list_get(lb, 0), list_get(lb, 1), list_get(lb, 2)])
bind p1 = m_apply(m, [list_get(lb, 3), list_get(lb, 4), list_get(lb, 5)])
bind b = [min(list_get(p0, 0), list_get(p1, 0)), min(list_get(p0, 1), list_get(p1, 1)), min(list_get(p0, 2), list_get(p1, 2)), max(list_get(p0, 0), list_get(p1, 0)), max(list_get(p0, 1), list_get(p1, 1)), max(list_get(p0, 2), list_get(p1, 2))]
}
} else { bind b = me_bounds(o) }
if len(b) > 0.5 {
bind any = true
bind k = 0.0
while k < 3.0 {
if list_get(b, k) < list_get(mn, k) { bind mn = list_set(mn, k, list_get(b, k)) }
if list_get(b, k + 3.0) > list_get(mx, k) { bind mx = list_set(mx, k, list_get(b, k + 3.0)) }
bind k = k + 1.0
}
}
}
bind i = i + 1.0
}
if !any { st_set("cam_tx", 0.0) st_set("cam_ty", 0.0) st_set("cam_tz", 0.0) st_set("cam_dist", 7.0) return 0.0 }
st_set("cam_tx", (list_get(mn, 0) + list_get(mx, 0)) * 0.5)
st_set("cam_ty", (list_get(mn, 1) + list_get(mx, 1)) * 0.5)
st_set("cam_tz", (list_get(mn, 2) + list_get(mx, 2)) * 0.5)
bind r = max(0.2, v3_dist([list_get(mn, 0), list_get(mn, 1), list_get(mn, 2)], [list_get(mx, 0), list_get(mx, 1), list_get(mx, 2)]) * 0.5)
st_set("cam_dist", r / mx_tan(mx_rad(st_get("cam_fov", 50.0)) * 0.5) * 1.15)
}
# ── projection of every visible object ───────────────────────────────────────
fn v3_project_all() {
bind ids = me_ids()
bind m = st_get("cam_m", m_ident())
bind persp = st_get("cam_persp", 1.0) > 0.5
bind f = st_get("cam_f", 500.0)
bind s = st_get("cam_s", 100.0)
bind cx = st_get("cam_cx", 0.0) bind cy = st_get("cam_cy", 0.0)
bind i = 0.0
while i < len(ids) {
bind o = list_get(ids, i)
if st_get(mk(o, "vis"), 1.0) > 0.5 {
bind mm = m_mul(m, me_matrix(o))
bind m0 = list_get(mm, 0) bind m1 = list_get(mm, 1) bind m2 = list_get(mm, 2) bind m3 = list_get(mm, 3)
bind m4 = list_get(mm, 4) bind m5 = list_get(mm, 5) bind m6 = list_get(mm, 6) bind m7 = list_get(mm, 7)
bind m8 = list_get(mm, 8) bind m9 = list_get(mm, 9) bind m10 = list_get(mm, 10) bind m11 = list_get(mm, 11)
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind def = ps_deformed(o)
if len(def) > 2.5 { bind vx = list_get(def, 0) bind vy = list_get(def, 1) bind vz = list_get(def, 2) }
bind n = len(vx)
# built with list_push only: on LingOS list_set copies the whole list
bind px = list_new() bind py = list_new() bind pd = list_new()
bind pcx = list_new() bind pcy = list_new() bind pcz = list_new()
bind k = 0.0
while k < n {
bind x = list_get(vx, k) bind y = list_get(vy, k) bind z = list_get(vz, k)
bind xc = m0 * x + m1 * y + m2 * z + m3
bind yc = m4 * x + m5 * y + m6 * z + m7
bind zc = m8 * x + m9 * y + m10 * z + m11
bind d = 0.0 - zc
bind pcx = list_push(pcx, xc) bind pcy = list_push(pcy, yc) bind pcz = list_push(pcz, zc)
bind pd = list_push(pd, d)
if persp {
bind dd = max(0.0001, d)
bind px = list_push(px, cx + xc * f / dd)
bind py = list_push(py, cy - yc * f / dd)
} else {
bind px = list_push(px, cx + xc * s)
bind py = list_push(py, cy - yc * s)
}
bind k = k + 1.0
}
st_set(pj(o, "x"), px) st_set(pj(o, "y"), py) st_set(pj(o, "d"), pd)
st_set(pj(o, "cx"), pcx) st_set(pj(o, "cy"), pcy) st_set(pj(o, "cz"), pcz)
}
bind i = i + 1.0
}
}
# ── grid + axes ──────────────────────────────────────────────────────────────
# draw a world-space line clipped at the near plane
fn v3_line3(ax, ay, az, bx, by, bz, c, a) {
bind m = st_get("cam_m", m_ident())
bind p = m_apply(m, [ax, ay, az])
bind q = m_apply(m, [bx, by, bz])
bind near = 0.05
bind dp = 0.0 - list_get(p, 2)
bind dq = 0.0 - list_get(q, 2)
if dp < near && dq < near { return 0.0 }
if dp < near {
bind t = (near - dp) / (dq - dp)
bind p = v3_add(p, v3_scale(v3_sub(q, p), t))
bind dp = near
} else if dq < near {
bind t = (near - dq) / (dp - dq)
bind q = v3_add(q, v3_scale(v3_sub(p, q), t))
bind dq = near
}
bind cx = st_get("cam_cx", 0.0) bind cy = st_get("cam_cy", 0.0)
if st_get("cam_persp", 1.0) > 0.5 {
bind f = st_get("cam_f", 500.0)
px_line(cx + list_get(p, 0) * f / dp, cy - list_get(p, 1) * f / dp, cx + list_get(q, 0) * f / dq, cy - list_get(q, 1) * f / dq, c, a)
} else {
bind s = st_get("cam_s", 100.0)
px_line(cx + list_get(p, 0) * s, cy - list_get(p, 1) * s, cx + list_get(q, 0) * s, cy - list_get(q, 1) * s, c, a)
}
}
fn v3_draw_grid() {
if st_get("v3_grid", 1.0) < 0.5 { return 0.0 }
bind dist = st_get("cam_dist", 7.0)
bind step = 1.0
if dist > 30.0 { bind step = 5.0 }
if dist > 120.0 { bind step = 25.0 }
bind n = 10.0
bind ext = n * step
bind i = 0.0 - n
while i <= n {
bind v = i * step
bind major = i % 5.0 == 0.0
bind col = sel(major, C_GRID_MAJ, C_GRID_MIN)
bind a = sel(major, 0.9, 0.7)
if i != 0.0 {
v3_line3(v, 0.0, 0.0 - ext, v, 0.0, ext, col, a)
v3_line3(0.0 - ext, 0.0, v, ext, 0.0, v, col, a)
}
bind i = i + 1.0
}
v3_line3(0.0 - ext, 0.0, 0.0, ext, 0.0, 0.0, C_AXIS_X, 0.55)
v3_line3(0.0, 0.0, 0.0 - ext, 0.0, 0.0, ext, C_AXIS_Z, 0.55)
}
fn v3_draw_axis_gizmo(x, y) {
bind m = st_get("cam_m", m_ident())
bind names = ["X", "Y", "Z"]
bind cols = [C_AXIS_X, C_AXIS_Y, C_AXIS_Z]
bind k = 0.0
while k < 3.0 {
bind dx = list_get(m, k)
bind dy = list_get(m, 4.0 + k)
bind ex = x + dx * 22.0
bind ey = y - dy * 22.0
px_line(x, y, ex, ey, list_get(cols, k), 0.9)
px_disc(ex, ey, 5.0, list_get(cols, k))
g_text_c(ex, ey - 6.0, 9.0, C_BG_DEEP, list_get(names, k))
bind k = k + 1.0
}
}
# ── rendering ────────────────────────────────────────────────────────────────
fn v3_shade(base, nx, ny, nz) {
# light from the camera's upper left, plus ambient (normal is in camera space)
bind l = (nx * 0.35 + ny * 0.5 + nz * 0.8)
bind inten = 0.32 + 0.68 * max(0.0, l)
# a hint of sky/ground fill so the undersides read
bind inten = inten + 0.08 * max(0.0, 0.0 - ny)
return c_tint(base, min(1.35, inten))
}
fn v3_draw_objects() {
bind ids = me_ids()
bind mode = st_get("mode", 0.0)
bind shade = st_get("v3_shade", 1.0) > 0.5
bind xray = st_get("v3_xray", 0.0) > 0.5
bind cull = st_get("v3_cull", 1.0) > 0.5 && !xray
bind show_w = mode == 3.0 || st_get("v3_weights", 0.0) > 0.5
bind edit_o = me_sel_o()
# 1. collect faces: object, face, depth, normal z sign
bind dr_o = list_new() bind dr_f = list_new() bind dr_z = list_new()
bind zmin = 1000000.0 bind zmax = 0.0 - 1000000.0
bind oi = 0.0
while oi < len(ids) {
bind o = list_get(ids, oi)
if st_get(mk(o, "vis"), 1.0) > 0.5 && shade {
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind pd = st_get(pj(o, "d"), [])
bind f = 0.0
while f < len(fc) {
bind s = list_get(fs, f) bind c = list_get(fc, f)
bind z = 0.0
bind okv = true
bind k = 0.0
while k < c {
bind d = list_get(pd, list_get(fi, s + k))
if d < 0.05 { bind okv = false }
bind z = z + d
bind k = k + 1.0
}
if okv {
bind z = z / c
bind dr_o = list_push(dr_o, o) bind dr_f = list_push(dr_f, f) bind dr_z = list_push(dr_z, z)
if z < zmin { bind zmin = z }
if z > zmax { bind zmax = z }
}
bind f = f + 1.0
}
}
bind oi = oi + 1.0
}
bind nf = len(dr_f)
# 2. far to near: sort by negated depth (merge sort, push only)
bind negz = list_new()
bind i = 0.0
while i < nf { bind negz = list_push(negz, 0.0 - list_get(dr_z, i)) bind i = i + 1.0 }
bind order = mx_argsort(negz)
st_set("dr_o", dr_o) st_set("dr_f", dr_f) st_set("dr_order", order)
# 3. fill faces
bind last_o = 0.0 - 1.0
bind fi = [] bind fs = [] bind fc = [] bind px = [] bind py = [] bind pcx = [] bind pcy = [] bind pcz = [] bind fsel = [] bind fcol = []
bind base = 0.0 bind is_edit = false bind osel = false bind smooth = false
bind k2 = 0.0
while k2 < len(order) {
bind di = list_get(order, k2)
bind o = list_get(dr_o, di)
bind f = list_get(dr_f, di)
if o != last_o {
bind last_o = o
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), [])
bind pcx = st_get(pj(o, "cx"), []) bind pcy = st_get(pj(o, "cy"), []) bind pcz = st_get(pj(o, "cz"), [])
bind fsel = st_get(mk(o, "fsel"), []) bind fcol = st_get(mk(o, "fcol"), [])
bind base = st_get(mk(o, "col"), C_BLUE)
bind is_edit = mode == 1.0 && o == edit_o
bind osel = st_get(mk(o, "osel"), 0.0) > 0.5
}
bind s = list_get(fs, f) bind c = list_get(fc, f)
# camera-space normal (Newell)
bind nx = 0.0 bind ny = 0.0 bind nz = 0.0
bind k = 0.0
while k < c {
bind a = list_get(fi, s + k) bind b2 = list_get(fi, s + (k + 1.0) % c)
bind ax = list_get(pcx, a) bind ay = list_get(pcy, a) bind az = list_get(pcz, a)
bind bx = list_get(pcx, b2) bind by = list_get(pcy, b2) bind bz = list_get(pcz, b2)
bind nx = nx + (ay - by) * (az + bz)
bind ny = ny + (az - bz) * (ax + bx)
bind nz = nz + (ax - bx) * (ay + by)
bind k = k + 1.0
}
bind nl = sqrt(nx * nx + ny * ny + nz * nz)
if nl > 0.000000001 {
bind nx = nx / nl bind ny = ny / nl bind nz = nz / nl
# facing test: the camera is at the origin of camera space
bind a0 = list_get(fi, s)
bind facing = nx * list_get(pcx, a0) + ny * list_get(pcy, a0) + nz * list_get(pcz, a0) < 0.0
if facing || !cull {
if !facing { bind nx = 0.0 - nx bind ny = 0.0 - ny bind nz = 0.0 - nz }
bind col = base
if f < len(fcol) { bind fcv = list_get(fcol, f) if fcv >= 0.0 { bind col = fcv } }
if show_w && o == edit_o { bind col = wp_face_color(o, f) }
bind col = v3_shade(col, nx, ny, nz)
bind fsel_on = false
if is_edit && f < len(fsel) { if list_get(fsel, f) > 0.5 { bind fsel_on = true } }
if fsel_on { bind col = c_mix(col, C_SEL_EL, 0.45) }
else if mode == 0.0 && osel { bind col = c_mix(col, C_SEL_EL, 0.08) }
if !facing { bind col = c_mix(col, C_RED, 0.25) }
bind pts = list_new()
bind k = 0.0
while k < c {
bind v = list_get(fi, s + k)
bind pts = list_push(pts, list_get(px, v))
bind pts = list_push(pts, list_get(py, v))
bind k = k + 1.0
}
if xray { p_poly(pts, col, 0.45) } else { p_poly(pts, col, 1.0) }
}
}
bind k2 = k2 + 1.0
}
# 4. overlays: edges, vertices, normals, bones
bind oi = 0.0
while oi < len(ids) {
bind o = list_get(ids, oi)
if st_get(mk(o, "vis"), 1.0) > 0.5 {
bind is_edit = mode == 1.0 && o == edit_o
bind osel = st_get(mk(o, "osel"), 0.0) > 0.5
bind wire_all = !shade || xray || is_edit || (mode >= 2.0 && o == edit_o)
if wire_all || osel { v3_draw_edges(o, is_edit, osel, wire_all) }
if is_edit { v3_draw_verts(o) }
if st_get("v3_normals", 0.0) > 0.5 && o == edit_o { v3_draw_normals(o) }
if mode >= 2.0 && mode <= 4.0 && o == edit_o { rg_draw(o) }
else if me_has_bones(o) && (osel || mode == 0.0) { rg_draw_faint(o) }
}
bind oi = oi + 1.0
}
}
fn v3_draw_edges(o, is_edit, osel, all) {
me_rebuild_edges(o)
bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), [])
bind esel = st_get(mk(o, "esel"), [])
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind n = len(ea)
bind mode = st_get("mode", 0.0)
bind shade = st_get("v3_shade", 1.0) > 0.5
bind col = C_WIRE_DIM
bind alpha = 0.5
if is_edit { bind col = C_WIRE bind alpha = 0.8 }
else if !shade { bind col = sel(osel, C_SEL_EL, C_WIRE) bind alpha = sel(osel, 0.9, 0.7) }
else if osel && mode == 0.0 { bind col = C_SEL_EL bind alpha = 0.35 }
else if mode >= 2.0 { bind col = C_WIRE_DIM bind alpha = 0.45 }
# very dense meshes: skip the wire overlay in object mode
if n > 40000.0 && !is_edit && shade { return 0.0 }
bind i = 0.0
while i < n {
bind a = list_get(ea, i) bind b = list_get(eb, i)
if list_get(pd, a) > 0.05 && list_get(pd, b) > 0.05 {
bind e_on = false
if is_edit && i < len(esel) { if list_get(esel, i) > 0.5 { bind e_on = true } }
if e_on {
px_line(list_get(px, a), list_get(py, a), list_get(px, b), list_get(py, b), C_SEL_EL, 1.0)
px_line(list_get(px, a) + 1.0, list_get(py, a), list_get(px, b) + 1.0, list_get(py, b), C_SEL_EL, 0.6)
} else {
px_line(list_get(px, a), list_get(py, a), list_get(px, b), list_get(py, b), col, alpha)
}
}
bind i = i + 1.0
}
}
fn v3_draw_verts(o) {
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind vsel = st_get(mk(o, "vsel"), [])
bind n = len(px)
bind act = st_get("act_v", 0.0 - 1.0)
bind i = 0.0
while i < n {
if list_get(pd, i) > 0.05 {
bind x = list_get(px, i) bind y = list_get(py, i)
bind v_on = false
if i < len(vsel) { if list_get(vsel, i) > 0.5 { bind v_on = true } }
if v_on {
px_rect(x - 2.0, y - 2.0, 5.0, 5.0, sel(i == act, C_ACT_EL, C_SEL_EL))
} else {
px_rect(x - 1.0, y - 1.0, 3.0, 3.0, C_VERT)
}
}
bind i = i + 1.0
}
}
fn v3_draw_normals(o) {
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind m = me_matrix(o)
bind f = 0.0
while f < len(fc) {
bind s = list_get(fs, f) bind c = list_get(fc, f)
bind n = v3_norm(me_face_normal(vx, vy, vz, fi, s, c))
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 p0 = m_apply(m, [cx / c, cy / c, cz / c])
bind p1 = m_apply(m, [cx / c + list_get(n, 0) * 0.25, cy / c + list_get(n, 1) * 0.25, cz / c + list_get(n, 2) * 0.25])
v3_line3(list_get(p0, 0), list_get(p0, 1), list_get(p0, 2), list_get(p1, 0), list_get(p1, 1), list_get(p1, 2), C_BLUE, 0.9)
bind f = f + 1.0
}
}
fn v3_draw_cursor() {
bind p = v3_w2s(st_get("cursor_x", 0.0), st_get("cursor_y", 0.0), st_get("cursor_z", 0.0))
if list_get(p, 2) < 0.05 { return 0.0 }
bind x = list_get(p, 0) bind y = list_get(p, 1)
px_ring(x, y, 7.0, C_RED, 0.9)
px_ring(x, y, 7.5, C_TEXT_STRONG, 0.5)
px_line(x - 12.0, y, x - 4.0, y, C_TEXT_STRONG, 0.8)
px_line(x + 4.0, y, x + 12.0, y, C_TEXT_STRONG, 0.8)
px_line(x, y - 12.0, x, y - 4.0, C_TEXT_STRONG, 0.8)
px_line(x, y + 4.0, x, y + 12.0, C_TEXT_STRONG, 0.8)
}
# ── picking ──────────────────────────────────────────────────────────────────
fn v3_pick_vertex(o, mx, my, radius) {
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind best = 0.0 - 1.0
bind bd = radius * radius
bind i = 0.0
while i < len(px) {
if list_get(pd, i) > 0.05 {
bind dx = list_get(px, i) - mx bind dy = list_get(py, i) - my
bind d2 = dx * dx + dy * dy
if d2 < bd { bind bd = d2 bind best = i }
}
bind i = i + 1.0
}
return best
}
fn v3_pick_edge(o, mx, my, radius) {
me_rebuild_edges(o)
bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), [])
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind best = 0.0 - 1.0
bind bd = radius
bind i = 0.0
while i < len(ea) {
bind a = list_get(ea, i) bind b = list_get(eb, i)
if list_get(pd, a) > 0.05 && list_get(pd, b) > 0.05 {
bind d = mx_pt_seg2(mx, my, list_get(px, a), list_get(py, a), list_get(px, b), list_get(py, b))
if d < bd { bind bd = d bind best = i }
}
bind i = i + 1.0
}
return best
}
# nearest face under the pointer (front-most). only_o = -1 for any object. returns [o, f] or [-1,-1]
fn v3_pick_face(mx, my, only_o) {
bind dr_o = st_get("dr_o", []) bind dr_f = st_get("dr_f", []) bind order = st_get("dr_order", [])
bind cull = st_get("v3_cull", 1.0) > 0.5 && st_get("v3_xray", 0.0) < 0.5
bind k = len(order) - 1.0
bind last_o = 0.0 - 1.0
bind fi = [] bind fs = [] bind fc = [] bind px = [] bind py = [] bind pcx = [] bind pcy = [] bind pcz = []
while k >= 0.0 {
bind di = list_get(order, k)
bind o = list_get(dr_o, di)
if only_o < 0.0 || o == only_o {
bind f = list_get(dr_f, di)
if o != last_o {
bind last_o = o
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), [])
bind pcx = st_get(pj(o, "cx"), []) bind pcy = st_get(pj(o, "cy"), []) bind pcz = st_get(pj(o, "cz"), [])
}
bind s = list_get(fs, f) bind c = list_get(fc, f)
# quick bbox reject
bind xmin = 1000000.0 bind xmax = 0.0 - 1000000.0 bind ymin = 1000000.0 bind ymax = 0.0 - 1000000.0
bind j = 0.0
while j < c {
bind v = list_get(fi, s + j)
bind x = list_get(px, v) bind y = list_get(py, v)
if x < xmin { bind xmin = x } if x > xmax { bind xmax = x }
if y < ymin { bind ymin = y } if y > ymax { bind ymax = y }
bind j = j + 1.0
}
if mx >= xmin && mx <= xmax && my >= ymin && my <= ymax {
bind pts = list_new()
bind j = 0.0
while j < c { bind v = list_get(fi, s + j) bind pts = list_push(pts, list_get(px, v)) bind pts = list_push(pts, list_get(py, v)) bind j = j + 1.0 }
if mx_pt_in_poly(mx, my, pts) {
bind facing = true
if cull {
bind a = list_get(fi, s) bind b = list_get(fi, s + 1.0) bind cc = list_get(fi, s + 2.0)
bind n = v3_cross(v3_sub([list_get(pcx, b), list_get(pcy, b), list_get(pcz, b)], [list_get(pcx, a), list_get(pcy, a), list_get(pcz, a)]), v3_sub([list_get(pcx, cc), list_get(pcy, cc), list_get(pcz, cc)], [list_get(pcx, a), list_get(pcy, a), list_get(pcz, a)]))
bind facing = v3_dot(n, [list_get(pcx, a), list_get(pcy, a), list_get(pcz, a)]) < 0.0
}
if facing { return [o, f] }
}
}
}
bind k = k - 1.0
}
return [0.0 - 1.0, 0.0 - 1.0]
}
fn v3_pick_object(mx, my) {
bind r = v3_pick_face(mx, my, 0.0 - 1.0)
if list_get(r, 0) >= 0.0 { return list_get(r, 0) }
# fall back to the nearest vertex of any object (wire objects / bones-only objects)
bind ids = me_ids()
bind best = 0.0 - 1.0
bind bd = 12.0
bind i = 0.0
while i < len(ids) {
bind o = list_get(ids, i)
if st_get(mk(o, "vis"), 1.0) > 0.5 {
bind v = v3_pick_vertex(o, mx, my, bd)
if v >= 0.0 {
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), [])
bind d = sqrt((list_get(px, v) - mx) * (list_get(px, v) - mx) + (list_get(py, v) - my) * (list_get(py, v) - my))
bind bd = d bind best = o
}
}
bind i = i + 1.0
}
return best
}
# ── box select ───────────────────────────────────────────────────────────────
fn v3_box_apply(x0, y0, x1, y1, extend) {
bind mode = st_get("mode", 0.0)
bind xa = min(x0, x1) bind xb = max(x0, x1) bind ya = min(y0, y1) bind yb = max(y0, y1)
if mode == 0.0 {
if !extend { me_deselect_all_objects() }
bind ids = me_ids()
bind i = 0.0
while i < len(ids) {
bind o = list_get(ids, i)
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind hit = false
bind k = 0.0
while k < len(px) && !hit {
if list_get(pd, k) > 0.05 && list_get(px, k) >= xa && list_get(px, k) <= xb && list_get(py, k) >= ya && list_get(py, k) <= yb { bind hit = true }
bind k = k + 1.0
}
if hit { st_set(mk(o, "osel"), 1.0) st_set("sel_o", o) }
bind i = i + 1.0
}
return 0.0
}
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
if mode == 2.0 || mode == 4.0 { rg_box_select(o, xa, ya, xb, yb, extend) return 0.0 }
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), []) bind pd = st_get(pj(o, "d"), [])
bind vsel = st_get(mk(o, "vsel"), [])
if !extend { bind vsel = me_zeros(len(px)) }
bind sm = st_get("sel_mode", 0.0)
if sm == 0.0 {
bind k = 0.0
while k < len(px) {
if list_get(pd, k) > 0.05 && list_get(px, k) >= xa && list_get(px, k) <= xb && list_get(py, k) >= ya && list_get(py, k) <= yb { bind vsel = list_set(vsel, k, 1.0) }
bind k = k + 1.0
}
st_set(mk(o, "vsel"), vsel)
me_sel_sync_from_verts(o)
} else if sm == 1.0 {
me_rebuild_edges(o)
bind ea = st_get(mk(o, "ea"), []) bind eb = st_get(mk(o, "eb"), [])
bind i = 0.0
while i < len(ea) {
bind a = list_get(ea, i) bind b = list_get(eb, i)
bind ina = list_get(pd, a) > 0.05 && list_get(px, a) >= xa && list_get(px, a) <= xb && list_get(py, a) >= ya && list_get(py, a) <= yb
bind inb = list_get(pd, b) > 0.05 && list_get(px, b) >= xa && list_get(px, b) <= xb && list_get(py, b) >= ya && list_get(py, b) <= yb
if ina && inb { bind vsel = list_set(vsel, a, 1.0) bind vsel = list_set(vsel, b, 1.0) }
bind i = i + 1.0
}
st_set(mk(o, "vsel"), vsel)
me_sel_sync_from_verts(o)
} else {
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind fsel = st_get(mk(o, "fsel"), [])
if !extend { 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 cy = 0.0 bind okv = true
bind k = 0.0
while k < c {
bind v = list_get(fi, s + k)
if list_get(pd, v) < 0.05 { bind okv = false }
bind cx = cx + list_get(px, v) bind cy = cy + list_get(py, v)
bind k = k + 1.0
}
bind cx = cx / c bind cy = cy / c
if okv && cx >= xa && cx <= xb && cy >= ya && cy <= yb {
bind fsel = list_set(fsel, f, 1.0)
bind k = 0.0
while k < c { bind vsel = list_set(vsel, list_get(fi, s + k), 1.0) bind k = k + 1.0 }
}
bind f = f + 1.0
}
st_set(mk(o, "vsel"), vsel)
me_sel_sync_from_verts(o)
st_set(mk(o, "fsel"), fsel)
}
}
# ── click selection ──────────────────────────────────────────────────────────
fn v3_click_select(mx, my, extend) {
bind mode = st_get("mode", 0.0)
if mode == 2.0 || mode == 4.0 { rg_click(mx, my, extend) return 0.0 }
if mode == 0.0 {
bind o = v3_pick_object(mx, my)
if o < 0.0 { if !extend { me_deselect_all_objects() } return 0.0 }
if extend { me_toggle_object(o) } else { me_select_object(o, false) }
return 0.0
}
bind o = me_sel_o()
if o < 0.0 { return 0.0 }
bind sm = st_get("sel_mode", 0.0)
if sm == 0.0 {
bind v = v3_pick_vertex(o, mx, my, 12.0)
if v < 0.0 { if !extend { me_sel_all(o, false) } return 0.0 }
if extend { me_sel_vert(o, v, !me_vert_selected(o, v)) } else { me_sel_all(o, false) me_sel_vert(o, v, true) }
st_set("act_v", v)
} else if sm == 1.0 {
bind e = v3_pick_edge(o, mx, my, 9.0)
if e < 0.0 { if !extend { me_sel_all(o, false) } return 0.0 }
if u_alt() { if !extend { me_sel_all(o, false) } me_sel_loop(o, e) return 0.0 }
bind esel = st_get(mk(o, "esel"), [])
bind on = list_get(esel, e) > 0.5
if extend { me_sel_edge(o, e, !on) } else { me_sel_all(o, false) me_sel_edge(o, e, true) }
} else {
bind r = v3_pick_face(mx, my, o)
bind f = list_get(r, 1)
if f < 0.0 { if !extend { me_sel_all(o, false) } return 0.0 }
bind on = me_face_selected(o, f)
if extend { me_sel_face(o, f, !on) } else { me_sel_all(o, false) me_sel_face(o, f, true) }
}
}
# ── modal transforms (G / R / S) ─────────────────────────────────────────────
fn v3_modal_active() { return st_get("tr_kind", "") != "" }
fn v3_begin_modal(kind) {
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
# what moves?
if mode == 0.0 { if len(me_selected_objects()) < 0.5 { st_set("status", "Nothing selected") return 0.0 } }
else if mode == 1.0 { if o < 0.0 || me_sel_count(o) < 0.5 { st_set("status", "Select some vertices first") return 0.0 } }
else if mode == 2.0 || mode == 4.0 { if o < 0.0 || rg_sel_count(o) < 0.5 { st_set("status", "Select a bone first") return 0.0 } }
else { return 0.0 }
if v3_modal_active() { v3_cancel_modal() }
me_undo_push()
st_set("tr_kind", kind)
st_set("tr_axis", 0.0 - 1.0)
st_set("tr_x0", u_mx()) st_set("tr_y0", u_my())
# pivot
bind pv = v3_pivot()
st_set("tr_pvx", list_get(pv, 0)) st_set("tr_pvy", list_get(pv, 1)) st_set("tr_pvz", list_get(pv, 2))
bind ps = v3_w2s(list_get(pv, 0), list_get(pv, 1), list_get(pv, 2))
st_set("tr_psx", list_get(ps, 0)) st_set("tr_psy", list_get(ps, 1)) st_set("tr_pd", max(0.1, list_get(ps, 2)))
# snapshot
if mode == 0.0 {
bind objs = me_selected_objects()
st_set("tr_objs", objs)
bind snap = list_new()
bind i = 0.0
while i < len(objs) {
bind id = list_get(objs, i)
bind keys = ["px", "py", "pz", "rx", "ry", "rz", "sx", "sy", "sz"]
bind k = 0.0
while k < 9.0 { bind snap = list_push(snap, st_get(mk(id, list_get(keys, k)), 0.0)) bind k = k + 1.0 }
bind i = i + 1.0
}
st_set("tr_snap", snap)
} else if mode == 1.0 {
st_set("tr_vi", me_selected_verts(o))
st_set("tr_ox", st_get(mk(o, "vx"), [])) st_set("tr_oy", st_get(mk(o, "vy"), [])) st_set("tr_oz", st_get(mk(o, "vz"), []))
} else { rg_modal_snapshot(o, mode) }
st_set("status", sel(kind == "g", "Move", sel(kind == "r", "Rotate", "Scale")) + ": X/Y/Z axis, Shift fine, Ctrl snap, click/Enter confirm, Esc cancel")
}
fn v3_pivot() {
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
if mode == 0.0 {
bind objs = me_selected_objects()
bind c = [0.0, 0.0, 0.0]
bind i = 0.0
while i < len(objs) {
bind id = list_get(objs, i)
bind c = v3_add(c, [st_get(mk(id, "px"), 0.0), st_get(mk(id, "py"), 0.0), st_get(mk(id, "pz"), 0.0)])
bind i = i + 1.0
}
return v3_scale(c, 1.0 / max(1.0, len(objs)))
}
if mode == 1.0 {
bind vi = me_selected_verts(o)
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind c = [0.0, 0.0, 0.0]
bind i = 0.0
while i < len(vi) {
bind v = list_get(vi, i)
bind c = v3_add(c, [list_get(vx, v), list_get(vy, v), list_get(vz, v)])
bind i = i + 1.0
}
return m_apply(me_matrix(o), v3_scale(c, 1.0 / max(1.0, len(vi))))
}
return rg_pivot(o, mode)
}
fn v3_cancel_modal() {
if !v3_modal_active() { return 0.0 }
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
if mode == 0.0 {
bind objs = st_get("tr_objs", [])
bind snap = st_get("tr_snap", [])
bind i = 0.0
while i < len(objs) {
bind id = list_get(objs, i)
bind keys = ["px", "py", "pz", "rx", "ry", "rz", "sx", "sy", "sz"]
bind k = 0.0
while k < 9.0 { st_set(mk(id, list_get(keys, k)), list_get(snap, i * 9.0 + k)) bind k = k + 1.0 }
bind i = i + 1.0
}
} else if mode == 1.0 && o >= 0.0 {
st_set(mk(o, "vx"), st_get("tr_ox", [])) st_set(mk(o, "vy"), st_get("tr_oy", [])) st_set(mk(o, "vz"), st_get("tr_oz", []))
} else if o >= 0.0 { rg_modal_restore(o, mode) }
st_set("tr_kind", "")
# the undo step pushed at the start is now a no-op; drop it
bind n = st_get("undo_n", 0.0)
if n > 0.5 { st_set("undo_n", n - 1.0) }
st_set("status", "Cancelled")
}
fn v3_confirm_modal() {
if !v3_modal_active() { return 0.0 }
bind o = me_sel_o()
if st_get("mode", 0.0) == 1.0 && o >= 0.0 { me_edges_dirty(o) }
st_set("tr_kind", "")
st_set("dirty", 1.0)
if st_get("mode", 0.0) == 4.0 && st_get("autokey", 0.0) > 0.5 { ps_key() }
st_set("status", "Done")
}
# compute the current transform from the mouse and apply it to the snapshot
fn v3_update_modal() {
bind kind = st_get("tr_kind", "")
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
bind x0 = st_get("tr_x0", 0.0) bind y0 = st_get("tr_y0", 0.0)
bind mx = u_mx() bind my = u_my()
if u_shift() { bind mx = x0 + (mx - x0) * 0.1 bind my = y0 + (my - y0) * 0.1 }
bind axis = st_get("tr_axis", 0.0 - 1.0)
bind pv = [st_get("tr_pvx", 0.0), st_get("tr_pvy", 0.0), st_get("tr_pvz", 0.0)]
bind psx = st_get("tr_psx", 0.0) bind psy = st_get("tr_psy", 0.0)
bind axv = [0.0, 0.0, 0.0]
if axis == 0.0 { bind axv = [1.0, 0.0, 0.0] }
if axis == 1.0 { bind axv = [0.0, 1.0, 0.0] }
if axis == 2.0 { bind axv = [0.0, 0.0, 1.0] }
# the transform as a 3x4 matrix in world space
bind M = m_ident()
bind info = ""
if kind == "g" {
bind k = v3_px_world(st_get("tr_pd", 1.0))
bind d = v3_add(v3_scale(v3_cam_right(), (mx - x0) * k), v3_scale(v3_cam_up(), (y0 - my) * k))
if axis >= 0.0 {
# project the mouse delta onto the axis's screen direction
bind a1 = v3_w2s(list_get(pv, 0) + list_get(axv, 0), list_get(pv, 1) + list_get(axv, 1), list_get(pv, 2) + list_get(axv, 2))
bind ex = list_get(a1, 0) - psx bind ey = list_get(a1, 1) - psy
bind el = ex * ex + ey * ey
bind amount = 0.0
if el > 0.0001 { bind amount = ((mx - x0) * ex + (my - y0) * ey) / el }
if u_ctrl() { bind amount = round(amount * 10.0) / 10.0 }
bind d = v3_scale(axv, amount)
bind info = "D " + fmt3(amount)
} else {
if u_ctrl() { bind d = [round(list_get(d, 0) * 10.0) / 10.0, round(list_get(d, 1) * 10.0) / 10.0, round(list_get(d, 2) * 10.0) / 10.0] }
bind info = "Dx " + fmt3(list_get(d, 0)) + " Dy " + fmt3(list_get(d, 1)) + " Dz " + fmt3(list_get(d, 2))
}
bind M = m_trans(list_get(d, 0), list_get(d, 1), list_get(d, 2))
} else if kind == "r" {
bind a = mx_atan2(my - psy, mx - psx) - mx_atan2(y0 - psy, x0 - psx)
bind u = v3_scale(v3_cam_fwd(), 0.0 - 1.0)
if axis >= 0.0 {
bind u = axv
# keep the on-screen sense when the axis points away from the viewer
if v3_dot(axv, v3_cam_fwd()) > 0.0 { bind a = 0.0 - a }
}
bind a = 0.0 - a
if u_ctrl() { bind a = round(a / 0.0872664626) * 0.0872664626 }
bind info = "Rot " + fmt1(mx_deg(a)) + " deg"
bind R = m_rot_axis(list_get(u, 0), list_get(u, 1), list_get(u, 2), a)
bind M = m_mul(m_trans(list_get(pv, 0), list_get(pv, 1), list_get(pv, 2)), m_mul(R, m_trans(0.0 - list_get(pv, 0), 0.0 - list_get(pv, 1), 0.0 - list_get(pv, 2))))
} else {
bind d0 = max(2.0, sqrt((x0 - psx) * (x0 - psx) + (y0 - psy) * (y0 - psy)))
bind d1 = sqrt((mx - psx) * (mx - psx) + (my - psy) * (my - psy))
bind f = d1 / d0
if u_ctrl() { bind f = round(f * 10.0) / 10.0 }
bind info = "Scale " + fmt3(f)
bind S = m_scale(f, f, f)
if axis >= 0.0 {
bind S = m_scale(sel(axis == 0.0, f, 1.0), sel(axis == 1.0, f, 1.0), sel(axis == 2.0, f, 1.0))
}
bind M = m_mul(m_trans(list_get(pv, 0), list_get(pv, 1), list_get(pv, 2)), m_mul(S, m_trans(0.0 - list_get(pv, 0), 0.0 - list_get(pv, 1), 0.0 - list_get(pv, 2))))
}
st_set("tr_info", info + sel(axis >= 0.0, " along " + list_get(["X", "Y", "Z"], axis), ""))
# apply
if mode == 0.0 {
bind objs = st_get("tr_objs", [])
bind snap = st_get("tr_snap", [])
bind i = 0.0
while i < len(objs) {
bind id = list_get(objs, i)
bind b = i * 9.0
bind p0 = [list_get(snap, b), list_get(snap, b + 1.0), list_get(snap, b + 2.0)]
bind p1 = m_apply(M, p0)
st_set(mk(id, "px"), list_get(p1, 0)) st_set(mk(id, "py"), list_get(p1, 1)) st_set(mk(id, "pz"), list_get(p1, 2))
if kind == "r" {
bind R0 = m_mul(m_rotz(list_get(snap, b + 5.0)), m_mul(m_roty(list_get(snap, b + 4.0)), m_rotx(list_get(snap, b + 3.0))))
bind R1 = m_mul(m_rot_axis(0.0, 0.0, 0.0, 0.0), R0)
bind Rm = [list_get(M, 0), list_get(M, 1), list_get(M, 2), 0.0, list_get(M, 4), list_get(M, 5), list_get(M, 6), 0.0, list_get(M, 8), list_get(M, 9), list_get(M, 10), 0.0]
bind e = m_to_euler(m_mul(Rm, R0))
st_set(mk(id, "rx"), list_get(e, 0)) st_set(mk(id, "ry"), list_get(e, 1)) st_set(mk(id, "rz"), list_get(e, 2))
}
if kind == "s" {
st_set(mk(id, "sx"), list_get(snap, b + 6.0) * list_get(M, 0))
st_set(mk(id, "sy"), list_get(snap, b + 7.0) * list_get(M, 5))
st_set(mk(id, "sz"), list_get(snap, b + 8.0) * list_get(M, 10))
}
bind i = i + 1.0
}
} else if mode == 1.0 && o >= 0.0 {
bind vi = st_get("tr_vi", [])
bind ox = st_get("tr_ox", []) bind oy = st_get("tr_oy", []) bind oz = st_get("tr_oz", [])
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind om = me_matrix(o)
bind T = m_mul(m_inv(om), m_mul(M, om))
bind i = 0.0
while i < len(vi) {
bind v = list_get(vi, i)
bind p = m_apply(T, [list_get(ox, v), list_get(oy, v), list_get(oz, v)])
bind vx = list_set(vx, v, list_get(p, 0)) bind vy = list_set(vy, v, list_get(p, 1)) bind vz = list_set(vz, v, 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)
} else if o >= 0.0 { rg_modal_apply(o, mode, kind, M, axis) }
}
# ── input ────────────────────────────────────────────────────────────────────
fn v3_input(x, y, w, h) {
bind inside = u_in(x, y, w, h)
bind mode = st_get("mode", 0.0)
bind blocked = st_get("blk", 0.0) > 0.5
# modal transform owns the mouse
if v3_modal_active() {
v3_update_modal()
if u_pressed() || key_pressed("enter") { v3_confirm_modal() }
else if u_rpressed() || key_pressed("escape") { v3_cancel_modal() }
return 0.0
}
if blocked { return 0.0 }
# navigation: middle drag orbits, shift+middle pans, alt+left drag orbits (laptops), wheel zooms
bind nav = st_get("nav_kind", "")
if nav == "" && inside {
if u_mpressed() || (u_pressed() && u_alt()) {
st_set("nav_kind", sel(u_shift(), "pan", "orbit"))
st_set("nav_x0", u_mx()) st_set("nav_y0", u_my())
st_set("nav_yaw", st_get("cam_yaw", 0.0)) st_set("nav_pitch", st_get("cam_pitch", 0.0))
st_set("nav_tx", st_get("cam_tx", 0.0)) st_set("nav_ty", st_get("cam_ty", 0.0)) st_set("nav_tz", st_get("cam_tz", 0.0))
bind nav = st_get("nav_kind", "")
}
}
if nav != "" {
bind dx = u_mx() - st_get("nav_x0", 0.0)
bind dy = u_my() - st_get("nav_y0", 0.0)
bind sens = st_get("orbit_sens", 1.0)
if nav == "orbit" {
st_set("cam_yaw", st_get("nav_yaw", 0.0) + dx * 0.008 * sens)
st_set("cam_pitch", clampf(st_get("nav_pitch", 0.0) + dy * 0.008 * sens, 0.0 - 1.55, 1.55))
} else {
bind k = v3_px_world(st_get("cam_dist", 7.0))
bind r = v3_cam_right() bind up = v3_cam_up()
st_set("cam_tx", st_get("nav_tx", 0.0) - list_get(r, 0) * dx * k + list_get(up, 0) * dy * k)
st_set("cam_ty", st_get("nav_ty", 0.0) - list_get(r, 1) * dx * k + list_get(up, 1) * dy * k)
st_set("cam_tz", st_get("nav_tz", 0.0) - list_get(r, 2) * dx * k + list_get(up, 2) * dy * k)
}
if !u_mdown() && !(u_down() && u_alt()) { st_set("nav_kind", "") }
return 0.0
}
if inside && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
st_set("cam_dist", clampf(st_get("cam_dist", 7.0) * pow(0.9, u_scroll()), 0.05, 2000.0))
st_set("wheel_used", 1.0)
}
# 3D cursor: shift + right click
if inside && u_rpressed() && u_shift() {
bind d = v3_depth_of(st_get("cursor_x", 0.0), st_get("cursor_y", 0.0), st_get("cursor_z", 0.0))
if d < 0.1 { bind d = st_get("cam_dist", 7.0) }
bind p = v3_s2w(u_mx(), u_my(), d)
st_set("cursor_x", list_get(p, 0)) st_set("cursor_y", list_get(p, 1)) st_set("cursor_z", list_get(p, 2))
return 0.0
}
# weight painting owns the left button in Weights mode
if mode == 3.0 { wp_input(x, y, w, h, inside) return 0.0 }
# box select (drag) / click select
bind bs = st_get("bs_on", 0.0) > 0.5
if bs {
if u_down() {
bind x0 = st_get("bs_x0", 0.0) bind y0 = st_get("bs_y0", 0.0)
g_dash_rect(min(x0, u_mx()), min(y0, u_my()), abs(u_mx() - x0), abs(u_my() - y0), C_TEXT_STRONG)
} else {
bind x0 = st_get("bs_x0", 0.0) bind y0 = st_get("bs_y0", 0.0)
if abs(u_mx() - x0) < 4.0 && abs(u_my() - y0) < 4.0 {
v3_click_select(x0, y0, u_shift())
} else {
v3_box_apply(x0, y0, u_mx(), u_my(), u_shift())
}
st_set("bs_on", 0.0)
}
return 0.0
}
if inside && u_pressed() && !u_alt() {
st_set("bs_on", 1.0)
st_set("bs_x0", u_mx()) st_set("bs_y0", u_my())
}
# right click: context menu
if inside && u_rpressed() && !u_shift() {
if mode == 0.0 { ctx_open(["Add Primitive >|sub:add||cube", "Duplicate|edit.dup|Shift+D|copy", "Delete|edit.delete|X|trash", "Frame Selected|view.frame||fit", "Edit Mode|edit.toggle|Tab|cube"], u_mx(), u_my()) }
else if mode == 1.0 { ctx_open(["Extrude|mesh.extrude|E|extrude", "Inset|mesh.inset|I|inset", "Bevel|mesh.bevel|Ctrl+B|bevel", "Loop Cut|mesh.loopcut|Ctrl+R|loopcut", "Fill|mesh.fill|F|face", "Merge|mesh.merge|M|merge", "Delete|edit.delete|X|trash", "Subdivide|mesh.subdivide||subdiv", "Object Mode|edit.toggle|Tab|cube"], u_mx(), u_my()) }
else if mode == 2.0 { ctx_open(["Add Bone|rig.add|Shift+A|bone", "Extrude Bone|rig.extrude|E|bone", "Parent|rig.parent|Ctrl+P|link", "Delete Bone|rig.delete|X|trash", "Rename...|rig.rename|F2|edit"], u_mx(), u_my()) }
else if mode == 4.0 { ctx_open(["Insert Keyframe|pose.key|I|key", "Delete Keyframe|pose.unkey|Alt+I|key_off", "Clear Pose|pose.clear|Alt+R|refresh"], u_mx(), u_my()) }
}
}
# ── the viewport ─────────────────────────────────────────────────────────────
fn v3_view(x, y, w, h) {
px_rect(x, y, w, h, C_BG_VIEW)
u_clip(x, y, w, h)
v3_update_camera(x, y, w, h)
v3_project_all()
v3_draw_grid()
v3_draw_objects()
v3_draw_cursor()
v3_input(x, y, w, h)
if st_get("mode", 0.0) == 3.0 { wp_draw_brush() }
u_unclip()
v3_draw_axis_gizmo(x + 34.0, y + h - 34.0)
# header text
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
bind title = list_get(["Object", "Edit", "Rig", "Weight Paint", "Pose", "", "", ""], mode)
if o >= 0.0 { bind title = title + " | " + me_name(o) }
if mode == 1.0 && o >= 0.0 { bind title = title + " | " + list_get(["Vertex", "Edge", "Face"], st_get("sel_mode", 0.0)) + " select" }
if v3_modal_active() { bind title = st_get("tr_info", "") }
g_text(x + 10.0, y + 8.0, 12.0, C_TEXT, title)
if st_get("v3_xray", 0.0) > 0.5 { g_text(x + 10.0, y + 26.0, 11.0, C_TEXT_DIM, "X-ray") }
if st_get("cam_persp", 1.0) < 0.5 { g_text_r(x + w - 10.0, y + 8.0, 11.0, C_TEXT_DIM, "Orthographic") }
}
fn v3_toolbar(x, by) {
bind mode = st_get("mode", 0.0)
bind xx = x
if mode == 0.0 {
if u_tool("t3_add", xx, by, 70.0, 24.0, "plus", "Add", false, C_GREEN, "Add a primitive (Edit > Add)") { ctx_open(menu_sub_def("add"), xx, by + 26.0) }
bind xx = xx + 74.0
if u_tool("t3_gen", xx, by, 84.0, 24.0, "gen", "Generate", false, C_GREEN, "Procedural game assets") { ctx_open(menu_sub_def("gen"), xx, by + 26.0) }
bind xx = xx + 88.0
if u_tool("t3_g", xx, by, 60.0, 24.0, "move", "Move", v3_modal_active() && st_get("tr_kind", "") == "g", C_BLUE, "Move selection (G)") { v3_begin_modal("g") }
bind xx = xx + 62.0
if u_tool("t3_r", xx, by, 64.0, 24.0, "rotate", "Rotate", v3_modal_active() && st_get("tr_kind", "") == "r", C_BLUE, "Rotate selection (R)") { v3_begin_modal("r") }
bind xx = xx + 66.0
if u_tool("t3_s", xx, by, 60.0, 24.0, "scale", "Scale", v3_modal_active() && st_get("tr_kind", "") == "s", C_BLUE, "Scale selection (S)") { v3_begin_modal("s") }
bind xx = xx + 70.0
if u_tool("t3_dup", xx, by, 28.0, 24.0, "copy", "", false, 0.0, "Duplicate (Shift+D)") { do_action("edit.dup") }
bind xx = xx + 30.0
if u_tool("t3_del", xx, by, 28.0, 24.0, "trash", "", false, C_RED, "Delete (X)") { do_action("edit.delete") }
bind xx = xx + 36.0
} else if mode == 1.0 {
bind sm = st_get("sel_mode", 0.0)
if u_tool("t3_v", xx, by, 28.0, 24.0, "vertex", "", sm == 0.0, C_ORANGE, "Vertex select (1)") { st_set("sel_mode", 0.0) }
if u_tool("t3_e", xx + 30.0, by, 28.0, 24.0, "edge", "", sm == 1.0, C_ORANGE, "Edge select (2)") { st_set("sel_mode", 1.0) }
if u_tool("t3_f", xx + 60.0, by, 28.0, 24.0, "face", "", sm == 2.0, C_ORANGE, "Face select (3)") { st_set("sel_mode", 2.0) }
bind xx = xx + 96.0
if u_tool("t3_g", xx, by, 28.0, 24.0, "move", "", v3_modal_active() && st_get("tr_kind", "") == "g", C_BLUE, "Move (G)") { v3_begin_modal("g") }
if u_tool("t3_r", xx + 30.0, by, 28.0, 24.0, "rotate", "", v3_modal_active() && st_get("tr_kind", "") == "r", C_BLUE, "Rotate (R)") { v3_begin_modal("r") }
if u_tool("t3_s", xx + 60.0, by, 28.0, 24.0, "scale", "", v3_modal_active() && st_get("tr_kind", "") == "s", C_BLUE, "Scale (S)") { v3_begin_modal("s") }
bind xx = xx + 96.0
if u_tool("t3_ex", xx, by, 70.0, 24.0, "extrude", "Extrude", false, C_GREEN, "Extrude (E)") { do_action("mesh.extrude") }
if u_tool("t3_in", xx + 72.0, by, 60.0, 24.0, "inset", "Inset", false, C_GREEN, "Inset faces (I)") { do_action("mesh.inset") }
if u_tool("t3_bv", xx + 134.0, by, 60.0, 24.0, "bevel", "Bevel", false, C_GREEN, "Bevel edges (Ctrl+B)") { do_action("mesh.bevel") }
if u_tool("t3_lc", xx + 196.0, by, 74.0, 24.0, "loopcut", "Loop cut", false, C_GREEN, "Loop cut (Ctrl+R): cuts across the selected edge's ring") { do_action("mesh.loopcut") }
if u_tool("t3_sd", xx + 272.0, by, 74.0, 24.0, "subdiv", "Subdiv", false, C_GREEN, "Subdivide selected faces") { do_action("mesh.subdivide") }
if u_tool("t3_mg", xx + 348.0, by, 62.0, 24.0, "merge", "Merge", false, C_GREEN, "Merge selected vertices at centre (M)") { do_action("mesh.merge") }
if u_tool("t3_fl", xx + 412.0, by, 48.0, 24.0, "face", "Fill", false, C_GREEN, "Fill a face from the selected vertices (F)") { do_action("mesh.fill") }
if u_tool("t3_dl", xx + 462.0, by, 28.0, 24.0, "trash", "", false, C_RED, "Delete selection (X)") { do_action("edit.delete") }
bind xx = xx + 498.0
} else if mode == 2.0 {
if u_tool("t3_ab", xx, by, 84.0, 24.0, "bone", "Add bone", false, C_VIOLET, "Add a bone at the 3D cursor (Shift+A)") { do_action("rig.add") }
if u_tool("t3_eb", xx + 86.0, by, 70.0, 24.0, "extrude", "Extrude", false, C_VIOLET, "Extrude a child bone from the selected tip (E)") { do_action("rig.extrude") }
if u_tool("t3_pb", xx + 158.0, by, 64.0, 24.0, "link", "Parent", false, C_VIOLET, "Parent selected bones to the active bone (Ctrl+P)") { do_action("rig.parent") }
if u_tool("t3_g", xx + 224.0, by, 28.0, 24.0, "move", "", v3_modal_active() && st_get("tr_kind", "") == "g", C_BLUE, "Move bone points (G)") { v3_begin_modal("g") }
if u_tool("t3_r", xx + 254.0, by, 28.0, 24.0, "rotate", "", v3_modal_active() && st_get("tr_kind", "") == "r", C_BLUE, "Rotate (R)") { v3_begin_modal("r") }
if u_tool("t3_s", xx + 284.0, by, 28.0, 24.0, "scale", "", v3_modal_active() && st_get("tr_kind", "") == "s", C_BLUE, "Scale (S)") { v3_begin_modal("s") }
if u_tool("t3_bind", xx + 320.0, by, 60.0, 24.0, "link", "Bind", st_get(mk(max(0.0, me_sel_o()), "bound"), 0.0) > 0.5, C_GREEN, "Bind the mesh to its bones (needed before weights)") { do_action("rig.bind") }
if u_tool("t3_aw", xx + 382.0, by, 100.0, 24.0, "weight", "Auto weights", false, C_GREEN, "Compute skin weights from bone distance (adjustable)") { do_action("rig.autoweights") }
if u_tool("t3_dl", xx + 484.0, by, 28.0, 24.0, "trash", "", false, C_RED, "Delete bone (X)") { do_action("rig.delete") }
bind xx = xx + 520.0
} else if mode == 3.0 {
bind xx = wp_toolbar(xx, by)
} else if mode == 4.0 {
bind xx = ps_toolbar(xx, by)
}
# view toggles common to all 3D modes
u_vsep(xx, by + 2.0, 20.0)
bind xx = xx + 8.0
if u_tool("t3_wire", xx, by, 28.0, 24.0, "wire", "", st_get("v3_shade", 1.0) < 0.5, 0.0, "Wireframe (Z)") { st_set("v3_shade", 1.0 - st_get("v3_shade", 1.0)) }
if u_tool("t3_xray", xx + 30.0, by, 28.0, 24.0, "xray", "", st_get("v3_xray", 0.0) > 0.5, 0.0, "X-ray (Alt+Z)") { st_set("v3_xray", 1.0 - st_get("v3_xray", 0.0)) }
if u_tool("t3_grid", xx + 60.0, by, 28.0, 24.0, "grid", "", st_get("v3_grid", 1.0) > 0.5, 0.0, "Grid") { st_set("v3_grid", 1.0 - st_get("v3_grid", 1.0)) }
if u_tool("t3_persp", xx + 90.0, by, 28.0, 24.0, "camera", "", st_get("cam_persp", 1.0) > 0.5, 0.0, "Perspective / orthographic (Numpad 5)") { v3_set_view("persp") }
if u_tool("t3_fit", xx + 120.0, by, 28.0, 24.0, "fit", "", false, 0.0, "Frame all (Home)") { v3_frame(true) }
return xx + 152.0
}
fn v3_status_text() {
bind o = me_sel_o()
bind mode = st_get("mode", 0.0)
bind t = num_str(me_count()) + " objects"
if o >= 0.0 {
bind t = t + " | " + me_name(o) + ": " + num_str(me_nverts(o)) + " verts, " + num_str(me_nfaces(o)) + " faces (" + num_str(me_ntris(o)) + " tris)"
if mode == 1.0 { bind t = t + " | selected: " + num_str(me_sel_count(o)) + " verts, " + num_str(len(me_selected_faces(o))) + " faces" }
if me_has_bones(o) { bind t = t + " | " + num_str(len(st_get(mk(o, "bn_name"), []))) + " bones" }
}
bind t = t + " | MMB orbit, Shift+MMB pan, wheel zoom, Shift+RMB cursor"
return t
}
# keyboard shortcuts for the 3D modes; returns true when handled
fn v3_keys(shift, ctrl, alt) {
bind mode = st_get("mode", 0.0)
bind o = me_sel_o()
if v3_modal_active() {
if key_pressed("x") { st_set("tr_axis", sel(st_get("tr_axis", 0.0 - 1.0) == 0.0, 0.0 - 1.0, 0.0)) }
if key_pressed("y") { st_set("tr_axis", sel(st_get("tr_axis", 0.0 - 1.0) == 1.0, 0.0 - 1.0, 1.0)) }
if key_pressed("z") { st_set("tr_axis", sel(st_get("tr_axis", 0.0 - 1.0) == 2.0, 0.0 - 1.0, 2.0)) }
return true
}
if key_pressed("g") && !ctrl { v3_begin_modal("g") return true }
if key_pressed("r") && !ctrl && !alt { v3_begin_modal("r") return true }
if key_pressed("s") && !ctrl && !alt { v3_begin_modal("s") return true }
if key_pressed("numpad1") || key_pressed("kp1") { v3_set_view(sel(ctrl, "back", "front")) return true }
if key_pressed("numpad3") || key_pressed("kp3") { v3_set_view(sel(ctrl, "left", "right")) return true }
if key_pressed("numpad7") || key_pressed("kp7") { v3_set_view(sel(ctrl, "bottom", "top")) return true }
if key_pressed("numpad5") || key_pressed("kp5") { v3_set_view("persp") return true }
if key_pressed("numpad.") || key_pressed("kp.") { v3_frame(false) return true }
if key_pressed("home") { v3_frame(true) return true }
if key_pressed("z") && !ctrl { if alt { st_set("v3_xray", 1.0 - st_get("v3_xray", 0.0)) } else { st_set("v3_shade", 1.0 - st_get("v3_shade", 1.0)) } return true }
if key_pressed("tab") && mode < 1.5 { set_mode(sel(mode < 0.5, 1.0, 0.0)) return true }
if key_pressed("a") && !ctrl { do_action(sel(alt, "sel.none", "sel.all")) return true }
if key_pressed("x") && !ctrl || key_pressed("delete") { do_action("edit.delete") return true }
if key_pressed("d") && shift { do_action("edit.dup") return true }
if key_pressed("h") && !ctrl { do_action(sel(alt, "view.unhide", "view.hide")) return true }
if mode == 1.0 {
if key_pressed("1") { st_set("sel_mode", 0.0) return true }
if key_pressed("2") && !ctrl { st_set("sel_mode", 1.0) return true }
if key_pressed("3") { st_set("sel_mode", 2.0) return true }
if key_pressed("e") && !ctrl { do_action("mesh.extrude") return true }
if key_pressed("i") && !ctrl { do_action("mesh.inset") return true }
if key_pressed("b") && ctrl { do_action("mesh.bevel") return true }
if key_pressed("r") && ctrl { do_action("mesh.loopcut") return true }
if key_pressed("f") && !ctrl { do_action("mesh.fill") return true }
if key_pressed("m") && !ctrl { do_action("mesh.merge") return true }
if key_pressed("j") && !ctrl && !alt { do_action("mesh.connect") return true }
if key_pressed("j") && alt { do_action("mesh.quads") return true }
if key_pressed("y") && !ctrl { do_action("mesh.split") return true }
if key_pressed("p") && !ctrl { do_action("mesh.separate") return true }
if key_pressed("l") && !ctrl { do_action("sel.linked") return true }
if key_pressed("2") && ctrl { do_action("mesh.subsurf") return true }
if key_pressed("t") && ctrl { do_action("mesh.tri") return true }
if key_pressed("n") && shift { do_action("mesh.recalc") return true }
if key_pressed("i") && ctrl { do_action("sel.invert") return true }
if (key_pressed("numpad+") || key_pressed("kp+") || key_pressed("=")) && ctrl { do_action("sel.grow") return true }
if (key_pressed("numpad-") || key_pressed("kp-") || key_pressed("-")) && ctrl { do_action("sel.shrink") return true }
}
if mode == 0.0 {
if key_pressed("j") && ctrl { do_action("mesh.join") return true }
}
if mode == 2.0 { return rg_keys(shift, ctrl, alt) }
if mode == 3.0 { return wp_keys(shift, ctrl, alt) }
if mode == 4.0 { return ps_keys(shift, ctrl, alt) }
return false
}