diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index 2862b5f41..204a29819 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -255,6 +255,7 @@ add_resources( icons/graphics-window/pointonx.png icons/graphics-window/point.png icons/graphics-window/rectangle.png + icons/graphics-window/relation.png icons/graphics-window/ref.png icons/graphics-window/revolve.png icons/graphics-window/same-orientation.png diff --git a/res/icons/graphics-window/relation.png b/res/icons/graphics-window/relation.png new file mode 100644 index 000000000..e60900c1c Binary files /dev/null and b/res/icons/graphics-window/relation.png differ diff --git a/src/clipboard.cpp b/src/clipboard.cpp index 0275b1707..88993eff8 100644 --- a/src/clipboard.cpp +++ b/src/clipboard.cpp @@ -244,6 +244,7 @@ void GraphicsWindow::PasteClipboard(Vector trans, double theta, double scale) { c.reference = cc->reference; c.disp = cc->disp; c.comment = cc->comment; + c.expression = cc->expression; bool dontAddConstraint = false; switch(c.type) { case Constraint::Type::COMMENT: diff --git a/src/confscreen.cpp b/src/confscreen.cpp index b4be1f82a..7141b4e5d 100644 --- a/src/confscreen.cpp +++ b/src/confscreen.cpp @@ -484,7 +484,7 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) { case Edit::EXPORT_OFFSET: { Expr *e = Expr::From(s, /*popUpError=*/true); if(e) { - double ev = SS.ExprToMm(e); + double ev = SS.NonConstraintExprToMm(e); if(IsReasonable(ev) || ev < 0) { Error(_("Cutter radius offset must not be negative!")); } else { @@ -498,7 +498,7 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) { if(!e) { break; } - float d = (float)SS.ExprToMm(e); + float d = (float)SS.NonConstraintExprToMm(e); switch(edit.i) { case 0: SS.exportMargin.left = d; break; case 1: SS.exportMargin.right = d; break; @@ -514,12 +514,12 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) { } case Edit::G_CODE_DEPTH: { Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) SS.gCode.depth = (float)SS.ExprToMm(e); + if(e) SS.gCode.depth = (float)SS.NonConstraintExprToMm(e); break; } case Edit::G_CODE_SAFE_HEIGHT: { Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) SS.gCode.safeHeight = (float)SS.ExprToMm(e); + if(e) SS.gCode.safeHeight = (float)SS.NonConstraintExprToMm(e); break; } case Edit::G_CODE_PASSES: { @@ -530,12 +530,12 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) { } case Edit::G_CODE_FEED: { Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) SS.gCode.feed = (float)SS.ExprToMm(e); + if(e) SS.gCode.feed = (float)SS.NonConstraintExprToMm(e); break; } case Edit::G_CODE_PLUNGE_FEED: { Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) SS.gCode.plungeFeed = (float)SS.ExprToMm(e); + if(e) SS.gCode.plungeFeed = (float)SS.NonConstraintExprToMm(e); break; } case Edit::AUTOSAVE_INTERVAL: { diff --git a/src/constraint.cpp b/src/constraint.cpp index 705feec6b..31cbca228 100644 --- a/src/constraint.cpp +++ b/src/constraint.cpp @@ -48,6 +48,7 @@ std::string Constraint::DescriptionString() const { case Type::EQUAL_LINE_ARC_LEN: s = C_("constr-name", "eq-line-len-arc-len"); break; case Type::WHERE_DRAGGED: s = C_("constr-name", "lock-where-dragged"); break; case Type::COMMENT: s = C_("constr-name", "comment"); break; + case Type::RELATION: s = C_("constr-name", "relation"); break; default: s = "???"; break; } @@ -889,6 +890,23 @@ void Constraint::MenuConstrain(Command id) { } break; + case Command::RELATION: + if(gs.points == 1 && gs.n == 1) { + c.type = Type::RELATION; + c.ptA = gs.point[0]; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.expression = _("x=5"); + AddConstraint(&c); + newcons.push_back(c); + } else { + SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND; + SS.GW.pending.command = Command::RELATION; + SS.GW.pending.description = _("click center of relation text"); + SS.ScheduleShowTW(); + } + break; + default: ssassert(false, "Unexpected menu ID"); } for (auto nc:newcons){ diff --git a/src/constrainteq.cpp b/src/constrainteq.cpp index c21d10721..5b4bb78f7 100644 --- a/src/constrainteq.cpp +++ b/src/constrainteq.cpp @@ -25,6 +25,7 @@ bool ConstraintBase::HasLabel() const { case Type::ARC_LINE_DIFFERENCE: case Type::ANGLE: case Type::COMMENT: + case Type::RELATION: return true; default: @@ -60,6 +61,7 @@ bool ConstraintBase::IsProjectible() const { case Type::PERPENDICULAR: case Type::WHERE_DRAGGED: case Type::COMMENT: + case Type::RELATION: return true; case Type::PT_PLANE_DISTANCE: @@ -248,6 +250,18 @@ void ConstraintBase::AddEq(IdList *l, const ExprVector &v, } void ConstraintBase::Generate(IdList *l) { + // apparently necessary to get params used into l, though we just have to "parse" everything, regardless of whether we use it or not. + if(type == Constraint::Type::RELATION) { + size_t eqpos = expression.find_first_of("="); + ssassert(eqpos == expression.find_last_of("="), "There is at most one equals sign in the relation \"expression\""); + ssassert(eqpos != std::string::npos, "There is at least one equals sign in the relation \"expression\""); + Expr::From(expression.substr(0, eqpos), false, l, NULL)->Minus(Expr::From(expression.substr(eqpos+1, SIZE_MAX), false, l, NULL)); + } else if(expression != "" && expr_scaling_to_base != 0) { + Expr::From(expression.c_str(), false, l, NULL)->Times(Expr::From(std::to_string(expr_scaling_to_base).c_str(), false, l, NULL)); + } else if(expression != "") { + Expr::From(expression.c_str(), false, l); + } + switch(type) { case Type::PARALLEL: case Type::CUBIC_LINE_TANGENT: @@ -270,9 +284,22 @@ void ConstraintBase::Generate(IdList *l) { void ConstraintBase::GenerateEquations(IdList *l, bool forReference) const { - if(reference && !forReference) return; + Expr *exA = {}; + if(reference && !forReference) { + return; + } else { + if(type == Constraint::Type::RELATION) { + size_t eqpos = expression.find_first_of("="); + exA = Expr::From(expression.substr(0, eqpos), false, &SK.param, NULL)->Minus(Expr::From(expression.substr(eqpos+1, SIZE_MAX), false, &SK.param, NULL)); + } else if(expression != "" && expr_scaling_to_base != 0) { + exA = Expr::From(expression.c_str(), false, &SK.param, NULL)->Times(Expr::From(std::to_string(expr_scaling_to_base).c_str(), false, &SK.param, NULL)); + } else if(expression != "") { + exA = Expr::From(expression.c_str(), false, &SK.param, NULL); + } else { + exA = Expr::From(valA); + } + } - Expr *exA = Expr::From(valA); switch(type) { case Type::PT_PT_DISTANCE: AddEq(l, Distance(workplane, ptA, ptB)->Minus(exA), 0); @@ -1053,6 +1080,11 @@ void ConstraintBase::GenerateEquations(IdList *l, case Type::COMMENT: return; + + case Type::RELATION: + AddEq(l, exA, 0); + return; + } ssassert(false, "Unexpected constraint ID"); } diff --git a/src/drawconstraint.cpp b/src/drawconstraint.cpp index e84c0ce48..d1a85c74e 100644 --- a/src/drawconstraint.cpp +++ b/src/drawconstraint.cpp @@ -26,6 +26,12 @@ std::string Constraint::Label() const { // valA has units of distance result = SS.MmToStringSI(fabs(valA)); } + if(expression != "") { + result = expression; + if((SS.MmPerUnit() != expr_scaling_to_base) && (expr_scaling_to_base != 0)) { + result = "("+result+")" + "*" + std::to_string(expr_scaling_to_base/SS.MmPerUnit()); + } + } if(reference) { result += " REF"; } @@ -60,7 +66,7 @@ void Constraint::DoLabel(Canvas *canvas, Canvas::hStroke hcs, // By default, the reference is from the center; but the style could // specify otherwise if one is present, and it could also specify a // rotation. - if(type == Type::COMMENT && disp.style.v) { + if(((type == Type::COMMENT) || (type == Type::RELATION)) && disp.style.v) { Style *st = Style::Get(disp.style); // rotation first double rads = st->textAngle*PI/180; @@ -1287,6 +1293,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas, } return; + case Type::RELATION: case Type::COMMENT: { Vector u, v; if(workplane == Entity::FREE_IN_3D) { @@ -1343,7 +1350,7 @@ void Constraint::GetReferencePoints(const Camera &camera, std::vector *r } bool Constraint::IsStylable() const { - if(type == Type::COMMENT) return true; + if((type == Type::COMMENT) || (type == Type::RELATION)) return true; return false; } @@ -1355,6 +1362,7 @@ hStyle Constraint::GetStyle() const { bool Constraint::HasLabel() const { switch(type) { case Type::COMMENT: + case Type::RELATION: case Type::PT_PT_DISTANCE: case Type::PT_PLANE_DISTANCE: case Type::PT_LINE_DISTANCE: diff --git a/src/drawentity.cpp b/src/drawentity.cpp index 7ac03386d..56abc34e7 100644 --- a/src/drawentity.cpp +++ b/src/drawentity.cpp @@ -661,7 +661,7 @@ void Entity::Draw(DrawAs how, Canvas *canvas) { double w = 60 - camera.width / 2.0; // Shift the axis to the right if they would overlap with the toolbar. if(SS.showToolbar) { - if(h + 30 > -(32*18 + 3*16 + 8) / 2) + if(h + 30 > -(32*19 + 3*16 + 8) / 2) w += 60; } tail = camera.projRight.ScaledBy(w/s).Plus( diff --git a/src/exportvector.cpp b/src/exportvector.cpp index 55abac685..268937a75 100644 --- a/src/exportvector.cpp +++ b/src/exportvector.cpp @@ -290,6 +290,7 @@ class DxfWriteInterface : public DRW_Interface { break; } + case Constraint::Type::RELATION: case Constraint::Type::COMMENT: { Style *st = SK.style.FindById(c.GetStyle()); writeText(xfrm(c.disp.offset), c.Label(), @@ -605,6 +606,7 @@ bool DxfFileWriter::NeedToOutput(Constraint *c) { case Constraint::Type::DIAMETER: case Constraint::Type::ANGLE: case Constraint::Type::COMMENT: + case Constraint::Type::RELATION: return c->IsVisible(); default: // See writeEntities(). diff --git a/src/expr.cpp b/src/expr.cpp index d5ed87b58..382321517 100644 --- a/src/expr.cpp +++ b/src/expr.cpp @@ -621,6 +621,9 @@ class ExprParser { std::string::const_iterator it, end; std::vector stack; + std::set newParams; + IdList *params; + hConstraint hc; char ReadChar(); char PeekChar(); @@ -637,7 +640,8 @@ class ExprParser { bool Reduce(std::string *error); bool Parse(std::string *error, size_t reduceUntil = 0); - static Expr *Parse(const std::string &input, std::string *error); + static Expr *Parse(const std::string &input, std::string *error, IdList *params = NULL, int *paramsCount = 0, hConstraint hc = {0}); }; ExprParser::Token ExprParser::Token::From(TokenType type, Expr *expr) { @@ -671,7 +675,7 @@ std::string ExprParser::ReadWord() { std::string s; while(char c = PeekChar()) { - if(!isalnum(c)) break; + if(!isalnum(c) && c != '_') break; s.push_back(ReadChar()); } @@ -735,6 +739,30 @@ ExprParser::Token ExprParser::Lex(std::string *error) { } else if(s == "pi") { t = Token::From(TokenType::OPERAND, Expr::Op::CONSTANT); t.expr->v = PI; + } else if(params != NULL) { + bool found = false; + for(const Param &p : *params) { + if(p.name != s) continue; + t = Token::From(TokenType::OPERAND, Expr::Op::PARAM); + t.expr->parh = p.h; + newParams.insert(p.h.v); + found = true; + } + if(!found) { + Param p = {}; + int count = 0; + + while(params->FindByIdNoOops(hc.param(count)) != NULL) { + count++; + } + + p.h = hc.param(count); + p.name = s; + params->Add(&p); + newParams.insert(p.h.v); + t = Token::From(TokenType::OPERAND, Expr::Op::PARAM); + t.expr->parh = p.h; + } } else { *error = "'" + s + "' is not a valid variable, function or constant"; } @@ -768,7 +796,12 @@ ExprParser::Token ExprParser::Lex(std::string *error) { ExprParser::Token ExprParser::PopOperand(std::string *error) { Token t = Token::From(); - if(stack.empty() || stack.back().type != TokenType::OPERAND) { + if(stack.empty() + || ( + stack.back().type != TokenType::OPERAND + && (stack.back().expr == nullptr) + ) + ) { *error = "Expected an operand"; } else { t = stack.back(); @@ -810,17 +843,31 @@ int ExprParser::Precedence(Token t) { bool ExprParser::Reduce(std::string *error) { Token a = PopOperand(error); + if((error != NULL) && (!error->empty())) return false; if(a.IsError()) return false; Token op = PopOperator(error); + + // support for multiplicaiton shorthand (i.e. 2x instead of 2*x) + if(op.IsError()) { + op = Token::From(TokenType::BINARY_OP, Expr::Op::TIMES); + error = NULL; + } + //redundant in current setup, since if there's an error op is likely to be null. keeping to preseve error handing in future changes + else if((error != NULL) && (!error->empty())) return false; + if(op.IsError()) return false; - Token r = Token::From(TokenType::OPERAND); switch(op.type) { case TokenType::BINARY_OP: { Token b = PopOperand(error); + if((error != NULL) && (!error->empty())) return false; if(b.IsError()) return false; - r.expr = b.expr->AnyOp(op.expr->op, a.expr); + + // gives the operand children: + // semantically this subtree represents an operand, so we change the token type accordingly + op.expr = b.expr->AnyOp(op.expr->op, a.expr); + stack.push_back(op); break; } @@ -836,13 +883,13 @@ bool ExprParser::Reduce(std::string *error) { case Expr::Op::ACOS: e = e->ACos()->Times(Expr::From(180/PI)); break; default: ssassert(false, "Unexpected unary operator"); } - r.expr = e; + op.expr = e; + stack.push_back(op); break; } default: ssassert(false, "Unexpected operator"); } - stack.push_back(r); return true; } @@ -906,14 +953,19 @@ bool ExprParser::Parse(std::string *error, size_t reduceUntil) { return true; } -Expr *ExprParser::Parse(const std::string &input, std::string *error) { +Expr *ExprParser::Parse(const std::string &input, std::string *error, + IdList *params, int *paramsCount, hConstraint hc) { ExprParser parser; parser.it = input.cbegin(); parser.end = input.cend(); + parser.params = params; + parser.newParams.clear(); + parser.hc = hc; if(!parser.Parse(error)) return NULL; Token r = parser.PopOperand(error); if(r.IsError()) return NULL; + if(paramsCount != NULL) *paramsCount = parser.newParams.size(); return r.expr; } @@ -921,9 +973,10 @@ Expr *Expr::Parse(const std::string &input, std::string *error) { return ExprParser::Parse(input, error); } -Expr *Expr::From(const std::string &input, bool popUpError) { +Expr *Expr::From(const std::string &input, bool popUpError, + IdList *params, int *paramsCount, hConstraint hc) { std::string error; - Expr *e = ExprParser::Parse(input, &error); + Expr *e = ExprParser::Parse(input, &error, params, paramsCount, hc); if(!e) { dbp("Parse/lex error: %s", error.c_str()); if(popUpError) { diff --git a/src/expr.h b/src/expr.h index 7eef4e005..609850638 100644 --- a/src/expr.h +++ b/src/expr.h @@ -37,12 +37,12 @@ class Expr { }; Op op; - Expr *a; + Expr *a; //nullptr if this is a PARAM, PARAM_PTR, CONSTANT, VARIABLE union { - double v; + double v; // this variant if CONSTANT hParam parh; - Param *parp; - Expr *b; + Param *parp; + Expr *b; // nullptr if this is a unary op, }; Expr() = default; @@ -98,7 +98,8 @@ class Expr { IdList *thenTry) const; static Expr *Parse(const std::string &input, std::string *error); - static Expr *From(const std::string &input, bool popUpError); + static Expr *From(const std::string &input, bool popUpError, + IdList *params = NULL, int *paramsCount = NULL, hConstraint hc = {0}); }; class ExprVector { diff --git a/src/file.cpp b/src/file.cpp index c68ba1379..ecc568c9a 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -122,6 +122,7 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { { 'p', "Param.h.v.", 'x', &(SS.sv.p.h.v) }, { 'p', "Param.val", 'f', &(SS.sv.p.val) }, + { 'p', "Param.name", 'S', &(SS.sv.p.name) }, { 'r', "Request.h.v", 'x', &(SS.sv.r.h.v) }, { 'r', "Request.type", 'd', &(SS.sv.r.type) }, @@ -185,6 +186,8 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { { 'c', "Constraint.other2", 'b', &(SS.sv.c.other2) }, { 'c', "Constraint.reference", 'b', &(SS.sv.c.reference) }, { 'c', "Constraint.comment", 'S', &(SS.sv.c.comment) }, + { 'c', "Constraint.expression", 'S', &(SS.sv.c.expression) }, + { 'c', "Constraint.expr_scaling_to_base", 'f', &(SS.sv.c.expr_scaling_to_base) }, { 'c', "Constraint.disp.offset.x", 'f', &(SS.sv.c.disp.offset.x) }, { 'c', "Constraint.disp.offset.y", 'f', &(SS.sv.c.disp.offset.y) }, { 'c', "Constraint.disp.offset.z", 'f', &(SS.sv.c.disp.offset.z) }, diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 38095c8d0..4deb269bc 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -165,6 +165,7 @@ const MenuEntry Menu[] = { { 1, N_("Lock Point Where &Dragged"), Command::WHERE_DRAGGED, ']', KN, mCon }, { 1, NULL, Command::NONE, 0, KN, NULL }, { 1, N_("Comment"), Command::COMMENT, ';', KN, mCon }, +{ 1, N_("Relation"), Command::RELATION, '/', KN, mCon }, { 0, N_("&Analyze"), Command::NONE, 0, KN, mAna }, { 1, N_("Measure &Volume"), Command::VOLUME, C|S|'v', KN, mAna }, @@ -431,7 +432,7 @@ void GraphicsWindow::Init() { using namespace std::placeholders; // Do this first, so that if it causes an onRender event we don't try to paint without // a canvas. - window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4); + window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 19 + 3 * 16 + 8 + 4); window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT); window->onContextLost = [&] { canvas = NULL; diff --git a/src/mouse.cpp b/src/mouse.cpp index 0826713d0..c04106a7d 100644 --- a/src/mouse.cpp +++ b/src/mouse.cpp @@ -4,6 +4,7 @@ // Copyright 2008-2013 Jonathan Westhues. //----------------------------------------------------------------------------- #include "solvespace.h" +#include "ui.h" void GraphicsWindow::UpdateDraggedPoint(hEntity hp, double mx, double my) { Entity *p = SK.GetEntity(hp); @@ -1161,6 +1162,18 @@ void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ct hc = Constraint::AddConstraint(&c); break; } + + case Command::RELATION: { + ClearSuper(); + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = Constraint::Type::RELATION; + c.disp.offset = v; + c.expression = _("x=5"); + hc = Constraint::AddConstraint(&c); + break; + } default: ssassert(false, "Unexpected pending menu id"); } break; @@ -1352,6 +1365,7 @@ void GraphicsWindow::EditConstraint(hConstraint constraint) { Constraint *c = SK.GetConstraint(constraintBeingEdited); if(!c->HasLabel()) { // Not meaningful to edit a constraint without a dimension + dbp("Wont edit without label"); return; } if(c->reference) { @@ -1373,26 +1387,48 @@ void GraphicsWindow::EditConstraint(hConstraint constraint) { default: { double value = fabs(c->valA); - // If displayed as radius, also edit as radius. - if(c->type == Constraint::Type::DIAMETER && c->other) - value /= 2; + // True if the quantity represented by this constraint is dimensionless (ratios, angles etc.) + bool dimless = c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO || c->type == Constraint::Type::ANGLE || c->type == Constraint::Type::RELATION; - // Try showing value with default number of digits after decimal first. - if(c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO) { - editValue = ssprintf("%.3f", value); - } else if(c->type == Constraint::Type::ANGLE) { - editValue = SS.DegreeToString(value); + // Render a value, or render an expression + if(c->expression.empty()) { + // Try showing value with default number of digits after decimal first. + if(dimless) { + // these ratios are dimensionless, so should not be scaled (not a length value) + if(c->type == Constraint::Type::ANGLE) { + editValue = SS.DegreeToString(value); + } else { + editValue = ssprintf("%.3f", value); + } + } else { + // If displayed as radius, also edit as radius. + if(c->type == Constraint::Type::DIAMETER && c->other) + value /= 2; + + editValue = SS.MmToString(value, true); + value /= SS.MmPerUnit(); + } + + // If that's not enough to represent it exactly, show the value with as many + // digits after decimal as required, up to 10. + int digits = 0; + while(fabs(std::stod(editValue) - value) > 1e-10) { + editValue = ssprintf("%.*f", digits, value); + digits++; + } } else { - editValue = SS.MmToString(value, true); - value /= SS.MmPerUnit(); - } - // If that's not enough to represent it exactly, show the value with as many - // digits after decimal as required, up to 10. - int digits = 0; - while(fabs(std::stod(editValue) - value) > 1e-10) { - editValue = ssprintf("%.*f", digits, value); - digits++; + if(c->type == Constraint::Type::DIAMETER && c->other && c->expr_scaling_to_base != 0) { + // Edit as radius instead of diameter due to user config + editValue = ssprintf("(%s)*%f", c->expression.c_str(), c->expr_scaling_to_base/2*SS.MmPerUnit()); + } else if(!dimless && c->expr_scaling_to_base != SS.MmPerUnit() && c->expr_scaling_to_base != 0) { + // Unit needs dimension scaling + editValue = ssprintf("(%s)*%f", c->expression.c_str(), c->expr_scaling_to_base/SS.MmPerUnit()); + } else { + // Unit does not need scaling + editValue = c->expression; + } } + editPlaceholder = "10.000000"; break; } @@ -1430,11 +1466,51 @@ void GraphicsWindow::EditControlDone(const std::string &s) { c->comment = s; return; } + + // decided not to parse equals signs in expressions for now since + // 1) A relation isn't an expression since it has no meaningful reducable value + // 2) There can only be one occurrence of an assignment in an expression, and this may only be in a relation + // To enforce #2 after future changes and to to not break implicit invariant #1, we process the assignment operation as a special case + // Since "equations" are just expressions = 0, we just "subtract everything after the equals sign from both sides of the equation" + Expr *e = NULL; + int usedParams; + if(c->type == Constraint::Type::RELATION) { + size_t eqpos = s.find_first_of("="); + if(eqpos == std::string::npos || eqpos != s.find_last_of("=")) { + Error("Relation constraints must have exactly one '=': '%s'", s.c_str()); + return; + } + else if(eqpos == 0) { + Error("Relation constraints must have a left-hand-side (\?\?%s)", s.c_str()); + return; + } else if(eqpos == s.length() - 1) { + Error("Relation constraints must have a right-hand-side (%s\?\?)", s.c_str()); + return; + } + + // (left_side) - (right_side) (... implicitly = 0) + e = Expr::From(s.substr(0, eqpos), true, &SK.param, &usedParams)->Minus(Expr::From(s.substr(eqpos+1, SIZE_MAX), true, &SK.param, &usedParams)); + + } else { + e = Expr::From(s, true, &SK.param, &usedParams); + } + - if(Expr *e = Expr::From(s, true)) { + if(e) { SS.UndoRemember(); + if(usedParams > 0) { + // after a user finishes editing an expression in a constraint that has a different scaling_to_base, we can assume that they intend for the expression to be in the currently selected units + c->expr_scaling_to_base = SS.MmPerUnit(); + c->expression = s; + } switch(c->type) { + case Constraint::Type::RELATION: + // on relation, expr_scaling_to_base should be ignored, since the scaling is done on constraints that directly constrain some entity + c->expr_scaling_to_base = 1; + c->expression = s; + break; + case Constraint::Type::PROJ_PT_DISTANCE: case Constraint::Type::PT_LINE_DISTANCE: case Constraint::Type::PT_FACE_DISTANCE: @@ -1442,14 +1518,15 @@ void GraphicsWindow::EditControlDone(const std::string &s) { case Constraint::Type::LENGTH_DIFFERENCE: case Constraint::Type::ARC_ARC_DIFFERENCE: case Constraint::Type::ARC_LINE_DIFFERENCE: { + // The sign is not displayed to the user, but this is a signed // distance internally. To flip the sign, the user enters a // negative distance. bool wasNeg = (c->valA < 0); if(wasNeg) { - c->valA = -SS.ExprToMm(e); + c->valA = -(e->Eval()) * c->expr_scaling_to_base; } else { - c->valA = SS.ExprToMm(e); + c->valA = (e->Eval()) * c->expr_scaling_to_base; } break; } @@ -1463,7 +1540,7 @@ void GraphicsWindow::EditControlDone(const std::string &s) { break; case Constraint::Type::DIAMETER: - c->valA = fabs(SS.ExprToMm(e)); + c->valA = fabs((e->Eval()) * c->expr_scaling_to_base); // If displayed and edited as radius, convert back // to diameter @@ -1473,9 +1550,11 @@ void GraphicsWindow::EditControlDone(const std::string &s) { default: // These are always positive, and they get the units conversion. - c->valA = fabs(SS.ExprToMm(e)); + // Use ExprToMm since this unparameterized expression will simplify immediately, and the dimension transformation is irrelevant + c->valA = fabs(SS.NonConstraintExprToMm(e)); break; } + SK.GetGroup(c->group)->dofCheckOk = false; // if an named param was used in the constraint, the DoF may have changed SS.MarkGroupDirty(c->group); } } diff --git a/src/sketch.h b/src/sketch.h index 020f40815..c90bb0a22 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -621,6 +621,7 @@ class Param { double val; bool known; bool free; + std::string name; // Used only in the solver Param *substd; @@ -688,7 +689,9 @@ class ConstraintBase { ARC_LINE_LEN_RATIO = 211, ARC_ARC_DIFFERENCE = 212, ARC_LINE_DIFFERENCE = 213, - COMMENT = 1000 + COMMENT = 1000, + //like COMMENT, but its contents are an equation (in particular, a relation). + RELATION = 1001 }; Type type; @@ -708,8 +711,10 @@ class ConstraintBase { bool other; bool other2; - bool reference; // a ref dimension, that generates no eqs - std::string comment; // since comments are represented as constraints + bool reference; // a ref dimension, that generates no eqs + std::string comment; // since comments are represented as constraints + std::string expression; // user-defined, may not be in mm (if entered in inch mode for example) + double expr_scaling_to_base; // scales expression to put in solvespace base units (like mm for distance). bool Equals(const ConstraintBase &c) const { return type == c.type && group == c.group && workplane == c.workplane && diff --git a/src/solvespace.cpp b/src/solvespace.cpp index aa135eaff..849f7cd5d 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -481,10 +481,13 @@ std::string SolveSpaceUI::DegreeToString(double v) { return ssprintf("%.0f", v); } } -double SolveSpaceUI::ExprToMm(Expr *e) { + +// Invariant: This function is wrong for constraints, since constraints with expressions may have their own scaling to base (for example since they were specified in a different system) +double SolveSpaceUI::NonConstraintExprToMm(Expr *e) { return (e->Eval()) * MmPerUnit(); } double SolveSpaceUI::StringToMm(const std::string &str) { + // we scale constants like this because we directly store values in Mm return std::stod(str) * MmPerUnit(); } double SolveSpaceUI::ChordTolMm() { diff --git a/src/solvespace.h b/src/solvespace.h index 6c3daa737..b306b10ea 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -623,7 +623,7 @@ class SolveSpaceUI { std::string MmToString(double v, bool editable=false); std::string MmToStringSI(double v, int dim = 0); std::string DegreeToString(double v); - double ExprToMm(Expr *e); + double NonConstraintExprToMm(Expr *e); double StringToMm(const std::string &s); const char *UnitName(); double MmPerUnit(); diff --git a/src/system.cpp b/src/system.cpp index c513796f6..3e812d174 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -425,7 +425,7 @@ SolveResult System::Solve(Group *g, int *rank, int *dof, List *bad, int x; dbp("%d equations", eq.n); for(x = 0; x < eq.n; x++) { - dbp(" %.3f = %s = 0", eq[x].e->Eval(), eq[x].e->Print()); + dbp(" %.3f = %s = 0", eq[x].e->Eval(), eq[x].e->Print().c_str()); } dbp("%d parameters", param.n); for(x = 0; x < param.n; x++) { diff --git a/src/textscreens.cpp b/src/textscreens.cpp index c73361f92..66e7675ad 100644 --- a/src/textscreens.cpp +++ b/src/textscreens.cpp @@ -889,7 +889,7 @@ void TextWindow::EditControlDone(std::string s) { case Edit::STEP_DIM_FINISH: if(Expr *e = Expr::From(s, /*popUpError=*/true)) { if(stepDim.isDistance) { - stepDim.finish = SS.ExprToMm(e); + stepDim.finish = SS.NonConstraintExprToMm(e); } else { stepDim.finish = e->Eval(); } @@ -906,7 +906,7 @@ void TextWindow::EditControlDone(std::string s) { Error(_("Radius cannot be zero or negative.")); break; } - SS.tangentArcRadius = SS.ExprToMm(e); + SS.tangentArcRadius = SS.NonConstraintExprToMm(e); } break; diff --git a/src/toolbar.cpp b/src/toolbar.cpp index 74764883a..0e5187bba 100644 --- a/src/toolbar.cpp +++ b/src/toolbar.cpp @@ -62,6 +62,8 @@ static ToolIcon Toolbar[] = { N_("Other supplementary angle"), {} }, { "ref", Command::REFERENCE, N_("Toggle reference dimension"), {} }, + { "relation", Command::RELATION, + N_("Constrain variables with equation"), {} }, { "", Command::NONE, "", {} }, { "extrude", Command::GROUP_EXTRUDE, @@ -158,7 +160,7 @@ bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my, UiCanvas *canvas, // When changing these values, also change the asReference drawing code in drawentity.cpp // as well as the "window->SetMinContentSize(720, 636);" in graphicswin.cpp int fudge = 8; - int h = 32*18 + 3*16 + fudge; // Toolbar height = 18 icons * 32 pixels + 3 dividers * 16 pixels + fudge + int h = 32*19 + 3*16 + fudge; // Toolbar height = 19 icons * 32 pixels + 3 dividers * 16 pixels + fudge if(h < y) { // If there is enough vertical space leave up to 32 pixels between the menu bar and the toolbar. diff --git a/src/ui.h b/src/ui.h index 49c1be64f..a3dd6164f 100644 --- a/src/ui.h +++ b/src/ui.h @@ -158,6 +158,7 @@ enum class Command : uint32_t { ORIENTED_SAME, WHERE_DRAGGED, COMMENT, + RELATION, // Analyze VOLUME, AREA,