unitytk 0.0.13__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.
Files changed (57) hide show
  1. {unitytk-0.0.13/unitytk.egg-info → unitytk-0.0.14}/PKG-INFO +5 -3
  2. {unitytk-0.0.13 → unitytk-0.0.14}/docs/README.md +1 -0
  3. {unitytk-0.0.13 → unitytk-0.0.14}/pyproject.toml +10 -2
  4. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_asset_delivery.py +1 -1
  5. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_base_test.py +8 -54
  6. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_keyed_visibility.py +6 -15
  7. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_launcher.py +61 -25
  8. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_multi_section_audio_integration.py +1 -4
  9. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_shadow_plane_integration.py +13 -27
  10. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_shared_material_standalone.py +5 -12
  11. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_standalone_enduser.py +7 -16
  12. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/__init__.py +1 -1
  13. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/launcher.py +70 -35
  14. {unitytk-0.0.13 → unitytk-0.0.14/unitytk.egg-info}/PKG-INFO +5 -3
  15. unitytk-0.0.14/unitytk.egg-info/requires.txt +1 -0
  16. unitytk-0.0.13/unitytk.egg-info/requires.txt +0 -1
  17. {unitytk-0.0.13 → unitytk-0.0.14}/LICENSE +0 -0
  18. {unitytk-0.0.13 → unitytk-0.0.14}/MANIFEST.in +0 -0
  19. {unitytk-0.0.13 → unitytk-0.0.14}/setup.cfg +0 -0
  20. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_audio_events_integration.py +0 -0
  21. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_bool_to_fade_curve.py +0 -0
  22. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_clip_name_contract_integration.py +0 -0
  23. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_controller_runtime_integration.py +0 -0
  24. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_deploy_templates.py +0 -0
  25. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_emissive_groups_integration.py +0 -0
  26. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_fade_window_preservation.py +0 -0
  27. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_lightmap_metadata_integration.py +0 -0
  28. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_render_effects_e2e.py +0 -0
  29. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_render_opacity_controller.py +0 -0
  30. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_render_opacity_e2e.py +0 -0
  31. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_render_opacity_integration.py +0 -0
  32. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_run_tests.py +0 -0
  33. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_scene_builder.py +0 -0
  34. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_shadow_plane_runtime_integration.py +0 -0
  35. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_shot_metadata_integration.py +0 -0
  36. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_shots_audio_sidebyside_integration.py +0 -0
  37. {unitytk-0.0.13 → unitytk-0.0.14}/test/test_temp_projects.py +0 -0
  38. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/asset_delivery.py +0 -0
  39. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/scene_builder.py +0 -0
  40. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/template_deployer.py +0 -0
  41. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/AudioEventController.cs +0 -0
  42. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/EmissiveGroupController.cs +0 -0
  43. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/EmissiveGroups.hlsl +0 -0
  44. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
  45. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/LightmapMetadataController.cs +0 -0
  46. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  47. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/README.md +0 -0
  48. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/RenderEffectsController.cs +0 -0
  49. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/ShadowPlane.shader +0 -0
  50. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/ShadowPlaneController.cs +0 -0
  51. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/ShadowPlaneHorizon.hlsl +0 -0
  52. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/ShotMetadataController.cs +0 -0
  53. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/UnitytkSettings.cs +0 -0
  54. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk/templates/__init__.py +0 -0
  55. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk.egg-info/SOURCES.txt +0 -0
  56. {unitytk-0.0.13 → unitytk-0.0.14}/unitytk.egg-info/dependency_links.txt +0 -0
  57. {unitytk-0.0.13 → 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.13
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 :: OS Independent
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.7.71
16
+ Requires-Dist: pythontk>=0.12.0
16
17
  Dynamic: license-file
17
18
 
18
19
  [![Tests](https://img.shields.io/badge/Tests-146%20passed-brightgreen.svg)](../test/)
20
+ ![Platform](https://img.shields.io/badge/Platform-Windows%2C%20Linux-lightgrey.svg)
19
21
 
20
22
  # unitytk
21
23
 
@@ -1,4 +1,5 @@
1
1
  [![Tests](https://img.shields.io/badge/Tests-146%20passed-brightgreen.svg)](../test/)
2
+ ![Platform](https://img.shields.io/badge/Platform-Windows%2C%20Linux-lightgrey.svg)
2
3
 
3
4
  # unitytk
4
5
 
@@ -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 :: OS Independent",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Operating System :: POSIX :: Linux",
16
17
  ]
17
- dependencies = ["pythontk>=0.7.71"]
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,7 +6,7 @@ import shutil
6
6
  import tempfile
7
7
  import unittest
8
8
 
9
- from pythontk.core_utils.app_handoff import HandoffRequest, Payload
9
+ from pythontk import HandoffRequest, Payload
10
10
  from unitytk.asset_delivery import (
11
11
  CopyToAssetsDeliverer,
12
12
  FileToUnityBridge,
@@ -356,7 +356,7 @@ class SuiteOptionsTest(unittest.TestCase):
356
356
 
357
357
 
358
358
  class TestAssetsTest(unittest.TestCase):
359
- """Fixture resolution, including the one-release legacy-name alias map."""
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 test_alias_map_is_not_empty(self):
384
- self.assertTrue(TestAssets.LEGACY_ALIASES)
385
-
386
- def test_legacy_name_resolves_when_only_it_is_staged(self):
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 and ``CLAUDE.md`` has to document the legacy asset names so a user
570
- can rename an already-staged tree.
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", exclude=(self.ALIAS_HOME,)):
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()
@@ -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 whichever name the fixture carries on disk (see
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
- hub = r"C:\Program Files\Unity\Hub\Editor"
91
- if not os.path.isdir(hub):
92
- return ""
93
- versions = sorted(
94
- [d for d in os.listdir(hub) if os.path.isdir(os.path.join(hub, d))],
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
  # ---------------------------------------------------------------------------
@@ -15,34 +15,70 @@ from unitytk.launcher import UnityFinder, UnityLauncher
15
15
 
16
16
 
17
17
  class TestUnityLauncher(UnityTkTestCase):
18
- @patch("unitytk.launcher.UnityFinder.get_hub_editor_path")
19
- @patch("os.listdir")
20
- @patch("os.path.isdir")
21
- @patch("os.path.exists")
22
- @patch("platform.system")
23
- def test_find_editors_windows(
24
- self, mock_system, mock_exists, mock_isdir, mock_listdir, mock_get_hub
25
- ):
26
- """Verify that find_editors correctly identifies Unity executables on Windows."""
27
- mock_system.return_value = "windows"
28
- mock_get_hub.return_value = "C:\\Hub"
29
- mock_listdir.return_value = ["2021.1.0f1", "2022.2.0b1"]
30
-
31
- # Mock isdir behavior: return True if path contains the mocked versions
32
- mock_isdir.return_value = True
33
-
34
- # Mock existence: Hub exists, and the Unity.exe inside the version folders exist
35
- mock_exists.side_effect = lambda p: (
36
- True if p == "C:\\Hub" or "Unity.exe" in p else False
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
- editors = UnityFinder.find_editors()
40
-
41
- self.assertIn("2021.1.0f1", editors)
42
- self.assertIn("2022.2.0b1", editors)
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["2021.1.0f1"],
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")
@@ -23,9 +23,6 @@ Source files (resolved under ``UNITYTK_TEST_ASSETS``; unset -> the suite skips):
23
23
  FBX: $UNITYTK_TEST_ASSETS/audio_files/fixture_multi_section_audio.fbx
24
24
  Audio: $UNITYTK_TEST_ASSETS/audio_files/fixture_audio_clips/ (73 MP3s)
25
25
 
26
- Pre-2026-08-17 fixture names still resolve for one release --
27
- ``base_test.TestAssets.LEGACY_ALIASES``.
28
-
29
26
  The FBX contains:
30
27
  - ``audio_manifest`` string attributes on 3 objects (sections A, B, C)
31
28
  with frame:eventName pairs whose names match the MP3 stems.
@@ -66,7 +63,7 @@ AUDIO_DIR = asset_path("audio_files", "fixture_audio_clips")
66
63
 
67
64
  #: Name the FBX is copied to inside the Unity project. Fixed rather than
68
65
  #: derived from FBX_PATH so the C# "Assets/<name>" literal below stays true
69
- #: whichever name the fixture carries on disk (see TestAssets.LEGACY_ALIASES).
66
+ #: whatever the fixture file on disk is called.
70
67
  FBX_ASSET_NAME = "fixture_multi_section_audio.fbx"
71
68
 
72
69
  # Expected counts (from binary scan 2026-02-24)
@@ -115,11 +115,10 @@ cmds.file(
115
115
  )
116
116
  mel.eval("FBXExportEmbeddedTextures -v false")
117
117
 
118
- # Third scenario: a schema-v2 fixture that pins the ENGINE's runtime placement
119
- # against the DCC's baked keys through a real FBX import. The producer still
120
- # publishes v1, so the v2 payload is hand-built from the rigs' own stamped
121
- # attrs (the contract's field semantics: mayatk/docs/shadow_rig_morphing.md).
122
- # Two rigs on two asymmetrically placed targets: a positional locator and a
118
+ # Third scenario: the v2 record the producer publishes pins the ENGINE's
119
+ # runtime placement against the DCC's baked keys through a real FBX import
120
+ # (the contract's field semantics: mayatk/docs/shadow_rig_morphing.md). Two
121
+ # rigs on two asymmetrically placed targets: a positional locator and a
123
122
  # directionalLight at an asymmetric rotation (so a wrong axis cannot hide).
124
123
  from mayatk.core_utils._core_utils import CoreUtils
125
124
 
@@ -136,33 +135,12 @@ rig_pt = ShadowRig.create([drum], source_name="pointSrc", light_pos=(-2.0, 7.0,
136
135
  rig_sun.bake(1, 3)
137
136
  rig_pt.bake(1, 3)
138
137
  FbxUtils.publish()
138
+ v2_payload = ptk.SceneRecords.SHADOWS.load(DataNodes)
139
139
 
140
140
 
141
141
  def _leaf(node):
142
142
  return CoreUtils.leaf_name(node)
143
143
 
144
-
145
- def _record(rig, source_type):
146
- plane = rig.shadow_plane
147
- g = lambda attr: float(cmds.getAttr(f"{plane}.{attr}"))
148
- return {
149
- "name": _leaf(plane), "type": "projected",
150
- "texture": os.path.basename(rig.texture_path), "intensity": 1.0,
151
- "source": _leaf(rig.light), "source_type": source_type,
152
- "source_size": 0.0, "source_angle": 0.0, "follow_source": True,
153
- "contact": _leaf(rig.contact_locator), "ground": g("groundHeight"),
154
- "radius": g("footprintRadius"), "height": g("objectHeight"),
155
- "max_stretch": g("maxStretch"),
156
- "canvas": [g("canvasU0"), g("canvasU1"), g("canvasW0"), g("canvasW1")],
157
- }
158
-
159
-
160
- v2_payload = {"version": 2, "unit_scale": 0.01,
161
- "planes": [_record(rig_sun, "directional"), _record(rig_pt, "point")]}
162
- DataNodes.write(
163
- ptk.Scope.DELIVERABLE, ShadowRig.SHADOW_METADATA, json.dumps(v2_payload)
164
- )
165
-
166
144
  cmds.currentTime(1)
167
145
 
168
146
 
@@ -486,6 +464,14 @@ class TestShadowPlaneIntegration(unittest.TestCase):
486
464
  recs = {p["name"]: p for p in meta.get("planes", [])}
487
465
  self.assertIn("Box_shadow", recs, f"shadow_metadata missing: {meta}")
488
466
  self.assertEqual(recs["Box_shadow"]["texture"], "Box_shadow.png")
467
+ # The producer publishes the schema ShadowPlaneController reads (v2),
468
+ # in Maya's centimetres; the runtime fixture below ships that record.
469
+ self.assertEqual(meta.get("version"), 2)
470
+ self.assertAlmostEqual(meta.get("unit_scale"), 0.01, places=6)
471
+ fixture = r["fixture"]
472
+ published = {p["name"] for p in (fixture["payload"] or {}).get("planes", [])}
473
+ for key in ("sun", "point"):
474
+ self.assertIn(fixture[key]["plane"], published)
489
475
  self.assertTrue(r["embedded_fbx_exists"], "embedded FBX export failed")
490
476
  self.assertEqual(r["embedded_texture"], "Box_keyLight_shadow.png")
491
477
 
@@ -57,18 +57,11 @@ SHARED_MAT_NAME = "shared_mat"
57
57
 
58
58
 
59
59
  def _find_unity_exe() -> str:
60
- hub = r"C:\Program Files\Unity\Hub\Editor"
61
- if not os.path.isdir(hub):
62
- return ""
63
- versions = sorted(
64
- [d for d in os.listdir(hub) if os.path.isdir(os.path.join(hub, d))],
65
- reverse=True,
66
- )
67
- for v in versions:
68
- exe = os.path.join(hub, v, "Editor", "Unity.exe")
69
- if os.path.isfile(exe):
70
- return exe
71
- return ""
60
+ """The newest installed Unity Editor, found the way the product finds it
61
+ (every Hub install location, on every OS), or ""."""
62
+ from unitytk.launcher import UnityLauncher
63
+
64
+ return UnityLauncher().executable_path or ""
72
65
 
73
66
 
74
67
  def _prerequisites_ok() -> bool:
@@ -44,9 +44,8 @@ AUDIO_DIR = asset_path("audio_files", "fixture_audio_clips")
44
44
 
45
45
  #: Name the FBX is copied to inside the Unity project, and the name the
46
46
  #: importer allowlist is written against. Fixed rather than derived from
47
- #: FBX_PATH so the C# "Assets/<name>" literals stay true whichever name the
48
- #: fixture carries on disk (base_test.TestAssets.LEGACY_ALIASES keeps the
49
- #: pre-2026-08-17 fixture names resolving for one release).
47
+ #: FBX_PATH so the C# "Assets/<name>" literals stay true whatever the
48
+ #: fixture file on disk is called.
50
49
  FBX_ASSET_NAME = "fixture_multi_section_audio.fbx"
51
50
 
52
51
  #: Subfolder the all-in-one deployment scenario builds under Assets/.
@@ -87,19 +86,11 @@ MAX_KEYS_PER_CURVE = 30
87
86
 
88
87
 
89
88
  def _find_unity_exe() -> str:
90
- """Find the latest Unity Editor executable via Hub default path."""
91
- hub = r"C:\Program Files\Unity\Hub\Editor"
92
- if not os.path.isdir(hub):
93
- return ""
94
- versions = sorted(
95
- [d for d in os.listdir(hub) if os.path.isdir(os.path.join(hub, d))],
96
- reverse=True,
97
- )
98
- for v in versions:
99
- exe = os.path.join(hub, v, "Editor", "Unity.exe")
100
- if os.path.isfile(exe):
101
- return exe
102
- return ""
89
+ """The newest installed Unity Editor, found the way the product finds it
90
+ (every Hub install location, on every OS), or ""."""
91
+ from unitytk.launcher import UnityLauncher
92
+
93
+ return UnityLauncher().executable_path or ""
103
94
 
104
95
 
105
96
  def _sources_ok() -> bool:
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.13"
1
+ __version__ = "0.0.14"
2
2
 
3
3
  from .launcher import UnityLauncher, UnityFinder
4
4
  from .scene_builder import SceneBuilder
@@ -1,4 +1,6 @@
1
1
  """Locate installed Unity Editor versions and launch the Editor or a Standalone Player."""
2
+ import glob
3
+ import json
2
4
  import os
3
5
  import re
4
6
  import platform
@@ -7,10 +9,11 @@ from typing import Optional, List, Dict, Tuple
7
9
  import subprocess
8
10
 
9
11
  try:
10
- from pythontk import AppLauncher
12
+ from pythontk import AppLauncher, UserConfig
11
13
  except ImportError:
12
14
  # Fallback for when pythontk is not fully bootstrapped or direct import needed
13
15
  from pythontk.core_utils.app_launcher import AppLauncher
16
+ from pythontk.core_utils.user_config import UserConfig
14
17
 
15
18
  logger = logging.getLogger(__name__)
16
19
 
@@ -31,62 +34,94 @@ LICENSE_FAILURE_MARKERS = (
31
34
  class UnityFinder:
32
35
  """Helper class to locate Unity installations."""
33
36
 
37
+ #: Where the editor binary sits inside a Hub version folder, per OS
38
+ #: (``platform.system().lower()``). The version folder is that many levels
39
+ #: above the binary.
40
+ EDITOR_BINARY = {
41
+ "windows": ("Editor", "Unity.exe"),
42
+ "darwin": ("Unity.app", "Contents", "MacOS", "Unity"),
43
+ "linux": ("Editor", "Unity"),
44
+ }
45
+
34
46
  @staticmethod
35
47
  def get_hub_editor_path() -> Optional[str]:
36
48
  """Returns the default Unity Hub Editor installation directory."""
37
49
  system = platform.system().lower()
38
50
  if system == "windows":
39
- return r"C:\Program Files\Unity\Hub\Editor"
51
+ program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
52
+ return os.path.join(program_files, "Unity", "Hub", "Editor")
40
53
  elif system == "darwin": # macOS
41
54
  return "/Applications/Unity/Hub/Editor"
42
55
  elif system == "linux":
43
56
  return os.path.expanduser("~/Unity/Hub/Editor")
44
57
  return None
45
58
 
59
+ @staticmethod
60
+ def _hub_config_dir() -> Optional[str]:
61
+ """Unity Hub's own config folder (where it records a custom install path)."""
62
+ system = platform.system().lower()
63
+ if system == "windows":
64
+ base = os.environ.get("APPDATA")
65
+ return os.path.join(base, "UnityHub") if base else None
66
+ if system == "darwin":
67
+ return os.path.expanduser("~/Library/Application Support/UnityHub")
68
+ if system == "linux":
69
+ return os.path.join(UserConfig.xdg_home("config"), "UnityHub")
70
+ return None
71
+
72
+ @staticmethod
73
+ def get_hub_editor_paths() -> List[str]:
74
+ """Every folder the Hub installs editors into: the location chosen in the
75
+ Hub's preferences (``secondaryInstallPath.json``, where new installs go)
76
+ first, then the default -- on every OS."""
77
+ roots: List[str] = []
78
+ config = UnityFinder._hub_config_dir()
79
+ if config:
80
+ try:
81
+ with open(
82
+ os.path.join(config, "secondaryInstallPath.json"), encoding="utf-8"
83
+ ) as f:
84
+ custom = json.load(f)
85
+ if isinstance(custom, str) and custom:
86
+ roots.append(custom)
87
+ except (OSError, ValueError):
88
+ pass
89
+ default = UnityFinder.get_hub_editor_path()
90
+ if default and default not in roots:
91
+ roots.append(default)
92
+ return roots
93
+
46
94
  @staticmethod
47
95
  def find_editors() -> Dict[str, str]:
48
96
  """
49
- Scans default Hub locations for installed Unity Editors.
97
+ Scans every Hub install location for installed Unity Editors.
50
98
 
51
99
  Returns:
52
100
  Dict[str, str]: A dictionary of {version: executable_path}.
53
101
  """
54
- hub_path = UnityFinder.get_hub_editor_path()
55
- if not hub_path or not os.path.exists(hub_path):
56
- logger.warning(f"Unity Hub path not found: {hub_path}")
102
+ roots = [r for r in UnityFinder.get_hub_editor_paths() if os.path.isdir(r)]
103
+ if not roots:
104
+ logger.warning(
105
+ f"Unity Hub editor folder not found: {UnityFinder.get_hub_editor_paths()}"
106
+ )
57
107
  return {}
58
108
 
59
- found_editors = {}
60
- system = platform.system().lower()
109
+ tail = UnityFinder.EDITOR_BINARY.get(platform.system().lower())
110
+ if tail is None:
111
+ return {}
61
112
 
62
- # Look for subdirectories (versions)
113
+ # The ecosystem's install scan, not a hand-rolled listdir: one pattern
114
+ # per root, in root priority (the Hub's chosen location, then the
115
+ # default), each ranked newest first. The first root holding a version
116
+ # wins it.
117
+ found_editors: Dict[str, str] = {}
63
118
  try:
64
- versions = [
65
- d
66
- for d in os.listdir(hub_path)
67
- if os.path.isdir(os.path.join(hub_path, d))
68
- ]
69
- for version in versions:
70
- editor_dir = os.path.join(hub_path, version)
71
- exe_path = None
72
-
73
- if system == "windows":
74
- candidate = os.path.join(editor_dir, "Editor", "Unity.exe")
75
- if os.path.exists(candidate):
76
- exe_path = candidate
77
- elif system == "darwin":
78
- candidate = os.path.join(
79
- editor_dir, "Unity.app", "Contents", "MacOS", "Unity"
80
- )
81
- if os.path.exists(candidate):
82
- exe_path = candidate
83
- elif system == "linux":
84
- candidate = os.path.join(editor_dir, "Editor", "Unity")
85
- if os.path.exists(candidate):
86
- exe_path = candidate
87
-
88
- if exe_path:
89
- found_editors[version] = exe_path
119
+ patterns = [os.path.join(glob.escape(root), "*", *tail) for root in roots]
120
+ for exe_path in AppLauncher.scan_install_dirs(patterns):
121
+ version_dir = exe_path
122
+ for _ in tail:
123
+ version_dir = os.path.dirname(version_dir)
124
+ found_editors.setdefault(os.path.basename(version_dir), exe_path)
90
125
  except Exception as e:
91
126
  logger.error(f"Error scanning for Unity Editors: {e}")
92
127
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.13
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 :: OS Independent
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.7.71
16
+ Requires-Dist: pythontk>=0.12.0
16
17
  Dynamic: license-file
17
18
 
18
19
  [![Tests](https://img.shields.io/badge/Tests-146%20passed-brightgreen.svg)](../test/)
20
+ ![Platform](https://img.shields.io/badge/Platform-Windows%2C%20Linux-lightgrey.svg)
19
21
 
20
22
  # unitytk
21
23
 
@@ -0,0 +1 @@
1
+ pythontk>=0.12.0
@@ -1 +0,0 @@
1
- pythontk>=0.7.71
File without changes
File without changes
File without changes