灵符lingfu

lingmodeler

↓ 0 下载

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
关键词 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodeler
许可证Apache-2.0 OR MIT
作者ling-lin
README代码 / 文件版本依赖关系图依赖此包的项目安全构建设置

代码 / 文件 (67)

ling/shell.ling
# ━━━ shell.ling — menu bar, mode-tab row, tool strip, status bar ━━━

# ── menus ────────────────────────────────────────────────────────────────────
# item format:  "Label|action|Shortcut|icon"   ("-" = separator, label ending in " >" opens a submenu)
fn menu_titles() { return ["File", "Edit", "Select", "Mesh", "Rig", "Vector", "Texture", "View", "Help"] }
fn menu_color(i) {
    if i < 0.5 { return C_MENU_FILE }
    if i < 1.5 { return C_MENU_EDIT }
    if i < 2.5 { return C_MENU_VIEW }
    if i < 3.5 { return C_MENU_TRANS }
    if i < 4.5 { return C_VIOLET }
    if i < 5.5 { return C_BLUE }
    if i < 6.5 { return C_INDIGO }
    if i < 7.5 { return C_MENU_VIEW }
    return C_MENU_HELP
}

fn menu_def(m) {
    if m == 0.0 {
        return ["New Scene|file.new|Ctrl+N|file", "Open...|file.open|Ctrl+O|folder", "Open Recent >|sub:recent||refresh", "Save|file.save|Ctrl+S|save", "Save As (.lmesh)...|file.save_as|Ctrl+Shift+S|save", "-", "Import >|sub:import||import", "Export >|sub:export||export", "-", "Settings...|file.settings|Ctrl+P|gear", "Quit|file.quit|Ctrl+Q|x"]
    }
    if m == 1.0 {
        return ["Undo|edit.undo|Ctrl+Z|undo", "Redo|edit.redo|Ctrl+Y|redo", "-", "Duplicate|edit.dup|Shift+D|copy", "Delete|edit.delete|X / Del|trash", "-", "Add Primitive >|sub:add||cube", "Generate Asset >|sub:gen||gen", "-", "Object Mode / Edit Mode|edit.toggle|Tab|cube"]
    }
    if m == 2.0 {
        return ["Select All|sel.all|A|select", "Select None|sel.none|Alt+A|select", "Invert|sel.invert|Ctrl+I|select", "-", "Vertices|sel.vert|1|vertex", "Edges|sel.edge|2|edge", "Faces|sel.face|3|face", "-", "Grow Selection|sel.grow|Ctrl++|plus", "Shrink Selection|sel.shrink|Ctrl+-|minus", "Select Linked|sel.linked|L|link", "Edge Loop|sel.loop|Alt+Click|loopcut", "Select by Material|sel.mat||texture"]
    }
    if m == 3.0 {
        return ["Extrude|mesh.extrude|E|extrude", "Inset Faces|mesh.inset|I|inset", "Bevel|mesh.bevel|Ctrl+B|bevel", "Loop Cut|mesh.loopcut|Ctrl+R|loopcut", "Subdivide|mesh.subdivide||subdiv", "Smooth Subdivide (Catmull-Clark)|mesh.subsurf|Ctrl+2|subdiv", "-", "Merge Vertices|mesh.merge|M|merge", "Merge by Distance|mesh.merge_dist||merge", "Dissolve|mesh.dissolve|Ctrl+X|x", "Fill / Make Face|mesh.fill|F|face", "Connect Vertices|mesh.connect|J|edge", "Split Off|mesh.split|Y|knife", "Separate Selection|mesh.separate|P|layers", "Join Objects|mesh.join|Ctrl+J|link", "-", "Flip Normals|mesh.flip||refresh", "Recalculate Normals|mesh.recalc|Shift+N|refresh", "Triangulate|mesh.tri|Ctrl+T|face", "Tris to Quads|mesh.quads|Alt+J|face", "Smooth Vertices|mesh.smooth||sphere", "Mirror X (apply)|mesh.mirror_x||mirror", "Symmetrize X|mesh.symm||mirror", "Spin / Lathe Selection|mesh.spin||spin", "-", "UV >|sub:uv||texture", "Shade Smooth / Flat|mesh.shade||sphere", "Apply Transform|mesh.apply_xf||move", "Center Origin|mesh.center||target"]
    }
    if m == 4.0 {
        return ["Add Bone|rig.add|Shift+A|bone", "Extrude Bone|rig.extrude|E|bone", "Parent to Active|rig.parent|Ctrl+P|link", "Clear Parent|rig.unparent|Alt+P|x", "Delete Bone|rig.delete|X|trash", "Symmetrize Bones (L/R)|rig.symm||mirror", "Rename Bone...|rig.rename|F2|edit", "-", "Bind Mesh to Armature|rig.bind||link", "Auto Weights...|rig.autoweights||weight", "Normalize Weights|rig.normalize||weight", "Clear Weights|rig.clear||trash", "Mirror Weights X|rig.mirror_w||mirror", "Smooth Weights|rig.smooth_w||sphere", "-", "Insert Keyframe|pose.key|I|key", "Delete Keyframe|pose.unkey|Alt+I|key_off", "Clear Pose|pose.clear|Alt+R|refresh", "Apply Pose as Rest|pose.apply_rest||pose", "New Action|pose.new_action||anim", "Bake Pose to Mesh|pose.bake||cube"]
    }
    if m == 5.0 {
        return ["New Document...|vec.new||file", "Import SVG...|vec.import_svg||import", "Export SVG...|vec.export_svg||export", "Save Vector (.ling)...|vec.save_ling||save", "Open Vector (.ling)...|vec.open_ling||folder", "-", "New Layer|vec.layer_add||layers", "Delete Layer|vec.layer_del||trash", "-", "Group|vec.group|Ctrl+G|layers", "Ungroup|vec.ungroup|Ctrl+Shift+G|layers", "Bring Forward|vec.fwd|PgUp|arrow_up", "Send Backward|vec.back|PgDn|arrow_down", "Flip Horizontal|vec.flip_h||mirror", "Flip Vertical|vec.flip_v||mirror", "Reverse Path|vec.reverse||refresh", "Simplify Path|vec.simplify||node", "Convert to Mesh (extrude)|vec.extrude||extrude", "Lathe Profile to Mesh|vec.lathe||spin", "-", "Insert Keyframe|anim.key|I|key", "Shape Keyframe|anim.shapekey|K|node", "Delete Keyframe|anim.unkey|Alt+I|key_off", "Export Frames (PNG)...|anim.export_png||image", "Export Sprite Sheet...|anim.export_sheet||image", "Export Animated SVG...|anim.export_svg||export"]
    }
    if m == 6.0 {
        return ["New Texture|tex.new||texture", "Duplicate Texture|tex.dup||copy", "Delete Texture|tex.del||trash", "Randomize Seed|tex.seed|R|refresh", "-", "Apply to Selected Object|tex.apply||cube", "Bake Vertex Colours|tex.bake||brush", "-", "Export PNG...|tex.export||image", "Export Sprite Sheet (animated)...|tex.export_sheet||anim", "Export Normal Map...|tex.export_normal||image", "Export All Scene Textures|tex.export_all||export"]
    }
    if m == 7.0 {
        return ["Outliner|view.sidebar||folder", "Properties|view.props||bars", "Timeline|view.timeline||anim", "-", "Wireframe|view.wire|Z|wire", "Solid|view.solid||solid", "X-Ray|view.xray|Alt+Z|xray", "Backface Culling|view.cull||face", "Show Grid|view.grid||grid", "Show Normals|view.normals||arrow_up", "Show Weights|view.weights||weight", "-", "Front|view.front|Numpad 1|camera", "Right|view.right|Numpad 3|camera", "Top|view.top|Numpad 7|camera", "Perspective / Ortho|view.persp|Numpad 5|camera", "Frame Selected|view.frame|Numpad .|fit", "Frame All|view.frame_all|Home|fit", "-", "Turntable Sprite Sheet...|view.turntable||image", "Screenshot|view.shot|F12|image"]
    }
    return ["About LingModeler|help.about||info", "Shortcuts|help.shortcuts|F1|bars", "File Formats|help.formats||file"]
}

fn menu_sub_def(name) {
    if name == "recent" { return st_get("recent_menu", ["(empty)|none||"]) }
    if name == "import" {
        return ["Wavefront OBJ (.obj)...|file.import_obj||import", "glTF 2.0 (.gltf)...|file.import_gltf||import", "Ling Mesh (.lmesh)...|file.import_lmesh||import", "SVG (.svg) into Vector|vec.import_svg||import", "Ling Vector (.ling)...|vec.open_ling||import", "Image as Texture (.png)...|tex.import_png||image"]
    }
    if name == "export" {
        return ["Wavefront OBJ (+ .mtl)...|file.export_obj||export", "glTF 2.0 (.gltf, embedded)...|file.export_gltf||export", "glTF Binary (.glb)...|file.export_glb||export", "Ling Mesh (.lmesh)...|file.save_as||export", "SVG (.svg)...|vec.export_svg||export", "Ling Vector (.ling)...|vec.save_ling||export", "Texture PNG...|tex.export||image", "Turntable Sprite Sheet...|view.turntable||image"]
    }
    if name == "add" {
        return ["Plane|add.plane||rect", "Cube|add.cube||cube", "Circle|add.circle||ellipse", "UV Sphere|add.sphere||sphere", "Icosphere|add.ico||sphere", "Cylinder|add.cylinder||cylinder", "Cone|add.cone||cylinder", "Torus|add.torus||ellipse", "Grid|add.grid||grid", "Capsule|add.capsule||cylinder", "-", "Armature (bone)|add.armature||bone", "Empty 2D Doc|vec.new||file"]
    }
    if name == "gen" {
        return ["Rock...|gen.rock||rock", "Crystal Cluster...|gen.crystal||star", "Low-poly Tree...|gen.tree||tree", "Barrel...|gen.barrel||cylinder", "Crate...|gen.crate||cube", "Pillar...|gen.pillar||cylinder", "Stairs...|gen.stairs||bars", "Ramp / Wedge...|gen.ramp||play", "Terrain Tile...|gen.terrain||grid", "Fence Section...|gen.fence||bars", "Coin / Gem...|gen.gem||star", "Character Base Mesh...|gen.character||pose", "-", "Extrude 2D Shape|vec.extrude||extrude", "Lathe 2D Profile|vec.lathe||spin"]
    }
    if name == "uv" {
        return ["Box Projection|uv.box||cube", "Planar (from view)|uv.planar||rect", "Cylinder Projection|uv.cyl||cylinder", "Sphere Projection|uv.sphere||sphere", "Reset (per face)|uv.reset||face", "Show UV Panel|uv.panel||texture"]
    }
    return []
}

# state of checkable menu items: 1 checked / 0 unchecked / -1 not checkable
fn menu_check(action) {
    if action == "view.sidebar" { return st_get("show_sidebar", 1.0) }
    if action == "view.props" { return st_get("show_props", 1.0) }
    if action == "view.timeline" { return st_get("show_timeline", 1.0) }
    if action == "view.wire" { return sel(st_get("v3_shade", 1.0) < 0.5, 1.0, 0.0) }
    if action == "view.solid" { return sel(st_get("v3_shade", 1.0) > 0.5, 1.0, 0.0) }
    if action == "view.xray" { return st_get("v3_xray", 0.0) }
    if action == "view.cull" { return st_get("v3_cull", 1.0) }
    if action == "view.grid" { return st_get("v3_grid", 1.0) }
    if action == "view.normals" { return st_get("v3_normals", 0.0) }
    if action == "view.weights" { return st_get("v3_weights", 0.0) }
    if action == "view.persp" { return st_get("cam_persp", 1.0) }
    if action == "uv.panel" { return st_get("show_uv", 0.0) }
    return 0.0 - 1.0
}

fn menu_item_h(s) {
    if s == "-" { return 7.0 }
    return 22.0
}

fn menu_field(item, k) {
    bind parts = str_split(item, "|")
    if k >= len(parts) { return "" }
    return list_get(parts, k)
}

fn menu_bar() {
    bind W = get_width()
    g_rect(0.0, 0.0, W, MENU_H, C_BG_PANEL)
    u_hsep(0.0, MENU_H - 1.0, W)
    bind titles = menu_titles()
    bind x = 6.0
    bind open = st_get("menu_open", 0.0 - 1.0)
    bind i = 0.0
    while i < len(titles) {
        bind t = list_get(titles, i)
        bind w = g_text_w(12.0, t) + 18.0
        bind hot = u_hot(x, 0.0, w, MENU_H - 1.0)
        bind col = menu_color(i)
        if open == i || hot { ds_hatch(x + 1.0, 3.0, w - 2.0, MENU_H - 7.0, col, 45.0, 4.0, 0.35) px_outline(x, 2.0, w, MENU_H - 5.0, col, 0.8) }
        g_text(x + 9.0, 5.0, 12.0, sel(open == i, C_TEXT_STRONG, col), t)
        if hot && u_pressed() {
            if open == i {
                st_set("menu_open", 0.0 - 1.0)
                st_set("menu_sub", "")
            } else {
                st_set("menu_open", i)
                st_set("menu_x", x)
                st_set("menu_sub", "")
            }
            st_set("menu_click_eat", 1.0)
        } else if hot && open >= 0.0 && open != i {
            st_set("menu_open", i)
            st_set("menu_x", x)
            st_set("menu_sub", "")
        }
        bind x = x + w + 2.0
        bind i = i + 1.0
    }
    bind p = st_get("scene_name", "untitled")
    if st_get("dirty", 0.0) > 0.5 { bind p = p + " *" }
    g_text_r(W - 10.0, 5.0, 12.0, C_TEXT_DIM, "LingModeler  -  " + p)
}

# draws a popup list at (x,y); returns the clicked action or ""
fn popup_list(items, x, y, mcol, level) {
    bind w = st_get("popup_w", 250.0)
    bind h = 8.0
    bind i = 0.0
    while i < len(items) {
        bind h = h + menu_item_h(list_get(items, i))
        bind i = i + 1.0
    }
    bind W = get_width()
    bind H = get_height()
    if x + w > W { bind x = W - w - 2.0 }
    bind full = h
    bind vh = min(h, H - 8.0)
    if y + vh > H - 2.0 { bind y = H - vh - 2.0 }
    if y < 2.0 { bind y = 2.0 }
    bind okey = "popup_off" + num_str(level)
    bind off = 0.0
    if full > vh {
        bind off = clampf(st_get(okey, 0.0), 0.0, full - vh)
        if u_mx() >= x && u_mx() < x + w && u_my() >= y && u_my() < y + vh && u_scroll() != 0.0 {
            bind off = clampf(off - u_scroll() * 36.0, 0.0, full - vh)
            st_set("wheel_used", 1.0)
        }
        st_set(okey, off)
    } else { st_set(okey, 0.0) }
    px_rect(x + 2.0, y + 3.0, w, vh, 0, 0.35)
    px_rect(x, y, w, vh, C_POPUP)
    px_outline(x, y, w, vh, C_POPUP_LINE)
    ds_brackets(x, y, w, vh, 7.0, mcol)
    if u_mx() >= x && u_mx() < x + w && u_my() >= y && u_my() < y + vh { st_set("popup_hit", 1.0) }
    bind out = ""
    bind cy = y + 4.0 - off
    u_clip(x, y, w, vh)
    bind i = 0.0
    while i < len(items) {
        bind it = list_get(items, i)
        bind ih = menu_item_h(it)
        bind vis = cy + ih >= y && cy <= y + vh
        if vis && it == "-" {
            g_rect(x + 8.0, cy + 3.0, w - 16.0, 1.0, C_LINE)
        } else if vis && it != "-" {
            bind label = menu_field(it, 0)
            bind act = menu_field(it, 1)
            bind shortcut = menu_field(it, 2)
            bind icon = menu_field(it, 3)
            bind hot = u_hot(x + 2.0, cy, w - 4.0, ih) && u_my() >= y && u_my() < y + vh
            if hot { px_rect(x + 3.0, cy, w - 6.0, ih, C_HOVER_ROW) ds_hatch(x + 3.0, cy + 1.0, w - 6.0, ih - 2.0, mcol, 45.0, 6.0, 0.2) }
            bind chk = menu_check(act)
            if chk > 0.5 { px_rect(x + 7.0, cy + 6.0, 10.0, 10.0, mcol) px_outline(x + 7.0, cy + 6.0, 10.0, 10.0, C_TEXT_STRONG) }
            else if chk > 0.0 - 0.5 { px_outline(x + 7.0, cy + 6.0, 10.0, 10.0, C_TEXT_DIM) }
            else if icon != "" { icon_draw(icon, x + 12.0, cy + ih * 0.5, 11.0, mcol) }
            bind is_sub = str_ends_with(label, " >")
            bind shown = label
            if is_sub { bind shown = str_slice(label, 0.0, str_len(label) - 2.0) }
            g_text(x + 24.0, cy + 4.0, 12.0, C_TEXT, shown)
            if is_sub { icon_draw("chev_r", x + w - 12.0, cy + ih * 0.5, 9.0, C_TEXT_DIM) }
            else if shortcut != "" { g_text_r(x + w - 10.0, cy + 5.0, 11.0, C_TEXT_DIM, shortcut) }
            if hot {
                if is_sub {
                    st_set("menu_sub", str_slice(act, 4.0, str_len(act)))
                    st_set("menu_sub_x", x + w - 2.0)
                    st_set("menu_sub_y", cy - 4.0)
                } else if level < 0.5 {
                    st_set("menu_sub", "")
                }
                if u_pressed() && !is_sub && act != "none" { bind out = act }
            }
        }
        bind cy = cy + ih
        bind i = i + 1.0
    }
    u_unclip()
    if full > vh {
        bind th = max(24.0, vh * vh / full)
        bind ty = y + (vh - th) * off / (full - vh)
        px_rect(x + w - 6.0, ty, 4.0, th, C_TEXT_DIM, 0.6)
    }
    return out
}

# menu overlay: call last each frame. returns action or ""
fn menu_overlay() {
    bind open = st_get("menu_open", 0.0 - 1.0)
    if open < 0.0 { return "" }
    st_set("blk", 0.0)
    st_set("popup_hit", 0.0)
    bind mcol = menu_color(open)
    bind x = st_get("menu_x", 0.0)
    bind items = menu_def(open)
    bind act = popup_list(items, x, MENU_H - 2.0, mcol, 0.0)
    bind sub = st_get("menu_sub", "")
    if sub != "" {
        bind sact = popup_list(menu_sub_def(sub), st_get("menu_sub_x", 0.0), st_get("menu_sub_y", 0.0), mcol, 1.0)
        if sact != "" { bind act = sact }
    }
    if u_pressed() && act == "" && st_get("menu_click_eat", 0.0) < 0.5 {
        if u_my() >= MENU_H && st_get("popup_hit", 0.0) < 0.5 {
            st_set("menu_open", 0.0 - 1.0)
            st_set("menu_sub", "")
        }
    }
    st_set("menu_click_eat", 0.0)
    if act != "" {
        st_set("menu_open", 0.0 - 1.0)
        st_set("menu_sub", "")
    }
    return act
}

# ── context menu (right click) ───────────────────────────────────────────────
fn ctx_open(items, x, y) {
    st_set("ctx_open", 1.0)
    st_set("ctx_items", items)
    st_set("ctx_x", x)
    st_set("ctx_y", y)
    st_set("ctx_eat", 1.0)
}
fn ctx_overlay() {
    if st_get("ctx_open", 0.0) < 0.5 { return "" }
    st_set("blk", 0.0)
    st_set("popup_hit", 0.0)
    bind act = popup_list(st_get("ctx_items", []), st_get("ctx_x", 0.0), st_get("ctx_y", 0.0), ds_col("call"), 2.0)
    if act != "" { st_set("ctx_open", 0.0) }
    else if (u_pressed() || u_rpressed()) && st_get("ctx_eat", 0.0) < 0.5 && st_get("popup_hit", 0.0) < 0.5 { st_set("ctx_open", 0.0) }
    else if key_pressed("escape") { st_set("ctx_open", 0.0) }
    st_set("ctx_eat", 0.0)
    return act
}

# ── mode tabs row ────────────────────────────────────────────────────────────
fn mode_names() { return ["Model", "Rig", "Weights", "Pose", "Vector", "Anim", "Texture"] }
fn mode_of_tab(i) {
    # tab -> mode: Model keeps whichever of object(0)/edit(1) was last used
    if i == 0.0 { return st_get("model_mode", 0.0) }
    if i == 1.0 { return 2.0 }
    if i == 2.0 { return 3.0 }
    if i == 3.0 { return 4.0 }
    if i == 4.0 { return 5.0 }
    if i == 5.0 { return 6.0 }
    return 7.0
}
fn tab_of_mode(m) {
    if m < 1.5 { return 0.0 }
    if m < 2.5 { return 1.0 }
    if m < 3.5 { return 2.0 }
    if m < 4.5 { return 3.0 }
    if m < 5.5 { return 4.0 }
    if m < 6.5 { return 5.0 }
    return 6.0
}
fn is_3d_mode() { return st_get("mode", 0.0) < 4.5 }

fn view_tabs() {
    bind W = get_width()
    bind y = MENU_H
    g_rect(0.0, y, W, TABS_H, C_BG_PANEL)
    u_hsep(0.0, y + TABS_H - 1.0, W)
    bind x = 8.0
    if u_tab("v_sidebar", x, y + 3.0, 66.0, 22.0, "Outliner", st_get("show_sidebar", 1.0) > 0.5, 0.0) { st_set("show_sidebar", 1.0 - st_get("show_sidebar", 1.0)) }
    bind x = x + 70.0
    if u_tab("v_props", x, y + 3.0, 70.0, 22.0, "Properties", st_get("show_props", 1.0) > 0.5, 0.0) { st_set("show_props", 1.0 - st_get("show_props", 1.0)) }
    bind x = x + 78.0
    u_vsep(x, y + 6.0, 16.0)
    bind x = x + 10.0
    g_text(x, y + 8.0, 12.0, C_TEXT_DIM, "Mode")
    bind x = x + 40.0
    bind names = mode_names()
    bind cols = [C_RED, C_VIOLET, C_ORANGE, C_YELLOW, C_BLUE, C_GREEN, C_INDIGO]
    bind keys = ["F1 / Tab", "F2", "F3", "F4", "F5", "F6", "F7"]
    bind cur = tab_of_mode(st_get("mode", 0.0))
    bind i = 0.0
    while i < 7.0 {
        bind n = list_get(names, i)
        bind w = g_text_w(12.0, n) + 22.0
        if u_hot(x, y + 3.0, w, 22.0) { u_tip(n + " (" + list_get(keys, i) + ")") }
        if u_tab("mtab" + num_str(i), x, y + 3.0, w, 22.0, n, cur == i, list_get(cols, i)) { set_mode(mode_of_tab(i)) }
        bind x = x + w + 3.0
        bind i = i + 1.0
    }
    # object / edit sub-switch for the Model tab
    if cur == 0.0 {
        bind x = x + 8.0
        u_vsep(x, y + 6.0, 16.0)
        bind x = x + 10.0
        bind m = st_get("mode", 0.0)
        if u_chip("m_obj", x, y + 5.0, 56.0, 18.0, "Object", m < 0.5, C_RED) { set_mode(0.0) }
        if u_chip("m_edit", x + 60.0, y + 5.0, 56.0, 18.0, "Edit", m > 0.5, C_ORANGE) { set_mode(1.0) }
    }
}

fn set_mode(m) {
    bind old = st_get("mode", 0.0)
    if old == m { return 0.0 }
    v3_cancel_modal()
    st_set("mode", m)
    if m < 1.5 { st_set("model_mode", m) }
    st_set("focus", "")
    if m == 1.0 && me_count() > 0.5 && st_get("sel_o", 0.0 - 1.0) < 0.0 { st_set("sel_o", me_first()) }
    st_set("status", list_get(["Object mode", "Edit mode: 1/2/3 vertex/edge/face, E extrude, I inset, Ctrl+B bevel, Ctrl+R loop cut", "Rig mode: Shift+A add bone, E extrude, Ctrl+P parent", "Weight paint: LMB paint, Ctrl paint subtract, [ ] radius", "Pose mode: R rotate, G move, I keyframe, Space play", "Vector: V select, N nodes, P pen, R rect, E ellipse", "Animation: I keyframe, K shape key, Space play", "Texture: pick a preset, tune, Apply or Export"], m))
}

# ── tool strip (mode specific) ──────────────────────────────────────────────
fn toolbar() {
    bind W = get_width()
    bind y = MENU_H + TABS_H
    g_rect(0.0, y, W, TOOL_H, C_BG_PANEL)
    u_hsep(0.0, y + TOOL_H - 1.0, W)
    bind by = y + 5.0
    bind m = st_get("mode", 0.0)
    bind x = 8.0
    if m < 4.5 { bind x = v3_toolbar(x, by) }
    else if m < 6.5 { bind x = vc_toolbar(x, by) }
    else { bind x = tx_toolbar(x, by) }
    # status text on the right part of the strip
    bind cur_status = st_get("status", "Ready")
    if cur_status != st_get("status_seen", "") {
        st_set("status_seen", cur_status)
        st_set("status_t", st_get("now", 0.0))
    }
    bind age = st_get("now", 0.0) - st_get("status_t", 0.0)
    bind fade = clampf((age - 6.0) / 6.0, 0.0, 1.0)
    bind sx = max(x + 16.0, W - 420.0)
    g_text_fit(sx, by + 7.0, W - sx - 8.0, 12.0, c_mix(C_TEXT, 4408131.0, fade * 0.8), cur_status)
}

# ── status bar (bottom) ─────────────────────────────────────────────────────
fn status_bar() {
    bind W = get_width()
    bind H = get_height()
    bind y = H - STATUS_H
    g_rect(0.0, y, W, STATUS_H, C_BG_PANEL)
    u_hsep(0.0, y, W)
    bind m = st_get("mode", 0.0)
    bind txt = ""
    if m < 4.5 { bind txt = v3_status_text() }
    else if m < 6.5 { bind txt = vc_status_text() }
    else { bind txt = tx_status_text() }
    g_text_fit(10.0, y + 5.0, W * 0.6, 11.0, C_TEXT_DIM, txt)
    bind fps = st_get("fps", 0.0)
    g_text_r(W - 10.0, y + 5.0, 11.0, C_TEXT_DIM, fmt0(fps) + " fps")
    bind hint = st_get("hint", "")
    if hint != "" { g_text_r(W - 70.0, y + 5.0, 11.0, C_TEXT, hint) }
}

← 返回索引