536 lines
15 KiB
JavaScript
536 lines
15 KiB
JavaScript
import { getTextByPathList } from './utils'
|
|
|
|
export function shapeArc(cX, cY, rX, rY, stAng, endAng, isClose) {
|
|
let dData
|
|
let angle = stAng
|
|
if (endAng >= stAng) {
|
|
while (angle <= endAng) {
|
|
const radians = angle * (Math.PI / 180)
|
|
const x = cX + Math.cos(radians) * rX
|
|
const y = cY + Math.sin(radians) * rY
|
|
if (angle === stAng) {
|
|
dData = ' M' + x + ' ' + y
|
|
}
|
|
dData += ' L' + x + ' ' + y
|
|
angle++
|
|
}
|
|
}
|
|
else {
|
|
while (angle > endAng) {
|
|
const radians = angle * (Math.PI / 180)
|
|
const x = cX + Math.cos(radians) * rX
|
|
const y = cY + Math.sin(radians) * rY
|
|
if (angle === stAng) {
|
|
dData = ' M ' + x + ' ' + y
|
|
}
|
|
dData += ' L ' + x + ' ' + y
|
|
angle--
|
|
}
|
|
}
|
|
dData += (isClose ? ' z' : '')
|
|
return dData
|
|
}
|
|
|
|
export function getCustomShapePath(custShapType, w, h) {
|
|
const pathLstNode = getTextByPathList(custShapType, ['a:pathLst'])
|
|
let pathNodes = getTextByPathList(pathLstNode, ['a:path'])
|
|
|
|
if (Array.isArray(pathNodes)) pathNodes = pathNodes.shift()
|
|
|
|
const maxX = parseInt(pathNodes['attrs']['w'])
|
|
const maxY = parseInt(pathNodes['attrs']['h'])
|
|
const cX = maxX === 0 ? 0 : (1 / maxX) * w
|
|
const cY = maxY === 0 ? 0 : (1 / maxY) * h
|
|
let d = ''
|
|
|
|
let moveToNode = getTextByPathList(pathNodes, ['a:moveTo'])
|
|
|
|
let lnToNodes = pathNodes['a:lnTo']
|
|
let cubicBezToNodes = pathNodes['a:cubicBezTo']
|
|
let quadBezToNodes = pathNodes['a:quadBezTo']
|
|
const arcToNodes = pathNodes['a:arcTo']
|
|
let closeNode = getTextByPathList(pathNodes, ['a:close'])
|
|
if (!Array.isArray(moveToNode)) moveToNode = [moveToNode]
|
|
|
|
const multiSapeAry = []
|
|
if (moveToNode.length > 0) {
|
|
Object.keys(moveToNode).forEach(key => {
|
|
const moveToPtNode = moveToNode[key]['a:pt']
|
|
if (moveToPtNode) {
|
|
Object.keys(moveToPtNode).forEach(key => {
|
|
const moveToNoPt = moveToPtNode[key]
|
|
const spX = moveToNoPt['x']
|
|
const spY = moveToNoPt['y']
|
|
const order = moveToNoPt['order']
|
|
multiSapeAry.push({
|
|
type: 'movto',
|
|
x: spX,
|
|
y: spY,
|
|
order,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
if (lnToNodes) {
|
|
if (!Array.isArray(lnToNodes)) lnToNodes = [lnToNodes]
|
|
Object.keys(lnToNodes).forEach(key => {
|
|
const lnToPtNode = lnToNodes[key]['a:pt']
|
|
if (lnToPtNode) {
|
|
Object.keys(lnToPtNode).forEach(key => {
|
|
const lnToNoPt = lnToPtNode[key]
|
|
const ptX = lnToNoPt['x']
|
|
const ptY = lnToNoPt['y']
|
|
const order = lnToNoPt['order']
|
|
multiSapeAry.push({
|
|
type: 'lnto',
|
|
x: ptX,
|
|
y: ptY,
|
|
order,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
if (cubicBezToNodes) {
|
|
const cubicBezToPtNodesAry = []
|
|
if (!Array.isArray(cubicBezToNodes)) cubicBezToNodes = [cubicBezToNodes]
|
|
Object.keys(cubicBezToNodes).forEach(key => {
|
|
cubicBezToPtNodesAry.push(cubicBezToNodes[key]['a:pt'])
|
|
})
|
|
|
|
cubicBezToPtNodesAry.forEach(key => {
|
|
const pts_ary = []
|
|
key.forEach(pt => {
|
|
const pt_obj = {
|
|
x: pt['attrs']['x'],
|
|
y: pt['attrs']['y'],
|
|
}
|
|
pts_ary.push(pt_obj)
|
|
})
|
|
const order = key[0]['attrs']['order']
|
|
multiSapeAry.push({
|
|
type: 'cubicBezTo',
|
|
cubBzPt: pts_ary,
|
|
order,
|
|
})
|
|
})
|
|
}
|
|
if (quadBezToNodes) {
|
|
const quadBezToPtNodesAry = []
|
|
if (!Array.isArray(quadBezToNodes)) quadBezToNodes = [quadBezToNodes]
|
|
Object.keys(quadBezToNodes).forEach(key => {
|
|
quadBezToPtNodesAry.push(quadBezToNodes[key]['a:pt'])
|
|
})
|
|
|
|
quadBezToPtNodesAry.forEach(key => {
|
|
const pts_ary = []
|
|
key.forEach(pt => {
|
|
const pt_obj = {
|
|
x: pt['attrs']['x'],
|
|
y: pt['attrs']['y'],
|
|
}
|
|
pts_ary.push(pt_obj)
|
|
})
|
|
const order = key[0]['attrs']['order']
|
|
multiSapeAry.push({
|
|
type: 'quadBezTo',
|
|
quadBzPt: pts_ary,
|
|
order,
|
|
})
|
|
})
|
|
}
|
|
if (arcToNodes) {
|
|
const arcToNodesAttrs = arcToNodes['attrs']
|
|
const order = arcToNodesAttrs['order']
|
|
const hR = arcToNodesAttrs['hR']
|
|
const wR = arcToNodesAttrs['wR']
|
|
const stAng = arcToNodesAttrs['stAng']
|
|
const swAng = arcToNodesAttrs['swAng']
|
|
let shftX = 0
|
|
let shftY = 0
|
|
const arcToPtNode = getTextByPathList(arcToNodes, ['a:pt', 'attrs'])
|
|
if (arcToPtNode) {
|
|
shftX = arcToPtNode['x']
|
|
shftY = arcToPtNode['y']
|
|
}
|
|
multiSapeAry.push({
|
|
type: 'arcTo',
|
|
hR: hR,
|
|
wR: wR,
|
|
stAng: stAng,
|
|
swAng: swAng,
|
|
shftX: shftX,
|
|
shftY: shftY,
|
|
order,
|
|
})
|
|
}
|
|
if (closeNode) {
|
|
if (!Array.isArray(closeNode)) closeNode = [closeNode]
|
|
Object.keys(closeNode).forEach(() => {
|
|
multiSapeAry.push({
|
|
type: 'close',
|
|
order: Infinity,
|
|
})
|
|
})
|
|
}
|
|
|
|
multiSapeAry.sort((a, b) => a.order - b.order)
|
|
|
|
let k = 0
|
|
while (k < multiSapeAry.length) {
|
|
if (multiSapeAry[k].type === 'movto') {
|
|
const spX = parseInt(multiSapeAry[k].x) * cX
|
|
const spY = parseInt(multiSapeAry[k].y) * cY
|
|
d += ' M' + spX + ',' + spY
|
|
}
|
|
else if (multiSapeAry[k].type === 'lnto') {
|
|
const Lx = parseInt(multiSapeAry[k].x) * cX
|
|
const Ly = parseInt(multiSapeAry[k].y) * cY
|
|
d += ' L' + Lx + ',' + Ly
|
|
}
|
|
else if (multiSapeAry[k].type === 'cubicBezTo') {
|
|
const Cx1 = parseInt(multiSapeAry[k].cubBzPt[0].x) * cX
|
|
const Cy1 = parseInt(multiSapeAry[k].cubBzPt[0].y) * cY
|
|
const Cx2 = parseInt(multiSapeAry[k].cubBzPt[1].x) * cX
|
|
const Cy2 = parseInt(multiSapeAry[k].cubBzPt[1].y) * cY
|
|
const Cx3 = parseInt(multiSapeAry[k].cubBzPt[2].x) * cX
|
|
const Cy3 = parseInt(multiSapeAry[k].cubBzPt[2].y) * cY
|
|
d += ' C' + Cx1 + ',' + Cy1 + ' ' + Cx2 + ',' + Cy2 + ' ' + Cx3 + ',' + Cy3
|
|
}
|
|
else if (multiSapeAry[k].type === 'quadBezTo') {
|
|
const Qx1 = parseInt(multiSapeAry[k].quadBzPt[0].x) * cX
|
|
const Qy1 = parseInt(multiSapeAry[k].quadBzPt[0].y) * cY
|
|
const Qx2 = parseInt(multiSapeAry[k].quadBzPt[1].x) * cX
|
|
const Qy2 = parseInt(multiSapeAry[k].quadBzPt[1].y) * cY
|
|
d += ' Q' + Qx1 + ',' + Qy1 + ' ' + Qx2 + ',' + Qy2
|
|
}
|
|
else if (multiSapeAry[k].type === 'arcTo') {
|
|
const hR = parseInt(multiSapeAry[k].hR) * cX
|
|
const wR = parseInt(multiSapeAry[k].wR) * cY
|
|
const stAng = parseInt(multiSapeAry[k].stAng) / 60000
|
|
const swAng = parseInt(multiSapeAry[k].swAng) / 60000
|
|
const endAng = stAng + swAng
|
|
d += shapeArc(wR, hR, wR, hR, stAng, endAng, false)
|
|
}
|
|
else if (multiSapeAry[k].type === 'close') d += 'z'
|
|
k++
|
|
}
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
export function identifyShape(shapeData) {
|
|
const pathLst = shapeData['a:pathLst']
|
|
if (!pathLst || !pathLst['a:path']) return 'custom'
|
|
|
|
const path = pathLst['a:path']
|
|
const pathWidth = parseInt(path.attrs?.w) || 0
|
|
const pathHeight = parseInt(path.attrs?.h) || 0
|
|
|
|
const commands = extractPathCommands(path)
|
|
|
|
if (commands.length === 0) return 'custom'
|
|
|
|
const analysis = analyzePathCommands(commands, pathWidth, pathHeight)
|
|
|
|
return matchShape(analysis)
|
|
}
|
|
|
|
function extractPathCommands(path) {
|
|
const commands = []
|
|
|
|
if (path['a:moveTo']) {
|
|
const moveTo = path['a:moveTo']
|
|
const pt = moveTo['a:pt']
|
|
if (pt) {
|
|
commands.push({
|
|
type: 'moveTo',
|
|
points: [{ x: parseInt(pt.attrs?.x) || 0, y: parseInt(pt.attrs?.y) || 0 }]
|
|
})
|
|
}
|
|
}
|
|
|
|
const lineToList = normalizeToArray(path['a:lnTo'])
|
|
lineToList.forEach(lnTo => {
|
|
const pt = lnTo['a:pt']
|
|
if (pt) {
|
|
commands.push({
|
|
type: 'lineTo',
|
|
points: [{ x: parseInt(pt.attrs?.x) || 0, y: parseInt(pt.attrs?.y) || 0 }]
|
|
})
|
|
}
|
|
})
|
|
|
|
const cubicList = normalizeToArray(path['a:cubicBezTo'])
|
|
cubicList.forEach(cubic => {
|
|
const pts = normalizeToArray(cubic['a:pt'])
|
|
const points = pts.map(pt => ({
|
|
x: parseInt(pt.attrs?.x) || 0,
|
|
y: parseInt(pt.attrs?.y) || 0
|
|
}))
|
|
if (points.length === 3) {
|
|
commands.push({ type: 'cubicBezTo', points })
|
|
}
|
|
})
|
|
|
|
const arcList = normalizeToArray(path['a:arcTo'])
|
|
arcList.forEach(arc => {
|
|
commands.push({
|
|
type: 'arcTo',
|
|
wR: parseInt(arc.attrs?.wR) || 0,
|
|
hR: parseInt(arc.attrs?.hR) || 0,
|
|
stAng: parseInt(arc.attrs?.stAng) || 0,
|
|
swAng: parseInt(arc.attrs?.swAng) || 0
|
|
})
|
|
})
|
|
|
|
const quadList = normalizeToArray(path['a:quadBezTo'])
|
|
quadList.forEach(quad => {
|
|
const pts = normalizeToArray(quad['a:pt'])
|
|
const points = pts.map(pt => ({
|
|
x: parseInt(pt.attrs?.x) || 0,
|
|
y: parseInt(pt.attrs?.y) || 0
|
|
}))
|
|
commands.push({ type: 'quadBezTo', points })
|
|
})
|
|
|
|
if (path['a:close']) {
|
|
commands.push({ type: 'close' })
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
function normalizeToArray(value) {
|
|
if (!value) return []
|
|
return Array.isArray(value) ? value : [value]
|
|
}
|
|
|
|
function analyzePathCommands(commands, pathWidth, pathHeight) {
|
|
const analysis = {
|
|
lineCount: 0,
|
|
curveCount: 0,
|
|
arcCount: 0,
|
|
isClosed: false,
|
|
vertices: [],
|
|
aspectRatio: pathHeight !== 0 ? pathWidth / pathHeight : 1,
|
|
pathWidth,
|
|
pathHeight,
|
|
hasCurves: false,
|
|
isCircular: false,
|
|
commands
|
|
}
|
|
|
|
commands.forEach(cmd => {
|
|
switch (cmd.type) {
|
|
case 'moveTo':
|
|
analysis.vertices.push(cmd.points[0])
|
|
break
|
|
case 'lineTo':
|
|
analysis.lineCount++
|
|
analysis.vertices.push(cmd.points[0])
|
|
break
|
|
case 'cubicBezTo':
|
|
analysis.curveCount++
|
|
analysis.hasCurves = true
|
|
if (cmd.points.length === 3) {
|
|
analysis.vertices.push(cmd.points[2])
|
|
}
|
|
break
|
|
case 'quadBezTo':
|
|
analysis.curveCount++
|
|
analysis.hasCurves = true
|
|
if (cmd.points.length >= 2) {
|
|
analysis.vertices.push(cmd.points[cmd.points.length - 1])
|
|
}
|
|
break
|
|
case 'arcTo':
|
|
analysis.arcCount++
|
|
analysis.hasCurves = true
|
|
break
|
|
case 'close':
|
|
analysis.isClosed = true
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
|
|
if (analysis.curveCount === 4 && analysis.lineCount === 0 && analysis.isClosed) {
|
|
analysis.isCircular = checkIfCircular(commands, pathWidth, pathHeight)
|
|
}
|
|
|
|
return analysis
|
|
}
|
|
|
|
function checkIfCircular(commands, width, height) {
|
|
const bezierCommands = commands.filter(c => c.type === 'cubicBezTo')
|
|
if (bezierCommands.length !== 4) return false
|
|
|
|
const endpoints = bezierCommands.map(cmd => cmd.points[2])
|
|
|
|
const hasTop = endpoints.some(p => Math.abs(p.y) < height * 0.1)
|
|
const hasBottom = endpoints.some(p => Math.abs(p.y - height) < height * 0.1)
|
|
const hasLeft = endpoints.some(p => Math.abs(p.x) < width * 0.1)
|
|
const hasRight = endpoints.some(p => Math.abs(p.x - width) < width * 0.1)
|
|
|
|
return (hasTop || hasBottom) && (hasLeft || hasRight)
|
|
}
|
|
|
|
function matchShape(analysis) {
|
|
const {
|
|
lineCount,
|
|
curveCount,
|
|
isClosed,
|
|
vertices,
|
|
hasCurves,
|
|
isCircular,
|
|
pathWidth,
|
|
pathHeight,
|
|
} = analysis
|
|
|
|
if (isCircular) return 'ellipse'
|
|
|
|
if (analysis.arcCount >= 2 && isClosed && lineCount === 0) return 'ellipse'
|
|
|
|
if (!hasCurves && isClosed && vertices.length >= 3) return matchPolygon(vertices, pathWidth, pathHeight)
|
|
|
|
if (lineCount === 4 && curveCount === 4 && isClosed) return 'roundRect'
|
|
|
|
if (lineCount >= 3 && curveCount > 0 && curveCount <= lineCount && isClosed) {
|
|
const baseShape = matchPolygonByLineCount(lineCount)
|
|
if (baseShape !== 'custom') return baseShape === 'rectangle' ? 'roundRect' : baseShape
|
|
}
|
|
return 'custom'
|
|
}
|
|
|
|
function matchPolygon(vertices, width, height) {
|
|
const uniqueVertices = removeDuplicateVertices(vertices)
|
|
const vertexCount = uniqueVertices.length
|
|
|
|
switch (vertexCount) {
|
|
case 3:
|
|
return 'triangle'
|
|
case 4:
|
|
return matchQuadrilateral(uniqueVertices, width, height)
|
|
case 5:
|
|
return 'pentagon'
|
|
case 6:
|
|
return 'hexagon'
|
|
case 7:
|
|
return 'heptagon'
|
|
case 8:
|
|
return 'octagon'
|
|
default:
|
|
if (vertexCount > 8) {
|
|
return 'ellipse'
|
|
}
|
|
return 'custom'
|
|
}
|
|
}
|
|
|
|
function removeDuplicateVertices(vertices) {
|
|
const threshold = 100
|
|
const unique = []
|
|
|
|
vertices.forEach(v => {
|
|
const isDuplicate = unique.some(u =>
|
|
Math.abs(u.x - v.x) < threshold && Math.abs(u.y - v.y) < threshold
|
|
)
|
|
if (!isDuplicate) unique.push(v)
|
|
})
|
|
|
|
return unique
|
|
}
|
|
|
|
function matchQuadrilateral(vertices) {
|
|
if (vertices.length !== 4) return 'custom'
|
|
|
|
const edges = []
|
|
for (let i = 0; i < 4; i++) {
|
|
const p1 = vertices[i]
|
|
const p2 = vertices[(i + 1) % 4]
|
|
edges.push({
|
|
dx: p2.x - p1.x,
|
|
dy: p2.y - p1.y,
|
|
length: Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
|
|
})
|
|
}
|
|
|
|
if (isRectangle(edges)) return 'roundRect'
|
|
if (isRhombus(edges)) return 'rhombus'
|
|
if (isParallelogram(edges)) return 'parallelogram'
|
|
if (isTrapezoid(edges)) return 'trapezoid'
|
|
|
|
return 'custom'
|
|
}
|
|
|
|
function isRectangle(edges) {
|
|
const tolerance = 0.1
|
|
const edge02Similar = Math.abs(edges[0].length - edges[2].length) / Math.max(edges[0].length, edges[2].length) < tolerance
|
|
const edge13Similar = Math.abs(edges[1].length - edges[3].length) / Math.max(edges[1].length, edges[3].length) < tolerance
|
|
|
|
if (!edge02Similar || !edge13Similar) return false
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
const e1 = edges[i]
|
|
const e2 = edges[(i + 1) % 4]
|
|
const dotProduct = e1.dx * e2.dx + e1.dy * e2.dy
|
|
const cosAngle = dotProduct / (e1.length * e2.length)
|
|
if (Math.abs(cosAngle) > 0.1) return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function isRhombus(edges) {
|
|
const tolerance = 0.1
|
|
const avgLength = edges.reduce((sum, e) => sum + e.length, 0) / 4
|
|
|
|
return edges.every(e => Math.abs(e.length - avgLength) / avgLength < tolerance)
|
|
}
|
|
|
|
function isParallelogram(edges) {
|
|
const tolerance = 0.15
|
|
|
|
const slope0 = edges[0].dx !== 0 ? edges[0].dy / edges[0].dx : Infinity
|
|
const slope2 = edges[2].dx !== 0 ? edges[2].dy / edges[2].dx : Infinity
|
|
const slope1 = edges[1].dx !== 0 ? edges[1].dy / edges[1].dx : Infinity
|
|
const slope3 = edges[3].dx !== 0 ? edges[3].dy / edges[3].dx : Infinity
|
|
|
|
const parallel02 = Math.abs(slope0 - slope2) < tolerance ||
|
|
(Math.abs(slope0) > 1000 && Math.abs(slope2) > 1000)
|
|
const parallel13 = Math.abs(slope1 - slope3) < tolerance ||
|
|
(Math.abs(slope1) > 1000 && Math.abs(slope3) > 1000)
|
|
|
|
return parallel02 && parallel13
|
|
}
|
|
|
|
function isTrapezoid(edges) {
|
|
const tolerance = 0.15
|
|
|
|
const slope0 = edges[0].dx !== 0 ? edges[0].dy / edges[0].dx : Infinity
|
|
const slope2 = edges[2].dx !== 0 ? edges[2].dy / edges[2].dx : Infinity
|
|
const slope1 = edges[1].dx !== 0 ? edges[1].dy / edges[1].dx : Infinity
|
|
const slope3 = edges[3].dx !== 0 ? edges[3].dy / edges[3].dx : Infinity
|
|
|
|
const parallel02 = Math.abs(slope0 - slope2) < tolerance ||
|
|
(Math.abs(slope0) > 1000 && Math.abs(slope2) > 1000)
|
|
const parallel13 = Math.abs(slope1 - slope3) < tolerance ||
|
|
(Math.abs(slope1) > 1000 && Math.abs(slope3) > 1000)
|
|
|
|
return (parallel02 && !parallel13) || (!parallel02 && parallel13)
|
|
}
|
|
|
|
function matchPolygonByLineCount(lineCount) {
|
|
switch (lineCount) {
|
|
case 3: return 'triangle'
|
|
case 4: return 'rectangle'
|
|
case 5: return 'pentagon'
|
|
case 6: return 'hexagon'
|
|
case 7: return 'heptagon'
|
|
case 8: return 'octagon'
|
|
default: return 'custom'
|
|
}
|
|
} |