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.viewport.vertex; 9 import creator.core; 10 import creator.panels; 11 import creator.widgets; 12 import creator.utils; 13 import creator.windows; 14 import creator; 15 import inochi2d; 16 import inochi2d.core.nodes.common; 17 import std.string; 18 import std.algorithm.searching; 19 import std.algorithm.mutation; 20 import std.conv; 21 import i18n; 22 23 import creator.actions.node; 24 25 /** 26 The inspector panel 27 */ 28 class InspectorPanel : Panel { 29 private: 30 31 32 protected: 33 override 34 void onUpdate() { 35 auto nodes = incSelectedNodes(); 36 if (nodes.length == 1) { 37 Node node = nodes[0]; 38 if (node !is null && node != incActivePuppet().root) { 39 40 // Per-edit mode inspector drawers 41 switch(incEditMode()) { 42 case EditMode.ModelEdit: 43 if (incArmedParameter()) { 44 Parameter param = incArmedParameter(); 45 vec2u cursor = param.findClosestKeypoint(); 46 incCommonNonEditHeader(node); 47 incInspectorDeformTRS(node, param, cursor); 48 49 // Node Part Section 50 if (Part part = cast(Part)node) { 51 52 // Padding 53 igSpacing(); 54 igSpacing(); 55 igSpacing(); 56 igSpacing(); 57 incInspectorDeformPart(part, param, cursor); 58 } 59 60 if (Composite composite = cast(Composite)node) { 61 62 // Padding 63 igSpacing(); 64 igSpacing(); 65 igSpacing(); 66 igSpacing(); 67 incInspectorDeformComposite(composite, param, cursor); 68 } 69 70 } else { 71 incModelModeHeader(node); 72 incInspectorModelTRS(node); 73 74 // Node Drawable Section 75 if (Composite composite = cast(Composite)node) { 76 77 // Padding 78 igSpacing(); 79 igSpacing(); 80 igSpacing(); 81 igSpacing(); 82 incInspectorModelComposite(composite); 83 } 84 85 86 // Node Drawable Section 87 if (Drawable drawable = cast(Drawable)node) { 88 89 // Padding 90 igSpacing(); 91 igSpacing(); 92 igSpacing(); 93 igSpacing(); 94 incInspectorModelDrawable(drawable); 95 } 96 97 // Node Part Section 98 if (Part part = cast(Part)node) { 99 100 // Padding 101 igSpacing(); 102 igSpacing(); 103 igSpacing(); 104 igSpacing(); 105 incInspectorModelPart(part); 106 } 107 108 // Node SimplePhysics Section 109 if (SimplePhysics part = cast(SimplePhysics)node) { 110 111 // Padding 112 igSpacing(); 113 igSpacing(); 114 igSpacing(); 115 igSpacing(); 116 incInspectorModelSimplePhysics(part); 117 } 118 } 119 120 break; 121 case EditMode.VertexEdit: 122 incCommonNonEditHeader(node); 123 incInspectorMeshEditDrawable(cast(Drawable)node); 124 break; 125 default: 126 incCommonNonEditHeader(node); 127 break; 128 } 129 } else incInspectorModelInfo(); 130 } else if (nodes.length == 0) { 131 igText(__("No nodes selected...")); 132 } else { 133 igText(__("Can only inspect a single node...")); 134 } 135 } 136 137 public: 138 this() { 139 super("Inspector", _("Inspector"), true); 140 } 141 } 142 143 /** 144 Generate logger frame 145 */ 146 mixin incPanel!InspectorPanel; 147 148 149 150 private: 151 152 // 153 // COMMON 154 // 155 156 void incCommonNonEditHeader(Node node) { 157 // Top level 158 igPushID(node.uuid); 159 string typeString = "%s\0".format(incTypeIdToIcon(node.typeId())); 160 auto len = incMeasureString(typeString); 161 igText(node.name.toStringz); 162 igSameLine(0, 0); 163 incDummy(ImVec2(-(len.x-14), len.y)); 164 igSameLine(0, 0); 165 igText(typeString.ptr); 166 igPopID(); 167 igSeparator(); 168 } 169 170 // 171 // MODEL MODE 172 // 173 174 void incInspectorModelInfo() { 175 auto rootNode = incActivePuppet().root; 176 auto puppet = incActivePuppet(); 177 178 // Top level 179 igPushID(rootNode.uuid); 180 string typeString = "\0"; 181 auto len = incMeasureString(typeString); 182 igText(__("Puppet")); 183 igSameLine(0, 0); 184 incDummy(ImVec2(-(len.x-14), len.y)); 185 igSameLine(0, 0); 186 igText(typeString.ptr); 187 igPopID(); 188 igSeparator(); 189 190 igSpacing(); 191 igSpacing(); 192 193 // Version info 194 { 195 len = incMeasureString(_("Inochi2D Ver.")); 196 igText(puppet.meta.version_.toStringz); 197 igSameLine(0, 0); 198 incDummy(ImVec2(-(len.x), len.y)); 199 igSameLine(0, 0); 200 igText(__("Inochi2D Ver.")); 201 } 202 203 igSpacing(); 204 igSpacing(); 205 206 if (igCollapsingHeader(__("General Info"), ImGuiTreeNodeFlags.DefaultOpen)) { 207 igPushID("Name"); 208 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Name")); 209 incTooltip(_("Name of the puppet")); 210 incInputText("", puppet.meta.name); 211 igPopID(); 212 igSpacing(); 213 214 igPushID("Artists"); 215 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Artist(s)")); 216 incTooltip(_("Artists who've drawn the puppet, seperated by comma")); 217 incInputText("", puppet.meta.artist); 218 igPopID(); 219 igSpacing(); 220 221 igPushID("Riggers"); 222 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Rigger(s)")); 223 incTooltip(_("Riggers who've rigged the puppet, seperated by comma")); 224 incInputText("", puppet.meta.rigger); 225 igPopID(); 226 igSpacing(); 227 228 igPushID("Contact"); 229 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Contact")); 230 incTooltip(_("Where to contact the main author of the puppet")); 231 incInputText("", puppet.meta.contact); 232 igPopID(); 233 igSpacing(); 234 } 235 236 if (igCollapsingHeader(__("Licensing"), ImGuiTreeNodeFlags.DefaultOpen)) { 237 igPushID("LicenseURL"); 238 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("License URL")); 239 incTooltip(_("Link/URL to license")); 240 incInputText("", puppet.meta.licenseURL); 241 igPopID(); 242 igSpacing(); 243 244 igPushID("Copyright"); 245 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Copyright")); 246 incTooltip(_("Copyright holder information of the puppet")); 247 incInputText("", puppet.meta.copyright); 248 igPopID(); 249 igSpacing(); 250 251 igPushID("Origin"); 252 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Origin")); 253 incTooltip(_("Where the model comes from on the internet.")); 254 incInputText("", puppet.meta.reference); 255 igPopID(); 256 } 257 258 if (igCollapsingHeader(__("Physics Globals"), ImGuiTreeNodeFlags.DefaultOpen)) { 259 igPushID("PixelsPerMeter"); 260 igText(__("Pixels per meter")); 261 incTooltip(_("Number of pixels that correspond to 1 meter in the physics engine.")); 262 incDragFloat("PixelsPerMeter", &puppet.physics.pixelsPerMeter, 1, 1, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 263 igPopID(); 264 igSpacing(); 265 266 igPushID("Gravity"); 267 igText(__("Gravity")); 268 incTooltip(_("Acceleration due to gravity, in m/s². Earth gravity is 9.8.")); 269 incDragFloat("Gravity", &puppet.physics.gravity, 0.01, 0, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 270 igPopID(); 271 igSpacing(); 272 } 273 } 274 275 void incModelModeHeader(Node node) { 276 // Top level 277 igPushID(node.uuid); 278 string typeString = "%s\0".format(incTypeIdToIcon(node.typeId())); 279 auto len = incMeasureString(typeString); 280 incInputText("", incAvailableSpace().x-24, node.name); 281 igSameLine(0, 0); 282 incDummy(ImVec2(-(len.x-14), len.y)); 283 igSameLine(0, 0); 284 igText(typeString.ptr); 285 igPopID(); 286 igSeparator(); 287 } 288 289 void incInspectorModelTRS(Node node) { 290 if (!igCollapsingHeader(__("Transform"), ImGuiTreeNodeFlags.DefaultOpen)) 291 return; 292 293 float adjustSpeed = 1; 294 // if (igIsKeyDown(igGetKeyIndex(ImGuiKeyModFlags_Shift))) { 295 // adjustSpeed = 0.1; 296 // } 297 298 ImVec2 avail; 299 igGetContentRegionAvail(&avail); 300 301 float fontSize = 16; 302 303 // 304 // Translation 305 // 306 307 // Translation portion of the transformation matrix. 308 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Translation")); 309 igPushItemWidth((avail.x-4f)/3f); 310 311 // Translation X 312 igPushID(0); 313 if (incDragFloat("translation_x", &node.localTransform.translation.vector[0], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 314 incActionPush( 315 new NodeValueChangeAction!(Node, float)( 316 "X", 317 node, 318 incGetDragFloatInitialValue("translation_x"), 319 node.localTransform.translation.vector[0], 320 &node.localTransform.translation.vector[0] 321 ) 322 ); 323 } 324 igPopID(); 325 326 igSameLine(0, 4); 327 328 // Translation Y 329 igPushID(1); 330 if (incDragFloat("translation_y", &node.localTransform.translation.vector[1], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 331 incActionPush( 332 new NodeValueChangeAction!(Node, float)( 333 "Y", 334 node, 335 incGetDragFloatInitialValue("translation_y"), 336 node.localTransform.translation.vector[1], 337 &node.localTransform.translation.vector[1] 338 ) 339 ); 340 } 341 igPopID(); 342 343 igSameLine(0, 4); 344 345 // Translation Z 346 igPushID(2); 347 if (incDragFloat("translation_z", &node.localTransform.translation.vector[2], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 348 incActionPush( 349 new NodeValueChangeAction!(Node, float)( 350 "Z", 351 node, 352 incGetDragFloatInitialValue("translation_z"), 353 node.localTransform.translation.vector[2], 354 &node.localTransform.translation.vector[2] 355 ) 356 ); 357 } 358 igPopID(); 359 360 361 362 // Padding 363 igSpacing(); 364 igSpacing(); 365 366 igBeginGroup(); 367 // Button which locks all transformation to be based off the root node 368 // of the puppet, this more or less makes the item stay in place 369 // even if the parent moves. 370 ImVec2 textLength = incMeasureString(_("Lock to Root Node")); 371 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Lock to Root Node")); 372 373 incSpacer(ImVec2(-12, 1)); 374 bool lockToRoot = node.lockToRoot; 375 if (incLockButton(&lockToRoot, "root_lk")) { 376 377 // TODO: Store this in undo history. 378 node.lockToRoot = lockToRoot; 379 } 380 igEndGroup(); 381 382 // Button which locks all transformation to be based off the root node 383 // of the puppet, this more or less makes the item stay in place 384 // even if the parent moves. 385 incTooltip(_("Makes so that the translation of this node is based off the root node, making it stay in place even if its parent moves.")); 386 387 // Padding 388 igSpacing(); 389 igSpacing(); 390 391 igPopItemWidth(); 392 393 394 // 395 // Rotation 396 // 397 igSpacing(); 398 399 // Rotation portion of the transformation matrix. 400 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Rotation")); 401 igPushItemWidth((avail.x-4f-(fontSize*3f))/3f); 402 403 // Rotation X 404 igPushID(3); 405 if (incDragFloat("rotation_x", &node.localTransform.rotation.vector[0], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 406 incActionPush( 407 new NodeValueChangeAction!(Node, float)( 408 _("Rotation X"), 409 node, 410 incGetDragFloatInitialValue("rotation_x"), 411 node.localTransform.rotation.vector[0], 412 &node.localTransform.rotation.vector[0] 413 ) 414 ); 415 } 416 igPopID(); 417 418 if (incLockButton(&node.localTransform.lockRotationX, "rot_x")) { 419 incActionPush( 420 new NodeValueChangeAction!(Node, bool)( 421 _("Lock Rotation X"), 422 node, 423 !node.localTransform.lockRotationX, 424 node.localTransform.lockRotationX, 425 &node.localTransform.lockRotationX 426 ) 427 ); 428 } 429 430 igSameLine(0, 4); 431 432 // Rotation Y 433 igPushID(4); 434 if (incDragFloat("rotation_y", &node.localTransform.rotation.vector[1], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 435 incActionPush( 436 new NodeValueChangeAction!(Node, float)( 437 _("Rotation Y"), 438 node, 439 incGetDragFloatInitialValue("rotation_y"), 440 node.localTransform.rotation.vector[1], 441 &node.localTransform.rotation.vector[1] 442 ) 443 ); 444 } 445 igPopID(); 446 447 if (incLockButton(&node.localTransform.lockRotationY, "rot_y")) { 448 incActionPush( 449 new NodeValueChangeAction!(Node, bool)( 450 _("Lock Rotation Y"), 451 node, 452 !node.localTransform.lockRotationY, 453 node.localTransform.lockRotationY, 454 &node.localTransform.lockRotationY 455 ) 456 ); 457 } 458 459 igSameLine(0, 4); 460 461 // Rotation Z 462 igPushID(5); 463 if (incDragFloat("rotation_z", &node.localTransform.rotation.vector[2], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 464 incActionPush( 465 new NodeValueChangeAction!(Node, float)( 466 _("Rotation Z"), 467 node, 468 incGetDragFloatInitialValue("rotation_z"), 469 node.localTransform.rotation.vector[2], 470 &node.localTransform.rotation.vector[2] 471 ) 472 ); 473 } 474 igPopID(); 475 476 if (incLockButton(&node.localTransform.lockRotationZ, "rot_z")) { 477 incActionPush( 478 new NodeValueChangeAction!(Node, bool)( 479 _("Lock Rotation Z"), 480 node, 481 !node.localTransform.lockRotationZ, 482 node.localTransform.lockRotationZ, 483 &node.localTransform.lockRotationZ 484 ) 485 ); 486 } 487 488 igPopItemWidth(); 489 490 avail.x += igGetFontSize(); 491 492 // 493 // Scaling 494 // 495 igSpacing(); 496 497 // Scaling portion of the transformation matrix. 498 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Scale")); 499 igPushItemWidth((avail.x-14f-(fontSize*2f))/2f); 500 501 // Scale X 502 igPushID(6); 503 if (incDragFloat("scale_x", &node.localTransform.scale.vector[0], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 504 incActionPush( 505 new NodeValueChangeAction!(Node, float)( 506 _("Scale X"), 507 node, 508 incGetDragFloatInitialValue("scale_x"), 509 node.localTransform.scale.vector[0], 510 &node.localTransform.scale.vector[0] 511 ) 512 ); 513 } 514 igPopID(); 515 if (incLockButton(&node.localTransform.lockScaleX, "sca_x")) { 516 incActionPush( 517 new NodeValueChangeAction!(Node, bool)( 518 _("Lock Scale X"), 519 node, 520 !node.localTransform.lockScaleX, 521 node.localTransform.lockScaleX, 522 &node.localTransform.lockScaleX 523 ) 524 ); 525 } 526 527 igSameLine(0, 4); 528 529 // Scale Y 530 igPushID(7); 531 if (incDragFloat("scale_y", &node.localTransform.scale.vector[1], adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 532 incActionPush( 533 new NodeValueChangeAction!(Node, float)( 534 _("Scale Y"), 535 node, 536 incGetDragFloatInitialValue("scale_y"), 537 node.localTransform.scale.vector[1], 538 &node.localTransform.scale.vector[1] 539 ) 540 ); 541 } 542 igPopID(); 543 if (incLockButton(&node.localTransform.lockScaleY, "sca_y")) { 544 incActionPush( 545 new NodeValueChangeAction!(Node, bool)( 546 _("Lock Scale Y"), 547 node, 548 !node.localTransform.lockScaleY, 549 node.localTransform.lockScaleY, 550 &node.localTransform.lockScaleY 551 ) 552 ); 553 } 554 555 igPopItemWidth(); 556 557 igSpacing(); 558 igSpacing(); 559 560 // An option in which positions will be snapped to whole integer values. 561 // In other words texture will always be on a pixel. 562 textLength = incMeasureString(_("Snap to Pixel")); 563 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Snap to Pixel")); 564 incSpacer(ImVec2(-12, 1)); 565 if (incLockButton(&node.localTransform.pixelSnap, "pix_lk")) { 566 incActionPush( 567 new NodeValueChangeAction!(Node, bool)( 568 _("Snap to Pixel"), 569 node, 570 !node.localTransform.pixelSnap, 571 node.localTransform.pixelSnap, 572 &node.localTransform.pixelSnap 573 ) 574 ); 575 } 576 577 // Padding 578 igSpacing(); 579 igSpacing(); 580 581 // The sorting order ID, which Inochi2D uses to sort 582 // Parts to draw in the user specified order. 583 // negative values = closer to camera 584 // positive values = further away from camera 585 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Sorting")); 586 float zsortV = node.relZSort; 587 if (igInputFloat("###ZSort", &zsortV, 0.01, 0.05, "%0.2f")) { 588 node.zSort = zsortV; 589 } 590 } 591 592 void incInspectorModelDrawable(Drawable node) { 593 // The main type of anything that can be drawn to the screen 594 // in Inochi2D. 595 if (!igCollapsingHeader(__("Drawable"), ImGuiTreeNodeFlags.DefaultOpen)) 596 return; 597 598 float adjustSpeed = 1; 599 ImVec2 avail = incAvailableSpace(); 600 601 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Texture Offset")); 602 igPushItemWidth((avail.x-4f)/2f); 603 604 // Translation X 605 igPushID(24); 606 if (incDragFloat("offset_x", &node.getMesh().origin.vector[0], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 607 incActionPush( 608 new NodeValueChangeAction!(Node, float)( 609 "X", 610 node, 611 incGetDragFloatInitialValue("offset_x"), 612 node.getMesh().origin.vector[0], 613 &node.getMesh().origin.vector[0] 614 ) 615 ); 616 } 617 igPopID(); 618 619 igSameLine(0, 4); 620 621 // Translation Y 622 igPushID(25); 623 if (incDragFloat("offset_y", &node.getMesh().origin.vector[1], adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 624 incActionPush( 625 new NodeValueChangeAction!(Node, float)( 626 "Y", 627 node, 628 incGetDragFloatInitialValue("offset_y"), 629 node.getMesh().origin.vector[1], 630 &node.getMesh().origin.vector[1] 631 ) 632 ); 633 } 634 igPopID(); 635 igPopItemWidth(); 636 637 igPushStyleVar_Vec2(ImGuiStyleVar.FramePadding, ImVec2(8, 8)); 638 igSpacing(); 639 igSpacing(); 640 641 if (igButton("", ImVec2(avail.x, 32))) { 642 incSetEditMode(EditMode.VertexEdit); 643 incSelectNode(node); 644 incVertexEditSetTarget(node); 645 incFocusCamera(node, vec2(0, 0)); 646 } 647 648 // Allow copying mesh data via drag n drop for now 649 if(igBeginDragDropTarget()) { 650 ImGuiPayload* payload = igAcceptDragDropPayload("_PUPPETNTREE"); 651 if (payload !is null) { 652 if (Drawable payloadDrawable = cast(Drawable)*cast(Node*)payload.Data) { 653 incSetEditMode(EditMode.VertexEdit); 654 incSelectNode(node); 655 incVertexEditSetTarget(node); 656 incFocusCamera(node, vec2(0, 0)); 657 incVertexEditCopyMeshDataToTarget(payloadDrawable.getMesh()); 658 } 659 } 660 661 igEndDragDropTarget(); 662 } else { 663 664 665 // Switches Inochi Creator over to Mesh Edit mode 666 // and selects the mesh that you had selected previously 667 // in Model Edit mode. 668 incTooltip(_("Edit Mesh")); 669 } 670 671 igSpacing(); 672 igSpacing(); 673 igPopStyleVar(); 674 } 675 676 void incInspectorModelPart(Part node) { 677 if (!igCollapsingHeader(__("Part"), ImGuiTreeNodeFlags.DefaultOpen)) 678 return; 679 680 if (!node.getMesh().isReady()) { 681 igSpacing(); 682 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Cannot inspect an unmeshed part")); 683 return; 684 } 685 igSpacing(); 686 687 // BLENDING MODE 688 import std.conv : text; 689 import std.string : toStringz; 690 691 igBeginGroup(); 692 igIndent(16); 693 // Header for texture options 694 if (igCollapsingHeader(__("Textures"))) { 695 696 igText("(TODO: Texture Select)"); 697 698 igSpacing(); 699 igSpacing(); 700 701 igText(__("Tint")); 702 igColorEdit3("", cast(float[3]*)node.tint.value_ptr); 703 704 // Padding 705 igSeparator(); 706 igSpacing(); 707 igSpacing(); 708 } 709 igUnindent(); 710 igEndGroup(); 711 712 // Header for the Blending options for Parts 713 igText(__("Blending")); 714 if (igBeginCombo("###Blending", __(node.blendingMode.text))) { 715 716 // Normal blending mode as used in Photoshop, generally 717 // the default blending mode photoshop starts a layer out as. 718 if (igSelectable(__("Normal"), node.blendingMode == BlendMode.Normal)) node.blendingMode = BlendMode.Normal; 719 720 // Multiply blending mode, in which this texture's color data 721 // will be multiplied with the color data already in the framebuffer. 722 if (igSelectable(__("Multiply"), node.blendingMode == BlendMode.Multiply)) node.blendingMode = BlendMode.Multiply; 723 724 // Color Dodge blending mode 725 if (igSelectable(__("Color Dodge"), node.blendingMode == BlendMode.ColorDodge)) node.blendingMode = BlendMode.ColorDodge; 726 727 // Linear Dodge blending mode 728 if (igSelectable(__("Linear Dodge"), node.blendingMode == BlendMode.LinearDodge)) node.blendingMode = BlendMode.LinearDodge; 729 730 // Screen blending mode 731 if (igSelectable(__("Screen"), node.blendingMode == BlendMode.Screen)) node.blendingMode = BlendMode.Screen; 732 733 // Clip to Lower blending mode 734 if (igSelectable(__("Clip to Lower"), node.blendingMode == BlendMode.ClipToLower)) node.blendingMode = BlendMode.ClipToLower; 735 incTooltip(_("Special blending mode that causes (while respecting transparency) the part to be clipped to everything underneath")); 736 737 // Slice from Lower blending mode 738 if (igSelectable(__("Slice from Lower"), node.blendingMode == BlendMode.SliceFromLower)) node.blendingMode = BlendMode.SliceFromLower; 739 incTooltip(_("Special blending mode that causes (while respecting transparency) the part to be slice by everything underneath.\nBasically reverse Clip to Lower.")); 740 741 igEndCombo(); 742 } 743 744 igSpacing(); 745 746 igText(__("Opacity")); 747 igSliderFloat("###Opacity", &node.opacity, 0, 1f, "%0.2f"); 748 igSpacing(); 749 igSpacing(); 750 751 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Masks")); 752 igSpacing(); 753 754 // Threshold slider name for adjusting how transparent a pixel can be 755 // before it gets discarded. 756 igText(__("Threshold")); 757 igSliderFloat("###Threshold", &node.maskAlphaThreshold, 0.0, 1.0, "%.2f"); 758 759 igSpacing(); 760 761 // The sources that the part gets masked by. Depending on the masking mode 762 // either the sources will cut out things that don't overlap, or cut out 763 // things that do. 764 igText(__("Mask Sources")); 765 if (igBeginListBox("###MaskSources", ImVec2(0, 128))) { 766 foreach(i; 0..node.masks.length) { 767 MaskBinding* masker = &node.masks[i]; 768 igPushID(cast(int)i); 769 if (igBeginPopup("###MaskSettings")) { 770 if (igBeginMenu(__("Mode"))) { 771 if (igMenuItem(__("Mask"), null, masker.mode == MaskingMode.Mask)) masker.mode = MaskingMode.Mask; 772 if (igMenuItem(__("Dodge"), null, masker.mode == MaskingMode.DodgeMask)) masker.mode = MaskingMode.DodgeMask; 773 774 igEndMenu(); 775 } 776 777 if (igMenuItem(__("Delete"))) { 778 import std.algorithm.mutation : remove; 779 node.masks = node.masks.remove(i); 780 igEndPopup(); 781 igPopID(); 782 igEndListBox(); 783 return; 784 } 785 786 igEndPopup(); 787 } 788 789 if (masker.mode == MaskingMode.Mask) igSelectable(_("%s (Mask)").format(masker.maskSrc.name).toStringz); 790 else igSelectable(_("%s (Dodge)").format(masker.maskSrc.name).toStringz); 791 792 793 if(igBeginDragDropTarget()) { 794 ImGuiPayload* payload = igAcceptDragDropPayload("_MASKITEM"); 795 if (payload !is null) { 796 if (MaskBinding* binding = cast(MaskBinding*)payload.Data) { 797 ptrdiff_t maskIdx = node.getMaskIdx(binding.maskSrcUUID); 798 if (maskIdx >= 0) { 799 import std.algorithm.mutation : remove; 800 801 node.masks = node.masks.remove(maskIdx); 802 if (i == 0) node.masks = *binding ~ node.masks; 803 else if (i >= node.masks.length-1) node.masks ~= *binding; 804 else node.masks = node.masks[0..i] ~ *binding ~ node.masks[i+1..$]; 805 } 806 } 807 } 808 809 igEndDragDropTarget(); 810 } 811 812 // TODO: We really should account for left vs. right handedness 813 if (igIsItemClicked(ImGuiMouseButton.Right)) { 814 igOpenPopup("###MaskSettings"); 815 } 816 817 if(igBeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) { 818 igSetDragDropPayload("_MASKITEM", cast(void*)masker, MaskBinding.sizeof, ImGuiCond.Always); 819 igText(masker.maskSrc.name.toStringz); 820 igEndDragDropSource(); 821 } 822 igPopID(); 823 } 824 igEndListBox(); 825 } 826 827 if(igBeginDragDropTarget()) { 828 ImGuiPayload* payload = igAcceptDragDropPayload("_PUPPETNTREE"); 829 if (payload !is null) { 830 if (Drawable payloadDrawable = cast(Drawable)*cast(Node*)payload.Data) { 831 832 // Make sure we don't mask against ourselves as well as don't double mask 833 if (payloadDrawable != node && !node.isMaskedBy(payloadDrawable)) { 834 node.masks ~= MaskBinding(payloadDrawable.uuid, MaskingMode.Mask, payloadDrawable); 835 } 836 } 837 } 838 839 igEndDragDropTarget(); 840 } 841 842 // Padding 843 igSpacing(); 844 igSpacing(); 845 } 846 847 void incInspectorModelComposite(Composite node) { 848 if (!igCollapsingHeader(__("Composite"), ImGuiTreeNodeFlags.DefaultOpen)) 849 return; 850 851 852 igSpacing(); 853 854 // BLENDING MODE 855 import std.conv : text; 856 import std.string : toStringz; 857 858 859 igText(__("Tint")); 860 igColorEdit3("", cast(float[3]*)node.tint.value_ptr); 861 862 // Header for the Blending options for Parts 863 igText(__("Blending")); 864 if (igBeginCombo("###Blending", __(node.blendingMode.text))) { 865 866 // Normal blending mode as used in Photoshop, generally 867 // the default blending mode photoshop starts a layer out as. 868 if (igSelectable(__("Normal"), node.blendingMode == BlendMode.Normal)) node.blendingMode = BlendMode.Normal; 869 870 // Multiply blending mode, in which this texture's color data 871 // will be multiplied with the color data already in the framebuffer. 872 if (igSelectable(__("Multiply"), node.blendingMode == BlendMode.Multiply)) node.blendingMode = BlendMode.Multiply; 873 874 // Color Dodge blending mode 875 if (igSelectable(__("Color Dodge"), node.blendingMode == BlendMode.ColorDodge)) node.blendingMode = BlendMode.ColorDodge; 876 877 // Linear Dodge blending mode 878 if (igSelectable(__("Linear Dodge"), node.blendingMode == BlendMode.LinearDodge)) node.blendingMode = BlendMode.LinearDodge; 879 880 // Screen blending mode 881 if (igSelectable(__("Screen"), node.blendingMode == BlendMode.Screen)) node.blendingMode = BlendMode.Screen; 882 883 igEndCombo(); 884 } 885 886 igSpacing(); 887 888 igText(__("Opacity")); 889 igSliderFloat("###Opacity", &node.opacity, 0, 1f, "%0.2f"); 890 igSpacing(); 891 igSpacing(); 892 893 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Masks")); 894 igSpacing(); 895 896 // Threshold slider name for adjusting how transparent a pixel can be 897 // before it gets discarded. 898 igText(__("Threshold")); 899 igSliderFloat("###Threshold", &node.threshold, 0.0, 1.0, "%.2f"); 900 901 igSpacing(); 902 903 // Padding 904 igSpacing(); 905 igSpacing(); 906 } 907 908 void incInspectorModelSimplePhysics(SimplePhysics node) { 909 if (!igCollapsingHeader(__("SimplePhysics"), ImGuiTreeNodeFlags.DefaultOpen)) 910 return; 911 912 float adjustSpeed = 1; 913 914 igSpacing(); 915 916 // BLENDING MODE 917 import std.conv : text; 918 import std.string : toStringz; 919 920 igPushID("TargetParam"); 921 igText(__("Parameter")); 922 string paramName = _("(unassigned)"); 923 if (node.param !is null) paramName = node.param.name; 924 igInputText("", cast(char*)paramName.toStringz, paramName.length, ImGuiInputTextFlags.ReadOnly); 925 926 if(igBeginDragDropTarget()) { 927 ImGuiPayload* payload = igAcceptDragDropPayload("_PARAMETER"); 928 if (payload !is null) { 929 Parameter payloadParam = *cast(Parameter*)payload.Data; 930 node.param = payloadParam; 931 } 932 933 igEndDragDropTarget(); 934 } 935 936 igPopID(); 937 938 igText(__("Type")); 939 if (igBeginCombo("###PhysType", __(node.modelType.text))) { 940 941 if (igSelectable(__("Pendulum"), node.modelType == PhysicsModel.Pendulum)) node.modelType = PhysicsModel.Pendulum; 942 943 if (igSelectable(__("SpringPendulum"), node.modelType == PhysicsModel.SpringPendulum)) node.modelType = PhysicsModel.SpringPendulum; 944 945 igEndCombo(); 946 } 947 948 igSpacing(); 949 950 igText(__("Mapping mode")); 951 if (igBeginCombo("###PhysMapMode", __(node.mapMode.text))) { 952 953 if (igSelectable(__("AngleLength"), node.mapMode == ParamMapMode.AngleLength)) node.mapMode = ParamMapMode.AngleLength; 954 955 if (igSelectable(__("XY"), node.mapMode == ParamMapMode.XY)) node.mapMode = ParamMapMode.XY; 956 957 igEndCombo(); 958 } 959 960 igSpacing(); 961 962 igPushID("SimplePhysics"); 963 964 igPushID(0); 965 igText(__("Gravity scale")); 966 incDragFloat("gravity", &node.gravity, adjustSpeed/100, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 967 igSpacing(); 968 igSpacing(); 969 igPopID(); 970 971 igPushID(1); 972 igText(__("Length")); 973 incDragFloat("length", &node.length, adjustSpeed/100, 0, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 974 igSpacing(); 975 igSpacing(); 976 igPopID(); 977 978 igPushID(2); 979 igText(__("Resonant frequency")); 980 incDragFloat("frequency", &node.frequency, adjustSpeed/100, 0.01, 30, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 981 igSpacing(); 982 igSpacing(); 983 igPopID(); 984 985 igPushID(3); 986 igText(__("Damping")); 987 incDragFloat("damping_angle", &node.angleDamping, adjustSpeed/100, 0, 5, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 988 igPopID(); 989 990 igPushID(4); 991 incDragFloat("damping_length", &node.lengthDamping, adjustSpeed/100, 0, 5, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 992 igSpacing(); 993 igSpacing(); 994 igPopID(); 995 996 igPushID(5); 997 igText(__("Output scale")); 998 incDragFloat("output_scale.x", &node.outputScale.vector[0], adjustSpeed/100, 0, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 999 igPopID(); 1000 1001 igPushID(6); 1002 incDragFloat("output_scale.y", &node.outputScale.vector[1], adjustSpeed/100, 0, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat); 1003 igSpacing(); 1004 igSpacing(); 1005 igPopID(); 1006 1007 // Padding 1008 igSpacing(); 1009 igSpacing(); 1010 1011 igPopID(); 1012 } 1013 1014 // 1015 // MODEL MODE ARMED 1016 // 1017 void incInspectorDeformFloatDragVal(string name, string paramName, float adjustSpeed, Node node, Parameter param, vec2u cursor) { 1018 float currFloat = node.getDefaultValue(paramName); 1019 if (ValueParameterBinding b = cast(ValueParameterBinding)param.getBinding(node, paramName)) { 1020 currFloat = b.getValue(cursor); 1021 } 1022 if (incDragFloat(name, &currFloat, adjustSpeed, -float.max, float.max, "%.2f", ImGuiSliderFlags.NoRoundToFormat)) { 1023 ValueParameterBinding b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramName); 1024 b.setValue(cursor, currFloat); 1025 } 1026 } 1027 1028 void incInspectorDeformInputFloat(string name, string paramName, float step, float stepFast, Node node, Parameter param, vec2u cursor) { 1029 float currFloat = node.getDefaultValue(paramName); 1030 if (ValueParameterBinding b = cast(ValueParameterBinding)param.getBinding(node, paramName)) { 1031 currFloat = b.getValue(cursor); 1032 } 1033 if (igInputFloat(name.toStringz, &currFloat, step, stepFast, "%.2f")) { 1034 ValueParameterBinding b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramName); 1035 b.setValue(cursor, currFloat); 1036 } 1037 } 1038 1039 void incInspectorDeformColorEdit3(string[3] paramNames, Node node, Parameter param, vec2u cursor) { 1040 import std.math : isNaN; 1041 float[3] rgb = [float.nan, float.nan, float.nan]; 1042 float[3] rgbadj = [1, 1, 1]; 1043 bool[3] rgbchange = [false, false, false]; 1044 ValueParameterBinding pbr = cast(ValueParameterBinding)param.getBinding(node, paramNames[0]); 1045 ValueParameterBinding pbg = cast(ValueParameterBinding)param.getBinding(node, paramNames[1]); 1046 ValueParameterBinding pbb = cast(ValueParameterBinding)param.getBinding(node, paramNames[2]); 1047 1048 if (pbr) { 1049 rgb[0] = pbr.getValue(cursor); 1050 rgbadj[0] = rgb[0]; 1051 } 1052 1053 if (pbg) { 1054 rgb[1] = pbg.getValue(cursor); 1055 rgbadj[1] = rgb[1]; 1056 } 1057 1058 if (pbb) { 1059 rgb[2] = pbb.getValue(cursor); 1060 rgbadj[2] = rgb[2]; 1061 } 1062 1063 if (igColorEdit3("", &rgbadj)) { 1064 1065 // RED 1066 if (rgbadj[0] != 1) { 1067 auto b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramNames[0]); 1068 b.setValue(cursor, rgbadj[0]); 1069 } else if (pbr) { 1070 pbr.setValue(cursor, rgbadj[0]); 1071 } 1072 1073 // GREEN 1074 if (rgbadj[1] != 1) { 1075 auto b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramNames[1]); 1076 b.setValue(cursor, rgbadj[1]); 1077 } else if (pbg) { 1078 pbg.setValue(cursor, rgbadj[1]); 1079 } 1080 1081 // BLUE 1082 if (rgbadj[2] != 1) { 1083 auto b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramNames[2]); 1084 b.setValue(cursor, rgbadj[2]); 1085 } else if (pbb) { 1086 pbb.setValue(cursor, rgbadj[2]); 1087 } 1088 } 1089 } 1090 1091 void incInspectorDeformSliderFloat(string name, string paramName, float min, float max, Node node, Parameter param, vec2u cursor) { 1092 float currFloat = node.getDefaultValue(paramName); 1093 if (ValueParameterBinding b = cast(ValueParameterBinding)param.getBinding(node, paramName)) { 1094 currFloat = b.getValue(cursor); 1095 } 1096 if (igSliderFloat(name.toStringz, &currFloat, min, max, "%.2f")) { 1097 ValueParameterBinding b = cast(ValueParameterBinding)param.getOrAddBinding(node, paramName); 1098 b.setValue(cursor, currFloat); 1099 } 1100 } 1101 1102 void incInspectorDeformTRS(Node node, Parameter param, vec2u cursor) { 1103 if (!igCollapsingHeader(__("Transform"), ImGuiTreeNodeFlags.DefaultOpen)) 1104 return; 1105 1106 float adjustSpeed = 1; 1107 1108 ImVec2 avail; 1109 igGetContentRegionAvail(&avail); 1110 1111 float fontSize = 16; 1112 1113 // 1114 // Translation 1115 // 1116 1117 1118 1119 // Translation portion of the transformation matrix. 1120 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Translation")); 1121 igPushItemWidth((avail.x-4f)/3f); 1122 1123 // Translation X 1124 igPushID(0); 1125 incInspectorDeformFloatDragVal("translation_x", "transform.t.x", 1f, node, param, cursor); 1126 igPopID(); 1127 1128 igSameLine(0, 4); 1129 1130 // Translation Y 1131 igPushID(1); 1132 incInspectorDeformFloatDragVal("translation_y", "transform.t.y", 1f, node, param, cursor); 1133 igPopID(); 1134 1135 igSameLine(0, 4); 1136 1137 // Translation Z 1138 igPushID(2); 1139 incInspectorDeformFloatDragVal("translation_z", "transform.t.z", 1f, node, param, cursor); 1140 igPopID(); 1141 1142 1143 1144 // Padding 1145 igSpacing(); 1146 igSpacing(); 1147 1148 igPopItemWidth(); 1149 1150 1151 // 1152 // Rotation 1153 // 1154 igSpacing(); 1155 1156 // Rotation portion of the transformation matrix. 1157 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Rotation")); 1158 igPushItemWidth((avail.x-4f)/3f); 1159 1160 // Rotation X 1161 igPushID(3); 1162 incInspectorDeformFloatDragVal("rotation.x", "transform.r.x", 0.05f, node, param, cursor); 1163 igPopID(); 1164 1165 igSameLine(0, 4); 1166 1167 // Rotation Y 1168 igPushID(4); 1169 incInspectorDeformFloatDragVal("rotation.y", "transform.r.y", 0.05f, node, param, cursor); 1170 igPopID(); 1171 1172 igSameLine(0, 4); 1173 1174 // Rotation Z 1175 igPushID(5); 1176 incInspectorDeformFloatDragVal("rotation.z", "transform.r.z", 0.05f, node, param, cursor); 1177 igPopID(); 1178 1179 igPopItemWidth(); 1180 1181 avail.x += igGetFontSize(); 1182 1183 // 1184 // Scaling 1185 // 1186 igSpacing(); 1187 1188 // Scaling portion of the transformation matrix. 1189 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Scale")); 1190 igPushItemWidth((avail.x-14f)/2f); 1191 1192 // Scale X 1193 igPushID(6); 1194 incInspectorDeformFloatDragVal("scale.x", "transform.s.x", 0.1f, node, param, cursor); 1195 igPopID(); 1196 1197 igSameLine(0, 4); 1198 1199 // Scale Y 1200 igPushID(7); 1201 incInspectorDeformFloatDragVal("scale.y", "transform.s.y", 0.1f, node, param, cursor); 1202 igPopID(); 1203 1204 igPopItemWidth(); 1205 1206 igSpacing(); 1207 igSpacing(); 1208 1209 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Sorting")); 1210 incInspectorDeformInputFloat("zSort", "zSort", 0.01, 0.05, node, param, cursor); 1211 } 1212 1213 void incInspectorDeformPart(Part node, Parameter param, vec2u cursor) { 1214 if (!igCollapsingHeader(__("Part"), ImGuiTreeNodeFlags.DefaultOpen)) 1215 return; 1216 1217 igBeginGroup(); 1218 igIndent(16); 1219 // Header for texture options 1220 if (igCollapsingHeader(__("Textures"))) { 1221 1222 igText(__("Tint")); 1223 1224 incInspectorDeformColorEdit3(["tint.r", "tint.g", "tint.b"], node, param, cursor); 1225 1226 // Padding 1227 igSeparator(); 1228 igSpacing(); 1229 igSpacing(); 1230 } 1231 igUnindent(); 1232 igEndGroup(); 1233 1234 igText(__("Opacity")); 1235 incInspectorDeformSliderFloat("###Opacity", "opacity", 0, 1f, node, param, cursor); 1236 igSpacing(); 1237 igSpacing(); 1238 1239 // Threshold slider name for adjusting how transparent a pixel can be 1240 // before it gets discarded. 1241 igText(__("Threshold")); 1242 incInspectorDeformSliderFloat("###Threshold", "alphaThreshold", 0.0, 1.0, node, param, cursor); 1243 } 1244 1245 void incInspectorDeformComposite(Composite node, Parameter param, vec2u cursor) { 1246 if (!igCollapsingHeader(__("Composite"), ImGuiTreeNodeFlags.DefaultOpen)) 1247 return; 1248 1249 igBeginGroup(); 1250 igIndent(16); 1251 // Header for texture options 1252 if (igCollapsingHeader(__("Textures"))) { 1253 1254 igText(__("Tint")); 1255 1256 incInspectorDeformColorEdit3(["tint.r", "tint.g", "tint.b"], node, param, cursor); 1257 1258 // Padding 1259 igSeparator(); 1260 igSpacing(); 1261 igSpacing(); 1262 } 1263 igUnindent(); 1264 igEndGroup(); 1265 1266 igText(__("Opacity")); 1267 incInspectorDeformSliderFloat("###Opacity", "opacity", 0, 1f, node, param, cursor); 1268 igSpacing(); 1269 igSpacing(); 1270 } 1271 1272 // 1273 // MESH EDIT MODE 1274 // 1275 void incInspectorMeshEditDrawable(Drawable node) { 1276 igPushStyleVar_Vec2(ImGuiStyleVar.FramePadding, ImVec2(8, 8)); 1277 igSpacing(); 1278 igSpacing(); 1279 1280 incViewportVertexInspector(node); 1281 1282 ImVec2 avail = incAvailableSpace(); 1283 incDummy(ImVec2(avail.x, avail.y-38)); 1284 1285 // Right align 1286 incDummy(ImVec2(avail.x-72, 32)); 1287 igSameLine(0, 0); 1288 1289 if (igButton("", ImVec2(32, 32))) { 1290 if (igGetIO().KeyShift) { 1291 incMeshEditReset(); 1292 } else { 1293 incMeshEditClear(); 1294 } 1295 1296 incSetEditMode(EditMode.ModelEdit); 1297 incSelectNode(node); 1298 incFocusCamera(node); 1299 } 1300 incTooltip(_("Cancel")); 1301 1302 igSameLine(0, 8); 1303 1304 if (igButton("", ImVec2(32, 32))) { 1305 incMeshEditApply(); 1306 1307 incSetEditMode(EditMode.ModelEdit); 1308 incSelectNode(node); 1309 incFocusCamera(node); 1310 } 1311 incTooltip(_("Apply")); 1312 igPopStyleVar(); 1313 }