diff --git a/apps/student_app/lib/core/services/english_reading_service.dart b/apps/student_app/lib/core/services/english_reading_service.dart index 5ac2c46..f817d1a 100644 --- a/apps/student_app/lib/core/services/english_reading_service.dart +++ b/apps/student_app/lib/core/services/english_reading_service.dart @@ -49,30 +49,49 @@ class EnglishReadingService { if (_isInitialized) return; try { - await _tts.setLanguage('en-US'); - await _tts.setSpeechRate(0.44); // Standard natural reading speed for learners + await _tts.setLanguage('en-GB'); + await _tts.setSpeechRate(0.40); // British English natural learner cadence await _tts.setVolume(1.0); await _tts.setPitch(1.0); - // Attempt to find and select enhanced/natural high-fidelity voice + // Select highest fidelity British English (en-GB / RP) voice try { final voices = await _tts.getVoices; if (voices is List) { + Map? preferredVoice; + Map? fallbackGbVoice; + for (var v in voices) { if (v is Map) { final name = (v['name'] ?? '').toString().toLowerCase(); final locale = (v['locale'] ?? '').toString().toLowerCase(); - if ((locale.contains('en-us') || locale.contains('en_us')) && - (name.contains('natural') || name.contains('enhanced') || name.contains('samantha') || name.contains('neural'))) { - await _tts.setVoice({'name': v['name'], 'locale': v['locale']}); - AppLogger.log('Selected high-quality natural voice: ${v['name']}', tag: 'ENGLISH_AUDIO'); - break; + + if (locale.contains('en-gb') || locale.contains('en_gb')) { + fallbackGbVoice ??= v; + if (name.contains('natural') || + name.contains('enhanced') || + name.contains('neural') || + name.contains('premium') || + name.contains('oliver') || + name.contains('george') || + name.contains('serena') || + name.contains('daniel') || + name.contains('kate')) { + preferredVoice = v; + break; + } } } } + + final selectedVoice = preferredVoice ?? fallbackGbVoice; + if (selectedVoice != null) { + await _tts.setVoice({'name': selectedVoice['name'], 'locale': selectedVoice['locale']}); + AppLogger.log('Selected British English (en-GB) voice: ${selectedVoice['name']}', tag: 'ENGLISH_AUDIO'); + } } } catch (e) { - AppLogger.log('Voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO'); + AppLogger.log('British voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO'); } _tts.setStartHandler(() { @@ -104,7 +123,7 @@ class EnglishReadingService { } } - /// قراءة فقرة محددة مع إيقاف أي قراءة جارية + /// قراءة فقرة محددة بالنموذج البريطاني النقي مع إيقاف أي قراءة جارية Future readParagraph({ required String paragraphId, required String text, @@ -134,11 +153,12 @@ class EnglishReadingService { return; } - await _tts.setSpeechRate(0.44); + await _tts.setLanguage('en-GB'); + await _tts.setSpeechRate(0.40); await _tts.speak(cleanText); } - /// نطق كلمة واحدة مفردة بوضوح صوتي نقي + /// نطق كلمة واحدة مفردة بمخارج حروف دقيقة جداً (British RP Articulation) Future speakWord(String word) async { await initialize(); if (word.trim().contains(' ')) { @@ -149,11 +169,12 @@ class EnglishReadingService { if (cleanWord.isEmpty) return; await _tts.stop(); - await _tts.setSpeechRate(0.38); // Slightly slower for distinct phonetic articulation + await _tts.setLanguage('en-GB'); + await _tts.setSpeechRate(0.35); // Slowed down slightly for clear phonetic articulation await _tts.speak(cleanWord); } - /// نطق جملة أو عبارة تعليمية بوضوح وصوت طبيعي + /// نطق جملة أو عبارة تعليمية باللكنة البريطانية المعتمدة Future speakSentence(String text) async { await initialize(); final cleanText = text @@ -163,7 +184,8 @@ class EnglishReadingService { if (cleanText.isEmpty) return; await _tts.stop(); - await _tts.setSpeechRate(0.44); + await _tts.setLanguage('en-GB'); + await _tts.setSpeechRate(0.40); await _tts.speak(cleanText); } diff --git a/apps/student_app/lib/presentation/screens/curriculum/english_interactive_lab_view.dart b/apps/student_app/lib/presentation/screens/curriculum/english_interactive_lab_view.dart index 33921a6..a706b32 100644 --- a/apps/student_app/lib/presentation/screens/curriculum/english_interactive_lab_view.dart +++ b/apps/student_app/lib/presentation/screens/curriculum/english_interactive_lab_view.dart @@ -532,18 +532,18 @@ class _EnglishInteractiveLabViewState extends State 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 // ============================================================================ // 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; - 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( @@ -961,7 +963,7 @@ class _EnglishInteractiveLabViewState extends State 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 ), ), ), + 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( + 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 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)), + ], ), ), ], diff --git a/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart b/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart index 86fd0e7..bad4caa 100644 --- a/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart +++ b/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart @@ -431,292 +431,315 @@ class _MathInteractiveLabViewState extends State final activeEx = _textbookExercises[_selectedExerciseIndex]; final points = activeEx['points'] as List; - 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 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>; + final activeLesson = activeLessons[_selectedLessonIndex]; + final lessonExercises = activeLesson['exercises'] as List; + + 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>; - final activeLesson = activeLessons[_selectedLessonIndex]; - final lessonExercises = activeLesson['exercises'] as List; - - 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); diff --git a/apps/student_app/lib/presentation/screens/curriculum/physics_interactive_lab_view.dart b/apps/student_app/lib/presentation/screens/curriculum/physics_interactive_lab_view.dart index dfe3d63..8ab79fc 100644 --- a/apps/student_app/lib/presentation/screens/curriculum/physics_interactive_lab_view.dart +++ b/apps/student_app/lib/presentation/screens/curriculum/physics_interactive_lab_view.dart @@ -5,17 +5,20 @@ import '../../../core/theme/app_colors.dart'; import '../../widgets/luxury_widgets.dart'; /// ============================================================================== -/// SAQEL ENTERPRISE (EDTECH 2.0) - SMART PHYSICS & STEM VIRTUAL LAB (120 FPS) +/// SAQEL ENTERPRISE (EDTECH 2.0) - GRADE 10 PHYSICS VIRTUAL LABORATORY (120 FPS) /// ============================================================================== /// -/// ملف: physics_interactive_lab_view.dart -/// الهدف المعماري: -/// بيئة المختبر التفاعلي الذكي لمادة الفيزياء والعلوم: -/// 1. تجسيد المفاهيم الفيزيائية بمحاكيات Canvas ناعمة (120fps) تعمل محلياً على كافة المنصات. -/// 2. محاكي جمع وتحليل المتجهات وحساب المحصلة والضرب القياسي والمتجهي لحظياً. -/// 3. محاكي الحركة في بعد واحد والسرعة والتسارع مع متجهات الحركة الآنية. -/// 4. علامة مائية مضمنة غير قابلة للنسخ لحفظ حقوق منصة صَقِل. -/// 5. فحوصات وتحديات سقراطية مباشرة لتشجيع التفكير التحليلي. +/// مختبر الفيزياء التفاعلي الشامل للصف العاشر الأساسي (المملكة الأردنية الهاشمية): +/// الوحدة الأولى: المتجهات (Vectors) +/// - الدرس 1: الكميات القياسية والكميات المتجهة +/// - الدرس 2: جمع المتجهات وطرحها (بيانياً وتحليلياً) +/// - الدرس 3: ضرب المتجهات (الضرب القياسي والمتجهي) +/// - إثراء الوحدة 1: الوعاء المغناطيسي وقوة لورنتز (F = q(v × B)) +/// الوحدة الثانية: الحركة والقوى (Motion & Forces) +/// - الدرس 1: الحركة في بُعد واحد بتسارع ثابت ومعادلات الحركة +/// - الدرس 2: حركة المقذوفات في بُعدين (Projectile Motion) +/// - الدرس 3: قوانين نيوتن في الحركة والقوة المحصلة (F = ma) +/// - إثراء الوحدة 2: فيزياء حوادث السيارات وأحزمة الأمان والقصور الذاتي (Δp = F Δt) class PhysicsInteractiveLabView extends StatefulWidget { final String? initialSimulationSlug; final VoidCallback? onOpenFullHtmlLab; @@ -30,68 +33,171 @@ class PhysicsInteractiveLabView extends StatefulWidget { State createState() => _PhysicsInteractiveLabViewState(); } -class _PhysicsInteractiveLabViewState extends State with SingleTickerProviderStateMixin { - int _selectedSimIndex = 0; // 0 = Vectors Lab, 1 = 1D Motion Lab +class _PhysicsInteractiveLabViewState extends State + with TickerProviderStateMixin { + int _selectedUnit = 0; // 0 = Unit 1 (Vectors), 1 = Unit 2 (Motion & Forces) + int _selectedLesson = 0; // 0..3 within selected unit - // Vector Lab Parameters - double _magA = 100.0; - double _angA = 30.0; - double _magB = 80.0; - double _angB = 120.0; - bool _showResultant = true; + // --------------------------------------------------------------------------- + // UNIT 1 STATE: VECTORS + // --------------------------------------------------------------------------- + // Lesson 1: Scalar vs Vector + double _u1l1Mag = 80.0; + double _u1l1Angle = 45.0; + int _u1l1QuantityType = 0; // 0 = Vector (Displacement/Velocity), 1 = Scalar (Mass/Time) - // 1D Motion Lab Parameters - double _v0 = 10.0; - double _a = -2.0; + // Lesson 2: Vector Addition & Subtraction + double _magA = 90.0; + double _angA = 35.0; + double _magB = 75.0; + double _angB = 125.0; + bool _isSubtraction = false; // false = A + B, true = A - B + bool _showComponents = true; + + // Lesson 3: Vector Products (Dot & Cross) + double _dotCrossMagA = 80.0; + double _dotCrossMagB = 60.0; + double _dotCrossAngle = 60.0; + + // Enrichment 1: Magnetic Bottle & Lorentz Force + double _bFieldStrength = 1.5; // Tesla + double _particleVelocity = 50.0; // km/s + double _particleCharge = 1.0; // +1e + late final AnimationController _magneticBottleController; + double _bottleAnimPhase = 0.0; + + // --------------------------------------------------------------------------- + // UNIT 2 STATE: MOTION & FORCES + // --------------------------------------------------------------------------- + // Lesson 1: 1D Motion + double _v0 = 12.0; + double _a = -2.5; double _simTime = 0.0; - bool _isMoving = false; - late final AnimationController _motionController; + bool _isMoving1D = false; + late final AnimationController _motion1DController; + + // Lesson 2: 2D Projectile Motion + double _projV0 = 35.0; + double _projAngle = 45.0; + double _projTime = 0.0; + bool _isProjFlying = false; + late final AnimationController _projController; + + // Lesson 3: Newton's Second Law (F = ma) + double _mass = 5.0; // kg + double _appliedForce = 25.0; // N + double _frictionCoeff = 0.20; // mu_k + + // Enrichment 2: Automotive Collision & Seatbelts (Impulse & Momentum) + double _vehicleSpeedKmh = 60.0; // km/h + double _passengerMass = 70.0; // kg + bool _withSeatbelt = true; // true = 0.15s cushion, false = 0.01s rigid impact @override void initState() { super.initState(); - if (widget.initialSimulationSlug == 'motion_1d_lab') { - _selectedSimIndex = 1; - } - _motionController = AnimationController( + + // 1. Magnetic Bottle Animation Controller + _magneticBottleController = AnimationController( + vsync: this, + duration: const Duration(seconds: 4), + )..addListener(() { + setState(() { + _bottleAnimPhase = _magneticBottleController.value * 2 * math.pi; + }); + }); + _magneticBottleController.repeat(); + + // 2. 1D Motion Animation Controller + _motion1DController = AnimationController( vsync: this, duration: const Duration(seconds: 10), )..addListener(() { - if (_isMoving) { + if (_isMoving1D) { setState(() { - _simTime += 0.016; // approx 60/120fps delta + _simTime += 0.02; final x = _v0 * _simTime + 0.5 * _a * _simTime * _simTime; - if (_simTime >= 10.0 || x.abs() > 140) { - _isMoving = false; - _motionController.stop(); + final v = _v0 + _a * _simTime; + if (_simTime >= 8.0 || x.abs() > 140 || (_v0 > 0 && v <= 0 && _a < 0 && _simTime > 4.0)) { + _isMoving1D = false; + _motion1DController.stop(); } }); } }); + + // 3. Projectile Motion Controller + _projController = AnimationController( + vsync: this, + duration: const Duration(seconds: 10), + )..addListener(() { + if (_isProjFlying) { + setState(() { + _projTime += 0.025; + final angleRad = _projAngle * math.pi / 180.0; + final totalTime = (2 * _projV0 * math.sin(angleRad)) / 9.8; + if (_projTime >= totalTime) { + _projTime = totalTime; + _isProjFlying = false; + _projController.stop(); + } + }); + } + }); + + if (widget.initialSimulationSlug == 'motion_1d_lab') { + _selectedUnit = 1; + _selectedLesson = 0; + } } @override void dispose() { - _motionController.dispose(); + _magneticBottleController.dispose(); + _motion1DController.dispose(); + _projController.dispose(); super.dispose(); } - void _toggleMotion() { + void _toggle1DMotion() { setState(() { - _isMoving = !_isMoving; - if (_isMoving) { - _motionController.repeat(); + _isMoving1D = !_isMoving1D; + if (_isMoving1D) { + if (_simTime >= 8.0) _simTime = 0.0; + _motion1DController.repeat(); } else { - _motionController.stop(); + _motion1DController.stop(); } }); } - void _resetMotion() { + void _reset1DMotion() { setState(() { - _isMoving = false; + _isMoving1D = false; _simTime = 0.0; - _motionController.reset(); + _motion1DController.reset(); + }); + } + + void _toggleProjectile() { + setState(() { + _isProjFlying = !_isProjFlying; + if (_isProjFlying) { + final angleRad = _projAngle * math.pi / 180.0; + final totalTime = (2 * _projV0 * math.sin(angleRad)) / 9.8; + if (_projTime >= totalTime) _projTime = 0.0; + _projController.repeat(); + } else { + _projController.stop(); + } + }); + } + + void _resetProjectile() { + setState(() { + _isProjFlying = false; + _projTime = 0.0; + _projController.reset(); }); } @@ -99,55 +205,97 @@ class _PhysicsInteractiveLabViewState extends State w Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20), + child: Column( children: [ - // Header / Lab Switcher + // Sub-navigation bar Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: const BoxDecoration( color: AppColors.darkSurface, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.darkCardBorder), + border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)), ), + child: Row( + children: [ + const Icon(CupertinoIcons.flame_fill, color: AppColors.saqelCyan, size: 20), + const SizedBox(width: 8), + const Text( + 'المختبر الفيزيائي الذكي — منهاج الفيزياء المعتمد (الصف العاشر)', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.saqelCyan.withAlpha(25), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.saqelCyan.withAlpha(80)), + ), + child: const Text( + '120 FPS Canvas Lab', + style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700), + ), + ), + ], + ), + ), + + // Unit Switcher Row + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + color: const Color(0xFF091220), child: Row( children: [ Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedSimIndex = 0), + onTap: () => setState(() { + _selectedUnit = 0; + _selectedLesson = 0; + }), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 9), decoration: BoxDecoration( - color: _selectedSimIndex == 0 ? AppColors.appleBlue : Colors.transparent, - borderRadius: BorderRadius.circular(10), + color: _selectedUnit == 0 ? AppColors.saqelCyan.withAlpha(40) : Colors.white.withAlpha(8), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _selectedUnit == 0 ? AppColors.saqelCyan : Colors.white12, + width: 1.5, + ), ), alignment: Alignment.center, child: Text( - '1. جمع وتحليل المتجهات 🧭', + 'الوحدة 1: المتجهات (Vectors) 🧭', style: TextStyle( - color: _selectedSimIndex == 0 ? Colors.white : AppColors.textSecondaryDark, - fontWeight: FontWeight.w700, + color: _selectedUnit == 0 ? AppColors.saqelCyan : Colors.white70, + fontWeight: _selectedUnit == 0 ? FontWeight.w800 : FontWeight.w600, fontSize: 12.5, ), ), ), ), ), + const SizedBox(width: 10), Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedSimIndex = 1), + onTap: () => setState(() { + _selectedUnit = 1; + _selectedLesson = 0; + }), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 9), decoration: BoxDecoration( - color: _selectedSimIndex == 1 ? AppColors.appleBlue : Colors.transparent, - borderRadius: BorderRadius.circular(10), + color: _selectedUnit == 1 ? AppColors.appleBlue.withAlpha(40) : Colors.white.withAlpha(8), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _selectedUnit == 1 ? AppColors.appleBlue : Colors.white12, + width: 1.5, + ), ), alignment: Alignment.center, child: Text( - '2. الحركة في بُعد واحد 🏎️', + 'الوحدة 2: الحركة والقوى (Motion & Forces) 🏎️', style: TextStyle( - color: _selectedSimIndex == 1 ? Colors.white : AppColors.textSecondaryDark, - fontWeight: FontWeight.w700, + color: _selectedUnit == 1 ? const Color(0xFF60A5FA) : Colors.white70, + fontWeight: _selectedUnit == 1 ? FontWeight.w800 : FontWeight.w600, fontSize: 12.5, ), ), @@ -157,97 +305,354 @@ class _PhysicsInteractiveLabViewState extends State w ], ), ), - const SizedBox(height: 16), - // Simulation Body - if (_selectedSimIndex == 0) _buildVectorsLab() else _buildMotionLab(), + // Lesson Selector Pills Row + Container( + padding: const EdgeInsets.fromLTRB(14, 0, 14, 10), + color: const Color(0xFF091220), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _getLessonList().asMap().entries.map((entry) { + final idx = entry.key; + final lesson = entry.value; + final isSelected = _selectedLesson == idx; + + return GestureDetector( + onTap: () => setState(() => _selectedLesson = idx), + child: Container( + margin: const EdgeInsets.only(left: 8), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: _selectedUnit == 0 + ? [AppColors.saqelCyan, AppColors.appleBlue] + : [const Color(0xFF3B82F6), const Color(0xFF1D4ED8)], + ) + : null, + color: isSelected ? null : const Color(0xFF0F1B2E), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.white : AppColors.darkCardBorder, + ), + ), + child: Text( + lesson['title'] as String, + style: TextStyle( + color: isSelected ? Colors.black : Colors.white, + fontSize: 11.5, + fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500, + ), + ), + ), + ); + }).toList(), + ), + ), + ), + + // Active Simulation Viewport + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + children: [ + _buildActiveSimulator(), + ], + ), + ), ], ), ); } - // ============================================================================== - // LAB 1: VECTORS ADDITION & DECOMPOSITION SIMULATION - // ============================================================================== - Widget _buildVectorsLab() { - // Vector Math Calculations + List> _getLessonList() { + if (_selectedUnit == 0) { + return [ + {'title': 'الدرس 1: الكميات القياسية والمتجهة'}, + {'title': 'الدرس 2: جمع المتجهات وطرحها'}, + {'title': 'الدرس 3: ضرب المتجهات (قياسي ومتجهي)'}, + {'title': 'إثراء: الوعاء المغناطيسي وقوة لورنتز 🧲'}, + ]; + } else { + return [ + {'title': 'الدرس 1: الحركة في بُعد واحد بتسارع ثابت'}, + {'title': 'الدرس 2: حركة المقذوفات في بُعدين 🎯'}, + {'title': 'الدرس 3: قوانين نيوتن والقوة المحصلة ⚖️'}, + {'title': 'إثراء: حوادث السيارات وأحزمة الأمان 🛡️'}, + ]; + } + } + + Widget _buildActiveSimulator() { + if (_selectedUnit == 0) { + switch (_selectedLesson) { + case 0: + return _buildUnit1Lesson1(); + case 1: + return _buildUnit1Lesson2(); + case 2: + return _buildUnit1Lesson3(); + case 3: + default: + return _buildUnit1Enrichment(); + } + } else { + switch (_selectedLesson) { + case 0: + return _buildUnit2Lesson1(); + case 1: + return _buildUnit2Lesson2(); + case 2: + return _buildUnit2Lesson3(); + case 3: + default: + return _buildUnit2Enrichment(); + } + } + } + + // =========================================================================== + // SIMULATOR 1.1: SCALAR VS VECTOR QUANTITIES + // =========================================================================== + Widget _buildUnit1Lesson1() { + final rad = _u1l1Angle * math.pi / 180.0; + final vx = _u1l1Mag * math.cos(rad); + final vy = _u1l1Mag * math.sin(rad); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Visual Canvas + _buildCanvasBox( + height: 280, + child: CustomPaint( + painter: _ScalarVectorPainter( + mag: _u1l1Mag, + angle: _u1l1Angle, + isVector: _u1l1QuantityType == 0, + ), + ), + ), + const SizedBox(height: 14), + + // Controls + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('نوع الكمية الفيزيائية:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ChoiceChip( + label: const Text('كمية متجهة (الإزاحة، السرعة، القوة) 🧭'), + selected: _u1l1QuantityType == 0, + selectedColor: AppColors.saqelCyan, + labelStyle: TextStyle( + color: _u1l1QuantityType == 0 ? Colors.black : Colors.white, + fontWeight: FontWeight.w700, + fontSize: 11.5, + ), + backgroundColor: AppColors.darkSurface, + onSelected: (_) => setState(() => _u1l1QuantityType = 0), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ChoiceChip( + label: const Text('كمية قياسية (الكتلة، الزمن، الطاقة) ⚖️'), + selected: _u1l1QuantityType == 1, + selectedColor: AppColors.guardianAmber, + labelStyle: TextStyle( + color: _u1l1QuantityType == 1 ? Colors.black : Colors.white, + fontWeight: FontWeight.w700, + fontSize: 11.5, + ), + backgroundColor: AppColors.darkSurface, + onSelected: (_) => setState(() => _u1l1QuantityType = 1), + ), + ), + ], + ), + const Divider(color: AppColors.darkCardBorder, height: 22), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('المقدار (Magnitude):', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text('${_u1l1Mag.toInt()} وحدات', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _u1l1Mag, + min: 20.0, + max: 120.0, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _u1l1Mag = v), + ), + + if (_u1l1QuantityType == 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('الاتجاه وزاوية الإسناد (θ مع محور +X):', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text('${_u1l1Angle.toInt()}°', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _u1l1Angle, + min: 0.0, + max: 360.0, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() => _u1l1Angle = v), + ), + ], + ], + ), + ), + const SizedBox(height: 12), + + // Socratic Pedagogical Card + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('تحليل المفهوم الوزاري 📖', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w800, fontSize: 13)), + const SizedBox(height: 6), + if (_u1l1QuantityType == 0) + Text( + 'الكمية المتجهة تتحدد بمقدار ووحدة قياس واتجاه. تحليل المركبات الديكارتية: \n' + '• المركبة الأفقية: Ax = A · cos(θ) = ${_u1l1Mag.toInt()} · cos(${_u1l1Angle.toInt()}°) = ${vx.toStringAsFixed(1)} \n' + '• المركبة الرأسية: Ay = A · sin(θ) = ${_u1l1Mag.toInt()} · sin(${_u1l1Angle.toInt()}°) = ${vy.toStringAsFixed(1)}', + style: const TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ) + else + const Text( + 'الكمية القياسية تتحدد بالمقدار ووحدة القياس فقط (مثل الكتلة m = 5 kg أو الزمن t = 10 s). لا تعتمد على الاتجاه ولا يمكن تحليلها إلى مركبات في المستوى الديكارتي.', + style: TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ), + ], + ), + ), + ], + ); + } + + // =========================================================================== + // SIMULATOR 1.2: VECTOR ADDITION & SUBTRACTION + // =========================================================================== + Widget _buildUnit1Lesson2() { final aRad = _angA * math.pi / 180.0; final bRad = _angB * math.pi / 180.0; final ax = _magA * math.cos(aRad); final ay = _magA * math.sin(aRad); - final bx = _magB * math.cos(bRad); - final by = _magB * math.sin(bRad); + final bx = _magB * math.cos(bRad) * (_isSubtraction ? -1.0 : 1.0); + final by = _magB * math.sin(bRad) * (_isSubtraction ? -1.0 : 1.0); final rx = ax + bx; final ry = ay + by; final rMag = math.sqrt(rx * rx + ry * ry); double rDeg = (math.atan2(ry, rx) * 180.0 / math.pi); if (rDeg < 0) rDeg += 360.0; - final dotProduct = (_magA * _magB * math.cos((_angA - _angB).abs() * math.pi / 180.0)).toStringAsFixed(1); - final crossProduct = (_magA * _magB * math.sin((_angA - _angB).abs() * math.pi / 180.0)).toStringAsFixed(1); - return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Canvas Container - Container( - height: 320, - decoration: BoxDecoration( - color: const Color(0xFF070B12), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.saqelCyan.withAlpha(40), width: 1.5), - boxShadow: [ - BoxShadow( - color: AppColors.saqelCyan.withAlpha(20), - blurRadius: 20, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(19), - child: CustomPaint( - painter: _VectorCanvasPainter( - magA: _magA, - angA: _angA, - magB: _magB, - angB: _angB, - showResultant: _showResultant, - ), + _buildCanvasBox( + height: 310, + child: CustomPaint( + painter: _VectorCanvasPainter( + magA: _magA, + angA: _angA, + magB: _magB, + angB: _angB, + isSubtraction: _isSubtraction, + showComponents: _showComponents, ), ), ), - const SizedBox(height: 16), + const SizedBox(height: 14), - // Vector A Sliders + // Operation Toggle + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _isSubtraction = false), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: !_isSubtraction ? AppColors.saqelCyan : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + 'جمع المتجهات: R = A + B ➕', + style: TextStyle( + color: !_isSubtraction ? Colors.black : Colors.white70, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _isSubtraction = true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: _isSubtraction ? const Color(0xFFFF2D55) : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + 'طرح المتجهات: R = A - B ➖', + style: TextStyle( + color: _isSubtraction ? Colors.white : Colors.white70, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + // Vector Controls LuxuryCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('المتجه A (السماوي 🟦)', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w800, fontSize: 13.5)), - Text('|A| = ${_magA.toInt()} N • θ = ${_angA.toInt()}°', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 13)), - ], + Text( + 'المتجه A: |A| = ${_magA.toInt()} N • θ = ${_angA.toInt()}°', + style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12.5), ), - const SizedBox(height: 8), Row( children: [ - const Text('المقدار:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5)), + const Text('المقدار:', style: TextStyle(color: Colors.white70, fontSize: 11)), Expanded( child: CupertinoSlider( value: _magA, min: 20.0, - max: 140.0, + max: 130.0, activeColor: AppColors.saqelCyan, onChanged: (v) => setState(() => _magA = v), ), ), - ], - ), - Row( - children: [ - const Text('الزاوية:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5)), + const Text('الزاوية:', style: TextStyle(color: Colors.white70, fontSize: 11)), Expanded( child: CupertinoSlider( value: _angA, @@ -259,41 +664,25 @@ class _PhysicsInteractiveLabViewState extends State w ), ], ), - ], - ), - ), - const SizedBox(height: 12), + const Divider(color: AppColors.darkCardBorder, height: 16), - // Vector B Sliders - LuxuryCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('المتجه B (الذهبي 🟨)', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w800, fontSize: 13.5)), - Text('|B| = ${_magB.toInt()} N • θ = ${_angB.toInt()}°', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 13)), - ], + Text( + 'المتجه B: |B| = ${_magB.toInt()} N • θ = ${_angB.toInt()}°', + style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12.5), ), - const SizedBox(height: 8), Row( children: [ - const Text('المقدار:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5)), + const Text('المقدار:', style: TextStyle(color: Colors.white70, fontSize: 11)), Expanded( child: CupertinoSlider( value: _magB, min: 20.0, - max: 140.0, + max: 130.0, activeColor: AppColors.guardianAmber, onChanged: (v) => setState(() => _magB = v), ), ), - ], - ), - Row( - children: [ - const Text('الزاوية:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5)), + const Text('الزاوية:', style: TextStyle(color: Colors.white70, fontSize: 11)), Expanded( child: CupertinoSlider( value: _angB, @@ -310,7 +699,7 @@ class _PhysicsInteractiveLabViewState extends State w ), const SizedBox(height: 12), - // Resultant Toggle & Mathematical Readout + // Resultant Telemetry LuxuryCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -318,59 +707,20 @@ class _PhysicsInteractiveLabViewState extends State w Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('إظهار محصلة المتجهين (R = A + B) 🎯', style: TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.w800, fontSize: 13.5)), - CupertinoSwitch( - value: _showResultant, - activeTrackColor: AppColors.emeraldGreen, - onChanged: (v) => setState(() => _showResultant = v), + Text( + _isSubtraction ? 'محصلة الطرح: R = A + (-B)' : 'محصلة الجمع: R = A + B', + style: const TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.w800, fontSize: 13.5), + ), + Text( + '|R| = ${rMag.toStringAsFixed(1)} N • θ_R = ${rDeg.toStringAsFixed(1)}°', + style: const TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.bold, fontSize: 13), ), ], ), - const Divider(color: AppColors.darkCardBorder, height: 24), - Text('Ax = ${ax.toStringAsFixed(1)} N | Ay = ${ay.toStringAsFixed(1)} N', style: const TextStyle(color: Colors.white70, fontSize: 12)), - Text('Bx = ${bx.toStringAsFixed(1)} N | By = ${by.toStringAsFixed(1)} N', style: const TextStyle(color: Colors.white70, fontSize: 12)), const SizedBox(height: 6), Text( - 'المحصلة |R| = √(${rx.toStringAsFixed(1)}² + ${ry.toStringAsFixed(1)}²) = ${rMag.toStringAsFixed(1)} N', - style: const TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.w800, fontSize: 14), - ), - Text('اتجاه المحصلة θ_R = ${rDeg.toStringAsFixed(1)}°', style: const TextStyle(color: AppColors.emeraldGreen, fontSize: 13, fontWeight: FontWeight.w600)), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white.withAlpha(8), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('الضرب القياسي (A · B): $dotProduct J', style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.w700)), - Text('الضرب المتجهي |A × B|: $crossProduct N·m', style: const TextStyle(color: AppColors.guardianAmber, fontSize: 12, fontWeight: FontWeight.w700)), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 12), - - // Socratic Challenge Card - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: AppColors.guardianAmber.withAlpha(20), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.guardianAmber.withAlpha(50)), - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('💡 فحص سقراطي مباشر:', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w800, fontSize: 13)), - SizedBox(height: 4), - Text( - 'اجعل الزاوية بين المتجهين 180° تماماً ولاحظ قيمة المحصلة R! متى تنعدم المحصلة بالكامل؟', - style: TextStyle(color: Colors.white, fontSize: 12.5, height: 1.4), + 'تحليلياً: Rx = Ax ${_isSubtraction ? "-" : "+"} Bx = ${rx.toStringAsFixed(1)} N | Ry = Ay ${_isSubtraction ? "-" : "+"} By = ${ry.toStringAsFixed(1)} N', + style: const TextStyle(color: Colors.white70, fontSize: 11.5), ), ], ), @@ -379,101 +729,336 @@ class _PhysicsInteractiveLabViewState extends State w ); } - // ============================================================================== - // LAB 2: 1D MOTION & ACCELERATION SIMULATION - // ============================================================================== - Widget _buildMotionLab() { + // =========================================================================== + // SIMULATOR 1.3: VECTOR PRODUCTS (DOT & CROSS PRODUCT) + // =========================================================================== + Widget _buildUnit1Lesson3() { + final thetaRad = _dotCrossAngle * math.pi / 180.0; + final dotVal = _dotCrossMagA * _dotCrossMagB * math.cos(thetaRad); + final crossVal = _dotCrossMagA * _dotCrossMagB * math.sin(thetaRad); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCanvasBox( + height: 290, + child: CustomPaint( + painter: _VectorProductPainter( + magA: _dotCrossMagA, + magB: _dotCrossMagB, + angleBetween: _dotCrossAngle, + ), + ), + ), + const SizedBox(height: 14), + + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('الزاوية بين المتجهين (θ):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)), + Text('${_dotCrossAngle.toInt()}°', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w900, fontSize: 15)), + ], + ), + CupertinoSlider( + value: _dotCrossAngle, + min: 0.0, + max: 180.0, + divisions: 36, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _dotCrossAngle = v), + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('مقدار |A| = ${_dotCrossMagA.toInt()}', style: const TextStyle(color: Colors.white70, fontSize: 11)), + CupertinoSlider( + value: _dotCrossMagA, + min: 20.0, + max: 120.0, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _dotCrossMagA = v), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('مقدار |B| = ${_dotCrossMagB.toInt()}', style: const TextStyle(color: Colors.white70, fontSize: 11)), + CupertinoSlider( + value: _dotCrossMagB, + min: 20.0, + max: 120.0, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() => _dotCrossMagB = v), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + + // Products Comparison Grid + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.appleBlue.withAlpha(25), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.appleBlue.withAlpha(90)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('الضرب القياسي (النقطي):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)), + const SizedBox(height: 4), + const Text('A · B = |A| |B| cos(θ)', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 6), + Text( + '${dotVal.toStringAsFixed(1)} J', + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 4), + Text( + _dotCrossAngle == 90 + ? 'متعامدان (cos 90° = 0)' + : (_dotCrossAngle == 0 ? 'أقصى قيمة (cos 0° = 1)' : 'كمية قياسية بدون اتجاه'), + style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10.5), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.guardianAmber.withAlpha(25), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.guardianAmber.withAlpha(90)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('الضرب المتجهي (التقاطعي):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)), + const SizedBox(height: 4), + const Text('|A × B| = |A| |B| sin(θ)', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 6), + Text( + '${crossVal.toStringAsFixed(1)} N·m', + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 4), + Text( + _dotCrossAngle == 90 + ? 'أقصى قيمة (sin 90° = 1)' + : (_dotCrossAngle == 0 ? 'متوازيان (sin 0° = 0)' : 'عمودي على مستوى المتجهين'), + style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10.5), + ), + ], + ), + ), + ), + ], + ), + ], + ); + } + + // =========================================================================== + // SIMULATOR 1.4: ENRICHMENT - MAGNETIC BOTTLE (LORENTZ FORCE) + // =========================================================================== + Widget _buildUnit1Enrichment() { + final lorentzF = _particleCharge * _particleVelocity * _bFieldStrength; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCanvasBox( + height: 310, + child: CustomPaint( + painter: _MagneticBottlePainter( + animPhase: _bottleAnimPhase, + bStrength: _bFieldStrength, + velocity: _particleVelocity, + ), + ), + ), + const SizedBox(height: 14), + + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('قوة المجال المغناطيسي (B):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_bFieldStrength.toStringAsFixed(1)} Tesla', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _bFieldStrength, + min: 0.5, + max: 3.0, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _bFieldStrength = v), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('سرعة الجسيم المشحون (v):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_particleVelocity.toInt()} km/s', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _particleVelocity, + min: 20.0, + max: 100.0, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() => _particleVelocity = v), + ), + ], + ), + ), + const SizedBox(height: 12), + + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 18), + SizedBox(width: 8), + Text('إثراء وتوسع: حصر البلازما في الوعاء المغناطيسي 🌌', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13)), + ], + ), + const SizedBox(height: 8), + Text( + 'تخضع الشحنات لقوة لورنتز: F_B = q(v × B). بما أن القوة عمودية دوماً على متجه السرعة، فإنها لا تبذل شغلاً بل تغير اتجاه الحركة لتصبح لولبية (Helical Motion). \n' + 'عند اقتراب الجسيم من أطراف الوعاء المغناطيسي تتقارب خطوط المجال، فتتولد مركبة قوة محورية عاكسة تعيد الشحنة نحو المركز وتمنعها من الهروب، وهو المبدأ المستخدم في مفاعلات الاندماج النووي (Tokamak) وحزام فان ألين الأرضي.', + style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.6), + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.saqelCyan.withAlpha(20), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.saqelCyan.withAlpha(60)), + ), + child: Text( + 'قوة لورنتز الآنية: F_B = q v B = ${lorentzF.toStringAsFixed(1)} × 10⁻¹⁴ N', + style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12), + ), + ), + ], + ), + ), + ], + ); + } + + // =========================================================================== + // SIMULATOR 2.1: 1D MOTION WITH CONSTANT ACCELERATION + // =========================================================================== + Widget _buildUnit2Lesson1() { final x = _v0 * _simTime + 0.5 * _a * _simTime * _simTime; final v = _v0 + _a * _simTime; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Track Canvas - Container( - height: 220, - decoration: BoxDecoration( - color: const Color(0xFF070B12), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.appleBlue.withAlpha(40), width: 1.5), - boxShadow: [ - BoxShadow( - color: AppColors.appleBlue.withAlpha(20), - blurRadius: 20, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(19), - child: CustomPaint( - painter: _MotionTrackPainter( - xPosition: x, - velocity: v, - acceleration: _a, - ), + _buildCanvasBox( + height: 250, + child: CustomPaint( + painter: _Motion1DTrackPainter( + xPosition: x, + velocity: v, + acceleration: _a, ), ), ), - const SizedBox(height: 16), - - // Motion Controls - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: _isMoving ? AppColors.crimsonRed : AppColors.emeraldGreen, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - onPressed: _toggleMotion, - icon: Icon(_isMoving ? CupertinoIcons.pause_fill : CupertinoIcons.play_arrow_solid, size: 18), - label: Text(_isMoving ? 'إيقاف مؤقت ⏸' : 'تشغيل الحركة ▶', style: const TextStyle(fontWeight: FontWeight.w800)), - ), - ), - const SizedBox(width: 12), - OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: AppColors.darkCardBorder), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - onPressed: _resetMotion, - icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16), - label: const Text('إعادة ضبط', style: TextStyle(fontWeight: FontWeight.w700)), - ), - ], - ), const SizedBox(height: 14), - // Sliders + // Controls LuxuryCard( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + ElevatedButton.icon( + icon: Icon(_isMoving1D ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill, size: 16), + label: Text(_isMoving1D ? 'إيقاف مؤقت' : 'بدء المحاكاة'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.saqelCyan, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _toggle1DMotion, + ), + const SizedBox(width: 8), + OutlinedButton.icon( + icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16), + label: const Text('إعادة ضبط'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _reset1DMotion, + ), + const Spacer(), + Text( + 't = ${_simTime.toStringAsFixed(2)} s', + style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w900, fontSize: 16), + ), + ], + ), + const Divider(color: AppColors.darkCardBorder, height: 20), + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('السرعة الابتدائية (v₀):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13)), - Text('${_v0.toInt()} m/s', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), + const Text('السرعة الابتدائية (v₀):', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text('${_v0.toStringAsFixed(1)} m/s', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), ], ), CupertinoSlider( value: _v0, - min: -15.0, - max: 25.0, + min: 0.0, + max: 30.0, activeColor: AppColors.saqelCyan, - onChanged: (v) => setState(() { - _v0 = v; - if (!_isMoving) _resetMotion(); - }), + onChanged: (val) { + setState(() { + _v0 = val; + _simTime = 0.0; + }); + }, ), - const SizedBox(height: 10), + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('التسارع الثابت (a):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 13)), + const Text('التسارع الثابت (a):', style: TextStyle(color: Colors.white70, fontSize: 12)), Text('${_a.toStringAsFixed(1)} m/s²', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), ], ), @@ -482,9 +1067,141 @@ class _PhysicsInteractiveLabViewState extends State w min: -6.0, max: 6.0, activeColor: AppColors.guardianAmber, + onChanged: (val) { + setState(() { + _a = val; + _simTime = 0.0; + }); + }, + ), + ], + ), + ), + const SizedBox(height: 12), + + // Live Equations Telemetry + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('معادلات الحركة بتسارع ثابت (وزارة التربية):', style: TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.bold, fontSize: 12.5)), + const SizedBox(height: 6), + Text( + '1. v₁ = v₀ + a·t ⟹ v = ${_v0.toStringAsFixed(1)} + (${_a.toStringAsFixed(1)})·(${_simTime.toStringAsFixed(2)}) = ${v.toStringAsFixed(2)} m/s', + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + const SizedBox(height: 4), + Text( + '2. Δx = v₀·t + ½·a·t² ⟹ Δx = ${x.toStringAsFixed(2)} m', + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), + ), + ], + ); + } + + // =========================================================================== + // SIMULATOR 2.2: 2D PROJECTILE MOTION + // =========================================================================== + Widget _buildUnit2Lesson2() { + final angleRad = _projAngle * math.pi / 180.0; + final totalFlightTime = (2 * _projV0 * math.sin(angleRad)) / 9.8; + final maxHeight = (_projV0 * _projV0 * math.sin(angleRad) * math.sin(angleRad)) / (2 * 9.8); + final maxRange = (_projV0 * _projV0 * math.sin(2 * angleRad)) / 9.8; + + final curT = _projTime.clamp(0.0, totalFlightTime); + final curX = _projV0 * math.cos(angleRad) * curT; + final curY = _projV0 * math.sin(angleRad) * curT - 0.5 * 9.8 * curT * curT; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCanvasBox( + height: 290, + child: CustomPaint( + painter: _ProjectilePainter( + v0: _projV0, + angleDeg: _projAngle, + curTime: curT, + totalTime: totalFlightTime, + curX: curX, + curY: math.max(0.0, curY), + maxRange: maxRange, + maxHeight: maxHeight, + ), + ), + ), + const SizedBox(height: 14), + + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ElevatedButton.icon( + icon: Icon(_isProjFlying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill, size: 16), + label: Text(_isProjFlying ? 'إيقاف' : 'إطلاق القذيفة 🚀'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.saqelCyan, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _toggleProjectile, + ), + const SizedBox(width: 8), + OutlinedButton.icon( + icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16), + label: const Text('إعادة ضبط'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _resetProjectile, + ), + const Spacer(), + Text('t = ${curT.toStringAsFixed(2)} s', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 14)), + ], + ), + const Divider(color: AppColors.darkCardBorder, height: 20), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('سرعة الإطلاق (v₀):', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text('${_projV0.toInt()} m/s', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _projV0, + min: 15.0, + max: 60.0, + activeColor: AppColors.saqelCyan, onChanged: (v) => setState(() { - _a = v; - if (!_isMoving) _resetMotion(); + _projV0 = v; + _projTime = 0.0; + }), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('زاوية الإطلاق (θ):', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text('${_projAngle.toInt()}°', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _projAngle, + min: 10.0, + max: 85.0, + divisions: 15, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() { + _projAngle = v; + _projTime = 0.0; }), ), ], @@ -492,47 +1209,427 @@ class _PhysicsInteractiveLabViewState extends State w ), const SizedBox(height: 12), - // Real-time Readout Card + // Telemetry + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('أقصى ارتفاع (H):', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 2), + Text('${maxHeight.toStringAsFixed(1)} m', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w800, fontSize: 16)), + ], + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('المدى الأفقي (R):', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 2), + Text('${maxRange.toStringAsFixed(1)} m', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w800, fontSize: 16)), + ], + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('زمن التحليق (T):', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 2), + Text('${totalFlightTime.toStringAsFixed(1)} s', style: const TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.w800, fontSize: 16)), + ], + ), + ), + ), + ], + ), + ], + ); + } + + // =========================================================================== + // SIMULATOR 2.3: NEWTON'S SECOND LAW & FREE BODY DIAGRAM (FBD) + // =========================================================================== + Widget _buildUnit2Lesson3() { + final weight = _mass * 9.8; + final normalForce = weight; + final frictionForce = _frictionCoeff * normalForce; + final netForce = math.max(0.0, _appliedForce - frictionForce); + final accel = _appliedForce > frictionForce ? netForce / _mass : 0.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCanvasBox( + height: 280, + child: CustomPaint( + painter: _NewtonFBDPainter( + appliedF: _appliedForce, + frictionF: frictionForce, + normalF: normalForce, + weightF: weight, + mass: _mass, + ), + ), + ), + const SizedBox(height: 14), + LuxuryCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('القراءات اللحظية للحركة (Kinematics Realtime):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)), - const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('الزمن: ${_simTime.toStringAsFixed(2)} s', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w600)), - Text('الموقع x: ${x.toStringAsFixed(2)} m', style: const TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.bold)), - Text('السرعة v: ${v.toStringAsFixed(2)} m/s', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + const Text('القوة المؤثرة المسحوبة (F):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_appliedForce.toInt()} N', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), ], ), + CupertinoSlider( + value: _appliedForce, + min: 0.0, + max: 80.0, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _appliedForce = v), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('كتلة الصندوق (m):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_mass.toStringAsFixed(1)} kg', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _mass, + min: 1.0, + max: 15.0, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() => _mass = v), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('معامل الاحتكاك الحركي (μ):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text(_frictionCoeff.toStringAsFixed(2), style: const TextStyle(color: Color(0xFFFF2D55), fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _frictionCoeff, + min: 0.0, + max: 0.60, + divisions: 12, + activeColor: const Color(0xFFFF2D55), + onChanged: (v) => setState(() => _frictionCoeff = v), + ), + ], + ), + ), + const SizedBox(height: 12), + + // Live Net Force & Acceleration + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF0D1B2A), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.emeraldGreen.withAlpha(100), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('القانون الثاني لنيوتن: ∑F = m·a', style: TextStyle(color: AppColors.emeraldGreen, fontWeight: FontWeight.w800, fontSize: 14)), const SizedBox(height: 6), - const Text('القانون المطبق: x(t) = v₀t + ½at² • v(t) = v₀ + at', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5)), + Text( + '• قوة الاحتكاك fₖ = μ·Fₙ = ${_frictionCoeff.toStringAsFixed(2)} × ${normalForce.toStringAsFixed(1)} = ${frictionForce.toStringAsFixed(1)} N \n' + '• القوة المحصلة F_net = F - fₖ = ${_appliedForce.toInt()} - ${frictionForce.toStringAsFixed(1)} = ${netForce.toStringAsFixed(1)} N \n' + '• التسارع المكتسب a = F_net / m = ${accel.toStringAsFixed(2)} m/s²', + style: const TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ), ], ), ), ], ); } + + // =========================================================================== + // SIMULATOR 2.4: ENRICHMENT - SEATBELT & COLLISION INERTIA (IMPULSE) + // =========================================================================== + Widget _buildUnit2Enrichment() { + final vInitialMs = _vehicleSpeedKmh / 3.6; // convert km/h to m/s + final momentum = _passengerMass * vInitialMs; + final impactDuration = _withSeatbelt ? 0.15 : 0.01; // seconds + final averageForce = momentum / impactDuration; // F = dp / dt + final forceG = averageForce / (_passengerMass * 9.8); // G-force equivalent + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCanvasBox( + height: 290, + child: CustomPaint( + painter: _SeatbeltCrashPainter( + withSeatbelt: _withSeatbelt, + forceG: forceG, + speedKmh: _vehicleSpeedKmh, + ), + ), + ), + const SizedBox(height: 14), + + // Seatbelt Switch + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _withSeatbelt = true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 9), + decoration: BoxDecoration( + color: _withSeatbelt ? AppColors.emeraldGreen : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + 'مع حزام أمان ووسادة هوائية (Δt = 0.15s) 🛡️', + style: TextStyle( + color: _withSeatbelt ? Colors.black : Colors.white70, + fontWeight: FontWeight.w700, + fontSize: 11.5, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _withSeatbelt = false), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 9), + decoration: BoxDecoration( + color: !_withSeatbelt ? const Color(0xFFFF2D55) : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + 'بدون حزام أمان (اصطدام صلب Δt = 0.01s) ⚠️', + style: TextStyle( + color: !_withSeatbelt ? Colors.white : Colors.white70, + fontWeight: FontWeight.w700, + fontSize: 11.5, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('سرعة المركبة قبل الارتطام:', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_vehicleSpeedKmh.toInt()} km/h (${vInitialMs.toStringAsFixed(1)} m/s)', style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _vehicleSpeedKmh, + min: 20.0, + max: 120.0, + activeColor: AppColors.saqelCyan, + onChanged: (v) => setState(() => _vehicleSpeedKmh = v), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('كتلة الراكب (m):', style: TextStyle(color: Colors.white, fontSize: 12)), + Text('${_passengerMass.toInt()} kg', style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)), + ], + ), + CupertinoSlider( + value: _passengerMass, + min: 40.0, + max: 110.0, + activeColor: AppColors.guardianAmber, + onChanged: (v) => setState(() => _passengerMass = v), + ), + ], + ), + ), + const SizedBox(height: 12), + + // Impact Force Readout + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _withSeatbelt ? const Color(0xFF0E2A1E) : const Color(0xFF330E14), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _withSeatbelt ? AppColors.emeraldGreen : const Color(0xFFFF2D55), + width: 1.5, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _withSeatbelt ? '✅ النتيجة الفيزيائية: حماية حياة الراكب' : '🚨 النتيجة الفيزيائية: خطر اصطدام مميت', + style: TextStyle( + color: _withSeatbelt ? AppColors.emeraldGreen : const Color(0xFFFF2D55), + fontWeight: FontWeight.w900, + fontSize: 14, + ), + ), + const SizedBox(height: 6), + Text( + '• التغير في الزخم: Δp = m·Δv = ${_passengerMass.toInt()} × ${vInitialMs.toStringAsFixed(1)} = ${momentum.toStringAsFixed(1)} kg·m/s \n' + '• زمن تلاشي الزخم: Δt = ${impactDuration} ثانية \n' + '• قوة الصدمة المؤثرة على جسد الراكب: F = Δp / Δt = ${averageForce.toStringAsFixed(0)} Newton \n' + '• تسارع القصور الذاتي: ${forceG.toStringAsFixed(1)} G (${forceG > 30 ? "يتجاوز قدرة تحمل العظام البشرية" : "مقبول وممتص عبر الوسادة الهوائية"})', + style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6), + ), + ], + ), + ), + ], + ); + } + + Widget _buildCanvasBox({required double height, required Widget child}) { + return Container( + height: height, + decoration: BoxDecoration( + color: const Color(0xFF070B12), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.saqelCyan.withAlpha(40), width: 1.5), + boxShadow: [ + BoxShadow( + color: AppColors.saqelCyan.withAlpha(20), + blurRadius: 20, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(19), + child: child, + ), + ); + } } -// ============================================================================== -// CUSTOM PAINTER: VECTORS ON 2D CARTESIAN GRID -// ============================================================================== +// ============================================================================= +// CANVAS PAINTERS (120 FPS HIGH QUALITY VIRTUAL LABS) +// ============================================================================= + +/// 1. Scalar vs Vector CustomPainter +class _ScalarVectorPainter extends CustomPainter { + final double mag; + final double angle; + final bool isVector; + + _ScalarVectorPainter({required this.mag, required this.angle, required this.isVector}); + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width / 2; + final cy = size.height / 2; + + _drawWatermark(canvas); + + if (isVector) { + // Cartesian Axes + final axisPaint = Paint()..color = Colors.white24..strokeWidth = 1; + canvas.drawLine(Offset(20, cy), Offset(size.width - 20, cy), axisPaint); + canvas.drawLine(Offset(cx, 20), Offset(cx, size.height - 20), axisPaint); + + final rad = angle * math.pi / 180.0; + final ex = cx + mag * math.cos(rad); + final ey = cy - mag * math.sin(rad); + + // Components + final compPaint = Paint()..color = Colors.white30..strokeWidth = 1..style = PaintingStyle.stroke; + canvas.drawLine(Offset(cx, cy), Offset(ex, cy), compPaint); + canvas.drawLine(Offset(ex, cy), Offset(ex, ey), compPaint); + + _drawArrow(canvas, Offset(cx, cy), Offset(ex, ey), AppColors.saqelCyan, 3.0, label: 'A'); + } else { + // Scalar visualization: balance scale + final paint = Paint()..color = AppColors.guardianAmber..style = PaintingStyle.fill; + canvas.drawCircle(Offset(cx, cy), mag * 0.4, paint..color = AppColors.guardianAmber.withAlpha(50)); + canvas.drawCircle(Offset(cx, cy), mag * 0.4, Paint()..color = AppColors.guardianAmber..style = PaintingStyle.stroke..strokeWidth = 2); + + final textSpan = TextSpan( + text: 'كمية قياسية: ${mag.toInt()} kg / Joule\n(لا يوجد اتجاه سهمي)', + style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.rtl)..layout(); + textPainter.paint(canvas, Offset(cx - textPainter.width / 2, cy - textPainter.height / 2)); + } + } + + @override + bool shouldRepaint(covariant _ScalarVectorPainter oldDelegate) => true; +} + +/// 2. Vector Addition & Subtraction Painter class _VectorCanvasPainter extends CustomPainter { final double magA; final double angA; final double magB; final double angB; - final bool showResultant; + final bool isSubtraction; + final bool showComponents; _VectorCanvasPainter({ required this.magA, required this.angA, required this.magB, required this.angB, - required this.showResultant, + required this.isSubtraction, + required this.showComponents, }); @override @@ -540,184 +1637,353 @@ class _VectorCanvasPainter extends CustomPainter { final cx = size.width / 2; final cy = size.height / 2; - // 1. Grid Background - final gridPaint = Paint() - ..color = Colors.white.withAlpha(12) - ..strokeWidth = 1; + _drawWatermark(canvas); - for (double x = 0; x < size.width; x += 25) { - canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); - } - for (double y = 0; y < size.height; y += 25) { - canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); - } + // Axes + final axisPaint = Paint()..color = Colors.white24..strokeWidth = 1; + canvas.drawLine(Offset(20, cy), Offset(size.width - 20, cy), axisPaint); + canvas.drawLine(Offset(cx, 20), Offset(cx, size.height - 20), axisPaint); - // 2. Axes - final axisPaint = Paint() - ..color = Colors.white.withAlpha(40) - ..strokeWidth = 1.5; - canvas.drawLine(Offset(0, cy), Offset(size.width, cy), axisPaint); - canvas.drawLine(Offset(cx, 0), Offset(cx, size.height), axisPaint); + final aRad = angA * math.pi / 180.0; + final bRad = angB * math.pi / 180.0; - // 3. Permanent Watermark (Saqel Lab - Anti-Copy Protection) - const watermarkSpan = TextSpan( - text: 'منصة صَقِل التعليمية الذكية © Saqel Lab', - style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold), - ); - final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout(); - watermarkPainter.paint(canvas, const Offset(12, 12)); + final ax = magA * math.cos(aRad); + final ay = -magA * math.sin(aRad); - // Scale factor: pixels per Newton - const scale = 1.0; + final bMult = isSubtraction ? -1.0 : 1.0; + final bx = magB * math.cos(bRad) * bMult; + final by = -magB * math.sin(bRad) * bMult; // Vector A - final aRad = angA * math.pi / 180.0; - final ax = magA * math.cos(aRad) * scale; - final ay = -magA * math.sin(aRad) * scale; // Screen Y inverted - _drawArrow(canvas, Offset(cx, cy), Offset(cx + ax, cy + ay), AppColors.saqelCyan, 3.5, label: 'A (${magA.toInt()} N)'); + _drawArrow(canvas, Offset(cx, cy), Offset(cx + ax, cy + ay), AppColors.saqelCyan, 3.0, label: 'A'); - // Vector B - final bRad = angB * math.pi / 180.0; - final bx = magB * math.cos(bRad) * scale; - final by = -magB * math.sin(bRad) * scale; - _drawArrow(canvas, Offset(cx, cy), Offset(cx + bx, cy + by), AppColors.guardianAmber, 3.5, label: 'B (${magB.toInt()} N)'); + // Vector B (Head-to-tail for addition) + _drawArrow(canvas, Offset(cx + ax, cy + ay), Offset(cx + ax + bx, cy + ay + by), AppColors.guardianAmber, 2.5, label: isSubtraction ? '-B' : 'B'); // Resultant Vector R - if (showResultant) { - final rx = ax + bx; - final ry = ay + by; - final rMag = math.sqrt(rx * rx + ry * ry) / scale; - - // Parallelogram guide lines - final guidePaint = Paint() - ..color = Colors.white.withAlpha(30) - ..strokeWidth = 1; - canvas.drawLine(Offset(cx + ax, cy + ay), Offset(cx + rx, cy + ry), guidePaint); - canvas.drawLine(Offset(cx + bx, cy + by), Offset(cx + rx, cy + ry), guidePaint); - - _drawArrow(canvas, Offset(cx, cy), Offset(cx + rx, cy + ry), AppColors.emeraldGreen, 4.5, label: 'R (${rMag.toInt()} N)'); - } - } - - void _drawArrow(Canvas canvas, Offset from, Offset to, Color color, double width, {String? label}) { - final paint = Paint() - ..color = color - ..strokeWidth = width - ..strokeCap = StrokeCap.round; - - canvas.drawLine(from, to, paint); - - final dx = to.dx - from.dx; - final dy = to.dy - from.dy; - final angle = math.atan2(dy, dx); - const headLen = 12.0; - - final path = Path() - ..moveTo(to.dx, to.dy) - ..lineTo(to.dx - headLen * math.cos(angle - math.pi / 6), to.dy - headLen * math.sin(angle - math.pi / 6)) - ..lineTo(to.dx - headLen * math.cos(angle + math.pi / 6), to.dy - headLen * math.sin(angle + math.pi / 6)) - ..close(); - - final fillPaint = Paint()..color = color; - canvas.drawPath(path, fillPaint); - - if (label != null) { - final textSpan = TextSpan( - text: label, - style: TextStyle(color: color, fontSize: 11.5, fontWeight: FontWeight.w800), - ); - final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); - textPainter.paint(canvas, Offset(to.dx + 6, to.dy - 12)); - } + final rx = ax + bx; + final ry = ay + by; + _drawArrow(canvas, Offset(cx, cy), Offset(cx + rx, cy + ry), AppColors.emeraldGreen, 3.5, label: 'R'); } @override bool shouldRepaint(covariant _VectorCanvasPainter oldDelegate) => true; } -// ============================================================================== -// CUSTOM PAINTER: 1D MOTION TRACK & CAR -// ============================================================================== -class _MotionTrackPainter extends CustomPainter { - final double xPosition; - final double velocity; - final double acceleration; +/// 3. Vector Product Painter +class _VectorProductPainter extends CustomPainter { + final double magA; + final double magB; + final double angleBetween; - _MotionTrackPainter({ - required this.xPosition, - required this.velocity, - required this.acceleration, - }); + _VectorProductPainter({required this.magA, required this.magB, required this.angleBetween}); + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width * 0.35; + final cy = size.height * 0.65; + + _drawWatermark(canvas); + + // Vector A along +X + _drawArrow(canvas, Offset(cx, cy), Offset(cx + magA, cy), AppColors.saqelCyan, 3.0, label: 'A'); + + // Vector B at angleBetween + final rad = angleBetween * math.pi / 180.0; + final bx = cx + magB * math.cos(rad); + final by = cy - magB * math.sin(rad); + _drawArrow(canvas, Offset(cx, cy), Offset(bx, by), AppColors.guardianAmber, 3.0, label: 'B'); + + // Angle Arc + final arcRect = Rect.fromCircle(center: Offset(cx, cy), radius: 30); + final arcPaint = Paint()..color = Colors.white54..style = PaintingStyle.stroke..strokeWidth = 1.5; + canvas.drawArc(arcRect, 0, -rad, false, arcPaint); + + final textSpan = TextSpan(text: '${angleBetween.toInt()}°', style: const TextStyle(color: Colors.white, fontSize: 11)); + final tp = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); + tp.paint(canvas, Offset(cx + 34, cy - 18)); + } + + @override + bool shouldRepaint(covariant _VectorProductPainter oldDelegate) => true; +} + +/// 4. Magnetic Bottle Painter +class _MagneticBottlePainter extends CustomPainter { + final double animPhase; + final double bStrength; + final double velocity; + + _MagneticBottlePainter({required this.animPhase, required this.bStrength, required this.velocity}); @override void paint(Canvas canvas, Size size) { final w = size.width; - final trackY = size.height * 0.6; + final h = size.height; + final cy = h / 2; + + _drawWatermark(canvas); + + // Draw convergent Magnetic Bottle Field Lines + final bPaint = Paint()..color = const Color(0xFF00F5D4).withAlpha(45)..style = PaintingStyle.stroke..strokeWidth = 1.5; + + for (double offset = -60; offset <= 60; offset += 30) { + final path = Path(); + path.moveTo(20, cy + offset * 0.4); + path.cubicTo(w * 0.25, cy + offset * 1.2, w * 0.75, cy + offset * 1.2, w - 20, cy + offset * 0.4); + canvas.drawPath(path, bPaint); + } + + // Charged particle helical trajectory + final px = w * 0.5 + (w * 0.35) * math.sin(animPhase); + final py = cy + 30 * math.sin(animPhase * 8); + + // Glow + canvas.drawCircle(Offset(px, py), 12, Paint()..color = const Color(0xFFFF2D55).withAlpha(80)); + canvas.drawCircle(Offset(px, py), 6, Paint()..color = const Color(0xFFFF2D55)); + + // Force vector + _drawArrow(canvas, Offset(px, py), Offset(px, py - 25), AppColors.saqelCyan, 2.0, label: 'F_B'); + } + + @override + bool shouldRepaint(covariant _MagneticBottlePainter oldDelegate) => true; +} + +/// 5. 1D Motion Track Painter +class _Motion1DTrackPainter extends CustomPainter { + final double xPosition; + final double velocity; + final double acceleration; + + _Motion1DTrackPainter({required this.xPosition, required this.velocity, required this.acceleration}); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final trackY = size.height * 0.65; final originX = w * 0.35; - const scale = 2.0; // pixels per meter + const scale = 2.0; - // 1. Watermark - const watermarkSpan = TextSpan( - text: 'منصة صَقِل التعليمية الذكية © Saqel Lab', - style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold), - ); - final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout(); - watermarkPainter.paint(canvas, const Offset(12, 12)); + _drawWatermark(canvas); - // 2. Track Line - final trackPaint = Paint() - ..color = Colors.white.withAlpha(35) - ..strokeWidth = 2; + // Track + final trackPaint = Paint()..color = Colors.white30..strokeWidth = 2; canvas.drawLine(Offset(20, trackY), Offset(w - 20, trackY), trackPaint); - // 3. Markings (-40m to +80m) for (int m = -40; m <= 80; m += 20) { final px = originX + m * scale; if (px >= 20 && px <= w - 20) { - final isOrigin = (m == 0); - final markPaint = Paint() - ..color = isOrigin ? AppColors.saqelCyan : Colors.white.withAlpha(50) - ..strokeWidth = isOrigin ? 2 : 1; - canvas.drawLine(Offset(px, trackY - 8), Offset(px, trackY + 8), markPaint); - - final textSpan = TextSpan( - text: '${m}m', - style: TextStyle(color: isOrigin ? AppColors.saqelCyan : AppColors.textSecondaryDark, fontSize: 10), - ); - final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); - textPainter.paint(canvas, Offset(px - textPainter.width / 2, trackY + 12)); + canvas.drawLine(Offset(px, trackY - 6), Offset(px, trackY + 6), Paint()..color = Colors.white24); } } - // 4. Vehicle Body + // Car Body final carX = (originX + xPosition * scale).clamp(30.0, w - 30.0); - final carRect = RRect.fromRectAndRadius( - Rect.fromCenter(center: Offset(carX, trackY - 14), width: 44, height: 22), - const Radius.circular(6), - ); - final carPaint = Paint()..color = AppColors.appleBlue; - canvas.drawRRect(carRect, carPaint); - final borderPaint = Paint() - ..color = AppColors.saqelCyan - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - canvas.drawRRect(carRect, borderPaint); + final carRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(carX, trackY - 14), width: 44, height: 22), const Radius.circular(6)); + canvas.drawRRect(carRect, Paint()..color = AppColors.appleBlue); + canvas.drawCircle(Offset(carX - 12, trackY - 2), 5, Paint()..color = Colors.black); + canvas.drawCircle(Offset(carX + 12, trackY - 2), 5, Paint()..color = Colors.black); - // Wheels - final wheelPaint = Paint()..color = const Color(0xFF1E293B); - canvas.drawCircle(Offset(carX - 12, trackY - 2), 5, wheelPaint); - canvas.drawCircle(Offset(carX + 12, trackY - 2), 5, wheelPaint); - - // 5. Velocity Vector Arrow - if (velocity.abs() > 0.3) { - final vLen = (velocity * 2.5).clamp(-50.0, 50.0); - final vPaint = Paint() - ..color = AppColors.saqelCyan - ..strokeWidth = 2.5; - canvas.drawLine(Offset(carX, trackY - 32), Offset(carX + vLen, trackY - 32), vPaint); + // Velocity Vector + if (velocity.abs() > 0.5) { + _drawArrow(canvas, Offset(carX, trackY - 28), Offset(carX + (velocity * 2.5).clamp(-45.0, 45.0), trackY - 28), AppColors.saqelCyan, 2.5, label: 'v'); } } @override - bool shouldRepaint(covariant _MotionTrackPainter oldDelegate) => true; + bool shouldRepaint(covariant _Motion1DTrackPainter oldDelegate) => true; +} + +/// 6. Projectile Painter +class _ProjectilePainter extends CustomPainter { + final double v0; + final double angleDeg; + final double curTime; + final double totalTime; + final double curX; + final double curY; + final double maxRange; + final double maxHeight; + + _ProjectilePainter({ + required this.v0, + required this.angleDeg, + required this.curTime, + required this.totalTime, + required this.curX, + required this.curY, + required this.maxRange, + required this.maxHeight, + }); + + @override + void paint(Canvas canvas, Size size) { + final groundY = size.height * 0.85; + const startX = 40.0; + final availableWidth = size.width - 80; + final scaleX = maxRange > 0 ? availableWidth / maxRange : 1.0; + final scaleY = maxHeight > 0 ? (groundY - 40) / maxHeight : 1.0; + + _drawWatermark(canvas); + + // Ground line + canvas.drawLine(Offset(20, groundY), Offset(size.width - 20, groundY), Paint()..color = Colors.white30..strokeWidth = 2); + + // Parabolic Trajectory Path + final path = Path(); + final angleRad = angleDeg * math.pi / 180.0; + for (double t = 0; t <= totalTime; t += 0.05) { + final x = v0 * math.cos(angleRad) * t; + final y = v0 * math.sin(angleRad) * t - 0.5 * 9.8 * t * t; + final px = startX + x * scaleX; + final py = groundY - y * scaleY; + if (t == 0) { + path.moveTo(px, py); + } else { + path.lineTo(px, py); + } + } + canvas.drawPath(path, Paint()..color = AppColors.saqelCyan.withAlpha(100)..style = PaintingStyle.stroke..strokeWidth = 2); + + // Projectile Ball + final curPx = startX + curX * scaleX; + final curPy = groundY - curY * scaleY; + canvas.drawCircle(Offset(curPx, curPy), 8, Paint()..color = const Color(0xFFFF2D55)); + } + + @override + bool shouldRepaint(covariant _ProjectilePainter oldDelegate) => true; +} + +/// 7. Newton Second Law Free Body Diagram (FBD) +class _NewtonFBDPainter extends CustomPainter { + final double appliedF; + final double frictionF; + final double normalF; + final double weightF; + final double mass; + + _NewtonFBDPainter({ + required this.appliedF, + required this.frictionF, + required this.normalF, + required this.weightF, + required this.mass, + }); + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width / 2; + final cy = size.height / 2; + + _drawWatermark(canvas); + + // Box + final boxRect = Rect.fromCenter(center: Offset(cx, cy), width: 60, height: 60); + canvas.drawRect(boxRect, Paint()..color = const Color(0xFF1E293B)); + canvas.drawRect(boxRect, Paint()..color = AppColors.saqelCyan..style = PaintingStyle.stroke..strokeWidth = 2); + + final labelSpan = TextSpan(text: '${mass.toInt()} kg', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)); + final tp = TextPainter(text: labelSpan, textDirection: TextDirection.ltr)..layout(); + tp.paint(canvas, Offset(cx - tp.width / 2, cy - tp.height / 2)); + + // Applied Force (Right) + _drawArrow(canvas, Offset(cx + 30, cy), Offset(cx + 30 + appliedF * 1.5, cy), AppColors.saqelCyan, 3.0, label: 'F_app'); + + // Friction Force (Left) + _drawArrow(canvas, Offset(cx - 30, cy), Offset(cx - 30 - frictionF * 1.5, cy), const Color(0xFFFF2D55), 2.5, label: 'f_k'); + + // Normal Force (Up) + _drawArrow(canvas, Offset(cx, cy - 30), Offset(cx, cy - 30 - normalF * 1.0), AppColors.guardianAmber, 2.5, label: 'F_N'); + + // Weight Force (Down) + _drawArrow(canvas, Offset(cx, cy + 30), Offset(cx, cy + 30 + weightF * 1.0), Colors.white70, 2.5, label: 'F_g'); + } + + @override + bool shouldRepaint(covariant _NewtonFBDPainter oldDelegate) => true; +} + +/// 8. Seatbelt & Collision Inertia Painter +class _SeatbeltCrashPainter extends CustomPainter { + final bool withSeatbelt; + final double forceG; + final double speedKmh; + + _SeatbeltCrashPainter({required this.withSeatbelt, required this.forceG, required this.speedKmh}); + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width / 2; + final cy = size.height / 2; + + _drawWatermark(canvas); + + // Vehicle barrier + final barrierPaint = Paint()..color = Colors.white24..strokeWidth = 4; + canvas.drawLine(Offset(size.width - 50, 40), Offset(size.width - 50, size.height - 40), barrierPaint); + + // Car silhouette + final carRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy), width: 140, height: 65), const Radius.circular(12)); + canvas.drawRRect(carRect, Paint()..color = const Color(0xFF131C2E)); + canvas.drawRRect(carRect, Paint()..color = withSeatbelt ? AppColors.emeraldGreen : const Color(0xFFFF2D55)..style = PaintingStyle.stroke..strokeWidth = 2); + + // Airbag & Seatbelt representation + if (withSeatbelt) { + canvas.drawCircle(Offset(cx + 40, cy), 18, Paint()..color = Colors.white.withAlpha(200)); + canvas.drawLine(Offset(cx - 20, cy - 20), Offset(cx + 20, cy + 20), Paint()..color = AppColors.emeraldGreen..strokeWidth = 4); + } else { + // Violent impact arrow + _drawArrow(canvas, Offset(cx, cy), Offset(size.width - 52, cy), const Color(0xFFFF2D55), 4.0, label: 'عزم تصادم صلب'); + } + } + + @override + bool shouldRepaint(covariant _SeatbeltCrashPainter oldDelegate) => true; +} + +// ----------------------------------------------------------------------------- +// HELPER CANVAS DRAWING UTILITIES +// ----------------------------------------------------------------------------- +void _drawWatermark(Canvas canvas) { + const watermarkSpan = TextSpan( + text: 'منصة صَقِل التعليمية الذكية © Saqel Virtual Lab', + style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold), + ); + final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout(); + watermarkPainter.paint(canvas, const Offset(12, 12)); +} + +void _drawArrow(Canvas canvas, Offset from, Offset to, Color color, double width, {String? label}) { + if (!from.dx.isFinite || !from.dy.isFinite || !to.dx.isFinite || !to.dy.isFinite) return; + + final paint = Paint() + ..color = color + ..strokeWidth = width + ..strokeCap = StrokeCap.round; + + canvas.drawLine(from, to, paint); + + final dx = to.dx - from.dx; + final dy = to.dy - from.dy; + final angle = math.atan2(dy, dx); + const headLen = 10.0; + + final path = Path() + ..moveTo(to.dx, to.dy) + ..lineTo(to.dx - headLen * math.cos(angle - math.pi / 6), to.dy - headLen * math.sin(angle - math.pi / 6)) + ..lineTo(to.dx - headLen * math.cos(angle + math.pi / 6), to.dy - headLen * math.sin(angle + math.pi / 6)) + ..close(); + + canvas.drawPath(path, Paint()..color = color); + + if (label != null) { + final textSpan = TextSpan( + text: label, + style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.w800), + ); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); + textPainter.paint(canvas, Offset(to.dx + 4, to.dy - 12)); + } } diff --git a/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart b/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart index ffee0df..f033771 100644 --- a/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart +++ b/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart @@ -12,6 +12,7 @@ import '../../../logic/cubits/dashboard_cubits.dart'; import '../../widgets/luxury_widgets.dart'; import '../curriculum/subjects_grid_screen.dart'; import '../notebook/smart_error_notebook_screen.dart'; +import '../vocational/vocational_training_screen.dart'; class UnifiedHomeScreen extends StatefulWidget { final UserModel user; @@ -319,6 +320,71 @@ class _UnifiedHomeScreenState extends State { ), const SizedBox(height: 12), + // Vocational Training Corporation (VTC) & EV Lab Gateway + InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const VocationalTrainingScreen()), + ); + }, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF064E3B), Color(0xFF022C22)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFF10B981).withAlpha(160), width: 1.5), + boxShadow: [ + BoxShadow( + color: const Color(0xFF10B981).withAlpha(35), + blurRadius: 18, + offset: const Offset(0, 5), + ), + ], + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF10B981), Color(0xFF059669)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(CupertinoIcons.wrench_fill, color: Colors.black, size: 24), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'مؤسسة التدريب المهني (VTC Hub) ⚡', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14.5), + ), + SizedBox(height: 4), + Text( + '140 مهنة • 8 قطاعات صناعية • مختبر فحص المركبات الكهربائية (EV)', + style: TextStyle(color: Color(0xFF6EE7B7), fontSize: 11.5, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const Icon(CupertinoIcons.chevron_back, color: Color(0xFF10B981), size: 18), + ], + ), + ), + ), + const SizedBox(height: 12), + // Smart Error Notebook & Remediation Pathway Gateway InkWell( onTap: () { diff --git a/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart b/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart new file mode 100644 index 0000000..3661606 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart @@ -0,0 +1,934 @@ +import 'dart:math' as math; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../widgets/luxury_widgets.dart'; + +/// ============================================================================== +/// SAQEL ENTERPRISE (EDTECH 2.0) - VOCATIONAL TRAINING CORPORATION (VTC) HUB +/// ============================================================================== +/// +/// بوابة منظومة مؤسسة التدريب المهني (VTC) والتعليم التقني والتدريب المهني: +/// 1. دليل الـ 140 مهنة وحرفة موزعة على 8 قطاعات إنتاجية وصناعية وطنية. +/// 2. تخصص صيانة وتشخيص المركبات الكهربائية والهجينة (EV & Hybrid Specialist). +/// 3. محاكي فحص بطاريات الجهد العالي ونظام العزل (HVIL & BMS 120 FPS Lab). +/// 4. نظام الكفايات المهنية (CBT) ومعايير السلامة المهنية للمركبات الكهربائية (NFPA 70E). +class VocationalTrainingScreen extends StatefulWidget { + const VocationalTrainingScreen({super.key}); + + @override + State createState() => _VocationalTrainingScreenState(); +} + +class _VocationalTrainingScreenState extends State + with SingleTickerProviderStateMixin { + int _selectedSectorIndex = 0; + int _activeTabIndex = 0; // 0 = 140 Trades Directory, 1 = EV Diagnostic Lab, 2 = CBT Modules + + // EV Lab Parameters + double _batterySoc = 82.0; // State of Charge % + double _packVoltage = 384.0; // Volts DC + double _isolationResistance = 5.2; // Mega-Ohms (Threshold > 0.5 MΩ) + bool _hvilInterlockClosed = true; // High Voltage Interlock Loop + bool _contactorPrechargeActive = true; + String _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; + bool _simulationRunning = true; + late final AnimationController _pulseController; + + final List> _sectors = [ + { + 'title': 'المركبات والطاقة المتجددة', + 'icon': CupertinoIcons.bolt_car, + 'color': Color(0xFF10B981), + 'tradesCount': 18, + 'featured': 'ميكانيك وتشخيص المركبات الكهربائية والهجينة (EV)', + 'description': 'فحص أنظمة الجهد العالي، بطاريات الليثيوم، ومحولات التيار (Inverters).', + 'trades': [ + 'تشخيص وصيانة المركبات الكهربائية والهجينة', + 'كهرباء وإلكترونيات السيارات الحديثة', + 'ميكانيك محركات الديزل والبنزين المتقدم', + 'تركيب وصيانة أنظمة الطاقة الشمسية الكهروضوئية', + 'صيانة طواحين الرياح وأنظمة التوليد الهجينة', + 'فحص بطاريات الجهد العالي وإعادة تدوير الخلايا', + ], + }, + { + 'title': 'التكنولوجيا والتحول الرقمي', + 'icon': CupertinoIcons.device_laptop, + 'color': Color(0xFF00F5D4), + 'tradesCount': 22, + 'featured': 'أمن الشبكات والدعم الفني السحابي', + 'description': 'هندسة الشبكات المحلية، الألياف الضوئية، وصيانة الخوادم والأنظمة الذكية.', + 'trades': [ + 'فني صيانة شبكات الحاسوب والألياف الضوئية', + 'الدعم الفني لأنظمة الحوسبة السحابية', + 'فني صيانة الهواتف الذكية والأجهزة اللوحية', + 'تشغيل وبرمجة أنظمة التحكم الرقمي (CNC)', + 'فني تركيب كاميرات المراقبة وأنظمة الأمان الذكية', + ], + }, + { + 'title': 'الصناعات الهندسية والميكانيكية', + 'icon': CupertinoIcons.wrench_fill, + 'color': Color(0xFFF59E0B), + 'tradesCount': 28, + 'featured': 'اللحام وتشكيل المعادن المتقدم (TIG/MIG)', + 'description': 'الخراطة، تشكيل المعادن، واللحام بالأرغون وخطوط الإنتاج المؤتمتة.', + 'trades': [ + 'لحام الأنابيب الصناعية وضغط الغاز (TIG/MIG)', + 'الخراطة والتفريز وتشكيل المعادن المبرمج', + 'صيانة خطوط الإنتاج والآلات الصناعية', + 'التمديدات الكهروميكانيكية للمصانع', + 'تشكيل وهيكلة الألمنيوم والواجهات المعمارية', + ], + }, + { + 'title': 'الضيافة والسياحة والفندقة', + 'icon': CupertinoIcons.building_2_fill, + 'color': Color(0xFFEC4899), + 'tradesCount': 16, + 'featured': 'فنون الطهي وإنتاج الأغذية الفندقية', + 'description': 'إدارة المكاتب الأمامية، فنون الطهي الدولي، وخدمة الغرف والضيافة.', + 'trades': [ + 'إنتاج الأغذية والطهي الشرقي والغربي', + 'فنون الحلويات والمخبوزات الفرنسية', + 'إدارة المطاعم وخدمة الضيوف الراقية', + 'الإشراف الداخلي وإدارة الغرف الفندقية', + ], + }, + { + 'title': 'الزراعة الحديثة والري الذكي', + 'icon': CupertinoIcons.leaf_arrow_circlepath, + 'color': Color(0xFF22C55E), + 'tradesCount': 14, + 'featured': 'الزراعة المائية والبيوت البلاستيكية الذكية', + 'description': 'أنظمة الزراعة بدون تربة (Hydroponics)، الري المحوسب، وتنسيق الحدائق.', + 'trades': [ + 'تقنيات الزراعة المائية والعمودية المغلقة', + 'إدارة وصيانة شبكات الري بالتنقيط المؤتمتة', + 'إنتاج النباتات الطبية والعطرية العضوية', + 'تنسيق وتصميم الحدائق والمتنزهات الخضراء', + ], + }, + { + 'title': 'الصناعات الدوائية والكيميائية', + 'icon': CupertinoIcons.lab_flask_solid, + 'color': Color(0xFF8B5CF6), + 'tradesCount': 12, + 'featured': 'تشغيل خطوط التعبئة والتغليف الدوائي', + 'description': 'تطبيق معايير ممارسات التصنيع الجيد (GMP) ومراقبة جودة الإنتاج الكيميائي.', + 'trades': [ + 'فني تشغيل آلات التصنيع الدوائي المعقم', + 'مساعد مراقبة الجودة والتحاليل الكيميائية', + 'صيانة المضخات والمفاعلات الكيميائية', + ], + }, + { + 'title': 'الحلاقة والتجميل والعناية', + 'icon': CupertinoIcons.scissors, + 'color': Color(0xFFF43F5E), + 'tradesCount': 15, + 'featured': 'تصفيف الشعر والتجميل الاحترافي', + 'description': 'تقنيات العناية بالبشرة، التصميم المسرحي، وإدارة الصالونات الحديثة.', + 'trades': [ + 'الحلاقة وتصفيف الشعر الرجالي المتقدم', + 'التجميل وتصفيف الشعر النسائي', + 'العناية بالبشرة والمساج العلاجي', + ], + }, + { + 'title': 'الحرف التقليدية والمشغولات اليدوية', + 'icon': CupertinoIcons.cube_box_fill, + 'color': Color(0xFFD97706), + 'tradesCount': 15, + 'featured': 'صياغة الذهب والمجوهرات والفسيفساء', + 'description': 'إحياء التراث الحرفي الأردني، فن الفسيفساء المادبي، وحفر الخشب التراثي.', + 'trades': [ + 'صياغة الحلي والمجوهرات وتركيب الأحجار الكريمة', + 'فن الفسيفساء والترميم الأثري الحرفي', + 'النجارة التراثية والحفر على الخشب والزخرفة', + 'الخزف والفخار الفني والتشكيل اليدوي', + ], + }, + ]; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + void _triggerFault(String faultCode, String description, double rIso, bool hvilState) { + setState(() { + _activeDtcCode = '$faultCode: $description'; + _isolationResistance = rIso; + _hvilInterlockClosed = hvilState; + if (!hvilState || rIso < 0.5) { + _contactorPrechargeActive = false; + } + }); + } + + void _clearFaults() { + setState(() { + _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; + _isolationResistance = 5.2; + _hvilInterlockClosed = true; + _contactorPrechargeActive = true; + }); + } + + @override + Widget build(BuildContext context) { + final activeSector = _sectors[_selectedSectorIndex]; + + return Scaffold( + backgroundColor: AppColors.darkBackground, + appBar: AppBar( + backgroundColor: const Color(0xFF091620), + elevation: 0, + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFF10B981).withAlpha(30), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF10B981).withAlpha(100)), + ), + child: const Icon(CupertinoIcons.wrench_fill, color: Color(0xFF10B981), size: 18), + ), + const SizedBox(width: 10), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'مؤسسة التدريب المهني (VTC Hub)', + style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800), + ), + Text( + '140 مهنة وحرفة معتمدة • الشراكة الاستراتيجية للتحول المهني', + style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11), + ), + ], + ), + ], + ), + ), + body: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + // Top Executive Stats Strip + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFF0A1E1D), + border: Border(bottom: BorderSide(color: Color(0xFF10B981), width: 0.8)), + ), + child: Row( + children: [ + _buildExecutiveStat('140 مهنة', 'دليل المهن الوزاري', CupertinoIcons.square_grid_2x2_fill), + _buildVerticalDivider(), + _buildExecutiveStat('8 قطاعات', 'سلاسل القيمة الصناعية', CupertinoIcons.layers_fill), + _buildVerticalDivider(), + _buildExecutiveStat('EV 384V', 'مختبر فحص الهايبرد والكهرباء', CupertinoIcons.bolt_car_fill), + _buildVerticalDivider(), + _buildExecutiveStat('CBT Level 3', 'الكفايات والسلامة الدولية', CupertinoIcons.checkmark_seal_fill), + ], + ), + ), + + // Navigation Tabs (Directory / EV Lab / CBT) + Container( + color: AppColors.darkSurface, + child: Row( + children: [ + _buildTopTabItem(0, 'دليل الـ 140 مهنة (8 قطاعات) 📋', CupertinoIcons.list_bullet), + _buildTopTabItem(1, 'مختبر فحص المركبات الكهربائية ⚡', CupertinoIcons.bolt_fill), + _buildTopTabItem(2, 'شهادات الكفايات المهنية (CBT) 🎓', CupertinoIcons.rosette), + ], + ), + ), + + // Tab View Body + Expanded( + child: IndexedStack( + index: _activeTabIndex, + children: [ + _buildTradesDirectoryTab(activeSector), + _buildEvDiagnosticLabTab(), + _buildCbtCompetenciesTab(), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildTopTabItem(int index, String label, IconData icon) { + final isSelected = _activeTabIndex == index; + + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _activeTabIndex = index), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isSelected ? const Color(0xFF10B981) : Colors.transparent, + width: 3, + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: isSelected ? const Color(0xFF10B981) : AppColors.textSecondaryDark), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : AppColors.textSecondaryDark, + fontSize: 11.5, + fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } + + Widget _buildExecutiveStat(String title, String subtitle, IconData icon) { + return Expanded( + child: Row( + children: [ + Icon(icon, color: const Color(0xFF10B981), size: 18), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12.5)), + Text(subtitle, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 9.5), maxLines: 1, overflow: TextOverflow.ellipsis), + ], + ), + ), + ], + ), + ); + } + + Widget _buildVerticalDivider() { + return Container( + width: 1, + height: 24, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: Colors.white12, + ); + } + + // =========================================================================== + // TAB 1: 140 VOCATIONAL TRADES DIRECTORY & 8 SECTORS + // =========================================================================== + Widget _buildTradesDirectoryTab(Map activeSector) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Sector Pill Chips Row + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _sectors.asMap().entries.map((entry) { + final idx = entry.key; + final sec = entry.value; + final isSelected = _selectedSectorIndex == idx; + final secColor = sec['color'] as Color; + + return GestureDetector( + onTap: () => setState(() => _selectedSectorIndex = idx), + child: Container( + margin: const EdgeInsets.only(left: 8), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? secColor.withAlpha(40) : AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isSelected ? secColor : AppColors.darkCardBorder, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(sec['icon'] as IconData, size: 15, color: isSelected ? secColor : Colors.white70), + const SizedBox(width: 6), + Text( + sec['title'] as String, + style: TextStyle( + color: isSelected ? Colors.white : Colors.white70, + fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500, + fontSize: 12, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: secColor.withAlpha(60), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${sec['tradesCount']}', + style: TextStyle(color: secColor, fontSize: 10, fontWeight: FontWeight.w900), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + + // Featured Specialization Spotlight Card + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + (activeSector['color'] as Color).withAlpha(40), + const Color(0xFF0F1A28), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: (activeSector['color'] as Color).withAlpha(120), width: 1.5), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 16, offset: Offset(0, 6)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (activeSector['color'] as Color).withAlpha(40), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(activeSector['icon'] as IconData, color: activeSector['color'] as Color, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activeSector['title'] as String, + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w800), + ), + const SizedBox(height: 2), + Text( + 'التخصص الاستراتيجي: ${activeSector['featured']}', + style: TextStyle(color: activeSector['color'] as Color, fontSize: 12, fontWeight: FontWeight.w700), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + activeSector['description'] as String, + style: const TextStyle(color: Colors.white70, fontSize: 12.5, height: 1.5), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Trades List + const Text( + 'المهن والحرف المعتمدة في هذا القطاع:', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5), + ), + const SizedBox(height: 10), + + ...((activeSector['trades'] as List).asMap().entries.map((tEntry) { + final idx = tEntry.key + 1; + final tradeTitle = tEntry.value; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Colors.white.withAlpha(10), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + '$idx', + style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + tradeTitle, + style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFF10B981).withAlpha(20), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFF10B981).withAlpha(80)), + ), + child: const Text('دبلوم مهني VTC', style: TextStyle(color: Color(0xFF10B981), fontSize: 10, fontWeight: FontWeight.bold)), + ), + ], + ), + ); + }).toList()), + ], + ); + } + + // =========================================================================== + // TAB 2: INTERACTIVE EV & HYBRID DIAGNOSTIC VIRTUAL LAB (120 FPS) + // =========================================================================== + Widget _buildEvDiagnosticLabTab() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Live Diagnostics Canvas (Battery Pack & Inverter) + Container( + height: 280, + decoration: BoxDecoration( + color: const Color(0xFF070E14), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981).withAlpha(80) : const Color(0xFFFF2D55), + width: 1.5, + ), + boxShadow: const [ + BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 6)), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(19), + child: CustomPaint( + painter: _EvDiagnosticsPainter( + batterySoc: _batterySoc, + packVoltage: _packVoltage, + isolationResistance: _isolationResistance, + hvilClosed: _hvilInterlockClosed, + isFaulted: !_activeDtcCode.startsWith('P0000'), + ), + ), + ), + ), + const SizedBox(height: 14), + + // Live Diagnostic Telemetry Grid + Row( + children: [ + _buildTelemetryCard( + 'حالة الشحن (SoC)', + '${_batterySoc.toInt()}%', + const Color(0xFF10B981), + CupertinoIcons.battery_75_percent, + ), + const SizedBox(width: 8), + _buildTelemetryCard( + 'جهد البطارية (Pack V)', + '${_packVoltage.toInt()} V', + AppColors.saqelCyan, + CupertinoIcons.bolt_fill, + ), + const SizedBox(width: 8), + _buildTelemetryCard( + 'مقاومة العزل (R_iso)', + '${_isolationResistance.toStringAsFixed(1)} MΩ', + _isolationResistance >= 0.5 ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + CupertinoIcons.shield_lefthalf_fill, + ), + const SizedBox(width: 8), + _buildTelemetryCard( + 'حلقة القفل (HVIL)', + _hvilInterlockClosed ? 'مغلقة (آمن)' : 'مفتوحة (خطر)', + _hvilInterlockClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + CupertinoIcons.lock_shield_fill, + ), + ], + ), + const SizedBox(height: 14), + + // Active DTC Code Box + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF0F261E) : const Color(0xFF330E14), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + ), + ), + child: Row( + children: [ + Icon( + _activeDtcCode.startsWith('P0000') ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.exclamationmark_triangle_fill, + color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('كود التشخيص والأعطال (OBD-II DTC Readout):', style: TextStyle(color: Colors.white70, fontSize: 11)), + const SizedBox(height: 2), + Text( + _activeDtcCode, + style: TextStyle( + color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + fontWeight: FontWeight.w800, + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 14), + + // Simulated Fault Injection Controls + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('حقن الأعطال المخبرية (Fault Injection Simulation):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13)), + const SizedBox(height: 4), + const Text('اختر عطلاً كهربائياً لاختبار استجابة نظام إدارة البطارية (BMS) وفصل قواطع الجهد العالي:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11)), + const SizedBox(height: 12), + + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + OutlinedButton.icon( + icon: const Icon(CupertinoIcons.shield_slash, color: Color(0xFFFF2D55), size: 16), + label: const Text('عطل تسريب العزل (P0AA6 - Loss of Isolation)', style: TextStyle(color: Colors.white, fontSize: 11.5)), + style: OutlinedButton.styleFrom(side: const BorderSide(color: Color(0xFFFF2D55))), + onPressed: () => _triggerFault('P0AA6', 'Hybrid Battery Voltage Isolation Fault (Leakage < 0.5 MΩ)', 0.1, true), + ), + OutlinedButton.icon( + icon: const Icon(CupertinoIcons.lock_slash_fill, color: Color(0xFFF59E0B), size: 16), + label: const Text('قطع حلقة الأمان (P0A0D - HVIL Open Circuit)', style: TextStyle(color: Colors.white, fontSize: 11.5)), + style: OutlinedButton.styleFrom(side: const BorderSide(color: Color(0xFFF59E0B))), + onPressed: () => _triggerFault('P0A0D', 'High Voltage System Interlock Circuit Open (HVIL Broken)', 5.2, false), + ), + ElevatedButton.icon( + icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16), + label: const Text('إعادة ضبط وتشغيل النظام السليم (Clear DTC)', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold)), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF10B981), foregroundColor: Colors.black), + onPressed: _clearFaults, + ), + ], + ), + ], + ), + ), + ], + ); + } + + Widget _buildTelemetryCard(String label, String value, Color color, IconData icon) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Column( + children: [ + Icon(icon, color: color, size: 18), + const SizedBox(height: 4), + Text(value, style: TextStyle(color: color, fontWeight: FontWeight.w800, fontSize: 13.5)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 9.5), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis), + ], + ), + ), + ); + } + + // =========================================================================== + // TAB 3: CBT COMPETENCIES & CERTIFICATION STANDARDS + // =========================================================================== + Widget _buildCbtCompetenciesTab() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Level 3 Certification Banner + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF132A1C), Color(0xFF0F1A28)], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFF10B981), width: 1.5), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(CupertinoIcons.rosette, color: Color(0xFF10B981), size: 24), + SizedBox(width: 10), + Text( + 'نظام التدريب المبني على الكفايات (CBT Level 3)', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15), + ), + ], + ), + SizedBox(height: 8), + Text( + 'برنامج معتمد دولياً لتأهيل فنيي المركبات الكهربائية والهجينة وفق معايير السلامة المهنية العالمية (NFPA 70E و ISO 26262 و ECE-R100). يشترط إتقان 5 كفايات إلزامية لنيل رخصة الفحص الميداني.', + style: TextStyle(color: Colors.white70, fontSize: 12.5, height: 1.5), + ), + ], + ), + ), + const SizedBox(height: 16), + + _buildCbtModuleCard( + 'الكفاية 1: إجراءات السلامة وفصل الجهد العالي (HV De-energization)', + 'التحقق من خلو الجهد الكهربائي (Zero Voltage Verification) واستخدام قفازات الفئة Class 0 (1000V).', + '95% إتقان مطلوب', + CupertinoIcons.shield_fill, + const Color(0xFF10B981), + ), + _buildCbtModuleCard( + 'الكفاية 2: تشخيص خلايا بطارية الجهد العالي (BMS Cell Balancing)', + 'قراءة قيم الجهد لكل خلية (3.2V - 4.2V) وفحص مقاومة العزل وتوازن درجات الحرارة.', + 'معتمد دولياً', + CupertinoIcons.battery_full, + AppColors.saqelCyan, + ), + _buildCbtModuleCard( + 'الكفاية 3: فحص محول التيار والمحرك الكهربائي (Inverter & Motor)', + 'فحص إشارات التردد العالي (PWM Gate Signals) وقياس مقاومة أطوار المحرك الثلاثية (U-V-W).', + 'مستوى متقدم', + CupertinoIcons.bolt_horizontal_fill, + const Color(0xFFF59E0B), + ), + _buildCbtModuleCard( + 'الكفاية 4: بروتوكول حلقة الأمان وقفل الفحص (HVIL Loop Diagnostics)', + 'كشف انقطاع أسلاك التوصيل السريع وإشارات تأكيد إغلاق غطاء مقبس الخدمة (Service Plug).', + 'إلزامي للفحص', + CupertinoIcons.lock_shield, + const Color(0xFF8B5CF6), + ), + ], + ); + } + + Widget _buildCbtModuleCard(String title, String desc, String badge, IconData icon, Color color) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.darkSurface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.darkCardBorder), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withAlpha(30), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withAlpha(20), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withAlpha(80)), + ), + child: Text(badge, style: TextStyle(color: color, fontSize: 10.5, fontWeight: FontWeight.bold)), + ), + ], + ), + const SizedBox(height: 6), + Text(desc, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12, height: 1.4)), + ], + ), + ), + ], + ), + ); + } +} + +// ============================================================================= +// EV DIAGNOSTICS CUSTOM PAINTER (120 FPS HIGH-VOLTAGE ARCHITECTURE) +// ============================================================================= +class _EvDiagnosticsPainter extends CustomPainter { + final double batterySoc; + final double packVoltage; + final double isolationResistance; + final bool hvilClosed; + final bool isFaulted; + + _EvDiagnosticsPainter({ + required this.batterySoc, + required this.packVoltage, + required this.isolationResistance, + required this.hvilClosed, + required this.isFaulted, + }); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + final cy = h / 2; + + // Watermark + const watermarkSpan = TextSpan( + text: 'منصة صَقِل • مختبر فحص المركبات الكهربائية VTC EV Lab', + style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold), + ); + final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout(); + watermarkPainter.paint(canvas, const Offset(12, 12)); + + // 1. High Voltage Battery Pack Silhouette + final bRect = RRect.fromRectAndRadius( + Rect.fromLTWH(24, cy - 65, w * 0.40, 130), + const Radius.circular(16), + ); + canvas.drawRRect(bRect, Paint()..color = const Color(0xFF0F1E29)); + canvas.drawRRect(bRect, Paint()..color = const Color(0xFF10B981)..style = PaintingStyle.stroke..strokeWidth = 2); + + // Battery Pack label + const bSpan = TextSpan( + text: 'بطارية الجهد العالي (384V DC)\nLi-Ion NMC Battery Pack', + style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold), + ); + final btp = TextPainter(text: bSpan, textDirection: TextDirection.rtl)..layout(); + btp.paint(canvas, Offset(36, cy - 45)); + + // SoC Bar inside pack + final socW = (w * 0.40 - 24) * (batterySoc / 100.0); + canvas.drawRRect( + RRect.fromRectAndRadius(Rect.fromLTWH(36, cy + 20, socW, 16), const Radius.circular(6)), + Paint()..color = const Color(0xFF10B981), + ); + + // 2. Inverter & Motor Unit + final invRect = RRect.fromRectAndRadius( + Rect.fromLTWH(w * 0.60, cy - 65, w * 0.34, 130), + const Radius.circular(16), + ); + canvas.drawRRect(invRect, Paint()..color = const Color(0xFF1E1A2E)); + canvas.drawRRect( + invRect, + Paint() + ..color = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFF8B5CF6) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + final invSpan = TextSpan( + text: 'محول القدرة والمحرك (Inverter)\nAC 3-Phase Permanent Magnet', + style: TextStyle(color: isFaulted ? const Color(0xFFFF2D55) : Colors.white, fontSize: 11, fontWeight: FontWeight.bold), + ); + final itp = TextPainter(text: invSpan, textDirection: TextDirection.rtl)..layout(); + itp.paint(canvas, Offset(w * 0.60 + 12, cy - 45)); + + // 3. High-Voltage Bus Cables (Orange Cables +HV and -HV) + final hvCableColor = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFFFF7A00); // Standard Orange EV Cable + final cablePaint = Paint()..color = hvCableColor..strokeWidth = 4..strokeCap = StrokeCap.round; + + // +HV Cable (Top) + canvas.drawLine(Offset(24 + w * 0.40, cy - 20), Offset(w * 0.60, cy - 20), cablePaint); + // -HV Cable (Bottom) + canvas.drawLine(Offset(24 + w * 0.40, cy + 20), Offset(w * 0.60, cy + 20), cablePaint); + + // Cable Voltage Badge + final voltSpan = TextSpan( + text: isFaulted ? '⚠️ جهد معزول تلقائياً' : '+384V DC Bus', + style: TextStyle(color: hvCableColor, fontSize: 10, fontWeight: FontWeight.w800), + ); + final vtp = TextPainter(text: voltSpan, textDirection: TextDirection.ltr)..layout(); + vtp.paint(canvas, Offset(w * 0.46, cy - 36)); + + // 4. HVIL Interlock Loop Indicator + final hvilColor = hvilClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55); + final hvilPaint = Paint()..color = hvilColor..strokeWidth = 2..style = PaintingStyle.stroke; + canvas.drawCircle(Offset(w * 0.50, cy + 45), 8, hvilPaint); + canvas.drawCircle(Offset(w * 0.50, cy + 45), 4, Paint()..color = hvilColor); + } + + @override + bool shouldRepaint(covariant _EvDiagnosticsPainter oldDelegate) => true; +}