Added/Implemented machi-editor
This commit is contained in:
942
lib/layout-machi/engine.lua
Normal file
942
lib/layout-machi/engine.lua
Normal file
@@ -0,0 +1,942 @@
|
||||
-- area {
|
||||
-- x, y, width, height
|
||||
-- parent_id
|
||||
-- parent_cid
|
||||
-- parent_x_shares
|
||||
-- parent_y_shares
|
||||
-- habitable
|
||||
-- hole (unique)
|
||||
-- }
|
||||
--
|
||||
-- split {
|
||||
-- method
|
||||
-- x_shares
|
||||
-- y_shares
|
||||
-- children
|
||||
-- }
|
||||
--
|
||||
-- share {weight, adjustment, dynamic, minimum}
|
||||
local in_module = ...
|
||||
|
||||
-- Split a length by `measures`, such that each split respect the
|
||||
-- weight [1], adjustment (user [2] + engine [3]) without breaking the minimum size [4].
|
||||
--
|
||||
-- The split algorithm has a worst case of O(n^2) where n = #shares,
|
||||
-- which should be fine for practical usage of screen partitions.
|
||||
-- Using geometric algorithm this can be optimized to O(n log n), but
|
||||
-- I don't think it is worth.
|
||||
|
||||
-- Returns two values:
|
||||
-- 1. the (accumulative) result if it is possible to give every share its minimum size, otherwise nil.
|
||||
-- 2. any spare space to adjust without capping any share.
|
||||
local function fair_split(length, shares)
|
||||
local ret = {}
|
||||
local normalized_adj = nil
|
||||
local sum_weight
|
||||
local sum_adj
|
||||
local remaining = #shares
|
||||
local spare = nil
|
||||
local need_recompute
|
||||
repeat
|
||||
need_recompute = false
|
||||
sum_weight = 0
|
||||
sum_adj = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
sum_weight = sum_weight + shares[i][1]
|
||||
if normalized_adj then
|
||||
sum_adj = sum_adj + normalized_adj[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if normalized_adj == nil then
|
||||
normalized_adj = {}
|
||||
for i = 1, #shares do
|
||||
if sum_weight > shares[i][1] then
|
||||
normalized_adj[i] = ((shares[i][2] or 0) + (shares[i][3] or 0)) * sum_weight / (sum_weight - shares[i][1])
|
||||
else
|
||||
normalized_adj[i] = 0
|
||||
end
|
||||
sum_adj = sum_adj + normalized_adj[i]
|
||||
end
|
||||
|
||||
for i = 1, #shares do
|
||||
local required = (shares[i][4] - normalized_adj[i]) * sum_weight / shares[i][1] + sum_adj
|
||||
if spare == nil or spare > length - required then
|
||||
spare = length - required
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local capped_length = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
local split = (length - sum_adj) * shares[i][1] / sum_weight + normalized_adj[i]
|
||||
if split < shares[i][4] then
|
||||
ret[i] = shares[i][4]
|
||||
capped_length = capped_length + shares[i][4]
|
||||
need_recompute = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
length = length - capped_length
|
||||
until not need_recompute
|
||||
|
||||
if #shares == 1 or spare < 0 then
|
||||
spare = 0
|
||||
end
|
||||
|
||||
if remaining == 0 then
|
||||
return nil, spare
|
||||
end
|
||||
|
||||
local acc_weight = 0
|
||||
local acc_adj = 0
|
||||
local acc_ret = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
acc_weight = acc_weight + shares[i][1]
|
||||
acc_adj = acc_adj + normalized_adj[i]
|
||||
ret[i] = remaining == 1 and length - acc_ret or math.floor((length - sum_adj) / sum_weight * acc_weight + acc_adj - acc_ret + 0.5)
|
||||
acc_ret = acc_ret + ret[i]
|
||||
remaining = remaining - 1
|
||||
end
|
||||
end
|
||||
|
||||
ret[0] = 0
|
||||
for i = 1, #shares do
|
||||
ret[i] = ret[i - 1] + ret[i]
|
||||
end
|
||||
|
||||
return ret, spare
|
||||
end
|
||||
|
||||
-- Static data
|
||||
|
||||
-- Command character info
|
||||
-- 3 for taking the arg string and an open area
|
||||
-- 2 for taking an open area
|
||||
-- 1 for taking nothing
|
||||
-- 0 for args
|
||||
local ch_info = {
|
||||
["h"] = 3, ["H"] = 3,
|
||||
["v"] = 3, ["V"] = 3,
|
||||
["w"] = 3, ["W"] = 3,
|
||||
["d"] = 3, ["D"] = 3,
|
||||
["s"] = 3,
|
||||
["t"] = 3,
|
||||
["c"] = 3,
|
||||
["x"] = 3,
|
||||
["-"] = 2,
|
||||
["/"] = 2,
|
||||
["."] = 1,
|
||||
[";"] = 1,
|
||||
["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0,
|
||||
["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0,
|
||||
["_"] = 0, [","] = 0,
|
||||
}
|
||||
|
||||
local function parse_arg_str(arg_str, default)
|
||||
local ret = {}
|
||||
local current = {}
|
||||
if #arg_str == 0 then return ret end
|
||||
local index = 1
|
||||
local split_mode = arg_str:find("[,_]") ~= nil
|
||||
|
||||
local p = index
|
||||
while index <= #arg_str do
|
||||
local ch = arg_str:sub(index, index)
|
||||
if split_mode then
|
||||
if ch == "_" then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
p = index + 1
|
||||
elseif ch == "," then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
ret[#ret + 1] = current
|
||||
current = {}
|
||||
p = index + 1
|
||||
end
|
||||
else
|
||||
local r = tonumber(ch)
|
||||
if r == nil then
|
||||
ret[#ret + 1] = {default}
|
||||
else
|
||||
ret[#ret + 1] = {r}
|
||||
end
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
if split_mode then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
ret[#ret + 1] = current
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
if not in_module then
|
||||
print("Testing parse_arg_str")
|
||||
local x = parse_arg_str("1234", 0)
|
||||
assert(#x == 4)
|
||||
assert(#x[1] == 1 and x[1][1] == 1)
|
||||
assert(#x[2] == 1 and x[2][1] == 2)
|
||||
assert(#x[3] == 1 and x[3][1] == 3)
|
||||
assert(#x[4] == 1 and x[4][1] == 4)
|
||||
local x = parse_arg_str("12_34_,", -1)
|
||||
assert(#x == 2)
|
||||
assert(#x[1] == 3 and x[1][1] == 12 and x[1][2] == 34 and x[1][3] == -1)
|
||||
assert(#x[2] == 1 and x[2][1] == -1)
|
||||
local x = parse_arg_str("12_34,56_,78_90_", -1)
|
||||
assert(#x == 3)
|
||||
assert(#x[1] == 2 and x[1][1] == 12 and x[1][2] == 34)
|
||||
assert(#x[2] == 2 and x[2][1] == 56 and x[2][2] == -1)
|
||||
assert(#x[3] == 3 and x[3][1] == 78 and x[3][2] == 90 and x[3][3] == -1)
|
||||
print("Passed.")
|
||||
end
|
||||
|
||||
local max_split = 1000
|
||||
local max_areas = 10000
|
||||
local default_expansion = 2
|
||||
|
||||
-- Execute a (partial) command, returns:
|
||||
-- 1. Closed areas: areas that will not be further partitioned by further input.
|
||||
-- 2. Open areas: areas that can be further partitioned.
|
||||
-- 3. Pending: if the command can take more argument into the last command.
|
||||
local function areas_from_command(command, workarea, minimum)
|
||||
local pending_op = nil
|
||||
local arg_str = ""
|
||||
local closed_areas = {}
|
||||
local open_areas
|
||||
local b = require("beautiful")
|
||||
local root = {
|
||||
expansion = default_expansion,
|
||||
x = workarea.x,
|
||||
y = workarea.y,
|
||||
width = workarea.width,
|
||||
height = workarea.height,
|
||||
bl = true,
|
||||
br = true,
|
||||
bu = true,
|
||||
bd = true,
|
||||
}
|
||||
|
||||
local function close_area()
|
||||
local a = open_areas[#open_areas]
|
||||
table.remove(open_areas, #open_areas)
|
||||
local i = #closed_areas + 1
|
||||
closed_areas[i] = a
|
||||
a.id = i
|
||||
a.habitable = true
|
||||
return a, i
|
||||
end
|
||||
|
||||
local function push_open_areas(areas)
|
||||
for i = #areas, 1, -1 do
|
||||
open_areas[#open_areas + 1] = areas[i]
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_op(method)
|
||||
local l = method:lower()
|
||||
local alt = method ~= l
|
||||
method = l
|
||||
|
||||
if method == "h" or method == "v" then
|
||||
|
||||
local args = parse_arg_str(arg_str, 0)
|
||||
if #args == 0 then
|
||||
args = {{1}, {1}}
|
||||
elseif #args == 1 then
|
||||
args[2] = {1}
|
||||
end
|
||||
|
||||
local total = 0
|
||||
local shares = { }
|
||||
for i = 1, #args do
|
||||
local arg
|
||||
if not alt then
|
||||
arg = args[i]
|
||||
else
|
||||
arg = args[#args - i + 1]
|
||||
end
|
||||
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
|
||||
shares[i] = arg
|
||||
end
|
||||
|
||||
if #shares > max_split then
|
||||
return nil
|
||||
end
|
||||
|
||||
local a, area_index = close_area()
|
||||
a.habitable = false
|
||||
a.split = {
|
||||
method = method,
|
||||
x_shares = method == "h" and shares or {{1}},
|
||||
y_shares = method == "v" and shares or {{1}},
|
||||
children = {}
|
||||
}
|
||||
local children = a.split.children
|
||||
|
||||
if method == "h" then
|
||||
for i = 1, #a.split.x_shares do
|
||||
local child = {
|
||||
parent_id = area_index,
|
||||
parent_cid = #children + 1,
|
||||
parent_x_shares = #children + 1,
|
||||
parent_y_shares = 1,
|
||||
expansion = a.expansion - 1,
|
||||
|
||||
bl = i == 1 and a.bl or false,
|
||||
br = i == #a.split.x_shares and a.br or false,
|
||||
bu = a.bu,
|
||||
bd = a.bd,
|
||||
}
|
||||
children[#children + 1] = child
|
||||
end
|
||||
else
|
||||
for i = 1, #a.split.y_shares do
|
||||
local child = {
|
||||
parent_id = area_index,
|
||||
parent_cid = #children + 1,
|
||||
parent_x_shares = 1,
|
||||
parent_y_shares = #children + 1,
|
||||
expansion = a.expansion - 1,
|
||||
|
||||
bl = a.bl,
|
||||
br = a.br,
|
||||
bu = i == 1 and a.bu or false,
|
||||
bd = i == #a.split.y_shares and a.bd or false,
|
||||
}
|
||||
children[#children + 1] = child
|
||||
end
|
||||
end
|
||||
|
||||
push_open_areas(children)
|
||||
|
||||
elseif method == "w" or method == "d" then
|
||||
|
||||
local args = parse_arg_str(arg_str, 0)
|
||||
|
||||
local x_shares = {}
|
||||
local y_shares = {}
|
||||
local m_start = #args + 1
|
||||
|
||||
if method == "w" then
|
||||
if #args == 0 then
|
||||
args = {{1}, {1}}
|
||||
elseif #args == 1 then
|
||||
args[2] = {1}
|
||||
end
|
||||
|
||||
local x_shares_count, y_shares_count
|
||||
if alt then
|
||||
x_shares_count = args[2][1]
|
||||
y_shares_count = args[1][1]
|
||||
else
|
||||
x_shares_count = args[1][1]
|
||||
y_shares_count = args[2][1]
|
||||
end
|
||||
if x_shares_count < 1 then x_shares_count = 1 end
|
||||
if y_shares_count < 1 then y_shares_count = 1 end
|
||||
|
||||
if x_shares_count * y_shares_count > max_split then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, x_shares_count do x_shares[i] = {1} end
|
||||
for i = 1, y_shares_count do y_shares[i] = {1} end
|
||||
|
||||
m_start = 3
|
||||
else
|
||||
local current = x_shares
|
||||
for i = 1, #args do
|
||||
if not alt then
|
||||
arg = args[i]
|
||||
else
|
||||
arg = args[#args - i + 1]
|
||||
end
|
||||
if arg[1] == 0 then
|
||||
if current == x_shares then current = y_shares else
|
||||
m_start = i + 1
|
||||
break
|
||||
end
|
||||
else
|
||||
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
|
||||
current[#current + 1] = arg
|
||||
end
|
||||
end
|
||||
|
||||
if #x_shares == 0 then
|
||||
x_shares = {{1}}
|
||||
end
|
||||
|
||||
if #y_shares == 0 then
|
||||
y_shares = {{1}}
|
||||
end
|
||||
|
||||
if #x_shares * #y_shares > max_split then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local a, area_index = close_area()
|
||||
a.habitable = false
|
||||
a.split = {
|
||||
method = method,
|
||||
x_shares = x_shares,
|
||||
y_shares = y_shares,
|
||||
children = {},
|
||||
}
|
||||
local children = {}
|
||||
|
||||
for y_index = 1, #a.split.y_shares do
|
||||
for x_index = 1, #a.split.x_shares do
|
||||
local r = {
|
||||
parent_id = area_index,
|
||||
-- parent_cid will be filled later.
|
||||
parent_x_shares = x_index,
|
||||
parent_y_shares = y_index,
|
||||
expansion = a.expansion - 1
|
||||
}
|
||||
if x_index == 1 then r.bl = a.bl else r.bl = false end
|
||||
if x_index == #a.split.x_shares then r.br = a.br else r.br = false end
|
||||
if y_index == 1 then r.bu = a.bu else r.bu = false end
|
||||
if y_index == #a.split.y_shares then r.bd = a.bd else r.bd = false end
|
||||
children[#children + 1] = r
|
||||
end
|
||||
end
|
||||
|
||||
local merged_children = {}
|
||||
local start_index = 1
|
||||
for i = m_start, #args - 1, 2 do
|
||||
-- find the first index that is not merged
|
||||
while start_index <= #children and children[start_index] == false do
|
||||
start_index = start_index + 1
|
||||
end
|
||||
if start_index > #children or children[start_index] == false then
|
||||
break
|
||||
end
|
||||
local x = (start_index - 1) % #x_shares
|
||||
local y = math.floor((start_index - 1) / #x_shares)
|
||||
local w = args[i][1]
|
||||
local h = args[i + 1][1]
|
||||
if w < 1 then w = 1 end
|
||||
if h == nil or h < 1 then h = 1 end
|
||||
if alt then
|
||||
local tmp = w
|
||||
w = h
|
||||
h = tmp
|
||||
end
|
||||
if x + w > #x_shares then w = #x_shares - x end
|
||||
if y + h > #y_shares then h = #y_shares - y end
|
||||
local end_index = start_index
|
||||
for ty = y, y + h - 1 do
|
||||
local succ = true
|
||||
for tx = x, x + w - 1 do
|
||||
if children[ty * #x_shares + tx + 1] == false then
|
||||
succ = false
|
||||
break
|
||||
elseif ty == y then
|
||||
end_index = ty * #x_shares + tx + 1
|
||||
end
|
||||
end
|
||||
|
||||
if not succ then
|
||||
break
|
||||
elseif ty > y then
|
||||
end_index = ty * #x_shares + x + w
|
||||
end
|
||||
end
|
||||
|
||||
local function generate_range(s, e)
|
||||
local r = {} for j = s, e do r[#r+1] = j end return r
|
||||
end
|
||||
|
||||
local r = {
|
||||
bu = children[start_index].bu, bl = children[start_index].bl,
|
||||
bd = children[end_index].bd, br = children[end_index].br,
|
||||
|
||||
parent_id = area_index,
|
||||
-- parent_cid will be filled later.
|
||||
parent_x_shares = generate_range(children[start_index].parent_x_shares, children[end_index].parent_x_shares),
|
||||
parent_y_shares = generate_range(children[start_index].parent_y_shares, children[end_index].parent_y_shares),
|
||||
expansion = a.expansion - 1
|
||||
}
|
||||
merged_children[#merged_children + 1] = r
|
||||
|
||||
for ty = y, y + h - 1 do
|
||||
local succ = true
|
||||
for tx = x, x + w - 1 do
|
||||
local index = ty * #x_shares + tx + 1
|
||||
if index <= end_index then
|
||||
children[index] = false
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #merged_children do
|
||||
a.split.children[#a.split.children + 1] = merged_children[i]
|
||||
a.split.children[#a.split.children].parent_cid = #a.split.children
|
||||
end
|
||||
|
||||
-- clean up children, remove all `false'
|
||||
for i = 1, #children do
|
||||
if children[i] ~= false then
|
||||
a.split.children[#a.split.children + 1] = children[i]
|
||||
a.split.children[#a.split.children].parent_cid = #a.split.children
|
||||
end
|
||||
end
|
||||
|
||||
push_open_areas(a.split.children)
|
||||
|
||||
elseif method == "s" then
|
||||
|
||||
if #open_areas > 0 then
|
||||
local times = arg_str == "" and 1 or tonumber(arg_str)
|
||||
local t = {}
|
||||
local c = #open_areas
|
||||
local p = open_areas[c].parent_id
|
||||
while c > 0 and open_areas[c].parent_id == p do
|
||||
t[#t + 1] = open_areas[c]
|
||||
open_areas[c] = nil
|
||||
c = c - 1
|
||||
end
|
||||
for i = #t, 1, -1 do
|
||||
open_areas[c + 1] = t[(i + times - 1) % #t + 1]
|
||||
c = c + 1
|
||||
end
|
||||
end
|
||||
|
||||
elseif method == "t" then
|
||||
|
||||
if #open_areas > 0 then
|
||||
open_areas[#open_areas].expansion = tonumber(arg_str) or default_expansion
|
||||
end
|
||||
|
||||
elseif method == "x" then
|
||||
|
||||
local a = close_area()
|
||||
a.layout = arg_str
|
||||
|
||||
elseif method == "-" then
|
||||
|
||||
close_area()
|
||||
|
||||
elseif method == "." then
|
||||
|
||||
while #open_areas > 0 do
|
||||
close_area()
|
||||
end
|
||||
|
||||
elseif method == "c" then
|
||||
|
||||
local limit = tonumber(arg_str)
|
||||
if limit == nil or limit > #open_areas then
|
||||
limit = #open_areas
|
||||
end
|
||||
local p = open_areas[#open_areas].parent_id
|
||||
while limit > 0 and open_areas[#open_areas].parent_id == p do
|
||||
close_area()
|
||||
limit = limit - 1
|
||||
end
|
||||
|
||||
elseif method == "/" then
|
||||
|
||||
close_area().habitable = false
|
||||
|
||||
elseif method == ";" then
|
||||
|
||||
-- nothing
|
||||
|
||||
end
|
||||
|
||||
if #open_areas + #closed_areas > max_areas then
|
||||
return nil
|
||||
end
|
||||
|
||||
while #open_areas > 0 and open_areas[#open_areas].expansion <= 0 do
|
||||
close_area()
|
||||
end
|
||||
|
||||
arg_str = ""
|
||||
return true
|
||||
end
|
||||
|
||||
open_areas = {root}
|
||||
|
||||
for i = 1, #command do
|
||||
local ch = command:sub(i, i)
|
||||
local t = ch_info[ch]
|
||||
local r = true
|
||||
if t == nil then
|
||||
return nil
|
||||
elseif t == 3 then
|
||||
if pending_op ~= nil then
|
||||
r = handle_op(pending_op)
|
||||
pending_op = nil
|
||||
end
|
||||
if #open_areas == 0 then return nil end
|
||||
if arg_str == "" then
|
||||
pending_op = ch
|
||||
else
|
||||
r = handle_op(ch)
|
||||
end
|
||||
elseif t == 2 or t == 1 then
|
||||
if pending_op ~= nil then
|
||||
handle_op(pending_op)
|
||||
pending_op = nil
|
||||
end
|
||||
if #open_areas == 0 and t == 2 then return nil end
|
||||
r = handle_op(ch)
|
||||
elseif t == 0 then
|
||||
arg_str = arg_str..ch
|
||||
end
|
||||
|
||||
if not r then return nil end
|
||||
end
|
||||
|
||||
if pending_op ~= nil then
|
||||
if not handle_op(pending_op) then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if #closed_areas == 0 then
|
||||
return closed_areas, open_areas, pending_op ~= nil
|
||||
end
|
||||
|
||||
local old_closed_areas = closed_areas
|
||||
closed_areas = {}
|
||||
local function reorder_and_fill_adj_min(old_id)
|
||||
local a = old_closed_areas[old_id]
|
||||
closed_areas[#closed_areas + 1] = a
|
||||
a.id = #closed_areas
|
||||
|
||||
if a.split then
|
||||
for i = 1, #a.split.x_shares do
|
||||
a.split.x_shares[i][3] = 0
|
||||
a.split.x_shares[i][4] = minimum
|
||||
end
|
||||
|
||||
for i = 1, #a.split.y_shares do
|
||||
a.split.y_shares[i][3] = 0
|
||||
a.split.y_shares[i][4] = minimum
|
||||
end
|
||||
|
||||
for _, c in ipairs(a.split.children) do
|
||||
if c.id then
|
||||
reorder_and_fill_adj_min(c.id)
|
||||
end
|
||||
|
||||
local x_minimum, y_minimum
|
||||
if c.split then
|
||||
x_minimum, y_minimum = c.x_minimum, c.y_minimum
|
||||
else
|
||||
x_minimum, y_minimum =
|
||||
minimum, minimum
|
||||
end
|
||||
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
local x_minimum_split = math.ceil(x_minimum / #c.parent_x_shares)
|
||||
for i = 1, #c.parent_x_shares do
|
||||
if a.split.x_shares[c.parent_x_shares[i]][4] < x_minimum_split then
|
||||
a.split.x_shares[c.parent_x_shares[i]][4] = x_minimum_split
|
||||
end
|
||||
end
|
||||
else
|
||||
if a.split.x_shares[c.parent_x_shares][4] < x_minimum then
|
||||
a.split.x_shares[c.parent_x_shares][4] = x_minimum
|
||||
end
|
||||
end
|
||||
|
||||
if type(c.parent_y_shares) == "table" then
|
||||
local y_minimum_split = math.ceil(y_minimum / #c.parent_y_shares)
|
||||
for i = 1, #c.parent_y_shares do
|
||||
if a.split.y_shares[c.parent_y_shares[i]][4] < y_minimum_split then
|
||||
a.split.y_shares[c.parent_y_shares[i]][4] = y_minimum_split
|
||||
end
|
||||
end
|
||||
else
|
||||
if a.split.y_shares[c.parent_y_shares][4] < y_minimum then
|
||||
a.split.y_shares[c.parent_y_shares][4] = y_minimum
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
a.x_minimum = 0
|
||||
a.x_total_weight = 0
|
||||
for i = 1, #a.split.x_shares do
|
||||
a.x_minimum = a.x_minimum + a.split.x_shares[i][4]
|
||||
a.x_total_weight = a.x_total_weight + (a.split.x_shares[i][2] or 0)
|
||||
end
|
||||
a.y_minimum = 0
|
||||
a.y_total_weight = 0
|
||||
for i = 1, #a.split.y_shares do
|
||||
a.y_minimum = a.y_minimum + a.split.y_shares[i][4]
|
||||
a.y_total_weight = a.y_total_weight + (a.split.y_shares[i][2] or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
reorder_and_fill_adj_min(1)
|
||||
|
||||
-- For debugging
|
||||
-- for i = 1, #closed_areas do
|
||||
-- print(i, closed_areas[i].parent_id, closed_areas[i].parent_x_shares, closed_areas[i].parent_y_shares)
|
||||
-- if closed_areas[i].split then
|
||||
-- print("/", closed_areas[i].split.method, #closed_areas[i].split.x_shares, #closed_areas[i].split.y_shares)
|
||||
-- for j = 1, #closed_areas[i].split.children do
|
||||
-- print("->", closed_areas[i].split.children[j].id)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local orig_width = root.width
|
||||
if root.x_minimum and root.width < root.x_minimum then
|
||||
root.width = root.x_minimum
|
||||
end
|
||||
local orig_height = root.height
|
||||
if root.y_minimum and root.height < root.y_minimum then
|
||||
root.height = root.y_minimum
|
||||
end
|
||||
|
||||
local function split(id)
|
||||
local a = closed_areas[id]
|
||||
if a.split then
|
||||
local x_shares, y_shares
|
||||
x_shares, a.split.x_spare = fair_split(a.width, a.split.x_shares)
|
||||
y_shares, a.split.y_spare = fair_split(a.height, a.split.y_shares)
|
||||
|
||||
for _, c in ipairs(a.split.children) do
|
||||
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
c.x = a.x + x_shares[c.parent_x_shares[1] - 1]
|
||||
c.width = 0
|
||||
for i = 1, #c.parent_x_shares do
|
||||
c.width = c.width + x_shares[c.parent_x_shares[i]] - x_shares[c.parent_x_shares[i] - 1]
|
||||
end
|
||||
else
|
||||
c.x = a.x + x_shares[c.parent_x_shares - 1]
|
||||
c.width = x_shares[c.parent_x_shares] - x_shares[c.parent_x_shares - 1]
|
||||
end
|
||||
|
||||
if type(c.parent_y_shares) == "table" then
|
||||
c.y = a.y + y_shares[c.parent_y_shares[1] - 1]
|
||||
c.height = 0
|
||||
for i = 1, #c.parent_y_shares do
|
||||
c.height = c.height + y_shares[c.parent_y_shares[i]] - y_shares[c.parent_y_shares[i] - 1]
|
||||
end
|
||||
else
|
||||
c.y = a.y + y_shares[c.parent_y_shares - 1]
|
||||
c.height = y_shares[c.parent_y_shares] - y_shares[c.parent_y_shares - 1]
|
||||
end
|
||||
|
||||
if c.id then
|
||||
split(c.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
split(1)
|
||||
|
||||
for i = 1, #closed_areas do
|
||||
if closed_areas[i].x + closed_areas[i].width > root.x + orig_width or
|
||||
closed_areas[i].y + closed_areas[i].height > root.y + orig_height
|
||||
then
|
||||
closed_areas[i].habitable = false
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #open_areas do
|
||||
if open_areas[i].x + open_areas[i].width > root.x + orig_width or
|
||||
open_areas[i].y + open_areas[i].height > root.y + orig_height
|
||||
then
|
||||
open_areas[i].habitable = false
|
||||
end
|
||||
end
|
||||
|
||||
return closed_areas, open_areas, pending_op ~= nil
|
||||
end
|
||||
|
||||
local function areas_to_command(areas, to_embed, root_area)
|
||||
root_area = root_area or 1
|
||||
if #areas < root_area then return nil end
|
||||
|
||||
local function shares_to_arg_str(shares)
|
||||
local arg_str = ""
|
||||
for _, share in ipairs(shares) do
|
||||
if #arg_str > 0 then arg_str = arg_str.."," end
|
||||
arg_str = arg_str..tostring(share[1])
|
||||
if not share[2] or share[2] == 0 then
|
||||
-- nothing
|
||||
elseif share[2] > 0 then
|
||||
arg_str = arg_str.."_"..tostring(share[2])
|
||||
else
|
||||
arg_str = arg_str.."__"..tostring(-share[2])
|
||||
end
|
||||
end
|
||||
return arg_str
|
||||
end
|
||||
|
||||
local function get_command(area_id)
|
||||
local r
|
||||
local handled_options = {}
|
||||
local a = areas[area_id]
|
||||
|
||||
if a == nil then
|
||||
return ""
|
||||
end
|
||||
|
||||
if a.hole then
|
||||
return "|"
|
||||
end
|
||||
|
||||
if a.split then
|
||||
for i = 1, #a.split.children do
|
||||
if a.split.children[i].hole then
|
||||
a.expansion = default_expansion + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local method = a.split.method
|
||||
if method == "h" then
|
||||
r = shares_to_arg_str(a.split.x_shares)
|
||||
r = "h"..r
|
||||
elseif method == "v" then
|
||||
r = shares_to_arg_str(a.split.y_shares)
|
||||
r = "v"..r
|
||||
elseif method == "d" or method == "w" then
|
||||
local simple = true
|
||||
for _, s in ipairs(a.split.x_shares) do
|
||||
if s[1] ~= 1 or s[2] then simple = false break end
|
||||
end
|
||||
if simple then
|
||||
for _, s in ipairs(a.split.y_shares) do
|
||||
if s[1] ~= 1 or s[2] then simple = false break end
|
||||
end
|
||||
end
|
||||
if method == "w" and simple then
|
||||
r = tostring(#a.split.x_shares)..","..tostring(#a.split.y_shares)
|
||||
else
|
||||
r = shares_to_arg_str(a.split.x_shares)..",,"..shares_to_arg_str(a.split.y_shares)
|
||||
method = "d"
|
||||
end
|
||||
local m = ""
|
||||
for _, c in ipairs(a.split.children) do
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
if #m > 0 then m = m.."," end
|
||||
m = m..tostring(c.parent_x_shares[#c.parent_x_shares] - c.parent_x_shares[1] + 1)..","..
|
||||
tostring(c.parent_y_shares[#c.parent_y_shares] - c.parent_y_shares[1] + 1)
|
||||
end
|
||||
end
|
||||
if method == "d" and r == "1,,1" then
|
||||
r = ""
|
||||
end
|
||||
r = method..r..(#m == 0 and m or (method == "w" and "," or ",,"))..m
|
||||
end
|
||||
local acc_dashes = 0
|
||||
if a.expansion > 1 then
|
||||
for _, c in ipairs(a.split.children) do
|
||||
local cr = get_command(c.id)
|
||||
if cr == "-" then
|
||||
acc_dashes = acc_dashes + 1
|
||||
else
|
||||
if acc_dashes == 0 then
|
||||
elseif acc_dashes == 1 then
|
||||
r = r.."-"
|
||||
else
|
||||
r = r.."c"..tonumber(acc_dashes)
|
||||
end
|
||||
acc_dashes = 0
|
||||
r = r..cr
|
||||
end
|
||||
end
|
||||
if acc_dashes > 0 then
|
||||
r = r.."c"
|
||||
end
|
||||
end
|
||||
|
||||
if area_id ~= root_area then
|
||||
if a.expansion ~= areas[a.parent_id].expansion - 1 then
|
||||
r = "t"..tostring(a.expansion)..r
|
||||
end
|
||||
else
|
||||
if a.expansion ~= default_expansion then
|
||||
r = "t"..tostring(a.expansion)..r
|
||||
end
|
||||
end
|
||||
elseif a.disabled then
|
||||
r = "/"
|
||||
elseif a.layout then
|
||||
r = "x"..a.layout
|
||||
else
|
||||
r = "-"
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
local r = get_command(root_area)
|
||||
if not to_embed then
|
||||
if r == "-" then
|
||||
r = "."
|
||||
else
|
||||
-- The last . may be redundant, but it makes sure no pending op.
|
||||
r = r:gsub("[\\c]+$", "").."."
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
if not in_module then
|
||||
print("Testing areas/command processing")
|
||||
local function check_transcoded_command(command, expectation)
|
||||
local areas, open_areas = areas_from_command(command, {x = 0, y = 0, width = 100, height = 100}, 0)
|
||||
if #open_areas > 0 then
|
||||
print("Found open areas after command "..command)
|
||||
assert(false)
|
||||
end
|
||||
local transcoded = areas_to_command(areas)
|
||||
if transcoded ~= expectation then
|
||||
print("Mismatched transcoding for "..command..": got "..transcoded..", expected "..expectation)
|
||||
assert(false)
|
||||
end
|
||||
end
|
||||
check_transcoded_command(".", ".")
|
||||
check_transcoded_command("3t.", ".")
|
||||
check_transcoded_command("121h.", "h1,2,1.")
|
||||
check_transcoded_command("1_10,2,1h1s131v.", "h1_10,2,1-v1,3,1.")
|
||||
check_transcoded_command("332111w.", "w3,3,2,1,1,1.")
|
||||
check_transcoded_command("1310111d.", "d1,3,1,,1,1,1.")
|
||||
check_transcoded_command("dw66.", "dw6,6.")
|
||||
check_transcoded_command(";dw66.", "dw6,6.")
|
||||
check_transcoded_command("101dw66.", "dw6,6.")
|
||||
check_transcoded_command("3tdw66.", "t3dw6,6.")
|
||||
print("Passed.")
|
||||
end
|
||||
|
||||
return {
|
||||
areas_from_command = areas_from_command,
|
||||
areas_to_command = areas_to_command,
|
||||
}
|
||||
Reference in New Issue
Block a user