From 0b1493a02e9062de96ad2587e50cd227397e9766 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 14 May 2026 22:06:10 +0700 Subject: [PATCH] Show ontology snowflake in admin reviews --- lib/screens/mapflow_shell.dart | 252 ++++++++++++++++++++++++++------- 1 file changed, 198 insertions(+), 54 deletions(-) diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index e97f5fa..b0fb882 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -1025,7 +1025,7 @@ class _AdminVoiceExperienceRow extends StatelessWidget { ), ], const SizedBox(height: 10), - _AdminOntologyTags(selectedTags: selectedTags), + _AdminOntologySnowflake(selectedTags: selectedTags), ], ), ), @@ -1033,75 +1033,219 @@ class _AdminVoiceExperienceRow extends StatelessWidget { } } -class _AdminOntologyTags extends StatelessWidget { - const _AdminOntologyTags({required this.selectedTags}); +class _AdminOntologySnowflake extends StatelessWidget { + const _AdminOntologySnowflake({required this.selectedTags}); final Set selectedTags; - static const _tags = [ - _AdminOntologyTag('energy:calm', 'спокойное'), - _AdminOntologyTag('energy:dynamic', 'живое'), - _AdminOntologyTag('privacy:intimate', 'камерное'), - _AdminOntologyTag('privacy:open', 'открытое'), - _AdminOntologyTag('sociality:solo', 'для себя'), - _AdminOntologyTag('sociality:group', 'для компании'), - _AdminOntologyTag('function:reset', 'выдохнуть'), - _AdminOntologyTag('function:impress', 'впечатлить'), - _AdminOntologyTag('function:transit', 'транзитное'), - _AdminOntologyTag('aesthetic:clean', 'чистое'), - _AdminOntologyTag('aesthetic:expressive', 'выразительное'), + static const _axes = [ + _AdminOntologyAxis( + id: 'energy', + label: 'энергия', + angle: -math.pi / 2, + leaves: [ + _AdminOntologyLeaf('calm', 'спокойное', -0.22), + _AdminOntologyLeaf('dynamic', 'живое', 0.22), + ], + ), + _AdminOntologyAxis( + id: 'privacy', + label: 'приватность', + angle: -math.pi / 2 + math.pi * 2 / 5, + leaves: [ + _AdminOntologyLeaf('intimate', 'камерное', -0.2), + _AdminOntologyLeaf('open', 'открытое', 0.2), + ], + ), + _AdminOntologyAxis( + id: 'function', + label: 'сценарий', + angle: -math.pi / 2 + math.pi * 4 / 5, + leaves: [ + _AdminOntologyLeaf('reset', 'выдохнуть', -0.25), + _AdminOntologyLeaf('impress', 'впечатлить', 0), + _AdminOntologyLeaf('transit', 'транзитное', 0.25), + ], + ), + _AdminOntologyAxis( + id: 'aesthetic', + label: 'образ', + angle: -math.pi / 2 + math.pi * 6 / 5, + leaves: [ + _AdminOntologyLeaf('clean', 'чистое', -0.2), + _AdminOntologyLeaf('expressive', 'выразительное', 0.2), + ], + ), + _AdminOntologyAxis( + id: 'sociality', + label: 'социальность', + angle: -math.pi / 2 + math.pi * 8 / 5, + leaves: [ + _AdminOntologyLeaf('solo', 'для себя', -0.2), + _AdminOntologyLeaf('group', 'для компании', 0.2), + ], + ), ]; @override Widget build(BuildContext context) { - return Wrap( - spacing: 6, - runSpacing: 6, - children: [ - for (final tag in _tags) - _AdminOntologyChip( - label: tag.label, - selected: selectedTags.contains(tag.id), - ), - ], - ); - } -} - -class _AdminOntologyChip extends StatelessWidget { - const _AdminOntologyChip({required this.label, required this.selected}); - - final String label; - final bool selected; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: selected ? const Color(0xFFE11D48) : const Color(0xFFF2EEE8), - borderRadius: BorderRadius.circular(999), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), - child: Text( - label, - style: TextStyle( - color: selected ? Colors.white : const Color(0xFF746A60), - fontSize: 12, - fontWeight: FontWeight.w800, - height: 1, - ), + return SizedBox( + height: 300, + width: double.infinity, + child: CustomPaint( + painter: _AdminOntologySnowflakePainter( + axes: _axes, + selectedTags: selectedTags, ), ), ); } } -class _AdminOntologyTag { - const _AdminOntologyTag(this.id, this.label); +class _AdminOntologySnowflakePainter extends CustomPainter { + const _AdminOntologySnowflakePainter({ + required this.axes, + required this.selectedTags, + }); + + final List<_AdminOntologyAxis> axes; + final Set selectedTags; + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = math.min(size.width, size.height); + final axisRadius = radius * 0.25; + final leafRadius = radius * 0.43; + + final baseLine = Paint() + ..color = const Color(0xFFE6DDD2) + ..strokeWidth = 1.3 + ..style = PaintingStyle.stroke; + final selectedLine = Paint() + ..color = const Color(0xFFE11D48) + ..strokeWidth = 2.2 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + final node = Paint()..color = const Color(0xFFDED3C7); + final selectedNode = Paint()..color = const Color(0xFFE11D48); + final centerNode = Paint()..color = const Color(0xFF241B18); + + canvas.drawCircle(center, 5, centerNode); + _drawLabel(canvas, size, center + const Offset(0, 14), 'место', true); + + for (final axis in axes) { + final axisOffset = Offset(math.cos(axis.angle), math.sin(axis.angle)); + final axisPoint = center + axisOffset * axisRadius; + final hasSelectedLeaf = axis.leaves.any( + (leaf) => selectedTags.contains('${axis.id}:${leaf.id}'), + ); + + canvas.drawLine( + center, + axisPoint, + hasSelectedLeaf ? selectedLine : baseLine, + ); + canvas.drawCircle( + axisPoint, + hasSelectedLeaf ? 5.5 : 4.5, + hasSelectedLeaf ? selectedNode : node, + ); + _drawLabel( + canvas, + size, + axisPoint + axisOffset * 18, + axis.label, + hasSelectedLeaf, + fontSize: 11, + ); + + for (final leaf in axis.leaves) { + final leafAngle = axis.angle + leaf.angleOffset; + final leafOffset = Offset(math.cos(leafAngle), math.sin(leafAngle)); + final leafPoint = center + leafOffset * leafRadius; + final tag = '${axis.id}:${leaf.id}'; + final selected = selectedTags.contains(tag); + + canvas.drawLine( + axisPoint, + leafPoint, + selected ? selectedLine : baseLine, + ); + canvas.drawCircle( + leafPoint, + selected ? 8 : 5.5, + selected ? selectedNode : node, + ); + _drawLabel( + canvas, + size, + leafPoint + leafOffset * 20, + leaf.label, + selected, + ); + } + } + } + + void _drawLabel( + Canvas canvas, + Size size, + Offset anchor, + String label, + bool selected, { + double fontSize = 12, + }) { + final painter = TextPainter( + text: TextSpan( + text: label, + style: TextStyle( + color: selected ? const Color(0xFFE11D48) : const Color(0xFF746A60), + fontSize: fontSize, + fontWeight: selected ? FontWeight.w900 : FontWeight.w700, + height: 1, + ), + ), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(maxWidth: 86); + final dx = (anchor.dx - painter.width / 2).clamp( + 0.0, + size.width - painter.width, + ); + final dy = (anchor.dy - painter.height / 2).clamp( + 0.0, + size.height - painter.height, + ); + painter.paint(canvas, Offset(dx, dy)); + } + + @override + bool shouldRepaint(covariant _AdminOntologySnowflakePainter oldDelegate) { + return oldDelegate.selectedTags != selectedTags; + } +} + +class _AdminOntologyAxis { + const _AdminOntologyAxis({ + required this.id, + required this.label, + required this.angle, + required this.leaves, + }); final String id; final String label; + final double angle; + final List<_AdminOntologyLeaf> leaves; +} + +class _AdminOntologyLeaf { + const _AdminOntologyLeaf(this.id, this.label, this.angleOffset); + + final String id; + final String label; + final double angleOffset; } Set _selectedAdminTags(Map? analysis) {