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 }