import 'dart:math' as math; import 'package:flutter/material.dart'; /// Radialmenue um den Reticle-Button im Editier-Modus (HTML-Demonstrator: /// #haloSvg mit den drei Ring-Segmenten Loiter/Landing/Remove). enum HaloAction { loiter, landing, remove } class HaloMenu extends StatelessWidget { const HaloMenu({ super.key, required this.loiterActive, required this.landingActive, required this.onAction, }); final bool loiterActive; final bool landingActive; final void Function(HaloAction action) onAction; // Radien/Groesse 1:1 aus dem Demonstrator uebernommen // (HALO_SCALE=1.2, HALO_RIN=27, HALO_ROUT=60, viewBox 192x192). static const _size = 192.0; static const _innerRadius = 27 * 1.2; static const _outerRadius = 60 * 1.2; static const _midRadius = (_innerRadius + _outerRadius) / 2; @override Widget build(BuildContext context) { return SizedBox( width: _size, height: _size, child: Stack( alignment: Alignment.center, children: [ _wedge( key: const ValueKey('halo-loiter'), startAngleDeg: -60, endAngleDeg: 60, active: loiterActive, onTap: () => onAction(HaloAction.loiter), ), _wedge( key: const ValueKey('halo-landing'), startAngleDeg: 60, endAngleDeg: 180, active: landingActive, onTap: () => onAction(HaloAction.landing), ), _wedge( key: const ValueKey('halo-remove'), startAngleDeg: 180, endAngleDeg: 300, active: false, onTap: () => onAction(HaloAction.remove), ), _wedgeIcon(angleDeg: 0, icon: Icons.loop), _wedgeIcon(angleDeg: 120, icon: Icons.flight_land), _wedgeIcon(angleDeg: 240, icon: Icons.remove), ], ), ); } Widget _wedge({ Key? key, required double startAngleDeg, required double endAngleDeg, required bool active, required VoidCallback onTap, }) { return ClipPath( key: key, clipper: _WedgeClipper( startAngleDeg: startAngleDeg, endAngleDeg: endAngleDeg, innerRadius: _innerRadius, outerRadius: _outerRadius, ), child: GestureDetector( onTap: onTap, child: Container( color: active ? const Color(0xFF4D9DFF).withValues(alpha: 0.55) : Colors.black.withValues(alpha: 0.62), ), ), ); } Widget _wedgeIcon({required double angleDeg, required IconData icon}) { final point = _polarPoint(angleDeg, _midRadius); return Positioned( left: _size / 2 + point.dx - 12, top: _size / 2 + point.dy - 12, child: IgnorePointer( child: Icon(icon, color: Colors.white, size: 22), ), ); } static Offset _polarPoint(double angleDeg, double radius) { final rad = angleDeg * math.pi / 180; return Offset(radius * math.sin(rad), -radius * math.cos(rad)); } } class _WedgeClipper extends CustomClipper { const _WedgeClipper({ required this.startAngleDeg, required this.endAngleDeg, required this.innerRadius, required this.outerRadius, }); final double startAngleDeg; final double endAngleDeg; final double innerRadius; final double outerRadius; static const _steps = 18; @override Path getClip(Size size) { final center = Offset(size.width / 2, size.height / 2); Offset pointAt(double angleDeg, double radius) { final rad = angleDeg * math.pi / 180; return Offset( center.dx + radius * math.sin(rad), center.dy - radius * math.cos(rad), ); } final path = Path(); final first = pointAt(startAngleDeg, outerRadius); path.moveTo(first.dx, first.dy); for (var i = 1; i <= _steps; i++) { final a = startAngleDeg + (endAngleDeg - startAngleDeg) * i / _steps; final p = pointAt(a, outerRadius); path.lineTo(p.dx, p.dy); } for (var i = _steps; i >= 0; i--) { final a = startAngleDeg + (endAngleDeg - startAngleDeg) * i / _steps; final p = pointAt(a, innerRadius); path.lineTo(p.dx, p.dy); } path.close(); return path; } @override bool shouldReclip(covariant _WedgeClipper oldClipper) => oldClipper.startAngleDeg != startAngleDeg || oldClipper.endAngleDeg != endAngleDeg || oldClipper.innerRadius != innerRadius || oldClipper.outerRadius != outerRadius; }