sonolus.py 0.3.2__py3-none-any.whl → 0.3.3__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,3 +1,5 @@
1
+ from math import isfinite, isinf, isnan
2
+
1
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
2
4
  from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
3
5
  from sonolus.backend.ops import Op
@@ -54,10 +56,20 @@ def cfg_to_engine_node(entry: BasicBlock):
54
56
 
55
57
  def ir_to_engine_node(stmt) -> EngineNode:
56
58
  match stmt:
57
- case int() | float():
58
- return ConstantNode(value=float(stmt))
59
- case IRConst(value=int(value) | float(value)):
60
- return ConstantNode(value=value)
59
+ case int(value) | float(value) | IRConst(value=int(value) | float(value)):
60
+ value = float(value)
61
+ if value.is_integer():
62
+ return ConstantNode(value=int(value))
63
+ elif isfinite(value):
64
+ return ConstantNode(value=value)
65
+ elif isinf(value):
66
+ # Read values from ROM
67
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
68
+ elif isnan(value):
69
+ # Read value from ROM
70
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
71
+ else:
72
+ raise ValueError(f"Invalid constant value: {value}")
61
73
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
62
74
  return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
63
75
  case IRGet(place=place):
@@ -359,7 +359,7 @@ class Visitor(ast.NodeVisitor):
359
359
  self.loop_head_ctxs.append(header_ctx)
360
360
  self.break_ctxs.append([])
361
361
  set_ctx(header_ctx)
362
- has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
362
+ has_next = self.convert_to_boolean_num(node, self.handle_call(node, iterator.has_next))
363
363
  if has_next._is_py_() and not has_next._as_py_():
364
364
  # The loop will never run, continue after evaluating the condition
365
365
  self.loop_head_ctxs.pop()
@@ -400,7 +400,7 @@ class Visitor(ast.NodeVisitor):
400
400
  self.loop_head_ctxs.append(header_ctx)
401
401
  self.break_ctxs.append([])
402
402
  set_ctx(header_ctx)
403
- test = self.ensure_boolean_num(self.visit(node.test))
403
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
404
404
  if test._is_py_():
405
405
  if test._as_py_():
406
406
  # The loop will run until a break / return
@@ -454,7 +454,7 @@ class Visitor(ast.NodeVisitor):
454
454
  set_ctx(after_ctx)
455
455
 
456
456
  def visit_If(self, node):
457
- test = self.ensure_boolean_num(self.visit(node.test))
457
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
458
458
 
459
459
  if test._is_py_():
460
460
  if test._as_py_():
@@ -507,7 +507,9 @@ class Visitor(ast.NodeVisitor):
507
507
  set_ctx(false_ctx)
508
508
  continue
509
509
  set_ctx(true_ctx)
510
- guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
510
+ guard = (
511
+ self.convert_to_boolean_num(case.guard, self.visit(case.guard)) if case.guard else validate_value(True)
512
+ )
511
513
  if guard._is_py_():
512
514
  if guard._as_py_():
513
515
  for stmt in case.body:
@@ -544,7 +546,7 @@ class Visitor(ast.NodeVisitor):
544
546
  match pattern:
545
547
  case ast.MatchValue(value=value):
546
548
  value = self.visit(value)
547
- test = self.ensure_boolean_num(validate_value(subject == value))
549
+ test = self.convert_to_boolean_num(pattern, validate_value(subject == value))
548
550
  if test._is_py_():
549
551
  if test._as_py_():
550
552
  return ctx(), ctx().into_dead()
@@ -574,7 +576,7 @@ class Visitor(ast.NodeVisitor):
574
576
  target_len = len(patterns)
575
577
  if not (isinstance(subject, Sequence | TupleImpl)):
576
578
  return ctx().into_dead(), ctx()
577
- length_test = self.ensure_boolean_num(validate_value(_len(subject) == target_len))
579
+ length_test = self.convert_to_boolean_num(pattern, validate_value(_len(subject) == target_len))
578
580
  ctx_init = ctx()
579
581
  if not length_test._is_py_():
580
582
  ctx_init.test = length_test.ir()
@@ -739,7 +741,7 @@ class Visitor(ast.NodeVisitor):
739
741
  def visit_UnaryOp(self, node):
740
742
  operand = self.visit(node.operand)
741
743
  if isinstance(node.op, ast.Not):
742
- return self.ensure_boolean_num(operand).not_()
744
+ return self.convert_to_boolean_num(node, operand).not_()
743
745
  op = unary_ops[type(node.op)]
744
746
  if operand._is_py_():
745
747
  operand_py = operand._as_py_()
@@ -768,7 +770,7 @@ class Visitor(ast.NodeVisitor):
768
770
  return validate_value(fn)
769
771
 
770
772
  def visit_IfExp(self, node):
771
- test = self.ensure_boolean_num(self.visit(node.test))
773
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
772
774
 
773
775
  if test._is_py_():
774
776
  if test._as_py_():
@@ -1076,7 +1078,7 @@ class Visitor(ast.NodeVisitor):
1076
1078
 
1077
1079
  def handle_call[**P, R](
1078
1080
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
1079
- ) -> R:
1081
+ ) -> R | Value:
1080
1082
  """Handles a call to the given callable."""
1081
1083
  self.active_ctx = ctx()
1082
1084
  if (
@@ -1127,6 +1129,20 @@ class Visitor(ast.NodeVisitor):
1127
1129
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
1128
1130
  return value
1129
1131
 
1132
+ def convert_to_boolean_num(self, node, value: Value) -> Num:
1133
+ if _is_num(value):
1134
+ return value
1135
+ if hasattr(type(value), "__bool__"):
1136
+ return self.ensure_boolean_num(self.handle_call(node, type(value).__bool__, validate_value(value)))
1137
+ if hasattr(type(value), "__len__"):
1138
+ length = self.handle_call(node, type(value).__len__, validate_value(value))
1139
+ if not _is_num(length):
1140
+ raise TypeError(f"Invalid type for __len__: {type(length).__name__}")
1141
+ if length._is_py_():
1142
+ return Num._accept_(length._as_py_() > 0)
1143
+ return length > Num._accept_(0)
1144
+ raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
1145
+
1130
1146
  def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
1131
1147
  parameters: list[inspect.Parameter] = []
1132
1148
  pos_only_count = len(arguments.posonlyargs)
@@ -549,11 +549,19 @@ class _BaseArchetype:
549
549
  metadata = _annotation_defaults.get(metadata, metadata)
550
550
  if isinstance(metadata, _ArchetypeFieldInfo):
551
551
  if field_info is not None:
552
- raise TypeError(
553
- f"Unexpected multiple field annotations for '{name}', "
554
- f"expected exactly one of imported, exported, entity_memory, or shared_memory"
555
- )
556
- field_info = metadata
552
+ if field_info.storage == metadata.storage and field_info.name is None:
553
+ field_info = metadata
554
+ elif field_info.storage == metadata.storage and (
555
+ metadata.name is None or field_info.name == metadata.name
556
+ ):
557
+ pass
558
+ else:
559
+ raise TypeError(
560
+ f"Unexpected multiple field annotations for '{name}', "
561
+ f"expected exactly one of imported, exported, entity_memory, or shared_memory"
562
+ )
563
+ else:
564
+ field_info = metadata
557
565
  if field_info is None:
558
566
  raise TypeError(
559
567
  f"Missing field annotation for '{name}', "
@@ -880,7 +888,7 @@ class WatchArchetype(_BaseArchetype):
880
888
  case _ArchetypeSelfData():
881
889
  return _deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
882
890
  case _ArchetypeReferenceData(index=index):
883
- return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
891
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
884
892
  case _:
885
893
  raise RuntimeError("Info is only accessible from the entity itself")
886
894
 
@@ -1106,10 +1114,20 @@ class EntityRef[A: _BaseArchetype](Record):
1106
1114
  """Return a new reference with the given archetype type."""
1107
1115
  return EntityRef[archetype](index=self.index)
1108
1116
 
1117
+ @meta_fn
1109
1118
  def get(self) -> A:
1110
1119
  """Get the entity."""
1120
+ if ref := getattr(self, "_ref_", None):
1121
+ return ref
1111
1122
  return self.archetype().at(self.index)
1112
1123
 
1124
+ @meta_fn
1125
+ def get_as(self, archetype: type[_BaseArchetype]) -> _BaseArchetype:
1126
+ """Get the entity as the given archetype type."""
1127
+ if getattr(archetype, "_ref_", None):
1128
+ raise TypeError("Using get_as in level data is not supported.")
1129
+ return self.with_archetype(archetype).get()
1130
+
1113
1131
  def archetype_matches(self) -> bool:
1114
1132
  """Check if entity at the index is precisely of the archetype."""
1115
1133
  return self.index >= 0 and self.archetype().is_at(self.index)
@@ -1117,7 +1135,7 @@ class EntityRef[A: _BaseArchetype](Record):
1117
1135
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1118
1136
  ref = getattr(self, "_ref_", None)
1119
1137
  if ref is None:
1120
- return [self.index]
1138
+ return Num._accept_(self.index)._to_list_()
1121
1139
  else:
1122
1140
  if ref not in level_refs:
1123
1141
  raise KeyError("Reference to entity not in level data")
sonolus/script/array.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any, Self, final
5
5
 
6
6
  from sonolus.backend.ir import IRConst, IRSet
7
7
  from sonolus.backend.place import BlockPlace
8
- from sonolus.script.array_like import ArrayLike
8
+ from sonolus.script.array_like import ArrayLike, get_positive_index
9
9
  from sonolus.script.debug import assert_unreachable
10
10
  from sonolus.script.internal.context import ctx
11
11
  from sonolus.script.internal.error import InternalError
@@ -18,6 +18,7 @@ from sonolus.script.num import Num
18
18
  class ArrayMeta(type):
19
19
  @meta_fn
20
20
  def __pos__[T](cls: type[T]) -> T:
21
+ """Create a zero-initialized array instance."""
21
22
  return cls._zero_()
22
23
 
23
24
 
@@ -186,7 +187,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
186
187
 
187
188
  @meta_fn
188
189
  def __getitem__(self, index: Num) -> T:
189
- index: Num = Num._accept_(index)
190
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
190
191
  if index._is_py_() and 0 <= index._as_py_() < self.size():
191
192
  const_index = index._as_py_()
192
193
  if isinstance(const_index, float) and not const_index.is_integer():
@@ -230,7 +231,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
230
231
 
231
232
  @meta_fn
232
233
  def __setitem__(self, index: Num, value: T):
233
- index: Num = Num._accept_(index)
234
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
234
235
  value = self.element_type()._accept_(value)
235
236
  if ctx():
236
237
  if isinstance(self._value, list):
@@ -303,6 +304,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
303
304
  else:
304
305
  return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
305
306
 
307
+ @meta_fn
306
308
  def __pos__(self) -> Self:
307
309
  """Return a copy of the array."""
308
310
  return self._copy_()
@@ -5,6 +5,8 @@ from abc import abstractmethod
5
5
  from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
+ from sonolus.script.internal.context import ctx
9
+ from sonolus.script.internal.impl import meta_fn
8
10
  from sonolus.script.iterator import SonolusIterator
9
11
  from sonolus.script.num import Num
10
12
  from sonolus.script.record import Record
@@ -302,3 +304,26 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
302
304
 
303
305
  def advance(self):
304
306
  self.i += 1
307
+
308
+
309
+ @meta_fn
310
+ def get_positive_index(index: Num, length: Num) -> Num:
311
+ """Get the positive index for the given index in the array of the given length.
312
+
313
+ This is used to convert negative indixes relative to the end of the array to positive indices.
314
+
315
+ Args:
316
+ index: The index to convert.
317
+ length: The length of the array.
318
+
319
+ Returns:
320
+ The positive index.
321
+ """
322
+ if not ctx():
323
+ return index if index >= 0 else index + length
324
+ index = Num._accept_(index)
325
+ length = Num._accept_(length)
326
+ if index._is_py_() and length._is_py_():
327
+ return Num._accept_(index._as_py_() + length._as_py_() if index._as_py_() < 0 else index._as_py_())
328
+ else:
329
+ return index + (index < 0) * length
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from sonolus.backend.visitor import compile_and_call
4
4
  from sonolus.script.array import Array
5
- from sonolus.script.array_like import ArrayLike
5
+ from sonolus.script.array_like import ArrayLike, get_positive_index
6
6
  from sonolus.script.debug import error
7
7
  from sonolus.script.internal.context import ctx
8
8
  from sonolus.script.internal.impl import meta_fn
9
+ from sonolus.script.interval import clamp
9
10
  from sonolus.script.iterator import SonolusIterator
10
11
  from sonolus.script.num import Num
11
12
  from sonolus.script.pointer import _deref
@@ -140,11 +141,11 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
140
141
  assert p == Pair(5, 6) # The value of p has changed
141
142
  ```
142
143
  """
143
- return self._array[item]
144
+ return self._array[get_positive_index(item, len(self))]
144
145
 
145
146
  def __setitem__(self, key: int, value: T):
146
147
  """Update the element at the given index."""
147
- self._array[key] = value
148
+ self._array[get_positive_index(key, len(self))] = value
148
149
 
149
150
  def __delitem__(self, key: int):
150
151
  """Remove the element at the given index."""
@@ -190,6 +191,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
190
191
  """
191
192
  if index is None:
192
193
  index = self._size - 1
194
+ index = get_positive_index(index, len(self))
193
195
  assert 0 <= index < self._size
194
196
  value = copy(self._array[index])
195
197
  self._size -= 1
@@ -207,7 +209,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
207
209
  index: The index at which to insert the value. Must be in the range [0, size].
208
210
  value: The value to insert.
209
211
  """
210
- assert 0 <= index <= self._size
212
+ index = clamp(get_positive_index(index, len(self)), 0, self._size)
211
213
  assert self._size < len(self._array)
212
214
  self._size += 1
213
215
  for i in range(self._size - 1, index, -1):
@@ -329,6 +331,7 @@ class ArrayPointer[T](Record, ArrayLike[T]):
329
331
 
330
332
  @meta_fn
331
333
  def _get_item(self, item: int) -> T:
334
+ item = get_positive_index(item, self.size)
332
335
  if not ctx():
333
336
  raise TypeError("ArrayPointer values cannot be accessed outside of a context")
334
337
  return _deref(
@@ -536,6 +539,23 @@ class ArrayMap[K, V, Capacity](Record):
536
539
  self._array[self._size] = _ArrayMapEntry(key, value)
537
540
  self._size += 1
538
541
 
542
+ def __delitem__(self, key: K):
543
+ """Remove the key-value pair associated with the given key.
544
+
545
+ Must be called with a key that is present in the map.
546
+
547
+ Args:
548
+ key: The key to remove
549
+ """
550
+ for i in range(self._size):
551
+ entry = self._array[i]
552
+ if entry.key == key:
553
+ self._size -= 1
554
+ if i < self._size:
555
+ self._array[i] = self._array[self._size]
556
+ return
557
+ error()
558
+
539
559
  def __contains__(self, key: K) -> bool:
540
560
  """Return whether the given key is present in the map.
541
561
 
@@ -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
 
@@ -1,4 +1,6 @@
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
@@ -36,7 +38,7 @@ class Range(Record, ArrayLike[Num]):
36
38
  return (diff - self.step - 1) // -self.step
37
39
 
38
40
  def __getitem__(self, index: Num) -> Num:
39
- return self.start + index * self.step
41
+ return self.start + get_positive_index(index, len(self)) * self.step
40
42
 
41
43
  def __setitem__(self, index: Num, value: Num):
42
44
  raise TypeError("Range does not support item assignment")
@@ -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: Num, stop: Num | None = None, step: Num = 1) -> Range | tuple[Num, ...]:
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
@@ -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
 
@@ -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
+ ):
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/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
 
@@ -283,6 +284,7 @@ class Record(GenericValue, metaclass=RecordMeta):
283
284
  def __hash__(self):
284
285
  return hash(tuple(field.__get__(self) for field in self._fields))
285
286
 
287
+ @meta_fn
286
288
  def __pos__(self) -> Self:
287
289
  """Return a copy of the record."""
288
290
  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
 
@@ -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:
sonolus/script/vec.py CHANGED
@@ -73,6 +73,18 @@ class Vec2(Record):
73
73
  """
74
74
  return cls(x=1, y=0)
75
75
 
76
+ @classmethod
77
+ def unit(cls, angle: Num) -> Self:
78
+ """Return a unit vector (magnitude 1) at a given angle in radians.
79
+
80
+ Args:
81
+ angle: The angle in radians.
82
+
83
+ Returns:
84
+ A new unit vector at the specified angle.
85
+ """
86
+ return Vec2(x=cos(angle), y=sin(angle))
87
+
76
88
  @property
77
89
  def magnitude(self) -> Num:
78
90
  """Calculate the magnitude (length) of the vector.
@@ -106,7 +118,7 @@ class Vec2(Record):
106
118
  """Rotate the vector by a given angle in radians and return a new vector.
107
119
 
108
120
  Args:
109
- angle: The angle to rotate the vector by, in radians.
121
+ angle: The angle to rotate the vector by, in radians. Positive angles rotate counterclockwise.
110
122
 
111
123
  Returns:
112
124
  A new vector rotated by the given angle.
@@ -120,7 +132,7 @@ class Vec2(Record):
120
132
  """Rotate the vector about a pivot by a given angle in radians and return a new vector.
121
133
 
122
134
  Args:
123
- angle: The angle to rotate the vector by, in radians.
135
+ angle: The angle to rotate the vector by, in radians. Positive angles rotate counterclockwise.
124
136
  pivot: The pivot point to rotate about.
125
137
 
126
138
  Returns:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -3,7 +3,7 @@ sonolus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  sonolus/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  sonolus/backend/blocks.py,sha256=3peyb9eYBy0s53xNVJ1KmK4IgoyVkkwG-lqDQ_VZTHc,18531
5
5
  sonolus/backend/excepthook.py,sha256=pqI9gtPBh0mlTgMNqul8bEVO1ARzKb8pNE9EN_CyDpk,994
6
- sonolus/backend/finalize.py,sha256=vNUGLCUDxNqXGxweNRA1NpRvtij7Tks9to1V_JTs7Jo,4020
6
+ sonolus/backend/finalize.py,sha256=Ab3Z-TdHhQXQ2KAMfQlDy2VYm4rjPpOZxbFonwQ-8Ic,4614
7
7
  sonolus/backend/interpret.py,sha256=B0jqlLmEGoyO2mxpcvwRwV17Tq_gOE9wLNt26Q5QOfs,14306
8
8
  sonolus/backend/ir.py,sha256=TCDLMvlX2S8emFDQwFVeD2OUC4fnhbrMObgYtoa_7PQ,2845
9
9
  sonolus/backend/mode.py,sha256=NkcPZJm8dn83LX35uP24MtQOCnfRDFZ280dHeEEfauE,613
@@ -11,7 +11,7 @@ sonolus/backend/node.py,sha256=eEzPP14jzWJp2xrZCAaPlNtokxdoqg0bSM7xQiwx1j8,1254
11
11
  sonolus/backend/ops.py,sha256=7DERBPU6z9Bz3i6UxyLdFTXYcCpvRZVNWw4ZQ-Djwnk,10510
12
12
  sonolus/backend/place.py,sha256=jABvLNNE-2pklTcb9WnyfHK8c-tYxn0ObsoLp5LYd5I,4703
13
13
  sonolus/backend/utils.py,sha256=BuJHI-qTVn-vYlQkSr4fSDPpgckkiO2RJmwlDXY_KzM,1818
14
- sonolus/backend/visitor.py,sha256=DyFXhhSMtpn0T0muQZz59pfbCMJtapz59CxctqRGxyk,49315
14
+ sonolus/backend/visitor.py,sha256=zANjR9PkUnWS5XnvA65OnYUSe54hNcad_nb1TFFTVMY,50213
15
15
  sonolus/backend/optimize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  sonolus/backend/optimize/allocate.py,sha256=-cpnCQ7S6wH3y-hCUGb2KT5TMuREifhsMXMI0-YtNjY,7380
17
17
  sonolus/backend/optimize/constant_evaluation.py,sha256=ozYB__haE2A_L2NBh2H3IT__Z65FBBtbjh6BJfQRC_A,15988
@@ -34,18 +34,18 @@ sonolus/build/level.py,sha256=AjvK4725nqDcg7oGn5kWocBdG-AcirXpku74T7c2epA,673
34
34
  sonolus/build/node.py,sha256=gnX71RYDUOK_gYMpinQi-bLWO4csqcfiG5gFmhxzSec,1330
35
35
  sonolus/build/project.py,sha256=DhNqgHnm73qKUOhrg1JPlWEL0Vg7VxcGUbNokpMWzVE,6315
36
36
  sonolus/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- sonolus/script/archetype.py,sha256=3pvTrQbVimNeU9EDO_2RVoqak3EF0D67OEnCcdaWz1w,40548
38
- sonolus/script/array.py,sha256=RxdZ-2Df-sYhiMT1xMsPJEGX1oCtDIE3TTntRsk56iA,11760
39
- sonolus/script/array_like.py,sha256=wFw_drg6aPk_17upAl2G2l-i5fE3rzaUtB8kYEkyJEk,8557
37
+ sonolus/script/archetype.py,sha256=rECMAhTbVyCuxOYO3gyuBle9oCT8W18EhsUz2U_8alQ,41417
38
+ sonolus/script/array.py,sha256=mq5-Im7PS0bJugBPI61D5M0gGZEUSbVcbOLORl-bfjM,11915
39
+ sonolus/script/array_like.py,sha256=2otrTauxlkyR0PltjNLVHQoq4HbW4m4iWq5ygEJklaY,9390
40
40
  sonolus/script/bucket.py,sha256=YFSyKS4ZngxerBlKwFBSCRAVewgQdwZ1-NqfPKcPZxI,7519
41
- sonolus/script/containers.py,sha256=PXwixFp1k1gqjp-gMfnlE1IadEfavFzAS99XH8kaw8Q,17640
41
+ sonolus/script/containers.py,sha256=-6QooG0vIXF3fmP97Lxada7Jklm6sXQFwkevGzVNo7s,18403
42
42
  sonolus/script/debug.py,sha256=JIMcGD6cQVWtIw3IIhq-5WenZZu0iArH6SAS1-XfR_8,5076
43
43
  sonolus/script/easing.py,sha256=7zaDKIfM_whUpb4FBz1DAF4NNG2vk_nDjl8kL2Y90aU,11396
44
44
  sonolus/script/effect.py,sha256=V9bJvMzs1O4C1PjTOKgsAXov-l4AnDb2h38-DzmeWpI,5838
45
45
  sonolus/script/engine.py,sha256=BhhQTrHuGAAAD6JPQ3R0jvHdimwW83PPghEIdAdtGMA,10683
46
46
  sonolus/script/globals.py,sha256=gUvSbxXW1ZOsyQF6qb5iMRnW-eRrg07rWOK1PAYRwsE,9741
47
47
  sonolus/script/instruction.py,sha256=PNfxC1dhT_hB0BxhDV3KXMn_kKxfI0t1iZmg8m6ddMU,6725
48
- sonolus/script/interval.py,sha256=IYNgJx0ngREnEVu_yMAu0MrA_Q7mZuToT8U3fbdb3Sc,9122
48
+ sonolus/script/interval.py,sha256=9UmHjK0fzEu3Qt6cnRviy93WKcbRlI7UG8Ez-Vz-gps,11185
49
49
  sonolus/script/iterator.py,sha256=wH6IM3ozLuMtCJZzhahIN3o2ykwUzDftU7ylbWo2UF8,4575
50
50
  sonolus/script/level.py,sha256=wR23xk-NOcW_JMRb3R12sqIXCLSZL-7cM3y7IpMF1J0,6333
51
51
  sonolus/script/metadata.py,sha256=ttRK27eojHf3So50KQJ-8yj3udZoN1bli5iD-knaeLw,753
@@ -55,22 +55,22 @@ sonolus/script/particle.py,sha256=oeVQF01xOeW2BEn04ZK1ZOP2HGvQzxBJCpITFjy9woQ,83
55
55
  sonolus/script/pointer.py,sha256=IH2_a0XE76uG_UyYM9jAYIf7qZ5LhUNc9ksXDIvAPZA,1511
56
56
  sonolus/script/printing.py,sha256=mNYu9QWiacBBGZrnePZQMVwbbguoelUps9GiOK_aVRU,2096
57
57
  sonolus/script/project.py,sha256=jLndgGJHdkqFYe-lDl_IzTjQ4gOSuy80en8WoSWXnB8,3340
58
- sonolus/script/quad.py,sha256=QgA4CdgLPGm2MtANKZPpxaM0qyh3DjxcXZi3D2QuLQs,10213
59
- sonolus/script/record.py,sha256=ZgQPmkA1GGoQ7aVpMB2xQ3ju4cyTru7nzEVl76P6dV0,12356
60
- sonolus/script/runtime.py,sha256=8UOedRhtnLQofChpApS-DHZh0VjEWBBktWsNaEubatQ,32245
58
+ sonolus/script/quad.py,sha256=wt0siM3SWPyQ2JEUP6sy_jVArZ62tLZjT-0ZOWPIcLw,11185
59
+ sonolus/script/record.py,sha256=o2GengeFlFU71WScql-75VgZCOKlq--IueHpZUBgo9s,12426
60
+ sonolus/script/runtime.py,sha256=XaS3op1BewSQchIffbJoz8WWw-prqk1PqBx2hdI3AMY,32971
61
61
  sonolus/script/sprite.py,sha256=CMcRAZ2hejXnaBmY2_n1_rj6hGOgPP5zEW-BpyaEVOY,16256
62
- sonolus/script/stream.py,sha256=S6ukJ251EHQaSYxuqqYa0BO_LGImYgNqd512rc_l_vA,24062
62
+ sonolus/script/stream.py,sha256=0Ikn5EUsFG301HORlF3n7tf5pq_nmQX-jBHycAC9pGM,24242
63
63
  sonolus/script/text.py,sha256=wxujIgKYcCfl2AD2_Im8g3vh0lDEHYwTSRZg9wsBPEU,13402
64
64
  sonolus/script/timing.py,sha256=ZR0ypV2PIoDCMHHGOMfCeezStCsBQdzomdqaz5VKex0,2981
65
- sonolus/script/transform.py,sha256=qtqh_Dpom698VrkKeMZLD3DjWtd3utkqx_nLrrIyWyY,20740
65
+ sonolus/script/transform.py,sha256=BKAokYC1EmyPcdlwapd5P1fA90LwVfJYrkDNDc4NHJ0,20904
66
66
  sonolus/script/ui.py,sha256=DYPGWIjHj1IFPxW1zaEuIUQx0b32FJPXtiwCvrtJ6oo,7528
67
67
  sonolus/script/values.py,sha256=woSW3we5OZZ1IaO3Qr0OAs_yuIpAzGw_IjwELKQHzv4,1469
68
- sonolus/script/vec.py,sha256=4ntfJ96zxKR-nVZluUgHd-Ud4vNfButfiv7EsroZxfE,7002
68
+ sonolus/script/vec.py,sha256=zA5aN9J63ihbiyYXvASIli-REVoIqWUac-STN6rtZVg,7404
69
69
  sonolus/script/internal/__init__.py,sha256=T6rzLoiOUaiSQtaHMZ88SNO-ijSjSSv33TKtUwu-Ms8,136
70
- sonolus/script/internal/builtin_impls.py,sha256=3e1jzDtyyB1QiCqkp4bRhLnnTMlviBi0QjKvhYwd3q8,8209
70
+ sonolus/script/internal/builtin_impls.py,sha256=VDeBpnLwu6v_r53qiXjgA1NF1aaBm9GTxP1XoWGkSLM,8569
71
71
  sonolus/script/internal/callbacks.py,sha256=vWzJG8uiJoEtsNnbeZPqOHogCwoLpz2D1MnHY2wVV8s,2801
72
72
  sonolus/script/internal/constant.py,sha256=k4kIBoy-c74UbfUR7GKzyTT6syhE-lSRJfw0nGuc8ZY,3904
73
- sonolus/script/internal/context.py,sha256=v6DbyqqhieSajcIYf__he4gFSB7UpCMljZaR3SYewzM,15473
73
+ sonolus/script/internal/context.py,sha256=aJUXAsL9KlO7i5jVvuWsbHkNi8o_YevtoWyzaUdIYp4,15561
74
74
  sonolus/script/internal/descriptor.py,sha256=XRFey-EjiAm_--KsNl-8N0Mi_iyQwlPh68gDp0pKf3E,392
75
75
  sonolus/script/internal/dict_impl.py,sha256=alu_wKGSk1kZajNf64qbe7t71shEzD4N5xNIATH8Swo,1885
76
76
  sonolus/script/internal/error.py,sha256=ZNnsvQVQAnFKzcvsm6-sste2lo-tP5pPI8sD7XlAZWc,490
@@ -80,13 +80,13 @@ sonolus/script/internal/introspection.py,sha256=SL2zaYjid0kkcj6ZbFLIwhgh7WKZBaAH
80
80
  sonolus/script/internal/math_impls.py,sha256=Xk7tLMnV2npzPJWtHlspONQHt09Gh2YLdHhAjx4jkdE,2320
81
81
  sonolus/script/internal/native.py,sha256=XKlNnWSJ-lxbwVGWhGj_CSSoWsVN18imqT5sAsDJT1w,1551
82
82
  sonolus/script/internal/random.py,sha256=6Ku5edRcDUh7rtqEEYCJz0BQavw69RALsVHS25z50pI,1695
83
- sonolus/script/internal/range.py,sha256=lrTanQFHU7RuQxSSPwDdoC30Y8FnHGxcP1Ahditu3zU,2297
83
+ sonolus/script/internal/range.py,sha256=rX4ixpy6vPEfT4HIAiMlAGKad50HjqL0StTCjqKBu-o,3370
84
84
  sonolus/script/internal/simulation_context.py,sha256=jUgNxCOEc_b99w1yFyVz0nVb6rgkEsyuCBvcYxq34Vk,4819
85
85
  sonolus/script/internal/transient.py,sha256=d6iYhM9f6DPUX5nkYQGm-x0b9XEfZUmB4AtUNnyhixo,1636
86
- sonolus/script/internal/tuple_impl.py,sha256=BVImJ5q8E9APMtSnHYnbFh6x8c36yktepewnnaQVWg4,3515
86
+ sonolus/script/internal/tuple_impl.py,sha256=DPNdmmRmupU8Ah4_XKq6-PdT336l4nt15_uCJKQGkkk,3587
87
87
  sonolus/script/internal/value.py,sha256=gQVNQD_xGpgrN4-UXFDmWRZCJCe8wPZ_wYv4QoPRJkM,5379
88
- sonolus_py-0.3.2.dist-info/METADATA,sha256=IcdTSXe5sjwmMh50kkhB2JZVSAU3GUUjqQzQFh_yGm0,302
89
- sonolus_py-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
90
- sonolus_py-0.3.2.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
91
- sonolus_py-0.3.2.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
92
- sonolus_py-0.3.2.dist-info/RECORD,,
88
+ sonolus_py-0.3.3.dist-info/METADATA,sha256=xdAh3JPnlG9kHnswSNxBFsPBk34G8uycm0sPS2N56Gw,302
89
+ sonolus_py-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
90
+ sonolus_py-0.3.3.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
91
+ sonolus_py-0.3.3.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
92
+ sonolus_py-0.3.3.dist-info/RECORD,,