예제 #1
0
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;
}
예제 #2
0
파일: style.cpp 프로젝트: Kenzu/solvespace
void TextWindow::ScreenBackgroundImage(int link, uint32_t v) {
    SS.bgImage.pixmap.Clear();

    if(link == 'l') {
        std::string bgImageFile;
        if(GetOpenFile(&bgImageFile, "", PngFileFilter)) {
            FILE *f = ssfopen(bgImageFile, "rb");
            if(f) {
                SS.bgImage.pixmap = Pixmap::FromPNG(f);
                SS.bgImage.scale  = SS.GW.scale;
                SS.bgImage.origin = SS.GW.offset.ScaledBy(-1);
            }
        }
    }
    SS.ScheduleShowTW();
}
예제 #3
0
void SolveSpaceUI::MenuAnalyze(int id) {
    SS.GW.GroupSelection();
#define gs (SS.GW.gs)

    switch(id) {
        case GraphicsWindow::MNU_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::ANGLE) &&
                        (c->type != Constraint::LENGTH_RATIO) &&
                        (c->type != Constraint::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 GraphicsWindow::MNU_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),
                SKdNode::NAKED_OR_SELF_INTER_EDGES, 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 GraphicsWindow::MNU_INTERFERENCE: {
            SS.nakedEdges.Clear();

            SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
            SKdNode *root = SKdNode::From(m);
            bool inters, leaks;
            root->MakeCertainEdgesInto(&(SS.nakedEdges),
                SKdNode::SELF_INTER_EDGES, 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 GraphicsWindow::MNU_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 == SolveSpaceUI::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 GraphicsWindow::MNU_AREA: {
            Group *g = SK.GetGroup(SS.GW.activeGroup);
            if(g->polyError.how != Group::POLY_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, 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 GraphicsWindow::MNU_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 GraphicsWindow::MNU_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 GraphicsWindow::MNU_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: oops();
    }
}
예제 #4
0
파일: file.cpp 프로젝트: jariou/solvespace
bool SolveSpaceUI::ReloadAllImported(bool canCancel)
{
    std::map<std::string, std::string> importMap;
    allConsistent = false;

    int i;
    for(i = 0; i < SK.group.n; i++) {
        Group *g = &(SK.group.elem[i]);
        if(g->type != Group::IMPORTED) continue;

        if(isalpha(g->impFile[0]) && g->impFile[1] == ':') {
            // Make sure that g->impFileRel always contains a relative path
            // in an UNIX format, even after we load an old file which had
            // the path in Windows format
            PathSepNormalize(g->impFileRel);
        }

        g->impEntity.Clear();
        g->impMesh.Clear();
        g->impShell.Clear();

        if(importMap.count(g->impFile)) {
            std::string newPath = importMap[g->impFile];
            if(!newPath.empty())
                g->impFile = newPath;
        }

        FILE *test = ssfopen(g->impFile, "rb");
        if(test) {
            fclose(test); // okay, exists
        } else {
            // It doesn't exist. Perhaps the entire tree has moved, and we
            // can use the relative filename to get us back.
            if(!SS.saveFile.empty()) {
                std::string rel = PathSepUNIXToPlatform(g->impFileRel);
                std::string fromRel = MakePathAbsolute(SS.saveFile, rel);
                test = ssfopen(fromRel, "rb");
                if(test) {
                    fclose(test);
                    // It worked, this is our new absolute path
                    g->impFile = fromRel;
                }
            }
        }

try_load_file:
        if(LoadEntitiesFromFile(g->impFile, &(g->impEntity), &(g->impMesh), &(g->impShell)))
        {
            if(!SS.saveFile.empty()) {
                // Record the imported file's name relative to our filename;
                // if the entire tree moves, then everything will still work
                std::string rel = MakePathRelative(SS.saveFile, g->impFile);
                g->impFileRel = PathSepPlatformToUNIX(rel);
            } else {
                // We're not yet saved, so can't make it absolute.
                // This will only be used for display purposes, as SS.saveFile
                // is always nonempty when we are actually writing anything.
                g->impFileRel = g->impFile;
            }
        } else if(!importMap.count(g->impFile)) {
            switch(LocateImportedFileYesNoCancel(g->impFileRel, canCancel)) {
            case DIALOG_YES: {
                std::string oldImpFile = g->impFile;
                if(!GetOpenFile(g->impFile, "", SLVS_PATTERN)) {
                    if(canCancel)
                        return false;
                    break;
                } else {
                    importMap[oldImpFile] = g->impFile;
                    goto try_load_file;
                }
            }

            case DIALOG_NO:
                importMap[g->impFile] = "";
                /* Geometry will be pruned by GenerateAll(). */
                break;

            case DIALOG_CANCEL:
                return false;
            }
        } else {
            // User was already asked to and refused to locate a missing
            // imported file.
        }
    }

    return true;
}
예제 #5
0
파일: file.cpp 프로젝트: jariou/solvespace
bool SolveSpaceUI::LoadEntitiesFromFile(const std::string &filename, EntityList *le,
                                        SMesh *m, SShell *sh)
{
    SSurface srf = {};
    SCurve crv = {};

    fh = ssfopen(filename, "rb");
    if(!fh) return false;

    le->Clear();
    sv = {};

    char line[1024];
    while(fgets(line, (int)sizeof(line), fh)) {
        char *s = strchr(line, '\n');
        if(s) *s = '\0';
        // We should never get files with \r characters in them, but mailers
        // will sometimes mangle attachments.
        s = strchr(line, '\r');
        if(s) *s = '\0';

        if(*line == '\0') continue;

        char *e = strchr(line, '=');
        if(e) {
            *e = '\0';
            char *key = line, *val = e+1;
            LoadUsingTable(key, val);
        } else if(strcmp(line, "AddGroup")==0) {
            // Don't leak memory; these get allocated whether we want them
            // or not.
            sv.g.remap.Clear();
        } else if(strcmp(line, "AddParam")==0) {

        } else if(strcmp(line, "AddEntity")==0) {
            le->Add(&(sv.e));
            sv.e = {};
        } else if(strcmp(line, "AddRequest")==0) {

        } else if(strcmp(line, "AddConstraint")==0) {

        } else if(strcmp(line, "AddStyle")==0) {

        } else if(strcmp(line, VERSION_STRING)==0) {

        } else if(StrStartsWith(line, "Triangle ")) {
            STriangle tr = {};
            unsigned int rgba = 0;
            if(sscanf(line, "Triangle %x %x  "
                             "%lf %lf %lf  %lf %lf %lf  %lf %lf %lf",
                &(tr.meta.face), &rgba,
                &(tr.a.x), &(tr.a.y), &(tr.a.z),
                &(tr.b.x), &(tr.b.y), &(tr.b.z),
                &(tr.c.x), &(tr.c.y), &(tr.c.z)) != 11) {
                oops();
            }
            tr.meta.color = RgbaColor::FromPackedInt((uint32_t)rgba);
            m->AddTriangle(&tr);
        } else if(StrStartsWith(line, "Surface ")) {
            unsigned int rgba = 0;
            if(sscanf(line, "Surface %x %x %x %d %d",
                &(srf.h.v), &rgba, &(srf.face),
                &(srf.degm), &(srf.degn)) != 5) {
                oops();
            }
            srf.color = RgbaColor::FromPackedInt((uint32_t)rgba);
        } else if(StrStartsWith(line, "SCtrl ")) {
            int i, j;
            Vector c;
            double w;
            if(sscanf(line, "SCtrl %d %d %lf %lf %lf Weight %lf",
                                &i, &j, &(c.x), &(c.y), &(c.z), &w) != 6)
            {
                oops();
            }
            srf.ctrl[i][j] = c;
            srf.weight[i][j] = w;
        } else if(StrStartsWith(line, "TrimBy ")) {
            STrimBy stb = {};
            int backwards;
            if(sscanf(line, "TrimBy %x %d  %lf %lf %lf  %lf %lf %lf",
                &(stb.curve.v), &backwards,
                &(stb.start.x), &(stb.start.y), &(stb.start.z),
                &(stb.finish.x), &(stb.finish.y), &(stb.finish.z)) != 8)
            {
                oops();
            }
            stb.backwards = (backwards != 0);
            srf.trim.Add(&stb);
        } else if(strcmp(line, "AddSurface")==0) {
            sh->surface.Add(&srf);
            srf = {};
        } else if(StrStartsWith(line, "Curve ")) {
            int isExact;
            if(sscanf(line, "Curve %x %d %d %x %x",
                &(crv.h.v),
                &(isExact),
                &(crv.exact.deg),
                &(crv.surfA.v), &(crv.surfB.v)) != 5)
            {
                oops();
            }
            crv.isExact = (isExact != 0);
        } else if(StrStartsWith(line, "CCtrl ")) {
            int i;
            Vector c;
            double w;
            if(sscanf(line, "CCtrl %d %lf %lf %lf Weight %lf",
                                &i, &(c.x), &(c.y), &(c.z), &w) != 5)
            {
                oops();
            }
            crv.exact.ctrl[i] = c;
            crv.exact.weight[i] = w;
        } else if(StrStartsWith(line, "CurvePt ")) {
            SCurvePt scpt;
            int vertex;
            if(sscanf(line, "CurvePt %d %lf %lf %lf",
                &vertex,
                &(scpt.p.x), &(scpt.p.y), &(scpt.p.z)) != 4)
            {
                oops();
            }
            scpt.vertex = (vertex != 0);
            crv.pts.Add(&scpt);
        } else if(strcmp(line, "AddCurve")==0) {
            sh->curve.Add(&crv);
            crv = {};
        } else {
            oops();
        }
    }

    fclose(fh);
    return true;
}
예제 #6
0
파일: file.cpp 프로젝트: jariou/solvespace
bool SolveSpaceUI::LoadFromFile(const std::string &filename) {
    allConsistent = false;
    fileLoadError = false;

    fh = ssfopen(filename, "rb");
    if(!fh) {
        Error("Couldn't read from file '%s'", filename.c_str());
        return false;
    }

    ClearExisting();

    sv = {};
    sv.g.scale = 1; // default is 1, not 0; so legacy files need this
    Style::FillDefaultStyle(&sv.s);

    char line[1024];
    while(fgets(line, (int)sizeof(line), fh)) {
        char *s = strchr(line, '\n');
        if(s) *s = '\0';
        // We should never get files with \r characters in them, but mailers
        // will sometimes mangle attachments.
        s = strchr(line, '\r');
        if(s) *s = '\0';

        if(*line == '\0') continue;

        char *e = strchr(line, '=');
        if(e) {
            *e = '\0';
            char *key = line, *val = e+1;
            LoadUsingTable(key, val);
        } else if(strcmp(line, "AddGroup")==0) {
            // legacy files have a spurious dependency between imported groups
            // and their parent groups, remove
            if(sv.g.type == Group::IMPORTED)
                sv.g.opA.v = 0;

            SK.group.Add(&(sv.g));
            sv.g = {};
            sv.g.scale = 1; // default is 1, not 0; so legacy files need this
        } else if(strcmp(line, "AddParam")==0) {
            // params are regenerated, but we want to preload the values
            // for initial guesses
            SK.param.Add(&(sv.p));
            sv.p = {};
        } else if(strcmp(line, "AddEntity")==0) {
            // entities are regenerated
        } else if(strcmp(line, "AddRequest")==0) {
            SK.request.Add(&(sv.r));
            sv.r = {};
        } else if(strcmp(line, "AddConstraint")==0) {
            SK.constraint.Add(&(sv.c));
            sv.c = {};
        } else if(strcmp(line, "AddStyle")==0) {
            SK.style.Add(&(sv.s));
            sv.s = {};
            Style::FillDefaultStyle(&sv.s);
        } else if(strcmp(line, VERSION_STRING)==0) {
            // do nothing, version string
        } else if(StrStartsWith(line, "Triangle ")      ||
                  StrStartsWith(line, "Surface ")       ||
                  StrStartsWith(line, "SCtrl ")         ||
                  StrStartsWith(line, "TrimBy ")        ||
                  StrStartsWith(line, "Curve ")         ||
                  StrStartsWith(line, "CCtrl ")         ||
                  StrStartsWith(line, "CurvePt ")       ||
                  strcmp(line, "AddSurface")==0         ||
                  strcmp(line, "AddCurve")==0)
        {
            // ignore the mesh or shell, since we regenerate that
        } else {
            fileLoadError = true;
        }
    }

    fclose(fh);

    if(fileLoadError) {
        Error("Unrecognized data in file. This file may be corrupt, or "
              "from a new version of the program.");
        // At least leave the program in a non-crashing state.
        if(SK.group.n == 0) {
            NewFile();
        }
    }

    return true;
}
예제 #7
0
파일: file.cpp 프로젝트: jariou/solvespace
bool SolveSpaceUI::SaveToFile(const std::string &filename) {
    // Make sure all the entities are regenerated up to date, since they
    // will be exported. We reload the imported files because that rewrites
    // the impFileRel for our possibly-new filename.
    SS.ScheduleShowTW();
    SS.ReloadAllImported();
    SS.GenerateAll(SolveSpaceUI::GENERATE_ALL);

    fh = ssfopen(filename, "wb");
    if(!fh) {
        Error("Couldn't write to file '%s'", filename.c_str());
        return false;
    }

    fprintf(fh, "%s\n\n\n", VERSION_STRING);

    int i, j;
    for(i = 0; i < SK.group.n; i++) {
        sv.g = SK.group.elem[i];
        SaveUsingTable('g');
        fprintf(fh, "AddGroup\n\n");
    }

    for(i = 0; i < SK.param.n; i++) {
        sv.p = SK.param.elem[i];
        SaveUsingTable('p');
        fprintf(fh, "AddParam\n\n");
    }

    for(i = 0; i < SK.request.n; i++) {
        sv.r = SK.request.elem[i];
        SaveUsingTable('r');
        fprintf(fh, "AddRequest\n\n");
    }

    for(i = 0; i < SK.entity.n; i++) {
        (SK.entity.elem[i]).CalculateNumerical(true);
        sv.e = SK.entity.elem[i];
        SaveUsingTable('e');
        fprintf(fh, "AddEntity\n\n");
    }

    for(i = 0; i < SK.constraint.n; i++) {
        sv.c = SK.constraint.elem[i];
        SaveUsingTable('c');
        fprintf(fh, "AddConstraint\n\n");
    }

    for(i = 0; i < SK.style.n; i++) {
        sv.s = SK.style.elem[i];
        if(sv.s.h.v >= Style::FIRST_CUSTOM) {
            SaveUsingTable('s');
            fprintf(fh, "AddStyle\n\n");
        }
    }

    // A group will have either a mesh or a shell, but not both; but the code
    // to print either of those just does nothing if the mesh/shell is empty.

    Group *g = SK.GetGroup(SK.groupOrder.elem[SK.groupOrder.n - 1]);
    SMesh *m = &g->runningMesh;
    for(i = 0; i < m->l.n; i++) {
        STriangle *tr = &(m->l.elem[i]);
        fprintf(fh, "Triangle %08x %08x "
                "%.20f %.20f %.20f  %.20f %.20f %.20f  %.20f %.20f %.20f\n",
            tr->meta.face, tr->meta.color.ToPackedInt(),
            CO(tr->a), CO(tr->b), CO(tr->c));
    }

    SShell *s = &g->runningShell;
    SSurface *srf;
    for(srf = s->surface.First(); srf; srf = s->surface.NextAfter(srf)) {
        fprintf(fh, "Surface %08x %08x %08x %d %d\n",
            srf->h.v, srf->color.ToPackedInt(), srf->face, srf->degm, srf->degn);
        for(i = 0; i <= srf->degm; i++) {
            for(j = 0; j <= srf->degn; j++) {
                fprintf(fh, "SCtrl %d %d %.20f %.20f %.20f Weight %20.20f\n",
                    i, j, CO(srf->ctrl[i][j]), srf->weight[i][j]);
            }
        }

        STrimBy *stb;
        for(stb = srf->trim.First(); stb; stb = srf->trim.NextAfter(stb)) {
            fprintf(fh, "TrimBy %08x %d %.20f %.20f %.20f  %.20f %.20f %.20f\n",
                stb->curve.v, stb->backwards ? 1 : 0,
                CO(stb->start), CO(stb->finish));
        }

        fprintf(fh, "AddSurface\n");
    }
    SCurve *sc;
    for(sc = s->curve.First(); sc; sc = s->curve.NextAfter(sc)) {
        fprintf(fh, "Curve %08x %d %d %08x %08x\n",
            sc->h.v,
            sc->isExact ? 1 : 0, sc->exact.deg,
            sc->surfA.v, sc->surfB.v);

        if(sc->isExact) {
            for(i = 0; i <= sc->exact.deg; i++) {
                fprintf(fh, "CCtrl %d %.20f %.20f %.20f Weight %.20f\n",
                    i, CO(sc->exact.ctrl[i]), sc->exact.weight[i]);
            }
        }
        SCurvePt *scpt;
        for(scpt = sc->pts.First(); scpt; scpt = sc->pts.NextAfter(scpt)) {
            fprintf(fh, "CurvePt %d %.20f %.20f %.20f\n",
                scpt->vertex ? 1 : 0, CO(scpt->p));
        }

        fprintf(fh, "AddCurve\n");
    }

    fclose(fh);

    return true;
}