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;
8 import inochi2d;
9 import inochi2d.core.dbg;
10 import inochi2d.core.nodes.common;
11 import creator.viewport;
12 import creator.viewport.model;
13 import creator.viewport.model.deform;
14 import creator.core;
15 import creator.core.actionstack;
16 import creator.windows;
17 import creator.atlas;
18 import creator.ext;
19 import creator.widgets.dialog;
20 
21 public import creator.ver;
22 public import creator.atlas;
23 public import creator.io;
24 import creator.core.colorbleed;
25 
26 import std.file;
27 import std.format;
28 import i18n;
29 
30 /**
31     A project
32 */
33 class Project {
34     /**
35         The puppet in the project
36     */
37     Puppet puppet;
38 
39     /**
40         Textures for use in the puppet
41 
42         Can be rearranged
43     */
44     Texture[] textures;
45 }
46 
47 private {
48     Project activeProject;
49     Node[] selectedNodes;
50     Drawable[] drawables;
51     Parameter armedParam;
52     size_t armedParamIdx;
53     string currProjectPath;
54     string[] prevProjects;
55 }
56 
57 /**
58     Edit modes
59 */
60 enum EditMode {
61     /**
62         Model editing mode
63     */
64     ModelEdit,
65 
66     /**
67         Vertex Editing Mode
68     */
69     VertexEdit,
70 
71     /**
72         Animation Editing Mode
73     */
74     AnimEdit,
75 
76     /**
77         Model testing mode
78     */
79     ModelTest
80 }
81 
82 bool incShowVertices    = true; /// Show vertices of selected parts
83 bool incShowBounds      = true; /// Show bounds of selected parts
84 bool incShowOrientation = true; /// Show orientation gizmo of selected parts
85 
86 /**
87     Current edit mode
88 */
89 EditMode editMode_;
90 
91 /**
92     Clears the imgui data
93 */
94 void incClearImguiData() {
95     auto ctx = igGetCurrentContext();
96     if (ctx) {
97         foreach(ImGuiWindow* window; ctx.Windows.Data[0..ctx.Windows.Size]) {
98             if (window) {
99                 ImGuiStorage_Clear(&window.StateStorage);
100             }
101         }
102     }
103 }
104 
105 /**
106     Returns the current project path
107 */
108 string incProjectPath() {
109     return currProjectPath;
110 }
111 
112 /**
113     Return a list of prior projects
114 */
115 string[] incGetPrevProjects() {
116     return incSettingsGet!(string[])("prev_projects");
117 }
118 
119 void incAddPrevProject(string path) {
120     import std.algorithm.searching : countUntil;
121     import std.algorithm.mutation : remove;
122     string[] projects = incGetPrevProjects();
123 
124     ptrdiff_t idx = projects.countUntil(path);
125     if (idx >= 0) {
126         projects = projects.remove(idx);
127     }
128 
129     // Put project to the start of the "previous" list and
130     // limit to 10 elements
131     projects = path.dup ~ projects;
132     if(projects.length > 10) projects.length = 10;
133 
134     // Then save.
135     incSettingsSet("prev_projects", projects);
136     incSettingsSave();
137 }
138 
139 /**
140     Creates a new project
141 */
142 void incNewProject() {
143     incClearImguiData();
144 
145     currProjectPath = "";
146     editMode_ = EditMode.ModelEdit;
147     import creator.viewport : incViewportReset;
148     
149     incPopWindowListAll();
150 
151     activeProject = new Project;
152     activeProject.puppet = new ExPuppet;
153     incFocusCamera(activeProject.puppet.root);
154     incSelectNode(null);
155     incDisarmParameter();
156 
157     inDbgDrawMeshVertexPoints = true;
158     inDbgDrawMeshOutlines = true;
159     inDbgDrawMeshOrientation = true;
160 
161     incViewportReset();
162 
163     incActionClearHistory();
164     incFreeMemory();
165 
166     incViewportPresentMode(editMode_);
167 }
168 
169 void incResetRootNode(ref Puppet puppet) {
170     puppet.root.localTransform.translation = vec3(0, 0, 0);
171     puppet.root.localTransform.rotation = vec3(0, 0, 0);
172     puppet.root.localTransform.scale = vec2(1, 1);
173 }
174 
175 void incOpenProject(string path) {
176     incClearImguiData();
177     
178     Puppet puppet;
179 
180     // Load the puppet from file
181     try {
182         puppet = inLoadPuppet!ExPuppet(path);
183     } catch (Exception ex) {
184         incDialog(__("Error"), ex.msg);
185         return;
186     }
187 
188     // Clear out stuff by creating a new project
189     incNewProject();
190 
191     // Set the path
192     currProjectPath = path;
193     incAddPrevProject(path);
194 
195     incResetRootNode(puppet);
196 
197     incActiveProject().puppet = puppet;
198     incFocusCamera(incActivePuppet().root);
199     incFreeMemory();
200 
201     incSetStatus(_("%s opened successfully.").format(currProjectPath));
202 }
203 
204 void incSaveProject(string path) {
205     import std.path : setExtension;
206     try {
207         string finalPath = path.setExtension(".inx");
208         currProjectPath = path;
209         incAddPrevProject(finalPath);
210 
211         // Remember to populate texture slots otherwise things will break real bad!
212         incActivePuppet().populateTextureSlots();
213 
214         // Write the puppet to file
215         inWriteINPPuppet(incActivePuppet(), finalPath);
216 
217         incSetStatus(_("%s saved successfully.").format(currProjectPath));
218     } catch(Exception ex) {
219         incSetStatus(_("Failed to save %s").format(currProjectPath));
220         incDialog(__("Error"), ex.msg);
221     }
222 }
223 
224 /**
225     Imports image files from a selected folder.
226 */
227 void incImportFolder(string folder) {
228     incNewProject();
229 
230     import std.file : dirEntries, SpanMode;
231     import std.path : stripExtension, baseName;
232 
233     string[] failedFiles;
234     // For each file find PNG, TGA and JPEG files and import them
235     Puppet puppet = new ExPuppet();
236     size_t i;
237     foreach(file; dirEntries(folder, SpanMode.shallow, false)) {
238         try {
239 
240             // TODO: Check for position.ini
241 
242             auto tex = ShallowTexture(file);
243             inTexPremultiply(tex.data);
244 
245             Part part = inCreateSimplePart(new Texture(tex), null, file.baseName.stripExtension);
246             part.zSort = -((cast(float)i++)/100);
247             puppet.root.addChild(part);
248         } catch(Exception ex) {
249             failedFiles ~= ex.msg;
250         }
251     }
252 
253     if (failedFiles.length > 0) {
254         import std.array : join;
255         incDialog("ImgLoadError", format(_("The following errors occured during file loading\n%s"), failedFiles.join("\n")));
256     }
257     
258     puppet.rescanNodes();
259     puppet.populateTextureSlots();
260     incActiveProject().puppet = puppet;
261     incFocusCamera(incActivePuppet().root);
262     incFreeMemory();
263 
264     if (failedFiles.length > 0) incSetStatus(_("Folder import completed with errors..."));
265     else incSetStatus(_("Folder import completed..."));
266     
267 }
268 
269 /**
270     Imports an Inochi2D puppet
271 */
272 void incImportINP(string file) {
273     incNewProject();
274     Puppet puppet;
275     try {
276 
277         puppet = inLoadPuppet(file);
278         incSetStatus(_("%s was imported...".format(file)));
279     } catch(Exception ex) {
280         
281         incDialog(__("Error"), ex.msg);
282         incSetStatus(_("Import failed..."));
283         return;
284     }
285     incActiveProject().puppet = puppet;
286     incFocusCamera(incActivePuppet().root);
287     incFreeMemory();
288 }
289 
290 /**
291     Exports an Inochi2D Puppet
292 */
293 void incExportINP(string file) {
294     import creator.windows.inpexport;
295     import std.path : setExtension;
296     string oFile = file.setExtension(".inp");
297     incPushWindow(new ExportWindow(oFile));
298 }
299 
300 void incRegenerateMipmaps() {
301 
302     // Allow for nice looking filtering
303     foreach(texture; incActiveProject().puppet.textureSlots) {
304         texture.genMipmap();
305         texture.setFiltering(Filtering.Linear);
306     }
307     incSetStatus(_("Mipmap generation completed."));
308 }
309 
310 /**
311     Re-bleeds textures in a model
312 */
313 void incRebleedTextures() {
314     incTaskAdd("Rebleed", () {
315         incTaskStatus("Bleeding textures...");
316         foreach(i, Texture texture; activeProject.puppet.textureSlots) {
317             incTaskProgress(cast(float)i/activeProject.puppet.textureSlots.length);
318             incTaskYield();
319             incColorBleedPixels(texture);
320         }
321     });
322     incSetStatus(_("Texture bleeding completed."));
323 }
324 
325 /**
326     Force the garbage collector to collect model memory
327 */
328 void incFreeMemory() {
329     import core.memory : GC;
330     GC.collect();
331     GC.minimize();
332 }
333 
334 /**
335     Gets puppet in active project
336 */
337 ref Puppet incActivePuppet() {
338     return activeProject.puppet;
339 }
340 
341 /**
342     Gets active project
343 */
344 ref Project incActiveProject() {
345     return activeProject;
346 }
347 
348 /**
349     Gets the currently armed parameter
350 */
351 Parameter incArmedParameter() {
352     return editMode_ == EditMode.ModelEdit ? armedParam : null;
353 }
354 
355 /**
356     Gets the currently armed parameter index
357 */
358 size_t incArmedParameterIdx() {
359     return editMode_ == EditMode.ModelEdit ? armedParamIdx : 0;
360 }
361 
362 /**
363     Gets the currently selected node
364 */
365 ref Node[] incSelectedNodes() {
366     return selectedNodes;
367 }
368 
369 /**
370     Gets a list of the current drawables
371 */
372 ref Drawable[] incDrawables() {
373     return drawables;
374 }
375 
376 /**
377     Gets the currently selected root node
378 */
379 ref Node incSelectedNode() {
380     return selectedNodes.length == 0 ? incActivePuppet.root : selectedNodes[0];
381 }
382 
383 /**
384     Arms a parameter
385 */
386 void incArmParameter(size_t i, ref Parameter param) {
387     armedParam = param;
388     armedParamIdx = i;
389     incViewportNodeDeformNotifyParamValueChanged();
390     incActivePuppet.enableDrivers = false;
391     incActivePuppet.resetDrivers();
392 }
393 
394 /**
395     Disarms parameter recording
396 */
397 void incDisarmParameter() {
398     armedParam = null;
399     armedParamIdx = 0;
400     incViewportNodeDeformNotifyParamValueChanged();
401     incActivePuppet.enableDrivers = true;
402     incActivePuppet.resetDrivers();
403 }
404 
405 /**
406     Selects a node
407 */
408 void incSelectNode(Node n = null) {
409     if (n is null) selectedNodes.length = 0;
410     else selectedNodes = [n];
411     incViewportModelNodeSelectionChanged();
412 }
413 
414 /**
415     Adds node to selection
416 */
417 void incAddSelectNode(Node n) {
418     if (incArmedParameter()) return;
419     selectedNodes ~= n;
420 }
421 
422 /**
423     Remove node from selection
424 */
425 void incRemoveSelectNode(Node n) {
426     foreach(i, nn; selectedNodes) {
427         if (n.uuid == nn.uuid) {
428             import std.algorithm.mutation : remove;
429             selectedNodes = selectedNodes.remove(i);
430         }
431     }
432 }
433 
434 private void incSelectAllRecurse(Node n) {
435     incAddSelectNode(n);
436     foreach(child; n.children) {
437         incSelectAllRecurse(child);
438     }
439 }
440 
441 /**
442     Selects all nodes
443 */
444 void incSelectAll() {
445     if (incArmedParameter()) return;
446     incSelectNode();
447     foreach(child; incActivePuppet().root.children) {
448         incSelectAllRecurse(child);
449     }
450 }
451 
452 /**
453     Gets whether the node is in the selection
454 */
455 bool incNodeInSelection(Node n) {
456     foreach(i, nn; selectedNodes) {
457         if (nn is null) continue;
458         
459         if (n.uuid == nn.uuid) return true;
460     }
461 
462     return false;
463 }
464 
465 /**
466     Focus camera at node
467 */
468 void incFocusCamera(Node node) {
469     import creator.viewport : incViewportTargetZoom, incViewportTargetPosition;
470     if (node is null) return;
471 
472     // Calculate actual center.
473     vec4 bounds = node.getCombinedBounds();
474     if (auto drawable = cast(Drawable)node) {
475         drawable.updateBounds();
476         bounds = drawable.bounds;
477     }
478     vec2 pos = bounds.xy+((bounds.zw - bounds.xy)*0.5);
479 
480     // Focus camera to calculated center
481     incFocusCamera(node, vec2(-pos.x, -pos.y));
482 }
483 
484 /**
485     Focus camera at node
486 */
487 void incFocusCamera(Node node, vec2 position) {
488     import creator.viewport : incViewportTargetZoom, incViewportTargetPosition;
489     if (node is null) return;
490 
491     int width, height;
492     inGetViewport(width, height);
493 
494     auto nt = node.transform;
495 
496     vec4 bounds = node.getCombinedBounds();
497     vec2 boundsSize = bounds.zw - bounds.xy;
498     if (auto drawable = cast(Drawable)node) {
499         boundsSize = drawable.bounds.zw - drawable.bounds.xy;
500     } else {
501         nt.translation = vec3(bounds.x + ((bounds.z-bounds.x)*0.5), bounds.y + ((bounds.w-bounds.y)*0.5), 0);
502     }
503     
504 
505     float largestViewport = min(width, height);
506     float largestBounds = max(boundsSize.x, boundsSize.y);
507 
508     float factor = largestViewport/largestBounds;
509     incViewportTargetZoom = clamp(factor*0.90, 0.1, 2.5);
510 
511     incViewportTargetPosition = vec2(
512         position.x,
513         position.y
514     );
515 }
516 
517 /**
518     Gets the current editing mode
519 */
520 EditMode incEditMode() {
521     return editMode_;
522 }
523 
524 /**
525     Sets the current editing mode
526 */
527 void incSetEditMode(EditMode editMode, bool unselect = true) {
528     incViewportWithdrawMode(editMode_);
529 
530     if (armedParam) {
531         armedParam.value = armedParam.getClosestKeypointValue(armedParam.value);
532     }
533     if (unselect) incSelectNode(null);
534     if (editMode != EditMode.ModelEdit) {
535         drawables = activeProject.puppet.findNodesType!Drawable(activeProject.puppet.root);
536     }
537     editMode_ = editMode;
538 
539     incViewportPresentMode(editMode_);
540 }