import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/houses_session_model.dart'; import 'symbol_detail_screen.dart'; enum GeometryViewMode { constellation, // Sacred polygon / constellation of chosen houses allChains, // All chains overlaid singleChain, // Focus on one specific chain } class MatrixGeometryScreen extends StatefulWidget { final HousesSession session; final int? initialChainIndex; // Optional focus on chain 1..N const MatrixGeometryScreen({ super.key, required this.session, this.initialChainIndex, }); @override State createState() => _MatrixGeometryScreenState(); } class _MatrixGeometryScreenState extends State with SingleTickerProviderStateMixin { final TransformationController _transformController = TransformationController(); late GeometryViewMode _viewMode; int _selectedChainIndex = 1; // 1-based index late AnimationController _animController; static const double kCellSize = 64.0; static const double kCellMargin = 3.0; static const double kHeaderHeight = 72.0; static const List kChainColors = [ Color(0xFF00E5FF), // Cyan Color(0xFFFF4081), // Pink / Magenta Color(0xFFFFD54F), // Gold Color(0xFFB388FF), // Purple Color(0xFF00E676), // Emerald Color(0xFFFF6E40), // Coral Color(0xFF448AFF), // Electric Blue Color(0xFFEEFF41), // Lime Color(0xFFFF80AB), // Rose Gold ]; @override void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: const Duration(seconds: 4), )..repeat(); if (widget.initialChainIndex != null && widget.initialChainIndex! > 0 && widget.initialChainIndex! <= widget.session.chains.length) { _viewMode = GeometryViewMode.singleChain; _selectedChainIndex = widget.initialChainIndex!; } else { _viewMode = GeometryViewMode.constellation; } _transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0); } @override void dispose() { _animController.dispose(); _transformController.dispose(); super.dispose(); } void _resetZoom() { HapticFeedback.lightImpact(); setState(() { _transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0); }); } String _getFigureName(int pointsCount) { switch (pointsCount) { case 1: return 'Монада (Точка Первоистока)'; case 2: return 'Диада (Вектор Полярности)'; case 3: return 'Триада (Дельта Творения)'; case 4: return 'Тетрада (Земной Четверик)'; case 5: return 'Пентаграмма (Узел Пяти Стихий)'; case 6: return 'Гексаграмма (Печать Равновесия)'; case 7: return 'Гептада (Семизвездный Венец)'; case 8: return 'Октада (Роза Пространства)'; case 9: return 'Эннеаграмма (Девятикратное Полотно Судьбы)'; default: return 'Сакральная Геометрия ($pointsCount узлов)'; } } @override Widget build(BuildContext context) { const totalWidth = 30 * (kCellSize + kCellMargin * 2); const totalHeight = 33 * (kCellSize + kCellMargin * 2) + kHeaderHeight; final selectedChain = widget.session.chains.isNotEmpty && _selectedChainIndex <= widget.session.chains.length ? widget.session.chains[_selectedChainIndex - 1] : null; return Scaffold( backgroundColor: const Color(0xFF0B0D18), appBar: AppBar( backgroundColor: const Color(0xFF101323), elevation: 0, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Геометрия Матрицы 33×30', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), Text( _viewMode == GeometryViewMode.constellation ? _getFigureName(widget.session.selectedHouses.length) : (_viewMode == GeometryViewMode.allChains ? 'Все Цепочки Домов (${widget.session.chains.length})' : 'Цепочка #$_selectedChainIndex: ${selectedChain?.endTypeTitle ?? ""}'), style: const TextStyle( fontSize: 11, color: Color(0xFFFFD54F), letterSpacing: 0.3, ), ), ], ), actions: [ IconButton( tooltip: 'Центрировать масштаб', icon: const Icon(Icons.center_focus_strong_rounded, color: Color(0xFF90CAF9)), onPressed: _resetZoom, ), IconButton( tooltip: 'Справка по геометрии', icon: const Icon(Icons.info_outline_rounded, color: Color(0xFFCFD8DC)), onPressed: () => _showHelpDialog(context), ), ], ), body: Stack( children: [ // 1. Zoomable & Pannable Matrix Canvas InteractiveViewer( transformationController: _transformController, constrained: false, minScale: 0.15, maxScale: 2.5, boundaryMargin: const EdgeInsets.all(500), child: SizedBox( width: totalWidth, height: totalHeight, child: Stack( children: [ _buildBaseGrid(totalWidth, totalHeight), AnimatedBuilder( animation: _animController, builder: (context, child) { return CustomPaint( size: Size(totalWidth, totalHeight), painter: _MatrixGeometryPainter( session: widget.session, viewMode: _viewMode, selectedChainIndex: _selectedChainIndex, pulseValue: _animController.value, chainColors: kChainColors, ), ); }, ), ], ), ), ), // 2. Top Control Filter Bar (Modes & Chains selector) Positioned( top: 12, left: 16, right: 16, child: _buildModeSelectorBar(), ), // 3. Bottom Information Card Positioned( bottom: 16, left: 16, right: 16, child: _buildBottomInfoCard(selectedChain), ), ], ), ); } Widget _buildModeSelectorBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: const Color(0xE613162A), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2C3258), width: 0.8), boxShadow: const [ BoxShadow( color: Color(0x66000000), blurRadius: 16, offset: Offset(0, 6), ), ], ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildModeChip( icon: Icons.auto_awesome_rounded, label: 'Фигура (${widget.session.selectedHouses.length})', isSelected: _viewMode == GeometryViewMode.constellation, activeColor: const Color(0xFFFFD54F), onTap: () { HapticFeedback.selectionClick(); setState(() => _viewMode = GeometryViewMode.constellation); }, ), const SizedBox(width: 6), _buildModeChip( icon: Icons.hub_rounded, label: 'Все Цепочки', isSelected: _viewMode == GeometryViewMode.allChains, activeColor: const Color(0xFF00E5FF), onTap: () { HapticFeedback.selectionClick(); setState(() => _viewMode = GeometryViewMode.allChains); }, ), const SizedBox(width: 8), Container(width: 1, height: 24, color: const Color(0xFF2C3258)), const SizedBox(width: 8), for (int i = 1; i <= widget.session.chains.length; i++) ...[ _buildChainChip(i), const SizedBox(width: 6), ], ], ), ), ); } Widget _buildModeChip({ required IconData icon, required String label, required bool isSelected, required Color activeColor, required VoidCallback onTap, }) { return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isSelected ? activeColor.withValues(alpha: 0.22) : Colors.transparent, borderRadius: BorderRadius.circular(10), border: Border.all( color: isSelected ? activeColor : const Color(0xFF2C3258), width: isSelected ? 1.2 : 0.6, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: isSelected ? activeColor : const Color(0xFF90A4AE)), const SizedBox(width: 5), Text( label, style: TextStyle( color: isSelected ? Colors.white : const Color(0xFF90A4AE), fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, fontSize: 12, ), ), ], ), ), ), ); } Widget _buildChainChip(int index) { final isSelected = _viewMode == GeometryViewMode.singleChain && _selectedChainIndex == index; final color = kChainColors[(index - 1) % kChainColors.length]; return Material( color: Colors.transparent, child: InkWell( onTap: () { HapticFeedback.selectionClick(); setState(() { _viewMode = GeometryViewMode.singleChain; _selectedChainIndex = index; }); }, borderRadius: BorderRadius.circular(10), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6), decoration: BoxDecoration( color: isSelected ? color.withValues(alpha: 0.24) : Colors.transparent, borderRadius: BorderRadius.circular(10), border: Border.all( color: isSelected ? color : const Color(0xFF2C3258), width: isSelected ? 1.2 : 0.6, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 7, height: 7, decoration: BoxDecoration(shape: BoxShape.circle, color: color), ), const SizedBox(width: 5), Text( '#$index', style: TextStyle( color: isSelected ? Colors.white : const Color(0xFFB0BEC5), fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 12, ), ), ], ), ), ), ); } Widget _buildBottomInfoCard(HouseChain? selectedChain) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xF2121528), borderRadius: BorderRadius.circular(18), border: Border.all(color: const Color(0xFF2E355F), width: 0.8), boxShadow: const [ BoxShadow( color: Color(0x77000000), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.session.querentWord != null) ...[ Row( children: [ const Icon(Icons.stars_rounded, color: Color(0xFFFFD54F), size: 15), const SizedBox(width: 5), Text( 'Кверент: «${widget.session.querentWord!.word.toUpperCase()}» (Дом #${(widget.session.querentHouseIndex! + 1).toString().padLeft(3, "0")})', style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 11.5, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 6), ], Row( children: [ if (_viewMode == GeometryViewMode.constellation) ...[ const Icon(Icons.star_rounded, color: Color(0xFFFFD54F), size: 20), const SizedBox(width: 8), Expanded( child: Text( _getFigureName(widget.session.selectedHouses.length), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0x33FFD54F), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFFFD54F), width: 0.6), ), child: Text( '${widget.session.selectedHouses.length} узлов', style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ] else if (_viewMode == GeometryViewMode.allChains) ...[ const Icon(Icons.hub_rounded, color: Color(0xFF00E5FF), size: 20), const SizedBox(width: 8), const Expanded( child: Text( 'Сводная Паутина Судеб (Все Цепочки)', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), Text( '${widget.session.chains.length} лучей', style: const TextStyle( color: Color(0xFF00E5FF), fontSize: 12, fontWeight: FontWeight.bold, ), ), ] else if (selectedChain != null) ...[ Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: kChainColors[(_selectedChainIndex - 1) % kChainColors.length], ), ), const SizedBox(width: 8), Expanded( child: Text( 'Цепочка #$_selectedChainIndex: ${selectedChain.endTypeTitle}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: selectedChain.statusColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), border: Border.all(color: selectedChain.statusColor, width: 0.6), ), child: Text( '${selectedChain.steps.length} звеньев', style: TextStyle( color: selectedChain.statusColor, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ], ), const SizedBox(height: 8), Text( _viewMode == GeometryViewMode.constellation ? 'Выбранные дома образуют сакральный сигил на полотне 33×30. Линии соединяют дома в порядке выпадения жребия, создавая геометрию пространства расклада.' : (_viewMode == GeometryViewMode.allChains ? 'Трассировка движения импульсов от каждого открытого дома (до 9 звеньев). Узоры переплетаются сквозь матрицу, выявляя пересечения и фокусы судьбы.' : (selectedChain?.description ?? '')), style: const TextStyle( color: Color(0xFFCFD8DC), fontSize: 12.5, height: 1.4, ), ), ], ), ); } Widget _buildBaseGrid(double totalWidth, double totalHeight) { final chosenIndices = { for (final h in widget.session.selectedHouses) h.houseIndex: h }; return Column( children: [ SizedBox( height: kHeaderHeight, child: Row( children: List.generate(10, (sectorIdx) { final sector = widget.session.sectors[sectorIdx]; final triadWidth = 3 * (kCellSize + kCellMargin * 2); return SizedBox( width: triadWidth, height: kHeaderHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFF1B1F38), sector.color.withValues(alpha: 0.18), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.circular(12), border: Border.all(color: sector.color.withValues(alpha: 0.5), width: 0.8), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'АРХОНТ ${sectorIdx + 1}', style: TextStyle( color: sector.color, fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.8, ), ), const SizedBox(height: 2), Text( sector.archonWord.word.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ), ); }), ), ), for (int r = 0; r < 33; r++) Row( children: [ for (int c = 0; c < 30; c++) _buildCellWidget(r, c, chosenIndices[r * 30 + c]), ], ), ], ); } Widget _buildCellWidget(int row, int col, HouseCell? chosenCell) { final isChosen = chosenCell != null; final cell = widget.session.grid[row * 30 + col]; final isQuerent = widget.session.querentHouseIndex != null && cell.houseIndex == widget.session.querentHouseIndex; return Container( width: kCellSize, height: kCellSize, margin: const EdgeInsets.all(kCellMargin), decoration: BoxDecoration( color: isQuerent ? const Color(0xFF381E52) : (isChosen ? const Color(0xFF2A224D) : const Color(0xFF101323).withValues(alpha: 0.75)), borderRadius: BorderRadius.circular(10), border: Border.all( color: isQuerent ? const Color(0xFFFFD54F) : (isChosen ? const Color(0xFFFFD54F) : const Color(0xFF1E223D).withValues(alpha: 0.8)), width: isQuerent ? 2.2 : (isChosen ? 1.5 : 0.6), ), boxShadow: isQuerent ? [ BoxShadow( color: const Color(0xFFFFD54F).withValues(alpha: 0.6), blurRadius: 12, spreadRadius: 1, ), BoxShadow( color: const Color(0xFFB388FF).withValues(alpha: 0.4), blurRadius: 16, spreadRadius: 2, ), ] : (isChosen ? [ BoxShadow( color: const Color(0xFFFFD54F).withValues(alpha: 0.35), blurRadius: 10, ), ] : null), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () { HapticFeedback.lightImpact(); Navigator.push( context, MaterialPageRoute( builder: (context) => SymbolDetailScreen(word: cell.houseWord), ), ); }, child: Padding( padding: const EdgeInsets.all(3.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (isQuerent) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(4), ), child: const Text( '👑 КВЕРЕНТ', style: TextStyle( color: Colors.black, fontSize: 7.5, fontWeight: FontWeight.w900, ), ), ), const SizedBox(height: 2), ] else if (isChosen) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(4), ), child: Text( '#${chosenCell.selectionOrder}', style: const TextStyle( color: Colors.black, fontSize: 9, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 2), ], Text( cell.houseWord.word, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: isChosen ? Colors.white : const Color(0xFF6B7280), fontSize: 10.5, fontWeight: isChosen ? FontWeight.bold : FontWeight.normal, ), ), Text( '#${(cell.houseIndex + 1).toString().padLeft(3, "0")}', style: TextStyle( color: isChosen ? const Color(0xFFFFD54F) : const Color(0xFF374151), fontSize: 8.5, ), ), ], ), ), ), ), ); } void _showHelpDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF15182D), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), title: const Row( children: [ Icon(Icons.auto_awesome_mosaic_rounded, color: Color(0xFFFFD54F)), SizedBox(width: 8), Text('Сакральная Геометрия', style: TextStyle(color: Colors.white, fontSize: 17)), ], ), content: const SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Полотно 33×30 образует единое координатное поле оракула из 990 домов и 10 архонтов.', style: TextStyle(color: Color(0xFFCFD8DC), fontSize: 13, height: 1.4), ), SizedBox(height: 12), Text( '• Фигура Расклада: золотые лучи соединяют открытые дома в порядке жребия, создавая сигил ситуации.', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 12.5, height: 1.4), ), SizedBox(height: 8), Text( '• Цепочки Домов: неоновые векторы трассируют путь гостя каждого дома (до 9 звеньев). При замыкании возникает «Петля Судьбы», при попадании в имя Архонта — луч восходит к Небесному Престолу.', style: TextStyle(color: Color(0xFF00E5FF), fontSize: 12.5, height: 1.4), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Понятно', style: TextStyle(color: Color(0xFFFFD54F))), ), ], ), ); } } class _MatrixGeometryPainter extends CustomPainter { final HousesSession session; final GeometryViewMode viewMode; final int selectedChainIndex; final double pulseValue; final List chainColors; static const double kCellSize = 64.0; static const double kCellMargin = 3.0; static const double kHeaderHeight = 72.0; _MatrixGeometryPainter({ required this.session, required this.viewMode, required this.selectedChainIndex, required this.pulseValue, required this.chainColors, }); Offset _getCellCenter(int row, int col) { final x = col * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2; final y = kHeaderHeight + row * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2; return Offset(x, y); } Offset _getArchonCenter(int sectorIndex) { final startX = (sectorIndex * 3) * (kCellSize + kCellMargin * 2); final width = 3 * (kCellSize + kCellMargin * 2); return Offset(startX + width / 2, kHeaderHeight / 2); } @override void paint(Canvas canvas, Size size) { if (viewMode == GeometryViewMode.constellation) { _paintConstellation(canvas); } else if (viewMode == GeometryViewMode.allChains) { for (int i = 0; i < session.chains.length; i++) { _paintChain(canvas, session.chains[i], i + 1, false); } } else { if (selectedChainIndex > 0 && selectedChainIndex <= session.chains.length) { _paintChain(canvas, session.chains[selectedChainIndex - 1], selectedChainIndex, true); } } _paintQuerentAnchor(canvas); } void _paintQuerentAnchor(Canvas canvas) { if (session.querentHouseIndex == null) return; final qRow = session.querentHouseIndex! ~/ 30; final qCol = session.querentHouseIndex! % 30; final qCenter = _getCellCenter(qRow, qCol); final wavePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..color = const Color(0xFFFFD54F).withValues(alpha: 0.55 + 0.25 * math.sin(pulseValue * math.pi * 2)); canvas.drawCircle(qCenter, 22.0 + 4.0 * math.sin(pulseValue * math.pi * 2), wavePaint); final auraPaint = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFFB388FF).withValues(alpha: 0.20 + 0.08 * math.cos(pulseValue * math.pi * 2)); canvas.drawCircle(qCenter, 30.0, auraPaint); if (viewMode == GeometryViewMode.constellation) { final tetherPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.4 ..color = const Color(0xFFFFD54F).withValues(alpha: 0.35 + 0.15 * math.sin(pulseValue * math.pi * 2)); for (final h in session.selectedHouses) { final hCenter = _getCellCenter(h.row, h.col); canvas.drawLine(qCenter, hCenter, tetherPaint); } } final coreGlow = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFFFFD54F); canvas.drawCircle(qCenter, 6.5, coreGlow); canvas.drawCircle(qCenter, 2.5, Paint()..color = Colors.white); } void _paintConstellation(Canvas canvas) { final houses = List.from(session.selectedHouses) ..sort((a, b) => (a.selectionOrder ?? 0).compareTo(b.selectionOrder ?? 0)); if (houses.isEmpty) return; final points = houses.map((h) => _getCellCenter(h.row, h.col)).toList(); if (points.length >= 3) { final polyPath = Path()..moveTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { polyPath.lineTo(points[i].dx, points[i].dy); } polyPath.close(); final fillPaint = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFFFFD54F).withValues(alpha: 0.10 + 0.04 * math.sin(pulseValue * math.pi * 2)); canvas.drawPath(polyPath, fillPaint); } final glowPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 6.0 + 1.5 * math.sin(pulseValue * math.pi * 2) ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..color = const Color(0xFFFFD54F).withValues(alpha: 0.35); final corePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.8 ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..color = const Color(0xFFFFE082); final linePath = Path()..moveTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { linePath.lineTo(points[i].dx, points[i].dy); } if (points.length >= 3) { linePath.close(); } canvas.drawPath(linePath, glowPaint); canvas.drawPath(linePath, corePaint); for (int i = 0; i < points.length; i++) { final p = points[i]; final nodeGlow = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFFFFD54F).withValues(alpha: 0.4 + 0.2 * math.sin(pulseValue * math.pi * 2 + i)); canvas.drawCircle(p, 12.0, nodeGlow); final nodeCore = Paint() ..style = PaintingStyle.fill ..color = Colors.white; canvas.drawCircle(p, 5.0, nodeCore); } } void _paintChain(Canvas canvas, HouseChain chain, int chainIdx, bool isFocused) { if (chain.steps.isEmpty) return; final color = chainColors[(chainIdx - 1) % chainColors.length]; final glowWidth = isFocused ? 7.0 : 4.0; final coreWidth = isFocused ? 3.0 : 2.0; final glowPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = glowWidth ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..color = color.withValues(alpha: isFocused ? 0.45 : 0.25); final corePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = coreWidth ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..color = color; final points = []; for (final step in chain.steps) { points.add(_getCellCenter(step.house.row, step.house.col)); } final path = Path()..moveTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { path.lineTo(points[i].dx, points[i].dy); } if (chain.endType == ChainEndType.archonThrone && chain.steps.last.destinationArchon != null) { final archonCenter = _getArchonCenter(chain.steps.last.destinationArchon!.index); path.lineTo(archonCenter.dx, archonCenter.dy); points.add(archonCenter); } if (chain.endType == ChainEndType.loop && points.length >= 2) { final lastP = points.last; final haloPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 3.0 ..color = const Color(0xFFFF5252); canvas.drawCircle(lastP, 18.0 + 4.0 * math.sin(pulseValue * math.pi * 2), haloPaint); } canvas.drawPath(path, glowPaint); canvas.drawPath(path, corePaint); for (int i = 0; i < points.length; i++) { final p = points[i]; final isStart = i == 0; final isEnd = i == points.length - 1; final nodePaint = Paint() ..style = PaintingStyle.fill ..color = isStart ? const Color(0xFFFFD54F) : (isEnd ? const Color(0xFFFF5252) : color); canvas.drawCircle(p, isStart || isEnd ? 6.5 : 4.0, nodePaint); final centerDot = Paint() ..style = PaintingStyle.fill ..color = Colors.white; canvas.drawCircle(p, isStart || isEnd ? 2.5 : 1.5, centerDot); } } @override bool shouldRepaint(covariant _MatrixGeometryPainter oldDelegate) { return true; } }