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