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       char[80] header;
148       header[] = "Created with Vasaro.";
149 
150       f.rawWrite(header);
151       f.rawWrite([cast(uint) model[currentModel].vertex.length / 3]);
152 
153       foreach(const ref v; model[currentModel].vertex.chunks(9).map!(x => cast(float[])x))
154       {
155          // I wont't calculate any surface normal
156          f.rawWrite([0.0f,0.0f,0.0f]);  
157 
158          // Vertices
159          f.rawWrite(v);
160 
161          // Reserved
162          f.rawWrite([cast(ushort)0]); 
163       }
164 
165       f.close();
166    }
167    catch (Exception e)
168    {
169       import gtk.MessageDialog;
170       auto md = new MessageDialog(mainWindow, GtkDialogFlags.DESTROY_WITH_PARENT, GtkMessageType.ERROR, GtkButtonsType.CLOSE, "Error during file saving. Try changing file path.");
171       md.run;
172       md.destroy();   
173    }
174 }
175 
176 void updateNoiseInterface()
177 {
178    bool noiseSelected = (noises.length > 0 && currentNoise >= 0);
179    bool visible = (noiseSelected && noises[currentNoise].visible);
180 
181    noiseRemove.setSensitive(visible);
182    noiseParamsGroup.setSensitive(visible);
183    noiseStrengthGroup.setSensitive(visible);
184 
185    if (noiseSelected)
186    {
187       adjusting = true;
188       
189       for(int i = 0; i < 10; ++i)
190       {
191          auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
192          tmpObj.setValue(noises[currentNoise].strengthPoints[i]);
193       }
194       noiseRotation.setValue(noises[currentNoise].rotation);
195       noiseAmplitude.setValue(noises[currentNoise].amplitude);
196       noiseXScale.setValue(noises[currentNoise].xScale);
197       noiseZScale.setValue(noises[currentNoise].yScale);
198       noiseRandomSeed.setValue(noises[currentNoise].seed);
199       
200       adjusting = false;
201    }
202    
203    build();
204 }
205 
206 int t;
207 
208 @event!Button("noiseAdd", "OnClicked")
209 void onNoiseAdded(Button b)
210 {
211    
212    import gtk.TreeIter;
213    import gtk.TreeSelection;
214    import std.random : uniform;
215    
216    noises ~= Noise();
217    noises[noises.length-1].seed = uniform(-10000, 10000);
218    noises[noises.length-1].amplitude = uniform(0.5, 2);
219    noises[noises.length-1].xScale = uniform(0.5, 5);
220    noises[noises.length-1].yScale = uniform(0.5, 5);
221    noises[noises.length-1].rotation = uniform(-0.5, 0.5);
222 
223    auto it = noiseListStore.createIter();
224    noiseListStore.setValue(it, 0, true);
225    noiseListStore.setValue(it, 1, "Noise #" ~ (noises.length-1).to!string);
226    noiseList.getSelection().selectIter(it);
227 
228 }
229 
230 
231 @event!Button("noiseRemove", "OnClicked")
232 void onNoiseRemoved(Button b)
233 {
234    noiseListStore.remove(noiseList.getSelection().getSelected());
235 
236    for (size_t i = noises.length-1; i > currentNoise; --i)
237       noises[i] = noises[i-1];
238    
239    noises.length--;
240 }
241 
242 @event!ComboBoxText("vaseProfile", "OnChanged")
243 void onProfileSelected(ComboBoxText changed)
244 {
245    import std.math : pow, sin, PI;
246    
247 
248    final switch(changed.getActive)
249    {
250       case 0: // CONSTANT
251          vaseProfileCheckPoints[] = 0.5; 
252          break;
253       
254       case 1: // LINEAR
255          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = i/9.0f;
256          break;
257 
258       case 2: // EXP
259          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = pow(i/9.0f,3);
260          break;
261 
262       case 3: // SIN
263          for(int i = 0; i < 10; i++) vaseProfileCheckPoints[i] = sin(i/9.0f*PI);
264          break;
265 
266       case 4: // Custom 
267          return;
268    }
269 
270    adjusting = true;
271    for(int i = 0; i < 10; ++i)
272    {
273       auto tmpObj = cast(Adjustment)b.getObject("radiusScale" ~ i.to!string);
274       tmpObj.setValue(vaseProfileCheckPoints[i]);
275    }
276    adjusting = false;
277 
278    build();
279 }
280 
281 @event!ComboBoxText("noiseStrength", "OnChanged")
282 void onNoiseStrengthSelected(ComboBoxText changed)
283 {
284    import std.math : pow, sin, PI;
285    
286    final switch(changed.getActive)
287    {
288       case 0: // CONSTANT
289          noises[currentNoise].strengthPoints[] = 1; 
290          break;
291       
292       case 1: // LINEAR
293          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = i/9.0f;
294          break;
295 
296       case 2: // EXP
297          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = pow(i/9.0f,3);
298          break;
299 
300       case 3: // SIN
301          for(int i = 0; i < 10; i++) noises[currentNoise].strengthPoints[i] = sin(i/9.0f*PI);
302          break;
303 
304       case 4: // Custom 
305          return;
306    }
307 
308    adjusting = true;
309    for(int i = 0; i < 10; ++i)
310    {
311       auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
312       tmpObj.setValue(noises[currentNoise].strengthPoints[i]);
313    }
314    adjusting = false;
315 
316    build();
317 }
318 
319 @event!Adjustment("noiseAmplitude", "OnValueChanged") @event!Adjustment("noiseXScale", "OnValueChanged")
320 @event!Adjustment("noiseZScale", "OnValueChanged") @event!Adjustment("noiseRandomSeed", "OnValueChanged") 
321 @event!Adjustment("noiseRotation", "OnValueChanged") @event!Adjustment("noiseRandomSeed", "OnValueChanged") 
322 void onNoiseParamsChanged(Adjustment changed)
323 {
324    bool rebuild = true;
325    
326    if (changed == noiseAmplitude) generator.noises[currentNoise].amplitude = changed.getValue();
327    else if (changed == noiseXScale) generator.noises[currentNoise].xScale = changed.getValue();
328    else if (changed == noiseZScale) generator.noises[currentNoise].yScale = changed.getValue();
329    else if (changed == noiseRandomSeed) generator.noises[currentNoise].seed = changed.getValue().to!long;
330    else if (changed == noiseRotation) generator.noises[currentNoise].rotation = changed.getValue();
331    else rebuild = false;
332    
333    if (rebuild)
334    {
335       build();
336    }
337 }
338 
339 @event!Adjustment("minDiameter", "OnValueChanged") @event!Adjustment("maxDiameter", "OnValueChanged")
340 @event!Adjustment("resolution", "OnValueChanged") @event!Adjustment("layerHeight", "OnValueChanged") @event!Adjustment("vaseHeight", "OnValueChanged")
341 void onGeneralParamsChanged(Adjustment changed)
342 {
343    bool rebuild = true;
344    if (changed == minDiameter) generator.minDiameter = changed.getValue;
345    else if (changed == maxDiameter)  generator.maxDiameter = changed.getValue;
346    else if (changed == resolution) generator.resolution = changed.getValue.to!int;
347    else if (changed == vaseHeight) generator.vaseHeight = changed.getValue;
348    else if (changed == layerHeight) generator.layerHeight = changed.getValue;
349    else rebuild = false;
350    
351 
352    if (rebuild)
353    {
354       build();
355    }
356 }
357 
358 @event!Adjustment("radiusScale0", "OnValueChanged") @event!Adjustment("radiusScale1", "OnValueChanged") @event!Adjustment("radiusScale2", "OnValueChanged")
359 @event!Adjustment("radiusScale3", "OnValueChanged") @event!Adjustment("radiusScale4", "OnValueChanged") @event!Adjustment("radiusScale5", "OnValueChanged")
360 @event!Adjustment("radiusScale6", "OnValueChanged") @event!Adjustment("radiusScale7", "OnValueChanged") @event!Adjustment("radiusScale8", "OnValueChanged")
361 @event!Adjustment("radiusScale9", "OnValueChanged")
362 void onProfileChange(Adjustment changed)
363 {
364    if (adjusting) return;
365 
366    bool rebuild = true;
367 
368    for(int i = 0; i < 10; ++i)
369    {
370       auto tmpObj = cast(Adjustment)b.getObject("radiusScale" ~ i.to!string);
371       vaseProfileCheckPoints[i] = tmpObj.getValue;
372    }
373 
374    vaseProfile.setActive(4);
375    build();
376 
377 
378 }
379 
380 @event!Adjustment("noiseScale0", "OnValueChanged") @event!Adjustment("noiseScale1", "OnValueChanged") @event!Adjustment("noiseScale2", "OnValueChanged")
381 @event!Adjustment("noiseScale3", "OnValueChanged") @event!Adjustment("noiseScale4", "OnValueChanged") @event!Adjustment("noiseScale5", "OnValueChanged")
382 @event!Adjustment("noiseScale6", "OnValueChanged") @event!Adjustment("noiseScale7", "OnValueChanged") @event!Adjustment("noiseScale8", "OnValueChanged")
383 @event!Adjustment("noiseScale9", "OnValueChanged")
384 void onNoiseStrengthChange(Adjustment changed)
385 {
386    if (adjusting) return;
387 
388    bool rebuild = true;
389 
390    for(int i = 0; i < 10; ++i)
391    {
392       auto tmpObj = cast(Adjustment)b.getObject("noiseScale" ~ i.to!string);
393       noises[currentNoise].strengthPoints[i] = tmpObj.getValue;
394    }
395 
396    noiseStrength.setActive(4);
397    build();
398 
399 }
400 
401 version(Windows)
402 {
403    // Copy/Pasted from DWiki
404    // It calls winmain to avoid terminal popup.
405 
406    import core.runtime;
407    import core.sys.windows.windows;
408    import std.string;
409 
410    extern (Windows)
411    int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
412    {
413       int result;
414 
415       try
416       {
417          Runtime.initialize();
418          result = mainImpl([]);
419          Runtime.terminate();
420       }
421       catch (Throwable e) 
422       {
423          MessageBoxA(null, e.toString().toStringz(), null,  MB_ICONEXCLAMATION);
424          result = 0; 
425       }
426 
427       return result;
428    }
429 }
430 
431 // Other systems.
432 else int main(string[] args) { return mainImpl(args); }
433 
434 int mainImpl(string[] args)
435 {    
436    // First start SDL ...
437    import viewer;
438    viewer.start();
439    
440    
441    // ... then gtk.
442    import resources;
443    import gtk.Main;
444    import gtk.Settings;
445 
446    Main.init(args);
447    b = new Builder();
448    b.addFromString(LAYOUT);
449    b.bindAll!mainwindow;
450 
451    // Add icon
452    {
453       import gdkpixbuf.Pixbuf;
454       auto pixbuf = new Pixbuf(cast(char[])LOGO, GdkColorspace.RGB, true, 8, LOGO_W, LOGO_H, LOGO_W*4, null, null);
455       
456       // GTK+
457       mainWindow.setIcon(pixbuf);
458 
459       // SDL
460       viewer.setIcon(pixbuf.getPixels, pixbuf.getWidth, pixbuf.getHeight);
461    }
462 
463    mainWindow.showAll();
464 
465    // For some obscures reasons, treeview won't work if
466    // designed on Glade. I have to build it here instead.
467    import gtk.TreeViewColumn;
468    import gtk.TreeSelection;
469    import gtk.ListStore;
470    import gtk.CellRendererToggle;
471    import gtk.CellRendererText;
472 
473    noiseListStore = new ListStore([GType.INT, GType.STRING]);
474 
475    TreeViewColumn column = new TreeViewColumn();
476    column.setTitle( "Layers" );
477    noiseList.appendColumn(column);
478 
479    CellRendererToggle cell_bool = new CellRendererToggle();
480    column.packStart(cell_bool, 0 );
481    column.addAttribute(cell_bool, "active", 0);
482 
483    CellRendererText cell_text = new CellRendererText();
484    column.packStart(cell_text, 0 );
485    column.addAttribute(cell_text, "text", 1);
486    cell_text.setProperty( "editable", 1 );
487 
488    // Line toggled
489    cell_bool.addOnToggled( delegate void(string p, CellRendererToggle){
490       import gtk.TreePath, gtk.TreeIter;
491         
492       auto path = new TreePath( p );
493       auto it = new TreeIter( noiseListStore, path );
494       noiseListStore.setValue(it, 0, it.getValueInt( 0 ) ? 0 : 1 );
495       
496       auto index = it.getTreePath().getIndices()[0];
497 
498       noises[index].visible = it.getValueInt(0) == 1;
499       updateNoiseInterface();
500    });
501 
502    // Line changed
503    cell_text.addOnEdited( delegate void(string p, string v, CellRendererText cell ){
504       
505       import gtk.TreePath, gtk.TreeIter;
506 
507       auto path = new TreePath( p );
508       auto it = new TreeIter( noiseListStore, path );
509       noiseListStore.setValue( it, 1, v );
510    });
511 
512    noiseList.setModel(noiseListStore);
513    noiseList.getSelection().addOnChanged(delegate(TreeSelection ts) { 
514       auto selected = ts.getSelected();
515       
516       if (selected is null) currentNoise = -1;
517       else currentNoise = selected.getTreePath().getIndices()[0];
518       
519       updateNoiseInterface();
520    });
521 
522 
523    // Start vase creation with default params
524    generator.start();
525    build();
526    
527    // Rendering loop (SDL)
528    renderTimeout = new Timeout(1000/30, 
529       delegate 
530       { 
531          if (viewer.isRunning) renderFrame(); 
532          else if (showPreview.getActive()) showPreview.setActive(false);
533          return true; 
534       }, 
535       false
536    );
537    
538    // Main loop (Gtk+)
539    Main.run();
540 
541    // Clear sdl stuffs
542    viewer.uninit();
543    return 0;
544 }
545