968 lines
34 KiB
Dart
968 lines
34 KiB
Dart
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<MatrixGeometryScreen> createState() => _MatrixGeometryScreenState();
|
|
}
|
|
|
|
class _MatrixGeometryScreenState extends State<MatrixGeometryScreen>
|
|
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<Color> 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<Color> 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<HouseCell>.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 = <Offset>[];
|
|
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;
|
|
}
|
|
}
|