コード例 #1
0
ファイル: solvespace.cpp プロジェクト: Evil-Spirit/solvespace
void SolveSpaceUI::MenuAnalyze(int id) {
    SS.GW.GroupSelection();
#define gs (SS.GW.gs)

    switch(id) {
        case GraphicsWindow::MNU_STEP_DIM:
            if(gs.constraints == 1 && gs.n == 0) {
                Constraint *c = SK.GetConstraint(gs.constraint[0]);
                if(c->HasLabel() && !c->reference) {
                    SS.TW.shown.dimFinish = c->valA;
                    SS.TW.shown.dimSteps = 10;
                    SS.TW.shown.dimIsDistance =
                        (c->type != Constraint::ANGLE) &&
                        (c->type != Constraint::LENGTH_RATIO) &&
                        (c->type != Constraint::LENGTH_DIFFERENCE);
                    SS.TW.shown.constraint = c->h;
                    SS.TW.shown.screen = TextWindow::SCREEN_STEP_DIMENSION;

                    // The step params are specified in the text window,
                    // so force that to be shown.
                    SS.GW.ForceTextWindowShown();

                    SS.ScheduleShowTW();
                    SS.GW.ClearSelection();
                } else {
                    Error("Constraint must have a label, and must not be "
                          "a reference dimension.");
                }
            } else {
                Error("Bad selection for step dimension; select a constraint.");
            }
            break;

        case GraphicsWindow::MNU_NAKED_EDGES: {
            SS.nakedEdges.Clear();

            Group *g = SK.GetGroup(SS.GW.activeGroup);
            SMesh *m = &(g->displayMesh);
            SKdNode *root = SKdNode::From(m);
            bool inters, leaks;
            root->MakeCertainEdgesInto(&(SS.nakedEdges),
                SKdNode::NAKED_OR_SELF_INTER_EDGES, true, &inters, &leaks);

            InvalidateGraphics();

            const char *intersMsg = inters ?
                "The mesh is self-intersecting (NOT okay, invalid)." :
                "The mesh is not self-intersecting (okay, valid).";
            const char *leaksMsg = leaks ?
                "The mesh has naked edges (NOT okay, invalid)." :
                "The mesh is watertight (okay, valid).";

            std::string cntMsg = ssprintf("\n\nThe model contains %d triangles, from "
                            "%d surfaces.", g->displayMesh.l.n, g->runningShell.surface.n);

            if(SS.nakedEdges.l.n == 0) {
                Message("%s\n\n%s\n\nZero problematic edges, good.%s",
                    intersMsg, leaksMsg, cntMsg.c_str());
            } else {
                Error("%s\n\n%s\n\n%d problematic edges, bad.%s",
                    intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str());
            }
            break;
        }

        case GraphicsWindow::MNU_INTERFERENCE: {
            SS.nakedEdges.Clear();

            SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
            SKdNode *root = SKdNode::From(m);
            bool inters, leaks;
            root->MakeCertainEdgesInto(&(SS.nakedEdges),
                SKdNode::SELF_INTER_EDGES, false, &inters, &leaks);

            InvalidateGraphics();

            if(inters) {
                Error("%d edges interfere with other triangles, bad.",
                    SS.nakedEdges.l.n);
            } else {
                Message("The assembly does not interfere, good.");
            }
            break;
        }

        case GraphicsWindow::MNU_VOLUME: {
            SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);

            double vol = 0;
            int i;
            for(i = 0; i < m->l.n; i++) {
                STriangle tr = m->l.elem[i];

                // Translate to place vertex A at (x, y, 0)
                Vector trans = Vector::From(tr.a.x, tr.a.y, 0);
                tr.a = (tr.a).Minus(trans);
                tr.b = (tr.b).Minus(trans);
                tr.c = (tr.c).Minus(trans);

                // Rotate to place vertex B on the y-axis. Depending on
                // whether the triangle is CW or CCW, C is either to the
                // right or to the left of the y-axis. This handles the
                // sign of our normal.
                Vector u = Vector::From(-tr.b.y, tr.b.x, 0);
                u = u.WithMagnitude(1);
                Vector v = Vector::From(tr.b.x, tr.b.y, 0);
                v = v.WithMagnitude(1);
                Vector n = Vector::From(0, 0, 1);

                tr.a = (tr.a).DotInToCsys(u, v, n);
                tr.b = (tr.b).DotInToCsys(u, v, n);
                tr.c = (tr.c).DotInToCsys(u, v, n);

                n = tr.Normal().WithMagnitude(1);

                // Triangles on edge don't contribute
                if(fabs(n.z) < LENGTH_EPS) continue;

                // The plane has equation p dot n = a dot n
                double d = (tr.a).Dot(n);
                // nx*x + ny*y + nz*z = d
                // nz*z = d - nx*x - ny*y
                double A = -n.x/n.z, B = -n.y/n.z, C = d/n.z;

                double mac = tr.c.y/tr.c.x, mbc = (tr.c.y - tr.b.y)/tr.c.x;
                double xc = tr.c.x, yb = tr.b.y;

                // I asked Maple for
                //    int(int(A*x + B*y +C, y=mac*x..(mbc*x + yb)), x=0..xc);
                double integral =
                    (1.0/3)*(
                        A*(mbc-mac)+
                        (1.0/2)*B*(mbc*mbc-mac*mac)
                    )*(xc*xc*xc)+
                    (1.0/2)*(A*yb+B*yb*mbc+C*(mbc-mac))*xc*xc+
                    C*yb*xc+
                    (1.0/2)*B*yb*yb*xc;

                vol += integral;
            }

            std::string msg = ssprintf("The volume of the solid model is:\n\n""    %.3f %s^3",
                vol / pow(SS.MmPerUnit(), 3),
                SS.UnitName());

            if(SS.viewUnits == SolveSpaceUI::UNIT_MM) {
                msg += ssprintf("\n    %.2f mL", vol/(10*10*10));
            }
            msg += "\n\nCurved surfaces have been approximated as triangles.\n"
                   "This introduces error, typically of around 1%.";
            Message("%s", msg.c_str());
            break;
        }

        case GraphicsWindow::MNU_AREA: {
            Group *g = SK.GetGroup(SS.GW.activeGroup);
            if(g->polyError.how != Group::POLY_GOOD) {
                Error("This group does not contain a correctly-formed "
                      "2d closed area. It is open, not coplanar, or self-"
                      "intersecting.");
                break;
            }
            SEdgeList sel = {};
            g->polyLoops.MakeEdgesInto(&sel);
            SPolygon sp = {};
            sel.AssemblePolygon(&sp, NULL, true);
            sp.normal = sp.ComputeNormal();
            sp.FixContourDirections();
            double area = sp.SignedArea();
            double scale = SS.MmPerUnit();
            Message("The area of the region sketched in this group is:\n\n"
                    "    %.3f %s^2\n\n"
                    "Curves have been approximated as piecewise linear.\n"
                    "This introduces error, typically of around 1%%.",
                area / (scale*scale),
                SS.UnitName());
            sel.Clear();
            sp.Clear();
            break;
        }

        case GraphicsWindow::MNU_SHOW_DOF:
            // This works like a normal solve, except that it calculates
            // which variables are free/bound at the same time.
            SS.GenerateAll(SolveSpaceUI::GENERATE_ALL, true);
            break;

        case GraphicsWindow::MNU_TRACE_PT:
            if(gs.points == 1 && gs.n == 1) {
                SS.traced.point = gs.point[0];
                SS.GW.ClearSelection();
            } else {
                Error("Bad selection for trace; select a single point.");
            }
            break;

        case GraphicsWindow::MNU_STOP_TRACING: {
            std::string exportFile;
            if(GetSaveFile(&exportFile, "", CsvFileFilter)) {
                FILE *f = ssfopen(exportFile, "wb");
                if(f) {
                    int i;
                    SContour *sc = &(SS.traced.path);
                    for(i = 0; i < sc->l.n; i++) {
                        Vector p = sc->l.elem[i].p;
                        double s = SS.exportScale;
                        fprintf(f, "%.10f, %.10f, %.10f\r\n",
                            p.x/s, p.y/s, p.z/s);
                    }
                    fclose(f);
                } else {
                    Error("Couldn't write to '%s'", exportFile.c_str());
                }
            }
            // Clear the trace, and stop tracing
            SS.traced.point = Entity::NO_ENTITY;
            SS.traced.path.l.Clear();
            InvalidateGraphics();
            break;
        }

        default: oops();
    }
}
コード例 #2
0
ファイル: mouse.cpp プロジェクト: astarasikov/solvespace
void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) {
    if(GraphicsEditControlIsVisible()) return;
    SS.TW.HideEditControl();

    if(hover.constraint.v) {
        constraintBeingEdited = hover.constraint;
        ClearSuper();

        Constraint *c = SK.GetConstraint(constraintBeingEdited);
        if(!c->HasLabel()) {
            // Not meaningful to edit a constraint without a dimension
            return;
        }
        if(c->reference) {
            // Not meaningful to edit a reference dimension
            return;
        }

        Vector p3 = c->GetLabelPos();
        Point2d p2 = ProjectPoint(p3);

        char s[1024];
        switch(c->type) {
            case Constraint::COMMENT:
                strcpy(s, c->comment.str);
                break;

            case Constraint::ANGLE:
            case Constraint::LENGTH_RATIO:
                sprintf(s, "%.3f", c->valA);
                break;

            default: {
                double v = fabs(c->valA);

                // If displayed as radius, also edit as radius.
                if(c->type == Constraint::DIAMETER && c->disp.toggleA)
                    v /= 2;

                char *def = SS.MmToString(v);
                double eps = 1e-12;
                if(fabs(SS.StringToMm(def) - v) < eps) {
                    // Show value with default number of digits after decimal,
                    // which is at least enough to represent it exactly.
                    strcpy(s, def);
                } else {
                    // Show value with as many digits after decimal as
                    // required to represent it exactly, up to 10.
                    v /= SS.MmPerUnit();
                    int i;
                    for(i = 0; i <= 10; i++) {
                        sprintf(s, "%.*f", i, v);
                        if(fabs(atof(s) - v) < eps) break;
                    }
                }
                break;
            }
        }
        ShowGraphicsEditControl((int)p2.x, (int)p2.y-4, s);
    }
}
コード例 #3
0
ファイル: mouse.cpp プロジェクト: astarasikov/solvespace
void GraphicsWindow::MouseRightUp(double x, double y) {
    SS.extraLine.draw = false;
    InvalidateGraphics();

    // Don't show a context menu if the user is right-clicking the toolbar,
    // or if they are finishing a pan.
    if(ToolbarMouseMoved((int)x, (int)y)) return;
    if(orig.startedMoving) return;

    if(context.active) return;

    if(pending.operation == DRAGGING_NEW_LINE_POINT ||
       pending.operation == DRAGGING_NEW_CUBIC_POINT)
    {
        // Special case; use a right click to stop drawing lines, since
        // a left click would draw another one. This is quicker and more
        // intuitive than hitting escape. Likewise for new cubic segments.
        ClearPending();
        return;
    }

    context.active = true;

    if(!hover.IsEmpty()) {
        MakeSelected(&hover);
        SS.ScheduleShowTW();
    }
    GroupSelection();

    bool itemsSelected = (gs.n > 0 || gs.constraints > 0);

    if(itemsSelected) {
        if(gs.stylables > 0) {
            ContextMenuListStyles();
            AddContextMenuItem("Assign to Style", CONTEXT_SUBMENU);
        }
        if(gs.n + gs.constraints == 1) {
            AddContextMenuItem("Group Info", CMNU_GROUP_INFO);
        }
        if(gs.n + gs.constraints == 1 && gs.stylables == 1) {
            AddContextMenuItem("Style Info", CMNU_STYLE_INFO);
        }
        if(gs.withEndpoints > 0) {
            AddContextMenuItem("Select Edge Chain", CMNU_SELECT_CHAIN);
        }
        if(gs.constraints == 1 && gs.n == 0) {
            Constraint *c = SK.GetConstraint(gs.constraint[0]);
            if(c->HasLabel() && c->type != Constraint::COMMENT) {
                AddContextMenuItem("Toggle Reference Dimension",
                    CMNU_REFERENCE_DIM);
            }
            if(c->type == Constraint::ANGLE ||
               c->type == Constraint::EQUAL_ANGLE)
            {
                AddContextMenuItem("Other Supplementary Angle",
                    CMNU_OTHER_ANGLE);
            }
        }
        if(gs.comments > 0 || gs.points > 0) {
            AddContextMenuItem("Snap to Grid", CMNU_SNAP_TO_GRID);
        }

        if(gs.points == 1) {
            Entity *p = SK.GetEntity(gs.point[0]);
            Constraint *c;
            IdList<Constraint,hConstraint> *lc = &(SK.constraint);
            for(c = lc->First(); c; c = lc->NextAfter(c)) {
                if(c->type != Constraint::POINTS_COINCIDENT) continue;
                if(c->ptA.v == p->h.v || c->ptB.v == p->h.v) {
                    break;
                }
            }
            if(c) {
                AddContextMenuItem("Delete Point-Coincident Constraint",
                                   CMNU_DEL_COINCIDENT);
            }
        }
        AddContextMenuItem(NULL, CONTEXT_SEPARATOR);
        if(LockedInWorkplane()) {
            AddContextMenuItem("Cut",  CMNU_CUT_SEL);
            AddContextMenuItem("Copy", CMNU_COPY_SEL);
        }
    }

    if(SS.clipboard.r.n > 0 && LockedInWorkplane()) {
        AddContextMenuItem("Paste", CMNU_PASTE_SEL);
    }

    if(itemsSelected) {
        AddContextMenuItem("Delete", CMNU_DELETE_SEL);
        AddContextMenuItem(NULL, CONTEXT_SEPARATOR);
        AddContextMenuItem("Unselect All", CMNU_UNSELECT_ALL);
    }
    // If only one item is selected, then it must be the one that we just
    // selected from the hovered item; in which case unselect all and hovered
    // are equivalent.
    if(!hover.IsEmpty() && selection.n > 1) {
        AddContextMenuItem("Unselect Hovered", CMNU_UNSELECT_HOVERED);
    }

    int ret = ShowContextMenu();
    switch(ret) {
        case CMNU_UNSELECT_ALL:
            MenuEdit(MNU_UNSELECT_ALL);
            break;

        case CMNU_UNSELECT_HOVERED:
            if(!hover.IsEmpty()) {
                MakeUnselected(&hover, true);
            }
            break;

        case CMNU_SELECT_CHAIN:
            MenuEdit(MNU_SELECT_CHAIN);
            break;

        case CMNU_CUT_SEL:
            MenuClipboard(MNU_CUT);
            break;

        case CMNU_COPY_SEL:
            MenuClipboard(MNU_COPY);
            break;

        case CMNU_PASTE_SEL:
            MenuClipboard(MNU_PASTE);
            break;

        case CMNU_DELETE_SEL:
            MenuClipboard(MNU_DELETE);
            break;

        case CMNU_REFERENCE_DIM:
            Constraint::MenuConstrain(MNU_REFERENCE);
            break;

        case CMNU_OTHER_ANGLE:
            Constraint::MenuConstrain(MNU_OTHER_ANGLE);
            break;

        case CMNU_DEL_COINCIDENT: {
            SS.UndoRemember();
            if(!gs.point[0].v) break;
            Entity *p = SK.GetEntity(gs.point[0]);
            if(!p->IsPoint()) break;

            SK.constraint.ClearTags();
            Constraint *c;
            for(c = SK.constraint.First(); c; c = SK.constraint.NextAfter(c)) {
                if(c->type != Constraint::POINTS_COINCIDENT) continue;
                if(c->ptA.v == p->h.v || c->ptB.v == p->h.v) {
                    c->tag = 1;
                }
            }
            SK.constraint.RemoveTagged();
            ClearSelection();
            break;
        }

        case CMNU_SNAP_TO_GRID:
            MenuEdit(MNU_SNAP_TO_GRID);
            break;

        case CMNU_GROUP_INFO: {
            hGroup hg;
            if(gs.entities == 1) {
                hg = SK.GetEntity(gs.entity[0])->group;
            } else if(gs.points == 1) {
                hg = SK.GetEntity(gs.point[0])->group;
            } else if(gs.constraints == 1) {
                hg = SK.GetConstraint(gs.constraint[0])->group;
            } else {
                break;
            }
            ClearSelection();

            SS.TW.GoToScreen(TextWindow::SCREEN_GROUP_INFO);
            SS.TW.shown.group = hg;
            SS.ScheduleShowTW();
            break;
        }

        case CMNU_STYLE_INFO: {
            hStyle hs;
            if(gs.entities == 1) {
                hs = Style::ForEntity(gs.entity[0]);
            } else if(gs.points == 1) {
                hs = Style::ForEntity(gs.point[0]);
            } else if(gs.constraints == 1) {
                hs = SK.GetConstraint(gs.constraint[0])->disp.style;
                if(!hs.v) hs.v = Style::CONSTRAINT;
            } else {
                break;
            }
            ClearSelection();

            SS.TW.GoToScreen(TextWindow::SCREEN_STYLE_INFO);
            SS.TW.shown.style = hs;
            SS.ScheduleShowTW();
            break;
        }

        case CMNU_NEW_CUSTOM_STYLE: {
            uint32_t v = Style::CreateCustomStyle();
            Style::AssignSelectionToStyle(v);
            break;
        }

        case CMNU_NO_STYLE:
            Style::AssignSelectionToStyle(0);
            break;

        default:
            if(ret >= CMNU_FIRST_STYLE) {
                Style::AssignSelectionToStyle(ret - CMNU_FIRST_STYLE);
            } else {
                // otherwise it was cancelled, so do nothing
                contextMenuCancelTime = GetMilliseconds();
            }
            break;
    }

    context.active = false;
    SS.ScheduleShowTW();
}
コード例 #4
0
ファイル: constraint.cpp プロジェクト: astarasikov/solvespace
void Constraint::MenuConstrain(int id) {
    Constraint c;
    ZERO(&c);
    c.group = SS.GW.activeGroup;
    c.workplane = SS.GW.ActiveWorkplane();

    SS.GW.GroupSelection();
#define gs (SS.GW.gs)

    switch(id) {
        case GraphicsWindow::MNU_DISTANCE_DIA: {
            if(gs.points == 2 && gs.n == 2) {
                c.type = PT_PT_DISTANCE;
                c.ptA = gs.point[0];
                c.ptB = gs.point[1];
            } else if(gs.lineSegments == 1 && gs.n == 1) {
                c.type = PT_PT_DISTANCE;
                Entity *e = SK.GetEntity(gs.entity[0]);
                c.ptA = e->point[0];
                c.ptB = e->point[1];
            } else if(gs.vectors == 1 && gs.points == 2 && gs.n == 3) {
                c.type = PROJ_PT_DISTANCE;
                c.ptA = gs.point[0];
                c.ptB = gs.point[1];
                c.entityA = gs.vector[0];
            } else if(gs.workplanes == 1 && gs.points == 1 && gs.n == 2) {
                c.type = PT_PLANE_DISTANCE;
                c.ptA = gs.point[0];
                c.entityA = gs.entity[0];
            } else if(gs.lineSegments == 1 && gs.points == 1 && gs.n == 2) {
                c.type = PT_LINE_DISTANCE;
                c.ptA = gs.point[0];
                c.entityA = gs.entity[0];
            } else if(gs.faces == 1 && gs.points == 1 && gs.n == 2) {
                c.type = PT_FACE_DISTANCE;
                c.ptA = gs.point[0];
                c.entityA = gs.face[0];
            } else if(gs.circlesOrArcs == 1 && gs.n == 1) {
                c.type = DIAMETER;
                c.entityA = gs.entity[0];
            } else {
                Error(
"Bad selection for distance / diameter constraint. This "
"constraint can apply to:\n\n"
"    * two points (distance between points)\n"
"    * a line segment (length)\n"
"    * two points and a line segment or normal (projected distance)\n"
"    * a workplane and a point (minimum distance)\n"
"    * a line segment and a point (minimum distance)\n"
"    * a plane face and a point (minimum distance)\n"
"    * a circle or an arc (diameter)\n");
                return;
            }
            if(c.type == PT_PT_DISTANCE || c.type == PROJ_PT_DISTANCE) {
                Vector n = SS.GW.projRight.Cross(SS.GW.projUp);
                Vector a = SK.GetEntity(c.ptA)->PointGetNum();
                Vector b = SK.GetEntity(c.ptB)->PointGetNum();
                c.disp.offset = n.Cross(a.Minus(b));
                c.disp.offset = (c.disp.offset).WithMagnitude(50/SS.GW.scale);
            } else {
                c.disp.offset = Vector::From(0, 0, 0);
            }

            c.valA = 0;
            c.ModifyToSatisfy();
            AddConstraint(&c);
            break;
        }

        case GraphicsWindow::MNU_ON_ENTITY:
            if(gs.points == 2 && gs.n == 2) {
                c.type = POINTS_COINCIDENT;
                c.ptA = gs.point[0];
                c.ptB = gs.point[1];
            } else if(gs.points == 1 && gs.workplanes == 1 && gs.n == 2) {
                c.type = PT_IN_PLANE;
                c.ptA = gs.point[0];
                c.entityA = gs.entity[0];
            } else if(gs.points == 1 && gs.lineSegments == 1 && gs.n == 2) {
                c.type = PT_ON_LINE;
                c.ptA = gs.point[0];
                c.entityA = gs.entity[0];
            } else if(gs.points == 1 && gs.circlesOrArcs == 1 && gs.n == 2) {
                c.type = PT_ON_CIRCLE;
                c.ptA = gs.point[0];
                c.entityA = gs.entity[0];
            } else if(gs.points == 1 && gs.faces == 1 && gs.n == 2) {
                c.type = PT_ON_FACE;
                c.ptA = gs.point[0];
                c.entityA = gs.face[0];
            } else {
                Error("Bad selection for on point / curve / plane constraint. "
                      "This constraint can apply to:\n\n"
                      "    * two points (points coincident)\n"
                      "    * a point and a workplane (point in plane)\n"
                      "    * a point and a line segment (point on line)\n"
                      "    * a point and a circle or arc (point on curve)\n"
                      "    * a point and a plane face (point on face)\n");
                return;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_EQUAL:
            if(gs.lineSegments == 2 && gs.n == 2) {
                c.type = EQUAL_LENGTH_LINES;
                c.entityA = gs.entity[0];
                c.entityB = gs.entity[1];
            } else if(gs.lineSegments == 2 && gs.points == 2 && gs.n == 4) {
                c.type = EQ_PT_LN_DISTANCES;
                c.entityA = gs.entity[0];
                c.ptA = gs.point[0];
                c.entityB = gs.entity[1];
                c.ptB = gs.point[1];
            } else if(gs.lineSegments == 1 && gs.points == 2 && gs.n == 3) {
                // The same line segment for the distances, but different
                // points.
                c.type = EQ_PT_LN_DISTANCES;
                c.entityA = gs.entity[0];
                c.ptA = gs.point[0];
                c.entityB = gs.entity[0];
                c.ptB = gs.point[1];
            } else if(gs.lineSegments == 2 && gs.points == 1 && gs.n == 3) {
                c.type = EQ_LEN_PT_LINE_D;
                c.entityA = gs.entity[0];
                c.entityB = gs.entity[1];
                c.ptA = gs.point[0];
            } else if(gs.vectors == 4 && gs.n == 4) {
                c.type = EQUAL_ANGLE;
                c.entityA = gs.vector[0];
                c.entityB = gs.vector[1];
                c.entityC = gs.vector[2];
                c.entityD = gs.vector[3];
            } else if(gs.vectors == 3 && gs.n == 3) {
                c.type = EQUAL_ANGLE;
                c.entityA = gs.vector[0];
                c.entityB = gs.vector[1];
                c.entityC = gs.vector[1];
                c.entityD = gs.vector[2];
            } else if(gs.circlesOrArcs == 2 && gs.n == 2) {
                c.type = EQUAL_RADIUS;
                c.entityA = gs.entity[0];
                c.entityB = gs.entity[1];
            } else if(gs.arcs == 1 && gs.lineSegments == 1 && gs.n == 2) {
                c.type = EQUAL_LINE_ARC_LEN;
                if(SK.GetEntity(gs.entity[0])->type == Entity::ARC_OF_CIRCLE) {
                    c.entityA = gs.entity[1];
                    c.entityB = gs.entity[0];
                } else {
                    c.entityA = gs.entity[0];
                    c.entityB = gs.entity[1];
                }
            } else {
                Error("Bad selection for equal length / radius constraint. "
                      "This constraint can apply to:\n\n"
                      "    * two line segments (equal length)\n"
                      "    * two line segments and two points "
                              "(equal point-line distances)\n"
                      "    * a line segment and two points "
                              "(equal point-line distances)\n"
                      "    * a line segment, and a point and line segment "
                              "(point-line distance equals length)\n"
                      "    * four line segments or normals "
                              "(equal angle between A,B and C,D)\n"
                      "    * three line segments or normals "
                              "(equal angle between A,B and B,C)\n"
                      "    * two circles or arcs (equal radius)\n"
                      "    * a line segment and an arc "
                              "(line segment length equals arc length)\n");
                return;
            }
            if(c.type == EQUAL_ANGLE) {
                // Infer the nearest supplementary angle from the sketch.
                Vector a1 = SK.GetEntity(c.entityA)->VectorGetNum(),
                       b1 = SK.GetEntity(c.entityB)->VectorGetNum(),
                       a2 = SK.GetEntity(c.entityC)->VectorGetNum(),
                       b2 = SK.GetEntity(c.entityD)->VectorGetNum();
                double d1 = a1.Dot(b1), d2 = a2.Dot(b2);

                if(d1*d2 < 0) {
                    c.other = true;
                }
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_RATIO:
            if(gs.lineSegments == 2 && gs.n == 2) {
                c.type = LENGTH_RATIO;
                c.entityA = gs.entity[0];
                c.entityB = gs.entity[1];
            } else {
                Error("Bad selection for length ratio constraint. This "
                      "constraint can apply to:\n\n"
                      "    * two line segments\n");
                return;
            }

            c.valA = 0;
            c.ModifyToSatisfy();
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_AT_MIDPOINT:
            if(gs.lineSegments == 1 && gs.points == 1 && gs.n == 2) {
                c.type = AT_MIDPOINT;
                c.entityA = gs.entity[0];
                c.ptA = gs.point[0];

                // If a point is at-midpoint, then no reason to also constrain
                // it on-line; so auto-remove that.
                DeleteAllConstraintsFor(PT_ON_LINE, c.entityA, c.ptA);
            } else if(gs.lineSegments == 1 && gs.workplanes == 1 && gs.n == 2) {
                c.type = AT_MIDPOINT;
                int i = SK.GetEntity(gs.entity[0])->IsWorkplane() ? 1 : 0;
                c.entityA = gs.entity[i];
                c.entityB = gs.entity[1-i];
            } else {
                Error("Bad selection for at midpoint constraint. This "
                      "constraint can apply to:\n\n"
                      "    * a line segment and a point "
                            "(point at midpoint)\n"
                      "    * a line segment and a workplane "
                            "(line's midpoint on plane)\n");
                return;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_SYMMETRIC:
            if(gs.points == 2 &&
                                ((gs.workplanes == 1 && gs.n == 3) ||
                                 (gs.n == 2)))
            {
                c.entityA = gs.entity[0];
                c.ptA = gs.point[0];
                c.ptB = gs.point[1];
            } else if(gs.lineSegments == 1 &&
                                ((gs.workplanes == 1 && gs.n == 2) ||
                                 (gs.n == 1)))
            {
                int i = SK.GetEntity(gs.entity[0])->IsWorkplane() ? 1 : 0;
                Entity *line = SK.GetEntity(gs.entity[i]);
                c.entityA = gs.entity[1-i];
                c.ptA = line->point[0];
                c.ptB = line->point[1];
            } else if(SS.GW.LockedInWorkplane()
                        && gs.lineSegments == 2 && gs.n == 2)
            {
                Entity *l0 = SK.GetEntity(gs.entity[0]),
                       *l1 = SK.GetEntity(gs.entity[1]);

                if((l1->group.v != SS.GW.activeGroup.v) ||
                   (l1->construction && !(l0->construction)))
                {
                    SWAP(Entity *, l0, l1);
                }
                c.ptA = l1->point[0];
                c.ptB = l1->point[1];
                c.entityA = l0->h;
                c.type = SYMMETRIC_LINE;
            } else if(SS.GW.LockedInWorkplane()
                        && gs.lineSegments == 1 && gs.points == 2 && gs.n == 3)
            {
                c.ptA = gs.point[0];
                c.ptB = gs.point[1];
                c.entityA = gs.entity[0];
                c.type = SYMMETRIC_LINE;
            } else {
                Error("Bad selection for symmetric constraint. This constraint "
                      "can apply to:\n\n"
                      "    * two points or a line segment "
                          "(symmetric about workplane's coordinate axis)\n"
                      "    * line segment, and two points or a line segment "
                          "(symmetric about line segment)\n"
                      "    * workplane, and two points or a line segment "
                          "(symmetric about workplane)\n");
                return;
            }
            if(c.type != 0) {
                // Already done, symmetry about a line segment in a workplane
            } else if(c.entityA.v == Entity::NO_ENTITY.v) {
                // Horizontal / vertical symmetry, implicit symmetry plane
                // normal to the workplane
                if(c.workplane.v == Entity::FREE_IN_3D.v) {
                    Error("Must be locked in to workplane when constraining "
                          "symmetric without an explicit symmetry plane.");
                    return;
                }
                Vector pa = SK.GetEntity(c.ptA)->PointGetNum();
                Vector pb = SK.GetEntity(c.ptB)->PointGetNum();
                Vector dp = pa.Minus(pb);
                EntityBase *norm = SK.GetEntity(c.workplane)->Normal();;
                Vector u = norm->NormalU(), v = norm->NormalV();
                if(fabs(dp.Dot(u)) > fabs(dp.Dot(v))) {
                    c.type = SYMMETRIC_HORIZ;
                } else {
                    c.type = SYMMETRIC_VERT;
                }
                if(gs.lineSegments == 1) {
                    // If this line segment is already constrained horiz or
                    // vert, then auto-remove that redundant constraint.
                    DeleteAllConstraintsFor(HORIZONTAL, (gs.entity[0]),
                        Entity::NO_ENTITY);
                    DeleteAllConstraintsFor(VERTICAL, (gs.entity[0]),
                        Entity::NO_ENTITY);

                }
            } else {
                // Symmetry with a symmetry plane specified explicitly.
                c.type = SYMMETRIC;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_VERTICAL:
        case GraphicsWindow::MNU_HORIZONTAL: {
            hEntity ha, hb;
            if(c.workplane.v == Entity::FREE_IN_3D.v) {
                Error("Select workplane before constraining horiz/vert.");
                return;
            }
            if(gs.lineSegments == 1 && gs.n == 1) {
                c.entityA = gs.entity[0];
                Entity *e = SK.GetEntity(c.entityA);
                ha = e->point[0];
                hb = e->point[1];
            } else if(gs.points == 2 && gs.n == 2) {
                ha = c.ptA = gs.point[0];
                hb = c.ptB = gs.point[1];
            } else {
                Error("Bad selection for horizontal / vertical constraint. "
                      "This constraint can apply to:\n\n"
                      "    * two points\n"
                      "    * a line segment\n");
                return;
            }
            if(id == GraphicsWindow::MNU_HORIZONTAL) {
                c.type = HORIZONTAL;
            } else {
                c.type = VERTICAL;
            }
            AddConstraint(&c);
            break;
        }

        case GraphicsWindow::MNU_ORIENTED_SAME: {
            if(gs.anyNormals == 2 && gs.n == 2) {
                c.type = SAME_ORIENTATION;
                c.entityA = gs.anyNormal[0];
                c.entityB = gs.anyNormal[1];
            } else {
                Error("Bad selection for same orientation constraint. This "
                      "constraint can apply to:\n\n"
                      "    * two normals\n");
                return;
            }
            SS.UndoRemember();

            Entity *nfree = SK.GetEntity(c.entityA);
            Entity *nref  = SK.GetEntity(c.entityB);
            if(nref->group.v == SS.GW.activeGroup.v) {
                SWAP(Entity *, nref, nfree);
            }
            if(nfree->group.v == SS.GW.activeGroup.v &&
               nref ->group.v != SS.GW.activeGroup.v)
            {
                // nfree is free, and nref is locked (since it came from a
                // previous group); so let's force nfree aligned to nref,
                // and make convergence easy
                Vector ru = nref ->NormalU(), rv = nref ->NormalV();
                Vector fu = nfree->NormalU(), fv = nfree->NormalV();

                if(fabs(fu.Dot(ru)) < fabs(fu.Dot(rv))) {
                    // There might be an odd*90 degree rotation about the
                    // normal vector; allow that, since the numerical
                    // constraint does
                    SWAP(Vector, ru, rv);
                }
                fu = fu.Dot(ru) > 0 ? ru : ru.ScaledBy(-1);
                fv = fv.Dot(rv) > 0 ? rv : rv.ScaledBy(-1);

                nfree->NormalForceTo(Quaternion::From(fu, fv));
            }
            AddConstraint(&c, false);
            break;
        }

        case GraphicsWindow::MNU_OTHER_ANGLE:
            if(gs.constraints == 1 && gs.n == 0) {
                Constraint *c = SK.GetConstraint(gs.constraint[0]);
                if(c->type == ANGLE) {
                    SS.UndoRemember();
                    c->other = !(c->other);
                    c->ModifyToSatisfy();
                    break;
                }
                if(c->type == EQUAL_ANGLE) {
                    SS.UndoRemember();
                    c->other = !(c->other);
                    SS.MarkGroupDirty(c->group);
                    SS.ScheduleGenerateAll();
                    break;
                }
            }
            Error("Must select an angle constraint.");
            return;

        case GraphicsWindow::MNU_REFERENCE:
            if(gs.constraints == 1 && gs.n == 0) {
                Constraint *c = SK.GetConstraint(gs.constraint[0]);
                if(c->HasLabel() && c->type != COMMENT) {
                    (c->reference) = !(c->reference);
                    SK.GetGroup(c->group)->clean = false;
                    SS.GenerateAll();
                    break;
                }
            }
            Error("Must select a constraint with associated label.");
            return;

        case GraphicsWindow::MNU_ANGLE: {
            if(gs.vectors == 2 && gs.n == 2) {
                c.type = ANGLE;
                c.entityA = gs.vector[0];
                c.entityB = gs.vector[1];
                c.valA = 0;
            } else {
                Error("Bad selection for angle constraint. This constraint "
                      "can apply to:\n\n"
                      "    * two line segments\n"
                      "    * a line segment and a normal\n"
                      "    * two normals\n");
                return;
            }

            Entity *ea = SK.GetEntity(c.entityA),
                   *eb = SK.GetEntity(c.entityB);
            if(ea->type == Entity::LINE_SEGMENT &&
               eb->type == Entity::LINE_SEGMENT)
            {
                Vector a0 = SK.GetEntity(ea->point[0])->PointGetNum(),
                       a1 = SK.GetEntity(ea->point[1])->PointGetNum(),
                       b0 = SK.GetEntity(eb->point[0])->PointGetNum(),
                       b1 = SK.GetEntity(eb->point[1])->PointGetNum();
                if(a0.Equals(b0) || a1.Equals(b1)) {
                    // okay, vectors should be drawn in same sense
                } else if(a0.Equals(b1) || a1.Equals(b0)) {
                    // vectors are in opposite sense
                    c.other = true;
                } else {
                    // no shared point; not clear which intersection to draw
                }
            }
            c.ModifyToSatisfy();
            AddConstraint(&c);
            break;
        }

        case GraphicsWindow::MNU_PARALLEL:
            if(gs.vectors == 2 && gs.n == 2) {
                c.type = PARALLEL;
                c.entityA = gs.vector[0];
                c.entityB = gs.vector[1];
            } else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) {
                Entity *line = SK.GetEntity(gs.entity[0]);
                Entity *arc  = SK.GetEntity(gs.entity[1]);
                if(line->type == Entity::ARC_OF_CIRCLE) {
                    SWAP(Entity *, line, arc);
                }
                Vector l0 = SK.GetEntity(line->point[0])->PointGetNum(),
                       l1 = SK.GetEntity(line->point[1])->PointGetNum();
                Vector a1 = SK.GetEntity(arc->point[1])->PointGetNum(),
                       a2 = SK.GetEntity(arc->point[2])->PointGetNum();

                if(l0.Equals(a1) || l1.Equals(a1)) {
                    c.other = false;
                } else if(l0.Equals(a2) || l1.Equals(a2)) {
                    c.other = true;
                } else {
                    Error("The tangent arc and line segment must share an "
                          "endpoint. Constrain them with Constrain -> "
                          "On Point before constraining tangent.");
                    return;
                }
                c.type = ARC_LINE_TANGENT;
                c.entityA = arc->h;
                c.entityB = line->h;
            } else if(gs.lineSegments == 1 && gs.cubics == 1 && gs.n == 2) {
                Entity *line  = SK.GetEntity(gs.entity[0]);
                Entity *cubic = SK.GetEntity(gs.entity[1]);
                if(line->type == Entity::CUBIC) {
                    SWAP(Entity *, line, cubic);
                }
                Vector l0 = SK.GetEntity(line->point[0])->PointGetNum(),
                       l1 = SK.GetEntity(line->point[1])->PointGetNum();
                Vector as = cubic->CubicGetStartNum(),
                       af = cubic->CubicGetFinishNum();

                if(l0.Equals(as) || l1.Equals(as)) {
                    c.other = false;
                } else if(l0.Equals(af) || l1.Equals(af)) {
                    c.other = true;
                } else {
                    Error("The tangent cubic and line segment must share an "
                          "endpoint. Constrain them with Constrain -> "
                          "On Point before constraining tangent.");
                    return;
                }
                c.type = CUBIC_LINE_TANGENT;
                c.entityA = cubic->h;
                c.entityB = line->h;
            } else if(gs.cubics + gs.arcs == 2 && gs.n == 2) {
                if(!SS.GW.LockedInWorkplane()) {
                    Error("Curve-curve tangency must apply in workplane.");
                    return;
                }
                Entity *eA = SK.GetEntity(gs.entity[0]),
                       *eB = SK.GetEntity(gs.entity[1]);
                Vector as = eA->EndpointStart(),
                       af = eA->EndpointFinish(),
                       bs = eB->EndpointStart(),
                       bf = eB->EndpointFinish();
                if(as.Equals(bs)) {
                    c.other = false; c.other2 = false;
                } else if(as.Equals(bf)) {
                    c.other = false; c.other2 = true;
                } else if(af.Equals(bs)) {
                    c.other = true; c.other2 = false;
                } else if(af.Equals(bf)) {
                    c.other = true; c.other2 = true;
                } else {
                    Error("The curves must share an endpoint. Constrain them "
                          "with Constrain -> On Point before constraining "
                          "tangent.");
                    return;
                }
                c.type = CURVE_CURVE_TANGENT;
                c.entityA = eA->h;
                c.entityB = eB->h;
            } else {
                Error("Bad selection for parallel / tangent constraint. This "
                      "constraint can apply to:\n\n"
                      "    * two line segments (parallel)\n"
                      "    * a line segment and a normal (parallel)\n"
                      "    * two normals (parallel)\n"
                      "    * two line segments, arcs, or beziers, that share "
                            "an endpoint (tangent)\n");
                return;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_PERPENDICULAR:
            if(gs.vectors == 2 && gs.n == 2) {
                c.type = PERPENDICULAR;
                c.entityA = gs.vector[0];
                c.entityB = gs.vector[1];
            } else {
                Error("Bad selection for perpendicular constraint. This "
                      "constraint can apply to:\n\n"
                      "    * two line segments\n"
                      "    * a line segment and a normal\n"
                      "    * two normals\n");
                return;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_WHERE_DRAGGED:
            if(gs.points == 1 && gs.n == 1) {
                c.type = WHERE_DRAGGED;
                c.ptA = gs.point[0];
            } else {
                Error("Bad selection for lock point where dragged constraint. "
                      "This constraint can apply to:\n\n"
                      "    * a point\n");
                return;
            }
            AddConstraint(&c);
            break;

        case GraphicsWindow::MNU_COMMENT:
            SS.GW.pending.operation = GraphicsWindow::MNU_COMMENT;
            SS.GW.pending.description = "click center of comment text";
            SS.ScheduleShowTW();
            break;

        default: oops();
    }

    SS.GW.ClearSelection();
    InvalidateGraphics();
}