unitytk 0.0.3__tar.gz → 0.0.4__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 (47) hide show
  1. {unitytk-0.0.3/unitytk.egg-info → unitytk-0.0.4}/PKG-INFO +2 -2
  2. {unitytk-0.0.3 → unitytk-0.0.4}/docs/README.md +1 -1
  3. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_c130_fcr_integration.py +1 -1
  4. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_deploy_templates.py +41 -0
  5. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_fade_window_preservation.py +3 -1
  6. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/__init__.py +1 -1
  7. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/template_deployer.py +98 -8
  8. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/AudioEventController.cs +2 -3
  9. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/EmissiveGroupController.cs +22 -5
  10. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/LightmapMetadataController.cs +2 -2
  11. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/README.md +12 -0
  12. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/RenderOpacityController.cs +17 -8
  13. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/ShadowPlaneController.cs +2 -2
  14. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/ShotMetadataController.cs +2 -2
  15. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/UnitytkSettings.cs +90 -4
  16. {unitytk-0.0.3 → unitytk-0.0.4/unitytk.egg-info}/PKG-INFO +2 -2
  17. {unitytk-0.0.3 → unitytk-0.0.4}/LICENSE +0 -0
  18. {unitytk-0.0.3 → unitytk-0.0.4}/MANIFEST.in +0 -0
  19. {unitytk-0.0.3 → unitytk-0.0.4}/pyproject.toml +0 -0
  20. {unitytk-0.0.3 → unitytk-0.0.4}/setup.cfg +0 -0
  21. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_asset_delivery.py +0 -0
  22. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_audio_events_integration.py +0 -0
  23. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_bool_to_fade_curve.py +0 -0
  24. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_c5m_visibility.py +0 -0
  25. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_clip_name_contract_integration.py +0 -0
  26. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_launcher.py +0 -0
  27. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_lightmap_metadata_integration.py +0 -0
  28. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_render_opacity_controller.py +0 -0
  29. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_render_opacity_e2e.py +0 -0
  30. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_render_opacity_integration.py +0 -0
  31. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_scene_builder.py +0 -0
  32. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_shadow_plane_integration.py +0 -0
  33. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_shared_material_standalone.py +0 -0
  34. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_shot_metadata_integration.py +0 -0
  35. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_shots_audio_sidebyside_integration.py +0 -0
  36. {unitytk-0.0.3 → unitytk-0.0.4}/test/test_standalone_enduser.py +0 -0
  37. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/asset_delivery.py +0 -0
  38. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/launcher.py +0 -0
  39. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/scene_builder.py +0 -0
  40. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/EmissiveGroups.hlsl +0 -0
  41. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
  42. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  43. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk/templates/__init__.py +0 -0
  44. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk.egg-info/SOURCES.txt +0 -0
  45. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk.egg-info/dependency_links.txt +0 -0
  46. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk.egg-info/requires.txt +0 -0
  47. {unitytk-0.0.3 → unitytk-0.0.4}/unitytk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.3
3
+ Version: 0.0.4
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-113%20passed-brightgreen.svg)](../test/)
18
+ [![Tests](https://img.shields.io/badge/Tests-116%20passed-brightgreen.svg)](../test/)
19
19
 
20
20
  # unitytk
21
21
 
@@ -1,4 +1,4 @@
1
- [![Tests](https://img.shields.io/badge/Tests-113%20passed-brightgreen.svg)](../test/)
1
+ [![Tests](https://img.shields.io/badge/Tests-116%20passed-brightgreen.svg)](../test/)
2
2
 
3
3
  # unitytk
4
4
 
@@ -641,7 +641,7 @@ class TestC130FcrIntegration(unittest.TestCase):
641
641
 
642
642
  @classmethod
643
643
  def setUpClass(cls):
644
- # Repo-local temp_tests (already on O:); temp dir — C: drive is often full
644
+ # Repo-local temp_tests (already on the O: drive; C: is often full).
645
645
  _temp_base = os.path.join(os.path.dirname(__file__), "temp_tests")
646
646
  os.makedirs(_temp_base, exist_ok=True)
647
647
  cls.temp_dir = tempfile.mkdtemp(prefix="c130_fcr_integ_", dir=_temp_base)
@@ -92,6 +92,47 @@ class DeployTemplateTest(unittest.TestCase):
92
92
  with open(shader, encoding="utf-8") as fh:
93
93
  self.assertIn('#include "EmissiveGroups.hlsl"', fh.read())
94
94
 
95
+ def test_status_reports_not_installed_then_up_to_date(self):
96
+ """The panels' Status action: absent → installed=False with the full
97
+ bundle missing; after deploy → current version, nothing missing."""
98
+ before = TemplateDeployer.status(self.project)
99
+ self.assertFalse(before["installed"])
100
+ self.assertFalse(before["up_to_date"])
101
+ self.assertIn("UnitytkSettings.cs", before["missing"])
102
+
103
+ TemplateDeployer.deploy_package(self.project)
104
+ after = TemplateDeployer.status(self.project)
105
+ self.assertTrue(after["installed"])
106
+ self.assertEqual(after["version"], unitytk.__version__)
107
+ self.assertEqual(after["missing"], [])
108
+ self.assertTrue(after["up_to_date"])
109
+
110
+ def test_status_flags_a_partial_deploy(self):
111
+ """A deleted file shows in ``missing`` and clears ``up_to_date`` — the
112
+ compile-coupled set is only valid whole."""
113
+ TemplateDeployer.deploy_package(self.project)
114
+ os.remove(os.path.join(self.package, "UnitytkSettings.cs"))
115
+ info = TemplateDeployer.status(self.project)
116
+ self.assertEqual(info["missing"], ["UnitytkSettings.cs"])
117
+ self.assertFalse(info["up_to_date"])
118
+
119
+ def test_uninstall_removes_only_our_package(self):
120
+ """Uninstall deletes the deployed folder (True), no-ops when absent
121
+ (False), and REFUSES a folder whose package.json is not ours — never
122
+ delete a directory we merely expected to own."""
123
+ self.assertFalse(TemplateDeployer.uninstall_package(self.project))
124
+
125
+ TemplateDeployer.deploy_package(self.project)
126
+ self.assertTrue(TemplateDeployer.uninstall_package(self.project))
127
+ self.assertFalse(os.path.isdir(self.package))
128
+
129
+ os.makedirs(self.package)
130
+ with open(os.path.join(self.package, "package.json"), "w") as fh:
131
+ json.dump({"name": "com.example.other"}, fh)
132
+ with self.assertRaises(ValueError):
133
+ TemplateDeployer.uninstall_package(self.project)
134
+ self.assertTrue(os.path.isdir(self.package), "foreign folder untouched")
135
+
95
136
  def test_deploy_package_never_writes_project_config(self):
96
137
  """Per-project settings live in ProjectSettings/UnitytkSettings.json,
97
138
  owned by the project — deploying must not create or clobber them."""
@@ -209,7 +209,9 @@ class TestFadeWindowPreservation(unittest.TestCase):
209
209
 
210
210
  @classmethod
211
211
  def setUpClass(cls):
212
- base = os.path.join(repo_root, "test", "temp_tests")
212
+ # Repo-local temp_tests (repo_root is the WORKSPACE root, for sys.path
213
+ # only -- joining it here scattered Unity projects outside the repo).
214
+ base = os.path.join(os.path.dirname(__file__), "temp_tests")
213
215
  os.makedirs(base, exist_ok=True)
214
216
  cls.temp_dir = tempfile.mkdtemp(prefix="fade_window_", dir=base)
215
217
  cls.fbx_path = None
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.3"
1
+ __version__ = "0.0.4"
2
2
 
3
3
  from .launcher import UnityLauncher, UnityFinder
4
4
  from .scene_builder import SceneBuilder
@@ -22,6 +22,7 @@ from __future__ import annotations
22
22
 
23
23
  import json
24
24
  import os
25
+ import shutil
25
26
  from importlib import resources
26
27
  from typing import List
27
28
 
@@ -106,15 +107,10 @@ class TemplateDeployer:
106
107
  f"Not a Unity project (no Assets/ folder): {project_root!r}"
107
108
  )
108
109
 
109
- package_dir = os.path.join(project_root, "Packages", UPM_PACKAGE_NAME)
110
- root = resources.files(TEMPLATE_PACKAGE)
111
- names = sorted(
112
- entry.name
113
- for entry in root.iterdir()
114
- if entry.name.endswith(CODE_SUFFIXES) or entry.name == "README.md"
115
- )
110
+ package_dir = cls.package_dir(project_root)
116
111
  written = [
117
- cls.deploy(name, os.path.join(package_dir, name)) for name in names
112
+ cls.deploy(name, os.path.join(package_dir, name))
113
+ for name in cls._bundle_names()
118
114
  ]
119
115
 
120
116
  manifest_path = os.path.join(package_dir, "package.json")
@@ -125,6 +121,100 @@ class TemplateDeployer:
125
121
 
126
122
  return sorted(written)
127
123
 
124
+ @classmethod
125
+ def package_dir(cls, project_root: str) -> str:
126
+ """The embedded package's folder inside *project_root* (may not exist)."""
127
+ return os.path.join(project_root, "Packages", UPM_PACKAGE_NAME)
128
+
129
+ @classmethod
130
+ def status(cls, project_root: str) -> dict:
131
+ """Report the deployed state of the embedded package in *project_root*.
132
+
133
+ Never raises for an absent/partial deployment -- "not installed" is a
134
+ reportable state, not an error (the DCC panels' *Status* action renders
135
+ this dict into their log).
136
+
137
+ Returns:
138
+ dict: ``installed`` (bool), ``package_dir`` (str),
139
+ ``version`` (str or None -- the deployed ``package.json`` version),
140
+ ``bundled_version`` (str -- this unitytk release),
141
+ ``missing`` (list[str] -- bundled files absent from the deploy),
142
+ ``up_to_date`` (bool -- current version and nothing missing).
143
+ """
144
+ import unitytk
145
+
146
+ package_dir = cls.package_dir(project_root)
147
+ bundled = cls._bundle_names()
148
+ result = {
149
+ "installed": False,
150
+ "package_dir": package_dir,
151
+ "version": None,
152
+ "bundled_version": unitytk.__version__,
153
+ "missing": list(bundled),
154
+ "up_to_date": False,
155
+ }
156
+ manifest_path = os.path.join(package_dir, "package.json")
157
+ if not os.path.isfile(manifest_path):
158
+ return result
159
+
160
+ result["installed"] = True
161
+ try:
162
+ with open(manifest_path, encoding="utf-8") as fh:
163
+ result["version"] = json.load(fh).get("version")
164
+ except (OSError, ValueError):
165
+ pass # corrupt manifest: installed but version unknown
166
+
167
+ present = set(os.listdir(package_dir))
168
+ result["missing"] = [n for n in bundled if n not in present]
169
+ result["up_to_date"] = (
170
+ result["version"] == unitytk.__version__ and not result["missing"]
171
+ )
172
+ return result
173
+
174
+ @classmethod
175
+ def uninstall_package(cls, project_root: str) -> bool:
176
+ """Remove the embedded package from *project_root*.
177
+
178
+ Returns:
179
+ bool: True when the package folder was removed; False when there
180
+ was nothing deployed.
181
+
182
+ Raises:
183
+ ValueError: the folder at the package path is NOT ours -- its
184
+ ``package.json`` is absent or names a different package.
185
+ Refusing beats deleting a directory we merely expected to own.
186
+ """
187
+ package_dir = cls.package_dir(project_root)
188
+ if not os.path.isdir(package_dir):
189
+ return False
190
+
191
+ name = None
192
+ try:
193
+ with open(
194
+ os.path.join(package_dir, "package.json"), encoding="utf-8"
195
+ ) as fh:
196
+ name = json.load(fh).get("name")
197
+ except (OSError, ValueError):
198
+ pass
199
+ if name != UPM_PACKAGE_NAME:
200
+ raise ValueError(
201
+ f"Refusing to delete {package_dir!r}: its package.json does "
202
+ f"not identify it as {UPM_PACKAGE_NAME!r}."
203
+ )
204
+
205
+ shutil.rmtree(package_dir)
206
+ return True
207
+
208
+ @staticmethod
209
+ def _bundle_names() -> "List[str]":
210
+ """The bundled deployables, sorted: code set + the catalog README."""
211
+ root = resources.files(TEMPLATE_PACKAGE)
212
+ return sorted(
213
+ entry.name
214
+ for entry in root.iterdir()
215
+ if entry.name.endswith(CODE_SUFFIXES) or entry.name == "README.md"
216
+ )
217
+
128
218
  @staticmethod
129
219
  def _package_manifest() -> dict:
130
220
  """The generated ``package.json`` -- version mirrors this release."""
@@ -230,9 +230,8 @@ public class AudioEventImporter : AssetPostprocessor
230
230
  // so the next import of this asset would inject every event twice.
231
231
  _pendingManifests.Remove(assetPath);
232
232
 
233
- if (!ShouldProcess()) return;
234
-
235
- CarrierImport.EnableUserProps(assetImporter);
233
+ // String channel: user properties arrive with Unity's default
234
+ // importer settings (probe-verified) — nothing to configure here.
236
235
  }
237
236
 
238
237
  // Why we attach the controller to the prefab ROOT, not the
@@ -215,18 +215,35 @@ public class EmissiveGroupImporter : AssetPostprocessor
215
215
  {
216
216
  // Fresh-import reset: JSON stranded by an import that aborted between
217
217
  // the callbacks must not resurrect on a re-export without the carrier.
218
+ // Touches NO importer settings — the manifest is a string channel that
219
+ // arrives with Unity's defaults; only Maya keyed weights need a flag,
220
+ // requested per-asset on detection below.
218
221
  _pending.Remove(assetPath);
219
-
220
- if (!ShouldProcess()) return;
221
- CarrierImport.EnableUserProps(assetImporter);
222
222
  }
223
223
 
224
224
  void OnPostprocessGameObjectWithUserProperties(
225
225
  GameObject go, string[] propNames, object[] values)
226
226
  {
227
227
  if (!ShouldProcess()) return;
228
- if (CarrierImport.TryGetProp(propNames, values, ATTR_MANIFEST, out var json))
229
- _pending[assetPath] = json;
228
+ if (!CarrierImport.TryGetProp(propNames, values, ATTR_MANIFEST, out var json))
229
+ return;
230
+ _pending[assetPath] = json;
231
+
232
+ // Maya keyed weights ride animated custom properties (per-group
233
+ // `attr` records). Only such assets need the importer flag — request
234
+ // it here so unrelated FBX keep the user's own settings.
235
+ var payload = ParsePayload(json, assetPath);
236
+ if (payload != null)
237
+ {
238
+ foreach (var rec in payload.groups)
239
+ {
240
+ if (rec != null && !string.IsNullOrEmpty(rec.attr))
241
+ {
242
+ CarrierImport.RequestAnimatedCustomProperties(assetPath);
243
+ break;
244
+ }
245
+ }
246
+ }
230
247
  }
231
248
 
232
249
  // --- Keyed weights: rebind DCC weight curves to the controller --------
@@ -134,8 +134,8 @@ public class LightmapMetadataImporter : AssetPostprocessor
134
134
  // the callbacks must not resurrect on a re-export without the carrier.
135
135
  _pending.Remove(assetPath);
136
136
 
137
- if (!ShouldProcess()) return;
138
- CarrierImport.EnableUserProps(assetImporter);
137
+ // String channel: user properties arrive with Unity's default
138
+ // importer settings (probe-verified) — nothing to configure here.
139
139
  // NOTE: deliberately does NOT touch generateSecondaryUV here — at
140
140
  // preprocess we don't yet know if this FBX carries Maya lightmaps, and
141
141
  // with no allowlist config every FBX is "allowed", so blindly flipping
@@ -82,6 +82,18 @@ Settings are stored as JSON in `ProjectSettings/UnitytkSettings.json` (missing
82
82
  file/keys = everything on, all FBX). External tools can write it directly;
83
83
  changes are picked up automatically (no restart needed).
84
84
 
85
+ ### Safety: unrelated assets are never modified
86
+
87
+ The importers **never touch a model's import settings up front**. Channel
88
+ detection works with Unity's default settings, so an FBX that carries no
89
+ unitytk metadata comes through the pipeline byte-identical to a project
90
+ without unitytk installed. Only when an asset is detected to carry *animated*
91
+ channel data (opacity fades, Maya keyed emissive weights) are the two settings
92
+ that data needs — *Animated Custom Properties* on, *Resample Curves* off —
93
+ enabled **on that one asset**, with a single automatic reimport (logged).
94
+ Nothing is ever switched off, and no other asset's settings are read or
95
+ written.
96
+
85
97
  ---
86
98
 
87
99
  ## Importing an FBX
@@ -209,15 +209,11 @@ public class RenderOpacityImporter : AssetPostprocessor
209
209
  // an import that aborts between the callbacks (another postprocessor
210
210
  // throwing) strands destroyed Transform refs here — EnsureControllers
211
211
  // would trip over them on the NEXT import of this asset.
212
+ //
213
+ // Deliberately touches NO importer settings: the settings this channel
214
+ // needs are requested per-asset on detection (see Step 1) and applied
215
+ // by UnitytkImportConfigurator — unrelated FBX are never modified.
212
216
  _opacityTransforms.Remove(assetPath);
213
-
214
- if (!ShouldProcess()) return;
215
-
216
- CarrierImport.EnableUserProps(assetImporter);
217
-
218
- var importer = assetImporter as ModelImporter;
219
- if (importer != null && importer.resampleCurves)
220
- importer.resampleCurves = false;
221
217
  }
222
218
 
223
219
  // --- Step 1: Record opacity transforms ------------------------------------
@@ -232,6 +228,13 @@ public class RenderOpacityImporter : AssetPostprocessor
232
228
  if (!ShouldProcess()) return;
233
229
  if (!CarrierImport.HasProp(propNames, ATTR_NAME)) return;
234
230
 
231
+ // This asset carries opacity data: it needs the custom-property curve
232
+ // (flattened 'opacity') and SPARSE visibility keys — the fade windows
233
+ // live in the gaps, which resampling destroys. Requested here, applied
234
+ // (with a one-time reimport of this asset only) after the batch.
235
+ CarrierImport.RequestAnimatedCustomProperties(assetPath);
236
+ CarrierImport.RequestSparseCurves(assetPath);
237
+
235
238
  if (!_opacityTransforms.ContainsKey(assetPath))
236
239
  _opacityTransforms[assetPath] = new HashSet<Transform>();
237
240
  _opacityTransforms[assetPath].Add(go.transform);
@@ -253,6 +256,12 @@ public class RenderOpacityImporter : AssetPostprocessor
253
256
  {
254
257
  if (!ShouldProcess()) return;
255
258
 
259
+ // Detection pass: the importer settings this conversion depends on
260
+ // (custom-prop curves present, resampling off) aren't in effect yet —
261
+ // converting RESAMPLED visibility would fabricate wrong fades. The
262
+ // configured reimport that follows runs the real conversion.
263
+ if (CarrierImport.IsDetectionPass(assetPath, assetImporter)) return;
264
+
256
265
  var opaPaths = EnsureControllers(assetPath, root.transform);
257
266
  if (opaPaths.Count == 0) return;
258
267
 
@@ -116,8 +116,8 @@ public class ShadowPlaneImporter : AssetPostprocessor
116
116
  // the callbacks must not resurrect on a re-export without the carrier.
117
117
  _pending.Remove(assetPath);
118
118
 
119
- if (!ShouldProcess()) return;
120
- CarrierImport.EnableUserProps(assetImporter);
119
+ // String channel: user properties arrive with Unity's default
120
+ // importer settings (probe-verified) — nothing to configure here.
121
121
  }
122
122
 
123
123
  // ---- silhouette texture: pin import settings by naming convention ----
@@ -105,8 +105,8 @@ public class ShotMetadataImporter : AssetPostprocessor
105
105
  // the callbacks must not resurrect on a re-export without the carrier.
106
106
  _pending.Remove(assetPath);
107
107
 
108
- if (!ShouldProcess()) return;
109
- CarrierImport.EnableUserProps(assetImporter);
108
+ // String channel: user properties arrive with Unity's default
109
+ // importer settings (probe-verified) — nothing to configure here.
110
110
  }
111
111
 
112
112
  void OnPostprocessGameObjectWithUserProperties(
@@ -212,12 +212,83 @@ public static class CarrierImport
212
212
  return false;
213
213
  }
214
214
 
215
- /// <summary>Enable animated custom-property import carrier channels ride on it.</summary>
216
- public static void EnableUserProps(AssetImporter importer)
215
+ // ---- Per-asset import-settings scoping -------------------------------
216
+ //
217
+ // Channel DETECTION needs no importer settings: FBX user properties reach
218
+ // OnPostprocessGameObjectWithUserProperties even with Unity's defaults
219
+ // (verified against a live batch 6000.3: string channels AND the animated
220
+ // 'opacity' attr arrive with importAnimatedCustomProperties=false — only
221
+ // the flattened curve needs the flag). So unitytk never touches a model
222
+ // importer up front; a channel importer that discovers it needs a setting
223
+ // REQUESTS it here for that one asset, and the pending set is applied
224
+ // (with a single reimport of just that asset) after the import batch.
225
+ // Unrelated FBX are never modified, and nothing is ever switched OFF —
226
+ // the user's own import settings are left exactly as they had them.
227
+
228
+ static readonly HashSet<string> _needCustomProps = new HashSet<string>();
229
+ static readonly HashSet<string> _needSparseCurves = new HashSet<string>();
230
+
231
+ /// <summary>This asset needs animated custom-property curves
232
+ /// (opacity / Maya keyed emissive weights).</summary>
233
+ public static void RequestAnimatedCustomProperties(string assetPath)
234
+ => _needCustomProps.Add(assetPath);
235
+
236
+ /// <summary>This asset needs sparse (unresampled) curves — the opacity
237
+ /// fade windows live in the gaps between keys.</summary>
238
+ public static void RequestSparseCurves(string assetPath)
239
+ => _needSparseCurves.Add(assetPath);
240
+
241
+ /// <summary>True while *importer*'s current settings can't yet satisfy the
242
+ /// asset's requested channels — i.e. this pass is detection-only and a
243
+ /// configured reimport is coming. Curve-rewriting importers skip their
244
+ /// work on such a pass rather than transform resampled data.</summary>
245
+ public static bool IsDetectionPass(string assetPath, AssetImporter importer)
217
246
  {
218
247
  var mi = importer as ModelImporter;
219
- if (mi != null && !mi.importAnimatedCustomProperties)
220
- mi.importAnimatedCustomProperties = true;
248
+ if (mi == null) return false;
249
+ if (_needCustomProps.Contains(assetPath) && !mi.importAnimatedCustomProperties)
250
+ return true;
251
+ if (_needSparseCurves.Contains(assetPath) && mi.resampleCurves)
252
+ return true;
253
+ return false;
254
+ }
255
+
256
+ /// <summary>Apply every pending request; called from
257
+ /// <c>UnitytkImportConfigurator.OnPostprocessAllAssets</c> (after the
258
+ /// import batch, when reimporting is legal). Reimports only assets whose
259
+ /// settings actually changed, so a configured asset never loops.</summary>
260
+ public static void ApplyPendingImportSettings()
261
+ {
262
+ if (_needCustomProps.Count == 0 && _needSparseCurves.Count == 0)
263
+ return;
264
+
265
+ var paths = new HashSet<string>(_needCustomProps);
266
+ paths.UnionWith(_needSparseCurves);
267
+ foreach (var assetPath in paths)
268
+ {
269
+ bool wantProps = _needCustomProps.Remove(assetPath);
270
+ bool wantSparse = _needSparseCurves.Remove(assetPath);
271
+ var mi = AssetImporter.GetAtPath(assetPath) as ModelImporter;
272
+ if (mi == null) continue;
273
+
274
+ var enabled = new List<string>();
275
+ if (wantProps && !mi.importAnimatedCustomProperties)
276
+ {
277
+ mi.importAnimatedCustomProperties = true;
278
+ enabled.Add("Animated Custom Properties");
279
+ }
280
+ if (wantSparse && mi.resampleCurves)
281
+ {
282
+ mi.resampleCurves = false;
283
+ enabled.Add("unresampled curves (Resample Curves off)");
284
+ }
285
+ if (enabled.Count == 0) continue;
286
+
287
+ Debug.Log($"[unitytk] '{assetPath}' carries animated channel data — "
288
+ + string.Join(" + ", enabled)
289
+ + " set on this asset only; reimporting it once.");
290
+ mi.SaveAndReimport();
291
+ }
221
292
  }
222
293
 
223
294
  /// <summary>Warn when a payload comes from a newer producer schema than this importer.</summary>
@@ -231,6 +302,21 @@ public static class CarrierImport
231
302
  }
232
303
  }
233
304
 
305
+ /// <summary>
306
+ /// Applies the per-asset import-settings requests collected during the batch
307
+ /// (see the scoping notes on <see cref="CarrierImport"/>). Lives in its own
308
+ /// postprocessor because reimporting from OnPostprocessAllAssets is the
309
+ /// supported pattern; doing it mid-import is not.
310
+ /// </summary>
311
+ public class UnitytkImportConfigurator : AssetPostprocessor
312
+ {
313
+ static void OnPostprocessAllAssets(
314
+ string[] imported, string[] deleted, string[] moved, string[] movedFrom)
315
+ {
316
+ CarrierImport.ApplyPendingImportSettings();
317
+ }
318
+ }
319
+
234
320
  /// <summary>
235
321
  /// Edit ▸ Project Settings ▸ unitytk — the native home for "what is this
236
322
  /// package doing to my imports": one toggle per channel plus the FBX
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.3
3
+ Version: 0.0.4
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-113%20passed-brightgreen.svg)](../test/)
18
+ [![Tests](https://img.shields.io/badge/Tests-116%20passed-brightgreen.svg)](../test/)
19
19
 
20
20
  # unitytk
21
21
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes