UNPKG

superfly-timeline

Version:

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

279 lines (278 loc) 15.1 kB
"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