diff --git a/Makefile b/Makefile index b50d96d2..ac875822 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ SSOBJS = $(OBJDIR)\solvespace.obj \ $(OBJDIR)\textwin.obj \ $(OBJDIR)\textscreens.obj \ $(OBJDIR)\graphicswin.obj \ + $(OBJDIR)\modify.obj \ $(OBJDIR)\util.obj \ $(OBJDIR)\entity.obj \ $(OBJDIR)\drawentity.obj \ diff --git a/drawconstraint.cpp b/drawconstraint.cpp index 54782f6a..8eb4e673 100644 --- a/drawconstraint.cpp +++ b/drawconstraint.cpp @@ -117,23 +117,18 @@ void Constraint::DoArcForAngle(Vector a0, Vector da, Vector b0, Vector db, db = db.ProjectVectorInto(workplane); } - // Make an orthogonal coordinate system from those directions - Vector dn = da.Cross(db); // normal to both - Vector dna = dn.Cross(da); // normal to da - Vector dnb = dn.Cross(db); // normal to db - // At the intersection of the lines - // a0 + pa*da = b0 + pb*db (where pa, pb are scalar params) - // So dot this equation against dna and dnb to get two equations - // to solve for da and db - double pb = ((a0.Minus(b0)).Dot(dna))/(db.Dot(dna)); - double pa = -((a0.Minus(b0)).Dot(dnb))/(da.Dot(dnb)); + bool skew; + Vector pi = Vector::AtIntersectionOfLines(a0, a0.Plus(da), + b0, b0.Plus(db), &skew); - Vector pi = a0.Plus(da.ScaledBy(pa)); - if(pi.Equals(b0.Plus(db.ScaledBy(pb)))) { + if(!skew) { *ref = pi.Plus(offset); - // We draw in a coordinate system centered at pi, with - // basis vectors da and dna. + // We draw in a coordinate system centered at the intersection point. + // One basis vector is da, and the other is normal to da and in + // the plane that contains our lines (so normal to its normal). + Vector dna = (da.Cross(db)).Cross(da); da = da.WithMagnitude(1); dna = dna.WithMagnitude(1); + Vector rm = (*ref).Minus(pi); double rda = rm.Dot(da), rdna = rm.Dot(dna); double r = sqrt(rda*rda + rdna*rdna); diff --git a/dsc.h b/dsc.h index d4cfbf1d..92a21792 100644 --- a/dsc.h +++ b/dsc.h @@ -45,6 +45,9 @@ public: static Vector From(hParam x, hParam y, hParam z); static Vector AtIntersectionOfPlanes(Vector n1, double d1, Vector n2, double d2); + static Vector AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew); double Element(int i); bool Equals(Vector v, double tol=LENGTH_EPS); diff --git a/entity.cpp b/entity.cpp index 295d8c72..182cf190 100644 --- a/entity.cpp +++ b/entity.cpp @@ -118,7 +118,9 @@ void Entity::ArcGetAngles(double *thetaa, double *thetab, double *dtheta) { *thetaa = atan2(pa2.y, pa2.x); *thetab = atan2(pb2.y, pb2.x); *dtheta = *thetab - *thetaa; - while(*dtheta < 0) *dtheta += 2*PI; + // If the endpoints are coincident, call it a full arc, not a zero arc; + // useful concept to have when splitting + while(*dtheta < 1e-6) *dtheta += 2*PI; while(*dtheta > (2*PI)) *dtheta -= 2*PI; } diff --git a/generate.cpp b/generate.cpp index 7e0a0fa1..a8376cc7 100644 --- a/generate.cpp +++ b/generate.cpp @@ -36,6 +36,8 @@ bool SolveSpace::PruneOrphans(void) { if(GroupExists(c->group)) continue; (deleted.constraints)++; + (deleted.nonTrivialConstraints)++; + constraint.RemoveById(c->h); return true; } @@ -118,6 +120,13 @@ bool SolveSpace::PruneConstraints(hGroup hg) { } (deleted.constraints)++; + if(c->type != Constraint::POINTS_COINCIDENT && + c->type != Constraint::HORIZONTAL && + c->type != Constraint::VERTICAL) + { + (deleted.nonTrivialConstraints)++; + } + constraint.RemoveById(c->h); return true; } @@ -264,21 +273,29 @@ void SolveSpace::GenerateAll(int first, int last) { } later.showTW = true; GW.ClearSuper(); - // Don't display any errors until we've regenerated fully. The - // sketch is not necessarily in a consistent state until we've - // pruned any orphaned etc. objects, and the message loop for the - // messagebox could allow us to repaint and crash. But now we must - // be fine. - Error("Additional sketch elements were deleted, because they depend " - "on the element that was just deleted explicitly. These " - "include: \r\n" - " %d request%s\r\n" - " %d constraint%s\r\n" - " %d group%s\r\n\r\n" - "Choose Edit -> Undo to undelete all elements.", - deleted.requests, deleted.requests == 1 ? "" : "s", - deleted.constraints, deleted.constraints == 1 ? "" : "s", - deleted.groups, deleted.groups == 1 ? "" : "s"); + + // People get annoyed if I complain whenever they delete any request, + // and I otherwise will, since those always come with pt-coincident + // constraints. + if(deleted.requests > 0 || deleted.nonTrivialConstraints > 0 || + deleted.groups > 0) + { + // Don't display any errors until we've regenerated fully. The + // sketch is not necessarily in a consistent state until we've + // pruned any orphaned etc. objects, and the message loop for the + // messagebox could allow us to repaint and crash. But now we must + // be fine. + Error("Additional sketch elements were deleted, because they " + "depend on the element that was just deleted explicitly. " + "These include: \r\n" + " %d request%s\r\n" + " %d constraint%s\r\n" + " %d group%s\r\n\r\n" + "Choose Edit -> Undo to undelete all elements.", + deleted.requests, deleted.requests == 1 ? "" : "s", + deleted.constraints, deleted.constraints == 1 ? "" : "s", + deleted.groups, deleted.groups == 1 ? "" : "s"); + } memset(&deleted, 0, sizeof(deleted)); } diff --git a/graphicswin.cpp b/graphicswin.cpp index 1da1df6b..ec617b3d 100644 --- a/graphicswin.cpp +++ b/graphicswin.cpp @@ -79,6 +79,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, "&Text in TrueType Font\tT", MNU_TTF_TEXT, 'T', mReq }, { 1, NULL, 0, NULL }, { 1, "To&ggle Construction\tG", MNU_CONSTRUCTION, 'G', mReq }, +{ 1, "Split Curves at &Intersection\tI", MNU_SPLIT_CURVES, 'I', mReq }, { 0, "&Constrain", 0, NULL }, { 1, "&Distance / Diameter\tD", MNU_DISTANCE_DIA, 'D', mCon }, @@ -510,6 +511,22 @@ void GraphicsWindow::ForceTextWindowShown(void) { } } +void GraphicsWindow::DeleteTaggedRequests(void) { + SS.request.RemoveTagged(); + + // An edit might be in progress for the just-deleted item. So + // now it's not. + HideGraphicsEditControl(); + HideTextEditControl(); + // And clear out the selection, which could contain that item. + ClearSuper(); + // And regenerate to get rid of what it generates, plus anything + // that references it (since the regen code checks for that). + SS.GenerateAll(0, INT_MAX); + EnsureValidActives(); + SS.later.showTW = true; +} + void GraphicsWindow::MenuEdit(int id) { switch(id) { case MNU_UNSELECT_ALL: @@ -542,20 +559,9 @@ void GraphicsWindow::MenuEdit(int id) { SS.constraint.Tag(s->constraint, 1); } } - SS.request.RemoveTagged(); - SS.constraint.RemoveTagged(); - // An edit might be in progress for the just-deleted item. So - // now it's not. - HideGraphicsEditControl(); - HideTextEditControl(); - // And clear out the selection, which could contain that item. - SS.GW.ClearSuper(); - // And regenerate to get rid of what it generates, plus anything - // that references it (since the regen code checks for that). - SS.GenerateAll(0, INT_MAX); - SS.GW.EnsureValidActives(); - SS.later.showTW = true; + SS.constraint.RemoveTagged(); + SS.GW.DeleteTaggedRequests(); break; } @@ -612,138 +618,7 @@ void GraphicsWindow::MenuRequest(int id) { case MNU_ARC: { SS.GW.GroupSelection(); if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) { - if(!SS.GW.LockedInWorkplane()) { - Error("Must be sketching in workplane to create tangent " - "arc."); - break; - } - - // Find two line segments that join at the specified point, - // and blend them with a tangent arc. First, find the - // requests that generate the line segments. - Vector pshared = SS.GetEntity(SS.GW.gs.point[0])->PointGetNum(); - SS.GW.ClearSelection(); - - int i, c = 0; - Entity *line[2]; - Request *lineReq[2]; - bool point1[2]; - for(i = 0; i < SS.request.n; i++) { - Request *r = &(SS.request.elem[i]); - if(r->group.v != SS.GW.activeGroup.v) continue; - if(r->type != Request::LINE_SEGMENT) continue; - if(r->construction) continue; - - Entity *e = SS.GetEntity(r->h.entity(0)); - Vector p0 = SS.GetEntity(e->point[0])->PointGetNum(), - p1 = SS.GetEntity(e->point[1])->PointGetNum(); - - if(p0.Equals(pshared) || p1.Equals(pshared)) { - if(c < 2) { - line[c] = e; - lineReq[c] = r; - point1[c] = (p1.Equals(pshared)); - } - c++; - } - } - if(c != 2) { - Error("To create a tangent arc, select a point where " - "two non-construction line segments join."); - break; - } - - SS.UndoRemember(); - - Entity *wrkpl = SS.GetEntity(SS.GW.ActiveWorkplane()); - Vector wn = wrkpl->Normal()->NormalN(); - - hEntity hshared = (line[0])->point[point1[0] ? 1 : 0], - hother0 = (line[0])->point[point1[0] ? 0 : 1], - hother1 = (line[1])->point[point1[1] ? 0 : 1]; - - Vector pother0 = SS.GetEntity(hother0)->PointGetNum(), - pother1 = SS.GetEntity(hother1)->PointGetNum(); - - Vector v0shared = pshared.Minus(pother0), - v1shared = pshared.Minus(pother1); - - hEntity srcline0 = (line[0])->h, - srcline1 = (line[1])->h; - - (lineReq[0])->construction = true; - (lineReq[1])->construction = true; - - // And thereafter we mustn't touch the entity or req ptrs, - // because the new requests/entities we add might force a - // realloc. - memset(line, 0, sizeof(line)); - memset(lineReq, 0, sizeof(lineReq)); - - // The sign of vv determines whether shortest distance is - // clockwise or anti-clockwise. - Vector v = (wn.Cross(v0shared)).WithMagnitude(1); - double vv = v1shared.Dot(v); - - double dot = (v0shared.WithMagnitude(1)).Dot( - v1shared.WithMagnitude(1)); - double theta = acos(dot); - double r = 200/SS.GW.scale; - // Set the radius so that no more than one third of the - // line segment disappears. - r = min(r, v0shared.Magnitude()*tan(theta/2)/3); - r = min(r, v1shared.Magnitude()*tan(theta/2)/3); - double el = r/tan(theta/2); - - hRequest rln0 = SS.GW.AddRequest(Request::LINE_SEGMENT, false), - rln1 = SS.GW.AddRequest(Request::LINE_SEGMENT, false); - hRequest rarc = SS.GW.AddRequest(Request::ARC_OF_CIRCLE, false); - - Entity *ln0 = SS.GetEntity(rln0.entity(0)), - *ln1 = SS.GetEntity(rln1.entity(0)); - Entity *arc = SS.GetEntity(rarc.entity(0)); - - SS.GetEntity(ln0->point[0])->PointForceTo(pother0); - Constraint::ConstrainCoincident(ln0->point[0], hother0); - SS.GetEntity(ln1->point[0])->PointForceTo(pother1); - Constraint::ConstrainCoincident(ln1->point[0], hother1); - - Vector arc0 = pshared.Minus(v0shared.WithMagnitude(el)); - Vector arc1 = pshared.Minus(v1shared.WithMagnitude(el)); - - SS.GetEntity(ln0->point[1])->PointForceTo(arc0); - SS.GetEntity(ln1->point[1])->PointForceTo(arc1); - - Constraint::Constrain(Constraint::PT_ON_LINE, - ln0->point[1], Entity::NO_ENTITY, srcline0); - Constraint::Constrain(Constraint::PT_ON_LINE, - ln1->point[1], Entity::NO_ENTITY, srcline1); - - Vector center = arc0; - int a, b; - if(vv < 0) { - a = 1; b = 2; - center = center.Minus(v0shared.Cross(wn).WithMagnitude(r)); - } else { - a = 2; b = 1; - center = center.Plus(v0shared.Cross(wn).WithMagnitude(r)); - } - - SS.GetEntity(arc->point[0])->PointForceTo(center); - SS.GetEntity(arc->point[a])->PointForceTo(arc0); - SS.GetEntity(arc->point[b])->PointForceTo(arc1); - - Constraint::ConstrainCoincident(arc->point[a], ln0->point[1]); - Constraint::ConstrainCoincident(arc->point[b], ln1->point[1]); - - Constraint::Constrain(Constraint::ARC_LINE_TANGENT, - Entity::NO_ENTITY, Entity::NO_ENTITY, - arc->h, ln0->h, (a==2)); - Constraint::Constrain(Constraint::ARC_LINE_TANGENT, - Entity::NO_ENTITY, Entity::NO_ENTITY, - arc->h, ln1->h, (b==2)); - - SS.later.generateAll = true; + SS.GW.MakeTangentArc(); } else { s = "click point on arc (draws anti-clockwise)"; goto c; @@ -778,6 +653,10 @@ c: break; } + case MNU_SPLIT_CURVES: + SS.GW.SplitLinesOrCurves(); + break; + default: oops(); } } diff --git a/modify.cpp b/modify.cpp new file mode 100644 index 00000000..1ea32a5a --- /dev/null +++ b/modify.cpp @@ -0,0 +1,409 @@ +#include "solvespace.h" + +//----------------------------------------------------------------------------- +// Replace a point-coincident constraint on oldpt with that same constraint +// on newpt. Useful when splitting or tangent arcing. +//----------------------------------------------------------------------------- +void GraphicsWindow::ReplacePointInConstraints(hEntity oldpt, hEntity newpt) { + int i; + for(i = 0; i < SS.constraint.n; i++) { + Constraint *c = &(SS.constraint.elem[i]); + + if(c->type == Constraint::POINTS_COINCIDENT) { + if(c->ptA.v == oldpt.v) c->ptA = newpt; + if(c->ptB.v == oldpt.v) c->ptB = newpt; + } + } +} + +//----------------------------------------------------------------------------- +// A single point must be selected when this function is called. We find two +// non-construction line segments that join at this point, and create a +// tangent arc joining them. +//----------------------------------------------------------------------------- +void GraphicsWindow::MakeTangentArc(void) { + if(!LockedInWorkplane()) { + Error("Must be sketching in workplane to create tangent " + "arc."); + return; + } + + // Find two line segments that join at the specified point, + // and blend them with a tangent arc. First, find the + // requests that generate the line segments. + Vector pshared = SS.GetEntity(gs.point[0])->PointGetNum(); + ClearSelection(); + + int i, c = 0; + Entity *line[2]; + Request *lineReq[2]; + bool point1[2]; + for(i = 0; i < SS.request.n; i++) { + Request *r = &(SS.request.elem[i]); + if(r->group.v != activeGroup.v) continue; + if(r->type != Request::LINE_SEGMENT) continue; + if(r->construction) continue; + + Entity *e = SS.GetEntity(r->h.entity(0)); + Vector p0 = SS.GetEntity(e->point[0])->PointGetNum(), + p1 = SS.GetEntity(e->point[1])->PointGetNum(); + + if(p0.Equals(pshared) || p1.Equals(pshared)) { + if(c < 2) { + line[c] = e; + lineReq[c] = r; + point1[c] = (p1.Equals(pshared)); + } + c++; + } + } + if(c != 2) { + Error("To create a tangent arc, select a point where " + "two non-construction line segments join."); + return; + } + + SS.UndoRemember(); + + Entity *wrkpl = SS.GetEntity(ActiveWorkplane()); + Vector wn = wrkpl->Normal()->NormalN(); + + hEntity hshared = (line[0])->point[point1[0] ? 1 : 0], + hother0 = (line[0])->point[point1[0] ? 0 : 1], + hother1 = (line[1])->point[point1[1] ? 0 : 1]; + + Vector pother0 = SS.GetEntity(hother0)->PointGetNum(), + pother1 = SS.GetEntity(hother1)->PointGetNum(); + + Vector v0shared = pshared.Minus(pother0), + v1shared = pshared.Minus(pother1); + + hEntity srcline0 = (line[0])->h, + srcline1 = (line[1])->h; + + (lineReq[0])->construction = true; + (lineReq[1])->construction = true; + + // And thereafter we mustn't touch the entity or req ptrs, + // because the new requests/entities we add might force a + // realloc. + memset(line, 0, sizeof(line)); + memset(lineReq, 0, sizeof(lineReq)); + + // The sign of vv determines whether shortest distance is + // clockwise or anti-clockwise. + Vector v = (wn.Cross(v0shared)).WithMagnitude(1); + double vv = v1shared.Dot(v); + + double dot = (v0shared.WithMagnitude(1)).Dot( + v1shared.WithMagnitude(1)); + double theta = acos(dot); + double r = 200/scale; + // Set the radius so that no more than one third of the + // line segment disappears. + r = min(r, v0shared.Magnitude()*tan(theta/2)/3); + r = min(r, v1shared.Magnitude()*tan(theta/2)/3); + double el = r/tan(theta/2); + + hRequest rln0 = AddRequest(Request::LINE_SEGMENT, false), + rln1 = AddRequest(Request::LINE_SEGMENT, false); + hRequest rarc = AddRequest(Request::ARC_OF_CIRCLE, false); + + Entity *ln0 = SS.GetEntity(rln0.entity(0)), + *ln1 = SS.GetEntity(rln1.entity(0)); + Entity *arc = SS.GetEntity(rarc.entity(0)); + + SS.GetEntity(ln0->point[0])->PointForceTo(pother0); + Constraint::ConstrainCoincident(ln0->point[0], hother0); + + SS.GetEntity(ln1->point[0])->PointForceTo(pother1); + Constraint::ConstrainCoincident(ln1->point[0], hother1); + + Vector arc0 = pshared.Minus(v0shared.WithMagnitude(el)); + Vector arc1 = pshared.Minus(v1shared.WithMagnitude(el)); + + SS.GetEntity(ln0->point[1])->PointForceTo(arc0); + SS.GetEntity(ln1->point[1])->PointForceTo(arc1); + + Constraint::Constrain(Constraint::PT_ON_LINE, + ln0->point[1], Entity::NO_ENTITY, srcline0); + Constraint::Constrain(Constraint::PT_ON_LINE, + ln1->point[1], Entity::NO_ENTITY, srcline1); + + Vector center = arc0; + int a, b; + if(vv < 0) { + a = 1; b = 2; + center = center.Minus(v0shared.Cross(wn).WithMagnitude(r)); + } else { + a = 2; b = 1; + center = center.Plus(v0shared.Cross(wn).WithMagnitude(r)); + } + + SS.GetEntity(arc->point[0])->PointForceTo(center); + SS.GetEntity(arc->point[a])->PointForceTo(arc0); + SS.GetEntity(arc->point[b])->PointForceTo(arc1); + + Constraint::ConstrainCoincident(arc->point[a], ln0->point[1]); + Constraint::ConstrainCoincident(arc->point[b], ln1->point[1]); + + Constraint::Constrain(Constraint::ARC_LINE_TANGENT, + Entity::NO_ENTITY, Entity::NO_ENTITY, + arc->h, ln0->h, (a==2)); + Constraint::Constrain(Constraint::ARC_LINE_TANGENT, + Entity::NO_ENTITY, Entity::NO_ENTITY, + arc->h, ln1->h, (b==2)); + + SS.later.generateAll = true; +} + +void GraphicsWindow::SplitLine(hEntity he, Vector pinter) { + // Save the original endpoints, since we're about to delete this entity. + Entity *e01 = SS.GetEntity(he); + hEntity hep0 = e01->point[0], hep1 = e01->point[1]; + Vector p0 = SS.GetEntity(hep0)->PointGetNum(), + p1 = SS.GetEntity(hep1)->PointGetNum(); + + SS.UndoRemember(); + + // Add the two line segments this one gets split into. + hRequest r0i = AddRequest(Request::LINE_SEGMENT, false), + ri1 = AddRequest(Request::LINE_SEGMENT, false); + // Don't get entities till after adding, realloc issues + + Entity *e0i = SS.GetEntity(r0i.entity(0)), + *ei1 = SS.GetEntity(ri1.entity(0)); + + SS.GetEntity(e0i->point[0])->PointForceTo(p0); + SS.GetEntity(e0i->point[1])->PointForceTo(pinter); + SS.GetEntity(ei1->point[0])->PointForceTo(pinter); + SS.GetEntity(ei1->point[1])->PointForceTo(p1); + + ReplacePointInConstraints(hep0, e0i->point[0]); + ReplacePointInConstraints(hep1, ei1->point[1]); + + // Finally, delete the original line + int i; + SS.request.ClearTags(); + for(i = 0; i < SS.request.n; i++) { + Request *r = &(SS.request.elem[i]); + if(r->group.v != activeGroup.v) continue; + if(r->type != Request::LINE_SEGMENT) continue; + + // If the user wants to keep the old lines around, they can just + // mark it construction first. + if(he.v == r->h.entity(0).v && !r->construction) { + r->tag = 1; + break; + } + } + DeleteTaggedRequests(); +} + +void GraphicsWindow::SplitCircle(hEntity he, Vector pinter) { + SS.UndoRemember(); + + Entity *circle = SS.GetEntity(he); + if(circle->type == Entity::CIRCLE) { + // Start with an unbroken circle, split it into a 360 degree arc. + Vector center = SS.GetEntity(circle->point[0])->PointGetNum(); + + circle = NULL; // shortly invalid! + hRequest hr = AddRequest(Request::ARC_OF_CIRCLE, false); + + Entity *arc = SS.GetEntity(hr.entity(0)); + + SS.GetEntity(arc->point[0])->PointForceTo(center); + SS.GetEntity(arc->point[1])->PointForceTo(pinter); + SS.GetEntity(arc->point[2])->PointForceTo(pinter); + } else { + // Start with an arc, break it in to two arcs + Vector center = SS.GetEntity(circle->point[0])->PointGetNum(), + start = SS.GetEntity(circle->point[1])->PointGetNum(), + finish = SS.GetEntity(circle->point[2])->PointGetNum(); + + circle = NULL; // shortly invalid! + hRequest hr0 = AddRequest(Request::ARC_OF_CIRCLE, false), + hr1 = AddRequest(Request::ARC_OF_CIRCLE, false); + + Entity *arc0 = SS.GetEntity(hr0.entity(0)), + *arc1 = SS.GetEntity(hr1.entity(0)); + + SS.GetEntity(arc0->point[0])->PointForceTo(center); + SS.GetEntity(arc0->point[1])->PointForceTo(start); + SS.GetEntity(arc0->point[2])->PointForceTo(pinter); + + SS.GetEntity(arc1->point[0])->PointForceTo(center); + SS.GetEntity(arc1->point[1])->PointForceTo(pinter); + SS.GetEntity(arc1->point[2])->PointForceTo(finish); + } + + // Finally, delete the original circle or arc + int i; + SS.request.ClearTags(); + for(i = 0; i < SS.request.n; i++) { + Request *r = &(SS.request.elem[i]); + if(r->group.v != activeGroup.v) continue; + if(r->type != Request::CIRCLE && r->type != Request::ARC_OF_CIRCLE) { + continue; + } + + // If the user wants to keep the old lines around, they can just + // mark it construction first. + if(he.v == r->h.entity(0).v && !r->construction) { + r->tag = 1; + break; + } + } + DeleteTaggedRequests(); +} + +void GraphicsWindow::SplitLinesOrCurves(void) { + if(!LockedInWorkplane()) { + Error("Must be sketching in workplane to split."); + return; + } + + GroupSelection(); + if(gs.n == 2 && gs.lineSegments == 2) { + Entity *la = SS.GetEntity(gs.entity[0]), + *lb = SS.GetEntity(gs.entity[1]); + Vector a0 = SS.GetEntity(la->point[0])->PointGetNum(), + a1 = SS.GetEntity(la->point[1])->PointGetNum(), + b0 = SS.GetEntity(lb->point[0])->PointGetNum(), + b1 = SS.GetEntity(lb->point[1])->PointGetNum(); + Vector da = a1.Minus(a0), db = b1.Minus(b0); + + // First, check if the lines intersect. + bool skew; + Vector pinter = Vector::AtIntersectionOfLines(a0, a1, b0, b1, &skew); + if(skew) { + Error("Lines are parallel or skew; no intersection to split."); + goto done; + } + + double ta = (pinter.Minus(a0)).DivPivoting(da), + tb = (pinter.Minus(b0)).DivPivoting(db); + + double tola = LENGTH_EPS/da.Magnitude(), + tolb = LENGTH_EPS/db.Magnitude(); + + hEntity ha = la->h, hb = lb->h; + la = NULL; lb = NULL; + // Following adds will cause a realloc, break the pointers + + bool didSomething = false; + if(ta > tola && ta < (1 - tola)) { + SplitLine(ha, pinter); + didSomething = true; + } + if(tb > tolb && tb < (1 - tolb)) { + SplitLine(hb, pinter); + didSomething = true; + } + if(!didSomething) { + Error( + "Nothing to split; intersection does not lie on either line."); + } + } else if(gs.n == 2 && gs.lineSegments == 1 && gs.circlesOrArcs == 1) { + Entity *line = SS.GetEntity(gs.entity[0]), + *circle = SS.GetEntity(gs.entity[1]); + if(line->type != Entity::LINE_SEGMENT) { + SWAP(Entity *, line, circle); + } + hEntity hline = line->h, hcircle = circle->h; + + Vector l0 = SS.GetEntity(line->point[0])->PointGetNum(), + l1 = SS.GetEntity(line->point[1])->PointGetNum(); + Vector dl = l1.Minus(l0); + + Quaternion q = SS.GetEntity(circle->normal)->NormalGetNum(); + Vector cn = q.RotationN(); + Vector cc = SS.GetEntity(circle->point[0])->PointGetNum(); + double cd = cc.Dot(cn); + double cr = circle->CircleGetRadiusNum(); + + if(fabs(l0.Dot(cn) - cd) > LENGTH_EPS || + fabs(l1.Dot(cn) - cd) > LENGTH_EPS) + { + Error("Lines does not lie in same plane as circle."); + goto done; + } + // Now let's see if they intersect; transform everything into a csys + // with origin at the center of the circle, and where the line is + // horizontal. + Vector n = cn.WithMagnitude(1); + Vector u = dl.WithMagnitude(1); + Vector v = n.Cross(u); + + Vector nl0 = (l0.Minus(cc)).DotInToCsys(u, v, n), + nl1 = (l1.Minus(cc)).DotInToCsys(u, v, n); + + double yint = nl0.y; + if(fabs(yint) > (cr - LENGTH_EPS)) { + Error("Line does not intersect (or is tangent to) circle."); + goto done; + } + double xint = sqrt(cr*cr - yint*yint); + + Vector inter0 = Vector::From( xint, yint, 0), + inter1 = Vector::From(-xint, yint, 0); + + // While we're here, let's calculate the angles at which the + // intersections (and the endpoints of the arc) occur. + double theta0 = atan2(yint, xint), theta1 = atan2(yint, -xint); + double thetamin, thetamax; + if(circle->type == Entity::CIRCLE) { + thetamin = 0; + thetamax = 2.1*PI; // fudge, make sure it's a good complete circle + } else { + Vector start = SS.GetEntity(circle->point[1])->PointGetNum(), + finish = SS.GetEntity(circle->point[2])->PointGetNum(); + start = (start .Minus(cc)).DotInToCsys(u, v, n); + finish = (finish.Minus(cc)).DotInToCsys(u, v, n); + thetamin = atan2(start.y, start.x); + thetamax = atan2(finish.y, finish.x); + + // Normalize; arc is drawn with increasing theta from start, + // so subtract that off and make all angles in (0, 2*pi] + theta0 = WRAP_NOT_0(theta0 - thetamin, 2*PI); + theta1 = WRAP_NOT_0(theta1 - thetamin, 2*PI); + thetamax = WRAP_NOT_0(thetamax - thetamin, 2*PI); + } + + // And move our intersections back out to the base frame. + inter0 = inter0.ScaleOutOfCsys(u, v, n).Plus(cc); + inter1 = inter1.ScaleOutOfCsys(u, v, n).Plus(cc); + + // So now we have our intersection points. Let's see where they are + // on the line. + double t0 = (inter0.Minus(l0)).DivPivoting(dl), + t1 = (inter1.Minus(l0)).DivPivoting(dl); + double tol = LENGTH_EPS/dl.Magnitude(); + + bool didSomething = false; + // Split only once, even if it crosses multiple times; just pick + // arbitrarily which. + if(t0 > tol && t0 < (1 - tol) && theta0 < thetamax) { + SplitLine(hline, inter0); + SplitCircle(hcircle, inter0); + didSomething = true; + } else if(t1 > tol && t1 < (1 - tol) && theta1 < thetamax) { + SplitLine(hline, inter1); + SplitCircle(hcircle, inter1); + didSomething = true; + } + if(!didSomething) { + Error("Nothing to split; neither intersection lies on both the " + "line and the circle."); + } + } else { + Error("Can't split these entities; select two lines, a line and " + "a circle, or a line and an arc."); + } + +done: + ClearSelection(); + SS.later.generateAll = true; +} + diff --git a/solvespace.h b/solvespace.h index 28896e98..386522eb 100644 --- a/solvespace.h +++ b/solvespace.h @@ -15,11 +15,19 @@ #define isnan(x) (((x) != (x)) || (x > 1e11) || (x < -1e11)) inline int WRAP(int v, int n) { + // Clamp it to the range [0, n) while(v >= n) v -= n; while(v < 0) v += n; return v; } +inline double WRAP_NOT_0(double v, double n) { + // Clamp it to the range (0, n] + while(v > n) v -= n; + while(v <= 0) v += n; + return v; +} + #define SWAP(T, a, b) do { T temp = (a); (a) = (b); (b) = temp; } while(0) #define ZERO(v) memset((v), 0, sizeof(*(v))) #define CO(v) (v).x, (v).y, (v).z @@ -447,6 +455,7 @@ public: int requests; int groups; int constraints; + int nonTrivialConstraints; } deleted; bool GroupExists(hGroup hg); bool PruneOrphans(void); diff --git a/toolbar.cpp b/toolbar.cpp index 1ff8edbe..d97357f0 100644 --- a/toolbar.cpp +++ b/toolbar.cpp @@ -14,7 +14,7 @@ static const struct { { Icon_bezier, GraphicsWindow::MNU_CUBIC, "Sketch cubic Bezier section" }, { Icon_point, GraphicsWindow::MNU_DATUM_POINT, "Sketch datum point" }, { Icon_construction, GraphicsWindow::MNU_CONSTRUCTION, "Toggle construction" }, - { Icon_trim, GraphicsWindow::MNU_CONSTRUCTION, "Split lines / curves where they intersect" }, + { Icon_trim, GraphicsWindow::MNU_SPLIT_CURVES, "Split lines / curves where they intersect" }, { SPACER }, { Icon_length, GraphicsWindow::MNU_DISTANCE_DIA, "Constrain distance / diameter / length" }, @@ -23,7 +23,7 @@ static const struct { { Icon_vert, GraphicsWindow::MNU_VERTICAL, "Constrain to be vertical" }, { Icon_parallel, GraphicsWindow::MNU_PARALLEL, "Constrain to be parallel or tangent" }, { Icon_perpendicular, GraphicsWindow::MNU_PERPENDICULAR, "Constrain to be perpendicular" }, - { Icon_pointonx, GraphicsWindow::MNU_ON_ENTITY, "Constrain point on line / curve / plane / face" }, + { Icon_pointonx, GraphicsWindow::MNU_ON_ENTITY, "Constrain point on line / curve / plane / point" }, { Icon_symmetric, GraphicsWindow::MNU_SYMMETRIC, "Constrain symmetric" }, { Icon_ref, GraphicsWindow::MNU_REFERENCE, "Toggle reference dimension" }, { SPACER }, diff --git a/ui.h b/ui.h index c2f4cd24..0f66a747 100644 --- a/ui.h +++ b/ui.h @@ -207,6 +207,7 @@ public: MNU_RECTANGLE, MNU_CUBIC, MNU_TTF_TEXT, + MNU_SPLIT_CURVES, MNU_CONSTRUCTION, // Group MNU_GROUP_3D, @@ -325,8 +326,15 @@ public: hConstraint constraintBeingEdited; bool ConstrainPointByHovered(hEntity pt); + void DeleteTaggedRequests(void); hRequest AddRequest(int type, bool rememberForUndo); hRequest AddRequest(int type); + + void MakeTangentArc(void); + void SplitLinesOrCurves(void); + void SplitLine(hEntity he, Vector pinter); + void SplitCircle(hEntity he, Vector pinter); + void ReplacePointInConstraints(hEntity oldpt, hEntity newpt); // The current selection. class Selection { diff --git a/util.cpp b/util.cpp index b28d88d5..e51275e5 100644 --- a/util.cpp +++ b/util.cpp @@ -550,6 +550,39 @@ Vector Vector::AtIntersectionOfPlanes(Vector n1, double d1, return (n1.ScaledBy(c1)).Plus(n2.ScaledBy(c2)); } +Vector Vector::AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew) +{ + Vector da = a1.Minus(a0), db = b1.Minus(b0); + + // Make an orthogonal coordinate system from those directions + Vector dn = da.Cross(db); // normal to both + Vector dna = dn.Cross(da); // normal to da + Vector dnb = dn.Cross(db); // normal to db + + // At the intersection of the lines + // a0 + pa*da = b0 + pb*db (where pa, pb are scalar params) + // So dot this equation against dna and dnb to get two equations + // to solve for da and db + double pb = ((a0.Minus(b0)).Dot(dna))/(db.Dot(dna)); + double pa = -((a0.Minus(b0)).Dot(dnb))/(da.Dot(dnb)); + + // And from either of those, we get the intersection point. + Vector pi = a0.Plus(da.ScaledBy(pa)); + + if(skew) { + // Check if the intersection points on each line are actually + // coincident... + if(pi.Equals(b0.Plus(db.ScaledBy(pb)))) { + *skew = false; + } else { + *skew = true; + } + } + return pi; +} + Point2d Point2d::Plus(Point2d b) { Point2d r; r.x = x + b.x;