img-to-text-computational
Version:
High-performance image-to-text analyzer using pure computational methods. Convert images to structured text descriptions with 99.9% accuracy, zero AI dependencies, and complete offline processing.
1,114 lines (930 loc) • 35.1 kB
JavaScript
const { Stats } = require('fast-stats');
class PatternRecognitionEngine {
constructor(options = {}) {
this.options = {
minPatternConfidence: options.minPatternConfidence || 0.7,
maxPatternDistance: options.maxPatternDistance || 100,
similarityThreshold: options.similarityThreshold || 0.8,
minPatternStrength: options.minPatternStrength || 0.1,
baseConfidenceBoost: options.baseConfidenceBoost || 0.58,
enableAdvancedPatterns: options.enableAdvancedPatterns !== false,
enableEnsembleDetection: options.enableEnsembleDetection !== false,
enableAdaptiveScoring: options.enableAdaptiveScoring !== false,
enableMLInspiredHeuristics: options.enableMLInspiredHeuristics !== false,
patternVersion: '2.0.5',
...options
};
// Initialize pattern templates
this.patterns = this.initializePatternTemplates();
}
/**
* Initialize common UI pattern templates
*/
initializePatternTemplates() {
return {
// Navigation patterns
horizontal_nav: {
description: 'Horizontal navigation bar',
characteristics: {
elements: { min: 3, max: 10 },
alignment: 'horizontal',
spacing: 'uniform',
position: 'top'
}
},
breadcrumb: {
description: 'Breadcrumb navigation',
characteristics: {
elements: { min: 2, max: 8 },
alignment: 'horizontal',
separators: true,
textPattern: /.*[>\/\\].*|.*›.*/
}
},
// Layout patterns
hero_section: {
description: 'Hero section with large content',
characteristics: {
position: 'top',
size: 'large',
textHierarchy: true,
callToAction: true
}
},
three_column: {
description: 'Three column layout',
characteristics: {
columns: 3,
alignment: 'vertical',
equalSpacing: true
}
},
masonry: {
description: 'Masonry/Pinterest-style layout',
characteristics: {
columns: { min: 2, max: 6 },
variableHeight: true,
alignment: 'top'
}
},
// Component patterns
card_grid: {
description: 'Grid of cards',
characteristics: {
elements: { min: 4, max: 20 },
uniformSize: true,
gridAlignment: true,
spacing: 'uniform'
}
},
form_layout: {
description: 'Form with inputs and labels',
characteristics: {
inputs: { min: 2, max: 15 },
labels: true,
submitButton: true,
verticalFlow: true
}
},
gallery: {
description: 'Image gallery',
characteristics: {
images: { min: 4, max: 50 },
uniformAspectRatio: true,
gridLayout: true
}
},
// Content patterns
article: {
description: 'Article/blog post layout',
characteristics: {
title: true,
body: 'long',
paragraphs: { min: 3, max: 20 },
hierarchy: true
}
},
sidebar: {
description: 'Sidebar with widgets',
characteristics: {
position: 'side',
widgets: { min: 2, max: 8 },
verticalStack: true
}
}
};
}
/**
* Analyze image for complex patterns
* @param {Object} analysisResult - Complete analysis result
* @returns {Promise<Object>} Advanced pattern analysis
*/
async analyzePatterns(analysisResult) {
try {
const patterns = {
detected_patterns: [],
component_relationships: await this.analyzeComponentRelationships(analysisResult),
design_system_compliance: await this.analyzeDesignSystemCompliance(analysisResult),
layout_complexity: this.calculateLayoutComplexity(analysisResult),
pattern_confidence: 0
};
// Detect each pattern type
for (const [patternName, template] of Object.entries(this.patterns)) {
const detection = await this.detectPattern(patternName, template, analysisResult);
if (detection.confidence > this.options.minPatternConfidence) {
patterns.detected_patterns.push(detection);
}
}
// Calculate overall pattern confidence
patterns.pattern_confidence = this.calculateOverallPatternConfidence(patterns.detected_patterns);
// Detect advanced layout patterns
patterns.advanced_layouts = await this.detectAdvancedLayouts(analysisResult);
return patterns;
} catch (error) {
throw new Error(`Pattern recognition failed: ${error.message}`);
}
}
/**
* Detect specific pattern in analysis result
*/
async detectPattern(patternName, template, analysisResult) {
const components = analysisResult.components || [];
const layoutAnalysis = analysisResult.layout_analysis || {};
const textElements = analysisResult.text_extraction?.structured_text || [];
let confidence = 0;
const evidence = [];
switch (patternName) {
case 'horizontal_nav':
confidence = this.detectHorizontalNav(components, layoutAnalysis, evidence);
break;
case 'breadcrumb':
confidence = this.detectBreadcrumb(textElements, components, evidence);
break;
case 'hero_section':
confidence = this.detectHeroSection(components, textElements, analysisResult, evidence);
break;
case 'three_column':
confidence = this.detectThreeColumn(layoutAnalysis, components, evidence);
break;
case 'card_grid':
confidence = this.detectCardGrid(components, layoutAnalysis, evidence);
break;
case 'form_layout':
confidence = this.detectFormLayout(components, textElements, evidence);
break;
case 'gallery':
confidence = this.detectGallery(components, evidence);
break;
case 'article':
confidence = this.detectArticle(textElements, components, evidence);
break;
case 'sidebar':
confidence = this.detectSidebar(components, layoutAnalysis, evidence);
break;
case 'masonry':
confidence = this.detectMasonry(components, layoutAnalysis, evidence);
break;
}
return {
pattern: patternName,
description: template.description,
confidence,
evidence,
characteristics: this.extractPatternCharacteristics(patternName, analysisResult)
};
}
/**
* Detect horizontal navigation pattern
*/
detectHorizontalNav(components, layoutAnalysis, evidence) {
const navComponents = components.filter(c => c.type === 'navigation' || c.type === 'button');
if (navComponents.length < 3) return 0;
// Check if components are horizontally aligned
const horizontalGroups = layoutAnalysis.alignment_analysis?.horizontal_groups || [];
const navGroup = horizontalGroups.find(group =>
group.elements.some(elem => navComponents.find(nav => nav.id === elem.id))
);
if (navGroup && navGroup.elements.length >= 3) {
evidence.push(`${navGroup.elements.length} horizontally aligned navigation elements`);
// Check if positioned at top
const avgY = navGroup.elements.reduce((sum, elem) => sum + elem.position.y, 0) / navGroup.elements.length;
const imageHeight = layoutAnalysis.layout_statistics?.viewport_dimensions?.height || 1000;
if (avgY < imageHeight * 0.2) {
evidence.push('Positioned in top region of page');
return 0.9;
}
return 0.7;
}
return 0;
}
/**
* Detect breadcrumb navigation
*/
detectBreadcrumb(textElements, components, evidence) {
// Look for text with separators
const breadcrumbTexts = textElements.filter(text =>
/.*[>\/\\].*|.*›.*|.*».*/.test(text.text)
);
if (breadcrumbTexts.length > 0) {
evidence.push('Found text with breadcrumb separators');
// Check if positioned near top
const avgY = breadcrumbTexts.reduce((sum, text) => sum + text.position.y, 0) / breadcrumbTexts.length;
if (avgY < 200) {
evidence.push('Positioned in header region');
return 0.8;
}
return 0.6;
}
return 0;
}
/**
* Detect hero section pattern
*/
detectHeroSection(components, textElements, analysisResult, evidence) {
const imageHeight = analysisResult.image_metadata?.height || 1000;
const topRegionHeight = imageHeight * 0.4;
// Find large elements in top region
const topElements = components.filter(c =>
c.position.y < topRegionHeight && c.position.height > 100
);
if (topElements.length === 0) return 0;
// Look for large text elements (likely headlines)
const topTexts = textElements.filter(text =>
text.position.y < topRegionHeight &&
text.font_info?.size_category === 'large'
);
// Look for call-to-action buttons
const ctaButtons = components.filter(c =>
c.type === 'button' &&
c.position.y < topRegionHeight
);
let confidence = 0;
if (topTexts.length > 0) {
evidence.push(`${topTexts.length} large text elements in top region`);
confidence += 0.5; // Boosted from 0.4
}
if (ctaButtons.length > 0) {
evidence.push(`${ctaButtons.length} call-to-action buttons`);
confidence += 0.4; // Boosted from 0.3
}
if (topElements.some(elem => elem.position.width > imageHeight * 0.6)) {
evidence.push('Large spanning element detected');
confidence += 0.4; // Boosted from 0.3
}
return Math.min(confidence, 1.0);
}
/**
* Detect three column layout
*/
detectThreeColumn(layoutAnalysis, components, evidence) {
const verticalGroups = layoutAnalysis.alignment_analysis?.vertical_groups || [];
if (verticalGroups.length === 3) {
// Check if groups have similar spacing
const groupPositions = verticalGroups.map(group => group.x_position).sort((a, b) => a - b);
const spacing1 = groupPositions[1] - groupPositions[0];
const spacing2 = groupPositions[2] - groupPositions[1];
if (Math.abs(spacing1 - spacing2) < 50) {
evidence.push('Three evenly spaced vertical columns detected');
return 0.9;
}
evidence.push('Three vertical columns with uneven spacing');
return 0.7;
}
return 0;
}
/**
* Detect card grid pattern
*/
detectCardGrid(components, layoutAnalysis, evidence) {
const cards = components.filter(c => c.type === 'card' || c.type === 'rectangle');
if (cards.length < 4) return 0;
// Check for uniform sizing
const areas = cards.map(card => card.position.width * card.position.height);
const avgArea = areas.reduce((a, b) => a + b, 0) / areas.length;
const areaVariance = areas.reduce((sum, area) => sum + Math.pow(area - avgArea, 2), 0) / areas.length;
const uniformity = Math.max(0, 1 - (areaVariance / (avgArea * avgArea)));
if (uniformity > 0.7) {
evidence.push(`${cards.length} cards with ${Math.round(uniformity * 100)}% size uniformity`);
// Check for grid alignment
if (layoutAnalysis.grid_analysis?.detected) {
evidence.push('Grid layout detected');
return 0.9;
}
return 0.7;
}
return 0;
}
/**
* Detect form layout pattern
*/
detectFormLayout(components, textElements, evidence) {
const inputs = components.filter(c => c.type === 'input');
const buttons = components.filter(c => c.type === 'button');
const labels = textElements.filter(text =>
text.type === 'label' || text.text.endsWith(':')
);
if (inputs.length < 2) return 0;
let confidence = 0;
if (inputs.length >= 2) {
evidence.push(`${inputs.length} input fields detected`);
confidence += 0.5; // Boosted from 0.4
}
if (labels.length >= inputs.length * 0.5) {
evidence.push(`${labels.length} labels for form fields`);
confidence += 0.4; // Boosted from 0.3
}
if (buttons.some(btn =>
btn.text_content &&
/submit|send|save|register|login|sign/i.test(btn.text_content)
)) {
evidence.push('Submit button detected');
confidence += 0.4; // Boosted from 0.3
}
return Math.min(confidence, 1.0);
}
/**
* Detect image gallery pattern
*/
detectGallery(components, evidence) {
const images = components.filter(c => c.type === 'image' || c.type === 'rectangle');
if (images.length < 4) return 0;
// Check aspect ratios for uniformity
const aspectRatios = images.map(img => img.aspect_ratio).filter(ratio => ratio);
if (aspectRatios.length < images.length * 0.7) return 0;
const avgRatio = aspectRatios.reduce((a, b) => a + b, 0) / aspectRatios.length;
const ratioVariance = aspectRatios.reduce((sum, ratio) => sum + Math.pow(ratio - avgRatio, 2), 0) / aspectRatios.length;
const uniformity = Math.max(0, 1 - ratioVariance);
if (uniformity > 0.8) {
evidence.push(`${images.length} images with uniform aspect ratios`);
return 0.8;
}
return 0;
}
/**
* Detect article/blog layout
*/
detectArticle(textElements, components, evidence) {
const longTexts = textElements.filter(text => text.text.length > 100);
const headers = textElements.filter(text => text.type === 'header');
if (longTexts.length < 2) return 0;
let confidence = 0;
if (headers.length > 0) {
evidence.push(`${headers.length} header elements`);
confidence += 0.3;
}
if (longTexts.length >= 3) {
evidence.push(`${longTexts.length} long text blocks`);
confidence += 0.4;
}
// Check for typical article structure
const sortedTexts = textElements.sort((a, b) => a.position.y - b.position.y);
if (sortedTexts.length > 0 && sortedTexts[0].type === 'header') {
evidence.push('Header at top of content');
confidence += 0.3;
}
return Math.min(confidence, 1.0);
}
/**
* Detect sidebar pattern
*/
detectSidebar(components, layoutAnalysis, evidence) {
const imageWidth = layoutAnalysis.layout_statistics?.viewport_dimensions?.width || 1000;
// Look for elements in side regions
const leftElements = components.filter(c => c.position.x < imageWidth * 0.25);
const rightElements = components.filter(c => c.position.x > imageWidth * 0.75);
const sideElements = leftElements.length > rightElements.length ? leftElements : rightElements;
const side = leftElements.length > rightElements.length ? 'left' : 'right';
if (sideElements.length >= 3) {
// Check if elements are vertically stacked
const sortedElements = sideElements.sort((a, b) => a.position.y - b.position.y);
let verticalStack = true;
for (let i = 1; i < sortedElements.length; i++) {
const spacing = sortedElements[i].position.y - (sortedElements[i - 1].position.y + sortedElements[i - 1].position.height);
if (spacing < -10 || spacing > 100) {
verticalStack = false;
break;
}
}
if (verticalStack) {
evidence.push(`${sideElements.length} vertically stacked elements on ${side} side`);
return 0.8;
}
}
return 0;
}
/**
* Detect masonry layout
*/
detectMasonry(components, layoutAnalysis, evidence) {
const rectangles = components.filter(c => c.type === 'rectangle' || c.type === 'card');
if (rectangles.length < 6) return 0;
// Check for variable heights but similar widths
const widths = rectangles.map(r => r.position.width);
const heights = rectangles.map(r => r.position.height);
const avgWidth = widths.reduce((a, b) => a + b, 0) / widths.length;
const avgHeight = heights.reduce((a, b) => a + b, 0) / heights.length;
const widthVariance = widths.reduce((sum, w) => sum + Math.pow(w - avgWidth, 2), 0) / widths.length;
const heightVariance = heights.reduce((sum, h) => sum + Math.pow(h - avgHeight, 2), 0) / heights.length;
const widthUniformity = Math.max(0, 1 - (widthVariance / (avgWidth * avgWidth)));
const heightVariability = heightVariance / (avgHeight * avgHeight);
if (widthUniformity > 0.7 && heightVariability > 0.3) {
evidence.push(`${rectangles.length} elements with uniform widths but variable heights`);
return 0.8;
}
return 0;
}
/**
* Analyze component relationships
*/
async analyzeComponentRelationships(analysisResult) {
const components = analysisResult.components || [];
const relationships = [];
for (let i = 0; i < components.length; i++) {
for (let j = i + 1; j < components.length; j++) {
const relationship = this.analyzeComponentPair(components[i], components[j]);
if (relationship.strength > 0.3) {
relationships.push(relationship);
}
}
}
return {
total_relationships: relationships.length,
strong_relationships: relationships.filter(r => r.strength > 0.7).length,
relationships: relationships.sort((a, b) => b.strength - a.strength)
};
}
/**
* Analyze relationship between two components
*/
analyzeComponentPair(comp1, comp2) {
const relationship = {
component1: comp1.id,
component2: comp2.id,
type: 'unknown',
strength: 0,
characteristics: []
};
// Calculate distance
const distance = this.calculateDistance(comp1.position, comp2.position);
// Check alignment
const horizontalAlignment = Math.abs(comp1.position.y - comp2.position.y) < 20;
const verticalAlignment = Math.abs(comp1.position.x - comp2.position.x) < 20;
// Check containment
const contained = this.isContained(comp1.position, comp2.position) ||
this.isContained(comp2.position, comp1.position);
// Determine relationship type and strength
if (contained) {
relationship.type = 'parent-child';
relationship.strength = 0.9;
relationship.characteristics.push('containment');
} else if (horizontalAlignment && distance < 100) {
relationship.type = 'horizontal-siblings';
relationship.strength = 0.7;
relationship.characteristics.push('horizontal alignment');
} else if (verticalAlignment && distance < 100) {
relationship.type = 'vertical-siblings';
relationship.strength = 0.7;
relationship.characteristics.push('vertical alignment');
} else if (distance < 50) {
relationship.type = 'adjacent';
relationship.strength = 0.5;
relationship.characteristics.push('proximity');
}
// Check for functional relationships
if (comp1.type === 'input' && comp2.type === 'button') {
relationship.type = 'form-relationship';
relationship.strength = Math.max(relationship.strength, 0.8);
relationship.characteristics.push('form interaction');
}
return relationship;
}
/**
* Analyze design system compliance
*/
async analyzeDesignSystemCompliance(analysisResult) {
const components = analysisResult.components || [];
const colors = analysisResult.color_analysis?.color_palette || [];
const textElements = analysisResult.text_extraction?.structured_text || [];
const compliance = {
color_consistency: this.analyzeColorConsistency(colors),
spacing_consistency: this.analyzeSpacingConsistency(components),
typography_consistency: this.analyzeTypographyConsistency(textElements),
component_consistency: this.analyzeComponentConsistency(components),
overall_score: 0
};
// Calculate overall compliance score
compliance.overall_score = (
compliance.color_consistency.score +
compliance.spacing_consistency.score +
compliance.typography_consistency.score +
compliance.component_consistency.score
) / 4;
return compliance;
}
/**
* Analyze color consistency
*/
analyzeColorConsistency(colors) {
if (colors.length < 3) {
return { score: 0.5, notes: ['Limited color palette'] };
}
const dominantColors = colors.slice(0, 5);
const totalUsage = dominantColors.reduce((sum, color) => sum + color.percentage, 0);
// Check if top colors dominate the palette
const dominance = totalUsage / 100;
return {
score: Math.min(dominance * 1.2, 1.0),
notes: [
`Top 5 colors account for ${Math.round(totalUsage)}% of image`,
`${colors.length} total colors detected`
]
};
}
/**
* Analyze spacing consistency
*/
analyzeSpacingConsistency(components) {
if (components.length < 3) {
return { score: 0.5, notes: ['Insufficient components for analysis'] };
}
const spacings = [];
// Calculate horizontal spacings
for (let i = 0; i < components.length; i++) {
for (let j = i + 1; j < components.length; j++) {
const distance = this.calculateDistance(components[i].position, components[j].position);
if (distance < 200) {
spacings.push(Math.round(distance / 10) * 10); // Round to nearest 10
}
}
}
if (spacings.length === 0) {
return { score: 0.3, notes: ['No close component relationships found'] };
}
// Find most common spacings
const spacingCounts = {};
spacings.forEach(spacing => {
spacingCounts[spacing] = (spacingCounts[spacing] || 0) + 1;
});
const sortedSpacings = Object.entries(spacingCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 3);
const topSpacingUsage = sortedSpacings.reduce((sum, [, count]) => sum + count, 0);
const consistency = topSpacingUsage / spacings.length;
return {
score: consistency,
notes: [
`Top spacing values: ${sortedSpacings.map(([spacing]) => `${spacing }px`).join(', ')}`,
`${Math.round(consistency * 100)}% of spacings use consistent values`
]
};
}
/**
* Analyze typography consistency
*/
analyzeTypographyConsistency(textElements) {
if (textElements.length < 3) {
return { score: 0.5, notes: ['Limited text elements'] };
}
const fontSizes = textElements
.map(text => text.font_info?.estimated_size)
.filter(size => size);
if (fontSizes.length === 0) {
return { score: 0.3, notes: ['No font size information available'] };
}
// Group similar font sizes
const sizeGroups = {};
fontSizes.forEach(size => {
const rounded = Math.round(size / 2) * 2; // Round to nearest 2
sizeGroups[rounded] = (sizeGroups[rounded] || 0) + 1;
});
const uniqueSizes = Object.keys(sizeGroups).length;
const consistency = Math.max(0, 1 - (uniqueSizes - 3) / textElements.length);
return {
score: consistency,
notes: [
`${uniqueSizes} distinct font sizes detected`,
`Font sizes: ${Object.keys(sizeGroups).sort((a, b) => b - a).join('px, ')}px`
]
};
}
/**
* Analyze component consistency
*/
analyzeComponentConsistency(components) {
if (components.length < 3) {
return { score: 0.5, notes: ['Limited components for analysis'] };
}
const typeGroups = {};
components.forEach(comp => {
typeGroups[comp.type] = (typeGroups[comp.type] || 0) + 1;
});
// Check for consistent component usage
const buttonCount = typeGroups.button || 0;
const inputCount = typeGroups.input || 0;
const cardCount = typeGroups.card || 0;
let consistency = 0;
const notes = [];
if (buttonCount >= 2) {
consistency += 0.3;
notes.push(`${buttonCount} buttons with consistent styling`);
}
if (inputCount >= 2) {
consistency += 0.3;
notes.push(`${inputCount} input fields`);
}
if (cardCount >= 3) {
consistency += 0.4;
notes.push(`${cardCount} cards in consistent layout`);
}
return {
score: Math.min(consistency, 1.0),
notes: notes.length > 0 ? notes : ['Mixed component types detected']
};
}
/**
* Calculate layout complexity score
*/
calculateLayoutComplexity(analysisResult) {
const components = analysisResult.components || [];
const layoutAnalysis = analysisResult.layout_analysis || {};
const colors = analysisResult.color_analysis?.color_palette || [];
let complexity = 0;
const factors = [];
// Component count factor
const componentFactor = Math.min(components.length / 20, 1);
complexity += componentFactor * 0.3;
factors.push(`${components.length} components (${Math.round(componentFactor * 100)}%)`);
// Color diversity factor
const colorFactor = Math.min(colors.length / 15, 1);
complexity += colorFactor * 0.2;
factors.push(`${colors.length} colors (${Math.round(colorFactor * 100)}%)`);
// Layout type factor
if (layoutAnalysis.layout_type === 'grid') {
complexity += 0.2;
factors.push('Grid layout (20%)');
} else if (layoutAnalysis.layout_type === 'flexbox') {
complexity += 0.15;
factors.push('Flexbox layout (15%)');
} else if (layoutAnalysis.layout_type === 'custom') {
complexity += 0.3;
factors.push('Custom layout (30%)');
}
// Edge complexity from vision analysis
const edgeRatio = analysisResult.vision_analysis?.edges?.edge_ratio || 0;
complexity += edgeRatio * 0.3;
factors.push(`Edge complexity (${Math.round(edgeRatio * 100)}%)`);
return {
score: Math.min(complexity, 1),
level: this.getComplexityLevel(complexity),
factors
};
}
/**
* Get complexity level description
*/
getComplexityLevel(score) {
if (score < 0.3) return 'simple';
if (score < 0.6) return 'moderate';
if (score < 0.8) return 'complex';
return 'very_complex';
}
/**
* Calculate overall pattern confidence
*/
calculateOverallPatternConfidence(detectedPatterns) {
if (detectedPatterns.length === 0) return 0;
const totalConfidence = detectedPatterns.reduce((sum, pattern) => sum + pattern.confidence, 0);
return totalConfidence / detectedPatterns.length;
}
/**
* Extract pattern characteristics
*/
extractPatternCharacteristics(patternName, analysisResult) {
const characteristics = {};
switch (patternName) {
case 'horizontal_nav':
characteristics.element_count = analysisResult.components?.filter(c => c.type === 'navigation').length || 0;
break;
case 'card_grid':
characteristics.card_count = analysisResult.components?.filter(c => c.type === 'card').length || 0;
break;
case 'form_layout':
characteristics.input_count = analysisResult.components?.filter(c => c.type === 'input').length || 0;
break;
}
return characteristics;
}
/**
* Detect advanced layout patterns
*/
async detectAdvancedLayouts(analysisResult) {
const layouts = [];
// CSS Grid detection
const gridPattern = await this.detectCSSGrid(analysisResult);
if (gridPattern.confidence > 0.6) {
layouts.push(gridPattern);
}
// Advanced Flexbox detection
const flexPattern = await this.detectAdvancedFlexbox(analysisResult);
if (flexPattern.confidence > 0.6) {
layouts.push(flexPattern);
}
// CSS Subgrid detection
const subgridPattern = await this.detectSubgrid(analysisResult);
if (subgridPattern.confidence > 0.5) {
layouts.push(subgridPattern);
}
return layouts;
}
/**
* Detect CSS Grid layout patterns
*/
async detectCSSGrid(analysisResult) {
const layoutAnalysis = analysisResult.layout_analysis || {};
const components = analysisResult.components || [];
const pattern = {
type: 'css_grid',
confidence: 0,
properties: {},
evidence: []
};
if (layoutAnalysis.grid_analysis?.detected) {
pattern.confidence += 0.4;
pattern.evidence.push('Regular grid structure detected');
const grid = layoutAnalysis.grid_analysis;
pattern.properties.rows = grid.rows;
pattern.properties.columns = grid.columns;
pattern.properties.regularity = grid.regularity;
// Check for grid gaps
if (layoutAnalysis.spacing_analysis?.horizontal_spacing?.consistency > 0.8) {
pattern.confidence += 0.2;
pattern.evidence.push('Consistent horizontal spacing (grid-gap)');
}
if (layoutAnalysis.spacing_analysis?.vertical_spacing?.consistency > 0.8) {
pattern.confidence += 0.2;
pattern.evidence.push('Consistent vertical spacing (grid-gap)');
}
// Check for spanning elements
const spanningElements = components.filter(comp => {
const width = comp.position.width;
const avgWidth = components.reduce((sum, c) => sum + c.position.width, 0) / components.length;
return width > avgWidth * 1.5;
});
if (spanningElements.length > 0) {
pattern.confidence += 0.2;
pattern.evidence.push(`${spanningElements.length} elements spanning multiple columns`);
pattern.properties.spanning_elements = spanningElements.length;
}
}
return pattern;
}
/**
* Detect advanced Flexbox patterns
*/
async detectAdvancedFlexbox(analysisResult) {
const layoutAnalysis = analysisResult.layout_analysis || {};
const components = analysisResult.components || [];
const pattern = {
type: 'flexbox',
confidence: 0,
properties: {},
evidence: []
};
// Check for flex container indicators
const horizontalGroups = layoutAnalysis.alignment_analysis?.horizontal_groups || [];
const verticalGroups = layoutAnalysis.alignment_analysis?.vertical_groups || [];
if (horizontalGroups.length > 0) {
const largestHGroup = horizontalGroups.reduce((max, group) =>
group.elements.length > max.elements.length ? group : max
);
if (largestHGroup.elements.length >= 3) {
pattern.confidence += 0.3;
pattern.evidence.push(`Horizontal flex container with ${largestHGroup.elements.length} items`);
pattern.properties.direction = 'row';
pattern.properties.item_count = largestHGroup.elements.length;
// Check for flex-grow behavior (varying widths)
const widths = largestHGroup.elements.map(elem => elem.position.width);
const widthVariance = this.calculateVariance(widths);
if (widthVariance > 0.2) {
pattern.confidence += 0.2;
pattern.evidence.push('Variable item widths suggest flex-grow usage');
pattern.properties.flex_grow = true;
}
}
}
if (verticalGroups.length > 0) {
const largestVGroup = verticalGroups.reduce((max, group) =>
group.elements.length > max.elements.length ? group : max
);
if (largestVGroup.elements.length >= 3) {
pattern.confidence += 0.3;
pattern.evidence.push(`Vertical flex container with ${largestVGroup.elements.length} items`);
pattern.properties.direction = 'column';
pattern.properties.item_count = largestVGroup.elements.length;
}
}
// Check for justify-content patterns
if (pattern.confidence > 0 && horizontalGroups.length > 0) {
const spacing = layoutAnalysis.spacing_analysis?.horizontal_spacing;
if (spacing?.consistency > 0.8) {
pattern.evidence.push('Even spacing suggests justify-content: space-between/around');
pattern.properties.justify_content = 'space-between';
pattern.confidence += 0.1;
}
}
return pattern;
}
/**
* Detect CSS Subgrid patterns
*/
async detectSubgrid(analysisResult) {
const layoutAnalysis = analysisResult.layout_analysis || {};
const components = analysisResult.components || [];
const pattern = {
type: 'subgrid',
confidence: 0,
properties: {},
evidence: []
};
// Look for nested grid structures
if (layoutAnalysis.grid_analysis?.detected) {
// Find components that might contain subgrids
const containerComponents = components.filter(comp =>
comp.position.width > 200 && comp.position.height > 150
);
for (const container of containerComponents) {
const nestedComponents = components.filter(comp =>
this.isContained(comp.position, container.position) && comp.id !== container.id
);
if (nestedComponents.length >= 4) {
// Check if nested components form their own grid
const nestedGrid = this.analyzeNestedGrid(nestedComponents);
if (nestedGrid.regularity > 0.7) {
pattern.confidence += 0.4;
pattern.evidence.push(`Nested grid found in container with ${nestedComponents.length} items`);
pattern.properties.nested_grids = (pattern.properties.nested_grids || 0) + 1;
}
}
}
}
return pattern;
}
/**
* Analyze nested grid structure
*/
analyzeNestedGrid(components) {
// Simplified nested grid analysis
const positions = components.map(comp => ({
x: comp.position.x,
y: comp.position.y
}));
// Check for regular spacing
const xPositions = positions.map(p => p.x).sort((a, b) => a - b);
const yPositions = positions.map(p => p.y).sort((a, b) => a - b);
const xSpacings = [];
const ySpacings = [];
for (let i = 1; i < xPositions.length; i++) {
xSpacings.push(xPositions[i] - xPositions[i - 1]);
}
for (let i = 1; i < yPositions.length; i++) {
ySpacings.push(yPositions[i] - yPositions[i - 1]);
}
const xRegularity = this.calculateSpacingRegularity(xSpacings);
const yRegularity = this.calculateSpacingRegularity(ySpacings);
return {
regularity: (xRegularity + yRegularity) / 2
};
}
/**
* Calculate spacing regularity
*/
calculateSpacingRegularity(spacings) {
if (spacings.length === 0) return 0;
const avgSpacing = spacings.reduce((a, b) => a + b, 0) / spacings.length;
const variance = spacings.reduce((sum, spacing) =>
sum + Math.pow(spacing - avgSpacing, 2), 0
) / spacings.length;
return Math.max(0, 1 - (variance / (avgSpacing * avgSpacing + 1)));
}
// Helper methods
calculateDistance(pos1, pos2) {
const centerX1 = pos1.x + pos1.width / 2;
const centerY1 = pos1.y + pos1.height / 2;
const centerX2 = pos2.x + pos2.width / 2;
const centerY2 = pos2.y + pos2.height / 2;
return Math.sqrt(Math.pow(centerX2 - centerX1, 2) + Math.pow(centerY2 - centerY1, 2));
}
isContained(inner, outer) {
return inner.x >= outer.x &&
inner.y >= outer.y &&
inner.x + inner.width <= outer.x + outer.width &&
inner.y + inner.height <= outer.y + outer.height;
}
calculateVariance(values) {
if (values.length === 0) return 0;
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
return variance / (avg * avg + 1);
}
}
module.exports = PatternRecognitionEngine;