1 /**
2     Root Puppet Object
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Luna Nielsen
12 */
13 module inochi2d.puppet;
14 import inochi2d.common;
15 import inochi2d.nodes;
16 import inochi2d.param;
17 import inochi2d.animation;
18 import inochi2d.core;
19 import inp.format;
20 import nulib.io.stream;
21 import nulib;
22 import numem;
23 
24 // NOTE:    Puppet has some legacy functionality, this allows turning that off.
25 version (IN_NO_LEGACY) {
26 } else {
27     pragma(msg, "WARNING: Legacy features are enabled, these may be removed in future updates.");
28     version = IN_LEGACY;
29 }
30 
31 /**
32     Puppet properties
33 */
34 class PuppetProperties : NuObject, ISerializable, IDeserializable!ModelState {
35 public:
36 
37     /**
38         Parent puppet object
39     */
40     Puppet parent;
41 
42     /**
43         Name of the puppet
44     */
45     nstring name;
46 
47     /**
48         Author of the puppet
49     */
50     nstring author;
51 
52     /**
53         Thumbnail of the puppet.
54     */
55     Texture thumbnail;
56 
57     /**
58         Pixels-per-meter for the physics system
59     */
60     float physicsPixelsPerMeter = 1000;
61 
62     /**
63         Gravity for the physics system
64     */
65     float physicsGravity = 9.8;
66 
67     /**
68         Whether the puppet should preserve pixel borders.
69         This feature is mainly useful for puppets which use pixel art.
70     */
71     bool graphicsUsePointFiltering = false;
72 
73     /**
74         Constructs a new properties object.
75     */
76     this(Puppet puppet) @nogc {
77         this.parent = puppet;
78     }
79 
80     /**
81         Serializes the type.
82     */
83     void onSerialize(ref DataNode object) @nogc {
84 
85         // General Properties.
86         object["name"] = name[];
87         object["author"] = author[];
88         object["thumbnail"] = parent.textureCache.find(thumbnail);
89 
90         // Physics properties.
91         object["physicsPixelsPerMeter"] = physicsPixelsPerMeter;
92         object["physicsGravity"] = physicsGravity;
93 
94         // Graphics properties
95         object["graphicsUsePointFiltering"] = graphicsUsePointFiltering;
96     }
97 
98     /**
99         Deserializes the type.
100     */
101     void onDeserialize(ref DataNode object, ref ModelState state) @nogc {
102 
103         // 0.8 backwards compatibility.
104         object.tryGetRef(state, author, "rigger", author);
105         object.tryGetRef(state, author, "artist", author);
106 
107         object.tryGetRef(state, name, "name");
108         object.tryGetRef(state, author, "author", author);
109         object.tryGetRef(state, physicsPixelsPerMeter, "pixelsPerMeter");
110         object.tryGetRef(state, physicsGravity, "gravity");
111     }
112 }
113 
114 /**
115     A puppet
116 */
117 class Puppet : NuRefCounted, ISerializable, IDeserializable!ModelState {
118 private:
119 @nogc:
120 
121     // The drawlist that the puppet passes to its nodes.
122     DrawList drawList_;
123 
124     // A list of parts that are not masked by other parts
125     weak_vector!Visual visuals_;
126 
127     // A list of parameters attached to the puppet.
128     vector!Parameter parameters_;
129 
130     // A dictionary of named animations
131     vector!Animation animations_;
132 
133     // Extended Vendor Data
134     weak_map!(string, ubyte[]) vendorData_;
135 
136     //
137     //          LEGACY CONSTRUCTS
138     //
139     version (IN_LEGACY) {
140 
141         // A list of parameters that are driven by drivers
142         weak_map!(Parameter, SimplePhysics) driven_;
143 
144         // A list of drivers that need to run to update the puppet
145         SimplePhysics[] drivers_;
146     }
147 
148     void scanParts(ref Node node) {
149         node.findVisuals(visuals_);
150 
151         // Legacy physics system.
152         version (IN_LEGACY) {
153             node.findNodes!SimplePhysics(drivers_);
154             driven_.clear();
155             foreach (driver; drivers_) {
156                 foreach (Parameter param; driver.affectedParameters)
157                     driven_[param] = driver;
158             }
159         }
160     }
161 
162     Node findNode(Node n, string name) @nogc {
163 
164         // Name matches!
165         if (n.name == name)
166             return n;
167 
168         // Recurse through children
169         foreach (child; n.children) {
170             if (Node c = findNode(child, name))
171                 return c;
172         }
173 
174         // Not found
175         return null;
176     }
177 
178     Node findNode(Node n, GUID guid) @nogc {
179 
180         // Name matches!
181         if (n.guid == guid)
182             return n;
183 
184         // Recurse through children
185         foreach (child; n.children) {
186             if (Node c = findNode(child, guid))
187                 return c;
188         }
189 
190         // Not found
191         return null;
192     }
193 
194     /// Loads textures from a DataNode into the texture cache.
195     void loadTextures(ref DataNode node) {
196         assert(textureCache !is null, "Texture cache is invalid!");
197         assert(node.isArray, "Not a texture cache array!");
198 
199         texLoadLoop: foreach (i, ref DataNode texture; node.array) {
200             TextureData textureData;
201 
202             // Skip invalid texture indices.
203             if (!texture.isObject || "encoding" !in texture || "data" !in texture)
204                 continue;
205 
206             uint encoding = texture["encoding"].tryCoerce!int(-1);
207             ubyte[] data = texture["data"].blob;
208 
209             // Invalid data?
210             if (encoding == -1 || data.length == 0)
211                 continue;
212 
213             // Handle different encodings.
214             switch (encoding) {
215             case INP_TEX_FMT_PNG:
216             case INP_TEX_FMT_TGA:
217                 textureData = TextureData.load(data);
218                 break;
219 
220             case INP_TEX_FMT_BC7:
221                 assert(0, "BC7 not implemented yet!");
222                 continue texLoadLoop;
223 
224             default:
225                 // Unknown format.
226                 continue texLoadLoop;
227             }
228             textureCache.insert(Texture.createForData(textureData.move()), i);
229         }
230     }
231 
232 protected:
233 
234     /**
235         Serializes a puppet into an existing object.
236     */
237     void onSerialize(ref DataNode object) @nogc {
238         object["properties"] = properties.serialize();
239         object["nodes"] = root.serialize();
240         object["param"] = parameters_.serialize();
241         object["animations"] = animations_.serialize();
242     }
243 
244     /**
245         Deserializes a puppet
246     */
247     void onDeserialize(ref DataNode object, ref ModelState state) @nogc {
248 
249         // Invalid type.
250         if (!object.isObject)
251             return;
252 
253         // Just set to basic initialized object if none was found.
254         object.tryGetRef(state, properties, "properties", properties);
255 
256         // Legacy "meta" key.
257         if ("meta" in object) {
258             object.tryGetRef(state, properties, "meta", properties);
259             object["meta"].tryGetRef(state, properties.graphicsUsePointFiltering, "preservePixels");
260         }
261 
262         // Legacy "physics" key.
263         if ("physics" in object) {
264             object["physics"].tryGetRef(state, properties.physicsPixelsPerMeter, "pixelsPerMeter");
265             object["physics"].tryGetRef(state, properties.physicsGravity, "gravity");
266         }
267 
268         // Add root node if it was found.
269         if ("nodes" in object) {
270             object.tryGetRef(state, root, "nodes", root);
271         }
272 
273         if (auto params = "param" in object) {
274             if ((*params).isArray) {
275                 parameters_.resize((*params).length);
276                 foreach (i, ref param; (*params).array) {
277                     parameters_[i] = param.tryDeserializeParam(state);
278                 }
279             }
280         }
281 
282         if (auto anim = "animation" in object) {
283             (*anim).deserialize(animations_, state);
284         }
285     }
286 
287     void onFinalize(ref ModelState state) @nogc {
288 
289         // Finally update link etc.
290         this.root.finalize(state);
291         this.root.updateTransform();
292 
293         foreach (parameter; parameters_) {
294             parameter.finalize(this, state);
295         }
296 
297         foreach (ref animation; animations_) {
298             animation.finalize(this);
299         }
300         this.rescanNodes();
301     }
302 
303 public:
304 
305     /**
306         Properties for the puppet.
307     */
308     PuppetProperties properties;
309 
310     /**
311         The root node of the puppet
312     */
313     Node root;
314 
315     /**
316         INP Texture slots for this puppet
317     */
318     TextureCache textureCache;
319 
320     /**
321         Extended vendor data
322     */
323     ubyte[][string] extData;
324 
325     /**
326         Whether parameters should be rendered
327     */
328     bool renderParameters = true;
329 
330     /**
331         Whether drivers should run
332     */
333     version (IN_LEGACY) bool enableDrivers = true;
334 
335     /**
336         Puppet render transform
337 
338         This transform does not affect physics
339     */
340     Transform transform;
341 
342     /**
343         The active draw list for the puppet.
344     */
345     final @property DrawList drawList() @nogc => drawList_;
346 
347     /**
348         A read-only slice of the root visuals being rendered.
349     */
350     final @property Visual[] visuals() => visuals_;
351 
352     /**
353         A read-only slice of drivers
354     */
355     version (IN_LEGACY) final @property SimplePhysics[] drivers() => drivers_;
356 
357     /**
358         A read-only slice of animations attached to this puppet.
359     */
360     final @property Animation[] animations() => animations_[];
361 
362     /**
363         A read-only slice of animations attached to this puppet.
364     */
365     final @property Parameter[] parameters() => parameters_[];
366 
367     // Destructor
368     ~this() {
369         nogc_delete(properties);
370         nogc_delete(drawList_);
371         nogc_delete(textureCache);
372     }
373 
374     /**
375         Constructs a new, empty puppet.
376 
377         Params:
378             cache = The texture cache to use during construction.
379             root =  The node to put as the root of the puppet.
380     */
381     this(TextureCache cache = null, Node root = null) {
382         this.properties = nogc_new!PuppetProperties(this);
383         this.textureCache = cache ? cache : nogc_new!TextureCache();
384         this.drawList_ = nogc_new!DrawList();
385 
386         // Setup root node
387         this.root = root ? root : nogc_new!Node(this);
388         this.root.setPuppet(this);
389         this.root.name = "Root";
390     }
391 
392     /**
393         Creates a new puppet from a node tree
394     */
395     this(Node root) {
396         this(null, root);
397     }
398 
399     version (WebAssembly) {
400     } else {
401 
402         /**
403             Loads a $(D Puppet) from a file.
404 
405             Params:
406                 path =  Path to the file to load.
407                 sink =  A sink to write logs to.
408             
409             Notes:
410                 Not available when compiling for WebAssembly.
411         */
412         static Result!Puppet fromFile(string path, IOSink sink = IOSink.init) @nogc {
413             import nulib.io.stream.file : FileStream;
414 
415             if (FileStream fstream = nogc_new!FileStream(path, "r+b")) {
416                 return Puppet.fromStream(fstream, sink);
417             }
418             return error!Puppet("Could not open file.");
419         }
420     }
421 
422     /**
423         Loads a $(D Puppet) from a Stream.
424 
425         Params:
426             stream =    The readable stream to load the puppet from.
427             sink =      A sink to write logs to.
428     */
429     static Result!Puppet fromStream(Stream stream, IOSink sink = IOSink.init) @nogc {
430         assert(stream);
431         assert(stream.canRead);
432 
433         // Set up model state.
434         ModelState state;
435         state.io = sink;
436 
437         // Identify Inochi2D 0.8 models.
438         INPFileFormat fileFormat = stream.detectFormat();
439         if (fileFormat == INPFileFormat.inp1) {
440             state.doUpgrade08 = true;
441             state.version_ = IN_MAKE_VERSION!(0, 8, 6);
442             state.warning("Inochi2D 0.8's file format is deprecated, it is recommended that you upgrade to Inochi2D 0.9.");
443         }
444 
445         auto result = stream.readINP();
446         if (!result)
447             return error!Puppet(result.error);
448 
449         DataNode node = result.get();
450         if (INP_TAG_PAYLOAD !in node)
451             return error!Puppet("No payload was found in the model!");
452 
453         // Create new puppet and deserialize the data.
454         Puppet puppet = nogc_new!Puppet(nogc_new!TextureCache());
455         puppet.deserialize(node, state);
456         return ok(puppet);
457     }
458 
459     /**
460         Loads a $(D Puppet) from a Stream.
461 
462         Params:
463             stream = The readable stream to load the puppet from.
464     */
465     final bool toStream(Stream stream) @nogc {
466         assert(stream);
467         assert(stream.canWrite);
468 
469         // Prepare data node.
470         DataNode data = DataNode.createObject();
471         data[INP_TAG_PAYLOAD] = DataNode.createObject();
472         data[INP_TAG_TEXTURES] = DataNode.createArray();
473 
474         // Serialize data
475         return false;
476     }
477 
478     /**
479         Serializes a puppet.
480 
481         Params:
482             node =  The payload DataNode to deserialize from.
483     */
484     final void serialize(ref DataNode node) {
485         assert(node.isObject, "Target DataNode was not an Object!");
486         node[INP_TAG_PAYLOAD] = DataNode.createObject();
487         this.onSerialize(node[INP_TAG_PAYLOAD]);
488     }
489 
490     /**
491         Deserializes a Puppet from a payload $(D DataNode).
492 
493         Params:
494             node =  The DataNode to deserialize from.
495             state = The state of the deserializer.
496     */
497     final void deserialize(ref DataNode node, ref ModelState state) @nogc {
498         assert(INP_TAG_PAYLOAD in node, "No payload was found!");
499         assert(node[INP_TAG_PAYLOAD].isObject, "Invalid payload object.");
500 
501         // NOTE:    Deserialization happens in multiple steps,
502         //          1. Load textures from TEX_SECT, assigning texture IDs.
503         //          2. Deserialize payload, (this MUST be present.)
504         //          3. Finalize any data.
505         if (INP_TAG_TEXTURES in node) {
506             this.loadTextures(node[INP_TAG_TEXTURES]);
507         }
508 
509         this.onDeserialize(node[INP_TAG_PAYLOAD], state);
510         this.onFinalize(state);
511     }
512 
513     /**
514         Updates the nodes
515     */
516     final void update(float delta) {
517         drawList_.clear();
518         root.preUpdate(drawList_);
519 
520         // Update parameters
521         if (renderParameters) {
522             foreach (parameter; parameters_) {
523                 parameter.update();
524             }
525         }
526 
527         // Ensure the transform tree is updated
528         root.updateTransform();
529 
530         version (IN_LEGACY) {
531             if (renderParameters && enableDrivers) {
532                 // Update parameter/node driver nodes (e.g. physics)
533                 foreach (driver; drivers_) {
534                     driver.updateDriver(delta);
535                 }
536             }
537 
538             // Update again after drivers.
539             root.updateTransform();
540         }
541 
542         // Update nodes
543         root.update(delta, drawList_);
544         root.postUpdate(drawList_);
545     }
546 
547     /**
548         Reset drivers/physics nodes
549     */
550     version (IN_LEGACY) final void resetDrivers() @nogc {
551         foreach (driver; drivers_) {
552             driver.reset();
553         }
554     }
555 
556     /**
557         Returns the index of a parameter by name
558     */
559     ptrdiff_t findParameterIndex(string name) @nogc {
560         foreach (i, parameter; parameters_) {
561             if (parameter.name == name) {
562                 return i;
563             }
564         }
565         return -1;
566     }
567 
568     /**
569         Returns a parameter by GUID
570     */
571     Parameter findParameter(GUID guid) @nogc {
572         foreach (i, parameter; parameters_) {
573             if (parameter.guid == guid) {
574                 return parameter;
575             }
576         }
577         return null;
578     }
579 
580     /**
581         Gets if a node is bound to ANY parameter.
582     */
583     bool getIsNodeBound(Node node) {
584         foreach (i, parameter; parameters_) {
585             if (parameter.hasAnyBindingsTo(node))
586                 return true;
587         }
588         return false;
589     }
590 
591     /**
592         Draws the puppet
593     */
594     final void draw(float delta) {
595         sortNodes(visuals_);
596 
597         foreach (visual; visuals_) {
598             if (!visual.enabled)
599                 continue;
600 
601             visual.draw(delta, drawList_);
602         }
603     }
604 
605     /**
606         Removes a parameter from this puppet
607     */
608     void addParameter(Parameter param) {
609         parameters_ ~= param;
610     }
611 
612     /**
613         Removes a parameter from this puppet
614     */
615     void removeParameter(Parameter param) {
616         parameters_.remove(param);
617     }
618 
619     /**
620         Rescans the puppet's nodes
621 
622         Run this every time you change the layout of the puppet's node tree
623     */
624     final void rescanNodes() {
625         this.scanParts(root);
626     }
627 
628     /**
629         Finds Node by its name
630     */
631     T find(T = Node)(string name) @nogc
632     if (is(T : Node)) {
633         return cast(T)findNode(root, name);
634     }
635 
636     /**
637         Finds Node by its unique id
638     */
639     T find(T = Node)(GUID guid) @nogc
640     if (is(T : Node)) {
641         return cast(T)findNode(root, guid);
642     }
643 
644     /**
645         Adds a texture to a new slot if it doesn't already exist within this puppet
646     */
647     final uint addTextureToSlot(Texture texture) @nogc {
648         return textureCache.add(texture);
649     }
650 
651     /**
652         Sets thumbnail of this puppet
653     */
654     final void setThumbnail(Texture texture) @nogc {
655         textureCache.add(texture);
656         this.properties.thumbnail = texture;
657     }
658 
659     /**
660         Gets the texture slot index for a texture
661 
662         returns -1 if none was found
663     */
664     final ptrdiff_t getTextureSlotIndexFor(Texture texture) @nogc {
665         return textureCache.find(texture);
666     }
667 
668     /**
669         Gets the combined bounds of the puppet
670     */
671     vec4 getCombinedBounds(bool reupdate = false)() {
672         return root.getCombinedBounds!(reupdate, true);
673     }
674 }