UNPKG

superfly-timeline

Version:

Resolver for defining objects with temporal boolean logic relationships on a timeline

1,071 lines (1,068 loc) 50.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResolvedTimelineHandler = void 0; const ExpressionHandler_1 = require("./ExpressionHandler"); const ReferenceHandler_1 = require("./ReferenceHandler"); const resolvedTimeline_1 = require("../api/resolvedTimeline"); const lib_1 = require("./lib/lib"); const InstanceHandler_1 = require("./InstanceHandler"); const reference_1 = require("./lib/reference"); const event_1 = require("./lib/event"); const instance_1 = require("./lib/instance"); const timeline_1 = require("./lib/timeline"); const LayerStateHandler_1 = require("./LayerStateHandler"); const expression_1 = require("./lib/expression"); const performance_1 = require("./lib/performance"); const CacheHandler_1 = require("./CacheHandler"); /** * A ResolvedTimelineHandler instance is short-lived and used to resolve a timeline. * Intended usage: * 1. const resolver = new ResolvedTimelineHandler(options) * 2. timelineObjects.forEach(obj => resolver.addTimelineObject(obj)) * 3. resolver.resolveAllTimelineObjs() */ class ResolvedTimelineHandler { constructor(options) { this.options = options; /** Maps object id to object */ this.objectsMap = new Map(); /** Maps className to a list of object ids */ this.classesMap = new Map(); /** Maps layer to a list of object ids */ this.layersMap = new Map(); /** * Maps an array of object ids to an object id (objects that directly reference an reference). */ this.directReferenceMap = new Map(); /** How many objects that was actually resolved (is affected when using cache) */ this.statisticResolvingObjectCount = 0; /** How many times an object where resolved. (is affected when using cache) */ this.statisticResolvingCount = 0; this._resolveTrace = []; /** * A Map of strings (instance hashes) that is used to determine if an objects instances have changed. * Maps objectId -> instancesHash */ this.resolvedObjInstancesHash = new Map(); /** * List of explanations fow why an object changed during a resolve iteration. * Used for debugging and Errors */ this.changedObjIdsExplanations = []; /** * A Map that contains the objects that needs to resolve again. * Object are added into this after this.resolveConflictsForLayer() */ this.objectsToReResolve = new Map(); /** Counter that increases during resolving, for every object that might need re-resolving*/ this.objectResolveCount = 0; /** Error message, is set when an error is encountered and this.options.dontThrowOnError is set */ this._resolveError = undefined; this._idCount = 0; this.expression = new ExpressionHandler_1.ExpressionHandler(false, this.options.skipValidation); this.instance = new InstanceHandler_1.InstanceHandler(this); this.reference = new ReferenceHandler_1.ReferenceHandler(this, this.instance); this.debug = this.options.debug ?? false; this.traceResolving = this.options.traceResolving || this.debug; } get resolveError() { return this._resolveError; } /** Populate ResolvedTimelineHandler with a timeline-object. */ addTimelineObject(obj) { this._addTimelineObject(obj, 0, undefined, false); } /** Resolve the timeline. */ resolveAllTimelineObjs() { const toc = (0, performance_1.tic)(' resolveAllTimelineObjs'); // Step 0: Preparations: /** Number of objects in timeline */ const objectCount = this.objectsMap.size; /** Max allowed number of iterations over objects */ const objectResolveCountMax = objectCount * (this.options.conflictMaxDepth ?? 5); /* The resolving algorithm basically works like this: 1a: Resolve all objects 1b: Resolve conflicts for all layers Also determine which objects depend on changed objects due to conflicts 2: Loop, until there are no more changed objects: 2a: Resolve objects that depend on changed objects 2b: Resolve conflicts for affected layers in 2a Also determine which objects depend on changed objects due to conflicts */ // Step 1a: Resolve all objects: if (this.traceResolving) this.addResolveTrace('Resolver: Step 1a'); for (const obj of this.objectsMap.values()) { this.resolveTimelineObj(obj); // Populate this.resolvedObjInstancesHash now, so that only changes to the timeline instances // in this.resolveConflictsForObjs() will be detected later: this.resolvedObjInstancesHash.set(obj.id, (0, instance_1.getInstancesHash)(obj.resolved.instances)); } if (this._resolveError) return; // Abort on error // Step 1b: Resolve conflicts for all objects: if (this.traceResolving) this.addResolveTrace('Resolver: Step 1b'); this.resolveConflictsForObjs(null); if (this._resolveError) return; // Abort on error // Step 2: re-resolve all changed objects, until no more changes are detected: if (this.traceResolving) this.addResolveTrace('Resolver: Step 2'); while (this.objectsToReResolve.size > 0) { if (this.objectResolveCount >= objectResolveCountMax) { const error = new Error(`Maximum conflict iteration reached (${this.objectResolveCount}). This is due to a circular dependency in the timeline. Latest changes:\n${this.changedObjIdsExplanations.join('Next iteration -------------------------\n')}`); if (this.options.dontThrowOnError) { this._resolveError = error; return; } else { throw error; } } if (this.traceResolving) { this.addResolveTrace(`Resolver: Step 2: objectsToReResolve: ${JSON.stringify(Array.from(this.objectsToReResolve.keys()))}`); } // Collect and reset all objects that depend on previously changed objects const conflictObjectsToResolve = []; for (const obj of this.objectsToReResolve.values()) { this.objectResolveCount++; // Force a new resolve, since the referenced objects might have changed (due to conflicts): let needsConflictResolve = false; if (!obj.resolved.resolvedReferences) { this.resolveTimelineObj(obj); needsConflictResolve = true; } if (!obj.resolved.resolvedConflicts) { needsConflictResolve = true; } if (needsConflictResolve) { conflictObjectsToResolve.push(obj); } } if (this._resolveError) return; // Abort on error // Resolve conflicts for objects that depend on previously changed objects: this.resolveConflictsForObjs(conflictObjectsToResolve); } toc(); } /** * Resolve a timeline-object. * The Resolve algorithm works like this: * 1. Go through the .enable expression(s) and look up all referenced objects. * 1.5 For each referenced object, recursively resolve it first if not already resolved. * 2. Collect the resolved instances and calculate the resulting list of resulting instances. */ resolveTimelineObj(obj) { if (obj.resolved.resolving) { // Circular dependency const error = Error(`Circular dependency when trying to resolve "${obj.id}"`); if (this.options.dontThrowOnError) { this._resolveError = error; obj.resolved.firstResolved = true; obj.resolved.resolvedReferences = true; obj.resolved.resolving = false; obj.resolved.instances = []; return; } else { throw error; } } if (obj.resolved.resolvedReferences) return; // already resolved const toc = (0, performance_1.tic)(' resolveTimelineObj'); obj.resolved.resolving = true; this.statisticResolvingCount++; if (!obj.resolved.firstResolved) { this.statisticResolvingObjectCount++; } if (this.traceResolving) this.addResolveTrace(`Resolver: Resolve object "${obj.id}"`); const directReferences = []; let resultingInstances = []; if (obj.disabled) { resultingInstances = []; } else { // Look up references to the parent: let parentInstances = null; let hasParent = false; let parentRef = undefined; if (obj.resolved.parentId) { hasParent = true; parentRef = `#${obj.resolved.parentId}`; const parentLookup = this.reference.lookupExpression(obj, this.expression.interpretExpression(parentRef), 'start'); // pushToArray(directReferences, parentLookup.allReferences) parentInstances = parentLookup.result; // a start-reference will always return an array, or null if (parentInstances !== null) { // Ensure that the parentInstances references the parent: for (const parentInstance of parentInstances) { parentInstance.references = (0, reference_1.joinReferences)(parentInstance.references, parentRef); } } } const enables = (0, lib_1.ensureArray)(obj.enable); for (let i = 0; i < enables.length; i++) { const enable = enables[i]; // Resolve the the enable.repeating expression: const lookupRepeating = enable.repeating !== undefined ? this.lookupExpression(obj, directReferences, enable.repeating, 'duration') : { result: null }; let lookedupRepeating; if (lookupRepeating.result === null) { // Do nothing lookedupRepeating = null; } else if ((0, lib_1.isArray)(lookupRepeating.result)) { if (lookupRepeating.result.length === 0) { lookedupRepeating = null; } else if (lookupRepeating.result.length === 1) { lookedupRepeating = (0, lib_1.literal)({ value: lookupRepeating.result[0].start, references: lookupRepeating.result[0].references, }); } else { // The lookup for repeating returned multiple instances. // Not supported at the moment, perhaps this could be supported in the future. /* istanbul ignore next */ throw new Error(`lookupExpression should never return an array for .duration lookup`); } } else { lookedupRepeating = lookupRepeating.result; } /** Array of instances this enable-expression resulted in */ let enableInstances; if (enable.while !== undefined) { const whileExpr = // Handle special case "1", 1: enable.while === '1' || enable.while === 1 ? 'true' : // Handle special case "0", 0: enable.while === '0' || enable.while === 0 ? 'false' : enable.while; // Note: a lookup for 'while' works the same as for 'start' const lookupWhile = this.lookupExpression(obj, directReferences, whileExpr, 'start'); if (lookupWhile.result === null) { // Do nothing enableInstances = []; } else if ((0, lib_1.isArray)(lookupWhile.result)) { enableInstances = lookupWhile.result; } else if (lookupWhile.result !== null) { enableInstances = [ { id: this.getInstanceId(), start: lookupWhile.result.value, end: null, references: lookupWhile.result.references, }, ]; } else { enableInstances = []; } } else if (enable.start !== undefined) { const lookupStart = this.lookupExpression(obj, directReferences, enable.start, 'start'); const lookedupStarts = lookupStart.refersToParent ? this.reference.applyParentInstances(parentInstances, lookupStart.result) : lookupStart.result; const events = []; // const endEvents: EventForInstance[] = [] let iStart = 0; let iEnd = 0; if (lookedupStarts === null) { // Do nothing } else if ((0, lib_1.isArray)(lookedupStarts)) { // Use the start-times of the instances and add them to the list of events: // (The end-times are irrelevant) for (let i = 0; i < lookedupStarts.length; i++) { const instance = lookedupStarts[i]; const eventId = `${obj.id}_${iStart++}`; events.push({ time: instance.start, value: true, data: { instance: instance, id: eventId }, references: instance.references, }); } } else { events.push({ time: lookedupStarts.value, value: true, data: { instance: { id: this.getInstanceId(), start: lookedupStarts.value, end: null, references: lookedupStarts.references, }, id: `${obj.id}_${iStart++}`, }, references: lookedupStarts.references, }); } if (enable.end !== undefined) { const lookupEnd = this.lookupExpression(obj, directReferences, enable.end, 'end'); /** Contains an inverted list of instances. Therefore .start means an end */ const lookedupEnds = !lookupEnd ? null : lookupEnd.refersToParent ? this.reference.applyParentInstances(parentInstances, lookupEnd.result) : lookupEnd.result; if (lookedupEnds === null) { // Do nothing } else if ((0, lib_1.isArray)(lookedupEnds)) { // Use the start-times of the instances and add them (as end-events) to the list: // (The end-times are irrelevant) for (let i = 0; i < lookedupEnds.length; i++) { const instance = lookedupEnds[i]; events.push({ time: instance.start, value: false, data: { instance: instance, id: `${obj.id}_${iEnd++}` }, references: instance.references, }); } } else if (lookedupEnds) { events.push({ time: lookedupEnds.value, value: false, data: { instance: { id: this.getInstanceId(), start: lookedupEnds.value, end: null, references: lookedupEnds.references, }, id: `${obj.id}_${iEnd++}`, }, references: lookedupEnds.references, }); } } else if (enable.duration !== undefined) { const lookupDuration = this.lookupExpression(obj, directReferences, enable.duration, 'duration'); let lookedupDuration = lookupDuration.result; if (lookedupDuration === null) { // Do nothing } else if ((0, lib_1.isArray)(lookedupDuration)) { if (lookedupDuration.length === 1) { lookedupDuration = (0, lib_1.literal)({ value: lookedupDuration[0].start, references: lookedupDuration[0].references, }); } else if (lookedupDuration.length === 0) { lookedupDuration = null; } else { // Lookup rendeded multiple durations. // This is unsupported at the moment, but could possibly be added in the future. /* istanbul ignore next */ throw new Error(`lookedupDuration should never return an array for .duration lookup`); } } if (lookedupDuration !== null) { if (lookedupRepeating !== null && lookedupDuration.value > lookedupRepeating.value) { // Cap duration to repeating duration lookedupDuration.value = lookedupRepeating.value; } // Go through all pre-existing start-events, and add end-events for each of them. for (let i = 0; i < events.length; i++) { const startEvent = events[i]; if (startEvent.value) { // Is a start-event const time = startEvent.time + lookedupDuration.value; const references = (0, reference_1.joinReferences)(startEvent.references, lookedupDuration.references); events.push({ time: time, value: false, data: { id: startEvent.data.id, instance: { id: startEvent.data.instance.id, start: time, end: null, references: references, }, }, references: references, }); } } } } enableInstances = this.instance.convertEventsToInstances(events, false, false, // Omit the referenced originalStart/End when using enable.start: true); // Cap those instances to the parent instances: if (parentRef && parentInstances !== null) { const parentInstanceMap = new Map(); for (const instance of parentInstances) { parentInstanceMap.set(instance.id, instance); } const cappedEnableInstances = []; for (const instance of enableInstances) { let matchedParentInstance = undefined; // Go through the references in reverse, because sometimes there are multiple matches, and the last one is probably the one we want to use. for (let i = instance.references.length - 1; i >= 0; i--) { const ref = instance.references[i]; if ((0, reference_1.isInstanceReference)(ref)) { matchedParentInstance = parentInstanceMap.get((0, reference_1.getRefInstanceId)(ref)); if (matchedParentInstance) break; } } if (matchedParentInstance) { const cappedInstance = this.instance.capInstance(instance, matchedParentInstance); if (!cappedInstance.caps) cappedInstance.caps = []; cappedInstance.caps.push((0, lib_1.literal)({ id: matchedParentInstance.id, start: matchedParentInstance.start, end: matchedParentInstance.end, })); cappedEnableInstances.push(cappedInstance); } else { cappedEnableInstances.push(instance); } } enableInstances = cappedEnableInstances; } } else { enableInstances = []; } enableInstances = this.instance.applyRepeatingInstances(enableInstances, lookedupRepeating); // Add the instances resulting from this enable-expression to the list: (0, lib_1.pushToArray)(resultingInstances, enableInstances); } // Cap the instances to the parent instances: if (hasParent) { resultingInstances = this.capInstancesToParentInstances({ instances: resultingInstances, parentInstances, }); } } // Make the instance ids unique: const idSet = new Set(); for (const instance of resultingInstances) { if (idSet.has(instance.id)) { instance.id = `${instance.id}_${this.getInstanceId()}`; } idSet.add(instance.id); } if (obj.seamless && resultingInstances.length > 1) { resultingInstances = this.instance.cleanInstances(resultingInstances, true, false); } if (obj.resolved.parentId) { directReferences.push(`#${obj.resolved.parentId}`); } if (!obj.resolved.firstResolved) { // This only needs to be done upon first resolve: this.updateDirectReferenceMap(obj, directReferences); } obj.resolved.firstResolved = true; obj.resolved.resolvedReferences = true; obj.resolved.resolving = false; obj.resolved.instances = resultingInstances; if (this.traceResolving) { this.addResolveTrace(`Resolver: object "${obj.id}" resolved.instances: ${JSON.stringify(obj.resolved.instances)}`); this.addResolveTrace(`Resolver: object "${obj.id}" directReferences: ${JSON.stringify(directReferences)}`); } // Finally: obj.resolved.resolving = false; toc(); } getStatistics() { const toc = (0, performance_1.tic)(' getStatistics'); if (this.options.skipStatistics) { return { totalCount: 0, resolvedInstanceCount: 0, resolvedObjectCount: 0, resolvedGroupCount: 0, resolvedKeyframeCount: 0, resolvingObjectCount: 0, resolvingCount: 0, resolveTrace: this._resolveTrace, }; } const statistics = { totalCount: 0, resolvedInstanceCount: 0, resolvedObjectCount: 0, resolvedGroupCount: 0, resolvedKeyframeCount: 0, resolvingObjectCount: this.statisticResolvingObjectCount, resolvingCount: this.statisticResolvingCount, resolveTrace: this._resolveTrace, }; for (const obj of this.objectsMap.values()) { statistics.totalCount += 1; if (obj.isGroup) { statistics.resolvedGroupCount += 1; } if (obj.resolved.isKeyframe) { statistics.resolvedKeyframeCount += 1; } else { statistics.resolvedObjectCount += 1; } statistics.resolvedInstanceCount += obj.resolved.instances.length; } toc(); return statistics; } initializeCache(cacheObj) { this.cache = new CacheHandler_1.CacheHandler(cacheObj, this); return this.cache; } /** * Returns an object. * type-wise, assumes you know what object you're looking for */ getObject(objId) { return this.objectsMap.get(objId); } /** * Returns object ids on a layer * type-wise, assumes you know what layer you're looking for */ getLayerObjects(layer) { return this.layersMap.get(layer); } /** * Returns object ids on a layer * type-wise, assumes you know what className you're looking for */ getClassObjects(className) { return this.classesMap.get(className); } capInstancesToParentInstances(arg) { if (!arg.parentInstances) return []; const events = []; for (const instance of arg.instances) { events.push({ time: instance.start, value: true, references: instance.references, data: { instance, isParent: false }, }); if (instance.end !== null) { events.push({ time: instance.end, value: false, references: instance.references, data: { instance, isParent: false }, }); } } for (const instance of arg.parentInstances) { events.push({ time: instance.start, value: true, references: instance.references, data: { instance, isParent: true }, }); if (instance.end !== null) { events.push({ time: instance.end, value: false, references: instance.references, data: { instance, isParent: true }, }); } } (0, event_1.sortEvents)(events, compareEvents); const parentActiveInstances = []; const childActiveInstances = []; let currentActive = undefined; const cappedInstances = []; function finalizeCurrentActive() { if (currentActive) { cappedInstances.push(currentActive.instance); currentActive = undefined; } } for (const event of events) { if (event.data.isParent) { // Parent instance if (event.value) { parentActiveInstances.push(event.data.instance); } else { (0, instance_1.spliceInstances)(parentActiveInstances, (i) => (i === event.data.instance ? undefined : i)); } } else { // Child instance if (event.value) { childActiveInstances.push(event.data.instance); } else { (0, instance_1.spliceInstances)(childActiveInstances, (i) => (i === event.data.instance ? undefined : i)); } } const childInstance = childActiveInstances[childActiveInstances.length - 1]; const parentInstance = parentActiveInstances[parentActiveInstances.length - 1]; /** If there is an active child instance */ const toBeEnabled = Boolean(childInstance && parentInstance); if (toBeEnabled) { if (currentActive) { if ( // Check if instance is still the same: childInstance.id !== currentActive.instance.id || (parentInstance !== currentActive.parent && // Check if parent still is active: !parentActiveInstances.includes(currentActive.parent))) { // parent isn't active anymore, stop and start a new instance: // Stop instance: currentActive.instance.end = event.time; currentActive.instance.originalEnd = currentActive.instance.originalEnd ?? event.time; currentActive.instance.references = (0, reference_1.joinReferences)(currentActive.instance.references, event.data.instance.references); finalizeCurrentActive(); } else { // Continue an active instance if (currentActive.instance.id !== childInstance.id) { currentActive.instance.references = (0, reference_1.joinReferences)(currentActive.instance.references, childInstance.references); } } } if (!currentActive) { // Start a new instance: currentActive = { instance: { ...childInstance, start: event.time, end: null, // originalStart: childInstance.originalStart ?? event.time, // originalEnd: childInstance.originalEnd ?? null, // set later originalStart: childInstance.originalStart ?? childInstance.start, originalEnd: childInstance.originalEnd ?? childInstance.end ?? null, references: (0, reference_1.joinReferences)(childInstance.references, ...parentActiveInstances.map((i) => i.references)), }, parent: parentInstance, }; } } else { if (currentActive) { // Stop instance: currentActive.instance.end = event.time; currentActive.instance.originalEnd = currentActive.instance.originalEnd ?? event.time; currentActive.instance.references = (0, reference_1.joinReferences)(currentActive.instance.references, event.data.instance.references); finalizeCurrentActive(); } } } finalizeCurrentActive(); return cappedInstances; } addResolveTrace(message) { this._resolveTrace.push(message); if (this.debug) console.log(message); } getResolvedTimeline() { return (0, lib_1.literal)({ objects: (0, lib_1.mapToObject)(this.objectsMap), classes: (0, lib_1.mapToObject)(this.classesMap), layers: (0, lib_1.mapToObject)(this.layersMap), nextEvents: this.getNextEvents(), statistics: this.getStatistics(), error: this.resolveError, }); } getNextEvents() { const toc = (0, performance_1.tic)(' getNextEvents'); const nextEvents = []; const allObjects = []; const allKeyframes = []; for (const obj of this.objectsMap.values()) { if (obj.resolved.isKeyframe) { allKeyframes.push(obj); } else { allObjects.push(obj); } } /** Used to fast-track in cases where there are no keyframes */ const hasKeyframes = allKeyframes.length > 0; const objectInstanceStartTimes = new Set(); const objectInstanceEndTimes = new Set(); // Go through keyframes last: for (const obj of [...allObjects, ...allKeyframes]) { if (!obj.resolved.isKeyframe) { if (!(0, timeline_1.objHasLayer)(obj)) continue; // transparent objects are omitted in NextEvents } else if (obj.resolved.parentId !== undefined) { const parentObj = this.getObject(obj.resolved.parentId); if (parentObj) { /* istanbul ignore if */ if (!(0, timeline_1.objHasLayer)(parentObj)) continue; // Keyframes of transparent objects are omitted in NextEvents } } for (let i = 0; i < obj.resolved.instances.length; i++) { const instance = obj.resolved.instances[i]; if (instance.start > this.options.time && instance.start < (this.options.limitTime ?? Infinity)) { let useThis = true; if (hasKeyframes) { if (!obj.resolved.isKeyframe) { objectInstanceStartTimes.add(`${obj.id}_${instance.start}`); } else { // No need to put keyframe event if its parent starts at the same time: if (objectInstanceStartTimes.has(`${obj.resolved.parentId}_${instance.start}`)) { useThis = false; } } } if (useThis) { nextEvents.push({ objId: obj.id, type: obj.resolved.isKeyframe ? resolvedTimeline_1.EventType.KEYFRAME : resolvedTimeline_1.EventType.START, time: instance.start, }); } } if (instance.end !== null && instance.end > this.options.time && instance.end < (this.options.limitTime ?? Infinity)) { let useThis = true; if (hasKeyframes) { if (!obj.resolved.isKeyframe) { objectInstanceEndTimes.add(`${obj.id}_${instance.end}`); } else { // No need to put keyframe event if its parent ends at the same time: if (objectInstanceEndTimes.has(`${obj.resolved.parentId}_${instance.end}`)) { useThis = false; } } } if (useThis) { nextEvents.push({ objId: obj.id, type: obj.resolved.isKeyframe ? resolvedTimeline_1.EventType.KEYFRAME : resolvedTimeline_1.EventType.END, time: instance.end, }); } } } } nextEvents.sort(compareNextEvents); toc(); return nextEvents; } updateDirectReferenceMap(obj, directReferences) { obj.resolved.directReferences = directReferences; for (const ref of directReferences) { const objectsThisIsReferencing = []; if ((0, reference_1.isParentReference)(ref)) { const parentObjId = obj.resolved.parentId; if (parentObjId) objectsThisIsReferencing.push(parentObjId); } else if ((0, reference_1.isObjectReference)(ref)) { const objId = (0, reference_1.getRefObjectId)(ref); objectsThisIsReferencing.push(objId); } else if ((0, reference_1.isClassReference)(ref)) { const className = (0, reference_1.getRefClass)(ref); for (const objId of this.getClassObjects(className) ?? []) { objectsThisIsReferencing.push(objId); } } else if ((0, reference_1.isLayerReference)(ref)) { const layer = (0, reference_1.getRefLayer)(ref); for (const objId of this.getLayerObjects(layer) ?? []) { objectsThisIsReferencing.push(objId); } } else if ( /* istanbul ignore next */ (0, reference_1.isInstanceReference)(ref)) { // do nothing } else { /* istanbul ignore next */ (0, lib_1.assertNever)(ref); } for (const refObjId of objectsThisIsReferencing) { let refs = this.directReferenceMap.get(refObjId); if (!refs) { refs = []; this.directReferenceMap.set(refObjId, refs); } refs.push(obj.id); } } } getLayersForObjects(objs) { const sortedLayers = this.getAllObjectLayers(); /** Map of layer and object count */ const usedLayers = new Set(); for (const obj of objs) { if ((0, timeline_1.objHasLayer)(obj)) { usedLayers.add(`${obj.layer}`); } } // Return the layers that are used by the objects, in the correct order: return sortedLayers.filter((layer) => usedLayers.has(layer)); } /** * Returns a list of all object's layers, sorted by object count ASC * Note: The order of the layers is important from a performance perspective. * By feeding layers with a low object count first into this.resolveConflictsForLayer(), * there is a higher likelihood that a conflict from a low-count layer will affect an object on * a high-count layer, so it can be skipped in this iteration. */ getAllObjectLayers() { if (!this.allObjectLayersCache) { // Cache this, since this won't change: // Sort the layers by count ASC: this.allObjectLayersCache = Array.from(this.layersMap.entries()) .sort((a, b) => a[1].length - b[1].length) .map(([layer, _count]) => layer); } return this.allObjectLayersCache; } /** Look up an expression, update references and return it. */ lookupExpression(obj, directReferences, expr, context) { const simplifiedExpression = this.expression.simplifyExpression(expr); const lookupResult = this.reference.lookupExpression(obj, simplifiedExpression, context); (0, lib_1.pushToArray)(directReferences, lookupResult.allReferences); // If expression is a constant, it is assumed to be a time relative to its parent: const refersToParent = obj.resolved.parentId && (0, expression_1.isConstantExpr)(simplifiedExpression); return { allReferences: lookupResult.allReferences, result: lookupResult.result, refersToParent, }; } /** * Add timelineObject or keyframe */ _addTimelineObject(obj, /** A number that increases the more levels inside of a group the objects is. 0 = no parent */ levelDeep, /** ID of the parent object */ parentId, isKeyframe) { const toc = (0, performance_1.tic)(' addTimelineObject'); // Is it already added? if (!this.options.skipValidation) { if (this.objectsMap.has(obj.id)) { /* istanbul ignore next */ throw Error(`All timelineObjects must be unique! (duplicate: "${obj.id}")`); } } // Add the object: { const o = { ...obj, resolved: { firstResolved: false, resolvedReferences: false, resolvedConflicts: false, resolving: false, instances: [], levelDeep: levelDeep, isSelfReferencing: false, directReferences: [], parentId: parentId, isKeyframe: isKeyframe, }, }; this.objectsMap.set(obj.id, o); if (obj.classes) { for (let i = 0; i < obj.classes.length; i++) { const className = obj.classes[i]; if (className) { let classList = this.classesMap.get(className); if (!classList) { classList = []; this.classesMap.set(className, classList); } classList.push(obj.id); } } } if ((0, timeline_1.objHasLayer)(obj)) { const layer = `${obj.layer}`; let layerList = this.layersMap.get(layer); if (!layerList) { layerList = []; this.layersMap.set(layer, layerList); } layerList.push(obj.id); } } // Go through children and keyframes: { // Add children: if (obj.isGroup && obj.children) { for (let i = 0; i < obj.children.length; i++) { const child = obj.children[i]; this._addTimelineObject(child, levelDeep + 1, obj.id, false); } } // Add keyframes: if (obj.keyframes) { for (let i = 0; i < obj.keyframes.length; i++) { const keyframe = obj.keyframes[i]; const kf2 = { ...keyframe, layer: '', }; this._addTimelineObject(kf2, levelDeep + 1, obj.id, true); } } } toc(); } /** * Resolve conflicts for all layers of the provided objects */ resolveConflictsForObjs( /** null means all layers */ objs) { const toc = (0, performance_1.tic)(' resolveConflictsForObjs'); // These need to be cleared, // as they are populated during the this.updateObjectsToReResolve() below: this.changedObjIdsExplanations = []; this.objectsToReResolve.clear(); /** List of layers to resolve conflicts on */ let layers; if (objs === null) { layers = this.getAllObjectLayers(); } else { layers = this.getLayersForObjects(objs); } if (this.traceResolving) this.addResolveTrace(`Resolver: Resolve conflicts for layers: ${JSON.stringify(layers)}`); for (const layer of layers) { const maybeChangedObjs = this.resolveConflictsForLayer(layer); // run this.updateObjectsToReResolve() here (as opposed to outside the loop), // to allow for a fast-path in resolveConflictsForLayer that skips resolving that layer if it contains // objects that depend on already changed objects. this.updateObjectsToReResolve(maybeChangedObjs); } toc(); } /** * Resolve conflicts for a layer * @returns A list of objects on that layer */ resolveConflictsForLayer(layer) { const handler = new LayerStateHandler_1.LayerStateHandler(this, this.instance, layer, this.directReferenceMap); // Fast path: If an object on this layer depends on an already changed object we should skip this layer, this iteration. // Because the objects will likely change during the next resolve-iteration anyway. for (const objId of handler.objectIdsOnLayer) { if (this.objectsToReResolve.has(objId)) { if (this.traceResolving) this.addResolveTrace(`Resolver: Skipping resolve conflicts for layer "${layer}" since "${objId}" changed`); return []; } } handler.resolveConflicts(); return handler.objectsOnLayer; } /** Returns the next unique instance id */ getInstanceId() { return `@${(this._idCount++).toString(36)}`; } updateObjectsToReResolve(maybeChangedObjs) { const toc = (0, performance_1.tic)(' updateObjectsToReResolve'); const changedObjs = new Set(); for (const obj of maybeChangedObjs) { // Check if the instances have changed: const instancesHash = (0, instance_1.getInstancesHash)(obj.resolved.instances); const prevHash = this.resolvedObjInstancesHash.get(obj.id) ?? 'not-found'; if (instancesHash !== prevHash) { this.changedObjIdsExplanations.push(`"${obj.id}" changed from: \n ${prevHash}\n , to \n ${instancesHash}\n`); if (this.changedObjIdsExplanations.length > 2) this.changedObjIdsExplanations.shift(); if (this.traceResolving) this.addResolveTrace(`Resolver: Object ${obj.id} changed from: "${prevHash}", to "${instancesHash}"`); changedObjs.add(obj.id); this.resolvedObjInstancesHash.set(obj.id, instancesHash); } } for (const changedObjId of changedObjs.values()) { // Find all objects that depend on this: const directReferenceIbjIds = this.directReferenceMap.get(changedObjId) ?? []; for (const objId of directReferenceIbjIds) { const obj = this.getObject(objId); if (!obj.resolved.resolvedReferences) continue; // Optimization: No need to go through the object again this.markObjectToBeReResolved(obj, changedObjId); // We also need to re-resolve the objects on the same layer: if ((0, timeline_1.objHasLayer)(obj)) { const onSameLayerObjIds = this.getLayerObjects(`${obj.layer}`); for (const onSameLayerObjId of onSameLayerObjIds) { const onSameLayerObj = this.getObject(onSameLayerObjId); this.markObjectToBeReResolved(onSameLayerObj, `same layer as "${obj.id}"`); } } } } toc(); } markObjectToBeReResolved(obj, reason) { if (!obj.resolved.resolvedReferences) return; obj.resolved.resolvedReferences = false; // Note: obj.resolved.resolvedConflicts will be set to false later when resolving references this.objectsToReResolve.set(obj.id, obj); if (this.traceResolving) this.addResolveTrace(`Resolver: Will re-resolve object ${obj.id} due to "${reason}"`); } } exports.ResolvedTimelineHandler = ResolvedTimelineHandler; function compareEvents(a, b) { // start event be first: const aValue = a.value; const bValue = b.value; if (aValue && !bValue) return -1; if (!aValue && bValue) return 1; const aIsParent = a.data.isParent; const bIsParent = b.data.isParent; if (aValue) { // start: parents first: if (aIsParent && !bIsParent) return -1; if (!aIsParent && bIsParent) return 1; } else { // end: parents last: if (aIsParent && !bIsParent) return 1; if (!aIsParent && bIsParent) return -1; } // parents first: