unitytk 0.2.0__tar.gz → 0.2.2__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.2.0/unitytk.egg-info → unitytk-0.2.2}/PKG-INFO +1 -1
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_asset_delivery.py +51 -8
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_audio_events_integration.py +167 -16
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_embedded_dependencies_integration.py +183 -1
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_run_tests.py +5 -2
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/__init__.py +1 -1
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/asset_delivery.py +20 -4
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +5 -2
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/AudioEvent/AudioEventController.cs +48 -19
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/README.md +1 -1
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/UnitytkSettings.cs +60 -8
- {unitytk-0.2.0 → unitytk-0.2.2/unitytk.egg-info}/PKG-INFO +1 -1
- {unitytk-0.2.0 → unitytk-0.2.2}/LICENSE +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/MANIFEST.in +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/docs/README.md +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/pyproject.toml +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/setup.cfg +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_articulated_rig_runtime.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_base_test.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_blender_render_effects_e2e.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_blender_shadow_plane_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_blender_shot_metadata_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_clip_name_contract_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_controller_runtime_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_deploy_templates.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_emissive_groups_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_fade_window_preservation.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_keyed_visibility.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_launcher.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_lightmap_metadata_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_maya_surface_materials_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_multi_section_audio_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_render_effects_e2e.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_render_opacity_controller.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_render_opacity_e2e.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_render_opacity_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_scene_builder.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_shadow_plane_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_shadow_plane_runtime_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_shared_material_standalone.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_shot_metadata_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_shots_audio_sidebyside_integration.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_standalone_enduser.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/test/test_temp_projects.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/launcher.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/scene_builder.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/template_deployer.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/AudioEvent/AudioEventController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/AudioEvent.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/M3trik.Unitytk.asmdef.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/MayaSurface.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/README.md.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/RenderEffects/RenderEffectsController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/RenderEffects.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlane.shader +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShadowPlane.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShotMetadata/ShotMetadataController.cs +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/ShotMetadata.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/UnitytkSettings.cs.meta +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk.egg-info/SOURCES.txt +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk.egg-info/requires.txt +0 -0
- {unitytk-0.2.0 → unitytk-0.2.2}/unitytk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# !/usr/bin/python
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
"""Tests for unitytk.CopyToAssetsDeliverer -- copy-to-Assets hand-off Strategy (no DCC)."""
|
|
4
|
+
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import struct
|
|
@@ -20,6 +21,7 @@ class _Logger:
|
|
|
20
21
|
self.errors = []
|
|
21
22
|
self.warnings = []
|
|
22
23
|
self.infos = []
|
|
24
|
+
self.debugs = []
|
|
23
25
|
|
|
24
26
|
def error(self, m, *args):
|
|
25
27
|
self.errors.append(m % args if args else m)
|
|
@@ -30,6 +32,9 @@ class _Logger:
|
|
|
30
32
|
def info(self, m, *args):
|
|
31
33
|
self.infos.append(m % args if args else m)
|
|
32
34
|
|
|
35
|
+
def debug(self, m, *args):
|
|
36
|
+
self.debugs.append(m % args if args else m)
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
def _carrier_fbx(path: str, channels) -> str:
|
|
35
40
|
"""A binary FBX (7700 layout) whose ``data_export`` carrier holds
|
|
@@ -108,7 +113,9 @@ def _payload(fbx, default_name="Export"):
|
|
|
108
113
|
|
|
109
114
|
class SanitizeTest(unittest.TestCase):
|
|
110
115
|
def test_strips_invalid_and_paths(self):
|
|
111
|
-
self.assertEqual(
|
|
116
|
+
self.assertEqual(
|
|
117
|
+
CopyToAssetsDeliverer.sanitize_asset_name("my/model:v2"), "my_model_v2"
|
|
118
|
+
)
|
|
112
119
|
|
|
113
120
|
def test_collapses_and_trims(self):
|
|
114
121
|
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name(" a b "), "a b")
|
|
@@ -160,6 +167,31 @@ class DeliveryTest(unittest.TestCase):
|
|
|
160
167
|
self.assertEqual(open(dest, "rb").read(), b"FBX-BYTES")
|
|
161
168
|
self.assertFalse(result["launched"])
|
|
162
169
|
|
|
170
|
+
def test_a_copy_cut_short_never_replaces_the_asset_unity_reads(self):
|
|
171
|
+
"""The copy lands whole or not at all (``ptk.FileUtils.atomic_write``):
|
|
172
|
+
a copy into the project cut short (a full disk, a sync client holding
|
|
173
|
+
the file) left Unity a truncated FBX under the asset's own name.
|
|
174
|
+
Added: 2026-10-13"""
|
|
175
|
+
from unittest import mock
|
|
176
|
+
|
|
177
|
+
import unitytk.asset_delivery as delivery
|
|
178
|
+
|
|
179
|
+
dest = os.path.join(self.project, "Assets", "Models", "Robot.fbx")
|
|
180
|
+
os.makedirs(os.path.dirname(dest))
|
|
181
|
+
with open(dest, "wb") as fh:
|
|
182
|
+
fh.write(b"OLD")
|
|
183
|
+
|
|
184
|
+
def cut_short(src, dst, *args, **kwargs):
|
|
185
|
+
with open(dst, "wb") as fh:
|
|
186
|
+
fh.write(b"HALF")
|
|
187
|
+
raise OSError(28, "No space left on device")
|
|
188
|
+
|
|
189
|
+
with mock.patch.object(delivery.shutil, "copy2", side_effect=cut_short):
|
|
190
|
+
self._deliver({"ASSETS_SUBDIR": "Models"}, default_name="Robot")
|
|
191
|
+
with open(dest, "rb") as fh:
|
|
192
|
+
self.assertEqual(fh.read(), b"OLD")
|
|
193
|
+
self.assertEqual(os.listdir(os.path.dirname(dest)), ["Robot.fbx"])
|
|
194
|
+
|
|
163
195
|
def test_deliver_uses_default_subdir_and_asset_name(self):
|
|
164
196
|
_bridge, result = self._deliver({}, default_name="Cube")
|
|
165
197
|
self.assertEqual(
|
|
@@ -168,7 +200,9 @@ class DeliveryTest(unittest.TestCase):
|
|
|
168
200
|
)
|
|
169
201
|
|
|
170
202
|
def test_explicit_asset_name_overrides_and_sanitizes(self):
|
|
171
|
-
_bridge, result = self._deliver(
|
|
203
|
+
_bridge, result = self._deliver(
|
|
204
|
+
{"ASSET_NAME": "Hero/Mesh:01"}, default_name="Cube"
|
|
205
|
+
)
|
|
172
206
|
self.assertEqual(os.path.basename(result["asset"]), "Hero_Mesh_01.fbx")
|
|
173
207
|
|
|
174
208
|
def test_launch_mode_open_invokes_windowed_launch(self):
|
|
@@ -257,6 +291,7 @@ class DeliveryTest(unittest.TestCase):
|
|
|
257
291
|
orig = ad.CopyToAssetsDeliverer._launch_editor
|
|
258
292
|
# Patch UnityLauncher inside the real hook (deferred import target).
|
|
259
293
|
import unitytk.launcher as launcher_mod
|
|
294
|
+
|
|
260
295
|
real = launcher_mod.UnityLauncher
|
|
261
296
|
launcher_mod.UnityLauncher = _FakeLauncher
|
|
262
297
|
try:
|
|
@@ -319,15 +354,14 @@ class DeliveryTest(unittest.TestCase):
|
|
|
319
354
|
)
|
|
320
355
|
# Genuinely under Assets/ (no drive escape).
|
|
321
356
|
assets = os.path.realpath(os.path.join(self.project, "Assets"))
|
|
322
|
-
self.assertTrue(
|
|
323
|
-
os.path.realpath(dest).startswith(assets), f"raw={raw!r}"
|
|
324
|
-
)
|
|
357
|
+
self.assertTrue(os.path.realpath(dest).startswith(assets), f"raw={raw!r}")
|
|
325
358
|
|
|
326
359
|
def test_explicit_subdir_reducing_to_nothing_drops_into_assets_root(self):
|
|
327
360
|
for raw in (".", "/", "../.."):
|
|
328
361
|
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
329
362
|
self.assertEqual(
|
|
330
|
-
result["asset"],
|
|
363
|
+
result["asset"],
|
|
364
|
+
os.path.join(self.project, "Assets", "Cube.fbx"),
|
|
331
365
|
f"raw={raw!r}",
|
|
332
366
|
)
|
|
333
367
|
self.assertEqual(result["subdir"], "")
|
|
@@ -363,7 +397,11 @@ class SelfContainedDeliveryTest(unittest.TestCase):
|
|
|
363
397
|
"lightmap_metadata": {
|
|
364
398
|
"version": 1,
|
|
365
399
|
"objects": [{"name": "Crate", "map": "Crate_Lightmap.exr", "uvIndex": 1}],
|
|
366
|
-
"probe": {
|
|
400
|
+
"probe": {
|
|
401
|
+
"map": "Room_Probe.exr",
|
|
402
|
+
"position": [0, 0, 0],
|
|
403
|
+
"unit_scale": 0.01,
|
|
404
|
+
},
|
|
367
405
|
},
|
|
368
406
|
"shadow_metadata": {
|
|
369
407
|
"version": 2,
|
|
@@ -386,7 +424,12 @@ class SelfContainedDeliveryTest(unittest.TestCase):
|
|
|
386
424
|
self.maps = os.path.join(self.tmp, "sourceimages")
|
|
387
425
|
os.makedirs(self.maps)
|
|
388
426
|
self.bytes = {}
|
|
389
|
-
for name in (
|
|
427
|
+
for name in (
|
|
428
|
+
"Crate_Lightmap.exr",
|
|
429
|
+
"Room_Probe.exr",
|
|
430
|
+
"Crate_shadow.png",
|
|
431
|
+
"Crate_horizon.png",
|
|
432
|
+
):
|
|
390
433
|
self.bytes[name] = name.encode() * 8
|
|
391
434
|
with open(os.path.join(self.maps, name), "wb") as fh:
|
|
392
435
|
fh.write(self.bytes[name])
|
|
@@ -24,6 +24,7 @@ Requires:
|
|
|
24
24
|
- Maya 2025 installed (mayapy.exe)
|
|
25
25
|
- Unity Hub with at least one editor installed
|
|
26
26
|
"""
|
|
27
|
+
|
|
27
28
|
import os
|
|
28
29
|
import json
|
|
29
30
|
import shutil
|
|
@@ -34,7 +35,15 @@ import logging
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
36
37
|
import base_test # noqa: F401 — sys.path bootstrap for the sibling repos
|
|
37
|
-
from base_test import
|
|
38
|
+
from base_test import (
|
|
39
|
+
BlenderEnv,
|
|
40
|
+
CarrierFbx,
|
|
41
|
+
EnvSkip,
|
|
42
|
+
MayaEnv,
|
|
43
|
+
TempProjects,
|
|
44
|
+
UnityEnv,
|
|
45
|
+
UserPropsProbe,
|
|
46
|
+
)
|
|
38
47
|
|
|
39
48
|
scripts_dir = base_test.SCRIPTS_ROOT
|
|
40
49
|
|
|
@@ -471,9 +480,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
471
480
|
events,
|
|
472
481
|
{("", 9, "footstep"), ("", 29, "jump"), ("", 44, "land")},
|
|
473
482
|
)
|
|
474
|
-
self.assertEqual(
|
|
475
|
-
sorted(r["tracks"]), ["footstep", "jump", "land"]
|
|
476
|
-
)
|
|
483
|
+
self.assertEqual(sorted(r["tracks"]), ["footstep", "jump", "land"])
|
|
477
484
|
|
|
478
485
|
# ------------------------------------------------------------------
|
|
479
486
|
# Step 2: Unity import and readback
|
|
@@ -552,17 +559,26 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
552
559
|
full_log = f.read()
|
|
553
560
|
# Surface AudioEvent / error / warning lines for analysis
|
|
554
561
|
relevant = [
|
|
555
|
-
ln
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
562
|
+
ln
|
|
563
|
+
for ln in full_log.splitlines()
|
|
564
|
+
if any(
|
|
565
|
+
s in ln
|
|
566
|
+
for s in (
|
|
567
|
+
"AudioEvent",
|
|
568
|
+
"ERROR",
|
|
569
|
+
"Error",
|
|
570
|
+
"Warning",
|
|
571
|
+
"NullReferenceException",
|
|
572
|
+
"Failed",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
560
575
|
]
|
|
561
576
|
self.__class__.unity_log_relevant = relevant
|
|
562
577
|
if relevant:
|
|
563
578
|
logger.info(
|
|
564
579
|
"Unity log relevant lines (%d):\n %s",
|
|
565
|
-
len(relevant),
|
|
580
|
+
len(relevant),
|
|
581
|
+
"\n ".join(relevant[-40:]),
|
|
566
582
|
)
|
|
567
583
|
else:
|
|
568
584
|
logger.info("Unity log: no AudioEvent/Warning/Error lines.")
|
|
@@ -571,8 +587,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
571
587
|
with open(results_path) as f:
|
|
572
588
|
self.__class__.unity_results = json.load(f)
|
|
573
589
|
|
|
574
|
-
postproc_path = os.path.join(
|
|
575
|
-
self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
|
|
590
|
+
postproc_path = os.path.join(self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
|
|
576
591
|
if os.path.exists(postproc_path):
|
|
577
592
|
with open(postproc_path) as f:
|
|
578
593
|
self.__class__.postproc = json.load(f)
|
|
@@ -608,7 +623,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
608
623
|
f"in postprocessor results: {list(postproc)}",
|
|
609
624
|
)
|
|
610
625
|
self.assertIn(
|
|
611
|
-
"audio_manifest",
|
|
626
|
+
"audio_manifest",
|
|
627
|
+
props,
|
|
612
628
|
f"audio_manifest user-property missing on {name}; saw: {list(props)}",
|
|
613
629
|
)
|
|
614
630
|
manifest = props["audio_manifest"]
|
|
@@ -655,7 +671,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
655
671
|
|
|
656
672
|
# (d) AnimationEvents injected on the imported clip
|
|
657
673
|
self.assertGreaterEqual(
|
|
658
|
-
unity.get("clip_count", 0),
|
|
674
|
+
unity.get("clip_count", 0),
|
|
675
|
+
1,
|
|
659
676
|
"Expected at least one AnimationClip in the imported FBX",
|
|
660
677
|
)
|
|
661
678
|
events = unity.get("animation_events", [])
|
|
@@ -676,8 +693,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
676
693
|
# In Unity-frame terms the events are at clip-relative frames 9,
|
|
677
694
|
# 29, 44 — which is what AnimationEvent.time * frameRate yields.
|
|
678
695
|
self.assertEqual(events_by_name.get("footstep"), 9)
|
|
679
|
-
self.assertEqual(events_by_name.get("jump"),
|
|
680
|
-
self.assertEqual(events_by_name.get("land"),
|
|
696
|
+
self.assertEqual(events_by_name.get("jump"), 29)
|
|
697
|
+
self.assertEqual(events_by_name.get("land"), 44)
|
|
681
698
|
|
|
682
699
|
# (e) Reimport idempotency. Two probes injected before the
|
|
683
700
|
# reimport tell us about both correctness and Unity behavior:
|
|
@@ -721,6 +738,140 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
721
738
|
)
|
|
722
739
|
|
|
723
740
|
|
|
741
|
+
# ======================================================================
|
|
742
|
+
# Carrier precedence: one audio_manifest per model, the scene's own first
|
|
743
|
+
# ======================================================================
|
|
744
|
+
|
|
745
|
+
CARRIER_VERIFIER_CS = r"""
|
|
746
|
+
using UnityEngine;
|
|
747
|
+
using UnityEditor;
|
|
748
|
+
using System;
|
|
749
|
+
using System.IO;
|
|
750
|
+
using System.Linq;
|
|
751
|
+
using System.Collections.Generic;
|
|
752
|
+
|
|
753
|
+
public class AudioCarrierVerifier
|
|
754
|
+
{
|
|
755
|
+
static string Q(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
|
|
756
|
+
|
|
757
|
+
public static void Verify()
|
|
758
|
+
{
|
|
759
|
+
var o = new List<string>();
|
|
760
|
+
try
|
|
761
|
+
{
|
|
762
|
+
// Again, now the clips are in the database: the startup import
|
|
763
|
+
// may have met the model before the audio.
|
|
764
|
+
AssetDatabase.ImportAsset("__FBX__",
|
|
765
|
+
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
766
|
+
AssetDatabase.Refresh();
|
|
767
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("__FBX__");
|
|
768
|
+
o.Add("\"prefab\":" + (prefab != null ? "true" : "false"));
|
|
769
|
+
var nodes = prefab != null
|
|
770
|
+
? prefab.GetComponentsInChildren<Transform>(true).Select(t => Q(t.name))
|
|
771
|
+
: Enumerable.Empty<string>();
|
|
772
|
+
o.Add("\"nodes\":[" + string.Join(",", nodes) + "]");
|
|
773
|
+
var ctrl = prefab != null ? prefab.GetComponent<AudioEventController>() : null;
|
|
774
|
+
o.Add("\"controller\":" + (ctrl != null ? "true" : "false"));
|
|
775
|
+
var clips = ctrl != null ? ctrl.clips : new List<AudioClip>();
|
|
776
|
+
o.Add("\"clips\":[" + string.Join(",", clips.Select(c => Q(c != null ? c.name : ""))) + "]");
|
|
777
|
+
}
|
|
778
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
779
|
+
File.WriteAllText(@"__RESULTS__", "{" + string.Join(",\n", o) + "}");
|
|
780
|
+
EditorApplication.Exit(0);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class TestAudioCarrierPrecedence(unittest.TestCase):
|
|
787
|
+
"""An FBX carrying ``audio_manifest`` on several carriers plays the
|
|
788
|
+
scene's own only.
|
|
789
|
+
|
|
790
|
+
An assembly ships a stray ``data_export1`` (and a module's
|
|
791
|
+
``NS:data_export``) beside the scene's own, each holding the events as
|
|
792
|
+
last exported alone -- measured on an assembly, 2026-10-13. The scene's
|
|
793
|
+
own record is re-derived from the whole merged scene, so every reader
|
|
794
|
+
takes ONE carrier per channel, the scene's own first: the other
|
|
795
|
+
importers (``CarrierImport.Collect``), the GLB applier, and pythontk's
|
|
796
|
+
``FbxMedia.audio_events``. The audio importer concatenated every
|
|
797
|
+
carrier's events instead, so the stray's stale event was bound and fired.
|
|
798
|
+
|
|
799
|
+
The fixture is a real FBX with both carriers (``base_test.CarrierFbx``,
|
|
800
|
+
written by a fresh Blender); pythontk's ``FbxMedia.audio_events`` pins the
|
|
801
|
+
same rule on the same shape (``test_fbx_media``).
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
FBX = "Assets/Delivered/assembly.fbx"
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
def setUpClass(cls):
|
|
808
|
+
cls.temp_dir = TempProjects.allocate("audio_carrier_precedence")
|
|
809
|
+
|
|
810
|
+
@BlenderEnv.skip_unless_available()
|
|
811
|
+
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
812
|
+
def test_the_scenes_own_carrier_wins(self):
|
|
813
|
+
own = {"version": 2, "events": [{"clip": "", "frame": 0, "name": "footstep"}]}
|
|
814
|
+
stray = {"version": 2, "events": [{"clip": "", "frame": 3, "name": "door"}]}
|
|
815
|
+
fixture = CarrierFbx.build(
|
|
816
|
+
os.path.join(self.temp_dir, "assembly.fbx"),
|
|
817
|
+
{
|
|
818
|
+
"data_export": {"audio_manifest": own},
|
|
819
|
+
"data_export1": {"audio_manifest": stray},
|
|
820
|
+
},
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
launcher = UnityLauncher()
|
|
824
|
+
project = os.path.join(self.temp_dir, "UnityProject")
|
|
825
|
+
if not launcher.create_project(project, batch_mode=True):
|
|
826
|
+
self.skipTest(UnityEnv.project_creation_reason())
|
|
827
|
+
delivered = os.path.join(project, "Assets", "Delivered")
|
|
828
|
+
os.makedirs(delivered, exist_ok=True)
|
|
829
|
+
shutil.copy2(fixture, os.path.join(delivered, "assembly.fbx"))
|
|
830
|
+
# Both tracks resolve, so only the carrier rule decides what binds.
|
|
831
|
+
for name in ("footstep", "door"):
|
|
832
|
+
CarrierFbx.wav(
|
|
833
|
+
os.path.join(project, "Assets", "Audio", f"{name}.wav"), name.encode()
|
|
834
|
+
)
|
|
835
|
+
TemplateDeployer.deploy_package(project, components=["audio_event"])
|
|
836
|
+
results = os.path.join(self.temp_dir, "unity_results.json")
|
|
837
|
+
editor_dir = os.path.join(project, "Assets", "Editor")
|
|
838
|
+
os.makedirs(editor_dir, exist_ok=True)
|
|
839
|
+
with open(
|
|
840
|
+
os.path.join(editor_dir, "AudioCarrierVerifier.cs"), "w", encoding="utf-8"
|
|
841
|
+
) as fh:
|
|
842
|
+
fh.write(
|
|
843
|
+
CARRIER_VERIFIER_CS.replace("__FBX__", self.FBX).replace(
|
|
844
|
+
"__RESULTS__", results
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
log = os.path.join(self.temp_dir, "unity_log.txt")
|
|
848
|
+
launcher.project_path = project
|
|
849
|
+
proc = launcher.launch_editor(
|
|
850
|
+
batch_mode=True,
|
|
851
|
+
execute_method="AudioCarrierVerifier.Verify",
|
|
852
|
+
log_file=log,
|
|
853
|
+
extra_args=["-quit"],
|
|
854
|
+
detached=False,
|
|
855
|
+
)
|
|
856
|
+
returncode = None
|
|
857
|
+
if proc:
|
|
858
|
+
try:
|
|
859
|
+
returncode = proc.wait(timeout=900)
|
|
860
|
+
except subprocess.TimeoutExpired:
|
|
861
|
+
proc.kill()
|
|
862
|
+
raise TimeoutError("Unity timed out")
|
|
863
|
+
UnityEnv.require_results(self, results, returncode, log)
|
|
864
|
+
with open(results) as fh:
|
|
865
|
+
unity = json.load(fh)
|
|
866
|
+
logger.info(f"Unity results:\n{json.dumps(unity, indent=2)}")
|
|
867
|
+
self.assertNotIn("error", unity, unity.get("error"))
|
|
868
|
+
self.assertTrue(unity["prefab"], unity)
|
|
869
|
+
# Premise: both carriers reached the import.
|
|
870
|
+
self.assertTrue({"data_export", "data_export1"} <= set(unity["nodes"]), unity)
|
|
871
|
+
self.assertTrue(unity["controller"], unity)
|
|
872
|
+
self.assertEqual(unity["clips"], ["footstep"], unity)
|
|
873
|
+
|
|
874
|
+
|
|
724
875
|
if __name__ == "__main__":
|
|
725
876
|
logging.basicConfig(level=logging.INFO)
|
|
726
877
|
unittest.main()
|
|
@@ -38,7 +38,7 @@ import subprocess
|
|
|
38
38
|
import unittest
|
|
39
39
|
|
|
40
40
|
import base_test # noqa: F401 -- sys.path bootstrap for the sibling repos
|
|
41
|
-
from base_test import EnvSkip, MayaEnv, TempProjects, UnityEnv
|
|
41
|
+
from base_test import BlenderEnv, CarrierFbx, EnvSkip, MayaEnv, TempProjects, UnityEnv
|
|
42
42
|
|
|
43
43
|
from unitytk import TemplateDeployer, UnityFinder, UnityLauncher
|
|
44
44
|
|
|
@@ -456,6 +456,188 @@ class TestEmbeddedDependenciesIntegration(unittest.TestCase):
|
|
|
456
456
|
self.assertTrue(unity["reexport_still_lightmap"], unity)
|
|
457
457
|
|
|
458
458
|
|
|
459
|
+
# ======================================================================
|
|
460
|
+
# When an import asks for the FBX's embedded files to be extracted
|
|
461
|
+
# ======================================================================
|
|
462
|
+
|
|
463
|
+
REQUEST_VERIFIER_CS = r"""
|
|
464
|
+
using UnityEngine;
|
|
465
|
+
using UnityEditor;
|
|
466
|
+
using System;
|
|
467
|
+
using System.IO;
|
|
468
|
+
using System.Linq;
|
|
469
|
+
using System.Collections.Generic;
|
|
470
|
+
|
|
471
|
+
public class EmbeddedRequestVerifier
|
|
472
|
+
{
|
|
473
|
+
static string Q(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
|
|
474
|
+
static string B(bool b) { return b ? "true" : "false"; }
|
|
475
|
+
|
|
476
|
+
static void Finish(List<string> o)
|
|
477
|
+
{
|
|
478
|
+
File.WriteAllText(@"__RESULTS__", "{" + string.Join(",\n", o) + "}");
|
|
479
|
+
EditorApplication.Exit(0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Session 1: the audio is in the database; the models arrive now.
|
|
483
|
+
public static void First()
|
|
484
|
+
{
|
|
485
|
+
var o = new List<string>();
|
|
486
|
+
try
|
|
487
|
+
{
|
|
488
|
+
foreach (var pair in new[] {
|
|
489
|
+
new[] { @"__RESOLVED_SRC__", "Assets/Resolved/resolved.fbx" },
|
|
490
|
+
new[] { @"__GHOSTED_SRC__", "Assets/Ghosted/ghosted.fbx" } })
|
|
491
|
+
{
|
|
492
|
+
Directory.CreateDirectory(Path.GetDirectoryName(pair[1]));
|
|
493
|
+
File.Copy(pair[0], pair[1], true);
|
|
494
|
+
}
|
|
495
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
496
|
+
o.Add("\"resolved_beside\":" + B(File.Exists("Assets/Resolved/footstep.wav")));
|
|
497
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Resolved/resolved.fbx");
|
|
498
|
+
var ctrl = prefab != null ? prefab.GetComponent<AudioEventController>() : null;
|
|
499
|
+
var clip = ctrl != null && ctrl.clips.Count > 0 ? ctrl.clips[0] : null;
|
|
500
|
+
o.Add("\"resolved_clip\":" + Q(clip != null ? AssetDatabase.GetAssetPath(clip) : ""));
|
|
501
|
+
o.Add("\"hit_beside\":" + B(File.Exists("Assets/Ghosted/hit.wav")));
|
|
502
|
+
}
|
|
503
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
504
|
+
Finish(o);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Session 2: hit.wav was touched up beside the model; the model reimports.
|
|
508
|
+
public static void Second()
|
|
509
|
+
{
|
|
510
|
+
var o = new List<string>();
|
|
511
|
+
try
|
|
512
|
+
{
|
|
513
|
+
AssetDatabase.ImportAsset("Assets/Ghosted/ghosted.fbx",
|
|
514
|
+
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
515
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
516
|
+
o.Add("\"touched_kept\":" + B(File.ReadAllBytes("Assets/Ghosted/hit.wav")
|
|
517
|
+
.SequenceEqual(File.ReadAllBytes(@"__TOUCHED__"))));
|
|
518
|
+
}
|
|
519
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
520
|
+
Finish(o);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class TestEmbeddedRequestPolicy(unittest.TestCase):
|
|
527
|
+
"""When an import asks for the FBX's embedded files to be extracted
|
|
528
|
+
beside it (``CarrierImport.RequestEmbeddedFiles``).
|
|
529
|
+
|
|
530
|
+
Two ways it overwrote what the user had:
|
|
531
|
+
|
|
532
|
+
- a name its importer finds ELSEWHERE (the audio lookup searches
|
|
533
|
+
``Assets/Audio``) counted as missing, because only the model's own folder
|
|
534
|
+
was looked in -- so the whole FBX was extracted and the clip rebound to
|
|
535
|
+
the copy beside the model;
|
|
536
|
+
- the memo of what was already asked lived for one editor session, so a
|
|
537
|
+
model naming a file it does not embed asked again on every session's
|
|
538
|
+
reimport, and each extraction overwrote the files beside it -- a
|
|
539
|
+
touched-up clip included.
|
|
540
|
+
|
|
541
|
+
The fixture is two real FBXs (``base_test.CarrierFbx``) with their audio
|
|
542
|
+
embedded as the DCC exporters do (``ptk.FbxMedia.embed``).
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def setUpClass(cls):
|
|
547
|
+
cls.temp_dir = TempProjects.allocate("embedded_request_policy")
|
|
548
|
+
|
|
549
|
+
def _launch(self, launcher, method: str, results: str) -> dict:
|
|
550
|
+
log = os.path.join(self.temp_dir, f"unity_{method}.txt")
|
|
551
|
+
proc = launcher.launch_editor(
|
|
552
|
+
batch_mode=True,
|
|
553
|
+
execute_method=f"EmbeddedRequestVerifier.{method}",
|
|
554
|
+
log_file=log,
|
|
555
|
+
extra_args=["-quit"],
|
|
556
|
+
detached=False,
|
|
557
|
+
)
|
|
558
|
+
returncode = None
|
|
559
|
+
if proc:
|
|
560
|
+
try:
|
|
561
|
+
returncode = proc.wait(timeout=900)
|
|
562
|
+
except subprocess.TimeoutExpired:
|
|
563
|
+
proc.kill()
|
|
564
|
+
raise TimeoutError("Unity timed out")
|
|
565
|
+
UnityEnv.require_results(self, results, returncode, log)
|
|
566
|
+
with open(results) as fh:
|
|
567
|
+
unity = json.load(fh)
|
|
568
|
+
logger.info(f"Unity {method}:\n{json.dumps(unity, indent=2)}")
|
|
569
|
+
self.assertNotIn("error", unity, unity.get("error"))
|
|
570
|
+
return unity
|
|
571
|
+
|
|
572
|
+
@BlenderEnv.skip_unless_available()
|
|
573
|
+
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
574
|
+
def test_found_elsewhere_or_asked_before_is_not_extracted_again(self):
|
|
575
|
+
import pythontk as ptk
|
|
576
|
+
|
|
577
|
+
def manifest(*names):
|
|
578
|
+
events = [{"clip": "", "frame": 0, "name": n} for n in names]
|
|
579
|
+
return {"data_export": {"audio_manifest": {"version": 2, "events": events}}}
|
|
580
|
+
|
|
581
|
+
staged = os.path.join(self.temp_dir, "staged")
|
|
582
|
+
os.makedirs(staged)
|
|
583
|
+
sources = {}
|
|
584
|
+
for stem, names, embed in (
|
|
585
|
+
("resolved", ("footstep",), "footstep"),
|
|
586
|
+
# "ghost" is embedded nowhere and resolves nowhere: always missing.
|
|
587
|
+
("ghosted", ("hit", "ghost"), "hit"),
|
|
588
|
+
):
|
|
589
|
+
bare = CarrierFbx.build(
|
|
590
|
+
os.path.join(self.temp_dir, f"{stem}_bare.fbx"), manifest(*names)
|
|
591
|
+
)
|
|
592
|
+
clip = CarrierFbx.wav(
|
|
593
|
+
os.path.join(self.temp_dir, "sound", f"{embed}.wav"), b"embedded"
|
|
594
|
+
)
|
|
595
|
+
sources[stem] = os.path.join(staged, f"{stem}.fbx")
|
|
596
|
+
ptk.FbxMedia.embed(bare, sources[stem], files=[(f"{embed}.wav", clip)])
|
|
597
|
+
|
|
598
|
+
launcher = UnityLauncher()
|
|
599
|
+
project = os.path.join(self.temp_dir, "UnityProject")
|
|
600
|
+
if not launcher.create_project(project, batch_mode=True):
|
|
601
|
+
self.skipTest(UnityEnv.project_creation_reason())
|
|
602
|
+
CarrierFbx.wav(
|
|
603
|
+
os.path.join(project, "Assets", "Audio", "footstep.wav"), b"library"
|
|
604
|
+
)
|
|
605
|
+
touched = CarrierFbx.wav(os.path.join(self.temp_dir, "touched.wav"), b"touched")
|
|
606
|
+
TemplateDeployer.deploy_package(project, components=["audio_event"])
|
|
607
|
+
results = os.path.join(self.temp_dir, "unity_results.json")
|
|
608
|
+
editor_dir = os.path.join(project, "Assets", "Editor")
|
|
609
|
+
os.makedirs(editor_dir, exist_ok=True)
|
|
610
|
+
with open(
|
|
611
|
+
os.path.join(editor_dir, "EmbeddedRequestVerifier.cs"),
|
|
612
|
+
"w",
|
|
613
|
+
encoding="utf-8",
|
|
614
|
+
) as fh:
|
|
615
|
+
fh.write(
|
|
616
|
+
REQUEST_VERIFIER_CS.replace("__RESULTS__", results)
|
|
617
|
+
.replace("__RESOLVED_SRC__", sources["resolved"])
|
|
618
|
+
.replace("__GHOSTED_SRC__", sources["ghosted"])
|
|
619
|
+
.replace("__TOUCHED__", touched)
|
|
620
|
+
)
|
|
621
|
+
launcher.project_path = project
|
|
622
|
+
|
|
623
|
+
first = self._launch(launcher, "First", results)
|
|
624
|
+
self.assertFalse(
|
|
625
|
+
first["resolved_beside"],
|
|
626
|
+
"a clip the lookup finds in Assets/Audio was extracted beside the model",
|
|
627
|
+
)
|
|
628
|
+
self.assertEqual(first["resolved_clip"], "Assets/Audio/footstep.wav", first)
|
|
629
|
+
self.assertTrue(first["hit_beside"], "premise: a missing clip is extracted")
|
|
630
|
+
|
|
631
|
+
# The user touches the extracted clip up; the next session reimports.
|
|
632
|
+
shutil.copy2(touched, os.path.join(project, "Assets", "Ghosted", "hit.wav"))
|
|
633
|
+
second = self._launch(launcher, "Second", results)
|
|
634
|
+
self.assertTrue(
|
|
635
|
+
second["touched_kept"],
|
|
636
|
+
"a later session's reimport extracted the FBX again over the touched-up "
|
|
637
|
+
"clip",
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
459
641
|
if __name__ == "__main__":
|
|
460
642
|
logging.basicConfig(level=logging.INFO)
|
|
461
643
|
unittest.main()
|
|
@@ -519,8 +519,11 @@ class ChildSettingsSandboxTest(unittest.TestCase):
|
|
|
519
519
|
with mock.patch.dict(os.environ, env, clear=True):
|
|
520
520
|
UnityTestRunner.sandbox_children(self.root)
|
|
521
521
|
armed = os.environ.copy()
|
|
522
|
-
# As in a run: TEMP
|
|
523
|
-
|
|
522
|
+
# As in a run: TEMP and pythontk's own root (read first, since
|
|
523
|
+
# pythontk 0.15.0) name the run's root, which the stores nest in.
|
|
524
|
+
armed.update(
|
|
525
|
+
TEMP=self.root, TMP=self.root, TMPDIR=self.root, PYTHONTK_TEMP_ROOT=self.root
|
|
526
|
+
)
|
|
524
527
|
armed.pop("PYTHONTK_TEST_TEMP_ROOT", None)
|
|
525
528
|
scripts = pathlib.Path(__file__).resolve().parents[2]
|
|
526
529
|
armed["PYTHONPATH"] = os.pathsep.join(str(scripts / pkg) for pkg in packages)
|
|
@@ -156,7 +156,7 @@ class CopyToAssetsDeliverer(ptk.Deliverer):
|
|
|
156
156
|
OSError: The copy failed.
|
|
157
157
|
"""
|
|
158
158
|
if not ptk.FbxFile.is_fbx(source):
|
|
159
|
-
|
|
159
|
+
CopyToAssetsDeliverer._place(source, dest)
|
|
160
160
|
return
|
|
161
161
|
hook = getattr(bridge, "lightmap_search_dirs", None)
|
|
162
162
|
try:
|
|
@@ -181,10 +181,27 @@ class CopyToAssetsDeliverer(ptk.Deliverer):
|
|
|
181
181
|
"exported."
|
|
182
182
|
)
|
|
183
183
|
report = None
|
|
184
|
-
|
|
184
|
+
CopyToAssetsDeliverer._place(
|
|
185
|
+
staged if report and report["embedded"] else source, dest
|
|
186
|
+
)
|
|
185
187
|
finally:
|
|
186
188
|
scratch.cleanup()
|
|
187
189
|
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _place(source: str, dest: str) -> None:
|
|
192
|
+
"""Copy *source* to *dest* whole or not at all.
|
|
193
|
+
|
|
194
|
+
Through a ``.part`` beside *dest*, promoted once complete
|
|
195
|
+
(``ptk.FileUtils.atomic_write``): a copy cut short -- a full disk, a
|
|
196
|
+
sync client holding the file -- leaves the asset Unity already
|
|
197
|
+
imported as it was, never a truncated FBX under its name. *source* is
|
|
198
|
+
only read (it may be the user's own file).
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
OSError: The copy failed; *dest* is untouched.
|
|
202
|
+
"""
|
|
203
|
+
ptk.FileUtils.atomic_write(dest, lambda part: shutil.copy2(source, part))
|
|
204
|
+
|
|
188
205
|
@staticmethod
|
|
189
206
|
def sanitize_asset_name(name: str) -> str:
|
|
190
207
|
"""Return *name* reduced to a safe Unity asset filename stem.
|
|
@@ -273,8 +290,7 @@ class FileToUnityBridge(ptk.HandoffBridge):
|
|
|
273
290
|
those export a live selection then deliver; this one takes a model already on
|
|
274
291
|
disk. It shares the exact same delivery Strategy -- :class:`CopyToAssetsDeliverer`
|
|
275
292
|
(copy into ``Assets/`` + optional Editor launch) -- so the standalone Unity
|
|
276
|
-
Workflow panel,
|
|
277
|
-
then hands the path here), a CLI, or a test all reuse one engine.
|
|
293
|
+
Workflow panel, a CLI, or a test all reuse one engine.
|
|
278
294
|
|
|
279
295
|
Set :attr:`project_path` to the target Unity project (the folder containing
|
|
280
296
|
``Assets/``) before calling :meth:`send`::
|