import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/services.dart' show rootBundle; import 'package:syncfusion_flutter_pdf/pdf.dart'; import 'package:flutter/material.dart'; import 'global_data.dart'; import 'models.dart'; import 'cai_mapping.dart'; class PdfEngine { static Future> generaDocumentoCai() async { PdfDocument? document; try { final ByteData data = await rootBundle.load('assets/CAI_p1.pdf'); // 1. Caricamento e Copia final List bytesCopia = data.buffer.asUint8List().toList(); document = PdfDocument(inputBytes: bytesCopia); final PdfForm form = document.form; final PdfPage page = document.pages[0]; // form.setDefaultAppearance(false); // REMOVED to fix empty fields issue // Mappatura Campi Map mappaCampi = {}; for (int i = 0; i < form.fields.count; i++) { if (form.fields[i].name != null) { mappaCampi[form.fields[i].name!.trim().toUpperCase()] = form.fields[i]; } } // --- COMPILAZIONE --- // 1. TESTI STANDARD (Escludiamo i Danni per gestirli dopo con split a 20 char) CaiMapping.testi.forEach((keyGlobal, keyPdf) { if (keyGlobal.contains("danni_visibili")) return; String valore = _valoreDaGlobal(keyGlobal); String keyPdfNorm = keyPdf.trim().toUpperCase(); if (mappaCampi.containsKey(keyPdfNorm) && valore.isNotEmpty) { final field = mappaCampi[keyPdfNorm]; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); // Font 8 field.foreColor = PdfColor(0, 0, 0); field.textAlignment = PdfTextAlignment.left; field.text = valore.toUpperCase(); } } }); // 1.1 GESTIONE SPECIALE DANNI (Split forzato a 20 caratteri) _riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_A, "DANNI_VIS_A1", "DANNI_VIS_A2"); _riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_B, "DANNI_VIS_B1", "DANNI_VIS_B2"); // 2. CHECKBOX _scriviX(mappaCampi, [GlobalData.feriti ? CaiMapping.feriti_SI : CaiMapping.feriti_NO]); _scriviX(mappaCampi, [GlobalData.Veicoli_danni_materiali_oltre ? CaiMapping.danni_veicoli_SI : CaiMapping.danni_veicoli_NO]); _scriviX(mappaCampi, [GlobalData.Oggetti_diversi_danni_materiali ? CaiMapping.danni_oggetti_SI : CaiMapping.danni_oggetti_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_A ? CaiMapping.danni_mat_A_SI : CaiMapping.danni_mat_A_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_B ? CaiMapping.danni_mat_B_SI : CaiMapping.danni_mat_B_NO]); String catA = GlobalData.Categoria_cond_A.toUpperCase().trim(); if (catA == 'A') _scriviX(mappaCampi, ['cat_a_A']); else if (catA == 'B') _scriviX(mappaCampi, ['cat_b_A']); else if (catA.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_A'], catA); String catB = GlobalData.Categoria_cond_B.toUpperCase().trim(); if (catB == 'A') _scriviX(mappaCampi, ['cat_a_B']); else if (catB == 'B') _scriviX(mappaCampi, ['cat_b_B']); else if (catB.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_B'], catB); // 3. CIRCOSTANZE int countA = 0; int countB = 0; for (int i = 1; i <= 17; i++) { if (GlobalData.circostanzeA[i] == true) { if (_scriviX(mappaCampi, [i < 10 ? "A0$i" : "A$i"])) countA++; } if (GlobalData.circostanzeB[i] == true) { List nomiTarget = []; if (i == 9) nomiTarget = ["Check Box 26", "CheckBox26", "26"]; else if (i == 10) nomiTarget = ["Check Box 27", "CheckBox27", "27"]; else if (i == 11) nomiTarget = ["Check Box 28", "CheckBox28", "28"]; else if (i == 12) nomiTarget = ["Check Box 29", "CheckBox29", "29"]; else nomiTarget = [i < 10 ? "B0$i" : "B$i"]; if (_scriviX(mappaCampi, nomiTarget)) countB++; } } _scriviTestoTotale(mappaCampi, ['A_TOT', 'A_tot'], countA.toString()); _scriviTestoTotale(mappaCampi, ['B_TOT', 'B_tot'], countB.toString()); // 4. PUNTI URTO for (String punto in GlobalData.puntiUrtoA_List) _scriviXRossa(mappaCampi, [punto]); for (String punto in GlobalData.puntiUrtoB_List) _scriviXRossa(mappaCampi, [punto]); // 5. IMMAGINI E GRAFICO // Render the updated graph (with new vehicle types) to an image Uint8List? graficoBytes = await _renderGraficoV40( GlobalData.tratti.cast().toList(), GlobalData.elementi.cast().toList() ); // Draw the graph image into the specific PDF box await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico, graficoBytes); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_A, await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black)); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black)); // ================================================================= // SALVATAGGIO SICURO // ================================================================= List bytesTemporanei = await document.save(); document.dispose(); PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei); try { docFinale.form.flattenAllFields(); } catch (e) { debugPrint("⚠️ Errore Flattening: $e"); docFinale.form.readOnly = true; } List bytesFinali = await docFinale.save(); docFinale.dispose(); return bytesFinali; } catch (e) { debugPrint("ERRORE GENERAZIONE PDF: $e"); return []; } } // ... (Keep existing helper methods: _riempiCampoSplit, _scriviX, _scriviTesto, _scriviXRossa, _scriviTestoTotale, _disegnaInBox, _renderFirmaTight, _valoreDaGlobal) // Re-pasting them here for completeness to ensure no missing dependencies static void _riempiCampoSplit(Map mappa, String testoCompleto, String key1, String key2) { if (testoCompleto.isEmpty) return; String riga1 = ""; String riga2 = ""; int limite = 20; if (testoCompleto.length <= limite) { riga1 = testoCompleto; } else { int splitIndex = testoCompleto.lastIndexOf(" ", limite); if (splitIndex == -1) splitIndex = limite; riga1 = testoCompleto.substring(0, splitIndex).trim(); riga2 = testoCompleto.substring(splitIndex).trim(); } if (mappa.containsKey(key1)) { final f1 = mappa[key1] as PdfTextBoxField; f1.font = PdfStandardFont(PdfFontFamily.helvetica, 8); f1.textAlignment = PdfTextAlignment.left; f1.text = riga1.toUpperCase(); } if (mappa.containsKey(key2)) { final f2 = mappa[key2] as PdfTextBoxField; f2.font = PdfStandardFont(PdfFontFamily.helvetica, 8); f2.textAlignment = PdfTextAlignment.left; f2.text = riga2.toUpperCase(); } } static bool _scriviX(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 14); field.foreColor = PdfColor(0, 0, 0); field.textAlignment = PdfTextAlignment.center; field.text = "X"; } else if (field is PdfCheckBoxField) { field.isChecked = true; } return true; } } return false; } static void _scriviTesto(Map mappa, List nomiPossibili, String testo) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.foreColor = PdfColor(0, 0, 0); field.text = testo; } return; } } } static bool _scriviXRossa(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold); field.foreColor = PdfColor(255, 0, 0); field.textAlignment = PdfTextAlignment.center; field.text = "X"; return true; } } } return false; } static void _scriviTestoTotale(Map mappa, List nomi, String testo) { for (String nome in nomi) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.textAlignment = PdfTextAlignment.center; field.text = testo; } return; } } } static Future _disegnaInBox(PdfPage page, Map mappa, String nomeCampo, Uint8List? imgBytes) async { String key = nomeCampo.trim().toUpperCase(); if (imgBytes == null || !mappa.containsKey(key)) return; Rect boxRect = mappa[key]!.bounds; PdfBitmap bitmap = PdfBitmap(imgBytes); double imageW = bitmap.width.toDouble(); double imageH = bitmap.height.toDouble(); if (imageW <= 0 || imageH <= 0) return; double ratioX = boxRect.width / imageW; double ratioY = boxRect.height / imageH; double scale = (ratioX < ratioY) ? ratioX : ratioY; double drawW = imageW * scale; double drawH = imageH * scale; double offsetX = boxRect.left + (boxRect.width - drawW) / 2; double offsetY = boxRect.top + (boxRect.height - drawH) / 2; page.graphics.drawImage(bitmap, Rect.fromLTWH(offsetX, offsetY, drawW, drawH)); } static Future _renderFirmaTight(List punti, Color colore) async { if (punti.isEmpty) return null; double minX = double.infinity, minY = double.infinity, maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var p in punti) { if (p != null) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } double padding = 20.0; double firmaW = maxX - minX; double firmaH = maxY - minY; if (firmaW <= 0) firmaW = 1; if (firmaH <= 0) firmaH = 1; double resolutionScale = 3.0; double canvasW = (firmaW + padding * 2) * resolutionScale; double canvasH = (firmaH + padding * 2) * resolutionScale; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.scale(resolutionScale); canvas.translate(-minX + padding, -minY + padding); final paint = Paint()..color = colore..strokeWidth = 5.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; for (int i = 0; i < punti.length - 1; i++) { if (punti[i] != null && punti[i+1] != null) { canvas.drawLine(punti[i]!, punti[i+1]!, paint); } } final img = await recorder.endRecording().toImage(canvasW.toInt(), canvasH.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static Future _renderGraficoV40(List tratti, List elementi) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final size = const Size(2000, 800); final painter = PainterV40(tratti, elementi); painter.paint(canvas, size); final img = await recorder.endRecording().toImage(size.width.toInt(), size.height.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static String _valoreDaGlobal(String key) { switch (key) { case 'data_incidente': return GlobalData.data_incidente; case 'ora': return GlobalData.ora; case 'luogo': return GlobalData.luogo; case 'testimoni': return GlobalData.testimoni; case 'danni_visibili_A': return GlobalData.danni_visibili_A; case 'osservazioni_A': return GlobalData.osservazioni_A; case 'danni_visibili_B': return GlobalData.danni_visibili_B; case 'osservazioni_B': return GlobalData.osservazioni_B; case 'Cognome_contraente_A': return GlobalData.Cognome_contraente_A; case 'Nome_contraente_A': return GlobalData.Nome_contraente_A; case 'Codice_Fiscale_contraente_A': return GlobalData.Codice_Fiscale_contraente_A; case 'Indirizzo_contraente_A': return GlobalData.Indirizzo_contraente_A; case 'CAP_contraente_A': return GlobalData.CAP_contraente_A; case 'Stato_contraente_A': return GlobalData.Stato_contraente_A; case 'N_telefono_mail_contraente_A': return GlobalData.N_telefono_mail_contraente_A; case 'Marca_e_Tipo_A': return GlobalData.Marca_e_Tipo_A; case 'Targa_A': return GlobalData.Targa_A; case 'Stato_immatricolazione_A': return GlobalData.Stato_immatricolazione_A; case 'Rimorchio_A': return GlobalData.Rimorchio_A; case 'Stato_immatricolazione2_A': return GlobalData.Stato_immatricolazione2_A; case 'Denominazione_A': return GlobalData.Denominazione_A; case 'Numero_Polizza_A': return GlobalData.Numero_Polizza_A; case 'N_carta_verde_A': return GlobalData.N_carta_verde_A; case 'Data_Inizio_Dal_A': return GlobalData.Data_Inizio_Dal_A; case 'Data_Scadenza_Al_A': return GlobalData.Data_Scadenza_Al_A; case 'Agenzia_A': return GlobalData.Agenzia_A; case 'Indirizzo_agenzia_A': return GlobalData.Indirizzo_agenzia_A; case 'Stato_agenzia_A': return GlobalData.Stato_agenzia_A; case 'Denominazione_agenzia_A': return GlobalData.Denominazione_agenzia_A; case 'N_tel_mail_agenzia_A': return GlobalData.N_tel_mail_agenzia_A; case 'Cognome_cond_A': return GlobalData.Cognome_cond_A; case 'Nome_cond_A': return GlobalData.Nome_cond_A; case 'Data_nascita_cond_A': return GlobalData.Data_nascita_cond_A; case 'Cod_fiscale_cond_A': return GlobalData.Cod_fiscale_cond_A; case 'Indirizzo_cond_A': return GlobalData.Indirizzo_cond_A; case 'Stato_cond_A': return GlobalData.Stato_cond_A; case 'N_tel_mail_cond_A': return GlobalData.N_tel_mail_cond_A; case 'N_Patente_cond_A': return GlobalData.N_Patente_cond_A; case 'Scadenza_cond_A': return GlobalData.Scadenza_cond_A; case 'Cognome_contraente_B': return GlobalData.Cognome_contraente_B; case 'Nome_contraente_B': return GlobalData.Nome_contraente_B; case 'Codice_Fiscale_contraente_B': return GlobalData.Codice_Fiscale_contraente_B; case 'Indirizzo_contraente_B': return GlobalData.Indirizzo_contraente_B; case 'CAP_contraente_B': return GlobalData.CAP_contraente_B; case 'Stato_contraente_B': return GlobalData.Stato_contraente_B; case 'N_telefono_mail_contraente_B': return GlobalData.N_telefono_mail_contraente_B; case 'Marca_e_Tipo_B': return GlobalData.Marca_e_Tipo_B; case 'Targa_B': return GlobalData.Targa_B; case 'Stato_immatricolazione_B': return GlobalData.Stato_immatricolazione_B; case 'Rimorchio_B': return GlobalData.Rimorchio_B; case 'Stato_immatricolazione2_B': return GlobalData.Stato_immatricolazione2_B; case 'Denominazione_B': return GlobalData.Denominazione_B; case 'Numero_Polizza_B': return GlobalData.Numero_Polizza_B; case 'N_carta_verde_B': return GlobalData.N_carta_verde_B; case 'Data_Inizio_Dal_B': return GlobalData.Data_Inizio_Dal_B; case 'Data_Scadenza_Al_B': return GlobalData.Data_Scadenza_Al_B; case 'Agenzia_B': return GlobalData.Agenzia_B; case 'Indirizzo_agenzia_B': return GlobalData.Indirizzo_agenzia_B; case 'Stato_agenzia_B': return GlobalData.Stato_agenzia_B; case 'Denominazione_agenzia_B': return GlobalData.Denominazione_agenzia_B; case 'N_tel_mail_agenzia_B': return GlobalData.N_tel_mail_agenzia_B; case 'Cognome_cond_B': return GlobalData.Cognome_cond_B; case 'Nome_cond_B': return GlobalData.Nome_cond_B; case 'Data_nascita_cond_B': return GlobalData.Data_nascita_cond_B; case 'Cod_fiscale_cond_B': return GlobalData.Cod_fiscale_cond_B; case 'Indirizzo_cond_B': return GlobalData.Indirizzo_cond_B; case 'Stato_cond_B': return GlobalData.Stato_cond_B; case 'N_tel_mail_cond_B': return GlobalData.N_tel_mail_cond_B; case 'N_Patente_cond_B': return GlobalData.N_Patente_cond_B; case 'Scadenza_cond_B': return GlobalData.Scadenza_cond_B; default: return ""; } } } class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); final List palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red]; @override void paint(Canvas canvas, Size size) { // 1. SFONDO BIANCO final Paint backgroundPaint = Paint()..color = Colors.white; canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint); // 2. GRIGLIA final Paint gridPaint = Paint()..color = Colors.grey.shade300..strokeWidth = 2.0; double step = 40.0; for (double x = 0; x <= size.width; x += step) { canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); } for (double y = 0; y <= size.height; y += step) { canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); } if (tr.isEmpty && el.isEmpty) return; // --- BOUNDING BOX --- double minX = double.infinity, minY = double.infinity; double maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var t in tr) { for (var p in t.punti) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } for (var e in el) { if (e.posizione.dx - 30 < minX) minX = e.posizione.dx - 30; if (e.posizione.dx + 30 > maxX) maxX = e.posizione.dx + 30; if (e.posizione.dy - 30 < minY) minY = e.posizione.dy - 30; if (e.posizione.dy + 30 > maxY) maxY = e.posizione.dy + 30; } if (minX == double.infinity) { minX = 0; maxX = 100; minY = 0; maxY = 100; } double padding = 40.0; double drawingW = maxX - minX + (padding * 2); double drawingH = maxY - minY + (padding * 2); if (drawingW <= 0) drawingW = 100; if (drawingH <= 0) drawingH = 100; double scaleX = size.width / drawingW; double scaleY = size.height / drawingH; double scale = (scaleX < scaleY) ? scaleX : scaleY; double offsetX = (size.width - (drawingW * scale)) / 2; double offsetY = (size.height - (drawingH * scale)) / 2; canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale); canvas.translate(-minX + padding, -minY + padding); // --- DISEGNO STRADE E FRECCE --- Paint pStrada = Paint() ..color = Colors.black ..strokeWidth = 4.0 / scale ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; for (var t in tr) { if (t.punti.length > 1) { 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); if (t.tipo == 'freccia') { double a = (t.punti.last - t.punti[t.punti.length - 2]).direction; canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a - 0.5, 15), pStrada); canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a + 0.5, 15), pStrada); } } } // --- DISEGNO ELEMENTI --- for (var e in el) { canvas.save(); canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { final tp = TextPainter( text: TextSpan(text: e.label ?? "", style: const TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } else { String lettera = e.label ?? "A"; int idx = (lettera.isNotEmpty) ? (lettera.codeUnitAt(0) - 65) % palette.length : 0; Color colore = palette[idx]; if (e.tipo.startsWith('auto')) { _disegnaAuto(canvas, colore, lettera); } else if (e.tipo.startsWith('moto')) { _disegnaMoto(canvas, colore, lettera); } else if (e.tipo.startsWith('furgone')) { _disegnaFurgone(canvas, colore, lettera); } } canvas.restore(); } canvas.restore(); } // --- METODI DI DISEGNO SPECIFICI --- void _disegnaAuto(Canvas canvas, Color colore, String 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; // Corpo RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5)); canvas.drawRRect(bodyRect, pBody); // Cabina 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); // Fari 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); // Bordo canvas.drawRRect(bodyRect, pBorder); // Lettera _disegnaLettera(canvas, lettera); } void _disegnaMoto(Canvas canvas, Color colore, String lettera) { double w = 34.0; double h = 12.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0; Paint pBlack = Paint()..color = Colors.black; // Ruote (piene) canvas.drawCircle(Offset(-w/2 + 4, 0), 5.0, pBlack); canvas.drawCircle(Offset(w/2 - 4, 0), 5.0, pBlack); // Corpo Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: w - 10, height: h - 4); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder); // Sella canvas.drawRect(Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8), pBlack); // Manubrio Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round; canvas.drawLine(Offset(w/2 - 12, -10), Offset(w/2 - 12, 10), pManubrio); // Faro canvas.drawCircle(Offset(w/2, 0), 3.0, Paint()..color = Colors.yellow); _disegnaLettera(canvas, lettera, fontSize: 10); } void _disegnaFurgone(Canvas canvas, Color colore, String 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; // Vano Carico (Box posteriore) Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2); canvas.drawRect(caricoRect, pBody); canvas.drawRect(caricoRect, pBorder); // Cabina (Box anteriore) Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1); canvas.drawRect(cabinaRect, pBody); canvas.drawRect(cabinaRect, pBorder); // Parabrezza 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); // Fari 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 _disegnaLettera(Canvas canvas, String txt, {double fontSize = 16}) { final tp = TextPainter( text: TextSpan(text: txt, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }