diff --git a/Makefile b/Makefile index 9fd81b94..05ba0417 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ SSOBJS = $(OBJDIR)\solvespace.obj \ $(OBJDIR)\polygon.obj \ $(OBJDIR)\mesh.obj \ $(OBJDIR)\bsp.obj \ + $(OBJDIR)\ttf.obj \ LIBS = user32.lib gdi32.lib comctl32.lib advapi32.lib opengl32.lib glu32.lib \ diff --git a/draw.cpp b/draw.cpp index 87f684c5..3eec33a4 100644 --- a/draw.cpp +++ b/draw.cpp @@ -324,6 +324,7 @@ bool GraphicsWindow::ConstrainPointByHovered(hEntity pt) { void GraphicsWindow::MouseLeftDown(double mx, double my) { if(GraphicsEditControlIsVisible()) return; + HideTextEditControl(); // Make sure the hover is up to date. MouseMoved(mx, my, false, false, false, false, false); @@ -453,6 +454,26 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) { ClearSuper(); break; + case MNU_TTF_TEXT: { + if(!SS.GW.LockedInWorkplane()) { + Error("Can't draw text in 3d; select a workplane first."); + ClearSuper(); + break; + } + hr = AddRequest(Request::TTF_TEXT); + Request *r = SS.GetRequest(hr); + r->str.strcpy("Abc"); + r->font.strcpy("arial.ttf"); + + SS.GetEntity(hr.entity(1))->PointForceTo(v); + SS.GetEntity(hr.entity(2))->PointForceTo(v); + + pending.operation = DRAGGING_NEW_POINT; + pending.point = hr.entity(2); + pending.description = "click to place bottom left of text"; + break; + } + case DRAGGING_RADIUS: case DRAGGING_NEW_POINT: // The MouseMoved event has already dragged it as desired. @@ -546,6 +567,7 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) { void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { if(GraphicsEditControlIsVisible()) return; + HideTextEditControl(); if(hover.constraint.v) { constraintBeingEdited = hover.constraint; diff --git a/drawentity.cpp b/drawentity.cpp index f39ced92..929c2c78 100644 --- a/drawentity.cpp +++ b/drawentity.cpp @@ -132,7 +132,7 @@ bool Entity::IsVisible(void) { return true; } -void Entity::DrawOrGetDistance(void) { +void Entity::DrawOrGetDistance(void) { // If an entity is invisible, then it doesn't get shown, and it doesn't // contribute a distance for the selection, but it still generates edges. if(!dogd.edges) { @@ -361,6 +361,17 @@ void Entity::DrawOrGetDistance(void) { break; } + case TTF_TEXT: { + Vector topLeft = SS.GetEntity(point[0])->PointGetNum(); + Vector botLeft = SS.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, str.str, 0, h, botLeft, u, v); + break; + } + case FACE_NORMAL_PT: case FACE_XPROD: case FACE_N_ROT_TRANS: diff --git a/file.cpp b/file.cpp index c3106a89..c60e670f 100644 --- a/file.cpp +++ b/file.cpp @@ -96,10 +96,14 @@ const SolveSpace::SaveTable SolveSpace::SAVED[] = { { 'r', "Request.workplane.v", 'x', &(SS.sv.r.workplane.v) }, { 'r', "Request.group.v", 'x', &(SS.sv.r.group.v) }, { 'r', "Request.construction", 'b', &(SS.sv.r.construction) }, + { 'r', "Request.str", 'N', &(SS.sv.r.str) }, + { 'r', "Request.font", 'N', &(SS.sv.r.font) }, { 'e', "Entity.h.v", 'x', &(SS.sv.e.h.v) }, { 'e', "Entity.type", 'd', &(SS.sv.e.type) }, { 'e', "Entity.construction", 'b', &(SS.sv.e.construction) }, + { 'e', "Entity.str", 'N', &(SS.sv.e.str) }, + { 'e', "Entity.font", 'N', &(SS.sv.e.font) }, { 'e', "Entity.point[0].v", 'x', &(SS.sv.e.point[0].v) }, { 'e', "Entity.point[1].v", 'x', &(SS.sv.e.point[1].v) }, { 'e', "Entity.point[2].v", 'x', &(SS.sv.e.point[2].v) }, @@ -149,6 +153,7 @@ void SolveSpace::SaveUsingTable(int type) { if(fmt == 'd' && *((int *)p) == 0) continue; if(fmt == 'x' && *((DWORD *)p) == 0) continue; if(fmt == 'f' && *((double *)p) == 0.0) continue; + if(fmt == 'N' && strlen(((NameStr *)p)->str) == 0) continue; fprintf(fh, "%s=", SAVED[i].desc); switch(fmt) { diff --git a/graphicswin.cpp b/graphicswin.cpp index e84881d0..08e78052 100644 --- a/graphicswin.cpp +++ b/graphicswin.cpp @@ -66,6 +66,8 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, "&Arc of a Circle\tA", MNU_ARC, 'A', mReq }, { 1, "&Cubic Segment\t3", MNU_CUBIC, '3', mReq }, { 1, NULL, 0, NULL }, +{ 1, "&Text in TrueType Font\tT", MNU_TTF_TEXT, 'T', mReq }, +{ 1, NULL, 0, NULL }, { 1, "To&ggle Construction\tG", MNU_CONSTRUCTION, 'G', mReq }, { 0, "&Constrain", 0, NULL }, @@ -423,6 +425,8 @@ void GraphicsWindow::MenuEdit(int id) { break; case MNU_DELETE: { + SS.UndoRemember(); + int i; SS.request.ClearTags(); SS.constraint.ClearTags(); @@ -507,6 +511,7 @@ void GraphicsWindow::MenuRequest(int id) { case MNU_ARC: s = "click point on arc (draws anti-clockwise)"; goto c; case MNU_WORKPLANE: s = "click origin of workplane"; goto c; case MNU_RECTANGLE: s = "click one corner of rectangular"; goto c; + case MNU_TTF_TEXT: s = "click top left of text"; goto c; c: SS.GW.pending.operation = id; SS.GW.pending.description = s; diff --git a/group.cpp b/group.cpp index 02f0ce59..e46de82a 100644 --- a/group.cpp +++ b/group.cpp @@ -70,7 +70,6 @@ void Group::MenuGroup(int id) { Error("Bad selection for new drawing in workplane."); return; } - SS.GW.ClearSelection(); break; case GraphicsWindow::MNU_GROUP_EXTRUDE: @@ -96,7 +95,6 @@ void Group::MenuGroup(int id) { g.type = LATHE; g.opA = SS.GW.activeGroup; g.name.strcpy("lathe"); - SS.GW.ClearSelection(); break; case GraphicsWindow::MNU_GROUP_SWEEP: { @@ -146,7 +144,6 @@ void Group::MenuGroup(int id) { g.valC = 0; // pitch in radius g.opA = SS.GW.activeGroup; g.name.strcpy("helical-sweep"); - SS.GW.ClearSelection(); break; } @@ -168,7 +165,6 @@ void Group::MenuGroup(int id) { g.valA = 3; g.subtype = ONE_SIDED; g.name.strcpy("rotate"); - SS.GW.ClearSelection(); break; } @@ -194,7 +190,9 @@ void Group::MenuGroup(int id) { default: oops(); } + SS.GW.ClearSelection(); SS.UndoRemember(); + SS.group.AddAndAssignId(&g); Group *gg = SS.GetGroup(g.h); @@ -205,6 +203,8 @@ void Group::MenuGroup(int id) { SS.GW.activeGroup = gg->h; SS.GenerateAll(); if(gg->type == DRAWING_WORKPLANE) { + // Can't set the active workplane for this one until after we've + // regenerated, because the workplane doesn't exist until then. gg->activeWorkplane = gg->h.entity(0); } gg->Activate(); @@ -613,6 +613,14 @@ void Group::CopyEntity(IdList *el, en.normal = Remap(ep->normal, remap); break; + case Entity::TTF_TEXT: + en.point[0] = Remap(ep->point[0], remap); + en.point[1] = Remap(ep->point[1], remap); + en.normal = Remap(ep->normal, remap); + en.str.strcpy(ep->str.str); + en.font.strcpy(ep->font.str); + break; + case Entity::POINT_N_COPY: case Entity::POINT_N_TRANS: case Entity::POINT_N_ROT_TRANS: diff --git a/request.cpp b/request.cpp index 132bf7bf..21f16e4b 100644 --- a/request.cpp +++ b/request.cpp @@ -53,6 +53,12 @@ void Request::Generate(IdList *entity, points = 4; break; + case Request::TTF_TEXT: + et = Entity::TTF_TEXT; + points = 2; + hasNormal = true; + break; + default: oops(); } @@ -61,6 +67,8 @@ void Request::Generate(IdList *entity, e.group = group; e.workplane = workplane; e.construction = construction; + e.str.strcpy(str.str); + e.font.strcpy(font.str); e.h = h.entity(0); // And generate entities for the points @@ -147,6 +155,7 @@ char *Request::DescriptionString(void) { case CUBIC: s = "cubic-bezier"; break; case CIRCLE: s = "circle"; break; case ARC_OF_CIRCLE: s = "arc-of-circle"; break; + case TTF_TEXT: s = "ttf-text"; break; default: s = "???"; break; } } diff --git a/sketch.h b/sketch.h index 49e364b0..80e1964e 100644 --- a/sketch.h +++ b/sketch.h @@ -228,6 +228,7 @@ public: static const int CUBIC = 300; static const int CIRCLE = 400; static const int ARC_OF_CIRCLE = 500; + static const int TTF_TEXT = 600; int type; @@ -235,6 +236,8 @@ public: hGroup group; bool construction; + NameStr str; + NameStr font; static hParam AddParam(ParamList *param, hParam hp); void Generate(EntityList *entity, ParamList *param); @@ -278,6 +281,7 @@ public: static const int CUBIC = 12000; static const int CIRCLE = 13000; static const int ARC_OF_CIRCLE = 14000; + static const int TTF_TEXT = 15000; int type; @@ -312,6 +316,8 @@ public: hEntity workplane; // or Entity::FREE_IN_3D bool construction; + NameStr str; + NameStr font; // For entities that are derived by a transformation, the number of // times to apply the transformation. diff --git a/solvespace.h b/solvespace.h index ef2dc365..8fea9c0d 100644 --- a/solvespace.h +++ b/solvespace.h @@ -29,6 +29,7 @@ inline int WRAP(int v, int n) { #define isforname(c) (isalnum(c) || (c) == '_' || (c) == '-' || (c) == '#') typedef signed long SDWORD; +typedef signed short SWORD; #include #include @@ -44,6 +45,7 @@ class ExprVector; class ExprQuaternion; +//================ // From the platform-specific code. #define MAX_RECENT 8 #define RECENT_OPEN (0xf000) @@ -59,6 +61,7 @@ int SaveFileYesNoCancel(void); BOOL GetSaveFile(char *file, char *defExtension, char *selPattern); BOOL GetOpenFile(char *file, char *defExtension, char *selPattern); void GetAbsoluteFilename(char *file); +void LoadAllFontFiles(void); void CheckMenuById(int id, BOOL checked); void EnableMenuById(int id, BOOL checked); @@ -94,7 +97,10 @@ void FreeAllTemporary(void); void *MemRealloc(void *p, int n); void *MemAlloc(int n); void MemFree(void *p); -void vl(void); // debug function to validate +void vl(void); // debug function to validate heaps + +// End of platform-specific functions +//================ #include "dsc.h" @@ -208,6 +214,86 @@ public: void Solve(Group *g); }; +class TtfFont { +public: + typedef struct { + bool onCurve; + bool lastInContour; + SWORD x; + SWORD y; + } FontPoint; + + typedef struct { + FontPoint *pt; + int pts; + + int xMax; + int xMin; + int leftSideBearing; + int advanceWidth; + } Glyph; + + typedef struct { + int x, y; + } IntPoint; + + char fontFile[MAX_PATH]; + NameStr name; + bool loaded; + + // The font itself, plus the mapping from ASCII codes to glyphs + int useGlyph[256]; + Glyph *glyph; + int glyphs; + + int maxPoints; + int scale; + + // The filehandle, while loading + FILE *fh; + // Some state while rendering a character to curves + static const int NOTHING = 0; + static const int ON_CURVE = 1; + static const int OFF_CURVE = 2; + int lastWas; + IntPoint lastOnCurve; + IntPoint lastOffCurve; + + // And the state that the caller must specify, determines where we + // render to and how + hEntity entity; + Vector origin, u, v; + + int Getc(void); + int GetBYTE(void); + int GetWORD(void); + int GetDWORD(void); + + void LoadGlyph(int index); + bool LoadFontFromFile(bool nameOnly); + char *FontFileBaseName(void); + + void Flush(void); + void Handle(int *dx, int x, int y, bool onCurve); + void PlotCharacter(int *dx, int c, double spacing); + void PlotString(char *str, double spacing, + hEntity he, Vector origin, Vector u, Vector v); + + Vector TransformIntPoint(int x, int y); + void LineSegment(int x0, int y0, int x1, int y1); + void Bezier(int x0, int y0, int x1, int y1, int x2, int y2); +}; + +class TtfFontList { +public: + bool loaded; + SList l; + + void LoadAll(void); + + void PlotString(char *font, char *str, double spacing, + hEntity he, Vector origin, Vector u, Vector v); +}; class SolveSpace { public: @@ -337,6 +423,9 @@ public: // The system to be solved. System sys; + // All the TrueType fonts in memory + TtfFontList fonts; + // Everything has been pruned, so we know there's no dangling references // to entities that don't exist. Before that, we mustn't try to display // the sketch! diff --git a/textscreens.cpp b/textscreens.cpp index 2b153e33..5dcfa7c3 100644 --- a/textscreens.cpp +++ b/textscreens.cpp @@ -684,7 +684,18 @@ void TextWindow::EditControlDone(char *s) { SS.later.generateAll = true; break; } + case EDIT_TTF_TEXT: { + SS.UndoRemember(); + Request *r = SS.request.FindByIdNoOops(edit.request); + if(r) { + r->str.strcpy(s); + SS.MarkGroupDirty(r->group); + SS.later.generateAll = true; + } + break; + } } + InvalidateGraphics(); SS.later.showTW = true; HideTextEditControl(); edit.meaning = EDIT_NOTHING; diff --git a/textwin.cpp b/textwin.cpp index acc975f8..c2b1e2b6 100644 --- a/textwin.cpp +++ b/textwin.cpp @@ -206,10 +206,11 @@ void TextWindow::Show(void) { Printf(false, ""); Printf(false, "%s", SS.GW.pending.description); } else if(gs.n > 0) { - HideTextEditControl(); + if(edit.meaning != EDIT_TTF_TEXT) HideTextEditControl(); ShowHeader(false); DescribeSelection(); } else { + if(edit.meaning == EDIT_TTF_TEXT) HideTextEditControl(); ShowHeader(true); switch(shown->screen) { default: @@ -229,6 +230,36 @@ void TextWindow::ScreenUnselectAll(int link, DWORD v) { GraphicsWindow::MenuEdit(GraphicsWindow::MNU_UNSELECT_ALL); } +void TextWindow::ScreenEditTtfText(int link, DWORD v) { + hRequest hr = { v }; + Request *r = SS.GetRequest(hr); + + ShowTextEditControl(13, 10, r->str.str); + SS.TW.edit.meaning = EDIT_TTF_TEXT; + SS.TW.edit.request = hr; +} + +void TextWindow::ScreenSetTtfFont(int link, DWORD v) { + int i = (int)v; + if(i < 0) return; + if(i >= SS.fonts.l.n) return; + + SS.GW.GroupSelection(); + if(gs.entities != 1 || gs.n != 1) return; + + Entity *e = SS.entity.FindByIdNoOops(gs.entity[0]); + if(!e || e->type != Entity::TTF_TEXT || !e->h.isFromRequest()) return; + + Request *r = SS.request.FindByIdNoOops(e->h.request()); + if(!r) return; + + SS.UndoRemember(); + r->font.strcpy(SS.fonts.l.elem[i].FontFileBaseName()); + SS.MarkGroupDirty(r->group); + SS.later.generateAll = true; + SS.later.showTW = true; +} + void TextWindow::DescribeSelection(void) { Entity *e; Vector p; @@ -331,6 +362,35 @@ void TextWindow::DescribeSelection(void) { Printf(false, " thru = " PT_AS_STR, COSTR(p)); break; + case Entity::TTF_TEXT: { + Printf(false, "%FtTRUETYPE FONT TEXT%E"); + Printf(true, " font = '%Fi%s%E'", e->font.str); + if(e->h.isFromRequest()) { + Printf(false, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E", + e->str.str, &ScreenEditTtfText, e->h.request()); + Printf(true, " select new font"); + SS.fonts.LoadAll(); + int i; + for(i = 0; i < SS.fonts.l.n; i++) { + TtfFont *tf = &(SS.fonts.l.elem[i]); + if(strcmp(e->font.str, tf->FontFileBaseName())==0) { + Printf(false, "%Bp %s", + (i & 1) ? 'd' : 'a', + tf->name.str); + } else { + Printf(false, "%Bp %f%D%Fl%Ll%s%E%Bp", + (i & 1) ? 'd' : 'a', + &ScreenSetTtfFont, i, + tf->name.str, + (i & 1) ? 'd' : 'a'); + } + } + } else { + Printf(false, " text = '%Fi%s%E'", e->str.str); + } + break; + } + default: Printf(true, "%Ft?? ENTITY%E"); break; @@ -340,7 +400,7 @@ void TextWindow::DescribeSelection(void) { Printf(false, ""); Printf(false, "%FtIN GROUP%E %s", g->DescriptionString()); if(e->workplane.v == Entity::FREE_IN_3D.v) { - Printf(false, "%FtNO WORKPLANE (FREE IN 3D)%E"); + Printf(false, "%FtNOT LOCKED IN WORKPLANE%E"); } else { Entity *w = SS.GetEntity(e->workplane); Printf(false, "%FtIN WORKPLANE%E %s", w->DescriptionString()); diff --git a/ttf.cpp b/ttf.cpp new file mode 100644 index 00000000..98260c9f --- /dev/null +++ b/ttf.cpp @@ -0,0 +1,712 @@ +#include "solvespace.h" + +//----------------------------------------------------------------------------- +// Get the list of available font filenames, and load the name for each of +// them. Only that, though, not the glyphs too. +//----------------------------------------------------------------------------- +void TtfFontList::LoadAll(void) { + if(loaded) return; + + // Get the list of font files from the platform-specific code. + LoadAllFontFiles(); + + int i; + for(i = 0; i < l.n; i++) { + TtfFont *tf = &(l.elem[i]); + tf->LoadFontFromFile(true); + } + + loaded = true; +} + +void TtfFontList::PlotString(char *font, char *str, double spacing, + hEntity he, Vector origin, Vector u, Vector v) +{ + LoadAll(); + + int i; + for(i = 0; i < l.n; i++) { + TtfFont *tf = &(l.elem[i]); + if(strcmp(tf->FontFileBaseName(), font)==0) { + tf->LoadFontFromFile(false); + tf->PlotString(str, spacing, he, origin, u, v); + return; + } + } + + // Couldn't find the font; so draw a big X for an error marker. + Entity *e = SS.GetEntity(he); + e->LineDrawOrGetDistanceOrEdge(origin, origin.Plus(u).Plus(v)); + e->LineDrawOrGetDistanceOrEdge(origin.Plus(v), origin.Plus(u)); +} + + +//============================================================================= + +//----------------------------------------------------------------------------- +// Get a single character from the open .ttf file; EOF is an error, since +// we can always see that coming. +//----------------------------------------------------------------------------- +int TtfFont::Getc(void) { + int c = fgetc(fh); + if(c < 0) { + throw "EOF"; + } + return c; +} + +//----------------------------------------------------------------------------- +// Helpers to get 1, 2, or 4 bytes from the .ttf file. Big endian. +//----------------------------------------------------------------------------- +int TtfFont::GetBYTE(void) { + return Getc(); +} +int TtfFont::GetWORD(void) { + BYTE b0, b1; + b1 = Getc(); + b0 = Getc(); + + return (b1 << 8) | b0; +} +int TtfFont::GetDWORD(void) { + BYTE b0, b1, b2, b3; + b3 = Getc(); + b2 = Getc(); + b1 = Getc(); + b0 = Getc(); + + return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; +} + +//----------------------------------------------------------------------------- +// Load a glyph from the .ttf file into memory. Assumes that the .ttf file +// is already seeked to the correct location, and writes the result to +// glyphs[index] +//----------------------------------------------------------------------------- +void TtfFont::LoadGlyph(int index) { + if(index < 0 || index >= glyphs) return; + + int i; + + SWORD contours = GetWORD(); + SWORD xMin = GetWORD(); + SWORD yMin = GetWORD(); + SWORD xMax = GetWORD(); + SWORD yMax = GetWORD(); + + if(useGlyph['A'] == index) { + scale = (1024*1024) / yMax; + } + + if(contours > 0) { + WORD *endPointsOfContours = + (WORD *)AllocTemporary(contours*sizeof(WORD)); + + for(i = 0; i < contours; i++) { + endPointsOfContours[i] = GetWORD(); + } + WORD totalPts = endPointsOfContours[i-1] + 1; + + WORD instructionLength = GetWORD(); + for(i = 0; i < instructionLength; i++) { + // We can ignore the instructions, since we're doing vector + // output. + (void)GetBYTE(); + } + + BYTE *flags = (BYTE *)AllocTemporary(totalPts*sizeof(BYTE)); + SWORD *x = (SWORD *)AllocTemporary(totalPts*sizeof(SWORD)); + SWORD *y = (SWORD *)AllocTemporary(totalPts*sizeof(SWORD)); + + // Flags, that indicate format of the coordinates +#define FLAG_ON_CURVE (1 << 0) +#define FLAG_DX_IS_BYTE (1 << 1) +#define FLAG_DY_IS_BYTE (1 << 2) +#define FLAG_REPEAT (1 << 3) +#define FLAG_X_IS_SAME (1 << 4) +#define FLAG_X_IS_POSITIVE (1 << 4) +#define FLAG_Y_IS_SAME (1 << 5) +#define FLAG_Y_IS_POSITIVE (1 << 5) + for(i = 0; i < totalPts; i++) { + flags[i] = GetBYTE(); + if(flags[i] & FLAG_REPEAT) { + int n = GetBYTE(); + BYTE f = flags[i]; + int j; + for(j = 0; j < n; j++) { + i++; + if(i >= totalPts) { + throw "too many points in glyph"; + } + flags[i] = f; + } + } + } + + // x coordinates + SWORD xa = 0; + for(i = 0; i < totalPts; i++) { + if(flags[i] & FLAG_DX_IS_BYTE) { + BYTE v = GetBYTE(); + if(flags[i] & FLAG_X_IS_POSITIVE) { + xa += v; + } else { + xa -= v; + } + } else { + if(flags[i] & FLAG_X_IS_SAME) { + // no change + } else { + SWORD d = GetWORD(); + xa += d; + } + } + x[i] = xa; + } + + // y coordinates + SWORD ya = 0; + for(i = 0; i < totalPts; i++) { + if(flags[i] & FLAG_DY_IS_BYTE) { + BYTE v = GetBYTE(); + if(flags[i] & FLAG_Y_IS_POSITIVE) { + ya += v; + } else { + ya -= v; + } + } else { + if(flags[i] & FLAG_Y_IS_SAME) { + // no change + } else { + SWORD d = GetWORD(); + ya += d; + } + } + y[i] = ya; + } + + Glyph *g = &(glyph[index]); + g->pt = (FontPoint *)MemAlloc(totalPts*sizeof(FontPoint)); + int contour = 0; + for(i = 0; i < totalPts; i++) { + g->pt[i].x = x[i]; + g->pt[i].y = y[i]; + g->pt[i].onCurve = (BYTE)(flags[i] & FLAG_ON_CURVE); + + if(i == endPointsOfContours[contour]) { + g->pt[i].lastInContour = true; + contour++; + } else { + g->pt[i].lastInContour = false; + } + } + g->pts = totalPts; + g->xMax = xMax; + g->xMin = xMin; + + } else { + // This is a composite glyph, TODO. + } +} + +//----------------------------------------------------------------------------- +// Return the basename of our font filename; that's how the requests and +// entities that reference us will store it. +//----------------------------------------------------------------------------- +char *TtfFont::FontFileBaseName(void) { + char *sb = strrchr(fontFile, '\\'); + char *sf = strrchr(fontFile, '/'); + char *s = sf ? sf : sb; + if(!s) return ""; + return s + 1; +} + +//----------------------------------------------------------------------------- +// Load a TrueType font into memory. We care about the curves that define +// the letter shapes, and about the mappings that determine which glyph goes +// with which character. +//----------------------------------------------------------------------------- +bool TtfFont::LoadFontFromFile(bool nameOnly) { + if(loaded) return true; + + int i; + + fh = fopen(fontFile, "rb"); + if(!fh) { + return false; + } + + try { + // First, load the Offset Table + DWORD version = GetDWORD(); + WORD numTables = GetWORD(); + WORD searchRange = GetWORD(); + WORD entrySelector = GetWORD(); + WORD rangeShift = GetWORD(); + + // Now load the Table Directory; our goal in doing this will be to + // find the addresses of the tables that we will need. + DWORD glyfAddr = -1, glyfLen; + DWORD cmapAddr = -1, cmapLen; + DWORD headAddr = -1, headLen; + DWORD locaAddr = -1, locaLen; + DWORD maxpAddr = -1, maxpLen; + DWORD nameAddr = -1, nameLen; + DWORD hmtxAddr = -1, hmtxLen; + DWORD hheaAddr = -1, hheaLen; + + for(i = 0; i < numTables; i++) { + char tag[5] = "xxxx"; + tag[0] = GetBYTE(); + tag[1] = GetBYTE(); + tag[2] = GetBYTE(); + tag[3] = GetBYTE(); + DWORD checksum = GetDWORD(); + DWORD offset = GetDWORD(); + DWORD length = GetDWORD(); + + if(strcmp(tag, "glyf")==0) { + glyfAddr = offset; + glyfLen = length; + } else if(strcmp(tag, "cmap")==0) { + cmapAddr = offset; + cmapLen = length; + } else if(strcmp(tag, "head")==0) { + headAddr = offset; + headLen = length; + } else if(strcmp(tag, "loca")==0) { + locaAddr = offset; + locaLen = length; + } else if(strcmp(tag, "maxp")==0) { + maxpAddr = offset; + maxpLen = length; + } else if(strcmp(tag, "name")==0) { + nameAddr = offset; + nameLen = length; + } else if(strcmp(tag, "hhea")==0) { + hheaAddr = offset; + hheaLen = length; + } else if(strcmp(tag, "hmtx")==0) { + hmtxAddr = offset; + hmtxLen = length; + } + } + + if(glyfAddr == -1 || cmapAddr == -1 || headAddr == -1 || + locaAddr == -1 || maxpAddr == -1 || hmtxAddr == -1 || + nameAddr == -1 || hheaAddr == -1) + { + throw "missing table addr"; + } + + // Load the name table. This gives us display names for the font, which + // we need when we're giving the user a list to choose from. + fseek(fh, nameAddr, SEEK_SET); + + WORD nameFormat = GetWORD(); + WORD nameCount = GetWORD(); + WORD nameStringOffset = GetWORD(); + // And now we're at the name records. Go through those till we find + // one that we want. + int displayNameOffset, displayNameLength; + for(i = 0; i < nameCount; i++) { + WORD platformID = GetWORD(); + WORD encodingID = GetWORD(); + WORD languageID = GetWORD(); + WORD nameId = GetWORD(); + WORD length = GetWORD(); + WORD offset = GetWORD(); + + if(nameId == 4) { + displayNameOffset = offset; + displayNameLength = length; + break; + } + } + if(nameOnly && i >= nameCount) { + throw "no name"; + } + + if(nameOnly) { + // Find the display name, and store it in the provided buffer. + fseek(fh, nameAddr+nameStringOffset+displayNameOffset, SEEK_SET); + int c = 0; + for(i = 0; i < displayNameLength; i++) { + BYTE b = GetBYTE(); + if(b && c < (sizeof(name.str) - 2)) { + name.str[c++] = b; + } + } + name.str[c++] = '\0'; + + fclose(fh); + return true; + } + + + // Load the head table; we need this to determine the format of the + // loca table, 16- or 32-bit entries + fseek(fh, headAddr, SEEK_SET); + + DWORD headVersion = GetDWORD(); + DWORD headFontRevision = GetDWORD(); + DWORD headCheckSumAdj = GetDWORD(); + DWORD headMagicNumber = GetDWORD(); + WORD headFlags = GetWORD(); + WORD headUnitsPerEm = GetWORD(); + (void)GetDWORD(); // created time + (void)GetDWORD(); + (void)GetDWORD(); // modified time + (void)GetDWORD(); + WORD headXmin = GetWORD(); + WORD headYmin = GetWORD(); + WORD headXmax = GetWORD(); + WORD headYmax = GetWORD(); + WORD headMacStyle = GetWORD(); + WORD headLowestRecPPEM = GetWORD(); + WORD headFontDirectionHint = GetWORD(); + WORD headIndexToLocFormat = GetWORD(); + WORD headGlyphDataFormat = GetWORD(); + + if(headMagicNumber != 0x5F0F3CF5) { + throw "bad magic number"; + } + + // Load the hhea table, which contains the number of entries in the + // horizontal metrics (hmtx) table. + fseek(fh, hheaAddr, SEEK_SET); + DWORD hheaVersion = GetDWORD(); + WORD hheaAscender = GetWORD(); + WORD hheaDescender = GetWORD(); + WORD hheaLineGap = GetWORD(); + WORD hheaAdvanceWidthMax = GetWORD(); + WORD hheaMinLsb = GetWORD(); + WORD hheaMinRsb = GetWORD(); + WORD hheaXMaxExtent = GetWORD(); + WORD hheaCaretSlopeRise = GetWORD(); + WORD hheaCaretSlopeRun = GetWORD(); + WORD hheaCaretOffset = GetWORD(); + (void)GetWORD(); + (void)GetWORD(); + (void)GetWORD(); + (void)GetWORD(); + WORD hheaMetricDataFormat = GetWORD(); + WORD hheaNumberOfMetrics = GetWORD(); + + // Load the maxp table, which determines (among other things) the number + // of glyphs in the font + fseek(fh, maxpAddr, SEEK_SET); + + DWORD maxpVersion = GetDWORD(); + WORD maxpNumGlyphs = GetWORD(); + WORD maxpMaxPoints = GetWORD(); + WORD maxpMaxContours = GetWORD(); + WORD maxpMaxComponentPoints = GetWORD(); + WORD maxpMaxComponentContours = GetWORD(); + WORD maxpMaxZones = GetWORD(); + WORD maxpMaxTwilightPoints = GetWORD(); + WORD maxpMaxStorage = GetWORD(); + WORD maxpMaxFunctionDefs = GetWORD(); + WORD maxpMaxInstructionDefs = GetWORD(); + WORD maxpMaxStackElements = GetWORD(); + WORD maxpMaxSizeOfInstructions = GetWORD(); + WORD maxpMaxComponentElements = GetWORD(); + WORD maxpMaxComponentDepth = GetWORD(); + + glyphs = maxpNumGlyphs; + glyph = (Glyph *)MemAlloc(glyphs*sizeof(glyph[0])); + + // Load the hmtx table, which gives the horizontal metrics (spacing + // and advance width) of the font. + fseek(fh, hmtxAddr, SEEK_SET); + + WORD hmtxAdvanceWidth; + SWORD hmtxLsb; + for(i = 0; i < min(glyphs, hheaNumberOfMetrics); i++) { + hmtxAdvanceWidth = GetWORD(); + hmtxLsb = (SWORD)GetWORD(); + + glyph[i].leftSideBearing = hmtxLsb; + glyph[i].advanceWidth = hmtxAdvanceWidth; + } + // The last entry in the table applies to all subsequent glyphs also. + for(; i < glyphs; i++) { + glyph[i].leftSideBearing = hmtxLsb; + glyph[i].advanceWidth = hmtxAdvanceWidth; + } + + // Load the cmap table, which determines the mapping of characters to + // glyphs. + fseek(fh, cmapAddr, SEEK_SET); + + DWORD usedTableAddr = -1; + + WORD cmapVersion = GetWORD(); + WORD cmapTableCount = GetWORD(); + for(i = 0; i < cmapTableCount; i++) { + WORD platformId = GetWORD(); + WORD encodingId = GetWORD(); + DWORD offset = GetDWORD(); + + if(platformId == 3 && encodingId == 1) { + // The Windows Unicode mapping is our preference + usedTableAddr = cmapAddr + offset; + } + } + + if(usedTableAddr == -1) { + throw "no used table addr"; + } + + // So we can load the desired subtable; in this case, Windows Unicode, + // which is us. + fseek(fh, usedTableAddr, SEEK_SET); + + WORD mapFormat = GetWORD(); + WORD mapLength = GetWORD(); + WORD mapVersion = GetWORD(); + WORD mapSegCountX2 = GetWORD(); + WORD mapSearchRange = GetWORD(); + WORD mapEntrySelector = GetWORD(); + WORD mapRangeShift = GetWORD(); + + if(mapFormat != 4) { + // Required to use format 4 per spec + throw "not format 4"; + } + + int segCount = mapSegCountX2 / 2; + WORD *endChar = (WORD *)AllocTemporary(segCount*sizeof(WORD)); + WORD *startChar = (WORD *)AllocTemporary(segCount*sizeof(WORD)); + WORD *idDelta = (WORD *)AllocTemporary(segCount*sizeof(WORD)); + WORD *idRangeOffset = (WORD *)AllocTemporary(segCount*sizeof(WORD)); + + DWORD *filePos = (DWORD *)AllocTemporary(segCount*sizeof(DWORD)); + + for(i = 0; i < segCount; i++) { + endChar[i] = GetWORD(); + } + WORD mapReservedPad = GetWORD(); + for(i = 0; i < segCount; i++) { + startChar[i] = GetWORD(); + } + for(i = 0; i < segCount; i++) { + idDelta[i] = GetWORD(); + } + for(i = 0; i < segCount; i++) { + filePos[i] = ftell(fh); + idRangeOffset[i] = GetWORD(); + } + + // So first, null out the glyph table in our in-memory representation + // of the font; any character for which cmap does not provide a glyph + // corresponds to -1 + for(i = 0; i < arraylen(useGlyph); i++) { + useGlyph[i] = 0; + } + + for(i = 0; i < segCount; i++) { + WORD v = idDelta[i]; + if(idRangeOffset[i] == 0) { + int j; + for(j = startChar[i]; j <= endChar[i]; j++) { + if(j > 0 && j < arraylen(useGlyph)) { + // Don't create a reference to a glyph that we won't + // store because it's bigger than the table. + if((WORD)(j + v) < glyphs) { + // Arithmetic is modulo 2^16 + useGlyph[j] = (WORD)(j + v); + } + } + } + } else { + int j; + for(j = startChar[i]; j <= endChar[i]; j++) { + if(j > 0 && j < arraylen(useGlyph)) { + int fp = filePos[i]; + fp += (j - startChar[i])*sizeof(WORD); + fp += idRangeOffset[i]; + fseek(fh, fp, SEEK_SET); + + useGlyph[j] = GetWORD(); + } + } + } + } + + // Load the loca table. This contains the offsets of each glyph, + // relative to the beginning of the glyf table. + fseek(fh, locaAddr, SEEK_SET); + + DWORD *glyphOffsets = (DWORD *)AllocTemporary(glyphs*sizeof(DWORD)); + + for(i = 0; i < glyphs; i++) { + if(headIndexToLocFormat == 1) { + // long offsets, 32 bits + glyphOffsets[i] = GetDWORD(); + } else if(headIndexToLocFormat == 0) { + // short offsets, 16 bits but divided by 2 + glyphOffsets[i] = GetWORD()*2; + } else { + throw "bad headIndexToLocFormat"; + } + } + + scale = 1024; + // Load the glyf table. This contains the actual representations of the + // letter forms, as piecewise linear or quadratic outlines. + for(i = 0; i < glyphs; i++) { + fseek(fh, glyfAddr + glyphOffsets[i], SEEK_SET); + LoadGlyph(i); + } + } catch (char *s) { + dbp("failed: '%s'", s); + fclose(fh); + return false; + } + + fclose(fh); + loaded = true; + return true; +} + +void TtfFont::Flush(void) { + lastWas = NOTHING; +} + +void TtfFont::Handle(int *dx, int x, int y, bool onCurve) { + x = ((x + *dx)*scale + 512) >> 10; + y = (y*scale + 512) >> 10; + + if(lastWas == ON_CURVE && onCurve) { + // This is a line segment. + LineSegment(lastOnCurve.x, lastOnCurve.y, x, y); + } else if(lastWas == ON_CURVE && !onCurve) { + // We can't do the Bezier until we get the next on-curve point, + // but we must store the off-curve point. + } else if(lastWas == OFF_CURVE && onCurve) { + // We are ready to do a Bezier. + Bezier(lastOnCurve.x, lastOnCurve.y, + lastOffCurve.x, lastOffCurve.y, + x, y); + } else if(lastWas == OFF_CURVE && !onCurve) { + // Two consecutive off-curve points implicitly have an on-point + // curve between them, and that should trigger us to generate a + // Bezier. + IntPoint fake; + fake.x = (x + lastOffCurve.x) / 2; + fake.y = (y + lastOffCurve.y) / 2; + Bezier(lastOnCurve.x, lastOnCurve.y, + lastOffCurve.x, lastOffCurve.y, + fake.x, fake.y); + + lastOnCurve.x = fake.x; + lastOnCurve.y = fake.y; + } + + if(onCurve) { + lastOnCurve.x = x; + lastOnCurve.y = y; + lastWas = ON_CURVE; + } else { + lastOffCurve.x = x; + lastOffCurve.y = y; + lastWas = OFF_CURVE; + } +} + +void TtfFont::PlotCharacter(int *dx, int c, double spacing) { + int gli = useGlyph[c]; + + if(gli < 0 || gli >= glyphs) return; + Glyph *g = &(glyph[gli]); + if(!g->pt) return; + + if(c == ' ') { + *dx += g->advanceWidth; + return; + } + + int dx0 = *dx; + + // A point that has x = xMin should be plotted at (dx0 + lsb); fix up + // our x-position so that the curve-generating code will put stuff + // at the right place. + *dx = dx0 - g->xMin; + *dx += g->leftSideBearing; + + int i; + int firstInContour = 0; + for(i = 0; i < g->pts; i++) { + Handle(dx, g->pt[i].x, g->pt[i].y, g->pt[i].onCurve); + + if(g->pt[i].lastInContour) { + int f = firstInContour; + Handle(dx, g->pt[f].x, g->pt[f].y, g->pt[f].onCurve); + firstInContour = i + 1; + Flush(); + } + } + + // And we're done, so advance our position by the requested advance + // width, plus the user-requested extra advance. + *dx = dx0 + g->advanceWidth + (int)(spacing + 0.5); +} + +void TtfFont::PlotString(char *str, double spacing, + hEntity he, Vector porigin, Vector pu, Vector pv) +{ + entity = he; + u = pu; + v = pv; + origin = porigin; + + if(!loaded || !str || *str == '\0') { + LineSegment(0, 0, 1024, 0); + LineSegment(1024, 0, 1024, 1024); + LineSegment(1024, 1024, 0, 1024); + LineSegment(0, 1024, 0, 0); + return; + } + + int dx = 0; + + while(*str) { + PlotCharacter(&dx, *str, spacing); + str++; + } +} + +Vector TtfFont::TransformIntPoint(int x, int y) { + Vector r = origin; + r = r.Plus(u.ScaledBy(x / 1024.0)); + r = r.Plus(v.ScaledBy(y / 1024.0)); + return r; +} + +void TtfFont::LineSegment(int x0, int y0, int x1, int y1) { + Entity *e = SS.GetEntity(entity); + e->LineDrawOrGetDistanceOrEdge(TransformIntPoint(x0, y0), + TransformIntPoint(x1, y1)); +} + +void TtfFont::Bezier(int x0, int y0, int x1, int y1, int x2, int y2) { + Entity *e = SS.GetEntity(entity); + + Vector p0 = TransformIntPoint(x0, y0), + p1 = TransformIntPoint(x1, y1), + p2 = TransformIntPoint(x2, y2); + + int i, n = max(2, (int)(4/sqrt(SS.meshTol))); + Vector prev = p0; + for(i = 1; i <= n; i++) { + double t = ((double)i)/n; + Vector p = + (p0.ScaledBy((1 - t)*(1 - t))).Plus( + (p1.ScaledBy(2*t*(1 - t))).Plus( + (p2.ScaledBy(t*t)))); + e->LineDrawOrGetDistanceOrEdge(prev, p); + prev = p; + } +} + diff --git a/ui.h b/ui.h index d1317bdc..678c80c9 100644 --- a/ui.h +++ b/ui.h @@ -5,7 +5,7 @@ class TextWindow { public: static const int MAX_COLS = 100; - static const int MAX_ROWS = 200; + static const int MAX_ROWS = 2000; #ifndef RGB #define RGB(r, g, b) ((r) | ((g) << 8) | ((b) << 16)) @@ -67,10 +67,12 @@ public: static const int EDIT_HELIX_TURNS = 8; static const int EDIT_HELIX_PITCH = 9; static const int EDIT_HELIX_DRADIUS = 10; + static const int EDIT_TTF_TEXT = 11; struct { - int meaning; - int i; - hGroup group; + int meaning; + int i; + hGroup group; + hRequest request; } edit; static void ReportHowGroupSolved(hGroup hg); @@ -89,7 +91,13 @@ public: void OneScreenForwardTo(int screen); - // All of these are callbacks from the GUI code. + // All of these are callbacks from the GUI code; first from when + // we're describing an entity + static void ScreenEditTtfText(int link, DWORD v); + static void ScreenSetTtfFont(int link, DWORD v); + static void ScreenUnselectAll(int link, DWORD v); + + // and the rest from the stuff in textscreens.cpp static void ScreenSelectGroup(int link, DWORD v); static void ScreenActivateGroup(int link, DWORD v); static void ScreenToggleGroupShown(int link, DWORD v); @@ -101,7 +109,6 @@ public: static void ScreenHoverRequest(int link, DWORD v); static void ScreenSelectRequest(int link, DWORD v); static void ScreenSelectConstraint(int link, DWORD v); - static void ScreenUnselectAll(int link, DWORD v); static void ScreenChangeOneOrTwoSides(int link, DWORD v); static void ScreenChangeSkipFirst(int link, DWORD v); @@ -162,6 +169,7 @@ public: MNU_ARC, MNU_RECTANGLE, MNU_CUBIC, + MNU_TTF_TEXT, MNU_CONSTRUCTION, // Group MNU_GROUP_3D, diff --git a/undoredo.cpp b/undoredo.cpp index 20d673a3..806bb691 100644 --- a/undoredo.cpp +++ b/undoredo.cpp @@ -1,6 +1,7 @@ #include "solvespace.h" void SolveSpace::UndoRemember(void) { + unsaved = true; PushFromCurrentOnto(&undo); UndoClearStack(&redo); UndoEnableMenus(); diff --git a/win32/w32main.cpp b/win32/w32main.cpp index 57120ff4..18ca6edb 100644 --- a/win32/w32main.cpp +++ b/win32/w32main.cpp @@ -754,6 +754,7 @@ int SaveFileYesNoCancel(void) return r; } + void GetAbsoluteFilename(char *file) { char absoluteFile[MAX_PATH]; @@ -761,6 +762,31 @@ void GetAbsoluteFilename(char *file) strcpy(file, absoluteFile); } +void LoadAllFontFiles(void) +{ + WIN32_FIND_DATA wfd; + char dir[MAX_PATH]; + GetWindowsDirectory(dir, MAX_PATH - 30); + strcat(dir, "\\fonts\\*.ttf"); + + HANDLE h = FindFirstFile(dir, &wfd); + + while(h != INVALID_HANDLE_VALUE) { + TtfFont tf; + ZERO(&tf); + + char fullPath[MAX_PATH]; + GetWindowsDirectory(fullPath, MAX_PATH - (30 + strlen(wfd.cFileName))); + strcat(fullPath, "\\fonts\\"); + strcat(fullPath, wfd.cFileName); + + strcpy(tf.fontFile, fullPath); + SS.fonts.l.Add(&tf); + + if(!FindNextFile(h, &wfd)) break; + } +} + static void MenuById(int id, BOOL yes, BOOL check) { int i; diff --git a/wishlist.txt b/wishlist.txt index 9ae75d42..21508c2b 100644 --- a/wishlist.txt +++ b/wishlist.txt @@ -2,12 +2,12 @@ STL check for meshes, and T intersection removal STL export DXF export -TTF font text some kind of rounding / chamfer remove back button in browser? auto-generate circles and faces when lathing copy the section geometry to other end when sweeping cylindrical faces +draw explicit edges long term