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