Add various matrix related expression functions (#8603)
Adds a few new Expression functions with the goal to: - Simplify Placement, Rotation, Vector and Matrix object creation. - Add new matrix functions for rotation and translation.
This commit is contained in:
committed by
GitHub
parent
57aac275c7
commit
827af464e3
@@ -106,17 +106,19 @@ FC_LOG_LEVEL_INIT("Expression", true, true)
|
||||
_e.raiseException();\
|
||||
}while(0)
|
||||
|
||||
#define EXPR_PY_THROW(_expr) _EXPR_PY_THROW("",_expr)
|
||||
#define EXPR_PY_THROW(_expr) _EXPR_PY_THROW("", _expr)
|
||||
|
||||
#define EXPR_THROW(_msg) _EXPR_THROW(_msg,this)
|
||||
#define EXPR_THROW(_msg) _EXPR_THROW(_msg, this)
|
||||
|
||||
#define RUNTIME_THROW(_msg) __EXPR_THROW(Base::RuntimeError,_msg, static_cast<Expression*>(nullptr))
|
||||
#define ARGUMENT_THROW(_msg) EXPR_THROW("Invalid number of arguments: " _msg)
|
||||
|
||||
#define TYPE_THROW(_msg) __EXPR_THROW(Base::TypeError,_msg, static_cast<Expression*>(nullptr))
|
||||
#define RUNTIME_THROW(_msg) __EXPR_THROW(Base::RuntimeError, _msg, static_cast<Expression*>(nullptr))
|
||||
|
||||
#define PARSER_THROW(_msg) __EXPR_THROW(Base::ParserError,_msg, static_cast<Expression*>(nullptr))
|
||||
#define TYPE_THROW(_msg) __EXPR_THROW(Base::TypeError, _msg, static_cast<Expression*>(nullptr))
|
||||
|
||||
#define PY_THROW(_msg) __EXPR_THROW(Py::RuntimeError,_msg, static_cast<Expression*>(nullptr))
|
||||
#define PARSER_THROW(_msg) __EXPR_THROW(Base::ParserError, _msg, static_cast<Expression*>(nullptr))
|
||||
|
||||
#define PY_THROW(_msg) __EXPR_THROW(Py::RuntimeError, _msg, static_cast<Expression*>(nullptr))
|
||||
|
||||
static inline std::ostream &operator<<(std::ostream &os, const App::Expression *expr) {
|
||||
if(expr) {
|
||||
@@ -1729,60 +1731,91 @@ FunctionExpression::FunctionExpression(const DocumentObject *_owner, Function _f
|
||||
, args(_args)
|
||||
{
|
||||
switch (f) {
|
||||
case ABS:
|
||||
case ACOS:
|
||||
case ASIN:
|
||||
case ATAN:
|
||||
case ABS:
|
||||
case EXP:
|
||||
case LOG:
|
||||
case LOG10:
|
||||
case SIN:
|
||||
case SINH:
|
||||
case TAN:
|
||||
case TANH:
|
||||
case SQRT:
|
||||
case CBRT:
|
||||
case CEIL:
|
||||
case COS:
|
||||
case COSH:
|
||||
case ROUND:
|
||||
case TRUNC:
|
||||
case CEIL:
|
||||
case EXP:
|
||||
case FLOOR:
|
||||
case MINVERT:
|
||||
case STR:
|
||||
case HIDDENREF:
|
||||
case HREF:
|
||||
case LOG:
|
||||
case LOG10:
|
||||
case MINVERT:
|
||||
case ROTATIONX:
|
||||
case ROTATIONY:
|
||||
case ROTATIONZ:
|
||||
case ROUND:
|
||||
case SIN:
|
||||
case SINH:
|
||||
case SQRT:
|
||||
case STR:
|
||||
case TAN:
|
||||
case TANH:
|
||||
case TRUNC:
|
||||
if (args.size() != 1)
|
||||
EXPR_THROW("Invalid number of arguments: exactly one required.");
|
||||
ARGUMENT_THROW("exactly one required.");
|
||||
break;
|
||||
case PLACEMENT:
|
||||
if (args.size() > 3)
|
||||
ARGUMENT_THROW("exactly one, two, or three required.");
|
||||
break;
|
||||
case TRANSLATIONM:
|
||||
if (args.size() != 1 && args.size() != 3)
|
||||
ARGUMENT_THROW("exactly one or three required.");
|
||||
break;
|
||||
case MOD:
|
||||
case ATAN2:
|
||||
case MOD:
|
||||
case MROTATEX:
|
||||
case MROTATEY:
|
||||
case MROTATEZ:
|
||||
case POW:
|
||||
if (args.size() != 2)
|
||||
EXPR_THROW("Invalid number of arguments: exactly two required.");
|
||||
ARGUMENT_THROW("exactly two required.");
|
||||
break;
|
||||
case HYPOT:
|
||||
case CATH:
|
||||
case HYPOT:
|
||||
case ROTATION:
|
||||
if (args.size() < 2 || args.size() > 3)
|
||||
EXPR_THROW("Invalid number of arguments: exactly two, or three required.");
|
||||
ARGUMENT_THROW("exactly two, or three required.");
|
||||
break;
|
||||
case MTRANSLATE:
|
||||
case MSCALE:
|
||||
if (args.size() != 2 && args.size() != 4)
|
||||
ARGUMENT_THROW("exactly two or four required.");
|
||||
break;
|
||||
case MROTATE:
|
||||
if (args.size() < 2 || args.size() > 4)
|
||||
ARGUMENT_THROW("exactly two, three, or four required.");
|
||||
break;
|
||||
case VECTOR:
|
||||
if (args.size() != 3)
|
||||
ARGUMENT_THROW("exactly three required.");
|
||||
break;
|
||||
case MATRIX:
|
||||
if (args.size() > 16)
|
||||
ARGUMENT_THROW("exactly 16 or less required.");
|
||||
break;
|
||||
case STDDEV:
|
||||
case SUM:
|
||||
case AVERAGE:
|
||||
case COUNT:
|
||||
case MIN:
|
||||
case MAX:
|
||||
case CREATE:
|
||||
case MSCALE:
|
||||
case MAX:
|
||||
case MIN:
|
||||
case STDDEV:
|
||||
case SUM:
|
||||
if (args.empty())
|
||||
EXPR_THROW("Invalid number of arguments: at least one required.");
|
||||
ARGUMENT_THROW("at least one required.");
|
||||
break;
|
||||
case LIST:
|
||||
case TUPLE:
|
||||
break;
|
||||
case NONE:
|
||||
case AGGREGATES:
|
||||
case LAST:
|
||||
case NONE:
|
||||
default:
|
||||
PARSER_THROW("Unknown function");
|
||||
break;
|
||||
@@ -1998,6 +2031,76 @@ Py::Object FunctionExpression::evalAggregate(
|
||||
return pyFromQuantity(c->getQuantity());
|
||||
}
|
||||
|
||||
Base::Vector3d FunctionExpression::evaluateSecondVectorArgument(const Expression *expression, const std::vector<Expression*> &arguments)
|
||||
{
|
||||
Py::Tuple vectorValues;
|
||||
Py::Object secondParameter = arguments[1]->getPyValue();
|
||||
|
||||
if (arguments.size() == 2) {
|
||||
if (!secondParameter.isSequence())
|
||||
_EXPR_THROW("Second parameter is not a sequence type: '" << secondParameter.as_string() << "'.", expression);
|
||||
if (PySequence_Size(secondParameter.ptr()) != 3)
|
||||
_EXPR_THROW("Second parameter provided has " << PySequence_Size(secondParameter.ptr()) << " elements instead of 3.", expression);
|
||||
|
||||
vectorValues = Py::Tuple(Py::Sequence(secondParameter));
|
||||
} else {
|
||||
vectorValues = Py::Tuple(3);
|
||||
vectorValues.setItem(0, secondParameter);
|
||||
vectorValues.setItem(1, arguments[2]->getPyValue());
|
||||
vectorValues.setItem(2, arguments[3]->getPyValue());
|
||||
}
|
||||
|
||||
Vector3d vector;
|
||||
if (!PyArg_ParseTuple(vectorValues.ptr(), "ddd", &vector.x, &vector.y, &vector.z)) {
|
||||
PyErr_Clear();
|
||||
_EXPR_THROW("Error parsing scale values.", expression);
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
|
||||
void FunctionExpression::initialiseObject(const Py::Object *object, const std::vector<Expression*> &arguments, const unsigned long offset)
|
||||
{
|
||||
if (arguments.size() > offset) {
|
||||
Py::Tuple constructorArguments(arguments.size() - offset);
|
||||
for (unsigned i = offset; i < arguments.size(); ++i)
|
||||
constructorArguments.setItem(i - offset, arguments[i]->getPyValue());
|
||||
Py::Dict kwd;
|
||||
PyObjectBase::__PyInit(object->ptr(), constructorArguments.ptr(), kwd.ptr());
|
||||
}
|
||||
}
|
||||
|
||||
Py::Object FunctionExpression::transformFirstArgument(
|
||||
const Expression* expression,
|
||||
const std::vector<Expression*> &arguments,
|
||||
const Base::Matrix4D* transformationMatrix
|
||||
)
|
||||
{
|
||||
Py::Object target = arguments[0]->getPyValue();
|
||||
|
||||
if (PyObject_TypeCheck(target.ptr(), &Base::MatrixPy::Type)) {
|
||||
Base::Matrix4D matrix = static_cast<Base::MatrixPy*>(target.ptr())->value();
|
||||
return Py::asObject(new Base::MatrixPy(*transformationMatrix * matrix));
|
||||
} else if (PyObject_TypeCheck(target.ptr(), &Base::PlacementPy::Type)) {
|
||||
Base::Matrix4D placementMatrix =
|
||||
static_cast<Base::PlacementPy*>(target.ptr())->getPlacementPtr()->toMatrix();
|
||||
return Py::asObject(new Base::PlacementPy(Base::Placement(*transformationMatrix * placementMatrix)));
|
||||
} else if (PyObject_TypeCheck(target.ptr(), &Base::RotationPy::Type)) {
|
||||
Base::Matrix4D rotatioMatrix;
|
||||
static_cast<Base::RotationPy*>(target.ptr())->getRotationPtr()->getValue(rotatioMatrix);
|
||||
return Py::asObject(new Base::RotationPy(Base::Rotation(*transformationMatrix * rotatioMatrix)));
|
||||
}
|
||||
|
||||
_EXPR_THROW("Function requires the first argument to be either Matrix, Placement or Rotation.", expression);
|
||||
}
|
||||
|
||||
Py::Object FunctionExpression::translationMatrix(double x, double y, double z)
|
||||
{
|
||||
Base::Matrix4D matrix;
|
||||
matrix.move(x, y, z);
|
||||
return Py::asObject(new Base::MatrixPy(matrix));
|
||||
}
|
||||
|
||||
Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std::vector<Expression*> &args)
|
||||
{
|
||||
if(!expr || !expr->getOwner())
|
||||
@@ -2007,103 +2110,163 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
if (f > AGGREGATES)
|
||||
return evalAggregate(expr, f, args);
|
||||
|
||||
if(f == LIST) {
|
||||
if(args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
|
||||
switch (f) {
|
||||
case LIST: {
|
||||
if (args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
|
||||
return args[0]->getPyValue();
|
||||
Py::List list(args.size());
|
||||
int i=0;
|
||||
for(auto &arg : args)
|
||||
list.setItem(i++,arg->getPyValue());
|
||||
int i = 0;
|
||||
for (auto &arg : args)
|
||||
list.setItem(i++, arg->getPyValue());
|
||||
return list;
|
||||
} else if (f == TUPLE) {
|
||||
if(args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
|
||||
}
|
||||
case TUPLE: {
|
||||
if (args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
|
||||
return Py::Tuple(args[0]->getPyValue());
|
||||
Py::Tuple tuple(args.size());
|
||||
int i=0;
|
||||
for(auto &arg : args)
|
||||
tuple.setItem(i++,arg->getPyValue());
|
||||
int i = 0;
|
||||
for (auto &arg : args)
|
||||
tuple.setItem(i++, arg->getPyValue());
|
||||
return tuple;
|
||||
} else if (f == MSCALE) {
|
||||
if(args.size() < 2)
|
||||
_EXPR_THROW("Function requires at least two arguments.",expr);
|
||||
Py::Object pymat = args[0]->getPyValue();
|
||||
Py::Object pyscale;
|
||||
if(PyObject_TypeCheck(pymat.ptr(),&Base::MatrixPy::Type)) {
|
||||
if(args.size() == 2) {
|
||||
Py::Object obj = args[1]->getPyValue();
|
||||
if(obj.isSequence() && PySequence_Size(obj.ptr())==3)
|
||||
pyscale = Py::Tuple(Py::Sequence(obj));
|
||||
} else if(args.size() == 4) {
|
||||
Py::Tuple tuple(3);
|
||||
tuple.setItem(0,args[1]->getPyValue());
|
||||
tuple.setItem(1,args[2]->getPyValue());
|
||||
tuple.setItem(2,args[3]->getPyValue());
|
||||
pyscale = tuple;
|
||||
}
|
||||
}
|
||||
if(!pyscale.isNone()) {
|
||||
Base::Vector3d vec;
|
||||
if (!PyArg_ParseTuple(pyscale.ptr(), "ddd", &vec.x,&vec.y,&vec.z))
|
||||
PyErr_Clear();
|
||||
else {
|
||||
auto mat = static_cast<Base::MatrixPy*>(pymat.ptr())->value();
|
||||
mat.scale(vec);
|
||||
return Py::asObject(new Base::MatrixPy(mat));
|
||||
}
|
||||
}
|
||||
_EXPR_THROW("Function requires arguments to be either "
|
||||
"(matrix,vector) or (matrix,number,number,number).", expr);
|
||||
}
|
||||
}
|
||||
|
||||
if(args.empty())
|
||||
_EXPR_THROW("Function requires at least one argument.",expr);
|
||||
|
||||
if (f == MINVERT) {
|
||||
switch (f) {
|
||||
case MINVERT: {
|
||||
Py::Object pyobj = args[0]->getPyValue();
|
||||
if (PyObject_TypeCheck(pyobj.ptr(),&Base::MatrixPy::Type)) {
|
||||
if (PyObject_TypeCheck(pyobj.ptr(), &Base::MatrixPy::Type)) {
|
||||
auto m = static_cast<Base::MatrixPy*>(pyobj.ptr())->value();
|
||||
if (fabs(m.determinant()) <= DBL_EPSILON)
|
||||
_EXPR_THROW("Cannot invert singular matrix.",expr);
|
||||
_EXPR_THROW("Cannot invert singular matrix.", expr);
|
||||
m.inverseGauss();
|
||||
return Py::asObject(new Base::MatrixPy(m));
|
||||
|
||||
} else if (PyObject_TypeCheck(pyobj.ptr(),&Base::PlacementPy::Type)) {
|
||||
} else if (PyObject_TypeCheck(pyobj.ptr(), &Base::PlacementPy::Type)) {
|
||||
const auto &pla = *static_cast<Base::PlacementPy*>(pyobj.ptr())->getPlacementPtr();
|
||||
return Py::asObject(new Base::PlacementPy(pla.inverse()));
|
||||
|
||||
} else if (PyObject_TypeCheck(pyobj.ptr(),&Base::RotationPy::Type)) {
|
||||
} else if (PyObject_TypeCheck(pyobj.ptr(), &Base::RotationPy::Type)) {
|
||||
const auto &rot = *static_cast<Base::RotationPy*>(pyobj.ptr())->getRotationPtr();
|
||||
return Py::asObject(new Base::RotationPy(rot.inverse()));
|
||||
}
|
||||
_EXPR_THROW("Function requires the first argument to be either Matrix, Placement or Rotation.",expr);
|
||||
_EXPR_THROW(
|
||||
"Function requires the first argument to be either Matrix, Placement or Rotation.",
|
||||
expr);
|
||||
break;
|
||||
}
|
||||
case MROTATE: {
|
||||
Py::Object rotationObject = args[1]->getPyValue();
|
||||
if (!PyObject_TypeCheck(rotationObject.ptr(), &Base::RotationPy::Type))
|
||||
{
|
||||
rotationObject = Py::asObject(new Base::RotationPy(Base::Rotation()));
|
||||
initialiseObject(&rotationObject, args, 1);
|
||||
}
|
||||
|
||||
} else if (f == CREATE) {
|
||||
Base::Matrix4D rotationMatrix;
|
||||
static_cast<Base::RotationPy*>(rotationObject.ptr())->getRotationPtr()->getValue(rotationMatrix);
|
||||
|
||||
return transformFirstArgument(expr, args, &rotationMatrix);
|
||||
}
|
||||
case MROTATEX:
|
||||
case MROTATEY:
|
||||
case MROTATEZ:
|
||||
{
|
||||
Py::Object rotationAngleParameter = args[1]->getPyValue();
|
||||
Quantity rotationAngle = pyToQuantity(rotationAngleParameter, expr, "Invalid rotation angle.");
|
||||
|
||||
if (!(rotationAngle.isDimensionlessOrUnit(Unit::Angle)))
|
||||
_EXPR_THROW("Unit must be either empty or an angle.", expr);
|
||||
|
||||
Rotation rotation = Base::Rotation(
|
||||
Vector3d(static_cast<double>(f == MROTATEX), static_cast<double>(f == MROTATEY), static_cast<double>(f == MROTATEZ)),
|
||||
rotationAngle.getValue() * M_PI / 180.0);
|
||||
Base::Matrix4D rotationMatrix;
|
||||
rotation.getValue(rotationMatrix);
|
||||
|
||||
return transformFirstArgument(expr, args, &rotationMatrix);
|
||||
}
|
||||
case MSCALE: {
|
||||
Vector3d scaleValues = evaluateSecondVectorArgument(expr, args);
|
||||
|
||||
Base::Matrix4D scaleMatrix;
|
||||
scaleMatrix.scale(scaleValues);
|
||||
|
||||
return transformFirstArgument(expr, args, &scaleMatrix);
|
||||
}
|
||||
case MTRANSLATE: {
|
||||
Vector3d translateValues = evaluateSecondVectorArgument(expr, args);
|
||||
|
||||
Base::Matrix4D translateMatrix;
|
||||
translateMatrix.move(translateValues);
|
||||
|
||||
Py::Object target = args[0]->getPyValue();
|
||||
if (PyObject_TypeCheck(target.ptr(), &Base::RotationPy::Type)) {
|
||||
Base::Matrix4D targetRotatioMatrix;
|
||||
static_cast<Base::RotationPy*>(target.ptr())->getRotationPtr()->getValue(targetRotatioMatrix);
|
||||
return Py::asObject(new Base::PlacementPy(Base::Placement(translateMatrix * targetRotatioMatrix)));
|
||||
}
|
||||
|
||||
return transformFirstArgument(expr, args, &translateMatrix);
|
||||
}
|
||||
case CREATE: {
|
||||
Py::Object pytype = args[0]->getPyValue();
|
||||
if(!pytype.isString())
|
||||
_EXPR_THROW("Function requires the first argument to be a string.",expr);
|
||||
if (!pytype.isString())
|
||||
_EXPR_THROW("Function requires the first argument to be a string.", expr);
|
||||
std::string type(pytype.as_string());
|
||||
Py::Object res;
|
||||
if(boost::iequals(type,"matrix"))
|
||||
if (boost::iequals(type, "matrix"))
|
||||
res = Py::asObject(new Base::MatrixPy(Base::Matrix4D()));
|
||||
else if(boost::iequals(type,"vector"))
|
||||
else if (boost::iequals(type, "vector"))
|
||||
res = Py::asObject(new Base::VectorPy(Base::Vector3d()));
|
||||
else if(boost::iequals(type,"placement"))
|
||||
else if (boost::iequals(type, "placement"))
|
||||
res = Py::asObject(new Base::PlacementPy(Base::Placement()));
|
||||
else if(boost::iequals(type,"rotation"))
|
||||
else if (boost::iequals(type, "rotation"))
|
||||
res = Py::asObject(new Base::RotationPy(Base::Rotation()));
|
||||
else
|
||||
_EXPR_THROW("Unknown type '" << type << "'.",expr);
|
||||
if(args.size()>1) {
|
||||
Py::Tuple tuple(args.size()-1);
|
||||
for(unsigned i=1;i<args.size();++i)
|
||||
tuple.setItem(i-1,args[i]->getPyValue());
|
||||
Py::Dict dict;
|
||||
PyObjectBase::__PyInit(res.ptr(),tuple.ptr(),dict.ptr());
|
||||
}
|
||||
_EXPR_THROW("Unknown type '" << type << "'.", expr);
|
||||
initialiseObject(&res, args, 1);
|
||||
return res;
|
||||
} else if (f == STR) {
|
||||
}
|
||||
case MATRIX: {
|
||||
Py::Object matrix = Py::asObject(new Base::MatrixPy(Base::Matrix4D()));
|
||||
initialiseObject(&matrix, args);
|
||||
return matrix;
|
||||
}
|
||||
case PLACEMENT: {
|
||||
Py::Object placement = Py::asObject(new Base::PlacementPy(Base::Placement()));
|
||||
initialiseObject(&placement, args);
|
||||
return placement;
|
||||
}
|
||||
case ROTATION: {
|
||||
Py::Object rotation = Py::asObject(new Base::RotationPy(Base::Rotation()));
|
||||
initialiseObject(&rotation, args);
|
||||
return rotation;
|
||||
}
|
||||
case STR:
|
||||
return Py::String(args[0]->getPyValue().as_string());
|
||||
} else if (f == HIDDENREF || f == HREF) {
|
||||
case TRANSLATIONM: {
|
||||
if (args.size() != 1)
|
||||
break; // Break and proceed to 3 size version.
|
||||
Py::Object parameter = args[0]->getPyValue();
|
||||
if (!parameter.isSequence())
|
||||
_EXPR_THROW("Not sequence type: '" << parameter.as_string() << "'.", expr);
|
||||
if (PySequence_Size(parameter.ptr()) != 3)
|
||||
_EXPR_THROW("Sequence provided has " << PySequence_Size(parameter.ptr()) << " elements instead of 3.", expr);
|
||||
double x, y, z;
|
||||
if (!PyArg_ParseTuple(Py::Tuple(Py::Sequence(parameter)).ptr(), "ddd", &x, &y, &z)) {
|
||||
PyErr_Clear();
|
||||
_EXPR_THROW("Error parsing sequence.", expr);
|
||||
}
|
||||
return translationMatrix(x, y, z);
|
||||
}
|
||||
case VECTOR: {
|
||||
Py::Object vector = Py::asObject(new Base::VectorPy(Base::Vector3d()));
|
||||
initialiseObject(&vector, args);
|
||||
return vector;
|
||||
}
|
||||
case HIDDENREF:
|
||||
case HREF:
|
||||
return args[0]->getPyValue();
|
||||
}
|
||||
|
||||
@@ -2111,13 +2274,13 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
Quantity v1 = pyToQuantity(e1,expr,"Invalid first argument.");
|
||||
Py::Object e2;
|
||||
Quantity v2;
|
||||
if(args.size()>1) {
|
||||
if (args.size() > 1) {
|
||||
e2 = args[1]->getPyValue();
|
||||
v2 = pyToQuantity(e2,expr,"Invalid second argument.");
|
||||
}
|
||||
Py::Object e3;
|
||||
Quantity v3;
|
||||
if(args.size()>2) {
|
||||
if (args.size() > 2) {
|
||||
e3 = args[2]->getPyValue();
|
||||
v3 = pyToQuantity(e3,expr,"Invalid third argument.");
|
||||
}
|
||||
@@ -2133,8 +2296,11 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
case COS:
|
||||
case SIN:
|
||||
case TAN:
|
||||
if (!(v1.getUnit() == Unit::Angle || v1.getUnit().isEmpty()))
|
||||
_EXPR_THROW("Unit must be either empty or an angle.",expr);
|
||||
case ROTATIONX:
|
||||
case ROTATIONY:
|
||||
case ROTATIONZ:
|
||||
if (!(v1.isDimensionlessOrUnit(Unit::Angle)))
|
||||
_EXPR_THROW("Unit must be either empty or an angle.", expr);
|
||||
|
||||
// Convert value to radians
|
||||
value *= M_PI / 180.0;
|
||||
@@ -2143,8 +2309,8 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
case ACOS:
|
||||
case ASIN:
|
||||
case ATAN:
|
||||
if (!v1.getUnit().isEmpty())
|
||||
_EXPR_THROW("Unit must be empty.",expr);
|
||||
if (!v1.isDimensionless())
|
||||
_EXPR_THROW("Unit must be empty.", expr);
|
||||
unit = Unit::Angle;
|
||||
scaler = 180.0 / M_PI;
|
||||
break;
|
||||
@@ -2154,7 +2320,7 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
case SINH:
|
||||
case TANH:
|
||||
case COSH:
|
||||
if (!v1.getUnit().isEmpty())
|
||||
if (!v1.isDimensionless())
|
||||
_EXPR_THROW("Unit must be empty.",expr);
|
||||
unit = Unit();
|
||||
break;
|
||||
@@ -2233,12 +2399,12 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
if (e2.isNone())
|
||||
_EXPR_THROW("Invalid second argument.",expr);
|
||||
|
||||
if (!v2.getUnit().isEmpty())
|
||||
if (!v2.isDimensionless())
|
||||
_EXPR_THROW("Exponent is not allowed to have a unit.",expr);
|
||||
|
||||
// Compute new unit for exponentiation
|
||||
double exponent = v2.getValue();
|
||||
if (!v1.getUnit().isEmpty()) {
|
||||
if (!v1.isDimensionless()) {
|
||||
if (exponent - boost::math::round(exponent) < 1e-9)
|
||||
unit = v1.getUnit().pow(exponent);
|
||||
else
|
||||
@@ -2261,6 +2427,10 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
}
|
||||
unit = v1.getUnit();
|
||||
break;
|
||||
case TRANSLATIONM:
|
||||
if (v1.isDimensionlessOrUnit(Unit::Length) && v2.isDimensionlessOrUnit(Unit::Length) && v3.isDimensionlessOrUnit(Unit::Length))
|
||||
break;
|
||||
_EXPR_THROW("Translation units must be a length or dimensionless.", expr);
|
||||
default:
|
||||
_EXPR_THROW("Unknown function: " << f,0);
|
||||
}
|
||||
@@ -2344,6 +2514,14 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
|
||||
case FLOOR:
|
||||
output = floor(value);
|
||||
break;
|
||||
case ROTATIONX:
|
||||
case ROTATIONY:
|
||||
case ROTATIONZ:
|
||||
return Py::asObject(new Base::RotationPy(Base::Rotation(
|
||||
Vector3d(static_cast<double>(f == ROTATIONX), static_cast<double>(f == ROTATIONY), static_cast<double>(f == ROTATIONZ)),
|
||||
value)));
|
||||
case TRANSLATIONM:
|
||||
return translationMatrix(v1.getValue(), v2.getValue(), v3.getValue());
|
||||
default:
|
||||
_EXPR_THROW("Unknown function: " << f,0);
|
||||
}
|
||||
@@ -2397,82 +2575,108 @@ Expression *FunctionExpression::simplify() const
|
||||
void FunctionExpression::_toString(std::ostream &ss, bool persistent,int) const
|
||||
{
|
||||
switch (f) {
|
||||
case ABS:
|
||||
ss << "abs("; break;;
|
||||
case ACOS:
|
||||
ss << "acos("; break;;
|
||||
case ASIN:
|
||||
ss << "asin("; break;;
|
||||
case ATAN:
|
||||
ss << "atan("; break;;
|
||||
case ABS:
|
||||
ss << "abs("; break;;
|
||||
case EXP:
|
||||
ss << "exp("; break;;
|
||||
case LOG:
|
||||
ss << "log("; break;;
|
||||
case LOG10:
|
||||
ss << "log10("; break;;
|
||||
case SIN:
|
||||
ss << "sin("; break;;
|
||||
case SINH:
|
||||
ss << "sinh("; break;;
|
||||
case TAN:
|
||||
ss << "tan("; break;;
|
||||
case TANH:
|
||||
ss << "tanh("; break;;
|
||||
case SQRT:
|
||||
ss << "sqrt("; break;;
|
||||
case ATAN2:
|
||||
ss << "atan2("; break;;
|
||||
case CATH:
|
||||
ss << "cath("; break;;
|
||||
case CBRT:
|
||||
ss << "cbrt("; break;;
|
||||
case CEIL:
|
||||
ss << "ceil("; break;;
|
||||
case COS:
|
||||
ss << "cos("; break;;
|
||||
case COSH:
|
||||
ss << "cosh("; break;;
|
||||
case MOD:
|
||||
ss << "mod("; break;;
|
||||
case ATAN2:
|
||||
ss << "atan2("; break;;
|
||||
case POW:
|
||||
ss << "pow("; break;;
|
||||
case HYPOT:
|
||||
ss << "hypot("; break;;
|
||||
case CATH:
|
||||
ss << "cath("; break;;
|
||||
case ROUND:
|
||||
ss << "round("; break;;
|
||||
case TRUNC:
|
||||
ss << "trunc("; break;;
|
||||
case CEIL:
|
||||
ss << "ceil("; break;;
|
||||
case EXP:
|
||||
ss << "exp("; break;;
|
||||
case FLOOR:
|
||||
ss << "floor("; break;;
|
||||
case SUM:
|
||||
ss << "sum("; break;;
|
||||
case COUNT:
|
||||
ss << "count("; break;;
|
||||
case AVERAGE:
|
||||
ss << "average("; break;;
|
||||
case STDDEV:
|
||||
ss << "stddev("; break;;
|
||||
case MIN:
|
||||
ss << "min("; break;;
|
||||
case MAX:
|
||||
ss << "max("; break;;
|
||||
case LIST:
|
||||
ss << "list("; break;;
|
||||
case TUPLE:
|
||||
ss << "tuple("; break;;
|
||||
case MSCALE:
|
||||
ss << "mscale("; break;;
|
||||
case HYPOT:
|
||||
ss << "hypot("; break;;
|
||||
case LOG:
|
||||
ss << "log("; break;;
|
||||
case LOG10:
|
||||
ss << "log10("; break;;
|
||||
case MOD:
|
||||
ss << "mod("; break;;
|
||||
case POW:
|
||||
ss << "pow("; break;;
|
||||
case ROUND:
|
||||
ss << "round("; break;;
|
||||
case SIN:
|
||||
ss << "sin("; break;;
|
||||
case SINH:
|
||||
ss << "sinh("; break;;
|
||||
case SQRT:
|
||||
ss << "sqrt("; break;;
|
||||
case TAN:
|
||||
ss << "tan("; break;;
|
||||
case TANH:
|
||||
ss << "tanh("; break;;
|
||||
case TRUNC:
|
||||
ss << "trunc("; break;;
|
||||
case MINVERT:
|
||||
ss << "minvert("; break;;
|
||||
case MROTATE:
|
||||
ss << "mrotate("; break;;
|
||||
case MROTATEX:
|
||||
ss << "mrotatex("; break;;
|
||||
case MROTATEY:
|
||||
ss << "mrotatey("; break;;
|
||||
case MROTATEZ:
|
||||
ss << "mrotatez("; break;;
|
||||
case MSCALE:
|
||||
ss << "mscale("; break;;
|
||||
case MTRANSLATE:
|
||||
ss << "mtranslate("; break;;
|
||||
case CREATE:
|
||||
ss << "create("; break;;
|
||||
case LIST:
|
||||
ss << "list("; break;;
|
||||
case MATRIX:
|
||||
ss << "matrix("; break;;
|
||||
case PLACEMENT:
|
||||
ss << "placement("; break;;
|
||||
case ROTATION:
|
||||
ss << "rotation("; break;;
|
||||
case ROTATIONX:
|
||||
ss << "rotationx("; break;;
|
||||
case ROTATIONY:
|
||||
ss << "rotationy("; break;;
|
||||
case ROTATIONZ:
|
||||
ss << "rotationz("; break;;
|
||||
case STR:
|
||||
ss << "str("; break;;
|
||||
case TRANSLATIONM:
|
||||
ss << "translationm("; break;;
|
||||
case TUPLE:
|
||||
ss << "tuple("; break;;
|
||||
case VECTOR:
|
||||
ss << "vector("; break;;
|
||||
case HIDDENREF:
|
||||
ss << "hiddenref("; break;;
|
||||
case HREF:
|
||||
ss << "href("; break;;
|
||||
case AVERAGE:
|
||||
ss << "average("; break;;
|
||||
case COUNT:
|
||||
ss << "count("; break;;
|
||||
case MAX:
|
||||
ss << "max("; break;;
|
||||
case MIN:
|
||||
ss << "min("; break;;
|
||||
case STDDEV:
|
||||
ss << "stddev("; break;;
|
||||
case SUM:
|
||||
ss << "sum("; break;;
|
||||
default:
|
||||
ss << fname << "("; break;;
|
||||
}
|
||||
@@ -3267,46 +3471,62 @@ static void initParser(const App::DocumentObject *owner)
|
||||
unitExpression = valueExpression = false;
|
||||
|
||||
if (!has_registered_functions) {
|
||||
registered_functions["abs"] = FunctionExpression::ABS;
|
||||
registered_functions["acos"] = FunctionExpression::ACOS;
|
||||
registered_functions["asin"] = FunctionExpression::ASIN;
|
||||
registered_functions["atan"] = FunctionExpression::ATAN;
|
||||
registered_functions["abs"] = FunctionExpression::ABS;
|
||||
registered_functions["exp"] = FunctionExpression::EXP;
|
||||
registered_functions["log"] = FunctionExpression::LOG;
|
||||
registered_functions["log10"] = FunctionExpression::LOG10;
|
||||
registered_functions["sin"] = FunctionExpression::SIN;
|
||||
registered_functions["sinh"] = FunctionExpression::SINH;
|
||||
registered_functions["tan"] = FunctionExpression::TAN;
|
||||
registered_functions["tanh"] = FunctionExpression::TANH;
|
||||
registered_functions["sqrt"] = FunctionExpression::SQRT;
|
||||
registered_functions["atan2"] = FunctionExpression::ATAN2;
|
||||
registered_functions["cath"] = FunctionExpression::CATH;
|
||||
registered_functions["cbrt"] = FunctionExpression::CBRT;
|
||||
registered_functions["ceil"] = FunctionExpression::CEIL;
|
||||
registered_functions["cos"] = FunctionExpression::COS;
|
||||
registered_functions["cosh"] = FunctionExpression::COSH;
|
||||
registered_functions["atan2"] = FunctionExpression::ATAN2;
|
||||
registered_functions["exp"] = FunctionExpression::EXP;
|
||||
registered_functions["floor"] = FunctionExpression::FLOOR;
|
||||
registered_functions["hypot"] = FunctionExpression::HYPOT;
|
||||
registered_functions["log"] = FunctionExpression::LOG;
|
||||
registered_functions["log10"] = FunctionExpression::LOG10;
|
||||
registered_functions["mod"] = FunctionExpression::MOD;
|
||||
registered_functions["pow"] = FunctionExpression::POW;
|
||||
registered_functions["round"] = FunctionExpression::ROUND;
|
||||
registered_functions["sin"] = FunctionExpression::SIN;
|
||||
registered_functions["sinh"] = FunctionExpression::SINH;
|
||||
registered_functions["sqrt"] = FunctionExpression::SQRT;
|
||||
registered_functions["tan"] = FunctionExpression::TAN;
|
||||
registered_functions["tanh"] = FunctionExpression::TANH;
|
||||
registered_functions["trunc"] = FunctionExpression::TRUNC;
|
||||
registered_functions["ceil"] = FunctionExpression::CEIL;
|
||||
registered_functions["floor"] = FunctionExpression::FLOOR;
|
||||
registered_functions["hypot"] = FunctionExpression::HYPOT;
|
||||
registered_functions["cath"] = FunctionExpression::CATH;
|
||||
registered_functions["list"] = FunctionExpression::LIST;
|
||||
registered_functions["tuple"] = FunctionExpression::TUPLE;
|
||||
registered_functions["mscale"] = FunctionExpression::MSCALE;
|
||||
|
||||
registered_functions["minvert"] = FunctionExpression::MINVERT;
|
||||
registered_functions["mrotate"] = FunctionExpression::MROTATE;
|
||||
registered_functions["mrotatex"] = FunctionExpression::MROTATEX;
|
||||
registered_functions["mrotatey"] = FunctionExpression::MROTATEY;
|
||||
registered_functions["mrotatez"] = FunctionExpression::MROTATEZ;
|
||||
registered_functions["mscale"] = FunctionExpression::MSCALE;
|
||||
registered_functions["mtranslate"] = FunctionExpression::MTRANSLATE;
|
||||
|
||||
registered_functions["create"] = FunctionExpression::CREATE;
|
||||
registered_functions["list"] = FunctionExpression::LIST;
|
||||
registered_functions["matrix"] = FunctionExpression::MATRIX;
|
||||
registered_functions["placement"] = FunctionExpression::PLACEMENT;
|
||||
registered_functions["rotation"] = FunctionExpression::ROTATION;
|
||||
registered_functions["rotationx"] = FunctionExpression::ROTATIONX;
|
||||
registered_functions["rotationy"] = FunctionExpression::ROTATIONY;
|
||||
registered_functions["rotationz"] = FunctionExpression::ROTATIONZ;
|
||||
registered_functions["str"] = FunctionExpression::STR;
|
||||
registered_functions["translationm"] = FunctionExpression::TRANSLATIONM;
|
||||
registered_functions["tuple"] = FunctionExpression::TUPLE;
|
||||
registered_functions["vector"] = FunctionExpression::VECTOR;
|
||||
|
||||
registered_functions["hiddenref"] = FunctionExpression::HIDDENREF;
|
||||
registered_functions["href"] = FunctionExpression::HREF;
|
||||
|
||||
// Aggregates
|
||||
registered_functions["sum"] = FunctionExpression::SUM;
|
||||
registered_functions["count"] = FunctionExpression::COUNT;
|
||||
registered_functions["average"] = FunctionExpression::AVERAGE;
|
||||
registered_functions["stddev"] = FunctionExpression::STDDEV;
|
||||
registered_functions["min"] = FunctionExpression::MIN;
|
||||
registered_functions["count"] = FunctionExpression::COUNT;
|
||||
registered_functions["max"] = FunctionExpression::MAX;
|
||||
registered_functions["min"] = FunctionExpression::MIN;
|
||||
registered_functions["stddev"] = FunctionExpression::STDDEV;
|
||||
registered_functions["sum"] = FunctionExpression::SUM;
|
||||
|
||||
has_registered_functions = true;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
#define EXPRESSION_PARSER_H
|
||||
|
||||
#include "Expression.h"
|
||||
#include <Base/Matrix.h>
|
||||
#include <Base/Quantity.h>
|
||||
#include <Base/Vector3D.h>
|
||||
|
||||
namespace App {
|
||||
|
||||
@@ -244,48 +246,66 @@ public:
|
||||
NONE,
|
||||
|
||||
// Normal functions taking one or two arguments
|
||||
ABS,
|
||||
ACOS,
|
||||
ASIN,
|
||||
ATAN,
|
||||
ABS,
|
||||
EXP,
|
||||
LOG,
|
||||
LOG10,
|
||||
SIN,
|
||||
SINH,
|
||||
TAN,
|
||||
TANH,
|
||||
SQRT,
|
||||
ATAN2,
|
||||
CATH,
|
||||
CBRT,
|
||||
CEIL,
|
||||
COS,
|
||||
COSH,
|
||||
ATAN2,
|
||||
EXP,
|
||||
FLOOR,
|
||||
HYPOT,
|
||||
LOG,
|
||||
LOG10,
|
||||
MOD,
|
||||
POW,
|
||||
ROUND,
|
||||
SIN,
|
||||
SINH,
|
||||
SQRT,
|
||||
TAN,
|
||||
TANH,
|
||||
TRUNC,
|
||||
CEIL,
|
||||
FLOOR,
|
||||
HYPOT,
|
||||
CATH,
|
||||
LIST,
|
||||
TUPLE,
|
||||
MSCALE, // matrix scale by vector
|
||||
|
||||
// Matrix
|
||||
MINVERT, // invert matrix/placement/rotation
|
||||
CREATE, // create new object of a given type
|
||||
MROTATE, // Rotate matrix/placement/rotation around axis, by rotation object, or by euler angles.
|
||||
MROTATEX, // Rotate matrix/placement/rotation around x-axis.
|
||||
MROTATEY, // Rotate matrix/placement/rotation around y-axis.
|
||||
MROTATEZ, // Rotate matrix/placement/rotation around z-axis.
|
||||
MSCALE, // matrix scale by vector
|
||||
MTRANSLATE, // Translate matrix/placement.
|
||||
|
||||
// Object creation
|
||||
CREATE, // Create new object of a given type.
|
||||
LIST, // Create Python list.
|
||||
MATRIX, // Create matrix object.
|
||||
PLACEMENT, // Create placement object.
|
||||
ROTATION, // Create rotation object.
|
||||
ROTATIONX, // Create x-axis rotation object.
|
||||
ROTATIONY, // Create y-axis rotation object.
|
||||
ROTATIONZ, // Create z-axis rotation object.
|
||||
STR, // stringify
|
||||
TRANSLATIONM, // Create translation matrix object.
|
||||
TUPLE, // Create Python tuple.
|
||||
VECTOR, // Create vector object.
|
||||
|
||||
HIDDENREF, // hidden reference that has no dependency check
|
||||
HREF, // deprecated alias of HIDDENREF
|
||||
|
||||
// Aggregates
|
||||
AGGREGATES,
|
||||
|
||||
SUM,
|
||||
AVERAGE,
|
||||
STDDEV,
|
||||
COUNT,
|
||||
MIN,
|
||||
MAX,
|
||||
MIN,
|
||||
STDDEV,
|
||||
SUM,
|
||||
|
||||
// Last one
|
||||
LAST,
|
||||
@@ -307,6 +327,13 @@ public:
|
||||
|
||||
protected:
|
||||
static Py::Object evalAggregate(const Expression *owner, int type, const std::vector<Expression*> &args);
|
||||
static Base::Vector3d evaluateSecondVectorArgument(const Expression *expression, const std::vector<Expression*> &arguments);
|
||||
static void initialiseObject(const Py::Object *object, const std::vector<Expression*> &arguments, const unsigned long offset = 0);
|
||||
static Py::Object transformFirstArgument(
|
||||
const Expression *expression,
|
||||
const std::vector<Expression*> &arguments,
|
||||
const Base::Matrix4D *transformationMatrix);
|
||||
static Py::Object translationMatrix(double x, double y, double z);
|
||||
Py::Object _getPyValue() const override;
|
||||
Expression * _copy() const override;
|
||||
void _visit(ExpressionVisitor & v) override;
|
||||
|
||||
@@ -271,6 +271,12 @@ bool Quantity::isDimensionless() const
|
||||
return isValid() && myUnit.isEmpty();
|
||||
}
|
||||
|
||||
/// true if it has a specific unit or no dimension.
|
||||
bool Quantity::isDimensionlessOrUnit(const Unit& unit) const
|
||||
{
|
||||
return isDimensionless() || myUnit == unit;
|
||||
}
|
||||
|
||||
// true if it has a number and a valid unit
|
||||
bool Quantity::isQuantity() const
|
||||
{
|
||||
|
||||
@@ -181,6 +181,8 @@ public:
|
||||
|
||||
/// true if it has a number without a unit
|
||||
bool isDimensionless()const;
|
||||
/// true if it has a specific unit or no dimension.
|
||||
bool isDimensionlessOrUnit(const Unit& unit)const;
|
||||
/// true if it has a number and a valid unit
|
||||
bool isQuantity()const;
|
||||
/// true if it has a number with or without a unit
|
||||
|
||||
@@ -894,7 +894,7 @@ class SpreadsheetCases(unittest.TestCase):
|
||||
pla = FreeCAD.Placement(vec,rot)
|
||||
ipla = pla.inverse()
|
||||
|
||||
sheet.set('A1', '=create(<<vector>>, 2, 1, 2)')
|
||||
sheet.set('A1', '=vector(2, 1, 2)')
|
||||
|
||||
# different ways of calling mscale()
|
||||
sheet.set('B1', '=mscale(create(<<matrix>>), A1)')
|
||||
@@ -907,10 +907,10 @@ class SpreadsheetCases(unittest.TestCase):
|
||||
sheet.set('D2', '=A2^0')
|
||||
sheet.set('E2', '=A2^1')
|
||||
sheet.set('F2', '=A2^2')
|
||||
sheet.set('G2', '=create(<<matrix>>, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)')
|
||||
sheet.set('G2', '=matrix(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)')
|
||||
sheet.set('H2', '=G2^-1')
|
||||
|
||||
sheet.set('A3', '=create(<<rotation>>, create(<<vector>>, 0, 1, 0), 45)')
|
||||
sheet.set('A3', '=rotation(vector(0, 1, 0), 45)')
|
||||
|
||||
# test rotation power operation
|
||||
sheet.set('B3', '=A3^-2')
|
||||
@@ -919,7 +919,7 @@ class SpreadsheetCases(unittest.TestCase):
|
||||
sheet.set('E3', '=A3^1')
|
||||
sheet.set('F3', '=A3^2')
|
||||
|
||||
sheet.set('A4', '=create(<<placement>>, A1, A3)')
|
||||
sheet.set('A4', '=placement(A1, A3)')
|
||||
|
||||
# test placement power operation
|
||||
sheet.set('B4', '=A4^-2')
|
||||
@@ -944,6 +944,20 @@ class SpreadsheetCases(unittest.TestCase):
|
||||
sheet.set('E6', '=(E2 * E3)^-1 * E4^-1 * E5')
|
||||
sheet.set('F6', '=(F3*F4*F2)^-1 * F5')
|
||||
|
||||
# Rotate and translate.
|
||||
sheet.set('A7', '=placement(vector(1; 2; 3), vector(1; 0; 0); 0)')
|
||||
sheet.set('B7', '=mrotate(A7; vector(1; 0; 0); 90)')
|
||||
sheet.set('C7', '=mrotatex(A7; 90)')
|
||||
sheet.set('D7', '=mrotatey(A7; 90)')
|
||||
sheet.set('E7', '=mrotatez(A7; 90)')
|
||||
sheet.set('F7', '=mtranslate(A7; vector(1; 2; 3))')
|
||||
sheet.set('G7', '=mtranslate(A7; 1; 2; 3)')
|
||||
|
||||
# Compatibility with old syntax.
|
||||
sheet.set('A8', '=create(<<vector>>, 2, 1, 2)')
|
||||
sheet.set('B8', '=create(<<rotation>>, create(<<vector>>, 0, 1, 0), 45)')
|
||||
sheet.set('C8', '=create(<<placement>>, A8, B8)')
|
||||
|
||||
self.doc.recompute()
|
||||
|
||||
self.assertEqual(sheet.A1,vec)
|
||||
@@ -1014,6 +1028,18 @@ class SpreadsheetCases(unittest.TestCase):
|
||||
self.assertLess(sheet.E6.distanceToPoint(vec),tol)
|
||||
self.assertLess(sheet.F6.distanceToPoint(vec),tol)
|
||||
|
||||
self.assertTrue(sheet.A7.Base.isEqual(FreeCAD.Vector(1, 2, 3), tol))
|
||||
self.assertTrue(sheet.B7.Base.isEqual(FreeCAD.Vector(1, -3, 2), tol))
|
||||
self.assertTrue(sheet.C7.Base.isEqual(FreeCAD.Vector(1, -3, 2), tol))
|
||||
self.assertTrue(sheet.D7.Base.isEqual(FreeCAD.Vector(3, 2.0, -1), tol))
|
||||
self.assertTrue(sheet.E7.Base.isEqual(FreeCAD.Vector(-2, 1, 3.0), tol))
|
||||
self.assertTrue(sheet.F7.Base.isEqual(FreeCAD.Vector(2, 4, 6), tol))
|
||||
self.assertTrue(sheet.G7.Base.isEqual(FreeCAD.Vector(2, 4, 6), tol))
|
||||
|
||||
self.assertEqual(sheet.A8, vec)
|
||||
self.assertEqual(sheet.B8, rot)
|
||||
self.assertEqual(sheet.C8, pla)
|
||||
|
||||
def testIssue3128(self):
|
||||
""" Regression test for issue 3128; mod should work with arbitrary units for both arguments """
|
||||
sheet = self.doc.addObject('Spreadsheet::Sheet','Spreadsheet')
|
||||
|
||||
Reference in New Issue
Block a user