LN Math Expression
First version: - Dynamically checking the correctness of an expression in the IDE (Blender). - The task is divided into 2 parts: checking the correctness through python in the Blender interface and when performing calculations when the application is running in haxe. The following decisions are taken as a basis: python - http://repl.it/3xv/1 haxe - https://github.com/maitag/formula (added all the necessary classes to the node code)
This commit is contained in:
parent
09ec8f23f8
commit
9ab9d5c5db
1914
Sources/armory/logicnode/MathExpressionNode.hx
Normal file
1914
Sources/armory/logicnode/MathExpressionNode.hx
Normal file
File diff suppressed because it is too large
Load diff
190
blender/arm/logicnode/math/LN_math_expression.py
Normal file
190
blender/arm/logicnode/math/LN_math_expression.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
from arm.logicnode.arm_nodes import *
|
||||
import re
|
||||
|
||||
class MathExpressionNode(ArmLogicTreeNode):
|
||||
"""Mathematical operations on values."""
|
||||
bl_idname = 'LNMathExpressionNode'
|
||||
bl_label = 'Math Expression'
|
||||
arm_version = 1
|
||||
min_inputs = 2
|
||||
max_inputs = 10
|
||||
|
||||
@staticmethod
|
||||
def get_variable_name(index):
|
||||
return {
|
||||
0: 'a',
|
||||
1: 'b',
|
||||
2: 'c',
|
||||
3: 'd',
|
||||
4: 'e',
|
||||
5: 'x',
|
||||
6: 'y',
|
||||
7: 'h',
|
||||
8: 'i',
|
||||
9: 'k'
|
||||
}.get(index, 'a')
|
||||
|
||||
@staticmethod
|
||||
def get_clear_exp(value):
|
||||
return re.sub(r'[\-\+\*\/\(\)\^\%abcdexyhik0123456789. ]', '', value).strip()
|
||||
|
||||
@staticmethod
|
||||
def get_invalid_characters(value):
|
||||
value = value.replace(' ', '')
|
||||
len_v = len(value)
|
||||
arg = ['a', 'b', 'c', 'd', 'e', 'x', 'y', 'h', 'i', 'k']
|
||||
for i in range(len_v):
|
||||
s = value[i]
|
||||
if s == '.':
|
||||
if ((i - 1) < 0) or ((i + 1) >= len_v) or (not value[i - 1].isnumeric()) or (not value[i + 1].isnumeric()):
|
||||
return False
|
||||
oper = ['+', '-', '*', '/', '%', '^']
|
||||
if s == '(':
|
||||
if (i > 0) and ((value[i - 1] not in oper) and (value[i - 1] != '(')):
|
||||
return False
|
||||
if (i < (len_v - 1)) and ((value[i + 1] not in arg) and (not value[i + 1].isnumeric()) and (value[i + 1] != '(')):
|
||||
return False
|
||||
if s == ')':
|
||||
if (i > 0) and ((value[i - 1] not in arg) and (not value[i - 1].isnumeric()) and (value[i - 1] != ')')):
|
||||
return False
|
||||
if (i < (len_v - 1)) and (not value[i + 1].isnumeric()) and ((value[i + 1] not in oper) and (value[i + 1] != ')')):
|
||||
return False
|
||||
if s in oper:
|
||||
if ((i > 0) and (value[i - 1] in oper)) or ((i < (len_v - 1)) and (value[i + 1] in oper)):
|
||||
return False
|
||||
last_sym = value[len_v - 1]
|
||||
if (not last_sym.isnumeric()) and (last_sym not in arg) and (last_sym != ')'):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def check_variable(self, value):
|
||||
variables = re.sub(r'[\-\+\*\/\(\)\^\%0123456789. ]', '', value).strip()
|
||||
for vr in variables:
|
||||
check = False
|
||||
for inp_key in self.inputs.keys():
|
||||
if (vr == inp_key):
|
||||
check = True
|
||||
break
|
||||
if not check:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def matches(line, opendelim='(', closedelim=')'):
|
||||
stack = []
|
||||
for m in re.finditer(r'[{}{}]'.format(opendelim, closedelim), line):
|
||||
pos = m.start()
|
||||
if line[pos-1] == '\\':
|
||||
# Skip escape sequence
|
||||
continue
|
||||
c = line[pos]
|
||||
if c == opendelim:
|
||||
stack.append(pos+1)
|
||||
elif c == closedelim:
|
||||
if len(stack) > 0:
|
||||
prevpos = stack.pop()
|
||||
yield (True, prevpos, pos, len(stack))
|
||||
else:
|
||||
# Error
|
||||
yield (False, 0, 0, 0)
|
||||
pass
|
||||
if len(stack) > 0:
|
||||
for pos in stack:
|
||||
yield (False, 0, 0, 0)
|
||||
|
||||
@staticmethod
|
||||
def isPartCorrect(s):
|
||||
if len(s.replace('p', '').replace(' ', '').split()) == 0:
|
||||
return True
|
||||
REGEX = re.compile(r"(([abcdexyhikp]|\d+)[\+\-\/\*\^\%]){1,}([abcdexyhikp]|\d+)(=([abcdexyhikp]|\d+))?")
|
||||
result = False
|
||||
if REGEX.match(s):
|
||||
result = True
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def isCorrect(self, s):
|
||||
result = True
|
||||
if s.find("(") >=0 or s.find(")") >= 0:
|
||||
for correct, openpos, closepos, level in self.matches(s):
|
||||
if correct:
|
||||
part = s[openpos:closepos]
|
||||
if part.find("(") == -1 and part.find(")") == -1:
|
||||
if not self.isPartCorrect(part):
|
||||
result = False
|
||||
break
|
||||
part = s[openpos-1:closepos+1]
|
||||
replaced = s.replace(part, "p")
|
||||
if replaced.find("(") >=0 or replaced.find(")") >= 0:
|
||||
if not self.isCorrect(self, replaced):
|
||||
result = False
|
||||
break
|
||||
else:
|
||||
if not self.isPartCorrect(replaced):
|
||||
result = False
|
||||
break
|
||||
else:
|
||||
result = False
|
||||
break
|
||||
else:
|
||||
result = self.isPartCorrect(s)
|
||||
return result
|
||||
|
||||
def set_exp_error(self, value):
|
||||
self['exp_error'] = value
|
||||
|
||||
def get_exp_error(self):
|
||||
return self.get('exp_error', False)
|
||||
|
||||
def set_exp(self, value):
|
||||
value = value.lower()
|
||||
self['property0'] = value
|
||||
# Check errors
|
||||
val_error = False
|
||||
if len(self.get_clear_exp(value)) > 0:
|
||||
val_error = True
|
||||
elif not self.get_invalid_characters(value):
|
||||
val_error = True
|
||||
elif not self.check_variable(self, value):
|
||||
val_error = True
|
||||
elif not self.isCorrect(self, value.replace(' ', '')):
|
||||
val_error = True
|
||||
self.set_exp_error(val_error)
|
||||
|
||||
def get_exp(self):
|
||||
return self.get('property0', 'a + b')
|
||||
|
||||
property0: StringProperty(name='', description='Expression (operation: +, -, *, /, ^, (, ), %)', set=set_exp, get=get_exp)
|
||||
property1: BoolProperty(name='Clamp', default=False)
|
||||
|
||||
def __init__(self):
|
||||
array_nodes[str(id(self))] = self
|
||||
|
||||
def init(self, context):
|
||||
super(MathExpressionNode, self).init(context)
|
||||
self.add_input('NodeSocketFloat', self.get_variable_name(0), default_value=0.0)
|
||||
self.add_input('NodeSocketFloat', self.get_variable_name(1), default_value=0.0)
|
||||
self.add_output('NodeSocketFloat', 'Result')
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
layout.prop(self, 'property1')
|
||||
# Expression
|
||||
row = layout.row(align=True)
|
||||
column = row.column(align=True)
|
||||
column.alert = self.get_exp_error()
|
||||
column.prop(self, 'property0', icon='FORCE_HARMONIC')
|
||||
# Buttons
|
||||
row = layout.row(align=True)
|
||||
column = row.column(align=True)
|
||||
op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True)
|
||||
if len(self.inputs) == 10:
|
||||
column.enabled = False
|
||||
op.node_index = str(id(self))
|
||||
op.socket_type = 'NodeSocketFloat'
|
||||
op.name_format = self.get_variable_name(len(self.inputs))
|
||||
column = row.column(align=True)
|
||||
op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True)
|
||||
op.node_index = str(id(self))
|
||||
if len(self.inputs) == 2:
|
||||
column.enabled = False
|
Loading…
Reference in a new issue