solvespace/solvespace.cpp
Jonathan Westhues b2f2f90a27 Add DXF export. The complexity comes from all the different ways to
specify the plane from which we want to grab the triangles. Shared
edges are then removed with the same code used to check for
watertight meshes, and the remaining edges are assembled into
polygons.

[git-p4: depot-paths = "//depot/solvespace/": change = 1823]
2008-07-07 22:30:13 -08:00

918 lines
26 KiB
C++

#include "solvespace.h"
#include <png.h>
SolveSpace SS;
void SolveSpace::Init(char *cmdLine) {
int i;
// Default list of colors for the model material
modelColor[0] = CnfThawDWORD(RGB(150, 150, 150), "ModelColor_0");
modelColor[1] = CnfThawDWORD(RGB(100, 100, 100), "ModelColor_1");
modelColor[2] = CnfThawDWORD(RGB( 30, 30, 30), "ModelColor_2");
modelColor[3] = CnfThawDWORD(RGB(150, 0, 0), "ModelColor_3");
modelColor[4] = CnfThawDWORD(RGB( 0, 100, 0), "ModelColor_4");
modelColor[5] = CnfThawDWORD(RGB( 0, 80, 80), "ModelColor_5");
modelColor[6] = CnfThawDWORD(RGB( 0, 0, 130), "ModelColor_6");
modelColor[7] = CnfThawDWORD(RGB( 80, 0, 80), "ModelColor_7");
// Light intensities
lightIntensity[0] = ((int)CnfThawDWORD( 700, "LightIntensity_0"))/1000.0;
lightIntensity[1] = ((int)CnfThawDWORD( 400, "LightIntensity_1"))/1000.0;
// Light positions
lightDir[0].x = ((int)CnfThawDWORD(-500, "LightDir_0_Right" ))/1000.0;
lightDir[0].y = ((int)CnfThawDWORD( 500, "LightDir_0_Up" ))/1000.0;
lightDir[0].z = ((int)CnfThawDWORD( 0, "LightDir_0_Forward" ))/1000.0;
lightDir[1].x = ((int)CnfThawDWORD( 500, "LightDir_1_Right" ))/1000.0;
lightDir[1].y = ((int)CnfThawDWORD( 0, "LightDir_1_Up" ))/1000.0;
lightDir[1].z = ((int)CnfThawDWORD( 0, "LightDir_1_Forward" ))/1000.0;
// Mesh tolerance
meshTol = ((int)CnfThawDWORD(1000, "MeshTolerance"))/1000.0;
// View units
viewUnits = (Unit)CnfThawDWORD((DWORD)UNIT_MM, "ViewUnits");
// Camera tangent (determines perspective)
cameraTangent = ((int)CnfThawDWORD(0, "CameraTangent"))/1e6;
// Color for edges (drawn as lines for emphasis)
edgeColor = CnfThawDWORD(RGB(0, 0, 0), "EdgeColor");
// Recent files menus
for(i = 0; i < MAX_RECENT; i++) {
char name[100];
sprintf(name, "RecentFile_%d", i);
strcpy(RecentFile[i], "");
CnfThawString(RecentFile[i], MAX_PATH, name);
}
RefreshRecentMenus();
// Start with either an empty file, or the file specified on the
// command line.
NewFile();
AfterNewFile();
if(strlen(cmdLine) != 0) {
if(LoadFromFile(cmdLine)) {
strcpy(saveFile, cmdLine);
} else {
NewFile();
}
}
AfterNewFile();
}
void SolveSpace::Exit(void) {
int i;
char name[100];
// Recent files
for(i = 0; i < MAX_RECENT; i++) {
sprintf(name, "RecentFile_%d", i);
CnfFreezeString(RecentFile[i], name);
}
// Model colors
for(i = 0; i < MODEL_COLORS; i++) {
sprintf(name, "ModelColor_%d", i);
CnfFreezeDWORD(modelColor[i], name);
}
// Light intensities
CnfFreezeDWORD((int)(lightIntensity[0]*1000), "LightIntensity_0");
CnfFreezeDWORD((int)(lightIntensity[1]*1000), "LightIntensity_1");
// Light directions
CnfFreezeDWORD((int)(lightDir[0].x*1000), "LightDir_0_Right");
CnfFreezeDWORD((int)(lightDir[0].y*1000), "LightDir_0_Up");
CnfFreezeDWORD((int)(lightDir[0].z*1000), "LightDir_0_Forward");
CnfFreezeDWORD((int)(lightDir[1].x*1000), "LightDir_1_Right");
CnfFreezeDWORD((int)(lightDir[1].y*1000), "LightDir_1_Up");
CnfFreezeDWORD((int)(lightDir[1].z*1000), "LightDir_1_Forward");
// Mesh tolerance
CnfFreezeDWORD((int)(meshTol*1000), "MeshTolerance");
// Display/entry units
CnfFreezeDWORD((int)viewUnits, "ViewUnits");
// Camera tangent (determines perspective)
CnfFreezeDWORD((int)(cameraTangent*1e6), "CameraTangent");
// Color for edges (drawn as lines for emphasis)
CnfFreezeDWORD(edgeColor, "EdgeColor");
ExitNow();
}
void SolveSpace::DoLater(void) {
if(later.generateAll) GenerateAll();
if(later.showTW) TW.Show();
ZERO(&later);
}
int SolveSpace::CircleSides(double r) {
int s = 7 + (int)(sqrt(r*SS.GW.scale/meshTol));
return min(s, 40);
}
char *SolveSpace::MmToString(double v) {
static int WhichBuf;
static char Bufs[8][128];
WhichBuf++;
if(WhichBuf >= 8 || WhichBuf < 0) WhichBuf = 0;
char *s = Bufs[WhichBuf];
if(viewUnits == UNIT_INCHES) {
sprintf(s, "%.3f", v/25.4);
} else {
sprintf(s, "%.2f", v);
}
return s;
}
double SolveSpace::ExprToMm(Expr *e) {
if(viewUnits == UNIT_INCHES) {
return (e->Eval())*25.4;
} else {
return e->Eval();
}
}
void SolveSpace::AfterNewFile(void) {
ReloadAllImported();
GenerateAll(-1, -1);
TW.Init();
GW.Init();
unsaved = false;
int w, h;
GetGraphicsWindowSize(&w, &h);
GW.width = w;
GW.height = h;
// The triangles haven't been generated yet, but zoom to fit the entities
// roughly in the window, since that sets the mesh tolerance.
GW.ZoomToFit();
GenerateAll(0, INT_MAX);
later.showTW = true;
// Then zoom to fit again, to fit the triangles
GW.ZoomToFit();
}
void SolveSpace::MarkGroupDirtyByEntity(hEntity he) {
Entity *e = SS.GetEntity(he);
MarkGroupDirty(e->group);
}
void SolveSpace::MarkGroupDirty(hGroup hg) {
int i;
bool go = false;
for(i = 0; i < group.n; i++) {
Group *g = &(group.elem[i]);
if(g->h.v == hg.v) {
go = true;
}
if(go) {
g->clean = false;
}
}
unsaved = true;
}
bool SolveSpace::PruneOrphans(void) {
int i;
for(i = 0; i < request.n; i++) {
Request *r = &(request.elem[i]);
if(GroupExists(r->group)) continue;
(deleted.requests)++;
request.RemoveById(r->h);
return true;
}
for(i = 0; i < constraint.n; i++) {
Constraint *c = &(constraint.elem[i]);
if(GroupExists(c->group)) continue;
(deleted.constraints)++;
constraint.RemoveById(c->h);
return true;
}
return false;
}
bool SolveSpace::GroupsInOrder(hGroup before, hGroup after) {
if(before.v == 0) return true;
if(after.v == 0) return true;
int beforep = -1, afterp = -1;
int i;
for(i = 0; i < group.n; i++) {
Group *g = &(group.elem[i]);
if(g->h.v == before.v) beforep = i;
if(g->h.v == after.v) afterp = i;
}
if(beforep < 0 || afterp < 0) return false;
if(beforep >= afterp) return false;
return true;
}
bool SolveSpace::GroupExists(hGroup hg) {
// A nonexistent group is not acceptable
return group.FindByIdNoOops(hg) ? true : false;
}
bool SolveSpace::EntityExists(hEntity he) {
// A nonexstient entity is acceptable, though, usually just means it
// doesn't apply.
if(he.v == Entity::NO_ENTITY.v) return true;
return entity.FindByIdNoOops(he) ? true : false;
}
bool SolveSpace::PruneGroups(hGroup hg) {
Group *g = GetGroup(hg);
if(GroupsInOrder(g->opA, hg) &&
EntityExists(g->predef.origin) &&
EntityExists(g->predef.entityB) &&
EntityExists(g->predef.entityC))
{
return false;
}
(deleted.groups)++;
group.RemoveById(g->h);
return true;
}
bool SolveSpace::PruneRequests(hGroup hg) {
int i;
for(i = 0; i < entity.n; i++) {
Entity *e = &(entity.elem[i]);
if(e->group.v != hg.v) continue;
if(EntityExists(e->workplane)) continue;
if(!e->h.isFromRequest()) oops();
(deleted.requests)++;
request.RemoveById(e->h.request());
return true;
}
return false;
}
bool SolveSpace::PruneConstraints(hGroup hg) {
int i;
for(i = 0; i < constraint.n; i++) {
Constraint *c = &(constraint.elem[i]);
if(c->group.v != hg.v) continue;
if(EntityExists(c->workplane) &&
EntityExists(c->ptA) &&
EntityExists(c->ptB) &&
EntityExists(c->ptC) &&
EntityExists(c->entityA) &&
EntityExists(c->entityB))
{
continue;
}
(deleted.constraints)++;
constraint.RemoveById(c->h);
return true;
}
return false;
}
void SolveSpace::GenerateAll(void) {
int i;
int firstDirty = INT_MAX, lastVisible = 0;
// Start from the first dirty group, and solve until the active group,
// since all groups after the active group are hidden.
for(i = 0; i < group.n; i++) {
Group *g = &(group.elem[i]);
if((!g->clean) || (g->solved.how != Group::SOLVED_OKAY)) {
firstDirty = min(firstDirty, i);
}
if(g->h.v == SS.GW.activeGroup.v) {
lastVisible = i;
}
}
if(firstDirty == INT_MAX || lastVisible == 0) {
// All clean; so just regenerate the entities, and don't solve anything.
GenerateAll(-1, -1);
} else {
GenerateAll(firstDirty, lastVisible);
}
}
void SolveSpace::GenerateAll(int first, int last) {
int i, j;
while(PruneOrphans())
;
// Don't lose our numerical guesses when we regenerate.
IdList<Param,hParam> prev;
param.MoveSelfInto(&prev);
entity.Clear();
for(i = 0; i < group.n; i++) {
Group *g = &(group.elem[i]);
// The group may depend on entities or other groups, to define its
// workplane geometry or for its operands. Those must already exist
// in a previous group, so check them before generating.
if(PruneGroups(g->h))
goto pruned;
for(j = 0; j < request.n; j++) {
Request *r = &(request.elem[j]);
if(r->group.v != g->h.v) continue;
r->Generate(&entity, &param);
}
g->Generate(&entity, &param);
// The requests and constraints depend on stuff in this or the
// previous group, so check them after generating.
if(PruneRequests(g->h) || PruneConstraints(g->h))
goto pruned;
// Use the previous values for params that we've seen before, as
// initial guesses for the solver.
for(j = 0; j < param.n; j++) {
Param *newp = &(param.elem[j]);
if(newp->known) continue;
Param *prevp = prev.FindByIdNoOops(newp->h);
if(prevp) newp->val = prevp->val;
}
if(g->h.v == Group::HGROUP_REFERENCES.v) {
ForceReferences();
g->solved.how = Group::SOLVED_OKAY;
g->clean = true;
} else {
if(i >= first && i <= last) {
// The group falls inside the range, so really solve it,
// and then regenerate the mesh based on the solved stuff.
SolveGroup(g->h);
g->GeneratePolygon();
g->GenerateMesh();
g->clean = true;
} else {
// The group falls outside the range, so just assume that
// it's good wherever we left it. The mesh is unchanged,
// and the parameters must be marked as known.
for(j = 0; j < param.n; j++) {
Param *newp = &(param.elem[j]);
Param *prevp = prev.FindByIdNoOops(newp->h);
if(prevp) newp->known = true;
}
}
}
}
// And update any reference dimensions with their new values
for(i = 0; i < constraint.n; i++) {
Constraint *c = &(constraint.elem[i]);
if(c->reference) {
c->ModifyToSatisfy();
}
}
prev.Clear();
InvalidateGraphics();
// Remove nonexistent selection items, for same reason we waited till
// the end to put up a dialog box.
GW.ClearNonexistentSelectionItems();
if(deleted.requests > 0 || deleted.constraints > 0 || deleted.groups > 0) {
// All sorts of interesting things could have happened; for example,
// the active group or active workplane could have been deleted. So
// clear all that out.
if(deleted.groups > 0) {
SS.TW.ClearSuper();
}
later.showTW = true;
GW.ClearSuper();
// Don't display any errors until we've regenerated fully. The
// sketch is not necessarily in a consistent state until we've
// pruned any orphaned etc. objects, and the message loop for the
// messagebox could allow us to repaint and crash. But now we must
// be fine.
Error("Additional sketch elements were deleted, because they depend "
"on the element that was just deleted explicitly. These "
"include: \r\n"
" %d request%s\r\n"
" %d constraint%s\r\n"
" %d group%s\r\n\r\n"
"Choose Edit -> Undo to undelete all elements.",
deleted.requests, deleted.requests == 1 ? "" : "s",
deleted.constraints, deleted.constraints == 1 ? "" : "s",
deleted.groups, deleted.groups == 1 ? "" : "s");
memset(&deleted, 0, sizeof(deleted));
}
FreeAllTemporary();
allConsistent = true;
return;
pruned:
// Restore the numerical guesses
param.Clear();
prev.MoveSelfInto(&param);
// Try again
GenerateAll(first, last);
}
void SolveSpace::ForceReferences(void) {
// Force the values of the paramters that define the three reference
// coordinate systems.
static const struct {
hRequest hr;
Quaternion q;
} Quat[] = {
{ Request::HREQUEST_REFERENCE_XY, { 1, 0, 0, 0, } },
{ Request::HREQUEST_REFERENCE_YZ, { 0.5, 0.5, 0.5, 0.5, } },
{ Request::HREQUEST_REFERENCE_ZX, { 0.5, -0.5, -0.5, -0.5, } },
};
for(int i = 0; i < 3; i++) {
hRequest hr = Quat[i].hr;
Entity *wrkpl = GetEntity(hr.entity(0));
// The origin for our coordinate system, always zero
Entity *origin = GetEntity(wrkpl->point[0]);
origin->PointForceTo(Vector::From(0, 0, 0));
GetParam(origin->param[0])->known = true;
GetParam(origin->param[1])->known = true;
GetParam(origin->param[2])->known = true;
// The quaternion that defines the rotation, from the table.
Entity *normal = GetEntity(wrkpl->normal);
normal->NormalForceTo(Quat[i].q);
GetParam(normal->param[0])->known = true;
GetParam(normal->param[1])->known = true;
GetParam(normal->param[2])->known = true;
GetParam(normal->param[3])->known = true;
}
}
void SolveSpace::SolveGroup(hGroup hg) {
int i;
// Clear out the system to be solved.
sys.entity.Clear();
sys.param.Clear();
sys.eq.Clear();
// And generate all the params for requests in this group
for(i = 0; i < request.n; i++) {
Request *r = &(request.elem[i]);
if(r->group.v != hg.v) continue;
r->Generate(&(sys.entity), &(sys.param));
}
// And for the group itself
Group *g = SS.GetGroup(hg);
g->Generate(&(sys.entity), &(sys.param));
// Set the initial guesses for all the params
for(i = 0; i < sys.param.n; i++) {
Param *p = &(sys.param.elem[i]);
p->known = false;
p->val = GetParam(p->h)->val;
}
sys.Solve(g);
FreeAllTemporary();
}
void SolveSpace::ExportDxfTo(char *filename) {
SPolygon *sp;
SPolygon spa;
ZERO(&spa);
Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp);
gn = gn.WithMagnitude(1);
SS.GW.GroupSelection();
#define gs (SS.GW.gs)
Group *g = SS.GetGroup(SS.GW.activeGroup);
// The plane in which the exported section lies; need this because we'll
// reorient from that plane into the xy plane before exporting.
Vector p, u, v, n;
double d;
if(gs.n == 0 && !(g->poly.IsEmpty())) {
// Easiest case--export the polygon drawn in this group
sp = &(g->poly);
p = sp->AnyPoint();
n = (sp->ComputeNormal()).WithMagnitude(1);
if(n.Dot(gn) < 0) n = n.ScaledBy(-1);
u = n.Normal(0);
v = n.Normal(1);
d = p.Dot(n);
goto havepoly;
}
if(g->runningMesh.l.n > 0 &&
((gs.n == 0 && g->activeWorkplane.v != Entity::FREE_IN_3D.v) ||
(gs.n == 1 && gs.faces == 1) ||
(gs.n == 3 && gs.vectors == 2 && gs.points == 1)))
{
if(gs.n == 0) {
Entity *wrkpl = SS.GetEntity(g->activeWorkplane);
p = wrkpl->WorkplaneGetOffset();
n = wrkpl->Normal()->NormalN();
u = wrkpl->Normal()->NormalU();
v = wrkpl->Normal()->NormalV();
} else if(gs.n == 1) {
Entity *face = SS.GetEntity(gs.entity[0]);
p = face->FaceGetPointNum();
n = face->FaceGetNormalNum();
if(n.Dot(gn) < 0) n = n.ScaledBy(-1);
u = n.Normal(0);
v = n.Normal(1);
} else if(gs.n == 3) {
Vector ut = SS.GetEntity(gs.entity[0])->VectorGetNum(),
vt = SS.GetEntity(gs.entity[1])->VectorGetNum();
ut = ut.WithMagnitude(1);
vt = vt.WithMagnitude(1);
if(fabs(SS.GW.projUp.Dot(vt)) < fabs(SS.GW.projUp.Dot(ut))) {
SWAP(Vector, ut, vt);
}
if(SS.GW.projRight.Dot(ut) < 0) ut = ut.ScaledBy(-1);
if(SS.GW.projUp. Dot(vt) < 0) vt = vt.ScaledBy(-1);
p = SS.GetEntity(gs.point[0])->PointGetNum();
n = ut.Cross(vt);
u = ut.WithMagnitude(1);
v = (n.Cross(u)).WithMagnitude(1);
} else oops();
n = n.WithMagnitude(1);
d = p.Dot(n);
SMesh m;
ZERO(&m);
m.MakeFromCopy(&(g->runningMesh));
m.l.ClearTags();
int i;
for(i = 0; i < m.l.n; i++) {
STriangle *tr = &(m.l.elem[i]);
if((fabs(n.Dot(tr->a) - d) >= LENGTH_EPS) ||
(fabs(n.Dot(tr->b) - d) >= LENGTH_EPS) ||
(fabs(n.Dot(tr->c) - d) >= LENGTH_EPS))
{
tr->tag = 1;
}
}
m.l.RemoveTagged();
SKdNode *root = SKdNode::From(&m);
root->SnapToMesh(&m);
SEdgeList el;
ZERO(&el);
root->MakeCertainEdgesInto(&el, false);
el.AssemblePolygon(&spa, NULL);
sp = &spa;
el.Clear();
m.Clear();
SS.GW.ClearSelection();
goto havepoly;
}
Error("Geometry to export not specified.");
return;
havepoly:
FILE *f = fopen(filename, "wb");
if(!f) {
Error("Couldn't write to '%s'", filename);
spa.Clear();
return;
}
// Some software, like Adobe Illustrator, insists on a header.
fprintf(f,
" 999\n"
"file created by SolveSpace\n"
" 0\n"
"SECTION\n"
" 2\n"
"HEADER\n"
" 9\n"
"$ACADVER\n"
" 1\n"
"AC1006\n"
" 9\n"
"$INSBASE\n"
" 10\n"
"0.0\n"
" 20\n"
"0.0\n"
" 30\n"
"0.0\n"
" 9\n"
"$EXTMIN\n"
" 10\n"
"0.0\n"
" 20\n"
"0.0\n"
" 9\n"
"$EXTMAX\n"
" 10\n"
"10000.0\n"
" 20\n"
"10000.0\n"
" 0\n"
"ENDSEC\n");
// Now begin the entities, which are just line segments reproduced from
// our piecewise linear curves.
fprintf(f,
" 0\n"
"SECTION\n"
" 2\n"
"ENTITIES\n");
int i, j;
for(i = 0; i < sp->l.n; i++) {
SContour *sc = &(sp->l.elem[i]);
for(j = 1; j < sc->l.n; j++) {
Vector p0 = sc->l.elem[j-1].p,
p1 = sc->l.elem[j].p;
Point2d e0 = p0.Project2d(u, v),
e1 = p1.Project2d(u, v);
fprintf(f,
" 0\n"
"LINE\n"
" 8\n" // Layer code
"%d\n"
" 10\n" // xA
"%.6f\n"
" 20\n" // yA
"%.6f\n"
" 30\n" // zA
"%.6f\n"
" 11\n" // xB
"%.6f\n"
" 21\n" // yB
"%.6f\n"
" 31\n" // zB
"%.6f\n",
0,
e0.x, e0.y, 0.0,
e1.x, e1.y, 0.0);
}
}
fprintf(f,
" 0\n"
"ENDSEC\n"
" 0\n"
"EOF\n" );
spa.Clear();
fclose(f);
}
void SolveSpace::ExportMeshTo(char *filename) {
SMesh *m = &(SS.GetGroup(SS.GW.activeGroup)->runningMesh);
if(m->l.n == 0) {
Error("Active group mesh is empty; nothing to export.");
return;
}
SKdNode *root = SKdNode::From(m);
root->SnapToMesh(m);
SMesh vvm;
ZERO(&vvm);
root->MakeMeshInto(&vvm);
FILE *f = fopen(filename, "wb");
if(!f) {
Error("Couldn't write to '%s'", filename);
vvm.Clear();
return;
}
char str[80];
memset(str, 0, sizeof(str));
strcpy(str, "STL exported mesh");
fwrite(str, 1, 80, f);
DWORD n = vvm.l.n;
fwrite(&n, 4, 1, f);
int i;
for(i = 0; i < vvm.l.n; i++) {
STriangle *tr = &(vvm.l.elem[i]);
Vector n = tr->Normal().WithMagnitude(1);
float w;
w = (float)n.x; fwrite(&w, 4, 1, f);
w = (float)n.y; fwrite(&w, 4, 1, f);
w = (float)n.z; fwrite(&w, 4, 1, f);
w = (float)tr->a.x; fwrite(&w, 4, 1, f);
w = (float)tr->a.y; fwrite(&w, 4, 1, f);
w = (float)tr->a.z; fwrite(&w, 4, 1, f);
w = (float)tr->b.x; fwrite(&w, 4, 1, f);
w = (float)tr->b.y; fwrite(&w, 4, 1, f);
w = (float)tr->b.z; fwrite(&w, 4, 1, f);
w = (float)tr->c.x; fwrite(&w, 4, 1, f);
w = (float)tr->c.y; fwrite(&w, 4, 1, f);
w = (float)tr->c.z; fwrite(&w, 4, 1, f);
fputc(0, f);
fputc(0, f);
}
vvm.Clear();
fclose(f);
}
void SolveSpace::ExportAsPngTo(char *filename) {
int w = (int)SS.GW.width, h = (int)SS.GW.height;
// No guarantee that the back buffer contains anything valid right now,
// so repaint the scene.
SS.GW.Paint(w, h);
FILE *f = fopen(filename, "wb");
if(!f) goto err;
png_struct *png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if(!png_ptr) goto err;
png_info *info_ptr = png_create_info_struct(png_ptr);
if(!png_ptr) goto err;
if(setjmp(png_jmpbuf(png_ptr))) goto err;
png_init_io(png_ptr, f);
// glReadPixels wants to align things on 4-boundaries, and there's 3
// bytes per pixel. As long as the row width is divisible by 4, all
// works out.
w &= ~3; h &= ~3;
png_set_IHDR(png_ptr, info_ptr, w, h,
8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
// Get the pixel data from the framebuffer
BYTE *pixels = (BYTE *)AllocTemporary(3*w*h);
BYTE **rowptrs = (BYTE **)AllocTemporary(h*sizeof(BYTE *));
glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, pixels);
int y;
for(y = 0; y < h; y++) {
// gl puts the origin at lower left, but png puts it top left
rowptrs[y] = pixels + ((h - 1) - y)*(3*w);
}
png_write_image(png_ptr, rowptrs);
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
return;
err:
Error("Error writing PNG file '%s'", filename);
if(f) fclose(f);
return;
}
void SolveSpace::RemoveFromRecentList(char *file) {
int src, dest;
dest = 0;
for(src = 0; src < MAX_RECENT; src++) {
if(strcmp(file, RecentFile[src]) != 0) {
if(src != dest) strcpy(RecentFile[dest], RecentFile[src]);
dest++;
}
}
while(dest < MAX_RECENT) strcpy(RecentFile[dest++], "");
RefreshRecentMenus();
}
void SolveSpace::AddToRecentList(char *file) {
RemoveFromRecentList(file);
int src;
for(src = MAX_RECENT - 2; src >= 0; src--) {
strcpy(RecentFile[src+1], RecentFile[src]);
}
strcpy(RecentFile[0], file);
RefreshRecentMenus();
}
bool SolveSpace::GetFilenameAndSave(bool saveAs) {
char newFile[MAX_PATH];
strcpy(newFile, saveFile);
if(saveAs || strlen(newFile)==0) {
if(!GetSaveFile(newFile, SLVS_EXT, SLVS_PATTERN)) return false;
}
if(SaveToFile(newFile)) {
AddToRecentList(newFile);
strcpy(saveFile, newFile);
unsaved = false;
return true;
} else {
return false;
}
}
bool SolveSpace::OkayToStartNewFile(void) {
if(!unsaved) return true;
switch(SaveFileYesNoCancel()) {
case IDYES:
return GetFilenameAndSave(false);
case IDNO:
return true;
case IDCANCEL:
return false;
default: oops();
}
}
void SolveSpace::MenuFile(int id) {
if(id >= RECENT_OPEN && id < (RECENT_OPEN+MAX_RECENT)) {
char newFile[MAX_PATH];
strcpy(newFile, RecentFile[id-RECENT_OPEN]);
RemoveFromRecentList(newFile);
if(SS.LoadFromFile(newFile)) {
strcpy(SS.saveFile, newFile);
AddToRecentList(newFile);
} else {
strcpy(SS.saveFile, "");
SS.NewFile();
}
SS.AfterNewFile();
return;
}
switch(id) {
case GraphicsWindow::MNU_NEW:
if(!SS.OkayToStartNewFile()) break;
strcpy(SS.saveFile, "");
SS.NewFile();
SS.AfterNewFile();
break;
case GraphicsWindow::MNU_OPEN: {
if(!SS.OkayToStartNewFile()) break;
char newFile[MAX_PATH] = "";
if(GetOpenFile(newFile, SLVS_EXT, SLVS_PATTERN)) {
if(SS.LoadFromFile(newFile)) {
strcpy(SS.saveFile, newFile);
AddToRecentList(newFile);
} else {
strcpy(SS.saveFile, "");
SS.NewFile();
}
SS.AfterNewFile();
}
break;
}
case GraphicsWindow::MNU_SAVE:
SS.GetFilenameAndSave(false);
break;
case GraphicsWindow::MNU_SAVE_AS:
SS.GetFilenameAndSave(true);
break;
case GraphicsWindow::MNU_EXPORT_PNG: {
char exportFile[MAX_PATH] = "";
if(!GetSaveFile(exportFile, PNG_EXT, PNG_PATTERN)) break;
SS.ExportAsPngTo(exportFile);
break;
}
case GraphicsWindow::MNU_EXPORT_DXF: {
char exportFile[MAX_PATH] = "";
if(!GetSaveFile(exportFile, DXF_EXT, DXF_PATTERN)) break;
SS.ExportDxfTo(exportFile);
break;
}
case GraphicsWindow::MNU_EXPORT_MESH: {
char exportFile[MAX_PATH] = "";
if(!GetSaveFile(exportFile, STL_EXT, STL_PATTERN)) break;
SS.ExportMeshTo(exportFile);
break;
}
case GraphicsWindow::MNU_EXIT:
if(!SS.OkayToStartNewFile()) break;
SS.Exit();
break;
default: oops();
}
}