FloriaKit 0.0.5__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.
- FloriaKit/__init__.py +0 -0
- FloriaKit/hints/__init__.py +14 -0
- FloriaKit/hints/color.py +36 -0
- FloriaKit/hints/common.py +44 -0
- FloriaKit/hints/exception.py +19 -0
- FloriaKit/hints/matrix.py +8 -0
- FloriaKit/hints/quaternion.py +5 -0
- FloriaKit/protocols/__init__.py +3 -0
- FloriaKit/protocols/functions.py +15 -0
- FloriaKit/py.typed +0 -0
- FloriaKit/utils/__init__.py +120 -0
- FloriaKit/utils/avg.py +74 -0
- FloriaKit/utils/calculated_fields.py +62 -0
- FloriaKit/utils/calculated_value.py +16 -0
- FloriaKit/utils/context.py +16 -0
- FloriaKit/utils/flag.py +50 -0
- FloriaKit/utils/interpolation_field.py +80 -0
- FloriaKit/utils/logging.py +42 -0
- FloriaKit/utils/matrix.py +70 -0
- FloriaKit/utils/quaternion.py +55 -0
- FloriaKit/utils/stopwatch.py +272 -0
- FloriaKit/utils/vector.py +17 -0
- FloriaKit/validator/__init__.py +67 -0
- floriakit-0.0.5.dist-info/METADATA +235 -0
- floriakit-0.0.5.dist-info/RECORD +28 -0
- floriakit-0.0.5.dist-info/WHEEL +5 -0
- floriakit-0.0.5.dist-info/licenses/LICENSE +201 -0
- floriakit-0.0.5.dist-info/top_level.txt +1 -0
FloriaKit/__init__.py
ADDED
|
File without changes
|
FloriaKit/hints/color.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from .common import number
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type rgb = tuple[number, number, number]
|
|
7
|
+
type rgba = tuple[number, number, number, number]
|
|
8
|
+
|
|
9
|
+
type color = t.Union[
|
|
10
|
+
rgb,
|
|
11
|
+
rgba,
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_rgba(value: color) -> rgba:
|
|
16
|
+
match len(value):
|
|
17
|
+
case 3:
|
|
18
|
+
return (*value, 255) # pyright: ignore[reportReturnType]
|
|
19
|
+
|
|
20
|
+
case 4:
|
|
21
|
+
return value # pyright: ignore[reportReturnType]
|
|
22
|
+
|
|
23
|
+
case _:
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_rgb(value: color) -> rgb:
|
|
28
|
+
match len(value):
|
|
29
|
+
case 3:
|
|
30
|
+
return value # pyright: ignore[reportReturnType]
|
|
31
|
+
|
|
32
|
+
case 4:
|
|
33
|
+
return value[:3]
|
|
34
|
+
|
|
35
|
+
case _:
|
|
36
|
+
raise
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type number = int | float
|
|
5
|
+
|
|
6
|
+
# vec2
|
|
7
|
+
|
|
8
|
+
type vec2 = tuple[number, number]
|
|
9
|
+
type pos2 = vec2
|
|
10
|
+
type size2 = vec2
|
|
11
|
+
type scale2 = vec2
|
|
12
|
+
|
|
13
|
+
type vec2i = tuple[int, int]
|
|
14
|
+
type pos2i = vec2i
|
|
15
|
+
type size2i = vec2i
|
|
16
|
+
type scale2i = vec2i
|
|
17
|
+
|
|
18
|
+
type vec2f = tuple[float, float]
|
|
19
|
+
type pos2f = vec2f
|
|
20
|
+
type size2f = vec2f
|
|
21
|
+
type scale2f = vec2f
|
|
22
|
+
|
|
23
|
+
# vec3
|
|
24
|
+
|
|
25
|
+
type vec3 = tuple[number, number]
|
|
26
|
+
type pos3 = vec3
|
|
27
|
+
type size3 = vec3
|
|
28
|
+
type scale3 = vec3
|
|
29
|
+
|
|
30
|
+
type vec3i = tuple[int, int, int]
|
|
31
|
+
type pos3i = vec3i
|
|
32
|
+
type size3i = vec3i
|
|
33
|
+
type scale3i = vec3i
|
|
34
|
+
|
|
35
|
+
type vec3f = tuple[float, float, float]
|
|
36
|
+
type pos3f = vec3f
|
|
37
|
+
type size3f = vec3f
|
|
38
|
+
type scale3f = vec3f
|
|
39
|
+
|
|
40
|
+
# vec4
|
|
41
|
+
|
|
42
|
+
type vec4 = tuple[number, number, number, number]
|
|
43
|
+
type vec4i = tuple[int, int, int, int]
|
|
44
|
+
type vec4f = tuple[float, float, float, float]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type ex = Exception | str
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if t.TYPE_CHECKING:
|
|
8
|
+
|
|
9
|
+
@t.overload
|
|
10
|
+
def get_exception(ex_: ex) -> Exception: ...
|
|
11
|
+
|
|
12
|
+
@t.overload
|
|
13
|
+
def get_exception(ex_: ex | None) -> Exception | None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_exception(ex_: ex | None):
|
|
17
|
+
if ex_ is None:
|
|
18
|
+
return None
|
|
19
|
+
return ex_ if isinstance(ex_, Exception) else Exception(ex_)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SyncFunction[**P = [], R = t.Any](t.Protocol):
|
|
5
|
+
def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncFunction[**P = [], R = t.Any](t.Protocol):
|
|
9
|
+
async def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
type AnyFunction[**P = [], R = t.Any] = t.Union[
|
|
13
|
+
SyncFunction[P, R],
|
|
14
|
+
AsyncFunction[P, R],
|
|
15
|
+
]
|
FloriaKit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from .calculated_value import CalculatedValue, gcv
|
|
5
|
+
from .calculated_fields import CalculatedFields
|
|
6
|
+
from .interpolation_field import InterpolationField
|
|
7
|
+
from .stopwatch import Stopwatch, stopwatch
|
|
8
|
+
from .avg import Avg
|
|
9
|
+
from .flag import Flag
|
|
10
|
+
|
|
11
|
+
from .. import hints
|
|
12
|
+
|
|
13
|
+
from . import (
|
|
14
|
+
context,
|
|
15
|
+
logging,
|
|
16
|
+
quaternion,
|
|
17
|
+
matrix,
|
|
18
|
+
vector,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if t.TYPE_CHECKING:
|
|
23
|
+
|
|
24
|
+
@t.overload
|
|
25
|
+
def coalapse[T](
|
|
26
|
+
*items: T | None,
|
|
27
|
+
exception: hints.exception.ex | None = None,
|
|
28
|
+
) -> T: ...
|
|
29
|
+
|
|
30
|
+
@t.overload
|
|
31
|
+
def coalapse[T, TD](
|
|
32
|
+
*items: T | None,
|
|
33
|
+
default: TD,
|
|
34
|
+
) -> T | TD: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def coalapse(
|
|
38
|
+
*items: t.Any | None,
|
|
39
|
+
**kw: t.Any,
|
|
40
|
+
) -> t.Any:
|
|
41
|
+
for item in items:
|
|
42
|
+
if item is not None:
|
|
43
|
+
return item
|
|
44
|
+
|
|
45
|
+
if 'default' in kw:
|
|
46
|
+
return kw['default']
|
|
47
|
+
|
|
48
|
+
if (ex := hints.exception.get_exception(kw.get('exception'))) is not None:
|
|
49
|
+
raise ex
|
|
50
|
+
|
|
51
|
+
raise ValueError()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if t.TYPE_CHECKING:
|
|
55
|
+
|
|
56
|
+
@t.overload
|
|
57
|
+
def coalapse_lazy[T](
|
|
58
|
+
*items: CalculatedValue[T | None] | T | None,
|
|
59
|
+
exception: hints.exception.ex | None = None,
|
|
60
|
+
) -> T: ...
|
|
61
|
+
|
|
62
|
+
@t.overload
|
|
63
|
+
def coalapse_lazy[T, TD](
|
|
64
|
+
*items: CalculatedValue[T | None] | T | None,
|
|
65
|
+
default: TD,
|
|
66
|
+
) -> T | TD: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def coalapse_lazy(
|
|
70
|
+
*items: CalculatedValue[t.Any] | t.Any | None,
|
|
71
|
+
**kw: t.Any,
|
|
72
|
+
) -> t.Any:
|
|
73
|
+
for item in items:
|
|
74
|
+
if (value := gcv(item)) is not None:
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
if 'default' in kw:
|
|
78
|
+
return kw['default']
|
|
79
|
+
|
|
80
|
+
if (ex := hints.exception.get_exception(kw.get('exception'))) is not None:
|
|
81
|
+
raise ex
|
|
82
|
+
|
|
83
|
+
raise ValueError()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def invoke[**P, R](
|
|
87
|
+
func: t.Callable[P, R | t.Coroutine[R, t.Any, t.Any]],
|
|
88
|
+
*args: P.args,
|
|
89
|
+
**kwargs: P.kwargs,
|
|
90
|
+
) -> R:
|
|
91
|
+
if asyncio.iscoroutine(result := func(*args, **kwargs)):
|
|
92
|
+
return await result
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def partition[T](
|
|
97
|
+
predicate: t.Callable[[T], bool] | None,
|
|
98
|
+
iterable: t.Iterable[T],
|
|
99
|
+
) -> tuple[list[T], list[T]]:
|
|
100
|
+
'''
|
|
101
|
+
return: trues, falses
|
|
102
|
+
'''
|
|
103
|
+
|
|
104
|
+
trues: list[T] = []
|
|
105
|
+
falses: list[T] = []
|
|
106
|
+
|
|
107
|
+
for item in iterable:
|
|
108
|
+
if item is not None if predicate is None else predicate(item):
|
|
109
|
+
trues.append(item)
|
|
110
|
+
else:
|
|
111
|
+
falses.append(item)
|
|
112
|
+
|
|
113
|
+
return trues, falses
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ensure_iterable[T](value: T | t.Iterable[T]) -> t.Iterable[T]:
|
|
117
|
+
if isinstance(value, t.Iterable) and not isinstance(value, (str, bytes)):
|
|
118
|
+
return value
|
|
119
|
+
else:
|
|
120
|
+
return [value]
|
FloriaKit/utils/avg.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Avg:
|
|
5
|
+
"""Accumulator for incremental average calculation.
|
|
6
|
+
|
|
7
|
+
Efficiently computes arithmetic mean without storing all values.
|
|
8
|
+
Suitable for real-time metrics tracking.
|
|
9
|
+
|
|
10
|
+
For example::
|
|
11
|
+
|
|
12
|
+
avg = Avg()
|
|
13
|
+
|
|
14
|
+
avg.add(60)
|
|
15
|
+
avg.add(30)
|
|
16
|
+
|
|
17
|
+
avg.value # 45
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ('_sum', '_count')
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize Avg instance"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
self._sum: float = 0
|
|
27
|
+
self._count: int = 0
|
|
28
|
+
|
|
29
|
+
def add(self, value: float):
|
|
30
|
+
"""Add new value to the accumulator.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
value: Numeric value to include in average calculation
|
|
34
|
+
"""
|
|
35
|
+
self._sum += value
|
|
36
|
+
self._count += 1
|
|
37
|
+
|
|
38
|
+
def extend(self, values: t.Iterable[float]):
|
|
39
|
+
"""Add multiple values efficiently.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
values: Iterable of numeric values
|
|
43
|
+
"""
|
|
44
|
+
for value in values:
|
|
45
|
+
self.add(value)
|
|
46
|
+
|
|
47
|
+
def clear(self):
|
|
48
|
+
"""Reset accumulator state."""
|
|
49
|
+
self._sum = self._count = 0
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def count(self) -> int:
|
|
53
|
+
"""Current number of accumulated values."""
|
|
54
|
+
return self._count
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def total(self) -> float:
|
|
58
|
+
"""Current accumulated sum of values."""
|
|
59
|
+
return self._sum
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def value(self) -> float:
|
|
63
|
+
"""Current arithmetic mean.
|
|
64
|
+
|
|
65
|
+
Returns 0.0 when no values accumulated to avoid division errors.
|
|
66
|
+
"""
|
|
67
|
+
return self._sum / self._count if self._count > 0 else 0
|
|
68
|
+
|
|
69
|
+
def __len__(self) -> int:
|
|
70
|
+
return self._count
|
|
71
|
+
|
|
72
|
+
def __iadd__(self, value: float) -> 'Avg':
|
|
73
|
+
self.add(value)
|
|
74
|
+
return self
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from .interpolation_field import InterpolationField
|
|
4
|
+
from .calculated_value import CalculatedValue, gcv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CalculatedFields[TK: str = str, **P = []]:
|
|
8
|
+
__slots__ = (
|
|
9
|
+
'_map',
|
|
10
|
+
'_process_func',
|
|
11
|
+
'__weakref__',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def default_process(value: t.Any) -> t.Any:
|
|
16
|
+
if isinstance(value, InterpolationField):
|
|
17
|
+
return t.cast(InterpolationField[t.Any], value).get_value()
|
|
18
|
+
|
|
19
|
+
elif isinstance(value, CalculatedValue):
|
|
20
|
+
return gcv(t.cast(CalculatedValue[t.Any], value))
|
|
21
|
+
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
map: t.Mapping[TK | str, CalculatedValue[t.Any, P] | t.Any] | None = None,
|
|
27
|
+
process_func: t.Callable[[t.Any], t.Any] | None = None,
|
|
28
|
+
):
|
|
29
|
+
self._map: dict[TK | str, CalculatedValue[t.Any, P] | t.Any] = (
|
|
30
|
+
{}
|
|
31
|
+
if map is None
|
|
32
|
+
else t.cast(dict[TK | str, CalculatedValue[t.Any, P] | t.Any], map)
|
|
33
|
+
)
|
|
34
|
+
self._process_func: t.Callable[[t.Any], t.Any] | None = process_func
|
|
35
|
+
|
|
36
|
+
def update(
|
|
37
|
+
self, map: t.Mapping[TK | str, CalculatedValue[t.Any, P] | t.Any]
|
|
38
|
+
) -> t.Self:
|
|
39
|
+
self._map.update(map)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def get[TD: t.Any | None = None](
|
|
43
|
+
self,
|
|
44
|
+
name: TK | str,
|
|
45
|
+
*args: P.args,
|
|
46
|
+
__default__: TD = None,
|
|
47
|
+
**kwargs: P.kwargs,
|
|
48
|
+
) -> t.Any | TD:
|
|
49
|
+
if (func := self._map.get(name)) is None:
|
|
50
|
+
return __default__
|
|
51
|
+
|
|
52
|
+
value = gcv(func, *args, **kwargs)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
value
|
|
56
|
+
if (process_func := self._process_func) is None
|
|
57
|
+
else process_func(value)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def names(self) -> list[TK | str]:
|
|
62
|
+
return list(self._map.keys())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@t.runtime_checkable
|
|
5
|
+
class CalculatedValue[R, **P = []](t.Protocol):
|
|
6
|
+
def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def gcv[T, **P = []](
|
|
10
|
+
value: CalculatedValue[T, P] | T,
|
|
11
|
+
*args: P.args,
|
|
12
|
+
**kwargs: P.kwargs,
|
|
13
|
+
) -> T:
|
|
14
|
+
if isinstance(value, CalculatedValue):
|
|
15
|
+
return value(*args, **kwargs) # pyright: ignore[reportUnknownVariableType]
|
|
16
|
+
return value
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@contextlib.contextmanager
|
|
6
|
+
def empty[TD: t.Any | None](
|
|
7
|
+
default: TD = None,
|
|
8
|
+
) -> t.Generator[TD, t.Any, None]:
|
|
9
|
+
yield default
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextlib.asynccontextmanager
|
|
13
|
+
async def async_empty[TD: t.Any | None](
|
|
14
|
+
default: TD = None,
|
|
15
|
+
) -> t.AsyncGenerator[TD, t.Any]:
|
|
16
|
+
yield default
|
FloriaKit/utils/flag.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Flag:
|
|
5
|
+
__slots__ = ('_depth',)
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Управляемый флаг с поддержкой контекстного менеджера.
|
|
9
|
+
|
|
10
|
+
Предоставляет механизм временного установления флага с
|
|
11
|
+
автоматическим сбросом при выходе из контекста.
|
|
12
|
+
|
|
13
|
+
Пример:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
|
|
17
|
+
flag = Flag()
|
|
18
|
+
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
with flag:
|
|
22
|
+
# Флаг установлен в True
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
# Флаг автоматически сброшен в False
|
|
26
|
+
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
if flag:
|
|
30
|
+
raise Exception(...)
|
|
31
|
+
```
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._depth: int = 0
|
|
36
|
+
|
|
37
|
+
def __enter__(self, *args: t.Any, **kwargs: t.Any):
|
|
38
|
+
self._depth += 1
|
|
39
|
+
|
|
40
|
+
def __exit__(self, *args: t.Any, **kwargs: t.Any):
|
|
41
|
+
self._depth -= 1
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def value(self) -> bool:
|
|
45
|
+
"""Текущее состояние флага"""
|
|
46
|
+
return self._depth > 0
|
|
47
|
+
|
|
48
|
+
def __bool__(self) -> bool:
|
|
49
|
+
"""Возвращает текущее состояние флага для использования в условиях"""
|
|
50
|
+
return self.value
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from .calculated_value import CalculatedValue, gcv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InterpolationField[T = t.Any]:
|
|
8
|
+
__slots__ = (
|
|
9
|
+
'_interpolation_func',
|
|
10
|
+
'_delay',
|
|
11
|
+
'_value',
|
|
12
|
+
'_next',
|
|
13
|
+
'__weakref__',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
interpolation_func: t.Callable[[T, T, float], T],
|
|
19
|
+
delay: CalculatedValue[float] | float,
|
|
20
|
+
default: T = None,
|
|
21
|
+
):
|
|
22
|
+
self._interpolation_func = interpolation_func
|
|
23
|
+
self._delay = delay
|
|
24
|
+
|
|
25
|
+
self._value: T = default
|
|
26
|
+
self._next: tuple[T, float] | None = None
|
|
27
|
+
'''next_value, next_time'''
|
|
28
|
+
|
|
29
|
+
def get_value(self) -> T:
|
|
30
|
+
if self._next is None:
|
|
31
|
+
return self._value
|
|
32
|
+
|
|
33
|
+
next_value, next_time = self._next
|
|
34
|
+
|
|
35
|
+
if time.perf_counter() >= next_time:
|
|
36
|
+
self._value = next_value
|
|
37
|
+
self._next = None
|
|
38
|
+
return self._value
|
|
39
|
+
|
|
40
|
+
return self._interpolation_func(
|
|
41
|
+
self._value,
|
|
42
|
+
next_value,
|
|
43
|
+
self.progress,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def set_value(self, value: T, flash: bool = False) -> t.Self:
|
|
47
|
+
if flash:
|
|
48
|
+
self._next = None
|
|
49
|
+
self._value = value
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
if self._next is not None:
|
|
53
|
+
self._value = self._next[0]
|
|
54
|
+
|
|
55
|
+
self._next = (
|
|
56
|
+
value,
|
|
57
|
+
time.perf_counter() + self.delay,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def progress(self) -> float:
|
|
64
|
+
if self._next is None:
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
_, next_time = self._next
|
|
68
|
+
|
|
69
|
+
if (cur_time := time.perf_counter()) >= next_time:
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
return min(1, max(0, 1 - (next_time - cur_time) / self.delay))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def delay(self):
|
|
76
|
+
return gcv(self._delay)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_interpolated(self):
|
|
80
|
+
return self._next is not None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_logger_level_ann = t.Literal[
|
|
6
|
+
'debug',
|
|
7
|
+
'info',
|
|
8
|
+
'warning',
|
|
9
|
+
'error',
|
|
10
|
+
'critical',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
_logger_level_map: dict[_logger_level_ann, int] = {
|
|
14
|
+
'debug': logging.DEBUG,
|
|
15
|
+
'info': logging.INFO,
|
|
16
|
+
'warning': logging.WARNING,
|
|
17
|
+
'error': logging.ERROR,
|
|
18
|
+
'critical': logging.CRITICAL,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create(
|
|
23
|
+
name: str,
|
|
24
|
+
level: int | _logger_level_ann,
|
|
25
|
+
terminal_format: logging.Formatter | str | None = None,
|
|
26
|
+
) -> logging.Logger:
|
|
27
|
+
level_ = _logger_level_map[level] if isinstance(level, str) else level
|
|
28
|
+
|
|
29
|
+
logger = logging.Logger(name, level_)
|
|
30
|
+
|
|
31
|
+
terminal_handler = logging.StreamHandler()
|
|
32
|
+
terminal_handler.setLevel(level_)
|
|
33
|
+
if terminal_format is not None:
|
|
34
|
+
terminal_handler.setFormatter(
|
|
35
|
+
terminal_format
|
|
36
|
+
if isinstance(terminal_format, logging.Formatter)
|
|
37
|
+
else logging.Formatter(terminal_format)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger.addHandler(terminal_handler)
|
|
41
|
+
|
|
42
|
+
return logger
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from pyglm import glm
|
|
3
|
+
|
|
4
|
+
if t.TYPE_CHECKING:
|
|
5
|
+
from .. import hints
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def model_matrix(
|
|
9
|
+
position: 'hints.pos3f',
|
|
10
|
+
angle: 'hints.quaternion',
|
|
11
|
+
scale: 'hints.scale3f',
|
|
12
|
+
) -> 'hints.mat4x4':
|
|
13
|
+
return (
|
|
14
|
+
glm.translate(
|
|
15
|
+
glm.mat4(1),
|
|
16
|
+
glm.vec3(*position),
|
|
17
|
+
)
|
|
18
|
+
* glm.mat4_cast(
|
|
19
|
+
angle,
|
|
20
|
+
)
|
|
21
|
+
* glm.scale(
|
|
22
|
+
glm.mat4(1.0),
|
|
23
|
+
glm.vec3(*scale),
|
|
24
|
+
)
|
|
25
|
+
).to_tuple() # pyright: ignore[reportReturnType]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def view_matrix(
|
|
29
|
+
position: 'hints.pos3f',
|
|
30
|
+
angle: 'hints.quaternion',
|
|
31
|
+
) -> 'hints.mat4x4':
|
|
32
|
+
return glm.lookAt(
|
|
33
|
+
position,
|
|
34
|
+
glm.vec3(*position) + (rotation_glm := glm.quat(*angle)) * (0.0, 0.0, -1.0),
|
|
35
|
+
rotation_glm * (0.0, 1.0, 0.0),
|
|
36
|
+
).to_tuple()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ortho_projection_matrix(
|
|
40
|
+
size: 'hints.size2f',
|
|
41
|
+
near_far: tuple[float, float],
|
|
42
|
+
zoom: float = 1,
|
|
43
|
+
) -> 'hints.mat4x4':
|
|
44
|
+
width, height = size
|
|
45
|
+
|
|
46
|
+
left = -width / 2 / zoom
|
|
47
|
+
right = width / 2 / zoom
|
|
48
|
+
bottom = -height / 2 / zoom
|
|
49
|
+
top = height / 2 / zoom
|
|
50
|
+
|
|
51
|
+
return glm.ortho(
|
|
52
|
+
left,
|
|
53
|
+
right,
|
|
54
|
+
bottom,
|
|
55
|
+
top,
|
|
56
|
+
*near_far,
|
|
57
|
+
).to_tuple()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def perspecrive_projection_matrix(
|
|
61
|
+
fov: float,
|
|
62
|
+
aspect: float,
|
|
63
|
+
near_far: tuple[float, float],
|
|
64
|
+
zoom: float = 1,
|
|
65
|
+
) -> 'hints.mat4x4':
|
|
66
|
+
return glm.perspective(
|
|
67
|
+
glm.radians(fov / zoom),
|
|
68
|
+
aspect,
|
|
69
|
+
*near_far,
|
|
70
|
+
).to_tuple()
|