superfly-timeline
Version:
Resolver for defining objects with temporal boolean logic relationships on a timeline
279 lines (278 loc) • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LayerStateHandler = void 0;
const lib_1 = require("./lib/lib");
const performance_1 = require("./lib/performance");
/**
* LayerStateHandler instances are short-lived.
* They are initialized, .resolveConflicts() is called and then discarded
*/
class LayerStateHandler {
constructor(resolvedTimeline, instance, layer,
/**
* Maps an array of object ids to an object id (objects that directly reference an reference).
*/
directReferenceMap) {
this.resolvedTimeline = resolvedTimeline;
this.instance = instance;
this.layer = layer;
this.directReferenceMap = directReferenceMap;
this.pointsInTime = {};
this.compareInstancesToCheck = (a, b) => {
// Note: we assume that there are no keyframes here. (if there where, they would be sorted first)
if (a.instance.id === b.instance.id &&
a.instance.start === b.instance.start &&
a.instance.end === b.instance.end) {
// A & B are the same instance, it is a zero-length instance!
// In this case, put the start before the end:
if (a.instanceEvent === 'start' && b.instanceEvent === 'end')
return -1;
if (a.instanceEvent === 'end' && b.instanceEvent === 'start')
return 1;
}
// Handle ending instances first:
if (a.instanceEvent === 'start' && b.instanceEvent === 'end')
return 1;
if (a.instanceEvent === 'end' && b.instanceEvent === 'start')
return -1;
if (a.instance.start === a.instance.end || b.instance.start === b.instance.end) {
// Put later-ending instances last (in the case of zero-length vs non-zero-length instance):
const difference = (a.instance.end ?? Infinity) - (b.instance.end ?? Infinity);
if (difference)
return difference;
}
// If A references B, A should be handled after B, (B might resolve into a zero-length instance)
const aRefObjIds = this.directReferenceMap.get(a.obj.id);
if (aRefObjIds?.includes(b.obj.id))
return -1;
const bRefObjIds = this.directReferenceMap.get(b.obj.id);
if (bRefObjIds?.includes(a.obj.id))
return 1;
if (a.obj.resolved && b.obj.resolved) {
// Deeper objects (children in groups) comes later, we want to check the parent groups first:
const difference = a.obj.resolved.levelDeep - b.obj.resolved.levelDeep;
if (difference)
return difference;
}
// Last resort, sort by id to make it deterministic:
return (0, lib_1.compareStrings)(a.obj.id, b.obj.id) || (0, lib_1.compareStrings)(a.instance.id, b.instance.id);
};
this.objectsOnLayer = [];
this.objectIdsOnLayer = this.resolvedTimeline.getLayerObjects(layer);
}
/** Resolve conflicts between objects on the layer. */
resolveConflicts() {
const toc = (0, performance_1.tic)(' resolveConflicts');
/*
This algorithm basically works like this:
1. Collect all instances start- and end-times as points-of-interest
2. Sweep through the points-of-interest and determine which instance is the "winning one" at every point in time
*/
// Populate this.objectsOnLayer:
for (const objId of this.objectIdsOnLayer) {
this.objectsOnLayer.push(this.resolvedTimeline.getObject(objId));
}
if (this.resolvedTimeline.traceResolving)
this.resolvedTimeline.addResolveTrace(`LayerState: Resolve conflicts for layer "${this.layer}", objects: ${this.objectsOnLayer
.map((o) => o.id)
.join(', ')}`);
// Fast-path: if there's only one object on the layer, it can't conflict with anything
if (this.objectsOnLayer.length === 1) {
for (const obj of this.objectsOnLayer) {
obj.resolved.resolvedConflicts = true;
for (const instance of obj.resolved.instances) {
instance.originalStart = instance.originalStart ?? instance.start;
instance.originalEnd = instance.originalEnd ?? instance.end;
}
}
return;
}
// Sort to make sure parent groups are evaluated before their children:
this.objectsOnLayer.sort(compareObjectsOnLayer);
// Step 1: Collect all points-of-interest (which points in time we want to evaluate)
// and which instances that are interesting
for (const obj of this.objectsOnLayer) {
// Notes:
// Since keyframes can't be placed on a layer, we assume that the object is not a keyframe
// We also assume that the object has a layer
for (const instance of obj.resolved.instances) {
const timeEvents = [];
timeEvents.push({ time: instance.start, enable: true });
if (instance.end)
timeEvents.push({ time: instance.end, enable: false });
// Save a reference to this instance on all points in time that could affect it:
for (const timeEvent of timeEvents) {
if (timeEvent.enable) {
this.addPointInTime(timeEvent.time, 'start', obj, instance);
}
else {
this.addPointInTime(timeEvent.time, 'end', obj, instance);
}
}
}
obj.resolved.resolvedConflicts = true;
obj.resolved.instances.splice(0); // clear the instances, so new instances can be re-added later
}
// Step 2: Resolve the state for the points-of-interest
// This is done by sweeping the points-of-interest chronologically,
// determining the state for every point in time by adding & removing objects from aspiringInstances
// Then sorting it to determine who takes precedence
let currentState = undefined;
const activeObjIds = {};
/** The objects in aspiringInstances */
let aspiringInstances = [];
const times = Object.keys(this.pointsInTime)
.map((time) => parseFloat(time))
// Sort chronologically:
.sort((a, b) => a - b);
// Iterate through all points-of-interest times:
for (const time of times) {
const traceConflicts = [];
/** A set of identifiers for which instance-events have been check at this point in time. Used to avoid looking at the same object twice. */
const checkedThisTime = new Set();
/** List of the instances to check at this point in time. */
const instancesToCheck = this.pointsInTime[time];
instancesToCheck.sort(this.compareInstancesToCheck);
for (let j = 0; j < instancesToCheck.length; j++) {
const o = instancesToCheck[j];
const obj = o.obj;
const instance = o.instance;
let toBeEnabled;
if (instance.start === time && instance.end === time) {
// Handle zero-length instances:
if (o.instanceEvent === 'start')
toBeEnabled = true; // Start a zero-length instance
else
toBeEnabled = false; // End a zero-length instance
}
else {
toBeEnabled = (instance.start || 0) <= time && (instance.end ?? Infinity) > time;
}
const identifier = `${obj.id}_${instance.id}_${o.instanceEvent}`;
if (!checkedThisTime.has(identifier)) {
// Only check each object and event-type once for every point in time
checkedThisTime.add(identifier);
if (toBeEnabled) {
// The instance wants to be enabled (is starting)
// Add to aspiringInstances:
aspiringInstances.push({ obj, instance });
}
else {
// The instance doesn't want to be enabled (is ending)
// Remove from aspiringInstances:
aspiringInstances = removeFromAspiringInstances(aspiringInstances, obj.id);
}
// Sort the instances on layer to determine who is the active one:
aspiringInstances.sort(compareAspiringInstances);
// At this point, the first instance in aspiringInstances is the active one.
const instanceOnTopOfLayer = aspiringInstances[0];
// Update current state:
const prevObjInstance = currentState;
const replaceOld = instanceOnTopOfLayer &&
(!prevObjInstance ||
prevObjInstance.id !== instanceOnTopOfLayer.obj.id ||
!prevObjInstance.instance.id.startsWith(`${instanceOnTopOfLayer.instance.id}`));
const removeOld = !instanceOnTopOfLayer && prevObjInstance;
if (replaceOld || removeOld) {
if (prevObjInstance) {
// Cap the old instance, so it'll end at this point in time:
this.instance.setInstanceEndTime(prevObjInstance.instance, time);
// Update activeObjIds:
delete activeObjIds[prevObjInstance.id];
if (this.resolvedTimeline.traceResolving)
traceConflicts.push(`${prevObjInstance.id} stop`);
}
}
if (replaceOld) {
// Set the new objectInstance to be the current one:
const currentObj = instanceOnTopOfLayer.obj;
const newInstance = {
...instanceOnTopOfLayer.instance,
// We're setting new start & end times so they match up with the state:
start: time,
end: null,
fromInstanceId: instanceOnTopOfLayer.instance.id,
originalEnd: instanceOnTopOfLayer.instance.originalEnd ?? instanceOnTopOfLayer.instance.end,
originalStart: instanceOnTopOfLayer.instance.originalStart ?? instanceOnTopOfLayer.instance.start,
};
// Make the instance id unique:
for (let i = 0; i < currentObj.resolved.instances.length; i++) {
if (currentObj.resolved.instances[i].id === newInstance.id) {
newInstance.id = `${newInstance.id}_$${currentObj.resolved.instances.length}`;
}
}
currentObj.resolved.instances.push(newInstance);
const newObjInstance = {
...currentObj,
instance: newInstance,
};
// Save to current state:
currentState = newObjInstance;
// Update activeObjIds:
activeObjIds[newObjInstance.id] = newObjInstance;
if (this.resolvedTimeline.traceResolving)
traceConflicts.push(`${newObjInstance.id} start`);
}
else if (removeOld) {
// Remove from current state:
currentState = undefined;
if (this.resolvedTimeline.traceResolving)
traceConflicts.push(`-nothing-`);
}
}
}
if (this.resolvedTimeline.traceResolving)
this.resolvedTimeline.addResolveTrace(`LayerState: Layer "${this.layer}": time: ${time}: ${traceConflicts.join(', ')}`);
}
// At this point, the instances of all objects are calculated,
// taking into account priorities, clashes etc.
// Cap children inside their parents:
// Functionally, this isn't needed since this is done in ResolvedTimelineHandler.resolveTimelineObj() anyway.
// However by capping children here some re-evaluating iterations can be avoided, so this increases performance.
{
const allChildren = this.objectsOnLayer
.filter((obj) => !!obj.resolved.parentId)
// Sort, so that the outermost are handled first:
.sort((a, b) => {
return a.resolved.levelDeep - b.resolved.levelDeep;
});
for (const obj of allChildren) {
if (obj.resolved.parentId) {
const parent = this.resolvedTimeline.getObject(obj.resolved.parentId);
if (parent) {
obj.resolved.instances = this.instance.cleanInstances(this.instance.capInstances(obj.resolved.instances, parent.resolved.instances), false, false);
}
}
}
}
toc();
}
/** Add an instance and event to a certain point-in-time */
addPointInTime(time, instanceEvent, obj, instance) {
// Note on order: Ending events come before starting events
if (!this.pointsInTime[time + ''])
this.pointsInTime[time + ''] = [];
this.pointsInTime[time + ''].push({ obj, instance, instanceEvent });
}
}
exports.LayerStateHandler = LayerStateHandler;
function compareObjectsOnLayer(a, b) {
// Sort to make sure parent groups are evaluated before their children:
return a.resolved.levelDeep - b.resolved.levelDeep || (0, lib_1.compareStrings)(a.id, b.id);
}
const removeFromAspiringInstances = (aspiringInstances, objId) => {
const returnInstances = [];
for (let i = 0; i < aspiringInstances.length; i++) {
if (aspiringInstances[i].obj.id !== objId)
returnInstances.push(aspiringInstances[i]);
}
return returnInstances;
};
function compareAspiringInstances(a, b) {
// Determine who takes precedence:
return ((b.obj.priority || 0) - (a.obj.priority || 0) || // First, sort using priority
b.instance.start - a.instance.start || // Then, sort using the start time
(0, lib_1.compareStrings)(a.obj.id, b.obj.id) || // Last resort, sort by id to make it deterministic
(0, lib_1.compareStrings)(a.instance.id, b.instance.id));
}
//# sourceMappingURL=LayerStateHandler.js.map