unityflow 0.3.4__py3-none-any.whl
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.
- unityflow/__init__.py +167 -0
- unityflow/asset_resolver.py +636 -0
- unityflow/asset_tracker.py +1687 -0
- unityflow/cli.py +2317 -0
- unityflow/data/__init__.py +1 -0
- unityflow/data/class_ids.json +336 -0
- unityflow/diff.py +234 -0
- unityflow/fast_parser.py +676 -0
- unityflow/formats.py +1558 -0
- unityflow/git_utils.py +307 -0
- unityflow/hierarchy.py +1672 -0
- unityflow/merge.py +226 -0
- unityflow/meta_generator.py +1291 -0
- unityflow/normalizer.py +529 -0
- unityflow/parser.py +698 -0
- unityflow/query.py +406 -0
- unityflow/script_parser.py +717 -0
- unityflow/sprite.py +378 -0
- unityflow/validator.py +783 -0
- unityflow-0.3.4.dist-info/METADATA +293 -0
- unityflow-0.3.4.dist-info/RECORD +25 -0
- unityflow-0.3.4.dist-info/WHEEL +5 -0
- unityflow-0.3.4.dist-info/entry_points.txt +2 -0
- unityflow-0.3.4.dist-info/licenses/LICENSE +21 -0
- unityflow-0.3.4.dist-info/top_level.txt +1 -0
unityflow/sprite.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Sprite Reference Utilities.
|
|
2
|
+
|
|
3
|
+
Provides utilities for working with Unity sprite references:
|
|
4
|
+
- Automatic fileID detection based on sprite mode
|
|
5
|
+
- Meta file parsing for sprite import settings
|
|
6
|
+
- Material reference helpers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from unityflow.asset_tracker import META_GUID_PATTERN
|
|
17
|
+
|
|
18
|
+
# Well-known material GUIDs
|
|
19
|
+
KNOWN_MATERIALS = {
|
|
20
|
+
# URP (Universal Render Pipeline)
|
|
21
|
+
"Sprite-Lit-Default": "a97c105638bdf8b4a8650670310a4cd3",
|
|
22
|
+
# Built-in render pipeline
|
|
23
|
+
"Sprites-Default": "10754", # This is a built-in material (fileID only)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Built-in material fileIDs (no GUID needed)
|
|
27
|
+
BUILTIN_MATERIAL_FILE_IDS = {
|
|
28
|
+
"Sprites-Default": 10754,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Default sprite fileID for Single mode
|
|
32
|
+
SPRITE_SINGLE_MODE_FILE_ID = 21300000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SpriteInfo:
|
|
37
|
+
"""Information about a sprite extracted from its meta file."""
|
|
38
|
+
|
|
39
|
+
guid: str
|
|
40
|
+
sprite_mode: int # 1 = Single, 2 = Multiple
|
|
41
|
+
sprites: list[dict[str, Any]] = field(default_factory=list)
|
|
42
|
+
internal_id_table: dict[str, int] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_single(self) -> bool:
|
|
46
|
+
"""Check if sprite is in Single mode."""
|
|
47
|
+
return self.sprite_mode == 1
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_multiple(self) -> bool:
|
|
51
|
+
"""Check if sprite is in Multiple mode."""
|
|
52
|
+
return self.sprite_mode == 2
|
|
53
|
+
|
|
54
|
+
def get_file_id(self, sub_sprite_name: str | None = None) -> int | None:
|
|
55
|
+
"""Get the fileID for referencing this sprite.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
sub_sprite_name: For Multiple mode, the name of the specific sub-sprite.
|
|
59
|
+
If None, returns the first sprite's ID for Multiple mode.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The fileID to use in the sprite reference, or None if not found.
|
|
63
|
+
"""
|
|
64
|
+
if self.is_single:
|
|
65
|
+
return SPRITE_SINGLE_MODE_FILE_ID
|
|
66
|
+
|
|
67
|
+
if self.is_multiple:
|
|
68
|
+
if sub_sprite_name:
|
|
69
|
+
# Look up by name in internal ID table
|
|
70
|
+
return self.internal_id_table.get(sub_sprite_name)
|
|
71
|
+
elif self.sprites:
|
|
72
|
+
# Return first sprite's internalID
|
|
73
|
+
return self.sprites[0].get("internalID")
|
|
74
|
+
elif self.internal_id_table:
|
|
75
|
+
# Fallback to first entry in internal ID table
|
|
76
|
+
return next(iter(self.internal_id_table.values()), None)
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def get_sprite_names(self) -> list[str]:
|
|
81
|
+
"""Get list of all sprite names (for Multiple mode)."""
|
|
82
|
+
if self.is_single:
|
|
83
|
+
return []
|
|
84
|
+
return list(self.internal_id_table.keys())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class SpriteReference:
|
|
89
|
+
"""A complete sprite reference for use in prefabs."""
|
|
90
|
+
|
|
91
|
+
file_id: int
|
|
92
|
+
guid: str
|
|
93
|
+
type: int = 3 # Unity reference type (3 = asset)
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict[str, Any]:
|
|
96
|
+
"""Convert to Unity reference dictionary format."""
|
|
97
|
+
return {
|
|
98
|
+
"fileID": self.file_id,
|
|
99
|
+
"guid": self.guid,
|
|
100
|
+
"type": self.type,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class MaterialReference:
|
|
106
|
+
"""A material reference for use in prefabs."""
|
|
107
|
+
|
|
108
|
+
file_id: int
|
|
109
|
+
guid: str | None = None
|
|
110
|
+
type: int = 2 # Unity reference type (2 for most assets)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Convert to Unity reference dictionary format."""
|
|
114
|
+
ref: dict[str, Any] = {"fileID": self.file_id}
|
|
115
|
+
if self.guid:
|
|
116
|
+
ref["guid"] = self.guid
|
|
117
|
+
ref["type"] = self.type
|
|
118
|
+
return ref
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_sprite_meta(meta_path: Path) -> SpriteInfo | None:
|
|
122
|
+
"""Parse a sprite meta file to extract sprite information.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
meta_path: Path to the .meta file
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
SpriteInfo object or None if parsing failed
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
132
|
+
except OSError:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Extract GUID
|
|
136
|
+
guid_match = META_GUID_PATTERN.search(content)
|
|
137
|
+
if not guid_match:
|
|
138
|
+
return None
|
|
139
|
+
guid = guid_match.group(1)
|
|
140
|
+
|
|
141
|
+
# Extract spriteMode
|
|
142
|
+
sprite_mode_match = re.search(r"^\s*spriteMode:\s*(\d+)", content, re.MULTILINE)
|
|
143
|
+
sprite_mode = int(sprite_mode_match.group(1)) if sprite_mode_match else 1
|
|
144
|
+
|
|
145
|
+
sprites: list[dict[str, Any]] = []
|
|
146
|
+
internal_id_table: dict[str, int] = {}
|
|
147
|
+
|
|
148
|
+
if sprite_mode == 2: # Multiple mode
|
|
149
|
+
# Parse internalIDToNameTable for sprite name -> ID mapping
|
|
150
|
+
# Format:
|
|
151
|
+
# internalIDToNameTable:
|
|
152
|
+
# - first:
|
|
153
|
+
# 213: <internal_id>
|
|
154
|
+
# second: sprite_name
|
|
155
|
+
internal_table_match = re.search(
|
|
156
|
+
r"internalIDToNameTable:\s*((?:\s*-\s+first:.*?second:.*?)+)",
|
|
157
|
+
content,
|
|
158
|
+
re.DOTALL,
|
|
159
|
+
)
|
|
160
|
+
if internal_table_match:
|
|
161
|
+
table_content = internal_table_match.group(1)
|
|
162
|
+
# Parse each entry
|
|
163
|
+
entry_pattern = re.compile(
|
|
164
|
+
r"-\s+first:\s*\n\s+213:\s*(-?\d+)\s*\n\s+second:\s*(\S+)",
|
|
165
|
+
re.MULTILINE,
|
|
166
|
+
)
|
|
167
|
+
for match in entry_pattern.finditer(table_content):
|
|
168
|
+
internal_id = int(match.group(1))
|
|
169
|
+
sprite_name = match.group(2).strip()
|
|
170
|
+
internal_id_table[sprite_name] = internal_id
|
|
171
|
+
sprites.append({"name": sprite_name, "internalID": internal_id})
|
|
172
|
+
|
|
173
|
+
# Also try to parse spriteSheet.sprites section
|
|
174
|
+
# Format:
|
|
175
|
+
# spriteSheet:
|
|
176
|
+
# sprites:
|
|
177
|
+
# - name: sprite_0
|
|
178
|
+
# internalID: 123456789
|
|
179
|
+
sprite_sheet_match = re.search(
|
|
180
|
+
r"spriteSheet:\s*\n\s+.*?sprites:\s*((?:\s+-\s+.*?\n)+)",
|
|
181
|
+
content,
|
|
182
|
+
re.DOTALL,
|
|
183
|
+
)
|
|
184
|
+
if sprite_sheet_match and not sprites:
|
|
185
|
+
sprites_content = sprite_sheet_match.group(1)
|
|
186
|
+
# Parse each sprite entry
|
|
187
|
+
sprite_entry_pattern = re.compile(
|
|
188
|
+
r"-\s+.*?name:\s*(\S+).*?internalID:\s*(-?\d+)",
|
|
189
|
+
re.DOTALL,
|
|
190
|
+
)
|
|
191
|
+
for match in sprite_entry_pattern.finditer(sprites_content):
|
|
192
|
+
sprite_name = match.group(1).strip()
|
|
193
|
+
internal_id = int(match.group(2))
|
|
194
|
+
if sprite_name not in internal_id_table:
|
|
195
|
+
internal_id_table[sprite_name] = internal_id
|
|
196
|
+
sprites.append({"name": sprite_name, "internalID": internal_id})
|
|
197
|
+
|
|
198
|
+
return SpriteInfo(
|
|
199
|
+
guid=guid,
|
|
200
|
+
sprite_mode=sprite_mode,
|
|
201
|
+
sprites=sprites,
|
|
202
|
+
internal_id_table=internal_id_table,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_sprite_reference(
|
|
207
|
+
sprite_path: Path | str,
|
|
208
|
+
sub_sprite_name: str | None = None,
|
|
209
|
+
) -> SpriteReference | None:
|
|
210
|
+
"""Get a sprite reference with automatic fileID detection.
|
|
211
|
+
|
|
212
|
+
Automatically determines the correct fileID based on the sprite's
|
|
213
|
+
import mode (Single vs Multiple) by reading the .meta file.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
sprite_path: Path to the sprite image file (e.g., "Assets/Sprites/player.png")
|
|
217
|
+
sub_sprite_name: For Multiple mode sprites, the name of the specific sub-sprite.
|
|
218
|
+
If None and sprite is Multiple mode, uses the first sprite.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
SpriteReference with correct fileID, or None if sprite not found/invalid
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> ref = get_sprite_reference("Assets/Sprites/icon.png")
|
|
225
|
+
>>> print(ref.to_dict())
|
|
226
|
+
{'fileID': 21300000, 'guid': 'abc123...', 'type': 3}
|
|
227
|
+
|
|
228
|
+
>>> ref = get_sprite_reference("Assets/Sprites/atlas.png", "sprite_0")
|
|
229
|
+
>>> print(ref.to_dict())
|
|
230
|
+
{'fileID': 1234567890, 'guid': 'def456...', 'type': 3}
|
|
231
|
+
"""
|
|
232
|
+
sprite_path = Path(sprite_path)
|
|
233
|
+
meta_path = Path(str(sprite_path) + ".meta")
|
|
234
|
+
|
|
235
|
+
if not meta_path.is_file():
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
sprite_info = parse_sprite_meta(meta_path)
|
|
239
|
+
if not sprite_info:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
file_id = sprite_info.get_file_id(sub_sprite_name)
|
|
243
|
+
if file_id is None:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
return SpriteReference(
|
|
247
|
+
file_id=file_id,
|
|
248
|
+
guid=sprite_info.guid,
|
|
249
|
+
type=3,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_material_reference(
|
|
254
|
+
material_name_or_path: str | Path,
|
|
255
|
+
project_root: Path | None = None,
|
|
256
|
+
) -> MaterialReference | None:
|
|
257
|
+
"""Get a material reference for use in SpriteRenderer.
|
|
258
|
+
|
|
259
|
+
Supports:
|
|
260
|
+
- Well-known material names (e.g., "Sprite-Lit-Default")
|
|
261
|
+
- Custom material paths (e.g., "Assets/Materials/Custom.mat")
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
material_name_or_path: Either a well-known material name or path to .mat file
|
|
265
|
+
project_root: Unity project root (for resolving relative paths)
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
MaterialReference or None if not found
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
>>> ref = get_material_reference("Sprite-Lit-Default")
|
|
272
|
+
>>> print(ref.to_dict())
|
|
273
|
+
{'fileID': 2100000, 'guid': 'a97c105638bdf8b4a8650670310a4cd3', 'type': 2}
|
|
274
|
+
|
|
275
|
+
>>> ref = get_material_reference("Sprites-Default") # Built-in
|
|
276
|
+
>>> print(ref.to_dict())
|
|
277
|
+
{'fileID': 10754}
|
|
278
|
+
"""
|
|
279
|
+
material_str = str(material_name_or_path)
|
|
280
|
+
|
|
281
|
+
# Check for well-known materials
|
|
282
|
+
if material_str in KNOWN_MATERIALS:
|
|
283
|
+
guid = KNOWN_MATERIALS[material_str]
|
|
284
|
+
|
|
285
|
+
# Check if it's a built-in material (no GUID, just fileID)
|
|
286
|
+
if material_str in BUILTIN_MATERIAL_FILE_IDS:
|
|
287
|
+
return MaterialReference(
|
|
288
|
+
file_id=BUILTIN_MATERIAL_FILE_IDS[material_str],
|
|
289
|
+
guid=None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# External material with GUID
|
|
293
|
+
return MaterialReference(
|
|
294
|
+
file_id=2100000, # Standard material fileID
|
|
295
|
+
guid=guid,
|
|
296
|
+
type=2,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Try as a path
|
|
300
|
+
material_path = Path(material_str)
|
|
301
|
+
if project_root and not material_path.is_absolute():
|
|
302
|
+
material_path = project_root / material_path
|
|
303
|
+
|
|
304
|
+
meta_path = Path(str(material_path) + ".meta")
|
|
305
|
+
if not meta_path.is_file():
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# Extract GUID from meta file
|
|
309
|
+
try:
|
|
310
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
311
|
+
guid_match = META_GUID_PATTERN.search(content)
|
|
312
|
+
if guid_match:
|
|
313
|
+
return MaterialReference(
|
|
314
|
+
file_id=2100000,
|
|
315
|
+
guid=guid_match.group(1),
|
|
316
|
+
type=2,
|
|
317
|
+
)
|
|
318
|
+
except OSError:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def link_sprite_to_renderer(
|
|
325
|
+
doc: Any, # UnityYAMLDocument
|
|
326
|
+
component_file_id: int,
|
|
327
|
+
sprite_ref: SpriteReference,
|
|
328
|
+
material_ref: MaterialReference | None = None,
|
|
329
|
+
) -> bool:
|
|
330
|
+
"""Link a sprite to a SpriteRenderer component in a document.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
doc: UnityYAMLDocument containing the SpriteRenderer
|
|
334
|
+
component_file_id: fileID of the SpriteRenderer component
|
|
335
|
+
sprite_ref: Sprite reference to set
|
|
336
|
+
material_ref: Optional material reference to set
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
True if successful, False if component not found
|
|
340
|
+
"""
|
|
341
|
+
obj = doc.get_by_file_id(component_file_id)
|
|
342
|
+
if not obj:
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
content = obj.get_content()
|
|
346
|
+
if not content:
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
# Set sprite reference
|
|
350
|
+
content["m_Sprite"] = sprite_ref.to_dict()
|
|
351
|
+
|
|
352
|
+
# Set material if provided
|
|
353
|
+
if material_ref:
|
|
354
|
+
materials = content.get("m_Materials", [])
|
|
355
|
+
if materials:
|
|
356
|
+
materials[0] = material_ref.to_dict()
|
|
357
|
+
else:
|
|
358
|
+
content["m_Materials"] = [material_ref.to_dict()]
|
|
359
|
+
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def get_sprite_info(sprite_path: Path | str) -> SpriteInfo | None:
|
|
364
|
+
"""Get detailed information about a sprite from its meta file.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
sprite_path: Path to the sprite image file
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
SpriteInfo with sprite mode and sub-sprite details
|
|
371
|
+
"""
|
|
372
|
+
sprite_path = Path(sprite_path)
|
|
373
|
+
meta_path = Path(str(sprite_path) + ".meta")
|
|
374
|
+
|
|
375
|
+
if not meta_path.is_file():
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
return parse_sprite_meta(meta_path)
|