One settings table in CameraBridge watches every camera value into the telemetry tick and backs a generic setCameraValue, so the tabbed camera menu (Exposure / Photo / Video / Other) is built from row specs and the camera's own capability ranges. Writes show at once and fall back to what the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider, shutter and aperture wheels, EV strip, metered EV in M), a new camera column with record timer and interval/pano progress, a zoom column for zoom cameras, tap-to-focus / tap-to-meter, and a live histogram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
5.4 KiB
Dart
143 lines
5.4 KiB
Dart
import 'product_profile.dart';
|
||
import 'state/camera_state.dart';
|
||
|
||
/// GO 4's four exposure modes (plan P4 quick exposure panel). The wire value is
|
||
/// the SDK's `ExposureMode` name, sent through `setCameraValue('exposureMode')`.
|
||
enum ExposureMode {
|
||
auto('PROGRAM', 'Auto'),
|
||
aperture('APERTURE_PRIORITY', 'A'),
|
||
shutter('SHUTTER_PRIORITY', 'S'),
|
||
manual('MANUAL', 'M');
|
||
|
||
const ExposureMode(this.wire, this.label);
|
||
|
||
final String wire;
|
||
final String label;
|
||
|
||
static ExposureMode? fromWire(String? wire) {
|
||
for (final ExposureMode m in values) {
|
||
if (m.wire == wire) return m;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// What the pilot may set by hand in this mode. In M the EV readout is the
|
||
/// meter's verdict rather than a setting.
|
||
bool get isoEditable => this != auto;
|
||
bool get shutterEditable => this == shutter || this == manual;
|
||
bool get apertureEditable => this == aperture || this == manual;
|
||
bool get evEditable => this != manual;
|
||
}
|
||
|
||
/// The option lists the exposure controls offer, from the camera's own
|
||
/// capability ranges (P0) and a conservative fallback while those are still
|
||
/// empty (before the camera's handshake, or with nothing connected).
|
||
abstract final class ExposureOptions {
|
||
static const List<String> _isos = <String>['100', '200', '400', '800', '1600', '3200'];
|
||
static const List<String> _shutters = <String>[
|
||
'1/8000', '1/4000', '1/2000', '1/1000', '1/500', '1/240', '1/120', '1/60', '1/30', '1/15', '1/8', '1/4', '1/2', '1',
|
||
];
|
||
static const List<String> _evs = <String>[
|
||
'-3.0', '-2.7', '-2.3', '-2.0', '-1.7', '-1.3', '-1.0', '-0.7', '-0.3', '0.0', //
|
||
'+0.3', '+0.7', '+1.0', '+1.3', '+1.7', '+2.0', '+2.3', '+2.7', '+3.0',
|
||
];
|
||
|
||
/// The modes the segmented control shows: the camera's own list, with A only
|
||
/// on adjustable-aperture lenses.
|
||
static List<ExposureMode> modes(CameraProfile? c) {
|
||
final List<ExposureMode> reported = <ExposureMode>[
|
||
for (final String w in c?.exposureModes ?? const <String>[])
|
||
if (ExposureMode.fromWire(w) case final ExposureMode m) m,
|
||
];
|
||
final List<ExposureMode> all = reported.isEmpty ? ExposureMode.values : reported;
|
||
final bool aperture = c == null || c.adjustableApertureSupported;
|
||
return <ExposureMode>[
|
||
for (final ExposureMode m in ExposureMode.values)
|
||
if (all.contains(m) && (m != ExposureMode.aperture || aperture)) m,
|
||
];
|
||
}
|
||
|
||
/// Numeric ISOs for the slider; `AUTO` is its own toggle.
|
||
static List<String> isos(CameraProfile? c) {
|
||
final List<String> l = <String>[for (final String v in c?.isos ?? const <String>[]) if (int.tryParse(v) != null) v];
|
||
return l.isEmpty ? _isos : l;
|
||
}
|
||
|
||
static bool isoAutoAvailable(CameraProfile? c) => c == null || c.isos.isEmpty || c.isos.contains('AUTO');
|
||
|
||
/// Fastest first, as the SDK lists them.
|
||
static List<String> shutters(CameraProfile? c) =>
|
||
(c?.shutterSpeeds.isNotEmpty ?? false) ? c!.shutterSpeeds : _shutters;
|
||
|
||
static List<String> apertures(CameraProfile? c) => c?.apertures ?? const <String>[];
|
||
|
||
/// Lowest first.
|
||
static List<String> evs(CameraProfile? c) {
|
||
final List<String> l = (c?.exposureCompensations.isNotEmpty ?? false) ? c!.exposureCompensations : _evs;
|
||
return List<String>.of(l)..sort((String a, String b) => (evValue(a) ?? 0).compareTo(evValue(b) ?? 0));
|
||
}
|
||
|
||
/// The EV one press of − / + lands on, or null at the end of the range.
|
||
static String? stepEv(String? current, List<String> evs, int direction) {
|
||
if (evs.isEmpty) return null;
|
||
final double now = evValue(current) ?? 0;
|
||
if (direction > 0) {
|
||
for (final String e in evs) {
|
||
if ((evValue(e) ?? 0) > now + 0.05) return e;
|
||
}
|
||
} else {
|
||
for (final String e in evs.reversed) {
|
||
if ((evValue(e) ?? 0) < now - 0.05) return e;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// `+0.7` → 0.7, `0.0` / `0` → 0.
|
||
static double? evValue(String? label) => label == null ? null : double.tryParse(label.replaceFirst('+', ''));
|
||
|
||
/// The index in [values] closest to [current], for a wheel to start on.
|
||
static int nearest(List<String> values, String? current) {
|
||
if (values.isEmpty) return 0;
|
||
final int exact = current == null ? -1 : values.indexOf(current);
|
||
if (exact >= 0) return exact;
|
||
final double? v = _numeric(current);
|
||
if (v == null) return 0;
|
||
int best = 0;
|
||
double bestD = double.infinity;
|
||
for (int i = 0; i < values.length; i++) {
|
||
final double? x = _numeric(values[i]);
|
||
if (x == null) continue;
|
||
final double d = (x - v).abs();
|
||
if (d < bestD) {
|
||
bestD = d;
|
||
best = i;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
/// `1/240` → 0.0041, `2.5` → 2.5, `f/2.8` → 2.8, `400` → 400.
|
||
static double? _numeric(String? s) {
|
||
if (s == null) return null;
|
||
final String t = s.replaceFirst('f/', '').replaceAll('"', '');
|
||
final int slash = t.indexOf('/');
|
||
if (slash > 0) {
|
||
final double? n = double.tryParse(t.substring(0, slash)), d = double.tryParse(t.substring(slash + 1));
|
||
return n == null || d == null || d == 0 ? null : n / d;
|
||
}
|
||
return double.tryParse(t);
|
||
}
|
||
}
|
||
|
||
/// How far the meter is from a correct exposure in M, for the indicator:
|
||
/// within a third of a stop is fine, past two-thirds is worth a colour.
|
||
enum MeterVerdict { ok, under, over }
|
||
|
||
MeterVerdict meterVerdict(CameraState c) {
|
||
final double ev = ExposureOptions.evValue(c.ev) ?? 0;
|
||
if (ev <= -0.7) return MeterVerdict.under;
|
||
if (ev >= 0.7) return MeterVerdict.over;
|
||
return MeterVerdict.ok;
|
||
}
|