//----------------------------------------------------------------------------- // User-initiated (not parametric) operations to modify our sketch, by // changing the requests, like to round a corner or split curves where they // intersect. // // Copyright 2008-2013 Jonathan Westhues. //----------------------------------------------------------------------------- #include "solvespace.h" //----------------------------------------------------------------------------- // Replace constraints on oldpt with the same constraints on newpt. // Useful when splitting, tangent arcing, or removing bezier points. //----------------------------------------------------------------------------- void GraphicsWindow::ReplacePointInConstraints(hEntity oldpt, hEntity newpt) { int i; for(i = 0; i < SK.constraint.n; i++) { Constraint *c = &(SK.constraint.elem[i]); if(c->ptA.v == oldpt.v) c->ptA = newpt; if(c->ptB.v == oldpt.v) c->ptB = newpt; } } //----------------------------------------------------------------------------- // Remove constraints on hpt. Useful when removing bezier points. //----------------------------------------------------------------------------- void GraphicsWindow::RemoveConstraintsForPointBeingDeleted(hEntity hpt) { SK.constraint.ClearTags(); for(int i = 0; i < SK.constraint.n; i++) { Constraint *c = &(SK.constraint.elem[i]); if(c->ptA.v == hpt.v || c->ptB.v == hpt.v) { c->tag = 1; (SS.deleted.constraints)++; if(c->type != Constraint::POINTS_COINCIDENT && c->type != Constraint::HORIZONTAL && c->type != Constraint::VERTICAL) { (SS.deleted.nonTrivialConstraints)++; } } } SK.constraint.RemoveTagged(); } //----------------------------------------------------------------------------- // Let's say that A is coincident with B, and B is coincident with C. This // implies that A is coincident with C; but if we delete B, then both // constraints must be deleted too (since they reference B), and A is no // longer constrained to C. This routine adds back that constraint. //----------------------------------------------------------------------------- void GraphicsWindow::FixConstraintsForRequestBeingDeleted(hRequest hr) { Request *r = SK.GetRequest(hr); if(r->group.v != SS.GW.activeGroup.v) return; Entity *e; for(e = SK.entity.First(); e; e = SK.entity.NextAfter(e)) { if(!(e->h.isFromRequest())) continue; if(e->h.request().v != hr.v) continue; if(e->type != Entity::POINT_IN_2D && e->type != Entity::POINT_IN_3D) { continue; } // This is a point generated by the request being deleted; so fix // the constraints for that. FixConstraintsForPointBeingDeleted(e->h); } } void GraphicsWindow::FixConstraintsForPointBeingDeleted(hEntity hpt) { List ld = {}; Constraint *c; SK.constraint.ClearTags(); for(c = SK.constraint.First(); c; c = SK.constraint.NextAfter(c)) { if(c->type != Constraint::POINTS_COINCIDENT) continue; if(c->group.v != SS.GW.activeGroup.v) continue; if(c->ptA.v == hpt.v) { ld.Add(&(c->ptB)); c->tag = 1; } if(c->ptB.v == hpt.v) { ld.Add(&(c->ptA)); c->tag = 1; } } // Remove constraints without waiting for regeneration; this way // if another point takes the place of the deleted one (e.g. when // removing control points of a bezier) the constraint doesn't // spuriously move. Similarly, subsequent calls of this function // (if multiple coincident points are getting deleted) will work // correctly. SK.constraint.RemoveTagged(); // If more than one point was constrained coincident with hpt, then // those two points were implicitly coincident with each other. By // deleting hpt (and all constraints that mention it), we will delete // that relationship. So put it back here now. int i; for(i = 1; i < ld.n; i++) { Constraint::ConstrainCoincident(ld.elem[i-1], ld.elem[i]); } ld.Clear(); } //----------------------------------------------------------------------------- // A curve by its parametric equation, helper functions for computing tangent // arcs by a numerical method. //----------------------------------------------------------------------------- void GraphicsWindow::ParametricCurve::MakeFromEntity(hEntity he, bool reverse) { *this = {}; Entity *e = SK.GetEntity(he); if(e->type == Entity::LINE_SEGMENT) { isLine = true; p0 = e->EndpointStart(), p1 = e->EndpointFinish(); if(reverse) { swap(p0, p1); } } else if(e->type == Entity::ARC_OF_CIRCLE) { isLine = false; p0 = SK.GetEntity(e->point[0])->PointGetNum(); Vector pe = SK.GetEntity(e->point[1])->PointGetNum(); r = (pe.Minus(p0)).Magnitude(); e->ArcGetAngles(&theta0, &theta1, &dtheta); if(reverse) { swap(theta0, theta1); dtheta = -dtheta; } EntityBase *wrkpln = SK.GetEntity(e->workplane)->Normal(); u = wrkpln->NormalU(); v = wrkpln->NormalV(); } else { oops(); } } double GraphicsWindow::ParametricCurve::LengthForAuto(void) { if(isLine) { // Allow a third of the line to disappear with auto radius return (p1.Minus(p0)).Magnitude() / 3; } else { // But only a twentieth of the arc; shorter means fewer numerical // problems since the curve is more linear over shorter sections. return (fabs(dtheta)*r)/20; } } Vector GraphicsWindow::ParametricCurve::PointAt(double t) { if(isLine) { return p0.Plus((p1.Minus(p0)).ScaledBy(t)); } else { double theta = theta0 + dtheta*t; return p0.Plus(u.ScaledBy(r*cos(theta)).Plus(v.ScaledBy(r*sin(theta)))); } } Vector GraphicsWindow::ParametricCurve::TangentAt(double t) { if(isLine) { return p1.Minus(p0); } else { double theta = theta0 + dtheta*t; Vector t = u.ScaledBy(-r*sin(theta)).Plus(v.ScaledBy(r*cos(theta))); t = t.ScaledBy(dtheta); return t; } } hRequest GraphicsWindow::ParametricCurve::CreateRequestTrimmedTo(double t, bool extraConstraints, hEntity orig, hEntity arc, bool arcFinish) { hRequest hr; Entity *e; if(isLine) { hr = SS.GW.AddRequest(Request::LINE_SEGMENT, false), e = SK.GetEntity(hr.entity(0)); SK.GetEntity(e->point[0])->PointForceTo(PointAt(t)); SK.GetEntity(e->point[1])->PointForceTo(PointAt(1)); ConstrainPointIfCoincident(e->point[0]); ConstrainPointIfCoincident(e->point[1]); if(extraConstraints) { Constraint::Constrain(Constraint::PT_ON_LINE, hr.entity(1), Entity::NO_ENTITY, orig); } Constraint::Constrain(Constraint::ARC_LINE_TANGENT, Entity::NO_ENTITY, Entity::NO_ENTITY, arc, e->h, arcFinish, false); } else { hr = SS.GW.AddRequest(Request::ARC_OF_CIRCLE, false), e = SK.GetEntity(hr.entity(0)); SK.GetEntity(e->point[0])->PointForceTo(p0); if(dtheta > 0) { SK.GetEntity(e->point[1])->PointForceTo(PointAt(t)); SK.GetEntity(e->point[2])->PointForceTo(PointAt(1)); } else { SK.GetEntity(e->point[2])->PointForceTo(PointAt(t)); SK.GetEntity(e->point[1])->PointForceTo(PointAt(1)); } ConstrainPointIfCoincident(e->point[0]); ConstrainPointIfCoincident(e->point[1]); ConstrainPointIfCoincident(e->point[2]); // The tangency constraint alone is enough to fully constrain it, // so there's no need for more. Constraint::Constrain(Constraint::CURVE_CURVE_TANGENT, Entity::NO_ENTITY, Entity::NO_ENTITY, arc, e->h, arcFinish, (dtheta < 0)); } return hr; } //----------------------------------------------------------------------------- // If a point in the same group as hpt, and numerically coincident with hpt, // happens to exist, then constrain that point coincident to hpt. //----------------------------------------------------------------------------- void GraphicsWindow::ParametricCurve::ConstrainPointIfCoincident(hEntity hpt) { Entity *e, *pt; pt = SK.GetEntity(hpt); Vector ev, ptv; ptv = pt->PointGetNum(); for(e = SK.entity.First(); e; e = SK.entity.NextAfter(e)) { if(e->h.v == pt->h.v) continue; if(!e->IsPoint()) continue; if(e->group.v != pt->group.v) continue; if(e->workplane.v != pt->workplane.v) continue; ev = e->PointGetNum(); if(!ev.Equals(ptv)) continue; Constraint::ConstrainCoincident(hpt, e->h); break; } } //----------------------------------------------------------------------------- // 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; } // The point corresponding to the vertex to be rounded. Vector pshared = SK.GetEntity(gs.point[0])->PointGetNum(); ClearSelection(); // First, find two requests (that are not construction, and that are // in our group and workplane) that generate entities that have an // endpoint at our vertex to be rounded. int i, c = 0; Entity *ent[2]; Request *req[2]; hRequest hreq[2]; hEntity hent[2]; bool pointf[2]; for(i = 0; i < SK.request.n; i++) { Request *r = &(SK.request.elem[i]); if(r->group.v != activeGroup.v) continue; if(r->workplane.v != ActiveWorkplane().v) continue; if(r->construction) continue; if(r->type != Request::LINE_SEGMENT && r->type != Request::ARC_OF_CIRCLE) { continue; } Entity *e = SK.GetEntity(r->h.entity(0)); Vector ps = e->EndpointStart(), pf = e->EndpointFinish(); if(ps.Equals(pshared) || pf.Equals(pshared)) { if(c < 2) { // We record the entity and request and their handles, // and whether the vertex to be rounded is the start or // finish of this entity. ent[c] = e; hent[c] = e->h; req[c] = r; hreq[c] = r->h; pointf[c] = (pf.Equals(pshared)); } c++; } } if(c != 2) { Error("To create a tangent arc, select a point where two " "non-construction lines or cicles in this group and " "workplane join."); return; } Entity *wrkpl = SK.GetEntity(ActiveWorkplane()); Vector wn = wrkpl->Normal()->NormalN(); // Based on these two entities, we make the objects that we'll use to // numerically find the tangent arc. ParametricCurve pc[2]; pc[0].MakeFromEntity(ent[0]->h, pointf[0]); pc[1].MakeFromEntity(ent[1]->h, pointf[1]); // And thereafter we mustn't touch the entity or req ptrs, // because the new requests/entities we add might force a // realloc. memset(ent, 0, sizeof(ent)); memset(req, 0, sizeof(req)); Vector pinter; double r = 0.0, vv = 0.0; // We now do Newton iterations to find the tangent arc, and its positions // t back along the two curves, starting from shared point of the curves // at t = 0. Lots of iterations helps convergence, and this is still // ~10 ms for everything. int iters = 1000; double t[2] = { 0, 0 }, tp[2]; for(i = 0; i < iters + 20; i++) { Vector p0 = pc[0].PointAt(t[0]), p1 = pc[1].PointAt(t[1]), t0 = pc[0].TangentAt(t[0]), t1 = pc[1].TangentAt(t[1]); pinter = Vector::AtIntersectionOfLines(p0, p0.Plus(t0), p1, p1.Plus(t1), NULL, NULL, NULL); // The sign of vv determines whether shortest distance is // clockwise or anti-clockwise. Vector v = (wn.Cross(t0)).WithMagnitude(1); vv = t1.Dot(v); double dot = (t0.WithMagnitude(1)).Dot(t1.WithMagnitude(1)); double theta = acos(dot); if(SS.tangentArcManual) { r = SS.tangentArcRadius; } else { r = 200/scale; // Set the radius so that no more than one third of the // line segment disappears. r = min(r, pc[0].LengthForAuto()*tan(theta/2)); r = min(r, pc[1].LengthForAuto()*tan(theta/2));; } // We are source-stepping the radius, to improve convergence. So // ramp that for most of the iterations, and then do a few at // the end with that constant for polishing. if(i < iters) { r *= 0.1 + 0.9*i/((double)iters); } // The distance from the intersection of the lines to the endpoint // of the arc, along each line. double el = r/tan(theta/2); // Compute the endpoints of the arc, for each curve Vector pa0 = pinter.Plus(t0.WithMagnitude(el)), pa1 = pinter.Plus(t1.WithMagnitude(el)); tp[0] = t[0]; tp[1] = t[1]; // And convert those points to parameter values along the curve. t[0] += (pa0.Minus(p0)).DivPivoting(t0); t[1] += (pa1.Minus(p1)).DivPivoting(t1); } // Stupid check for convergence, and for an out of range result (as // we would get, for example, if the line is too short to fit the // rounding arc). if(fabs(tp[0] - t[0]) > 1e-3 || fabs(tp[1] - t[1]) > 1e-3 || t[0] < 0.01 || t[1] < 0.01 || t[0] > 0.99 || t[1] > 0.99 || isnan(t[0]) || isnan(t[1])) { Error("Couldn't round this corner. Try a smaller radius, or try " "creating the desired geometry by hand with tangency " "constraints."); return; } // Compute the location of the center of the arc Vector center = pc[0].PointAt(t[0]), v0inter = pinter.Minus(center); int a, b; if(vv < 0) { a = 1; b = 2; center = center.Minus(v0inter.Cross(wn).WithMagnitude(r)); } else { a = 2; b = 1; center = center.Plus(v0inter.Cross(wn).WithMagnitude(r)); } SS.UndoRemember(); hRequest harc = AddRequest(Request::ARC_OF_CIRCLE, false); Entity *earc = SK.GetEntity(harc.entity(0)); hEntity hearc = earc->h; SK.GetEntity(earc->point[0])->PointForceTo(center); SK.GetEntity(earc->point[a])->PointForceTo(pc[0].PointAt(t[0])); SK.GetEntity(earc->point[b])->PointForceTo(pc[1].PointAt(t[1])); earc = NULL; pc[0].CreateRequestTrimmedTo(t[0], !SS.tangentArcDeleteOld, hent[0], hearc, (b == 1)); pc[1].CreateRequestTrimmedTo(t[1], !SS.tangentArcDeleteOld, hent[1], hearc, (a == 1)); // Now either make the original entities construction, or delete them // entirely, according to user preference. Request *re; SK.request.ClearTags(); for(re = SK.request.First(); re; re = SK.request.NextAfter(re)) { if(re->h.v == hreq[0].v || re->h.v == hreq[1].v) { if(SS.tangentArcDeleteOld) { re->tag = 1; } else { re->construction = true; } } } if(SS.tangentArcDeleteOld) { DeleteTaggedRequests(); } SS.ScheduleGenerateAll(); } hEntity GraphicsWindow::SplitLine(hEntity he, Vector pinter) { // Save the original endpoints, since we're about to delete this entity. Entity *e01 = SK.GetEntity(he); hEntity hep0 = e01->point[0], hep1 = e01->point[1]; Vector p0 = SK.GetEntity(hep0)->PointGetNum(), p1 = SK.GetEntity(hep1)->PointGetNum(); // 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 = SK.GetEntity(r0i.entity(0)), *ei1 = SK.GetEntity(ri1.entity(0)); SK.GetEntity(e0i->point[0])->PointForceTo(p0); SK.GetEntity(e0i->point[1])->PointForceTo(pinter); SK.GetEntity(ei1->point[0])->PointForceTo(pinter); SK.GetEntity(ei1->point[1])->PointForceTo(p1); ReplacePointInConstraints(hep0, e0i->point[0]); ReplacePointInConstraints(hep1, ei1->point[1]); Constraint::ConstrainCoincident(e0i->point[1], ei1->point[0]); return e0i->point[1]; } hEntity GraphicsWindow::SplitCircle(hEntity he, Vector pinter) { Entity *circle = SK.GetEntity(he); if(circle->type == Entity::CIRCLE) { // Start with an unbroken circle, split it into a 360 degree arc. Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); circle = NULL; // shortly invalid! hRequest hr = AddRequest(Request::ARC_OF_CIRCLE, false); Entity *arc = SK.GetEntity(hr.entity(0)); SK.GetEntity(arc->point[0])->PointForceTo(center); SK.GetEntity(arc->point[1])->PointForceTo(pinter); SK.GetEntity(arc->point[2])->PointForceTo(pinter); Constraint::ConstrainCoincident(arc->point[1], arc->point[2]); return arc->point[1]; } else { // Start with an arc, break it in to two arcs hEntity hc = circle->point[0], hs = circle->point[1], hf = circle->point[2]; Vector center = SK.GetEntity(hc)->PointGetNum(), start = SK.GetEntity(hs)->PointGetNum(), finish = SK.GetEntity(hf)->PointGetNum(); circle = NULL; // shortly invalid! hRequest hr0 = AddRequest(Request::ARC_OF_CIRCLE, false), hr1 = AddRequest(Request::ARC_OF_CIRCLE, false); Entity *arc0 = SK.GetEntity(hr0.entity(0)), *arc1 = SK.GetEntity(hr1.entity(0)); SK.GetEntity(arc0->point[0])->PointForceTo(center); SK.GetEntity(arc0->point[1])->PointForceTo(start); SK.GetEntity(arc0->point[2])->PointForceTo(pinter); SK.GetEntity(arc1->point[0])->PointForceTo(center); SK.GetEntity(arc1->point[1])->PointForceTo(pinter); SK.GetEntity(arc1->point[2])->PointForceTo(finish); ReplacePointInConstraints(hs, arc0->point[1]); ReplacePointInConstraints(hf, arc1->point[2]); Constraint::ConstrainCoincident(arc0->point[2], arc1->point[1]); return arc0->point[2]; } } hEntity GraphicsWindow::SplitCubic(hEntity he, Vector pinter) { // Save the original endpoints, since we're about to delete this entity. Entity *e01 = SK.GetEntity(he); SBezierList sbl = {}; e01->GenerateBezierCurves(&sbl); hEntity hep0 = e01->point[0], hep1 = e01->point[3+e01->extraPoints], hep0n = Entity::NO_ENTITY, // the new start point hep1n = Entity::NO_ENTITY, // the new finish point hepin = Entity::NO_ENTITY; // the intersection point // The curve may consist of multiple cubic segments. So find which one // contains the intersection point. double t; int i, j; for(i = 0; i < sbl.l.n; i++) { SBezier *sb = &(sbl.l.elem[i]); if(sb->deg != 3) oops(); sb->ClosestPointTo(pinter, &t, false); if(pinter.Equals(sb->PointAt(t))) { // Split that segment at the intersection. SBezier b0i, bi1, b01 = *sb; b01.SplitAt(t, &b0i, &bi1); // Add the two cubic segments this one gets split into. hRequest r0i = AddRequest(Request::CUBIC, false), ri1 = AddRequest(Request::CUBIC, false); // Don't get entities till after adding, realloc issues Entity *e0i = SK.GetEntity(r0i.entity(0)), *ei1 = SK.GetEntity(ri1.entity(0)); for(j = 0; j <= 3; j++) { SK.GetEntity(e0i->point[j])->PointForceTo(b0i.ctrl[j]); } for(j = 0; j <= 3; j++) { SK.GetEntity(ei1->point[j])->PointForceTo(bi1.ctrl[j]); } Constraint::ConstrainCoincident(e0i->point[3], ei1->point[0]); if(i == 0) hep0n = e0i->point[0]; hep1n = ei1->point[3]; hepin = e0i->point[3]; } else { hRequest r = AddRequest(Request::CUBIC, false); Entity *e = SK.GetEntity(r.entity(0)); for(j = 0; j <= 3; j++) { SK.GetEntity(e->point[j])->PointForceTo(sb->ctrl[j]); } if(i == 0) hep0n = e->point[0]; hep1n = e->point[3]; } } sbl.Clear(); ReplacePointInConstraints(hep0, hep0n); ReplacePointInConstraints(hep1, hep1n); return hepin; } hEntity GraphicsWindow::SplitEntity(hEntity he, Vector pinter) { Entity *e = SK.GetEntity(he); int entityType = e->type; hEntity ret; if(e->IsCircle()) { ret = SplitCircle(he, pinter); } else if(e->type == Entity::LINE_SEGMENT) { ret = SplitLine(he, pinter); } else if(e->type == Entity::CUBIC || e->type == Entity::CUBIC_PERIODIC) { ret = SplitCubic(he, pinter); } else { Error("Couldn't split this entity; lines, circles, or cubics only."); return Entity::NO_ENTITY; } // Finally, delete the request that generated the original entity. int reqType = EntReqTable::GetRequestForEntity(entityType); SK.request.ClearTags(); for(int i = 0; i < SK.request.n; i++) { Request *r = &(SK.request.elem[i]); if(r->group.v != activeGroup.v) continue; if(r->type != reqType) continue; // If the user wants to keep the old entities around, they can just // mark them construction first. if(he.v == r->h.entity(0).v && !r->construction) { r->tag = 1; break; } } DeleteTaggedRequests(); return ret; } void GraphicsWindow::SplitLinesOrCurves(void) { if(!LockedInWorkplane()) { Error("Must be sketching in workplane to split."); return; } GroupSelection(); if(!(gs.n == 2 &&(gs.lineSegments + gs.circlesOrArcs + gs.cubics + gs.periodicCubics) == 2)) { Error("Select two entities that intersect each other (e.g. two lines " "or two circles or a circle and a line)."); return; } hEntity ha = gs.entity[0], hb = gs.entity[1]; Entity *ea = SK.GetEntity(ha), *eb = SK.GetEntity(hb); // Compute the possibly-rational Bezier curves for each of these entities SBezierList sbla, sblb; sbla = {}; sblb = {}; ea->GenerateBezierCurves(&sbla); eb->GenerateBezierCurves(&sblb); // and then compute the points where they intersect, based on those curves. SPointList inters = {}; sbla.AllIntersectionsWith(&sblb, &inters); if(inters.l.n > 0) { Vector pi = Vector::From(0, 0, 0); // If there's multiple points, then take the one closest to the // mouse pointer. double dmin = VERY_POSITIVE; SPoint *sp; for(sp = inters.l.First(); sp; sp = inters.l.NextAfter(sp)) { double d = ProjectPoint(sp->p).DistanceTo(currentMousePosition); if(d < dmin) { dmin = d; pi = sp->p; } } SS.UndoRemember(); hEntity hia = SplitEntity(ha, pi), hib = SplitEntity(hb, pi); // SplitEntity adds the coincident constraints to join the split halves // of each original entity; and then we add the constraint to join // the two entities together at the split point. if(hia.v && hib.v) { Constraint::ConstrainCoincident(hia, hib); } } else { Error("Can't split; no intersection found."); } // All done, clean up and regenerate. inters.Clear(); sbla.Clear(); sblb.Clear(); ClearSelection(); SS.ScheduleGenerateAll(); }