Skip to content
Merged
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
4 changes: 2 additions & 2 deletions meshroom/core/desc/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def __init__(self, name, label, description, value, range=None, group="allParams
self._valueType = int

def validateValue(self, value):
if value is None:
if value is None or isinstance(value, str):
return value
# Handle unsigned int values that are translated to int by shiboken and may overflow
try:
Expand Down Expand Up @@ -371,7 +371,7 @@ def __init__(self, name, label, description, value, range=None, group="allParams
self._valueType = float

def validateValue(self, value):
if value is None:
if value is None or isinstance(value, str):
return value
try:
return float(value)
Expand Down
39 changes: 24 additions & 15 deletions meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ RowLayout {
switch (attribute.type) {
case "IntParam":
case "FloatParam":
_reconstruction.setAttribute(root.attribute, Number(value))
// We don't set a number because we want to keep the invalid expression
// _reconstruction.setAttribute(root.attribute, Number(value))
_reconstruction.setAttribute(root.attribute, value)
updateAttributeLabel()
break
case "File":
Expand Down Expand Up @@ -605,14 +607,8 @@ RowLayout {
Component {
id: sliderComponent
RowLayout {
TextField {
IntValidator {
id: intValidator
}
DoubleValidator {
id: doubleValidator
locale: 'C' // Use '.' decimal separator disregarding the system locale
}
ExpressionTextField {
id: expressionTextField
implicitWidth: 100
Layout.fillWidth: !slider.active
enabled: root.editable
Expand All @@ -624,18 +620,31 @@ RowLayout {
// When the value change keep the text align to the left to be able to read the most important part
// of the number. When we are editing (item is in focus), the content should follow the editing.
autoScroll: activeFocus
validator: attribute.type === "FloatParam" ? doubleValidator : intValidator
onEditingFinished: setTextFieldAttribute(text)
isInt: attribute.type === "FloatParam" ? false : true

onEditingFinished: {
if (hasExprError)
setTextFieldAttribute(expressionTextField.text) // On the undo stack we keep the expression
else
setTextFieldAttribute(expressionTextField.evaluatedValue)
}
onAccepted: {
setTextFieldAttribute(text)

if (hasExprError)
setTextFieldAttribute(expressionTextField.text) // On the undo stack we keep the expression
else
setTextFieldAttribute(expressionTextField.evaluatedValue)
// When the text is too long, display the left part
// (with the most important values and cut the floating point details)
ensureVisible(0)
}

Component.onDestruction: {
if (activeFocus)
setTextFieldAttribute(text)
if (activeFocus) {
if (hasExprError)
setTextFieldAttribute(expressionTextField.text)
else
setTextFieldAttribute(expressionTextField.evaluatedValue)
}
}
Component.onCompleted: {
// When the text is too long, display the left part
Expand Down
96 changes: 96 additions & 0 deletions meshroom/ui/qml/Utils/ExpressionTextField.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Controls

TextField {
id: root

// evaluated numeric value (NaN if invalid)
// It helps keeping the connection that text has so that we don't loose ability to undo/reset
property real evaluatedValue: 0

property bool hasExprError: false
property bool isInt: false
property int decimals: 2

// Overlay for error state (red border on top of default background)
Rectangle {
anchors.fill: parent
radius: 4
border.color: "red"
color: "transparent"
visible: root.hasExprError
z: 1
}

function raiseError() {
hasExprError = true
}

function clearError() {
hasExprError = false
}

function reset(_value) {
clearError()
evaluatedValue = _value
if (isInt) {
root.text = _value.toFixed(0)
} else {
root.text = _value.toFixed(decimals)
}
}

function getEvalExpression(_text) {
try {
var result = MathEvaluator.eval(_text)
if (isInt)
result = Math.round(result)
else
result = result.toFixed(decimals)
return result
} catch (err) {
console.error("Error evaluating expression (", _text, "):", err)
return NaN
}
}

function refreshStatus() {
if (isNaN(getEvalExpression(root.text))) {
raiseError()
} else {
clearError()
}
}

function updateExpression() {
var result = getEvalExpression(root.text)
if (!isNaN(result)) {
evaluatedValue = result
clearError()
return result
} else {
evaluatedValue = NaN
raiseError()
return NaN
}
}

// When user commits input, evaluate but do NOT overwrite text
onAccepted: {
updateExpression()
}

onEditingFinished: {
updateExpression()
}

onTextChanged: {
if (!activeFocus) {
refreshStatus()
}
}

Component.onCompleted: {
refreshStatus()
}
}
61 changes: 61 additions & 0 deletions meshroom/ui/qml/Utils/mathEvaluator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.pragma library


var symbols = {
pi: Math.PI,
e: Math.E,
};

var functions = {
abs: Math.abs,
min: Math.min,
max: Math.max,
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
pow: Math.pow,
sqrt: Math.sqrt,
exp: Math.exp,
log: Math.log
};


function removeLeadingZeros(str) {
return str.replace(/\b0*(\d+(?:\.\d*)?)/g, (match, number) => {
// If the number starts with a decimal, add a leading zero
if (number.startsWith('.')) {
return '0' + number;
}
// Otherwise just return the number without leading zeros
return number;
});
}


/**
* Evaluate an expression
*
* @param {*} expr Math expression
* @returns float or int
*/
function eval(expr) {
// Warning : commas are for separating function args and dot to indicate decimals

// Only allow numbers, operators, parentheses, and function names
if (!/^[0-9+\-*/^()e.,\s]*$/.test(expr.replace(/\b[a-zA-Z]+\b/g, ""))) {
throw "Invalid characters in expression";
}

expr = removeLeadingZeros(expr);

// Replace symbols and functions
for (var symbol in symbols) {
expr = expr.replace(new RegExp("\\b" + symbol + "\\b", "g"), symbols[symbol]);
}
for (var func in functions) { // Warning : not really a map
expr = expr.replace(new RegExp("\\b" + func + "\\b", "g"), "Math." + func);
}

// Eval with function to avoid issues with undeclared variables
return Function('"use strict"; return (' + expr + ')')();
}
2 changes: 2 additions & 0 deletions meshroom/ui/qml/Utils/qmldir
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ singleton ExifOrientation 1.0 ExifOrientation.qml
# singleton Filepath 1.0 Filepath.qml
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
# singleton Transformations3DHelper 1.0 Transformations3DHelper.qml
MathEvaluator 1.0 mathEvaluator.js
ExpressionTextField 1.0 ExpressionTextField.qml
45 changes: 35 additions & 10 deletions meshroom/ui/qml/Viewer/HdrImageToolbar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts

import Controls 1.0
import Utils 1.0

FloatingPane {
id: root
Expand All @@ -16,8 +17,8 @@ FloatingPane {


property real slidersPowerValue: 4.0
property real gainValue: Math.pow(gainCtrl.value, slidersPowerValue)
property real gammaValue: Math.pow(gammaCtrl.value, slidersPowerValue)
property real gainValue: Math.pow(gainCtrl.value, slidersPowerValue).toFixed(2)
property real gammaValue: Math.pow(gammaCtrl.value, slidersPowerValue).toFixed(2)
property alias channelModeValue: channelsCtrl.value
property variant colorRGBA: null
property variant mousePosition: ({x:0, y:0})
Expand Down Expand Up @@ -112,21 +113,32 @@ FloatingPane {

onClicked: {
gainCtrl.value = gainDefaultValue
gainLabel.reset(gainValue)
}
}
TextField {
ExpressionTextField {
id: gainLabel

ToolTip.visible: ToolTip.text && hovered
ToolTip.delay: 100
ToolTip.text: "Color Gain (in linear colorspace)"

text: gainValue.toFixed(2)
text: gainValue
decimals: 2
Layout.preferredWidth: textMetrics_gainValue.width
selectByMouse: true
validator: doubleValidator
onAccepted: {
gainCtrl.value = Math.pow(Number(gainLabel.text), 1.0 / slidersPowerValue)
if (!gainLabel.hasExprError) {
if (gainLabel.evaluatedValue <= 0) {
gainLabel.evaluatedValue = 0
gainCtrl.value = gainLabel.evaluatedValue
} else {
gainCtrl.value = Math.pow(Number(gainLabel.evaluatedValue), 1.0 / slidersPowerValue)
}
} else {
gainLabel.evaluatedValue = 0
gainCtrl.value = gainLabel.evaluatedValue
}
}
}
Slider {
Expand All @@ -136,6 +148,7 @@ FloatingPane {
to: 2
value: gainDefaultValue
stepSize: 0.01
onMoved: gainLabel.reset(Math.pow(value, slidersPowerValue))
}
}

Expand All @@ -152,21 +165,32 @@ FloatingPane {

onClicked: {
gammaCtrl.value = gammaDefaultValue;
gammaLabel.reset(gammaValue)
}
}
TextField {
ExpressionTextField {
id: gammaLabel

ToolTip.visible: ToolTip.text && hovered
ToolTip.delay: 100
ToolTip.text: "Apply Gamma (after Gain and in linear colorspace)"

text: gammaValue.toFixed(2)
text: gammaValue
decimals: 2
Layout.preferredWidth: textMetrics_gainValue.width
selectByMouse: true
validator: doubleValidator
onAccepted: {
gammaCtrl.value = Math.pow(Number(gammaLabel.text), 1.0 / slidersPowerValue)
if (!gammaLabel.hasExprError) {
if (gammaLabel.evaluatedValue <= 0) {
gammaLabel.evaluatedValue = 0
gammaCtrl.value = gammaLabel.evaluatedValue
} else {
gammaCtrl.value = Math.pow(Number(gammaLabel.evaluatedValue), 1.0 / slidersPowerValue)
}
} else {
gainLabel.evaluatedValue = 0
gainCtrl.value = gainLabel.evaluatedValue
}
}
}
Slider {
Expand All @@ -176,6 +200,7 @@ FloatingPane {
to: 2
value: gammaDefaultValue
stepSize: 0.01
onMoved: gammaLabel.reset(Math.pow(value, slidersPowerValue))
}
}

Expand Down
Loading