
introduced by the bsp routines. It's usually, though not always, possible to generate a watertight mesh. The occasions where it's not look ugly, floating point issues, no quick fix. And use those to generate a list of edges where two different faces meet, which I can emphasize for cosmetic reasons (and some UI to specify whether to do that, and with what color). And make the right mouse button rotate the model, since that was previously doing nothing. [git-p4: depot-paths = "//depot/solvespace/": change = 1821]
631 lines
18 KiB
C++
631 lines
18 KiB
C++
#include "solvespace.h"
|
|
|
|
void SMesh::Clear(void) {
|
|
l.Clear();
|
|
}
|
|
|
|
void SMesh::AddTriangle(STriMeta meta, Vector n, Vector a, Vector b, Vector c) {
|
|
Vector ab = b.Minus(a), bc = c.Minus(b);
|
|
Vector np = ab.Cross(bc);
|
|
if(np.Magnitude() < 1e-10) {
|
|
// ugh; gl sometimes tesselates to collinear triangles
|
|
return;
|
|
}
|
|
if(np.Dot(n) > 0) {
|
|
AddTriangle(meta, a, b, c);
|
|
} else {
|
|
AddTriangle(meta, c, b, a);
|
|
}
|
|
}
|
|
void SMesh::AddTriangle(STriMeta meta, Vector a, Vector b, Vector c) {
|
|
STriangle t; ZERO(&t);
|
|
t.meta = meta;
|
|
t.a = a;
|
|
t.b = b;
|
|
t.c = c;
|
|
AddTriangle(&t);
|
|
}
|
|
void SMesh::AddTriangle(STriangle *st) {
|
|
l.Add(st);
|
|
}
|
|
|
|
void SMesh::DoBounding(Vector v, Vector *vmax, Vector *vmin) {
|
|
vmax->x = max(vmax->x, v.x);
|
|
vmax->y = max(vmax->y, v.y);
|
|
vmax->z = max(vmax->z, v.z);
|
|
|
|
vmin->x = min(vmin->x, v.x);
|
|
vmin->y = min(vmin->y, v.y);
|
|
vmin->z = min(vmin->z, v.z);
|
|
}
|
|
void SMesh::GetBounding(Vector *vmax, Vector *vmin) {
|
|
int i;
|
|
*vmin = Vector::From( 1e12, 1e12, 1e12);
|
|
*vmax = Vector::From(-1e12, -1e12, -1e12);
|
|
for(i = 0; i < l.n; i++) {
|
|
STriangle *st = &(l.elem[i]);
|
|
DoBounding(st->a, vmax, vmin);
|
|
DoBounding(st->b, vmax, vmin);
|
|
DoBounding(st->c, vmax, vmin);
|
|
}
|
|
}
|
|
|
|
void SMesh::Simplify(int start) {
|
|
#define MAX_TRIANGLES 2000
|
|
if(l.n - start > MAX_TRIANGLES) oops();
|
|
|
|
STriMeta meta = l.elem[start].meta;
|
|
|
|
STriangle tout[MAX_TRIANGLES];
|
|
int toutc = 0;
|
|
|
|
Vector n, conv[MAX_TRIANGLES*3];
|
|
int convc = 0;
|
|
|
|
int start0 = start;
|
|
|
|
int i, j;
|
|
for(i = start; i < l.n; i++) {
|
|
STriangle *tr = &(l.elem[i]);
|
|
if(tr->MinAltitude() < LENGTH_EPS) {
|
|
tr->tag = 1;
|
|
} else {
|
|
tr->tag = 0;
|
|
}
|
|
}
|
|
|
|
for(;;) {
|
|
bool didAdd;
|
|
convc = 0;
|
|
for(i = start; i < l.n; i++) {
|
|
STriangle *tr = &(l.elem[i]);
|
|
if(tr->tag) continue;
|
|
|
|
tr->tag = 1;
|
|
n = (tr->Normal()).WithMagnitude(1);
|
|
conv[convc++] = tr->a;
|
|
conv[convc++] = tr->b;
|
|
conv[convc++] = tr->c;
|
|
|
|
start = i+1;
|
|
break;
|
|
}
|
|
if(i >= l.n) break;
|
|
|
|
do {
|
|
didAdd = false;
|
|
|
|
for(j = 0; j < convc; j++) {
|
|
Vector a = conv[WRAP((j-1), convc)],
|
|
b = conv[j],
|
|
d = conv[WRAP((j+1), convc)],
|
|
e = conv[WRAP((j+2), convc)];
|
|
|
|
Vector c;
|
|
for(i = start; i < l.n; i++) {
|
|
STriangle *tr = &(l.elem[i]);
|
|
if(tr->tag) continue;
|
|
|
|
if((tr->a).Equals(d) && (tr->b).Equals(b)) {
|
|
c = tr->c;
|
|
} else if((tr->b).Equals(d) && (tr->c).Equals(b)) {
|
|
c = tr->a;
|
|
} else if((tr->c).Equals(d) && (tr->a).Equals(b)) {
|
|
c = tr->b;
|
|
} else {
|
|
continue;
|
|
}
|
|
// The vertex at C must be convex; but the others must
|
|
// be tested
|
|
Vector ab = b.Minus(a);
|
|
Vector bc = c.Minus(b);
|
|
Vector cd = d.Minus(c);
|
|
Vector de = e.Minus(d);
|
|
|
|
double bDot = (ab.Cross(bc)).Dot(n);
|
|
double dDot = (cd.Cross(de)).Dot(n);
|
|
|
|
bDot /= min(ab.Magnitude(), bc.Magnitude());
|
|
dDot /= min(cd.Magnitude(), de.Magnitude());
|
|
|
|
if(fabs(bDot) < LENGTH_EPS && fabs(dDot) < LENGTH_EPS) {
|
|
conv[WRAP((j+1), convc)] = c;
|
|
// and remove the vertex at j, which is a dup
|
|
memmove(conv+j, conv+j+1,
|
|
(convc - j - 1)*sizeof(conv[0]));
|
|
convc--;
|
|
} else if(fabs(bDot) < LENGTH_EPS && dDot > 0) {
|
|
conv[j] = c;
|
|
} else if(fabs(dDot) < LENGTH_EPS && bDot > 0) {
|
|
conv[WRAP((j+1), convc)] = c;
|
|
} else if(bDot > 0 && dDot > 0) {
|
|
// conv[j] is unchanged, conv[j+1] goes to [j+2]
|
|
memmove(conv+j+2, conv+j+1,
|
|
(convc - j - 1)*sizeof(conv[0]));
|
|
conv[j+1] = c;
|
|
convc++;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
didAdd = true;
|
|
tr->tag = 1;
|
|
break;
|
|
}
|
|
}
|
|
} while(didAdd);
|
|
|
|
// I need to debug why this is required; sometimes the above code
|
|
// still generates a convex polygon
|
|
for(i = 0; i < convc; i++) {
|
|
Vector a = conv[WRAP((i-1), convc)],
|
|
b = conv[i],
|
|
c = conv[WRAP((i+1), convc)];
|
|
Vector ab = b.Minus(a);
|
|
Vector bc = c.Minus(b);
|
|
double bDot = (ab.Cross(bc)).Dot(n);
|
|
bDot /= min(ab.Magnitude(), bc.Magnitude());
|
|
|
|
if(bDot < 0) oops();
|
|
}
|
|
|
|
for(i = 0; i < convc - 2; i++) {
|
|
STriangle tr = STriangle::From(meta, conv[0], conv[i+1], conv[i+2]);
|
|
if(tr.MinAltitude() > LENGTH_EPS) {
|
|
tout[toutc++] = tr;
|
|
}
|
|
}
|
|
}
|
|
|
|
l.n = start0;
|
|
for(i = 0; i < toutc; i++) {
|
|
AddTriangle(&(tout[i]));
|
|
}
|
|
}
|
|
|
|
void SMesh::AddAgainstBsp(SMesh *srcm, SBsp3 *bsp3) {
|
|
int i;
|
|
|
|
for(i = 0; i < srcm->l.n; i++) {
|
|
STriangle *st = &(srcm->l.elem[i]);
|
|
int pn = l.n;
|
|
atLeastOneDiscarded = false;
|
|
bsp3->Insert(st, this);
|
|
if(!atLeastOneDiscarded && (l.n != (pn+1))) {
|
|
l.n = pn;
|
|
if(flipNormal) {
|
|
AddTriangle(st->meta, st->c, st->b, st->a);
|
|
} else {
|
|
AddTriangle(st->meta, st->a, st->b, st->c);
|
|
}
|
|
}
|
|
if(l.n - pn > 1) {
|
|
Simplify(pn);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SMesh::MakeFromUnion(SMesh *a, SMesh *b) {
|
|
SBsp3 *bspa = SBsp3::FromMesh(a);
|
|
SBsp3 *bspb = SBsp3::FromMesh(b);
|
|
|
|
flipNormal = false;
|
|
keepCoplanar = false;
|
|
AddAgainstBsp(b, bspa);
|
|
|
|
flipNormal = false;
|
|
keepCoplanar = true;
|
|
AddAgainstBsp(a, bspb);
|
|
}
|
|
|
|
void SMesh::MakeFromDifference(SMesh *a, SMesh *b) {
|
|
SBsp3 *bspa = SBsp3::FromMesh(a);
|
|
SBsp3 *bspb = SBsp3::FromMesh(b);
|
|
|
|
flipNormal = true;
|
|
keepCoplanar = true;
|
|
AddAgainstBsp(b, bspa);
|
|
|
|
flipNormal = false;
|
|
keepCoplanar = false;
|
|
AddAgainstBsp(a, bspb);
|
|
}
|
|
|
|
bool SMesh::MakeFromInterferenceCheck(SMesh *srca, SMesh *srcb, SMesh *error) {
|
|
SBsp3 *bspa = SBsp3::FromMesh(srca);
|
|
SBsp3 *bspb = SBsp3::FromMesh(srcb);
|
|
|
|
error->Clear();
|
|
error->flipNormal = true;
|
|
error->keepCoplanar = false;
|
|
|
|
error->AddAgainstBsp(srcb, bspa);
|
|
error->AddAgainstBsp(srca, bspb);
|
|
// Now we have a list of all the triangles (or fragments thereof) from
|
|
// A that lie inside B, or vice versa. That's the interference, and
|
|
// we report it so that it can be flagged.
|
|
|
|
// But as far as the actual model, we just copy everything over.
|
|
int i;
|
|
for(i = 0; i < srca->l.n; i++) {
|
|
AddTriangle(&(srca->l.elem[i]));
|
|
}
|
|
for(i = 0; i < srcb->l.n; i++) {
|
|
AddTriangle(&(srcb->l.elem[i]));
|
|
}
|
|
return (error->l.n == 0);
|
|
}
|
|
|
|
void SMesh::MakeFromCopy(SMesh *a) {
|
|
int i;
|
|
for(i = 0; i < a->l.n; i++) {
|
|
AddTriangle(&(a->l.elem[i]));
|
|
}
|
|
}
|
|
|
|
DWORD SMesh::FirstIntersectionWith(Point2d mp) {
|
|
Vector p0 = Vector::From(mp.x, mp.y, 0);
|
|
Vector gn = Vector::From(0, 0, 1);
|
|
|
|
double maxT = -1e12;
|
|
DWORD face = 0;
|
|
|
|
int i;
|
|
for(i = 0; i < l.n; i++) {
|
|
STriangle tr = l.elem[i];
|
|
tr.a = SS.GW.ProjectPoint3(tr.a);
|
|
tr.b = SS.GW.ProjectPoint3(tr.b);
|
|
tr.c = SS.GW.ProjectPoint3(tr.c);
|
|
|
|
Vector n = tr.Normal();
|
|
|
|
if(n.Dot(gn) < LENGTH_EPS) continue; // back-facing or on edge
|
|
|
|
if(tr.ContainsPointProjd(gn, p0)) {
|
|
// Let our line have the form r(t) = p0 + gn*t
|
|
double t = -(n.Dot((tr.a).Minus(p0)))/(n.Dot(gn));
|
|
if(t > maxT) {
|
|
maxT = t;
|
|
face = tr.meta.face;
|
|
}
|
|
}
|
|
}
|
|
return face;
|
|
}
|
|
|
|
#define KDTREE_EPS (20*LENGTH_EPS) // nice and sloppy
|
|
|
|
STriangleLl *STriangleLl::Alloc(void)
|
|
{ return (STriangleLl *)AllocTemporary(sizeof(STriangleLl)); }
|
|
SKdNode *SKdNode::Alloc(void)
|
|
{ return (SKdNode *)AllocTemporary(sizeof(SKdNode)); }
|
|
|
|
SKdNode *SKdNode::From(SMesh *m) {
|
|
int i;
|
|
STriangle *tra = (STriangle *)AllocTemporary((m->l.n) * sizeof(*tra));
|
|
|
|
for(i = 0; i < m->l.n; i++) {
|
|
tra[i] = m->l.elem[i];
|
|
}
|
|
|
|
srand(0);
|
|
int n = m->l.n;
|
|
while(n > 1) {
|
|
int k = rand() % n;
|
|
n--;
|
|
SWAP(STriangle, tra[k], tra[n]);
|
|
}
|
|
|
|
STriangleLl *tll = NULL;
|
|
for(i = 0; i < m->l.n; i++) {
|
|
STriangleLl *tn = STriangleLl::Alloc();
|
|
tn->tri = &(tra[i]);
|
|
tn->next = tll;
|
|
tll = tn;
|
|
}
|
|
|
|
return SKdNode::From(tll, 0);
|
|
}
|
|
|
|
SKdNode *SKdNode::From(STriangleLl *tll, int which) {
|
|
SKdNode *ret = Alloc();
|
|
|
|
if(!tll) goto leaf;
|
|
|
|
int i;
|
|
int gtc[3] = { 0, 0, 0 }, ltc[3] = { 0, 0, 0 }, allc = 0;
|
|
double badness[3];
|
|
double split[3];
|
|
for(i = 0; i < 3; i++) {
|
|
int tcnt = 0;
|
|
STriangleLl *ll;
|
|
for(ll = tll; ll; ll = ll->next) {
|
|
STriangle *tr = ll->tri;
|
|
split[i] += (ll->tri->a).Element(i);
|
|
split[i] += (ll->tri->b).Element(i);
|
|
split[i] += (ll->tri->c).Element(i);
|
|
tcnt++;
|
|
}
|
|
split[i] /= (tcnt*3);
|
|
|
|
for(ll = tll; ll; ll = ll->next) {
|
|
STriangle *tr = ll->tri;
|
|
|
|
double a = (tr->a).Element(i),
|
|
b = (tr->b).Element(i),
|
|
c = (tr->c).Element(i);
|
|
|
|
if(a < split[i] + KDTREE_EPS ||
|
|
b < split[i] + KDTREE_EPS ||
|
|
c < split[i] + KDTREE_EPS)
|
|
{
|
|
ltc[i]++;
|
|
}
|
|
if(a > split[i] - KDTREE_EPS ||
|
|
b > split[i] - KDTREE_EPS ||
|
|
c > split[i] - KDTREE_EPS)
|
|
{
|
|
gtc[i]++;
|
|
}
|
|
if(i == 0) allc++;
|
|
}
|
|
badness[i] = pow((double)ltc[i], 4) + pow((double)gtc[i], 4);
|
|
}
|
|
if(badness[0] < badness[1] && badness[0] < badness[2]) {
|
|
which = 0;
|
|
} else if(badness[1] < badness[2]) {
|
|
which = 1;
|
|
} else {
|
|
which = 2;
|
|
}
|
|
|
|
if(allc < 10) goto leaf;
|
|
if(allc == gtc[which] || allc == ltc[which]) goto leaf;
|
|
|
|
STriangleLl *ll;
|
|
STriangleLl *lgt = NULL, *llt = NULL;
|
|
for(ll = tll; ll; ll = ll->next) {
|
|
STriangle *tr = ll->tri;
|
|
|
|
double a = (tr->a).Element(which),
|
|
b = (tr->b).Element(which),
|
|
c = (tr->c).Element(which);
|
|
|
|
if(a < split[which] + KDTREE_EPS ||
|
|
b < split[which] + KDTREE_EPS ||
|
|
c < split[which] + KDTREE_EPS)
|
|
{
|
|
STriangleLl *n = STriangleLl::Alloc();
|
|
*n = *ll;
|
|
n->next = llt;
|
|
llt = n;
|
|
}
|
|
if(a > split[which] - KDTREE_EPS ||
|
|
b > split[which] - KDTREE_EPS ||
|
|
c > split[which] - KDTREE_EPS)
|
|
{
|
|
STriangleLl *n = STriangleLl::Alloc();
|
|
*n = *ll;
|
|
n->next = lgt;
|
|
lgt = n;
|
|
}
|
|
}
|
|
|
|
ret->which = which;
|
|
ret->c = split[which];
|
|
ret->gt = SKdNode::From(lgt, (which + 1) % 3);
|
|
ret->lt = SKdNode::From(llt, (which + 1) % 3);
|
|
return ret;
|
|
|
|
leaf:
|
|
// dbp("leaf: allc=%d gtc=%d ltc=%d which=%d", allc, gtc[which], ltc[which], which);
|
|
ret->tris = tll;
|
|
return ret;
|
|
}
|
|
|
|
void SKdNode::ClearTags(void) {
|
|
if(gt && lt) {
|
|
gt->ClearTags();
|
|
lt->ClearTags();
|
|
} else {
|
|
STriangleLl *ll;
|
|
for(ll = tris; ll; ll = ll->next) {
|
|
ll->tri->tag = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SKdNode::AddTriangle(STriangle *tr) {
|
|
if(gt && lt) {
|
|
double ta = (tr->a).Element(which),
|
|
tb = (tr->b).Element(which),
|
|
tc = (tr->c).Element(which);
|
|
if(ta < c + KDTREE_EPS ||
|
|
tb < c + KDTREE_EPS ||
|
|
tc < c + KDTREE_EPS)
|
|
{
|
|
lt->AddTriangle(tr);
|
|
}
|
|
if(ta > c - KDTREE_EPS ||
|
|
tb > c - KDTREE_EPS ||
|
|
tc > c - KDTREE_EPS)
|
|
{
|
|
gt->AddTriangle(tr);
|
|
}
|
|
} else {
|
|
STriangleLl *tn = STriangleLl::Alloc();
|
|
tn->tri = tr;
|
|
tn->next = tris;
|
|
tris = tn;
|
|
}
|
|
}
|
|
|
|
void SKdNode::MakeMeshInto(SMesh *m) {
|
|
if(gt) gt->MakeMeshInto(m);
|
|
if(lt) lt->MakeMeshInto(m);
|
|
|
|
STriangleLl *ll;
|
|
for(ll = tris; ll; ll = ll->next) {
|
|
if(ll->tri->tag) continue;
|
|
|
|
m->AddTriangle(ll->tri);
|
|
ll->tri->tag = 1;
|
|
}
|
|
}
|
|
|
|
void SKdNode::SnapToVertex(Vector v, SMesh *extras) {
|
|
if(gt && lt) {
|
|
double vc = v.Element(which);
|
|
if(vc < c + KDTREE_EPS) {
|
|
lt->SnapToVertex(v, extras);
|
|
}
|
|
if(vc > c - KDTREE_EPS) {
|
|
gt->SnapToVertex(v, extras);
|
|
}
|
|
// Nothing bad happens if the triangle to be split appears in both
|
|
// branches; the first call will split the triangle, so that the
|
|
// second call will do nothing, because the modified triangle will
|
|
// already contain v
|
|
} else {
|
|
STriangleLl *ll;
|
|
for(ll = tris; ll; ll = ll->next) {
|
|
STriangle *tr = ll->tri;
|
|
|
|
// Do a cheap bbox test first
|
|
int k;
|
|
bool mightHit = true;
|
|
|
|
for(k = 0; k < 3; k++) {
|
|
if((tr->a).Element(k) < v.Element(k) - KDTREE_EPS &&
|
|
(tr->b).Element(k) < v.Element(k) - KDTREE_EPS &&
|
|
(tr->c).Element(k) < v.Element(k) - KDTREE_EPS)
|
|
{
|
|
mightHit = false;
|
|
break;
|
|
}
|
|
if((tr->a).Element(k) > v.Element(k) + KDTREE_EPS &&
|
|
(tr->b).Element(k) > v.Element(k) + KDTREE_EPS &&
|
|
(tr->c).Element(k) > v.Element(k) + KDTREE_EPS)
|
|
{
|
|
mightHit = false;
|
|
break;
|
|
}
|
|
}
|
|
if(!mightHit) continue;
|
|
|
|
if(tr->a.Equals(v)) { tr->a = v; continue; }
|
|
if(tr->b.Equals(v)) { tr->b = v; continue; }
|
|
if(tr->c.Equals(v)) { tr->c = v; continue; }
|
|
|
|
if(v.OnLineSegment(tr->a, tr->b)) {
|
|
STriangle nt = STriangle::From(tr->meta, tr->a, v, tr->c);
|
|
extras->AddTriangle(&nt);
|
|
tr->a = v;
|
|
continue;
|
|
}
|
|
if(v.OnLineSegment(tr->b, tr->c)) {
|
|
STriangle nt = STriangle::From(tr->meta, tr->b, v, tr->a);
|
|
extras->AddTriangle(&nt);
|
|
tr->b = v;
|
|
continue;
|
|
}
|
|
if(v.OnLineSegment(tr->c, tr->a)) {
|
|
STriangle nt = STriangle::From(tr->meta, tr->c, v, tr->b);
|
|
extras->AddTriangle(&nt);
|
|
tr->c = v;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SKdNode::SnapToMesh(SMesh *m) {
|
|
int i, j, k;
|
|
for(i = 0; i < m->l.n; i++) {
|
|
STriangle *tr = &(m->l.elem[i]);
|
|
for(j = 0; j < 3; j++) {
|
|
Vector v = ((j == 0) ? tr->a :
|
|
((j == 1) ? tr->b :
|
|
tr->c));
|
|
|
|
SMesh extra;
|
|
ZERO(&extra);
|
|
SnapToVertex(v, &extra);
|
|
|
|
for(k = 0; k < extra.l.n; k++) {
|
|
STriangle *tra = (STriangle *)AllocTemporary(sizeof(*tra));
|
|
*tra = extra.l.elem[k];
|
|
AddTriangle(tra);
|
|
}
|
|
extra.Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SKdNode::FindEdgeOn(Vector a, Vector b, int *n, int *nOther,
|
|
STriMeta m, int cnt)
|
|
{
|
|
if(gt && lt) {
|
|
double ac = a.Element(which),
|
|
bc = b.Element(which);
|
|
if(ac < c + KDTREE_EPS ||
|
|
bc < c + KDTREE_EPS)
|
|
{
|
|
lt->FindEdgeOn(a, b, n, nOther, m, cnt);
|
|
}
|
|
if(ac > c - KDTREE_EPS ||
|
|
bc > c - KDTREE_EPS)
|
|
{
|
|
gt->FindEdgeOn(a, b, n, nOther, m, cnt);
|
|
}
|
|
} else {
|
|
STriangleLl *ll;
|
|
for(ll = tris; ll; ll = ll->next) {
|
|
STriangle *tr = ll->tri;
|
|
|
|
if(tr->tag == cnt) continue;
|
|
|
|
if((a.EqualsExactly(tr->b) && b.EqualsExactly(tr->a)) ||
|
|
(a.EqualsExactly(tr->c) && b.EqualsExactly(tr->b)) ||
|
|
(a.EqualsExactly(tr->a) && b.EqualsExactly(tr->c)))
|
|
{
|
|
(*n)++;
|
|
if(tr->meta.face != m.face) (*nOther)++;
|
|
}
|
|
|
|
tr->tag = cnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SKdNode::MakeEdgesToEmphasizeInto(SEdgeList *sel) {
|
|
SMesh m;
|
|
ZERO(&m);
|
|
ClearTags();
|
|
MakeMeshInto(&m);
|
|
|
|
int cnt = 1234;
|
|
int i, j;
|
|
for(i = 0; i < m.l.n; i++) {
|
|
STriangle *tr = &(m.l.elem[i]);
|
|
|
|
for(j = 0; j < 3; j++) {
|
|
Vector a = (j == 0) ? tr->a : ((j == 1) ? tr->b : tr->c);
|
|
Vector b = (j == 0) ? tr->b : ((j == 1) ? tr->c : tr->a);
|
|
|
|
int n = 0, nOther = 0;
|
|
FindEdgeOn(a, b, &n, &nOther, tr->meta, cnt++);
|
|
if(n != 1) {
|
|
dbp("hanging edge: n=%d (%.3f %.3f %.3f) (%.3f %.3f %.3f)",
|
|
n, CO(a), CO(b));
|
|
}
|
|
if(nOther > 0) {
|
|
sel->AddEdge(a, b);
|
|
}
|
|
}
|
|
}
|
|
|
|
m.Clear();
|
|
}
|
|
|