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.
Files changed (22) hide show
  1. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/__init__.py +2 -2
  2. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/core.py +43 -12
  3. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/entities.py +33 -1
  4. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/math_engine.py +8 -3
  5. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/probes.py +38 -5
  6. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/regions.py +1 -1
  7. fluxrender-0.2.0/FluxRender/shortcuts.py +302 -0
  8. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/ui.py +623 -122
  9. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/validators.py +31 -0
  10. {fluxrender-0.1.4/fluxrender.egg-info → fluxrender-0.2.0}/PKG-INFO +1 -1
  11. {fluxrender-0.1.4 → fluxrender-0.2.0/fluxrender.egg-info}/PKG-INFO +1 -1
  12. {fluxrender-0.1.4 → fluxrender-0.2.0}/pyproject.toml +1 -1
  13. fluxrender-0.1.4/FluxRender/shortcuts.py +0 -138
  14. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/colors.py +0 -0
  15. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/constants.py +0 -0
  16. {fluxrender-0.1.4 → fluxrender-0.2.0}/FluxRender/graphics.py +0 -0
  17. {fluxrender-0.1.4 → fluxrender-0.2.0}/README.md +0 -0
  18. {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/SOURCES.txt +0 -0
  19. {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/dependency_links.txt +0 -0
  20. {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/requires.txt +0 -0
  21. {fluxrender-0.1.4 → fluxrender-0.2.0}/fluxrender.egg-info/top_level.txt +0 -0
  22. {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, width: int, height: int, keep_aspect_ratio: bool = False):
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
- width (int): Window/buffer width in pixels.
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.width = width
170
- self.height = height
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
- final_rgb = ui.xyz * ui.w + color_step2 * (1.0 - ui.w)
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(self)
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[magnitudes == 0] = 1.0
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, math_engine: me.MathEngine, measured_property: Property = None):
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
- math_engine (me.MathEngine): The core mathematical engine responsible for parsing and
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
- math_engine = math_engine,
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 = 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
- math_engine=math_engine,
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
+