460 lines
No EOL
20 KiB
Dart
460 lines
No EOL
20 KiB
Dart
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<List<int>> generaDocumentoCai() async {
|
|
PdfDocument? document;
|
|
try {
|
|
final ByteData data = await rootBundle.load('assets/CAI_p1.pdf');
|
|
|
|
// 1. Caricamento e Copia
|
|
final List<int> bytesCopia = data.buffer.asUint8List().toList();
|
|
document = PdfDocument(inputBytes: bytesCopia);
|
|
|
|
final PdfForm form = document.form;
|
|
final PdfPage page = document.pages[0];
|
|
|
|
form.setDefaultAppearance(false);
|
|
|
|
// Mappatura
|
|
Map<String, PdfField> 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 STANDARD (La tua versione preferita) ---
|
|
|
|
// 1. TESTI
|
|
CaiMapping.testi.forEach((keyGlobal, keyPdf) {
|
|
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);
|
|
field.text = valore.toUpperCase();
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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<String> 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
|
|
await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico,
|
|
await _renderGraficoV40(GlobalData.tratti.cast<TrattoPenna>().toList(), GlobalData.elementi.cast<ElementoGrafico>().toList()));
|
|
|
|
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));
|
|
|
|
// =================================================================
|
|
// FASE CRITICA: SALVATAGGIO -> RICARICA -> FLATTEN (Anti-Crash)
|
|
// =================================================================
|
|
|
|
// 1. Salviamo il file compilato in memoria. Questo corregge gli errori interni del PDF.
|
|
List<int> bytesTemporanei = await document.save();
|
|
document.dispose(); // Chiudiamo il vecchio
|
|
|
|
// 2. Riapriamo il file "pulito"
|
|
PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei);
|
|
|
|
// 3. Ora eseguiamo il FLATTEN.
|
|
// È INDISPENSABILE per vedere le X nell'immagine di anteprima.
|
|
// Poiché il file è stato appena rigenerato, NON DOVREBBE CRASHARE.
|
|
try {
|
|
docFinale.form.flattenAllFields();
|
|
} catch (e) {
|
|
debugPrint("⚠️ Errore Flattening anche dopo pulizia: $e");
|
|
// Se fallisce ancora, usiamo il fallback ReadOnly, ma l'immagine potrebbe essere incompleta.
|
|
docFinale.form.readOnly = true;
|
|
}
|
|
|
|
// 4. Salvataggio finale
|
|
List<int> bytesFinali = await docFinale.save();
|
|
docFinale.dispose();
|
|
|
|
return bytesFinali;
|
|
|
|
} catch (e) {
|
|
debugPrint("ERRORE GENERAZIONE PDF: $e");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// --- HELPERS (Standard) ---
|
|
|
|
static bool _scriviX(Map<String, PdfField> mappa, List<String> 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.text = "X";
|
|
} else if (field is PdfCheckBoxField) {
|
|
field.isChecked = true;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void _scriviTesto(Map<String, PdfField> mappa, List<String> 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, 10);
|
|
field.foreColor = PdfColor(0, 0, 0);
|
|
field.text = testo;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool _scriviXRossa(Map<String, PdfField> mappa, List<String> 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.text = "X";
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void _scriviTestoTotale(Map<String, PdfField> mappa, List<String> 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<void> _disegnaInBox(PdfPage page, Map<String, PdfField> 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<Uint8List?> _renderFirmaTight(List<Offset?> 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<Uint8List?> _renderGraficoV40(List<TrattoPenna> tratti, List<ElementoGrafico> 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<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];
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
// 1. SFONDO BIANCO (Risolve il problema del nero)
|
|
final Paint backgroundPaint = Paint()..color = Colors.white;
|
|
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);
|
|
|
|
// 2. DISEGNO GRIGLIA (Opzionale, ma rende il disegno professionale come l'originale)
|
|
final Paint gridPaint = Paint()
|
|
..color = Colors.grey.shade300
|
|
..strokeWidth = 2.0;
|
|
|
|
double step = 40.0; // Dimensione quadretti
|
|
// Linee verticali
|
|
for (double x = 0; x <= size.width; x += step) {
|
|
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
|
|
}
|
|
// Linee orizzontali
|
|
for (double y = 0; y <= size.height; y += step) {
|
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
|
|
}
|
|
|
|
// Se non ci sono tratti o elementi, ci fermiamo qui (abbiamo disegnato solo sfondo e griglia pulita)
|
|
if (tr.isEmpty && el.isEmpty) return;
|
|
|
|
// --- CALCOLO BOUNDING BOX PER IL CONTENUTO ---
|
|
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;
|
|
}
|
|
|
|
// Se non abbiamo trovato nulla (caso raro), usiamo valori di default
|
|
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;
|
|
|
|
// Scala per adattare il disegno al box (Contain)
|
|
double scaleX = size.width / drawingW;
|
|
double scaleY = size.height / drawingH;
|
|
double scale = (scaleX < scaleY) ? scaleX : scaleY;
|
|
|
|
// Centratura
|
|
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 AUTO E TESTI ---
|
|
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 if (e.tipo.startsWith('auto')) {
|
|
int idx = ((e.label ?? "A").codeUnitAt(0) - 65) % palette.length;
|
|
Paint p = Paint()..color = palette[idx];
|
|
|
|
// Corpo auto colorato
|
|
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), p);
|
|
// Bordo auto nero
|
|
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), Paint()..style=PaintingStyle.stroke..color=Colors.black..strokeWidth=2);
|
|
|
|
// Lettera A/B Bianca
|
|
final tp = TextPainter(
|
|
text: TextSpan(text: e.label ?? "A", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)),
|
|
textDirection: TextDirection.ltr
|
|
)..layout();
|
|
tp.paint(canvas, Offset(-tp.width/2, -tp.height/2));
|
|
}
|
|
canvas.restore();
|
|
}
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
} |