409 lines
15 KiB
Dart
409 lines
15 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.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;
|
|
}
|
|
}
|