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