feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,967 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user