diff --git a/drawconstraint.cpp b/drawconstraint.cpp index 9aac25c..6eab77e 100644 --- a/drawconstraint.cpp +++ b/drawconstraint.cpp @@ -102,6 +102,142 @@ void Constraint::DoProjectedPoint(Vector *r) { *r = p; } +//----------------------------------------------------------------------------- +// There is a rectangular box, aligned to our display axes (projRight, projUp) +// centered at ref. This is where a dimension label will be drawn. We want to +// draw a line from A to B. If that line would intersect the label box, then +// trim the line to leave a gap for it, and return zero. If not, then extend +// the line to almost meet the box, and return either positive or negative, +// depending whether that extension was from A or from B. +//----------------------------------------------------------------------------- +int Constraint::DoLineTrimmedAgainstBox(Vector ref, Vector a, Vector b) { + Vector gu = SS.GW.projUp.WithMagnitude(1), + gr = SS.GW.projRight.WithMagnitude(1); + + double pixels = 1.0 / SS.GW.scale; + char *s = Label(); + double swidth = glxStrWidth(s) + 4*pixels, + sheight = glxStrHeight() + 8*pixels; + + struct { + Vector n; + double d; + } planes[4]; + // reference pos is the center of box occupied by text; build a rectangle + // around that, aligned to axes gr and gu, from four planes will all four + // normals pointing inward + planes[0].n = gu.ScaledBy(-1); planes[0].d = -(gu.Dot(ref) + sheight/2); + planes[1].n = gu; planes[1].d = gu.Dot(ref) - sheight/2; + planes[2].n = gr; planes[2].d = gr.Dot(ref) - swidth/2; + planes[3].n = gr.ScaledBy(-1); planes[3].d = -(gr.Dot(ref) + swidth/2); + + double tmin = VERY_POSITIVE, tmax = VERY_NEGATIVE; + Vector dl = b.Minus(a); + + for(int i = 0; i < 4; i++) { + bool parallel; + Vector p = Vector::AtIntersectionOfPlaneAndLine( + planes[i].n, planes[i].d, + a, b, ¶llel); + if(parallel) continue; + + int j; + for(j = 0; j < 4; j++) { + double d = (planes[j].n).Dot(p) - planes[j].d; + if(d < -LENGTH_EPS) break; + } + if(j < 4) continue; + + double t = (p.Minus(a)).DivPivoting(dl); + tmin = min(t, tmin); + tmax = max(t, tmax); + } + + int within = 0; + + if(tmin > -0.01 && tmin < 1.01 && tmax > -0.01 && tmax < 1.01) { + // Both in range; so there's pieces of the line on both sides of the + // label box. + LineDrawOrGetDistance(a, a.Plus(dl.ScaledBy(tmin))); + LineDrawOrGetDistance(a.Plus(dl.ScaledBy(tmax)), b); + } else if(tmin > -0.01 && tmin < 1.01) { + // Only one intersection in range; so the box is right on top of the + // endpoint + LineDrawOrGetDistance(a, a.Plus(dl.ScaledBy(tmin))); + } else if(tmax > -0.01 && tmax < 1.01) { + // Likewise. + LineDrawOrGetDistance(a.Plus(dl.ScaledBy(tmax)), b); + } else { + // The line does not intersect the label; so the line should get + // extended to just barely meet the label. + if(tmin < 0.01 && tmax < 0.01) { + LineDrawOrGetDistance(a.Plus(dl.ScaledBy(tmax)), b); + within = 1; + } else if(tmin > 0.99 && tmax > 0.99) { + LineDrawOrGetDistance(a, a.Plus(dl.ScaledBy(tmin))); + within = -1; + } else { + // This will happen if the entire line lies within the box. + LineDrawOrGetDistance(a, b); + } + } + // 0 means the label lies within the line, negative means it's outside + // and closer to b, positive means outside and closer to a. + return within; +} + +//----------------------------------------------------------------------------- +// Draw a line with arrows on both ends, and possibly a gap in the middle for +// the dimension. We will use these for most length dimensions. The length +// being dimensioned is from A to B; but those points get extended perpendicular +// to the line AB, until the line between the extensions crosses ref (the +// center of the label). +//----------------------------------------------------------------------------- +void Constraint::DoLineWithArrows(Vector ref, Vector a, Vector b, + bool onlyOneExt) +{ + Vector gn = (SS.GW.projRight.Cross(SS.GW.projUp)).WithMagnitude(1); + double pixels = 1.0 / SS.GW.scale; + + Vector ab = a.Minus(b); + Vector ar = a.Minus(ref); + // Normal to a plane containing the line and the label origin. + Vector n = ab.Cross(ar); + // Within that plane, and normal to the line AB; so that's our extension + // line. + Vector out = ab.Cross(n).WithMagnitude(1); + out = out.ScaledBy(-out.Dot(ar)); + + Vector ae = a.Plus(out), be = b.Plus(out); + + // Extension lines extend 10 pixels beyond where the arrows get + // drawn (which is at the same offset perpendicular from AB as the + // label). + LineDrawOrGetDistance(a, ae.Plus(out.WithMagnitude(10*pixels))); + if(!onlyOneExt) { + LineDrawOrGetDistance(b, be.Plus(out.WithMagnitude(10*pixels))); + } + + int within = DoLineTrimmedAgainstBox(ref, ae, be); + + // Arrow heads are 13 pixels long, with an 18 degree half-angle. + double theta = 18*PI/180; + Vector arrow = (be.Minus(ae)).WithMagnitude(13*pixels); + + if(within != 0) { + arrow = arrow.ScaledBy(-1); + Vector seg = (be.Minus(ae)).WithMagnitude(18*pixels); + if(within < 0) LineDrawOrGetDistance(ae, ae.Minus(seg)); + if(within > 0) LineDrawOrGetDistance(be, be.Plus(seg)); + } + + LineDrawOrGetDistance(ae, ae.Plus(arrow.RotatedAbout(gn, theta))); + LineDrawOrGetDistance(ae, ae.Plus(arrow.RotatedAbout(gn, -theta))); + arrow = arrow.ScaledBy(-1); + LineDrawOrGetDistance(be, be.Plus(arrow.RotatedAbout(gn, theta))); + LineDrawOrGetDistance(be, be.Plus(arrow.RotatedAbout(gn, -theta))); +} + void Constraint::DoEqualLenTicks(Vector a, Vector b, Vector gn) { Vector m = (a.ScaledBy(1.0/3)).Plus(b.ScaledBy(2.0/3)); Vector ab = a.Minus(b); @@ -181,6 +317,9 @@ void Constraint::DoArcForAngle(Vector a0, Vector da, Vector b0, Vector db, prev = p; } + // The elliptical approximation isn't exactly right, but the correct + // calculation (against the bounding box of the text) would be rather + // complex and this looks pretty good. double tl = atan2(rm.Dot(gu), rm.Dot(gr)); double adj = EllipticalInterpolation( glxStrWidth(Label())/2, glxStrHeight()/2, tl); @@ -224,16 +363,7 @@ void Constraint::DrawOrGetDistance(Vector *labelPos) { Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(disp.offset); - Vector ab = ap.Minus(bp); - Vector ar = ap.Minus(ref); - // Normal to a plan containing the line and the label origin. - Vector n = ab.Cross(ar); - Vector out = ab.Cross(n).WithMagnitude(1); - out = out.ScaledBy(-out.Dot(ar)); - - LineDrawOrGetDistance(ap, ap.Plus(out)); - LineDrawOrGetDistance(bp, bp.Plus(out)); - + DoLineWithArrows(ref, ap, bp, false); DoLabel(ref, labelPos, gr, gu); break; } @@ -252,11 +382,14 @@ void Constraint::DrawOrGetDistance(Vector *labelPos) { } double d = (p.Minus(pt)).Dot(n); - Vector closest = pt.Plus(n.WithMagnitude(d)); - LineDrawOrGetDistance(pt, closest); Vector ref = ((closest.Plus(pt)).ScaledBy(0.5)).Plus(disp.offset); + + if(!pt.Equals(closest)) { + DoLineWithArrows(ref, pt, closest, true); + } + DoLabel(ref, labelPos, gr, gu); break; } @@ -266,6 +399,7 @@ void Constraint::DrawOrGetDistance(Vector *labelPos) { Entity *line = SK.GetEntity(entityA); Vector lA = SK.GetEntity(line->point[0])->PointGetNum(); Vector lB = SK.GetEntity(line->point[1])->PointGetNum(); + Vector dl = lB.Minus(lA); if(workplane.v != Entity::FREE_IN_3D.v) { lA = lA.ProjectInto(workplane); @@ -274,12 +408,15 @@ void Constraint::DrawOrGetDistance(Vector *labelPos) { } // Find the closest point on the line - Vector closest = pt.ClosestPointOnLine(lA, (lA.Minus(lB))); + Vector closest = pt.ClosestPointOnLine(lA, dl); - LineDrawOrGetDistance(pt, closest); Vector ref = ((closest.Plus(pt)).ScaledBy(0.5)).Plus(disp.offset); DoLabel(ref, labelPos, gr, gu); + if(!pt.Equals(closest)) { + DoLineWithArrows(ref, pt, closest, true); + } + if(workplane.v != Entity::FREE_IN_3D.v) { // Draw the projection marker from the closest point on the // projected line to the projected point on the real line. @@ -298,17 +435,17 @@ void Constraint::DrawOrGetDistance(Vector *labelPos) { case DIAMETER: { Entity *circle = SK.GetEntity(entityA); Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); + Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum(); + Vector n = q.RotationN().WithMagnitude(1); double r = circle->CircleGetRadiusNum(); - Vector ref = center.Plus(disp.offset); - double theta = atan2(disp.offset.Dot(gu), disp.offset.Dot(gr)); - double adj = EllipticalInterpolation( - glxStrWidth(Label())/2, glxStrHeight()/2, theta); + Vector ref = center.Plus(disp.offset); + // Force the label into the same plane as the circle. + ref = ref.Minus(n.ScaledBy(n.Dot(ref) - n.Dot(center))); Vector mark = ref.Minus(center); mark = mark.WithMagnitude(mark.Magnitude()-r); - LineDrawOrGetDistance(ref.Minus(mark.WithMagnitude(adj)), - ref.Minus(mark)); + DoLineTrimmedAgainstBox(ref, ref, ref.Minus(mark)); DoLabel(ref, labelPos, gr, gu); break; diff --git a/export.cpp b/export.cpp index bd6877c..4d1af4a 100644 --- a/export.cpp +++ b/export.cpp @@ -202,7 +202,7 @@ void SolveSpace::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm, ZERO(&compd); sp.normal = Vector::From(0, 0, -1); sp.FixContourDirections(); - sp.OffsetInto(&compd, SS.exportOffset); + sp.OffsetInto(&compd, SS.exportOffset*s); sp.Clear(); compd.MakeEdgesInto(sel); @@ -382,6 +382,22 @@ void VectorFileWriter::Output(SEdgeList *sel, SBezierList *sbl, SMesh *sm) { } } + // And now we compute the canvas size. + double s = 1.0 / SS.exportScale; + if(SS.exportCanvasSizeAuto) { + // It's based on the calculated bounding box; we grow it along each + // boundary by the specified amount. + ptMin.x -= s*SS.exportMargin.left; + ptMax.x += s*SS.exportMargin.right; + ptMin.y -= s*SS.exportMargin.bottom; + ptMax.y += s*SS.exportMargin.top; + } else { + ptMin.x = -(s*SS.exportCanvas.dx); + ptMin.y = -(s*SS.exportCanvas.dy); + ptMax.x = ptMin.x + (s*SS.exportCanvas.width); + ptMax.y = ptMin.y + (s*SS.exportCanvas.height); + } + StartFile(); if(sm && SS.exportShadedTriangles) { for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { diff --git a/sketch.h b/sketch.h index 5139abb..8ff08a7 100644 --- a/sketch.h +++ b/sketch.h @@ -557,6 +557,8 @@ public: char *Label(void); void DoArcForAngle(Vector a0, Vector da, Vector b0, Vector db, Vector offset, Vector *ref); + void DoLineWithArrows(Vector ref, Vector a, Vector b, bool onlyOneExt); + int DoLineTrimmedAgainstBox(Vector ref, Vector a, Vector b); void DoLabel(Vector ref, Vector *labelPos, Vector gr, Vector gu); void DoProjectedPoint(Vector *p); void DoEqualLenTicks(Vector a, Vector b, Vector gn); diff --git a/solvespace.cpp b/solvespace.cpp index 4ed1188..354e886 100644 --- a/solvespace.cpp +++ b/solvespace.cpp @@ -76,6 +76,18 @@ void SolveSpace::Init(char *cmdLine) { exportShadedTriangles = CnfThawDWORD(1, "ExportShadedTriangles"); // Export pwl curves (instead of exact) always exportPwlCurves = CnfThawDWORD(0, "ExportPwlCurves"); + // Whether export canvas size is fixed or derived from bbox + exportCanvasSizeAuto = CnfThawDWORD(1, "ExportCanvasSizeAuto"); + // Margins for automatic canvas size + exportMargin.left = CnfThawFloat(5.0f, "ExportMargin_Left"); + exportMargin.right = CnfThawFloat(5.0f, "ExportMargin_Right"); + exportMargin.bottom = CnfThawFloat(5.0f, "ExportMargin_Bottom"); + exportMargin.top = CnfThawFloat(5.0f, "ExportMargin_Top"); + // Dimensions for fixed canvas size + exportCanvas.width = CnfThawFloat(100.0f, "ExportCanvas_Width"); + exportCanvas.height = CnfThawFloat(100.0f, "ExportCanvas_Height"); + exportCanvas.dx = CnfThawFloat( 5.0f, "ExportCanvas_Dx"); + exportCanvas.dy = CnfThawFloat( 5.0f, "ExportCanvas_Dy"); // Show toolbar in the graphics window showToolbar = CnfThawDWORD(1, "ShowToolbar"); // Recent files menus @@ -144,6 +156,18 @@ void SolveSpace::Exit(void) { CnfFreezeDWORD(exportShadedTriangles, "ExportShadedTriangles"); // Export pwl curves (instead of exact) always CnfFreezeDWORD(exportPwlCurves, "ExportPwlCurves"); + // Whether export canvas size is fixed or derived from bbox + CnfFreezeDWORD(exportCanvasSizeAuto, "ExportCanvasSizeAuto"); + // Margins for automatic canvas size + CnfFreezeFloat(exportMargin.left, "ExportMargin_Left"); + CnfFreezeFloat(exportMargin.right, "ExportMargin_Right"); + CnfFreezeFloat(exportMargin.bottom, "ExportMargin_Bottom"); + CnfFreezeFloat(exportMargin.top, "ExportMargin_Top"); + // Dimensions for fixed canvas size + CnfFreezeFloat(exportCanvas.width, "ExportCanvas_Width"); + CnfFreezeFloat(exportCanvas.height, "ExportCanvas_Height"); + CnfFreezeFloat(exportCanvas.dx, "ExportCanvas_Dx"); + CnfFreezeFloat(exportCanvas.dy, "ExportCanvas_Dy"); // Show toolbar in the graphics window CnfFreezeDWORD(showToolbar, "ShowToolbar"); diff --git a/solvespace.h b/solvespace.h index 18dcc7f..2e6ebe0 100644 --- a/solvespace.h +++ b/solvespace.h @@ -529,6 +529,19 @@ public: int showToolbar; int exportShadedTriangles; int exportPwlCurves; + int exportCanvasSizeAuto; + struct { + float left; + float right; + float bottom; + float top; + } exportMargin; + struct { + float width; + float height; + float dx; + float dy; + } exportCanvas; int CircleSides(double r); typedef enum { diff --git a/textscreens.cpp b/textscreens.cpp index a46bfe3..02b5e50 100644 --- a/textscreens.cpp +++ b/textscreens.cpp @@ -640,10 +640,7 @@ void TextWindow::ScreenChangeExportScale(int link, DWORD v) { SS.TW.edit.meaning = EDIT_EXPORT_SCALE; } void TextWindow::ScreenChangeExportOffset(int link, DWORD v) { - char str[1024]; - sprintf(str, "%.2f", (double)SS.exportOffset); - - ShowTextEditControl(63, 3, str); + ShowTextEditControl(63, 3, SS.MmToString(SS.exportOffset)); SS.TW.edit.meaning = EDIT_EXPORT_OFFSET; } void TextWindow::ScreenChangeBackFaces(int link, DWORD v) { @@ -658,6 +655,37 @@ void TextWindow::ScreenChangePwlCurves(int link, DWORD v) { SS.exportPwlCurves = !SS.exportPwlCurves; InvalidateGraphics(); } +void TextWindow::ScreenChangeCanvasSizeAuto(int link, DWORD v) { + SS.exportCanvasSizeAuto = !SS.exportCanvasSizeAuto; + InvalidateGraphics(); +} +void TextWindow::ScreenChangeCanvasSize(int link, DWORD v) { + double d; + switch(v) { + case 0: d = SS.exportMargin.left; break; + case 1: d = SS.exportMargin.right; break; + case 2: d = SS.exportMargin.bottom; break; + case 3: d = SS.exportMargin.top; break; + + case 10: d = SS.exportCanvas.width; break; + case 11: d = SS.exportCanvas.height; break; + case 12: d = SS.exportCanvas.dx; break; + case 13: d = SS.exportCanvas.dy; break; + + default: return; + } + int row = 77, col; + if(v < 10) { + row += v*2; + col = 11; + } else { + row += (v - 10)*2; + col = 13; + } + ShowTextEditControl(row, col, SS.MmToString(d)); + SS.TW.edit.meaning = EDIT_CANVAS_SIZE; + SS.TW.edit.i = v; +} void TextWindow::ShowConfiguration(void) { int i; Printf(true, "%Ft material color-(r, g, b)"); @@ -711,9 +739,9 @@ void TextWindow::ShowConfiguration(void) { Printf(false, "%Ba %3 %Fl%Ll%f%D[change]%E", (double)SS.exportScale, &ScreenChangeExportScale, 0); - Printf(false, "%Ft cutter radius offset (in export units) "); - Printf(false, "%Ba %2 %Fl%Ll%f%D[change]%E", - (double)SS.exportOffset, + Printf(false, "%Ft cutter radius offset (0=no offset) "); + Printf(false, "%Ba %s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportOffset), &ScreenChangeExportOffset, 0); Printf(false, ""); @@ -726,8 +754,8 @@ void TextWindow::ShowConfiguration(void) { (!SS.exportShadedTriangles ? "" : "no"), (!SS.exportShadedTriangles ? "no" : "")); if(fabs(SS.exportOffset) > LENGTH_EPS) { - Printf(false, "%Ft curves as piecewise linear:%E %Fsyes"); - Printf(false, " (always pwl if cutter radius offset isn't 0)"); + Printf(false, "%Ft curves as piecewise linear:%E %Fsyes%Ft " + "(since cutter radius is not zero)"); } else { Printf(false, "%Ft curves as piecewise linear: " "%Fh%f%Ll%s%E%Fs%s%E / %Fh%f%Ll%s%E%Fs%s%E", @@ -739,6 +767,37 @@ void TextWindow::ShowConfiguration(void) { (!SS.exportPwlCurves ? "no" : "")); } + Printf(false, ""); + Printf(false, "%Ft export canvas size: " + "%Fh%f%Ll%s%E%Fs%s%E / %Fh%f%Ll%s%E%Fs%s%E", + &ScreenChangeCanvasSizeAuto, + (!SS.exportCanvasSizeAuto ? "" : "fixed"), + (!SS.exportCanvasSizeAuto ? "fixed" : ""), + &ScreenChangeCanvasSizeAuto, + (SS.exportCanvasSizeAuto ? "" : "auto"), + (SS.exportCanvasSizeAuto ? "auto" : "")); + if(SS.exportCanvasSizeAuto) { + Printf(false, "%Ft (by margins around exported geometry)"); + Printf(false, "%Ba%Ft left: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.left), &ScreenChangeCanvasSize, 0); + Printf(false, "%Bd%Ft right: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.right), &ScreenChangeCanvasSize, 1); + Printf(false, "%Ba%Ft bottom: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.bottom), &ScreenChangeCanvasSize, 2); + Printf(false, "%Bd%Ft top: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.top), &ScreenChangeCanvasSize, 3); + } else { + Printf(false, "%Ft (by absolute dimensions and offsets)"); + Printf(false, "%Ba%Ft width: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.width), &ScreenChangeCanvasSize, 10); + Printf(false, "%Bd%Ft height: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.height), &ScreenChangeCanvasSize, 11); + Printf(false, "%Ba%Ft offset x: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.dx), &ScreenChangeCanvasSize, 12); + Printf(false, "%Bd%Ft offset y: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.dy), &ScreenChangeCanvasSize, 13); + } + Printf(false, ""); Printf(false, "%Ft draw back faces: " "%Fh%f%Ll%s%E%Fs%s%E / %Fh%f%Ll%s%E%Fs%s%E", @@ -965,7 +1024,7 @@ void TextWindow::EditControlDone(char *s) { case EDIT_EXPORT_OFFSET: { Expr *e = Expr::From(s); if(e) { - double ev = e->Eval(); + double ev = SS.ExprToMm(e); if(isnan(ev) || ev < 0) { Error("Cutter radius offset must not be negative!"); } else { @@ -1025,6 +1084,27 @@ void TextWindow::EditControlDone(char *s) { case EDIT_STEP_DIM_STEPS: shown.dimSteps = min(300, max(1, atoi(s))); break; + + case EDIT_CANVAS_SIZE: { + Expr *e = Expr::From(s); + if(!e) { + Error("Not a valid number or expression: '%s'", s); + break; + } + float d = (float)SS.ExprToMm(e); + switch(edit.i) { + case 0: SS.exportMargin.left = d; break; + case 1: SS.exportMargin.right = d; break; + case 2: SS.exportMargin.bottom = d; break; + case 3: SS.exportMargin.top = d; break; + + case 10: SS.exportCanvas.width = d; break; + case 11: SS.exportCanvas.height = d; break; + case 12: SS.exportCanvas.dx = d; break; + case 13: SS.exportCanvas.dy = d; break; + } + break; + } } InvalidateGraphics(); SS.later.showTW = true; diff --git a/ui.h b/ui.h index 61394cd..ab6bf5b 100644 --- a/ui.h +++ b/ui.h @@ -80,6 +80,7 @@ public: static const int EDIT_EDGE_COLOR = 16; static const int EDIT_EXPORT_SCALE = 17; static const int EDIT_EXPORT_OFFSET = 18; + static const int EDIT_CANVAS_SIZE = 19; // For the helical sweep static const int EDIT_HELIX_TURNS = 20; static const int EDIT_HELIX_PITCH = 21; @@ -147,6 +148,8 @@ public: static void ScreenChangeBackFaces(int link, DWORD v); static void ScreenChangePwlCurves(int link, DWORD v); + static void ScreenChangeCanvasSizeAuto(int link, DWORD v); + static void ScreenChangeCanvasSize(int link, DWORD v); static void ScreenChangeShadedTriangles(int link, DWORD v); static void ScreenStepDimSteps(int link, DWORD v); diff --git a/wishlist.txt b/wishlist.txt index 029b5d1..8d564aa 100644 --- a/wishlist.txt +++ b/wishlist.txt @@ -1,11 +1,10 @@ grid line styles (color, thickness) -margins in exported vector art background color setting better text -better drawing of dimensions faster triangulation +interpolating splines ----- copy and paste