1 /*
2     Copyright © 2022, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module inochi2d.core.automation.phys;
8 import inochi2d.core.automation;
9 import inochi2d;
10 import std.math;
11 
12 struct VerletNode {
13     float distance = 1f;
14     vec2 position;
15     vec2 oldPosition;
16     
17     this(vec2 pos) {
18         this.position = pos;
19         this.oldPosition = pos;
20     }
21 
22     /**
23         Serializes a parameter
24     */
25     void serialize(S)(ref S serializer) {
26         auto state = serializer.objectBegin;
27             serializer.putKey("distance");
28             serializer.putValue(distance);
29             serializer.putKey("position");
30             position.serialize(serializer);
31             serializer.putKey("old_position");
32             oldPosition.serialize(serializer);
33         serializer.objectEnd(state);
34     }
35 
36     /**
37         Deserializes a parameter
38     */
39     SerdeException deserializeFromFghj(Fghj data) {
40         data["distance"].deserializeValue(distance);
41         position.deserialize(data["position"]);
42         oldPosition.deserialize(data["old_position"]);
43         return null;
44     }
45 }
46 
47 @TypeId("physics")
48 class PhysicsAutomation : Automation {
49 protected:
50 
51     void simulate(size_t i, ref AutomationBinding binding) {
52         auto node = &nodes[i];
53 
54         vec2 tmp = node.position;
55         node.position = (node.position - node.oldPosition) + vec2(0, gravity) * (deltaTime * deltaTime) * bounciness;
56         node.oldPosition = tmp;
57     }
58 
59     void constrain() {
60         foreach(i; 0..nodes.length-1) {
61             auto node1 = &nodes[i];
62             auto node2 = &nodes[i+1];
63 
64             // idx 0 = first node in param, this always is the reference node.
65             // We base our "hinge" of the value of this reference value
66             if (i == 0) {
67                 node1.position = vec2(bindings[i].getAxisValue(), 0);
68             }
69 
70             // Then we calculate the distance of the difference between
71             // node 1 and 2, 
72             float diffX = node1.position.x - node2.position.x;
73             float diffY = node1.position.y - node2.position.y;
74             float dist = node1.position.distance(node2.position);
75             float diff = 0;
76 
77             // We need the distance to be larger than 0 so that
78             // we don't get division by zero problems.
79             if (dist > 0) {
80                 // Node2 decides how far away it wants to be from Node1
81                 diff = (node2.distance - dist) / dist;
82             }
83 
84             // Apply out fancy new link
85             vec2 trans = vec2(diffX, diffY) * (.5 * diff);
86             node1.position += trans;
87             node2.position -= trans;
88 
89             // Clamp so that we don't start flopping above the hinge above us
90             node2.position.y = clamp(node2.position.y, node1.position.y, float.max);
91         }
92     }
93 
94     override
95     void onUpdate() {
96         if (bindings.length > 1) {
97 
98             // simulate each link in our chain
99             foreach(i, ref binding; bindings) {
100                 this.simulate(i, binding);
101             }
102 
103             // Constrain values
104             foreach(i; 0..4+(bindings.length*2)) {
105                 this.constrain();
106             }
107 
108             // Clamp and apply everything to be within range
109             foreach(i, ref node; nodes) {
110                 node.position.x = clamp(node.position.x, bindings[i].range.x, bindings[i].range.y);
111                 bindings[i].addAxisOffset(node.position.x);
112             }
113         }
114     }
115 
116     override
117     void serializeSelf(ref InochiSerializer serializer) {
118         serializer.putKey("nodes");
119         serializer.serializeValue(nodes);
120         serializer.putKey("damping");
121         serializer.putValue(damping);
122         serializer.putKey("bounciness");
123         serializer.putValue(bounciness);
124         serializer.putKey("gravity");
125         serializer.putValue(gravity);
126     }
127 
128     override
129     void serializeSelf(ref InochiSerializerCompact serializer) {
130         serializer.putKey("nodes");
131         serializer.serializeValue(nodes);
132         serializer.putKey("damping");
133         serializer.putValue(damping);
134         serializer.putKey("bounciness");
135         serializer.putValue(bounciness);
136         serializer.putKey("gravity");
137         serializer.putValue(gravity);
138     }
139 
140     override
141     void deserializeSelf(Fghj data) {
142         data["nodes"].deserializeValue(nodes);
143         data["damping"].deserializeValue(damping);
144         data["bounciness"].deserializeValue(bounciness);
145         data["gravity"].deserializeValue(gravity);
146     }
147 public:
148 
149     /**
150         A node in the internal verlet simulation
151     */
152     VerletNode[] nodes;
153 
154     /**
155         Amount of damping to apply to movement
156     */
157     float damping = 0.05f;
158 
159     /**
160         How bouncy movement should be
161         1 = default bounciness
162     */
163     float bounciness = 1f;
164 
165     /**
166         Gravity to apply to each link
167     */
168     float gravity = 20f;
169 
170     /**
171         Adds a binding
172     */
173     override
174     void bind(AutomationBinding binding) {
175         super.bind(binding);
176 
177         nodes ~= VerletNode(vec2(0, 1));
178     }
179 
180     this(Puppet parent) {
181         this.typeId = "physics";
182         super(parent);
183     }
184 }
185 
186 mixin InAutomation!PhysicsAutomation;