//----------------------------------------------------------------------------- // Draw a representation of an entity on-screen, in the case of curves up // to our chord tolerance, or return the distance from the user's mouse pointer // to the entity for selection. // // Copyright 2008-2013 Jonathan Westhues. //----------------------------------------------------------------------------- #include "solvespace.h" std::string Entity::DescriptionString() const { if(h.isFromRequest()) { Request *r = SK.GetRequest(h.request()); return r->DescriptionString(); } else { Group *g = SK.GetGroup(h.group()); return g->DescriptionString(); } } void Entity::GenerateEdges(SEdgeList *el) { SBezierList *sbl = GetOrGenerateBezierCurves(); int i, j; for(i = 0; i < sbl->l.n; i++) { SBezier *sb = &(sbl->l.elem[i]); List lv = {}; sb->MakePwlInto(&lv); for(j = 1; j < lv.n; j++) { el->AddEdge(lv.elem[j-1], lv.elem[j], style.v, i); } lv.Clear(); } } SBezierList *Entity::GetOrGenerateBezierCurves() { if(beziers.l.n == 0) GenerateBezierCurves(&beziers); return &beziers; } SEdgeList *Entity::GetOrGenerateEdges() { if(edges.l.n != 0) { if(EXACT(edgesChordTol == SS.ChordTolMm())) return &edges; edges.l.Clear(); } if(edges.l.n == 0) GenerateEdges(&edges); edgesChordTol = SS.ChordTolMm(); return &edges; } BBox Entity::GetOrGenerateScreenBBox(bool *hasBBox) { SBezierList *sbl = GetOrGenerateBezierCurves(); // We don't bother with bounding boxes for workplanes, etc. *hasBBox = (IsPoint() || IsNormal() || sbl->l.n > 0); if(!*hasBBox) return {}; if(screenBBoxValid) return screenBBox; if(IsPoint()) { Vector proj = SS.GW.ProjectPoint3(PointGetNum()); screenBBox = BBox::From(proj, proj); } else if(IsNormal()) { Vector proj = SK.GetEntity(point[0])->PointGetNum(); screenBBox = BBox::From(proj, proj); } else if(sbl->l.n > 0) { Vector first = SS.GW.ProjectPoint3(sbl->l.elem[0].ctrl[0]); screenBBox = BBox::From(first, first); for(int i = 0; i < sbl->l.n; i++) { SBezier *sb = &sbl->l.elem[i]; for(int i = 0; i <= sb->deg; i++) { screenBBox.Include(SS.GW.ProjectPoint3(sb->ctrl[i])); } } } else ssassert(false, "Expected entity to be a point or have beziers"); screenBBoxValid = true; return screenBBox; } void Entity::GetReferencePoints(std::vector *refs) { switch(type) { case Type::POINT_N_COPY: case Type::POINT_N_TRANS: case Type::POINT_N_ROT_TRANS: case Type::POINT_N_ROT_AA: case Type::POINT_IN_3D: case Type::POINT_IN_2D: refs->push_back(PointGetNum()); break; case Type::NORMAL_N_COPY: case Type::NORMAL_N_ROT: case Type::NORMAL_N_ROT_AA: case Type::NORMAL_IN_3D: case Type::NORMAL_IN_2D: case Type::WORKPLANE: case Type::CIRCLE: case Type::ARC_OF_CIRCLE: case Type::CUBIC: case Type::CUBIC_PERIODIC: case Type::TTF_TEXT: refs->push_back(SK.GetEntity(point[0])->PointGetNum()); break; case Type::LINE_SEGMENT: { Vector a = SK.GetEntity(point[0])->PointGetNum(), b = SK.GetEntity(point[1])->PointGetNum(); refs->push_back(b.Plus(a.Minus(b).ScaledBy(0.5))); break; } case Type::DISTANCE: case Type::DISTANCE_N_COPY: case Type::FACE_NORMAL_PT: case Type::FACE_XPROD: case Type::FACE_N_ROT_TRANS: case Type::FACE_N_TRANS: case Type::FACE_N_ROT_AA: break; } } int Entity::GetPositionOfPoint(const Camera &camera, Point2d p) { int position; ObjectPicker canvas = {}; canvas.camera = camera; canvas.point = p; canvas.minDistance = 1e12; Draw(DrawAs::DEFAULT, &canvas); position = canvas.position; canvas.Clear(); return position; } bool Entity::IsStylable() const { if(IsPoint()) return false; if(IsWorkplane()) return false; if(IsNormal()) return false; return true; } bool Entity::IsVisible() const { Group *g = SK.GetGroup(group); if(g->h.v == Group::HGROUP_REFERENCES.v && IsNormal()) { // The reference normals are always shown return true; } if(!(g->IsVisible())) return false; if(IsPoint() && !SS.GW.showPoints) return false; if(IsNormal() && !SS.GW.showNormals) return false; if(!SS.GW.showWorkplanes) { if(IsWorkplane() && !h.isFromRequest()) { if(g->h.v != SS.GW.activeGroup.v) { // The group-associated workplanes are hidden outside // their group. return false; } } } if(style.v) { Style *s = Style::Get(style); if(!s->visible) return false; } if(forceHidden) return false; return true; } void Entity::CalculateNumerical(bool forExport) { if(IsPoint()) actPoint = PointGetNum(); if(IsNormal()) actNormal = NormalGetNum(); if(type == Type::DISTANCE || type == Type::DISTANCE_N_COPY) { actDistance = DistanceGetNum(); } if(IsFace()) { actPoint = FaceGetPointNum(); Vector n = FaceGetNormalNum(); actNormal = Quaternion::From(0, n.x, n.y, n.z); } if(forExport) { // Visibility in copied linked entities follows source file actVisible = IsVisible(); } else { // Copied entities within a file are always visible actVisible = true; } } //----------------------------------------------------------------------------- // Compute a cubic, second derivative continuous, interpolating spline. Same // routine for periodic splines (in a loop) or open splines (with specified // end tangents). //----------------------------------------------------------------------------- void Entity::ComputeInterpolatingSpline(SBezierList *sbl, bool periodic) const { static const int MAX_N = BandedMatrix::MAX_UNKNOWNS; int ep = extraPoints; // The number of unknowns to solve for. int n = periodic ? 3 + ep : ep; ssassert(n < MAX_N, "Too many unknowns"); // The number of on-curve points, one more than the number of segments. int pts = periodic ? 4 + ep : 2 + ep; int i, j, a; // The starting and finishing control points that define our end tangents // (if the spline isn't periodic), and the on-curve points. Vector ctrl_s = Vector::From(0, 0, 0); Vector ctrl_f = Vector::From(0, 0, 0); Vector pt[MAX_N+4]; if(periodic) { for(i = 0; i < ep + 3; i++) { pt[i] = SK.GetEntity(point[i])->PointGetNum(); } pt[i++] = SK.GetEntity(point[0])->PointGetNum(); } else { ctrl_s = SK.GetEntity(point[1])->PointGetNum(); ctrl_f = SK.GetEntity(point[ep+2])->PointGetNum(); j = 0; pt[j++] = SK.GetEntity(point[0])->PointGetNum(); for(i = 2; i <= ep + 1; i++) { pt[j++] = SK.GetEntity(point[i])->PointGetNum(); } pt[j++] = SK.GetEntity(point[ep+3])->PointGetNum(); } // The unknowns that we will be solving for, a set for each coordinate. double Xx[MAX_N], Xy[MAX_N], Xz[MAX_N]; // For a cubic Bezier section f(t) as t goes from 0 to 1, // f' (0) = 3*(P1 - P0) // f' (1) = 3*(P3 - P2) // f''(0) = 6*(P0 - 2*P1 + P2) // f''(1) = 6*(P3 - 2*P2 + P1) for(a = 0; a < 3; a++) { BandedMatrix bm = {}; bm.n = n; for(i = 0; i < n; i++) { int im, it, ip; if(periodic) { im = WRAP(i - 1, n); it = i; ip = WRAP(i + 1, n); } else { im = i; it = i + 1; ip = i + 2; } // All of these are expressed in terms of a constant part, and // of X[i-1], X[i], and X[i+1]; so let these be the four // components of that vector; Vector4 A, B, C, D, E; // The on-curve interpolated point C = Vector4::From((pt[it]).Element(a), 0, 0, 0); // control point one back, C - X[i] B = C.Plus(Vector4::From(0, 0, -1, 0)); // control point one forward, C + X[i] D = C.Plus(Vector4::From(0, 0, 1, 0)); // control point two back if(i == 0 && !periodic) { A = Vector4::From(ctrl_s.Element(a), 0, 0, 0); } else { // pt[im] + X[i-1] A = Vector4::From(pt[im].Element(a), 1, 0, 0); } // control point two forward if(i == (n - 1) && !periodic) { E = Vector4::From(ctrl_f.Element(a), 0, 0, 0); } else { // pt[ip] - X[i+1] E = Vector4::From((pt[ip]).Element(a), 0, 0, -1); } // Write the second derivatives of each segment, dropping constant Vector4 fprev_pp = (C.Minus(B.ScaledBy(2))).Plus(A), fnext_pp = (C.Minus(D.ScaledBy(2))).Plus(E), eq = fprev_pp.Minus(fnext_pp); bm.B[i] = -eq.w; if(periodic) { bm.A[i][WRAP(i-2, n)] = eq.x; bm.A[i][WRAP(i-1, n)] = eq.y; bm.A[i][i] = eq.z; } else { // The wrapping would work, except when n = 1 and everything // wraps to zero... if(i > 0) bm.A[i][i - 1] = eq.x; bm.A[i][i] = eq.y; if(i < (n-1)) bm.A[i][i + 1] = eq.z; } } bm.Solve(); double *X = (a == 0) ? Xx : (a == 1) ? Xy : Xz; memcpy(X, bm.X, n*sizeof(double)); } for(i = 0; i < pts - 1; i++) { Vector p0, p1, p2, p3; if(periodic) { p0 = pt[i]; int iw = WRAP(i - 1, n); p1 = p0.Plus(Vector::From(Xx[iw], Xy[iw], Xz[iw])); } else if(i == 0) { p0 = pt[0]; p1 = ctrl_s; } else { p0 = pt[i]; p1 = p0.Plus(Vector::From(Xx[i-1], Xy[i-1], Xz[i-1])); } if(periodic) { p3 = pt[i+1]; int iw = WRAP(i, n); p2 = p3.Minus(Vector::From(Xx[iw], Xy[iw], Xz[iw])); } else if(i == (pts - 2)) { p3 = pt[pts-1]; p2 = ctrl_f; } else { p3 = pt[i+1]; p2 = p3.Minus(Vector::From(Xx[i], Xy[i], Xz[i])); } SBezier sb = SBezier::From(p0, p1, p2, p3); sbl->l.Add(&sb); } } void Entity::GenerateBezierCurves(SBezierList *sbl) const { SBezier sb; int i = sbl->l.n; switch(type) { case Type::LINE_SEGMENT: { Vector a = SK.GetEntity(point[0])->PointGetNum(); Vector b = SK.GetEntity(point[1])->PointGetNum(); sb = SBezier::From(a, b); sb.entity = h.v; sbl->l.Add(&sb); break; } case Type::CUBIC: ComputeInterpolatingSpline(sbl, /*periodic=*/false); break; case Type::CUBIC_PERIODIC: ComputeInterpolatingSpline(sbl, /*periodic=*/true); break; case Type::CIRCLE: case Type::ARC_OF_CIRCLE: { Vector center = SK.GetEntity(point[0])->PointGetNum(); Quaternion q = SK.GetEntity(normal)->NormalGetNum(); Vector u = q.RotationU(), v = q.RotationV(); double r = CircleGetRadiusNum(); double thetaa, thetab, dtheta; if(r < LENGTH_EPS) { // If a circle or an arc gets dragged through zero radius, // then we just don't generate anything. break; } if(type == Type::CIRCLE) { thetaa = 0; thetab = 2*PI; dtheta = 2*PI; } else { ArcGetAngles(&thetaa, &thetab, &dtheta); } int i, n; if(dtheta > (3*PI/2 + 0.01)) { n = 4; } else if(dtheta > (PI + 0.01)) { n = 3; } else if(dtheta > (PI/2 + 0.01)) { n = 2; } else { n = 1; } dtheta /= n; for(i = 0; i < n; i++) { double s, c; c = cos(thetaa); s = sin(thetaa); // The start point of the curve, and the tangent vector at // that start point. Vector p0 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), t0 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); thetaa += dtheta; c = cos(thetaa); s = sin(thetaa); Vector p2 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), t2 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); // The control point must lie on both tangents. Vector p1 = Vector::AtIntersectionOfLines(p0, p0.Plus(t0), p2, p2.Plus(t2), NULL); SBezier sb = SBezier::From(p0, p1, p2); sb.weight[1] = cos(dtheta/2); sbl->l.Add(&sb); } break; } case Type::TTF_TEXT: { Vector topLeft = SK.GetEntity(point[0])->PointGetNum(); Vector botLeft = SK.GetEntity(point[1])->PointGetNum(); Vector n = Normal()->NormalN(); Vector v = topLeft.Minus(botLeft); Vector u = (v.Cross(n)).WithMagnitude(v.Magnitude()); SS.fonts.PlotString(font, str, sbl, botLeft, u, v); break; } default: // Not a problem, points and normals and such don't generate curves break; } // Record our style for all of the Beziers that we just created. for(; i < sbl->l.n; i++) { sbl->l.elem[i].auxA = style.v; } } void Entity::Draw(DrawAs how, Canvas *canvas) { if(!IsVisible()) return; int zIndex; if(how == DrawAs::HIDDEN) { zIndex = 2; } else if(group.v != SS.GW.activeGroup.v) { zIndex = 3; } else { zIndex = 4; } hStyle hs; if(IsPoint()) { hs.v = Style::DATUM; } else if(IsNormal() || type == Type::WORKPLANE) { hs.v = Style::NORMALS; } else { hs = Style::ForEntity(h); } Canvas::Stroke stroke = {}; switch(how) { case DrawAs::DEFAULT: stroke = Style::Stroke(hs); stroke.layer = Canvas::Layer::NORMAL; break; case DrawAs::OVERLAY: stroke.layer = Canvas::Layer::FRONT; break; case DrawAs::HIDDEN: stroke = Style::Stroke(Style::HIDDEN_EDGE); stroke.layer = Canvas::Layer::OCCLUDED; break; case DrawAs::HOVERED: stroke = Style::Stroke(hs); stroke.layer = Canvas::Layer::FRONT; stroke.color = Style::Color(Style::HOVERED); break; case DrawAs::SELECTED: stroke = Style::Stroke(hs); stroke.layer = Canvas::Layer::FRONT; stroke.color = Style::Color(Style::SELECTED); break; } stroke.zIndex = zIndex; Canvas::hStroke hcs = canvas->GetStroke(stroke); Canvas::Stroke pointStroke = {}; pointStroke.layer = stroke.layer; pointStroke.zIndex = IsPoint() ? zIndex + 1 : 0; pointStroke.color = stroke.color; pointStroke.width = 7.0; pointStroke.unit = Canvas::Unit::PX; Canvas::hStroke hcsPoint = canvas->GetStroke(pointStroke); switch(type) { case Type::POINT_N_COPY: case Type::POINT_N_TRANS: case Type::POINT_N_ROT_TRANS: case Type::POINT_N_ROT_AA: case Type::POINT_IN_3D: case Type::POINT_IN_2D: { if(how == DrawAs::HIDDEN) return; // If we're analyzing the sketch to show the degrees of freedom, // then we draw big colored squares over the points that are // free to move. bool free = false; if(type == Type::POINT_IN_3D) { Param *px = SK.GetParam(param[0]), *py = SK.GetParam(param[1]), *pz = SK.GetParam(param[2]); free = px->free || py->free || pz->free; } else if(type == Type::POINT_IN_2D) { Param *pu = SK.GetParam(param[0]), *pv = SK.GetParam(param[1]); free = pu->free || pv->free; } if(free) { Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE); analyzeStroke.width = 14.0; Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke); canvas->DrawPoint(PointGetNum(), hcsAnalyze); } canvas->DrawPoint(PointGetNum(), hcsPoint); return; } case Type::NORMAL_N_COPY: case Type::NORMAL_N_ROT: case Type::NORMAL_N_ROT_AA: case Type::NORMAL_IN_3D: case Type::NORMAL_IN_2D: { const Camera &camera = canvas->GetCamera(); if(how == DrawAs::HIDDEN) return; for(int i = 0; i < 2; i++) { bool asReference = (i == 1); if(asReference) { if(!h.request().IsFromReferences()) continue; } else { if(!SK.GetGroup(group)->IsVisible() || !SS.GW.showNormals) continue; } stroke.layer = (asReference) ? Canvas::Layer::FRONT : Canvas::Layer::NORMAL; if(how != DrawAs::HOVERED && how != DrawAs::SELECTED) { // Always draw the x, y, and z axes in red, green, and blue; // brighter for the ones at the bottom left of the screen, // dimmer for the ones at the model origin. hRequest hr = h.request(); uint8_t luma = (asReference) ? 255 : 100; if(hr.v == Request::HREQUEST_REFERENCE_XY.v) { stroke.color = RgbaColor::From(0, 0, luma); } else if(hr.v == Request::HREQUEST_REFERENCE_YZ.v) { stroke.color = RgbaColor::From(luma, 0, 0); } else if(hr.v == Request::HREQUEST_REFERENCE_ZX.v) { stroke.color = RgbaColor::From(0, luma, 0); } } Quaternion q = NormalGetNum(); Vector tail; if(asReference) { // Draw an extra copy of the x, y, and z axes, that's // always in the corner of the view and at the front. // So those are always available, perhaps useful. stroke.width = 2; double s = camera.scale; double h = 60 - camera.height / 2.0; double w = 60 - camera.width / 2.0; tail = camera.projRight.ScaledBy(w/s).Plus( camera.projUp. ScaledBy(h/s)).Minus(camera.offset); } else { tail = SK.GetEntity(point[0])->PointGetNum(); } tail = camera.AlignToPixelGrid(tail); hcs = canvas->GetStroke(stroke); Vector v = (q.RotationN()).WithMagnitude(50.0 / camera.scale); Vector tip = tail.Plus(v); canvas->DrawLine(tail, tip, hcs); v = v.WithMagnitude(12.0 / camera.scale); Vector axis = q.RotationV(); canvas->DrawLine(tip, tip.Minus(v.RotatedAbout(axis, 0.6)), hcs); canvas->DrawLine(tip, tip.Minus(v.RotatedAbout(axis, -0.6)), hcs); } return; } case Type::DISTANCE: case Type::DISTANCE_N_COPY: // These are used only as data structures, nothing to display. return; case Type::WORKPLANE: { const Camera &camera = canvas->GetCamera(); Vector p = SK.GetEntity(point[0])->PointGetNum(); p = camera.AlignToPixelGrid(p); Vector u = Normal()->NormalU(); Vector v = Normal()->NormalV(); double s = (std::min(camera.width, camera.height)) * 0.45 / camera.scale; Vector us = u.ScaledBy(s); Vector vs = v.ScaledBy(s); Vector pp = p.Plus (us).Plus (vs); Vector pm = p.Plus (us).Minus(vs); Vector mm = p.Minus(us).Minus(vs), mm2 = mm; Vector mp = p.Minus(us).Plus (vs); Canvas::Stroke strokeBorder = stroke; strokeBorder.zIndex -= 3; strokeBorder.stipplePattern = StipplePattern::SHORT_DASH; strokeBorder.stippleScale = 8.0; Canvas::hStroke hcsBorder = canvas->GetStroke(strokeBorder); double textHeight = Style::TextHeight(hs) / camera.scale; if(!h.isFromRequest()) { mm = mm.Plus(v.ScaledBy(textHeight * 4.7)); mm2 = mm2.Plus(u.ScaledBy(textHeight * 4.7)); canvas->DrawLine(mm2, mm, hcsBorder); } canvas->DrawLine(pp, pm, hcsBorder); canvas->DrawLine(mm2, pm, hcsBorder); canvas->DrawLine(mm, mp, hcsBorder); canvas->DrawLine(pp, mp, hcsBorder); Vector o = mm2.Plus(u.ScaledBy(3.0 / camera.scale)).Plus( v.ScaledBy(3.0 / camera.scale)); std::string shortDesc = DescriptionString().substr(5); canvas->DrawVectorText(shortDesc, textHeight, o, u, v, hcs); return; } case Type::LINE_SEGMENT: case Type::CIRCLE: case Type::ARC_OF_CIRCLE: case Type::CUBIC: case Type::CUBIC_PERIODIC: case Type::TTF_TEXT: { // Generate the rational polynomial curves, then piecewise linearize // them, and display those. if(!canvas->DrawBeziers(*GetOrGenerateBezierCurves(), hcs)) { canvas->DrawEdges(*GetOrGenerateEdges(), hcs); } return; } case Type::FACE_NORMAL_PT: case Type::FACE_XPROD: case Type::FACE_N_ROT_TRANS: case Type::FACE_N_TRANS: case Type::FACE_N_ROT_AA: // Do nothing; these are drawn with the triangle mesh return; } ssassert(false, "Unexpected entity type"); }