superfly-timeline
Version:
Resolver for defining objects with temporal boolean logic relationships on a timeline
1,071 lines (1,068 loc) • 50.3 kB
JavaScript
"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: