sonolus.py 0.10.6__py3-none-any.whl → 0.10.7__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,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Self
3
+ from collections.abc import Callable
4
+ from typing import Any, Protocol, Self
4
5
 
6
+ from sonolus.script.archetype import AnyArchetype, EntityRef
5
7
  from sonolus.script.array import Array
6
8
  from sonolus.script.array_like import ArrayLike, get_positive_index
7
9
  from sonolus.script.debug import error
@@ -643,3 +645,255 @@ class _ArrayMapEntryIterator[K, V, Capacity](Record, SonolusIterator):
643
645
  self._index += 1
644
646
  return Some(result)
645
647
  return Nothing
648
+
649
+
650
+ class _LinkedListNodeRef[TKey, TValue](Protocol):
651
+ def get_value(self) -> TValue: ...
652
+
653
+ def get_next(self) -> Self: ...
654
+
655
+ def set_next(self, next_node: Self): ...
656
+
657
+ def set_prev(self, prev_node: Self):
658
+ # No-op for singly linked lists
659
+ return
660
+
661
+ def is_present(self) -> bool: ...
662
+
663
+ def set(self, other: Self): ...
664
+
665
+ def copy(self) -> Self: ...
666
+
667
+ def empty(self) -> Self: ...
668
+
669
+
670
+ def _merge_linked_list_nodes[TNode: _LinkedListNodeRef](
671
+ a: TNode,
672
+ b: TNode,
673
+ ) -> TNode:
674
+ head = a.empty()
675
+ tail = a.empty()
676
+ left = a.copy()
677
+ right = b.copy()
678
+
679
+ while left.is_present() and right.is_present():
680
+ if left.get_value() <= right.get_value():
681
+ if not head.is_present():
682
+ head.set(left)
683
+ tail.set(left)
684
+ else:
685
+ tail.set_next(left)
686
+ tail.set(left)
687
+ left.set(left.get_next())
688
+ else:
689
+ if not head.is_present():
690
+ head.set(right)
691
+ tail.set(right)
692
+ else:
693
+ tail.set_next(right)
694
+ tail.set(right)
695
+ right.set(right.get_next())
696
+
697
+ while left.is_present():
698
+ if not head.is_present():
699
+ head.set(left)
700
+ tail.set(left)
701
+ else:
702
+ tail.set_next(left)
703
+ tail.set(left)
704
+ left.set(left.get_next())
705
+
706
+ while right.is_present():
707
+ if not head.is_present():
708
+ head.set(right)
709
+ tail.set(right)
710
+ else:
711
+ tail.set_next(right)
712
+ tail.set(right)
713
+ right.set(right.get_next())
714
+
715
+ if tail.is_present():
716
+ tail.set_next(a.empty())
717
+
718
+ return head
719
+
720
+
721
+ def _merge_sort_linked_list_nodes[TNode: _LinkedListNodeRef](
722
+ head: TNode,
723
+ ) -> TNode:
724
+ # Calculate length
725
+ length = 0
726
+ node = head.copy()
727
+ while node.is_present():
728
+ length += 1
729
+ node.set(node.get_next())
730
+
731
+ # Trivial case
732
+ if length <= 1:
733
+ return head
734
+
735
+ # Bottom-up merge sort: start with sublists of size 1, then 2, 4, 8, etc.
736
+ size = 1
737
+ while size < length:
738
+ current = head.copy()
739
+ new_head = head.empty()
740
+ new_tail = head.empty()
741
+
742
+ # Process all pairs of sublists of the current size
743
+ while current.is_present():
744
+ # Extract the first sublist
745
+ left = current.copy()
746
+ prev = current.empty()
747
+ i = 0
748
+ while i < size and current.is_present():
749
+ prev.set(current)
750
+ current.set(current.get_next())
751
+ i += 1
752
+ if prev.is_present():
753
+ prev.set_next(prev.empty())
754
+
755
+ # We've made it to the end without a second sublist to merge, so just attach it to the end
756
+ if not current.is_present():
757
+ # Since size < length, we know a full iteration must have happened already, so new_tail is valid
758
+ new_tail.set_next(left)
759
+ break
760
+
761
+ # Extract the second sublist
762
+ right = current.copy()
763
+ prev = current.empty()
764
+ i = 0
765
+ while i < size and current.is_present():
766
+ prev.set(current)
767
+ current.set(current.get_next())
768
+ i += 1
769
+ if prev.is_present():
770
+ prev.set_next(prev.empty())
771
+
772
+ merged = _merge_linked_list_nodes(left, right)
773
+
774
+ # Append the merged result
775
+ if not new_head.is_present():
776
+ new_head.set(merged)
777
+ new_tail.set(merged)
778
+ else:
779
+ new_tail.set_next(merged)
780
+
781
+ # Move tail to the end of the merged section
782
+ while new_tail.get_next().is_present():
783
+ new_tail.set(new_tail.get_next())
784
+
785
+ # Update head for the next iteration
786
+ head.set(new_head)
787
+ size *= 2
788
+
789
+ return head
790
+
791
+
792
+ class _EntityNodeRef[Archetype, GetValue, GetNextRef, GetPrevRef](Record):
793
+ index: int
794
+
795
+ def get_value(self) -> Any:
796
+ return self.get_value_fn(self.archetype.at(self.index))
797
+
798
+ def get_next(self) -> _EntityNodeRef:
799
+ next_ref = self.get_next_ref_fn(self.archetype.at(self.index))
800
+ return self.with_index(next_ref.index)
801
+
802
+ def set_next(self, next_node: _EntityNodeRef):
803
+ entity = self.archetype.at(self.index)
804
+ next_ref = self.get_next_ref_fn(entity)
805
+ next_ref.index = next_node.index
806
+
807
+ def set_prev(self, prev_node: _EntityNodeRef):
808
+ if self.get_prev_ref_fn is not None:
809
+ entity = self.archetype.at(self.index)
810
+ prev_ref = self.get_prev_ref_fn(entity)
811
+ prev_ref.index = prev_node.index
812
+
813
+ def is_present(self) -> bool:
814
+ return self.index > 0
815
+
816
+ def set(self, other: _EntityNodeRef):
817
+ self.index = other.index
818
+
819
+ def copy(self) -> _EntityNodeRef:
820
+ return self.with_index(self.index)
821
+
822
+ def empty(self) -> _EntityNodeRef:
823
+ return self.with_index(0)
824
+
825
+ def with_index(self, index: int) -> _EntityNodeRef:
826
+ return _EntityNodeRef[
827
+ self.archetype,
828
+ self.get_value_fn,
829
+ self.get_next_ref_fn,
830
+ self.get_prev_ref_fn,
831
+ ](index)
832
+
833
+ @property
834
+ def archetype(self):
835
+ return self.type_var_value(Archetype)
836
+
837
+ @property
838
+ def get_value_fn(self):
839
+ return self.type_var_value(GetValue)
840
+
841
+ @property
842
+ def get_next_ref_fn(self):
843
+ return self.type_var_value(GetNextRef)
844
+
845
+ @property
846
+ def get_prev_ref_fn(self):
847
+ return self.type_var_value(GetPrevRef)
848
+
849
+
850
+ def sort_linked_entities[T: AnyArchetype](
851
+ head_ref: EntityRef[T],
852
+ /,
853
+ *,
854
+ get_value: Callable[[T], Any],
855
+ get_next_ref: Callable[[T], EntityRef[T]],
856
+ get_prev_ref: Callable[[T], EntityRef[T]] | None = None,
857
+ ) -> EntityRef[T]:
858
+ """Sort a linked list of entities using merge sort.
859
+
860
+ If get_prev_ref is provided, the backward links will be updated as well.
861
+
862
+ Usage:
863
+ ```python
864
+ class MyArchetype(PlayArchetype):
865
+ sort_key: int
866
+ next: EntityRef[MyArchetype]
867
+
868
+ def sort_my_archetype(head: EntityRef[MyArchetype]) -> EntityRef[MyArchetype]:
869
+ return sort_linked_entities(
870
+ head,
871
+ get_value=lambda e: e.sort_key,
872
+ get_next_ref=lambda e: e.next,
873
+ )
874
+ ```
875
+
876
+ Args:
877
+ head_ref: A reference to the head of the linked list.
878
+ get_value: A function that takes an entity and returns the value to sort by.
879
+ get_next_ref: A function that takes an entity and returns a reference to the next entity.
880
+ get_prev_ref: An optional function that takes an entity and returns a reference to the previous entity.
881
+
882
+ Returns:
883
+ A reference to the head of the sorted linked list.
884
+ """
885
+ archetype = head_ref.archetype()
886
+
887
+ sorted_head_index = _merge_sort_linked_list_nodes(
888
+ _EntityNodeRef[archetype, get_value, get_next_ref, get_prev_ref](head_ref.index)
889
+ ).index
890
+
891
+ if get_prev_ref is not None:
892
+ current_ref = _EntityNodeRef[archetype, get_value, get_next_ref, get_prev_ref](sorted_head_index)
893
+ prev_ref = current_ref.empty()
894
+ while current_ref.is_present():
895
+ current_ref.set_prev(prev_ref)
896
+ prev_ref.set(current_ref)
897
+ current_ref.set(current_ref.get_next())
898
+
899
+ return EntityRef[archetype](sorted_head_index)
@@ -388,6 +388,11 @@ def _super(*args):
388
388
  return super(*(arg._as_py_() if arg._is_py_() else arg for arg in args))
389
389
 
390
390
 
391
+ @meta_fn
392
+ def _type(value):
393
+ return type(value)
394
+
395
+
391
396
  @meta_fn
392
397
  def _assert_never(arg: Never, /):
393
398
  error("Expected code to be unreachable")
@@ -416,6 +421,7 @@ BUILTIN_IMPLS = {
416
421
  id(range): Range,
417
422
  id(reversed): _reversed,
418
423
  id(super): _super,
424
+ id(type): _type,
419
425
  id(zip): _zip,
420
426
  id(assert_never): _assert_never,
421
427
  **MATH_BUILTIN_IMPLS, # Includes round
sonolus/script/vec.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from math import pi
4
+
3
5
  from sonolus.script.array import Array
4
6
  from sonolus.script.array_like import ArrayLike
5
7
  from sonolus.script.debug import assert_true
@@ -285,3 +287,22 @@ def pnpoly(vertices: ArrayLike[Vec2] | tuple[Vec2, ...], test: Vec2) -> bool:
285
287
  j = i
286
288
  i += 1
287
289
  return c
290
+
291
+
292
+ def angle_diff(a: float, b: float, /) -> float:
293
+ """Return the smallest absolute difference between two angles in radians.
294
+
295
+ The result is in the range [0, π].
296
+ """
297
+ return abs((a - b + pi) % (2 * pi) - pi)
298
+
299
+
300
+ def signed_angle_diff(a: float, b: float, /) -> float:
301
+ """Return the signed smallest difference between two angles in radians.
302
+
303
+ The result is in the range [-π, π). A positive result means a is counter-clockwise from b.
304
+ A negative result means a is clockwise from b.
305
+
306
+ If the two angles are exactly opposite, the result will be -π, but this should not be relied upon.
307
+ """
308
+ return (a - b + pi) % (2 * pi) - pi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.10.6
3
+ Version: 0.10.7
4
4
  Summary: Sonolus engine development in Python
5
5
  Project-URL: Documentation, https://sonolus.py.qwewqa.xyz/
6
6
  Project-URL: Repository, https://github.com/qwewqa/sonolus.py
@@ -39,7 +39,7 @@ sonolus/script/archetype.py,sha256=ck_LR8z0ipVq3T9b735VwvQI2mxVUyjHylr4BFagXT8,4
39
39
  sonolus/script/array.py,sha256=EbrNwl_WuJ0JjjkX0s_VJNXWqvYdm_ljTbyrDEMLGUY,13348
40
40
  sonolus/script/array_like.py,sha256=E6S4TW2muXgcyVkhUASQVt7JSYUkpvdJPgHz6YiSHNo,14708
41
41
  sonolus/script/bucket.py,sha256=yIod3DgX7Hv7RLe-4Cn81FcydvbkbdMt26FzpRj7oUI,7794
42
- sonolus/script/containers.py,sha256=nPjyyLmGnTxndMipqdfJ9CxmIT09wNLRQM7efagtZHI,19417
42
+ sonolus/script/containers.py,sha256=SnLXflwlX47EMQmTWnSzm9NYCi-as8lWnKzgCIK0PmM,26940
43
43
  sonolus/script/debug.py,sha256=yYg6EZt3NUOUeph1pu_5cA_2lxs8SZ91v76eOC1Sw-8,7747
44
44
  sonolus/script/easing.py,sha256=2FUJI_nfp990P_armCcRqHm2329O985glJAhSC6tnxs,11379
45
45
  sonolus/script/effect.py,sha256=SfJxSNF3RlPCRXnkt62ZlWhCXw3mmmRCsoMsvTErUP0,7960
@@ -67,9 +67,9 @@ sonolus/script/timing.py,sha256=DklMvuxcFg3MzXsecUo6Yhdk7pScOJ7STwXvAiTvLKM,3067
67
67
  sonolus/script/transform.py,sha256=4aS7-NNzX0v9KMXZ4gIGOaU1Cd-ok7DO_OvIBca0mGU,21418
68
68
  sonolus/script/ui.py,sha256=DYPGWIjHj1IFPxW1zaEuIUQx0b32FJPXtiwCvrtJ6oo,7528
69
69
  sonolus/script/values.py,sha256=6iJG6h4IDlbcK8FH4GENSHOQc7C_7fCGa34wM80qToA,1629
70
- sonolus/script/vec.py,sha256=s-_lplwu6UIvNpxiJHi2y1sM58-3ot1v1knAGbTJzCA,8162
70
+ sonolus/script/vec.py,sha256=LueAHrx5pKouHvLaO1nMbl5Plh3UgPn8J7UdJPHWFCM,8840
71
71
  sonolus/script/internal/__init__.py,sha256=T6rzLoiOUaiSQtaHMZ88SNO-ijSjSSv33TKtUwu-Ms8,136
72
- sonolus/script/internal/builtin_impls.py,sha256=tpNbaH6fLICd8TYj9Hf_wrPSWk3RkhmSPVN9nqOuqj4,13372
72
+ sonolus/script/internal/builtin_impls.py,sha256=b4gt54JyFoM9mhtxrhfCR8w2u38h4sJ3bzdOB0usNEs,13445
73
73
  sonolus/script/internal/callbacks.py,sha256=vWzJG8uiJoEtsNnbeZPqOHogCwoLpz2D1MnHY2wVV8s,2801
74
74
  sonolus/script/internal/constant.py,sha256=3ycbGkDJVUwcrCZ96vLjAoAARgsvaqDM8rJ_YCrLrvo,4289
75
75
  sonolus/script/internal/context.py,sha256=56pPjiPy8ZaxY3t5iEufsOMEj6BSy31G-5SoYqS6tPo,19694
@@ -87,8 +87,8 @@ sonolus/script/internal/simulation_context.py,sha256=LGxLTvxbqBIhoe1R-SfwGajNIDw
87
87
  sonolus/script/internal/transient.py,sha256=y2AWABqF1aoaP6H4_2u4MMpNioC4OsZQCtPyNI0txqo,1634
88
88
  sonolus/script/internal/tuple_impl.py,sha256=DPNdmmRmupU8Ah4_XKq6-PdT336l4nt15_uCJKQGkkk,3587
89
89
  sonolus/script/internal/value.py,sha256=OngrCdmY_h6mV2Zgwqhuo4eYFad0kTk6263UAxctZcY,6963
90
- sonolus_py-0.10.6.dist-info/METADATA,sha256=eg4Gb5x3XZxZZ3IV4bjs1p0ZXGUxHdiMaJlRG1_bAS8,554
91
- sonolus_py-0.10.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- sonolus_py-0.10.6.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
- sonolus_py-0.10.6.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
- sonolus_py-0.10.6.dist-info/RECORD,,
90
+ sonolus_py-0.10.7.dist-info/METADATA,sha256=sXdmgTuEdNIRF2McqWR7H1c_l-uolUuow8Nt3-ZhYpY,554
91
+ sonolus_py-0.10.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ sonolus_py-0.10.7.dist-info/entry_points.txt,sha256=oTYspY_b7SA8TptEMTDxh4-Aj-ZVPnYC9f1lqH6s9G4,54
93
+ sonolus_py-0.10.7.dist-info/licenses/LICENSE,sha256=JEKpqVhQYfEc7zg3Mj462sKbKYmO1K7WmvX1qvg9IJk,1067
94
+ sonolus_py-0.10.7.dist-info/RECORD,,