sonolus.py 0.3.2__py3-none-any.whl → 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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

@@ -1,6 +1,7 @@
1
1
  from collections.abc import Iterable
2
2
  from typing import overload
3
3
 
4
+ from sonolus.script.array import Array
4
5
  from sonolus.script.array_like import ArrayLike
5
6
  from sonolus.script.internal.context import ctx
6
7
  from sonolus.script.internal.dict_impl import DictImpl
@@ -129,6 +130,8 @@ def _max(*args, key: callable = _identity):
129
130
  (iterable,) = args
130
131
  if isinstance(iterable, ArrayLike):
131
132
  return compile_and_call(iterable._max_, key=key)
133
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
134
+ return compile_and_call(Array(*iterable.value)._max_, key=key)
132
135
  else:
133
136
  raise TypeError(f"Unsupported type: {type(iterable)} for max")
134
137
  else:
@@ -169,6 +172,8 @@ def _min(*args, key: callable = _identity):
169
172
  (iterable,) = args
170
173
  if isinstance(iterable, ArrayLike):
171
174
  return compile_and_call(iterable._min_, key=key)
175
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
176
+ return compile_and_call(Array(*iterable.value)._min_, key=key)
172
177
  else:
173
178
  raise TypeError(f"Unsupported type: {type(iterable)} for min")
174
179
  else:
@@ -221,12 +226,12 @@ def _float(value=0.0):
221
226
  return value
222
227
 
223
228
 
224
- @meta_fn
225
229
  def _bool(value=False):
226
- value = validate_value(value)
227
- if not _is_num(value):
228
- raise TypeError("Only numeric arguments to bool() are supported")
229
- return value != 0
230
+ # Relies on the compiler to perform the conversion in a boolean context
231
+ if value: # noqa: SIM103
232
+ return True
233
+ else:
234
+ return False
230
235
 
231
236
 
232
237
  _int._type_mapping_ = Num
@@ -291,7 +291,11 @@ class ReadOnlyMemory:
291
291
  _lock: Lock
292
292
 
293
293
  def __init__(self):
294
- self.values = []
294
+ self.values = [
295
+ float("nan"),
296
+ float("inf"),
297
+ float("-inf"),
298
+ ]
295
299
  self.indexes = {}
296
300
  self._lock = Lock()
297
301
 
@@ -97,6 +97,22 @@ def _log(x: float, base: float | None = None) -> float:
97
97
  return _ln(x) / _ln(base)
98
98
 
99
99
 
100
+ def _sqrt(x: float) -> float:
101
+ return x**0.5
102
+
103
+
104
+ @native_function(Op.Degree)
105
+ def _degrees(x: float) -> float:
106
+ """Convert radians to degrees."""
107
+ return math.degrees(x)
108
+
109
+
110
+ @native_function(Op.Radian)
111
+ def _radians(x: float) -> float:
112
+ """Convert degrees to radians."""
113
+ return math.radians(x)
114
+
115
+
100
116
  @native_function(Op.Rem)
101
117
  def _remainder(x: float, y: float) -> float:
102
118
  # This is different from math.remainder in Python's math package, which could be confusing
@@ -119,4 +135,5 @@ MATH_BUILTIN_IMPLS = {
119
135
  id(math.trunc): _trunc,
120
136
  id(round): _round,
121
137
  id(math.log): _log,
138
+ id(math.sqrt): _sqrt,
122
139
  }
@@ -9,7 +9,7 @@ from sonolus.script.internal.impl import meta_fn, validate_value
9
9
  from sonolus.script.num import Num, _is_num
10
10
 
11
11
 
12
- def native_call(op: Op, *args: Num) -> Num:
12
+ def native_call(op: Op, *args: int | float | bool) -> Num:
13
13
  if not ctx():
14
14
  raise RuntimeError("Unexpected native call")
15
15
  args = tuple(validate_value(arg) for arg in args)
@@ -21,12 +21,12 @@ def native_call(op: Op, *args: Num) -> Num:
21
21
 
22
22
 
23
23
  def native_function[**P, R](op: Op) -> Callable[[Callable[P, R]], Callable[P, R]]:
24
- def decorator(fn: Callable[P, Num]) -> Callable[P, Num]:
24
+ def decorator(fn: Callable[P, int | float | bool]) -> Callable[P, Num]:
25
25
  signature = inspect.signature(fn)
26
26
 
27
27
  @functools.wraps(fn)
28
28
  @meta_fn
29
- def wrapper(*args: Num) -> Num:
29
+ def wrapper(*args: int | float | bool) -> Num:
30
30
  if len(args) < sum(1 for p in signature.parameters.values() if p.default == inspect.Parameter.empty):
31
31
  raise TypeError(f"Expected {len(signature.parameters)} arguments, got {len(args)}")
32
32
  if ctx():
@@ -1,15 +1,17 @@
1
- from sonolus.script.array_like import ArrayLike
1
+ from sonolus.script.array_like import ArrayLike, get_positive_index
2
+ from sonolus.script.internal.context import ctx
3
+ from sonolus.script.internal.impl import meta_fn, validate_value
2
4
  from sonolus.script.iterator import SonolusIterator
3
5
  from sonolus.script.num import Num
4
6
  from sonolus.script.record import Record
5
7
 
6
8
 
7
- class Range(Record, ArrayLike[Num]):
9
+ class Range(Record, ArrayLike[int]):
8
10
  start: int
9
11
  stop: int
10
12
  step: int
11
13
 
12
- def __new__(cls, start: Num, stop: Num | None = None, step: Num = 1):
14
+ def __new__(cls, start: int, stop: int | None = None, step: int = 1):
13
15
  if stop is None:
14
16
  start, stop = 0, start
15
17
  return super().__new__(cls, start, stop, step)
@@ -35,14 +37,14 @@ class Range(Record, ArrayLike[Num]):
35
37
  return 0
36
38
  return (diff - self.step - 1) // -self.step
37
39
 
38
- def __getitem__(self, index: Num) -> Num:
39
- return self.start + index * self.step
40
+ def __getitem__(self, index: int) -> int:
41
+ return self.start + get_positive_index(index, len(self)) * self.step
40
42
 
41
- def __setitem__(self, index: Num, value: Num):
43
+ def __setitem__(self, index: int, value: int):
42
44
  raise TypeError("Range does not support item assignment")
43
45
 
44
46
  @property
45
- def last(self) -> Num:
47
+ def last(self) -> int:
46
48
  return self[len(self) - 1]
47
49
 
48
50
  def __eq__(self, other):
@@ -79,3 +81,24 @@ class RangeIterator(Record, SonolusIterator):
79
81
 
80
82
  def advance(self):
81
83
  self.value += self.step
84
+
85
+
86
+ @meta_fn
87
+ def range_or_tuple(start: int, stop: int | None = None, step: int = 1) -> Range | tuple[int, ...]:
88
+ if stop is None:
89
+ start, stop = 0, start
90
+ if not ctx():
91
+ return range(start, stop, step) # type: ignore
92
+ start = Num._accept_(start)
93
+ stop = Num._accept_(stop) if stop is not None else None
94
+ step = Num._accept_(step)
95
+ if start._is_py_() and stop._is_py_() and step._is_py_():
96
+ start_int = start._as_py_()
97
+ stop_int = stop._as_py_() if stop is not None else None
98
+ if stop_int is None:
99
+ start_int, stop_int = 0, start_int
100
+ step_int = step._as_py_()
101
+ if start_int % 1 != 0 or stop_int % 1 != 0 or step_int % 1 != 0:
102
+ raise TypeError("Range arguments must be integers")
103
+ return validate_value(tuple(range(int(start_int), int(stop_int), int(step_int)))) # type: ignore
104
+ return Range(start, stop, step)
@@ -21,8 +21,10 @@ class TupleImpl(TransientValue):
21
21
  raise TypeError(f"Cannot index tuple with {item}")
22
22
  if int(item) != item:
23
23
  raise TypeError(f"Cannot index tuple with non-integer {item}")
24
- if not (0 <= item < len(self.value)):
24
+ if not (-len(self.value) <= item < len(self.value)):
25
25
  raise IndexError(f"Tuple index out of range: {item}")
26
+ if item < 0:
27
+ item += len(self.value)
26
28
  return self.value[int(item)]
27
29
 
28
30
  @meta_fn
@@ -122,7 +122,7 @@ class Value:
122
122
  For instance:
123
123
  ```
124
124
  class X(Record):
125
- v: Num
125
+ v: int
126
126
 
127
127
  a = 1
128
128
  b = X(a) # (1) _get_() is called on a
@@ -1,8 +1,10 @@
1
1
  from typing import Self
2
2
 
3
3
  from sonolus.backend.ops import Op
4
+ from sonolus.script.array_like import ArrayLike
4
5
  from sonolus.script.debug import static_error
5
6
  from sonolus.script.internal.native import native_function
7
+ from sonolus.script.internal.range import range_or_tuple
6
8
  from sonolus.script.num import Num
7
9
  from sonolus.script.record import Record
8
10
 
@@ -34,7 +36,7 @@ class Interval(Record):
34
36
 
35
37
  @property
36
38
  def is_empty(self) -> bool:
37
- """Whether the has a start greater than its end."""
39
+ """Whether the interval has a start greater than its end."""
38
40
  return self.start > self.end
39
41
 
40
42
  @property
@@ -296,7 +298,7 @@ def unlerp_clamped(a: float, b: float, x: float, /) -> float:
296
298
 
297
299
  @native_function(Op.Remap)
298
300
  def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
299
- """Remap a value from one interval to another.
301
+ """Linearly remap a value from one interval to another.
300
302
 
301
303
  Args:
302
304
  a: The start of the input interval.
@@ -313,7 +315,7 @@ def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
313
315
 
314
316
  @native_function(Op.RemapClamped)
315
317
  def remap_clamped(a: float, b: float, c: float, d: float, x: float, /) -> float:
316
- """Remap a value from one interval to another, clamped to the output interval.
318
+ """Linearly remap a value from one interval to another, clamped to the output interval.
317
319
 
318
320
  Args:
319
321
  a: The start of the input interval.
@@ -341,3 +343,59 @@ def clamp(x: float, a: float, b: float, /) -> float:
341
343
  The clamped value.
342
344
  """
343
345
  return max(a, min(b, x))
346
+
347
+
348
+ def interp(
349
+ xp: ArrayLike[float] | tuple[float, ...],
350
+ fp: ArrayLike[float] | tuple[float, ...],
351
+ x: float,
352
+ ) -> float:
353
+ """Linearly interpolate a value within a sequence of points.
354
+
355
+ The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
356
+ For values of x outside the range of xp, the slope of the first or last segment is used to extrapolate.
357
+
358
+ Args:
359
+ xp: The x-coordinates of the points in increasing order.
360
+ fp: The y-coordinates of the points.
361
+ x: The x-coordinate to interpolate.
362
+
363
+ Returns:
364
+ The interpolated value.
365
+ """
366
+ assert len(xp) == len(fp)
367
+ assert len(xp) >= 2
368
+ for i in range_or_tuple(1, len(xp) - 1):
369
+ # At i == 1, x may be less than x[0], but since we're extrapolating, we use the first segment regardless.
370
+ if x <= xp[i]:
371
+ return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
372
+ # x > xp[-2] so we can just use the last segment regardless of whether x is in it or to the right of it.
373
+ return remap(xp[-2], xp[-1], fp[-2], fp[-1], x)
374
+
375
+
376
+ def interp_clamped(
377
+ xp: ArrayLike[float] | tuple[float, ...],
378
+ fp: ArrayLike[float] | tuple[float, ...],
379
+ x: float,
380
+ ) -> float:
381
+ """Linearly interpolate a value within a sequence of points.
382
+
383
+ The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
384
+ For x-coordinates outside the range of the sequence, the respective endpoint of fp is returned.
385
+
386
+ Args:
387
+ xp: The x-coordinates of the points in increasing order.
388
+ fp: The y-coordinates of the points.
389
+ x: The x-coordinate to interpolate.
390
+
391
+ Returns:
392
+ The interpolated value.
393
+ """
394
+ assert len(xp) == len(fp)
395
+ assert len(xp) >= 2
396
+ if x <= xp[0]:
397
+ return fp[0]
398
+ for i in range_or_tuple(1, len(xp)):
399
+ if x <= xp[i]:
400
+ return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
401
+ return fp[-1]
sonolus/script/num.py CHANGED
@@ -439,17 +439,17 @@ if TYPE_CHECKING:
439
439
 
440
440
  @runtime_checkable
441
441
  class Num[T](Protocol, int, bool, float):
442
- def __add__(self, other: T, /) -> Num: ...
443
- def __sub__(self, other: T, /) -> Num: ...
444
- def __mul__(self, other: T, /) -> Num: ...
445
- def __truediv__(self, other: T, /) -> Num: ...
446
- def __floordiv__(self, other: T, /) -> Num: ...
447
- def __mod__(self, other: T, /) -> Num: ...
448
- def __pow__(self, other: T, /) -> Num: ...
449
-
450
- def __neg__(self, /) -> Num: ...
451
- def __pos__(self, /) -> Num: ...
452
- def __abs__(self, /) -> Num: ...
442
+ def __add__(self, other: T, /) -> Num | int | bool | float: ...
443
+ def __sub__(self, other: T, /) -> Num | int | bool | float: ...
444
+ def __mul__(self, other: T, /) -> Num | int | bool | float: ...
445
+ def __truediv__(self, other: T, /) -> Num | int | bool | float: ...
446
+ def __floordiv__(self, other: T, /) -> Num | int | bool | float: ...
447
+ def __mod__(self, other: T, /) -> Num | int | bool | float: ...
448
+ def __pow__(self, other: T, /) -> Num | int | bool | float: ...
449
+
450
+ def __neg__(self, /) -> Num | int | bool | float: ...
451
+ def __pos__(self, /) -> Num | int | bool | float: ...
452
+ def __abs__(self, /) -> Num | int | bool | float: ...
453
453
 
454
454
  def __eq__(self, other: Any, /) -> bool: ...
455
455
  def __ne__(self, other: Any, /) -> bool: ...
@@ -461,10 +461,10 @@ if TYPE_CHECKING:
461
461
  def __hash__(self, /) -> int: ...
462
462
 
463
463
  @property
464
- def real(self) -> Num: ...
464
+ def real(self) -> Num | int | bool | float: ...
465
465
 
466
466
  @property
467
- def imag(self) -> Num: ...
467
+ def imag(self) -> Num | int | bool | float: ...
468
468
  else:
469
469
  # Need to do this to satisfy type checkers (especially Pycharm)
470
470
  _Num.__name__ = "Num"
sonolus/script/pointer.py CHANGED
@@ -6,7 +6,7 @@ from sonolus.script.num import Num, _is_num
6
6
 
7
7
 
8
8
  @meta_fn
9
- def _deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
9
+ def _deref[T: Value](block: int, offset: int, type_: type[T]) -> T:
10
10
  block = Num._accept_(block)
11
11
  offset = Num._accept_(offset)
12
12
  type_ = validate_value(type_)._as_py_()
sonolus/script/quad.py CHANGED
@@ -28,6 +28,20 @@ class Quad(Record):
28
28
  br: Vec2
29
29
  """The bottom-right corner of the quad."""
30
30
 
31
+ @classmethod
32
+ def zero(cls) -> Quad:
33
+ """Return a quad with all corners set to (0, 0).
34
+
35
+ Returns:
36
+ A new quad with all corners at the origin.
37
+ """
38
+ return cls(
39
+ bl=Vec2.zero(),
40
+ tl=Vec2.zero(),
41
+ tr=Vec2.zero(),
42
+ br=Vec2.zero(),
43
+ )
44
+
31
45
  @classmethod
32
46
  def from_quad(cls, value: QuadLike, /) -> Quad:
33
47
  """Create a quad from a quad-like value."""
@@ -80,7 +94,14 @@ class Quad(Record):
80
94
  ).translate(self.center * (Vec2(1, 1) - factor))
81
95
 
82
96
  def rotate(self, angle: float, /) -> Self:
83
- """Rotate the quad by the given angle about the origin and return a new quad."""
97
+ """Rotate the quad by the given angle about the origin and return a new quad.
98
+
99
+ Args:
100
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
101
+
102
+ Returns:
103
+ A new quad rotated by the given angle.
104
+ """
84
105
  return Quad(
85
106
  bl=self.bl.rotate(angle),
86
107
  tl=self.tl.rotate(angle),
@@ -94,7 +115,15 @@ class Quad(Record):
94
115
  /,
95
116
  pivot: Vec2,
96
117
  ) -> Self:
97
- """Rotate the quad by the given angle about the given pivot and return a new quad."""
118
+ """Rotate the quad by the given angle about the given pivot and return a new quad.
119
+
120
+ Args:
121
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
122
+ pivot: The pivot point for rotation.
123
+
124
+ Returns:
125
+ A new quad rotated about the pivot by the given angle.
126
+ """
98
127
  return Quad(
99
128
  bl=self.bl.rotate_about(angle, pivot),
100
129
  tl=self.tl.rotate_about(angle, pivot),
@@ -103,7 +132,14 @@ class Quad(Record):
103
132
  )
104
133
 
105
134
  def rotate_centered(self, angle: float, /) -> Self:
106
- """Rotate the quad by the given angle about its center and return a new quad."""
135
+ """Rotate the quad by the given angle about its center and return a new quad.
136
+
137
+ Args:
138
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
139
+
140
+ Returns:
141
+ A new quad rotated about its center by the given angle.
142
+ """
107
143
  return self.rotate_about(angle, self.center)
108
144
 
109
145
  def permute(self, count: int = 1, /) -> Self:
sonolus/script/record.py CHANGED
@@ -23,6 +23,7 @@ from sonolus.script.num import Num
23
23
  class RecordMeta(type):
24
24
  @meta_fn
25
25
  def __pos__[T](cls: type[T]) -> T:
26
+ """Create a zero-initialized record instance."""
26
27
  return cls._zero_()
27
28
 
28
29
 
@@ -53,6 +54,11 @@ class Record(GenericValue, metaclass=RecordMeta):
53
54
  record_4 = +MyRecord # Create a zero-initialized record
54
55
  record_5 = +MyGenericRecord[int, int]
55
56
  ```
57
+
58
+ Copying a record:
59
+ ```python
60
+ record_copy = +record
61
+ ```
56
62
  """
57
63
 
58
64
  _value: dict[str, Value]
@@ -283,6 +289,7 @@ class Record(GenericValue, metaclass=RecordMeta):
283
289
  def __hash__(self):
284
290
  return hash(tuple(field.__get__(self) for field in self._fields))
285
291
 
292
+ @meta_fn
286
293
  def __pos__(self) -> Self:
287
294
  """Return a copy of the record."""
288
295
  return self._copy_()
sonolus/script/runtime.py CHANGED
@@ -988,6 +988,25 @@ def time() -> float:
988
988
  return 0
989
989
 
990
990
 
991
+ @meta_fn
992
+ def offset_adjusted_time() -> float:
993
+ """Get the current time of the game adjusted by the input offset.
994
+
995
+ Returns 0 in preview mode and tutorial mode.
996
+ """
997
+ if not ctx():
998
+ return 0
999
+ match ctx().global_state.mode:
1000
+ case Mode.PLAY:
1001
+ return _PlayRuntimeUpdate.time - _PlayRuntimeEnvironment.input_offset
1002
+ case Mode.WATCH:
1003
+ return _WatchRuntimeUpdate.time - _WatchRuntimeEnvironment.input_offset
1004
+ case Mode.TUTORIAL:
1005
+ return _TutorialRuntimeUpdate.time
1006
+ case _:
1007
+ return 0
1008
+
1009
+
991
1010
  @meta_fn
992
1011
  def delta_time() -> float:
993
1012
  """Get the time elapsed since the last frame.
@@ -1026,6 +1045,15 @@ def scaled_time() -> float:
1026
1045
  return 0
1027
1046
 
1028
1047
 
1048
+ @meta_fn
1049
+ def prev_time() -> float:
1050
+ """Get the time of the previous frame.
1051
+
1052
+ Returns 0 in preview mode.
1053
+ """
1054
+ return time() - delta_time()
1055
+
1056
+
1029
1057
  @meta_fn
1030
1058
  def touches() -> ArrayLike[Touch]:
1031
1059
  """Get the current touches of the game."""
sonolus/script/stream.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from math import inf
3
4
  from typing import cast, dataclass_transform
4
5
 
5
6
  from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr
@@ -14,7 +15,7 @@ from sonolus.script.internal.value import BackingValue, Value
14
15
  from sonolus.script.iterator import SonolusIterator
15
16
  from sonolus.script.num import Num
16
17
  from sonolus.script.record import Record
17
- from sonolus.script.runtime import delta_time, time
18
+ from sonolus.script.runtime import prev_time, time
18
19
  from sonolus.script.values import sizeof
19
20
 
20
21
 
@@ -79,12 +80,12 @@ def streams[T](cls: type[T]) -> T:
79
80
  ```python
80
81
  @streams
81
82
  class Streams:
82
- stream_1: Stream[Num] # A stream of Num values
83
+ stream_1: Stream[int] # A stream of int values
83
84
  stream_2: Stream[Vec2] # A stream of Vec2 values
84
- group_1: StreamGroup[Num, 10] # A group of 10 Num streams
85
+ group_1: StreamGroup[int, 10] # A group of 10 int streams
85
86
  group_2: StreamGroup[Vec2, 5] # A group of 5 Vec2 streams
86
87
 
87
- data_field_1: Num # A data field of type Num
88
+ data_field_1: int # A data field of type int
88
89
  data_field_2: Vec2 # A data field of type Vec2
89
90
  ```
90
91
  """
@@ -316,6 +317,14 @@ class Stream[T](Record):
316
317
  _check_can_read_stream()
317
318
  return self[self.next_key_inclusive(key)]
318
319
 
320
+ def get_previous_inclusive(self, key: int | float) -> T:
321
+ """Get the value corresponding to the previous key, or the value at the given key if it is in the stream.
322
+
323
+ Equivalent to `self[self.previous_key_inclusive(key)]`.
324
+ """
325
+ _check_can_read_stream()
326
+ return self[self.previous_key_inclusive(key)]
327
+
319
328
  def iter_items_from(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
320
329
  """Iterate over the items in the stream in ascending order starting from the given key.
321
330
 
@@ -345,7 +354,7 @@ class Stream[T](Record):
345
354
  ```
346
355
  """
347
356
  _check_can_read_stream()
348
- return _StreamBoundedAscIterator(self, self.next_key(time() - delta_time()), time())
357
+ return _StreamBoundedAscIterator(self, self.next_key(prev_time()), time())
349
358
 
350
359
  def iter_items_from_desc(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
351
360
  """Iterate over the items in the stream in descending order starting from the given key.
@@ -391,7 +400,7 @@ class Stream[T](Record):
391
400
  ```
392
401
  """
393
402
  _check_can_read_stream()
394
- return _StreamBoundedAscKeyIterator(self, self.next_key(time() - delta_time()), time())
403
+ return _StreamBoundedAscKeyIterator(self, self.next_key(prev_time()), time())
395
404
 
396
405
  def iter_keys_from_desc(self, start: int | float, /) -> SonolusIterator[int | float]:
397
406
  """Iterate over the keys in the stream in descending order starting from the given key.
@@ -437,7 +446,7 @@ class Stream[T](Record):
437
446
  ```
438
447
  """
439
448
  _check_can_read_stream()
440
- return _StreamBoundedAscValueIterator(self, self.next_key(time() - delta_time()), time())
449
+ return _StreamBoundedAscValueIterator(self, self.next_key(prev_time()), time())
441
450
 
442
451
  def iter_values_from_desc(self, start: int | float, /) -> SonolusIterator[T]:
443
452
  """Iterate over the values in the stream in descending order starting from the given key.
@@ -513,7 +522,7 @@ class _StreamAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
513
522
  return self.current_key, self.stream[self.current_key]
514
523
 
515
524
  def advance(self):
516
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
525
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
517
526
 
518
527
 
519
528
  class _StreamBoundedAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
@@ -528,7 +537,7 @@ class _StreamBoundedAscIterator[T](Record, SonolusIterator[tuple[int | float, T]
528
537
  return self.current_key, self.stream[self.current_key]
529
538
 
530
539
  def advance(self):
531
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
540
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
532
541
 
533
542
 
534
543
  class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
@@ -542,7 +551,7 @@ class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
542
551
  return self.current_key, self.stream[self.current_key]
543
552
 
544
553
  def advance(self):
545
- self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
554
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
546
555
 
547
556
 
548
557
  class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -556,7 +565,7 @@ class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
556
565
  return self.current_key
557
566
 
558
567
  def advance(self):
559
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
568
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
560
569
 
561
570
 
562
571
  class _StreamBoundedAscKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -571,7 +580,7 @@ class _StreamBoundedAscKeyIterator[T](Record, SonolusIterator[int | float]):
571
580
  return self.current_key
572
581
 
573
582
  def advance(self):
574
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
583
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
575
584
 
576
585
 
577
586
  class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -585,7 +594,7 @@ class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
585
594
  return self.current_key
586
595
 
587
596
  def advance(self):
588
- self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
597
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
589
598
 
590
599
 
591
600
  class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
@@ -599,7 +608,7 @@ class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
599
608
  return self.stream[self.current_key]
600
609
 
601
610
  def advance(self):
602
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
611
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
603
612
 
604
613
 
605
614
  class _StreamBoundedAscValueIterator[T](Record, SonolusIterator[T]):
@@ -614,7 +623,7 @@ class _StreamBoundedAscValueIterator[T](Record, SonolusIterator[T]):
614
623
  return self.stream[self.current_key]
615
624
 
616
625
  def advance(self):
617
- self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
626
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
618
627
 
619
628
 
620
629
  class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
@@ -628,7 +637,7 @@ class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
628
637
  return self.stream[self.current_key]
629
638
 
630
639
  def advance(self):
631
- self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
640
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
632
641
 
633
642
 
634
643
  @native_function(Op.StreamGetNextKey)
@@ -137,7 +137,7 @@ class Transform2d(Record):
137
137
  """Rotate about the origin and return a new transform.
138
138
 
139
139
  Args:
140
- angle: The angle of rotation in radians.
140
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
141
141
 
142
142
  Returns:
143
143
  A new transform after rotation.
@@ -160,7 +160,7 @@ class Transform2d(Record):
160
160
  """Rotate about the pivot and return a new transform.
161
161
 
162
162
  Args:
163
- angle: The angle of rotation in radians.
163
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
164
164
  pivot: The pivot point for rotation.
165
165
 
166
166
  Returns:
@@ -470,7 +470,7 @@ class InvertibleTransform2d(Record):
470
470
  """Rotate about the origin and return a new transform.
471
471
 
472
472
  Args:
473
- angle: The angle of rotation in radians.
473
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
474
474
 
475
475
  Returns:
476
476
  A new invertible transform after rotation.
@@ -484,7 +484,7 @@ class InvertibleTransform2d(Record):
484
484
  """Rotate about the pivot and return a new transform.
485
485
 
486
486
  Args:
487
- angle: The angle of rotation in radians.
487
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
488
488
  pivot: The pivot point for rotation.
489
489
 
490
490
  Returns: