azurpaint 1.0.0__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.
- azurpaint/__init__.py +6 -0
- azurpaint/classes/AABB.py +26 -0
- azurpaint/classes/AssetReader.py +259 -0
- azurpaint/classes/GameObject.py +284 -0
- azurpaint/classes/MeshImage.py +196 -0
- azurpaint/classes/RectTransform.py +32 -0
- azurpaint/classes/Vector2.py +228 -0
- azurpaint/classes/__init__.py +6 -0
- azurpaint/exception.py +28 -0
- azurpaint/lib/Inspector.py +282 -0
- azurpaint/lib/Tree.py +253 -0
- azurpaint/lib/__init__.py +2 -0
- azurpaint/main.py +134 -0
- azurpaint/types.py +13 -0
- azurpaint-1.0.0.dist-info/LICENSE +21 -0
- azurpaint-1.0.0.dist-info/METADATA +116 -0
- azurpaint-1.0.0.dist-info/RECORD +19 -0
- azurpaint-1.0.0.dist-info/WHEEL +5 -0
- azurpaint-1.0.0.dist-info/top_level.txt +1 -0
azurpaint/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .Vector2 import Vector2
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AABB:
|
|
7
|
+
center: Vector2
|
|
8
|
+
extend: Vector2
|
|
9
|
+
|
|
10
|
+
def __init__(self, center: Vector2, extend: Vector2) -> None:
|
|
11
|
+
self.center = center
|
|
12
|
+
self.extend = extend
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str:
|
|
16
|
+
return f"<{self.__class__.__name__} center={self.center.values()} extend={self.extend.values()}>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def padding(self) -> Vector2:
|
|
21
|
+
return self.center - self.extend
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def size(self) -> Vector2:
|
|
26
|
+
return self.center + self.extend
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, cast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from UnityPy import Environment, classes
|
|
6
|
+
from UnityPy.enums import ClassIDType
|
|
7
|
+
from UnityPy.files import ObjectReader
|
|
8
|
+
|
|
9
|
+
from ..types import PathLike
|
|
10
|
+
from ..exception import PrefabNotFound
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AssetReader:
|
|
14
|
+
"""
|
|
15
|
+
Read UnityAsset using UnityPy.
|
|
16
|
+
"""
|
|
17
|
+
path: Path
|
|
18
|
+
prefab: Path
|
|
19
|
+
environment: Environment
|
|
20
|
+
files: List[str] = []
|
|
21
|
+
face: Dict[str, int]
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: PathLike, prefab: PathLike) -> None:
|
|
24
|
+
self.face = {}
|
|
25
|
+
self.path = Path(path)
|
|
26
|
+
self.prefab = Path(prefab)
|
|
27
|
+
|
|
28
|
+
if not self.filepath.exists():
|
|
29
|
+
raise FileNotFoundError(f"File {self.filepath.as_posix()!r} not exists.")
|
|
30
|
+
|
|
31
|
+
self.environment = Environment(self.filepath.as_posix())
|
|
32
|
+
self.files.append(self.prefab.as_posix())
|
|
33
|
+
|
|
34
|
+
if not self.has_prefab:
|
|
35
|
+
raise PrefabNotFound(f"Prefab {self.prefab.as_posix()!r} not exists.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"<{self.__class__.__name__} prefab={self.prefab.as_posix()}>"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def root(self) -> classes.GameObject:
|
|
44
|
+
return self.get_object_by_path_id(next(iter(self.environment.container.values())).path_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def filepath(self) -> Path:
|
|
49
|
+
return Path(self.path, self.prefab)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def has_prefab(self) -> bool:
|
|
54
|
+
try:
|
|
55
|
+
return any(key.endswith('.prefab') for key in self.environment.container.keys())
|
|
56
|
+
except:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def cabs(self) -> List[str]:
|
|
62
|
+
return list(k for k in self.environment.cabs.keys() if not k.endswith('.ress'))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def dependencies(self) -> List[str]:
|
|
67
|
+
assets: classes.AssetBundle = self.get_object_by_path_id(1)
|
|
68
|
+
|
|
69
|
+
if not hasattr(assets, 'm_Dependencies'):
|
|
70
|
+
raise ValueError(f"Prefab {self.prefab.as_posix()} is missing 'AssetBundle.m_Dependencies'.")
|
|
71
|
+
|
|
72
|
+
return cast(List[str], assets.m_Dependencies)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_cabs(self, name: PathLike) -> List[str]:
|
|
76
|
+
environment = Environment(Path(name).as_posix())
|
|
77
|
+
return list(k for k in environment.cabs.keys() if not k.endswith('.ress'))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load(self, path: PathLike, is_face: bool = False) -> Path:
|
|
81
|
+
path = Path(path)
|
|
82
|
+
|
|
83
|
+
if not path.is_relative_to(self.path):
|
|
84
|
+
path = Path(self.path, path)
|
|
85
|
+
|
|
86
|
+
# is_dependencies is not used, it's a bit complicated with azurlane assets
|
|
87
|
+
# since some asset like paintingface is not an direct dependency for a lot ship
|
|
88
|
+
self.environment.load_file(path.as_posix())
|
|
89
|
+
self.files.append(path.relative_to(self.path).as_posix())
|
|
90
|
+
|
|
91
|
+
if is_face or path.is_relative_to(Path(self.path, 'paintingface')):
|
|
92
|
+
self.face = self._get_face(path)
|
|
93
|
+
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def loads(self, path: Union[PathLike, Iterable[PathLike]]) -> List[Path]:
|
|
98
|
+
if not isinstance(path, Iterable) or isinstance(path, str):
|
|
99
|
+
path = [path]
|
|
100
|
+
|
|
101
|
+
_loaded = []
|
|
102
|
+
|
|
103
|
+
for link in path:
|
|
104
|
+
_loaded.append(self.load(link))
|
|
105
|
+
|
|
106
|
+
return _loaded
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _gather_files(self, folders: List[str] = ['painting', 'paintings', 'paintingface']) -> List[str]:
|
|
110
|
+
"""
|
|
111
|
+
Get all files list that have same name as prefab in painting, paintings and paintingface
|
|
112
|
+
"""
|
|
113
|
+
files: List[str] = []
|
|
114
|
+
|
|
115
|
+
for folder in folders:
|
|
116
|
+
glob = Path(self.path, folder).glob(f"{self.prefab.name.split('_')[0]}*")
|
|
117
|
+
files.extend([file.relative_to(self.path).as_posix() for file in glob])
|
|
118
|
+
|
|
119
|
+
return files
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_face(self, path: Path) -> Dict[str, int]:
|
|
123
|
+
face = {}
|
|
124
|
+
|
|
125
|
+
for obj in cast(List[ObjectReader], self.environment.files[path.as_posix()].get_objects()):
|
|
126
|
+
if obj.type != ClassIDType.Sprite:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
obj = cast(classes.Sprite, obj.read())
|
|
130
|
+
face[obj.name] = obj.path_id
|
|
131
|
+
|
|
132
|
+
return {key: face[key] for key in sorted(face.keys())}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_face(self, force: bool = True) -> None:
|
|
137
|
+
if len(self.face):
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
files = self._gather_files(['paintingface'])
|
|
141
|
+
prefab_face = Path('paintingface', self.prefab.name).as_posix()
|
|
142
|
+
|
|
143
|
+
if prefab_face in files:
|
|
144
|
+
self.load(prefab_face, is_face=True)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if not force:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# face not found or not same as prefab name
|
|
151
|
+
# sometimes face have different name with underscore extension
|
|
152
|
+
# but still have same basename eg like _hx extension
|
|
153
|
+
# this is not perfect it may load wrong face
|
|
154
|
+
possible_name: List[str] = [prefab_face]
|
|
155
|
+
|
|
156
|
+
while '_' in prefab_face:
|
|
157
|
+
prefab_face = prefab_face.rsplit('_', 1)[0]
|
|
158
|
+
possible_name.append(prefab_face)
|
|
159
|
+
|
|
160
|
+
matches: List[str] = [file for file in files if file in possible_name]
|
|
161
|
+
|
|
162
|
+
if len(matches) == 0:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# when matches is more than one. probably the longest is more desired one
|
|
166
|
+
# since it splitted with underscore so we're prefer longer string
|
|
167
|
+
self.load(max(matches, key=len), is_face=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def find_dependencies(self) -> Tuple[Set[str], Set[str]]:
|
|
171
|
+
"""
|
|
172
|
+
Find all dependencies in loaded path
|
|
173
|
+
|
|
174
|
+
:returns: tuple[`result`, `missing_dependencies`]
|
|
175
|
+
"""
|
|
176
|
+
result: Set[str] = set()
|
|
177
|
+
dependencies: Set[str] = set(self.dependencies)
|
|
178
|
+
|
|
179
|
+
if not len(dependencies):
|
|
180
|
+
return result, dependencies
|
|
181
|
+
|
|
182
|
+
files: List[str] = self._gather_files()
|
|
183
|
+
|
|
184
|
+
# blank image used by asset to create canvas
|
|
185
|
+
files.append('painting/touming_tex')
|
|
186
|
+
|
|
187
|
+
for file in files:
|
|
188
|
+
if not len(dependencies):
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
for cab in self.get_cabs(Path(self.path, file)):
|
|
192
|
+
if cab in dependencies:
|
|
193
|
+
result.add(file)
|
|
194
|
+
dependencies.remove(cab)
|
|
195
|
+
|
|
196
|
+
return result, dependencies
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def load_dependencies(self, force_face_load: bool = False) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Automatically search for dependencies by name.
|
|
202
|
+
|
|
203
|
+
:params force_face_load: force load face with matching name
|
|
204
|
+
|
|
205
|
+
Warning
|
|
206
|
+
-------
|
|
207
|
+
This is still in experimental state, undefined behaviour may occurs.
|
|
208
|
+
"""
|
|
209
|
+
dependencies, _ = self.find_dependencies()
|
|
210
|
+
|
|
211
|
+
if not len(self.face):
|
|
212
|
+
self._load_face(force=force_face_load)
|
|
213
|
+
|
|
214
|
+
self.loads(dependencies)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_object_by_path_id(self, path_id: int) -> Any:
|
|
218
|
+
for obj in self.environment.objects:
|
|
219
|
+
if obj.path_id == path_id:
|
|
220
|
+
return cast(Any, obj.read(return_typetree_on_error=False))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_object_by_name(self, name: str, type: ClassIDType) -> Any:
|
|
224
|
+
for obj in self.environment.objects:
|
|
225
|
+
try:
|
|
226
|
+
if not hasattr(obj, 'name') or (obj.type != type):
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
obj = cast(Any, obj.read(return_typetree_on_error=False))
|
|
230
|
+
|
|
231
|
+
if hasattr(obj, 'name') and (str(obj.name) == str(name)):
|
|
232
|
+
return obj
|
|
233
|
+
|
|
234
|
+
except:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_component_from_object(
|
|
239
|
+
self,
|
|
240
|
+
gameobject: classes.GameObject,
|
|
241
|
+
types: Optional[Iterable[ClassIDType]] = None,
|
|
242
|
+
names: Optional[Set[str]] = None,
|
|
243
|
+
attributes: Optional[Set[str]] = None
|
|
244
|
+
) -> Any:
|
|
245
|
+
for component_pptr in cast(List[classes.PPtr], gameobject.m_Components):
|
|
246
|
+
if types and (not component_pptr.type in types):
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
component = self.get_object_by_path_id(component_pptr.path_id)
|
|
250
|
+
|
|
251
|
+
if names and (not component.name in names):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if attributes:
|
|
255
|
+
for attribute in attributes:
|
|
256
|
+
if hasattr(component, attribute):
|
|
257
|
+
return component
|
|
258
|
+
|
|
259
|
+
return component
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from typing import Generator, List, Optional, Union, cast
|
|
5
|
+
from UnityPy import classes
|
|
6
|
+
from UnityPy.enums import ClassIDType
|
|
7
|
+
|
|
8
|
+
from ..exception import \
|
|
9
|
+
MeshImageNotFound, MonoBehaviourNotFound, RectTransformNotFound, TransformNotFound
|
|
10
|
+
|
|
11
|
+
from .AssetReader import AssetReader
|
|
12
|
+
from .MeshImage import MeshImage
|
|
13
|
+
from .RectTransform import RectTransform
|
|
14
|
+
from .Vector2 import Vector2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GameObject:
|
|
19
|
+
reader: AssetReader
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
path_id: int
|
|
23
|
+
active: bool
|
|
24
|
+
parent: Optional[GameObject]
|
|
25
|
+
children: List[GameObject]
|
|
26
|
+
|
|
27
|
+
# Unity component
|
|
28
|
+
Transform: Union[classes.Transform, classes.RectTransform]
|
|
29
|
+
RectTransform: Optional[RectTransform]
|
|
30
|
+
MonoBehaviour: Optional[MeshImage]
|
|
31
|
+
|
|
32
|
+
# variable
|
|
33
|
+
scale: Vector2
|
|
34
|
+
size: Vector2
|
|
35
|
+
local_offset: Vector2
|
|
36
|
+
global_offset: Vector2
|
|
37
|
+
image: Optional[Image.Image]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
reader: AssetReader,
|
|
43
|
+
gameobject: classes.GameObject,
|
|
44
|
+
parent: Optional[GameObject] = None
|
|
45
|
+
) -> None:
|
|
46
|
+
self.reader = reader
|
|
47
|
+
|
|
48
|
+
self.name = gameobject.name
|
|
49
|
+
self.path_id = gameobject.path_id
|
|
50
|
+
self.active = bool(gameobject.m_IsActive)
|
|
51
|
+
self.parent = parent
|
|
52
|
+
self.children = []
|
|
53
|
+
|
|
54
|
+
# default value to prevent attr not found
|
|
55
|
+
self.RectTransform = None
|
|
56
|
+
self.MonoBehaviour = None
|
|
57
|
+
self.image = None
|
|
58
|
+
self.scale = Vector2.one()
|
|
59
|
+
self.size = Vector2.zero()
|
|
60
|
+
self.local_offset = Vector2.zero()
|
|
61
|
+
self.global_offset = Vector2.zero()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# this can be improved but im too lazy to change this
|
|
65
|
+
self.Transform = cast(Union[classes.RectTransform, classes.Transform],
|
|
66
|
+
self.reader.get_component_from_object(
|
|
67
|
+
gameobject=gameobject,
|
|
68
|
+
types=[ClassIDType.RectTransform, ClassIDType.Transform]
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not self.Transform:
|
|
73
|
+
# unexpected behaviour, since every component in unity
|
|
74
|
+
# should have an Transform or RectTransform by default
|
|
75
|
+
raise TransformNotFound(f"Transform not found in {self}.")
|
|
76
|
+
|
|
77
|
+
if self.Transform.type == ClassIDType.RectTransform:
|
|
78
|
+
self.RectTransform = RectTransform(cast(classes.RectTransform, self.Transform))
|
|
79
|
+
|
|
80
|
+
# Transform have scale too
|
|
81
|
+
self.scale = Vector2.from_value(self.Transform.m_LocalScale)
|
|
82
|
+
|
|
83
|
+
if self.parent and not self.parent.is_root:
|
|
84
|
+
self.scale *= self.parent.scale
|
|
85
|
+
|
|
86
|
+
if not (self.is_root or self.active):
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
self.MonoBehaviour = MeshImage(self.reader, gameobject=gameobject)
|
|
91
|
+
|
|
92
|
+
if self.is_root or self.active:
|
|
93
|
+
self.image = self.MonoBehaviour.image
|
|
94
|
+
|
|
95
|
+
if self.image:
|
|
96
|
+
self.size = Vector2.from_value(self.image.size)
|
|
97
|
+
|
|
98
|
+
except (MeshImageNotFound, MonoBehaviourNotFound):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
if not self.image:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if not self.RectTransform:
|
|
105
|
+
raise RectTransformNotFound(f"RectTransform not found in {self}.")
|
|
106
|
+
|
|
107
|
+
size_delta = self.RectTransform.size_delta
|
|
108
|
+
|
|
109
|
+
# root always use size_delta when active
|
|
110
|
+
# some root doesn't active like qiye_4 or kelifulan_4 it's gonna stretch the image
|
|
111
|
+
# resize only smaller image than size delta
|
|
112
|
+
# we handle image that larger in size delta in offset
|
|
113
|
+
if (self.is_root and self.active) or size_delta.is_bigger(self.size):
|
|
114
|
+
self.image = self.image.resize(size_delta.as_size(), Image.Resampling.LANCZOS)
|
|
115
|
+
self.size = Vector2.from_value(self.image.size)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def __repr__(self) -> str:
|
|
120
|
+
return f"<{self.__class__.__name__} name={self.name}>"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def root(self) -> GameObject:
|
|
125
|
+
root = self
|
|
126
|
+
|
|
127
|
+
while self.parent:
|
|
128
|
+
root = self.parent
|
|
129
|
+
|
|
130
|
+
return root
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def is_root(self) -> bool:
|
|
135
|
+
return bool(self.parent == None)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def change_face(self, path_id: int) -> bool:
|
|
139
|
+
if self.name != 'face':
|
|
140
|
+
gameobject = self.root.find_child('face')
|
|
141
|
+
|
|
142
|
+
# face should be available in all ship, even ship without face asset still have face gameobject
|
|
143
|
+
if not gameobject:
|
|
144
|
+
raise Exception(f"ERROR: {self.reader.prefab.as_posix()!r} <GameObject name=face> not found.")
|
|
145
|
+
|
|
146
|
+
return gameobject.change_face(path_id=path_id)
|
|
147
|
+
|
|
148
|
+
sprite: Optional[classes.Sprite] = self.reader.get_object_by_path_id(path_id=path_id)
|
|
149
|
+
|
|
150
|
+
if not sprite:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
texture2d: classes.Texture2D = self.reader.get_object_by_path_id(
|
|
154
|
+
path_id=sprite.m_RD.texture.path_id)
|
|
155
|
+
|
|
156
|
+
self.active = True
|
|
157
|
+
self.image = texture2d.image
|
|
158
|
+
self.size = Vector2.from_value(self.image.size)
|
|
159
|
+
|
|
160
|
+
if not self.RectTransform:
|
|
161
|
+
raise RectTransformNotFound(f"RectTransform not found in {self}.")
|
|
162
|
+
|
|
163
|
+
size_delta = self.RectTransform.size_delta
|
|
164
|
+
|
|
165
|
+
if size_delta.is_bigger(self.size):
|
|
166
|
+
self.image = self.image.resize(size_delta.as_size(), Image.Resampling.LANCZOS)
|
|
167
|
+
self.size = Vector2.from_value(self.image.size)
|
|
168
|
+
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def find_child(self, name: str) -> Optional[GameObject]:
|
|
173
|
+
for child in self.children:
|
|
174
|
+
if child.name == name:
|
|
175
|
+
return child
|
|
176
|
+
|
|
177
|
+
from_child = child.find_child(name)
|
|
178
|
+
|
|
179
|
+
if from_child:
|
|
180
|
+
return from_child
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def retrieve_children(self, recursive: bool = True) -> bool:
|
|
184
|
+
if not self.Transform:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
for child in self.Transform.m_Children:
|
|
188
|
+
child_transform = self.reader.get_object_by_path_id(child.path_id)
|
|
189
|
+
child_object = self.reader.get_object_by_path_id(child_transform.m_GameObject.path_id)
|
|
190
|
+
object_layer = GameObject(reader=self.reader, gameobject=child_object, parent=self)
|
|
191
|
+
|
|
192
|
+
if recursive:
|
|
193
|
+
object_layer.retrieve_children(recursive=recursive)
|
|
194
|
+
self.children.append(object_layer)
|
|
195
|
+
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# todo: well move this method into RectTransform?
|
|
200
|
+
def calculate_local_offset(self, recursive: bool = True) -> Vector2:
|
|
201
|
+
if self.RectTransform:
|
|
202
|
+
anchor_min = self.RectTransform.anchor_min
|
|
203
|
+
anchor_max = self.RectTransform.anchor_max
|
|
204
|
+
anchor_pos = self.RectTransform.anchor_pos
|
|
205
|
+
size_delta = self.RectTransform.size_delta
|
|
206
|
+
pivot = self.RectTransform.pivot
|
|
207
|
+
|
|
208
|
+
if anchor_min == anchor_max:
|
|
209
|
+
if self.parent:
|
|
210
|
+
pivot = Vector2(x=pivot.x, y=1 - pivot.y)
|
|
211
|
+
|
|
212
|
+
pivot_offset = (pivot * size_delta) + (0, self.size.y - size_delta.y)
|
|
213
|
+
pivot_anchor = self.scale * pivot_offset
|
|
214
|
+
|
|
215
|
+
anchor_offset = Vector2(x=anchor_pos.x - pivot_anchor.x, y=anchor_pos.y + pivot_anchor.y)
|
|
216
|
+
self.local_offset = (self.parent.size * anchor_min) + (anchor_offset.x, -anchor_offset.y)
|
|
217
|
+
|
|
218
|
+
# resize when local_offset already calculated to prevent miss calculation
|
|
219
|
+
# self.size is the truesize, so size not updated here to make debug easier
|
|
220
|
+
if self.image:
|
|
221
|
+
self.image = self.image.resize(
|
|
222
|
+
(self.size * self.scale).as_size(), Image.Resampling.LANCZOS)
|
|
223
|
+
|
|
224
|
+
else:
|
|
225
|
+
if self.parent:
|
|
226
|
+
self.size = self.parent.size * (anchor_min - anchor_max).apply(abs)
|
|
227
|
+
|
|
228
|
+
self.local_offset = Vector2(x=-anchor_pos.x, y=anchor_pos.y)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if recursive:
|
|
232
|
+
for child in self.children:
|
|
233
|
+
child.calculate_local_offset(recursive=recursive)
|
|
234
|
+
|
|
235
|
+
return self.local_offset
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_smallest_offset(self) -> Vector2:
|
|
239
|
+
min_offset = Vector2.zero()
|
|
240
|
+
|
|
241
|
+
if self.image:
|
|
242
|
+
min_offset = self.local_offset
|
|
243
|
+
|
|
244
|
+
for child in self.children:
|
|
245
|
+
child_offset = child.get_smallest_offset()
|
|
246
|
+
min_offset = Vector2.min(min_offset, child_offset)
|
|
247
|
+
|
|
248
|
+
return min_offset
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def calculate_global_offset(self, offset: Optional[Vector2] = None) -> Vector2:
|
|
252
|
+
offset = offset or self.get_smallest_offset()
|
|
253
|
+
|
|
254
|
+
self.global_offset = self.local_offset - offset
|
|
255
|
+
|
|
256
|
+
for child in self.children:
|
|
257
|
+
child.calculate_global_offset(offset=self.global_offset)
|
|
258
|
+
|
|
259
|
+
return self.global_offset
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_biggset_size(self) -> Vector2:
|
|
263
|
+
size_offset = Vector2.zero()
|
|
264
|
+
|
|
265
|
+
if self.image:
|
|
266
|
+
# we doesn't want to calculate root scale
|
|
267
|
+
# dev sometimes put root scale into unreasonable value like 90x
|
|
268
|
+
scale = Vector2.one() if self.is_root else self.scale
|
|
269
|
+
size_offset = self.global_offset + (self.size * scale)
|
|
270
|
+
|
|
271
|
+
for child in self.children:
|
|
272
|
+
child_offset = child.get_biggset_size()
|
|
273
|
+
size_offset = Vector2.max(size_offset, child_offset)
|
|
274
|
+
|
|
275
|
+
return size_offset
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def yield_layers(self) -> Generator[GameObject, None, None]:
|
|
279
|
+
if self.image:
|
|
280
|
+
yield self
|
|
281
|
+
|
|
282
|
+
for child in self.children:
|
|
283
|
+
for layer in child.yield_layers():
|
|
284
|
+
yield layer
|