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