diff --git a/.gitignore b/.gitignore
index c440836..eb6994f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,16 @@
node_modules/
+node_modules
dist/
*.tsbuildinfo
__pycache__/
*.py[cod]
+
+# Trail-code's graft map is regenerable local state (run `trail build`).
+/graft/
+
+# Local agent state and notes.
+/.codex/
+/.agent
+/.agents/
+/.trail/
+/.ignore
diff --git a/autonomous-grid-search/README.md b/autonomous-grid-search/README.md
new file mode 100644
index 0000000..9040e4d
--- /dev/null
+++ b/autonomous-grid-search/README.md
@@ -0,0 +1,63 @@
+# Autonomous Grid Search
+
+The Pi companion API plans square-cell camera surveys, and the standalone web app previews routes, battery sorties, and simulation. Live Pixhawk mission execution remains locked.
+
+## Auto-grid survey planner
+
+Start the companion API on the Pi, or run the simulator locally from the repository root:
+
+```bash
+cd telemetry/handheld-nrf-monitor/pi-code
+python -m pip install -r ../../../autonomous-grid-search/backend/requirements.txt
+python main.py --simulate --no-radio
+```
+
+In a second terminal, start the standalone webpage from the repository root:
+
+```bash
+cd autonomous-grid-search/web
+npm ci
+npm run dev
+```
+
+Open `http://127.0.0.1:5174/` and enter the Pi address and port 8765 in **Companion API**. The Python companion runs separately and must be reachable from the browser. Use a trusted network; the planning/simulation API has no authentication.
+
+1. Click the map to select the **search centre**. Coordinate entry is available under **Connection & coordinate entry** for keyboard use.
+2. Enter **centre to edge in metres**, then select **Generate survey on Pi**. The search area is a square: 50 m centre-to-edge means a 100 × 100 m square.
+3. The browser sends only `search_area: {center: {latitude, longitude}, radius_m}`; the existing API field `radius_m` now represents square half-side. The Pi reads `survey_config.json`, gets launch/home from fresh 3D vehicle GPS, divides a square in a local metric projection into equal square cells, and computes a complete camera grid in every cell. The vehicle must be disarmed. Without usable GPS, the Pi previews coverage but blocks export and simulation.
+4. Review the returned route. Select a sortie and **Export to Mission Planner** to download `sentinelblue_survey_N.waypoints` (QGC WPL 110). In Mission Planner use **PLAN → Load WP File**. Export each battery sortie separately. Changes to the point or radius invalidate the old plan.
+5. **Play swarm preview** animates the Pi-generated assignments and timelines for virtual drones. Pause, reset or change playback speed; the display tracks completed cells and each drone's phase. This visual preview sends no vehicle commands. **Run single-drone simulator** executes the selected cached sortie after acknowledgement. Plans expire after 10 minutes or API restart. The API rejects replacement while the simulator is armed and rejects all live execution.
+
+### Configure the aircraft once on the Pi
+
+Edit `autonomous-grid-search/backend/survey_config.json` on the companion computer. Defaults are 5 m height, 2 m/s, 78° × 60° FOV, 70% side overlap, 75% forward overlap, 20 minute endurance and 20% reserve. These are starting values, not measured specifications for your hardware. Calibrate the camera, endurance and trigger interval for the actual aircraft before flight validation. The file is loaded for each planning request; the browser cannot override it through a point/radius request. Existing reviewed plans retain their original settings.
+
+`max_cell_side_m` defaults to **50 m**, `swarm_drone_count` to **4**, `square_grid_angle_deg` to **0°** (north/south tracks), and `battery_swap_seconds` to **120 s** between a virtual drone's sorties. These values are configured on the Pi. Cell count per axis is `ceil(square_side / max_cell_side_m)`; the entire area is tiled by equal cells no larger than the maximum. For example, a 120 m square becomes nine 40 × 40 m cells, with no uncovered remainder strip.
+
+The Pi calculates camera spacing and produces alternating parallel tracks within each cell. It assigns whole cells to the least-loaded virtual drone by accumulated estimated flight time. Every cell has one owner; its complete lines are split into battery sorties capped at 500 commands and the configured battery budget. Every sortie contains takeoff, speed, survey/camera commands, return and land. Planning is capped at 100 cells, 100 sorties and 10,000 lines; oversized requests return an error instead of dropping coverage.
+
+**Swarm preview is a planning visualization, not live swarm control.** Timelines include takeoff/landing allowances, survey, transfers, turns and battery changes. Virtual drones share the same launch/home position. Shared launch and transit paths are not collision-deconflicted. Real simultaneous multi-drone flight remains disabled; this does not implement multi-vehicle MAVLink connections or formation control.
+
+The geometry engine retains its polygon API for integration; the operator-facing screen uses only a centre and square extent. Exports remain separate by cell/drone/sortie, with the selected assignment shown in the sortie picker.
+
+### Camera and flight assumptions
+
+Footprint = `2 × height × tan(FOV / 2)`. Maximum line gap = cross-track footprint × `(1 − side overlap)`. Trigger distance = along-track footprint × `(1 − forward overlap)`. Spacing tightens to fit the polygon extent. Crosshatch passes can have slightly different actual gaps; the displayed gap is the first pass. Modelled coverage uses flat, nadir, aligned footprints with distance-triggered photos. At least 99.5% modelled coverage is required to export; this is not measured image coverage.
+
+A camera-rate check blocks intervals shorter than the entered camera minimum. Export includes `DO_CHANGE_SPEED` and `DO_SET_CAM_TRIGG_DIST`; configure and test the actual camera interface and yaw behaviour in ArduPilot before flight. The simulator models route, commanded speed and waypoint dwell; it does not simulate actual photos, wind, vehicle dynamics or camera latency.
+
+For ArduCopter, configure face-next-waypoint yaw and a waypoint radius smaller than the line gap, and validate the route in ArduPilot SITL. Per-waypoint yaw and acceptance-radius fields do not configure these behaviours in Copter; see the [official mission command reference](https://docs.ardupilot.org/copter/docs/mission-command-list.html). The built-in simulator is not ArduPilot SITL.
+
+Each line has a one-second dwell at each end. Estimates add the entered turn allowance per line and two minutes for takeoff/landing. They do not measure battery capacity, wind or power consumption. A line that cannot fit one sortie is blocked rather than truncated.
+
+**Altitude is relative to home, not terrain-following.** Coverage assumes a flat surface at home elevation. A 5 m mission over water requires a validated downward sensor and flight-controller configuration; this planner does not configure them. DEM data is not water clearance. Run-outs and straight transfers may leave the boundary; the boundary is not a geofence. The planner does not provide obstacle avoidance, airspace checks or industrial flight certification.
+
+### Maps and terrain
+
+The default map uses OpenStreetMap raster tiles and MapLibre demo hillshade. Set `VITE_MAPTILER_KEY` at build time for MapTiler hybrid imagery. The map stays top-down for boundary accuracy.
+
+Optional terrain context samples home, centre and boundary corners using OpenTopoData SRTM90m. Set `TERRAIN_API_URL` to another compatible endpoint. Missing tiles or terrain data do not change mission altitude. Coordinates remain editable when imagery is unavailable.
+
+### Verification
+
+Run `python -m unittest discover -s tests -v` from `autonomous-grid-search/backend` and `npm run check` from `autonomous-grid-search/web`. Tests cover camera axes, sensor dimensions, coverage, invalid polygons, rotation, reverse entry, concave boundaries, crosshatch, camera rate, sortie limits, exact-plan execution, expiry, confirmation, live/armed guards, WPL coordinate frames and boundary parsing.
diff --git a/autonomous-grid-search/backend/auto_grid_planner.py b/autonomous-grid-search/backend/auto_grid_planner.py
new file mode 100644
index 0000000..2b89f47
--- /dev/null
+++ b/autonomous-grid-search/backend/auto_grid_planner.py
@@ -0,0 +1,490 @@
+"""Polygon camera surveys in a local metric projection; review/simulation only.
+
+Tracks are clipped to the survey polygon. Transit legs and optional run-outs
+are displayed explicitly and are NOT obstacle/geofence avoidance routes.
+"""
+
+from dataclasses import dataclass
+import math
+import json
+import pathlib
+import time
+from typing import Any, Dict
+
+from pyproj import CRS, Transformer
+from shapely import affinity
+from shapely.geometry import LineString, Polygon, box
+from shapely.ops import unary_union
+from shapely.validation import explain_validity
+
+from search_mission_planner import _bounded_number, _navigation_item, _camera_trigger_item
+
+MAX_LINES = 10000
+MAX_ITEMS = 500
+MAX_SORTIES = 100
+
+
+def build_radius_survey(payload, telemetry):
+ """The browser supplies an area only; all flight settings belong to the Pi."""
+ area = payload.get("search_area")
+ if not isinstance(area, dict):
+ raise ValueError("search_area must contain center and radius_m")
+ center = position(area.get("center"), "search center")
+ radius = number(area, "radius_m", None, 2, 10000)
+ if telemetry.get("armed"):
+ raise ValueError("Finish the current flight before planning from a new launch position")
+ try:
+ config = json.loads(pathlib.Path(__file__).with_name("survey_config.json").read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as exc:
+ raise ValueError("Pi survey_config.json is missing or invalid; check the aircraft configuration") from exc
+ if not isinstance(config, dict) or not isinstance(config.get("survey_options"), dict):
+ raise ValueError("Pi survey_config.json must include survey_options")
+ source = dict(telemetry)
+ try:
+ fresh = all(0 <= time.time() - float(source.get(key, 0)) <= 5
+ for key in ("last_position_timestamp", "last_gps_timestamp"))
+ except (TypeError, ValueError):
+ fresh = False
+ if not fresh or source.get("gps_fix_type") not in ("3D FIX", "DGPS", "RTK FLT", "RTK FIX"):
+ source["connected"] = False
+ plan = build_cell_survey(config, center, radius, source)
+ plan.data["search_area"] = {"center": center, "radius_m": radius, "shape": "square", "side_m": radius * 2}
+ plan.data["profile_name"] = str(config.get("profile_name", "pi_survey_profile"))
+ plan.data["terrain_enabled"] = config.get("terrain_enabled") is True
+ plan.data["surface_type"] = config.get("surface_type", "water")
+ if plan.data["home_source"] == "area_preview_only":
+ reason = "Pi needs fresh 3D GPS from the disarmed vehicle to determine launch/home"
+ plan.data["estimate"]["blocking_reasons"] = [reason] + plan.data["estimate"]["blocking_reasons"][1:]
+ for sortie in plan.data["sorties"]:
+ sortie["blocking_reasons"][0] = reason
+ return plan
+
+
+def build_cell_survey(config, center, half_side, telemetry):
+ """Partition a metric square; every cell gets a full coverage mission.
+
+ Drone assignments and timelines are for visual simulation only. Shared
+ launch/transit paths are not deconflicted for simultaneous real flight.
+ """
+ maximum = number(config, "max_cell_side_m", 50, 5, 1000)
+ drone_count = number(config, "swarm_drone_count", 4, 1, 8)
+ battery_swap = number(config, "battery_swap_seconds", 120, 0, 3600)
+ if not drone_count.is_integer():
+ raise ValueError("swarm_drone_count must be an integer")
+ count = math.ceil(2 * half_side / maximum)
+ if count * count > 100:
+ raise ValueError("Search exceeds 100 square cells; reduce the search extent or adjust the Pi cell size")
+ cell_side = 2 * half_side / count
+ local = CRS.from_proj4(f"+proj=aeqd +lat_0={center['latitude']} +lon_0={center['longitude']} +datum=WGS84 +units=m")
+ to_geo = Transformer.from_crs(local, "EPSG:4326", always_xy=True)
+ to_local = Transformer.from_crs("EPSG:4326", local, always_xy=True)
+
+ def geo(x, y):
+ lon, lat = to_geo.transform(x, y)
+ return {"latitude": lat, "longitude": lon}
+
+ def corners(west, south, east, north):
+ return [geo(west, south), geo(east, south), geo(east, north), geo(west, north)]
+
+ home = position(telemetry, "vehicle position") if telemetry.get("connected") else center
+ home_source = "vehicle_position" if telemetry.get("connected") else "area_preview_only"
+ drones = [{"drone_id": index + 1, "cell_ids": [], "sortie_ids": [], "duration_s": 0.0, "segments": []}
+ for index in range(min(int(drone_count), count * count))]
+ cells, sorties, lines, components = [], [], [], []
+ options = {**config["survey_options"], "angle_mode": "manual",
+ "grid_angle_deg": number(config, "square_grid_angle_deg", 0, 0, 179.99)}
+
+ for row in range(count):
+ for col in (range(count) if row % 2 == 0 else reversed(range(count))):
+ cell_id = row * count + col + 1
+ west, south = -half_side + col * cell_side, -half_side + row * cell_side
+ boundary = corners(west, south, west + cell_side, south + cell_side)
+ data = build_auto_grid_mission({**config, "survey_options": options, "home": home, "boundary": boundary}, {}).to_dict()
+ drone = min(drones, key=lambda entry: (entry["duration_s"], entry["drone_id"]))
+ drone["cell_ids"].append(cell_id)
+ cell_sorties = []
+ for sortie in data["sorties"]:
+ original_id = sortie["sortie_id"]
+ sortie_id = len(sorties) + 1
+ sortie.update(sortie_id=sortie_id, cell_id=cell_id, drone_id=drone["drone_id"])
+ if home_source == "area_preview_only":
+ sortie["ready"] = False
+ sortie["mission_items"] = []
+ sortie["blocking_reasons"].insert(0, "Pi needs fresh 3D GPS from the disarmed vehicle to determine launch/home")
+ sorties.append(sortie)
+ cell_sorties.append(sortie_id)
+ drone["sortie_ids"].append(sortie_id)
+ for track in data["survey_lines"]:
+ if track["sortie_id"] == original_id:
+ # Copy: original local IDs are needed for subsequent sorties.
+ lines.append({**track, "cell_id": cell_id, "drone_id": drone["drone_id"],
+ "sortie_id": sortie_id, "line_index": len(lines) + 1})
+ start_time = drone["duration_s"]
+
+ def segment(start, end, duration, phase):
+ begin = drone["duration_s"]
+ drone["segments"].append({"start_s": round(begin, 3), "end_s": round(begin + duration, 3),
+ "start": start, "end": end, "phase": phase,
+ "cell_id": cell_id, "sortie_id": sortie_id})
+ drone["duration_s"] += duration
+
+ if len(drone["sortie_ids"]) > 1:
+ segment(home, home, battery_swap, "battery_swap")
+ segment(home, home, 60, "takeoff")
+ route = sortie["route"]
+ for index, (a, b) in enumerate(zip(route, route[1:])):
+ distance = math.dist(to_local.transform(a["longitude"], a["latitude"]), to_local.transform(b["longitude"], b["latitude"]))
+ survey = index % 2 == 1
+ segment(a, b, max(.001, distance / data["cruise_speed_m_s"]), "survey" if survey else "transit")
+ if survey:
+ segment(b, b, 2 + number(options, "turn_seconds", 3, 0, 30), "turn")
+ segment(home, home, 60, "landing")
+ sortie["scheduled_start_s"] = round(start_time, 3)
+ sortie["scheduled_end_s"] = round(drone["duration_s"], 3)
+ cells.append({"cell_id": cell_id, "row": row + 1, "column": col + 1, "side_m": cell_side,
+ "boundary": boundary, "center": geo(west + cell_side / 2, south + cell_side / 2),
+ "drone_id": drone["drone_id"], "sortie_ids": cell_sorties,
+ "coverage_percent": data["coverage_percent"], "line_count": len(data["survey_lines"])})
+ components.append(data)
+ if len(sorties) > MAX_SORTIES or len(lines) > MAX_LINES:
+ raise ValueError("Search exceeds 100 sorties or 10000 lines; reduce the search extent")
+ for drone in drones:
+ drone["duration_s"] = round(drone["duration_s"], 3)
+ result = dict(components[0])
+ result.update(center=center, home=home, home_source=home_source, boundary=corners(-half_side, -half_side, half_side, half_side),
+ cells=sorted(cells, key=lambda cell: cell["cell_id"]), cell_count=len(cells),
+ max_cell_side_m=maximum, cell_side_m=cell_side, rows=count, columns=count,
+ area_m2=round((2 * half_side) ** 2, 1), sorties=sorties, survey_lines=lines,
+ coverage_percent=round(sum(item["coverage_percent"] for item in components) / len(components), 2),
+ swarm={"simulation_only": True, "drone_count": len(drones), "drones": drones,
+ "duration_s": max(drone["duration_s"] for drone in drones),
+ "warning": "Visual swarm simulation only. Shared home and transit paths are not collision-deconflicted; real multi-drone launch is disabled."})
+ result["estimate"] = {**components[0]["estimate"], "survey_line_count": len(lines), "recommended_sorties": len(sorties),
+ "total_distance_m": round(sum(s["distance_m"] for s in sorties), 1),
+ "estimated_minutes": round(sum(s["estimated_minutes"] for s in sorties), 1),
+ "estimated_photos": sum(s["estimated_photos"] for s in sorties),
+ "simulation_ready": all(s["ready"] for s in sorties),
+ "blocking_reasons": list(dict.fromkeys(reason for s in sorties for reason in s["blocking_reasons"]))}
+ return AutoGridPlan(result)
+
+
+def number(data, key, default, minimum, maximum):
+ value = data.get(key, default)
+ if isinstance(value, bool):
+ raise ValueError(f"{key} must be a number")
+ return _bounded_number(key, value, minimum, maximum)
+
+
+def choice(data, key, default, options):
+ value = data.get(key, default)
+ if value not in options:
+ raise ValueError(f"{key} must be one of {', '.join(options)}")
+ return value
+
+
+def position(value, name):
+ if not isinstance(value, dict):
+ raise ValueError(f"{name} must contain latitude and longitude")
+ return {
+ "latitude": number(value, "latitude", None, -85, 85),
+ "longitude": number(value, "longitude", None, -180, 180),
+ }
+
+
+@dataclass
+class AutoGridPlan:
+ data: Dict[str, Any]
+
+ @property
+ def altitude_m(self):
+ return self.data["altitude_m"]
+
+ @property
+ def home_latitude(self):
+ return self.data["home"]["latitude"]
+
+ @property
+ def home_longitude(self):
+ return self.data["home"]["longitude"]
+
+ @property
+ def center_latitude(self):
+ return self.data["center"]["latitude"]
+
+ @property
+ def center_longitude(self):
+ return self.data["center"]["longitude"]
+
+ @property
+ def cells(self):
+ return ()
+
+ def to_dict(self):
+ return self.data
+
+
+def build_auto_grid_mission(payload, telemetry):
+ options = payload.get("survey_options")
+ if not isinstance(options, dict):
+ raise ValueError("survey_options must be an object")
+ raw_boundary = payload.get("boundary")
+ if not isinstance(raw_boundary, list) or not 3 <= len(raw_boundary) <= 64:
+ raise ValueError("Draw a closed boundary with 3 to 64 vertices")
+ boundary = [position(point, "boundary vertex") for point in raw_boundary]
+ if boundary[0] == boundary[-1]:
+ boundary.pop()
+ if len({(p['latitude'], p['longitude']) for p in boundary}) != len(boundary):
+ raise ValueError("Boundary contains duplicate vertices; remove repeated points")
+ if len(boundary) < 3:
+ raise ValueError("Boundary needs at least 3 distinct vertices")
+ lat = sum(p["latitude"] for p in boundary) / len(boundary)
+ lon = sum(p["longitude"] for p in boundary) / len(boundary)
+ local_crs = CRS.from_proj4(f"+proj=aeqd +lat_0={lat} +lon_0={lon} +datum=WGS84 +units=m")
+ to_local = Transformer.from_crs("EPSG:4326", local_crs, always_xy=True)
+ to_geo = Transformer.from_crs(local_crs, "EPSG:4326", always_xy=True)
+ polygon = Polygon([to_local.transform(p["longitude"], p["latitude"]) for p in boundary])
+ if not polygon.is_valid:
+ raise ValueError(f"Invalid boundary: {explain_validity(polygon)}. Redraw without crossing edges")
+ if polygon.area < 4:
+ raise ValueError("Survey area must be at least 4 square metres")
+ x0, y0, x1, y1 = polygon.bounds
+ if max(x1 - x0, y1 - y0) > 20000:
+ raise ValueError("Survey boundary must fit within a 20 km extent; divide the area")
+
+ def geo(point):
+ longitude, latitude = to_geo.transform(*point)
+ return {"latitude": round(latitude, 7), "longitude": round(longitude, 7)}
+
+ home_source = "manual"
+ if payload.get("home") is not None:
+ home = position(payload["home"], "home")
+ elif telemetry.get("connected") and telemetry.get("latitude") is not None and telemetry.get("longitude") is not None:
+ home = position(telemetry, "telemetry position")
+ home_source = "vehicle_position"
+ else:
+ home = geo(polygon.representative_point().coords[0])
+ home_source = "area_preview_only"
+ home_xy = to_local.transform(home["longitude"], home["latitude"])
+ if math.hypot(*home_xy) > 30000:
+ raise ValueError("Home is more than 30 km from the area; set a nearby launch position")
+
+ altitude = number(payload, "altitude_m", 5, 5, 120)
+ speed = number(payload, "cruise_speed_m_s", 2, 0.5, 15)
+ side = number(payload, "side_overlap_percent", 70, 0, 95)
+ forward = number(payload, "forward_overlap_percent", 75, 0, 95)
+ model = choice(options, "camera_model", "fov", ("fov", "sensor"))
+ if model == "sensor":
+ sensor_width = number(options, "sensor_width_mm", 6.17, 1, 60)
+ sensor_height = number(options, "sensor_height_mm", 4.55, 1, 60)
+ focal = number(options, "focal_length_mm", 4.5, 1, 300)
+ hfov = math.degrees(2 * math.atan(sensor_width / (2 * focal)))
+ vfov = math.degrees(2 * math.atan(sensor_height / (2 * focal)))
+ else:
+ hfov = number(payload, "camera_horizontal_fov_deg", 78, 10, 150)
+ vfov = number(payload, "camera_vertical_fov_deg", 60, 10, 150)
+ width = 2 * altitude * math.tan(math.radians(hfov / 2))
+ height = 2 * altitude * math.tan(math.radians(vfov / 2))
+ image_width = number(options, "image_width_px", 4000, 100, 30000)
+ image_height = number(options, "image_height_px", 3000, 100, 30000)
+ orientation = choice(options, "camera_orientation", "landscape", ("landscape", "portrait"))
+ across, along = (width, height) if orientation == "landscape" else (height, width)
+ automatic_spacing = across * (1 - side / 100)
+ trigger = round(along * (1 - forward / 100), 6)
+ spacing_mode = choice(options, "spacing_mode", "camera", ("camera", "manual"))
+ spacing = automatic_spacing if spacing_mode == "camera" else number(options, "line_spacing_m", 2, 0.25, 1000)
+ if min(spacing, trigger) < 0.25:
+ raise ValueError("Spacing below 0.25 m is unsupported; adjust altitude or overlap")
+ min_interval = number(options, "minimum_trigger_interval_s", 0.5, 0.1, 60)
+ overshoot = number(options, "overshoot_m", 0, 0, 50)
+ endurance = number(options, "endurance_minutes", 20, 3, 120)
+ reserve = number(options, "reserve_percent", 20, 10, 50)
+ budget = endurance * (1 - reserve / 100)
+ turn_seconds = 2 + number(options, "turn_seconds", 3, 0, 30)
+ angle_mode = choice(options, "angle_mode", "auto", ("auto", "manual"))
+ requested_angle = number(options, "grid_angle_deg", 90, 0, 179.99)
+ entry = choice(options, "entry", "nearest", ("nearest", "reverse"))
+ pattern = choice(options, "pattern", "single", ("single", "crosshatch"))
+
+ def make_tracks(heading):
+ # Rotated x is along flight; y is across flight. Bearing is clockwise from north.
+ rotation = heading - 90
+ rotated = affinity.rotate(polygon, rotation, origin=(0, 0))
+ left, bottom, right, top = rotated.bounds
+ count = max(1, math.ceil((top - bottom) / spacing))
+ if count > MAX_LINES:
+ raise ValueError("Grid exceeds 10000 lines; reduce the area or increase spacing")
+ actual = (top - bottom) / count
+ tracks = []
+ for row in range(count):
+ y = bottom + (row + 0.5) * actual
+ cut = rotated.intersection(LineString([(left - 1, y), (right + 1, y)]))
+ pieces = [cut] if cut.geom_type == "LineString" else list(getattr(cut, "geoms", []))
+ pieces = sorted((p for p in pieces if p.geom_type == "LineString" and p.length > 0.01), key=lambda p: p.bounds[0])
+ if row % 2:
+ pieces.reverse()
+ for piece in pieces:
+ start, end = (piece.bounds[0] - overshoot, y), (piece.bounds[2] + overshoot, y)
+ if row % 2:
+ start, end = end, start
+ line = affinity.rotate(LineString([start, end]), -rotation, origin=(0, 0))
+ tracks.append((tuple(line.coords[0]), tuple(line.coords[-1]), heading))
+ if len(tracks) > MAX_LINES:
+ raise ValueError("Grid exceeds 10000 line segments; divide the boundary")
+ return tracks, actual
+
+ def duration(tracks):
+ distance = 0.0
+ previous = home_xy
+ for start, end, _ in tracks:
+ distance += math.dist(previous, start) + math.dist(start, end)
+ previous = end
+ distance += math.dist(previous, home_xy)
+ return distance, (distance / speed + 120 + turn_seconds * len(tracks)) / 60
+
+ if angle_mode == "auto":
+ # Bounded candidate search, not a claim of a globally optimal route.
+ candidates = list(range(0, 180, 15))
+ edge = max(zip(polygon.exterior.coords, list(polygon.exterior.coords)[1:]), key=lambda p: math.dist(*p))
+ candidates.append(math.degrees(math.atan2(edge[1][0] - edge[0][0], edge[1][1] - edge[0][1])) % 180)
+ choices = []
+ for candidate in candidates:
+ try:
+ tracks, _ = make_tracks(candidate)
+ if tracks:
+ choices.append((duration(tracks)[1], candidate))
+ except ValueError:
+ continue
+ if not choices:
+ raise ValueError("No grid within the 10000 line limit; divide the survey area")
+ angle = min(choices)[1]
+ else:
+ angle = requested_angle
+ tracks, actual_spacing = make_tracks(angle)
+ if pattern == "crosshatch":
+ second, _ = make_tracks((angle + 90) % 180)
+ tracks += second
+ if not tracks or len(tracks) > MAX_LINES:
+ raise ValueError("Grid must contain between 1 and 10000 line segments")
+ if math.dist(home_xy, tracks[-1][1]) < math.dist(home_xy, tracks[0][0]):
+ tracks = [(end, start, heading) for start, end, heading in reversed(tracks)]
+ if entry == "reverse":
+ tracks = [(end, start, heading) for start, end, heading in reversed(tracks)]
+
+ footprints = []
+ for start, end, heading in tracks:
+ rotation = heading - 90
+ line = affinity.rotate(LineString([start, end]), rotation, origin=(0, 0))
+ left, y, right, _ = line.bounds
+ # Continuous footprint union, conservative for discrete triggering when
+ # the final photograph is up to one trigger distance short of the end.
+ if line.coords[0][0] <= line.coords[-1][0]:
+ right = left + math.floor((right - left) / trigger) * trigger
+ else:
+ left = right - math.floor((right - left) / trigger) * trigger
+ footprints.append(affinity.rotate(box(left - along / 2, y - across / 2, right + along / 2, y + across / 2), -rotation, origin=(0, 0)))
+ covered = unary_union(footprints)
+ coverage = min(100, covered.intersection(polygon).area / polygon.area * 100)
+ blockers = []
+ warnings = ["Flat-surface, nadir-camera model; terrain samples do not change mission altitudes.",
+ "Transit legs are straight connections, not obstacle or geofence avoidance.",
+ "ArduCopter setup: validate face-next-waypoint yaw, waypoint radius smaller than the line gap, and the camera trigger interface in SITL. Waypoint yaw/radius fields do not configure these settings."]
+ if home_source == "area_preview_only":
+ blockers.append("Set a launch/home position before export or simulation")
+ if coverage < 99.5:
+ blockers.append(f"Predicted footprint covers {coverage:.2f}% of the boundary; increase overlap or change grid angle")
+ if spacing > automatic_spacing + 0.001:
+ blockers.append("Manual line spacing exceeds the requested side-overlap spacing")
+ if trigger / speed < min_interval - 1e-9:
+ blockers.append(f"Camera cannot keep up: reduce speed to {trigger / min_interval:.2f} m/s or change forward overlap")
+ if altitude <= 15:
+ warnings.append("Low-altitude water clearance requires a validated sensor and autopilot setup; this planner does not configure surface following.")
+
+ groups = []
+ group = []
+ group_distance = 0.0
+ for track in tracks:
+ start, end, _ = track
+ previous = group[-1][1] if group else home_xy
+ candidate_distance = group_distance + math.dist(previous, start) + math.dist(start, end)
+ candidate_minutes = ((candidate_distance + math.dist(end, home_xy)) / speed + 120 + turn_seconds * (len(group) + 1)) / 60
+ if group and (5 + 4 * (len(group) + 1) > MAX_ITEMS or candidate_minutes > budget):
+ groups.append(group)
+ group = []
+ candidate_distance = math.dist(home_xy, start) + math.dist(start, end)
+ group.append(track)
+ group_distance = candidate_distance
+ if group:
+ groups.append(group)
+ if len(groups) > MAX_SORTIES:
+ raise ValueError("Survey needs more than 100 sorties; divide it into smaller areas")
+
+ sorties, survey_lines = [], []
+ # A centimetre tolerance avoids classifying coincident boundary transfers
+ # as outside because of floating-point intersection coordinates.
+ boundary_tolerance = polygon.buffer(0.01)
+ for group_index, group in enumerate(groups):
+ distance, minutes = duration(group)
+ items = [_navigation_item(0, 16, home["latitude"], home["longitude"], 0, current=1),
+ _navigation_item(1, 22, home["latitude"], home["longitude"], altitude)]
+ speed_item = _camera_trigger_item(2, 0)
+ speed_item.update(command=178, param1=1, param2=speed, param3=-1)
+ items.append(speed_item)
+ photo_count = 0
+ path = [home]
+ outside = 0.0
+ previous = home_xy
+ for start, end, heading in group:
+ start_geo, end_geo = geo(start), geo(end)
+ length = math.dist(start, end)
+ for point, before in ((start_geo, True), (end_geo, False)):
+ item = _navigation_item(len(items), 16, point["latitude"], point["longitude"], altitude)
+ # Copter uses its configured yaw behaviour and waypoint radius;
+ # it does not implement per-waypoint yaw/acceptance parameters.
+ item.update(param1=1.0, param2=0.0, param4=0.0)
+ items.append(item)
+ trigger_item = _camera_trigger_item(len(items), trigger if before else 0)
+ trigger_item["param1"] = trigger if before else 0
+ items.append(trigger_item)
+ photo_count += math.floor(length / trigger) + 1
+ outside += LineString([previous, start]).difference(boundary_tolerance).length
+ outside += LineString([start, end]).difference(boundary_tolerance).length
+ path.extend([start_geo, end_geo])
+ survey_lines.append({"cell_id": 1, "line_index": len(survey_lines) + 1,
+ "sortie_id": group_index + 1, "start_latitude": start_geo["latitude"],
+ "start_longitude": start_geo["longitude"], "end_latitude": end_geo["latitude"],
+ "end_longitude": end_geo["longitude"], "length_m": round(length, 2)})
+ previous = end
+ outside += LineString([previous, home_xy]).difference(boundary_tolerance).length
+ path.append(home)
+ items.append(_navigation_item(len(items), 16, home["latitude"], home["longitude"], altitude))
+ items.append(_navigation_item(len(items), 21, home["latitude"], home["longitude"], 0))
+ local_blockers = list(blockers)
+ if minutes > budget:
+ local_blockers.append("A single line plus home transit exceeds the sortie budget; reduce the area or move home")
+ sorties.append({"sortie_id": group_index + 1, "line_count": len(group), "route": path,
+ "distance_m": round(distance, 1), "estimated_minutes": round(minutes, 1),
+ "estimated_photos": photo_count, "outside_boundary_m": round(outside, 1),
+ "mission_item_count": len(items), "ready": not local_blockers,
+ "blocking_reasons": local_blockers, "mission_items": items if not local_blockers else []})
+
+ camera = {"horizontal_fov_deg": round(hfov, 2), "vertical_fov_deg": round(vfov, 2),
+ "footprint_width_m": round(width, 3), "footprint_height_m": round(height, 3),
+ "cross_track_m": round(across, 3), "along_track_m": round(along, 3),
+ "line_spacing_m": round(actual_spacing, 3), "requested_line_spacing_m": round(spacing, 3),
+ "trigger_distance_m": round(trigger, 3), "trigger_interval_s": round(trigger / speed, 3),
+ "effective_side_overlap_percent": round(100 * (1 - actual_spacing / across), 2),
+ "gsd_width_cm": round(width / image_width * 100, 3),
+ "gsd_height_cm": round(height / image_height * 100, 3)}
+ return AutoGridPlan({"schema_version": 2, "center": geo(polygon.centroid.coords[0]), "home": home,
+ "home_source": home_source, "boundary": boundary, "altitude_m": altitude,
+ "altitude_reference": "relative_home", "camera": camera,
+ "cruise_speed_m_s": speed, "survey_lines": survey_lines, "sorties": sorties,
+ "grid_angle_deg": round(angle, 2), "pattern": pattern, "area_m2": round(polygon.area, 1),
+ "coverage_percent": round(coverage, 2), "sortie_budget_minutes": round(budget, 2),
+ "estimate": {"survey_line_count": len(tracks), "recommended_sorties": len(sorties),
+ "total_distance_m": round(sum(s["distance_m"] for s in sorties), 1),
+ "estimated_minutes": round(sum(s["estimated_minutes"] for s in sorties), 1),
+ "estimated_photos": sum(s["estimated_photos"] for s in sorties),
+ "simulation_ready": all(s["ready"] for s in sorties),
+ "blocking_reasons": list(dict.fromkeys(r for s in sorties for r in s["blocking_reasons"])),
+ "warnings": warnings}})
diff --git a/autonomous-grid-search/backend/companion_api.py b/autonomous-grid-search/backend/companion_api.py
new file mode 100644
index 0000000..946035c
--- /dev/null
+++ b/autonomous-grid-search/backend/companion_api.py
@@ -0,0 +1,262 @@
+#!/usr/bin/env python3
+"""HTTP API for mission preview and simulation-only execution."""
+
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+import threading
+import time
+import uuid
+from typing import Any, Dict, Optional
+from urllib.parse import urlparse
+
+from search_mission_planner import build_search_mission
+from terrain_provider import sample_plan_terrain
+
+
+MAX_REQUEST_BYTES = 64 * 1024
+
+
+class CompanionAPIServer:
+ def __init__(self, mav_manager, host: str = "0.0.0.0", port: int = 8765):
+ self.mav_manager = mav_manager
+ self.host = host
+ self.port = port
+ self.server: Optional[ThreadingHTTPServer] = None
+ self.thread: Optional[threading.Thread] = None
+
+ def start(self) -> None:
+ if self.server:
+ return
+ handler = _handler_for(self.mav_manager)
+ self.server = ThreadingHTTPServer((self.host, self.port), handler)
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
+ self.thread.start()
+ bound_port = self.server.server_address[1]
+ print(f"[OK] Companion API: http://{self.host}:{bound_port} (live execution locked)")
+
+ def stop(self) -> None:
+ if not self.server:
+ return
+ self.server.shutdown()
+ self.server.server_close()
+ if self.thread and self.thread.is_alive():
+ self.thread.join(timeout=1.0)
+ self.server = None
+ self.thread = None
+
+
+def _handler_for(mav_manager):
+ mission_lock = threading.Lock()
+ plan_lock = threading.Lock()
+ plans = {}
+
+ class CompanionRequestHandler(BaseHTTPRequestHandler):
+ def do_OPTIONS(self) -> None:
+ self.send_response(204)
+ self._cors_headers()
+ self.end_headers()
+
+ def do_GET(self) -> None:
+ path = urlparse(self.path).path
+ if path == "/api/health":
+ telemetry = mav_manager.get_telemetry_snapshot()
+ self._send_json(200, {
+ "status": "ok",
+ "simulation": bool(mav_manager.simulate),
+ "connected": bool(telemetry.get("connected", False)),
+ "live_execution_enabled": False,
+ "armed": bool(telemetry.get("armed", False)),
+ "mission_state": telemetry.get("mission_state", "idle"),
+ })
+ return
+ self._send_json(404, {"error": "endpoint not found"})
+
+ def do_POST(self) -> None:
+ path = urlparse(self.path).path
+ if path not in {"/api/mission/plan", "/api/mission/simulate"}:
+ self._send_json(404, {"error": "endpoint not found"})
+ return
+ try:
+ payload = self._read_json()
+ if path.endswith("simulate") and not mav_manager.simulate:
+ self._send_json(403, {"error": "live Pixhawk execution is locked"})
+ return
+ if "plan_id" in payload:
+ self._start_reviewed_plan(payload)
+ return
+ plan = _plan_from_payload(payload, mav_manager.get_telemetry_snapshot())
+ settings = plan.to_dict() if "search_area" in payload else payload
+ terrain = (
+ sample_plan_terrain(plan, str(settings.get("surface_type", "water")))
+ if settings.get("terrain_enabled", False)
+ else {"status": "disabled", "used_for_clearance": False}
+ )
+ except (ValueError, UnicodeError, json.JSONDecodeError) as exc:
+ self._send_json(400, {"error": str(exc)})
+ return
+ except ImportError:
+ self._send_json(503, {"error": "Install the Pi requirements.txt to enable polygon surveys"})
+ return
+
+ if path == "/api/mission/plan":
+ plan_payload = plan.to_dict()
+ plan_payload["terrain"] = terrain
+ if plan_payload.get("schema_version") == 2:
+ plan_payload["plan_id"] = uuid.uuid4().hex
+ with plan_lock:
+ expired = [key for key, (created, _) in plans.items() if time.monotonic() - created > 600]
+ for key in expired:
+ del plans[key]
+ while len(plans) >= 16:
+ del plans[next(iter(plans))]
+ plans[plan_payload["plan_id"]] = (time.monotonic(), plan_payload)
+ self._send_json(200, {"status": "planned", "plan": plan_payload})
+ return
+
+ if "survey_options" in payload or "search_area" in payload:
+ self._send_json(400, {"error": "Generate and review a plan, then submit its plan_id and sortie_id"})
+ return
+
+ if not mav_manager.simulate:
+ self._send_json(403, {
+ "error": "live Pixhawk execution is locked; validate this mission in simulation first"
+ })
+ return
+
+ if not plan.estimate.simulation_ready:
+ self._send_json(422, {
+ "error": "mission exceeds single-sortie limits",
+ "reasons": list(plan.estimate.blocking_reasons),
+ })
+ return
+
+ if not mission_lock.acquire(blocking=False):
+ self._send_json(409, {"error": "another mission operation is in progress"})
+ return
+ try:
+ if mav_manager.get_telemetry_snapshot().get("armed"):
+ self._send_json(409, {"error": "Simulator is armed; finish the current flight first"})
+ return
+ if not mav_manager.clear_all_missions():
+ self._send_json(500, {"error": "could not clear the simulated mission"})
+ return
+ if not mav_manager.upload_mission_items(list(plan.mission_items)):
+ self._send_json(500, {"error": "could not load the simulated mission"})
+ return
+ if not mav_manager.set_arm(True) or not mav_manager.set_flight_mode("AUTO"):
+ self._send_json(500, {"error": "could not start the simulated mission"})
+ return
+ plan_payload = plan.to_dict()
+ plan_payload["terrain"] = terrain
+ self._send_json(202, {"status": "simulation_started", "plan": plan_payload})
+ finally:
+ mission_lock.release()
+
+ def _start_reviewed_plan(self, payload):
+ if urlparse(self.path).path != "/api/mission/simulate":
+ raise ValueError("plan_id is only accepted for simulation")
+ if not isinstance(payload["plan_id"], str):
+ raise ValueError("plan_id must be a string")
+ with plan_lock:
+ cached = plans.get(payload["plan_id"])
+ if not cached or time.monotonic() - cached[0] > 600:
+ self._send_json(409, {"error": "Plan expired or API restarted; generate and review it again"})
+ return
+ plan = cached[1]
+ sortie = next((s for s in plan["sorties"] if s["sortie_id"] == payload.get("sortie_id")), None)
+ if not sortie:
+ raise ValueError("Select a valid sortie_id")
+ if not sortie["ready"]:
+ self._send_json(422, {"error": "Resolve the survey checks", "reasons": sortie["blocking_reasons"]})
+ return
+ if payload.get("review_confirmed") is not True:
+ raise ValueError("Confirm the selected route and relative-home altitude before simulation")
+ if not mission_lock.acquire(blocking=False):
+ self._send_json(409, {"error": "Another mission operation is in progress"})
+ return
+ try:
+ if mav_manager.get_telemetry_snapshot().get("armed"):
+ self._send_json(409, {"error": "Simulator is armed; finish the current flight first"})
+ return
+ if not mav_manager.clear_all_missions() or not mav_manager.upload_mission_items(sortie["mission_items"]):
+ self._send_json(500, {"error": "Could not load the simulated mission"})
+ return
+ if not mav_manager.set_arm(True) or not mav_manager.set_flight_mode("AUTO"):
+ self._send_json(500, {"error": "Could not start the simulated mission"})
+ return
+ self._send_json(202, {"status": "simulation_started", "plan": plan, "sortie_id": sortie["sortie_id"]})
+ finally:
+ mission_lock.release()
+
+ def _read_json(self) -> Dict[str, Any]:
+ try:
+ content_length = int(self.headers.get("Content-Length", "0"))
+ except ValueError as exc:
+ raise ValueError("invalid Content-Length") from exc
+ if content_length <= 0 or content_length > MAX_REQUEST_BYTES:
+ raise ValueError("request body must be between 1 byte and 64 KiB")
+ body = self.rfile.read(content_length)
+ payload = json.loads(body.decode("utf-8"))
+ if not isinstance(payload, dict):
+ raise ValueError("request body must be a JSON object")
+ return payload
+
+ def _send_json(self, status: int, payload: Dict[str, Any]) -> None:
+ body = json.dumps(payload, allow_nan=False).encode("utf-8")
+ self.send_response(status)
+ self._cors_headers()
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def _cors_headers(self) -> None:
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+
+ def log_message(self, format_string: str, *args) -> None:
+ print(f"[companion-api] {self.address_string()} {format_string % args}")
+
+ return CompanionRequestHandler
+
+
+def _plan_from_payload(payload: Dict[str, Any], telemetry: Dict[str, Any]):
+ if "search_area" in payload:
+ from auto_grid_planner import build_radius_survey
+ return build_radius_survey(payload, telemetry)
+ if "survey_options" in payload:
+ from auto_grid_planner import build_auto_grid_mission
+ return build_auto_grid_mission(payload, telemetry)
+ center = payload.get("center")
+ if not isinstance(center, dict):
+ raise ValueError("center must contain latitude and longitude")
+
+ home_latitude = telemetry.get("latitude")
+ home_longitude = telemetry.get("longitude")
+ if not _valid_position(home_latitude, home_longitude):
+ home_latitude = center.get("latitude")
+ home_longitude = center.get("longitude")
+
+ return build_search_mission(
+ center_latitude=center.get("latitude"),
+ center_longitude=center.get("longitude"),
+ radius_km=payload.get("radius_km"),
+ altitude_m=payload.get("altitude_m"),
+ home_latitude=home_latitude,
+ home_longitude=home_longitude,
+ camera_horizontal_fov_deg=payload.get("camera_horizontal_fov_deg", 78.0),
+ camera_vertical_fov_deg=payload.get("camera_vertical_fov_deg", 60.0),
+ side_overlap_percent=payload.get("side_overlap_percent", 70.0),
+ forward_overlap_percent=payload.get("forward_overlap_percent", 75.0),
+ cruise_speed_m_s=payload.get("cruise_speed_m_s", 3.0),
+ )
+
+
+def _valid_position(latitude: Any, longitude: Any) -> bool:
+ try:
+ return -90 <= float(latitude) <= 90 and -180 <= float(longitude) <= 180 and not (
+ float(latitude) == 0 and float(longitude) == 0
+ )
+ except (TypeError, ValueError):
+ return False
diff --git a/autonomous-grid-search/backend/requirements.txt b/autonomous-grid-search/backend/requirements.txt
new file mode 100644
index 0000000..92c7178
--- /dev/null
+++ b/autonomous-grid-search/backend/requirements.txt
@@ -0,0 +1,3 @@
+shapely>=2.0,<3
+pyproj>=3.6,<4
+pymavlink>=2.4,<3
diff --git a/autonomous-grid-search/backend/search_mission_planner.py b/autonomous-grid-search/backend/search_mission_planner.py
new file mode 100644
index 0000000..66af839
--- /dev/null
+++ b/autonomous-grid-search/backend/search_mission_planner.py
@@ -0,0 +1,399 @@
+#!/usr/bin/env python3
+"""Generate camera-aware square-grid search missions for simulation review."""
+
+from dataclasses import asdict, dataclass
+import math
+from typing import Any, Dict, List, Optional, Tuple
+
+
+METRES_PER_LATITUDE_DEGREE = 111_320.0
+MAV_CMD_NAV_WAYPOINT = 16
+MAV_CMD_NAV_LAND = 21
+MAV_CMD_NAV_TAKEOFF = 22
+MAV_CMD_DO_SET_CAM_TRIGG_DIST = 206
+MAX_MISSION_ITEMS = 500
+MAX_SORTIE_MINUTES = 20.0
+MAX_SURVEY_LINES = 10_000
+
+
+@dataclass(frozen=True)
+class SearchCell:
+ cell_id: int
+ row: int
+ column: int
+ north_m: float
+ east_m: float
+ south_m: float
+ west_m: float
+ center_latitude: float
+ center_longitude: float
+
+
+@dataclass(frozen=True)
+class SurveyLine:
+ cell_id: int
+ line_index: int
+ start_latitude: float
+ start_longitude: float
+ end_latitude: float
+ end_longitude: float
+ length_m: float
+
+
+@dataclass(frozen=True)
+class CameraCoverage:
+ horizontal_fov_deg: float
+ vertical_fov_deg: float
+ side_overlap_percent: float
+ forward_overlap_percent: float
+ footprint_width_m: float
+ footprint_height_m: float
+ line_spacing_m: float
+ trigger_distance_m: float
+
+
+@dataclass(frozen=True)
+class MissionEstimate:
+ survey_line_count: int
+ survey_distance_m: float
+ total_distance_m: float
+ estimated_minutes: float
+ estimated_photos: int
+ mission_item_count: int
+ recommended_sorties: int
+ simulation_ready: bool
+ blocking_reasons: Tuple[str, ...]
+ warnings: Tuple[str, ...]
+
+
+@dataclass(frozen=True)
+class SearchMissionPlan:
+ center_latitude: float
+ center_longitude: float
+ home_latitude: float
+ home_longitude: float
+ radius_km: float
+ altitude_m: float
+ cruise_speed_m_s: float
+ cell_count: int
+ rows: int
+ columns: int
+ cells: Tuple[SearchCell, ...]
+ route_cell_ids: Tuple[int, ...]
+ survey_lines: Tuple[SurveyLine, ...]
+ camera: CameraCoverage
+ estimate: MissionEstimate
+ mission_items: Tuple[Dict[str, Any], ...]
+
+ def to_dict(self) -> Dict[str, Any]:
+ browser_mission_items = list(self.mission_items) if self.estimate.simulation_ready else []
+ return {
+ "center": {"latitude": self.center_latitude, "longitude": self.center_longitude},
+ "home": {"latitude": self.home_latitude, "longitude": self.home_longitude},
+ "radius_km": self.radius_km,
+ "altitude_m": self.altitude_m,
+ "cruise_speed_m_s": self.cruise_speed_m_s,
+ "cell_count": self.cell_count,
+ "rows": self.rows,
+ "columns": self.columns,
+ "cells": [asdict(cell) for cell in self.cells],
+ "route_cell_ids": list(self.route_cell_ids),
+ "survey_lines": [asdict(line) for line in self.survey_lines],
+ "camera": asdict(self.camera),
+ "estimate": asdict(self.estimate),
+ "surface_tracking": {
+ "required": self.altitude_m <= 15.0,
+ "source": "downward_rangefinder",
+ "terrain_database_is_clearance_source": False,
+ },
+ "phases": ["takeoff", "camera_grid_survey", "return_home", "land"],
+ "mission_items": browser_mission_items,
+ }
+
+
+def build_search_mission(
+ center_latitude: float,
+ center_longitude: float,
+ radius_km: float,
+ altitude_m: float,
+ home_latitude: Optional[float] = None,
+ home_longitude: Optional[float] = None,
+ camera_horizontal_fov_deg: float = 78.0,
+ camera_vertical_fov_deg: float = 60.0,
+ side_overlap_percent: float = 70.0,
+ forward_overlap_percent: float = 75.0,
+ cruise_speed_m_s: float = 3.0,
+) -> SearchMissionPlan:
+ """Build a camera survey with parallel flight lines inside every cell."""
+ center_latitude = _bounded_number("center_latitude", center_latitude, -85.0, 85.0)
+ center_longitude = _bounded_number("center_longitude", center_longitude, -180.0, 180.0)
+ radius_km = _bounded_number("radius_km", radius_km, 0.01, 10.0)
+ altitude_m = _bounded_number("altitude_m", altitude_m, 5.0, 120.0)
+ home_latitude = center_latitude if home_latitude is None else _bounded_number(
+ "home_latitude", home_latitude, -85.0, 85.0
+ )
+ home_longitude = center_longitude if home_longitude is None else _bounded_number(
+ "home_longitude", home_longitude, -180.0, 180.0
+ )
+ horizontal_fov = _bounded_number("camera_horizontal_fov_deg", camera_horizontal_fov_deg, 10.0, 170.0)
+ vertical_fov = _bounded_number("camera_vertical_fov_deg", camera_vertical_fov_deg, 10.0, 170.0)
+ side_overlap = _bounded_number("side_overlap_percent", side_overlap_percent, 0.0, 95.0)
+ forward_overlap = _bounded_number("forward_overlap_percent", forward_overlap_percent, 0.0, 95.0)
+ cruise_speed = _bounded_number("cruise_speed_m_s", cruise_speed_m_s, 0.5, 15.0)
+
+ footprint_width = 2.0 * altitude_m * math.tan(math.radians(horizontal_fov / 2.0))
+ footprint_height = 2.0 * altitude_m * math.tan(math.radians(vertical_fov / 2.0))
+ line_spacing = footprint_height * (1.0 - side_overlap / 100.0)
+ trigger_distance = footprint_width * (1.0 - forward_overlap / 100.0)
+ if line_spacing < 0.25 or trigger_distance < 0.25:
+ raise ValueError("camera overlap creates spacing below the 0.25 m planning limit")
+
+ camera = CameraCoverage(
+ horizontal_fov_deg=horizontal_fov,
+ vertical_fov_deg=vertical_fov,
+ side_overlap_percent=side_overlap,
+ forward_overlap_percent=forward_overlap,
+ footprint_width_m=round(footprint_width, 2),
+ footprint_height_m=round(footprint_height, 2),
+ line_spacing_m=round(line_spacing, 2),
+ trigger_distance_m=round(trigger_distance, 2),
+ )
+
+ cell_count = max(1, int(math.ceil(radius_km * 4.0)))
+ rows, columns = _closest_factors(cell_count)
+ radius_m = radius_km * 1000.0
+ cell_height_m = (radius_m * 2.0) / rows
+ cell_width_m = (radius_m * 2.0) / columns
+ cell_line_count = max(1, int(math.ceil(cell_height_m / line_spacing)))
+ predicted_line_count = cell_count * cell_line_count
+ if predicted_line_count > MAX_SURVEY_LINES:
+ raise ValueError(
+ f"survey requires {predicted_line_count} flight lines; reduce the area or overlap, "
+ f"or increase altitude (planning limit {MAX_SURVEY_LINES} lines)"
+ )
+
+ cells: List[SearchCell] = []
+ for row in range(rows):
+ north_m = radius_m - row * cell_height_m
+ south_m = north_m - cell_height_m
+ for column in range(columns):
+ cell_id = row * columns + column + 1
+ east_m = -radius_m + (column + 1) * cell_width_m
+ west_m = east_m - cell_width_m
+ latitude, longitude = _offset_coordinate(
+ center_latitude,
+ center_longitude,
+ (north_m + south_m) / 2.0,
+ (east_m + west_m) / 2.0,
+ )
+ cells.append(SearchCell(
+ cell_id=cell_id,
+ row=row,
+ column=column,
+ north_m=round(north_m, 2),
+ east_m=round(east_m, 2),
+ south_m=round(south_m, 2),
+ west_m=round(west_m, 2),
+ center_latitude=round(latitude, 7),
+ center_longitude=round(longitude, 7),
+ ))
+
+ route_cells: List[SearchCell] = []
+ for row in range(rows):
+ row_cells = [cell for cell in cells if cell.row == row]
+ route_cells.extend(row_cells if row % 2 == 0 else reversed(row_cells))
+
+ survey_lines: List[SurveyLine] = []
+ route_offsets: List[Tuple[float, float]] = []
+ survey_distance_m = 0.0
+ for cell_order, cell in enumerate(route_cells):
+ actual_spacing = cell_height_m / cell_line_count
+ for line_index in range(cell_line_count):
+ north_m = cell.north_m - actual_spacing * (line_index + 0.5)
+ forward = (cell_order + line_index) % 2 == 0
+ start_east_m = cell.west_m if forward else cell.east_m
+ end_east_m = cell.east_m if forward else cell.west_m
+ start_latitude, start_longitude = _offset_coordinate(
+ center_latitude, center_longitude, north_m, start_east_m
+ )
+ end_latitude, end_longitude = _offset_coordinate(
+ center_latitude, center_longitude, north_m, end_east_m
+ )
+ survey_lines.append(SurveyLine(
+ cell_id=cell.cell_id,
+ line_index=line_index + 1,
+ start_latitude=round(start_latitude, 7),
+ start_longitude=round(start_longitude, 7),
+ end_latitude=round(end_latitude, 7),
+ end_longitude=round(end_longitude, 7),
+ length_m=round(cell_width_m, 2),
+ ))
+ route_offsets.extend([(north_m, start_east_m), (north_m, end_east_m)])
+ survey_distance_m += cell_width_m
+
+ home_offset = _coordinate_offset_metres(
+ center_latitude, center_longitude, home_latitude, home_longitude
+ )
+ total_distance_m = _polyline_distance([home_offset] + route_offsets + [home_offset])
+ mission_item_count = 6 + len(route_offsets)
+ estimated_minutes = (total_distance_m / cruise_speed + 120.0) / 60.0
+ estimated_photos = int(math.ceil(survey_distance_m / trigger_distance))
+ recommended_sorties = max(
+ 1,
+ int(math.ceil(mission_item_count / MAX_MISSION_ITEMS)),
+ int(math.ceil(estimated_minutes / MAX_SORTIE_MINUTES)),
+ )
+
+ blocking_reasons: List[str] = []
+ if mission_item_count > MAX_MISSION_ITEMS:
+ blocking_reasons.append(
+ f"mission has {mission_item_count} items; split it below {MAX_MISSION_ITEMS} items per sortie"
+ )
+ if estimated_minutes > MAX_SORTIE_MINUTES:
+ blocking_reasons.append(
+ f"estimated flight time is {estimated_minutes:.1f} minutes; split it below {MAX_SORTIE_MINUTES:.0f} minutes per sortie"
+ )
+ warnings: List[str] = []
+ if altitude_m <= 15.0:
+ warnings.append("low-altitude flight requires a healthy downward rangefinder; terrain elevation is not a clearance sensor")
+ if radius_km >= 1.0 and altitude_m <= 10.0:
+ warnings.append("this area is extremely large for low-altitude camera coverage")
+
+ estimate = MissionEstimate(
+ survey_line_count=len(survey_lines),
+ survey_distance_m=round(survey_distance_m, 1),
+ total_distance_m=round(total_distance_m, 1),
+ estimated_minutes=round(estimated_minutes, 1),
+ estimated_photos=estimated_photos,
+ mission_item_count=mission_item_count,
+ recommended_sorties=recommended_sorties,
+ simulation_ready=not blocking_reasons,
+ blocking_reasons=tuple(blocking_reasons),
+ warnings=tuple(warnings),
+ )
+
+ mission_items: List[Dict[str, Any]] = [
+ _navigation_item(0, MAV_CMD_NAV_WAYPOINT, home_latitude, home_longitude, 0.0, current=1),
+ _navigation_item(1, MAV_CMD_NAV_TAKEOFF, home_latitude, home_longitude, altitude_m),
+ _camera_trigger_item(2, trigger_distance),
+ ]
+ for line in survey_lines:
+ mission_items.append(_navigation_item(
+ len(mission_items), MAV_CMD_NAV_WAYPOINT,
+ line.start_latitude, line.start_longitude, altitude_m,
+ ))
+ mission_items.append(_navigation_item(
+ len(mission_items), MAV_CMD_NAV_WAYPOINT,
+ line.end_latitude, line.end_longitude, altitude_m,
+ ))
+ mission_items.append(_camera_trigger_item(len(mission_items), 0.0))
+ mission_items.append(_navigation_item(
+ len(mission_items), MAV_CMD_NAV_WAYPOINT,
+ home_latitude, home_longitude, altitude_m,
+ ))
+ mission_items.append(_navigation_item(
+ len(mission_items), MAV_CMD_NAV_LAND,
+ home_latitude, home_longitude, 0.0,
+ ))
+
+ return SearchMissionPlan(
+ center_latitude=center_latitude,
+ center_longitude=center_longitude,
+ home_latitude=home_latitude,
+ home_longitude=home_longitude,
+ radius_km=radius_km,
+ altitude_m=altitude_m,
+ cruise_speed_m_s=cruise_speed,
+ cell_count=cell_count,
+ rows=rows,
+ columns=columns,
+ cells=tuple(cells),
+ route_cell_ids=tuple(cell.cell_id for cell in route_cells),
+ survey_lines=tuple(survey_lines),
+ camera=camera,
+ estimate=estimate,
+ mission_items=tuple(mission_items),
+ )
+
+
+def _closest_factors(value: int) -> Tuple[int, int]:
+ rows = int(math.sqrt(value))
+ while rows > 1 and value % rows != 0:
+ rows -= 1
+ return rows, value // rows
+
+
+def _offset_coordinate(latitude: float, longitude: float, north_m: float, east_m: float) -> Tuple[float, float]:
+ latitude_offset = north_m / METRES_PER_LATITUDE_DEGREE
+ longitude_scale = METRES_PER_LATITUDE_DEGREE * max(0.01, math.cos(math.radians(latitude)))
+ return latitude + latitude_offset, longitude + east_m / longitude_scale
+
+
+def _coordinate_offset_metres(
+ origin_latitude: float,
+ origin_longitude: float,
+ latitude: float,
+ longitude: float,
+) -> Tuple[float, float]:
+ north_m = (latitude - origin_latitude) * METRES_PER_LATITUDE_DEGREE
+ longitude_scale = METRES_PER_LATITUDE_DEGREE * max(0.01, math.cos(math.radians(origin_latitude)))
+ east_m = (longitude - origin_longitude) * longitude_scale
+ return north_m, east_m
+
+
+def _polyline_distance(points: List[Tuple[float, float]]) -> float:
+ return sum(
+ math.hypot(current[0] - previous[0], current[1] - previous[1])
+ for previous, current in zip(points, points[1:])
+ )
+
+
+def _navigation_item(
+ seq: int,
+ command: int,
+ latitude: float,
+ longitude: float,
+ altitude_m: float,
+ current: int = 0,
+) -> Dict[str, Any]:
+ return {
+ "seq": seq,
+ "frame": 6,
+ "command": command,
+ "current": current,
+ "autocontinue": 1,
+ "param1": 0.0,
+ "param2": 2.0,
+ "param3": 0.0,
+ "param4": 0.0,
+ "x": int(round(latitude * 1e7)),
+ "y": int(round(longitude * 1e7)),
+ "z": altitude_m,
+ }
+
+
+def _camera_trigger_item(seq: int, trigger_distance_m: float) -> Dict[str, Any]:
+ return {
+ "seq": seq,
+ "frame": 2,
+ "command": MAV_CMD_DO_SET_CAM_TRIGG_DIST,
+ "current": 0,
+ "autocontinue": 1,
+ "param1": round(trigger_distance_m, 2),
+ "param2": 0.0,
+ "param3": 1.0 if trigger_distance_m > 0 else 0.0,
+ "param4": 0.0,
+ "x": 0,
+ "y": 0,
+ "z": 0.0,
+ }
+
+
+def _bounded_number(name: str, value: Any, minimum: float, maximum: float) -> float:
+ try:
+ number = float(value)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"{name} must be a number") from exc
+ if not math.isfinite(number) or number < minimum or number > maximum:
+ raise ValueError(f"{name} must be between {minimum:g} and {maximum:g}")
+ return number
diff --git a/autonomous-grid-search/backend/survey_config.json b/autonomous-grid-search/backend/survey_config.json
new file mode 100644
index 0000000..619ba70
--- /dev/null
+++ b/autonomous-grid-search/backend/survey_config.json
@@ -0,0 +1,30 @@
+{
+ "profile_name": "downward_water_camera",
+ "altitude_m": 5,
+ "cruise_speed_m_s": 2,
+ "camera_horizontal_fov_deg": 78,
+ "camera_vertical_fov_deg": 60,
+ "side_overlap_percent": 70,
+ "forward_overlap_percent": 75,
+ "surface_type": "water",
+ "terrain_enabled": false,
+ "max_cell_side_m": 50,
+ "swarm_drone_count": 4,
+ "battery_swap_seconds": 120,
+ "square_grid_angle_deg": 0,
+ "survey_options": {
+ "camera_model": "fov",
+ "camera_orientation": "landscape",
+ "image_width_px": 4000,
+ "image_height_px": 3000,
+ "minimum_trigger_interval_s": 0.5,
+ "spacing_mode": "camera",
+ "angle_mode": "auto",
+ "entry": "nearest",
+ "pattern": "single",
+ "overshoot_m": 0,
+ "endurance_minutes": 20,
+ "reserve_percent": 20,
+ "turn_seconds": 3
+ }
+}
diff --git a/autonomous-grid-search/backend/terrain_provider.py b/autonomous-grid-search/backend/terrain_provider.py
new file mode 100644
index 0000000..f27e8f1
--- /dev/null
+++ b/autonomous-grid-search/backend/terrain_provider.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""Optional terrain elevation sampling for mission planning context."""
+
+import json
+import os
+from typing import Any, Dict, Iterable, Tuple
+from urllib.request import Request, urlopen
+
+
+DEFAULT_TERRAIN_API_URL = "https://api.opentopodata.org/v1/srtm90m"
+
+
+def sample_plan_terrain(plan, surface_type: str = "water") -> Dict[str, Any]:
+ """Sample home, centre, and cell centres without using DEM as clearance data."""
+ points = [
+ (plan.home_latitude, plan.home_longitude),
+ (plan.center_latitude, plan.center_longitude),
+ ]
+ points.extend((cell.center_latitude, cell.center_longitude) for cell in plan.cells)
+ points.extend((point["latitude"], point["longitude"]) for point in plan.to_dict().get("boundary", []))
+ result = sample_elevations(points)
+ result["surface_type"] = surface_type
+ result["used_for_clearance"] = False
+ result["clearance_source"] = "downward_rangefinder" if surface_type == "water" else "rangefinder_or_validated_terrain"
+ return result
+
+
+def sample_elevations(points: Iterable[Tuple[float, float]]) -> Dict[str, Any]:
+ point_list = list(points)
+ api_url = os.environ.get("TERRAIN_API_URL", DEFAULT_TERRAIN_API_URL).rstrip("/")
+ locations = "|".join(f"{latitude:.7f},{longitude:.7f}" for latitude, longitude in point_list)
+ request = Request(
+ api_url,
+ data=json.dumps({"locations": locations, "interpolation": "cubic"}).encode("utf-8"),
+ headers={"Content-Type": "application/json", "User-Agent": "SentinelBlue-Companion/1.0"},
+ method="POST",
+ )
+ try:
+ with urlopen(request, timeout=5) as response:
+ payload = json.load(response)
+ elevations = [
+ float(item["elevation"])
+ for item in payload.get("results", [])
+ if item.get("elevation") is not None
+ ]
+ if not elevations:
+ return {
+ "status": "unavailable",
+ "provider": api_url,
+ "sample_count": 0,
+ "message": "terrain service returned no elevations for this area",
+ }
+ return {
+ "status": "available",
+ "provider": api_url,
+ "sample_count": len(elevations),
+ "minimum_elevation_m": round(min(elevations), 1),
+ "maximum_elevation_m": round(max(elevations), 1),
+ "elevation_range_m": round(max(elevations) - min(elevations), 1),
+ }
+ except Exception as exc:
+ return {
+ "status": "unavailable",
+ "provider": api_url,
+ "sample_count": 0,
+ "message": f"terrain service unavailable: {type(exc).__name__}",
+ }
diff --git a/autonomous-grid-search/backend/tests/test_auto_grid_planner.py b/autonomous-grid-search/backend/tests/test_auto_grid_planner.py
new file mode 100644
index 0000000..10668c4
--- /dev/null
+++ b/autonomous-grid-search/backend/tests/test_auto_grid_planner.py
@@ -0,0 +1,204 @@
+import math
+import pathlib
+import sys
+import unittest
+import time
+from unittest.mock import patch
+
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3] / "telemetry" / "handheld-nrf-monitor" / "pi-code"))
+from auto_grid_planner import build_auto_grid_mission, build_radius_survey
+from mavlink_manager import MAVLinkManager
+
+
+def survey_payload(width=50, height=50):
+ lat, lon = 12.971598, 77.594562
+ dx, dy = width / 111320 / math.cos(math.radians(lat)), height / 111320
+ return {"boundary": [{"latitude": lat, "longitude": lon}, {"latitude": lat, "longitude": lon + dx},
+ {"latitude": lat + dy, "longitude": lon + dx}, {"latitude": lat + dy, "longitude": lon}],
+ "home": {"latitude": lat, "longitude": lon}, "altitude_m": 5, "cruise_speed_m_s": 2,
+ "survey_options": {"angle_mode": "manual", "grid_angle_deg": 90}}
+
+
+class AutoGridTests(unittest.TestCase):
+ def test_radius_request_uses_pi_settings_and_vehicle_home(self):
+ center = {"latitude": 12.971598, "longitude": 77.594562}
+ request = {"search_area": {"center": center, "radius_m": 25}, "altitude_m": 100,
+ "home": {"latitude": 0, "longitude": 0}, "survey_options": {"spacing_mode": "manual", "line_spacing_m": 100}}
+ telemetry = {**center, "connected": True, "armed": False, "gps_fix_type": "3D FIX",
+ "last_position_timestamp": time.time(), "last_gps_timestamp": time.time()}
+ plan = build_radius_survey(request, telemetry).to_dict()
+ self.assertEqual(5, plan["altitude_m"])
+ self.assertEqual(center, plan["home"])
+ self.assertEqual("vehicle_position", plan["home_source"])
+ self.assertEqual(4, len(plan["boundary"]))
+ self.assertEqual(2500, plan["area_m2"])
+ self.assertEqual("square", plan["search_area"]["shape"])
+ self.assertLess(plan["camera"]["line_spacing_m"], 3)
+ self.assertTrue(all(sortie["ready"] for sortie in plan["sorties"]))
+ stale = build_radius_survey(request, {**telemetry, "last_gps_timestamp": 0}).to_dict()
+ self.assertFalse(stale["sorties"][0]["ready"])
+ self.assertIn("fresh 3D GPS", stale["estimate"]["blocking_reasons"][0])
+ with self.assertRaisesRegex(ValueError, "current flight"):
+ build_radius_survey(request, {**telemetry, "armed": True})
+
+ def test_square_cells_cover_area_once_and_assign_every_line(self):
+ from pyproj import CRS, Transformer
+ from shapely.geometry import Polygon
+ from shapely.ops import unary_union
+ center = {"latitude": 12.971598, "longitude": 77.594562}
+ telemetry = {**center, "connected": True, "gps_fix_type": "3D FIX",
+ "last_position_timestamp": time.time(), "last_gps_timestamp": time.time()}
+ for extent, count in [(50, 4), (60, 9)]:
+ plan = build_radius_survey({"search_area": {"center": center, "radius_m": extent}}, telemetry).to_dict()
+ self.assertEqual(count, plan["cell_count"])
+ self.assertLessEqual(plan["cell_side_m"], 50)
+ crs = CRS.from_proj4("+proj=aeqd +lat_0=12.971598 +lon_0=77.594562 +datum=WGS84 +units=m")
+ projection = Transformer.from_crs("EPSG:4326", crs, always_xy=True)
+ polygons = [Polygon([projection.transform(p["longitude"], p["latitude"]) for p in cell["boundary"]]) for cell in plan["cells"]]
+ self.assertAlmostEqual(sum(p.area for p in polygons), (2 * extent) ** 2, places=2)
+ self.assertAlmostEqual(unary_union(polygons).area, (2 * extent) ** 2, places=2)
+ assignments = [cell for drone in plan["swarm"]["drones"] for cell in drone["cell_ids"]]
+ self.assertEqual(list(range(1, count + 1)), sorted(assignments))
+ for cell in plan["cells"]:
+ tracks = [line for line in plan["survey_lines"] if line["cell_id"] == cell["cell_id"]]
+ self.assertEqual(cell["line_count"], len(tracks))
+ self.assertGreater(len(tracks), 1)
+ self.assertGreaterEqual(cell["coverage_percent"], 99.5)
+ for track in tracks:
+ self.assertEqual(cell["drone_id"], track["drone_id"])
+ self.assertIn(track["sortie_id"], cell["sortie_ids"])
+ for drone in plan["swarm"]["drones"]:
+ segments = drone["segments"]
+ self.assertEqual("takeoff", segments[0]["phase"])
+ self.assertEqual("landing", segments[-1]["phase"])
+ self.assertEqual(plan["home"], segments[-1]["end"])
+ for a, b in zip(segments, segments[1:]):
+ self.assertEqual(a["end_s"], b["start_s"])
+ self.assertEqual(a["end"], b["start"])
+ self.assertTrue(plan["swarm"]["simulation_only"])
+
+ def test_square_cell_limit_rejects_without_truncation(self):
+ with self.assertRaisesRegex(ValueError, "100 square cells"):
+ build_radius_survey({"search_area": {"center": {"latitude": 12, "longitude": 77}, "radius_m": 1000}}, {})
+
+ def plan(self, payload):
+ return build_auto_grid_mission(payload, {}).to_dict()
+
+ def test_camera_axes_coverage_and_export_sequence(self):
+ plan = self.plan(survey_payload())
+ camera = plan["camera"]
+ self.assertAlmostEqual(camera["cross_track_m"], 10 * math.tan(math.radians(39)), places=3)
+ self.assertAlmostEqual(camera["trigger_distance_m"], 10 * math.tan(math.radians(30)) * .25, places=3)
+ self.assertLessEqual(camera["line_spacing_m"], camera["cross_track_m"] * .3 + .001)
+ self.assertGreaterEqual(plan["coverage_percent"], 99.5)
+ for sortie in plan["sorties"]:
+ self.assertTrue(sortie["ready"], sortie["blocking_reasons"])
+ self.assertLessEqual(sortie["outside_boundary_m"], .1)
+ items = sortie["mission_items"]
+ self.assertEqual([16, 22, 178], [item["command"] for item in items[:3]])
+ self.assertEqual(2, items[2]["param2"])
+ self.assertEqual([16, 206, 16, 206], [item["command"] for item in items[3:7]])
+ self.assertGreater(items[4]["param1"], 0)
+ self.assertEqual(0, items[6]["param1"])
+ self.assertEqual(21, items[-1]["command"])
+ self.assertEqual(list(range(len(items))), [item["seq"] for item in items])
+ self.assertEqual(items[0]["x"], items[-1]["x"])
+
+ def test_sensor_orientation_and_gsd(self):
+ payload = survey_payload()
+ payload["survey_options"].update(camera_model="sensor", sensor_width_mm=6, sensor_height_mm=4,
+ focal_length_mm=5, camera_orientation="portrait", minimum_trigger_interval_s=.1)
+ camera = self.plan(payload)["camera"]
+ self.assertEqual(4, camera["cross_track_m"])
+ self.assertEqual(6, camera["along_track_m"])
+ self.assertEqual(.15, camera["gsd_width_cm"])
+
+ def test_invalid_geometry_and_numeric_values(self):
+ payload = survey_payload()
+ payload["boundary"][1], payload["boundary"][2] = payload["boundary"][2], payload["boundary"][1]
+ with self.assertRaisesRegex(ValueError, "Invalid boundary"):
+ self.plan(payload)
+ for value in [float("nan"), float("inf"), True, -10]:
+ payload = survey_payload(); payload["altitude_m"] = value
+ with self.assertRaises(ValueError):
+ self.plan(payload)
+
+ def test_camera_rate_manual_gaps_and_missing_home_block(self):
+ payload = survey_payload(); payload["cruise_speed_m_s"] = 15
+ plan = self.plan(payload)
+ self.assertIn("Camera cannot keep up", " ".join(plan["estimate"]["blocking_reasons"]))
+ self.assertEqual([], plan["sorties"][0]["mission_items"])
+ payload = survey_payload(); payload["survey_options"].update(spacing_mode="manual", line_spacing_m=20)
+ plan = self.plan(payload)
+ self.assertLess(plan["coverage_percent"], 99.5)
+ self.assertIn("Manual line spacing", " ".join(plan["estimate"]["blocking_reasons"]))
+ payload = survey_payload(); del payload["home"]
+ self.assertFalse(self.plan(payload)["sorties"][0]["ready"])
+
+ def test_split_keeps_every_line_and_limits_each_sortie(self):
+ plan = self.plan(survey_payload(150, 150))
+ self.assertGreater(len(plan["sorties"]), 1)
+ self.assertEqual(len(plan["survey_lines"]), sum(sortie["line_count"] for sortie in plan["sorties"]))
+ for sortie in plan["sorties"]:
+ self.assertLessEqual(sortie["mission_item_count"], 500)
+ self.assertLessEqual(sortie["estimated_minutes"], plan["sortie_budget_minutes"] + .05)
+ self.assertTrue(sortie["ready"])
+
+ def test_rotation_reverse_crosshatch_and_concave_boundary(self):
+ payload = survey_payload(60, 30); payload["survey_options"]["grid_angle_deg"] = 35
+ first = self.plan(payload)
+ payload["survey_options"]["entry"] = "reverse"
+ reversed_plan = self.plan(payload)
+ self.assertEqual(first["survey_lines"][0]["start_latitude"], reversed_plan["survey_lines"][-1]["end_latitude"])
+ payload["survey_options"]["pattern"] = "crosshatch"
+ self.assertGreater(self.plan(payload)["estimate"]["survey_line_count"], first["estimate"]["survey_line_count"])
+ payload = survey_payload(); a, b, c, d = payload["boundary"]
+ middle = {"latitude": (a["latitude"] + c["latitude"]) / 2, "longitude": (a["longitude"] + c["longitude"]) / 2}
+ payload["boundary"] = [a, b, middle, c, d]
+ plan = self.plan(payload)
+ self.assertGreater(plan["area_m2"], 1000)
+ self.assertTrue(plan["survey_lines"])
+ payload["survey_options"]["angle_mode"] = "auto"
+ self.assertGreaterEqual(self.plan(payload)["grid_angle_deg"], 0)
+
+ def test_indivisible_line_exceeding_budget_is_blocked(self):
+ payload = survey_payload(500, 10)
+ payload["survey_options"].update(endurance_minutes=3, reserve_percent=50)
+ plan = self.plan(payload)
+ self.assertFalse(plan["sorties"][0]["ready"])
+ self.assertEqual([], plan["sorties"][0]["mission_items"])
+
+ def test_simulator_completes_at_home_and_stays_landed(self):
+ payload = survey_payload(5, 5)
+ plan = self.plan(payload)
+ manager = MAVLinkManager(simulate=True)
+ manager.running = True
+ ticks, snapshots = 0, []
+
+ def tick(_):
+ nonlocal ticks
+ ticks += 1
+ if ticks == 1:
+ manager.upload_mission_items(plan["sorties"][0]["mission_items"])
+ manager.set_arm(True)
+ manager.set_flight_mode("AUTO")
+ snapshots.append(manager.get_telemetry_snapshot())
+ if ticks >= 600:
+ manager.running = False
+
+ with patch("mavlink_manager.time.sleep", side_effect=tick):
+ manager._run_simulation()
+ final = manager.get_telemetry_snapshot()
+ self.assertEqual("MISSION COMPLETED", final["mission_state"])
+ self.assertFalse(final["armed"])
+ self.assertEqual(0, final["altitude_relative"])
+ self.assertEqual(0, final["ground_speed"])
+ self.assertEqual(100, final["mission_progress_percent"])
+ self.assertAlmostEqual(payload["home"]["latitude"], final["latitude"], places=6)
+ self.assertTrue(any(1.9 <= item["ground_speed"] <= 2.1 for item in snapshots))
+ self.assertEqual(0, snapshots[-50]["altitude_relative"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/autonomous-grid-search/backend/tests/test_companion_api.py b/autonomous-grid-search/backend/tests/test_companion_api.py
new file mode 100644
index 0000000..daae3af
--- /dev/null
+++ b/autonomous-grid-search/backend/tests/test_companion_api.py
@@ -0,0 +1,145 @@
+import json
+import pathlib
+import sys
+import unittest
+import time
+from unittest.mock import patch
+from urllib.error import HTTPError
+from urllib.request import Request, urlopen
+
+
+PI_CODE = pathlib.Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(PI_CODE))
+
+from companion_api import CompanionAPIServer # noqa: E402
+from test_auto_grid_planner import survey_payload
+
+
+class FakeMAVLinkManager:
+ def __init__(self, simulate: bool):
+ self.simulate = simulate
+ self.calls = []
+ self.armed = False
+ self.uploaded = []
+
+ def get_telemetry_snapshot(self):
+ return {"connected": True, "latitude": 12.9700, "longitude": 77.5900, "armed": self.armed,
+ "gps_fix_type": "3D FIX", "last_position_timestamp": time.time(), "last_gps_timestamp": time.time()}
+
+ def clear_all_missions(self):
+ self.calls.append("clear")
+ return True
+
+ def upload_mission_items(self, items):
+ self.calls.append(("upload", len(items)))
+ self.uploaded = items
+ return True
+
+ def set_arm(self, arm):
+ self.calls.append(("arm", arm))
+ self.armed = arm
+ return True
+
+ def set_flight_mode(self, mode):
+ self.calls.append(("mode", mode))
+ return True
+
+
+class CompanionAPITests(unittest.TestCase):
+ def setUp(self):
+ self.payload = json.dumps({
+ "center": {"latitude": 12.9716, "longitude": 77.5946},
+ "radius_km": 0.01,
+ "altitude_m": 30,
+ }).encode("utf-8")
+
+ def _start(self, simulate):
+ manager = FakeMAVLinkManager(simulate)
+ server = CompanionAPIServer(manager, host="127.0.0.1", port=0)
+ server.start()
+ self.addCleanup(server.stop)
+ return manager, server.server.server_address[1]
+
+ def _post(self, port, path):
+ request = Request(
+ f"http://127.0.0.1:{port}{path}",
+ data=self.payload,
+ headers={"Content-Type": "application/json"},
+ method="POST",
+ )
+ return urlopen(request, timeout=2)
+
+ def test_plan_endpoint_returns_generated_grid(self):
+ _, port = self._start(simulate=False)
+
+ with self._post(port, "/api/mission/plan") as response:
+ body = json.load(response)
+
+ self.assertEqual("planned", body["status"])
+ self.assertEqual(1, body["plan"]["cell_count"])
+ self.assertGreater(body["plan"]["estimate"]["survey_line_count"], 0)
+
+ def test_radius_only_api_request_loads_pi_profile(self):
+ _, port = self._start(simulate=True)
+ self.payload = json.dumps({"search_area": {"center": {"latitude": 12.9700, "longitude": 77.5900}, "radius_m": 25}}).encode()
+ with self._post(port, "/api/mission/plan") as response:
+ plan = json.load(response)["plan"]
+ self.assertEqual(25, plan["search_area"]["radius_m"])
+ self.assertEqual("downward_water_camera", plan["profile_name"])
+ self.assertTrue(plan["plan_id"])
+ self.assertEqual("disabled", plan["terrain"]["status"])
+
+ def test_live_execution_is_locked(self):
+ manager, port = self._start(simulate=False)
+
+ with self.assertRaises(HTTPError) as raised:
+ self._post(port, "/api/mission/simulate")
+
+ self.assertEqual(403, raised.exception.code)
+ self.assertEqual([], manager.calls)
+
+ def test_simulation_execution_loads_and_starts_mission(self):
+ manager, port = self._start(simulate=True)
+
+ with self._post(port, "/api/mission/simulate") as response:
+ body = json.load(response)
+
+ self.assertEqual(202, response.status)
+ self.assertEqual("simulation_started", body["status"])
+ self.assertEqual("clear", manager.calls[0])
+ self.assertEqual("upload", manager.calls[1][0])
+ self.assertGreater(manager.calls[1][1], 8)
+ self.assertEqual([("arm", True), ("mode", "AUTO")], manager.calls[2:])
+
+ def test_reviewed_sortie_runs_exact_cached_items_and_rejects_replacement(self):
+ manager, port = self._start(simulate=True)
+ self.payload = json.dumps(survey_payload()).encode()
+ with self._post(port, "/api/mission/plan") as response:
+ plan = json.load(response)["plan"]
+ self.payload = json.dumps({"plan_id": plan["plan_id"], "sortie_id": 1, "review_confirmed": True,
+ "altitude_m": 100}).encode()
+ with self._post(port, "/api/mission/simulate") as response:
+ self.assertEqual(202, response.status)
+ self.assertEqual(plan["sorties"][0]["mission_items"], manager.uploaded)
+ with self.assertRaises(HTTPError) as raised:
+ self._post(port, "/api/mission/simulate")
+ self.assertEqual(409, raised.exception.code)
+
+ def test_cache_expiry_and_review_confirmation(self):
+ manager, port = self._start(simulate=True)
+ self.payload = json.dumps(survey_payload()).encode()
+ with self._post(port, "/api/mission/plan") as response:
+ plan = json.load(response)["plan"]
+ self.payload = json.dumps({"plan_id": plan["plan_id"], "sortie_id": 1}).encode()
+ with self.assertRaises(HTTPError) as raised:
+ self._post(port, "/api/mission/simulate")
+ self.assertEqual(400, raised.exception.code)
+ with patch("companion_api.time.monotonic", return_value=float("inf")):
+ with self.assertRaises(HTTPError) as raised:
+ self._post(port, "/api/mission/simulate")
+ self.assertEqual(409, raised.exception.code)
+ self.assertEqual([], manager.calls)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/autonomous-grid-search/backend/tests/test_search_mission_planner.py b/autonomous-grid-search/backend/tests/test_search_mission_planner.py
new file mode 100644
index 0000000..7ce82bd
--- /dev/null
+++ b/autonomous-grid-search/backend/tests/test_search_mission_planner.py
@@ -0,0 +1,74 @@
+import pathlib
+import sys
+import unittest
+
+
+PI_CODE = pathlib.Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(PI_CODE))
+
+from search_mission_planner import build_search_mission # noqa: E402
+
+
+class SearchMissionPlannerTests(unittest.TestCase):
+ def test_one_kilometre_radius_creates_four_cells(self):
+ plan = build_search_mission(12.9716, 77.5946, 1, 5)
+
+ self.assertEqual(4, plan.cell_count)
+ self.assertEqual((2, 2), (plan.rows, plan.columns))
+ self.assertEqual((1, 2, 4, 3), plan.route_cell_ids)
+ self.assertGreater(plan.estimate.survey_line_count, 2_000)
+ self.assertFalse(plan.estimate.simulation_ready)
+
+ def test_two_kilometre_radius_creates_eight_cells(self):
+ plan = build_search_mission(12.9716, 77.5946, 2, 40)
+
+ self.assertEqual(8, plan.cell_count)
+ self.assertEqual((2, 4), (plan.rows, plan.columns))
+ self.assertEqual((1, 2, 3, 4, 8, 7, 6, 5), plan.route_cell_ids)
+
+ def test_mission_contains_takeoff_search_return_and_land(self):
+ plan = build_search_mission(12.9716, 77.5946, 0.01, 30, 12.9700, 77.5900)
+ commands = [item["command"] for item in plan.mission_items]
+
+ self.assertEqual(16, commands[0])
+ self.assertEqual(22, commands[1])
+ self.assertEqual(206, commands[2])
+ self.assertEqual(206, commands[-3])
+ self.assertEqual(16, commands[-2])
+ self.assertEqual(21, commands[-1])
+ self.assertTrue(plan.estimate.simulation_ready)
+
+ def test_camera_footprint_controls_line_and_photo_spacing(self):
+ plan = build_search_mission(
+ 12.9716, 77.5946, 0.01, 5,
+ camera_horizontal_fov_deg=78,
+ camera_vertical_fov_deg=60,
+ side_overlap_percent=70,
+ forward_overlap_percent=75,
+ )
+
+ self.assertAlmostEqual(8.1, plan.camera.footprint_width_m, places=1)
+ self.assertAlmostEqual(5.77, plan.camera.footprint_height_m, places=1)
+ self.assertAlmostEqual(1.73, plan.camera.line_spacing_m, places=1)
+ self.assertAlmostEqual(2.02, plan.camera.trigger_distance_m, places=1)
+ self.assertEqual(12, plan.estimate.survey_line_count)
+
+ def test_rejects_unsafe_or_invalid_ranges(self):
+ invalid_values = [
+ (91, 77, 1, 30),
+ (12, 181, 1, 30),
+ (12, 77, 0.001, 30),
+ (12, 77, 1, 121),
+ ]
+ for values in invalid_values:
+ with self.subTest(values=values):
+ with self.assertRaises(ValueError):
+ build_search_mission(*values)
+
+ def test_rejects_plan_too_large_to_render_safely(self):
+ with self.assertRaisesRegex(ValueError, "planning limit 10000 lines"):
+ build_search_mission(12.9716, 77.5946, 10, 5)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/autonomous-grid-search/web/eslint.config.js b/autonomous-grid-search/web/eslint.config.js
new file mode 100644
index 0000000..041127d
--- /dev/null
+++ b/autonomous-grid-search/web/eslint.config.js
@@ -0,0 +1,26 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
+ }
+ }
+);
diff --git a/autonomous-grid-search/web/index.html b/autonomous-grid-search/web/index.html
new file mode 100644
index 0000000..c01ed53
--- /dev/null
+++ b/autonomous-grid-search/web/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ SentinelBlue Autonomous Grid Search
+
+
+
+
+
+
diff --git a/autonomous-grid-search/web/package-lock.json b/autonomous-grid-search/web/package-lock.json
new file mode 100644
index 0000000..cc90b64
--- /dev/null
+++ b/autonomous-grid-search/web/package-lock.json
@@ -0,0 +1,4295 @@
+{
+ "name": "sentinelblue-autonomous-grid-search",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sentinelblue-autonomous-grid-search",
+ "version": "0.1.0",
+ "dependencies": {
+ "@react-three/fiber": "^8.17.10",
+ "lucide-react": "^0.468.0",
+ "maplibre-gl": "^6.14.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "recharts": "^3.10.1",
+ "three": "^0.165.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.5",
+ "@tailwindcss/postcss": "^4.3.3",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@types/three": "^0.165.0",
+ "@vitejs/plugin-react": "^6.1.2",
+ "eslint": "^9.39.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.26",
+ "globals": "^17.13.0",
+ "postcss": "^8.5.30",
+ "tailwindcss": "^4.3.3",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.71.2",
+ "vite": "^8.3.4",
+ "vitest": "^5.0.4"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz",
+ "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz",
+ "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.3.2",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
+ "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@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,
+ "license": "MIT",
+ "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",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
+ "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 22"
+ }
+ },
+ "node_modules/@mapbox/point-geometry": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
+ "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
+ "license": "ISC"
+ },
+ "node_modules/@mapbox/tiny-sdf": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.3.0.tgz",
+ "integrity": "sha512-gBvcNDBBB66bgQYvnaJ6Jk+VCaMmVq5+IZR2kWPisT88Z8s0kBQt6hfVi8QgRW8Vpg0UWZzBA4ZSyzHHC+VeRA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/unitbezier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
+ "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/vector-tile": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz",
+ "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/point-geometry": "~1.1.0",
+ "@types/geojson": "^7946.0.16",
+ "pbf": "^5.0.0"
+ }
+ },
+ "node_modules/@maplibre/geojson-vt": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.2.tgz",
+ "integrity": "sha512-2ZYhobdAY+y4/P/z1xe0698165LywD0yDdWugdu3NRUrajgVWut+tLkcz7i/RJguMTURwK7gxwmwYMFJbgbQFw==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.1.0"
+ }
+ },
+ "node_modules/@maplibre/maplibre-gl-style-spec": {
+ "version": "26.4.4",
+ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz",
+ "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==",
+ "license": "ISC",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "^2.0.3",
+ "@mapbox/unitbezier": "^1.0.0",
+ "json-stringify-pretty-compact": "^4.0.0",
+ "minimist": "^1.2.8",
+ "quickselect": "^3.0.0",
+ "tinyqueue": "^3.0.0"
+ },
+ "bin": {
+ "gl-style-format": "dist/gl-style-format.mjs",
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
+ "gl-style-validate": "dist/gl-style-validate.mjs"
+ }
+ },
+ "node_modules/@maplibre/mlt": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.3.0.tgz",
+ "integrity": "sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==",
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0"
+ }
+ },
+ "node_modules/@maplibre/vt-pbf": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz",
+ "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0",
+ "@types/geojson": "^7946.0.16",
+ "pbf": "^5.1.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.153.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz",
+ "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
+ "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=18 <19",
+ "react-dom": ">=18 <19",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.13.0.tgz",
+ "integrity": "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz",
+ "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz",
+ "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz",
+ "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz",
+ "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz",
+ "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz",
+ "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz",
+ "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz",
+ "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz",
+ "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz",
+ "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz",
+ "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz",
+ "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz",
+ "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz",
+ "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz",
+ "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz",
+ "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "postcss": "^8.5.16",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+ "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.165.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.165.0.tgz",
+ "integrity": "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "~23.1.1",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz",
+ "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/type-utils": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.71.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz",
+ "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
+ "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
+ "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.71.2",
+ "@typescript-eslint/types": "^8.71.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
+ "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz",
+ "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
+ "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz",
+ "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz",
+ "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.71.2",
+ "@typescript-eslint/tsconfig-utils": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/visitor-keys": "8.71.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
+ "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.71.2",
+ "@typescript-eslint/types": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
+ "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.71.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz",
+ "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.152.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.4.tgz",
+ "integrity": "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.4.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9 || ^3.0.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.4.tgz",
+ "integrity": "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
+ "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/earcut": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz",
+ "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==",
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
+ "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz",
+ "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks",
+ "tests/types",
+ "tests/browser-compat"
+ ]
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
+ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.6",
+ "@eslint/js": "9.39.5",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "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,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gl-matrix": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+ "license": "MIT"
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
+ "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "11.1.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.21.tgz",
+ "integrity": "sha512-bZSadeCPGdaY5/qRLgr6mDSH1ra21rD3rqpiWk7+QTr4ayYPTE9qZYdLh/NdPhMe0wSrTvHD/Lk8q5EwhUs0aw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
+ "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-pretty-compact": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
+ "license": "MIT"
+ },
+ "node_modules/kdbush": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
+ "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
+ "license": "ISC"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
+ "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/maplibre-gl": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.14.0.tgz",
+ "integrity": "sha512-L7U9y9TyPJ3P/JJbW+eH+A3FzdPq+3fnZFqqeCwf/S6cRGganHwVI5b/C8/zOYzreUzsloDPZVVaMQA94wOt4A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/tiny-sdf": "^2.2.0",
+ "@mapbox/unitbezier": "^1.0.0",
+ "@mapbox/vector-tile": "^3.0.0",
+ "@maplibre/geojson-vt": "^6.1.2",
+ "@maplibre/maplibre-gl-style-spec": "^26.4.4",
+ "@maplibre/mlt": "^1.3.0",
+ "@maplibre/vt-pbf": "^4.3.2",
+ "@types/geojson": "^7946.0.16",
+ "bidi-js": "^1.1.0",
+ "earcut": "^3.2.4",
+ "gl-matrix": "^3.4.4",
+ "kdbush": "^4.1.0",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^5.1.2",
+ "potpack": "^2.1.0",
+ "quickselect": "^3.0.0",
+ "tinyqueue": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.14.0",
+ "npm": ">=8.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "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",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pbf": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz",
+ "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
+ "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
+ "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"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.20",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/potpack": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
+ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
+ "license": "ISC"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
+ "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+ "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.21.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/react-redux": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+ "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
+ "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^11.1.8",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.2.0",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
+ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
+ "license": "MIT"
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz",
+ "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.153.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.13",
+ "@rolldown/binding-android-arm64": "1.2.13",
+ "@rolldown/binding-darwin-arm64": "1.2.13",
+ "@rolldown/binding-darwin-x64": "1.2.13",
+ "@rolldown/binding-freebsd-x64": "1.2.13",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.13",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.13",
+ "@rolldown/binding-linux-arm64-musl": "1.2.13",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.13",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.13",
+ "@rolldown/binding-linux-x64-gnu": "1.2.13",
+ "@rolldown/binding-linux-x64-musl": "1.2.13",
+ "@rolldown/binding-openharmony-arm64": "1.2.13",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.13",
+ "@rolldown/binding-win32-x64-msvc": "1.2.13"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+ "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.165.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.165.0.tgz",
+ "integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==",
+ "license": "MIT"
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
+ "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
+ "license": "ISC"
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.71.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz",
+ "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.71.2",
+ "@typescript-eslint/parser": "8.71.2",
+ "@typescript-eslint/typescript-estree": "8.71.2",
+ "@typescript-eslint/utils": "8.71.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
+ "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
+ "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.29",
+ "rolldown": "~1.2.12",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.7.1",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "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/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.4.tgz",
+ "integrity": "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.4",
+ "chai": "^6.3.0",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.4.3",
+ "obug": "^3.0.0",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.3.0",
+ "tinybench": "^6.2.2",
+ "tinyexec": "^1.3.1",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "3.2.1"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.4",
+ "@vitest/browser-preview": "5.0.4",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.4",
+ "@vitest/coverage-v8": "5.0.4",
+ "@vitest/ui": "5.0.4",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
+ "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.11"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/autonomous-grid-search/web/package.json b/autonomous-grid-search/web/package.json
new file mode 100644
index 0000000..13f50e4
--- /dev/null
+++ b/autonomous-grid-search/web/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "sentinelblue-autonomous-grid-search",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview --host 0.0.0.0",
+ "lint": "eslint .",
+ "test": "vitest run",
+ "check": "npm run lint && npm run test && npm run build"
+ },
+ "dependencies": {
+ "@react-three/fiber": "^8.17.10",
+ "lucide-react": "^0.468.0",
+ "maplibre-gl": "^6.14.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "recharts": "^3.10.1",
+ "three": "^0.165.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.5",
+ "@tailwindcss/postcss": "^4.3.3",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@types/three": "^0.165.0",
+ "@vitejs/plugin-react": "^6.1.2",
+ "eslint": "^9.39.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.26",
+ "globals": "^17.13.0",
+ "postcss": "^8.5.30",
+ "tailwindcss": "^4.3.3",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.71.2",
+ "vite": "^8.3.4",
+ "vitest": "^5.0.4"
+ }
+}
diff --git a/autonomous-grid-search/web/postcss.config.js b/autonomous-grid-search/web/postcss.config.js
new file mode 100644
index 0000000..9b544e3
--- /dev/null
+++ b/autonomous-grid-search/web/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {}
+ }
+};
diff --git a/autonomous-grid-search/web/src/assets/JetBrainsMono-OFL.txt b/autonomous-grid-search/web/src/assets/JetBrainsMono-OFL.txt
new file mode 100644
index 0000000..23a3dca
--- /dev/null
+++ b/autonomous-grid-search/web/src/assets/JetBrainsMono-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/autonomous-grid-search/web/src/assets/JetBrainsMono-Regular.woff2 b/autonomous-grid-search/web/src/assets/JetBrainsMono-Regular.woff2
new file mode 100644
index 0000000..8a7726b
Binary files /dev/null and b/autonomous-grid-search/web/src/assets/JetBrainsMono-Regular.woff2 differ
diff --git a/autonomous-grid-search/web/src/assets/JetBrainsMono-SemiBold.woff2 b/autonomous-grid-search/web/src/assets/JetBrainsMono-SemiBold.woff2
new file mode 100644
index 0000000..18dbaf3
Binary files /dev/null and b/autonomous-grid-search/web/src/assets/JetBrainsMono-SemiBold.woff2 differ
diff --git a/autonomous-grid-search/web/src/components/mission_planner.tsx b/autonomous-grid-search/web/src/components/mission_planner.tsx
new file mode 100644
index 0000000..693b410
--- /dev/null
+++ b/autonomous-grid-search/web/src/components/mission_planner.tsx
@@ -0,0 +1,165 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { DownloadIcon, MapPinnedIcon } from "lucide-react";
+import { TerrainMissionMap } from "./terrain_mission_map";
+import { downloadText, missionPlannerFile, squarePreview, swarmPositions, droneColors, type Position, type SurveyPlan } from "../lib/survey_plan";
+
+const inputClass = "mt-1 h-11 w-full rounded border border-hud-line bg-hud-bg px-3 font-mono text-sm text-hud-text focus:border-teal-300 focus:outline-none";
+const buttonClass = "min-h-11 rounded border border-hud-line px-3 py-2 text-sm font-semibold hover:border-teal-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-300 disabled:cursor-not-allowed disabled:opacity-40";
+function apiOrigin(input: string) {
+ const url = new URL(/^https?:\/\//i.test(input) ? input : `http://${input}`);
+ if (!/^https?:$/.test(url.protocol) || url.username || url.password) throw new Error("Enter an HTTP or HTTPS companion API address.");
+ return url.origin;
+}
+
+export function MissionPlanner({ currentLatitude, currentLongitude, telemetryAvailable }: {
+ currentLatitude: number; currentLongitude: number; telemetryAvailable: boolean;
+}) {
+ const [api, setApi] = useState(() => { try { return localStorage.getItem("droneCompanionApi") ?? "localhost:8765"; } catch { return "localhost:8765"; } });
+ const [center, setCenter] = useState(null);
+ const [radius, setRadius] = useState(50);
+ const [focus, setFocus] = useState({ latitude: 12.971598, longitude: 77.594562 });
+ const [latitude, setLatitude] = useState("");
+ const [longitude, setLongitude] = useState("");
+ const [fitVersion, setFitVersion] = useState(0);
+ const [plan, setPlan] = useState(null);
+ const [sortie, setSortie] = useState(1);
+ const [confirmed, setConfirmed] = useState(false);
+ const [busy, setBusy] = useState<"planning" | "starting" | null>(null);
+ const [running, setRunning] = useState(false);
+ const [swarmPlaying, setSwarmPlaying] = useState(false);
+ const [swarmElapsed, setSwarmElapsed] = useState(0);
+ const [swarmRate, setSwarmRate] = useState(30);
+ const [message, setMessage] = useState("Click the map to choose the search centre.");
+ const [failed, setFailed] = useState(false);
+ const request = useRef(null);
+ const version = useRef(0);
+ const initialized = useRef(false);
+ const vehicleValid = telemetryAvailable && Number.isFinite(currentLatitude) && Number.isFinite(currentLongitude) && Math.abs(currentLatitude) <= 85 && Math.abs(currentLongitude) <= 180 && (currentLatitude !== 0 || currentLongitude !== 0);
+ useEffect(() => {
+ if (!initialized.current && vehicleValid) {
+ initialized.current = true; setFocus({ latitude: currentLatitude, longitude: currentLongitude });
+ }
+ }, [vehicleValid, currentLatitude, currentLongitude]);
+ useEffect(() => () => request.current?.abort(), []);
+ const preview = useMemo(() => squarePreview(center, radius), [center, radius]);
+ const boundary = plan?.boundary ?? preview;
+ const virtualPositions = useMemo(() => swarmElapsed > 0 || swarmPlaying ? swarmPositions(plan?.swarm, swarmElapsed) : [], [plan, swarmElapsed, swarmPlaying]);
+ useEffect(() => {
+ if (!swarmPlaying || !plan?.swarm) return;
+ const duration = plan.swarm.duration_s;
+ let previous = performance.now();
+ const timer = setInterval(() => {
+ const now = performance.now(); const delta = (now - previous) / 1000; previous = now;
+ setSwarmElapsed(value => Math.min(duration, value + delta * swarmRate));
+ }, 100);
+ return () => clearInterval(timer);
+ }, [plan, swarmPlaying, swarmRate]);
+ useEffect(() => {
+ if (plan?.swarm && swarmElapsed >= plan.swarm.duration_s) setSwarmPlaying(false);
+ }, [plan, swarmElapsed]);
+ const invalidate = () => {
+ setSwarmPlaying(false); setSwarmElapsed(0);
+ version.current += 1; request.current?.abort(); setPlan(null); setConfirmed(false); setBusy(null); setFailed(false);
+ setMessage("Search area changed. Send it to the Pi to generate the flight plan.");
+ };
+ const chooseCenter = (point: Position) => {
+ if (busy === "starting") return;
+ invalidate(); initialized.current = true; setCenter(point);
+ setLatitude(point.latitude.toFixed(7)); setLongitude(point.longitude.toFixed(7));
+ };
+ const fail = (error: unknown) => { setFailed(true); setMessage(error instanceof Error ? error.message : "The companion API could not be reached."); };
+ const generate = async () => {
+ if (!center || !preview.length) return;
+ invalidate(); const revision = version.current;
+ const controller = new AbortController(); request.current = controller;
+ const timeout = setTimeout(() => controller.abort(), 60000);
+ setBusy("planning"); setMessage("Pi is calculating camera coverage, grid direction and battery sorties…");
+ try {
+ try { localStorage.setItem("droneCompanionApi", api); } catch { /* Storage is optional. */ }
+ const response = await fetch(`${apiOrigin(api)}/api/mission/plan`, { method: "POST", signal: controller.signal, headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ search_area: { center, radius_m: radius } }) });
+ const body = await response.json();
+ if (!response.ok) throw new Error(body.error ?? `Planning failed (${response.status}).`);
+ if (!body.plan?.cells || !body.plan?.swarm || !Array.isArray(body.plan.sorties)) throw new Error("Update the Pi companion API for square-cell planning.");
+ if (revision !== version.current) return;
+ setPlan(body.plan); setSortie(1); setFitVersion(value => value + 1);
+ setMessage("Pi-generated plan ready. Review the route and select a flight sortie.");
+ } catch (error) { if (revision === version.current) fail(controller.signal.aborted ? new Error("Planning timed out. Check the Pi connection or reduce the search extent.") : error); }
+ finally { clearTimeout(timeout); if (revision === version.current) setBusy(null); }
+ };
+ const simulate = async () => {
+ if (!plan || !confirmed) return;
+ setBusy("starting"); setFailed(false);
+ try {
+ const response = await fetch(`${apiOrigin(api)}/api/mission/simulate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ plan_id: plan.plan_id, sortie_id: sortie, review_confirmed: true }) });
+ const body = await response.json();
+ if (!response.ok) throw new Error(body.error ?? "Simulator could not start.");
+ setRunning(true); setMessage(`Sortie ${sortie} is running in simulation.`);
+ } catch (error) { fail(error); } finally { setBusy(null); }
+ };
+ useEffect(() => {
+ if (!running) return;
+ const controller = new AbortController();
+ const timer = setInterval(() => {
+ fetch(`${apiOrigin(api)}/api/health`, { signal: controller.signal }).then(response => response.json()).then(body => {
+ if (body.armed === false) { setRunning(false); setMessage("Simulator is disarmed. Review the next sortie before starting it."); }
+ }).catch(() => { /* Connection loss must not enable another start. */ });
+ }, 3000);
+ return () => { clearInterval(timer); controller.abort(); };
+ }, [api, running]);
+ const selected = plan?.sorties.find(item => item.sortie_id === sortie);
+
+ return
+
+
+
+ Click the map to move the search centre.
+ {}} swarmPositions={virtualPositions} />
+ Cells: C1, C2… · D: virtual drone · H: home · dashed: transfers{center ? `${radius * 2} × ${radius * 2} m square` : "No point selected"}
+
+
+
+ ;
+}
diff --git a/autonomous-grid-search/web/src/components/terrain_mission_map.tsx b/autonomous-grid-search/web/src/components/terrain_mission_map.tsx
new file mode 100644
index 0000000..706c95d
--- /dev/null
+++ b/autonomous-grid-search/web/src/components/terrain_mission_map.tsx
@@ -0,0 +1,134 @@
+import { useEffect, useRef, useState } from "react";
+import { Map, Marker, LngLatBounds, NavigationControl, setWorkerUrl, type GeoJSONSource, type StyleSpecification } from "maplibre-gl";
+import mapLibreWorkerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?url";
+import { droneColors, type Position, type SurveyPlan, type SwarmPosition } from "../lib/survey_plan";
+
+setWorkerUrl(mapLibreWorkerUrl);
+export type DrawMode = "navigate" | "rectangle" | "polygon" | "home" | "center";
+interface Props {
+ boundary: Position[]; home: Position | null; plan: SurveyPlan | null; sortie: number;
+ center?: Position | null;
+ swarmPositions?: SwarmPosition[];
+ mode: DrawMode; focus: Position; fitVersion: number;
+ onClick: (point: Position) => void; onMoveVertex: (index: number, point: Position) => void;
+}
+const coordinates = (point: Position): [number, number] => [point.longitude, point.latitude];
+const collection = (features: GeoJSON.Feature[]): GeoJSON.FeatureCollection => ({ type: "FeatureCollection", features });
+const line = (points: Position[], properties = {}): GeoJSON.Feature => ({ type: "Feature", properties, geometry: { type: "LineString", coordinates: points.map(coordinates) } });
+
+function mapStyle(): StyleSpecification | string {
+ const key = (import.meta as ImportMeta & { env?: Record }).env?.VITE_MAPTILER_KEY;
+ if (key) return `https://api.maptiler.com/maps/hybrid/style.json?key=${encodeURIComponent(key)}`;
+ return { version: 8, sources: {
+ tiles: { type: "raster", tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, maxzoom: 19, attribution: "© OpenStreetMap contributors" },
+ dem: { type: "raster-dem", url: "https://demotiles.maplibre.org/terrain-tiles/tiles.json", tileSize: 256 }
+ }, layers: [{ id: "tiles", type: "raster", source: "tiles" }, { id: "terrain", type: "hillshade", source: "dem", paint: { "hillshade-exaggeration": 0.25 } }] };
+}
+
+export function TerrainMissionMap(props: Props) {
+ const { boundary, home, plan, sortie, mode, center } = props;
+ const container = useRef(null);
+ const mapRef = useRef