unitytk 0.1.0__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.1.0/unitytk.egg-info → unitytk-0.2.0}/PKG-INFO +2 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/pyproject.toml +6 -4
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_articulated_rig_runtime.py +310 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_asset_delivery.py +190 -9
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_base_test.py +109 -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.1.0 → unitytk-0.2.0}/test/test_deploy_templates.py +14 -5
- unitytk-0.2.0/test/test_embedded_dependencies_integration.py +461 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_lightmap_metadata_integration.py +244 -4
- unitytk-0.2.0/test/test_maya_surface_materials_integration.py +1027 -0
- unitytk-0.2.0/test/test_render_effects_e2e.py +1002 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_run_tests.py +149 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_scene_builder.py +1 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_shadow_plane_integration.py +82 -3
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_shadow_plane_runtime_integration.py +402 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_shots_audio_sidebyside_integration.py +130 -10
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/__init__.py +1 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/asset_delivery.py +56 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/template_deployer.py +6 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs +113 -20
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/AudioEvent/AudioEventController.cs +176 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs +1 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +402 -64
- 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.1.0 → unitytk-0.2.0}/unitytk/templates/README.md +47 -15
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/RenderEffects/RenderEffectsController.cs +443 -30
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlane.shader +319 -0
- unitytk-0.2.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +1949 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl +157 -63
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShotMetadata/ShotMetadataController.cs +2 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/UnitytkSettings.cs +118 -15
- {unitytk-0.1.0 → unitytk-0.2.0/unitytk.egg-info}/PKG-INFO +2 -2
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk.egg-info/SOURCES.txt +8 -0
- unitytk-0.2.0/unitytk.egg-info/requires.txt +1 -0
- unitytk-0.1.0/test/test_render_effects_e2e.py +0 -479
- unitytk-0.1.0/unitytk/templates/ShadowPlane/ShadowPlane.shader +0 -223
- unitytk-0.1.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +0 -1201
- unitytk-0.1.0/unitytk.egg-info/requires.txt +0 -1
- {unitytk-0.1.0 → unitytk-0.2.0}/LICENSE +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/MANIFEST.in +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/docs/README.md +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/setup.cfg +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_audio_events_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_clip_name_contract_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_controller_runtime_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_emissive_groups_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_fade_window_preservation.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_keyed_visibility.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_launcher.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_multi_section_audio_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_render_opacity_controller.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_render_opacity_e2e.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_render_opacity_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_shared_material_standalone.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_shot_metadata_integration.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_standalone_enduser.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/test/test_temp_projects.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/launcher.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/scene_builder.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig/XR.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ArticulatedRig.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/AudioEvent/AudioEventController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/AudioEvent.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/EmissiveGroup.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/LightmapMetadata.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/M3trik.Unitytk.asmdef.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/README.md.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/RenderEffects.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShadowPlane.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/ShotMetadata.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/UnitytkSettings.cs.meta +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.1.0 → unitytk-0.2.0}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.1.0 → 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.
|
|
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.13.
|
|
16
|
+
Requires-Dist: pythontk>=0.13.1
|
|
17
17
|
Dynamic: license-file
|
|
18
18
|
|
|
19
19
|
[](../test/)
|
|
@@ -17,15 +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.13.
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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:
|
|
23
25
|
# TemplateDeployer builds its deprecation notices at import through
|
|
24
26
|
# `Deprecation.values(..., since=)` (new in 0.11.3). UnityFinder.find_editors
|
|
25
27
|
# needs 0.8.84: it scans through `AppLauncher.scan_install_dirs` (new in
|
|
26
28
|
# 0.8.72) and needs its pattern order to be a priority (0.8.84), so the Hub's
|
|
27
29
|
# chosen install location wins over the default.
|
|
28
|
-
dependencies = ["pythontk>=0.13.
|
|
30
|
+
dependencies = ["pythontk>=0.13.1"]
|
|
29
31
|
|
|
30
32
|
[project.urls]
|
|
31
33
|
Homepage = "https://github.com/m3trik/unitytk"
|
|
@@ -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
|
|
@@ -562,6 +563,151 @@ class FixtureScenario:
|
|
|
562
563
|
return out
|
|
563
564
|
|
|
564
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
|
+
|
|
565
711
|
#: A real DCC export of an articulated rig (an FBX carrying the
|
|
566
712
|
#: ``articulation`` record and a baked clip). Machine-local and never checked
|
|
567
713
|
#: in: unset (or missing) = the live-export tests env-skip.
|
|
@@ -799,6 +945,7 @@ public class ArticulatedRigVerifier
|
|
|
799
945
|
Run("boundary", () => Boundary(O(inputs["boundary"])));
|
|
800
946
|
Run("hierarchy", () => Hierarchy(O(inputs["doc"]), O(inputs["hierarchy"])));
|
|
801
947
|
Run("controller", () => Controller(O(inputs["controller"])));
|
|
948
|
+
Run("binding", () => Binding(O(inputs["binding"])));
|
|
802
949
|
Run("animator", () => AnimatorOverride(O(inputs["controller"])));
|
|
803
950
|
Run("importer", () => Importer(O(inputs["importer"])));
|
|
804
951
|
if (inputs.ContainsKey("live")) Run("live", () => Live(O(inputs["live"])));
|
|
@@ -1149,6 +1296,69 @@ public class ArticulatedRigVerifier
|
|
|
1149
1296
|
UnityEngine.Object.DestroyImmediate(root.gameObject);
|
|
1150
1297
|
}
|
|
1151
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
|
+
|
|
1152
1362
|
// ---- 3b. the Animator override (edit mode, no play mode) ---------------
|
|
1153
1363
|
// A frame is driven by hand: Animator.Update(dt) -- which evaluates the
|
|
1154
1364
|
// controller's clip and writes the transforms in edit mode -- then the
|
|
@@ -1515,6 +1725,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
|
|
|
1515
1725
|
cls.doc = ptk.ArticulationConformance.cases(seed=SEED, per_rig=PER_RIG)
|
|
1516
1726
|
cls.scenario = ControllerScenario(cls.doc)
|
|
1517
1727
|
cls.fixture = FixtureScenario(cls.doc)
|
|
1728
|
+
cls.binding = BindingScenario(cls.doc)
|
|
1518
1729
|
rng = random.Random(5)
|
|
1519
1730
|
cls.boundary = {
|
|
1520
1731
|
"k": 0.037,
|
|
@@ -1607,6 +1818,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
|
|
|
1607
1818
|
"boundary": cls.boundary,
|
|
1608
1819
|
"hierarchy": cls.hierarchy,
|
|
1609
1820
|
"controller": cls.scenario.inputs(),
|
|
1821
|
+
"binding": cls.binding.cases,
|
|
1610
1822
|
"importer": dict(variants, asset=f"Assets/{fixture_name}"),
|
|
1611
1823
|
}
|
|
1612
1824
|
live = _live_fbx()
|
|
@@ -1792,7 +2004,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
|
|
|
1792
2004
|
|
|
1793
2005
|
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
1794
2006
|
def test_09_controller_binds_and_measures(self):
|
|
1795
|
-
"""Joints bind by
|
|
2007
|
+
"""Joints bind by name, rig space is the rig group, the unit
|
|
1796
2008
|
scale is MEASURED (0.37), grab parts get convex colliders, and the
|
|
1797
2009
|
state reads back off the float transforms."""
|
|
1798
2010
|
unity = self._results()
|
|
@@ -1817,6 +2029,103 @@ class TestArticulatedRigRuntime(unittest.TestCase):
|
|
|
1817
2029
|
"a transform outside every grab part is refused",
|
|
1818
2030
|
)
|
|
1819
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
|
+
|
|
1820
2129
|
def _solve(self, state, joint, local, target, rotation):
|
|
1821
2130
|
return self.scenario.model.solve(state, joint, local, target, rotation)
|
|
1822
2131
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# !/usr/bin/python
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
"""Tests for unitytk.CopyToAssetsDeliverer -- copy-to-Assets hand-off Strategy (no DCC)."""
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import struct
|
|
5
7
|
import unittest
|
|
6
8
|
|
|
7
|
-
from pythontk import HandoffRequest, Payload, TempArtifacts
|
|
9
|
+
from pythontk import FbxMedia, HandoffRequest, Payload, TempArtifacts
|
|
8
10
|
from unitytk.asset_delivery import (
|
|
9
11
|
CopyToAssetsDeliverer,
|
|
10
12
|
FileToUnityBridge,
|
|
@@ -12,21 +14,80 @@ from unitytk.asset_delivery import (
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class _Logger:
|
|
15
|
-
"""Minimal logger capturing messages by level."""
|
|
17
|
+
"""Minimal logger capturing messages by level (``%``-args formatted)."""
|
|
16
18
|
|
|
17
19
|
def __init__(self):
|
|
18
20
|
self.errors = []
|
|
19
21
|
self.warnings = []
|
|
20
22
|
self.infos = []
|
|
21
23
|
|
|
22
|
-
def error(self, m):
|
|
23
|
-
self.errors.append(m)
|
|
24
|
-
|
|
25
|
-
def warning(self, m):
|
|
26
|
-
self.warnings.append(m)
|
|
24
|
+
def error(self, m, *args):
|
|
25
|
+
self.errors.append(m % args if args else m)
|
|
26
|
+
|
|
27
|
+
def warning(self, m, *args):
|
|
28
|
+
self.warnings.append(m % args if args else m)
|
|
29
|
+
|
|
30
|
+
def info(self, m, *args):
|
|
31
|
+
self.infos.append(m % args if args else m)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _carrier_fbx(path: str, channels) -> str:
|
|
35
|
+
"""A binary FBX (7700 layout) whose ``data_export`` carrier holds
|
|
36
|
+
*channels* ``{record key: payload}`` -- what a DCC export hands over."""
|
|
37
|
+
|
|
38
|
+
def prop(value):
|
|
39
|
+
if isinstance(value, int):
|
|
40
|
+
return b"L" + struct.pack("<q", value)
|
|
41
|
+
return b"S" + struct.pack("<I", len(value)) + value
|
|
42
|
+
|
|
43
|
+
def record(name, props, children, offset):
|
|
44
|
+
body_props = b"".join(prop(p) for p in props)
|
|
45
|
+
head = 24 + 1 + len(name)
|
|
46
|
+
body, at = b"", offset + head + len(body_props)
|
|
47
|
+
for child in children:
|
|
48
|
+
chunk = record(*child, at)
|
|
49
|
+
body, at = body + chunk, at + len(chunk)
|
|
50
|
+
if children:
|
|
51
|
+
body += b"\0" * 25
|
|
52
|
+
end = offset + head + len(body_props) + len(body)
|
|
53
|
+
return (
|
|
54
|
+
struct.pack("<QQQB", end, len(props), len(body_props), len(name))
|
|
55
|
+
+ name
|
|
56
|
+
+ body_props
|
|
57
|
+
+ body
|
|
58
|
+
)
|
|
27
59
|
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
p70 = [
|
|
61
|
+
(b"P", [k.encode(), b"KString", b"", b"U", json.dumps(v).encode()], [])
|
|
62
|
+
for k, v in channels.items()
|
|
63
|
+
]
|
|
64
|
+
roots = [
|
|
65
|
+
(
|
|
66
|
+
b"Objects",
|
|
67
|
+
[],
|
|
68
|
+
[
|
|
69
|
+
(b"Model", [1001, b"Crate\x00\x01Model", b"Mesh"], []),
|
|
70
|
+
(
|
|
71
|
+
b"Model",
|
|
72
|
+
[1002, b"data_export\x00\x01Model", b"Null"],
|
|
73
|
+
[(b"Properties70", [], p70)],
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
(b"Connections", [], [(b"C", [b"OO", 1001, 0], [])]),
|
|
78
|
+
]
|
|
79
|
+
magic = b"Kaydara FBX Binary \x00\x1a\x00"
|
|
80
|
+
body, offset = b"", len(magic) + 4
|
|
81
|
+
for root in roots:
|
|
82
|
+
chunk = record(*root, offset)
|
|
83
|
+
body, offset = body + chunk, offset + len(chunk)
|
|
84
|
+
at = offset + 25 + 16
|
|
85
|
+
pad = ((at + 15) & ~15) - at or 16
|
|
86
|
+
footer = bytes(range(16)) + b"\0" * pad + b"\0" * 4 + struct.pack("<I", 7700)
|
|
87
|
+
tail = b"\0" * 120 + bytes(16)
|
|
88
|
+
with open(path, "wb") as fh:
|
|
89
|
+
fh.write(magic + struct.pack("<I", 7700) + body + b"\0" * 25 + footer + tail)
|
|
90
|
+
return path
|
|
30
91
|
|
|
31
92
|
|
|
32
93
|
class _StubBridge:
|
|
@@ -281,6 +342,126 @@ class DeliveryTest(unittest.TestCase):
|
|
|
281
342
|
)
|
|
282
343
|
|
|
283
344
|
|
|
345
|
+
class _HostBridge(_StubBridge):
|
|
346
|
+
"""A DCC bridge: it says where its host keeps the maps the records name
|
|
347
|
+
(the ``lightmap_search_dirs`` hook every DCC export mixin answers)."""
|
|
348
|
+
|
|
349
|
+
def __init__(self, project_path, search_dirs):
|
|
350
|
+
super().__init__(project_path)
|
|
351
|
+
self._dirs = search_dirs
|
|
352
|
+
|
|
353
|
+
def lightmap_search_dirs(self):
|
|
354
|
+
return list(self._dirs)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class SelfContainedDeliveryTest(unittest.TestCase):
|
|
358
|
+
"""The FBX lands in Assets/ carrying every file its records name, so the
|
|
359
|
+
Unity importers find them with nothing copied beside it (the user's
|
|
360
|
+
decision of 2026-10-05: every dependency embedded, zero sidecar files)."""
|
|
361
|
+
|
|
362
|
+
RECORDS = {
|
|
363
|
+
"lightmap_metadata": {
|
|
364
|
+
"version": 1,
|
|
365
|
+
"objects": [{"name": "Crate", "map": "Crate_Lightmap.exr", "uvIndex": 1}],
|
|
366
|
+
"probe": {"map": "Room_Probe.exr", "position": [0, 0, 0], "unit_scale": 0.01},
|
|
367
|
+
},
|
|
368
|
+
"shadow_metadata": {
|
|
369
|
+
"version": 2,
|
|
370
|
+
"planes": [
|
|
371
|
+
{
|
|
372
|
+
"name": "Crate_shadow",
|
|
373
|
+
"texture": "Crate_shadow.png",
|
|
374
|
+
"horizon": {"texture": "Crate_horizon.png"},
|
|
375
|
+
}
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
def setUp(self):
|
|
381
|
+
store = TempArtifacts("unitytk_delivery_embed", policy="scoped")
|
|
382
|
+
self.addCleanup(store.cleanup)
|
|
383
|
+
self.tmp = store.dir_path()
|
|
384
|
+
self.project = os.path.join(self.tmp, "MyUnityProj")
|
|
385
|
+
os.makedirs(os.path.join(self.project, "Assets"))
|
|
386
|
+
self.maps = os.path.join(self.tmp, "sourceimages")
|
|
387
|
+
os.makedirs(self.maps)
|
|
388
|
+
self.bytes = {}
|
|
389
|
+
for name in ("Crate_Lightmap.exr", "Room_Probe.exr", "Crate_shadow.png", "Crate_horizon.png"):
|
|
390
|
+
self.bytes[name] = name.encode() * 8
|
|
391
|
+
with open(os.path.join(self.maps, name), "wb") as fh:
|
|
392
|
+
fh.write(self.bytes[name])
|
|
393
|
+
self.fbx = _carrier_fbx(os.path.join(self.tmp, "export.fbx"), self.RECORDS)
|
|
394
|
+
with open(self.fbx, "rb") as fh:
|
|
395
|
+
self.source = fh.read()
|
|
396
|
+
self.deliverer = CopyToAssetsDeliverer()
|
|
397
|
+
|
|
398
|
+
def _deliver(self, bridge):
|
|
399
|
+
return self.deliverer.deliver(bridge, _payload(self.fbx, "Crate"), _request({}))
|
|
400
|
+
|
|
401
|
+
def test_the_fbx_carries_every_file_its_records_name(self):
|
|
402
|
+
bridge = _HostBridge(self.project, [self.maps])
|
|
403
|
+
result = self._deliver(bridge)
|
|
404
|
+
dest = result["asset"]
|
|
405
|
+
self.assertEqual(
|
|
406
|
+
{row["name"]: row["bytes"] for row in FbxMedia.embedded(dest)},
|
|
407
|
+
{name: len(data) for name, data in self.bytes.items()},
|
|
408
|
+
)
|
|
409
|
+
# Nothing ships beside it, and the source is the file it was.
|
|
410
|
+
self.assertEqual(os.listdir(os.path.dirname(dest)), ["Crate.fbx"])
|
|
411
|
+
with open(self.fbx, "rb") as fh:
|
|
412
|
+
self.assertEqual(fh.read(), self.source)
|
|
413
|
+
self.assertFalse(bridge.logger.warnings, bridge.logger.warnings)
|
|
414
|
+
|
|
415
|
+
def test_a_bridge_without_a_host_answer_looks_beside_the_source(self):
|
|
416
|
+
"""The file-driven bridge has no host: the maps beside the picked FBX."""
|
|
417
|
+
for name, data in self.bytes.items():
|
|
418
|
+
with open(os.path.join(self.tmp, name), "wb") as fh:
|
|
419
|
+
fh.write(data)
|
|
420
|
+
result = self._deliver(_StubBridge(self.project))
|
|
421
|
+
self.assertEqual(
|
|
422
|
+
sorted(row["name"] for row in FbxMedia.embedded(result["asset"])),
|
|
423
|
+
sorted(self.bytes),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def test_a_map_found_nowhere_is_warned_and_the_fbx_still_ships(self):
|
|
427
|
+
os.remove(os.path.join(self.maps, "Crate_horizon.png"))
|
|
428
|
+
bridge = _HostBridge(self.project, [self.maps])
|
|
429
|
+
result = self._deliver(bridge)
|
|
430
|
+
self.assertTrue(os.path.isfile(result["asset"]))
|
|
431
|
+
self.assertIn("Crate_horizon.png", "\n".join(bridge.logger.warnings))
|
|
432
|
+
self.assertNotIn(
|
|
433
|
+
"Crate_horizon.png", [r["name"] for r in FbxMedia.embedded(result["asset"])]
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def test_an_embed_pass_that_fails_ships_the_fbx_as_exported(self):
|
|
437
|
+
"""The pass is an addition to the send, never a reason to fail it: a
|
|
438
|
+
file it cannot read (a pythontk before the truncated-file fix raised
|
|
439
|
+
``struct.error``) ships as exported, with a warning."""
|
|
440
|
+
from unittest.mock import patch
|
|
441
|
+
|
|
442
|
+
bridge = _HostBridge(self.project, [self.maps])
|
|
443
|
+
with patch.object(
|
|
444
|
+
FbxMedia, "embed_dependencies", side_effect=struct.error("cut short")
|
|
445
|
+
):
|
|
446
|
+
result = self._deliver(bridge)
|
|
447
|
+
with open(result["asset"], "rb") as fh:
|
|
448
|
+
self.assertEqual(fh.read(), self.source)
|
|
449
|
+
self.assertIn("cut short", "\n".join(bridge.logger.warnings))
|
|
450
|
+
|
|
451
|
+
def test_a_truncated_fbx_ships_as_it_is(self):
|
|
452
|
+
"""The magic but not the body: nothing to embed into, the copy stands."""
|
|
453
|
+
from pythontk.file_utils.mesh_convert.fbx_file import FBX_MAGIC
|
|
454
|
+
|
|
455
|
+
cut = FBX_MAGIC + struct.pack("<I", 7700) + b"\x10" * 20
|
|
456
|
+
with open(self.fbx, "wb") as fh:
|
|
457
|
+
fh.write(cut)
|
|
458
|
+
bridge = _HostBridge(self.project, [self.maps])
|
|
459
|
+
result = self._deliver(bridge)
|
|
460
|
+
with open(result["asset"], "rb") as fh:
|
|
461
|
+
self.assertEqual(fh.read(), cut)
|
|
462
|
+
self.assertTrue(bridge.logger.warnings)
|
|
463
|
+
|
|
464
|
+
|
|
284
465
|
class FileToUnityBridgeTest(unittest.TestCase):
|
|
285
466
|
"""The DCC-free file-driven bridge: pick a model file -> copy into Assets/."""
|
|
286
467
|
|