feat(ground-suite): add local vehicle catalog
No files matched your search
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,46 @@
|
||||
import '../../vehicle_workspace/domain/vehicle_definition.dart';
|
||||
|
||||
/// An operator-created fleet unit in the local planning workspace.
|
||||
///
|
||||
/// It deliberately contains no received identity, platform, authority, or
|
||||
/// physical pose. Those must arrive through separate reviewed bindings later.
|
||||
class FleetUnit {
|
||||
const FleetUnit({
|
||||
required this.localUnitId,
|
||||
required this.displayName,
|
||||
required this.definition,
|
||||
required this.localPlacement,
|
||||
});
|
||||
|
||||
final String localUnitId;
|
||||
final String displayName;
|
||||
final VehicleDefinition definition;
|
||||
final LocalScenePoint localPlacement;
|
||||
|
||||
FleetUnit copyWith({String? displayName}) => FleetUnit(
|
||||
localUnitId: localUnitId,
|
||||
displayName: displayName ?? this.displayName,
|
||||
definition: definition,
|
||||
localPlacement: localPlacement,
|
||||
);
|
||||
}
|
||||
|
||||
/// A normalized placement in the operator's local tactical scene.
|
||||
///
|
||||
/// This is deliberately not a geographic position, vehicle pose, or received
|
||||
/// location. A future map binding will keep reported coordinates separate.
|
||||
class LocalScenePoint {
|
||||
const LocalScenePoint({required this.x, required this.y})
|
||||
: assert(x >= 0 && x <= 1),
|
||||
assert(y >= 0 && y <= 1);
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is LocalScenePoint && other.x == x && other.y == y;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(x, y);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'vehicle_definition.dart';
|
||||
|
||||
/// Revision of the local reviewed definition catalog.
|
||||
///
|
||||
/// TODO(gcs-vehicle-catalog): generate this catalog from reviewed firmware
|
||||
/// build manifests, then persist both this revision and a definition ID in a
|
||||
/// versioned local vehicle workspace record.
|
||||
const vehicleDefinitionCatalogRevision = 1;
|
||||
|
||||
/// Local vehicle types available in the tactical scene catalog.
|
||||
const supportedVehicleDefinitions = <VehicleDefinition>[
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.quadcopter.local',
|
||||
name: 'Quadcopter',
|
||||
vehicleClass: 'Multirotor',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/quadcopter.png',
|
||||
),
|
||||
),
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.fixed-wing.local',
|
||||
name: 'Fixed-wing',
|
||||
vehicleClass: 'Fixed-wing',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/fixed_wing.png',
|
||||
),
|
||||
),
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.tiltrotor-vtol.local',
|
||||
name: 'Tiltrotor VTOL',
|
||||
vehicleClass: 'VTOL',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/tiltrotor_vtol.png',
|
||||
),
|
||||
),
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.helicopter.local',
|
||||
name: 'Helicopter',
|
||||
vehicleClass: 'Rotorcraft',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/helicopter.png',
|
||||
),
|
||||
),
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.rover.local',
|
||||
name: 'Ground rover',
|
||||
vehicleClass: 'Ground vehicle',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/rover.png',
|
||||
),
|
||||
),
|
||||
VehicleDefinition(
|
||||
id: 'kaptan.vehicle.surface-vessel.local',
|
||||
name: 'Surface vessel',
|
||||
vehicleClass: 'Marine vehicle',
|
||||
supportedProfile: 'Local scene unit',
|
||||
media: VehicleCatalogMedia(
|
||||
imageAssetPath: 'assets/vehicle_catalog/photos/surface_vessel.png',
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
/// Bundled profile image for a local vehicle definition.
|
||||
class VehicleCatalogMedia {
|
||||
const VehicleCatalogMedia({required this.imageAssetPath});
|
||||
|
||||
final String imageAssetPath;
|
||||
}
|
||||
|
||||
/// A reviewed vehicle definition that can own a Ground Suite workspace.
|
||||
///
|
||||
/// It is deliberately separate from platform identity, compatibility, and
|
||||
/// write authority reported by a future live vehicle source.
|
||||
class VehicleDefinition {
|
||||
const VehicleDefinition({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.vehicleClass,
|
||||
required this.supportedProfile,
|
||||
required this.media,
|
||||
});
|
||||
|
||||
/// Stable local identity for persistence and future build matching.
|
||||
final String id;
|
||||
final String name;
|
||||
final String vehicleClass;
|
||||
final String supportedProfile;
|
||||
final VehicleCatalogMedia media;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../fleet/domain/fleet_unit.dart';
|
||||
import '../domain/vehicle_definition.dart';
|
||||
|
||||
final vehicleWorkspaceProvider =
|
||||
NotifierProvider<VehicleWorkspaceController, VehicleWorkspaceState>(
|
||||
VehicleWorkspaceController.new,
|
||||
);
|
||||
|
||||
/// Local fleet workspace state.
|
||||
///
|
||||
/// TODO(gcs-fleet-storage): persist local-unit IDs and reviewed definition IDs
|
||||
/// in a versioned workspace store.
|
||||
class VehicleWorkspaceState {
|
||||
const VehicleWorkspaceState({required this.units});
|
||||
|
||||
const VehicleWorkspaceState.unselected() : units = const [];
|
||||
|
||||
final List<FleetUnit> units;
|
||||
|
||||
bool get hasUnits => units.isNotEmpty;
|
||||
|
||||
VehicleWorkspaceState copyWith({List<FleetUnit>? units}) =>
|
||||
VehicleWorkspaceState(units: units ?? this.units);
|
||||
}
|
||||
|
||||
class VehicleWorkspaceController extends Notifier<VehicleWorkspaceState> {
|
||||
static const maximumLocalUnits = 64;
|
||||
static const _minimumSeparationSquared = .0016;
|
||||
|
||||
@override
|
||||
VehicleWorkspaceState build() => const VehicleWorkspaceState.unselected();
|
||||
|
||||
/// Validation shared by the hangar draft and committed workspace updates.
|
||||
/// It applies only to the operator-local display label, never a vehicle ID.
|
||||
static bool isValidLocalDisplayName(String value) =>
|
||||
_normalizeDisplayName(value) != null;
|
||||
|
||||
bool canPlace(LocalScenePoint localPlacement) =>
|
||||
state.units.length < maximumLocalUnits &&
|
||||
state.units.every((unit) {
|
||||
final dx = unit.localPlacement.x - localPlacement.x;
|
||||
final dy = unit.localPlacement.y - localPlacement.y;
|
||||
return dx * dx + dy * dy >= _minimumSeparationSquared;
|
||||
});
|
||||
|
||||
bool addUnit({
|
||||
required VehicleDefinition definition,
|
||||
required LocalScenePoint localPlacement,
|
||||
String? displayName,
|
||||
}) {
|
||||
if (!canPlace(localPlacement)) {
|
||||
return false;
|
||||
}
|
||||
final sequence = state.units.length + 1;
|
||||
final unit = FleetUnit(
|
||||
localUnitId: 'local-unit-$sequence',
|
||||
displayName:
|
||||
_normalizeDisplayName(displayName) ??
|
||||
'${definition.name.toUpperCase()}-${sequence.toString().padLeft(2, '0')}',
|
||||
definition: definition,
|
||||
localPlacement: localPlacement,
|
||||
);
|
||||
state = VehicleWorkspaceState(units: [...state.units, unit]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Creates a local unit at a deterministic, scattered scene position.
|
||||
///
|
||||
/// Until a reviewed MAVLink position binding exists, this is presentation
|
||||
/// placement only: it is neither a GPS position nor received vehicle data.
|
||||
bool addUnitAutomatically({
|
||||
required VehicleDefinition definition,
|
||||
String? displayName,
|
||||
}) {
|
||||
for (var attempt = 0; attempt < maximumLocalUnits; attempt++) {
|
||||
final sequence = state.units.length + attempt + 1;
|
||||
final placement = LocalScenePoint(
|
||||
x: .14 + _fraction(sequence * .61803398875) * .72,
|
||||
y: .16 + _fraction(sequence * .41421356237) * .66,
|
||||
);
|
||||
if (addUnit(
|
||||
definition: definition,
|
||||
localPlacement: placement,
|
||||
displayName: displayName,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static double _fraction(double value) => value - value.floorToDouble();
|
||||
|
||||
bool renameUnit(String localUnitId, String displayName) {
|
||||
final normalized = _normalizeDisplayName(displayName);
|
||||
if (normalized == null) {
|
||||
return false;
|
||||
}
|
||||
var changed = false;
|
||||
final units = state.units
|
||||
.map((unit) {
|
||||
if (unit.localUnitId != localUnitId) {
|
||||
return unit;
|
||||
}
|
||||
changed = true;
|
||||
return unit.copyWith(displayName: normalized);
|
||||
})
|
||||
.toList(growable: false);
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
state = state.copyWith(units: units);
|
||||
return true;
|
||||
}
|
||||
|
||||
static String? _normalizeDisplayName(String? value) {
|
||||
final normalized = value?.trim();
|
||||
if (normalized == null ||
|
||||
normalized.isEmpty ||
|
||||
normalized.runes.length > 32) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../shared/widgets/surfaces.dart';
|
||||
import '../../domain/vehicle_definition.dart';
|
||||
|
||||
/// A compact, keyboard-selectable workspace definition tile.
|
||||
///
|
||||
/// Only a reviewed [VehicleDefinition] reaches this widget. The radio control
|
||||
/// changes local pending workspace state; it does not connect, configure, or
|
||||
/// identify a vehicle.
|
||||
class VehicleCatalogCard extends StatefulWidget {
|
||||
const VehicleCatalogCard({
|
||||
super.key,
|
||||
required this.definition,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final VehicleDefinition definition;
|
||||
final bool selected;
|
||||
final VoidCallback onSelected;
|
||||
|
||||
@override
|
||||
State<VehicleCatalogCard> createState() => _VehicleCatalogCardState();
|
||||
}
|
||||
|
||||
class _VehicleCatalogCardState extends State<VehicleCatalogCard> {
|
||||
bool _showFocus = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final accent = widget.selected || _showFocus ? scheme.primary : null;
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
checked: widget.selected,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
label:
|
||||
'${widget.definition.name}, ${widget.definition.vehicleClass}, '
|
||||
'${widget.definition.supportedProfile}, local unit type, '
|
||||
'local vehicle image, not telemetry',
|
||||
onTap: widget.onSelected,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: _showFocus
|
||||
? Border.all(color: scheme.primary, width: 2)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
excludeFromSemantics: true,
|
||||
onFocusChange: (value) => setState(() => _showFocus = value),
|
||||
onTap: widget.onSelected,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InstrumentPanel(
|
||||
accent: accent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_VehicleImage(media: widget.definition.media),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ConsoleLabel('Local unit type'),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
widget.definition.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${widget.definition.vehicleClass} · ${widget.definition.supportedProfile}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.selected
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_off,
|
||||
size: 18,
|
||||
color: widget.selected
|
||||
? scheme.primary
|
||||
: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.selected
|
||||
? 'Selected for workspace'
|
||||
: 'Select for workspace',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VehicleImage extends StatelessWidget {
|
||||
const _VehicleImage({required this.media});
|
||||
|
||||
final VehicleCatalogMedia media;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(11)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 10,
|
||||
child: Image.asset(
|
||||
media.imageAssetPath,
|
||||
key: Key('vehicle-image-${media.imageAssetPath}'),
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
excludeFromSemantics: true,
|
||||
errorBuilder: (context, error, stackTrace) => ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: const Center(child: Icon(Icons.image_not_supported_outlined)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -65,6 +65,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -105,6 +113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -163,6 +179,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -187,6 +227,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -275,6 +339,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -291,6 +363,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -307,6 +443,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -512,6 +656,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -522,4 +674,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.2 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.38.4"
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ground_suite
|
||||
description: "Kaptan's desktop ground-control observability console."
|
||||
description: "Kaptan's desktop ground-control workspace."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
@@ -32,6 +32,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter_riverpod: ^3.4.2
|
||||
google_fonts: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -55,10 +56,10 @@ flutter:
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/test.png
|
||||
- assets/vehicle_catalog/
|
||||
- assets/vehicle_catalog/photos/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dart:ui' show CheckedState;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
||||
import 'package:ground_suite/features/vehicle_workspace/presentation/widgets/vehicle_catalog_card.dart';
|
||||
|
||||
void main() {
|
||||
test('every local vehicle type has a clear bundled profile image', () {
|
||||
expect(supportedVehicleDefinitions, hasLength(6));
|
||||
expect(
|
||||
supportedVehicleDefinitions.map((definition) => definition.name),
|
||||
containsAll(<String>[
|
||||
'Quadcopter',
|
||||
'Fixed-wing',
|
||||
'Tiltrotor VTOL',
|
||||
'Helicopter',
|
||||
'Ground rover',
|
||||
'Surface vessel',
|
||||
]),
|
||||
);
|
||||
for (final definition in supportedVehicleDefinitions) {
|
||||
expect(definition.id, isNotEmpty);
|
||||
expect(definition.name, isNotEmpty);
|
||||
expect(definition.media.imageAssetPath, isNotEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('catalog card is a single keyboard-activatable radio control', (
|
||||
tester,
|
||||
) async {
|
||||
var selections = 0;
|
||||
final definition = supportedVehicleDefinitions.first;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 760,
|
||||
child: VehicleCatalogCard(
|
||||
definition: definition,
|
||||
selected: false,
|
||||
onSelected: () => selections += 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final card = find.byType(VehicleCatalogCard);
|
||||
final semantics = tester.getSemantics(card);
|
||||
expect(semantics.flagsCollection.isChecked, CheckedState.isFalse);
|
||||
expect(semantics.flagsCollection.isInMutuallyExclusiveGroup, isTrue);
|
||||
expect(semantics.label, contains('local vehicle image, not telemetry'));
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pump();
|
||||
|
||||
expect(selections, 2);
|
||||
});
|
||||
|
||||
testWidgets('definition images are bundled locally', (tester) async {
|
||||
for (final definition in supportedVehicleDefinitions) {
|
||||
final image = await rootBundle.load(definition.media.imageAssetPath);
|
||||
expect(image.lengthInBytes, greaterThan(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ground_suite/features/fleet/domain/fleet_unit.dart';
|
||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
||||
import 'package:ground_suite/features/vehicle_workspace/presentation/vehicle_workspace_controller.dart';
|
||||
|
||||
void main() {
|
||||
test('a reviewed definition creates a selected local fleet unit', () {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
||||
expect(container.read(vehicleWorkspaceProvider).hasUnits, isFalse);
|
||||
|
||||
final definition = supportedVehicleDefinitions.first;
|
||||
expect(
|
||||
controller.addUnit(
|
||||
definition: definition,
|
||||
localPlacement: const LocalScenePoint(x: .35, y: .45),
|
||||
displayName: 'North survey',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(container.read(vehicleWorkspaceProvider).hasUnits, isTrue);
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.single.definition,
|
||||
definition,
|
||||
);
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.single.localUnitId,
|
||||
'local-unit-1',
|
||||
);
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
||||
'North survey',
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.addUnit(
|
||||
definition: definition,
|
||||
localPlacement: const LocalScenePoint(x: .72, y: .45),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(container.read(vehicleWorkspaceProvider).units, hasLength(2));
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.last.localUnitId,
|
||||
'local-unit-2',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'rejects overlapping local placements and keeps a bounded workspace',
|
||||
() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
||||
final definition = supportedVehicleDefinitions.first;
|
||||
const first = LocalScenePoint(x: .2, y: .2);
|
||||
expect(
|
||||
controller.addUnit(definition: definition, localPlacement: first),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.addUnit(definition: definition, localPlacement: first),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.addUnit(
|
||||
definition: definition,
|
||||
localPlacement: const LocalScenePoint(x: .23, y: .2),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.addUnit(
|
||||
definition: definition,
|
||||
localPlacement: const LocalScenePoint(x: .65, y: .7),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'catalog additions receive a scattered local presentation placement',
|
||||
() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
||||
final definition = supportedVehicleDefinitions.first;
|
||||
expect(controller.addUnitAutomatically(definition: definition), isTrue);
|
||||
expect(controller.addUnitAutomatically(definition: definition), isTrue);
|
||||
|
||||
final units = container.read(vehicleWorkspaceProvider).units;
|
||||
expect(units, hasLength(2));
|
||||
expect(units.first.localPlacement, isNot(units.last.localPlacement));
|
||||
for (final unit in units) {
|
||||
expect(unit.localPlacement.x, inInclusiveRange(0.0, 1.0));
|
||||
expect(unit.localPlacement.y, inInclusiveRange(0.0, 1.0));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'local unit names are bounded workspace data, not vehicle configuration',
|
||||
() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
||||
expect(
|
||||
controller.addUnitAutomatically(
|
||||
definition: supportedVehicleDefinitions.first,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.renameUnit('local-unit-1', ' Ridge observer '),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
||||
'Ridge observer',
|
||||
);
|
||||
expect(controller.renameUnit('local-unit-1', ' '), isFalse);
|
||||
expect(
|
||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
||||
'Ridge observer',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||