{/* Main content */}
{/* Sidebar */}
{activeTab === 'gauge' && (
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
)} {activeTab === 'icons' && (
{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" />
))}
)}
{/* Canvas area */}
{!fontEditorOpen ? ( ) : (

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" />
)} {/* Validation Results Modal */} {validationResults && (

{validationResults.success ? <> Validation Successful : <> Validation Issues Found}

{validationResults.issues && validationResults.issues.map((issue, i) => (

{issue.severity === 'error' ? : } {issue.title}

{issue.description}

{issue.suggestedFix && (
Suggested Fix:
{issue.suggestedFix}
{issue.canAutoFix && ( )}
)}
))} {validationResults.success && (

{validationResults.message || "All files validated successfully! Your designs are ready to export."}

)}
{validationResults.success ? ( ) : ( )}
)} {/* Design Principles Modal */} {showPrinciples && (

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.
)} {/* Loading Indicator for Compilation */} {isCompiling && (

Compiling and validating your gauge design...

)}
); }; export default GaugeDesigner; /> setFontArcRadius(parseInt(e.target.value))} className="w-16 bg-gray-700 rounded px-2 py-1" />
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 }; }); };