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