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/modals.ling
# ━━━ modals.ling — floating windows: prompts, settings, about, help, parameter dialogs ━━━
fn modal_open(name) {
st_set("modal", name)
st_set("modal_t0", st_get("now", 0.0))
st_set("modal_tab", 0.0)
st_set("focus", "")
}
fn modal_close() {
st_set("modal", "")
st_set("focus", "")
}
# open the text prompt: kind picks what happens on OK (see text_commit in actions.ling)
fn modal_text(title, kind, initial) {
st_set("text_title", title)
st_set("text_kind", kind)
modal_open("text")
st_set("text_val", initial)
p_text_set(initial)
st_set("focus", "text_modal")
}
# window chrome; leaves content origin in wx, wy
fn win_begin(title, w, h) {
bind W = get_width()
bind H = get_height()
bind x = floor((W - w) * 0.5)
bind y = floor((H - h) * 0.5) - 20.0
g_rect_a(0.0, 0.0, W, H, 0, 0.45)
px_rect(x + 3.0, y + 5.0, w, h, 0, 0.5)
px_rect(x, y, w, h, C_BG_PANEL)
px_outline(x, y, w, h, C_POPUP_LINE)
ds_brackets(x - 1.0, y - 1.0, w + 2.0, h + 2.0, 10.0, ds_col("call"))
px_rect(x, y, w, 26.0, 2434344.0)
ds_hatch(x + 1.0, y + 1.0, w - 2.0, 24.0, ds_col("call"), 45.0, 7.0, 0.18)
g_text(x + 12.0, y + 6.0, 13.0, C_TEXT_STRONG, title)
bind hot = u_hot(x + w - 26.0, y + 3.0, 20.0, 20.0)
if hot { px_rect(x + w - 26.0, y + 3.0, 20.0, 20.0, C_RED) }
icon_draw("x", x + w - 16.0, y + 13.0, 10.0, C_TEXT_STRONG)
if hot && u_pressed() { modal_close() }
u_hsep(x, y + 26.0, w)
st_set("wx", x)
st_set("wy", y + 34.0)
st_set("ww", w)
st_set("wh", h - 34.0)
}
fn modal_overlay() {
bind m = st_get("modal", "")
if m == "" { return 0.0 }
st_set("blk", sel(st_get("cb_open", "") != "" || st_get("ctx_open", 0.0) > 0.5, 1.0, 0.0))
if m == "text" { modal_text_win() }
else if m == "confirm" { modal_confirm() }
else if m == "about" { modal_about() }
else if m == "help" { modal_help() }
else if m == "shortcuts" { modal_shortcuts() }
else if m == "formats" { modal_formats() }
else if m == "settings" { modal_settings() }
else if m == "params" { modal_params() }
else if m == "autoweights" { modal_autoweights() }
else if m == "vecnew" { modal_vecnew() }
else if m == "sheet" { modal_sheet() }
else if m == "turntable" { modal_turntable() }
if key_pressed("escape") { modal_close() }
}
# generic yes/cancel prompt; runs do_action(confirm_act) on Yes
fn confirm_open(title, message, act) {
st_set("confirm_title", title)
st_set("confirm_msg", message)
st_set("confirm_act", act)
modal_open("confirm")
}
fn modal_confirm() {
win_begin(st_get("confirm_title", "Confirm"), 420.0, 136.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
g_text_wrap(x + 16.0, y + 10.0, 388.0, 13.0, C_TEXT, st_get("confirm_msg", ""))
if u_button("cf_yes", x + 16.0, y + 62.0, 110.0, 26.0, "Yes", C_RED) || key_pressed("enter") {
bind a = st_get("confirm_act", "")
modal_close()
if a != "" { do_action(a) }
}
if u_button("cf_no", x + 136.0, y + 62.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}
fn modal_text_win() {
win_begin(st_get("text_title", "Name"), 420.0, 140.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind v = u_textfield("text_modal", x + 16.0, y + 14.0, 388.0, 26.0, st_get("text_val", ""))
st_set("text_val", v)
if st_get("focus", "") == "" && !u_down() { st_set("focus", "text_modal") p_text_set(v) }
if u_button("tx_ok", x + 16.0, y + 62.0, 110.0, 26.0, "OK", C_BLUE) || key_pressed("enter") {
bind kind = st_get("text_kind", "")
modal_close()
text_commit(kind, v)
}
if u_button("tx_cancel", x + 136.0, y + 62.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}
fn modal_about() {
win_begin("About LingModeler", 520.0, 300.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
ds_word(x + 20.0, y + 12.0, 22.0, "LINGMODELER", ds_col("call"), false)
g_text(x + 20.0, y + 46.0, 13.0, C_TEXT, "A lightweight vector 3D modeler, 2D illustration + animation studio,")
g_text(x + 20.0, y + 64.0, 13.0, C_TEXT, "procedural texture lab and character rigging tool - written in Ling.")
g_text(x + 20.0, y + 94.0, 12.0, C_TEXT_DIM, "Formats: .lmesh / .ling (native, valid Ling source), OBJ, glTF 2.0, SVG, PNG")
g_text(x + 20.0, y + 112.0, 12.0, C_TEXT_DIM, "Runs on Windows, macOS, Linux and inside LingOS (x86-64 and Raspberry Pi 5).")
g_text(x + 20.0, y + 130.0, 12.0, C_TEXT_DIM, "Skinned like LingStationX: line work, crosshatch, tile lettering.")
g_text(x + 20.0, y + 160.0, 12.0, C_TEXT_DIM, "Version " + APP_VERSION + " - https://ling-lang.org")
bind k = 0.0
while k < 7.0 { px_rect(x + 20.0 + k * 26.0, y + 190.0, 22.0, 8.0, c_pal(k)) bind k = k + 1.0 }
if u_button("ab_ok", x + 20.0, y + 216.0, 110.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_help() {
win_begin("Help", 560.0, 360.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = ["Model: add primitives from Edit > Add Primitive, Tab toggles Edit mode. Orbit with the middle mouse", "(or Alt + left drag), pan with Shift + middle drag, wheel zooms. Numpad 1/3/7 are front/right/top.", "Edit mode: 1/2/3 pick vertex/edge/face selection, click or box-drag to select, Shift adds.", "G/R/S move/rotate/scale (X/Y/Z lock an axis, Shift fine, click confirms, Esc cancels).", "E extrude, I inset, Ctrl+B bevel, Ctrl+R loop cut, F fill, M merge, X delete, Ctrl+2 subsurf.", "Rig: Shift+A adds a bone at the 3D cursor; E extrudes a child bone; Ctrl+P parents. Bind the", "mesh (Rig menu), then Auto Weights with an adjustable falloff and smoothing; paint in Weights.", "Pose: select bones, R/G/S pose them, I inserts keyframes on the timeline, Space plays.", "Vector: pen (P) draws bezier paths, click = corner, drag = curve; N edits nodes; shapes R/E/G/S.", "Anim: keyframe position/rotation/scale/opacity with I, shape keys with K, export frames or SVG.", "Texture: pick a procedural preset, tune its sliders, Apply to the selected object or Export."]
bind i = 0.0
while i < len(lines) { g_text(x + 16.0, y + 10.0 + i * 20.0, 12.0, C_TEXT, list_get(lines, i)) bind i = i + 1.0 }
if u_button("hp_ok", x + 16.0, y + 250.0, 110.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_shortcuts() {
win_begin("Shortcuts", 620.0, 420.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind left = ["Tab object / edit", "1 2 3 vertex / edge / face", "A select all Alt+A none", "B box select L linked", "G R S move / rotate / scale", "X Y Z axis lock (during G/R/S)", "E extrude I inset", "Ctrl+B bevel Ctrl+R loop cut", "F fill M merge J connect", "X / Del delete Shift+D duplicate", "Ctrl+2 subsurf Ctrl+T triangulate", "Shift+N recalc normals H hide"]
bind right = ["Numpad 1/3/7 front / right / top", "Numpad 5 persp / ortho Home frame all", "Z wireframe Alt+Z x-ray", "Ctrl+Z / Ctrl+Y undo / redo", "Ctrl+S / Ctrl+O save / open", "F1..F7 modes", "Rig: Shift+A add bone, Ctrl+P parent", "Weights: [ ] brush size, Ctrl subtract", "Pose: I key, Alt+I unkey, Space play", "Vector: V N P R E G S L tools", "Anim: I key, K shape key, , . prev/next key", "Texture: R new seed"]
bind i = 0.0
while i < len(left) {
g_text(x + 16.0, y + 10.0 + i * 22.0, 12.0, C_TEXT, list_get(left, i))
g_text(x + 316.0, y + 10.0 + i * 22.0, 12.0, C_TEXT, list_get(right, i))
bind i = i + 1.0
}
if u_button("sc_ok", x + 16.0, y + 300.0, 110.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_formats() {
win_begin("File formats", 620.0, 380.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = [".lmesh - native scene: objects, polygon faces (tris/quads/ngons), UVs, materials, procedural", " textures, armatures, skin weights and actions. It is valid Ling source: `use` it and call", " draw_<name>() from any Ling program (mesh data is exposed as flat lists).", ".ling - native 2D vector document: layers, bezier paths, fills, strokes, keyframes and shape keys.", " Also valid Ling source with a draw_<name>(ox, oy, scale) function. SVG round-trips through it.", ".obj - Wavefront import/export with .mtl colours and texture maps; n-gons and UVs preserved.", ".gltf - glTF 2.0 import (embedded buffers) and export with materials, textures, skins + animations.", ".glb - glTF binary export.", ".svg - import paths/shapes/transforms; export layers, paths and SMIL animation.", ".png - textures, sprite sheets, turntable sheets, rendered animation frames."]
bind i = 0.0
while i < len(lines) { g_text(x + 16.0, y + 10.0 + i * 22.0, 12.0, C_TEXT, list_get(lines, i)) bind i = i + 1.0 }
if u_button("fm_ok", x + 16.0, y + 270.0, 110.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_settings() {
win_begin("Settings", 460.0, 330.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind cy = y + 12.0
if u_check("se_black", x + 16.0, cy, "Black colour scheme", st_get("theme_black", 1.0) > 0.5) { st_set("theme_black", 1.0 - st_get("theme_black", 1.0)) px_theme(st_get("theme_black", 1.0)) }
bind cy = cy + 26.0
if u_check("se_load", x + 16.0, cy, "Open the last scene at startup", st_get("load_last", 0.0) > 0.5) { st_set("load_last", 1.0 - st_get("load_last", 0.0)) }
bind cy = cy + 26.0
if u_check("se_reduce", x + 16.0, cy, "Reduce motion (no hatch animation)", st_get("reduce_motion", 0.0) > 0.5) { st_set("reduce_motion", 1.0 - st_get("reduce_motion", 0.0)) }
bind cy = cy + 26.0
if u_check("se_auto", x + 16.0, cy, "Auto-normalize weights while painting", st_get("wp_autonorm", 1.0) > 0.5) { st_set("wp_autonorm", 1.0 - st_get("wp_autonorm", 1.0)) }
bind cy = cy + 30.0
bind nv = u_param("se_autosave", x + 16.0, cy, 300.0, "Autosave every (minutes, 0 = off)", st_get("autosave", 5.0), 0.0, 30.0, C_GREEN, 0.0)
if u_has(nv) { st_set("autosave", round(nv)) }
bind cy = cy + 36.0
bind nv = u_param("se_sens", x + 16.0, cy, 300.0, "Orbit sensitivity", st_get("orbit_sens", 1.0), 0.2, 3.0, C_BLUE, 1.0)
if u_has(nv) { st_set("orbit_sens", nv) }
bind cy = cy + 36.0
bind nv = u_param("se_fov", x + 16.0, cy, 300.0, "Field of view (degrees)", st_get("cam_fov", 50.0), 20.0, 100.0, C_BLUE, 0.0)
if u_has(nv) { st_set("cam_fov", round(nv)) }
bind cy = cy + 40.0
if u_button("se_ok", x + 16.0, cy, 110.0, 26.0, "Save", C_BLUE) { settings_save() modal_close() }
if u_button("se_cancel", x + 136.0, cy, 110.0, 26.0, "Close", 0.0) { modal_close() }
}
# ── generic parameter dialog: a list of sliders + Apply, used by primitives / generators / ops ──
# params_open(title, kind, names, values, mins, maxs, decimals)
fn params_open(title, kind, names, vals, mins, maxs, decs) {
st_set("pm_title", title)
st_set("pm_kind", kind)
st_set("pm_names", names)
st_set("pm_vals", vals)
st_set("pm_mins", mins)
st_set("pm_maxs", maxs)
st_set("pm_decs", decs)
modal_open("params")
}
fn pm_val(i) { return list_get(st_get("pm_vals", []), i) }
fn modal_params() {
bind names = st_get("pm_names", [])
bind n = len(names)
bind h = 110.0 + n * 36.0
win_begin(st_get("pm_title", "Parameters"), 420.0, h)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind vals = st_get("pm_vals", [])
bind mins = st_get("pm_mins", [])
bind maxs = st_get("pm_maxs", [])
bind decs = st_get("pm_decs", [])
bind i = 0.0
while i < n {
bind nv = u_param("pm" + num_str(i), x + 16.0, y + 10.0 + i * 36.0, 388.0, list_get(names, i), list_get(vals, i), list_get(mins, i), list_get(maxs, i), c_pal(i + 3.0), list_get(decs, i))
if u_has(nv) {
bind vals = list_set(vals, i, sel(list_get(decs, i) < 0.5, round(nv), nv))
st_set("pm_vals", vals)
if st_get("pm_live", 0.0) > 0.5 { params_apply(st_get("pm_kind", ""), true) }
}
bind i = i + 1.0
}
bind by = y + 16.0 + n * 36.0
if u_button("pm_ok", x + 16.0, by, 110.0, 26.0, "Apply", C_BLUE) || key_pressed("enter") {
params_apply(st_get("pm_kind", ""), false)
modal_close()
}
if u_button("pm_cancel", x + 136.0, by, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}
# ── auto weights dialog (adjustable, re-runnable) ───────────────────────────
fn modal_autoweights() {
win_begin("Auto Weights", 460.0, 352.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
g_text_wrap(x + 16.0, y + 6.0, 428.0, 11.0, C_TEXT_DIM, "Weights fall off with the distance from each bone (power = sharpness). Envelope widens the zone that gets full weight; smoothing blurs weights across connected vertices. Apply as often as you like, then refine in Weights mode.")
bind cy = y + 56.0
bind nv = u_param("aw_pow", x + 16.0, cy, 428.0, "Falloff power", st_get("aw_pow", 4.0), 1.0, 12.0, C_ORANGE, 1.0)
if u_has(nv) { st_set("aw_pow", nv) }
bind cy = cy + 36.0
bind nv = u_param("aw_env", x + 16.0, cy, 428.0, "Envelope (x bone length)", st_get("aw_env", 0.15), 0.0, 1.0, C_YELLOW, 2.0)
if u_has(nv) { st_set("aw_env", nv) }
bind cy = cy + 36.0
bind nv = u_param("aw_max", x + 16.0, cy, 428.0, "Max influences per vertex", st_get("aw_max", 4.0), 1.0, 4.0, C_GREEN, 0.0)
if u_has(nv) { st_set("aw_max", round(nv)) }
bind cy = cy + 36.0
bind nv = u_param("aw_smooth", x + 16.0, cy, 428.0, "Smoothing iterations", st_get("aw_smooth", 2.0), 0.0, 10.0, C_BLUE, 0.0)
if u_has(nv) { st_set("aw_smooth", round(nv)) }
bind cy = cy + 36.0
bind nv = u_param("aw_cut", x + 16.0, cy, 428.0, "Minimum weight kept", st_get("aw_cut", 0.02), 0.0, 0.3, C_INDIGO, 2.0)
if u_has(nv) { st_set("aw_cut", nv) }
bind cy = cy + 32.0
if u_check("aw_selonly", x + 16.0, cy, "Only selected bones", st_get("aw_selonly", 0.0) > 0.5) { st_set("aw_selonly", 1.0 - st_get("aw_selonly", 0.0)) }
if u_check("aw_heat", x + 220.0, cy, "Surface-aware (slower)", st_get("aw_heat", 1.0) > 0.5) { st_set("aw_heat", 1.0 - st_get("aw_heat", 1.0)) }
bind cy = cy + 28.0
if u_button("aw_ok", x + 16.0, cy, 130.0, 26.0, "Apply weights", C_BLUE) || key_pressed("enter") {
rg_auto_weights()
st_set("status", "Auto weights applied - adjust and apply again, or paint in Weights mode")
}
if u_button("aw_close", x + 156.0, cy, 110.0, 26.0, "Close", 0.0) { modal_close() }
}
# ── new vector document ─────────────────────────────────────────────────────
fn modal_vecnew() {
win_begin("New vector document", 420.0, 200.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind nv = u_param("vn_w", x + 16.0, y + 8.0, 388.0, "Width (px)", st_get("vn_w", 512.0), 16.0, 4096.0, C_BLUE, 0.0)
if u_has(nv) { st_set("vn_w", round(nv)) }
bind nv = u_param("vn_h", x + 16.0, y + 44.0, 388.0, "Height (px)", st_get("vn_h", 512.0), 16.0, 4096.0, C_BLUE, 0.0)
if u_has(nv) { st_set("vn_h", round(nv)) }
bind nv = u_param("vn_fps", x + 16.0, y + 80.0, 388.0, "Frames per second", st_get("vn_fps", 24.0), 1.0, 60.0, C_GREEN, 0.0)
if u_has(nv) { st_set("vn_fps", round(nv)) }
if u_button("vn_ok", x + 16.0, y + 124.0, 110.0, 26.0, "Create", C_BLUE) || key_pressed("enter") {
vc_new_doc(st_get("vn_w", 512.0), st_get("vn_h", 512.0), st_get("vn_fps", 24.0))
modal_close()
}
if u_button("vn_cancel", x + 136.0, y + 124.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}
# ── sprite sheet / frame export (2D animation + texture animation) ───────────
fn modal_sheet() {
win_begin(st_get("sh_title", "Export sprite sheet"), 440.0, 260.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind nv = u_param("sh_frames", x + 16.0, y + 8.0, 408.0, "Frames", st_get("sh_frames", 8.0), 1.0, 64.0, C_GREEN, 0.0)
if u_has(nv) { st_set("sh_frames", round(nv)) }
bind nv = u_param("sh_size", x + 16.0, y + 44.0, 408.0, "Frame size (px)", st_get("sh_size", 128.0), 16.0, 512.0, C_BLUE, 0.0)
if u_has(nv) { st_set("sh_size", round(nv)) }
bind nv = u_param("sh_cols", x + 16.0, y + 80.0, 408.0, "Columns (0 = one row)", st_get("sh_cols", 0.0), 0.0, 16.0, C_YELLOW, 0.0)
if u_has(nv) { st_set("sh_cols", round(nv)) }
bind nv = u_param("sh_cycle", x + 16.0, y + 116.0, 408.0, "Cycle length (seconds)", st_get("sh_cycle", 1.0), 0.1, 10.0, C_ORANGE, 1.0)
if u_has(nv) { st_set("sh_cycle", nv) }
if !p_has_images() { g_text(x + 16.0, y + 152.0, 11.0, C_RED, "Image export is not available on this platform.") }
if u_button("sh_ok", x + 16.0, y + 180.0, 120.0, 26.0, "Export...", C_BLUE) {
bind kind = st_get("sh_kind", "")
modal_close()
sheet_export(kind)
}
if u_button("sh_cancel", x + 146.0, y + 180.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}
# ── turntable render ────────────────────────────────────────────────────────
fn modal_turntable() {
win_begin("Turntable sprite sheet", 440.0, 230.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
g_text(x + 16.0, y + 6.0, 11.0, C_TEXT_DIM, "Renders the scene from N angles around Y into one PNG (transparent background).")
bind nv = u_param("tt_n", x + 16.0, y + 28.0, 408.0, "Angles", st_get("tt_n", 8.0), 1.0, 32.0, C_GREEN, 0.0)
if u_has(nv) { st_set("tt_n", round(nv)) }
bind nv = u_param("tt_size", x + 16.0, y + 64.0, 408.0, "Frame size (px)", st_get("tt_size", 128.0), 32.0, 512.0, C_BLUE, 0.0)
if u_has(nv) { st_set("tt_size", round(nv)) }
bind nv = u_param("tt_pitch", x + 16.0, y + 100.0, 408.0, "Camera pitch (degrees)", st_get("tt_pitch", 25.0), 0.0 - 80.0, 80.0, C_YELLOW, 0.0)
if u_has(nv) { st_set("tt_pitch", round(nv)) }
if u_button("tt_ok", x + 16.0, y + 150.0, 120.0, 26.0, "Render...", C_BLUE) { modal_close() ra_turntable() }
if u_button("tt_cancel", x + 146.0, y + 150.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
}