sonolus.py 0.1.6__tar.gz → 0.1.8__tar.gz

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 (168) hide show
  1. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/.github/workflows/publish.yaml +2 -2
  2. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/PKG-INFO +2 -2
  3. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/pyproject.toml +17 -2
  4. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/array.py +1 -1
  5. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/context.py +8 -0
  6. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/num.py +2 -2
  7. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/quad.py +23 -1
  8. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/vec.py +29 -0
  9. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/conftest.py +4 -1
  10. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_flow.py +13 -0
  11. sonolus_py-0.1.8/tests/script/test_quad.py +294 -0
  12. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/uv.lock +157 -2
  13. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/.gitignore +0 -0
  14. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/.python-version +0 -0
  15. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/LICENSE +0 -0
  16. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/README.md +0 -0
  17. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/doc_stubs/__init__.py +0 -0
  18. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/doc_stubs/builtins.pyi +0 -0
  19. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/doc_stubs/math.pyi +0 -0
  20. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/doc_stubs/num.pyi +0 -0
  21. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/doc_stubs/random.pyi +0 -0
  22. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/CNAME +0 -0
  23. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/builtins.md +0 -0
  24. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/cli.md +0 -0
  25. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/constructs.md +0 -0
  26. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/index.md +0 -0
  27. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/project.md +0 -0
  28. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/resources.md +0 -0
  29. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/concepts/types.md +0 -0
  30. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/index.md +0 -0
  31. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/builtins.md +0 -0
  32. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/index.md +0 -0
  33. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/math.md +0 -0
  34. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/random.md +0 -0
  35. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.archetype.md +0 -0
  36. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.array.md +0 -0
  37. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.array_like.md +0 -0
  38. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.bucket.md +0 -0
  39. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.containers.md +0 -0
  40. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.debug.md +0 -0
  41. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.easing.md +0 -0
  42. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.effect.md +0 -0
  43. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.engine.md +0 -0
  44. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.globals.md +0 -0
  45. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.instruction.md +0 -0
  46. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.interval.md +0 -0
  47. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.iterator.md +0 -0
  48. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.level.md +0 -0
  49. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.num.md +0 -0
  50. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.options.md +0 -0
  51. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.particle.md +0 -0
  52. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.print.md +0 -0
  53. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.project.md +0 -0
  54. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.quad.md +0 -0
  55. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.record.md +0 -0
  56. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.runtime.md +0 -0
  57. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.sprite.md +0 -0
  58. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.text.md +0 -0
  59. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.timing.md +0 -0
  60. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.transform.md +0 -0
  61. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.ui.md +0 -0
  62. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.values.md +0 -0
  63. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/docs/reference/sonolus.script.vec.md +0 -0
  64. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/mkdocs.yml +0 -0
  65. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/generate.py +0 -0
  66. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/runtimes/Engine/Tutorial/Blocks.json +0 -0
  67. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/runtimes/Functions.json +0 -0
  68. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/runtimes/Level/Play/Blocks.json +0 -0
  69. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/runtimes/Level/Preview/Blocks.json +0 -0
  70. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/scripts/runtimes/Level/Watch/Blocks.json +0 -0
  71. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/__init__.py +0 -0
  72. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/__init__.py +0 -0
  73. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/blocks.py +0 -0
  74. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/excepthook.py +0 -0
  75. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/finalize.py +0 -0
  76. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/interpret.py +0 -0
  77. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/ir.py +0 -0
  78. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/mode.py +0 -0
  79. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/node.py +0 -0
  80. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/ops.py +0 -0
  81. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/__init__.py +0 -0
  82. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/allocate.py +0 -0
  83. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/constant_evaluation.py +0 -0
  84. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/copy_coalesce.py +0 -0
  85. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/dead_code.py +0 -0
  86. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/dominance.py +0 -0
  87. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/flow.py +0 -0
  88. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/inlining.py +0 -0
  89. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/liveness.py +0 -0
  90. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/optimize.py +0 -0
  91. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/passes.py +0 -0
  92. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/simplify.py +0 -0
  93. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/optimize/ssa.py +0 -0
  94. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/place.py +0 -0
  95. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/utils.py +0 -0
  96. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/backend/visitor.py +0 -0
  97. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/__init__.py +0 -0
  98. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/cli.py +0 -0
  99. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/collection.py +0 -0
  100. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/compile.py +0 -0
  101. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/engine.py +0 -0
  102. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/level.py +0 -0
  103. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/node.py +0 -0
  104. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/build/project.py +0 -0
  105. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/py.typed +0 -0
  106. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/__init__.py +0 -0
  107. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/archetype.py +0 -0
  108. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/array_like.py +0 -0
  109. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/bucket.py +0 -0
  110. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/containers.py +0 -0
  111. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/debug.py +0 -0
  112. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/easing.py +0 -0
  113. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/effect.py +0 -0
  114. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/engine.py +0 -0
  115. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/globals.py +0 -0
  116. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/instruction.py +0 -0
  117. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/__init__.py +0 -0
  118. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/builtin_impls.py +0 -0
  119. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/callbacks.py +0 -0
  120. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/constant.py +0 -0
  121. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/descriptor.py +0 -0
  122. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/dict_impl.py +0 -0
  123. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/error.py +0 -0
  124. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/generic.py +0 -0
  125. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/impl.py +0 -0
  126. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/introspection.py +0 -0
  127. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/math_impls.py +0 -0
  128. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/native.py +0 -0
  129. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/random.py +0 -0
  130. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/range.py +0 -0
  131. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/transient.py +0 -0
  132. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/tuple_impl.py +0 -0
  133. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/internal/value.py +0 -0
  134. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/interval.py +0 -0
  135. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/iterator.py +0 -0
  136. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/level.py +0 -0
  137. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/options.py +0 -0
  138. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/particle.py +0 -0
  139. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/pointer.py +0 -0
  140. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/print.py +0 -0
  141. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/project.py +0 -0
  142. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/record.py +0 -0
  143. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/runtime.py +0 -0
  144. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/sprite.py +0 -0
  145. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/text.py +0 -0
  146. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/timing.py +0 -0
  147. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/transform.py +0 -0
  148. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/ui.py +0 -0
  149. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/sonolus/script/values.py +0 -0
  150. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/__init__.py +0 -0
  151. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/__init__.py +0 -0
  152. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_array.py +0 -0
  153. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_array_map.py +0 -0
  154. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_assert.py +0 -0
  155. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_dict.py +0 -0
  156. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_functions.py +0 -0
  157. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_helpers.py +0 -0
  158. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_interval.py +0 -0
  159. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_match.py +0 -0
  160. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_num.py +0 -0
  161. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_operator.py +0 -0
  162. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_random.py +0 -0
  163. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_range.py +0 -0
  164. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_record.py +0 -0
  165. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_transform.py +0 -0
  166. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_tuple.py +0 -0
  167. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_var_array.py +0 -0
  168. {sonolus_py-0.1.6 → sonolus_py-0.1.8}/tests/script/test_vec.py +0 -0
@@ -16,13 +16,13 @@ jobs:
16
16
  version: 0.5.2
17
17
  - name: Install Python
18
18
  run: |
19
- uv python install
19
+ uv python install 3.12 3.13
20
20
  - name: Install project
21
21
  run: |
22
22
  uv sync --all-extras --dev
23
23
  - name: Run tests
24
24
  run: |
25
- uv run pytest tests -n auto
25
+ uv run tox
26
26
  - name: Build
27
27
  run: |
28
28
  uv build
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sonolus.py
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Sonolus engine development in Python
5
- Requires-Python: >=3.13
5
+ Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
7
 
8
8
  # Sonolus.py
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "sonolus.py"
3
- version = "0.1.6"
3
+ version = "0.1.8"
4
4
  description = "Sonolus engine development in Python"
5
5
  readme = "README.md"
6
- requires-python = ">=3.13"
6
+ requires-python = ">=3.12"
7
7
 
8
8
  [project.scripts]
9
9
  sonolus-py = "sonolus.build.cli:main"
@@ -16,6 +16,8 @@ dev-dependencies = [
16
16
  "pytest>=8.3.3",
17
17
  "ruff>=0.6.9",
18
18
  "pytest-cov>=6.0.0",
19
+ "tox>=4.23.2",
20
+ "tox-uv>=1.16.0",
19
21
  ]
20
22
 
21
23
  [tool.ruff]
@@ -43,3 +45,16 @@ docs = [
43
45
 
44
46
  [tool.hatch.build.targets.wheel]
45
47
  packages = ["sonolus"]
48
+
49
+ [tool.tox]
50
+ requires = ["tox>=4.19"]
51
+ env_list = ["py312", "py313"]
52
+
53
+ [tool.tox.env_run_base]
54
+ description = "Run tests"
55
+ deps = [
56
+ "hypothesis>=6.115.3",
57
+ "pytest-xdist>=3.6.1",
58
+ "pytest>=8.3.3",
59
+ ]
60
+ commands = [["pytest", "tests", "-n", "auto"]]
@@ -60,7 +60,7 @@ class Array[T, Size](GenericValue, ArrayLike[T]):
60
60
  raise ValueError(f"{cls.__name__} constructor should be used with {cls.size()} values, got {len(args)}")
61
61
  parameterized_cls = cls
62
62
  if ctx():
63
- place = ctx().alloc(size=parameterized_cls.size())
63
+ place = ctx().alloc(size=parameterized_cls._size_())
64
64
  result: parameterized_cls = parameterized_cls._from_place_(place)
65
65
  result._copy_from_(parameterized_cls._with_value(values))
66
66
  return result
@@ -182,10 +182,18 @@ class Context:
182
182
  return
183
183
  assert len(self.outgoing) == 0
184
184
  self.outgoing[None] = header
185
+ values = {}
186
+ # First do a pass through and copy every value
185
187
  for name, target_value in header.loop_variables.items():
186
188
  with using_ctx(self):
187
189
  if type(target_value)._is_value_type_():
188
190
  value = self.scope.get_value(name)
191
+ values[name] = value._get_() # _get_() will make a copy on value types
192
+ # Then actually set them
193
+ for name, target_value in header.loop_variables.items():
194
+ with using_ctx(self):
195
+ if type(target_value)._is_value_type_():
196
+ value = values[name]
189
197
  value = type(target_value)._accept_(value)
190
198
  target_value._set_(value)
191
199
  else:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import operator
5
5
  from collections.abc import Callable, Iterable
6
- from typing import TYPE_CHECKING, Any, Self, TypeIs, final, runtime_checkable
6
+ from typing import TYPE_CHECKING, Any, Self, final, runtime_checkable, TypeGuard
7
7
 
8
8
  from sonolus.backend.ir import IRConst, IRGet, IRPureInstr, IRSet
9
9
  from sonolus.backend.ops import Op
@@ -19,7 +19,7 @@ class _NumMeta(type):
19
19
  return isinstance(instance, float | int | bool) or _is_num(instance)
20
20
 
21
21
 
22
- def _is_num(value: Any) -> TypeIs[Num]:
22
+ def _is_num(value: Any) -> TypeGuard[Num]:
23
23
  """Check if a value is a precisely Num instance."""
24
24
  return type.__instancecheck__(Num, value) # type: ignore # noqa: PLC2801
25
25
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from typing import Protocol, Self
4
4
 
5
5
  from sonolus.script.record import Record
6
- from sonolus.script.vec import Vec2
6
+ from sonolus.script.vec import Vec2, pnpoly
7
7
 
8
8
 
9
9
  class Quad(Record):
@@ -95,6 +95,17 @@ class Quad(Record):
95
95
  """Rotate the quad by the given angle about its center and return a new quad."""
96
96
  return self.rotate_about(angle, self.center)
97
97
 
98
+ def contains_point(self, point: Vec2, /) -> bool:
99
+ """Check if the quad contains the given point.
100
+
101
+ Args:
102
+ point: The point to check.
103
+
104
+ Returns:
105
+ True if the point is inside the quad, False otherwise.
106
+ """
107
+ return pnpoly((self.bl, self.tl, self.tr, self.br), point)
108
+
98
109
 
99
110
  class Rect(Record):
100
111
  """A rectangle defined by its top, right, bottom, and left edges.
@@ -225,6 +236,17 @@ class Rect(Record):
225
236
  l=self.l + shrinkage.x,
226
237
  )
227
238
 
239
+ def contains_point(self, point: Vec2, /) -> bool:
240
+ """Check if the rectangle contains the given point.
241
+
242
+ Args:
243
+ point: The point to check.
244
+
245
+ Returns:
246
+ True if the point is inside the rectangle, False otherwise.
247
+ """
248
+ return self.l <= point.x <= self.r and self.b <= point.y <= self.t
249
+
228
250
 
229
251
  class QuadLike(Protocol):
230
252
  """A protocol for types that can be used as quads."""
@@ -1,6 +1,8 @@
1
1
  from math import atan2, cos, sin
2
2
  from typing import Self
3
3
 
4
+ from sonolus.script.array import Array
5
+ from sonolus.script.array_like import ArrayLike
4
6
  from sonolus.script.num import Num
5
7
  from sonolus.script.record import Record
6
8
 
@@ -194,3 +196,30 @@ class Vec2(Record):
194
196
  A new vector with inverted direction.
195
197
  """
196
198
  return Vec2(x=-self.x, y=-self.y)
199
+
200
+
201
+ def pnpoly(vertices: ArrayLike[Vec2] | tuple[Vec2, ...], test: Vec2) -> bool:
202
+ """Check if a point is inside a polygon.
203
+
204
+ No guaranteed behavior for points on the edges or very close to the edges.
205
+
206
+ Args:
207
+ vertices: The vertices of the polygon.
208
+ test: The point to test.
209
+
210
+ Returns:
211
+ Whether the point is inside the polygon.
212
+ """
213
+ if isinstance(vertices, tuple):
214
+ vertices = Array(*vertices)
215
+ i = 0
216
+ j = len(vertices) - 1
217
+ c = False
218
+ while i < len(vertices):
219
+ if (vertices[i].y > test.y) != (vertices[j].y > test.y) and test.x < (vertices[j].x - vertices[i].x) * (
220
+ test.y - vertices[i].y
221
+ ) / (vertices[j].y - vertices[i].y) + vertices[i].x:
222
+ c = not c
223
+ j = i
224
+ i += 1
225
+ return c
@@ -16,6 +16,7 @@ from sonolus.script.internal.context import GlobalContextState
16
16
  from sonolus.script.internal.error import CompilationError
17
17
  from sonolus.script.internal.impl import meta_fn
18
18
  from sonolus.script.num import Num
19
+ from sonolus.script.vec import Vec2
19
20
 
20
21
  settings.register_profile(
21
22
  "standard",
@@ -119,5 +120,7 @@ def implies(a: bool, b: bool) -> bool:
119
120
  return True
120
121
 
121
122
 
122
- def is_close(a: float, b: float, rel_tol: float = 1e-8, abs_tol: float = 1e-8) -> bool:
123
+ def is_close(a: float | Vec2, b: float | Vec2, rel_tol: float = 1e-8, abs_tol: float = 1e-8) -> bool:
124
+ if isinstance(a, Vec2):
125
+ return is_close(a.x, b.x, rel_tol, abs_tol) and is_close(a.y, b.y, rel_tol, abs_tol)
123
126
  return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
@@ -866,3 +866,16 @@ def test_for_empty():
866
866
  debug_log(4)
867
867
 
868
868
  validate_dual_run(fn)
869
+
870
+
871
+ def test_loop_with_aug_assign():
872
+ def fn():
873
+ a = 0
874
+ b = 0
875
+ while a < 5:
876
+ debug_log(a + b)
877
+ b = a
878
+ a += 1
879
+
880
+ for _ in range(100):
881
+ validate_dual_run(fn)
@@ -0,0 +1,294 @@
1
+ # ruff: noqa: E741
2
+ import itertools
3
+ import math
4
+ from datetime import timedelta
5
+
6
+ from hypothesis import assume, given, settings
7
+ from hypothesis import strategies as st
8
+
9
+ from sonolus.script.quad import Quad, Rect
10
+ from sonolus.script.vec import Vec2
11
+ from tests.script.conftest import is_close, validate_dual_run
12
+
13
+ floats = st.floats(min_value=-9, max_value=9, allow_nan=False, allow_infinity=False)
14
+ nonzero_floats = floats.filter(lambda x: abs(x) > 1e-2)
15
+
16
+
17
+ @st.composite
18
+ def vecs(draw):
19
+ x = draw(floats)
20
+ y = draw(floats)
21
+ return Vec2(x, y)
22
+
23
+
24
+ def _area(points: list[Vec2]) -> float:
25
+ return 0.5 * abs(sum(p.x * q.y - p.y * q.x for p, q in zip(points, points[1:] + points[:1], strict=False)))
26
+
27
+
28
+ @st.composite
29
+ def quads(draw):
30
+ points = [draw(vecs()) for _ in range(4)]
31
+ for p1, p2 in itertools.combinations(points, 2):
32
+ assume((p1 - p2).magnitude > 1e-2)
33
+ centroid = sum(points, Vec2(0, 0)) / 4
34
+ points = sorted(points, key=lambda p: (p - centroid).angle)
35
+ assume(_area(points) > 1e-2)
36
+ return Quad(*points)
37
+
38
+
39
+ @st.composite
40
+ def rects(draw):
41
+ l = draw(floats)
42
+ r = draw(floats)
43
+ if l > r:
44
+ l, r = r, l
45
+
46
+ b = draw(floats)
47
+ t = draw(floats)
48
+ if b > t:
49
+ b, t = t, b
50
+
51
+ return Rect(t=t, r=r, b=b, l=l)
52
+
53
+
54
+ @given(
55
+ quad=quads(),
56
+ translation=vecs(),
57
+ )
58
+ @settings(deadline=timedelta(seconds=2))
59
+ def test_quad_translate(quad, translation):
60
+ def fn():
61
+ return quad.translate(translation)
62
+
63
+ result = validate_dual_run(fn)
64
+ assert is_close(result.bl, quad.bl + translation)
65
+ assert is_close(result.tl, quad.tl + translation)
66
+ assert is_close(result.tr, quad.tr + translation)
67
+ assert is_close(result.br, quad.br + translation)
68
+
69
+
70
+ @given(
71
+ quad=quads(),
72
+ factor=vecs(),
73
+ )
74
+ @settings(deadline=timedelta(seconds=2))
75
+ def test_quad_scale(quad, factor):
76
+ def fn():
77
+ return quad.scale(factor)
78
+
79
+ result = validate_dual_run(fn)
80
+ assert is_close(result.bl, quad.bl * factor)
81
+ assert is_close(result.tl, quad.tl * factor)
82
+ assert is_close(result.tr, quad.tr * factor)
83
+ assert is_close(result.br, quad.br * factor)
84
+
85
+
86
+ @given(
87
+ quad=quads(),
88
+ factor=vecs(),
89
+ pivot=vecs(),
90
+ )
91
+ @settings(deadline=timedelta(seconds=2))
92
+ def test_quad_scale_about(quad, factor, pivot):
93
+ def fn():
94
+ return quad.scale_about(factor, pivot)
95
+
96
+ result = validate_dual_run(fn)
97
+ assert is_close(result.bl, (quad.bl - pivot) * factor + pivot)
98
+ assert is_close(result.tl, (quad.tl - pivot) * factor + pivot)
99
+ assert is_close(result.tr, (quad.tr - pivot) * factor + pivot)
100
+ assert is_close(result.br, (quad.br - pivot) * factor + pivot)
101
+
102
+
103
+ @given(
104
+ quad=quads(),
105
+ angle=floats,
106
+ )
107
+ @settings(deadline=timedelta(seconds=2))
108
+ def test_quad_rotate(quad, angle):
109
+ def fn():
110
+ return quad.rotate(angle)
111
+
112
+ result = validate_dual_run(fn)
113
+ assert is_close(result.bl, quad.bl.rotate(angle))
114
+ assert is_close(result.tl, quad.tl.rotate(angle))
115
+ assert is_close(result.tr, quad.tr.rotate(angle))
116
+ assert is_close(result.br, quad.br.rotate(angle))
117
+
118
+
119
+ @given(
120
+ quad=quads(),
121
+ angle=floats,
122
+ pivot=vecs(),
123
+ )
124
+ @settings(deadline=timedelta(seconds=2))
125
+ def test_quad_rotate_about(quad, angle, pivot):
126
+ def fn():
127
+ return quad.rotate_about(angle, pivot)
128
+
129
+ result = validate_dual_run(fn)
130
+ assert is_close(result.bl, quad.bl.rotate_about(angle, pivot))
131
+ assert is_close(result.tl, quad.tl.rotate_about(angle, pivot))
132
+ assert is_close(result.tr, quad.tr.rotate_about(angle, pivot))
133
+ assert is_close(result.br, quad.br.rotate_about(angle, pivot))
134
+
135
+
136
+ @given(quad=quads())
137
+ @settings(deadline=timedelta(seconds=2))
138
+ def test_quad_center(quad):
139
+ def fn():
140
+ return quad.center
141
+
142
+ result = validate_dual_run(fn)
143
+ expected = (quad.bl + quad.tr + quad.tl + quad.br) / 4
144
+ assert is_close(result, expected)
145
+
146
+
147
+ # Rect Tests
148
+ @given(
149
+ rect=rects(),
150
+ translation=vecs(),
151
+ )
152
+ @settings(deadline=timedelta(seconds=2))
153
+ def test_rect_translate(rect, translation):
154
+ def fn():
155
+ return rect.translate(translation)
156
+
157
+ result = validate_dual_run(fn)
158
+ assert is_close(result.t, rect.t + translation.y)
159
+ assert is_close(result.r, rect.r + translation.x)
160
+ assert is_close(result.b, rect.b + translation.y)
161
+ assert is_close(result.l, rect.l + translation.x)
162
+
163
+
164
+ @given(
165
+ rect=rects(),
166
+ factor=vecs(),
167
+ )
168
+ @settings(deadline=timedelta(seconds=2))
169
+ def test_rect_scale(rect, factor):
170
+ def fn():
171
+ return rect.scale(factor)
172
+
173
+ result = validate_dual_run(fn)
174
+ assert is_close(result.t, rect.t * factor.y)
175
+ assert is_close(result.r, rect.r * factor.x)
176
+ assert is_close(result.b, rect.b * factor.y)
177
+ assert is_close(result.l, rect.l * factor.x)
178
+
179
+
180
+ @given(
181
+ rect=rects(),
182
+ factor=vecs(),
183
+ pivot=vecs(),
184
+ )
185
+ @settings(deadline=timedelta(seconds=2))
186
+ def test_rect_scale_about(rect, factor, pivot):
187
+ def fn():
188
+ return rect.scale_about(factor, pivot)
189
+
190
+ result = validate_dual_run(fn)
191
+ assert is_close(result.t, (rect.t - pivot.y) * factor.y + pivot.y)
192
+ assert is_close(result.r, (rect.r - pivot.x) * factor.x + pivot.x)
193
+ assert is_close(result.b, (rect.b - pivot.y) * factor.y + pivot.y)
194
+ assert is_close(result.l, (rect.l - pivot.x) * factor.x + pivot.x)
195
+
196
+
197
+ @given(
198
+ rect=rects(),
199
+ expansion=vecs(),
200
+ )
201
+ @settings(deadline=timedelta(seconds=2))
202
+ def test_rect_expand(rect, expansion):
203
+ def fn():
204
+ return rect.expand(expansion)
205
+
206
+ result = validate_dual_run(fn)
207
+ assert is_close(result.t, rect.t + expansion.y)
208
+ assert is_close(result.r, rect.r + expansion.x)
209
+ assert is_close(result.b, rect.b - expansion.y)
210
+ assert is_close(result.l, rect.l - expansion.x)
211
+
212
+
213
+ @given(
214
+ rect=rects(),
215
+ point=vecs(),
216
+ )
217
+ @settings(deadline=timedelta(seconds=2))
218
+ def test_rect_contains_point(rect, point):
219
+ def fn():
220
+ return rect.contains_point(point)
221
+
222
+ result = validate_dual_run(fn)
223
+ expected = rect.l <= point.x <= rect.r and rect.b <= point.y <= rect.t
224
+ assert is_close(result, expected)
225
+
226
+
227
+ @given(
228
+ center=vecs(),
229
+ dimensions=vecs(),
230
+ )
231
+ @settings(deadline=timedelta(seconds=2))
232
+ def test_rect_from_center(center, dimensions):
233
+ def fn():
234
+ return Rect.from_center(center, dimensions)
235
+
236
+ result = validate_dual_run(fn)
237
+ assert is_close(result.center.x, center.x)
238
+ assert is_close(result.center.y, center.y)
239
+ assert is_close(result.w, dimensions.x)
240
+ assert is_close(result.h, dimensions.y)
241
+
242
+
243
+ @given(rect=rects())
244
+ @settings(deadline=timedelta(seconds=2))
245
+ def test_rect_as_quad(rect):
246
+ def fn():
247
+ return rect.as_quad()
248
+
249
+ result = validate_dual_run(fn)
250
+ assert is_close(result.bl, rect.bl)
251
+ assert is_close(result.tl, rect.tl)
252
+ assert is_close(result.tr, rect.tr)
253
+ assert is_close(result.br, rect.br)
254
+
255
+
256
+ @st.composite
257
+ def points_with_expected_containment(draw):
258
+ """Generate a point and whether it should be inside a given quad."""
259
+ quad = draw(quads())
260
+
261
+ is_inside = draw(st.booleans())
262
+
263
+ if is_inside:
264
+ margin = 1.001
265
+ a = draw(st.floats(min_value=0, max_value=1)) + margin
266
+ b = draw(st.floats(min_value=0, max_value=1)) + margin
267
+ c = draw(st.floats(min_value=0, max_value=1)) + margin
268
+ d = draw(st.floats(min_value=0, max_value=1)) + margin
269
+ total = a + b + c + d
270
+ a /= total
271
+ b /= total
272
+ c /= total
273
+ d /= total
274
+ # Not totally uniform, but good enough for testing
275
+ point = quad.bl * a + quad.tl * b + quad.tr * c + quad.br * d
276
+ else:
277
+ diam = max((quad.tl - quad.br).magnitude, (quad.tr - quad.bl).magnitude)
278
+ angle = draw(st.floats(min_value=0, max_value=2 * math.pi))
279
+ scale = draw(st.floats(min_value=1, max_value=2))
280
+ point = quad.center + Vec2(math.cos(angle), math.sin(angle)) * scale * (diam + 0.001)
281
+
282
+ return quad, point, is_inside
283
+
284
+
285
+ @given(quad_point_expected=points_with_expected_containment())
286
+ @settings(deadline=timedelta(seconds=2))
287
+ def test_quad_contains_point(quad_point_expected):
288
+ quad, point, expected = quad_point_expected
289
+
290
+ def fn():
291
+ return quad.contains_point(point)
292
+
293
+ result = validate_dual_run(fn)
294
+ assert result == expected, f"Expected point {point} to be {'inside' if expected else 'outside'} quad {quad}"