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/ds.ling
# ━━━ ds.ling — Ling design system toolkit (shared with LingStationX) ━━━
# Line work only (outline + crosshatch), square corners, tile lettering.
# Colour keys (what each colour means on a panel):
# asm red = sources / danger bind yellow = presets ctl green = time, animation
# call blue = in / out, selection func indigo = transforms mod violet = identity ink grey = structure
# Colours are lifted 22% toward white so they read on the black ground.
fn ds_col(key) {
if key == "asm" { return c_mix(c_rgb(229.0, 56.0, 59.0), 16777215.0, 0.22) }
if key == "lit" { return c_mix(c_rgb(247.0, 127.0, 0.0), 16777215.0, 0.22) }
if key == "bind" { return c_mix(c_rgb(209.0, 155.0, 0.0), 16777215.0, 0.22) }
if key == "ctl" { return c_mix(c_rgb(42.0, 157.0, 73.0), 16777215.0, 0.22) }
if key == "call" { return c_mix(c_rgb(31.0, 111.0, 214.0), 16777215.0, 0.22) }
if key == "func" { return c_mix(c_rgb(75.0, 63.0, 196.0), 16777215.0, 0.22) }
if key == "mod" { return c_mix(c_rgb(155.0, 63.0, 201.0), 16777215.0, 0.22) }
return c_mix(c_rgb(86.0, 101.0, 122.0), 16777215.0, 0.22)
}
# clip to a rectangle without losing the surrounding scroll clip
fn ds_clip_to(x, y, w, h) {
bind x0 = x
bind y0 = y
bind x1 = x + w
bind y1 = y + h
if st_get("clip_on", 0.0) > 0.5 {
bind x0 = max(x0, st_get("clip_x0", 0.0))
bind y0 = max(y0, st_get("clip_y0", 0.0))
bind x1 = min(x1, st_get("clip_x1", 100000.0))
bind y1 = min(y1, st_get("clip_y1", 100000.0))
}
px_clip(x0, y0, max(0.0, x1 - x0), max(0.0, y1 - y0))
}
fn ds_clip_end() {
if st_get("clip_on", 0.0) > 0.5 {
px_clip(st_get("clip_x0", 0.0), st_get("clip_y0", 0.0), st_get("clip_x1", 0.0) - st_get("clip_x0", 0.0), st_get("clip_y1", 0.0) - st_get("clip_y0", 0.0))
} else { px_unclip() }
}
# parallel hatch lines at `ang` degrees (45 = rising to the right), `sp` px apart, clipped to the box
fn ds_hatch(x, y, w, h, col, ang, sp, a) {
if w < 1.0 || h < 1.0 { return 0.0 }
if sp < 1.0 { bind sp = 1.0 }
bind r = ang * 3.14159265 / 180.0
bind dx = cos(r)
bind dy = 0.0 - sin(r)
bind nx = sin(r)
bind ny = cos(r)
bind p0 = x * nx + y * ny
bind p1 = (x + w) * nx + y * ny
bind p2 = x * nx + (y + h) * ny
bind p3 = (x + w) * nx + (y + h) * ny
bind lo = min(min(p0, p1), min(p2, p3))
bind hi = max(max(p0, p1), max(p2, p3))
bind ln = w + h
bind s0 = (x + w * 0.5) * dx + (y + h * 0.5) * dy
ds_clip_to(x, y, w, h)
bind t = lo + sp * 0.5
while t < hi {
px_line(nx * t + dx * (s0 - ln), ny * t + dy * (s0 - ln), nx * t + dx * (s0 + ln), ny * t + dy * (s0 + ln), col, a)
bind t = t + sp
}
ds_clip_end()
}
# crosshatch: 45 and -45
fn ds_cross(x, y, w, h, col, sp, a) {
ds_hatch(x, y, w, h, col, 45.0, sp, a)
ds_hatch(x, y, w, h, col, 135.0, sp, a)
}
# corner brackets
fn ds_brackets(x, y, w, h, n, col) {
px_hline(x, y, n, col)
px_vline(x, y, n, col)
px_hline(x + w - n, y, n, col)
px_vline(x + w - 1.0, y, n, col)
px_hline(x, y + h - 1.0, n, col)
px_vline(x, y + h - n, n, col)
px_hline(x + w - n, y + h - 1.0, n, col)
px_vline(x + w - 1.0, y + h - n, n, col)
}
# one letter in a square tile (glyph fitted to 60% of the tile)
fn ds_tile(x, y, s, ch, col, on) {
px_outline(x, y, s, s, col, 0.7)
ds_brackets(x, y, s, s, max(2.0, floor(s * 0.28)), col)
bind fs = s * 0.8
bind gw = g_text_w(fs, ch)
px_text(st_get("font", 0.0), x + (s - gw) * 0.5, y + (s - fs) * 0.5 - 1.0, fs, sel(on, 0.0, col), ch)
}
fn ds_tile_fill(x, y, s, col) {
ds_cross(x + 1.0, y + 1.0, s - 2.0, s - 2.0, col, 3.0, 0.55)
}
# a word, one letter per tile; `on` = hatched. Returns the width.
fn ds_word(x, y, s, text, col, on) {
bind n = str_len(text)
bind i = 0.0
while i < n {
bind tx = x + i * (s + 1.0)
if on { ds_hatch(tx + 1.0, y + 1.0, s - 2.0, s - 2.0, col, 45.0, 3.0, 0.7) }
ds_tile(tx, y, s, str_slice(text, i, 1.0), col, false)
bind i = i + 1.0
}
return n * (s + 1.0)
}
fn ds_word_w(s, text) { return str_len(text) * (s + 1.0) }
# double-rule frame with corner brackets and a tabbed title
fn ds_frame(x, y, w, h, col, title) {
px_outline(x, y, w, h, col, 0.55)
px_outline(x + 3.0, y + 3.0, w - 6.0, h - 6.0, col, 0.35)
ds_brackets(x - 1.0, y - 1.0, w + 2.0, h + 2.0, 10.0, col)
bind tw = ds_word_w(14.0, title) + 10.0
px_rect(x + 14.0, y - 8.0, tw, 16.0, 0.0)
ds_word(x + 19.0, y - 7.0, 14.0, title, col, false)
}
# ── controls ─────────────────────────────────────────────────────────────────
# selector: a row of tile words; returns the new index or -1
fn ds_selector(id, x, y, s, names, cur, col, tip) {
bind out = 0.0 - 1.0
bind px = x
bind i = 0.0
while i < len(names) {
bind nm = list_get(names, i)
bind ww = ds_word_w(s, nm)
bind on = i == cur
ds_word(px, y, s, nm, sel(on, col, ds_col("ink")), on)
if u_hot(px, y, ww, s) {
u_tip(tip + ": " + nm)
if u_pressed() { bind out = i }
}
bind px = px + ww + 7.0
bind i = i + 1.0
}
return out
}
fn ds_selector_w(s, names) {
bind w = 0.0
bind i = 0.0
while i < len(names) {
bind w = w + ds_word_w(s, list_get(names, i)) + 7.0
bind i = i + 1.0
}
return w
}
# switch: a tile, crosshatched when on, with a tile-word label. Returns the new state (0/1) or -1.
fn ds_switch(id, x, y, s, label, on, col, tip) {
bind out = 0.0 - 1.0
px_outline(x, y, s, s, sel(on, col, ds_col("ink")), 0.8)
ds_brackets(x, y, s, s, 4.0, sel(on, col, ds_col("ink")))
if on { ds_cross(x + 1.0, y + 1.0, s - 2.0, s - 2.0, col, 3.0, 0.7) }
ds_word(x + s + 5.0, y + (s - 12.0) * 0.5, 12.0, label, sel(on, col, ds_col("ink")), false)
bind w = s + 8.0 + ds_word_w(12.0, label)
if u_hot(x, y, w, s) {
u_tip(tip + sel(on, " (on)", " (off)"))
if u_pressed() { bind out = sel(on, 0.0, 1.0) }
}
return out
}
# knob: 270 degree arc, swept part crosshatched, the rest single-hatched, hatched tapered hand, pin.
# Drag vertically (Shift = fine), wheel works too. Returns the new 0..1 value or -999.
fn ds_knob(id, cx, cy, r, label, v, col, readout, tip) {
bind result = 0.0 - 999.0
bind rin = r * 0.58
bind hot = u_hot(cx - r - 3.0, cy - r - 3.0, (r + 3.0) * 2.0, (r + 3.0) * 2.0 + 14.0)
bind dragging = u_active() == id
if hot && u_pressed() {
u_set_active(id)
st_set("kn_y", u_my())
st_set("kn_v", v)
bind dragging = true
}
if dragging {
if u_down() {
bind sens = sel(u_shift(), 600.0, 150.0)
bind nv = clampf(st_get("kn_v", v) + (st_get("kn_y", 0.0) - u_my()) / sens, 0.0, 1.0)
if abs(nv - v) > 0.0005 { bind result = nv }
} else { u_set_active("") bind dragging = false }
} else if hot && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
bind result = clampf(v + u_scroll() * sel(u_shift(), 0.004, 0.02), 0.0, 1.0)
st_set("wheel_used", 1.0)
}
if hot { u_tip(tip) }
bind shown = sel(result > -500.0, result, v)
bind a0 = 135.0
bind sw = 270.0 * shown
bind k = 0.0
while k < 270.0 {
bind ang = (a0 + k) * 3.14159265 / 180.0
bind ang2 = (a0 + k + 15.0) * 3.14159265 / 180.0
bind swept = k < sw
px_line(cx + cos(ang) * r, cy + sin(ang) * r, cx + cos(ang2) * r, cy + sin(ang2) * r, col, sel(swept, 0.95, 0.4))
px_line(cx + cos(ang) * rin, cy + sin(ang) * rin, cx + cos(ang2) * rin, cy + sin(ang2) * rin, col, sel(swept, 0.8, 0.3))
bind k = k + 15.0
}
bind k = 0.0
while k <= 270.0 {
bind ang = (a0 + k) * 3.14159265 / 180.0
bind swept = k <= sw
if swept {
px_line(cx + cos(ang) * rin, cy + sin(ang) * rin, cx + cos(ang) * r, cy + sin(ang) * r, col, 0.7)
} else if k % 20.0 < 0.5 {
px_line(cx + cos(ang) * rin, cy + sin(ang) * rin, cx + cos(ang) * r, cy + sin(ang) * r, col, 0.3)
}
bind k = k + 10.0
}
bind ha = (a0 + sw) * 3.14159265 / 180.0
bind hx = cos(ha)
bind hy = sin(ha)
bind tipx = cx + hx * (rin - 1.0)
bind tipy = cy + hy * (rin - 1.0)
px_line(cx - hy * 2.5, cy + hx * 2.5, tipx, tipy, col, 0.9)
px_line(cx + hy * 2.5, cy - hx * 2.5, tipx, tipy, col, 0.9)
px_line(cx, cy, tipx, tipy, col, 0.9)
px_ring(cx, cy, 2.5, col, 1.0)
bind txt = sel(dragging, readout, label)
bind ts = 9.0
bind lw = ds_word_w(ts, txt)
ds_word(cx - lw * 0.5, cy + r + 4.0, ts, txt, sel(dragging, c_mix(col, 16777215.0, 0.35), col), dragging)
return result
}
# vertical fader: hatched fill up to the value. Returns new 0..1 or -999.
fn ds_fader(id, x, y, w, h, label, v, col, spring, tip) {
bind result = 0.0 - 999.0
bind hot = u_hot(x, y, w, h)
if hot { u_tip(tip) }
if hot && u_pressed() { u_set_active(id) }
if u_active() == id {
if u_down() {
bind result = clampf(1.0 - (u_my() - y) / h, 0.0, 1.0)
} else {
u_set_active("")
if spring >= 0.0 { bind result = spring }
}
}
bind shown = sel(result > -500.0, result, v)
px_outline(x, y, w, h, col, 0.7)
px_outline(x + 2.0, y + 2.0, w - 4.0, h - 4.0, col, 0.3)
bind fh = (h - 4.0) * shown
ds_cross(x + 3.0, y + h - 3.0 - fh, w - 6.0, fh, col, 3.0, 0.6)
px_hline(x - 2.0, y + h - 3.0 - fh, w + 4.0, col, 1.0)
bind lw = ds_word_w(9.0, label)
ds_word(x + (w - lw) * 0.5, y + h + 4.0, 9.0, label, col, false)
return result
}