1 /* 2 Copyright © 2020, Inochi2D Project 3 Distributed under the 2-Clause BSD License, see LICENSE file. 4 5 Authors: Luna Nielsen 6 */ 7 module creator.windows.inpexport; 8 import creator.widgets.dummy; 9 import creator.widgets.tooltip; 10 import creator.widgets.label; 11 import creator.widgets.dialog; 12 import creator.widgets.texture; 13 import creator.windows; 14 import creator.core; 15 import creator.io; 16 import creator; 17 import std.string; 18 import creator.utils.link; 19 import inochi2d; 20 import i18n; 21 import std.stdio; 22 import std.conv; 23 import std.algorithm.sorting; 24 import std.algorithm.mutation; 25 26 enum ExportOptionsPane { 27 Atlassing = "Atlassing" 28 } 29 30 struct ExportOptions { 31 size_t atlasResolution = 2048; 32 const(char)* atlasResolutionString = "2048x2048"; 33 34 float resolutionScale = 1; 35 const(char)* resolutionScaleString = "100%"; 36 37 int padding = 16; 38 } 39 40 class ExportWindow : Window { 41 private: 42 string outFile; 43 ExportOptionsPane pane = ExportOptionsPane.Atlassing; 44 ExportOptions options; 45 46 Atlas preview; 47 48 bool forcedScale; 49 50 void beginSection(string title) { 51 incText(title); 52 incDummy(ImVec2(0, 4)); 53 igIndent(); 54 } 55 56 void endSection() { 57 igUnindent(); 58 igNewLine(); 59 } 60 61 void regenPreview() { 62 Part[] parts = incActivePuppet().getAllParts(); 63 64 // Force things to fit. 65 foreach(part; parts) { 66 vec2 size = vec2( 67 (part.bounds.z-part.bounds.x)+preview.padding, 68 (part.bounds.w-part.bounds.y)+preview.padding 69 ); 70 float xRatio = ((size.x*options.resolutionScale)/cast(float)options.atlasResolution)+0.01; 71 float yRatio = ((size.y*options.resolutionScale)/cast(float)options.atlasResolution)+0.01; 72 if (xRatio > 1.0) options.resolutionScale = cast(float)(options.atlasResolution/size.x)-0.01; 73 if (yRatio > 1.0) options.resolutionScale = cast(float)(options.atlasResolution/size.y)-0.01; 74 75 forcedScale = true; 76 } 77 78 preview.scale = options.resolutionScale; 79 preview.padding = options.padding; 80 81 preview.resize(options.atlasResolution); 82 preview.clear(); 83 84 int i = 0; 85 while (i < parts.length && preview.pack(parts[i++])) { } 86 preview.finalize(); 87 } 88 89 Atlas[] generateAtlasses() { 90 Atlas[] atlasses = [new Atlas(options.atlasResolution, options.padding, options.resolutionScale)]; 91 92 Part[] parts = incActivePuppet().getAllParts(); 93 size_t partsLeft = parts.length; 94 bool[Part] taken; 95 96 bool failed = false; 97 98 // Fill out taken list 99 foreach(part; parts) taken[part] = false; 100 101 // Sort parts by size 102 import std.math : cmp; 103 parts.sort!( 104 (a, b) => a.textures[0].width+a.textures[0].height > b.textures[0].width+b.textures[0].height, 105 SwapStrategy.stable 106 )(); 107 108 mwhile: while(partsLeft > 0) { 109 foreach(part; parts) { 110 if (taken[part] == true) continue; 111 112 if (atlasses[$-1].pack(part)) { 113 taken[part] = true; 114 partsLeft--; 115 failed = false; 116 continue mwhile; 117 } 118 } 119 120 // Prevent memory leak 121 if (failed) throw new Exception("A texture is too big for the atlas."); 122 123 // Failed putting elements in to atlas, create new empty atlas 124 failed = true; 125 atlasses[$-1].finalize(); 126 atlasses ~= new Atlas(options.atlasResolution, options.padding, options.resolutionScale); 127 } 128 129 return atlasses; 130 } 131 132 protected: 133 134 override 135 void onBeginUpdate() { 136 flags |= ImGuiWindowFlags.NoSavedSettings; 137 138 ImVec2 wpos = ImVec2( 139 igGetMainViewport().Pos.x+(igGetMainViewport().Size.x/2), 140 igGetMainViewport().Pos.y+(igGetMainViewport().Size.y/2), 141 ); 142 143 ImVec2 uiSize = ImVec2( 144 512, 145 256+128 146 ); 147 148 igSetNextWindowPos(wpos, ImGuiCond.Appearing, ImVec2(0.5, 0.5)); 149 igSetNextWindowSize(uiSize, ImGuiCond.Appearing); 150 igSetNextWindowSizeConstraints(uiSize, ImVec2(float.max, float.max)); 151 super.onBeginUpdate(); 152 } 153 154 override 155 void onUpdate() { 156 float availX = incAvailableSpace().x; 157 158 // Sidebar 159 if (igBeginChild("SettingsSidebar", ImVec2(availX/3.5, -28), true)) { 160 igPushTextWrapPos(128); 161 if (igSelectable(__("Atlassing"), pane == ExportOptionsPane.Atlassing)) { 162 pane = ExportOptionsPane.Atlassing; 163 } 164 igPopTextWrapPos(); 165 } 166 igEndChild(); 167 168 // Nice spacing 169 igSameLine(0, 4); 170 171 // Contents 172 if (igBeginChild("SettingsContent", ImVec2(0, -28), true)) { 173 ImVec2 avail = incAvailableSpace(); 174 175 // Begins section, REMEMBER TO END IT 176 beginSection(_(cast(string)pane)); 177 178 // Start settings panel elements 179 igPushItemWidth(avail.x/2); 180 switch(pane) { 181 case ExportOptionsPane.Atlassing: 182 float previewSize = avail.y/2; 183 184 // Funky tricks to center preview 185 igUnindent(); 186 incDummy(ImVec2((avail.x/2)-(previewSize/2), previewSize)); 187 igSameLine(); 188 incTextureSlotUntitled("PREVIEW0", preview.textures[0], ImVec2(previewSize, previewSize), 64); 189 igIndent(); 190 191 if (igBeginCombo(__("Resolution"), options.atlasResolutionString)) { 192 193 size_t size = 1024; 194 foreach(i; 0..3) { 195 size <<= 1; 196 197 const(char)* sizestr = "%1$sx%1$s".format(size.text).toStringz; 198 if (igMenuItem(sizestr, null, options.atlasResolution == size)) { 199 options.atlasResolution = size; 200 options.atlasResolutionString = sizestr; 201 this.regenPreview(); 202 } 203 } 204 igEndCombo(); 205 } 206 207 int resScaleInt = cast(int)(options.resolutionScale*100); 208 if (igInputInt(__("Texture Scale"), &resScaleInt, 1, 10)) { 209 if (resScaleInt < 25) resScaleInt = 25; 210 if (resScaleInt > 200) resScaleInt = 200; 211 options.resolutionScale = (cast(float)resScaleInt/100.0); 212 this.regenPreview(); 213 } 214 215 if (igInputInt(__("Padding"), &options.padding, 1, 10)) { 216 if (options.padding < 0) options.padding = 0; 217 this.regenPreview(); 218 } 219 break; 220 default: 221 incText(_("No settings for this category.")); 222 break; 223 } 224 igPopItemWidth(); 225 } 226 igEndChild(); 227 228 // Bottom buttons 229 if (igBeginChild("SettingsButtons", ImVec2(0, 0), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) { 230 availX = incAvailableSpace().x; 231 if (forcedScale) { 232 igPushTextWrapPos(availX-128); 233 incTextColored( 234 ImVec4(0.8, 0.2, 0.2, 1), 235 _("A texture was too large to fit the texture atlas, the textures have been scaled down.") 236 ); 237 igPopTextWrapPos(); 238 } 239 igSameLine(0, 0); 240 incDummy(ImVec2(-64, 0)); 241 igSameLine(0, 0); 242 243 if (igButton(__("Export"), ImVec2(64, 24))) { 244 try { 245 // Write the puppet to file 246 incExportINP(incActivePuppet(), generateAtlasses(), outFile); 247 incSetStatus(_("%s was exported...".format(outFile))); 248 } catch(Exception ex) { 249 incDialog(__("Error"), ex.msg); 250 incSetStatus(_("Export failed...")); 251 } 252 253 // TODO: Show error in export window? 254 this.close(); 255 } 256 } 257 igEndChild(); 258 } 259 260 public: 261 this(string outFile) { 262 super(_("Export Options")); 263 this.outFile = outFile; 264 265 preview = new Atlas(2048, 16, 1); 266 this.regenPreview(); 267 } 268 }