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