Profile
- Name
- Manage Bands v1.0
- ID
- 109218
- Shared with
- Public
- Parent
- None
- Children
- Created on
- October 23, 2025 at 20:11 PM UTC
- Updated on
- October 23, 2025 at 20:56 PM UTC
- Description
Best for
Code
-- .
-- Recipe to manage bands. It's a bit less friendly than Band Equalizer (https://fold.it/recipes/46757), but has more features.
--
-------------------------
-- Utilities
-------------------------
local function clamp(v, lo, hi)
if v < lo then return lo end
if v > hi then return hi end
return v
end
local function fmt(n, prec)
prec = prec or 3
if n == nil then return "nil" end
return string.format("%." .. tostring(prec) .. "f", n)
end
local function has_band_api()
return band and band.GetCount and band.GetResidueBase and band.GetResidueEnd
end
local function safe_pcall(f, ...)
if type(f) ~= "function" then return false, nil end
return pcall(f, ...)
end
-- Try to get actual band length if available.
local function safe_get_actual_length(i)
if band and band.GetLength then
local ok, val = pcall(band.GetLength, i)
if ok and type(val) == "number" then return val end
end
if band and band.GetGoalLength then
local ok, val = pcall(band.GetGoalLength, i)
if ok and type(val) == "number" then return val end
end
-- Fallback for segment-to-segment bands: use residue distance
if band and structure and structure.GetDistance then
local rb = band.GetResidueBase(i)
local re = band.GetResidueEnd(i)
if rb and re and rb > 0 and re > 0 then
local ok, dist = pcall(structure.GetDistance, rb, re)
if ok and type(dist) == "number" then return dist end
end
end
return nil
end
-- Sidechain detection: a band is considered sidechain if it references atoms
-- (non-zero atom indices), except the CA-CA case (atom index == 2 on both ends),
-- which should be treated as a segment-to-segment band.
local function is_sidechain(i)
if not band or not band.GetAtomBase or not band.GetAtomEnd then return false end
local ab = band.GetAtomBase(i) or 0
local ae = band.GetAtomEnd(i) or 0
if ab == 2 and ae == 2 then return false end
return (ab > 0) or (ae > 0)
end
local function band_enabled(i)
if band and band.IsEnabled then
local ok, val = pcall(band.IsEnabled, i)
if ok then return val and true or false end
end
-- Assume enabled if API not available
return true
end
-- Amino-acid mapping and helpers
local aa3_map = {
g = "Gly", a = "Ala", v = "Val", l = "Leu", i = "Ile",
m = "Met", f = "Phe", w = "Trp", p = "Pro", s = "Ser",
t = "Thr", c = "Cys", y = "Tyr", n = "Asn", q = "Gln",
d = "Asp", e = "Glu", k = "Lys", r = "Arg", h = "His",
}
local function get_aa3(i)
if not structure or not structure.GetAminoAcid or not i or i <= 0 then return "Unk" end
local ok, k = pcall(structure.GetAminoAcid, i)
if not ok or not k then return "Unk" end
k = string.lower(tostring(k))
return aa3_map[k] or "Unk"
end
local function seg_label(b)
local rb, re = b.residueBase or 0, b.residueEnd or 0
local ab, ae = b.atomBase or 0, b.atomEnd or 0
local aaB, aaE = get_aa3(rb), get_aa3(re)
-- Avoid '>' symbol to prevent HTML rendering issues in some UIs
return string.format("seg %d(%s)[%d] to %d(%s)[%d]", rb, aaB, ab, re, aaE, ae)
end
local function get_band_info(i)
local info = {
index = i,
residueBase = band.GetResidueBase and band.GetResidueBase(i) or 0,
residueEnd = band.GetResidueEnd and band.GetResidueEnd(i) or 0,
atomBase = band.GetAtomBase and band.GetAtomBase(i) or 0,
atomEnd = band.GetAtomEnd and band.GetAtomEnd(i) or 0,
strength = band.GetStrength and band.GetStrength(i) or nil,
goalLength = band.GetGoalLength and band.GetGoalLength(i) or nil,
actualLength= safe_get_actual_length(i),
isSidechain = is_sidechain(i),
isBiS = false,
isEnabled = band_enabled(i),
}
-- BiS detection: if any residue index is <= 0, treat as Bands in Space
if (info.residueBase or 0) <= 0 or (info.residueEnd or 0) <= 0 then
info.isBiS = true
info.isSidechain = false
end
return info
end
local function gather_all_bands()
local count = band.GetCount()
local list = {}
for i = 1, count do
list[#list + 1] = get_band_info(i)
end
return list
end
-- Compute global ranges for filters and actions
local function compute_ranges(items)
local smin, smax = math.huge, -math.huge
local lmin, lmax = math.huge, -math.huge
for _, b in ipairs(items) do
if b.strength ~= nil then
if b.strength < smin then smin = b.strength end
if b.strength > smax then smax = b.strength end
end
if b.goalLength ~= nil then
if b.goalLength < lmin then lmin = b.goalLength end
if b.goalLength > lmax then lmax = b.goalLength end
end
end
if smin == math.huge then smin, smax = 0.0, 0.0 end
if lmin == math.huge then lmin, lmax = 0.0, 0.0 end
return { smin = smin, smax = smax, lmin = lmin, lmax = lmax }
end
-- Build groups by (strength, length), both rounded to 3 decimals, sorted by strength then length
local function roundfmt(x)
if x == nil then return "nil" end
return string.format("%.3f", x)
end
local function compute_groups(items)
-- Build groups separately for enabled and disabled bands.
-- For each status:
-- 1) Build exact-value groups (rounded to 0.001) and keep only those with count >= 2.
-- 2) Collect remaining singletons and bin them into up to 10 ranges (equal width).
-- Concatenate in order: Enabled S exact -> Enabled S ranges -> Enabled L exact -> Enabled L ranges ->
-- Disabled S exact -> Disabled S ranges -> Disabled L exact -> Disabled L ranges
local function build_groups_for(arr, status_label)
local s_map = {}
local l_map = {}
local s_list, l_list = {}, {}
local s_singles, l_singles = {}, {}
local smin, smax = math.huge, -math.huge
local lmin, lmax = math.huge, -math.huge
for _, b in ipairs(arr) do
if b.strength ~= nil then
local s_str = roundfmt(b.strength)
local s_val = tonumber(s_str) or b.strength
if s_val < smin then smin = s_val end
if s_val > smax then smax = s_val end
local g = s_map[s_str]
if not g then
g = { kind='S', key=s_str, val=s_val, items={}, status=status_label }
s_map[s_str] = g
s_list[#s_list+1] = g
end
g.items[#g.items+1] = b
end
if b.goalLength ~= nil then
local l_str = roundfmt(b.goalLength)
local l_val = tonumber(l_str) or b.goalLength
if l_val < lmin then lmin = l_val end
if l_val > lmax then lmax = l_val end
local g2 = l_map[l_str]
if not g2 then
g2 = { kind='L', key=l_str, val=l_val, items={}, status=status_label }
l_map[l_str] = g2
l_list[#l_list+1] = g2
end
g2.items[#g2.items+1] = b
end
end
-- Split exact (count>=2) vs singletons (count==1)
local s_exact, l_exact = {}, {}
for _, g in ipairs(s_list) do
if #g.items >= 2 then s_exact[#s_exact+1] = g else for _, it in ipairs(g.items) do s_singles[#s_singles+1] = it end end
end
for _, g in ipairs(l_list) do
if #g.items >= 2 then l_exact[#l_exact+1] = g else for _, it in ipairs(g.items) do l_singles[#l_singles+1] = it end end
end
table.sort(s_exact, function(a,b) return a.val < b.val end)
table.sort(l_exact, function(a,b) return a.val < b.val end)
-- Bin singletons into up to 10 ranges
local function bin_singletons(kind, arr2, vmin, vmax, get_val)
if #arr2 == 0 then return {} end
local count = 10
local width = (vmax - vmin) / count
if width <= 0 then
local g = { kind = kind, isRange = true, lo = vmin, hi = vmax, key = roundfmt(vmin) .. ".." .. roundfmt(vmax), items = {}, status=status_label }
for _, it in ipairs(arr2) do g.items[#g.items+1] = it end
return { g }
end
local bins = {}
for i=1,count do
local lo = vmin + (i-1)*width
local hi = (i==count) and vmax or (vmin + i*width)
bins[i] = { kind = kind, isRange = true, lo = lo, hi = hi, key = roundfmt(lo) .. ".." .. roundfmt(hi), items = {}, status=status_label }
end
for _, it in ipairs(arr2) do
local val = get_val(it)
local idx = math.floor(((val - vmin) / width) + 1)
if idx < 1 then idx = 1 end
if idx > count then idx = count end
table.insert(bins[idx].items, it)
end
local compact = {}
for i=1,count do
if #bins[i].items > 0 then compact[#compact+1] = bins[i] end
end
return compact
end
-- Compute singles min/max separately to bin only singles span
local smin_single, smax_single = math.huge, -math.huge
for _, it in ipairs(s_singles) do
local v = it.strength or 0
if v < smin_single then smin_single = v end
if v > smax_single then smax_single = v end
end
if smin_single == math.huge then smin_single, smax_single = smin, smax end
local lmin_single, lmax_single = math.huge, -math.huge
for _, it in ipairs(l_singles) do
local v = it.goalLength or 0
if v < lmin_single then lmin_single = v end
if v > lmax_single then lmax_single = v end
end
if lmin_single == math.huge then lmin_single, lmax_single = lmin, lmax end
local s_ranges = bin_singletons('S', s_singles, smin_single, smax_single, function(it) return it.strength or 0 end)
local l_ranges = bin_singletons('L', l_singles, lmin_single, lmax_single, function(it) return it.goalLength or 0 end)
local groups = {}
for _, g in ipairs(s_exact) do groups[#groups+1] = g end
for _, g in ipairs(s_ranges) do groups[#groups+1] = g end
for _, g in ipairs(l_exact) do groups[#groups+1] = g end
for _, g in ipairs(l_ranges) do groups[#groups+1] = g end
return groups
end
local enabled_items, disabled_items = {}, {}
for _, b in ipairs(items) do
if b.isEnabled then enabled_items[#enabled_items+1] = b else disabled_items[#disabled_items+1] = b end
end
local groups_all = {}
local en_groups = build_groups_for(enabled_items, 'enabled')
for _, g in ipairs(en_groups) do groups_all[#groups_all+1] = g end
local dis_groups = build_groups_for(disabled_items, 'disabled')
for _, g in ipairs(dis_groups) do groups_all[#groups_all+1] = g end
for i, g in ipairs(groups_all) do g.id = i end
return groups_all
end
local function print_groups(groups)
print("Groups by status: exact equals (count>=2) by Strength [S] and Length [L]; singles are binned into ranges:")
if not groups or #groups == 0 then
print(" (no groups)")
return
end
-- Helper to compute counterpart range label
local function counterpart_range(g)
local mn, mx
if g.kind == 'S' then
for _, it in ipairs(g.items) do
local v = it.goalLength
if v ~= nil then
mn = (mn == nil or v < mn) and v or mn
mx = (mx == nil or v > mx) and v or mx
end
end
if mn == nil then return "" end
return string.format(" | L=%s..%s", roundfmt(mn), roundfmt(mx))
else
for _, it in ipairs(g.items) do
local v = it.strength
if v ~= nil then
mn = (mn == nil or v < mn) and v or mn
mx = (mx == nil or v > mx) and v or mx
end
end
if mn == nil then return "" end
return string.format(" | S=%s..%s", roundfmt(mn), roundfmt(mx))
end
end
local en_s_lines, en_l_lines, dis_s_lines, dis_l_lines = {}, {}, {}, {}
for _, g in ipairs(groups) do
local status_tag = (g.status == 'disabled') and "[DIS] " or "[EN] "
local prefix = status_tag .. ((g.kind == 'S') and "[S] S=" or "[L] L=")
local label = g.key
if g.isRange then
if #g.items == 1 then
local it = g.items[1]
label = (g.kind == 'S') and roundfmt(it.strength) or roundfmt(it.goalLength)
else
-- For range groups with multiple items, show actual min..max across items
local mn, mx
if g.kind == 'S' then
for _, it in ipairs(g.items) do
local v = it.strength
if v ~= nil then
mn = (mn == nil or v < mn) and v or mn
mx = (mx == nil or v > mx) and v or mx
end
end
else
for _, it in ipairs(g.items) do
local v = it.goalLength
if v ~= nil then
mn = (mn == nil or v < mn) and v or mn
mx = (mx == nil or v > mx) and v or mx
end
end
end
if mn ~= nil and mx ~= nil then
if math.abs(mx - mn) < 1e-12 then
label = roundfmt(mn)
else
label = roundfmt(mn) .. ".." .. roundfmt(mx)
end
end
end
end
local tail = counterpart_range(g)
local line = string.format(" #%d %s%s%s | count=%d", g.id, prefix, label, tail, #g.items)
if g.status == 'disabled' then
if g.kind == 'S' then dis_s_lines[#dis_s_lines+1] = line else dis_l_lines[#dis_l_lines+1] = line end
else
if g.kind == 'S' then en_s_lines[#en_s_lines+1] = line else en_l_lines[#en_l_lines+1] = line end
end
end
if #en_s_lines + #en_l_lines > 0 then
print("Enabled:")
for _, line in ipairs(en_s_lines) do print(line) end
if #en_l_lines > 0 then print("-") end
for _, line in ipairs(en_l_lines) do print(line) end
end
if #dis_s_lines + #dis_l_lines > 0 then
if #en_s_lines + #en_l_lines > 0 then print("=") end
print("Disabled:")
for _, line in ipairs(dis_s_lines) do print(line) end
if #dis_l_lines > 0 then print("-") end
for _, line in ipairs(dis_l_lines) do print(line) end
end
end
local function update_state(cfg)
local items = gather_all_bands()
cfg.items = items
cfg.ranges = compute_ranges(items)
cfg.groups = compute_groups(items)
-- Initialize default filter ranges to the full existing ranges, once
if not cfg.defaults_set then
cfg.strength_min = cfg.ranges.smin
cfg.strength_max = cfg.ranges.smax
cfg.length_min = cfg.ranges.lmin
cfg.length_max = cfg.ranges.lmax
cfg.defaults_set = true
end
end
-------------------------
-- Reporting
-------------------------
local function stats(values)
local n = #values
if n == 0 then return {n=0, min=nil, max=nil, avg=nil} end
local mn, mx = values[1], values[1]
local sum = 0
for i=1,n do
local v = values[i]
if v ~= nil then
if v < mn then mn = v end
if v > mx then mx = v end
sum = sum + v
end
end
return { n = n, min = mn, max = mx, avg = sum / n }
end
-- Histograms removed per new requirements
local function print_group_stats(group_name, arr)
local strengths, lengths = {}, {}
for _, b in ipairs(arr) do
if b.strength ~= nil then strengths[#strengths+1] = b.strength end
if b.goalLength ~= nil then lengths[#lengths+1] = b.goalLength end
end
local sst = stats(strengths)
local lst = stats(lengths)
print(string.format("%s | count=%d", group_name, #arr))
print(string.format(" Strengths: n=%d, min=%s, max=%s, avg=%s",
sst.n, fmt(sst.min), fmt(sst.max), fmt(sst.avg)))
print(string.format(" Lengths: n=%d, min=%s, max=%s, avg=%s",
lst.n, fmt(lst.min), fmt(lst.max), fmt(lst.avg)))
end
local function print_report(cfg)
local count = band.GetCount()
if count == 0 then
print("No bands found.")
return
end
local items = gather_all_bands()
print(string.format("Bands total: %d", #items))
local seg_enabled, seg_disabled, side_enabled, side_disabled, bis_enabled, bis_disabled = {}, {}, {}, {}, {}, {}
for _, b in ipairs(items) do
if b.isBiS then
if b.isEnabled then bis_enabled[#bis_enabled+1] = b else bis_disabled[#bis_disabled+1] = b end
elseif b.isSidechain then
if b.isEnabled then side_enabled[#side_enabled+1] = b else side_disabled[#side_disabled+1] = b end
else
if b.isEnabled then seg_enabled[#seg_enabled+1] = b else seg_disabled[#seg_disabled+1] = b end
end
end
print_group_stats("Segment bands (enabled)", seg_enabled)
print_group_stats("Segment bands (disabled)", seg_disabled)
print_group_stats("Sidechain bands (enabled)", side_enabled)
print_group_stats("Sidechain bands (disabled)", side_disabled)
print_group_stats("BiS bands (enabled)", bis_enabled)
print_group_stats("BiS bands (disabled)", bis_disabled)
-- Histograms removed
-- Show top extremes
table.sort(items, function(a,b)
local la = a.goalLength or math.huge
local lb = b.goalLength or math.huge
if la == lb then return (a.index or 0) < (b.index or 0) end
return la < lb
end)
print("\nShortest bands (up to 5):")
for i=1, math.min(5, #items) do
local b = items[i]
local t = b.isBiS and "BiS" or (b.isSidechain and "sidechain" or "segment")
print(string.format(
" #%d %s | goal=%s L=%s | S=%s | %s | %s",
b.index, seg_label(b),
fmt(b.goalLength), fmt(b.actualLength), fmt(b.strength), t,
b.isEnabled and "enabled" or "disabled"
))
end
table.sort(items, function(a,b)
local la = a.goalLength or -math.huge
local lb = b.goalLength or -math.huge
if la == lb then return (a.index or 0) < (b.index or 0) end
return la > lb
end)
print("\nLongest bands (up to 5):")
for i=1, math.min(5, #items) do
local b = items[i]
local t = b.isBiS and "BiS" or (b.isSidechain and "sidechain" or "segment")
print(string.format(
" #%d %s | goal=%s L=%s | S=%s | %s | %s",
b.index, seg_label(b),
fmt(b.goalLength), fmt(b.actualLength), fmt(b.strength), t,
b.isEnabled and "enabled" or "disabled"
))
end
print("\nAll bands (full list):")
table.sort(items, function(a,b) return (a.index or 0) < (b.index or 0) end)
for _, b in ipairs(items) do
local t = b.isBiS and "BiS" or (b.isSidechain and "sidechain" or "segment")
print(string.format(
" #%d type=%s status=%s | S=%s goal=%s L=%s | %s",
b.index, t,
b.isEnabled and "enabled" or "disabled",
fmt(b.strength), fmt(b.goalLength), fmt(b.actualLength),
seg_label(b)
))
end
end
-------------------------
-- Filtering and actions
-------------------------
local EPS = 1e-6
local function passes_filters(b, cfg)
-- Type filter
if b.isBiS and not cfg.include_bis then return false end
if (not b.isBiS) then
if b.isSidechain and not cfg.include_sidechain then return false end
if (not b.isSidechain) and not cfg.include_segment then return false end
end
-- Enabled filter
if b.isEnabled and not cfg.include_enabled then return false end
if (not b.isEnabled) and not cfg.include_disabled then return false end
-- Strength range (always on; treat missing values as pass-through)
local s = b.strength
if s ~= nil then
if cfg.strength_min and s < cfg.strength_min - EPS then return false end
if cfg.strength_max and s > cfg.strength_max + EPS then return false end
end
-- Length range (uses goal length; always on; treat missing as pass-through)
local L = b.goalLength
if L ~= nil then
if cfg.length_min and L < cfg.length_min - EPS then return false end
if cfg.length_max and L > cfg.length_max + EPS then return false end
end
return true
end
local function select_bands(cfg)
local base = {}
if cfg.group_index and cfg.group_index > 0 and cfg.groups and cfg.groups[cfg.group_index] then
base = cfg.groups[cfg.group_index].items
else
base = gather_all_bands()
end
local out = {}
for _, b in ipairs(base) do
if passes_filters(b, cfg) then out[#out+1] = b end
end
return out
end
-- Action types
-- Actions are configured via checkboxes and two sliders now
local function apply_action(selection, cfg)
local n = #selection
if n == 0 then
print("No bands match the current filters — nothing to apply.")
return
end
local updated = 0
undo.SetUndo(false)
for _, b in ipairs(selection) do
local i = b.index
-- Set Strength
if cfg.act_set_strength and band.SetStrength and cfg.action_strength_value then
local newv = clamp(cfg.action_strength_value, 0.0, 10.0)
band.SetStrength(i, newv)
updated = updated + 1
end
-- Set Length (goal)
if cfg.act_set_length and band.SetGoalLength and cfg.action_length_value then
local L = math.max(0.01, cfg.action_length_value)
band.SetGoalLength(i, L)
updated = updated + 1
end
-- Set Goal Length = Actual length via band.GetLength
if cfg.act_len_to_actual and band.SetGoalLength and band.GetLength then
local okL, L = pcall(band.GetLength, i)
if okL and type(L) == 'number' and L > 0 then
band.SetGoalLength(i, L)
updated = updated + 1
end
end
-- Enable/Disable
if cfg.act_enable and band.Enable then
pcall(band.Enable, i)
updated = updated + 1
end
if cfg.act_disable and band.Disable then
pcall(band.Disable, i)
updated = updated + 1
end
end
undo.SetUndo(true)
-- Fallback: only if no actions at all are checked
-- If nothing was updated and values changed vs remembered defaults,
-- auto-apply those changed values (warn user).
local no_actions_checked = not (
(cfg.act_set_strength or false) or
(cfg.act_set_length or false) or
(cfg.act_len_to_actual or false) or
(cfg.act_enable or false) or
(cfg.act_disable or false) or
(cfg.act_remove or false)
)
if updated == 0 and n > 0 and no_actions_checked then
local warn_msgs = {}
local did_any = false
local strength_changed = (cfg.action_strength_value ~= nil and cfg.def_strength_value ~= nil and math.abs(cfg.action_strength_value - cfg.def_strength_value) > EPS)
local length_changed = (cfg.action_length_value ~= nil and cfg.def_length_value ~= nil and math.abs(cfg.action_length_value - cfg.def_length_value) > EPS)
if (strength_changed or length_changed) then
undo.SetUndo(false)
for _, b in ipairs(selection) do
local i = b.index
if strength_changed and band.SetStrength then
local newv = clamp(cfg.action_strength_value, 0.0, 10.0)
band.SetStrength(i, newv)
updated = updated + 1
did_any = true
end
if length_changed and band.SetGoalLength then
local L = math.max(0.01, cfg.action_length_value)
band.SetGoalLength(i, L)
updated = updated + 1
did_any = true
end
end
undo.SetUndo(true)
if strength_changed then warn_msgs[#warn_msgs+1] = string.format("Strength default changed: %s to %s", fmt(cfg.def_strength_value), fmt(cfg.action_strength_value)) end
if length_changed then warn_msgs[#warn_msgs+1] = string.format("Length default changed: %s to %s", fmt(cfg.def_length_value), fmt(cfg.action_length_value)) end
if did_any then
print("Note: No actions were checked. Applied changed defaults to selection:")
for _, m in ipairs(warn_msgs) do print(" - " .. m) end
end
end
end
local removed, failed = 0, 0
if cfg.act_remove and band.Delete then
-- Delete after applying other actions; use descending order
table.sort(selection, function(a,b) return a.index > b.index end)
undo.SetUndo(false)
for _, b in ipairs(selection) do
local okd = pcall(band.Delete, b.index)
if okd then removed = removed + 1 else failed = failed + 1 end
end
undo.SetUndo(true)
end
-- Remember current action values as new defaults
cfg.def_strength_value = cfg.action_strength_value or cfg.def_strength_value
cfg.def_length_value = cfg.action_length_value or cfg.def_length_value
if cfg.act_remove then
print(string.format("Applied changes to %d bands (selected=%d). Removed=%d, failed=%d", updated, n, removed, failed))
else
print(string.format("Applied changes to %d bands (selected=%d)", updated, n))
end
end
local function preview_selection(selection)
print(string.format("Preview: %d bands selected.", #selection))
for _, b in ipairs(selection) do
local t = b.isBiS and "BiS" or (b.isSidechain and "sidechain" or "segment")
print(string.format(
" #%d %s | L=%s | S=%s | %s | %s",
b.index, seg_label(b),
fmt(b.actualLength), fmt(b.strength), t,
b.isEnabled and "enabled" or "disabled"
))
end
end
-- Export selected bands to compact string (for copy/paste) and pretty list
local function serialize_compact(tbl)
if not tbl or #tbl == 0 then return "" end
local out = {"{"}
for i, b in ipairs(tbl) do
local piece = string.format(
"{rb=%d,re=%d,ab=%d,ae=%d,s=%s,gl=%s,al=%s,en=%s}",
tonumber(b.residueBase or 0) or 0,
tonumber(b.residueEnd or 0) or 0,
tonumber(b.atomBase or 0) or 0,
tonumber(b.atomEnd or 0) or 0,
b.strength ~= nil and string.format("%.6f", b.strength) or "nil",
b.goalLength ~= nil and string.format("%.6f", b.goalLength) or "nil",
b.actualLength ~= nil and string.format("%.6f", b.actualLength) or "nil",
tostring(b.isEnabled ~= false)
)
out[#out + 1] = piece .. (i < #tbl and "," or "")
end
out[#out + 1] = "}"
return table.concat(out)
end
local function export_selection(selection)
print(string.format("Export: %d bands selected.", #selection))
for _, b in ipairs(selection) do
local t = b.isBiS and "BiS" or (b.isSidechain and "sidechain" or "segment")
print(string.format(
" #%d type=%s status=%s | S=%s goal=%s L=%s | %s",
b.index, t,
b.isEnabled and "enabled" or "disabled",
fmt(b.strength), fmt(b.goalLength), fmt(b.actualLength),
seg_label(b)
))
end
local compact = serialize_compact(selection)
if compact ~= "" then
print("\nSerialized (compact):\n" .. compact .. "\n")
end
end
-------------------------
-- Help/Instructions
-------------------------
local function print_help()
print("")
end
-------------------------
-- Dialog / UI
-------------------------
local function build_dialog(cfg)
local dlg = dialog.CreateDialog("Manage Bands")
-- Group selection
local gcount = (cfg.groups and #cfg.groups) or 0
local grp_val = clamp(cfg.group_index or 0, 0, gcount)
dlg.lblG = dialog.AddLabel(string.format("Groups available: %d", gcount))
if gcount > 0 then
dlg.grp_idx = dialog.AddSlider("Select group (0=all)", grp_val, 0, gcount, 0)
else
dlg.grp_note = dialog.AddLabel("No groups to select (0 = all)")
end
-- Type/status filters
dlg.f_seg = dialog.AddCheckbox("Include seg-seg bands", cfg.include_segment ~= false)
dlg.f_atom = dialog.AddCheckbox("Include sidechain bands", cfg.include_sidechain ~= false)
dlg.f_bis = dialog.AddCheckbox("Include bands in space", cfg.include_bis ~= false)
dlg.f_en = dialog.AddCheckbox("Include enabled bands", cfg.include_enabled ~= false)
dlg.f_dis= dialog.AddCheckbox("Include disabled bands", cfg.include_disabled ~= false)
-- Strength range (always active)
local smin = cfg.ranges and cfg.ranges.smin or 0.0
local smax = cfg.ranges and cfg.ranges.smax or 10.0
if smax < smin then smax = smin end
if not (smax > smin) then
local pad = math.max(0.01, math.abs(smin) * 0.1)
smin = math.max(0.0, smin - pad)
smax = smax + pad
if not (smax > smin) then smax = smin + 0.01 end
end
local s_val_min = clamp(cfg.strength_min or smin, smin, smax)
local s_val_max = clamp(cfg.strength_max or smax, smin, smax)
dlg.f_s_min = dialog.AddSlider("Min strength", s_val_min, smin, smax, 2)
dlg.f_s_max = dialog.AddSlider("Max strength", s_val_max, smin, smax, 2)
-- Length range (always active)
local lmin = cfg.ranges and cfg.ranges.lmin or 0.0
local lmax = cfg.ranges and cfg.ranges.lmax or 10.0
if lmax < lmin then lmax = lmin end
if not (lmax > lmin) then
local pad = math.max(0.01, math.abs(lmin) * 0.1)
lmin = math.max(0.0, lmin - pad)
lmax = lmax + pad
if not (lmax > lmin) then lmax = lmin + 0.01 end
end
local l_val_min = clamp(cfg.length_min or lmin, lmin, lmax)
local l_val_max = clamp(cfg.length_max or lmax, lmin, lmax)
dlg.f_l_min = dialog.AddSlider("Min length", l_val_min, lmin, lmax, 2)
dlg.f_l_max = dialog.AddSlider("Max length", l_val_max, lmin, lmax, 2)
-- Action config via checkboxes
dlg.lblAct = dialog.AddLabel("Actions:")
dlg.act_s_on = dialog.AddCheckbox("Set strength", cfg.act_set_strength or false)
local act_s_val = clamp(cfg.action_strength_value or 1.0, 0.0, 100.0)
dlg.act_s_val = dialog.AddSlider("Strength value", act_s_val, 0.0, 10.0, 2)
local l_upper = math.max((cfg.ranges and cfg.ranges.lmax or 0.0) * 10.0, 10.0)
dlg.act_l_on = dialog.AddCheckbox("Set length", cfg.act_set_length or false)
local act_l_val = clamp(cfg.action_length_value or 2.0, 0.0, l_upper)
dlg.act_l_val = dialog.AddSlider("Length value", act_l_val, 0.0, l_upper, 2)
dlg.act_en = dialog.AddCheckbox("Enable", cfg.act_enable or false)
dlg.act_dis = dialog.AddCheckbox("Disable", cfg.act_disable or false)
dlg.act_rm = dialog.AddCheckbox("Remove", cfg.act_remove or false)
dlg.act_l_actual = dialog.AddCheckbox("Len = actual", cfg.act_len_to_actual or false)
-- Buttons (order per request)
dlg.btn_exit = dialog.AddButton("Exit", 0)
dlg.btn_report = dialog.AddButton("Report", 10)
dlg.btn_preview = dialog.AddButton("Preview", 11)
dlg.btn_apply = dialog.AddButton("Apply", 12)
return dlg
end
local function read_dialog(dlg, cfg)
cfg = cfg or {}
-- Type/status
cfg.include_segment = dlg.f_seg and dlg.f_seg.value or false
cfg.include_sidechain = dlg.f_atom and dlg.f_atom.value or false
cfg.include_bis = dlg.f_bis and dlg.f_bis.value or false
cfg.include_enabled = dlg.f_en and dlg.f_en.value or false
cfg.include_disabled = dlg.f_dis and dlg.f_dis.value or false
-- Strength
cfg.strength_min = dlg.f_s_min and dlg.f_s_min.value or cfg.strength_min
cfg.strength_max = dlg.f_s_max and dlg.f_s_max.value or cfg.strength_max
if cfg.strength_min and cfg.strength_max and cfg.strength_min > cfg.strength_max then
cfg.strength_min, cfg.strength_max = cfg.strength_max, cfg.strength_min
end
-- Length
cfg.length_min = dlg.f_l_min and dlg.f_l_min.value or cfg.length_min
cfg.length_max = dlg.f_l_max and dlg.f_l_max.value or cfg.length_max
if cfg.length_min and cfg.length_max and cfg.length_min > cfg.length_max then
cfg.length_min, cfg.length_max = cfg.length_max, cfg.length_min
end
-- Group
cfg.group_index = math.floor(dlg.grp_idx and dlg.grp_idx.value or 0)
-- Actions
cfg.act_set_strength = dlg.act_s_on and dlg.act_s_on.value or false
cfg.action_strength_value = dlg.act_s_val and dlg.act_s_val.value or cfg.action_strength_value
cfg.act_set_length = dlg.act_l_on and dlg.act_l_on.value or false
cfg.action_length_value = dlg.act_l_val and dlg.act_l_val.value or cfg.action_length_value
cfg.act_len_to_actual = dlg.act_l_actual and dlg.act_l_actual.value or false
cfg.act_enable = dlg.act_en and dlg.act_en.value or false
cfg.act_disable = dlg.act_dis and dlg.act_dis.value or false
cfg.act_remove = dlg.act_rm and dlg.act_rm.value or false
return cfg
end
-------------------------
-- Main loop
-------------------------
local function main()
if not has_band_api() then
print("Band API is not available in this environment.")
return
end
undo.SetUndo(false)
local cfg = {
-- Filters defaults
include_segment = true,
include_sidechain = true,
include_bis = true,
include_enabled = true,
include_disabled = true,
strength_min = nil,
strength_max = nil,
length_min = nil,
length_max = nil,
-- Group & action defaults
group_index = 0,
action_strength_value = 1.0,
action_length_value = 2.0,
-- Remember last applied defaults for auto-apply fallback
def_strength_value = 1.0,
def_length_value = 2.0,
}
-- Initial state and prints
update_state(cfg)
print_help()
print("")
print_groups(cfg.groups)
while true do
update_state(cfg)
local dlg = build_dialog(cfg)
local rv = dialog.Show(dlg)
cfg = read_dialog(dlg, cfg)
if rv == 0 then
print("Exit requested by user.")
return
elseif rv == 10 then
print_report(cfg)
elseif rv == 11 then
local sel = select_bands(cfg)
preview_selection(sel)
elseif rv == 12 then
local sel = select_bands(cfg)
apply_action(sel, cfg)
break
else
-- Unknown button or closed: loop again
end
end
undo.SetUndo(true)
save.Quicksave(100)
save.Quickload(100)
end
-- Entry
main()