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