Torque3D Documentation / _generateds / forestBrushTool.cpp

forestBrushTool.cpp

Engine/source/forest/editor/forestBrushTool.cpp

More...

Public Variables

Public Functions

ConsoleDocClass(ForestBrushTool , "@brief Defines the brush properties when painting trees in <a href="/coding/class/classforest/">Forest</a> <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">Editor\n\n</a>" "Editor use <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">only.\n\n</a>" "@internal" )
DefineEngineMethod(ForestBrushTool , collectElements , void , () , "" )
ImplementEnumType(ForestBrushMode , "Active brush <a href="/coding/file/zipobject_8cpp/#zipobject_8cpp_1ac6c3dfb4c3a68f849f32cbfb21da4e77">mode</a> <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">type.\n</a>" "@<a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">internal\n\n</a>" )

Detailed Description

Public Variables

 EndImplementEnumType 

Public Functions

ConsoleDocClass(ForestBrushTool , "@brief Defines the brush properties when painting trees in <a href="/coding/class/classforest/">Forest</a> <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">Editor\n\n</a>" "Editor use <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">only.\n\n</a>" "@internal" )

DefineEngineMethod(ForestBrushTool , collectElements , void , () , "" )

findSelectedElements(ForestBrushElement * obj)

IMPLEMENT_CONOBJECT(ForestBrushTool )

ImplementEnumType(ForestBrushMode , "Active brush <a href="/coding/file/zipobject_8cpp/#zipobject_8cpp_1ac6c3dfb4c3a68f849f32cbfb21da4e77">mode</a> <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">type.\n</a>" "@<a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">internal\n\n</a>" )

mCircleArea(F32 radius)

  1
  2//-----------------------------------------------------------------------------
  3// Copyright (c) 2012 GarageGames, LLC
  4//
  5// Permission is hereby granted, free of charge, to any person obtaining a copy
  6// of this software and associated documentation files (the "Software"), to
  7// deal in the Software without restriction, including without limitation the
  8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  9// sell copies of the Software, and to permit persons to whom the Software is
 10// furnished to do so, subject to the following conditions:
 11//
 12// The above copyright notice and this permission notice shall be included in
 13// all copies or substantial portions of the Software.
 14//
 15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 21// IN THE SOFTWARE.
 22//-----------------------------------------------------------------------------
 23
 24#include "platform/platform.h"
 25#include "forest/editor/forestBrushTool.h"
 26
 27#include "forest/forest.h"
 28#include "forest/editor/forestUndo.h"
 29#include "forest/editor/forestBrushElement.h"
 30#include "forest/editor/forestEditorCtrl.h"
 31
 32#include "gui/worldEditor/editTSCtrl.h"
 33#include "console/consoleTypes.h"
 34#include "console/engineAPI.h"
 35#include "core/util/tVector.h"
 36#include "gfx/gfxDrawUtil.h"
 37#include "gui/core/guiCanvas.h"
 38#include "gfx/primBuilder.h"
 39#include "gui/controls/guiTreeViewCtrl.h"
 40#include "core/strings/stringUnit.h"
 41#include "math/mRandomDeck.h"
 42#include "math/mRandomSet.h"
 43
 44
 45bool ForestBrushTool::protectedSetSize( void *object, const char *index, const char *data )
 46{
 47   ForestBrushTool *tool = static_cast<ForestBrushTool*>( object );
 48   F32 val = dAtof(data);
 49   tool->setSize( val );
 50
 51   return false;
 52}
 53
 54bool ForestBrushTool::protectedSetPressure( void *object, const char *index, const char *data )
 55{
 56   ForestBrushTool *tool = static_cast<ForestBrushTool*>( object );
 57   F32 val = dAtof(data);
 58   tool->setPressure( val );
 59
 60   return false;
 61}
 62
 63bool ForestBrushTool::protectedSetHardness( void *object, const char *index, const char *data )
 64{
 65   ForestBrushTool *tool = static_cast<ForestBrushTool*>( object );
 66   F32 val = dAtof(data);
 67   tool->setHardness( val );
 68
 69   return false;
 70}
 71
 72ImplementEnumType( ForestBrushMode,
 73   "Active brush mode type.\n"
 74   "@internal\n\n")
 75   { ForestBrushTool::Paint, "Paint", "Creates Items based on the Elements you have selected.\n" },
 76   { ForestBrushTool::Erase, "Erase", "Erases Items of any Mesh type.\n" },
 77   { ForestBrushTool::EraseSelected, "EraseSelected", "Erases items of a specific type.\n"  },
 78EndImplementEnumType;
 79
 80
 81ForestBrushTool::ForestBrushTool()
 82 : mRandom( Platform::getRealMilliseconds() + 1 ),
 83   mSize( 5.0f ),
 84   mPressure( 0.1f ),
 85   mHardness( 1.0f ),
 86   mMode( Paint ),
 87   mColor( ColorI::WHITE ),
 88   mBrushDown( false ),
 89   mDrawBrush( false ),
 90   mStrokeEvent( 0 ),
 91   mCurrAction( NULL )
 92{  
 93}
 94
 95ForestBrushTool::~ForestBrushTool()
 96{
 97}
 98
 99IMPLEMENT_CONOBJECT( ForestBrushTool );
100
101ConsoleDocClass( ForestBrushTool,
102   "@brief Defines the brush properties when painting trees in Forest Editor\n\n"
103   "Editor use only.\n\n"
104   "@internal"
105);
106
107void ForestBrushTool::initPersistFields()
108{  
109   addGroup( "ForestBrushTool" );
110      
111      addField( "mode", TYPEID< BrushMode >(), Offset( mMode, ForestBrushTool) );
112      
113      addProtectedField( "size", TypeF32, Offset( mSize, ForestBrushTool ), 
114         &protectedSetSize, &defaultProtectedGetFn, "Brush Size" );
115
116      addProtectedField( "pressure", TypeF32, Offset( mPressure, ForestBrushTool ), 
117         &protectedSetPressure, &defaultProtectedGetFn, "Brush Pressure" );
118
119      addProtectedField( "hardness", TypeF32, Offset( mHardness, ForestBrushTool ), 
120         &protectedSetHardness, &defaultProtectedGetFn, "Brush Hardness" );
121
122   endGroup( "ForestBrushTool" );
123
124   Parent::initPersistFields();
125}
126
127bool ForestBrushTool::onAdd()
128{
129   if ( !Parent::onAdd() )
130      return false;
131
132   return true;
133}
134
135void ForestBrushTool::onRemove()
136{  
137   Parent::onRemove();
138}
139
140void ForestBrushTool::on3DMouseDown( const Gui3DMouseEvent &evt )
141{   
142   Con::executef( this, "onMouseDown" );
143
144   if ( !_updateBrushPoint( evt ) || !mForest )
145      return;
146
147   mBrushDown = true;   
148
149   mEditor->getRoot()->showCursor( false );
150
151   _collectElements();
152   _onStroke();
153
154   return;
155}
156
157void ForestBrushTool::on3DMouseUp( const Gui3DMouseEvent &evt )
158{
159   _updateBrushPoint( evt );
160   Sim::cancelEvent( mStrokeEvent );
161   mBrushDown = false;  
162
163   mEditor->getRoot()->showCursor( true );
164
165   if ( mCurrAction )
166   {
167      _submitUndo( mCurrAction );
168      mCurrAction = NULL;
169   }
170
171   //mElements.clear();
172}
173
174void ForestBrushTool::on3DMouseMove( const Gui3DMouseEvent &evt )
175{
176   _updateBrushPoint( evt );   
177}
178
179void ForestBrushTool::on3DMouseDragged( const Gui3DMouseEvent &evt )
180{
181   _updateBrushPoint( evt );
182
183   if ( mBrushDown && !Sim::isEventPending( mStrokeEvent ) )         
184      mStrokeEvent = Sim::postEvent( this, new ForestBrushToolEvent(), Sim::getCurrentTime() + 250 );   
185}
186
187bool ForestBrushTool::onMouseWheel( const GuiEvent &evt )
188{
189   if ( evt.modifier & SI_PRIMARY_CTRL )
190      setPressure( mPressure + evt.fval * ( 0.05f / 120.0f ) );
191   else if ( evt.modifier & SI_SHIFT )
192      setHardness( mHardness + evt.fval * ( 0.05f / 120.0f ) );
193   else
194      setSize( mSize + evt.fval * ( 1.0f / 120.0f ) );
195
196   return true;
197}
198
199void ForestBrushTool::onRender3D( )
200{      
201}
202
203void ForestBrushTool::onRender2D()
204{
205   if ( !mDrawBrush )
206      return;
207
208   if ( !mEditor->getRoot()->isCursorON() && !mBrushDown )
209      return;   
210   
211   RayInfo ri;
212   Point3F start( 0, 0, mLastBrushPoint.z + mSize );
213   Point3F end( 0, 0, mLastBrushPoint.z - mSize );
214
215   Vector<Point3F> pointList;
216
217   const U32 steps = 32;
218
219   if ( mForest )
220      mForest->disableCollision();
221
222   for ( S32 i = 0; i < steps; i++ )
223   {
224      F32 radians = (F32)i / (F32)(steps-1) * M_2PI_F;
225      VectorF vec(0,1,0);
226      MathUtils::vectorRotateZAxis( vec, radians );
227
228      end.x = start.x = mLastBrushPoint.x + vec.x * mSize;
229      end.y = start.y = mLastBrushPoint.y + vec.y * mSize;
230
231      bool hit = gServerContainer.castRay( start, end, TerrainObjectType | StaticShapeObjectType, &ri );
232
233      if ( hit )
234         pointList.push_back( ri.point );
235   }
236
237   if ( mForest )
238      mForest->enableCollision();
239
240   if ( pointList.empty() )
241      return;
242
243   ColorI brushColor( ColorI::WHITE );
244
245   if ( mMode == Paint )
246      brushColor = ColorI::BLUE;
247   else if ( mMode == Erase )
248      brushColor = ColorI::RED;
249   else if ( mMode == EraseSelected )
250      brushColor.set( 150, 0, 0 );
251
252   if ( mMode == Paint || mMode == EraseSelected )
253   {
254      if ( mElements.empty() )
255      {
256         brushColor.set( 140, 140, 140 );
257      }
258   }
259   
260   mEditor->drawLineList( pointList, brushColor, 1.5f );   
261}
262
263void ForestBrushTool::onActivated( const Gui3DMouseEvent &lastEvent )
264{
265   _updateBrushPoint( lastEvent );
266
267   Con::executef( this, "onActivated" );
268}
269
270void ForestBrushTool::onDeactivated()
271{
272   Con::executef( this, "onDeactivated" );
273}
274
275bool ForestBrushTool::updateGuiInfo()
276{
277   GuiTextCtrl *statusbar;
278   Sim::findObject( "EWorldEditorStatusBarInfo", statusbar );
279
280   GuiTextCtrl *selectionBar;
281   Sim::findObject( "EWorldEditorStatusBarSelection", selectionBar );
282
283   String text;
284
285   if ( mMode == Paint )
286      text = "Forest Editor ( Paint Tool ) - This brush creates Items based on the Elements you have selected.";
287   else if ( mMode == Erase )
288      text = "Forest Editor ( Erase Tool ) - This brush erases Items of any Mesh type.";
289   else if ( mMode == EraseSelected )   
290      text = "Forest Editor ( Erase Selected ) - This brush erases Items based on the Elements you have selected.";      
291   
292   if ( statusbar )
293      statusbar->setText( text );
294
295   if ( mMode == Paint || mMode == EraseSelected )
296      text = String::ToString( "%i elements selected", mElements.size() );
297   else
298      text = "";
299
300   if ( selectionBar )
301      selectionBar->setText( text );
302
303   return true;
304}
305
306void ForestBrushTool::setSize( F32 val )
307{
308   mSize = mClampF( val, 0.0f, 150.0f );
309   Con::executef( this, "syncBrushToolbar" );
310}
311
312void ForestBrushTool::setPressure( F32 val )
313{   
314   mPressure = mClampF( val, 0.0f, 1.0f );
315   Con::executef( this, "syncBrushToolbar" );
316}
317
318void ForestBrushTool::setHardness( F32 val )
319{
320   mHardness = mClampF( val, 0.0f, 1.0f );
321   Con::executef( this, "syncBrushToolbar" );
322}
323
324void ForestBrushTool::_onStroke()
325{
326   if ( !mForest || !mBrushDown )
327      return;
328
329   _action( mLastBrushPoint );
330}
331
332void ForestBrushTool::_action( const Point3F &point )
333{
334   if ( mMode == Paint )
335      _paint( point );
336   else if ( mMode == Erase || mMode == EraseSelected )
337      _erase( point );
338}
339
340inline F32 mCircleArea( F32 radius )
341{
342   return radius * radius * M_PI_F;   
343}
344
345void ForestBrushTool::_paint( const Point3F &point )
346{
347   AssertFatal( mForest, "ForestBrushTool::_paint() - Can't paint without a Forest!" );
348
349   if ( mElements.empty() )
350      return;
351
352   // Iterators, pointers, and temporaries.
353   ForestBrushElement *pElement;
354   ForestItemData *pData;
355   F32 radius, area;
356   
357   // How much area do we have to fill with trees ( within the brush ).
358   F32 fillArea = mCircleArea( mSize );
359
360   // Scale that down by pressure, this is how much area we want to fill.
361   fillArea *= mPressure;
362
363   // Create an MRandomSet we can get items we are painting out of with
364   // the desired distribution.
365   // Also grab the smallest and largest radius elements while we are looping.
366   
367   ForestBrushElement *smallestElement, *largestElement;
368   smallestElement = largestElement = mElements[0];
369
370   MRandomSet<ForestBrushElement*> randElementSet(&mRandom);
371
372   for ( S32 i = 0; i < mElements.size(); i++ )
373   {
374      pElement = mElements[i];
375
376      if ( pElement->mData->mRadius > largestElement->mData->mRadius )
377         largestElement = pElement;
378      if ( pElement->mData->mRadius < smallestElement->mData->mRadius )
379         smallestElement = pElement;
380
381      randElementSet.add( pElement, pElement->mProbability );
382   }
383   
384   // Pull elements from the random set until we would theoretically fill
385   // the desired area.
386
387   F32 areaLeft = fillArea;
388   F32 scaleFactor, sink, randRot, worldCoordZ, slope;
389   Point2F worldCoords;
390   Point3F normalVector, right;
391   Point2F temp;
392
393   const S32 MaxTries = 5;
394
395
396   while ( areaLeft > 0.0f )
397   {
398      pElement = randElementSet.get();
399      pData = pElement->mData;
400
401      scaleFactor =  mLerp( pElement->mScaleMin, pElement->mScaleMax, mClampF( mPow( mRandom.randF(), pElement->mScaleExponent ), 0.0f, 1.0f ) );
402      radius = getMax( pData->mRadius * scaleFactor, 0.1f );
403      area = mCircleArea( radius );
404
405      areaLeft -= area * 5.0f; // fudge value
406
407      // No room left we are done.
408      //if ( areaLeft < 0.0f )
409      //   break;
410
411      // We have area left to fill...
412      
413      const F32 rotRange = mDegToRad( pElement->mRotationRange );
414
415      // Get a random sink value.
416      sink = mRandom.randF( pElement->mSinkMin, pElement->mSinkMax ) * scaleFactor;
417
418      // Get a random rotation.
419      randRot = mRandom.randF( 0.0f, rotRange );
420
421      // Look for a place within the brush area to place this item.
422      // We may have to try several times or give and go onto the next.
423
424      S32 i = 0;
425      for ( ; i < MaxTries; i++ )
426      {
427         // Pick some randoms for placement.
428         worldCoords = MathUtils::randomPointInCircle( mSize ) + point.asPoint2F();                  
429
430         // Look for the ground at this position.
431         if ( !getGroundAt( Point3F( worldCoords.x, worldCoords.y , point.z ), 
432                                     &worldCoordZ, 
433                                     &normalVector ) )
434         {
435            continue;
436         }
437
438         // Does this pass our slope and elevation limits.
439         right.set( normalVector.x, normalVector.y, 0 );
440         right.normalizeSafe();
441         slope = mRadToDeg( mDot( right, normalVector ) );
442
443         if ( worldCoordZ < pElement->mElevationMin ||
444              worldCoordZ > pElement->mElevationMax ||
445              slope < pElement->mSlopeMin ||
446              slope > pElement->mSlopeMax )
447         {
448            continue;
449         }
450
451         // Are we up against another tree?
452         if ( mForest->getData()->getItems( worldCoords, radius, NULL ) > 0 )
453            continue;
454
455         // If the trunk radius is set then we need to sink
456         // the tree into the ground a bit to hide the bottom.
457         if ( pElement->mSinkRadius > 0.0f )
458         {
459            // Items that are not aligned to the ground surface
460            // get sunken down to hide the bottom of their trunks.
461
462            // sunk down a bit to hide their bottoms on slopes.
463            normalVector.z = 0;
464            normalVector.normalizeSafe();
465            normalVector *= sink;
466            temp = worldCoords + normalVector.asPoint2F();
467            getGroundAt( Point3F( temp.x, temp.y, point.z ),
468                         &worldCoordZ, 
469                         NULL );
470         }
471
472         worldCoordZ -= sink;
473
474         // Create a new undo action if this is a new stroke.
475         ForestCreateUndoAction *action = dynamic_cast<ForestCreateUndoAction*>( mCurrAction );
476         if ( !action )
477         {
478            action = new ForestCreateUndoAction( mForest->getData(), mEditor );
479            mCurrAction = action;
480         }
481
482         //Con::printf( "worldCoords = %g, %g, %g", worldCoords.x, worldCoords.y, worldCoordZ );
483
484         // Let the action manage adding it to the forest.
485         action->addItem( pData,
486                          Point3F( worldCoords.x, worldCoords.y, worldCoordZ ),
487                          randRot,
488                          scaleFactor );
489         break;
490      }
491   }      
492}
493
494void ForestBrushTool::_erase( const Point3F &point )
495{
496   AssertFatal( mForest, "ForestBrushTool::_erase() - Can't erase without a Forest!" );
497
498   // First grab all the forest items around the point.
499   ForestItemVector trees;
500   if ( mForest->getData()->getItems( point.asPoint2F(), mSize, &trees ) == 0 )
501      return;
502
503   if ( mMode == EraseSelected )
504   {
505      for ( U32 i = 0; i < trees.size(); i++ )
506      {
507         const ForestItem &tree = trees[i];
508
509         if ( !mDatablocks.contains( tree.getData() ) )    
510         {
511            trees.erase_fast( i );
512            i--;
513         }
514      }
515   }
516
517   if ( trees.empty() )
518      return;
519
520   // Number of trees to erase depending on pressure.
521   S32 eraseCount = getMax( (S32)mCeil( (F32)trees.size() * mPressure ), 0 );
522   
523   // Initialize an MRandomDeck with trees under the brush.
524   MRandomDeck<ForestItem> deck(&mRandom);
525   deck.addToPile( trees );
526   deck.shuffle();
527
528   ForestItem currentTree;
529   
530   // Draw eraseCount number of trees from MRandomDeck, adding them to our erase action.
531   for ( U32 i = 0; i < eraseCount; i++ )
532   {
533      deck.draw(&currentTree);
534
535      // Create a new undo action if this is a new stroke.
536      ForestDeleteUndoAction *action = dynamic_cast<ForestDeleteUndoAction*>( mCurrAction );
537      if ( !action )
538      {
539         action = new ForestDeleteUndoAction( mForest->getData(), mEditor );
540         mCurrAction = action;
541      }
542
543      action->removeItem( currentTree );      
544   }
545}
546
547bool ForestBrushTool::_updateBrushPoint( const Gui3DMouseEvent &event_ )
548{
549   // Do a raycast for terrain... thats the placement center.
550   const U32 mask = TerrainObjectType | StaticShapeObjectType; // TODO: Make an option!
551
552   Point3F start( event_.pos );
553   Point3F end( event_.pos + ( event_.vec * 10000.0f ) );
554
555   if ( mForest )
556      mForest->disableCollision();
557
558   RayInfo rinfo;
559   mDrawBrush = gServerContainer.castRay( start, end, mask, &rinfo );
560
561   if ( mForest )
562      mForest->enableCollision();
563
564   if ( mDrawBrush )
565   {
566      mLastBrushPoint = rinfo.point;
567      mLastBrushNormal = rinfo.normal;
568   }
569
570   return mDrawBrush;
571}
572
573bool findSelectedElements( ForestBrushElement *obj )
574{   
575   if ( obj->isSelectedRecursive() )
576      return true;
577
578   return false;
579}
580
581void ForestBrushTool::_collectElements()
582{
583   mElements.clear();
584
585   // Get the selected objects from the tree view.
586   // These can be a combination of ForestBrush(s) and ForestBrushElement(s).
587
588   GuiTreeViewCtrl *brushTree;
589   if ( !Sim::findObject( "ForestEditBrushTree", brushTree ) )
590      return;
591      
592   const char* objectIdList = Con::executef( brushTree, "getSelectedObjectList" );
593
594   // Collect those objects in a vector and mark them as selected.
595
596   Vector<SimObject*> objectList;
597   SimObject *simobj;
598
599   S32 wordCount = StringUnit::getUnitCount( objectIdList, " " );
600   
601   for ( S32 i = 0; i < wordCount; i++ )
602   {
603      const char* word = StringUnit::getUnit( objectIdList, i, " " );
604      
605      if ( Sim::findObject( word, simobj ) )
606      {
607         objectList.push_back( simobj );
608         simobj->setSelected(true);
609      }
610   }
611
612   // Find all ForestBrushElements that are directly or indirectly selected.
613
614   SimGroup *brushGroup = ForestBrush::getGroup();
615   brushGroup->findObjectByCallback( findSelectedElements, mElements );
616
617   // We just needed to flag these objects as selected for the benefit of our
618   // findSelectedElements callback, we can now mark them un-selected again.
619
620   for ( S32 i = 0; i < objectList.size(); i++ )   
621      objectList[i]->setSelected(false);   
622
623   // If we are in Paint or EraseSelected mode we filter out elements with
624   // a non-positive probability. 
625   
626   if ( mMode == Paint || mMode == EraseSelected )
627   {
628      for ( S32 i = 0; i < mElements.size(); i++ )   
629      {
630         if ( mElements[i]->mProbability <= 0.0f )
631         {
632            mElements.erase_fast(i);
633            i--;
634         }
635      }
636   }
637
638   // Filter out elements with NULL datablocks and collect all unique datablocks
639   // in a vector.
640
641   mDatablocks.clear();
642   for ( S32 i = 0; i < mElements.size(); i++ )   
643   {
644      if ( mElements[i]->mData == NULL )
645      {
646         mElements.erase_fast(i);
647         i--;
648         continue;
649      }
650
651      mDatablocks.push_back_unique( mElements[i]->mData );   
652   }
653}
654
655bool ForestBrushTool::getGroundAt( const Point3F &worldPt, F32 *zValueOut, VectorF *normalOut )
656{
657   const U32 mask = TerrainObjectType | StaticShapeObjectType;
658
659   Point3F start( worldPt.x, worldPt.y, worldPt.z + mSize );
660   Point3F end( worldPt.x, worldPt.y, worldPt.z - mSize );
661
662   if ( mForest )
663      mForest->disableCollision();
664
665   // Do a cast ray at this point from the top to 
666   // the bottom of our brush radius.
667
668   RayInfo rinfo;   
669   bool hit = gServerContainer.castRay( start, end, mask, &rinfo );
670
671   if ( mForest )
672      mForest->enableCollision();
673
674   if ( !hit )
675      return false;
676
677   if (zValueOut)
678      *zValueOut = rinfo.point.z;
679
680   if (normalOut)
681      *normalOut = rinfo.normal;
682
683   return true;
684}
685
686DefineEngineMethod( ForestBrushTool, collectElements, void, (), , "" )
687{
688   object->collectElements();
689}
690