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.
- {unitytk-0.0.15 → unitytk-0.2.0}/MANIFEST.in +1 -1
- {unitytk-0.0.15/unitytk.egg-info → unitytk-0.2.0}/PKG-INFO +2 -2
- {unitytk-0.0.15 → unitytk-0.2.0}/pyproject.toml +12 -7
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_articulated_rig_runtime.py +399 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_asset_delivery.py +190 -9
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_audio_events_integration.py +11 -16
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_base_test.py +194 -0
- unitytk-0.2.0/test/test_blender_render_effects_e2e.py +496 -0
- unitytk-0.2.0/test/test_blender_shadow_plane_integration.py +875 -0
- unitytk-0.2.0/test/test_blender_shot_metadata_integration.py +472 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_clip_name_contract_integration.py +10 -15
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_controller_runtime_integration.py +1 -16
- unitytk-0.2.0/test/test_deploy_templates.py +1150 -0
- unitytk-0.2.0/test/test_embedded_dependencies_integration.py +461 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_emissive_groups_integration.py +5 -16
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_fade_window_preservation.py +12 -6
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_keyed_visibility.py +17 -15
- unitytk-0.2.0/test/test_lightmap_metadata_integration.py +1321 -0
- unitytk-0.2.0/test/test_maya_surface_materials_integration.py +1027 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_multi_section_audio_integration.py +16 -9
- unitytk-0.2.0/test/test_render_effects_e2e.py +1002 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_controller.py +12 -13
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_e2e.py +22 -22
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_render_opacity_integration.py +14 -14
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_run_tests.py +149 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_scene_builder.py +1 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shadow_plane_integration.py +88 -6
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shadow_plane_runtime_integration.py +510 -4
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shared_material_standalone.py +10 -13
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shot_metadata_integration.py +345 -45
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_shots_audio_sidebyside_integration.py +142 -16
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_standalone_enduser.py +14 -28
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/__init__.py +1 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/asset_delivery.py +56 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/template_deployer.py +419 -137
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ArticulatedRig}/ArticulatedRigController.cs +253 -100
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +199 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +7 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR.meta +8 -0
- unitytk-0.2.0/unitytk/templates/ArticulatedRig.meta +8 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/AudioEvent}/AudioEventController.cs +176 -2
- unitytk-0.2.0/unitytk/templates/AudioEvent/AudioEventController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/AudioEvent.meta +8 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroupController.cs +1 -1
- unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +7 -0
- unitytk-0.2.0/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +9 -0
- unitytk-0.2.0/unitytk/templates/EmissiveGroup.meta +8 -0
- unitytk-0.2.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +1465 -0
- unitytk-0.2.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/LightmapMetadata.meta +8 -0
- unitytk-0.2.0/unitytk/templates/M3trik.Unitytk.asmdef.meta +7 -0
- unitytk-0.2.0/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs +763 -0
- unitytk-0.2.0/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/MayaSurface.meta +8 -0
- unitytk-0.2.0/unitytk/templates/README.md +350 -0
- unitytk-0.2.0/unitytk/templates/README.md.meta +7 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/RenderEffects}/RenderEffectsController.cs +495 -50
- unitytk-0.2.0/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/RenderEffects.meta +8 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlane.shader +319 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +9 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +1949 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +11 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ShadowPlane}/ShadowPlaneHorizon.hlsl +157 -63
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +7 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane.meta +8 -0
- unitytk-0.2.0/unitytk/templates/ShotMetadata/ShotMetadataController.cs +309 -0
- unitytk-0.2.0/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +11 -0
- unitytk-0.2.0/unitytk/templates/ShotMetadata.meta +8 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/UnitytkSettings.cs +118 -15
- unitytk-0.2.0/unitytk/templates/UnitytkSettings.cs.meta +11 -0
- {unitytk-0.0.15 → unitytk-0.2.0/unitytk.egg-info}/PKG-INFO +2 -2
- unitytk-0.2.0/unitytk.egg-info/SOURCES.txt +96 -0
- unitytk-0.2.0/unitytk.egg-info/requires.txt +1 -0
- unitytk-0.0.15/test/test_deploy_templates.py +0 -620
- unitytk-0.0.15/test/test_lightmap_metadata_integration.py +0 -588
- unitytk-0.0.15/test/test_render_effects_e2e.py +0 -420
- unitytk-0.0.15/unitytk/templates/LightmapMetadataController.cs +0 -551
- unitytk-0.0.15/unitytk/templates/README.md +0 -249
- unitytk-0.0.15/unitytk/templates/ShadowPlane.shader +0 -223
- unitytk-0.0.15/unitytk/templates/ShadowPlaneController.cs +0 -1201
- unitytk-0.0.15/unitytk/templates/ShotMetadataController.cs +0 -177
- unitytk-0.0.15/unitytk.egg-info/SOURCES.txt +0 -60
- unitytk-0.0.15/unitytk.egg-info/requires.txt +0 -1
- {unitytk-0.0.15 → unitytk-0.2.0}/LICENSE +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/docs/README.md +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/setup.cfg +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_launcher.py +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/test/test_temp_projects.py +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/launcher.py +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/scene_builder.py +0 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/ArticulatedRig}/ArticulatedRigPointerGrab.cs +0 -0
- {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRGrab.cs +0 -0
- {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRSetup.cs +0 -0
- {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.2.0/unitytk/templates/ArticulatedRig/XR}/M3trik.Unitytk.XR.asmdef +0 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroups.hlsl +0 -0
- {unitytk-0.0.15/unitytk/templates → unitytk-0.2.0/unitytk/templates/EmissiveGroup}/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.0.15 → unitytk-0.2.0}/unitytk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitytk
|
|
3
|
-
Version: 0.0
|
|
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.
|
|
16
|
+
Requires-Dist: pythontk>=0.13.1
|
|
17
17
|
Dynamic: license-file
|
|
18
18
|
|
|
19
19
|
[](../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.
|
|
21
|
-
#
|
|
22
|
-
# `
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
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
|
|
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
|