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.psdmerge;
8 import creator.windows;
9 import creator.core;
10 import creator.widgets;
11 import creator;
12 import creator.ext;
13 import std.string;
14 import creator.utils.link;
15 import inochi2d;
16 import i18n;
17 import psd;
18 import std.uni : toLower;
19 import std.stdio : File;
20 
21 /**
22     Binding between layer and node
23 */
24 struct NodeLayerBinding {
25     Layer layer;
26     Texture layerTexture;
27     vec4 texturePreviewBounds;
28 
29     Node node;
30     bool replaceTexture;
31     string layerPath;
32     const(char)* layerName;
33     string indexableName;
34     bool ignore;
35     int depth() {
36         return replaceTexture ? node.depth-1 : node.depth;
37     }
38 }
39 
40 class PSDMergeWindow : Window {
41 private:
42     File file;
43     PSD document;
44     NodeLayerBinding*[] bindings;
45     bool renameMapped;
46     bool retranslateMapped;
47     bool resortModel;
48     bool onlyUnmapped;
49     ExPart[] parts;
50 
51     string layerFilter;
52     string nodeFilter;
53 
54     bool appliedTextures;
55 
56     enum PreviewSize = 128f;
57 
58     void populateBindings() {
59         import std.array : join;
60         auto puppet = incActivePuppet();
61         parts = puppet.findNodesType!ExPart(puppet.root);
62 
63 
64         ExPart findPartForSegment(string segment) {
65             foreach(ref ExPart part; parts) {
66                 if (part.layerPath == segment) return part;
67             }
68             return null;
69         }
70 
71         ExPart findPartForName(string segment) {
72             import std.path : baseName;
73             foreach(ref ExPart part; parts) {
74                 if (baseName(part.layerPath) == baseName(segment)) return part;
75             }
76             return null;
77         }
78 
79         string[] layerPathSegments;
80         string calcSegment;
81         foreach_reverse(layer; document.layers) {
82 
83             // Build layer path segments
84             if (layer.type != LayerType.Any) {
85                 if (layer.name != "</Layer set>" && layer.name != "</Layer group>") layerPathSegments ~= layer.name; 
86                 else layerPathSegments.length--;
87 
88                 calcSegment = layerPathSegments.length > 0 ? "/"~layerPathSegments.join("/") : "";
89                 continue;
90             }
91 
92             // Load texture in to memory
93             layer.extractLayerImage();
94             inTexPremultiply(layer.data);
95             auto layerTexture = new Texture(layer.data, layer.width, layer.height);
96             layer.data = null;
97 
98             // Calculate render size
99             float widthScale = PreviewSize / cast(float)layer.width;
100             float heightScale = PreviewSize / cast(float)layer.height;
101             float scale = min(widthScale, heightScale);
102             
103             vec4 bounds = vec4(0, 0, layer.width*scale, layer.height*scale);
104             if (widthScale > heightScale) bounds.x = (PreviewSize-bounds.z)/2;
105             else if (widthScale < heightScale) bounds.y = (PreviewSize-bounds.w)/2;
106 
107             // See if any matching segments can be found
108             string currSegment = "%s/%s".format(calcSegment, layer.name);
109             ExPart seg = findPartForSegment(currSegment);
110             if (seg) {
111 
112                 // If so, default to replace
113                 bindings ~= new NodeLayerBinding(layer, layerTexture, bounds, seg, true, currSegment, layer.name.toStringz, layer.name.toLower);
114             } else {
115 
116                 // Try to match name only if path match fails
117                 seg = findPartForName(currSegment);
118                 if (seg) {
119 
120                     // If so, default to replace
121                     bindings ~= new NodeLayerBinding(layer, layerTexture, bounds, seg, true, currSegment, layer.name.toStringz, layer.name.toLower);
122                     continue;
123                 }
124 
125                 // Otherwise, default to add
126                 bindings ~= new NodeLayerBinding(layer, layerTexture, bounds, puppet.root, false, currSegment, layer.name.toStringz, layer.name.toLower);
127             }
128         }
129     }
130 
131     // rebuffer Part to update shape correctly. (code copied from mesheditor.d)
132     void updatePart(Node node) {
133         import creator.viewport.common.mesh;
134         Part part = cast(Part)node;
135         if (part !is null) {
136             auto mesh = new IncMesh(part.getMesh());
137             MeshData data = mesh.export_();
138             data.fixWinding();
139 
140             // Fix UVs
141             foreach(i; 0..data.uvs.length) {
142                 // Texture 0 is always albedo texture
143                 auto tex = part.textures[0];
144 
145                 // By dividing by width and height we should get the values in UV coordinate space.
146                 data.uvs[i].x /= cast(float)tex.width;
147                 data.uvs[i].y /= cast(float)tex.height;
148                 data.uvs[i] += vec2(0.5, 0.5);
149             }
150             part.rebuffer(data);
151         }
152         foreach (Node child; node.children) {
153             updatePart(child);
154         }
155     }
156 
157     void apply() {
158         import std.algorithm.sorting : sort;
159         bindings.sort!((a, b) => a.depth < b.depth)();
160         
161         vec2i docCenter = vec2i(document.width/2, document.height/2);
162         auto puppet = incActivePuppet();
163 
164         // Apply all the bindings to the node tree.
165         foreach(binding; bindings) {
166             if (binding.ignore) continue;
167 
168             auto layerSize = cast(int[2])binding.layer.size();
169             vec2i layerPosition = vec2i(
170                 binding.layer.left,
171                 binding.layer.top
172             );
173 
174             vec3 worldTranslation = vec3(
175                 (layerPosition.x+(layerSize[0]/2))-cast(float)docCenter.x,
176                 (layerPosition.y+(layerSize[1]/2))-cast(float)docCenter.y,
177                 0
178             );
179 
180             vec3 localPosition = 
181                 binding.node ? 
182                 Node.getRelativePosition(binding.node.transformNoLock.matrix, mat4.translation(worldTranslation)) : 
183                 worldTranslation;
184 
185             if (binding.replaceTexture) {
186                 localPosition = 
187                     binding.node.parent ? 
188                     Node.getRelativePosition(binding.node.parent.transformNoLock.matrix, mat4.translation(worldTranslation)) : 
189                     worldTranslation;
190 
191                 // If we don't do this the subsequent child nodes will work on old data.
192                 binding.node.recalculateTransform = true;
193 
194                 // If the user requests that nodes should be renamed to match their respective layers, do so.
195                 if (renameMapped) {
196                     (cast(ExPart)binding.node).name = binding.layer.name;
197                 }
198 
199                 if (retranslateMapped) {
200                     if (binding.node.lockToRoot) {
201                         binding.node.localTransform.translation = worldTranslation;
202                     } else {
203                         binding.node.localTransform.translation = localPosition;
204                     }
205                 }
206 
207                 (cast(ExPart)binding.node).textures[0].dispose();
208                 (cast(ExPart)binding.node).textures[0] = binding.layerTexture;
209                 (cast(ExPart)binding.node).layerPath = binding.layerPath;
210             } else {
211                 auto part = incCreateExPart(binding.layerTexture, binding.node, binding.layer.name);
212                 part.layerPath = binding.layerPath;
213                 part.localTransform.translation = localPosition;
214             }
215         }
216         
217         // Unload PSD, we're done with it
218         destroy(document);
219         puppet.root.transformChanged();
220         updatePart(puppet.root);
221 
222         // Repopulate texture slots, removing unused textures
223         puppet.populateTextureSlots();
224     }
225 
226     void layerView() {
227 
228         import std.algorithm.searching : canFind;
229         foreach(i; 0..bindings.length) {
230             auto layer = bindings[i];
231 
232             if (onlyUnmapped && layer.replaceTexture) continue;
233             if (layerFilter.length > 0 && !layer.indexableName.canFind(layerFilter.toLower)) continue;
234 
235             igPushID(cast(int)i);
236                 const(char)* displayName = layer.layerName;
237                 if (layer.replaceTexture) {
238                     displayName = _("%s  %s").format(layer.layer.name, layer.node.name).toStringz;
239                 }
240 
241                 if (layer.ignore) incTextDisabled("");
242                 else incText("");
243                 if (igIsItemClicked()) {
244                     layer.ignore = !layer.ignore;
245                 }
246                 igSameLine(0, 8);
247 
248                 igSelectable(displayName, false, ImGuiSelectableFlagsI.SpanAvailWidth);
249 
250                 if(igBeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) {
251                     igSetDragDropPayload("__REMAP", cast(void*)&layer, (&layer).sizeof, ImGuiCond.Always);
252                     igText(layer.layerName);
253                     igEndDragDropSource();
254                 }
255 
256                 if (igIsItemHovered()) {
257                     igBeginTooltip();
258                         ImVec2 tl;
259                         igGetCursorPos(&tl);
260 
261                         igItemSize(ImVec2(PreviewSize, PreviewSize));
262 
263                         igSetCursorPos(
264                             ImVec2(tl.x+layer.texturePreviewBounds.x, tl.y+layer.texturePreviewBounds.y)
265                         );
266 
267                         igImage(
268                             cast(void*)layer.layerTexture.getTextureId(), 
269                             ImVec2(layer.texturePreviewBounds.z, layer.texturePreviewBounds.w)
270                         );
271                     igEndTooltip();
272                 }
273                 igOpenPopupOnItemClick("LAYER_POPUP");
274 
275                 if (igBeginPopup("LAYER_POPUP")) {
276                     if (igMenuItem(__("Unmap"))) {
277                         layer.replaceTexture = false;
278                         layer.node = incActivePuppet.root;
279                     }
280 
281                     if (igMenuItem(!layer.ignore ? __("Ignore") : __("Use"))) {
282                         layer.ignore = !layer.ignore;
283                     }
284                     igEndPopup();
285                 }
286             igPopID();
287         }
288     }
289 
290     void treeView() {
291 
292         import std.algorithm.searching : canFind;
293         foreach(ref ExPart part; parts) {
294             if (nodeFilter.length > 0 && !part.name.toLower.canFind(nodeFilter.toLower)) continue;
295 
296             igSelectable(part.cName, false, ImGuiSelectableFlagsI.SpanAvailWidth);
297 
298             // Only allow reparenting one node
299             if(igBeginDragDropTarget()) {
300                 const(ImGuiPayload)* payload = igAcceptDragDropPayload("__REMAP");
301                 if (payload !is null) {
302                     NodeLayerBinding* payloadNode = *cast(NodeLayerBinding**)payload.Data;
303                     
304                     // Don't allow multiple bindings to a single part.
305                     foreach(ref expn; bindings) {
306                         if (expn.node == part) {
307                             expn.node = null;
308                             expn.replaceTexture = false;
309                         }
310                     }
311 
312                     payloadNode.node = part;
313                     payloadNode.replaceTexture = true;
314 
315                     igEndDragDropTarget();
316                     return;
317                 }
318                 igEndDragDropTarget();
319             }
320 
321             // Incredibly cursed preview image
322             if (igIsItemHovered()) {
323                 igBeginTooltip();
324                     incText(part.getNodePath());
325                     // Calculate render size
326                     float widthScale = PreviewSize / cast(float)part.textures[0].width;
327                     float heightScale = PreviewSize / cast(float)part.textures[0].height;
328                     float fscale = min(widthScale, heightScale);
329                     
330                     vec4 bounds = vec4(0, 0, part.textures[0].width*fscale, part.textures[0].height*fscale);
331                     if (widthScale > heightScale) bounds.x = (PreviewSize-bounds.z)/2;
332                     else if (widthScale < heightScale) bounds.y = (PreviewSize-bounds.w)/2;
333 
334                     ImVec2 tl;
335                     igGetCursorPos(&tl);
336 
337                     igItemSize(ImVec2(PreviewSize, PreviewSize));
338 
339                     igSetCursorPos(
340                         ImVec2(tl.x+bounds.x, tl.y+bounds.y)
341                     );
342 
343                     igImage(
344                         cast(void*)part.textures[0].getTextureId(), 
345                         ImVec2(bounds.z, bounds.w)
346                     );
347                 igEndTooltip();
348             }
349         }
350 
351     }
352 
353 protected:
354 
355     override
356     void onBeginUpdate() {
357         igSetNextWindowSizeConstraints(ImVec2(640, 480), ImVec2(float.max, float.max));
358         super.onBeginUpdate();
359     }
360 
361     override
362     void onUpdate() {
363         ImVec2 space = incAvailableSpace();
364         float gapspace = 8;
365         float childWidth = (space.x/2);
366         float childHeight = space.y-(24);
367         float filterWidgetHeight = 24;
368         float optionsListHeight = 24;
369 
370         igBeginGroup();
371             if (igBeginChild("###Layers", ImVec2(childWidth, childHeight))) {
372                 incInputText("##", childWidth-gapspace, layerFilter);
373 
374                 igBeginListBox("###LayerList", ImVec2(childWidth-gapspace, childHeight-filterWidgetHeight-optionsListHeight));
375                     layerView();
376                 igEndListBox();
377                 
378                 igCheckbox(__("Only show unmapped"), &onlyUnmapped);
379             }
380             igEndChild();
381 
382             igSameLine(0, gapspace);
383 
384             if (igBeginChild("###Nodes", ImVec2(childWidth, childHeight))) {
385                 incInputText("##", childWidth, nodeFilter);
386 
387                 igBeginListBox("###NodeList", ImVec2(childWidth, childHeight-filterWidgetHeight-optionsListHeight));
388                     treeView();
389                 igEndListBox();
390             }
391             igEndChild();
392         igEndGroup();
393 
394 
395         igBeginGroup();
396 
397             // Auto-rename
398             igCheckbox(__("Auto-rename"), &renameMapped);
399             incTooltip(_("Renames all mapped nodes to match the names of the PSD layer that was merged in to them."));
400 
401             igSameLine(0, 8);
402             igCheckbox(__("Re-translate"), &retranslateMapped);
403             incTooltip(_("Moves all nodes so that they visually match their position in the canvas."));
404 
405             // igSameLine(0, 8);
406             // igCheckbox(__("Re-sort"), &resortModel);
407             // incTooltip(_("[NOT IMPLEMENTED] Sorts all nodes zSorting position to match the sorting in the PSD."));
408 
409 
410             // Spacer
411             space = incAvailableSpace();
412             incSpacer(ImVec2(-(64), 32));
413 
414             // 
415             igSameLine(0, 0);
416             if (igButton(__("Merge"), ImVec2(64, 24))) {
417                 appliedTextures = true;
418                 apply();
419                 this.close();
420                 
421                 igEndGroup();
422                 return;
423             }
424         igEndGroup();
425     }
426 
427     override
428     void onClose() {
429         import core.memory : GC;
430 
431         file.close();
432         if (!appliedTextures) {
433             foreach(ref binding; bindings) {
434                 binding.layerTexture.dispose();
435             }
436         }
437         bindings.length = 0;
438         destroy(document);
439 
440         GC.collect();
441         GC.minimize();
442     }
443 
444 public:
445     ~this() { }
446 
447     this(string path) {
448         file = File(path);
449         document = parseDocument(file);
450         this.populateBindings();
451         super(_("PSD Merging"));
452     }
453 }
454