lingdev
↓ 0 下载LingDev: 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).
关键词 game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev代码 / 文件 (136)
ling/view3d.ling
# view3d.ling - the 3-D viewport: orbit / pan / zoom / fly camera, grid, the scene through the
# software renderer, picking, move / rotate / scale gizmos, asset drops, cutscene camera preview.
fn view3d(x, y, w, h) {
bind t = st_get("tl_t", 0.0)
bind kind = sc_get("kind", "3d")
g_rect(x, y, w, h, sc_get("bg", 526603.0))
cam_viewport(x, y, w, h)
bind hot = u_hot(x, y, w, h) && st_get("bar_busy", 0.0) < 0.5
# camera: editor orbit, scene camera preview, or the cutscene's camera
bind preview = false
bind ci = 0.0 - 1.0
if kind == "cutscene" && st_get("cut_preview", 1.0) > 0.5 && clip_active("camera", t) >= 0.0 {
cut_tick_preview(t)
bind preview = true
} else if st_get("view_campre", 0.0) > 0.5 {
bind ci = game_camera_node()
if ci >= 0.0 {
bind wp = nd_world_pos_t(ci, t)
cam_set(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), nd_world_yaw_t(ci, t), nd_val(ci, "rx", t), max(0.3, nd_get(ci, "a")))
bind preview = true
}
}
if !preview { view3d_camera_input(hot) }
st_set("ed_cam_pose", fmt3(cam_x()) + "," + fmt3(cam_y()) + "," + fmt3(cam_z()) + "," + fmt3(cam_yaw()) + "," + fmt3(cam_pitch()) + "," + fmt3(cam_zd()))
u_clip(x, y, w, h)
if st_get("view_grid", 1.0) > 0.5 {
bind step = max(0.1, st_get("grid_step", 1.0))
sw_grid(step * 20.0, step, sc_get("floor", 0.0), C_GRID_MAJ, C_GRID_MIN)
}
draw_scene_sw(t, st_get("view_solid", 1.0) > 0.5, true)
# gizmo of the selection
bind sidx = sel_index()
if sidx >= 0.0 && !nd_is_2d(nd_get(sidx, "kind")) { view3d_gizmo(sidx, hot, t) } else { st_set("gz_hover", "") }
if preview && kind == "cutscene" { cut_overlay(x, y, w, h) }
# picking / node drag / context
if hot && !preview { view3d_pick_input(x, y, w, h, t) }
if hot && preview && kind == "cutscene" && u_pressed() && st_get("gz_hover", "") == "" { view3d_pick_input(x, y, w, h, t) }
# drops from the asset browser
if st_get("drop_path", "") != "" && u_in(x, y, w, h) {
view3d_hit(u_mx(), u_my())
asset_add_to_scene(st_get("drop_path", ""), true)
st_set("drop_path", "")
} else if st_get("drop_path", "") != "" && !u_in(x, y, w, h) && st_get("show_dock", 1.0) > 0.5 && u_my() > y + h { st_set("drop_path", "") }
# overlays
view3d_overlay(x, y, w, h, preview, ci)
u_unclip()
}
# cutscene preview: camera + fx tracks without touching node positions
fn cut_tick_preview(t) {
bind ci = clip_active("camera", t)
if ci >= 0.0 { cut_apply_camera(ci, t) }
# fx only
bind n = clip_count()
bind fade = 0.0
bind box = 0.0
bind flash = 0.0
bind i = 0.0
while i < n {
if clip_get(i, "track") == "fx" {
bind c0 = clip_get(i, "t")
bind d = max(0.001, clip_get(i, "d"))
bind k = clip_get(i, "kind")
bind s = to_number(clip_get(i, "c"))
if s <= 0.0 { bind s = 1.0 }
if t >= c0 && t < c0 + d {
bind u = (t - c0) / d
if k == "fade_in" { bind fade = max(fade, (1.0 - u) * s) }
else if k == "fade_out" { bind fade = max(fade, u * s) }
else if k == "letterbox" { bind box = max(box, min(1.0, min(u * 4.0, (1.0 - u) * 4.0)) * s) }
else if k == "flash" { bind flash = max(flash, (1.0 - u) * s) }
} else if k == "fade_out" && t >= c0 + d { bind fade = max(fade, s) }
}
bind i = i + 1.0
}
st_set("cut_fade", clampf(fade, 0.0, 1.0))
st_set("cut_box", clampf(box, 0.0, 1.0))
st_set("cut_flash", clampf(flash, 0.0, 1.0))
bind di = clip_active("dialog", t)
if di >= 0.0 { st_set("cut_speaker", clip_get(di, "a")) st_set("cut_text", clip_get(di, "b")) st_set("cut_dialog_kind", clip_get(di, "kind")) }
else { st_set("cut_speaker", "") st_set("cut_text", "") }
}
# ── camera input ─────────────────────────────────────────────────────────────
fn view3d_camera_input(hot) {
bind yaw = st_get("ed_yaw", 0.6)
bind pitch = st_get("ed_pitch", 0.42)
bind dist = st_get("ed_dist", 16.0)
bind spd = st_get("orbit_speed", 1.0)
if hot && u_rpressed() { u_set_active("v3_orbit") st_set("v3_rx", u_mx()) st_set("v3_ry", u_my()) st_set("v3_moved", 0.0) }
if hot && u_mpressed() { u_set_active("v3_pan") }
if u_active() == "v3_orbit" && u_rdown() {
bind dx = u_dx()
bind dy = u_dy()
if abs(dx) + abs(dy) > 0.0 { st_set("v3_moved", st_get("v3_moved", 0.0) + abs(dx) + abs(dy)) }
if u_shift() { view3d_pan(dx, dy, dist) }
else {
bind yaw = yaw + dx * 0.0055 * spd
bind pitch = clampf(pitch + dy * 0.0055 * spd, 0.0 - 1.52, 1.52)
}
# fly with WASD while orbiting
bind fly = dist * u_dt() * 0.9
bind fwd = cam_forward()
bind rgt = cam_right()
bind mv = 0.0
if key_down("w") { st_set("ed_tx", st_get("ed_tx", 0.0) + list_get(fwd, 0) * fly) st_set("ed_tz", st_get("ed_tz", 0.0) + list_get(fwd, 2) * fly) st_set("ed_ty", st_get("ed_ty", 0.0) + list_get(fwd, 1) * fly) }
if key_down("s") { st_set("ed_tx", st_get("ed_tx", 0.0) - list_get(fwd, 0) * fly) st_set("ed_tz", st_get("ed_tz", 0.0) - list_get(fwd, 2) * fly) st_set("ed_ty", st_get("ed_ty", 0.0) - list_get(fwd, 1) * fly) }
if key_down("a") { st_set("ed_tx", st_get("ed_tx", 0.0) - list_get(rgt, 0) * fly) st_set("ed_tz", st_get("ed_tz", 0.0) - list_get(rgt, 2) * fly) }
if key_down("d") { st_set("ed_tx", st_get("ed_tx", 0.0) + list_get(rgt, 0) * fly) st_set("ed_tz", st_get("ed_tz", 0.0) + list_get(rgt, 2) * fly) }
if key_down("e") { st_set("ed_ty", st_get("ed_ty", 0.0) - fly) }
if key_down("q") { st_set("ed_ty", st_get("ed_ty", 0.0) + fly) }
}
if u_active() == "v3_orbit" && !u_rdown() {
u_set_active("")
if st_get("v3_moved", 0.0) < 4.0 && u_in(st_get("vp_x", 0.0), st_get("vp_y", 0.0), st_get("vp_w", 0.0), st_get("vp_h", 0.0)) {
view3d_hit(u_mx(), u_my())
ctx_open("viewport", "", u_mx(), u_my())
}
}
if u_active() == "v3_pan" {
if u_mdown() { view3d_pan(u_dx(), u_dy(), dist) } else { u_set_active("") }
}
if hot && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
bind dist = clampf(dist * pow(0.88, u_scroll()), 0.5, 2000.0)
st_set("wheel_used", 1.0)
}
st_set("ed_yaw", yaw)
st_set("ed_pitch", pitch)
st_set("ed_dist", dist)
cam_orbit(st_get("ed_tx", 0.0), st_get("ed_ty", 0.0), st_get("ed_tz", 0.0), yaw, pitch, dist, 2.0)
}
fn view3d_pan(dx, dy, dist) {
bind k = dist / max(1.0, st_get("vp_f", 600.0)) * 2.0
bind rgt = cam_right()
bind up = cam_up()
st_set("ed_tx", st_get("ed_tx", 0.0) - (list_get(rgt, 0) * dx + list_get(up, 0) * dy * (0.0 - 1.0)) * k)
st_set("ed_ty", st_get("ed_ty", 0.0) - (list_get(up, 1) * dy * (0.0 - 1.0)) * k)
st_set("ed_tz", st_get("ed_tz", 0.0) - (list_get(rgt, 2) * dx + list_get(up, 2) * dy * (0.0 - 1.0)) * k)
}
# ray from the mouse to the ground plane -> hit_x/y/z
fn view3d_hit(mx, my) {
bind ray = cam_ray(mx, my)
bind p = cam_ray_plane_y(ray, sc_get("floor", 0.0))
if len(p) < 3.0 {
st_set("hit_ok", 0.0)
return false
}
st_set("hit_ok", 1.0)
st_set("hit_x", sel(st_get("snap_on", 1.0) > 0.5, snap_v(list_get(p, 0)), list_get(p, 0)))
st_set("hit_y", list_get(p, 1))
st_set("hit_z", sel(st_get("snap_on", 1.0) > 0.5, snap_v(list_get(p, 2)), list_get(p, 2)))
return true
}
# ── picking and dragging nodes ───────────────────────────────────────────────
fn view3d_pick(mx, my, t) {
bind best = 0.0 - 1.0
bind bestd = 999999.0
bind n = nd_count()
bind i = 0.0
while i < n {
bind kind = nd_get(i, "kind")
if nd_get(i, "vis") > 0.5 && nd_get(i, "lock") < 0.5 && !nd_is_2d(kind) && kind != "plane" {
bind wp = nd_world_pos_t(i, t)
bind p = cam_project(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2))
if cam_visible(list_get(p, 2)) {
bind r = max(nd_get(i, "sx"), max(nd_get(i, "sy"), nd_get(i, "sz")))
if kind == "light" || kind == "camera" || kind == "spawn" || kind == "audio" || kind == "music" || kind == "midi" || kind == "empty" { bind r = 0.6 }
bind pr = max(10.0, r * cam_scale_at(list_get(p, 2)))
bind d = sqrt(pow(list_get(p, 0) - mx, 2.0) + pow(list_get(p, 1) - my, 2.0))
if d < pr && d - pr * 0.0 < bestd { bind best = i bind bestd = d - sel(d < 14.0, 1000.0, 0.0) }
}
}
bind i = i + 1.0
}
if best < 0.0 {
# planes last: hit by the ground ray
bind ray = cam_ray(mx, my)
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "plane" && nd_get(i, "vis") > 0.5 && nd_get(i, "lock") < 0.5 {
bind wp = nd_world_pos_t(i, t)
bind hp = cam_ray_plane_y(ray, list_get(wp, 1))
if len(hp) >= 3.0 {
if abs(list_get(hp, 0) - list_get(wp, 0)) <= nd_get(i, "sx") && abs(list_get(hp, 2) - list_get(wp, 2)) <= nd_get(i, "sz") { bind best = i }
}
}
bind i = i + 1.0
}
}
return best
}
fn view3d_pick_input(x, y, w, h, t) {
bind gz = st_get("gz_hover", "")
if u_pressed() && gz == "" && u_active() == "" {
bind pi = view3d_pick(u_mx(), u_my(), t)
if pi >= 0.0 {
bind id = nd_id(pi)
if u_ctrl() { sel_toggle(id) } else if !sel_has(id) { sel_set(id) }
if u_double() { do_action("view.focus") }
# body drag with the move tool
if st_get("tool", "move") == "move" && !u_ctrl() {
u_set_active("v3_move")
undo_push()
bind wp = nd_world_pos_t(pi, t)
st_set("v3_drag_y", list_get(wp, 1))
bind ray = cam_ray(u_mx(), u_my())
bind hp = cam_ray_plane_y(ray, list_get(wp, 1))
if len(hp) >= 3.0 { st_set("v3_ox", list_get(hp, 0) - nd_get(pi, "px")) st_set("v3_oz", list_get(hp, 2) - nd_get(pi, "pz")) } else { u_set_active("") }
st_set("v3_drag_id", id)
}
} else if !u_ctrl() { sel_clear() }
}
if u_active() == "v3_move" {
bind i = nd_find(st_get("v3_drag_id", 0.0))
if u_down() && i >= 0.0 {
bind ray = cam_ray(u_mx(), u_my())
bind hp = cam_ray_plane_y(ray, st_get("v3_drag_y", 0.0))
if len(hp) >= 3.0 {
bind nx = list_get(hp, 0) - st_get("v3_ox", 0.0)
bind nz = list_get(hp, 2) - st_get("v3_oz", 0.0)
if st_get("snap_on", 1.0) > 0.5 && !u_ctrl() { bind nx = snap_v(nx) bind nz = snap_v(nz) }
bind par = nd_get(i, "par")
if par > 0.0 {
bind lp = nd_local_from_world(par, nx, st_get("v3_drag_y", 0.0), nz)
set_prop(i, "px", list_get(lp, 0))
set_prop(i, "pz", list_get(lp, 2))
} else {
set_prop(i, "px", nx)
set_prop(i, "pz", nz)
}
}
} else { u_set_active("") }
}
}
# ── gizmo: axis handles for move / rotate / scale ────────────────────────────
fn view3d_gizmo(i, hot, t) {
bind wp = nd_world_pos_t(i, t)
bind ox = list_get(wp, 0)
bind oy = list_get(wp, 1)
bind oz = list_get(wp, 2)
bind p0 = cam_project(ox, oy, oz)
if !cam_visible(list_get(p0, 2)) { st_set("gz_hover", "") return 0.0 }
bind tool = st_get("tool", "move")
bind ln = 110.0 / max(20.0, cam_scale_at(list_get(p0, 2)))
bind px = cam_project(ox + ln, oy, oz)
bind py = cam_project(ox, oy - ln, oz)
bind pz = cam_project(ox, oy, oz + ln)
bind sx0 = list_get(p0, 0)
bind sy0 = list_get(p0, 1)
bind hover = ""
if u_active() == "" && hot {
if view3d_near(u_mx(), u_my(), list_get(px, 0), list_get(px, 1)) { bind hover = "x" }
else if view3d_near(u_mx(), u_my(), list_get(py, 0), list_get(py, 1)) { bind hover = "y" }
else if view3d_near(u_mx(), u_my(), list_get(pz, 0), list_get(pz, 1)) { bind hover = "z" }
else if tool == "rotate" && abs(sqrt(pow(u_mx() - sx0, 2.0) + pow(u_my() - sy0, 2.0)) - 44.0) < 8.0 { bind hover = "ring" }
else if tool == "scale" && sqrt(pow(u_mx() - sx0, 2.0) + pow(u_my() - sy0, 2.0)) < 10.0 { bind hover = "uni" }
}
st_set("gz_hover", hover)
bind act = st_get("gz_axis", "")
bind dragging = u_active() == "v3_gizmo"
if tool == "select" {
g_ring(sx0, sy0, 6.0, C_GIZMO)
return 0.0
}
# draw
bind cx = sel(hover == "x" || (dragging && act == "x"), C_WHITE, C_AXIS_X)
bind cy = sel(hover == "y" || (dragging && act == "y"), C_WHITE, C_AXIS_Y)
bind cz = sel(hover == "z" || (dragging && act == "z"), C_WHITE, C_AXIS_Z)
if tool == "move" || tool == "scale" {
px_line(sx0, sy0, list_get(px, 0), list_get(px, 1), cx, 1.0)
px_line(sx0, sy0, list_get(py, 0), list_get(py, 1), cy, 1.0)
px_line(sx0, sy0, list_get(pz, 0), list_get(pz, 1), cz, 1.0)
if tool == "move" {
view3d_arrow(sx0, sy0, list_get(px, 0), list_get(px, 1), cx)
view3d_arrow(sx0, sy0, list_get(py, 0), list_get(py, 1), cy)
view3d_arrow(sx0, sy0, list_get(pz, 0), list_get(pz, 1), cz)
} else {
g_rect(list_get(px, 0) - 5.0, list_get(px, 1) - 5.0, 10.0, 10.0, cx)
g_rect(list_get(py, 0) - 5.0, list_get(py, 1) - 5.0, 10.0, 10.0, cy)
g_rect(list_get(pz, 0) - 5.0, list_get(pz, 1) - 5.0, 10.0, 10.0, cz)
g_rect(sx0 - 5.0, sy0 - 5.0, 10.0, 10.0, sel(hover == "uni" || (dragging && act == "uni"), C_WHITE, C_GIZMO))
}
g_text(list_get(px, 0) + 6.0, list_get(px, 1) - 6.0, 10.0, cx, "X")
g_text(list_get(py, 0) + 6.0, list_get(py, 1) - 6.0, 10.0, cy, "Y")
g_text(list_get(pz, 0) + 6.0, list_get(pz, 1) - 6.0, 10.0, cz, "Z")
} else if tool == "rotate" {
g_ring(sx0, sy0, 44.0, sel(hover == "ring" || dragging, C_WHITE, C_AXIS_Y))
g_ring(sx0, sy0, 43.0, sel(hover == "ring" || dragging, C_WHITE, C_AXIS_Y))
sw_ring(ox, oy, oz, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, ln, 32.0, C_AXIS_Y, 0.5)
g_text(sx0 + 48.0, sy0 - 6.0, 10.0, C_AXIS_Y, "yaw " + fmt1(nd_get(i, "ry") * 57.29578))
}
# start a drag
if hover != "" && u_pressed() {
u_set_active("v3_gizmo")
st_set("gz_axis", hover)
st_set("gz_id", nd_id(i))
undo_push()
st_set("gz_x0", u_mx())
st_set("gz_y0", u_my())
st_set("gz_v0", [nd_get(i, "px"), nd_get(i, "py"), nd_get(i, "pz"), nd_get(i, "rx"), nd_get(i, "ry"), nd_get(i, "rz"), nd_get(i, "sx"), nd_get(i, "sy"), nd_get(i, "sz")])
st_set("gz_scale", cam_scale_at(list_get(p0, 2)))
# screen direction of each axis for move projection
st_set("gz_dx", [list_get(px, 0) - sx0, list_get(px, 1) - sy0, list_get(py, 0) - sx0, list_get(py, 1) - sy0, list_get(pz, 0) - sx0, list_get(pz, 1) - sy0])
}
if dragging {
bind gi = nd_find(st_get("gz_id", 0.0))
if !u_down() || gi < 0.0 { u_set_active("") return 0.0 }
bind v0 = st_get("gz_v0", [])
if len(v0) < 9.0 { return 0.0 }
bind mdx = u_mx() - st_get("gz_x0", 0.0)
bind mdy = u_my() - st_get("gz_y0", 0.0)
bind ax = st_get("gz_axis", "")
bind dirs = st_get("gz_dx", [])
if tool == "move" || (tool == "scale" && ax != "uni") {
bind k = sel(ax == "x", 0.0, sel(ax == "y", 2.0, 4.0))
bind adx = list_get(dirs, k)
bind ady = list_get(dirs, k + 1.0)
bind al = max(1.0, sqrt(adx * adx + ady * ady))
# pixels moved along the axis on screen -> world units (the axis is `ln` long)
bind units = (mdx * adx + mdy * ady) / al / al * ln
if tool == "move" {
bind sn = st_get("snap_on", 1.0) > 0.5 && !u_ctrl()
if ax == "x" { set_prop(gi, "px", sel(sn, snap_v(list_get(v0, 0) + units), list_get(v0, 0) + units)) }
if ax == "y" { set_prop(gi, "py", sel(sn, snap_v(list_get(v0, 1) - units), list_get(v0, 1) - units)) }
if ax == "z" { set_prop(gi, "pz", sel(sn, snap_v(list_get(v0, 2) + units), list_get(v0, 2) + units)) }
} else {
bind f = max(0.01, 1.0 + units / max(0.5, ln))
if ax == "x" { set_prop(gi, "sx", max(0.001, list_get(v0, 6) * f)) }
if ax == "y" { set_prop(gi, "sy", max(0.001, list_get(v0, 7) * f)) }
if ax == "z" { set_prop(gi, "sz", max(0.001, list_get(v0, 8) * f)) }
}
} else if tool == "scale" {
bind f = max(0.01, 1.0 + mdx / 120.0)
set_prop(gi, "sx", max(0.001, list_get(v0, 6) * f))
set_prop(gi, "sy", max(0.001, list_get(v0, 7) * f))
set_prop(gi, "sz", max(0.001, list_get(v0, 8) * f))
} else if tool == "rotate" {
bind ang = list_get(v0, 4) + mdx * 0.012
if st_get("snap_on", 1.0) > 0.5 && !u_ctrl() { bind ang = round(ang / 0.2617994) * 0.2617994 }
set_prop(gi, "ry", ang)
}
g_text(u_mx() + 14.0, u_my() + 14.0, 11.0, C_TEXT_STRONG, sel(tool == "rotate", fmt1(nd_get(gi, "ry") * 57.29578) + " deg", sel(tool == "scale", fmt2(nd_get(gi, "sx")) + " / " + fmt2(nd_get(gi, "sy")) + " / " + fmt2(nd_get(gi, "sz")), fmt2(nd_get(gi, "px")) + ", " + fmt2(nd_get(gi, "py")) + ", " + fmt2(nd_get(gi, "pz")))))
}
return 0.0
}
fn view3d_near(mx, my, px, py) { return abs(mx - px) < 10.0 && abs(my - py) < 10.0 }
fn view3d_arrow(x0, y0, x1, y1, c) {
bind dx = x1 - x0
bind dy = y1 - y0
bind l = max(1.0, sqrt(dx * dx + dy * dy))
bind ux = dx / l
bind uy = dy / l
g_tri(x1 + ux * 6.0, y1 + uy * 6.0, x1 - ux * 6.0 - uy * 5.0, y1 - uy * 6.0 + ux * 5.0, x1 - ux * 6.0 + uy * 5.0, y1 - uy * 6.0 - ux * 5.0, c)
}
# ── overlays: info, axis gizmo, camera-preview frame ─────────────────────────
fn view3d_overlay(x, y, w, h, preview, ci) {
bind kind = sc_get("kind", "3d")
bind label = sel(preview, sel(kind == "cutscene", "CUTSCENE CAMERA", "SCENE CAMERA: " + sel(ci >= 0.0, nd_get(ci, "name"), "")), sel(st_get("view_solid", 1.0) > 0.5, "SOLID", "WIRE"))
g_text(x + 12.0, y + 10.0, 11.0, C_TEXT_DIM, sc_get("name", "") + " " + label + " " + st_get("tool", "move"))
if preview {
g_frame(x + 2.0, y + 2.0, w - 4.0, h - 4.0, C_BLUE)
bind vw = sc_get("cw", 1280.0)
bind vh = sc_get("ch", 720.0)
# 16:9 safe frame
bind fw = w - 40.0
bind fh = fw * 9.0 / 16.0
if fh > h - 40.0 { bind fh = h - 40.0 bind fw = fh * 16.0 / 9.0 }
g_frame_a(x + (w - fw) * 0.5, y + (h - fh) * 0.5, fw, fh, C_BLUE, 0.5)
}
# small axis gizmo bottom-left
bind gx = x + 34.0
bind gy = y + h - 34.0
bind cy = cos(cam_yaw())
bind sy = sin(cam_yaw())
bind cp = cos(cam_pitch())
bind sp = sin(cam_pitch())
bind lx = 22.0
# project unit axes with rotation only
bind xx = cy * lx
bind xy = 0.0 - sp * (sy * lx)
bind zx = 0.0 - sy * lx
bind zy = 0.0 - sp * (cy * lx)
bind yy = 0.0 - cp * lx
px_line(gx, gy, gx + xx, gy + xy, C_AXIS_X, 1.0)
px_line(gx, gy, gx + zx, gy + zy, C_AXIS_Z, 1.0)
px_line(gx, gy, gx, gy + yy, C_AXIS_Y, 1.0)
g_text(gx + xx, gy + xy - 10.0, 9.0, C_AXIS_X, "x")
g_text(gx + zx, gy + zy - 10.0, 9.0, C_AXIS_Z, "z")
g_text(gx + 3.0, gy + yy - 10.0, 9.0, C_AXIS_Y, "y")
g_text_r(x + w - 12.0, y + 10.0, 11.0, C_TEXT_DIM, num_str(nd_count()) + " nodes " + fmt1(st_get("ed_dist", 16.0)) + " u")
if nd_count() < 0.5 { g_text_c(x + w * 0.5, y + h * 0.5 - 8.0, 14.0, C_TEXT_DIM, "Empty scene - Scene > Add, Shift+A, or drag an asset here") }
}