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.panels.inspector; 8 import creator.core; 9 import creator.panels; 10 import creator.widgets; 11 import creator.utils; 12 import creator.windows; 13 import creator; 14 import inochi2d; 15 import std.string; 16 import std.algorithm.searching; 17 import std.algorithm.mutation; 18 import std.conv; 19 import creator.medit; 20 import i18n; 21 22 import creator.actions.node; 23 24 /** 25 The inspector panel 26 */ 27 class InspectorPanel : Panel { 28 private: 29 30 31 protected: 32 override 33 void onUpdate() { 34 auto nodes = incSelectedNodes(); 35 if (nodes.length == 1) { 36 Node node = nodes[0]; 37 if (node !is null && node != incActivePuppet().root) { 38 39 // Per-edit mode inspector drawers 40 switch(incEditMode()) { 41 case EditMode.ModelEdit: 42 incModelModeHeader(node); 43 incInspectorModelTRS(node); 44 45 // The sorting order ID, which Inochi2D uses to sort 46 // Parts to draw in the user specified order. 47 // negative values = closer to camera 48 // positive values = further away from camera 49 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Sorting")); 50 float zsortV = node.relZSort; 51 if (igInputFloat("ZSort", &zsortV, 0.01, 0.05, "%0.2f")) { 52 node.zSort = zsortV; 53 } 54 55 // Node Drawable Section 56 if (Drawable drawable = cast(Drawable)node) { 57 58 // Padding 59 igSpacing(); 60 igSpacing(); 61 igSpacing(); 62 igSpacing(); 63 incInspectorModelDrawable(drawable); 64 } 65 66 // Node Part Section 67 if (Part part = cast(Part)node) { 68 69 // Padding 70 igSpacing(); 71 igSpacing(); 72 igSpacing(); 73 igSpacing(); 74 incInspectorModelPart(part); 75 } 76 77 break; 78 case EditMode.VertexEdit: 79 incCommonNonEditHeader(node); 80 incInspectorMeshEditDrawable(cast(Drawable)node); 81 break; 82 case EditMode.DeformEdit: 83 incCommonNonEditHeader(node); 84 break; 85 default: assert(0); 86 } 87 } else incInspectorModelInfo(); 88 } else if (nodes.length == 0) { 89 igText(__("No nodes selected...")); 90 } else { 91 igText(__("Can only inspect a single node...")); 92 } 93 } 94 95 public: 96 this() { 97 super("Inspector", _("Inspector"), true); 98 } 99 } 100 101 /** 102 Generate logger frame 103 */ 104 mixin incPanel!InspectorPanel; 105 106 107 108 private: 109 110 // 111 // COMMON 112 // 113 114 void incCommonNonEditHeader(Node node) { 115 // Top level 116 igPushID(node.uuid); 117 string typeString = "%s\0".format(incTypeIdToIcon(node.typeId())); 118 auto len = incMeasureString(typeString); 119 igText(node.name.toStringz); 120 igSameLine(0, 0); 121 incDummy(ImVec2(-(len.x-14), len.y)); 122 igSameLine(0, 0); 123 igText(typeString.ptr); 124 igPopID(); 125 igSeparator(); 126 } 127 128 // 129 // MODEL MODE 130 // 131 132 void incInspectorModelInfo() { 133 auto rootNode = incActivePuppet().root; 134 auto puppet = incActivePuppet(); 135 136 // Top level 137 igPushID(rootNode.uuid); 138 string typeString = "\0"; 139 auto len = incMeasureString(typeString); 140 igText(__("Puppet")); 141 igSameLine(0, 0); 142 incDummy(ImVec2(-(len.x-14), len.y)); 143 igSameLine(0, 0); 144 igText(typeString.ptr); 145 igPopID(); 146 igSeparator(); 147 148 igSpacing(); 149 igSpacing(); 150 151 // Version info 152 { 153 len = incMeasureString(_("Inochi2D Ver.")); 154 igText(puppet.meta.version_.toStringz); 155 igSameLine(0, 0); 156 incDummy(ImVec2(-(len.x), len.y)); 157 igSameLine(0, 0); 158 igText(__("Inochi2D Ver.")); 159 } 160 161 igSpacing(); 162 igSpacing(); 163 164 igText(__("General Info")); 165 igSeparator(); 166 167 igPushID("Name"); 168 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Name")); 169 incTooltip(_("Name of the puppet")); 170 incInputText("", puppet.meta.name); 171 igPopID(); 172 igSpacing(); 173 174 igPushID("Artists"); 175 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Artist(s)")); 176 incTooltip(_("Artists who've drawn the puppet, seperated by comma")); 177 incInputText("", puppet.meta.artist); 178 igPopID(); 179 igSpacing(); 180 181 igPushID("Riggers"); 182 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Rigger(s)")); 183 incTooltip(_("Riggers who've rigged the puppet, seperated by comma")); 184 incInputText("", puppet.meta.rigger); 185 igPopID(); 186 igSpacing(); 187 188 igPushID("Contact"); 189 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Contact")); 190 incTooltip(_("Where to contact the main author of the puppet")); 191 incInputText("", puppet.meta.contact); 192 igPopID(); 193 igSpacing(); 194 195 igText(__("License Info")); 196 igSeparator(); 197 198 igPushID("LicenseURL"); 199 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("License URL")); 200 incTooltip(_("Link/URL to license")); 201 incInputText("", puppet.meta.licenseURL); 202 igPopID(); 203 igSpacing(); 204 205 igPushID("Copyright"); 206 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Copyright")); 207 incTooltip(_("Copyright holder information of the puppet")); 208 incInputText("", puppet.meta.copyright); 209 igPopID(); 210 igSpacing(); 211 212 igPushID("Origin"); 213 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Origin")); 214 incTooltip(_("Where the model comes from on the internet.")); 215 incInputText("", puppet.meta.reference); 216 igPopID(); 217 } 218 219 void incModelModeHeader(Node node) { 220 // Top level 221 igPushID(node.uuid); 222 string typeString = "%s\0".format(incTypeIdToIcon(node.typeId())); 223 auto len = incMeasureString(typeString); 224 incInputText("", node.name); 225 igSameLine(0, 0); 226 incDummy(ImVec2(-(len.x-14), len.y)); 227 igSameLine(0, 0); 228 igText(typeString.ptr); 229 igPopID(); 230 igSeparator(); 231 } 232 233 void incInspectorModelTRS(Node node) { 234 float adjustSpeed = 1; 235 // if (igIsKeyDown(igGetKeyIndex(ImGuiKeyModFlags_Shift))) { 236 // adjustSpeed = 0.1; 237 // } 238 239 ImVec2 avail; 240 igGetContentRegionAvail(&avail); 241 242 float fontSize = 16; 243 244 // 245 // Translation 246 // 247 248 // Translation portion of the transformation matrix. 249 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Translation")); 250 igPushItemWidth((avail.x-4f-(fontSize*3f))/3f); 251 252 // Translation X 253 igPushID(0); 254 if (incDragFloat("translation_x", &node.localTransform.translation.vector[0], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 255 incActionPush( 256 new NodeValueChangeAction!(Node, float)( 257 "X", 258 node, 259 incGetDragFloatInitialValue("translation_x"), 260 node.localTransform.translation.vector[0], 261 &node.localTransform.translation.vector[0] 262 ) 263 ); 264 } 265 igPopID(); 266 267 if (incLockButton(&node.localTransform.lockTranslationX, "tra_x")) { 268 incActionPush( 269 new NodeValueChangeAction!(Node, bool)( 270 _("Lock Translate X"), 271 node, 272 !node.localTransform.lockTranslationX, 273 node.localTransform.lockTranslationX, 274 &node.localTransform.lockTranslationX 275 ) 276 ); 277 } 278 279 igSameLine(0, 4); 280 281 // Translation Y 282 igPushID(1); 283 if (incDragFloat("translation_y", &node.localTransform.translation.vector[1], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 284 incActionPush( 285 new NodeValueChangeAction!(Node, float)( 286 "Y", 287 node, 288 incGetDragFloatInitialValue("translation_y"), 289 node.localTransform.translation.vector[1], 290 &node.localTransform.translation.vector[1] 291 ) 292 ); 293 } 294 igPopID(); 295 296 if (incLockButton(&node.localTransform.lockTranslationY, "tra_y")) { 297 incActionPush( 298 new NodeValueChangeAction!(Node, bool, )( 299 _("Lock Translate Y"), 300 node, 301 !node.localTransform.lockTranslationY, 302 node.localTransform.lockTranslationY, 303 &node.localTransform.lockTranslationY 304 ) 305 ); 306 } 307 308 igSameLine(0, 4); 309 310 // Translation Z 311 igPushID(2); 312 if (incDragFloat("translation_z", &node.localTransform.translation.vector[2], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 313 incActionPush( 314 new NodeValueChangeAction!(Node, float)( 315 "Z", 316 node, 317 incGetDragFloatInitialValue("translation_z"), 318 node.localTransform.translation.vector[2], 319 &node.localTransform.translation.vector[2] 320 ) 321 ); 322 } 323 igPopID(); 324 325 if (incLockButton(&node.localTransform.lockTranslationZ, "tra_z")) { 326 incActionPush( 327 new NodeValueChangeAction!(Node, bool)( 328 _("Lock Translate Z"), 329 node, 330 !node.localTransform.lockTranslationZ, 331 node.localTransform.lockTranslationZ, 332 &node.localTransform.lockTranslationZ 333 ) 334 ); 335 } 336 337 igPopItemWidth(); 338 339 340 // 341 // Rotation 342 // 343 igSpacing(); 344 345 // Rotation portion of the transformation matrix. 346 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Rotation")); 347 igPushItemWidth((avail.x-4f-(fontSize*3f))/3f); 348 349 // Rotation X 350 igPushID(3); 351 if (incDragFloat("rotation_x", &node.localTransform.rotation.vector[0], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 352 incActionPush( 353 new NodeValueChangeAction!(Node, float)( 354 _("Rotation X"), 355 node, 356 incGetDragFloatInitialValue("rotation_x"), 357 node.localTransform.rotation.vector[0], 358 &node.localTransform.rotation.vector[0] 359 ) 360 ); 361 } 362 igPopID(); 363 364 if (incLockButton(&node.localTransform.lockRotationX, "rot_x")) { 365 incActionPush( 366 new NodeValueChangeAction!(Node, bool)( 367 _("Lock Rotation X"), 368 node, 369 !node.localTransform.lockRotationX, 370 node.localTransform.lockRotationX, 371 &node.localTransform.lockRotationX 372 ) 373 ); 374 } 375 376 igSameLine(0, 4); 377 378 // Rotation Y 379 igPushID(4); 380 if (incDragFloat("rotation_y", &node.localTransform.rotation.vector[1], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 381 incActionPush( 382 new NodeValueChangeAction!(Node, float)( 383 _("Rotation Y"), 384 node, 385 incGetDragFloatInitialValue("rotation_y"), 386 node.localTransform.rotation.vector[1], 387 &node.localTransform.rotation.vector[1] 388 ) 389 ); 390 } 391 igPopID(); 392 393 if (incLockButton(&node.localTransform.lockRotationY, "rot_y")) { 394 incActionPush( 395 new NodeValueChangeAction!(Node, bool)( 396 _("Lock Rotation Y"), 397 node, 398 !node.localTransform.lockRotationY, 399 node.localTransform.lockRotationY, 400 &node.localTransform.lockRotationY 401 ) 402 ); 403 } 404 405 igSameLine(0, 4); 406 407 // Rotation Z 408 igPushID(5); 409 if (incDragFloat("rotation_z", &node.localTransform.rotation.vector[2], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 410 incActionPush( 411 new NodeValueChangeAction!(Node, float)( 412 _("Rotation Z"), 413 node, 414 incGetDragFloatInitialValue("rotation_z"), 415 node.localTransform.rotation.vector[2], 416 &node.localTransform.rotation.vector[2] 417 ) 418 ); 419 } 420 igPopID(); 421 422 if (incLockButton(&node.localTransform.lockRotationZ, "rot_z")) { 423 incActionPush( 424 new NodeValueChangeAction!(Node, bool)( 425 _("Lock Rotation Z"), 426 node, 427 !node.localTransform.lockRotationZ, 428 node.localTransform.lockRotationZ, 429 &node.localTransform.lockRotationZ 430 ) 431 ); 432 } 433 434 igPopItemWidth(); 435 436 avail.x += igGetFontSize(); 437 438 // 439 // Scaling 440 // 441 igSpacing(); 442 443 // Scaling portion of the transformation matrix. 444 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Scale")); 445 igPushItemWidth((avail.x-14f-(fontSize*2f))/2f); 446 447 // Scale X 448 igPushID(6); 449 if (incDragFloat("scale_x", &node.localTransform.scale.vector[0], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 450 incActionPush( 451 new NodeValueChangeAction!(Node, float)( 452 _("Scale X"), 453 node, 454 incGetDragFloatInitialValue("scale_x"), 455 node.localTransform.scale.vector[0], 456 &node.localTransform.scale.vector[0] 457 ) 458 ); 459 } 460 igPopID(); 461 if (incLockButton(&node.localTransform.lockScaleX, "sca_x")) { 462 incActionPush( 463 new NodeValueChangeAction!(Node, bool)( 464 _("Lock Scale X"), 465 node, 466 !node.localTransform.lockScaleX, 467 node.localTransform.lockScaleX, 468 &node.localTransform.lockScaleX 469 ) 470 ); 471 } 472 473 igSameLine(0, 4); 474 475 // Scale Y 476 igPushID(7); 477 if (incDragFloat("scale_y", &node.localTransform.scale.vector[1], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 478 incActionPush( 479 new NodeValueChangeAction!(Node, float)( 480 _("Scale Y"), 481 node, 482 incGetDragFloatInitialValue("scale_y"), 483 node.localTransform.scale.vector[1], 484 &node.localTransform.scale.vector[1] 485 ) 486 ); 487 } 488 igPopID(); 489 if (incLockButton(&node.localTransform.lockScaleY, "sca_y")) { 490 incActionPush( 491 new NodeValueChangeAction!(Node, bool)( 492 _("Lock Scale Y"), 493 node, 494 !node.localTransform.lockScaleY, 495 node.localTransform.lockScaleY, 496 &node.localTransform.lockScaleY 497 ) 498 ); 499 } 500 501 igPopItemWidth(); 502 503 igSpacing(); 504 igSpacing(); 505 506 // An option in which positions will be snapped to whole integer values. 507 // In other words texture will always be on a pixel. 508 ImVec2 textLength = incMeasureString(_("Snap to Pixel")); 509 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Snap to Pixel")); 510 incSpacer(ImVec2(-12, 1)); 511 if (incLockButton(&node.localTransform.pixelSnap, "pix_lk")) { 512 incActionPush( 513 new NodeValueChangeAction!(Node, bool)( 514 _("Snap to Pixel"), 515 node, 516 !node.localTransform.pixelSnap, 517 node.localTransform.pixelSnap, 518 &node.localTransform.pixelSnap 519 ) 520 ); 521 } 522 523 // Padding 524 igSpacing(); 525 igSpacing(); 526 } 527 528 void incInspectorModelDrawable(Drawable node) { 529 // The main type of anything that can be drawn to the screen 530 // in Inochi2D. 531 igText(__("Drawable")); 532 igSeparator(); 533 534 igPushStyleVar_Vec2(ImGuiStyleVar.FramePadding, ImVec2(8, 8)); 535 igSpacing(); 536 igSpacing(); 537 538 if (igButton("")) { 539 incSetEditMode(EditMode.VertexEdit); 540 incSelectNode(node); 541 incFocusCamera(node); 542 incMeshEditSetTarget(node); 543 } 544 545 // Switches Inochi Creator over to Mesh Edit mode 546 // and selects the mesh that you had selected previously 547 // in Model Edit mode. 548 incTooltip(_("Edit Mesh")); 549 550 igSpacing(); 551 igSpacing(); 552 igPopStyleVar(); 553 } 554 555 void incInspectorModelPart(Part node) { 556 if (!node.getMesh().isReady()) { 557 igText(__("Part")); 558 igSeparator(); 559 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Cannot inspect an unmeshed part")); 560 return; 561 } 562 563 igText(__("Part")); 564 igSeparator(); 565 566 // BLENDING MODE 567 import std.conv : text; 568 import std.string : toStringz; 569 570 // Header for the Blending options for Parts 571 igText(__("Blending")); 572 if (igBeginCombo("###Blending", __(node.blendingMode.text))) { 573 574 // Normal blending mode as used in Photoshop, generally 575 // the default blending mode photoshop starts a layer out as. 576 if (igSelectable(__("Normal"), node.blendingMode == BlendMode.Normal)) node.blendingMode = BlendMode.Normal; 577 578 // Multiply blending mode, in which this texture's color data 579 // will be multiplied with the color data already in the framebuffer. 580 if (igSelectable(__("Multiply"), node.blendingMode == BlendMode.Multiply)) node.blendingMode = BlendMode.Multiply; 581 582 igEndCombo(); 583 } 584 585 igSpacing(); 586 587 igText(__("Opacity")); 588 igSliderFloat("###Opacity", &node.opacity, 0, 1f, "%0.2f"); 589 igSpacing(); 590 igSpacing(); 591 592 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Masks")); 593 igSpacing(); 594 595 // The masking mode to apply if there's any source specified. 596 igText(__("Mode")); 597 if (igBeginCombo("###Mode", node.maskingMode ? __("Dodge") : __("Mask"))) { 598 599 // A masking mode where only areas of this Part that overlap the other 600 // source drawables gets drawn 601 if (igSelectable(__("Mask"), node.maskingMode == MaskingMode.Mask)) { 602 node.maskingMode = MaskingMode.Mask; 603 } 604 605 // A masking mode where areas of this Part that overlap the other 606 // source drawables gets discarded 607 if (igSelectable(__("Dodge"), node.maskingMode == MaskingMode.DodgeMask)) { 608 node.maskingMode = MaskingMode.DodgeMask; 609 } 610 igEndCombo(); 611 } 612 613 igSpacing(); 614 615 // Threshold slider name for adjusting how transparent a pixel can be 616 // before it gets discarded. 617 igText(__("Threshold")); 618 igSliderFloat("###Threshold", &node.maskAlphaThreshold, 0.0, 1.0, "%.2f"); 619 620 igSpacing(); 621 622 // The sources that the part gets masked by. Depending on the masking mode 623 // either the sources will cut out things that don't overlap, or cut out 624 // things that do. 625 igText(__("Mask Sources")); 626 if (igBeginListBox("###MaskSources", ImVec2(0, 128))) { 627 foreach(i, masker; node.mask) { 628 igPushID(cast(int)i); 629 igText(masker.name.toStringz); 630 if(igBeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) { 631 igSetDragDropPayload("_MASKITEM", cast(void*)&masker, (&masker).sizeof, ImGuiCond.Always); 632 igText(masker.name.toStringz); 633 igEndDragDropSource(); 634 } 635 igPopID(); 636 } 637 igEndListBox(); 638 } 639 640 if(igBeginDragDropTarget()) { 641 ImGuiPayload* payload = igAcceptDragDropPayload("_PUPPETNTREE"); 642 if (payload !is null) { 643 if (Drawable payloadDrawable = cast(Drawable)*cast(Node*)payload.Data) { 644 645 // Make sure we don't mask against ourselves as well as don't double mask 646 if (payloadDrawable != node && !node.mask.canFind(payloadDrawable)) { 647 node.mask ~= payloadDrawable; 648 } 649 } 650 } 651 652 igEndDragDropTarget(); 653 } 654 655 igButton("ー", ImVec2(0, 0)); 656 if(igBeginDragDropTarget()) { 657 ImGuiPayload* payload = igAcceptDragDropPayload("_MASKITEM"); 658 if (payload !is null) { 659 if (Drawable payloadDrawable = cast(Drawable)*cast(Node*)payload.Data) { 660 foreach(i; 0..node.mask.length) { 661 if (payloadDrawable.uuid == node.mask[i].uuid) { 662 node.mask = node.mask.remove(i); 663 break; 664 } 665 } 666 } 667 } 668 igEndDragDropTarget(); 669 } 670 671 // Padding 672 igSpacing(); 673 igSpacing(); 674 } 675 676 // 677 // MESH EDIT MODE 678 // 679 void incInspectorMeshEditDrawable(Drawable node) { 680 igText(__("Drawable")); 681 igSeparator(); 682 683 igPushStyleVar_Vec2(ImGuiStyleVar.FramePadding, ImVec2(8, 8)); 684 igSpacing(); 685 igSpacing(); 686 687 igBeginDisabled(!incMeshEditCanTriangulate()); 688 if (igButton(__("Triangulate"))) { 689 incMeshEditDbg(); 690 incMeshEditDbg(); 691 } 692 incTooltip(_("Automatically connects vertices")); 693 igEndDisabled(); 694 695 696 igBeginDisabled(!incMeshEditCanApply()); 697 if (igButton("")) { 698 // incSetEditMode(EditMode.ModelEdit); 699 // incSelectNode(node); 700 // incFocusCamera(node); 701 incMeshEditApply(); 702 } 703 incTooltip(_("Apply")); 704 igEndDisabled(); 705 706 igSameLine(0, 4); 707 708 if (igButton("")) { 709 // incSetEditMode(EditMode.ModelEdit); 710 // incSelectNode(node); 711 // incFocusCamera(node); 712 incMeshEditReset(); 713 } 714 incTooltip(_("Cancel")); 715 716 igSpacing(); 717 igSpacing(); 718 igPopStyleVar(); 719 }