Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/GUI/Expressions/expressiondialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ void addBasicDefs(QsciAPIs* const target)
target->add("Math.tanh(x)");
target->add("Math.trunc(x)");

target->add("valueAtTime(time)");
target->add("valueAtTime(\"bindingName\", time)");

// add expressions presets
const auto expressions = eSettings::sInstance->fExpressions.getAll();
for (const auto &expr : expressions) {
Expand Down Expand Up @@ -647,7 +650,7 @@ bool ExpressionDialog::apply(const bool action)
QJSValue eEvaluate;
try {
Expression::sAddScriptTo(scriptStr, bindings, *engine, eEvaluate,
Expression::sQrealAnimatorTester);
Expression::sQrealAnimatorTester, mTarget);
} catch (const std::exception& e) {
mScriptError->setText(e.what());
mBindingsButton->setIcon(mRedDotIcon);
Expand Down
3 changes: 2 additions & 1 deletion src/app/GUI/Expressions/expressionhighlighter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ ExpressionHighlighter::ExpressionHighlighter(
"\\s*=\\s*" + propPath);
mFrameValueSetRegex = QRegularExpression("^\\s*"
"([A-Za-z_][A-Za-z0-9_]*)"
"\\s*=\\s*(\\$frame|\\$scene.fps|\\$scene.width|\\$scene.height|\\$scene.rangeMin|\\$scene.rangeMax|\\$value)");
"\\s*=\\s*(\\$frame|\\$time|\\$scene.fps|\\$scene.width|\\$scene.height|\\$scene.rangeMin|\\$scene.rangeMax|\\$value)");
// const auto propPathRegex = QRegularExpression(propPath);

mPropPathFormat.setFontWeight(QFont::Bold);
Expand All @@ -62,6 +62,7 @@ ExpressionHighlighter::ExpressionHighlighter(
const QStringList specs = {
QStringLiteral("$value"),
QStringLiteral("$frame"),
QStringLiteral("$time"),
QStringLiteral("$scene.fps"),
QStringLiteral("$scene.width"),
QStringLiteral("$scene.height"),
Expand Down
2 changes: 1 addition & 1 deletion src/app/GUI/graphboxeslist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ bool KeysView::graphEasingApplyExpression(QrealAnimator *anim,
QJSValue eEvaluate;
try {
Expression::sAddScriptTo(script, bindings, *engine, eEvaluate,
Expression::sQrealAnimatorTester);
Expression::sQrealAnimatorTester, anim);
} catch(const std::exception& e) { return false; }

try {
Expand Down
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ set(
grid.cpp
Boxes/nullobject.cpp
Expressions/expression.cpp
Expressions/expressioncontext.cpp
Expressions/expressionpresets.cpp
Expressions/framebinding.cpp
Expressions/scenebinding.cpp
Expressions/timebinding.cpp
Expressions/propertybinding.cpp
Animators/SmartPath/listofnodes.cpp
Animators/SmartPath/smartpath.cpp
Expand Down
78 changes: 70 additions & 8 deletions src/core/Expressions/expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@

#include "expression.h"

#include "expressioncontext.h"
#include "exceptions.h"
#include "Private/esettings.h"
#include "canvas.h"

namespace {
const QString gExpressionContextName = "__frictionExpressionContext";
}

Expression::ResultTester Expression::sQrealAnimatorTester =
[](const QJSValue& val) {
Expand All @@ -42,12 +48,24 @@ Expression::Expression(const QString& definitionsStr,
mScriptStr(scriptStr),
mEEvaluate(std::move(eEvaluate)),
mBindings(std::move(bindings)),
mEngine(std::move(engine)) {
mEngine(std::move(engine)),
mExpressionContext(qobject_cast<ExpressionContext*>(
mEngine->globalObject().property(gExpressionContextName).toQObject())) {
for(const auto& binding : mBindings) {
connect(binding.second.get(), &PropertyBinding::currentValueChanged,
this, &Expression::currentValueChanged);
connect(binding.second.get(), &PropertyBinding::relRangeChanged,
this, &Expression::relRangeChanged);
this, [this](const FrameRange& range) {
emit relRangeChanged(mExpressionContext &&
mExpressionContext->usesTemporalSampling() ?
FrameRange::EMINMAX : range);
});
}
if(mExpressionContext) {
connect(mExpressionContext, &ExpressionContext::temporalSamplingUsed,
this, [this]() {
emit relRangeChanged(FrameRange::EMINMAX);
});
}
}

Expand Down Expand Up @@ -75,10 +93,45 @@ void Expression::sAddDefinitionsTo(const QString& definitionsStr,
void Expression::sAddScriptTo(const QString& scriptStr,
const PropertyBindingMap& bindings,
QJSEngine& e, QJSValue& eEvaluate,
const ResultTester& resultTester) {
const ResultTester& resultTester,
const Property* const context) {
if(bindings.find("valueAtTime") != bindings.end()) {
PrettyRuntimeThrow("'valueAtTime' is a reserved expression function");
}

const auto expressionContext =
new ExpressionContext(context, bindings, &e);
expressionContext->setParent(&e);
e.globalObject().setProperty(
gExpressionContextName, e.newQObject(expressionContext));

const auto builtinsResult = e.evaluate(
"var valueAtTime = function(sourceOrTime, time) {"
" if (arguments.length === 1) {"
" if (typeof sourceOrTime !== 'number' || !isFinite(sourceOrTime)) {"
" throw new TypeError(\"valueAtTime(): time must be a finite number\");"
" }"
" return __frictionExpressionContext.valueAtTime(sourceOrTime);"
" }"
" if (arguments.length === 2) {"
" if (typeof sourceOrTime !== 'string') {"
" throw new TypeError(\"valueAtTime(): the first argument must be a binding name\");"
" }"
" if (typeof time !== 'number' || !isFinite(time)) {"
" throw new TypeError(\"valueAtTime(): time must be a finite number\");"
" }"
" return __frictionExpressionContext.bindingValueAtTime(sourceOrTime, time);"
" }"
" throw new TypeError(\"valueAtTime() expects one or two arguments\");"
"};");
throwIfError(builtinsResult, "Expression built-ins");

QStringList bindingVars;
QJSValueList testArgs;
const auto scene = context ? context->getParentScene() : nullptr;
const int testAbsFrame = scene ? scene->getCurrentFrame() : 0;
for(const auto& binding : bindings) {
binding.second->setAbsFrame(testAbsFrame);
bindingVars << binding.first;
testArgs << binding.second->getJSValue(e);
}
Expand Down Expand Up @@ -108,7 +161,8 @@ qsptr<Expression> Expression::sCreate(const QString& bindingsStr,
auto engine = std::make_unique<QJSEngine>();
sAddDefinitionsTo(definitionsStr, *engine);
QJSValue eEvaluate;
sAddScriptTo(scriptStr, bindings, *engine, eEvaluate, resultTester);
sAddScriptTo(scriptStr, bindings, *engine, eEvaluate,
resultTester, context);
return sCreate(definitionsStr, scriptStr,
std::move(bindings),
std::move(engine),
Expand Down Expand Up @@ -138,6 +192,8 @@ bool Expression::setAbsFrame(const int absFrame) {
}

bool Expression::isStatic() const {
if(mExpressionContext && mExpressionContext->usesTemporalSampling())
return false;
return identicalRelRange(0) == FrameRange::EMINMAX;
}

Expand Down Expand Up @@ -169,16 +225,18 @@ QJSValue Expression::evaluate(const qreal relFrame)
{
QJSValueList values;
for (const auto& binding : mBindings) {
QString path = binding.second->path();
QJSValue val = binding.second->getJSValue(*mEngine, relFrame);
if (path == "$frame") { values << QJSValue(relFrame); }
else { values << val; }
values << binding.second->getJSValue(*mEngine, relFrame);
}
QJSValue res = mEEvaluate.call(values);
return res;
}

FrameRange Expression::identicalRelRange(const int absFrame) const {
if(mExpressionContext && mExpressionContext->usesTemporalSampling()) {
const int relFrame = qRound(
mExpressionContext->contextRelFrame(absFrame));
return {relFrame, relFrame};
}
FrameRange result{FrameRange::EMINMAX};
for(const auto& binding : mBindings) {
const auto prop = binding.second.get();
Expand All @@ -190,6 +248,10 @@ FrameRange Expression::identicalRelRange(const int absFrame) const {

FrameRange Expression::nextNonUnaryIdenticalRelRange(const int absFrame) const
{
if(mExpressionContext && mExpressionContext->usesTemporalSampling()) {
Q_UNUSED(absFrame)
return {FrameRange::EMAX/2, FrameRange::EMAX};
}
for (int i = absFrame; i < FrameRange::EMAX; i++) {
FrameRange result{FrameRange::EMINMAX};
int lowestMax = INT_MAX;
Expand Down
6 changes: 5 additions & 1 deletion src/core/Expressions/expression.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

#include "propertybindingparser.h"

class ExpressionContext;

class CORE_EXPORT Expression : public QObject {
Q_OBJECT
Expression(const QString& definitionsStr,
Expand All @@ -45,7 +47,8 @@ class CORE_EXPORT Expression : public QObject {
static void sAddScriptTo(const QString& scriptStr,
const PropertyBindingMap& bindings,
QJSEngine& e, QJSValue& eEvaluate,
const ResultTester& resultTester);
const ResultTester& resultTester,
const Property* context = nullptr);
static qsptr<Expression> sCreate(const QString& definitionsStr,
const QString& scriptStr,
PropertyBindingMap&& bindings,
Expand Down Expand Up @@ -88,6 +91,7 @@ class CORE_EXPORT Expression : public QObject {
QJSValue mEEvaluate;
const PropertyBindingMap mBindings;
const std::unique_ptr<QJSEngine> mEngine;
ExpressionContext* const mExpressionContext;
};

#endif // EXPRESSION_H
104 changes: 104 additions & 0 deletions src/core/Expressions/expressioncontext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
#
# Friction - https://friction.graphics
#
# Copyright (c) Ole-André Rodlie and contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
*/

#include "expressioncontext.h"

#include <QJSEngine>
#include <QtGlobal>

ExpressionContext::ExpressionContext(
const Property* const context,
const PropertyBindingMap& bindings,
QJSEngine* const engine) :
mContext(context),
mBindings(bindings),
mEngine(engine) {}

QJSValue ExpressionContext::valueAtTime(const qreal time) {
markTemporalSamplingUsed();
if(!mContext) {
return throwReferenceError(
"valueAtTime() has no expression property context");
}

qreal absFrame;
if(!absFrameAtTime(time, absFrame)) return QJSValue();
const qreal relFrame = mContext->prp_absFrameToRelFrameF(absFrame);
return mContext->prp_getBaseJSValue(*mEngine, relFrame);
}

QJSValue ExpressionContext::bindingValueAtTime(
const QString& bindingName, const qreal time) {
markTemporalSamplingUsed();
const auto binding = mBindings.find(bindingName);
if(binding == mBindings.end()) {
return throwReferenceError(
"valueAtTime(): unknown binding '" + bindingName + "'");
}

qreal absFrame;
if(!absFrameAtTime(time, absFrame)) return QJSValue();
return binding->second->getJSValueAtAbsFrame(*mEngine, absFrame);
}

qreal ExpressionContext::contextRelFrame(const qreal absFrame) const {
if(!mContext) return absFrame;
return mContext->prp_absFrameToRelFrameF(absFrame);
}

bool ExpressionContext::absFrameAtTime(const qreal time, qreal& absFrame) {
if(!qIsFinite(time)) {
throwTypeError("valueAtTime(): time must be a finite number");
return false;
}
if(!mContext) {
throwReferenceError(
"valueAtTime() has no expression property context");
return false;
}

const qreal fps = mContext->prp_getSceneFPS();
if(!qIsFinite(fps) || fps <= 0) {
throwTypeError("valueAtTime(): scene FPS must be greater than zero");
return false;
}

absFrame = time*fps;
if(!qIsFinite(absFrame) ||
absFrame <= FrameRange::EMIN || absFrame >= FrameRange::EMAX) {
throwRangeError("valueAtTime(): sampled frame is outside the valid range");
return false;
}
return true;
}

QJSValue ExpressionContext::throwTypeError(const QString& message) {
mEngine->throwError(QJSValue::TypeError, message);
return QJSValue();
}

QJSValue ExpressionContext::throwRangeError(const QString& message) {
mEngine->throwError(QJSValue::RangeError, message);
return QJSValue();
}

QJSValue ExpressionContext::throwReferenceError(const QString& message) {
mEngine->throwError(QJSValue::ReferenceError, message);
return QJSValue();
}

void ExpressionContext::markTemporalSamplingUsed() {
if(mUsesTemporalSampling) return;
mUsesTemporalSampling = true;
emit temporalSamplingUsed();
}
54 changes: 54 additions & 0 deletions src/core/Expressions/expressioncontext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
#
# Friction - https://friction.graphics
#
# Copyright (c) Ole-André Rodlie and contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
*/

#ifndef EXPRESSIONCONTEXT_H
#define EXPRESSIONCONTEXT_H

#include <QObject>
#include <QJSValue>

#include "propertybindingparser.h"

class QJSEngine;

class ExpressionContext : public QObject {
Q_OBJECT
public:
ExpressionContext(const Property* context,
const PropertyBindingMap& bindings,
QJSEngine* engine);

Q_INVOKABLE QJSValue valueAtTime(qreal time);
Q_INVOKABLE QJSValue bindingValueAtTime(const QString& bindingName,
qreal time);

bool usesTemporalSampling() const { return mUsesTemporalSampling; }
qreal contextRelFrame(const qreal absFrame) const;

signals:
void temporalSamplingUsed();

private:
bool absFrameAtTime(qreal time, qreal& absFrame);
QJSValue throwTypeError(const QString& message);
QJSValue throwRangeError(const QString& message);
QJSValue throwReferenceError(const QString& message);
void markTemporalSamplingUsed();

const qptr<const Property> mContext;
const PropertyBindingMap mBindings;
QJSEngine* const mEngine;
bool mUsesTemporalSampling = false;
};

#endif // EXPRESSIONCONTEXT_H
3 changes: 1 addition & 2 deletions src/core/Expressions/framebinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ QJSValue FrameBinding::getJSValue(QJSEngine& e) {

QJSValue FrameBinding::getJSValue(QJSEngine& e, const qreal relFrame) {
Q_UNUSED(e)
Q_UNUSED(relFrame)
return this->relFrame();
return relFrame;
}

FrameRange FrameBinding::identicalRelRange(const int absFrame) {
Expand Down
Loading
Loading