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.

@@ -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
- for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
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 annotation_type is not Particle:
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}, expected a single string annotation value"
164
+ f"Invalid annotation for particles: {annotation} on field {name}, too many annotation values"
128
165
  )
129
- particle_name = annotation_values[0].name
130
- names.append(particle_name)
131
- setattr(instance, name, Particle(i))
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
- build_collection(self, Path(build_dir), config)
69
- run_server(Path(build_dir) / "site", port=port)
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 # noqa: E741
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 = skin_sprite("other")
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
- for i, (name, annotation) in enumerate(get_field_specifiers(cls, skip={"render_mode"}).items()):
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 annotation_type is not Sprite:
310
- raise TypeError(f"Invalid annotation for skin: {annotation}, expected annotation of type Sprite")
311
- if len(annotation_values) != 1 or not isinstance(annotation_values[0], SkinSprite):
312
- raise TypeError(f"Invalid annotation for skin: {annotation}, expected a single string annotation value")
313
- sprite_name = annotation_values[0].name
314
- names.append(sprite_name)
315
- setattr(instance, name, Sprite(i))
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.magnitude
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12