superfly-timeline
Advanced tools
Comparing version 7.0.2 to 7.1.0
@@ -5,2 +5,19 @@ # Change Log | ||
<a name="7.1.0"></a> | ||
# [7.1.0](https://github.com/SuperFlyTV/supertimeline/compare/7.0.2...7.1.0) (2019-05-18) | ||
### Bug Fixes | ||
* carefully merging of caps of instances ([4e0dd91](https://github.com/SuperFlyTV/supertimeline/commit/4e0dd91)) | ||
* issue with non-unique instance ids ([f594aa4](https://github.com/SuperFlyTV/supertimeline/commit/f594aa4)) | ||
### Features | ||
* implement resolveStates ([76a4ec6](https://github.com/SuperFlyTV/supertimeline/commit/76a4ec6)) | ||
* Implement support for negations ("!") together with parentheses ([efe79a6](https://github.com/SuperFlyTV/supertimeline/commit/efe79a6)) | ||
<a name="7.0.2"></a> | ||
@@ -7,0 +24,0 @@ ## [7.0.2](https://github.com/SuperFlyTV/supertimeline/compare/7.0.1...7.0.2) (2019-04-07) |
@@ -19,2 +19,4 @@ import { EventType } from './enums'; | ||
limitTime?: Time; | ||
/** If set to true, the resolver will go through the instances of the objects and fix collisions, so that the instances more closely resembles the end state. */ | ||
resolveInstanceCollisions?: boolean; | ||
} | ||
@@ -79,2 +81,3 @@ export interface TimelineObject { | ||
}; | ||
/** Map of the object ids, per layer */ | ||
layers: { | ||
@@ -130,3 +133,3 @@ [layer: string]: Array<string>; | ||
} | ||
export interface Reference { | ||
export interface ValueWithReference { | ||
value: number; | ||
@@ -156,7 +159,9 @@ references: Array<string>; | ||
time: Time; | ||
layers: { | ||
[layer: string]: ResolvedTimelineObjectInstance; | ||
}; | ||
layers: StateInTime; | ||
nextEvents: Array<NextEvent>; | ||
} | ||
export interface ResolvedStates extends ResolvedTimeline { | ||
state: AllStates; | ||
nextEvents: Array<NextEvent>; | ||
} | ||
export interface ResolvedTimelineObjectInstance extends ResolvedTimelineObject { | ||
@@ -170,1 +175,18 @@ instance: TimelineObjectInstance; | ||
} | ||
export interface ResolvedTimelineObjectInstanceKeyframe extends ResolvedTimelineObjectInstance { | ||
isKeyframe?: boolean; | ||
keyframeEndTime?: TimeMaybe; | ||
} | ||
export interface AllStates { | ||
[layer: string]: { | ||
[time: string]: ResolvedTimelineObjectInstanceKeyframe[] | null; | ||
}; | ||
} | ||
export interface StateInTime { | ||
[layer: string]: ResolvedTimelineObjectInstance; | ||
} | ||
export interface TimeEvent { | ||
time: number; | ||
/** true when the event indicate that something starts, false when something ends */ | ||
enable: boolean; | ||
} |
@@ -1,2 +0,2 @@ | ||
import { InstanceEvent, TimelineObjectInstance, ResolveOptions, Reference, Cap } from './api/api'; | ||
import { InstanceEvent, TimelineObjectInstance, ResolveOptions, ValueWithReference, Cap } from './api/api'; | ||
/** | ||
@@ -43,3 +43,3 @@ * Thanks to https://github.com/Microsoft/TypeScript/issues/23126#issuecomment-395929162 | ||
*/ | ||
export declare function operateOnArrays(array0: Array<TimelineObjectInstance> | Reference | null, array1: Array<TimelineObjectInstance> | Reference | null, operate: (a: Reference | null, b: Reference | null) => Reference | null): Array<TimelineObjectInstance> | Reference | null; | ||
export declare function operateOnArrays(array0: Array<TimelineObjectInstance> | ValueWithReference | null, array1: Array<TimelineObjectInstance> | ValueWithReference | null, operate: (a: ValueWithReference | null, b: ValueWithReference | null) => ValueWithReference | null): Array<TimelineObjectInstance> | ValueWithReference | null; | ||
/** | ||
@@ -51,6 +51,7 @@ * Like operateOnArrays, but will multiply the number of elements in array0, with the number of elements in array1 | ||
*/ | ||
export declare function applyRepeatingInstances(instances: TimelineObjectInstance[], repeatTime0: Reference | null, options: ResolveOptions): TimelineObjectInstance[]; | ||
export declare function capInstances(instances: TimelineObjectInstance[], parentInstances: Reference | TimelineObjectInstance[] | null): TimelineObjectInstance[]; | ||
export declare function isReference(ref: any): ref is Reference; | ||
export declare function applyRepeatingInstances(instances: TimelineObjectInstance[], repeatTime0: ValueWithReference | null, options: ResolveOptions): TimelineObjectInstance[]; | ||
export declare function capInstances(instances: TimelineObjectInstance[], parentInstances: ValueWithReference | TimelineObjectInstance[] | null): TimelineObjectInstance[]; | ||
export declare function isReference(ref: any): ref is ValueWithReference; | ||
export declare function joinReferences(...references: Array<Array<string> | string>): Array<string>; | ||
export declare function addCapsToResuming(instance: TimelineObjectInstance, ...caps: Array<Array<Cap> | undefined>): void; | ||
export declare function joinCaps(...caps: Array<Array<Cap> | undefined>): Array<Cap>; | ||
@@ -57,0 +58,0 @@ /** |
@@ -152,3 +152,3 @@ "use strict"; | ||
lastInstance.references = joinReferences(lastInstance.references, event.references); | ||
lastInstance.caps = joinCaps(lastInstance.caps, event.data.instance.caps); | ||
addCapsToResuming(lastInstance, event.data.instance.caps); | ||
} | ||
@@ -171,3 +171,3 @@ else if (!lastInstance || | ||
lastInstance.references = joinReferences(lastInstance.references, event.references); | ||
lastInstance.caps = joinCaps(lastInstance.caps, event.data.instance.caps); | ||
addCapsToResuming(lastInstance, event.data.instance.caps); | ||
} | ||
@@ -433,2 +433,22 @@ if (lastInstance && lastInstance.caps && !lastInstance.caps.length) | ||
exports.joinReferences = joinReferences; | ||
function addCapsToResuming(instance) { | ||
var caps = []; | ||
for (var _i = 1; _i < arguments.length; _i++) { | ||
caps[_i - 1] = arguments[_i]; | ||
} | ||
var capsToAdd = []; | ||
_.each(joinCaps.apply(void 0, caps), function (cap) { | ||
if (cap.end && | ||
instance.end && | ||
cap.end > instance.end) { | ||
capsToAdd.push({ | ||
id: cap.id, | ||
start: 0, | ||
end: cap.end | ||
}); | ||
} | ||
}); | ||
instance.caps = joinCaps(instance.caps, capsToAdd); | ||
} | ||
exports.addCapsToResuming = addCapsToResuming; | ||
function joinCaps() { | ||
@@ -435,0 +455,0 @@ var caps = []; |
import { Expression, ExpressionObj } from '../api/api'; | ||
export declare const OPERATORS: string[]; | ||
export declare function interpretExpression(expr: Expression): ExpressionObj | number | null; | ||
interface InnerExpression { | ||
inner: Array<any>; | ||
rest: Array<string>; | ||
} | ||
export declare function wrapInnerExpressions(words: Array<any>): InnerExpression; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var _ = require("underscore"); | ||
exports.OPERATORS = ['&', '|', '+', '-', '*', '/', '%']; | ||
exports.OPERATORS = ['&', '|', '+', '-', '*', '/', '%', '!']; | ||
function interpretExpression(expr) { | ||
@@ -48,2 +48,3 @@ if (_.isString(expr)) { | ||
// Turns ['a', '(', 'b', 'c', ')'] into ['a', ['b', 'c']] | ||
// or ['a', '&', '!', 'b'] into ['a', '&', ['', '!', 'b']] | ||
function wrapInnerExpressions(words) { | ||
@@ -63,2 +64,8 @@ for (var i = 0; i < words.length; i++) { | ||
} | ||
else if (words[i] === '!') { | ||
var tmp = wrapInnerExpressions(words.slice(i + 1)); | ||
// insert inner expression after the '!' | ||
words[i] = ['', '!'].concat(tmp.inner); | ||
words.splice.apply(words, [i + 1, 99999].concat(tmp.rest)); | ||
} | ||
} | ||
@@ -70,2 +77,3 @@ return { | ||
} | ||
exports.wrapInnerExpressions = wrapInnerExpressions; | ||
function words2Expression(operatorList, words) { | ||
@@ -72,0 +80,0 @@ if (!words || !words.length) |
@@ -1,2 +0,2 @@ | ||
import { TimelineObject, ResolvedTimeline, ResolveOptions, Expression, ResolvedTimelineObject, TimelineObjectInstance, Time, TimelineState, Reference } from '../api/api'; | ||
import { TimelineObject, ResolvedTimeline, ResolveOptions, Expression, ResolvedTimelineObject, TimelineObjectInstance, Time, TimelineState, ValueWithReference, ResolvedStates } from '../api/api'; | ||
export declare class Resolver { | ||
@@ -10,4 +10,8 @@ /** | ||
static resolveTimeline(timeline: Array<TimelineObject>, options: ResolveOptions): ResolvedTimeline; | ||
/** Calculate the state for all points in time. */ | ||
static resolveAllStates(resolvedTimeline: ResolvedTimeline): ResolvedStates; | ||
/** | ||
* Calculate the state at a given point in time. Using a ResolvedTimeline calculated by Resolver.resolveTimeline. | ||
* Calculate the state at a given point in time. | ||
* Using a ResolvedTimeline calculated by Resolver.resolveTimeline() or | ||
* a ResolvedStates calculated by Resolver.resolveAllStates() | ||
* @param resolved ResolvedTimeline calculated by Resolver.resolveTimeline. | ||
@@ -21,3 +25,3 @@ * @param time The point in time where to calculate the state | ||
declare type ObjectRefType = 'start' | 'end' | 'duration'; | ||
export declare function lookupExpression(resolvedTimeline: ResolvedTimeline, obj: TimelineObject, expr: Expression | null, context: ObjectRefType): Array<TimelineObjectInstance> | Reference | null; | ||
export declare function lookupExpression(resolvedTimeline: ResolvedTimeline, obj: TimelineObject, expr: Expression | null, context: ObjectRefType): Array<TimelineObjectInstance> | ValueWithReference | null; | ||
export {}; |
@@ -8,2 +8,3 @@ "use strict"; | ||
var state_1 = require("./state"); | ||
var common_1 = require("./common"); | ||
var Resolver = /** @class */ (function () { | ||
@@ -55,17 +56,3 @@ function Resolver() { | ||
o.resolved.isKeyframe = true; | ||
resolvedTimeline.objects[obj.id] = o; | ||
if (obj.classes) { | ||
_.each(obj.classes, function (className) { | ||
if (className) { | ||
if (!resolvedTimeline.classes[className]) | ||
resolvedTimeline.classes[className] = []; | ||
resolvedTimeline.classes[className].push(obj.id); | ||
} | ||
}); | ||
} | ||
if (obj.layer) { | ||
if (!resolvedTimeline.layers[obj.layer]) | ||
resolvedTimeline.layers[obj.layer] = []; | ||
resolvedTimeline.layers[obj.layer].push(obj.id); | ||
} | ||
common_1.addObjectToResolvedTimeline(resolvedTimeline, o); | ||
// Add children: | ||
@@ -96,4 +83,10 @@ if (obj.isGroup && obj.children) { | ||
}; | ||
/** Calculate the state for all points in time. */ | ||
Resolver.resolveAllStates = function (resolvedTimeline) { | ||
return state_1.resolveStates(resolvedTimeline); | ||
}; | ||
/** | ||
* Calculate the state at a given point in time. Using a ResolvedTimeline calculated by Resolver.resolveTimeline. | ||
* Calculate the state at a given point in time. | ||
* Using a ResolvedTimeline calculated by Resolver.resolveTimeline() or | ||
* a ResolvedStates calculated by Resolver.resolveAllStates() | ||
* @param resolved ResolvedTimeline calculated by Resolver.resolveTimeline. | ||
@@ -183,3 +176,3 @@ * @param time The point in time where to calculate the state | ||
value: true, | ||
data: { instance: instance, id: 'a' + iStart_1++ }, | ||
data: { instance: instance, id: obj.id + '_' + iStart_1++ }, | ||
references: instance.references | ||
@@ -193,3 +186,3 @@ }); | ||
value: true, | ||
data: { instance: { id: lib_1.getId(), start: lookedupStarts.value, end: null, references: lookedupStarts.references }, id: 'a' + iStart_1++ }, | ||
data: { instance: { id: lib_1.getId(), start: lookedupStarts.value, end: null, references: lookedupStarts.references }, id: obj.id + '_' + iStart_1++ }, | ||
references: lookedupStarts.references | ||
@@ -212,3 +205,3 @@ }); | ||
value: false, | ||
data: { instance: instance, id: 'a' + iEnd_1++ }, | ||
data: { instance: instance, id: obj.id + '_' + iEnd_1++ }, | ||
references: instance.references | ||
@@ -222,3 +215,3 @@ }); | ||
value: false, | ||
data: { instance: { id: lib_1.getId(), start: lookedupEnds.value, end: null, references: lookedupEnds.references }, id: 'a' + iEnd_1++ }, | ||
data: { instance: { id: lib_1.getId(), start: lookedupEnds.value, end: null, references: lookedupEnds.references }, id: obj.id + '_' + iEnd_1++ }, | ||
references: lookedupEnds.references | ||
@@ -350,9 +343,6 @@ }); | ||
// Match id, example: "#objectId.start" | ||
var m = expr.match(/^(!)?\W*#([^.]+)(.*)/); | ||
var m = expr.match(/^\W*#([^.]+)(.*)/); | ||
if (m) { | ||
var exclamation = m[1]; | ||
var id = m[2]; | ||
rest = m[3]; | ||
if (exclamation === '!') | ||
invert = !invert; | ||
var id = m[1]; | ||
rest = m[2]; | ||
var obj_1 = resolvedTimeline.objects[id]; | ||
@@ -365,9 +355,6 @@ if (obj_1) { | ||
// Match class, example: ".className.start" | ||
var m_1 = expr.match(/^(!)?\W*\.([^.]+)(.*)/); | ||
var m_1 = expr.match(/^\W*\.([^.]+)(.*)/); | ||
if (m_1) { | ||
var exclamation = m_1[1]; | ||
var className = m_1[2]; | ||
rest = m_1[3]; | ||
if (exclamation === '!') | ||
invert = !invert; | ||
var className = m_1[1]; | ||
rest = m_1[2]; | ||
var objIds = resolvedTimeline.classes[className] || []; | ||
@@ -383,9 +370,6 @@ _.each(objIds, function (objId) { | ||
// Match layer, example: "$layer" | ||
var m_2 = expr.match(/^(!)?\W*\$([^.]+)(.*)/); | ||
var m_2 = expr.match(/^\W*\$([^.]+)(.*)/); | ||
if (m_2) { | ||
var exclamation = m_2[1]; | ||
var layer = m_2[2]; | ||
rest = m_2[3]; | ||
if (exclamation === '!') | ||
invert = !invert; | ||
var layer = m_2[1]; | ||
rest = m_2[2]; | ||
var objIds = resolvedTimeline.layers[layer] || []; | ||
@@ -483,110 +467,122 @@ _.each(objIds, function (objId) { | ||
}; | ||
if (_.isNull(lookupExpr.l) || | ||
_.isNull(lookupExpr.r)) { | ||
return null; | ||
if (lookupExpr.o === '!') { | ||
// Discard l, invert and return r: | ||
if (lookupExpr.r && _.isArray(lookupExpr.r) && lookupExpr.r.length) { | ||
return lib_1.invertInstances(lookupExpr.r); | ||
} | ||
else { | ||
// We can't invert a value | ||
return lookupExpr.r; | ||
} | ||
} | ||
if (lookupExpr.o === '&' || | ||
lookupExpr.o === '|') { | ||
var events_2 = []; | ||
var addEvents = function (instances, left) { | ||
_.each(instances, function (instance) { | ||
events_2.push({ | ||
left: left, | ||
time: instance.start, | ||
value: true, | ||
references: [], | ||
data: true, | ||
instance: instance | ||
}); | ||
if (instance.end !== null) { | ||
else { | ||
if (_.isNull(lookupExpr.l) || | ||
_.isNull(lookupExpr.r)) { | ||
return null; | ||
} | ||
if (lookupExpr.o === '&' || | ||
lookupExpr.o === '|') { | ||
var events_2 = []; | ||
var addEvents = function (instances, left) { | ||
_.each(instances, function (instance) { | ||
events_2.push({ | ||
left: left, | ||
time: instance.end, | ||
value: false, | ||
time: instance.start, | ||
value: true, | ||
references: [], | ||
data: false, | ||
data: true, | ||
instance: instance | ||
}); | ||
} | ||
}); | ||
}; | ||
if (_.isArray(lookupExpr.l)) | ||
addEvents(lookupExpr.l, true); | ||
if (_.isArray(lookupExpr.r)) | ||
addEvents(lookupExpr.r, false); | ||
events_2 = lib_1.sortEvents(events_2); | ||
var calcResult = (lookupExpr.o === '&' ? | ||
function (left, right) { return !!(left && right); } : | ||
lookupExpr.o === '|' ? | ||
function (left, right) { return !!(left || right); } : | ||
function () { return false; }); | ||
var leftValue = (lib_1.isReference(lookupExpr.l) ? !!lookupExpr.l.value : false); | ||
var rightValue = (lib_1.isReference(lookupExpr.r) ? !!lookupExpr.r.value : false); | ||
var leftInstance = null; | ||
var rightInstance = null; | ||
var resultValue = calcResult(leftValue, rightValue); | ||
var resultReferences = lib_1.joinReferences((lib_1.isReference(lookupExpr.l) ? lookupExpr.l.references : []), (lib_1.isReference(lookupExpr.r) ? lookupExpr.r.references : [])); | ||
var instances_1 = []; | ||
var updateInstance = function (time, value, references, caps) { | ||
if (value) { | ||
instances_1.push({ | ||
id: lib_1.getId(), | ||
start: time, | ||
end: null, | ||
references: references, | ||
caps: caps | ||
if (instance.end !== null) { | ||
events_2.push({ | ||
left: left, | ||
time: instance.end, | ||
value: false, | ||
references: [], | ||
data: false, | ||
instance: instance | ||
}); | ||
} | ||
}); | ||
} | ||
else { | ||
var last = _.last(instances_1); | ||
if (last) { | ||
last.end = time; | ||
// don't update reference on end | ||
}; | ||
if (_.isArray(lookupExpr.l)) | ||
addEvents(lookupExpr.l, true); | ||
if (_.isArray(lookupExpr.r)) | ||
addEvents(lookupExpr.r, false); | ||
events_2 = lib_1.sortEvents(events_2); | ||
var calcResult = (lookupExpr.o === '&' ? | ||
function (left, right) { return !!(left && right); } : | ||
lookupExpr.o === '|' ? | ||
function (left, right) { return !!(left || right); } : | ||
function () { return false; }); | ||
var leftValue = (lib_1.isReference(lookupExpr.l) ? !!lookupExpr.l.value : false); | ||
var rightValue = (lib_1.isReference(lookupExpr.r) ? !!lookupExpr.r.value : false); | ||
var leftInstance = null; | ||
var rightInstance = null; | ||
var resultValue = calcResult(leftValue, rightValue); | ||
var resultReferences = lib_1.joinReferences((lib_1.isReference(lookupExpr.l) ? lookupExpr.l.references : []), (lib_1.isReference(lookupExpr.r) ? lookupExpr.r.references : [])); | ||
var instances_1 = []; | ||
var updateInstance = function (time, value, references, caps) { | ||
if (value) { | ||
instances_1.push({ | ||
id: lib_1.getId(), | ||
start: time, | ||
end: null, | ||
references: references, | ||
caps: caps | ||
}); | ||
} | ||
} | ||
}; | ||
updateInstance(0, resultValue, resultReferences, []); | ||
for (var i = 0; i < events_2.length; i++) { | ||
var e = events_2[i]; | ||
var next = events_2[i + 1]; | ||
if (e.left) { | ||
leftValue = e.value; | ||
leftInstance = e.instance; | ||
} | ||
else { | ||
rightValue = e.value; | ||
rightInstance = e.instance; | ||
} | ||
if (!next || next.time !== e.time) { | ||
var newResultValue = calcResult(leftValue, rightValue); | ||
var resultReferences_1 = lib_1.joinReferences(leftInstance ? leftInstance.references : [], rightInstance ? rightInstance.references : []); | ||
var resultCaps = ((leftInstance ? leftInstance.caps || [] : []).concat(rightInstance ? rightInstance.caps || [] : [])); | ||
if (newResultValue !== resultValue) { | ||
updateInstance(e.time, newResultValue, resultReferences_1, resultCaps); | ||
resultValue = newResultValue; | ||
else { | ||
var last = _.last(instances_1); | ||
if (last) { | ||
last.end = time; | ||
// don't update reference on end | ||
} | ||
} | ||
}; | ||
updateInstance(0, resultValue, resultReferences, []); | ||
for (var i = 0; i < events_2.length; i++) { | ||
var e = events_2[i]; | ||
var next = events_2[i + 1]; | ||
if (e.left) { | ||
leftValue = e.value; | ||
leftInstance = e.instance; | ||
} | ||
else { | ||
rightValue = e.value; | ||
rightInstance = e.instance; | ||
} | ||
if (!next || next.time !== e.time) { | ||
var newResultValue = calcResult(leftValue, rightValue); | ||
var resultReferences_1 = lib_1.joinReferences(leftInstance ? leftInstance.references : [], rightInstance ? rightInstance.references : []); | ||
var resultCaps = ((leftInstance ? leftInstance.caps || [] : []).concat(rightInstance ? rightInstance.caps || [] : [])); | ||
if (newResultValue !== resultValue) { | ||
updateInstance(e.time, newResultValue, resultReferences_1, resultCaps); | ||
resultValue = newResultValue; | ||
} | ||
} | ||
} | ||
return instances_1; | ||
} | ||
return instances_1; | ||
else { | ||
var operateInner_1 = (lookupExpr.o === '+' ? | ||
function (a, b) { return { value: a.value + b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '-' ? | ||
function (a, b) { return { value: a.value - b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '*' ? | ||
function (a, b) { return { value: a.value * b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '/' ? | ||
function (a, b) { return { value: a.value / b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '%' ? | ||
function (a, b) { return { value: a.value % b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
function () { return null; }); | ||
var operate = function (a, b) { | ||
if (a === null || b === null) | ||
return null; | ||
return operateInner_1(a, b); | ||
}; | ||
var result = lib_1.operateOnArrays(lookupExpr.l, lookupExpr.r, operate); | ||
return result; | ||
} | ||
} | ||
else { | ||
var operateInner_1 = (lookupExpr.o === '+' ? | ||
function (a, b) { return { value: a.value + b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '-' ? | ||
function (a, b) { return { value: a.value - b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '*' ? | ||
function (a, b) { return { value: a.value * b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '/' ? | ||
function (a, b) { return { value: a.value / b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
lookupExpr.o === '%' ? | ||
function (a, b) { return { value: a.value % b.value, references: lib_1.joinReferences(a.references, b.references) }; } : | ||
function () { return null; }); | ||
var operate = function (a, b) { | ||
if (a === null || b === null) | ||
return null; | ||
return operateInner_1(a, b); | ||
}; | ||
var result = lib_1.operateOnArrays(lookupExpr.l, lookupExpr.r, operate); | ||
return result; | ||
} | ||
} | ||
@@ -593,0 +589,0 @@ } |
@@ -1,3 +0,4 @@ | ||
import { TimelineState, ResolvedTimeline, Time, Content } from '../api/api'; | ||
export declare function getState(resolved: ResolvedTimeline, time: Time, eventLimit?: number): TimelineState; | ||
import { TimelineState, ResolvedTimeline, Time, Content, ResolvedStates } from '../api/api'; | ||
export declare function getState(resolved: ResolvedTimeline | ResolvedStates, time: Time, eventLimit?: number): TimelineState; | ||
export declare function resolveStates(resolved: ResolvedTimeline, onlyForTime?: Time): ResolvedStates; | ||
export declare function applyKeyframeContent(parentContent: Content, keyframeContent: Content): void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var tslib_1 = require("tslib"); | ||
var _ = require("underscore"); | ||
var common_1 = require("./common"); | ||
var enums_1 = require("../api/enums"); | ||
var lib_1 = require("../lib"); | ||
function getState(resolved, time, eventLimit) { | ||
if (eventLimit === void 0) { eventLimit = 0; } | ||
var resolvedStates = (isResolvedStates(resolved) ? | ||
resolved : | ||
resolveStates(resolved, time)); | ||
var state = { | ||
time: time, | ||
layers: {}, | ||
nextEvents: _.filter(resolvedStates.nextEvents, function (e) { return e.time > time; }) | ||
}; | ||
if (eventLimit) | ||
state.nextEvents = state.nextEvents.slice(0, eventLimit); | ||
_.each(_.keys(resolvedStates.layers), function (layer) { | ||
var o = getStateAtTime(resolvedStates.state, layer, time); | ||
if (o) | ||
state.layers[layer] = o; | ||
}); | ||
return state; | ||
} | ||
exports.getState = getState; | ||
function resolveStates(resolved, onlyForTime) { | ||
var resolvedStates = { | ||
options: resolved.options, | ||
statistics: resolved.statistics, | ||
// These will be re-created during the state-resolving: | ||
objects: {}, | ||
classes: {}, | ||
layers: {}, | ||
state: {}, | ||
nextEvents: [] | ||
}; | ||
if (!resolved.objects) | ||
throw new Error('getState: input data missing .objects attribute'); | ||
var resolvedObjects = _.values(resolved.objects); | ||
@@ -22,5 +45,11 @@ // Sort to make sure parent groups are evaluated before their children: | ||
return -1; | ||
if (a.id > a.id) | ||
return 1; | ||
if (a.id < a.id) | ||
return -1; | ||
return 0; | ||
}); | ||
var activeObjIds = {}; | ||
// Step 1: Collect all points-of-interest (which points in time we want to evaluate) | ||
// and which instances that are interesting | ||
var pointsInTime = {}; | ||
var eventObjectTimes = {}; | ||
@@ -31,72 +60,35 @@ _.each(resolvedObjects, function (obj) { | ||
!obj.resolved.isKeyframe) { | ||
_.each(obj.resolved.instances, function (instance) { | ||
if (obj.layer) { // if layer is empty, don't put in state | ||
if ( | ||
// object instance is active: | ||
(instance.end === null || | ||
instance.end > time) && | ||
instance.start <= time) { | ||
var parentObj = (obj.resolved.parentId ? | ||
resolved.objects[obj.resolved.parentId] : | ||
null); | ||
// If object has a parent, only set if parent is on layer (if layer is set for parent) | ||
if (!obj.resolved.parentId || | ||
(parentObj && | ||
(!parentObj.layer || | ||
activeObjIds[parentObj.id]))) { | ||
var clone = lib_1.extendMandadory(_.clone(obj), { | ||
instance: _.clone(instance) | ||
}); | ||
clone.content = JSON.parse(JSON.stringify(clone.content)); | ||
var setObj = false; | ||
var existingObj = state.layers[obj.layer]; | ||
if (!existingObj) { | ||
setObj = true; | ||
var parentTimes_1 = getTimesFromParents(resolved, obj); | ||
if (obj.layer) { // if layer is empty, don't put in state | ||
_.each(obj.resolved.instances, function (instance) { | ||
var useInstance = true; | ||
if (onlyForTime) { | ||
useInstance = ((instance.start || 0) <= onlyForTime && | ||
(instance.end || Infinity) > onlyForTime); | ||
} | ||
if (useInstance) { | ||
var timeEvents_1 = []; | ||
timeEvents_1.push({ time: instance.start, enable: true }); | ||
if (instance.end) | ||
timeEvents_1.push({ time: instance.end, enable: false }); | ||
// Also include times from parents, as they could affect the state of this instance: | ||
_.each(parentTimes_1, function (parentTime) { | ||
if (parentTime && (parentTime.time > (instance.start || 0) && | ||
parentTime.time < (instance.end || Infinity))) { | ||
timeEvents_1.push(parentTime); | ||
} | ||
else { | ||
// Priority: | ||
if (((obj.priority || 0) > (existingObj.priority || 0) // obj has higher priority => replaces existingObj | ||
) || ((obj.priority || 0) === (existingObj.priority || 0) && | ||
(instance.start || 0) > (existingObj.instance.start || 0) // obj starts later => replaces existingObj | ||
)) { | ||
setObj = true; | ||
} | ||
} | ||
if (setObj) { | ||
if (existingObj) { | ||
// replace object on layer: | ||
delete activeObjIds[existingObj.id]; | ||
} | ||
state.layers[obj.layer] = clone; | ||
activeObjIds[clone.id] = clone; | ||
} | ||
} | ||
} | ||
if (instance.start > time) { | ||
state.nextEvents.push({ | ||
type: enums_1.EventType.START, | ||
time: instance.start, | ||
objId: obj.id | ||
}); | ||
eventObjectTimes['' + instance.start] = enums_1.EventType.START; | ||
} | ||
if (instance.end !== null && | ||
instance.end > time) { | ||
state.nextEvents.push({ | ||
type: enums_1.EventType.END, | ||
time: instance.end, | ||
objId: obj.id | ||
// Save a reference to this instance on all points in time that could affect it: | ||
_.each(timeEvents_1, function (timeEvent) { | ||
if (!pointsInTime[timeEvent.time + '']) | ||
pointsInTime[timeEvent.time + ''] = []; | ||
pointsInTime[timeEvent.time + ''].push({ obj: obj, instance: instance, enable: timeEvent.enable }); | ||
}); | ||
eventObjectTimes['' + instance.end] = enums_1.EventType.END; | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
}); | ||
// _.each(state.layers, (obj) => { | ||
// activeObjIds[obj.id] = obj | ||
// }) | ||
// Keyframes: | ||
var keyframes = []; | ||
_.each(resolved.objects, function (obj) { | ||
// Also add keyframes to pointsInTime: | ||
_.each(resolvedObjects, function (obj) { | ||
if (!obj.disabled && | ||
@@ -107,62 +99,222 @@ obj.resolved.resolved && | ||
_.each(obj.resolved.instances, function (instance) { | ||
var kf = lib_1.extendMandadory(obj, { | ||
instance: instance | ||
}); | ||
keyframes.push(kf); | ||
if (instance.start > time && | ||
// tslint:disable-next-line | ||
eventObjectTimes['' + instance.start] === undefined // no need to put a keyframe event if there's already another event | ||
) { | ||
state.nextEvents.push({ | ||
type: enums_1.EventType.KEYFRAME, | ||
time: instance.start, | ||
objId: obj.id | ||
}); | ||
eventObjectTimes['' + instance.start] = enums_1.EventType.KEYFRAME; | ||
var timeEvents = []; | ||
if (instance.start) { | ||
timeEvents.push({ time: instance.start, enable: true }); | ||
} | ||
if (instance.end !== null && | ||
instance.end > time && | ||
// tslint:disable-next-line | ||
eventObjectTimes['' + instance.end] === undefined // no need to put a keyframe event if there's already another event | ||
) { | ||
state.nextEvents.push({ | ||
type: enums_1.EventType.KEYFRAME, | ||
time: instance.end, | ||
objId: obj.id | ||
}); | ||
eventObjectTimes['' + instance.end] = enums_1.EventType.KEYFRAME; | ||
else { | ||
timeEvents.push({ time: instance.start, enable: true }); | ||
} | ||
_.each(timeEvents, function (timeEvent) { | ||
if (!pointsInTime[timeEvent.time + '']) | ||
pointsInTime[timeEvent.time + ''] = []; | ||
pointsInTime[timeEvent.time + ''].push({ obj: obj, instance: instance, enable: timeEvent.enable }); | ||
}); | ||
}); | ||
} | ||
}); | ||
keyframes.sort(function (a, b) { | ||
if (a.instance.start > b.instance.start) | ||
return 1; | ||
if (a.instance.start < b.instance.start) | ||
return -1; | ||
if (a.id > b.id) | ||
return -1; | ||
if (a.id < b.id) | ||
return 1; | ||
return 0; | ||
// Step 2: Resolve the state for the points-of-interest | ||
// This is done by sweeping the points-of-interest chronologically, | ||
// determining the state for every point in time by adding & removing objects from aspiringInstances | ||
// Then sorting it to determine who takes precedence | ||
var currentState = {}; | ||
var activeObjIds = {}; | ||
/** The objects in aspiringInstances */ | ||
var aspiringInstances = {}; | ||
var keyframeEvents = []; | ||
var times = _.map(_.keys(pointsInTime), function (time) { return parseFloat(time); }); | ||
// Sort chronologically: | ||
times.sort(function (a, b) { | ||
return a - b; | ||
}); | ||
_.each(keyframes, function (keyframe) { | ||
if (keyframe.resolved.parentId) { | ||
var parentObj = activeObjIds[keyframe.resolved.parentId]; | ||
if (parentObj) { // keyframe is on an active object | ||
if ( | ||
// keyframe instance is active: | ||
(keyframe.instance.end === null || | ||
keyframe.instance.end > time) && | ||
keyframe.instance.start <= time && | ||
// keyframe is within the keyframe.instance of its parent: | ||
keyframe.instance.start >= parentObj.instance.start && | ||
(parentObj.instance.end === null || | ||
keyframe.instance.start < parentObj.instance.end)) { | ||
applyKeyframeContent(parentObj.content, keyframe.content); | ||
_.each(times, function (time) { | ||
var instancesToCheck = pointsInTime[time]; | ||
var checkedObjectsThisTime = {}; | ||
instancesToCheck.sort(function (a, b) { | ||
if (a.obj.resolved && b.obj.resolved) { | ||
// Keyframes comes last: | ||
if (a.obj.resolved.isKeyframe && !b.obj.resolved.isKeyframe) | ||
return 1; | ||
if (!a.obj.resolved.isKeyframe && b.obj.resolved.isKeyframe) | ||
return -1; | ||
// Ending events come before starting events: | ||
if (a.enable && !b.enable) | ||
return 1; | ||
if (!a.enable && b.enable) | ||
return -1; | ||
// Deeper objects (children in groups) comes later, we want to check the parent groups first: | ||
if ((a.obj.resolved.levelDeep || 0) > (b.obj.resolved.levelDeep || 0)) | ||
return 1; | ||
if ((a.obj.resolved.levelDeep || 0) < (b.obj.resolved.levelDeep || 0)) | ||
return -1; | ||
} | ||
return 0; | ||
}); | ||
_.each(instancesToCheck, function (o) { | ||
var obj = o.obj; | ||
var instance = o.instance; | ||
var toBeEnabled = ((instance.start || 0) <= time && | ||
(instance.end || Infinity) > time); | ||
var layer = obj.layer + ''; | ||
if (!checkedObjectsThisTime[obj.id + '_' + instance.id + '_' + o.enable]) { // Only check each object and event-type once for every point in time | ||
checkedObjectsThisTime[obj.id + '_' + instance.id + '_' + o.enable] = true; | ||
if (!obj.resolved.isKeyframe) { | ||
// If object has a parent, only set if parent is on a layer (if layer is set for parent) | ||
if (toBeEnabled && obj.resolved.parentId) { | ||
var parentObj = (obj.resolved.parentId ? | ||
resolved.objects[obj.resolved.parentId] : | ||
null); | ||
toBeEnabled = !!(parentObj && | ||
(!parentObj.layer || | ||
activeObjIds[parentObj.id])); | ||
} | ||
if (!aspiringInstances[obj.layer]) | ||
aspiringInstances[obj.layer] = []; | ||
if (toBeEnabled) { | ||
// The instance wants to be enabled (is starting) | ||
// Add to aspiringInstances: | ||
aspiringInstances[obj.layer].push({ obj: obj, instance: instance }); | ||
} | ||
else { | ||
// The instance doesn't want to be enabled (is ending) | ||
// Remove from aspiringInstances: | ||
aspiringInstances[layer] = _.reject(aspiringInstances[layer] || [], function (o) { return o.obj.id === obj.id; }); | ||
} | ||
// Evaluate the layer to determine who has the throne: | ||
aspiringInstances[layer].sort(function (a, b) { | ||
// Determine who takes precedence: | ||
// First, sort using priority | ||
if ((a.obj.priority || 0) < (b.obj.priority || 0)) | ||
return 1; | ||
if ((a.obj.priority || 0) > (b.obj.priority || 0)) | ||
return -1; | ||
// Then, sort using the start time | ||
if ((a.instance.start || 0) < (b.instance.start || 0)) | ||
return 1; | ||
if ((a.instance.start || 0) > (b.instance.start || 0)) | ||
return -1; | ||
// Last resort: sort using id: | ||
if (a.obj.id > b.obj.id) | ||
return 1; | ||
if (a.obj.id < b.obj.id) | ||
return -1; | ||
return 0; | ||
}); | ||
// Now, the one on top has the throne | ||
// Update current state: | ||
var currentOnTopOfLayer = aspiringInstances[layer][0]; | ||
var prevObj = currentState[layer]; | ||
var replaceOldObj = (currentOnTopOfLayer && | ||
(!prevObj || | ||
prevObj.id !== currentOnTopOfLayer.obj.id || | ||
prevObj.instance.id !== currentOnTopOfLayer.instance.id)); | ||
var removeOldObj = (!currentOnTopOfLayer && | ||
prevObj); | ||
if (replaceOldObj || removeOldObj) { | ||
if (prevObj) { | ||
// Cap the old instance, so it'll end at this point in time: | ||
prevObj.instance.end = time; | ||
// Update activeObjIds: | ||
delete activeObjIds[prevObj.id]; | ||
// Add to nextEvents: | ||
if (!onlyForTime || | ||
prevObj.instance.end > onlyForTime) { | ||
resolvedStates.nextEvents.push({ | ||
type: enums_1.EventType.END, | ||
time: prevObj.instance.end, | ||
objId: prevObj.id | ||
}); | ||
eventObjectTimes[instance.end + ''] = enums_1.EventType.END; | ||
} | ||
} | ||
} | ||
if (replaceOldObj) { | ||
// Set the new object to State | ||
// Construct a new object clone: | ||
var newObj = void 0; | ||
if (resolvedStates.objects[currentOnTopOfLayer.obj.id]) { | ||
// Use the already existing one | ||
newObj = resolvedStates.objects[currentOnTopOfLayer.obj.id]; | ||
} | ||
else { | ||
newObj = _.clone(currentOnTopOfLayer.obj); | ||
newObj.content = JSON.parse(JSON.stringify(newObj.content)); | ||
newObj.resolved = tslib_1.__assign({}, newObj.resolved || {}, { instances: [] }); | ||
common_1.addObjectToResolvedTimeline(resolvedStates, newObj); | ||
} | ||
var newInstance = tslib_1.__assign({}, currentOnTopOfLayer.instance, { | ||
// We're setting new start & end times so they match up with the state: | ||
start: time, end: null }); | ||
newObj.resolved.instances.push(newInstance); | ||
var newObjInstance = tslib_1.__assign({}, newObj, { instance: newInstance }); | ||
// Save to current state: | ||
currentState[layer] = newObjInstance; | ||
// Update activeObjIds: | ||
activeObjIds[newObjInstance.id] = newObjInstance; | ||
// Update the tracking state as well: | ||
setStateAtTime(resolvedStates.state, layer, time, newObjInstance); | ||
// Add to nextEvents: | ||
if (newInstance.start > (onlyForTime || 0)) { | ||
resolvedStates.nextEvents.push({ | ||
type: enums_1.EventType.START, | ||
time: newInstance.start, | ||
objId: obj.id | ||
}); | ||
eventObjectTimes[newInstance.start + ''] = enums_1.EventType.START; | ||
} | ||
} | ||
else if (removeOldObj) { | ||
// Remove from current state: | ||
delete currentState[layer]; | ||
// Update the tracking state as well: | ||
setStateAtTime(resolvedStates.state, layer, time, null); | ||
} | ||
} | ||
else { | ||
// Is a keyframe | ||
var keyframe = obj; | ||
// Add keyframe to resolvedStates.objects: | ||
resolvedStates.objects[keyframe.id] = keyframe; | ||
// Check if the keyframe's parent is currently active? | ||
if (keyframe.resolved.parentId) { | ||
var parentObj = activeObjIds[keyframe.resolved.parentId]; | ||
if (parentObj && parentObj.layer) { // keyframe is on an active object | ||
var parentObjInstance = currentState[parentObj.layer]; | ||
if (parentObjInstance) { | ||
var keyframeInstance = tslib_1.__assign({}, keyframe, { instance: instance, isKeyframe: true, keyframeEndTime: instance.end }); | ||
// Note: The keyframes are a little bit special, since their contents are applied to their parents. | ||
// That application is done in the getStateAtTime function. | ||
// Add keyframe to the tracking state: | ||
addKeyframeAtTime(resolvedStates.state, parentObj.layer + '', time, keyframeInstance); | ||
// Add keyframe to nextEvents: | ||
keyframeEvents.push({ | ||
type: enums_1.EventType.KEYFRAME, | ||
time: instance.start, | ||
objId: keyframe.id | ||
}); | ||
if (instance.end !== null) { | ||
keyframeEvents.push({ | ||
type: enums_1.EventType.KEYFRAME, | ||
time: instance.end, | ||
objId: keyframe.id | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
// Go through the keyframe events and add them to nextEvents: | ||
_.each(keyframeEvents, function (keyframeEvent) { | ||
// tslint:disable-next-line | ||
if (eventObjectTimes[keyframeEvent.time + ''] === undefined) { // no need to put a keyframe event if there's already another event there | ||
resolvedStates.nextEvents.push(keyframeEvent); | ||
eventObjectTimes[keyframeEvent.time + ''] = enums_1.EventType.KEYFRAME; | ||
} | ||
}); | ||
state.nextEvents.sort(function (a, b) { | ||
if (onlyForTime) { | ||
resolvedStates.nextEvents = _.filter(resolvedStates.nextEvents, function (e) { return e.time > onlyForTime; }); | ||
} | ||
resolvedStates.nextEvents.sort(function (a, b) { | ||
if (a.time > b.time) | ||
@@ -182,7 +334,5 @@ return 1; | ||
}); | ||
if (eventLimit > 0 && state.nextEvents.length > eventLimit) | ||
state.nextEvents.splice(eventLimit); // delete the rest | ||
return state; | ||
return resolvedStates; | ||
} | ||
exports.getState = getState; | ||
exports.resolveStates = resolveStates; | ||
function applyKeyframeContent(parentContent, keyframeContent) { | ||
@@ -208,2 +358,82 @@ _.each(keyframeContent, function (value, attr) { | ||
exports.applyKeyframeContent = applyKeyframeContent; | ||
function getTimesFromParents(resolved, obj) { | ||
var times = []; | ||
var parentObj = (obj.resolved.parentId ? | ||
resolved.objects[obj.resolved.parentId] : | ||
null); | ||
if (parentObj && parentObj.resolved.resolved) { | ||
_.each(parentObj.resolved.instances, function (instance) { | ||
times.push({ time: instance.start, enable: true }); | ||
if (instance.end) | ||
times.push({ time: instance.end, enable: false }); | ||
}); | ||
times = times.concat(getTimesFromParents(resolved, parentObj)); | ||
} | ||
return times; | ||
} | ||
function setStateAtTime(states, layer, time, objInstance) { | ||
if (!states[layer]) | ||
states[layer] = {}; | ||
states[layer][time + ''] = objInstance ? [objInstance] : objInstance; | ||
} | ||
function addKeyframeAtTime(states, layer, time, objInstanceKf) { | ||
if (!states[layer]) | ||
states[layer] = {}; | ||
if (!states[layer][time + '']) | ||
states[layer][time + ''] = []; | ||
// @ts-ignore object is possibly null | ||
states[layer][time + ''].push(objInstanceKf); | ||
} | ||
function getStateAtTime(states, layer, requestTime) { | ||
var layerStates = states[layer] || {}; | ||
var times = _.map(_.keys(layerStates), function (time) { return parseFloat(time); }); | ||
times.sort(function (a, b) { | ||
return a - b; | ||
}); | ||
var state = null; | ||
var isCloned = false; | ||
_.find(times, function (time) { | ||
if (time <= requestTime) { | ||
var currentStateInstances = layerStates[time + '']; | ||
if (currentStateInstances && currentStateInstances.length) { | ||
_.each(currentStateInstances, function (currentState) { | ||
if (currentState && | ||
currentState.isKeyframe) { | ||
var keyframe = currentState; | ||
if (state && keyframe.resolved.parentId === state.id) { | ||
if ((keyframe.keyframeEndTime || Infinity) > requestTime) { | ||
if (!isCloned) { | ||
isCloned = true; | ||
state = tslib_1.__assign({}, state, { content: JSON.parse(JSON.stringify(state.content)) }); | ||
} | ||
// Apply the keyframe on the state: | ||
applyKeyframeContent(state.content, keyframe.content); | ||
} | ||
} | ||
} | ||
else { | ||
state = currentState; | ||
isCloned = false; | ||
} | ||
}); | ||
} | ||
else { | ||
state = null; | ||
isCloned = false; | ||
} | ||
return false; | ||
} | ||
else { | ||
return true; | ||
} | ||
}); | ||
return state; | ||
} | ||
function isResolvedStates(resolved) { | ||
return !!(resolved && | ||
typeof resolved === 'object' && | ||
resolved.objects && | ||
resolved.state && | ||
resolved.nextEvents); | ||
} | ||
//# sourceMappingURL=state.js.map |
{ | ||
"name": "superfly-timeline", | ||
"version": "7.0.2", | ||
"version": "7.1.0", | ||
"description": "A collection of rules as well as a resolver for placing objects on a virtual timeline.", | ||
@@ -100,2 +100,5 @@ "license": "MIT", | ||
}, | ||
"resolutions": { | ||
"typedoc/**/marked": "^0.6.2" | ||
}, | ||
"keywords": [ | ||
@@ -102,0 +105,0 @@ "broadcast", |
# SuperFly-Timeline | ||
[![CircleCI](https://circleci.com/gh/SuperFlyTV/supertimeline.svg?style=svg)](https://circleci.com/gh/SuperFlyTV/supertimeline) | ||
[![CircleCI](https://circleci.com/gh/SuperFlyTV/supertimeline/tree/develop.svg?style=svg)](https://circleci.com/gh/SuperFlyTV/supertimeline/tree/develop) | ||
[![codecov](https://codecov.io/gh/SuperFlyTV/supertimeline/branch/master/graph/badge.svg)](https://codecov.io/gh/SuperFlyTV/supertimeline) | ||
@@ -73,5 +74,7 @@ The SuperFly-Timeline is a collection of rules as well as a resolver for placing objects on a virtual timeline. It uses the concept of timing objects in sequences -– absolute or relative timings -– which resolves recursively in nested structures. This means it supports grouping, combinations of timing between groups, and objects within groups. It also supports logical conditions instead of timed conditions. | ||
// Use the resolved timeline and pre-calculate states, instance collisions, etc. | ||
const resolvedStates = Timeline.Resolver.resolveAllStates(resolvedTimeline); | ||
// Fetch the state at time 10: | ||
const state0 = Timeline.Resolver.getState(resolvedTimeline, 10); | ||
const state0 = Timeline.Resolver.getState(resolvedStates, 10); | ||
console.log(`At the time ${state0.time}, the active objects are "${ | ||
@@ -82,3 +85,3 @@ _.map(state0.layers, (o, l) => `${o.id} at layer ${l}`).join(', ') | ||
// Fetch the state at time 25: | ||
const state1 = Timeline.Resolver.getState(resolvedTimeline, 25); | ||
const state1 = Timeline.Resolver.getState(resolvedStates, 25); | ||
console.log(`At the time ${state1.time}, the active objects are "${ | ||
@@ -281,3 +284,2 @@ _.map(state1.layers, (o, l) => `${o.id} at layer ${l}`).join(', ') | ||
} | ||
*/ | ||
``` | ||
@@ -284,0 +286,0 @@ [Try it in JSFiddle!](https://jsfiddle.net/nytamin/ydznup0k/) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
216536
32
2131
287