Compare commits
No commits in common. "setup" and "master" have entirely different histories.
1
.aegisub/automation/.envrc
Normal file
1
.aegisub/automation/.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
export AEGISUB_LUA=1
|
1
.aegisub/automation/0x539
Submodule
1
.aegisub/automation/0x539
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit be2957e58b7ebdc6d3522a792f0120798dfbafa7
|
75
.aegisub/automation/autoload/Flux.Selegator.moon
Normal file
75
.aegisub/automation/autoload/Flux.Selegator.moon
Normal file
|
@ -0,0 +1,75 @@
|
|||
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 }
|
||||
}
|
727
.aegisub/automation/autoload/a-mo.Aegisub-Motion.moon
Normal file
727
.aegisub/automation/autoload/a-mo.Aegisub-Motion.moon
Normal file
File diff suppressed because it is too large
Load diff
25
.aegisub/automation/autoload/clean-k-tags.lua
Normal file
25
.aegisub/automation/autoload/clean-k-tags.lua
Normal file
|
@ -0,0 +1,25 @@
|
|||
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)
|
178
.aegisub/automation/autoload/duetto-meika.lua
Normal file
178
.aegisub/automation/autoload/duetto-meika.lua
Normal file
|
@ -0,0 +1,178 @@
|
|||
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)
|
24
.aegisub/automation/autoload/frz_endro.lua
Normal file
24
.aegisub/automation/autoload/frz_endro.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
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)
|
235
.aegisub/automation/autoload/lyger.CircleText.lua
Normal file
235
.aegisub/automation/autoload/lyger.CircleText.lua
Normal file
|
@ -0,0 +1,235 @@
|
|||
--[[
|
||||
==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)
|
472
.aegisub/automation/autoload/lyger.ClipBlur.lua
Normal file
472
.aegisub/automation/autoload/lyger.ClipBlur.lua
Normal file
|
@ -0,0 +1,472 @@
|
|||
--[[
|
||||
==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)
|
77
.aegisub/automation/autoload/lyger.ClipShifter.lua
Normal file
77
.aegisub/automation/autoload/lyger.ClipShifter.lua
Normal file
|
@ -0,0 +1,77 @@
|
|||
--[[
|
||||
==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)
|
398
.aegisub/automation/autoload/lyger.FbfTransform.moon
Normal file
398
.aegisub/automation/autoload/lyger.FbfTransform.moon
Normal file
|
@ -0,0 +1,398 @@
|
|||
[[
|
||||
==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
|
227
.aegisub/automation/autoload/lyger.GradientByChar.lua
Normal file
227
.aegisub/automation/autoload/lyger.GradientByChar.lua
Normal file
|
@ -0,0 +1,227 @@
|
|||
--[[
|
||||
==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}
|
||||
}
|
569
.aegisub/automation/autoload/lyger.Image2ASS.lua
Normal file
569
.aegisub/automation/autoload/lyger.Image2ASS.lua
Normal file
File diff suppressed because it is too large
Load diff
175
.aegisub/automation/autoload/lyger.KaraHelper.lua
Normal file
175
.aegisub/automation/autoload/lyger.KaraHelper.lua
Normal file
|
@ -0,0 +1,175 @@
|
|||
--[[
|
||||
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)
|
||||
|
||||
|
||||
|
224
.aegisub/automation/autoload/lyger.KaraReplacer.lua
Normal file
224
.aegisub/automation/autoload/lyger.KaraReplacer.lua
Normal file
|
@ -0,0 +1,224 @@
|
|||
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)
|
633
.aegisub/automation/autoload/lyger.LuaInterpret.lua
Normal file
633
.aegisub/automation/autoload/lyger.LuaInterpret.lua
Normal file
File diff suppressed because it is too large
Load diff
591
.aegisub/automation/autoload/myaa.MergeScripts.moon
Normal file
591
.aegisub/automation/autoload/myaa.MergeScripts.moon
Normal file
File diff suppressed because it is too large
Load diff
1498
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
1498
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
File diff suppressed because it is too large
Load diff
1196
.aegisub/automation/autoload/ua.MultiCopy.lua
Normal file
1196
.aegisub/automation/autoload/ua.MultiCopy.lua
Normal file
File diff suppressed because it is too large
Load diff
1243
.aegisub/automation/autoload/ua.NecrosCopy.lua
Normal file
1243
.aegisub/automation/autoload/ua.NecrosCopy.lua
Normal file
File diff suppressed because it is too large
Load diff
3493
.aegisub/automation/autoload/ua.Relocator.lua
Normal file
3493
.aegisub/automation/autoload/ua.Relocator.lua
Normal file
File diff suppressed because it is too large
Load diff
3134
.aegisub/automation/autoload/ua.Significance.lua
Normal file
3134
.aegisub/automation/autoload/ua.Significance.lua
Normal file
File diff suppressed because it is too large
Load diff
185
.aegisub/automation/autoload/ua.TimeSigns.lua
Normal file
185
.aegisub/automation/autoload/ua.TimeSigns.lua
Normal file
|
@ -0,0 +1,185 @@
|
|||
-- 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
|
142
.aegisub/automation/include/a-mo/ConfigHandler.moon
Normal file
142
.aegisub/automation/include/a-mo/ConfigHandler.moon
Normal file
|
@ -0,0 +1,142 @@
|
|||
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
|
132
.aegisub/automation/include/a-mo/DataHandler.moon
Normal file
132
.aegisub/automation/include/a-mo/DataHandler.moon
Normal file
|
@ -0,0 +1,132 @@
|
|||
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
|
72
.aegisub/automation/include/a-mo/DataWrapper.moon
Normal file
72
.aegisub/automation/include/a-mo/DataWrapper.moon
Normal file
|
@ -0,0 +1,72 @@
|
|||
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
|
483
.aegisub/automation/include/a-mo/Line.moon
Normal file
483
.aegisub/automation/include/a-mo/Line.moon
Normal file
|
@ -0,0 +1,483 @@
|
|||
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
|
272
.aegisub/automation/include/a-mo/LineCollection.moon
Normal file
272
.aegisub/automation/include/a-mo/LineCollection.moon
Normal file
|
@ -0,0 +1,272 @@
|
|||
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
|
63
.aegisub/automation/include/a-mo/Log.moon
Normal file
63
.aegisub/automation/include/a-mo/Log.moon
Normal file
|
@ -0,0 +1,63 @@
|
|||
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!
|
||||
}
|
20
.aegisub/automation/include/a-mo/Math.moon
Normal file
20
.aegisub/automation/include/a-mo/Math.moon
Normal file
|
@ -0,0 +1,20 @@
|
|||
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
|
||||
}
|
312
.aegisub/automation/include/a-mo/MotionHandler.moon
Normal file
312
.aegisub/automation/include/a-mo/MotionHandler.moon
Normal file
|
@ -0,0 +1,312 @@
|
|||
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
|
130
.aegisub/automation/include/a-mo/ShakeShapeHandler.moon
Normal file
130
.aegisub/automation/include/a-mo/ShakeShapeHandler.moon
Normal file
|
@ -0,0 +1,130 @@
|
|||
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
|
153
.aegisub/automation/include/a-mo/Statistics.moon
Normal file
153
.aegisub/automation/include/a-mo/Statistics.moon
Normal file
|
@ -0,0 +1,153 @@
|
|||
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
|
193
.aegisub/automation/include/a-mo/Tags.moon
Normal file
193
.aegisub/automation/include/a-mo/Tags.moon
Normal file
|
@ -0,0 +1,193 @@
|
|||
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
|
143
.aegisub/automation/include/a-mo/Transform.moon
Normal file
143
.aegisub/automation/include/a-mo/Transform.moon
Normal file
|
@ -0,0 +1,143 @@
|
|||
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
|
185
.aegisub/automation/include/a-mo/TrimHandler.moon
Normal file
185
.aegisub/automation/include/a-mo/TrimHandler.moon
Normal file
|
@ -0,0 +1,185 @@
|
|||
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
|
901
.aegisub/automation/include/l0/Functional.moon
Normal file
901
.aegisub/automation/include/l0/Functional.moon
Normal file
File diff suppressed because it is too large
Load diff
362
.aegisub/automation/include/lyger/LibLyger.moon
Normal file
362
.aegisub/automation/include/lyger/LibLyger.moon
Normal file
|
@ -0,0 +1,362 @@
|
|||
[[
|
||||
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
|
387
.aegisub/automation/include/myaa/ASSParser.moon
Normal file
387
.aegisub/automation/include/myaa/ASSParser.moon
Normal file
|
@ -0,0 +1,387 @@
|
|||
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
|
34
.aegisub/automation/include/myaa/pl.lua
Normal file
34
.aegisub/automation/include/myaa/pl.lua
Normal file
|
@ -0,0 +1,34 @@
|
|||
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
|
663
.aegisub/automation/include/myaa/pl/Date.lua
Normal file
663
.aegisub/automation/include/myaa/pl/Date.lua
Normal file
File diff suppressed because it is too large
Load diff
566
.aegisub/automation/include/myaa/pl/List.lua
Normal file
566
.aegisub/automation/include/myaa/pl/List.lua
Normal file
File diff suppressed because it is too large
Load diff
113
.aegisub/automation/include/myaa/pl/Map.lua
Normal file
113
.aegisub/automation/include/myaa/pl/Map.lua
Normal file
|
@ -0,0 +1,113 @@
|
|||
--- 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
|
54
.aegisub/automation/include/myaa/pl/MultiMap.lua
Normal file
54
.aegisub/automation/include/myaa/pl/MultiMap.lua
Normal file
|
@ -0,0 +1,54 @@
|
|||
--- 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
|
167
.aegisub/automation/include/myaa/pl/OrderedMap.lua
Normal file
167
.aegisub/automation/include/myaa/pl/OrderedMap.lua
Normal file
|
@ -0,0 +1,167 @@
|
|||
--- 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
|
||||
|
||||
|
||||
|
222
.aegisub/automation/include/myaa/pl/Set.lua
Normal file
222
.aegisub/automation/include/myaa/pl/Set.lua
Normal file
|
@ -0,0 +1,222 @@
|
|||
--- 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
|
159
.aegisub/automation/include/myaa/pl/app.lua
Normal file
159
.aegisub/automation/include/myaa/pl/app.lua
Normal file
|
@ -0,0 +1,159 @@
|
|||
--- 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
|
493
.aegisub/automation/include/myaa/pl/array2d.lua
Normal file
493
.aegisub/automation/include/myaa/pl/array2d.lua
Normal file
|
@ -0,0 +1,493 @@
|
|||
--- 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
|
||||
|
||||
|
261
.aegisub/automation/include/myaa/pl/class.lua
Normal file
261
.aegisub/automation/include/myaa/pl/class.lua
Normal file
|
@ -0,0 +1,261 @@
|
|||
--- 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
|
||||
|
165
.aegisub/automation/include/myaa/pl/compat.lua
Normal file
165
.aegisub/automation/include/myaa/pl/compat.lua
Normal file
|
@ -0,0 +1,165 @@
|
|||
----------------
|
||||
--- 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