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