'use client'; import type { ReactNode } from 'react'; import type { PPTShapeElement, ShapeText } from '@openmaic/dsl'; import { useElementOutline } from '../shared/useElementOutline'; import { useElementShadow } from '../shared/useElementShadow'; import { useElementFlip } from '../shared/useElementFlip'; import { useElementFill } from '../shared/useElementFill'; import { GradientDefs } from './GradientDefs'; import { PatternDefs } from './PatternDefs'; import { preservesPlainTextLineBreaks } from '../../utils/richText'; export interface BaseShapeElementProps { elementInfo: PPTShapeElement; /** Replace the static Shape label with editor content. */ renderLabel?: (element: PPTShapeElement, defaultContent: ReactNode) => ReactNode; } /** * Bounding box of a path's coordinates (path/viewBox space). Uses control * points for curves (a safe superset). Handles the commands our shapes emit * (M/L/C/Q/S/H/V/A/T/Z, absolute). Returns null if no coordinates. */ function pathCoordBBox( d: string, ): { minX: number; minY: number; maxX: number; maxY: number } | null { const tokens = d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi); if (!tokens) return null; let i = 0; let cmd = ''; let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; const addX = (x: number) => { if (x < minX) minX = x; if (x > maxX) maxX = x; }; const addY = (y: number) => { if (y < minY) minY = y; if (y > maxY) maxY = y; }; const num = () => parseFloat(tokens[i++]); while (i < tokens.length) { // Path commands are single letters. Use an anchored test so number tokens // in scientific notation (e.g. "4.74e-16") aren't misread as commands. if (/^[a-zA-Z]$/.test(tokens[i])) { cmd = tokens[i]; i++; } const c = cmd.toUpperCase(); if (c === 'M' || c === 'L' || c === 'T') { addX(num()); addY(num()); } else if (c === 'C') { addX(num()); addY(num()); addX(num()); addY(num()); addX(num()); addY(num()); } else if (c === 'Q' || c === 'S') { addX(num()); addY(num()); addX(num()); addY(num()); } else if (c === 'H') { addX(num()); } else if (c === 'V') { addY(num()); } else if (c === 'A') { num(); num(); num(); num(); num(); addX(num()); addY(num()); } else if (c === 'Z') { // no coords } else { i++; // unknown token, skip to avoid infinite loop } } if ( !Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY) ) { return null; } return { minX, minY, maxX, maxY }; } export function BaseShapeElement({ elementInfo, renderLabel }: BaseShapeElementProps) { const { fill } = useElementFill(elementInfo, 'base'); const { outlineWidth, outlineColor, strokeDashArray } = useElementOutline(elementInfo.outline); const { shadowStyle } = useElementShadow(elementInfo.shadow); const { flipStyle } = useElementFlip(elementInfo.flipH, elementInfo.flipV); const text: ShapeText = elementInfo.text || { content: '', align: 'middle', defaultFontName: 'Microsoft YaHei', defaultColor: '#333333', }; const justifyContent = text.align === 'top' ? 'flex-start' : text.align === 'bottom' ? 'flex-end' : 'center'; const defaultLabelContent = (