fluxrender 0.1.4__tar.gz → 0.2.0__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.
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/__init__.py +2 -2
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/core.py +43 -12
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/entities.py +33 -1
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/math_engine.py +8 -3
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/probes.py +38 -5
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/regions.py +1 -1
- fluxrender-0.2.0/FluxRender/shortcuts.py +302 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/ui.py +623 -122
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/validators.py +31 -0
- {fluxrender-0.1.4/fluxrender.egg-info → fluxrender-0.2.0}/PKG-INFO +1 -1
- {fluxrender-0.1.4 → fluxrender-0.2.0/fluxrender.egg-info}/PKG-INFO +1 -1
- {fluxrender-0.1.4 → fluxrender-0.2.0}/pyproject.toml +1 -1
- fluxrender-0.1.4/FluxRender/shortcuts.py +0 -138
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/colors.py +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/constants.py +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/graphics.py +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/README.md +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/SOURCES.txt +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/dependency_links.txt +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/requires.txt +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/top_level.txt +0 -0
- {fluxrender-0.1.4 → fluxrender-0.2.0}/setup.cfg +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from .core import Scene, CoordinateSystem
|
|
2
|
-
from .ui import Button, UIStyle, Grid, Axis, VBox, HBox, create_mode_switch, create_property_switch, create_color_scale_switch
|
|
2
|
+
from .ui import Button, UIStyle, Grid, Axis, VBox, HBox, create_mode_switch, create_property_switch, create_color_scale_switch, DynamicText, create_cursor_probe_display
|
|
3
3
|
from .entities import VectorField, ParticleSystem
|
|
4
4
|
from .regions import CircularRegion, CursorRegion
|
|
5
5
|
from .math_engine import VectorMathEngine
|
|
6
6
|
from .probes import DataProbe
|
|
7
|
-
from .shortcuts import create_workspace, quick_simulate
|
|
7
|
+
from .shortcuts import create_workspace, quick_simulate, as_vector_field
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from .constants import Align, Property, ArrowStyle, FieldMode, ScaleType
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import atexit
|
|
8
8
|
|
|
9
9
|
from .colors import ColorSequence
|
|
10
|
-
from .validators import _fatal_error, PositiveInt, StrictBool, StrictString, NumberRange
|
|
10
|
+
from .validators import _fatal_error, PositiveInt, StrictBool, StrictString, NumberRange, _count_function_parameters
|
|
11
11
|
|
|
12
12
|
# region initial settings [blue]
|
|
13
13
|
|
|
@@ -120,6 +120,7 @@ class CoordinateSystem:
|
|
|
120
120
|
remain circular).
|
|
121
121
|
|
|
122
122
|
Attributes:
|
|
123
|
+
resolution (tuple): The screen resolution as (width, height) in pixels.
|
|
123
124
|
width (int): Screen width in pixels.
|
|
124
125
|
height (int): Screen height in pixels.
|
|
125
126
|
x_min (float): The actual lower bound of the mathematical X axis.
|
|
@@ -135,7 +136,7 @@ class CoordinateSystem:
|
|
|
135
136
|
keep_aspect_ratio = StrictBool()
|
|
136
137
|
|
|
137
138
|
|
|
138
|
-
def __init__(self, x_range: tuple, y_range: tuple
|
|
139
|
+
def __init__(self, x_range: tuple = (-8, 8), y_range: tuple = (-8, 8), resolution: tuple = (1200, 800), keep_aspect_ratio: bool = False):
|
|
139
140
|
"""
|
|
140
141
|
Initializes the coordinate system with specific bounds and screen dimensions.
|
|
141
142
|
|
|
@@ -147,8 +148,7 @@ class CoordinateSystem:
|
|
|
147
148
|
Args:
|
|
148
149
|
x_range (tuple[float, float]): The desired (min, max) values for the X axis.
|
|
149
150
|
y_range (tuple[float, float]): The desired (min, max) values for the Y axis.
|
|
150
|
-
|
|
151
|
-
height (int): Window/buffer height in pixels.
|
|
151
|
+
resolution (tuple[int, int], optional): The screen resolution as (width, height) in pixels. Defaults to (1200, 800).
|
|
152
152
|
keep_aspect_ratio (bool, optional): If True, adjusts x_range or y_range
|
|
153
153
|
to preserve 1:1 scaling (square pixels). Defaults to False.
|
|
154
154
|
|
|
@@ -156,18 +156,19 @@ class CoordinateSystem:
|
|
|
156
156
|
Example:
|
|
157
157
|
To create a coordinate system that maps the mathematical range of -2 to 2 on both axes to a screen resolution of 1800x950 pixels without keeping the aspect ratio:
|
|
158
158
|
```python
|
|
159
|
-
coords = CoordinateSystem((-2, 2), (-2, 2), 1800, 950, keep_aspect_ratio=False)
|
|
159
|
+
coords = CoordinateSystem((-2, 2), (-2, 2), (1800, 950), keep_aspect_ratio=False)
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
To create the same coordinate system but with aspect ratio correction (ensuring circles look like circles):
|
|
163
163
|
```python
|
|
164
|
-
coords = CoordinateSystem((-2, 2), (-2, 2), 1800, 950, keep_aspect_ratio=True)
|
|
164
|
+
coords = CoordinateSystem((-2, 2), (-2, 2), (1800, 950), keep_aspect_ratio=True)
|
|
165
165
|
```
|
|
166
166
|
Then, in the above case, the ranges on the X and Y axis will be adjusted accordingly to maintain the aspect ratio.
|
|
167
167
|
"""
|
|
168
168
|
|
|
169
|
-
self.
|
|
170
|
-
self.
|
|
169
|
+
self.resolution = resolution
|
|
170
|
+
self.width = resolution[0]
|
|
171
|
+
self.height = resolution[1]
|
|
171
172
|
|
|
172
173
|
self.math_width = x_range[1] - x_range[0]
|
|
173
174
|
self.math_height = y_range[1] - y_range[0]
|
|
@@ -175,7 +176,7 @@ class CoordinateSystem:
|
|
|
175
176
|
y_center = (y_range[0] + y_range[1]) / 2.0
|
|
176
177
|
|
|
177
178
|
if keep_aspect_ratio:
|
|
178
|
-
screen_ratio = width / height
|
|
179
|
+
screen_ratio = self.width / self.height
|
|
179
180
|
data_ratio = self.math_width / self.math_height
|
|
180
181
|
|
|
181
182
|
if screen_ratio < data_ratio:
|
|
@@ -416,7 +417,7 @@ class Scene:
|
|
|
416
417
|
```python
|
|
417
418
|
import FluxRender as fr
|
|
418
419
|
|
|
419
|
-
coords = fr.CoordinateSystem((-10, 10), (-10, 10), 1200, 800, keep_aspect_ratio=True)
|
|
420
|
+
coords = fr.CoordinateSystem(x_range=(-10, 10), y_range=(-10, 10), resolution=(1200, 800), keep_aspect_ratio=True)
|
|
420
421
|
|
|
421
422
|
scene = fr.Scene(
|
|
422
423
|
"My Simulation",
|
|
@@ -445,6 +446,7 @@ class Scene:
|
|
|
445
446
|
self.scene_layer = ti.Vector.field(4, dtype=float, shape=(self.width, self.height))
|
|
446
447
|
self.ui_layer = ti.Vector.field(4, dtype=float, shape=(self.width, self.height))
|
|
447
448
|
self.ui_layer.fill(0)
|
|
449
|
+
self.dynamic_ui_layer = ti.Vector.field(4, dtype=float, shape=(self.width, self.height))
|
|
448
450
|
self.pixels = ti.Vector.field(3, dtype=float, shape=(self.width, self.height))
|
|
449
451
|
|
|
450
452
|
self.zoom_speed = 0.02
|
|
@@ -452,6 +454,7 @@ class Scene:
|
|
|
452
454
|
self._use_trails = False
|
|
453
455
|
|
|
454
456
|
self.objects = []
|
|
457
|
+
self.functions = []
|
|
455
458
|
|
|
456
459
|
self.time = 0.0
|
|
457
460
|
self.dt = 0.005
|
|
@@ -498,6 +501,7 @@ class Scene:
|
|
|
498
501
|
self._fade_trails(self.trail_fade_factor)
|
|
499
502
|
|
|
500
503
|
self.scene_layer.fill(0) # Completely clear scene_layer
|
|
504
|
+
self.dynamic_ui_layer.fill(0) # Clear dynamic UI layer
|
|
501
505
|
|
|
502
506
|
# Handling user interactions (e.g., mouse clicks)
|
|
503
507
|
mx, my = window.get_cursor_pos()
|
|
@@ -530,6 +534,9 @@ class Scene:
|
|
|
530
534
|
obj.render(self)
|
|
531
535
|
obj.update(self)
|
|
532
536
|
|
|
537
|
+
# Execute all registered callable functions
|
|
538
|
+
for func in self.functions:
|
|
539
|
+
func()
|
|
533
540
|
|
|
534
541
|
# User update() function executed every frame
|
|
535
542
|
if user_update_func:
|
|
@@ -555,6 +562,7 @@ class Scene:
|
|
|
555
562
|
trail = self.trail_layer[i, j]
|
|
556
563
|
overlay = self.scene_layer[i, j]
|
|
557
564
|
ui = self.ui_layer[i, j]
|
|
565
|
+
dynamic_ui = self.dynamic_ui_layer[i, j]
|
|
558
566
|
|
|
559
567
|
# background + trail_layer
|
|
560
568
|
color_step1 = trail.xyz * trail.w + self._background_color_ti * (1.0 - trail.w)
|
|
@@ -563,7 +571,10 @@ class Scene:
|
|
|
563
571
|
color_step2 = overlay.xyz * overlay.w + color_step1 * (1.0 - overlay.w)
|
|
564
572
|
|
|
565
573
|
# color_step2 + ui_layer
|
|
566
|
-
|
|
574
|
+
color_step3 = ui.xyz * ui.w + color_step2 * (1.0 - ui.w)
|
|
575
|
+
|
|
576
|
+
# color_step3 + dynamic_ui_layer
|
|
577
|
+
final_rgb = dynamic_ui.xyz * dynamic_ui.w + color_step3 * (1.0 - dynamic_ui.w)
|
|
567
578
|
|
|
568
579
|
self.pixels[i, j] = final_rgb
|
|
569
580
|
|
|
@@ -601,7 +612,27 @@ class Scene:
|
|
|
601
612
|
self.objects.append(new_object)
|
|
602
613
|
self._process_addition(new_object)
|
|
603
614
|
if hasattr(new_object, '_init'):
|
|
604
|
-
new_object._init
|
|
615
|
+
num_of_parameters = _count_function_parameters(new_object._init)
|
|
616
|
+
if num_of_parameters == 1:
|
|
617
|
+
new_object._init(self)
|
|
618
|
+
elif num_of_parameters == 0:
|
|
619
|
+
new_object._init()
|
|
620
|
+
|
|
621
|
+
def add_callable(self, func):
|
|
622
|
+
"""
|
|
623
|
+
Adds a callable function to be executed every frame.
|
|
624
|
+
|
|
625
|
+
The function must be a callable object (e.g., a function or a lambda).
|
|
626
|
+
It will be called on every frame update, just before the rendering phase.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
func (Callable): A callable object to be executed every frame. It can be used for custom animations, state updates, or any logic that needs to run continuously.
|
|
630
|
+
"""
|
|
631
|
+
|
|
632
|
+
if not callable(func):
|
|
633
|
+
_fatal_error("The provided object must be callable (e.g., a function or a lambda).", "TypeError")
|
|
634
|
+
|
|
635
|
+
self.functions.append(func)
|
|
605
636
|
|
|
606
637
|
def _process_addition(self, entity):
|
|
607
638
|
"""
|
|
@@ -21,7 +21,7 @@ class Renderable:
|
|
|
21
21
|
def update(self, scene: cr.Scene):
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
|
-
def _init(self, scene: cr.Scene):
|
|
24
|
+
def _init(self, scene: cr.Scene = None):
|
|
25
25
|
pass
|
|
26
26
|
|
|
27
27
|
class VectorEntity(Renderable):
|
|
@@ -272,6 +272,38 @@ class VectorEntity(Renderable):
|
|
|
272
272
|
|
|
273
273
|
return self.math_engine._safe_evaluate_scalar_function(user_defined_function, *spatial_arguments)
|
|
274
274
|
|
|
275
|
+
def evaluate_field_and_property(self, property_type: Property | None, spatial_coordinate_x: float, spatial_coordinate_y: float):
|
|
276
|
+
"""
|
|
277
|
+
Evaluates the primary vector function and the specified property at the given spatial coordinates.
|
|
278
|
+
|
|
279
|
+
This method behaves exactly like evaluate_vector_field, but additionally calculates
|
|
280
|
+
a scalar value given by property_type (e.g. divergence, rotation, velocity).
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
property_type (Property | None): The specific property to calculate based on the evaluated vector field. If set to None, the method will only evaluate the primary vector function and bypass any property calculations for maximum performance when only vector components are needed.
|
|
284
|
+
spatial_coordinate_x (float / ndarray): The x-coordinate(s) in the mathematical world space.
|
|
285
|
+
spatial_coordinate_y (float / ndarray): The y-coordinate(s) in the mathematical world space.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
tuple: A tuple (vector_x, vector_y, property_value) where:
|
|
289
|
+
- vector_x (float / ndarray): The x-component(s) of the evaluated vector field.
|
|
290
|
+
- vector_y (float / ndarray): The y-component(s) of the evaluated vector field.
|
|
291
|
+
- property_value (float / ndarray or None): The calculated property value based on the specified property_type. This will be None if property_type is set to None, indicating that no property calculation was performed.
|
|
292
|
+
|
|
293
|
+
Notes:
|
|
294
|
+
* **Performance Optimization** This method is optimized for performance. If the caller only requires the vector components without any derived properties, they can set property_type to None to skip the property evaluation step entirely, which can significantly reduce computation time, especially for complex properties that require additional function evaluations.
|
|
295
|
+
* **Time Injection** If the primary vector function or the property evaluator function is time-dependent, this method will automatically inject the current simulation time during their evaluation, allowing for dynamic, time-evolving fields without requiring the user to manage time parameters manually.
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
Evaluating the vector field and its velocity property at a single point:
|
|
299
|
+
```python
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
return self.math_engine.evaluate_field_and_property(property_type, spatial_coordinate_x, spatial_coordinate_y)
|
|
306
|
+
|
|
275
307
|
|
|
276
308
|
|
|
277
309
|
@ti.dataclass
|
|
@@ -376,7 +376,7 @@ class VectorMathEngine(MathEngine):
|
|
|
376
376
|
spatial_coordinate_y,
|
|
377
377
|
)
|
|
378
378
|
|
|
379
|
-
def evaluate_field_and_property(self, property_type: Property, spatial_coordinate_x: float, spatial_coordinate_y: float) -> tuple:
|
|
379
|
+
def evaluate_field_and_property(self, property_type: Property | None, spatial_coordinate_x: float, spatial_coordinate_y: float) -> tuple:
|
|
380
380
|
"""
|
|
381
381
|
Evaluates the primary vector function and the specified property at the given spatial coordinates.
|
|
382
382
|
|
|
@@ -384,7 +384,7 @@ class VectorMathEngine(MathEngine):
|
|
|
384
384
|
a scalar value given by property_type (e.g. divergence, rotation, velocity).
|
|
385
385
|
|
|
386
386
|
Args:
|
|
387
|
-
property_type (Property): The specific property to calculate based on the evaluated vector field. If set to None, the method will only evaluate the primary vector function and bypass any property calculations for maximum performance when only vector components are needed.
|
|
387
|
+
property_type (Property | None): The specific property to calculate based on the evaluated vector field. If set to None, the method will only evaluate the primary vector function and bypass any property calculations for maximum performance when only vector components are needed.
|
|
388
388
|
spatial_coordinate_x (float / ndarray): The x-coordinate(s) in the mathematical world space.
|
|
389
389
|
spatial_coordinate_y (float / ndarray): The y-coordinate(s) in the mathematical world space.
|
|
390
390
|
|
|
@@ -498,7 +498,12 @@ class VectorMathEngine(MathEngine):
|
|
|
498
498
|
magnitudes = np.hypot(vec_dx, vec_dy) * np.hypot(base_vec_dx, base_vec_dy)
|
|
499
499
|
|
|
500
500
|
|
|
501
|
-
magnitudes
|
|
501
|
+
if isinstance(magnitudes, np.ndarray):
|
|
502
|
+
magnitudes[magnitudes == 0] = 1.0
|
|
503
|
+
else:
|
|
504
|
+
if magnitudes == 0:
|
|
505
|
+
magnitudes = 1.0
|
|
506
|
+
|
|
502
507
|
angles = np.arccos(np.clip(dot_products / magnitudes, -1.0, 1.0)) # Angle in radians between the vector and the base angle vector
|
|
503
508
|
|
|
504
509
|
return angles
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from .constants import Property
|
|
2
2
|
from .validators import _count_function_parameters, _fatal_error
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
from . import math_engine as me
|
|
5
|
+
from . import entities as en
|
|
6
6
|
|
|
7
7
|
class DataProbe:
|
|
8
8
|
"""A measurement instrument that tracks a spatial target and evaluates mathematical properties.
|
|
@@ -14,12 +14,12 @@ class DataProbe:
|
|
|
14
14
|
listener functions via a callback system.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
def __init__(self, target_region,
|
|
17
|
+
def __init__(self, target_region, target_entity: me.VectorMathEngine | en.VectorField | en.ParticleSystem, measured_property: Property = None):
|
|
18
18
|
"""
|
|
19
19
|
Args:
|
|
20
20
|
target_region (SpatialRegion): The spatial entity to be tracked. This object must provide
|
|
21
21
|
a mechanism to retrieve its current coordinates (e.g., a `center` property or method).
|
|
22
|
-
|
|
22
|
+
target_entity (me.VectorMathEngine | en.VectorField | en.ParticleSystem): The core mathematical engine or entity responsible for parsing and
|
|
23
23
|
evaluating the vector field logic at the given coordinates.
|
|
24
24
|
measured_property (Property, optional): The specific mathematical property to measure
|
|
25
25
|
at the target's location (e.g., Property.DIVERGENCE). If set to None, the probe evaluates
|
|
@@ -50,17 +50,48 @@ class DataProbe:
|
|
|
50
50
|
|
|
51
51
|
probe = fr.DataProbe(
|
|
52
52
|
target_region = interactive_cursor,
|
|
53
|
-
|
|
53
|
+
target_entity = math_engine, # You can also use a VectorField or ParticleSystem instance here since they also implement the necessary evaluation interface
|
|
54
54
|
measured_property = fr.Property.VELOCITY
|
|
55
55
|
)
|
|
56
56
|
probe.add_listener(velocity_callback) # Registers the callback to receive velocity updates at the cursor's position
|
|
57
57
|
```
|
|
58
|
+
|
|
59
|
+
A comprehensive example of using DataProbe to display velocity:
|
|
60
|
+
```python
|
|
61
|
+
import FluxRender as fr
|
|
62
|
+
|
|
63
|
+
scene = fr.create_workspace()
|
|
64
|
+
|
|
65
|
+
def swirling_vortex(x, y):
|
|
66
|
+
return y, -x
|
|
67
|
+
|
|
68
|
+
vector_field = fr.VectorField(swirling_vortex)
|
|
69
|
+
|
|
70
|
+
# To pass data (coordinates) to DataProbe we need a region
|
|
71
|
+
cursor = fr.CursorRegion(always_active=True)
|
|
72
|
+
|
|
73
|
+
# We are creating a DataProbe that will measure velocity
|
|
74
|
+
probe = fr.DataProbe(
|
|
75
|
+
target_region=cursor,
|
|
76
|
+
target_entity=vector_field,
|
|
77
|
+
measured_property=fr.Property.VELOCITY
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# We display velocity using DynamicText
|
|
81
|
+
text = fr.DynamicText(text=lambda: f"Velocity: {probe.value}",)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
scene.add(vector_field,cursor, probe, text)
|
|
85
|
+
scene.run()
|
|
86
|
+
```
|
|
58
87
|
"""
|
|
59
88
|
|
|
60
89
|
self.target_region = target_region
|
|
61
|
-
self.math_engine =
|
|
90
|
+
self.math_engine = target_entity
|
|
62
91
|
self.measured_property = measured_property
|
|
63
92
|
|
|
93
|
+
self.value = 0 if measured_property is not None else (0, 0) # Initialize value based on property type
|
|
94
|
+
|
|
64
95
|
self._callbacks = []
|
|
65
96
|
|
|
66
97
|
def add_listener(self, callback_function):
|
|
@@ -108,6 +139,8 @@ class DataProbe:
|
|
|
108
139
|
probe_y
|
|
109
140
|
)
|
|
110
141
|
|
|
142
|
+
self.value = calculated_value if self.measured_property is not None else (vector_dx, vector_dy)
|
|
143
|
+
|
|
111
144
|
for callback in self._callbacks:
|
|
112
145
|
if self.measured_property is None:
|
|
113
146
|
callback(vector_dx, vector_dy)
|
|
@@ -351,7 +351,7 @@ class CursorRegion(SpatialRegion):
|
|
|
351
351
|
|
|
352
352
|
probe = fr.DataProbe(
|
|
353
353
|
target_region=probing_cursor,
|
|
354
|
-
|
|
354
|
+
target_entity=math_engine,
|
|
355
355
|
measured_property=fr.Property.VELOCITY
|
|
356
356
|
)
|
|
357
357
|
probe.add_listener(lambda value: print(f"Current velocity at cursor: {value}", end="\\r"))
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import FluxRender.core as cr
|
|
2
|
+
import FluxRender.entities as en
|
|
3
|
+
import FluxRender.ui as ui
|
|
4
|
+
import FluxRender.regions as rg
|
|
5
|
+
import FluxRender.probes as pr
|
|
6
|
+
import FluxRender.math_engine as me
|
|
7
|
+
import FluxRender.constants as ct
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Callable, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_workspace(
|
|
13
|
+
resolution: Tuple[int, int] = (1200, 800),
|
|
14
|
+
x_range: Tuple[float, float] = (-4, 4),
|
|
15
|
+
y_range: Tuple[float, float] = (-4, 4),
|
|
16
|
+
window_title: str = "FluxRender Visualization"
|
|
17
|
+
) -> cr.Scene:
|
|
18
|
+
"""
|
|
19
|
+
Creates a fully configured scene with a ready-to-use coordinate system,
|
|
20
|
+
axes, and a highly aesthetic double-grid overlay.
|
|
21
|
+
|
|
22
|
+
This is an advanced factory function designed to eliminate boilerplate code
|
|
23
|
+
for users who want a professional workspace out of the box.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
resolution (tuple[int, int]): The dimensions of the application window in pixels. (Default: (1200, 800))
|
|
27
|
+
x_range (tuple[float, float]): The mathematical boundaries of the X-axis. (Default: (-4, 4))
|
|
28
|
+
y_range (tuple[float, float]): The mathematical boundaries of the Y-axis. (Default: (-4, 4))
|
|
29
|
+
window_title (str): The title displayed on the application window. (Default: "FluxRender Visualization")
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
scene (cr.Scene): A fully initialized scene object containing the coordinate system,
|
|
33
|
+
double grid, and axes, ready for vector fields or particle systems to be added.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
import FluxRender as fr
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
workspace = fr.create_workspace()
|
|
41
|
+
|
|
42
|
+
def swirling_vortex(x, y):
|
|
43
|
+
vector_dx = np.sin(y) * x
|
|
44
|
+
vector_dy = np.cos(x) * y
|
|
45
|
+
return vector_dx, vector_dy
|
|
46
|
+
|
|
47
|
+
vector_field = fr.VectorField(swirling_vortex)
|
|
48
|
+
particles = fr.ParticleSystem(swirling_vortex)
|
|
49
|
+
|
|
50
|
+
workspace.add(particles, vector_field)
|
|
51
|
+
workspace.run()
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import FluxRender as fr
|
|
56
|
+
|
|
57
|
+
coordinate_system = fr.CoordinateSystem(
|
|
58
|
+
x_range=x_range,
|
|
59
|
+
y_range=y_range,
|
|
60
|
+
resolution=resolution,
|
|
61
|
+
keep_aspect_ratio=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
scene = fr.Scene(window_title, coordinate_system)
|
|
65
|
+
|
|
66
|
+
# Professional aesthetic: Thick, transparent major grid
|
|
67
|
+
major_grid = fr.Grid()
|
|
68
|
+
|
|
69
|
+
# Professional aesthetic: Thin, dense minor grid
|
|
70
|
+
minor_grid = fr.Grid(color=(0.6, 0.6, 0.6, 0.2), density=50)
|
|
71
|
+
|
|
72
|
+
standard_axes = fr.Axis(
|
|
73
|
+
color=(0.8, 0.8, 0.8, 1.0),
|
|
74
|
+
cover_background=True
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
scene.add(minor_grid, major_grid, standard_axes)
|
|
78
|
+
return scene
|
|
79
|
+
|
|
80
|
+
def quick_simulate(
|
|
81
|
+
vec_function: callable,
|
|
82
|
+
resolution: Tuple[int, int] = (1200, 800),
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Instantly generates and runs a complete, interactive simulation from a single vector function.
|
|
86
|
+
|
|
87
|
+
This function is the ultimate high-level wrapper. It sets up a standard workspace,
|
|
88
|
+
injects both a vector field and a particle system based on the provided mathematical
|
|
89
|
+
function, generates an interactive UI menu for property switching, and starts the render loop.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
vec_function (callable): The mathematical function defining the vector field (e.g., f(x, y, t)).
|
|
93
|
+
resolution (tuple[int, int]): The dimensions of the application window in pixels. (Default: (1200, 800))
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
scene (cr.Scene): The fully constructed scene. Returned immediately if auto_run is False.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
```python
|
|
100
|
+
import FluxRender as fr
|
|
101
|
+
import numpy as np
|
|
102
|
+
|
|
103
|
+
def swirling_vortex(x, y):
|
|
104
|
+
vector_dx = np.sin(y) * x
|
|
105
|
+
vector_dy = np.cos(x) * y
|
|
106
|
+
return vector_dx, vector_dy
|
|
107
|
+
|
|
108
|
+
scene = fr.quick_simulate(swirling_vortex)
|
|
109
|
+
```
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
import FluxRender as fr
|
|
113
|
+
|
|
114
|
+
# region 1. Engine & Entities Setup
|
|
115
|
+
workspace = fr.create_workspace(resolution)
|
|
116
|
+
|
|
117
|
+
math_engine = fr.VectorMathEngine(workspace, vec_function)
|
|
118
|
+
color_mapper = fr.ColorMapper()
|
|
119
|
+
|
|
120
|
+
vector_field = fr.VectorField(math_engine, color_mapper=color_mapper)
|
|
121
|
+
particles = fr.ParticleSystem(math_engine, color_mapper=color_mapper)
|
|
122
|
+
|
|
123
|
+
target_entities = [vector_field, particles]
|
|
124
|
+
# endregion
|
|
125
|
+
|
|
126
|
+
# region 2. Data Probes
|
|
127
|
+
# Initialize the interactive region tracking the mouse cursor
|
|
128
|
+
cursor_tracking_region = rg.CursorRegion(always_active=True)
|
|
129
|
+
|
|
130
|
+
# Probe for the currently selected scalar property (e.g., Divergence, Curl).
|
|
131
|
+
# We initialize it with the current property of the vector field.
|
|
132
|
+
data_probe_property = pr.DataProbe(cursor_tracking_region, math_engine, vector_field.color_property)
|
|
133
|
+
|
|
134
|
+
# Probe for the raw mathematical vector value from the math engine
|
|
135
|
+
data_probe_vector = pr.DataProbe(cursor_tracking_region, math_engine, None)
|
|
136
|
+
# endregion
|
|
137
|
+
|
|
138
|
+
# region 3. Property Switch Construction
|
|
139
|
+
property_mapping = [
|
|
140
|
+
("Component X", ct.Property.COMPONENT_X),
|
|
141
|
+
("Component Y", ct.Property.COMPONENT_Y),
|
|
142
|
+
("Velocity", ct.Property.VELOCITY),
|
|
143
|
+
("Angle", ct.Property.ANGLE),
|
|
144
|
+
("Divergence", ct.Property.DIVERGENCE),
|
|
145
|
+
("Curl", ct.Property.CURL),
|
|
146
|
+
("Jacobian", ct.Property.JACOBIAN),
|
|
147
|
+
("Okubo-Weiss", ct.Property.OKUBO_WEISS),
|
|
148
|
+
("Convective Acceleration", ct.Property.CONVECTIVE_ACCELERATION),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# UI Styles for the property buttons
|
|
152
|
+
active_style = ui.UIStyle(
|
|
153
|
+
background_color=(0.027, 0.212, 0.439, 0.878),
|
|
154
|
+
hover_background_color=(0.027, 0.212, 0.439, 0.878)
|
|
155
|
+
)
|
|
156
|
+
inactive_style = ui.UIStyle(
|
|
157
|
+
background_color=(0.0, 0.5, 1.0, 0.45),
|
|
158
|
+
hover_background_color=(0.0, 0.6, 1.0, 0.55),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Internal callback for handling state changes
|
|
162
|
+
def toggle_property(clicked_button):
|
|
163
|
+
# Update the visual representation in entities
|
|
164
|
+
for entity in target_entities:
|
|
165
|
+
setattr(entity, 'color_property', clicked_button.property)
|
|
166
|
+
|
|
167
|
+
# Update the mathematical probe target
|
|
168
|
+
data_probe_property.measured_property = clicked_button.property
|
|
169
|
+
|
|
170
|
+
# Update button visual states
|
|
171
|
+
for current_button in generated_buttons:
|
|
172
|
+
is_custom = (current_button.property == ct.Property.CUSTOM)
|
|
173
|
+
if current_button == clicked_button:
|
|
174
|
+
current_button.style = active_style
|
|
175
|
+
else:
|
|
176
|
+
current_button.style = inactive_style
|
|
177
|
+
|
|
178
|
+
generated_buttons = []
|
|
179
|
+
|
|
180
|
+
# Dynamically generate buttons based on the mapping
|
|
181
|
+
for label_text, target_property in property_mapping:
|
|
182
|
+
is_active = all(getattr(entity, 'color_property', None) == target_property for entity in target_entities)
|
|
183
|
+
|
|
184
|
+
initial_style = inactive_style
|
|
185
|
+
if is_active:
|
|
186
|
+
initial_style = active_style
|
|
187
|
+
|
|
188
|
+
new_button = ui.Button(label_text, toggle_property, style=initial_style)
|
|
189
|
+
new_button.property = target_property
|
|
190
|
+
generated_buttons.append(new_button)
|
|
191
|
+
|
|
192
|
+
property_switch_container = ui.VBox(
|
|
193
|
+
spacing=12,
|
|
194
|
+
common_width=230,
|
|
195
|
+
common_height=35,
|
|
196
|
+
style=ui.UIStyle(font_size=16, padding=(12, 12))
|
|
197
|
+
)
|
|
198
|
+
property_switch_container.add(*generated_buttons)
|
|
199
|
+
# endregion
|
|
200
|
+
|
|
201
|
+
# region 4. Additional UI Switches (Scale & Mode)
|
|
202
|
+
color_scale_switch = fr.create_color_scale_switch(workspace, color_mapper, add_to_scene=False)
|
|
203
|
+
mode_switch = fr.create_mode_switch(workspace, vector_field, add_to_scene=False)
|
|
204
|
+
|
|
205
|
+
scale_mode_container = fr.VBox(
|
|
206
|
+
style=fr.UIStyle(padding=(12, 12)),
|
|
207
|
+
spacing=12,
|
|
208
|
+
common_width=230,
|
|
209
|
+
)
|
|
210
|
+
scale_mode_container.add(color_scale_switch, mode_switch)
|
|
211
|
+
# endregion
|
|
212
|
+
|
|
213
|
+
# region 5. Dynamic Text Displays
|
|
214
|
+
def text_property_provider() -> str:
|
|
215
|
+
current_value = data_probe_property.value
|
|
216
|
+
# Convert Enum name to a readable format (e.g., "CONVECTIVE_ACCELERATION" -> "Convective Acceleration")
|
|
217
|
+
formatted_name = data_probe_property.measured_property.name.replace("_", " ").title()
|
|
218
|
+
return f"{formatted_name}: {current_value:.3f}"
|
|
219
|
+
|
|
220
|
+
dynamic_text_property = ui.DynamicText(
|
|
221
|
+
text=text_property_provider,
|
|
222
|
+
width=230,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def text_vector_provider() -> str:
|
|
226
|
+
current_value = data_probe_vector.value
|
|
227
|
+
return f"Vector: ({current_value[0]:.3f}, {current_value[1]:.3f})"
|
|
228
|
+
|
|
229
|
+
dynamic_text_vector = ui.DynamicText(
|
|
230
|
+
text=text_vector_provider,
|
|
231
|
+
width=230,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
informations_container = fr.VBox(
|
|
235
|
+
style=fr.UIStyle(font_size=14, padding=(12, 12)),
|
|
236
|
+
spacing=12
|
|
237
|
+
)
|
|
238
|
+
informations_container.add(dynamic_text_vector, dynamic_text_property)
|
|
239
|
+
# endregion
|
|
240
|
+
|
|
241
|
+
# region 6. Main Layout Assembly
|
|
242
|
+
# Transparent wrapper linking all panels together vertically
|
|
243
|
+
main_container_style = fr.UIStyle(visible=False, padding=(0, 0))
|
|
244
|
+
|
|
245
|
+
main_wrapper = fr.VBox(
|
|
246
|
+
position=(10, workspace.height - 10),
|
|
247
|
+
style=main_container_style,
|
|
248
|
+
spacing=15
|
|
249
|
+
)
|
|
250
|
+
main_wrapper.add(property_switch_container, scale_mode_container, informations_container)
|
|
251
|
+
|
|
252
|
+
# Inject all components into the rendering pipeline
|
|
253
|
+
workspace.add(
|
|
254
|
+
particles,
|
|
255
|
+
vector_field,
|
|
256
|
+
main_wrapper,
|
|
257
|
+
cursor_tracking_region,
|
|
258
|
+
data_probe_property,
|
|
259
|
+
data_probe_vector
|
|
260
|
+
)
|
|
261
|
+
# endregion
|
|
262
|
+
|
|
263
|
+
workspace.run()
|
|
264
|
+
|
|
265
|
+
return workspace
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def as_vector_field(**field_configuration_parameters):
|
|
270
|
+
"""
|
|
271
|
+
A mathematical decorator that automatically converts a standard Python function
|
|
272
|
+
into a fully initialized VectorField entity.
|
|
273
|
+
|
|
274
|
+
It accepts any number of keyword arguments that the underlying VectorField
|
|
275
|
+
constructor supports, passing them directly to the instance.
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
```python
|
|
279
|
+
import FluxRender as fr
|
|
280
|
+
import numpy as np
|
|
281
|
+
|
|
282
|
+
scene = fr.create_workspace()
|
|
283
|
+
|
|
284
|
+
@fr.as_vector_field(color_property=fr.Property.CURL)
|
|
285
|
+
def vector_function(x, y):
|
|
286
|
+
return np.sin(x) * y, np.cos(y) * x
|
|
287
|
+
|
|
288
|
+
scene.add(vector_function)
|
|
289
|
+
scene.run()
|
|
290
|
+
```
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def wrapper(vec_function: Callable):
|
|
294
|
+
vector_field = en.VectorField(vec_function, **field_configuration_parameters)
|
|
295
|
+
return vector_field
|
|
296
|
+
|
|
297
|
+
return wrapper
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|