codenuk_frontend_mine/src/lib/wireframe-converter.tsx

475 lines
14 KiB
TypeScript

"use client"
import React from 'react'
import { ComponentInstance } from './store'
export interface WireframeElement {
id: string
type: 'rect' | 'circle' | 'text' | 'path' | 'line' | 'group'
x: number
y: number
width: number
height: number
fill?: string
stroke?: string
strokeWidth?: number
text?: string
fontSize?: number
fontFamily?: string
fontWeight?: string
rx?: number
ry?: number
cx?: number
cy?: number
r?: number
children?: WireframeElement[]
}
export interface ParsedWireframe {
elements: WireframeElement[]
width: number
height: number
deviceType: 'desktop' | 'tablet' | 'mobile'
}
/**
* Converts SVG wireframe data to React components for the component canvas
*/
export class WireframeToComponentsConverter {
private static instance: WireframeToComponentsConverter
private componentCounter = 0
static getInstance(): WireframeToComponentsConverter {
if (!WireframeToComponentsConverter.instance) {
WireframeToComponentsConverter.instance = new WireframeToComponentsConverter()
}
return WireframeToComponentsConverter.instance
}
/**
* Parse SVG string and extract wireframe elements
*/
parseSVGToWireframe(svgString: string, deviceType: 'desktop' | 'tablet' | 'mobile' = 'desktop'): ParsedWireframe {
try {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml')
const svgElement = svgDoc.querySelector('svg')
if (!svgElement) {
throw new Error('No SVG element found')
}
// Get SVG dimensions
const viewBox = svgElement.getAttribute('viewBox')
let width = 800
let height = 600
if (viewBox) {
const values = viewBox.split(/\s+|,/).map(Number).filter(n => !isNaN(n))
if (values.length >= 4) {
width = values[2]
height = values[3]
}
} else {
const widthAttr = svgElement.getAttribute('width')
const heightAttr = svgElement.getAttribute('height')
if (widthAttr && heightAttr) {
width = parseFloat(widthAttr.replace(/px|pt|em|rem|%/g, '')) || 800
height = parseFloat(heightAttr.replace(/px|pt|em|rem|%/g, '')) || 600
}
}
const elements = this.parseSVGElements(svgElement)
return {
elements,
width,
height,
deviceType
}
} catch (error) {
console.error('Error parsing SVG:', error)
return {
elements: [],
width: 800,
height: 600,
deviceType
}
}
}
/**
* Parse SVG elements recursively
*/
private parseSVGElements(svgElement: Element): WireframeElement[] {
const elements: WireframeElement[] = []
Array.from(svgElement.children).forEach((element) => {
const tagName = element.tagName.toLowerCase()
// Skip non-visual elements
if (['defs', 'style', 'title', 'desc', 'metadata', 'clippath', 'mask'].includes(tagName)) {
return
}
const wireframeElement = this.parseElement(element)
if (wireframeElement) {
elements.push(wireframeElement)
}
})
return elements
}
/**
* Parse individual SVG element
*/
private parseElement(element: Element): WireframeElement | null {
const tagName = element.tagName.toLowerCase()
const id = element.getAttribute('id') || `element-${++this.componentCounter}`
// Check visibility
const visibility = element.getAttribute('visibility')
const display = element.getAttribute('display')
const opacity = element.getAttribute('opacity')
if (visibility === 'hidden' || display === 'none' || opacity === '0') {
return null
}
switch (tagName) {
case 'rect':
return this.parseRect(element as SVGRectElement, id)
case 'circle':
return this.parseCircle(element as SVGCircleElement, id)
case 'ellipse':
return this.parseEllipse(element as SVGEllipseElement, id)
case 'text':
return this.parseText(element as SVGTextElement, id)
case 'path':
return this.parsePath(element as SVGPathElement, id)
case 'line':
return this.parseLine(element as SVGLineElement, id)
case 'g':
return this.parseGroup(element, id)
default:
return null
}
}
private parseRect(element: SVGRectElement, id: string): WireframeElement {
const x = parseFloat(element.getAttribute('x') || '0')
const y = parseFloat(element.getAttribute('y') || '0')
const width = parseFloat(element.getAttribute('width') || '100')
const height = parseFloat(element.getAttribute('height') || '100')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
const rx = parseFloat(element.getAttribute('rx') || '0')
const ry = parseFloat(element.getAttribute('ry') || '0')
return {
id,
type: 'rect',
x,
y,
width,
height,
fill,
stroke,
strokeWidth,
rx,
ry
}
}
private parseCircle(element: SVGCircleElement, id: string): WireframeElement {
const cx = parseFloat(element.getAttribute('cx') || '0')
const cy = parseFloat(element.getAttribute('cy') || '0')
const r = parseFloat(element.getAttribute('r') || '50')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
return {
id,
type: 'circle',
x: cx - r,
y: cy - r,
width: r * 2,
height: r * 2,
fill,
stroke,
strokeWidth,
cx,
cy,
r
}
}
private parseEllipse(element: SVGEllipseElement, id: string): WireframeElement {
const cx = parseFloat(element.getAttribute('cx') || '0')
const cy = parseFloat(element.getAttribute('cy') || '0')
const rx = parseFloat(element.getAttribute('rx') || '50')
const ry = parseFloat(element.getAttribute('ry') || '30')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
return {
id,
type: 'circle',
x: cx - rx,
y: cy - ry,
width: rx * 2,
height: ry * 2,
fill,
stroke,
strokeWidth,
cx,
cy,
r: rx
}
}
private parseText(element: SVGTextElement, id: string): WireframeElement {
const x = parseFloat(element.getAttribute('x') || '0')
const y = parseFloat(element.getAttribute('y') || '0')
const text = element.textContent || ''
const fill = element.getAttribute('fill') || '#000000'
const fontSize = parseFloat(element.getAttribute('font-size') || '16')
const fontFamily = element.getAttribute('font-family') || 'sans-serif'
const fontWeight = element.getAttribute('font-weight') || 'normal'
// Estimate text dimensions
const estimatedWidth = text.length * (fontSize * 0.6)
const estimatedHeight = fontSize
return {
id,
type: 'text',
x,
y,
width: estimatedWidth,
height: estimatedHeight,
fill,
text,
fontSize,
fontFamily,
fontWeight
}
}
private parsePath(element: SVGPathElement, id: string): WireframeElement {
const d = element.getAttribute('d') || ''
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
// For paths, we'll create a bounding box
// This is a simplified approach - in a real implementation, you'd parse the path data
const bounds = this.getPathBounds(d)
return {
id,
type: 'path',
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
fill,
stroke,
strokeWidth
}
}
private parseLine(element: SVGLineElement, id: string): WireframeElement {
const x1 = parseFloat(element.getAttribute('x1') || '0')
const y1 = parseFloat(element.getAttribute('y1') || '0')
const x2 = parseFloat(element.getAttribute('x2') || '0')
const y2 = parseFloat(element.getAttribute('y2') || '0')
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
const x = Math.min(x1, x2)
const y = Math.min(y1, y2)
const width = Math.abs(x2 - x1)
const height = Math.abs(y2 - y1)
return {
id,
type: 'line',
x,
y,
width,
height,
stroke,
strokeWidth
}
}
private parseGroup(element: Element, id: string): WireframeElement {
const children = this.parseSVGElements(element)
// Calculate group bounds
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
children.forEach(child => {
minX = Math.min(minX, child.x)
minY = Math.min(minY, child.y)
maxX = Math.max(maxX, child.x + child.width)
maxY = Math.max(maxY, child.y + child.height)
})
return {
id,
type: 'group',
x: minX === Infinity ? 0 : minX,
y: minY === Infinity ? 0 : minY,
width: maxX === -Infinity ? 0 : maxX - minX,
height: maxY === -Infinity ? 0 : maxY - minY,
children
}
}
private getPathBounds(d: string): { x: number; y: number; width: number; height: number } {
// Simplified path bounds calculation
// In a real implementation, you'd parse the path data properly
return { x: 0, y: 0, width: 100, height: 50 }
}
/**
* Convert wireframe elements to React components
*/
convertToComponents(wireframe: ParsedWireframe): ComponentInstance[] {
const components: ComponentInstance[] = []
wireframe.elements.forEach((element, index) => {
const component = this.elementToComponent(element, index)
if (component) {
components.push(component)
}
})
return components
}
/**
* Convert a single wireframe element to a React component
*/
private elementToComponent(element: WireframeElement, index: number): ComponentInstance | null {
const baseProps = {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
fill: element.fill,
stroke: element.stroke,
strokeWidth: element.strokeWidth
}
switch (element.type) {
case 'rect':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-rect',
props: {
...baseProps,
rx: element.rx || 0,
ry: element.ry || 0
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'circle':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-circle',
props: {
...baseProps,
cx: element.cx || element.x + element.width / 2,
cy: element.cy || element.y + element.height / 2,
r: element.r || Math.min(element.width, element.height) / 2
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'text':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-text',
props: {
...baseProps,
text: element.text || '',
fontSize: element.fontSize || 16,
fontFamily: element.fontFamily || 'sans-serif',
fontWeight: element.fontWeight || 'normal'
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'line':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-line',
props: baseProps,
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'path':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-path',
props: baseProps,
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'group':
// For groups, we'll create a container component
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-group',
props: {
...baseProps,
children: element.children || []
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
default:
return null
}
}
/**
* Convert wireframe to a single SVG component for display
*/
convertToSVGComponent(wireframe: ParsedWireframe): ComponentInstance {
const timestamp = Date.now()
const randomId = Math.random().toString(36).substr(2, 9)
return {
id: `wireframe-svg-${timestamp}-${randomId}`,
type: 'wireframe-svg',
props: {
elements: wireframe.elements,
width: wireframe.width,
height: wireframe.height,
deviceType: wireframe.deviceType
},
position: { x: 0, y: 0 },
size: { width: wireframe.width, height: wireframe.height }
}
}
}
export const wireframeConverter = WireframeToComponentsConverter.getInstance()