1 /+
2    Vasaro Copyright © 2018 Andrea Fontana
3    This file is part of Vasaro.
4 
5    Vasaro is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9 
10    Vasaro is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with Vasaro.  If not, see <http://www.gnu.org/licenses/>.
17 +/
18 
19 module mainwindow;
20 
21 import viewer;
22 import generator;
23 
24 // Ui binding
25 import gtkattributes;
26 mixin GtkAttributes;
27 
28 import   gtk.Button, gtk.CheckButton, gtk.ComboBoxText,
29          gtk.ToggleButton, gtk.TreeView, gtk.Grid, gtk.Frame, 
30          gtk.Adjustment, gtk.Window, gtk.Widget, gtk.ListStore;
31 
32 import glib.Timeout;
33 
34 // Used everywhere
35 import std.conv : to;
36 
37 @ui Window   mainWindow;
38 
39 // General
40 @ui CheckButton         showPreview;
41 @ui Adjustment          minDiameter;
42 @ui Adjustment          maxDiameter;
43 @ui Adjustment          resolution;
44 @ui Adjustment          vaseHeight;
45 @ui Adjustment          layerHeight;
46 @ui ComboBoxText        vaseProfile;
47 
48 // Noise
49 @ui Adjustment          noiseAmplitude;
50 @ui Adjustment          noiseRotation;
51 @ui Adjustment          noiseXScale;
52 @ui Adjustment          noiseZScale;
53 @ui Adjustment          noiseRandomSeed;
54 @ui ComboBoxText        noiseStrength;
55 @ui Button              noiseRemove;
56 @ui Grid                noiseParamsGroup;
57 @ui Frame               noiseStrengthGroup;
58 @ui TreeView            noiseList;
59 
60 
61 import gtk.Builder;
62 Builder b;
63 
64 // Just two fields: noise name and status (active/not active)
65 ListStore   noiseListStore;
66 
67 // Timer I use to render
68 Timeout     renderTimeout;
69 
70 
71 bool  adjusting      = false; // To avoid conflicts between gui elements that are working on same params
72 int   currentNoise   = -1;
73 
74 //  Closing window
75 @event!(Window)("mainWindow", "OnHide")
76 void onWindowClose(Widget){ 
77    import gtk.Main;
78    viewer.stop();
79    Main.quit(); 
80 }
81 
82 // User toggle checkbox
83 @event!CheckButton("showPreview", "OnToggled")
84 void onShowPreview(ToggleButton t)
85 {
86    if ((cast(CheckButton)t).getActive) viewer.start();
87    else viewer.stop();
88 }
89 
90 // User click export
91 @event!Button("about", "OnClicked")
92 void onAbout(Button t)
93 {
94    import resources;
95    import gtk.AboutDialog;
96    import gdkpixbuf.Pixbuf;
97    auto pixbuf = new Pixbuf(cast(char[])LOGO, GdkColorspace.RGB, true, 8, LOGO_W, LOGO_H, LOGO_W*4, null, null);
98    pixbuf = pixbuf.scaleSimple(256,256,GdkInterpType.HYPER);
99 
100    auto dialog = new AboutDialog();
101    dialog.setLicenseType(GtkLicense.GPL_3_0);
102    dialog.setLogo(pixbuf);
103    dialog.setProgramName("Vasaro");
104    dialog.setVersion(VERSION);
105    dialog.setWebsite("https://github.com/trikko/vasaro");
106    dialog.setWebsiteLabel("https://github.com/trikko/vasaro");
107    dialog.setComments("Your printable vase creator.");
108    dialog.setCopyright("Copyright © 2018 Andrea Fontana");
109    
110    dialog.setTransientFor(mainWindow);
111    dialog.run();
112    dialog.hide();
113 }
114 
115 // User click export
116 @event!Button("exportSTL", "OnClicked")
117 void onExport(Button t)
118 {
119    import gtk.FileFilter;
120    import gtk.FileChooserNative;
121    import std..string    : empty, toLower, endsWith;
122    import std.stdio     : File;
123    import std.range     : chunks;
124    import std.algorithm : map;
125 
126    auto ff = new FileFilter();
127    ff.addPattern("*.stl");
128    ff.setName("Stereo Lithography interface format (*.stl)");
129 
130    FileChooserNative fn = new FileChooserNative("Export to...", mainWindow, GtkFileChooserAction.SAVE, "Ok", "Cancel");
131    fn.setModal(true);
132    fn.setDoOverwriteConfirmation(true);
133    fn.addFilter(ff);
134    fn.run;
135 
136    string filename = fn.getFilename();
137 
138    // "Cancel"
139    if (filename.empty) return; 
140    
141    if (!filename.toLower.endsWith(".stl")) filename ~= ".stl";
142 
143    try
144    {
145       File f = File(filename, "wb");
146 
147       import resources : VERSION;
148       import std..string : representation;
149 
150       string signature = "Created with vasaro " ~ VERSION;
151 
152       ubyte[80] header;
153       header[0..signature.representation.length] = signature.representation;
154       f.rawWrite(header);
155       f.rawWrite([ cast(uint)(model[currentModel].vertex.length / 9) ]);
156 
157       foreach(const ref v; model[currentModel].vertex.chunks(9).map!(x => cast(float[])x))
158       {
159          // I wont't calculate any surface normal
160          f.rawWrite([0.0f,0.0f,0.0f]);  
161 
162          // Vertices
163          f.rawWrite(v);
164 
165          // Reserved
166          f.rawWrite([cast(ushort)0]); 
167       }
168 
169       f.close();
170    }
171    catch (Exception e)
172    {
173       import gtk.MessageDialog;
174       auto md = new MessageDialog(mainWindow, GtkDialogFlags.DESTROY_WITH_PARENT, GtkMessageType.ERROR, GtkButtonsType.CLOSE, "Error during file saving. Try changing file path.");
175       md.run;
176       md.destroy();   
177    }
178 }
179 
180 void updateNoiseInterface()
181 {
182    bool noiseSelected = (noises.length > 0 && currentNoise >= 0);
183    bool visible = (noiseSelected && noises[currentNoise].visible);
184 
185    noiseRemove.setSensitive(visible);
186    noiseParamsGroup.setSensitive(visible);
187    noiseStrengthGroup.setSensitive(visible);
188 
189    if (noiseSelected)
190    {
191       adjusting = true;
192       
193       for(int i = 0; i < 10; ++i)
194       {
195          auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
196          tmpObj.setValue(noises[currentNoise].strengthPoints[i]);
197       }
198       noiseRotation.setValue(noises[currentNoise].rotation);
199       noiseAmplitude.setValue(noises[currentNoise].amplitude);
200       noiseXScale.setValue(noises[currentNoise].xScale);
201       noiseZScale.setValue(noises[currentNoise].yScale);
202       noiseRandomSeed.setValue(noises[currentNoise].seed);
203       
204       adjusting = false;
205    }
206    
207    build();
208 }
209 
210 int t;
211 
212 @event!Button("noiseAdd", "OnClicked")
213 void onNoiseAdded(Button b)
214 {
215    
216    import gtk.TreeIter;
217    import gtk.TreeSelection;
218    import std.random : uniform;
219    
220    noises ~= Noise();
221    noises[noises.length-1].seed = uniform(-10000, 10000);
222    noises[noises.length-1].amplitude = uniform(0.5, 2);
223    noises[noises.length-1].xScale = uniform(0.5, 5);
224    noises[noises.length-1].yScale = uniform(0.5, 5);
225    noises[noises.length-1].rotation = uniform(-0.5, 0.5);
226 
227    auto it = noiseListStore.createIter();
228    noiseListStore.setValue(it, 0, true);
229    noiseListStore.setValue(it, 1, "Noise #" ~ (noises.length-1).to!string);
230    noiseList.getSelection().selectIter(it);
231 
232 }
233 
234 
235 @event!Button("noiseRemove", "OnClicked")
236 void onNoiseRemoved(Button b)
237 {
238    noiseListStore.remove(noiseList.getSelection().getSelected());
239 
240    for (size_t i = noises.length-1; i > currentNoise; --i)
241       noises[i] = noises[i-1];
242    
243    noises.length--;
244 }
245 
246 @event!ComboBoxText("vaseProfile", "OnChanged")
247 void onProfileSelected(ComboBoxText changed)
248 {
249    import std.math : pow, sin, PI;
250    
251 
252    final switch(changed.getActive)
253    {
254       case 0: // CONSTANT
255          vaseProfileCheckPoints[] = 0.5; 
256          break;
257       
258       case 1: // LINEAR
259          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = i/9.0f;
260          break;
261 
262       case 2: // EXP
263          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = pow(i/9.0f,3);
264          break;
265 
266       case 3: // SIN
267          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = sin(i/9.0f*PI);
268          break;
269 
270       case 4: // Custom 
271          return;
272    }
273 
274    adjusting = true;
275    for(int i = 0; i < 10; ++i)
276    {
277       auto tmpObj = cast(Adjustment)b.getObject("radiusScale" ~ i.to!string);
278       tmpObj.setValue(vaseProfileCheckPoints[i]);
279    }
280    adjusting = false;
281 
282    build();
283 }
284 
285 @event!ComboBoxText("noiseStrength", "OnChanged")
286 void onNoiseStrengthSelected(ComboBoxText changed)
287 {
288    import std.math : pow, sin, PI;
289    
290    final switch(changed.getActive)
291    {
292       case 0: // CONSTANT
293          noises[currentNoise].strengthPoints[] = 1; 
294          break;
295       
296       case 1: // LINEAR
297          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = i/9.0f;
298          break;
299 
300       case 2: // EXP
301          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = pow(i/9.0f,3);
302          break;
303 
304       case 3: // SIN
305          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = sin(i/9.0f*PI);
306          break;
307 
308       case 4: // Custom 
309          return;
310    }
311 
312    adjusting = true;
313    for(int i = 0; i < 10; ++i)
314    {
315       auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
316       tmpObj.setValue(noises[currentNoise].strengthPoints[i]);
317    }
318    adjusting = false;
319 
320    build();
321 }
322 
323 @event!Adjustment("noiseAmplitude", "OnValueChanged") @event!Adjustment("noiseXScale", "OnValueChanged")
324 @event!Adjustment("noiseZScale", "OnValueChanged") @event!Adjustment("noiseRandomSeed", "OnValueChanged") 
325 @event!Adjustment("noiseRotation", "OnValueChanged") @event!Adjustment("noiseRandomSeed", "OnValueChanged") 
326 void onNoiseParamsChanged(Adjustment changed)
327 {
328    bool rebuild = true;
329    
330    if (changed == noiseAmplitude) generator.noises[currentNoise].amplitude = changed.getValue();
331    else if (changed == noiseXScale) generator.noises[currentNoise].xScale = changed.getValue();
332    else if (changed == noiseZScale) generator.noises[currentNoise].yScale = changed.getValue();
333    else if (changed == noiseRandomSeed) generator.noises[currentNoise].seed = changed.getValue().to!long;
334    else if (changed == noiseRotation) generator.noises[currentNoise].rotation = changed.getValue();
335    else rebuild = false;
336    
337    if (rebuild)
338    {
339       build();
340    }
341 }
342 
343 @event!Adjustment("minDiameter", "OnValueChanged") @event!Adjustment("maxDiameter", "OnValueChanged")
344 @event!Adjustment("resolution", "OnValueChanged") @event!Adjustment("layerHeight", "OnValueChanged") @event!Adjustment("vaseHeight", "OnValueChanged")
345 void onGeneralParamsChanged(Adjustment changed)
346 {
347    bool rebuild = true;
348    if (changed == minDiameter) generator.minDiameter = changed.getValue;
349    else if (changed == maxDiameter)  generator.maxDiameter = changed.getValue;
350    else if (changed == resolution) generator.resolution = changed.getValue.to!int;
351    else if (changed == vaseHeight) generator.vaseHeight = changed.getValue;
352    else if (changed == layerHeight) generator.layerHeight = changed.getValue;
353    else rebuild = false;
354    
355 
356    if (rebuild)
357    {
358       build();
359    }
360 }
361 
362 @event!Adjustment("radiusScale0", "OnValueChanged") @event!Adjustment("radiusScale1", "OnValueChanged") @event!Adjustment("radiusScale2", "OnValueChanged")
363 @event!Adjustment("radiusScale3", "OnValueChanged") @event!Adjustment("radiusScale4", "OnValueChanged") @event!Adjustment("radiusScale5", "OnValueChanged")
364 @event!Adjustment("radiusScale6", "OnValueChanged") @event!Adjustment("radiusScale7", "OnValueChanged") @event!Adjustment("radiusScale8", "OnValueChanged")
365 @event!Adjustment("radiusScale9", "OnValueChanged")
366 void onProfileChange(Adjustment changed)
367 {
368    if (adjusting) return;
369 
370    bool rebuild = true;
371 
372    for(int i = 0; i < 10; ++i)
373    {
374       auto tmpObj = cast(Adjustment)b.getObject("radiusScale" ~ i.to!string);
375       vaseProfileCheckPoints[i] = tmpObj.getValue;
376    }
377 
378    vaseProfile.setActive(4);
379    build();
380 
381 
382 }
383 
384 @event!Adjustment("noiseScale0", "OnValueChanged") @event!Adjustment("noiseScale1", "OnValueChanged") @event!Adjustment("noiseScale2", "OnValueChanged")
385 @event!Adjustment("noiseScale3", "OnValueChanged") @event!Adjustment("noiseScale4", "OnValueChanged") @event!Adjustment("noiseScale5", "OnValueChanged")
386 @event!Adjustment("noiseScale6", "OnValueChanged") @event!Adjustment("noiseScale7", "OnValueChanged") @event!Adjustment("noiseScale8", "OnValueChanged")
387 @event!Adjustment("noiseScale9", "OnValueChanged")
388 void onNoiseStrengthChange(Adjustment changed)
389 {
390    if (adjusting) return;
391 
392    bool rebuild = true;
393 
394    for(int i = 0; i < 10; ++i)
395    {
396       auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
397       noises[currentNoise].strengthPoints[i] = tmpObj.getValue;
398    }
399 
400    noiseStrength.setActive(4);
401    build();
402 
403 }
404 
405 version(Windows)
406 {
407    // Copy/Pasted from DWiki
408    // It calls winmain to avoid terminal popup.
409 
410    import core.runtime;
411    import core.sys.windows.windows;
412    import std..string;
413 
414    extern (Windows)
415    int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
416    {
417       int result;
418 
419       try
420       {
421          Runtime.initialize();
422          result = mainImpl([]);
423          Runtime.terminate();
424       }
425       catch (Throwable e) 
426       {
427          MessageBoxA(null, e.toString().toStringz(), null,  MB_ICONEXCLAMATION);
428          result = 0; 
429       }
430 
431       return result;
432    }
433 }
434 
435 // Other systems.
436 else int main(string[] args) { return mainImpl(args); }
437 
438 int mainImpl(string[] args)
439 {    
440    // First start SDL ...
441    import viewer;
442    viewer.start();
443    
444    
445    // ... then gtk.
446    import resources;
447    import gtk.Main;
448    import gtk.Settings;
449 
450    Main.init(args);
451    b = new Builder();
452    b.addFromString(LAYOUT);
453    b.bindAll!mainwindow;
454 
455    // Add icon
456    {
457       import gdkpixbuf.Pixbuf;
458       auto pixbuf = new Pixbuf(cast(char[])LOGO, GdkColorspace.RGB, true, 8, LOGO_W, LOGO_H, LOGO_W*4, null, null);
459       
460       // GTK+
461       mainWindow.setIcon(pixbuf);
462 
463       // SDL
464       viewer.setIcon(pixbuf.getPixels, pixbuf.getWidth, pixbuf.getHeight);
465    }
466 
467    mainWindow.showAll();
468 
469    // For some obscures reasons, treeview won't work if
470    // designed on Glade. I have to build it here instead.
471    import gtk.TreeViewColumn;
472    import gtk.TreeSelection;
473    import gtk.ListStore;
474    import gtk.CellRendererToggle;
475    import gtk.CellRendererText;
476 
477    noiseListStore = new ListStore([GType.INT, GType.STRING]);
478 
479    TreeViewColumn column = new TreeViewColumn();
480    column.setTitle( "Layers" );
481    noiseList.appendColumn(column);
482 
483    CellRendererToggle cell_bool = new CellRendererToggle();
484    column.packStart(cell_bool, 0 );
485    column.addAttribute(cell_bool, "active", 0);
486 
487    CellRendererText cell_text = new CellRendererText();
488    column.packStart(cell_text, 0 );
489    column.addAttribute(cell_text, "text", 1);
490    cell_text.setProperty( "editable", 1 );
491 
492    // Line toggled
493    cell_bool.addOnToggled( delegate void(string p, CellRendererToggle){
494       import gtk.TreePath, gtk.TreeIter;
495         
496       auto path = new TreePath( p );
497       auto it = new TreeIter( noiseListStore, path );
498       noiseListStore.setValue(it, 0, it.getValueInt( 0 ) ? 0 : 1 );
499       
500       auto index = it.getTreePath().getIndices()[0];
501 
502       noises[index].visible = it.getValueInt(0) == 1;
503       updateNoiseInterface();
504    });
505 
506    // Line changed
507    cell_text.addOnEdited( delegate void(string p, string v, CellRendererText cell ){
508       
509       import gtk.TreePath, gtk.TreeIter;
510 
511       auto path = new TreePath( p );
512       auto it = new TreeIter( noiseListStore, path );
513       noiseListStore.setValue( it, 1, v );
514    });
515 
516    noiseList.setModel(noiseListStore);
517    noiseList.getSelection().addOnChanged(delegate(TreeSelection ts) { 
518       auto selected = ts.getSelected();
519       
520       if (selected is null) currentNoise = -1;
521       else currentNoise = selected.getTreePath().getIndices()[0];
522       
523       updateNoiseInterface();
524    });
525 
526 
527    // Start vase creation with default params
528    generator.start();
529    build();
530    
531    // Rendering loop (SDL)
532    renderTimeout = new Timeout(1000/30, 
533       delegate 
534       { 
535          if (viewer.isRunning) renderFrame(); 
536          else if (showPreview.getActive()) showPreview.setActive(false);
537          return true; 
538       }, 
539       false
540    );
541    
542    // Main loop (Gtk+)
543    Main.run();
544 
545    // Clear sdl stuffs
546    viewer.uninit();
547    return 0;
548 }
549