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 }