source: projectionDesigner/trunk/projdesigner/include/gmtl/Containment.h @ 49

Last change on this file since 49 was 4, checked in by Torben Dannhauer, 15 years ago
File size: 21.5 KB
Line 
1/************************************************************** ggt-head beg
2 *
3 * GGT: Generic Graphics Toolkit
4 *
5 * Original Authors:
6 *   Allen Bierbaum
7 *
8 * -----------------------------------------------------------------
9 * File:          Containment.h,v
10 * Date modified: 2005/05/16 14:19:44
11 * Version:       1.17
12 * -----------------------------------------------------------------
13 *
14 *********************************************************** ggt-head end */
15/*************************************************************** ggt-cpr beg
16*
17* GGT: The Generic Graphics Toolkit
18* Copyright (C) 2001,2002 Allen Bierbaum
19*
20* This library is free software; you can redistribute it and/or
21* modify it under the terms of the GNU Lesser General Public
22* License as published by the Free Software Foundation; either
23* version 2.1 of the License, or (at your option) any later version.
24*
25* This library is distributed in the hope that it will be useful,
26* but WITHOUT ANY WARRANTY; without even the implied warranty of
27* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28* Lesser General Public License for more details.
29*
30* You should have received a copy of the GNU Lesser General Public
31* License along with this library; if not, write to the Free Software
32* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
33*
34 ************************************************************ ggt-cpr end */
35#ifndef _GMTL_CONTAINMENT_H_
36#define _GMTL_CONTAINMENT_H_
37
38// new stuff
39#include <vector>
40#include <gmtl/Sphere.h>
41#include <gmtl/AABox.h>
42#include <gmtl/VecOps.h>
43
44// old stuff
45//#include <gmtl/OOBox.h>
46//#include <gmtl/AABox.h>
47//#include <gmtl/Fit/GaussPointsFit.h>
48//#include <gmtl/matVecFuncs.h>
49//#include <gmtl/Quat.h>
50
51namespace gmtl
52{
53
54//-----------------------------------------------------------------------------
55// Sphere
56//-----------------------------------------------------------------------------
57
58/**
59 * Tests if the given point is inside or on the surface of the given spherical
60 * volume.
61 *
62 * @param container  the sphere to test against
63 * @param pt         the point to test with
64 *
65 * @return  true if pt is inside container, false otherwise
66 */
67template< class DATA_TYPE >
68bool isInVolume( const Sphere<DATA_TYPE>& container,
69                 const Point<DATA_TYPE, 3>& pt )
70{
71   // The point is inside the sphere if the vector computed from the center of
72   // the sphere to the point has a magnitude less than or equal to the radius
73   // of the sphere.
74   // |pt - center| <= radius
75   return ( length(gmtl::Vec<DATA_TYPE,3>(pt - container.mCenter)) <= container.mRadius );
76}
77
78/**
79 * Tests if the given sphere is completely inside or on the surface of the given
80 * spherical volume.
81 *
82 * @param container  the sphere acting as the container
83 * @param sphere     the sphere that may be inside container
84 *
85 * @return  true if sphere is inside container, false otherwise
86 */
87template< class DATA_TYPE >
88bool isInVolume( const Sphere<DATA_TYPE>& container,
89                 const Sphere<DATA_TYPE>& sphere )
90{
91   // the sphere is inside container if the distance between the centers of the
92   // spheres plus the radius of the inner sphere is less than or equal to the
93   // radius of the containing sphere.
94   // |sphere.center - container.center| + sphere.radius <= container.radius
95   return ( length(gmtl::Vec<DATA_TYPE,3>(sphere.mCenter - container.mCenter)) + sphere.mRadius
96            <= container.mRadius );
97}
98
99/**
100 * Modifies the existing sphere to tightly enclose itself and the given point.
101 *
102 * @param container  [in,out] the sphere that will be extended
103 * @param pt         [in]     the point which the sphere should contain
104 */
105template< class DATA_TYPE >
106void extendVolume( Sphere<DATA_TYPE>& container,
107                   const Point<DATA_TYPE, 3>& pt )
108{
109   // check if we already contain the point
110   if ( isInVolume( container, pt ) )
111   {
112      return;
113   }
114
115   // make a vector pointing from the center of the sphere to pt. this is the
116   // direction in which we need to move the sphere's center
117   Vec<DATA_TYPE, 3> dir = pt - container.mCenter;
118   DATA_TYPE len = normalize( dir );
119
120   // compute what the new radius should be
121   DATA_TYPE newRadius =  (len + container.mRadius) * DATA_TYPE(0.5);
122
123   // compute the new center for the sphere
124   Point<DATA_TYPE, 3> newCenter = container.mCenter +
125                                   (dir * (newRadius - container.mRadius));
126
127   // modify container to its new values
128   container.mCenter = newCenter;
129   container.mRadius = newRadius;
130}
131
132/**
133 * Modifies the container to tightly enclose itself and the given sphere.
134 *
135 * @param container  [in,out] the sphere that will be extended
136 * @param sphere     [in]     the sphere which container should contain
137 */
138template< class DATA_TYPE >
139void extendVolume( Sphere<DATA_TYPE>& container,
140                   const Sphere<DATA_TYPE>& sphere )
141{
142   // check if we already contain the sphere
143   if ( isInVolume( container, sphere ) )
144   {
145      return;
146   }
147
148   // make a vector pointing from the center of container to sphere. this is the
149   // direction in which we need to move container's center
150   Vec<DATA_TYPE, 3> dir = sphere.mCenter - container.mCenter;
151   DATA_TYPE len = normalize( dir );
152
153   // compute what the new radius should be
154   DATA_TYPE newRadius = (len + sphere.mRadius + container.mRadius) *
155                         DATA_TYPE(0.5);
156
157   // compute the new center for container
158   Point<DATA_TYPE, 3> newCenter = container.mCenter +
159                                   (dir * (newRadius - container.mRadius));
160
161   // modify container to its new values
162   container.mCenter = newCenter;
163   container.mRadius = newRadius;
164}
165
166/**
167 * Modifies the given sphere to tightly enclose all points in the given
168 * std::vector. This operation is O(n) and uses sqrt(..) liberally. :(
169 *
170 * @param container  [out]    the sphere that will be modified to tightly
171 *                            enclose all the points in pts
172 * @param pts        [in]     the list of points to contain
173 *
174 * @pre  pts must contain at least 2 points
175 */
176template< class DATA_TYPE >
177void makeVolume( Sphere<DATA_TYPE>& container,
178                 const std::vector< Point<DATA_TYPE, 3> >& pts )
179{
180   gmtlASSERT( pts.size() > 0  && "pts must contain at least 1 point" );
181
182   // Implementation based on the Sphere Centered at Average of Points algorithm
183   // found in "3D Game Engine Design" by Devud G, Eberly (pg. 27)
184   typename std::vector< Point<DATA_TYPE, 3> >::const_iterator itr = pts.begin();
185
186   // compute the average of the points as the center
187   Point<DATA_TYPE, 3> sum = *itr;
188   ++itr;
189   while ( itr != pts.end() )
190   {
191      sum += *itr;
192      ++itr;
193   }
194   container.mCenter = sum / DATA_TYPE(pts.size());
195
196   // compute the distance from the computed center to point furthest from that
197   // center as the radius
198   DATA_TYPE radiusSqr(0);
199   for ( itr = pts.begin(); itr != pts.end(); ++itr )
200   {
201      DATA_TYPE len = lengthSquared( gmtl::Vec<DATA_TYPE,3>( (*itr) - container.mCenter) );
202      if ( len > radiusSqr )
203         radiusSqr = len;
204   }
205
206   container.mRadius = Math::sqrt( radiusSqr );
207}
208
209/*
210template< class DATA_TYPE >
211void makeVolume( Sphere<DATA_TYPE>& container,
212                 const std::vector< Point<DATA_TYPE, 3> >& pts )
213{
214   gmtlASSERT( pts.size() > 1  && "pts must contain at least 2 points" );
215
216   // make a sphere around the first 2 points and then extend the sphere by each
217   // point thereafter. we could probably be smarter about this ...
218   std::vector< Point<DATA_TYPE, 3> >::const_iterator itr = pts.begin();
219
220   // make the initial sphere
221   const Point<DATA_TYPE, 3>& first = *itr;
222   ++itr;
223   const Point<DATA_TYPE, 3>& second = *itr;
224   ++itr;
225   const Vec<DATA_TYPE, 3> dir = second - first;
226   container.mRadius = length(dir) * DATA_TYPE(0.5);
227   container.mCenter = first + (dir * container.mRadius);
228
229   // iterate through the remaining points and extend the container to fit each
230   // point. yay code reuse!
231   while ( itr != pts.end() )
232   {
233      extendVolume( container, *itr );
234      ++itr;
235   }
236}
237*/
238/**
239 * Modifies the given sphere to tightly enclose all spheres in the given
240 * std::vector. This operation is O(n) and uses sqrt(..) liberally. :(
241 *
242 * @param container  [out]    the sphere that will be modified to tightly
243 *                            enclose all the spheres in spheres
244 * @param spheres    [in]     the list of spheres to contain
245 *
246 * @pre  spheres must contain at least 2 points
247 */
248/*
249template< class DATA_TYPE >
250void makeVolume( Sphere<DATA_TYPE>& container,
251                 const std::vector< Sphere<DATA_TYPE> >& spheres )
252{
253   gmtlASSERT( spheres.size() > 1  && "spheres must contain at least 2 points" );
254
255   // make a sphere around the first 2 points and then extend the sphere by each
256   // point thereafter. we could probably be smarter about this ...
257   std::vector< Sphere<DATA_TYPE> >::const_iterator itr = spheres.begin();
258
259   // make the initial sphere
260   const Sphere<DATA_TYPE>& first = *itr;
261   ++itr;
262   const Sphere<DATA_TYPE>& second = *itr;
263   ++itr;
264   const Vec<DATA_TYPE, 3> dir = second.mCenter - first.mCenter;
265   container.mRadius = (length(dir) + first.mRadius + second.mRadius) *
266                       DATA_TYPE(0.5);
267   container.mCenter = first.mCenter +
268                       (dir * (container.mRadius - first.mRadius));
269
270   // iterate through the remaining points and extend the container to fit each
271   // point. yay code reuse!
272   while ( itr != spheres.end() )
273   {
274      extendVolume( container, *itr );
275      ++itr;
276   }
277}
278*/
279/**
280 * Tests if the given point is on the surface of the container with zero
281 * tolerance.
282 *
283 * @param container     the container to test against
284 * @param pt            the test point
285 *
286 * @return  true if pt is on the surface of container, false otherwise
287 */
288template< class DATA_TYPE >
289bool isOnVolume( const Sphere<DATA_TYPE>& container,
290                 const Point<DATA_TYPE, 3>& pt )
291{
292   // |center - pt| - radius == 0
293   return ( length(gmtl::Vec<DATA_TYPE,3>(container.mCenter - pt)) - container.mRadius == 0 );
294}
295
296/**
297 * Tests of the given point is on the surface of the container with the given
298 * tolerance.
299 *
300 * @param container     the container to test against
301 * @param pt            the test point
302 * @param tol           the epsilon tolerance
303 *
304 * @return  true if pt is on the surface of container, false otherwise
305 */
306template< class DATA_TYPE >
307bool isOnVolume( const Sphere<DATA_TYPE>& container,
308                 const Point<DATA_TYPE, 3>& pt,
309                 const DATA_TYPE& tol )
310{
311   gmtlASSERT( tol >= 0 && "tolerance must be positive" );
312
313   // abs( |center-pt| - radius ) < tol
314   return ( Math::abs( length( gmtl::Vec<DATA_TYPE,3>(container.mCenter - pt)) - container.mRadius )
315            <= tol );
316}
317
318//-----------------------------------------------------------------------------
319// AABox
320//-----------------------------------------------------------------------------
321
322/**
323 * Tests if the given point is inside (or on) the surface of the given AABox
324 * volume.
325 *
326 * @param container  the AABox to test against
327 * @param pt         the point to test with
328 *
329 * @return  true if pt is inside container, false otherwise
330 */
331template< class DATA_TYPE>
332bool isInVolume(const AABox<DATA_TYPE>& container,
333                const Point<DATA_TYPE, 3>& pt)
334{
335   if (! container.isEmpty())
336   {
337      return ( pt[0] >= container.mMin[0] &&
338               pt[1] >= container.mMin[1] &&
339               pt[2] >= container.mMin[2] &&
340               pt[0] <= container.mMax[0] &&
341               pt[1] <= container.mMax[1] &&
342               pt[2] <= container.mMax[2]);
343   }
344   else
345   {
346      return false;
347   }
348}
349
350/**
351 * Tests if the given point is inside (not on) the surface of the given AABox
352 * volume.  This method is "exclusive" because it does not consider the surface
353 * to be a part of the space.
354 *
355 * @param container  the AABox to test against
356 * @param pt         the point to test with
357 *
358 * @return  true if pt is inside container (but not on surface), false otherwise
359 */
360template< class DATA_TYPE>
361bool isInVolumeExclusive(const AABox<DATA_TYPE>& container,
362                const Point<DATA_TYPE, 3>& pt)
363{
364   if (! container.isEmpty())
365   {
366      return ( pt[0] > container.mMin[0] &&
367               pt[1] > container.mMin[1] &&
368               pt[2] > container.mMin[2] &&
369               pt[0] < container.mMax[0] &&
370               pt[1] < container.mMax[1] &&
371               pt[2] < container.mMax[2]);
372   }
373   else
374   {
375      return false;
376   }
377}
378
379
380
381
382/**
383 * Tests if the given AABox is completely inside or on the surface of the given
384 * AABox container.
385 *
386 * @param container  the AABox acting as the container
387 * @param box        the AABox that may be inside container
388 *
389 * @return  true if AABox is inside container, false otherwise
390 */
391template< class DATA_TYPE >
392bool isInVolume(const AABox<DATA_TYPE>& container,
393                const AABox<DATA_TYPE>& box)
394{
395   // Empty boxes don't overlap
396   if (container.isEmpty() || box.isEmpty())
397   {
398      return false;
399   }
400
401   // Test that the boxes are not overlapping on any axis
402   if (container.mMax[0] < box.mMin[0] || container.mMin[0] > box.mMax[0] ||
403       container.mMax[1] < box.mMin[1] || container.mMin[1] > box.mMax[1] ||
404       container.mMax[2] < box.mMin[2] || container.mMin[2] > box.mMax[2])
405   {
406      return false;
407   }
408   else
409   {
410      return true;
411   }
412}
413
414/**
415 * Modifies the existing AABox to tightly enclose itself and the given point.
416 *
417 * @param container  [in,out] the AABox that will be extended
418 * @param pt         [in]     the point which the AABox should contain
419 */
420template< class DATA_TYPE >
421void extendVolume(AABox<DATA_TYPE>& container,
422                  const Point<DATA_TYPE, 3>& pt)
423{
424   if (! container.isEmpty())
425   {
426      // X coord
427      if (pt[0] > container.mMax[0])
428      {
429         container.mMax[0] = pt[0];
430      }
431      else if (pt[0] < container.mMin[0])
432      {
433         container.mMin[0] = pt[0];
434      }
435
436      // Y coord
437      if (pt[1] > container.mMax[1])
438      {
439         container.mMax[1] = pt[1];
440      }
441      else if (pt[1] < container.mMin[1])
442      {
443         container.mMin[1] = pt[1];
444      }
445
446      // Z coord
447      if (pt[2] > container.mMax[2])
448      {
449         container.mMax[2] = pt[2];
450      }
451      else if (pt[2] < container.mMin[2])
452      {
453         container.mMin[2] = pt[2];
454      }
455   }
456   else
457   {
458      // Make a box with essentially zero volume at the point
459      container.setMin(pt);
460      container.setMax(pt);
461      container.setEmpty(false);
462   }
463}
464
465/**
466 * Modifies the container to tightly enclose itself and the given AABox.
467 *
468 * @param container  [in,out] the AABox that will be extended
469 * @param box        [in]     the AABox which container should contain
470 */
471template< class DATA_TYPE >
472void extendVolume(AABox<DATA_TYPE>& container,
473                  const AABox<DATA_TYPE>& box)
474{
475   // Can't extend by an empty box
476   if (box.isEmpty())
477   {
478      return;
479   }
480
481   // An empty container is extended to be the box
482   if (container.isEmpty())
483   {
484      container = box;
485   }
486
487   // Just extend by the corners of the box
488   extendVolume(container, box.getMin());
489   extendVolume(container, box.getMax());
490}
491
492/**
493 * Creates an AABox that tightly encloses the given Sphere.
494 *
495 * @param box     set to the box
496 */
497template< class DATA_TYPE >
498void makeVolume(AABox<DATA_TYPE>& box, const Sphere<DATA_TYPE>& sph)
499{
500   const gmtl::Point<DATA_TYPE, 3>& center = sph.getCenter();
501   const DATA_TYPE& radius = sph.getRadius();
502
503   // Calculate the min and max points for the box
504   gmtl::Point<DATA_TYPE, 3> min_pt(center[0] - radius,
505                                    center[1] - radius,
506                                    center[2] - radius);
507   gmtl::Point<DATA_TYPE, 3> max_pt(center[0] + radius,
508                                    center[1] + radius,
509                                    center[2] + radius);
510
511   box.setMin(min_pt);
512   box.setMax(max_pt);
513   box.setEmpty(radius == DATA_TYPE(0));
514}
515
516/*
517//------------------------------------------------------------------------------
518// Begin old GMTL code
519//------------------------------------------------------------------------------
520inline void computeContainment( AABox& box, const std::vector<gmtl::Point3>& points,
521                         Point3& minPt, Point3& maxPt )
522//void MgcContAlignedBox (int iQuantity, const MgcVector3* akPoint,
523//    MgcVector3& rkMin, MgcVector3& rkMax)
524{
525   if (points.empty())
526      return;
527
528   minPt = points[0];
529   maxPt = minPt;
530
531   for (unsigned i = 1; i < points.size(); i++)
532   {
533      if ( points[i][Xelt] < minPt[Xelt] )
534         minPt[Xelt] = points[i][Xelt];
535      else if ( points[i][Xelt] > maxPt[Xelt] )
536         maxPt[Xelt] = points[i][Xelt];
537
538      if ( points[i][Yelt] < minPt[Yelt] )
539         minPt[Yelt] = points[i][Yelt];
540      else if ( points[i][Yelt] > maxPt[Yelt] )
541         maxPt[Yelt] = points[i][Yelt];
542
543      if ( points[i][Zelt] < minPt[Zelt] )
544         minPt[Zelt] = points[i][Zelt];
545      else if ( points[i][Zelt] > maxPt[Zelt] )
546         maxPt[Zelt] = points[i][Zelt];
547   }
548
549   // Now update the box
550   box.makeEmpty();
551   box.mMax = maxPt;
552   box.mMin = minPt;
553}
554//----------------------------------------------------------------------------
555//----------------------------------------------------------------------------
556//MgcBox3 MgcContOrientedBox (int iQuantity, const MgcVector3* akPoint)
557inline void computeContainment( OOBox& box, const std::vector<gmtl::Point3>& points)
558{
559    //MgcGaussPointsFit(iQuantity,akPoint,kBox.Center(),kBox.Axes(),
560    //    kBox.Extents());
561
562    gmtl::GaussPointsFit(points.size(), &points[0], box.center(), box.axes(), box.halfLens());
563
564    // Let C be the box center and let U0, U1, and U2 be the box axes.  Each
565    // input point is of the form X = C + y0*U0 + y1*U1 + y2*U2.  The
566    // following code computes min(y0), max(y0), min(y1), max(y1), min(y2),
567    // and max(y2).  The box center is then adjusted to be
568    //   C' = C + 0.5*(min(y0)+max(y0))*U0 + 0.5*(min(y1)+max(y1))*U1 +
569    //        0.5*(min(y2)+max(y2))*U2
570#ifdef _DEBUG
571   gmtl::OOBox box_test;
572   box_test = box;
573   gmtl::Vec3 ax0 = box_test.axis(0);
574   gmtl::Vec3 ax1 = box_test.axis(1);
575   gmtl::Vec3 ax2 = box_test.axis(2);
576#endif
577
578    gmtlASSERT(box.axis(0).isNormalized());
579    gmtlASSERT(box.axis(1).isNormalized());
580    gmtlASSERT(box.axis(2).isNormalized());
581
582    // XXX: Sign is sometimes wrong here
583    // This is hack code to make it "work right"
584    Vec3 cross;
585    cross = box.axis(0).cross(box.axis(1));
586    cross.normalize();
587    box.axis(2) = cross;
588
589    Vec3 kDiff = points[0] - box.center();
590    float fY0Min = kDiff.dot(box.axis(0)), fY0Max = fY0Min;
591    float fY1Min = kDiff.dot(box.axis(1)), fY1Max = fY1Min;
592    float fY2Min = kDiff.dot(box.axis(2)), fY2Max = fY2Min;
593
594    float fY0, fY1, fY2;
595
596    for (unsigned i = 1; i < points.size(); i++)
597    {
598        kDiff = points[i] - box.center();
599
600        fY0 = kDiff.dot(box.axis(0));
601        if ( fY0 < fY0Min )
602            fY0Min = fY0;
603        else if ( fY0 > fY0Max )
604            fY0Max = fY0;
605
606        fY1 = kDiff.dot(box.axis(1));
607        if ( fY1 < fY1Min )
608            fY1Min = fY1;
609        else if ( fY1 > fY1Max )
610            fY1Max = fY1;
611
612        fY2 = kDiff.dot(box.axis(2));
613        if ( fY2 < fY2Min )
614            fY2Min = fY2;
615        else if ( fY2 > fY2Max )
616            fY2Max = fY2;
617    }
618
619    box.center() += (0.5*(fY0Min+fY0Max))*box.axis(0) +
620                    (0.5*(fY1Min+fY1Max))*box.axis(1) +
621                    (0.5*(fY2Min+fY2Max))*box.axis(2);
622
623    box.halfLen(0) = 0.5*(fY0Max - fY0Min);
624    box.halfLen(1) = 0.5*(fY1Max - fY1Min);
625    box.halfLen(2) = 0.5*(fY2Max - fY2Min);
626}
627
628*/
629/*
630inline void computeContainment (OOBox& out_box, const OOBox& box0, const OOBox& box1, bool fast=true)
631{
632   gmtl::OOBox ret_box;    // The resulting box
633
634   if (fast)
635   {
636      ret_box.center() = 0.5*(box0.center() + box1.center());       // Center at average
637
638      // Average the quats to get a new orientation
639      Quat quat0, quat1;
640      quat0.makeAxes(box0.axis(0), box0.axis(1), box0.axis(2));
641      quat1.makeAxes(box1.axis(0), box1.axis(1), box1.axis(2));
642      if ( quat0.dot(quat1) < 0.0 )
643         quat1 = -quat1;
644
645      Quat full_quat = quat0 + quat1;
646
647      //float inv_len = 1.0/Math::sqrt(full_quat.norm());
648      //full_quat = inv_len*full_quat;
649      full_quat.normalize();
650      full_quat.getAxes(ret_box.axis(0), ret_box.axis(1), ret_box.axis(2));
651
652      // Now that we have new orientation, extend half lens to cover the volume
653      //
654      unsigned i, j;
655      Point3 verts[8];
656      Vec3 diff;
657      float aDot;
658
659      ret_box.halfLen(0) = 0.0;
660      ret_box.halfLen(1) = 0.0;
661      ret_box.halfLen(2) = 0.0;
662
663      box0.getVerts(verts);
664      for (i = 0; i < 8; i++)
665      {
666         diff = verts[i] - ret_box.center();
667         for (j = 0; j < 3; j++)                       // For each axis of box0
668         {
669            aDot = Math::abs(diff.dot(ret_box.axis(j)));
670            if ( aDot > ret_box.halfLen(j) )
671               ret_box.halfLen(j) = aDot;
672         }
673      }
674
675      box1.getVerts(verts);
676      for (i = 0; i < 8; i++)
677      {
678         diff = verts[i] - ret_box.center();
679         for (j = 0; j < 3; j++)
680         {
681            aDot = Math::abs(diff.dot(ret_box.axis(j)));
682            if ( aDot > ret_box.halfLen(j) )
683               ret_box.halfLen(j) = aDot;
684         }
685      }
686   }
687   else     // Tighter fit
688   {
689      // Hack that will do it correctly, but is slow
690      Point3 verts[8];
691      std::vector<gmtl::Point3> vert_points;
692
693      box0.getVerts(verts);
694      for (unsigned i=0;i<8;i++) vert_points.push_back(verts[i]);
695      box1.getVerts(verts);
696      for (unsigned i=0;i<8;i++) vert_points.push_back(verts[i]);
697
698      computeContainment(ret_box, vert_points);
699   }
700
701   out_box = ret_box;
702}
703*/
704
705
706}
707
708#endif
Note: See TracBrowser for help on using the repository browser.