484 lines
15 KiB
Plaintext
484 lines
15 KiB
Plaintext
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
|