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 }