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/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
}