]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/csg.cpp
Merge commit '839c9693774fdb0e420391f65b8066e8bd04c591' 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                                 scene::Node& parent = path.parent();
657                                 Path_deleteTop( path );
658                                 if( Node_getTraversable( parent )->empty() ){
659                                         m_eraseParent = true;
660                                 }
661                         }
662                 }
663         }
664         if( m_eraseParent && !Node_isPrimitive( path.top() ) && path.size() > 1 ){
665                 m_eraseParent = false;
666                 Entity* entity = Node_getEntity( path.top() );
667                 if ( entity != 0 && path.top().get_pointer() != Map_FindWorldspawn( g_map )
668                         && Node_getTraversable( path.top() )->empty() ) {
669                         Path_deleteTop( path );
670                 }
671         }
672 }
673 };
674
675 void CSG_Subtract(){
676         brush_vector_t selected_brushes;
677         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
678
679         if ( selected_brushes.empty() ) {
680                 globalOutputStream() << "CSG Subtract: No brushes selected.\n";
681         } else {
682                 globalOutputStream() << "CSG Subtract: Subtracting " << Unsigned( selected_brushes.size() ) << " brushes.\n";
683
684                 UndoableCommand undo( "brushSubtract" );
685
686                 // subtract selected from unselected
687                 std::size_t before = 0;
688                 std::size_t after = 0;
689                 GlobalSceneGraph().traverse( SubtractBrushesFromUnselected( selected_brushes, before, after ) );
690                 globalOutputStream() << "CSG Subtract: Result: "
691                                                          << Unsigned( after ) << " fragment" << ( after == 1 ? "" : "s" )
692                                                          << " from " << Unsigned( before ) << " brush" << ( before == 1 ? "" : "es" ) << ".\n";
693
694                 SceneChangeNotify();
695         }
696 }
697
698 class BrushSplitByPlaneSelected : public scene::Graph::Walker
699 {
700 const Vector3& m_p0;
701 const Vector3& m_p1;
702 const Vector3& m_p2;
703 const char* m_shader;
704 const TextureProjection& m_projection;
705 EBrushSplit m_split;
706 public:
707 BrushSplitByPlaneSelected( const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, const TextureProjection& projection, EBrushSplit split )
708         : m_p0( p0 ), m_p1( p1 ), m_p2( p2 ), m_shader( shader ), m_projection( projection ), m_split( split ){
709 }
710
711 bool pre( const scene::Path& path, scene::Instance& instance ) const {
712         return true;
713 }
714
715 void post( const scene::Path& path, scene::Instance& instance ) const {
716         if ( !path.top().get().visible() ) {
717                 return;
718         }
719
720                 Brush* brush = Node_getBrush( path.top() );
721         if ( brush == nullptr || !Instance_getSelectable( instance )->isSelected() ) {
722                 return;
723         }
724
725                         Plane3 plane( plane3_for_points( m_p0, m_p1, m_p2 ) );
726         if ( !plane3_valid( plane ) ) {
727                 return;
728         }
729
730                                 brushsplit_t split = Brush_classifyPlane( *brush, m_split == eFront ? plane3_flipped( plane ) : plane );
731                                 if ( split.counts[ePlaneBack] && split.counts[ePlaneFront] ) {
732                                         // the plane intersects this brush
733                                         if ( m_split == eFrontAndBack ) {
734                                                 NodeSmartReference node( ( new BrushNode() )->node() );
735                                                 Brush* fragment = Node_getBrush( node );
736                                                 fragment->copy( *brush );
737                         std::shared_ptr<Face> newFace =
738                                 fragment->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
739                                                 if ( newFace != 0 && m_split != eFront ) {
740                                                         newFace->flipWinding();
741                                                 }
742                                                 fragment->removeEmptyFaces();
743                                                 ASSERT_MESSAGE( !fragment->empty(), "brush left with no faces after split" );
744
745                                                 Node_getTraversable( path.parent() )->insert( node );
746                                                 {
747                                                         scene::Path fragmentPath = path;
748                                                         fragmentPath.top() = makeReference( node.get() );
749                                                         selectPath( fragmentPath, true );
750                                                 }
751                                         }
752
753                 std::shared_ptr<Face> newFace = brush->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
754                                         if ( newFace != 0 && m_split == eFront ) {
755                                                 newFace->flipWinding();
756                                         }
757                                         brush->removeEmptyFaces();
758                                         ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after split" );
759                                 }
760                                 else
761                                 // the plane does not intersect this brush
762                                 if ( m_split != eFrontAndBack && split.counts[ePlaneBack] != 0 ) {
763                                         // the brush is "behind" the plane
764                                         Path_deleteTop( path );
765         }
766 }
767 };
768
769 void Scene_BrushSplitByPlane( scene::Graph& graph, const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, EBrushSplit split ){
770         TextureProjection projection;
771         TexDef_Construct_Default( projection );
772         graph.traverse( BrushSplitByPlaneSelected( p0, p1, p2, shader, projection, split ) );
773         SceneChangeNotify();
774 }
775
776
777 class BrushInstanceSetClipPlane : public scene::Graph::Walker
778 {
779 Plane3 m_plane;
780 public:
781 BrushInstanceSetClipPlane( const Plane3& plane )
782         : m_plane( plane ){
783 }
784
785 bool pre( const scene::Path& path, scene::Instance& instance ) const {
786         BrushInstance* brush = Instance_getBrush( instance );
787         if ( brush != 0
788                  && path.top().get().visible()
789                  && brush->isSelected() ) {
790                 BrushInstance& brushInstance = *brush;
791                 brushInstance.setClipPlane( m_plane );
792         }
793         return true;
794 }
795 };
796
797 void Scene_BrushSetClipPlane( scene::Graph& graph, const Plane3& plane ){
798         graph.traverse( BrushInstanceSetClipPlane( plane ) );
799 }
800
801 /*
802    =============
803    CSG_Merge
804    =============
805  */
806 bool Brush_merge( Brush& brush, const brush_vector_t& in, bool onlyshape ){
807         // gather potential outer faces
808
809         {
810                 typedef std::vector<const Face*> Faces;
811                 Faces faces;
812                 for ( brush_vector_t::const_iterator i( in.begin() ); i != in.end(); ++i )
813                 {
814                         ( *i )->evaluateBRep();
815                         for ( Brush::const_iterator j( ( *i )->begin() ); j != ( *i )->end(); ++j )
816                         {
817                                 if ( !( *j )->contributes() ) {
818                                         continue;
819                                 }
820
821                                 const Face& face1 = *( *j );
822
823                                 bool skip = false;
824                                 // test faces of all input brushes
825                                 //!\todo SPEEDUP: Flag already-skip faces and only test brushes from i+1 upwards.
826                                 for ( brush_vector_t::const_iterator k( in.begin() ); !skip && k != in.end(); ++k )
827                                 {
828                                         if ( k != i ) { // don't test a brush against itself
829                                                 for ( Brush::const_iterator l( ( *k )->begin() ); !skip && l != ( *k )->end(); ++l )
830                                                 {
831                                                         const Face& face2 = *( *l );
832
833                                                         // face opposes another face
834                                                         if ( plane3_opposing( face1.plane3(), face2.plane3() ) ) {
835                                                                 // skip opposing planes
836                                                                 skip  = true;
837                                                                 break;
838                                                         }
839                                                 }
840                                         }
841                                 }
842
843                                 // check faces already stored
844                                 for ( Faces::const_iterator m = faces.begin(); !skip && m != faces.end(); ++m )
845                                 {
846                                         const Face& face2 = *( *m );
847
848                                         // face equals another face
849                                         if ( plane3_equal( face1.plane3(), face2.plane3() ) ) {
850                                                 //if the texture/shader references should be the same but are not
851                                                 if ( !onlyshape && !shader_equal( face1.getShader().getShader(), face2.getShader().getShader() ) ) {
852                                                         return false;
853                                                 }
854                                                 // skip duplicate planes
855                                                 skip = true;
856                                                 break;
857                                         }
858
859                                         // face1 plane intersects face2 winding or vice versa
860                                         if ( Winding_PlanesConcave( face1.getWinding(), face2.getWinding(), face1.plane3(), face2.plane3() ) ) {
861                                                 // result would not be convex
862                                                 return false;
863                                         }
864                                 }
865
866                                 if ( !skip ) {
867                                         faces.push_back( &face1 );
868                                 }
869                         }
870                 }
871                 for ( Faces::const_iterator i = faces.begin(); i != faces.end(); ++i )
872                 {
873                         if ( !brush.addFace( *( *i ) ) ) {
874                                 // result would have too many sides
875                                 return false;
876                         }
877                 }
878         }
879
880         brush.removeEmptyFaces();
881
882         return true;
883 }
884
885 void CSG_Merge( void ){
886         brush_vector_t selected_brushes;
887
888         // remove selected
889         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
890
891         if ( selected_brushes.empty() ) {
892                 globalOutputStream() << "CSG Merge: No brushes selected.\n";
893                 return;
894         }
895
896         if ( selected_brushes.size() < 2 ) {
897                 globalOutputStream() << "CSG Merge: At least two brushes have to be selected.\n";
898                 return;
899         }
900
901         globalOutputStream() << "CSG Merge: Merging " << Unsigned( selected_brushes.size() ) << " brushes.\n";
902
903         UndoableCommand undo( "brushMerge" );
904
905         scene::Path merged_path = GlobalSelectionSystem().ultimateSelected().path();
906
907         NodeSmartReference node( ( new BrushNode() )->node() );
908         Brush* brush = Node_getBrush( node );
909         // if the new brush would not be convex
910         if ( !Brush_merge( *brush, selected_brushes, true ) ) {
911                 globalOutputStream() << "CSG Merge: Failed - result would not be convex.\n";
912         }
913         else
914         {
915                 ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after merge" );
916
917                 // free the original brushes
918                 GlobalSceneGraph().traverse( BrushDeleteSelected( merged_path.parent().get_pointer() ) );
919
920                 merged_path.pop();
921                 Node_getTraversable( merged_path.top() )->insert( node );
922                 merged_path.push( makeReference( node.get() ) );
923
924                 selectPath( merged_path, true );
925
926                 globalOutputStream() << "CSG Merge: Succeeded.\n";
927                 SceneChangeNotify();
928         }
929 }
930
931
932
933
934
935
936 /*
937    =============
938    CSG_Tool
939    =============
940  */
941 #include "mainframe.h"
942 #include <gtk/gtk.h>
943 #include "gtkutil/dialog.h"
944 #include "gtkutil/button.h"
945 #include "gtkutil/accelerator.h"
946
947 struct CSGToolDialog
948 {
949         GtkSpinButton* spin;
950         bool allocated{false};
951         ui::Window window{ui::null};
952         GtkToggleButton *radXYZ, *radX, *radY, *radZ, *caulk, *removeInner;
953 };
954
955 CSGToolDialog g_csgtool_dialog;
956
957 DoubleVector3 getExclusion(){
958         if( gtk_toggle_button_get_active( g_csgtool_dialog.radX ) ){
959                 return DoubleVector3( 1, 0, 0 );
960         }
961         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radY ) ){
962                 return DoubleVector3( 0, 1, 0 );
963         }
964         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radZ ) ){
965                 return DoubleVector3( 0, 0, 1 );
966         }
967         return DoubleVector3( 0, 0, 0 );
968 }
969
970 bool getCaulk(){
971                 if( gtk_toggle_button_get_active( g_csgtool_dialog.caulk ) ){
972                 return true;
973         }
974         return false;
975 }
976
977 bool getRemoveInner(){
978                 if( gtk_toggle_button_get_active( g_csgtool_dialog.removeInner ) ){
979                 return true;
980         }
981         return false;
982 }
983
984 class BrushFaceOffset
985 {
986 float offset;
987 public:
988 BrushFaceOffset( float offset )
989         : offset( offset ){
990 }
991 void operator()( BrushInstance& brush ) const {
992         double mindot = 0;
993         double maxdot = 0;
994         Brush_forEachFace( brush, FaceExclude( getExclusion(), mindot, maxdot ) );
995         Brush_forEachFace( brush, FaceOffset( offset, getExclusion(), mindot, maxdot ) );
996 }
997 };
998
999 //=================DLG
1000
1001 static gboolean CSGdlg_HollowDiag( GtkWidget *widget, CSGToolDialog* dialog ){
1002         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1003         UndoableCommand undo( "brushHollow::Diag" );
1004         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, diag ) );
1005         if( getRemoveInner() )
1006                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1007         SceneChangeNotify();
1008         return TRUE;
1009 }
1010
1011 static gboolean CSGdlg_HollowWrap( GtkWidget *widget, CSGToolDialog* dialog ){
1012         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1013         UndoableCommand undo( "brushHollow::Wrap" );
1014         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, wrap ) );
1015         if( getRemoveInner() )
1016                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1017         SceneChangeNotify();
1018         return TRUE;
1019 }
1020
1021 static gboolean CSGdlg_HollowExtrude( GtkWidget *widget, CSGToolDialog* dialog ){
1022         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1023         UndoableCommand undo( "brushHollow::Extrude" );
1024         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, extrude ) );
1025         if( getRemoveInner() )
1026                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1027         SceneChangeNotify();
1028         return TRUE;
1029 }
1030
1031 static gboolean CSGdlg_HollowPull( GtkWidget *widget, CSGToolDialog* dialog ){
1032         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1033         UndoableCommand undo( "brushHollow::Pull" );
1034         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, pull ) );
1035         if( getRemoveInner() )
1036                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1037         SceneChangeNotify();
1038         return TRUE;
1039 }
1040
1041 static gboolean CSGdlg_BrushShrink( GtkWidget *widget, CSGToolDialog* dialog ){
1042         gtk_spin_button_update ( dialog->spin );
1043         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1044         offset *= -1;
1045         UndoableCommand undo( "Shrink brush" );
1046 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
1047         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1048         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1049         SceneChangeNotify();
1050         return TRUE;
1051 }
1052
1053 static gboolean CSGdlg_BrushExpand( GtkWidget *widget, CSGToolDialog* dialog ){
1054         gtk_spin_button_update ( dialog->spin );
1055         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1056         UndoableCommand undo( "Expand brush" );
1057 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
1058         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1059         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1060         SceneChangeNotify();
1061         return TRUE;
1062 }
1063
1064 static gboolean CSGdlg_grid2spin( GtkWidget *widget, CSGToolDialog* dialog ){
1065         gtk_spin_button_set_value( dialog->spin, GetGridSize() );
1066         return TRUE;
1067 }
1068
1069 static gboolean CSGdlg_delete( GtkWidget *widget, GdkEventAny *event, CSGToolDialog* dialog ){
1070         gtk_widget_hide( GTK_WIDGET( dialog->window ) );
1071         return TRUE;
1072 }
1073
1074 void CSG_Tool(){
1075         // FIXME: there is probably improvements to do less raw GTK stuff, more GTK wrapper
1076         if ( !g_csgtool_dialog.allocated ) {
1077                 g_csgtool_dialog.allocated = true;
1078                 g_csgtool_dialog.window = MainFrame_getWindow().create_dialog_window( "CSG Tool", G_CALLBACK( CSGdlg_delete ), &g_csgtool_dialog );
1079                 gtk_window_set_type_hint( g_csgtool_dialog.window, GDK_WINDOW_TYPE_HINT_UTILITY );
1080
1081                 //GtkAccelGroup* accel = gtk_accel_group_new();
1082                 //gtk_window_add_accel_group( g_csgtool_dialog.window, accel );
1083                 global_accel_connect_window( g_csgtool_dialog.window );
1084
1085                 {
1086                         auto hbox = create_dialog_hbox( 4, 4 );
1087                         gtk_container_add( GTK_CONTAINER( g_csgtool_dialog.window ), GTK_WIDGET( hbox ) );
1088                         {
1089                                 auto table = create_dialog_table( 3, 8, 4, 4 );
1090                                 gtk_box_pack_start( GTK_BOX( hbox ), GTK_WIDGET( table ), TRUE, TRUE, 0 );
1091                                 {
1092                                         //GtkWidget* label = gtk_label_new( "<->" );
1093                                         //gtk_widget_show( label );
1094                                         auto button = ui::Button( "Grid->" );
1095                                         table.attach( button, {0, 1, 0, 1}, {0, 0} );
1096                                         button.show();
1097                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_grid2spin ), &g_csgtool_dialog );
1098                                 }
1099                                 {
1100                                         GtkAdjustment* adj = GTK_ADJUSTMENT( gtk_adjustment_new( 16, 0, 9999, 1, 10, 0 ) );
1101                                         GtkSpinButton* spin = GTK_SPIN_BUTTON( gtk_spin_button_new( adj, 1, 3 ) );
1102                                         gtk_widget_show( GTK_WIDGET( spin ) );
1103                                         gtk_widget_set_tooltip_text( GTK_WIDGET( spin ), "Thickness" );
1104                                         gtk_table_attach( table, GTK_WIDGET( spin ), 1, 2, 0, 1,
1105                                                                           (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1106                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1107                                         gtk_widget_set_size_request( GTK_WIDGET( spin ), 64, -1 );
1108                                         gtk_spin_button_set_numeric( spin, TRUE );
1109
1110                                         g_csgtool_dialog.spin = spin;
1111                                 }
1112                                 {
1113                                         //radio button group for choosing the exclude axis
1114                                         GtkWidget* radXYZ = gtk_radio_button_new_with_label( NULL, "XYZ" );
1115                                         GtkWidget* radX = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-X" );
1116                                         GtkWidget* radY = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Y" );
1117                                         GtkWidget* radZ = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Z" );
1118                                         gtk_widget_show( radXYZ );
1119                                         gtk_widget_show( radX );
1120                                         gtk_widget_show( radY );
1121                                         gtk_widget_show( radZ );
1122
1123                                         gtk_table_attach( table, radXYZ, 2, 3, 0, 1,
1124                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1125                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1126                                         gtk_table_attach( table, radX, 3, 4, 0, 1,
1127                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1128                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1129                                         gtk_table_attach( table, radY, 4, 5, 0, 1,
1130                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1131                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1132                                         gtk_table_attach( table, radZ, 5, 6, 0, 1,
1133                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1134                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1135
1136                                         g_csgtool_dialog.radXYZ = GTK_TOGGLE_BUTTON( radXYZ );
1137                                         g_csgtool_dialog.radX = GTK_TOGGLE_BUTTON( radX );
1138                                         g_csgtool_dialog.radY = GTK_TOGGLE_BUTTON( radY );
1139                                         g_csgtool_dialog.radZ = GTK_TOGGLE_BUTTON( radZ );
1140                                 }
1141                                 {
1142                                         GtkWidget* button = gtk_toggle_button_new();
1143                                         auto ubutton = ui::Button::from( button );
1144                                         button_set_icon( ubutton, "f-caulk.png" );
1145                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1146                                         table.attach( ubutton, { 6, 7, 0, 1 }, { GTK_EXPAND, 0 } );
1147                                         gtk_widget_set_tooltip_text( button, "Caulk some faces" );
1148                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1149                                         ubutton.show();
1150                                         g_csgtool_dialog.caulk = GTK_TOGGLE_BUTTON( button );
1151                                 }
1152                                 {
1153                                         GtkWidget* button = gtk_toggle_button_new();
1154                                         auto ubutton = ui::Button::from( button );
1155                                         button_set_icon( ubutton, "csgtool_removeinner.png" );
1156                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1157                                         table.attach( ubutton, { 7, 8, 0, 1 }, { GTK_EXPAND, 0 } );
1158                                         gtk_widget_set_tooltip_text( button, "Remove inner brush" );
1159                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1160                                         ubutton.show();
1161                                         g_csgtool_dialog.removeInner = GTK_TOGGLE_BUTTON( button );
1162                                 }
1163                                 {
1164                                         GtkWidget* sep = gtk_hseparator_new();
1165                                         gtk_widget_show( sep );
1166                                         gtk_table_attach( table, sep, 0, 8, 1, 2,
1167                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1168                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1169                                 }
1170                                 {
1171                                         GtkWidget* button = gtk_button_new();
1172                                         auto ubutton = ui::Button::from( button );
1173                                         button_set_icon( ubutton, "csgtool_shrink.png" );
1174                                         table.attach( ubutton, { 0, 1, 2, 3 }, { GTK_EXPAND, 0 } );
1175                                         gtk_widget_set_tooltip_text( button, "Shrink brush" );
1176                                         ubutton.show();
1177                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushShrink ), &g_csgtool_dialog );
1178                                 }
1179                                 {
1180                                         GtkWidget* button = gtk_button_new();
1181                                         auto ubutton = ui::Button::from( button );
1182                                         button_set_icon( ubutton, "csgtool_expand.png" );
1183                                         table.attach( ubutton, { 1, 2, 2, 3 }, { GTK_EXPAND, 0 } );
1184                                         gtk_widget_set_tooltip_text( button, "Expand brush" );
1185                                         ubutton.show();
1186                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushExpand ), &g_csgtool_dialog );
1187                                 }
1188                                 {
1189                                         GtkWidget* button = gtk_button_new();
1190                                         auto ubutton = ui::Button::from( button );
1191                                         button_set_icon( ubutton, "csgtool_diagonal.png" );
1192                                         table.attach( ubutton, { 3, 4, 2, 3 }, { GTK_EXPAND, 0 } );
1193                                         gtk_widget_set_tooltip_text( button, "Hollow::diagonal joints" );
1194                                         ubutton.show();
1195                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowDiag ), &g_csgtool_dialog );
1196                                 }
1197                                 {
1198                                         GtkWidget* button = gtk_button_new();
1199                                         auto ubutton = ui::Button::from( button );
1200                                         button_set_icon( ubutton, "csgtool_wrap.png" );
1201                                         table.attach( ubutton, { 4, 5, 2, 3 }, { GTK_EXPAND, 0 } );
1202                                         gtk_widget_set_tooltip_text( button, "Hollow::warp" );
1203                                         ubutton.show();
1204                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowWrap ), &g_csgtool_dialog );
1205                                 }
1206                                 {
1207                                         GtkWidget* button = gtk_button_new();
1208                                         auto ubutton = ui::Button::from( button );
1209                                         button_set_icon( ubutton, "csgtool_extrude.png" );
1210                                         table.attach( ubutton, { 5, 6, 2, 3 }, { GTK_EXPAND, 0 } );
1211                                         gtk_widget_set_tooltip_text( button, "Hollow::extrude faces" );
1212                                         ubutton.show();
1213                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowExtrude ), &g_csgtool_dialog );
1214                                 }
1215                                 {
1216                                         GtkWidget* button = gtk_button_new();
1217                                         auto ubutton = ui::Button::from( button );
1218                                         button_set_icon( ubutton, "csgtool_pull.png" );
1219                                         table.attach( ubutton, { 6, 7, 2, 3 }, { GTK_EXPAND, 0 } );
1220                                         gtk_widget_set_tooltip_text( button, "Hollow::pull faces" );
1221                                         ubutton.show();
1222                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowPull ), &g_csgtool_dialog );
1223                                 }
1224
1225                         }
1226                 }
1227         }
1228
1229         gtk_widget_show( GTK_WIDGET( g_csgtool_dialog.window ) );
1230         gtk_window_present( g_csgtool_dialog.window );
1231 }
1232