灵符lingfu

lingmodeler

↓ 0 downloads

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

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

Code / Files (67)

ling/vec2d.ling
# ━━━ vec2d.ling — 2D vector illustration: layers, bezier paths, shapes, pen + node editing, groups ━━━
# Document: vc_w vc_h vc_name vc_bg (-1 = transparent). Layers: vc_layers (names), vc_lvis, vc_llock, vc_layer (active).
# Items (draw order, bottom first): vc_ids. Item k keys "v<k>_*":
#   kind name layer closed fill stroke sw op  — fill/stroke packed colour or -1 = none
#   pts: 6 numbers per node: x y  inx iny  outx outy  (bezier handles, absolute doc coords; handle == node = corner)
#   tx ty rot sx sy: transform about the pivot (centre of the untransformed bounds); grp: group id (0 none); sel
#   keys: flat [channel, frame, value] (0 tx 1 ty 2 rot 3 sx 4 sy 5 op); skf / skp: shape-key frames / pts lists

fn vk(k, key) { return "v" + num_str(k) + "_" + key }
fn vc_item_keys() { return ["kind", "name", "layer", "closed", "fill", "stroke", "sw", "op", "pts", "tx", "ty", "rot", "sx", "sy", "grp", "keys", "skf", "skp", "sel"] }

fn vc_init() {
    vc_new_doc(512.0, 512.0, 24.0)
    st_set("vc_tool", "select")
    st_set("vc_fill", c_rgb(59.0, 170.0, 235.0))
    st_set("vc_stroke", c_rgb(20.0, 24.0, 32.0))
    st_set("vc_sw", 3.0)
    st_set("vc_sides", 6.0)
    st_set("vc_undo_n", 0.0)
    st_set("vc_redo_n", 0.0)
    st_set("vc_onion", 0.0)
}
fn vc_new_doc(w, h, fps) {
    bind ids = st_get("vc_ids", [])
    bind i = 0.0
    while i < len(ids) { vc_free(list_get(ids, i)) bind i = i + 1.0 }
    st_set("vc_ids", [])
    st_set("vc_next", 1.0)
    st_set("vc_gnext", 1.0)
    st_set("vc_w", w) st_set("vc_h", h)
    st_set("vc_name", "drawing")
    st_set("vc_bg", 16777215.0)
    st_set("vc_layers", ["Layer 1"]) st_set("vc_lvis", [1.0]) st_set("vc_llock", [0.0])
    st_set("vc_layer", 0.0)
    st_set("vc_zoom", 0.0)
    st_set("an_fps", fps)
    st_set("tl_fps", fps)
    st_set("vc_pen", 0.0 - 1.0)
    st_set("vc_path", "")
}
fn vc_free(k) {
    bind keys = vc_item_keys()
    bind i = 0.0
    while i < len(keys) { st_set(vk(k, list_get(keys, i)), 0.0) bind i = i + 1.0 }
}
fn vc_ids() { return st_get("vc_ids", []) }
fn vc_add_item(kind, pts, closed) {
    bind k = st_get("vc_next", 1.0)
    st_set("vc_next", k + 1.0)
    st_lpush("vc_ids", k)
    st_set(vk(k, "kind"), kind)
    st_set(vk(k, "name"), kind + " " + num_str(k))
    st_set(vk(k, "layer"), st_get("vc_layer", 0.0))
    st_set(vk(k, "closed"), sel(closed, 1.0, 0.0))
    st_set(vk(k, "fill"), sel(closed, st_get("vc_fill", 0.0), 0.0 - 1.0))
    st_set(vk(k, "stroke"), st_get("vc_stroke", 0.0))
    st_set(vk(k, "sw"), st_get("vc_sw", 2.0))
    st_set(vk(k, "op"), 1.0)
    st_set(vk(k, "pts"), pts)
    st_set(vk(k, "tx"), 0.0) st_set(vk(k, "ty"), 0.0) st_set(vk(k, "rot"), 0.0) st_set(vk(k, "sx"), 1.0) st_set(vk(k, "sy"), 1.0)
    st_set(vk(k, "grp"), 0.0)
    st_set(vk(k, "keys"), []) st_set(vk(k, "skf"), []) st_set(vk(k, "skp"), [])
    st_set(vk(k, "sel"), 0.0)
    st_set("vc_dirty", 1.0)
    return k
}
fn vc_node(x, y) { return [x, y, x, y, x, y] }
fn vc_nodes_from_poly(flat) {
    bind out = list_new()
    bind i = 0.0
    while i + 1.0 < len(flat) {
        bind x = list_get(flat, i) bind y = list_get(flat, i + 1.0)
        bind out = list_push(out, x) bind out = list_push(out, y) bind out = list_push(out, x) bind out = list_push(out, y) bind out = list_push(out, x) bind out = list_push(out, y)
        bind i = i + 2.0
    }
    return out
}
# shapes as bezier nodes
fn vc_rect_pts(x0, y0, x1, y1) { return vc_nodes_from_poly([x0, y0, x1, y0, x1, y1, x0, y1]) }
fn vc_ellipse_pts(cx, cy, rx, ry) {
    bind k = 0.5522847
    return [cx + rx, cy, cx + rx, cy - ry * k, cx + rx, cy + ry * k, cx, cy + ry, cx + rx * k, cy + ry, cx - rx * k, cy + ry, cx - rx, cy, cx - rx, cy + ry * k, cx - rx, cy - ry * k, cx, cy - ry, cx - rx * k, cy - ry, cx + rx * k, cy - ry]
}
fn vc_poly_pts(cx, cy, r, n, inner) {
    bind flat = list_new()
    bind count = sel(inner > 0.0, n * 2.0, n)
    bind i = 0.0
    while i < count {
        bind a = i / count * 6.28318530718 - 1.5707963268
        bind rr = r
        if inner > 0.0 { if i % 2.0 > 0.5 { bind rr = r * inner } }
        bind flat = list_push(flat, cx + cos(a) * rr) bind flat = list_push(flat, cy + sin(a) * rr)
        bind i = i + 1.0
    }
    return vc_nodes_from_poly(flat)
}

# ── selection ────────────────────────────────────────────────────────────────
fn vc_is_sel(k) { return st_get(vk(k, "sel"), 0.0) > 0.5 }
fn vc_selected() {
    bind ids = vc_ids()
    bind out = list_new()
    bind i = 0.0
    while i < len(ids) { bind k = list_get(ids, i) if vc_is_sel(k) { bind out = list_push(out, k) } bind i = i + 1.0 }
    return out
}
fn vc_sel_count() { return len(vc_selected()) }
fn vc_select_all(on) {
    bind ids = vc_ids()
    bind i = 0.0
    while i < len(ids) {
        bind k = list_get(ids, i)
        bind ok = vc_layer_visible(st_get(vk(k, "layer"), 0.0)) && !vc_layer_locked(st_get(vk(k, "layer"), 0.0))
        st_set(vk(k, "sel"), sel(on && ok, 1.0, 0.0))
        bind i = i + 1.0
    }
}
fn vc_select(k, extend) {
    if !extend { vc_select_all(false) }
    if k < 0.0 { return 0.0 }
    bind on = sel(extend && vc_is_sel(k), 0.0, 1.0)
    bind g = st_get(vk(k, "grp"), 0.0)
    if g > 0.5 {
        bind ids = vc_ids()
        bind i = 0.0
        while i < len(ids) { bind q = list_get(ids, i) if st_get(vk(q, "grp"), 0.0) == g { st_set(vk(q, "sel"), on) } bind i = i + 1.0 }
    } else { st_set(vk(k, "sel"), on) }
    st_set("vc_active", k)
}
fn vc_layer_visible(l) { bind v = st_get("vc_lvis", []) if l >= len(v) { return true } return list_get(v, l) > 0.5 }
fn vc_layer_locked(l) { bind v = st_get("vc_llock", []) if l >= len(v) { return false } return list_get(v, l) > 0.5 }

# ── transforms ───────────────────────────────────────────────────────────────
fn vc_bounds_pts(pts) {
    bind x0 = 1000000.0 bind y0 = 1000000.0 bind x1 = 0.0 - 1000000.0 bind y1 = 0.0 - 1000000.0
    bind i = 0.0
    while i + 5.0 < len(pts) {
        bind x = list_get(pts, i) bind y = list_get(pts, i + 1.0)
        if x < x0 { bind x0 = x } if x > x1 { bind x1 = x }
        if y < y0 { bind y0 = y } if y > y1 { bind y1 = y }
        bind i = i + 6.0
    }
    if x1 < x0 { return [0.0, 0.0, 0.0, 0.0] }
    return [x0, y0, x1, y1]
}
fn vc_pivot(k) {
    bind b = vc_bounds_pts(st_get(vk(k, "pts"), []))
    return [(list_get(b, 0) + list_get(b, 2)) * 0.5, (list_get(b, 1) + list_get(b, 3)) * 0.5]
}
# 2D affine [a b c d e f]: x' = a x + c y + e, y' = b x + d y + f
fn vc_xf(k) {
    bind pv = vc_pivot(k)
    bind px = list_get(pv, 0) bind py = list_get(pv, 1)
    bind r = st_get(vk(k, "rot"), 0.0)
    bind sx = st_get(vk(k, "sx"), 1.0) bind sy = st_get(vk(k, "sy"), 1.0)
    bind cr = cos(r) bind sr = sin(r)
    bind a = cr * sx bind b = sr * sx bind c = 0.0 - sr * sy bind d = cr * sy
    bind e = px - (a * px + c * py) + st_get(vk(k, "tx"), 0.0)
    bind f = py - (b * px + d * py) + st_get(vk(k, "ty"), 0.0)
    return [a, b, c, d, e, f]
}
fn xf_pt(m, x, y) { return [list_get(m, 0) * x + list_get(m, 2) * y + list_get(m, 4), list_get(m, 1) * x + list_get(m, 3) * y + list_get(m, 5)] }
fn xf_inv_vec(m, dx, dy) {
    bind a = list_get(m, 0) bind b = list_get(m, 1) bind c = list_get(m, 2) bind d = list_get(m, 3)
    bind det = a * d - b * c
    if abs(det) < 0.0000001 { return [dx, dy] }
    return [(d * dx - c * dy) / det, (0.0 - b * dx + a * dy) / det]
}
# flatten the item's beziers to a flat [x,y,...] in document space (transform applied); tol in doc units
fn vc_flatten(k, tol) {
    bind pts = st_get(vk(k, "pts"), [])
    bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
    bind m = vc_xf(k)
    return vc_flatten_pts(pts, closed, m, tol)
}
fn vc_flatten_pts(pts, closed, m, tol) {
    bind n = floor(len(pts) / 6.0)
    bind out = list_new()
    if n < 1.0 { return out }
    bind segs = sel(closed, n, n - 1.0)
    bind p0 = xf_pt(m, list_get(pts, 0), list_get(pts, 1))
    bind out = list_push(out, list_get(p0, 0)) bind out = list_push(out, list_get(p0, 1))
    bind i = 0.0
    while i < segs {
        bind j = (i + 1.0) % n
        bind ax = list_get(pts, i * 6.0) bind ay = list_get(pts, i * 6.0 + 1.0)
        bind bx = list_get(pts, i * 6.0 + 4.0) bind by = list_get(pts, i * 6.0 + 5.0)
        bind cx = list_get(pts, j * 6.0 + 2.0) bind cy = list_get(pts, j * 6.0 + 3.0)
        bind dx = list_get(pts, j * 6.0) bind dy = list_get(pts, j * 6.0 + 1.0)
        bind straight = abs(bx - ax) + abs(by - ay) + abs(cx - dx) + abs(cy - dy) < 0.0001
        if straight {
            bind q = xf_pt(m, dx, dy)
            bind out = list_push(out, list_get(q, 0)) bind out = list_push(out, list_get(q, 1))
        } else {
            bind l = sqrt((bx - ax) * (bx - ax) + (by - ay) * (by - ay)) + sqrt((cx - bx) * (cx - bx) + (cy - by) * (cy - by)) + sqrt((dx - cx) * (dx - cx) + (dy - cy) * (dy - cy))
            bind steps = clampf(ceil(l / max(0.01, tol)), 2.0, 32.0)
            bind s = 1.0
            while s <= steps {
                bind t = s / steps
                bind u = 1.0 - t
                bind x = u * u * u * ax + 3.0 * u * u * t * bx + 3.0 * u * t * t * cx + t * t * t * dx
                bind y = u * u * u * ay + 3.0 * u * u * t * by + 3.0 * u * t * t * cy + t * t * t * dy
                bind q = xf_pt(m, x, y)
                bind out = list_push(out, list_get(q, 0)) bind out = list_push(out, list_get(q, 1))
                bind s = s + 1.0
            }
        }
        bind i = i + 1.0
    }
    if closed && len(out) > 3.5 {
        # drop the duplicated closing point
        bind lx = list_get(out, len(out) - 2.0) bind ly = list_get(out, len(out) - 1.0)
        if abs(lx - list_get(out, 0)) < 0.0001 && abs(ly - list_get(out, 1)) < 0.0001 {
            bind t2 = list_new()
            bind q = 0.0
            while q < len(out) - 2.0 { bind t2 = list_push(t2, list_get(out, q)) bind q = q + 1.0 }
            bind out = t2
        }
    }
    return out
}
fn vc_item_bounds(k) {
    bind f = vc_flatten(k, 4.0)
    bind x0 = 1000000.0 bind y0 = 1000000.0 bind x1 = 0.0 - 1000000.0 bind y1 = 0.0 - 1000000.0
    bind i = 0.0
    while i + 1.0 < len(f) {
        bind x = list_get(f, i) bind y = list_get(f, i + 1.0)
        if x < x0 { bind x0 = x } if x > x1 { bind x1 = x }
        if y < y0 { bind y0 = y } if y > y1 { bind y1 = y }
        bind i = i + 2.0
    }
    return [x0, y0, x1, y1]
}
fn vc_sel_bounds() {
    bind s = vc_selected()
    bind x0 = 1000000.0 bind y0 = 1000000.0 bind x1 = 0.0 - 1000000.0 bind y1 = 0.0 - 1000000.0
    bind i = 0.0
    while i < len(s) {
        bind b = vc_item_bounds(list_get(s, i))
        bind x0 = min(x0, list_get(b, 0)) bind y0 = min(y0, list_get(b, 1)) bind x1 = max(x1, list_get(b, 2)) bind y1 = max(y1, list_get(b, 3))
        bind i = i + 1.0
    }
    return [x0, y0, x1, y1]
}

# ── view mapping ─────────────────────────────────────────────────────────────
fn vc_fit(x, y, w, h) {
    bind z = min((w - 60.0) / st_get("vc_w", 512.0), (h - 60.0) / st_get("vc_h", 512.0))
    st_set("vc_zoom", max(0.05, z))
    st_set("vc_panx", x + (w - st_get("vc_w", 512.0) * z) * 0.5)
    st_set("vc_pany", y + (h - st_get("vc_h", 512.0) * z) * 0.5)
}
fn vc_sx(x) { return st_get("vc_panx", 0.0) + x * st_get("vc_zoom", 1.0) }
fn vc_sy(y) { return st_get("vc_pany", 0.0) + y * st_get("vc_zoom", 1.0) }
fn vc_dx(sx) { return (sx - st_get("vc_panx", 0.0)) / st_get("vc_zoom", 1.0) }
fn vc_dy(sy) { return (sy - st_get("vc_pany", 0.0)) / st_get("vc_zoom", 1.0) }
fn vc_to_screen(flat) {
    bind out = list_new()
    bind z = st_get("vc_zoom", 1.0) bind px = st_get("vc_panx", 0.0) bind py = st_get("vc_pany", 0.0)
    bind i = 0.0
    while i + 1.0 < len(flat) {
        bind out = list_push(out, px + list_get(flat, i) * z)
        bind out = list_push(out, py + list_get(flat, i + 1.0) * z)
        bind i = i + 2.0
    }
    return out
}

# ── drawing ──────────────────────────────────────────────────────────────────
fn vc_draw_stroke(spts, closed, col, w, a) {
    bind n = floor(len(spts) / 2.0)
    bind segs = sel(closed, n, n - 1.0)
    bind i = 0.0
    while i < segs {
        bind j = (i + 1.0) % n
        bind x0 = list_get(spts, i * 2.0) bind y0 = list_get(spts, i * 2.0 + 1.0)
        bind x1 = list_get(spts, j * 2.0) bind y1 = list_get(spts, j * 2.0 + 1.0)
        if w <= 1.5 { px_line(x0, y0, x1, y1, col, a) }
        else {
            bind dx = x1 - x0 bind dy = y1 - y0
            bind l = sqrt(dx * dx + dy * dy)
            if l > 0.001 {
                bind nx = 0.0 - dy / l * w * 0.5 bind ny = dx / l * w * 0.5
                p_poly([x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny], col, a)
            }
            px_disc(x1, y1, w * 0.5, col, a)
            if i == 0.0 { px_disc(x0, y0, w * 0.5, col, a) }
        }
        bind i = i + 1.0
    }
}
fn vc_draw_item(k, ghost) {
    bind z = st_get("vc_zoom", 1.0)
    bind flat = vc_flatten(k, 1.5 / z)
    if len(flat) < 3.5 { return 0.0 }
    bind s = vc_to_screen(flat)
    bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
    bind op = st_get(vk(k, "op"), 1.0)
    bind fill = st_get(vk(k, "fill"), 0.0 - 1.0)
    bind stroke = st_get(vk(k, "stroke"), 0.0 - 1.0)
    if ghost >= 0.0 {
        vc_draw_stroke(s, closed, ghost, 1.0, 0.55)
        return 0.0
    }
    if closed && fill >= 0.0 && len(s) >= 6.0 { p_poly(s, fill, op) }
    if stroke >= 0.0 { vc_draw_stroke(s, closed, stroke, st_get(vk(k, "sw"), 1.0) * z, op) }
}
fn vc_draw_doc(x, y, w, h) {
    bind dw = st_get("vc_w", 512.0) bind dh = st_get("vc_h", 512.0)
    bind x0 = vc_sx(0.0) bind y0 = vc_sy(0.0)
    bind sw = dw * st_get("vc_zoom", 1.0) bind sh = dh * st_get("vc_zoom", 1.0)
    px_rect(x0 + 4.0, y0 + 5.0, sw, sh, 0, 0.5)
    bind bg = st_get("vc_bg", 16777215.0)
    if bg >= 0.0 { px_rect(x0, y0, sw, sh, bg) }
    else {
        # checkerboard for transparency
        bind cs = 16.0
        bind yy = 0.0
        while yy < sh {
            bind xx = 0.0
            while xx < sw {
                bind odd = (floor(xx / cs) + floor(yy / cs)) % 2.0
                px_rect(x0 + xx, y0 + yy, min(cs, sw - xx), min(cs, sh - yy), sel(odd > 0.5, 13421772.0, 16777215.0))
                bind xx = xx + cs
            }
            bind yy = yy + cs
        }
    }
    # onion skin (previous / next keyframe ghosts) in animation mode
    if st_get("mode", 0.0) == 6.0 && st_get("vc_onion", 0.0) > 0.5 && !tl_playing() { an_draw_onion() }
    bind ids = vc_ids()
    bind i = 0.0
    while i < len(ids) {
        bind k = list_get(ids, i)
        if vc_layer_visible(st_get(vk(k, "layer"), 0.0)) { vc_draw_item(k, 0.0 - 1.0) }
        bind i = i + 1.0
    }
    px_outline(x0, y0, sw, sh, ds_col("ink"), 0.8)
    ds_brackets(x0 - 3.0, y0 - 3.0, sw + 6.0, sh + 6.0, 12.0, ds_col("call"))
}
fn vc_draw_selection() {
    bind tool = st_get("vc_tool", "select")
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    if tool == "node" || tool == "pen" {
        bind i = 0.0
        while i < len(s) { vc_draw_nodes(list_get(s, i)) bind i = i + 1.0 }
        return 0.0
    }
    bind b = vc_sel_bounds()
    bind x0 = vc_sx(list_get(b, 0)) bind y0 = vc_sy(list_get(b, 1)) bind x1 = vc_sx(list_get(b, 2)) bind y1 = vc_sy(list_get(b, 3))
    g_dash_rect(x0 - 4.0, y0 - 4.0, x1 - x0 + 8.0, y1 - y0 + 8.0, ds_col("call"))
    bind hs = vc_handles(b)
    bind i = 0.0
    while i < 4.0 {
        bind hx = list_get(hs, i * 2.0) bind hy = list_get(hs, i * 2.0 + 1.0)
        px_rect(hx - 4.0, hy - 4.0, 8.0, 8.0, C_BG_DEEP)
        px_outline(hx - 4.0, hy - 4.0, 8.0, 8.0, ds_col("call"))
        bind i = i + 1.0
    }
    bind rx = (x0 + x1) * 0.5 bind ry = y0 - 26.0
    px_line(rx, y0 - 4.0, rx, ry, ds_col("call"), 0.8)
    px_disc(rx, ry, 5.0, C_BG_DEEP)
    px_ring(rx, ry, 5.0, ds_col("call"), 1.0)
}
# corner handle screen positions [x0,y0, x1,y0, x1,y1, x0,y1] (padded)
fn vc_handles(b) {
    bind x0 = vc_sx(list_get(b, 0)) - 4.0 bind y0 = vc_sy(list_get(b, 1)) - 4.0 bind x1 = vc_sx(list_get(b, 2)) + 4.0 bind y1 = vc_sy(list_get(b, 3)) + 4.0
    return [x0, y0, x1, y0, x1, y1, x0, y1]
}
fn vc_draw_nodes(k) {
    bind pts = st_get(vk(k, "pts"), [])
    bind m = vc_xf(k)
    bind n = floor(len(pts) / 6.0)
    bind nsel = st_get("vc_nsel_" + num_str(k), [])
    bind flat = vc_flatten(k, 1.5 / st_get("vc_zoom", 1.0))
    vc_draw_stroke(vc_to_screen(flat), st_get(vk(k, "closed"), 0.0) > 0.5, ds_col("call"), 1.0, 0.9)
    bind i = 0.0
    while i < n {
        bind p = xf_pt(m, list_get(pts, i * 6.0), list_get(pts, i * 6.0 + 1.0))
        bind hi = xf_pt(m, list_get(pts, i * 6.0 + 2.0), list_get(pts, i * 6.0 + 3.0))
        bind ho = xf_pt(m, list_get(pts, i * 6.0 + 4.0), list_get(pts, i * 6.0 + 5.0))
        bind sx = vc_sx(list_get(p, 0)) bind sy = vc_sy(list_get(p, 1))
        bind on = false
        if i < len(nsel) { if list_get(nsel, i) > 0.5 { bind on = true } }
        if on {
            bind hx = vc_sx(list_get(hi, 0)) bind hy = vc_sy(list_get(hi, 1))
            bind ox = vc_sx(list_get(ho, 0)) bind oy = vc_sy(list_get(ho, 1))
            if abs(hx - sx) + abs(hy - sy) > 0.5 { px_line(sx, sy, hx, hy, C_TEXT_DIM, 0.9) px_disc(hx, hy, 3.5, C_ORANGE) }
            if abs(ox - sx) + abs(oy - sy) > 0.5 { px_line(sx, sy, ox, oy, C_TEXT_DIM, 0.9) px_disc(ox, oy, 3.5, C_ORANGE) }
        }
        px_rect(sx - 4.0, sy - 4.0, 8.0, 8.0, sel(on, C_SEL_EL, C_BG_DEEP))
        px_outline(sx - 4.0, sy - 4.0, 8.0, 8.0, sel(on, C_TEXT_STRONG, ds_col("call")))
        if i == 0.0 { px_ring(sx, sy, 7.0, ds_col("call"), 0.6) }
        bind i = i + 1.0
    }
}

# ── hit testing ──────────────────────────────────────────────────────────────
fn vc_pick(mx, my) {
    bind dx = vc_dx(mx) bind dy = vc_dy(my)
    bind tol = 5.0 / st_get("vc_zoom", 1.0)
    bind ids = vc_ids()
    bind i = len(ids) - 1.0
    while i >= 0.0 {
        bind k = list_get(ids, i)
        bind l = st_get(vk(k, "layer"), 0.0)
        if vc_layer_visible(l) && !vc_layer_locked(l) {
            bind f = vc_flatten(k, tol)
            bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
            if closed && st_get(vk(k, "fill"), 0.0 - 1.0) >= 0.0 && len(f) >= 6.0 { if mx_pt_in_poly(dx, dy, f) { return k } }
            # near the outline
            bind n = floor(len(f) / 2.0)
            bind segs = sel(closed, n, n - 1.0)
            bind wtol = tol + st_get(vk(k, "sw"), 1.0) * 0.5
            bind j = 0.0
            while j < segs {
                bind q = (j + 1.0) % n
                if mx_pt_seg2(dx, dy, list_get(f, j * 2.0), list_get(f, j * 2.0 + 1.0), list_get(f, q * 2.0), list_get(f, q * 2.0 + 1.0)) < wtol { return k }
                bind j = j + 1.0
            }
        }
        bind i = i - 1.0
    }
    return 0.0 - 1.0
}
# node/handle under the pointer among the selected items: [item, node, part(0 node 1 in 2 out)] or [-1,-1,-1]
fn vc_pick_node(mx, my) {
    bind s = vc_selected()
    bind best = [0.0 - 1.0, 0.0 - 1.0, 0.0 - 1.0]
    bind bd = 8.0
    bind i = 0.0
    while i < len(s) {
        bind k = list_get(s, i)
        bind pts = st_get(vk(k, "pts"), [])
        bind m = vc_xf(k)
        bind nsel = st_get("vc_nsel_" + num_str(k), [])
        bind n = floor(len(pts) / 6.0)
        bind j = 0.0
        while j < n {
            bind part = 0.0
            while part < 3.0 {
                bind check = part == 0.0
                if part > 0.5 { if j < len(nsel) { if list_get(nsel, j) > 0.5 { bind check = true } } }
                if check {
                    bind p = xf_pt(m, list_get(pts, j * 6.0 + part * 2.0), list_get(pts, j * 6.0 + part * 2.0 + 1.0))
                    bind d = sqrt((vc_sx(list_get(p, 0)) - mx) * (vc_sx(list_get(p, 0)) - mx) + (vc_sy(list_get(p, 1)) - my) * (vc_sy(list_get(p, 1)) - my))
                    # prefer handles over nodes when they overlap
                    if d < bd || (d <= bd + 0.01 && part > 0.5) { bind bd = d bind best = [k, j, part] }
                }
                bind part = part + 1.0
            }
            bind j = j + 1.0
        }
        bind i = i + 1.0
    }
    return best
}

# ── undo (vector document snapshots) ─────────────────────────────────────────
fn vc_doc_keys() { return ["vc_ids", "vc_next", "vc_gnext", "vc_w", "vc_h", "vc_bg", "vc_layers", "vc_lvis", "vc_llock", "vc_layer", "vc_name"] }
fn vc_snap(prefix) {
    bind dk = vc_doc_keys()
    bind i = 0.0
    while i < len(dk) { st_set(prefix + list_get(dk, i), st_get(list_get(dk, i), 0.0)) bind i = i + 1.0 }
    bind ids = vc_ids()
    bind keys = vc_item_keys()
    bind j = 0.0
    while j < len(ids) {
        bind k = list_get(ids, j)
        bind i = 0.0
        while i < len(keys) { st_set(prefix + vk(k, list_get(keys, i)), st_get(vk(k, list_get(keys, i)), 0.0)) bind i = i + 1.0 }
        bind j = j + 1.0
    }
}
fn vc_unsnap(prefix) {
    bind dk = vc_doc_keys()
    bind i = 0.0
    while i < len(dk) { st_set(list_get(dk, i), st_get(prefix + list_get(dk, i), 0.0)) bind i = i + 1.0 }
    bind ids = vc_ids()
    bind keys = vc_item_keys()
    bind j = 0.0
    while j < len(ids) {
        bind k = list_get(ids, j)
        bind i = 0.0
        while i < len(keys) { st_set(vk(k, list_get(keys, i)), st_get(prefix + vk(k, list_get(keys, i)), 0.0)) bind i = i + 1.0 }
        bind j = j + 1.0
    }
    tl_mark_dirty()
}
fn vc_push() {
    bind n = st_get("vc_undo_n", 0.0)
    if n >= 40.0 {
        bind i = 1.0
        while i < n { vc_snap_copy("vu" + num_str(i) + "_", "vu" + num_str(i - 1.0) + "_") bind i = i + 1.0 }
        bind n = n - 1.0
    }
    vc_snap("vu" + num_str(n) + "_")
    st_set("vc_undo_n", n + 1.0)
    st_set("vc_redo_n", 0.0)
    st_set("vc_dirty", 1.0)
}
fn vc_snap_copy(from, to) {
    bind dk = vc_doc_keys()
    bind i = 0.0
    while i < len(dk) { st_set(to + list_get(dk, i), st_get(from + list_get(dk, i), 0.0)) bind i = i + 1.0 }
    bind ids = st_get(from + "vc_ids", [])
    bind keys = vc_item_keys()
    bind j = 0.0
    while j < len(ids) {
        bind k = list_get(ids, j)
        bind i = 0.0
        while i < len(keys) { st_set(to + vk(k, list_get(keys, i)), st_get(from + vk(k, list_get(keys, i)), 0.0)) bind i = i + 1.0 }
        bind j = j + 1.0
    }
}
fn vc_undo() {
    bind n = st_get("vc_undo_n", 0.0)
    if n < 0.5 { st_set("status", "Nothing to undo") return 0.0 }
    bind r = st_get("vc_redo_n", 0.0)
    vc_snap("vr" + num_str(r) + "_")
    st_set("vc_redo_n", r + 1.0)
    vc_unsnap("vu" + num_str(n - 1.0) + "_")
    st_set("vc_undo_n", n - 1.0)
    st_set("vc_drag", "")
    st_set("status", "Undo")
}
fn vc_redo() {
    bind r = st_get("vc_redo_n", 0.0)
    if r < 0.5 { st_set("status", "Nothing to redo") return 0.0 }
    bind n = st_get("vc_undo_n", 0.0)
    vc_snap("vu" + num_str(n) + "_")
    st_set("vc_undo_n", n + 1.0)
    vc_unsnap("vr" + num_str(r - 1.0) + "_")
    st_set("vc_redo_n", r - 1.0)
    st_set("status", "Redo")
}

# ── commands ─────────────────────────────────────────────────────────────────
fn vc_delete() {
    bind tool = st_get("vc_tool", "select")
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    vc_push()
    if tool == "node" {
        # delete selected nodes
        bind i = 0.0
        while i < len(s) {
            bind k = list_get(s, i)
            bind nsel = st_get("vc_nsel_" + num_str(k), [])
            bind pts = st_get(vk(k, "pts"), [])
            bind out = list_new()
            bind n = floor(len(pts) / 6.0)
            bind j = 0.0
            while j < n {
                bind drop = false
                if j < len(nsel) { if list_get(nsel, j) > 0.5 { bind drop = true } }
                if !drop { bind q = 0.0 while q < 6.0 { bind out = list_push(out, list_get(pts, j * 6.0 + q)) bind q = q + 1.0 } }
                bind j = j + 1.0
            }
            st_set(vk(k, "pts"), out)
            st_set("vc_nsel_" + num_str(k), [])
            bind i = i + 1.0
        }
        vc_drop_empty()
        return 0.0
    }
    bind ids = vc_ids()
    bind keep = list_new()
    bind i = 0.0
    while i < len(ids) { bind k = list_get(ids, i) if vc_is_sel(k) { vc_free(k) } else { bind keep = list_push(keep, k) } bind i = i + 1.0 }
    st_set("vc_ids", keep)
    st_set("status", "Deleted " + num_str(len(s)) + " items")
}
fn vc_drop_empty() {
    bind ids = vc_ids()
    bind keep = list_new()
    bind i = 0.0
    while i < len(ids) { bind k = list_get(ids, i) if len(st_get(vk(k, "pts"), [])) < 6.0 { vc_free(k) } else { bind keep = list_push(keep, k) } bind i = i + 1.0 }
    st_set("vc_ids", keep)
}
fn vc_copy_item(k) {
    bind n = vc_add_item(st_get(vk(k, "kind"), "path"), st_get(vk(k, "pts"), []), st_get(vk(k, "closed"), 0.0) > 0.5)
    bind keys = vc_item_keys()
    bind i = 0.0
    while i < len(keys) { bind key = list_get(keys, i) if key != "sel" { st_set(vk(n, key), st_get(vk(k, key), 0.0)) } bind i = i + 1.0 }
    st_set(vk(n, "name"), st_get(vk(k, "name"), "") + " copy")
    return n
}
fn vc_dup() {
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    vc_push()
    bind news = list_new()
    bind i = 0.0
    while i < len(s) {
        bind n = vc_copy_item(list_get(s, i))
        st_set(vk(n, "tx"), st_get(vk(n, "tx"), 0.0) + 12.0)
        st_set(vk(n, "ty"), st_get(vk(n, "ty"), 0.0) + 12.0)
        bind news = list_push(news, n)
        bind i = i + 1.0
    }
    vc_select_all(false)
    bind i = 0.0
    while i < len(news) { st_set(vk(list_get(news, i), "sel"), 1.0) bind i = i + 1.0 }
    st_set("status", "Duplicated " + num_str(len(news)) + " items")
}
fn vc_group() {
    bind s = vc_selected()
    if len(s) < 2.0 { st_set("status", "Select two or more items to group") return 0.0 }
    vc_push()
    bind g = st_get("vc_gnext", 1.0)
    st_set("vc_gnext", g + 1.0)
    bind i = 0.0
    while i < len(s) { st_set(vk(list_get(s, i), "grp"), g) bind i = i + 1.0 }
    st_set("status", "Grouped " + num_str(len(s)) + " items")
}
fn vc_ungroup() {
    bind s = vc_selected()
    vc_push()
    bind i = 0.0
    while i < len(s) { st_set(vk(list_get(s, i), "grp"), 0.0) bind i = i + 1.0 }
    st_set("status", "Ungrouped")
}
fn vc_reorder(dir) {
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    vc_push()
    bind ids = vc_ids()
    bind n = len(ids)
    if dir > 0.0 {
        bind i = n - 2.0
        while i >= 0.0 {
            bind a = list_get(ids, i) bind b = list_get(ids, i + 1.0)
            if vc_is_sel(a) && !vc_is_sel(b) { bind ids = list_set(ids, i, b) bind ids = list_set(ids, i + 1.0, a) }
            bind i = i - 1.0
        }
    } else {
        bind i = 1.0
        while i < n {
            bind a = list_get(ids, i) bind b = list_get(ids, i - 1.0)
            if vc_is_sel(a) && !vc_is_sel(b) { bind ids = list_set(ids, i, b) bind ids = list_set(ids, i - 1.0, a) }
            bind i = i + 1.0
        }
    }
    st_set("vc_ids", ids)
}
fn vc_flip(horiz) {
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    vc_push()
    bind i = 0.0
    while i < len(s) {
        bind k = list_get(s, i)
        bind pv = vc_pivot(k)
        bind pts = st_get(vk(k, "pts"), [])
        bind j = 0.0
        while j < len(pts) {
            if horiz { if j % 2.0 < 0.5 { bind pts = list_set(pts, j, 2.0 * list_get(pv, 0) - list_get(pts, j)) } }
            else { if j % 2.0 > 0.5 { bind pts = list_set(pts, j, 2.0 * list_get(pv, 1) - list_get(pts, j)) } }
            bind j = j + 1.0
        }
        st_set(vk(k, "pts"), pts)
        bind i = i + 1.0
    }
}
fn vc_reverse() {
    bind s = vc_selected()
    vc_push()
    bind i = 0.0
    while i < len(s) {
        bind k = list_get(s, i)
        bind pts = st_get(vk(k, "pts"), [])
        bind n = floor(len(pts) / 6.0)
        bind out = list_new()
        bind j = n - 1.0
        while j >= 0.0 {
            bind out = list_push(out, list_get(pts, j * 6.0)) bind out = list_push(out, list_get(pts, j * 6.0 + 1.0))
            bind out = list_push(out, list_get(pts, j * 6.0 + 4.0)) bind out = list_push(out, list_get(pts, j * 6.0 + 5.0))
            bind out = list_push(out, list_get(pts, j * 6.0 + 2.0)) bind out = list_push(out, list_get(pts, j * 6.0 + 3.0))
            bind j = j - 1.0
        }
        st_set(vk(k, "pts"), out)
        bind i = i + 1.0
    }
}
# simplify: drop nodes that sit on a nearly straight run (corner nodes only)
fn vc_simplify() {
    bind s = vc_selected()
    vc_push()
    bind removed = 0.0
    bind i = 0.0
    while i < len(s) {
        bind k = list_get(s, i)
        bind pts = st_get(vk(k, "pts"), [])
        bind n = floor(len(pts) / 6.0)
        bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
        bind out = list_new()
        bind j = 0.0
        while j < n {
            bind keep = true
            if n > 3.0 && (closed || (j > 0.0 && j < n - 1.0)) {
                bind a = (j + n - 1.0) % n bind c = (j + 1.0) % n
                bind d = mx_pt_seg2(list_get(pts, j * 6.0), list_get(pts, j * 6.0 + 1.0), list_get(pts, a * 6.0), list_get(pts, a * 6.0 + 1.0), list_get(pts, c * 6.0), list_get(pts, c * 6.0 + 1.0))
                bind corner = abs(list_get(pts, j * 6.0 + 2.0) - list_get(pts, j * 6.0)) + abs(list_get(pts, j * 6.0 + 5.0) - list_get(pts, j * 6.0 + 1.0)) < 0.001
                if d < 1.0 && corner { bind keep = false bind removed = removed + 1.0 }
            }
            if keep { bind q = 0.0 while q < 6.0 { bind out = list_push(out, list_get(pts, j * 6.0 + q)) bind q = q + 1.0 } }
            bind j = j + 1.0
        }
        st_set(vk(k, "pts"), out)
        bind i = i + 1.0
    }
    st_set("status", "Simplified: removed " + num_str(removed) + " nodes")
}
fn vc_layer_add() {
    vc_push()
    bind names = st_get("vc_layers", [])
    st_set("vc_layers", list_push(names, "Layer " + num_str(len(names) + 1.0)))
    st_set("vc_lvis", list_push(st_get("vc_lvis", []), 1.0))
    st_set("vc_llock", list_push(st_get("vc_llock", []), 0.0))
    st_set("vc_layer", len(names))
}
fn vc_layer_del() {
    bind names = st_get("vc_layers", [])
    if len(names) < 1.5 { st_set("status", "Keep at least one layer") return 0.0 }
    vc_push()
    bind l = st_get("vc_layer", 0.0)
    bind ids = vc_ids()
    bind keep = list_new()
    bind i = 0.0
    while i < len(ids) {
        bind k = list_get(ids, i)
        bind kl = st_get(vk(k, "layer"), 0.0)
        if kl == l { vc_free(k) } else { if kl > l { st_set(vk(k, "layer"), kl - 1.0) } bind keep = list_push(keep, k) }
        bind i = i + 1.0
    }
    st_set("vc_ids", keep)
    bind nn = list_new() bind nv = list_new() bind nl = list_new()
    bind i = 0.0
    while i < len(names) {
        if i != l { bind nn = list_push(nn, list_get(names, i)) bind nv = list_push(nv, list_get(st_get("vc_lvis", []), i)) bind nl = list_push(nl, list_get(st_get("vc_llock", []), i)) }
        bind i = i + 1.0
    }
    st_set("vc_layers", nn) st_set("vc_lvis", nv) st_set("vc_llock", nl)
    st_set("vc_layer", max(0.0, l - 1.0))
}
fn vc_rename_layer(t) { vc_push() st_lset("vc_layers", st_get("vc_layer", 0.0), t) }
fn vc_rename_item(t) { bind k = st_get("vc_active", 0.0 - 1.0) if k > 0.0 { vc_push() st_set(vk(k, "name"), t) } }
fn vc_set_tool(t) {
    st_set("vc_tool", t)
    vc_pen_finish()
    st_set("status", "Tool: " + t)
}

# ── pen ──────────────────────────────────────────────────────────────────────
fn vc_pen_finish() {
    bind k = st_get("vc_pen", 0.0 - 1.0)
    if k > 0.0 {
        if len(st_get(vk(k, "pts"), [])) < 12.0 { vc_drop_empty() }
        st_set("status", "Path finished")
    }
    st_set("vc_pen", 0.0 - 1.0)
}

# ── mesh conversion ──────────────────────────────────────────────────────────
fn vc_extrude_to_mesh(vals) {
    bind depth = list_get(vals, 0) bind scale = list_get(vals, 1)
    bind s = vc_selected()
    bind cx = st_get("vc_w", 512.0) * 0.5 bind cy = st_get("vc_h", 512.0)
    me_undo_push()
    bind made = 0.0
    bind i = 0.0
    while i < len(s) {
        bind k = list_get(s, i)
        if st_get(vk(k, "closed"), 0.0) > 0.5 {
            bind f = vc_flatten(k, 2.0)
            bind n = floor(len(f) / 2.0)
            if n >= 3.0 {
                # 2D outline -> mesh units (y up, centred on the document's bottom centre)
                bind pts2 = list_new()
                bind j = 0.0
                while j < n { bind pts2 = list_push(pts2, (list_get(f, j * 2.0) - cx) * scale) bind pts2 = list_push(pts2, (cy - list_get(f, j * 2.0 + 1.0)) * scale) bind j = j + 1.0 }
                # make it counter-clockwise (front faces +Z)
                if mx_poly_area(pts2) < 0.0 {
                    bind rev = list_new()
                    bind j = n - 1.0
                    while j >= 0.0 { bind rev = list_push(rev, list_get(pts2, j * 2.0)) bind rev = list_push(rev, list_get(pts2, j * 2.0 + 1.0)) bind j = j - 1.0 }
                    bind pts2 = rev
                }
                bind vx = list_new() bind vy = list_new() bind vz = list_new()
                bind j = 0.0
                while j < n { bind vx = list_push(vx, list_get(pts2, j * 2.0)) bind vy = list_push(vy, list_get(pts2, j * 2.0 + 1.0)) bind vz = list_push(vz, depth * 0.5) bind j = j + 1.0 }
                bind j = 0.0
                while j < n { bind vx = list_push(vx, list_get(pts2, j * 2.0)) bind vy = list_push(vy, list_get(pts2, j * 2.0 + 1.0)) bind vz = list_push(vz, 0.0 - depth * 0.5) bind j = j + 1.0 }
                bind tris = mx_earclip(pts2)
                bind fi = list_new() bind fs = list_new() bind fc = list_new()
                bind t = 0.0
                while t + 2.0 < len(tris) {
                    bind r = pr_tri(fi, fs, fc, list_get(tris, t), list_get(tris, t + 1.0), list_get(tris, t + 2.0))
                    bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                    bind r = pr_tri(fi, fs, fc, n + list_get(tris, t + 2.0), n + list_get(tris, t + 1.0), n + list_get(tris, t))
                    bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                    bind t = t + 3.0
                }
                bind j = 0.0
                while j < n {
                    bind q = (j + 1.0) % n
                    bind r = pr_quad(fi, fs, fc, j, n + j, n + q, q)
                    bind fi = list_get(r, 0) bind fs = list_get(r, 1) bind fc = list_get(r, 2)
                    bind j = j + 1.0
                }
                bind o = me_add_object(st_get(vk(k, "name"), "Shape"), vx, vy, vz, fi, fs, fc)
                bind col = st_get(vk(k, "fill"), 0.0 - 1.0)
                if col >= 0.0 { st_set(mk(o, "col"), col) }
                uv_box(o)
                bind made = made + 1.0
            }
        }
        bind i = i + 1.0
    }
    if made < 0.5 { st_set("status", "Select closed shapes to extrude") return 0.0 }
    set_mode(0.0)
    v3_frame(false)
    st_set("status", "Extruded " + num_str(made) + " shapes into meshes")
}
fn vc_lathe_to_mesh(vals) {
    bind segs = max(3.0, floor(list_get(vals, 0))) bind scale = list_get(vals, 1)
    bind s = vc_selected()
    if len(s) < 0.5 { return 0.0 }
    bind k = list_get(s, 0)
    bind f = vc_flatten(k, 3.0)
    bind n = floor(len(f) / 2.0)
    if n < 2.0 { return 0.0 }
    bind xmin = 1000000.0 bind ymax = 0.0 - 1000000.0
    bind j = 0.0
    while j < n { bind xmin = min(xmin, list_get(f, j * 2.0)) bind ymax = max(ymax, list_get(f, j * 2.0 + 1.0)) bind j = j + 1.0 }
    me_undo_push()
    bind vx = list_new() bind vy = list_new() bind vz = list_new()
    bind r = 0.0
    while r < segs {
        bind a = r / segs * 6.28318530718
        bind j = 0.0
        while j < n {
            bind rad = (list_get(f, j * 2.0) - xmin) * scale
            bind vx = list_push(vx, cos(a) * rad) bind vy = list_push(vy, (ymax - list_get(f, j * 2.0 + 1.0)) * scale) bind vz = list_push(vz, sin(a) * rad)
            bind j = j + 1.0
        }
        bind r = r + 1.0
    }
    bind fi = list_new() bind fs = list_new() bind fc = list_new()
    bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
    bind rows = sel(closed, n, n - 1.0)
    bind r = 0.0
    while r < segs {
        bind r2 = (r + 1.0) % segs
        bind j = 0.0
        while j < rows {
            bind j2 = (j + 1.0) % n
            bind q = pr_quad(fi, fs, fc, r * n + j, r * n + j2, r2 * n + j2, r2 * n + j)
            bind fi = list_get(q, 0) bind fs = list_get(q, 1) bind fc = list_get(q, 2)
            bind j = j + 1.0
        }
        bind r = r + 1.0
    }
    bind o = me_add_object(st_get(vk(k, "name"), "Lathe"), vx, vy, vz, fi, fs, fc)
    bind col = st_get(vk(k, "fill"), 0.0 - 1.0)
    if col < 0.0 { bind col = st_get(vk(k, "stroke"), C_BLUE) }
    st_set(mk(o, "col"), col)
    mo_merge_dist(o, 0.0001)
    mo_recalc(o)
    uv_cyl(o)
    set_mode(0.0)
    v3_frame(false)
    st_set("status", "Lathed " + st_get(vk(k, "name"), "") + " into a mesh (" + num_str(segs) + " segments)")
}

# ── input ────────────────────────────────────────────────────────────────────
fn vc_input(x, y, w, h) {
    bind inside = u_in(x, y, w, h)
    bind tool = st_get("vc_tool", "select")
    bind drag = st_get("vc_drag", "")
    if st_get("blk", 0.0) > 0.5 && drag == "" { return 0.0 }
    # pan / zoom
    if inside && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
        bind z0 = st_get("vc_zoom", 1.0)
        bind z1 = clampf(z0 * pow(1.15, u_scroll()), 0.02, 64.0)
        bind dx = vc_dx(u_mx()) bind dy = vc_dy(u_my())
        st_set("vc_zoom", z1)
        st_set("vc_panx", u_mx() - dx * z1)
        st_set("vc_pany", u_my() - dy * z1)
        st_set("wheel_used", 1.0)
    }
    if drag == "" && inside && (u_mpressed() || (u_pressed() && (tool == "hand" || key_down("space")))) {
        st_set("vc_drag", "pan") st_set("vd_x", u_mx()) st_set("vd_y", u_my()) st_set("vd_px", st_get("vc_panx", 0.0)) st_set("vd_py", st_get("vc_pany", 0.0))
        return 0.0
    }
    if drag == "pan" {
        st_set("vc_panx", st_get("vd_px", 0.0) + u_mx() - st_get("vd_x", 0.0))
        st_set("vc_pany", st_get("vd_py", 0.0) + u_my() - st_get("vd_y", 0.0))
        if !u_down() && !u_mdown() { st_set("vc_drag", "") }
        return 0.0
    }
    bind mx = u_mx() bind my = u_my()
    bind dx = vc_dx(mx) bind dy = vc_dy(my)
    bind snap = u_ctrl()
    if snap { bind dx = round(dx / 8.0) * 8.0 bind dy = round(dy / 8.0) * 8.0 }
    # ── drags in progress ──
    if drag != "" {
        bind ox = st_get("vd_dx", 0.0) bind oy = st_get("vd_dy", 0.0)
        if drag == "move" {
            bind s = st_get("vd_items", [])
            bind snapv = st_get("vd_snap", [])
            bind i = 0.0
            while i < len(s) {
                bind k = list_get(s, i)
                st_set(vk(k, "tx"), list_get(snapv, i * 2.0) + dx - ox)
                st_set(vk(k, "ty"), list_get(snapv, i * 2.0 + 1.0) + dy - oy)
                bind i = i + 1.0
            }
        } else if drag == "scale" || drag == "rotate" {
            bind cxp = st_get("vd_cx", 0.0) bind cyp = st_get("vd_cy", 0.0)
            bind s = st_get("vd_items", [])
            bind snapv = st_get("vd_snap", [])
            bind fx = 1.0 bind fy = 1.0 bind da = 0.0
            if drag == "scale" {
                bind d0x = ox - cxp bind d0y = oy - cyp
                bind fx = sel(abs(d0x) > 0.5, (dx - cxp) / d0x, 1.0)
                bind fy = sel(abs(d0y) > 0.5, (dy - cyp) / d0y, 1.0)
                if !u_shift() { bind f = max(abs(fx), abs(fy)) * sel(fx * fy < 0.0, 0.0 - 1.0, 1.0) bind fx = sel(fx < 0.0, 0.0 - abs(f), abs(f)) bind fy = sel(fy < 0.0, 0.0 - abs(f), abs(f)) }
            } else {
                bind da = mx_atan2(dy - cyp, dx - cxp) - mx_atan2(oy - cyp, ox - cxp)
                if u_shift() { bind da = round(da / 0.2617993878) * 0.2617993878 }
            }
            bind i = 0.0
            while i < len(s) {
                bind k = list_get(s, i)
                bind b = i * 7.0
                bind pvx = list_get(snapv, b) bind pvy = list_get(snapv, b + 1.0)
                bind rx = pvx - cxp bind ry = pvy - cyp
                bind npx = cxp + rx * fx bind npy = cyp + ry * fy
                if drag == "rotate" { bind npx = cxp + rx * cos(da) - ry * sin(da) bind npy = cyp + rx * sin(da) + ry * cos(da) }
                st_set(vk(k, "tx"), list_get(snapv, b + 2.0) + npx - pvx)
                st_set(vk(k, "ty"), list_get(snapv, b + 3.0) + npy - pvy)
                if drag == "scale" { st_set(vk(k, "sx"), list_get(snapv, b + 5.0) * fx) st_set(vk(k, "sy"), list_get(snapv, b + 6.0) * fy) }
                else { st_set(vk(k, "rot"), list_get(snapv, b + 4.0) + da) }
                bind i = i + 1.0
            }
        } else if drag == "node" {
            bind k = st_get("vd_k", 0.0)
            bind part = st_get("vd_part", 0.0)
            bind m = vc_xf(k)
            bind dl = xf_inv_vec(m, dx - ox, dy - oy)
            bind ddx = list_get(dl, 0) bind ddy = list_get(dl, 1)
            bind pts = st_get("vd_pts", [])
            bind nsel = st_get("vc_nsel_" + num_str(k), [])
            bind n = floor(len(pts) / 6.0)
            if part == 0.0 {
                bind j = 0.0
                while j < n {
                    bind on = false
                    if j < len(nsel) { if list_get(nsel, j) > 0.5 { bind on = true } }
                    if on { bind q = 0.0 while q < 6.0 { bind pts = list_set(pts, j * 6.0 + q, list_get(pts, j * 6.0 + q) + sel(q % 2.0 < 0.5, ddx, ddy)) bind q = q + 1.0 } }
                    bind j = j + 1.0
                }
            } else {
                bind j = st_get("vd_node", 0.0)
                bind hx = list_get(pts, j * 6.0 + part * 2.0) + ddx bind hy = list_get(pts, j * 6.0 + part * 2.0 + 1.0) + ddy
                bind pts = list_set(pts, j * 6.0 + part * 2.0, hx) bind pts = list_set(pts, j * 6.0 + part * 2.0 + 1.0, hy)
                if !u_alt() {
                    # smooth: mirror the opposite handle
                    bind other = sel(part == 1.0, 2.0, 1.0)
                    bind nx = list_get(pts, j * 6.0) bind ny = list_get(pts, j * 6.0 + 1.0)
                    bind pts = list_set(pts, j * 6.0 + other * 2.0, nx * 2.0 - hx)
                    bind pts = list_set(pts, j * 6.0 + other * 2.0 + 1.0, ny * 2.0 - hy)
                }
            }
            st_set(vk(k, "pts"), pts)
        } else if drag == "pen" {
            # dragging out the handles of the node just placed
            bind k = st_get("vc_pen", 0.0 - 1.0)
            if k > 0.0 {
                bind pts = st_get(vk(k, "pts"), [])
                bind j = floor(len(pts) / 6.0) - 1.0
                bind nx = list_get(pts, j * 6.0) bind ny = list_get(pts, j * 6.0 + 1.0)
                if abs(dx - nx) + abs(dy - ny) > 2.0 {
                    bind pts = list_set(pts, j * 6.0 + 4.0, dx) bind pts = list_set(pts, j * 6.0 + 5.0, dy)
                    bind pts = list_set(pts, j * 6.0 + 2.0, nx * 2.0 - dx) bind pts = list_set(pts, j * 6.0 + 3.0, ny * 2.0 - dy)
                    st_set(vk(k, "pts"), pts)
                }
            }
        } else if drag == "shape" {
            bind k = st_get("vd_k", 0.0)
            bind x0 = ox bind y0 = oy bind x1 = dx bind y1 = dy
            if u_shift() { bind sz = max(abs(x1 - x0), abs(y1 - y0)) bind x1 = x0 + sel(x1 < x0, 0.0 - sz, sz) bind y1 = y0 + sel(y1 < y0, 0.0 - sz, sz) }
            bind kind = st_get(vk(k, "kind"), "rect")
            bind pts = []
            if kind == "rect" { bind pts = vc_rect_pts(min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)) }
            if kind == "ellipse" { bind pts = vc_ellipse_pts((x0 + x1) * 0.5, (y0 + y1) * 0.5, max(0.5, abs(x1 - x0) * 0.5), max(0.5, abs(y1 - y0) * 0.5)) }
            if kind == "polygon" { bind pts = vc_poly_pts(x0, y0, max(0.5, sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0))), st_get("vc_sides", 6.0), 0.0) }
            if kind == "star" { bind pts = vc_poly_pts(x0, y0, max(0.5, sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0))), st_get("vc_sides", 5.0), 0.45) }
            if kind == "line" { bind pts = vc_nodes_from_poly([x0, y0, x1, y1]) }
            st_set(vk(k, "pts"), pts)
        } else if drag == "box" {
            bind sx0 = st_get("vd_sx", 0.0) bind sy0 = st_get("vd_sy", 0.0)
            g_dash_rect(min(sx0, mx), min(sy0, my), abs(mx - sx0), abs(my - sy0), C_TEXT_STRONG)
        }
        if !u_down() {
            if drag == "box" { vc_box_select(st_get("vd_sx", 0.0), st_get("vd_sy", 0.0), mx, my, u_shift()) }
            if drag == "shape" {
                bind k = st_get("vd_k", 0.0)
                bind b = vc_bounds_pts(st_get(vk(k, "pts"), []))
                if list_get(b, 2) - list_get(b, 0) < 1.0 && list_get(b, 3) - list_get(b, 1) < 1.0 {
                    # a click without a drag: make a default-sized shape
                    bind kind = st_get(vk(k, "kind"), "rect")
                    if kind == "rect" { st_set(vk(k, "pts"), vc_rect_pts(ox - 40.0, oy - 30.0, ox + 40.0, oy + 30.0)) }
                    if kind == "ellipse" { st_set(vk(k, "pts"), vc_ellipse_pts(ox, oy, 40.0, 40.0)) }
                    if kind == "polygon" { st_set(vk(k, "pts"), vc_poly_pts(ox, oy, 40.0, st_get("vc_sides", 6.0), 0.0)) }
                    if kind == "star" { st_set(vk(k, "pts"), vc_poly_pts(ox, oy, 40.0, st_get("vc_sides", 5.0), 0.45)) }
                    if kind == "line" { st_set(vk(k, "pts"), vc_nodes_from_poly([ox - 40.0, oy, ox + 40.0, oy])) }
                }
            }
            if drag == "move" || drag == "scale" || drag == "rotate" || drag == "node" {
                if st_get("mode", 0.0) == 6.0 && st_get("autokey", 0.0) > 0.5 { if drag == "node" { an_shapekey() } else { an_key() } }
            }
            st_set("vc_drag", "")
        }
        return 0.0
    }
    if !inside { return 0.0 }
    # ── presses ──
    if u_rpressed() {
        if tool == "pen" && st_get("vc_pen", 0.0 - 1.0) > 0.0 { vc_pen_finish() return 0.0 }
        ctx_open(["Duplicate|edit.dup|Ctrl+D|copy", "Delete|edit.delete|X|trash", "Group|vec.group|Ctrl+G|layers", "Ungroup|vec.ungroup||layers", "Bring Forward|vec.fwd|PgUp|arrow_up", "Send Backward|vec.back|PgDn|arrow_down", "Flip Horizontal|vec.flip_h||mirror", "Extrude to 3D...|vec.extrude||extrude", "Lathe to 3D...|vec.lathe||spin"], mx, my)
        return 0.0
    }
    if !u_pressed() { return 0.0 }
    st_set("vd_dx", dx) st_set("vd_dy", dy)
    if tool == "select" {
        # handles of the current selection first
        if vc_sel_count() > 0.5 {
            bind b = vc_sel_bounds()
            bind hs = vc_handles(b)
            bind cxp = (list_get(b, 0) + list_get(b, 2)) * 0.5 bind cyp = (list_get(b, 1) + list_get(b, 3)) * 0.5
            bind rx = (vc_sx(list_get(b, 0)) + vc_sx(list_get(b, 2))) * 0.5 bind ry = vc_sy(list_get(b, 1)) - 26.0
            bind hit = ""
            if abs(mx - rx) < 7.0 && abs(my - ry) < 7.0 { bind hit = "rotate" }
            bind i = 0.0
            while i < 4.0 {
                if abs(mx - list_get(hs, i * 2.0)) < 6.0 && abs(my - list_get(hs, i * 2.0 + 1.0)) < 6.0 {
                    bind hit = "scale"
                    # scale about the opposite corner
                    bind oi = (i + 2.0) % 4.0
                    bind cxp = vc_dx(list_get(hs, oi * 2.0)) bind cyp = vc_dy(list_get(hs, oi * 2.0 + 1.0))
                }
                bind i = i + 1.0
            }
            if hit != "" {
                vc_push()
                bind s = vc_selected()
                bind snapv = list_new()
                bind i = 0.0
                while i < len(s) {
                    bind k = list_get(s, i)
                    bind m = vc_xf(k)
                    bind pv = vc_pivot(k)
                    bind pw = xf_pt(m, list_get(pv, 0), list_get(pv, 1))
                    bind snapv = list_push(snapv, list_get(pw, 0)) bind snapv = list_push(snapv, list_get(pw, 1))
                    bind snapv = list_push(snapv, st_get(vk(k, "tx"), 0.0)) bind snapv = list_push(snapv, st_get(vk(k, "ty"), 0.0))
                    bind snapv = list_push(snapv, st_get(vk(k, "rot"), 0.0)) bind snapv = list_push(snapv, st_get(vk(k, "sx"), 1.0)) bind snapv = list_push(snapv, st_get(vk(k, "sy"), 1.0))
                    bind i = i + 1.0
                }
                st_set("vd_items", s) st_set("vd_snap", snapv)
                st_set("vd_cx", cxp) st_set("vd_cy", cyp)
                st_set("vc_drag", hit)
                return 0.0
            }
        }
        bind k = vc_pick(mx, my)
        if k < 0.0 {
            if !u_shift() { vc_select_all(false) }
            st_set("vc_drag", "box") st_set("vd_sx", mx) st_set("vd_sy", my)
            return 0.0
        }
        if !vc_is_sel(k) || u_shift() { vc_select(k, u_shift()) }
        st_set("vc_active", k)
        if u_double() { st_set("vc_tool", "node") return 0.0 }
        vc_push()
        bind s = vc_selected()
        bind snapv = list_new()
        bind i = 0.0
        while i < len(s) { bind snapv = list_push(snapv, st_get(vk(list_get(s, i), "tx"), 0.0)) bind snapv = list_push(snapv, st_get(vk(list_get(s, i), "ty"), 0.0)) bind i = i + 1.0 }
        st_set("vd_items", s) st_set("vd_snap", snapv)
        st_set("vc_drag", "move")
        return 0.0
    }
    if tool == "node" {
        bind r = vc_pick_node(mx, my)
        bind k = list_get(r, 0)
        if k > 0.0 {
            bind j = list_get(r, 1) bind part = list_get(r, 2)
            bind key = "vc_nsel_" + num_str(k)
            bind n = floor(len(st_get(vk(k, "pts"), [])) / 6.0)
            bind nsel = st_get(key, [])
            if len(nsel) != n { bind nsel = me_zeros(n) }
            if part == 0.0 {
                if u_double() {
                    # toggle corner / smooth
                    vc_push()
                    bind pts = st_get(vk(k, "pts"), [])
                    bind corner = abs(list_get(pts, j * 6.0 + 4.0) - list_get(pts, j * 6.0)) + abs(list_get(pts, j * 6.0 + 5.0) - list_get(pts, j * 6.0 + 1.0)) < 0.001
                    bind nx = list_get(pts, j * 6.0) bind ny = list_get(pts, j * 6.0 + 1.0)
                    if corner {
                        bind a = (j + n - 1.0) % n bind c = (j + 1.0) % n
                        bind tx = (list_get(pts, c * 6.0) - list_get(pts, a * 6.0)) * 0.25 bind ty = (list_get(pts, c * 6.0 + 1.0) - list_get(pts, a * 6.0 + 1.0)) * 0.25
                        bind pts = list_set(pts, j * 6.0 + 2.0, nx - tx) bind pts = list_set(pts, j * 6.0 + 3.0, ny - ty)
                        bind pts = list_set(pts, j * 6.0 + 4.0, nx + tx) bind pts = list_set(pts, j * 6.0 + 5.0, ny + ty)
                    } else {
                        bind pts = list_set(pts, j * 6.0 + 2.0, nx) bind pts = list_set(pts, j * 6.0 + 3.0, ny)
                        bind pts = list_set(pts, j * 6.0 + 4.0, nx) bind pts = list_set(pts, j * 6.0 + 5.0, ny)
                    }
                    st_set(vk(k, "pts"), pts)
                    return 0.0
                }
                if !u_shift() {
                    bind on = list_get(nsel, j) > 0.5
                    if !on { bind nsel = me_zeros(n) }
                }
                bind nsel = list_set(nsel, j, sel(u_shift() && list_get(nsel, j) > 0.5, 0.0, 1.0))
                st_set(key, nsel)
            }
            vc_push()
            st_set("vd_k", k) st_set("vd_node", j) st_set("vd_part", part)
            st_set("vd_pts", st_get(vk(k, "pts"), []))
            st_set("vc_drag", "node")
            return 0.0
        }
        # ctrl+click on an outline inserts a node
        bind k2 = vc_pick(mx, my)
        if k2 > 0.0 && u_ctrl() && vc_is_sel(k2) { vc_insert_node(k2, dx, dy) return 0.0 }
        if k2 > 0.0 { vc_select(k2, u_shift()) st_set("vc_nsel_" + num_str(k2), []) return 0.0 }
        vc_select_all(false)
        return 0.0
    }
    if tool == "pen" {
        bind k = st_get("vc_pen", 0.0 - 1.0)
        if k > 0.0 {
            # clicking the first node closes the path
            bind pts = st_get(vk(k, "pts"), [])
            if len(pts) >= 18.0 {
                bind fx = vc_sx(list_get(pts, 0)) bind fy = vc_sy(list_get(pts, 1))
                if abs(mx - fx) < 8.0 && abs(my - fy) < 8.0 {
                    vc_push()
                    st_set(vk(k, "closed"), 1.0)
                    if st_get(vk(k, "fill"), 0.0 - 1.0) < 0.0 { st_set(vk(k, "fill"), st_get("vc_fill", 0.0)) }
                    vc_pen_finish()
                    st_set("status", "Path closed")
                    return 0.0
                }
            }
            vc_push()
            bind n = vc_node(dx, dy)
            bind q = 0.0
            while q < 6.0 { bind pts = list_push(pts, list_get(n, q)) bind q = q + 1.0 }
            st_set(vk(k, "pts"), pts)
        } else {
            vc_push()
            bind k = vc_add_item("path", vc_node(dx, dy), false)
            st_set(vk(k, "fill"), 0.0 - 1.0)
            st_set("vc_pen", k)
            vc_select(k, false)
            st_set("status", "Pen: click = corner, drag = curve, click the first node to close, Enter / right-click to finish")
        }
        st_set("vc_drag", "pen")
        return 0.0
    }
    if tool == "rect" || tool == "ellipse" || tool == "polygon" || tool == "star" || tool == "line" {
        vc_push()
        bind k = vc_add_item(tool, vc_nodes_from_poly([dx, dy, dx, dy]), tool != "line")
        if tool == "line" { st_set(vk(k, "fill"), 0.0 - 1.0) }
        vc_select(k, false)
        st_set("vd_k", k)
        st_set("vc_drag", "shape")
        return 0.0
    }
}
fn vc_box_select(sx0, sy0, sx1, sy1, extend) {
    bind x0 = vc_dx(min(sx0, sx1)) bind y0 = vc_dy(min(sy0, sy1)) bind x1 = vc_dx(max(sx0, sx1)) bind y1 = vc_dy(max(sy0, sy1))
    if !extend { vc_select_all(false) }
    if st_get("vc_tool", "select") == "node" {
        bind s = vc_selected()
        bind i = 0.0
        while i < len(s) {
            bind k = list_get(s, i)
            bind pts = st_get(vk(k, "pts"), [])
            bind m = vc_xf(k)
            bind n = floor(len(pts) / 6.0)
            bind nsel = me_zeros(n)
            bind j = 0.0
            while j < n {
                bind p = xf_pt(m, list_get(pts, j * 6.0), list_get(pts, j * 6.0 + 1.0))
                if list_get(p, 0) >= x0 && list_get(p, 0) <= x1 && list_get(p, 1) >= y0 && list_get(p, 1) <= y1 { bind nsel = list_set(nsel, j, 1.0) }
                bind j = j + 1.0
            }
            st_set("vc_nsel_" + num_str(k), nsel)
            bind i = i + 1.0
        }
        return 0.0
    }
    bind ids = vc_ids()
    bind i = 0.0
    while i < len(ids) {
        bind k = list_get(ids, i)
        bind l = st_get(vk(k, "layer"), 0.0)
        if vc_layer_visible(l) && !vc_layer_locked(l) {
            bind b = vc_item_bounds(k)
            if list_get(b, 0) >= x0 && list_get(b, 2) <= x1 && list_get(b, 1) >= y0 && list_get(b, 3) <= y1 { vc_select(k, true) }
        }
        bind i = i + 1.0
    }
}
# insert a node on the segment nearest to (dx,dy) (straight split at the nearest parameter)
fn vc_insert_node(k, dx, dy) {
    bind pts = st_get(vk(k, "pts"), [])
    bind m = vc_xf(k)
    bind n = floor(len(pts) / 6.0)
    bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
    bind segs = sel(closed, n, n - 1.0)
    bind best = 0.0 - 1.0 bind bd = 1000000.0
    bind i = 0.0
    while i < segs {
        bind j = (i + 1.0) % n
        bind a = xf_pt(m, list_get(pts, i * 6.0), list_get(pts, i * 6.0 + 1.0))
        bind b = xf_pt(m, list_get(pts, j * 6.0), list_get(pts, j * 6.0 + 1.0))
        bind d = mx_pt_seg2(dx, dy, list_get(a, 0), list_get(a, 1), list_get(b, 0), list_get(b, 1))
        if d < bd { bind bd = d bind best = i }
        bind i = i + 1.0
    }
    if best < 0.0 { return 0.0 }
    vc_push()
    # local coordinates of the click
    bind e = list_get(m, 4) bind f = list_get(m, 5)
    bind loc = xf_inv_vec(m, dx - e, dy - f)
    bind out = list_new()
    bind i = 0.0
    while i < n {
        bind q = 0.0
        while q < 6.0 { bind out = list_push(out, list_get(pts, i * 6.0 + q)) bind q = q + 1.0 }
        if i == best {
            bind nd = vc_node(list_get(loc, 0), list_get(loc, 1))
            bind q = 0.0
            while q < 6.0 { bind out = list_push(out, list_get(nd, q)) bind q = q + 1.0 }
        }
        bind i = i + 1.0
    }
    st_set(vk(k, "pts"), out)
    st_set("vc_nsel_" + num_str(k), [])
    st_set("status", "Inserted a node")
}

# ── view / toolbar / panels ──────────────────────────────────────────────────
fn vc_view(x, y, w, h) {
    px_rect(x, y, w, h, C_BG_VIEW)
    if st_get("vc_zoom", 0.0) <= 0.0 { vc_fit(x, y, w, h) }
    u_clip(x, y, w, h)
    # dotted grid
    bind z = st_get("vc_zoom", 1.0)
    bind step = 32.0 * z
    if step >= 6.0 {
        bind gx = st_get("vc_panx", 0.0) % step
        bind gy = st_get("vc_pany", 0.0) % step
        bind yy = y + gy
        while yy < y + h {
            bind xx = x + gx
            while xx < x + w { px_rect(xx, yy, 1.0, 1.0, C_GRID_MAJ) bind xx = xx + step }
            bind yy = yy + step
        }
    }
    vc_draw_doc(x, y, w, h)
    vc_draw_selection()
    vc_input(x, y, w, h)
    # pen preview line
    bind pk = st_get("vc_pen", 0.0 - 1.0)
    if st_get("vc_tool", "") == "pen" && pk > 0.0 && st_get("vc_drag", "") == "" {
        bind pts = st_get(vk(pk, "pts"), [])
        if len(pts) >= 6.0 {
            bind j = floor(len(pts) / 6.0) - 1.0
            bind p = xf_pt(vc_xf(pk), list_get(pts, j * 6.0), list_get(pts, j * 6.0 + 1.0))
            g_dash(vc_sx(list_get(p, 0)), vc_sy(list_get(p, 1)), u_mx(), u_my(), ds_col("call"), 4.0)
        }
    }
    u_unclip()
    bind title = sel(st_get("mode", 0.0) == 6.0, "Animation", "Vector") + "  |  " + st_get("vc_name", "drawing") + "  " + fmt0(st_get("vc_w", 512.0)) + " x " + fmt0(st_get("vc_h", 512.0)) + "  |  " + fmt0(z * 100.0) + "%"
    if st_get("mode", 0.0) == 6.0 { bind title = title + "  |  frame " + fmt0(tl_frame()) }
    g_text(x + 10.0, y + 8.0, 12.0, C_TEXT, title)
}
fn vc_tools() { return ["select", "node", "pen", "rect", "ellipse", "polygon", "star", "line", "hand"] }
fn vc_toolbar(x, by) {
    bind tools = vc_tools()
    bind icons = ["select", "node", "pen", "rect", "ellipse", "polygon", "star", "line", "hand"]
    bind tips = ["Select / move / scale / rotate (V)", "Edit nodes and bezier handles (N): double-click toggles corner/smooth, Ctrl+click adds, Alt breaks handles", "Pen (P): click corners, drag curves", "Rectangle (R), Shift = square", "Ellipse (E), Shift = circle", "Polygon (G)", "Star (S)", "Line (L)", "Pan (H, or Space / middle drag)"]
    bind cur = st_get("vc_tool", "select")
    bind xx = x
    bind i = 0.0
    while i < len(tools) {
        if u_tool("vct_" + list_get(tools, i), xx, by, 28.0, 24.0, list_get(icons, i), "", cur == list_get(tools, i), C_BLUE, list_get(tips, i)) { vc_set_tool(list_get(tools, i)) }
        bind xx = xx + 30.0
        bind i = i + 1.0
    }
    bind xx = xx + 8.0
    u_vsep(xx, by + 2.0, 20.0)
    bind xx = xx + 8.0
    # current fill / stroke swatches
    g_text(xx, by + 6.0, 11.0, C_TEXT_DIM, "Fill")
    if u_swatch("vct_fill", xx + 26.0, by + 3.0, 18.0, max(0.0, st_get("vc_fill", 0.0)), false) { st_set("vcp_target", 0.0) }
    g_text(xx + 52.0, by + 6.0, 11.0, C_TEXT_DIM, "Stroke")
    if u_swatch("vct_stroke", xx + 92.0, by + 3.0, 18.0, max(0.0, st_get("vc_stroke", 0.0)), false) { st_set("vcp_target", 1.0) }
    bind nv = u_dragval("vct_sw", xx + 116.0, by, 64.0, 24.0, st_get("vc_sw", 2.0), 0.0, 100.0, 0.1, "W " + fmt1(st_get("vc_sw", 2.0)))
    if u_has(nv) { st_set("vc_sw", nv) }
    bind nv = u_dragval("vct_sides", xx + 184.0, by, 64.0, 24.0, st_get("vc_sides", 6.0), 3.0, 64.0, 0.05, "Sides " + fmt0(st_get("vc_sides", 6.0)))
    if u_has(nv) { st_set("vc_sides", round(nv)) }
    bind xx = xx + 256.0
    if u_tool("vct_fit", xx, by, 28.0, 24.0, "fit", "", false, 0.0, "Fit the page (Home)") { st_set("vc_zoom", 0.0) }
    if u_tool("vct_ext", xx + 30.0, by, 28.0, 24.0, "extrude", "", false, C_GREEN, "Extrude selection to a 3D mesh") { do_action("vec.extrude") }
    if u_tool("vct_lathe", xx + 60.0, by, 28.0, 24.0, "spin", "", false, C_GREEN, "Lathe a profile into a 3D mesh") { do_action("vec.lathe") }
    if st_get("mode", 0.0) == 6.0 {
        if u_tool("vct_onion", xx + 96.0, by, 28.0, 24.0, "onion", "", st_get("vc_onion", 0.0) > 0.5, C_VIOLET, "Onion skin (neighbouring keyframes)") { st_set("vc_onion", 1.0 - st_get("vc_onion", 0.0)) }
        return xx + 128.0
    }
    return xx + 96.0
}
fn vc_status_text() {
    return num_str(len(vc_ids())) + " items, " + num_str(vc_sel_count()) + " selected   |   wheel zoom, middle drag / Space pan, Ctrl snaps to 8 px, Shift constrains"
}
fn vc_sidebar(x, y, w, h) {
    u_header(x + 8.0, y + 8.0, w - 16.0, "Layers", ds_col("call"))
    bind names = st_get("vc_layers", [])
    bind cur = st_get("vc_layer", 0.0)
    bind ly = y + 26.0
    bind i = len(names) - 1.0
    while i >= 0.0 {
        bind r = u_row("vcl_row" + num_str(i), x + 2.0, ly, w - 56.0, 20.0, list_get(names, i), i == cur, C_BLUE, "layers")
        if r > 1.5 { st_set("vc_layer", i) modal_text("Rename layer", "layer", list_get(names, i)) }
        else if r > 0.5 { st_set("vc_layer", i) }
        bind vis = vc_layer_visible(i)
        if u_hot(x + w - 50.0, ly, 20.0, 20.0) { u_tip("Show / hide") if u_pressed() { st_lset("vc_lvis", i, sel(vis, 0.0, 1.0)) } }
        icon_draw(sel(vis, "eye", "eye_off"), x + w - 40.0, ly + 10.0, 11.0, sel(vis, C_TEXT_DIM, C_LINE))
        bind lk = vc_layer_locked(i)
        if u_hot(x + w - 26.0, ly, 20.0, 20.0) { u_tip("Lock") if u_pressed() { st_lset("vc_llock", i, sel(lk, 0.0, 1.0)) } }
        icon_draw("lock", x + w - 16.0, ly + 10.0, 10.0, sel(lk, C_ORANGE, C_LINE))
        bind ly = ly + 22.0
        bind i = i - 1.0
    }
    bind bw = floor((w - 20.0) / 2.0)
    if u_button("vcl_add", x + 8.0, ly + 2.0, bw, 20.0, "+ Layer", 0.0) { vc_layer_add() }
    if u_button("vcl_del", x + 12.0 + bw, ly + 2.0, bw, 20.0, "Delete", 0.0) { vc_layer_del() }
    bind ly = ly + 30.0
    u_header(x + 8.0, ly, w - 16.0, "Items", ds_col("func"))
    bind ids = vc_ids()
    bind top = ly + 18.0
    bind lh = y + h - top - 4.0
    bind off = u_scroll_y("vci_scroll", x, top, w, lh, len(ids) * 20.0 + 4.0, st_get("vci_off", 0.0))
    st_set("vci_off", off)
    u_clip(x, top, w, lh)
    bind i = len(ids) - 1.0
    bind row = 0.0
    while i >= 0.0 {
        bind k = list_get(ids, i)
        bind ry = top + 2.0 + row * 20.0 - off
        bind label = st_get(vk(k, "name"), "") + sel(st_get(vk(k, "grp"), 0.0) > 0.5, "  [g" + fmt0(st_get(vk(k, "grp"), 0.0)) + "]", "")
        bind col = st_get(vk(k, "fill"), 0.0 - 1.0)
        if col < 0.0 { bind col = st_get(vk(k, "stroke"), C_TEXT_DIM) }
        bind r = u_row("vci_row" + num_str(k), x + 2.0, ry, w - 4.0, 20.0, label, vc_is_sel(k), col, list_get(["rect", "ellipse", "polygon", "star", "line", "pen"], vc_kind_icon(st_get(vk(k, "kind"), "path"))))
        if r > 1.5 { st_set("vc_active", k) modal_text("Rename item", "item", st_get(vk(k, "name"), "")) }
        else if r > 0.5 { vc_select(k, u_shift()) }
        bind row = row + 1.0
        bind i = i - 1.0
    }
    u_unclip()
}
fn vc_kind_icon(kind) {
    if kind == "rect" { return 0.0 }
    if kind == "ellipse" { return 1.0 }
    if kind == "polygon" { return 2.0 }
    if kind == "star" { return 3.0 }
    if kind == "line" { return 4.0 }
    return 5.0
}
# colour picker block shared by fill / stroke. returns the picked colour, -1 = none chosen, -2 = no change
fn vc_color_block(id, x, y, w, cur) {
    bind pal = [0.0, 4210752.0, 8421504.0, 12632256.0, 16777215.0, C_RED, C_ORANGE, C_YELLOW, C_GREEN, C_BLUE, C_INDIGO, C_VIOLET, c_rgb(120.0, 70.0, 40.0), c_rgb(250.0, 200.0, 170.0)]
    bind out = 0.0 - 2.0
    bind i = 0.0
    while i < len(pal) {
        bind sx = x + (i % 7.0) * 24.0
        bind sy = y + floor(i / 7.0) * 24.0
        if u_swatch(id + num_str(i), sx, sy, 20.0, list_get(pal, i), cur == list_get(pal, i)) { bind out = list_get(pal, i) }
        bind i = i + 1.0
    }
    bind hy = y + 50.0
    if u_chip(id + "none", x, hy, 44.0, 20.0, "None", cur < 0.0, C_RED) { bind out = 0.0 - 1.0 }
    bind shown = sel(cur >= 0.0, c_hex(max(0.0, cur)), "none")
    bind hx = u_textfield(id + "hex", x + 50.0, hy, 80.0, 20.0, sel(st_get("focus", "") == id + "hex", st_get(id + "hex_t", shown), shown))
    st_set(id + "hex_t", hx)
    if st_get("focus", "") != id + "hex" && hx != shown { bind v = c_from_hex(hx) if v >= 0.0 { bind out = v } }
    if cur >= 0.0 { px_rect(x + 136.0, hy, 20.0, 20.0, cur) px_outline(x + 136.0, hy, 20.0, 20.0, C_LINE) }
    return out
}
fn vc_props(x, y, w) {
    bind cy = y
    u_header(x, cy, w, "Document", ds_col("call"))
    bind cy = cy + 20.0
    bind nm = u_textfield("vcp_doc", x, cy, w, 22.0, sel(st_get("focus", "") == "vcp_doc", st_get("vcp_doc_t", st_get("vc_name", "drawing")), st_get("vc_name", "drawing")))
    st_set("vcp_doc_t", nm)
    if st_get("focus", "") != "vcp_doc" && nm != st_get("vc_name", "") && str_trim(nm) != "" { st_set("vc_name", str_trim(nm)) }
    bind cy = cy + 28.0
    bind nv = u_dragval("vcp_w", x, cy, floor(w / 2.0) - 3.0, 22.0, st_get("vc_w", 512.0), 16.0, 8192.0, 1.0, "W " + fmt0(st_get("vc_w", 512.0)))
    if u_has(nv) { st_set("vc_w", round(nv)) }
    bind nv = u_dragval("vcp_h", x + floor(w / 2.0) + 3.0, cy, floor(w / 2.0) - 3.0, 22.0, st_get("vc_h", 512.0), 16.0, 8192.0, 1.0, "H " + fmt0(st_get("vc_h", 512.0)))
    if u_has(nv) { st_set("vc_h", round(nv)) }
    bind cy = cy + 28.0
    if u_check("vcp_bg", x, cy, "Transparent background", st_get("vc_bg", 16777215.0) < 0.0) { st_set("vc_bg", sel(st_get("vc_bg", 0.0) < 0.0, 16777215.0, 0.0 - 1.0)) }
    bind cy = cy + 28.0
    bind s = vc_selected()
    if len(s) < 0.5 {
        u_header(x, cy, w, "New shapes", ds_col("bind"))
        bind cy = cy + 20.0
        g_text(x, cy, 11.0, C_TEXT_DIM, "Fill")
        bind c = vc_color_block("vcp_nf", x, cy + 16.0, w, st_get("vc_fill", 0.0))
        if c > 0.0 - 1.5 { st_set("vc_fill", c) }
        bind cy = cy + 96.0
        g_text(x, cy, 11.0, C_TEXT_DIM, "Stroke")
        bind c = vc_color_block("vcp_ns", x, cy + 16.0, w, st_get("vc_stroke", 0.0))
        if c > 0.0 - 1.5 { st_set("vc_stroke", c) }
        bind cy = cy + 96.0
        g_text_wrap(x, cy, w, 11.0, C_TEXT_DIM, "Select an item to edit it. Pen: P, rectangle: R, ellipse: E, nodes: N.")
        return cy + 40.0
    }
    bind k = list_get(s, 0)
    u_header(x, cy, w, sel(len(s) > 1.0, num_str(len(s)) + " items", "Item"), ds_col("bind"))
    bind cy = cy + 20.0
    g_text(x, cy, 11.0, C_TEXT_DIM, "Fill")
    bind c = vc_color_block("vcp_f", x, cy + 16.0, w, st_get(vk(k, "fill"), 0.0 - 1.0))
    if c > 0.0 - 1.5 { vc_push() bind i = 0.0 while i < len(s) { st_set(vk(list_get(s, i), "fill"), c) bind i = i + 1.0 } if c >= 0.0 { st_set("vc_fill", c) } }
    bind cy = cy + 96.0
    g_text(x, cy, 11.0, C_TEXT_DIM, "Stroke")
    bind c = vc_color_block("vcp_s", x, cy + 16.0, w, st_get(vk(k, "stroke"), 0.0 - 1.0))
    if c > 0.0 - 1.5 { vc_push() bind i = 0.0 while i < len(s) { st_set(vk(list_get(s, i), "stroke"), c) bind i = i + 1.0 } if c >= 0.0 { st_set("vc_stroke", c) } }
    bind cy = cy + 96.0
    bind nv = u_param("vcp_sw", x, cy, w, "Stroke width", st_get(vk(k, "sw"), 1.0), 0.0, 64.0, C_BLUE, 1.0)
    if u_has(nv) { bind i = 0.0 while i < len(s) { st_set(vk(list_get(s, i), "sw"), nv) bind i = i + 1.0 } }
    bind cy = cy + 34.0
    bind nv = u_param("vcp_op", x, cy, w, "Opacity", st_get(vk(k, "op"), 1.0), 0.0, 1.0, C_GREEN, 2.0)
    if u_has(nv) { bind i = 0.0 while i < len(s) { st_set(vk(list_get(s, i), "op"), nv) bind i = i + 1.0 } }
    bind cy = cy + 34.0
    if u_check("vcp_closed", x, cy, "Closed path", st_get(vk(k, "closed"), 0.0) > 0.5) { vc_push() st_set(vk(k, "closed"), 1.0 - st_get(vk(k, "closed"), 0.0)) }
    bind cy = cy + 26.0
    u_header(x, cy, w, "Transform", ds_col("func"))
    bind cy = cy + 20.0
    g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Move")
    bind bw2 = floor((w - 46.0) / 2.0)
    bind nv = u_dragval("vcp_tx", x + 40.0, cy, bw2, 20.0, st_get(vk(k, "tx"), 0.0), 0.0 - 100000.0, 100000.0, 0.5, fmt1(st_get(vk(k, "tx"), 0.0)))
    if u_has(nv) { st_set(vk(k, "tx"), nv) }
    bind nv = u_dragval("vcp_ty", x + 46.0 + bw2, cy, bw2, 20.0, st_get(vk(k, "ty"), 0.0), 0.0 - 100000.0, 100000.0, 0.5, fmt1(st_get(vk(k, "ty"), 0.0)))
    if u_has(nv) { st_set(vk(k, "ty"), nv) }
    bind cy = cy + 24.0
    g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Scale")
    bind nv = u_dragval("vcp_sx", x + 40.0, cy, bw2, 20.0, st_get(vk(k, "sx"), 1.0), 0.0 - 1000.0, 1000.0, 0.005, fmt2(st_get(vk(k, "sx"), 1.0)))
    if u_has(nv) { st_set(vk(k, "sx"), nv) }
    bind nv = u_dragval("vcp_sy", x + 46.0 + bw2, cy, bw2, 20.0, st_get(vk(k, "sy"), 1.0), 0.0 - 1000.0, 1000.0, 0.005, fmt2(st_get(vk(k, "sy"), 1.0)))
    if u_has(nv) { st_set(vk(k, "sy"), nv) }
    bind cy = cy + 24.0
    g_text(x, cy + 4.0, 11.0, C_TEXT_DIM, "Rotate")
    bind nv = u_dragval("vcp_rot", x + 40.0, cy, w - 40.0, 20.0, mx_deg(st_get(vk(k, "rot"), 0.0)), 0.0 - 100000.0, 100000.0, 0.5, fmt1(mx_deg(st_get(vk(k, "rot"), 0.0))) + " deg")
    if u_has(nv) { st_set(vk(k, "rot"), mx_rad(nv)) }
    bind cy = cy + 28.0
    bind names = st_get("vc_layers", [])
    g_text(x, cy + 5.0, 11.0, C_TEXT_DIM, "Layer")
    bind nv = u_combo("vcp_layer", x + 40.0, cy, w - 40.0, 22.0, names, st_get(vk(k, "layer"), 0.0))
    if nv >= 0.0 { vc_push() bind i = 0.0 while i < len(s) { st_set(vk(list_get(s, i), "layer"), nv) bind i = i + 1.0 } }
    bind cy = cy + 30.0
    bind bw = floor((w - 6.0) / 2.0)
    bind labels = ["Group", "Ungroup", "Forward", "Backward", "Flip H", "Flip V", "Reverse", "Simplify", "Duplicate", "Delete", "Extrude 3D", "Lathe 3D"]
    bind acts = ["vec.group", "vec.ungroup", "vec.fwd", "vec.back", "vec.flip_h", "vec.flip_v", "vec.reverse", "vec.simplify", "edit.dup", "edit.delete", "vec.extrude", "vec.lathe"]
    bind i = 0.0
    while i < len(labels) {
        if u_button("vcp_b" + num_str(i), x + (i % 2.0) * (bw + 6.0), cy + floor(i / 2.0) * 26.0, bw, 22.0, list_get(labels, i), sel(list_get(acts, i) == "edit.delete", C_RED, 0.0)) { do_action(list_get(acts, i)) }
        bind i = i + 1.0
    }
    return cy + ceil(len(labels) / 2.0) * 26.0 + 10.0
}
fn vc_keys(shift, ctrl, alt) {
    if ctrl {
        if key_pressed("g") { if shift { vc_ungroup() } else { vc_group() } return true }
        if key_pressed("d") { vc_dup() return true }
        if key_pressed("a") { vc_select_all(true) return true }
        return false
    }
    if key_pressed("enter") || key_pressed("escape") { vc_pen_finish() if key_pressed("escape") { vc_select_all(false) } return true }
    if key_pressed("delete") || key_pressed("x") || key_pressed("backspace") { vc_delete() return true }
    if key_pressed("home") { st_set("vc_zoom", 0.0) return true }
    if key_pressed("pageup") { vc_reorder(1.0) return true }
    if key_pressed("pagedown") { vc_reorder(0.0 - 1.0) return true }
    if key_pressed("v") { vc_set_tool("select") return true }
    if key_pressed("n") { vc_set_tool("node") return true }
    if key_pressed("p") { vc_set_tool("pen") return true }
    if key_pressed("r") { vc_set_tool("rect") return true }
    if key_pressed("e") { vc_set_tool("ellipse") return true }
    if key_pressed("g") { vc_set_tool("polygon") return true }
    if key_pressed("s") && !shift { vc_set_tool("star") return true }
    if key_pressed("l") { vc_set_tool("line") return true }
    if key_pressed("h") { vc_set_tool("hand") return true }
    if key_pressed("a") && alt { vc_select_all(false) return true }
    if key_pressed("a") { vc_select_all(true) return true }
    if st_get("mode", 0.0) == 6.0 {
        if key_pressed("i") { if alt { an_unkey() } else { an_key() } return true }
        if key_pressed("k") { an_shapekey() return true }
        if key_pressed("comma") || key_pressed(",") { an_jump(0.0 - 1.0) return true }
        if key_pressed("period") || key_pressed(".") { an_jump(1.0) 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 }
    }
    # arrow keys nudge
    bind step = sel(shift, 10.0, 1.0)
    bind nx = 0.0 bind ny = 0.0
    if key_pressed("left") { bind nx = 0.0 - step }
    if key_pressed("right") { bind nx = step }
    if key_pressed("up") { bind ny = 0.0 - step }
    if key_pressed("down") { bind ny = step }
    if nx != 0.0 || ny != 0.0 {
        bind s = vc_selected()
        bind i = 0.0
        while i < len(s) { bind k = list_get(s, i) st_set(vk(k, "tx"), st_get(vk(k, "tx"), 0.0) + nx) st_set(vk(k, "ty"), st_get(vk(k, "ty"), 0.0) + ny) bind i = i + 1.0 }
        return true
    }
    return false
}

← Back to index