Fixed & tested exposure pairs list builder

This commit is contained in:
Vadim 2023-07-17 18:06:10 +02:00
parent 9ef7764b58
commit df81e13a63
4 changed files with 714 additions and 18 deletions

View file

@ -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);
}

View file

@ -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
///

View file

@ -30,7 +30,7 @@ class MeteringScreen extends StatelessWidget {
children: [
Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (_, state) => _MeteringContainerBuidler(
builder: (_, state) => MeteringContainerBuidler(
ev: state is MeteringDataState ? state.ev : null,
film: state.film,
iso: state.iso,
@ -76,13 +76,6 @@ class _InheritedListeners extends StatelessWidget {
onDidChangeDependencies: (value) {
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
},
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
aspect: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) {
if (!value) {
EquipmentProfileProvider.of(context).setProfile(context.get<EquipmentProfiles>().first);
}
},
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
aspect: MeteringScreenLayoutFeature.filmPicker,
onDidChangeDependencies: (value) {
@ -90,6 +83,14 @@ class _InheritedListeners extends StatelessWidget {
context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
}
},
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
aspect: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) {
if (!value) {
EquipmentProfileProvider.of(context)
.setProfile(context.get<EquipmentProfiles>().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<IsoValue> onIsoChanged;
final ValueChanged<NdValue> 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) : <ExposurePair>[];
final exposurePairs = ev != null
? buildExposureValues(
ev!,
context.listen<StopType>(),
context.listen<EquipmentProfile>(),
film,
)
: <ExposurePair>[];
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
return context.listen<EvSourceType>() == EvSourceType.camera
@ -146,28 +154,30 @@ class _MeteringContainerBuidler extends StatelessWidget {
);
}
List<ExposurePair> buildExposureValues(BuildContext context, double ev, Film film) {
@visibleForTesting
static List<ExposurePair> 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<StopType>();
final int evSteps = (ev * (stopType.index + 1)).round();
final EquipmentProfile equipmentProfile = context.listen<EquipmentProfile>();
final List<ApertureValue> apertureValues =
equipmentProfile.apertureValues.whereStopType(stopType);
final List<ShutterSpeedValue> 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);
}
}

View file

@ -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<ExposurePair> 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<ExposurePair> 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<ExposurePair> 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<ExposurePair> 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<ExposurePair> 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<ExposurePair> 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<ExposurePair> 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),
),
);
});
});
});
}