592 lines
22 KiB
Dart
592 lines
22 KiB
Dart
/// Outil de calibration de la cible.
|
||
///
|
||
/// Permet l'ajustement interactif du centre et du rayon global.
|
||
/// Les anneaux sont répartis proportionnellement.
|
||
library;
|
||
|
||
import 'package:flutter/material.dart';
|
||
import '../../../core/theme/app_theme.dart';
|
||
import '../../../data/models/target_type.dart';
|
||
|
||
class TargetCalibration extends StatefulWidget {
|
||
final double initialCenterX;
|
||
final double initialCenterY;
|
||
final double initialRadius;
|
||
final double initialInnerRadius;
|
||
final int initialRingCount;
|
||
final TargetType targetType;
|
||
final List<double>? initialRingRadii;
|
||
final Function(
|
||
double centerX,
|
||
double centerY,
|
||
double innerRadius,
|
||
double radius,
|
||
int ringCount, {
|
||
List<double>? ringRadii,
|
||
})
|
||
onCalibrationChanged;
|
||
|
||
const TargetCalibration({
|
||
super.key,
|
||
required this.initialCenterX,
|
||
required this.initialCenterY,
|
||
required this.initialRadius,
|
||
required this.initialInnerRadius,
|
||
this.initialRingCount = 10,
|
||
required this.targetType,
|
||
this.initialRingRadii,
|
||
required this.onCalibrationChanged,
|
||
});
|
||
|
||
@override
|
||
State<TargetCalibration> createState() => TargetCalibrationState();
|
||
}
|
||
|
||
class TargetCalibrationState extends State<TargetCalibration> {
|
||
late double _centerX;
|
||
late double _centerY;
|
||
late double _radius;
|
||
late double _innerRadius;
|
||
late int _ringCount;
|
||
late List<double> _ringRadii;
|
||
|
||
// Variable dédiée pour piloter la jauge orange sans conflit
|
||
late double _currentEspacementRatio;
|
||
|
||
bool _isDraggingCenter = false;
|
||
bool _isDraggingRadius = false;
|
||
bool _isDraggingInnerRadius = false;
|
||
|
||
bool _showEspacement = false;
|
||
double _baseRadiusBeforeScale = 1.0;
|
||
|
||
// SAUVEGARDE CRITIQUE : Pour mémoriser la structure d'origine de l'IA
|
||
List<double>? _originalRingRadii;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_centerX = widget.initialCenterX;
|
||
_centerY = widget.initialCenterY;
|
||
_radius = widget.initialRadius;
|
||
_innerRadius = widget.initialInnerRadius;
|
||
_ringCount = widget.initialRingCount;
|
||
|
||
// On mémorise la configuration d'usine de l'image
|
||
if (widget.initialRingRadii != null) {
|
||
_originalRingRadii = List.from(widget.initialRingRadii!);
|
||
}
|
||
|
||
// Initialisation du ratio par défaut
|
||
_currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.70) : 0.1;
|
||
_initRingRadii();
|
||
}
|
||
|
||
/// Fige et propage la calibration courante vers le provider.
|
||
///
|
||
/// Appelée par l'écran d'analyse (via GlobalKey) juste avant de basculer
|
||
/// dans l'instance de Plotting, pour garantir que l'état affiché en Plotting
|
||
/// correspond exactement au dernier réglage validé, sans dépendre d'un
|
||
/// éventuel rebuild intermédiaire.
|
||
void commitCalibration() {
|
||
_notifyChange();
|
||
}
|
||
|
||
void _initRingRadii({bool forceRecalculate = false}) {
|
||
// CORRECTION : Si on ne recalcule pas activement l'espacement linéaire, on préserve en priorité la structure d'origine
|
||
if (!forceRecalculate && _originalRingRadii != null && _originalRingRadii!.length == _ringCount) {
|
||
_ringRadii = List.from(_originalRingRadii!);
|
||
} else if (!forceRecalculate && widget.initialRingRadii != null && widget.initialRingRadii!.length == _ringCount) {
|
||
_ringRadii = List.from(widget.initialRingRadii!);
|
||
} else {
|
||
_ringRadii = List.generate(_ringCount, (i) {
|
||
if (_ringCount <= 1) return 1.0;
|
||
return _currentEspacementRatio + (1.0 - _currentEspacementRatio) * i / (_ringCount - 1);
|
||
});
|
||
}
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(TargetCalibration oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
bool shouldReinit = false;
|
||
|
||
if (widget.initialCenterX != oldWidget.initialCenterX && !_isDraggingCenter) {
|
||
_centerX = widget.initialCenterX;
|
||
}
|
||
if (widget.initialCenterY != oldWidget.initialCenterY && !_isDraggingCenter) {
|
||
_centerY = widget.initialCenterY;
|
||
}
|
||
if (widget.initialRingCount != oldWidget.initialRingCount) {
|
||
_ringCount = widget.initialRingCount;
|
||
shouldReinit = true;
|
||
}
|
||
if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius) {
|
||
_radius = widget.initialRadius;
|
||
shouldReinit = true;
|
||
}
|
||
if (widget.initialInnerRadius != oldWidget.initialInnerRadius && !_isDraggingInnerRadius) {
|
||
_innerRadius = widget.initialInnerRadius;
|
||
_currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.70) : 0.1;
|
||
shouldReinit = true;
|
||
}
|
||
if (widget.initialRingRadii != oldWidget.initialRingRadii && widget.initialRingRadii != null) {
|
||
_originalRingRadii = List.from(widget.initialRingRadii!);
|
||
shouldReinit = true;
|
||
}
|
||
|
||
if (shouldReinit) {
|
||
_initRingRadii();
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final size = constraints.biggest;
|
||
|
||
return Stack(
|
||
children: [
|
||
GestureDetector(
|
||
onScaleStart: (details) {
|
||
_baseRadiusBeforeScale = _radius;
|
||
final tapX = details.localFocalPoint.dx / size.width;
|
||
final tapY = details.localFocalPoint.dy / size.height;
|
||
final distToCenter = _distance(tapX, tapY, _centerX, _centerY);
|
||
|
||
if (distToCenter < 0.05 || distToCenter < _radius + 0.02) {
|
||
setState(() {
|
||
_isDraggingCenter = true;
|
||
});
|
||
}
|
||
},
|
||
onScaleUpdate: (details) => _onScaleUpdate(details, size),
|
||
onScaleEnd: (_) => _onScaleEnd(),
|
||
child: CustomPaint(
|
||
size: size,
|
||
painter: _CalibrationPainter(
|
||
centerX: _centerX,
|
||
centerY: _centerY,
|
||
radius: _radius,
|
||
innerRadius: _innerRadius,
|
||
ringCount: _ringCount,
|
||
ringRadii: _ringRadii,
|
||
targetType: widget.targetType,
|
||
isDraggingCenter: _isDraggingCenter,
|
||
isDraggingRadius: false,
|
||
isDraggingInnerRadius: false,
|
||
),
|
||
),
|
||
),
|
||
|
||
Positioned(
|
||
top: 10,
|
||
left: 40,
|
||
right: 40,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black54,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'Options d\'espacement avancées',
|
||
style: TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.w500),
|
||
),
|
||
SizedBox(
|
||
height: 28,
|
||
child: Switch(
|
||
value: _showEspacement,
|
||
activeThumbColor: const Color(0xFF00FF00),
|
||
onChanged: (bool value) {
|
||
setState(() {
|
||
_showEspacement = value;
|
||
// Quand on désactive l'espacement manuel, on restaure la configuration d'usine !
|
||
if (!value) {
|
||
_initRingRadii(forceRecalculate: false);
|
||
}
|
||
});
|
||
_notifyChange();
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Slider pour la taille (toujours visible)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black54,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text('Taille ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
|
||
const Icon(Icons.zoom_out, color: Colors.white, size: 16),
|
||
Expanded(
|
||
child: Slider(
|
||
// SÉCURITÉ : Ouverture mesurée des bornes pour plus de liberté sans débordement incontrôlé
|
||
value: _radius.clamp(0.3, 0.95),
|
||
min: 0.3,
|
||
max: 0.95,
|
||
activeColor: AppTheme.primaryColor,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
_radius = value;
|
||
_innerRadius = _radius * _currentEspacementRatio;
|
||
// CORRECTION : Si le mode avancé n'est pas coché, on applique la taille pure sans détruire le ratio d'origine
|
||
_initRingRadii(forceRecalculate: _showEspacement);
|
||
});
|
||
_notifyChange();
|
||
},
|
||
),
|
||
),
|
||
const Icon(Icons.zoom_in, color: Colors.white, size: 16),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Affichage conditionnel du slider d'espacement orange
|
||
if (_showEspacement) ...[
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black54,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text('Espacement ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
|
||
const Icon(Icons.compress, color: Colors.white, size: 16),
|
||
Expanded(
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Slider(
|
||
value: _currentEspacementRatio,
|
||
min: 0.01,
|
||
// SÉCURITÉ : Écartement max bridé à 0.70 pour confiner le dernier cercle
|
||
max: 0.70,
|
||
activeColor: Colors.orange,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
_currentEspacementRatio = value;
|
||
_innerRadius = _radius * _currentEspacementRatio;
|
||
_initRingRadii(forceRecalculate: true);
|
||
});
|
||
_notifyChange();
|
||
},
|
||
),
|
||
),
|
||
const Icon(Icons.expand, color: Colors.white, size: 16),
|
||
],
|
||
),
|
||
),
|
||
// CORRECTION DU BOUTON RESET : Restaure désormais le vrai profil d'usine de l'IA
|
||
IconButton(
|
||
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
|
||
onPressed: () {
|
||
setState(() {
|
||
if (_originalRingRadii != null) {
|
||
_initRingRadii(forceRecalculate: false);
|
||
if (_ringRadii.isNotEmpty) {
|
||
_currentEspacementRatio = _ringRadii.first;
|
||
_innerRadius = _radius * _currentEspacementRatio;
|
||
}
|
||
} else {
|
||
_currentEspacementRatio = 0.1;
|
||
_innerRadius = _radius * _currentEspacementRatio;
|
||
_initRingRadii(forceRecalculate: true);
|
||
}
|
||
});
|
||
_notifyChange();
|
||
},
|
||
tooltip: 'Réinitialiser l\'espacement',
|
||
constraints: const BoxConstraints(),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget buildDirectionalControls(BuildContext context, Size size) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Colors.white10),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildSignLabel('−'),
|
||
const SizedBox(width: 12),
|
||
Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildDirectionButton(Icons.keyboard_arrow_up, () => _moveCenterByPixels(0, -1, size)),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildDirectionButton(Icons.keyboard_arrow_left, () => _moveCenterByPixels(-1, 0, size)),
|
||
const SizedBox(width: 40),
|
||
_buildDirectionButton(Icons.keyboard_arrow_right, () => _moveCenterByPixels(1, 0, size)),
|
||
],
|
||
),
|
||
_buildDirectionButton(Icons.keyboard_arrow_down, () => _moveCenterByPixels(0, 1, size)),
|
||
],
|
||
),
|
||
const SizedBox(width: 12),
|
||
_buildSignLabel('+'),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// Symbole purement décoratif affiché de part et d'autre de la croix.
|
||
Widget _buildSignLabel(String text) {
|
||
return Text(
|
||
text,
|
||
style: const TextStyle(color: Colors.white54, fontSize: 28, fontWeight: FontWeight.bold),
|
||
);
|
||
}
|
||
|
||
Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) {
|
||
return Container(
|
||
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)),
|
||
margin: const EdgeInsets.all(2),
|
||
child: IconButton(
|
||
icon: Icon(icon, color: Colors.white, size: 28),
|
||
onPressed: onPressed,
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _notifyChange() {
|
||
widget.onCalibrationChanged(
|
||
_centerX,
|
||
_centerY,
|
||
_innerRadius,
|
||
_radius,
|
||
_ringCount,
|
||
ringRadii: _ringRadii,
|
||
);
|
||
}
|
||
|
||
void _moveCenterByPixels(double dx, double dy, Size size) {
|
||
setState(() {
|
||
_centerX = _centerX + (dx / size.width);
|
||
_centerY = _centerY + (dy / size.height);
|
||
});
|
||
_notifyChange();
|
||
}
|
||
|
||
void _onScaleUpdate(ScaleUpdateDetails details, Size size) {
|
||
setState(() {
|
||
if (details.pointerCount == 2) {
|
||
_radius = (_baseRadiusBeforeScale * details.scale).clamp(0.3, 0.95);
|
||
_innerRadius = _radius * _currentEspacementRatio;
|
||
_initRingRadii(forceRecalculate: _showEspacement);
|
||
} else if (_isDraggingCenter) {
|
||
final deltaX = details.focalPointDelta.dx / size.width;
|
||
final deltaY = details.focalPointDelta.dy / size.height;
|
||
_centerX = _centerX + deltaX;
|
||
_centerY = _centerY + deltaY;
|
||
}
|
||
});
|
||
_notifyChange();
|
||
}
|
||
|
||
void _onScaleEnd() {
|
||
setState(() {
|
||
_isDraggingCenter = false;
|
||
_isDraggingRadius = false;
|
||
_isDraggingInnerRadius = false;
|
||
});
|
||
}
|
||
|
||
double _distance(double x1, double y1, double x2, double y2) {
|
||
final dx = x1 - x2;
|
||
final dy = y1 - y2;
|
||
return (dx * dx + dy * dy);
|
||
}
|
||
}
|
||
|
||
class _CalibrationPainter extends CustomPainter {
|
||
final double centerX;
|
||
final double centerY;
|
||
final double radius;
|
||
final double innerRadius;
|
||
final int ringCount;
|
||
final List<double> ringRadii;
|
||
final TargetType targetType;
|
||
final bool isDraggingCenter;
|
||
final bool isDraggingRadius;
|
||
final bool isDraggingInnerRadius;
|
||
|
||
_CalibrationPainter({
|
||
required this.centerX,
|
||
required this.centerY,
|
||
required this.radius,
|
||
required this.innerRadius,
|
||
required this.ringCount,
|
||
required this.ringRadii,
|
||
required this.targetType,
|
||
required this.isDraggingCenter,
|
||
required this.isDraggingRadius,
|
||
required this.isDraggingInnerRadius,
|
||
});
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final centerPx = Offset(centerX * size.width, centerY * size.height);
|
||
final minDim = size.width < size.height ? size.width : size.height;
|
||
final baseRadiusPx = radius * minDim;
|
||
|
||
if (targetType == TargetType.concentric) {
|
||
_drawConcentricZones(canvas, size, centerPx, baseRadiusPx);
|
||
} else {
|
||
_drawSilhouetteZones(canvas, size, centerPx, baseRadiusPx);
|
||
}
|
||
|
||
if (isDraggingCenter) {
|
||
final crosshairLinePaint = Paint()..color = Colors.red.withValues(alpha: 0.5)..strokeWidth = 1;
|
||
canvas.drawLine(Offset(0, centerPx.dy), Offset(size.width, centerPx.dy), crosshairLinePaint);
|
||
canvas.drawLine(Offset(centerPx.dx, 0), Offset(centerPx.dx, size.height), crosshairLinePaint);
|
||
}
|
||
|
||
_drawCenterHandle(canvas, centerPx);
|
||
_drawInstructions(canvas, size);
|
||
}
|
||
|
||
void _drawConcentricZones(Canvas canvas, Size size, Offset center, double baseRadius) {
|
||
List<Color> zoneColors = [];
|
||
for (int i = 0; i < ringCount; i++) {
|
||
final ratio = i / ringCount;
|
||
if (ratio < 0.2) {
|
||
zoneColors.add(Colors.yellow.withValues(alpha: 0.3 - ratio * 0.5));
|
||
} else if (ratio < 0.4) {
|
||
zoneColors.add(Colors.orange.withValues(alpha: 0.25 - ratio * 0.3));
|
||
} else if (ratio < 0.6) {
|
||
zoneColors.add(Colors.blue.withValues(alpha: 0.2 - ratio * 0.2));
|
||
} else if (ratio < 0.8) {
|
||
zoneColors.add(Colors.green.withValues(alpha: 0.15 - ratio * 0.1));
|
||
} else {
|
||
zoneColors.add(Colors.white.withValues(alpha: 0.1));
|
||
}
|
||
}
|
||
|
||
final zonePaint = Paint()..style = PaintingStyle.fill;
|
||
final strokePaint = Paint()
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = 1
|
||
..color = Colors.red.withValues(alpha: 0.8);
|
||
|
||
for (int i = ringCount - 1; i >= 0; i--) {
|
||
final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount;
|
||
final zoneRadius = baseRadius * ringRadius;
|
||
|
||
zonePaint.color = zoneColors[i];
|
||
canvas.drawCircle(center, zoneRadius, zonePaint);
|
||
canvas.drawCircle(center, zoneRadius, strokePaint);
|
||
}
|
||
|
||
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||
|
||
for (int i = 0; i < ringCount; i++) {
|
||
final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount;
|
||
final textSpanRatio = i > 0 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0;
|
||
final zoneRadius = baseRadius * (ringRadius + textSpanRatio) / 2;
|
||
|
||
final score = 10 - i;
|
||
final labelX = center.dx + zoneRadius;
|
||
if (labelX < 0 || labelX > size.width) continue;
|
||
|
||
textPainter.text = TextSpan(
|
||
text: '$score',
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.9),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
shadows: const [Shadow(color: Colors.black, blurRadius: 2)],
|
||
),
|
||
);
|
||
textPainter.layout();
|
||
|
||
final labelY = center.dy - textPainter.height / 2;
|
||
if (labelY >= 0 && labelY <= size.height) {
|
||
textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY));
|
||
}
|
||
}
|
||
}
|
||
|
||
void _drawSilhouetteZones(Canvas canvas, Size size, Offset center, double radius) {
|
||
final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2;
|
||
final silhouetteWidth = radius * 0.8;
|
||
final silhouetteHeight = radius * 2;
|
||
|
||
paint.color = Colors.green.withValues(alpha: 0.5);
|
||
canvas.drawRect(Rect.fromCenter(center: center, width: silhouetteWidth, height: silhouetteHeight), paint);
|
||
}
|
||
|
||
void _drawCenterHandle(Canvas canvas, Offset center) {
|
||
final outerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.stroke..strokeWidth = 3;
|
||
canvas.drawCircle(center, 15, outerPaint);
|
||
|
||
final innerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.fill;
|
||
canvas.drawCircle(center, 5, innerPaint);
|
||
|
||
final crossPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..strokeWidth = 2;
|
||
canvas.drawLine(Offset(center.dx - 20, center.dy), Offset(center.dx - 8, center.dy), crossPaint);
|
||
canvas.drawLine(Offset(center.dx + 8, center.dy), Offset(center.dx + 20, center.dy), crossPaint);
|
||
canvas.drawLine(Offset(center.dx, center.dy - 20), Offset(center.dx, center.dy - 8), crossPaint);
|
||
canvas.drawLine(Offset(center.dx, center.dy + 4), Offset(center.dx, center.dy + 20), crossPaint);
|
||
}
|
||
|
||
void _drawInstructions(Canvas canvas, Size size) {
|
||
const instruction = 'Deplacez le centre ou ajustez le rayon';
|
||
final textPainter = TextPainter(
|
||
text: TextSpan(text: instruction, style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 12, backgroundColor: Colors.black54)),
|
||
textDirection: TextDirection.ltr,
|
||
);
|
||
textPainter.layout();
|
||
textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, size.height - 30));
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||
if (oldDelegate is! _CalibrationPainter) return true;
|
||
return centerX != oldDelegate.centerX ||
|
||
centerY != oldDelegate.centerY ||
|
||
radius != oldDelegate.radius ||
|
||
innerRadius != oldDelegate.innerRadius ||
|
||
ringCount != oldDelegate.ringCount ||
|
||
isDraggingCenter != oldDelegate.isDraggingCenter ||
|
||
isDraggingRadius != oldDelegate.isDraggingRadius ||
|
||
isDraggingInnerRadius != oldDelegate.isDraggingInnerRadius ||
|
||
ringRadii != oldDelegate.ringRadii;
|
||
}
|
||
} |