sonolus.py 0.7.1__py3-none-any.whl → 0.9.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.
Potentially problematic release.
This version of sonolus.py might be problematic. Click here for more details.
- sonolus/backend/ir.py +32 -2
- sonolus/backend/optimize/copy_coalesce.py +5 -1
- sonolus/backend/optimize/flow.py +14 -0
- sonolus/backend/optimize/inlining.py +1 -1
- sonolus/backend/optimize/liveness.py +6 -4
- sonolus/backend/visitor.py +32 -14
- sonolus/build/cli.py +18 -50
- sonolus/build/collection.py +2 -0
- sonolus/build/compile.py +49 -5
- sonolus/build/dev_server.py +222 -0
- sonolus/build/engine.py +23 -2
- sonolus/build/project.py +13 -4
- sonolus/script/array.py +2 -1
- sonolus/script/bucket.py +11 -9
- sonolus/script/debug.py +52 -5
- sonolus/script/effect.py +66 -9
- sonolus/script/internal/impl.py +8 -4
- sonolus/script/interval.py +17 -6
- sonolus/script/num.py +10 -6
- sonolus/script/particle.py +68 -9
- sonolus/script/project.py +15 -2
- sonolus/script/quad.py +95 -2
- sonolus/script/record.py +9 -0
- sonolus/script/runtime.py +4 -3
- sonolus/script/sprite.py +73 -10
- sonolus/script/vec.py +31 -14
- {sonolus_py-0.7.1.dist-info → sonolus_py-0.9.0.dist-info}/METADATA +1 -1
- {sonolus_py-0.7.1.dist-info → sonolus_py-0.9.0.dist-info}/RECORD +31 -30
- {sonolus_py-0.7.1.dist-info → sonolus_py-0.9.0.dist-info}/WHEEL +0 -0
- {sonolus_py-0.7.1.dist-info → sonolus_py-0.9.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.7.1.dist-info → sonolus_py-0.9.0.dist-info}/licenses/LICENSE +0 -0
sonolus/script/particle.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Annotated, Any, NewType, dataclass_transform, get_origin
|
|
5
6
|
|
|
6
7
|
from sonolus.backend.ops import Op
|
|
8
|
+
from sonolus.script.array_like import ArrayLike
|
|
9
|
+
from sonolus.script.debug import static_error
|
|
7
10
|
from sonolus.script.internal.introspection import get_field_specifiers
|
|
8
11
|
from sonolus.script.internal.native import native_function
|
|
9
12
|
from sonolus.script.quad import QuadLike, flatten_quad
|
|
@@ -52,6 +55,29 @@ class ParticleHandle(Record):
|
|
|
52
55
|
_destroy_particle_effect(self.id)
|
|
53
56
|
|
|
54
57
|
|
|
58
|
+
class ParticleGroup(Record, ArrayLike[Particle]):
|
|
59
|
+
"""A group of particle effects.
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
```python
|
|
63
|
+
ParticleGroup(start_id: int, size: int)
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
start_id: int
|
|
68
|
+
size: int
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
return self.size
|
|
72
|
+
|
|
73
|
+
def __getitem__(self, index: int) -> Particle:
|
|
74
|
+
assert 0 <= index < self.size
|
|
75
|
+
return Particle(self.start_id + index)
|
|
76
|
+
|
|
77
|
+
def __setitem__(self, index: int, value: Particle) -> None:
|
|
78
|
+
static_error("ParticleGroup is read-only")
|
|
79
|
+
|
|
80
|
+
|
|
55
81
|
@native_function(Op.HasParticleEffect)
|
|
56
82
|
def _has_particle_effect(particle_id: int) -> bool:
|
|
57
83
|
raise NotImplementedError
|
|
@@ -91,11 +117,21 @@ class _ParticleInfo:
|
|
|
91
117
|
name: str
|
|
92
118
|
|
|
93
119
|
|
|
120
|
+
@dataclass
|
|
121
|
+
class _ParticleGroupInfo:
|
|
122
|
+
names: list[str]
|
|
123
|
+
|
|
124
|
+
|
|
94
125
|
def particle(name: str) -> Any:
|
|
95
126
|
"""Define a particle with the given name."""
|
|
96
127
|
return _ParticleInfo(name)
|
|
97
128
|
|
|
98
129
|
|
|
130
|
+
def particle_group(names: Iterable[str]) -> Any:
|
|
131
|
+
"""Define a particle group with the given names."""
|
|
132
|
+
return _ParticleGroupInfo(list(names))
|
|
133
|
+
|
|
134
|
+
|
|
99
135
|
type Particles = NewType("Particles", Any) # type: ignore
|
|
100
136
|
|
|
101
137
|
|
|
@@ -109,26 +145,49 @@ def particles[T](cls: type[T]) -> T | Particles:
|
|
|
109
145
|
class Particles:
|
|
110
146
|
tap: StandardParticle.NOTE_CIRCULAR_TAP_RED
|
|
111
147
|
other: Particle = particle("other")
|
|
148
|
+
group_1: ParticleGroup = particle_group(["one", "two", "three"])
|
|
149
|
+
group_2: ParticleGroup = particle_group(f"name_{i}" for i in range(10))
|
|
112
150
|
```
|
|
113
151
|
"""
|
|
114
152
|
if len(cls.__bases__) != 1:
|
|
115
153
|
raise ValueError("Particles class must not inherit from any class (except object)")
|
|
116
154
|
instance = cls()
|
|
117
155
|
names = []
|
|
118
|
-
|
|
156
|
+
i = 0
|
|
157
|
+
for name, annotation in get_field_specifiers(cls).items():
|
|
119
158
|
if get_origin(annotation) is not Annotated:
|
|
120
|
-
raise TypeError(f"Invalid annotation for particles: {annotation}")
|
|
159
|
+
raise TypeError(f"Invalid annotation for particles: {annotation} on field {name}")
|
|
121
160
|
annotation_type = annotation.__args__[0]
|
|
122
161
|
annotation_values = annotation.__metadata__
|
|
123
|
-
if
|
|
124
|
-
raise TypeError(f"Invalid annotation for particles: {annotation}, expected annotation of type Particle")
|
|
125
|
-
if len(annotation_values) != 1 or not isinstance(annotation_values[0], _ParticleInfo):
|
|
162
|
+
if len(annotation_values) != 1:
|
|
126
163
|
raise TypeError(
|
|
127
|
-
f"Invalid annotation for particles: {annotation}
|
|
164
|
+
f"Invalid annotation for particles: {annotation} on field {name}, too many annotation values"
|
|
128
165
|
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
particle_info = annotation_values[0]
|
|
167
|
+
match particle_info:
|
|
168
|
+
case _ParticleInfo(name=particle_name):
|
|
169
|
+
if annotation_type is not Particle:
|
|
170
|
+
raise TypeError(
|
|
171
|
+
f"Invalid annotation for particles: {annotation} on field {name}, expected Particle"
|
|
172
|
+
)
|
|
173
|
+
names.append(particle_name)
|
|
174
|
+
setattr(instance, name, Particle(i))
|
|
175
|
+
i += 1
|
|
176
|
+
case _ParticleGroupInfo(names=particle_names):
|
|
177
|
+
if annotation_type is not ParticleGroup:
|
|
178
|
+
raise TypeError(
|
|
179
|
+
f"Invalid annotation for particles: {annotation} on field {name}, expected ParticleGroup"
|
|
180
|
+
)
|
|
181
|
+
start_id = i
|
|
182
|
+
count = len(particle_names)
|
|
183
|
+
names.extend(particle_names)
|
|
184
|
+
setattr(instance, name, ParticleGroup(start_id, count))
|
|
185
|
+
i += count
|
|
186
|
+
case _:
|
|
187
|
+
raise TypeError(
|
|
188
|
+
f"Invalid annotation for particles: {annotation} on field {name}, unknown particle info, "
|
|
189
|
+
f"expected a particle() or particle_group() specifier"
|
|
190
|
+
)
|
|
132
191
|
instance._particles_ = names
|
|
133
192
|
instance._is_comptime_value_ = True
|
|
134
193
|
return instance
|
sonolus/script/project.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import ClassVar, TypedDict
|
|
|
8
8
|
|
|
9
9
|
from sonolus.backend.optimize import optimize
|
|
10
10
|
from sonolus.backend.optimize.passes import CompilerPass
|
|
11
|
+
from sonolus.build.compile import CompileCache
|
|
11
12
|
from sonolus.script.archetype import ArchetypeSchema
|
|
12
13
|
from sonolus.script.engine import Engine
|
|
13
14
|
from sonolus.script.level import ExternalLevelData, Level, LevelData
|
|
@@ -65,8 +66,20 @@ class Project:
|
|
|
65
66
|
"""
|
|
66
67
|
from sonolus.build.cli import build_collection, run_server
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
if config is None:
|
|
70
|
+
config = BuildConfig()
|
|
71
|
+
|
|
72
|
+
cache = CompileCache()
|
|
73
|
+
build_collection(self, Path(build_dir), config, cache=cache)
|
|
74
|
+
run_server(
|
|
75
|
+
Path(build_dir) / "site",
|
|
76
|
+
port=port,
|
|
77
|
+
project_module_name=None,
|
|
78
|
+
core_module_names=None,
|
|
79
|
+
build_dir=Path(build_dir),
|
|
80
|
+
config=config,
|
|
81
|
+
cache=cache,
|
|
82
|
+
)
|
|
70
83
|
|
|
71
84
|
def build(self, build_dir: PathLike, config: BuildConfig | None = None):
|
|
72
85
|
"""Build the project.
|
sonolus/script/quad.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Protocol
|
|
3
|
+
from typing import Protocol, Self, assert_never, overload
|
|
4
4
|
|
|
5
|
+
from sonolus.script.internal.impl import perf_meta_fn
|
|
5
6
|
from sonolus.script.record import Record
|
|
6
7
|
from sonolus.script.values import zeros
|
|
7
8
|
from sonolus.script.vec import Vec2, pnpoly
|
|
@@ -57,6 +58,26 @@ class Quad(Record):
|
|
|
57
58
|
"""The center of the quad."""
|
|
58
59
|
return (self.bl + self.tr + self.tl + self.br) / 4
|
|
59
60
|
|
|
61
|
+
@property
|
|
62
|
+
def mt(self) -> Vec2:
|
|
63
|
+
"""The midpoint of the top edge of the quad."""
|
|
64
|
+
return (self.tl + self.tr) / 2
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def mr(self) -> Vec2:
|
|
68
|
+
"""The midpoint of the right edge of the quad."""
|
|
69
|
+
return (self.tr + self.br) / 2
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def mb(self) -> Vec2:
|
|
73
|
+
"""The midpoint of the bottom edge of the quad."""
|
|
74
|
+
return (self.bl + self.br) / 2
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def ml(self) -> Vec2:
|
|
78
|
+
"""The midpoint of the left edge of the quad."""
|
|
79
|
+
return (self.bl + self.tl) / 2
|
|
80
|
+
|
|
60
81
|
def translate(self, translation: Vec2, /) -> Quad:
|
|
61
82
|
"""Translate the quad by the given translation and return a new quad."""
|
|
62
83
|
return Quad(
|
|
@@ -212,7 +233,7 @@ class Rect(Record):
|
|
|
212
233
|
b: float
|
|
213
234
|
"""The bottom edge of the rectangle."""
|
|
214
235
|
|
|
215
|
-
l: float
|
|
236
|
+
l: float
|
|
216
237
|
"""The left edge of the rectangle."""
|
|
217
238
|
|
|
218
239
|
@classmethod
|
|
@@ -225,6 +246,57 @@ class Rect(Record):
|
|
|
225
246
|
l=center.x - dimensions.x / 2,
|
|
226
247
|
)
|
|
227
248
|
|
|
249
|
+
@overload
|
|
250
|
+
@classmethod
|
|
251
|
+
def from_margin(cls, trbl: float, /) -> Rect: ...
|
|
252
|
+
|
|
253
|
+
@overload
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_margin(cls, tb: float, lr: float, /) -> Rect: ...
|
|
256
|
+
|
|
257
|
+
@overload
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_margin(cls, t: float, lr: float, b: float, /) -> Rect: ...
|
|
260
|
+
|
|
261
|
+
@overload
|
|
262
|
+
@classmethod
|
|
263
|
+
def from_margin(cls, t: float, r: float, b: float, l: float, /) -> Rect: ...
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def from_margin(cls, a: float, b: float | None = None, c: float | None = None, d: float | None = None, /) -> Self:
|
|
267
|
+
"""Create a rectangle based on margins (edge distances) from the origin.
|
|
268
|
+
|
|
269
|
+
Compared to the regular [`Rect`][sonolus.script.quad.Rect] constructor, this method negates the bottom and
|
|
270
|
+
left values, and supports shorthands when fewer than four arguments are provided.
|
|
271
|
+
|
|
272
|
+
The following signatures are supported:
|
|
273
|
+
|
|
274
|
+
- `from_margin(trbl)`: All margins set to `trbl`.
|
|
275
|
+
- `from_margin(tb, lr)`: Top and bottom margins set to `tb`, left and right margins set to `lr`.
|
|
276
|
+
- `from_margin(t, lr, b)`: Top margin set to `t`, left and right margins set to `lr`, bottom margin set to `b`.
|
|
277
|
+
- `from_margin(t, r, b, l)`: Top, right, bottom, and left margins set to `t`, `r`, `b`, and `l` respectively.
|
|
278
|
+
|
|
279
|
+
Usage:
|
|
280
|
+
```python
|
|
281
|
+
Rect.from_margin(1) # Rect(t=1, r=1, b=-1, l=-1)
|
|
282
|
+
Rect.from_margin(1, 2) # Rect(t=1, r=2, b=-1, l=-2)
|
|
283
|
+
Rect.from_margin(1, 2, 3) # Rect(t=1, r=2, b=-3, l=-2)
|
|
284
|
+
Rect.from_margin(1, 2, 3, 4) # Rect(t=1, r=2, b=-3, l=-4)
|
|
285
|
+
```
|
|
286
|
+
"""
|
|
287
|
+
args = (a, b, c, d)
|
|
288
|
+
match args:
|
|
289
|
+
case (a, None, None, None):
|
|
290
|
+
return cls(t=a, r=a, b=-a, l=-a)
|
|
291
|
+
case (a, b, None, None):
|
|
292
|
+
return cls(t=a, r=b, b=-a, l=-b)
|
|
293
|
+
case (a, b, c, None):
|
|
294
|
+
return cls(t=a, r=b, b=-c, l=-b)
|
|
295
|
+
case (a, b, c, d):
|
|
296
|
+
return cls(t=a, r=b, b=-c, l=-d)
|
|
297
|
+
case _:
|
|
298
|
+
assert_never(args)
|
|
299
|
+
|
|
228
300
|
@property
|
|
229
301
|
def w(self) -> float:
|
|
230
302
|
"""The width of the rectangle."""
|
|
@@ -260,6 +332,26 @@ class Rect(Record):
|
|
|
260
332
|
"""The center of the rectangle."""
|
|
261
333
|
return Vec2((self.l + self.r) / 2, (self.t + self.b) / 2)
|
|
262
334
|
|
|
335
|
+
@property
|
|
336
|
+
def mt(self) -> Vec2:
|
|
337
|
+
"""The middle-top point of the rectangle."""
|
|
338
|
+
return Vec2((self.l + self.r) / 2, self.t)
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def mr(self) -> Vec2:
|
|
342
|
+
"""The middle-right point of the rectangle."""
|
|
343
|
+
return Vec2(self.r, (self.t + self.b) / 2)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def mb(self) -> Vec2:
|
|
347
|
+
"""The middle-bottom point of the rectangle."""
|
|
348
|
+
return Vec2((self.l + self.r) / 2, self.b)
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def ml(self) -> Vec2:
|
|
352
|
+
"""The middle-left point of the rectangle."""
|
|
353
|
+
return Vec2(self.l, (self.t + self.b) / 2)
|
|
354
|
+
|
|
263
355
|
def as_quad(self) -> Quad:
|
|
264
356
|
"""Convert the rectangle to a [`Quad`][sonolus.script.quad.Quad]."""
|
|
265
357
|
return Quad(
|
|
@@ -360,6 +452,7 @@ type QuadLike = _QuadLike | Quad
|
|
|
360
452
|
"""A type that can be used as a quad."""
|
|
361
453
|
|
|
362
454
|
|
|
455
|
+
@perf_meta_fn
|
|
363
456
|
def flatten_quad(quad: QuadLike) -> tuple[float, float, float, float, float, float, float, float]:
|
|
364
457
|
bl = quad.bl
|
|
365
458
|
tl = quad.tl
|
sonolus/script/record.py
CHANGED
|
@@ -171,6 +171,15 @@ class Record(GenericValue, metaclass=RecordMeta):
|
|
|
171
171
|
result._value_ = kwargs
|
|
172
172
|
return result
|
|
173
173
|
|
|
174
|
+
@classmethod
|
|
175
|
+
def _quick_construct(cls, **kwargs) -> Self:
|
|
176
|
+
result = object.__new__(cls)
|
|
177
|
+
for k, v in kwargs.items():
|
|
178
|
+
if isinstance(v, int | float):
|
|
179
|
+
kwargs[k] = Num._accept_(v)
|
|
180
|
+
result._value_ = kwargs
|
|
181
|
+
return result
|
|
182
|
+
|
|
174
183
|
@classmethod
|
|
175
184
|
def _size_(cls) -> int:
|
|
176
185
|
return sum(field.type._size_() for field in cls._fields_)
|
sonolus/script/runtime.py
CHANGED
|
@@ -30,7 +30,7 @@ from sonolus.script.globals import (
|
|
|
30
30
|
_watch_runtime_update,
|
|
31
31
|
)
|
|
32
32
|
from sonolus.script.internal.context import ctx
|
|
33
|
-
from sonolus.script.internal.impl import meta_fn
|
|
33
|
+
from sonolus.script.internal.impl import meta_fn, perf_meta_fn
|
|
34
34
|
from sonolus.script.num import Num
|
|
35
35
|
from sonolus.script.quad import Quad, Rect
|
|
36
36
|
from sonolus.script.record import Record
|
|
@@ -992,7 +992,7 @@ def time() -> float:
|
|
|
992
992
|
def offset_adjusted_time() -> float:
|
|
993
993
|
"""Get the current time of the game adjusted by the input offset.
|
|
994
994
|
|
|
995
|
-
Returns 0 in preview mode and tutorial mode.
|
|
995
|
+
Returns 0 in preview mode and the current time without adjustment in tutorial mode.
|
|
996
996
|
"""
|
|
997
997
|
if not ctx():
|
|
998
998
|
return 0
|
|
@@ -1154,9 +1154,10 @@ def canvas() -> _PreviewRuntimeCanvas:
|
|
|
1154
1154
|
return _PreviewRuntimeCanvas # type: ignore
|
|
1155
1155
|
|
|
1156
1156
|
|
|
1157
|
+
@perf_meta_fn
|
|
1157
1158
|
def screen() -> Rect:
|
|
1158
1159
|
"""Get the screen boundaries as a rectangle."""
|
|
1159
|
-
return Rect(t=1, r=aspect_ratio(), b=-1, l=-aspect_ratio())
|
|
1160
|
+
return Rect._quick_construct(t=1, r=aspect_ratio(), b=-1, l=-aspect_ratio())
|
|
1160
1161
|
|
|
1161
1162
|
|
|
1162
1163
|
def level_score() -> _LevelScore:
|
sonolus/script/sprite.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from enum import StrEnum
|
|
3
4
|
from typing import Annotated, Any, NewType, dataclass_transform, get_origin
|
|
4
5
|
|
|
5
6
|
from sonolus.backend.ops import Op
|
|
7
|
+
from sonolus.script.array_like import ArrayLike
|
|
8
|
+
from sonolus.script.debug import static_error
|
|
9
|
+
from sonolus.script.internal.impl import perf_meta_fn
|
|
6
10
|
from sonolus.script.internal.introspection import get_field_specifiers
|
|
7
11
|
from sonolus.script.internal.native import native_function
|
|
8
12
|
from sonolus.script.quad import QuadLike, flatten_quad
|
|
@@ -26,6 +30,7 @@ class Sprite(Record):
|
|
|
26
30
|
"""Check if the sprite is available."""
|
|
27
31
|
return _has_skin_sprite(self.id)
|
|
28
32
|
|
|
33
|
+
@perf_meta_fn
|
|
29
34
|
def draw(self, quad: QuadLike, z: float = 0.0, a: float = 1.0):
|
|
30
35
|
"""Draw the sprite.
|
|
31
36
|
|
|
@@ -36,6 +41,7 @@ class Sprite(Record):
|
|
|
36
41
|
"""
|
|
37
42
|
_draw(self.id, *flatten_quad(quad), z, a)
|
|
38
43
|
|
|
44
|
+
@perf_meta_fn
|
|
39
45
|
def draw_curved_b(self, quad: QuadLike, cp: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
40
46
|
"""Draw the sprite with a curved bottom with a quadratic Bézier curve.
|
|
41
47
|
|
|
@@ -48,6 +54,7 @@ class Sprite(Record):
|
|
|
48
54
|
"""
|
|
49
55
|
_draw_curved_b(self.id, *flatten_quad(quad), z, a, n, *cp.tuple)
|
|
50
56
|
|
|
57
|
+
@perf_meta_fn
|
|
51
58
|
def draw_curved_t(self, quad: QuadLike, cp: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
52
59
|
"""Draw the sprite with a curved top with a quadratic Bézier curve.
|
|
53
60
|
|
|
@@ -60,6 +67,7 @@ class Sprite(Record):
|
|
|
60
67
|
"""
|
|
61
68
|
_draw_curved_t(self.id, *flatten_quad(quad), z, a, n, *cp.tuple)
|
|
62
69
|
|
|
70
|
+
@perf_meta_fn
|
|
63
71
|
def draw_curved_l(self, quad: QuadLike, cp: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
64
72
|
"""Draw the sprite with a curved left side with a quadratic Bézier curve.
|
|
65
73
|
|
|
@@ -72,6 +80,7 @@ class Sprite(Record):
|
|
|
72
80
|
"""
|
|
73
81
|
_draw_curved_l(self.id, *flatten_quad(quad), z, a, n, *cp.tuple)
|
|
74
82
|
|
|
83
|
+
@perf_meta_fn
|
|
75
84
|
def draw_curved_r(self, quad: QuadLike, cp: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
76
85
|
"""Draw the sprite with a curved right side with a quadratic Bézier curve.
|
|
77
86
|
|
|
@@ -84,6 +93,7 @@ class Sprite(Record):
|
|
|
84
93
|
"""
|
|
85
94
|
_draw_curved_r(self.id, *flatten_quad(quad), z, a, n, *cp.tuple)
|
|
86
95
|
|
|
96
|
+
@perf_meta_fn
|
|
87
97
|
def draw_curved_bt(self, quad: QuadLike, cp1: Vec2, cp2: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
88
98
|
"""Draw the sprite with a curved bottom and top with a cubic Bézier curve.
|
|
89
99
|
|
|
@@ -97,6 +107,7 @@ class Sprite(Record):
|
|
|
97
107
|
"""
|
|
98
108
|
_draw_curved_bt(self.id, *flatten_quad(quad), z, a, n, *cp1.tuple, *cp2.tuple)
|
|
99
109
|
|
|
110
|
+
@perf_meta_fn
|
|
100
111
|
def draw_curved_lr(self, quad: QuadLike, cp1: Vec2, cp2: Vec2, n: float, z: float = 0.0, a: float = 1.0):
|
|
101
112
|
"""Draw the sprite with a curved left and right side with a cubic Bézier curve.
|
|
102
113
|
|
|
@@ -111,6 +122,29 @@ class Sprite(Record):
|
|
|
111
122
|
_draw_curved_lr(self.id, *flatten_quad(quad), z, a, n, *cp1.tuple, *cp2.tuple)
|
|
112
123
|
|
|
113
124
|
|
|
125
|
+
class SpriteGroup(Record, ArrayLike[Sprite]):
|
|
126
|
+
"""A group of sprites.
|
|
127
|
+
|
|
128
|
+
Usage:
|
|
129
|
+
```python
|
|
130
|
+
SpriteGroup(start_id: int, size: int)
|
|
131
|
+
```
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
start_id: int
|
|
135
|
+
size: int
|
|
136
|
+
|
|
137
|
+
def __len__(self) -> int:
|
|
138
|
+
return self.size
|
|
139
|
+
|
|
140
|
+
def __getitem__(self, index: int) -> Sprite:
|
|
141
|
+
assert 0 <= index < self.size
|
|
142
|
+
return Sprite(self.start_id + index)
|
|
143
|
+
|
|
144
|
+
def __setitem__(self, index: int, value: Sprite) -> None:
|
|
145
|
+
static_error("SpriteGroup is read-only")
|
|
146
|
+
|
|
147
|
+
|
|
114
148
|
@native_function(Op.HasSkinSprite)
|
|
115
149
|
def _has_skin_sprite(sprite_id: int) -> bool:
|
|
116
150
|
raise NotImplementedError
|
|
@@ -262,11 +296,21 @@ class SkinSprite:
|
|
|
262
296
|
name: str
|
|
263
297
|
|
|
264
298
|
|
|
299
|
+
@dataclass
|
|
300
|
+
class SkinSpriteGroup:
|
|
301
|
+
names: list[str]
|
|
302
|
+
|
|
303
|
+
|
|
265
304
|
def sprite(name: str) -> Any:
|
|
266
305
|
"""Define a sprite with the given name."""
|
|
267
306
|
return SkinSprite(name)
|
|
268
307
|
|
|
269
308
|
|
|
309
|
+
def sprite_group(names: Iterable[str]) -> Any:
|
|
310
|
+
"""Define a sprite group with the given names."""
|
|
311
|
+
return SkinSpriteGroup(list(names))
|
|
312
|
+
|
|
313
|
+
|
|
270
314
|
type Skin = NewType("Skin", Any) # type: ignore
|
|
271
315
|
|
|
272
316
|
|
|
@@ -294,25 +338,44 @@ def skin[T](cls: type[T]) -> T | Skin:
|
|
|
294
338
|
render_mode: RenderMode = RenderMode.LIGHTWEIGHT
|
|
295
339
|
|
|
296
340
|
note: StandardSprite.NOTE_HEAD_RED
|
|
297
|
-
other: Sprite =
|
|
341
|
+
other: Sprite = sprite("other")
|
|
342
|
+
group_1: SpriteGroup = sprite_group(["one", "two", "three"])
|
|
343
|
+
group_2: SpriteGroup = sprite_group(f"name_{i}" for i in range(10))
|
|
298
344
|
```
|
|
299
345
|
"""
|
|
300
346
|
if len(cls.__bases__) != 1:
|
|
301
347
|
raise ValueError("Skin class must not inherit from any class (except object)")
|
|
302
348
|
instance = cls()
|
|
303
349
|
names = []
|
|
304
|
-
|
|
350
|
+
i = 0
|
|
351
|
+
for name, annotation in get_field_specifiers(cls, skip={"render_mode"}).items():
|
|
305
352
|
if get_origin(annotation) is not Annotated:
|
|
306
|
-
raise TypeError(f"Invalid annotation for skin: {annotation}")
|
|
353
|
+
raise TypeError(f"Invalid annotation for skin: {annotation} on field {name}")
|
|
307
354
|
annotation_type = annotation.__args__[0]
|
|
308
355
|
annotation_values = annotation.__metadata__
|
|
309
|
-
if
|
|
310
|
-
raise TypeError(f"Invalid annotation for skin: {annotation},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
356
|
+
if len(annotation_values) != 1:
|
|
357
|
+
raise TypeError(f"Invalid annotation for skin: {annotation} on field {name}, too many annotation values")
|
|
358
|
+
sprite_info = annotation_values[0]
|
|
359
|
+
match sprite_info:
|
|
360
|
+
case SkinSprite(name=sprite_name):
|
|
361
|
+
if annotation_type is not Sprite:
|
|
362
|
+
raise TypeError(f"Invalid annotation for skin: {annotation} on field {name}, expected Sprite")
|
|
363
|
+
names.append(sprite_name)
|
|
364
|
+
setattr(instance, name, Sprite(i))
|
|
365
|
+
i += 1
|
|
366
|
+
case SkinSpriteGroup(names=sprite_names):
|
|
367
|
+
if annotation_type is not SpriteGroup:
|
|
368
|
+
raise TypeError(f"Invalid annotation for skin: {annotation} on field {name}, expected SpriteGroup")
|
|
369
|
+
start_id = i
|
|
370
|
+
count = len(sprite_names)
|
|
371
|
+
names.extend(sprite_names)
|
|
372
|
+
setattr(instance, name, SpriteGroup(start_id, count))
|
|
373
|
+
i += count
|
|
374
|
+
case _:
|
|
375
|
+
raise TypeError(
|
|
376
|
+
f"Invalid annotation for skin: {annotation} on field {name}, unknown sprite info, "
|
|
377
|
+
f"expected a skin() or sprite_group() specifier"
|
|
378
|
+
)
|
|
316
379
|
instance._sprites_ = names
|
|
317
380
|
instance.render_mode = RenderMode(getattr(instance, "render_mode", RenderMode.DEFAULT))
|
|
318
381
|
instance._is_comptime_value_ = True
|
sonolus/script/vec.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from math import atan2, cos, sin
|
|
4
|
-
|
|
5
3
|
from sonolus.script.array import Array
|
|
6
4
|
from sonolus.script.array_like import ArrayLike
|
|
5
|
+
from sonolus.script.internal.impl import perf_meta_fn
|
|
6
|
+
from sonolus.script.internal.math_impls import _atan2, _cos, _sin
|
|
7
7
|
from sonolus.script.num import Num
|
|
8
8
|
from sonolus.script.record import Record
|
|
9
9
|
|
|
10
|
+
atan2 = _atan2
|
|
11
|
+
sin = _sin
|
|
12
|
+
cos = _cos
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class Vec2(Record):
|
|
12
16
|
"""A 2D vector.
|
|
@@ -87,6 +91,7 @@ class Vec2(Record):
|
|
|
87
91
|
return Vec2(x=cos(angle), y=sin(angle))
|
|
88
92
|
|
|
89
93
|
@property
|
|
94
|
+
@perf_meta_fn
|
|
90
95
|
def magnitude(self) -> float:
|
|
91
96
|
"""Calculate the magnitude (length) of the vector.
|
|
92
97
|
|
|
@@ -96,6 +101,7 @@ class Vec2(Record):
|
|
|
96
101
|
return (self.x**2 + self.y**2) ** 0.5
|
|
97
102
|
|
|
98
103
|
@property
|
|
104
|
+
@perf_meta_fn
|
|
99
105
|
def angle(self) -> float:
|
|
100
106
|
"""Calculate the angle of the vector in radians from the positive x-axis.
|
|
101
107
|
|
|
@@ -104,6 +110,7 @@ class Vec2(Record):
|
|
|
104
110
|
"""
|
|
105
111
|
return atan2(self.y, self.x)
|
|
106
112
|
|
|
113
|
+
@perf_meta_fn
|
|
107
114
|
def dot(self, other: Vec2) -> float:
|
|
108
115
|
"""Calculate the dot product of this vector with another vector.
|
|
109
116
|
|
|
@@ -115,6 +122,7 @@ class Vec2(Record):
|
|
|
115
122
|
"""
|
|
116
123
|
return self.x * other.x + self.y * other.y
|
|
117
124
|
|
|
125
|
+
@perf_meta_fn
|
|
118
126
|
def rotate(self, angle: float) -> Vec2:
|
|
119
127
|
"""Rotate the vector by a given angle in radians and return a new vector.
|
|
120
128
|
|
|
@@ -124,11 +132,12 @@ class Vec2(Record):
|
|
|
124
132
|
Returns:
|
|
125
133
|
A new vector rotated by the given angle.
|
|
126
134
|
"""
|
|
127
|
-
return Vec2(
|
|
135
|
+
return Vec2._quick_construct(
|
|
128
136
|
x=self.x * cos(angle) - self.y * sin(angle),
|
|
129
137
|
y=self.x * sin(angle) + self.y * cos(angle),
|
|
130
138
|
)
|
|
131
139
|
|
|
140
|
+
@perf_meta_fn
|
|
132
141
|
def rotate_about(self, angle: float, pivot: Vec2) -> Vec2:
|
|
133
142
|
"""Rotate the vector about a pivot by a given angle in radians and return a new vector.
|
|
134
143
|
|
|
@@ -141,15 +150,17 @@ class Vec2(Record):
|
|
|
141
150
|
"""
|
|
142
151
|
return (self - pivot).rotate(angle) + pivot
|
|
143
152
|
|
|
153
|
+
@perf_meta_fn
|
|
144
154
|
def normalize(self) -> Vec2:
|
|
145
155
|
"""Normalize the vector (set the magnitude to 1) and return a new vector.
|
|
146
156
|
|
|
147
157
|
Returns:
|
|
148
158
|
A new vector with magnitude 1.
|
|
149
159
|
"""
|
|
150
|
-
magnitude = self.
|
|
151
|
-
return Vec2(x=self.x / magnitude, y=self.y / magnitude)
|
|
160
|
+
magnitude = (self.x**2 + self.y**2) ** 0.5
|
|
161
|
+
return Vec2._quick_construct(x=self.x / magnitude, y=self.y / magnitude)
|
|
152
162
|
|
|
163
|
+
@perf_meta_fn
|
|
153
164
|
def orthogonal(self) -> Vec2:
|
|
154
165
|
"""Return a vector orthogonal to this vector.
|
|
155
166
|
|
|
@@ -158,7 +169,7 @@ class Vec2(Record):
|
|
|
158
169
|
Returns:
|
|
159
170
|
A new vector orthogonal to this vector.
|
|
160
171
|
"""
|
|
161
|
-
return Vec2(x=-self.y, y=self.x)
|
|
172
|
+
return Vec2._quick_construct(x=-self.y, y=self.x)
|
|
162
173
|
|
|
163
174
|
@property
|
|
164
175
|
def tuple(self) -> tuple[float, float]:
|
|
@@ -169,6 +180,7 @@ class Vec2(Record):
|
|
|
169
180
|
"""
|
|
170
181
|
return self.x, self.y
|
|
171
182
|
|
|
183
|
+
@perf_meta_fn
|
|
172
184
|
def __add__(self, other: Vec2) -> Vec2:
|
|
173
185
|
"""Add this vector to another vector and return a new vector.
|
|
174
186
|
|
|
@@ -178,8 +190,9 @@ class Vec2(Record):
|
|
|
178
190
|
Returns:
|
|
179
191
|
A new vector resulting from the addition.
|
|
180
192
|
"""
|
|
181
|
-
return Vec2(x=self.x + other.x, y=self.y + other.y)
|
|
193
|
+
return Vec2._quick_construct(x=self.x + other.x, y=self.y + other.y)
|
|
182
194
|
|
|
195
|
+
@perf_meta_fn
|
|
183
196
|
def __sub__(self, other: Vec2) -> Vec2:
|
|
184
197
|
"""Subtract another vector from this vector and return a new vector.
|
|
185
198
|
|
|
@@ -189,8 +202,9 @@ class Vec2(Record):
|
|
|
189
202
|
Returns:
|
|
190
203
|
A new vector resulting from the subtraction.
|
|
191
204
|
"""
|
|
192
|
-
return Vec2(x=self.x - other.x, y=self.y - other.y)
|
|
205
|
+
return Vec2._quick_construct(x=self.x - other.x, y=self.y - other.y)
|
|
193
206
|
|
|
207
|
+
@perf_meta_fn
|
|
194
208
|
def __mul__(self, other: Vec2 | float) -> Vec2:
|
|
195
209
|
"""Multiply this vector by another vector or a scalar and return a new vector.
|
|
196
210
|
|
|
@@ -202,19 +216,21 @@ class Vec2(Record):
|
|
|
202
216
|
"""
|
|
203
217
|
match other:
|
|
204
218
|
case Vec2(x, y):
|
|
205
|
-
return Vec2(x=self.x * x, y=self.y * y)
|
|
219
|
+
return Vec2._quick_construct(x=self.x * x, y=self.y * y)
|
|
206
220
|
case Num(factor):
|
|
207
|
-
return Vec2(x=self.x * factor, y=self.y * factor)
|
|
221
|
+
return Vec2._quick_construct(x=self.x * factor, y=self.y * factor)
|
|
208
222
|
case _:
|
|
209
223
|
return NotImplemented
|
|
210
224
|
|
|
225
|
+
@perf_meta_fn
|
|
211
226
|
def __rmul__(self, other):
|
|
212
227
|
match other:
|
|
213
228
|
case Num(factor):
|
|
214
|
-
return Vec2(x=self.x * factor, y=self.y * factor)
|
|
229
|
+
return Vec2._quick_construct(x=self.x * factor, y=self.y * factor)
|
|
215
230
|
case _:
|
|
216
231
|
return NotImplemented
|
|
217
232
|
|
|
233
|
+
@perf_meta_fn
|
|
218
234
|
def __truediv__(self, other: Vec2 | float) -> Vec2:
|
|
219
235
|
"""Divide this vector by another vector or a scalar and return a new vector.
|
|
220
236
|
|
|
@@ -226,19 +242,20 @@ class Vec2(Record):
|
|
|
226
242
|
"""
|
|
227
243
|
match other:
|
|
228
244
|
case Vec2(x, y):
|
|
229
|
-
return Vec2(x=self.x / x, y=self.y / y)
|
|
245
|
+
return Vec2._quick_construct(x=self.x / x, y=self.y / y)
|
|
230
246
|
case Num(factor):
|
|
231
|
-
return Vec2(x=self.x / factor, y=self.y / factor)
|
|
247
|
+
return Vec2._quick_construct(x=self.x / factor, y=self.y / factor)
|
|
232
248
|
case _:
|
|
233
249
|
return NotImplemented
|
|
234
250
|
|
|
251
|
+
@perf_meta_fn
|
|
235
252
|
def __neg__(self) -> Vec2:
|
|
236
253
|
"""Negate the vector (invert the direction) and return a new vector.
|
|
237
254
|
|
|
238
255
|
Returns:
|
|
239
256
|
A new vector with inverted direction.
|
|
240
257
|
"""
|
|
241
|
-
return Vec2(x=-self.x, y=-self.y)
|
|
258
|
+
return Vec2._quick_construct(x=-self.x, y=-self.y)
|
|
242
259
|
|
|
243
260
|
|
|
244
261
|
def pnpoly(vertices: ArrayLike[Vec2] | tuple[Vec2, ...], test: Vec2) -> bool:
|