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.parameters; 8 import creator.viewport.model.deform; 9 import creator.panels; 10 import creator.widgets; 11 import creator.windows; 12 import creator.core; 13 import creator; 14 import std.string; 15 import inochi2d; 16 import i18n; 17 import std.uni : toLower; 18 import std.stdio; 19 import creator.utils; 20 21 private { 22 ParameterBinding[][Node] cParamBindingEntries; 23 ParameterBinding[][Node] cParamBindingEntriesAll; 24 ParameterBinding[BindTarget] cSelectedBindings; 25 Node[] cCompatibleNodes; 26 vec2u cParamPoint; 27 28 void refreshBindingList(Parameter param) { 29 // Filter selection to remove anything that went away 30 ParameterBinding[BindTarget] newSelectedBindings; 31 32 cParamBindingEntriesAll.clear(); 33 foreach(ParameterBinding binding; param.bindings) { 34 BindTarget target = binding.getTarget(); 35 if (target in cSelectedBindings) newSelectedBindings[target] = binding; 36 cParamBindingEntriesAll[binding.getNode()] ~= binding; 37 } 38 cSelectedBindings = newSelectedBindings; 39 paramPointChanged(param); 40 } 41 42 void paramPointChanged(Parameter param) { 43 cParamBindingEntries.clear(); 44 45 cParamPoint = param.findClosestKeypoint(); 46 foreach(ParameterBinding binding; param.bindings) { 47 if (binding.isSet(cParamPoint)) { 48 cParamBindingEntries[binding.getNode()] ~= binding; 49 } 50 } 51 } 52 53 void mirrorAll(Parameter param, uint axis) { 54 foreach(ParameterBinding binding; param.bindings) { 55 uint xCount = param.axisPointCount(0); 56 uint yCount = param.axisPointCount(1); 57 foreach(x; 0..xCount) { 58 foreach(y; 0..yCount) { 59 vec2u index = vec2u(x, y); 60 if (binding.isSet(index)) { 61 binding.scaleValueAt(index, axis, -1); 62 } 63 } 64 } 65 } 66 } 67 68 void mirroredAutofill(Parameter param, uint axis, float min, float max) { 69 foreach(ParameterBinding binding; param.bindings) { 70 uint xCount = param.axisPointCount(0); 71 uint yCount = param.axisPointCount(1); 72 foreach(x; 0..xCount) { 73 float offX = param.axisPoints[0][x]; 74 if (axis == 0 && (offX < min || offX > max)) continue; 75 foreach(y; 0..yCount) { 76 float offY = param.axisPoints[1][x]; 77 if (axis == 1 && (offY < min || offY > max)) continue; 78 79 vec2u index = vec2u(x, y); 80 if (!binding.isSet(index)) binding.extrapolateValueAt(index, axis); 81 } 82 } 83 } 84 } 85 86 void fixScales(Parameter param) { 87 foreach(ParameterBinding binding; param.bindings) { 88 switch(binding.getName()) { 89 case "transform.s.x": 90 case "transform.s.y": 91 if (ValueParameterBinding b = cast(ValueParameterBinding)binding) { 92 uint xCount = param.axisPointCount(0); 93 uint yCount = param.axisPointCount(1); 94 foreach(x; 0..xCount) { 95 foreach(y; 0..yCount) { 96 vec2u index = vec2u(x, y); 97 if (b.isSet(index)) { 98 b.values[x][y] += 1; 99 } 100 } 101 } 102 b.reInterpolate(); 103 } 104 break; 105 default: break; 106 } 107 } 108 } 109 110 Node[] getCompatibleNodes() { 111 Node thisNode = null; 112 113 foreach(binding; cSelectedBindings.byValue()) { 114 if (thisNode is null) thisNode = binding.getNode(); 115 else if (!(binding.getNode() is thisNode)) return null; 116 } 117 if (thisNode is null) return null; 118 119 Node[] compatible; 120 nodeLoop: foreach(otherNode; cParamBindingEntriesAll.byKey()) { 121 if (otherNode is thisNode) continue; 122 123 foreach(binding; cSelectedBindings.byValue()) { 124 if (!binding.isCompatibleWithNode(otherNode)) 125 continue nodeLoop; 126 } 127 compatible ~= otherNode; 128 } 129 130 return compatible; 131 } 132 133 void copySelectionToNode(Parameter param, Node target) { 134 Node src = cSelectedBindings.keys[0].node; 135 136 foreach(binding; cSelectedBindings.byValue()) { 137 assert(binding.getNode() is src, "selection mismatch"); 138 139 ParameterBinding b = param.getOrAddBinding(target, binding.getName()); 140 binding.copyKeypointToBinding(cParamPoint, b, cParamPoint); 141 } 142 143 refreshBindingList(param); 144 } 145 146 void swapSelectionWithNode(Parameter param, Node target) { 147 Node src = cSelectedBindings.keys[0].node; 148 149 foreach(binding; cSelectedBindings.byValue()) { 150 assert(binding.getNode() is src, "selection mismatch"); 151 152 ParameterBinding b = param.getOrAddBinding(target, binding.getName()); 153 binding.swapKeypointWithBinding(cParamPoint, b, cParamPoint); 154 } 155 156 refreshBindingList(param); 157 } 158 159 void keypointActions(Parameter param, ParameterBinding[] bindings) { 160 if (igMenuItem(__("Unset"), "", false, true)) { 161 foreach(binding; bindings) { 162 binding.unset(cParamPoint); 163 } 164 incViewportNodeDeformNotifyParamValueChanged(); 165 } 166 if (igMenuItem(__("Set to current"), "", false, true)) { 167 foreach(binding; bindings) { 168 binding.setCurrent(cParamPoint); 169 } 170 incViewportNodeDeformNotifyParamValueChanged(); 171 } 172 if (igMenuItem(__("Reset"), "", false, true)) { 173 foreach(binding; bindings) { 174 binding.reset(cParamPoint); 175 } 176 incViewportNodeDeformNotifyParamValueChanged(); 177 } 178 if (igMenuItem(__("Invert"), "", false, true)) { 179 foreach(binding; bindings) { 180 binding.scaleValueAt(cParamPoint, -1, -1); 181 } 182 incViewportNodeDeformNotifyParamValueChanged(); 183 } 184 if (param.isVec2) { 185 if (igBeginMenu(__("Mirror"), true)) { 186 if (igMenuItem(__("Horizontally"), "", false, true)) { 187 foreach(binding; bindings) { 188 binding.scaleValueAt(cParamPoint, 0, -1); 189 } 190 incViewportNodeDeformNotifyParamValueChanged(); 191 } 192 if (igMenuItem(__("Vertically"), "", false, true)) { 193 foreach(binding; bindings) { 194 binding.scaleValueAt(cParamPoint, 1, -1); 195 } 196 incViewportNodeDeformNotifyParamValueChanged(); 197 } 198 igEndMenu(); 199 } 200 } 201 if (param.isVec2) { 202 if (igBeginMenu(__("Set from mirror"), true)) { 203 if (igMenuItem(__("Horizontally"), "", false, true)) { 204 foreach(binding; bindings) { 205 binding.extrapolateValueAt(cParamPoint, 0); 206 } 207 incViewportNodeDeformNotifyParamValueChanged(); 208 } 209 if (igMenuItem(__("Vertically"), "", false, true)) { 210 foreach(binding; bindings) { 211 binding.extrapolateValueAt(cParamPoint, 1); 212 } 213 incViewportNodeDeformNotifyParamValueChanged(); 214 } 215 if (igMenuItem(__("Diagonally"), "", false, true)) { 216 foreach(binding; bindings) { 217 binding.extrapolateValueAt(cParamPoint, -1); 218 } 219 incViewportNodeDeformNotifyParamValueChanged(); 220 } 221 igEndMenu(); 222 } 223 } else { 224 if (igMenuItem(__("Set from mirror"), "", false, true)) { 225 foreach(binding; bindings) { 226 binding.extrapolateValueAt(cParamPoint, 0); 227 } 228 incViewportNodeDeformNotifyParamValueChanged(); 229 } 230 } 231 } 232 233 void bindingList(Parameter param) { 234 if (!igCollapsingHeader(__("Bindings"), ImGuiTreeNodeFlags.DefaultOpen)) return; 235 236 refreshBindingList(param); 237 238 auto io = igGetIO(); 239 auto style = igGetStyle(); 240 ImS32 inactiveColor = igGetColorU32(style.Colors[ImGuiCol.TextDisabled]); 241 242 igBeginChild("BindingList", ImVec2(0, 256), false); 243 igPushStyleVar(ImGuiStyleVar.CellPadding, ImVec2(4, 1)); 244 igPushStyleVar(ImGuiStyleVar.IndentSpacing, 14); 245 246 foreach(node, allBindings; cParamBindingEntriesAll) { 247 ParameterBinding[] *bindings = (node in cParamBindingEntries); 248 249 // Figure out if node is selected ( == all bindings selected) 250 bool nodeSelected = true; 251 bool someSelected = false; 252 foreach(binding; allBindings) { 253 if ((binding.getTarget() in cSelectedBindings) is null) 254 nodeSelected = false; 255 else 256 someSelected = true; 257 } 258 259 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow; 260 if (nodeSelected) 261 flags |= ImGuiTreeNodeFlags.Selected; 262 263 if (bindings is null) igPushStyleColor(ImGuiCol.Text, inactiveColor); 264 string nodeName = incTypeIdToIconConcat(node.typeId) ~ " " ~ node.name; 265 if (igTreeNodeEx(cast(void*)node.uuid, flags, nodeName.toStringz)) { 266 if (bindings is null) igPopStyleColor(); 267 if (igBeginPopup("###BindingPopup")) { 268 if (igMenuItem(__("Remove"), "", false, true)) { 269 foreach(binding; cSelectedBindings.byValue()) { 270 param.removeBinding(binding); 271 } 272 incViewportNodeDeformNotifyParamValueChanged(); 273 } 274 275 keypointActions(param, cSelectedBindings.values); 276 277 bool haveCompatible = cCompatibleNodes.length > 0; 278 if (igBeginMenu(__("Copy to"), haveCompatible)) { 279 foreach(node; cCompatibleNodes) { 280 if (igMenuItem(node.name.toStringz, "", false, true)) { 281 copySelectionToNode(param, node); 282 } 283 } 284 igEndMenu(); 285 } 286 if (igBeginMenu(__("Swap with"), haveCompatible)) { 287 foreach(node; cCompatibleNodes) { 288 if (igMenuItem(node.name.toStringz, "", false, true)) { 289 swapSelectionWithNode(param, node); 290 } 291 } 292 igEndMenu(); 293 } 294 295 igEndPopup(); 296 } 297 if (igIsItemClicked(ImGuiMouseButton.Right)) { 298 if (!someSelected) { 299 cSelectedBindings.clear(); 300 foreach(binding; allBindings) { 301 cSelectedBindings[binding.getTarget()] = binding; 302 } 303 } 304 cCompatibleNodes = getCompatibleNodes(); 305 igOpenPopup("###BindingPopup"); 306 } 307 308 // Node selection logic 309 if (igIsItemClicked(ImGuiMouseButton.Left) && !igIsItemToggledOpen()) { 310 311 // Select the node you've clicked in the bindings list 312 if (incNodeInSelection(node)) { 313 incFocusCamera(node); 314 } else incSelectNode(node); 315 316 if (!io.KeyCtrl) { 317 cSelectedBindings.clear(); 318 nodeSelected = false; 319 } 320 foreach(binding; allBindings) { 321 if (nodeSelected) cSelectedBindings.remove(binding.getTarget()); 322 else cSelectedBindings[binding.getTarget()] = binding; 323 } 324 } 325 foreach(binding; allBindings) { 326 ImGuiTreeNodeFlags flags = 327 ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | 328 ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; 329 330 bool selected = cast(bool)(binding.getTarget() in cSelectedBindings); 331 if (selected) flags |= ImGuiTreeNodeFlags.Selected; 332 333 // Style as inactive if not set at this keypoint 334 if (!binding.isSet(cParamPoint)) 335 igPushStyleColor(ImGuiCol.Text, inactiveColor); 336 337 // Binding entry 338 auto value = cast(ValueParameterBinding)binding; 339 string label; 340 if (value && binding.isSet(cParamPoint)) { 341 label = format("%s (%.02f)", binding.getName(), value.getValue(cParamPoint)); 342 } else { 343 label = binding.getName(); 344 } 345 igTreeNodeEx("binding", flags, label.toStringz); 346 if (!binding.isSet(cParamPoint)) igPopStyleColor(); 347 348 // Binding selection logic 349 if (igIsItemClicked(ImGuiMouseButton.Right)) { 350 if (!selected) { 351 cSelectedBindings.clear(); 352 cSelectedBindings[binding.getTarget()] = binding; 353 } 354 cCompatibleNodes = getCompatibleNodes(); 355 igOpenPopup("###BindingPopup"); 356 } 357 if (igIsItemClicked(ImGuiMouseButton.Left)) { 358 if (!io.KeyCtrl) { 359 cSelectedBindings.clear(); 360 selected = false; 361 } 362 if (selected) cSelectedBindings.remove(binding.getTarget()); 363 else cSelectedBindings[binding.getTarget()] = binding; 364 } 365 } 366 igTreePop(); 367 } else if (bindings is null) igPopStyleColor(); 368 } 369 igPopStyleVar(); 370 igPopStyleVar(); 371 igEndChild(); 372 } 373 374 } 375 376 /** 377 Generates a parameter view 378 */ 379 void incParameterView(Parameter param) { 380 if (!igCollapsingHeader(param.name.toStringz, ImGuiTreeNodeFlags.DefaultOpen)) return; 381 igIndent(); 382 igPushID(cast(void*)param); 383 384 float reqSpace = param.isVec2 ? 144 : 52; 385 386 // Parameter Control 387 ImVec2 avail = incAvailableSpace(); 388 if (igBeginChild("###PARAM", ImVec2(avail.x-24, reqSpace))) { 389 // Popup for rightclicking the controller 390 if (igBeginPopup("###ControlPopup")) { 391 if (incArmedParameter() == param) { 392 keypointActions(param, param.bindings); 393 } 394 igEndPopup(); 395 } 396 397 if (param.isVec2) igText("%.2f %.2f", param.value.x, param.value.y); 398 else igText("%.2f", param.value.x); 399 400 if (incController("###CONTROLLER", param, ImVec2(avail.x-18, reqSpace-24), incArmedParameter() == param)) { 401 if (incArmedParameter() == param) { 402 incViewportNodeDeformNotifyParamValueChanged(); 403 paramPointChanged(param); 404 } 405 } 406 if (igIsItemClicked(ImGuiMouseButton.Right)) { 407 if (incArmedParameter() == param) incViewportNodeDeformNotifyParamValueChanged(); 408 refreshBindingList(param); 409 igOpenPopup("###ControlPopup"); 410 } 411 } 412 igEndChild(); 413 414 415 if (incEditMode == EditMode.ModelEdit) { 416 igSameLine(0, 0); 417 // Parameter Setting Buttons 418 if(igBeginChild("###SETTING", ImVec2(avail.x-24, reqSpace))) { 419 if (igBeginPopup("###EditParam")) { 420 if (igMenuItem(__("Edit Properties"), "", false, true)) { 421 incPushWindowList(new ParamPropWindow(param)); 422 } 423 424 if (igMenuItem(__("Edit Axes Points"), "", false, true)) { 425 incPushWindowList(new ParamAxesWindow(param)); 426 } 427 428 if (param.isVec2) { 429 if (igMenuItem(__("Flip X"), "", false, true)) { 430 param.reverseAxis(0); 431 } 432 if (igMenuItem(__("Flip Y"), "", false, true)) { 433 param.reverseAxis(1); 434 } 435 } else { 436 if (igMenuItem(__("Flip"), "", false, true)) { 437 param.reverseAxis(0); 438 } 439 } 440 if (igBeginMenu(__("Mirror"), true)) { 441 if (igMenuItem(__("Horizontally"), "", false, true)) { 442 mirrorAll(param, 0); 443 incViewportNodeDeformNotifyParamValueChanged(); 444 } 445 if (igMenuItem(__("Vertically"), "", false, true)) { 446 mirrorAll(param, 1); 447 incViewportNodeDeformNotifyParamValueChanged(); 448 } 449 igEndMenu(); 450 } 451 if (igBeginMenu(__("Mirrored Autofill"), true)) { 452 if (igMenuItem(__(""), "", false, true)) { 453 mirroredAutofill(param, 0, 0, 0.4999); 454 incViewportNodeDeformNotifyParamValueChanged(); 455 } 456 if (igMenuItem(__(""), "", false, true)) { 457 mirroredAutofill(param, 0, 0.5001, 1); 458 incViewportNodeDeformNotifyParamValueChanged(); 459 } 460 if (param.isVec2) { 461 if (igMenuItem(__(""), "", false, true)) { 462 mirroredAutofill(param, 1, 0, 0.4999); 463 incViewportNodeDeformNotifyParamValueChanged(); 464 } 465 if (igMenuItem(__(""), "", false, true)) { 466 mirroredAutofill(param, 1, 0.5001, 1); 467 incViewportNodeDeformNotifyParamValueChanged(); 468 } 469 } 470 igEndMenu(); 471 } 472 473 igNewLine(); 474 igSeparator(); 475 476 if (igMenuItem(__("Delete"), "", false, true)) { 477 if (incArmedParameter() == param) { 478 incDisarmParameter(); 479 } 480 incActivePuppet().removeParameter(param); 481 } 482 483 igNewLine(); 484 igSeparator(); 485 486 if (igMenuItem(__("Fix Scales"), "", false, true)) { 487 fixScales(param); 488 } 489 igEndPopup(); 490 } 491 492 if (igButton("", ImVec2(24, 24))) { 493 igOpenPopup("###EditParam"); 494 } 495 496 497 if (incButtonColored("", ImVec2(24, 24), incArmedParameter() == param ? ImVec4(1f, 0f, 0f, 1f) : *igGetStyleColorVec4(ImGuiCol.Text))) { 498 if (incArmedParameter() == param) { 499 incDisarmParameter(); 500 } else { 501 param.value = param.getClosestKeypointValue(); 502 paramPointChanged(param); 503 incArmParameter(param); 504 } 505 } 506 507 // Arms the parameter for recording values. 508 incTooltip(_("Arm Parameter")); 509 } 510 igEndChild(); 511 } 512 if (incArmedParameter() == param) { 513 bindingList(param); 514 } 515 igPopID(); 516 igUnindent(); 517 } 518 519 /** 520 The logger frame 521 */ 522 class ParametersPanel : Panel { 523 private: 524 string filter; 525 protected: 526 override 527 void onUpdate() { 528 auto parameters = incActivePuppet().parameters; 529 530 if (igBeginPopup("###AddParameter")) { 531 if (igMenuItem(__("Add 1D Parameter (0..1)"), "", false, true)) { 532 Parameter param = new Parameter( 533 "Param #%d\0".format(parameters.length), 534 false 535 ); 536 incActivePuppet().parameters ~= param; 537 } 538 if (igMenuItem(__("Add 1D Parameter (-1..1)"), "", false, true)) { 539 Parameter param = new Parameter( 540 "Param #%d\0".format(parameters.length), 541 false 542 ); 543 param.min.x = -1; 544 param.max.x = 1; 545 param.insertAxisPoint(0, 0.5); 546 incActivePuppet().parameters ~= param; 547 } 548 if (igMenuItem(__("Add 2D Parameter (0..1)"), "", false, true)) { 549 Parameter param = new Parameter( 550 "Param #%d\0".format(parameters.length), 551 true 552 ); 553 incActivePuppet().parameters ~= param; 554 } 555 if (igMenuItem(__("Add 2D Parameter (-1..+1)"), "", false, true)) { 556 Parameter param = new Parameter( 557 "Param #%d\0".format(parameters.length), 558 true 559 ); 560 param.min = vec2(-1, -1); 561 param.max = vec2(1, 1); 562 param.insertAxisPoint(0, 0.5); 563 param.insertAxisPoint(1, 0.5); 564 incActivePuppet().parameters ~= param; 565 } 566 if (igMenuItem(__("Add Mouth Shape"), "", false, true)) { 567 Parameter param = new Parameter( 568 "Mouth #%d\0".format(parameters.length), 569 true 570 ); 571 param.min = vec2(-1, 0); 572 param.max = vec2(1, 1); 573 param.insertAxisPoint(0, 0.25); 574 param.insertAxisPoint(0, 0.5); 575 param.insertAxisPoint(0, 0.6); 576 param.insertAxisPoint(1, 0.3); 577 param.insertAxisPoint(1, 0.5); 578 param.insertAxisPoint(1, 0.6); 579 incActivePuppet().parameters ~= param; 580 } 581 igEndPopup(); 582 } 583 if (igBeginChild("###FILTER", ImVec2(0, 32))) { 584 if (incInputText("Filter", filter)) { 585 filter = filter.toLower; 586 } 587 incTooltip("Filter, search for specific parameters"); 588 } 589 igEndChild(); 590 591 if (igBeginChild("ParametersList", ImVec2(0, -36))) { 592 593 // Always render the currently armed parameter on top 594 if (incArmedParameter()) { 595 incParameterView(incArmedParameter()); 596 } 597 598 // Render other parameters 599 foreach(ref param; parameters) { 600 if (incArmedParameter() == param) continue; 601 import std.algorithm.searching : canFind; 602 if (filter.length == 0 || param.indexableName.canFind(filter)) { 603 incParameterView(param); 604 } 605 } 606 } 607 igEndChild(); 608 609 // Right align add button 610 ImVec2 avail = incAvailableSpace(); 611 incDummy(ImVec2(avail.x-32, 32)); 612 igSameLine(0, 0); 613 614 // Add button 615 if (igButton("", ImVec2(32, 32))) { 616 igOpenPopup("###AddParameter"); 617 } 618 incTooltip(_("Add Parameter")); 619 } 620 621 public: 622 this() { 623 super("Parameters", _("Parameters"), false); 624 } 625 } 626 627 /** 628 Generate logger frame 629 */ 630 mixin incPanel!ParametersPanel; 631 632