]> git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/csg.cpp
Radiant:
[xonotic/netradiant.git] / radiant / csg.cpp
1 /*
2    Copyright (C) 1999-2006 Id Software, Inc. and contributors.
3    For a list of contributors, see the accompanying CONTRIBUTORS file.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 #include "csg.h"
23
24 #include "debugging/debugging.h"
25
26 #include <list>
27
28 #include "map.h"
29 #include "brushmanip.h"
30 #include "brushnode.h"
31 #include "grid.h"
32 /*
33 void Face_makeBrush( Face& face, const Brush& brush, brush_vector_t& out, float offset ){
34         if ( face.contributes() ) {
35                 out.push_back( new Brush( brush ) );
36                 Face* newFace = out.back()->addFace( face );
37                 face.getPlane().offset( -offset );
38                 face.planeChanged();
39                 if ( newFace != 0 ) {
40                         newFace->flipWinding();
41                         newFace->getPlane().offset( offset );
42                         newFace->planeChanged();
43                 }
44         }
45 }
46
47 void Face_extrude( Face& face, const Brush& brush, brush_vector_t& out, float offset ){
48         if ( face.contributes() ) {
49                 face.getPlane().offset( offset );
50                 out.push_back( new Brush( brush ) );
51                 face.getPlane().offset( -offset );
52                 Face* newFace = out.back()->addFace( face );
53                 if ( newFace != 0 ) {
54                         newFace->flipWinding();
55                         newFace->planeChanged();
56                 }
57         }
58 }
59 */
60 #include "preferences.h"
61 #include "texwindow.h"
62
63
64 enum eHollowType
65 {
66         diag = 0,
67         wrap = 1,
68         extrude = 2,
69         pull = 3,
70         room = 4,
71 };
72
73 const char* getCaulkShader(){
74         const char* gotShader = g_pGameDescription->getKeyValue( "shader_caulk" );
75         if ( gotShader && *gotShader ){
76                 return gotShader;
77         }
78         return "textures/common/caulk";
79 }
80
81 class CaulkFace
82 {
83 DoubleVector3 ExclusionAxis;
84 double &mindot;
85 double &maxdot;
86 public:
87 CaulkFace( DoubleVector3 ExclusionAxis,
88                         double &mindot,
89                         double &maxdot ):
90                         ExclusionAxis( ExclusionAxis ),
91                         mindot( mindot ),
92                         maxdot( maxdot ){}
93 void operator()( Face& face ) const {
94         double dot = vector3_dot( face.getPlane().plane3().normal(), ExclusionAxis );
95         if( dot == 0 || ( dot > mindot + 0.005 && dot < maxdot - 0.005 ) )
96                 face.SetShader( getCaulkShader() );
97 }
98 };
99
100 class FaceMakeBrush
101 {
102 const Brush& brush;
103 brush_vector_t& out;
104 float offset;
105 eHollowType HollowType;
106 DoubleVector3 ExclusionAxis;
107 double &mindot;
108 double &maxdot;
109 bool caulk;
110 bool RemoveInner;
111 public:
112 FaceMakeBrush( const Brush& brush,
113                         brush_vector_t& out,
114                         float offset,
115                         eHollowType HollowType,
116                         DoubleVector3 ExclusionAxis,
117                         double &mindot,
118                         double &maxdot,
119                         bool caulk,
120                         bool RemoveInner )
121         : brush( brush ),
122         out( out ),
123         offset( offset ),
124         HollowType( HollowType ),
125         ExclusionAxis( ExclusionAxis ),
126         mindot( mindot ),
127         maxdot( maxdot ),
128         caulk( caulk ),
129         RemoveInner( RemoveInner ){
130 }
131 void operator()( Face& face ) const {
132         double dot = vector3_dot( face.getPlane().plane3().normal(), ExclusionAxis );
133         if( dot == 0 || ( dot > mindot + 0.005 && dot < maxdot - 0.005 ) ){
134                 if( HollowType == pull ){
135                         if ( face.contributes() ) {
136                                 face.getPlane().offset( offset );
137                                 face.planeChanged();
138                                 out.push_back( new Brush( brush ) );
139                                 face.getPlane().offset( -offset );
140                                 face.planeChanged();
141
142                                 if( caulk ){
143                                         Brush_forEachFace( *out.back(), CaulkFace( ExclusionAxis, mindot, maxdot ) );
144                                 }
145                                 Face* newFace = out.back()->addFace( face );
146                                 if ( newFace != 0 ) {
147                                         newFace->flipWinding();
148                                 }
149                         }
150                 }
151                 else if( HollowType == wrap ){
152                         //Face_makeBrush( face, brush, out, offset );
153                         if ( face.contributes() ) {
154                                 face.undoSave();
155                                 out.push_back( new Brush( brush ) );
156                                 if( !RemoveInner && caulk )
157                                         face.SetShader( getCaulkShader() );
158                                 Face* newFace = out.back()->addFace( face );
159                                 face.getPlane().offset( -offset );
160                                 face.planeChanged();
161                                 if( caulk )
162                                         face.SetShader( getCaulkShader() );
163                                 if ( newFace != 0 ) {
164                                         newFace->flipWinding();
165                                         newFace->getPlane().offset( offset );
166                                         newFace->planeChanged();
167                                 }
168                         }
169                 }
170                 else if( HollowType == extrude ){
171                         if ( face.contributes() ) {
172                                 //face.undoSave();
173                                 out.push_back( new Brush( brush ) );
174                                 out.back()->clear();
175
176                                 Face* newFace = out.back()->addFace( face );
177                                 if ( newFace != 0 ) {
178                                         newFace->getPlane().offset( offset );
179                                         newFace->planeChanged();
180                                 }
181
182                                 if( !RemoveInner && caulk )
183                                         face.SetShader( getCaulkShader() );
184                                 newFace = out.back()->addFace( face );
185                                 if ( newFace != 0 ) {
186                                         newFace->flipWinding();
187                                 }
188                                 Winding& winding = face.getWinding();
189                                 TextureProjection projection;
190                                 TexDef_Construct_Default( projection );
191                                 for ( Winding::iterator j = winding.begin(); j != winding.end(); ++j ){
192                                         std::size_t index = std::distance( winding.begin(), j );
193                                         std::size_t next = Winding_next( winding, index );
194
195                                         out.back()->addPlane( winding[index].vertex, winding[next].vertex, winding[next].vertex + face.getPlane().plane3().normal() * offset, TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ), projection );
196                                 }
197                         }
198                 }
199                 else if( HollowType == diag ){
200                         if ( face.contributes() ) {
201                                 out.push_back( new Brush( brush ) );
202                                 out.back()->clear();
203
204                                 Face* newFace = out.back()->addFace( face );
205                                 if ( newFace != 0 ) {
206
207                                         newFace->planeChanged();
208                                 }
209                                 newFace = out.back()->addFace( face );
210
211                                 if ( newFace != 0 ) {
212                                         if( !RemoveInner && caulk )
213                                                 newFace->SetShader( getCaulkShader() );
214                                         newFace->flipWinding();
215                                         newFace->getPlane().offset( offset );
216                                         newFace->planeChanged();
217                                 }
218
219                                 Winding& winding = face.getWinding();
220                                 TextureProjection projection;
221                                 TexDef_Construct_Default( projection );
222                                 for ( Winding::iterator i = winding.begin(); i != winding.end(); ++i ){
223                                         std::size_t index = std::distance( winding.begin(), i );
224                                         std::size_t next = Winding_next( winding, index );
225                                         Vector3 BestPoint;
226                                         float bestdist = 999999;
227
228                                         for( Brush::const_iterator j = brush.begin(); j != brush.end(); ++j ){
229                                                 Winding& winding2 = ( *j )->getWinding();
230                                                 for ( Winding::iterator k = winding2.begin(); k != winding2.end(); ++k ){
231                                                         std::size_t index2 = std::distance( winding2.begin(), k );
232                                                         float testdist = vector3_length( winding[index].vertex - winding2[index2].vertex );
233                                                         if( testdist < bestdist ){
234                                                                 bestdist = testdist;
235                                                                 BestPoint = winding2[index2].vertex;
236                                                         }
237                                                 }
238                                         }
239                                         out.back()->addPlane( winding[next].vertex, winding[index].vertex, BestPoint, caulk? getCaulkShader() : TextureBrowser_GetSelectedShader( GlobalTextureBrowser() ), projection );
240                                 }
241                         }
242                 }
243         }
244 }
245 };
246
247 class FaceExclude
248 {
249 DoubleVector3 ExclusionAxis;
250 double &mindot;
251 double &maxdot;
252 public:
253 FaceExclude( DoubleVector3 ExclusionAxis, double &mindot, double &maxdot )
254         : ExclusionAxis( ExclusionAxis ), mindot( mindot ), maxdot( maxdot ){
255 }
256 void operator()( Face& face ) const {
257         if( vector3_length_squared( ExclusionAxis ) != 0 ){
258                 double dot = vector3_dot( face.getPlane().plane3().normal(), ExclusionAxis );
259                 if( dot < mindot ){
260                         mindot = dot;
261                 }
262                 else if( dot > maxdot ){
263                         maxdot = dot;
264                 }
265         }
266 }
267 };
268
269 class FaceOffset
270 {
271 float offset;
272 DoubleVector3 ExclusionAxis;
273 double &mindot;
274 double &maxdot;
275 public:
276 FaceOffset( float offset, DoubleVector3 ExclusionAxis, double &mindot, double &maxdot )
277         : offset( offset ), ExclusionAxis( ExclusionAxis ), mindot( mindot ), maxdot( maxdot ){
278 }
279 void operator()( Face& face ) const {
280         double dot = vector3_dot( face.getPlane().plane3().normal(), ExclusionAxis );
281         if( dot == 0 || ( dot > mindot + 0.005 && dot < maxdot - 0.005 ) ){
282                 face.undoSave();
283                 face.getPlane().offset( offset );
284                 face.planeChanged();
285         }
286 }
287 };
288
289
290 DoubleVector3 getExclusion();
291 bool getCaulk();
292 bool getRemoveInner();
293
294 class BrushHollowSelectedWalker : public scene::Graph::Walker
295 {
296 float offset;
297 eHollowType HollowType;
298 public:
299 BrushHollowSelectedWalker( float offset, eHollowType HollowType )
300         : offset( offset ), HollowType( HollowType ){
301 }
302 bool pre( const scene::Path& path, scene::Instance& instance ) const {
303         if ( path.top().get().visible() ) {
304                 Brush* brush = Node_getBrush( path.top() );
305                 if ( brush != 0
306                          && Instance_getSelectable( instance )->isSelected()
307                          && path.size() > 1 ) {
308                         brush_vector_t out;
309                         double mindot = 0;
310                         double maxdot = 0;
311                         if( HollowType != room ){
312                                 Brush_forEachFace( *brush, FaceExclude( getExclusion(), mindot, maxdot ) );
313                         }
314                         if( HollowType == room ){
315                                 Brush* tmpbrush = new Brush( *brush );
316                                 tmpbrush->removeEmptyFaces();
317                                 Brush_forEachFace( *brush, FaceMakeBrush( *brush, out, offset, pull, DoubleVector3( 0, 0, 0 ), mindot, maxdot, true, true ) );
318                                 delete tmpbrush;
319                         }
320                         else if( HollowType == pull ){
321                                 if( !getRemoveInner() && getCaulk() ){
322                                         Brush_forEachFace( *brush, CaulkFace( getExclusion(), mindot, maxdot ) );
323                                 }
324                                 Brush* tmpbrush = new Brush( *brush );
325                                 tmpbrush->removeEmptyFaces();
326                                 Brush_forEachFace( *tmpbrush, FaceMakeBrush( *tmpbrush, out, offset, HollowType, getExclusion(), mindot, maxdot, getCaulk(), getRemoveInner() ) );
327                                 delete tmpbrush;
328                         }
329                         else if( HollowType == diag ){
330                                 Brush* tmpbrush = new Brush( *brush );
331                                 Brush_forEachFace( *tmpbrush, FaceOffset( offset, getExclusion(), mindot, maxdot ) );
332                                 tmpbrush->removeEmptyFaces();
333                                 Brush_forEachFace( *tmpbrush, FaceMakeBrush( *brush, out, offset, HollowType, getExclusion(), mindot, maxdot, getCaulk(), getRemoveInner() ) );
334                                 delete tmpbrush;
335                                 if( !getRemoveInner() && getCaulk() ){
336                                         Brush_forEachFace( *brush, CaulkFace( getExclusion(), mindot, maxdot ) );
337                                 }
338                         }
339                         else{
340                                 Brush_forEachFace( *brush, FaceMakeBrush( *brush, out, offset, HollowType, getExclusion(), mindot, maxdot, getCaulk(), getRemoveInner() ) );
341                         }
342                         for ( brush_vector_t::const_iterator i = out.begin(); i != out.end(); ++i )
343                         {
344                                 ( *i )->removeEmptyFaces();
345                                 if( ( *i )->hasContributingFaces() ){
346                                         NodeSmartReference node( ( new BrushNode() )->node() );
347                                         Node_getBrush( node )->copy( *( *i ) );
348                                         delete ( *i );
349                                         Node_getTraversable( path.parent() )->insert( node );
350                                         //path.push( makeReference( node.get() ) );
351                                         //selectPath( path, true );
352                                         //Instance_getSelectable( *GlobalSceneGraph().find( path ) )->setSelected( true );
353                                         //Path_deleteTop( path );
354                                 }
355                         }
356                 }
357         }
358         return true;
359 }
360 };
361
362 typedef std::list<Brush*> brushlist_t;
363
364 class BrushGatherSelected : public scene::Graph::Walker
365 {
366 brush_vector_t& m_brushlist;
367 public:
368 BrushGatherSelected( brush_vector_t& brushlist )
369         : m_brushlist( brushlist ){
370 }
371 bool pre( const scene::Path& path, scene::Instance& instance ) const {
372         if ( path.top().get().visible() ) {
373                 Brush* brush = Node_getBrush( path.top() );
374                 if ( brush != 0
375                          && Instance_getSelectable( instance )->isSelected() ) {
376                         m_brushlist.push_back( brush );
377                 }
378         }
379         return true;
380 }
381 };
382
383 class BrushDeleteSelected : public scene::Graph::Walker
384 {
385 public:
386 bool pre( const scene::Path& path, scene::Instance& instance ) const {
387         return true;
388 }
389 void post( const scene::Path& path, scene::Instance& instance ) const {
390         if ( path.top().get().visible() ) {
391                 Brush* brush = Node_getBrush( path.top() );
392                 if ( brush != 0
393                          && Instance_getSelectable( instance )->isSelected()
394                          && path.size() > 1 ) {
395                         Path_deleteTop( path );
396                 }
397         }
398 }
399 };
400
401
402 /*
403    =============
404    CSG_MakeRoom
405    =============
406  */
407 void CSG_MakeRoom( void ){
408         UndoableCommand undo( "makeRoom" );
409         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( GetGridSize(), room ) );
410         GlobalSceneGraph().traverse( BrushDeleteSelected() );
411         SceneChangeNotify();
412 }
413
414 template<typename Type>
415 class RemoveReference
416 {
417 public:
418 typedef Type type;
419 };
420
421 template<typename Type>
422 class RemoveReference<Type&>
423 {
424 public:
425 typedef Type type;
426 };
427
428 template<typename Functor>
429 class Dereference
430 {
431 const Functor& functor;
432 public:
433 typedef typename RemoveReference<typename Functor::first_argument_type>::type* first_argument_type;
434 typedef typename Functor::result_type result_type;
435 Dereference( const Functor& functor ) : functor( functor ){
436 }
437 result_type operator()( first_argument_type firstArgument ) const {
438         return functor( *firstArgument );
439 }
440 };
441
442 template<typename Functor>
443 inline Dereference<Functor> makeDereference( const Functor& functor ){
444         return Dereference<Functor>( functor );
445 }
446
447 typedef Face* FacePointer;
448 const FacePointer c_nullFacePointer = 0;
449
450 template<typename Predicate>
451 Face* Brush_findIf( const Brush& brush, const Predicate& predicate ){
452         Brush::const_iterator i = std::find_if( brush.begin(), brush.end(), makeDereference( predicate ) );
453         return i == brush.end() ? c_nullFacePointer : *i; // uses c_nullFacePointer instead of 0 because otherwise gcc 4.1 attempts conversion to int
454 }
455
456 template<typename Caller>
457 class BindArguments1
458 {
459 typedef typename Caller::second_argument_type FirstBound;
460 FirstBound firstBound;
461 public:
462 typedef typename Caller::result_type result_type;
463 typedef typename Caller::first_argument_type first_argument_type;
464 BindArguments1( FirstBound firstBound )
465         : firstBound( firstBound ){
466 }
467 result_type operator()( first_argument_type firstArgument ) const {
468         return Caller::call( firstArgument, firstBound );
469 }
470 };
471
472 template<typename Caller>
473 class BindArguments2
474 {
475 typedef typename Caller::second_argument_type FirstBound;
476 typedef typename Caller::third_argument_type SecondBound;
477 FirstBound firstBound;
478 SecondBound secondBound;
479 public:
480 typedef typename Caller::result_type result_type;
481 typedef typename Caller::first_argument_type first_argument_type;
482 BindArguments2( FirstBound firstBound, SecondBound secondBound )
483         : firstBound( firstBound ), secondBound( secondBound ){
484 }
485 result_type operator()( first_argument_type firstArgument ) const {
486         return Caller::call( firstArgument, firstBound, secondBound );
487 }
488 };
489
490 template<typename Caller, typename FirstBound, typename SecondBound>
491 BindArguments2<Caller> bindArguments( const Caller& caller, FirstBound firstBound, SecondBound secondBound ){
492         return BindArguments2<Caller>( firstBound, secondBound );
493 }
494
495 inline bool Face_testPlane( const Face& face, const Plane3& plane, bool flipped ){
496         return face.contributes() && !Winding_TestPlane( face.getWinding(), plane, flipped );
497 }
498 typedef Function3<const Face&, const Plane3&, bool, bool, Face_testPlane> FaceTestPlane;
499
500
501
502 /// \brief Returns true if
503 /// \li !flipped && brush is BACK or ON
504 /// \li flipped && brush is FRONT or ON
505 bool Brush_testPlane( const Brush& brush, const Plane3& plane, bool flipped ){
506         brush.evaluateBRep();
507 #if 1
508         for ( Brush::const_iterator i( brush.begin() ); i != brush.end(); ++i )
509         {
510                 if ( Face_testPlane( *( *i ), plane, flipped ) ) {
511                         return false;
512                 }
513         }
514         return true;
515 #else
516         return Brush_findIf( brush, bindArguments( FaceTestPlane(), makeReference( plane ), flipped ) ) == 0;
517 #endif
518 }
519
520 brushsplit_t Brush_classifyPlane( const Brush& brush, const Plane3& plane ){
521         brush.evaluateBRep();
522         brushsplit_t split;
523         for ( Brush::const_iterator i( brush.begin() ); i != brush.end(); ++i )
524         {
525                 if ( ( *i )->contributes() ) {
526                         split += Winding_ClassifyPlane( ( *i )->getWinding(), plane );
527                 }
528         }
529         return split;
530 }
531
532 bool Brush_subtract( const Brush& brush, const Brush& other, brush_vector_t& ret_fragments ){
533         if ( aabb_intersects_aabb( brush.localAABB(), other.localAABB() ) ) {
534                 brush_vector_t fragments;
535                 fragments.reserve( other.size() );
536                 Brush back( brush );
537
538                 for ( Brush::const_iterator i( other.begin() ); i != other.end(); ++i )
539                 {
540                         if ( ( *i )->contributes() ) {
541                                 brushsplit_t split = Brush_classifyPlane( back, ( *i )->plane3() );
542                                 if ( split.counts[ePlaneFront] != 0
543                                          && split.counts[ePlaneBack] != 0 ) {
544                                         fragments.push_back( new Brush( back ) );
545                                         Face* newFace = fragments.back()->addFace( *( *i ) );
546                                         if ( newFace != 0 ) {
547                                                 newFace->flipWinding();
548                                         }
549                                         back.addFace( *( *i ) );
550                                 }
551                                 else if ( split.counts[ePlaneBack] == 0 ) {
552                                         for ( brush_vector_t::iterator i = fragments.begin(); i != fragments.end(); ++i )
553                                         {
554                                                 delete( *i );
555                                         }
556                                         return false;
557                                 }
558                         }
559                 }
560                 ret_fragments.insert( ret_fragments.end(), fragments.begin(), fragments.end() );
561                 return true;
562         }
563         return false;
564 }
565
566 class SubtractBrushesFromUnselected : public scene::Graph::Walker
567 {
568 const brush_vector_t& m_brushlist;
569 std::size_t& m_before;
570 std::size_t& m_after;
571 public:
572 SubtractBrushesFromUnselected( const brush_vector_t& brushlist, std::size_t& before, std::size_t& after )
573         : m_brushlist( brushlist ), m_before( before ), m_after( after ){
574 }
575 bool pre( const scene::Path& path, scene::Instance& instance ) const {
576         return true;
577 }
578 void post( const scene::Path& path, scene::Instance& instance ) const {
579         if ( path.top().get().visible() ) {
580                 Brush* brush = Node_getBrush( path.top() );
581                 if ( brush != 0
582                          && !Instance_getSelectable( instance )->isSelected() ) {
583                         brush_vector_t buffer[2];
584                         bool swap = false;
585                         Brush* original = new Brush( *brush );
586                         buffer[static_cast<std::size_t>( swap )].push_back( original );
587
588                         {
589                                 for ( brush_vector_t::const_iterator i( m_brushlist.begin() ); i != m_brushlist.end(); ++i )
590                                 {
591                                         for ( brush_vector_t::iterator j( buffer[static_cast<std::size_t>( swap )].begin() ); j != buffer[static_cast<std::size_t>( swap )].end(); ++j )
592                                         {
593                                                 if ( Brush_subtract( *( *j ), *( *i ), buffer[static_cast<std::size_t>( !swap )] ) ) {
594                                                         delete ( *j );
595                                                 }
596                                                 else
597                                                 {
598                                                         buffer[static_cast<std::size_t>( !swap )].push_back( ( *j ) );
599                                                 }
600                                         }
601                                         buffer[static_cast<std::size_t>( swap )].clear();
602                                         swap = !swap;
603                                 }
604                         }
605
606                         brush_vector_t& out = buffer[static_cast<std::size_t>( swap )];
607
608                         if ( out.size() == 1 && out.back() == original ) {
609                                 delete original;
610                         }
611                         else
612                         {
613                                 ++m_before;
614                                 for ( brush_vector_t::const_iterator i = out.begin(); i != out.end(); ++i )
615                                 {
616                                         ++m_after;
617                                         ( *i )->removeEmptyFaces();
618                                         if ( !( *i )->empty() ) {
619                                                 NodeSmartReference node( ( new BrushNode() )->node() );
620                                                 Node_getBrush( node )->copy( *( *i ) );
621                                                 delete ( *i );
622                                                 Node_getTraversable( path.parent() )->insert( node );
623                                         }
624                                         else{
625                                                 delete ( *i );
626                                         }
627                                 }
628                                 Path_deleteTop( path );
629                         }
630                 }
631         }
632 }
633 };
634
635 void CSG_Subtract(){
636         brush_vector_t selected_brushes;
637         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
638
639         if ( selected_brushes.empty() ) {
640                 globalOutputStream() << "CSG Subtract: No brushes selected.\n";
641         }
642         else
643         {
644                 globalOutputStream() << "CSG Subtract: Subtracting " << Unsigned( selected_brushes.size() ) << " brushes.\n";
645
646                 UndoableCommand undo( "brushSubtract" );
647
648                 // subtract selected from unselected
649                 std::size_t before = 0;
650                 std::size_t after = 0;
651                 GlobalSceneGraph().traverse( SubtractBrushesFromUnselected( selected_brushes, before, after ) );
652                 globalOutputStream() << "CSG Subtract: Result: "
653                                                          << Unsigned( after ) << " fragment" << ( after == 1 ? "" : "s" )
654                                                          << " from " << Unsigned( before ) << " brush" << ( before == 1 ? "" : "es" ) << ".\n";
655
656                 SceneChangeNotify();
657         }
658 }
659
660 class BrushSplitByPlaneSelected : public scene::Graph::Walker
661 {
662 const Vector3& m_p0;
663 const Vector3& m_p1;
664 const Vector3& m_p2;
665 const char* m_shader;
666 const TextureProjection& m_projection;
667 EBrushSplit m_split;
668 public:
669 BrushSplitByPlaneSelected( const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, const TextureProjection& projection, EBrushSplit split )
670         : m_p0( p0 ), m_p1( p1 ), m_p2( p2 ), m_shader( shader ), m_projection( projection ), m_split( split ){
671 }
672 bool pre( const scene::Path& path, scene::Instance& instance ) const {
673         return true;
674 }
675 void post( const scene::Path& path, scene::Instance& instance ) const {
676         if ( path.top().get().visible() ) {
677                 Brush* brush = Node_getBrush( path.top() );
678                 if ( brush != 0
679                          && Instance_getSelectable( instance )->isSelected() ) {
680                         Plane3 plane( plane3_for_points( m_p0, m_p1, m_p2 ) );
681                         if ( plane3_valid( plane ) ) {
682                                 brushsplit_t split = Brush_classifyPlane( *brush, m_split == eFront ? plane3_flipped( plane ) : plane );
683                                 if ( split.counts[ePlaneBack] && split.counts[ePlaneFront] ) {
684                                         // the plane intersects this brush
685                                         if ( m_split == eFrontAndBack ) {
686                                                 NodeSmartReference node( ( new BrushNode() )->node() );
687                                                 Brush* fragment = Node_getBrush( node );
688                                                 fragment->copy( *brush );
689                                                 Face* newFace = fragment->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
690                                                 if ( newFace != 0 && m_split != eFront ) {
691                                                         newFace->flipWinding();
692                                                 }
693                                                 fragment->removeEmptyFaces();
694                                                 ASSERT_MESSAGE( !fragment->empty(), "brush left with no faces after split" );
695
696                                                 Node_getTraversable( path.parent() )->insert( node );
697                                                 {
698                                                         scene::Path fragmentPath = path;
699                                                         fragmentPath.top() = makeReference( node.get() );
700                                                         selectPath( fragmentPath, true );
701                                                 }
702                                         }
703
704                                         Face* newFace = brush->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
705                                         if ( newFace != 0 && m_split == eFront ) {
706                                                 newFace->flipWinding();
707                                         }
708                                         brush->removeEmptyFaces();
709                                         ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after split" );
710                                 }
711                                 else
712                                 // the plane does not intersect this brush
713                                 if ( m_split != eFrontAndBack && split.counts[ePlaneBack] != 0 ) {
714                                         // the brush is "behind" the plane
715                                         Path_deleteTop( path );
716                                 }
717                         }
718                 }
719         }
720 }
721 };
722
723 void Scene_BrushSplitByPlane( scene::Graph& graph, const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, EBrushSplit split ){
724         TextureProjection projection;
725         TexDef_Construct_Default( projection );
726         graph.traverse( BrushSplitByPlaneSelected( p0, p1, p2, shader, projection, split ) );
727         SceneChangeNotify();
728 }
729
730
731 class BrushInstanceSetClipPlane : public scene::Graph::Walker
732 {
733 Plane3 m_plane;
734 public:
735 BrushInstanceSetClipPlane( const Plane3& plane )
736         : m_plane( plane ){
737 }
738 bool pre( const scene::Path& path, scene::Instance& instance ) const {
739         BrushInstance* brush = Instance_getBrush( instance );
740         if ( brush != 0
741                  && path.top().get().visible()
742                  && brush->isSelected() ) {
743                 BrushInstance& brushInstance = *brush;
744                 brushInstance.setClipPlane( m_plane );
745         }
746         return true;
747 }
748 };
749
750 void Scene_BrushSetClipPlane( scene::Graph& graph, const Plane3& plane ){
751         graph.traverse( BrushInstanceSetClipPlane( plane ) );
752 }
753
754 /*
755    =============
756    CSG_Merge
757    =============
758  */
759 bool Brush_merge( Brush& brush, const brush_vector_t& in, bool onlyshape ){
760         // gather potential outer faces
761
762         {
763                 typedef std::vector<const Face*> Faces;
764                 Faces faces;
765                 for ( brush_vector_t::const_iterator i( in.begin() ); i != in.end(); ++i )
766                 {
767                         ( *i )->evaluateBRep();
768                         for ( Brush::const_iterator j( ( *i )->begin() ); j != ( *i )->end(); ++j )
769                         {
770                                 if ( !( *j )->contributes() ) {
771                                         continue;
772                                 }
773
774                                 const Face& face1 = *( *j );
775
776                                 bool skip = false;
777                                 // test faces of all input brushes
778                                 //!\todo SPEEDUP: Flag already-skip faces and only test brushes from i+1 upwards.
779                                 for ( brush_vector_t::const_iterator k( in.begin() ); !skip && k != in.end(); ++k )
780                                 {
781                                         if ( k != i ) { // don't test a brush against itself
782                                                 for ( Brush::const_iterator l( ( *k )->begin() ); !skip && l != ( *k )->end(); ++l )
783                                                 {
784                                                         const Face& face2 = *( *l );
785
786                                                         // face opposes another face
787                                                         if ( plane3_opposing( face1.plane3(), face2.plane3() ) ) {
788                                                                 // skip opposing planes
789                                                                 skip  = true;
790                                                                 break;
791                                                         }
792                                                 }
793                                         }
794                                 }
795
796                                 // check faces already stored
797                                 for ( Faces::const_iterator m = faces.begin(); !skip && m != faces.end(); ++m )
798                                 {
799                                         const Face& face2 = *( *m );
800
801                                         // face equals another face
802                                         if ( plane3_equal( face1.plane3(), face2.plane3() ) ) {
803                                                 //if the texture/shader references should be the same but are not
804                                                 if ( !onlyshape && !shader_equal( face1.getShader().getShader(), face2.getShader().getShader() ) ) {
805                                                         return false;
806                                                 }
807                                                 // skip duplicate planes
808                                                 skip = true;
809                                                 break;
810                                         }
811
812                                         // face1 plane intersects face2 winding or vice versa
813                                         if ( Winding_PlanesConcave( face1.getWinding(), face2.getWinding(), face1.plane3(), face2.plane3() ) ) {
814                                                 // result would not be convex
815                                                 return false;
816                                         }
817                                 }
818
819                                 if ( !skip ) {
820                                         faces.push_back( &face1 );
821                                 }
822                         }
823                 }
824                 for ( Faces::const_iterator i = faces.begin(); i != faces.end(); ++i )
825                 {
826                         if ( !brush.addFace( *( *i ) ) ) {
827                                 // result would have too many sides
828                                 return false;
829                         }
830                 }
831         }
832
833         brush.removeEmptyFaces();
834
835         return true;
836 }
837
838 void CSG_Merge( void ){
839         brush_vector_t selected_brushes;
840
841         // remove selected
842         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
843
844         if ( selected_brushes.empty() ) {
845                 globalOutputStream() << "CSG Merge: No brushes selected.\n";
846                 return;
847         }
848
849         if ( selected_brushes.size() < 2 ) {
850                 globalOutputStream() << "CSG Merge: At least two brushes have to be selected.\n";
851                 return;
852         }
853
854         globalOutputStream() << "CSG Merge: Merging " << Unsigned( selected_brushes.size() ) << " brushes.\n";
855
856         UndoableCommand undo( "brushMerge" );
857
858         scene::Path merged_path = GlobalSelectionSystem().ultimateSelected().path();
859
860         NodeSmartReference node( ( new BrushNode() )->node() );
861         Brush* brush = Node_getBrush( node );
862         // if the new brush would not be convex
863         if ( !Brush_merge( *brush, selected_brushes, true ) ) {
864                 globalOutputStream() << "CSG Merge: Failed - result would not be convex.\n";
865         }
866         else
867         {
868                 ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after merge" );
869
870                 // free the original brushes
871                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
872
873                 merged_path.pop();
874                 Node_getTraversable( merged_path.top() )->insert( node );
875                 merged_path.push( makeReference( node.get() ) );
876
877                 selectPath( merged_path, true );
878
879                 globalOutputStream() << "CSG Merge: Succeeded.\n";
880                 SceneChangeNotify();
881         }
882 }
883
884
885
886
887
888
889 /*
890    =============
891    CSG_Tool
892    =============
893  */
894 #include "mainframe.h"
895 #include <gtk/gtk.h>
896 #include "gtkutil/dialog.h"
897 #include "gtkutil/button.h"
898 #include "gtkutil/accelerator.h"
899
900 struct CSGToolDialog
901 {
902         GtkSpinButton* spin;
903         GtkWindow *window;
904         GtkToggleButton *radXYZ, *radX, *radY, *radZ, *caulk, *removeInner;
905 };
906
907 CSGToolDialog g_csgtool_dialog;
908
909 DoubleVector3 getExclusion(){
910         if( gtk_toggle_button_get_active( g_csgtool_dialog.radX ) ){
911                 return DoubleVector3( 1, 0, 0 );
912         }
913         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radY ) ){
914                 return DoubleVector3( 0, 1, 0 );
915         }
916         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radZ ) ){
917                 return DoubleVector3( 0, 0, 1 );
918         }
919         return DoubleVector3( 0, 0, 0 );
920 }
921
922 bool getCaulk(){
923                 if( gtk_toggle_button_get_active( g_csgtool_dialog.caulk ) ){
924                 return true;
925         }
926         return false;
927 }
928
929 bool getRemoveInner(){
930                 if( gtk_toggle_button_get_active( g_csgtool_dialog.removeInner ) ){
931                 return true;
932         }
933         return false;
934 }
935
936 class BrushFaceOffset
937 {
938 float offset;
939 public:
940 BrushFaceOffset( float offset )
941         : offset( offset ){
942 }
943 void operator()( BrushInstance& brush ) const {
944         double mindot = 0;
945         double maxdot = 0;
946         Brush_forEachFace( brush, FaceExclude( getExclusion(), mindot, maxdot ) );
947         Brush_forEachFace( brush, FaceOffset( offset, getExclusion(), mindot, maxdot ) );
948 }
949 };
950
951 //=================DLG
952
953 static gboolean CSGdlg_HollowDiag( GtkWidget *widget, CSGToolDialog* dialog ){
954         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
955         UndoableCommand undo( "brushHollow::Diag" );
956         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, diag ) );
957         if( getRemoveInner() )
958                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
959         SceneChangeNotify();
960         return TRUE;
961 }
962
963 static gboolean CSGdlg_HollowWrap( GtkWidget *widget, CSGToolDialog* dialog ){
964         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
965         UndoableCommand undo( "brushHollow::Wrap" );
966         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, wrap ) );
967         if( getRemoveInner() )
968                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
969         SceneChangeNotify();
970         return TRUE;
971 }
972
973 static gboolean CSGdlg_HollowExtrude( GtkWidget *widget, CSGToolDialog* dialog ){
974         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
975         UndoableCommand undo( "brushHollow::Extrude" );
976         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, extrude ) );
977         if( getRemoveInner() )
978                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
979         SceneChangeNotify();
980         return TRUE;
981 }
982
983 static gboolean CSGdlg_HollowPull( GtkWidget *widget, CSGToolDialog* dialog ){
984         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
985         UndoableCommand undo( "brushHollow::Pull" );
986         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, pull ) );
987         if( getRemoveInner() )
988                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
989         SceneChangeNotify();
990         return TRUE;
991 }
992
993 static gboolean CSGdlg_BrushShrink( GtkWidget *widget, CSGToolDialog* dialog ){
994         gtk_spin_button_update ( dialog->spin );
995         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
996         offset *= -1;
997         UndoableCommand undo( "Shrink brush" );
998 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
999         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1000         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1001         SceneChangeNotify();
1002         return TRUE;
1003 }
1004
1005 static gboolean CSGdlg_BrushExpand( GtkWidget *widget, CSGToolDialog* dialog ){
1006         gtk_spin_button_update ( dialog->spin );
1007         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1008         UndoableCommand undo( "Expand brush" );
1009 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
1010         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1011         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1012         SceneChangeNotify();
1013         return TRUE;
1014 }
1015
1016 static gboolean CSGdlg_grid2spin( GtkWidget *widget, CSGToolDialog* dialog ){
1017         gtk_spin_button_set_value( dialog->spin, GetGridSize() );
1018         return TRUE;
1019 }
1020
1021 static gboolean CSGdlg_delete( GtkWidget *widget, GdkEventAny *event, CSGToolDialog* dialog ){
1022         gtk_widget_hide( GTK_WIDGET( dialog->window ) );
1023         return TRUE;
1024 }
1025
1026 void CSG_Tool(){
1027         if ( g_csgtool_dialog.window == NULL ) {
1028                 g_csgtool_dialog.window = create_dialog_window( MainFrame_getWindow(), "CSG Tool", G_CALLBACK( CSGdlg_delete ), &g_csgtool_dialog );
1029                 gtk_window_set_type_hint( g_csgtool_dialog.window, GDK_WINDOW_TYPE_HINT_UTILITY );
1030
1031                 //GtkAccelGroup* accel = gtk_accel_group_new();
1032                 //gtk_window_add_accel_group( g_csgtool_dialog.window, accel );
1033                 global_accel_connect_window( g_csgtool_dialog.window );
1034
1035                 {
1036                         GtkHBox* hbox = create_dialog_hbox( 4, 4 );
1037                         gtk_container_add( GTK_CONTAINER( g_csgtool_dialog.window ), GTK_WIDGET( hbox ) );
1038                         {
1039                                 GtkTable* table = create_dialog_table( 3, 8, 4, 4 );
1040                                 gtk_box_pack_start( GTK_BOX( hbox ), GTK_WIDGET( table ), TRUE, TRUE, 0 );
1041                                 {
1042                                         //GtkWidget* label = gtk_label_new( "<->" );
1043                                         //gtk_widget_show( label );
1044                                         GtkWidget* button = gtk_button_new_with_label( "Grid->" );
1045                                         gtk_table_attach( table, button, 0, 1, 0, 1,
1046                                                                           (GtkAttachOptions) ( 0 ),
1047                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1048                                         gtk_widget_show( button );
1049                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_grid2spin ), &g_csgtool_dialog );
1050                                 }
1051                                 {
1052                                         GtkAdjustment* adj = GTK_ADJUSTMENT( gtk_adjustment_new( 16, 0, 9999, 1, 10, 0 ) );
1053                                         GtkSpinButton* spin = GTK_SPIN_BUTTON( gtk_spin_button_new( adj, 1, 3 ) );
1054                                         gtk_widget_show( GTK_WIDGET( spin ) );
1055                                         gtk_widget_set_tooltip_text( GTK_WIDGET( spin ), "Thickness" );
1056                                         gtk_table_attach( table, GTK_WIDGET( spin ), 1, 2, 0, 1,
1057                                                                           (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1058                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1059                                         gtk_widget_set_size_request( GTK_WIDGET( spin ), 64, -1 );
1060                                         gtk_spin_button_set_numeric( spin, TRUE );
1061
1062                                         g_csgtool_dialog.spin = spin;
1063                                 }
1064                                 {
1065                                         //radio button group for choosing the exclude axis
1066                                         GtkWidget* radXYZ = gtk_radio_button_new_with_label( NULL, "XYZ" );
1067                                         GtkWidget* radX = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-X" );
1068                                         GtkWidget* radY = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Y" );
1069                                         GtkWidget* radZ = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Z" );
1070                                         gtk_widget_show( radXYZ );
1071                                         gtk_widget_show( radX );
1072                                         gtk_widget_show( radY );
1073                                         gtk_widget_show( radZ );
1074
1075                                         gtk_table_attach( table, radXYZ, 2, 3, 0, 1,
1076                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1077                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1078                                         gtk_table_attach( table, radX, 3, 4, 0, 1,
1079                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1080                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1081                                         gtk_table_attach( table, radY, 4, 5, 0, 1,
1082                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1083                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1084                                         gtk_table_attach( table, radZ, 5, 6, 0, 1,
1085                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1086                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1087
1088                                         g_csgtool_dialog.radXYZ = GTK_TOGGLE_BUTTON( radXYZ );
1089                                         g_csgtool_dialog.radX = GTK_TOGGLE_BUTTON( radX );
1090                                         g_csgtool_dialog.radY = GTK_TOGGLE_BUTTON( radY );
1091                                         g_csgtool_dialog.radZ = GTK_TOGGLE_BUTTON( radZ );
1092                                 }
1093                                 {
1094                                         GtkWidget* button = gtk_toggle_button_new();
1095                                         button_set_icon( GTK_BUTTON( button ), "f-caulk.png" );
1096                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1097                                         gtk_table_attach( table, button, 6, 7, 0, 1,
1098                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1099                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1100                                         gtk_widget_set_tooltip_text( button, "Caulk some faces" );
1101                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1102                                         gtk_widget_show( button );
1103                                         g_csgtool_dialog.caulk = GTK_TOGGLE_BUTTON( button );
1104                                 }
1105                                 {
1106                                         GtkWidget* button = gtk_toggle_button_new();
1107                                         button_set_icon( GTK_BUTTON( button ), "csgtool_removeinner.png" );
1108                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1109                                         gtk_table_attach( table, button, 7, 8, 0, 1,
1110                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1111                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1112                                         gtk_widget_set_tooltip_text( button, "Remove inner brush" );
1113                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1114                                         gtk_widget_show( button );
1115                                         g_csgtool_dialog.removeInner = GTK_TOGGLE_BUTTON( button );
1116                                 }
1117                                 {
1118                                         GtkWidget* sep = gtk_hseparator_new();
1119                                         gtk_widget_show( sep );
1120                                         gtk_table_attach( table, sep, 0, 8, 1, 2,
1121                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1122                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1123                                 }
1124                                 {
1125                                         GtkWidget* button = gtk_button_new();
1126                                         button_set_icon( GTK_BUTTON( button ), "csgtool_shrink.png" );
1127                                         gtk_table_attach( table, button, 0, 1, 2, 3,
1128                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1129                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1130                                         gtk_widget_set_tooltip_text( button, "Shrink brush" );
1131                                         gtk_widget_show( button );
1132                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushShrink ), &g_csgtool_dialog );
1133                                 }
1134                                 {
1135                                         GtkWidget* button = gtk_button_new();
1136                                         button_set_icon( GTK_BUTTON( button ), "csgtool_expand.png" );
1137                                         gtk_table_attach( table, button, 1, 2, 2, 3,
1138                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1139                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1140                                         gtk_widget_set_tooltip_text( button, "Expand brush" );
1141                                         gtk_widget_show( button );
1142                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushExpand ), &g_csgtool_dialog );
1143                                 }
1144                                 {
1145                                         GtkWidget* button = gtk_button_new();
1146                                         button_set_icon( GTK_BUTTON( button ), "csgtool_diagonal.png" );
1147                                         gtk_table_attach( table, button, 3, 4, 2, 3,
1148                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1149                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1150                                         gtk_widget_set_tooltip_text( button, "Hollow::diagonal joints" );
1151                                         gtk_widget_show( button );
1152                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowDiag ), &g_csgtool_dialog );
1153                                 }
1154                                 {
1155                                         GtkWidget* button = gtk_button_new();
1156                                         button_set_icon( GTK_BUTTON( button ), "csgtool_wrap.png" );
1157                                         gtk_table_attach( table, button, 4, 5, 2, 3,
1158                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1159                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1160                                         gtk_widget_set_tooltip_text( button, "Hollow::warp" );
1161                                         gtk_widget_show( button );
1162                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowWrap ), &g_csgtool_dialog );
1163                                 }
1164                                 {
1165                                         GtkWidget* button = gtk_button_new();
1166                                         button_set_icon( GTK_BUTTON( button ), "csgtool_extrude.png" );
1167                                         gtk_table_attach( table, button, 5, 6, 2, 3,
1168                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1169                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1170                                         gtk_widget_set_tooltip_text( button, "Hollow::extrude faces" );
1171                                         gtk_widget_show( button );
1172                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowExtrude ), &g_csgtool_dialog );
1173                                 }
1174                                 {
1175                                         GtkWidget* button = gtk_button_new();
1176                                         button_set_icon( GTK_BUTTON( button ), "csgtool_pull.png" );
1177                                         gtk_table_attach( table, button, 6, 7, 2, 3,
1178                                                                           (GtkAttachOptions) ( GTK_EXPAND ),
1179                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1180                                         gtk_widget_set_tooltip_text( button, "Hollow::pull faces" );
1181                                         gtk_widget_show( button );
1182                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowPull ), &g_csgtool_dialog );
1183                                 }
1184
1185                         }
1186                 }
1187         }
1188
1189         gtk_widget_show( GTK_WIDGET( g_csgtool_dialog.window ) );
1190         gtk_window_present( g_csgtool_dialog.window );
1191 }
1192