1 /*
2     Inochi2D Node
3 
4     Copyright © 2020, Inochi2D Project
5     Distributed under the 2-Clause BSD License, see LICENSE file.
6     
7     Authors: Luna Nielsen
8 */
9 module inochi2d.core.nodes;
10 import inochi2d.math;
11 import inochi2d.fmt.serialize;
12 import inochi2d.math.serialization;
13 import inochi2d.core.dbg;
14 import inochi2d.core;
15 
16 public import inochi2d.core.nodes.part;
17 public import inochi2d.core.nodes.mask;
18 public import inochi2d.core.nodes.pathdeform;
19 public import inochi2d.core.nodes.drawable;
20 public import inochi2d.core.nodes.composite;
21 public import inochi2d.core.nodes.drivers;
22 //public import inochi2d.core.nodes.shapes; // This isn't mainline yet!
23 
24 import std.exception;
25 
26 private {
27     uint[] takenUUIDs;
28 }
29 
30 package(inochi2d) {
31     void inInitNodes() {
32         inRegisterNodeType!Node;
33     }
34 }
35 
36 enum InInvalidUUID = uint.max;
37 
38 /**
39     Creates a new UUID for a node
40 */
41 uint inCreateUUID() {
42     import std.algorithm.searching : canFind;
43     import std.random : uniform;
44 
45     uint id = uniform(uint.min, InInvalidUUID);
46     while (takenUUIDs.canFind(id)) { id = uniform!uint(); } // Make sure the ID is actually unique in the current context
47 
48     return id;
49 }
50 
51 /**
52     Unloads a single UUID from the internal listing, freeing it up for reuse
53 */
54 void inUnloadUUID(uint id) {
55     import std.algorithm.searching : countUntil;
56     import std.algorithm.mutation : remove;
57     ptrdiff_t idx = takenUUIDs.countUntil(id);
58     if (idx != -1) takenUUIDs.remove(idx);
59 }
60 
61 /**
62     Clears all UUIDs from the internal listing
63 */
64 void inClearUUIDs() {
65     takenUUIDs.length = 0;
66 }
67 
68 /**
69     A node in the Inochi2D rendering tree
70 */
71 @TypeId("Node")
72 class Node : ISerializable {
73 private:
74     this() { }
75 
76     @Ignore
77     Puppet puppet_;
78 
79     @Ignore
80     Node parent_;
81     
82     @Ignore
83     Node[] children_;
84     
85     @Ignore
86     uint uuid_;
87     
88     @Name("zsort")
89     float zsort_ = 0;
90 
91     @Name("lockToRoot")
92     bool lockToRoot_;
93 
94 protected:
95 
96     /**
97         The offset to the transform to apply
98     */
99     Transform offsetTransform;
100 
101     /**
102         The offset to apply to sorting
103     */
104     float offsetSort = 0f;
105 
106     // Send mask reset request one node up
107     void resetMask() {
108         if (parent !is null) parent.resetMask();
109     }
110 
111     void serializeSelf(ref InochiSerializer serializer) {
112         
113         serializer.putKey("uuid");
114         serializer.putValue(uuid);
115         
116         serializer.putKey("name");
117         serializer.putValue(name);
118         
119         serializer.putKey("type");
120         serializer.putValue(typeId);
121         
122         serializer.putKey("enabled");
123         serializer.putValue(enabled);
124         
125         serializer.putKey("zsort");
126         serializer.putValue(zsort_);
127         
128         serializer.putKey("transform");
129         serializer.serializeValue(this.localTransform);
130         
131         serializer.putKey("lockToRoot");
132         serializer.serializeValue(this.lockToRoot_);
133         
134         if (children.length > 0) {
135             serializer.putKey("children");
136             auto childArray = serializer.arrayBegin();
137             foreach(child; children) {
138                 serializer.elemBegin;
139                 serializer.serializeValue(child);
140             }
141             serializer.arrayEnd(childArray);
142         }
143     }
144 
145     void serializeSelf(ref InochiSerializerCompact serializer) {
146         serializer.putKey("uuid");
147         serializer.putValue(uuid);
148         
149         serializer.putKey("name");
150         serializer.putValue(name);
151         
152         serializer.putKey("type");
153         serializer.putValue(typeId);
154         
155         serializer.putKey("enabled");
156         serializer.putValue(enabled);
157         
158         serializer.putKey("zsort");
159         serializer.putValue(zsort_);
160         
161         serializer.putKey("transform");
162         serializer.serializeValue(this.localTransform);
163         
164         serializer.putKey("lockToRoot");
165         serializer.serializeValue(this.lockToRoot_);
166         
167         if (children.length > 0) {
168             serializer.putKey("children");
169             auto childArray = serializer.arrayBegin();
170             foreach(child; children) {
171                 serializer.elemBegin;
172                 serializer.serializeValue(child);
173             }
174             serializer.arrayEnd(childArray);
175         }
176     }
177 
178 package(inochi2d):
179 
180     /**
181         Needed for deserialization
182     */
183     void setPuppet(Puppet puppet) {
184         this.puppet_ = puppet;
185     }
186 
187 public:
188 
189     /**
190         Whether the node is enabled
191     */
192     bool enabled = true;
193 
194     /**
195         Whether the node is enabled for rendering
196 
197         Disabled nodes will not be drawn.
198 
199         This happens recursively
200     */
201     bool renderEnabled() {
202         if (parent) return !parent.renderEnabled ? false : enabled;
203         return enabled;
204     }
205 
206     /**
207         Visual name of the node
208     */
209     string name = "Unnamed Node";
210 
211     /**
212         Returns the unique identifier for this node
213     */
214     uint uuid() {
215         return uuid_;
216     }
217 
218     /**
219         This node's type ID
220     */
221     string typeId() { return "Node"; }
222 
223     /**
224         Gets the relative Z sorting
225     */
226     float relZSort() {
227         return zsort_;
228     }
229 
230     /**
231         Gets the Z sorting
232     */
233     float zSort() {
234         return (parent !is null ? parent.zSort() : 0) + relZSort + offsetSort;
235     }
236 
237     /**
238         Sets the (relative) Z sorting
239     */
240     void zSort(float value) {
241         zsort_ = value;
242     }
243 
244     /**
245         Lock translation to root
246     */
247     ref bool lockToRoot() {
248         return lockToRoot_;
249     }
250 
251     /**
252         Lock translation to root
253     */
254     void lockToRoot(bool value) {
255         
256         // Automatically handle converting lock space and proper world space.
257         if (value && !lockToRoot_) {
258             localTransform.translation = transformNoLock().translation;
259         } else if (!value && lockToRoot_) {
260             localTransform.translation = localTransform.translation-parent.transformNoLock().translation;
261         }
262 
263         lockToRoot_ = value;
264     }
265 
266     /**
267         Constructs a new puppet root node
268     */
269     this(Puppet puppet) {
270         puppet_ = puppet;
271     }
272 
273     /**
274         Constructs a new node
275     */
276     this(Node parent = null) {
277         this(inCreateUUID(), parent);
278     }
279 
280     /**
281         Constructs a new node with an UUID
282     */
283     this(uint uuid, Node parent = null) {
284         this.parent = parent;
285         this.uuid_ = uuid;
286     }
287 
288     /**
289         The local transform of the node
290     */
291     Transform localTransform;
292 
293     /**
294         The cached world space transform of the node
295     */
296     @Ignore
297     Transform globalTransform;
298 
299     @Ignore
300     bool recalculateTransform = true;
301 
302     /**
303         The transform in world space
304     */
305     @Ignore
306     Transform transform(bool ignoreParam=false)() {
307         if (recalculateTransform) {
308             localTransform.update();
309             offsetTransform.update();
310 
311             static if (!ignoreParam) {
312                 if (lockToRoot_)
313                     globalTransform = offsetTransform * localTransform * puppet.root.localTransform;
314                 else if (parent !is null)
315                     globalTransform = offsetTransform * localTransform * parent.transform();
316                 else
317                     globalTransform = offsetTransform * localTransform;
318 
319                 recalculateTransform = false;
320             } else {
321 
322                 if (lockToRoot_)
323                     globalTransform = localTransform * puppet.root.localTransform;
324                 else if (parent !is null)
325                     globalTransform = localTransform * parent.transform();
326                 else
327                     globalTransform = localTransform;
328 
329                 recalculateTransform = false;
330             }
331         }
332 
333         return globalTransform;
334     }
335 
336     /**
337         The transform in world space without locking
338     */
339     @Ignore
340     Transform transformNoLock() {
341         localTransform.update();
342         
343         if (parent !is null) return localTransform * parent.transform();
344         return localTransform;
345     }
346 
347     /**
348         Gets a list of this node's children
349     */
350     final Node[] children() {
351         return children_;
352     }
353 
354     /**
355         Gets the parent of this node
356     */
357     final ref Node parent() {
358         return parent_;
359     }
360 
361     /**
362         The puppet this node is attached to
363     */
364     final Puppet puppet() {
365         return parent_ !is null ? parent_.puppet : puppet_;
366     }
367 
368     /**
369         Removes all children from this node
370     */
371     final void clearChildren() {
372         foreach(child; children_) {
373             child.parent_ = null;
374         }
375         this.children_ = [];
376     }
377 
378     /**
379         Adds a node as a child of this node.
380     */
381     final void addChild(Node child) {
382         child.parent = this;
383     }
384 
385     /**
386         Sets the parent of this node
387     */
388     final void parent(Node node) {
389         this.insertInto(node, OFFSET_END);
390     }
391 
392     final ptrdiff_t getIndexInParent() {
393         import std.algorithm.searching : countUntil;
394         return parent_.children_.countUntil(this);
395     }
396 
397     final ptrdiff_t getIndexInNode(Node n) {
398         import std.algorithm.searching : countUntil;
399         return n.children_.countUntil(this);
400     }
401 
402     enum OFFSET_START = size_t.min;
403     enum OFFSET_END = size_t.max;
404     final void insertInto(Node node, size_t offset) {
405         import std.algorithm.mutation : remove;
406         import std.algorithm.searching : countUntil;
407         
408         // Remove ourselves from our current parent if we are
409         // the child of one already.
410         if (parent_ !is null) {
411             
412             // Try to find ourselves in our parent
413             // note idx will be -1 if we can't be found
414             ptrdiff_t idx = parent_.children_.countUntil(this);
415             assert(idx >= 0, "Invalid parent-child relationship!");
416 
417             // Remove ourselves
418             parent_.children_ = parent_.children_.remove(idx);
419         }
420 
421         // If we want to become parentless we need to handle that
422         // seperately, as null parents have no children to update
423         if (node is null) {
424             this.parent_ = null;
425             return;
426         }
427 
428         // Update our relationship with our new parent
429         this.parent_ = node;
430 
431         // Update position
432         if (offset == OFFSET_START) {
433             this.parent_.children_ = this ~ this.parent_.children_;
434         } else if (offset == OFFSET_END || offset >= parent_.children_.length) {
435             this.parent_.children_ ~= this;
436         } else {
437             this.parent_.children_ = this.parent_.children_[0..offset] ~ this ~ this.parent_.children_[offset..$];
438         }
439         if (this.puppet !is null) this.puppet.rescanNodes();
440     }
441 
442     /**
443         Return whether this node supports a parameter
444     */
445     bool hasParam(string key) {
446         switch(key) {
447             case "zSort":
448             case "transform.t.x":
449             case "transform.t.y":
450             case "transform.t.z":
451             case "transform.r.x":
452             case "transform.r.y":
453             case "transform.r.z":
454             case "transform.s.x":
455             case "transform.s.y":
456                 return true;
457             default:
458                 return false;
459         }
460     }
461 
462     /**
463         Gets the default offset value
464     */
465     float getDefaultValue(string key) {
466         switch(key) {
467             case "zSort":
468             case "transform.t.x":
469             case "transform.t.y":
470             case "transform.t.z":
471             case "transform.r.x":
472             case "transform.r.y":
473             case "transform.r.z":
474                 return 0;
475             case "transform.s.x":
476             case "transform.s.y":
477                 return 1;
478             default:
479                 return float();
480         }
481     }
482 
483     /**
484         Sets offset value
485     */
486     bool setValue(string key, float value) {
487         switch(key) {
488             case "zSort":
489                 offsetSort += value;
490                 return true;
491             case "transform.t.x":
492                 offsetTransform.translation.x += value;
493                 transformChanged();
494                 return true;
495             case "transform.t.y":
496                 offsetTransform.translation.y += value;
497                 transformChanged();
498                 return true;
499             case "transform.t.z":
500                 offsetTransform.translation.z += value;
501                 transformChanged();
502                 return true;
503             case "transform.r.x":
504                 offsetTransform.rotation.x += value;
505                 transformChanged();
506                 return true;
507             case "transform.r.y":
508                 offsetTransform.rotation.y += value;
509                 transformChanged();
510                 return true;
511             case "transform.r.z":
512                 offsetTransform.rotation.z += value;
513                 transformChanged();
514                 return true;
515             case "transform.s.x":
516                 offsetTransform.scale.x *= value;
517                 transformChanged();
518                 return true;
519             case "transform.s.y":
520                 offsetTransform.scale.y *= value;
521                 transformChanged();
522                 return true;
523             default: return false;
524         }
525     }
526 
527     /**
528         Scale an offset value, given an axis and a scale
529 
530         If axis is -1, apply magnitude and sign to signed properties.
531         If axis is 0 or 1, apply magnitude only unless the property is
532         signed and aligned with that axis.
533 
534         Note that scale adjustments are not considered aligned,
535         since we consider preserving aspect ratio to be the user
536         intent by default.
537     */
538     float scaleValue(string key, float value, int axis, float scale) {
539         if (axis == -1) return value * scale;
540 
541         float newVal = abs(scale) * value;
542         switch(key) {
543             case "transform.r.z": // Z-rotation is XY-mirroring
544                 newVal = scale * value;
545                 break;
546             case "transform.r.y": // Y-rotation is X-mirroring
547             case "transform.t.x":
548                 if (axis == 0) newVal = scale * value;
549                 break;
550             case "transform.r.x": // X-rotation is Y-mirroring
551             case "transform.t.y":
552                 if (axis == 1) newVal = scale * value;
553                 break;
554             default:
555                 break;
556         }
557         return newVal;
558     }
559 
560     /**
561         Draws this node and it's subnodes
562     */
563     void draw() {
564         if (!renderEnabled) return;
565 
566         foreach(child; children) {
567             child.draw();
568         }
569     }
570 
571     /**
572         Draws this node.
573     */
574     void drawOne() { }
575 
576 
577     /**
578         Finalizes this node and any children
579     */
580     void finalize() {
581         foreach(child; children) {
582             child.finalize();
583         }
584     }
585 
586     void beginUpdate() {
587         offsetSort = 0;
588         offsetTransform.clear();
589 
590         // Iterate through children
591         foreach(child; children_) {
592             child.beginUpdate();
593         }
594     }
595 
596     /**
597         Updates the node
598     */
599     void update() {
600         if (!enabled) return;
601 
602         foreach(child; children) {
603             child.update();
604         }
605     }
606 
607     /**
608         Marks this node's transform (and its descendents') as dirty
609     */
610     void transformChanged() {
611         recalculateTransform = true;
612 
613         foreach(child; children) {
614             child.transformChanged();
615         }
616     }
617 
618     override
619     string toString() {
620         return name;
621     }
622 
623     /**
624         Allows serializing a node (with pretty serializer)
625     */
626     void serialize(S)(ref S serializer) {
627         auto state = serializer.objectBegin();
628             this.serializeSelf(serializer);
629         serializer.objectEnd(state);
630     }
631 
632     /**
633         Deserializes node from Fghj formatted JSON data.
634     */
635     SerdeException deserializeFromFghj(Fghj data) {
636 
637         if (auto exc = data["uuid"].deserializeValue(this.uuid_)) return exc;
638 
639         if (!data["name"].isEmpty) {
640             if (auto exc = data["name"].deserializeValue(this.name)) return exc;
641         }
642 
643         if (auto exc = data["enabled"].deserializeValue(this.enabled)) return exc;
644 
645         if (auto exc = data["zsort"].deserializeValue(this.zsort_)) return exc;
646         
647         if (auto exc = data["transform"].deserializeValue(this.localTransform)) return exc;
648         
649         if (!data["lockToRoot"].isEmpty) {
650             if (auto exc = data["lockToRoot"].deserializeValue(this.lockToRoot_)) return exc;
651         }
652 
653         // Pre-populate our children with the correct types
654         foreach(child; data["children"].byElement) {
655             
656             // Fetch type from json
657             string type;
658             if (auto exc = child["type"].deserializeValue(type)) return exc;
659             
660             // Skips unknown node types
661             // TODO: A logging system that shows a warning for this?
662             if (!inHasNodeType(type)) continue;
663 
664             // instantiate it
665             Node n = inInstantiateNode(type, this);
666             if (auto exc = child.deserializeValue(n)) return exc;
667         }
668 
669 
670         return null;
671     }
672 
673     /**
674         Force sets the node's ID
675 
676         THIS IS NOT A SAFE OPERATION.
677     */
678     final void forceSetUUID(uint uuid) {
679         this.uuid_ = uuid;
680     }
681 
682     /**
683         Gets the combined bounds of the node
684     */
685     vec4 getCombinedBounds() {
686         auto tr = transform;
687         vec4 combined = vec4(tr.translation.x, tr.translation.y, tr.translation.x, tr.translation.y);
688         
689         // Get Bounds as drawable
690         if (Drawable drawable = cast(Drawable)this) {
691             combined = drawable.bounds;
692         }
693 
694         foreach(child; children) {
695             vec4 cbounds = child.getCombinedBounds();
696             if (cbounds.x < combined.x) combined.x = cbounds.x;
697             if (cbounds.y < combined.y) combined.y = cbounds.y;
698             if (cbounds.z > combined.z) combined.z = cbounds.z;
699             if (cbounds.w > combined.w) combined.w = cbounds.w;
700         }
701 
702         return combined;
703     }
704 
705     /**
706         Gets whether nodes can be reparented
707     */
708     bool canReparent(Node to) {
709         Node tmp = to;
710         while(tmp !is null) {
711             if (tmp.uuid == this.uuid) return false;
712             
713             // Check next up
714             tmp = tmp.parent;
715         }
716         return true;
717     }
718 
719     /**
720         Draws orientation of the node
721     */
722     void drawOrientation() {
723         auto trans = transform.matrix();
724         inDbgLineWidth(4);
725 
726         // X
727         inDbgSetBuffer([vec3(0, 0, 0), vec3(32, 0, 0)], [0, 1]);
728         inDbgDrawLines(vec4(1, 0, 0, 0.7), trans);
729 
730         // Y
731         inDbgSetBuffer([vec3(0, 0, 0), vec3(0, -32, 0)], [0, 1]);
732         inDbgDrawLines(vec4(0, 1, 0, 0.7), trans);
733         
734         // Z
735         inDbgSetBuffer([vec3(0, 0, 0), vec3(0, 0, -32)], [0, 1]);
736         inDbgDrawLines(vec4(0, 0, 1, 0.7), trans);
737 
738         inDbgLineWidth(1);
739     }
740 
741     /**
742         Draws bounds
743     */
744     void drawBounds() {
745         vec4 bounds = this.getCombinedBounds;
746 
747         float width = bounds.z-bounds.x;
748         float height = bounds.w-bounds.y;
749         inDbgSetBuffer([
750             vec3(bounds.x, bounds.y, 0),
751             vec3(bounds.x + width, bounds.y, 0),
752             
753             vec3(bounds.x + width, bounds.y, 0),
754             vec3(bounds.x + width, bounds.y+height, 0),
755             
756             vec3(bounds.x + width, bounds.y+height, 0),
757             vec3(bounds.x, bounds.y+height, 0),
758             
759             vec3(bounds.x, bounds.y+height, 0),
760             vec3(bounds.x, bounds.y, 0),
761         ]);
762         inDbgLineWidth(3);
763         inDbgDrawLines(vec4(.5, .5, .5, 1));
764         inDbgLineWidth(1);
765     }
766 }
767 
768 //
769 //  SERIALIZATION SHENNANIGANS
770 //
771 private {
772     Node delegate(Node parent)[string] typeFactories;
773 
774     Node inInstantiateNode(string id, Node parent = null) {
775         return typeFactories[id](parent);
776     }
777 }
778 
779 void inRegisterNodeType(T)() if (is(T : Node)) {
780     import std.traits : getUDAs;
781     typeFactories[getUDAs!(T, TypeId)[0].id] = (Node parent) {
782         return new T(parent);
783     };
784 }
785 
786 /**
787     Gets whether a node type is present in the factories
788 */
789 bool inHasNodeType(string id) {
790     return (id in typeFactories) !is null;
791 }
792 
793 mixin template InNode(T) {
794     static this() {
795         inRegisterNodeType!(T);
796     }
797 }