Skip to content

Commit 4ac8d67

Browse files
committed
Inline scene launcher regex, minor type fixes, remove scene.scale in favor of .main and .resize
1 parent 847e12b commit 4ac8d67

7 files changed

Lines changed: 56 additions & 81 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
[project.urls]
2-
GitHub = "https://github.com/BrokenSource/ShaderFlow"
3-
41
[project]
52
name = "shaderflow"
63
description = "🔥 Modular shader engine designed for simplicity and speed"
@@ -35,6 +32,9 @@ dependencies = [
3532
"xxhash",
3633
]
3734

35+
[project.urls]
36+
GitHub = "https://github.com/BrokenSource/ShaderFlow"
37+
3838
[project.scripts]
3939
shaderflow = "shaderflow.__main__:main"
4040
shader = "shaderflow.__main__:main"

shaderflow/exporting.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from subprocess import PIPE
1010
from tempfile import TemporaryFile as SafePipe
11-
from typing import TYPE_CHECKING, Any, Optional
11+
from typing import TYPE_CHECKING, Any, Optional, Union
1212

1313
import moderngl
1414
import tqdm
@@ -57,9 +57,13 @@ def tcp_output(self) -> bool:
5757
relay: Optional[Callable[[int, int], None]] = None
5858
bar: Optional[tqdm.tqdm] = None
5959

60+
@property
61+
def total_frames(self) -> int:
62+
return max(1, round(self.scene.runtime * self.scene.fps))
63+
6064
def open_bar(self) -> None:
6165
self.bar = tqdm.tqdm(
62-
total=self.scene.total_frames,
66+
total=self.total_frames,
6367
disable=((self.relay is False) or self.relay or self.scene.realtime),
6468
desc=f"Scene ({self.scene.name}) → Video",
6569
colour="#43BFEF",
@@ -73,14 +77,14 @@ def open_bar(self) -> None:
7377

7478
def update(self) -> None:
7579
if self.relay:
76-
self.relay(self.frame, self.scene.total_frames)
80+
self.relay(self.frame, self.total_frames)
7781
if self.bar:
7882
self.bar.update(1)
7983
self.frame += 1
8084

8185
@property
8286
def finished(self) -> bool:
83-
return (self.frame >= self.scene.total_frames)
87+
return (self.frame >= self.total_frames)
8488

8589
# # FFmpeg configuration
8690

@@ -98,7 +102,7 @@ def ffmpeg_sizes(self, width: int, height: int) -> None:
98102
self.ffmpeg.scale(width=width, height=height)
99103
self.ffmpeg.vflip()
100104

101-
def ffmpeg_output(self, output: str) -> None:
105+
def ffmpeg_output(self, output: Union[Path, str]) -> None:
102106
if (output in ("pipe", "-", bytes)):
103107
self.type = OutputType.PIPE
104108
self.ffmpeg.pipe_output()

shaderflow/launcher.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ class SceneLauncher:
2222
help_flags=[],
2323
))
2424

25-
tag: str = "Scene"
26-
"""Search classes that contains 'tag' in their inheritance"""
27-
28-
def common(self, package: Path=None) -> None:
25+
def common(self, package: Path) -> None:
2926
search = deque()
3027

3128
# Search all local files
@@ -48,16 +45,8 @@ def common(self, package: Path=None) -> None:
4845
for path in search:
4946
self.search(path)
5047

51-
@property
52-
def regex(self) -> re.Pattern:
53-
return re.compile(
54-
r"^class\s+(\w+)\s*\(.*?(?:" + self.tag + r").*\):\s*(?:\"\"\"((?:\n|.)*?)\"\"\")?",
55-
re.MULTILINE
56-
)
57-
5848
def search(self, script: Path) -> bool:
59-
if not (script := Path(script)).exists():
60-
return False
49+
"""Safe search classes without running script, add to cli"""
6150

6251
def wrapper(script: Path, clsname: str):
6352
def run(*args: Annotated[str, Parameter(
@@ -67,13 +56,14 @@ def run(*args: Annotated[str, Parameter(
6756
sys.argv[1:] = args
6857

6958
# Warn: Point of trust transfer to the file the user is running
70-
scene: ShaderScene = runpy.run_path(script)[clsname]()
59+
scene: ShaderScene = runpy.run_path(str(script))[clsname]()
7160
scene.cli.meta(args)
7261

7362
return run
7463

7564
# Match all projects and their optional docstrings
76-
matches = list(self.regex.finditer(script.read_text()))
65+
pattern = re.compile(r"^class\s+(\w+)\s*\(.*?(?:Scene).*\):\s*(?:\"\"\"((?:\n|.)*?)\"\"\")?", re.MULTILINE)
66+
matches = list(pattern.finditer(script.read_text()))
7767

7868
# Add a command for each match
7969
for match in matches:
@@ -82,7 +72,7 @@ def run(*args: Annotated[str, Parameter(
8272
wrapper(script, clsname),
8373
name=clsname.lower(),
8474
help=(docstring or "No description provided"),
85-
group=f"📦 {self.tag}s at ({script})",
75+
group=f"📦 Scenes at ({script})",
8676
)
8777

8878
return bool(matches)

shaderflow/resolution.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class Resolution:
77

88
@classmethod
99
def fit(cls,
10-
old: Optional[tuple[int, int]] = None,
11-
new: Optional[tuple[int, int]] = None,
12-
max: Optional[tuple[int, int]] = None,
10+
old: Optional[tuple[int | None, int | None]] = None,
11+
new: Optional[tuple[int | None, int | None]] = None,
12+
max: Optional[tuple[int | None, int | None]] = None,
1313
ar: Optional[float] = None,
1414
scale: float = 1.0,
1515
multiple: int = 2,

shaderflow/scene.py

Lines changed: 33 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
import contextlib
42
import gc
53
import importlib
@@ -10,7 +8,7 @@
108
from collections.abc import Iterable
119
from enum import Enum
1210
from pathlib import Path
13-
from typing import TYPE_CHECKING, Annotated, Any, Optional, Self, Union
11+
from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
1412

1513
import moderngl
1614
import numpy as np
@@ -36,7 +34,7 @@
3634
from shaderflow.variable import ShaderVariable, Uniform
3735

3836
if TYPE_CHECKING:
39-
from moderngl_window.context.base import BaseWindow as ModernglWindow
37+
from moderngl_window.context.glfw import Window as GlfwWindow
4038

4139
# ---------------------------------------------------------------------------- #
4240

@@ -45,17 +43,15 @@ class WindowBackend(Enum):
4543
GLFW = "glfw"
4644

4745
@classmethod
48-
def infer(cls) -> Self:
46+
def infer(cls) -> "WindowBackend":
4947

5048
# Optional external user override
5149
if (option := os.getenv("WINDOW_BACKEND")):
52-
if (value := cls.get(option)) is None:
53-
raise ValueError(f"Invalid window backend '{option}', options are {cls.values()}")
54-
return value
50+
return cls(option)
5551

5652
# Infer headless if exporting the scene via cli
5753
if ("main" in sys.argv) and (args := sys.argv[sys.argv.index("main"):]):
58-
if any(x in args for x in ("--render", "-r", "--output", "-o")):
54+
if any(x in args for x in ("--output", "-o")):
5955
return cls.Headless
6056

6157
return cls.GLFW
@@ -68,34 +64,34 @@ class ShaderScene(ShaderModule):
6864
backend: WindowBackend = Factory(WindowBackend.infer)
6965
"""ModernGL Window backend, cannot be changed after creation"""
7066

71-
window: 'ModernglWindow' = None
67+
window: 'GlfwWindow' = None # type: ignore
7268
"""ModernGL Window class instance at `moderngl_window.context.<backend>.Window`"""
7369

74-
opengl: moderngl.Context = None
70+
opengl: moderngl.Context = None # type: ignore
7571
"""ModernGL Context bound to this Scene"""
7672

77-
quality: float = field(default=50.0, converter=lambda x: min(max(0.0, float(x)), 100.0))
73+
quality: float = field(default=50.0, converter=float)
7874
"""Global quality level, if implemented on the shader/scene"""
7975

8076
# -------------------------------------------|
8177
# Modules
8278

83-
ffmpeg: FFmpeg = Factory(FFmpeg)
84-
"""FFmpeg configuration for exporting videos"""
85-
8679
modules: list[ShaderModule] = Factory(list)
8780
"""List of all Modules in order of addition (including self)"""
8881

89-
frametimer: ShaderFrametimer = None
82+
ffmpeg: FFmpeg = Factory(FFmpeg)
83+
"""FFmpeg configuration for exporting videos"""
84+
85+
frametimer: ShaderFrametimer = None # type: ignore
9086
"""Default Frametimer module"""
9187

92-
keyboard: ShaderKeyboard = None
88+
keyboard: ShaderKeyboard = None # type: ignore
9389
"""Default Keyboard module"""
9490

95-
camera: ShaderCamera = None
91+
camera: ShaderCamera = None # type: ignore
9692
"""Default Camera module"""
9793

98-
shader: ShaderProgram = None
94+
shader: ShaderProgram = None # type: ignore
9995
"""The main shader of the scene"""
10096

10197
def __del__(self):
@@ -110,7 +106,7 @@ def __del__(self):
110106
# -------------------------------------------|
111107
# Super Sampling Anti-Aliasing
112108

113-
_final: ShaderProgram = None
109+
_final: ShaderProgram = None # type: ignore
114110
"""Internal shader used for downsampling final frames"""
115111

116112
@property
@@ -138,7 +134,6 @@ def initialize(self) -> None:
138134
self.camera = ShaderCamera(scene=self)
139135

140136
# Linux: Use EGL for creating a OpenGL context, allows true headless with GPU acceleration
141-
# Note: (https://forums.developer.nvidia.com/t/81412) (https://brokensrc.dev/get/docker/)
142137
if (sys.platform == "linux") and (os.getenv("EGL", "1") == "1"):
143138
backend = "egl"
144139

@@ -265,10 +260,6 @@ def frame(self) -> int:
265260
def frame(self, value: int):
266261
self.time = (value / self.fps)
267262

268-
@property
269-
def total_frames(self) -> int:
270-
return max(1, round(self.runtime * self.fps))
271-
272263
# Total Duration
273264

274265
@property
@@ -280,7 +271,7 @@ def max_duration(self) -> float:
280271
"""The longest module duration"""
281272
return max((module.duration or 0.0) for module in self.modules)
282273

283-
def set_duration(self, override: float=None) -> float:
274+
def set_duration(self, override: Optional[float]=None) -> float:
284275
"""Either force the duration, find the longest module or use base duration"""
285276
self.runtime = (override or self.max_duration)
286277
self.runtime /= self.speed
@@ -319,20 +310,6 @@ def _window_proxy(self, attribute, value) -> Any:
319310
# -------------------------------------------------------------------------|
320311
# Resolution
321312

322-
# # Scale
323-
324-
_scale: float = field(default=1.0, converter=lambda x: max(0.01, x))
325-
326-
@property
327-
def scale(self) -> float:
328-
"""Resolution scale factor"""
329-
return self._scale
330-
331-
@scale.setter
332-
def scale(self, value: float):
333-
logger.debug(f"Changing Resolution Scale to ({value})")
334-
self.resize(scale=value)
335-
336313
# # Width
337314

338315
_width: int = field(default=1920)
@@ -343,7 +320,7 @@ def width(self) -> int:
343320

344321
@width.setter
345322
def width(self, value: int):
346-
self.resize(width=(value*self._scale))
323+
self.resize(width=value)
347324

348325
# # Height
349326

@@ -355,7 +332,7 @@ def height(self) -> int:
355332

356333
@height.setter
357334
def height(self, value: int):
358-
self.resize(height=(value*self._scale))
335+
self.resize(height=value)
359336

360337
# # SSAA
361338

@@ -396,15 +373,15 @@ def render_resolution(self) -> tuple[int, int]:
396373

397374
# # Aspect Ratio
398375

399-
_aspect_ratio: float = None
376+
_aspect_ratio: Optional[float] = None
400377

401378
@property
402379
def aspect_ratio(self) -> float:
403380
"""Either the forced `self._aspect_ratio` or dynamic from `self.width/self.height`"""
404381
return self._aspect_ratio or (self.width/self.height)
405382

406383
@aspect_ratio.setter
407-
def aspect_ratio(self, value: Union[float, str]):
384+
def aspect_ratio(self, value: Optional[Union[float, str]]):
408385
logger.debug(f"Changing Aspect Ratio to {value}")
409386

410387
# The aspect ratio can be sent as a fraction or "none", "false"
@@ -418,20 +395,23 @@ def aspect_ratio(self, value: Union[float, str]):
418395
self._aspect_ratio = value
419396

420397
if (self.backend == WindowBackend.GLFW) and (self._aspect_ratio is not None):
421-
__import__("glfw").set_window_aspect_ratio(self.window._window, 2**16, int(2**16 / self._aspect_ratio))
398+
import glfw
399+
glfw.set_window_aspect_ratio(
400+
self.window._window,
401+
2**16, int(2**16 / self._aspect_ratio)
402+
)
422403

423404
def resize(self,
424-
width: Optional[int | float] = None,
425-
height: Optional[int | float] = None,
405+
width: Optional[int] = None,
406+
height: Optional[int] = None,
426407
ratio: Optional[float | str] = None,
427408
bounds: Optional[tuple[int, int]] = None,
428-
scale: Optional[float] = None,
429409
ssaa: Optional[float] = None,
410+
scale: float = 1.0,
430411
) -> tuple[int, int]:
431412

432413
# Maybe update auxiliary properties
433414
self.aspect_ratio = (ratio or self._aspect_ratio)
434-
self._scale = (scale or self._scale)
435415
self._ssaa = (ssaa or self._ssaa)
436416

437417
# The parameters aren't trivial. The idea is to fit resolution from the scale-less components,
@@ -441,7 +421,7 @@ def resize(self,
441421
new=(width, height),
442422
max=bounds,
443423
ar=self._aspect_ratio,
444-
scale=self._scale,
424+
scale=scale,
445425
)
446426

447427
# Optimization: Only resize if target is different
@@ -516,9 +496,9 @@ def main(self, *,
516496
help="Height of the rendering resolution (None to keep or find by --ar aspect ratio)",
517497
group="🔴 Basic", name=("height", "-h"))] = 1080,
518498

519-
scale: Annotated[Optional[float], Parameter(
499+
scale: Annotated[float, Parameter(
520500
help="Post-multiply width and height by a scale factor (None to keep)",
521-
group="🔴 Basic", name=("scale", "-S"))] = None,
501+
group="🔴 Basic", name=("scale", "-x"))] = 1.0,
522502

523503
ratio: Annotated[Optional[Union[float, str]], Parameter(
524504
help="Force resolution aspect ratio (Examples: '16:9', '16/9', '1.777') (None for dynamic)",
@@ -617,7 +597,7 @@ def main(self, *,
617597
if (self.exporting):
618598
export.ffmpeg_clean()
619599
export.ffmpeg_sizes(width=_width, height=_height)
620-
export.ffmpeg_output(output)
600+
export.ffmpeg_output(output) # type: ignore
621601
export.make_buffers(buffers)
622602
export.ffhook()
623603
export.popen()

shaderflow/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class CycloUtils:
88

99
@staticmethod
10-
def chain(app: App) -> callable:
10+
def chain(app: App):
1111
"""Implements known commands chaining in meta app"""
1212
def meta(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)]):
1313
if (splits := [n for (n, token) in enumerate(tokens) if token in app]):

website/about/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ icon: material/file-document-edit
1010
!!! quote ""
1111
- Move website to tremeschin.com domain
1212
- Fix `Scene.resolution = (w, h)` using positionals in `Scene.resize`
13+
- Simplify remove `scene._scale` field in favor of `.main()` or `.resize()` argument
1314

1415
### 📦 v0.10.0 <small>March 12, 2026</small> {#v0.10.0}
1516

0 commit comments

Comments
 (0)