sonolus.py 0.3.1__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.

Files changed (36) hide show
  1. sonolus/backend/finalize.py +16 -4
  2. sonolus/backend/node.py +13 -5
  3. sonolus/backend/optimize/allocate.py +41 -4
  4. sonolus/backend/optimize/flow.py +24 -7
  5. sonolus/backend/optimize/optimize.py +2 -9
  6. sonolus/backend/utils.py +6 -1
  7. sonolus/backend/visitor.py +72 -23
  8. sonolus/build/cli.py +6 -1
  9. sonolus/build/engine.py +1 -1
  10. sonolus/script/archetype.py +52 -24
  11. sonolus/script/array.py +20 -8
  12. sonolus/script/array_like.py +30 -3
  13. sonolus/script/containers.py +27 -7
  14. sonolus/script/debug.py +66 -8
  15. sonolus/script/globals.py +17 -0
  16. sonolus/script/internal/builtin_impls.py +12 -8
  17. sonolus/script/internal/context.py +55 -1
  18. sonolus/script/internal/range.py +25 -2
  19. sonolus/script/internal/simulation_context.py +131 -0
  20. sonolus/script/internal/tuple_impl.py +18 -11
  21. sonolus/script/interval.py +60 -2
  22. sonolus/script/iterator.py +3 -2
  23. sonolus/script/num.py +11 -2
  24. sonolus/script/options.py +24 -1
  25. sonolus/script/quad.py +41 -3
  26. sonolus/script/record.py +24 -3
  27. sonolus/script/runtime.py +411 -0
  28. sonolus/script/stream.py +133 -16
  29. sonolus/script/transform.py +291 -2
  30. sonolus/script/values.py +9 -3
  31. sonolus/script/vec.py +14 -2
  32. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/METADATA +1 -1
  33. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/RECORD +36 -35
  34. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/WHEEL +0 -0
  35. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/entry_points.txt +0 -0
  36. {sonolus_py-0.3.1.dist-info → sonolus_py-0.3.3.dist-info}/licenses/LICENSE +0 -0
sonolus/script/stream.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from math import inf
3
4
  from typing import cast, dataclass_transform
4
5
 
5
- from sonolus.backend.ir import IRExpr, IRInstr, IRPureInstr, IRConst
6
+ from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr
6
7
  from sonolus.backend.mode import Mode
7
8
  from sonolus.backend.ops import Op
8
9
  from sonolus.script.internal.context import ctx
@@ -14,6 +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
18
+ from sonolus.script.runtime import prev_time, time
17
19
  from sonolus.script.values import sizeof
18
20
 
19
21
 
@@ -174,10 +176,11 @@ class _SparseStreamBacking(BackingValue):
174
176
  )
175
177
 
176
178
 
177
- class Stream[T](Record, BackingValue):
179
+ class Stream[T](Record):
178
180
  """Represents a stream.
179
181
 
180
- Most users should use `@stream` to declare streams and stream groups rather than using this class directly.
182
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare streams and stream groups rather than
183
+ using this class directly.
181
184
 
182
185
  If used directly, it is important that streams do not overlap. No other streams should have an offset in
183
186
  `range(self.offset, self.offset + max(1, sizeof(self.element_type())))`, or they will overlap and interfere
@@ -248,6 +251,12 @@ class Stream[T](Record, BackingValue):
248
251
  _check_can_read_stream()
249
252
  return _stream_get_next_key(self.offset, key)
250
253
 
254
+ def next_key_or_default(self, key: int | float, default: int | float) -> int:
255
+ """Get the next key, or the default value if there is no next key."""
256
+ _check_can_read_stream()
257
+ next_key = self.next_key(key)
258
+ return next_key if next_key > key else default
259
+
251
260
  def previous_key(self, key: int | float) -> int:
252
261
  """Get the previous key, or the key unchanged if it is the first key or the stream is empty.
253
262
 
@@ -256,6 +265,12 @@ class Stream[T](Record, BackingValue):
256
265
  _check_can_read_stream()
257
266
  return _stream_get_previous_key(self.offset, key)
258
267
 
268
+ def previous_key_or_default(self, key: int | float, default: int | float) -> int:
269
+ """Get the previous key, or the default value if there is no previous key."""
270
+ _check_can_read_stream()
271
+ previous_key = self.previous_key(key)
272
+ return previous_key if previous_key < key else default
273
+
259
274
  def has_next_key(self, key: int | float) -> bool:
260
275
  """Check if there is a next key after the given key in the stream."""
261
276
  _check_can_read_stream()
@@ -302,6 +317,14 @@ class Stream[T](Record, BackingValue):
302
317
  _check_can_read_stream()
303
318
  return self[self.next_key_inclusive(key)]
304
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
+
305
328
  def iter_items_from(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
306
329
  """Iterate over the items in the stream in ascending order starting from the given key.
307
330
 
@@ -317,6 +340,22 @@ class Stream[T](Record, BackingValue):
317
340
  _check_can_read_stream()
318
341
  return _StreamAscIterator(self, self.next_key_inclusive(start))
319
342
 
343
+ def iter_items_since_previous_frame(self) -> SonolusIterator[tuple[int | float, T]]:
344
+ """Iterate over the items in the stream since the last frame.
345
+
346
+ This is a convenience method that iterates over the items in the stream occurring after the time of the
347
+ previous frame and up to and including the current time.
348
+
349
+ Usage:
350
+ ```python
351
+ stream = ...
352
+ for key, value in stream.iter_items_since_previous_frame():
353
+ do_something(key, value)
354
+ ```
355
+ """
356
+ _check_can_read_stream()
357
+ return _StreamBoundedAscIterator(self, self.next_key(prev_time()), time())
358
+
320
359
  def iter_items_from_desc(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
321
360
  """Iterate over the items in the stream in descending order starting from the given key.
322
361
 
@@ -347,6 +386,22 @@ class Stream[T](Record, BackingValue):
347
386
  _check_can_read_stream()
348
387
  return _StreamAscKeyIterator(self, self.next_key_inclusive(start))
349
388
 
389
+ def iter_keys_since_previous_frame(self) -> SonolusIterator[int | float]:
390
+ """Iterate over the keys in the stream since the last frame.
391
+
392
+ This is a convenience method that iterates over the keys in the stream occurring after the time of the
393
+ previous frame and up to and including the current time.
394
+
395
+ Usage:
396
+ ```python
397
+ stream = ...
398
+ for key in stream.iter_keys_since_previous_frame():
399
+ do_something(key)
400
+ ```
401
+ """
402
+ _check_can_read_stream()
403
+ return _StreamBoundedAscKeyIterator(self, self.next_key(prev_time()), time())
404
+
350
405
  def iter_keys_from_desc(self, start: int | float, /) -> SonolusIterator[int | float]:
351
406
  """Iterate over the keys in the stream in descending order starting from the given key.
352
407
 
@@ -377,6 +432,22 @@ class Stream[T](Record, BackingValue):
377
432
  _check_can_read_stream()
378
433
  return _StreamAscValueIterator(self, self.next_key_inclusive(start))
379
434
 
435
+ def iter_values_since_previous_frame(self) -> SonolusIterator[T]:
436
+ """Iterate over the values in the stream since the last frame.
437
+
438
+ This is a convenience method that iterates over the values in the stream occurring after the time of the
439
+ previous frame and up to and including the current time.
440
+
441
+ Usage:
442
+ ```python
443
+ stream = ...
444
+ for value in stream.iter_values_since_previous_frame():
445
+ do_something(value)
446
+ ```
447
+ """
448
+ _check_can_read_stream()
449
+ return _StreamBoundedAscValueIterator(self, self.next_key(prev_time()), time())
450
+
380
451
  def iter_values_from_desc(self, start: int | float, /) -> SonolusIterator[T]:
381
452
  """Iterate over the values in the stream in descending order starting from the given key.
382
453
 
@@ -396,7 +467,8 @@ class Stream[T](Record, BackingValue):
396
467
  class StreamGroup[T, Size](Record):
397
468
  """Represents a group of streams.
398
469
 
399
- Most users should use `@stream` to declare stream groups rather than using this class directly.
470
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare stream groups rather than using this
471
+ class directly.
400
472
 
401
473
  Usage:
402
474
  Declaring a stream group:
@@ -444,13 +516,28 @@ class _StreamAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
444
516
  current_key: int | float
445
517
 
446
518
  def has_next(self) -> bool:
447
- return self.stream.next_key(self.current_key) > self.current_key
519
+ return self.current_key in self.stream
520
+
521
+ def get(self) -> tuple[int | float, T]:
522
+ return self.current_key, self.stream[self.current_key]
523
+
524
+ def advance(self):
525
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
526
+
527
+
528
+ class _StreamBoundedAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
529
+ stream: Stream[T]
530
+ current_key: int | float
531
+ end_key: int | float
532
+
533
+ def has_next(self) -> bool:
534
+ return self.current_key in self.stream and self.current_key <= self.end_key
448
535
 
449
536
  def get(self) -> tuple[int | float, T]:
450
537
  return self.current_key, self.stream[self.current_key]
451
538
 
452
539
  def advance(self):
453
- self.current_key = self.stream.next_key(self.current_key)
540
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
454
541
 
455
542
 
456
543
  class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
@@ -458,13 +545,13 @@ class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
458
545
  current_key: int | float
459
546
 
460
547
  def has_next(self) -> bool:
461
- return self.stream.previous_key(self.current_key) < self.current_key
548
+ return self.current_key in self.stream
462
549
 
463
550
  def get(self) -> tuple[int | float, T]:
464
551
  return self.current_key, self.stream[self.current_key]
465
552
 
466
553
  def advance(self):
467
- self.current_key = self.stream.previous_key(self.current_key)
554
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
468
555
 
469
556
 
470
557
  class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -472,13 +559,28 @@ class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
472
559
  current_key: int | float
473
560
 
474
561
  def has_next(self) -> bool:
475
- return self.stream.next_key(self.current_key) > self.current_key
562
+ return self.current_key in self.stream
563
+
564
+ def get(self) -> int | float:
565
+ return self.current_key
566
+
567
+ def advance(self):
568
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
569
+
570
+
571
+ class _StreamBoundedAscKeyIterator[T](Record, SonolusIterator[int | float]):
572
+ stream: Stream[T]
573
+ current_key: int | float
574
+ end_key: int | float
575
+
576
+ def has_next(self) -> bool:
577
+ return self.current_key in self.stream and self.current_key <= self.end_key
476
578
 
477
579
  def get(self) -> int | float:
478
580
  return self.current_key
479
581
 
480
582
  def advance(self):
481
- self.current_key = self.stream.next_key(self.current_key)
583
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
482
584
 
483
585
 
484
586
  class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -486,13 +588,13 @@ class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
486
588
  current_key: int | float
487
589
 
488
590
  def has_next(self) -> bool:
489
- return self.stream.previous_key(self.current_key) < self.current_key
591
+ return self.current_key in self.stream
490
592
 
491
593
  def get(self) -> int | float:
492
594
  return self.current_key
493
595
 
494
596
  def advance(self):
495
- self.current_key = self.stream.previous_key(self.current_key)
597
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
496
598
 
497
599
 
498
600
  class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
@@ -500,13 +602,28 @@ class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
500
602
  current_key: int | float
501
603
 
502
604
  def has_next(self) -> bool:
503
- return self.stream.next_key(self.current_key) > self.current_key
605
+ return self.current_key in self.stream
606
+
607
+ def get(self) -> T:
608
+ return self.stream[self.current_key]
609
+
610
+ def advance(self):
611
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
612
+
613
+
614
+ class _StreamBoundedAscValueIterator[T](Record, SonolusIterator[T]):
615
+ stream: Stream[T]
616
+ current_key: int | float
617
+ end_key: int | float
618
+
619
+ def has_next(self) -> bool:
620
+ return self.current_key in self.stream and self.current_key <= self.end_key
504
621
 
505
622
  def get(self) -> T:
506
623
  return self.stream[self.current_key]
507
624
 
508
625
  def advance(self):
509
- self.current_key = self.stream.next_key(self.current_key)
626
+ self.current_key = self.stream.next_key_or_default(self.current_key, inf)
510
627
 
511
628
 
512
629
  class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
@@ -514,13 +631,13 @@ class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
514
631
  current_key: int | float
515
632
 
516
633
  def has_next(self) -> bool:
517
- return self.stream.previous_key(self.current_key) < self.current_key
634
+ return self.current_key in self.stream
518
635
 
519
636
  def get(self) -> T:
520
637
  return self.stream[self.current_key]
521
638
 
522
639
  def advance(self):
523
- self.current_key = self.stream.previous_key(self.current_key)
640
+ self.current_key = self.stream.previous_key_or_default(self.current_key, -inf)
524
641
 
525
642
 
526
643
  @native_function(Op.StreamGetNextKey)
@@ -1,6 +1,7 @@
1
1
  from math import cos, sin
2
2
  from typing import Self
3
3
 
4
+ from sonolus.script.interval import lerp, remap
4
5
  from sonolus.script.quad import Quad, QuadLike
5
6
  from sonolus.script.record import Record
6
7
  from sonolus.script.vec import Vec2
@@ -136,7 +137,7 @@ class Transform2d(Record):
136
137
  """Rotate about the origin and return a new transform.
137
138
 
138
139
  Args:
139
- angle: The angle of rotation in radians.
140
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
140
141
 
141
142
  Returns:
142
143
  A new transform after rotation.
@@ -159,7 +160,7 @@ class Transform2d(Record):
159
160
  """Rotate about the pivot and return a new transform.
160
161
 
161
162
  Args:
162
- angle: The angle of rotation in radians.
163
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
163
164
  pivot: The pivot point for rotation.
164
165
 
165
166
  Returns:
@@ -396,3 +397,291 @@ class Transform2d(Record):
396
397
  tl=self.transform_vec(quad.tl),
397
398
  tr=self.transform_vec(quad.tr),
398
399
  )
400
+
401
+
402
+ class InvertibleTransform2d(Record):
403
+ """A transformation matrix for 2D points that can be inverted.
404
+
405
+ Usage:
406
+ ```python
407
+ InvertibleTransform2d.new()
408
+ ```
409
+ """
410
+
411
+ forward: Transform2d
412
+ inverse: Transform2d
413
+
414
+ @classmethod
415
+ def new(cls) -> Self:
416
+ """Create a new identity transform.
417
+
418
+ Returns:
419
+ A new identity transform.
420
+ """
421
+ return cls(
422
+ forward=Transform2d.new(),
423
+ inverse=Transform2d.new(),
424
+ )
425
+
426
+ def translate(self, translation: Vec2, /) -> Self:
427
+ """Translate along the x and y axes and return a new transform.
428
+
429
+ Args:
430
+ translation: The translation vector.
431
+
432
+ Returns:
433
+ A new invertible transform after translation.
434
+ """
435
+ return InvertibleTransform2d(
436
+ forward=self.forward.translate(translation),
437
+ inverse=Transform2d.new().translate(-translation).compose(self.inverse),
438
+ )
439
+
440
+ def scale(self, factor: Vec2, /) -> Self:
441
+ """Scale about the origin and return a new transform.
442
+
443
+ Args:
444
+ factor: The scale factor vector.
445
+
446
+ Returns:
447
+ A new invertible transform after scaling.
448
+ """
449
+ return InvertibleTransform2d(
450
+ forward=self.forward.scale(factor),
451
+ inverse=Transform2d.new().scale(Vec2.one() / factor).compose(self.inverse),
452
+ )
453
+
454
+ def scale_about(self, factor: Vec2, /, pivot: Vec2) -> Self:
455
+ """Scale about the pivot and return a new transform.
456
+
457
+ Args:
458
+ factor: The scale factor vector.
459
+ pivot: The pivot point for scaling.
460
+
461
+ Returns:
462
+ A new invertible transform after scaling.
463
+ """
464
+ return InvertibleTransform2d(
465
+ forward=self.forward.scale_about(factor, pivot),
466
+ inverse=Transform2d.new().scale_about(Vec2.one() / factor, pivot).compose(self.inverse),
467
+ )
468
+
469
+ def rotate(self, angle: float, /) -> Self:
470
+ """Rotate about the origin and return a new transform.
471
+
472
+ Args:
473
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
474
+
475
+ Returns:
476
+ A new invertible transform after rotation.
477
+ """
478
+ return InvertibleTransform2d(
479
+ forward=self.forward.rotate(angle),
480
+ inverse=Transform2d.new().rotate(-angle).compose(self.inverse),
481
+ )
482
+
483
+ def rotate_about(self, angle: float, /, pivot: Vec2) -> Self:
484
+ """Rotate about the pivot and return a new transform.
485
+
486
+ Args:
487
+ angle: The angle of rotation in radians. Positive angles rotate counterclockwise.
488
+ pivot: The pivot point for rotation.
489
+
490
+ Returns:
491
+ A new invertible transform after rotation.
492
+ """
493
+ return InvertibleTransform2d(
494
+ forward=self.forward.rotate_about(angle, pivot),
495
+ inverse=Transform2d.new().rotate_about(-angle, pivot).compose(self.inverse),
496
+ )
497
+
498
+ def shear_x(self, m: float, /) -> Self:
499
+ """Shear along the x-axis and return a new transform.
500
+
501
+ Args:
502
+ m: The shear factor along the x-axis.
503
+
504
+ Returns:
505
+ A new invertible transform after shearing.
506
+ """
507
+ return InvertibleTransform2d(
508
+ forward=self.forward.shear_x(m),
509
+ inverse=Transform2d.new().shear_x(-m).compose(self.inverse),
510
+ )
511
+
512
+ def shear_y(self, m: float, /) -> Self:
513
+ """Shear along the y-axis and return a new transform.
514
+
515
+ Args:
516
+ m: The shear factor along the y-axis.
517
+
518
+ Returns:
519
+ A new invertible transform after shearing.
520
+ """
521
+ return InvertibleTransform2d(
522
+ forward=self.forward.shear_y(m),
523
+ inverse=Transform2d.new().shear_y(-m).compose(self.inverse),
524
+ )
525
+
526
+ def simple_perspective_x(self, x: float, /) -> Self:
527
+ """Apply perspective along the x-axis with vanishing point at the given x coordinate and return a new transform.
528
+
529
+ Args:
530
+ x: The x coordinate of the vanishing point.
531
+
532
+ Returns:
533
+ A new invertible transform after applying perspective.
534
+ """
535
+ return InvertibleTransform2d(
536
+ forward=self.forward.simple_perspective_x(x),
537
+ inverse=Transform2d.new().simple_perspective_x(-x).compose(self.inverse),
538
+ )
539
+
540
+ def simple_perspective_y(self, y: float, /) -> Self:
541
+ """Apply perspective along the y-axis with vanishing point at the given y coordinate and return a new transform.
542
+
543
+ Args:
544
+ y: The y coordinate of the vanishing point.
545
+
546
+ Returns:
547
+ A new invertible transform after applying perspective.
548
+ """
549
+ return InvertibleTransform2d(
550
+ forward=self.forward.simple_perspective_y(y),
551
+ inverse=Transform2d.new().simple_perspective_y(-y).compose(self.inverse),
552
+ )
553
+
554
+ def perspective_x(self, foreground_x: float, vanishing_point: Vec2, /) -> Self:
555
+ """Apply a perspective transformation along the x-axis and return a new transform.
556
+
557
+ Args:
558
+ foreground_x: The foreground x-coordinate.
559
+ vanishing_point: The vanishing point vector.
560
+
561
+ Returns:
562
+ A new invertible transform after applying perspective.
563
+ """
564
+ return InvertibleTransform2d(
565
+ forward=self.forward.perspective_x(foreground_x, vanishing_point),
566
+ inverse=Transform2d.new().inverse_perspective_x(foreground_x, vanishing_point).compose(self.inverse),
567
+ )
568
+
569
+ def perspective_y(self, foreground_y: float, vanishing_point: Vec2, /) -> Self:
570
+ """Apply a perspective transformation along the y-axis and return a new transform.
571
+
572
+ Args:
573
+ foreground_y: The foreground y-coordinate.
574
+ vanishing_point: The vanishing point vector.
575
+
576
+ Returns:
577
+ A new invertible transform after applying perspective.
578
+ """
579
+ return InvertibleTransform2d(
580
+ forward=self.forward.perspective_y(foreground_y, vanishing_point),
581
+ inverse=Transform2d.new().inverse_perspective_y(foreground_y, vanishing_point).compose(self.inverse),
582
+ )
583
+
584
+ def normalize(self) -> Self:
585
+ """Normalize the transform to have a 1 in the bottom right corner and return a new transform.
586
+
587
+ This may fail in some special cases involving perspective transformations where the bottom right corner is 0.
588
+
589
+ Returns:
590
+ A new normalized invertible transform.
591
+ """
592
+ return InvertibleTransform2d(
593
+ forward=self.forward.normalize(),
594
+ inverse=self.inverse.normalize(),
595
+ )
596
+
597
+ def compose(self, other: Self, /) -> Self:
598
+ """Compose with another invertible transform which is applied after this transform and return a new transform.
599
+
600
+ Args:
601
+ other: The other invertible transform to compose with.
602
+
603
+ Returns:
604
+ A new invertible transform resulting from the composition.
605
+ """
606
+ return InvertibleTransform2d(
607
+ forward=self.forward.compose(other.forward),
608
+ inverse=other.inverse.compose(self.inverse),
609
+ )
610
+
611
+ def compose_before(self, other: Self, /) -> Self:
612
+ """Compose with another invertible transform which is applied before this transform and return a new transform.
613
+
614
+ Args:
615
+ other: The other invertible transform to compose with.
616
+
617
+ Returns:
618
+ A new invertible transform resulting from the composition.
619
+ """
620
+ return other.compose(self)
621
+
622
+ def transform_vec(self, v: Vec2) -> Vec2:
623
+ """Transform a [`Vec2`][sonolus.script.vec.Vec2] and return a new [`Vec2`][sonolus.script.vec.Vec2].
624
+
625
+ Args:
626
+ v: The vector to transform.
627
+
628
+ Returns:
629
+ A new transformed vector.
630
+ """
631
+ return self.forward.transform_vec(v)
632
+
633
+ def inverse_transform_vec(self, v: Vec2) -> Vec2:
634
+ """Inverse transform a [`Vec2`][sonolus.script.vec.Vec2] and return a new [`Vec2`][sonolus.script.vec.Vec2].
635
+
636
+ Args:
637
+ v: The vector to inverse transform.
638
+
639
+ Returns:
640
+ A new inverse transformed vector.
641
+ """
642
+ return self.inverse.transform_vec(v)
643
+
644
+ def transform_quad(self, quad: QuadLike) -> Quad:
645
+ """Transform a [`Quad`][sonolus.script.quad.Quad] and return a new [`Quad`][sonolus.script.quad.Quad].
646
+
647
+ Args:
648
+ quad: The quad to transform.
649
+
650
+ Returns:
651
+ A new transformed quad.
652
+ """
653
+ return self.forward.transform_quad(quad)
654
+
655
+ def inverse_transform_quad(self, quad: QuadLike) -> Quad:
656
+ """Inverse transform a [`Quad`][sonolus.script.quad.Quad] and return a new [`Quad`][sonolus.script.quad.Quad].
657
+
658
+ Args:
659
+ quad: The quad to inverse transform.
660
+
661
+ Returns:
662
+ A new inverse transformed quad.
663
+ """
664
+ return self.inverse.transform_quad(quad)
665
+
666
+
667
+ def perspective_approach(
668
+ distance_ratio: float,
669
+ progress: float,
670
+ ) -> float:
671
+ """Calculate the perspective correct approach curve given the initial distance, target distance, and progress.
672
+
673
+ For typical engines with stage tilt, distance_ratio is the displayed width of a lane at the judge line divided
674
+ by the displayed width of a lane at note spawn. For flat stages, this will be 1.0, and this function would simply
675
+ return progress unchanged.
676
+
677
+ Args:
678
+ distance_ratio: The ratio of the distance at note spawn to the distance at the judge line.
679
+ progress: The progress value, where 0 corresponds to note spawn and 1 corresponds to the judge line.
680
+
681
+ Returns:
682
+ The perspective-corrected progress value.
683
+ """
684
+ d_0 = distance_ratio
685
+ d_1 = 1.0
686
+ d = max(lerp(d_0, d_1, progress), 1e-6) # Avoid a zero or negative distance
687
+ return remap(1 / d_0, 1 / d_1, 0, 1, 1 / d)
sonolus/script/values.py CHANGED
@@ -19,13 +19,19 @@ def alloc[T](type_: type[T]) -> T:
19
19
 
20
20
  @meta_fn
21
21
  def zeros[T](type_: type[T]) -> T:
22
- """Make a new instance of the given type initialized with zeros."""
22
+ """Make a new instance of the given type initialized with zeros.
23
+
24
+ Generally works the same as the unary `+` operator on record and array types.
25
+ """
23
26
  return validate_concrete_type(type_)._zero_()
24
27
 
25
28
 
26
29
  @meta_fn
27
30
  def copy[T](value: T) -> T:
28
- """Make a deep copy of the given value."""
31
+ """Make a deep copy of the given value.
32
+
33
+ Generally works the same as the unary `+` operator on records and arrays.
34
+ """
29
35
  value = validate_value(value)
30
36
  if ctx():
31
37
  return value._copy_()
@@ -34,7 +40,7 @@ def copy[T](value: T) -> T:
34
40
 
35
41
 
36
42
  def swap[T](a: T, b: T):
37
- """Swap the values of the two given arguments."""
43
+ """Swap the values of the two provided mutable values."""
38
44
  temp = copy(a)
39
45
  a @= b
40
46
  b @= temp
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.1
3
+ Version: 0.3.3
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12