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