TKK_E32230206/lib/features/iot/widgets/isometric_kumbung_painter.dart

408 lines
14 KiB
Dart

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.withOpacity(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.withOpacity(0.12)
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = Colors.teal.withOpacity(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.withOpacity(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 heatmap berdasarkan nilai aktual sensor
Color centerColor;
if (isSuhuMode) {
// Peta Suhu: Dingin (< 22°C) = Biru, Optimal (22 - 28°C) = Hijau, Panas (> 28°C) = Merah/Oranye
if (value < 22) {
centerColor = Colors.blue.withOpacity(0.5);
} else if (value <= 28) {
centerColor = Colors.green.withOpacity(0.45);
} else {
// Semakin panas semakin merah menyala
final factor = ((value - 28) / 10).clamp(0.0, 1.0);
centerColor = Color.lerp(Colors.orange, Colors.redAccent, factor)!.withOpacity(0.55);
}
} else {
// Peta Kelembapan: Kering (< 60%) = Kuning/Oranye, Cukup (60 - 80%) = Hijau, Basah/Optimal (> 80%) = Biru/Sian
if (value < 60) {
centerColor = Colors.orange.withOpacity(0.5);
} else if (value < 80) {
centerColor = Colors.green.withOpacity(0.45);
} else {
centerColor = Colors.cyan.withOpacity(0.5);
}
}
final paint = Paint()
..shader = RadialGradient(
colors: [
centerColor,
centerColor.withOpacity(0.15),
Colors.transparent,
],
stops: const [0.0, 0.6, 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.withOpacity(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.withOpacity(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.withOpacity(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;
}
}