unitytk 0.0.12__tar.gz → 0.0.14__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.12/unitytk.egg-info → unitytk-0.0.14}/PKG-INFO +5 -3
- {unitytk-0.0.12 → unitytk-0.0.14}/docs/README.md +1 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/pyproject.toml +10 -2
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_asset_delivery.py +1 -2
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_audio_events_integration.py +0 -1
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_base_test.py +8 -54
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_clip_name_contract_integration.py +54 -36
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_emissive_groups_integration.py +11 -11
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_fade_window_preservation.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_keyed_visibility.py +12 -21
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_launcher.py +62 -26
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_lightmap_metadata_integration.py +87 -4
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_multi_section_audio_integration.py +10 -13
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_render_opacity_controller.py +2 -2
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_render_opacity_e2e.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_render_opacity_integration.py +3 -4
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_shadow_plane_integration.py +24 -35
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_shared_material_standalone.py +9 -16
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_shot_metadata_integration.py +11 -2
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_shots_audio_sidebyside_integration.py +22 -10
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_standalone_enduser.py +11 -20
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/__init__.py +1 -1
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/launcher.py +70 -35
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/LightmapMetadataController.cs +147 -12
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/README.md +4 -4
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/ShadowPlaneController.cs +2 -2
- {unitytk-0.0.12 → unitytk-0.0.14/unitytk.egg-info}/PKG-INFO +5 -3
- unitytk-0.0.14/unitytk.egg-info/requires.txt +1 -0
- unitytk-0.0.12/unitytk.egg-info/requires.txt +0 -1
- {unitytk-0.0.12 → unitytk-0.0.14}/LICENSE +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/MANIFEST.in +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/setup.cfg +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_controller_runtime_integration.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_deploy_templates.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_render_effects_e2e.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_run_tests.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_scene_builder.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_shadow_plane_runtime_integration.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/test/test_temp_projects.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/asset_delivery.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/scene_builder.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/template_deployer.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/AudioEventController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/EmissiveGroupController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/EmissiveGroups.hlsl +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/RenderEffectsController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/ShadowPlane.shader +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/ShadowPlaneHorizon.hlsl +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/ShotMetadataController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/UnitytkSettings.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk.egg-info/SOURCES.txt +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.0.12 → unitytk-0.0.14}/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.0.14
|
|
4
4
|
Summary: A modular Python toolkit for Unity interaction.
|
|
5
5
|
Author-email: Ryan Simpson <m3trik@outlook.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,14 +8,16 @@ Project-URL: Homepage, https://github.com/m3trik/unitytk
|
|
|
8
8
|
Project-URL: Repository, https://github.com/m3trik/unitytk
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System ::
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
13
|
Requires-Python: >=3.9
|
|
13
14
|
Description-Content-Type: text/markdown
|
|
14
15
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: pythontk>=0.
|
|
16
|
+
Requires-Dist: pythontk>=0.12.0
|
|
16
17
|
Dynamic: license-file
|
|
17
18
|
|
|
18
19
|
[](../test/)
|
|
20
|
+

|
|
19
21
|
|
|
20
22
|
# unitytk
|
|
21
23
|
|
|
@@ -12,9 +12,17 @@ requires-python = ">=3.9"
|
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
14
|
"License :: OSI Approved :: MIT License",
|
|
15
|
-
"Operating System ::
|
|
15
|
+
"Operating System :: Microsoft :: Windows",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
16
17
|
]
|
|
17
|
-
|
|
18
|
+
# The floor is HAND-RATCHETED: unitytk is off-chain from m3trik/push.ps1, so no
|
|
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"]
|
|
18
26
|
|
|
19
27
|
[project.urls]
|
|
20
28
|
Homepage = "https://github.com/m3trik/unitytk"
|
|
@@ -6,11 +6,10 @@ import shutil
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import unittest
|
|
8
8
|
|
|
9
|
-
from pythontk
|
|
9
|
+
from pythontk import HandoffRequest, Payload
|
|
10
10
|
from unitytk.asset_delivery import (
|
|
11
11
|
CopyToAssetsDeliverer,
|
|
12
12
|
FileToUnityBridge,
|
|
13
|
-
CopyToAssetsDeliverer,
|
|
14
13
|
)
|
|
15
14
|
|
|
16
15
|
|
|
@@ -356,7 +356,7 @@ class SuiteOptionsTest(unittest.TestCase):
|
|
|
356
356
|
|
|
357
357
|
|
|
358
358
|
class TestAssetsTest(unittest.TestCase):
|
|
359
|
-
"""Fixture resolution
|
|
359
|
+
"""Fixture resolution: a plain join under ``UNITYTK_TEST_ASSETS``."""
|
|
360
360
|
|
|
361
361
|
def setUp(self):
|
|
362
362
|
import tempfile
|
|
@@ -368,41 +368,15 @@ class TestAssetsTest(unittest.TestCase):
|
|
|
368
368
|
patcher.start()
|
|
369
369
|
self.addCleanup(patcher.stop)
|
|
370
370
|
|
|
371
|
-
def _touch(self, *parts):
|
|
372
|
-
path = os.path.join(self.root, *parts)
|
|
373
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
374
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
375
|
-
fh.write("x")
|
|
376
|
-
return path
|
|
377
|
-
|
|
378
371
|
def test_path_joins_under_the_root(self):
|
|
379
372
|
self.assertEqual(
|
|
380
373
|
TestAssets.path("a", "b.fbx"), os.path.join(self.root, "a", "b.fbx")
|
|
381
374
|
)
|
|
382
375
|
|
|
383
|
-
def
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
"""An already-staged asset tree keeps working for one release."""
|
|
388
|
-
for new_parts, legacy_parts in TestAssets.LEGACY_ALIASES.items():
|
|
389
|
-
with self.subTest(asset=new_parts):
|
|
390
|
-
staged = self._touch(*legacy_parts)
|
|
391
|
-
try:
|
|
392
|
-
self.assertEqual(
|
|
393
|
-
os.path.normcase(TestAssets.path(*new_parts)),
|
|
394
|
-
os.path.normcase(staged),
|
|
395
|
-
)
|
|
396
|
-
finally:
|
|
397
|
-
os.remove(staged)
|
|
398
|
-
|
|
399
|
-
def test_new_name_wins_when_both_are_staged(self):
|
|
400
|
-
new_parts, legacy_parts = next(iter(TestAssets.LEGACY_ALIASES.items()))
|
|
401
|
-
self._touch(*legacy_parts)
|
|
402
|
-
fresh = self._touch(*new_parts)
|
|
403
|
-
self.assertEqual(
|
|
404
|
-
os.path.normcase(TestAssets.path(*new_parts)), os.path.normcase(fresh)
|
|
405
|
-
)
|
|
376
|
+
def test_the_one_release_alias_map_is_gone(self):
|
|
377
|
+
"""Deleted 2026-09-23, as its own note asked: nothing maps a neutral
|
|
378
|
+
fixture name to a legacy one, so ``path`` is the plain join above."""
|
|
379
|
+
self.assertFalse(hasattr(TestAssets, "LEGACY_ALIASES"))
|
|
406
380
|
|
|
407
381
|
def test_unset_root_cannot_exist_under_either_guard(self):
|
|
408
382
|
from pathlib import Path
|
|
@@ -566,17 +540,13 @@ class PublicHygieneTest(unittest.TestCase):
|
|
|
566
540
|
The absolute studio paths, vendor name and person name went 2026-08-15; the
|
|
567
541
|
program identifiers went 2026-08-17. This is the guard that keeps them out.
|
|
568
542
|
Markdown is deliberately NOT scanned: ``CHANGELOG.md`` records what factually
|
|
569
|
-
happened
|
|
570
|
-
|
|
543
|
+
happened. Since the one-release alias map went (2026-09-23), no Python or C#
|
|
544
|
+
file may name the legacy assets -- ``base_test.py`` included.
|
|
571
545
|
"""
|
|
572
546
|
|
|
573
547
|
#: Program identifiers that must not appear as code identifiers.
|
|
574
548
|
PROGRAM_RE = re.compile(r"C-?130|C5M|FCR", re.IGNORECASE)
|
|
575
549
|
|
|
576
|
-
#: The one file allowed to name the legacy assets: the deprecation alias
|
|
577
|
-
#: map, which exists precisely so the old names keep resolving.
|
|
578
|
-
ALIAS_HOME = "base_test.py"
|
|
579
|
-
|
|
580
550
|
def test_no_module_filename_carries_a_program_identifier(self):
|
|
581
551
|
offenders = [
|
|
582
552
|
os.path.relpath(p, REPO_ROOT)
|
|
@@ -587,7 +557,7 @@ class PublicHygieneTest(unittest.TestCase):
|
|
|
587
557
|
|
|
588
558
|
def test_no_program_identifier_in_python_or_cs_source(self):
|
|
589
559
|
offenders = []
|
|
590
|
-
for path in _source_files(".py", ".cs"
|
|
560
|
+
for path in _source_files(".py", ".cs"):
|
|
591
561
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
592
562
|
for lineno, line in enumerate(fh, 1):
|
|
593
563
|
if self.PROGRAM_RE.search(line):
|
|
@@ -596,22 +566,6 @@ class PublicHygieneTest(unittest.TestCase):
|
|
|
596
566
|
)
|
|
597
567
|
self.assertEqual(offenders, [], f"program identifier in source: {offenders}")
|
|
598
568
|
|
|
599
|
-
def test_the_alias_map_is_the_only_home_for_the_legacy_names(self):
|
|
600
|
-
"""base_test may name them -- but only inside LEGACY_ALIASES."""
|
|
601
|
-
legacy = [part for parts in TestAssets.LEGACY_ALIASES.values() for part in parts]
|
|
602
|
-
self.assertTrue(
|
|
603
|
-
any(self.PROGRAM_RE.search(p) for p in legacy),
|
|
604
|
-
"the alias map should be carrying the legacy program-identified names",
|
|
605
|
-
)
|
|
606
|
-
path = os.path.join(os.path.dirname(base_test.__file__), self.ALIAS_HOME)
|
|
607
|
-
with open(path, encoding="utf-8") as fh:
|
|
608
|
-
source = fh.read()
|
|
609
|
-
start = source.index("LEGACY_ALIASES")
|
|
610
|
-
end = source.index("@classmethod", start)
|
|
611
|
-
outside = source[:start] + source[end:]
|
|
612
|
-
hits = sorted({m.group(0) for m in self.PROGRAM_RE.finditer(outside)})
|
|
613
|
-
self.assertEqual(hits, [], f"legacy identifier outside the alias map: {hits}")
|
|
614
|
-
|
|
615
569
|
|
|
616
570
|
if __name__ == "__main__":
|
|
617
571
|
unittest.main()
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
"""Integration test: the Maya→Unity clip-name JOIN contract under stress.
|
|
4
4
|
|
|
5
|
-
The whole shots→Unity scheme hinges on one identity: the
|
|
6
|
-
imported ``AnimationClip.name`` == the ``shot_metadata`` ``clip``
|
|
7
|
-
|
|
8
|
-
``[A-Za-z0-9_]
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
The whole shots→Unity scheme hinges on one identity: the shot name == the FBX
|
|
6
|
+
take name == the imported ``AnimationClip.name`` == the ``shot_metadata`` ``clip``
|
|
7
|
+
join key. The store refuses any name outside ``ShotStore.NAME_PATTERN``
|
|
8
|
+
(``[A-Za-z0-9_]``, unique ignoring case) instead of respelling it on export, so
|
|
9
|
+
what the user typed is what ships. This test proves both halves end to end:
|
|
10
|
+
Maya refuses the names the carrier would respell, and every name it accepts
|
|
11
|
+
arrives in Unity VERBATIM -- including the forms the pre-2026-09-18 respelling
|
|
12
|
+
collapsed or stripped (a doubled ``_``, a trailing ``_``), a leading digit and
|
|
13
|
+
a leading ``_``.
|
|
11
14
|
|
|
12
15
|
If Unity ever re-sanitized clip names (e.g. stripped a leading digit), the join
|
|
13
16
|
would silently break and ``ShotMetadataController.GetShot`` would return null; this
|
|
14
17
|
test is the guard against that.
|
|
15
18
|
"""
|
|
16
19
|
import os
|
|
17
|
-
import sys
|
|
18
20
|
import json
|
|
19
21
|
import shutil
|
|
20
22
|
import unittest
|
|
@@ -36,14 +38,16 @@ from unitytk import UnityLauncher, UnityFinder, TemplateDeployer
|
|
|
36
38
|
#: any other machine while the run still reported green.
|
|
37
39
|
MAYAPY = MayaEnv.mayapy()
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
41
|
+
# Names the store accepts; each must be the clip key AND the Unity clip name,
|
|
42
|
+
# character for character.
|
|
43
|
+
NAMES = [
|
|
44
|
+
"01_Start", # leading digit
|
|
45
|
+
"Fade__In", # doubled underscore (the legacy respelling collapsed it)
|
|
46
|
+
"Wipe_Out_", # trailing underscore (the legacy respelling stripped it)
|
|
47
|
+
"_Lead", # leading underscore
|
|
48
|
+
]
|
|
49
|
+
# Names the store refuses rather than let the export respell them.
|
|
50
|
+
REFUSED = ["01 Start", "Fade-In!", "fade__in"] # space, punctuation, case twin
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
def _unity_available() -> bool:
|
|
@@ -60,8 +64,10 @@ maya.standalone.initialize(name="python")
|
|
|
60
64
|
import maya.cmds as cmds
|
|
61
65
|
import maya.mel as mel
|
|
62
66
|
|
|
67
|
+
import pythontk as ptk
|
|
63
68
|
from mayatk.anim_utils.shots._shots import ShotStore
|
|
64
69
|
from mayatk.env_utils.fbx_utils import FbxUtils
|
|
70
|
+
from mayatk.node_utils.data_nodes import DataNodes
|
|
65
71
|
|
|
66
72
|
output_dir = sys.argv[1]
|
|
67
73
|
fbx_path = os.path.join(output_dir, "clip_name_contract.fbx").replace(os.sep, "/")
|
|
@@ -77,16 +83,24 @@ for t, v in ((1, 0.0), (5, 1.0), (10, 2.0), (15, 3.0), (20, 4.0),
|
|
|
77
83
|
|
|
78
84
|
store = ShotStore()
|
|
79
85
|
ShotStore.set_active(store)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
store.define_shot(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
names = json.loads(sys.argv[2])
|
|
87
|
+
for i, name in enumerate(names):
|
|
88
|
+
store.define_shot(name, 1 + 10 * i, 10 + 10 * i)
|
|
89
|
+
refused = []
|
|
90
|
+
for name in json.loads(sys.argv[3]):
|
|
91
|
+
try:
|
|
92
|
+
store.define_shot(name, 41, 45)
|
|
93
|
+
except ValueError:
|
|
94
|
+
refused.append(name)
|
|
86
95
|
|
|
87
96
|
view = store.to_export_view()
|
|
88
97
|
store.publish_export_view()
|
|
89
98
|
n_takes = FbxUtils.apply_takes_from_node()
|
|
99
|
+
# The take list the split realizes, read back off the carrier through the ONE
|
|
100
|
+
# reader of it (each shot_metadata clip's own range).
|
|
101
|
+
takes = ptk.SceneRecords.declared_takes(
|
|
102
|
+
lambda key: ptk.SceneRecords.resolve(key).load(DataNodes)
|
|
103
|
+
)
|
|
90
104
|
|
|
91
105
|
if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
|
|
92
106
|
cmds.loadPlugin("fbxmaya")
|
|
@@ -98,7 +112,9 @@ results = {
|
|
|
98
112
|
"fbx_path": fbx_path,
|
|
99
113
|
"fbx_exists": os.path.exists(fbx_path),
|
|
100
114
|
"n_takes": n_takes,
|
|
101
|
-
"clip_keys": [t["name"] for t in
|
|
115
|
+
"clip_keys": [t["name"] for t in takes],
|
|
116
|
+
"meta_keys": [s["clip"] for s in view["shot_metadata"]["shots"]],
|
|
117
|
+
"refused": refused,
|
|
102
118
|
}
|
|
103
119
|
with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
|
|
104
120
|
json.dump(results, f, indent=2)
|
|
@@ -214,7 +230,7 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
214
230
|
)
|
|
215
231
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
216
232
|
proc = subprocess.run(
|
|
217
|
-
[MAYAPY, script_path, self.temp_dir],
|
|
233
|
+
[MAYAPY, script_path, self.temp_dir, json.dumps(NAMES), json.dumps(REFUSED)],
|
|
218
234
|
capture_output=True, text=True, timeout=180, env=env,
|
|
219
235
|
)
|
|
220
236
|
if proc.stderr:
|
|
@@ -226,9 +242,12 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
226
242
|
r = self.__class__.maya_results
|
|
227
243
|
self.__class__.fbx_path = r["fbx_path"]
|
|
228
244
|
self.assertTrue(r["fbx_exists"])
|
|
229
|
-
self.assertEqual(r["n_takes"],
|
|
230
|
-
# Maya half
|
|
231
|
-
|
|
245
|
+
self.assertEqual(r["n_takes"], len(NAMES))
|
|
246
|
+
# The Maya half: every refused name was refused, every accepted name
|
|
247
|
+
# is its own take and join key, unchanged and in order.
|
|
248
|
+
self.assertEqual(r["refused"], REFUSED)
|
|
249
|
+
self.assertEqual(r["clip_keys"], NAMES)
|
|
250
|
+
self.assertEqual(r["meta_keys"], NAMES)
|
|
232
251
|
|
|
233
252
|
@MayaEnv.skip_unless_available()
|
|
234
253
|
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
@@ -304,18 +323,17 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
304
323
|
clip_names = set(unity.get("clip_names", []))
|
|
305
324
|
clip_keys = set(unity.get("clip_keys", []))
|
|
306
325
|
|
|
307
|
-
# Every
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
for sanitized in EXPECTED.values():
|
|
326
|
+
# Every authored name imported as a clip with that exact name. (Unity
|
|
327
|
+
# also imports a default "Take 001" clip for the full baked timeline; it
|
|
328
|
+
# carries no shot record and is ignored by the join — so this is a
|
|
329
|
+
# superset check, not equality.)
|
|
330
|
+
for name in NAMES:
|
|
313
331
|
self.assertIn(
|
|
314
|
-
|
|
315
|
-
f"Unity altered/dropped clip {
|
|
332
|
+
name, clip_names,
|
|
333
|
+
f"Unity altered/dropped clip {name!r}; imported: {sorted(clip_names)}",
|
|
316
334
|
)
|
|
317
|
-
# Metadata keys are exactly the
|
|
318
|
-
self.assertEqual(clip_keys, set(
|
|
335
|
+
# Metadata keys are exactly the authored shots, and each joins to a clip.
|
|
336
|
+
self.assertEqual(clip_keys, set(NAMES))
|
|
319
337
|
for key in clip_keys:
|
|
320
338
|
self.assertIn(key, clip_names, f"Join broken: key {key!r} has no clip.")
|
|
321
339
|
|
|
@@ -7,9 +7,9 @@ transports, curve rebinding, proxy-node deletion) and was the only one with no
|
|
|
7
7
|
batch-Unity coverage at all. This closes the import-side half:
|
|
8
8
|
|
|
9
9
|
1. Maya (mayapy): cube → two real ``EmissiveGroups.add_group`` calls with
|
|
10
|
-
distinct default weights → ``FbxUtils.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
distinct default weights → ``FbxUtils.publish()`` (the one publish of
|
|
11
|
+
every producer the Scene Exporter makes, validating the
|
|
12
|
+
``FbxUtils.PRODUCERS`` wiring) → export mesh + ``data_export`` carrier.
|
|
13
13
|
Then clear the groups and export a SECOND FBX with no channel.
|
|
14
14
|
2. Unity (batchmode): import the first FBX with the real templates
|
|
15
15
|
deployed and verify the populated ``EmissiveGroupController`` on the
|
|
@@ -30,7 +30,6 @@ Requires:
|
|
|
30
30
|
- Unity Hub with at least one editor installed (skips on license wall)
|
|
31
31
|
"""
|
|
32
32
|
import os
|
|
33
|
-
import sys
|
|
34
33
|
import json
|
|
35
34
|
import shutil
|
|
36
35
|
import unittest
|
|
@@ -70,6 +69,7 @@ import maya.standalone
|
|
|
70
69
|
maya.standalone.initialize(name="python")
|
|
71
70
|
import maya.cmds as cmds
|
|
72
71
|
|
|
72
|
+
import pythontk as ptk
|
|
73
73
|
from mayatk.mat_utils.emissive_groups import EmissiveGroups
|
|
74
74
|
from mayatk.env_utils.fbx_utils import FbxUtils
|
|
75
75
|
from mayatk.node_utils.data_nodes import DataNodes
|
|
@@ -86,10 +86,10 @@ EmissiveGroups.add_group("taillights", faces=[cube + ".f[2:3]"], default=0.25)
|
|
|
86
86
|
|
|
87
87
|
warnings = EmissiveGroups.validate()
|
|
88
88
|
|
|
89
|
-
# The
|
|
90
|
-
#
|
|
91
|
-
FbxUtils.
|
|
92
|
-
manifest = DataNodes.
|
|
89
|
+
# The one publish of every producer -- exactly what the Scene Exporter makes
|
|
90
|
+
# before an export (validates the FbxUtils.PRODUCERS wiring).
|
|
91
|
+
FbxUtils.publish()
|
|
92
|
+
manifest = DataNodes.read(ptk.Scope.DELIVERABLE, EmissiveGroups.DATA_CHANNEL)
|
|
93
93
|
|
|
94
94
|
if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
|
|
95
95
|
cmds.loadPlugin("fbxmaya")
|
|
@@ -109,8 +109,8 @@ _export(with_path)
|
|
|
109
109
|
# Second export with the channel gone — drives the stale-removal path.
|
|
110
110
|
for name in list(EmissiveGroups.list_groups()):
|
|
111
111
|
EmissiveGroups.remove_group(name)
|
|
112
|
-
FbxUtils.
|
|
113
|
-
cleared = DataNodes.
|
|
112
|
+
FbxUtils.publish()
|
|
113
|
+
cleared = DataNodes.read(ptk.Scope.DELIVERABLE, EmissiveGroups.DATA_CHANNEL)
|
|
114
114
|
|
|
115
115
|
without_path = os.path.join(output_dir, "Lamp_emissive_cleared.fbx")
|
|
116
116
|
_export(without_path)
|
|
@@ -233,7 +233,7 @@ class TestEmissiveGroupsIntegration(unittest.TestCase):
|
|
|
233
233
|
|
|
234
234
|
@MayaEnv.skip_unless_available()
|
|
235
235
|
def test_01_maya_export(self):
|
|
236
|
-
"""cube → two add_group calls →
|
|
236
|
+
"""cube → two add_group calls → FbxUtils.publish → FBX with carrier."""
|
|
237
237
|
script_path = os.path.join(self.temp_dir, "maya_export.py")
|
|
238
238
|
with open(script_path, "w", encoding="utf-8") as f:
|
|
239
239
|
f.write(MAYA_EXPORT_SCRIPT)
|
|
@@ -57,7 +57,7 @@ import maya.standalone
|
|
|
57
57
|
maya.standalone.initialize(name="python")
|
|
58
58
|
import maya.cmds as cmds
|
|
59
59
|
import maya.mel as mel
|
|
60
|
-
from mayatk import
|
|
60
|
+
from mayatk import RenderEffects
|
|
61
61
|
|
|
62
62
|
output_dir = sys.argv[1]
|
|
63
63
|
fbx_path = os.path.join(output_dir, "fade_window.fbx")
|
|
@@ -68,12 +68,12 @@ cmds.currentUnit(time="film")
|
|
|
68
68
|
cmds.playbackOptions(min=1, max=160)
|
|
69
69
|
|
|
70
70
|
cube = cmds.polyCube(name="WindowCube")[0]
|
|
71
|
-
|
|
71
|
+
RenderEffects.create(objects=[cube], mode="attribute")
|
|
72
72
|
|
|
73
73
|
# Short fade-in: frame 1 → 11 (10 frames, 0.417s @ 24fps)
|
|
74
|
-
|
|
74
|
+
RenderEffects.key_fade(objects=[cube], start=1, end=11, direction="in")
|
|
75
75
|
# Long fade-out: frame 91 → 151 (60 frames, 2.5s @ 24fps)
|
|
76
|
-
|
|
76
|
+
RenderEffects.key_fade(objects=[cube], start=91, end=151, direction="out")
|
|
77
77
|
|
|
78
78
|
# Add a translate carrier so the FBX has a clip spanning the timeline
|
|
79
79
|
cmds.setKeyframe(cube, attribute="translateY", time=1, value=0)
|
|
@@ -40,9 +40,7 @@ FBX_PATH = asset_path("fixture_keyed_visibility.fbx")
|
|
|
40
40
|
|
|
41
41
|
#: Name the FBX is copied to inside the Unity project. Fixed rather than
|
|
42
42
|
#: derived from FBX_PATH so the C# "Assets/<name>" literal and the importer
|
|
43
|
-
#: allowlist stay true
|
|
44
|
-
#: base_test.TestAssets.LEGACY_ALIASES, which keeps the pre-2026-08-17
|
|
45
|
-
#: fixture names resolving for one release).
|
|
43
|
+
#: allowlist stay true whatever the fixture file on disk is called.
|
|
46
44
|
FBX_ASSET_NAME = "fixture_keyed_visibility.fbx"
|
|
47
45
|
|
|
48
46
|
# Template CS files — need RenderEffectsController for the importer
|
|
@@ -87,18 +85,11 @@ EXPECTED_VISIBILITY_OBJECTS = [
|
|
|
87
85
|
# Unity finder
|
|
88
86
|
# ---------------------------------------------------------------------------
|
|
89
87
|
def _find_unity_exe() -> str:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
reverse=True,
|
|
96
|
-
)
|
|
97
|
-
for v in versions:
|
|
98
|
-
exe = os.path.join(hub, v, "Editor", "Unity.exe")
|
|
99
|
-
if os.path.isfile(exe):
|
|
100
|
-
return exe
|
|
101
|
-
return ""
|
|
88
|
+
"""The newest installed Unity Editor, found the way the product finds it
|
|
89
|
+
(every Hub install location, on every OS), or ""."""
|
|
90
|
+
from unitytk.launcher import UnityLauncher
|
|
91
|
+
|
|
92
|
+
return UnityLauncher().executable_path or ""
|
|
102
93
|
|
|
103
94
|
|
|
104
95
|
# ---------------------------------------------------------------------------
|
|
@@ -523,7 +514,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
523
514
|
logger.error(f"Unity log tail:\n{tail}")
|
|
524
515
|
# Check for compilation errors
|
|
525
516
|
if "error CS" in log_content:
|
|
526
|
-
errors = [
|
|
517
|
+
errors = [ln for ln in log_content.split("\n") if "error CS" in ln]
|
|
527
518
|
for e in errors[:10]:
|
|
528
519
|
logger.error(f" {e.strip()}")
|
|
529
520
|
|
|
@@ -545,7 +536,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
545
536
|
self.assertTrue(r.get("import_visibility"), "importVisibility was not enabled")
|
|
546
537
|
self.assertTrue(r.get("import_animation"), "importAnimation was not enabled")
|
|
547
538
|
self.assertFalse(r.get("resample_curves"), "resampleCurves should be OFF")
|
|
548
|
-
print(
|
|
539
|
+
print(" Import Settings: visibility=ON, animation=ON, resample=OFF")
|
|
549
540
|
|
|
550
541
|
# Prefab
|
|
551
542
|
self.assertTrue(r.get("prefab_loaded"), "FBX prefab failed to load")
|
|
@@ -626,7 +617,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
626
617
|
print(f" Found with curves: {len(found)}")
|
|
627
618
|
print(f" MISSING curves: {len(missing)}")
|
|
628
619
|
if missing:
|
|
629
|
-
print(
|
|
620
|
+
print("\n *** MISSING VISIBILITY CURVES ***")
|
|
630
621
|
for m in missing:
|
|
631
622
|
in_hierarchy = any(m in obj for obj in loc_objects)
|
|
632
623
|
status = "IN HIERARCHY" if in_hierarchy else "NOT IN FBX"
|
|
@@ -638,7 +629,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
638
629
|
if sample_error:
|
|
639
630
|
print(f"\n Sampling Error: {sample_error}")
|
|
640
631
|
elif sample_results:
|
|
641
|
-
print(
|
|
632
|
+
print("\n AnimationMode Sampling:")
|
|
642
633
|
for sr in sample_results:
|
|
643
634
|
print(f" {sr}")
|
|
644
635
|
|
|
@@ -657,8 +648,8 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
657
648
|
print(
|
|
658
649
|
f"\n WARNING: {len(missing)} expected objects missing visibility curves"
|
|
659
650
|
)
|
|
660
|
-
print(
|
|
661
|
-
print(
|
|
651
|
+
print(" These objects may need .visibility keyed directly in Maya")
|
|
652
|
+
print(" (display layer visibility does NOT export to FBX)")
|
|
662
653
|
|
|
663
654
|
print("\n VERDICT: See results above for visibility import status.")
|
|
664
655
|
print("=" * 70 + "\n")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
-
from unittest.mock import patch
|
|
4
|
+
from unittest.mock import patch
|
|
5
5
|
|
|
6
6
|
# Ensure unitytk and module imports work whether run directly or via runner
|
|
7
7
|
try:
|
|
@@ -15,34 +15,70 @@ from unitytk.launcher import UnityFinder, UnityLauncher
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class TestUnityLauncher(UnityTkTestCase):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
def test_find_editors_windows(self):
|
|
19
|
+
"""Windows: <Hub>/<ver>/Editor/Unity.exe, found through the shared
|
|
20
|
+
install scan (a real tree -- no listdir/exists mocks to drift from it).
|
|
21
|
+
A version folder without the binary is not an editor."""
|
|
22
|
+
import tempfile
|
|
23
|
+
|
|
24
|
+
with tempfile.TemporaryDirectory() as hub:
|
|
25
|
+
for version in ("2021.1.0f1", "2022.2.0b1"):
|
|
26
|
+
exe = os.path.join(hub, version, "Editor", "Unity.exe")
|
|
27
|
+
os.makedirs(os.path.dirname(exe))
|
|
28
|
+
open(exe, "w").close()
|
|
29
|
+
os.makedirs(os.path.join(hub, "2023.1.0f1", "Editor")) # no binary
|
|
30
|
+
with patch("platform.system", return_value="Windows"), patch(
|
|
31
|
+
"unitytk.launcher.UnityFinder.get_hub_editor_paths", return_value=[hub]
|
|
32
|
+
):
|
|
33
|
+
editors = UnityFinder.find_editors()
|
|
34
|
+
|
|
35
|
+
self.assertEqual(sorted(editors), ["2021.1.0f1", "2022.2.0b1"])
|
|
36
|
+
self.assertEqual(
|
|
37
|
+
os.path.normpath(editors["2021.1.0f1"]),
|
|
38
|
+
os.path.join(hub, "2021.1.0f1", "Editor", "Unity.exe"),
|
|
37
39
|
)
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def test_find_editors_reads_the_hubs_install_location_on_linux(self):
|
|
42
|
+
"""Linux: ~/Unity/Hub/Editor/<ver>/Editor/Unity, and the location picked
|
|
43
|
+
in the Hub's preferences (secondaryInstallPath.json, where new installs
|
|
44
|
+
go) -- scanned first, on every OS. Only the default was ever read."""
|
|
45
|
+
import json
|
|
46
|
+
import tempfile
|
|
47
|
+
|
|
48
|
+
with tempfile.TemporaryDirectory() as home:
|
|
49
|
+
default = os.path.join(home, "Unity", "Hub", "Editor")
|
|
50
|
+
custom = os.path.join(home, "editors")
|
|
51
|
+
config = os.path.join(home, ".config", "UnityHub")
|
|
52
|
+
for root, version in ((default, "2022.3.1f1"), (custom, "6000.0.1f1")):
|
|
53
|
+
exe = os.path.join(root, version, "Editor", "Unity")
|
|
54
|
+
os.makedirs(os.path.dirname(exe))
|
|
55
|
+
open(exe, "w").close()
|
|
56
|
+
os.makedirs(config)
|
|
57
|
+
with open(os.path.join(config, "secondaryInstallPath.json"), "w") as f:
|
|
58
|
+
json.dump(custom, f)
|
|
59
|
+
env = {"HOME": home, "XDG_CONFIG_HOME": os.path.join(home, ".config")}
|
|
60
|
+
|
|
61
|
+
def expanduser(path):
|
|
62
|
+
# A LEADING "~" only, as the real one: the scan expands every
|
|
63
|
+
# pattern, and a Windows temp dir can hold one mid-path (the
|
|
64
|
+
# 8.3 name C:\Users\RUNNER~1 on GitHub's runners).
|
|
65
|
+
return home + path[1:] if path.startswith("~") else path
|
|
66
|
+
|
|
67
|
+
with patch("platform.system", return_value="Linux"), patch.dict(
|
|
68
|
+
os.environ, env
|
|
69
|
+
), patch("os.path.expanduser", expanduser):
|
|
70
|
+
self.assertEqual(
|
|
71
|
+
[os.path.normpath(r) for r in UnityFinder.get_hub_editor_paths()],
|
|
72
|
+
[custom, default],
|
|
73
|
+
)
|
|
74
|
+
editors = {
|
|
75
|
+
v: os.path.normpath(e) for v, e in UnityFinder.find_editors().items()
|
|
76
|
+
}
|
|
77
|
+
self.assertEqual(
|
|
78
|
+
editors["6000.0.1f1"], os.path.join(custom, "6000.0.1f1", "Editor", "Unity")
|
|
79
|
+
)
|
|
43
80
|
self.assertEqual(
|
|
44
|
-
editors["
|
|
45
|
-
os.path.join("C:\\Hub", "2021.1.0f1", "Editor", "Unity.exe"),
|
|
81
|
+
editors["2022.3.1f1"], os.path.join(default, "2022.3.1f1", "Editor", "Unity")
|
|
46
82
|
)
|
|
47
83
|
|
|
48
84
|
@patch("unitytk.launcher.UnityFinder.find_editors")
|