1 /*
2     Copyright © 2022, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4 
5     Authors:
6     - Luna Nielsen
7     - Asahi Lina
8 */
9 module inochi2d.core.param;
10 import inochi2d.fmt.serialize;
11 import inochi2d.math.serialization;
12 import inochi2d.core;
13 import inochi2d.math;
14 import std.exception;
15 import std.array;
16 import std.algorithm.mutation;
17 import std.stdio;
18 
19 public import inochi2d.core.param.binding;
20 
21 enum ParamMergeMode {
22 
23     /**
24         Parameters are merged additively
25     */
26     @serdeFlexible
27     @serdeKeys("Additive", "additive")
28     Additive,
29 
30     /**
31         Parameters are merged with a weighted average
32     */
33     @serdeFlexible
34     @serdeKeys("Weighted", "weighted")
35     Weighted,
36 
37     /**
38         Parameters are merged multiplicatively
39     */
40     @serdeFlexible
41     @serdeKeys("Multiplicative", "multiplicative")
42     Multiplicative,
43 
44     /**
45         Forces parameter to be given value
46     */
47     @serdeFlexible
48     @serdeKeys("Forced", "forced")
49     Forced,
50 
51     /**
52         Merge mode is passthrough
53     */
54     @serdeFlexible
55     @serdeKeys("Passthrough", "passthrough")
56     Passthrough,
57 }
58 
59 /**
60     A parameter
61 */
62 class Parameter {
63 private:
64     struct Combinator {
65         vec2[] ivalues;
66         float[] iweights;
67         int isum;
68 
69         void clear() {
70             isum = 0;
71         }
72 
73         void resize(int reqLength) {
74             ivalues.length = reqLength;
75             iweights.length = reqLength;
76         }
77 
78         void add(vec2 value, float weight) {
79             if (isum >= ivalues.length) resize(isum+8);
80 
81             ivalues[isum] = value;
82             iweights[isum] = weight;
83             isum++;
84         }
85 
86         void add(int axis, float value, float weight) {
87             if (isum >= ivalues.length) resize(isum+8);
88 
89             ivalues[isum] = vec2(axis == 0 ? value : 1, axis == 1 ? value : 1);
90             iweights[isum] = weight;
91             isum++;
92         }
93         
94         vec2 csum() {
95             vec2 val = vec2(0, 0);
96             foreach(i; 0..isum) {
97                 val += ivalues[i];
98             }
99             return val;
100         }
101 
102         vec2 avg() {
103             if (isum == 0) return vec2(1, 1);
104 
105             vec2 val = vec2(0, 0);
106             foreach(i; 0..isum) {
107                 val += ivalues[i]*iweights[i];
108             }
109             return val/isum;
110         }
111     }
112 
113     Combinator iadd;
114     Combinator imul;
115 protected:
116     void serializeSelf(ref InochiSerializer serializer) {
117         serializer.putKey("uuid");
118         serializer.putValue(uuid);
119         serializer.putKey("name");
120         serializer.putValue(name);
121         serializer.putKey("is_vec2");
122         serializer.putValue(isVec2);
123         serializer.putKey("min");
124         min.serialize(serializer);
125         serializer.putKey("max");
126         max.serialize(serializer);
127         serializer.putKey("defaults");
128         defaults.serialize(serializer);
129         serializer.putKey("axis_points");
130         serializer.serializeValue(axisPoints);
131         serializer.putKey("merge_mode");
132         serializer.serializeValue(mergeMode);
133         serializer.putKey("bindings");
134         auto arrstate = serializer.arrayBegin();
135             foreach(binding; bindings) {
136                 serializer.elemBegin();
137                 binding.serializeSelf(serializer);
138             }
139         serializer.arrayEnd(arrstate);
140     }
141 
142 public:
143     /**
144         Unique ID of parameter
145     */
146     uint uuid;
147 
148     /**
149         Name of the parameter
150     */
151     string name;
152 
153     /**
154         Optimized indexable name generated at runtime
155 
156         DO NOT SERIALIZE THIS.
157     */
158     string indexableName;
159 
160     /**
161         Whether this parameter updates the model
162     */
163     bool active = true;
164 
165     /**
166         The current parameter value
167     */
168     vec2 value = vec2(0);
169 
170     /**
171         The previous internal value offset
172     */
173     vec2 lastInternal = vec2(0);
174 
175     /**
176         Parameter merge mode
177     */
178     ParamMergeMode mergeMode;
179 
180     /**
181         The default value
182     */
183     vec2 defaults = vec2(0);
184 
185     /**
186         Whether the parameter is 2D
187     */
188     bool isVec2;
189 
190     /**
191         The parameter's minimum bounds
192     */
193     vec2 min = vec2(0, 0);
194 
195     /**
196         The parameter's maximum bounds
197     */
198     vec2 max = vec2(1, 1);
199 
200     /**
201         Position of the keypoints along each axis
202     */
203     float[][2] axisPoints = [[0, 1], [0, 1]];
204 
205     /**
206         Binding to targets
207     */
208     ParameterBinding[] bindings;
209 
210     /**
211         Gets the value normalized to the internal range (0.0->1.0)
212     */
213     vec2 normalizedValue() {
214         return this.mapValue(value);
215     }
216 
217     /**
218         Sets the value normalized up from the internal range (0.0->1.0)
219         to the user defined range.
220     */
221     void normalizedValue(vec2 value) {
222         this.value = vec2(
223             value.x * (max.x-min.x) + min.x,
224             value.y * (max.y-min.y) + min.y
225         );
226     }
227 
228     /**
229         For serialization
230     */
231     this() { }
232 
233     /**
234         Unload UUID on clear
235     */
236     ~this() {
237         inUnloadUUID(this.uuid);
238     }
239 
240     /**
241         Create new parameter
242     */
243     this(string name, bool isVec2) {
244         this.uuid = inCreateUUID();
245         this.name = name;
246         this.isVec2 = isVec2;
247         if (!isVec2)
248             axisPoints[1] = [0];
249         
250         this.makeIndexable();
251     }
252 
253     /**
254         Clone this parameter
255     */
256     Parameter dup() {
257         Parameter newParam = new Parameter(name ~ " (Copy)", isVec2);
258 
259         newParam.min = min;
260         newParam.max = max;
261         newParam.axisPoints = axisPoints.dup;
262 
263         foreach(binding; bindings) {
264             ParameterBinding newBinding = newParam.createBinding(
265                 binding.getNode(),
266                 binding.getName(),
267                 false
268             );
269             newBinding.interpolateMode = binding.interpolateMode;
270             foreach(x; 0..axisPointCount(0)) {
271                 foreach(y; 0..axisPointCount(1)) {
272                     binding.copyKeypointToBinding(vec2u(x, y), newBinding, vec2u(x, y));
273                 }
274             }
275             newParam.addBinding(newBinding);
276         }
277 
278         return newParam;
279     }
280 
281     /**
282         Serializes a parameter
283     */
284     void serialize(ref InochiSerializer serializer) {
285         auto state = serializer.objectBegin;
286         serializeSelf(serializer);
287         serializer.objectEnd(state);
288     }
289 
290     /**
291         Deserializes a parameter
292     */
293     SerdeException deserializeFromFghj(Fghj data) {
294         data["uuid"].deserializeValue(this.uuid);
295         data["name"].deserializeValue(this.name);
296         if (!data["is_vec2"].isEmpty) data["is_vec2"].deserializeValue(this.isVec2);
297         if (!data["min"].isEmpty) min.deserialize(data["min"]);
298         if (!data["max"].isEmpty) max.deserialize(data["max"]);
299         if (!data["axis_points"].isEmpty) data["axis_points"].deserializeValue(this.axisPoints);
300         if (!data["defaults"].isEmpty) defaults.deserialize(data["defaults"]);
301         if (!data["merge_mode"].isEmpty) data["merge_mode"].deserializeValue(this.mergeMode);
302 
303         if (!data["bindings"].isEmpty) {
304             foreach(Fghj child; data["bindings"].byElement) {
305                 
306                 // Skip empty children
307                 if (child["param_name"].isEmpty) continue;
308 
309                 string paramName;
310                 child["param_name"].deserializeValue(paramName);
311 
312                 if (paramName == "deform") {
313                     auto binding = new DeformationParameterBinding(this);
314                     binding.deserializeFromFghj(child);
315                     bindings ~= binding;
316                 } else {
317                     auto binding = new ValueParameterBinding(this);
318                     binding.deserializeFromFghj(child);
319                     bindings ~= binding;
320                 }
321             }
322         }
323 
324         return null;
325     }
326 
327     void reconstruct(Puppet puppet) {
328         foreach(i, binding; bindings) {
329             binding.reconstruct(puppet);
330         }
331     }
332 
333     /**
334         Finalize loading of parameter
335     */
336     void finalize(Puppet puppet) {
337         this.makeIndexable();
338         this.value = defaults;
339 
340         ParameterBinding[] validBindingList;
341         foreach(i, binding; bindings) {
342             if (puppet.find!Node(binding.getNodeUUID())) {
343                 binding.finalize(puppet);
344                 validBindingList ~= binding;
345             }
346         }
347         
348         bindings = validBindingList;
349     }
350 
351     void findOffset(vec2 offset, out vec2u index, out vec2 outOffset) {
352         void interpAxis(uint axis, float val, out uint index, out float offset) {
353             float[] pos = axisPoints[axis];
354 
355             foreach(i; 0..pos.length - 1) {
356                 if (pos[i + 1] > val || i == (pos.length - 2)) {
357                     index = cast(uint)i;
358                     offset = (val - pos[i]) / (pos[i + 1] - pos[i]);
359                     return;
360                 }
361             }
362         }
363 
364         interpAxis(0, offset.x, index.x, outOffset.x);
365         if (isVec2) interpAxis(1, offset.y, index.y, outOffset.y);
366     }
367 
368     void update() {
369         vec2u index;
370         vec2 offset_;
371 
372         if (!active)
373             return;
374 
375         lastInternal = (value + iadd.csum()) * imul.avg();
376 
377         findOffset(this.mapValue(lastInternal), index, offset_);
378         foreach(binding; bindings) {
379             binding.apply(index, offset_);
380         }
381 
382         // Reset combinatorics
383         iadd.clear();
384         imul.clear();
385     }
386 
387     void pushIOffset(vec2 offset, ParamMergeMode mode = ParamMergeMode.Passthrough, float weight=1) {
388         if (mode == ParamMergeMode.Passthrough) mode = mergeMode;
389         switch(mode) {
390             case ParamMergeMode.Forced:
391                 this.value = offset;
392                 break;
393             case ParamMergeMode.Additive:
394                 iadd.add(offset, 1);
395                 break;
396             case ParamMergeMode.Multiplicative:
397                 imul.add(offset, 1);
398                 break;
399             case ParamMergeMode.Weighted:
400                 imul.add(offset, weight);
401                 break;
402             default: break;
403         }
404     }
405 
406     void pushIOffsetAxis(int axis, float offset, ParamMergeMode mode = ParamMergeMode.Passthrough, float weight=1) {
407         if (mode == ParamMergeMode.Passthrough) mode = mergeMode;
408         switch(mode) {
409             case ParamMergeMode.Forced:
410                 this.value.vector[axis] = offset;
411                 break;
412             case ParamMergeMode.Additive:
413                 iadd.add(axis, offset, 1);
414                 break;
415             case ParamMergeMode.Multiplicative:
416                 imul.add(axis, offset, 1);
417                 break;
418             case ParamMergeMode.Weighted:
419                 imul.add(axis, offset, weight);
420                 break;
421             default: break;
422         }
423     }
424 
425     /**
426         Get number of points for an axis
427     */
428     uint axisPointCount(uint axis = 0) {
429         return cast(uint)axisPoints[axis].length;
430     }
431 
432     /**
433         Move an axis point to a new offset
434     */
435     void moveAxisPoint(uint axis, uint oldidx, float newoff) {
436         assert(oldidx > 0 && oldidx < this.axisPointCount(axis)-1, "invalid point index");
437         assert(newoff > 0 && newoff < 1, "offset out of bounds");
438         if (isVec2)
439             assert(axis <= 1, "bad axis");
440         else
441             assert(axis == 0, "bad axis");
442 
443         // Find the index at which to insert
444         uint index;
445         for(index = 1; index < axisPoints[axis].length; index++) {
446             if (axisPoints[axis][index+1] > newoff)
447                 break;
448         }
449         
450         if (oldidx != index) {
451             // BUG: Apparently deleting the oldindex and replacing it with newindex causes a crash.
452 
453             // Insert it into the new position in the list
454             auto swap = axisPoints[oldidx];
455             axisPoints[axis] = axisPoints[axis].remove(oldidx);
456             axisPoints[axis].insertInPlace(index, swap);
457             debug writeln("after move ", this.axisPointCount(0));
458         }
459 
460         // Tell all bindings to reinterpolate
461         foreach(binding; bindings) {
462             binding.moveKeypoints(axis, oldidx, index);
463         }
464     }
465 
466     /**
467         Add a new axis point at the given offset
468     */
469     void insertAxisPoint(uint axis, float off) {
470         assert(off > 0 && off < 1, "offset out of bounds");
471         if (isVec2)
472             assert(axis <= 1, "bad axis");
473         else
474             assert(axis == 0, "bad axis");
475 
476         // Find the index at which to insert
477         uint index;
478         for(index = 1; index < axisPoints[axis].length; index++) {
479             if (axisPoints[axis][index] > off)
480                 break;
481         }
482 
483         // Insert it into the position list
484         axisPoints[axis].insertInPlace(index, off);
485 
486         // Tell all bindings to insert space into their arrays
487         foreach(binding; bindings) {
488             binding.insertKeypoints(axis, index);
489         }
490     }
491 
492     /**
493         Delete a specified axis point by index
494     */
495     void deleteAxisPoint(uint axis, uint index) {
496         if (isVec2)
497             assert(axis <= 1, "bad axis");
498         else
499             assert(axis == 0, "bad axis");
500 
501         assert(index > 0, "cannot delete axis point at 0");
502         assert(index < (axisPoints[axis].length - 1), "cannot delete axis point at 1");
503 
504         // Remove the keypoint
505         axisPoints[axis] = axisPoints[axis].remove(index);
506 
507         // Tell all bindings to remove it from their arrays
508         foreach(binding; bindings) {
509             binding.deleteKeypoints(axis, index);
510         }
511     }
512 
513     /**
514         Flip the mapping across an axis
515     */
516     void reverseAxis(uint axis) {
517         axisPoints[axis].reverse();
518         foreach(ref i; axisPoints[axis]) {
519             i = 1 - i;
520         }
521         foreach(binding; bindings) {
522             binding.reverseAxis(axis);
523         }
524     }
525 
526     /**
527         Get the offset (0..1) of a specified keypoint index
528     */
529     vec2 getKeypointOffset(vec2u index) {
530         return vec2(axisPoints[0][index.x], axisPoints[1][index.y]);
531     }
532 
533     /**
534         Get the value at a specified keypoint index
535     */
536     vec2 getKeypointValue(vec2u index) {
537         return unmapValue(getKeypointOffset(index));
538     }
539 
540     /**
541         Maps an input value to an offset (0.0->1.0)
542     */
543     vec2 mapValue(vec2 value) {
544         vec2 range = max - min;
545         vec2 tmp = (value - min);
546         vec2 off = vec2(tmp.x / range.x, tmp.y / range.y);
547 
548         vec2 clamped = vec2(
549             clamp(off.x, 0, 1),
550             clamp(off.y, 0, 1),
551         );
552         return clamped;
553     }
554 
555     /**
556         Maps an offset (0.0->1.0) to a value
557     */
558     vec2 unmapValue(vec2 offset) {
559         vec2 range = max - min;
560         return vec2(range.x * offset.x, range.y * offset.y) + min;
561     }
562 
563     /**
564         Maps an input value to an offset (0.0->1.0) for an axis
565     */
566     float mapAxis(uint axis, float value) {
567         vec2 input = min;
568         if (axis == 0) input.x = value;
569         else input.y = value;
570         vec2 output = mapValue(input);
571         if (axis == 0) return output.x;
572         else return output.y;
573     }
574 
575     /**
576         Maps an internal value (0.0->1.0) to the input range for an axis
577     */
578     float unmapAxis(uint axis, float offset) {
579         vec2 input = min;
580         if (axis == 0) input.x = offset;
581         else input.y = offset;
582         vec2 output = unmapValue(input);
583         if (axis == 0) return output.x;
584         else return output.y;
585     }
586 
587     /**
588         Gets the axis point closest to a given offset
589     */
590     uint getClosestAxisPointIndex(uint axis, float offset) {
591         uint closestPoint = 0;
592         float closestDist = float.infinity;
593 
594         foreach(i, pointVal; axisPoints[axis]) {
595             float dist = abs(pointVal - offset);
596             if (dist < closestDist) {
597                 closestDist = dist;
598                 closestPoint = cast(uint)i;
599             }
600         }
601 
602         return closestPoint;
603     }
604 
605     /**
606         Find the keypoint closest to the current value
607     */
608     vec2u findClosestKeypoint() {
609         return findClosestKeypoint(value);
610     }
611 
612     /**
613         Find the keypoint closest to a value
614     */
615     vec2u findClosestKeypoint(vec2 value) {
616         vec2 mapped = mapValue(value);
617         uint x = getClosestAxisPointIndex(0, mapped.x);
618         uint y = getClosestAxisPointIndex(1, mapped.y);
619 
620         return vec2u(x, y);
621     }
622 
623     /**
624         Find the keypoint closest to the current value
625     */
626     vec2 getClosestKeypointValue() {
627         return getKeypointValue(findClosestKeypoint());
628     }
629 
630     /**
631         Find the keypoint closest to a value
632     */
633     vec2 getClosestKeypointValue(vec2 value) {
634         return getKeypointValue(findClosestKeypoint(value));
635     }
636 
637     /**
638         Find a binding by node ref and name
639     */
640     ParameterBinding getBinding(Node n, string bindingName) {
641         foreach(ref binding; bindings) {
642             if (binding.getNode() != n) continue;
643             if (binding.getName == bindingName) return binding;
644         }
645         return null;
646     }
647 
648     /**
649         Check if a binding exists for a given node and name
650     */
651     bool hasBinding(Node n, string bindingName) {
652         foreach(ref binding; bindings) {
653             if (binding.getNode() != n) continue;
654             if (binding.getName == bindingName) return true;
655         }
656         return false;
657     }
658 
659     /**
660         Check if any bindings exists for a given node
661     */
662     bool hasAnyBinding(Node n) {
663         foreach(ref binding; bindings) {
664             if (binding.getNode() == n) return true;
665         }
666         return false;
667     }
668 
669     /**
670         Create a new binding (without adding it) for a given node and name
671     */
672     ParameterBinding createBinding(Node n, string bindingName, bool setZero = true) {
673         ParameterBinding b;
674         if (bindingName == "deform") {
675             b = new DeformationParameterBinding(this, n, bindingName);
676         } else {
677             b = new ValueParameterBinding(this, n, bindingName);
678         }
679 
680         if (setZero) {
681             vec2u zeroIndex = findClosestKeypoint(vec2(0, 0));
682             vec2 zero = getKeypointValue(zeroIndex);
683             if (abs(zero.x) < 0.001 && abs(zero.y) < 0.001) b.reset(zeroIndex);
684         }
685 
686         return b;
687     }
688 
689     /**
690         Find a binding if it exists, or create and add a new one, and return it
691     */
692     ParameterBinding getOrAddBinding(Node n, string bindingName, bool setZero = true) {
693         ParameterBinding binding = getBinding(n, bindingName);
694         if (binding is null) {
695             binding = createBinding(n, bindingName, setZero);
696             addBinding(binding);
697         }
698         return binding;
699     }
700 
701     /**
702         Add a new binding (must not exist)
703     */
704     void addBinding(ParameterBinding binding) {
705         assert(!hasBinding(binding.getNode, binding.getName));
706         bindings ~= binding;
707     }
708 
709     /**
710         Remove an existing binding by ref
711     */
712     void removeBinding(ParameterBinding binding) {
713         import std.algorithm.searching : countUntil;
714         import std.algorithm.mutation : remove;
715         ptrdiff_t idx = bindings.countUntil(binding);
716         if (idx >= 0) {
717             bindings = bindings.remove(idx);
718         }
719     }
720 
721     void makeIndexable() {
722         import std.uni : toLower;
723         indexableName = name.toLower;
724     }
725 }
726 
727 private {
728     Parameter delegate(Fghj) createFunc;
729 }
730 
731 Parameter inParameterCreate(Fghj data) {
732     return createFunc(data);
733 }
734 
735 void inParameterSetFactory(Parameter delegate(Fghj) createFunc_) {
736     createFunc = createFunc_;
737 }