diff --git a/src/__tests__/legacy.spec.ts b/src/__tests__/legacy.spec.ts index a499a157..c3572034 100644 --- a/src/__tests__/legacy.spec.ts +++ b/src/__tests__/legacy.spec.ts @@ -1,21 +1,42 @@ +/** + * Note: The tests in this file where originally created for version 1, but has been converted to apply for version 2 + */ import { TriggerType, EventType, TraceLevel } from '../api/legacyEnums' import { TimelineResolvedObject, TimelineState, DevelopedTimeline, ExternalFunctions, - TimelineObject + TimelineObject, + TimelineKeyframe } from '../api/legacy' import { - Resolver + ResolvedTimelineObject as NewResolvedTimelineObject, + TimelineState as NewTimelineState, + // DevelopedTimeline as NewDevelopedTimeline, + // ExternalFunctions as NewExternalFunctions, + TimelineObject as NewTimelineObject, + TimelineKeyframe as NewTimelineKeyframe, + ResolveOptions +} from '../api/api' +import { + Resolver as LegacyResolver } from '../resolver/legacy' +import { + Resolver +} from '../resolver/resolver' import * as _ from 'underscore' // let assert = require('assert') -const clone = require('fast-clone') +const clone0 = require('fast-clone') +function clone (o: T): T { + return clone0(o) +} const now = 1000 -/* -const testData = { + +const testDataOld: { + [dataset: string]: Array +} = { 'basic': [ { id: 'obj0', // the id must be unique @@ -26,7 +47,8 @@ const testData = { }, duration: 60, // 1 minute long LLayer: 1, - classes: ['obj0Class'] + classes: ['obj0Class', 'L1'], + content: {} }, { id: 'obj1', // the id must be unique @@ -36,7 +58,9 @@ const testData = { value: '#obj0.end' }, duration: 60, // 1 minute long - LLayer: 1 + LLayer: 1, + content: {}, + classes: ['L1'] } ], 'basic2': [ @@ -223,7 +247,7 @@ const testData = { }, trigger: { type: TriggerType.TIME_RELATIVE, - value: '#obj0 + (9 * 2)' + value: '#obj0.end + (9 * 2)' }, LLayer: 10, // Logical layer @@ -402,7 +426,7 @@ const testData = { duration: 5, trigger: { type: TriggerType.TIME_ABSOLUTE, - value: '1' // relative to parent start time + value: '1' // relative to parent start time // 966 }, content: { mixer: { @@ -416,7 +440,7 @@ const testData = { duration: 5, trigger: { type: TriggerType.TIME_RELATIVE, - value: '#K0.start + 1' // relative to parent start time + value: '#K0.start + 1' // 967 }, content: { mixer: { @@ -429,7 +453,7 @@ const testData = { duration: 5, trigger: { type: TriggerType.TIME_RELATIVE, - value: '#obj3.start - 1' + value: '#obj3.start - 1' // 986 }, content: { mixer: { @@ -443,7 +467,7 @@ const testData = { }, trigger: { type: TriggerType.TIME_RELATIVE, - value: '#obj0.start + 15' + value: '#obj0.start + 15' // 965 }, LLayer: 10, // Logical layer @@ -462,7 +486,7 @@ const testData = { }, trigger: { type: TriggerType.TIME_RELATIVE, - value: '#obj2.start + 20' + value: '#obj2.start + 20' // 985 }, LLayer: 10, // Logical layer @@ -549,7 +573,7 @@ const testData = { duration: 50, trigger: { type: TriggerType.LOGICAL, - value: '$L1' // LLayer 1 + value: '.L1' }, LLayer: 2, content: { @@ -561,7 +585,7 @@ const testData = { duration: 50, trigger: { type: TriggerType.LOGICAL, - value: '!$L1' // LLayer 1 + value: '!.L1' }, LLayer: 3, content: { @@ -575,7 +599,7 @@ const testData = { duration: 50, trigger: { type: TriggerType.LOGICAL, - value: '$L1' // LLayer 1 + value: '$1' // LLayer 1 }, LLayer: 2, content: { @@ -587,7 +611,7 @@ const testData = { duration: 50, trigger: { type: TriggerType.LOGICAL, - value: '!$G1' // GLayer 1 + value: '!$1' // GLayer 1 }, LLayer: 3, content: { @@ -603,7 +627,8 @@ const testData = { value: now - 10 // 10 seconds ago }, duration: 0, // infinite - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj1', // the id must be unique @@ -613,7 +638,8 @@ const testData = { value: '#obj0.end' // will essentially never play }, duration: 60, // 1 minute long - LLayer: 1 + LLayer: 1, + content: {} } ], 'simplegroup': [ @@ -637,7 +663,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 15, - LLayer: 2 + LLayer: 2, + content: {} }, { id: 'child1', // the id must be unique @@ -647,7 +674,8 @@ const testData = { value: '#child0.end' }, duration: 10, - LLayer: 2 + LLayer: 2, + content: {} } ] } @@ -660,7 +688,8 @@ const testData = { value: '#group0.end' // will essentially never play }, duration: 60, // 1 minute long - LLayer: 2 + LLayer: 2, + content: {} } ], 'infinitegroup': [ @@ -684,7 +713,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 15, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'child1', // the id must be unique @@ -694,7 +724,8 @@ const testData = { value: '#child0.end' }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -720,7 +751,8 @@ const testData = { type: TriggerType.LOGICAL, value: '1' }, - LLayer: 2 + LLayer: 2, + content: {} } ] } @@ -732,7 +764,8 @@ const testData = { type: TriggerType.LOGICAL, value: '1' }, - LLayer: 3 + LLayer: 3, + content: {} } ], 'logicalInGroupLogical': [ @@ -755,7 +788,8 @@ const testData = { type: TriggerType.LOGICAL, value: '1' }, - LLayer: 2 + LLayer: 2, + content: {} } ] } @@ -779,7 +813,8 @@ const testData = { type: TriggerType.LOGICAL, value: '1' }, - LLayer: 4 + LLayer: 4, + content: {} } ] } @@ -791,12 +826,14 @@ const testData = { trigger: { type: TriggerType.TIME_ABSOLUTE, - value: now - 10 // 10 seconds ago + value: now - 10 // 10 seconds ago // 990 }, duration: 63, // 63 seconds - LLayer: 1, + LLayer: '0', isGroup: true, repeating: true, + // @ts-ignore + legacyRepeatingTime: '25', // to work with the breaking change to repeating content: { objects: [ { @@ -804,20 +841,22 @@ const testData = { trigger: { type: TriggerType.TIME_ABSOLUTE, - value: 0 // Relative to parent object + value: 0 // Relative to parent object // 990 }, duration: 15, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'child1', // the id must be unique trigger: { type: TriggerType.TIME_RELATIVE, - value: '#child0.end' + value: '#child0.end' // 1005 }, - duration: 10, - LLayer: 1 + duration: 10, // 1015 + LLayer: 1, + content: {} } ] } @@ -827,10 +866,11 @@ const testData = { trigger: { type: TriggerType.TIME_RELATIVE, - value: '#group0.end' // will essentially never play + value: '#group0.end' }, - duration: 17, - LLayer: 1 + duration: 6, + LLayer: 1, + content: {} } ], 'repeatinggroupinrepeatinggroup': [ // repeating group in repeating group @@ -842,6 +882,10 @@ const testData = { value: now }, duration: 300, // 5 minutes long + + // @ts-ignore + legacyRepeatingTime: '92', // to work with the breaking change to repeating + LLayer: 1, isGroup: true, repeating: true, @@ -855,7 +899,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 30, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'group1', // the id must be unique @@ -865,6 +910,8 @@ const testData = { value: '#child0.end' }, duration: 62, // 62 seconds + // @ts-ignore + legacyRepeatingTime: '25', // to work with the breaking change to repeating LLayer: 1, isGroup: true, repeating: true, @@ -878,7 +925,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'child2', // the id must be unique @@ -888,7 +936,8 @@ const testData = { value: '#child1.end' }, duration: 15, - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -901,10 +950,11 @@ const testData = { trigger: { type: TriggerType.TIME_RELATIVE, - value: '#group0.end' // will essentially never play + value: '#group0.end' }, duration: 60, // 1 minute long - LLayer: 1 + LLayer: 1, + content: {} } ], 'keyframeingroup': [ @@ -973,7 +1023,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -989,7 +1040,8 @@ const testData = { }, duration: 60, // 60s LLayer: 1, - classes: ['obj0Class'] + classes: ['obj0Class'], + content: {} }, { id: 'obj1', // the id must be unique @@ -999,7 +1051,8 @@ const testData = { value: '#obj0.end' }, duration: '#obj0.duration / 2', // 30s - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj2', // the id must be unique @@ -1009,7 +1062,8 @@ const testData = { value: '#obj1.end' }, duration: '#obj1.end - #obj0.start', // 90s - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj3', // the id must be unique @@ -1019,7 +1073,8 @@ const testData = { value: '#obj2.end' }, duration: '5400 - #.start', // so it ends at 5400 (#.start = my own start time) - LLayer: 1 + LLayer: 1, + content: {} } ], 'relativedurationorder0': [ @@ -1044,7 +1099,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -1070,7 +1126,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 2 + LLayer: 2, + content: {} } ] } @@ -1086,7 +1143,8 @@ const testData = { }, duration: '0', LLayer: 1, - classes: ['obj0Class'] + classes: ['obj0Class'], + content: {} }, { id: 'obj1', // the id must be unique @@ -1096,7 +1154,8 @@ const testData = { value: '#obj0.end' }, duration: '0', - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj2', // the id must be unique @@ -1106,7 +1165,8 @@ const testData = { value: '#obj1.end' }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} } ], 'circulardependency1': [ @@ -1119,7 +1179,8 @@ const testData = { }, duration: '#obj2.end', // 60s LLayer: 1, - classes: ['obj0Class'] + classes: ['obj0Class'], + content: {} }, { id: 'obj1', // the id must be unique @@ -1129,7 +1190,8 @@ const testData = { value: '#obj0.end' }, duration: '#obj0.duration / 2', // 30s - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj2', // the id must be unique @@ -1139,7 +1201,8 @@ const testData = { value: '#obj1.end' }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} } ], 'dependenciesBetweengroupchildren': [ @@ -1164,7 +1227,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'child1', // the id must be unique @@ -1174,7 +1238,8 @@ const testData = { value: '#child0.end' }, duration: 20, - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -1200,7 +1265,8 @@ const testData = { value: 0 // Relative to parent object }, duration: '#child0.duration + #child1.duration', - LLayer: 1 + LLayer: 1, + content: {} } ] } @@ -1214,7 +1280,8 @@ const testData = { value: now }, duration: 10, - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj1', @@ -1223,7 +1290,8 @@ const testData = { value: '#obj0.start + 5' }, duration: '#obj0.end - #.start', // make it end at the same time as obj0 - LLayer: 1 + LLayer: 1, + content: {} } ], 'relativeStartOrder0': [ @@ -1248,7 +1316,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 3 + LLayer: 3, + content: {} } ] } @@ -1274,7 +1343,8 @@ const testData = { value: '#trans0.start + 1500' }, duration: 0, - LLayer: 3 + LLayer: 3, + content: {} } ] } @@ -1313,7 +1383,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 4 + LLayer: 4, + content: {} } ] } @@ -1337,7 +1408,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 6 + LLayer: 6, + content: {} } ] } @@ -1377,7 +1449,8 @@ const testData = { value: 0 }, duration: 0, - LLayer: 8 + LLayer: 8, + content: {} } ] } @@ -1419,7 +1492,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 4 + LLayer: 4, + content: {} } ] } @@ -1459,7 +1533,8 @@ const testData = { value: 0 }, duration: 0, - LLayer: 8 + LLayer: 8, + content: {} } ] } @@ -1501,7 +1576,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 3 + LLayer: 3, + content: {} } ] } @@ -1541,7 +1617,8 @@ const testData = { value: 0 }, duration: 0, - LLayer: 3 + LLayer: 3, + content: {} } ] } @@ -1558,7 +1635,8 @@ const testData = { value: now + 3000 }, duration: 0, - LLayer: 0 + LLayer: 0, + content: {} }, { id: 'obj1', @@ -1567,7 +1645,8 @@ const testData = { value: now }, duration: '((#obj0.start - #.start) - (1000 + (2000 / 2)))', - LLayer: 1 + LLayer: 1, + content: {} } ], 'operatorOrder': [ @@ -1578,7 +1657,8 @@ const testData = { value: now + 3000 }, duration: 0, - LLayer: 0 + LLayer: 0, + content: {} }, { id: 'obj1', @@ -1587,7 +1667,8 @@ const testData = { value: now }, duration: '(#obj0.start - #.start) - 2000', - LLayer: 1 + LLayer: 1, + content: {} }, { id: 'obj2', @@ -1596,7 +1677,8 @@ const testData = { value: now }, duration: '#obj0.start - 2000 - #.start', - LLayer: 2 + LLayer: 2, + content: {} }, { id: 'obj3', @@ -1605,7 +1687,8 @@ const testData = { value: now }, duration: '#obj0.start - #.start - 2000', - LLayer: 3 + LLayer: 3, + content: {} }, { id: 'obj4', @@ -1614,7 +1697,8 @@ const testData = { value: now }, duration: '(#obj0.start - #.start) + 2000', - LLayer: 4 + LLayer: 4, + content: {} }, { id: 'obj5', @@ -1623,7 +1707,8 @@ const testData = { value: now }, duration: '#obj0.start + 2000 - #.start', - LLayer: 5 + LLayer: 5, + content: {} }, { id: 'obj6', @@ -1632,7 +1717,8 @@ const testData = { value: now }, duration: '#obj0.start - #.start + 2000', - LLayer: 6 + LLayer: 6, + content: {} } ], 'childWithStartBeforeParent': [ @@ -1656,7 +1742,8 @@ const testData = { value: -1000 // Relative to parent object }, duration: 0, - LLayer: 2 + LLayer: 2, + content: {} }, { id: 'child1', @@ -1665,7 +1752,8 @@ const testData = { value: '#group0.start - 1000' }, duration: 0, - LLayer: 3 + LLayer: 3, + content: {} } ] } @@ -1679,7 +1767,8 @@ const testData = { value: now }, duration: 0, - LLayer: 'layer0' + LLayer: 'layer0', + content: {} }, { id: 'obj1', // The object we will trigger on @@ -1689,7 +1778,8 @@ const testData = { }, duration: 0, LLayer: 'layer1', // This ensures it works with string layer names - classes: ['class0'] + classes: ['class0'], + content: {} }, { id: 'obj2', @@ -1699,52 +1789,58 @@ const testData = { }, duration: 0, priority: 1, // Needs to be more important than obj0 - LLayer: 'layer0' + LLayer: 'layer0', + content: {} }, { id: 'obj3', trigger: { type: TriggerType.LOGICAL, - value: '$Llayer1' // Something exists on layer1 + value: '$layer1' // Something exists on layer1 }, duration: 0, - LLayer: 'layer3' + LLayer: 'layer3', + content: {} }, { id: 'obj4', trigger: { type: TriggerType.LOGICAL, - value: '$Llayer1.class0' // layer1 has object with class + value: '$layer1.class0' // layer1 has object with class }, duration: 0, - LLayer: 'layer4' + LLayer: 'layer4', + content: {} }, { id: 'obj5', trigger: { type: TriggerType.LOGICAL, - value: '$Llayer1.class1' // layer 1 has object with class + value: '$layer1.class1' // layer 1 has object with class }, duration: 0, - LLayer: 'layer5' + LLayer: 'layer5', + content: {} }, { id: 'obj6', trigger: { type: TriggerType.LOGICAL, - value: '!$Llayer1.class0' // layer 1 does not have object with class + value: '!$layer1.class0' // layer 1 does not have object with class }, duration: 0, - LLayer: 'layer6' + LLayer: 'layer6', + content: {} }, { id: 'obj7', trigger: { type: TriggerType.LOGICAL, - value: '!$Llayer1.class1' // layer 1 does not have object with class + value: '!$layer1.class1' // layer 1 does not have object with class }, duration: 0, - LLayer: 'layer7' + LLayer: 'layer7', + content: {} } ], 'logicalTriggers2': [ @@ -1755,7 +1851,8 @@ const testData = { value: '1' }, duration: 0, - LLayer: 'layer0' + LLayer: 'layer0', + content: {} }, { id: 'group0', // the id must be unique @@ -1778,7 +1875,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 'layer1_first' + LLayer: 'layer1_first', + content: {} }, { id: 'obj1', // the id must be unique @@ -1788,7 +1886,8 @@ const testData = { value: 0 // Relative to parent object }, duration: 0, - LLayer: 'layer0' + LLayer: 'layer0', + content: {} } ] } @@ -1814,13 +1913,95 @@ const testData = { }, duration: 1000, priority: 10, // Needs to be more important than obj0 - LLayer: 'layer0' + LLayer: 'layer0', + content: {} } ] } } ] } +const testData: { + [dataset: string]: Array +} = {} +// Convert test-data to new data structure: +function convertTimelineObject (obj: TimelineObject): NewTimelineObject { + const newObj: NewTimelineObject = { + id: obj.id, + enable: { + }, + layer: obj.LLayer, + // children?: Array + // keyframes?: Array + classes: obj.classes, + disabled: obj.disabled, + isGroup: obj.isGroup, + priority: obj.priority, + content: obj.content + } + + if (obj.trigger.type === TriggerType.TIME_ABSOLUTE) { + newObj.enable.start = obj.trigger.value + } else if (obj.trigger.type === TriggerType.TIME_RELATIVE) { + newObj.enable.start = obj.trigger.value + } else if (obj.trigger.type === TriggerType.LOGICAL) { + newObj.enable.while = obj.trigger.value + } + if (obj.duration) { + newObj.enable.duration = obj.duration + } + // @ts-ignore + if (obj.legacyRepeatingTime) { + // @ts-ignore + newObj.enable.repeating = obj.legacyRepeatingTime + } + if (obj.content.keyframes) { + newObj.keyframes = [] + _.each(obj.content.keyframes, (kf: TimelineKeyframe) => { + newObj.keyframes.push(convertTimelineKeyframe(kf)) + }) + delete obj.content.keyframes + } + if (obj.isGroup && obj.content.objects) { + newObj.isGroup = true + newObj.children = [] + _.each(obj.content.objects, (obj: TimelineObject) => { + newObj.children.push(convertTimelineObject(obj)) + }) + delete obj.content.objects + } + return newObj +} +function convertTimelineKeyframe (obj: TimelineKeyframe): NewTimelineKeyframe { + const newKf: NewTimelineKeyframe = { + id: obj.id, + enable: { + }, + // children?: Array + // keyframes?: Array + classes: obj.classes, + // disabled: boolean + content: obj.content + } + if (obj.trigger.type === TriggerType.TIME_ABSOLUTE) { + newKf.enable.start = obj.trigger.value + } else if (obj.trigger.type === TriggerType.TIME_RELATIVE) { + newKf.enable.start = obj.trigger.value + } else if (obj.trigger.type === TriggerType.LOGICAL) { + newKf.enable.while = obj.trigger.value + } + if (obj.duration) { + newKf.enable.duration = obj.duration + } + return newKf +} +_.each(testDataOld, (dataset, key) => { + const newDataset: any = [] + _.each(dataset, (obj: TimelineObject) => { + newDataset.push(convertTimelineObject(obj)) + }) + testData[key] = newDataset +}) let reverseData = false const reverseDataObjs = (objs: Array) => { objs = objs.reverse() @@ -1831,50 +2012,54 @@ const reverseDataObjs = (objs: Array) => { }) return objs } -const getTestData = (dataset: string) => { +const getTestData = (dataset: string): NewTimelineObject[] => { let data = clone(testData[dataset]) if (reverseData) { data = reverseDataObjs(data) } return data } +const stdOpts: ResolveOptions = { + time: 1000 +} + type Tests = {[key: string]: any} let tests: Tests = { 'Basic timeline': () => { expect(() => { // @ts-ignore bad input - const tl = Resolver.getTimelineInWindow() + const tl = Resolver.resolveTimeline() }).toThrowError() const data = getTestData('basic') - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) expect(data).toEqual(getTestData('basic')) // Make sure the original data is unmodified - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) + expect(tl.statistics.unresolvedCount).toEqual(0) - const nextEvents = Resolver.getNextEvents(data, now - 100) + const state0 = Resolver.getState(tl, now - 100) + const nextEvents = state0.nextEvents expect(data).toEqual(getTestData('basic')) // Make sure the original data was unmodified expect(nextEvents).toHaveLength(4) expect(nextEvents[0]).toMatchObject({ - type: EventType.START, time: 990, obj: { id: 'obj0' }}) + type: EventType.START, time: 990, objId: 'obj0' }) expect(nextEvents[1]).toMatchObject({ - type: EventType.END, time: 1050, obj: { id: 'obj0' }}) + type: EventType.END, time: 1050, objId: 'obj0' }) expect(nextEvents[2]).toMatchObject({ - type: EventType.START, time: 1050, obj: { id: 'obj1' }}) + type: EventType.START, time: 1050, objId: 'obj1' }) - const state = Resolver.getState(data, now) + const state = Resolver.getState(tl, now) expect(data).toEqual(getTestData('basic')) // Make sure the original data was unmodified - expect(state.GLayers['1']).toBeTruthy() // TimelineObject + expect(state.layers['1']).toBeTruthy() // TimelineObject expect(state.time).toBe(now) - expect(state.GLayers['1'].id).toBe('obj0') - expect(state.LLayers['1'].id).toBe('obj0') + expect(state.layers['1'].id).toBe('obj0') const state2 = Resolver.getState(tl, now) - expect(state2.GLayers['1'].id).toBe('obj0') + expect(state2.layers['1'].id).toBe('obj0') expect(data).toEqual(getTestData('basic')) // Make sure the original data was unmodified }, @@ -1882,26 +2067,26 @@ let tests: Tests = { const data = getTestData('basic2') - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(0) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(0) - const nextEvents = Resolver.getNextEvents(data, 900) + const tmpState = Resolver.getState(tl, 900) + const nextEvents = tmpState.nextEvents expect(nextEvents).toHaveLength(2) expect(nextEvents[0].type).toBe(EventType.START) expect(nextEvents[0].time).toBe(950) expect(nextEvents[1].type).toBe(EventType.END) expect(nextEvents[1].time).toBe(1050) - const state = Resolver.getState(data, 1000) + const state = Resolver.getState(tl, 1000) expect(state.time).toBe(1000) - expect(state.GLayers['10'].id).toBe('obj0') - expect(state.LLayers['10'].id).toBe('obj0') + expect(state.layers['10'].id).toBe('obj0') const state2 = Resolver.getState(tl, 1000) - expect(state2.GLayers['10'].id).toBe('obj0') + expect(state2.layers['10'].id).toBe('obj0') expect(data).toEqual(getTestData('basic2')) // Make sure the original data is unmodified }, 'Basic timeline 3': () => { @@ -1909,18 +2094,19 @@ let tests: Tests = { const data = getTestData('basic2') .concat(getTestData('basic3')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(3) - expect(tl.unresolved).toHaveLength(0) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(3) + expect(tl.statistics.unresolvedCount).toEqual(0) - const nextEvents = Resolver.getNextEvents(tl, 1000, 1) // limit + const tmpState = Resolver.getState(tl, 1000, 1) // limit + const nextEvents = tmpState.nextEvents expect(nextEvents).toHaveLength(1) // see that the limit is working expect(nextEvents[0].type).toBe(EventType.END) expect(nextEvents[0].time).toBe(1050) const state = Resolver.getState(tl, 1000) - expect(state.LLayers['10'].id).toBe('obj1') - expect(state.LLayers['11'].id).toBe('obj2') + expect(state.layers['10'].id).toBe('obj1') + expect(state.layers['11'].id).toBe('obj2') expect(data).toEqual(getTestData('basic2') .concat(getTestData('basic3'))) // Make sure the original data is unmodified }, @@ -1929,21 +2115,21 @@ let tests: Tests = { const data = getTestData('basic2') .concat(getTestData('basic3')) .concat(getTestData('override')) - - const state = Resolver.getState(data, 1000) - expect(state.LLayers['10'].id).toBe('obj3') + const tl = Resolver.resolveTimeline(data, stdOpts) + const state = Resolver.getState(tl, 1000) + expect(state.layers['10'].id).toBe('obj3') }, 'Timeline, override object 2': () => { const data = getTestData('basic2') .concat(getTestData('basic3')) .concat(getTestData('override')) .concat(getTestData('override2')) + const tl = Resolver.resolveTimeline(data, stdOpts) + const state = Resolver.getState(tl, 1000) + expect(state.layers['10'].id).toBe('obj5') - const state = Resolver.getState(data, 1000) - expect(state.LLayers['10'].id).toBe('obj5') - - const stateInFuture = Resolver.getState(data, 10000) - expect(stateInFuture.LLayers['10'].id).toBe('obj5') + const stateInFuture = Resolver.getState(tl, 10000) + expect(stateInFuture.layers['10'].id).toBe('obj5') }, 'Timeline, override object 3': () => { @@ -1952,79 +2138,84 @@ let tests: Tests = { .concat(getTestData('override')) .concat(getTestData('override2')) .concat(getTestData('override3')) + const tl = Resolver.resolveTimeline(data, stdOpts) + const state = Resolver.getState(tl, 1000) + expect(state.layers['10'].id).toBe('obj6') - const state = Resolver.getState(data, 1000) - expect(state.LLayers['10'].id).toBe('obj6') - - const stateInFuture = Resolver.getState(data, 10000) - expect(stateInFuture.LLayers['10'].id).toBe('obj5') + const stateInFuture = Resolver.getState(tl, 10000) + expect(stateInFuture.layers['10'].id).toBe('obj5') }, 'Timeline, relative timing': () => { const data = getTestData('relative1') - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(6) - expect(tl.unresolved).toHaveLength(1) + expect(tl.statistics.resolvedObjectCount).toEqual(6) + expect(tl.statistics.unresolvedCount).toEqual(1) - const obj1 = _.find(tl.resolved, { id: 'obj1' }) + const obj1 = tl.objects['obj1'] expect(obj1).toBeTruthy() // TimelineObject - expect(obj1.resolved.startTime).toBe(1054) - expect(obj1.resolved.endTime).toBe(1064) + expect(obj1.resolved.instances[0].start).toBe(1054) + expect(obj1.resolved.instances[0].end).toBe(1064) - const obj2 = _.find(tl.resolved, { id: 'obj2' }) + const obj2 = tl.objects['obj2'] expect(obj2).toBeTruthy() // TimelineObject - expect(obj2.resolved.startTime).toBe(1068) - expect(obj2.resolved.endTime).toBe(1118) + expect(obj2.resolved.instances[0].start).toBe(1068) + expect(obj2.resolved.instances[0].end).toBe(1118) - const obj3 = _.find(tl.resolved, { id: 'obj3' }) + const obj3 = tl.objects['obj3'] expect(obj3).toBeTruthy() // TimelineObject - expect(obj3.resolved.startTime).toBe(1058) - expect(obj3.resolved.endTime).toBe(1068) + expect(obj3.resolved.instances[0].start).toBe(1058) + expect(obj3.resolved.instances[0].end).toBe(1068) - const obj4 = _.find(tl.resolved, { id: 'obj4' }) + const obj4 = tl.objects['obj4'] expect(obj4).toBeTruthy() // TimelineObject - expect(obj4.resolved.startTime).toBe(1118) - expect(obj4.resolved.endTime).toBe(1128) + expect(obj4.resolved.instances[0].start).toBe(1118) + expect(obj4.resolved.instances[0].end).toBe(1128) - const obj5 = _.find(tl.resolved, { id: 'obj5' }) + const obj5 = tl.objects['obj5'] expect(obj5).toBeTruthy() // TimelineObject - expect(obj5.resolved.startTime).toBe(1123) - expect(obj5.resolved.endTime).toBe(1133) + expect(obj5.resolved.instances[0].start).toBe(1123) + expect(obj5.resolved.instances[0].end).toBe(1133) - const state0 = Resolver.getState(data, 1067) + const state0 = Resolver.getState(tl, 1067) - expect(state0.GLayers['10'].id).toBe('obj3') + expect(state0.layers['10'].id).toBe('obj3') - const state1 = Resolver.getState(data, 1068) + const state1 = Resolver.getState(tl, 1068) - expect(state1.GLayers['10'].id).toBe('obj2') + expect(state1.layers['10'].id).toBe('obj2') - const nextEvents = Resolver.getNextEvents(tl, 900) + const tmpState = Resolver.getState(tl, 900) + const nextEvents = tmpState.nextEvents - expect(nextEvents).toHaveLength(10) + expect(nextEvents).toHaveLength(12) expect(nextEvents[0]).toMatchObject({ - type: EventType.START, time: 950, obj: { id: 'obj0' }}) + type: EventType.START, time: 950, objId: 'obj0' }) expect(nextEvents[1]).toMatchObject({ - type: EventType.END, time: 1050, obj: { id: 'obj0' }}) + type: EventType.END, time: 1050, objId: 'obj0' }) expect(nextEvents[2]).toMatchObject({ - type: EventType.START, time: 1054, obj: { id: 'obj1' }}) + type: EventType.START, time: 1054, objId: 'obj1' }) expect(nextEvents[3]).toMatchObject({ - type: EventType.START, time: 1058, obj: { id: 'obj3' }}) + type: EventType.START, time: 1058, objId: 'obj3' }) expect(nextEvents[4]).toMatchObject({ - type: EventType.END, time: 1064, obj: { id: 'obj1' }}) + type: EventType.END, time: 1064, objId: 'obj1' }) expect(nextEvents[5]).toMatchObject({ - type: EventType.END, time: 1068, obj: { id: 'obj3' }}) + type: EventType.END, time: 1068, objId: 'obj3' }) expect(nextEvents[6]).toMatchObject({ - type: EventType.START, time: 1068, obj: { id: 'obj2' }}) + type: EventType.START, time: 1068, objId: 'obj2' }) expect(nextEvents[7]).toMatchObject({ - type: EventType.END, time: 1118, obj: { id: 'obj2' }}) + type: EventType.END, time: 1118, objId: 'obj2' }) expect(nextEvents[8]).toMatchObject({ - type: EventType.START, time: 1118, obj: { id: 'obj4' }}) + type: EventType.START, time: 1118, objId: 'obj4' }) expect(nextEvents[9]).toMatchObject({ - type: EventType.START, time: 1123, obj: { id: 'obj5' }}) + type: EventType.START, time: 1123, objId: 'obj5' }) + expect(nextEvents[10]).toMatchObject({ + type: EventType.END, time: 1128, objId: 'obj4' }) + expect(nextEvents[11]).toMatchObject({ + type: EventType.END, time: 1133, objId: 'obj5' }) expect(data).toEqual(getTestData('relative1')) // Make sure the original data is unmodified }, @@ -2032,71 +2223,75 @@ let tests: Tests = { const data = getTestData('relative2') - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(1) + expect(tl.statistics.resolvedObjectCount).toEqual(2) + expect(tl.statistics.unresolvedCount).toEqual(1) - const obj2 = _.find(tl.resolved, { id: 'obj2' }) + const obj2 = tl.objects['obj2'] expect(obj2).toBeTruthy() // TimelineObject - expect(obj2.resolved.startTime).toBe(965) - expect(obj2.resolved.endTime).toBe(1015) + expect(obj2.resolved.instances[0].start).toBe(965) + expect(obj2.resolved.instances[0].end).toBe(1015) - const state0 = Resolver.getState(data, 1000) + const state0 = Resolver.getState(tl, 1000) - expect(state0.GLayers['10'].id).toBe('obj2') + expect(state0.layers['10'].id).toBe('obj2') - const state1 = Resolver.getState(data, 2000) + const state1 = Resolver.getState(tl, 2000) - expect(state1.GLayers['10'].id).toBe('obj0') + expect(state1.layers['10'].id).toBe('obj0') }, 'Timeline, relative timing and keyframes': () => { const data = getTestData('keyframes1') - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(3) - expect(tl.unresolved).toHaveLength(0) + expect(tl.statistics.resolvedObjectCount).toEqual(3) + expect(tl.statistics.resolvedKeyframeCount).toEqual(3) + expect(tl.statistics.unresolvedCount).toEqual(0) - const obj2 = _.find(tl.resolved, { id: 'obj2' }) + const obj2 = tl.objects['obj2'] expect(obj2).toBeTruthy() // TimelineObject - expect(obj2.resolved.startTime).toBe(965) - expect(obj2.resolved.endTime).toBe(1015) + expect(obj2.resolved.instances[0].start).toBe(965) + expect(obj2.resolved.instances[0].end).toBe(1015) - const obj3 = _.find(tl.resolved, { id: 'obj3' }) + const obj3 = tl.objects['obj3'] expect(obj3).toBeTruthy() // TimelineObject - expect(obj3.resolved.startTime).toBe(985) + expect(obj3.resolved.instances[0].start).toBe(985) - const nextEvents0 = Resolver.getNextEvents(data, 100) + const tmpState0 = Resolver.getState(tl, 100, 10) + + const nextEvents0 = tmpState0.nextEvents expect(nextEvents0).toHaveLength(10) - const nextEvents1 = Resolver.getNextEvents(data, 2000) + const tmpState1 = Resolver.getState(tl, 2000) + const nextEvents1 = tmpState1.nextEvents expect(nextEvents1).toHaveLength(0) - const state0 = Resolver.getState(data, 966) + const state0 = Resolver.getState(tl, 966) - let sobj2 = state0.GLayers['10'] + let sobj2 = state0.layers['10'] expect(sobj2.id).toBe('obj2') - expect(sobj2.resolved.mixer.opacity).toBe(0.1) - expect(sobj2.resolved.mixer.brightness).toBe(0.1) - expect(sobj2.resolved.mixer.myCustomAttribute).toBeFalsy() + expect(sobj2.content.mixer.opacity).toBe(0.1) + expect(sobj2.content.mixer.brightness).toBe(0.1) + expect(sobj2.content.mixer.myCustomAttribute).toBeFalsy() - const state1 = Resolver.getState(data, 967) + const state1 = Resolver.getState(tl, 967) - sobj2 = state1.GLayers['10'] + sobj2 = state1.layers['10'] - expect(sobj2.resolved.mixer.opacity).toBe(0.2) - expect(sobj2.resolved.mixer.brightness).toBe(0.1) - expect(sobj2.resolved.mixer.myCustomAttribute).toBeFalsy() + expect(sobj2.content.mixer.opacity).toBe(0.2) + expect(sobj2.content.mixer.brightness).toBe(0.1) + expect(sobj2.content.mixer.myCustomAttribute).toBeFalsy() - const state2 = Resolver.getState(data, 984) + const state2 = Resolver.getState(tl, 984) - sobj2 = state2.GLayers['10'] + sobj2 = state2.layers['10'] - expect(sobj2.resolved.mixer.opacity).toBe(0.3) - expect(sobj2.resolved.mixer.myCustomAttribute).toBe(1) + expect(sobj2.content.mixer.opacity).toBe(0.3) + expect(sobj2.content.mixer.myCustomAttribute).toBe(1) expect(data).toEqual(getTestData('keyframes1')) // Make sure the original data is unmodified }, @@ -2104,55 +2299,54 @@ let tests: Tests = { const data = getTestData('abskeyframe') - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(0) + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(0) - const obj0 = _.find(tl.resolved, { id: 'obj0' }) + const obj0 = tl.objects['obj0'] expect(obj0).toBeTruthy() // TimelineObject - expect(obj0.resolved.startTime).toBe(1000) - expect(obj0.resolved.endTime).toBe(1050) + expect(obj0.resolved.instances[0].start).toBe(1000) + expect(obj0.resolved.instances[0].end).toBe(1050) - const state0 = Resolver.getState(clone(data), 1000) + const state0 = Resolver.getState(tl, 1000) - const sobj0 = state0.GLayers['1'] + const sobj0 = state0.layers['1'] expect(sobj0).toBeTruthy() // TimelineResolvedObject expect(sobj0.id).toBe('obj0') expect(sobj0.resolved).toBeTruthy() // TimelineResolvedObject - expect(sobj0.resolved.attributes).toMatchObject({ + expect(sobj0.content.attributes).toMatchObject({ positionX: 0, positionY: 0, scale: 1 }) - expect(sobj0.resolved.attributes.opacity).toBeFalsy() + expect(sobj0.content.attributes.opacity).toBeFalsy() - const state1 = Resolver.getState(clone(data), 1005) + const state1 = Resolver.getState(tl, 1005) - const sobj1 = state1.GLayers['1'] + const sobj1 = state1.layers['1'] expect(sobj1).toBeTruthy() // TimelineResolvedObject - expect(sobj1.resolved.attributes).toMatchObject({ + expect(sobj1.content.attributes).toMatchObject({ positionX: 0, positionY: 0, scale: 0.5, opacity: 0.5 }) + const state2 = Resolver.getState(tl, 1010) - const state2 = Resolver.getState(clone(data), 1010) - - const sobj2 = state2.GLayers['1'] + const sobj2 = state2.layers['1'] expect(sobj2).toBeTruthy() // TimelineResolvedObject - expect(sobj2.resolved.attributes).toMatchObject({ + expect(sobj2.content.attributes).toMatchObject({ positionX: 0, positionY: 0, scale: 1 }) - expect(sobj2.resolved.attributes).toBeTruthy() - expect(sobj2.resolved.attributes.opacity).toBeFalsy() + expect(sobj2.content.attributes).toBeTruthy() + expect(sobj2.content.attributes.opacity).toBeFalsy() expect(data).toEqual(getTestData('abskeyframe')) // Make sure the original data is unmodified }, @@ -2161,35 +2355,36 @@ let tests: Tests = { const data = getTestData('basic') .concat(getTestData('logical1')) - // let tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) + // let tl = Resolver.resolveTimeline(data, stdOpts) // console.log('tl.resolved',tl.resolved) - // expect(tl.resolved).toHaveLength( 3) - // expect(tl.unresolved).toHaveLength( 0) + // expect(tl.statistics.resolvedObjectCount).toEqual( 3) + // expect(tl.statistics.unresolvedCount).toEqual( 0) // let logical0 = _.find(tl.resolved, {id: 'logical0'}) // expect(logical0).toBeTruthy() // TimelineObject // let logical1 = _.find(tl.resolved, {id: 'logical1'}) // expect(logical1).toBeTruthy() // TimelineObject - const state0 = Resolver.getState(clone(data), now) + const state0 = Resolver.getState(tl, now) - expect(state0.GLayers['1']).toBeTruthy() // TimelineResolvedObject - expect(state0.GLayers['1'].id).toBe('obj0') + expect(state0.layers['1']).toBeTruthy() // TimelineResolvedObject + expect(state0.layers['1'].id).toBe('obj0') - expect(state0.GLayers['2']).toBeTruthy() // TimelineResolvedObject - expect(state0.GLayers['2'].id).toBe('logical0') + expect(state0.layers['2']).toBeTruthy() // TimelineResolvedObject + expect(state0.layers['2'].id).toBe('logical0') - expect(state0.GLayers['3']).toBeTruthy() // TimelineResolvedObject - expect(state0.GLayers['3'].id).toBe('logical1') + expect(state0.layers['3']).toBeTruthy() // TimelineResolvedObject + expect(state0.layers['3'].id).toBe('logical1') - expect(state0.GLayers['4']).toBeTruthy() // TimelineResolvedObject - expect(state0.GLayers['4'].id).toBe('logical2') + expect(state0.layers['4']).toBeTruthy() // TimelineResolvedObject + expect(state0.layers['4'].id).toBe('logical2') - const state1 = Resolver.getState(clone(data), now + 1000) + const state1 = Resolver.getState(tl, now + 1000) - expect(state1.GLayers['2']).toBeTruthy() // TimelineResolvedObject - expect(state1.GLayers['3']).toBeFalsy() // TimelineResolvedObject - expect(state1.GLayers['4']).toBeTruthy() // TimelineResolvedObject + expect(state1.layers['2']).toBeTruthy() // TimelineResolvedObject + expect(state1.layers['3']).toBeFalsy() // TimelineResolvedObject + expect(state1.layers['4']).toBeTruthy() // TimelineResolvedObject expect(data).toEqual(getTestData('basic') .concat(getTestData('logical1'))) // Make sure the original data is unmodified @@ -2199,19 +2394,19 @@ let tests: Tests = { const data = getTestData('basic') .concat(getTestData('logical2')) - // let tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - const state0 = Resolver.getState(clone(data), now) + const state0 = Resolver.getState(tl, now) - expect(state0.GLayers['1']).toBeTruthy() - expect(state0.GLayers['2']).toBeTruthy() - expect(state0.GLayers['3']).toBeFalsy() + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['3']).toBeFalsy() - const state1 = Resolver.getState(clone(data), now + 1000) + const state1 = Resolver.getState(tl, now + 1000) - expect(state1.GLayers['1']).toBeFalsy() // TimelineResolvedObject - expect(state1.GLayers['2']).toBeFalsy() // TimelineResolvedObject - expect(state1.GLayers['3']).toBeTruthy() // TimelineResolvedObject + expect(state1.layers['1']).toBeFalsy() // TimelineResolvedObject + expect(state1.layers['2']).toBeFalsy() // TimelineResolvedObject + expect(state1.layers['3']).toBeTruthy() // TimelineResolvedObject expect(data).toEqual(getTestData('basic') .concat(getTestData('logical2'))) // Make sure the original data is unmodified @@ -2221,404 +2416,552 @@ let tests: Tests = { const data = getTestData('basic') .concat(getTestData('logical3')) - // let tl = Resolver.getTimelineInWindow(data) - try { - const state0 = Resolver.getState(clone(data), now) - - expect(state0.GLayers['1']).toBeTruthy() - expect(state0.GLayers['2']).toBeTruthy() - expect(state0.GLayers['3']).toBeFalsy() - - const state1 = Resolver.getState(clone(data), now + 1000) - - expect(state1.GLayers['1']).toBeFalsy() // TimelineResolvedObject - expect(state1.GLayers['2']).toBeFalsy() // TimelineResolvedObject - expect(state1.GLayers['3']).toBeTruthy() // TimelineResolvedObject - } catch (e) { - console.log(e, e.stack) - } - }, - 'setTraceLevel': () => { - Resolver.setTraceLevel(TraceLevel.INFO) - expect(Resolver.getTraceLevel()).toEqual(TraceLevel.INFO) - - Resolver.setTraceLevel(TraceLevel.ERRORS) - expect(Resolver.getTraceLevel()).toEqual(TraceLevel.ERRORS) - - Resolver.setTraceLevel('INFO') - expect(Resolver.getTraceLevel()).toEqual(TraceLevel.INFO) - - Resolver.setTraceLevel('asdf') - expect(Resolver.getTraceLevel()).toEqual(TraceLevel.ERRORS) - }, - 'getObjectsInWindow': () => { - const data = clone(getTestData('basic')) - - const tld = Resolver.getObjectsInWindow(clone(data), now - 10, now + 10) - - expect(tld.resolved).toHaveLength(1) - expect(tld.unresolved).toHaveLength(0) - - expect(data).toEqual(getTestData('basic')) // Make sure the original data is unmodified - }, - 'External functions': () => { - const data = clone(getTestData('basic')) - - const state0 = Resolver.getState(data, now) - expect(data).toEqual(getTestData('basic')) // Make sure the original data is unmodified - - expect(state0.LLayers['1']).toBeTruthy() // TimelineObject - expect(state0.LLayers['1'].id).toBe('obj0') - - const obj0: TimelineObject = _.findWhere(data, { id: 'obj0' }) - obj0.externalFunction = 'ext0' + const tl = Resolver.resolveTimeline(data, stdOpts) - const externalFunctions0: ExternalFunctions = { - 'ext0': jest.fn((resolvedObj: TimelineResolvedObject, state: TimelineState, tld: DevelopedTimeline) => { - // disable this object - resolvedObj.resolved.disabled = true - state = state - tld = tld - - return true - }) - } + const state0 = Resolver.getState(tl, now) - const state1 = Resolver.getState(data, now, externalFunctions0) + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['3']).toBeFalsy() - expect(externalFunctions0.ext0).toHaveBeenCalledTimes(1) + const state1 = Resolver.getState(tl, now + 1000) - expect(state1.LLayers['1']).toBeFalsy() // TimelineObject + expect(state1.layers['1']).toBeFalsy() // TimelineResolvedObject + expect(state1.layers['2']).toBeFalsy() // TimelineResolvedObject + expect(state1.layers['3']).toBeTruthy() // TimelineResolvedObject }, - 'Expressions': () => { + // 'setTraceLevel': () => { + // Resolver.setTraceLevel(TraceLevel.INFO) + // expect(Resolver.getTraceLevel()).toEqual(TraceLevel.INFO) - expect(Resolver.interpretExpression('1 + 2')).toMatchObject({ - l: '1', - o: '+', - r: '2' - }) + // Resolver.setTraceLevel(TraceLevel.ERRORS) + // expect(Resolver.getTraceLevel()).toEqual(TraceLevel.ERRORS) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('1 + 2') - )).toEqual(3) + // Resolver.setTraceLevel('INFO') + // expect(Resolver.getTraceLevel()).toEqual(TraceLevel.INFO) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('5 + 4 - 2 + 1 - 5 + 7') - )).toEqual(10) + // Resolver.setTraceLevel('asdf') + // expect(Resolver.getTraceLevel()).toEqual(TraceLevel.ERRORS) + // }, + // 'getObjectsInWindow': () => { + // const data = clone(getTestData('basic')) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('5 - 4 - 3') - )).toEqual(-2) + // const tld = Resolver.getObjectsInWindow(clone(data), now - 10, now + 10) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('5 - 4 - 3 - 10 + 2') - )).toEqual(-10) + // expect(tld.resolved).toHaveLength(1) + // expect(tld.unresolved).toHaveLength(0) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('4 * 5.5') - )).toEqual(22) + // expect(data).toEqual(getTestData('basic')) // Make sure the original data is unmodified + // }, + // 'External functions': () => { + // const data = clone(getTestData('basic')) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('2 * 3 * 4') - )).toEqual(24) + // const state0 = Resolver.getState(tl, now) + // expect(data).toEqual(getTestData('basic')) // Make sure the original data is unmodified - expect(Resolver.resolveExpression( - Resolver.interpretExpression('20 / 4 / 2') - )).toEqual(2.5) + // expect(state0.layers['1']).toBeTruthy() // TimelineObject + // expect(state0.layers['1'].id).toBe('obj0') - expect(Resolver.resolveExpression( - Resolver.interpretExpression('2 * (2 + 3) - 2 * 2') - )).toEqual(6) + // const obj0: TimelineObject = _.findWhere(data, { id: 'obj0' }) + // obj0.externalFunction = 'ext0' - expect(Resolver.resolveExpression( - Resolver.interpretExpression('2 * 2 + 3 - 2 * 2') - )).toEqual(3) + // const externalFunctions0: ExternalFunctions = { + // 'ext0': jest.fn((resolvedObj: TimelineResolvedObject, state: TimelineState, tld: DevelopedTimeline) => { + // // disable this object + // resolvedObj.resolved.disabled = true + // state = state + // tld = tld - expect(Resolver.resolveExpression( - Resolver.interpretExpression('2 * 2 + 3 - 2 * 2') - )).toEqual(3) + // return true + // }) + // } - expect(Resolver.resolveExpression( - Resolver.interpretExpression('5 + -3') - )).toEqual(2)yarn + // const state1 = Resolver.getState(tl, now, externalFunctions0) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('5 + - 3') - )).toEqual(2) + // expect(externalFunctions0.ext0).toHaveBeenCalledTimes(1) - expect(Resolver.resolveExpression( - Resolver.interpretExpression('') - )).toEqual(NaN) + // expect(state1.layers['1']).toBeFalsy() // TimelineObject - expect(() => { - Resolver.resolveLogicalExpression( - Resolver.interpretExpression('5 + ) 2') // unbalanced paranthesis - ) - }).toThrowError() - expect(() => { - Resolver.resolveLogicalExpression( - Resolver.interpretExpression('5 ( + 2') // unbalanced paranthesis - ) - }).toThrowError() - expect(() => { - Resolver.resolveLogicalExpression( - Resolver.interpretExpression('5 * ') // unbalanced expression - ) - }).toThrowError() - - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('1 | 0', true) - )).toEqual(true) - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('1 & 0', true) - )).toEqual(false) - - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('1 | 0 & 0', true) - )).toEqual(false) - - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('0 & 1 | 1', true) - )).toEqual(false) - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('(0 & 1) | 1', true) - )).toEqual(true) - - expect(() => { - Resolver.resolveLogicalExpression( - Resolver.interpretExpression('(0 & 1) | 1 a', true) // strange operator - ) - }).toThrowError() - - expect(Resolver.resolveLogicalExpression( - Resolver.interpretExpression('(0 & 1) | a', true) // strange operand - )).toEqual(false) - - expect(() => { - Resolver.resolveLogicalExpression( - Resolver.interpretExpression('14 + #badReference.start', true) - ) - }).toThrowError() + // }, + 'Expressions': () => { - const data = clone(getTestData('logical1')) - const state: TimelineState = { - time: now, - GLayers: {}, - LLayers: {} - } - const val = Resolver.decipherLogicalValue('1', data[0], state) - expect(val).toBeTruthy() + // expect(Resolver.interpretExpression('1 + 2')).toMatchObject({ + // l: '1', + // o: '+', + // r: '2' + // }) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('1 + 2') + // )).toEqual(3) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('5 + 4 - 2 + 1 - 5 + 7') + // )).toEqual(10) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('5 - 4 - 3') + // )).toEqual(-2) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('5 - 4 - 3 - 10 + 2') + // )).toEqual(-10) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('4 * 5.5') + // )).toEqual(22) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('2 * 3 * 4') + // )).toEqual(24) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('20 / 4 / 2') + // )).toEqual(2.5) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('2 * (2 + 3) - 2 * 2') + // )).toEqual(6) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('2 * 2 + 3 - 2 * 2') + // )).toEqual(3) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('2 * 2 + 3 - 2 * 2') + // )).toEqual(3) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('5 + -3') + // )).toEqual(2)yarn + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('5 + - 3') + // )).toEqual(2) + + // expect(Resolver.resolveExpression( + // Resolver.interpretExpression('') + // )).toEqual(NaN) + + // expect(() => { + // Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('5 + ) 2') // unbalanced paranthesis + // ) + // }).toThrowError() + // expect(() => { + // Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('5 ( + 2') // unbalanced paranthesis + // ) + // }).toThrowError() + // expect(() => { + // Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('5 * ') // unbalanced expression + // ) + // }).toThrowError() + + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('1 | 0', true) + // )).toEqual(true) + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('1 & 0', true) + // )).toEqual(false) + + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('1 | 0 & 0', true) + // )).toEqual(false) + + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('0 & 1 | 1', true) + // )).toEqual(false) + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('(0 & 1) | 1', true) + // )).toEqual(true) + + // expect(() => { + // Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('(0 & 1) | 1 a', true) // strange operator + // ) + // }).toThrowError() + + // expect(Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('(0 & 1) | a', true) // strange operand + // )).toEqual(false) + + // expect(() => { + // Resolver.resolveLogicalExpression( + // Resolver.interpretExpression('14 + #badReference.start', true) + // ) + // }).toThrowError() + + // const data = clone(getTestData('logical1')) + // const state: TimelineState = { + // time: now, + // GLayers: {}, + // LLayers: {} + // } + // const val = Resolver.decipherLogicalValue('1', data[0], state) + // expect(val).toBeTruthy() }, 'disabled objects on timeline': () => { const data = clone(getTestData('basic')) - const obj0: TimelineObject = _.findWhere(data, { id: 'obj0' }) + const obj0: NewTimelineObject = _.findWhere(data, { id: 'obj0' }) obj0.disabled = true - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) + expect(tl.statistics.unresolvedCount).toEqual(0) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeFalsy() + expect(state0.layers['1']).toBeFalsy() }, 'object with infinite duration': () => { const data = clone(getTestData('infiniteduration')) - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(1) // because obj0 has infinite duration + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(1) // because obj0 has infinite duration const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toBe('obj0') + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toBe('obj0') }, 'bad objects on timeline': () => { expect(() => { const data = clone(getTestData('basic')) delete data[0].id - Resolver.getState(clone(data), now) + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() - expect(() => { const data = clone(getTestData('basic')) - delete data[0].trigger - Resolver.getState(clone(data), now) + delete data[0].enable + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() expect(() => { const data = clone(getTestData('basic')) - delete data[0].trigger.type - Resolver.getState(clone(data), now) + delete data[0].enable.start + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() expect(() => { const data = clone(getTestData('basic')) - delete data[0].LLayer - Resolver.getState(clone(data), now) + delete data[0].layer + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) + }).toThrowError() + expect(() => { + const data = clone(getTestData('basic')) + delete data[0].content + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() expect(() => { const data = clone(getTestData('basic')) data[0].id = 'asdf' data[1].id = 'asdf' // should be unique - Resolver.getState(clone(data), now) + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() expect(() => { const data = clone(getTestData('simplegroup')) - delete data[0].content.objects - Resolver.getState(clone(data), now) + delete data[0].children + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) + }).toThrowError() + expect(() => { + const data = clone(getTestData('simplegroup')) + delete data[0].isGroup + const tl = Resolver.resolveTimeline(data, stdOpts) + Resolver.getState(tl, now) }).toThrowError() }, 'simple group': () => { const data = clone(getTestData('simplegroup')) - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) expect(data).toEqual(getTestData('simplegroup')) // Make sure the original data is unmodified - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(0) - const tld = Resolver.developTimelineAroundTime(tl, now) - expect(tld.resolved).toHaveLength(3) - expect(tld.unresolved).toHaveLength(0) + // console.log(JSON.stringify(tl,'', 3)) + expect(tl.statistics.resolvedCount).toEqual(4) + expect(tl.statistics.resolvedObjectCount).toEqual(4) + expect(tl.statistics.resolvedGroupCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(0) - const child0: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child0' }) - const child1: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child1' }) - const obj1: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'obj1' }) + const child0: NewResolvedTimelineObject = tl.objects['child0'] + const child1: NewResolvedTimelineObject = tl.objects['child1'] + const obj1: NewResolvedTimelineObject = tl.objects['obj1'] - expect(child0.resolved.startTime).toBe(990) - expect(child0.resolved.endTime).toBe(1005) - expect(child1.resolved.startTime).toBe(1005) - expect(child1.resolved.endTime).toBe(1015) - expect(obj1.resolved.startTime).toBe(1050) + expect(child0.resolved.instances[0].start).toBe(990) + expect(child0.resolved.instances[0].end).toBe(1005) + expect(child1.resolved.instances[0].start).toBe(1005) + expect(child1.resolved.instances[0].end).toBe(1015) + expect(obj1.resolved.instances[0].start).toBe(1050) - const events0 = Resolver.getNextEvents(tl, now) + const tmpState0 = Resolver.getState(tl, 0) + const events0 = tmpState0.nextEvents // console.log('tld', tld.resolved) // console.log('events0', events0) - expect(events0).toHaveLength(5) + expect(events0).toMatchObject([ + { objId: 'child0', time: 990, type: EventType.START }, + { objId: 'group0', time: 990, type: EventType.START }, + { objId: 'child0', time: 1005, type: EventType.END }, + { objId: 'child1', time: 1005, type: EventType.START }, + { objId: 'child1', time: 1015, type: EventType.END }, + { objId: 'group0', time: 1050, type: EventType.END }, + { objId: 'obj1', time: 1050, type: EventType.START }, + { objId: 'obj1', time: 1110, type: EventType.END } + ]) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['2']).toBeTruthy() - expect(state0.GLayers['2']).toBeTruthy() - expect(state0.LLayers['2'].id).toBe('child0') + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2'].id).toBe('child0') + + const tmpState1 = Resolver.getState(tl, now + 10) + const events1 = tmpState1.nextEvents + expect(events1).toMatchObject([ + { objId: 'child1', time: 1015, type: EventType.END }, + { objId: 'group0', time: 1050, type: EventType.END }, + { objId: 'obj1', time: 1050, type: EventType.START }, + { objId: 'obj1', time: 1110, type: EventType.END } + ]) - const events1 = Resolver.getNextEvents(tl, now + 10) - expect(events1).toHaveLength(3) const state1 = Resolver.getState(tl, now + 10) - expect(state1.LLayers['2']).toBeTruthy() - expect(state1.GLayers['2']).toBeTruthy() - expect(state1.LLayers['2'].id).toBe('child1') + expect(state1.layers['2']).toBeTruthy() + expect(state1.layers['2']).toBeTruthy() + expect(state1.layers['2'].id).toBe('child1') + + const tmpState2 = Resolver.getState(tl, now + 25) + const events2 = tmpState2.nextEvents + expect(events2).toMatchObject([ + { objId: 'group0', time: 1050, type: EventType.END }, + { objId: 'obj1', time: 1050, type: EventType.START }, + { objId: 'obj1', time: 1110, type: EventType.END } + ]) - const events2 = Resolver.getNextEvents(tl, now + 25) - expect(events2).toHaveLength(2) const state2 = Resolver.getState(tl, now + 25) - expect(state2.LLayers['2']).toBeFalsy() - expect(state2.GLayers['2']).toBeFalsy() + expect(state2.layers['2']).toBeFalsy() + expect(state2.layers['2']).toBeFalsy() const state3 = Resolver.getState(tl, now + 60) - expect(state3.LLayers['2']).toBeTruthy() - expect(state3.GLayers['2']).toBeTruthy() - expect(state3.LLayers['2'].id).toBe('obj1') + expect(state3.layers['2']).toBeTruthy() + expect(state3.layers['2']).toBeTruthy() + expect(state3.layers['2'].id).toBe('obj1') expect(data).toEqual(getTestData('simplegroup')) // Make sure the original data was unmodified }, 'repeating group': () => { const data = clone(getTestData('repeatinggroup')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(0) - - const tld0 = Resolver.developTimelineAroundTime(tl, now) - expect(tld0.resolved).toHaveLength(3) - expect(tld0.resolved[0].id).toBe('child0') - expect(tld0.resolved[0].resolved.startTime).toBe(990) - expect(tld0.resolved[1].id).toBe('child1') - expect(tld0.resolved[1].resolved.startTime).toBe(1005) - - const events0 = Resolver.getNextEvents(tl, now) - expect(events0).toHaveLength(5) - expect(events0[0]).toMatchObject({ - type: EventType.END, time: 1005, obj: { id: 'child0' }}) - expect(events0[1]).toMatchObject({ - type: EventType.START, time: 1005, obj: { id: 'child1' }}) - expect(events0[2]).toMatchObject({ - type: EventType.END, time: 1015, obj: { id: 'child1' }}) - - const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toBe('child0') - - const tld1 = Resolver.developTimelineAroundTime(tl, now + 10) - expect(tld1.resolved).toHaveLength(3) - expect(tld1.resolved[0].id).toBe('child1') - expect(tld1.resolved[0].resolved.startTime).toBe(1005) - expect(tld1.resolved[1].id).toBe('child0') - expect(tld1.resolved[1].resolved.startTime).toBe(1015) - const events1 = Resolver.getNextEvents(tl, now + 10) - expect(events1).toHaveLength(5) - expect(events1[0]).toMatchObject({ - type: EventType.END, time: 1015, obj: { id: 'child1' }}) - expect(events1[1]).toMatchObject({ - type: EventType.START, time: 1015, obj: { id: 'child0' }}) - expect(events1[2]).toMatchObject({ - type: EventType.END, time: 1030, obj: { id: 'child0' }}) - - const state1 = Resolver.getState(tl, now + 10) - expect(state1.LLayers['1']).toBeTruthy() - expect(state1.LLayers['1'].id).toBe('child1') + const tl = Resolver.resolveTimeline(data, _.extend(stdOpts, { limitCount: 99, limitTime: 1050 })) + expect(tl.statistics.resolvedObjectCount).toEqual(4) + expect(tl.statistics.unresolvedCount).toEqual(0) + + expect(tl.objects['group0']).toBeTruthy() + expect(tl.objects['child0']).toBeTruthy() + expect(tl.objects['child1']).toBeTruthy() + expect(tl.objects['obj1']).toBeTruthy() + // console.log(tl.objects['group0'].resolved.instances) + expect(tl.objects['group0'].resolved.instances).toMatchObject([ + { start: 990, end: 1015 }, + { start: 1015, end: 1040 }, + { start: 1040, end: 1065 } + ]) + expect(tl.objects['child0'].resolved.instances).toMatchObject([ + { start: 990, end: 1005 }, + { start: 1015, end: 1030 }, + { start: 1040, end: 1055 } + ]) + expect(tl.objects['child1'].resolved.instances).toMatchObject([ + { start: 1005, end: 1015 }, + { start: 1030, end: 1040 }, + { start: 1055, end: 1065 } + ]) + expect(tl.objects['obj1'].resolved.instances).toMatchObject([ + { start: 1015, end: 1021 }, + { start: 1040, end: 1046 }, + { start: 1065, end: 1071 } + ]) + expect(tl.objects['child1']).toBeTruthy() + expect(tl.objects['child1'].resolved.instances[0].start).toBe(1005) + + const tmpState0 = Resolver.getState(tl, now, 99) + const events0 = tmpState0.nextEvents + expect(events0).toMatchObject([ + // { time: 990, type: EventType.START, objId: 'child0' }, + // { time: 990, type: EventType.START, objId: 'group0' }, + + { time: 1005, type: EventType.END, objId: 'child0' }, + { time: 1005, type: EventType.START, objId: 'child1' }, + { time: 1015, type: EventType.END, objId: 'child1' }, + { time: 1015, type: EventType.END, objId: 'group0' }, + { time: 1015, type: EventType.START, objId: 'child0' }, + { time: 1015, type: EventType.START, objId: 'group0' }, + { time: 1015, type: EventType.START, objId: 'obj1' }, + { time: 1021, type: EventType.END, objId: 'obj1' }, + { time: 1030, type: EventType.END, objId: 'child0' }, + { time: 1030, type: EventType.START, objId: 'child1' }, + { time: 1040, type: EventType.END, objId: 'child1' }, + { time: 1040, type: EventType.END, objId: 'group0' }, + { time: 1040, type: EventType.START, objId: 'child0' }, + { time: 1040, type: EventType.START, objId: 'group0' }, + { time: 1040, type: EventType.START, objId: 'obj1' }, + { time: 1046, type: EventType.END, objId: 'obj1' }, + { time: 1055, type: EventType.END, objId: 'child0' }, + { time: 1055, type: EventType.START, objId: 'child1' }, + { time: 1065, type: EventType.END, objId: 'child1' }, + { time: 1065, type: EventType.END, objId: 'group0' }, + { time: 1065, type: EventType.START, objId: 'obj1' }, + { time: 1071, type: EventType.END, objId: 'obj1' } + ]) + + expect(Resolver.getState(tl, now).layers).toMatchObject({ + '1': { + id: 'child0' + } + }) + expect(Resolver.getState(tl, now + 10).layers).toMatchObject({ + '1': { + id: 'child1' + } + }) // Next loop: - const tld2 = Resolver.developTimelineAroundTime(tl, now + 25) - expect(tld2.resolved).toHaveLength(3) - expect(tld2.resolved[0].id).toBe('child0') - expect(tld2.resolved[0].resolved.startTime).toBe(1015) - expect(tld2.resolved[1].id).toBe('child1') - expect(tld2.resolved[1].resolved.startTime).toBe(1030) - const events2 = Resolver.getNextEvents(tl, now + 25) - expect(events2).toHaveLength(5) - expect(events2[0]).toMatchObject({ - type: EventType.END, time: 1030, obj: { id: 'child0' }}) - expect(events2[1]).toMatchObject({ - type: EventType.START, time: 1030, obj: { id: 'child1' }}) - expect(events2[2]).toMatchObject({ - type: EventType.END, time: 1040, obj: { id: 'child1' }}) - - const state2 = Resolver.getState(tl, now + 25) - expect(state2.LLayers['1']).toBeTruthy() - expect(state2.LLayers['1'].id).toBe('child0') - - const state3 = Resolver.getState(tl, now + 35) - expect(state3.LLayers['1']).toBeTruthy() - expect(state3.LLayers['1'].id).toBe('child1') - const events3 = Resolver.getNextEvents(tl, now + 35) - expect(events3).toHaveLength(5) - - // just before group0 is done: - // let tld4 = Resolver.developTimelineAroundTime(tl, now + 50) - const events4 = Resolver.getNextEvents(tl, now + 50) - expect(events4[0]).toMatchObject({ - type: EventType.END, time: 1053, obj: { id: 'child0' }}) - expect(events4[1]).toMatchObject({ - type: EventType.START, time: 1053, obj: { id: 'obj1' }}) - expect(events4[2]).toMatchObject({ - type: EventType.END, time: 1070, obj: { id: 'obj1' }}) + expect(Resolver.getState(tl, now + 25).layers).toMatchObject({ + '1': { + id: 'child0' + } + }) + expect(Resolver.getState(tl, now + 35).layers).toMatchObject({ + '1': { + id: 'child1' + } + }) expect(data).toEqual(getTestData('repeatinggroup')) // Make sure the original data is unmodified - }, 'repeating group in repeating group': () => { const data = clone(getTestData('repeatinggroupinrepeatinggroup')) + const tl = Resolver.resolveTimeline(data, _.extend(stdOpts, { limitCount: 99, limitTime: 1150 })) + expect(tl.statistics.resolvedObjectCount).toEqual(6) + expect(tl.statistics.unresolvedCount).toEqual(0) + + expect(tl.objects['group0']).toBeTruthy() + expect(tl.objects['child0']).toBeTruthy() + expect(tl.objects['group1']).toBeTruthy() + expect(tl.objects['child1']).toBeTruthy() + expect(tl.objects['child2']).toBeTruthy() + expect(tl.objects['obj1']).toBeTruthy() + // console.log(tl.objects['group0'].resolved.instances) + expect(tl.objects['group0'].resolved.instances).toMatchObject([ + { start: 1000, end: 1092 }, + { start: 1092, end: 1184 } + ]) + expect(tl.objects['child0'].resolved.instances).toMatchObject([ + { start: 1000, end: 1030 }, + { start: 1092, end: 1122 } + ]) + expect(tl.objects['group1'].resolved.instances).toMatchObject([ + { start: 1030, end: 1055 }, + { start: 1055, end: 1080 }, + { start: 1080, end: 1092 }, // capped in parent + // received: 1105 1130 + // next loop: + { start: 1122, end: 1147 }, + { start: 1147, end: null } // 1172 + // { start: 1172, end: 1197 } // capped in parent + ]) + expect(tl.objects['child1'].resolved.instances).toMatchObject([ + { start: 1030, end: 1040 }, + { start: 1055, end: 1065 }, + { start: 1080, end: 1090 }, + { start: 1122, end: 1132 }, + { start: 1147, end: 1157 }, + { start: 1172, end: 1182 } + ]) + expect(tl.objects['child1'].resolved.instances).toMatchObject([ + { start: 1040, end: 1055 }, + { start: 1065, end: 1080 }, + { start: 1090, end: 1105 }, + { start: 1132, end: 1147 }, + { start: 1157, end: 1172 }, + { start: 1182, end: 1197 } + ]) + expect(tl.objects['obj1'].resolved.instances).toMatchObject([ + { start: 1092, end: 1152 }, + { start: 1184, end: 1244 } + ]) + expect(tl.objects['child1']).toBeTruthy() + expect(tl.objects['child1'].resolved.instances[0].start).toBe(1005) + + const tmpState0 = Resolver.getState(tl, now, 99) + const events0 = tmpState0.nextEvents + expect(events0).toMatchObject([ + // { time: 990, type: EventType.START, objId: 'child0' }, + // { time: 990, type: EventType.START, objId: 'group0' }, + + { time: 1005, type: EventType.END, objId: 'child0' }, + { time: 1005, type: EventType.START, objId: 'child1' }, + { time: 1015, type: EventType.END, objId: 'child1' }, + { time: 1015, type: EventType.END, objId: 'group0' }, + { time: 1015, type: EventType.START, objId: 'child0' }, + { time: 1015, type: EventType.START, objId: 'group0' }, + { time: 1015, type: EventType.START, objId: 'obj1' }, + { time: 1021, type: EventType.END, objId: 'obj1' }, + { time: 1030, type: EventType.END, objId: 'child0' }, + { time: 1030, type: EventType.START, objId: 'child1' }, + { time: 1040, type: EventType.END, objId: 'child1' }, + { time: 1040, type: EventType.END, objId: 'group0' }, + { time: 1040, type: EventType.START, objId: 'child0' }, + { time: 1040, type: EventType.START, objId: 'group0' }, + { time: 1040, type: EventType.START, objId: 'obj1' }, + { time: 1046, type: EventType.END, objId: 'obj1' }, + { time: 1055, type: EventType.END, objId: 'child0' }, + { time: 1055, type: EventType.START, objId: 'child1' }, + { time: 1065, type: EventType.END, objId: 'child1' }, + { time: 1065, type: EventType.END, objId: 'group0' }, + { time: 1065, type: EventType.START, objId: 'obj1' }, + { time: 1071, type: EventType.END, objId: 'obj1' } + ]) + + expect(Resolver.getState(tl, now).layers).toMatchObject({ + '1': { + id: 'child0' + } + }) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(2) - expect(tl.unresolved).toHaveLength(0) + expect(Resolver.getState(tl, now + 10).layers).toMatchObject({ + '1': { + id: 'child1' + } + }) + // Next loop: + + expect(Resolver.getState(tl, now + 25).layers).toMatchObject({ + '1': { + id: 'child0' + } + }) + expect(Resolver.getState(tl, now + 35).layers).toMatchObject({ + '1': { + id: 'child1' + } + }) + expect(data).toEqual(getTestData('repeatinggroup')) // Make sure the original data is unmodified + /* + expect(tl.statistics.resolvedObjectCount).toEqual(2) + expect(tl.statistics.unresolvedCount).toEqual(0) expect(data).toEqual(getTestData('repeatinggroupinrepeatinggroup')) // Make sure the original data is unmodified @@ -2640,67 +2983,72 @@ let tests: Tests = { id: 'obj1', resolved: { startTime: 1300 }}) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toBe('child0') + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toBe('child0') expect(data).toEqual(getTestData('repeatinggroupinrepeatinggroup')) // Make sure the original data is unmodified - const events0 = Resolver.getNextEvents(tl, now) + const tmpState = Resolver.getState(tl, now) + const events0 = tmpState.nextEvents expect(events0).toHaveLength(8) expect(events0[0]).toMatchObject({ - type: EventType.START, time: 1000, obj: { id: 'child0' }}) + type: EventType.START, time: 1000, objId: 'child0' }) expect(events0[1]).toMatchObject({ - type: EventType.END, time: 1030, obj: { id: 'child0' }}) + type: EventType.END, time: 1030, objId: 'child0' }) expect(events0[2]).toMatchObject({ - type: EventType.START, time: 1030, obj: { id: 'child1' }}) + type: EventType.START, time: 1030, objId: 'child1' }) expect(events0[3]).toMatchObject({ - type: EventType.END, time: 1040, obj: { id: 'child1' }}) + type: EventType.END, time: 1040, objId: 'child1' }) expect(events0[4]).toMatchObject({ - type: EventType.START, time: 1040, obj: { id: 'child2' }}) + type: EventType.START, time: 1040, objId: 'child2' }) expect(events0[5]).toMatchObject({ - type: EventType.END, time: 1055, obj: { id: 'child2' }}) + type: EventType.END, time: 1055, objId: 'child2' }) expect(events0[6]).toMatchObject({ - type: EventType.START, time: 1300, obj: { id: 'obj1' }}) + type: EventType.START, time: 1300, objId: 'obj1' }) expect(events0[7]).toMatchObject({ - type: EventType.END, time: 1360, obj: { id: 'obj1' }}) + type: EventType.END, time: 1360, objId: 'obj1' }) // a bit in: const state1 = Resolver.getState(tl, now + 50) - expect(state1.LLayers['1']).toBeTruthy() - expect(state1.LLayers['1'].id).toBe('child2') + expect(state1.layers['1']).toBeTruthy() + expect(state1.layers['1'].id).toBe('child2') - const events1 = Resolver.getNextEvents(tl, now + 50) + const tmpState = Resolver.getState(tl, now + 50) + const events1 = tmpState.nextEvents expect(events1).toHaveLength(7) expect(events1[0]).toMatchObject({ - type: EventType.END, time: 1055, obj: { id: 'child2' }}) + type: EventType.END, time: 1055, objId: 'child2' }) expect(events1[1]).toMatchObject({ - type: EventType.START, time: 1055, obj: { id: 'child1' }}) + type: EventType.START, time: 1055, objId: 'child1' }) expect(events1[2]).toMatchObject({ - type: EventType.END, time: 1065, obj: { id: 'child1' }}) + type: EventType.END, time: 1065, objId: 'child1' }) // just before group1 is done playing: const state2 = Resolver.getState(tl, now + 91) - expect(state2.LLayers['1']).toBeTruthy() - expect(state2.LLayers['1'].id).toBe('child2') + expect(state2.layers['1']).toBeTruthy() + expect(state2.layers['1'].id).toBe('child2') - const events2 = Resolver.getNextEvents(tl, now + 91) + const tmpState = Resolver.getState(tl, now + 91) + const events2 = tmpState.nextEvents expect(events2[0]).toMatchObject({ - type: EventType.END, time: 1092, obj: { id: 'child2' }}) + type: EventType.END, time: 1092, objId: 'child2' }) expect(events2[1]).toMatchObject({ - type: EventType.START, time: 1092, obj: { id: 'child0' }}) + type: EventType.START, time: 1092, objId: 'child0' }) expect(events2[2]).toMatchObject({ - type: EventType.END, time: 1122, obj: { id: 'child0' }}) + type: EventType.END, time: 1122, objId: 'child0' }) expect(data).toEqual(getTestData('repeatinggroupinrepeatinggroup')) // Make sure the original data is unmodified + */ }, + /* 'test group with duration and infinite child': () => { const data = clone(getTestData('groupwithduration')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(0) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(0) const tld0 = Resolver.developTimelineAroundTime(tl, now) @@ -2712,127 +3060,135 @@ let tests: Tests = { id: 'child0', resolved: { startTime: 1000 }}) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toBe('child0') + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toBe('child0') - const events0 = Resolver.getNextEvents(tl, now) + const tmpState = Resolver.getState(tl, now) + const events0 = tmpState.nextEvents expect(events0).toHaveLength(2) expect(events0[0]).toMatchObject({ - type: EventType.START, time: 1000, obj: { id: 'child0' }}) + type: EventType.START, time: 1000, objId: 'child0' }) expect(events0[1]).toMatchObject({ - type: EventType.END, time: 1030, obj: { id: 'child0' }}) + type: EventType.END, time: 1030, objId: 'child0' }) // a bit in: const state1 = Resolver.getState(tl, now + 50) - expect(state1.LLayers['1']).toBeFalsy() + expect(state1.layers['1']).toBeFalsy() - const events1 = Resolver.getNextEvents(tl, now + 50) + const tmpState = Resolver.getState(tl, now + 50) + const events1 = tmpState.nextEvents expect(events1).toHaveLength(0) // just before group1 is done playing: const state2 = Resolver.getState(tl, now + 29) - expect(state2.LLayers['1']).toBeTruthy() - expect(state2.LLayers['1'].id).toBe('child0') + expect(state2.layers['1']).toBeTruthy() + expect(state2.layers['1'].id).toBe('child0') - const events2 = Resolver.getNextEvents(tl, now + 29) + const tmpState = Resolver.getState(tl, now + 29) + const events2 = tmpState.nextEvents expect(events2[0]).toMatchObject({ - type: EventType.END, time: 1030, obj: { id: 'child0' }}) + type: EventType.END, time: 1030, objId: 'child0' }) expect(data).toEqual(getTestData('groupwithduration')) // Make sure the original data is unmodified }, 'infinite group': () => { const data = clone(getTestData('infinitegroup')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(0) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(0) const tld = Resolver.developTimelineAroundTime(tl, now) expect(tld.resolved).toHaveLength(2) expect(tld.unresolved).toHaveLength(0) - const events0 = Resolver.getNextEvents(tl, now) + const tmpState = Resolver.getState(tl, now) + const events0 = tmpState.nextEvents expect(events0).toHaveLength(3) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toBe('child0') + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toBe('child0') expect(data).toEqual(getTestData('infinitegroup')) // Make sure the original data is unmodified }, 'logical objects in group': () => { const data = clone(getTestData('logicalInGroup')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(1) - expect(tl.unresolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(1) + expect(tl.statistics.unresolvedCount).toEqual(2) const tld = Resolver.developTimelineAroundTime(tl, now) expect(tld.resolved).toHaveLength(1) expect(tld.unresolved).toHaveLength(2) - const events0 = Resolver.getNextEvents(tl, now) + const tmpState = Resolver.getState(tl, now) + const events0 = tmpState.nextEvents expect(events0).toHaveLength(0) // Resolver.setTraceLevel(TraceLevel.TRACE) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['2']).toBeTruthy() - expect(state0.GLayers['2']).toBeTruthy() - expect(state0.LLayers['2'].id).toBe('child0') + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2'].id).toBe('child0') - expect(state0.LLayers['3']).toBeTruthy() - expect(state0.GLayers['3']).toBeTruthy() - expect(state0.LLayers['3'].id).toBe('outside0') + expect(state0.layers['3']).toBeTruthy() + expect(state0.layers['3']).toBeTruthy() + expect(state0.layers['3'].id).toBe('outside0') expect(data).toEqual(getTestData('logicalInGroup')) // Make sure the original data is unmodified }, 'logical objects in group with logical expr': () => { const data = clone(getTestData('logicalInGroupLogical')) - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) expect(data).toEqual(getTestData('logicalInGroupLogical')) // Make sure the original data is unmodified - // expect(tl.resolved).toHaveLength(1) - // expect(tl.unresolved).toHaveLength(1) + // expect(tl.statistics.resolvedObjectCount).toEqual(1) + // expect(tl.statistics.unresolvedCount).toEqual(1) // let tld = Resolver.developTimelineAroundTime(tl, now) // expect(tld.resolved).toHaveLength(2) // expect(tld.unresolved).toHaveLength(1) - // let events0 = Resolver.getNextEvents(tl, now) + const tmpState = Resolver.getState(tl, now) + // let events0 = tmpState.nextEvents // expect(events0).toHaveLength(0) // Resolver.setTraceLevel(TraceLevel.TRACE) const state0 = Resolver.getState(tl, now) - expect(state0.LLayers['2']).toBeTruthy() - expect(state0.GLayers['2']).toBeTruthy() - expect(state0.LLayers['2'].id).toBe('child0') + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2']).toBeTruthy() + expect(state0.layers['2'].id).toBe('child0') - expect(state0.LLayers['4']).toBeFalsy() - expect(state0.GLayers['4']).toBeFalsy() + expect(state0.layers['4']).toBeFalsy() + expect(state0.layers['4']).toBeFalsy() expect(data).toEqual(getTestData('logicalInGroupLogical')) // Make sure the original data is unmodified }, 'keyframe in a grouped object': () => { const data = clone(getTestData('keyframeingroup')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(1) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(1) - const events = Resolver.getNextEvents(data, 1000, 4) + const tmpState = Resolver.getState(tla, 1000, 4) + const events = tmpState.nextEvents expect(events.length).toEqual(4) expect(events[1].time).toEqual(1010) expect(events[2].time).toEqual(1020) - const state0 = Resolver.getState(data, 1015) + const state0 = Resolver.getState(tl, 1015) - expect(state0.LLayers['2']).toBeTruthy() - const obj2 = state0.LLayers['2'] + expect(state0.layers['2']).toBeTruthy() + const obj2 = state0.layers['2'] expect(obj2.resolved.mixer.opacity).toEqual(0) }, 'relative durations': () => { const data = clone(getTestData('relativeduration0')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(4) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(4) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(8) expect(events[0].time).toEqual(1000) expect(events[1].time).toEqual(1060) @@ -2843,95 +3199,96 @@ let tests: Tests = { expect(events[6].time).toEqual(1180) expect(events[7].time).toEqual(5400) - const state0 = Resolver.getState(data, 1030) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toEqual('obj0') + const state0 = Resolver.getState(tl, 1030) + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toEqual('obj0') - const state1 = Resolver.getState(data, 1070) - expect(state1.LLayers['1']).toBeTruthy() - expect(state1.LLayers['1'].id).toEqual('obj1') + const state1 = Resolver.getState(tl, 1070) + expect(state1.layers['1']).toBeTruthy() + expect(state1.layers['1'].id).toEqual('obj1') - const state2 = Resolver.getState(data, 1100) - expect(state2.LLayers['1']).toBeTruthy() - expect(state2.LLayers['1'].id).toEqual('obj2') + const state2 = Resolver.getState(tl, 1100) + expect(state2.layers['1']).toBeTruthy() + expect(state2.layers['1'].id).toEqual('obj2') - const state3 = Resolver.getState(data, 1190) - expect(state3.LLayers['1']).toBeTruthy() - expect(state3.LLayers['1'].id).toEqual('obj3') + const state3 = Resolver.getState(tl, 1190) + expect(state3.layers['1']).toBeTruthy() + expect(state3.layers['1'].id).toEqual('obj3') - const state4 = Resolver.getState(data, 5390) - expect(state4.LLayers['1']).toBeTruthy() - expect(state4.LLayers['1'].id).toEqual('obj3') + const state4 = Resolver.getState(tl, 5390) + expect(state4.layers['1']).toBeTruthy() + expect(state4.layers['1'].id).toEqual('obj3') - const state5 = Resolver.getState(data, 5401) - expect(state5.LLayers['1']).toBeFalsy() + const state5 = Resolver.getState(tl, 5401) + expect(state5.layers['1']).toBeFalsy() }, 'Circular dependency 1': () => { // console.log('======') // Resolver.setTraceLevel(TraceLevel.TRACE) const data = clone(getTestData('circulardependency0')) - const tl = Resolver.getTimelineInWindow(data) + const tl = Resolver.resolveTimeline(data, stdOpts) // Resolver.setTraceLevel(TraceLevel.ERRORS) - expect(tl.resolved).toHaveLength(0) + expect(tl.statistics.resolvedObjectCount).toEqual(0) // const obj0: TimelineResolvedObject = _.findWhere(data, { id: 'obj0' }) // obj0.duration = 10 // break the circular dependency - // const tl2 = Resolver.getTimelineInWindow(data) + // const tl2 = Resolver.resolveTimeline(data, stdOpts) // expect(tl2.resolved).toHaveLength(3) }, 'Circular dependency 2': () => { const data = clone(getTestData('circulardependency1')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(0) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(0) const obj0: TimelineResolvedObject = _.findWhere(data, { id: 'obj0' }) obj0.duration = 10 // break the circular dependency - const tl2 = Resolver.getTimelineInWindow(data) + const tl2 = Resolver.resolveTimeline(data, stdOpts) expect(tl2.resolved).toHaveLength(3) }, 'relative durations object order': () => { const data = clone(getTestData('relativedurationorder0')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const tld = Resolver.developTimelineAroundTime(tl, now) expect(tld.resolved).toHaveLength(2) const child0: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child0' }) const child1: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child1' }) - expect(child0.resolved.startTime).toBe(1000) - expect(child0.resolved.endTime).toBe(1150) - expect(child1.resolved.startTime).toBe(1150) - expect(child1.resolved.endTime).toBe(Infinity) + expect(child0.resolved.instances[0].start).toBe(1000) + expect(child0.resolved.instances[0].end).toBe(1150) + expect(child1.resolved.instances[0].start).toBe(1150) + expect(child1.resolved.instances[0].end).toBe(Infinity) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(3) expect(events[0].time).toEqual(1000) expect(events[1].time).toEqual(1150) expect(events[2].time).toEqual(1150) - const state0 = Resolver.getState(data, 1030) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toEqual('child0') - expect(state0.LLayers['2']).toBeFalsy() + const state0 = Resolver.getState(tl, 1030) + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toEqual('child0') + expect(state0.layers['2']).toBeFalsy() - const state1 = Resolver.getState(data, 1170) - expect(state1.LLayers['2']).toBeTruthy() - expect(state1.LLayers['2'].id).toEqual('child1') - expect(state1.LLayers['1']).toBeFalsy() + const state1 = Resolver.getState(tl, 1170) + expect(state1.layers['2']).toBeTruthy() + expect(state1.layers['2'].id).toEqual('child1') + expect(state1.layers['1']).toBeFalsy() }, 'Cross-dependencies between group children': () => { const data = clone(getTestData('dependenciesBetweengroupchildren')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) const group1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group1' }) - expect(group0.resolved.startTime).toBe(1000) - expect(group1.resolved.startTime).toBe(1030) + expect(group0.resolved.instances[0].start).toBe(1000) + expect(group1.resolved.instances[0].start).toBe(1030) const tld = Resolver.developTimelineAroundTime(tl, now) expect(tld.resolved).toHaveLength(3) @@ -2940,14 +3297,15 @@ let tests: Tests = { const child1: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child1' }) const child2: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child2' }) - expect(child0.resolved.startTime).toBe(1000) - expect(child0.resolved.endTime).toBe(1010) - expect(child1.resolved.startTime).toBe(1010) - expect(child1.resolved.endTime).toBe(1030) - expect(child2.resolved.startTime).toBe(1030) - expect(child2.resolved.endTime).toBe(1060) + expect(child0.resolved.instances[0].start).toBe(1000) + expect(child0.resolved.instances[0].end).toBe(1010) + expect(child1.resolved.instances[0].start).toBe(1010) + expect(child1.resolved.instances[0].end).toBe(1030) + expect(child2.resolved.instances[0].start).toBe(1030) + expect(child2.resolved.instances[0].end).toBe(1060) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(6) expect(events[0].time).toEqual(1000) expect(events[1].time).toEqual(1010) @@ -2956,31 +3314,31 @@ let tests: Tests = { expect(events[4].time).toEqual(1030) expect(events[5].time).toEqual(1060) - const state0 = Resolver.getState(data, 1005) - expect(state0.LLayers['1']).toBeTruthy() - expect(state0.LLayers['1'].id).toEqual('child0') + const state0 = Resolver.getState(tl, 1005) + expect(state0.layers['1']).toBeTruthy() + expect(state0.layers['1'].id).toEqual('child0') - const state1 = Resolver.getState(data, 1015) - expect(state1.LLayers['1']).toBeTruthy() - expect(state1.LLayers['1'].id).toEqual('child1') + const state1 = Resolver.getState(tl, 1015) + expect(state1.layers['1']).toBeTruthy() + expect(state1.layers['1'].id).toEqual('child1') - const state2 = Resolver.getState(data, 1040) - expect(state2.LLayers['1']).toBeTruthy() - expect(state2.LLayers['1'].id).toEqual('child2') + const state2 = Resolver.getState(tl, 1040) + expect(state2.layers['1']).toBeTruthy() + expect(state2.layers['1'].id).toEqual('child2') }, 'self-reference expression': () => { const data = clone(getTestData('selfreferenceexpression0')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const obj0 = _.findWhere(tl.resolved, { id: 'obj0' }) const obj1 = _.findWhere(tl.resolved, { id: 'obj1' }) - expect(obj0.resolved.startTime).toEqual(1000) - expect(obj0.resolved.endTime).toEqual(1010) + expect(obj0.resolved.instances[0].start).toEqual(1000) + expect(obj0.resolved.instances[0].end).toEqual(1010) - expect(obj1.resolved.startTime).toEqual(1005) - expect(obj1.resolved.endTime).toEqual(1010) + expect(obj1.resolved.instances[0].start).toEqual(1005) + expect(obj1.resolved.instances[0].end).toEqual(1010) }, 'large dataset': () => { // worst-case dataset: only relative objects, in random order @@ -3006,31 +3364,31 @@ let tests: Tests = { } data = _.sortBy(data, Math.random) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.resolved).toHaveLength(size) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.resolvedObjectCount).toEqual(size) // const obj0 = _.findWhere(tl.resolved, { id: 'obj0' }) // const obj1 = _.findWhere(tl.resolved, { id: 'obj1' }) - // expect(obj0.resolved.startTime).toEqual(1000) - // expect(obj0.resolved.endTime).toEqual(1010) + // expect(obj0.resolved.instances[0].start).toEqual(1000) + // expect(obj0.resolved.instances[0].end).toEqual(1010) - // expect(obj1.resolved.startTime).toEqual(1005) - // expect(obj1.resolved.endTime).toEqual(1010) + // expect(obj1.resolved.instances[0].start).toEqual(1005) + // expect(obj1.resolved.instances[0].end).toEqual(1010) }, 'Relative start order': () => { const data = clone(getTestData('relativeStartOrder0')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) const trans0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'trans0' }) - expect(group0.resolved.startTime).toBe(1000) + expect(group0.resolved.instances[0].start).toBe(1000) expect(group0.resolved.outerDuration).toBe(Infinity) - expect(trans0.resolved.startTime).toBe(1000) + expect(trans0.resolved.instances[0].start).toBe(1000) expect(trans0.resolved.outerDuration).toBe(2500) const tld = Resolver.developTimelineAroundTime(tl, 1500) @@ -3039,40 +3397,41 @@ let tests: Tests = { const child0: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child0' }) const child1: TimelineResolvedObject = _.findWhere(tld.resolved, { id: 'child1' }) - expect(child0.resolved.startTime).toBe(2500) + expect(child0.resolved.instances[0].start).toBe(2500) expect(child0.resolved.outerDuration).toBe(Infinity) - expect(child1.resolved.startTime).toBe(1000) + expect(child1.resolved.instances[0].start).toBe(1000) expect(child1.resolved.outerDuration).toBe(Infinity) - const state0 = Resolver.getState(data, 1500) - expect(state0.LLayers['3']).toBeTruthy() - expect(state0.LLayers['3'].id).toEqual('child1') + const state0 = Resolver.getState(tl, 1500) + expect(state0.layers['3']).toBeTruthy() + expect(state0.layers['3'].id).toEqual('child1') - const state1 = Resolver.getState(data, 3500) - expect(state1.LLayers['3']).toBeTruthy() - expect(state1.LLayers['3'].id).toEqual('child0') + const state1 = Resolver.getState(tl, 3500) + expect(state1.layers['3']).toBeTruthy() + expect(state1.layers['3'].id).toEqual('child0') - const state2 = Resolver.getState(data, 4500) - expect(state2.LLayers['3']).toBeTruthy() - expect(state2.LLayers['3'].id).toEqual('child0') + const state2 = Resolver.getState(tl, 4500) + expect(state2.layers['3']).toBeTruthy() + expect(state2.layers['3'].id).toEqual('child0') }, 'Relative with something past the end': () => { const data = clone(getTestData('relativePastEnd')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) const group1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group1' }) - expect(group0.resolved.startTime).toBe(1000) + expect(group0.resolved.instances[0].start).toBe(1000) expect(group0.resolved.outerDuration).toBe(3100) - expect(group1.resolved.startTime).toBe(4000) + expect(group1.resolved.instances[0].start).toBe(4000) expect(group1.resolved.outerDuration).toBe(Infinity) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(3) expect(events[0].obj.id).toEqual('child1') expect(events[0].time).toEqual(1000) @@ -3081,26 +3440,27 @@ let tests: Tests = { expect(events[2].obj.id).toEqual('child1') expect(events[2].time).toEqual(4100) - const state0 = Resolver.getState(data, 5500) - expect(state0.LLayers['4']).toBeFalsy() - expect(state0.LLayers['6']).toBeFalsy() + const state0 = Resolver.getState(tl, 5500) + expect(state0.layers['4']).toBeFalsy() + expect(state0.layers['6']).toBeFalsy() }, 'Child relative duration before parent end': () => { const data = clone(getTestData('childEndRelativeParentEnd')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) const group1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group1' }) - expect(group0.resolved.startTime).toBe(1000) + expect(group0.resolved.instances[0].start).toBe(1000) expect(group0.resolved.outerDuration).toBe(5600) - expect(group1.resolved.startTime).toBe(6000) + expect(group1.resolved.instances[0].start).toBe(6000) expect(group1.resolved.outerDuration).toBe(Infinity) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(4) expect(events[0].obj.id).toEqual('child1') expect(events[0].time).toEqual(1000) @@ -3111,26 +3471,27 @@ let tests: Tests = { expect(events[3].obj.id).toEqual('child0') expect(events[3].time).toEqual(9600) - const state0 = Resolver.getState(data, 6500) - expect(state0.LLayers['4']).toBeFalsy() - expect(state0.LLayers['8']).toBeTruthy() + const state0 = Resolver.getState(tl, 6500) + expect(state0.layers['4']).toBeFalsy() + expect(state0.layers['8']).toBeTruthy() }, 'Relative duration resolving to zero length': () => { const data = clone(getTestData('relativeDurationZeroLength')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) const group1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group1' }) - expect(group0.resolved.startTime).toBe(1000) + expect(group0.resolved.instances[0].start).toBe(1000) expect(group0.resolved.outerDuration).toBe(Infinity) - expect(group1.resolved.startTime).toBe(1000) + expect(group1.resolved.instances[0].start).toBe(1000) expect(group1.resolved.outerDuration).toBe(Infinity) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(3) expect(events[0].obj.id).toEqual('child0') expect(events[0].time).toEqual(1000) @@ -3139,30 +3500,30 @@ let tests: Tests = { expect(events[2].obj.id).toEqual('child0') expect(events[2].time).toEqual(1000) - const state0 = Resolver.getState(data, 1500) - expect(state0.LLayers['3']).toBeTruthy() - expect(state0.LLayers['3'].id).toEqual('child1') + const state0 = Resolver.getState(tl, 1500) + expect(state0.layers['3']).toBeTruthy() + expect(state0.layers['3'].id).toEqual('child1') }, 'Many parentheses': () => { const data = clone(getTestData('manyParentheses')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const obj0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj0' }) const obj1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj1' }) - expect(obj0.resolved.startTime).toBe(4000) + expect(obj0.resolved.instances[0].start).toBe(4000) expect(obj0.resolved.outerDuration).toBe(Infinity) - expect(obj1.resolved.startTime).toBe(1000) + expect(obj1.resolved.instances[0].start).toBe(1000) expect(obj1.resolved.outerDuration).toBe(1000) }, 'Operator order': () => { const data = clone(getTestData('operatorOrder')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(7) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(7) const obj0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj0' }) const obj1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj1' }) @@ -3172,39 +3533,40 @@ let tests: Tests = { const obj5: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj5' }) const obj6: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj6' }) - expect(obj0.resolved.startTime).toBe(4000) + expect(obj0.resolved.instances[0].start).toBe(4000) expect(obj0.resolved.outerDuration).toBe(Infinity) - expect(obj1.resolved.startTime).toBe(1000) + expect(obj1.resolved.instances[0].start).toBe(1000) expect(obj1.resolved.outerDuration).toBe(1000) - expect(obj2.resolved.startTime).toBe(1000) + expect(obj2.resolved.instances[0].start).toBe(1000) expect(obj2.resolved.outerDuration).toBe(1000) - expect(obj3.resolved.startTime).toBe(1000) + expect(obj3.resolved.instances[0].start).toBe(1000) expect(obj3.resolved.outerDuration).toBe(1000) - expect(obj4.resolved.startTime).toBe(1000) + expect(obj4.resolved.instances[0].start).toBe(1000) expect(obj4.resolved.outerDuration).toBe(5000) - expect(obj5.resolved.startTime).toBe(1000) + expect(obj5.resolved.instances[0].start).toBe(1000) expect(obj5.resolved.outerDuration).toBe(5000) - expect(obj6.resolved.startTime).toBe(1000) + expect(obj6.resolved.instances[0].start).toBe(1000) expect(obj6.resolved.outerDuration).toBe(5000) }, 'Child with a start before parent': () => { const data = clone(getTestData('childWithStartBeforeParent')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(0) - expect(tl.resolved).toHaveLength(1) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(0) + expect(tl.statistics.resolvedObjectCount).toEqual(1) const group0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'group0' }) - expect(group0.resolved.startTime).toBe(6000) + expect(group0.resolved.instances[0].start).toBe(6000) expect(group0.resolved.outerDuration).toBe(Infinity) - const events = Resolver.getNextEvents(data, 1000) + const tmpState = Resolver.getState(tl, 1000) + const events = tmpState.nextEvents expect(events.length).toEqual(2) expect(events[0].obj.id).toEqual('child1') expect(events[0].time).toEqual(6000) @@ -3213,17 +3575,17 @@ let tests: Tests = { }, 'Logical triggers': () => { const data = clone(getTestData('logicalTriggers')) - const tl = Resolver.getTimelineInWindow(data) - expect(tl.unresolved).toHaveLength(6) - expect(tl.resolved).toHaveLength(2) + const tl = Resolver.resolveTimeline(data, stdOpts) + expect(tl.statistics.unresolvedCount).toEqual(6) + expect(tl.statistics.resolvedObjectCount).toEqual(2) const obj0: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj0' }) const obj1: TimelineResolvedObject = _.findWhere(tl.resolved, { id: 'obj1' }) - expect(obj0.resolved.startTime).toBe(1000) + expect(obj0.resolved.instances[0].start).toBe(1000) expect(obj0.resolved.outerDuration).toBe(Infinity) - expect(obj1.resolved.startTime).toBe(4000) + expect(obj1.resolved.instances[0].start).toBe(4000) expect(obj1.resolved.outerDuration).toBe(Infinity) expect(_.findWhere(tl.unresolved, { id: 'obj2' })).toBeTruthy() @@ -3234,49 +3596,50 @@ let tests: Tests = { expect(_.findWhere(tl.unresolved, { id: 'obj7' })).toBeTruthy() // Sample before logical trigger is true - const state0 = Resolver.getState(data, 1500) - expect(state0.LLayers['layer0']).toBeTruthy() - expect(state0.LLayers['layer0'].id).toEqual('obj0') - expect(state0.LLayers['layer1']).toBeFalsy() - expect(state0.LLayers['layer3']).toBeFalsy() - expect(state0.LLayers['layer4']).toBeFalsy() - expect(state0.LLayers['layer5']).toBeFalsy() - expect(state0.LLayers['layer6']).toBeTruthy() - expect(state0.LLayers['layer6'].id).toEqual('obj6') - expect(state0.LLayers['layer7']).toBeTruthy() - expect(state0.LLayers['layer7'].id).toEqual('obj7') + const state0 = Resolver.getState(tl, 1500) + expect(state0.layers['layer0']).toBeTruthy() + expect(state0.layers['layer0'].id).toEqual('obj0') + expect(state0.layers['layer1']).toBeFalsy() + expect(state0.layers['layer3']).toBeFalsy() + expect(state0.layers['layer4']).toBeFalsy() + expect(state0.layers['layer5']).toBeFalsy() + expect(state0.layers['layer6']).toBeTruthy() + expect(state0.layers['layer6'].id).toEqual('obj6') + expect(state0.layers['layer7']).toBeTruthy() + expect(state0.layers['layer7'].id).toEqual('obj7') // Sample after logical trigger is true - const state1 = Resolver.getState(data, 4500) - expect(state1.LLayers['layer0']).toBeTruthy() - expect(state1.LLayers['layer0'].id).toEqual('obj2') - expect(state1.LLayers['layer1']).toBeTruthy() - expect(state1.LLayers['layer1'].id).toEqual('obj1') - expect(state1.LLayers['layer3']).toBeTruthy() - expect(state1.LLayers['layer3'].id).toEqual('obj3') - expect(state1.LLayers['layer4']).toBeTruthy() - expect(state1.LLayers['layer4'].id).toEqual('obj4') - expect(state1.LLayers['layer5']).toBeFalsy() - expect(state1.LLayers['layer6']).toBeFalsy() - expect(state1.LLayers['layer7']).toBeTruthy() - expect(state1.LLayers['layer7'].id).toEqual('obj7') + const state1 = Resolver.getState(tl, 4500) + expect(state1.layers['layer0']).toBeTruthy() + expect(state1.layers['layer0'].id).toEqual('obj2') + expect(state1.layers['layer1']).toBeTruthy() + expect(state1.layers['layer1'].id).toEqual('obj1') + expect(state1.layers['layer3']).toBeTruthy() + expect(state1.layers['layer3'].id).toEqual('obj3') + expect(state1.layers['layer4']).toBeTruthy() + expect(state1.layers['layer4'].id).toEqual('obj4') + expect(state1.layers['layer5']).toBeFalsy() + expect(state1.layers['layer6']).toBeFalsy() + expect(state1.layers['layer7']).toBeTruthy() + expect(state1.layers['layer7'].id).toEqual('obj7') }, 'Logical triggers 2': () => { const data = clone(getTestData('logicalTriggers2')) // Sample before logical trigger is true - const state0 = Resolver.getState(data, 1500) - expect(state0.LLayers['layer0']).toBeTruthy() - expect(state0.LLayers['layer0'].id).toEqual('obj0') - expect(state0.LLayers['layer1_first']).toBeFalsy() + const state0 = Resolver.getState(tl, 1500) + expect(state0.layers['layer0']).toBeTruthy() + expect(state0.layers['layer0'].id).toEqual('obj0') + expect(state0.layers['layer1_first']).toBeFalsy() // Sample after logical trigger is true - const state1 = Resolver.getState(data, 2500) - expect(state1.LLayers['layer0']).toBeTruthy() - expect(state1.LLayers['layer0'].id).toEqual('obj2') - expect(state1.LLayers['layer1_first']).toBeTruthy() - expect(state1.LLayers['layer1_first'].id).toEqual('group0_first') + const state1 = Resolver.getState(tl, 2500) + expect(state1.layers['layer0']).toBeTruthy() + expect(state1.layers['layer0'].id).toEqual('obj2') + expect(state1.layers['layer1_first']).toBeTruthy() + expect(state1.layers['layer1_first'].id).toEqual('group0_first') } + */ } const onlyTests: Tests = {} _.each(tests, (t, key: string) => { @@ -3294,6 +3657,7 @@ describe('All tests', () => { }) }) }) +/* describe('Tests with reversed data', () => { _.each(tests, (t, key) => { test(key, () => { @@ -3303,6 +3667,7 @@ describe('Tests with reversed data', () => { }) }) */ + // TODO: test .useExternalFunctions test('tmp', () => { expect(1).toEqual(1) diff --git a/src/__tests__/lib.spec.ts b/src/__tests__/lib.spec.ts index 1a047795..d61a0bd4 100644 --- a/src/__tests__/lib.spec.ts +++ b/src/__tests__/lib.spec.ts @@ -5,7 +5,9 @@ import { cleanInstances, invertInstances, operateOnArrays, - applyRepeatingInstances + applyRepeatingInstances, + capInstances, + operateOnArraysMulti } from '../lib' describe('lib', () => { @@ -36,13 +38,13 @@ describe('lib', () => { { time: 1, value: true } ])).toEqual([ { time: 1, value: true }, - { time: 2, value: true }, { time: 2, value: false }, + { time: 2, value: true }, { time: 3, value: true }, - { time: 20, value: true }, { time: 20, value: false }, - { time: 100, value: true }, + { time: 20, value: true }, { time: 100, value: false }, + { time: 100, value: true }, { time: 300, value: true } ]) }) @@ -67,6 +69,22 @@ describe('lib', () => { ], true)).toEqual([ { start: 10, end: 70 } ]) + + expect(cleanInstances([ + { start: 10, end: 50 }, + { start: 50, end: 70 } + ], true, true)).toEqual([ // allow zero-width gaps + { start: 10, end: 50 }, + { start: 50, end: 70 } + ]) + + expect(cleanInstances([ + { start: 10, end: 60 }, + { start: 50, end: 70 } + ], true, true)).toEqual([ // allow zero-width gaps + { start: 10, end: 70 } + ]) + expect(cleanInstances([ { start: 10, end: 50 }, { start: 50, end: null } @@ -130,6 +148,7 @@ describe('lib', () => { ]) }) test('invertInstances', () => { + expect(invertInstances([ { start: 10, end: 50 }, { start: 100, end: 110 } @@ -154,6 +173,16 @@ describe('lib', () => { { start: 100, end: 110 } ])).toEqual([ { start: 0, end: 10, isFirst: true }, + { start: 100, end: 100 }, + { start: 110, end: null } + ]) + + expect(invertInstances([ + { start: 10, end: 50 }, + { start: 50, end: 110 } + ])).toEqual([ + { start: 0, end: 10, isFirst: true }, + { start: 50, end: 50 }, // zero-width gap { start: 110, end: null } ]) }) @@ -195,6 +224,33 @@ describe('lib', () => { { start: 101, end: 115 } ]) + }) + test('operateOnArraysMulti', () => { + const plus = (a: number | null, b: number | null): number | null => { + if (a === null || b === null) return null + return a + b + } + + expect(operateOnArraysMulti( + [ + { start: 1, end: 3 }, + { start: 5, end: 7 } + ], + [ + { start: 10, end: 20 }, + { start: 50, end: 60 }, + { start: 60, end: 70 } + ], + plus + )).toEqual([ + { start: 11, end: 13 }, + { start: 15, end: 17 }, + { start: 51, end: 53 }, + { start: 55, end: 57 }, + { start: 61, end: 63 }, + { start: 65, end: 67 } + ]) + }) test('applyRepeatingInstances', () => { expect(applyRepeatingInstances( @@ -250,4 +306,24 @@ describe('lib', () => { ]) }) + test('capInstances', () => { + + expect(capInstances([ + { start: 10, end: 20 }, + { start: 30, end: 40 }, + { start: 50, end: 60 }, + { start: 70, end: 80 }, + { start: 90, end: 100 } + ], [ + { start: 25, end: 55 }, + { start: 60, end: 65 }, + { start: 75, end: 95 } + ])).toEqual([ + { start: 30, end: 40 }, + { start: 50, end: 55 }, // capped + // { start: 60, end: 60 }, // ? + { start: 75, end: 80 }, // capped + { start: 90, end: 95 } // capped + ]) + }) }) diff --git a/src/api/api.ts b/src/api/api.ts index 4509cd27..f72740d8 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -29,6 +29,7 @@ export interface ResolvedTimeline { objects: ResolvedTimelineObjects /** Map of all classes on timeline, maps className to object ids */ classes: {[className: string]: Array} + layers: {[layer: string]: Array} statistics: { /** Number of objects that were unable to resolve */ unresolvedCount: number diff --git a/src/index.ts b/src/index.ts index c741892b..c3eaa834 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,11 @@ export * from './api/enums' -export { Resolver } from './resolver/resolver' +export { + Resolver +} from './resolver/resolver' +export { + validateTimeline, + validateObject, + validateKeyframe +} from './resolver/validate' + export { Resolver as LegacyResolver } from './resolver/legacy' diff --git a/src/lib.ts b/src/lib.ts index dc20c4fc..d3d7b9e1 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -43,8 +43,8 @@ export function sortEvents (events: Array): Array if (a.time > b.time) return 1 if (a.time < b.time) return -1 - if (a.value && !b.value) return -1 - if (!a.value && b.value) return 1 + if (a.value && !b.value) return 1 + if (!a.value && b.value) return -1 return 0 }) } @@ -52,7 +52,11 @@ export function sortEvents (events: Array): Array * Clean up instances, join overlapping etc.. * @param instances */ -export function cleanInstances (instances: Array, allowMerge: boolean): Array { +export function cleanInstances ( + instances: Array, + allowMerge: boolean, + allowZeroGaps: boolean = false +): Array { if (allowMerge) { const events: Array> = [] @@ -83,21 +87,26 @@ export function cleanInstances (instances: Array, allowM } const lastInstance = _.last(returnInstances) if (_.keys(activeInstances).length) { + // Instance is active if ( + !allowZeroGaps && lastInstance && lastInstance.end === event.time ) { + // resume previous instance: lastInstance.end = null } else if ( !lastInstance || lastInstance.end !== null ) { + // Start a new instance: returnInstances.push({ start: event.time, end: null }) } } else { + // No instances are active if (lastInstance) { lastInstance.end = event.time } @@ -146,10 +155,12 @@ export function cleanInstances (instances: Array, allowM } } -export function invertInstances (instances: Array): Array { +export function invertInstances ( + instances: Array +): Array { if (instances.length) { - instances = cleanInstances(instances, true) + instances = cleanInstances(instances, true, true) const invertedInstances: Array = [] if (instances[0].start !== 0) { invertedInstances.push({ @@ -170,10 +181,7 @@ export function invertInstances (instances: Array): Arra }) } }) - return _.compact(_.map(invertedInstances, (instance) => { - if (instance.end === instance.start) return null - return instance - })) + return invertedInstances } else { return [{ isFirst: true, @@ -182,6 +190,12 @@ export function invertInstances (instances: Array): Arra }] } } +/** + * Perform an action on 2 arrays. Behaves somewhat like the ".*"-operator in Matlab + * @param array0 + * @param array1 + * @param operate + */ export function operateOnArrays ( array0: Array | number | null, array1: Array | number | null, @@ -239,6 +253,41 @@ export function operateOnArrays ( } return cleanInstances(result, false) } +/** + * Like operateOnArrays, but will multiply the number of elements in array0, with the number of elements in array1 + * @param array0 + * @param array1 + * @param operate + */ +export function operateOnArraysMulti ( + array0: Array | number | null, + array1: Array | number | null, + operate: (a: number | null, b: number | null) => number | null +) { + if (array0 === null) return null + + if (_.isArray(array1)) { + let resultArray: Array = [] + _.each(array1, (array1Val) => { + const result = operateOnArrays(array0, array1Val.start, operate) + if (_.isArray(result)) { + resultArray = resultArray.concat(result) + } else if (result !== null) { + resultArray.push({ + start: result, + end: ( + array1Val.end !== null ? + result + (array1Val.end - array1Val.start) : + null + ) + }) + } + }) + return resultArray + } else { + return operateOnArrays(array0, array1, operate) + } +} export function applyRepeatingInstances ( instances: number | TimelineObjectInstance[] | null, repeatTime0: number | null, @@ -315,6 +364,21 @@ export function capInstances (instances: TimelineObjectInstance[], parentInstanc } } }) + if (!parent) { + _.each(parentInstances, (p) => { + if ( + (instance.end || Infinity) > p.start && + (instance.end || Infinity) <= (p.end || Infinity) + ) { + if ( + parent === null || + (p.end || Infinity) < (parent.end || Infinity) + ) { + parent = p + } + } + }) + } if (parent) { const i2 = _.clone(instance) if ( @@ -323,6 +387,10 @@ export function capInstances (instances: TimelineObjectInstance[], parentInstanc ) { i2.end = parent.end } + if ((i2.start || Infinity) < parent.start) { + i2.start = parent.start + } + returnInstances.push(i2) } }) diff --git a/src/resolver/__tests__/resolver.spec.ts b/src/resolver/__tests__/resolver.spec.ts index 497c836c..d5a179ac 100644 --- a/src/resolver/__tests__/resolver.spec.ts +++ b/src/resolver/__tests__/resolver.spec.ts @@ -15,6 +15,7 @@ describe('resolver', () => { }, objects: {}, classes: {}, + layers: {}, statistics: { unresolvedCount: 0, resolvedCount: 0, @@ -234,7 +235,8 @@ describe('resolver', () => { } } }, - classes: {} + classes: {}, + layers: {} } const obj: TimelineObject = { id: 'obj0', @@ -403,6 +405,7 @@ describe('resolver', () => { }) // todo test: different triggers, start, end, duration, while // If object has a parent, only set if parent is on layer (if layer is set for parent) + // repeatingobject with duration==repeating, and other object referring that .end test('repeating object', () => { const timeline: TimelineObject[] = [ { @@ -568,7 +571,7 @@ describe('resolver', () => { layer: '1', enable: { start: '5', // 15 - duration: 10 + duration: 10 // 25 }, content: {} }, @@ -577,7 +580,7 @@ describe('resolver', () => { layer: '1', enable: { start: '#child0.end', // 25 - duration: 10 + duration: 10 // 35 }, content: {} } @@ -586,7 +589,7 @@ describe('resolver', () => { ] const resolved = Resolver.resolveTimeline(timeline, { time: 0 }) - + // console.log(JSON.stringify(resolved, '', 3)) expect(resolved.statistics.resolvedObjectCount).toEqual(3) expect(resolved.statistics.unresolvedCount).toEqual(0) @@ -979,13 +982,19 @@ describe('resolver', () => { id: 'child0' } }) - expect(Resolver.getState(resolved, 56).layers).toMatchObject({ - '1': { - id: 'child0' + expect(Resolver.getState(resolved, 56)).toMatchObject({ + layers: { + '1': { + id: 'child0' + }, + '2': { + id: 'child1' + } }, - '2': { - id: 'child1' - } + nextEvents: [ + { objId: 'child0', time: 100, type: EventType.END }, + { objId: 'child1', time: 100, type: EventType.END } + ] }) // objects should be capped inside their parent: @@ -1065,26 +1074,166 @@ describe('resolver', () => { instances: [{ start: 55, end: 100 }] }) - expect(Resolver.getState(resolved, 16).layers).toMatchObject({ - 'g0': { - id: 'group0' + expect(Resolver.getState(resolved, 16)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + }, + '1': { + id: 'child0' + } }, - '1': { - id: 'child0' - } + nextEvents: [ + { objId: 'group1', time: 50, type: EventType.START }, + { objId: 'child1', time: 55, type: EventType.START }, + { objId: 'child0', time: 100, type: EventType.END }, + { objId: 'child1', time: 100, type: EventType.END }, + { objId: 'group0', time: 100, type: EventType.END }, + { objId: 'group1', time: 100, type: EventType.END } + ] }) - const state0 = Resolver.getState(resolved, 56) - expect(state0.layers).toMatchObject({ - 'g0': { - id: 'group1' + expect(Resolver.getState(resolved, 56)).toMatchObject({ + layers: { + 'g0': { + id: 'group1' + }, + '2': { + id: 'child1' + } }, - '2': { - id: 'child1' - } + nextEvents: [ + { objId: 'child0', time: 100, type: EventType.END }, + { objId: 'child1', time: 100, type: EventType.END }, + { objId: 'group0', time: 100, type: EventType.END }, + { objId: 'group1', time: 100, type: EventType.END } + ] }) const state1 = Resolver.getState(resolved, 120) expect(state1.layers['g0']).toBeFalsy() expect(state1.layers['1']).toBeFalsy() expect(state1.layers['2']).toBeFalsy() }) + test('cap in repeating parent group', () => { + const timeline: TimelineObject[] = [ + { + id: 'group0', + layer: 'g0', + enable: { + start: 0, + duration: 100, + repeating: 100 + }, + content: {}, + isGroup: true, + children: [ + { + id: 'child0', + layer: '1', + enable: { + start: '50', // 50 + duration: 20 // 70 + }, + content: {} + }, + { + id: 'child1', + layer: '2', + enable: { + start: '#child0.end', // 70 + duration: 50 // 120, to be capped at 100 + }, + content: {} + } + ] + } + ] + + const resolved = Resolver.resolveTimeline(timeline, { time: 0, limitCount: 99, limitTime: 199 }) + + expect(resolved.statistics.resolvedObjectCount).toEqual(3) + expect(resolved.statistics.unresolvedCount).toEqual(0) + + expect(resolved.objects['group0']).toBeTruthy() + expect(resolved.objects['child0']).toBeTruthy() + expect(resolved.objects['child1']).toBeTruthy() + + expect(resolved.objects['group0'].resolved).toMatchObject({ + resolved: true, + instances: [ + { start: 0, end: 80 }, + { start: 100, end: 180 } + ] + }) + expect(resolved.objects['child0'].resolved).toMatchObject({ + resolved: true, + instances: [ + { start: 50, end: 70 }, + { start: 150, end: 170 } + ] + }) + expect(resolved.objects['child1'].resolved).toMatchObject({ + resolved: true, + instances: [ + { start: 70, end: 80 }, + { start: 170, end: 180 } + ] + }) + expect(Resolver.getState(resolved, 10)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + } + } + }) + expect(Resolver.getState(resolved, 55)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + }, + '1': { + id: 'child0' + } + } + }) + expect(Resolver.getState(resolved, 78)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + }, + '2': { + id: 'child1' + } + } + }) + expect(Resolver.getState(resolved, 85).layers).toEqual({}) + + expect(Resolver.getState(resolved, 110)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + } + } + }) + expect(Resolver.getState(resolved, 155)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + }, + '1': { + id: 'child0' + } + } + }) + expect(Resolver.getState(resolved, 178)).toMatchObject({ + layers: { + 'g0': { + id: 'group0' + }, + '2': { + id: 'child1' + } + } + }) + expect(Resolver.getState(resolved, 185).layers).toEqual({}) + }) }) diff --git a/src/resolver/__tests__/validate.ts b/src/resolver/__tests__/validate.ts new file mode 100644 index 00000000..572bcb52 --- /dev/null +++ b/src/resolver/__tests__/validate.ts @@ -0,0 +1,95 @@ +import { validateObject, validateKeyframe, validateTimeline } from '../validate' +import { TimelineObject, TimelineKeyframe } from '../../api/api' +import _ = require('underscore') + +describe('validate', () => { + const obj: TimelineObject = { + id: 'obj0', + enable: {}, + layer: '1', + content: {} + } + const keyframe: TimelineKeyframe = { + id: 'obj0', + enable: {}, + content: {} + } + const timeline: TimelineObject[] = [ + { + id: 'obj0', + enable: {}, + layer: '1', + content: {} + }, + { + id: 'obj1', + enable: {}, + layer: '1', + content: {} + } + ] + test('validateObject', () => { + expect(() => { + validateObject(obj, true) + }).not.toThrowError() + + expect(() => { + const o = _.clone(obj) + delete o.id + validateObject(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(obj) + delete o.enable + validateObject(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(obj) + delete o.layer + validateObject(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(obj) + delete o.content + validateObject(o, true) + }).toThrowError() + }) + test('validateKeyframe', () => { + expect(() => { + validateKeyframe(keyframe, true) + }).not.toThrowError() + + expect(() => { + const o = _.clone(keyframe) + delete o.id + validateKeyframe(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(keyframe) + delete o.enable + validateKeyframe(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(keyframe) + validateKeyframe(o, true) + }).toThrowError() + + expect(() => { + const o = _.clone(keyframe) + delete o.content + validateKeyframe(o, true) + }).toThrowError() + }) + test('validateTimeline', () => { + expect(() => { + const tl = _.clone(timeline) + tl[1].id = tl[0].id + validateTimeline(tl, false) + }).toThrowError() + }) +}) diff --git a/src/resolver/resolver.ts b/src/resolver/resolver.ts index 6805563a..7008794a 100644 --- a/src/resolver/resolver.ts +++ b/src/resolver/resolver.ts @@ -5,30 +5,47 @@ import { ResolveOptions, Expression, ExpressionObj, - InstanceEvent, ExpressionEvent, ResolvedTimelineObject, TimelineObjectInstance, Time, TimelineState, - Duration, TimelineKeyframe, TimelineObjectKeyframe } from '../api/api' +import { + extendMandadory, + operateOnArrays, + isNumeric, + applyRepeatingInstances, + sortEvents, + cleanInstances, + invertInstances, + capInstances +} from '../lib' +import { validateTimeline } from './validate' import { interpretExpression } from './expression' import { getState } from './state' -import { extendMandadory, operateOnArrays, isNumeric, applyRepeatingInstances, sortEvents, cleanInstances, invertInstances, capInstances } from '../lib' export class Resolver { + /** + * Go through all objects on the timeline and calculate all the timings. + * Returns a ResolvedTimeline which can be piped into Resolver.getState() + * @param timeline Array of timeline objects + * @param options Resolve options + */ static resolveTimeline (timeline: Array, options: ResolveOptions): ResolvedTimeline { if (!_.isArray(timeline)) throw new Error('resolveTimeline: parameter timeline missing') if (!options) throw new Error('resolveTimeline: parameter options missing') + validateTimeline(timeline, false) + const resolvedTimeline: ResolvedTimeline = { options: _.clone(options), objects: {}, classes: {}, + layers: {}, statistics: { unresolvedCount: 0, resolvedCount: 0, @@ -67,6 +84,10 @@ export class Resolver { } }) } + if (obj.layer) { + if (!resolvedTimeline.layers[obj.layer]) resolvedTimeline.layers[obj.layer] = [] + resolvedTimeline.layers[obj.layer].push(obj.id) + } if (obj.isGroup && obj.children) { _.each(obj.children, (child) => { @@ -92,6 +113,12 @@ export class Resolver { return resolvedTimeline } + /** + * Calculate the state at a given point in time. Using a ResolvedTimeline calculated by Resolver.resolveTimeline. + * @param resolved ResolvedTimeline calculated by Resolver.resolveTimeline. + * @param time The point in time where to calculate the state + * @param eventLimit (Optional) Limits the number of returned upcoming events. + */ static getState (resolved: ResolvedTimeline, time: Time, eventLimit?: number): TimelineState { return getState(resolved, time, eventLimit) } @@ -104,7 +131,7 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res if (obj.resolved.resolved) return if (obj.resolved.resolving) throw new Error(`Circular dependency when trying to resolve "${obj.id}"`) obj.resolved.resolving = true - + console.log('resolveTimelineObj', obj.id) let instances: Array = [] const repeatingExpr: Expression | null = ( @@ -125,18 +152,26 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res '' ) const startExpr: ExpressionObj | number | null = interpretExpression(start) - + console.log('parentId', obj.resolved.parentId) let parentInstances: TimelineObjectInstance[] | null | number = null - let useParent: boolean = false - if (obj.resolved.parentId && isNumeric(startExpr)) { - useParent = true + let hasParent: boolean = false + let referToParent: boolean = false + if (obj.resolved.parentId) { + hasParent = true parentInstances = lookupExpression( resolvedTimeline, obj, interpretExpression(`#${obj.resolved.parentId}`), 'start' ) + if (isNumeric(startExpr)) { + // Only use parent if the expression resolves to a number (ie doesn't contain any references) + referToParent = true + } } + let lookedupStarts = lookupExpression(resolvedTimeline, obj, startExpr, 'start') + console.log('referToParent', referToParent) + console.log('parentInstances', parentInstances) const applyParentInstances = (value: TimelineObjectInstance[] | null | number): TimelineObjectInstance[] | null | number => { const operate = (a: number | null, b: number | null): number | null => { if (a === null || b === null) return null @@ -144,12 +179,13 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res } return operateOnArrays(parentInstances, value, operate) } - - let lookedupStarts = lookupExpression(resolvedTimeline, obj, startExpr, 'start') - lookedupStarts = applyRepeatingInstances(lookedupStarts, lookedupRepeating, resolvedTimeline.options) - if (useParent) { + if (referToParent) { lookedupStarts = applyParentInstances(lookedupStarts) } + + lookedupStarts = applyRepeatingInstances(lookedupStarts, lookedupRepeating, resolvedTimeline.options) + console.log('lookedupStarts after', lookedupStarts) + if (obj.enable.while) { if (_.isArray(lookedupStarts)) { @@ -186,7 +222,7 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res null ) lookedupEnds = applyRepeatingInstances(lookedupEnds, lookedupRepeating, resolvedTimeline.options) - if (useParent && isNumeric(endExpr)) { + if (referToParent && isNumeric(endExpr)) { lookedupEnds = applyParentInstances(lookedupEnds) } @@ -205,15 +241,22 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res } } else if (obj.enable.duration !== undefined) { const durationExpr: ExpressionObj | number | null = interpretExpression(obj.enable.duration) - const lookedupDuration = lookupExpression(resolvedTimeline, obj, durationExpr, 'duration') + let lookedupDuration = lookupExpression(resolvedTimeline, obj, durationExpr, 'duration') if (_.isArray(lookedupDuration)) { throw new Error(`lookupExpression should never return an array for .duration lookup`) // perhaps tmp? maybe revisit this at some point } else if (lookedupDuration !== null) { + + if ( + lookedupRepeating !== null && + lookedupDuration > lookedupRepeating + ) lookedupDuration = lookedupRepeating + + const tmpLookedupDuration: number = lookedupDuration // cast type _.each(events, (e) => { if (e.value) { events.push({ - time: e.time + lookedupDuration, + time: e.time + tmpLookedupDuration, value: false }) } @@ -238,7 +281,11 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res } }) } - instances = capInstances(instances, parentInstances) + console.log('capInstances', instances, parentInstances) + if (hasParent) { + instances = capInstances(instances, parentInstances) + } + console.log('result', instances) obj.resolved.resolved = true obj.resolved.resolving = false @@ -246,6 +293,7 @@ export function resolveTimelineObj (resolvedTimeline: ResolvedTimeline, obj: Res if (instances.length) { resolvedTimeline.statistics.resolvedInstanceCount += instances.length + resolvedTimeline.statistics.resolvedCount += 1 if (obj.isGroup) { resolvedTimeline.statistics.resolvedGroupCount += 1 @@ -314,6 +362,24 @@ export function lookupExpression ( referencedObjs.push(obj) } }) + } else { + // Match layer, example: "$layer" + const m = expr.match(/^(!)?\W*\$([^.]+)(.*)/) + if (m) { + const exclamation = m[1] + const layer = m[2] + rest = m[3] + if (exclamation === '!') invert = !invert + + const objIds: string[] = resolvedTimeline.layers[layer] || [] + + _.each(objIds, (objId: string) => { + const obj = resolvedTimeline.objects[objId] + if (obj) { + referencedObjs.push(obj) + } + }) + } } } diff --git a/src/resolver/state.ts b/src/resolver/state.ts index 8306d8b4..57f496e8 100644 --- a/src/resolver/state.ts +++ b/src/resolver/state.ts @@ -5,7 +5,6 @@ import { ResolvedTimelineObject, TimelineObjectInstance, ResolvedTimelineObjectInstance, - TimelineKeyframe, Content } from '../api/api' import * as _ from 'underscore' @@ -38,15 +37,15 @@ export function getState (resolved: ResolvedTimeline, time: Time, eventLimit: nu ) { _.each(obj.resolved.instances, (instance: TimelineObjectInstance) => { - if ( - // object instance is active: - ( - instance.end === null || - instance.end > time - ) && - instance.start <= time - ) { - if (obj.layer) { // if layer is empty, don't put in state + 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 + ) { const parentObj = ( obj.resolved.parentId ? @@ -95,28 +94,28 @@ export function getState (resolved: ResolvedTimeline, time: Time, eventLimit: nu } } } + if (instance.start > time) { + + state.nextEvents.push({ + type: EventType.START, + time: instance.start, + objId: obj.id + }) + eventObjectTimes['' + instance.start] = EventType.START + } + if ( + instance.end !== null && + instance.end > time + ) { + state.nextEvents.push({ + type: EventType.END, + time: instance.end, + objId: obj.id + }) + eventObjectTimes['' + instance.end] = EventType.END + } } - if (instance.start > time) { - - state.nextEvents.push({ - type: EventType.START, - time: instance.start, - objId: obj.id - }) - eventObjectTimes['' + instance.start] = EventType.START - } - if ( - instance.end !== null && - instance.end > time - ) { - state.nextEvents.push({ - type: EventType.END, - time: instance.end, - objId: obj.id - }) - eventObjectTimes['' + instance.end] = EventType.END - } }) } }) @@ -209,8 +208,8 @@ export function getState (resolved: ResolvedTimeline, time: Time, eventLimit: nu if (a.type > b.type) return -1 if (a.type < b.type) return 1 - if (a.objId > b.objId) return -1 - if (a.objId < b.objId) return 1 + if (a.objId < b.objId) return -1 + if (a.objId > b.objId) return 1 return 0 }) diff --git a/src/resolver/validate.ts b/src/resolver/validate.ts new file mode 100644 index 00000000..c4560c4b --- /dev/null +++ b/src/resolver/validate.ts @@ -0,0 +1,120 @@ +import { + TimelineObject, TimelineKeyframe +} from '../api/api' +import _ = require('underscore') + +interface Ids { + [id: string]: true +} +export function validateObject (obj: TimelineObject, strict?: boolean): void { + validateObject0(obj, strict) +} +export function validateKeyframe (keyframe: TimelineKeyframe, strict?: boolean): void { + validateKeyframe0(keyframe, strict) +} +function validateObject0 (obj: TimelineObject, strict?: boolean, uniqueIds?: Ids): void { + if (!uniqueIds) uniqueIds = {} + + if (!obj) throw new Error(`Object is undefined`) + if (!_.isObject(obj)) throw new Error(`Object is not an object`) + + if (!obj.id) throw new Error(`Object missing "id" attribute`) + if (!_.isString(obj.id)) throw new Error(`Object "id" attribute is not a string: "${obj.id}"`) + + if (uniqueIds[obj.id]) throw new Error(`Object id "${obj.id}" is not unique`) + uniqueIds[obj.id] = true + + if (obj.layer === undefined) throw new Error(`Object "${obj.id}": "layer" attribute is undefined`) + + if (!obj.content) throw new Error(`Object "${obj.id}": "content" attribute must be set`) + if (!obj.enable) throw new Error(`Object "${obj.id}": "enable" attribute must be set`) + + if (obj.enable.start !== undefined) { + if (strict && obj.enable.while !== undefined) throw new Error(`Object "${obj.id}": "enable.start" and "enable.while" cannot be combined`) + + if ( + strict && + obj.enable.end !== undefined && + obj.enable.duration !== undefined + ) throw new Error(`Object "${obj.id}": "enable.end" and "enable.duration" cannot be combined`) + + } else if (obj.enable.while !== undefined) { + + if (strict && obj.enable.end !== undefined) throw new Error(`Object "${obj.id}": "enable.while" and "enable.end" cannot be combined`) + if (strict && obj.enable.duration !== undefined) throw new Error(`Object "${obj.id}": "enable.while" and "enable.duration" cannot be combined`) + + } else throw new Error(`Object "${obj.id}": "enable.start" or "enable.while" must be set`) + + if (obj.keyframes) { + _.each(obj.keyframes, (keyframe, i) => { + try { + validateKeyframe0(keyframe, strict, uniqueIds) + } catch (e) { + throw new Error(`Object "${obj.id}" keyframe[${i}]: ${e}`) + } + }) + } + if (obj.classes) { + _.each(obj.classes, (className, i) => { + if (className && !_.isString(className)) throw new Error(`Object "${obj.id}": "classes[${i}]" is not a string`) + }) + } + + if (obj.children && !obj.isGroup) throw new Error(`Object "${obj.id}": attribute "children" is set but "isGroup" is not`) + if (obj.isGroup && !obj.children) throw new Error(`Object "${obj.id}": attribute "isGroup" is set but "children" missing`) + + if (obj.children) { + _.each(obj.children, (child, i) => { + try { + validateObject0(child, strict, uniqueIds) + } catch (e) { + throw new Error(`Object "${obj.id}" child[${i}]: ${e}`) + } + }) + } + if (obj.priority !== undefined && !_.isNumber(obj.priority)) throw new Error(`Object "${obj.id}": attribute "priority" is not a number`) +} +function validateKeyframe0 (keyframe: TimelineKeyframe, strict?: boolean, uniqueIds?: Ids): void { + if (!uniqueIds) uniqueIds = {} + + if (!keyframe) throw new Error(`Keyframe is undefined`) + if (!_.isObject(keyframe)) throw new Error(`Keyframe is not an object`) + + if (!keyframe.id) throw new Error(`Keyframe missing id attribute`) + if (!_.isString(keyframe.id)) throw new Error(`Keyframe id attribute is not a string: "${keyframe.id}"`) + + if (uniqueIds[keyframe.id]) throw new Error(`Keyframe id "${keyframe.id}" is not unique`) + uniqueIds[keyframe.id] = true + + if (!keyframe.content) throw new Error(`Keyframe "${keyframe.id}": "content" attribute must be set`) + if (!keyframe.enable) throw new Error(`Keyframe "${keyframe.id}": "enable" attribute must be set`) + + if (keyframe.enable.start !== undefined) { + if (strict && keyframe.enable.while !== undefined) throw new Error(`Keyframe "${keyframe.id}": "enable.start" and "enable.while" cannot be combined`) + + if ( + strict && + keyframe.enable.end !== undefined && + keyframe.enable.duration !== undefined + ) throw new Error(`Keyframe "${keyframe.id}": "enable.end" and "enable.duration" cannot be combined`) + + } else if (keyframe.enable.while !== undefined) { + + if (strict && keyframe.enable.end !== undefined) throw new Error(`Keyframe "${keyframe.id}": "enable.while" and "enable.end" cannot be combined`) + if (strict && keyframe.enable.duration !== undefined) throw new Error(`Keyframe "${keyframe.id}": "enable.while" and "enable.duration" cannot be combined`) + + } else throw new Error(`Keyframe "${keyframe.id}": "enable.start" or "enable.while" must be set`) + + if (keyframe.classes) { + _.each(keyframe.classes, (className, i) => { + if (className && !_.isString(className)) throw new Error(`Keyframe "${keyframe.id}": "classes[${i}]" is not a string`) + }) + } +} + +export function validateTimeline (timeline: Array, strict?: boolean) { + const uniqueIds: {[id: string]: true} = {} + _.each(timeline, (obj) => { + validateObject0(obj, strict, uniqueIds) + }) +}