Update Saqel Platform: 2026-09-05 22:17:15
This commit is contained in:
+282
-171
@@ -532,18 +532,18 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(30),
|
||||
color: const Color(0xFF1E3A8A).withAlpha(140),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.appleBlue.withAlpha(90)),
|
||||
border: Border.all(color: const Color(0xFF60A5FA).withAlpha(160)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.speaker_2_fill, color: AppColors.appleBlue, size: 12),
|
||||
SizedBox(width: 4),
|
||||
Text('🇬🇧', style: TextStyle(fontSize: 12)),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
'صوتيات ذكية (IPA & TTS)',
|
||||
style: TextStyle(color: AppColors.appleBlue, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
'صوت بريطاني فصيح (en-GB RP)',
|
||||
style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -929,27 +929,29 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
// ============================================================================
|
||||
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (IPA & TTS)
|
||||
// ============================================================================
|
||||
// ============================================================================
|
||||
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (BRITISH RP & TTS)
|
||||
// ============================================================================
|
||||
Widget _buildPhoneticsStudioView() {
|
||||
final activeUnit = _vocabUnits[_selectedVocabUnitIndex];
|
||||
final words = activeUnit['words'] as List;
|
||||
final activeWord = words[_selectedWordIndex] as Map<String, dynamic>;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Units Selector & Words List
|
||||
Container(
|
||||
width: 280,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: Column(
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isCompact = constraints.maxWidth < 760;
|
||||
|
||||
if (isCompact) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Unit Dropdown
|
||||
// Unit selector dropdown
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
@@ -961,7 +963,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
value: i,
|
||||
child: Text(
|
||||
_vocabUnits[i]['unit'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -976,171 +978,280 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Words List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: words.length,
|
||||
itemBuilder: (context, index) {
|
||||
// Words chips list
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(words.length, (index) {
|
||||
final item = words[index];
|
||||
final isSelected = _selectedWordIndex == index;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(item['word'] as String),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.saqelCyan,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
item['word'] as String,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
item['ar'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
|
||||
onPressed: () => _speakWord(item['word'] as String),
|
||||
),
|
||||
onTap: () => setState(() => _selectedWordIndex = index),
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
onSelected: (_) => setState(() => _selectedWordIndex = index),
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Phonetics Card
|
||||
_buildPhoneticsWorkbenchCard(activeWord),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Units Selector & Words List
|
||||
Container(
|
||||
width: 280,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Unit Dropdown
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: _selectedVocabUnitIndex,
|
||||
isExpanded: true,
|
||||
dropdownColor: AppColors.darkSurface,
|
||||
items: List.generate(_vocabUnits.length, (i) {
|
||||
return DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(
|
||||
_vocabUnits[i]['unit'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() {
|
||||
_selectedVocabUnitIndex = val;
|
||||
_selectedWordIndex = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Words List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: words.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = words[index];
|
||||
final isSelected = _selectedWordIndex == index;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
item['word'] as String,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
item['ar'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
|
||||
onPressed: () => _speakWord(item['word'] as String),
|
||||
),
|
||||
onTap: () => setState(() => _selectedWordIndex = index),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Phonetics Audio Workbench
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: _buildPhoneticsWorkbenchCard(activeWord),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneticsWorkbenchCard(Map<String, dynamic> activeWord) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Big Pronunciation Header Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0B192C), Color(0xFF1E293B)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
activeWord['pos'] as String,
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E3A8A).withAlpha(160),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF60A5FA).withAlpha(120)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('🇬🇧', style: TextStyle(fontSize: 12)),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'British RP',
|
||||
style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
activeWord['word'] as String,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
activeWord['ipa'] as String,
|
||||
style: const TextStyle(
|
||||
color: AppColors.saqelCyan,
|
||||
fontSize: 19,
|
||||
fontFamily: 'Courier',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
activeWord['ar'] as String,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Audio Play Button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(
|
||||
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
_isPlayingAudio ? 'جاري النطق البريطاني...' : 'استمع للنطق البريطاني الفصيح (British Audio Pronunciation)',
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
onPressed: () => _speakWord(activeWord['word'] as String),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Phonetics Audio Workbench
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Big Pronunciation Header Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0D1B2A), Color(0xFF1B263B)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
// Detailed Definition & Collocations
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
|
||||
const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
activeWord['pos'] as String,
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
activeWord['word'] as String,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
activeWord['ipa'] as String,
|
||||
style: const TextStyle(
|
||||
color: AppColors.saqelCyan,
|
||||
fontSize: 18,
|
||||
fontFamily: 'Courier',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
activeWord['ar'] as String,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Audio Play Button
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(
|
||||
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
_isPlayingAudio ? 'جاري النطق...' : 'استمع للنطق الصوتي النقي (Audio Pronunciation)',
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
onPressed: () => _speakWord(activeWord['word'] as String),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
|
||||
onPressed: () => _speakWord(activeWord['sentence'] as String),
|
||||
tooltip: 'استمع للجملة',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
// Detailed Definition & Collocations
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
|
||||
const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
|
||||
onPressed: () => _speakWord(activeWord['sentence'] as String),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
|
||||
const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+325
-287
@@ -431,292 +431,315 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
final activeEx = _textbookExercises[_selectedExerciseIndex];
|
||||
final points = activeEx['points'] as List<Offset>;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Cartesian Graph Canvas
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF070C16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMobile = constraints.maxWidth < 720;
|
||||
|
||||
if (isMobile) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 330,
|
||||
child: _buildCanvasContainer(points),
|
||||
),
|
||||
_buildControlsPanel(),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _CartesianCanvasPainter(
|
||||
curve1Type: _curve1Type,
|
||||
radiusSquared: _radiusSquared,
|
||||
a1: _a1,
|
||||
c1: _c1,
|
||||
curve2Type: _curve2Type,
|
||||
m: _m,
|
||||
b: _b,
|
||||
c2: _c2,
|
||||
scale: _scale,
|
||||
intersectionPoints: points,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Controls Overlay (Zoom)
|
||||
Positioned(
|
||||
top: 14,
|
||||
left: 14,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(180),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
|
||||
onPressed: () => setState(() => _scale = (_scale + 4).clamp(12.0, 45.0)),
|
||||
tooltip: 'تكبير',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
|
||||
onPressed: () => setState(() => _scale = (_scale - 4).clamp(12.0, 45.0)),
|
||||
tooltip: 'تصغير',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
|
||||
onPressed: () => setState(() => _scale = 22.0),
|
||||
tooltip: 'إعادة ضبط المحاور',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: _buildCanvasContainer(points),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _buildControlsPanel(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Curves Legend Badge
|
||||
Positioned(
|
||||
bottom: 14,
|
||||
right: 14,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 12, height: 3, color: AppColors.saqelCyan),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_curve1Type == 0
|
||||
? 'الدائرة: x² + y² = ${_radiusSquared.toStringAsFixed(1)}'
|
||||
: 'القطع المكافئ 1: y = ${_a1.toStringAsFixed(1)}x² + ${_c1.toStringAsFixed(1)}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 12, height: 3, color: AppColors.guardianAmber),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_curve2Type == 0
|
||||
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
|
||||
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
|
||||
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget _buildCanvasContainer(List<Offset> points) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF070C16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _CartesianCanvasPainter(
|
||||
curve1Type: _curve1Type,
|
||||
radiusSquared: _radiusSquared,
|
||||
a1: _a1,
|
||||
c1: _c1,
|
||||
curve2Type: _curve2Type,
|
||||
m: _m,
|
||||
b: _b,
|
||||
c2: _c2,
|
||||
scale: _scale,
|
||||
intersectionPoints: points,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Sliders & Controls Panel
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16, bottom: 16, left: 16),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
// Controls Overlay (Zoom)
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: 10,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(180),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
|
||||
onPressed: () => setState(() => _scale = (_scale + 4).clamp(10.0, 50.0)),
|
||||
tooltip: 'تكبير',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
|
||||
onPressed: () => setState(() => _scale = (_scale - 4).clamp(10.0, 50.0)),
|
||||
tooltip: 'تصغير',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
|
||||
onPressed: () => setState(() => _scale = 22.0),
|
||||
tooltip: 'إعادة ضبط المحاور',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
// Curves Legend Badge
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(210),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 10, height: 3, color: AppColors.saqelCyan),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_curve1Type == 0
|
||||
? 'الدائرة: x² + y² = ${_radiusSquared.toStringAsFixed(1)}'
|
||||
: 'القطع المكافئ 1: y = ${_a1.toStringAsFixed(1)}x² + ${_c1.toStringAsFixed(1)}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 10, height: 3, color: AppColors.guardianAmber),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_curve2Type == 0
|
||||
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
|
||||
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
|
||||
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 10.5, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlsPanel() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'لوحة المعاملات الحية 🎛️',
|
||||
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 20),
|
||||
|
||||
// Curve 1 controls
|
||||
Text(
|
||||
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
),
|
||||
if (_curve1Type == 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _radiusSquared,
|
||||
min: 1.0,
|
||||
max: 25.0,
|
||||
divisions: 24,
|
||||
activeColor: AppColors.saqelCyan,
|
||||
onChanged: (val) => setState(() => _radiusSquared = val),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 8),
|
||||
// Curve 2 controls
|
||||
Text(
|
||||
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
|
||||
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
),
|
||||
if (_curve2Type == 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _m,
|
||||
min: -5.0,
|
||||
max: 5.0,
|
||||
divisions: 20,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _m = val),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _b,
|
||||
min: -10.0,
|
||||
max: 10.0,
|
||||
divisions: 40,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _b = val),
|
||||
),
|
||||
] else ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _c2,
|
||||
min: -12.0,
|
||||
max: 6.0,
|
||||
divisions: 36,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _c2 = val),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// Quick Textbook Presets Buttons
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
|
||||
final activeLesson = activeLessons[_selectedLessonIndex];
|
||||
final lessonExercises = activeLesson['exercises'] as List<int>;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'لوحة المعاملات الحية 🎛️',
|
||||
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
|
||||
// Curve 1 controls
|
||||
Text(
|
||||
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
'تدريبات (${activeLesson['title']}):',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
),
|
||||
if (_curve1Type == 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _radiusSquared,
|
||||
min: 1.0,
|
||||
max: 25.0,
|
||||
divisions: 24,
|
||||
activeColor: AppColors.saqelCyan,
|
||||
onChanged: (val) => setState(() => _radiusSquared = val),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// Curve 2 controls
|
||||
Text(
|
||||
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
|
||||
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
),
|
||||
if (_curve2Type == 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _m,
|
||||
min: -5.0,
|
||||
max: 5.0,
|
||||
divisions: 20,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _m = val),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _b,
|
||||
min: -10.0,
|
||||
max: 10.0,
|
||||
divisions: 40,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _b = val),
|
||||
),
|
||||
] else ...[
|
||||
Row(
|
||||
children: [
|
||||
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
|
||||
const Spacer(),
|
||||
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _c2,
|
||||
min: -12.0,
|
||||
max: 6.0,
|
||||
divisions: 36,
|
||||
activeColor: AppColors.guardianAmber,
|
||||
onChanged: (val) => setState(() => _c2 = val),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
// Quick Textbook Presets Buttons
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
|
||||
final activeLesson = activeLessons[_selectedLessonIndex];
|
||||
final lessonExercises = activeLesson['exercises'] as List<int>;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'تدريبات (${activeLesson['title']}):',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: lessonExercises.map((i) {
|
||||
final isSelected = _selectedExerciseIndex == i;
|
||||
return ChoiceChip(
|
||||
label: Text(
|
||||
i == 0 ? 'نشاط ص16' : 'تدريب $i',
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: lessonExercises.map((i) {
|
||||
final isSelected = _selectedExerciseIndex == i;
|
||||
return ChoiceChip(
|
||||
label: Text(
|
||||
i == 0 ? 'نشاط ص16' : 'تدريب $i',
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.saqelCyan,
|
||||
backgroundColor: AppColors.darkCard,
|
||||
onSelected: (_) => _applyTextbookPreset(i),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.saqelCyan,
|
||||
backgroundColor: AppColors.darkCard,
|
||||
onSelected: (_) => _applyTextbookPreset(i),
|
||||
);
|
||||
},
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1001,18 +1024,20 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (size.width <= 0 || size.height <= 0) return;
|
||||
final cx = size.width / 2;
|
||||
final cy = size.height / 2;
|
||||
final safeScale = scale.clamp(6.0, 60.0);
|
||||
|
||||
// 1. Draw Grid Lines
|
||||
final gridPaint = Paint()
|
||||
..color = const Color(0xFF1B263B)
|
||||
..strokeWidth = 0.8;
|
||||
|
||||
for (double x = cx % scale; x < size.width; x += scale) {
|
||||
for (double x = cx % safeScale; x < size.width; x += safeScale) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
|
||||
}
|
||||
for (double y = cy % scale; y < size.height; y += scale) {
|
||||
for (double y = cy % safeScale; y < size.height; y += safeScale) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
|
||||
}
|
||||
|
||||
@@ -1032,16 +1057,20 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
if (curve1Type == 0) {
|
||||
// Circle: x² + y² = r²
|
||||
final rPixel = math.sqrt(radiusSquared) * scale;
|
||||
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint);
|
||||
final safeR2 = radiusSquared > 0 ? radiusSquared : 0.0;
|
||||
final rPixel = math.sqrt(safeR2) * safeScale;
|
||||
if (rPixel.isFinite && rPixel > 0) {
|
||||
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint);
|
||||
}
|
||||
} else {
|
||||
// Parabola: y = a1*x² + c1
|
||||
final path = Path();
|
||||
bool started = false;
|
||||
for (double px = 0; px <= size.width; px += 2) {
|
||||
final mathX = (px - cx) / scale;
|
||||
final mathX = (px - cx) / safeScale;
|
||||
final mathY = a1 * mathX * mathX + c1;
|
||||
final py = cy - mathY * scale;
|
||||
final py = cy - mathY * safeScale;
|
||||
if (!py.isFinite) continue;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
@@ -1049,7 +1078,9 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, curve1Paint);
|
||||
if (started) {
|
||||
canvas.drawPath(path, curve1Paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Draw Curve 2 (Amber)
|
||||
@@ -1060,23 +1091,26 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
if (curve2Type == 0) {
|
||||
// Line: y = m*x + b
|
||||
final x1 = (0 - cx) / scale;
|
||||
final x1 = (0 - cx) / safeScale;
|
||||
final y1 = m * x1 + b;
|
||||
final py1 = cy - y1 * scale;
|
||||
final py1 = cy - y1 * safeScale;
|
||||
|
||||
final x2 = (size.width - cx) / scale;
|
||||
final x2 = (size.width - cx) / safeScale;
|
||||
final y2 = m * x2 + b;
|
||||
final py2 = cy - y2 * scale;
|
||||
final py2 = cy - y2 * safeScale;
|
||||
|
||||
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
|
||||
if (py1.isFinite && py2.isFinite) {
|
||||
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
|
||||
}
|
||||
} else {
|
||||
// Parabola: y = x² + c2
|
||||
final path = Path();
|
||||
bool started = false;
|
||||
for (double px = 0; px <= size.width; px += 2) {
|
||||
final mathX = (px - cx) / scale;
|
||||
final mathX = (px - cx) / safeScale;
|
||||
final mathY = mathX * mathX + c2;
|
||||
final py = cy - mathY * scale;
|
||||
final py = cy - mathY * safeScale;
|
||||
if (!py.isFinite) continue;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
@@ -1084,7 +1118,9 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, curve2Paint);
|
||||
if (started) {
|
||||
canvas.drawPath(path, curve2Paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Draw Intersection Points (Red Glow)
|
||||
@@ -1097,8 +1133,10 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
for (var pt in intersectionPoints) {
|
||||
final px = cx + pt.dx * scale;
|
||||
final py = cy - pt.dy * scale;
|
||||
if (!pt.dx.isFinite || !pt.dy.isFinite) continue;
|
||||
final px = cx + pt.dx * safeScale;
|
||||
final py = cy - pt.dy * safeScale;
|
||||
if (!px.isFinite || !py.isFinite) continue;
|
||||
|
||||
canvas.drawCircle(Offset(px, py), 9.0, ptGlow);
|
||||
canvas.drawCircle(Offset(px, py), 5.0, ptPaint);
|
||||
|
||||
+1678
-412
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user