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 }