Compare commits
No commits in common. "master" and "portage" have entirely different histories.
|
@ -1 +0,0 @@
|
|||
export AEGISUB_LUA=1
|
|
@ -1 +0,0 @@
|
|||
Subproject commit be2957e58b7ebdc6d3522a792f0120798dfbafa7
|
|
@ -1,75 +0,0 @@
|
|||
export script_name = 'Selegator'
|
||||
export script_description = 'Select/navigate in the subtitle grid'
|
||||
export script_author = 'tophf'
|
||||
export script_version = '1.1.5'
|
||||
export script_namespace = 'Flux.Selegator'
|
||||
|
||||
DependencyControl = require('l0.DependencyControl') {
|
||||
url: 'https://github.com/TypesettingTools/CoffeeFlux-Aegisub-Scripts/blob/master/macros/Flux.Selegator.moon'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/CoffeeFlux-Aegisub-Scripts/master/DependencyControl.json'
|
||||
{}
|
||||
}
|
||||
|
||||
selectAll = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
if #sel>1
|
||||
[i for i in *sel when subs[i].style==lookforstyle]
|
||||
else
|
||||
[k for k,s in ipairs subs when s.style==lookforstyle]
|
||||
|
||||
findPrevious = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
for i = act-1,1,-1
|
||||
return if subs[i].class!='dialogue'
|
||||
if subs[i].style==lookforstyle
|
||||
return {i}
|
||||
|
||||
findNext = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
for i = act+1,#subs
|
||||
if subs[i].style==lookforstyle
|
||||
return {i}
|
||||
|
||||
firstInBlock = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
for i = act-1,1,-1
|
||||
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
|
||||
return {i+1}
|
||||
|
||||
lastInBlock = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
for i = act+1,#subs
|
||||
if subs[i].style!=lookforstyle
|
||||
return {i-1}
|
||||
{#subs}
|
||||
|
||||
selectBlock = (subs, sel, act) ->
|
||||
lookforstyle = subs[act].style
|
||||
first, last = act, #subs
|
||||
for i = act-1,1,-1
|
||||
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
|
||||
first = i + 1
|
||||
break
|
||||
for i = act+1,#subs
|
||||
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
|
||||
last = i - 1
|
||||
break
|
||||
[i for i=first,last]
|
||||
|
||||
untilStart = (subs, sel, act) ->
|
||||
[i for i = 1,act when subs[i].class=='dialogue']
|
||||
|
||||
untilEnd = (subs, sel, act) ->
|
||||
[i for i = act,#subs when subs[i].class=='dialogue']
|
||||
|
||||
DependencyControl\registerMacros {
|
||||
{ 'Current Style/Select All', '', selectAll }
|
||||
{ 'Current Style/Previous', '', findPrevious }
|
||||
{ 'Current Style/Next', '', findNext }
|
||||
{ 'Current Style/First In Block', '', firstInBlock }
|
||||
{ 'Current Style/Last In Block', '', lastInBlock }
|
||||
{ 'Current Style/Select Block', '', selectBlock }
|
||||
|
||||
{ 'Select Until Start', '', untilStart }
|
||||
{ 'Select Until End', '', untilEnd }
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,25 +0,0 @@
|
|||
local tr = aegisub.gettext
|
||||
|
||||
script_name = tr"Clean k tags"
|
||||
script_description = tr"Remove double k tags"
|
||||
script_author = "amoethyst"
|
||||
script_version = "1.0"
|
||||
|
||||
function special_k(subs, sel)
|
||||
|
||||
-- if the first tag is K/kf this would break the timing for the previous timing
|
||||
local expr = "^(.-){\\(ko?)([0-9.]*)[^}]-}([^{]-){\\[kK][fo]?([0-9.]*)[^}]-}( -{(\\[kK][fo]?)[0-9.]*[^}]-}.*)$"
|
||||
|
||||
for _, i in ipairs(sel) do
|
||||
line = subs[i]
|
||||
before, tag, k1, between, k2, after = line.text:match(expr)
|
||||
while after ~= nil do
|
||||
line.text = before .. "{\\" .. tag .. tonumber(k1) + tonumber(k2) .. "}" .. between .. after
|
||||
subs[i] = line
|
||||
before, tag, k1, between, k2, after = line.text:match(expr)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
aegisub.register_macro(script_name, script_description, special_k)
|
|
@ -1,178 +0,0 @@
|
|||
local tr = aegisub.gettext
|
||||
|
||||
script_name = tr"Duetto Meika"
|
||||
script_description = tr"The ultimate tool for karaoke duets"
|
||||
script_author = "amoethyst"
|
||||
|
||||
include("utils.lua")
|
||||
|
||||
|
||||
function replace_style(line, style_name, style_string)
|
||||
before_style, after_style = line.text:match("^(.-{[^}]-)\\?s:".. style_name .."(.*)$")
|
||||
return before_style .. style_string .. after_style
|
||||
end
|
||||
|
||||
|
||||
function duetto(subs, sel)
|
||||
styles = {}
|
||||
|
||||
-- create the style map
|
||||
for _, line in ipairs(subs) do
|
||||
if line.class == "style" then
|
||||
styles[line.name] = line
|
||||
end
|
||||
end
|
||||
|
||||
-- duetto~
|
||||
for _, i in ipairs(sel) do
|
||||
line = subs[i]
|
||||
|
||||
current_style = styles[line.style]
|
||||
-- match every `s:` marker
|
||||
for style_name in line.text:gmatch("{[^}]*s:([^}\\]*)[^}]*}") do
|
||||
if style_name ~= current_style.name then
|
||||
|
||||
style = styles[style_name]
|
||||
-- build the tags to use the new style
|
||||
style_string = ""
|
||||
if current_style.color1 ~= style.color1 then
|
||||
style_string = style_string .. "\\c" .. style.color1
|
||||
end
|
||||
if current_style.color2 ~= style.color2 then
|
||||
style_string = style_string .. "\\2c" .. style.color2
|
||||
end
|
||||
if current_style.color3 ~= style.color3 then
|
||||
style_string = style_string .. "\\3c" .. style.color3
|
||||
end
|
||||
if current_style.color4 ~= style.color4 then
|
||||
style_string = style_string .. "\\4c" .. style.color4
|
||||
end
|
||||
|
||||
-- set style
|
||||
line.text = replace_style(line, style_name, style_string)
|
||||
current_style = style
|
||||
else
|
||||
-- remove marker to not break everything
|
||||
line.text = replace_style(line, style_name, "")
|
||||
end
|
||||
end
|
||||
subs[i] = line
|
||||
end
|
||||
|
||||
aegisub.set_undo_point(script_name)
|
||||
end
|
||||
|
||||
|
||||
function test_colors(c1, c2)
|
||||
return color_from_style(c1) == color_from_style(c2)
|
||||
end
|
||||
|
||||
|
||||
function get_script_style(style, styles)
|
||||
for key, script_style in pairs(styles) do
|
||||
if (test_colors(style.color1, script_style.color1)
|
||||
and test_colors(style.color2, script_style.color2)
|
||||
and test_colors(style.color3, script_style.color3)
|
||||
and test_colors(style.color4, script_style.color4)
|
||||
and tonumber(style.fontsize) == tonumber(script_style.fontsize)
|
||||
and style.fontname == script_style.fontname) then
|
||||
return script_style
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
function deduetto_meika(subs, sel)
|
||||
local styles = {}
|
||||
local last_style = -1
|
||||
|
||||
-- create the style map
|
||||
for i, line in ipairs(subs) do
|
||||
if line.class == "style" then
|
||||
styles[line.name] = line
|
||||
last_style = i
|
||||
end
|
||||
end
|
||||
|
||||
local new_styles = {}
|
||||
|
||||
for _, i in ipairs(sel) do
|
||||
local line = subs[i]
|
||||
local current_style = table.copy(styles[line.style])
|
||||
|
||||
local search_index = 1
|
||||
while search_index < #line.text do
|
||||
local match_start, match_end = line.text:find("{[^}]*}", search_index)
|
||||
if match_start == nil then
|
||||
break
|
||||
end
|
||||
|
||||
local bracketed = line.text:sub(match_start, match_end)
|
||||
local new_style = false
|
||||
|
||||
-- change style's colors
|
||||
for tag, value in bracketed:gmatch("\\([1-4]?c)([^}\\]*)") do
|
||||
new_style = true
|
||||
if tag == "c" or tag == "1c" then
|
||||
current_style.color1 = value
|
||||
elseif tag == "2c" then
|
||||
current_style.color2 = value
|
||||
elseif tag == "3c" then
|
||||
current_style.color3 = value
|
||||
elseif tag == "4c" then
|
||||
current_style.color4 = value
|
||||
end
|
||||
end
|
||||
|
||||
-- change style's font
|
||||
for tag, value in bracketed:gmatch("\\(f[sn])([^}\\]*)") do
|
||||
new_style = true
|
||||
if tag == "fs" then
|
||||
current_style.fontsize = value
|
||||
elseif tag == "fn" then
|
||||
current_style.fontname = value
|
||||
end
|
||||
end
|
||||
|
||||
if new_style then
|
||||
aegisub.log("oof\n")
|
||||
local script_style = get_script_style(current_style, styles)
|
||||
if script_style == nil then
|
||||
aegisub.log("foo\n")
|
||||
if get_script_style(current_style, new_styles) == nil then
|
||||
new_styles[#new_styles+1] = table.copy(current_style)
|
||||
end
|
||||
else
|
||||
aegisub.log("ofo\n")
|
||||
-- remove inline colors
|
||||
bracketed = bracketed:gsub("\\[1-4]?c[^\\}]*", "")
|
||||
bracketed = bracketed:gsub("\\[1-4]?a[^\\}]*", "")
|
||||
-- remove inline fonts
|
||||
bracketed = bracketed:gsub("\\f[sn][^\\}]*", "")
|
||||
|
||||
-- add style marker
|
||||
bracketed = "{s:" .. script_style.name .. bracketed:sub(2, #bracketed)
|
||||
line.text = line.text:sub(1, match_start-1) .. bracketed .. line.text:sub(match_end + 1, #line.text)
|
||||
end
|
||||
end
|
||||
|
||||
search_index = match_start + 1
|
||||
end
|
||||
|
||||
subs[i] = line
|
||||
end
|
||||
|
||||
if #new_styles > 0 then
|
||||
for i, new_style in ipairs(new_styles) do
|
||||
new_style.name = "Deduetto style " .. i
|
||||
subs.insert(last_style, new_style)
|
||||
last_style = last_style + 1
|
||||
aegisub.log("Created new style: " .. new_style.name .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
aegisub.register_macro(script_name, script_description, duetto)
|
||||
aegisub.register_macro(tr"Deduetto Meika", tr"Create styles from inline color tags", deduetto_meika)
|
|
@ -1,24 +0,0 @@
|
|||
local tr = aegisub.gettext
|
||||
|
||||
script_name = tr"frz endro"
|
||||
script_description = tr"probably won't be useful anytime soon"
|
||||
script_author = "amoethyst"
|
||||
script_version = "1.0"
|
||||
|
||||
function frz_combo(subs, sel)
|
||||
|
||||
local expr = "frz([0-9.-]+)"
|
||||
|
||||
for _, i in ipairs(sel) do
|
||||
line = subs[i]
|
||||
aegisub.log(line.text)
|
||||
frz_text = line.text:match(expr)
|
||||
aegisub.log(frz_text)
|
||||
frz = tonumber(frz_text) * -1
|
||||
line.text = line.text .. "{\\frz"..frz.."}"
|
||||
subs[i] = line
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
aegisub.register_macro(script_name, script_description, frz_combo)
|
|
@ -1,235 +0,0 @@
|
|||
--[[
|
||||
==README==
|
||||
|
||||
Circular Text
|
||||
|
||||
Define an origin, and this will put the text on a circular arc centered on that origin.
|
||||
|
||||
An origin must be defined and it must be different from the position of the line. You can't have
|
||||
a circle if the radius is zero.
|
||||
|
||||
The x coordinate of the position tag should match the x coordinate of the origin tag for best
|
||||
results. In other words, your original line should be at a right angle to the radius. Note that
|
||||
these are the x coordinates in the tags, not the x coordinates on screen, which will change if
|
||||
you rotate the tag.
|
||||
|
||||
Supports varied fonts, font sizes, font spacings, and x/y scales in the same line.
|
||||
|
||||
The resulting arc will be centered on the original rotation of your line.
|
||||
|
||||
Only works on static lines. If you want the line to move or rotate, use another macro.
|
||||
|
||||
|
||||
]]--
|
||||
|
||||
script_name = "Circular text"
|
||||
script_description = "Puts the text on a circular arc centered on the origin."
|
||||
script_version = "0.2.0"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.CircleText"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
"aegisub.util"
|
||||
}
|
||||
}
|
||||
local LibLyger, util = rec:requireModules()
|
||||
local libLyger = LibLyger()
|
||||
|
||||
--[[
|
||||
Tags that can have any character after the tag declaration:
|
||||
\r
|
||||
\fn
|
||||
Otherwise, the first character after the tag declaration must be:
|
||||
a number, decimal point, open parentheses, minus sign, or ampersand
|
||||
]]--
|
||||
|
||||
|
||||
--Distance between two points
|
||||
local function distance(x1,y1,x2,y2)
|
||||
return math.sqrt((x2-x1)^2+(y2-y1)^2)
|
||||
end
|
||||
|
||||
--Sign of a value
|
||||
local function sign(n)
|
||||
return n/math.abs(n)
|
||||
end
|
||||
|
||||
--Angle in degrees, given the arc length and radius
|
||||
local function arc_angle(arc_length,radius)
|
||||
return arc_length/radius * 180/math.pi
|
||||
end
|
||||
|
||||
--Main processing function
|
||||
function circle_text(sub,sel)
|
||||
libLyger:set_sub(sub, sel)
|
||||
for si,li in ipairs(sel) do
|
||||
--Progress report
|
||||
aegisub.progress.task("Processing line "..si.."/"..#sel)
|
||||
aegisub.progress.set(100*si/#sel)
|
||||
|
||||
--Read in the line
|
||||
line = libLyger.lines[li]
|
||||
|
||||
--Get position and origin
|
||||
px, py = libLyger:get_pos(line)
|
||||
ox, oy = libLyger:get_org(line)
|
||||
|
||||
--Make sure pos and org are not the same
|
||||
if px==ox and py==oy then
|
||||
aegisub.log(1,"Error on line %d: Position and origin cannot be the same!",li)
|
||||
return
|
||||
end
|
||||
|
||||
--Get radius
|
||||
radius=distance(px,py,ox,oy)
|
||||
|
||||
--Remove \pos and \move
|
||||
--If your line was non-static, too bad
|
||||
line.text = LibLyger.line_exclude(line.text,{"pos","move"})
|
||||
|
||||
--Make sure line starts with a tag block
|
||||
if line.text:find("^{")==nil then
|
||||
line.text="{}"..line.text
|
||||
end
|
||||
|
||||
--Rotation direction: positive if each character adds to the angle,
|
||||
--negative if each character subtracts from the angle
|
||||
rot_dir=sign(py-oy)
|
||||
|
||||
--Add the \pos back with recalculated position
|
||||
line.text=line.text:gsub("^{",string.format("{\\pos(%d,%d)",ox,oy+rot_dir*radius))
|
||||
|
||||
--Get z rotation
|
||||
--Will only take the first one, because if you wanted the text to be on a circular arc,
|
||||
--why do you have more than one z rotation tag in the first place?
|
||||
_,_,zrot=line.text:find("\\frz([%-%.%d]+)")
|
||||
zrot=zrot or line.styleref.angle
|
||||
|
||||
--Make line table
|
||||
line_table={}
|
||||
for thistag,thistext in line.text:gmatch("({[^{}]*})([^{}]*)") do
|
||||
table.insert(line_table,{tag=thistag,text=thistext})
|
||||
end
|
||||
|
||||
--Where data on the character widths will be stored
|
||||
char_data={}
|
||||
|
||||
--Total width of line
|
||||
cum_width=0
|
||||
|
||||
--Stores current state of the line as style table
|
||||
current_style = util.deep_copy(line.styleref)
|
||||
|
||||
--First pass to collect data on character widths
|
||||
for i,val in ipairs(line_table) do
|
||||
|
||||
char_data[i]={}
|
||||
|
||||
--Fix style tables to reflect override tags
|
||||
local _,_,font_name=val.tag:find("\\fn([^\\{}]+)")
|
||||
local _,_,font_size=val.tag:find("\\fs([%-%.%d]+)")
|
||||
local _,_,font_scx=val.tag:find("\\fscx([%-%.%d]+)")
|
||||
local _,_,font_scy=val.tag:find("\\fscy([%-%.%d]+)")
|
||||
local _,_,font_sp=val.tag:find("\\fsp([%-%.%d]+)")
|
||||
local _,_,_bold=val.tag:find("\\b([01])")
|
||||
local _,_,_italic=val.tag:find("\\i([01])")
|
||||
|
||||
current_style.fontname=font_name or current_style.fontname
|
||||
current_style.fontsize=tonumber(font_size) or current_style.fontsize
|
||||
current_style.scale_x=tonumber(font_scx) or current_style.scale_x
|
||||
current_style.scale_y=tonumber(font_scy) or current_style.scale_y
|
||||
current_style.spacing=tonumber(font_sp) or current_style.spacing
|
||||
if _bold~=nil then
|
||||
if _bold=="1" then current_style.bold=true
|
||||
else current_style.bold=false end
|
||||
end
|
||||
if _italic~=nil then
|
||||
if _italic=="1" then current_style.italic=true
|
||||
else current_style.italic=false end
|
||||
end
|
||||
|
||||
val.style = util.deep_copy(current_style)
|
||||
|
||||
--Collect width data on each char
|
||||
for thischar in val.text:gmatch(".") do
|
||||
cwidth=aegisub.text_extents(val.style,thischar)
|
||||
table.insert(char_data[i],{char=thischar,width=cwidth})
|
||||
end
|
||||
|
||||
--Increment cumulative width
|
||||
cum_width=cum_width+aegisub.text_extents(val.style,val.text)
|
||||
|
||||
end
|
||||
|
||||
--The angle that the rotation will begin at
|
||||
start_angle=zrot-(rot_dir*arc_angle(cum_width,radius))/2
|
||||
|
||||
rebuilt_text=""
|
||||
cum_rot=0
|
||||
|
||||
--Second pass to rebuild line with new tags
|
||||
for i,val in ipairs(line_table) do
|
||||
|
||||
rebuilt_text=rebuilt_text..val.tag:gsub("\\fsp[%-%.%d]+",""):gsub("\\frz[%-%.%d]+","")
|
||||
|
||||
for k,tchar in ipairs(char_data[i]) do
|
||||
--Character spacing should be the average of this character's width and the next one's
|
||||
--For spacing, scale width back up by the character's relevant scale_x,
|
||||
--because \fsp scales too. Also, subtract the existing font spacing
|
||||
this_spacing=0
|
||||
this_width=0
|
||||
if k~=#char_data[i] then
|
||||
this_width=(tchar.width+char_data[i][k+1].width)/2
|
||||
this_spacing=-1*(this_width*100/val.style.scale_x-val.style.spacing)
|
||||
else
|
||||
this_width=i~=#line_table and (tchar.width+char_data[i+1][1].width)/2 or 0
|
||||
this_spacing=i~=#line_table
|
||||
and -1*((tchar.width*100/val.style.scale_x
|
||||
+ char_data[i+1][1].width*100/line_table[i+1].style.scale_x)/2
|
||||
-val.style.spacing)
|
||||
or 0
|
||||
end
|
||||
|
||||
rebuilt_text=rebuilt_text..string.format("{\\frz%.3f\\fsp%.2f}%s",
|
||||
(start_angle+rot_dir*cum_rot)%360,this_spacing,tchar.char)
|
||||
|
||||
cum_rot=cum_rot+arc_angle(this_width,radius)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
--Fuck the re library. Maybe I'll come back to this
|
||||
whitespaces=re.find(rebuilt_text,
|
||||
'(\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\S)((?:\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\s)+)')
|
||||
|
||||
for j=1,#whitespaces-1,2 do
|
||||
first_tag=whitespaces[j].str
|
||||
other_tags=whitespaces[j+1].str
|
||||
aegisub.log("%s%s\n",first_tag,other_tags)
|
||||
first_space=first_tag:match("\\fsp([%d%.%-]+)")
|
||||
other_spaces=0
|
||||
total_wsp=0
|
||||
for _sp in other_tags:gmatch("\\fsp([%d%.%-]+)") do
|
||||
other_spaces=other_spaces+tonumber(_sp)
|
||||
total_wsp=total_wsp+1
|
||||
end
|
||||
total_space=tonumber(first_space)+other_spaces
|
||||
rebuilt_text=rebuilt_text:gsub(first_tag..other_tags,
|
||||
first_tag:gsub("\\fsp[%d%.%-]+",string.format("\\fsp%.2f",total_space))..string.rep(" ",total_wsp))
|
||||
end]]--
|
||||
|
||||
line.text=rebuilt_text:gsub("}{","")
|
||||
|
||||
sub[li]=line
|
||||
|
||||
end
|
||||
|
||||
aegisub.set_undo_point(script_name)
|
||||
|
||||
end
|
||||
|
||||
rec:registerMacro(circle_text)
|
|
@ -1,472 +0,0 @@
|
|||
--[[
|
||||
==README==
|
||||
|
||||
Blur clip
|
||||
|
||||
There's really not much to explain here. \clip statements produce a sharp edge. This script
|
||||
draws new \clip statements with decreasing alphas in order to imitate the effect of a blur.
|
||||
|
||||
The appearance won't always be perfect because of the limitations of precision with vector
|
||||
clip coordinates. The "precision" parameter ameliorates this somewhat, but the odd jagged
|
||||
line here and there is inevitable.
|
||||
|
||||
A note on the "precision" parameter: it scales exponentionally. If you want a 5-pixel blur,
|
||||
then a precision of 1 produces 6 lines (5 for the blur, 1 for the center). Precision 2 will
|
||||
generate 11 lines (10 for the blur, 1 for the center) and precision 3 will generate 21 lines
|
||||
(20 for the blur, 1 for the center). As you've probably figured out, a precision of 4 will
|
||||
create a whopping 41 lines. Use with caution.
|
||||
|
||||
|
||||
]]--
|
||||
script_name = "Blur clip"
|
||||
script_description = "Blurs a vector clip."
|
||||
script_version = "1.2.0"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.ClipBlur"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
"aegisub.util"
|
||||
}
|
||||
}
|
||||
local LibLyger, util = rec:requireModules()
|
||||
local libLyger = LibLyger()
|
||||
|
||||
--Distance between two points
|
||||
local function distance(x1,y1,x2,y2)
|
||||
return math.sqrt((x2-x1)^2+(y2-y1)^2)
|
||||
end
|
||||
|
||||
--Sign of a value
|
||||
local function sign(n)
|
||||
return n/math.abs(n)
|
||||
end
|
||||
|
||||
--Haha I didn't know these functions existed. May as well just alias them
|
||||
local todegree=math.deg
|
||||
local torad=math.rad
|
||||
|
||||
--Parses vector shape and makes it into a table
|
||||
function make_vector_table(vstring)
|
||||
local vtable={}
|
||||
local vexp=vstring:match("^([1-4]),")
|
||||
vexp=tonumber(vexp) or 1
|
||||
for vtype,vcoords in vstring:gmatch("([mlb])([%d%s%-]+)") do
|
||||
for vx,vy in vcoords:gmatch("([%d%-]+)%s+([%d%-]+)") do
|
||||
table.insert(vtable,{["class"]=vtype,["x"]=tonumber(vx),["y"]=tonumber(vy)})
|
||||
end
|
||||
end
|
||||
return vtable,vexp
|
||||
end
|
||||
|
||||
--Reverses a vector table object
|
||||
function reverse_vector_table(vtable)
|
||||
local nvtable={}
|
||||
if #vtable<1 then return nvtable end
|
||||
--Make sure vtable does not end in an m. I don't know why this would happen but still
|
||||
maxi=#vtable
|
||||
while vtable[maxi].class=="m" do
|
||||
maxi=maxi-1
|
||||
end
|
||||
|
||||
--All vector shapes start with m
|
||||
nstart = util.copy(vtable[maxi])
|
||||
tclass=nstart.class
|
||||
nstart.class="m"
|
||||
table.insert(nvtable,nstart)
|
||||
|
||||
--Reinsert coords in backwards order, but shift the class over by 1
|
||||
--because that's how vector shapes behave in aegi
|
||||
for i=maxi-1,1,-1 do
|
||||
tcoord = util.copy(vtable[i])
|
||||
_temp=tcoord.class
|
||||
tcoord.class=tclass
|
||||
tclass=_temp
|
||||
table.insert(nvtable,tcoord)
|
||||
end
|
||||
|
||||
return nvtable
|
||||
end
|
||||
|
||||
--Turns vector table into string
|
||||
function vtable_to_string(vt)
|
||||
cclass=nil
|
||||
result=""
|
||||
|
||||
for i=1,#vt,1 do
|
||||
if vt[i].class~=cclass then
|
||||
result=result..string.format("%s %d %d ",vt[i].class,vt[i].x,vt[i].y)
|
||||
cclass=vt[i].class
|
||||
else
|
||||
result=result..string.format("%d %d ",vt[i].x,vt[i].y)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--Rounds to the given number of decimal places
|
||||
function round(n,dec)
|
||||
dec=dec or 0
|
||||
return math.floor(n*10^dec+0.5)/(10^dec)
|
||||
end
|
||||
|
||||
--Grows vt outward by the radius r scaled by sc
|
||||
function grow(vt,r,sc)
|
||||
ch=get_chirality(vt)
|
||||
local wvt=wrap(vt)
|
||||
local nvt={}
|
||||
sc=sc or 1
|
||||
|
||||
--Grow
|
||||
for i=2,#wvt-1,1 do
|
||||
cpt=wvt[i]
|
||||
ppt=wvt[i].prev
|
||||
npt=wvt[i].next
|
||||
while distance(cpt.x,cpt.y,ppt.x,ppt.y)==0 do
|
||||
ppt=ppt.prev
|
||||
end
|
||||
while distance(cpt.x,cpt.y,npt.x,npt.y)==0 do
|
||||
npt=npt.prev
|
||||
end
|
||||
rot1=todegree(math.atan2(cpt.y-ppt.y,cpt.x-ppt.x))
|
||||
rot2=todegree(math.atan2(npt.y-cpt.y,npt.x-cpt.x))
|
||||
drot=(rot2-rot1)%360
|
||||
|
||||
--Angle to expand at
|
||||
nrot=(0.5*drot+90)%180
|
||||
if ch<0 then nrot=nrot+180 end
|
||||
|
||||
--Adjusted radius
|
||||
__ar=math.cos(torad(ch*90-nrot)) --<3
|
||||
ar=(__ar<0.00001 and r) or r/math.abs(__ar)
|
||||
|
||||
newx=cpt.x*sc
|
||||
newy=cpt.y*sc
|
||||
|
||||
if r~=0 then
|
||||
newx=newx+sc*round(ar*math.cos(torad(nrot+rot1)))
|
||||
newy=newy+sc*round(ar*math.sin(torad(nrot+rot1)))
|
||||
end
|
||||
|
||||
table.insert(nvt,{["class"]=cpt.class,
|
||||
["x"]=newx,
|
||||
["y"]=newy})
|
||||
end
|
||||
|
||||
--Check for "crossovers"
|
||||
--New data type to store points with same coordinates
|
||||
local mvt={}
|
||||
local wnvt=wrap(nvt)
|
||||
for i,p in ipairs(wnvt) do
|
||||
table.insert(mvt,{["class"]={p.class},["x"]=p.x,["y"]=p.y})
|
||||
end
|
||||
|
||||
--Number of merges so far
|
||||
merges=0
|
||||
|
||||
for i=2,#wnvt,1 do
|
||||
mi=i-merges
|
||||
dx=wvt[i].x-wvt[i-1].x
|
||||
dy=wvt[i].y-wvt[i-1].y
|
||||
ndx=wnvt[i].x-wnvt[i-1].x
|
||||
ndy=wnvt[i].y-wnvt[i-1].y
|
||||
|
||||
if (dy*ndy<0 or dx*ndx<0) then
|
||||
--Multiplicities
|
||||
c1=#mvt[mi-1].class
|
||||
c2=#mvt[mi].class
|
||||
|
||||
--Weighted average
|
||||
mvt[mi-1].x=(c1*mvt[mi-1].x+c2*mvt[mi].x)/(c1+c2)
|
||||
mvt[mi-1].y=(c1*mvt[mi-1].y+c2*mvt[mi].y)/(c1+c2)
|
||||
|
||||
--Merge classes
|
||||
mvt[mi-1].class={unpack(mvt[mi-1].class),unpack(mvt[mi].class)}
|
||||
|
||||
--Delete point
|
||||
table.remove(mvt,mi)
|
||||
merges=merges+1
|
||||
end
|
||||
end
|
||||
|
||||
--Rebuild wrapped new vector table
|
||||
wnvt={}
|
||||
for i,p in ipairs(mvt) do
|
||||
for k,pclass in ipairs(p.class) do
|
||||
table.insert(wnvt,{["class"]=pclass,["x"]=p.x,["y"]=p.y})
|
||||
end
|
||||
end
|
||||
|
||||
return unwrap(wnvt)
|
||||
end
|
||||
|
||||
function merge_identical(vt)
|
||||
local mvt = util.copy(vt)
|
||||
i=2
|
||||
lx=mvt[1].x
|
||||
ly=mvt[1].y
|
||||
while i<#mvt do
|
||||
if mvt[i].x==lx and mvt[i].y==ly then
|
||||
table.remove(mvt,i)
|
||||
else
|
||||
lx=mvt[i].x
|
||||
ly=mvt[i].y
|
||||
i=i+1
|
||||
end
|
||||
end
|
||||
return mvt
|
||||
end
|
||||
|
||||
--Returns chirality of vector shape. +1 if counterclockwise, -1 if clockwise
|
||||
function get_chirality(vt)
|
||||
local wvt=wrap(vt)
|
||||
wvt=merge_identical(wvt)
|
||||
trot=0
|
||||
for i=2,#wvt-1,1 do
|
||||
rot1=math.atan2(wvt[i].y-wvt[i-1].y,wvt[i].x-wvt[i-1].x)
|
||||
rot2=math.atan2(wvt[i+1].y-wvt[i].y,wvt[i+1].x-wvt[i].x)
|
||||
drot=todegree(rot2-rot1)%360
|
||||
if drot>180 then drot=360-drot elseif drot==180 then drot=0 else drot=-1*drot end
|
||||
trot=trot+drot
|
||||
end
|
||||
return sign(trot)
|
||||
end
|
||||
|
||||
--Duplicates first and last coordinates at the end and beginning of shape,
|
||||
--to allow for wraparound calculations
|
||||
function wrap(vt)
|
||||
local wvt={}
|
||||
table.insert(wvt,util.copy(vt[#vt]))
|
||||
for i=1,#vt,1 do
|
||||
table.insert(wvt,util.copy(vt[i]))
|
||||
end
|
||||
table.insert(wvt,util.copy(vt[1]))
|
||||
|
||||
--Add linked list capability. Because. Hacky fix gogogogo
|
||||
for i=2,#wvt-1 do
|
||||
wvt[i].prev=wvt[i-1]
|
||||
wvt[i].next=wvt[i+1]
|
||||
end
|
||||
--And link the start and end
|
||||
wvt[2].prev=wvt[#wvt-1]
|
||||
wvt[#wvt-1].next=wvt[2]
|
||||
|
||||
return wvt
|
||||
end
|
||||
|
||||
--Cuts off the first and last coordinates, to undo the effects of "wrap"
|
||||
function unwrap(wvt)
|
||||
local vt={}
|
||||
for i=2,#wvt-1,1 do
|
||||
table.insert(vt,util.copy(wvt[i]))
|
||||
end
|
||||
return vt
|
||||
end
|
||||
|
||||
--Main execution function
|
||||
function blur_clip(sub,sel)
|
||||
--GUI config
|
||||
config=
|
||||
{
|
||||
{
|
||||
class="label",
|
||||
label="Blur size:",
|
||||
x=0,y=0,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="floatedit",
|
||||
name="bsize",
|
||||
min=0,step=0.5,value=1,
|
||||
x=1,y=0,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="label",
|
||||
label="Blur position:",
|
||||
x=0,y=1,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="dropdown",
|
||||
name="bpos",
|
||||
items={"outside","middle","inside"},
|
||||
value="outside",
|
||||
x=1,y=1,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="label",
|
||||
label="Precision:",
|
||||
x=0,y=2,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="intedit",
|
||||
name="bprec",
|
||||
min=1,max=4,value=2,
|
||||
x=1,y=2,width=1,height=1
|
||||
}
|
||||
}
|
||||
|
||||
--Show dialog
|
||||
pressed,results=aegisub.dialog.display(config,{"Go","Cancel"})
|
||||
if pressed=="Cancel" then aegisub.cancel() end
|
||||
|
||||
--Size of the blur
|
||||
bsize=results["bsize"]
|
||||
|
||||
--Scale exponent for all the numbers
|
||||
sexp=results["bprec"]
|
||||
|
||||
--How far to offset the blur by
|
||||
boffset=0
|
||||
if results["bpos"]=="inside" then boffset=bsize
|
||||
elseif results["bpos"]=="middle" then boffset=bsize/2 end
|
||||
|
||||
--How far to offset the next line read
|
||||
lines_added=0
|
||||
|
||||
libLyger:set_sub(sub, sel)
|
||||
for si,li in ipairs(sel) do
|
||||
--Progress report
|
||||
aegisub.progress.task("Processing line "..si.."/"..#sel)
|
||||
aegisub.progress.set(100*si/#sel)
|
||||
|
||||
--Read in the line
|
||||
line = libLyger.lines[li]
|
||||
|
||||
--Comment it out
|
||||
line.comment=true
|
||||
sub[li+lines_added]=line
|
||||
line.comment=false
|
||||
|
||||
--Find the clipping shape
|
||||
ctype,tvector=line.text:match("\\(i?clip)%(([^%(%)]+)%)")
|
||||
|
||||
--Cancel if it doesn't exist
|
||||
if tvector==nil then
|
||||
aegisub.log("Make sure all lines have a clip statement.")
|
||||
aegisub.cancel()
|
||||
end
|
||||
|
||||
--Get position and add
|
||||
px,py = libLyger:get_pos(line)
|
||||
if line.text:match("\\pos")==nil and line.text:match("\\move")==nil then
|
||||
line.text=string.format("{\\pos(%d,%d)}",px,py)..line.text
|
||||
end
|
||||
|
||||
--Round
|
||||
local function rnd(num)
|
||||
num=tonumber(num) or 0
|
||||
if num<0 then
|
||||
num=num-0.5
|
||||
return math.ceil(num)
|
||||
end
|
||||
num=num+0.5
|
||||
return math.floor(num)
|
||||
end
|
||||
--If it's a rectangular clip, convert to vector clip
|
||||
if tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)")~=nil then
|
||||
_x1,_y1,_x2,_y2=tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)")
|
||||
tvector=string.format("m %d %d l %d %d %d %d %d %d",
|
||||
rnd(_x1),rnd(_y1),rnd(_x2),rnd(_y1),rnd(_x2),rnd(_y2),rnd(_x1),rnd(_y2))
|
||||
end
|
||||
|
||||
--The original table and original scale exponent
|
||||
otable,oexp=make_vector_table(tvector)
|
||||
|
||||
--Effective scale and scale exponent
|
||||
eexp=sexp-oexp+1
|
||||
escale=2^(eexp-1)
|
||||
--aegisub.log("Escale: %.2f",escale)
|
||||
|
||||
--The innermost line
|
||||
iline = util.copy(line)
|
||||
itable={}
|
||||
if ctype=="iclip" then
|
||||
itable=grow(otable,bsize*2^(oexp-1)-boffset,escale)
|
||||
else
|
||||
itable=grow(otable,-1*boffset,escale)
|
||||
end
|
||||
iline.text=iline.text:gsub("\\i?clip%([^%(%)]+%)","\\"..ctype.."("..sexp..","..vtable_to_string(itable)..")")
|
||||
|
||||
--Add it to the subs
|
||||
sub.insert(li+lines_added+1,iline)
|
||||
lines_added=lines_added+1
|
||||
|
||||
--Set default alpha values
|
||||
dalpha={}
|
||||
dalpha[1]=alpha_from_style(line.styleref.color1)
|
||||
dalpha[2]=alpha_from_style(line.styleref.color2)
|
||||
dalpha[3]=alpha_from_style(line.styleref.color3)
|
||||
dalpha[4]=alpha_from_style(line.styleref.color4)
|
||||
|
||||
--First tag block
|
||||
ftag=line.text:match("^{[^{}]*}")
|
||||
if ftag==nil then
|
||||
ftag="{}"
|
||||
line.text="{}"..line.text
|
||||
end
|
||||
|
||||
--List of alphas not yet accounted for in the first tag
|
||||
unacc={}
|
||||
|
||||
if ftag:match("\\alpha")==nil then
|
||||
if ftag:match("\\1a")==nil then table.insert(unacc,1) end
|
||||
if ftag:match("\\2a")==nil then table.insert(unacc,2) end
|
||||
if ftag:match("\\3a")==nil then table.insert(unacc,3) end
|
||||
if ftag:match("\\4a")==nil then table.insert(unacc,4) end
|
||||
end
|
||||
|
||||
--Add tags if any are unaccounted for
|
||||
if #unacc>0 then
|
||||
--If all the unaccounted-for alphas are equal, only add an "alpha" tag
|
||||
_tempa=dalpha[unacc[1]]
|
||||
_equal=true
|
||||
for _k,_a in ipairs(unacc) do
|
||||
if dalpha[_a]~=_tempa then _equal=false end
|
||||
end
|
||||
|
||||
if _equal then line.text=line.text:gsub("^{","{\\alpha"..dalpha[unacc[1]])
|
||||
else
|
||||
for _k,ui in ipairs(unacc) do
|
||||
line.text=line.text:gsub("^{","{\\"..ui.."a"..dalpha[ui])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
prevclip=itable
|
||||
|
||||
for j=1,math.ceil(bsize*escale*2^(oexp-1)),1 do
|
||||
|
||||
--Interpolation factor
|
||||
factor=j/(bsize*escale+1)
|
||||
|
||||
--Flip if it's an iclip
|
||||
if ctype=="iclip" then factor=1-factor end
|
||||
|
||||
--Copy the line
|
||||
tline = util.copy(line)
|
||||
|
||||
--Sub in the interpolated alphas
|
||||
tline.text=tline.text:gsub("\\alpha([^\\{}]+)",
|
||||
function(a) return "\\alpha"..interpolate_alpha(factor,a,"&HFF&") end)
|
||||
tline.text=tline.text:gsub("\\([1-4]a)([^\\{}]+)",
|
||||
function(a,b) return "\\"..a..interpolate_alpha(factor,b,"&HFF&") end)
|
||||
|
||||
--Write the correct clip
|
||||
thisclip=grow(otable,j/escale-boffset,escale)
|
||||
clipstring=vtable_to_string(thisclip)..vtable_to_string(reverse_vector_table(prevclip))
|
||||
prevclip=thisclip
|
||||
|
||||
tline.text=tline.text:gsub("\\i?clip%([^%(%)]+%)","\\clip("..sexp..","..clipstring..")")
|
||||
|
||||
--Insert the line
|
||||
sub.insert(li+lines_added+1,tline)
|
||||
lines_added=lines_added+1
|
||||
end
|
||||
end
|
||||
aegisub.set_undo_point(script_name)
|
||||
end
|
||||
|
||||
rec:registerMacro(blur_clip)
|
|
@ -1,77 +0,0 @@
|
|||
--[[
|
||||
==README==
|
||||
|
||||
Put a rectangular clip in the first line.
|
||||
|
||||
Highlight that line and all the other lines you want to add the clip to.
|
||||
|
||||
Run, and the position-shifted clip will be added to those lines
|
||||
|
||||
]]--
|
||||
|
||||
script_name = "Clip shifter"
|
||||
script_description = "Reads a rectangular clip from the first line and places it on the other highlighted ones."
|
||||
script_version = "0.2.0"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.ClipShifter"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
"aegisub.util"
|
||||
}
|
||||
}
|
||||
local LibLyger, util = rec:requireModules()
|
||||
local libLyger = LibLyger()
|
||||
|
||||
function clip_shift(sub,sel)
|
||||
libLyger:set_sub(sub, sel)
|
||||
|
||||
--Read in first line
|
||||
local first_line = libLyger.lines[sel[1]]
|
||||
|
||||
--Read in the clip
|
||||
--No need to double check, since the validate function ensures this
|
||||
local _,_,sclip1,sclip2,sclip3,sclip4=
|
||||
first_line.text:find("\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)")
|
||||
sclip1=tonumber(sclip1)
|
||||
sclip2=tonumber(sclip2)
|
||||
sclip3=tonumber(sclip3)
|
||||
sclip4=tonumber(sclip4)
|
||||
|
||||
|
||||
--Get position
|
||||
sx, sy = libLyger:get_pos(first_line)
|
||||
|
||||
for i=2,#sel,1 do
|
||||
--Read the line
|
||||
this_line = libLyger.lines[sel[i]]
|
||||
|
||||
--Get its position
|
||||
tx, ty = libLyger:get_pos(this_line)
|
||||
|
||||
--Deltas
|
||||
d_x,d_y=tx-sx,ty-sy
|
||||
|
||||
--Remove any existing rectangular clip
|
||||
this_line.text=this_line.text:gsub("\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)","")
|
||||
|
||||
--Add clip
|
||||
this_line.text=string.format("{\\clip(%d,%d,%d,%d)}",
|
||||
sclip1+d_x,sclip2+d_y,sclip3+d_x,sclip4+d_y)..this_line.text
|
||||
this_line.text=this_line.text:gsub("}{","")
|
||||
sub[sel[i]]=this_line
|
||||
end
|
||||
|
||||
aegisub.set_undo_point(script_name)
|
||||
end
|
||||
|
||||
--Make sure the first line contains a rectangular clip
|
||||
function validate_clip_shift(sub,sel)
|
||||
return #sel>1 and
|
||||
sub[sel[1]].text:find("\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)")~=nil
|
||||
end
|
||||
|
||||
rec:registerMacro(clip_shift, validate_clip_shift)
|
|
@ -1,398 +0,0 @@
|
|||
[[
|
||||
==README==
|
||||
|
||||
Frame-by-Frame Transform Automation Script
|
||||
|
||||
Smoothly transforms various parameters across multi-line, frame-by-frame typesets.
|
||||
|
||||
Useful for adding smooth transitions to frame-by-frame typesets that cannot be tracked with mocha,
|
||||
or as a substitute for the stepped \t transforms generated by the Aegisub-Motion.lua script, which
|
||||
may cause more lag than hard-coded values.
|
||||
|
||||
First generate the frame-by-frame typeset that you will be adding the effect to. Find the lines where
|
||||
you want the effect to begin and the effect to end, and visually typeset them until they look the way
|
||||
you want them to.
|
||||
|
||||
These lines will act as "keyframes", and the automation will modify all the lines in between so that
|
||||
the appearance of the first line smoothly transitions into the appearance of the last line. Simply
|
||||
highlight the first line, the last line, and all the lines in between, and run the automation.
|
||||
|
||||
It will only affect the tags that are checked in the popup menu when you run the automation. If you wish
|
||||
to save specific sets of parameters that you would like to run together, you can use the presets manager.
|
||||
For example, you can go to the presets manager, check all the color tags, and save a preset named "Colors".
|
||||
The next time you want to transform all the colors, just select "Colors" from the preset dropdown menu.
|
||||
The "All" preset is included by default and cannot be deleted. If you want a specific preset to be loaded
|
||||
when you start the script, name it "Default" when you define the preset.
|
||||
|
||||
This may be obvious, but this automation only works on one layer or one component of a frame-by-frame
|
||||
typeset at a time. If you have a frame-by-frame typeset that has two lines per frame, which looks like:
|
||||
|
||||
A1
|
||||
B1
|
||||
A2
|
||||
B2
|
||||
A3
|
||||
B3
|
||||
etc.
|
||||
|
||||
Then this automation will not work. The lines must be organized as:
|
||||
|
||||
A1
|
||||
A2
|
||||
A3
|
||||
etc.
|
||||
B1
|
||||
B2
|
||||
B3
|
||||
etc.
|
||||
|
||||
And you would have to run the automation twice, once on A and once on B. Furthermore, the text of each
|
||||
line must be exactly the same once all tags are removed. You can have as many tag blocks as you want
|
||||
in whatever positions you want for the "keyframe" lines (the first and the last). But once the tags are
|
||||
taken out, the text of the lines must be identical, down to the last space. If you are using ctrl-D or
|
||||
copy-pasting, this should be a given, but it's worth a warning.
|
||||
|
||||
The lines in between can have any tags you want in them. So long as the automation is not transforming
|
||||
those particular tags, they will be left untouched. If you need the typeset to suddenly turn huge for one
|
||||
frame, simply uncheck "fscx" and "fscy" when you run the automation, and the size of the line won't be
|
||||
touched.
|
||||
|
||||
If you are transforming rotations, there is something to watch out for. If you want a line to start
|
||||
with \frz10 and rotate to \frz350, then with default options, the line will rotate 340 degrees around the
|
||||
circle until it gets to 350. You probably wanted it to rotate only 20 degrees, passing through 0. The
|
||||
solution is to check the "Rotate in shortest direction" checkbox from the popup window. This will cause
|
||||
the line to always pick the rotation direction that has a total rotation of less than 180 degrees.
|
||||
|
||||
New feature: ignore text. Requires you to only have one tag block in each line, at the beginning.
|
||||
|
||||
|
||||
Comes with an extra automation "Remove tags" that utilizes functions that were written for the main
|
||||
automation. You can comment out (two dashes) the line at the bottom that adds this automation if you don't
|
||||
want it.
|
||||
|
||||
|
||||
TODO:
|
||||
Check that all lines text match
|
||||
iclip support
|
||||
|
||||
]]
|
||||
|
||||
export script_name = "Frame-by-frame transform"
|
||||
export script_description = "Smoothly transforms between the first and last selected lines."
|
||||
export script_version = "2.0.1"
|
||||
export script_namespace = "lyger.FbfTransform"
|
||||
|
||||
DependencyControl = require "l0.DependencyControl"
|
||||
rec = DependencyControl{
|
||||
feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version: "2.0.0", url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
{"l0.Functional", version: "0.3.0", url: "https://github.com/TypesettingTools/ASSFoundation",
|
||||
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
|
||||
}
|
||||
}
|
||||
LibLyger, Functional = rec\requireModules!
|
||||
import list, math, string, table, unicode, util, re from Functional
|
||||
logger, libLyger = rec\getLogger!, LibLyger!
|
||||
|
||||
-- tag list, grouped by dialog layout
|
||||
tags_grouped = {
|
||||
{"c", "2c", "3c", "4c"},
|
||||
{"alpha", "1a", "2a", "3a", "4a"},
|
||||
{"fscx", "fscy", "fax", "fay"},
|
||||
{"frx", "fry", "frz"},
|
||||
{"bord", "shad", "fs", "fsp"},
|
||||
{"xbord", "ybord", "xshad", "yshad"},
|
||||
{"blur", "be"},
|
||||
{"pos", "org", "clip"}
|
||||
}
|
||||
tags_flat = list.join unpack tags_grouped
|
||||
|
||||
-- default settings for every preset
|
||||
preset_defaults = { skiptext: false, flip_rot: false, accel: 1.0,
|
||||
tags: {tag, false for tag in *tags_flat }
|
||||
}
|
||||
|
||||
-- the default preset must always be available and cannot be deleted
|
||||
config = rec\getConfigHandler {
|
||||
presets: {
|
||||
Default: {}
|
||||
"[Last Settings]": {description: "Repeats the last #{script_name} operation"}
|
||||
}
|
||||
startupPreset: "Default"
|
||||
}
|
||||
unless config\load!
|
||||
-- write example preset on first time load
|
||||
config.c.presets["All"] = tags: {tag, true for tag in *tags_flat}
|
||||
config\write!
|
||||
|
||||
create_dialog = (preset) ->
|
||||
config\load!
|
||||
preset_names = [preset for preset, _ in pairs config.c.presets]
|
||||
table.sort preset_names
|
||||
dlg = {
|
||||
-- Flip rotation
|
||||
{ name: "flip_rot", class: "checkbox", x: 0, y: 9, width: 3, height: 1,
|
||||
label: "Rotate in shortest direction", value: preset.c.flip_rot },
|
||||
{ name: "skiptext", class: "checkbox", x: 3, y: 9, width: 2, height: 1,
|
||||
label: "Ignore text", value: preset.c.skiptext },
|
||||
-- Acceleration
|
||||
{ class: "label", x: 0, y: 10, width: 2, height: 1,
|
||||
label: "Acceleration: ", },
|
||||
{ name: "accel", class:"floatedit", x: 2, y: 10, width: 3, height: 1,
|
||||
value: preset.c.accel, hint: "1 means no acceleration, >1 starts slow and ends fast, <1 starts fast and ends slow" },
|
||||
{ class: "label", x: 0, y: 11, width: 2, height: 1,
|
||||
label: "Preset: " },
|
||||
{ name: "preset_select", class: "dropdown", x: 2, y: 11, width: 2, height: 1,
|
||||
items: preset_names, value: preset.section[#preset.section] },
|
||||
{ name: "preset_modify", class: "dropdown", x: 4, y: 11, width: 2, height: 1,
|
||||
items: {"Load", "Save", "Delete", "Rename"}, value: "Load" }
|
||||
}
|
||||
|
||||
-- generate tag checkboxes
|
||||
for y, group in ipairs tags_grouped
|
||||
dlg[#dlg+1] = { name: tag, class: "checkbox", x: x-1, y: y, width: 1, height: 1,
|
||||
label: "\\#{tag}", value: preset.c.tags[tag] } for x, tag in ipairs group
|
||||
|
||||
btn, res = aegisub.dialog.display dlg, {"OK", "Cancel", "Mod Preset", "Create Preset"}
|
||||
return btn, res, preset
|
||||
|
||||
save_preset = (preset, res) ->
|
||||
preset\import res, nil, true
|
||||
if res.__class != DependencyControl.ConfigHandler
|
||||
preset.c.tags[k] = res[k] for k in *tags_flat
|
||||
preset\write!
|
||||
|
||||
create_preset = (settings, name) ->
|
||||
msg = if not name
|
||||
"Onii-chan, what name would you like your preset to listen to?"
|
||||
elseif name == ""
|
||||
"Onii-chan, did you forget to name the preset?"
|
||||
elseif config.c.presets[name]
|
||||
"Onii-chan, it's not good to name a preset the same thing as another one~"
|
||||
|
||||
if msg
|
||||
btn, res = aegisub.dialog.display {
|
||||
{ class: "label", x: 0, y: 0, width: 2, height: 1, label: msg }
|
||||
{ class: "label", x: 0, y: 1, width: 1, height: 1, label: "Preset Name: " },
|
||||
{ class: "edit", x: 1, y: 1, width: 1, height: 1, name: "name", text: name }
|
||||
}
|
||||
return btn and create_preset settings, res.name
|
||||
|
||||
preset = config\getSectionHandler {"presets", name}, preset_defaults
|
||||
save_preset preset, settings
|
||||
return name
|
||||
|
||||
prepare_line = (i, preset) ->
|
||||
line = libLyger.lines[i]
|
||||
|
||||
-- Figure out the correct position and origin values
|
||||
posx, posy = libLyger\get_pos line
|
||||
orgx, orgy = libLyger\get_org line
|
||||
|
||||
-- Look for clips
|
||||
clip = {line.text\match "\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)"}
|
||||
|
||||
-- Make sure each line starts with tags
|
||||
line.text = "{}#{line.text}" unless line.text\find "^{"
|
||||
-- Turn all \1c tags into \c tags, just for convenience
|
||||
line.text = line.text\gsub "\\1c", "\\c"
|
||||
|
||||
--Separate line into a table of tags and text
|
||||
line_table = if preset.c.skiptext
|
||||
while not line.text\match "^{[^}]+}[^{]"
|
||||
line.text = line.text\gsub "}{", "", 1
|
||||
tag, text = line.text\match "^({[^}]+})(.+)$"
|
||||
{{:tag, :text}}
|
||||
else [{:tag, :text} for tag, text in line.text\gmatch "({[^}]*})([^{]*)"]
|
||||
|
||||
return line, line_table, posx, posy, orgx, orgy, #clip > 0 and clip
|
||||
|
||||
--The main body of code that runs the frame transform
|
||||
frame_transform = (sub, sel, res) ->
|
||||
-- save last settings
|
||||
preset = config\getSectionHandler {"presets", "[Last Settings]"}, preset_defaults
|
||||
save_preset preset, res
|
||||
|
||||
libLyger\set_sub sub
|
||||
-- Set the first and last lines in the selection
|
||||
first_line, start_table, sposx, sposy, sorgx, sorgy, sclip = prepare_line sel[1], preset
|
||||
last_line, end_table, eposx, eposy, eorgx, eorgy, eclip = prepare_line sel[#sel], preset
|
||||
|
||||
-- If either the first or last line do not contain a rectangular clip,
|
||||
-- you will not be clipping today
|
||||
preset.c.tags.clip = false unless sclip and eclip
|
||||
-- These are the tags to transform
|
||||
transform_tags = [tag for tag in *tags_flat when preset.c.tags[tag]]
|
||||
|
||||
-- Make sure both lines have the same splits
|
||||
LibLyger.match_splits start_table, end_table
|
||||
|
||||
-- Tables that store tables for each tag block, consisting of the state of all relevant tags
|
||||
-- that are in the transform_tags table
|
||||
start_state_table = LibLyger.make_state_table start_table, transform_tags
|
||||
end_state_table = LibLyger.make_state_table end_table, transform_tags
|
||||
|
||||
-- Insert default values when not included for the state of each tag block,
|
||||
-- or inherit values from previous tag block
|
||||
start_style = libLyger\style_lookup first_line
|
||||
end_style = libLyger\style_lookup last_line
|
||||
|
||||
current_end_state, current_start_state = {}, {}
|
||||
|
||||
for k, sval in ipairs start_state_table
|
||||
-- build current state tables
|
||||
for skey, sparam in pairs sval
|
||||
current_start_state[skey] = sparam
|
||||
|
||||
for ekey, eparam in pairs end_state_table[k]
|
||||
current_end_state[ekey] = eparam
|
||||
|
||||
-- check if end is missing any tags that start has
|
||||
for skey, sparam in pairs sval
|
||||
end_state_table[k][skey] or= current_end_state[skey] or end_style[skey]
|
||||
|
||||
-- check if start is missing any tags that end has
|
||||
for ekey, eparam in pairs end_state_table[ k]
|
||||
start_state_table[k][ekey] or= current_start_state[ekey] or start_style[ekey]
|
||||
|
||||
-- Insert proper state into each intervening line
|
||||
for i = 2, #sel-1
|
||||
aegisub.progress.set 100 * (i-1) / (#sel-1)
|
||||
this_line = libLyger.lines[sel[i]]
|
||||
|
||||
-- Turn all \1c tags into \c tags, just for convenience
|
||||
this_line.text = this_line.text\gsub "\\1c","\\c"
|
||||
|
||||
-- Remove all the relevant tags so they can be replaced with their proper interpolated values
|
||||
this_line.text = LibLyger.time_exclude this_line.text, transform_tags
|
||||
this_line.text = LibLyger.line_exclude this_line.text, transform_tags
|
||||
this_line.text = this_line.text\gsub "{}",""
|
||||
|
||||
-- Make sure this line starts with tags
|
||||
this_line.text = "{}#{this_line.text}" unless this_line.text\find "^{"
|
||||
|
||||
-- The interpolation factor for this particular line
|
||||
factor = (i-1)^preset.c.accel / (#sel-1)^preset.c.accel
|
||||
|
||||
-- Handle pos transform
|
||||
if preset.c.tags.pos then
|
||||
x = LibLyger.float2str util.interpolate factor, sposx, eposx
|
||||
y = LibLyger.float2str util.interpolate factor, sposy, eposy
|
||||
this_line.text = this_line.text\gsub "^{", "{\\pos(#{x},#{y})"
|
||||
|
||||
-- Handle org transform
|
||||
if preset.c.tags.org then
|
||||
x = LibLyger.float2str util.interpolate factor, sorgx, eorgx
|
||||
y = LibLyger.float2str util.interpolate factor, sorgy, eorgy
|
||||
this_line.text = this_line.text\gsub "^{", "{\\org(#{x},#{y})"
|
||||
|
||||
-- Handle clip transform
|
||||
if preset.c.tags.clip then
|
||||
clip = [util.interpolate factor, ord, eclip[i] for i, ord in ipairs sclip]
|
||||
logger\dump{clip, sclip, eclip}
|
||||
this_line.text = this_line.text\gsub "^{", "{\\clip(%d,%d,%d,%d)"\format unpack clip
|
||||
|
||||
-- Break the line into a table
|
||||
local this_table
|
||||
if preset.c.skiptext
|
||||
while not this_line.text\match "^{[^}]+}[^{]"
|
||||
this_line.text = this_line.text\gsub "}{", "", 1
|
||||
tag, text = this_line.text\match "^({[^}]+})(.+)$"
|
||||
this_table = {{:tag, :text}}
|
||||
else
|
||||
this_table = [{:tag, :text} for tag, text in this_line.text\gmatch "({[^}]*})([^{]*)"]
|
||||
-- Make sure it has the same splits
|
||||
j = 1
|
||||
while j <= #start_table
|
||||
stext = start_table[j].text
|
||||
ttext, ttag = this_table[j].text, this_table[j].tag
|
||||
|
||||
-- ttext might contain miscellaneous tags that are not being checked for,
|
||||
-- so remove them temporarily
|
||||
ttext_temp = ttext\gsub "{[^{}]*}", ""
|
||||
|
||||
-- If this table item has longer text, break it in two based on
|
||||
-- the text of the start table
|
||||
if #ttext_temp > #stext
|
||||
newtext = ttext_temp\match "#{LibLyger.esc stext}(.*)"
|
||||
for i = #this_table, j+1,-1
|
||||
this_table[i+1] = this_table[i]
|
||||
|
||||
this_table[j] = tag: ttag, text: ttext\gsub "#{LibLyger.esc newtext}$",""
|
||||
this_table[j+1] = tag: "{}", text: newtext
|
||||
|
||||
-- If the start table has longer text, then perhaps ttext was split
|
||||
-- at a tag that's not being transformed
|
||||
if #ttext < #stext
|
||||
-- It should be impossible for this to happen at the end, but check anyway
|
||||
assert this_table[j+1], "You fucked up big time somewhere. Sorry."
|
||||
|
||||
this_table[j].text = table.concat {ttext, this_table[j+1].tag, this_table[j+1].text}
|
||||
if this_table[j+2]
|
||||
this_table[i] = this_table[i+1] for i = j+1, #this_table-1
|
||||
|
||||
this_table[#this_table] = nil
|
||||
j -= 1
|
||||
|
||||
j += 1
|
||||
|
||||
--Interpolate all the relevant parameters and insert
|
||||
this_line.text = LibLyger.interpolate this_table, start_state_table, end_state_table,
|
||||
factor, preset
|
||||
sub[sel[i]] = this_line
|
||||
|
||||
|
||||
validate_fbf = (sub, sel) -> #sel >= 3
|
||||
|
||||
load_tags_remove = (sub, sel) ->
|
||||
pressed, res = aegisub.dialog.display {
|
||||
{ class: "label", label: "Enter the tags you would like to remove: ",
|
||||
x: 0, y: 0, width: 1,height: 1 },
|
||||
{ class: "textbox", name: "tag_list", text: "",
|
||||
x: 0, y: 1,width: 1, height: 1 },
|
||||
{ class: "checkbox", label: "Remove all EXCEPT", name: "do_except", value: false,
|
||||
x: 0,y: 2, width: 1, height: 1 }
|
||||
}, {"Remove","Cancel"}, {ok: "Remove", cancel: "Cancel"}
|
||||
|
||||
return if pressed == "Cancel"
|
||||
|
||||
tag_list = [tag for tag in res.tag_list\gmatch "\\?(%w+)[%s\\n,;]*"]
|
||||
|
||||
--Remove or remove except the tags in the table
|
||||
for li in *sel
|
||||
line = sub[li]
|
||||
f = res.do_except and LibLyger.line_exclude_except or LibLyger.line_exclude
|
||||
line.text = f(line.text, tag_list)\gsub "{}", ""
|
||||
sub[li] = line
|
||||
|
||||
fbf_gui = (sub, sel, _, preset_name = config.c.startupPreset) ->
|
||||
preset = config\getSectionHandler {"presets", preset_name}, preset_defaults
|
||||
btn, res = create_dialog preset
|
||||
|
||||
switch btn
|
||||
when "OK" do frame_transform sub, sel, res
|
||||
when "Create Preset" do fbf_gui sub, sel, nil, create_preset res
|
||||
when "Mod Preset"
|
||||
if preset_name != res.preset_select
|
||||
preset = config\getSectionHandler {"presets", res.preset_select}, preset_defaults
|
||||
preset_name = res.preset_select
|
||||
|
||||
switch res.preset_modify
|
||||
when "Delete"
|
||||
preset\delete!
|
||||
preset_name = nil
|
||||
when "Save" do save_preset preset, res
|
||||
when "Rename"
|
||||
preset_name = create_preset preset.userConfig, preset_name
|
||||
preset\delete!
|
||||
fbf_gui sub, sel, nil, preset_name
|
||||
|
||||
-- register macros
|
||||
rec\registerMacros {
|
||||
{script_name, nil, fbf_gui, validate_fbf},
|
||||
{"Remove tags", "Remove or remove all except the input tags.", load_tags_remove}
|
||||
}
|
||||
for name, preset in pairs config.c.presets
|
||||
f = (sub, sel) -> frame_transform sub, sel, config\getSectionHandler {"presets", name}
|
||||
rec\registerMacro "Presets/#{name}", preset.description, f, validate_fbf, nil, true
|
|
@ -1,227 +0,0 @@
|
|||
--[[
|
||||
==README==
|
||||
|
||||
No GUI, pretty straightforward.
|
||||
|
||||
For example, to make a line bend in an arc and transition from blue to red, do this:
|
||||
|
||||
{\frz10\c&HFF0000&}This is a line of tex{\frz350\c&H0000FF&}t
|
||||
|
||||
Run the automation and it'll add a tag before each character to make the rotation and color
|
||||
transition change smoothly across the line.
|
||||
|
||||
Rotations are locked to less than 180 degree rotations. If you want a bend of more than 180,
|
||||
then split it up into multiple rotations of less than 180 each. This script is meant for
|
||||
convenience above all, so it runs with a single button press and no time-consuming options menu.
|
||||
|
||||
]]--
|
||||
|
||||
script_name = "Gradient by character"
|
||||
script_description = "Smoothly transforms tags across your line, by character."
|
||||
script_version = "1.3.1"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.GradientByChar"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
"aegisub.util", "aegisub.re"
|
||||
}
|
||||
}
|
||||
local LibLyger, util, re = rec:requireModules()
|
||||
local libLyger = LibLyger()
|
||||
|
||||
function grad_char(sub,sel)
|
||||
libLyger:set_sub(sub, sel)
|
||||
|
||||
for si,li in ipairs(sel) do
|
||||
--Read in the line
|
||||
this_line=libLyger.lines[li]
|
||||
|
||||
--Make sure line starts with tags
|
||||
if this_line.text:find("^{")==nil then this_line.text="{}"..this_line.text end
|
||||
|
||||
--Turn all \1c tags into \c tags, just for convenience
|
||||
this_line.text=this_line.text:gsub("\\1c","\\c")
|
||||
|
||||
--Make line table
|
||||
this_table={}
|
||||
x=1
|
||||
for thistag,thistext in this_line.text:gsub("}","}\t"):gmatch("({[^{}]*})([^{}]*)") do
|
||||
this_table[x]={tag=thistag,text=thistext:gsub("\t","")}
|
||||
x=x+1
|
||||
end
|
||||
|
||||
if #this_table<2 then
|
||||
aegisub.log("There must be more than one tag block in the line!")
|
||||
return
|
||||
end
|
||||
|
||||
--Transform these tags
|
||||
transform_tags={
|
||||
"c","2c","3c","4c",
|
||||
"alpha","1a","2a","3a",
|
||||
"fscx","fscy","fax","fay",
|
||||
"frx","fry","frz",
|
||||
"fs","fsp",
|
||||
"bord","shad",
|
||||
"xbord","ybord","xshad","yshad",
|
||||
"blur","be"
|
||||
}
|
||||
|
||||
--Make state table
|
||||
this_state = LibLyger.make_state_table(this_table,transform_tags)
|
||||
|
||||
--Style lookup
|
||||
this_style = libLyger:style_lookup(this_line)
|
||||
|
||||
--Running record of the state of the line
|
||||
current_state={}
|
||||
|
||||
--Outer control loop
|
||||
for i=2,#this_table,1 do
|
||||
--Update current state
|
||||
for ctag,cval in pairs(this_state[i-1]) do
|
||||
current_state[ctag]=cval
|
||||
end
|
||||
|
||||
--Stores state of each character, to prevent redundant tags
|
||||
char_state=util.deep_copy(current_state)
|
||||
|
||||
--Local function for interpolation
|
||||
local function handle_interpolation(factor,tag,sval,eval)
|
||||
local param_type, ivalue = libLyger.param_type, ""
|
||||
--Handle differently depending on the type of tag
|
||||
if param_type[tag]=="alpha" then
|
||||
ivalue=interpolate_alpha(factor,sval,eval)
|
||||
elseif param_type[tag]=="color" then
|
||||
ivalue=interpolate_color(factor,sval,eval)
|
||||
elseif param_type[tag]=="angle" then
|
||||
nstart=tonumber(sval)
|
||||
nend=tonumber(eval)
|
||||
|
||||
--Use "Rotate in shortest direction" by default
|
||||
nstart=nstart%360
|
||||
nend=nend%360
|
||||
ndelta=nend-nstart
|
||||
if math.abs(ndelta)>180 then nstart=nstart+(ndelta*360)/math.abs(ndelta) end
|
||||
|
||||
--Interpolate
|
||||
nvalue=interpolate(factor,nstart,nend)
|
||||
if nvalue<0 then nvalue=nvalue+360 end
|
||||
|
||||
--Convert to string
|
||||
ivalue=libLyger.float2str(nvalue)
|
||||
|
||||
elseif param_type[tag]=="number" then
|
||||
nstart=tonumber(sval)
|
||||
nend=tonumber(eval)
|
||||
|
||||
--Interpolate and convert to string
|
||||
ivalue=libLyger.float2str(interpolate(factor,nstart,nend))
|
||||
end
|
||||
return ivalue
|
||||
end
|
||||
|
||||
local ttext=this_table[i-1].text
|
||||
|
||||
if ttext:len()>0 then
|
||||
|
||||
--Rebuilt text
|
||||
local rtext=""
|
||||
|
||||
--Skip the first character
|
||||
local first=true
|
||||
|
||||
--Starting values
|
||||
idx=1
|
||||
|
||||
matches=re.find(ttext,'\\\\[Nh]|\\X')
|
||||
|
||||
total=#matches
|
||||
|
||||
for _,match in ipairs(matches) do
|
||||
|
||||
ch=match.str
|
||||
|
||||
if not first then
|
||||
--Interpolation factor
|
||||
factor=idx/total
|
||||
|
||||
idx=idx+1
|
||||
|
||||
--Do nothing if the character is a space
|
||||
if ch:find("%s")~=nil then
|
||||
rtext=rtext..ch
|
||||
else
|
||||
|
||||
--The tags in and out of the time statement
|
||||
local non_time_tags=""
|
||||
|
||||
--Go through all the state tags in this tag block
|
||||
for ttag,tparam in pairs(this_state[i]) do
|
||||
--Figure out the starting state of the param
|
||||
local sparam=current_state[ttag]
|
||||
if sparam==nil then sparam=this_style[ttag] end
|
||||
if type(sparam)~="number" then sparam=sparam:gsub("%)","") end--Just in case a \t tag snuck in
|
||||
|
||||
--Prevent redundancy
|
||||
if sparam~=tparam then
|
||||
--The string version of the interpolated parameter
|
||||
local iparam=handle_interpolation(factor,ttag,sparam,tparam)
|
||||
|
||||
if iparam~=tostring(char_state[ttag]) then
|
||||
non_time_tags=non_time_tags.."\\"..ttag..iparam
|
||||
char_state[ttag]=iparam
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if non_time_tags:len() < 1 then
|
||||
--If no tags were added, do nothing
|
||||
rtext=rtext..ch
|
||||
else
|
||||
--The final tag, with a star to indicate it was added through interpolation
|
||||
rtext=rtext.."{*"..non_time_tags.."}"..ch
|
||||
end
|
||||
|
||||
end
|
||||
else
|
||||
rtext=rtext..ch
|
||||
end
|
||||
first=false
|
||||
end
|
||||
|
||||
this_table[i-1].text=rtext
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
rebuilt_text=""
|
||||
|
||||
for i,val in pairs(this_table) do
|
||||
rebuilt_text=rebuilt_text..val.tag..val.text
|
||||
end
|
||||
this_line.text=rebuilt_text
|
||||
sub[li]=this_line
|
||||
end
|
||||
|
||||
aegisub.set_undo_point(script_name)
|
||||
end
|
||||
|
||||
function remove_grad_char(sub,sel)
|
||||
for si,li in ipairs(sel) do
|
||||
this_line=sub[li]
|
||||
this_line.text=this_line.text:gsub("{%*[^{}]*}","")
|
||||
sub[li]=this_line
|
||||
end
|
||||
end
|
||||
|
||||
-- Register the macro
|
||||
rec:registerMacros{
|
||||
{"Apply Gradient", script_description, grad_char},
|
||||
{"Remove Gradient", "Removes gradient generated by #{script_name}", remove_grad_char}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,175 +0,0 @@
|
|||
--[[
|
||||
README
|
||||
|
||||
Karaoke Helper
|
||||
|
||||
Does simple karaoke tasks. Adds blank padding syllables to the beginning of lines,
|
||||
and also adjusts final syllable so it matches the line length.
|
||||
|
||||
Will add more features as ktimers suggest them to me.
|
||||
|
||||
|
||||
]]--
|
||||
|
||||
script_name = "Karaoke helper"
|
||||
script_description = "Miscellaneous tools for assisting in karaoke timing."
|
||||
script_version = "0.2.0"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.KaraHelper"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
|
||||
}
|
||||
}
|
||||
local LibLyger = rec:requireModules()
|
||||
local libLyger = LibLyger()
|
||||
|
||||
function make_config(styles)
|
||||
local stopts={"selected lines"}
|
||||
for i=1,styles.n,1 do
|
||||
stopts[i+1] = ("style: %q").format(styles[i].name)
|
||||
end
|
||||
local config=
|
||||
{
|
||||
--What to apply the automation on
|
||||
{
|
||||
class="label",
|
||||
label="Apply to:",
|
||||
x=0,y=0,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="dropdown",
|
||||
name="sselect",items=stopts,
|
||||
x=1,y=0,width=1,height=1,
|
||||
value="selected lines"
|
||||
},
|
||||
--Match syls to line length
|
||||
{
|
||||
class="checkbox",
|
||||
name="match",label="Match syllable lengths to line length",
|
||||
x=0,y=1,width=2,height=1,
|
||||
value=true
|
||||
},
|
||||
--Add blank syl at the start
|
||||
{
|
||||
class="checkbox",
|
||||
name="leadin",label="Add start padding:",
|
||||
x=0,y=2,width=1,height=1,
|
||||
value=false
|
||||
},
|
||||
{
|
||||
class="intedit",
|
||||
name="leadindur",
|
||||
x=1,y=2,width=1,height=1,
|
||||
min=0,
|
||||
value=0
|
||||
},
|
||||
--Add blank syl at the end
|
||||
{
|
||||
class="checkbox",
|
||||
name="leadout",label="Add end padding:",
|
||||
x=0,y=3,width=1,height=1,
|
||||
value=false
|
||||
},
|
||||
{
|
||||
class="intedit",
|
||||
name="leadoutdur",
|
||||
x=1,y=3,width=1,height=1,
|
||||
min=0,
|
||||
value=0
|
||||
}
|
||||
}
|
||||
return config
|
||||
end
|
||||
|
||||
--Match syllable and line durations
|
||||
function match_durs(line)
|
||||
local ldur=line.end_time-line.start_time
|
||||
local cum_sdur=0
|
||||
for sdur in line.text:gmatch("\\[Kk][fo]?(%d+)") do
|
||||
cum_sdur=cum_sdur+tonumber(sdur)
|
||||
end
|
||||
local delta=math.floor(ldur/10)-cum_sdur
|
||||
line.text=line.text:gsub("({[^{}]*\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$",
|
||||
function(pre,val,post)
|
||||
return ("%s%d%s"):format(pre, tonumber(val)+delta, post)
|
||||
end)
|
||||
return line
|
||||
end
|
||||
|
||||
--Add padding at the start
|
||||
function add_prepad(line,pdur)
|
||||
line.text=line.text:gsub("^({[^{}]*\\[Kk][fo]?)(%d+)",
|
||||
function(pre,val)
|
||||
return ("{\\k%d}%s%d"):format(pdur, pre, tonumber(val)-pdur)
|
||||
end)
|
||||
line.text=line.text:gsub("^{\\k(%d+)}({[^{}]*\\[Kk][fo]?)(%-?%d+)([^{}]*}{)",
|
||||
function(val1,mid,val2,post)
|
||||
return ("%s%d%s"):format(mid, tonumber(val1)+tonumber(val2), post)
|
||||
end)
|
||||
return line
|
||||
end
|
||||
|
||||
--Add padding at the end
|
||||
function add_postpad(line,pdur)
|
||||
line.text=line.text:gsub("(\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$",
|
||||
function(pre,val,post)
|
||||
return ("%s%d%s{\\k%d}"):format(pre, tonumber(val)-pdur, post, pdur)
|
||||
end)
|
||||
line.text=line.text:gsub("(\\[Kk][fo]?)(%-?%d+)([^{}]*}){\\k(%d+)}$",
|
||||
function(pre,val1,mid,val2)
|
||||
return ("%s%d%s"):format(pre, tonumber(val1)+tonumber(val2), mid)
|
||||
end)
|
||||
return line
|
||||
end
|
||||
|
||||
--Load config and display
|
||||
function load_kh(sub,sel)
|
||||
libLyger:set_sub(sub, sel)
|
||||
|
||||
-- Basic header collection, config, dialog display
|
||||
local config = make_config(libLyger.styles)
|
||||
local pressed,results=aegisub.dialog.display(config)
|
||||
if pressed=="Cancel" then aegisub.cancel() end
|
||||
|
||||
--Determine how to retrieve the next line, based on the dropdown selection
|
||||
local tstyle, line_cnt, get_next = results["sselect"], #sub
|
||||
|
||||
if tstyle:match("^style: ") then
|
||||
tstyle=tstyle:match("^style: \"(.+)\"$")
|
||||
get_next = function(uindex)
|
||||
for i = uindex, line_cnt do
|
||||
local line = libLyger.dialogue[uindex]
|
||||
if line.style == tstyle and (not line.comment or line.effect == "karaoke") then
|
||||
return line, i
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
get_next = function(uindex)
|
||||
if uindex <= #sel then
|
||||
return libLyger.lines[sel[uindex]], uindex+1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--Control loop
|
||||
local line, uindex = get_next(1)
|
||||
while line do
|
||||
if results["match"] then match_durs(line) end
|
||||
if results["leadin"] then add_prepad(line, results["leadindur"]) end
|
||||
if results["leadout"] then add_postpad(line, results["leadoutdur"]) end
|
||||
sub[line.i] = line
|
||||
line, uindex = get_next(uindex)
|
||||
end
|
||||
|
||||
aegisub.set_undo_point(script_name)
|
||||
end
|
||||
|
||||
rec:registerMacro(load_kh)
|
||||
|
||||
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
script_name = "Karaoke replacer"
|
||||
script_description = "Replaces the syllables of a verse."
|
||||
script_version = "0.3.0"
|
||||
script_author = "lyger"
|
||||
script_namespace = "lyger.KaraReplacer"
|
||||
|
||||
local DependencyControl = require("l0.DependencyControl")
|
||||
local rec = DependencyControl{
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json"
|
||||
}
|
||||
|
||||
--Fuck it, I should comment this code. Her goes
|
||||
function kara_replace(sub,sel)
|
||||
for si,li in ipairs(sel) do
|
||||
--Read in line
|
||||
local line = sub[li]
|
||||
|
||||
--Split at karaoke tags and create table of tags and text
|
||||
local line_table = {}
|
||||
|
||||
for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do
|
||||
table.insert(line_table,{["tag"]=tag,["text"]=text})
|
||||
end
|
||||
|
||||
--Put the line back together with spaces between syllables
|
||||
local rebuilt_parts = {}
|
||||
|
||||
for i,val in ipairs(line_table) do
|
||||
rebuilt_parts[i] = val.text
|
||||
end
|
||||
|
||||
--Add some padding so it displays better
|
||||
local rebuilt_original = table.concat(rebuilt_parts, " ")
|
||||
rebuilt_original = rebuilt_original .. string.rep(" ", math.floor(rebuilt_original:len()/2) -1)
|
||||
|
||||
--Dialog display
|
||||
local config = {
|
||||
{
|
||||
class="label",
|
||||
label=rebuilt_original,
|
||||
x=0,y=0,width=1,height=1
|
||||
}
|
||||
,
|
||||
{
|
||||
class="edit",
|
||||
name="replace",
|
||||
x=0,y=1,width=1,height=1
|
||||
}
|
||||
}
|
||||
|
||||
--Instructions
|
||||
local help_config = {
|
||||
{
|
||||
class="label",
|
||||
label=
|
||||
"The syllables of the original line will be displayed.\n"..
|
||||
"Type the syllables you would like to replace them with,\n"..
|
||||
"with spaces between each syllable.\n\n"..
|
||||
"If you want a space in the lyrics, type a double space.\n"..
|
||||
"Two join a syllable with the one after it, put a + after\n"..
|
||||
"the syllable.\n"..
|
||||
"To split a syllable (you'll have to adjust it yourself),\n"..
|
||||
"put a | where you want the split.\n"..
|
||||
"To insert a blank syllable (for padding), type _\n"..
|
||||
"You can ignore any blank syllables in the original line.\n\n"..
|
||||
"Example:\n"..
|
||||
"_ ko re wa+ re|i de su",
|
||||
x=0,y=0,width=1,height=1
|
||||
}
|
||||
}
|
||||
|
||||
--Show dialog and get input for each line
|
||||
local pressed
|
||||
|
||||
repeat
|
||||
pressed,result=aegisub.dialog.display(config,{"Next line","Help"})
|
||||
if pressed=="Help" then
|
||||
aegisub.dialog.display(help_config,{"OK"})
|
||||
end
|
||||
until pressed~="Help"
|
||||
|
||||
--Split input at spaces and store in table
|
||||
local replace = {}
|
||||
|
||||
for newsyl in result["replace"]:gsub(" ","\t "):gmatch("[^ ]+") do
|
||||
newsyl=newsyl:gsub("\t"," ")
|
||||
table.insert(replace,newsyl)
|
||||
end
|
||||
|
||||
local rebuilt_text, r = {}, 1
|
||||
--Indices of original and replacement tables
|
||||
local oi, ri = 1, 1
|
||||
|
||||
while oi<=#line_table do
|
||||
--Skip if it's a blank syl (used for padding) or we're out of replacements
|
||||
if line_table[oi].text:len()>0 and replace[ri]~=nil then
|
||||
--Handle splitting syls
|
||||
if replace[ri]:find("|")~=nil then
|
||||
|
||||
--Split the replacement line at | characters
|
||||
subtab={}
|
||||
for subsyl in replace[ri]:gmatch("[^|]+") do
|
||||
table.insert(subtab,subsyl)
|
||||
end
|
||||
|
||||
--Find the original time of the karaoke syllable
|
||||
local otime = tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)"))
|
||||
--The remaining time (for last syl, to ensure they add up to the original time)
|
||||
local ltime = otime
|
||||
--Add all but the last syl
|
||||
for x=1,#subtab-1,1 do
|
||||
--To minimize truncation error, alternate between ceil and floor
|
||||
local ttime = 0
|
||||
if x%2==1 then
|
||||
ttime = math.floor(otime/#subtab)
|
||||
else
|
||||
ttime = math.ceil(otime/#subtab)
|
||||
end
|
||||
rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ttime))
|
||||
rebuilt_text[r+1], r = subtab[x], r+2
|
||||
ltime=ltime-ttime
|
||||
end
|
||||
--Add the last syl
|
||||
rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ltime))
|
||||
rebuilt_text[r+1], r = subtab[#subtab], r+2
|
||||
|
||||
--Handle merging syls
|
||||
--Only merge if it's not the last syl
|
||||
elseif replace[ri]:find("+")~=nil and oi<#line_table then
|
||||
local temp_tag = line_table[oi].tag
|
||||
oi=oi+1
|
||||
stime=tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)"))
|
||||
temp_tag=temp_tag:gsub("(\\[kK][fo]?)(%d+)",function(a,b)
|
||||
return a..tostring(tonumber(b)+stime)
|
||||
end)
|
||||
rebuilt_text[r], rebuilt_text[r+1] = temp_tag, replace[ri]:gsub("+","")
|
||||
r = r+2
|
||||
|
||||
--The usual replacement
|
||||
else
|
||||
rebuilt_text[r], rebuilt_text[r+1] = line_table[oi].tag, replace[ri]:gsub("_","")
|
||||
r = r+2
|
||||
end
|
||||
|
||||
--Increment indices
|
||||
oi=oi+1
|
||||
ri=ri+1
|
||||
else
|
||||
rebuilt_text[r], r = line_table[oi].tag, r+1
|
||||
oi=oi+1
|
||||
end
|
||||
end
|
||||
|
||||
line.text = table.concat(rebuilt_text)
|
||||
sub[li]=line
|
||||
if finished then break end
|
||||
end
|
||||
end
|
||||
|
||||
--Old behavior. If automations are ever modified so that hitting "enter" from a text box
|
||||
--will execute the "OK" button, then this behavior is probably better.
|
||||
--For now, this function doesn't do anything
|
||||
function kara_replace_old(sub,sel)
|
||||
for si,li in ipairs(sel) do
|
||||
line=sub[li]
|
||||
|
||||
line_table={}
|
||||
|
||||
for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do
|
||||
table.insert(line_table,{["tag"]=tag,["text"]=text})
|
||||
end
|
||||
|
||||
rebuilt_text=""
|
||||
|
||||
finished=false
|
||||
|
||||
for i,val in ipairs(line_table) do
|
||||
|
||||
local function hl_syl(lt,idx)
|
||||
result=""
|
||||
for k,a in ipairs(lt) do
|
||||
if k==idx then result=result.." ["..a.text:upper().."] "
|
||||
else result=result..a.text end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
if val.text:len()<1 or finished then
|
||||
rebuilt_text=rebuilt_text..val.tag..val.text
|
||||
else
|
||||
config=
|
||||
{
|
||||
{
|
||||
class="label",
|
||||
label="Enter the syllable to replace with, or nothing to close.",
|
||||
x=0,y=0,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="label",
|
||||
label=hl_syl(line_table,i),
|
||||
x=0,y=2,width=1,height=1
|
||||
},
|
||||
{
|
||||
class="edit",
|
||||
name="replace",
|
||||
x=0,y=3,width=1,height=1
|
||||
}
|
||||
}
|
||||
_,res=aegisub.dialog.display(config,{"OK"})
|
||||
if res["replace"]:len()<1 then
|
||||
rebuilt_text=rebuilt_text..val.tag..val.text
|
||||
finished=true
|
||||
else
|
||||
rebuilt_text=rebuilt_text..val.tag..res["replace"]
|
||||
end
|
||||
end
|
||||
end
|
||||
line.text=rebuilt_text
|
||||
sub[li]=line
|
||||
if finished then break end
|
||||
end
|
||||
end
|
||||
|
||||
rec:registerMacro(kara_replace)
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,185 +0,0 @@
|
|||
-- Times a sign with {TS 3:24} to 3:24-3:25. Can convert and use a few other formats, like {3:24}, {TS,3:24}, {3,24}, etc.
|
||||
-- supported timecodes: {TS 1:23}, {TS 1:23 words}, {TS words 1:23}, {TS,1:23}, {1:23}, {1;23}, {1,23}, {1.23}, [1:23], and variations
|
||||
-- Manual: http://unanimated.xtreemhost.com/ts/scripts-manuals.htm#timesigns
|
||||
|
||||
script_name="Time Signs"
|
||||
script_description="Rough-times signs from TS timecodes"
|
||||
script_author="unanimated"
|
||||
script_version="2.8"
|
||||
script_namespace="ua.TimeSigns"
|
||||
|
||||
local haveDepCtrl,DependencyControl,depRec=pcall(require,"l0.DependencyControl")
|
||||
if haveDepCtrl then
|
||||
script_version="2.8.0"
|
||||
depRec=DependencyControl{feed="https://raw.githubusercontent.com/TypesettingTools/unanimated-Aegisub-Scripts/master/DependencyControl.json"}
|
||||
end
|
||||
|
||||
function signtime(subs,sel)
|
||||
for z=#sel,1,-1 do
|
||||
i=sel[z]
|
||||
line=subs[i]
|
||||
text=line.text
|
||||
-- format timecodes
|
||||
text=text
|
||||
:gsub("({.-})({TS.-})","%2%1")
|
||||
:gsub("(%d+):(%d%d):(%d%d)",
|
||||
function(a,b,c) return a*60+b..":"..c end) -- hours
|
||||
:gsub("^(%d%d%d%d)%s%s*","{TS %1}") -- ^1234 text
|
||||
:gsub("(%d%d)ish","%1") -- 1:23ish
|
||||
:gsub("^([%d%s:,%-]+)","{%1}") -- ^1:23, 2:34, 4:56
|
||||
:gsub("^(%d+:%d%d)%s%-%s","{TS %1}") -- ^12?:34 -
|
||||
:gsub("^(%d+:%d%d%s*)","{TS %1}") -- ^12?:34
|
||||
:gsub("^{(%d+:%d%d)","{TS %1") -- ^{12?:34
|
||||
:gsub("^%[(%d+:%d%d)%]:?%s*","{TS %1}") -- ^[12?:34]:?
|
||||
:gsub("{TS[%s%p]+(%d)","{TS %1") -- {TS ~12:34
|
||||
:gsub("({[^}]-)(%d+)[%;%.,]?(%d%d)([^:%d][^}]-})","%1%2:%3%4") -- {1;23 / 1.23 / 1,23 123}
|
||||
:gsub("{TS%s([^%d\\}]+)%s(%d+:%d%d)","{TS %2 %1") -- {TS comment 12:34}
|
||||
:gsub(":%s?}","}") -- {TS 12:34: }
|
||||
:gsub("|","\\N")
|
||||
tc=text:match("^{[^}]-}") or ""
|
||||
tc=tc:gsub("(%d+)(%d%d)([^:])","%1:%2%3")
|
||||
text=text:gsub("^{[^}]-}%s*",tc)
|
||||
if res.blur then text=text:gsub("\\blur[%d%.]+",""):gsub("{}",""):gsub("^","{\\blur"..res.bl.."}") end
|
||||
line.text=text
|
||||
|
||||
tstags=text:match("{TS[^}]-}") or ""
|
||||
|
||||
times={} -- collect times if there are more
|
||||
for tag in tstags:gmatch("%d+:%d+") do table.insert(times,tag) end
|
||||
|
||||
for t=#times,1,-1 do
|
||||
tim=times[t]
|
||||
-- convert to start time
|
||||
tstid1,tstid2=tim:match("(%d+):(%d%d)")
|
||||
if tstid1 then tid=(tstid1*60000+tstid2*1000-500) end
|
||||
-- shifting times
|
||||
if tid then
|
||||
if res.shift then tid=tid+res.secs*1000 end
|
||||
-- set start and end time [500ms before and after the timecode]
|
||||
line.start_time=tid line.end_time=(tid+1000)
|
||||
end
|
||||
|
||||
-- snapping to keyframes
|
||||
if res.snap then
|
||||
start=line.start_time
|
||||
endt=line.end_time
|
||||
startf=ms2fr(start)
|
||||
endf=ms2fr(endt)
|
||||
diff=250
|
||||
diffe=250
|
||||
startkf=keyframes[1]
|
||||
endkf=keyframes[#keyframes]
|
||||
|
||||
-- check for nearby keyframes
|
||||
for k,kf in ipairs(keyframes) do
|
||||
-- startframe snap up to 24 frames back [scroll down to change default] and 5 frames forward
|
||||
if kf>=startf-res.kfs and kf<startf+5 then
|
||||
tdiff=math.abs(startf-kf)
|
||||
if tdiff<=diff then diff=tdiff startkf=kf end
|
||||
start=fr2ms(startkf)
|
||||
line.start_time=start
|
||||
end
|
||||
-- endframe snap up to 24 frames forward [scroll down to change default] and 10 frames back
|
||||
if kf>=endf-10 and kf<=endf+res.kfe then
|
||||
tdiff=math.abs(endf-kf)
|
||||
if tdiff<diffe then diffe=tdiff endkf=kf end
|
||||
endt=fr2ms(endkf)
|
||||
line.end_time=endt
|
||||
end
|
||||
end
|
||||
end
|
||||
line.text=line.text:gsub("{TS[^}]-}","{TS "..tim.."}")
|
||||
if res.nots then line.text=line.text:gsub("{TS[^}]-}",""):gsub("{(\\[^}]-)}{(\\[^}]-)}","{%1%2}"):gsub("^({[^}]-})%s*","%1") end
|
||||
subs.insert(i+1,line)
|
||||
if t>1 then table.insert(sel,sel[#sel]+1) end
|
||||
end
|
||||
if #times>0 then subs.delete(i) end
|
||||
end
|
||||
if res.copy then taim=0 tame=0
|
||||
for z,i in ipairs(sel) do
|
||||
l=subs[i]
|
||||
if l.start_time==0 then l.start_time=taim l.end_time=tame end
|
||||
taim=l.start_time
|
||||
tame=l.end_time
|
||||
subs[i]=l
|
||||
end
|
||||
end
|
||||
return sel
|
||||
end
|
||||
|
||||
-- Config Stuff --
|
||||
function saveconfig()
|
||||
savecfg="Time Signs Settings\n\n"
|
||||
for key,val in ipairs(GUI) do
|
||||
if val.class=="floatedit" or val.class=="intedit" or val.class=="checkbox" and val.name~="save" then
|
||||
savecfg=savecfg..val.name..":"..tf(res[val.name]).."\n"
|
||||
end
|
||||
end
|
||||
file=io.open(cfgpath,"w")
|
||||
file:write(savecfg)
|
||||
file:close()
|
||||
ADD({{class="label",label="Config saved to:\n"..cfgpath}},{"OK"},{close='OK'})
|
||||
end
|
||||
|
||||
function loadconfig()
|
||||
cfgpath=aegisub.decode_path("?user").."\\timesigns.conf"
|
||||
file=io.open(cfgpath)
|
||||
if file~=nil then
|
||||
konf=file:read("*all")
|
||||
file:close()
|
||||
for key,val in ipairs(GUI) do
|
||||
if val.class=="floatedit" or val.class=="checkbox" or val.class=="intedit" then
|
||||
if konf:match(val.name) then val.value=detf(konf:match(val.name..":(.-)\n")) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function tf(val)
|
||||
if val==true then ret="true"
|
||||
elseif val==false then ret="false"
|
||||
else ret=val end
|
||||
return ret
|
||||
end
|
||||
|
||||
function detf(txt)
|
||||
if txt=="true" then ret=true
|
||||
elseif txt=="false" then ret=false
|
||||
else ret=txt end
|
||||
return ret
|
||||
end
|
||||
|
||||
|
||||
function timesigns(subs,sel)
|
||||
ADD=aegisub.dialog.display
|
||||
GUI={
|
||||
{x=0,y=0,width=4,class="label",label="Check this if all your timecodes are too late or early:"},
|
||||
{x=0,y=1,class="checkbox",name="shift",label="Shift timecodes by "},
|
||||
{x=1,y=1,width=2,class="floatedit",name="secs",value=-10,hint="Negative=backward / positive=forward",step=0.5},
|
||||
{x=3,y=1,class="label",label=" sec."},
|
||||
{x=0,y=2,width=4,class="checkbox",name="copy",label="For lines without timecodes, copy from the previous line"},
|
||||
{x=0,y=3,width=4,class="checkbox",name="nots",label="Automatically remove {TS ...} comments"},
|
||||
{x=0,y=4,width=2,class="checkbox",name="blur",label="Automatically add blur:"},
|
||||
{x=2,y=4,width=2,class="floatedit",name="bl",value="0.6"},
|
||||
{x=0,y=5,width=2,class="checkbox",name="snap",label="Snapping to keyframes:",value=true},
|
||||
{x=0,y=6,width=2,class="label",label="Frames to search back:"},
|
||||
{x=0,y=7,width=2,class="label",label="Frames to search forward:"},
|
||||
{x=2,y=6,width=2,class="intedit",name="kfs",value="24",step=1,min=1,max=250},
|
||||
{x=2,y=7,width=2,class="intedit",name="kfe",value="24",step=1,min=1,max=250},
|
||||
{x=0,y=8,width=2,class="checkbox",name="save",label="Save current settings"},
|
||||
{x=2,y=8,width=2,class="label",label=" [Time Signs v"..script_version.."]"},
|
||||
}
|
||||
loadconfig()
|
||||
buttons={"No more suffering with SHAFT signs!","Exit"}
|
||||
P,res=ADD(GUI,buttons,{ok='No more suffering with SHAFT signs!',cancel='Exit'})
|
||||
if P=="Exit" then aegisub.cancel() end
|
||||
ms2fr=aegisub.frame_from_ms
|
||||
fr2ms=aegisub.ms_from_frame
|
||||
keyframes=aegisub.keyframes()
|
||||
if res.save then saveconfig() end
|
||||
if P=="No more suffering with SHAFT signs!" then sel=signtime(subs,sel) end
|
||||
aegisub.set_undo_point(script_name)
|
||||
return sel
|
||||
end
|
||||
|
||||
if haveDepCtrl then depRec:registerMacro(timesigns) else aegisub.register_macro(script_name,script_description,timesigns) end
|
|
@ -1,142 +0,0 @@
|
|||
local json, log
|
||||
version = '1.1.4'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'ConfigHandler',
|
||||
:version,
|
||||
description: 'A class for mapping dialogs to persistent configuration.',
|
||||
author: 'torque',
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.ConfigHandler'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'json' }
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
}
|
||||
}
|
||||
json, log = version\requireModules!
|
||||
else
|
||||
json = require 'json'
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
class ConfigHandler
|
||||
@version: version
|
||||
|
||||
-- The minimum required format for `optionTables` is
|
||||
-- { section: { optionname: { value: optionvalue, config: (true|false) } } }
|
||||
-- the config key exists because this is designed to be embedded in dialog
|
||||
-- tables. Some dialog elements may not be intended to be saved to a
|
||||
-- config file, or are labels that do not return a value.
|
||||
|
||||
-- Constructor
|
||||
new: ( @optionTables, fileName, hasSections, @version = "0.0.1", filePath = "?user" ) =>
|
||||
@fileName = aegisub.decode_path "#{filePath}/#{fileName}"
|
||||
@fileHandle = nil
|
||||
loadDefault @
|
||||
|
||||
-- Private methods (I probably shouldn't have bothered to do this!)
|
||||
loadDefault = =>
|
||||
@configuration = { }
|
||||
for sectionName, configEntries in pairs @optionTables
|
||||
@configuration[sectionName] = { }
|
||||
for optionName, configEntry in pairs configEntries
|
||||
if configEntry.name != optionName and configEntry.class != "label"
|
||||
configEntry.name = optionName
|
||||
if configEntry.config
|
||||
@configuration[sectionName][optionName] = configEntry.value
|
||||
|
||||
parse = =>
|
||||
rawConfigText = @fileHandle\read '*a'
|
||||
-- Avoid clobbering the things loaded by loadDefault. I need to
|
||||
-- decide how I want to handle version changes between a script's
|
||||
-- built-in defaults and the serialized configuration on disk. This
|
||||
-- is currently biased towards a script's built-in defaults.
|
||||
parsedConfig = json.decode rawConfigText
|
||||
if parsedConfig
|
||||
for sectionName, configEntries in pairs parsedConfig
|
||||
if configSection = @configuration[sectionName]
|
||||
for optionName, optionValue in pairs configEntries
|
||||
if configSection[optionName] != nil
|
||||
configSection[optionName] = optionValue
|
||||
|
||||
doInterfaceUpdate = ( interfaceSection, sectionName ) =>
|
||||
for tableKey, tableValue in pairs interfaceSection
|
||||
if tableValue.config and @configuration[sectionName][tableKey] != nil
|
||||
tableValue.value = @configuration[sectionName][tableKey]
|
||||
|
||||
doConfigUpdate = ( newValues, sectionName ) =>
|
||||
-- have to loop across @configuration because not all of the
|
||||
-- fields in the result table are going to be serialized, and it
|
||||
-- contains no information about which ones should be and which
|
||||
-- ones should not be.
|
||||
for configKey, configValue in pairs @configuration[sectionName]
|
||||
if newValues[configKey] != nil
|
||||
@configuration[sectionName][configKey] = newValues[configKey]
|
||||
|
||||
-- Public methods
|
||||
read: =>
|
||||
if @fileHandle = io.open @fileName, 'r'
|
||||
parse @
|
||||
@fileHandle\close!
|
||||
return true
|
||||
else
|
||||
log.debug "Configuration file \"#{@fileName}\" can't be read. Writing defaults."
|
||||
@write!
|
||||
return false
|
||||
|
||||
-- todo: find keys missing from either @conf or interface, and warn
|
||||
-- (maybe error?) about mismatching config versions.
|
||||
updateInterface: ( sectionNames ) =>
|
||||
if sectionNames
|
||||
if "table" == type sectionNames
|
||||
for sectionName in *sectionNames
|
||||
if @configuration[sectionName]
|
||||
doInterfaceUpdate @, @optionTables[sectionName], sectionName
|
||||
else
|
||||
log.debug "Cannot update section %s, as it doesn't exist.", sectionName
|
||||
else
|
||||
if @configuration[sectionNames]
|
||||
doInterfaceUpdate @, @optionTables[sectionNames], sectionNames
|
||||
else
|
||||
log.debug "Cannot update section %s, as it doesn't exist.", sectionNames
|
||||
|
||||
else
|
||||
for sectionName, section in pairs @optionTables
|
||||
if @configuration[sectionName] != nil
|
||||
doInterfaceUpdate @, section, sectionName
|
||||
|
||||
-- maybe updateConfigurationFromDialog (but then we're getting into
|
||||
-- obj-c identifier verbosity territory, and I'd rather not go there)
|
||||
updateConfiguration: ( resultTable, sectionNames ) =>
|
||||
-- do nothing if sectionNames isn't defined.
|
||||
if sectionNames
|
||||
if "table" == type sectionNames
|
||||
for section in *sectionNames
|
||||
doConfigUpdate @, resultTable[section], section
|
||||
else
|
||||
doConfigUpdate @, resultTable, sectionNames
|
||||
else
|
||||
log.debug "Section Name not provided. You are doing it wrong."
|
||||
|
||||
write: =>
|
||||
-- Make sure @configuration is not an empty table.
|
||||
unless next( @configuration ) == nil
|
||||
@configuration.__version = @version
|
||||
serializedConfig = json.encode @configuration
|
||||
@configuration.__version = nil
|
||||
if @fileHandle = io.open @fileName, 'w'
|
||||
@fileHandle\write serializedConfig
|
||||
@fileHandle\close!
|
||||
else
|
||||
log.warn "Could not write the configuration file \"#{@fileName}\"."
|
||||
|
||||
delete: =>
|
||||
os.remove @fileName
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register ConfigHandler
|
||||
else
|
||||
return ConfigHandler
|
|
@ -1,132 +0,0 @@
|
|||
local log
|
||||
version = '1.0.5'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'DataHandler',
|
||||
:version,
|
||||
description: 'A class for parsing After Effects motion data.',
|
||||
author: 'torque',
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.DataHandler'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
}
|
||||
}
|
||||
log = version\requireModules!
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
class DataHandler
|
||||
@version: version
|
||||
|
||||
new: ( input, @scriptResX, @scriptResY ) =>
|
||||
-- (length-22)/4
|
||||
if input
|
||||
unless @parseRawDataString input
|
||||
@parseFile input
|
||||
|
||||
parseRawDataString: ( rawDataString ) =>
|
||||
@tableize rawDataString
|
||||
if next @rawData
|
||||
unless @rawData[1]\match "Adobe After Effects 6.0 Keyframe Data"
|
||||
return false
|
||||
width = @rawData[3]\match "Source Width[\t ]+([0-9]+)"
|
||||
height = @rawData[4]\match "Source Height[\t ]+([0-9]+)"
|
||||
unless width and height
|
||||
return false
|
||||
@xPosScale = @scriptResX/tonumber width
|
||||
@yPosScale = @scriptResY/tonumber height
|
||||
|
||||
parse @
|
||||
if #@xPosition != @length or #@yPosition != @length or #@xScale != @length or #@yScale != @length or #@zRotation != @length or 0 == @length
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
parseFile: ( fileName ) =>
|
||||
if file = io.open fileName, 'r'
|
||||
return @parseRawDataString file\read '*a'
|
||||
|
||||
return false
|
||||
|
||||
tableize: ( rawDataString ) =>
|
||||
@rawData = { }
|
||||
rawDataString\gsub "([^\r\n]+)", ( line ) ->
|
||||
table.insert @rawData, line
|
||||
|
||||
parse = =>
|
||||
-- Initialize these here so they don't get appended if
|
||||
-- parseRawDataString is called twice.
|
||||
@xPosition = { }
|
||||
@yPosition = { }
|
||||
@xScale = { }
|
||||
@yScale = @xScale
|
||||
@zRotation = { }
|
||||
length = 0
|
||||
section = 0
|
||||
for _index, line in ipairs @rawData
|
||||
unless line\match("^[\t ]+")
|
||||
if line == "Position"
|
||||
section = 1
|
||||
elseif line == "Scale"
|
||||
section = 2
|
||||
elseif line == "Rotation"
|
||||
section = 3
|
||||
else
|
||||
section = 0
|
||||
else
|
||||
line\gsub "^[\t ]+([%d%.%-]+)[\t ]+([%d%.%-e%+]+)(.*)", ( value1, value2, remainder ) ->
|
||||
switch section
|
||||
when 1
|
||||
table.insert @xPosition, @xPosScale*tonumber value2
|
||||
table.insert @yPosition, @yPosScale*tonumber remainder\match "^[\t ]+([%d%.%-e%+]+)"
|
||||
length += 1
|
||||
when 2
|
||||
-- Sort of future proof against having different scale
|
||||
-- values for different axes.
|
||||
table.insert @xScale, tonumber value2
|
||||
-- table.insert @yScale, tonumber value2
|
||||
when 3
|
||||
-- Sort of future proof having rotation around different
|
||||
-- axes.
|
||||
table.insert @zRotation, -tonumber value2
|
||||
|
||||
@length = length
|
||||
|
||||
-- Arguments: just your friendly neighborhood options table.
|
||||
stripFields: ( options ) =>
|
||||
defaults = { xPosition: @xStartPosition, yPosition: @yStartPosition, xScale: @xStartScale, zRotation: @zStartRotation }
|
||||
for field, defaultValue in pairs defaults
|
||||
unless options[field]
|
||||
for index, value in ipairs @[field]
|
||||
@[field][index] = defaultValue
|
||||
|
||||
checkLength: ( totalFrames ) =>
|
||||
if totalFrames == @length
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
addReferenceFrame: ( frame ) =>
|
||||
@startFrame = frame
|
||||
@xStartPosition = @xPosition[frame]
|
||||
@yStartPosition = @yPosition[frame]
|
||||
@zStartRotation = @zRotation[frame]
|
||||
@xStartScale = @xScale[frame]
|
||||
@yStartScale = @yScale[frame]
|
||||
|
||||
calculateCurrentState: ( frame ) =>
|
||||
@xCurrentPosition = @xPosition[frame]
|
||||
@yCurrentPosition = @yPosition[frame]
|
||||
@xRatio = @xScale[frame]/@xStartScale
|
||||
@yRatio = @yScale[frame]/@yStartScale
|
||||
@zRotationDiff = @zRotation[frame] - @zStartRotation
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register DataHandler
|
||||
else
|
||||
return DataHandler
|
|
@ -1,72 +0,0 @@
|
|||
local log, DataHandler, ShakeShapeHandler
|
||||
version = '1.0.2'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'DataWrapper',
|
||||
:version,
|
||||
description: 'A class for wrapping motion data.',
|
||||
author: 'torque',
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.DataWrapper'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.DataHandler', version: '1.0.5' }
|
||||
{ 'a-mo.ShakeShapeHandler', version: '1.0.2' }
|
||||
}
|
||||
}
|
||||
log, DataHandler, ShakeShapeHandler = version\requireModules!
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
DataHandler = require 'a-mo.DataHandler'
|
||||
ShakeShapeHandler = require 'a-mo.ShakeShapeHandler'
|
||||
|
||||
class DataWrapper
|
||||
@version: version
|
||||
new: =>
|
||||
|
||||
tryDataHandler = ( input ) =>
|
||||
@dataObject = DataHandler input, @scriptResX, @scriptResY
|
||||
if @dataObject.length
|
||||
@type = "TSR"
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
tryShakeShape = ( input ) =>
|
||||
@dataObject = ShakeShapeHandler input, @scriptResY
|
||||
if @dataObject.length
|
||||
@type = "SRS"
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
bestEffortParsingAttempt: ( input, scriptResX, scriptResY ) =>
|
||||
if "string" != type( input )
|
||||
return false
|
||||
|
||||
@scriptResX, @scriptResY = tonumber( scriptResX ), tonumber( scriptResY )
|
||||
if input\match '^Adobe After Effects 6.0 Keyframe Data'
|
||||
if tryDataHandler @, input
|
||||
return true
|
||||
|
||||
elseif input\match '^shake_shape_data 4.0'
|
||||
if tryShakeShape @, input
|
||||
return true
|
||||
|
||||
else
|
||||
if tryDataHandler @, input
|
||||
return true
|
||||
|
||||
if tryShakeShape @, input
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register DataWrapper
|
||||
else
|
||||
return DataWrapper
|
|
@ -1,483 +0,0 @@
|
|||
local util, json, log, tags, Transform
|
||||
version = '1.5.3'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'Line',
|
||||
:version,
|
||||
description: 'A class for containing and manipulating a line.',
|
||||
author: 'torque',
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.Line'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'aegisub.util' }
|
||||
{ 'json' }
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.Tags', version: '1.3.4' }
|
||||
{ 'a-mo.Transform', version: '1.2.4' }
|
||||
}
|
||||
}
|
||||
util, json, log, tags, Transform = version\requireModules!
|
||||
|
||||
else
|
||||
util = require 'aegisub.util'
|
||||
json = require 'json'
|
||||
|
||||
log = require 'a-mo.Log'
|
||||
tags = require 'a-mo.Tags'
|
||||
Transform = require 'a-mo.Transform'
|
||||
|
||||
frameFromMs = aegisub.frame_from_ms
|
||||
msFromFrame = aegisub.ms_from_frame
|
||||
|
||||
class Line
|
||||
@version: version
|
||||
|
||||
fieldsToDeepCopy: {
|
||||
'extra'
|
||||
}
|
||||
|
||||
fieldsToCopy: {
|
||||
-- Line fields
|
||||
'actor', 'class', 'comment', 'effect', 'end_time', 'layer', 'margin_l', 'margin_r', 'margin_t', 'section', 'start_time', 'style', 'text'
|
||||
-- Our fields
|
||||
'number', 'transforms', 'transformShift', 'transformsAreTokenized', 'properties', 'styleRef', 'wasLinear'
|
||||
}
|
||||
|
||||
splitChar: "\\\6"
|
||||
tPlaceholder: ( count ) -> "\\\3#{count}\\\3"
|
||||
tTokenPattern: "(\\\3(%d+)\\\3)"
|
||||
|
||||
defaultXPosition: {
|
||||
-- align 3, 6, 9
|
||||
( subResX, leftMargin, rightMargin ) ->
|
||||
return subResX - rightMargin
|
||||
-- align 1, 4, 7
|
||||
( subResX, leftMargin, rightMargin ) ->
|
||||
return leftMargin
|
||||
-- align 2, 5, 8
|
||||
( subResX, leftMargin, rightMargin ) ->
|
||||
return 0.5*subResX
|
||||
}
|
||||
|
||||
defaultYPosition: {
|
||||
-- align 1, 2, 3
|
||||
( subResY, verticalMargin ) ->
|
||||
return subResY - verticalMargin
|
||||
-- align 4, 5, 6
|
||||
( subResY, verticalMargin ) ->
|
||||
return 0.5*subResY
|
||||
-- align 7, 8, 9
|
||||
( subResY, verticalMargin ) ->
|
||||
return verticalMargin
|
||||
}
|
||||
|
||||
new: ( line, @parentCollection, overrides ) =>
|
||||
for field in *@fieldsToDeepCopy
|
||||
if "table" == type line[field]
|
||||
-- safe to assume that all fields to be deep copied are expected
|
||||
-- to be tables, otherwise they wouldn't be being deep copied
|
||||
if "table" == type( overrides ) and "table" == type overrides[field]
|
||||
@[field] = util.deep_copy overrides[field]
|
||||
else
|
||||
@[field] = util.deep_copy line[field]
|
||||
else
|
||||
if overrides[field] != nil
|
||||
@[field] = overrides[field]
|
||||
else
|
||||
@[field] = line[field]
|
||||
|
||||
if "table" == type overrides
|
||||
for field in *@fieldsToCopy
|
||||
if overrides[field] != nil
|
||||
@[field] = overrides[field]
|
||||
else
|
||||
@[field] = line[field]
|
||||
else
|
||||
for field in *@fieldsToCopy
|
||||
@[field] = line[field]
|
||||
|
||||
@duration = @end_time - @start_time
|
||||
|
||||
-- Gathers extra line metrics: the alignment and position.
|
||||
-- Returns false if there is not already a position tag in the line.
|
||||
extraMetrics: ( styleRef = @styleRef ) =>
|
||||
alignPattern = tags.allTags.align.pattern
|
||||
posPattern = tags.allTags.pos.pattern
|
||||
moveTag = tags.allTags.move
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
tagBlock\gsub alignPattern, ( value ) ->
|
||||
unless @align
|
||||
@align = tonumber value
|
||||
|
||||
tagBlock\gsub posPattern, ( value ) ->
|
||||
unless @xPosition or @move
|
||||
x, y = value\match "([%.%d%-]+),([%.%d%-]+)"
|
||||
@xPosition, @yPosition = tonumber( x ), tonumber( y )
|
||||
|
||||
tagBlock\gsub moveTag.pattern, ( value ) ->
|
||||
unless @xPosition or @move
|
||||
@move = moveTag\convert value
|
||||
|
||||
unless @align
|
||||
@align = styleRef.align
|
||||
|
||||
unless @xPosition or @move
|
||||
@xPosition, @yPosition = @getDefaultPosition!
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
formatTime = ( time ) ->
|
||||
seconds = time/1000
|
||||
minutes = seconds/60
|
||||
hours = minutes/60
|
||||
return ("%d:%02d:%05.2f")\format math.floor( hours ), math.floor( minutes%60 ), seconds%60
|
||||
|
||||
__tostring: =>
|
||||
@createRaw!
|
||||
return @raw
|
||||
|
||||
createRaw: =>
|
||||
line = {
|
||||
(@comment and ("Comment: %d")\format( @layer ) or ("Dialogue: %d")\format( @layer ))
|
||||
formatTime @start_time
|
||||
formatTime @end_time
|
||||
@style
|
||||
@actor
|
||||
@margin_l
|
||||
@margin_r
|
||||
@margin_t
|
||||
@effect
|
||||
@text
|
||||
}
|
||||
|
||||
@raw = table.concat line, ','
|
||||
|
||||
generateTagIndex: ( major, minor ) ->
|
||||
return tonumber tostring( major ) .. "." .. tostring minor
|
||||
|
||||
splitTagIndex: ( index ) ->
|
||||
major = math.floor index
|
||||
minor = tostring( index )\match "%d+.(%d+)"
|
||||
return major, tonumber minor
|
||||
|
||||
-- Tries to guarantee there will be no redundantly duplicate tags in
|
||||
-- the line. Does no other processing. Unfortunately, actually doing
|
||||
-- this perfectly is very complicated because, for example, \t() is
|
||||
-- actually position dependent. e.g. with \t(\c&HFF0000&)\c&HFF0000&,
|
||||
-- the \t will not actually do anything.
|
||||
deduplicateTags: =>
|
||||
-- Combine contiguous override blocks.
|
||||
@text = @text\gsub "}{", @splitChar
|
||||
-- note: most tags can appear multiple times in a line and only the
|
||||
-- last instance in a given tag block is used. Some tags (\pos,
|
||||
-- \move, \org, \an) can only appear once and only the first
|
||||
-- instance in the entire line is used.
|
||||
tagCollection = { }
|
||||
@runCallbackOnOverrides ( tagBlock, major ) =>
|
||||
for tag in *tags.oneTimeTags
|
||||
tagBlock = tagBlock\gsub tag.pattern, ( value ) ->
|
||||
unless tagCollection[tag.name]
|
||||
tagCollection[tag.name] = @.generateTagIndex major, tagBlock\find tag.pattern
|
||||
return nil
|
||||
else
|
||||
log.debug "#{tag.name} previously found at #{tagCollection[tag.name]}"
|
||||
return ""
|
||||
return tagBlock
|
||||
|
||||
-- Quirks: 2 clips are allowed, as long as one is vector and one is
|
||||
-- rectangular. Move and pos obviously conflict, and whichever is
|
||||
-- the first is the one that's used. The same happens with fad and
|
||||
-- fade. And again, the same with clip and iclip. Also, rectangular
|
||||
-- clips can exist inside of transforms. If a rect clip exists in a
|
||||
-- transform, its type (i or not) dictates the type of all rect
|
||||
-- clips in the line.
|
||||
for _, v in ipairs {
|
||||
{ "move", "pos" }
|
||||
{ "fade", "fad" }
|
||||
{ "rectClip", "rectiClip" }
|
||||
{ "vectClip", "vectiClip" }
|
||||
}
|
||||
if tagCollection[v[1]] and tagCollection[v[2]]
|
||||
if tagCollection[v[1]] < tagCollection[v[2]]
|
||||
-- get rid of tagCollection[v[2]]
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
tagBlock = tagBlock\gsub tags.allTags[v[2]].pattern, ""
|
||||
else
|
||||
-- get rid of tagCollection[v[1]]
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
tagBlock = tagBlock\gsub tags.allTags[v[1]].pattern, ""
|
||||
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
for tag in *tags.repeatTags
|
||||
-- Calculates the number of times the pattern will be replaced.
|
||||
_, num = tagBlock\gsub tag.pattern, ""
|
||||
-- Replaces all instances except the last one.
|
||||
tagBlock = tagBlock\gsub tag.pattern, "", num - 1
|
||||
|
||||
return tagBlock
|
||||
|
||||
-- Now the whole thing has to be rerun on the contents of all
|
||||
-- transforms.
|
||||
@text = @text\gsub @splitChar, "}{"
|
||||
@text = @text\gsub "{}", ""
|
||||
@text = @text\gsub "\\clip%(%)", "" -- useless even inside transforms
|
||||
|
||||
-- Find the first instance of an override tag in a line following
|
||||
-- startIndex.
|
||||
-- Arguments:
|
||||
-- tag [table]: A well-formatted tag table, probably taken from tags.allTags.
|
||||
-- text [string]: The text that will be searched for the tag.
|
||||
-- Default: @text, the entire line text.
|
||||
-- startIndex [number]: A number specifying the point at which the
|
||||
-- search should start.
|
||||
-- Default: 1, the beginning of the provided text block.
|
||||
|
||||
-- Returns:
|
||||
-- - The value of the tag.
|
||||
-- On error:
|
||||
-- - nil
|
||||
-- - A string containing an error message.
|
||||
getTagValue: ( tag, text = @text, startIndex = 1 ) =>
|
||||
unless tag
|
||||
return nil, "No tag table was supplied."
|
||||
|
||||
value = text\match tag.pattern, startIndex
|
||||
if value
|
||||
return tag\convert value
|
||||
else
|
||||
return nil, "The specified tag could not be found"
|
||||
|
||||
-- Find all instances of a tag in a line. Only looks through override
|
||||
-- tag blocks.
|
||||
getAllTagValues: ( tag ) =>
|
||||
values = { }
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
value = @getTagValue tag, tagBlock
|
||||
if value
|
||||
table.insert values, value
|
||||
return tagBlock
|
||||
|
||||
return values
|
||||
|
||||
-- Sets all values of a tag in a line. The provided table of values
|
||||
-- must have the same number of tables
|
||||
setAllTagValues: ( tag, values ) =>
|
||||
replacements = 1
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
tagBlock, count = tagBlock\gsub tag.pattern, ->
|
||||
tag.format\format values[replacements]
|
||||
replacements += 1
|
||||
|
||||
return tagBlock
|
||||
|
||||
-- combines getAllTagValues and setAllTagValues by running the
|
||||
-- provided callback on all of the values collected.
|
||||
modifyAllTagValues: ( tag, callback ) =>
|
||||
values = @getAllTagValues tag
|
||||
|
||||
-- Callback modifies the values table in whatever way.
|
||||
callback @, values
|
||||
|
||||
@setAllTagValues tag, values
|
||||
|
||||
-- Adds an empty override tag to the beginning of the line text if
|
||||
-- there is not an override tag there already.
|
||||
ensureLeadingOverrideBlockExists: =>
|
||||
if '{' != @text\sub 1, 1
|
||||
@text = "{}" .. @text
|
||||
|
||||
-- Runs the provided callback on all of the override tag blocks
|
||||
-- present in the line.
|
||||
runCallbackOnOverrides: ( callback, count ) =>
|
||||
major = 0
|
||||
@text = @text\gsub "({.-})", ( tagBlock ) ->
|
||||
major += 1
|
||||
return callback @, tagBlock, major,
|
||||
count
|
||||
|
||||
-- Runs the provided callback on the first override tag block in the
|
||||
-- line, provided that override tag occurs before any other text in
|
||||
-- the line.
|
||||
runCallbackOnFirstOverride: ( callback ) =>
|
||||
@text = @text\gsub "^({.-})", ( tagBlock ) ->
|
||||
return callback @, tagBlock
|
||||
|
||||
-- Runs the provided callback on all overrides that aren't the first
|
||||
-- one.
|
||||
runCallbackOnOtherOverrides: ( callback ) =>
|
||||
@text = @text\sub( 1, 1 ) .. @text\sub( 2, -1 )\gsub "({.-})", ( tagBlock ) ->
|
||||
return callback @, tagBlock
|
||||
|
||||
getPropertiesFromStyle: ( styleRef = @styleRef ) =>
|
||||
@properties = { }
|
||||
for tag in *tags.styleTags
|
||||
switch tag.type
|
||||
when "alpha"
|
||||
@properties[tag] = tag\convert styleRef[tag.style]\sub( 3, 4 )
|
||||
|
||||
when "color"
|
||||
@properties[tag] = tag\convert styleRef[tag.style]\sub( 5, 10 )
|
||||
|
||||
else
|
||||
@properties[tag] = tag\convert styleRef[tag.style]
|
||||
|
||||
-- Because duplicate tags may exist within transforms, it becomes
|
||||
-- useful to remove transforms from a line before doing various
|
||||
-- processing.
|
||||
tokenizeTransforms: =>
|
||||
unless @transformsAreTokenized
|
||||
@transforms = { }
|
||||
count = 0
|
||||
tagIndex = 0
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
tagIndex += 1
|
||||
return tagBlock\gsub tags.allTags.transform.pattern, ( transform ) ->
|
||||
count += 1
|
||||
token = @.tPlaceholder count
|
||||
transform = Transform\fromString transform, @duration, tagIndex, @
|
||||
transform.token = token
|
||||
@transforms[count] = transform
|
||||
-- create a token for the transforms
|
||||
return token
|
||||
|
||||
@transformsAreTokenized = true
|
||||
|
||||
loopOverTokenizedTransforms: ( callback ) =>
|
||||
if @transformsAreTokenized
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
return tagBlock\gsub @tTokenPattern, ( placeholder, index ) ->
|
||||
return callback @transforms[tonumber index], placeholder
|
||||
|
||||
detokenizeTransformsCopy: ( shift = 0 ) =>
|
||||
if @transformsAreTokenized
|
||||
return @text\gsub "({.-})", ( tagBlock ) ->
|
||||
return tagBlock\gsub @tTokenPattern, ( placeholder, index ) ->
|
||||
transform = @transforms[tonumber index]
|
||||
transform.startTime -= shift
|
||||
transform.endTime -= shift
|
||||
result = transform\toString!
|
||||
transform.startTime += shift
|
||||
transform.endTime += shift
|
||||
return result
|
||||
|
||||
detokenizeTransforms: ( shift = 0 ) =>
|
||||
@loopOverTokenizedTransforms ( transform, placeholder ) ->
|
||||
transform.startTime -= shift
|
||||
transform.endTime -= shift
|
||||
result = transform\toString!
|
||||
transform.startTime += shift
|
||||
transform.endTime += shift
|
||||
return result
|
||||
|
||||
@transformsAreTokenized = false
|
||||
|
||||
-- detokenize using transform.rawString
|
||||
dontTouchTransforms: =>
|
||||
@loopOverTokenizedTransforms ( transform, placeholder ) ->
|
||||
return "\\t" .. transform.rawString
|
||||
|
||||
@transformsAreTokenized = false
|
||||
|
||||
interpolateTransformsCopy: ( shift = 0, start = @start_time ) =>
|
||||
newText = @text
|
||||
@loopOverTokenizedTransforms ( transform, placeholder ) ->
|
||||
transform.startTime -= shift
|
||||
transform.endTime -= shift
|
||||
frame = frameFromMs start
|
||||
newText = transform\interpolate @, newText, placeholder, math.floor( 0.5*( msFromFrame( frame ) + msFromFrame( frame + 1 ) ) ) - start
|
||||
transform.startTime += shift
|
||||
transform.endTime += shift
|
||||
return nil
|
||||
|
||||
return newText
|
||||
|
||||
interpolateTransforms: ( shift = 0, start = @start_time ) =>
|
||||
newText = @text
|
||||
@loopOverTokenizedTransforms ( transform, placeholder ) ->
|
||||
transform.startTime -= shift
|
||||
transform.endTime -= shift
|
||||
frame = frameFromMs start
|
||||
newText = transform\interpolate @, newText, placeholder, math.floor( 0.5*( msFromFrame( frame ) + msFromFrame( frame + 1 ) ) ) - start
|
||||
transform.startTime += shift
|
||||
transform.endTime += shift
|
||||
return nil
|
||||
@text = newText
|
||||
|
||||
@transformsAreTokenized = false
|
||||
|
||||
shiftKaraoke: ( shift = @karaokeShift ) =>
|
||||
karaokeTag = tags.allTags.karaoke
|
||||
@runCallbackOnOverrides ( tagBlock ) =>
|
||||
return tagBlock\gsub karaokeTag.pattern, ( ... ) ->
|
||||
time = karaokeTag\convert ...
|
||||
|
||||
if shift > 0
|
||||
oldShift = -shift
|
||||
newTime = time - shift
|
||||
shift -= time
|
||||
if newTime > 0
|
||||
if karaokeTag.tag == "\\kf"
|
||||
return karaokeTag\format( oldShift ) .. karaokeTag\format time
|
||||
else
|
||||
return karaokeTag\format newTime
|
||||
else
|
||||
return ""
|
||||
else
|
||||
return nil
|
||||
|
||||
combineWithLine: ( line ) =>
|
||||
if @text == line.text and @style == line.style and (@start_time == line.end_time or @end_time == line.start_time)
|
||||
@start_time = math.min @start_time, line.start_time
|
||||
@end_time = math.max @end_time, line.end_time
|
||||
return true
|
||||
return false
|
||||
|
||||
delete: ( sub = @parentCollection.sub ) =>
|
||||
unless sub
|
||||
log.windowError "Sub doesn't exist, so I can't delete things. This isn't gonna work."
|
||||
unless @hasBeenDeleted
|
||||
sub.delete @number
|
||||
@hasBeenDeleted = true
|
||||
|
||||
getDefaultPosition: ( styleRef = @styleRef ) =>
|
||||
verticalMargin = if @margin_t == 0 then styleRef.margin_t else @margin_t
|
||||
leftMargin = if @margin_l == 0 then styleRef.margin_l else @margin_l
|
||||
rightMargin = if @margin_r == 0 then styleRef.margin_r else @margin_r
|
||||
align = @align or styleRef.align
|
||||
return @defaultXPosition[align%3+1]( @parentCollection.meta.PlayResX, leftMargin, rightMargin ), @defaultYPosition[math.ceil align/3]( @parentCollection.meta.PlayResY, verticalMargin )
|
||||
|
||||
setExtraData: ( field, data ) =>
|
||||
if "table" != type @extra
|
||||
@extra = {}
|
||||
|
||||
switch type data
|
||||
when "table"
|
||||
@extra[field] = json.encode data
|
||||
when "string"
|
||||
@extra[field] = data
|
||||
else
|
||||
@extra[field] = tostring data
|
||||
|
||||
getExtraData: ( field ) =>
|
||||
if "table" != type @extra
|
||||
return nil
|
||||
|
||||
value = @extra[field]
|
||||
success, res = pcall json.decode, value
|
||||
-- Should probably add something for luabins here but it is
|
||||
-- extremely stupid and dumb so I really don't want to.
|
||||
|
||||
if success
|
||||
return res
|
||||
else
|
||||
return value
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register Line
|
||||
else
|
||||
return Line
|
|
@ -1,272 +0,0 @@
|
|||
local log, Line
|
||||
version = '1.3.0'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'LineCollection'
|
||||
:version
|
||||
description: 'A class for handling collections of lines.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.LineCollection'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.Line', version: '1.5.3' }
|
||||
}
|
||||
}
|
||||
log, Line = version\requireModules!
|
||||
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
Line = require 'a-mo.Line'
|
||||
|
||||
frameFromMs = aegisub.frame_from_ms
|
||||
|
||||
class LineCollection
|
||||
@version: version
|
||||
|
||||
@fromAllLines: ( sub, validationCb, selectLines ) =>
|
||||
sel = { }
|
||||
for i = 1, #@sub
|
||||
table.insert( sel, i ) if @sub[i].class == "dialogue"
|
||||
@ sub, sel, validationCb, selectLines
|
||||
|
||||
new: ( @sub, sel, validationCb, selectLines = true ) =>
|
||||
@lines = { }
|
||||
|
||||
meta = getmetatable @
|
||||
if 'function' != type meta.__index
|
||||
metaIndex = meta.__index
|
||||
meta.__index = ( index ) =>
|
||||
if 'number' == type index
|
||||
@lines[index]
|
||||
else
|
||||
metaIndex[index]
|
||||
|
||||
if type( sel ) == "table" and #sel > 0
|
||||
@collectLines sel, validationCb, selectLines
|
||||
if frameFromMs 0
|
||||
@getFrameInfo!
|
||||
else
|
||||
for i = #@sub, 1, -1
|
||||
if @sub[i].class != "dialogue" then
|
||||
@firstLineNumber = i + 1
|
||||
@lastLineNumber = i + 1
|
||||
break
|
||||
|
||||
-- This method should update various properties such as
|
||||
-- (start|end)(Time|Frame).
|
||||
addLine: ( line, validationCb = (-> return true), selectLine = true, index = false ) =>
|
||||
if validationCb line
|
||||
line.parentCollection = @
|
||||
line.inserted = false
|
||||
line.selected = selectLine
|
||||
line.number = index == true and line.number or index or nil
|
||||
|
||||
-- if @startTime is unset, @endTime should damn well be too.
|
||||
if @startTime
|
||||
if @startTime > line.start_time
|
||||
@startTime = line.start_time
|
||||
|
||||
if @endTime < line.end_time
|
||||
@endTime = line.end_time
|
||||
|
||||
else
|
||||
@startTime = line.start_time
|
||||
@endTime = line.end_time
|
||||
|
||||
if @hasMetaStyles
|
||||
line.styleRef = @styles[line.style]
|
||||
|
||||
if @hasFrameInfo
|
||||
line.startFrame = frameFromMs line.start_time
|
||||
line.endFrame = frameFromMs line.end_time
|
||||
@startFrame = frameFromMs @startTime
|
||||
@endFrame = frameFromMs @endTime
|
||||
@totalFrames = @endFrame - @startFrame
|
||||
|
||||
table.insert @lines, line
|
||||
|
||||
generateMetaAndStyles: =>
|
||||
@styles = { }
|
||||
@meta = { }
|
||||
for i = 1, #@sub
|
||||
line = @sub[i]
|
||||
|
||||
if line.class == "style"
|
||||
@styles[line.name] = line
|
||||
-- not going to bother porting all the special-case bullshit over
|
||||
-- from karaskel.
|
||||
elseif line.class == "info"
|
||||
@meta[line.key] = line.value
|
||||
|
||||
elseif line.class == "dialogue"
|
||||
break
|
||||
|
||||
unless next @styles
|
||||
log.windowError "No styles could be found and I guarantee that's gonna break something."
|
||||
|
||||
@hasMetaStyles = true
|
||||
|
||||
collectLines: ( sel, validationCb = (( line ) -> return not line.comment), selectLines = true ) =>
|
||||
unless @hasMetaStyles
|
||||
@generateMetaAndStyles!
|
||||
|
||||
dialogueStart = 0
|
||||
for x = 1, #@sub
|
||||
if @sub[x].class == "dialogue"
|
||||
dialogueStart = x - 1 -- start line of dialogue subs
|
||||
break
|
||||
|
||||
@startTime = @sub[sel[1]].start_time
|
||||
@endTime = @sub[sel[1]].end_time
|
||||
@lastLineNumber = 0
|
||||
|
||||
for i = #sel, 1, -1
|
||||
with line = Line @sub[sel[i]], @
|
||||
if validationCb line
|
||||
.number = sel[i]
|
||||
@firstLineNumber = math.min .number, @firstLineNumber or .number
|
||||
@lastLineNumber = math.max .number, @lastLineNumber
|
||||
.inserted = true
|
||||
.hasBeenDeleted = false
|
||||
.selected = selectLines
|
||||
.humanizedNumber = .number - dialogueStart
|
||||
.styleRef = @styles[.style]
|
||||
|
||||
if .start_time < @startTime
|
||||
@startTime = .start_time
|
||||
|
||||
if .end_time > @endTime
|
||||
@endTime = .end_time
|
||||
|
||||
table.insert @lines, line
|
||||
|
||||
getFrameInfo: =>
|
||||
|
||||
for line in *@lines
|
||||
line.startFrame = frameFromMs line.start_time
|
||||
line.endFrame = frameFromMs line.end_time
|
||||
|
||||
@startFrame = frameFromMs @startTime
|
||||
@endFrame = frameFromMs @endTime
|
||||
@totalFrames = @endFrame - @startFrame
|
||||
@hasFrameInfo = true
|
||||
|
||||
callMethodOnAllLines: ( methodName, ... ) =>
|
||||
for line in *@lines
|
||||
line[methodName] line, ...
|
||||
|
||||
combineIdenticalLines: =>
|
||||
lastLine = @lines[1]
|
||||
linesToSkip = { }
|
||||
for i = 2, #@lines
|
||||
log.checkCancellation!
|
||||
|
||||
if lastLine\combineWithLine @lines[i]
|
||||
linesToSkip[#linesToSkip+1] = @lines[i]
|
||||
@shouldInsertLines = true
|
||||
continue
|
||||
else lastLine = @lines[i]
|
||||
@deleteLines linesToSkip
|
||||
|
||||
-- The third value passed to the callback is for progress reporting only,
|
||||
-- and the fourth is the actual index.
|
||||
runCallback: ( callback, reverse ) =>
|
||||
lineCount = #@lines
|
||||
if reverse
|
||||
for index = lineCount, 1, -1
|
||||
callback @, @lines[index], lineCount - index + 1, index
|
||||
else
|
||||
for index = 1, lineCount
|
||||
callback @, @lines[index], index, index
|
||||
|
||||
deleteLines: ( lines = @lines, doShift = true ) =>
|
||||
if lines.__class == Line
|
||||
lines = { lines }
|
||||
|
||||
lineSet = {line,true for _,line in pairs lines when not line.hasBeenDeleted}
|
||||
-- make sure all lines are unique and have not actually been already removed
|
||||
lines = [k for k,v in pairs lineSet]
|
||||
|
||||
@sub.delete [line.number for line in *lines when line.inserted]
|
||||
|
||||
@lastLineNumber = @firstLineNumber
|
||||
shift = #lines or 0
|
||||
for line in *@lines
|
||||
if lineSet[line]
|
||||
line.hasBeenDeleted = true
|
||||
shift -= line.inserted and 1 or 0
|
||||
elseif not line.hasBeenDeleted and line.inserted
|
||||
line.number -= doShift and shift or 0
|
||||
@lastLineNumber = math.max(line.number, @lastLineNumber)
|
||||
|
||||
insertLines: =>
|
||||
toInsert = [line for line in *@lines when not (line.inserted or line.hasBeenDeleted)]
|
||||
tailLines, numberedLines = {}, {}
|
||||
|
||||
for i = 1, #toInsert
|
||||
line = toInsert[i]
|
||||
if line.number
|
||||
numberedLines[#numberedLines + 1] = line
|
||||
line.i = i
|
||||
else
|
||||
tailLines[#tailLines + 1] = line
|
||||
line.number = @lastLineNumber + i
|
||||
line.inserted = true
|
||||
|
||||
table.sort numberedLines, ( a, b ) ->
|
||||
return (a.number < b.number) or (a.number == b.number) and (a.i < b.i)
|
||||
|
||||
for line in *numberedLines
|
||||
@sub.insert line.number, line
|
||||
line.inserted = true
|
||||
@lastLineNumber = math.max @lastLineNumber, line.number
|
||||
|
||||
tailLineCnt, chunkSize = #tailLines, 1000
|
||||
if tailLineCnt > 0
|
||||
for i = 1, tailLineCnt, chunkSize
|
||||
chunkSize = math.min chunkSize, tailLineCnt - i + 1
|
||||
@sub.insert @lastLineNumber + i, unpack tailLines, i, i+chunkSize-1
|
||||
@lastLineNumber = math.max @lastLineNumber, tailLines[tailLineCnt].number
|
||||
|
||||
replaceLines: =>
|
||||
if @shouldInsertLines
|
||||
@insertLines!
|
||||
else
|
||||
for line in *@lines
|
||||
if line.inserted and not line.hasBeenDeleted
|
||||
@sub[line.number] = line
|
||||
|
||||
getSelection: =>
|
||||
sel = [line.number for line in *@lines when line.selected and line.inserted and not line.hasBeenDeleted]
|
||||
return sel, sel[#sel]
|
||||
|
||||
__newindex: ( index, value ) =>
|
||||
if 'number' == type index
|
||||
@lines[index] = value
|
||||
else
|
||||
rawset @, index, value
|
||||
|
||||
__len: =>
|
||||
#@lines
|
||||
|
||||
__ipairs: =>
|
||||
iterator = ( tbl, i ) ->
|
||||
i += 1
|
||||
value = tbl[i]
|
||||
if value
|
||||
i, value
|
||||
iterator, @lines, 0
|
||||
|
||||
-- There's no real reason to use pairs, but I've preserved it anyways
|
||||
__pairs: =>
|
||||
next, @lines, nil
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register LineCollection
|
||||
else
|
||||
return LineCollection
|
|
@ -1,63 +0,0 @@
|
|||
return {
|
||||
version: "1.0.0"
|
||||
|
||||
debug: (...) ->
|
||||
aegisub.log 4, ...
|
||||
aegisub.log 4, '\n'
|
||||
|
||||
warn: (...) ->
|
||||
aegisub.log 2, ...
|
||||
aegisub.log 2, '\n'
|
||||
|
||||
-- I am not sure this is the logical place for this function.
|
||||
checkCancellation: ->
|
||||
if aegisub.progress.is_cancelled!
|
||||
aegisub.cancel!
|
||||
|
||||
dump: ( item, ignore ) ->
|
||||
level = 2
|
||||
if "table" != type item
|
||||
aegisub.log level, tostring item
|
||||
aegisub.log level, "\n"
|
||||
return
|
||||
|
||||
count = 1
|
||||
tablecount = 1
|
||||
|
||||
result = { "{ @#{tablecount}" }
|
||||
seen = { [item]: tablecount }
|
||||
recurse = ( item, space ) ->
|
||||
for key, value in pairs item
|
||||
unless key == ignore
|
||||
if "number" == type key
|
||||
key = "##{key}"
|
||||
if "table" == type value
|
||||
unless seen[value]
|
||||
tablecount += 1
|
||||
seen[value] = tablecount
|
||||
count += 1
|
||||
result[count] = space .. "#{key}: { @#{tablecount}"
|
||||
recurse value, space .. " "
|
||||
count += 1
|
||||
result[count] = space .. "}"
|
||||
else
|
||||
count += 1
|
||||
result[count] = space .. "#{key}: @#{seen[value]}"
|
||||
|
||||
else
|
||||
if "string" == type value
|
||||
value = ("%q")\format value
|
||||
|
||||
count += 1
|
||||
result[count] = space .. "#{key}: #{value}"
|
||||
|
||||
recurse item, " "
|
||||
|
||||
count += 1
|
||||
result[count] = "}\n"
|
||||
aegisub.log level, table.concat result, "\n"
|
||||
|
||||
windowError: ( errorMessage ) ->
|
||||
aegisub.dialog.display { { class: "label", label: errorMessage } }, { "&Close" }, { cancel: "&Close" }
|
||||
aegisub.cancel!
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
return {
|
||||
version: "1.0.0"
|
||||
|
||||
round: ( num, idp ) ->
|
||||
mult = 10^(idp or 0)
|
||||
math.floor( num * mult + 0.5 ) / mult
|
||||
|
||||
dCos: (a) ->
|
||||
math.cos math.rad a
|
||||
|
||||
dSin: (a) ->
|
||||
math.sin math.rad a
|
||||
|
||||
dAtan: (y, x) ->
|
||||
math.deg math.atan2 y, x
|
||||
|
||||
uuid: ->
|
||||
('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')\gsub "[xy]", ( char ) ->
|
||||
('%x')\format char=="x" and math.random( 0, 15 ) or math.random 8, 11
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
local log, Line, LineCollection, Math, tags, Transform
|
||||
version = '1.1.8'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'MotionHandler'
|
||||
:version
|
||||
description: 'A class for applying motion data to a LineCollection.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.MotionHandler'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.Line', version: '1.5.3' }
|
||||
{ 'a-mo.LineCollection', version: '1.3.0' }
|
||||
{ 'a-mo.Math', version: '1.0.0' }
|
||||
{ 'a-mo.Tags', version: '1.3.4' }
|
||||
{ 'a-mo.Transform', version: '1.2.4' }
|
||||
}
|
||||
}
|
||||
log, Line, LineCollection, Math, tags, Transform = version\requireModules!
|
||||
|
||||
else
|
||||
Line = require 'a-mo.Line'
|
||||
LineCollection = require 'a-mo.LineCollection'
|
||||
log = require 'a-mo.Log'
|
||||
Math = require 'a-mo.Math'
|
||||
tags = require 'a-mo.Tags'
|
||||
Transform = require 'a-mo.Transform'
|
||||
|
||||
class MotionHandler
|
||||
@version: version
|
||||
|
||||
new: ( @lineCollection, mainData, rectClipData = { }, vectClipData = { } ) =>
|
||||
-- Create a local reference to the options table.
|
||||
@options = @lineCollection.options
|
||||
@lineTrackingData = mainData.dataObject
|
||||
@rectClipData = rectClipData.dataObject
|
||||
@vectClipData = vectClipData.dataObject
|
||||
@xDelta = 0
|
||||
@yDelta = 0
|
||||
|
||||
@callbacks = { }
|
||||
|
||||
-- Do NOT perform any normal callbacks if mainData is shake
|
||||
-- rotoshape. In theory it would be possible to do plain translation
|
||||
-- because the SRS data contains a center_x and center_y field for
|
||||
-- each frame.
|
||||
unless 'SRS' == mainData.type or @options.main.clipOnly
|
||||
if @options.main.xPosition or @options.main.yPosition or @options.main.xScale or @options.main.zRotation
|
||||
|
||||
if @options.main.absPos
|
||||
@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = absolutePosition
|
||||
else
|
||||
@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = position
|
||||
|
||||
if @options.main.origin
|
||||
@callbacks["(\\org)%(([%-%d%.]+,[%-%d%.]+)%)"] = origin
|
||||
|
||||
if @options.main.xScale then
|
||||
@callbacks["(\\fsc[xy])([%d%.]+)"] = scale
|
||||
if @options.main.border
|
||||
@callbacks["(\\[xy]?bord)([%d%.]+)"] = scale
|
||||
if @options.main.shadow
|
||||
@callbacks["(\\[xy]?shad)([%-%d%.]+)"] = scale
|
||||
if @options.main.blur
|
||||
@callbacks["(\\blur)([%d%.]+)"] = blur
|
||||
|
||||
if @options.main.zRotation
|
||||
@callbacks["(\\frz?)([%-%d%.]+)"] = rotate
|
||||
|
||||
-- Don't support SRS for rectangular clips.
|
||||
if @rectClipData and 'SRS' != rectClipData.type
|
||||
@callbacks['(\\i?clip)(%([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+%))'] = rectangularClip
|
||||
|
||||
if @vectClipData
|
||||
if 'SRS' == vectClipData.type
|
||||
@callbacks['(\\i?clip)%(([^,]-)%)'] = vectorClipSRS
|
||||
else
|
||||
@callbacks['(\\i?clip)(%([^,]-%))'] = vectorClip
|
||||
|
||||
@resultingCollection = LineCollection @lineCollection.sub
|
||||
@resultingCollection.shouldInsertLines = true
|
||||
@resultingCollection.options = @options
|
||||
-- This has to be copied over for clip interpolation
|
||||
@resultingCollection.meta = @lineCollection.meta
|
||||
for line in *@lineCollection.lines
|
||||
if @options.main.linear and not (@options.main.origin and line.hasOrg) and not ((@rectClipData or @vectClipData) and line.hasClip)
|
||||
line.method = linear
|
||||
else
|
||||
line.method = nonlinear
|
||||
|
||||
applyMotion: =>
|
||||
setProgress = aegisub.progress.set
|
||||
setProgress 0
|
||||
|
||||
totalLines = #@lineCollection.lines
|
||||
-- The lines are collected in reverse order in LineCollection so
|
||||
-- that we don't need to do things in reverse here.
|
||||
insertNumber = @lineCollection.lines[totalLines].number
|
||||
for index = 1, totalLines
|
||||
with line = @lineCollection.lines[index]
|
||||
|
||||
-- start frame of line relative to start frame of tracked data
|
||||
.relativeStart = .startFrame - @lineCollection.startFrame + 1
|
||||
-- end frame of line relative to start frame of tracked data
|
||||
.relativeEnd = .endFrame - @lineCollection.startFrame
|
||||
.number = insertNumber
|
||||
.method @, line
|
||||
|
||||
setProgress index/totalLines*100
|
||||
|
||||
return @resultingCollection
|
||||
|
||||
linear = ( line ) =>
|
||||
moveTag = tags.allTags.move
|
||||
posTag = tags.allTags.pos
|
||||
with line
|
||||
startFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .start_time
|
||||
frameAfterStartTime = aegisub.ms_from_frame aegisub.frame_from_ms( .start_time ) + 1
|
||||
frameBeforeEndTime = aegisub.ms_from_frame aegisub.frame_from_ms( .end_time ) - 1
|
||||
endFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .end_time
|
||||
-- Calculates the time length (in ms) from the start of the first
|
||||
-- subtitle frame to the actual start of the line time.
|
||||
beginTime = math.floor 0.5*(startFrameTime + frameAfterStartTime) - .start_time
|
||||
-- Calculates the total length of the line plus the difference
|
||||
-- (which is negative) between the start of the last frame the
|
||||
-- line is on and the end time of the line.
|
||||
endTime = math.floor 0.5*(frameBeforeEndTime + endFrameTime) - .start_time
|
||||
|
||||
if .move
|
||||
.text = .text\gsub moveTag.pattern, ->
|
||||
move = .move
|
||||
progress = (.start_time - move.start)/(move.end - move.start)
|
||||
return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress
|
||||
|
||||
for pattern, callback in pairs @callbacks
|
||||
log.checkCancellation!
|
||||
.text = .text\gsub pattern, ( tag, value ) ->
|
||||
values = { }
|
||||
for frame in *{ .relativeStart, .relativeEnd }
|
||||
@lineTrackingData\calculateCurrentState frame
|
||||
values[#values+1] = callback @, value, frame
|
||||
("%s%s\\t(%d,%d,%s%s)")\format tag, values[1], beginTime, endTime, tag, values[2]
|
||||
|
||||
if @options.main.xPosition or @options.main.yPosition
|
||||
.text = .text\gsub "\\pos(%b())\\t%((%d+,%d+),\\pos(%b())%)", ( start, time, finish ) ->
|
||||
"\\move" .. start\sub( 1, -2 ) .. ',' .. finish\sub( 2, -2 ) .. ',' .. time .. ")"
|
||||
|
||||
@resultingCollection\addLine Line( line, nil, { wasLinear: true } ), nil, true, true
|
||||
|
||||
nonlinear = ( line ) =>
|
||||
moveTag = tags.allTags.move
|
||||
posTag = tags.allTags.pos
|
||||
for frame = line.relativeEnd, line.relativeStart, -1
|
||||
with line
|
||||
|
||||
log.checkCancellation!
|
||||
|
||||
newStartTime = math.floor(math.max(0, aegisub.ms_from_frame( @lineCollection.startFrame + frame - 1 ))/10)*10
|
||||
newEndTime = math.floor(aegisub.ms_from_frame( @lineCollection.startFrame + frame )/10)*10
|
||||
|
||||
timeDelta = newStartTime - math.floor(math.max(0,aegisub.ms_from_frame( @lineCollection.startFrame + .relativeStart - 1 ))/10)*10
|
||||
|
||||
local newText
|
||||
if @options.main.killTrans
|
||||
newText = \interpolateTransformsCopy timeDelta, newStartTime
|
||||
else
|
||||
newText = \detokenizeTransformsCopy timeDelta
|
||||
|
||||
fadeTag = tags.allTags.fade
|
||||
if @options.main.killTrans
|
||||
local fade
|
||||
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
|
||||
fade = fadeTag\convert value
|
||||
return ""
|
||||
|
||||
if fade
|
||||
-- multiplies every alpha tag by a scaling factor determined by the fade envelope
|
||||
local fadeFactor
|
||||
-- Piecewise function done with logicals :)
|
||||
-- probably should go in fadeTag\interpolate, but I dunno how that whole section is supposed to work
|
||||
f = { k, tonumber v for k, v in pairs fade }
|
||||
fadeFactor = (
|
||||
(timeDelta < f.t1) and f.a1 or
|
||||
(timeDelta < f.t2) and f.a1 + (f.a2 - f.a1) * (timeDelta - f.t1) / (f.t2 - f.t1) or
|
||||
(timeDelta < f.t3) and f.a2 or
|
||||
(timeDelta < f.t4) and f.a2 + (f.a3 - f.a2) * (timeDelta - f.t3) / (f.t4 - f.t3) or
|
||||
f.a3
|
||||
)
|
||||
-- factor between 0 and 1 representing opacity of fade line
|
||||
fadeFactor = (255 - fadeFactor) / 255
|
||||
|
||||
-- apply the opacity factor to all alpha tags
|
||||
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub "(\\[1234]?a[lpha]-)&H(%x%x)&", ( alpha, value ) ->
|
||||
value = Math.round( 255 - ( fadeFactor * (255 - tonumber value, 16) ) )
|
||||
return alpha .. "&H%02X&"\format value
|
||||
|
||||
else
|
||||
-- modified each fade tag in every override block
|
||||
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
|
||||
fade = fadeTag\convert value
|
||||
-- Erroneous fade tags are being ignored and left sitting around as long as they doesn't have 2 or 7 arguments.
|
||||
-- if t1 == nil
|
||||
-- message = "There is a malformed \\fade you must fix.\n\\fade requires 7 integer arguments.\nLine: #{.number}, tag: \\fade(#{fade})."
|
||||
-- if fade\match("(%d+),(%d+)")
|
||||
-- message ..= "\nPerhaps you meant to use \\fad."
|
||||
-- log.windowError message
|
||||
for i = 4, 7 -- t1 - t4
|
||||
fade[i] -= timeDelta
|
||||
return fadeTag\format fade
|
||||
|
||||
if .move
|
||||
newText = newText\gsub moveTag.pattern, ->
|
||||
move = .move
|
||||
progress = (timeDelta - move.start)/(move.end - move.start)
|
||||
return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress
|
||||
|
||||
-- In theory, this is more optimal if we loop over the frames on
|
||||
-- the outside loop and over the lines on the inside loop, as
|
||||
-- this only needs to be calculated once for each frame, whereas
|
||||
-- currently it is being calculated for each frame for each
|
||||
-- line. However, if the loop structure is changed, then
|
||||
-- inserting lines into the resultingCollection would need to be
|
||||
-- more clever to compensate for the fact that lines would no
|
||||
-- longer be added to it in order.
|
||||
@lineTrackingData\calculateCurrentState frame
|
||||
|
||||
-- iterate through the necessary operations
|
||||
for pattern, callback in pairs @callbacks
|
||||
newText = newText\gsub pattern, ( tag, value ) ->
|
||||
tag .. callback @, value, frame
|
||||
|
||||
newLine = Line line, @resultingCollection, {
|
||||
text: newText,
|
||||
start_time: newStartTime,
|
||||
end_time: newEndTime,
|
||||
transformsAreTokenized: false,
|
||||
}
|
||||
newLine.karaokeShift = (newStartTime - .start_time)*0.1
|
||||
|
||||
@resultingCollection\addLine newLine, nil, true, true
|
||||
|
||||
position = ( pos, frame ) =>
|
||||
x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
|
||||
x, y = positionMath x, y, @lineTrackingData
|
||||
("(%g,%g)")\format Math.round( x, 2 ), Math.round( y, 2 )
|
||||
|
||||
positionMath = ( x, y, data ) ->
|
||||
x = (tonumber( x ) - data.xStartPosition)*data.xRatio
|
||||
y = (tonumber( y ) - data.yStartPosition)*data.yRatio
|
||||
radius = math.sqrt( x^2 + y^2 )
|
||||
alpha = Math.dAtan( y, x )
|
||||
x = data.xCurrentPosition + radius*Math.dCos( alpha - data.zRotationDiff )
|
||||
y = data.yCurrentPosition + radius*Math.dSin( alpha - data.zRotationDiff )
|
||||
return x, y
|
||||
|
||||
absolutePosition = ( pos, frame ) =>
|
||||
x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
|
||||
@xDelta = @lineTrackingData.xPosition[frame] - x
|
||||
@yDelta = @lineTrackingData.yPosition[frame] - y
|
||||
("(%g,%g)")\format Math.round( @lineTrackingData.xPosition[frame], 2 ), Math.round( @lineTrackingData.yPosition[frame], 2 )
|
||||
|
||||
-- Needs to be fixed.
|
||||
origin = ( origin, frame ) =>
|
||||
ox, oy = origin\match("([%-%d%.]+),([%-%d%.]+)")
|
||||
ox, oy = positionMath ox, oy, @lineTrackingData
|
||||
("(%g,%g)")\format Math.round( ox, 2 ), Math.round( oy, 2 )
|
||||
|
||||
scale = ( scale, frame ) =>
|
||||
scale *= @lineTrackingData.xRatio
|
||||
tostring Math.round scale, 2
|
||||
|
||||
blur = ( blur, frame ) =>
|
||||
ratio = @lineTrackingData.xRatio
|
||||
ratio = 1 - (1 - ratio)*@options.main.blurScale
|
||||
|
||||
tostring Math.round blur*ratio, 2
|
||||
|
||||
rotate = ( rotation, frame ) =>
|
||||
rotation += @lineTrackingData.zRotationDiff
|
||||
tostring Math.round rotation, 2
|
||||
|
||||
rectangularClip = ( clip, frame ) =>
|
||||
@rectClipData\calculateCurrentState frame
|
||||
@rectClipData.zRotationDiff = 0
|
||||
|
||||
return clip\gsub "([%.%d%-]+),([%.%d%-]+)", ( x, y ) ->
|
||||
x, y = x + @xDelta, y + @yDelta
|
||||
x, y = positionMath x, y, @rectClipData
|
||||
("%g,%g")\format Math.round( x, 2 ), Math.round( y, 2 )
|
||||
|
||||
vectorClip = ( clip, frame ) =>
|
||||
-- This is redundant if vectClipData is the same as
|
||||
-- lineTrackingData.
|
||||
@vectClipData\calculateCurrentState frame
|
||||
|
||||
return clip\gsub "([%.%d%-]+) ([%.%d%-]+)", ( x, y ) ->
|
||||
x, y = x + @xDelta, y + @yDelta
|
||||
x, y = positionMath x, y, @vectClipData
|
||||
("%g %g")\format Math.round( x, 2 ), Math.round( y, 2 )
|
||||
|
||||
vectorClipSRS = ( clip, frame ) =>
|
||||
return '(' .. clip .. ' ' .. @vectClipData.data[frame]\sub( 1, -2 ) .. ')'
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register MotionHandler
|
||||
else
|
||||
return MotionHandler
|
|
@ -1,130 +0,0 @@
|
|||
local log
|
||||
version = '1.0.2'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'ShakeShapeHandler'
|
||||
:version
|
||||
description: 'A class for parsing shake shape motion data.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.ShakeShapeHandler'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
}
|
||||
}
|
||||
log = version\requireModules!
|
||||
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
class ShakeShapeHandler
|
||||
@version: version
|
||||
|
||||
new: ( input, @scriptHeight ) =>
|
||||
if input
|
||||
unless @parseRawDataString input
|
||||
@parseFile input
|
||||
|
||||
if @rawData
|
||||
@createDrawings!
|
||||
|
||||
parseRawDataString: ( rawDataString ) =>
|
||||
if rawDataString\match "^shake_shape_data 4.0"
|
||||
@tableize rawDataString
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
parseFile: ( fileName ) =>
|
||||
if fileName\match "^\"[^\"]-\"$"
|
||||
fileName = fileName\sub 2, -2
|
||||
if file = io.open fileName, 'r'
|
||||
return @parseRawDataString file\read '*a'
|
||||
|
||||
return false
|
||||
|
||||
tableize: ( rawDataString ) =>
|
||||
shapes = rawDataString\match "num_shapes (%d+)"
|
||||
@rawData = { }
|
||||
rawDataString\gsub "([^\r\n]+)", ( line ) ->
|
||||
if line\match "vertex_data"
|
||||
table.insert @rawData, line
|
||||
|
||||
@numShapes = tonumber shapes
|
||||
@length = #@rawData / @numShapes
|
||||
|
||||
createDrawings: =>
|
||||
@data = { }
|
||||
for baseIndex = 1, @length
|
||||
results = { }
|
||||
|
||||
for curveIndex = baseIndex, @numShapes*@length, @length
|
||||
line = @rawData[curveIndex]
|
||||
table.insert results, convertVertex line, @scriptHeight
|
||||
|
||||
table.insert @data, table.concat results, ' '
|
||||
|
||||
fields = { "vx", "vy", "lx", "ly", "rx", "ry" }
|
||||
updateCurve = ( curve, height, args ) ->
|
||||
for index = 1, 6
|
||||
field = fields[index]
|
||||
if index % 2 == 0
|
||||
curve[field] = height - args[index]
|
||||
else
|
||||
curve[field] = args[index]
|
||||
|
||||
processSegment = ( curveState, prevCurve, currCurve ) ->
|
||||
result = ''
|
||||
if ( prevCurve.vx == prevCurve.rx and prevCurve.vy == prevCurve.ry and
|
||||
currCurve.lx == currCurve.vx and currCurve.ly == currCurve.vy )
|
||||
unless curveState == 'l'
|
||||
curveState = 'l'
|
||||
result ..= 'l '
|
||||
return curveState, result .. "#{currCurve.vx} #{currCurve.vy} "
|
||||
else
|
||||
unless curveState == 'b'
|
||||
curveState = 'b'
|
||||
result ..= 'b '
|
||||
return curveState, result .. "#{prevCurve.rx} #{prevCurve.ry} #{currCurve.lx} #{currCurve.ly} #{currCurve.vx} #{currCurve.vy} "
|
||||
|
||||
|
||||
convertVertex = ( vertex, scriptHeight ) ->
|
||||
curveState = 'm'
|
||||
drawString = {'m '}
|
||||
prevCurve = { }
|
||||
currCurve = { }
|
||||
vertex = vertex\gsub "vertex_data ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+", ( ... ) ->
|
||||
updateCurve prevCurve, scriptHeight, { ... }
|
||||
table.insert drawString, "#{prevCurve.vx} #{prevCurve.vy} "
|
||||
return ""
|
||||
|
||||
firstCurve = { k, v for k, v in pairs prevCurve }
|
||||
|
||||
vertex\gsub "([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+", ( ... ) ->
|
||||
updateCurve currCurve, scriptHeight, { ... }
|
||||
curveState, segment = processSegment curveState, prevCurve, currCurve
|
||||
table.insert drawString, segment
|
||||
prevCurve, currCurve = currCurve, prevCurve
|
||||
|
||||
curveState, segment = processSegment curveState, prevCurve, firstCurve
|
||||
table.insert drawString, segment
|
||||
return table.concat drawString
|
||||
|
||||
-- A function stub because I am too lazy to do this sort of thing
|
||||
-- properly.
|
||||
calculateCurrentState: =>
|
||||
|
||||
checkLength: ( totalFrames ) =>
|
||||
if totalFrames == @length
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register ShakeShapeHandler
|
||||
else
|
||||
return ShakeShapeHandler
|
|
@ -1,153 +0,0 @@
|
|||
local json, log
|
||||
version = '0.1.3'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'Statistics'
|
||||
:version
|
||||
description: 'A class for proving how cool you are.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.Statistics'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'json' }
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
}
|
||||
}
|
||||
json, log = version\requireModules!
|
||||
|
||||
else
|
||||
json = require 'json'
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
-- example = {
|
||||
-- macroRunCount: {
|
||||
-- Apply: 0
|
||||
-- Trim: 0
|
||||
-- Revert: 0
|
||||
-- }
|
||||
-- longestLine: 0
|
||||
-- longestTrack: 0
|
||||
-- largestOutput: 0
|
||||
-- totalProduced: 0
|
||||
-- uuid: 0
|
||||
-- }
|
||||
|
||||
-- No way to migrate to different layouts. Seems like a pain in the ass.
|
||||
-- Probably won't get implemented.
|
||||
class Statistics
|
||||
@version: version
|
||||
|
||||
new: ( @stats, fileName, filePath = aegisub.decode_path( '?user' ) ) =>
|
||||
@fileName = ('%s/%s')\format filePath, fileName
|
||||
@read!
|
||||
|
||||
merge = ( memory, disk, seenTables ) ->
|
||||
unless seenTables[memory]
|
||||
seenTables[memory] = true
|
||||
for k, memVal in pairs memory
|
||||
if ("table" == type( disk )) and (nil != disk[k])
|
||||
diskVal = disk[k]
|
||||
if ("table" == type( diskVal )) and ("table" == type( memVal ))
|
||||
merge memVal, diskVal, seenTables
|
||||
else
|
||||
memory[k] = diskVal
|
||||
|
||||
read: =>
|
||||
if fileHandle = io.open @fileName, 'r'
|
||||
success, serializedStats = pcall json.decode, fileHandle\read '*a'
|
||||
fileHandle\close!
|
||||
unless success
|
||||
log.warn "Couldn't parse stats from #{@filename} as valid json. This file will be overwritten."
|
||||
@write!
|
||||
return
|
||||
|
||||
if serializedStats
|
||||
merge @stats, serializedStats, {}
|
||||
|
||||
else
|
||||
@write!
|
||||
|
||||
write: =>
|
||||
if fileHandle = io.open @fileName, 'w'
|
||||
serializedStats = json.encode @stats
|
||||
if serializedStats
|
||||
fileHandle\write serializedStats
|
||||
else
|
||||
log.debug "Couldn't serialize stats."
|
||||
fileHandle\close!
|
||||
else
|
||||
log.debug "Can't write statsfile: #{@fileName}"
|
||||
|
||||
fullFieldNamePriv = false
|
||||
fieldBaseNamePriv = false
|
||||
fieldNamePriv = false
|
||||
fieldPriv = false
|
||||
|
||||
pushFieldPriv = ( fieldName ) =>
|
||||
-- primary cache: nothing needs to change.
|
||||
if fullFieldNamePriv == fieldName
|
||||
return
|
||||
|
||||
-- secondary cache: fieldPriv doesn't need to change, nor does
|
||||
-- fieldBaseNamePriv, but fullFieldNamePriv and fieldNamePriv do.
|
||||
tempFieldName = fieldName\gsub ".+%.", ""
|
||||
if fieldBaseNamePriv == fieldName\sub 0, -(#tempFieldName + 2)
|
||||
fullFieldNamePriv = fieldName
|
||||
fieldNamePriv = tempFieldName
|
||||
return
|
||||
|
||||
-- Have to do everything from scratch.
|
||||
fieldPriv = @stats
|
||||
done = false
|
||||
fieldNamePriv = fieldName\gsub "([^%.]+)%.", ( subName ) ->
|
||||
if done
|
||||
return nil
|
||||
if "table" != type fieldPriv[subName]
|
||||
done = true
|
||||
return nil
|
||||
fieldPriv = fieldPriv[subName]
|
||||
return ""
|
||||
|
||||
-- Bad things will occur if fieldNamePriv has a '.' in it.
|
||||
fullFieldNamePriv = fieldName
|
||||
fieldBaseNamePriv = fieldName\sub 0, -(#fieldNamePriv + 2)
|
||||
|
||||
-- Accept syntax like 'macroRunCount.Apply' for fieldName.
|
||||
-- `valueCb` is a function with the signature ( currentValue )
|
||||
setValuePriv = ( fieldName, valueCb ) =>
|
||||
pushFieldPriv @, fieldName
|
||||
fieldPriv[fieldNamePriv] = valueCb fieldPriv[fieldNamePriv]
|
||||
|
||||
incrementValue: ( fieldName, amount = 1 ) =>
|
||||
setValuePriv @, fieldName, ( value ) ->
|
||||
value + amount
|
||||
|
||||
-- Convenience.
|
||||
decrementValue: ( fieldName, amount = 1 ) =>
|
||||
setValuePriv @, fieldName, ( value ) ->
|
||||
value - amount
|
||||
|
||||
setValue: ( fieldName, newValue ) =>
|
||||
setValuePriv @, fieldName, ( value ) ->
|
||||
newValue
|
||||
|
||||
setMax: ( fieldName, amount ) =>
|
||||
setValuePriv @, fieldName, ( value ) ->
|
||||
math.max value, amount
|
||||
|
||||
setMin: ( fieldName, amount ) =>
|
||||
setValuePriv @, fieldName, ( value ) ->
|
||||
math.min value, amount
|
||||
|
||||
getValue: ( fieldName ) =>
|
||||
pushFieldPriv @, fieldName
|
||||
return fieldPriv[fieldNamePriv]
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register Statistics
|
||||
else
|
||||
return Statistics
|
|
@ -1,193 +0,0 @@
|
|||
local log, Transform
|
||||
version = '1.3.4'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'Tags'
|
||||
:version
|
||||
description: 'A mess for manipulating tags.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.Tags'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.Transform', version: '1.2.3' }
|
||||
}
|
||||
}
|
||||
log, Transform = version\requireModules!
|
||||
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
-- In the following conversion functions, self refers to the tag table.
|
||||
convertStringValue = ( value ) =>
|
||||
return value
|
||||
|
||||
convertNumberValue = ( value ) =>
|
||||
return tonumber value
|
||||
|
||||
convertHexValue = ( value ) =>
|
||||
return tonumber value, 16
|
||||
|
||||
convertColorValue = ( value ) =>
|
||||
output = { }
|
||||
for i = 1, 5, 2
|
||||
table.insert output, tonumber value\sub( i, i+1 ), 16
|
||||
output.r = output[3]
|
||||
output.b = output[1]
|
||||
output.g = output[2]
|
||||
return output
|
||||
|
||||
convertKaraoke = ( ... ) =>
|
||||
args = {...}
|
||||
@tag = args[1]
|
||||
return tonumber args[2]
|
||||
|
||||
-- This doesn't actually work with vector clips but i dont care.
|
||||
convertMultiValue = ( value ) =>
|
||||
output = { }
|
||||
value\gsub "[%.%d%-]+", ( coord ) ->
|
||||
table.insert output, coord
|
||||
|
||||
for index = 1, #@fieldnames
|
||||
output[@fieldnames[index]] = output[index]
|
||||
|
||||
return output
|
||||
|
||||
convertTransformValue = ( value ) =>
|
||||
-- awkwardly solve circular require.
|
||||
Transform = Transform or require 'a-mo.Transform'
|
||||
return Transform\fromString value
|
||||
|
||||
interpolateNumber = ( before, after, progress ) =>
|
||||
return (1 - progress)*before + progress*after
|
||||
|
||||
interpolateMulti = ( before, after, progress ) =>
|
||||
result = { }
|
||||
for index = 1, #@fieldnames
|
||||
key = @fieldnames[index]
|
||||
result[index] = interpolateNumber @, before[index], after[index], progress
|
||||
result[key] = result[index]
|
||||
|
||||
return result
|
||||
|
||||
interpolatePosition = ( before, after, progress ) =>
|
||||
return {
|
||||
interpolateNumber @, before[1], after[1], progress
|
||||
interpolateNumber @, before[2], after[2], progress
|
||||
}
|
||||
|
||||
interpolateColor = ( before, after, progress ) =>
|
||||
return interpolateMulti { fieldnames: { 'b', 'g', 'r' } }, before, after, progress
|
||||
|
||||
formatString = ( value ) =>
|
||||
return @tag .. value
|
||||
|
||||
formatInt = ( value ) =>
|
||||
return ("%s%d")\format @tag, value
|
||||
|
||||
formatFloat = ( value ) =>
|
||||
return ("%s%g")\format @tag, value
|
||||
|
||||
formatAlpha = ( alpha ) =>
|
||||
return ("%s&H%02X&")\format @tag, alpha
|
||||
|
||||
formatColor = ( color ) =>
|
||||
return ("%s&H%02X%02X%02X&")\format @tag, color.b, color.g, color.r
|
||||
|
||||
formatKaraoke = ( time ) =>
|
||||
result = ("%s%d")\format @tag, time
|
||||
return result
|
||||
|
||||
formatTransform = ( transform ) =>
|
||||
return transform\toString!
|
||||
|
||||
formatMulti = ( value ) =>
|
||||
return ("%s(%s)")\format @tag, table.concat value, ','
|
||||
|
||||
allTags = {
|
||||
fontName: { pattern: "\\fn([^\\}]+)" , tag: "\\fn" , format: formatString, style: "fontname" , convert: convertStringValue }
|
||||
fontSize: { pattern: "\\fs(%d+)" , tag: "\\fs" , format: formatInt , style: "fontsize", transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
fontSp: { pattern: "\\fsp([%.%d%-]+)" , tag: "\\fsp" , format: formatFloat , style: "spacing" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
xscale: { pattern: "\\fscx([%d%.]+)" , tag: "\\fscx" , format: formatFloat , style: "scale_x" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
yscale: { pattern: "\\fscy([%d%.]+)" , tag: "\\fscy" , format: formatFloat , style: "scale_y" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
zrot: { pattern: "\\frz?([%-%d%.]+)" , tag: "\\frz" , format: formatFloat , style: "angle" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
xrot: { pattern: "\\frx([%-%d%.]+)" , tag: "\\frx" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
yrot: { pattern: "\\fry([%-%d%.]+)" , tag: "\\fry" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
border: { pattern: "\\bord([%d%.]+)" , tag: "\\bord" , format: formatFloat , style: "outline" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
xborder: { pattern: "\\xbord([%d%.]+)" , tag: "\\xbord", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
yborder: { pattern: "\\ybord([%d%.]+)" , tag: "\\ybord", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
shadow: { pattern: "\\shad([%-%d%.]+)" , tag: "\\shad" , format: formatFloat , style: "shadow" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
xshadow: { pattern: "\\xshad([%-%d%.]+)", tag: "\\xshad", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
yshadow: { pattern: "\\yshad([%-%d%.]+)", tag: "\\yshad", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
reset: { pattern: "\\r([^\\}]*)" , tag: "\\r" , format: formatString , convert: convertStringValue }
|
||||
alpha: { pattern: "\\alpha&H(%x%x)&" , tag: "\\alpha", format: formatAlpha , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha" }
|
||||
alpha1: { pattern: "\\1a&H(%x%x)&" , tag: "\\1a" , format: formatAlpha , style: "color1" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
|
||||
alpha2: { pattern: "\\2a&H(%x%x)&" , tag: "\\2a" , format: formatAlpha , style: "color2" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
|
||||
alpha3: { pattern: "\\3a&H(%x%x)&" , tag: "\\3a" , format: formatAlpha , style: "color3" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
|
||||
alpha4: { pattern: "\\4a&H(%x%x)&" , tag: "\\4a" , format: formatAlpha , style: "color4" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
|
||||
color1: { pattern: "\\1?c&H(%x+)&" , tag: "\\1c" , format: formatColor , style: "color1" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
|
||||
color2: { pattern: "\\2c&H(%x+)&" , tag: "\\2c" , format: formatColor , style: "color2" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
|
||||
color3: { pattern: "\\3c&H(%x+)&" , tag: "\\3c" , format: formatColor , style: "color3" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
|
||||
color4: { pattern: "\\4c&H(%x+)&" , tag: "\\4c" , format: formatColor , style: "color4" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
|
||||
be: { pattern: "\\be([%d%.]+)" , tag: "\\be" , format: formatInt , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
blur: { pattern: "\\blur([%d%.]+)" , tag: "\\blur" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
xshear: { pattern: "\\fax([%-%d%.]+)" , tag: "\\fax" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
yshear: { pattern: "\\fay([%-%d%.]+)" , tag: "\\fay" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
|
||||
align: { pattern: "\\an([1-9])" , tag: "\\an" , format: formatInt , style: "align" , convert: convertNumberValue , global: true }
|
||||
-- bold, italic, underline and strikeout are actually stored in the style table as boolean values.
|
||||
bold: { pattern: "\\b(%d+)" , tag: "\\b" , format: formatInt , style: "bold" , convert: convertNumberValue }
|
||||
underline:{ pattern: "\\u([01])" , tag: "\\u" , format: formatInt , style: "underline" , convert: convertNumberValue }
|
||||
italic: { pattern: "\\i([01])" , tag: "\\i" , format: formatInt , style: "italic" , convert: convertNumberValue }
|
||||
strike: { pattern: "\\s([01])" , tag: "\\s" , format: formatInt , style: "strikeout" , convert: convertNumberValue }
|
||||
drawing: { pattern: "\\p(%d+)" , tag: "\\p" , format: formatInt , convert: convertNumberValue }
|
||||
transform:{ pattern: "\\t(%(.-%))" , tag: "\\t" , format: formatTransform , convert: convertTransformValue }
|
||||
karaoke: { pattern: "(\\[kK][fo]?)(%d+)" , format: formatInt , convert: convertKaraoke }
|
||||
-- Problematic tags:
|
||||
pos: { fieldnames: { "x", "y" } , output: "multi", pattern: "\\pos%(([%.%d%-]+,[%.%d%-]+)%)", tag: "\\pos" , format: formatMulti, convert: convertMultiValue, global: true }
|
||||
org: { fieldnames: { "x", "y" } , output: "multi", pattern: "\\org%(([%.%d%-]+,[%.%d%-]+)%)", tag: "\\org" , format: formatMulti, convert: convertMultiValue, global: true }
|
||||
fad: { fieldnames: { "in", "out" } , output: "multi", pattern: "\\fade?%((%d+,%d+)%)" , tag: "\\fad" , format: formatMulti, convert: convertMultiValue, global: true }
|
||||
vectClip: { fieldnames: { "scale", "shape" }, output: "multi", pattern: "\\clip%((%d+,)?([^,]-)%)" , tag: "\\clip" , format: formatMulti, convert: convertMultiValue, global: true }
|
||||
vectiClip:{ fieldnames: { "scale", "shape" }, output: "multi", pattern: "\\iclip%((%d+,)?([^,]-)%)" , tag: "\\iclip", format: formatMulti, convert: convertMultiValue, global: true }
|
||||
rectClip: { fieldnames: { "xLeft", "yTop", "xRight", "yBottom" } , output: "multi", pattern: "\\clip%(([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+)%)?" , transformable: true, tag: "\\clip" , format: formatMulti, convert: convertMultiValue, interpolate: interpolateMulti, global: true }
|
||||
rectiClip:{ fieldnames: { "xLeft", "yTop", "xRight", "yBottom" } , output: "multi", pattern: "\\iclip%(([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+)%)?", transformable: true, tag: "\\iclip", format: formatMulti, convert: convertMultiValue, interpolate: interpolateMulti, global: true }
|
||||
move: { fieldnames: { "x1", "y1", "x2", "y2", "start", "end" } , output: "multi", pattern: "\\move%(([%.%d%-]+,[%.%d%-]+,[%.%d%-]+,[%.%d%-]+,[%d%-]+,[%d%-]+)%)" , tag: "\\move" , format: formatMulti, convert: convertMultiValue, interpolate: interpolatePosition, global: true }
|
||||
fade: { fieldnames: { "a1", "a2", "a3", "t1", "t2", "t3", "t4" }, output: "multi", pattern: "\\fade%((%d+,%d+,%d+,[%d%-]+,[%d%-]+,[%d%-]+,[%d%-]+)%)" , tag: "\\fade" , format: formatMulti, convert: convertMultiValue, global: true }
|
||||
}
|
||||
|
||||
repeatTags = { }
|
||||
oneTimeTags = { }
|
||||
styleTags = { }
|
||||
transformTags = { }
|
||||
|
||||
for k, v in pairs allTags
|
||||
v.name = k
|
||||
unless v.global
|
||||
table.insert repeatTags, v
|
||||
else
|
||||
table.insert oneTimeTags, v
|
||||
|
||||
if v.style
|
||||
table.insert styleTags, v
|
||||
|
||||
if v.transformable
|
||||
table.insert transformTags, v
|
||||
|
||||
tags = {
|
||||
:version
|
||||
|
||||
:repeatTags
|
||||
:oneTimeTags
|
||||
:styleTags
|
||||
:transformTags
|
||||
|
||||
:allTags
|
||||
}
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register tags
|
||||
else
|
||||
return tags
|
|
@ -1,143 +0,0 @@
|
|||
local log, Math, tags
|
||||
version = '1.2.4'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'Transform'
|
||||
:version
|
||||
description: 'A class for managing the transform tag.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.Transform'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
{ 'a-mo.Math', version: '1.0.0' }
|
||||
{ 'a-mo.Tags', version: '1.3.4' }
|
||||
}
|
||||
}
|
||||
log, Math, tags = version\requireModules!
|
||||
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
Math = require 'a-mo.Math'
|
||||
|
||||
class Transform
|
||||
@version: version
|
||||
|
||||
tags = tags or require 'a-mo.Tags'
|
||||
|
||||
-- An alternate constructor.
|
||||
@fromString: ( transformString, lineDuration, tagIndex, parentLine ) =>
|
||||
transStart, transEnd, transExp, transEffect = transformString\match "%(([%-%d]*),?([%-%d]*),?([%d%.]*),?(.+)%)"
|
||||
-- Catch the case of \t(2.345,\1c&H0000FF&), where the 2 gets
|
||||
-- matched to transStart and the .345 gets matched to transEnd.
|
||||
if tonumber( transStart ) and not tonumber( transEnd )
|
||||
transExp = transStart .. transExp
|
||||
transStart = ""
|
||||
|
||||
transExp = tonumber( transExp ) or 1
|
||||
transStart = tonumber( transStart ) or 0
|
||||
|
||||
transEnd = tonumber( transEnd ) or 0
|
||||
if transEnd == 0
|
||||
transEnd = lineDuration
|
||||
|
||||
object = @ transStart, transEnd, transExp, transEffect, tagIndex, parentLine
|
||||
object.rawString = transformString
|
||||
return object
|
||||
|
||||
new: ( @startTime, @endTime, @accel, @effect, @index, @parentLine ) =>
|
||||
@gatherTagsInEffect!
|
||||
|
||||
__tostring: => return @toString!
|
||||
toString: ( line = @parentLine ) =>
|
||||
if @effect == ""
|
||||
return ""
|
||||
elseif @endTime <= 0
|
||||
return @effect
|
||||
elseif @startTime > line.duration or @endTime < @startTime
|
||||
return ""
|
||||
elseif @accel == 1
|
||||
return ("\\t(%s,%s,%s)")\format @startTime, @endTime, @effect
|
||||
else
|
||||
return ("\\t(%s,%s,%s,%s)")\format @startTime, @endTime, @accel, @effect
|
||||
|
||||
gatherTagsInEffect: =>
|
||||
if @effectTags
|
||||
return
|
||||
@effectTags = { }
|
||||
for tag in *tags.transformTags
|
||||
@effect\gsub tag.pattern, ( value ) ->
|
||||
log.debug "Found tag: %s -> %s", tag.name, value
|
||||
unless @effectTags[tag]
|
||||
@effectTags[tag] = { }
|
||||
endValue = tag\convert value
|
||||
table.insert @effectTags[tag], endValue
|
||||
@effectTags[tag].last = endValue
|
||||
|
||||
collectPriorState: ( line, text, placeholder ) =>
|
||||
-- Fill out all of the relevant tag defaults. This works great for
|
||||
-- everything except \clip, which defaults to 0, 0, width, height
|
||||
@priorValues = { }
|
||||
for tag, _ in pairs @effectTags
|
||||
if tag.style
|
||||
@priorValues[tag] = line.properties[tag]
|
||||
else
|
||||
@priorValues[tag] = 0
|
||||
|
||||
if @effectTags[tags.allTags.rectClip]
|
||||
@priorValues[tags.allTags.rectClip] = { 0, 0, line.parentCollection.meta.PlayResX, line.parentCollection.meta.PlayResY }
|
||||
if @effectTags[tags.allTags.rectiClip]
|
||||
@priorValues[tags.allTags.rectiClip] = { 0, 0, line.parentCollection.meta.PlayResX, line.parentCollection.meta.PlayResY }
|
||||
|
||||
i = 1
|
||||
text\gsub "({.-})", ( tagBlock ) ->
|
||||
for tag, _ in pairs @effectTags
|
||||
if tag.affectedBy
|
||||
newTagBlock = tagBlock\gsub ".-"..tag.pattern, ( value ) ->
|
||||
@priorValues[tag] = tag\convert value
|
||||
return ""
|
||||
for tagName in *tag.affectedBy
|
||||
newTag = tags.allTags[tagName]
|
||||
newTagBlock = newTagBlock\gsub ".-"..newTag.pattern, ( value ) ->
|
||||
@priorValues[tag] = newTag\convert value
|
||||
return ""
|
||||
else
|
||||
tagBlock\gsub tag.pattern, ( value ) ->
|
||||
@priorValues[tag] = tag\convert value
|
||||
|
||||
i += 1
|
||||
return nil,
|
||||
@index
|
||||
|
||||
interpolate: ( line, text, placeholder, time ) =>
|
||||
@collectPriorState line, text, placeholder
|
||||
|
||||
linearProgress = (time - @startTime)/(@endTime - @startTime)
|
||||
progress = math.pow linearProgress, @accel
|
||||
|
||||
text = text\gsub placeholder, ->
|
||||
resultString = {}
|
||||
for tag, endValues in pairs @effectTags
|
||||
if linearProgress <= 0
|
||||
table.insert resultString, tag\format @priorValues[tag]
|
||||
elseif linearProgress >= 1
|
||||
table.insert resultString, tag\format endValues.last
|
||||
else
|
||||
value = @priorValues[tag]
|
||||
for endValue in *endValues
|
||||
value = tag\interpolate value, endValue, progress
|
||||
|
||||
table.insert resultString, tag\format value
|
||||
|
||||
return table.concat resultString
|
||||
|
||||
return text
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register Transform
|
||||
else
|
||||
return Transform
|
|
@ -1,185 +0,0 @@
|
|||
local log
|
||||
version = '1.0.5'
|
||||
|
||||
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
|
||||
|
||||
if haveDepCtrl
|
||||
version = DependencyControl {
|
||||
name: 'TrimHandler'
|
||||
:version
|
||||
description: 'A class for encoding video clips.'
|
||||
author: 'torque'
|
||||
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
|
||||
moduleName: 'a-mo.TrimHandler'
|
||||
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
|
||||
{
|
||||
{ 'a-mo.Log', version: '1.0.0' }
|
||||
}
|
||||
}
|
||||
log = version\requireModules!
|
||||
|
||||
else
|
||||
log = require 'a-mo.Log'
|
||||
|
||||
windows = jit.os == "Windows"
|
||||
|
||||
class TrimHandler
|
||||
@version: version
|
||||
|
||||
@windows: windows
|
||||
|
||||
existingPresets: {
|
||||
"x264", "ffmpeg"
|
||||
}
|
||||
|
||||
-- Set up encoder presets.
|
||||
defaults: {
|
||||
x264: '"#{encbin}" --crf 16 --tune fastdecode -i 250 --fps 23.976 --sar 1:1 --index "#{prefix}/#{index}.index" --seek #{startf} --frames #{lenf} -o "#{prefix}/#{output}[#{startf}-#{endf}].mp4" "#{inpath}/#{input}"'
|
||||
ffmpeg: '"#{encbin}" -ss #{startt} -an -sn -i "#{inpath}/#{input}" -q:v 1 -vsync passthrough -frames:v #{lenf} "#{prefix}/#{output}[#{startf}-#{endf}]-%05d.jpg"'
|
||||
-- avs2pipe: 'echo FFVideoSource("#{inpath}#{input}",cachefile="#{prefix}#{index}.index").trim(#{startf},#{endf}).ConvertToRGB.ImageWriter("#{prefix}/#{output}-[#{startf}-#{endf}]\\",type="png").ConvertToYV12 > "#{temp}/a-mo.encode.avs"\nmkdir "#{prefix}#{output}-[#{startf}-#{endf}]"\n"#{encbin}" video "#{temp}/a-mo.encode.avs"\ndel "#{temp}/a-mo.encode.avs"'
|
||||
-- vapoursynth:
|
||||
}
|
||||
|
||||
-- Example trimConfig:
|
||||
-- trimConfig = {
|
||||
-- -- The prefix is the directory the output will be written to. It
|
||||
-- -- is passed through aegisub.decode_path.
|
||||
-- prefix: "?video"
|
||||
|
||||
-- -- The name of the built in encoding preset to use. Overridden by
|
||||
-- -- command if that is neither nil nor an empty string.
|
||||
-- preset: "x264"
|
||||
|
||||
-- -- The path of the executable used to actually do the encoding.
|
||||
-- -- Full path is recommended as the shell environment may be
|
||||
-- -- different than expected on non-windows systems.
|
||||
-- encBin: "C:\x264.exe"
|
||||
|
||||
-- -- A custom encoding command that can be used to override the
|
||||
-- -- built-in defaults. Usable token documentation to come.
|
||||
-- -- Overrides preset if that is set.
|
||||
-- command: nil
|
||||
|
||||
-- -- Script should attempt to create prefix directory.
|
||||
-- makePfix: nil
|
||||
|
||||
-- -- Script should attempt to log output of the encoding command.
|
||||
-- writeLog: true
|
||||
-- }
|
||||
new: ( trimConfig ) =>
|
||||
@tokens = { }
|
||||
if trimConfig.command != nil
|
||||
trimConfig.command = trimConfig.command\gsub "[\t \r\n]*$", ""
|
||||
if trimConfig.command != ""
|
||||
@command = trimConfig.command
|
||||
else
|
||||
@command = @defaults[trimConfig.preset]
|
||||
else
|
||||
@command = @defaults[trimConfig.preset]
|
||||
|
||||
@makePrefix = trimConfig.makePfix
|
||||
@writeLog = trimConfig.writeLog
|
||||
|
||||
with @tokens
|
||||
.temp = aegisub.decode_path "?temp"
|
||||
-- For some reason, aegisub appends / to the end of ?temp but not
|
||||
-- other tokens.
|
||||
finalTemp = .temp\sub -1, -1
|
||||
if finalTemp == '\\' or finalTemp == '/'
|
||||
.temp = .temp\sub 1, -2
|
||||
.encbin = trimConfig.encBin
|
||||
.prefix = aegisub.decode_path trimConfig.prefix
|
||||
.inpath = aegisub.decode_path "?video"
|
||||
.log = aegisub.decode_path "#{.temp}/a-mo.encode.log"
|
||||
|
||||
getVideoName @
|
||||
|
||||
getVideoName = =>
|
||||
with @tokens
|
||||
video = aegisub.project_properties!.video_file
|
||||
if video\len! == 0
|
||||
log.windowError "Aegisub thinks your video is 0 frames long.\nTheoretically it should be impossible to get this error."
|
||||
if video\match "^?dummy"
|
||||
log.windowError "I can't encode that dummy video for you."
|
||||
.input = video\gsub( "^[A-Z]:\\", "", 1 )\gsub ".+[^\\/]-[\\/]", "", 1
|
||||
.index = .input\match "(.+)%.[^%.]+$"
|
||||
.output = .index
|
||||
|
||||
calculateTrimLength: ( lineCollection ) =>
|
||||
with @tokens
|
||||
.startt = lineCollection.startTime / 1000
|
||||
.endt = lineCollection.endTime / 1000
|
||||
.lent = .endt - .startt
|
||||
.startf = lineCollection.startFrame
|
||||
.endf = lineCollection.endFrame - 1
|
||||
.lenf = lineCollection.totalFrames
|
||||
|
||||
performTrim: =>
|
||||
with platform = ({
|
||||
[true]: {
|
||||
pre: @tokens.temp
|
||||
ext: ".ps1"
|
||||
-- This needs to be run from cmd or it will not work.
|
||||
exec: 'powershell -c iex "$(gc "%s" -en UTF8)"'
|
||||
preCom: (@makePrefix and "mkdir -Force \"#{@tokens.prefix}\"; & " or "& ")
|
||||
postCom: (@writeLog and " 2>&1 | Out-File #{@tokens.log} -en UTF8; if($LASTEXITCODE -ne 0) {echo \"If there is no log before this, your encoder is not a working executable or your encoding command is invalid.\" | ac -en utf8 #{@tokens.log}; exit 1}" or "") .. "; exit 0"
|
||||
execFunc: ( encodeScript ) ->
|
||||
-- clear out old logfile because it doesn't get overwritten
|
||||
-- when certain errors occur.
|
||||
if @writeLog
|
||||
logFile = io.open @tokens.log, 'wb'
|
||||
logFile\close! if logFile
|
||||
success = os.execute encodeScript
|
||||
if @writeLog and not success
|
||||
logFile = io.open @tokens.log, 'r'
|
||||
unless logFile
|
||||
log.windowError "Could not read log file #{@tokens.log}.\nSomething has gone horribly wrong."
|
||||
encodeLog = logFile\read '*a'
|
||||
logFile\close!
|
||||
log.warn "\nEncoding error:"
|
||||
log.warn encodeLog
|
||||
log.windowError "Encoding failed. Log has been printed to progress window."
|
||||
elseif not success
|
||||
log.windowError "Encoding seems to have failed but you didn't write a log file."
|
||||
}
|
||||
[false]: {
|
||||
pre: @tokens.temp
|
||||
ext: ".sh"
|
||||
exec: 'sh "%s"'
|
||||
preCom: @makePrefix and "mkdir -p \"#{@tokens.prefix}\"\n" or ""
|
||||
postCom: " 2>&1; if [[ $? -ne 0 ]]; then echo \"If there is no log before this, your encoder is not a working executable or your encoding command is invalid.\"; false; fi"
|
||||
execFunc: ( encodeScript ) ->
|
||||
logFile = io.popen encodeScript, 'r'
|
||||
encodeLog = logFile\read '*a'
|
||||
-- When closing a file handle created with io.popen,
|
||||
-- file:close returns the same values returned by
|
||||
-- os.execute.
|
||||
success = logFile\close!
|
||||
unless success
|
||||
log.warn "\nEncoding error:"
|
||||
log.warn encodeLog
|
||||
log.windowError "Encoding failed. Log has been printed to progress window."
|
||||
}
|
||||
})[windows]
|
||||
-- check encoder binary exists
|
||||
encoder = io.open @tokens.encbin, "rb"
|
||||
unless encoder
|
||||
log.windowError "Encoding binary (#{@tokens.encbin}) does not appear to exist."
|
||||
encoder\close!
|
||||
|
||||
encodeScript = aegisub.decode_path "#{.pre}/a-mo.encode#{.ext}"
|
||||
encodeScriptFile = io.open encodeScript, "w+"
|
||||
unless encodeScriptFile
|
||||
log.windowError "Encoding script could not be written.\nSomething is wrong with your temp dir (#{.pre})."
|
||||
encodeString = .preCom .. @command\gsub( "#{(.-)}", ( token ) -> @tokens[token] ) .. .postCom
|
||||
if windows
|
||||
encodeString = encodeString\gsub "`", "``"
|
||||
log.debug encodeString
|
||||
encodeScriptFile\write encodeString
|
||||
encodeScriptFile\close!
|
||||
.execFunc .exec\format encodeScript
|
||||
|
||||
if haveDepCtrl
|
||||
return version\register TrimHandler
|
||||
else
|
||||
return TrimHandler
|
File diff suppressed because it is too large
Load diff
|
@ -1,362 +0,0 @@
|
|||
[[
|
||||
README
|
||||
|
||||
This file is a library of commonly used functions across all my automation
|
||||
scripts. This way, if there are errors or updates for any of these functions,
|
||||
I'll only need to update one file.
|
||||
|
||||
The filename is a bit vain, perhaps, but I couldn't come up with anything else.
|
||||
|
||||
]]
|
||||
|
||||
DependencyControl = require("l0.DependencyControl")
|
||||
version = DependencyControl{
|
||||
name: "LibLyger",
|
||||
version: "2.0.3",
|
||||
description: "Library of commonly used functions across all of lyger's automation scripts.",
|
||||
author: "lyger",
|
||||
url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts",
|
||||
moduleName: "lyger.LibLyger",
|
||||
feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
"aegisub.util", "karaskel"
|
||||
}
|
||||
}
|
||||
util = version\requireModules!
|
||||
|
||||
class LibLyger
|
||||
msgs = {
|
||||
preproc_lines: {
|
||||
bad_type: "Error: argument #1 must be either a line object, an index into the subtitle object or a table of indexes; got a %s."
|
||||
}
|
||||
}
|
||||
new: (sub, sel, generate_furigana) =>
|
||||
@set_sub sub, sel, generate_furigana if sub
|
||||
|
||||
set_sub: (@sub, @sel = {}, generate_furigana = false) =>
|
||||
@script_info, @lines, @dialogue, @dlg_cnt = {}, {}, {}, 0
|
||||
for i, line in ipairs sub
|
||||
@lines[i] = line
|
||||
switch line.class
|
||||
when "info" then @script_info[line.key] = line.value
|
||||
when "dialogue"
|
||||
@dlg_cnt += 1
|
||||
@dialogue[@dlg_cnt], line.i = line, i
|
||||
|
||||
@meta, @styles = karaskel.collect_head @sub, generate_furigana
|
||||
@preproc_lines @sel
|
||||
|
||||
insert_line: (line, i = #@lines + 1) =>
|
||||
table.insert(@lines, i, line)
|
||||
@sub.insert(i, line)
|
||||
|
||||
preproc_lines: (lines) =>
|
||||
val_type = type lines
|
||||
-- indexes into the subtitles object
|
||||
if val_type == "number"
|
||||
lines, val_type = {@lines[lines]}, "table"
|
||||
assert val_type == "table", msgs.preproc_lines.bad_type\format val_type
|
||||
|
||||
-- line objects
|
||||
if lines.raw and lines.section and not lines.duration
|
||||
karaskel.preproc_line @sub, @meta, @styles, lines
|
||||
-- tables of line numbers/objects such as the selection
|
||||
else @preproc_lines line for line in *lines
|
||||
|
||||
-- returns a "Lua" portable version of the string
|
||||
exportstring: (s) -> string.format "%q", s
|
||||
|
||||
--Lookup table for the nature of each kind of parameter
|
||||
param_type: {
|
||||
alpha: "alpha"
|
||||
"1a": "alpha"
|
||||
"2a": "alpha"
|
||||
"3a": "alpha"
|
||||
"4a": "alpha"
|
||||
c: "color"
|
||||
"1c": "color"
|
||||
"2c": "color"
|
||||
"3c": "color"
|
||||
"4c": "color"
|
||||
fscx: "number"
|
||||
fscy: "number"
|
||||
frz: "angle"
|
||||
frx: "angle"
|
||||
fry: "angle"
|
||||
shad: "number"
|
||||
bord: "number"
|
||||
fsp: "number"
|
||||
fs: "number"
|
||||
fax: "number"
|
||||
fay: "number"
|
||||
blur: "number"
|
||||
be: "number"
|
||||
xbord: "number"
|
||||
ybord: "number"
|
||||
xshad: "number"
|
||||
yshad: "number"
|
||||
pos: "point"
|
||||
org: "point"
|
||||
clip: "clip"
|
||||
}
|
||||
|
||||
--Convert float to neatly formatted string
|
||||
float2str: (f) -> "%.3f"\format(f)\gsub("%.(%d-)0+$","%.%1")\gsub "%.$", ""
|
||||
|
||||
--Escapes string for use in gsub
|
||||
esc: (str) -> str\gsub "([%%%(%)%[%]%.%*%-%+%?%$%^])","%%%1"
|
||||
|
||||
[[
|
||||
Tags that can have any character after the tag declaration: \r, \fn
|
||||
Otherwise, the first character after the tag declaration must be:
|
||||
a number, decimal point, open parentheses, minus sign, or ampersand
|
||||
]]
|
||||
|
||||
-- Remove listed tags from the given text
|
||||
line_exclude: (text, exclude) ->
|
||||
remove_t = false
|
||||
new_text = text\gsub "\\([^\\{}]*)", (a) ->
|
||||
if a\match "^r"
|
||||
for val in *exclude
|
||||
return "" if val == "r"
|
||||
elseif a\match "^fn"
|
||||
for val in *exclude
|
||||
return "" if val == "fn"
|
||||
else
|
||||
tag = a\match "^[1-4]?%a+"
|
||||
for val in *exclude
|
||||
if val == tag
|
||||
--Hacky exception handling for \t statements
|
||||
if val == "t"
|
||||
remove_t = true
|
||||
return "\\#{a}"
|
||||
elseif a\match "%)$"
|
||||
return a\match("%b()") and "" or ")"
|
||||
else
|
||||
return ""
|
||||
return "\\"..a
|
||||
|
||||
if remove_t
|
||||
new_text = new_text\gsub "\\t%b()", ""
|
||||
|
||||
return new_text\gsub "{}", ""
|
||||
|
||||
-- Remove all tags except the given ones
|
||||
line_exclude_except: (text, exclude) ->
|
||||
remove_t = true
|
||||
new_text = text\gsub "\\([^\\{}]*)", (a) ->
|
||||
if a\match "^r"
|
||||
for val in *exclude
|
||||
return "\\#{a}" if val == "r"
|
||||
elseif a\match "^fn"
|
||||
for val in *exclude
|
||||
return "\\#{a}" if val == "fn"
|
||||
else
|
||||
tag = a\match "^[1-4]?%a+"
|
||||
for val in *exclude
|
||||
if val == tag
|
||||
remove_t = false if val == "t"
|
||||
return "\\#{a}"
|
||||
|
||||
if a\match "^t"
|
||||
return "\\#{a}"
|
||||
elseif a\match "%)$"
|
||||
return a\match("%b()") and "" or ")"
|
||||
else return ""
|
||||
|
||||
if remove_t
|
||||
new_text = new_text\gsub "\\t%b()", ""
|
||||
|
||||
return new_text
|
||||
|
||||
-- Returns the position of a line
|
||||
get_default_pos: (line, align_x, align_y) =>
|
||||
@preproc_lines line
|
||||
x = {
|
||||
@script_info.PlayResX - line.eff_margin_r,
|
||||
line.eff_margin_l,
|
||||
line.eff_margin_l + (@script_info.PlayResX - line.eff_margin_l - line.eff_margin_r) / 2
|
||||
}
|
||||
y = {
|
||||
@script_info.PlayResY - line.eff_margin_b,
|
||||
@script_info.PlayResY / 2
|
||||
line.eff_margin_t
|
||||
}
|
||||
return x[align_x], y[align_y]
|
||||
|
||||
get_pos: (line) =>
|
||||
posx, posy = line.text\match "\\pos%(([%d%.%-]*),([%d%.%-]*)%)"
|
||||
unless posx
|
||||
posx, posy = line.text\match "\\move%(([%d%.%-]*),([%d%.%-]*),"
|
||||
return tonumber(posx), tonumber(posy) if posx
|
||||
|
||||
-- \an alignment
|
||||
if align = tonumber line.text\match "\\an([%d%.%-]+)"
|
||||
return @get_default_pos line, align%3 + 1, math.ceil align/3
|
||||
-- \a alignment
|
||||
elseif align = tonumber line.text\match "\\a([%d%.%-]+)"
|
||||
return @get_default_pos line, align%4,
|
||||
align > 8 and 2 or align> 4 and 3 or 1
|
||||
-- no alignment tags (take karaskel values)
|
||||
else return line.x, line.y
|
||||
|
||||
-- Returns the origin of a line
|
||||
get_org: (line) =>
|
||||
orgx, orgy = line.text\match "\\org%(([%d%.%-]*),([%d%.%-]*)%)"
|
||||
if orgx
|
||||
return orgx, orgy
|
||||
else return @get_pos line
|
||||
|
||||
-- Returns a table of default values
|
||||
style_lookup: (line) =>
|
||||
@preproc_lines line
|
||||
return {
|
||||
alpha: "&H00&"
|
||||
"1a": util.alpha_from_style line.styleref.color1
|
||||
"2a": util.alpha_from_style line.styleref.color2
|
||||
"3a": util.alpha_from_style line.styleref.color3
|
||||
"4a": util.alpha_from_style line.styleref.color4
|
||||
c: util.color_from_style line.styleref.color1
|
||||
"1c": util.color_from_style line.styleref.color1
|
||||
"2c": util.color_from_style line.styleref.color2
|
||||
"3c": util.color_from_style line.styleref.color3
|
||||
"4c": util.color_from_style line.styleref.color4
|
||||
fscx: line.styleref.scale_x
|
||||
fscy: line.styleref.scale_y
|
||||
frz: line.styleref.angle
|
||||
frx: 0
|
||||
fry: 0
|
||||
shad: line.styleref.shadow
|
||||
bord: line.styleref.outline
|
||||
fsp: line.styleref.spacing
|
||||
fs: line.styleref.fontsize
|
||||
fax: 0
|
||||
fay: 0
|
||||
xbord: line.styleref.outline
|
||||
ybord: line.styleref.outline
|
||||
xshad: line.styleref.shadow
|
||||
yshad: line.styleref.shadow
|
||||
blur: 0
|
||||
be: 0
|
||||
}
|
||||
|
||||
-- Modify the line tables so they are split at the same locations
|
||||
match_splits: (line_table1, line_table2) ->
|
||||
for i=1, #line_table1
|
||||
text1 = line_table1[i].text
|
||||
text2 = line_table2[i].text
|
||||
|
||||
insert = (target, text, i) ->
|
||||
for j = #target, i+1, -1
|
||||
target[j+1] = target[j]
|
||||
|
||||
target[i+1] = tag: "{}", text: target[i].text\match "#{LibLyger.esc(text)}(.*)"
|
||||
target[i] = tag: target[i].tag, :text
|
||||
|
||||
if #text1 > #text2
|
||||
-- If the table1 item has longer text, break it in two based on the text of table2
|
||||
insert line_table1, text2, i
|
||||
elseif #text2 > #text1
|
||||
-- If the table2 item has longer text, break it in two based on the text of table1
|
||||
insert line_table2, text1, i
|
||||
|
||||
return line_table1, line_table2
|
||||
|
||||
-- Remove listed tags from any \t functions in the text
|
||||
time_exclude: (text, exclude) ->
|
||||
text = text\gsub "(\\t%b())", (a) ->
|
||||
b = a
|
||||
for tag in *exclude
|
||||
if a\match "\\#{tag}"
|
||||
b = b\gsub(tag == "clip" and "\\#{tag}%b()" or "\\#{tag}[^\\%)]*", "")
|
||||
return b
|
||||
|
||||
-- get rid of empty blocks
|
||||
return text\gsub "\\t%([%-%.%d,]*%)", ""
|
||||
|
||||
-- Returns a state table, restricted by the tags given in "tag_table"
|
||||
-- WILL NOT WORK FOR \fn AND \r
|
||||
make_state_table: (line_table, tag_table) ->
|
||||
this_state_table = {}
|
||||
for i, val in ipairs line_table
|
||||
temp_line_table = {}
|
||||
pstate = LibLyger.line_exclude_except val.tag, tag_table
|
||||
for j, ctag in ipairs tag_table
|
||||
-- param MUST start in a non-alpha character, because ctag will never be \r or \fn
|
||||
-- If it is, you fucked up
|
||||
param = pstate\match "\\#{ctag}(%A[^\\{}]*)"
|
||||
temp_line_table[ctag] = param if param
|
||||
|
||||
this_state_table[i] = temp_line_table
|
||||
return this_state_table
|
||||
interpolate: (this_table, start_state_table, end_state_table, factor, preset) ->
|
||||
this_current_state = {}
|
||||
|
||||
rebuilt_text = for k, val in ipairs this_table
|
||||
temp_tag = val.tag
|
||||
-- Cycle through all the tag blocks and interpolate
|
||||
for ctag, param in pairs start_state_table[k]
|
||||
temp_tag = "{}" if #temp_tag == 0
|
||||
temp_tag = temp_tag\gsub "}", ->
|
||||
tval_start, tval_end = start_state_table[k][ctag], end_state_table[k][ctag]
|
||||
tag_type = LibLyger.param_type[ctag]
|
||||
ivalue = switch tag_type
|
||||
when "alpha"
|
||||
util.interpolate_alpha factor, tval_start, tval_end
|
||||
when "color"
|
||||
util.interpolate_color factor, tval_start, tval_end
|
||||
when "number", "angle"
|
||||
nstart, nend = tonumber(tval_start), tonumber(tval_end)
|
||||
if tag_type == "angle" and preset.c.flip_rot
|
||||
nstart %= 360
|
||||
nend %= 360
|
||||
ndelta = nend - nstart
|
||||
if 180 < math.abs ndelta
|
||||
nstart += ndelta * 360 / math.abs ndelta
|
||||
|
||||
nvalue = util.interpolate factor, nstart, nend
|
||||
nvalue += 360 if tag_type == "angle" and nvalue < 0
|
||||
|
||||
LibLyger.float2str nvalue
|
||||
when "point", "clip" then nil -- not touched by this function
|
||||
else ""
|
||||
|
||||
-- check for redundancy
|
||||
if this_current_state[ctag] == ivalue
|
||||
return "}"
|
||||
this_current_state[ctag] = ivalue
|
||||
return "\\#{ctag..ivalue}}"
|
||||
temp_tag .. val.text
|
||||
|
||||
return table.concat(rebuilt_text)\gsub "{}", ""
|
||||
|
||||
write_table: (my_table, file, indent) ->
|
||||
indent or= ""
|
||||
charS, charE = " ", "\n"
|
||||
|
||||
--Opening brace of the table
|
||||
file\write "#{indent}{#{charE}"
|
||||
|
||||
for key,val in pairs my_table
|
||||
file\write switch type key
|
||||
when "number" then indent..charS
|
||||
when "string" then table.concat {indent, charS, "[", LibLyger.exportstring(key), "]="}
|
||||
else "#{indent}#{charS}#{key}="
|
||||
|
||||
switch type val
|
||||
when "table"
|
||||
file\write charE
|
||||
LibLyger.write_table val, file, indent..charS
|
||||
file\write indent..charS
|
||||
when "string" then file\write LibLyger.exportstring val
|
||||
when "number" then file\write tostring val
|
||||
when "boolean" then file\write val and "true" or "false"
|
||||
|
||||
file\write ","..charE
|
||||
|
||||
-- Closing brace of the table
|
||||
file\write "#{indent}}#{charE}"
|
||||
|
||||
:version
|
||||
|
||||
return version\register LibLyger
|
|
@ -1,387 +0,0 @@
|
|||
DependencyControl = require "l0.DependencyControl"
|
||||
|
||||
version = DependencyControl {
|
||||
name: "ASSParser",
|
||||
version: "0.0.4",
|
||||
description: "Utility function for parsing ASS files",
|
||||
author: "Myaamori",
|
||||
url: "http://github.com/TypesettingTools/Myaamori-Aegisub-Scripts",
|
||||
moduleName: "myaa.ASSParser",
|
||||
feed: "https://raw.githubusercontent.com/TypesettingTools/Myaamori-Aegisub-Scripts/master/DependencyControl.json",
|
||||
{
|
||||
"aegisub.re", "aegisub.util",
|
||||
{"l0.Functional", version: "0.6.0", url: "https://github.com/TypesettingTools/Functional",
|
||||
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}
|
||||
}
|
||||
}
|
||||
|
||||
re, util, F = version\requireModules!
|
||||
|
||||
import lshift, rshift, band, bor from bit
|
||||
|
||||
parser = {}
|
||||
|
||||
STYLE_FORMAT_STRING = "Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, " ..
|
||||
"OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, " ..
|
||||
"Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, " ..
|
||||
"MarginV, Encoding"
|
||||
EVENT_FORMAT_STRING = "Layer, Start, End, Style, Name, MarginL, MarginR, " ..
|
||||
"MarginV, Effect, Text"
|
||||
DATA_FORMAT_STRING = "Id, Key, Value"
|
||||
|
||||
DIALOGUE_DEFAULTS =
|
||||
actor: "", class: "dialogue", comment: false, effect: "",
|
||||
start_time: 0, end_time: 0, layer: 0, margin_l: 0,
|
||||
margin_r: 0, margin_t: 0, section: "[Events]", style: "Default",
|
||||
text: "", extra: nil
|
||||
|
||||
STYLE_DEFAULTS =
|
||||
class: "style", section: "[V4+ Styles]", name: "Default",
|
||||
fontname: "Arial", fontsize: 45, color1: "&H00FFFFFF",
|
||||
color2: "&H000000FF", color3: "&H00000000", color4: "&H00000000",
|
||||
bold: false, italic: false, underline: false, strikeout: false,
|
||||
scale_x: 100, scale_y: 100, spacing: 0, angle: 0,
|
||||
borderstyle: 1, outline: 4.5, shadow: 4.5, align: 2,
|
||||
margin_l: 23, margin_r: 23, margin_t: 23, encoding: 1
|
||||
|
||||
create_line_from = (line, fields)->
|
||||
line = util.copy line
|
||||
if fields
|
||||
for key, value in pairs fields
|
||||
line[key] = value
|
||||
return line
|
||||
|
||||
parser.create_dialogue_line = (fields)->
|
||||
line = create_line_from DIALOGUE_DEFAULTS, fields
|
||||
line.extra = line.extra or {}
|
||||
line
|
||||
|
||||
parser.create_style_line = (fields)-> create_line_from STYLE_DEFAULTS, fields
|
||||
|
||||
parser.decode_extradata_value = (value)->
|
||||
enc, data = value\match "^([eu])(.*)$"
|
||||
|
||||
if enc == 'e'
|
||||
return parser.inline_string_decode data
|
||||
else
|
||||
return parser.uudecode data
|
||||
|
||||
parse_format_line = (format_string)-> [match for match in format_string\gmatch "([^, ]+)"]
|
||||
|
||||
parser.raw_to_line = (raw, extradata=nil, format=nil)->
|
||||
line_type, value = raw\match "^([^:]+):%s*(.*)$"
|
||||
if not value
|
||||
return nil
|
||||
|
||||
default_format = {Dialogue: EVENT_FORMAT_STRING,
|
||||
Comment: EVENT_FORMAT_STRING,
|
||||
Style: STYLE_FORMAT_STRING,
|
||||
Data: DATA_FORMAT_STRING}
|
||||
|
||||
if line_type == "Format"
|
||||
return {class: "format", format: parse_format_line value}
|
||||
elseif not default_format[line_type]
|
||||
return {class: "info", key: line_type, value: value}
|
||||
|
||||
format = format or parse_format_line default_format[line_type]
|
||||
elements = F.string.split value, ",", 1, true, #format - 1
|
||||
return nil if #elements != #format
|
||||
|
||||
fields = {format[i], elements[i] for i=1,#elements}
|
||||
|
||||
if line_type == "Dialogue" or line_type == "Comment"
|
||||
line = parser.create_dialogue_line
|
||||
actor: fields.Name, comment: line_type == "Comment"
|
||||
effect: fields.Effect, start_time: F.util.assTimecode2ms(fields.Start)
|
||||
end_time: F.util.assTimecode2ms(fields.End), layer: tonumber(fields.Layer)
|
||||
margin_l: tonumber(fields.MarginL), margin_r: tonumber(fields.MarginR)
|
||||
margin_t: tonumber(fields.MarginV), style: fields.Style
|
||||
text: fields.Text
|
||||
|
||||
-- handle extradata (e.g. '{=32=33}Line text')
|
||||
extramatch = re.match line.text, "^\\{((?:=\\d+)+)\\}(.*)$"
|
||||
if extramatch
|
||||
line.text = extramatch[3].str
|
||||
if extradata
|
||||
for id in extramatch[2].str\gmatch "=(%d+)"
|
||||
id = tonumber id
|
||||
if extradata[id]
|
||||
eline = extradata[id]
|
||||
line.extra[eline.key] = eline.value
|
||||
else
|
||||
aegisub.log 2,
|
||||
"WARNING: Found extradata ID, but no extradata mapping provided: " ..
|
||||
"#{raw}\n"
|
||||
|
||||
return line
|
||||
elseif line_type == "Style"
|
||||
boolean_map = {["-1"]: true, ["0"]: false}
|
||||
line = parser.create_style_line
|
||||
name: fields.Name, fontname: fields.Fontname
|
||||
fontsize: tonumber(fields.Fontsize), color1: fields.PrimaryColour
|
||||
color2: fields.SecondaryColour, color3: fields.OutlineColour
|
||||
color4: fields.BackColour, bold: boolean_map[fields.Bold]
|
||||
italic: boolean_map[fields.Italic], underline: boolean_map[fields.Underline]
|
||||
strikeout: boolean_map[fields.StrikeOut], scale_x: tonumber(fields.ScaleX)
|
||||
scale_y: tonumber(fields.ScaleY), spacing: tonumber(fields.Spacing)
|
||||
angle: tonumber(fields.Angle), borderstyle: tonumber(fields.BorderStyle)
|
||||
outline: tonumber(fields.Outline), shadow: tonumber(fields.Shadow)
|
||||
align: tonumber(fields.Alignment), margin_l: tonumber(fields.MarginL)
|
||||
margin_r: tonumber(fields.MarginR), margin_t: tonumber(fields.MarginV)
|
||||
encoding: tonumber(fields.Encoding)
|
||||
|
||||
return line
|
||||
elseif line_type == "Data"
|
||||
return {class: "data", id: tonumber(fields.Id),
|
||||
key: fields.Key, value: parser.decode_extradata_value fields.Value}
|
||||
|
||||
parser.line_to_raw = (line)->
|
||||
if line.class == "dialogue"
|
||||
prefix = if line.comment then "Comment" else "Dialogue"
|
||||
"#{prefix}: #{line.layer},#{F.util.ms2AssTimecode line.start_time}," ..
|
||||
"#{F.util.ms2AssTimecode line.end_time},#{line.style},#{line.actor}," ..
|
||||
"#{line.margin_l},#{line.margin_r},#{line.margin_t},#{line.effect},#{line.text}"
|
||||
elseif line.class == "style"
|
||||
map = {[true]: "-1", [false]: "0"}
|
||||
clr = (color)-> util.ass_style_color util.extract_color color
|
||||
"Style: #{line.name},#{line.fontname},#{line.fontsize},#{clr line.color1}," ..
|
||||
"#{clr line.color2},#{clr line.color3},#{clr line.color4},#{map[line.bold]}," ..
|
||||
"#{map[line.italic]},#{map[line.underline]},#{map[line.strikeout]}," ..
|
||||
"#{line.scale_x},#{line.scale_y},#{line.spacing},#{line.angle}," ..
|
||||
"#{line.borderstyle},#{line.outline},#{line.shadow},#{line.align}," ..
|
||||
"#{line.margin_l},#{line.margin_r},#{line.margin_t},#{line.encoding}"
|
||||
elseif line.class == "info"
|
||||
"#{line.key}: #{line.value}"
|
||||
|
||||
parser.inline_string_encode = (input)->
|
||||
output = {}
|
||||
for i=1,#input
|
||||
c = input\byte i
|
||||
if c <= 0x1F or c >= 0x80 or c == 0x23 or c == 0x2C or c == 0x3A or c == 0x7C
|
||||
table.insert output, string.format "#%02X", c
|
||||
else
|
||||
table.insert output, input\sub i,i
|
||||
return table.concat output
|
||||
|
||||
parser.inline_string_decode = (input)->
|
||||
output = {}
|
||||
i = 1
|
||||
while i <= #input
|
||||
if (input\sub i, i) != "#" or i + 1 > #input
|
||||
table.insert output, input\sub i, i
|
||||
else
|
||||
table.insert output, string.char tonumber (input\sub i+1, i+2), 16
|
||||
i += 2
|
||||
i += 1
|
||||
return table.concat output
|
||||
|
||||
parser.uuencode = (input)->
|
||||
ret = {}
|
||||
for pos=1,#input,3
|
||||
chunk = input\sub pos, pos+2
|
||||
src = [c\byte! for c in chunk\gmatch "."]
|
||||
while #src < 3
|
||||
src[#src+1] = 0
|
||||
|
||||
dst = {(rshift src[1], 2),
|
||||
(bor (lshift (band src[1], 0x3), 4), (rshift (band src[2], 0xF0), 4)),
|
||||
(bor (lshift (band src[2], 0xF), 2), (rshift (band src[3], 0xC0), 6)),
|
||||
(band src[3], 0x3F)}
|
||||
|
||||
for i=1,math.min(#input - pos + 2, 4)
|
||||
table.insert ret, dst[i] + 33
|
||||
|
||||
return table.concat [string.char i for i in *ret]
|
||||
|
||||
parser.uudecode = (input)->
|
||||
ret = {}
|
||||
pos = 1
|
||||
|
||||
while pos <= #input
|
||||
chunk = input\sub pos, pos+3
|
||||
src = [(string.byte c) - 33 for c in chunk\gmatch "."]
|
||||
if #src > 1
|
||||
table.insert ret, bor (lshift src[1], 2), (rshift src[2], 4)
|
||||
if #src > 2
|
||||
table.insert ret, bor (lshift (band src[2], 0xF), 4), (rshift src[3], 2)
|
||||
if #src > 3
|
||||
table.insert ret, bor (lshift (band src[3], 0x3), 6), src[4]
|
||||
|
||||
pos += #src
|
||||
|
||||
return table.concat [string.char i for i in *ret]
|
||||
|
||||
class ASSFile
|
||||
new: (file)=>
|
||||
@sections = {}
|
||||
@styles = {}
|
||||
@events = {}
|
||||
@script_info = {}
|
||||
@script_info_mapping = {}
|
||||
@aegisub_garbage = {}
|
||||
@aegisub_garbage_mapping = {}
|
||||
@extradata = {}
|
||||
@extradata_mapping = {}
|
||||
|
||||
@parse file
|
||||
|
||||
parse: (file)=>
|
||||
@read_sections file
|
||||
|
||||
@parse_extradata!
|
||||
@script_info = @parse_section "Script Info", {"info": true}
|
||||
@aegisub_garbage = @parse_section "Aegisub Project Garbage", {"info": true}
|
||||
@styles = @parse_section "V4+ Styles", {"style": true}
|
||||
@events = @parse_section "Events", {"dialogue": true}
|
||||
|
||||
for info in *@script_info
|
||||
@script_info_mapping[info.key] = info.value
|
||||
for garbage in *@aegisub_garbage
|
||||
@aegisub_garbage_mapping[garbage.key] = garbage.value
|
||||
|
||||
read_sections: (file)=>
|
||||
current_section = nil
|
||||
|
||||
-- read lines from file, sort into sections
|
||||
for row in file\lines!
|
||||
-- remove BOM if present, remove newlines, and trim leading spaces
|
||||
row = F.string.trimLeft (row\gsub "^\xEF\xBB\xBF", "")\gsub "[\r\n]*$", ""
|
||||
|
||||
if row == "" or row\match "^;"
|
||||
continue
|
||||
|
||||
section = row\match "^%[(.*)%]$"
|
||||
if section
|
||||
current_section = section
|
||||
@sections[current_section] = {}
|
||||
continue
|
||||
|
||||
table.insert @sections[current_section], row
|
||||
|
||||
parse_extradata: =>
|
||||
if @sections["Aegisub Extradata"]
|
||||
for row in *@sections["Aegisub Extradata"]
|
||||
line = parser.raw_to_line row
|
||||
if not line or line.class != "data"
|
||||
aegisub.log 2, "WARNING: Malformed data line: #{row}\n"
|
||||
continue
|
||||
|
||||
@extradata[line.id] = line
|
||||
@extradata_mapping[line.key] = @extradata_mapping[line.key] or {}
|
||||
@extradata_mapping[line.key][line.value] = line.id
|
||||
|
||||
parse_section: (section, expected_classes)=>
|
||||
lines = {}
|
||||
return lines if not @sections[section]
|
||||
|
||||
format = nil
|
||||
for row in *@sections[section]
|
||||
line = parser.raw_to_line row, @extradata, format
|
||||
|
||||
if not line
|
||||
aegisub.log 2, "WARNING: Malformed line: #{line}\n"
|
||||
elseif line.class == "format"
|
||||
format = line.format
|
||||
elseif expected_classes[line.class]
|
||||
table.insert lines, line
|
||||
else
|
||||
aegisub.log 2, "WARNING: Unexpected type #{line.class} in section #{section}\n"
|
||||
|
||||
return lines
|
||||
|
||||
parser.parse_file = (file)->
|
||||
return ASSFile file
|
||||
|
||||
parser.generate_styles_section = (styles, callback)->
|
||||
callback "[V4+ Styles]\n"
|
||||
callback "Format: #{STYLE_FORMAT_STRING}\n"
|
||||
for line in *styles
|
||||
callback parser.line_to_raw(line) .. "\n"
|
||||
|
||||
parser.generate_events_section = (events, extradata_mapping, callback)->
|
||||
callback "[Events]\n"
|
||||
callback "Format: #{EVENT_FORMAT_STRING}\n"
|
||||
|
||||
-- find the largest extradata ID seen so far
|
||||
last_eid = 0
|
||||
if extradata_mapping
|
||||
for key, v in pairs extradata_mapping
|
||||
for value, eid in pairs v
|
||||
last_eid = math.max last_eid, eid
|
||||
|
||||
extradata_to_write = {}
|
||||
|
||||
for line in *events
|
||||
-- handle extradata
|
||||
if line.extra and extradata_mapping
|
||||
lineindices = {}
|
||||
for key, value in pairs line.extra
|
||||
-- look for data in the original file's extradata
|
||||
cached_id = extradata_mapping[key] and extradata_mapping[key][value]
|
||||
if not cached_id
|
||||
-- if new extradata, generate new ID and cache it
|
||||
last_eid += 1
|
||||
cached_id = last_eid
|
||||
extradata_mapping[key] = extradata_mapping[key] or {}
|
||||
extradata_mapping[key][value] = cached_id
|
||||
|
||||
table.insert lineindices, cached_id
|
||||
extradata_to_write[cached_id] = {key, value}
|
||||
|
||||
-- add indices to line text (e.g. {=32=33}Text)
|
||||
if #lineindices > 0
|
||||
table.sort lineindices
|
||||
indexstring = table.concat ["=#{ind}" for ind in *lineindices]
|
||||
line.text = "{#{indexstring}}" .. line.text
|
||||
|
||||
callback parser.line_to_raw(line) .. "\n"
|
||||
|
||||
out_indices = [ind for ind, _ in pairs extradata_to_write]
|
||||
if #out_indices > 0
|
||||
callback "\n[Aegisub Extradata]\n"
|
||||
|
||||
table.sort out_indices
|
||||
for ind in *out_indices
|
||||
{key, value} = extradata_to_write[ind]
|
||||
encoded_data = parser.inline_string_encode value
|
||||
-- a mystical incantation passed down from subtitle_format_ass.cpp
|
||||
if 4*#value < 3*#encoded_data
|
||||
value = "u" .. parser.uuencode value
|
||||
else
|
||||
value = "e" .. encoded_data
|
||||
callback "Data: #{ind},#{key},#{value}\n"
|
||||
|
||||
parser.generate_script_info_section = (lines, callback, bom=true)->
|
||||
if bom
|
||||
callback "\xEF\xBB\xBF"
|
||||
callback "[Script Info]\n"
|
||||
for line in *lines
|
||||
callback parser.line_to_raw(line) .. "\n"
|
||||
|
||||
parser.generate_aegisub_garbage_section = (lines, callback)->
|
||||
callback "[Aegisub Project Garbage]\n"
|
||||
for line in *lines
|
||||
callback parser.line_to_raw(line) .. "\n"
|
||||
|
||||
parser.generate_file = (script_info, aegisub_garbage, styles, events, extradata_mapping, callback)->
|
||||
sec_added = false
|
||||
new_section = ->
|
||||
if sec_added
|
||||
callback "\n"
|
||||
sec_added = true
|
||||
|
||||
if script_info
|
||||
new_section!
|
||||
parser.generate_script_info_section script_info, callback
|
||||
if aegisub_garbage
|
||||
new_section!
|
||||
parser.generate_aegisub_garbage_section aegisub_garbage, callback
|
||||
if styles
|
||||
new_section!
|
||||
parser.generate_styles_section styles, callback
|
||||
if events
|
||||
new_section!
|
||||
parser.generate_events_section events, extradata_mapping, callback
|
||||
|
||||
parser.version = version
|
||||
return version\register parser
|
|
@ -1,34 +0,0 @@
|
|||
local DependencyControl = require("l0.DependencyControl")
|
||||
local version = DependencyControl{
|
||||
name = "Penlight",
|
||||
version = "1.6.0",
|
||||
description = "Python-inspired utility library.",
|
||||
author = "stevedonovan",
|
||||
url = "http://github.com/Myaamori/Penlight",
|
||||
moduleName = "myaa.pl",
|
||||
}
|
||||
|
||||
package.path = package.path .. ";" .. aegisub.decode_path("?user/automation/include/myaa/?.lua")
|
||||
|
||||
local modules_to_load = {
|
||||
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
|
||||
input=true,seq=true,lexer=true,stringx=true,
|
||||
config=true,pretty=true,data=true,func=true,text=true,
|
||||
operator=true,lapp=true,array2d=true,
|
||||
comprehension=true,xml=true,types=true,
|
||||
test = true, app = true, file = true, class = true,
|
||||
luabalanced = true, permute = true, template = true,
|
||||
url = true, compat = true, List = true, Map = true, Set = true,
|
||||
OrderedMap = true, MultiMap = true, Date = true,
|
||||
-- classes --
|
||||
}
|
||||
|
||||
local modules = {}
|
||||
|
||||
for k, v in pairs(modules_to_load) do
|
||||
modules[k] = require ("pl." .. k)
|
||||
end
|
||||
|
||||
modules.version = version
|
||||
|
||||
return modules
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,113 +0,0 @@
|
|||
--- A Map class.
|
||||
--
|
||||
-- > Map = require 'pl.Map'
|
||||
-- > m = Map{one=1,two=2}
|
||||
-- > m:update {three=3,four=4,two=20}
|
||||
-- > = m == M{one=1,two=20,three=3,four=4}
|
||||
-- true
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty`
|
||||
-- @classmod pl.Map
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local stdmt = utils.stdmt
|
||||
local deepcompare = tablex.deepcompare
|
||||
|
||||
local pretty_write = require 'pl.pretty' . write
|
||||
local Map = stdmt.Map
|
||||
local Set = stdmt.Set
|
||||
|
||||
local class = require 'pl.class'
|
||||
|
||||
-- the Map class ---------------------
|
||||
class(nil,nil,Map)
|
||||
|
||||
function Map:_init (t)
|
||||
local mt = getmetatable(t)
|
||||
if mt == Set or mt == Map then
|
||||
self:update(t)
|
||||
else
|
||||
return t -- otherwise assumed to be a map-like table
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function makelist(t)
|
||||
return setmetatable(t, require('pl.List'))
|
||||
end
|
||||
|
||||
--- list of keys.
|
||||
Map.keys = tablex.keys
|
||||
|
||||
--- list of values.
|
||||
Map.values = tablex.values
|
||||
|
||||
--- return an iterator over all key-value pairs.
|
||||
function Map:iter ()
|
||||
return pairs(self)
|
||||
end
|
||||
|
||||
--- return a List of all key-value pairs, sorted by the keys.
|
||||
function Map:items()
|
||||
local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self))
|
||||
ls:sort(function(t1,t2) return t1[1] < t2[1] end)
|
||||
return ls
|
||||
end
|
||||
|
||||
-- Will return the existing value, or if it doesn't exist it will set
|
||||
-- a default value and return it.
|
||||
function Map:setdefault(key, defaultval)
|
||||
return self[key] or self:set(key,defaultval) or defaultval
|
||||
end
|
||||
|
||||
--- size of map.
|
||||
-- note: this is a relatively expensive operation!
|
||||
-- @class function
|
||||
-- @name Map:len
|
||||
Map.len = tablex.size
|
||||
|
||||
--- put a value into the map.
|
||||
-- This will remove the key if the value is `nil`
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
function Map:set (key,val)
|
||||
self[key] = val
|
||||
end
|
||||
|
||||
--- get a value from the map.
|
||||
-- @param key the key
|
||||
-- @return the value, or nil if not found.
|
||||
function Map:get (key)
|
||||
return rawget(self,key)
|
||||
end
|
||||
|
||||
local index_by = tablex.index_by
|
||||
|
||||
--- get a list of values indexed by a list of keys.
|
||||
-- @param keys a list-like table of keys
|
||||
-- @return a new list
|
||||
function Map:getvalues (keys)
|
||||
return makelist(index_by(self,keys))
|
||||
end
|
||||
|
||||
--- update the map using key/value pairs from another table.
|
||||
-- @tab table
|
||||
-- @function Map:update
|
||||
Map.update = tablex.update
|
||||
|
||||
--- equality between maps.
|
||||
-- @within metamethods
|
||||
-- @tparam Map m another map.
|
||||
function Map:__eq (m)
|
||||
-- note we explicitly ask deepcompare _not_ to use __eq!
|
||||
return deepcompare(self,m,true)
|
||||
end
|
||||
|
||||
--- string representation of a map.
|
||||
-- @within metamethods
|
||||
function Map:__tostring ()
|
||||
return pretty_write(self,'')
|
||||
end
|
||||
|
||||
return Map
|
|
@ -1,54 +0,0 @@
|
|||
--- MultiMap, a Map which has multiple values per key.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.class`, `pl.List`, `pl.Map`
|
||||
-- @classmod pl.MultiMap
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local class = require 'pl.class'
|
||||
local List = require 'pl.List'
|
||||
local Map = require 'pl.Map'
|
||||
|
||||
-- MultiMap is a standard MT
|
||||
local MultiMap = utils.stdmt.MultiMap
|
||||
|
||||
class(Map,nil,MultiMap)
|
||||
MultiMap._name = 'MultiMap'
|
||||
|
||||
function MultiMap:_init (t)
|
||||
if not t then return end
|
||||
self:update(t)
|
||||
end
|
||||
|
||||
--- update a MultiMap using a table.
|
||||
-- @param t either a Multimap or a map-like table.
|
||||
-- @return the map
|
||||
function MultiMap:update (t)
|
||||
utils.assert_arg(1,t,'table')
|
||||
if Map:class_of(t) then
|
||||
for k,v in pairs(t) do
|
||||
self[k] = List()
|
||||
self[k]:append(v)
|
||||
end
|
||||
else
|
||||
for k,v in pairs(t) do
|
||||
self[k] = List(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- add a new value to a key. Setting a nil value removes the key.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
-- @return the map
|
||||
function MultiMap:set (key,val)
|
||||
if val == nil then
|
||||
self[key] = nil
|
||||
else
|
||||
if not self[key] then
|
||||
self[key] = List()
|
||||
end
|
||||
self[key]:append(val)
|
||||
end
|
||||
end
|
||||
|
||||
return MultiMap
|
|
@ -1,167 +0,0 @@
|
|||
--- OrderedMap, a map which preserves ordering.
|
||||
--
|
||||
-- Derived from `pl.Map`.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.List`, `pl.Map`
|
||||
-- @classmod pl.OrderedMap
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local List = require 'pl.List'
|
||||
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
|
||||
|
||||
local class = require 'pl.class'
|
||||
local Map = require 'pl.Map'
|
||||
|
||||
local OrderedMap = class(Map)
|
||||
OrderedMap._name = 'OrderedMap'
|
||||
|
||||
local rawset = rawset
|
||||
|
||||
--- construct an OrderedMap.
|
||||
-- Will throw an error if the argument is bad.
|
||||
-- @param t optional initialization table, same as for @{OrderedMap:update}
|
||||
function OrderedMap:_init (t)
|
||||
rawset(self,'_keys',List())
|
||||
if t then
|
||||
local map,err = self:update(t)
|
||||
if not map then error(err,2) end
|
||||
end
|
||||
end
|
||||
|
||||
local assert_arg,raise = utils.assert_arg,utils.raise
|
||||
|
||||
--- update an OrderedMap using a table.
|
||||
-- If the table is itself an OrderedMap, then its entries will be appended.
|
||||
-- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended.
|
||||
--
|
||||
-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
|
||||
-- @tab t a table.
|
||||
-- @return the map, or nil in case of error
|
||||
-- @return the error message
|
||||
function OrderedMap:update (t)
|
||||
assert_arg(1,t,'table')
|
||||
if OrderedMap:class_of(t) then
|
||||
for k,v in t:iter() do
|
||||
self:set(k,v)
|
||||
end
|
||||
elseif #t > 0 then -- an array must contain {key=val} tables
|
||||
if type(t[1]) == 'table' then
|
||||
for _,pair in ipairs(t) do
|
||||
local key,value = next(pair)
|
||||
if not key then return raise 'empty pair initialization table' end
|
||||
self:set(key,value)
|
||||
end
|
||||
else
|
||||
return raise 'cannot use an array to initialize an OrderedMap'
|
||||
end
|
||||
else
|
||||
for k,v in pairs(t) do
|
||||
self:set(k,v)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- set the key's value. This key will be appended at the end of the map.
|
||||
--
|
||||
-- If the value is nil, then the key is removed.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
-- @return the map
|
||||
function OrderedMap:set (key,val)
|
||||
if rawget(self, key) == nil and val ~= nil then -- new key
|
||||
self._keys:append(key) -- we keep in order
|
||||
rawset(self,key,val) -- don't want to provoke __newindex!
|
||||
else -- existing key-value pair
|
||||
if val == nil then
|
||||
self._keys:remove_value(key)
|
||||
rawset(self,key,nil)
|
||||
else
|
||||
self[key] = val
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
OrderedMap.__newindex = OrderedMap.set
|
||||
|
||||
--- insert a key/value pair before a given position.
|
||||
-- Note: if the map already contains the key, then this effectively
|
||||
-- moves the item to the new position by first removing at the old position.
|
||||
-- Has no effect if the key does not exist and val is nil
|
||||
-- @int pos a position starting at 1
|
||||
-- @param key the key
|
||||
-- @param val the value; if nil use the old value
|
||||
function OrderedMap:insert (pos,key,val)
|
||||
local oldval = self[key]
|
||||
val = val or oldval
|
||||
if oldval then
|
||||
self._keys:remove_value(key)
|
||||
end
|
||||
if val then
|
||||
self._keys:insert(pos,key)
|
||||
rawset(self,key,val)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- return the keys in order.
|
||||
-- (Not a copy!)
|
||||
-- @return List
|
||||
function OrderedMap:keys ()
|
||||
return self._keys
|
||||
end
|
||||
|
||||
--- return the values in order.
|
||||
-- this is relatively expensive.
|
||||
-- @return List
|
||||
function OrderedMap:values ()
|
||||
return List(index_by(self,self._keys))
|
||||
end
|
||||
|
||||
--- sort the keys.
|
||||
-- @func cmp a comparison function as for @{table.sort}
|
||||
-- @return the map
|
||||
function OrderedMap:sort (cmp)
|
||||
tsort(self._keys,cmp)
|
||||
return self
|
||||
end
|
||||
|
||||
--- iterate over key-value pairs in order.
|
||||
function OrderedMap:iter ()
|
||||
local i = 0
|
||||
local keys = self._keys
|
||||
local idx
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > #keys then return nil end
|
||||
idx = keys[i]
|
||||
return idx,self[idx]
|
||||
end
|
||||
end
|
||||
|
||||
--- iterate over an ordered map (5.2).
|
||||
-- @within metamethods
|
||||
-- @function OrderedMap:__pairs
|
||||
OrderedMap.__pairs = OrderedMap.iter
|
||||
|
||||
--- string representation of an ordered map.
|
||||
-- @within metamethods
|
||||
function OrderedMap:__tostring ()
|
||||
local res = {}
|
||||
for i,v in ipairs(self._keys) do
|
||||
local val = self[v]
|
||||
local vs = tostring(val)
|
||||
if type(val) ~= 'number' then
|
||||
vs = '"'..vs..'"'
|
||||
end
|
||||
res[i] = tostring(v)..'='..vs
|
||||
end
|
||||
return '{'..concat(res,',')..'}'
|
||||
end
|
||||
|
||||
return OrderedMap
|
||||
|
||||
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
--- A Set class.
|
||||
--
|
||||
-- > Set = require 'pl.Set'
|
||||
-- > = Set{'one','two'} == Set{'two','one'}
|
||||
-- true
|
||||
-- > fruit = Set{'apple','banana','orange'}
|
||||
-- > = fruit['banana']
|
||||
-- true
|
||||
-- > = fruit['hazelnut']
|
||||
-- nil
|
||||
-- > colours = Set{'red','orange','green','blue'}
|
||||
-- > = fruit,colours
|
||||
-- [apple,orange,banana] [blue,green,orange,red]
|
||||
-- > = fruit+colours
|
||||
-- [blue,green,apple,red,orange,banana]
|
||||
-- [orange]
|
||||
-- > more_fruits = fruit + 'apricot'
|
||||
-- > = fruit*colours
|
||||
-- > = more_fruits, fruit
|
||||
-- [banana,apricot,apple,orange] [banana,apple,orange]
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.Map`, (`pl.List` if __tostring is used)
|
||||
-- @module pl.Set
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local array_tostring, concat = utils.array_tostring, table.concat
|
||||
local merge,difference = tablex.merge,tablex.difference
|
||||
local Map = require 'pl.Map'
|
||||
local class = require 'pl.class'
|
||||
local stdmt = utils.stdmt
|
||||
local Set = stdmt.Set
|
||||
|
||||
-- the Set class --------------------
|
||||
class(Map,nil,Set)
|
||||
|
||||
-- note: Set has _no_ methods!
|
||||
Set.__index = nil
|
||||
|
||||
local function makeset (t)
|
||||
return setmetatable(t,Set)
|
||||
end
|
||||
|
||||
--- create a set. <br>
|
||||
-- @param t may be a Set, Map or list-like table.
|
||||
-- @class function
|
||||
-- @name Set
|
||||
function Set:_init (t)
|
||||
t = t or {}
|
||||
local mt = getmetatable(t)
|
||||
if mt == Set or mt == Map then
|
||||
for k in pairs(t) do self[k] = true end
|
||||
else
|
||||
for _,v in ipairs(t) do self[v] = true end
|
||||
end
|
||||
end
|
||||
|
||||
--- string representation of a set.
|
||||
-- @within metamethods
|
||||
function Set:__tostring ()
|
||||
return '['..concat(array_tostring(Set.values(self)),',')..']'
|
||||
end
|
||||
|
||||
--- get a list of the values in a set.
|
||||
-- @param self a Set
|
||||
-- @function Set.values
|
||||
-- @return a list
|
||||
Set.values = Map.keys
|
||||
|
||||
--- map a function over the values of a set.
|
||||
-- @param self a Set
|
||||
-- @param fn a function
|
||||
-- @param ... extra arguments to pass to the function.
|
||||
-- @return a new set
|
||||
function Set.map (self,fn,...)
|
||||
fn = utils.function_arg(1,fn)
|
||||
local res = {}
|
||||
for k in pairs(self) do
|
||||
res[fn(k,...)] = true
|
||||
end
|
||||
return makeset(res)
|
||||
end
|
||||
|
||||
--- union of two sets (also +).
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set.union (self,set)
|
||||
return merge(self,set,true)
|
||||
end
|
||||
|
||||
--- modifies '+' operator to allow addition of non-Set elements
|
||||
--- Preserves +/- semantics - does not modify first argument.
|
||||
local function setadd(self,other)
|
||||
local mt = getmetatable(other)
|
||||
if mt == Set or mt == Map then
|
||||
return Set.union(self,other)
|
||||
else
|
||||
local new = Set(self)
|
||||
new[other] = true
|
||||
return new
|
||||
end
|
||||
end
|
||||
|
||||
--- union of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__add
|
||||
|
||||
Set.__add = setadd
|
||||
|
||||
--- intersection of two sets (also *).
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
-- @usage
|
||||
-- > s = Set{10,20,30}
|
||||
-- > t = Set{20,30,40}
|
||||
-- > = t
|
||||
-- [20,30,40]
|
||||
-- > = Set.intersection(s,t)
|
||||
-- [30,20]
|
||||
-- > = s*t
|
||||
-- [30,20]
|
||||
|
||||
function Set.intersection (self,set)
|
||||
return merge(self,set,false)
|
||||
end
|
||||
|
||||
--- intersection of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__mul
|
||||
Set.__mul = Set.intersection
|
||||
|
||||
--- new set with elements in the set that are not in the other (also -).
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set.difference (self,set)
|
||||
return difference(self,set,false)
|
||||
end
|
||||
|
||||
--- modifies "-" operator to remove non-Set values from set.
|
||||
--- Preserves +/- semantics - does not modify first argument.
|
||||
local function setminus (self,other)
|
||||
local mt = getmetatable(other)
|
||||
if mt == Set or mt == Map then
|
||||
return Set.difference(self,other)
|
||||
else
|
||||
local new = Set(self)
|
||||
new[other] = nil
|
||||
return new
|
||||
end
|
||||
end
|
||||
|
||||
--- difference of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__sub
|
||||
Set.__sub = setminus
|
||||
|
||||
-- a new set with elements in _either_ the set _or_ other but not both (also ^).
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set.symmetric_difference (self,set)
|
||||
return difference(self,set,true)
|
||||
end
|
||||
|
||||
--- symmetric difference of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__pow
|
||||
Set.__pow = Set.symmetric_difference
|
||||
|
||||
--- is the first set a subset of the second (also <)?.
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return true or false
|
||||
function Set.issubset (self,set)
|
||||
for k in pairs(self) do
|
||||
if not set[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- first set subset of second?
|
||||
-- @within metamethods
|
||||
-- @function Set.__lt
|
||||
Set.__lt = Set.issubset
|
||||
|
||||
--- is the set empty?.
|
||||
-- @param self a Set
|
||||
-- @return true or false
|
||||
function Set.isempty (self)
|
||||
return next(self) == nil
|
||||
end
|
||||
|
||||
--- are the sets disjoint? (no elements in common).
|
||||
-- Uses naive definition, i.e. that intersection is empty
|
||||
-- @param s1 a Set
|
||||
-- @param s2 another set
|
||||
-- @return true or false
|
||||
function Set.isdisjoint (s1,s2)
|
||||
return Set.isempty(Set.intersection(s1,s2))
|
||||
end
|
||||
|
||||
--- size of this set (also # for 5.2).
|
||||
-- @param s a Set
|
||||
-- @return size
|
||||
-- @function Set.len
|
||||
Set.len = tablex.size
|
||||
|
||||
--- cardinality of set (5.2).
|
||||
-- @within metamethods
|
||||
-- @function Set.__len
|
||||
Set.__len = Set.len
|
||||
|
||||
--- equality between sets.
|
||||
-- @within metamethods
|
||||
function Set.__eq (s1,s2)
|
||||
return Set.issubset(s1,s2) and Set.issubset(s2,s1)
|
||||
end
|
||||
|
||||
return Set
|
|
@ -1,159 +0,0 @@
|
|||
--- Application support functions.
|
||||
-- See @{01-introduction.md.Application_Support|the Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.path`
|
||||
-- @module pl.app
|
||||
|
||||
local io,package,require = _G.io, _G.package, _G.require
|
||||
local utils = require 'pl.utils'
|
||||
local path = require 'pl.path'
|
||||
|
||||
local app = {}
|
||||
|
||||
local function check_script_name ()
|
||||
if _G.arg == nil then error('no command line args available\nWas this run from a main script?') end
|
||||
return _G.arg[0]
|
||||
end
|
||||
|
||||
--- add the current script's path to the Lua module path.
|
||||
-- Applies to both the source and the binary module paths. It makes it easy for
|
||||
-- the main file of a multi-file program to access its modules in the same directory.
|
||||
-- `base` allows these modules to be put in a specified subdirectory, to allow for
|
||||
-- cleaner deployment and resolve potential conflicts between a script name and its
|
||||
-- library directory.
|
||||
-- @string base optional base directory.
|
||||
-- @treturn string the current script's path with a trailing slash
|
||||
function app.require_here (base)
|
||||
local p = path.dirname(check_script_name())
|
||||
if not path.isabs(p) then
|
||||
p = path.join(path.currentdir(),p)
|
||||
end
|
||||
if p:sub(-1,-1) ~= path.sep then
|
||||
p = p..path.sep
|
||||
end
|
||||
if base then
|
||||
p = p..base..path.sep
|
||||
end
|
||||
local so_ext = path.is_windows and 'dll' or 'so'
|
||||
local lsep = package.path:find '^;' and '' or ';'
|
||||
local csep = package.cpath:find '^;' and '' or ';'
|
||||
package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
|
||||
package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
|
||||
return p
|
||||
end
|
||||
|
||||
--- return a suitable path for files private to this application.
|
||||
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
|
||||
-- SNAME is the name of the script without .lua extension.
|
||||
-- @string file a filename (w/out path)
|
||||
-- @return a full pathname, or nil
|
||||
-- @return 'cannot create' error
|
||||
function app.appfile (file)
|
||||
local sname = path.basename(check_script_name())
|
||||
local name = path.splitext(sname)
|
||||
local dir = path.join(path.expanduser('~'),'.'..name)
|
||||
if not path.isdir(dir) then
|
||||
local ret = path.mkdir(dir)
|
||||
if not ret then return utils.raise ('cannot create '..dir) end
|
||||
end
|
||||
return path.join(dir,file)
|
||||
end
|
||||
|
||||
--- return string indicating operating system.
|
||||
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
|
||||
function app.platform()
|
||||
if path.is_windows then
|
||||
return 'Windows'
|
||||
else
|
||||
local f = io.popen('uname')
|
||||
local res = f:read()
|
||||
if res == 'Darwin' then res = 'OSX' end
|
||||
f:close()
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
--- return the full command-line used to invoke this script.
|
||||
-- Any extra flags occupy slots, so that `lua -lpl` gives us `{[-2]='lua',[-1]='-lpl'}`
|
||||
-- @return command-line
|
||||
-- @return name of Lua program used
|
||||
function app.lua ()
|
||||
local args = _G.arg or error "not in a main program"
|
||||
local imin = 0
|
||||
for i in pairs(args) do
|
||||
if i < imin then imin = i end
|
||||
end
|
||||
local cmd, append = {}, table.insert
|
||||
for i = imin,-1 do
|
||||
append(cmd, utils.quote_arg(args[i]))
|
||||
end
|
||||
return table.concat(cmd,' '),args[imin]
|
||||
end
|
||||
|
||||
--- parse command-line arguments into flags and parameters.
|
||||
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
|
||||
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`);
|
||||
-- note that a number value can be given without a space.
|
||||
-- Multiple short args can be combined like so: ( `-abcd`).
|
||||
-- @tparam {string} args an array of strings (default is the global `arg`)
|
||||
-- @tab flags_with_values any flags that take values, e.g. `{out=true}`
|
||||
-- @return a table of flags (flag=value pairs)
|
||||
-- @return an array of parameters
|
||||
-- @raise if args is nil, then the global `args` must be available!
|
||||
function app.parse_args (args,flags_with_values)
|
||||
if not args then
|
||||
args = _G.arg
|
||||
if not args then error "Not in a main program: 'arg' not found" end
|
||||
end
|
||||
flags_with_values = flags_with_values or {}
|
||||
local _args = {}
|
||||
local flags = {}
|
||||
local i = 1
|
||||
while i <= #args do
|
||||
local a = args[i]
|
||||
local v = a:match('^-(.+)')
|
||||
local is_long
|
||||
if v then -- we have a flag
|
||||
if v:find '^-' then
|
||||
is_long = true
|
||||
v = v:sub(2)
|
||||
end
|
||||
if flags_with_values[v] then
|
||||
if i == #args or args[i+1]:find '^-' then
|
||||
return utils.raise ("no value for '"..v.."'")
|
||||
end
|
||||
flags[v] = args[i+1]
|
||||
i = i + 1
|
||||
else
|
||||
-- a value can also be indicated with = or :
|
||||
local var,val = utils.splitv (v,'[=:]')
|
||||
var = var or v
|
||||
val = val or true
|
||||
if not is_long then
|
||||
if #var > 1 then
|
||||
if var:find '.%d+' then -- short flag, number value
|
||||
val = var:sub(2)
|
||||
var = var:sub(1,1)
|
||||
else -- multiple short flags
|
||||
for i = 1,#var do
|
||||
flags[var:sub(i,i)] = true
|
||||
end
|
||||
val = nil -- prevents use of var as a flag below
|
||||
end
|
||||
else -- single short flag (can have value, defaults to true)
|
||||
val = val or true
|
||||
end
|
||||
end
|
||||
if val then
|
||||
flags[var] = val
|
||||
end
|
||||
end
|
||||
else
|
||||
_args[#_args+1] = a
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return flags,_args
|
||||
end
|
||||
|
||||
return app
|
|
@ -1,493 +0,0 @@
|
|||
--- Operations on two-dimensional arrays.
|
||||
-- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.types`
|
||||
-- @module pl.array2d
|
||||
|
||||
local tonumber,assert,tostring,io,ipairs,string,table =
|
||||
_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
|
||||
local setmetatable,getmetatable = setmetatable,getmetatable
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local types = require 'pl.types'
|
||||
local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
|
||||
local remove = table.remove
|
||||
local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
|
||||
local byte = string.byte
|
||||
local stdout = io.stdout
|
||||
local min = math.min
|
||||
|
||||
|
||||
local array2d = {}
|
||||
|
||||
local function obj (int,out)
|
||||
local mt = getmetatable(int)
|
||||
if mt then
|
||||
setmetatable(out,mt)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function makelist (res)
|
||||
return setmetatable(res, require('pl.List'))
|
||||
end
|
||||
|
||||
|
||||
local function index (t,k)
|
||||
return t[k]
|
||||
end
|
||||
|
||||
--- return the row and column size.
|
||||
-- @array2d t a 2d array
|
||||
-- @treturn int number of rows
|
||||
-- @treturn int number of cols
|
||||
function array2d.size (t)
|
||||
assert_arg(1,t,'table')
|
||||
return #t,#t[1]
|
||||
end
|
||||
|
||||
--- extract a column from the 2D array.
|
||||
-- @array2d a 2d array
|
||||
-- @param key an index or key
|
||||
-- @return 1d array
|
||||
function array2d.column (a,key)
|
||||
assert_arg(1,a,'table')
|
||||
return makelist(imap(index,a,key))
|
||||
end
|
||||
local column = array2d.column
|
||||
|
||||
--- map a function over a 2D array
|
||||
-- @func f a function of at least one argument
|
||||
-- @array2d a 2d array
|
||||
-- @param arg an optional extra argument to be passed to the function.
|
||||
-- @return 2d array
|
||||
function array2d.map (f,a,arg)
|
||||
assert_arg(1,a,'table')
|
||||
f = utils.function_arg(1,f)
|
||||
return obj(a,imap(function(row) return imap(f,row,arg) end, a))
|
||||
end
|
||||
|
||||
--- reduce the rows using a function.
|
||||
-- @func f a binary function
|
||||
-- @array2d a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_rows (f,a)
|
||||
assert_arg(1,a,'table')
|
||||
return tmap(function(row) return reduce(f,row) end, a)
|
||||
end
|
||||
|
||||
--- reduce the columns using a function.
|
||||
-- @func f a binary function
|
||||
-- @array2d a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_cols (f,a)
|
||||
assert_arg(1,a,'table')
|
||||
return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1]))
|
||||
end
|
||||
|
||||
--- reduce a 2D array into a scalar, using two operations.
|
||||
-- @func opc operation to reduce the final result
|
||||
-- @func opr operation to reduce the rows
|
||||
-- @param a 2D array
|
||||
function array2d.reduce2 (opc,opr,a)
|
||||
assert_arg(3,a,'table')
|
||||
local tmp = array2d.reduce_rows(opr,a)
|
||||
return reduce(opc,tmp)
|
||||
end
|
||||
|
||||
--- map a function over two arrays.
|
||||
-- They can be both or either 2D arrays
|
||||
-- @func f function of at least two arguments
|
||||
-- @int ad order of first array (1 or 2)
|
||||
-- @int bd order of second array (1 or 2)
|
||||
-- @tab a 1d or 2d array
|
||||
-- @tab b 1d or 2d array
|
||||
-- @param arg optional extra argument to pass to function
|
||||
-- @return 2D array, unless both arrays are 1D
|
||||
function array2d.map2 (f,ad,bd,a,b,arg)
|
||||
assert_arg(1,a,'table')
|
||||
assert_arg(2,b,'table')
|
||||
f = utils.function_arg(1,f)
|
||||
if ad == 1 and bd == 2 then
|
||||
return imap(function(row)
|
||||
return tmap2(f,a,row,arg)
|
||||
end, b)
|
||||
elseif ad == 2 and bd == 1 then
|
||||
return imap(function(row)
|
||||
return tmap2(f,row,b,arg)
|
||||
end, a)
|
||||
elseif ad == 1 and bd == 1 then
|
||||
return tmap2(f,a,b)
|
||||
elseif ad == 2 and bd == 2 then
|
||||
return tmap2(function(rowa,rowb)
|
||||
return tmap2(f,rowa,rowb,arg)
|
||||
end, a,b)
|
||||
end
|
||||
end
|
||||
|
||||
--- cartesian product of two 1d arrays.
|
||||
-- @func f a function of 2 arguments
|
||||
-- @array t1 a 1d table
|
||||
-- @array t2 a 1d table
|
||||
-- @return 2d table
|
||||
-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
|
||||
function array2d.product (f,t1,t2)
|
||||
f = utils.function_arg(1,f)
|
||||
assert_arg(2,t1,'table')
|
||||
assert_arg(3,t2,'table')
|
||||
local res, map = {}, tablex.map
|
||||
for i,v in ipairs(t2) do
|
||||
res[i] = map(f,t1,v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- flatten a 2D array.
|
||||
-- (this goes over columns first.)
|
||||
-- @array2d t 2d table
|
||||
-- @return a 1d table
|
||||
-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
|
||||
function array2d.flatten (t)
|
||||
local res = {}
|
||||
local k = 1
|
||||
for _,a in ipairs(t) do -- for all rows
|
||||
for i = 1,#a do
|
||||
res[k] = a[i]
|
||||
k = k + 1
|
||||
end
|
||||
end
|
||||
return makelist(res)
|
||||
end
|
||||
|
||||
--- reshape a 2D array.
|
||||
-- @array2d t 2d array
|
||||
-- @int nrows new number of rows
|
||||
-- @bool co column-order (Fortran-style) (default false)
|
||||
-- @return a new 2d array
|
||||
function array2d.reshape (t,nrows,co)
|
||||
local nr,nc = array2d.size(t)
|
||||
local ncols = nr*nc / nrows
|
||||
local res = {}
|
||||
local ir,ic = 1,1
|
||||
for i = 1,nrows do
|
||||
local row = {}
|
||||
for j = 1,ncols do
|
||||
row[j] = t[ir][ic]
|
||||
if not co then
|
||||
ic = ic + 1
|
||||
if ic > nc then
|
||||
ir = ir + 1
|
||||
ic = 1
|
||||
end
|
||||
else
|
||||
ir = ir + 1
|
||||
if ir > nr then
|
||||
ic = ic + 1
|
||||
ir = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
res[i] = row
|
||||
end
|
||||
return obj(t,res)
|
||||
end
|
||||
|
||||
--- swap two rows of an array.
|
||||
-- @array2d t a 2d array
|
||||
-- @int i1 a row index
|
||||
-- @int i2 a row index
|
||||
function array2d.swap_rows (t,i1,i2)
|
||||
assert_arg(1,t,'table')
|
||||
t[i1],t[i2] = t[i2],t[i1]
|
||||
end
|
||||
|
||||
--- swap two columns of an array.
|
||||
-- @array2d t a 2d array
|
||||
-- @int j1 a column index
|
||||
-- @int j2 a column index
|
||||
function array2d.swap_cols (t,j1,j2)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
local row = t[i]
|
||||
row[j1],row[j2] = row[j2],row[j1]
|
||||
end
|
||||
end
|
||||
|
||||
--- extract the specified rows.
|
||||
-- @array2d t 2d array
|
||||
-- @tparam {int} ridx a table of row indices
|
||||
function array2d.extract_rows (t,ridx)
|
||||
return obj(t,index_by(t,ridx))
|
||||
end
|
||||
|
||||
--- extract the specified columns.
|
||||
-- @array2d t 2d array
|
||||
-- @tparam {int} cidx a table of column indices
|
||||
function array2d.extract_cols (t,cidx)
|
||||
assert_arg(1,t,'table')
|
||||
local res = {}
|
||||
for i = 1,#t do
|
||||
res[i] = index_by(t[i],cidx)
|
||||
end
|
||||
return obj(t,res)
|
||||
end
|
||||
|
||||
--- remove a row from an array.
|
||||
-- @function array2d.remove_row
|
||||
-- @array2d t a 2d array
|
||||
-- @int i a row index
|
||||
array2d.remove_row = remove
|
||||
|
||||
--- remove a column from an array.
|
||||
-- @array2d t a 2d array
|
||||
-- @int j a column index
|
||||
function array2d.remove_col (t,j)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
remove(t[i],j)
|
||||
end
|
||||
end
|
||||
|
||||
local function _parse (s)
|
||||
local c,r
|
||||
if s:sub(1,1) == 'R' then
|
||||
r,c = s:match 'R(%d+)C(%d+)'
|
||||
r,c = tonumber(r),tonumber(c)
|
||||
else
|
||||
c,r = s:match '(.)(.)'
|
||||
c = byte(c) - byte 'A' + 1
|
||||
r = tonumber(r)
|
||||
end
|
||||
assert(c ~= nil and r ~= nil,'bad cell specifier: '..s)
|
||||
return r,c
|
||||
end
|
||||
|
||||
--- parse a spreadsheet range.
|
||||
-- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
|
||||
-- a special case is a single element (e.g 'A1' or 'R1C1')
|
||||
-- @string s a range.
|
||||
-- @treturn int start col
|
||||
-- @treturn int start row
|
||||
-- @treturn int end col
|
||||
-- @treturn int end row
|
||||
function array2d.parse_range (s)
|
||||
if s:find ':' then
|
||||
local start,finish = splitv(s,':')
|
||||
local i1,j1 = _parse(start)
|
||||
local i2,j2 = _parse(finish)
|
||||
return i1,j1,i2,j2
|
||||
else -- single value
|
||||
local i,j = _parse(s)
|
||||
return i,j
|
||||
end
|
||||
end
|
||||
|
||||
--- get a slice of a 2D array using spreadsheet range notation. @see parse_range
|
||||
-- @array2d t a 2D array
|
||||
-- @string rstr range expression
|
||||
-- @return a slice
|
||||
-- @see array2d.parse_range
|
||||
-- @see array2d.slice
|
||||
function array2d.range (t,rstr)
|
||||
assert_arg(1,t,'table')
|
||||
local i1,j1,i2,j2 = array2d.parse_range(rstr)
|
||||
if i2 then
|
||||
return array2d.slice(t,i1,j1,i2,j2)
|
||||
else -- single value
|
||||
return t[i1][j1]
|
||||
end
|
||||
end
|
||||
|
||||
local function default_range (t,i1,j1,i2,j2)
|
||||
local nr, nc = array2d.size(t)
|
||||
i1,j1 = i1 or 1, j1 or 1
|
||||
i2,j2 = i2 or nr, j2 or nc
|
||||
if i2 < 0 then i2 = nr + i2 + 1 end
|
||||
if j2 < 0 then j2 = nc + j2 + 1 end
|
||||
return i1,j1,i2,j2
|
||||
end
|
||||
|
||||
--- get a slice of a 2D array. Note that if the specified range has
|
||||
-- a 1D result, the rank of the result will be 1.
|
||||
-- @array2d t a 2D array
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @return an array, 2D in general but 1D in special cases.
|
||||
function array2d.slice (t,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
local res = {}
|
||||
for i = i1,i2 do
|
||||
local val
|
||||
local row = t[i]
|
||||
if j1 == j2 then
|
||||
val = row[j1]
|
||||
else
|
||||
val = {}
|
||||
for j = j1,j2 do
|
||||
val[#val+1] = row[j]
|
||||
end
|
||||
end
|
||||
res[#res+1] = val
|
||||
end
|
||||
if i1 == i2 then res = res[1] end
|
||||
return obj(t,res)
|
||||
end
|
||||
|
||||
--- set a specified range of an array to a value.
|
||||
-- @array2d t a 2D array
|
||||
-- @param value the value (may be a function)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @see tablex.set
|
||||
function array2d.set (t,value,i1,j1,i2,j2)
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
for i = i1,i2 do
|
||||
tset(t[i],value,j1,j2)
|
||||
end
|
||||
end
|
||||
|
||||
--- write a 2D array to a file.
|
||||
-- @array2d t a 2D array
|
||||
-- @param f a file object (default stdout)
|
||||
-- @string fmt a format string (default is just to use tostring)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.write (t,f,fmt,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
f = f or stdout
|
||||
local rowop
|
||||
if fmt then
|
||||
rowop = function(row,j) fprintf(f,fmt,row[j]) end
|
||||
else
|
||||
rowop = function(row,j) f:write(tostring(row[j]),' ') end
|
||||
end
|
||||
local function newline()
|
||||
f:write '\n'
|
||||
end
|
||||
array2d.forall(t,rowop,newline,i1,j1,i2,j2)
|
||||
end
|
||||
|
||||
--- perform an operation for all values in a 2D array.
|
||||
-- @array2d t 2D array
|
||||
-- @func row_op function to call on each value
|
||||
-- @func end_row_op function to call at end of each row
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
for i = i1,i2 do
|
||||
local row = t[i]
|
||||
for j = j1,j2 do
|
||||
row_op(row,j)
|
||||
end
|
||||
if end_row_op then end_row_op(i) end
|
||||
end
|
||||
end
|
||||
|
||||
---- move a block from the destination to the source.
|
||||
-- @array2d dest a 2D array
|
||||
-- @int di start row in dest
|
||||
-- @int dj start col in dest
|
||||
-- @array2d src a 2D array
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
|
||||
assert_arg(1,dest,'table')
|
||||
assert_arg(4,src,'table')
|
||||
i1,j1,i2,j2 = default_range(src,i1,j1,i2,j2)
|
||||
local nr,nc = array2d.size(dest)
|
||||
i2, j2 = min(nr,i2), min(nc,j2)
|
||||
--i1, j1 = max(1,i1), max(1,j1)
|
||||
dj = dj - 1
|
||||
for i = i1,i2 do
|
||||
local drow, srow = dest[i+di-1], src[i]
|
||||
for j = j1,j2 do
|
||||
drow[j+dj] = srow[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- iterate over all elements in a 2D array, with optional indices.
|
||||
-- @array2d a 2D array
|
||||
-- @tparam {int} indices with indices (default false)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @return either value or i,j,value depending on indices
|
||||
function array2d.iter (a,indices,i1,j1,i2,j2)
|
||||
assert_arg(1,a,'table')
|
||||
local norowset = not (i2 and j2)
|
||||
i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2)
|
||||
local i,j = i1-1,j1-1
|
||||
local row,nr = nil,0
|
||||
local onr = j2 - j1 + 1
|
||||
return function()
|
||||
j = j + 1
|
||||
if j > nr then
|
||||
j = j1
|
||||
i = i + 1
|
||||
if i > i2 then return nil end
|
||||
row = a[i]
|
||||
nr = norowset and #row or onr
|
||||
end
|
||||
if indices then
|
||||
return i,j,row[j]
|
||||
else
|
||||
return row[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- iterate over all columns.
|
||||
-- @array2d a a 2D array
|
||||
-- @return each column in turn
|
||||
function array2d.columns (a)
|
||||
assert_arg(1,a,'table')
|
||||
local n = a[1][1]
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > n then return nil end
|
||||
return column(a,i)
|
||||
end
|
||||
end
|
||||
|
||||
--- new array of specified dimensions
|
||||
-- @int rows number of rows
|
||||
-- @int cols number of cols
|
||||
-- @param val initial value; if it's a function then use `val(i,j)`
|
||||
-- @return new 2d array
|
||||
function array2d.new(rows,cols,val)
|
||||
local res = {}
|
||||
local fun = types.is_callable(val)
|
||||
for i = 1,rows do
|
||||
local row = {}
|
||||
if fun then
|
||||
for j = 1,cols do row[j] = val(i,j) end
|
||||
else
|
||||
for j = 1,cols do row[j] = val end
|
||||
end
|
||||
res[i] = row
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
return array2d
|
||||
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
--- Provides a reuseable and convenient framework for creating classes in Lua.
|
||||
-- Two possible notations:
|
||||
--
|
||||
-- B = class(A)
|
||||
-- class.B(A)
|
||||
--
|
||||
-- The latter form creates a named class within the current environment. Note
|
||||
-- that this implicitly brings in `pl.utils` as a dependency.
|
||||
--
|
||||
-- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion}
|
||||
-- @module pl.class
|
||||
|
||||
local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
|
||||
_G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
|
||||
local compat
|
||||
|
||||
-- this trickery is necessary to prevent the inheritance of 'super' and
|
||||
-- the resulting recursive call problems.
|
||||
local function call_ctor (c,obj,...)
|
||||
-- nice alias for the base class ctor
|
||||
local base = rawget(c,'_base')
|
||||
if base then
|
||||
local parent_ctor = rawget(base,'_init')
|
||||
while not parent_ctor do
|
||||
base = rawget(base,'_base')
|
||||
if not base then break end
|
||||
parent_ctor = rawget(base,'_init')
|
||||
end
|
||||
if parent_ctor then
|
||||
rawset(obj,'super',function(obj,...)
|
||||
call_ctor(base,obj,...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
local res = c._init(obj,...)
|
||||
rawset(obj,'super',nil)
|
||||
return res
|
||||
end
|
||||
|
||||
--- initializes an __instance__ upon creation.
|
||||
-- @function class:_init
|
||||
-- @param ... parameters passed to the constructor
|
||||
-- @usage local Cat = class()
|
||||
-- function Cat:_init(name)
|
||||
-- --self:super(name) -- call the ancestor initializer if needed
|
||||
-- self.name = name
|
||||
-- end
|
||||
--
|
||||
-- local pussycat = Cat("pussycat")
|
||||
-- print(pussycat.name) --> pussycat
|
||||
|
||||
--- checks whether an __instance__ is derived from some class.
|
||||
-- Works the other way around as `class_of`. It has two ways of using;
|
||||
-- 1) call with a class to check against, 2) call without params.
|
||||
-- @function instance:is_a
|
||||
-- @param some_class class to check against, or `nil` to return the class
|
||||
-- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then
|
||||
-- it returns the class table of the instance
|
||||
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
|
||||
-- if pussycat:is_a(Cat) then
|
||||
-- -- it's true, it is a Lion, but also a Cat
|
||||
-- end
|
||||
--
|
||||
-- if pussycat:is_a() == Lion then
|
||||
-- -- It's true
|
||||
-- end
|
||||
local function is_a(self,klass)
|
||||
if klass == nil then
|
||||
-- no class provided, so return the class this instance is derived from
|
||||
return getmetatable(self)
|
||||
end
|
||||
local m = getmetatable(self)
|
||||
if not m then return false end --*can't be an object!
|
||||
while m do
|
||||
if m == klass then return true end
|
||||
m = rawget(m,'_base')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- checks whether an __instance__ is derived from some class.
|
||||
-- Works the other way around as `is_a`.
|
||||
-- @function some_class:class_of
|
||||
-- @param some_instance instance to check against
|
||||
-- @return `true` if `some_instance` is derived from `some_class`
|
||||
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
|
||||
-- if Cat:class_of(pussycat) then
|
||||
-- -- it's true
|
||||
-- end
|
||||
local function class_of(klass,obj)
|
||||
if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
|
||||
return klass.is_a(obj,klass)
|
||||
end
|
||||
|
||||
--- cast an object to another class.
|
||||
-- It is not clever (or safe!) so use carefully.
|
||||
-- @param some_instance the object to be changed
|
||||
-- @function some_class:cast
|
||||
local function cast (klass, obj)
|
||||
return setmetatable(obj,klass)
|
||||
end
|
||||
|
||||
|
||||
local function _class_tostring (obj)
|
||||
local mt = obj._class
|
||||
local name = rawget(mt,'_name')
|
||||
setmetatable(obj,nil)
|
||||
local str = tostring(obj)
|
||||
setmetatable(obj,mt)
|
||||
if name then str = name ..str:gsub('table','') end
|
||||
return str
|
||||
end
|
||||
|
||||
local function tupdate(td,ts,dont_override)
|
||||
for k,v in pairs(ts) do
|
||||
if not dont_override or td[k] == nil then
|
||||
td[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _class(base,c_arg,c)
|
||||
-- the class `c` will be the metatable for all its objects,
|
||||
-- and they will look up their methods in it.
|
||||
local mt = {} -- a metatable for the class to support __call and _handler
|
||||
-- can define class by passing it a plain table of methods
|
||||
local plain = type(base) == 'table' and not getmetatable(base)
|
||||
if plain then
|
||||
c = base
|
||||
base = c._base
|
||||
else
|
||||
c = c or {}
|
||||
end
|
||||
|
||||
if type(base) == 'table' then
|
||||
-- our new class is a shallow copy of the base class!
|
||||
-- but be careful not to wipe out any methods we have been given at this point!
|
||||
tupdate(c,base,plain)
|
||||
c._base = base
|
||||
-- inherit the 'not found' handler, if present
|
||||
if rawget(c,'_handler') then mt.__index = c._handler end
|
||||
elseif base ~= nil then
|
||||
error("must derive from a table type",3)
|
||||
end
|
||||
|
||||
c.__index = c
|
||||
setmetatable(c,mt)
|
||||
if not plain then
|
||||
c._init = nil
|
||||
end
|
||||
|
||||
if base and rawget(base,'_class_init') then
|
||||
base._class_init(c,c_arg)
|
||||
end
|
||||
|
||||
-- expose a ctor which can be called by <classname>(<args>)
|
||||
mt.__call = function(class_tbl,...)
|
||||
local obj
|
||||
if rawget(c,'_create') then obj = c._create(...) end
|
||||
if not obj then obj = {} end
|
||||
setmetatable(obj,c)
|
||||
|
||||
if rawget(c,'_init') then -- explicit constructor
|
||||
local res = call_ctor(c,obj,...)
|
||||
if res then -- _if_ a ctor returns a value, it becomes the object...
|
||||
obj = res
|
||||
setmetatable(obj,c)
|
||||
end
|
||||
elseif base and rawget(base,'_init') then -- default constructor
|
||||
-- make sure that any stuff from the base class is initialized!
|
||||
call_ctor(base,obj,...)
|
||||
end
|
||||
|
||||
if base and rawget(base,'_post_init') then
|
||||
base._post_init(obj)
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
-- Call Class.catch to set a handler for methods/properties not found in the class!
|
||||
c.catch = function(self, handler)
|
||||
if type(self) == "function" then
|
||||
-- called using . instead of :
|
||||
handler = self
|
||||
end
|
||||
c._handler = handler
|
||||
mt.__index = handler
|
||||
end
|
||||
c.is_a = is_a
|
||||
c.class_of = class_of
|
||||
c.cast = cast
|
||||
c._class = c
|
||||
|
||||
if not rawget(c,'__tostring') then
|
||||
c.__tostring = _class_tostring
|
||||
end
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
--- create a new class, derived from a given base class.
|
||||
-- Supporting two class creation syntaxes:
|
||||
-- either `Name = class(base)` or `class.Name(base)`.
|
||||
-- The first form returns the class directly and does not set its `_name`.
|
||||
-- The second form creates a variable `Name` in the current environment set
|
||||
-- to the class, and also sets `_name`.
|
||||
-- @function class
|
||||
-- @param base optional base class
|
||||
-- @param c_arg optional parameter to class constructor
|
||||
-- @param c optional table to be used as class
|
||||
local class
|
||||
class = setmetatable({},{
|
||||
__call = function(fun,...)
|
||||
return _class(...)
|
||||
end,
|
||||
__index = function(tbl,key)
|
||||
if key == 'class' then
|
||||
io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
|
||||
return class
|
||||
end
|
||||
compat = compat or require 'pl.compat'
|
||||
local env = compat.getfenv(2)
|
||||
return function(...)
|
||||
local c = _class(...)
|
||||
c._name = key
|
||||
rawset(env,key,c)
|
||||
return c
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
class.properties = class()
|
||||
|
||||
function class.properties._class_init(klass)
|
||||
klass.__index = function(t,key)
|
||||
-- normal class lookup!
|
||||
local v = klass[key]
|
||||
if v then return v end
|
||||
-- is it a getter?
|
||||
v = rawget(klass,'get_'..key)
|
||||
if v then
|
||||
return v(t)
|
||||
end
|
||||
-- is it a field?
|
||||
return rawget(t,'_'..key)
|
||||
end
|
||||
klass.__newindex = function (t,key,value)
|
||||
-- if there's a setter, use that, otherwise directly set table
|
||||
local p = 'set_'..key
|
||||
local setter = klass[p]
|
||||
if setter then
|
||||
setter(t,value)
|
||||
else
|
||||
rawset(t,key,value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return class
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
----------------
|
||||
--- Lua 5.1/5.2/5.3 compatibility.
|
||||
-- Ensures that `table.pack` and `package.searchpath` are available
|
||||
-- for Lua 5.1 and LuaJIT.
|
||||
-- The exported function `load` is Lua 5.2 compatible.
|
||||
-- `compat.setfenv` and `compat.getfenv` are available for Lua 5.2, although
|
||||
-- they are not always guaranteed to work.
|
||||
-- @module pl.compat
|
||||
|
||||
local compat = {}
|
||||
|
||||
compat.lua51 = _VERSION == 'Lua 5.1'
|
||||
|
||||
local isJit = (tostring(assert):match('builtin') ~= nil)
|
||||
if isJit then
|
||||
-- 'goto' is a keyword when 52 compatibility is enabled in LuaJit
|
||||
compat.jit52 = not loadstring("local goto = 1")
|
||||
end
|
||||
|
||||
compat.dir_separator = _G.package.config:sub(1,1)
|
||||
compat.is_windows = compat.dir_separator == '\\'
|
||||
|
||||
--- execute a shell command.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function compat.execute (cmd)
|
||||
local res1,_,res3 = os.execute(cmd)
|
||||
if compat.lua51 and not compat.jit52 then
|
||||
if compat.is_windows then
|
||||
res1 = res1 > 255 and res1 % 256 or res1
|
||||
return res1==0,res1
|
||||
else
|
||||
res1 = res1 > 255 and res1 / 256 or res1
|
||||
return res1==0,res1
|
||||
end
|
||||
else
|
||||
if compat.is_windows then
|
||||
res3 = res3 > 255 and res3 % 256 or res3
|
||||
return res3==0,res3
|
||||
else
|
||||
return not not res1,res3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
----------------
|
||||
-- Load Lua code as a text or binary chunk.
|
||||
-- @param ld code string or loader
|
||||
-- @param[opt] source name of chunk for errors
|
||||
-- @param[opt] mode 'b', 't' or 'bt'
|
||||
-- @param[opt] env environment to load the chunk in
|
||||
-- @function compat.load
|
||||
|
||||
---------------
|
||||
-- Get environment of a function.
|
||||
-- With Lua 5.2, may return nil for a function with no global references!
|
||||
-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
|
||||
-- @param f a function or a call stack reference
|
||||
-- @function compat.getfenv
|
||||
|
||||
---------------
|
||||
-- Set environment of a function
|
||||
-- @param f a function or a call stack reference
|
||||
-- @param env a table that becomes the new environment of `f`
|
||||
-- @function compat.setfenv
|
||||
|
||||
if compat.lua51 then -- define Lua 5.2 style load()
|
||||
if not isJit then -- but LuaJIT's load _is_ compatible
|
||||
local lua51_load = load
|
||||
function compat.load(str,src,mode,env)
|
||||
local chunk,err
|
||||
if type(str) == 'string' then
|
||||
if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then
|
||||
return nil,"attempt to load a binary chunk"
|
||||
end
|
||||
chunk,err = loadstring(str,src)
|
||||
else
|
||||
chunk,err = lua51_load(str,src)
|
||||
end
|
||||
if chunk and env then setfenv(chunk,env) end
|
||||
return chunk,err
|
||||
end
|
||||
else
|
||||
compat.load = load
|
||||
end
|
||||
compat.setfenv, compat.getfenv = setfenv, getfenv
|
||||
else
|
||||
compat.load = load
|
||||
-- setfenv/getfenv replacements for Lua 5.2
|
||||
-- by Sergey Rozhenko
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
|
||||
-- in the case of a function with no globals:
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
|
||||
function compat.setfenv(f, t)
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
if name then
|
||||
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
|
||||
debug.setupvalue(f, up, t)
|
||||
end
|
||||
if f ~= 0 then return f end
|
||||
end
|
||||
|
||||
function compat.getfenv(f)
|
||||
local f = f or 0
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name, val
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name, val = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
return val
|
||||
end
|
||||
end
|
||||
|
||||
--- Lua 5.2 Functions Available for 5.1
|
||||
-- @section lua52
|
||||
|
||||
--- pack an argument list into a table.
|
||||
-- @param ... any arguments
|
||||
-- @return a table with field n set to the length
|
||||
-- @function table.pack
|
||||
if not table.pack then
|
||||
function table.pack (...) -- luacheck: ignore
|
||||
return {n=select('#',...); ...}
|
||||
end
|
||||
end
|
||||
|
||||
--- unpack a table and return the elements.
|
||||
-- @param t table to unpack
|
||||
-- @param[opt] i index from which to start unpacking, defaults to 1
|
||||
-- @param[opt] t index of the last element to unpack, defaults to #t
|
||||
-- @return multiple returns values from the table
|
||||
if not table.unpack then
|
||||
table.unpack = unpack -- luacheck: ignore
|
||||
end
|
||||
|
||||
------
|
||||
-- return the full path where a Lua module name would be matched.
|
||||
-- @param mod module name, possibly dotted
|
||||
-- @param path a path in the same form as package.path or package.cpath
|
||||
-- @see path.package_path
|
||||
-- @function package.searchpath
|
||||
if not package.searchpath then
|
||||
local sep = package.config:sub(1,1)
|
||||
function package.searchpath (mod,path) -- luacheck: ignore
|
||||
mod = mod:gsub('%.',sep)
|
||||
for m in path:gmatch('[^;]+') do
|
||||
local nm = m:gsub('?',mod)
|
||||
local f = io.open(nm,'r')
|
||||
if f then f:close(); return nm end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return compat
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue