import 'dart:math' as math; import 'package:flutter/material.dart'; import 'isometric_transform.dart'; // ============================================================================= // LAYER 1: STATIC STRUCTURE PAINTER // ============================================================================= /// Menggambar grid lantai, dinding transparan, dan barisan rak baglog jamur (balok 3D). class StaticKumbungPainter extends CustomPainter { final IsometricTransform transformer; final double roomWidth; // Dimensi Y (lebar) dalam meter final double roomLength; // Dimensi X (panjang) dalam meter final double roomHeight; // Dimensi Z (tinggi) dalam meter const StaticKumbungPainter({ required this.transformer, this.roomWidth = 6.0, this.roomLength = 8.0, this.roomHeight = 2.5, }); @override void paint(Canvas canvas, Size size) { final gridPaint = Paint() ..color = Colors.grey.shade300 ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; final wallPaint = Paint() ..color = Colors.blueGrey.withValues(alpha: 0.15) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; // 1. Menggambar Grid Lantai (Z = 0) // Garis sepanjang X (Panjang) for (double y = 0; y <= roomWidth; y += 1.0) { final pStart = transformer.toScreen(0, y, 0); final pEnd = transformer.toScreen(roomLength, y, 0); canvas.drawLine(pStart, pEnd, gridPaint); } // Garis sepanjang Y (Lebar) for (double x = 0; x <= roomLength; x += 1.0) { final pStart = transformer.toScreen(x, 0, 0); final pEnd = transformer.toScreen(x, roomWidth, 0); canvas.drawLine(pStart, pEnd, gridPaint); } // 2. Menggambar Rak Jamur (3D Blocks) // Kita buat 4 baris rak jamur vertikal (sejajar sumbu X/Panjang) // Format rak: { xStart, xEnd, yStart, yEnd, zHeight } final raks = [ _Rak3D(xStart: 1.0, xEnd: 7.0, yStart: 0.6, yEnd: 1.2, height: 1.8), _Rak3D(xStart: 1.0, xEnd: 7.0, yStart: 1.9, yEnd: 2.5, height: 1.8), _Rak3D(xStart: 1.0, xEnd: 7.0, yStart: 3.5, yEnd: 4.1, height: 1.8), _Rak3D(xStart: 1.0, xEnd: 7.0, yStart: 4.8, yEnd: 5.4, height: 1.8), ]; for (final rak in raks) { _drawRak(canvas, rak); } // 3. Menggambar Outline Dinding Kumbung (Wireframe Box) // Sudut-sudut dasar lantai final p000 = transformer.toScreen(0, 0, 0); final pL00 = transformer.toScreen(roomLength, 0, 0); final p0W0 = transformer.toScreen(0, roomWidth, 0); final pLW0 = transformer.toScreen(roomLength, roomWidth, 0); // Sudut-sudut atap (Z = roomHeight) final p00H = transformer.toScreen(0, 0, roomHeight); final pL0H = transformer.toScreen(roomLength, 0, roomHeight); final p0WH = transformer.toScreen(0, roomWidth, roomHeight); final pLWH = transformer.toScreen(roomLength, roomWidth, roomHeight); // Gambar tiang vertikal sudut canvas.drawLine(p000, p00H, wallPaint); canvas.drawLine(pL00, pL0H, wallPaint); canvas.drawLine(p0W0, p0WH, wallPaint); canvas.drawLine(pLW0, pLWH, wallPaint); // Gambar garis atap final roofPath = Path() ..moveTo(p00H.dx, p00H.dy) ..lineTo(pL0H.dx, pL0H.dy) ..lineTo(pLWH.dx, pLWH.dy) ..lineTo(p0WH.dx, p0WH.dy) ..close(); canvas.drawPath(roofPath, wallPaint); // Gambar garis dasar (jika tertutup) final floorPath = Path() ..moveTo(p000.dx, p000.dy) ..lineTo(pL00.dx, pL00.dy) ..lineTo(pLW0.dx, pLW0.dy) ..lineTo(p0W0.dx, p0W0.dy) ..close(); canvas.drawPath(floorPath, wallPaint); } /// Menggambar balok 3D isometrik untuk rak jamur void _drawRak(Canvas canvas, _Rak3D rak) { final fillPaint = Paint() ..color = Colors.teal.withValues(alpha: 0.12) ..style = PaintingStyle.fill; final borderPaint = Paint() ..color = Colors.teal.withValues(alpha: 0.4) ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; // Titik-titik sudut bawah (Z = 0) final p10 = transformer.toScreen(rak.xEnd, rak.yStart, 0); final p01 = transformer.toScreen(rak.xStart, rak.yEnd, 0); final p11 = transformer.toScreen(rak.xEnd, rak.yEnd, 0); // Titik-titik sudut atas (Z = height) final p00H = transformer.toScreen(rak.xStart, rak.yStart, rak.height); final p10H = transformer.toScreen(rak.xEnd, rak.yStart, rak.height); final p01H = transformer.toScreen(rak.xStart, rak.yEnd, rak.height); final p11H = transformer.toScreen(rak.xEnd, rak.yEnd, rak.height); // 1. Gambar sisi kanan (X-Right Face) final rightFace = Path() ..moveTo(p10.dx, p10.dy) ..lineTo(p11.dx, p11.dy) ..lineTo(p11H.dx, p11H.dy) ..lineTo(p10H.dx, p10H.dy) ..close(); canvas.drawPath(rightFace, fillPaint); canvas.drawPath(rightFace, borderPaint); // 2. Gambar sisi kiri (Y-Left Face) final leftFace = Path() ..moveTo(p01.dx, p01.dy) ..lineTo(p11.dx, p11.dy) ..lineTo(p11H.dx, p11H.dy) ..lineTo(p01H.dx, p01H.dy) ..close(); canvas.drawPath(leftFace, fillPaint); canvas.drawPath(leftFace, borderPaint); // 3. Gambar sisi atas (Top Face) final topFace = Path() ..moveTo(p00H.dx, p00H.dy) ..lineTo(p10H.dx, p10H.dy) ..lineTo(p11H.dx, p11H.dy) ..lineTo(p01H.dx, p01H.dy) ..close(); canvas.drawPath(topFace, fillPaint..color = Colors.teal.withValues(alpha: 0.18)); canvas.drawPath(topFace, borderPaint); } @override bool shouldRepaint(covariant StaticKumbungPainter oldDelegate) { return oldDelegate.roomWidth != roomWidth || oldDelegate.roomLength != roomLength || oldDelegate.roomHeight != roomHeight || oldDelegate.transformer.scale != transformer.scale || oldDelegate.transformer.origin != transformer.origin; } } class _Rak3D { final double xStart; final double xEnd; final double yStart; final double yEnd; final double height; _Rak3D({required this.xStart, required this.xEnd, required this.yStart, required this.yEnd, required this.height}); } // ============================================================================= // LAYER 2: HEATMAP PAINTER // ============================================================================= /// Menggambar lingkaran gradien heatmap isometrik (elips) di sekitar posisi sensor di atas lantai. class HeatmapKumbungPainter extends CustomPainter { final IsometricTransform transformer; final Offset sensorPos; // Koordinat X dan Y sensor (meter) final double radius; // Radius cakupan dalam meter final double value; // Nilai aktual sensor (suhu atau kelembapan) final bool isSuhuMode; // true untuk Suhu, false untuk Kelembapan const HeatmapKumbungPainter({ required this.transformer, required this.sensorPos, required this.radius, required this.value, required this.isSuhuMode, }); @override void paint(Canvas canvas, Size size) { if (radius <= 0) return; // Titik pusat lingkaran di ground (Z = 0) dalam koordinat piksel final center = transformer.toScreen(sensorPos.dx, sensorPos.dy, 0); // Radius dalam proyeksi isometrik (elips) // Sumbu horizontal (Major Axis): radius * scale // Sumbu vertikal (Minor Axis): radius * scale * sin(30) -> sin(30) = 0.5 final double radiusX = radius * transformer.scale * math.cos(30.0 * math.pi / 180.0); final double radiusY = radius * transformer.scale * math.sin(30.0 * math.pi / 180.0); // Tentukan warna dasar heatmap (tanpa opacity bawaan agar pencampuran warna gradien lebih terkontrol) Color baseColor; if (isSuhuMode) { // Peta Suhu: Dingin (< 20°C) = Electric Blue, Optimal (20 - 33°C) = Neon Green, Panas (> 33°C) = Neon Orange ke Red if (value < 20) { baseColor = const Color(0xFF2979FF); // Electric Blue } else if (value <= 33) { baseColor = const Color(0xFF00E676); // Neon Green } else { // Semakin panas semakin merah menyala final factor = ((value - 33) / 10).clamp(0.0, 1.0); baseColor = Color.lerp(const Color(0xFFFFA000), const Color(0xFFFF1744), factor)!; } } else { // Peta Kelembapan: Kering (< 60%) = Kuning/Oranye, Cukup (60 - 80%) = Neon Green, Basah/Optimal (> 80%) = Electric Cyan if (value < 60) { baseColor = const Color(0xFFFF9100); // Vibrant Orange } else if (value < 80) { baseColor = const Color(0xFF00E676); // Neon Green } else { baseColor = const Color(0xFF00E5FF); // Electric Cyan } } final paint = Paint() ..shader = RadialGradient( colors: [ baseColor.withValues(alpha: 0.70), // Center: strong glow baseColor.withValues(alpha: 0.45), // Mid: visible effect area baseColor.withValues(alpha: 0.15), // Edge: soft fade Colors.transparent, ], stops: const [0.0, 0.5, 0.85, 1.0], ).createShader(Rect.fromLTRB( center.dx - radiusX, center.dy - radiusY, center.dx + radiusX, center.dy + radiusY, )); // Gambar elips heatmap isometrik canvas.drawOval( Rect.fromCenter(center: center, width: radiusX * 2, height: radiusY * 2), paint, ); } @override bool shouldRepaint(covariant HeatmapKumbungPainter oldDelegate) { return oldDelegate.sensorPos != sensorPos || oldDelegate.radius != radius || oldDelegate.value != value || oldDelegate.isSuhuMode != isSuhuMode || oldDelegate.transformer.scale != transformer.scale || oldDelegate.transformer.origin != transformer.origin; } } // ============================================================================= // LAYER 3: DYNAMIC SENSOR PIN & TAG PAINTER // ============================================================================= /// Menggambar penanda pin sensor melayang (Z = height), garis bantu vertikal ke ground, /// lingkaran pulsing glow, serta tag nilai real-time di atas pin. class PinKumbungPainter extends CustomPainter { final IsometricTransform transformer; final Offset sensorPos; // X, Y koordinat sensor (meter) final double sensorHeight; // Z (tinggi sensor) melayang dalam meter final double value; // Nilai sensor yang ditampilkan final String unit; // Satuan nilai (contoh: "°C" atau "%") final double animationValue; // Nilai animasi 0.0 s.d 1.0 untuk denyut pulsing final Color pinColor; // Warna pin (menyesuaikan status) const PinKumbungPainter({ required this.transformer, required this.sensorPos, required this.sensorHeight, required this.value, required this.unit, required this.animationValue, required this.pinColor, }); @override void paint(Canvas canvas, Size size) { // 1. Koordinat Piksel Ground (Z = 0) dan Koordinat Floating (Z = sensorHeight) final pGround = transformer.toScreen(sensorPos.dx, sensorPos.dy, 0); final pFloat = transformer.toScreen(sensorPos.dx, sensorPos.dy, sensorHeight); // 2. Menggambar Garis Bantu Vertikal (Dotted/Dashed Line) final linePaint = Paint() ..color = pinColor.withValues(alpha: 0.5) ..strokeWidth = 1.2 ..style = PaintingStyle.stroke; _drawDashedLine(canvas, pGround, pFloat, linePaint); // 3. Menggambar Lingkaran Pulsing Glow di Lantai (Z = 0) final double pulseMaxRadius = 24.0; final double pulseRadius = pulseMaxRadius * animationValue; final double pulseOpacity = (1.0 - animationValue).clamp(0.0, 1.0); final pulsePaint = Paint() ..color = pinColor.withValues(alpha: pulseOpacity * 0.4) ..style = PaintingStyle.fill; // Proyeksi isometrik elips untuk pulsing final double prX = pulseRadius; final double prY = pulseRadius * math.sin(30.0 * math.pi / 180.0); canvas.drawOval( Rect.fromCenter(center: pGround, width: prX * 2, height: prY * 2), pulsePaint, ); // 4. Menggambar Pin Sensor Melayang (Floating Pin) // Efek mengambang naik-turun menggunakan nilai animasi final double floatOffset = 6.0 * math.sin(animationValue * 2 * math.pi); final pFloatingPin = Offset(pFloat.dx, pFloat.dy + floatOffset); // Lingkaran pin inti final corePaint = Paint() ..color = pinColor ..style = PaintingStyle.fill; final borderPaint = Paint() ..color = Colors.white ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; canvas.drawCircle(pFloatingPin, 8.0, corePaint); canvas.drawCircle(pFloatingPin, 8.0, borderPaint); // 5. Menggambar Tag Pembacaan Nilai (Tooltip Tag) di atas Pin final tagText = '${value.toStringAsFixed(1)}$unit'; _drawTooltipTag(canvas, pFloatingPin - const Offset(0, 14), tagText); } /// Helper untuk menggambar dashed line void _drawDashedLine(Canvas canvas, Offset p1, Offset p2, Paint paint) { const double dashHeight = 4.0; const double dashGap = 3.0; double startY = p1.dy; double endY = p2.dy; // Arah vertical naik (dy mengecil) if (startY < endY) { final temp = startY; startY = endY; endY = temp; } double currentY = startY; while (currentY > endY) { final double nextY = math.max(currentY - dashHeight, endY); canvas.drawLine(Offset(p1.dx, currentY), Offset(p1.dx, nextY), paint); currentY = nextY - dashGap; } } /// Helper untuk menggambar text tag box (tooltip) di atas pin sensor void _drawTooltipTag(Canvas canvas, Offset position, String text) { final textPainter = TextPainter( text: TextSpan( text: text, style: const TextStyle( color: Colors.white, fontSize: 10.0, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, )..layout(); final double paddingX = 8.0; final double paddingY = 4.0; final double width = textPainter.width + paddingX * 2; final double height = textPainter.height + paddingY * 2; final rect = Rect.fromCenter(center: position, width: width, height: height); final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(6.0)); // Paint background tag box (semacam pill box gelap) final bgPaint = Paint() ..color = Colors.black.withValues(alpha: 0.7) ..style = PaintingStyle.fill; canvas.drawRRect(rrect, bgPaint); // Gambar teks di tengah box textPainter.paint( canvas, Offset(position.dx - textPainter.width / 2, position.dy - textPainter.height / 2), ); } @override bool shouldRepaint(covariant PinKumbungPainter oldDelegate) { return oldDelegate.sensorPos != sensorPos || oldDelegate.sensorHeight != sensorHeight || oldDelegate.value != value || oldDelegate.unit != unit || oldDelegate.animationValue != animationValue || oldDelegate.pinColor != pinColor || oldDelegate.transformer.scale != transformer.scale || oldDelegate.transformer.origin != transformer.origin; } }