solvespace/src/solvespace.cpp

909 lines
31 KiB
C++

//-----------------------------------------------------------------------------
// Entry point in to the program, our registry-stored settings and top-level
// housekeeping when we open, save, and create new files.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "solvespace.h"
#include "config.h"
SolveSpaceUI SolveSpace::SS = {};
Sketch SolveSpace::SK = {};
std::string SolveSpace::RecentFile[MAX_RECENT] = {};
void SolveSpaceUI::Init() {
#if !defined(HEADLESS)
// Check that the resource system works.
dbp("%s", LoadString("banner.txt").data());
#endif
SS.tangentArcRadius = 10.0;
// Then, load the registry settings.
// Default list of colors for the model material
modelColor[0] = CnfThawColor(RGBi(150, 150, 150), "ModelColor_0");
modelColor[1] = CnfThawColor(RGBi(100, 100, 100), "ModelColor_1");
modelColor[2] = CnfThawColor(RGBi( 30, 30, 30), "ModelColor_2");
modelColor[3] = CnfThawColor(RGBi(150, 0, 0), "ModelColor_3");
modelColor[4] = CnfThawColor(RGBi( 0, 100, 0), "ModelColor_4");
modelColor[5] = CnfThawColor(RGBi( 0, 80, 80), "ModelColor_5");
modelColor[6] = CnfThawColor(RGBi( 0, 0, 130), "ModelColor_6");
modelColor[7] = CnfThawColor(RGBi( 80, 0, 80), "ModelColor_7");
// Light intensities
lightIntensity[0] = CnfThawFloat(1.0f, "LightIntensity_0");
lightIntensity[1] = CnfThawFloat(0.5f, "LightIntensity_1");
ambientIntensity = 0.3; // no setting for that yet
// Light positions
lightDir[0].x = CnfThawFloat(-1.0f, "LightDir_0_Right" );
lightDir[0].y = CnfThawFloat( 1.0f, "LightDir_0_Up" );
lightDir[0].z = CnfThawFloat( 0.0f, "LightDir_0_Forward" );
lightDir[1].x = CnfThawFloat( 1.0f, "LightDir_1_Right" );
lightDir[1].y = CnfThawFloat( 0.0f, "LightDir_1_Up" );
lightDir[1].z = CnfThawFloat( 0.0f, "LightDir_1_Forward" );
exportMode = false;
// Chord tolerance
chordTol = CnfThawFloat(0.5f, "ChordTolerancePct");
// Max pwl segments to generate
maxSegments = CnfThawInt(10, "MaxSegments");
// Chord tolerance
exportChordTol = CnfThawFloat(0.1f, "ExportChordTolerance");
// Max pwl segments to generate
exportMaxSegments = CnfThawInt(64, "ExportMaxSegments");
// View units
viewUnits = (Unit)CnfThawInt((uint32_t)Unit::MM, "ViewUnits");
// Number of digits after the decimal point
afterDecimalMm = CnfThawInt(2, "AfterDecimalMm");
afterDecimalInch = CnfThawInt(3, "AfterDecimalInch");
// Camera tangent (determines perspective)
cameraTangent = CnfThawFloat(0.3f/1e3f, "CameraTangent");
// Grid spacing
gridSpacing = CnfThawFloat(5.0f, "GridSpacing");
// Export scale factor
exportScale = CnfThawFloat(1.0f, "ExportScale");
// Export offset (cutter radius comp)
exportOffset = CnfThawFloat(0.0f, "ExportOffset");
// Rewrite exported colors close to white into black (assuming white bg)
fixExportColors = CnfThawBool(true, "FixExportColors");
// Draw back faces of triangles (when mesh is leaky/self-intersecting)
drawBackFaces = CnfThawBool(true, "DrawBackFaces");
// Check that contours are closed and not self-intersecting
checkClosedContour = CnfThawBool(true, "CheckClosedContour");
// Export shaded triangles in a 2d view
exportShadedTriangles = CnfThawBool(true, "ExportShadedTriangles");
// Export pwl curves (instead of exact) always
exportPwlCurves = CnfThawBool(false, "ExportPwlCurves");
// Background color on-screen
backgroundColor = CnfThawColor(RGBi(0, 0, 0), "BackgroundColor");
// Whether export canvas size is fixed or derived from bbox
exportCanvasSizeAuto = CnfThawBool(true, "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");
// Extra parameters when exporting G code
gCode.depth = CnfThawFloat(10.0f, "GCode_Depth");
gCode.passes = CnfThawInt(1, "GCode_Passes");
gCode.feed = CnfThawFloat(10.0f, "GCode_Feed");
gCode.plungeFeed = CnfThawFloat(10.0f, "GCode_PlungeFeed");
// Show toolbar in the graphics window
showToolbar = CnfThawBool(true, "ShowToolbar");
// Recent files menus
for(size_t i = 0; i < MAX_RECENT; i++) {
RecentFile[i] = CnfThawString("", "RecentFile_" + std::to_string(i));
}
RefreshRecentMenus();
// Autosave timer
autosaveInterval = CnfThawInt(5, "AutosaveInterval");
// The default styles (colors, line widths, etc.) are also stored in the
// configuration file, but we will automatically load those as we need
// them.
SetAutosaveTimerFor(autosaveInterval);
NewFile();
AfterNewFile();
}
bool SolveSpaceUI::LoadAutosaveFor(const std::string &filename) {
std::string autosaveFile = filename + AUTOSAVE_SUFFIX;
FILE *f = ssfopen(autosaveFile, "rb");
if(!f)
return false;
fclose(f);
if(LoadAutosaveYesNo() == DIALOG_YES) {
unsaved = true;
return LoadFromFile(autosaveFile);
}
return false;
}
bool SolveSpaceUI::OpenFile(const std::string &filename) {
bool autosaveLoaded = LoadAutosaveFor(filename);
bool fileLoaded = autosaveLoaded || LoadFromFile(filename);
if(fileLoaded)
saveFile = filename;
bool success = fileLoaded && ReloadAllImported(/*canCancel=*/true);
if(success) {
AddToRecentList(filename);
} else {
saveFile = "";
NewFile();
}
AfterNewFile();
unsaved = autosaveLoaded;
return success;
}
void SolveSpaceUI::Exit() {
// Recent files
for(size_t i = 0; i < MAX_RECENT; i++)
CnfFreezeString(RecentFile[i], "RecentFile_" + std::to_string(i));
// Model colors
for(size_t i = 0; i < MODEL_COLORS; i++)
CnfFreezeColor(modelColor[i], "ModelColor_" + std::to_string(i));
// Light intensities
CnfFreezeFloat((float)lightIntensity[0], "LightIntensity_0");
CnfFreezeFloat((float)lightIntensity[1], "LightIntensity_1");
// Light directions
CnfFreezeFloat((float)lightDir[0].x, "LightDir_0_Right");
CnfFreezeFloat((float)lightDir[0].y, "LightDir_0_Up");
CnfFreezeFloat((float)lightDir[0].z, "LightDir_0_Forward");
CnfFreezeFloat((float)lightDir[1].x, "LightDir_1_Right");
CnfFreezeFloat((float)lightDir[1].y, "LightDir_1_Up");
CnfFreezeFloat((float)lightDir[1].z, "LightDir_1_Forward");
// Chord tolerance
CnfFreezeFloat((float)chordTol, "ChordTolerancePct");
// Max pwl segments to generate
CnfFreezeInt((uint32_t)maxSegments, "MaxSegments");
// Export Chord tolerance
CnfFreezeFloat((float)exportChordTol, "ExportChordTolerance");
// Export Max pwl segments to generate
CnfFreezeInt((uint32_t)exportMaxSegments, "ExportMaxSegments");
// View units
CnfFreezeInt((uint32_t)viewUnits, "ViewUnits");
// Number of digits after the decimal point
CnfFreezeInt((uint32_t)afterDecimalMm, "AfterDecimalMm");
CnfFreezeInt((uint32_t)afterDecimalInch, "AfterDecimalInch");
// Camera tangent (determines perspective)
CnfFreezeFloat((float)cameraTangent, "CameraTangent");
// Grid spacing
CnfFreezeFloat(gridSpacing, "GridSpacing");
// Export scale
CnfFreezeFloat(exportScale, "ExportScale");
// Export offset (cutter radius comp)
CnfFreezeFloat(exportOffset, "ExportOffset");
// Rewrite exported colors close to white into black (assuming white bg)
CnfFreezeBool(fixExportColors, "FixExportColors");
// Draw back faces of triangles (when mesh is leaky/self-intersecting)
CnfFreezeBool(drawBackFaces, "DrawBackFaces");
// Check that contours are closed and not self-intersecting
CnfFreezeBool(checkClosedContour, "CheckClosedContour");
// Export shaded triangles in a 2d view
CnfFreezeBool(exportShadedTriangles, "ExportShadedTriangles");
// Export pwl curves (instead of exact) always
CnfFreezeBool(exportPwlCurves, "ExportPwlCurves");
// Background color on-screen
CnfFreezeColor(backgroundColor, "BackgroundColor");
// Whether export canvas size is fixed or derived from bbox
CnfFreezeBool(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");
// Extra parameters when exporting G code
CnfFreezeFloat(gCode.depth, "GCode_Depth");
CnfFreezeInt(gCode.passes, "GCode_Passes");
CnfFreezeFloat(gCode.feed, "GCode_Feed");
CnfFreezeFloat(gCode.plungeFeed, "GCode_PlungeFeed");
// Show toolbar in the graphics window
CnfFreezeBool(showToolbar, "ShowToolbar");
// Autosave timer
CnfFreezeInt(autosaveInterval, "AutosaveInterval");
// And the default styles, colors and line widths and such.
Style::FreezeDefaultStyles();
ExitNow();
}
void SolveSpaceUI::ScheduleGenerateAll() {
if(!later.scheduled) ScheduleLater();
later.scheduled = true;
later.generateAll = true;
}
void SolveSpaceUI::ScheduleShowTW() {
if(!later.scheduled) ScheduleLater();
later.scheduled = true;
later.showTW = true;
}
void SolveSpaceUI::DoLater() {
if(later.generateAll) GenerateAll();
if(later.showTW) TW.Show();
later = {};
}
double SolveSpaceUI::MmPerUnit() {
if(viewUnits == Unit::INCHES) {
return 25.4;
} else {
return 1.0;
}
}
const char *SolveSpaceUI::UnitName() {
if(viewUnits == Unit::INCHES) {
return "inch";
} else {
return "mm";
}
}
std::string SolveSpaceUI::MmToString(double v) {
if(viewUnits == Unit::INCHES) {
return ssprintf("%.*f", afterDecimalInch, v/25.4);
} else {
return ssprintf("%.*f", afterDecimalMm, v);
}
}
double SolveSpaceUI::ExprToMm(Expr *e) {
return (e->Eval()) * MmPerUnit();
}
double SolveSpaceUI::StringToMm(const std::string &str) {
return std::stod(str) * MmPerUnit();
}
double SolveSpaceUI::ChordTolMm() {
if(exportMode) return ExportChordTolMm();
return chordTolCalculated;
}
double SolveSpaceUI::ExportChordTolMm() {
return exportChordTol / exportScale;
}
int SolveSpaceUI::GetMaxSegments() {
if(exportMode) return exportMaxSegments;
return maxSegments;
}
int SolveSpaceUI::UnitDigitsAfterDecimal() {
return (viewUnits == Unit::INCHES) ? afterDecimalInch : afterDecimalMm;
}
void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) {
if(viewUnits == Unit::INCHES) {
afterDecimalInch = v;
} else {
afterDecimalMm = v;
}
}
double SolveSpaceUI::CameraTangent() {
if(!usePerspectiveProj) {
return 0;
} else {
return cameraTangent;
}
}
void SolveSpaceUI::AfterNewFile() {
// Clear out the traced point, which is no longer valid
traced.point = Entity::NO_ENTITY;
traced.path.l.Clear();
// and the naked edges
nakedEdges.Clear();
// Quit export mode
justExportedInfo.draw = false;
exportMode = false;
// GenerateAll() expects the view to be valid, because it uses that to
// fill in default values for extrusion depths etc. (which won't matter
// here, but just don't let it work on garbage)
SS.GW.offset = Vector::From(0, 0, 0);
SS.GW.projRight = Vector::From(1, 0, 0);
SS.GW.projUp = Vector::From(0, 1, 0);
GenerateAll(Generate::REGEN);
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. Consider
// invisible entities, so we still get something reasonable if the only
// thing visible is the not-yet-generated surfaces.
GW.ZoomToFit(/*includingInvisibles=*/true);
GenerateAll(Generate::ALL);
SS.ScheduleShowTW();
// Then zoom to fit again, to fit the triangles
GW.ZoomToFit(/*includingInvisibles=*/false);
// Create all the default styles; they'll get created on the fly anyways,
// but can't hurt to do it now.
Style::CreateAllDefaultStyles();
UpdateWindowTitle();
}
void SolveSpaceUI::RemoveFromRecentList(const std::string &filename) {
int dest = 0;
for(int src = 0; src < (int)MAX_RECENT; src++) {
if(filename != RecentFile[src]) {
if(src != dest) RecentFile[dest] = RecentFile[src];
dest++;
}
}
while(dest < (int)MAX_RECENT) RecentFile[dest++].clear();
RefreshRecentMenus();
}
void SolveSpaceUI::AddToRecentList(const std::string &filename) {
RemoveFromRecentList(filename);
for(int src = MAX_RECENT - 2; src >= 0; src--) {
RecentFile[src+1] = RecentFile[src];
}
RecentFile[0] = filename;
RefreshRecentMenus();
}
bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
std::string prevSaveFile = saveFile;
if(saveAs || saveFile.empty()) {
if(!GetSaveFile(&saveFile, "", SlvsFileFilter)) return false;
// need to get new filename directly into saveFile, since that
// determines linkFileRel path
}
if(SaveToFile(saveFile)) {
AddToRecentList(saveFile);
RemoveAutosave();
unsaved = false;
return true;
} else {
// don't store an invalid save filename
saveFile = prevSaveFile;
return false;
}
}
bool SolveSpaceUI::Autosave()
{
SetAutosaveTimerFor(autosaveInterval);
if(!saveFile.empty() && unsaved)
return SaveToFile(saveFile + AUTOSAVE_SUFFIX);
return false;
}
void SolveSpaceUI::RemoveAutosave()
{
std::string autosaveFile = saveFile + AUTOSAVE_SUFFIX;
ssremove(autosaveFile);
}
bool SolveSpaceUI::OkayToStartNewFile() {
if(!unsaved) return true;
switch(SaveFileYesNoCancel()) {
case DIALOG_YES:
return GetFilenameAndSave(/*saveAs=*/false);
case DIALOG_NO:
RemoveAutosave();
return true;
case DIALOG_CANCEL:
return false;
}
ssassert(false, "Unexpected dialog choice");
}
void SolveSpaceUI::UpdateWindowTitle() {
SetCurrentFilename(saveFile);
}
static std::string Extension(const std::string &filename) {
int dot = filename.rfind('.');
if(dot >= 0)
return filename.substr(dot + 1, filename.length());
return "";
}
void SolveSpaceUI::MenuFile(Command id) {
if((uint32_t)id >= (uint32_t)Command::RECENT_OPEN &&
(uint32_t)id < ((uint32_t)Command::RECENT_OPEN+MAX_RECENT)) {
if(!SS.OkayToStartNewFile()) return;
std::string newFile = RecentFile[(uint32_t)id - (uint32_t)Command::RECENT_OPEN];
SS.OpenFile(newFile);
return;
}
switch(id) {
case Command::NEW:
if(!SS.OkayToStartNewFile()) break;
SS.saveFile = "";
SS.NewFile();
SS.AfterNewFile();
break;
case Command::OPEN: {
if(!SS.OkayToStartNewFile()) break;
std::string newFile;
if(GetOpenFile(&newFile, "", SlvsFileFilter)) {
SS.OpenFile(newFile);
}
break;
}
case Command::SAVE:
SS.GetFilenameAndSave(/*saveAs=*/false);
break;
case Command::SAVE_AS:
SS.GetFilenameAndSave(/*saveAs=*/true);
break;
case Command::EXPORT_PNG: {
std::string exportFile;
if(!GetSaveFile(&exportFile, "", PngFileFilter)) break;
SS.ExportAsPngTo(exportFile);
break;
}
case Command::EXPORT_VIEW: {
std::string exportFile;
if(!GetSaveFile(&exportFile, CnfThawString("", "ViewExportFormat"),
VectorFileFilter)) break;
CnfFreezeString(Extension(exportFile), "ViewExportFormat");
// If the user is exporting something where it would be
// inappropriate to include the constraints, then warn.
if(SS.GW.showConstraints &&
(FilenameHasExtension(exportFile, ".txt") ||
fabs(SS.exportOffset) > LENGTH_EPS))
{
Message("Constraints are currently shown, and will be exported "
"in the toolpath. This is probably not what you want; "
"hide them by clicking the link at the top of the "
"text window.");
}
SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/false);
break;
}
case Command::EXPORT_WIREFRAME: {
std::string exportFile;
if(!GetSaveFile(&exportFile, CnfThawString("", "WireframeExportFormat"),
Vector3dFileFilter)) break;
CnfFreezeString(Extension(exportFile), "WireframeExportFormat");
SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/true);
break;
}
case Command::EXPORT_SECTION: {
std::string exportFile;
if(!GetSaveFile(&exportFile, CnfThawString("", "SectionExportFormat"),
VectorFileFilter)) break;
CnfFreezeString(Extension(exportFile), "SectionExportFormat");
SS.ExportSectionTo(exportFile);
break;
}
case Command::EXPORT_MESH: {
std::string exportFile;
if(!GetSaveFile(&exportFile, CnfThawString("", "MeshExportFormat"),
MeshFileFilter)) break;
CnfFreezeString(Extension(exportFile), "MeshExportFormat");
SS.ExportMeshTo(exportFile);
break;
}
case Command::EXPORT_SURFACES: {
std::string exportFile;
if(!GetSaveFile(&exportFile, CnfThawString("", "SurfacesExportFormat"),
SurfaceFileFilter)) break;
CnfFreezeString(Extension(exportFile), "SurfacesExportFormat");
StepFileWriter sfw = {};
sfw.ExportSurfacesTo(exportFile);
break;
}
case Command::IMPORT: {
std::string importFile;
if(!GetOpenFile(&importFile, CnfThawString("", "ImportFormat"),
ImportableFileFilter)) break;
CnfFreezeString(Extension(importFile), "ImportFormat");
if(Extension(importFile) == "dxf") {
ImportDxf(importFile);
} else if(Extension(importFile) == "dwg") {
ImportDwg(importFile);
} else ssassert(false, "Unexpected extension of file to import");
SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
SS.ScheduleShowTW();
break;
}
case Command::EXIT:
if(!SS.OkayToStartNewFile()) break;
SS.Exit();
break;
default: ssassert(false, "Unexpected menu ID");
}
SS.UpdateWindowTitle();
}
void SolveSpaceUI::MenuAnalyze(Command id) {
SS.GW.GroupSelection();
#define gs (SS.GW.gs)
switch(id) {
case Command::STEP_DIM:
if(gs.constraints == 1 && gs.n == 0) {
Constraint *c = SK.GetConstraint(gs.constraint[0]);
if(c->HasLabel() && !c->reference) {
SS.TW.shown.dimFinish = c->valA;
SS.TW.shown.dimSteps = 10;
SS.TW.shown.dimIsDistance =
(c->type != Constraint::Type::ANGLE) &&
(c->type != Constraint::Type::LENGTH_RATIO) &&
(c->type != Constraint::Type::LENGTH_DIFFERENCE);
SS.TW.shown.constraint = c->h;
SS.TW.shown.screen = TextWindow::Screen::STEP_DIMENSION;
// The step params are specified in the text window,
// so force that to be shown.
SS.GW.ForceTextWindowShown();
SS.ScheduleShowTW();
SS.GW.ClearSelection();
} else {
Error("Constraint must have a label, and must not be "
"a reference dimension.");
}
} else {
Error("Bad selection for step dimension; select a constraint.");
}
break;
case Command::NAKED_EDGES: {
SS.nakedEdges.Clear();
Group *g = SK.GetGroup(SS.GW.activeGroup);
SMesh *m = &(g->displayMesh);
SKdNode *root = SKdNode::From(m);
bool inters, leaks;
root->MakeCertainEdgesInto(&(SS.nakedEdges),
EdgeKind::NAKED_OR_SELF_INTER, /*coplanarIsInter=*/true, &inters, &leaks);
InvalidateGraphics();
const char *intersMsg = inters ?
"The mesh is self-intersecting (NOT okay, invalid)." :
"The mesh is not self-intersecting (okay, valid).";
const char *leaksMsg = leaks ?
"The mesh has naked edges (NOT okay, invalid)." :
"The mesh is watertight (okay, valid).";
std::string cntMsg = ssprintf("\n\nThe model contains %d triangles, from "
"%d surfaces.", g->displayMesh.l.n, g->runningShell.surface.n);
if(SS.nakedEdges.l.n == 0) {
Message("%s\n\n%s\n\nZero problematic edges, good.%s",
intersMsg, leaksMsg, cntMsg.c_str());
} else {
Error("%s\n\n%s\n\n%d problematic edges, bad.%s",
intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str());
}
break;
}
case Command::INTERFERENCE: {
SS.nakedEdges.Clear();
SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
SKdNode *root = SKdNode::From(m);
bool inters, leaks;
root->MakeCertainEdgesInto(&(SS.nakedEdges),
EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks);
InvalidateGraphics();
if(inters) {
Error("%d edges interfere with other triangles, bad.",
SS.nakedEdges.l.n);
} else {
Message("The assembly does not interfere, good.");
}
break;
}
case Command::VOLUME: {
SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
double vol = 0;
int i;
for(i = 0; i < m->l.n; i++) {
STriangle tr = m->l.elem[i];
// Translate to place vertex A at (x, y, 0)
Vector trans = Vector::From(tr.a.x, tr.a.y, 0);
tr.a = (tr.a).Minus(trans);
tr.b = (tr.b).Minus(trans);
tr.c = (tr.c).Minus(trans);
// Rotate to place vertex B on the y-axis. Depending on
// whether the triangle is CW or CCW, C is either to the
// right or to the left of the y-axis. This handles the
// sign of our normal.
Vector u = Vector::From(-tr.b.y, tr.b.x, 0);
u = u.WithMagnitude(1);
Vector v = Vector::From(tr.b.x, tr.b.y, 0);
v = v.WithMagnitude(1);
Vector n = Vector::From(0, 0, 1);
tr.a = (tr.a).DotInToCsys(u, v, n);
tr.b = (tr.b).DotInToCsys(u, v, n);
tr.c = (tr.c).DotInToCsys(u, v, n);
n = tr.Normal().WithMagnitude(1);
// Triangles on edge don't contribute
if(fabs(n.z) < LENGTH_EPS) continue;
// The plane has equation p dot n = a dot n
double d = (tr.a).Dot(n);
// nx*x + ny*y + nz*z = d
// nz*z = d - nx*x - ny*y
double A = -n.x/n.z, B = -n.y/n.z, C = d/n.z;
double mac = tr.c.y/tr.c.x, mbc = (tr.c.y - tr.b.y)/tr.c.x;
double xc = tr.c.x, yb = tr.b.y;
// I asked Maple for
// int(int(A*x + B*y +C, y=mac*x..(mbc*x + yb)), x=0..xc);
double integral =
(1.0/3)*(
A*(mbc-mac)+
(1.0/2)*B*(mbc*mbc-mac*mac)
)*(xc*xc*xc)+
(1.0/2)*(A*yb+B*yb*mbc+C*(mbc-mac))*xc*xc+
C*yb*xc+
(1.0/2)*B*yb*yb*xc;
vol += integral;
}
std::string msg = ssprintf("The volume of the solid model is:\n\n"" %.3f %s^3",
vol / pow(SS.MmPerUnit(), 3),
SS.UnitName());
if(SS.viewUnits == Unit::MM) {
msg += ssprintf("\n %.2f mL", vol/(10*10*10));
}
msg += "\n\nCurved surfaces have been approximated as triangles.\n"
"This introduces error, typically of around 1%.";
Message("%s", msg.c_str());
break;
}
case Command::AREA: {
Group *g = SK.GetGroup(SS.GW.activeGroup);
if(g->polyError.how != PolyError::GOOD) {
Error("This group does not contain a correctly-formed "
"2d closed area. It is open, not coplanar, or self-"
"intersecting.");
break;
}
SEdgeList sel = {};
g->polyLoops.MakeEdgesInto(&sel);
SPolygon sp = {};
sel.AssemblePolygon(&sp, NULL, /*keepDir=*/true);
sp.normal = sp.ComputeNormal();
sp.FixContourDirections();
double area = sp.SignedArea();
double scale = SS.MmPerUnit();
Message("The area of the region sketched in this group is:\n\n"
" %.3f %s^2\n\n"
"Curves have been approximated as piecewise linear.\n"
"This introduces error, typically of around 1%%.",
area / (scale*scale),
SS.UnitName());
sel.Clear();
sp.Clear();
break;
}
case Command::PERIMETER: {
if(gs.n > 0 && gs.n == gs.entities) {
double perimeter = 0.0;
for(int i = 0; i < gs.entities; i++) {
Entity *e = SK.entity.FindById(gs.entity[i]);
SEdgeList *el = e->GetOrGenerateEdges();
for(const SEdge &e : el->l) {
perimeter += e.b.Minus(e.a).Magnitude();
}
}
double scale = SS.MmPerUnit();
Message("The total length of the selected entities is:\n\n"
" %.3f %s\n\n"
"Curves have been approximated as piecewise linear.\n"
"This introduces error, typically of around 1%%.",
perimeter / scale,
SS.UnitName());
} else {
Error("Bad selection for perimeter; select line segments, arcs, and curves.");
}
break;
}
case Command::SHOW_DOF:
// This works like a normal solve, except that it calculates
// which variables are free/bound at the same time.
SS.GenerateAll(SolveSpaceUI::Generate::ALL, true);
break;
case Command::TRACE_PT:
if(gs.points == 1 && gs.n == 1) {
SS.traced.point = gs.point[0];
SS.GW.ClearSelection();
} else {
Error("Bad selection for trace; select a single point.");
}
break;
case Command::STOP_TRACING: {
std::string exportFile;
if(GetSaveFile(&exportFile, "", CsvFileFilter)) {
FILE *f = ssfopen(exportFile, "wb");
if(f) {
int i;
SContour *sc = &(SS.traced.path);
for(i = 0; i < sc->l.n; i++) {
Vector p = sc->l.elem[i].p;
double s = SS.exportScale;
fprintf(f, "%.10f, %.10f, %.10f\r\n",
p.x/s, p.y/s, p.z/s);
}
fclose(f);
} else {
Error("Couldn't write to '%s'", exportFile.c_str());
}
}
// Clear the trace, and stop tracing
SS.traced.point = Entity::NO_ENTITY;
SS.traced.path.l.Clear();
InvalidateGraphics();
break;
}
default: ssassert(false, "Unexpected menu ID");
}
}
void SolveSpaceUI::MenuHelp(Command id) {
switch(id) {
case Command::WEBSITE:
OpenWebsite("http://solvespace.com/helpmenu");
break;
case Command::ABOUT:
Message(
"This is SolveSpace version " PACKAGE_VERSION ".\n"
"\n"
"For more information, see http://solvespace.com/\n"
"\n"
"SolveSpace is free software: you are free to modify\n"
"and/or redistribute it under the terms of the GNU\n"
"General Public License (GPL) version 3 or later.\n"
"\n"
"There is NO WARRANTY, to the extent permitted by\n"
"law. For details, visit http://gnu.org/licenses/\n"
"\n"
"© 2008-2016 Jonathan Westhues and other authors.\n"
);
break;
default: ssassert(false, "Unexpected menu ID");
}
}
void SolveSpaceUI::Clear() {
sys.Clear();
for(int i = 0; i < MAX_UNDO; i++) {
if(i < undo.cnt) undo.d[i].Clear();
if(i < redo.cnt) redo.d[i].Clear();
}
}
void Sketch::Clear() {
group.Clear();
groupOrder.Clear();
constraint.Clear();
request.Clear();
style.Clear();
entity.Clear();
param.Clear();
}
BBox Sketch::CalculateEntityBBox(bool includingInvisible) {
BBox box = {};
bool first = true;
for(int i = 0; i < entity.n; i++) {
Entity *e = (Entity *)&entity.elem[i];
if(!(e->IsVisible() || includingInvisible)) continue;
Vector point;
double r = 0.0;
if(e->IsPoint()) {
point = e->PointGetNum();
} else {
switch(e->type) {
case Entity::Type::ARC_OF_CIRCLE:
case Entity::Type::CIRCLE:
r = e->CircleGetRadiusNum();
point = GetEntity(e->point[0])->PointGetNum();
break;
default: continue;
}
}
if(first) {
box.minp = point;
box.maxp = point;
box.Include(point, r);
first = false;
} else {
box.Include(point, r);
}
}
return box;
}
Group *Sketch::GetRunningMeshGroupFor(hGroup h) {
Group *g = GetGroup(h);
while(g != NULL) {
if(g->IsMeshGroup()) {
return g;
}
g = g->PreviousGroup();
}
return NULL;
}