diff --git a/lib/data/models/exposure_pair.dart b/lib/data/models/exposure_pair.dart index 9df3ada..d3aa823 100644 --- a/lib/data/models/exposure_pair.dart +++ b/lib/data/models/exposure_pair.dart @@ -8,4 +8,16 @@ class ExposurePair { @override String toString() => '$aperture - $shutterSpeed'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ExposurePair && + other.aperture == aperture && + other.shutterSpeed == shutterSpeed; + } + + @override + int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType); } diff --git a/lib/data/models/film.dart b/lib/data/models/film.dart index 2da1c9b..ab651e8 100644 --- a/lib/data/models/film.dart +++ b/lib/data/models/film.dart @@ -13,6 +13,8 @@ double log10polynomian( ) => a * pow(log10(x), 2) + b * log10(x) + c; +typedef ReciprocityFailureBuilder = ShutterSpeedValue Function(ShutterSpeedValue shutterSpeed); + /// Only Ilford films have reciprocity formulas provided by the manufacturer: /// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf /// diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index e4b3b91..ceaca2e 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -30,7 +30,7 @@ class MeteringScreen extends StatelessWidget { children: [ Expanded( child: BlocBuilder( - builder: (_, state) => _MeteringContainerBuidler( + builder: (_, state) => MeteringContainerBuidler( ev: state is MeteringDataState ? state.ev : null, film: state.film, iso: state.iso, @@ -77,17 +77,18 @@ class _InheritedListeners extends StatelessWidget { context.read().add(EquipmentProfileChangedEvent(value)); }, child: InheritedModelAspectListener( - aspect: MeteringScreenLayoutFeature.equipmentProfiles, + aspect: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { if (!value) { - EquipmentProfileProvider.of(context).setProfile(context.get().first); + context.read().add(const FilmChangedEvent(Film.other())); } }, child: InheritedModelAspectListener( - aspect: MeteringScreenLayoutFeature.filmPicker, + aspect: MeteringScreenLayoutFeature.equipmentProfiles, onDidChangeDependencies: (value) { if (!value) { - context.read().add(const FilmChangedEvent(Film.other())); + EquipmentProfileProvider.of(context) + .setProfile(context.get().first); } }, child: child, @@ -97,7 +98,7 @@ class _InheritedListeners extends StatelessWidget { } } -class _MeteringContainerBuidler extends StatelessWidget { +class MeteringContainerBuidler extends StatelessWidget { final double? ev; final Film film; final IsoValue iso; @@ -106,7 +107,7 @@ class _MeteringContainerBuidler extends StatelessWidget { final ValueChanged onIsoChanged; final ValueChanged onNdChanged; - const _MeteringContainerBuidler({ + const MeteringContainerBuidler({ required this.ev, required this.film, required this.iso, @@ -118,7 +119,14 @@ class _MeteringContainerBuidler extends StatelessWidget { @override Widget build(BuildContext context) { - final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : []; + final exposurePairs = ev != null + ? buildExposureValues( + ev!, + context.listen(), + context.listen(), + film, + ) + : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null; return context.listen() == EvSourceType.camera @@ -146,28 +154,30 @@ class _MeteringContainerBuidler extends StatelessWidget { ); } - List buildExposureValues(BuildContext context, double ev, Film film) { + @visibleForTesting + static List buildExposureValues( + double ev, + StopType stopType, + EquipmentProfile equipmentProfile, + Film film, + ) { if (ev.isNaN || ev.isInfinite) { return List.empty(); } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 - final StopType stopType = context.listen(); final int evSteps = (ev * (stopType.index + 1)).round(); - final EquipmentProfile equipmentProfile = context.listen(); - final List apertureValues = - equipmentProfile.apertureValues.whereStopType(stopType); - final List shutterSpeedValues = - equipmentProfile.shutterSpeedValues.whereStopType(stopType); + final apertureValues = ApertureValue.whereStopType(stopType); + final shutterSpeedValues = ShutterSpeedValue.whereStopType(stopType); /// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list. /// But user can exclude this value from the list using custom equipment profile. /// So we have to restore the index of the anchor value. - const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); + const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed); if (anchorIndex < 0) { - final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType); + final filteredFullList = ShutterSpeedValue.whereStopType(stopType); final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first); final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed); if (customListStartIndex < fullListAnchor) { @@ -200,7 +210,8 @@ class _MeteringContainerBuidler extends StatelessWidget { if (itemsCount < 0) { return List.empty(); } - return List.generate( + + final exposurePairs = List.generate( itemsCount, (index) => ExposurePair( apertureValues[index + apertureOffset], @@ -208,5 +219,30 @@ class _MeteringContainerBuidler extends StatelessWidget { ), growable: false, ); + + /// Full equipment profile, nothing to cut + if (equipmentProfile.id == "") { + return exposurePairs; + } + + final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType); + final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType); + + final startCutEV = max( + exposurePairs.first.aperture.difference(equipmentApertureValues.first), + exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first), + ); + final endCutEV = max( + equipmentApertureValues.last.difference(exposurePairs.last.aperture), + equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed), + ); + + final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + + if (startCut > itemsCount - endCut) { + return const []; + } + return exposurePairs.sublist(startCut, itemsCount - endCut); } } diff --git a/test/screens/metering/screen_metering_test.dart b/test/screens/metering/screen_metering_test.dart new file mode 100644 index 0000000..2bbd2a1 --- /dev/null +++ b/test/screens/metering/screen_metering_test.dart @@ -0,0 +1,646 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +void main() { + const defaultEquipmentProfile = EquipmentProfileData( + id: "", + name: 'Default', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ); + + group('Empty list', () { + List exposurePairsFull(double ev) => MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('isNan', () { + expect(exposurePairsFull(double.nan), const []); + }); + + test('isInifinity', () { + expect(exposurePairsFull(double.infinity), const []); + }); + }); + + group('Default equipment profile', () { + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2.5, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.3, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + }); + + group('Reduced equipment profile', () { + final equipmentProfile = EquipmentProfileData( + id: "1", + name: 'Test1', + apertureValues: ApertureValue.values.sublist(4), + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values.sublist(0, ShutterSpeedValue.values.length - 4), + isoValues: IsoValue.values, + ); + + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4.0, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.5, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4.8, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.5, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4.8, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.5, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4.8, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(4.5, StopType.third), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.6, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.0, StopType.third), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(1.6, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.0, StopType.third), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ), + ); + }); + }); + }); +}