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
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +113,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -163,6 +179,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
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:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -187,6 +227,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -275,6 +339,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -291,6 +363,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -307,6 +443,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -512,6 +656,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
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:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -522,4 +674,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.12.2 <4.0.0"
|
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
|
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
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# 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
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
@@ -32,6 +32,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_riverpod: ^3.4.2
|
flutter_riverpod: ^3.4.2
|
||||||
|
google_fonts: ^6.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -55,10 +56,10 @@ flutter:
|
|||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
assets:
|
||||||
# assets:
|
- assets/test.png
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/vehicle_catalog/
|
||||||
# - images/a_dot_ham.jpeg
|
- assets/vehicle_catalog/photos/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# 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',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||