sonolus.py 0.1.5__tar.gz → 0.1.7__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.
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/PKG-INFO +1 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/cli.md +12 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/pyproject.toml +1 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/cli.py +14 -3
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/project.py +30 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/archetype.py +10 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/array.py +1 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/context.py +8 -0
- sonolus_py-0.1.7/sonolus/script/project.py +76 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/quad.py +23 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/sprite.py +1 -3
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/vec.py +29 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/conftest.py +4 -1
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_flow.py +13 -0
- sonolus_py-0.1.7/tests/script/test_quad.py +289 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/uv.lock +1 -1
- sonolus_py-0.1.5/sonolus/script/project.py +0 -25
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/.github/workflows/publish.yaml +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/.gitignore +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/.python-version +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/LICENSE +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/README.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/doc_stubs/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/doc_stubs/builtins.pyi +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/doc_stubs/math.pyi +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/doc_stubs/num.pyi +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/doc_stubs/random.pyi +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/CNAME +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/builtins.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/constructs.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/index.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/project.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/resources.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/concepts/types.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/index.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/builtins.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/index.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/math.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/random.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.archetype.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.array.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.array_like.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.bucket.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.containers.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.debug.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.easing.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.effect.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.engine.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.globals.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.instruction.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.interval.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.iterator.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.level.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.num.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.options.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.particle.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.print.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.project.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.quad.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.record.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.runtime.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.sprite.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.text.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.timing.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.transform.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.ui.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.values.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/docs/reference/sonolus.script.vec.md +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/mkdocs.yml +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/generate.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/runtimes/Engine/Tutorial/Blocks.json +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/runtimes/Functions.json +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/runtimes/Level/Play/Blocks.json +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/runtimes/Level/Preview/Blocks.json +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/scripts/runtimes/Level/Watch/Blocks.json +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/blocks.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/excepthook.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/finalize.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/interpret.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/ir.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/mode.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/node.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/ops.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/allocate.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/constant_evaluation.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/copy_coalesce.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/dead_code.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/dominance.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/flow.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/inlining.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/liveness.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/optimize.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/passes.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/simplify.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/optimize/ssa.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/place.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/utils.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/backend/visitor.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/collection.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/compile.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/engine.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/level.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/build/node.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/py.typed +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/array_like.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/bucket.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/containers.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/debug.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/easing.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/effect.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/engine.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/globals.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/instruction.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/builtin_impls.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/callbacks.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/constant.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/descriptor.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/dict_impl.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/error.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/generic.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/impl.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/introspection.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/math_impls.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/native.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/random.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/range.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/transient.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/tuple_impl.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/internal/value.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/interval.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/iterator.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/level.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/num.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/options.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/particle.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/pointer.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/print.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/record.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/runtime.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/text.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/timing.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/transform.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/ui.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/sonolus/script/values.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/__init__.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_array.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_array_map.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_assert.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_dict.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_functions.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_helpers.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_interval.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_match.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_num.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_operator.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_random.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_range.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_record.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_transform.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_tuple.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_var_array.py +0 -0
- {sonolus_py-0.1.5 → sonolus_py-0.1.7}/tests/script/test_vec.py +0 -0
|
@@ -14,3 +14,15 @@ To build the project, run the following command in the root directory of your pr
|
|
|
14
14
|
```bash
|
|
15
15
|
sonolus-py build
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
## Outputting the level schema
|
|
19
|
+
To output the level schema of the project, run the following command in the root directory of your project:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sonolus-py schema
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Programmatic usage
|
|
26
|
+
The same functionality can be accessed programmatically as methods of a project.
|
|
27
|
+
|
|
28
|
+
See [Project][sonolus.script.project.Project] for more information.
|
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import http.server
|
|
4
4
|
import importlib
|
|
5
|
+
import json
|
|
5
6
|
import shutil
|
|
6
7
|
import socket
|
|
7
8
|
import socketserver
|
|
@@ -11,7 +12,7 @@ from time import perf_counter
|
|
|
11
12
|
|
|
12
13
|
from sonolus.build.engine import package_engine
|
|
13
14
|
from sonolus.build.level import package_level_data
|
|
14
|
-
from sonolus.build.project import build_project_to_collection
|
|
15
|
+
from sonolus.build.project import build_project_to_collection, get_project_schema
|
|
15
16
|
from sonolus.script.project import Project
|
|
16
17
|
|
|
17
18
|
|
|
@@ -147,6 +148,14 @@ def main():
|
|
|
147
148
|
dev_parser.add_argument("--build-dir", type=str, default="./build")
|
|
148
149
|
dev_parser.add_argument("--port", type=int, default=8000)
|
|
149
150
|
|
|
151
|
+
schema_parser = subparsers.add_parser("schema")
|
|
152
|
+
schema_parser.add_argument(
|
|
153
|
+
"module",
|
|
154
|
+
type=str,
|
|
155
|
+
nargs="?",
|
|
156
|
+
help="Module path (e.g., 'module.name'). If omitted, will auto-detect if only one module exists.",
|
|
157
|
+
)
|
|
158
|
+
|
|
150
159
|
args = parser.parse_args()
|
|
151
160
|
|
|
152
161
|
if not args.module:
|
|
@@ -161,16 +170,18 @@ def main():
|
|
|
161
170
|
if project is None:
|
|
162
171
|
sys.exit(1)
|
|
163
172
|
|
|
164
|
-
build_dir = Path(args.build_dir)
|
|
165
|
-
|
|
166
173
|
if args.command == "build":
|
|
174
|
+
build_dir = Path(args.build_dir)
|
|
167
175
|
start_time = perf_counter()
|
|
168
176
|
build_project(project, build_dir)
|
|
169
177
|
end_time = perf_counter()
|
|
170
178
|
print(f"Project built successfully to '{build_dir.resolve()}' in {end_time - start_time:.2f}s")
|
|
171
179
|
elif args.command == "dev":
|
|
180
|
+
build_dir = Path(args.build_dir)
|
|
172
181
|
start_time = perf_counter()
|
|
173
182
|
build_collection(project, build_dir)
|
|
174
183
|
end_time = perf_counter()
|
|
175
184
|
print(f"Build finished in {end_time - start_time:.2f}s")
|
|
176
185
|
run_server(build_dir / "site", port=args.port)
|
|
186
|
+
elif args.command == "schema":
|
|
187
|
+
print(json.dumps(get_project_schema(project), indent=2))
|
|
@@ -5,7 +5,7 @@ from sonolus.build.engine import package_engine
|
|
|
5
5
|
from sonolus.build.level import package_level_data
|
|
6
6
|
from sonolus.script.engine import Engine
|
|
7
7
|
from sonolus.script.level import Level
|
|
8
|
-
from sonolus.script.project import Project
|
|
8
|
+
from sonolus.script.project import Project, ProjectSchema
|
|
9
9
|
|
|
10
10
|
BLANK_PNG = (
|
|
11
11
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x00\x007n\xf9$"
|
|
@@ -92,3 +92,32 @@ def load_resources_files_to_collection(base_path: Path) -> Collection:
|
|
|
92
92
|
collection.load_from_scp(path)
|
|
93
93
|
collection.load_from_source(base_path)
|
|
94
94
|
return collection
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_project_schema(project: Project) -> ProjectSchema:
|
|
98
|
+
by_archetype: dict[str, dict[str, bool]] = {}
|
|
99
|
+
for archetype in project.engine.data.play.archetypes:
|
|
100
|
+
fields = by_archetype.setdefault(archetype.name, {})
|
|
101
|
+
# If a field is exported, we should exclude it if it's imported in watch mode
|
|
102
|
+
for field in archetype._exported_keys_:
|
|
103
|
+
fields[field] = False
|
|
104
|
+
for field in archetype._imported_keys_:
|
|
105
|
+
fields[field] = True
|
|
106
|
+
for archetype in project.engine.data.watch.archetypes:
|
|
107
|
+
fields = by_archetype.setdefault(archetype.name, {})
|
|
108
|
+
for field in archetype._imported_keys_:
|
|
109
|
+
if field not in fields:
|
|
110
|
+
fields[field] = True
|
|
111
|
+
for archetype in project.engine.data.preview.archetypes:
|
|
112
|
+
fields = by_archetype.setdefault(archetype.name, {})
|
|
113
|
+
for field in archetype._imported_keys_:
|
|
114
|
+
fields[field] = True
|
|
115
|
+
return {
|
|
116
|
+
"archetypes": [
|
|
117
|
+
{
|
|
118
|
+
"name": name,
|
|
119
|
+
"fields": [*fields],
|
|
120
|
+
}
|
|
121
|
+
for name, fields in by_archetype.items()
|
|
122
|
+
]
|
|
123
|
+
}
|
|
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import Enum, StrEnum
|
|
7
7
|
from types import FunctionType
|
|
8
|
-
from typing import Annotated, Any, ClassVar, Self, get_origin
|
|
8
|
+
from typing import Annotated, Any, ClassVar, Self, TypedDict, get_origin
|
|
9
9
|
|
|
10
10
|
from sonolus.backend.ir import IRConst, IRInstr
|
|
11
11
|
from sonolus.backend.mode import Mode
|
|
@@ -307,6 +307,11 @@ class _ArchetypeLevelData:
|
|
|
307
307
|
type _ArchetypeData = _ArchetypeSelfData | _ArchetypeReferenceData | _ArchetypeLevelData
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
class ArchetypeSchema(TypedDict):
|
|
311
|
+
name: str
|
|
312
|
+
fields: list[str]
|
|
313
|
+
|
|
314
|
+
|
|
310
315
|
class _BaseArchetype:
|
|
311
316
|
_is_comptime_value_ = True
|
|
312
317
|
|
|
@@ -401,6 +406,10 @@ class _BaseArchetype:
|
|
|
401
406
|
data.extend(field.type._accept_(bound.arguments[field.name] or zeros(field.type))._to_list_())
|
|
402
407
|
native_call(Op.Spawn, archetype_id, *(Num(x) for x in data))
|
|
403
408
|
|
|
409
|
+
@classmethod
|
|
410
|
+
def schema(cls) -> ArchetypeSchema:
|
|
411
|
+
return {"name": cls.name or "unnamed", "fields": list(cls._imported_fields_)}
|
|
412
|
+
|
|
404
413
|
def _level_data_entries(self, level_refs: dict[Any, int] | None = None):
|
|
405
414
|
if not isinstance(self._data_, _ArchetypeLevelData):
|
|
406
415
|
raise RuntimeError("Entity is not level data")
|
|
@@ -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.
|
|
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:
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Self, TypedDict
|
|
6
|
+
|
|
7
|
+
from sonolus.script.archetype import ArchetypeSchema
|
|
8
|
+
from sonolus.script.engine import Engine
|
|
9
|
+
from sonolus.script.level import Level
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Project:
|
|
13
|
+
"""A Sonolus.py project.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
engine: The engine of the project.
|
|
17
|
+
levels: The levels of the project.
|
|
18
|
+
resources: The path to the resources of the project.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
engine: Engine,
|
|
24
|
+
levels: list[Level] | None = None,
|
|
25
|
+
resources: PathLike | None = None,
|
|
26
|
+
):
|
|
27
|
+
self.engine = engine
|
|
28
|
+
self.levels = levels or []
|
|
29
|
+
self.resources = Path(resources or "resources")
|
|
30
|
+
|
|
31
|
+
def with_levels(self, levels: list[Level]) -> Self:
|
|
32
|
+
"""Create a new project with the specified levels.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
levels: The levels of the project.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The new project.
|
|
39
|
+
"""
|
|
40
|
+
return Project(self.engine, levels, self.resources)
|
|
41
|
+
|
|
42
|
+
def dev(self, build_dir: PathLike, port: int = 8080):
|
|
43
|
+
"""Start a development server for the project.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
build_dir: The path to the build directory.
|
|
47
|
+
port: The port of the development server.
|
|
48
|
+
"""
|
|
49
|
+
from sonolus.build.cli import build_collection, run_server
|
|
50
|
+
|
|
51
|
+
build_collection(self, Path(build_dir))
|
|
52
|
+
run_server(Path(build_dir) / "site", port=port)
|
|
53
|
+
|
|
54
|
+
def build(self, build_dir: PathLike):
|
|
55
|
+
"""Build the project.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
build_dir: The path to the build directory.
|
|
59
|
+
"""
|
|
60
|
+
from sonolus.build.cli import build_project
|
|
61
|
+
|
|
62
|
+
build_project(self, Path(build_dir))
|
|
63
|
+
|
|
64
|
+
def schema(self) -> ProjectSchema:
|
|
65
|
+
"""Generate the schema of the project.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The schema of the project.
|
|
69
|
+
"""
|
|
70
|
+
from sonolus.build.project import get_project_schema
|
|
71
|
+
|
|
72
|
+
return get_project_schema(self)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ProjectSchema(TypedDict):
|
|
76
|
+
archetypes: list[ArchetypeSchema]
|
|
@@ -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."""
|
|
@@ -353,9 +353,7 @@ class StandardSprite:
|
|
|
353
353
|
SIMULTANEOUS_CONNECTION_PURPLE = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_PURPLE")]
|
|
354
354
|
SIMULTANEOUS_CONNECTION_CYAN = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_CYAN")]
|
|
355
355
|
|
|
356
|
-
SIMULTANEOUS_CONNECTION_NEUTRAL_SEAMLESS = Annotated[
|
|
357
|
-
Sprite, sprite("#SIMULTANEOUS_CONNECTION_NEUTRAL_SEAMLESS")
|
|
358
|
-
]
|
|
356
|
+
SIMULTANEOUS_CONNECTION_NEUTRAL_SEAMLESS = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_NEUTRAL_SEAMLESS")]
|
|
359
357
|
SIMULTANEOUS_CONNECTION_RED_SEAMLESS = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_RED_SEAMLESS")]
|
|
360
358
|
SIMULTANEOUS_CONNECTION_GREEN_SEAMLESS = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_GREEN_SEAMLESS")]
|
|
361
359
|
SIMULTANEOUS_CONNECTION_BLUE_SEAMLESS = Annotated[Sprite, sprite("#SIMULTANEOUS_CONNECTION_BLUE_SEAMLESS")]
|
|
@@ -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,289 @@
|
|
|
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
|
+
@st.composite
|
|
25
|
+
def quads(draw):
|
|
26
|
+
points = [draw(vecs()) for _ in range(4)]
|
|
27
|
+
for p1, p2 in itertools.combinations(points, 2):
|
|
28
|
+
assume((p1 - p2).magnitude > 1e-2)
|
|
29
|
+
centroid = sum(points, Vec2(0, 0)) / 4
|
|
30
|
+
points = sorted(points, key=lambda p: (p - centroid).angle)
|
|
31
|
+
return Quad(*points)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@st.composite
|
|
35
|
+
def rects(draw):
|
|
36
|
+
l = draw(floats)
|
|
37
|
+
r = draw(floats)
|
|
38
|
+
if l > r:
|
|
39
|
+
l, r = r, l
|
|
40
|
+
|
|
41
|
+
b = draw(floats)
|
|
42
|
+
t = draw(floats)
|
|
43
|
+
if b > t:
|
|
44
|
+
b, t = t, b
|
|
45
|
+
|
|
46
|
+
return Rect(t=t, r=r, b=b, l=l)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@given(
|
|
50
|
+
quad=quads(),
|
|
51
|
+
translation=vecs(),
|
|
52
|
+
)
|
|
53
|
+
@settings(deadline=timedelta(seconds=2))
|
|
54
|
+
def test_quad_translate(quad, translation):
|
|
55
|
+
def fn():
|
|
56
|
+
return quad.translate(translation)
|
|
57
|
+
|
|
58
|
+
result = validate_dual_run(fn)
|
|
59
|
+
assert is_close(result.bl, quad.bl + translation)
|
|
60
|
+
assert is_close(result.tl, quad.tl + translation)
|
|
61
|
+
assert is_close(result.tr, quad.tr + translation)
|
|
62
|
+
assert is_close(result.br, quad.br + translation)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@given(
|
|
66
|
+
quad=quads(),
|
|
67
|
+
factor=vecs(),
|
|
68
|
+
)
|
|
69
|
+
@settings(deadline=timedelta(seconds=2))
|
|
70
|
+
def test_quad_scale(quad, factor):
|
|
71
|
+
def fn():
|
|
72
|
+
return quad.scale(factor)
|
|
73
|
+
|
|
74
|
+
result = validate_dual_run(fn)
|
|
75
|
+
assert is_close(result.bl, quad.bl * factor)
|
|
76
|
+
assert is_close(result.tl, quad.tl * factor)
|
|
77
|
+
assert is_close(result.tr, quad.tr * factor)
|
|
78
|
+
assert is_close(result.br, quad.br * factor)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@given(
|
|
82
|
+
quad=quads(),
|
|
83
|
+
factor=vecs(),
|
|
84
|
+
pivot=vecs(),
|
|
85
|
+
)
|
|
86
|
+
@settings(deadline=timedelta(seconds=2))
|
|
87
|
+
def test_quad_scale_about(quad, factor, pivot):
|
|
88
|
+
def fn():
|
|
89
|
+
return quad.scale_about(factor, pivot)
|
|
90
|
+
|
|
91
|
+
result = validate_dual_run(fn)
|
|
92
|
+
assert is_close(result.bl, (quad.bl - pivot) * factor + pivot)
|
|
93
|
+
assert is_close(result.tl, (quad.tl - pivot) * factor + pivot)
|
|
94
|
+
assert is_close(result.tr, (quad.tr - pivot) * factor + pivot)
|
|
95
|
+
assert is_close(result.br, (quad.br - pivot) * factor + pivot)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@given(
|
|
99
|
+
quad=quads(),
|
|
100
|
+
angle=floats,
|
|
101
|
+
)
|
|
102
|
+
@settings(deadline=timedelta(seconds=2))
|
|
103
|
+
def test_quad_rotate(quad, angle):
|
|
104
|
+
def fn():
|
|
105
|
+
return quad.rotate(angle)
|
|
106
|
+
|
|
107
|
+
result = validate_dual_run(fn)
|
|
108
|
+
assert is_close(result.bl, quad.bl.rotate(angle))
|
|
109
|
+
assert is_close(result.tl, quad.tl.rotate(angle))
|
|
110
|
+
assert is_close(result.tr, quad.tr.rotate(angle))
|
|
111
|
+
assert is_close(result.br, quad.br.rotate(angle))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@given(
|
|
115
|
+
quad=quads(),
|
|
116
|
+
angle=floats,
|
|
117
|
+
pivot=vecs(),
|
|
118
|
+
)
|
|
119
|
+
@settings(deadline=timedelta(seconds=2))
|
|
120
|
+
def test_quad_rotate_about(quad, angle, pivot):
|
|
121
|
+
def fn():
|
|
122
|
+
return quad.rotate_about(angle, pivot)
|
|
123
|
+
|
|
124
|
+
result = validate_dual_run(fn)
|
|
125
|
+
assert is_close(result.bl, quad.bl.rotate_about(angle, pivot))
|
|
126
|
+
assert is_close(result.tl, quad.tl.rotate_about(angle, pivot))
|
|
127
|
+
assert is_close(result.tr, quad.tr.rotate_about(angle, pivot))
|
|
128
|
+
assert is_close(result.br, quad.br.rotate_about(angle, pivot))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@given(quad=quads())
|
|
132
|
+
@settings(deadline=timedelta(seconds=2))
|
|
133
|
+
def test_quad_center(quad):
|
|
134
|
+
def fn():
|
|
135
|
+
return quad.center
|
|
136
|
+
|
|
137
|
+
result = validate_dual_run(fn)
|
|
138
|
+
expected = (quad.bl + quad.tr + quad.tl + quad.br) / 4
|
|
139
|
+
assert is_close(result, expected)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Rect Tests
|
|
143
|
+
@given(
|
|
144
|
+
rect=rects(),
|
|
145
|
+
translation=vecs(),
|
|
146
|
+
)
|
|
147
|
+
@settings(deadline=timedelta(seconds=2))
|
|
148
|
+
def test_rect_translate(rect, translation):
|
|
149
|
+
def fn():
|
|
150
|
+
return rect.translate(translation)
|
|
151
|
+
|
|
152
|
+
result = validate_dual_run(fn)
|
|
153
|
+
assert is_close(result.t, rect.t + translation.y)
|
|
154
|
+
assert is_close(result.r, rect.r + translation.x)
|
|
155
|
+
assert is_close(result.b, rect.b + translation.y)
|
|
156
|
+
assert is_close(result.l, rect.l + translation.x)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@given(
|
|
160
|
+
rect=rects(),
|
|
161
|
+
factor=vecs(),
|
|
162
|
+
)
|
|
163
|
+
@settings(deadline=timedelta(seconds=2))
|
|
164
|
+
def test_rect_scale(rect, factor):
|
|
165
|
+
def fn():
|
|
166
|
+
return rect.scale(factor)
|
|
167
|
+
|
|
168
|
+
result = validate_dual_run(fn)
|
|
169
|
+
assert is_close(result.t, rect.t * factor.y)
|
|
170
|
+
assert is_close(result.r, rect.r * factor.x)
|
|
171
|
+
assert is_close(result.b, rect.b * factor.y)
|
|
172
|
+
assert is_close(result.l, rect.l * factor.x)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@given(
|
|
176
|
+
rect=rects(),
|
|
177
|
+
factor=vecs(),
|
|
178
|
+
pivot=vecs(),
|
|
179
|
+
)
|
|
180
|
+
@settings(deadline=timedelta(seconds=2))
|
|
181
|
+
def test_rect_scale_about(rect, factor, pivot):
|
|
182
|
+
def fn():
|
|
183
|
+
return rect.scale_about(factor, pivot)
|
|
184
|
+
|
|
185
|
+
result = validate_dual_run(fn)
|
|
186
|
+
assert is_close(result.t, (rect.t - pivot.y) * factor.y + pivot.y)
|
|
187
|
+
assert is_close(result.r, (rect.r - pivot.x) * factor.x + pivot.x)
|
|
188
|
+
assert is_close(result.b, (rect.b - pivot.y) * factor.y + pivot.y)
|
|
189
|
+
assert is_close(result.l, (rect.l - pivot.x) * factor.x + pivot.x)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@given(
|
|
193
|
+
rect=rects(),
|
|
194
|
+
expansion=vecs(),
|
|
195
|
+
)
|
|
196
|
+
@settings(deadline=timedelta(seconds=2))
|
|
197
|
+
def test_rect_expand(rect, expansion):
|
|
198
|
+
def fn():
|
|
199
|
+
return rect.expand(expansion)
|
|
200
|
+
|
|
201
|
+
result = validate_dual_run(fn)
|
|
202
|
+
assert is_close(result.t, rect.t + expansion.y)
|
|
203
|
+
assert is_close(result.r, rect.r + expansion.x)
|
|
204
|
+
assert is_close(result.b, rect.b - expansion.y)
|
|
205
|
+
assert is_close(result.l, rect.l - expansion.x)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@given(
|
|
209
|
+
rect=rects(),
|
|
210
|
+
point=vecs(),
|
|
211
|
+
)
|
|
212
|
+
@settings(deadline=timedelta(seconds=2))
|
|
213
|
+
def test_rect_contains_point(rect, point):
|
|
214
|
+
def fn():
|
|
215
|
+
return rect.contains_point(point)
|
|
216
|
+
|
|
217
|
+
result = validate_dual_run(fn)
|
|
218
|
+
expected = rect.l <= point.x <= rect.r and rect.b <= point.y <= rect.t
|
|
219
|
+
assert is_close(result, expected)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@given(
|
|
223
|
+
center=vecs(),
|
|
224
|
+
dimensions=vecs(),
|
|
225
|
+
)
|
|
226
|
+
@settings(deadline=timedelta(seconds=2))
|
|
227
|
+
def test_rect_from_center(center, dimensions):
|
|
228
|
+
def fn():
|
|
229
|
+
return Rect.from_center(center, dimensions)
|
|
230
|
+
|
|
231
|
+
result = validate_dual_run(fn)
|
|
232
|
+
assert is_close(result.center.x, center.x)
|
|
233
|
+
assert is_close(result.center.y, center.y)
|
|
234
|
+
assert is_close(result.w, dimensions.x)
|
|
235
|
+
assert is_close(result.h, dimensions.y)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@given(rect=rects())
|
|
239
|
+
@settings(deadline=timedelta(seconds=2))
|
|
240
|
+
def test_rect_as_quad(rect):
|
|
241
|
+
def fn():
|
|
242
|
+
return rect.as_quad()
|
|
243
|
+
|
|
244
|
+
result = validate_dual_run(fn)
|
|
245
|
+
assert is_close(result.bl, rect.bl)
|
|
246
|
+
assert is_close(result.tl, rect.tl)
|
|
247
|
+
assert is_close(result.tr, rect.tr)
|
|
248
|
+
assert is_close(result.br, rect.br)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@st.composite
|
|
252
|
+
def points_with_expected_containment(draw):
|
|
253
|
+
"""Generate a point and whether it should be inside a given quad."""
|
|
254
|
+
quad = draw(quads())
|
|
255
|
+
|
|
256
|
+
is_inside = draw(st.booleans())
|
|
257
|
+
|
|
258
|
+
if is_inside:
|
|
259
|
+
margin = 1.001
|
|
260
|
+
a = draw(st.floats(min_value=0, max_value=1)) + margin
|
|
261
|
+
b = draw(st.floats(min_value=0, max_value=1)) + margin
|
|
262
|
+
c = draw(st.floats(min_value=0, max_value=1)) + margin
|
|
263
|
+
d = draw(st.floats(min_value=0, max_value=1)) + margin
|
|
264
|
+
total = a + b + c + d
|
|
265
|
+
a /= total
|
|
266
|
+
b /= total
|
|
267
|
+
c /= total
|
|
268
|
+
d /= total
|
|
269
|
+
# Not totally uniform, but good enough for testing
|
|
270
|
+
point = quad.bl * a + quad.tl * b + quad.tr * c + quad.br * d
|
|
271
|
+
else:
|
|
272
|
+
diam = max((quad.tl - quad.br).magnitude, (quad.tr - quad.bl).magnitude)
|
|
273
|
+
angle = draw(st.floats(min_value=0, max_value=2 * math.pi))
|
|
274
|
+
scale = draw(st.floats(min_value=1, max_value=2))
|
|
275
|
+
point = quad.center + Vec2(math.cos(angle), math.sin(angle)) * scale * (diam + 0.001)
|
|
276
|
+
|
|
277
|
+
return quad, point, is_inside
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@given(quad_point_expected=points_with_expected_containment())
|
|
281
|
+
@settings(deadline=timedelta(seconds=2))
|
|
282
|
+
def test_quad_contains_point(quad_point_expected):
|
|
283
|
+
quad, point, expected = quad_point_expected
|
|
284
|
+
|
|
285
|
+
def fn():
|
|
286
|
+
return quad.contains_point(point)
|
|
287
|
+
|
|
288
|
+
result = validate_dual_run(fn)
|
|
289
|
+
assert result == expected, f"Expected point {point} to be {'inside' if expected else 'outside'} quad {quad}"
|