unitytk 0.0.4__tar.gz → 0.0.6__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 (49) hide show
  1. {unitytk-0.0.4/unitytk.egg-info → unitytk-0.0.6}/PKG-INFO +2 -2
  2. {unitytk-0.0.4 → unitytk-0.0.6}/docs/README.md +1 -1
  3. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_audio_events_integration.py +5 -1
  4. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_clip_name_contract_integration.py +5 -1
  5. unitytk-0.0.6/test/test_controller_runtime_integration.py +323 -0
  6. unitytk-0.0.6/test/test_emissive_groups_integration.py +438 -0
  7. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_lightmap_metadata_integration.py +5 -1
  8. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_render_opacity_controller.py +46 -2
  9. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_render_opacity_e2e.py +5 -1
  10. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_render_opacity_integration.py +5 -1
  11. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_shadow_plane_integration.py +5 -1
  12. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_shot_metadata_integration.py +5 -1
  13. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_shots_audio_sidebyside_integration.py +5 -1
  14. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/__init__.py +1 -1
  15. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/EmissiveGroupController.cs +77 -16
  16. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/EmissiveGroupsLit.shader +7 -1
  17. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/README.md +12 -3
  18. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/RenderOpacityController.cs +87 -11
  19. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/ShadowPlaneController.cs +6 -0
  20. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/UnitytkSettings.cs +20 -0
  21. {unitytk-0.0.4 → unitytk-0.0.6/unitytk.egg-info}/PKG-INFO +2 -2
  22. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk.egg-info/SOURCES.txt +2 -0
  23. {unitytk-0.0.4 → unitytk-0.0.6}/LICENSE +0 -0
  24. {unitytk-0.0.4 → unitytk-0.0.6}/MANIFEST.in +0 -0
  25. {unitytk-0.0.4 → unitytk-0.0.6}/pyproject.toml +0 -0
  26. {unitytk-0.0.4 → unitytk-0.0.6}/setup.cfg +0 -0
  27. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_asset_delivery.py +0 -0
  28. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_bool_to_fade_curve.py +0 -0
  29. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_c130_fcr_integration.py +0 -0
  30. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_c5m_visibility.py +0 -0
  31. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_deploy_templates.py +0 -0
  32. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_fade_window_preservation.py +0 -0
  33. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_launcher.py +0 -0
  34. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_scene_builder.py +0 -0
  35. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_shared_material_standalone.py +0 -0
  36. {unitytk-0.0.4 → unitytk-0.0.6}/test/test_standalone_enduser.py +0 -0
  37. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/asset_delivery.py +0 -0
  38. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/launcher.py +0 -0
  39. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/scene_builder.py +0 -0
  40. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/template_deployer.py +0 -0
  41. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/AudioEventController.cs +0 -0
  42. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/EmissiveGroups.hlsl +0 -0
  43. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/LightmapMetadataController.cs +0 -0
  44. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  45. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/ShotMetadataController.cs +0 -0
  46. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk/templates/__init__.py +0 -0
  47. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk.egg-info/dependency_links.txt +0 -0
  48. {unitytk-0.0.4 → unitytk-0.0.6}/unitytk.egg-info/requires.txt +0 -0
  49. {unitytk-0.0.4 → unitytk-0.0.6}/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.4
3
+ Version: 0.0.6
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -15,7 +15,7 @@ License-File: LICENSE
15
15
  Requires-Dist: pythontk>=0.7.71
16
16
  Dynamic: license-file
17
17
 
18
- [![Tests](https://img.shields.io/badge/Tests-116%20passed-brightgreen.svg)](../test/)
18
+ [![Tests](https://img.shields.io/badge/Tests-124%20passed-brightgreen.svg)](../test/)
19
19
 
20
20
  # unitytk
21
21
 
@@ -1,4 +1,4 @@
1
- [![Tests](https://img.shields.io/badge/Tests-116%20passed-brightgreen.svg)](../test/)
1
+ [![Tests](https://img.shields.io/badge/Tests-124%20passed-brightgreen.svg)](../test/)
2
2
 
3
3
  # unitytk
4
4
 
@@ -461,7 +461,11 @@ class TestAudioEventsIntegration(unittest.TestCase):
461
461
 
462
462
  @classmethod
463
463
  def setUpClass(cls):
464
- cls.temp_dir = tempfile.mkdtemp(prefix="audio_events_integ_")
464
+ # In-repo scratch (see CLAUDE.md), not the system temp dir —
465
+ # these preserve a full Unity project on purpose.
466
+ _temp_base = os.path.join(os.path.dirname(__file__), "temp_tests")
467
+ os.makedirs(_temp_base, exist_ok=True)
468
+ cls.temp_dir = tempfile.mkdtemp(prefix="audio_events_integ_", dir=_temp_base)
465
469
  cls.fbx_path = None
466
470
  cls.maya_results = None
467
471
  cls.unity_results = None
@@ -198,7 +198,11 @@ class TestClipNameContract(unittest.TestCase):
198
198
 
199
199
  @classmethod
200
200
  def setUpClass(cls):
201
- cls.temp_dir = tempfile.mkdtemp(prefix="clip_name_contract_")
201
+ # In-repo scratch (see CLAUDE.md), not the system temp dir —
202
+ # these preserve a full Unity project on purpose.
203
+ _temp_base = os.path.join(os.path.dirname(__file__), "temp_tests")
204
+ os.makedirs(_temp_base, exist_ok=True)
205
+ cls.temp_dir = tempfile.mkdtemp(prefix="clip_name_contract_", dir=_temp_base)
202
206
  cls.fbx_path = None
203
207
  cls.maya_results = None
204
208
  cls.unity_results = None
@@ -0,0 +1,323 @@
1
+ # !/usr/bin/python
2
+ # coding=utf-8
3
+ """Integration test: unitytk runtime controller behaviour inside a real Unity.
4
+
5
+ The import-side templates are well covered by the per-channel integration
6
+ tests, but the *runtime* half (what the MonoBehaviours actually do to a
7
+ Renderer) had no live coverage at all. This test closes that gap with a
8
+ Maya-free batch Unity run:
9
+
10
+ 1. Create a Unity project and deploy the real template package.
11
+ 2. In an Editor script, build a cube with a Standard material, drive
12
+ ``RenderOpacityController`` / ``EmissiveGroupController`` directly, and
13
+ read the resulting MaterialPropertyBlock back.
14
+
15
+ Covers three behaviours that inspection alone can't settle:
16
+
17
+ a. **Built-in RP color-property resolution.** A fresh Unity project is
18
+ Built-in RP, whose materials expose ``_Color`` — not the ``_BaseColor``
19
+ the component serializes by default. When the two disagree the apply
20
+ returns early and the object hard-pops at zero instead of fading.
21
+ ``Reset()`` auto-detects on the normal import path (measured in
22
+ ``test_render_opacity_controller``), but it cannot help when the
23
+ material arrives after the component, when a prefab authored under one
24
+ pipeline is opened under another, or in a player build where ``Reset``
25
+ does not exist at all — so the runtime resolution is what actually
26
+ guarantees it. This test forces the serialized ``_BaseColor`` back after
27
+ ``AddComponent`` to measure that path specifically.
28
+ b. **Auto-hide at zero**, and restoring visibility above zero.
29
+ c. **Property-block co-existence.** Both controllers write to the same
30
+ Renderer's MaterialPropertyBlock. Each must read-modify-write it; a
31
+ blanket ``MaterialPropertyBlock.Clear()`` on teardown would wipe the
32
+ other's data.
33
+
34
+ Requires:
35
+ - Unity Hub with at least one editor installed (skips on license wall)
36
+ """
37
+ import os
38
+ import sys
39
+ import json
40
+ import tempfile
41
+ import unittest
42
+ import subprocess
43
+ import logging
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ scripts_dir = r"O:\Cloud\Code\_scripts"
48
+ if scripts_dir not in sys.path:
49
+ sys.path.insert(0, scripts_dir)
50
+
51
+ from unitytk import UnityLauncher, UnityFinder, TemplateDeployer
52
+
53
+
54
+ def _unity_available() -> bool:
55
+ return bool(UnityFinder.find_editors())
56
+
57
+
58
+ # ======================================================================
59
+ # Unity-side verifier
60
+ # ======================================================================
61
+
62
+ UNITY_VERIFIER_CS = r"""
63
+ using UnityEngine;
64
+ using UnityEditor;
65
+ using System.Globalization;
66
+ using System.IO;
67
+ using System.Text;
68
+
69
+ public class ControllerRuntimeVerifier
70
+ {
71
+ /// <summary>Invariant-culture float, so a European locale can't emit
72
+ /// "0,5" and hand the Python side unparseable JSON.</summary>
73
+ static string F(float v) => v.ToString("R", CultureInfo.InvariantCulture);
74
+
75
+ // Called via -executeMethod ControllerRuntimeVerifier.Verify
76
+ public static void Verify()
77
+ {
78
+ string resultsFile = @"__RESULTS_PATH__";
79
+ var sb = new StringBuilder();
80
+ sb.AppendLine("{");
81
+
82
+ // ---- Setup: cube + Built-in RP Standard material ----------------
83
+ var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
84
+ var rend = go.GetComponent<Renderer>();
85
+ var mat = new Material(Shader.Find("Standard"));
86
+ mat.name = "OpacityRuntimeTestMat";
87
+ rend.sharedMaterial = mat;
88
+
89
+ // Documents the premise: a fresh project really is Built-in RP, so
90
+ // the serialized "_BaseColor" default names a property that is absent.
91
+ sb.AppendLine(" \"mat_has_base_color\": "
92
+ + (mat.HasProperty("_BaseColor") ? "true," : "false,"));
93
+ sb.AppendLine(" \"mat_has_color\": "
94
+ + (mat.HasProperty("_Color") ? "true," : "false,"));
95
+
96
+ // ---- (a) Partial fade must reach the property block --------------
97
+ var opa = go.AddComponent<RenderOpacityController>();
98
+ // AddComponent in the editor DOES run Reset(), which auto-detects the
99
+ // property — recorded here, but it is not something the import path can
100
+ // rely on (it only helps when a material is already assigned at that
101
+ // moment). Force the serialized URP default back so what follows
102
+ // measures the RUNTIME resolution, which always applies.
103
+ sb.AppendLine(" \"reset_autodetected\": "
104
+ + (opa.colorProperty == "_Color" ? "true," : "false,"));
105
+ opa.colorProperty = "_BaseColor";
106
+
107
+ opa.opacity = 0.5f;
108
+ opa.ApplyNow();
109
+
110
+ var mpb = new MaterialPropertyBlock();
111
+ rend.GetPropertyBlock(mpb);
112
+ sb.AppendLine(" \"alpha_applied\": " + F(mpb.GetColor("_Color").a) + ",");
113
+
114
+ // ---- (b) Auto-hide at zero, restore above zero -------------------
115
+ opa.opacity = 0f;
116
+ opa.ApplyNow();
117
+ sb.AppendLine(" \"hidden_at_zero\": " + (!rend.enabled ? "true," : "false,"));
118
+
119
+ opa.opacity = 1f;
120
+ opa.ApplyNow();
121
+ sb.AppendLine(" \"restored_above_zero\": " + (rend.enabled ? "true," : "false,"));
122
+
123
+ // Back to a partial fade for the co-existence check.
124
+ opa.opacity = 0.5f;
125
+ opa.ApplyNow();
126
+
127
+ // ---- (c) Second controller on the same Renderer ------------------
128
+ var emi = go.AddComponent<EmissiveGroupController>();
129
+ emi.groups.Add(new EmissiveGroup {
130
+ name = "headlights", slot = 0, weight = 0.25f });
131
+ emi.Rebuild();
132
+ emi.ApplyNow();
133
+
134
+ rend.GetPropertyBlock(mpb);
135
+ sb.AppendLine(" \"alpha_after_emissive\": "
136
+ + F(mpb.GetColor("_Color").a) + ",");
137
+ sb.AppendLine(" \"weight_after_emissive\": "
138
+ + F(mpb.GetVector("_GroupWeights").x) + ",");
139
+
140
+ // Tearing the opacity controller down must leave the emissive weights
141
+ // alone. Measured: Unity does NOT call OnDisable for a plain
142
+ // (non-[ExecuteAlways]) MonoBehaviour in edit mode, so this does not
143
+ // reach ClearOverrides — the alpha readback below is what proves that,
144
+ // and the Python side asserts it so the day it changes we find out.
145
+ Object.DestroyImmediate(opa);
146
+ rend.GetPropertyBlock(mpb);
147
+ sb.AppendLine(" \"alpha_after_destroy\": "
148
+ + F(mpb.GetColor("_Color").a) + ",");
149
+ sb.AppendLine(" \"weight_after_destroy\": "
150
+ + F(mpb.GetVector("_GroupWeights").x));
151
+
152
+ Object.DestroyImmediate(go);
153
+
154
+ sb.AppendLine("}");
155
+ File.WriteAllText(resultsFile, sb.ToString());
156
+ Debug.Log("CONTROLLER RUNTIME TEST COMPLETE. Results at: " + resultsFile);
157
+ EditorApplication.Exit(0);
158
+ }
159
+ }
160
+ """
161
+
162
+
163
+ # ======================================================================
164
+ # Test class
165
+ # ======================================================================
166
+
167
+
168
+ class TestControllerRuntimeIntegration(unittest.TestCase):
169
+ """unitytk MonoBehaviour runtime behaviour, read back from a live Unity."""
170
+
171
+ @classmethod
172
+ def setUpClass(cls):
173
+ # In-repo scratch (see CLAUDE.md), not the system temp dir.
174
+ _temp_base = os.path.join(os.path.dirname(__file__), "temp_tests")
175
+ os.makedirs(_temp_base, exist_ok=True)
176
+ cls.temp_dir = tempfile.mkdtemp(prefix="ctrl_runtime_integ_", dir=_temp_base)
177
+ cls.unity_results = None
178
+ logger.info(f"Temp dir: {cls.temp_dir}")
179
+
180
+ @classmethod
181
+ def tearDownClass(cls):
182
+ logger.info(f"Results preserved in: {cls.temp_dir}")
183
+
184
+ @unittest.skipUnless(_unity_available(), "No Unity editor found")
185
+ def test_01_unity_runtime(self):
186
+ """Drive both controllers in a real Unity; capture the readback."""
187
+ editor = os.environ.get("UNITYTK_TEST_EDITOR")
188
+ launcher = UnityLauncher(editor) if editor else UnityLauncher()
189
+ project_path = os.path.join(self.temp_dir, "UnityProject")
190
+ if not launcher.create_project(project_path, batch_mode=True):
191
+ self.skipTest(
192
+ "Unity batch project creation failed — typically no batch-mode "
193
+ "license on this machine."
194
+ )
195
+
196
+ # Deploy the real templates -- the full embedded UPM package.
197
+ TemplateDeployer.deploy_package(project_path)
198
+
199
+ editor_path = os.path.join(project_path, "Assets", "Editor")
200
+ os.makedirs(editor_path, exist_ok=True)
201
+ results_path = os.path.join(self.temp_dir, "unity_results.json")
202
+ with open(
203
+ os.path.join(editor_path, "ControllerRuntimeVerifier.cs"), "w",
204
+ encoding="utf-8",
205
+ ) as f:
206
+ f.write(UNITY_VERIFIER_CS.replace("__RESULTS_PATH__", results_path))
207
+
208
+ log_path = os.path.join(self.temp_dir, "unity_log.txt")
209
+ launcher.project_path = project_path
210
+ proc = launcher.launch_editor(
211
+ batch_mode=True,
212
+ execute_method="ControllerRuntimeVerifier.Verify",
213
+ log_file=log_path,
214
+ extra_args=["-quit"],
215
+ detached=False,
216
+ )
217
+ returncode = None
218
+ if proc:
219
+ try:
220
+ returncode = proc.wait(timeout=300)
221
+ except subprocess.TimeoutExpired:
222
+ proc.kill()
223
+ raise TimeoutError("Unity timed out")
224
+
225
+ if not os.path.exists(results_path):
226
+ log_text = ""
227
+ if os.path.exists(log_path):
228
+ with open(log_path, encoding="utf-8", errors="ignore") as fh:
229
+ log_text = fh.read()
230
+ if UnityLauncher.is_license_failure(returncode, log_text):
231
+ self.skipTest(
232
+ "Unity exited without a valid batch license "
233
+ f"(rc={returncode}) — see {log_path}"
234
+ )
235
+ self.fail(
236
+ f"unity_results.json not created (Unity rc={returncode}) — "
237
+ f"check {log_path}"
238
+ )
239
+ with open(results_path) as f:
240
+ self.__class__.unity_results = json.load(f)
241
+ logger.info(
242
+ f"Unity results:\n{json.dumps(self.__class__.unity_results, indent=2)}"
243
+ )
244
+
245
+ @unittest.skipUnless(_unity_available(), "No Unity editor found")
246
+ def test_02_color_property_resolves_on_builtin(self):
247
+ """The fade reaches the material even though the default names _BaseColor."""
248
+ unity = self.__class__.unity_results
249
+ if not unity:
250
+ self.skipTest("Unity results not available — test_01 must run first")
251
+
252
+ # Premise: a fresh batch project is Built-in RP.
253
+ self.assertTrue(unity.get("mat_has_color"),
254
+ "Standard material should expose _Color")
255
+ self.assertFalse(unity.get("mat_has_base_color"),
256
+ "premise broken: this project is no longer Built-in RP")
257
+
258
+ # The regression: colorProperty is "_BaseColor" (the URP default the
259
+ # component serializes) and the material has no such property, so
260
+ # without runtime resolution the alpha never reaches it and this is 0.0.
261
+ self.assertAlmostEqual(
262
+ unity.get("alpha_applied", 0.0), 0.5, places=3,
263
+ msg="opacity never reached the material — colorProperty did not "
264
+ "resolve to the Built-in _Color",
265
+ )
266
+
267
+ @unittest.skipUnless(_unity_available(), "No Unity editor found")
268
+ def test_03_auto_hide_at_zero(self):
269
+ """Zero opacity disables the Renderer; raising it restores visibility."""
270
+ unity = self.__class__.unity_results
271
+ if not unity:
272
+ self.skipTest("Unity results not available — test_01 must run first")
273
+
274
+ self.assertTrue(unity.get("hidden_at_zero"),
275
+ "Renderer should be disabled at zero opacity")
276
+ self.assertTrue(unity.get("restored_above_zero"),
277
+ "Renderer should be re-enabled above zero opacity")
278
+
279
+ @unittest.skipUnless(_unity_available(), "No Unity editor found")
280
+ def test_04_property_block_coexistence(self):
281
+ """Two controllers share one Renderer's block without clobbering."""
282
+ unity = self.__class__.unity_results
283
+ if not unity:
284
+ self.skipTest("Unity results not available — test_01 must run first")
285
+
286
+ # Emissive writing _GroupWeights must not drop the opacity alpha...
287
+ self.assertAlmostEqual(
288
+ unity.get("alpha_after_emissive", 0.0), 0.5, places=3,
289
+ msg="EmissiveGroupController wiped RenderOpacityController's alpha",
290
+ )
291
+ # ...and vice versa.
292
+ self.assertAlmostEqual(
293
+ unity.get("weight_after_emissive", 0.0), 0.25, places=3,
294
+ msg="_GroupWeights did not reach the property block",
295
+ )
296
+
297
+ # Destroying the controller must leave the weights intact either way.
298
+ self.assertAlmostEqual(
299
+ unity.get("weight_after_destroy", 0.0), 0.25, places=3,
300
+ msg="destroying RenderOpacityController wiped the emissive weights",
301
+ )
302
+
303
+ # KNOWN COVERAGE GAP, asserted rather than assumed: Unity does not call
304
+ # OnDisable for a plain (non-[ExecuteAlways]) MonoBehaviour in edit
305
+ # mode, so DestroyImmediate above does NOT reach ClearOverrides — the
306
+ # alpha stays at the value we last applied instead of reverting to the
307
+ # material's own. That makes the weight assertion above necessary but
308
+ # not sufficient: the teardown path itself (read-modify-write of the
309
+ # property block, rather than a blanket Clear() that would wipe the
310
+ # emissive weights) is only exercised in play mode, which batch
311
+ # -executeMethod cannot enter synchronously. Pinned here so that if a
312
+ # future Unity DOES start firing OnDisable in edit mode, this test
313
+ # fails loudly and the assertion above becomes real coverage.
314
+ self.assertAlmostEqual(
315
+ unity.get("alpha_after_destroy", -1.0), 0.5, places=3,
316
+ msg="OnDisable now fires in edit mode — retarget this test at the "
317
+ "teardown path, which is finally reachable",
318
+ )
319
+
320
+
321
+ if __name__ == "__main__":
322
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
323
+ unittest.main(verbosity=2)