cid_app/lib/comp_13.dart

534 lines
19 KiB
Dart
Raw Normal View History

2026-02-27 23:26:13 +01:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'global_data.dart';
import 'models.dart';
import 'comp_15.dart';
class Comp13Screen extends StatefulWidget {
const Comp13Screen({super.key});
@override
State<Comp13Screen> createState() => _Comp13ScreenState();
}
class _Comp13ScreenState extends State<Comp13Screen> {
List<ElementoGrafico> _elementi = [];
List<TrattoPenna> _tratti = [];
String modo = 'penna';
@override
void initState() {
super.initState();
_elementi = List.from(GlobalData.elementi);
_tratti = List.from(GlobalData.tratti);
// Proviamo a chiedere il landscape al sistema (non sempre funziona su iPad)
_setLandscape();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _mostraIstruzioni();
});
}
Future<void> _setLandscape() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
Future<void> _setPortrait() async {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
}
Future<void> _esci() async {
GlobalData.tratti = List.from(_tratti);
GlobalData.elementi = List.from(_elementi);
await _setPortrait();
if (mounted) Navigator.pop(context);
}
Future<void> _vaiAvanti() async {
GlobalData.tratti = List.from(_tratti);
GlobalData.elementi = List.from(_elementi);
// Prima di cambiare pagina, rimettiamo in verticale
// await _setPortrait();
if (mounted) {
await Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp15Screen()));
// Al ritorno, forziamo di nuovo orizzontale
await _setLandscape();
}
}
// --- FUNZIONE HELPER PER RUOTARE I DIALOGHI ---
// Se l'iPad è verticale, ruota il contenuto del dialogo di 90 gradi
Widget _ruotaSeNecessario(BuildContext context, Widget child) {
return OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? RotatedBox(quarterTurns: 1, child: child)
: child;
},
);
}
void _mostraIstruzioni() {
showDialog(
context: context,
builder: (ctx) => _ruotaSeNecessario(ctx, AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Row(
children: [
Icon(Icons.help_outline, color: Colors.blue.shade900),
const SizedBox(width: 10),
const Text("Guida al Disegno", style: TextStyle(fontWeight: FontWeight.bold)),
],
),
content: SizedBox(
width: 500, // Larghezza fissa per evitare problemi di layout ruotato
height: 300, // Altezza fissa per scrollare comodamente
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: const Text(
"• 🖊 Usa la penna per disegnare le strade\n"
"• 🚗 🏍️ 🚛 Seleziona e tocca per aggiungere veicoli\n"
"• ↗️ Inserisci frecce di direzione\n"
"• 📝 Aggiungi testo (es. nomi vie)\n"
"• 🔄 Tocca un elemento per ruotarlo\n"
"• ❌ Premi a lungo un oggetto per eliminarlo\n"
"• 🗑️ Usa il tasto Cestino per resettare tutto",
style: TextStyle(fontSize: 16, height: 1.6, color: Colors.black87),
),
),
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade900,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () => Navigator.pop(ctx),
child: const Text("HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold)),
),
)
],
)),
);
}
void _gestisciInserimento(Offset pos) async {
if (modo == 'testo') {
TextEditingController tc = TextEditingController();
await showDialog(
context: context,
builder: (c) => _ruotaSeNecessario(c, Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
width: 300,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Inserisci Testo", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: tc,
autofocus: true,
decoration: const InputDecoration(hintText: "Nome via...", border: OutlineInputBorder())
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (tc.text.isNotEmpty) {
setState(() => _elementi.add(ElementoGrafico(pos, 'testo', label: tc.text)));
}
Navigator.pop(c);
},
child: const Text("INSERISCI")
)
],
),
),
)),
);
} else if (['auto', 'moto', 'furgone'].contains(modo)) {
int numeroVeicoli = _elementi.where((e) =>
e.tipo.startsWith('auto') ||
e.tipo.startsWith('moto') ||
e.tipo.startsWith('furgone')).length;
String prossimaLettera = String.fromCharCode(65 + numeroVeicoli);
setState(() {
_elementi.add(ElementoGrafico(pos, '$modo$prossimaLettera', label: prossimaLettera));
});
}
}
@override
Widget build(BuildContext context) {
// 1. Logica di orientamento principale (Body)
// Se siamo verticali, ruotiamo tutto di 90 gradi.
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) await _esci();
},
child: OrientationBuilder(
builder: (context, orientation) {
final bool isPortrait = orientation == Orientation.portrait;
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
// Se Portrait -> Ruota Body. Se Landscape -> Body normale.
body: isPortrait
? RotatedBox(quarterTurns: 1, child: _buildBodyContent(context))
: _buildBodyContent(context),
);
},
),
);
}
// Contenuto principale estratto per facilitare la rotazione
Widget _buildBodyContent(BuildContext context) {
// Calcoliamo dimensioni sicure
final size = MediaQuery.of(context).size;
final double maxToolbarHeight = size.shortestSide * 0.8;
return SafeArea(
child: Stack(
children: [
Column(
children: [
Container(
height: 50,
color: Colors.blueGrey[50],
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: _esci,
),
const Expanded(
child: Text(
"13. Grafico",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold),
),
),
IconButton(
icon: const Icon(Icons.help_outline, color: Colors.black),
onPressed: _mostraIstruzioni
),
const SizedBox(width: 15),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8)
),
icon: const Icon(Icons.check),
label: const Text("SALVA"),
onPressed: _vaiAvanti,
),
],
),
),
Expanded(
child: GestureDetector(
onLongPressStart: (d) {
setState(() {
_elementi.removeWhere((e) => e.contiene(d.localPosition));
_tratti.removeWhere((t) => t.contiene(d.localPosition));
});
},
onTapDown: (d) {
bool colpito = false;
for (var e in _elementi) {
if (e.contiene(d.localPosition)) {
setState(() => e.rotazione += 0.785);
colpito = true;
break;
}
}
if (!colpito && (['auto', 'moto', 'furgone', 'testo'].contains(modo))) {
_gestisciInserimento(d.localPosition);
}
},
onPanStart: (d) {
if (modo == 'penna' || modo == 'freccia') {
setState(() => _tratti.add(TrattoPenna([d.localPosition], tipo: modo)));
}
},
onPanUpdate: (d) {
if (modo == 'penna' || modo == 'freccia') {
setState(() => _tratti.last.punti.add(d.localPosition));
}
},
child: Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: PainterV40(_tratti, _elementi),
size: Size.infinite,
),
),
),
),
],
),
Positioned(
left: 10, top: 60,
child: Container(
width: 55,
constraints: BoxConstraints(maxHeight: maxToolbarHeight),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(25),
boxShadow: [const BoxShadow(color: Colors.black26, blurRadius: 4)],
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_toolBtn(Icons.edit, 'penna', Colors.black),
_toolBtn(Icons.trending_flat, 'freccia', Colors.black),
_toolBtn(Icons.title, 'testo', Colors.black),
const Divider(indent: 8, endIndent: 8, height: 15),
_toolBtn(Icons.directions_car, 'auto', Colors.blue),
_toolBtn(Icons.two_wheeler, 'moto', Colors.orange.shade800),
_toolBtn(Icons.local_shipping, 'furgone', Colors.green),
const Divider(indent: 8, endIndent: 8, height: 15),
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.red),
tooltip: "Cancella tutto",
onPressed: () => setState(() { _tratti.clear(); _elementi.clear(); }),
),
],
),
),
),
),
),
],
),
);
}
Widget _toolBtn(IconData icon, String tool, Color activeColor) {
bool isSelected = modo == tool;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => setState(() => modo = tool),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected ? activeColor.withOpacity(0.15) : Colors.transparent,
border: isSelected ? Border.all(color: activeColor, width: 2) : null,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: activeColor,
size: 24,
),
),
),
),
);
}
}
class PainterV40 extends CustomPainter {
final List<TrattoPenna> tr;
final List<ElementoGrafico> el;
PainterV40(this.tr, this.el);
final List<Color> palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red, Colors.teal];
@override
void paint(Canvas canvas, Size size) {
Paint pStrada = Paint()..color = Colors.black..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
for (var t in tr) {
if (t.punti.length > 1) {
// Disegno il tratto stradale
Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy);
for (var pt in t.punti) path.lineTo(pt.dx, pt.dy);
canvas.drawPath(path, pStrada);
// --- CORREZIONE FRECCIA ---
if (t.tipo == 'freccia') {
Offset pTip = t.punti.last;
Offset pBack = t.punti[t.punti.length - 2];
// STABILIZZAZIONE: Cerco un punto precedente che sia distante almeno 10 pixel
// dalla punta. Questo ignora i micro-movimenti finali del dito (jitter)
// che causavano l'inversione della freccia.
for (int i = t.punti.length - 2; i >= 0; i--) {
if ((t.punti[i] - pTip).distance > 10.0) {
pBack = t.punti[i];
break;
}
}
_disegnaPunta(canvas, pBack, pTip, pStrada);
}
// --------------------------
}
}
for (var e in el) {
canvas.save();
canvas.translate(e.posizione.dx, e.posizione.dy);
canvas.rotate(e.rotazione);
if (e.tipo == 'testo') {
_disegnaTesto(canvas, e.label ?? "");
} else if (e.tipo.startsWith('auto')) {
_disegnaAuto(canvas, e);
} else if (e.tipo.startsWith('moto')) {
_disegnaMoto(canvas, e);
} else if (e.tipo.startsWith('furgone')) {
_disegnaFurgone(canvas, e);
}
canvas.restore();
}
}
// ... (TUTTI GLI ALTRI METODI RESTANO IDENTICI) ...
Color _getColoreDaLettera(String lettera) {
if (lettera.isEmpty) return Colors.grey;
int idx = (lettera.codeUnitAt(0) - 65) % palette.length;
return palette[idx];
}
void _disegnaLettera(Canvas canvas, String lettera, {double fontSize = 16}) {
final tp = TextPainter(
text: TextSpan(text: lettera, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
}
void _disegnaAuto(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double w = 48.0;
double h = 24.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5));
canvas.drawRRect(bodyRect, pBody);
Paint pCabin = Paint()..color = Colors.black.withOpacity(0.2);
canvas.drawRect(Rect.fromLTRB(-w/2 + 6, -h/2 + 3, w/4, h/2 - 3), pCabin);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawCircle(Offset(w/2 - 2, h/2 - 4), 2.5, pLights);
canvas.drawCircle(Offset(w/2 - 2, -h/2 + 4), 2.5, pLights);
canvas.drawRRect(bodyRect, pBorder);
_disegnaLettera(canvas, lettera);
}
void _disegnaMoto(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double len = 40.0;
double wid = 14.0;
Paint pBody = Paint()..color = colore;
Paint pBlack = Paint()..color = Colors.black;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0;
canvas.drawCircle(Offset(-len/2 + 4, 0), 5.0, pBlack);
canvas.drawCircle(Offset(len/2 - 4, 0), 5.0, pBlack);
Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: len - 10, height: wid - 4);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder);
Rect sellaRect = Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8);
canvas.drawRect(sellaRect, pBlack);
Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round;
double xHandle = len/2 - 12;
canvas.drawLine(Offset(xHandle, -10), Offset(xHandle, 10), pManubrio);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawCircle(Offset(len/2, 0), 3.0, pLights);
_disegnaLettera(canvas, lettera, fontSize: 10);
}
void _disegnaFurgone(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double w = 60.0;
double h = 26.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2);
canvas.drawRect(caricoRect, pBody);
canvas.drawRect(caricoRect, pBorder);
Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1);
canvas.drawRect(cabinaRect, pBody);
canvas.drawRect(cabinaRect, pBorder);
Paint pVetro = Paint()..color = Colors.black.withOpacity(0.3);
canvas.drawRect(Rect.fromLTRB(w/4 + 2, -h/2 + 3, w/2 - 2, h/2 - 3), pVetro);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawRect(Rect.fromLTWH(w/2 - 2, -h/2 + 2, 2, 4), pLights);
canvas.drawRect(Rect.fromLTWH(w/2 - 2, h/2 - 6, 2, 4), pLights);
_disegnaLettera(canvas, lettera);
}
void _disegnaTesto(Canvas canvas, String txt) {
final tp = TextPainter(
text: TextSpan(text: txt, style: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width/2, -tp.height/2));
}
void _disegnaPunta(Canvas canvas, Offset p1, Offset p2, Paint paint) {
double angle = (p2 - p1).direction;
canvas.drawLine(p2, p2 - Offset.fromDirection(angle - 0.5, 10), paint);
canvas.drawLine(p2, p2 - Offset.fromDirection(angle + 0.5, 10), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}