init
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import '../../../data/local/models/models.dart';
|
||||
|
||||
/// Computes archery score from a normalized hit coordinate.
|
||||
///
|
||||
/// Normalized coordinates use the range [-1, 1] where (0, 0) is the target
|
||||
/// center and radius 1.0 is the outer scoring edge. Hits outside radius 1.0
|
||||
/// count as a miss (M, score 0).
|
||||
class ScoreMath {
|
||||
const ScoreMath._();
|
||||
|
||||
/// Returns the score [0..10] for a hit at [normalizedX]/[normalizedY].
|
||||
static int scoreFor(double normalizedX, double normalizedY) {
|
||||
final d = distance(normalizedX, normalizedY);
|
||||
if (d > 1.0) return 0; // miss
|
||||
final ring = (d * 10).floor();
|
||||
var score = 10 - ring;
|
||||
if (score < 1) score = 1;
|
||||
if (score > 10) score = 10;
|
||||
return score;
|
||||
}
|
||||
|
||||
/// Euclidean distance from center for normalized coords.
|
||||
static double distance(double normalizedX, double normalizedY) {
|
||||
final sq = normalizedX * normalizedX + normalizedY * normalizedY;
|
||||
if (sq <= 0) return 0.0;
|
||||
return _sqrt(sq);
|
||||
}
|
||||
|
||||
/// Converts a pixel tap (relative to a square target of [size]) into a
|
||||
/// normalized coordinate in [-1, 1] and returns the resulting [HitPoint].
|
||||
static HitPoint hitPointFromPixels(
|
||||
double localX,
|
||||
double localY,
|
||||
double size, {
|
||||
required int arrowIndex,
|
||||
required int endIndex,
|
||||
}) {
|
||||
final half = size / 2;
|
||||
final nx = (localX - half) / half;
|
||||
final ny = (localY - half) / half;
|
||||
final score = scoreFor(nx, ny);
|
||||
return HitPoint(
|
||||
arrowIndex: arrowIndex,
|
||||
endIndex: endIndex,
|
||||
normalizedX: nx,
|
||||
normalizedY: ny,
|
||||
score: score,
|
||||
isMiss: score == 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _sqrt(double x) {
|
||||
// Newton's method; precise enough for scoring distances.
|
||||
if (x < 0) return double.nan;
|
||||
if (x == 0) return 0.0;
|
||||
var r = x;
|
||||
for (var i = 0; i < 20; i++) {
|
||||
r = (r + x / r) / 2;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../../data/local/models/models.dart';
|
||||
|
||||
/// All local [PointRecord]s (drafts + completed), newest first.
|
||||
final pointRecordsProvider =
|
||||
AsyncNotifierProvider<PointRecordsNotifier, List<PointRecord>>(
|
||||
PointRecordsNotifier.new,
|
||||
);
|
||||
|
||||
/// Loads a single record by id (used by the edit/view page).
|
||||
final recordByIdProvider =
|
||||
FutureProvider.family<PointRecord?, int>((ref, id) async {
|
||||
final isar = await ref.watch(isarProvider.future);
|
||||
return isar.pointRecords.get(id);
|
||||
});
|
||||
|
||||
class PointRecordsNotifier extends AsyncNotifier<List<PointRecord>> {
|
||||
@override
|
||||
Future<List<PointRecord>> build() async {
|
||||
final isar = await ref.watch(isarProvider.future);
|
||||
return isar.pointRecords.where().sortByCreatedAtDesc().findAll();
|
||||
}
|
||||
|
||||
Future<Isar> _isar() => ref.read(isarProvider.future);
|
||||
|
||||
/// Returns the current draft record, if any.
|
||||
Future<PointRecord?> currentDraft() async {
|
||||
final isar = await _isar();
|
||||
return isar.pointRecords
|
||||
.filter()
|
||||
.statusEqualTo(PointRecordStatus.draft)
|
||||
.sortByCreatedAtDesc()
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/// Creates a new draft from scoring parameters and returns it.
|
||||
Future<PointRecord> createDraft({
|
||||
required BowType bowType,
|
||||
required TargetFaceType targetFaceType,
|
||||
required int distanceMeters,
|
||||
required int endCount,
|
||||
required int arrowsPerEnd,
|
||||
String? title,
|
||||
}) async {
|
||||
final isar = await _isar();
|
||||
final draft = PointRecord.createDraft(
|
||||
bowType: bowType,
|
||||
targetFaceType: targetFaceType,
|
||||
distanceMeters: distanceMeters,
|
||||
endCount: endCount,
|
||||
arrowsPerEnd: arrowsPerEnd,
|
||||
title: title,
|
||||
);
|
||||
await isar.writeTxn(() => isar.pointRecords.put(draft));
|
||||
state = AsyncData([draft, ...?state.value]);
|
||||
return draft;
|
||||
}
|
||||
|
||||
/// Replaces a record (used by the edit page to persist ongoing changes).
|
||||
Future<void> upsert(PointRecord record) async {
|
||||
final isar = await _isar();
|
||||
final updated = record..updatedAt = DateTime.now();
|
||||
await isar.writeTxn(() => isar.pointRecords.put(updated));
|
||||
state = AsyncData(state.value?.map((e) => e.id == updated.id ? updated : e).toList() ?? [updated]);
|
||||
}
|
||||
|
||||
/// Marks a draft as completed and persists it. Does NOT touch trial count.
|
||||
Future<PointRecord> complete(PointRecord record) async {
|
||||
final completed = record.markCompleted();
|
||||
final isar = await _isar();
|
||||
await isar.writeTxn(() => isar.pointRecords.put(completed));
|
||||
state = AsyncData(
|
||||
state.value?.map((e) => e.id == completed.id ? completed : e).toList() ?? [completed],
|
||||
);
|
||||
return completed;
|
||||
}
|
||||
|
||||
/// Deletes a single record by id.
|
||||
Future<void> delete(int id) async {
|
||||
final isar = await _isar();
|
||||
await isar.writeTxn(() => isar.pointRecords.delete(id));
|
||||
state = AsyncData(state.value?.where((e) => e.id != id).toList() ?? const []);
|
||||
}
|
||||
|
||||
/// Removes any existing draft records (used when the user picks
|
||||
/// "重新计分" on the home draft-confirmation dialog).
|
||||
Future<void> clearDrafts() async {
|
||||
final isar = await _isar();
|
||||
final drafts = await isar.pointRecords
|
||||
.filter()
|
||||
.statusEqualTo(PointRecordStatus.draft)
|
||||
.findAll();
|
||||
if (drafts.isEmpty) return;
|
||||
await isar.writeTxn(() => isar.pointRecords.deleteAll(drafts.map((e) => e.id).toList()));
|
||||
state = AsyncData(state.value?.where((e) => !drafts.any((d) => d.id == e.id)).toList() ?? const []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user