unitytk 0.0.11__tar.gz → 0.0.13__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 (56) hide show
  1. {unitytk-0.0.11/unitytk.egg-info → unitytk-0.0.13}/PKG-INFO +1 -1
  2. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_asset_delivery.py +0 -1
  3. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_audio_events_integration.py +0 -1
  4. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_clip_name_contract_integration.py +54 -36
  5. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_emissive_groups_integration.py +11 -11
  6. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_fade_window_preservation.py +4 -4
  7. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_keyed_visibility.py +6 -6
  8. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_launcher.py +1 -1
  9. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_lightmap_metadata_integration.py +87 -4
  10. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_multi_section_audio_integration.py +9 -9
  11. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_render_opacity_controller.py +2 -2
  12. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_render_opacity_e2e.py +17 -16
  13. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_render_opacity_integration.py +3 -4
  14. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_shadow_plane_integration.py +14 -11
  15. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_shared_material_standalone.py +29 -30
  16. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_shot_metadata_integration.py +11 -2
  17. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_shots_audio_sidebyside_integration.py +22 -10
  18. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_standalone_enduser.py +4 -4
  19. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/__init__.py +1 -1
  20. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/LightmapMetadataController.cs +147 -12
  21. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/README.md +4 -4
  22. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/RenderEffectsController.cs +1 -1
  23. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/ShadowPlaneController.cs +2 -2
  24. {unitytk-0.0.11 → unitytk-0.0.13/unitytk.egg-info}/PKG-INFO +1 -1
  25. {unitytk-0.0.11 → unitytk-0.0.13}/LICENSE +0 -0
  26. {unitytk-0.0.11 → unitytk-0.0.13}/MANIFEST.in +0 -0
  27. {unitytk-0.0.11 → unitytk-0.0.13}/docs/README.md +0 -0
  28. {unitytk-0.0.11 → unitytk-0.0.13}/pyproject.toml +0 -0
  29. {unitytk-0.0.11 → unitytk-0.0.13}/setup.cfg +0 -0
  30. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_base_test.py +0 -0
  31. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_bool_to_fade_curve.py +0 -0
  32. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_controller_runtime_integration.py +0 -0
  33. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_deploy_templates.py +0 -0
  34. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_render_effects_e2e.py +0 -0
  35. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_run_tests.py +0 -0
  36. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_scene_builder.py +0 -0
  37. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_shadow_plane_runtime_integration.py +0 -0
  38. {unitytk-0.0.11 → unitytk-0.0.13}/test/test_temp_projects.py +0 -0
  39. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/asset_delivery.py +0 -0
  40. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/launcher.py +0 -0
  41. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/scene_builder.py +0 -0
  42. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/template_deployer.py +0 -0
  43. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/AudioEventController.cs +0 -0
  44. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/EmissiveGroupController.cs +0 -0
  45. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/EmissiveGroups.hlsl +0 -0
  46. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
  47. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  48. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/ShadowPlane.shader +0 -0
  49. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/ShadowPlaneHorizon.hlsl +0 -0
  50. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/ShotMetadataController.cs +0 -0
  51. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/UnitytkSettings.cs +0 -0
  52. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk/templates/__init__.py +0 -0
  53. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk.egg-info/SOURCES.txt +0 -0
  54. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk.egg-info/dependency_links.txt +0 -0
  55. {unitytk-0.0.11 → unitytk-0.0.13}/unitytk.egg-info/requires.txt +0 -0
  56. {unitytk-0.0.11 → unitytk-0.0.13}/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.11
3
+ Version: 0.0.13
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -10,7 +10,6 @@ from pythontk.core_utils.app_handoff import HandoffRequest, Payload
10
10
  from unitytk.asset_delivery import (
11
11
  CopyToAssetsDeliverer,
12
12
  FileToUnityBridge,
13
- CopyToAssetsDeliverer,
14
13
  )
15
14
 
16
15
 
@@ -25,7 +25,6 @@ Requires:
25
25
  - Unity Hub with at least one editor installed
26
26
  """
27
27
  import os
28
- import sys
29
28
  import json
30
29
  import shutil
31
30
  import unittest
@@ -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 FBX take name == the
6
- imported ``AnimationClip.name`` == the ``shot_metadata`` ``clip`` join key. Maya
7
- guarantees the first equality (single resolution) and sanitizes to strictly
8
- ``[A-Za-z0-9_]``; this test proves the *second* — that Unity preserves such names
9
- verbatim on FBX import — for the cases most likely to diverge: a space, embedded
10
- punctuation, a leading digit, and an exact collision (de-duped with a suffix).
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
- # Authored shot name -> expected sanitized clip key (Maya side, verified by the
40
- # mayatk pure tests). Unity must reproduce these exactly as clip names.
41
- EXPECTED = {
42
- "01 Start": "01_Start", # leading digit + space
43
- "Fade-In!": "Fade_In", # punctuation
44
- "Wipe Out": "Wipe_Out", # space
45
- "Wipe Out!": "Wipe_Out_1", # collides with "Wipe Out" -> suffixed
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
- # Order is significant: "Wipe Out" before "Wipe Out!" so the collision suffixes
81
- # the second one (matches the deterministic resolve over sorted order).
82
- store.define_shot("01 Start", 1, 10)
83
- store.define_shot("Fade-In!", 11, 20)
84
- store.define_shot("Wipe Out", 21, 30)
85
- store.define_shot("Wipe Out!", 31, 40)
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 view["fbx_takes"]],
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"], 4)
230
- # Maya half already produced the expected sanitized keys.
231
- self.assertEqual(set(r["clip_keys"]), set(EXPECTED.values()))
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 expected sanitized name imported as a clip with that exact name —
308
- # space→_, punctuation→_, leading digit, and collision suffix all survive
309
- # Unity's FBX import verbatim. (Unity also imports a default "Take 001"
310
- # clip for the full baked timeline; it carries no shot record and is
311
- # ignored by the join — so this is a superset check, not equality.)
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
- sanitized, clip_names,
315
- f"Unity altered/dropped clip {sanitized!r}; imported: {sorted(clip_names)}",
332
+ name, clip_names,
333
+ f"Unity altered/dropped clip {name!r}; imported: {sorted(clip_names)}",
316
334
  )
317
- # Metadata keys are exactly the four shots, and each joins to a clip.
318
- self.assertEqual(clip_keys, set(EXPECTED.values()))
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.run_export_preparers()`` (the
11
- registered known-producer refresh the Scene Exporter calls, validating
12
- the ``_KNOWN_PRODUCERS`` wiring) → export mesh + ``data_export`` carrier.
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 registered known-producer refresh — exactly what the Scene Exporter
90
- # calls before an export (validates the _KNOWN_PRODUCERS wiring).
91
- FbxUtils.run_export_preparers()
92
- manifest = DataNodes.get_export_string(EmissiveGroups.DATA_CHANNEL)
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.run_export_preparers()
113
- cleared = DataNodes.get_export_string(EmissiveGroups.DATA_CHANNEL)
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 → run_export_preparers → FBX with carrier."""
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 RenderOpacity
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
- RenderOpacity.create(objects=[cube], mode="attribute")
71
+ RenderEffects.create(objects=[cube], mode="attribute")
72
72
 
73
73
  # Short fade-in: frame 1 → 11 (10 frames, 0.417s @ 24fps)
74
- RenderOpacity.key_fade(objects=[cube], start=1, end=11, direction="in")
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
- RenderOpacity.key_fade(objects=[cube], start=91, end=151, direction="out")
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)
@@ -523,7 +523,7 @@ class TestKeyedVisibility(unittest.TestCase):
523
523
  logger.error(f"Unity log tail:\n{tail}")
524
524
  # Check for compilation errors
525
525
  if "error CS" in log_content:
526
- errors = [l for l in log_content.split("\n") if "error CS" in l]
526
+ errors = [ln for ln in log_content.split("\n") if "error CS" in ln]
527
527
  for e in errors[:10]:
528
528
  logger.error(f" {e.strip()}")
529
529
 
@@ -545,7 +545,7 @@ class TestKeyedVisibility(unittest.TestCase):
545
545
  self.assertTrue(r.get("import_visibility"), "importVisibility was not enabled")
546
546
  self.assertTrue(r.get("import_animation"), "importAnimation was not enabled")
547
547
  self.assertFalse(r.get("resample_curves"), "resampleCurves should be OFF")
548
- print(f" Import Settings: visibility=ON, animation=ON, resample=OFF")
548
+ print(" Import Settings: visibility=ON, animation=ON, resample=OFF")
549
549
 
550
550
  # Prefab
551
551
  self.assertTrue(r.get("prefab_loaded"), "FBX prefab failed to load")
@@ -626,7 +626,7 @@ class TestKeyedVisibility(unittest.TestCase):
626
626
  print(f" Found with curves: {len(found)}")
627
627
  print(f" MISSING curves: {len(missing)}")
628
628
  if missing:
629
- print(f"\n *** MISSING VISIBILITY CURVES ***")
629
+ print("\n *** MISSING VISIBILITY CURVES ***")
630
630
  for m in missing:
631
631
  in_hierarchy = any(m in obj for obj in loc_objects)
632
632
  status = "IN HIERARCHY" if in_hierarchy else "NOT IN FBX"
@@ -638,7 +638,7 @@ class TestKeyedVisibility(unittest.TestCase):
638
638
  if sample_error:
639
639
  print(f"\n Sampling Error: {sample_error}")
640
640
  elif sample_results:
641
- print(f"\n AnimationMode Sampling:")
641
+ print("\n AnimationMode Sampling:")
642
642
  for sr in sample_results:
643
643
  print(f" {sr}")
644
644
 
@@ -657,8 +657,8 @@ class TestKeyedVisibility(unittest.TestCase):
657
657
  print(
658
658
  f"\n WARNING: {len(missing)} expected objects missing visibility curves"
659
659
  )
660
- print(f" These objects may need .visibility keyed directly in Maya")
661
- print(f" (display layer visibility does NOT export to FBX)")
660
+ print(" These objects may need .visibility keyed directly in Maya")
661
+ print(" (display layer visibility does NOT export to FBX)")
662
662
 
663
663
  print("\n VERDICT: See results above for visibility import status.")
664
664
  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, MagicMock
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:
@@ -27,7 +27,6 @@ Requires:
27
27
  - Unity Hub with at least one editor installed
28
28
  """
29
29
  import os
30
- import sys
31
30
  import json
32
31
  import shutil
33
32
  import unittest
@@ -68,6 +67,7 @@ maya.standalone.initialize(name="python")
68
67
  import maya.cmds as cmds
69
68
  import maya.mel as mel
70
69
 
70
+ import pythontk as ptk
71
71
  from mayatk.light_utils.lightmap_baker.lightmap_baker import LightmapBaker
72
72
  from mayatk.uv_utils._uv_utils import UvUtils
73
73
  from mayatk.node_utils.data_nodes import DataNodes
@@ -104,7 +104,7 @@ except Exception:
104
104
 
105
105
  long = cmds.ls(cube, long=True)[0]
106
106
  baker = LightmapBaker(resolution=64)
107
- baker.commit_lightmap({long: tex_path}, intensity=1.0)
107
+ baker.commit_lightmap({long: tex_path})
108
108
 
109
109
  # Material must be untouched (the whole point of lighting-only mode).
110
110
  sgs_after = cmds.listConnections(shape, type="shadingEngine") or []
@@ -124,7 +124,9 @@ results = {
124
124
  "fbx_exists": os.path.exists(fbx_path),
125
125
  "material_kept": material_kept,
126
126
  "real_exr": real_exr,
127
- "metadata": json.loads(DataNodes.get_export_string(LightmapBaker.LIGHTMAP_METADATA)),
127
+ "metadata": json.loads(
128
+ DataNodes.read(ptk.Scope.DELIVERABLE, LightmapBaker.LIGHTMAP_METADATA)
129
+ ),
128
130
  }
129
131
  with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
130
132
  json.dump(results, f, indent=2)
@@ -284,7 +286,76 @@ public class LightmapMetaTestVerifier
284
286
  + (nonDirectional ? "true," : "false,"));
285
287
  sb.AppendLine(" \"tex_type_lightmap\": " + (texLightmap ? "true," : "false,"));
286
288
  sb.AppendLine(" \"tex_srgb_off\": " + (texSrgbOff ? "true," : "false,"));
287
- sb.AppendLine(" \"tex_mips_on\": " + (texMips ? "true" : "false"));
289
+ sb.AppendLine(" \"tex_mips_on\": " + (texMips ? "true," : "false,"));
290
+
291
+ // Same-named objects bind by hierarchy, as ONE assignment (the rule
292
+ // pythontk's GLB applier follows): FBX carries leaf names only, so a
293
+ // production room's two machine bodies are both BODY, and the
294
+ // manifest is the whole SCENE's, so a partial export carries records
295
+ // whose objects are not here. Built in memory; the REAL (private)
296
+ // ResolveRenderers settles each case.
297
+ string byHierarchy = "not run";
298
+ try
299
+ {
300
+ Func<string, Transform, bool, Transform> node = (nodeName, under, withRenderer) =>
301
+ {
302
+ var go = new GameObject(nodeName);
303
+ go.transform.SetParent(under, false);
304
+ if (withRenderer) go.AddComponent<MeshRenderer>();
305
+ return go.transform;
306
+ };
307
+ var resolve = typeof(LightmapMetadataApplier).GetMethod("ResolveRenderers",
308
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
309
+ // -> (bound, ambiguous) for these records under that root.
310
+ Func<Transform, LightmapRecord[], Tuple<Dictionary<LightmapRecord, Renderer>, HashSet<LightmapRecord>>> call =
311
+ (under, recs) =>
312
+ {
313
+ var args = new object[] { under, new List<LightmapRecord>(recs), null };
314
+ var map = (Dictionary<LightmapRecord, Renderer>)resolve.Invoke(null, args);
315
+ return Tuple.Create(map, (HashSet<LightmapRecord>)args[2]);
316
+ };
317
+ var root = new GameObject("fr_root").transform;
318
+ var lxLeaf = node("BODY", node("BODY", node("MACHINE_B", root, false), false), true)
319
+ .GetComponent<Renderer>();
320
+ var alLeaf = node("BODY", node("MACHINE_A", root, false), true)
321
+ .GetComponent<Renderer>();
322
+ var lx = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "MACHINE_B", "BODY", "BODY" } };
323
+ var al = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "MACHINE_A", "BODY" } };
324
+ var vend = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "VENDING", "BODY" } };
325
+
326
+ // Both bodies bind their own; a third BODY whose object is not
327
+ // here binds nothing and is not called ambiguous either.
328
+ var full = call(root, new[] { lx, al, vend });
329
+ bool own = full.Item1.TryGetValue(lx, out var r1) && r1 == lxLeaf
330
+ && full.Item1.TryGetValue(al, out var r2) && r2 == alLeaf;
331
+ bool absent = !full.Item1.ContainsKey(vend) && !full.Item2.Contains(vend);
332
+
333
+ // A partial export: only the MACHINE_A is here. The MACHINE_B's
334
+ // record found only that BODY and took it, overwriting its map.
335
+ var solo = new GameObject("fr_solo").transform;
336
+ var soloLeaf = node("BODY", node("MACHINE_A", solo, false), true)
337
+ .GetComponent<Renderer>();
338
+ var part = call(solo, new[] { lx, al });
339
+ bool partial = part.Item1.Count == 1
340
+ && part.Item1.TryGetValue(al, out var r3) && r3 == soloLeaf;
341
+
342
+ // Same-named records with no hierarchy are never guessed.
343
+ var bare = call(root, new[] {
344
+ new LightmapRecord { name = "BODY" }, new LightmapRecord { name = "BODY" } });
345
+ bool unguessed = bare.Item1.Count == 0 && bare.Item2.Count == 2;
346
+
347
+ byHierarchy = (own ? "ok" : "bodies missed") + "|"
348
+ + (absent ? "ok" : "an absent record bound") + "|"
349
+ + (partial ? "ok" : "partial export mis-bound") + "|"
350
+ + (unguessed ? "ok" : "no-hierarchy duplicates guessed");
351
+ UnityEngine.Object.DestroyImmediate(root.gameObject);
352
+ UnityEngine.Object.DestroyImmediate(solo.gameObject);
353
+ }
354
+ catch (Exception e)
355
+ {
356
+ byHierarchy = "raised: " + e.GetType().Name + " " + e.Message;
357
+ }
358
+ sb.AppendLine(" \"find_by_hierarchy\": \"" + Escape(byHierarchy) + "\"");
288
359
 
289
360
  sb.AppendLine("}");
290
361
 
@@ -366,6 +437,11 @@ class TestLightmapMetadataIntegration(unittest.TestCase):
366
437
  self.assertIn("Crate", objs)
367
438
  self.assertEqual(objs["Crate"]["map"], "Crate_Lightmap.exr")
368
439
  self.assertEqual(objs["Crate"]["uvIndex"], 1)
440
+ # The map is a join token by FILE NAME: the manifest carries no
441
+ # authoring folder (the absolute ``dir`` / ``dirs`` it once published
442
+ # resolved nowhere but the baking machine).
443
+ self.assertNotIn("dir", r["metadata"])
444
+ self.assertNotIn("dirs", r["metadata"])
369
445
 
370
446
  # ---- Step 2: Unity import ----------------------------------------
371
447
 
@@ -499,6 +575,13 @@ class TestLightmapMetadataIntegration(unittest.TestCase):
499
575
  self.assertTrue(unity.get("tex_srgb_off"), "sRGB not pinned off")
500
576
  self.assertTrue(unity.get("tex_mips_on"), "mipmaps not pinned on")
501
577
 
578
+ # (e) same-named objects bind by hierarchy, not first match (the
579
+ # production room: MACHINE_B|BODY|BODY beside MACHINE_A|BODY); a record
580
+ # without a hierarchy, as older manifests carry, still takes the first.
581
+ self.assertEqual(
582
+ unity.get("find_by_hierarchy"), "ok|ok|ok|ok", unity.get("find_by_hierarchy")
583
+ )
584
+
502
585
 
503
586
  if __name__ == "__main__":
504
587
  logging.basicConfig(level=logging.INFO)
@@ -903,7 +903,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
903
903
  audio_stems,
904
904
  f"{obj_name}: clip '{name}' not in source audio dir",
905
905
  )
906
- print(f" Clip names verified against source audio directory")
906
+ print(" Clip names verified against source audio directory")
907
907
 
908
908
  # ================================================================
909
909
  # 3. RenderEffectsController — components + Renderer gating
@@ -917,7 +917,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
917
917
  print(f" With Renderer: {oc_with_rend}")
918
918
  print(f" Without Renderer: {oc_without_rend}")
919
919
  if oc_details:
920
- print(f" Details (first 5):")
920
+ print(" Details (first 5):")
921
921
  for d in oc_details[:5]:
922
922
  print(f" {d}")
923
923
  if len(oc_details) > 5:
@@ -960,7 +960,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
960
960
  # ================================================================
961
961
  binding_freq = r.get("curve_binding_freq", [])
962
962
  if binding_freq:
963
- print(f"\n Curve binding frequency (top 20):")
963
+ print("\n Curve binding frequency (top 20):")
964
964
  for entry in binding_freq[:20]:
965
965
  print(f" {entry}")
966
966
  if len(binding_freq) > 20:
@@ -1023,7 +1023,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
1023
1023
  # Find keys like "opacity_curve_samples_Take 001"
1024
1024
  sample_keys = [k for k in r if k.startswith("opacity_curve_samples_")]
1025
1025
  if sample_keys:
1026
- print(f"\n Opacity curve samples:")
1026
+ print("\n Opacity curve samples:")
1027
1027
  for sk in sample_keys:
1028
1028
  samples = r[sk]
1029
1029
  print(f" [{sk}] ({len(samples)} curves sampled)")
@@ -1082,7 +1082,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
1082
1082
  audio_stems = set(_collect_audio_files(AUDIO_DIR))
1083
1083
  if event_time_keys:
1084
1084
  all_event_names_from_clip: set[str] = set()
1085
- print(f"\n Audio event times:")
1085
+ print("\n Audio event times:")
1086
1086
  for etk in event_time_keys:
1087
1087
  events = r[etk]
1088
1088
  print(f" [{etk}] {len(events)} events")
@@ -1205,7 +1205,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
1205
1205
  ri_oc = r.get("reimport_opacity_controllers", -1)
1206
1206
  ri_ac = r.get("reimport_audio_controllers", -1)
1207
1207
 
1208
- print(f"\n Reimport idempotency:")
1208
+ print("\n Reimport idempotency:")
1209
1209
  print(f" Events: {total_events} -> {ri_events}")
1210
1210
  print(f" Opa curves: {oc_curves_rebound} -> {ri_opacity}")
1211
1211
  print(f" audio_trigger: {at_curves} -> {ri_at}")
@@ -1248,7 +1248,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
1248
1248
  changed_500 = r.get("anim_sample_changed_by_t500", 0)
1249
1249
  anim_details = r.get("anim_sample_details", [])
1250
1250
 
1251
- print(f"\n AnimationMode runtime sampling:")
1251
+ print("\n AnimationMode runtime sampling:")
1252
1252
  print(f" Controllers sampled: {anim_count}")
1253
1253
  print(f" Changed by t=15s: {changed_15}")
1254
1254
  print(f" Changed by t=500s: {changed_500}")
@@ -1264,8 +1264,8 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
1264
1264
  self.assertGreater(
1265
1265
  changed_15 + changed_500,
1266
1266
  0,
1267
- f"No opacity values changed between t=0 and t=15s/t=500s — "
1268
- f"curves may not be driving RenderEffectsController.opacity",
1267
+ "No opacity values changed between t=0 and t=15s/t=500s — "
1268
+ "curves may not be driving RenderEffectsController.opacity",
1269
1269
  )
1270
1270
 
1271
1271
  print("=" * 70)
@@ -59,7 +59,7 @@ import maya.standalone
59
59
  maya.standalone.initialize(name="python")
60
60
  import maya.cmds as cmds
61
61
  import maya.mel as mel
62
- from mayatk import RenderOpacity
62
+ from mayatk import RenderEffects
63
63
 
64
64
  output_dir = sys.argv[1]
65
65
  fbx_path = os.path.join(output_dir, "fade_test.fbx")
@@ -71,7 +71,7 @@ cmds.playbackOptions(min=1, max=140)
71
71
  cube = cmds.polyCube(name="FadeCube")[0]
72
72
 
73
73
  # Initialize attribute-mode fade
74
- RenderOpacity.setup(objects=[cube], mode="attribute")
74
+ RenderEffects.create(objects=[cube], mode="attribute")
75
75
 
76
76
  # Animate opacity: Fade In -> Hold -> Fade Out
77
77
  cmds.cutKey(cube, attribute="opacity")
@@ -11,8 +11,8 @@ curve values, reimport idempotency).
11
11
  Pipeline:
12
12
  1. Maya (mayapy): build a cube + 'opacity' attr, key opacity in a
13
13
  4-key fade-in/hold/fade-out pattern, call
14
- :meth:`RenderOpacity.prepare_for_export` to dual-key visibility,
15
- export FBX.
14
+ :meth:`RenderEffects.prepare_for_export` to stage the curve proxy
15
+ (nothing keys visibility), export FBX.
16
16
  2. Unity (batchmode): deploy ``UnitytkSettings.cs`` (the shared
17
17
  ImportGate/CarrierImport home), ``RenderEffectsController.cs`` and
18
18
  ``AudioEventController.cs``. Import the FBX. Verify:
@@ -65,7 +65,7 @@ def _unity_available() -> bool:
65
65
  # ======================================================================
66
66
 
67
67
  MAYA_EXPORT_SCRIPT = r'''
68
- """Executed inside mayapy — keys opacity, dual-keys visibility, exports FBX."""
68
+ """Executed inside mayapy — keys opacity, stages the curve proxy, exports FBX."""
69
69
  import sys, os, json
70
70
 
71
71
  # mayatk/pythontk repo roots arrive via PYTHONPATH set by the parent test.
@@ -75,7 +75,7 @@ maya.standalone.initialize(name="python")
75
75
  import maya.cmds as cmds
76
76
  import maya.mel as mel
77
77
 
78
- from mayatk.mat_utils.render_opacity._render_opacity import RenderOpacity
78
+ from mayatk.mat_utils.render_opacity.render_effects import RenderEffects
79
79
 
80
80
  output_dir = sys.argv[1]
81
81
  fbx_path = os.path.join(output_dir, "opacity_test.fbx")
@@ -86,7 +86,7 @@ cmds.playbackOptions(min=1, max=140)
86
86
  cube = cmds.polyCube(name="fade_cube")[0]
87
87
 
88
88
  # Initialize attribute-mode opacity (creates the 'opacity' attr)
89
- RenderOpacity.create(objects=[cube], mode="attribute")
89
+ RenderEffects.create(objects=[cube], mode="attribute")
90
90
 
91
91
  # Author a 4-key fade-in → hold → fade-out pattern
92
92
  cmds.cutKey(cube, attribute="opacity")
@@ -97,11 +97,12 @@ cmds.setKeyframe(cube, attribute="opacity", time=140, value=0.0) # faded out
97
97
 
98
98
  assert cmds.attributeQuery("opacity", node=str(cube), exists=True)
99
99
 
100
- # Dual-key visibility from opacity — the Unity importer reconstructs the
101
- # fade from these step-keyed visibility transitions because Unity collapses
102
- # importAnimatedCustomProperties curves to the root and loses per-object
103
- # binding.
104
- synced = RenderOpacity.prepare_for_export(objects=[cube])
100
+ # Stage the curve-proxy transport: a fade_cube__opacity child carries the
101
+ # opacity curve on scale.x, which the importer rebinds to the controller
102
+ # (Unity flattens animated custom properties onto the root). Nothing keys
103
+ # visibility: presence is derived from the authored opacity downstream.
104
+ synced = RenderEffects.prepare_for_export()
105
+ proxies = cmds.ls("*__opacity") or []
105
106
 
106
107
  vis_keys = cmds.keyframe(cube, attribute="visibility", q=True, tc=True) or []
107
108
  opa_keys = cmds.keyframe(cube, attribute="opacity", q=True, tc=True) or []
@@ -127,6 +128,7 @@ results = {
127
128
  "opacity_keys": opa_keys,
128
129
  "visibility_keys": vis_keys,
129
130
  "synced_objects": synced,
131
+ "proxies": proxies,
130
132
  }
131
133
 
132
134
  with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
@@ -407,7 +409,7 @@ class TestRenderOpacityE2E(unittest.TestCase):
407
409
 
408
410
  @MayaEnv.skip_unless_available()
409
411
  def test_01_maya_export(self):
410
- """Author opacity, dual-key visibility via prepare_for_export, export FBX."""
412
+ """Author opacity, stage the curve proxy via prepare_for_export, export FBX."""
411
413
  script_path = os.path.join(self.temp_dir, "maya_export.py")
412
414
  with open(script_path, "w", encoding="utf-8") as f:
413
415
  f.write(MAYA_EXPORT_SCRIPT)
@@ -444,12 +446,11 @@ class TestRenderOpacityE2E(unittest.TestCase):
444
446
  r = self.__class__.maya_results
445
447
  self.__class__.fbx_path = r["fbx_path"]
446
448
  self.assertTrue(r["fbx_exists"])
447
- # prepare_for_export must have dual-keyed visibility
448
- self.assertGreater(
449
- len(r["visibility_keys"]),
450
- 0,
451
- "prepare_for_export should have synced visibility keys",
449
+ # prepare_for_export stages the curve proxy and keys no visibility
450
+ self.assertTrue(
451
+ r["proxies"], "prepare_for_export should have staged the opacity proxy"
452
452
  )
453
+ self.assertEqual(r["visibility_keys"], [], "no visibility mirror is written")
453
454
  # The opacity keys should match what we authored
454
455
  opa_set = {int(round(t)) for t in r["opacity_keys"]}
455
456
  self.assertEqual(opa_set, {1, 20, 100, 140})