Solvespace
1017 строк · 40.7 Кб
1//-----------------------------------------------------------------------------
2// Routines to write and read our .slvs file format.
3//
4// Copyright 2008-2013 Jonathan Westhues.
5//-----------------------------------------------------------------------------
6#include "solvespace.h"7
8#define VERSION_STRING "\261\262\263" "SolveSpaceREVa"9
10static int StrStartsWith(const char *str, const char *start) {11return memcmp(str, start, strlen(start)) == 0;12}
13
14//-----------------------------------------------------------------------------
15// Clear and free all the dynamic memory associated with our currently-loaded
16// sketch. This does not leave the program in an acceptable state (with the
17// references created, and so on), so anyone calling this must fix that later.
18//-----------------------------------------------------------------------------
19void SolveSpaceUI::ClearExisting() {20UndoClearStack(&redo);21UndoClearStack(&undo);22
23for(hGroup hg : SK.groupOrder) {24Group *g = SK.GetGroup(hg);25g->Clear();26}27
28SK.constraint.Clear();29SK.request.Clear();30SK.group.Clear();31SK.groupOrder.Clear();32SK.style.Clear();33
34SK.entity.Clear();35SK.param.Clear();36images.clear();37}
38
39hGroup SolveSpaceUI::CreateDefaultDrawingGroup() {40Group g = {};41
42// And an empty group, for the first stuff the user draws.43g.visible = true;44g.name = C_("group-name", "sketch-in-plane");45g.type = Group::Type::DRAWING_WORKPLANE;46g.subtype = Group::Subtype::WORKPLANE_BY_POINT_ORTHO;47g.order = 1;48g.predef.q = Quaternion::From(1, 0, 0, 0);49hRequest hr = Request::HREQUEST_REFERENCE_XY;50g.predef.origin = hr.entity(1);51SK.group.AddAndAssignId(&g);52SK.GetGroup(g.h)->activeWorkplane = g.h.entity(0);53return g.h;54}
55
56void SolveSpaceUI::NewFile() {57ClearExisting();58
59// Our initial group, that contains the references.60Group g = {};61g.visible = true;62g.name = C_("group-name", "#references");63g.type = Group::Type::DRAWING_3D;64g.order = 0;65g.h = Group::HGROUP_REFERENCES;66SK.group.Add(&g);67
68// Let's create three two-d coordinate systems, for the coordinate69// planes; these are our references, present in every sketch.70Request r = {};71r.type = Request::Type::WORKPLANE;72r.group = Group::HGROUP_REFERENCES;73r.workplane = Entity::FREE_IN_3D;74
75r.h = Request::HREQUEST_REFERENCE_XY;76SK.request.Add(&r);77
78r.h = Request::HREQUEST_REFERENCE_YZ;79SK.request.Add(&r);80
81r.h = Request::HREQUEST_REFERENCE_ZX;82SK.request.Add(&r);83
84CreateDefaultDrawingGroup();85}
86
87const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = {88{ 'g', "Group.h.v", 'x', &(SS.sv.g.h.v) },89{ 'g', "Group.type", 'd', &(SS.sv.g.type) },90{ 'g', "Group.order", 'd', &(SS.sv.g.order) },91{ 'g', "Group.name", 'S', &(SS.sv.g.name) },92{ 'g', "Group.activeWorkplane.v", 'x', &(SS.sv.g.activeWorkplane.v) },93{ 'g', "Group.opA.v", 'x', &(SS.sv.g.opA.v) },94{ 'g', "Group.opB.v", 'x', &(SS.sv.g.opB.v) },95{ 'g', "Group.valA", 'f', &(SS.sv.g.valA) },96{ 'g', "Group.valB", 'f', &(SS.sv.g.valB) },97{ 'g', "Group.valC", 'f', &(SS.sv.g.valB) },98{ 'g', "Group.color", 'c', &(SS.sv.g.color) },99{ 'g', "Group.subtype", 'd', &(SS.sv.g.subtype) },100{ 'g', "Group.skipFirst", 'b', &(SS.sv.g.skipFirst) },101{ 'g', "Group.meshCombine", 'd', &(SS.sv.g.meshCombine) },102{ 'g', "Group.forceToMesh", 'd', &(SS.sv.g.forceToMesh) },103{ 'g', "Group.predef.q.w", 'f', &(SS.sv.g.predef.q.w) },104{ 'g', "Group.predef.q.vx", 'f', &(SS.sv.g.predef.q.vx) },105{ 'g', "Group.predef.q.vy", 'f', &(SS.sv.g.predef.q.vy) },106{ 'g', "Group.predef.q.vz", 'f', &(SS.sv.g.predef.q.vz) },107{ 'g', "Group.predef.origin.v", 'x', &(SS.sv.g.predef.origin.v) },108{ 'g', "Group.predef.entityB.v", 'x', &(SS.sv.g.predef.entityB.v) },109{ 'g', "Group.predef.entityC.v", 'x', &(SS.sv.g.predef.entityC.v) },110{ 'g', "Group.predef.swapUV", 'b', &(SS.sv.g.predef.swapUV) },111{ 'g', "Group.predef.negateU", 'b', &(SS.sv.g.predef.negateU) },112{ 'g', "Group.predef.negateV", 'b', &(SS.sv.g.predef.negateV) },113{ 'g', "Group.visible", 'b', &(SS.sv.g.visible) },114{ 'g', "Group.suppress", 'b', &(SS.sv.g.suppress) },115{ 'g', "Group.relaxConstraints", 'b', &(SS.sv.g.relaxConstraints) },116{ 'g', "Group.allowRedundant", 'b', &(SS.sv.g.allowRedundant) },117{ 'g', "Group.allDimsReference", 'b', &(SS.sv.g.allDimsReference) },118{ 'g', "Group.scale", 'f', &(SS.sv.g.scale) },119{ 'g', "Group.remap", 'M', &(SS.sv.g.remap) },120{ 'g', "Group.impFile", 'i', NULL },121{ 'g', "Group.impFileRel", 'P', &(SS.sv.g.linkFile) },122
123{ 'p', "Param.h.v.", 'x', &(SS.sv.p.h.v) },124{ 'p', "Param.val", 'f', &(SS.sv.p.val) },125
126{ 'r', "Request.h.v", 'x', &(SS.sv.r.h.v) },127{ 'r', "Request.type", 'd', &(SS.sv.r.type) },128{ 'r', "Request.extraPoints", 'd', &(SS.sv.r.extraPoints) },129{ 'r', "Request.workplane.v", 'x', &(SS.sv.r.workplane.v) },130{ 'r', "Request.group.v", 'x', &(SS.sv.r.group.v) },131{ 'r', "Request.construction", 'b', &(SS.sv.r.construction) },132{ 'r', "Request.style", 'x', &(SS.sv.r.style) },133{ 'r', "Request.str", 'S', &(SS.sv.r.str) },134{ 'r', "Request.font", 'S', &(SS.sv.r.font) },135{ 'r', "Request.file", 'P', &(SS.sv.r.file) },136{ 'r', "Request.aspectRatio", 'f', &(SS.sv.r.aspectRatio) },137
138{ 'e', "Entity.h.v", 'x', &(SS.sv.e.h.v) },139{ 'e', "Entity.type", 'd', &(SS.sv.e.type) },140{ 'e', "Entity.construction", 'b', &(SS.sv.e.construction) },141{ 'e', "Entity.style", 'x', &(SS.sv.e.style) },142{ 'e', "Entity.str", 'S', &(SS.sv.e.str) },143{ 'e', "Entity.font", 'S', &(SS.sv.e.font) },144{ 'e', "Entity.file", 'P', &(SS.sv.e.file) },145{ 'e', "Entity.point[0].v", 'x', &(SS.sv.e.point[0].v) },146{ 'e', "Entity.point[1].v", 'x', &(SS.sv.e.point[1].v) },147{ 'e', "Entity.point[2].v", 'x', &(SS.sv.e.point[2].v) },148{ 'e', "Entity.point[3].v", 'x', &(SS.sv.e.point[3].v) },149{ 'e', "Entity.point[4].v", 'x', &(SS.sv.e.point[4].v) },150{ 'e', "Entity.point[5].v", 'x', &(SS.sv.e.point[5].v) },151{ 'e', "Entity.point[6].v", 'x', &(SS.sv.e.point[6].v) },152{ 'e', "Entity.point[7].v", 'x', &(SS.sv.e.point[7].v) },153{ 'e', "Entity.point[8].v", 'x', &(SS.sv.e.point[8].v) },154{ 'e', "Entity.point[9].v", 'x', &(SS.sv.e.point[9].v) },155{ 'e', "Entity.point[10].v", 'x', &(SS.sv.e.point[10].v) },156{ 'e', "Entity.point[11].v", 'x', &(SS.sv.e.point[11].v) },157{ 'e', "Entity.extraPoints", 'd', &(SS.sv.e.extraPoints) },158{ 'e', "Entity.normal.v", 'x', &(SS.sv.e.normal.v) },159{ 'e', "Entity.distance.v", 'x', &(SS.sv.e.distance.v) },160{ 'e', "Entity.workplane.v", 'x', &(SS.sv.e.workplane.v) },161{ 'e', "Entity.actPoint.x", 'f', &(SS.sv.e.actPoint.x) },162{ 'e', "Entity.actPoint.y", 'f', &(SS.sv.e.actPoint.y) },163{ 'e', "Entity.actPoint.z", 'f', &(SS.sv.e.actPoint.z) },164{ 'e', "Entity.actNormal.w", 'f', &(SS.sv.e.actNormal.w) },165{ 'e', "Entity.actNormal.vx", 'f', &(SS.sv.e.actNormal.vx) },166{ 'e', "Entity.actNormal.vy", 'f', &(SS.sv.e.actNormal.vy) },167{ 'e', "Entity.actNormal.vz", 'f', &(SS.sv.e.actNormal.vz) },168{ 'e', "Entity.actDistance", 'f', &(SS.sv.e.actDistance) },169{ 'e', "Entity.actVisible", 'b', &(SS.sv.e.actVisible), },170
171
172{ 'c', "Constraint.h.v", 'x', &(SS.sv.c.h.v) },173{ 'c', "Constraint.type", 'd', &(SS.sv.c.type) },174{ 'c', "Constraint.group.v", 'x', &(SS.sv.c.group.v) },175{ 'c', "Constraint.workplane.v", 'x', &(SS.sv.c.workplane.v) },176{ 'c', "Constraint.valA", 'f', &(SS.sv.c.valA) },177{ 'c', "Constraint.valP.v", 'x', &(SS.sv.c.valP.v) },178{ 'c', "Constraint.ptA.v", 'x', &(SS.sv.c.ptA.v) },179{ 'c', "Constraint.ptB.v", 'x', &(SS.sv.c.ptB.v) },180{ 'c', "Constraint.entityA.v", 'x', &(SS.sv.c.entityA.v) },181{ 'c', "Constraint.entityB.v", 'x', &(SS.sv.c.entityB.v) },182{ 'c', "Constraint.entityC.v", 'x', &(SS.sv.c.entityC.v) },183{ 'c', "Constraint.entityD.v", 'x', &(SS.sv.c.entityD.v) },184{ 'c', "Constraint.other", 'b', &(SS.sv.c.other) },185{ 'c', "Constraint.other2", 'b', &(SS.sv.c.other2) },186{ 'c', "Constraint.reference", 'b', &(SS.sv.c.reference) },187{ 'c', "Constraint.comment", 'S', &(SS.sv.c.comment) },188{ 'c', "Constraint.disp.offset.x", 'f', &(SS.sv.c.disp.offset.x) },189{ 'c', "Constraint.disp.offset.y", 'f', &(SS.sv.c.disp.offset.y) },190{ 'c', "Constraint.disp.offset.z", 'f', &(SS.sv.c.disp.offset.z) },191{ 'c', "Constraint.disp.style", 'x', &(SS.sv.c.disp.style) },192
193{ 's', "Style.h.v", 'x', &(SS.sv.s.h.v) },194{ 's', "Style.name", 'S', &(SS.sv.s.name) },195{ 's', "Style.width", 'f', &(SS.sv.s.width) },196{ 's', "Style.widthAs", 'd', &(SS.sv.s.widthAs) },197{ 's', "Style.textHeight", 'f', &(SS.sv.s.textHeight) },198{ 's', "Style.textHeightAs", 'd', &(SS.sv.s.textHeightAs) },199{ 's', "Style.textAngle", 'f', &(SS.sv.s.textAngle) },200{ 's', "Style.textOrigin", 'x', &(SS.sv.s.textOrigin) },201{ 's', "Style.color", 'c', &(SS.sv.s.color) },202{ 's', "Style.fillColor", 'c', &(SS.sv.s.fillColor) },203{ 's', "Style.filled", 'b', &(SS.sv.s.filled) },204{ 's', "Style.visible", 'b', &(SS.sv.s.visible) },205{ 's', "Style.exportable", 'b', &(SS.sv.s.exportable) },206{ 's', "Style.stippleType", 'd', &(SS.sv.s.stippleType) },207{ 's', "Style.stippleScale", 'f', &(SS.sv.s.stippleScale) },208
209{ 0, NULL, 0, NULL }210};211
212struct SAVEDptr {213EntityMap &M() { return *((EntityMap *)this); }214std::string &S() { return *((std::string *)this); }215Platform::Path &P() { return *((Platform::Path *)this); }216bool &b() { return *((bool *)this); }217RgbaColor &c() { return *((RgbaColor *)this); }218int &d() { return *((int *)this); }219double &f() { return *((double *)this); }220uint32_t &x() { return *((uint32_t *)this); }221};222
223void SolveSpaceUI::SaveUsingTable(const Platform::Path &filename, int type) {224int i;225for(i = 0; SAVED[i].type != 0; i++) {226if(SAVED[i].type != type) continue;227
228int fmt = SAVED[i].fmt;229SAVEDptr *p = (SAVEDptr *)SAVED[i].ptr;230// Any items that aren't specified are assumed to be zero231if(fmt == 'S' && p->S().empty()) continue;232if(fmt == 'P' && p->P().IsEmpty()) continue;233if(fmt == 'd' && p->d() == 0) continue;234if(fmt == 'f' && EXACT(p->f() == 0.0)) continue;235if(fmt == 'x' && p->x() == 0) continue;236if(fmt == 'i') continue;237
238fprintf(fh, "%s=", SAVED[i].desc);239switch(fmt) {240case 'S': fprintf(fh, "%s", p->S().c_str()); break;241case 'b': fprintf(fh, "%d", p->b() ? 1 : 0); break;242case 'c': fprintf(fh, "%08x", p->c().ToPackedInt()); break;243case 'd': fprintf(fh, "%d", p->d()); break;244case 'f': fprintf(fh, "%.20f", p->f()); break;245case 'x': fprintf(fh, "%08x", p->x()); break;246
247case 'P': {248if(!p->P().IsEmpty()) {249Platform::Path relativePath = p->P().RelativeTo(filename.Parent());250ssassert(!relativePath.IsEmpty(), "Cannot relativize path");251fprintf(fh, "%s", relativePath.ToPortable().c_str());252}253break;254}255
256case 'M': {257fprintf(fh, "{\n");258// Sort the mapping, since EntityMap is not deterministic.259std::vector<std::pair<EntityKey, EntityId>> sorted(p->M().begin(), p->M().end());260std::sort(sorted.begin(), sorted.end(),261[](std::pair<EntityKey, EntityId> &a, std::pair<EntityKey, EntityId> &b) {262return a.second.v < b.second.v;263});264for(auto it : sorted) {265fprintf(fh, " %d %08x %d\n",266it.second.v, it.first.input.v, it.first.copyNumber);267}268fprintf(fh, "}");269break;270}271
272case 'i': break;273
274default: ssassert(false, "Unexpected value format");275}276fprintf(fh, "\n");277}278}
279
280bool SolveSpaceUI::SaveToFile(const Platform::Path &filename) {281// Make sure all the entities are regenerated up to date, since they will be exported.282SS.ScheduleShowTW();283SS.GenerateAll(SolveSpaceUI::Generate::ALL);284
285for(Group &g : SK.group) {286if(g.type != Group::Type::LINKED) continue;287
288if(g.linkFile.RelativeTo(filename).IsEmpty()) {289Error("This sketch links the sketch '%s'; it can only be saved "290"on the same volume.", g.linkFile.raw.c_str());291return false;292}293}294
295fh = OpenFile(filename, "wb");296if(!fh) {297Error("Couldn't write to file '%s'", filename.raw.c_str());298return false;299}300
301fprintf(fh, "%s\n\n\n", VERSION_STRING);302
303int i, j;304for(auto &g : SK.group) {305sv.g = g;306SaveUsingTable(filename, 'g');307fprintf(fh, "AddGroup\n\n");308}309
310for(auto &p : SK.param) {311sv.p = p;312SaveUsingTable(filename, 'p');313fprintf(fh, "AddParam\n\n");314}315
316for(auto &r : SK.request) {317sv.r = r;318SaveUsingTable(filename, 'r');319fprintf(fh, "AddRequest\n\n");320}321
322for(auto &e : SK.entity) {323e.CalculateNumerical(/*forExport=*/true);324sv.e = e;325SaveUsingTable(filename, 'e');326fprintf(fh, "AddEntity\n\n");327}328
329for(auto &c : SK.constraint) {330sv.c = c;331SaveUsingTable(filename, 'c');332fprintf(fh, "AddConstraint\n\n");333}334
335for(auto &s : SK.style) {336sv.s = s;337if(sv.s.h.v >= Style::FIRST_CUSTOM) {338SaveUsingTable(filename, 's');339fprintf(fh, "AddStyle\n\n");340}341}342
343// A group will have either a mesh or a shell, but not both; but the code344// to print either of those just does nothing if the mesh/shell is empty.345
346Group *g = SK.GetGroup(*SK.groupOrder.Last());347SMesh *m = &g->runningMesh;348for(i = 0; i < m->l.n; i++) {349STriangle *tr = &(m->l[i]);350fprintf(fh, "Triangle %08x %08x "351"%.20f %.20f %.20f %.20f %.20f %.20f %.20f %.20f %.20f\n",352tr->meta.face, tr->meta.color.ToPackedInt(),353CO(tr->a), CO(tr->b), CO(tr->c));354}355
356SShell *s = &g->runningShell;357for(SSurface &srf : s->surface) {358fprintf(fh, "Surface %08x %08x %08x %d %d\n",359srf.h.v, srf.color.ToPackedInt(), srf.face, srf.degm, srf.degn);360for(i = 0; i <= srf.degm; i++) {361for(j = 0; j <= srf.degn; j++) {362fprintf(fh, "SCtrl %d %d %.20f %.20f %.20f Weight %20.20f\n",363i, j, CO(srf.ctrl[i][j]), srf.weight[i][j]);364}365}366
367STrimBy *stb;368for(stb = srf.trim.First(); stb; stb = srf.trim.NextAfter(stb)) {369fprintf(fh, "TrimBy %08x %d %.20f %.20f %.20f %.20f %.20f %.20f\n",370stb->curve.v, stb->backwards ? 1 : 0,371CO(stb->start), CO(stb->finish));372}373
374fprintf(fh, "AddSurface\n");375}376for(SCurve &sc : s->curve) {377fprintf(fh, "Curve %08x %d %d %08x %08x\n",378sc.h.v,379sc.isExact ? 1 : 0, sc.exact.deg,380sc.surfA.v, sc.surfB.v);381
382if(sc.isExact) {383for(i = 0; i <= sc.exact.deg; i++) {384fprintf(fh, "CCtrl %d %.20f %.20f %.20f Weight %.20f\n",385i, CO(sc.exact.ctrl[i]), sc.exact.weight[i]);386}387}388SCurvePt *scpt;389for(scpt = sc.pts.First(); scpt; scpt = sc.pts.NextAfter(scpt)) {390fprintf(fh, "CurvePt %d %.20f %.20f %.20f\n",391scpt->vertex ? 1 : 0, CO(scpt->p));392}393
394fprintf(fh, "AddCurve\n");395}396
397fclose(fh);398
399return true;400}
401
402void SolveSpaceUI::LoadUsingTable(const Platform::Path &filename, char *key, char *val) {403int i;404for(i = 0; SAVED[i].type != 0; i++) {405if(strcmp(SAVED[i].desc, key)==0) {406SAVEDptr *p = (SAVEDptr *)SAVED[i].ptr;407unsigned int u = 0;408switch(SAVED[i].fmt) {409case 'S': p->S() = val; break;410case 'b': p->b() = (atoi(val) != 0); break;411case 'd': p->d() = atoi(val); break;412case 'f': p->f() = atof(val); break;413case 'x': sscanf(val, "%x", &u); p->x()= u; break;414
415case 'P': {416Platform::Path path = Platform::Path::FromPortable(val);417if(!path.IsEmpty()) {418p->P() = filename.Parent().Join(path).Expand();419}420break;421}422
423case 'c':424sscanf(val, "%x", &u);425p->c() = RgbaColor::FromPackedInt(u);426break;427
428case 'M': {429p->M().clear();430for(;;) {431EntityKey ek;432EntityId ei;433char line2[1024];434if (fgets(line2, (int)sizeof(line2), fh) == NULL)435break;436if(sscanf(line2, "%d %x %d", &(ei.v), &(ek.input.v),437&(ek.copyNumber)) == 3) {438if(ei.v == Entity::NO_ENTITY.v) {439// Commit bd84bc1a mistakenly introduced code that would remap440// some entities to NO_ENTITY. This was fixed in commit bd84bc1a,441// but files created meanwhile are corrupt, and can cause crashes.442//443// To fix this, we skip any such remaps when loading; they will be444// recreated on the next regeneration. Any resulting orphans will445// be pruned in the usual way, recovering to a well-defined state.446continue;447}448p->M().insert({ ek, ei });449} else {450break;451}452}453break;454}455
456case 'i': break;457
458default: ssassert(false, "Unexpected value format");459}460break;461}462}463if(SAVED[i].type == 0) {464fileLoadError = true;465}466}
467
468bool SolveSpaceUI::LoadFromFile(const Platform::Path &filename, bool canCancel) {469bool fileIsEmpty = true;470allConsistent = false;471fileLoadError = false;472
473fh = OpenFile(filename, "rb");474if(!fh) {475Error("Couldn't read from file '%s'", filename.raw.c_str());476return false;477}478
479ClearExisting();480
481sv = {};482sv.g.scale = 1; // default is 1, not 0; so legacy files need this483Style::FillDefaultStyle(&sv.s);484
485char line[1024];486while(fgets(line, (int)sizeof(line), fh)) {487fileIsEmpty = false;488
489char *s = strchr(line, '\n');490if(s) *s = '\0';491// We should never get files with \r characters in them, but mailers492// will sometimes mangle attachments.493s = strchr(line, '\r');494if(s) *s = '\0';495
496if(*line == '\0') continue;497
498char *e = strchr(line, '=');499if(e) {500*e = '\0';501char *key = line, *val = e+1;502LoadUsingTable(filename, key, val);503} else if(strcmp(line, "AddGroup")==0) {504// legacy files have a spurious dependency between linked groups505// and their parent groups, remove506if(sv.g.type == Group::Type::LINKED)507sv.g.opA.v = 0;508
509SK.group.Add(&(sv.g));510sv.g = {};511sv.g.scale = 1; // default is 1, not 0; so legacy files need this512} else if(strcmp(line, "AddParam")==0) {513// params are regenerated, but we want to preload the values514// for initial guesses515SK.param.Add(&(sv.p));516sv.p = {};517} else if(strcmp(line, "AddEntity")==0) {518// entities are regenerated519} else if(strcmp(line, "AddRequest")==0) {520SK.request.Add(&(sv.r));521sv.r = {};522} else if(strcmp(line, "AddConstraint")==0) {523SK.constraint.Add(&(sv.c));524sv.c = {};525} else if(strcmp(line, "AddStyle")==0) {526SK.style.Add(&(sv.s));527sv.s = {};528Style::FillDefaultStyle(&sv.s);529} else if(strcmp(line, VERSION_STRING)==0) {530// do nothing, version string531} else if(StrStartsWith(line, "Triangle ") ||532StrStartsWith(line, "Surface ") ||533StrStartsWith(line, "SCtrl ") ||534StrStartsWith(line, "TrimBy ") ||535StrStartsWith(line, "Curve ") ||536StrStartsWith(line, "CCtrl ") ||537StrStartsWith(line, "CurvePt ") ||538strcmp(line, "AddSurface")==0 ||539strcmp(line, "AddCurve")==0)540{541// ignore the mesh or shell, since we regenerate that542} else {543fileLoadError = true;544}545}546
547fclose(fh);548
549if(fileIsEmpty) {550Error(_("The file is empty. It may be corrupt."));551NewFile();552}553
554if(fileLoadError) {555Error(_("Unrecognized data in file. This file may be corrupt, or "556"from a newer version of the program."));557// At least leave the program in a non-crashing state.558if(SK.group.IsEmpty()) {559NewFile();560}561}562if(!ReloadAllLinked(filename, canCancel)) {563return false;564}565UpgradeLegacyData();566
567return true;568}
569
570void SolveSpaceUI::UpgradeLegacyData() {571for(Request &r : SK.request) {572switch(r.type) {573// TTF text requests saved in versions prior to 3.0 only have two574// reference points (origin and origin plus v); version 3.0 adds two575// more points, and if we don't do anything, then they will appear576// at workplane origin, and the solver will mess up the sketch if577// it is not fully constrained.578case Request::Type::TTF_TEXT: {579IdList<Entity,hEntity> entity = {};580IdList<Param,hParam> param = {};581r.Generate(&entity, ¶m);582
583// If we didn't load all of the entities and params that this584// request would generate, then add them now, so that we can585// force them to their appropriate positions.586for(Param &p : param) {587if(SK.param.FindByIdNoOops(p.h) != NULL) continue;588SK.param.Add(&p);589}590bool allPointsExist = true;591for(Entity &e : entity) {592if(SK.entity.FindByIdNoOops(e.h) != NULL) continue;593SK.entity.Add(&e);594allPointsExist = false;595}596
597if(!allPointsExist) {598Entity *text = entity.FindById(r.h.entity(0));599Entity *b = entity.FindById(text->point[2]);600Entity *c = entity.FindById(text->point[3]);601ExprVector bex, cex;602text->RectGetPointsExprs(&bex, &cex);603b->PointForceParamTo(bex.Eval());604c->PointForceParamTo(cex.Eval());605}606entity.Clear();607param.Clear();608break;609}610
611default:612break;613}614}615
616// Constraints saved in versions prior to 3.0 never had any params;617// version 3.0 introduced params to constraints to avoid the hairy ball problem,618// so force them where they belong.619IdList<Param,hParam> oldParam = {};620SK.param.DeepCopyInto(&oldParam);621SS.GenerateAll(SolveSpaceUI::Generate::REGEN);622
623auto AllParamsExistFor = [&](Constraint &c) {624IdList<Param,hParam> param = {};625c.Generate(¶m);626bool allParamsExist = true;627for(Param &p : param) {628if(oldParam.FindByIdNoOops(p.h) != NULL) continue;629allParamsExist = false;630break;631}632param.Clear();633return allParamsExist;634};635
636for(Constraint &c : SK.constraint) {637switch(c.type) {638case Constraint::Type::PT_ON_LINE: {639if(AllParamsExistFor(c)) continue;640
641EntityBase *eln = SK.GetEntity(c.entityA);642EntityBase *ea = SK.GetEntity(eln->point[0]);643EntityBase *eb = SK.GetEntity(eln->point[1]);644EntityBase *ep = SK.GetEntity(c.ptA);645
646ExprVector exp = ep->PointGetExprsInWorkplane(c.workplane);647ExprVector exa = ea->PointGetExprsInWorkplane(c.workplane);648ExprVector exb = eb->PointGetExprsInWorkplane(c.workplane);649ExprVector exba = exb.Minus(exa);650Param *p = SK.GetParam(c.h.param(0));651p->val = exba.Dot(exp.Minus(exa))->Eval() / exba.Dot(exba)->Eval();652break;653}654
655case Constraint::Type::CUBIC_LINE_TANGENT: {656if(AllParamsExistFor(c)) continue;657
658EntityBase *cubic = SK.GetEntity(c.entityA);659EntityBase *line = SK.GetEntity(c.entityB);660
661ExprVector a;662if(c.other) {663a = cubic->CubicGetFinishTangentExprs();664} else {665a = cubic->CubicGetStartTangentExprs();666}667
668ExprVector b = line->VectorGetExprs();669
670Param *param = SK.GetParam(c.h.param(0));671param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval();672break;673}674
675case Constraint::Type::SAME_ORIENTATION: {676if(AllParamsExistFor(c)) continue;677
678EntityBase *an = SK.GetEntity(c.entityA);679EntityBase *bn = SK.GetEntity(c.entityB);680
681ExprVector a = an->NormalExprsN();682ExprVector b = bn->NormalExprsN();683
684Param *param = SK.GetParam(c.h.param(0));685param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval();686break;687}688
689case Constraint::Type::PARALLEL: {690if(AllParamsExistFor(c)) continue;691
692EntityBase *ea = SK.GetEntity(c.entityA),693*eb = SK.GetEntity(c.entityB);694ExprVector a = ea->VectorGetExprsInWorkplane(c.workplane);695ExprVector b = eb->VectorGetExprsInWorkplane(c.workplane);696
697Param *param = SK.GetParam(c.h.param(0));698param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval();699break;700}701
702default:703break;704}705}706oldParam.Clear();707}
708
709bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le,710SMesh *m, SShell *sh)711{
712if(strcmp(filename.Extension().c_str(), "emn")==0) {713return LinkIDF(filename, le, m, sh);714} else if(strcmp(filename.Extension().c_str(), "stl")==0) {715return LinkStl(filename, le, m, sh);716} else {717return LoadEntitiesFromSlvs(filename, le, m, sh);718}719}
720
721bool SolveSpaceUI::LoadEntitiesFromSlvs(const Platform::Path &filename, EntityList *le,722SMesh *m, SShell *sh)723{
724SSurface srf = {};725SCurve crv = {};726
727fh = OpenFile(filename, "rb");728if(!fh) return false;729
730le->Clear();731sv = {};732
733char line[1024];734while(fgets(line, (int)sizeof(line), fh)) {735char *s = strchr(line, '\n');736if(s) *s = '\0';737// We should never get files with \r characters in them, but mailers738// will sometimes mangle attachments.739s = strchr(line, '\r');740if(s) *s = '\0';741
742if(*line == '\0') continue;743
744char *e = strchr(line, '=');745if(e) {746*e = '\0';747char *key = line, *val = e+1;748LoadUsingTable(filename, key, val);749} else if(strcmp(line, "AddGroup")==0) {750// These get allocated whether we want them or not.751sv.g.remap.clear();752} else if(strcmp(line, "AddParam")==0) {753
754} else if(strcmp(line, "AddEntity")==0) {755le->Add(&(sv.e));756sv.e = {};757} else if(strcmp(line, "AddRequest")==0) {758
759} else if(strcmp(line, "AddConstraint")==0) {760
761} else if(strcmp(line, "AddStyle")==0) {762// Linked file contains a style that we don't have yet,763// so import it.764if (SK.style.FindByIdNoOops(sv.s.h) == nullptr) {765SK.style.Add(&(sv.s));766}767sv.s = {};768Style::FillDefaultStyle(&sv.s);769} else if(strcmp(line, VERSION_STRING)==0) {770
771} else if(StrStartsWith(line, "Triangle ")) {772STriangle tr = {};773unsigned int rgba = 0;774if(sscanf(line, "Triangle %x %x "775"%lf %lf %lf %lf %lf %lf %lf %lf %lf",776&(tr.meta.face), &rgba,777&(tr.a.x), &(tr.a.y), &(tr.a.z),778&(tr.b.x), &(tr.b.y), &(tr.b.z),779&(tr.c.x), &(tr.c.y), &(tr.c.z)) != 11) {780ssassert(false, "Unexpected Triangle format");781}782tr.meta.color = RgbaColor::FromPackedInt((uint32_t)rgba);783m->AddTriangle(&tr);784} else if(StrStartsWith(line, "Surface ")) {785unsigned int rgba = 0;786if(sscanf(line, "Surface %x %x %x %d %d",787&(srf.h.v), &rgba, &(srf.face),788&(srf.degm), &(srf.degn)) != 5) {789ssassert(false, "Unexpected Surface format");790}791srf.color = RgbaColor::FromPackedInt((uint32_t)rgba);792} else if(StrStartsWith(line, "SCtrl ")) {793int i, j;794Vector c;795double w;796if(sscanf(line, "SCtrl %d %d %lf %lf %lf Weight %lf",797&i, &j, &(c.x), &(c.y), &(c.z), &w) != 6)798{799ssassert(false, "Unexpected SCtrl format");800}801srf.ctrl[i][j] = c;802srf.weight[i][j] = w;803} else if(StrStartsWith(line, "TrimBy ")) {804STrimBy stb = {};805int backwards;806if(sscanf(line, "TrimBy %x %d %lf %lf %lf %lf %lf %lf",807&(stb.curve.v), &backwards,808&(stb.start.x), &(stb.start.y), &(stb.start.z),809&(stb.finish.x), &(stb.finish.y), &(stb.finish.z)) != 8)810{811ssassert(false, "Unexpected TrimBy format");812}813stb.backwards = (backwards != 0);814srf.trim.Add(&stb);815} else if(strcmp(line, "AddSurface")==0) {816sh->surface.Add(&srf);817srf = {};818} else if(StrStartsWith(line, "Curve ")) {819int isExact;820if(sscanf(line, "Curve %x %d %d %x %x",821&(crv.h.v),822&(isExact),823&(crv.exact.deg),824&(crv.surfA.v), &(crv.surfB.v)) != 5)825{826ssassert(false, "Unexpected Curve format");827}828crv.isExact = (isExact != 0);829} else if(StrStartsWith(line, "CCtrl ")) {830int i;831Vector c;832double w;833if(sscanf(line, "CCtrl %d %lf %lf %lf Weight %lf",834&i, &(c.x), &(c.y), &(c.z), &w) != 5)835{836ssassert(false, "Unexpected CCtrl format");837}838crv.exact.ctrl[i] = c;839crv.exact.weight[i] = w;840} else if(StrStartsWith(line, "CurvePt ")) {841SCurvePt scpt;842int vertex;843if(sscanf(line, "CurvePt %d %lf %lf %lf",844&vertex,845&(scpt.p.x), &(scpt.p.y), &(scpt.p.z)) != 4)846{847ssassert(false, "Unexpected CurvePt format");848}849scpt.vertex = (vertex != 0);850crv.pts.Add(&scpt);851} else if(strcmp(line, "AddCurve")==0) {852sh->curve.Add(&crv);853crv = {};854} else ssassert(false, "Unexpected operation");855}856
857fclose(fh);858return true;859}
860
861static Platform::MessageDialog::Response LocateImportedFile(const Platform::Path &filename,862bool canCancel) {863Platform::MessageDialogRef dialog = CreateMessageDialog(SS.GW.window);864
865using Platform::MessageDialog;866dialog->SetType(MessageDialog::Type::QUESTION);867dialog->SetTitle(C_("title", "Missing File"));868dialog->SetMessage(ssprintf(C_("dialog", "The linked file “%s” is not present."),869filename.raw.c_str()));870dialog->SetDescription(C_("dialog", "Do you want to locate it manually?\n\n"871"If you decline, any geometry that depends on "872"the missing file will be permanently removed."));873dialog->AddButton(C_("button", "&Yes"), MessageDialog::Response::YES,874/*isDefault=*/true);875dialog->AddButton(C_("button", "&No"), MessageDialog::Response::NO);876if(canCancel) {877dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL);878}879
880// FIXME(async): asyncify this call881return dialog->RunModal();882}
883
884bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) {885Platform::SettingsRef settings = Platform::GetSettings();886
887std::map<Platform::Path, Platform::Path, Platform::PathLess> linkMap;888
889allConsistent = false;890
891for(Group &g : SK.group) {892if(g.type != Group::Type::LINKED) continue;893
894g.impEntity.Clear();895g.impMesh.Clear();896g.impShell.Clear();897
898// If we prompted for this specific file before, don't ask again.899if(linkMap.count(g.linkFile)) {900g.linkFile = linkMap[g.linkFile];901}902
903try_again:904if(LoadEntitiesFromFile(g.linkFile, &g.impEntity, &g.impMesh, &g.impShell)) {905// We loaded the data, good. Now import its dependencies as well.906for(Entity &e : g.impEntity) {907if(e.type != Entity::Type::IMAGE) continue;908if(!ReloadLinkedImage(g.linkFile, &e.file, canCancel)) {909return false;910}911}912if(g.IsTriangleMeshAssembly())913g.forceToMesh = true;914} else if(linkMap.count(g.linkFile) == 0) {915dbp("Missing file for group: %s", g.name.c_str());916// The file was moved; prompt the user for its new location.917const auto linkFileRelative = g.linkFile.RelativeTo(saveFile);918switch(LocateImportedFile(linkFileRelative, canCancel)) {919case Platform::MessageDialog::Response::YES: {920Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);921dialog->AddFilters(Platform::SolveSpaceLinkFileFilters);922dialog->ThawChoices(settings, "LinkSketch");923dialog->SuggestFilename(linkFileRelative);924if(dialog->RunModal()) {925dialog->FreezeChoices(settings, "LinkSketch");926linkMap[g.linkFile] = dialog->GetFilename();927g.linkFile = dialog->GetFilename();928goto try_again;929} else {930if(canCancel) return false;931break;932}933}934
935case Platform::MessageDialog::Response::NO:936linkMap[g.linkFile].Clear();937// Geometry will be pruned by GenerateAll().938break;939
940case Platform::MessageDialog::Response::CANCEL:941return false;942
943default:944ssassert(false, "Unexpected dialog response");945}946} else {947// User was already asked to and refused to locate a missing linked file.948}949}950
951for(Request &r : SK.request) {952if(r.type != Request::Type::IMAGE) continue;953
954if(!ReloadLinkedImage(saveFile, &r.file, canCancel)) {955return false;956}957}958
959return true;960}
961
962bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile,963Platform::Path *filename, bool canCancel) {964Platform::SettingsRef settings = Platform::GetSettings();965
966std::shared_ptr<Pixmap> pixmap;967bool promptOpenFile = false;968if(filename->IsEmpty()) {969// We're prompting the user for a new image.970promptOpenFile = true;971} else {972auto image = SS.images.find(*filename);973if(image != SS.images.end()) return true;974
975pixmap = Pixmap::ReadPng(*filename);976if(pixmap == NULL) {977// The file was moved; prompt the user for its new location.978switch(LocateImportedFile(filename->RelativeTo(saveFile), canCancel)) {979case Platform::MessageDialog::Response::YES:980promptOpenFile = true;981break;982
983case Platform::MessageDialog::Response::NO:984// We don't know where the file is, record it as absent.985break;986
987case Platform::MessageDialog::Response::CANCEL:988return false;989
990default:991ssassert(false, "Unexpected dialog response");992}993}994}995
996if(promptOpenFile) {997Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);998dialog->AddFilters(Platform::RasterFileFilters);999dialog->ThawChoices(settings, "LinkImage");1000dialog->SuggestFilename(filename->RelativeTo(saveFile));1001if(dialog->RunModal()) {1002dialog->FreezeChoices(settings, "LinkImage");1003*filename = dialog->GetFilename();1004pixmap = Pixmap::ReadPng(*filename);1005if(pixmap == NULL) {1006Error("The image '%s' is corrupted.", filename->raw.c_str());1007}1008// We know where the file is now, good.1009} else if(canCancel) {1010return false;1011}1012}1013
1014// We loaded the data, good.1015SS.images[*filename] = pixmap;1016return true;1017}
1018