1 /**************************************************************************/
2 /*  Copyright 2012 Tim Day                                                */
3 /*                                                                        */
4 /*  This file is part of Evolvotron                                       */
5 /*                                                                        */
6 /*  Evolvotron is free software: you can redistribute it and/or modify    */
7 /*  it under the terms of the GNU General Public License as published by  */
8 /*  the Free Software Foundation, either version 3 of the License, or     */
9 /*  (at your option) any later version.                                   */
10 /*                                                                        */
11 /*  Evolvotron is distributed in the hope that it will be useful,         */
12 /*  but WITHOUT ANY WARRANTY; without even the implied warranty of        */
13 /*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         */
14 /*  GNU General Public License for more details.                          */
15 /*                                                                        */
16 /*  You should have received a copy of the GNU General Public License     */
17 /*  along with Evolvotron.  If not, see <http://www.gnu.org/licenses/>.   */
18 /**************************************************************************/
19 
20 /*! \file
21   \brief Implementation of class MutatableImageDisplay.
22 */
23 
24 #include "mutatable_image_display.h"
25 
26 #include "mutatable_image_display_big.h"
27 #include "evolvotron_main.h"
28 #include "mutatable_image_computer_task.h"
29 #include "transform_factory.h"
30 #include "function_pre_transform.h"
31 #include "function_top.h"
32 
33 /*! The constructor is passed:
34     - the owning widget (probably either a QGrid or null if top-level),
35     - the EvolvotronMain providing spawn and farm services,
36     - a flag to specify whether this is a fully functional display or a restricted one (e.g no spawning for a single top level display),
37     - a flag to specify whether the offscreen buffer has fixed size (in which case scrollbars may be used),
38     - and the size of the offscreen buffer (only used if fixed_size is true).
39     Note that we use Qt's WDestructiveCode flag to ensure the destructor is called on close
40  */
MutatableImageDisplay(EvolvotronMain * mn,bool full_functionality,bool fixed_size,const QSize & sz,uint f,uint fr)41 MutatableImageDisplay::MutatableImageDisplay(EvolvotronMain* mn,bool full_functionality,bool fixed_size,const QSize& sz,uint f,uint fr)
42   :_main(mn)
43   ,_full_functionality(full_functionality)
44   ,_fixed_size(fixed_size)
45   ,_image_size(sz)
46   ,_frames(f)
47   ,_framerate(fr)
48   ,_current_frame(0)
49   ,_animate_reverse(false)
50   ,_timer(0)
51   ,_resize_in_progress(false)
52   ,_current_display_level(0)
53   ,_current_display_multisample_grid(0)
54   ,_icon_serial(0LL)
55   ,_properties(0)
56   ,_menu(0)
57   ,_menu_big(0)
58   ,_menu_item_action_lock(0)
59   ,_serial(0LL)
60 {
61   setAttribute(Qt::WA_DeleteOnClose,true);
62 
63   setFocusPolicy(Qt::StrongFocus);
64 
65   _properties=new DialogMutatableImageDisplay(this);
66 
67   _menu=new QMenu(this);
68 
69   // Most items on the context menu aren't appropriate for a window displaying a single big image
70   if (_full_functionality)
71     {
72       _menu->addAction("Respawn",this,SLOT(menupick_respawn()));
73 
74       _menu->addSeparator();
75 
76       _menu->addAction("Spawn",this,SLOT(menupick_spawn()));
77       _menu->addAction("Spawn recoloured",this,SLOT(menupick_spawn_recoloured()));
78 
79       _menu_warped=_menu->addMenu("Spawn warped");
80       _menu_warped->addAction("Random Mix",this,SLOT(menupick_spawn_warped_random())  );
81       _menu_warped->addAction("Zoom In"   ,this,SLOT(menupick_spawn_warped_zoom_in()) );
82       _menu_warped->addAction("Zoom Out"  ,this,SLOT(menupick_spawn_warped_zoom_out()));
83       _menu_warped->addAction("Rotate"    ,this,SLOT(menupick_spawn_warped_rotate())  );
84       _menu_warped->addAction("Pan XY"    ,this,SLOT(menupick_spawn_warped_pan_xy())  );
85       _menu_warped->addAction("Pan X"     ,this,SLOT(menupick_spawn_warped_pan_x())   );
86       _menu_warped->addAction("Pan Y"     ,this,SLOT(menupick_spawn_warped_pan_y())   );
87       _menu_warped->addAction("Pan Z"     ,this,SLOT(menupick_spawn_warped_pan_z())   );
88 
89       _menu->addSeparator();
90 
91       _menu_item_action_lock=_menu->addAction("Lock",this,SLOT(menupick_lock()));
92       _menu_item_action_lock->setCheckable(true);
93 
94       _menu->addSeparator();
95     }
96 
97   _menu_big=_menu->addMenu("Enlarge");
98   _menu_big->addAction("Resizable",this,SLOT(menupick_big_resizable()));
99   _menu_big->addSeparator();
100   _menu_big->addAction("256x256"  ,this,SLOT(menupick_big_256x256()));
101   _menu_big->addAction("512x512"  ,this,SLOT(menupick_big_512x512())  );
102   _menu_big->addAction("768x768"  ,this,SLOT(menupick_big_768x768())  );
103   _menu_big->addAction("1024x1024",this,SLOT(menupick_big_1024x1024()));
104   _menu_big->addAction("2048x2048",this,SLOT(menupick_big_2048x2048()));
105   _menu_big->addAction("4096x4096",this,SLOT(menupick_big_4096x4096()));
106   _menu_big->addSeparator();
107   _menu_big->addAction("640x480"  ,this,SLOT(menupick_big_640x480())  );
108   _menu_big->addAction("1024x768" ,this,SLOT(menupick_big_1024x768()) );
109   _menu_big->addAction("1280x960" ,this,SLOT(menupick_big_1280x960()) );
110   _menu_big->addAction("1600x1200",this,SLOT(menupick_big_1600x1200()));
111 
112   _menu->addSeparator();
113 
114   _menu->addAction("Save image",this,SLOT(menupick_save_image()));
115   _menu->addAction("Save function",this,SLOT(menupick_save_function()));
116 
117   if (_full_functionality)
118     {
119       _menu->addAction("Load function",this,SLOT(menupick_load_function()));
120     }
121 
122   _menu->addSeparator();
123   _menu->addAction("Simplify function",this,SLOT(menupick_simplify()));
124   _menu->addAction("Properties...",this,SLOT(menupick_properties()));
125 
126   main().hello(this);
127 
128   if (_fixed_size)
129     {
130       setGeometry(0,0,image_size().width(),image_size().height());
131     }
132 
133   for (uint f=0;f<_frames;f++)
134     {
135       _offscreen_pixmaps.push_back(QPixmap());
136     }
137 
138   _timer=new QTimer(this);
139   connect
140     (
141      _timer,SIGNAL(timeout()),
142      this,SLOT(frame_advance())
143      );
144   if (_frames>1) _timer->start(1000/_framerate);
145 }
146 
147 /*! Destructor signs off from EvolvotronMain to prevent further attempts at completed task delivery.
148  */
~MutatableImageDisplay()149 MutatableImageDisplay::~MutatableImageDisplay()
150 {
151   assert(!_image_function.get() || _image_function->ok());
152 
153   // During app shutdown EvolvotronMain might have already been destroyed so only call it if it's there.
154   // Don't use main() because it asserts non-null.
155   if (_main)
156     {
157       farm().abort_for(this);
158       main().goodbye(this);
159     }
160 
161   _image_function.reset();
162   _offscreen_pixmaps.clear();
163 
164   _offscreen_images.clear();
165 }
166 
simplify_constants(bool single_action)167 uint MutatableImageDisplay::simplify_constants(bool single_action)
168 {
169   if (single_action) main().history().begin_action("simplify");
170 
171   uint old_nodes;
172   uint old_parameters;
173   uint old_depth;
174   uint old_width;
175   real old_const;
176   _image_function->get_stats(old_nodes,old_parameters,old_depth,old_width,old_const);
177 
178   main().history().replacing(this);
179 
180   image_function(_image_function->simplified(),!single_action);
181 
182   uint new_nodes;
183   uint new_parameters;
184   uint new_depth;
185   uint new_width;
186   real new_const;
187   _image_function->get_stats(new_nodes,new_parameters,new_depth,new_width,new_const);
188 
189   const uint nodes_eliminated=old_nodes-new_nodes;
190 
191   if (single_action) main().history().end_action();
192 
193   if (single_action)
194     {
195       if (_icon.get()) _main->setWindowIcon(*_icon);
196 
197       std::stringstream msg;
198       msg << "Eliminated " << nodes_eliminated << " redundant function nodes\n";
199       QMessageBox::information(this,"Evolvotron",msg.str().c_str(),QMessageBox::Ok);
200     }
201   return nodes_eliminated;
202 }
203 
frame_advance()204 void MutatableImageDisplay::frame_advance()
205 {
206   assert(!(_current_frame==0         && _animate_reverse==true));
207   assert(!(_current_frame==_frames-1 && _animate_reverse==false));
208 
209   if (_animate_reverse)
210     {
211       _current_frame--;
212       if (_current_frame==0)
213 	{
214 	  _animate_reverse=false;
215 	}
216     }
217   else
218     {
219       _current_frame++;
220       if (_current_frame==_frames-1)
221 	{
222 	  _animate_reverse=true;
223 	}
224     }
225   repaint();  // Use repaint rather than update because we really do want this to happen immediately.
226 }
227 
image_function(const boost::shared_ptr<const MutatableImage> & i,bool one_of_many)228 void MutatableImageDisplay::image_function(const boost::shared_ptr<const MutatableImage>& i,bool one_of_many)
229 {
230   assert(_image_function.get()==0 || _image_function->ok());
231   assert(i.get()==0 || i->ok());
232 
233   // New image, so increment serial number so any old incoming stuff which somehow avoids abort is ignored.
234   _serial++;
235 
236   // This might have already been done (e.g by resizeEvent), but it can't hurt to be sure.
237   farm().abort_for(this);
238 
239   // Careful: we could be passed our own existing (and already owned) image
240   // (a trick used by resize to trigger recompute & redisplay)
241   if (i.get()==0 || _image_function.get()==0 || i->serial()!=_image_function->serial())
242     {
243       _image_function=i;
244 
245       // If we're part of a fullscale change then better to display back rather than something misleading
246       // but for one-offs (e.g middle mouse drag) is better not to clear.
247       if (one_of_many)
248 	{
249 	  // Clear any existing image data - stops old animations continuing to play
250 	  for (uint f=0;f<_offscreen_pixmaps.size();f++)
251 	    _offscreen_pixmaps[f].fill(QColor(0,0,0));
252 
253 	  // Queue a redraw
254 	  update();
255 	}
256     }
257 
258   // If we start recomputing again we need to accept any delivered images.
259   _current_display_level=static_cast<uint>(-1);
260   _current_display_multisample_grid=static_cast<uint>(-1);
261 
262   // Clear up staging area... its contents are now useless
263   _offscreen_images_inbox.clear();
264 
265   // Update lock status displayed in menu
266   if (_menu_item_action_lock)
267     _menu_item_action_lock->setChecked(_image_function.get() ? _image_function->locked() : false);
268 
269   if (_image_function.get())
270     {
271       // Allow for displays up to 4096 pixels high or wide
272       for (int level=12;level>=0;level--)
273 	{
274 	  const int s=(1<<level);
275 	  const QSize render_size(image_size()/s);
276 
277 	  // Don't bother rendering anything less than 4x4 unless that's all there is
278 	  if ((render_size.width()>=4 && render_size.height()>=4) || level==0)
279 	    {
280 	      const int fragments
281 		=(
282 		  one_of_many
283 		  ?
284 		  1
285 		  :
286 		  std::min
287 		  (
288 		   2*farm().num_threads(),
289 		   static_cast<uint>(render_size.height())
290 		   )
291 		  );
292 
293 	      std::vector<uint> multisample_grid;
294 	      multisample_grid.push_back(1);
295 	      if (level==0)
296 		{
297 		  // Only the final full resolution level gets an additional multisampling task.
298 		  // For 4x4 sampling, do an initial 2x2 too.
299 		  if (main().render_parameters().multisample_grid()==4) multisample_grid.push_back(2);
300 		  if (main().render_parameters().multisample_grid()>1) multisample_grid.push_back(main().render_parameters().multisample_grid());
301 		}
302 
303 	      for (std::vector<uint>::const_iterator multisample_it=multisample_grid.begin();multisample_it!=multisample_grid.end();multisample_it++)
304 		{
305 		  //! \todo Should computed animation frames be constant or reduced c.f spatial resolution ?  (Do full z resolution for now)
306 		  const boost::shared_ptr<const MutatableImage> task_image(_image_function);
307 		  assert(task_image->ok());
308 
309 		  // Use number of samples in unfragmented image as priority
310 		  const uint task_priority=render_size.width()*render_size.height()*(*multisample_it)*(*multisample_it);
311 
312 		  int fragment_start_row=0;
313 		  for (int f=0;f<fragments;f++)
314 		    {
315 		      const int fragment_end_row=(render_size.height()*(f+1))/fragments;
316 		      const boost::shared_ptr<MutatableImageComputerTask> task
317 			(
318 			 new MutatableImageComputerTask
319 			 (
320 			  this,
321 			  task_image,
322 			  task_priority,
323 			  QSize(0,fragment_start_row),
324 			  QSize(render_size.width(),fragment_end_row-fragment_start_row),
325 			  render_size,
326 			  _frames,
327 			  level,
328 			  f,
329 			  fragments,
330 			  main().render_parameters().jittered_samples(),
331 			  (*multisample_it),
332 			  _serial
333 			  )
334 			 );
335 		      farm().push_todo(task);
336 		      fragment_start_row=fragment_end_row;
337 		    }
338 		}
339 	    }
340 	}
341     }
342 }
343 
deliver(const boost::shared_ptr<const MutatableImageComputerTask> & task)344 void MutatableImageDisplay::deliver(const boost::shared_ptr<const MutatableImageComputerTask>& task)
345 {
346   // Ignore tasks which were aborted or which have somehow got out of order
347   // (entirely possible with multiple compute threads).
348   if (
349       task->aborted()
350       || task->serial()!=_serial
351       || task->level()>_current_display_level
352       || (task->level()==_current_display_level && task->multisample_grid()<=_current_display_multisample_grid)
353       )
354     return;
355 
356   // Record the fragment in the inbox
357   const OffscreenImageInbox::key_type inbox_key(task->level(),task->multisample_grid());
358   OffscreenImageInbox::mapped_type& inbox_level=_offscreen_images_inbox[inbox_key];
359   assert(inbox_level.find(task->fragment())==inbox_level.end());
360   inbox_level[task->fragment()]=task;
361 
362   if (inbox_level.size()!=task->number_of_fragments())
363     return;
364 
365   // If the level is complete, we can proceed to displaying it
366 
367   // Note that obsolete levels will never complete once a better one is displayed.
368   // So clear up previous levels; they're now irrelevant
369   _offscreen_images_inbox.erase
370     (
371      _offscreen_images_inbox.upper_bound(inbox_key),  // upper_bound is the NEXT key from the one we're about to display
372      _offscreen_images_inbox.end()
373      );
374 
375   const QSize render_size(task->whole_image_size());
376 
377   if (task->number_of_fragments()==1)
378     {
379       // If there's only one fragment in the task, just use it
380       _offscreen_images=task->images();
381     }
382   else
383     {
384       // Otherwise we need to assemble the fragments together
385       _offscreen_images.resize(0);
386       for (uint f=0;f<_frames;f++)
387 	{
388 	  _offscreen_images.push_back(QImage(render_size,QImage::Format_RGB32));
389 
390 	  for (OffscreenImageInbox::mapped_type::const_iterator it=inbox_level.begin();it!=inbox_level.end();++it)
391 	    {
392 	      QPainter painter(&_offscreen_images.back());
393 	      painter.drawImage
394 		(
395 		 QPoint((*it).second->fragment_origin().width(),(*it).second->fragment_origin().height()),
396 		 (*it).second->images()[f]
397 		 );
398 	    }
399 	}
400     }
401 
402   for (uint f=0;f<_frames;f++)
403     {
404       //! \todo Pick a scaling mode: Qt::SmoothTransformation vs Qt::FastTransformation (default) (and put it under GUI control).
405       //! \todo Expose dither mode control: Qt::DiffuseDither vs Qt::ThresholdDither
406       _offscreen_pixmaps[f]=QPixmap::fromImage(_offscreen_images[f].scaled(image_size()),(Qt::ColorOnly|Qt::ThresholdDither));
407     }
408 
409   //! Note the resolution we've displayed so out-of-order low resolution images are dropped
410   _current_display_level=task->level();
411   _current_display_multisample_grid=task->multisample_grid();
412 
413   // For an icon, take the first image big enough to (hopefully) be filtered down nicely.
414   // The (Qt3) converter seems to auto-create an alpha mask sometimes (images with const-color areas), which is quite cool.
415   const QSize icon_size(32,32);
416   if (task->serial()!=_icon_serial && (task->level()==0 || (render_size.width()>=2*icon_size.width() && render_size.height()>=2*icon_size.height())))
417     {
418       const QImage icon_image(_offscreen_images[_offscreen_images.size()/2].scaled(icon_size));
419 
420       if (!_icon.get()) _icon=std::unique_ptr<QPixmap>(new QPixmap(icon_size));
421       (*_icon)=QPixmap::fromImage(icon_image,Qt::ColorOnly);
422 
423       _icon_serial=task->serial();
424     }
425 
426   // Update what's on the screen.
427   update();
428 }
429 
lock(bool l,bool record_in_history)430 void MutatableImageDisplay::lock(bool l,bool record_in_history)
431 {
432   // This might be called (with l=false) with null _image during start-up reset.
433   if (_image_function && _image_function->locked()!=l)
434     {
435       if (record_in_history)
436 	{
437 	  main().history().begin_action(l ? "lock" : "unlock");
438 	  main().history().replacing(this);
439 	}
440       const boost::shared_ptr<const MutatableImage> new_image_function(_image_function->deepclone(l));
441       image_function(new_image_function,false);
442       if (record_in_history)
443 	{
444 	  main().history().end_action();
445 	}
446     }
447 
448   _menu_item_action_lock->setChecked(l);
449 }
450 
451 /*! Enlargements are implied by a non-full-functionality displays.
452  */
farm() const453 MutatableImageComputerFarm& MutatableImageDisplay::farm() const
454 {
455   return main().farm(!_full_functionality);
456 }
457 
paintEvent(QPaintEvent *)458 void MutatableImageDisplay::paintEvent(QPaintEvent*)
459 {
460   // Repaint the screen from the offscreen pixmaps
461   QPainter painter(this);
462   painter.drawPixmap(0,0,_offscreen_pixmaps[_current_frame]);
463 
464   // If this is the first paint event after a resize we can start computing images for the new size.
465   if (_resize_in_progress)
466     {
467       image_function(_image_function,false);  // A resize should really be considered one-of-many, but because the image doesn't change we seem to be able to get away with it
468       _resize_in_progress=false;
469     }
470 }
471 
472 /*! In the resize event we just focus on shutting down existing compute threads
473   because they'll all have to be restarted.
474   NB Multiple resize events can be received before a repaint occurs.
475   NB There's nothing to be done for fixed size images (not even setting _resize_in_progress).
476  */
resizeEvent(QResizeEvent * event)477 void MutatableImageDisplay::resizeEvent(QResizeEvent* event)
478 {
479   // Fixed size images don't need anything doing here.  We don't even set _resize_in_progress.
480   if (!_fixed_size)
481     {
482       _image_size=event->size();
483 
484       // Abort all current tasks because they'll be the wrong size.
485       farm().abort_for(this);
486 
487       // Resize and reset our offscreen pixmap (something to do while we wait)
488       for (uint f=0;f<_offscreen_pixmaps.size();f++)
489 	{
490 	  _offscreen_pixmaps[f]=QPixmap(image_size());
491 	  _offscreen_pixmaps[f].fill(QColor(0,0,0));
492 	}
493 
494       // Flag for the next paintEvent to tell it a recompute can be started now.
495       _resize_in_progress=true;
496     }
497 }
498 
mousePressEvent(QMouseEvent * event)499 void MutatableImageDisplay::mousePressEvent(QMouseEvent* event)
500 {
501   if (event->button()==Qt::RightButton)
502     {
503       _menu->exec(QCursor::pos());
504     }
505   else if (event->button()==Qt::MidButton)
506     {
507       // Take a snapshot to undo back to.
508       main().history().begin_action("middle-button drag");
509       main().history().replacing(this);
510       main().history().end_action();
511 
512       _mid_button_adjust_start_pos=event->pos();
513       _mid_button_adjust_last_pos=event->pos();
514     }
515   else if (_full_functionality && event->button()==Qt::LeftButton)
516     {
517       if (_icon.get()) _main->setWindowIcon(*_icon);
518 
519       menupick_spawn();
520     }
521 }
522 
mouseMoveEvent(QMouseEvent * event)523 void MutatableImageDisplay::mouseMoveEvent(QMouseEvent* event)
524 {
525   if (event->buttons()&Qt::MidButton)
526     {
527       if (locked())
528 	{
529 	  QMessageBox::warning(this,"Evolvotron","Cannot middle-mouse adjust a locked image.\nUnlock and try again.");
530 	}
531       else
532 	{
533 	  const QPoint pixel_delta(event->pos()-_mid_button_adjust_last_pos);
534 
535 	  // Build the transform caused by the adjustment
536 	  Transform transform=TransformIdentity();
537 
538 	  // Shift button (no ctrl) is various zooms
539 	  if (event->modifiers()&Qt::ShiftModifier && !(event->modifiers()&Qt::ControlModifier))
540 	    {
541 	      // Alt-Shift is anisotropic
542 	      if (event->modifiers()&Qt::AltModifier)
543 		{
544 		  // Only scale in non-degenerate cases
545 		  if (
546 		      event->pos().x()!=image_size().width()/2
547 		      && event->pos().y()!=image_size().height()/2
548 		      && _mid_button_adjust_last_pos.x()!=image_size().width()/2
549 		      && _mid_button_adjust_last_pos.y()!=image_size().height()/2
550 		      )
551 		    {
552 		      XYZ scale(
553 				(event->pos().x()-image_size().width() /2) / static_cast<real>(_mid_button_adjust_last_pos.x()-image_size().width() /2),
554 				(event->pos().y()-image_size().height()/2) / static_cast<real>(_mid_button_adjust_last_pos.y()-image_size().height()/2),
555 				0.0
556 				);
557 
558 		      transform.basis_x(XYZ(1.0/scale.x(),          0.0,0.0));
559 		      transform.basis_y(XYZ(          0.0,1.0/scale.y(),0.0));
560 
561 		      std::clog << "[Anisotropic scale]";
562 		    }
563 		}
564 	      // Shift button alone is isotropic zoom
565 	      else
566 		{
567 		  const real cx=image_size().width()/2.0;
568 		  const real cy=image_size().width()/2.0;
569 
570 		  const real dx=event->pos().x()-cx;
571 		  const real dy=event->pos().y()-cy;
572 
573 		  const real last_dx=_mid_button_adjust_last_pos.x()-cx;
574 		  const real last_dy=_mid_button_adjust_last_pos.y()-cy;
575 
576 		  const real radius=sqrt(dx*dx+dy*dy);
577 		  const real last_radius=sqrt(last_dx*last_dx+last_dy*last_dy);
578 
579 		  // Only scale in non-degenerate cases
580 		  if (radius!=0.0 && last_radius!=0.0)
581 		    {
582 		      const real scale=radius/last_radius;
583 		      transform.basis_x(XYZ(1.0/scale,          0.0,0.0));
584 		      transform.basis_y(XYZ(      0.0,1.0/scale,0.0));
585 
586 		      std::clog << "[Isotropic scale]";
587 		    }
588 		}
589 	    }
590 	  else if (event->modifiers()&Qt::ControlModifier)
591 	    {
592 	      // Control-alt is shear
593 	      if (event->modifiers()&Qt::AltModifier)
594 		{
595 		  const real cx=image_size().width()/2.0;
596 		  const real cy=image_size().width()/2.0;
597 
598 		  const real dx=(event->pos().x()-_mid_button_adjust_last_pos.x())/cx;
599 		  const real dy=(event->pos().y()-_mid_button_adjust_last_pos.y())/cy;
600 
601 		  transform.basis_x(XYZ(1.0, -dy,0.0));
602 		  transform.basis_y(XYZ(  dx,1.0,0.0));
603 
604 		  std::clog << "[Shear]";
605 		}
606 	      // Control button only is rotate
607 	      else
608 		{
609 		  const real cx=image_size().width()/2.0;
610 		  const real cy=image_size().width()/2.0;
611 
612 		  const real dx=event->pos().x()-cx;
613 		  const real dy=event->pos().y()-cy;
614 
615 		  const real last_dx=_mid_button_adjust_last_pos.x()-cx;
616 		  const real last_dy=_mid_button_adjust_last_pos.y()-cy;
617 
618 		  const real a=atan2(dy,dx);
619 		  const real last_a=atan2(last_dy,last_dx);
620 
621 		  const real rot=a-last_a;
622 
623 		  const real sr=sin(rot);
624 		  const real cr=cos(rot);
625 
626 		  transform.basis_x(XYZ( cr,sr,0.0));
627 		  transform.basis_y(XYZ(-sr,cr,0.0));
628 
629 		  std::clog << "[Rotate]";
630 		}
631 	    }
632 	  // Default (no interesting modifier keys detected) is panning around
633 	  else
634 	    {
635 	      XYZ translate(
636 			    (-2.0*pixel_delta.x())/image_size().width(),
637 			    ( 2.0*pixel_delta.y())/image_size().height(),
638 			    0.0
639 			    );
640 	      transform.translate(translate);
641 
642 	      std::clog << "[Pan]";
643 	    }
644 
645 	  std::unique_ptr<FunctionTop> new_root(image_function()->top().typed_deepclone());
646 	  new_root->concatenate_pretransform_on_right(transform);
647 
648 	  // Install new image (triggers recompute).
649 	  const boost::shared_ptr<const MutatableImage> new_image_function(new MutatableImage(new_root,image_function()->sinusoidal_z(),image_function()->spheremap(),false));
650 	  image_function(new_image_function,false);
651 
652 	  // Finally, record position of this event as last event
653 	  _mid_button_adjust_last_pos=event->pos();
654 	}
655     }
656 }
657 
658 /*! This slot is called by selecting the "Respawn" context menu item
659  */
menupick_respawn()660 void MutatableImageDisplay::menupick_respawn()
661 {
662   main().respawn(this);
663 }
664 
665 /*! This slot is called by selecting the "Spawn" context menu item, or by clicking the image
666  */
menupick_spawn()667 void MutatableImageDisplay::menupick_spawn()
668 {
669   main().spawn_normal(this);
670 }
671 
672 /*! This slot is called by selecting the "Spawn Recoloured" context menu item
673  */
menupick_spawn_recoloured()674 void MutatableImageDisplay::menupick_spawn_recoloured()
675 {
676   main().spawn_recoloured(this);
677 }
678 
679 /*! This slot is called by selecting the "Spawn Warped/Random" context menu item
680  */
menupick_spawn_warped_random()681 void MutatableImageDisplay::menupick_spawn_warped_random()
682 {
683   TransformFactoryRandomWarpXY transform_factory;
684 
685   main().spawn_warped(this,transform_factory);
686 }
687 
menupick_spawn_warped_zoom_in()688 void MutatableImageDisplay::menupick_spawn_warped_zoom_in()
689 {
690   TransformFactoryRandomScaleXY transform_factory(-2.0,0.0);
691 
692   main().spawn_warped(this,transform_factory);
693 }
694 
menupick_spawn_warped_zoom_out()695 void MutatableImageDisplay::menupick_spawn_warped_zoom_out()
696 {
697   TransformFactoryRandomScaleXY transform_factory(0.0,2.0);
698 
699   main().spawn_warped(this,transform_factory);
700 }
701 
menupick_spawn_warped_rotate()702 void MutatableImageDisplay::menupick_spawn_warped_rotate()
703 {
704   TransformFactoryRandomRotateZ transform_factory;
705 
706   main().spawn_warped(this,transform_factory);
707 }
708 
menupick_spawn_warped_pan_xy()709 void MutatableImageDisplay::menupick_spawn_warped_pan_xy()
710 {
711   TransformFactoryRandomTranslateXYZ transform_factory(XYZ(0.0,0.0,0.0),XYZ(1.0,1.0,0.0));
712 
713   main().spawn_warped(this,transform_factory);
714 }
715 
menupick_spawn_warped_pan_x()716 void MutatableImageDisplay::menupick_spawn_warped_pan_x()
717 {
718   TransformFactoryRandomTranslateXYZ transform_factory(XYZ(0.0,0.0,0.0),XYZ(1.0,0.0,0.0));
719 
720   main().spawn_warped(this,transform_factory);
721 }
722 
menupick_spawn_warped_pan_y()723 void MutatableImageDisplay::menupick_spawn_warped_pan_y()
724 {
725   TransformFactoryRandomTranslateXYZ transform_factory(XYZ(0.0,0.0,0.0),XYZ(0.0,1.0,0.0));
726 
727   main().spawn_warped(this,transform_factory);
728 }
729 
menupick_spawn_warped_pan_z()730 void MutatableImageDisplay::menupick_spawn_warped_pan_z()
731 {
732   TransformFactoryRandomTranslateXYZ transform_factory(XYZ(0.0,0.0,0.0),XYZ(0.0,0.0,1.0));
733 
734   main().spawn_warped(this,transform_factory);
735 }
736 
737 /*! This slot is called by selecting the "Lock" context menu item.
738   It stops the image from being overwritten by a new image.
739  */
menupick_lock()740 void MutatableImageDisplay::menupick_lock()
741 {
742   lock(!_image_function->locked(),true);
743 }
744 
menupick_simplify()745 void MutatableImageDisplay::menupick_simplify()
746 {
747   simplify_constants(true);
748 }
749 
750 
751 /*! Saves image (unless the image is not full resolution yet, in which case an informative dialog is generated.
752  */
menupick_save_image()753 void MutatableImageDisplay::menupick_save_image()
754 {
755   if (_icon.get()) _main->setWindowIcon(*_icon);
756 
757   std::clog << "Save requested...\n";
758 
759   if (_current_display_level!=0 || _current_display_multisample_grid!=main().render_parameters().multisample_grid())
760     {
761       QMessageBox::information(this,"Evolvotron","The selected image has not yet been generated at maximum resolution.\nPlease try again later.");
762     }
763   else
764     {
765       const QString save_filename=QFileDialog::getSaveFileName
766 	(
767 	 this,
768 	 "Save image to a PNG or PPM file",
769 	 ".",
770 	 "Images (*.png *.ppm)"
771 	 );
772 
773       if (!save_filename.isEmpty())
774 	{
775 	  QString save_format="PNG";
776 	  if (save_filename.toUpper().endsWith(".PPM"))
777 	    {
778 	      save_format="PPM";
779 	    }
780 	  else if (save_filename.toUpper().endsWith(".PNG"))
781 	    {
782 	      save_format="PNG";
783 	    }
784 	  else
785 	    {
786 	      QMessageBox::warning
787 		(
788 		 this,
789 		 "Evolvotron",
790 		 QString("Unrecognised file suffix.\nFile will be written in ")+save_format+QString(" format.")
791 		 );
792 	    }
793 
794 	  for (uint f=0;f<_offscreen_images.size();f++)
795 	    {
796 	      QString actual_save_filename(save_filename);
797 
798 	      if (_offscreen_images.size()>1)
799 		{
800 		  QString frame_component;
801 		  frame_component.sprintf(".f%06d",f);
802 		  int insert_point=save_filename.lastIndexOf(QString("."));
803 		  if (insert_point==-1)
804 		    {
805 		      actual_save_filename.append(frame_component);
806 		    }
807 		  else
808 		    {
809 		      actual_save_filename.insert(insert_point,frame_component);
810 		    }
811 		}
812 
813 	      if (!_offscreen_images[f].save(actual_save_filename,save_format.toLocal8Bit()))
814 		{
815 		  QMessageBox::critical(this,"Evolvotron","Failed to write file "+actual_save_filename);
816 		  if (f<_offscreen_images.size()-1)
817 		    {
818 		      QMessageBox::critical(this,"Evolvotron","Not attempting to save remaining images in animation");
819 		    }
820 		  break;
821 		}
822 	    }
823 	}
824     }
825   std::clog << "...save done\n";
826 }
827 
menupick_save_function()828 void MutatableImageDisplay::menupick_save_function()
829 {
830   if (_icon.get()) _main->setWindowIcon(*_icon);
831 
832   const QString save_filename=QFileDialog::getSaveFileName
833     (
834      this,
835      "Save image function to an XML file",
836      ".",
837      "Functions (*.xml)"
838      );
839 
840   if (!save_filename.isEmpty())
841     {
842       std::ofstream file(save_filename.toLocal8Bit());
843       _image_function->save_function(file);
844       file.flush();
845       if (!file)
846 	{
847 	  QMessageBox::critical(this,"Evolvotron","File write failed");
848 	}
849     }
850 }
851 
load_function_file(const QString & load_filename)852 void MutatableImageDisplay::load_function_file(const QString& load_filename)
853 {
854   const std::string filename(load_filename.toLocal8Bit());
855   std::ifstream file(filename.c_str());
856 
857   if (!file)
858   {
859     QMessageBox::critical(
860       this,
861       "Evolvotron",
862       ("Filename '"+filename+"' could not be opened\n").c_str()
863     );
864   }
865   else
866   {
867     std::string report;
868     boost::shared_ptr<const MutatableImage> new_image_function(MutatableImage::load_function(_main->mutation_parameters().function_registry(),file,report));
869 
870     if (new_image_function.get()==0)
871     {
872       QMessageBox::critical(
873 	this,
874 	"Evolvotron",
875 	("Function in filename '"+filename+"' not loaded due to errors:\n"+report).c_str()
876       );
877     }
878     else
879     {
880       if (!report.empty())
881       {
882 	QMessageBox::warning(
883 	  this,
884 	  "Evolvotron",
885 	  ("Function in filename '"+filename+"' +loaded with warnings:\n"+report).c_str(),
886 	  QMessageBox::Ok,
887 	  QMessageBox::NoButton
888 	);
889       }
890 
891       main().history().begin_action("load");
892       main().history().replacing(this);
893       main().history().end_action();
894       image_function(new_image_function,false);
895     }
896   }
897 }
898 
menupick_load_function()899 void MutatableImageDisplay::menupick_load_function()
900 {
901   const QString load_filename=QFileDialog::getOpenFileName
902     (
903      this,
904      "Load image function from an XML file",
905      ".",
906      "Functions (*.xml)"
907      );
908 
909   if (!load_filename.isEmpty())
910     {
911       load_function_file(load_filename);
912     }
913 }
914 
menupick_big_resizable()915 void MutatableImageDisplay::menupick_big_resizable()
916 {
917   spawn_big(false,QSize(0,0));
918 }
919 
menupick_big_640x480()920 void MutatableImageDisplay::menupick_big_640x480()
921 {
922   spawn_big(true,QSize(640,480));
923 }
924 
menupick_big_1024x768()925 void MutatableImageDisplay::menupick_big_1024x768()
926 {
927   spawn_big(true,QSize(1024,768));
928 }
929 
menupick_big_1280x960()930 void MutatableImageDisplay::menupick_big_1280x960()
931 {
932   spawn_big(true,QSize(1280,960));
933 }
934 
menupick_big_1600x1200()935 void MutatableImageDisplay::menupick_big_1600x1200()
936 {
937   spawn_big(true,QSize(1600,1200));
938 }
939 
menupick_big_256x256()940 void MutatableImageDisplay::menupick_big_256x256()
941 {
942   spawn_big(true,QSize(256,256));
943 }
944 
menupick_big_512x512()945 void MutatableImageDisplay::menupick_big_512x512()
946 {
947   spawn_big(true,QSize(512,512));
948 }
949 
menupick_big_768x768()950 void MutatableImageDisplay::menupick_big_768x768()
951 {
952   spawn_big(true,QSize(768,768));
953 }
954 
menupick_big_1024x1024()955 void MutatableImageDisplay::menupick_big_1024x1024()
956 {
957   spawn_big(true,QSize(1024,1024));
958 }
959 
menupick_big_2048x2048()960 void MutatableImageDisplay::menupick_big_2048x2048()
961 {
962   spawn_big(true,QSize(2048,2048));
963 }
964 
menupick_big_4096x4096()965 void MutatableImageDisplay::menupick_big_4096x4096()
966 {
967   spawn_big(true,QSize(4096,4096));
968 }
969 
menupick_properties()970 void MutatableImageDisplay::menupick_properties()
971 {
972   uint total_nodes;
973   uint total_parameters;
974   uint depth;
975   uint width;
976   real proportion_constant;
977 
978   image_function()->get_stats(total_nodes,total_parameters,depth,width,proportion_constant);
979 
980   std::stringstream msg;
981   msg << " " << total_nodes      << "\t function nodes\n";
982   msg << " " << total_parameters << "\t parameters\n";
983   msg << " " << depth            << "\t maximum depth\n";
984   msg << " " << width            << "\t width\n";
985   msg << " " << std::setprecision(3) << 100.0*proportion_constant << "%\t constant\n";
986 
987   std::stringstream xml;
988   image_function()->save_function(xml);
989 
990   _properties->set_content(msg.str(),xml.str());
991   if (_icon.get()) _properties->setWindowIcon(*_icon);
992   _properties->exec();
993 }
994 
995 /*! Create an image display with no parent: becomes a top level window
996   Disable full menu functionality because there's less we can do with a single image (e.g no spawn_target)
997 */
spawn_big(bool scrollable,const QSize & sz)998 void MutatableImageDisplay::spawn_big(bool scrollable,const QSize& sz)
999 {
1000   MutatableImageDisplayBig*const top_level_widget=new MutatableImageDisplayBig(&main());
1001   top_level_widget->setLayout(new QVBoxLayout);
1002   if (_icon.get()) top_level_widget->setWindowIcon(*_icon);
1003 
1004   MutatableImageDisplay* display=0;
1005 
1006   if (scrollable)
1007     {
1008       QScrollArea*const scrollview=new QScrollArea;
1009       top_level_widget->layout()->addWidget(scrollview);
1010       display=new MutatableImageDisplay(&main(),false,true,sz,_frames,_framerate);
1011       scrollview->setWidget(display);
1012     }
1013   else
1014     {
1015       display=new MutatableImageDisplay(&main(),false,false,QSize(0,0),_frames,_framerate);
1016       top_level_widget->layout()->addWidget(display);
1017     }
1018 
1019   top_level_widget->show();
1020 
1021   //Propagate full screen mode
1022   if (main().isFullScreen()) top_level_widget->showFullScreen();
1023 
1024   // Fire up image calculation
1025   display->image_function(_image_function,false);
1026 }
1027