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