BulletLab 0.1.2__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.
- bulletlab/__init__.py +48 -0
- bulletlab/core/__init__.py +10 -0
- bulletlab/core/simulation.py +377 -0
- bulletlab/core/world.py +173 -0
- bulletlab/logging/__init__.py +9 -0
- bulletlab/logging/csv_writer.py +54 -0
- bulletlab/logging/json_writer.py +70 -0
- bulletlab/logging/logger.py +258 -0
- bulletlab/plotting/__init__.py +9 -0
- bulletlab/plotting/live_plot.py +364 -0
- bulletlab/robot/__init__.py +12 -0
- bulletlab/robot/joint.py +402 -0
- bulletlab/robot/link.py +401 -0
- bulletlab/robot/robot.py +533 -0
- bulletlab/telemetry/__init__.py +10 -0
- bulletlab/telemetry/channel.py +124 -0
- bulletlab/telemetry/manager.py +227 -0
- bulletlab/ui/__init__.py +31 -0
- bulletlab/ui/app.py +543 -0
- bulletlab/ui/panels/__init__.py +16 -0
- bulletlab/ui/panels/console.py +189 -0
- bulletlab/ui/panels/explorer.py +144 -0
- bulletlab/ui/panels/plots.py +143 -0
- bulletlab/ui/panels/properties.py +265 -0
- bulletlab/ui/panels/telemetry.py +105 -0
- bulletlab/ui/widgets.py +348 -0
- bulletlab/utils/__init__.py +26 -0
- bulletlab/utils/math_utils.py +183 -0
- bulletlab/utils/timer.py +107 -0
- bulletlab/utils/urdf_utils.py +114 -0
- bulletlab-0.1.2.dist-info/METADATA +284 -0
- bulletlab-0.1.2.dist-info/RECORD +35 -0
- bulletlab-0.1.2.dist-info/WHEEL +5 -0
- bulletlab-0.1.2.dist-info/licenses/LICENSE +21 -0
- bulletlab-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConsolePanel – interactive Python command console in the BulletLab UI.
|
|
3
|
+
|
|
4
|
+
Provides a single-line input field and a scrollable output log. Commands
|
|
5
|
+
are executed via exec() in a configurable namespace, making robot objects
|
|
6
|
+
and sim directly accessible.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from bulletlab.ui.panels.console import ConsolePanel
|
|
11
|
+
|
|
12
|
+
console = ConsolePanel(namespace={"sim": sim, "robot": robot})
|
|
13
|
+
console.render()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import traceback
|
|
19
|
+
from collections import deque
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import imgui
|
|
24
|
+
|
|
25
|
+
_HAS_IMGUI = True
|
|
26
|
+
except ImportError: # pragma: no cover
|
|
27
|
+
imgui = None # type: ignore[assignment]
|
|
28
|
+
_HAS_IMGUI = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConsolePanel:
|
|
32
|
+
"""Interactive Python console panel.
|
|
33
|
+
|
|
34
|
+
Executes commands via ``exec()`` in a provided namespace, with output
|
|
35
|
+
captured and displayed in a scrollable log.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
namespace: Dictionary of variables available in the console namespace.
|
|
39
|
+
Typically includes ``sim``, ``robot``, ``telemetry``, etc.
|
|
40
|
+
max_history: Maximum number of output lines to retain.
|
|
41
|
+
|
|
42
|
+
Example::
|
|
43
|
+
|
|
44
|
+
console = ConsolePanel(namespace={"sim": sim, "robot": robot})
|
|
45
|
+
console.execute("robot.links['wheel'].mass = 5")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
namespace: dict[str, Any] | None = None,
|
|
51
|
+
max_history: int = 200,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._namespace: dict[str, Any] = namespace if namespace is not None else {}
|
|
54
|
+
self._history: deque[str] = deque(maxlen=max_history)
|
|
55
|
+
self._input_buf: list[str] = [""] # mutable for imgui input
|
|
56
|
+
self._cmd_history: list[str] = []
|
|
57
|
+
self._cmd_index: int = -1
|
|
58
|
+
self._scroll_to_bottom: bool = False
|
|
59
|
+
|
|
60
|
+
# Add some helpful builtins
|
|
61
|
+
import builtins
|
|
62
|
+
self._namespace.setdefault("__builtins__", builtins)
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# API
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def update_namespace(self, updates: dict[str, Any]) -> None:
|
|
69
|
+
"""Update the console execution namespace.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
updates: Key-value pairs to add or overwrite.
|
|
73
|
+
|
|
74
|
+
Example::
|
|
75
|
+
|
|
76
|
+
console.update_namespace({"robot": new_robot})
|
|
77
|
+
"""
|
|
78
|
+
self._namespace.update(updates)
|
|
79
|
+
|
|
80
|
+
def execute(self, command: str) -> None:
|
|
81
|
+
"""Execute a Python command string in the console namespace.
|
|
82
|
+
|
|
83
|
+
Appends both the command and the result/error to the output log.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
command: Python source code to execute.
|
|
87
|
+
|
|
88
|
+
Example::
|
|
89
|
+
|
|
90
|
+
console.execute("robot.links['wheel'].mass = 10")
|
|
91
|
+
"""
|
|
92
|
+
command = command.strip()
|
|
93
|
+
if not command:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
self._history.append(f">>> {command}")
|
|
97
|
+
self._cmd_history.append(command)
|
|
98
|
+
self._cmd_index = -1
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Try eval first (for expressions)
|
|
102
|
+
try:
|
|
103
|
+
result = eval(command, self._namespace) # noqa: S307
|
|
104
|
+
if result is not None:
|
|
105
|
+
self._history.append(f" {result!r}")
|
|
106
|
+
except SyntaxError:
|
|
107
|
+
# Pass namespace as both globals AND locals so that
|
|
108
|
+
# assignments (x = 42) are written back into the shared dict.
|
|
109
|
+
exec(command, self._namespace, self._namespace) # noqa: S102
|
|
110
|
+
except Exception:
|
|
111
|
+
for line in traceback.format_exc().splitlines():
|
|
112
|
+
self._history.append(f" {line}")
|
|
113
|
+
|
|
114
|
+
self._scroll_to_bottom = True
|
|
115
|
+
|
|
116
|
+
def log(self, message: str) -> None:
|
|
117
|
+
"""Append an informational message to the console log.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
message: Text to display in the log.
|
|
121
|
+
"""
|
|
122
|
+
self._history.append(f"# {message}")
|
|
123
|
+
self._scroll_to_bottom = True
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# Render
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def render(self) -> None:
|
|
130
|
+
"""Draw the console panel contents.
|
|
131
|
+
|
|
132
|
+
Must be called inside an active ImGui window context.
|
|
133
|
+
"""
|
|
134
|
+
if not _HAS_IMGUI:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Output area (child window for scrolling)
|
|
138
|
+
avail_height = imgui.get_content_region_available()[1] - 30
|
|
139
|
+
imgui.begin_child(
|
|
140
|
+
"console_output",
|
|
141
|
+
width=0,
|
|
142
|
+
height=max(avail_height, 50),
|
|
143
|
+
border=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
for line in self._history:
|
|
147
|
+
if line.startswith(">>>"):
|
|
148
|
+
imgui.text_colored(line, 0.4, 0.9, 0.4, 1.0)
|
|
149
|
+
elif line.startswith(" Traceback") or "Error" in line:
|
|
150
|
+
imgui.text_colored(line, 1.0, 0.3, 0.3, 1.0)
|
|
151
|
+
elif line.startswith("#"):
|
|
152
|
+
imgui.text_colored(line, 0.6, 0.6, 1.0, 1.0)
|
|
153
|
+
else:
|
|
154
|
+
imgui.text(line)
|
|
155
|
+
|
|
156
|
+
if self._scroll_to_bottom:
|
|
157
|
+
imgui.set_scroll_here_y(1.0)
|
|
158
|
+
self._scroll_to_bottom = False
|
|
159
|
+
|
|
160
|
+
imgui.end_child()
|
|
161
|
+
|
|
162
|
+
# Input field
|
|
163
|
+
imgui.push_item_width(-65)
|
|
164
|
+
enter_pressed = False
|
|
165
|
+
|
|
166
|
+
# We need to use input_text - imgui binding style
|
|
167
|
+
input_flags = imgui.INPUT_TEXT_ENTER_RETURNS_TRUE
|
|
168
|
+
changed, new_text = imgui.input_text(
|
|
169
|
+
"##console_input",
|
|
170
|
+
self._input_buf[0],
|
|
171
|
+
256,
|
|
172
|
+
flags=input_flags,
|
|
173
|
+
)
|
|
174
|
+
self._input_buf[0] = new_text
|
|
175
|
+
|
|
176
|
+
if changed: # Enter was pressed
|
|
177
|
+
enter_pressed = True
|
|
178
|
+
|
|
179
|
+
imgui.pop_item_width()
|
|
180
|
+
imgui.same_line()
|
|
181
|
+
|
|
182
|
+
if imgui.button("Run##console_run") or enter_pressed:
|
|
183
|
+
cmd = self._input_buf[0].strip()
|
|
184
|
+
if cmd:
|
|
185
|
+
self.execute(cmd)
|
|
186
|
+
self._input_buf[0] = ""
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return f"ConsolePanel(history={len(self._history)} lines)"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ExplorerPanel – displays the simulation scene tree.
|
|
3
|
+
|
|
4
|
+
Shows the hierarchy: Simulation → Robots → Joints / Links.
|
|
5
|
+
Clicking an item fires a selection callback that the PropertiesPanel
|
|
6
|
+
(and other panels) can subscribe to.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from bulletlab.ui.panels.explorer import ExplorerPanel
|
|
11
|
+
|
|
12
|
+
explorer = ExplorerPanel(sim=sim, robots=[robot])
|
|
13
|
+
# In your render loop:
|
|
14
|
+
explorer.render()
|
|
15
|
+
selected = explorer.selected_object
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import imgui
|
|
24
|
+
|
|
25
|
+
_HAS_IMGUI = True
|
|
26
|
+
except ImportError: # pragma: no cover
|
|
27
|
+
imgui = None # type: ignore[assignment]
|
|
28
|
+
_HAS_IMGUI = False
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from bulletlab.core.simulation import Simulation
|
|
32
|
+
from bulletlab.robot.robot import Robot
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExplorerPanel:
|
|
36
|
+
"""Renders a tree view of the simulation scene.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
sim: The :class:`~bulletlab.core.simulation.Simulation` instance.
|
|
40
|
+
robots: List of robots to display in the tree.
|
|
41
|
+
on_select: Optional callback called with the selected object
|
|
42
|
+
whenever the user clicks on a tree item.
|
|
43
|
+
|
|
44
|
+
Example::
|
|
45
|
+
|
|
46
|
+
explorer = ExplorerPanel(sim=sim, robots=[robot])
|
|
47
|
+
explorer.render()
|
|
48
|
+
selected = explorer.selected_object
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
sim: "Simulation",
|
|
54
|
+
robots: list["Robot"] | None = None,
|
|
55
|
+
on_select: Callable[[Any], None] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._sim = sim
|
|
58
|
+
self._robots: list["Robot"] = robots or []
|
|
59
|
+
self._on_select = on_select
|
|
60
|
+
self._selected: Any = None
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
# Public API
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def selected_object(self) -> Any:
|
|
68
|
+
"""The currently selected object (Robot, Joint, Link, or None)."""
|
|
69
|
+
return self._selected
|
|
70
|
+
|
|
71
|
+
def add_robot(self, robot: "Robot") -> None:
|
|
72
|
+
"""Add a robot to the explorer tree.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
robot: The robot to add.
|
|
76
|
+
"""
|
|
77
|
+
if robot not in self._robots:
|
|
78
|
+
self._robots.append(robot)
|
|
79
|
+
|
|
80
|
+
def render(self) -> None:
|
|
81
|
+
"""Draw the explorer panel contents.
|
|
82
|
+
|
|
83
|
+
Must be called inside an active ImGui window context.
|
|
84
|
+
|
|
85
|
+
Example::
|
|
86
|
+
|
|
87
|
+
imgui.begin("Explorer")
|
|
88
|
+
explorer.render()
|
|
89
|
+
imgui.end()
|
|
90
|
+
"""
|
|
91
|
+
if not _HAS_IMGUI:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Simulation node
|
|
95
|
+
sim_open = imgui.tree_node("Simulation##root")
|
|
96
|
+
if imgui.is_item_clicked():
|
|
97
|
+
self._select(self._sim)
|
|
98
|
+
if sim_open:
|
|
99
|
+
for robot in self._robots:
|
|
100
|
+
self._render_robot(robot)
|
|
101
|
+
imgui.tree_pop()
|
|
102
|
+
|
|
103
|
+
def _render_robot(self, robot: "Robot") -> None:
|
|
104
|
+
"""Render tree node for a single robot."""
|
|
105
|
+
robot_label = f"\U0001F916 {robot.name}##robot_{id(robot)}"
|
|
106
|
+
robot_open = imgui.tree_node(robot_label)
|
|
107
|
+
if imgui.is_item_clicked():
|
|
108
|
+
self._select(robot)
|
|
109
|
+
|
|
110
|
+
if robot_open:
|
|
111
|
+
# Joints subtree
|
|
112
|
+
joints_open = imgui.tree_node(f"Joints ({len(robot.joints)})##joints_{id(robot)}")
|
|
113
|
+
if joints_open:
|
|
114
|
+
for name, joint in robot.joints.items():
|
|
115
|
+
clicked = imgui.selectable(
|
|
116
|
+
f" \U0001F517 {name}##joint_{id(joint)}",
|
|
117
|
+
self._selected is joint,
|
|
118
|
+
)[0]
|
|
119
|
+
if clicked:
|
|
120
|
+
self._select(joint)
|
|
121
|
+
imgui.tree_pop()
|
|
122
|
+
|
|
123
|
+
# Links subtree
|
|
124
|
+
links_open = imgui.tree_node(f"Links ({len(robot.links)})##links_{id(robot)}")
|
|
125
|
+
if links_open:
|
|
126
|
+
for name, link in robot.links.items():
|
|
127
|
+
clicked = imgui.selectable(
|
|
128
|
+
f" \U0001F9F1 {name}##link_{id(link)}",
|
|
129
|
+
self._selected is link,
|
|
130
|
+
)[0]
|
|
131
|
+
if clicked:
|
|
132
|
+
self._select(link)
|
|
133
|
+
imgui.tree_pop()
|
|
134
|
+
|
|
135
|
+
imgui.tree_pop()
|
|
136
|
+
|
|
137
|
+
def _select(self, obj: Any) -> None:
|
|
138
|
+
"""Handle selection of an object."""
|
|
139
|
+
self._selected = obj
|
|
140
|
+
if self._on_select is not None:
|
|
141
|
+
self._on_select(obj)
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return f"ExplorerPanel(robots={[r.name for r in self._robots]})"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlotsPanel – renders inline live plots using ImGui's plot_lines primitive.
|
|
3
|
+
|
|
4
|
+
Displays sparkline-style charts for telemetry channel histories directly
|
|
5
|
+
inside the BulletLab ImGui control window. These are lightweight inline
|
|
6
|
+
plots (not PyQtGraph). For full-featured windowed plots, use LivePlot.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from bulletlab.ui.panels.plots import PlotsPanel
|
|
11
|
+
from bulletlab.telemetry import TelemetryManager
|
|
12
|
+
|
|
13
|
+
telemetry = TelemetryManager()
|
|
14
|
+
telemetry.watch("Speed", lambda: robot.speed)
|
|
15
|
+
|
|
16
|
+
plots = PlotsPanel(telemetry)
|
|
17
|
+
plots.render()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import array
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import imgui
|
|
27
|
+
|
|
28
|
+
_HAS_IMGUI = True
|
|
29
|
+
except ImportError: # pragma: no cover
|
|
30
|
+
imgui = None # type: ignore[assignment]
|
|
31
|
+
_HAS_IMGUI = False
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from bulletlab.telemetry.manager import TelemetryManager
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlotsPanel:
|
|
38
|
+
"""Renders inline sparkline plots for all telemetry channels.
|
|
39
|
+
|
|
40
|
+
Uses ImGui's built-in ``plot_lines`` which requires no external
|
|
41
|
+
plotting library. For production-quality plots, use
|
|
42
|
+
:class:`~bulletlab.plotting.live_plot.LivePlot`.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
telemetry: The :class:`~bulletlab.telemetry.manager.TelemetryManager`
|
|
46
|
+
providing channel histories.
|
|
47
|
+
plot_height: Height of each individual sparkline in pixels.
|
|
48
|
+
max_display: Maximum number of channels to plot simultaneously.
|
|
49
|
+
|
|
50
|
+
Example::
|
|
51
|
+
|
|
52
|
+
plots_panel = PlotsPanel(telemetry)
|
|
53
|
+
plots_panel.render()
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
telemetry: "TelemetryManager",
|
|
59
|
+
plot_height: float = 60.0,
|
|
60
|
+
max_display: int = 8,
|
|
61
|
+
) -> None:
|
|
62
|
+
self._telemetry = telemetry
|
|
63
|
+
self._plot_height = plot_height
|
|
64
|
+
self._max_display = max_display
|
|
65
|
+
|
|
66
|
+
def render(self) -> None:
|
|
67
|
+
"""Draw the plots panel contents.
|
|
68
|
+
|
|
69
|
+
Must be called inside an active ImGui window context.
|
|
70
|
+
"""
|
|
71
|
+
if not _HAS_IMGUI:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
channels = self._telemetry.channels
|
|
75
|
+
if not channels:
|
|
76
|
+
imgui.text_colored("No channels to plot.", 0.5, 0.5, 0.5, 1.0)
|
|
77
|
+
imgui.text("Add channels with telemetry.watch(...)")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
shown = 0
|
|
81
|
+
for name, channel in channels.items():
|
|
82
|
+
if shown >= self._max_display:
|
|
83
|
+
imgui.text_colored(
|
|
84
|
+
f"... and {len(channels) - shown} more channels",
|
|
85
|
+
0.5, 0.5, 0.5, 1.0,
|
|
86
|
+
)
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
values = channel.values
|
|
90
|
+
if not values:
|
|
91
|
+
imgui.text(f"{name}: (no data)")
|
|
92
|
+
shown += 1
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Build a contiguous C-float buffer.
|
|
96
|
+
# imgui.plot_lines() requires a bytes-like / buffer object —
|
|
97
|
+
# a plain Python list raises TypeError.
|
|
98
|
+
float_vals: list[float] = []
|
|
99
|
+
for v in values:
|
|
100
|
+
try:
|
|
101
|
+
float_vals.append(float(v))
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
float_vals.append(0.0)
|
|
104
|
+
|
|
105
|
+
if not float_vals:
|
|
106
|
+
shown += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
buf = array.array("f", float_vals) # contiguous single-precision floats
|
|
110
|
+
|
|
111
|
+
latest = float_vals[-1]
|
|
112
|
+
unit = channel.unit
|
|
113
|
+
unit_str = f" {unit}" if unit else ""
|
|
114
|
+
vmin = min(float_vals)
|
|
115
|
+
vmax = max(float_vals)
|
|
116
|
+
overlay = f"{latest:.3f}{unit_str}"
|
|
117
|
+
avail_w = imgui.get_content_region_available()[0]
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
imgui.plot_lines(
|
|
121
|
+
f"##{name}_plot",
|
|
122
|
+
buf,
|
|
123
|
+
overlay_text=overlay,
|
|
124
|
+
scale_min=vmin - abs(vmin) * 0.1 - 1e-6,
|
|
125
|
+
scale_max=vmax + abs(vmax) * 0.1 + 1e-6,
|
|
126
|
+
graph_size=(avail_w, self._plot_height),
|
|
127
|
+
)
|
|
128
|
+
except Exception:
|
|
129
|
+
# Graceful fallback if this imgui version's plot_lines binding
|
|
130
|
+
# uses a different signature
|
|
131
|
+
imgui.text(f"{name}: {latest:.4f}{unit_str}")
|
|
132
|
+
shown += 1
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
imgui.text_colored(
|
|
136
|
+
f"{name}: {latest:.4f}{unit_str} [{vmin:.3f} – {vmax:.3f}]",
|
|
137
|
+
0.7, 0.9, 0.7, 1.0,
|
|
138
|
+
)
|
|
139
|
+
imgui.separator()
|
|
140
|
+
shown += 1
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
return f"PlotsPanel(channels={self._telemetry.channel_names})"
|