superfly-timeline
Version:
Resolver for defining objects with temporal boolean logic relationships on a timeline
449 lines • 19 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReferenceHandler = void 0;
const lib_1 = require("./lib/lib");
const cap_1 = require("./lib/cap");
const event_1 = require("./lib/event");
const reference_1 = require("./lib/reference");
const expression_1 = require("./lib/expression");
const operator_1 = require("./lib/operator");
class ReferenceHandler {
constructor(resolvedTimeline, instance) {
this.resolvedTimeline = resolvedTimeline;
this.instance = instance;
this.operateApplyParentInstance = (a, b) => {
if (a === null || b === null)
return null;
return {
value: a.value + b.value,
references: (0, reference_1.joinReferences)(a.references, b.references),
};
};
}
/**
* Look up a reference on the timeline
* Return values:
* TimelineObjectInstance[]: Instances on the timeline where the reference expression is true
* ValueWithReference: A singular value which can be combined arithmetically with Instances
* null: Means "something is invalid", an null-value will always return null when combined with other values
*
* @param obj
* @param expr
* @param context
*/
lookupExpression(obj, expr, context) {
if (expr === null)
return { result: null, allReferences: [] };
if (typeof expr === 'string' && (0, expression_1.isNumericExpr)(expr)) {
return {
result: {
value: parseFloat(expr),
references: [],
},
allReferences: [],
};
}
else if (typeof expr === 'number') {
return {
result: {
value: expr,
references: [],
},
allReferences: [],
};
}
else if (typeof expr === 'string') {
expr = expr.trim();
const exprLower = expr.toLowerCase();
if (exprLower === 'true') {
return {
result: {
value: 0,
references: [],
},
allReferences: [],
};
}
else if (exprLower === 'false') {
return {
result: null,
allReferences: [],
};
}
// Look up string
let referencedObjs = [];
let ref = context;
let rest = '';
let objIdsToReference = [];
const allReferences = [];
let referenceIsOk = false;
// Match id, example: "#objectId.start"
const m = /^[^\w#.$]*#([^.]+)(.*)/.exec(expr);
if (m) {
let id = m[1];
rest = m[2];
// Special case: if the id is '#parent', use the parent id
if (id === '#parent')
id = obj.resolved.parentId || id;
referenceIsOk = true;
objIdsToReference = [id];
allReferences.push(`#${id}`);
}
else {
// Match class, example: ".className.start"
const m = /^\W*\.([^.]+)(.*)/.exec(expr);
if (m) {
const className = m[1];
rest = m[2];
referenceIsOk = true;
objIdsToReference = this.resolvedTimeline.getClassObjects(className) ?? [];
allReferences.push(`.${className}`);
}
else {
// Match layer, example: "$layer"
const m = /^\W*\$([^.]+)(.*)/.exec(expr);
if (m) {
const layer = m[1];
rest = m[2];
referenceIsOk = true;
objIdsToReference = this.resolvedTimeline.getLayerObjects(layer) ?? [];
allReferences.push(`$${layer}`);
}
}
}
for (let i = 0; i < objIdsToReference.length; i++) {
const refObjId = objIdsToReference[i];
if (refObjId === obj.id) {
// Looks like the object is referencing itself!
if (obj.resolved.resolving) {
obj.resolved.isSelfReferencing = true;
}
}
else {
const refObj = this.resolvedTimeline.getObject(refObjId);
if (refObj)
referencedObjs.push(refObj);
}
}
if (!referenceIsOk) {
return { result: null, allReferences: [] };
}
if (obj.resolved.isSelfReferencing) {
// Exclude any self-referencing objects:
referencedObjs = referencedObjs.filter((refObj) => {
return !refObj.resolved.isSelfReferencing;
});
}
if (referencedObjs.length) {
if (/start/.exec(rest))
ref = 'start';
else if (/end/.exec(rest))
ref = 'end';
else if (/duration/.exec(rest))
ref = 'duration';
if (ref === 'duration') {
// Duration refers to the first object on the resolved timeline
return this.lookupReferencedObjsDuration(obj, referencedObjs, allReferences);
}
else if (ref === 'start') {
return this.lookupReferencedObjs(obj, referencedObjs, allReferences, false, false);
}
else if (ref === 'end') {
return this.lookupReferencedObjs(obj, referencedObjs, allReferences, true, true);
}
else {
/* istanbul ignore next */
(0, lib_1.assertNever)(ref);
}
}
return { result: [], allReferences: allReferences };
}
else if (!expr) {
return { result: null, allReferences: [] };
}
else {
// expr is an expressionObj
return this.lookupExpressionObj(obj, context, expr);
}
}
applyParentInstances(parentInstances, value) {
return this.operateOnArrays(parentInstances, value, this.operateApplyParentInstance);
}
/**
* Perform an action on 2 arrays. Behaves somewhat like the ".*"-operator in Matlab
* @param array0
* @param array1
* @param operate
*/
operateOnArrays(array0, array1, operate) {
if (array0 === null || array1 === null)
return null;
if ((0, reference_1.isReference)(array0) && (0, reference_1.isReference)(array1)) {
return operate(array0, array1);
}
const result = [];
const minLength = Math.min((0, lib_1.isArray)(array0) ? array0.length : Infinity, (0, lib_1.isArray)(array1) ? array1.length : Infinity);
for (let i = 0; i < minLength; i++) {
const a = (0, lib_1.isArray)(array0)
? array0[i]
: { id: '@', start: array0.value, end: array0.value, references: array0.references };
const b = (0, lib_1.isArray)(array1)
? array1[i]
: { id: '@', start: array1.value, end: array1.value, references: array1.references };
const start = a.isFirst
? { value: a.start, references: a.references }
: b.isFirst
? { value: b.start, references: b.references }
: operate({ value: a.start, references: (0, reference_1.joinReferences)(a.references, a.id === '@' ? [] : `@${a.id}`) }, { value: b.start, references: (0, reference_1.joinReferences)(b.references, b.id === '@' ? [] : `@${b.id}`) });
const end = a.isFirst
? a.end !== null
? { value: a.end, references: a.references }
: null
: b.isFirst
? b.end !== null
? { value: b.end, references: b.references }
: null
: operate(a.end !== null
? {
value: a.end,
references: (0, reference_1.joinReferences)(a.references, a.id === '@' ? [] : `@${a.id}`),
}
: null, b.end !== null
? {
value: b.end,
references: (0, reference_1.joinReferences)(b.references, b.id === '@' ? [] : `@${b.id}`),
}
: null);
if (start !== null) {
result.push({
id: this.resolvedTimeline.getInstanceId(),
start: start.value,
end: end === null ? null : end.value,
references: (0, reference_1.joinReferences)(start.references, end !== null ? end.references : []),
caps: (0, cap_1.joinCaps)(a.caps, b.caps),
});
}
}
return this.instance.cleanInstances(result, false);
}
/**
* Look up the referenced objects (in the context of a duration-reference)
*/
lookupReferencedObjsDuration(obj, referencedObjs, allReferences) {
const instanceDurations = [];
for (let i = 0; i < referencedObjs.length; i++) {
const referencedObj = referencedObjs[i];
// Ensure that the referenced object is resolved.
// Note: This is where referenced object(s) are recursively resolved
this.resolvedTimeline.resolveTimelineObj(referencedObj);
if (referencedObj.resolved.resolvedReferences) {
if (obj.resolved.isSelfReferencing && referencedObj.resolved.isSelfReferencing) {
// If the querying object is self-referencing, exclude any other self-referencing objects,
// ignore the object
}
else {
const firstInstance = referencedObj.resolved.instances[0];
if (firstInstance) {
const duration = firstInstance.end !== null ? firstInstance.end - firstInstance.start : null;
if (duration !== null) {
instanceDurations.push({
value: duration,
references: (0, reference_1.joinReferences)([`#${referencedObj.id}`], firstInstance.references),
});
}
}
}
}
}
let firstDuration = null;
for (let i = 0; i < instanceDurations.length; i++) {
const d = instanceDurations[i];
if (firstDuration === null || d.value < firstDuration.value)
firstDuration = d;
}
return { result: firstDuration, allReferences: allReferences };
}
/**
* Look up the referenced objects
*/
lookupReferencedObjs(obj, referencedObjs, allReferences, invert, ignoreFirstIfZero) {
let referencedInstances = [];
for (let i = 0; i < referencedObjs.length; i++) {
const referencedObj = referencedObjs[i];
// Ensure that the referenced object is resolved.
// Note: This is where referenced object(s) are recursively resolved
this.resolvedTimeline.resolveTimelineObj(referencedObj);
if (referencedObj.resolved.resolvedReferences) {
if (obj.resolved.isSelfReferencing && referencedObj.resolved.isSelfReferencing) {
// If the querying object is self-referencing, exclude any other self-referencing objects,
// ignore the object
}
else {
referencedInstances = referencedInstances.concat(referencedObj.resolved.instances);
}
}
}
if (referencedInstances.length) {
if (invert) {
referencedInstances = this.instance.invertInstances(referencedInstances);
}
else {
referencedInstances = this.instance.cleanInstances(referencedInstances, true, true);
}
if (ignoreFirstIfZero) {
const first = referencedInstances[0];
if (first && first.start === 0) {
referencedInstances.splice(0, 1);
}
}
return { result: referencedInstances, allReferences: allReferences };
}
else {
return { result: [], allReferences: allReferences };
}
}
/**
* Look up an ExpressionObj
*/
lookupExpressionObj(obj, context, expr) {
const l = this.lookupExpression(obj, expr.l, context);
const r = this.lookupExpression(obj, expr.r, context);
const lookupExpr = {
l: l.result,
o: expr.o,
r: r.result,
};
const allReferences = l.allReferences.concat(r.allReferences);
if (lookupExpr.o === '!') {
// Invert, ie discard l, invert and return r:
if (lookupExpr.r && (0, lib_1.isArray)(lookupExpr.r)) {
return {
result: this.instance.invertInstances(lookupExpr.r),
allReferences: allReferences,
};
}
else {
// We can't invert a value
return {
result: lookupExpr.r,
allReferences: allReferences,
};
}
}
else if (lookupExpr.l === null || lookupExpr.r === null) {
return { result: null, allReferences: allReferences };
}
else if (lookupExpr.o === '&' || lookupExpr.o === '|') {
const combiner = new ReferenceAndOrCombiner(this.resolvedTimeline, lookupExpr.l, lookupExpr.r, lookupExpr.o);
const instances = combiner.calculateResult();
return { result: instances, allReferences: allReferences };
}
else {
const operate = operator_1.Operator.get(lookupExpr.o);
const result = this.operateOnArrays(lookupExpr.l, lookupExpr.r, operate);
return { result: result, allReferences: allReferences };
}
}
}
exports.ReferenceHandler = ReferenceHandler;
/** Helper class that deals with an And ('&') or an Or ('|') expression */
class ReferenceAndOrCombiner {
constructor(resolvedTimeline, leftOperand, rightOperand, operator) {
this.resolvedTimeline = resolvedTimeline;
this.leftOperand = leftOperand;
this.rightOperand = rightOperand;
this.events = [];
this.instances = [];
if (operator === '&') {
this.calcResult = (left, right) => !!(left && right);
}
else if (operator === '|') {
this.calcResult = (left, right) => !!(left || right);
}
else {
/* istanbul ignore next */
(0, lib_1.assertNever)(operator);
/* istanbul ignore next */
this.calcResult = () => false;
}
if ((0, lib_1.isArray)(leftOperand))
this._addInstanceEvents(leftOperand, true);
if ((0, lib_1.isArray)(rightOperand))
this._addInstanceEvents(rightOperand, false);
this.events = (0, event_1.sortEvents)(this.events);
}
_addInstanceEvents(instances, left) {
for (let i = 0; i < instances.length; i++) {
const instance = instances[i];
if (instance.start !== instance.end) {
// event doesn't actually exist...
this.events.push({
left: left,
time: instance.start,
value: true,
references: [],
data: true,
instance: instance,
});
if (instance.end !== null) {
this.events.push({
left: left,
time: instance.end,
value: false,
references: [],
data: false,
instance: instance,
});
}
}
}
}
calculateResult() {
let leftValue = (0, reference_1.isReference)(this.leftOperand) ? !!this.leftOperand.value : false;
let rightValue = (0, reference_1.isReference)(this.rightOperand) ? !!this.rightOperand.value : false;
let leftInstance = null;
let rightInstance = null;
let resultValue = this.calcResult(leftValue, rightValue);
this.updateInstance(0, resultValue, (0, reference_1.joinReferences)((0, reference_1.isReference)(this.leftOperand) ? this.leftOperand.references : [], (0, reference_1.isReference)(this.rightOperand) ? this.rightOperand.references : []), []);
for (let i = 0; i < this.events.length; i++) {
const e = this.events[i];
const next = this.events[i + 1];
if (e.left) {
leftValue = e.value;
leftInstance = e.instance;
}
else {
rightValue = e.value;
rightInstance = e.instance;
}
if (!next || next.time !== e.time) {
const newResultValue = this.calcResult(leftValue, rightValue);
const resultCaps = (leftInstance ? leftInstance.caps ?? [] : []).concat(rightInstance ? rightInstance.caps ?? [] : []);
if (newResultValue !== resultValue) {
this.updateInstance(e.time, newResultValue, (0, reference_1.joinReferences)(leftInstance ? leftInstance.references : [], rightInstance ? rightInstance.references : []), resultCaps);
resultValue = newResultValue;
}
}
}
return this.instances;
}
updateInstance(time, value, references, caps) {
if (value) {
this.instances.push({
id: this.resolvedTimeline.getInstanceId(),
start: time,
end: null,
references: references,
caps: caps,
});
}
else {
const lastInstance = (0, lib_1.last)(this.instances);
if (lastInstance) {
lastInstance.end = time;
// don't update reference on end
}
}
}
}
//# sourceMappingURL=ReferenceHandler.js.map