unitytk 0.0.15__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {unitytk-0.0.15 → unitytk-0.2.0}/MANIFEST.in +1 -1
  2. {unitytk-0.0.15/unitytk.egg-info → unitytk-0.2.0}/PKG-INFO +2 -2
  3. {unitytk-0.0.15 → unitytk-0.2.0}/pyproject.toml +12 -7
  4. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_articulated_rig_runtime.py +399 -1
  5. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_asset_delivery.py +190 -9
  6. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_audio_events_integration.py +11 -16
  7. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_base_test.py +194 -0
  8. unitytk-0.2.0/test/test_blender_render_effects_e2e.py +496 -0
  9. unitytk-0.2.0/test/test_blender_shadow_plane_integration.py +875 -0
  10. unitytk-0.2.0/test/test_blender_shot_metadata_integration.py +472 -0
  11. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_clip_name_contract_integration.py +10 -15
  12. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_controller_runtime_integration.py +1 -16
  13. unitytk-0.2.0/test/test_deploy_templates.py +1150 -0
  14. unitytk-0.2.0/test/test_embedded_dependencies_integration.py +461 -0
  15. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_emissive_groups_integration.py +5 -16
  16. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_fade_window_preservation.py +12 -6
  17. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_keyed_visibility.py +17 -15
  18. unitytk-0.2.0/test/test_lightmap_metadata_integration.py +1321 -0
  19. unitytk-0.2.0/test/test_maya_surface_materials_integration.py +1027 -0
  20. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_multi_section_audio_integration.py +16 -9
  21. unitytk-0.2.0/test/test_render_effects_e2e.py +1002 -0
  22. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_controller.py +12 -13
  23. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_e2e.py +22 -22
  24. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_integration.py +14 -14
  25. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_run_tests.py +149 -1
  26. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_scene_builder.py +1 -1
  27. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shadow_plane_integration.py +88 -6
  28. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shadow_plane_runtime_integration.py +510 -4
  29. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shared_material_standalone.py +10 -13
  30. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shot_metadata_integration.py +345 -45
  31. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shots_audio_sidebyside_integration.py +142 -16
  32. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_standalone_enduser.py +14 -28
  33. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/__init__.py +1 -1
  34. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/asset_delivery.py +56 -1
  35. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/template_deployer.py +419 -137
  36. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ArticulatedRig}/ArticulatedRigController.cs +253 -100
  37. unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +11 -0
  38. unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +11 -0
  39. unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +199 -0
  40. unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +11 -0
  41. unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +11 -0
  42. unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +11 -0
  43. unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +7 -0
  44. unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR.meta +8 -0
  45. unitytk-0.2.0/unitytk/templates/ArticulatedRig.meta +8 -0
  46. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/AudioEvent}/AudioEventController.cs +176 -2
  47. unitytk-0.2.0/unitytk/templates/AudioEvent/AudioEventController.cs.meta +11 -0
  48. unitytk-0.2.0/unitytk/templates/AudioEvent.meta +8 -0
  49. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroupController.cs +1 -1
  50. unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +11 -0
  51. unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +7 -0
  52. unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +9 -0
  53. unitytk-0.2.0/unitytk/templates/EmissiveGroup.meta +8 -0
  54. unitytk-0.2.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +1465 -0
  55. unitytk-0.2.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +11 -0
  56. unitytk-0.2.0/unitytk/templates/LightmapMetadata.meta +8 -0
  57. unitytk-0.2.0/unitytk/templates/M3trik.Unitytk.asmdef.meta +7 -0
  58. unitytk-0.2.0/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs +763 -0
  59. unitytk-0.2.0/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta +11 -0
  60. unitytk-0.2.0/unitytk/templates/MayaSurface.meta +8 -0
  61. unitytk-0.2.0/unitytk/templates/README.md +350 -0
  62. unitytk-0.2.0/unitytk/templates/README.md.meta +7 -0
  63. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/RenderEffects}/RenderEffectsController.cs +495 -50
  64. unitytk-0.2.0/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +11 -0
  65. unitytk-0.2.0/unitytk/templates/RenderEffects.meta +8 -0
  66. unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlane.shader +319 -0
  67. unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +9 -0
  68. unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +1949 -0
  69. unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +11 -0
  70. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ShadowPlane}/ShadowPlaneHorizon.hlsl +157 -63
  71. unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +7 -0
  72. unitytk-0.2.0/unitytk/templates/ShadowPlane.meta +8 -0
  73. unitytk-0.2.0/unitytk/templates/ShotMetadata/ShotMetadataController.cs +309 -0
  74. unitytk-0.2.0/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +11 -0
  75. unitytk-0.2.0/unitytk/templates/ShotMetadata.meta +8 -0
  76. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/UnitytkSettings.cs +118 -15
  77. unitytk-0.2.0/unitytk/templates/UnitytkSettings.cs.meta +11 -0
  78. {unitytk-0.0.15 → unitytk-0.2.0/unitytk.egg-info}/PKG-INFO +2 -2
  79. unitytk-0.2.0/unitytk.egg-info/SOURCES.txt +96 -0
  80. unitytk-0.2.0/unitytk.egg-info/requires.txt +1 -0
  81. unitytk-0.0.15/test/test_deploy_templates.py +0 -620
  82. unitytk-0.0.15/test/test_lightmap_metadata_integration.py +0 -588
  83. unitytk-0.0.15/test/test_render_effects_e2e.py +0 -420
  84. unitytk-0.0.15/unitytk/templates/LightmapMetadataController.cs +0 -551
  85. unitytk-0.0.15/unitytk/templates/README.md +0 -249
  86. unitytk-0.0.15/unitytk/templates/ShadowPlane.shader +0 -223
  87. unitytk-0.0.15/unitytk/templates/ShadowPlaneController.cs +0 -1201
  88. unitytk-0.0.15/unitytk/templates/ShotMetadataController.cs +0 -177
  89. unitytk-0.0.15/unitytk.egg-info/SOURCES.txt +0 -60
  90. unitytk-0.0.15/unitytk.egg-info/requires.txt +0 -1
  91. {unitytk-0.0.15 → unitytk-0.2.0}/LICENSE +0 -0
  92. {unitytk-0.0.15 → unitytk-0.2.0}/docs/README.md +0 -0
  93. {unitytk-0.0.15 → unitytk-0.2.0}/setup.cfg +0 -0
  94. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_bool_to_fade_curve.py +0 -0
  95. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_launcher.py +0 -0
  96. {unitytk-0.0.15 → unitytk-0.2.0}/test/test_temp_projects.py +0 -0
  97. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/launcher.py +0 -0
  98. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/scene_builder.py +0 -0
  99. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ArticulatedRig}/ArticulatedRigPointerGrab.cs +0 -0
  100. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRGrab.cs +0 -0
  101. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRSetup.cs +0 -0
  102. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/M3trik.Unitytk.XR.asmdef +0 -0
  103. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroups.hlsl +0 -0
  104. {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroupsLit.shader +0 -0
  105. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  106. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/__init__.py +0 -0
  107. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk.egg-info/dependency_links.txt +0 -0
  108. {unitytk-0.0.15 → unitytk-0.2.0}/unitytk.egg-info/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  include LICENSE
2
2
  include docs/README.md
3
3
  include pyproject.toml
4
- recursive-include unitytk *.ui *.gif *.png *.json *.txt *.cs *.hlsl *.shader *.asmdef
4
+ recursive-include unitytk *.ui *.gif *.png *.json *.txt *.cs *.hlsl *.shader *.asmdef *.meta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.15
3
+ Version: 0.2.0
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -13,7 +13,7 @@ Classifier: Operating System :: POSIX :: Linux
13
13
  Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: pythontk>=0.12.0
16
+ Requires-Dist: pythontk>=0.13.1
17
17
  Dynamic: license-file
18
18
 
19
19
  [![Tests](https://img.shields.io/badge/Tests-146%20passed-brightgreen.svg)](../test/)
@@ -17,12 +17,17 @@ classifiers = [
17
17
  ]
18
18
  # The floor is HAND-RATCHETED: unitytk is off-chain from m3trik/push.ps1, so no
19
19
  # release cascade bumps it.
20
- # pythontk 0.12.0, the release this one ships and was tested with. The hard
21
- # minimum is 0.8.84: UnityFinder.find_editors scans through
22
- # `AppLauncher.scan_install_dirs` (new in 0.8.72) and needs its pattern order to
23
- # be a priority (0.8.84), so the Hub's chosen install location wins over the
24
- # default. Below 0.8.72 the scan fails, is logged, and no editor is found.
25
- dependencies = ["pythontk>=0.12.0"]
20
+ # pythontk 0.13.1, the release this one ships and was tested with: an FBX
21
+ # deliverable carries the files its records name through
22
+ # `FbxMedia.embed_dependencies` (new in 0.13.1; on 0.13.0 the copy deliverer
23
+ # ships the FBX without its maps), and the suite holds the templates to its
24
+ # `Conformance` cases and its articulation solve's halving step (0.13.0). The hard minimum is 0.11.3:
25
+ # TemplateDeployer builds its deprecation notices at import through
26
+ # `Deprecation.values(..., since=)` (new in 0.11.3). UnityFinder.find_editors
27
+ # needs 0.8.84: it scans through `AppLauncher.scan_install_dirs` (new in
28
+ # 0.8.72) and needs its pattern order to be a priority (0.8.84), so the Hub's
29
+ # chosen install location wins over the default.
30
+ dependencies = ["pythontk>=0.13.1"]
26
31
 
27
32
  [project.urls]
28
33
  Homepage = "https://github.com/m3trik/unitytk"
@@ -37,7 +42,7 @@ where = ["."]
37
42
  include = ["unitytk*"]
38
43
 
39
44
  [tool.setuptools.package-data]
40
- "*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs", "*.hlsl", "*.shader", "*.asmdef"]
45
+ "*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs", "*.hlsl", "*.shader", "*.asmdef", "*.meta"]
41
46
 
42
47
  [tool.pytest.ini_options]
43
48
  testpaths = ["test"]
@@ -58,6 +58,7 @@ import os
58
58
  import random
59
59
  import re
60
60
  import shutil
61
+ import struct
61
62
  import subprocess
62
63
  import unittest
63
64
  import logging
@@ -69,6 +70,7 @@ from base_test import EnvSkip, TempProjects, UnityEnv
69
70
 
70
71
  import pythontk as ptk
71
72
  from unitytk import UnityLauncher, UnityFinder, TemplateDeployer
73
+ from unitytk.template_deployer import UPM_PACKAGE_NAME
72
74
 
73
75
 
74
76
  def _unity_available() -> bool:
@@ -433,9 +435,17 @@ class ControllerScenario:
433
435
  }
434
436
  )
435
437
  s_end = self.path_states()[-1]
438
+
439
+ def cut_short(**field):
440
+ """The record with one joint's ``t`` or ``q`` cut short."""
441
+ rig = json.loads(json.dumps(self.rig))
442
+ rig["joints"][1].update(field)
443
+ return json.dumps(dict(self.payload, rigs=[rig]))
444
+
436
445
  return {
437
446
  "record": json.dumps(self.payload),
438
447
  "record_newer": json.dumps(dict(self.payload, version=2)),
448
+ "records_short": [cut_short(t=[1.0]), cut_short(q=[0.0, 0.0])],
439
449
  "root": {
440
450
  "p": list(self.ROOT["p"]),
441
451
  "q": list(self.ROOT["q"]),
@@ -553,6 +563,151 @@ class FixtureScenario:
553
563
  return out
554
564
 
555
565
 
566
+ class BindingScenario:
567
+ """Hierarchies that put the controller's name binding to the test, with
568
+ the GLB applier as the reference: pythontk's
569
+ ``MeshConvert.apply_glb_articulation`` binds a joint to the one node of
570
+ its name -- exactly, else by namespace-stripped leaf -- and a grab part
571
+ to the one of its name UNDER its joint, and refuses an ambiguous name.
572
+ The two deliverables of one export must pose the same parts, so each
573
+ case is also written as a GLB of the same nodes and bound there
574
+ (:meth:`glb_binding`).
575
+
576
+ A case is ``{"nodes": [[name, parent index or None], ...], "record"}``;
577
+ ``None`` hangs a node off the model root.
578
+ """
579
+
580
+ class Tree:
581
+ """A node list under construction."""
582
+
583
+ def __init__(self):
584
+ self.nodes = []
585
+
586
+ def add(self, name, parent=None):
587
+ self.nodes.append([name, parent])
588
+ return len(self.nodes) - 1
589
+
590
+ def rig(self, rig, group, ns=""):
591
+ """*rig*'s joints under a group node, every name prefixed with
592
+ *ns*; returns joint index -> node index."""
593
+ ids = {}
594
+ parent = self.add(ns + group)
595
+ for index, joint in enumerate(rig["joints"]):
596
+ up = parent if joint["parent"] is None else ids[joint["parent"]]
597
+ ids[index] = self.add(ns + joint["name"], up)
598
+ return ids
599
+
600
+ def __init__(self, doc):
601
+ arm, tree = doc["rigs"]["arm"], doc["rigs"]["tree"]
602
+ self.cases = {}
603
+ # Parts named alike under sibling joints: each grab entry takes the
604
+ # one under ITS joint.
605
+ t = self.Tree()
606
+ ids = t.rig(tree, "tree_RIG")
607
+ t.add("cap_geo", ids[1])
608
+ t.add("cap_geo", ids[3])
609
+ self._case(t, tree, "repeated_part", ("cap_geo", 1), ("cap_geo", 3))
610
+ # A namespaced export of a rig the record names bare; one part
611
+ # namespaced on both sides, one only in the record.
612
+ t = self.Tree()
613
+ ids = t.rig(arm, "arm_RIG", ns="lamp:")
614
+ t.add("lamp:head_geo", ids[4])
615
+ t.add("elbow_geo", ids[1])
616
+ self._case(t, arm, "namespaced", ("lamp:head_geo", 4), ("rig:elbow_geo", 1))
617
+ # A joint's exact name twice: the rig is refused, not bound to the first.
618
+ t = self.Tree()
619
+ t.rig(arm, "arm_RIG")
620
+ t.add("elbow", t.add("props"))
621
+ self._case(t, arm, "duplicate_joint")
622
+ # Two namespaced instances of the rig: every bare name matches both.
623
+ t = self.Tree()
624
+ t.rig(arm, "arm_RIG", ns="A:")
625
+ t.rig(arm, "arm_RIG", ns="B:")
626
+ self._case(t, arm, "two_instances")
627
+ # Two parts of one name under one joint: that part is not grabbable,
628
+ # the rest of the rig binds.
629
+ t = self.Tree()
630
+ ids = t.rig(arm, "arm_RIG")
631
+ t.add("head_geo", ids[4])
632
+ t.add("head_geo", ids[4])
633
+ t.add("elbow_geo", ids[1])
634
+ self._case(t, arm, "ambiguous_part", ("head_geo", 4), ("elbow_geo", 1))
635
+ # Namesakes ahead of the real joints -- one differing only by case,
636
+ # one by namespace -- never steal an exact match.
637
+ t = self.Tree()
638
+ t.add("Elbow")
639
+ t.add("NS:base")
640
+ t.rig(arm, "arm_RIG")
641
+ self._case(t, arm, "exact_wins")
642
+ # Two root joints under different nodes: a rig is solved in ONE space,
643
+ # its roots' parent -- refused by the page, the GLB applier and here.
644
+ split = json.loads(json.dumps(arm))
645
+ split["joints"][1]["parent"] = None
646
+ t = self.Tree()
647
+ spaces = (t.add("arm_RIG"), t.add("other_RIG"))
648
+ ids = {}
649
+ for index, joint in enumerate(split["joints"]):
650
+ if joint["parent"] is None:
651
+ up = spaces[min(index, 1)]
652
+ else:
653
+ up = ids[joint["parent"]]
654
+ ids[index] = t.add(joint["name"], up)
655
+ self._case(t, split, "split_roots")
656
+
657
+ def _case(self, tree, rig, name, *grab):
658
+ record = dict(rig, grab=[{"node": n, "joint": j} for n, j in grab])
659
+ self.cases[name] = {
660
+ "nodes": tree.nodes,
661
+ "record": json.dumps({"version": 1, "rigs": [record]}),
662
+ }
663
+
664
+ @staticmethod
665
+ def glb_binding(case, path):
666
+ """What the GLB applier binds of *case*: ``{"joints": [path, ...],
667
+ "grabs": {path: joint}}`` (paths below the model root), or None when
668
+ it refuses the rig. Writes a JSON-only GLB of the case's nodes plus
669
+ a ``data_export`` carrier to *path* and runs the applier on it."""
670
+ nodes = [{"name": name} for name, _parent in case["nodes"]]
671
+ roots = []
672
+ for index, (_name, parent) in enumerate(case["nodes"]):
673
+ if parent is None:
674
+ roots.append(index)
675
+ else:
676
+ nodes[parent].setdefault("children", []).append(index)
677
+ nodes.append(
678
+ {"name": "data_export", "extras": {"articulation": case["record"]}}
679
+ )
680
+ roots.append(len(nodes) - 1)
681
+ gltf = {
682
+ "asset": {"version": "2.0"},
683
+ "scene": 0,
684
+ "scenes": [{"nodes": roots}],
685
+ "nodes": nodes,
686
+ }
687
+ payload = json.dumps(gltf).encode("utf-8")
688
+ payload += b" " * (-len(payload) % 4)
689
+ with open(path, "wb") as fh:
690
+ fh.write(struct.pack("<4sII", b"glTF", 2, 20 + len(payload)))
691
+ fh.write(struct.pack("<I4s", len(payload), b"JSON"))
692
+ fh.write(payload)
693
+ manifest = ptk.MeshConvert.apply_glb_articulation(path)
694
+ if not manifest:
695
+ return None
696
+
697
+ def path_of(index):
698
+ names = []
699
+ while index is not None:
700
+ names.append(case["nodes"][index][0])
701
+ index = case["nodes"][index][1]
702
+ return "/".join(reversed(names))
703
+
704
+ rig = manifest["rigs"][0]
705
+ return {
706
+ "joints": [path_of(j["node"]) for j in rig["joints"]],
707
+ "grabs": {path_of(g["node"]): g["joint"] for g in rig["grab"]},
708
+ }
709
+
710
+
556
711
  #: A real DCC export of an articulated rig (an FBX carrying the
557
712
  #: ``articulation`` record and a baked clip). Machine-local and never checked
558
713
  #: in: unset (or missing) = the live-export tests env-skip.
@@ -784,11 +939,13 @@ public class ArticulatedRigVerifier
784
939
  catch (Exception e) { Put("inputs_error", S(e.ToString())); }
785
940
  if (inputs != null)
786
941
  {
942
+ Run("guids", () => Guids(L(inputs["guids"])));
787
943
  Run("conformance", () => Conformance(O(inputs["doc"])));
788
944
  Run("invalid", () => Invalid(L(inputs["invalid"])));
789
945
  Run("boundary", () => Boundary(O(inputs["boundary"])));
790
946
  Run("hierarchy", () => Hierarchy(O(inputs["doc"]), O(inputs["hierarchy"])));
791
947
  Run("controller", () => Controller(O(inputs["controller"])));
948
+ Run("binding", () => Binding(O(inputs["binding"])));
792
949
  Run("animator", () => AnimatorOverride(O(inputs["controller"])));
793
950
  Run("importer", () => Importer(O(inputs["importer"])));
794
951
  if (inputs.ContainsKey("live")) Run("live", () => Live(O(inputs["live"])));
@@ -806,6 +963,15 @@ public class ArticulatedRigVerifier
806
963
  catch (Exception e) { Put(section + "_error", S(e.ToString())); }
807
964
  }
808
965
 
966
+ // ---- 0. fixed GUIDs ---------------------------------------------------
967
+ // The editor's own answer, per deployed asset path (files and folders):
968
+ // the GUID committed in the bundle's .meta, unless Unity re-minted it.
969
+ static void Guids(IList<object> paths)
970
+ {
971
+ var rows = paths.Select(p => S((string)p) + ": " + S(AssetDatabase.AssetPathToGUID((string)p)));
972
+ Put("guids", "{" + string.Join(", ", rows) + "}");
973
+ }
974
+
809
975
  // ---- 1. conformance ---------------------------------------------------
810
976
  static void Conformance(IDictionary<string, object> doc)
811
977
  {
@@ -1113,9 +1279,86 @@ public class ArticulatedRigVerifier
1113
1279
  var futureCtrl = future.AddComponent<ArticulatedRigController>();
1114
1280
  futureCtrl.record = (string)c["record_newer"];
1115
1281
  Put("ctrl_newer", "{\"resolved\": " + B(futureCtrl.Resolve()) + ", \"rigs\": " + futureCtrl.RigCount + "}");
1282
+
1283
+ // A joint whose t or q is cut short is refused with a warning, never
1284
+ // thrown out of Resolve (the importer calls it: the import would fail).
1285
+ var cutRows = new List<string>();
1286
+ foreach (var raw in L(c["records_short"]))
1287
+ {
1288
+ var cut = new GameObject("CutShortModel");
1289
+ cut.transform.SetParent(root, false);
1290
+ var cutCtrl = cut.AddComponent<ArticulatedRigController>();
1291
+ cutCtrl.record = (string)raw;
1292
+ try { cutRows.Add("{\"resolved\": " + B(cutCtrl.Resolve()) + ", \"rigs\": " + cutCtrl.RigCount + "}"); }
1293
+ catch (Exception thrown) { cutRows.Add("{\"threw\": " + S(thrown.GetType().Name) + "}"); }
1294
+ }
1295
+ Put("ctrl_short", "[" + string.Join(",", cutRows) + "]");
1116
1296
  UnityEngine.Object.DestroyImmediate(root.gameObject);
1117
1297
  }
1118
1298
 
1299
+ // ---- 3a. binding by name (the GLB applier's rules) ----------------------
1300
+ // Each case: a hierarchy of named transforms under a model root ([name,
1301
+ // parent index], null = the root) and a record. Reported: what the
1302
+ // controller bound, as paths below the root (null joints = the rig is not
1303
+ // bound), and the [ArticulatedRig] errors / warnings Resolve logged.
1304
+ static string RelPath(Transform root, Transform t)
1305
+ {
1306
+ if (t == null) return null;
1307
+ var names = new List<string>();
1308
+ for (var p = t; p != null && p != root; p = p.parent) names.Add(p.name);
1309
+ names.Reverse();
1310
+ return string.Join("/", names);
1311
+ }
1312
+
1313
+ static void Binding(IDictionary<string, object> cases)
1314
+ {
1315
+ var rows = new List<string>();
1316
+ foreach (var kv in cases)
1317
+ {
1318
+ var c = O(kv.Value);
1319
+ var root = new GameObject("BindingModel").transform;
1320
+ var made = new List<Transform>();
1321
+ foreach (var raw in L(c["nodes"]))
1322
+ {
1323
+ var n = L(raw);
1324
+ var t = new GameObject((string)n[0]).transform;
1325
+ t.SetParent(n[1] == null ? root : made[(int)D(n[1])], false);
1326
+ made.Add(t);
1327
+ }
1328
+ var errors = new List<string>();
1329
+ var warnings = new List<string>();
1330
+ Application.LogCallback hook = (message, stack, type) =>
1331
+ {
1332
+ if (!message.StartsWith("[ArticulatedRig]")) return;
1333
+ if (type == LogType.Error) errors.Add(message);
1334
+ else if (type == LogType.Warning) warnings.Add(message);
1335
+ };
1336
+ var ctrl = root.gameObject.AddComponent<ArticulatedRigController>();
1337
+ ctrl.record = (string)c["record"];
1338
+ bool resolved;
1339
+ Application.logMessageReceived += hook;
1340
+ try { resolved = ctrl.Resolve(); }
1341
+ finally { Application.logMessageReceived -= hook; }
1342
+ string joints = "null", grabs = "{}";
1343
+ if (ctrl.IsBound(0))
1344
+ {
1345
+ var model = ctrl.GetModel(0);
1346
+ joints = "[" + string.Join(",", Enumerable.Range(0, model.JointCount)
1347
+ .Select(j => S(RelPath(root, ctrl.GetJoint(0, j))))) + "]";
1348
+ grabs = "{" + string.Join(",", ctrl.GetGrabNodes().Select(t =>
1349
+ {
1350
+ ctrl.FindGrab(t, out int rig, out int joint, out Transform node);
1351
+ return S(RelPath(root, t)) + ": " + joint;
1352
+ })) + "}";
1353
+ }
1354
+ rows.Add(S(kv.Key) + ": {\"resolved\": " + B(resolved) + ", \"joints\": " + joints
1355
+ + ", \"grabs\": " + grabs + ", \"errors\": [" + string.Join(",", errors.Select(S))
1356
+ + "], \"warnings\": [" + string.Join(",", warnings.Select(S)) + "]}");
1357
+ UnityEngine.Object.DestroyImmediate(root.gameObject);
1358
+ }
1359
+ Put("binding", "{" + string.Join(",\n", rows) + "}");
1360
+ }
1361
+
1119
1362
  // ---- 3b. the Animator override (edit mode, no play mode) ---------------
1120
1363
  // A frame is driven by hand: Animator.Update(dt) -- which evaluates the
1121
1364
  // controller's clip and writes the transforms in edit mode -- then the
@@ -1407,6 +1650,22 @@ def _compile_errors(log_path: str) -> str:
1407
1650
  return "\n".join(errors[:20])
1408
1651
 
1409
1652
 
1653
+ #: The embedded package's root as an AssetDatabase path.
1654
+ _PACKAGE_ASSET_ROOT = f"Packages/{UPM_PACKAGE_NAME}"
1655
+
1656
+
1657
+ def _bundle_assets():
1658
+ """Every deployed bundle file and folder, as "/"-joined package paths."""
1659
+ names = TemplateDeployer._bundle_names()
1660
+ return list(names) + TemplateDeployer._folders(names)
1661
+
1662
+
1663
+ def _committed_meta(name):
1664
+ """Bundle asset *name*'s committed .meta, LF (as the deployer writes it)."""
1665
+ ref = TemplateDeployer._bundle_ref(name + ".meta")
1666
+ return ref.read_bytes().replace(b"\r\n", b"\n")
1667
+
1668
+
1410
1669
  def _run_unity(test, temp_dir, execute_method, prepare, timeout=600):
1411
1670
  """Create a project, let *prepare(project_path)* stage it, run the
1412
1671
  verifier; returns the parsed results (or skips on the license wall)."""
@@ -1466,6 +1725,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1466
1725
  cls.doc = ptk.ArticulationConformance.cases(seed=SEED, per_rig=PER_RIG)
1467
1726
  cls.scenario = ControllerScenario(cls.doc)
1468
1727
  cls.fixture = FixtureScenario(cls.doc)
1728
+ cls.binding = BindingScenario(cls.doc)
1469
1729
  rng = random.Random(5)
1470
1730
  cls.boundary = {
1471
1731
  "k": 0.037,
@@ -1552,11 +1812,13 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1552
1812
  variants[key] = os.path.join(cls.temp_dir, f"fixture_{key}.fbx.txt")
1553
1813
  cls.fixture.fbx(**kwargs).write(variants[key])
1554
1814
  inputs = {
1815
+ "guids": [f"{_PACKAGE_ASSET_ROOT}/{n}" for n in _bundle_assets()],
1555
1816
  "doc": cls.doc,
1556
1817
  "invalid": _invalid_rigs(),
1557
1818
  "boundary": cls.boundary,
1558
1819
  "hierarchy": cls.hierarchy,
1559
1820
  "controller": cls.scenario.inputs(),
1821
+ "binding": cls.binding.cases,
1560
1822
  "importer": dict(variants, asset=f"Assets/{fixture_name}"),
1561
1823
  }
1562
1824
  live = _live_fbx()
@@ -1586,6 +1848,34 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1586
1848
  cls.unity = unity
1587
1849
  logger.info("Unity %s", unity.get("unity_version"))
1588
1850
 
1851
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
1852
+ def test_01b_the_editor_reads_the_committed_guids(self):
1853
+ """Fixed GUIDs, proven by the editor rather than by reading files:
1854
+ AssetDatabase names every deployed script, shader, asmdef, the README
1855
+ and every component folder (the XR sub-assembly's included) by the
1856
+ GUID committed in the bundle's .meta -- and the import left those
1857
+ .meta files byte-identical, so a project never churns them."""
1858
+ got = self._section("guids")
1859
+ expected = {
1860
+ f"{_PACKAGE_ASSET_ROOT}/{name}": re.search(
1861
+ rb"^guid: ([0-9a-f]{32})$", _committed_meta(name), re.M
1862
+ )
1863
+ .group(1)
1864
+ .decode()
1865
+ for name in _bundle_assets()
1866
+ }
1867
+ self.assertEqual(got, expected, "Unity named a deployed asset differently")
1868
+ package = TemplateDeployer.package_dir(
1869
+ os.path.join(self.temp_dir, "UnityProject")
1870
+ )
1871
+ rewritten = []
1872
+ for name in _bundle_assets():
1873
+ path = os.path.join(package, *name.split("/")) + ".meta"
1874
+ with open(path, "rb") as fh:
1875
+ if fh.read() != _committed_meta(name):
1876
+ rewritten.append(name)
1877
+ self.assertEqual(rewritten, [], "Unity rewrote these .meta files on import")
1878
+
1589
1879
  # ---- Step 2: conformance -------------------------------------------------
1590
1880
 
1591
1881
  def _conformance(self):
@@ -1714,7 +2004,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1714
2004
 
1715
2005
  @EnvSkip.unless(_unity_available(), "no Unity editor found")
1716
2006
  def test_09_controller_binds_and_measures(self):
1717
- """Joints bind by leaf name, rig space is the rig group, the unit
2007
+ """Joints bind by name, rig space is the rig group, the unit
1718
2008
  scale is MEASURED (0.37), grab parts get convex colliders, and the
1719
2009
  state reads back off the float transforms."""
1720
2010
  unity = self._results()
@@ -1739,6 +2029,103 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1739
2029
  "a transform outside every grab part is refused",
1740
2030
  )
1741
2031
 
2032
+ # ---- Step 4a: binding by name ----------------------------------------------
2033
+
2034
+ def _binding(self, case):
2035
+ got = self._section("binding")
2036
+ self.assertIn(case, got, "no such binding case in the Unity results")
2037
+ return got[case]
2038
+
2039
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2040
+ def test_09b_binding_matches_the_glb_applier(self):
2041
+ """Every hierarchy binds in Unity exactly as pythontk's GLB applier
2042
+ binds a GLB of the same nodes: the same joints, the same grab parts,
2043
+ the same rigs refused -- the two deliverables of one export pose the
2044
+ same parts."""
2045
+ for case, spec in self.binding.cases.items():
2046
+ glb = BindingScenario.glb_binding(
2047
+ spec, os.path.join(self.temp_dir, f"binding_{case}.glb")
2048
+ )
2049
+ got = self._binding(case)
2050
+ self.assertEqual(got["joints"], glb and glb["joints"], f"{case}: joints")
2051
+ self.assertEqual(
2052
+ got["grabs"], glb["grabs"] if glb else {}, f"{case}: grab parts"
2053
+ )
2054
+
2055
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2056
+ def test_09c_a_repeated_part_name_binds_under_its_own_joint(self):
2057
+ """Two parts share a name under sibling joints: each grab entry takes
2058
+ the one under its own joint, and both are grabbable."""
2059
+ got = self._binding("repeated_part")
2060
+ self.assertTrue(got["resolved"])
2061
+ self.assertEqual(
2062
+ got["grabs"],
2063
+ {"tree_RIG/hub/left/cap_geo": 1, "tree_RIG/hub/right/tip/cap_geo": 3},
2064
+ )
2065
+ self.assertEqual(got["errors"], [])
2066
+
2067
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2068
+ def test_09d_namespaced_names_bind_by_their_leaf(self):
2069
+ """A namespaced export binds a record that names its joints bare, and
2070
+ a grab part namespaced on either side binds by its leaf."""
2071
+ got = self._binding("namespaced")
2072
+ self.assertTrue(got["resolved"])
2073
+ chain = "lamp:arm_RIG/lamp:base/lamp:elbow"
2074
+ self.assertEqual(got["joints"][1], chain)
2075
+ self.assertEqual(
2076
+ got["grabs"],
2077
+ {
2078
+ f"{chain}/lamp:collar/lamp:extend/lamp:head/lamp:head_geo": 4,
2079
+ f"{chain}/elbow_geo": 1,
2080
+ },
2081
+ )
2082
+ self.assertEqual(got["errors"], [])
2083
+
2084
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2085
+ def test_09e_an_ambiguous_name_is_refused_with_an_error(self):
2086
+ """A joint name that matches several transforms -- exactly, or by leaf
2087
+ across two namespaced instances -- leaves the rig unbound; two parts
2088
+ of one name under one joint leave that part ungrabbable while the rig
2089
+ binds. Each logs an error naming what was ambiguous, never a guess."""
2090
+ for case, named, bound in (
2091
+ ("duplicate_joint", "joint 'elbow'", False),
2092
+ ("two_instances", "joint 'base'", False),
2093
+ ("ambiguous_part", "grab part 'head_geo'", True),
2094
+ ):
2095
+ got = self._binding(case)
2096
+ self.assertEqual(got["resolved"], bound, case)
2097
+ self.assertTrue(
2098
+ any(named in e and "ambiguous" in e for e in got["errors"]),
2099
+ f"{case}: no error naming {named}: {got['errors']}",
2100
+ )
2101
+ self.assertIsNone(self._binding("duplicate_joint")["joints"])
2102
+ self.assertEqual(
2103
+ self._binding("ambiguous_part")["grabs"],
2104
+ {"arm_RIG/base/elbow/elbow_geo": 1},
2105
+ )
2106
+
2107
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2108
+ def test_09g_a_rig_whose_roots_hang_apart_is_refused(self):
2109
+ """Root joints under different nodes leave no one space to solve the
2110
+ rig in: it is not wired, with an error saying why -- as the page and
2111
+ the GLB applier refuse it (test_09b holds the GLB half)."""
2112
+ got = self._binding("split_roots")
2113
+ self.assertFalse(got["resolved"])
2114
+ self.assertTrue(
2115
+ any("different nodes" in e for e in got["errors"]), got["errors"]
2116
+ )
2117
+
2118
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2119
+ def test_09f_an_exact_name_wins_over_its_namesakes(self):
2120
+ """Transforms ahead of the joints that differ only by case
2121
+ (``Elbow``) or namespace (``NS:base``) never take an exact match."""
2122
+ got = self._binding("exact_wins")
2123
+ chain = ["arm_RIG/base"]
2124
+ for name in ("elbow", "collar", "extend", "head"):
2125
+ chain.append(f"{chain[-1]}/{name}")
2126
+ self.assertEqual(got["joints"], chain)
2127
+ self.assertEqual(got["errors"], [])
2128
+
1742
2129
  def _solve(self, state, joint, local, target, rotation):
1743
2130
  return self.scenario.model.solve(state, joint, local, target, rotation)
1744
2131
 
@@ -2082,6 +2469,17 @@ class TestArticulatedRigRuntime(unittest.TestCase):
2082
2469
  in): nothing binds."""
2083
2470
  self.assertEqual(self._section("ctrl_newer"), {"resolved": False, "rigs": 0})
2084
2471
 
2472
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2473
+ def test_16c_controller_refuses_a_joint_cut_short(self):
2474
+ """A joint whose ``t`` holds fewer than 3 values, or ``q`` fewer than
2475
+ 4, is a rig the runtime refuses with a warning: nothing binds, and
2476
+ nothing is thrown out of ``Resolve``, which the importer calls. The
2477
+ reference has no such check (a short ``q`` is an IndexError there), so
2478
+ this is the port's own contract, not a ``_invalid_rigs`` case."""
2479
+ self.assertEqual(
2480
+ self._section("ctrl_short"), [{"resolved": False, "rigs": 0}] * 2
2481
+ )
2482
+
2085
2483
  @EnvSkip.unless(_unity_available(), "no Unity editor found")
2086
2484
  def test_17_xr_adapter_inert_without_xri(self):
2087
2485
  """Without the XR Interaction Toolkit the XR assembly is excluded by