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/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)