lingdev
↓ 0 downloadsLingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdevCode / Files (136)
ling/inspector.ling
# inspector.ling - properties of the selected node: transform with key diamonds, colour,
# kind-specific fields, script / tag, keyed properties. Edits go through set_prop (auto-key aware).
# a labelled draggable number with a key diamond; returns nothing, writes the node
fn prop_num(label, x, y, w, i, col, lo, hi, speed, fmt) {
bind id = nd_id(i)
g_text(x, y + 6.0, 11.0, C_TEXT_DIM, label)
bind v = nd_get(i, col)
bind shown = sel(fmt == "deg", fmt1(v * 57.29578), sel(fmt == "int", num_str(floor(v)), fmt2(v)))
bind fx = x + 22.0
bind fw = w - 22.0 - 18.0
bind hot = u_hot(fx, y, fw, 24.0)
if hot && u_pressed() { undo_push() }
bind nv = u_dragval("pn_" + col + num_str(id), fx, y, fw, 24.0, sel(fmt == "deg", v * 57.29578, v), lo, hi, speed, shown)
if u_has(nv) {
if fmt == "deg" { set_prop(i, col, nv / 57.29578) } else if fmt == "int" { set_prop(i, col, floor(nv)) } else { set_prop(i, col, nv) }
}
# key diamond
bind kx = x + w - 8.0
bind t = st_get("tl_t", 0.0)
bind has = key_has(id, col)
bind at = has && key_at(id, col, t) >= 0.0
bind khot = u_hot(kx - 8.0, y + 4.0, 16.0, 16.0)
if at { g_diamond(kx, y + 12.0, 5.0, C_RED) }
else if has { icon_draw("key_off", kx, y + 12.0, 11.0, C_RED) }
else { icon_draw("key_off", kx, y + 12.0, 11.0, sel(khot, C_TEXT, C_LINE)) }
if khot {
u_tip(sel(at, "Remove this key", sel(has, "Add a key at the playhead (right-click: clear all keys of " + col + ")", "Key " + col + " at the playhead")))
if u_pressed() {
undo_push()
if at { key_del(key_at(id, col, t)) } else { key_add(id, col, t, nd_get(i, col), "cubic_in_out") }
}
if u_rpressed() && has { undo_push() key_remove_prop(id, col) }
}
}
# a text field bound to a column
fn prop_text(label, x, y, w, i, col) {
g_text(x, y + 6.0, 11.0, C_TEXT_DIM, label)
bind fid = "pt_" + col + num_str(nd_id(i))
bind cur = nd_get(i, col)
bind was_focus = st_get("focus", "") == fid
bind v = u_textfield(fid, x + 46.0, y, w - 46.0, 24.0, cur)
if v != cur {
if st_get("insp_undo_fid", "") != fid { undo_push() st_set("insp_undo_fid", fid) }
nd_set(i, col, v)
mark_dirty()
}
if !was_focus && st_get("insp_undo_fid", "") == fid && st_get("focus", "") != fid { st_set("insp_undo_fid", "") }
}
fn prop_pick(x, y, w, i, col, exts, title) {
if u_button("pp_" + col + num_str(nd_id(i)), x, y, w, 24.0, "Pick", 0.0) {
st_set("pending_src", nd_id(i))
st_set("pending_col", col)
picker_open("open", title, exts, "node.set_src", p_join(proj_root(), "assets"), "")
}
}
fn inspector_panel(x, y, w, h) {
g_rect(x, y, w, h, C_BG_SIDE)
u_vsep(x, y, h)
ds_word(x + 12.0, y + 8.0, 10.0, "INSPECTOR", ds_col("func"), true)
u_hsep(x + 6.0, y + 31.0, w - 12.0)
bind i = sel_index()
bind top = y + 36.0
bind vh = h - 40.0
bind cx = x + 12.0
bind cw = w - 34.0
if i < 0.0 {
inspector_scene(cx, top, cw)
return 0.0
}
bind id = nd_id(i)
bind kind = nd_get(i, "kind")
bind is2d = nd_is_2d(kind)
bind content = max(sel(is2d, 620.0, 720.0), st_get("insp_content", 700.0))
bind off = u_scroll_y("insp_sb", x + 1.0, top, w - 1.0, vh, content, st_get("insp_scroll", 0.0))
st_set("insp_scroll", off)
u_clip(x + 1.0, top, w - 1.0, vh)
bind yy = top + 6.0 - off
# header: icon, kind, name
icon_draw(kind_icon(kind), cx + 12.0, yy + 12.0, 20.0, kind_col(kind))
g_text(cx + 30.0, yy + 1.0, 11.0, kind_col(kind), kind + " #" + num_str(id) + sel(nd_get(i, "par") > 0.0, " child of " + nd_gid(nd_get(i, "par"), "name"), ""))
bind yy = yy + 18.0
prop_text("Name", cx, yy, cw, i, "name")
bind yy = yy + 30.0
bind v = u_check("insp_vis", cx, yy, "Visible", nd_get(i, "vis") > 0.5)
if v { undo_push() nd_set(i, "vis", 1.0 - nd_get(i, "vis")) }
bind v = u_check("insp_lock", cx + 90.0, yy, "Locked", nd_get(i, "lock") > 0.5)
if v { undo_push() nd_set(i, "lock", 1.0 - nd_get(i, "lock")) }
bind yy = yy + 26.0
# transform
u_section(cx, yy, cw, "TRANSFORM", ds_col("ctl"))
bind yy = yy + 18.0
bind third = floor(cw / 3.0)
if is2d {
prop_num("X", cx, yy, third, i, "px", 0.0 - 10000.0, 10000.0, 1.0, "int")
prop_num("Y", cx + third, yy, third, i, "py", 0.0 - 10000.0, 10000.0, 1.0, "int")
bind yy = yy + 30.0
prop_num("W", cx, yy, third, i, "w", 1.0, 8000.0, 1.0, "int")
prop_num("H", cx + third, yy, third, i, "h", 1.0, 8000.0, 1.0, "int")
bind yy = yy + 30.0
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Anchor")
bind ancs = ["tl", "tc", "tr", "cl", "cc", "cr", "bl", "bc", "br"]
bind an = ["top left", "top centre", "top right", "centre left", "centre", "centre right", "bottom left", "bottom centre", "bottom right"]
bind ai = u_combo("insp_anc", cx + 46.0, yy, cw - 46.0, 24.0, an, max(0.0, l_index(ancs, nd_get(i, "anc"))))
if ai >= 0.0 { undo_push() nd_set(i, "anc", list_get(ancs, ai)) }
bind yy = yy + 30.0
} else {
prop_num("X", cx, yy, third, i, "px", 0.0 - 100000.0, 100000.0, 0.02, "")
prop_num("Y", cx + third, yy, third, i, "py", 0.0 - 100000.0, 100000.0, 0.02, "")
prop_num("Z", cx + third * 2.0, yy, third, i, "pz", 0.0 - 100000.0, 100000.0, 0.02, "")
bind yy = yy + 30.0
g_text(cx, yy, 10.0, C_TEXT_DIM, "rotation (degrees)")
bind yy = yy + 14.0
prop_num("X", cx, yy, third, i, "rx", 0.0 - 3600.0, 3600.0, 0.5, "deg")
prop_num("Y", cx + third, yy, third, i, "ry", 0.0 - 3600.0, 3600.0, 0.5, "deg")
prop_num("Z", cx + third * 2.0, yy, third, i, "rz", 0.0 - 3600.0, 3600.0, 0.5, "deg")
bind yy = yy + 30.0
g_text(cx, yy, 10.0, C_TEXT_DIM, sel(kind == "model" || kind == "actor", "scale", "size (half extents)"))
bind yy = yy + 14.0
prop_num("X", cx, yy, third, i, "sx", 0.001, 10000.0, 0.01, "")
prop_num("Y", cx + third, yy, third, i, "sy", 0.001, 10000.0, 0.01, "")
prop_num("Z", cx + third * 2.0, yy, third, i, "sz", 0.001, 10000.0, 0.01, "")
bind yy = yy + 30.0
}
# colour
u_section(cx, yy, cw, "COLOUR", ds_col("asm"))
bind yy = yy + 18.0
bind c = nd_get(i, "col")
bind q = floor((cw - 36.0) / 3.0)
bind r = u_dragval("insp_r" + num_str(id), cx, yy, q, 24.0, c_r(c), 0.0, 255.0, 1.0, "R " + num_str(c_r(c)))
bind g = u_dragval("insp_g" + num_str(id), cx + q + 4.0, yy, q, 24.0, c_g(c), 0.0, 255.0, 1.0, "G " + num_str(c_g(c)))
bind b = u_dragval("insp_b" + num_str(id), cx + q * 2.0 + 8.0, yy, q, 24.0, c_b(c), 0.0, 255.0, 1.0, "B " + num_str(c_b(c)))
if u_has(r) { set_prop(i, "col", c_rgb(r, c_g(c), c_b(c))) }
if u_has(g) { set_prop(i, "col", c_rgb(c_r(c), g, c_b(c))) }
if u_has(b) { set_prop(i, "col", c_rgb(c_r(c), c_g(c), b)) }
g_rect(cx + cw - 24.0, yy + 2.0, 20.0, 20.0, c)
g_frame(cx + cw - 24.0, yy + 2.0, 20.0, 20.0, C_LINE)
bind yy = yy + 30.0
bind k = 0.0
while k < 9.0 {
bind pc = sel(k < 7.0, c_pal(k), sel(k < 7.5, C_WHITE, C_TEXT_DIM))
bind sx = cx + k * 20.0
g_rect(sx, yy, 16.0, 14.0, pc)
if u_hot(sx, yy, 16.0, 14.0) {
g_frame(sx, yy, 16.0, 14.0, C_WHITE)
if u_pressed() { undo_push() nd_set(i, "col", pc) }
}
bind k = k + 1.0
}
bind yy = yy + 24.0
# kind-specific
u_section(cx, yy, cw, str_upper(kind), kind_col(kind))
bind yy = yy + 18.0
bind yy = inspector_kind(cx, yy, cw, i, kind)
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Extra")
bind exv = u_textfield("insp_ex" + num_str(id), cx + 46.0, yy, cw - 46.0, 24.0, nd_get(i, "ex"))
if exv != nd_get(i, "ex") { nd_set(i, "ex", exv) mark_dirty() }
if u_hot(cx + 46.0, yy, cw - 46.0, 24.0) { u_tip("k=v;k=v options: hue=0.85 gain=1.2 (lmesh colour cycling), shape=sphere (orb actor), rainbow=1, flat=1, style=text, blink=1, font=Daemon") }
bind yy = yy + 30.0
# script / tag
u_section(cx, yy, cw, "SCRIPT", ds_col("mod"))
bind yy = yy + 18.0
prop_text("Script", cx, yy, cw - 54.0, i, "scr")
if u_button("insp_scr_new", cx + cw - 50.0, yy, 50.0, 24.0, "Edit", 0.0) {
bind sn = nd_get(i, "scr")
if sn == "" { modal_text("New script name", "new_script", str_lower(str_replace(nd_get(i, "name"), " ", "_"))) }
else { script_open(p_join(proj_root(), "scripts/" + sn + ".ling")) st_set("dock_tab", 2.0) st_set("show_dock", 1.0) }
}
bind yy = yy + 30.0
prop_text("Tag", cx, yy, cw, i, "tag")
bind yy = yy + 30.0
# keys
bind props = key_props_of(id)
if len(props) > 0.5 {
u_section(cx, yy, cw, "KEYS", ds_col("bind"))
bind yy = yy + 18.0
bind p = 0.0
while p < len(props) {
bind prop = list_get(props, p)
bind ks = key_list(id, prop)
g_diamond(cx + 6.0, yy + 8.0, 4.0, C_RED)
g_text(cx + 16.0, yy + 2.0, 11.0, C_TEXT, prop + " " + num_str(len(ks)) + " keys")
if u_button("insp_kclr" + prop, cx + cw - 54.0, yy - 2.0, 54.0, 20.0, "clear", 0.0) { undo_push() key_remove_prop(id, prop) }
bind yy = yy + 24.0
bind p = p + 1.0
}
}
bind yy = yy + 8.0
if u_button("insp_dup", cx, yy, 80.0, 24.0, "Duplicate", 0.0) { do_action("edit.dup") }
if u_button("insp_focus", cx + 86.0, yy, 60.0, 24.0, "Focus", 0.0) { do_action("view.focus") }
if u_button("insp_del", cx + cw - 70.0, yy, 70.0, 24.0, "Delete", C_RED) { do_action("edit.delete") }
bind yy = yy + 40.0
st_set("insp_content", yy - (top + 6.0 - off) + 20.0)
u_unclip()
return 0.0
}
# kind-specific fields; returns the next y
fn inspector_kind(cx, yy, cw, i, kind) {
bind half = floor(cw / 2.0)
if kind == "model" || kind == "actor" {
prop_text("File", cx, yy, cw - 54.0, i, "src")
prop_pick(cx + cw - 50.0, yy, 50.0, i, "src", "glb,gltf,lmesh", "Model file")
bind yy = yy + 30.0
bind hnd = model_handle(nd_get(i, "src"))
bind ac = plat_model_anim_count(hnd)
g_text(cx, yy + 2.0, 11.0, C_TEXT_DIM, sel(hnd >= 0.0, "loaded: " + num_str(plat_model_tris(hnd)) + " tris, " + num_str(ac) + " clips", sel(nd_get(i, "src") == "", "no file", sel(plat_has_models(), "could not load", "models load in the desktop runtime"))))
bind yy = yy + 18.0
prop_num("Clip", cx, yy, half, i, "a", 0.0, max(0.0, ac - 1.0), 0.05, "int")
prop_num("Spd", cx + half, yy, half, i, "b", 0.0, 10.0, 0.01, "")
bind yy = yy + 30.0
if kind == "actor" {
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Control")
bind ctl = ["none", "player (WASD / pad)", "AI: follow player", "AI: patrol", "AI: guard"]
bind ci = u_combo("insp_ctl", cx + 56.0, yy, cw - 56.0, 24.0, ctl, clampf(nd_get(i, "c"), 0.0, 4.0))
if ci >= 0.0 { undo_push() nd_set(i, "c", ci) if ci == 1.0 { nd_set(i, "tag", "player") } }
bind yy = yy + 30.0
prop_num("Speed", cx, yy, half, i, "d", 0.1, 60.0, 0.05, "")
bind yy = yy + 30.0
}
} else if kind == "light" {
prop_num("Int", cx, yy, half, i, "a", 0.0, 20.0, 0.02, "")
prop_num("Rad", cx + half, yy, half, i, "b", 0.1, 500.0, 0.1, "")
bind yy = yy + 30.0
} else if kind == "camera" {
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Rig")
bind rigs = ["fixed (pose)", "orbit target", "follow target", "dolly (timeline)"]
bind ri = u_combo("insp_rig", cx + 46.0, yy, cw - 46.0, 24.0, rigs, clampf(nd_get(i, "b"), 0.0, 3.0))
if ri >= 0.0 { undo_push() nd_set(i, "b", ri) }
bind yy = yy + 30.0
prop_num("Zd", cx, yy, half, i, "a", 0.3, 20.0, 0.01, "")
prop_num("Dist", cx + half, yy, half, i, "d", 0.5, 200.0, 0.05, "")
bind yy = yy + 30.0
prop_text("Target", cx, yy, cw, i, "txt")
bind yy = yy + 30.0
prop_num("Spin", cx, yy, half, i, "c", 0.0 - 3.0, 3.0, 0.005, "")
if u_button("insp_cam_here", cx + half + 4.0, yy, half - 4.0, 24.0, "From view", 0.0) {
undo_push()
nd_set(i, "px", cam_x()) nd_set(i, "py", cam_y()) nd_set(i, "pz", cam_z())
nd_set(i, "ry", cam_yaw()) nd_set(i, "rx", cam_pitch())
}
bind yy = yy + 30.0
} else if kind == "spawn" {
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "The player actor starts here (tag `player`); the Y rotation is the facing direction.")
bind yy = yy + 34.0
} else if kind == "trigger" {
prop_text("Action", cx, yy, cw, i, "txt")
bind yy = yy + 30.0
g_text_wrap(cx, yy, cw, 10.0, C_TEXT_DIM, "goto:scenes/x.lscene cutscene:scenes/c.lscene say:text sound:file music:file call:hook set:k=v add:k=v quit")
bind yy = yy + 36.0
bind v = u_check("insp_once", cx, yy, "Fire once", nd_get(i, "a") > 0.5)
if v { undo_push() nd_set(i, "a", 1.0 - nd_get(i, "a")) }
bind yy = yy + 26.0
} else if kind == "audio" || kind == "music" {
prop_text("File", cx, yy, cw - 54.0, i, "src")
prop_pick(cx + cw - 50.0, yy, 50.0, i, "src", "wav,ogg,mp3,flac", "Sound file")
bind yy = yy + 30.0
prop_num("Vol", cx, yy, half, i, "a", 0.0, 1.0, 0.005, "")
if kind == "audio" { prop_num("Spread", cx + half, yy, half, i, "d", 0.1, 50.0, 0.05, "") }
bind yy = yy + 30.0
bind v = u_check("insp_loop", cx, yy, "Loop", nd_get(i, "b") > 0.5)
if v { undo_push() nd_set(i, "b", 1.0 - nd_get(i, "b")) }
bind v = u_check("insp_auto", cx + 80.0, yy, "Autoplay", nd_get(i, "c") > 0.5)
if v { undo_push() nd_set(i, "c", 1.0 - nd_get(i, "c")) }
bind yy = yy + 26.0
} else if kind == "midi" {
prop_text("File", cx, yy, cw - 54.0, i, "src")
prop_pick(cx + cw - 50.0, yy, 50.0, i, "src", "mid,midi", "MIDI file")
bind yy = yy + 30.0
prop_text("Patch", cx, yy, cw - 54.0, i, "txt")
prop_pick(cx + cw - 50.0, yy, 50.0, i, "txt", "ling", "Instrument patch (.ling)")
bind yy = yy + 30.0
bind v = u_check("insp_vis_notes", cx, yy, "Visualise notes", nd_get(i, "b") > 0.5)
if v { undo_push() nd_set(i, "b", 1.0 - nd_get(i, "b")) }
bind yy = yy + 26.0
} else if kind == "text3d" || kind == "label" || kind == "button" {
prop_text("Text", cx, yy, cw - 54.0, i, "txt")
if u_button("insp_txt_big", cx + cw - 50.0, yy, 50.0, 24.0, "Edit", 0.0) { st_set("insp_field", "txt") modal_text("Text", "node_field", nd_get(i, "txt")) }
bind yy = yy + 30.0
prop_num(sel(kind == "text3d", "Size", "Font"), cx, yy, half, i, "a", 0.1, 400.0, 0.1, sel(kind == "text3d", "", "int"))
bind yy = yy + 30.0
if kind == "button" {
prop_text("Action", cx, yy, cw, i, "scr")
bind yy = yy + 30.0
g_text_wrap(cx, yy, cw, 10.0, C_TEXT_DIM, "goto:scene cutscene:scene back quit host:7777 join:ip:port vol:0.1 toggle:k set:k=v call:hook")
bind yy = yy + 30.0
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Style")
bind sti = u_combo("insp_bstyle", cx + 46.0, yy, cw - 46.0, 24.0, ["framed (hatched box)", "text row with ink cursor"], sel(nd_ex(i, "style", "") == "text", 1.0, 0.0))
if sti >= 0.0 { undo_push() nd_ex_set(i, "style", sel(sti > 0.5, "text", "")) }
bind yy = yy + 30.0
}
if kind == "label" {
bind v = u_check("insp_blink", cx, yy, "Blink", nd_ex(i, "blink", "0") == "1")
if v { undo_push() nd_ex_set(i, "blink", sel(nd_ex(i, "blink", "0") == "1", "", "1")) }
bind yy = yy + 26.0
}
if kind != "text3d" {
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Font")
bind fv = u_textfield("insp_font" + num_str(nd_id(i)), cx + 46.0, yy, cw - 46.0, 24.0, nd_ex(i, "font", ""))
if fv != nd_ex(i, "font", "") { nd_ex_set(i, "font", fv) mark_dirty() }
bind yy = yy + 30.0
}
} else if kind == "image" {
prop_text("File", cx, yy, cw - 54.0, i, "src")
prop_pick(cx + cw - 50.0, yy, 50.0, i, "src", "png,jpg,jpeg,bmp", "Image file")
bind yy = yy + 30.0
} else if kind == "panel" {
prop_num("Alpha", cx, yy, half, i, "a", 0.0, 1.0, 0.005, "")
bind yy = yy + 30.0
} else if kind == "bar" {
prop_num("Val", cx, yy, half, i, "a", 0.0, 100000.0, 0.5, "")
prop_num("Max", cx + half, yy, half, i, "b", 0.001, 100000.0, 0.5, "")
bind yy = yy + 30.0
g_text_wrap(cx, yy, cw, 10.0, C_TEXT_DIM, "Tag `health` / `score` binds the bar (or a label's {}) to game_var of that name.")
bind yy = yy + 30.0
} else if kind == "swarm" {
prop_num("Count", cx, yy, half, i, "a", 1.0, 400.0, 1.0, "int")
prop_num("Radius", cx + half, yy, half, i, "b", 0.1, 500.0, 0.05, "")
bind yy = yy + 30.0
g_text(cx, yy + 6.0, 11.0, C_TEXT_DIM, "Shape")
bind shapes = ["wire cubes", "stars (points)", "orbs (spheres)"]
bind shi = u_combo("insp_swarm", cx + 46.0, yy, cw - 46.0, 24.0, shapes, clampf(nd_get(i, "c"), 0.0, 2.0))
if shi >= 0.0 { undo_push() nd_set(i, "c", shi) }
bind yy = yy + 30.0
prop_num("Drift", cx, yy, half, i, "d", 0.0, 5.0, 0.005, "")
bind v = u_check("insp_rainbow", cx + half + 4.0, yy + 4.0, "Rainbow", nd_ex(i, "rainbow", "0") == "1")
if v { undo_push() nd_ex_set(i, "rainbow", sel(nd_ex(i, "rainbow", "0") == "1", "", "1")) }
bind yy = yy + 30.0
bind v = u_check("insp_flat", cx, yy, "Flat (a disc, not a ball)", nd_ex(i, "flat", "0") == "1")
if v { undo_push() nd_ex_set(i, "flat", sel(nd_ex(i, "flat", "0") == "1", "", "1")) }
bind yy = yy + 26.0
} else if kind == "knot" {
prop_num("Count", cx, yy, half, i, "a", 1.0, 120.0, 1.0, "int")
prop_num("Speed", cx + half, yy, half, i, "d", 0.0 - 2.0, 2.0, 0.002, "")
bind yy = yy + 30.0
prop_num("Outer", cx, yy, half, i, "b", 0.1, 200.0, 0.02, "")
prop_num("Inner", cx + half, yy, half, i, "c", 0.0, 200.0, 0.02, "")
bind yy = yy + 30.0
bind v = u_check("insp_rainbow2", cx, yy, "Rainbow chain", nd_ex(i, "rainbow", "1") == "1")
if v { undo_push() nd_ex_set(i, "rainbow", sel(nd_ex(i, "rainbow", "1") == "1", "0", "1")) }
bind yy = yy + 26.0
} else if kind == "plane" {
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "Tag `solid` makes a box or plane collide with the player.")
bind yy = yy + 30.0
} else if kind == "empty" {
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "A group: children follow its position and Y rotation.")
bind yy = yy + 30.0
} else {
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "Tag `solid` makes it collide with the player.")
bind yy = yy + 30.0
}
return yy
}
fn inspector_scene(cx, top, cw) {
bind yy = top + 6.0
ds_word(cx, yy, 11.0, "SCENE", ds_col("call"), false)
bind yy = yy + 24.0
u_kv(cx, yy, cw, "Name", sc_get("name", ""))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Kind", sc_get("kind", "3d"))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "File", sel(sc_get("path", "") == "", "(unsaved)", p_base(sc_get("path", ""))))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Nodes", num_str(nd_count()))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Timeline", fmt1(sc_get("len", 10.0)) + " s @ " + num_str(sc_get("fps", 30.0)) + " fps")
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Keys / clips", num_str(key_count()) + " / " + num_str(clip_count()))
bind yy = yy + 18.0
if sc_get("hud", "") != "" { u_kv(cx, yy, cw, "HUD", sc_get("hud", "")) bind yy = yy + 18.0 }
if sc_get("next", "") != "" { u_kv(cx, yy, cw, "Next", sc_get("next", "")) bind yy = yy + 18.0 }
bind yy = yy + 10.0
if u_button("insp_scn", cx, yy, 120.0, 24.0, "Scene Settings", C_BLUE) { modal_open("scene") }
if u_button("insp_addn", cx + 126.0, yy, 80.0, 24.0, "+ Add", 0.0) { modal_open("addnode") }
bind yy = yy + 40.0
ds_word(cx, yy, 11.0, "PROJECT", ds_col("bind"), false)
bind yy = yy + 24.0
if proj_root() == "" {
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "No project open. File > New Project, or Open Project.")
bind yy = yy + 30.0
if u_button("insp_np", cx, yy, 120.0, 24.0, "New Project", C_BLUE) { do_action("project.new") }
} else {
u_kv(cx, yy, cw, "Name", st_get("proj_name", ""))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Start", st_get("proj_start", ""))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Scenes", num_str(st_llen("proj_scenes")))
bind yy = yy + 18.0
u_kv(cx, yy, cw, "Window", num_str(st_get("proj_w", 1280.0)) + "x" + num_str(st_get("proj_h", 720.0)))
bind yy = yy + 24.0
if u_button("insp_build", cx, yy, 120.0, 24.0, "Build & Export", 0.0) { do_action("build.panel") }
}
bind yy = yy + 40.0
g_text_wrap(cx, yy, cw, 11.0, C_TEXT_DIM, "Click a node in the viewport or the Hierarchy to edit it. Right-drag orbits, middle-drag pans, wheel zooms.")
}
fn inspector_text_submit(v) {
bind i = sel_index()
if i < 0.0 { return 0.0 }
bind col = st_get("insp_field", "txt")
undo_push()
nd_set(i, col, v)
return 0.0
}