shaderflow 0.4.0__py2.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.
- ShaderFlow/Common/Imgui.py +0 -0
- ShaderFlow/Common/Notes.py +134 -0
- ShaderFlow/Common/__init__.py +0 -0
- ShaderFlow/Message.py +147 -0
- ShaderFlow/Module.py +162 -0
- ShaderFlow/Modules/Audio.py +431 -0
- ShaderFlow/Modules/Bouncing.py +94 -0
- ShaderFlow/Modules/Camera.py +451 -0
- ShaderFlow/Modules/Depth.py +0 -0
- ShaderFlow/Modules/Dynamics.py +288 -0
- ShaderFlow/Modules/Frametimer.py +79 -0
- ShaderFlow/Modules/Keyboard.py +43 -0
- ShaderFlow/Modules/Noise.py +80 -0
- ShaderFlow/Modules/OpticalFlow.py +1 -0
- ShaderFlow/Modules/Piano.py +362 -0
- ShaderFlow/Modules/Spectrogram.py +315 -0
- ShaderFlow/Modules/Video.py +234 -0
- ShaderFlow/Modules/Waveform.py +100 -0
- ShaderFlow/Modules/__init__.py +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.png +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.svg +1 -0
- ShaderFlow/Resources/Scenes/Examples/Demo.py +284 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bars.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bouncing.frag +50 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Simulation.glsl +49 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Visuals.glsl +31 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/MotionBlur.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Multipass.frag +43 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/RayMarch.frag +59 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/ShaderToy.frag +65 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Video.frag +5 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Visualizer.frag +75 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Waveform.frag +18 -0
- ShaderFlow/Resources/Shaders/Fragment/Default.glsl +65 -0
- ShaderFlow/Resources/Shaders/Fragment/Final.glsl +7 -0
- ShaderFlow/Resources/Shaders/Fragment/Loading.frag +40 -0
- ShaderFlow/Resources/Shaders/Fragment/Missing.glsl +23 -0
- ShaderFlow/Resources/Shaders/Include/Camera.glsl +166 -0
- ShaderFlow/Resources/Shaders/Include/Complex.glsl +0 -0
- ShaderFlow/Resources/Shaders/Include/ShaderFlow.glsl +444 -0
- ShaderFlow/Resources/Shaders/Vertex/Default.glsl +21 -0
- ShaderFlow/Scene.py +1124 -0
- ShaderFlow/Shader.py +391 -0
- ShaderFlow/Texture.py +528 -0
- ShaderFlow/Variable.py +128 -0
- ShaderFlow/__init__.py +11 -0
- ShaderFlow/__main__.py +22 -0
- shaderflow-0.4.0.dist-info/METADATA +83 -0
- shaderflow-0.4.0.dist-info/RECORD +51 -0
- shaderflow-0.4.0.dist-info/WHEEL +5 -0
- shaderflow-0.4.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import math
|
|
3
|
+
from typing import Any, Self
|
|
4
|
+
|
|
5
|
+
from attr import define
|
|
6
|
+
|
|
7
|
+
from Broken import BrokenFluentBuilder
|
|
8
|
+
|
|
9
|
+
PIANO_NOTES = "C C# D D# E F F# G G# A A# B".split()
|
|
10
|
+
|
|
11
|
+
@define(eq=False)
|
|
12
|
+
class BrokenPianoNote(BrokenFluentBuilder):
|
|
13
|
+
note: int = 60
|
|
14
|
+
start: float = 0
|
|
15
|
+
end: float = 0
|
|
16
|
+
channel: int = 0
|
|
17
|
+
velocity: int = 100
|
|
18
|
+
tuning: float = 440
|
|
19
|
+
|
|
20
|
+
def __hash__(self):
|
|
21
|
+
return hash((self.note, self.start, self.end, self.channel, self.velocity))
|
|
22
|
+
|
|
23
|
+
def idk_hash(self):
|
|
24
|
+
return hash((self.note, self.channel))
|
|
25
|
+
|
|
26
|
+
def __eq__(self, other):
|
|
27
|
+
return hash(self) == hash(other)
|
|
28
|
+
|
|
29
|
+
# # Initialization
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
@functools.lru_cache
|
|
33
|
+
def from_index(cls, note: int, **kwargs) -> Self:
|
|
34
|
+
return cls(note=note, **kwargs)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
@functools.lru_cache
|
|
38
|
+
def from_name(cls, name: str, **kwargs) -> Self:
|
|
39
|
+
return cls(note=BrokenPianoNote.name_to_index(name), **kwargs)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
@functools.lru_cache
|
|
43
|
+
def from_frequency(cls, frequency: float, **kwargs) -> Self:
|
|
44
|
+
return cls(note=BrokenPianoNote.frequency_to_index(frequency), **kwargs)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
@functools.lru_cache
|
|
48
|
+
def get(cls, object: Any, **kwargs) -> Self:
|
|
49
|
+
if isinstance(object, BrokenPianoNote):
|
|
50
|
+
return object(**kwargs)
|
|
51
|
+
elif isinstance(object, int):
|
|
52
|
+
return cls.from_index(object, **kwargs)
|
|
53
|
+
elif isinstance(object, str):
|
|
54
|
+
return cls.from_name(object, **kwargs)
|
|
55
|
+
elif isinstance(object, float):
|
|
56
|
+
return cls.from_frequency(object, **kwargs)
|
|
57
|
+
return cls(**kwargs)
|
|
58
|
+
|
|
59
|
+
# # Conversion
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
@functools.lru_cache
|
|
63
|
+
def index_to_name(index: int) -> str:
|
|
64
|
+
return f"{PIANO_NOTES[index % 12]}{index//12 - 1}"
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
@functools.lru_cache
|
|
68
|
+
def index_to_frequency(index: int, *, tuning: float=440) -> float:
|
|
69
|
+
return tuning * 2**((index - 69)/12)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
@functools.lru_cache
|
|
73
|
+
def name_to_index(name: str) -> int:
|
|
74
|
+
note, octave = name[:-1].upper(), int(name[-1])
|
|
75
|
+
return PIANO_NOTES.index(note) + 12*(octave + 1)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
@functools.lru_cache
|
|
79
|
+
def name_to_frequency(name: str, *, tuning: float=440) -> float:
|
|
80
|
+
return BrokenPianoNote.index_to_frequency(BrokenPianoNote.name_to_index(name), tuning=tuning)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
@functools.lru_cache
|
|
84
|
+
def frequency_to_index(frequency: float, *, tuning: float=440) -> int:
|
|
85
|
+
return round(12*math.log2(frequency/tuning) + 69)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
@functools.lru_cache
|
|
89
|
+
def frequency_to_name(frequency: float, *, tuning: float=440) -> str:
|
|
90
|
+
return BrokenPianoNote.index_to_name(BrokenPianoNote.frequency_to_index(frequency, tuning=tuning))
|
|
91
|
+
|
|
92
|
+
# # Utilities
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def frequency(self) -> float:
|
|
96
|
+
return BrokenPianoNote.index_to_frequency(self.note, tuning=self.tuning)
|
|
97
|
+
|
|
98
|
+
@frequency.setter
|
|
99
|
+
def frequency(self, value: float):
|
|
100
|
+
self.note = BrokenPianoNote.frequency_to_index(value, tuning=self.tuning)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return BrokenPianoNote.index_to_name(self.note)
|
|
105
|
+
|
|
106
|
+
@name.setter
|
|
107
|
+
def name(self, value: str):
|
|
108
|
+
self.note = BrokenPianoNote.name_to_index(value)
|
|
109
|
+
|
|
110
|
+
# Black and White
|
|
111
|
+
|
|
112
|
+
def is_white(note: int) -> bool:
|
|
113
|
+
return (note % 12) in {0, 2, 4, 5, 7, 9, 11}
|
|
114
|
+
|
|
115
|
+
def is_black(note: int) -> bool:
|
|
116
|
+
return (note % 12) in {1, 3, 6, 8, 10}
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def white(self) -> bool:
|
|
120
|
+
return BrokenPianoNote.is_white(self.note)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def black(self) -> bool:
|
|
124
|
+
return BrokenPianoNote.is_black(self.note)
|
|
125
|
+
|
|
126
|
+
# Temporal
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def duration(self):
|
|
130
|
+
return self.end - self.start
|
|
131
|
+
|
|
132
|
+
@duration.setter
|
|
133
|
+
def duration(self, value: float):
|
|
134
|
+
self.end = self.start + value
|
|
File without changes
|
ShaderFlow/Message.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from attr import define
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShaderMessage:
|
|
7
|
+
|
|
8
|
+
# # Special
|
|
9
|
+
|
|
10
|
+
class Custom:
|
|
11
|
+
"""Any data type"""
|
|
12
|
+
data: Any
|
|
13
|
+
|
|
14
|
+
# # Mouse
|
|
15
|
+
|
|
16
|
+
class Mouse:
|
|
17
|
+
|
|
18
|
+
@define
|
|
19
|
+
class Position:
|
|
20
|
+
# Real
|
|
21
|
+
x: int = 0
|
|
22
|
+
y: int = 0
|
|
23
|
+
dx: int = 0
|
|
24
|
+
dy: int = 0
|
|
25
|
+
|
|
26
|
+
# Normalized
|
|
27
|
+
u: float = 0.0
|
|
28
|
+
v: float = 0.0
|
|
29
|
+
du: float = 0.0
|
|
30
|
+
dv: float = 0.0
|
|
31
|
+
|
|
32
|
+
@define
|
|
33
|
+
class Press:
|
|
34
|
+
button: int = 0
|
|
35
|
+
|
|
36
|
+
# Real
|
|
37
|
+
x: int = 0
|
|
38
|
+
y: int = 0
|
|
39
|
+
|
|
40
|
+
# Normalized
|
|
41
|
+
u: float = 0.0
|
|
42
|
+
v: float = 0.0
|
|
43
|
+
|
|
44
|
+
@define
|
|
45
|
+
class Release:
|
|
46
|
+
button: int = 0
|
|
47
|
+
|
|
48
|
+
# Real
|
|
49
|
+
x: int = 0
|
|
50
|
+
y: int = 0
|
|
51
|
+
|
|
52
|
+
# Normalized
|
|
53
|
+
u: float = 0.0
|
|
54
|
+
v: float = 0.0
|
|
55
|
+
|
|
56
|
+
@define
|
|
57
|
+
class Drag:
|
|
58
|
+
# Real
|
|
59
|
+
x: int = 0
|
|
60
|
+
y: int = 0
|
|
61
|
+
dx: int = 0
|
|
62
|
+
dy: int = 0
|
|
63
|
+
|
|
64
|
+
# Normalized
|
|
65
|
+
u: float = 0.0
|
|
66
|
+
v: float = 0.0
|
|
67
|
+
du: float = 0.0
|
|
68
|
+
dv: float = 0.0
|
|
69
|
+
|
|
70
|
+
@define
|
|
71
|
+
class Scroll:
|
|
72
|
+
# Real
|
|
73
|
+
dx: int = 0
|
|
74
|
+
dy: int = 0
|
|
75
|
+
|
|
76
|
+
# Normalized
|
|
77
|
+
du: float = 0.0
|
|
78
|
+
dv: float = 0.0
|
|
79
|
+
|
|
80
|
+
@define
|
|
81
|
+
class Enter:
|
|
82
|
+
state: bool
|
|
83
|
+
|
|
84
|
+
# # Window
|
|
85
|
+
|
|
86
|
+
class Window:
|
|
87
|
+
|
|
88
|
+
@define
|
|
89
|
+
class Resize:
|
|
90
|
+
width: int = None
|
|
91
|
+
height: int = None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def size(self) -> tuple[int, int]:
|
|
95
|
+
return self.width, self.height
|
|
96
|
+
|
|
97
|
+
@define
|
|
98
|
+
class Iconify:
|
|
99
|
+
state: bool = None
|
|
100
|
+
|
|
101
|
+
@define
|
|
102
|
+
class FileDrop:
|
|
103
|
+
files: list[str] = None
|
|
104
|
+
|
|
105
|
+
@define
|
|
106
|
+
class Close:
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
# # Shader
|
|
110
|
+
|
|
111
|
+
class Shader:
|
|
112
|
+
|
|
113
|
+
@define
|
|
114
|
+
class RecreateTextures:
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
@define
|
|
118
|
+
class Compile:
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
@define
|
|
122
|
+
class Render:
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
# # Keyboard
|
|
126
|
+
|
|
127
|
+
class Keyboard:
|
|
128
|
+
|
|
129
|
+
@define
|
|
130
|
+
class Press:
|
|
131
|
+
key: int = None
|
|
132
|
+
action: int = None
|
|
133
|
+
modifiers: int = None
|
|
134
|
+
|
|
135
|
+
@define
|
|
136
|
+
class KeyDown:
|
|
137
|
+
key: int = None
|
|
138
|
+
modifiers: int = None
|
|
139
|
+
|
|
140
|
+
@define
|
|
141
|
+
class KeyUp:
|
|
142
|
+
key: int = None
|
|
143
|
+
modifiers: int = None
|
|
144
|
+
|
|
145
|
+
@define
|
|
146
|
+
class Unicode:
|
|
147
|
+
char: str = None
|
ShaderFlow/Module.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING, Iterable, Self, Type, Union
|
|
6
|
+
|
|
7
|
+
from attr import Factory, define, field
|
|
8
|
+
|
|
9
|
+
from Broken import BrokenAttrs, BrokenFluentBuilder, log
|
|
10
|
+
from Broken.Externals.FFmpeg import BrokenFFmpeg
|
|
11
|
+
from ShaderFlow.Message import ShaderMessage
|
|
12
|
+
from ShaderFlow.Variable import ShaderVariable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@define
|
|
16
|
+
class ShaderModule(BrokenFluentBuilder, BrokenAttrs):
|
|
17
|
+
scene: ShaderScene = field(default=None, repr=False)
|
|
18
|
+
"""The ShaderScene this module belongs to. Must be set on initialization of any module with
|
|
19
|
+
`ShaderModule(scene=...)` even though marked as `default=None` to make flexible MRO parenting"""
|
|
20
|
+
|
|
21
|
+
uuid: int = Factory(itertools.count(1).__next__)
|
|
22
|
+
"""A module identifier, mostly used for differentiating log statements of same type modules"""
|
|
23
|
+
|
|
24
|
+
name: str = None
|
|
25
|
+
"""The base name for exported GLSL variables, textures, etc. It is technically optional, but
|
|
26
|
+
it's not a bad idea for all modules to have a default value for this attribute than None"""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def who(self) -> str:
|
|
30
|
+
return f"({self.uuid:>2}) {type(self).__name__[:18].ljust(18)} │ ▸"
|
|
31
|
+
|
|
32
|
+
def __post__(self):
|
|
33
|
+
# Post-import for type checking to avoid circular reference
|
|
34
|
+
from ShaderFlow.Scene import ShaderScene
|
|
35
|
+
|
|
36
|
+
# The module can be a ShaderScene itself
|
|
37
|
+
self.scene = self.scene or self
|
|
38
|
+
|
|
39
|
+
# Module must be part of a 'scene=instance(ShaderScene)'
|
|
40
|
+
if not isinstance(self.scene, ShaderScene):
|
|
41
|
+
raise RuntimeError(log.error('\n'.join((
|
|
42
|
+
f"Module of type '{type(self).__name__}' must be added to a 'ShaderScene' instance",
|
|
43
|
+
f"• Initialize it with {type(self).__name__}(scene='instance(ShaderScene)', ...)",
|
|
44
|
+
))))
|
|
45
|
+
|
|
46
|
+
log.trace(f"{self.who} Module added to the Scene")
|
|
47
|
+
self.scene.modules.append(self)
|
|
48
|
+
self.commands()
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def commands(self) -> None:
|
|
52
|
+
"""Add commands to the scene with `self.scene.typer.command(target=..., ...)`"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
# # Messaging
|
|
56
|
+
|
|
57
|
+
def relay(self, message: Union[ShaderMessage, Type[ShaderMessage]]) -> Self:
|
|
58
|
+
"""Send a message to all modules in the scene. Handle it defining a `self.handle(message)`"""
|
|
59
|
+
if isinstance(message, type):
|
|
60
|
+
message = message()
|
|
61
|
+
for module in self.scene.modules:
|
|
62
|
+
module.handle(message)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def handle(self, message: ShaderMessage) -> None:
|
|
67
|
+
"""Whenever a module relays a message on the scene, all modules are signaled via this method
|
|
68
|
+
for potentially acting on it. A Camera might move on WASD keys, for example"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def find(self, type: Type[ShaderModule]) -> Iterable[ShaderModule]:
|
|
72
|
+
for module in self.scene.modules:
|
|
73
|
+
if isinstance(module, type):
|
|
74
|
+
yield module
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def duration(self) -> float:
|
|
79
|
+
"""Self-reported 'time to render' until the module is finished. A ShaderAudio shall export
|
|
80
|
+
the input audio duration, for example"""
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def build(self) -> None:
|
|
85
|
+
"""Create Textures, child ShaderModules, load base shaders, etc. Happens only once, and it's
|
|
86
|
+
a good place to set default values for attributes, such as a background image that can be
|
|
87
|
+
later changed on `self.setup()` or, better yet, on the CLI of the module/custom Scene"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def setup(self) -> None:
|
|
92
|
+
"""Called every time before the initialization (rendering) of the Scene. Useful for managing
|
|
93
|
+
the behavior of batch exporting per export index; also a good place to reset values to their
|
|
94
|
+
defaults or create procedural objects (seeds) after `self.build()`"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def pipeline(self) -> Iterable[ShaderVariable]:
|
|
99
|
+
"""Returns the list of variables that will be exported to all Shaders of the scene. The
|
|
100
|
+
first compilation happens after `self.build()`, where all variables are metaprogrammed into
|
|
101
|
+
the GLSL code. Subsequent calls happens after all `self.update()` on every frame and the
|
|
102
|
+
variables are updated to the yielded values here"""
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def update(self) -> None:
|
|
107
|
+
"""Called every frame. This defines the main behavior of the module inside the event loop.
|
|
108
|
+
All non-ShaderModules are called first, then the former ones. Access state data directly
|
|
109
|
+
on the Scene with `self.scene.{dt,time,width,height,...}`"""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def ffmpeg(self, ffmpeg: BrokenFFmpeg) -> None:
|
|
114
|
+
"""When exporting the Scene, after the initial CLI configuration of FFmpeg by the Scene's
|
|
115
|
+
`self.main` method, all modules have an option to change the FFmpeg settings on the fly.
|
|
116
|
+
Note that this can also be implemented on a custom Scene itself, and behavior _can_ be
|
|
117
|
+
changed per batch exporting"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------|
|
|
121
|
+
# Stuff pending a remaster
|
|
122
|
+
|
|
123
|
+
def __process_include__(self, include: str) -> str:
|
|
124
|
+
return include.format({f"${k}": v for k, v in vars(self).items()})
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def includes(self) -> Iterable[str]:
|
|
128
|
+
yield ""
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def defines(self) -> Iterable[str]:
|
|
132
|
+
yield None
|
|
133
|
+
|
|
134
|
+
# # User interface
|
|
135
|
+
|
|
136
|
+
def __shaderflow_ui__(self) -> None:
|
|
137
|
+
"""Basic info of a Module"""
|
|
138
|
+
# Todo: Make automatic Imgui methods
|
|
139
|
+
|
|
140
|
+
# Module - self.__ui__ must be implemented
|
|
141
|
+
if not getattr(self.__ui__, "__isabstractmethod__", False):
|
|
142
|
+
self.__ui__()
|
|
143
|
+
|
|
144
|
+
# Module - self.ui must be implemented
|
|
145
|
+
if not getattr(self.ui, "__isabstractmethod__", False):
|
|
146
|
+
self.ui()
|
|
147
|
+
|
|
148
|
+
@abstractmethod
|
|
149
|
+
def __ui__(self) -> None:
|
|
150
|
+
"""Internal method for self.ui"""
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def ui(self) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Draw the UI for this module
|
|
157
|
+
"""
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Avoid circular reference on Module importing partial Scene
|
|
161
|
+
if TYPE_CHECKING:
|
|
162
|
+
from ShaderFlow.Scene import ShaderScene
|