quakeforge/tools/qfbsp/source/solidbsp.c

713 lines
16 KiB
C
Raw Normal View History

2002-09-19 18:51:19 +00:00
/*
Copyright (C) 1996-1997 Id Software, Inc.
2002-09-19 18:51:19 +00:00
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
2002-09-19 18:51:19 +00:00
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
2002-09-19 18:51:19 +00:00
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2002-09-19 18:51:19 +00:00
See file, 'COPYING', for details.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#include <stdlib.h>
#include "QF/sys.h"
#include "tools/qfbsp/include/brush.h"
#include "tools/qfbsp/include/csg4.h"
#include "tools/qfbsp/include/bsp5.h"
#include "tools/qfbsp/include/draw.h"
#include "tools/qfbsp/include/solidbsp.h"
#include "tools/qfbsp/include/surfaces.h"
/** \addtogroup qfbsp_solidbsp
*/
//@{
2002-09-19 17:14:23 +00:00
int leaffaces;
int nodefaces;
int splitnodes;
2002-09-19 17:14:23 +00:00
int c_solid, c_empty, c_water;
bool usemidsplit;
2010-08-30 08:34:41 +00:00
/** Determine on which side of the plane a face is.
\param in The face.
\param split The plane.
\return <dl>
<dt>SIDE_FRONT</dt>
<dd>The face is in front of or on the plane.</dd>
<dt>SIDE_BACK</dt>
<dd>The face is behind or on the plane.</dd>
<dt>SIDE_ON</dt>
<dd>The face is on or cut by the plane.</dd>
</dl>
*/
static int
2010-09-02 04:01:45 +00:00
FaceSide (const face_t *in, const plane_t *split)
{
2002-09-19 18:51:19 +00:00
int frontcount, backcount, i;
2002-09-19 17:14:23 +00:00
vec_t dot;
2010-09-02 04:01:45 +00:00
const vec_t *p;
const winding_t *inp = in->points;
2002-09-19 17:14:23 +00:00
frontcount = backcount = 0;
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// axial planes are fast
if (split->type < 3) {
2003-09-08 03:00:53 +00:00
for (i = 0, p = inp->points[0] + split->type; i < inp->numpoints;
2002-09-19 17:14:23 +00:00
i++, p += 3) {
if (*p > split->dist + ON_EPSILON) {
if (backcount)
return SIDE_ON;
frontcount = 1;
2002-09-19 17:14:23 +00:00
} else if (*p < split->dist - ON_EPSILON) {
if (frontcount)
return SIDE_ON;
backcount = 1;
}
}
} else {
2002-09-23 16:27:17 +00:00
// sloping planes take longer
2003-09-08 03:00:53 +00:00
for (i = 0, p = inp->points[0]; i < inp->numpoints; i++, p += 3) {
dot = DotProduct (p, split->normal);
dot -= split->dist;
2002-09-19 17:14:23 +00:00
if (dot > ON_EPSILON) {
if (backcount)
return SIDE_ON;
frontcount = 1;
2002-09-19 17:14:23 +00:00
} else if (dot < -ON_EPSILON) {
if (frontcount)
return SIDE_ON;
backcount = 1;
}
}
}
2002-09-19 17:14:23 +00:00
if (!frontcount)
return SIDE_BACK;
if (!backcount)
return SIDE_FRONT;
2002-09-19 17:14:23 +00:00
return SIDE_ON;
}
2022-05-19 02:26:19 +00:00
/** Choose the best plane for dividing the bsp.
2010-08-30 08:34:41 +00:00
The clipping hull BSP doesn't worry about avoiding splits, so this
function tries to find the plane that gives the most even split of the
bounding volume.
\param surfaces The surface chain of the bsp.
\param mins The minimum coordinate of the boundiing box.
\param maxs The maximum coordinate of the boundiing box.
\return The chosen surface.
*/
static __attribute__((pure)) surface_t *
2010-09-02 04:01:45 +00:00
ChooseMidPlaneFromList (surface_t *surfaces,
const vec3_t mins, const vec3_t maxs)
{
2002-09-19 17:14:23 +00:00
int j, l;
2002-09-19 18:51:19 +00:00
plane_t *plane;
2002-09-19 17:14:23 +00:00
surface_t *p, *bestsurface;
vec_t bestvalue, value, dist;
2002-09-23 16:27:17 +00:00
// pick the plane that splits the least
2002-09-19 17:14:23 +00:00
bestvalue = 6 * 8192 * 8192;
bestsurface = NULL;
2002-09-19 17:14:23 +00:00
for (p = surfaces; p; p = p->next) {
if (p->onnode)
continue;
plane = &planes.a[p->planenum];
2002-09-19 17:14:23 +00:00
// check for axis aligned surfaces
l = plane->type;
if (l > PLANE_Z)
continue;
2002-09-19 17:14:23 +00:00
// calculate the split metric along axis l, smaller values are better
value = 0;
dist = plane->dist * plane->normal[l];
2002-09-19 17:14:23 +00:00
for (j = 0; j < 3; j++) {
if (j == l) {
value += (maxs[l] - dist) * (maxs[l] - dist);
value += (dist - mins[l]) * (dist - mins[l]);
} else
value += 2 * (maxs[j] - mins[j]) * (maxs[j] - mins[j]);
}
2002-09-19 17:14:23 +00:00
if (value > bestvalue)
continue;
2002-09-19 17:14:23 +00:00
// currently the best!
bestvalue = value;
bestsurface = p;
}
2002-09-19 17:14:23 +00:00
if (!bestsurface) {
for (p = surfaces; p; p = p->next)
if (!p->onnode)
2002-09-19 17:14:23 +00:00
return p; // first valid surface
Sys_Error ("ChooseMidPlaneFromList: no valid planes");
}
2002-09-19 17:14:23 +00:00
return bestsurface;
}
2010-08-30 08:34:41 +00:00
/** Choose the best plane that produces the fewest splits.
2010-08-30 08:34:41 +00:00
\param surfaces The surface chain of the bsp.
\param mins The minimum coordinate of the boundiing box.
\param maxs The maximum coordinate of the boundiing box.
\param usefloors If false, floors will not be chosen.
\param usedetail If true, the plain must have structure faces, else
the plain must not have structure faces.
\return The chosen surface, or NULL if a suitable surface could
not be found.
*/
static __attribute__((pure)) surface_t *
2010-09-02 04:01:45 +00:00
ChoosePlaneFromList (surface_t *surfaces, const vec3_t mins, const vec3_t maxs,
bool usefloors, bool usedetail)
{
2002-09-19 18:51:19 +00:00
face_t *f;
int j, k, l, ishint;
2002-09-19 18:51:19 +00:00
plane_t *plane;
2002-09-19 17:14:23 +00:00
surface_t *p, *p2, *bestsurface;
vec_t bestvalue, bestdistribution, value, dist;
2002-09-23 16:27:17 +00:00
// pick the plane that splits the least
bestvalue = 999999;
bestsurface = NULL;
bestdistribution = 9e30;
2002-09-19 17:14:23 +00:00
for (p = surfaces; p; p = p->next) {
if (p->onnode)
continue;
for (f = p->faces; f; f = f->next)
if (f->texturenum == TEX_HINT)
break;
ishint = f != 0;
if (p->has_struct && usedetail)
continue;
if (!p->has_struct && !usedetail)
continue;
plane = &planes.a[p->planenum];
k = 0;
if (!usefloors && plane->normal[2] == 1)
continue;
2002-09-19 17:14:23 +00:00
for (p2 = surfaces; p2; p2 = p2->next) {
if (p2 == p)
continue;
if (p2->onnode)
continue;
2002-09-19 17:14:23 +00:00
for (f = p2->faces; f; f = f->next) {
if (FaceSide (f, plane) == SIDE_ON) {
if (!ishint && f->texturenum == TEX_HINT)
k += 9999;
else
k++;
if (k >= bestvalue)
break;
}
2002-09-19 17:14:23 +00:00
}
if (k > bestvalue)
break;
}
if (k > bestvalue)
continue;
2002-09-19 17:14:23 +00:00
// if equal numbers, axial planes win, then decide on spatial
// subdivision
if (k < bestvalue || (k == bestvalue && plane->type < PLANE_ANYX)) {
// check for axis aligned surfaces
l = plane->type;
2002-09-19 17:14:23 +00:00
if (l <= PLANE_Z) { // axial aligned
// calculate the split metric along axis l
value = 0;
2002-09-19 17:14:23 +00:00
for (j = 0; j < 3; j++) {
if (j == l) {
dist = plane->dist * plane->normal[l];
2002-09-19 17:14:23 +00:00
value += (maxs[l] - dist) * (maxs[l] - dist);
value += (dist - mins[l]) * (dist - mins[l]);
} else
value += 2 * (maxs[j] - mins[j]) * (maxs[j] - mins[j]);
}
2002-09-19 17:14:23 +00:00
if (value > bestdistribution && k == bestvalue)
continue;
bestdistribution = value;
}
2002-09-19 17:14:23 +00:00
// currently the best!
bestvalue = k;
bestsurface = p;
}
}
return bestsurface;
}
2010-08-30 08:34:41 +00:00
/** Select a surface on which to split the group of surfaces.
2010-08-30 08:34:41 +00:00
\param surfaces The group of surfaces.
\param detail Set to 1 if the selected surface has detail.
\return The selected surface or NULL if the list can no longer
be defined (ie, a leaf).
*/
2010-08-30 07:58:40 +00:00
static surface_t *
SelectPartition (surface_t *surfaces, int *detail)
{
2002-09-19 17:14:23 +00:00
int i, j;
surface_t *p, *bestsurface;
2002-09-19 18:51:19 +00:00
vec3_t mins, maxs;
*detail = 0;
2002-09-23 16:27:17 +00:00
// count onnode surfaces
i = 0;
bestsurface = NULL;
2010-08-30 07:58:40 +00:00
for (p = surfaces; p; p = p->next) {
2002-09-19 17:14:23 +00:00
if (!p->onnode) {
i++;
bestsurface = p;
}
2010-08-30 07:58:40 +00:00
}
2002-09-19 17:14:23 +00:00
if (i == 0)
return NULL;
2002-09-19 17:14:23 +00:00
if (i == 1) {
if (!bestsurface->has_struct && !usemidsplit)
*detail = 1;
2002-09-19 17:14:23 +00:00
return bestsurface; // this is a final split
}
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// calculate a bounding box of the entire surfaceset
2002-09-19 17:14:23 +00:00
for (i = 0; i < 3; i++) {
mins[i] = BOGUS_RANGE;
maxs[i] = -BOGUS_RANGE;
}
2010-08-30 07:58:40 +00:00
for (p = surfaces; p; p = p->next) {
2002-09-19 17:14:23 +00:00
for (j = 0; j < 3; j++) {
if (p->mins[j] < mins[j])
mins[j] = p->mins[j];
if (p->maxs[j] > maxs[j])
maxs[j] = p->maxs[j];
}
2010-08-30 07:58:40 +00:00
}
2002-09-19 17:14:23 +00:00
if (usemidsplit) // do fast way for clipping hull
return ChooseMidPlaneFromList (surfaces, mins, maxs);
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// do slow way to save poly splits for drawing hull
#if 0
bestsurface = ChoosePlaneFromList (surfaces, mins, maxs, false);
2002-09-19 17:14:23 +00:00
if (bestsurface)
return bestsurface;
2002-09-19 17:14:23 +00:00
#endif
bestsurface = ChoosePlaneFromList (surfaces, mins, maxs, true, false);
if (bestsurface)
return bestsurface;
*detail = 1;
return ChoosePlaneFromList (surfaces, mins, maxs, true, true);
}
2002-09-19 17:14:23 +00:00
void
2010-08-30 07:58:40 +00:00
CalcSurfaceInfo (surface_t *surf)
{
2002-09-19 17:14:23 +00:00
face_t *f;
2002-09-19 18:51:19 +00:00
int i, j;
2002-09-19 17:14:23 +00:00
if (!surf->faces)
Sys_Error ("CalcSurfaceInfo: surface without a face");
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// calculate a bounding box
2002-09-19 17:14:23 +00:00
for (i = 0; i < 3; i++) {
surf->mins[i] = BOGUS_RANGE;
surf->maxs[i] = -BOGUS_RANGE;
}
2002-09-19 17:14:23 +00:00
for (f = surf->faces; f; f = f->next) {
2003-09-08 03:00:53 +00:00
winding_t *fp = f->points;
2002-09-19 17:14:23 +00:00
if (f->contents[0] >= 0 || f->contents[1] >= 0)
Sys_Error ("Bad contents");
2010-08-30 07:58:40 +00:00
for (i = 0; i < fp->numpoints; i++) {
2002-09-19 17:14:23 +00:00
for (j = 0; j < 3; j++) {
2003-09-08 03:00:53 +00:00
if (fp->points[i][j] < surf->mins[j])
surf->mins[j] = fp->points[i][j];
if (fp->points[i][j] > surf->maxs[j])
surf->maxs[j] = fp->points[i][j];
}
2010-08-30 07:58:40 +00:00
}
}
}
2010-08-30 08:34:41 +00:00
/** Divide a surface by the plane.
\param in The surface to divide.
\param split The plane by which to divide the surface.
\param front Tne part of the surface in front of the plane.
\param back The part of the surface behind the plane.
*/
static void
2002-09-19 18:51:19 +00:00
DividePlane (surface_t *in, plane_t *split, surface_t **front,
surface_t **back)
{
2002-09-19 17:14:23 +00:00
face_t *facet, *next;
face_t *frontlist, *backlist;
face_t *frontfrag, *backfrag;
plane_t *inplane;
2002-09-19 18:51:19 +00:00
surface_t *news;
2002-09-19 17:14:23 +00:00
int have[2][2]; // [front|back][detail|struct]
inplane = &planes.a[in->planenum];
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// parallel case is easy
if (_VectorCompare (inplane->normal, split->normal)) {
2002-09-23 16:27:17 +00:00
// check for exactly on node
if (inplane->dist == split->dist) {
// divide the facets to the front and back sides
news = AllocSurface ();
*news = *in;
2002-09-19 17:14:23 +00:00
facet = in->faces;
in->faces = NULL;
news->faces = NULL;
in->onnode = news->onnode = true;
in->has_detail = in->has_struct = false;
2002-09-19 17:14:23 +00:00
for (; facet; facet = next) {
next = facet->next;
2002-09-19 17:14:23 +00:00
if (facet->planeside == 1) {
facet->next = news->faces;
news->faces = facet;
if (facet->detail)
news->has_detail = true;
else
news->has_struct = true;
2002-09-19 17:14:23 +00:00
} else {
facet->next = in->faces;
in->faces = facet;
if (facet->detail)
in->has_detail = true;
else
in->has_struct = true;
}
}
2002-09-19 17:14:23 +00:00
if (in->faces)
*front = in;
else
*front = NULL;
if (news->faces)
*back = news;
else
*back = NULL;
return;
}
2002-09-19 17:14:23 +00:00
if (inplane->dist > split->dist) {
*front = in;
*back = NULL;
2002-09-19 17:14:23 +00:00
} else {
*front = NULL;
*back = in;
}
return;
}
2002-09-23 16:27:17 +00:00
// do a real split. may still end up entirely on one side
// OPTIMIZE: use bounding box for fast test
frontlist = NULL;
backlist = NULL;
2002-09-19 17:14:23 +00:00
memset (have, 0, sizeof (have));
2002-09-19 17:14:23 +00:00
for (facet = in->faces; facet; facet = next) {
next = facet->next;
SplitFace (facet, split, &frontfrag, &backfrag);
2002-09-19 17:14:23 +00:00
if (frontfrag) {
frontfrag->next = frontlist;
frontlist = frontfrag;
have[0][frontfrag->detail] = 1;
}
2002-09-19 17:14:23 +00:00
if (backfrag) {
backfrag->next = backlist;
backlist = backfrag;
have[1][backfrag->detail] = 1;
}
}
2002-09-23 16:27:17 +00:00
// if nothing actually got split, just move the in plane
2002-09-19 17:14:23 +00:00
if (frontlist == NULL) {
*front = NULL;
*back = in;
in->faces = backlist;
return;
}
2002-09-19 17:14:23 +00:00
if (backlist == NULL) {
*front = in;
*back = NULL;
in->faces = frontlist;
return;
}
2002-09-23 16:27:17 +00:00
// stuff got split, so allocate one new plane and reuse in
news = AllocSurface ();
*news = *in;
news->faces = backlist;
*back = news;
2002-09-19 17:14:23 +00:00
in->faces = frontlist;
*front = in;
2002-09-19 17:14:23 +00:00
in->has_struct = have[0][0];
in->has_detail = have[0][1];
news->has_struct = have[1][0];
news->has_detail = have[1][1];
2002-09-23 16:27:17 +00:00
// recalc bboxes and flags
CalcSurfaceInfo (news);
2002-09-19 17:14:23 +00:00
CalcSurfaceInfo (in);
}
2010-08-30 08:34:41 +00:00
/** Determine the contents of the leaf and create the final list of
2002-09-23 16:27:17 +00:00
original faces that have some fragment inside this leaf
2010-08-30 08:34:41 +00:00
\param planelist surfaces bounding the leaf.
\param leafnode The leaf.
*/
static void
2002-09-19 18:51:19 +00:00
LinkConvexFaces (surface_t *planelist, node_t *leafnode)
{
2002-09-19 17:14:23 +00:00
face_t *f, *next;
int i, count;
2002-09-19 18:51:19 +00:00
surface_t *surf, *pnext;
2002-09-19 17:14:23 +00:00
leafnode->faces = NULL;
leafnode->contents = 0;
leafnode->planenum = -1;
count = 0;
2002-09-19 17:14:23 +00:00
for (surf = planelist; surf; surf = surf->next) {
for (f = surf->faces; f; f = f->next) {
if (f->texturenum < 0)
continue;
count++;
if (!leafnode->contents)
leafnode->contents = f->contents[0];
else if (leafnode->contents != f->contents[0])
Sys_Error ("Mixed face contents in leafnode");
}
}
if (!leafnode->contents)
leafnode->contents = CONTENTS_SOLID;
2002-09-19 17:14:23 +00:00
switch (leafnode->contents) {
case CONTENTS_EMPTY:
c_empty++;
break;
case CONTENTS_SOLID:
c_solid++;
break;
case CONTENTS_WATER:
case CONTENTS_SLIME:
case CONTENTS_LAVA:
case CONTENTS_SKY:
c_water++;
break;
default:
Sys_Error ("LinkConvexFaces: bad contents number");
}
2002-09-23 16:27:17 +00:00
// write the list of faces, and free the originals
leaffaces += count;
2002-09-19 17:14:23 +00:00
leafnode->markfaces = malloc (sizeof (face_t *) * (count + 1));
i = 0;
2002-09-19 17:14:23 +00:00
for (surf = planelist; surf; surf = pnext) {
pnext = surf->next;
2002-09-19 17:14:23 +00:00
for (f = surf->faces; f; f = next) {
next = f->next;
if (f->texturenum >= 0) {
leafnode->markfaces[i] = f->original;
i++;
}
FreeFace (f);
}
FreeSurface (surf);
}
2002-09-19 17:14:23 +00:00
leafnode->markfaces[i] = NULL; // sentinal
}
2010-08-30 08:34:41 +00:00
/** Return a duplicated list of all faces on surface
2010-08-30 08:34:41 +00:00
\param surface The surface of which to duplicate the faces.
\return The duplicated list.
*/
2010-08-30 07:58:40 +00:00
static face_t *
2002-09-19 18:51:19 +00:00
LinkNodeFaces (surface_t *surface)
{
2002-09-19 18:51:19 +00:00
face_t *list, *new, **prevptr, *f;
2002-09-19 17:14:23 +00:00
list = NULL;
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// subdivide
prevptr = &surface->faces;
2002-09-19 17:14:23 +00:00
while (1) {
f = *prevptr;
if (!f)
break;
SubdivideFace (f, prevptr);
f = *prevptr;
prevptr = &f->next;
}
2002-09-23 16:27:17 +00:00
// copy
2002-09-19 17:14:23 +00:00
for (f = surface->faces; f; f = f->next) {
nodefaces++;
new = AllocFace ();
*new = *f;
2003-09-08 03:00:53 +00:00
new->points = CopyWinding (f->points);
f->original = new;
new->next = list;
list = new;
}
return list;
}
2010-08-30 08:34:41 +00:00
/** Partition the surfaces, creating a nice bsp.
\param surfaces The surfaces to partition.
\param node The current node.
*/
static void
2002-09-19 18:51:19 +00:00
PartitionSurfaces (surface_t *surfaces, node_t *node)
{
2002-09-19 17:14:23 +00:00
surface_t *split, *p, *next;
surface_t *frontlist, *backlist;
surface_t *frontfrag, *backfrag;
plane_t *splitplane;
split = SelectPartition (surfaces, &node->detail);
2002-09-19 17:14:23 +00:00
if (!split) { // this is a leaf node
node->detail = 0;
node->planenum = PLANENUM_LEAF;
LinkConvexFaces (surfaces, node);
return;
}
2002-09-19 17:14:23 +00:00
splitnodes++;
node->faces = LinkNodeFaces (split);
node->children[0] = AllocNode ();
node->children[1] = AllocNode ();
node->planenum = split->planenum;
splitplane = &planes.a[split->planenum];
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// multiple surfaces, so split all the polysurfaces into front and back
// lists
frontlist = NULL;
backlist = NULL;
2002-09-19 17:14:23 +00:00
for (p = surfaces; p; p = next) {
next = p->next;
DividePlane (p, splitplane, &frontfrag, &backfrag);
2002-09-19 17:14:23 +00:00
if (frontfrag && backfrag) {
// the plane was split, which may expose oportunities to merge
// adjacent faces into a single face
2002-09-19 18:51:19 +00:00
// MergePlaneFaces (frontfrag);
// MergePlaneFaces (backfrag);
}
2002-09-19 17:14:23 +00:00
if (frontfrag) {
if (!frontfrag->faces)
Sys_Error ("surface with no faces");
frontfrag->next = frontlist;
frontlist = frontfrag;
}
2002-09-19 17:14:23 +00:00
if (backfrag) {
if (!backfrag->faces)
Sys_Error ("surface with no faces");
backfrag->next = backlist;
backlist = backfrag;
}
}
PartitionSurfaces (frontlist, node->children[0]);
PartitionSurfaces (backlist, node->children[1]);
}
2010-08-30 07:58:40 +00:00
node_t *
SolidBSP (surface_t *surfhead, bool midsplit)
{
2002-09-19 17:14:23 +00:00
int i;
node_t *headnode;
qprintf ("----- SolidBSP -----\n");
headnode = AllocNode ();
usemidsplit = midsplit;
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// calculate a bounding box for the entire model
2002-09-19 17:14:23 +00:00
for (i = 0; i < 3; i++) {
headnode->mins[i] = brushset->mins[i] - SIDESPACE;
headnode->maxs[i] = brushset->maxs[i] + SIDESPACE;
}
2002-09-19 17:14:23 +00:00
2002-09-23 16:27:17 +00:00
// recursively partition everything
Draw_ClearWindow ();
splitnodes = 0;
leaffaces = 0;
nodefaces = 0;
c_solid = c_empty = c_water = 0;
PartitionSurfaces (surfhead, headnode);
qprintf ("%5i split nodes\n", splitnodes);
qprintf ("%5i solid leafs\n", c_solid);
qprintf ("%5i empty leafs\n", c_empty);
2002-09-19 17:14:23 +00:00
qprintf ("%5i water leafs\n", c_water);
qprintf ("%5i leaffaces\n", leaffaces);
qprintf ("%5i nodefaces\n", nodefaces);
2002-09-19 17:14:23 +00:00
return headnode;
}
//@}