]> git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/csg.cpp
Merge commit '37bef590b1d2161b16b2bd33349fc8b56af60920' 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                         Path_deleteTop( path );
425                         if( Node_getTraversable( path.parent() )->empty() ){
426                                 m_eraseParent = true;
427                                 //globalOutputStream() << "Empty node?!.\n";
428                         }
429                 }
430         }
431         if( m_eraseParent && !Node_isPrimitive( path.top() ) && path.size() > 1 ){
432                 //globalOutputStream() << "about to Delete empty node!.\n";
433                 m_eraseParent = false;
434                 Entity* entity = Node_getEntity( path.top() );
435                 if ( entity != 0 && path.top().get_pointer() != Map_FindWorldspawn( g_map )
436                         && Node_getTraversable( path.top() )->empty() && path.top().get_pointer() != m_keepNode ) {
437                         //globalOutputStream() << "now Deleting empty node!.\n";
438                         Path_deleteTop( path );
439                 }
440         }
441 }
442 };
443
444 /*
445    =============
446    CSG_MakeRoom
447    =============
448  */
449 void CSG_MakeRoom( void ){
450         UndoableCommand undo( "makeRoom" );
451         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( GetGridSize(), room ) );
452         GlobalSceneGraph().traverse( BrushDeleteSelected() );
453         SceneChangeNotify();
454 }
455
456 template<typename Type>
457 class RemoveReference
458 {
459 public:
460 typedef Type type;
461 };
462
463 template<typename Type>
464 class RemoveReference<Type&>
465 {
466 public:
467 typedef Type type;
468 };
469
470 template<typename Functor>
471 class Dereference
472 {
473 const Functor& functor;
474 public:
475 Dereference( const Functor& functor ) : functor( functor ){
476 }
477 get_result_type<Functor> operator()( typename RemoveReference<get_argument<Functor, 0>>::type *firstArgument ) const {
478         return functor( *firstArgument );
479 }
480 };
481
482 template<typename Functor>
483 inline Dereference<Functor> makeDereference( const Functor& functor ){
484         return Dereference<Functor>( functor );
485 }
486
487 template<typename Caller>
488 class BindArguments1
489 {
490 typedef get_argument<Caller, 1> FirstBound;
491 FirstBound firstBound;
492 public:
493 BindArguments1( FirstBound firstBound )
494         : firstBound( firstBound ){
495 }
496
497 get_result_type<Caller> operator()( get_argument<Caller, 0> firstArgument ) const {
498         return Caller::call( firstArgument, firstBound );
499 }
500 };
501
502 template<typename Caller>
503 class BindArguments2
504 {
505 typedef get_argument<Caller, 1> FirstBound;
506 typedef get_argument<Caller, 2> SecondBound;
507 FirstBound firstBound;
508 SecondBound secondBound;
509 public:
510 BindArguments2( FirstBound firstBound, SecondBound secondBound )
511         : firstBound( firstBound ), secondBound( secondBound ){
512 }
513
514 get_result_type<Caller> operator()( get_argument<Caller, 0> firstArgument ) const {
515         return Caller::call( firstArgument, firstBound, secondBound );
516 }
517 };
518
519 template<typename Caller, typename FirstBound, typename SecondBound>
520 BindArguments2<Caller> bindArguments( const Caller& caller, FirstBound firstBound, SecondBound secondBound ){
521         return BindArguments2<Caller>( firstBound, secondBound );
522 }
523
524 inline bool Face_testPlane( const Face& face, const Plane3& plane, bool flipped ){
525         return face.contributes() && !Winding_TestPlane( face.getWinding(), plane, flipped );
526 }
527
528 typedef Function<bool ( const Face &, const Plane3 &, bool ), Face_testPlane> FaceTestPlane;
529
530
531 /// \brief Returns true if
532 /// \li !flipped && brush is BACK or ON
533 /// \li flipped && brush is FRONT or ON
534 bool Brush_testPlane( const Brush& brush, const Plane3& plane, bool flipped ){
535         brush.evaluateBRep();
536         for ( Brush::const_iterator i( brush.begin() ); i != brush.end(); ++i )
537         {
538                 if ( Face_testPlane( *( *i ), plane, flipped ) ) {
539                         return false;
540                 }
541         }
542         return true;
543 }
544
545 brushsplit_t Brush_classifyPlane( const Brush& brush, const Plane3& plane ){
546         brush.evaluateBRep();
547         brushsplit_t split;
548         for ( Brush::const_iterator i( brush.begin() ); i != brush.end(); ++i )
549         {
550                 if ( ( *i )->contributes() ) {
551                         split += Winding_ClassifyPlane( ( *i )->getWinding(), plane );
552                 }
553         }
554         return split;
555 }
556
557 bool Brush_subtract( const Brush& brush, const Brush& other, brush_vector_t& ret_fragments ){
558         if ( aabb_intersects_aabb( brush.localAABB(), other.localAABB() ) ) {
559                 brush_vector_t fragments;
560                 fragments.reserve( other.size() );
561                 Brush back( brush );
562
563                 for ( const std::shared_ptr<Face>& b : other )
564                 {
565                         if ( b->contributes() ) {
566                                 brushsplit_t split = Brush_classifyPlane( back, b->plane3() );
567                                 if ( split.counts[ePlaneFront] != 0
568                                          && split.counts[ePlaneBack] != 0 ) {
569                                         fragments.push_back( new Brush( back ) );
570                                         std::shared_ptr<Face> newFace = fragments.back()->addFace( *b );
571                                         if ( newFace != nullptr ) {
572                                                 newFace->flipWinding();
573                                         }
574                                         back.addFace( *b );
575                                 }
576                                 else if ( split.counts[ePlaneBack] == 0 ) {
577                                         for ( Brush *i : fragments ) {
578                                                 delete( i );
579                                         }
580                                         fragments.clear();
581                                         return false;
582                                 }
583                         }
584                 }
585                 ret_fragments.insert( ret_fragments.end(), fragments.begin(), fragments.end() );
586                 return true;
587         }
588         return false;
589 }
590
591 class SubtractBrushesFromUnselected : public scene::Graph::Walker
592 {
593 const brush_vector_t& m_brushlist;
594 std::size_t& m_before;
595 std::size_t& m_after;
596 mutable bool m_eraseParent;
597 public:
598 SubtractBrushesFromUnselected( const brush_vector_t& brushlist, std::size_t& before, std::size_t& after )
599         : m_brushlist( brushlist ), m_before( before ), m_after( after ), m_eraseParent( false ){
600 }
601
602 bool pre( const scene::Path& path, scene::Instance& instance ) const {
603         if ( path.top().get().visible() ) {
604                 return true;
605         }
606         return false;
607 }
608
609 void post( const scene::Path& path, scene::Instance& instance ) const {
610         if ( path.top().get().visible() ) {
611                 Brush* brush = Node_getBrush( path.top() );
612                 if ( brush != 0
613                          && !Instance_getSelectable( instance )->isSelected() ) {
614                         brush_vector_t buffer[2];
615                         bool swap = false;
616                         Brush* original = new Brush( *brush );
617                         buffer[static_cast<std::size_t>( swap )].push_back( original );
618
619                         {
620                                 for ( brush_vector_t::const_iterator i( m_brushlist.begin() ); i != m_brushlist.end(); ++i )
621                                 {
622                                         for ( brush_vector_t::iterator j( buffer[static_cast<std::size_t>( swap )].begin() ); j != buffer[static_cast<std::size_t>( swap )].end(); ++j )
623                                         {
624                                                 if ( Brush_subtract( *( *j ), *( *i ), buffer[static_cast<std::size_t>( !swap )] ) ) {
625                                                         delete ( *j );
626                                                 }
627                                                 else
628                                                 {
629                                                         buffer[static_cast<std::size_t>( !swap )].push_back( ( *j ) );
630                                                 }
631                                         }
632                                         buffer[static_cast<std::size_t>( swap )].clear();
633                                         swap = !swap;
634                                 }
635                         }
636
637                         brush_vector_t& out = buffer[static_cast<std::size_t>( swap )];
638
639                         if ( out.size() == 1 && out.back() == original ) {
640                                 delete original;
641                         }
642                         else
643                         {
644                                 ++m_before;
645                                 for ( Brush *b : out ) {
646                                         ++m_after;
647                                         b->removeEmptyFaces();
648                                         if ( !b->empty() ) {
649                                                 NodeSmartReference node( ( new BrushNode() )->node() );
650                                                 Node_getBrush( node )->copy( *b );
651                                                 Node_getTraversable( path.parent() )->insert( node );
652                                         }
653                                         delete b;
654                                 }
655                                 Path_deleteTop( path );
656                                 if( Node_getTraversable( path.parent() )->empty() ){
657                                         m_eraseParent = true;
658                                 }
659                         }
660                 }
661         }
662         if( m_eraseParent && !Node_isPrimitive( path.top() ) && path.size() > 1 ){
663                 m_eraseParent = false;
664                 Entity* entity = Node_getEntity( path.top() );
665                 if ( entity != 0 && path.top().get_pointer() != Map_FindWorldspawn( g_map )
666                         && Node_getTraversable( path.top() )->empty() ) {
667                         Path_deleteTop( path );
668                 }
669         }
670 }
671 };
672
673 void CSG_Subtract(){
674         brush_vector_t selected_brushes;
675         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
676
677         if ( selected_brushes.empty() ) {
678                 globalOutputStream() << "CSG Subtract: No brushes selected.\n";
679         } else {
680                 globalOutputStream() << "CSG Subtract: Subtracting " << Unsigned( selected_brushes.size() ) << " brushes.\n";
681
682                 UndoableCommand undo( "brushSubtract" );
683
684                 // subtract selected from unselected
685                 std::size_t before = 0;
686                 std::size_t after = 0;
687                 GlobalSceneGraph().traverse( SubtractBrushesFromUnselected( selected_brushes, before, after ) );
688                 globalOutputStream() << "CSG Subtract: Result: "
689                                                          << Unsigned( after ) << " fragment" << ( after == 1 ? "" : "s" )
690                                                          << " from " << Unsigned( before ) << " brush" << ( before == 1 ? "" : "es" ) << ".\n";
691
692                 SceneChangeNotify();
693         }
694 }
695
696 class BrushSplitByPlaneSelected : public scene::Graph::Walker
697 {
698 const Vector3& m_p0;
699 const Vector3& m_p1;
700 const Vector3& m_p2;
701 const char* m_shader;
702 const TextureProjection& m_projection;
703 EBrushSplit m_split;
704 public:
705 BrushSplitByPlaneSelected( const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, const TextureProjection& projection, EBrushSplit split )
706         : m_p0( p0 ), m_p1( p1 ), m_p2( p2 ), m_shader( shader ), m_projection( projection ), m_split( split ){
707 }
708
709 bool pre( const scene::Path& path, scene::Instance& instance ) const {
710         return true;
711 }
712
713 void post( const scene::Path& path, scene::Instance& instance ) const {
714         if ( !path.top().get().visible() ) {
715                 return;
716         }
717
718                 Brush* brush = Node_getBrush( path.top() );
719         if ( brush == nullptr || !Instance_getSelectable( instance )->isSelected() ) {
720                 return;
721         }
722
723                         Plane3 plane( plane3_for_points( m_p0, m_p1, m_p2 ) );
724         if ( !plane3_valid( plane ) ) {
725                 return;
726         }
727
728                                 brushsplit_t split = Brush_classifyPlane( *brush, m_split == eFront ? plane3_flipped( plane ) : plane );
729                                 if ( split.counts[ePlaneBack] && split.counts[ePlaneFront] ) {
730                                         // the plane intersects this brush
731                                         if ( m_split == eFrontAndBack ) {
732                                                 NodeSmartReference node( ( new BrushNode() )->node() );
733                                                 Brush* fragment = Node_getBrush( node );
734                                                 fragment->copy( *brush );
735                         std::shared_ptr<Face> newFace =
736                                 fragment->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
737                                                 if ( newFace != 0 && m_split != eFront ) {
738                                                         newFace->flipWinding();
739                                                 }
740                                                 fragment->removeEmptyFaces();
741                                                 ASSERT_MESSAGE( !fragment->empty(), "brush left with no faces after split" );
742
743                                                 Node_getTraversable( path.parent() )->insert( node );
744                                                 {
745                                                         scene::Path fragmentPath = path;
746                                                         fragmentPath.top() = makeReference( node.get() );
747                                                         selectPath( fragmentPath, true );
748                                                 }
749                                         }
750
751                 std::shared_ptr<Face> newFace = brush->addPlane( m_p0, m_p1, m_p2, m_shader, m_projection );
752                                         if ( newFace != 0 && m_split == eFront ) {
753                                                 newFace->flipWinding();
754                                         }
755                                         brush->removeEmptyFaces();
756                                         ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after split" );
757                                 }
758                                 else
759                                 // the plane does not intersect this brush
760                                 if ( m_split != eFrontAndBack && split.counts[ePlaneBack] != 0 ) {
761                                         // the brush is "behind" the plane
762                                         Path_deleteTop( path );
763         }
764 }
765 };
766
767 void Scene_BrushSplitByPlane( scene::Graph& graph, const Vector3& p0, const Vector3& p1, const Vector3& p2, const char* shader, EBrushSplit split ){
768         TextureProjection projection;
769         TexDef_Construct_Default( projection );
770         graph.traverse( BrushSplitByPlaneSelected( p0, p1, p2, shader, projection, split ) );
771         SceneChangeNotify();
772 }
773
774
775 class BrushInstanceSetClipPlane : public scene::Graph::Walker
776 {
777 Plane3 m_plane;
778 public:
779 BrushInstanceSetClipPlane( const Plane3& plane )
780         : m_plane( plane ){
781 }
782
783 bool pre( const scene::Path& path, scene::Instance& instance ) const {
784         BrushInstance* brush = Instance_getBrush( instance );
785         if ( brush != 0
786                  && path.top().get().visible()
787                  && brush->isSelected() ) {
788                 BrushInstance& brushInstance = *brush;
789                 brushInstance.setClipPlane( m_plane );
790         }
791         return true;
792 }
793 };
794
795 void Scene_BrushSetClipPlane( scene::Graph& graph, const Plane3& plane ){
796         graph.traverse( BrushInstanceSetClipPlane( plane ) );
797 }
798
799 /*
800    =============
801    CSG_Merge
802    =============
803  */
804 bool Brush_merge( Brush& brush, const brush_vector_t& in, bool onlyshape ){
805         // gather potential outer faces
806
807         {
808                 typedef std::vector<const Face*> Faces;
809                 Faces faces;
810                 for ( brush_vector_t::const_iterator i( in.begin() ); i != in.end(); ++i )
811                 {
812                         ( *i )->evaluateBRep();
813                         for ( Brush::const_iterator j( ( *i )->begin() ); j != ( *i )->end(); ++j )
814                         {
815                                 if ( !( *j )->contributes() ) {
816                                         continue;
817                                 }
818
819                                 const Face& face1 = *( *j );
820
821                                 bool skip = false;
822                                 // test faces of all input brushes
823                                 //!\todo SPEEDUP: Flag already-skip faces and only test brushes from i+1 upwards.
824                                 for ( brush_vector_t::const_iterator k( in.begin() ); !skip && k != in.end(); ++k )
825                                 {
826                                         if ( k != i ) { // don't test a brush against itself
827                                                 for ( Brush::const_iterator l( ( *k )->begin() ); !skip && l != ( *k )->end(); ++l )
828                                                 {
829                                                         const Face& face2 = *( *l );
830
831                                                         // face opposes another face
832                                                         if ( plane3_opposing( face1.plane3(), face2.plane3() ) ) {
833                                                                 // skip opposing planes
834                                                                 skip  = true;
835                                                                 break;
836                                                         }
837                                                 }
838                                         }
839                                 }
840
841                                 // check faces already stored
842                                 for ( Faces::const_iterator m = faces.begin(); !skip && m != faces.end(); ++m )
843                                 {
844                                         const Face& face2 = *( *m );
845
846                                         // face equals another face
847                                         if ( plane3_equal( face1.plane3(), face2.plane3() ) ) {
848                                                 //if the texture/shader references should be the same but are not
849                                                 if ( !onlyshape && !shader_equal( face1.getShader().getShader(), face2.getShader().getShader() ) ) {
850                                                         return false;
851                                                 }
852                                                 // skip duplicate planes
853                                                 skip = true;
854                                                 break;
855                                         }
856
857                                         // face1 plane intersects face2 winding or vice versa
858                                         if ( Winding_PlanesConcave( face1.getWinding(), face2.getWinding(), face1.plane3(), face2.plane3() ) ) {
859                                                 // result would not be convex
860                                                 return false;
861                                         }
862                                 }
863
864                                 if ( !skip ) {
865                                         faces.push_back( &face1 );
866                                 }
867                         }
868                 }
869                 for ( Faces::const_iterator i = faces.begin(); i != faces.end(); ++i )
870                 {
871                         if ( !brush.addFace( *( *i ) ) ) {
872                                 // result would have too many sides
873                                 return false;
874                         }
875                 }
876         }
877
878         brush.removeEmptyFaces();
879
880         return true;
881 }
882
883 void CSG_Merge( void ){
884         brush_vector_t selected_brushes;
885
886         // remove selected
887         GlobalSceneGraph().traverse( BrushGatherSelected( selected_brushes ) );
888
889         if ( selected_brushes.empty() ) {
890                 globalOutputStream() << "CSG Merge: No brushes selected.\n";
891                 return;
892         }
893
894         if ( selected_brushes.size() < 2 ) {
895                 globalOutputStream() << "CSG Merge: At least two brushes have to be selected.\n";
896                 return;
897         }
898
899         globalOutputStream() << "CSG Merge: Merging " << Unsigned( selected_brushes.size() ) << " brushes.\n";
900
901         UndoableCommand undo( "brushMerge" );
902
903         scene::Path merged_path = GlobalSelectionSystem().ultimateSelected().path();
904
905         NodeSmartReference node( ( new BrushNode() )->node() );
906         Brush* brush = Node_getBrush( node );
907         // if the new brush would not be convex
908         if ( !Brush_merge( *brush, selected_brushes, true ) ) {
909                 globalOutputStream() << "CSG Merge: Failed - result would not be convex.\n";
910         }
911         else
912         {
913                 ASSERT_MESSAGE( !brush->empty(), "brush left with no faces after merge" );
914
915                 // free the original brushes
916                 GlobalSceneGraph().traverse( BrushDeleteSelected( merged_path.parent().get_pointer() ) );
917
918                 merged_path.pop();
919                 Node_getTraversable( merged_path.top() )->insert( node );
920                 merged_path.push( makeReference( node.get() ) );
921
922                 selectPath( merged_path, true );
923
924                 globalOutputStream() << "CSG Merge: Succeeded.\n";
925                 SceneChangeNotify();
926         }
927 }
928
929
930
931
932
933
934 /*
935    =============
936    CSG_Tool
937    =============
938  */
939 #include "mainframe.h"
940 #include <gtk/gtk.h>
941 #include "gtkutil/dialog.h"
942 #include "gtkutil/button.h"
943 #include "gtkutil/accelerator.h"
944
945 struct CSGToolDialog
946 {
947         GtkSpinButton* spin;
948         bool allocated{false};
949         ui::Window window{ui::null};
950         GtkToggleButton *radXYZ, *radX, *radY, *radZ, *caulk, *removeInner;
951 };
952
953 CSGToolDialog g_csgtool_dialog;
954
955 DoubleVector3 getExclusion(){
956         if( gtk_toggle_button_get_active( g_csgtool_dialog.radX ) ){
957                 return DoubleVector3( 1, 0, 0 );
958         }
959         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radY ) ){
960                 return DoubleVector3( 0, 1, 0 );
961         }
962         else if( gtk_toggle_button_get_active( g_csgtool_dialog.radZ ) ){
963                 return DoubleVector3( 0, 0, 1 );
964         }
965         return DoubleVector3( 0, 0, 0 );
966 }
967
968 bool getCaulk(){
969                 if( gtk_toggle_button_get_active( g_csgtool_dialog.caulk ) ){
970                 return true;
971         }
972         return false;
973 }
974
975 bool getRemoveInner(){
976                 if( gtk_toggle_button_get_active( g_csgtool_dialog.removeInner ) ){
977                 return true;
978         }
979         return false;
980 }
981
982 class BrushFaceOffset
983 {
984 float offset;
985 public:
986 BrushFaceOffset( float offset )
987         : offset( offset ){
988 }
989 void operator()( BrushInstance& brush ) const {
990         double mindot = 0;
991         double maxdot = 0;
992         Brush_forEachFace( brush, FaceExclude( getExclusion(), mindot, maxdot ) );
993         Brush_forEachFace( brush, FaceOffset( offset, getExclusion(), mindot, maxdot ) );
994 }
995 };
996
997 //=================DLG
998
999 static gboolean CSGdlg_HollowDiag( GtkWidget *widget, CSGToolDialog* dialog ){
1000         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1001         UndoableCommand undo( "brushHollow::Diag" );
1002         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, diag ) );
1003         if( getRemoveInner() )
1004                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1005         SceneChangeNotify();
1006         return TRUE;
1007 }
1008
1009 static gboolean CSGdlg_HollowWrap( GtkWidget *widget, CSGToolDialog* dialog ){
1010         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1011         UndoableCommand undo( "brushHollow::Wrap" );
1012         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, wrap ) );
1013         if( getRemoveInner() )
1014                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1015         SceneChangeNotify();
1016         return TRUE;
1017 }
1018
1019 static gboolean CSGdlg_HollowExtrude( GtkWidget *widget, CSGToolDialog* dialog ){
1020         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1021         UndoableCommand undo( "brushHollow::Extrude" );
1022         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, extrude ) );
1023         if( getRemoveInner() )
1024                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1025         SceneChangeNotify();
1026         return TRUE;
1027 }
1028
1029 static gboolean CSGdlg_HollowPull( GtkWidget *widget, CSGToolDialog* dialog ){
1030         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1031         UndoableCommand undo( "brushHollow::Pull" );
1032         GlobalSceneGraph().traverse( BrushHollowSelectedWalker( offset, pull ) );
1033         if( getRemoveInner() )
1034                 GlobalSceneGraph().traverse( BrushDeleteSelected() );
1035         SceneChangeNotify();
1036         return TRUE;
1037 }
1038
1039 static gboolean CSGdlg_BrushShrink( GtkWidget *widget, CSGToolDialog* dialog ){
1040         gtk_spin_button_update ( dialog->spin );
1041         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1042         offset *= -1;
1043         UndoableCommand undo( "Shrink brush" );
1044 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
1045         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1046         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1047         SceneChangeNotify();
1048         return TRUE;
1049 }
1050
1051 static gboolean CSGdlg_BrushExpand( GtkWidget *widget, CSGToolDialog* dialog ){
1052         gtk_spin_button_update ( dialog->spin );
1053         float offset = static_cast<float>( gtk_spin_button_get_value( dialog->spin ) );
1054         UndoableCommand undo( "Expand brush" );
1055 //      GlobalSceneGraph().traverse( OffsetBrushFacesSelectedWalker( offset ) );
1056         //Scene_ForEachSelectedBrush_ForEachFace( GlobalSceneGraph(), BrushFaceOffset( offset ) );
1057         Scene_forEachSelectedBrush( BrushFaceOffset( offset ) );
1058         SceneChangeNotify();
1059         return TRUE;
1060 }
1061
1062 static gboolean CSGdlg_grid2spin( GtkWidget *widget, CSGToolDialog* dialog ){
1063         gtk_spin_button_set_value( dialog->spin, GetGridSize() );
1064         return TRUE;
1065 }
1066
1067 static gboolean CSGdlg_delete( GtkWidget *widget, GdkEventAny *event, CSGToolDialog* dialog ){
1068         gtk_widget_hide( GTK_WIDGET( dialog->window ) );
1069         return TRUE;
1070 }
1071
1072 void CSG_Tool(){
1073         // FIXME: there is probably improvements to do less raw GTK stuff, more GTK wrapper
1074         if ( !g_csgtool_dialog.allocated ) {
1075                 g_csgtool_dialog.allocated = true;
1076                 g_csgtool_dialog.window = MainFrame_getWindow().create_dialog_window( "CSG Tool", G_CALLBACK( CSGdlg_delete ), &g_csgtool_dialog );
1077                 gtk_window_set_type_hint( g_csgtool_dialog.window, GDK_WINDOW_TYPE_HINT_UTILITY );
1078
1079                 //GtkAccelGroup* accel = gtk_accel_group_new();
1080                 //gtk_window_add_accel_group( g_csgtool_dialog.window, accel );
1081                 global_accel_connect_window( g_csgtool_dialog.window );
1082
1083                 {
1084                         auto hbox = create_dialog_hbox( 4, 4 );
1085                         gtk_container_add( GTK_CONTAINER( g_csgtool_dialog.window ), GTK_WIDGET( hbox ) );
1086                         {
1087                                 auto table = create_dialog_table( 3, 8, 4, 4 );
1088                                 gtk_box_pack_start( GTK_BOX( hbox ), GTK_WIDGET( table ), TRUE, TRUE, 0 );
1089                                 {
1090                                         //GtkWidget* label = gtk_label_new( "<->" );
1091                                         //gtk_widget_show( label );
1092                                         auto button = ui::Button( "Grid->" );
1093                                         table.attach( button, {0, 1, 0, 1}, {0, 0} );
1094                                         button.show();
1095                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_grid2spin ), &g_csgtool_dialog );
1096                                 }
1097                                 {
1098                                         GtkAdjustment* adj = GTK_ADJUSTMENT( gtk_adjustment_new( 16, 0, 9999, 1, 10, 0 ) );
1099                                         GtkSpinButton* spin = GTK_SPIN_BUTTON( gtk_spin_button_new( adj, 1, 3 ) );
1100                                         gtk_widget_show( GTK_WIDGET( spin ) );
1101                                         gtk_widget_set_tooltip_text( GTK_WIDGET( spin ), "Thickness" );
1102                                         gtk_table_attach( table, GTK_WIDGET( spin ), 1, 2, 0, 1,
1103                                                                           (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1104                                                                           (GtkAttachOptions) ( 0 ), 0, 0 );
1105                                         gtk_widget_set_size_request( GTK_WIDGET( spin ), 64, -1 );
1106                                         gtk_spin_button_set_numeric( spin, TRUE );
1107
1108                                         g_csgtool_dialog.spin = spin;
1109                                 }
1110                                 {
1111                                         //radio button group for choosing the exclude axis
1112                                         GtkWidget* radXYZ = gtk_radio_button_new_with_label( NULL, "XYZ" );
1113                                         GtkWidget* radX = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-X" );
1114                                         GtkWidget* radY = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Y" );
1115                                         GtkWidget* radZ = gtk_radio_button_new_with_label_from_widget( GTK_RADIO_BUTTON(radXYZ), "-Z" );
1116                                         gtk_widget_show( radXYZ );
1117                                         gtk_widget_show( radX );
1118                                         gtk_widget_show( radY );
1119                                         gtk_widget_show( radZ );
1120
1121                                         gtk_table_attach( table, radXYZ, 2, 3, 0, 1,
1122                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1123                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1124                                         gtk_table_attach( table, radX, 3, 4, 0, 1,
1125                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1126                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1127                                         gtk_table_attach( table, radY, 4, 5, 0, 1,
1128                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1129                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1130                                         gtk_table_attach( table, radZ, 5, 6, 0, 1,
1131                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1132                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1133
1134                                         g_csgtool_dialog.radXYZ = GTK_TOGGLE_BUTTON( radXYZ );
1135                                         g_csgtool_dialog.radX = GTK_TOGGLE_BUTTON( radX );
1136                                         g_csgtool_dialog.radY = GTK_TOGGLE_BUTTON( radY );
1137                                         g_csgtool_dialog.radZ = GTK_TOGGLE_BUTTON( radZ );
1138                                 }
1139                                 {
1140                                         GtkWidget* button = gtk_toggle_button_new();
1141                                         auto ubutton = ui::Button::from( button );
1142                                         button_set_icon( ubutton, "f-caulk.png" );
1143                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1144                                         table.attach( ubutton, { 6, 7, 0, 1 }, { GTK_EXPAND, 0 } );
1145                                         gtk_widget_set_tooltip_text( button, "Caulk some faces" );
1146                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1147                                         ubutton.show();
1148                                         g_csgtool_dialog.caulk = GTK_TOGGLE_BUTTON( button );
1149                                 }
1150                                 {
1151                                         GtkWidget* button = gtk_toggle_button_new();
1152                                         auto ubutton = ui::Button::from( button );
1153                                         button_set_icon( ubutton, "csgtool_removeinner.png" );
1154                                         gtk_button_set_relief( GTK_BUTTON( button ), GTK_RELIEF_NONE );
1155                                         table.attach( ubutton, { 7, 8, 0, 1 }, { GTK_EXPAND, 0 } );
1156                                         gtk_widget_set_tooltip_text( button, "Remove inner brush" );
1157                                         gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( button ), TRUE );
1158                                         ubutton.show();
1159                                         g_csgtool_dialog.removeInner = GTK_TOGGLE_BUTTON( button );
1160                                 }
1161                                 {
1162                                         GtkWidget* sep = gtk_hseparator_new();
1163                                         gtk_widget_show( sep );
1164                                         gtk_table_attach( table, sep, 0, 8, 1, 2,
1165                                                                         (GtkAttachOptions) ( GTK_EXPAND | GTK_FILL ),
1166                                                                         (GtkAttachOptions) ( 0 ), 0, 0 );
1167                                 }
1168                                 {
1169                                         GtkWidget* button = gtk_button_new();
1170                                         auto ubutton = ui::Button::from( button );
1171                                         button_set_icon( ubutton, "csgtool_shrink.png" );
1172                                         table.attach( ubutton, { 0, 1, 2, 3 }, { GTK_EXPAND, 0 } );
1173                                         gtk_widget_set_tooltip_text( button, "Shrink brush" );
1174                                         ubutton.show();
1175                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushShrink ), &g_csgtool_dialog );
1176                                 }
1177                                 {
1178                                         GtkWidget* button = gtk_button_new();
1179                                         auto ubutton = ui::Button::from( button );
1180                                         button_set_icon( ubutton, "csgtool_expand.png" );
1181                                         table.attach( ubutton, { 1, 2, 2, 3 }, { GTK_EXPAND, 0 } );
1182                                         gtk_widget_set_tooltip_text( button, "Expand brush" );
1183                                         ubutton.show();
1184                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_BrushExpand ), &g_csgtool_dialog );
1185                                 }
1186                                 {
1187                                         GtkWidget* button = gtk_button_new();
1188                                         auto ubutton = ui::Button::from( button );
1189                                         button_set_icon( ubutton, "csgtool_diagonal.png" );
1190                                         table.attach( ubutton, { 3, 4, 2, 3 }, { GTK_EXPAND, 0 } );
1191                                         gtk_widget_set_tooltip_text( button, "Hollow::diagonal joints" );
1192                                         ubutton.show();
1193                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowDiag ), &g_csgtool_dialog );
1194                                 }
1195                                 {
1196                                         GtkWidget* button = gtk_button_new();
1197                                         auto ubutton = ui::Button::from( button );
1198                                         button_set_icon( ubutton, "csgtool_wrap.png" );
1199                                         table.attach( ubutton, { 4, 5, 2, 3 }, { GTK_EXPAND, 0 } );
1200                                         gtk_widget_set_tooltip_text( button, "Hollow::warp" );
1201                                         ubutton.show();
1202                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowWrap ), &g_csgtool_dialog );
1203                                 }
1204                                 {
1205                                         GtkWidget* button = gtk_button_new();
1206                                         auto ubutton = ui::Button::from( button );
1207                                         button_set_icon( ubutton, "csgtool_extrude.png" );
1208                                         table.attach( ubutton, { 5, 6, 2, 3 }, { GTK_EXPAND, 0 } );
1209                                         gtk_widget_set_tooltip_text( button, "Hollow::extrude faces" );
1210                                         ubutton.show();
1211                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowExtrude ), &g_csgtool_dialog );
1212                                 }
1213                                 {
1214                                         GtkWidget* button = gtk_button_new();
1215                                         auto ubutton = ui::Button::from( button );
1216                                         button_set_icon( ubutton, "csgtool_pull.png" );
1217                                         table.attach( ubutton, { 6, 7, 2, 3 }, { GTK_EXPAND, 0 } );
1218                                         gtk_widget_set_tooltip_text( button, "Hollow::pull faces" );
1219                                         ubutton.show();
1220                                         g_signal_connect( G_OBJECT( button ), "clicked", G_CALLBACK( CSGdlg_HollowPull ), &g_csgtool_dialog );
1221                                 }
1222
1223                         }
1224                 }
1225         }
1226
1227         gtk_widget_show( GTK_WIDGET( g_csgtool_dialog.window ) );
1228         gtk_window_present( g_csgtool_dialog.window );
1229 }
1230