diff --git a/dimos/core/blueprints.py b/dimos/core/blueprints.py index 14338d6a38..1560554eed 100644 --- a/dimos/core/blueprints.py +++ b/dimos/core/blueprints.py @@ -23,6 +23,9 @@ from types import MappingProxyType from typing import Any, Literal, get_args, get_origin, get_type_hints +import rerun as rr +import rerun.blueprint as rrb + from dimos.core.global_config import GlobalConfig from dimos.core.module import Module from dimos.core.module_coordinator import ModuleCoordinator @@ -280,6 +283,50 @@ def _connect_rpc_methods(self, module_coordinator: ModuleCoordinator) -> None: requested_method_name, rpc_methods_dot[requested_method_name] ) + def _init_rerun_blueprint(self, module_coordinator: ModuleCoordinator) -> None: + """Compose and send Rerun blueprint from module contributions. + + Collects rerun_views() from all modules and composes them into a unified layout. + """ + # Collect view contributions from all modules + side_panels = [] + for blueprint in self.blueprints: + if hasattr(blueprint.module, "rerun_views"): + views = blueprint.module.rerun_views() + if views: + side_panels.extend(views) + + # Always include latency panel if we have any panels + if side_panels: + side_panels.append( + rrb.TimeSeriesView( + name="Latency (ms)", + origin="/metrics", + contents=[ + "+ /metrics/voxel_map/latency_ms", + "+ /metrics/costmap/latency_ms", + ], + ) + ) + + # Compose final layout + if side_panels: + composed_blueprint = rrb.Blueprint( + rrb.Horizontal( + rrb.Spatial3DView( + name="3D View", + origin="world", + background=[0, 0, 0], + ), + rrb.Vertical(*side_panels, row_shares=[2] + [1] * (len(side_panels) - 1)), + column_shares=[3, 1], + ), + rrb.TimePanel(state="collapsed"), + rrb.SelectionPanel(state="collapsed"), + rrb.BlueprintPanel(state="collapsed"), + ) + rr.send_blueprint(composed_blueprint) + def build( self, global_config: GlobalConfig | None = None, @@ -294,6 +341,17 @@ def build( self._check_requirements() self._verify_no_name_conflicts() + # Initialize Rerun server before deploying modules (if backend is Rerun) + if global_config.rerun_enabled and global_config.viewer_backend.startswith("rerun"): + try: + from dimos.dashboard.rerun_init import init_rerun_server + + server_addr = init_rerun_server(viewer_mode=global_config.viewer_backend) + global_config = global_config.model_copy(update={"rerun_server_addr": server_addr}) + logger.info("Rerun server initialized", addr=server_addr) + except Exception as e: + logger.warning(f"Failed to initialize Rerun server: {e}") + module_coordinator = ModuleCoordinator(global_config=global_config) module_coordinator.start() @@ -303,6 +361,10 @@ def build( module_coordinator.start_all_modules() + # Compose and send Rerun blueprint from module contributions + if global_config.viewer_backend.startswith("rerun"): + self._init_rerun_blueprint(module_coordinator) + return module_coordinator diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py index 8d8edc2598..dfa54830ac 100644 --- a/dimos/core/global_config.py +++ b/dimos/core/global_config.py @@ -14,12 +14,15 @@ from functools import cached_property import re +from typing import Literal, TypeAlias from pydantic_settings import BaseSettings, SettingsConfigDict from dimos.mapping.occupancy.path_map import NavigationStrategy from dimos.navigation.global_planner.types import AStarAlgorithm +ViewerBackend: TypeAlias = Literal["rerun-web", "rerun-native", "foxglove"] + def _get_all_numbers(s: str) -> list[float]: return [float(x) for x in re.findall(r"-?\d+\.?\d*", s)] @@ -29,6 +32,9 @@ class GlobalConfig(BaseSettings): robot_ip: str | None = None simulation: bool = False replay: bool = False + rerun_enabled: bool = True + rerun_server_addr: str | None = None + viewer_backend: ViewerBackend = "rerun-native" n_dask_workers: int = 2 memory_limit: str = "auto" mujoco_camera_position: str | None = None diff --git a/dimos/core/stream.py b/dimos/core/stream.py index 652223d9ab..70384ffc77 100644 --- a/dimos/core/stream.py +++ b/dimos/core/stream.py @@ -26,6 +26,7 @@ import reactivex as rx from reactivex import operators as ops from reactivex.disposable import Disposable +import rerun as rr import dimos.core.colors as colors from dimos.core.resource import Resource @@ -137,11 +138,13 @@ def __str__(self) -> str: ) -class Out(Stream[T]): +class Out(Stream[T], ObservableMixin[T]): _transport: Transport # type: ignore[type-arg] def __init__(self, *argv, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*argv, **kwargs) + self._rerun_config: dict | None = None # type: ignore[type-arg] + self._rerun_last_log: float = 0.0 @property def transport(self) -> Transport[T]: @@ -149,8 +152,7 @@ def transport(self) -> Transport[T]: @transport.setter def transport(self, value: Transport[T]) -> None: - # just for type checking - ... + self._transport = value @property def state(self) -> State: @@ -173,8 +175,76 @@ def publish(self, msg) -> None: # type: ignore[no-untyped-def] if not hasattr(self, "_transport") or self._transport is None: logger.warning(f"Trying to publish on Out {self} without a transport") return + + # Log to Rerun directly if configured + if self._rerun_config is not None: + self._log_to_rerun(msg) + self._transport.broadcast(self, msg) + def subscribe(self, cb) -> Callable[[], None]: # type: ignore[no-untyped-def] + """Subscribe to this output stream. + + Args: + cb: Callback function to receive messages + + Returns: + Unsubscribe function + """ + return self.transport.subscribe(cb, self) # type: ignore[arg-type, func-returns-value, no-any-return] + + def autolog_to_rerun( + self, + entity_path: str, + rate_limit: float | None = None, + **rerun_kwargs: Any, + ) -> None: + """Configure this output to auto-log to Rerun (fire-and-forget). + + Call once in start() - messages auto-logged when published. + + Args: + entity_path: Rerun entity path (e.g., "world/map") + rate_limit: Max Hz to log (None = unlimited) + **rerun_kwargs: Passed to msg.to_rerun() for rendering config + (e.g., radii=0.02, colormap="turbo", colors=[255,0,0]) + + Example: + def start(self): + super().start() + # Just declare it - fire and forget! + self.global_map.autolog_to_rerun("world/map", rate_limit=5.0, radii=0.02) + """ + self._rerun_config = { + "entity_path": entity_path, + "rate_limit": rate_limit, + "rerun_kwargs": rerun_kwargs, + } + self._rerun_last_log = 0.0 + + def _log_to_rerun(self, msg: T) -> None: + """Log message to Rerun with rate limiting.""" + if not hasattr(msg, "to_rerun"): + return + + if self._rerun_config is None: + return + + import time + + config = self._rerun_config + + # Rate limiting + if config["rate_limit"] is not None: + now = time.monotonic() + min_interval = 1.0 / config["rate_limit"] + if now - self._rerun_last_log < min_interval: + return # Skip - too soon + self._rerun_last_log = now + + rerun_data = msg.to_rerun(**config["rerun_kwargs"]) + rr.log(config["entity_path"], rerun_data) + class RemoteStream(Stream[T]): @property diff --git a/dimos/dashboard/__init__.py b/dimos/dashboard/__init__.py new file mode 100644 index 0000000000..fc97805936 --- /dev/null +++ b/dimos/dashboard/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dashboard module for visualization and monitoring. + +Rerun Initialization: + Main process (e.g., blueprints.build) starts Rerun server automatically. + Worker modules connect to the server via connect_rerun(). + +Usage in modules: + import rerun as rr + from dimos.dashboard.rerun_init import connect_rerun + + class MyModule(Module): + def start(self): + super().start() + connect_rerun() # Connect to Rerun server + rr.log("my/entity", my_data.to_rerun()) +""" + +from dimos.dashboard.rerun_init import connect_rerun, init_rerun_server, shutdown_rerun + +__all__ = ["connect_rerun", "init_rerun_server", "shutdown_rerun"] diff --git a/dimos/dashboard/dimos.rbl b/dimos/dashboard/dimos.rbl new file mode 100644 index 0000000000..160180e27a Binary files /dev/null and b/dimos/dashboard/dimos.rbl differ diff --git a/dimos/dashboard/rerun_init.py b/dimos/dashboard/rerun_init.py new file mode 100644 index 0000000000..81beb40d6a --- /dev/null +++ b/dimos/dashboard/rerun_init.py @@ -0,0 +1,165 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rerun initialization with multi-process support. + +Architecture: + - Main process calls init_rerun_server() to start gRPC server + viewer + - Worker processes call connect_rerun() to connect to the server + - All processes share the same Rerun recording stream + +Viewer modes (set via VIEWER_BACKEND config or environment variable): + - "rerun-web" (default): Web viewer on port 9090 + - "rerun-native": Native Rerun viewer (requires display) + - "foxglove": Use Foxglove instead of Rerun + +Usage: + # Set via environment: + VIEWER_BACKEND=rerun-web # or rerun-native or foxglove + + # Or via .env file: + viewer_backend=rerun-native + + # In main process (blueprints.py handles this automatically): + from dimos.dashboard.rerun_init import init_rerun_server + server_addr = init_rerun_server(viewer_mode="rerun-web") + + # In worker modules: + from dimos.dashboard.rerun_init import connect_rerun + connect_rerun() + + # On shutdown: + from dimos.dashboard.rerun_init import shutdown_rerun + shutdown_rerun() +""" + +import atexit +import threading + +import rerun as rr + +from dimos.core.global_config import GlobalConfig +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + +RERUN_GRPC_PORT = 9876 +RERUN_WEB_PORT = 9090 +RERUN_GRPC_ADDR = f"rerun+http://127.0.0.1:{RERUN_GRPC_PORT}/proxy" + +# Track initialization state +_server_started = False +_connected = False +_rerun_init_lock = threading.Lock() + + +def init_rerun_server(viewer_mode: str = "rerun-web") -> str: + """Initialize Rerun server in the main process. + + Starts the gRPC server and optionally the web/native viewer. + Should only be called once from the main process. + + Args: + viewer_mode: One of "rerun-web", "rerun-native", or "rerun-grpc-only" + + Returns: + Server address for workers to connect to. + + Raises: + RuntimeError: If server initialization fails. + """ + global _server_started + + if _server_started: + logger.debug("Rerun server already started") + return RERUN_GRPC_ADDR + + rr.init("dimos") + + if viewer_mode == "rerun-native": + # Spawn native viewer (requires display) + rr.spawn(port=RERUN_GRPC_PORT, connect=True) + logger.info("Rerun: spawned native viewer", port=RERUN_GRPC_PORT) + elif viewer_mode == "rerun-web": + # Start gRPC + web viewer (headless friendly) + server_uri = rr.serve_grpc(grpc_port=RERUN_GRPC_PORT) + rr.serve_web_viewer(web_port=RERUN_WEB_PORT, open_browser=False, connect_to=server_uri) + logger.info( + "Rerun: web viewer started", + web_port=RERUN_WEB_PORT, + url=f"http://localhost:{RERUN_WEB_PORT}", + ) + else: + # Just gRPC server, no viewer (connect externally) + rr.serve_grpc(grpc_port=RERUN_GRPC_PORT) + logger.info( + "Rerun: gRPC server only", + port=RERUN_GRPC_PORT, + connect_command=f"rerun --connect {RERUN_GRPC_ADDR}", + ) + + _server_started = True + + # Register shutdown handler + atexit.register(shutdown_rerun) + + return RERUN_GRPC_ADDR + + +def connect_rerun( + global_config: GlobalConfig | None = None, + server_addr: str | None = None, +) -> None: + """Connect to Rerun server from a worker process. + + Modules should check global_config.viewer_backend before calling this. + + Args: + global_config: Global configuration (checks viewer_backend) + server_addr: Server address to connect to. Defaults to RERUN_GRPC_ADDR. + """ + global _connected + + with _rerun_init_lock: + if _connected: + logger.debug("Already connected to Rerun server") + return + + # Skip if foxglove backend selected + if global_config and not global_config.viewer_backend.startswith("rerun"): + logger.debug("Rerun connection skipped", viewer_backend=global_config.viewer_backend) + return + + addr = server_addr or RERUN_GRPC_ADDR + + rr.init("dimos") + rr.connect_grpc(addr) + logger.info("Rerun: connected to server", addr=addr) + + _connected = True + + +def shutdown_rerun() -> None: + """Disconnect from Rerun and cleanup resources.""" + global _server_started, _connected + + if _server_started or _connected: + try: + rr.disconnect() + logger.info("Rerun: disconnected") + except Exception as e: + logger.warning("Rerun: error during disconnect", error=str(e)) + + _server_started = False + _connected = False diff --git a/dimos/mapping/costmapper.py b/dimos/mapping/costmapper.py index 022751e0cf..e637126a04 100644 --- a/dimos/mapping/costmapper.py +++ b/dimos/mapping/costmapper.py @@ -13,18 +13,28 @@ # limitations under the License. from dataclasses import asdict, dataclass, field +import queue +import threading +import time from reactivex import operators as ops +import rerun as rr +import rerun.blueprint as rrb from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig from dimos.core.module import ModuleConfig +from dimos.dashboard.rerun_init import connect_rerun from dimos.mapping.pointclouds.occupancy import ( OCCUPANCY_ALGOS, HeightCostConfig, OccupancyConfig, ) from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() @dataclass @@ -37,27 +47,118 @@ class CostMapper(Module): default_config = Config config: Config - global_map: In[LidarMessage] + global_map: In[PointCloud2] global_costmap: Out[OccupancyGrid] + # Background Rerun logging (decouples viz from data pipeline) + _rerun_queue: queue.Queue[tuple[OccupancyGrid, float, float] | None] + _rerun_thread: threading.Thread | None = None + + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for costmap visualization.""" + return [ + rrb.TimeSeriesView( + name="Costmap (ms)", + origin="/metrics/costmap", + contents=["+ /metrics/costmap/calc_ms"], + ), + ] + + def __init__(self, global_config: GlobalConfig | None = None, **kwargs: object) -> None: + super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() + self._rerun_queue = queue.Queue(maxsize=2) + + def _rerun_worker(self) -> None: + """Background thread: pull from queue and log to Rerun (non-blocking).""" + while True: + try: + item = self._rerun_queue.get(timeout=1.0) + if item is None: # Shutdown signal + break + + grid, calc_time_ms, rx_monotonic = item + + # Generate mesh + log to Rerun (blocks in background, not on data path) + try: + # 2D image panel + rr.log( + "world/nav/costmap/image", + grid.to_rerun( + mode="image", + colormap="RdBu_r", + ), + ) + # 3D floor overlay (expensive mesh generation) + rr.log( + "world/nav/costmap/floor", + grid.to_rerun( + mode="mesh", + colormap=None, # Grayscale: free=white, occupied=black + z_offset=0.02, + ), + ) + + # Log timing metrics + rr.log("metrics/costmap/calc_ms", rr.Scalars(calc_time_ms)) + latency_ms = (time.monotonic() - rx_monotonic) * 1000 + rr.log("metrics/costmap/latency_ms", rr.Scalars(latency_ms)) + except Exception as e: + logger.warning(f"Rerun logging error: {e}") + except queue.Empty: + continue + @rpc def start(self) -> None: super().start() + # Only start Rerun logging if Rerun backend is selected + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Start background Rerun logging thread + self._rerun_thread = threading.Thread(target=self._rerun_worker, daemon=True) + self._rerun_thread.start() + logger.info("CostMapper: started async Rerun logging thread") + + def _publish_costmap(grid: OccupancyGrid, calc_time_ms: float, rx_monotonic: float) -> None: + # Publish to downstream FIRST (fast, not blocked by Rerun) + self.global_costmap.publish(grid) + + # Queue for async Rerun logging (non-blocking, drops if queue full) + if self._rerun_thread and self._rerun_thread.is_alive(): + try: + self._rerun_queue.put_nowait((grid, calc_time_ms, rx_monotonic)) + except queue.Full: + pass # Drop viz frame, data pipeline continues + + def _calculate_and_time( + msg: PointCloud2, + ) -> tuple[OccupancyGrid, float, float]: + rx_monotonic = time.monotonic() # Capture receipt time + start = time.perf_counter() + grid = self._calculate_costmap(msg) + elapsed_ms = (time.perf_counter() - start) * 1000 + return grid, elapsed_ms, rx_monotonic + self._disposables.add( self.global_map.observable() # type: ignore[no-untyped-call] - .pipe(ops.map(self._calculate_costmap)) - .subscribe( - self.global_costmap.publish, - ) + .pipe(ops.map(_calculate_and_time)) + .subscribe(lambda result: _publish_costmap(result[0], result[1], result[2])) ) @rpc def stop(self) -> None: + # Shutdown background Rerun thread + if self._rerun_thread and self._rerun_thread.is_alive(): + self._rerun_queue.put(None) # Shutdown signal + self._rerun_thread.join(timeout=2.0) + super().stop() # @timed() # TODO: fix thread leak in timed decorator - def _calculate_costmap(self, msg: LidarMessage) -> OccupancyGrid: + def _calculate_costmap(self, msg: PointCloud2) -> OccupancyGrid: fn = OCCUPANCY_ALGOS[self.config.algo] return fn(msg, **asdict(self.config.config)) diff --git a/dimos/mapping/voxels.py b/dimos/mapping/voxels.py index 6b122213c3..a36dc9bc17 100644 --- a/dimos/mapping/voxels.py +++ b/dimos/mapping/voxels.py @@ -13,6 +13,8 @@ # limitations under the License. from dataclasses import dataclass +import queue +import threading import time import numpy as np @@ -21,14 +23,21 @@ from reactivex import interval, operators as ops from reactivex.disposable import Disposable from reactivex.subject import Subject +import rerun as rr +import rerun.blueprint as rrb from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig from dimos.core.module import ModuleConfig +from dimos.dashboard.rerun_init import connect_rerun from dimos.msgs.sensor_msgs import PointCloud2 from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.utils.decorators import simple_mcache +from dimos.utils.logging_config import setup_logger from dimos.utils.reactive import backpressure +logger = setup_logger() + @dataclass class Config(ModuleConfig): @@ -46,10 +55,32 @@ class VoxelGridMapper(Module): config: Config lidar: In[LidarMessage] - global_map: Out[LidarMessage] - - def __init__(self, **kwargs: object) -> None: + global_map: Out[PointCloud2] + + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for voxel map visualization.""" + return [ + rrb.TimeSeriesView( + name="Voxel Pipeline (ms)", + origin="/metrics/voxel_map", + contents=[ + "+ /metrics/voxel_map/extract_ms", + "+ /metrics/voxel_map/transport_ms", + "+ /metrics/voxel_map/publish_ms", + ], + ), + rrb.TimeSeriesView( + name="Voxel Count", + origin="/metrics/voxel_map", + contents=["+ /metrics/voxel_map/voxel_count"], + ), + ] + + def __init__(self, global_config: GlobalConfig | None = None, **kwargs: object) -> None: super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() + dev = ( o3c.Device(self.config.device) if (self.config.device.startswith("CUDA") and o3c.cuda.is_available()) @@ -72,11 +103,49 @@ def __init__(self, **kwargs: object) -> None: self._voxel_hashmap = self.vbg.hashmap() self._key_dtype = self._voxel_hashmap.key_tensor().dtype self._latest_frame_ts: float = 0.0 + # Monotonic timestamp of last received frame (for accurate latency in replay) + self._latest_frame_rx_monotonic: float | None = None + + # Background Rerun logging (decouples viz from data pipeline) + self._rerun_queue: queue.Queue[PointCloud2 | None] = queue.Queue(maxsize=2) + self._rerun_thread: threading.Thread | None = None + + def _rerun_worker(self) -> None: + """Background thread: pull from queue and log to Rerun (non-blocking).""" + while True: + try: + pc = self._rerun_queue.get(timeout=1.0) + if pc is None: # Shutdown signal + break + + # Log to Rerun (blocks in background, doesn't affect data pipeline) + try: + rr.log( + "world/map", + pc.to_rerun( + mode="boxes", + size=self.config.voxel_size, + colormap="turbo", + ), + ) + except Exception as e: + logger.warning(f"Rerun logging error: {e}") + except queue.Empty: + continue @rpc def start(self) -> None: super().start() + # Only start Rerun logging if Rerun backend is selected + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Start background Rerun logging thread (decouples viz from data pipeline) + self._rerun_thread = threading.Thread(target=self._rerun_worker, daemon=True) + self._rerun_thread.start() + logger.info("VoxelGridMapper: started async Rerun logging thread") + # Subject to trigger publishing, with backpressure to drop if busy self._publish_trigger: Subject[None] = Subject() self._disposables.add( @@ -98,15 +167,53 @@ def start(self) -> None: @rpc def stop(self) -> None: + # Shutdown background Rerun thread + if self._rerun_thread and self._rerun_thread.is_alive(): + self._rerun_queue.put(None) # Shutdown signal + self._rerun_thread.join(timeout=2.0) + super().stop() def _on_frame(self, frame: LidarMessage) -> None: + # Track receipt time with monotonic clock (works correctly in replay) + self._latest_frame_rx_monotonic = time.monotonic() self.add_frame(frame) if self.config.publish_interval == 0: self._publish_trigger.on_next(None) def publish_global_map(self) -> None: - self.global_map.publish(self.get_global_pointcloud2()) + # Snapshot monotonic timestamp once (won't be overwritten during slow publish) + rx_monotonic = self._latest_frame_rx_monotonic + + start_total = time.perf_counter() + + # 1. Extract pointcloud from GPU hashmap + t1 = time.perf_counter() + pc = self.get_global_pointcloud2() + extract_ms = (time.perf_counter() - t1) * 1000 + + # 2. Publish to downstream (NO auto-logging - fast!) + t2 = time.perf_counter() + self.global_map.publish(pc) + publish_ms = (time.perf_counter() - t2) * 1000 + + # 3. Queue for async Rerun logging (non-blocking, drops if queue full) + try: + self._rerun_queue.put_nowait(pc) + except queue.Full: + pass # Drop viz frame, data pipeline continues + + # Log detailed timing breakdown to Rerun + total_ms = (time.perf_counter() - start_total) * 1000 + rr.log("metrics/voxel_map/publish_ms", rr.Scalars(total_ms)) + rr.log("metrics/voxel_map/extract_ms", rr.Scalars(extract_ms)) + rr.log("metrics/voxel_map/transport_ms", rr.Scalars(publish_ms)) + rr.log("metrics/voxel_map/voxel_count", rr.Scalars(float(len(pc)))) + + # Log pipeline latency (time from frame receipt to publish complete) + if rx_monotonic is not None: + latency_ms = (time.monotonic() - rx_monotonic) * 1000 + rr.log("metrics/voxel_map/latency_ms", rr.Scalars(latency_ms)) def size(self) -> int: return self._voxel_hashmap.size() # type: ignore[no-any-return] diff --git a/dimos/msgs/geometry_msgs/PoseStamped.py b/dimos/msgs/geometry_msgs/PoseStamped.py index 0d53c22dd5..406c5d7ac7 100644 --- a/dimos/msgs/geometry_msgs/PoseStamped.py +++ b/dimos/msgs/geometry_msgs/PoseStamped.py @@ -26,8 +26,8 @@ ) except ImportError: ROSPoseStamped = None # type: ignore[assignment, misc] - from plum import dispatch +import rerun as rr from dimos.msgs.geometry_msgs.Pose import Pose from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable @@ -87,6 +87,31 @@ def __str__(self) -> str: f"euler=[{math.degrees(self.roll):.1f}, {math.degrees(self.pitch):.1f}, {math.degrees(self.yaw):.1f}])" ) + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Transform3D format. + + Returns a Transform3D that can be logged to Rerun to position + child entities in the transform hierarchy. + """ + return rr.Transform3D( + translation=[self.x, self.y, self.z], + rotation=rr.Quaternion( + xyzw=[ + self.orientation.x, + self.orientation.y, + self.orientation.z, + self.orientation.w, + ] + ), + ) + + def to_rerun_arrow(self, length: float = 0.5): # type: ignore[no-untyped-def] + """Convert to rerun Arrows3D format for visualization.""" + origin = [[self.x, self.y, self.z]] + forward = self.orientation.rotate_vector(Vector3(length, 0, 0)) + vector = [[forward.x, forward.y, forward.z]] + return rr.Arrows3D(origins=origin, vectors=vector) + def new_transform_to(self, name: str) -> Transform: return self.find_transform( PoseStamped( diff --git a/dimos/msgs/geometry_msgs/Transform.py b/dimos/msgs/geometry_msgs/Transform.py index e1a6f7d424..cb112ee5c9 100644 --- a/dimos/msgs/geometry_msgs/Transform.py +++ b/dimos/msgs/geometry_msgs/Transform.py @@ -34,6 +34,7 @@ ROSTransform = None # type: ignore[assignment, misc] ROSVector3 = None # type: ignore[assignment, misc] ROSQuaternion = None # type: ignore[assignment, misc] +import rerun as rr from dimos.msgs.geometry_msgs.Quaternion import Quaternion from dimos.msgs.geometry_msgs.Vector3 import Vector3 @@ -359,3 +360,18 @@ def lcm_decode(cls, data: bytes | BinaryIO) -> Transform: child_frame_id=lcm_transform_stamped.child_frame_id, ts=ts, ) + + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Transform3D format with frame IDs. + + Returns: + rr.Transform3D archetype for logging to rerun with parent/child frames + """ + return rr.Transform3D( + translation=[self.translation.x, self.translation.y, self.translation.z], + rotation=rr.Quaternion( + xyzw=[self.rotation.x, self.rotation.y, self.rotation.z, self.rotation.w] + ), + parent_frame=self.frame_id, # type: ignore[call-arg] + child_frame=self.child_frame_id, # type: ignore[call-arg] + ) diff --git a/dimos/msgs/nav_msgs/OccupancyGrid.py b/dimos/msgs/nav_msgs/OccupancyGrid.py index 193abff756..62ee9aa072 100644 --- a/dimos/msgs/nav_msgs/OccupancyGrid.py +++ b/dimos/msgs/nav_msgs/OccupancyGrid.py @@ -15,20 +15,30 @@ from __future__ import annotations from enum import IntEnum +from functools import lru_cache import time -from typing import TYPE_CHECKING, BinaryIO +from typing import TYPE_CHECKING, Any, BinaryIO from dimos_lcm.nav_msgs import ( MapMetaData, OccupancyGrid as LCMOccupancyGrid, ) -from dimos_lcm.std_msgs import Time as LCMTime +from dimos_lcm.std_msgs import Time as LCMTime # type: ignore[import-untyped] +import matplotlib.pyplot as plt import numpy as np from PIL import Image +import rerun as rr from dimos.msgs.geometry_msgs import Pose, Vector3, VectorLike from dimos.types.timestamped import Timestamped + +@lru_cache(maxsize=16) +def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] + """Get a matplotlib colormap by name (cached for performance).""" + return plt.get_cmap(name) + + if TYPE_CHECKING: from pathlib import Path @@ -416,3 +426,245 @@ def cell_value(self, world_position: Vector3) -> int: return CostValues.UNKNOWN return int(self.grid[y, x]) + + def to_rerun( # type: ignore[no-untyped-def] + self, + colormap: str | None = None, + mode: str = "image", + z_offset: float = 0.01, + **kwargs: Any, + ): # type: ignore[no-untyped-def] + """Convert to Rerun visualization format. + + Args: + colormap: Optional colormap name (e.g., "RdBu_r" for blue=free, red=occupied). + If None, uses grayscale for image mode or default colors for 3D modes. + mode: Visualization mode: + - "image": 2D grayscale/colored image (default) + - "mesh": 3D textured plane overlay on floor + - "points": 3D points for occupied cells only + z_offset: Height offset for 3D modes (default 0.01m above floor) + **kwargs: Additional args (ignored for compatibility) + + Returns: + Rerun archetype for logging (rr.Image, rr.Mesh3D, or rr.Points3D) + + The visualization uses: + - Free space (value 0): white/blue + - Unknown space (value -1): gray/transparent + - Occupied space (value > 0): black/red with gradient + """ + if self.grid.size == 0: + if mode == "image": + return rr.Image(np.zeros((1, 1), dtype=np.uint8), color_model="L") + elif mode == "mesh": + return rr.Mesh3D(vertex_positions=[]) + else: + return rr.Points3D([]) + + if mode == "points": + return self._to_rerun_points(colormap, z_offset) + elif mode == "mesh": + return self._to_rerun_mesh(colormap, z_offset) + else: + return self._to_rerun_image(colormap) + + def _to_rerun_image(self, colormap: str | None = None): # type: ignore[no-untyped-def] + """Convert to 2D image visualization.""" + # Use existing cached visualization functions for supported palettes + if colormap in ("turbo", "rainbow"): + from dimos.mapping.occupancy.visualizations import rainbow_image, turbo_image + + if colormap == "turbo": + bgr_image = turbo_image(self.grid) + else: + bgr_image = rainbow_image(self.grid) + + # Convert BGR to RGB and flip for world coordinates + rgb_image = np.flipud(bgr_image[:, :, ::-1]) + return rr.Image(rgb_image, color_model="RGB") + + if colormap is not None: + # Use matplotlib colormap (cached for performance) + cmap = _get_matplotlib_cmap(colormap) + + grid_float = self.grid.astype(np.float32) + + # Create RGBA image + vis = np.zeros((self.height, self.width, 4), dtype=np.uint8) + + # Free space: low cost (blue in RdBu_r) + free_mask = self.grid == 0 + # Occupied: high cost (red in RdBu_r) + occupied_mask = self.grid > 0 + # Unknown: transparent gray + unknown_mask = self.grid == -1 + + # Map free to 0, costs to normalized value + if np.any(free_mask): + colors_free = (cmap(0.0)[:3] * np.array([255, 255, 255])).astype(np.uint8) + vis[free_mask, :3] = colors_free + vis[free_mask, 3] = 255 + + if np.any(occupied_mask): + # Normalize costs 1-100 to 0.5-1.0 range + costs = grid_float[occupied_mask] + cost_norm = 0.5 + (costs / 100) * 0.5 + colors_occ = (cmap(cost_norm)[:, :3] * 255).astype(np.uint8) + vis[occupied_mask, :3] = colors_occ + vis[occupied_mask, 3] = 255 + + if np.any(unknown_mask): + vis[unknown_mask] = [128, 128, 128, 100] # Semi-transparent gray + + # Flip vertically to match world coordinates (y=0 at bottom) + return rr.Image(np.flipud(vis), color_model="RGBA") + + # Grayscale visualization (no colormap) + vis_gray = np.zeros((self.height, self.width), dtype=np.uint8) + + # Free space = white + vis_gray[self.grid == 0] = 255 + + # Unknown = gray + vis_gray[self.grid == -1] = 128 + + # Occupied (100) = black, costs (1-99) = gradient + occupied_mask = self.grid > 0 + if np.any(occupied_mask): + # Map 1-100 to 127-0 (darker = more occupied) + costs = self.grid[occupied_mask].astype(np.float32) + vis_gray[occupied_mask] = (127 * (1 - costs / 100)).astype(np.uint8) + + # Flip vertically to match world coordinates (y=0 at bottom) + return rr.Image(np.flipud(vis_gray), color_model="L") + + def _to_rerun_points(self, colormap: str | None = None, z_offset: float = 0.01): # type: ignore[no-untyped-def] + """Convert to 3D points for occupied cells.""" + # Find occupied cells (cost > 0) + occupied_mask = self.grid > 0 + if not np.any(occupied_mask): + return rr.Points3D([]) + + # Get grid coordinates of occupied cells + gy, gx = np.where(occupied_mask) + costs = self.grid[occupied_mask].astype(np.float32) + + # Convert to world coordinates + ox = self.origin.position.x + oy = self.origin.position.y + wx = ox + (gx + 0.5) * self.resolution + wy = oy + (gy + 0.5) * self.resolution + wz = np.full_like(wx, z_offset) + + points = np.column_stack([wx, wy, wz]) + + # Determine colors + if colormap is not None: + # Normalize costs to 0-1 range + cost_norm = costs / 100.0 + cmap = _get_matplotlib_cmap(colormap) + point_colors = (cmap(cost_norm)[:, :3] * 255).astype(np.uint8) + else: + # Default: red gradient based on cost + intensity = (costs / 100.0 * 255).astype(np.uint8) + point_colors = np.column_stack( + [intensity, np.zeros_like(intensity), np.zeros_like(intensity)] + ) + + return rr.Points3D( + positions=points, + radii=self.resolution / 2, + colors=point_colors, + ) + + def _to_rerun_mesh(self, colormap: str | None = None, z_offset: float = 0.01): # type: ignore[no-untyped-def] + """Convert to 3D mesh overlay on floor plane. + + Only renders known cells (free or occupied), skipping unknown cells. + Uses per-vertex colors for proper alpha blending. + Fully vectorized for performance (~100x faster than loop version). + """ + # Only render known cells (not unknown = -1) + known_mask = self.grid != -1 + if not np.any(known_mask): + return rr.Mesh3D(vertex_positions=[]) + + # Get grid coordinates of known cells + gy, gx = np.where(known_mask) + n_cells = len(gy) + + ox = self.origin.position.x + oy = self.origin.position.y + r = self.resolution + + # === VECTORIZED VERTEX GENERATION === + # World positions of cell corners (bottom-left of each cell) + wx = ox + gx.astype(np.float32) * r + wy = oy + gy.astype(np.float32) * r + + # Each cell has 4 vertices: (wx,wy), (wx+r,wy), (wx+r,wy+r), (wx,wy+r) + # Shape: (n_cells, 4, 3) + vertices = np.zeros((n_cells, 4, 3), dtype=np.float32) + vertices[:, 0, 0] = wx + vertices[:, 0, 1] = wy + vertices[:, 0, 2] = z_offset + vertices[:, 1, 0] = wx + r + vertices[:, 1, 1] = wy + vertices[:, 1, 2] = z_offset + vertices[:, 2, 0] = wx + r + vertices[:, 2, 1] = wy + r + vertices[:, 2, 2] = z_offset + vertices[:, 3, 0] = wx + vertices[:, 3, 1] = wy + r + vertices[:, 3, 2] = z_offset + # Flatten to (n_cells*4, 3) + flat_vertices = vertices.reshape(-1, 3) + + # === VECTORIZED INDEX GENERATION === + # Base vertex indices for each cell: [0, 4, 8, 12, ...] + base_v = np.arange(n_cells, dtype=np.uint32) * 4 + # Two triangles per cell: (0,1,2) and (0,2,3) relative to base + indices = np.zeros((n_cells, 2, 3), dtype=np.uint32) + indices[:, 0, 0] = base_v + indices[:, 0, 1] = base_v + 1 + indices[:, 0, 2] = base_v + 2 + indices[:, 1, 0] = base_v + indices[:, 1, 1] = base_v + 2 + indices[:, 1, 2] = base_v + 3 + # Flatten to (n_cells*2, 3) + flat_indices = indices.reshape(-1, 3) + + # === VECTORIZED COLOR GENERATION === + cell_values = self.grid[gy, gx] # Get all cell values at once + + if colormap: + cmap = _get_matplotlib_cmap(colormap) + # Normalize costs: free(0) -> 0.0, cost(1-100) -> 0.5-1.0 + cost_norm = np.where(cell_values == 0, 0.0, 0.5 + (cell_values / 100) * 0.5) + # Sample colormap for all cells at once (returns Nx4 RGBA float) + rgba_float = cmap(cost_norm)[:, :3] # Drop alpha, we set our own + rgb = (rgba_float * 255).astype(np.uint8) + # Alpha: 180 for free, 220 for occupied + alpha = np.where(cell_values == 0, 180, 220).astype(np.uint8) + else: + # Default coloring: dark grey for free, black for occupied + rgb = np.zeros((n_cells, 3), dtype=np.uint8) + is_free = cell_values == 0 + # Free space: dark grey + rgb[is_free] = [40, 40, 40] + # Occupied: black to dark grey gradient (darker = more occupied) + intensity = (40 * (1 - cell_values / 100)).astype(np.uint8) + rgb[~is_free] = np.column_stack([intensity[~is_free]] * 3) + alpha = np.where(is_free, 150, 200).astype(np.uint8) + + # Combine RGB and alpha into RGBA + colors_per_cell = np.column_stack([rgb, alpha]) # (n_cells, 4) + # Repeat each color 4 times (one per vertex) + colors = np.repeat(colors_per_cell, 4, axis=0) # (n_cells*4, 4) + + return rr.Mesh3D( + vertex_positions=flat_vertices, + triangle_indices=flat_indices, + vertex_colors=colors, + ) diff --git a/dimos/msgs/nav_msgs/Path.py b/dimos/msgs/nav_msgs/Path.py index deecbb52e1..ff30b88fa6 100644 --- a/dimos/msgs/nav_msgs/Path.py +++ b/dimos/msgs/nav_msgs/Path.py @@ -30,6 +30,7 @@ from nav_msgs.msg import Path as ROSPath # type: ignore[attr-defined] except ImportError: ROSPath = None # type: ignore[assignment, misc] +import rerun as rr from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped from dimos.types.timestamped import Timestamped @@ -231,3 +232,18 @@ def to_ros_msg(self) -> ROSPath: ros_msg.poses.append(pose.to_ros_msg()) return ros_msg + + def to_rerun(self, color: tuple[int, int, int] = (0, 255, 128)): # type: ignore[no-untyped-def] + """Convert to rerun LineStrips3D format. + + Args: + color: RGB color tuple for the path line + + Returns: + rr.LineStrips3D archetype for logging to rerun + """ + if not self.poses: + return rr.LineStrips3D([]) + + points = [[p.x, p.y, p.z] for p in self.poses] + return rr.LineStrips3D([points], colors=[color]) diff --git a/dimos/msgs/sensor_msgs/CameraInfo.py b/dimos/msgs/sensor_msgs/CameraInfo.py index 482ec44b48..c54b6565fa 100644 --- a/dimos/msgs/sensor_msgs/CameraInfo.py +++ b/dimos/msgs/sensor_msgs/CameraInfo.py @@ -20,6 +20,7 @@ from dimos_lcm.sensor_msgs import CameraInfo as LCMCameraInfo from dimos_lcm.std_msgs.Header import Header import numpy as np +import rerun as rr # Import ROS types try: @@ -372,6 +373,28 @@ def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] and self.frame_id == other.frame_id ) + def to_rerun(self, image_plane_distance: float = 0.5): # type: ignore[no-untyped-def] + """Convert to Rerun Pinhole archetype for camera frustum visualization. + + Args: + image_plane_distance: Distance to draw the image plane in the frustum + + Returns: + rr.Pinhole archetype for logging to Rerun + """ + # Extract intrinsics from K matrix + # K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + fx, fy = self.K[0], self.K[4] + cx, cy = self.K[2], self.K[5] + + return rr.Pinhole( + focal_length=[fx, fy], + principal_point=[cx, cy], + width=self.width, + height=self.height, + image_plane_distance=image_plane_distance, + ) + class CalibrationProvider: """Provides lazy-loaded access to camera calibration YAML files in a directory.""" diff --git a/dimos/msgs/sensor_msgs/Image.py b/dimos/msgs/sensor_msgs/Image.py index 4e07544444..cab6526f3b 100644 --- a/dimos/msgs/sensor_msgs/Image.py +++ b/dimos/msgs/sensor_msgs/Image.py @@ -16,7 +16,7 @@ import base64 import time -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, TypedDict import cv2 from dimos_lcm.sensor_msgs.Image import Image as LCMImage @@ -319,6 +319,10 @@ def to_bgr(self) -> Image: def to_grayscale(self) -> Image: return Image(self._impl.to_grayscale()) + def to_rerun(self) -> Any: + """Convert to rerun Image format.""" + return self._impl.to_rerun() + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> Image: return Image(self._impl.resize(width, height, interpolation)) diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py index 78e979b998..1e842a0b49 100644 --- a/dimos/msgs/sensor_msgs/PointCloud2.py +++ b/dimos/msgs/sensor_msgs/PointCloud2.py @@ -21,11 +21,13 @@ from dimos_lcm.sensor_msgs.PointCloud2 import ( PointCloud2 as LCMPointCloud2, ) -from dimos_lcm.sensor_msgs.PointField import PointField -from dimos_lcm.std_msgs.Header import Header +from dimos_lcm.sensor_msgs.PointField import PointField # type: ignore[import-untyped] +from dimos_lcm.std_msgs.Header import Header # type: ignore[import-untyped] +import matplotlib.pyplot as plt import numpy as np import open3d as o3d # type: ignore[import-untyped] import open3d.core as o3c # type: ignore[import-untyped] +import rerun as rr from dimos.msgs.geometry_msgs import Vector3 @@ -44,6 +46,12 @@ from dimos.types.timestamped import Timestamped +@functools.lru_cache(maxsize=16) +def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] + """Get a matplotlib colormap by name (cached for performance).""" + return plt.get_cmap(name) + + # TODO: encode/decode need to be updated to work with full spectrum of pointcloud2 fields class PointCloud2(Timestamped): msg_name = "sensor_msgs.PointCloud2" @@ -410,6 +418,62 @@ def __len__(self) -> int: return 0 return int(self._pcd_tensor.point["positions"].shape[0]) + def to_rerun( # type: ignore[no-untyped-def] + self, + radii: float = 0.02, + colormap: str | None = None, + colors: list[int] | None = None, + mode: str = "boxes", + size: float | None = None, + fill_mode: str = "solid", + **kwargs, # type: ignore[no-untyped-def] + ): # type: ignore[no-untyped-def] + """Convert to Rerun Points3D or Boxes3D archetype. + + Args: + radii: Point radius for visualization (only for mode="points") + colormap: Optional colormap name (e.g., "turbo", "viridis") to color by height + colors: Optional RGB color [r, g, b] for all points (0-255) + mode: Visualization mode - "points" for spheres, "boxes" for cubes (default) + size: Box size for mode="boxes" (e.g., voxel_size). Defaults to radii*2. + fill_mode: Fill mode for boxes - "solid", "majorwireframe", or "densewireframe" + **kwargs: Additional args (ignored for compatibility) + + Returns: + rr.Points3D or rr.Boxes3D archetype for logging to Rerun + """ + points = self.as_numpy() + if len(points) == 0: + return rr.Points3D([]) if mode == "points" else rr.Boxes3D(centers=[]) + + # Determine colors + point_colors = None + if colormap is not None: + # Color by height (z-coordinate) + z = points[:, 2] + z_norm = (z - z.min()) / (z.max() - z.min() + 1e-8) + cmap = _get_matplotlib_cmap(colormap) + point_colors = (cmap(z_norm)[:, :3] * 255).astype(np.uint8) + elif colors is not None: + point_colors = colors + + if mode == "boxes": + # Use boxes for voxel visualization + box_size = size if size is not None else radii * 2 + half = box_size / 2 + return rr.Boxes3D( + centers=points, + half_sizes=[half, half, half], + colors=point_colors, + fill_mode=fill_mode, # type: ignore[arg-type] + ) + else: + return rr.Points3D( + positions=points, + radii=radii, + colors=point_colors, + ) + def filter_by_height( self, min_height: float | None = None, diff --git a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py index 8de01f9a9d..f5d92a3bc6 100644 --- a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py @@ -22,6 +22,7 @@ import cv2 import numpy as np +import rerun as rr try: import cupy as cp # type: ignore[import-not-found] @@ -108,6 +109,37 @@ def _encode_nvimgcodec_cuda(bgr_cu, quality: int = 80) -> bytes: # type: ignore return bytes(bs0) +def format_to_rerun(data, fmt: ImageFormat): # type: ignore[no-untyped-def] + """Convert image data to Rerun archetype based on format. + + Args: + data: Image data (numpy array or cupy array on CPU) + fmt: ImageFormat enum value + + Returns: + Rerun archetype (rr.Image or rr.DepthImage) + """ + match fmt: + case ImageFormat.RGB: + return rr.Image(data, color_model="RGB") + case ImageFormat.RGBA: + return rr.Image(data, color_model="RGBA") + case ImageFormat.BGR: + return rr.Image(data, color_model="BGR") + case ImageFormat.BGRA: + return rr.Image(data, color_model="BGRA") + case ImageFormat.GRAY: + return rr.Image(data, color_model="L") + case ImageFormat.GRAY16: + return rr.Image(data, color_model="L") + case ImageFormat.DEPTH: + return rr.DepthImage(data) + case ImageFormat.DEPTH16: + return rr.DepthImage(data) + case _: + raise ValueError(f"Unsupported format for Rerun: {fmt}") + + class AbstractImage(ABC): data: Any format: ImageFormat @@ -165,6 +197,10 @@ def resize( ) -> AbstractImage: # pragma: no cover - abstract ... + @abstractmethod + def to_rerun(self) -> Any: # pragma: no cover - abstract + ... + @abstractmethod def sharpness(self) -> float: # pragma: no cover - abstract ... diff --git a/dimos/msgs/sensor_msgs/image_impls/CudaImage.py b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py index 027719ffc6..8230daae29 100644 --- a/dimos/msgs/sensor_msgs/image_impls/CudaImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py @@ -647,6 +647,20 @@ def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) _resize_bilinear_hwc_cuda(self.data, height, width), self.format, self.frame_id, self.ts ) + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Image format. + + Transfers data from GPU to CPU and converts to appropriate format. + + Returns: + rr.Image or rr.DepthImage archetype for logging to rerun + """ + from dimos.msgs.sensor_msgs.image_impls.AbstractImage import format_to_rerun + + # Transfer to CPU + cpu_data = cp.asnumpy(self.data) + return format_to_rerun(cpu_data, self.format) + def crop(self, x: int, y: int, width: int, height: int) -> CudaImage: """Crop the image to the specified region. diff --git a/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py index ff6272e30c..250b951371 100644 --- a/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py @@ -126,6 +126,12 @@ def to_grayscale(self) -> NumpyImage: ) raise ValueError(f"Unsupported format: {self.format}") + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Image format.""" + from dimos.msgs.sensor_msgs.image_impls.AbstractImage import format_to_rerun + + return format_to_rerun(self.data, self.format) + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> NumpyImage: return NumpyImage( cv2.resize(self.data, (width, height), interpolation=interpolation), diff --git a/dimos/msgs/tf2_msgs/TFMessage.py b/dimos/msgs/tf2_msgs/TFMessage.py index 225580b83e..29e890de47 100644 --- a/dimos/msgs/tf2_msgs/TFMessage.py +++ b/dimos/msgs/tf2_msgs/TFMessage.py @@ -159,3 +159,22 @@ def to_ros_msg(self) -> ROSTFMessage: ros_msg.transforms.append(transform.to_ros_transform_stamped()) return ros_msg + + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to a list of rerun Transform3D archetypes. + + Returns a list of tuples (entity_path, Transform3D) for each transform + in the message. The entity_path is derived from the child_frame_id. + + Returns: + List of (entity_path, rr.Transform3D) tuples + + Example: + for path, transform in tf_msg.to_rerun(): + rr.log(path, transform) + """ + results = [] + for transform in self.transforms: + entity_path = f"world/{transform.child_frame_id}" + results.append((entity_path, transform.to_rerun())) # type: ignore[no-untyped-call] + return results diff --git a/dimos/navigation/replanning_a_star/module.py b/dimos/navigation/replanning_a_star/module.py index 9c43c9037c..d8b582be83 100644 --- a/dimos/navigation/replanning_a_star/module.py +++ b/dimos/navigation/replanning_a_star/module.py @@ -19,6 +19,7 @@ from dimos.core import In, Module, Out, rpc from dimos.core.global_config import GlobalConfig +from dimos.dashboard.rerun_init import connect_rerun from dimos.msgs.geometry_msgs import PoseStamped, Twist from dimos.msgs.nav_msgs import OccupancyGrid, Path from dimos.msgs.sensor_msgs import Image @@ -49,6 +50,10 @@ def __init__(self, global_config: GlobalConfig | None = None) -> None: @rpc def start(self) -> None: super().start() + connect_rerun(global_config=self._global_config) + + # Auto-log path to Rerun + self.path.autolog_to_rerun("world/nav/path") unsub = self.odom.subscribe(self._planner.handle_odom) self._disposables.add(Disposable(unsub)) diff --git a/dimos/perception/detection/module2D.py b/dimos/perception/detection/module2D.py index fdf276bcae..cfca3b2192 100644 --- a/dimos/perception/detection/module2D.py +++ b/dimos/perception/detection/module2D.py @@ -56,7 +56,7 @@ class Detection2DModule(Module): config: Config detector: Detector - image: In[Image] + color_image: In[Image] detections: Out[Detection2DArray] annotations: Out[ImageAnnotations] @@ -82,7 +82,7 @@ def process_image_frame(self, image: Image) -> ImageDetections2D: @simple_mcache def sharp_image_stream(self) -> Observable[Image]: return backpressure( - self.image.pure_observable().pipe( + self.color_image.pure_observable().pipe( sharpness_barrier(self.config.max_freq), ) ) @@ -166,7 +166,7 @@ def deploy( # type: ignore[no-untyped-def] from dimos.core import LCMTransport detector = Detection2DModule(**kwargs) - detector.image.connect(camera.color_image) + detector.color_image.connect(camera.color_image) detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) diff --git a/dimos/perception/detection/module3D.py b/dimos/perception/detection/module3D.py index eddba06e3e..037376f995 100644 --- a/dimos/perception/detection/module3D.py +++ b/dimos/perception/detection/module3D.py @@ -37,7 +37,7 @@ class Detection3DModule(Detection2DModule): - image: In[Image] + color_image: In[Image] pointcloud: In[PointCloud2] detections: Out[Detection2DArray] @@ -113,7 +113,7 @@ def ask_vlm(self, question: str) -> str: from dimos.models.vl.qwen import QwenVlModel model = QwenVlModel() - image = self.image.get_next() + image = self.color_image.get_next() return model.query(image, question) # @skill @@ -130,7 +130,7 @@ def nav_vlm(self, question: str) -> str: from dimos.models.vl.qwen import QwenVlModel model = QwenVlModel() - image = self.image.get_next() + image = self.color_image.get_next() result = model.query_detections(image, question) print("VLM result:", result, "for", image, "and question", question) diff --git a/dimos/perception/detection/moduleDB.py b/dimos/perception/detection/moduleDB.py index e9dc1d9518..c37dff8dea 100644 --- a/dimos/perception/detection/moduleDB.py +++ b/dimos/perception/detection/moduleDB.py @@ -23,8 +23,7 @@ from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] from reactivex.observable import Observable -from dimos import spec -from dimos.core import DimosCluster, In, Out, rpc +from dimos.core import In, Out, rpc from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 from dimos.msgs.sensor_msgs import Image, PointCloud2 from dimos.msgs.vision_msgs import Detection2DArray @@ -143,7 +142,7 @@ class ObjectDBModule(Detection3DModule, TableStr): goto: Callable[[PoseStamped], Any] | None = None - image: In[Image] + color_image: In[Image] pointcloud: In[PointCloud2] detections: Out[Detection2DArray] @@ -308,36 +307,6 @@ def __len__(self) -> int: return len(self.objects.values()) -def deploy( # type: ignore[no-untyped-def] - dimos: DimosCluster, - lidar: spec.Pointcloud, - camera: spec.Camera, - prefix: str = "/detectorDB", - **kwargs, -) -> Detection3DModule: - from dimos.core import LCMTransport - - detector = dimos.deploy(ObjectDBModule, camera_info=camera.camera_info_stream, **kwargs) # type: ignore[attr-defined] - - detector.image.connect(camera.color_image) - detector.pointcloud.connect(lidar.pointcloud) - - detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) - detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) - detector.scene_update.transport = LCMTransport(f"{prefix}/scene_update", SceneUpdate) - - detector.detected_image_0.transport = LCMTransport(f"{prefix}/image/0", Image) - detector.detected_image_1.transport = LCMTransport(f"{prefix}/image/1", Image) - detector.detected_image_2.transport = LCMTransport(f"{prefix}/image/2", Image) - - detector.detected_pointcloud_0.transport = LCMTransport(f"{prefix}/pointcloud/0", PointCloud2) - detector.detected_pointcloud_1.transport = LCMTransport(f"{prefix}/pointcloud/1", PointCloud2) - detector.detected_pointcloud_2.transport = LCMTransport(f"{prefix}/pointcloud/2", PointCloud2) - - detector.start() - return detector # type: ignore[no-any-return] - - detectionDB_module = ObjectDBModule.blueprint -__all__ = ["ObjectDBModule", "deploy", "detectionDB_module"] +__all__ = ["ObjectDBModule", "detectionDB_module"] diff --git a/dimos/perception/detection/person_tracker.py b/dimos/perception/detection/person_tracker.py index 4d4fa149dc..6212080858 100644 --- a/dimos/perception/detection/person_tracker.py +++ b/dimos/perception/detection/person_tracker.py @@ -29,7 +29,7 @@ class PersonTracker(Module): detections: In[Detection2DArray] - image: In[Image] + color_image: In[Image] target: Out[PoseStamped] camera_info: CameraInfo @@ -76,7 +76,7 @@ def center_to_3d( def detections_stream(self) -> Observable[ImageDetections2D]: return backpressure( align_timestamped( - self.image.pure_observable(), + self.color_image.pure_observable(), self.detections.pure_observable().pipe( ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] ), diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index df975d87df..e30d42dc32 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -17,8 +17,9 @@ # The blueprints are defined as import strings so as not to trigger unnecessary imports. all_blueprints = { "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:nav", - "unitree-go2-nav": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:nav", "unitree-go2-basic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:basic", + "unitree-go2-nav": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:nav", + "unitree-go2-detection": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:detection", "unitree-go2-spatial": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:spatial", "unitree-go2-agentic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic", "unitree-go2-agentic-ollama": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic_ollama", diff --git a/dimos/robot/foxglove_bridge.py b/dimos/robot/foxglove_bridge.py index 4a208dd78e..ed14a06495 100644 --- a/dimos/robot/foxglove_bridge.py +++ b/dimos/robot/foxglove_bridge.py @@ -15,6 +15,7 @@ import asyncio import logging import threading +from typing import TYPE_CHECKING, Any # this is missing, I'm just trying to import lcm_foxglove_bridge.py from dimos_lcm from dimos_lcm.foxglove_bridge import ( @@ -22,24 +23,46 @@ ) from dimos.core import DimosCluster, Module, rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from dimos.core.global_config import GlobalConfig logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) +logger = setup_logger() + class FoxgloveBridge(Module): _thread: threading.Thread _loop: asyncio.AbstractEventLoop - - def __init__(self, *args, shm_channels=None, jpeg_shm_channels=None, **kwargs) -> None: # type: ignore[no-untyped-def] + _global_config: "GlobalConfig | None" = None + + def __init__( + self, + *args: Any, + shm_channels: list[str] | None = None, + jpeg_shm_channels: list[str] | None = None, + global_config: "GlobalConfig | None" = None, + **kwargs: Any, + ) -> None: super().__init__(*args, **kwargs) self.shm_channels = shm_channels or [] self.jpeg_shm_channels = jpeg_shm_channels or [] + self._global_config = global_config @rpc def start(self) -> None: super().start() + # Skip if Rerun is the selected viewer backend + if self._global_config and self._global_config.viewer_backend.startswith("rerun"): + logger.info( + "Foxglove bridge skipped", viewer_backend=self._global_config.viewer_backend + ) + return + def run_bridge() -> None: self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) diff --git a/dimos/robot/unitree/connection/go2.py b/dimos/robot/unitree/connection/go2.py index 0e9037f25d..03d7677f48 100644 --- a/dimos/robot/unitree/connection/go2.py +++ b/dimos/robot/unitree/connection/go2.py @@ -13,16 +13,20 @@ # limitations under the License. import logging +from pathlib import Path from threading import Thread import time from typing import Any, Protocol from reactivex.disposable import Disposable from reactivex.observable import Observable +import rerun as rr +import rerun.blueprint as rrb from dimos import spec from dimos.core import DimosCluster, In, LCMTransport, Module, Out, pSHMTransport, rpc from dimos.core.global_config import GlobalConfig +from dimos.dashboard.rerun_init import connect_rerun from dimos.msgs.geometry_msgs import ( PoseStamped, Quaternion, @@ -40,6 +44,9 @@ logger = setup_logger(level=logging.INFO) +# URDF path for Go2 robot +_GO2_URDF = Path(__file__).parent.parent / "go2" / "go2.urdf" + class Go2ConnectionProtocol(Protocol): """Protocol defining the interface for Go2 robot connections.""" @@ -137,6 +144,16 @@ class GO2Connection(Module, spec.Camera, spec.Pointcloud): _global_config: GlobalConfig _camera_info_thread: Thread | None = None + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for GO2 camera visualization.""" + return [ + rrb.Spatial2DView( + name="Camera", + origin="world/robot/camera/rgb", + ), + ] + def __init__( # type: ignore[no-untyped-def] self, ip: str | None = None, @@ -179,9 +196,17 @@ def start(self) -> None: self.connection.start() + # Initialize Rerun world frame and load URDF (only if Rerun backend) + if self._global_config.viewer_backend.startswith("rerun"): + self._init_rerun_world() + + def onimage(image: Image) -> None: + self.color_image.publish(image) + rr.log("world/robot/camera/rgb", image.to_rerun()) + self._disposables.add(self.connection.lidar_stream().subscribe(self.lidar.publish)) self._disposables.add(self.connection.odom_stream().subscribe(self._publish_tf)) - self._disposables.add(self.connection.video_stream().subscribe(self.color_image.publish)) + self._disposables.add(self.connection.video_stream().subscribe(onimage)) self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) self._camera_info_thread = Thread( @@ -193,6 +218,45 @@ def start(self) -> None: self.standup() # self.record("go2_bigoffice") + def _init_rerun_world(self) -> None: + """Set up Rerun world frame, load URDF, and static assets. + + Does NOT compose blueprint - that's handled by ModuleBlueprintSet.build(). + """ + connect_rerun(global_config=self._global_config) + + # Set up world coordinate system AND register it as a named frame + # This is KEY - it connects entity paths to the named frame system + rr.log( + "world", + rr.ViewCoordinates.RIGHT_HAND_Z_UP, + rr.CoordinateFrame("world"), # type: ignore[attr-defined] + static=True, + ) + + # Bridge the named frame "world" to the implicit frame hierarchy "tf#/world" + # This connects TF named frames to entity path hierarchy + rr.log( + "world", + rr.Transform3D( + parent_frame="world", # type: ignore[call-arg] + child_frame="tf#/world", # type: ignore[call-arg] + ), + static=True, + ) + + # Load robot URDF + if _GO2_URDF.exists(): + rr.log_file_from_path( + str(_GO2_URDF), + entity_path_prefix="world/robot", + static=True, + ) + logger.info(f"Loaded URDF from {_GO2_URDF}") + + # Log static camera pinhole (for frustum) + rr.log("world/robot/camera", _camera_info_static().to_rerun(), static=True) + @rpc def stop(self) -> None: self.liedown() @@ -248,10 +312,47 @@ def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]: ] def _publish_tf(self, msg: PoseStamped) -> None: - self.tf.publish(*self._odom_to_tf(msg)) + transforms = self._odom_to_tf(msg) + self.tf.publish(*transforms) if self.odom.transport: self.odom.publish(msg) + # Log to Rerun: robot pose (relative to parent entity "world") + rr.log( + "world/robot", + rr.Transform3D( + translation=[msg.x, msg.y, msg.z], + rotation=rr.Quaternion( + xyzw=[ + msg.orientation.x, + msg.orientation.y, + msg.orientation.z, + msg.orientation.w, + ] + ), + ), + ) + # Log axes as a child entity for visualization + rr.log("world/robot/axes", rr.TransformAxes3D(0.5)) # type: ignore[attr-defined] + + # Log camera transform (compose base_link -> camera_link -> camera_optical) + # transforms[1] is camera_link, transforms[2] is camera_optical + cam_tf = transforms[1] + transforms[2] # compose transforms + rr.log( + "world/robot/camera", + rr.Transform3D( + translation=[cam_tf.translation.x, cam_tf.translation.y, cam_tf.translation.z], + rotation=rr.Quaternion( + xyzw=[ + cam_tf.rotation.x, + cam_tf.rotation.y, + cam_tf.rotation.z, + cam_tf.rotation.w, + ] + ), + ), + ) + def publish_camera_info(self) -> None: while True: self.camera_info.publish(_camera_info_static()) diff --git a/dimos/robot/unitree/g1/g1detector.py b/dimos/robot/unitree/g1/g1detector.py index ca549025af..55986eb087 100644 --- a/dimos/robot/unitree/g1/g1detector.py +++ b/dimos/robot/unitree/g1/g1detector.py @@ -31,7 +31,7 @@ def deploy(dimos: DimosCluster, ip: str): # type: ignore[no-untyped-def] detector=YoloPersonDetector, ) - detector3d = moduleDB.deploy( + detector3d = moduleDB.deploy( # type: ignore[attr-defined] dimos, camera=camera, lidar=nav, diff --git a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py index bf65447e25..8173f27527 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py +++ b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py @@ -16,6 +16,10 @@ import platform +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( # type: ignore[import-untyped] + ImageAnnotations, +) + from dimos.agents.agent import llm_agent from dimos.agents.cli.human import human_input from dimos.agents.cli.web import web_input @@ -25,19 +29,21 @@ from dimos.agents.spec import Provider from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE from dimos.core.blueprints import autoconnect -from dimos.core.transport import JpegLcmTransport, JpegShmTransport, pSHMTransport +from dimos.core.transport import JpegLcmTransport, JpegShmTransport, LCMTransport, pSHMTransport from dimos.mapping.costmapper import cost_mapper from dimos.mapping.voxels import voxel_mapper -from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray from dimos.navigation.frontier_exploration import ( wavefront_frontier_explorer, ) from dimos.navigation.replanning_a_star.module import ( replanning_a_star_planner, ) +from dimos.perception.detection.moduleDB import ObjectDBModule, detectionDB_module from dimos.perception.spatial_perception import spatial_memory from dimos.robot.foxglove_bridge import foxglove_bridge -from dimos.robot.unitree.connection.go2 import go2_connection +from dimos.robot.unitree.connection.go2 import GO2Connection, go2_connection from dimos.robot.unitree_webrtc.unitree_skill_container import unitree_skills from dimos.utils.monitoring import utilization from dimos.web.websocket_vis.websocket_vis_module import websocket_vis @@ -71,12 +77,53 @@ nav = autoconnect( basic, - voxel_mapper(voxel_size=0.05), + voxel_mapper(voxel_size=0.1), cost_mapper(), replanning_a_star_planner(), wavefront_frontier_explorer(), ).global_config(n_dask_workers=6, robot_model="unitree_go2") +detection = ( + autoconnect( + nav, + detectionDB_module( + camera_info=GO2Connection.camera_info_static, + ), + ) + .remappings( + [ + (ObjectDBModule, "pointcloud", "global_map"), + ] + ) + .transports( + { + # Detection 3D module outputs + ("detections", ObjectDBModule): LCMTransport( + "/detector3d/detections", Detection2DArray + ), + ("annotations", ObjectDBModule): LCMTransport( + "/detector3d/annotations", ImageAnnotations + ), + # ("scene_update", ObjectDBModule): LCMTransport( + # "/detector3d/scene_update", SceneUpdate + # ), + ("detected_pointcloud_0", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/0", PointCloud2 + ), + ("detected_pointcloud_1", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/1", PointCloud2 + ), + ("detected_pointcloud_2", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/2", PointCloud2 + ), + ("detected_image_0", ObjectDBModule): LCMTransport("/detector3d/image/0", Image), + ("detected_image_1", ObjectDBModule): LCMTransport("/detector3d/image/1", Image), + ("detected_image_2", ObjectDBModule): LCMTransport("/detector3d/image/2", Image), + } + ) +) + + spatial = autoconnect( nav, spatial_memory(), diff --git a/dimos/web/command-center-extension/.gitignore b/dimos/web/command-center-extension/.gitignore index 3f7224ed26..1cb79e0e3c 100644 --- a/dimos/web/command-center-extension/.gitignore +++ b/dimos/web/command-center-extension/.gitignore @@ -1,5 +1,6 @@ *.foxe /dist +/dist-standalone /node_modules !/package.json !/package-lock.json diff --git a/dimos/web/command-center-extension/index.html b/dimos/web/command-center-extension/index.html new file mode 100644 index 0000000000..e1e9ce85ad --- /dev/null +++ b/dimos/web/command-center-extension/index.html @@ -0,0 +1,18 @@ + + + + + + Command Center + + + +
+ + + diff --git a/dimos/web/command-center-extension/package-lock.json b/dimos/web/command-center-extension/package-lock.json index 6446666ebc..09f9be88b4 100644 --- a/dimos/web/command-center-extension/package-lock.json +++ b/dimos/web/command-center-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "command-center-extension", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "command-center-extension", - "version": "0.0.0", + "version": "0.0.1", "license": "UNLICENSED", "dependencies": { "@types/pako": "^2.0.4", @@ -23,12 +23,732 @@ "@types/leaflet": "^1.9.21", "@types/react": "18.3.24", "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "^4.3.4", "create-foxglove-extension": "1.0.6", "eslint": "9.34.0", "prettier": "3.6.2", "react": "18.3.1", "react-dom": "^18.3.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -375,6 +1095,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -476,6 +1206,298 @@ "react-dom": "^18.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -489,6 +1511,47 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -1139,6 +2202,26 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -1882,6 +2965,12 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2791,6 +3880,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3576,6 +4706,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3617,6 +4761,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4552,6 +5705,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4873,6 +6038,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5293,6 +6476,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5437,6 +6648,15 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5628,6 +6848,47 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6041,6 +7302,15 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -6485,6 +7755,51 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6795,6 +8110,109 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -7161,6 +8579,12 @@ "node": ">=0.4.0" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/dimos/web/command-center-extension/package.json b/dimos/web/command-center-extension/package.json index cd05ffb1dd..f3cd836205 100644 --- a/dimos/web/command-center-extension/package.json +++ b/dimos/web/command-center-extension/package.json @@ -1,7 +1,7 @@ { "name": "command-center-extension", "displayName": "command-center-extension", - "description": "", + "description": "2D costmap visualization with robot and path overlay", "publisher": "dimensional", "homepage": "", "version": "0.0.1", @@ -10,6 +10,9 @@ "keywords": [], "scripts": { "build": "foxglove-extension build", + "build:standalone": "vite build", + "dev": "vite", + "preview": "vite preview", "foxglove:prepublish": "foxglove-extension build --mode production", "lint": "eslint .", "lint:ci": "eslint .", @@ -25,12 +28,14 @@ "@types/leaflet": "^1.9.21", "@types/react": "18.3.24", "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "^4.3.4", "create-foxglove-extension": "1.0.6", "eslint": "9.34.0", "prettier": "3.6.2", "react": "18.3.1", "react-dom": "^18.3.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "vite": "^6.0.0" }, "dependencies": { "@types/pako": "^2.0.4", diff --git a/dimos/web/command-center-extension/src/standalone.tsx b/dimos/web/command-center-extension/src/standalone.tsx new file mode 100644 index 0000000000..7fefcab0fd --- /dev/null +++ b/dimos/web/command-center-extension/src/standalone.tsx @@ -0,0 +1,20 @@ +/** + * Standalone entry point for the Command Center React app. + * This allows the command center to run outside of Foxglove as a regular web page. + */ +import * as React from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App"; + +const container = document.getElementById("root"); +if (container) { + const root = createRoot(container); + root.render( + + + + ); +} else { + console.error("Root element not found"); +} diff --git a/dimos/web/command-center-extension/vite.config.ts b/dimos/web/command-center-extension/vite.config.ts new file mode 100644 index 0000000000..064f2bc7c5 --- /dev/null +++ b/dimos/web/command-center-extension/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [react()], + root: ".", + build: { + outDir: "dist-standalone", + emptyDirBeforeWrite: true, + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + }, + }, + }, + server: { + port: 3000, + open: false, + }, +}); diff --git a/dimos/web/templates/rerun_dashboard.html b/dimos/web/templates/rerun_dashboard.html new file mode 100644 index 0000000000..9917d9d2af --- /dev/null +++ b/dimos/web/templates/rerun_dashboard.html @@ -0,0 +1,20 @@ + + + + Dimos Dashboard + + + +
+ + +
+ + diff --git a/dimos/web/websocket_vis/websocket_vis_module.py b/dimos/web/websocket_vis/websocket_vis_module.py index 698701058f..e3638a6fe9 100644 --- a/dimos/web/websocket_vis/websocket_vis_module.py +++ b/dimos/web/websocket_vis/websocket_vis_module.py @@ -16,9 +16,14 @@ """ WebSocket Visualization Module for Dimos navigation and mapping. + +This module provides a WebSocket data server for real-time visualization. +The frontend is served from a separate HTML file. """ import asyncio +import os +from pathlib import Path as FilePath import threading import time from typing import Any @@ -27,10 +32,18 @@ from reactivex.disposable import Disposable import socketio # type: ignore[import-untyped] from starlette.applications import Starlette -from starlette.responses import HTMLResponse -from starlette.routing import Route +from starlette.responses import FileResponse, RedirectResponse, Response +from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles import uvicorn +# Path to the frontend HTML templates and command-center build +_TEMPLATES_DIR = FilePath(__file__).parent.parent / "templates" +_DASHBOARD_HTML = _TEMPLATES_DIR / "rerun_dashboard.html" +_COMMAND_CENTER_DIR = ( + FilePath(__file__).parent.parent / "command-center-extension" / "dist-standalone" +) + from dimos.core import In, Module, Out, rpc from dimos.mapping.occupancy.gradient import gradient from dimos.mapping.occupancy.inflation import simple_inflate @@ -128,6 +141,9 @@ def start(self) -> None: self._uvicorn_server_thread = threading.Thread(target=self._run_uvicorn_server, daemon=True) self._uvicorn_server_thread.start() + # Show control center link in terminal + logger.info(f"Command Center: http://localhost:{self.port}/command-center") + try: unsub = self.odom.subscribe(self._on_robot_pose) self._disposables.add(Disposable(unsub)) @@ -186,9 +202,40 @@ def _create_server(self) -> None: self.sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") async def serve_index(request): # type: ignore[no-untyped-def] - return HTMLResponse("Use the extension.") + """Serve appropriate HTML based on viewer mode.""" + # If running native Rerun, redirect to standalone command center + viewer_backend = os.environ.get("VIEWER_BACKEND", "rerun-web").lower() + if viewer_backend == "rerun-native": + return RedirectResponse(url="/command-center") + # Otherwise serve full dashboard with Rerun iframe + return FileResponse(_DASHBOARD_HTML, media_type="text/html") + + async def serve_command_center(request): # type: ignore[no-untyped-def] + """Serve the command center 2D visualization (built React app).""" + index_file = _COMMAND_CENTER_DIR / "index.html" + if index_file.exists(): + return FileResponse(index_file, media_type="text/html") + else: + return Response( + content="Command center not built. Run: cd dimos/web/command-center-extension && npm install && npm run build:standalone", + status_code=503, + media_type="text/plain", + ) - routes = [Route("/", serve_index)] + routes = [ + Route("/", serve_index), + Route("/command-center", serve_command_center), + ] + + # Add static file serving for command-center assets if build exists + if _COMMAND_CENTER_DIR.exists(): + routes.append( + Mount( # type: ignore[arg-type] + "/assets", + app=StaticFiles(directory=_COMMAND_CENTER_DIR / "assets"), + name="assets", + ) + ) starlette_app = Starlette(routes=routes) self.app = socketio.ASGIApp(self.sio, starlette_app) diff --git a/docs/VIEWER_BACKENDS.md b/docs/VIEWER_BACKENDS.md new file mode 100644 index 0000000000..d70e3fa824 --- /dev/null +++ b/docs/VIEWER_BACKENDS.md @@ -0,0 +1,77 @@ +# Viewer Backends + +Dimos supports three visualization backends: Rerun (web or native) and Foxglove. + +## Quick Start + +Choose your viewer backend with the `VIEWER_BACKEND` environment variable: + +```bash +# Rerun native viewer (default) - Fast native window + control center +dimos run unitree-go2 +# or explicitly: +VIEWER_BACKEND=rerun-native dimos run unitree-go2 + +# Rerun web viewer - Full dashboard in browser +VIEWER_BACKEND=rerun-web dimos run unitree-go2 + +# Foxglove - Use Foxglove Studio instead of Rerun +VIEWER_BACKEND=foxglove dimos run unitree-go2 +``` + +## Viewer Modes Explained + +### Rerun Web (`rerun-web`) + +**What you get:** +- Full dashboard at http://localhost:7779 +- Rerun 3D viewer + command center sidebar in one page +- Works in browser, no display required (headless-friendly) + +--- + +### Rerun Native (`rerun-native`) + +**What you get:** +- Native Rerun application (separate window opens automatically) +- Command center at http://localhost:7779 +- Better performance with larger maps/higher resolution + +--- + +### Foxglove (`foxglove`) + +**What you get:** +- Foxglove bridge on ws://localhost:8765 +- No Rerun (saves resources) +- Better performance with larger maps/higher resolution +- Open layout: `dimos/assets/foxglove_dashboards/go2.json` + +--- + +## Performance Tuning + +### Symptom: Slow Map Updates + +If you notice: +- Robot appears to "walk across empty space" +- Costmap updates lag behind the robot +- Visualization stutters or freezes + +This happens on lower-end hardware (NUC, older laptops) with large maps. + +### Increase Voxel Size + +Edit [`dimos/robot/unitree_webrtc/unitree_go2_blueprints.py`](../dimos/robot/unitree_webrtc/unitree_go2_blueprints.py) line 82: + +```python +# Before (high detail, slower on large maps) +voxel_mapper(voxel_size=0.05), # 5cm voxels + +# After (lower detail, 8x faster) +voxel_mapper(voxel_size=0.1), # 10cm voxels +``` + +**Trade-off:** +- Larger voxels = fewer voxels = faster updates +- But slightly less detail in the map diff --git a/pyproject.toml b/pyproject.toml index f4bd6c9fa2..9cbe25dded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ exclude = ["dimos.web.websocket_vis.node_modules*"] [tool.setuptools.package-data] "dimos" = ["*.html", "*.css", "*.js", "*.json", "*.txt", "*.yaml", "*.yml"] "dimos.utils.cli" = ["*.tcss"] +"dimos.robot.unitree.go2" = ["*.urdf"] "dimos.robot.unitree_webrtc.params" = ["*.yaml", "*.yml"] "dimos.web.templates" = ["*"] "dimos.rxpy_backpressure" = ["*.txt"] @@ -38,6 +39,7 @@ dependencies = [ "cerebras-cloud-sdk", "moondream", "numpy>=1.26.4", + "rerun-sdk>=0.20.0", "colorlog==6.9.0", "yapf==0.40.2", "typeguard", diff --git a/uv.lock b/uv.lock index da7940bcdc..6e5e4d940b 100644 --- a/uv.lock +++ b/uv.lock @@ -1505,6 +1505,7 @@ dependencies = [ { name = "pyturbojpeg" }, { name = "reactivex" }, { name = "requests" }, + { name = "rerun-sdk" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1705,6 +1706,7 @@ requires-dist = [ { name = "regex", marker = "extra == 'cuda'" }, { name = "requests" }, { name = "requests-mock", marker = "extra == 'dev'", specifier = "==1.12.1" }, + { name = "rerun-sdk", specifier = ">=0.20.0" }, { name = "rtree", marker = "extra == 'manipulation'" }, { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.3" }, { name = "scikit-learn" }, @@ -7520,6 +7522,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rerun-sdk" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "pyarrow" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/4f/7105b8aa0cfb2afdef53ef34d82a043126b6a69e74fbc530c08362b756b5/rerun_sdk-0.28.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:efc4534ad2db8ec8c3b3c68f5883537d639c6758943e1ee10cfe6fb2022708e5", size = 108152262, upload-time = "2025-12-19T22:15:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/2a170f9d7c59888875cd60f9b756fc88eef6bf431ffbfe030291bb272754/rerun_sdk-0.28.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:af0631b2598c7edb1872b42ec67ff04ff07bda60db6a4f9987821e4628271cbb", size = 115949311, upload-time = "2025-12-19T22:15:56.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/f7/ca2231395357d874f6e94daede9d726f9a5969654ae7efca4990cf3b3c6e/rerun_sdk-0.28.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:330ff7df6bcc31d45ccea84e8790b8755353731230c036bfa33fed53a48a02e7", size = 123828354, upload-time = "2025-12-19T22:16:02.755Z" }, + { url = "https://files.pythonhosted.org/packages/bb/36/81fda4823c56c492cc5dc0408acb3635f08b0b17b1471b90dfbc4bea2793/rerun_sdk-0.28.1-cp310-abi3-win_amd64.whl", hash = "sha256:04e70610090dee4128b404a7f010c3b25208708c9dd2b0a279dbd27a69ccf453", size = 105707482, upload-time = "2025-12-19T22:16:08.704Z" }, +] + [[package]] name = "retrying" version = "1.4.2"