475 lines
14 KiB
TypeScript
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()
|