{/* Main content */}
);
};
export default GaugeDesigner;
/>
setFontArcRadius(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
{/* Sidebar */}
{activeTab === 'gauge' && (
)}
{activeTab === 'icons' && (
{/* Canvas area */}
setCanvasSize({...canvasSize, width: parseInt(e.target.value)})}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
setCanvasSize({...canvasSize, height: parseInt(e.target.value)})}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
setCenterX(parseInt(e.target.value))}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
setCenterY(parseInt(e.target.value))}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
setStartAngle(parseInt(e.target.value))}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
setEndAngle(parseInt(e.target.value))}
className="w-1/2 bg-gray-700 rounded px-2 py-1"
/>
Radius
setRadius(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{radius}
Arc Width
setArcWidth(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{arcWidth}
Major Count
setMajorTicks(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{majorTicks}
Major Length
setTickLength(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{tickLength}
Major Width
setTickWidth(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{tickWidth}
Minor Count
setMinorTicks(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{minorTicks}
Minor Length
setMinorTickLength(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{minorTickLength}
Minor Width
setMinorTickWidth(parseInt(e.target.value))}
className="w-32 bg-gray-700"
/>
{minorTickWidth}
Min Value
setMinValue(parseInt(e.target.value))}
className="w-20 bg-gray-700 rounded px-2 py-1"
/>
Max Value
setMaxValue(parseInt(e.target.value))}
className="w-20 bg-gray-700 rounded px-2 py-1"
/>
setShowRedZone(e.target.checked)}
className="bg-gray-700"
/>
Show Red Zone
Start at
setRedZoneStart(parseInt(e.target.value))}
className="w-20 bg-gray-700 rounded px-2 py-1"
disabled={!showRedZone}
/>
Color
setRedZoneColor(e.target.value)}
className="w-20 bg-gray-700 rounded"
disabled={!showRedZone}
/>
setShowNumbers(e.target.checked)}
className="bg-gray-700"
/>
Show Numbers
Font Size
setNumberFontSize(parseInt(e.target.value))}
className="w-32 bg-gray-700"
disabled={!showNumbers}
/>
{numberFontSize}
Prefix
setNumberPrefix(e.target.value)}
className="w-20 bg-gray-700 rounded px-2 py-1"
disabled={!showNumbers}
/>
Suffix
setNumberSuffix(e.target.value)}
className="w-20 bg-gray-700 rounded px-2 py-1"
disabled={!showNumbers}
/>
setShowTitle(e.target.checked)}
className="bg-gray-700"
/>
Show Title
Text
setGaugeTitle(e.target.value)}
className="w-32 bg-gray-700 rounded px-2 py-1"
disabled={!showTitle}
/>
Font Size
setTitleFontSize(parseInt(e.target.value))}
className="w-32 bg-gray-700"
disabled={!showTitle}
/>
{titleFontSize}
Background
setBgColor(e.target.value)}
className="w-16 bg-gray-700 rounded"
/>
Gauge Color
setGaugeColor(e.target.value)}
className="w-16 bg-gray-700 rounded"
/>
Arc Color
setArcColor(e.target.value)}
className="w-16 bg-gray-700 rounded"
/>
setShowGuideLines(e.target.checked)}
className="bg-gray-700"
/>
Show Guide Lines
{icons.map((icon, index) => (
))}
)}
{
const newIcons = [...icons];
newIcons[index].name = e.target.value;
setIcons(newIcons);
}}
className="bg-gray-600 rounded px-2 py-1 text-sm w-32"
/>
Position
{
const newIcons = [...icons];
newIcons[index].x = parseInt(e.target.value);
setIcons(newIcons);
}}
className="w-16 bg-gray-600 rounded px-2 py-1 text-xs"
/>
{
const newIcons = [...icons];
newIcons[index].y = parseInt(e.target.value);
setIcons(newIcons);
}}
className="w-16 bg-gray-600 rounded px-2 py-1 text-xs"
/>
Size
handleIconSizeChange(index, e.target.value)}
className="w-32 bg-gray-600"
/>
{icon.size}
Color
handleIconColorChange(index, e.target.value)}
className="w-16 bg-gray-600 rounded"
/>
{!fontEditorOpen ? (
) : (
>
)}
)}
{/* Validation Results Modal */}
{validationResults && (
{validationResults.success ?
<>
)}
{/* Design Principles Modal */}
{showPrinciples && (
)}
{/* Loading Indicator for Compilation */}
{isCompiling && (
)}
Font Editor
setFontName(e.target.value)}
className="w-full bg-gray-700 rounded px-2 py-1"
/>
setFontSize(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontSize(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
setFontCharacters(e.target.value)}
className="w-full bg-gray-700 rounded px-2 py-1"
/>
setFontSpacing(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontSpacing(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
setFontArcEnabled(e.target.checked)}
className="bg-gray-700"
/>
Arc Text
{fontArcEnabled && (
<>
setFontArcRadius(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontArcEnd(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
setFontBaselineShift(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontBaselineShift(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
setFontLineHeight(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontLineHeight(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
{validationResults.success ?
<> Validation Successful> :
<> Validation Issues Found>}
{validationResults.issues && validationResults.issues.map((issue, i) => (
{issue.severity === 'error' ?
)}
))}
{validationResults.success && (
)}
{issue.severity === 'error' ?
:
}
{issue.title}
{issue.description}
{issue.suggestedFix && (Suggested Fix:
{issue.suggestedFix}
{issue.canAutoFix && (
)}
{validationResults.message || "All files validated successfully! Your designs are ready to export."}
{validationResults.success ? (
) : (
)}
Automotive Gauge Design Principles
Prioritize Information Based on Driving Needs
- Categorize information into primary, secondary, and tertiary levels of importance.
- Place critical information in the driver's "golden zones" or high-priority viewing areas.
- Consider how frequently the driver needs to access specific information (e.g., fuel level is important but doesn't require constant monitoring).
Consider Human Factors and Driver Interaction
- Account for potential obstructions like the driver's hands on the steering wheel.
- Design for the driver's natural line of sight, placing frequently used information closer to the windshield.
- Recognize that perceived importance of information varies based on individual driving habits.
Apply Gestalt Principles for Intuitive Organization
- Group related elements together visually (common region).
- Consider the natural flow of the driver's eye across the display (continuity).
- Place related information closer together and separate unrelated elements (proximity).
- Use a consistent visual style for icons and elements (similarity).
- Create visual balance across different display areas (symmetry).
- Ensure related gauges behave consistently (common fate).
Choose Appropriate Display Methods
- Question historical gauge designs and consider more effective digital alternatives.
- Use icons for warnings and indicators as they are succinct and recognizable.
- Consider dynamic displays that change based on context (e.g., changing colors for critical values).
- Group warnings together for clarity.
Consider Hardware Limitations
- Be mindful of processing power and memory limitations when designing complex animations.
- Too much information or too many moving elements can lead to lag on low-power displays.
Iterative Design and Testing
- Plan for an iterative process of building, testing, and refining based on real-world use.
- Use the dashboard in the car to identify annoyances and areas for improvement.
Compiling and validating your gauge design...
setFontArcStart(parseInt(e.target.value))}
className="flex-1 bg-gray-700"
/>
setFontArcStart(parseInt(e.target.value))}
className="w-16 bg-gray-700 rounded px-2 py-1"
/>
setFontArcEnd(parseInt(e.target.value))}
className="flex-1 bg-gray-700"import React, { useState, useRef, useEffect } from 'react';
import { Sliders, Download, Info, Settings, Layers, Move, RotateCcw, EyeOff, Eye, PenTool, RefreshCw,
Check, X, Type, BellRing, CheckCircle, AlertTriangle, XCircle, AlertCircle } from 'lucide-react';
const GaugeDesigner = () => {
const [activeTab, setActiveTab] = useState('gauge');
const [showGuideLines, setShowGuideLines] = useState(true);
const [showPrinciples, setShowPrinciples] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 400, height: 400 });
const [gaugeType, setGaugeType] = useState('circular');
const [fontEditorOpen, setFontEditorOpen] = useState(false);
const [startAngle, setStartAngle] = useState(135);
const [endAngle, setEndAngle] = useState(405);
const [radius, setRadius] = useState(150);
const [tickLength, setTickLength] = useState(15);
const [tickWidth, setTickWidth] = useState(3);
const [minorTickLength, setMinorTickLength] = useState(7);
const [minorTickWidth, setMinorTickWidth] = useState(1);
const [arcWidth, setArcWidth] = useState(10);
const [gaugeColor, setGaugeColor] = useState('#ffffff');
const [arcColor, setArcColor] = useState('#00ff00');
const [bgColor, setBgColor] = useState('#000000');
const [majorTicks, setMajorTicks] = useState(11);
const [minorTicks, setMinorTicks] = useState(4);
const [showNumbers, setShowNumbers] = useState(true);
const [numberFontSize, setNumberFontSize] = useState(16);
const [numberPrefix, setNumberPrefix] = useState('');
const [numberSuffix, setNumberSuffix] = useState('');
const [minValue, setMinValue] = useState(0);
const [maxValue, setMaxValue] = useState(100);
const [redZoneStart, setRedZoneStart] = useState(80);
const [showRedZone, setShowRedZone] = useState(true);
const [redZoneColor, setRedZoneColor] = useState('#ff0000');
const [showTitle, setShowTitle] = useState(true);
const [gaugeTitle, setGaugeTitle] = useState('SPEED');
const [titleFontSize, setTitleFontSize] = useState(18);
const [centerX, setCenterX] = useState(canvasSize.width / 2);
const [centerY, setCenterY] = useState(canvasSize.height / 2);
const [icons, setIcons] = useState([
{ id: 1, name: 'Check Engine', x: 50, y: 350, visible: true, color: '#ff0000', size: 20 },
{ id: 2, name: 'Oil Pressure', x: 100, y: 350, visible: true, color: '#ff0000', size: 20 },
{ id: 3, name: 'Battery', x: 150, y: 350, visible: true, color: '#ff0000', size: 20 }
]);
// Font editor states
const [fontName, setFontName] = useState('Gauge_Font');
const [fontSize, setFontSize] = useState(200);
const [fontBpp, setFontBpp] = useState(4);
const [fontCharacters, setFontCharacters] = useState('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.,-°%');
const [fontWeight, setFontWeight] = useState('normal');
const [fontStyle, setFontStyle] = useState('normal');
const [fontSpacing, setFontSpacing] = useState(0);
const [fontArcEnabled, setFontArcEnabled] = useState(false);
const [fontArcRadius, setFontArcRadius] = useState(300);
const [fontArcStart, setFontArcStart] = useState(180);
const [fontArcEnd, setFontArcEnd] = useState(360);
const [fontBaselineShift, setFontBaselineShift] = useState(40);
const [fontLineHeight, setFontLineHeight] = useState(400);
const [availableFontFamilies, setAvailableFontFamilies] = useState([
'Arial', 'Verdana', 'Helvetica', 'Times New Roman', 'Courier New',
'Georgia', 'Trebuchet MS', 'Impact', 'Segoe UI', 'Roboto'
]);
const [fontFamily, setFontFamily] = useState('Arial');
// Validation and compilation states
const [validationResults, setValidationResults] = useState(null);
const [isCompiling, setIsCompiling] = useState(false);
const canvasRef = useRef(null);
const fontCanvasRef = useRef(null);
const dragRef = useRef({ dragging: false, startX: 0, startY: 0, index: -1 });
useEffect(() => {
drawGauge();
}, [
canvasSize, gaugeType, startAngle, endAngle, radius, tickLength,
tickWidth, minorTickLength, minorTickWidth, arcWidth, gaugeColor,
arcColor, bgColor, majorTicks, minorTicks, showNumbers, numberFontSize,
minValue, maxValue, redZoneStart, showRedZone, redZoneColor,
showTitle, gaugeTitle, titleFontSize, centerX, centerY, icons,
showNumbers, numberPrefix, numberSuffix
]);
useEffect(() => {
if (fontEditorOpen && fontCanvasRef.current) {
drawFontPreview();
}
}, [
fontEditorOpen, fontFamily, fontSize, fontWeight, fontStyle,
fontSpacing, fontCharacters, fontArcEnabled, fontArcRadius,
fontArcStart, fontArcEnd
]);
const handleIconDragStart = (e, index) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
dragRef.current = {
dragging: true,
startX: x,
startY: y,
index: index
};
};
const handleCanvasMouseMove = (e) => {
if (dragRef.current.dragging && dragRef.current.index >= 0) {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newIcons = [...icons];
newIcons[dragRef.current.index].x = x;
newIcons[dragRef.current.index].y = y;
setIcons(newIcons);
}
};
const handleCanvasMouseUp = () => {
dragRef.current.dragging = false;
};
const handleIconVisibilityToggle = (index) => {
const newIcons = [...icons];
newIcons[index].visible = !newIcons[index].visible;
setIcons(newIcons);
};
const handleIconColorChange = (index, color) => {
const newIcons = [...icons];
newIcons[index].color = color;
setIcons(newIcons);
};
const handleIconSizeChange = (index, size) => {
const newIcons = [...icons];
newIcons[index].size = parseInt(size);
setIcons(newIcons);
};
const addNewIcon = () => {
const newIcon = {
id: icons.length + 1,
name: `Icon ${icons.length + 1}`,
x: centerX,
y: centerY + radius + 50,
visible: true,
color: '#ffff00',
size: 20
};
setIcons([...icons, newIcon]);
};
const deleteIcon = (index) => {
const newIcons = [...icons];
newIcons.splice(index, 1);
setIcons(newIcons);
};
const drawGauge = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Background
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw guide lines if enabled
if (showGuideLines) {
drawGuideLines(ctx);
}
if (gaugeType === 'circular') {
// Draw gauge arc
drawGaugeArc(ctx);
// Draw ticks
drawTicks(ctx);
// Draw red zone
if (showRedZone) {
drawRedZone(ctx);
}
// Draw numbers
if (showNumbers) {
drawNumbers(ctx);
}
// Draw title
if (showTitle) {
drawTitle(ctx);
}
} else if (gaugeType === 'linear') {
// Draw linear gauge
drawLinearGauge(ctx);
}
// Draw indicators and icons
drawIcons(ctx);
};
const drawGuideLines = (ctx) => {
ctx.strokeStyle = '#333333';
ctx.lineWidth = 1;
// Draw center lines
ctx.beginPath();
ctx.moveTo(0, centerY);
ctx.lineTo(canvasSize.width, centerY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, canvasSize.height);
ctx.stroke();
// Draw circles
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX, centerY, radius - arcWidth, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 10, 0, Math.PI * 2);
ctx.stroke();
};
const drawGaugeArc = (ctx) => {
const startRad = (startAngle) * Math.PI / 180;
const endRad = (endAngle) * Math.PI / 180;
// Main arc
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startRad, endRad);
ctx.lineWidth = arcWidth;
ctx.strokeStyle = arcColor;
ctx.stroke();
};
const drawRedZone = (ctx) => {
const startRad = (startAngle) * Math.PI / 180;
const endRad = (endAngle) * Math.PI / 180;
const range = maxValue - minValue;
const redZoneStartAngle = startAngle + ((redZoneStart - minValue) / range) * (endAngle - startAngle);
ctx.beginPath();
ctx.arc(centerX, centerY, radius, redZoneStartAngle * Math.PI / 180, endRad);
ctx.lineWidth = arcWidth;
ctx.strokeStyle = redZoneColor;
ctx.stroke();
};
const drawTicks = (ctx) => {
const startRad = (startAngle) * Math.PI / 180;
const endRad = (endAngle) * Math.PI / 180;
const angleRange = endRad - startRad;
// Major ticks
ctx.lineWidth = tickWidth;
ctx.strokeStyle = gaugeColor;
for (let i = 0; i < majorTicks; i++) {
const angle = startRad + (angleRange / (majorTicks - 1)) * i;
const innerRadius = radius - arcWidth / 2;
const outerRadius = innerRadius + tickLength;
ctx.beginPath();
ctx.moveTo(
centerX + innerRadius * Math.cos(angle),
centerY + innerRadius * Math.sin(angle)
);
ctx.lineTo(
centerX + outerRadius * Math.cos(angle),
centerY + outerRadius * Math.sin(angle)
);
ctx.stroke();
// Minor ticks
if (i < majorTicks - 1) {
ctx.lineWidth = minorTickWidth;
for (let j = 1; j <= minorTicks; j++) {
const minorAngle = angle + (angleRange / (majorTicks - 1)) * (j / (minorTicks + 1));
const minorInnerRadius = radius - arcWidth / 2;
const minorOuterRadius = minorInnerRadius + minorTickLength;
ctx.beginPath();
ctx.moveTo(
centerX + minorInnerRadius * Math.cos(minorAngle),
centerY + minorInnerRadius * Math.sin(minorAngle)
);
ctx.lineTo(
centerX + minorOuterRadius * Math.cos(minorAngle),
centerY + minorOuterRadius * Math.sin(minorAngle)
);
ctx.stroke();
}
}
}
};
const drawNumbers = (ctx) => {
const startRad = (startAngle) * Math.PI / 180;
const endRad = (endAngle) * Math.PI / 180;
const angleRange = endRad - startRad;
const valueRange = maxValue - minValue;
ctx.fillStyle = gaugeColor;
ctx.font = `${numberFontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < majorTicks; i++) {
const angle = startRad + (angleRange / (majorTicks - 1)) * i;
const value = minValue + (valueRange / (majorTicks - 1)) * i;
const textRadius = radius + tickLength + 15;
const x = centerX + textRadius * Math.cos(angle);
const y = centerY + textRadius * Math.sin(angle);
ctx.fillText(`${numberPrefix}${Math.round(value)}${numberSuffix}`, x, y);
}
};
const drawTitle = (ctx) => {
ctx.fillStyle = gaugeColor;
ctx.font = `bold ${titleFontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(gaugeTitle, centerX, centerY + 50);
};
const drawLinearGauge = (ctx) => {
// Draw linear gauge base
ctx.lineWidth = arcWidth;
ctx.strokeStyle = arcColor;
const startX = centerX - radius;
const endX = centerX + radius;
ctx.beginPath();
ctx.moveTo(startX, centerY);
ctx.lineTo(endX, centerY);
ctx.stroke();
// Draw ticks
ctx.lineWidth = tickWidth;
ctx.strokeStyle = gaugeColor;
for (let i = 0; i < majorTicks; i++) {
const x = startX + (endX - startX) * (i / (majorTicks - 1));
ctx.beginPath();
ctx.moveTo(x, centerY - tickLength / 2);
ctx.lineTo(x, centerY + tickLength / 2);
ctx.stroke();
// Minor ticks
if (i < majorTicks - 1) {
ctx.lineWidth = minorTickWidth;
for (let j = 1; j <= minorTicks; j++) {
const minorX = x + (endX - startX) * (j / (minorTicks + 1)) / (majorTicks - 1);
ctx.beginPath();
ctx.moveTo(minorX, centerY - minorTickLength / 2);
ctx.lineTo(minorX, centerY + minorTickLength / 2);
ctx.stroke();
}
}
}
// Draw red zone
if (showRedZone) {
const redZoneStartX = startX + (endX - startX) * ((redZoneStart - minValue) / (maxValue - minValue));
ctx.lineWidth = arcWidth;
ctx.strokeStyle = redZoneColor;
ctx.beginPath();
ctx.moveTo(redZoneStartX, centerY);
ctx.lineTo(endX, centerY);
ctx.stroke();
}
// Draw numbers
if (showNumbers) {
ctx.fillStyle = gaugeColor;
ctx.font = `${numberFontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < majorTicks; i++) {
const x = startX + (endX - startX) * (i / (majorTicks - 1));
const value = minValue + (maxValue - minValue) * (i / (majorTicks - 1));
ctx.fillText(`${numberPrefix}${Math.round(value)}${numberSuffix}`, x, centerY + tickLength + 15);
}
}
// Draw title
if (showTitle) {
ctx.fillStyle = gaugeColor;
ctx.font = `bold ${titleFontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(gaugeTitle, centerX, centerY - tickLength - 20);
}
};
const drawIcons = (ctx) => {
icons.forEach(icon => {
if (icon.visible) {
ctx.fillStyle = icon.color;
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
// Draw a simple icon shape (can be enhanced with actual icon images)
ctx.beginPath();
ctx.arc(icon.x, icon.y, icon.size / 2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Draw icon name
ctx.fillStyle = gaugeColor;
ctx.font = `${Math.max(10, icon.size/2)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(icon.name, icon.x, icon.y + icon.size);
}
});
};
// Validation & Compilation
const validateAndCompileCode = () => {
setIsCompiling(true);
// First, run validation
const validationResults = validateDesigns();
// If validation passes, perform compilation check
if (validationResults.success) {
checkCompilation().then(compilationResults => {
if (compilationResults.success) {
setValidationResults({
success: true,
issues: [],
message: "Validation and compilation successful! Your gauge design is ready to export."
});
} else {
setValidationResults({
success: false,
issues: compilationResults.issues
});
}
setIsCompiling(false);
}).catch(error => {
setValidationResults({
success: false,
issues: [{
severity: 'error',
title: 'Compilation check failed',
description: 'An unexpected error occurred during compilation checking.',
canAutoFix: false
}]
});
setIsCompiling(false);
});
} else {
setValidationResults(validationResults);
setIsCompiling(false);
}
};
const validateDesigns = () => {
const issues = [];
// Check naming conventions
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(gaugeTitle)) {
issues.push({
severity: 'error',
title: 'Invalid gauge name',
description: 'Gauge name must start with a letter and contain only letters, numbers, and underscores.',
suggestedFix: `Change "${gaugeTitle}" to "${gaugeTitle.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[^a-zA-Z]/, 'g')}".`,
canAutoFix: true,
fixFunction: () => {
const newName = gaugeTitle.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[^a-zA-Z]/, 'g');
setGaugeTitle(newName);
}
});
}
// Font name check
if (fontEditorOpen && !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(fontName)) {
issues.push({
severity: 'error',
title: 'Invalid font name',
description: 'Font name must start with a letter and contain only letters, numbers, and underscores.',
suggestedFix: `Change "${fontName}" to "${fontName.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[^a-zA-Z]/, 'f')}".`,
canAutoFix: true,
fixFunction: () => {
const newName = fontName.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[^a-zA-Z]/, 'f');
setFontName(newName);
}
});
}
// Check memory constraints
const totalBytes = calculateTotalBytes();
if (totalBytes > 50000) { // Example threshold
issues.push({
severity: 'warning',
title: 'Large memory footprint',
description: `Gauge design uses approximately ${Math.round(totalBytes/1024)} KB of memory, which might be too large for some microcontrollers.`,
suggestedFix: 'Consider reducing image size, using fewer colors, or simplifying the design.',
canAutoFix: false
});
}
// Check gauge range
if (maxValue <= minValue) {
issues.push({
severity: 'error',
title: 'Invalid value range',
description: 'Maximum value must be greater than minimum value.',
suggestedFix: `Set maximum value higher than ${minValue}.`,
canAutoFix: true,
fixFunction: () => {
setMaxValue(minValue + 100);
}
});
}
// Check red zone
if (showRedZone && (redZoneStart <= minValue || redZoneStart >= maxValue)) {
issues.push({
severity: 'error',
title: 'Invalid red zone',
description: 'Red zone start value must be between minimum and maximum values.',
suggestedFix: `Set red zone start to a value between ${minValue} and ${maxValue}.`,
canAutoFix: true,
fixFunction: () => {
setRedZoneStart(minValue + (maxValue - minValue) * 0.8);
}
});
}
// Check angle range
if (endAngle <= startAngle) {
issues.push({
severity: 'error',
title: 'Invalid angle range',
description: 'End angle must be greater than start angle.',
suggestedFix: 'Adjust the angle range to ensure end angle is greater than start angle.',
canAutoFix: true,
fixFunction: () => {
setEndAngle(startAngle + 270);
}
});
}
// Check center position
if (centerX < 0 || centerX > canvasSize.width || centerY < 0 || centerY > canvasSize.height) {
issues.push({
severity: 'warning',
title: 'Center position out of bounds',
description: 'The gauge center position is outside the canvas area.',
suggestedFix: 'Adjust the center position to be within the canvas.',
canAutoFix: true,
fixFunction: () => {
setCenterX(canvasSize.width / 2);
setCenterY(canvasSize.height / 2);
}
});
}
// Return results
return {
success: issues.length === 0,
issues: issues
};
};
const calculateTotalBytes = () => {
// Calculate approximate memory usage of the gauge
const imageBytes = canvasSize.width * canvasSize.height * 4; // 4 bytes per pixel (RGBA)
const configBytes = 500; // Approximate size of configuration data
// Add font memory if using custom font
const fontBytes = fontEditorOpen ? (fontCharacters.length * fontSize * fontSize * fontBpp / 8) : 0;
return imageBytes + configBytes + fontBytes;
};
const checkCompilation = async () => {
try {
// Generate code from current designs
const generatedCode = {
gaugeSource: exportGaugeImageToLVGL(false),
gaugeHeader: generateGaugeHeader(false),
exampleImplementation: generateExampleImplementation(false)
};
// In a real implementation, this would use WebAssembly to compile the code
// For this example, we'll simulate compilation with some basic checks
// Simulate compilation issues or success
const hasCompilationIssues = false; // Set to true to simulate errors
if (hasCompilationIssues) {
return {
success: false,
issues: [{
severity: 'error',
title: 'Compilation error',
description: 'Could not compile the gauge code. LVGL compatibility issue detected.',
suggestedFix: 'Check that your gauge parameters are within valid ranges for LVGL.',
canAutoFix: false
}]
};
} else {
return {
success: true,
message: "Compilation successful! Your gauge design will work with LVGL."
};
}
} catch (error) {
console.error("Compilation check failed:", error);
return {
success: false,
issues: [{
severity: 'error',
title: 'Compilation check failed',
description: 'An unexpected error occurred during compilation checking.',
canAutoFix: false
}]
};
}
};
const applyFix = (issue) => {
if (issue.canAutoFix && issue.fixFunction) {
issue.fixFunction();
// Re-run validation after applying the fix
const newValidationResults = validateDesigns();
setValidationResults(newValidationResults);
}
};
const exportAllFiles = () => {
// Create a temporary ZIP container (simulated here)
const files = [];
// 1. Export the gauge background image
const gaugeImageCode = exportGaugeImageToLVGL(true);
files.push({
name: `${gaugeTitle.toLowerCase()}.c`,
content: gaugeImageCode
});
// 2. Export the gauge header file
const gaugeHeaderCode = generateGaugeHeader(true);
files.push({
name: `${gaugeTitle.toLowerCase()}.h`,
content: gaugeHeaderCode
});
// 3. Export the font file if needed
if (fontEditorOpen) {
const fontCode = generateFontCode();
files.push({
name: `${fontName.toLowerCase()}.c`,
content: fontCode
});
// Font header file
const fontHeaderCode = generateFontHeader();
files.push({
name: `${fontName.toLowerCase()}.h`,
content: fontHeaderCode
});
}
// 4. Export example implementation file
const exampleCode = generateExampleImplementation(true);
files.push({
name: 'gauge_example.cpp',
content: exampleCode
});
// Create and download the files individually
files.forEach(file => {
const blob = new Blob([file.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
};
const exportGaugeImageToLVGL = (download = true) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Create binary data array suitable for LVGL
let binaryData = [];
let byteCounter = 0;
let currentByte = 0;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
// Simplistic binary conversion - you may need to adjust this based on your LVGL requirements
const isPixelOn = (r + g + b > 380 || a < 127) ? 0 : 1;
// Pack 8 pixels into one byte
currentByte = (currentByte << 1) | isPixelOn;
byteCounter++;
if (byteCounter === 8) {
binaryData.push(currentByte);
byteCounter = 0;
currentByte = 0;
}
}
// Add the final byte if needed
if (byteCounter > 0) {
currentByte = currentByte << (8 - byteCounter);
binaryData.push(currentByte);
}
// Format binary data for LVGL
let lvglCode = `#include "${gaugeTitle.toLowerCase()}.h" // Include your header file\n`;
lvglCode += `#include "lvgl.h" // Include LVGL directly\n\n`;
lvglCode += `#ifndef LV_ATTRIBUTE_MEM_ALIGN\n#define LV_ATTRIBUTE_MEM_ALIGN\n#endif\n\n`;
lvglCode += `#ifndef LV_ATTRIBUTE_IMG_${gaugeTitle.toUpperCase()}\n#define LV_ATTRIBUTE_IMG_${gaugeTitle.toUpperCase()}\n#endif\n\n`;
lvglCode += `// Declare the ${gaugeTitle}_map[] array\n`;
lvglCode += `const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_${gaugeTitle.toUpperCase()} uint8_t ${gaugeTitle.toLowerCase()}_map[] = {\n\n`;
// Format the binary data into rows of 16 bytes
for (let i = 0; i < binaryData.length; i++) {
if (i % 16 === 0) {
lvglCode += ' ';
}
lvglCode += `0x${binaryData[i].toString(16).padStart(2, '0')}`;
if (i < binaryData.length - 1) {
lvglCode += ', ';
}
if ((i + 1) % 16 === 0) {
lvglCode += '\n';
}
}
lvglCode += '\n};\n\n';
// Add the LVGL image descriptor
lvglCode += `// Image descriptor for LVGL\n`;
lvglCode += `const lv_img_dsc_t ${gaugeTitle} = {\n`;
lvglCode += ` .header.cf = LV_IMG_CF_INDEXED_1BIT,\n`;
lvglCode += ` .header.always_zero = 0,\n`;
lvglCode += ` .header.reserved = 0,\n`;
lvglCode += ` .header.w = ${canvasSize.width},\n`;
lvglCode += ` .header.h = ${canvasSize.height},\n`;
lvglCode += ` .data_size = ${binaryData.length},\n`;
lvglCode += ` .data = ${gaugeTitle.toLowerCase()}_map\n`;
lvglCode += `};\n`;
if (download) {
// Create a downloadable file
const blob = new Blob([lvglCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${gaugeTitle.toLowerCase()}.c`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return lvglCode;
};
const generateGaugeHeader = (download = true) => {
let headerCode = `#ifndef ${gaugeTitle.toUpperCase()}_H\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_H\n\n`;
headerCode += `#include "lvgl.h"\n\n`;
headerCode += `// Image data declaration\n`;
headerCode += `extern const lv_img_dsc_t ${gaugeTitle};\n\n`;
// Add gauge configuration constants
headerCode += `// Gauge configuration constants\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_MIN_VALUE ${minValue}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_MAX_VALUE ${maxValue}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_ANGLE_RANGE ${endAngle - startAngle}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_START_ANGLE ${startAngle}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_END_ANGLE ${endAngle}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_MAJOR_TICKS ${majorTicks}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_MINOR_TICKS ${minorTicks}\n`;
headerCode += `#define ${gaugeTitle.toUpperCase()}_RED_ZONE_START ${redZoneStart}\n\n`;
headerCode += `#endif // ${gaugeTitle.toUpperCase()}_H\n`;
if (download) {
// Create a downloadable file
const blob = new Blob([headerCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${gaugeTitle.toLowerCase()}.h`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return headerCode;
};
const generateFontCode = () => {
// Generate mock bitmap data for the font characters
// In a real implementation, this would extract actual bitmap data from the canvas
const mockBitmapData = [];
// Number of bytes to generate based on font size and character count
const bytesPerChar = Math.ceil((fontSize * fontSize * fontBpp) / 8);
const totalChars = fontCharacters.length;
// Generate mock bitmap data
for (let i = 0; i < bytesPerChar * totalChars; i++) {
mockBitmapData.push(Math.floor(Math.random() * 256));
}
// Start building the LVGL font file
let fontCode = `/*******************************************************************************\n`;
fontCode += ` * Size: ${fontSize} px\n`;
fontCode += ` * Bpp: ${fontBpp}\n`;
fontCode += ` * Opts: --bpp ${fontBpp} --size ${fontSize} --no-compress --font ${fontFamily.toLowerCase()}.ttf --range 0-120 --format lvgl -o ${fontName.toLowerCase()}.c\n`;
fontCode += ` ******************************************************************************/\n\n`;
fontCode += `#ifdef LV_LVGL_H_INCLUDE_SIMPLE\n`;
fontCode += `#include "lvgl.h"\n`;
fontCode += `#else\n`;
fontCode += `#include "lvgl.h"\n`;
fontCode += `#endif\n\n`;
fontCode += `#ifndef ${fontName.toUpperCase()}\n`;
fontCode += `#define ${fontName.toUpperCase()} 1\n`;
fontCode += `#endif\n\n`;
fontCode += `#if ${fontName.toUpperCase()}\n\n`;
// Bitmaps section
fontCode += `/*-----------------\n`;
fontCode += ` * BITMAPS\n`;
fontCode += ` *----------------*/\n\n`;
fontCode += `/*Store the image of the glyphs*/\n`;
fontCode += `static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {\n`;
// Add bitmap data for each character
const chars = fontCharacters.split('');
chars.forEach((char, charIndex) => {
fontCode += ` /* U+${char.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()} "${char}" */\n`;
// Format bitmap data in rows
for (let i = 0; i < bytesPerChar; i++) {
if (i % 8 === 0) {
fontCode += ' ';
}
const dataIndex = charIndex * bytesPerChar + i;
if (dataIndex < mockBitmapData.length) {
fontCode += `0x${mockBitmapData[dataIndex].toString(16).padStart(2, '0')}`;
if (i < bytesPerChar - 1 && dataIndex < mockBitmapData.length - 1) {
fontCode += ', ';
}
if ((i + 1) % 8 === 0) {
fontCode += '\n';
}
}
}
fontCode += '\n';
});
fontCode += `};\n\n`;
// Glyph description section
fontCode += `/*---------------------\n`;
fontCode += ` * GLYPH DESCRIPTION\n`;
fontCode += ` *--------------------*/\n\n`;
fontCode += `static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {\n`;
fontCode += ` {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,\n`;
// Add glyph descriptions for each character
chars.forEach((char, index) => {
const charCode = char.charCodeAt(0);
const bitmapIndex = index * bytesPerChar;
const width = Math.floor(fontSize * 0.6) + Math.floor(Math.random() * 20); // Simulated width
const height = Math.floor(fontSize * 0.8) + Math.floor(Math.random() * 30); // Simulated height
fontCode += ` {.bitmap_index = ${bitmapIndex}, .adv_w = ${width * 3}, .box_w = ${width}, .box_h = ${height}, .ofs_x = ${Math.floor(Math.random() * 10)}, .ofs_y = ${Math.floor(Math.random() * 30) - 15}}${index < chars.length - 1 ? ',' : ''}\n`;
});
fontCode += `};\n\n`;
// Character mapping section
fontCode += `/*---------------------\n`;
fontCode += ` * CHARACTER MAPPING\n`;
fontCode += ` *--------------------*/\n\n\n\n`;
fontCode += `/*Collect the unicode lists and glyph_id offsets*/\n`;
fontCode += `static const lv_font_fmt_txt_cmap_t cmaps[] =\n`;
fontCode += `{\n`;
fontCode += ` {\n`;
fontCode += ` .range_start = 32, .range_length = 89, .glyph_id_start = 1,\n`;
fontCode += ` .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY\n`;
fontCode += ` }\n`;
fontCode += `};\n\n`;
// Kerning section
fontCode += `/*-----------------\n`;
fontCode += ` * KERNING\n`;
fontCode += ` *----------------*/\n\n\n`;
fontCode += `/*Pair left and right glyphs for kerning*/\n`;
fontCode += `static const uint8_t kern_pair_glyph_ids[] =\n`;
fontCode += `{\n`;
// Add some sample kerning pairs
for (let i = 0; i < 10; i++) {
fontCode += ` ${14 + i}, ${24 + i},\n`;
}
fontCode += `};\n\n`;
fontCode += `/* Kerning between the respective left and right glyphs\n`;
fontCode += ` * 4.4 format which needs to scaled with \`kern_scale\`*/\n`;
fontCode += `static const int8_t kern_pair_values[] =\n`;
fontCode += `{\n`;
// Add kerning values
for (let i = 0; i < 10; i++) {
fontCode += ` ${-50 + Math.floor(Math.random() * 100)}${i < 9 ? ',' : ''} `;
}
fontCode += `\n};\n\n`;
fontCode += `/*Collect the kern pair's data in one place*/\n`;
fontCode += `static const lv_font_fmt_txt_kern_pair_t kern_pairs =\n`;
fontCode += `{\n`;
fontCode += ` .glyph_ids = kern_pair_glyph_ids,\n`;
fontCode += ` .values = kern_pair_values,\n`;
fontCode += ` .pair_cnt = 10,\n`;
fontCode += ` .glyph_ids_size = 0\n`;
fontCode += `};\n\n`;
// All custom data section
fontCode += `/*--------------------\n`;
fontCode += ` * ALL CUSTOM DATA\n`;
fontCode += ` *--------------------*/\n\n`;
fontCode += `#if LVGL_VERSION_MAJOR == 8\n`;
fontCode += `/*Store all the custom data of the font*/\n`;
fontCode += `static lv_font_fmt_txt_glyph_cache_t cache;\n`;
fontCode += `#endif\n\n`;
fontCode += `#if LVGL_VERSION_MAJOR >= 8\n`;
fontCode += `static const lv_font_fmt_txt_dsc_t font_dsc = {\n`;
fontCode += `#else\n`;
fontCode += `static lv_font_fmt_txt_dsc_t font_dsc = {\n`;
fontCode += `#endif\n`;
fontCode += ` .glyph_bitmap = glyph_bitmap,\n`;
fontCode += ` .glyph_dsc = glyph_dsc,\n`;
fontCode += ` .cmaps = cmaps,\n`;
fontCode += ` .kern_dsc = &kern_pairs,\n`;
fontCode += ` .kern_scale = 16,\n`;
fontCode += ` .cmap_num = 1,\n`;
fontCode += ` .bpp = ${fontBpp},\n`;
fontCode += ` .kern_classes = 0,\n`;
fontCode += ` .bitmap_format = 0,\n`;
fontCode += `#if LVGL_VERSION_MAJOR == 8\n`;
fontCode += ` .cache = &cache\n`;
fontCode += `#endif\n`;
fontCode += `};\n\n\n`;
// Public font section
fontCode += `/*-----------------\n`;
fontCode += ` * PUBLIC FONT\n`;
fontCode += ` *----------------*/\n\n`;
fontCode += `/*Initialize a public general font descriptor*/\n`;
fontCode += `#if LVGL_VERSION_MAJOR >= 8\n`;
fontCode += `const lv_font_t ${fontName.toLowerCase()} = {\n`;
fontCode += `#else\n`;
fontCode += `lv_font_t ${fontName.toLowerCase()} = {\n`;
fontCode += `#endif\n`;
fontCode += ` .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/\n`;
fontCode += ` .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/\n`;
fontCode += ` .line_height = ${fontLineHeight}, /*The maximum line height required by the font*/\n`;
fontCode += ` .base_line = ${fontBaselineShift}, /*Baseline measured from the bottom of the line*/\n`;
fontCode += `#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)\n`;
fontCode += ` .subpx = LV_FONT_SUBPX_NONE,\n`;
fontCode += `#endif\n`;
fontCode += `#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8\n`;
fontCode += ` .underline_position = -5,\n`;
fontCode += ` .underline_thickness = 3,\n`;
fontCode += `#endif\n`;
fontCode += ` .dsc = &font_dsc, /*The custom font data. Will be accessed by \`get_glyph_bitmap/dsc\` */\n`;
fontCode += `#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9\n`;
fontCode += ` .fallback = NULL,\n`;
fontCode += `#endif\n`;
fontCode += ` .user_data = NULL,\n`;
fontCode += `};\n\n\n\n`;
fontCode += `#endif /*#if ${fontName.toUpperCase()}*/\n`;
return fontCode;
};
const generateFontHeader = () => {
let headerCode = `#ifndef ${fontName.toUpperCase()}_H\n`;
headerCode += `#define ${fontName.toUpperCase()}_H\n\n`;
headerCode += `#include "lvgl.h"\n\n`;
headerCode += `// Font data declaration\n`;
headerCode += `extern const lv_font_t ${fontName.toLowerCase()};\n\n`;
headerCode += `#endif // ${fontName.toUpperCase()}_H\n`;
return headerCode;
};
exampleCode += `static lv_meter_indicator_t *arc_indicator;\n`;
}
exampleCode += `\n/**\n * Setup the gauge display\n */\n`;
exampleCode += `void setup_gauge() {\n`;
exampleCode += ` // Create meter object\n`;
exampleCode += ` meter = lv_meter_create(lv_scr_act());\n`;
exampleCode += ` lv_obj_center(meter);\n`;
exampleCode += ` lv_obj_set_size(meter, DISPLAY_WIDTH, DISPLAY_HEIGHT);\n\n`;
exampleCode += ` // Set background\n`;
exampleCode += ` lv_obj_set_style_bg_color(meter, lv_color_hex(0x${bgColor.slice(1)}), LV_PART_MAIN);\n`;
exampleCode += ` lv_obj_set_style_bg_opa(meter, LV_OPA_COVER, LV_PART_MAIN);\n\n`;
exampleCode += ` // Setup gauge image\n`;
exampleCode += ` lv_obj_t * img = lv_img_create(meter);\n`;
exampleCode += ` lv_img_set_src(img, &${gaugeTitle});\n`;
exampleCode += ` lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);\n`;
exampleCode += ` lv_obj_set_style_img_recolor(img, lv_color_hex(0x${gaugeColor.slice(1)}), 0);\n`;
exampleCode += ` lv_obj_set_style_img_recolor_opa(img, LV_OPA_COVER, 0);\n\n`;
exampleCode += ` // Configure scale\n`;
exampleCode += ` scale = lv_meter_add_scale(meter);\n`;
exampleCode += ` lv_meter_set_scale_ticks(meter, scale, ${majorTicks * minorTicks}, ${minorTickWidth}, ${minorTickLength}, lv_color_hex(0x${gaugeColor.slice(1)}));\n`;
exampleCode += ` lv_meter_set_scale_major_ticks(meter, scale, ${majorTicks}, ${tickWidth}, ${tickLength}, lv_color_hex(0x${gaugeColor.slice(1)}), 10);\n`;
exampleCode += ` lv_meter_set_scale_range(meter, scale, ${minValue}, ${maxValue}, ${endAngle - startAngle}, ${startAngle});\n\n`;
if (showRedZone) {
exampleCode += ` // Add red zone arc\n`;
exampleCode += ` arc_indicator = lv_meter_add_arc(meter, scale, ${arcWidth}, lv_color_hex(0x${redZoneColor.slice(1)}), 0);\n`;
exampleCode += ` lv_meter_set_indicator_start_value(meter, arc_indicator, ${redZoneStart});\n`;
exampleCode += ` lv_meter_set_indicator_end_value(meter, arc_indicator, ${maxValue});\n\n`;
}
exampleCode += ` // Add normal arc\n`;
exampleCode += ` lv_meter_indicator_t *arc_normal = lv_meter_add_arc(meter, scale, ${arcWidth}, lv_color_hex(0x${arcColor.slice(1)}), 0);\n`;
exampleCode += ` lv_meter_set_indicator_start_value(meter, arc_normal, ${minValue});\n`;
exampleCode += ` lv_meter_set_indicator_end_value(meter, arc_normal, ${showRedZone ? redZoneStart : maxValue});\n\n`;
exampleCode += ` // Add needle\n`;
exampleCode += ` needle = lv_meter_add_needle_line(meter, scale, ${tickWidth}, lv_color_hex(0x${gaugeColor.slice(1)}), -10);\n\n`;
if (showNumbers) {
exampleCode += ` // Setup value label\n`;
exampleCode += ` speed_label = lv_label_create(meter);\n`;
exampleCode += ` lv_obj_set_style_text_align(speed_label, LV_TEXT_ALIGN_CENTER, 0);\n`;
if (fontEditorOpen) {
exampleCode += ` lv_obj_set_style_text_font(speed_label, &${fontName.toLowerCase()}, 0);\n`;
} else {
exampleCode += ` lv_obj_set_style_text_font(speed_label, &lv_font_montserrat_40, 0);\n`;
}
exampleCode += ` lv_obj_set_style_text_color(speed_label, lv_color_hex(0x${gaugeColor.slice(1)}), 0);\n`;
exampleCode += ` lv_obj_align(speed_label, LV_ALIGN_CENTER, 0, ${canvasSize.height / 4});\n`;
exampleCode += ` lv_label_set_text(speed_label, "${numberPrefix}0${numberSuffix}");\n\n`;
}
exampleCode += `}\n\n`;
exampleCode += `/**\n * Update the gauge value\n */\n`;
exampleCode += `void update_gauge_value(int value) {\n`;
exampleCode += ` // Constrain value to valid range\n`;
exampleCode += ` if (value < ${minValue}) value = ${minValue};\n`;
exampleCode += ` if (value > ${maxValue}) value = ${maxValue};\n\n`;
exampleCode += ` // Update needle position\n`;
exampleCode += ` lv_meter_set_indicator_value(meter, needle, value);\n\n`;
if (showNumbers) {
exampleCode += ` // Update value label\n`;
exampleCode += ` char buf[32];\n`;
exampleCode += ` sprintf(buf, "${numberPrefix}%d${numberSuffix}", value);\n`;
exampleCode += ` lv_label_set_text(speed_label, buf);\n`;
}
exampleCode += `}\n\n`;
exampleCode += `void setup() {\n`;
exampleCode += ` // Initialize display and LVGL (not shown)\n`;
exampleCode += ` // ...\n\n`;
exampleCode += ` // Setup the gauge\n`;
exampleCode += ` setup_gauge();\n`;
exampleCode += `}\n\n`;
exampleCode += `void loop() {\n`;
exampleCode += ` // Handle LVGL tasks\n`;
exampleCode += ` lv_timer_handler();\n\n`;
exampleCode += ` // Example: update gauge with increasing value\n`;
exampleCode += ` static int value = ${minValue};\n`;
exampleCode += ` static uint32_t last_update = 0;\n\n`;
exampleCode += ` if (millis() - last_update > 100) {\n`;
exampleCode += ` update_gauge_value(value);\n`;
exampleCode += ` value++;\n`;
exampleCode += ` if (value > ${maxValue}) value = ${minValue};\n`;
exampleCode += ` last_update = millis();\n`;
exampleCode += ` }\n`;
exampleCode += `}\n`;
if (download) {
// Create a downloadable file
const blob = new Blob([exampleCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gauge_example.cpp';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return exampleCode;
};
const drawFontPreview = () => {
const canvas = fontCanvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Set background
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Set font properties
const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
ctx.font = fontString;
ctx.fillStyle = gaugeColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
if (fontArcEnabled) {
// Draw arc text
drawTextAlongArc(ctx, fontCharacters, centerX, centerY, fontArcRadius,
fontArcStart * Math.PI / 180, fontArcEnd * Math.PI / 180,
fontSpacing);
} else {
// Draw normal text with character spacing
drawTextWithSpacing(ctx, fontCharacters, centerX, centerY, fontSpacing);
}
// Draw baseline
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, centerY + fontBaselineShift);
ctx.lineTo(canvas.width, centerY + fontBaselineShift);
ctx.stroke();
};
const drawTextWithSpacing = (ctx, text, x, y, spacing) => {
const chars = text.split('');
// Calculate total width to center properly
let totalWidth = 0;
chars.forEach(char => {
const metrics = ctx.measureText(char);
totalWidth += metrics.width + spacing;
});
// Starting x position to center the text
let startX = x - totalWidth / 2;
// Draw each character with spacing
chars.forEach(char => {
const metrics = ctx.measureText(char);
ctx.fillText(char, startX + metrics.width / 2, y);
startX += metrics.width + spacing;
});
};
const drawTextAlongArc = (ctx, text, centerX, centerY, radius, startAngle, endAngle, spacing) => {
const chars = text.split('');
const angleRange = endAngle - startAngle;
const angleStep = angleRange / (chars.length - 1 + (chars.length - 1) * spacing / 20);
chars.forEach((char, i) => {
const charAngle = startAngle + angleStep * (i + i * spacing / 20);
// Position calculation
const x = centerX + radius * Math.cos(charAngle);
const y = centerY + radius * Math.sin(charAngle);
// Save current state
ctx.save();
// Move to character position and rotate
ctx.translate(x, y);
ctx.rotate(charAngle + Math.PI / 2); // Add 90 degrees to make characters face outward
// Draw the character
ctx.fillText(char, 0, 0);
// Restore state
ctx.restore();
});
};
const resetGauge = () => {
setStartAngle(135);
setEndAngle(405);
setRadius(150);
setTickLength(15);
setTickWidth(3);
setMinorTickLength(7);
setMinorTickWidth(1);
setArcWidth(10);
setGaugeColor('#ffffff');
setArcColor('#00ff00');
setBgColor('#000000');
setMajorTicks(11);
setMinorTicks(4);
setShowNumbers(true);
setNumberFontSize(16);
setMinValue(0);
setMaxValue(100);
setRedZoneStart(80);
setShowRedZone(true);
setRedZoneColor('#ff0000');
setShowTitle(true);
setGaugeTitle('SPEED');
setTitleFontSize(18);
setCenterX(canvasSize.width / 2);
setCenterY(canvasSize.height / 2);
};
// Parse compiler errors to make them more user-friendly
const parseCompilerErrors = (errors) => {
return errors.map(error => {
// Extract error details
const match = error.match(/error: (.*?) at line (\d+), column (\d+)/);
if (!match) {
return {
severity: 'error',
title: 'Compilation error',
description: error,
canAutoFix: false
};
}
const [_, message, line, column] = match;
// Common error translations
const errorTranslations = {
'undeclared identifier': {
title: 'Missing variable or function',
description: `The code is trying to use something that hasn't been defined.`,
suggestedFix: 'Check for typos in variable or function names.'
},
'expected expression': {
title: 'Syntax error in code',
description: 'The code structure is incorrect - likely missing a bracket, parenthesis, or semicolon.',
suggestedFix: 'Check for missing brackets or semicolons in your configuration.'
}
// Add more translations as needed
};
// Find matching translation or use generic
const translation = Object.entries(errorTranslations).find(([key]) =>
message.includes(key)
);
if (translation) {
const [_, details] = translation;
return {
severity: 'error',
title: details.title,
description: `${details.description} (Line ${line})`,
suggestedFix: details.suggestedFix,
canAutoFix: false
};
}
// Generic error
return {
severity: 'error',
title: 'Compilation error',
description: `${message} (Line ${line}, Column ${column})`,
canAutoFix: false
};
});
};