import { createLogger } from '@sim/logger' const logger = createLogger('PptxShapePresets') /** * Preset shape SVG path generators for OOXML preset geometry types. * * Each generator takes width, height, and optional adjustment values, * returning an SVG path d-attribute string. * * Adjustment values follow OOXML convention: values are in 100000ths * (so 50000 = 50%). */ import { shapeArc } from './shape-arc' type PresetShapeGenerator = (w: number, h: number, adjustments?: Map) => string /** Helper: get adjustment value or default, converting from 100000ths to fraction. */ function adj( adjustments: Map | undefined, name: string, defaultVal: number ): number { const raw = adjustments?.get(name) ?? defaultVal return raw / 100000 } /** Helper: generate a regular polygon path (inscribed in bounding box). */ function _regularPolygon(w: number, h: number, sides: number): string { const cx = w / 2 const cy = h / 2 const rx = w / 2 const ry = h / 2 const parts: string[] = [] for (let i = 0; i < sides; i++) { // Start from top center (-90 degrees) const angle = (2 * Math.PI * i) / sides - Math.PI / 2 const x = cx + rx * Math.cos(angle) const y = cy + ry * Math.sin(angle) parts.push(i === 0 ? `M${x},${y}` : `L${x},${y}`) } parts.push('Z') return parts.join(' ') } /** Raw adj helper: get adjustment value without dividing by 100000. */ function adjRaw( adjustments: Map | undefined, name: string, defaultVal: number ): number { return adjustments?.get(name) ?? defaultVal } /** Helper: generate a star polygon. */ function starShape(w: number, h: number, points: number, innerRatio = 0.4): string { const cx = w / 2 const cy = h / 2 const outerRx = w / 2 const outerRy = h / 2 const innerRx = outerRx * innerRatio const innerRy = outerRy * innerRatio const totalPoints = points * 2 const parts: string[] = [] for (let i = 0; i < totalPoints; i++) { const angle = (2 * Math.PI * i) / totalPoints - Math.PI / 2 const isOuter = i % 2 === 0 const rx = isOuter ? outerRx : innerRx const ry = isOuter ? outerRy : innerRy const x = cx + rx * Math.cos(angle) const y = cy + ry * Math.sin(angle) parts.push(i === 0 ? `M${x},${y}` : `L${x},${y}`) } parts.push('Z') return parts.join(' ') } /** * Mirror an absolute SVG path horizontally across the given width. * Supports the command subset used by preset arrow shapes: M, L, A, Z. */ function mirrorAbsolutePathHorizontally(path: string, width: number): string { const tokens = path.match(/[MLAZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) if (!tokens) return path const out: string[] = [] let i = 0 while (i < tokens.length) { const cmd = tokens[i++] if (!cmd) break out.push(cmd) if (cmd === 'Z') continue if (cmd === 'M' || cmd === 'L') { const x = Number(tokens[i++]) const y = Number(tokens[i++]) out.push(String(width - x), String(y)) continue } if (cmd === 'A') { const rx = tokens[i++] const ry = tokens[i++] const rot = tokens[i++] const largeArc = tokens[i++] const sweep = Number(tokens[i++]) const x = Number(tokens[i++]) const y = Number(tokens[i++]) out.push(rx, ry, rot, largeArc, String(sweep ? 0 : 1), String(width - x), String(y)) continue } return path } return out.join(' ') } function mirrorAbsolutePathVertically(path: string, height: number): string { const tokens = path.match(/[MLAZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) if (!tokens) return path const out: string[] = [] let i = 0 while (i < tokens.length) { const cmd = tokens[i++] if (!cmd) break out.push(cmd) if (cmd === 'Z') continue if (cmd === 'M' || cmd === 'L') { const x = Number(tokens[i++]) const y = Number(tokens[i++]) out.push(String(x), String(height - y)) continue } if (cmd === 'A') { const rx = tokens[i++] const ry = tokens[i++] const rot = tokens[i++] const largeArc = tokens[i++] const sweep = Number(tokens[i++]) const x = Number(tokens[i++]) const y = Number(tokens[i++]) out.push(rx, ry, rot, largeArc, String(sweep ? 0 : 1), String(x), String(height - y)) } } return out.join(' ') } // --------------------------------------------------------------------------- // Preset shape registry // --------------------------------------------------------------------------- export const presetShapes: Map = new Map() // ===== Basic Shapes ===== presetShapes.set('rect', (w, h) => `M0,0 L${w},0 L${w},${h} L0,${h} Z`) presetShapes.set('roundRect', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 16667) const r = Math.min(w, h) * a return [ `M${r},0`, `L${w - r},0`, `A${r},${r} 0 0,1 ${w},${r}`, `L${w},${h - r}`, `A${r},${r} 0 0,1 ${w - r},${h}`, `L${r},${h}`, `A${r},${r} 0 0,1 0,${h - r}`, `L0,${r}`, `A${r},${r} 0 0,1 ${r},0`, 'Z', ].join(' ') }) presetShapes.set('plaque', (w, h, adjustments) => { // OOXML: adj default 16667, concave (inward) arc corners via negative sweep arcTo const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 16667), 0), 50000) const x1 = (Math.min(w, h) * a) / 100000 const x2 = w - x1 const y2 = h - x1 // Start at (0, x1), arcTo with negative sweep creates concave corner const a1 = ooArcTo(0, x1, x1, x1, 90, -90) // top-left: ends at (x1, 0) const a2 = ooArcTo(x2, 0, x1, x1, 180, -90) // top-right: ends at (w, x1) const a3 = ooArcTo(w, y2, x1, x1, 270, -90) // bottom-right: ends at (x2, h) const a4 = ooArcTo(x1, h, x1, x1, 0, -90) // bottom-left: ends at (0, y2) -> close to (0, x1) return [ `M0,${x1}`, a1.svg, `L${x2},0`, a2.svg, `L${w},${y2}`, a3.svg, `L${x1},${h}`, a4.svg, 'Z', ].join(' ') }) // Tab family: OOXML uses dx = sqrt(w²+h²)/20 (diagonal/20) presetShapes.set('cornerTabs', (w, h) => { const dx = Math.sqrt(w * w + h * h) / 20 return [ `M0,0 L${dx},0 L0,${dx} Z`, `M${w},0 L${w - dx},0 L${w},${dx} Z`, `M${w},${h} L${w - dx},${h} L${w},${h - dx} Z`, `M0,${h} L${dx},${h} L0,${h - dx} Z`, ].join(' ') }) presetShapes.set('squareTabs', (w, h) => { const dx = Math.sqrt(w * w + h * h) / 20 return [ `M0,0 L${dx},0 L${dx},${dx} L0,${dx} Z`, `M${w - dx},0 L${w},0 L${w},${dx} L${w - dx},${dx} Z`, `M0,${h - dx} L${dx},${h - dx} L${dx},${h} L0,${h} Z`, `M${w - dx},${h - dx} L${w},${h - dx} L${w},${h} L${w - dx},${h} Z`, ].join(' ') }) presetShapes.set('plaqueTabs', (w, h) => { const dx = Math.sqrt(w * w + h * h) / 20 return [ `M0,0 L${dx},0 A${dx},${dx} 0 0,1 0,${dx} Z`, `M${w},0 L${w - dx},0 A${dx},${dx} 0 0,0 ${w},${dx} Z`, `M0,${h} L0,${h - dx} A${dx},${dx} 0 0,1 ${dx},${h} Z`, `M${w},${h} L${w - dx},${h} A${dx},${dx} 0 0,1 ${w},${h - dx} Z`, ].join(' ') }) presetShapes.set('ellipse', (w, h) => { const rx = w / 2 const ry = h / 2 return [`M${w},${ry}`, `A${rx},${ry} 0 1,1 0,${ry}`, `A${rx},${ry} 0 1,1 ${w},${ry}`, 'Z'].join( ' ' ) }) presetShapes.set('triangle', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 50000) const topX = w * a return `M${topX},0 L${w},${h} L0,${h} Z` }) presetShapes.set('isosTriangle', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 50000) const topX = w * a return `M${topX},0 L${w},${h} L0,${h} Z` }) presetShapes.set('rtTriangle', (w, h) => `M0,0 L${w},${h} L0,${h} Z`) presetShapes.set('diamond', (w, h) => { const cx = w / 2 const cy = h / 2 return `M${cx},0 L${w},${cy} L${cx},${h} L0,${cy} Z` }) presetShapes.set('pentagon', (w, h) => { // OOXML pentagon: hf=105146, vf=110557 with center shifted to svc so top vertex = y=0. const hc = w / 2 const swd2 = (hc * 105146) / 100000 const shd2 = ((h / 2) * 110557) / 100000 const svc = shd2 // svc = vc * vf/100000 = shd2, so top vertex at svc - shd2 = 0 const dx1 = swd2 * Math.cos((18 * Math.PI) / 180) // cos 1080000 const dx2 = swd2 * Math.cos((54 * Math.PI) / 180) // cos 18360000 const dy1 = shd2 * Math.sin((18 * Math.PI) / 180) // sin 1080000 const dy2 = shd2 * Math.sin((54 * Math.PI) / 180) // |sin 18360000| return [ `M${hc - dx1},${svc - dy1}`, // x1, y1 (upper-left) `L${hc},0`, // hc, t (top) `L${hc + dx1},${svc - dy1}`, // x4, y1 (upper-right) `L${hc + dx2},${svc + dy2}`, // x3, y2 (lower-right) `L${hc - dx2},${svc + dy2}`, // x2, y2 (lower-left) 'Z', ].join(' ') }) presetShapes.set('hexagon', (w, h, adjustments) => { // OOXML hexagon: adj=25000, vf=115470 (2/√3 scale factor for regular hex). const ss = Math.min(w, h) const a = Math.min( Math.max(adjRaw(adjustments, 'adj', 25000), 0), ss > 0 ? (50000 * w) / ss : 50000 ) const vf = 115470 const shd2 = ((h / 2) * vf) / 100000 const x1 = (ss * a) / 100000 const x2 = w - x1 const _hc = w / 2 const vc = h / 2 // dy1 = sin(shd2, 60°) = shd2 * sin(60°) const dy1 = shd2 * Math.sin((60 * Math.PI) / 180) const y1 = vc - dy1 const y2 = vc + dy1 return [ `M0,${vc}`, `L${x1},${y1}`, `L${x2},${y1}`, `L${w},${vc}`, `L${x2},${y2}`, `L${x1},${y2}`, 'Z', ].join(' ') }) presetShapes.set('octagon', (w, h, adjustments) => { // OOXML octagon: adj=29289 (≈1-1/√2). Uses ss-based cuts for both x and y. const ss = Math.min(w, h) const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 29289), 0), 50000) const x1 = (ss * a) / 100000 const x2 = w - x1 const y2 = h - x1 return [ `M0,${x1}`, `L${x1},0`, `L${x2},0`, `L${w},${x1}`, `L${w},${y2}`, `L${x2},${h}`, `L${x1},${h}`, `L0,${y2}`, 'Z', ].join(' ') }) presetShapes.set('heptagon', (w, h) => { // OOXML heptagon: hf=102572, vf=105210 with shifted center. const hc = w / 2 const swd2 = (hc * 102572) / 100000 const shd2 = ((h / 2) * 105210) / 100000 const svc = ((h / 2) * 105210) / 100000 // Pre-computed trig ratios from OOXML spec (scaled by 100000) const dx1 = (swd2 * 97493) / 100000 // cos(12.857°) ≈ sin(77.14°) const dx2 = (swd2 * 78183) / 100000 // cos(38.57°) const dx3 = (swd2 * 43388) / 100000 // cos(64.29°) const dy1 = (shd2 * 62349) / 100000 // sin(38.57°) const dy2 = (shd2 * 22252) / 100000 // sin(12.857°) const dy3 = (shd2 * 90097) / 100000 // sin(64.29°) return [ `M${hc - dx1},${svc + dy2}`, // x1, y2 (left) `L${hc - dx2},${svc - dy1}`, // x2, y1 (upper-left) `L${hc},0`, // hc, t (top: svc - shd2 = 0) `L${hc + dx2},${svc - dy1}`, // x5, y1 (upper-right) `L${hc + dx1},${svc + dy2}`, // x6, y2 (right) `L${hc + dx3},${svc + dy3}`, // x4, y3 (lower-right) `L${hc - dx3},${svc + dy3}`, // x3, y3 (lower-left) 'Z', ].join(' ') }) presetShapes.set('decagon', (w, h) => { // OOXML decagon: vf=105146 (no hf, uses wd2 for x). 10 vertices starting from left. const hc = w / 2 const vc = h / 2 const shd2 = (vc * 105146) / 100000 // OOXML angles: 2160000=36°, 4320000=72° const dx1 = hc * Math.cos((36 * Math.PI) / 180) // cos(wd2, 2160000) const dx2 = hc * Math.cos((72 * Math.PI) / 180) // cos(wd2, 4320000) const dy1 = shd2 * Math.sin((72 * Math.PI) / 180) // sin(shd2, 4320000) const dy2 = shd2 * Math.sin((36 * Math.PI) / 180) // sin(shd2, 2160000) return [ `M0,${vc}`, // l, vc `L${hc - dx1},${vc - dy2}`, // x1, y2 `L${hc - dx2},${vc - dy1}`, // x2, y1 `L${hc + dx2},${vc - dy1}`, // x3, y1 `L${hc + dx1},${vc - dy2}`, // x4, y2 `L${w},${vc}`, // r, vc `L${hc + dx1},${vc + dy2}`, // x4, y3 `L${hc + dx2},${vc + dy1}`, // x3, y4 `L${hc - dx2},${vc + dy1}`, // x2, y4 `L${hc - dx1},${vc + dy2}`, // x1, y3 'Z', ].join(' ') }) presetShapes.set('dodecagon', (w, h) => { // OOXML dodecagon: 21600-unit coordinate space, simple ratios. const x1 = (w * 2894) / 21600 const x2 = (w * 7906) / 21600 const x3 = (w * 13694) / 21600 const x4 = (w * 18706) / 21600 const y1 = (h * 2894) / 21600 const y2 = (h * 7906) / 21600 const y3 = (h * 13694) / 21600 const y4 = (h * 18706) / 21600 return [ `M0,${y2}`, `L${x1},${y1}`, `L${x2},0`, `L${x3},0`, `L${x4},${y1}`, `L${w},${y2}`, `L${w},${y3}`, `L${x4},${y4}`, `L${x3},${h}`, `L${x2},${h}`, `L${x1},${y4}`, `L0,${y3}`, 'Z', ].join(' ') }) presetShapes.set('parallelogram', (w, h, adjustments) => { // OOXML: adj=25000, x2 = ss * a / 100000, path: M(l,b)→L(x2,t)→L(r,t)→L(r-x2,b)→Z const ss = Math.min(w, h) const maxAdj = ss > 0 ? (100000 * w) / ss : 100000 const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 25000), 0), maxAdj) const x2 = (ss * a) / 100000 const x5 = w - x2 return `M0,${h} L${x2},0 L${w},0 L${x5},${h} Z` }) presetShapes.set('trapezoid', (w, h, adjustments) => { // OOXML: adj=25000, x2 = ss * a / 100000, x3 = r - x2 const ss = Math.min(w, h) const maxAdj = ss > 0 ? (50000 * w) / ss : 50000 const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 25000), 0), maxAdj) const x2 = (ss * a) / 100000 const x3 = w - x2 return `M0,${h} L${x2},0 L${x3},0 L${w},${h} Z` }) presetShapes.set('nonIsoscelesTrapezoid', (w, h, adjustments) => { // OOXML: Two independent top insets. adj1=25000, adj2=25000 const ss = Math.min(w, h) const maxAdj = ss > 0 ? (50000 * w) / ss : 50000 const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 25000), 0), maxAdj) const a2 = Math.min(Math.max(adjRaw(adjustments, 'adj2', 25000), 0), maxAdj) const x2 = (ss * a1) / 100000 const dx3 = (ss * a2) / 100000 const x3 = w - dx3 return `M0,${h} L${x2},0 L${x3},0 L${w},${h} Z` }) presetShapes.set('corner', (w, h, adjustments) => { // OOXML corner: two adjustments control horizontal and vertical arm thickness. // adj1 (default 50000) → vertical arm height from bottom: dy1 = ss * a1, y1 = h - dy1 // adj2 (default 50000) → horizontal arm width from left: x1 = ss * a2 const ss = Math.min(w, h) const a1 = Math.min(Math.max(adj(adjustments, 'adj1', 50000), 0), 1) const a2 = Math.min(Math.max(adj(adjustments, 'adj2', 50000), 0), 1) const x1 = ss * a2 const dy1 = ss * a1 const y1 = h - dy1 return [`M0,0`, `L${x1},0`, `L${x1},${y1}`, `L${w},${y1}`, `L${w},${h}`, `L0,${h}`, 'Z'].join(' ') }) presetShapes.set('diagStripe', (w, h, adjustments) => { const a = Math.min(Math.max(adj(adjustments, 'adj', 50000), 0), 1) const x2 = w * a const y2 = h * a return [`M0,${y2}`, `L${x2},0`, `L${w},0`, `L0,${h}`, 'Z'].join(' ') }) // ===== Star Shapes ===== presetShapes.set('star4', (w, h, adjustments) => { // OOXML default adj=12500 → innerRatio = 12500/50000 = 0.25 const a = adj(adjustments, 'adj', 12500) * 2 return starShape(w, h, 4, Math.min(Math.max(a, 0), 1)) }) presetShapes.set('star5', (w, h, adjustments) => { // OOXML: adj=19098, hf=105146, vf=110557 — scaling factors for non-square bounding box const aRaw = adjustments?.get('adj') ?? 19098 const a = Math.min(Math.max(aRaw, 0), 50000) const hf = 105146 const vf = 110557 const swd2 = ((w / 2) * hf) / 100000 const shd2 = ((h / 2) * vf) / 100000 const svc = ((h / 2) * vf) / 100000 const iwd2 = (swd2 * a) / 50000 const ihd2 = (shd2 * a) / 50000 const cx = w / 2 const step = (2 * Math.PI) / 5 const halfStep = step / 2 const startAngle = -Math.PI / 2 const parts: string[] = [] for (let i = 0; i < 5; i++) { const outerAngle = startAngle + step * i const innerAngle = outerAngle + halfStep const ox = cx + swd2 * Math.cos(outerAngle) const oy = svc + shd2 * Math.sin(outerAngle) const ix = cx + iwd2 * Math.cos(innerAngle) const iy = svc + ihd2 * Math.sin(innerAngle) parts.push(i === 0 ? `M${ox},${oy}` : `L${ox},${oy}`) parts.push(`L${ix},${iy}`) } parts.push('Z') return parts.join(' ') }) presetShapes.set('star6', (w, h, adjustments) => { // OOXML: adj=28868, hf=115470 — horizontal scaling factor const aRaw = adjustments?.get('adj') ?? 28868 const a = Math.min(Math.max(aRaw, 0), 50000) const hf = 115470 const swd2 = ((w / 2) * hf) / 100000 const shd2 = h / 2 // no vf for star6 const iwd2 = (swd2 * a) / 50000 const ihd2 = (shd2 * a) / 50000 const cx = w / 2 const cy = h / 2 const step = (2 * Math.PI) / 6 const halfStep = step / 2 const startAngle = -Math.PI / 2 const parts: string[] = [] for (let i = 0; i < 6; i++) { const outerAngle = startAngle + step * i const innerAngle = outerAngle + halfStep const ox = cx + swd2 * Math.cos(outerAngle) const oy = cy + shd2 * Math.sin(outerAngle) const ix = cx + iwd2 * Math.cos(innerAngle) const iy = cy + ihd2 * Math.sin(innerAngle) parts.push(i === 0 ? `M${ox},${oy}` : `L${ox},${oy}`) parts.push(`L${ix},${iy}`) } parts.push('Z') return parts.join(' ') }) presetShapes.set('star7', (w, h, adjustments) => { // OOXML star7: adj=34601, hf=102572, vf=105210 — center shifted to svc const aRaw = adjustments?.get('adj') ?? 34601 const a = Math.min(Math.max(aRaw, 0), 50000) const swd2 = ((w / 2) * 102572) / 100000 const shd2 = ((h / 2) * 105210) / 100000 const svc = shd2 // = vc * vf/100000 so top vertex at svc - shd2 = 0 const iwd2 = (swd2 * a) / 50000 const ihd2 = (shd2 * a) / 50000 const cx = w / 2 const step = (2 * Math.PI) / 7 const halfStep = step / 2 const startAngle = -Math.PI / 2 const parts: string[] = [] for (let i = 0; i < 7; i++) { const outerAngle = startAngle + step * i const innerAngle = outerAngle + halfStep const ox = cx + swd2 * Math.cos(outerAngle) const oy = svc + shd2 * Math.sin(outerAngle) const ix = cx + iwd2 * Math.cos(innerAngle) const iy = svc + ihd2 * Math.sin(innerAngle) parts.push(i === 0 ? `M${ox},${oy}` : `L${ox},${oy}`) parts.push(`L${ix},${iy}`) } parts.push('Z') return parts.join(' ') }) presetShapes.set('star8', (w, h, adjustments) => { // OOXML: iwd2 = wd2 * adj / 50000. adj default=37500 → innerRatio = 37500/50000 = 0.75 // adj() divides by 100000, so we multiply by 2 to get adj/50000. const a = adj(adjustments, 'adj', 37500) * 2 return starShape(w, h, 8, Math.min(Math.max(a, 0), 1)) }) presetShapes.set('star10', (w, h, adjustments) => { // OOXML: adj=42533, hf=105146 — horizontal scaling factor const aRaw = adjustments?.get('adj') ?? 42533 const a = Math.min(Math.max(aRaw, 0), 50000) const hf = 105146 const swd2 = ((w / 2) * hf) / 100000 const shd2 = h / 2 // no vf for star10 const iwd2 = (swd2 * a) / 50000 const ihd2 = (shd2 * a) / 50000 const cx = w / 2 const cy = h / 2 const step = (2 * Math.PI) / 10 const halfStep = step / 2 const startAngle = -Math.PI / 2 const parts: string[] = [] for (let i = 0; i < 10; i++) { const outerAngle = startAngle + step * i const innerAngle = outerAngle + halfStep const ox = cx + swd2 * Math.cos(outerAngle) const oy = cy + shd2 * Math.sin(outerAngle) const ix = cx + iwd2 * Math.cos(innerAngle) const iy = cy + ihd2 * Math.sin(innerAngle) parts.push(i === 0 ? `M${ox},${oy}` : `L${ox},${oy}`) parts.push(`L${ix},${iy}`) } parts.push('Z') return parts.join(' ') }) presetShapes.set('star12', (w, h, adjustments) => { // OOXML default adj=37500 → innerRatio = 0.75 const a = adj(adjustments, 'adj', 37500) * 2 return starShape(w, h, 12, Math.min(Math.max(a, 0), 1)) }) presetShapes.set('star16', (w, h, adjustments) => { // OOXML default adj=37500 → innerRatio = 0.75 const a = adj(adjustments, 'adj', 37500) * 2 return starShape(w, h, 16, Math.min(Math.max(a, 0), 1)) }) presetShapes.set('star24', (w, h, adjustments) => { // OOXML default adj=37500 → innerRatio = 0.75 const a = adj(adjustments, 'adj', 37500) * 2 return starShape(w, h, 24, Math.min(Math.max(a, 0), 1)) }) presetShapes.set('star32', (w, h, adjustments) => { // OOXML default adj=37500 → innerRatio = 0.75 const a = adj(adjustments, 'adj', 37500) * 2 return starShape(w, h, 32, Math.min(Math.max(a, 0), 1)) }) // ===== Lines & Connectors ===== // OOXML line: diagonal (0,0→w,h) when both extents are non-zero. // Keep explicit horizontal/vertical handling for zero-extent cases so 1px SVGs remain visible. presetShapes.set('line', (w, h) => { const safeH = h || 1 const safeW = w || 1 if (w === 0) return `M0.5,0 L0.5,${safeH}` if (h === 0) return `M0,0.5 L${safeW},0.5` return `M0,0 L${w},${h}` }) // Inverse diagonal line (top-right to bottom-left). presetShapes.set('lineInv', (w, h) => { const safeH = h || 1 const safeW = w || 1 if (w === 0) return `M0.5,0 L0.5,${safeH}` if (h === 0) return `M0,0.5 L${safeW},0.5` return `M${w},0 L0,${h}` }) // When one dimension is 0, draw horizontal or vertical line (same as 'line') so gradient and stroke are correct presetShapes.set('straightConnector1', (w, h) => { const safeH = h || 1 const safeW = w || 1 if (w === 0) return `M0.5,0 L0.5,${safeH}` if (h === 0) return `M0,0.5 L${safeW},0.5` return `M0,0 L${w},${h}` }) presetShapes.set('bentConnector2', (w, h) => `M0,0 L${w},0 L${w},${h}`) presetShapes.set('bentConnector3', (w, h, adjustments) => { const a = adj(adjustments, 'adj1', 50000) const midX = w * a return `M0,0 L${midX},0 L${midX},${h} L${w},${h}` }) presetShapes.set('bentConnector4', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) const a2 = adj(adjustments, 'adj2', 50000) const midX = w * a1 const midY = h * a2 return `M0,0 L${midX},0 L${midX},${midY} L${w},${midY} L${w},${h}` }) presetShapes.set('curvedConnector2', (w, h) => { return `M0,0 C${w},0 0,${h} ${w},${h}` }) presetShapes.set('curvedConnector3', (w, h, adjustments) => { // OOXML: two cubic Bezier segments joined at midpoint (x2, vc) const x2 = w * adj(adjustments, 'adj1', 50000) const x1 = x2 / 2 // +/ l x2 2 const x3 = (w + x2) / 2 // +/ r x2 2 const vc = h / 2 const hd4 = h / 4 const y3 = (h * 3) / 4 return `M0,0 C${x1},0 ${x2},${hd4} ${x2},${vc} C${x2},${y3} ${x3},${h} ${w},${h}` }) presetShapes.set('curvedConnector4', (w, h, adjustments) => { // OOXML: three cubic Bezier segments const x2 = w * adj(adjustments, 'adj1', 50000) const y4 = h * adj(adjustments, 'adj2', 50000) const x1 = x2 / 2 // +/ l x2 2 const x3 = (w + x2) / 2 // +/ r x2 2 const x4 = (x2 + x3) / 2 // +/ x2 x3 2 const x5 = (x3 + w) / 2 // +/ x3 r 2 const y1 = y4 / 2 // +/ t y4 2 const y2 = y1 / 2 // +/ t y1 2 const y3 = (y1 + y4) / 2 // +/ y1 y4 2 const y5 = (h + y4) / 2 // +/ b y4 2 return [ `M0,0`, `C${x1},0 ${x2},${y2} ${x2},${y1}`, `C${x2},${y3} ${x4},${y4} ${x3},${y4}`, `C${x5},${y4} ${w},${y5} ${w},${h}`, ].join(' ') }) presetShapes.set('curvedConnector5', (w, h, adjustments) => { // OOXML: four cubic Bezier segments const x3 = w * adj(adjustments, 'adj1', 50000) const y4 = h * adj(adjustments, 'adj2', 50000) const x6 = w * adj(adjustments, 'adj3', 50000) const x1 = (x3 + x6) / 2 // +/ x3 x6 2 const x2 = x3 / 2 // +/ l x3 2 const x4 = (x3 + x1) / 2 // +/ x3 x1 2 const x5 = (x6 + x1) / 2 // +/ x6 x1 2 const x7 = (x6 + w) / 2 // +/ x6 r 2 const y1 = y4 / 2 // +/ t y4 2 const y2 = y1 / 2 // +/ t y1 2 const y3 = (y1 + y4) / 2 // +/ y1 y4 2 const y5 = (h + y4) / 2 // +/ b y4 2 const y6 = (y5 + y4) / 2 // +/ y5 y4 2 const y7 = (y5 + h) / 2 // +/ y5 b 2 return [ `M0,0`, `C${x2},0 ${x3},${y2} ${x3},${y1}`, `C${x3},${y3} ${x4},${y4} ${x1},${y4}`, `C${x5},${y4} ${x6},${y6} ${x6},${y5}`, `C${x6},${y7} ${x7},${h} ${w},${h}`, ].join(' ') }) presetShapes.set('bentConnector5', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) const a2 = adj(adjustments, 'adj2', 50000) const a3 = adj(adjustments, 'adj3', 50000) const x1 = w * a1 const y1 = h * a2 const x2 = w * a3 return `M0,0 L${x1},0 L${x1},${y1} L${x2},${y1} L${x2},${h} L${w},${h}` }) // ===== Arrow Shapes ===== presetShapes.set('rightArrow', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) // shaft width ratio const a2 = adj(adjustments, 'adj2', 50000) // head length ratio const ss = Math.min(w, h) // OOXML uses short side for head length const shaftHalfH = (h * a1) / 2 const headLen = ss * a2 const cy = h / 2 const shaftEnd = w - headLen return [ `M0,${cy - shaftHalfH}`, `L${shaftEnd},${cy - shaftHalfH}`, `L${shaftEnd},0`, `L${w},${cy}`, `L${shaftEnd},${h}`, `L${shaftEnd},${cy + shaftHalfH}`, `L0,${cy + shaftHalfH}`, 'Z', ].join(' ') }) presetShapes.set('leftArrow', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) const a2 = adj(adjustments, 'adj2', 50000) const ss = Math.min(w, h) const shaftHalfH = (h * a1) / 2 const headLen = ss * a2 const cy = h / 2 return [ `M${w},${cy - shaftHalfH}`, `L${headLen},${cy - shaftHalfH}`, `L${headLen},0`, `L0,${cy}`, `L${headLen},${h}`, `L${headLen},${cy + shaftHalfH}`, `L${w},${cy + shaftHalfH}`, 'Z', ].join(' ') }) presetShapes.set('upArrow', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) const a2 = adj(adjustments, 'adj2', 50000) const shaftHalfW = (w * a1) / 2 const headLen = h * a2 const cx = w / 2 return [ `M${cx - shaftHalfW},${h}`, `L${cx - shaftHalfW},${headLen}`, `L0,${headLen}`, `L${cx},0`, `L${w},${headLen}`, `L${cx + shaftHalfW},${headLen}`, `L${cx + shaftHalfW},${h}`, 'Z', ].join(' ') }) presetShapes.set('downArrow', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) const a2 = adj(adjustments, 'adj2', 50000) const shaftHalfW = (w * a1) / 2 const headLen = h * a2 const cx = w / 2 const shaftEnd = h - headLen return [ `M${cx - shaftHalfW},0`, `L${cx + shaftHalfW},0`, `L${cx + shaftHalfW},${shaftEnd}`, `L${w},${shaftEnd}`, `L${cx},${h}`, `L0,${shaftEnd}`, `L${cx - shaftHalfW},${shaftEnd}`, 'Z', ].join(' ') }) presetShapes.set('downArrowCallout', (w, h, adjustments) => { // ECMA-like callout geometry (4 adjustments). const adj1 = adjustments?.get('adj1') ?? 25000 const adj2 = adjustments?.get('adj2') ?? 25000 const adj3 = adjustments?.get('adj3') ?? 25000 const adj4 = adjustments?.get('adj4') ?? 64977 const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2, (50000 * w) / Math.max(ss, 1))) const a1 = Math.max(0, Math.min(adj1, a2 * 2)) const a3 = Math.max(0, Math.min(adj3, (100000 * h) / Math.max(ss, 1))) const q2 = (a3 * ss) / Math.max(h, 1) const a4 = Math.max(0, Math.min(adj4, 100000 - q2)) const hc = w / 2 const dx1 = (ss * a2) / 100000 const dx2 = (ss * a1) / 200000 const x1 = hc - dx1 const x2 = hc - dx2 const x3 = hc + dx2 const x4 = hc + dx1 const y3 = h - (ss * a3) / 100000 const y2 = (h * a4) / 100000 return [ `M0,0`, `L${w},0`, `L${w},${y2}`, `L${x3},${y2}`, `L${x3},${y3}`, `L${x4},${y3}`, `L${hc},${h}`, `L${x1},${y3}`, `L${x2},${y3}`, `L${x2},${y2}`, `L0,${y2}`, 'Z', ].join(' ') }) presetShapes.set('rightArrowCallout', (w, h, adjustments) => { // OOXML: Rectangle body + right-pointing arrowhead (11-point polygon, 4 adj) const ss = Math.min(w, h) const maxAdj2 = (50000 * h) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, maxAdj2)) const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, a2 * 2)) const maxAdj3 = (100000 * w) / Math.max(ss, 1) const a3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, maxAdj3)) const q2 = (a3 * ss) / Math.max(w, 1) const a4 = Math.max(0, Math.min(adjustments?.get('adj4') ?? 64977, 100000 - q2)) const vc = h / 2 const dy1 = (ss * a2) / 100000 const dy2 = (ss * a1) / 200000 const y1 = vc - dy1 const y2 = vc - dy2 const y3 = vc + dy2 const y4 = vc + dy1 const dx3 = (ss * a3) / 100000 const x3 = w - dx3 const x2 = (w * a4) / 100000 return [ `M0,0`, `L${x2},0`, `L${x2},${y2}`, `L${x3},${y2}`, `L${x3},${y1}`, `L${w},${vc}`, `L${x3},${y4}`, `L${x3},${y3}`, `L${x2},${y3}`, `L${x2},${h}`, `L0,${h}`, 'Z', ].join(' ') }) presetShapes.set('leftArrowCallout', (w, h, adjustments) => { // OOXML: Mirror of rightArrowCallout — arrowhead points left const ss = Math.min(w, h) const maxAdj2 = (50000 * h) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, maxAdj2)) const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, a2 * 2)) const maxAdj3 = (100000 * w) / Math.max(ss, 1) const a3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, maxAdj3)) const q2 = (a3 * ss) / Math.max(w, 1) const a4 = Math.max(0, Math.min(adjustments?.get('adj4') ?? 64977, 100000 - q2)) const vc = h / 2 const dy1 = (ss * a2) / 100000 const dy2 = (ss * a1) / 200000 const y1 = vc - dy1 const y2 = vc - dy2 const y3 = vc + dy2 const y4 = vc + dy1 const x1 = (ss * a3) / 100000 const dx2 = (w * a4) / 100000 const x2 = w - dx2 return [ `M0,${vc}`, `L${x1},${y1}`, `L${x1},${y2}`, `L${x2},${y2}`, `L${x2},0`, `L${w},0`, `L${w},${h}`, `L${x2},${h}`, `L${x2},${y3}`, `L${x1},${y3}`, `L${x1},${y4}`, 'Z', ].join(' ') }) presetShapes.set('upArrowCallout', (w, h, adjustments) => { // OOXML: Vertical variant — arrowhead points up const ss = Math.min(w, h) const maxAdj2 = (50000 * w) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, maxAdj2)) const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, a2 * 2)) const maxAdj3 = (100000 * h) / Math.max(ss, 1) const a3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, maxAdj3)) const q2 = (a3 * ss) / Math.max(h, 1) const a4 = Math.max(0, Math.min(adjustments?.get('adj4') ?? 64977, 100000 - q2)) const hc = w / 2 const dx1 = (ss * a2) / 100000 const dx2 = (ss * a1) / 200000 const x1 = hc - dx1 const x2 = hc - dx2 const x3 = hc + dx2 const x4 = hc + dx1 const y1 = (ss * a3) / 100000 const dy2 = (h * a4) / 100000 const y2 = h - dy2 return [ `M0,${y2}`, `L${x2},${y2}`, `L${x2},${y1}`, `L${x1},${y1}`, `L${hc},0`, `L${x4},${y1}`, `L${x3},${y1}`, `L${x3},${y2}`, `L${w},${y2}`, `L${w},${h}`, `L0,${h}`, 'Z', ].join(' ') }) presetShapes.set('upDownArrowCallout', (w, h, adjustments) => { // OOXML spec: 4 adjustments const adj1Raw = adjustments?.get('adj1') ?? 25000 const adj2Raw = adjustments?.get('adj2') ?? 25000 const adj3Raw = adjustments?.get('adj3') ?? 25000 const adj4Raw = adjustments?.get('adj4') ?? 48123 const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2Raw, (50000 * w) / Math.max(ss, 1))) const a1 = Math.max(0, Math.min(adj1Raw, a2 * 2)) const a3 = Math.max(0, Math.min(adj3Raw, (50000 * h) / Math.max(ss, 1))) const q2 = (a3 * ss) / Math.max(h, 1) const a4 = Math.max(0, Math.min(adj4Raw, 100000 - q2 - q2)) const dx1 = (ss * a2) / 100000 const dx2 = (ss * a1) / 200000 const hc = w / 2 const x1 = hc - dx1 const x2 = hc - dx2 const x3 = hc + dx2 const x4 = hc + dx1 const y1 = (ss * a3) / 100000 const dy2 = (h * a4) / 200000 const y2 = h / 2 - dy2 const y3 = h / 2 + dy2 const y4 = h - y1 return [ `M${hc},0`, `L${x4},${y1}`, `L${x3},${y1}`, `L${x3},${y2}`, `L${w},${y2}`, `L${w},${y3}`, `L${x3},${y3}`, `L${x3},${y4}`, `L${x4},${y4}`, `L${hc},${h}`, `L${x1},${y4}`, `L${x2},${y4}`, `L${x2},${y3}`, `L0,${y3}`, `L0,${y2}`, `L${x2},${y2}`, `L${x2},${y1}`, `L${x1},${y1}`, 'Z', ].join(' ') }) presetShapes.set('leftRightArrowCallout', (w, h, adjustments) => { // OOXML spec: 4 adjustments const adj1Raw = adjustments?.get('adj1') ?? 25000 const adj2Raw = adjustments?.get('adj2') ?? 25000 const adj3Raw = adjustments?.get('adj3') ?? 25000 const adj4Raw = adjustments?.get('adj4') ?? 48123 const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2Raw, (50000 * h) / Math.max(ss, 1))) const a1 = Math.max(0, Math.min(adj1Raw, a2 * 2)) const a3 = Math.max(0, Math.min(adj3Raw, (50000 * w) / Math.max(ss, 1))) const q2 = (a3 * ss) / Math.max(w, 1) const a4 = Math.max(0, Math.min(adj4Raw, 100000 - q2 - q2)) const dy1 = (ss * a2) / 100000 const dy2 = (ss * a1) / 200000 const vc = h / 2 const y1 = vc - dy1 const y2 = vc - dy2 const y3 = vc + dy2 const y4 = vc + dy1 const x1 = (ss * a3) / 100000 const dx2 = (w * a4) / 200000 const x2 = w / 2 - dx2 const x3 = w / 2 + dx2 const x4 = w - x1 return [ `M0,${vc}`, `L${x1},${y1}`, `L${x1},${y2}`, `L${x2},${y2}`, `L${x2},0`, `L${x3},0`, `L${x3},${y2}`, `L${x4},${y2}`, `L${x4},${y1}`, `L${w},${vc}`, `L${x4},${y4}`, `L${x4},${y3}`, `L${x3},${y3}`, `L${x3},${h}`, `L${x2},${h}`, `L${x2},${y3}`, `L${x1},${y3}`, `L${x1},${y4}`, 'Z', ].join(' ') }) presetShapes.set('uturnArrow', (w, h, adjustments) => { // ECMA-like U-turn arrow geometry (5 adjustments). const adj1 = adjustments?.get('adj1') ?? 25000 const adj2 = adjustments?.get('adj2') ?? 25000 const adj3 = adjustments?.get('adj3') ?? 25000 const adj4 = adjustments?.get('adj4') ?? 43750 const adj5 = adjustments?.get('adj5') ?? 75000 const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2, 25000)) const a1 = Math.max(0, Math.min(adj1, a2 * 2)) const q2 = (a1 * ss) / Math.max(h, 1) const q3 = 100000 - q2 const a3 = Math.max(0, Math.min(adj3, (q3 * h) / Math.max(ss, 1))) const minAdj5 = ((a3 + a1) * ss) / Math.max(h, 1) const a5 = Math.max(minAdj5, Math.min(adj5, 100000)) const th = (ss * a1) / 100000 const aw2 = (ss * a2) / 100000 const th2 = th / 2 const dh2 = aw2 - th2 const y5 = (h * a5) / 100000 const ah = (ss * a3) / 100000 const y4 = y5 - ah const x9 = w - dh2 const bs = Math.min(x9 / 2, y4) const a4 = Math.max(0, Math.min(adj4, (100000 * bs) / Math.max(ss, 1))) const bd = (ss * a4) / 100000 const bd2 = Math.max(bd - th, 0) const x3 = th + bd2 const x8 = w - aw2 const x6 = x8 - aw2 const x7 = x6 + dh2 const x4 = x9 - bd const x5 = x7 - bd2 return [ `M0,${h}`, `L0,${bd}`, bd > 0.1 ? `A${bd},${bd} 0 0,1 ${bd},0` : `L0,0`, `L${x4},0`, bd > 0.1 ? `A${bd},${bd} 0 0,1 ${x9},${bd}` : `L${x9},0`, `L${x9},${y4}`, `L${w},${y4}`, `L${x8},${y5}`, `L${x6},${y4}`, `L${x7},${y4}`, `L${x7},${x3}`, bd2 > 0.1 ? `A${bd2},${bd2} 0 0,0 ${x5},${th}` : `L${x5},${th}`, `L${x3},${th}`, bd2 > 0.1 ? `A${bd2},${bd2} 0 0,0 ${th},${x3}` : `L${th},${x3}`, `L${th},${h}`, 'Z', ].join(' ') }) presetShapes.set('leftRightArrow', (w, h, adjustments) => { // OOXML: adj1=50000 (shaft width), adj2=50000 (head length based on ss) const ss = Math.min(w, h) const hd2 = h / 2 const maxAdj2 = ss > 0 ? (50000 * w) / ss : 0 const a1 = Math.min(Math.max(adjustments?.get('adj1') ?? 50000, 0), 100000) const a2 = Math.min(Math.max(adjustments?.get('adj2') ?? 50000, 0), maxAdj2) const x2 = (ss * a2) / 100000 const x3 = w - x2 const dy = (h * a1) / 200000 const vc = hd2 const y1 = vc - dy const y2 = vc + dy const dx1 = hd2 > 0 ? (y1 * x2) / hd2 : 0 const _x1 = x2 - dx1 const _x4 = x3 + dx1 return [ `M0,${vc}`, `L${x2},0`, `L${x2},${y1}`, `L${x3},${y1}`, `L${x3},0`, `L${w},${vc}`, `L${x3},${h}`, `L${x3},${y2}`, `L${x2},${y2}`, `L${x2},${h}`, 'Z', ].join(' ') }) presetShapes.set('leftUpArrow', (w, h, adjustments) => { // OOXML preset formula (presetShapeDefinitions.xml -> leftUpArrow) const rawAdj2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, 50000)) const maxAdj1 = rawAdj2 * 2 const rawAdj1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, maxAdj1)) const maxAdj3 = 100000 - maxAdj1 const rawAdj3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, maxAdj3)) const ss = Math.min(w, h) const x1 = (ss * rawAdj3) / 100000 const dx2 = (ss * rawAdj2) / 50000 const x2 = w - dx2 const y2 = h - dx2 const dx4 = (ss * rawAdj2) / 100000 const x4 = w - dx4 const y4 = h - dx4 const dx3 = (ss * rawAdj1) / 200000 const x3 = x4 - dx3 const x5 = x4 + dx3 const y3 = y4 - dx3 const y5 = y4 + dx3 return [ `M0,${y4}`, `L${x1},${y2}`, `L${x1},${y3}`, `L${x3},${y3}`, `L${x3},${x1}`, `L${x2},${x1}`, `L${x4},0`, `L${w},${x1}`, `L${x5},${x1}`, `L${x5},${y5}`, `L${x1},${y5}`, `L${x1},${h}`, 'Z', ].join(' ') }) presetShapes.set('upDownArrow', (w, h, adjustments) => { // OOXML spec: adj1=50000 (shaft width), adj2=50000 (head length on ss) const adj1Raw = adjustments?.get('adj1') ?? 50000 const adj2Raw = adjustments?.get('adj2') ?? 50000 const ss = Math.min(w, h) const maxAdj2 = (50000 * h) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(adj2Raw, maxAdj2)) const a1 = Math.max(0, Math.min(adj1Raw, 100000)) const dx1 = (ss * a1) / 200000 // shaft half-width const dy = (ss * a2) / 100000 // arrowhead length const hc = w / 2 return [ `M${hc},0`, `L${w},${dy}`, `L${hc + dx1},${dy}`, `L${hc + dx1},${h - dy}`, `L${w},${h - dy}`, `L${hc},${h}`, `L0,${h - dy}`, `L${hc - dx1},${h - dy}`, `L${hc - dx1},${dy}`, `L0,${dy}`, 'Z', ].join(' ') }) presetShapes.set('notchedRightArrow', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 50000) // shaft width ratio const a2 = adj(adjustments, 'adj2', 50000) // head length ratio const ss = Math.min(w, h) // OOXML uses short side for head length const shaftHalfH = (h * a1) / 2 const headLen = ss * a2 const cy = h / 2 const shaftEnd = w - headLen // Notch depth: OOXML formula dxn = dy1 * dx2 / hd2 = shaftHalfH * headLen / (h/2) const notchDepth = cy > 0 ? (shaftHalfH * headLen) / cy : 0 return [ `M0,${cy - shaftHalfH}`, `L${shaftEnd},${cy - shaftHalfH}`, `L${shaftEnd},0`, `L${w},${cy}`, `L${shaftEnd},${h}`, `L${shaftEnd},${cy + shaftHalfH}`, `L0,${cy + shaftHalfH}`, `L${notchDepth},${cy}`, 'Z', ].join(' ') }) presetShapes.set('chevron', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 50000) const ss = Math.min(w, h) const offset = ss * a return [ `M0,0`, `L${w - offset},0`, `L${w},${h / 2}`, `L${w - offset},${h}`, `L0,${h}`, `L${offset},${h / 2}`, 'Z', ].join(' ') }) presetShapes.set('homePlate', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 50000) const ss = Math.min(w, h) const offset = ss * a const shoulderX = w - offset return [`M0,0`, `L${shoulderX},0`, `L${w},${h / 2}`, `L${shoulderX},${h}`, `L0,${h}`, 'Z'].join( ' ' ) }) presetShapes.set('stripedRightArrow', (w, h, adjustments) => { // OOXML: adj1=50000, adj2=50000 (max 84375). Stripes at ssd32, ssd16-ssd8, x4=ss*5/32. const ss = Math.min(w, h) const maxAdj2 = ss > 0 ? (84375 * w) / ss : 84375 const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 50000), 0), 100000) const a2 = Math.min(Math.max(adjRaw(adjustments, 'adj2', 50000), 0), maxAdj2) const dy1 = (h * a1) / 200000 const dx5 = (ss * a2) / 100000 const x5 = w - dx5 const vc = h / 2 const y1 = vc - dy1 const y2 = vc + dy1 const ssd32 = ss / 32 const ssd16 = ss / 16 const ssd8 = ss / 8 const x4 = (ss * 5) / 32 return [ // Stripe 1: 0 to ssd32 `M0,${y1} L${ssd32},${y1} L${ssd32},${y2} L0,${y2} Z`, // Stripe 2: ssd16 to ssd8 `M${ssd16},${y1} L${ssd8},${y1} L${ssd8},${y2} L${ssd16},${y2} Z`, // Main body + arrowhead: x4 to r `M${x4},${y1}`, `L${x5},${y1}`, `L${x5},0`, `L${w},${vc}`, `L${x5},${h}`, `L${x5},${y2}`, `L${x4},${y2}`, 'Z', ].join(' ') }) // ===== Bent / Curved / Special Arrows ===== presetShapes.set('bentArrow', (w, h, adjustments) => { // OOXML bentArrow: L-shaped arrow with rounded bend, arrowhead pointing right. // Uses 4 adjustments per ECMA-376 spec. const ss = Math.min(w, h) // Constrained adjustments (raw values, not fractions — we do our own math) const adj2Raw = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, 50000)) const maxAdj1 = adj2Raw * 2 const adj1Raw = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, maxAdj1)) const adj3Raw = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, 50000)) const th = (ss * adj1Raw) / 100000 // shaft width const aw2 = (ss * adj2Raw) / 100000 // arrowhead half-width const th2 = th / 2 const dh2 = aw2 - th2 // arrowhead extension beyond shaft const ah = (ss * adj3Raw) / 100000 // arrowhead length const bw = w - ah const bh = h - dh2 const bs = Math.min(bw, bh) const maxAdj4 = bs > 0 ? (100000 * bs) / ss : 0 const adj4Raw = Math.max(0, Math.min(adjustments?.get('adj4') ?? 43750, maxAdj4)) const bd = (ss * adj4Raw) / 100000 // outer bend radius const bd2 = Math.max(bd - th, 0) // inner bend radius const x3 = th + bd2 const x4 = w - ah const y3 = dh2 + th const y4 = y3 + dh2 const y5 = dh2 + bd // OOXML arcTo: from current point, arc with radii (wR, hR), start angle stAng, sweep swAng. // Arc 1: outer bend — from (0, y5), radii=bd, 180°→270° (sweep +90°) // Center of arc is at (bd, y5) relative, endpoint at (bd, y5-bd) = (bd, dh2) // SVG: A bd,bd 0 0,1 bd,dh2 // Arc 2: inner bend — from (x3, y3), radii=bd2, 270°→180° (sweep -90°) // Center at (x3, y3+bd2), endpoint at (x3-bd2, y3+bd2) = (th, y3+bd2) // SVG: A bd2,bd2 0 0,0 th,y6 where y6 = y3+bd2 const y6 = y3 + bd2 const parts: string[] = [ `M0,${h}`, // bottom-left `L0,${y5}`, // up left edge to arc start ] // Outer arc (rounded bend, going from left edge up to top edge) if (bd > 0.1) { parts.push(`A${bd},${bd} 0 0,1 ${bd},${dh2}`) } else { parts.push(`L0,${dh2}`) // degenerate: straight corner } parts.push( `L${x4},${dh2}`, // horizontal to arrowhead base (top) `L${x4},0`, // up to arrowhead top-left wing `L${w},${aw2}`, // arrowhead tip (pointing right) `L${x4},${y4}`, // arrowhead bottom wing `L${x4},${y3}`, // back to arrowhead base (bottom) `L${x3},${y3}` // horizontal back toward bend ) // Inner arc (rounded bend, going from top down to right side of shaft) if (bd2 > 0.1) { parts.push(`A${bd2},${bd2} 0 0,0 ${th},${y6}`) } else { parts.push(`L${th},${y3}`) // degenerate: straight corner } parts.push( `L${th},${h}`, // down right side of shaft to bottom 'Z' ) return parts.join(' ') }) presetShapes.set('bentUpArrow', (w, h, adjustments) => { // OOXML preset formula (presetShapeDefinitions.xml -> bentUpArrow): // x/y variables are solved from adj1/2/3 in [0..50000], ss=min(w,h). const raw1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, 50000)) const raw2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, 50000)) const raw3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, 50000)) const ss = Math.min(w, h) const y1 = (ss * raw3) / 100000 const dx1 = (ss * raw2) / 50000 const x1 = w - dx1 const dx3 = (ss * raw2) / 100000 const x3 = w - dx3 const dx2 = (ss * raw1) / 200000 const x2 = x3 - dx2 const x4 = x3 + dx2 const dy2 = (ss * raw1) / 100000 const y2 = h - dy2 return [ `M0,${y2}`, `L${x2},${y2}`, `L${x2},${y1}`, `L${x1},${y1}`, `L${x3},0`, `L${w},${y1}`, `L${x4},${y1}`, `L${x4},${h}`, `L0,${h}`, 'Z', ].join(' ') }) presetShapes.set('curvedRightArrow', (w, h, adjustments) => { // Keep geometry aligned with OOXML preset math. Use local arc helper here // because preset formulas mix positive/negative sweeps that do not map 1:1 // to the generic shapeArc() helper used in other shapes. const adj1Raw = adjustments?.get('adj1') ?? 25000 const adj2Raw = adjustments?.get('adj2') ?? 50000 const adj3Raw = adjustments?.get('adj3') ?? 25000 const cnstVal1 = 50000 const cnstVal2 = 100000 const hd2 = h / 2 const r = w const b = h const l = 0 const c3d4 = 270 const cd2 = 180 const cd4 = 90 const ss = Math.max(Math.min(w, h), 1) const maxAdj2 = (cnstVal1 * h) / ss const a2 = Math.max(0, Math.min(adj2Raw, maxAdj2)) const a1 = Math.max(0, Math.min(adj1Raw, a2)) const th = (ss * a1) / cnstVal2 const aw = (ss * a2) / cnstVal2 const q1 = (th + aw) / 4 const hR = hd2 - q1 const q7 = hR * 2 const q8 = q7 * q7 const q9 = th * th const q10 = Math.max(q8 - q9, 0) const q11 = Math.sqrt(q10) const iDx = (q11 * w) / Math.max(q7, 1e-6) const maxAdj3 = (cnstVal2 * iDx) / ss const a3 = Math.max(0, Math.min(adj3Raw, maxAdj3)) const ah = (ss * a3) / cnstVal2 const y3 = hR + th const q2 = w * w const q3 = ah * ah const q4 = Math.max(q2 - q3, 0) const q5 = Math.sqrt(q4) const dy = (q5 * hR) / Math.max(w, 1e-6) const y5 = hR + dy const y7 = y3 + dy const q6 = aw - th const dh = q6 / 2 const y4 = y5 - dh const y8 = y7 + dh const aw2 = aw / 2 const y6 = b - aw2 const x1 = r - ah const swAng = Math.atan(dy / Math.max(ah, 1e-6)) const stAng = Math.PI - swAng const mswAng = -swAng const q12 = th / 2 const dang2 = Math.atan2(q12, Math.max(iDx, 1e-6)) const swAng2 = dang2 - Math.PI / 2 const stAngDg = (stAng * 180) / Math.PI const mswAngDg = (mswAng * 180) / Math.PI const swAngDg = (swAng * 180) / Math.PI const swAng2Dg = (swAng2 * 180) / Math.PI const arc = ( cx: number, cy: number, rx: number, ry: number, startDeg: number, endDeg: number ): string => { const s = (startDeg * Math.PI) / 180 const e = (endDeg * Math.PI) / 180 const xS = cx + rx * Math.cos(s) const yS = cy + ry * Math.sin(s) const xE = cx + rx * Math.cos(e) const yE = cy + ry * Math.sin(e) const delta = endDeg - startDeg const largeArc = Math.abs(delta) > 180 ? 1 : 0 const sweep = delta >= 0 ? 1 : 0 return `M${xS},${yS} A${rx},${ry} 0 ${largeArc},${sweep} ${xE},${yE}` } return [ `M${l},${hR}`, arc(w, hR, w, hR, cd2, cd2 + mswAngDg).replace('M', 'L'), `L${x1},${y5}`, `L${x1},${y4}`, `L${r},${y6}`, `L${x1},${y8}`, `L${x1},${y7}`, arc(w, y3, w, hR, stAngDg, stAngDg + swAngDg).replace('M', 'L'), 'Z', arc(w, hR, w, hR, cd2, cd2 + cd4), `L${r},${th}`, arc(w, y3, w, hR, c3d4, c3d4 + swAng2Dg).replace('M', 'L'), 'Z', ].join(' ') }) presetShapes.set('curvedLeftArrow', (w, h, adjustments) => mirrorAbsolutePathHorizontally(presetShapes.get('curvedRightArrow')!(w, h, adjustments), w) ) function splitFirstClosedContour(path: string): { outer: string; remainder: string } { const closeIdx = path.indexOf('Z') if (closeIdx === -1) { return { outer: path, remainder: '' } } const outer = path.slice(0, closeIdx + 1).trim() const remainder = path.slice(closeIdx + 1).trim() return { outer, remainder } } function buildCurvedArrowMultiPath( shapeName: 'curvedRightArrow' | 'curvedLeftArrow', w: number, h: number, adjustments?: Map ): PresetSubPath[] { const fullPath = presetShapes.get(shapeName)!(w, h, adjustments) const { outer, remainder } = splitFirstClosedContour(fullPath) if (!remainder) { return [{ d: fullPath, fill: 'norm', stroke: true }] } if (shapeName === 'curvedRightArrow') { return [ { d: remainder, fill: 'norm', stroke: true }, { d: outer, fill: 'norm', stroke: true }, ] } return [ { d: outer, fill: 'norm', stroke: true }, { d: remainder, fill: 'norm', stroke: true }, ] } function buildCurvedVerticalArrowMultiPath( shapeName: 'curvedUpArrow' | 'curvedDownArrow', w: number, h: number, adjustments?: Map ): PresetSubPath[] { const downFullPath = presetShapes.get('curvedDownArrow')!(w, h, adjustments) const { outer, remainder } = splitFirstClosedContour(downFullPath) const ordered: PresetSubPath[] = remainder ? [ { d: remainder, fill: 'norm', stroke: true }, { d: outer, fill: 'norm', stroke: true }, ] : [{ d: downFullPath, fill: 'norm', stroke: true }] if (shapeName === 'curvedDownArrow') { return ordered } const mirrored: PresetSubPath[] = ordered.map((path) => ({ ...path, d: mirrorAbsolutePathVertically(path.d, h), })) return mirrored.reverse() } /** * Convert OOXML arcTo to SVG arc endpoint and command string. * OOXML arcTo: wR, hR (radii), stAng, swAng (degrees). * Current point is at stAng on the arc ellipse. * Returns { path, endX, endY }. */ presetShapes.set('curvedUpArrow', (w, h, adjustments) => { const arc = ( cx: number, cy: number, rx: number, ry: number, startDeg: number, endDeg: number ): string => { const s = (startDeg * Math.PI) / 180 const e = (endDeg * Math.PI) / 180 const xS = cx + rx * Math.cos(s) const yS = cy + ry * Math.sin(s) const xE = cx + rx * Math.cos(e) const yE = cy + ry * Math.sin(e) const delta = endDeg - startDeg const largeArc = Math.abs(delta) > 180 ? 1 : 0 const sweep = delta >= 0 ? 1 : 0 return `M${xS},${yS} A${rx},${ry} 0 ${largeArc},${sweep} ${xE},${yE}` } const ss = Math.min(w, h) const wd2 = w / 2 const a1Raw = adjustments?.get('adj1') ?? 25000 const a2Raw = adjustments?.get('adj2') ?? 50000 const a3Raw = adjustments?.get('adj3') ?? 25000 const maxAdj2 = (50000 * w) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(a2Raw, maxAdj2)) const a1 = Math.max(0, Math.min(a1Raw, 100000)) const th = (ss * a1) / 100000 const aw = (ss * a2) / 100000 const q1 = (th + aw) / 4 const wR = wd2 - q1 const q7 = wR * 2 const idy = (Math.sqrt(Math.max(q7 * q7 - th * th, 0)) * h) / Math.max(q7, 1) const maxAdj3 = (100000 * idy) / Math.max(ss, 1) const a3 = Math.max(0, Math.min(a3Raw, maxAdj3)) const ah = (ss * a3) / 100000 const x3 = wR + th const dx = (Math.sqrt(Math.max(h * h - ah * ah, 0)) * wR) / Math.max(h, 1) const x5 = wR + dx const x7 = x3 + dx const dh = (aw - th) / 2 const x4 = x5 - dh const x8 = x7 + dh const x6 = w - aw / 2 const y1 = ah const swAng = Math.atan2(dx, ah) const dang2 = Math.atan2(th / 2, idy) const stAng2 = Math.PI / 2 - dang2 const swAng2 = dang2 - swAng const stAng3 = Math.PI / 2 - swAng const stAng2Deg = (stAng2 * 180) / Math.PI const swAng2Deg = (swAng2 * 180) / Math.PI const stAng3Deg = (stAng3 * 180) / Math.PI const swAngDeg = (swAng * 180) / Math.PI return [ arc(wR, 0, wR, h, stAng2Deg, stAng2Deg + swAng2Deg), `L${x5},${y1}`, `L${x4},${y1}`, `L${x6},0`, `L${x8},${y1}`, `L${x7},${y1}`, arc(x3, 0, wR, h, stAng3Deg, stAng3Deg + swAngDeg).replace('M', 'L'), `L${wR},${h}`, arc(wR, 0, wR, h, 90, 180).replace('M', 'L'), `L${th},0`, arc(x3, 0, wR, h, 180, 90).replace('M', 'L'), 'Z', ].join(' ') }) presetShapes.set('curvedDownArrow', (w, h, adjustments) => { const arc = ( cx: number, cy: number, rx: number, ry: number, startDeg: number, endDeg: number ): string => { const s = (startDeg * Math.PI) / 180 const e = (endDeg * Math.PI) / 180 const xS = cx + rx * Math.cos(s) const yS = cy + ry * Math.sin(s) const xE = cx + rx * Math.cos(e) const yE = cy + ry * Math.sin(e) const delta = endDeg - startDeg const largeArc = Math.abs(delta) > 180 ? 1 : 0 const sweep = delta >= 0 ? 1 : 0 return `M${xS},${yS} A${rx},${ry} 0 ${largeArc},${sweep} ${xE},${yE}` } const ss = Math.min(w, h) const wd2 = w / 2 const a1Raw = adjustments?.get('adj1') ?? 25000 const a2Raw = adjustments?.get('adj2') ?? 50000 const a3Raw = adjustments?.get('adj3') ?? 25000 const maxAdj2 = (50000 * w) / Math.max(ss, 1) const a2 = Math.max(0, Math.min(a2Raw, maxAdj2)) const a1 = Math.max(0, Math.min(a1Raw, 100000)) const th = (ss * a1) / 100000 const aw = (ss * a2) / 100000 const q1 = (th + aw) / 4 const wR = wd2 - q1 const q7 = wR * 2 const idy = (Math.sqrt(Math.max(q7 * q7 - th * th, 0)) * h) / Math.max(q7, 1) const maxAdj3 = (100000 * idy) / Math.max(ss, 1) const a3 = Math.max(0, Math.min(a3Raw, maxAdj3)) const ah = (ss * a3) / 100000 const x3 = wR + th const dx = (Math.sqrt(Math.max(h * h - ah * ah, 0)) * wR) / Math.max(h, 1) const x5 = wR + dx const x7 = x3 + dx const dh = (aw - th) / 2 const x4 = x5 - dh const x8 = x7 + dh const x6 = w - aw / 2 const y1 = h - ah const swAng = Math.atan2(dx, ah) const swAngDeg = (swAng * 180) / Math.PI const dang2 = Math.atan2(th / 2, idy) const dang2Deg = (dang2 * 180) / Math.PI const stAng = 270 + swAngDeg const stAng2 = 270 - dang2Deg const swAng2 = dang2Deg - 90 const swAng3 = 90 + dang2Deg return [ `M${x6},${h}`, `L${x4},${y1}`, `L${x5},${y1}`, arc(wR, h, wR, h, stAng, stAng - swAngDeg).replace('M', 'L'), `L${x3},0`, arc(x3, h, wR, h, 270, 270 + swAngDeg).replace('M', 'L'), `L${x5 + th},${y1}`, `L${x8},${y1}`, 'Z', `M${x3},0`, arc(x3, h, wR, h, stAng2, stAng2 + swAng2).replace('M', 'L'), arc(wR, h, wR, h, 180, 180 + swAng3).replace('M', 'L'), 'Z', ].join(' ') }) function buildCircularArrowPath( w: number, h: number, adjustments?: Map, _mirrorX = false, variant: 'circularArrow' | 'leftCircularArrow' = 'circularArrow' ): string { // OOXML circularArrow / leftCircularArrow: same guide formulas, different default adjustments. const hc = w / 2 const vc = h / 2 const wd2 = w / 2 const hd2 = h / 2 const ss = Math.min(w, h) const cd2 = 10800000 // 180° in 60000ths const toRad60k = (a: number) => ((a / 60000) * Math.PI) / 180 // OOXML formula helpers const ooxSin = (val: number, ang: number) => val * Math.sin(toRad60k(ang)) const ooxCos = (val: number, ang: number) => val * Math.cos(toRad60k(ang)) const cat2 = (r: number, ht: number, wt: number) => r * Math.cos(Math.atan2(wt, ht)) const sat2 = (r: number, ht: number, wt: number) => r * Math.sin(Math.atan2(wt, ht)) // OOXML: at2(x, y) = atan2(y, x) — first arg is x, second is y const at2 = (x: number, y: number) => ((Math.atan2(y, x) * 180) / Math.PI) * 60000 const modF = (x: number, y: number, z: number) => Math.sqrt(x * x + y * y + z * z) // Adjustments — leftCircularArrow has different OOXML defaults const isLeft = variant === 'leftCircularArrow' const adj1 = adjustments?.get('adj1') ?? 12500 const adj2 = adjustments?.get('adj2') ?? (isLeft ? -1142319 : 1142319) const adj3 = adjustments?.get('adj3') ?? (isLeft ? 1142319 : 20457681) const adj4 = adjustments?.get('adj4') ?? 10800000 const adj5v = adjustments?.get('adj5') ?? 12500 const a5 = Math.max(0, Math.min(adj5v, 25000)) const maxAdj1 = a5 * 2 const a1 = Math.max(0, Math.min(adj1, maxAdj1)) const enAng = Math.max(1, Math.min(adj3, 21599999)) const stAng = Math.max(0, Math.min(adj4, 21599999)) const th = (ss * a1) / 100000 const thh = (ss * a5) / 100000 const th2 = th / 2 const rw1 = wd2 + th2 - thh const rh1 = hd2 + th2 - thh const rw2 = rw1 - th const rh2 = rh1 - th const rw3 = rw2 + th2 const rh3 = rh2 + th2 // Point H (mid-radius at end angle) const wtH = ooxSin(rw3, enAng) const htH = ooxCos(rh3, enAng) const dxH = cat2(rw3, htH, wtH) const dyH = sat2(rh3, htH, wtH) const xH = hc + dxH const yH = vc + dyH // Compute max arrowhead angle const rI = Math.min(rw2, rh2) const u1 = dxH * dxH const u2 = dyH * dyH const u3 = rI * rI const u4 = u1 - u3 const u5 = u2 - u3 const u6 = u2 !== 0 ? (u4 * u5) / u1 : 0 const u7 = u2 !== 0 ? u6 / u2 : 0 const u8 = 1 - u7 const u9 = Math.sqrt(Math.max(0, u8)) const u10 = dxH !== 0 ? u4 / dxH : 0 const u11 = dyH !== 0 ? u10 / dyH : 0 const u12 = u11 !== 0 ? (1 + u9) / u11 : 0 const u13 = at2(1, u12) const u14 = u13 + 21600000 const u15 = u13 >= 0 ? u13 : u14 const u16 = u15 - enAng const u17 = u16 + 21600000 const u18 = u16 >= 0 ? u16 : u17 const u19 = u18 - cd2 const u20 = u18 - 21600000 const u21 = u19 >= 0 ? u20 : u18 const maxAng = Math.abs(u21) let aAng: number if (isLeft) { // leftCircularArrow: minAng = -abs(u21), a2 = -abs(adj2), aAng = pin(minAng, a2, 0) const minAng = -maxAng const a2 = -Math.abs(adj2) aAng = Math.max(minAng, Math.min(a2, 0)) } else { aAng = Math.max(0, Math.min(adj2, maxAng)) } const ptAng = enAng + aAng // Point A (arrowhead tip) const wtA = ooxSin(rw3, ptAng) const htA = ooxCos(rh3, ptAng) const dxA = cat2(rw3, htA, wtA) const dyA = sat2(rh3, htA, wtA) const xA = hc + dxA const yA = vc + dyA // Point E (outer arc start) const wtE = ooxSin(rw1, stAng) const htE = ooxCos(rh1, stAng) const dxE = cat2(rw1, htE, wtE) const dyE = sat2(rh1, htE, wtE) const xE = hc + dxE const yE = vc + dyE // Points G and B (arrowhead base, offset from H by thh at angle ptAng) const dxG = ooxCos(thh, ptAng) const dyG = ooxSin(thh, ptAng) const xG = xH + dxG const yG = yH + dyG const xB = xH - dxG const yB = yH - dyG // Scale to normalized circle for line-circle intersection const sx1 = xB - hc const sy1 = yB - vc const sx2 = xG - hc const sy2 = yG - vc // Outer circle intersection const rO = Math.min(rw1, rh1) const x1O = rw1 !== 0 ? (sx1 * rO) / rw1 : 0 const y1O = rh1 !== 0 ? (sy1 * rO) / rh1 : 0 const x2O = rw1 !== 0 ? (sx2 * rO) / rw1 : 0 const y2O = rh1 !== 0 ? (sy2 * rO) / rh1 : 0 const dxO = x2O - x1O const dyO = y2O - y1O const dOval = modF(dxO, dyO, 0) const q1 = x1O * y2O const q2 = x2O * y1O const DO = q1 - q2 const q3 = rO * rO const q4 = dOval * dOval const q5 = q3 * q4 const q6 = DO * DO const q7 = q5 - q6 const q8 = Math.max(q7, 0) const sdelO = Math.sqrt(q8) const ndyO = dyO * -1 const sdyO = ndyO >= 0 ? -1 : 1 const q9 = sdyO * dxO const q10 = q9 * sdelO const q11 = DO * dyO const dxF1 = q4 !== 0 ? (q11 + q10) / q4 : 0 const q12 = q11 - q10 const dxF2 = q4 !== 0 ? q12 / q4 : 0 const adyO = Math.abs(dyO) const q13 = adyO * sdelO const q14 = DO * dxO * -1 const dyF1 = q4 !== 0 ? (q14 + q13) / q4 : 0 const q15 = q14 - q13 const dyF2 = q4 !== 0 ? q15 / q4 : 0 // Pick intersection closest to G side const q16 = x2O - dxF1 const q17 = x2O - dxF2 const q18 = y2O - dyF1 const q19 = y2O - dyF2 const q20 = modF(q16, q18, 0) const q21 = modF(q17, q19, 0) const q22 = q21 - q20 const dxF = q22 >= 0 ? dxF1 : dxF2 const dyF = q22 >= 0 ? dyF1 : dyF2 const sdxF = rO !== 0 ? (dxF * rw1) / rO : 0 const sdyF = rO !== 0 ? (dyF * rh1) / rO : 0 const xF = hc + sdxF const yF = vc + sdyF // Inner circle intersection const x1I = rw2 !== 0 ? (sx1 * rI) / rw2 : 0 const y1I = rh2 !== 0 ? (sy1 * rI) / rh2 : 0 const x2I = rw2 !== 0 ? (sx2 * rI) / rw2 : 0 const y2I = rh2 !== 0 ? (sy2 * rI) / rh2 : 0 const dxI = x2I - x1I const dyI = y2I - y1I const dI = modF(dxI, dyI, 0) const v1 = x1I * y2I const v2 = x2I * y1I const DI = v1 - v2 const v3 = rI * rI const v4 = dI * dI const v5 = v3 * v4 const v6 = DI * DI const v7 = v5 - v6 const v8 = Math.max(v7, 0) const sdelI = Math.sqrt(v8) const v9 = sdyO * dxI const v10 = v9 * sdelI const v11 = DI * dyI const dxC1 = v4 !== 0 ? (v11 + v10) / v4 : 0 const v12 = v11 - v10 const dxC2 = v4 !== 0 ? v12 / v4 : 0 const adyI = Math.abs(dyI) const v13 = adyI * sdelI const v14 = DI * dxI * -1 const dyC1 = v4 !== 0 ? (v14 + v13) / v4 : 0 const v15 = v14 - v13 const dyC2 = v4 !== 0 ? v15 / v4 : 0 // Pick intersection closest to B side (x1I) const v16 = x1I - dxC1 const v17 = x1I - dxC2 const v18 = y1I - dyC1 const v19 = y1I - dyC2 const v20 = modF(v16, v18, 0) const v21 = modF(v17, v19, 0) const v22 = v21 - v20 const dxC = v22 >= 0 ? dxC1 : dxC2 const dyC = v22 >= 0 ? dyC1 : dyC2 const sdxC = rI !== 0 ? (dxC * rw2) / rI : 0 const sdyC = rI !== 0 ? (dyC * rh2) / rI : 0 const xC = hc + sdxC const yC = vc + sdyC // Inner arc angles — leftCircularArrow uses intermediate istAng0/iswAng0 const ist0 = at2(sdxC, sdyC) const ist1 = ist0 + 21600000 const istAng0 = ist0 >= 0 ? ist0 : ist1 const isw1 = stAng - istAng0 let istAng: number let iswAng: number if (isLeft) { // leftCircularArrow: iswAng0 always ≥ 0, then istAng shifted, iswAng negated const iswAng0 = isw1 >= 0 ? isw1 : isw1 + 21600000 istAng = istAng0 + iswAng0 iswAng = -iswAng0 } else { // circularArrow: iswAng always ≤ 0 (clockwise inner arc) istAng = istAng0 iswAng = isw1 >= 0 ? isw1 - 21600000 : isw1 } // Adjusted arrowhead points (clamp when too close) const p1 = xF - xC const p2 = yF - yC const p3 = modF(p1, p2, 0) const p4 = p3 / 2 const p5 = p4 - thh const xGp = p5 >= 0 ? xF : xG const yGp = p5 >= 0 ? yF : yG const xBp = p5 >= 0 ? xC : xB const yBp = p5 >= 0 ? yC : yB // Outer arc sweep angle const en0 = at2(sdxF, sdyF) const en1 = en0 + 21600000 const en2 = en0 >= 0 ? en0 : en1 const sw0 = en2 - stAng let outerArcStAng: number let outerArcSwAng: number if (isLeft) { // leftCircularArrow: swAng ≤ 0, then stAng0 = stAng + swAng, swAng0 = -swAng const swAngRaw = sw0 >= 0 ? sw0 - 21600000 : sw0 outerArcStAng = stAng + swAngRaw // stAng0 outerArcSwAng = -swAngRaw // swAng0 (positive) } else { const swAng = sw0 >= 0 ? sw0 : sw0 + 21600000 outerArcStAng = stAng outerArcSwAng = swAng } // Compute end points for SVG arcs using OOXML arcTo semantics // Outer arc: from outerArcStAng sweeping outerArcSwAng const outerEndAng = outerArcStAng + outerArcSwAng const wtOE = ooxSin(rw1, outerEndAng) const htOE = ooxCos(rh1, outerEndAng) const xOE = hc + cat2(rw1, htOE, wtOE) const yOE = vc + sat2(rh1, htOE, wtOE) // Inner arc: from istAng sweeping iswAng const innerEndAng = istAng + iswAng const wtIE = ooxSin(rw2, innerEndAng) const htIE = ooxCos(rh2, innerEndAng) const xIE = hc + cat2(rw2, htIE, wtIE) const yIE = vc + sat2(rh2, htIE, wtIE) // SVG arc flags const outerSweepDeg = Math.abs(outerArcSwAng / 60000) const outerLargeArc = outerSweepDeg > 180 ? 1 : 0 const outerSweepFlag = outerArcSwAng > 0 ? 1 : 0 const innerSweepDeg = Math.abs(iswAng / 60000) const innerLargeArc = innerSweepDeg > 180 ? 1 : 0 const innerSweepFlag = iswAng > 0 ? 1 : 0 if (isLeft) { // leftCircularArrow path: M(xE) → L(xD) → inner arc → arrowhead → L(xF) → outer arc → Z // Point D: inner arc start at stAng on rw2/rh2 const wtD = ooxSin(rw2, stAng) const htD = ooxCos(rh2, stAng) const xD = hc + cat2(rw2, htD, wtD) const yD = vc + sat2(rh2, htD, wtD) return [ `M${xE},${yE}`, `L${xD},${yD}`, `A${rw2},${rh2} 0 ${innerLargeArc},${innerSweepFlag} ${xIE},${yIE}`, `L${xBp},${yBp}`, `L${xA},${yA}`, `L${xGp},${yGp}`, `L${xF},${yF}`, `A${rw1},${rh1} 0 ${outerLargeArc},${outerSweepFlag} ${xOE},${yOE}`, 'Z', ].join(' ') } return [ `M${xE},${yE}`, `A${rw1},${rh1} 0 ${outerLargeArc},${outerSweepFlag} ${xOE},${yOE}`, `L${xGp},${yGp}`, `L${xA},${yA}`, `L${xBp},${yBp}`, `L${xC},${yC}`, `A${rw2},${rh2} 0 ${innerLargeArc},${innerSweepFlag} ${xIE},${yIE}`, 'Z', ].join(' ') } presetShapes.set('circularArrow', (w, h, adjustments) => { return buildCircularArrowPath(w, h, adjustments, false, 'circularArrow') }) // leftCircularArrow uses same OOXML guide formulas as circularArrow but different default adjustments. presetShapes.set('leftCircularArrow', (w, h, adjustments) => { return buildCircularArrowPath(w, h, adjustments, false, 'leftCircularArrow') }) presetShapes.set('leftRightCircularArrow', (w, h, _adjustments) => { // Build from the actual oracle PDF vector path (shape id 0177), // normalized to a 400x280 reference box. const sx = w / 400 const sy = h / 280 const p = (x: number, y: number) => ({ x: x * sx, y: y * sy }) const p1 = p(35.0, 140.0) const p2 = p(19.9536, 89.9471) const p3 = p(33.4296, 89.9471) const c1 = p(74.6127, 28.1974) const c2 = p(182.5744, 0.5489) const p4 = p(274.5688, 28.1924) const c3 = p(315.4978, 40.4912) const c4 = p(348.2481, 62.4743) const p5 = p(366.5707, 89.9471) const p6 = p(380.0463, 89.9471) const p7 = p(365.0, 140.0) const p8 = p(310.0463, 89.9471) const p9 = p(320.9838, 89.9471) const c5 = p(274.3848, 50.3095) const c6 = p(182.4425, 40.5864) const p10 = p(115.6249, 68.2298) const c7 = p(101.3589, 74.1319) const c8 = p(88.9651, 81.4842) const p11 = p(79.0159, 89.947) const p12 = p(89.9536, 89.9471) return [ `M${p1.x},${p1.y}`, `L${p2.x},${p2.y}`, `L${p3.x},${p3.y}`, `C${c1.x},${c1.y} ${c2.x},${c2.y} ${p4.x},${p4.y}`, `C${c3.x},${c3.y} ${c4.x},${c4.y} ${p5.x},${p5.y}`, `L${p6.x},${p6.y}`, `L${p7.x},${p7.y}`, `L${p8.x},${p8.y}`, `L${p9.x},${p9.y}`, `C${c5.x},${c5.y} ${c6.x},${c6.y} ${p10.x},${p10.y}`, `C${c7.x},${c7.y} ${c8.x},${c8.y} ${p11.x},${p11.y}`, `L${p12.x},${p12.y}`, 'Z', ].join(' ') }) presetShapes.set('quadArrow', (w, h, adjustments) => { const adj1Raw = adjustments?.get('adj1') ?? 22500 const adj2Raw = adjustments?.get('adj2') ?? 22500 const adj3Raw = adjustments?.get('adj3') ?? 22500 const vc = h / 2 const hc = w / 2 const minWH = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2Raw, 50000)) const a1 = Math.max(0, Math.min(adj1Raw, 2 * a2)) const a3 = Math.max(0, Math.min(adj3Raw, (100000 - 2 * a2) / 2)) const x1 = (minWH * a3) / 100000 const dx2 = (minWH * a2) / 100000 const x2 = hc - dx2 const x5 = hc + dx2 const dx3 = (minWH * a1) / 200000 const x3 = hc - dx3 const x4 = hc + dx3 const x6 = w - x1 const y2 = vc - dx2 const y5 = vc + dx2 const y3 = vc - dx3 const y4 = vc + dx3 const y6 = h - x1 return [ `M0,${vc}`, `L${x1},${y2}`, `L${x1},${y3}`, `L${x3},${y3}`, `L${x3},${x1}`, `L${x2},${x1}`, `L${hc},0`, `L${x5},${x1}`, `L${x4},${x1}`, `L${x4},${y3}`, `L${x6},${y3}`, `L${x6},${y2}`, `L${w},${vc}`, `L${x6},${y5}`, `L${x6},${y4}`, `L${x4},${y4}`, `L${x4},${y6}`, `L${x5},${y6}`, `L${hc},${h}`, `L${x2},${y6}`, `L${x3},${y6}`, `L${x3},${y4}`, `L${x1},${y4}`, `L${x1},${y5}`, 'Z', ].join(' ') }) presetShapes.set('quadArrowCallout', (w, h, adjustments) => { // OOXML: 28-point polygon with 4 arrowheads (4 adj) const ss = Math.min(w, h) const hc = w / 2 const vc = h / 2 const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 18515, 50000)) const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 18515, a2 * 2)) const maxAdj3 = 50000 - a2 const a3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 18515, maxAdj3)) const q2 = a3 * 2 const a4 = Math.max(a1, Math.min(adjustments?.get('adj4') ?? 48123, 100000 - q2)) const dx2 = (ss * a2) / 100000 const dx3 = (ss * a1) / 200000 const ah = (ss * a3) / 100000 const dx1 = (w * a4) / 200000 const dy1 = (h * a4) / 200000 const x8 = w - ah const x2 = hc - dx1 const x7 = hc + dx1 const x3 = hc - dx2 const x6 = hc + dx2 const x4 = hc - dx3 const x5 = hc + dx3 const y8 = h - ah const y2 = vc - dy1 const y7 = vc + dy1 const y3 = vc - dx2 const y6 = vc + dx2 const y4 = vc - dx3 const y5 = vc + dx3 return [ `M0,${vc}`, `L${ah},${y3}`, `L${ah},${y4}`, `L${x2},${y4}`, `L${x2},${y2}`, `L${x4},${y2}`, `L${x4},${ah}`, `L${x3},${ah}`, `L${hc},0`, `L${x6},${ah}`, `L${x5},${ah}`, `L${x5},${y2}`, `L${x7},${y2}`, `L${x7},${y4}`, `L${x8},${y4}`, `L${x8},${y3}`, `L${w},${vc}`, `L${x8},${y6}`, `L${x8},${y5}`, `L${x7},${y5}`, `L${x7},${y7}`, `L${x5},${y7}`, `L${x5},${y8}`, `L${x6},${y8}`, `L${hc},${h}`, `L${x3},${y8}`, `L${x4},${y8}`, `L${x4},${y7}`, `L${x2},${y7}`, `L${x2},${y5}`, `L${ah},${y5}`, `L${ah},${y6}`, 'Z', ].join(' ') }) presetShapes.set('leftRightUpArrow', (w, h, adjustments) => { // OOXML preset formula (presetShapeDefinitions.xml -> leftRightUpArrow) const rawAdj2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 25000, 50000)) const maxAdj1 = rawAdj2 * 2 const rawAdj1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 25000, maxAdj1)) const q1 = 100000 - maxAdj1 const maxAdj3 = q1 / 2 const rawAdj3 = Math.max(0, Math.min(adjustments?.get('adj3') ?? 25000, maxAdj3)) const ss = Math.min(w, h) const hc = w / 2 const x1 = (ss * rawAdj3) / 100000 const dx2 = (ss * rawAdj2) / 100000 const x2 = hc - dx2 const x5 = hc + dx2 const dx3 = (ss * rawAdj1) / 200000 const x3 = hc - dx3 const x4 = hc + dx3 const x6 = w - x1 const dy2 = (ss * rawAdj2) / 50000 const y2 = h - dy2 const y4 = h - dx2 const y3 = y4 - dx3 const y5 = y4 + dx3 return [ `M0,${y4}`, `L${x1},${y2}`, `L${x1},${y3}`, `L${x3},${y3}`, `L${x3},${x1}`, `L${x2},${x1}`, `L${hc},0`, `L${x5},${x1}`, `L${x4},${x1}`, `L${x4},${y3}`, `L${x6},${y3}`, `L${x6},${y2}`, `L${w},${y4}`, `L${x6},${h}`, `L${x6},${y5}`, `L${x1},${y5}`, `L${x1},${h}`, 'Z', ].join(' ') }) presetShapes.set('swooshArrow', (w, h, adjustments) => { // OOXML swooshArrow: curved swoosh with arrowhead on the right. const ss = Math.min(w, h) const raw1 = adjustments?.get('adj1') ?? 25000 const raw2 = adjustments?.get('adj2') ?? 16667 const a1 = Math.max(1, Math.min(raw1, 75000)) const maxAdj2 = (70000 * w) / ss const a2 = Math.max(0, Math.min(raw2, maxAdj2)) const ad1 = (h * a1) / 100000 const ad2 = (ss * a2) / 100000 const ssd8 = ss / 8 const hd6 = h / 6 const alfa = Math.PI / 2 / 14 // cd4/14 in radians const tanAlfa = Math.tan(alfa) const xB = w - ad2 const yB = ssd8 const dx0 = ssd8 * tanAlfa const xC = xB - dx0 const dx1 = ad1 * tanAlfa const yF = yB + ad1 const xF = xB + dx1 const xE = xF + dx0 const yE = yF + ssd8 const dy2 = yE const dy22 = dy2 / 2 const dy3 = h / 20 const yD = dy22 + dy3 const xP1 = w / 6 const yP1 = hd6 + hd6 // h/3 const dy5 = hd6 / 2 const yP2 = yF + dy5 const xP2 = w / 4 return [ `M0,${h}`, `Q${xP1},${yP1} ${xB},${yB}`, `L${xC},0`, `L${w},${yD}`, `L${xE},${yE}`, `L${xF},${yF}`, `Q${xP2},${yP2} 0,${h}`, 'Z', ].join(' ') }) // ===== Flowchart Shapes ===== presetShapes.set('flowChartProcess', (w, h) => `M0,0 L${w},0 L${w},${h} L0,${h} Z`) presetShapes.set('flowChartDecision', (w, h) => { const cx = w / 2 const cy = h / 2 return `M${cx},0 L${w},${cy} L${cx},${h} L0,${cy} Z` }) presetShapes.set('flowChartTerminator', (w, h) => { // OOXML: path w=21600 h=21600, wR=3475, hR=10800 (elliptical caps, not circular) const x1 = (w * 3475) / 21600 const x2 = (w * 18125) / 21600 const wR = x1 // w * 3475/21600 const hR = h / 2 // h * 10800/21600 return [ `M${x1},0`, `L${x2},0`, `A${wR},${hR} 0 0,1 ${x2},${h}`, `L${x1},${h}`, `A${wR},${hR} 0 0,1 ${x1},0`, 'Z', ].join(' ') }) presetShapes.set('flowChartDocument', (w, h) => { // OOXML: path w=21600 h=21600, cubic (21600,17322)(10800,17322)(10800,23922)(0,20172) const y1 = (h * 17322) / 21600 const cy1 = y1 // h * 17322/21600 const cy2 = (h * 23922) / 21600 // extends below h (overshoot for curve) const y2 = (h * 20172) / 21600 return [`M0,0`, `L${w},0`, `L${w},${y1}`, `C${w / 2},${cy1} ${w / 2},${cy2} 0,${y2}`, 'Z'].join( ' ' ) }) presetShapes.set('flowChartInputOutput', (w, h) => { // OOXML: path w=5 h=5, points: (0,5)(1,0)(5,0)(4,5) — offset = w/5 const offset = w / 5 return `M${offset},0 L${w},0 L${w - offset},${h} L0,${h} Z` }) presetShapes.set('flowChartPredefinedProcess', (w, h) => { const inset = w * 0.1 return [ // Outer rectangle `M0,0 L${w},0 L${w},${h} L0,${h} Z`, // Left inner line `M${inset},0 L${inset},${h}`, // Right inner line `M${w - inset},0 L${w - inset},${h}`, ].join(' ') }) presetShapes.set('flowChartAlternateProcess', (w, h) => { // OOXML spec: corner radius = ssd6 = min(w,h)/6 const r = Math.min(w, h) / 6 return [ `M${r},0`, `L${w - r},0`, `A${r},${r} 0 0,1 ${w},${r}`, `L${w},${h - r}`, `A${r},${r} 0 0,1 ${w - r},${h}`, `L${r},${h}`, `A${r},${r} 0 0,1 0,${h - r}`, `L0,${r}`, `A${r},${r} 0 0,1 ${r},0`, 'Z', ].join(' ') }) presetShapes.set('flowChartManualInput', (w, h) => { const topOffset = h * 0.2 return `M0,${topOffset} L${w},0 L${w},${h} L0,${h} Z` }) presetShapes.set('flowChartManualOperation', (w, h) => { // OOXML: path w=5 h=5: (0,0)→(5,0)→(4,5)→(1,5)→close → inset = w/5 return `M0,0 L${w},0 L${(w * 4) / 5},${h} L${w / 5},${h} Z` }) presetShapes.set('flowChartPreparation', (w, h) => { const inset = w * 0.2 const cy = h / 2 return `M${inset},0 L${w - inset},0 L${w},${cy} L${w - inset},${h} L${inset},${h} L0,${cy} Z` }) presetShapes.set('flowChartData', (w, h) => { const offset = w * 0.15 return `M${offset},0 L${w},0 L${w - offset},${h} L0,${h} Z` }) presetShapes.set('flowChartInternalStorage', (w, h) => { const inset = Math.min(w, h) * 0.12 return [ `M0,0 L${w},0 L${w},${h} L0,${h} Z`, `M${inset},0 L${inset},${h}`, `M0,${inset} L${w},${inset}`, ].join(' ') }) presetShapes.set('flowChartMagneticDisk', (w, h) => { // OOXML spec: path w=6 h=6, top at y=1, arc hR=1 → ry = h/6 const ry = h / 6 const bodyTop = ry const bodyBottom = h - ry return [ // Top ellipse `M0,${bodyTop}`, `A${w / 2},${ry} 0 1,1 ${w},${bodyTop}`, // Right side down `L${w},${bodyBottom}`, // Bottom ellipse `A${w / 2},${ry} 0 1,1 0,${bodyBottom}`, // Left side up `L0,${bodyTop}`, 'Z', // Top ellipse visible arc (back half) `M${w},${bodyTop}`, `A${w / 2},${ry} 0 1,1 0,${bodyTop}`, ].join(' ') }) presetShapes.set('flowChartDelay', (w, h) => { // OOXML: M(0,0) L(hc,0) arcTo(wd2,hd2, 270°, 180°) L(0,h) Z // Arc from (hc,0) with wR=w/2 hR=h/2, stAng=270° swAng=180° → semicircle right side const hc = w / 2 const a = ooArcTo(hc, 0, hc, h / 2, 270, 180) return [`M0,0`, `L${hc},0`, a.svg, `L0,${h}`, 'Z'].join(' ') }) presetShapes.set('flowChartDisplay', (w, h) => { // OOXML: path w=6 h=6, points: (0,3)(1,0)(5,0) arcTo(1,3,270°,180°) (1,6) close // Scaled: left point at (0, h/2), top-left at (w/6, 0), arc center at (5w/6, h/2) const sx = w / 6 const sy = h / 6 const arcWR = sx // wR = 1 * (w/6) const arcHR = sy * 3 // hR = 3 * (h/6) = h/2 const a = ooArcTo(5 * sx, 0, arcWR, arcHR, 270, 180) return [`M0,${3 * sy}`, `L${sx},0`, `L${5 * sx},0`, a.svg, `L${sx},${h}`, 'Z'].join(' ') }) presetShapes.set('flowChartExtract', (w, h) => `M${w / 2},0 L${w},${h} L0,${h} Z`) presetShapes.set('flowChartMerge', (w, h) => `M0,0 L${w},0 L${w / 2},${h} Z`) presetShapes.set('flowChartOffpageConnector', (w, h) => { const arrowH = h * 0.2 return [`M0,0`, `L${w},0`, `L${w},${h - arrowH}`, `L${w / 2},${h}`, `L0,${h - arrowH}`, 'Z'].join( ' ' ) }) presetShapes.set('flowChartConnector', (w, h) => { const rx = w / 2 const ry = h / 2 return [`M${w},${ry}`, `A${rx},${ry} 0 1,1 0,${ry}`, `A${rx},${ry} 0 1,1 ${w},${ry}`, 'Z'].join( ' ' ) }) presetShapes.set('flowChartSort', (w, h) => { const cx = w / 2 const cy = h / 2 return [`M${cx},0 L${w},${cy} L${cx},${h} L0,${cy} Z`, `M0,${cy} L${w},${cy}`].join(' ') }) presetShapes.set('flowChartCollate', (w, h) => { const cx = w / 2 const cy = h / 2 return [ // top inverted triangle `M0,0 L${w},0 L${cx},${cy} Z`, // bottom upright triangle `M0,${h} L${w},${h} L${cx},${cy} Z`, ].join(' ') }) presetShapes.set('flowChartPunchedTape', (w, h) => { // OOXML: path w="20" h="20" with arcTo operations. // Start at (0, 2), four arcs for wavy top/bottom. const sx = w / 20 const sy = h / 20 const arcTo = ( curX: number, curY: number, wR: number, hR: number, stAng60k: number, swAng60k: number ) => { const stDeg = stAng60k / 60000 const swDeg = swAng60k / 60000 const stRad = (stDeg * Math.PI) / 180 const endRad = ((stDeg + swDeg) * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hR * Math.sin(stRad) const endX = cx + wR * Math.cos(endRad) const endY = cy + hR * Math.sin(endRad) const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 return { endX, endY, svg: `A${wR},${hR} 0 ${largeArc},${sweep} ${endX},${endY}` } } // cd2 = 10800000 (180°) const wR = 5 * sx const hR = 2 * sy let x = 0 let y = 2 * sy const parts = [`M${x},${y}`] // Top-left: stAng=cd2(180°), swAng=-cd2(-180°) → dips down let a = arcTo(x, y, wR, hR, 10800000, -10800000) parts.push(a.svg) x = a.endX y = a.endY // Top-right: stAng=cd2(180°), swAng=+cd2(+180°) → bumps up a = arcTo(x, y, wR, hR, 10800000, 10800000) parts.push(a.svg) x = a.endX y = a.endY // Line to bottom-right const bx = 20 * sx const by = 18 * sy parts.push(`L${bx},${by}`) x = bx y = by // Bottom-right: stAng=0, swAng=-cd2(-180°) → bumps up a = arcTo(x, y, wR, hR, 0, -10800000) parts.push(a.svg) x = a.endX y = a.endY // Bottom-left: stAng=0, swAng=+cd2(+180°) → dips down a = arcTo(x, y, wR, hR, 0, 10800000) parts.push(a.svg) parts.push('Z') return parts.join(' ') }) presetShapes.set('flowChartPunchedCard', (w, h) => { // OOXML spec: path w=5, h=5. Points: (0,1)(1,0)(5,0)(5,5)(0,5) const sx = w / 5 const sy = h / 5 return `M0,${sy} L${sx},0 L${w},0 L${w},${h} L0,${h} Z` }) presetShapes.set('flowChartSummingJunction', (w, h) => { // OOXML: Circle with X cross. Returns single path with circle + X lines. const wd2 = w / 2 const hd2 = h / 2 const idx = wd2 * Math.cos(Math.PI / 4) // cos(45°) const idy = hd2 * Math.sin(Math.PI / 4) const il = wd2 - idx const ir = wd2 + idx const it = hd2 - idy const ib = hd2 + idy return [ // Circle `M0,${hd2}`, `A${wd2},${hd2} 0 1,1 ${w},${hd2}`, `A${wd2},${hd2} 0 1,1 0,${hd2}`, 'Z', // X cross `M${il},${it} L${ir},${ib}`, `M${ir},${it} L${il},${ib}`, ].join(' ') }) presetShapes.set('flowChartOr', (w, h) => { // OOXML: Circle with + cross. const wd2 = w / 2 const hd2 = h / 2 return [ // Circle `M0,${hd2}`, `A${wd2},${hd2} 0 1,1 ${w},${hd2}`, `A${wd2},${hd2} 0 1,1 0,${hd2}`, 'Z', // + cross `M${wd2},0 L${wd2},${h}`, `M0,${hd2} L${w},${hd2}`, ].join(' ') }) presetShapes.set('flowChartOnlineStorage', (w, h) => { // OOXML: Rounded left side rectangle with concave right cap. // Normalized: left arc (convex) at x=w/6, right arc (concave) at x=w const x1 = w / 6 return [ `M${x1},0`, `L${w},0`, `A${x1},${h / 2} 0 0,0 ${w},${h}`, `L${x1},${h}`, `A${x1},${h / 2} 0 0,1 ${x1},0`, 'Z', ].join(' ') }) presetShapes.set('flowChartMagneticDrum', (w, h) => { // OOXML: Horizontal cylinder (magnetic drum). Right ellipse cap visible. const x1 = w / 6 const x2 = (w * 5) / 6 const ry = h / 2 return [ // Body `M${x1},0`, `L${x2},0`, `A${x1},${ry} 0 0,1 ${x2},${h}`, `L${x1},${h}`, `A${x1},${ry} 0 0,1 ${x1},0`, 'Z', // Right ellipse back-face (visible part) `M${x2},${h}`, `A${x1},${ry} 0 0,1 ${x2},0`, ].join(' ') }) presetShapes.set('flowChartMagneticTape', (w, h) => { // OOXML: Nearly full ellipse (circle) with a tape tail to the bottom-right. // 3 quarter-arcs (270°) + partial arc of ang1 = at2(w,h) = atan2(h,w), // then line to (r, ib) → (r, b) → close. const wd2 = w / 2 const hd2 = h / 2 const hc = wd2 const vc = hd2 const ang1 = Math.atan2(h, w) // OOXML at2 w h = atan2(h, w) const ib = vc + hd2 * Math.sin(Math.PI / 4) // sin(45°) * hd2 // arcTo helper: compute SVG arc from OOXML arcTo parameters const arcTo = ( curX: number, curY: number, wR: number, hR: number, stDeg: number, swDeg: number ) => { const stRad = (stDeg * Math.PI) / 180 const endRad = ((stDeg + swDeg) * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hR * Math.sin(stRad) const endX = cx + wR * Math.cos(endRad) const endY = cy + hR * Math.sin(endRad) const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 return { endX, endY, svg: `A${wR},${hR} 0 ${largeArc},${sweep} ${endX},${endY}` } } // Start at bottom center: M(hc, b) let curX = hc let curY = h const a1 = arcTo(curX, curY, wd2, hd2, 90, 90) // cd4, cd4 → 90° to 180° curX = a1.endX curY = a1.endY const a2 = arcTo(curX, curY, wd2, hd2, 180, 90) // cd2, cd4 → 180° to 270° curX = a2.endX curY = a2.endY const a3 = arcTo(curX, curY, wd2, hd2, 270, 90) // 3cd4, cd4 → 270° to 360° curX = a3.endX curY = a3.endY const ang1Deg = (ang1 * 180) / Math.PI const a4 = arcTo(curX, curY, wd2, hd2, 0, ang1Deg) // 0, ang1 return [`M${hc},${h}`, a1.svg, a2.svg, a3.svg, a4.svg, `L${w},${ib}`, `L${w},${h}`, 'Z'].join(' ') }) presetShapes.set('flowChartMultidocument', (w, h) => { // OOXML: 21600-unit coordinates. Three stacked documents with cubic bezier waves. const s = (x: number) => (w * x) / 21600 const t = (y: number) => (h * y) / 21600 return [ // Front doc (bottom layer, with wave) `M0,${t(20782)}`, `C${s(9298)},${t(23542)} ${s(9298)},${t(18022)} ${s(18595)},${t(18022)}`, `L${s(18595)},${t(3675)} L0,${t(3675)} Z`, // Middle doc `M${s(1532)},${t(3675)} L${s(1532)},${t(1815)} L${s(20000)},${t(1815)}`, `L${s(20000)},${t(16252)}`, `C${s(19298)},${t(16252)} ${s(18595)},${t(16352)} ${s(18595)},${t(16352)}`, `L${s(18595)},${t(3675)} Z`, // Back doc (top layer) `M${s(2972)},${t(1815)} L${s(2972)},0 L${w},0`, `L${w},${t(14392)}`, `C${s(20800)},${t(14392)} ${s(20000)},${t(14467)} ${s(20000)},${t(14467)}`, `L${s(20000)},${t(1815)} Z`, ].join(' ') }) // ===== Callout Shapes ===== presetShapes.set('wedgeRectCallout', (w, h, adjustments) => { // OOXML spec: adaptive callout pointer on the edge closest to the tip const hc = w / 2 const vc = h / 2 const dxPos = (w * (adjustments?.get('adj1') ?? -20833)) / 100000 const dyPos = (h * (adjustments?.get('adj2') ?? 62500)) / 100000 const xPos = hc + dxPos const yPos = vc + dyPos const dq = (dxPos * h) / w const ady = Math.abs(dyPos) const adq = Math.abs(dq) const dz = ady - adq // Notch bracket positions (7/12 or 2/12 depending on tip direction) const x1 = (w * (dxPos >= 0 ? 7 : 2)) / 12 const x2 = (w * (dxPos >= 0 ? 10 : 5)) / 12 const y1 = (h * (dyPos >= 0 ? 7 : 2)) / 12 const y2 = (h * (dyPos >= 0 ? 10 : 5)) / 12 // Conditional notch points per edge (collapse to edge if not the active edge) const xl = dz > 0 ? 0 : dxPos >= 0 ? 0 : xPos const xt = dz > 0 ? (dyPos >= 0 ? x1 : xPos) : x1 const xr = dz > 0 ? w : dxPos >= 0 ? xPos : w const xb = dz > 0 ? (dyPos >= 0 ? xPos : x1) : x1 const yl = dz > 0 ? y1 : dxPos >= 0 ? y1 : yPos const yt = dz > 0 ? (dyPos >= 0 ? 0 : yPos) : 0 const yr = dz > 0 ? y1 : dxPos >= 0 ? yPos : y1 const yb = dz > 0 ? (dyPos >= 0 ? yPos : h) : h return [ `M0,0`, `L${x1},0`, `L${xt},${yt}`, `L${x2},0`, `L${w},0`, `L${w},${y1}`, `L${xr},${yr}`, `L${w},${y2}`, `L${w},${h}`, `L${x2},${h}`, `L${xb},${yb}`, `L${x1},${h}`, `L0,${h}`, `L0,${y2}`, `L${xl},${yl}`, `L0,${y1}`, 'Z', ].join(' ') }) presetShapes.set('wedgeRoundRectCallout', (w, h, adjustments) => { // OOXML spec: rounded rect with adaptive callout pointer const hc = w / 2 const vc = h / 2 const ss = Math.min(w, h) const dxPos = (w * (adjustments?.get('adj1') ?? -20833)) / 100000 const dyPos = (h * (adjustments?.get('adj2') ?? 62500)) / 100000 const u1 = (ss * (adjustments?.get('adj3') ?? 16667)) / 100000 const xPos = hc + dxPos const yPos = vc + dyPos const dq = (dxPos * h) / w const ady = Math.abs(dyPos) const adq = Math.abs(dq) const dz = ady - adq const u2 = w - u1 const v2 = h - u1 const x1 = (w * (dxPos >= 0 ? 7 : 2)) / 12 const x2 = (w * (dxPos >= 0 ? 10 : 5)) / 12 const y1 = (h * (dyPos >= 0 ? 7 : 2)) / 12 const y2 = (h * (dyPos >= 0 ? 10 : 5)) / 12 const xl = dz > 0 ? 0 : dxPos >= 0 ? 0 : xPos const xt = dz > 0 ? (dyPos >= 0 ? x1 : xPos) : x1 const xr = dz > 0 ? w : dxPos >= 0 ? xPos : w const xb = dz > 0 ? (dyPos >= 0 ? xPos : x1) : x1 const yl = dz > 0 ? y1 : dxPos >= 0 ? y1 : yPos const yt = dz > 0 ? (dyPos >= 0 ? 0 : yPos) : 0 const yr = dz > 0 ? y1 : dxPos >= 0 ? yPos : y1 const yb = dz > 0 ? (dyPos >= 0 ? yPos : h) : h return [ `M0,${u1}`, `A${u1},${u1} 0 0,1 ${u1},0`, `L${x1},0`, `L${xt},${yt}`, `L${x2},0`, `L${u2},0`, `A${u1},${u1} 0 0,1 ${w},${u1}`, `L${w},${y1}`, `L${xr},${yr}`, `L${w},${y2}`, `L${w},${v2}`, `A${u1},${u1} 0 0,1 ${u2},${h}`, `L${x2},${h}`, `L${xb},${yb}`, `L${x1},${h}`, `L${u1},${h}`, `A${u1},${u1} 0 0,1 0,${v2}`, `L0,${y2}`, `L${xl},${yl}`, `L0,${y1}`, 'Z', ].join(' ') }) presetShapes.set('wedgeEllipseCallout', (w, h, adjustments) => { const ax = adj(adjustments, 'adj1', -20833) const ay = adj(adjustments, 'adj2', 62500) const rx = w / 2 const ry = h / 2 const tipX = rx + w * ax const tipY = ry + h * ay // Approximate: ellipse with a pointer const angle = Math.atan2(tipY - ry, tipX - rx) const gapAngle = 0.15 const _x1 = rx + rx * Math.cos(angle - gapAngle) const _y1 = ry + ry * Math.sin(angle - gapAngle) const _x2 = rx + rx * Math.cos(angle + gapAngle) const _y2 = ry + ry * Math.sin(angle + gapAngle) return [ shapeArc( rx, ry, rx, ry, ((angle + gapAngle) * 180) / Math.PI, ((angle - gapAngle + 2 * Math.PI) * 180) / Math.PI, false ), `L${tipX},${tipY}`, 'Z', ].join(' ') }) presetShapes.set('cloudCallout', (w, h, adjustments) => { const ax = adj(adjustments, 'adj1', -20833) const ay = adj(adjustments, 'adj2', 62500) const tipX = w / 2 + w * ax const tipY = h / 2 + h * ay // Simplified cloud with callout circles const cloud = presetShapes.get('cloud')!(w, h) // Small circles leading to tip const cx = w / 2 const cy = h / 2 const dx = tipX - cx const dy = tipY - cy const r1 = Math.min(w, h) * 0.04 const r2 = Math.min(w, h) * 0.025 const c1x = cx + dx * 0.5 const c1y = cy + dy * 0.5 const c2x = cx + dx * 0.75 const c2y = cy + dy * 0.75 return [ cloud, // Connector circles (approximated as small ellipses) `M${c1x + r1},${c1y} A${r1},${r1} 0 1,1 ${c1x - r1},${c1y} A${r1},${r1} 0 1,1 ${c1x + r1},${c1y} Z`, `M${c2x + r2},${c2y} A${r2},${r2} 0 1,1 ${c2x - r2},${c2y} A${r2},${r2} 0 1,1 ${c2x + r2},${c2y} Z`, ].join(' ') }) presetShapes.set('borderCallout1', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 112500)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -38333)) / 100000 return `M0,0 L${w},0 L${w},${h} L0,${h} Z M${x1},${y1} L${x2},${y2}` }) // ===== Block / 3D Shapes ===== presetShapes.set('cube', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 25000) const depth = Math.min(w, h) * a return [ // Front face `M0,${depth} L${w - depth},${depth} L${w - depth},${h} L0,${h} Z`, // Top face `M0,${depth} L${depth},0 L${w},0 L${w - depth},${depth} Z`, // Right face `M${w - depth},${depth} L${w},0 L${w},${h - depth} L${w - depth},${h} Z`, ].join(' ') }) // can is implemented as multiPathPreset (see multiPathPresets below) // ribbon2 is implemented as multiPathPreset (see multiPathPresets below) presetShapes.set('plus', (w, h, adjustments) => { // OOXML: adj=25000 (max 50000), x1 = ss * a / 100000 (uses ss for both x and y) const ss = Math.min(w, h) const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 25000), 0), 50000) const x1 = (ss * a) / 100000 const x2 = w - x1 const y2 = h - x1 return [ `M0,${x1}`, `L${x1},${x1}`, `L${x1},0`, `L${x2},0`, `L${x2},${x1}`, `L${w},${x1}`, `L${w},${y2}`, `L${x2},${y2}`, `L${x2},${h}`, `L${x1},${h}`, `L${x1},${y2}`, `L0,${y2}`, 'Z', ].join(' ') }) presetShapes.set('heart', (w, h) => { // OOXML spec: two cubic beziers from (hc, hd4) through (hc, b) and back. // dx1 = w*49/48 (slightly wider than w/2), dx2 = w*10/48 // y1 = t - hd3 (above top edge) const hc = w / 2 const hd4 = h / 4 const hd3 = h / 3 const dx1 = (w * 49) / 48 const dx2 = (w * 10) / 48 const x1 = hc - dx1 // far left control const x2 = hc - dx2 // inner left control const x3 = hc + dx2 // inner right control const x4 = hc + dx1 // far right control const y1 = -hd3 // above top (negative y) return [ `M${hc},${hd4}`, `C${x3},${y1} ${x4},${hd4} ${hc},${h}`, `C${x1},${hd4} ${x2},${y1} ${hc},${hd4}`, 'Z', ].join(' ') }) presetShapes.set('cloud', (w, h) => { // OOXML cloud: 11 arcTo operations in 43200×43200 coordinate space const sx = w / 43200 const sy = h / 43200 // OOXML arcTo: wR/hR are radii, stAng/swAng in 60000ths of degree const arcs: [number, number, number, number][] = [ [6753, 9190, -11429249, 7426832], [5333, 7267, -8646143, 5396714], [4365, 5945, -8748475, 5983381], [4857, 6595, -7859164, 7034504], [5333, 7273, -4722533, 6541615], [6775, 9220, -2776035, 7816140], [5785, 7867, 37501, 6842000], [6752, 9215, 1347096, 6910353], [7720, 10543, 3974558, 4542661], [4360, 5918, -16496525, 8804134], [4345, 5945, -14809710, 9151131], ] let curX = 3900 * sx let curY = 14370 * sy const parts = [`M${curX},${curY}`] // Track position in unscaled 43200×43200 space for accurate arcTo computation. // OOXML arcTo angles are visual (geometric ray) angles in the path coordinate space. // Convert to parametric before computing center/endpoint positions. let ux = 3900 let uy = 14370 // unscaled current position for (const [wR, hR, stAng60k, swAng60k] of arcs) { const stDeg = stAng60k / 60000 const swDeg = swAng60k / 60000 // Visual→parametric using UNSCALED radii (path coordinate space) const stVisRad = (stDeg * Math.PI) / 180 const stRad = Math.atan2(wR * Math.sin(stVisRad), hR * Math.cos(stVisRad)) const endVisRad = ((stDeg + swDeg) * Math.PI) / 180 const endRad = Math.atan2(wR * Math.sin(endVisRad), hR * Math.cos(endVisRad)) // Compute center and endpoint in unscaled space const acx = ux - wR * Math.cos(stRad) const acy = uy - hR * Math.sin(stRad) const endUX = acx + wR * Math.cos(endRad) const endUY = acy + hR * Math.sin(endRad) // Scale to pixel space for SVG output const endX = endUX * sx const endY = endUY * sy const rwS = wR * sx const rhS = hR * sy const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 parts.push(`A${rwS},${rhS} 0 ${largeArc},${sweep} ${endX},${endY}`) ux = endUX uy = endUY curX = endX curY = endY } parts.push('Z') return parts.join(' ') }) // ===== Frame, Donut, Misc ===== presetShapes.set('frame', (w, h, adjustments) => { const a = adj(adjustments, 'adj1', 12500) const t = Math.min(w, h) * a return [ // Outer rectangle `M0,0 L${w},0 L${w},${h} L0,${h} Z`, // Inner rectangle (counter-clockwise for hole) `M${t},${t} L${t},${h - t} L${w - t},${h - t} L${w - t},${t} Z`, ].join(' ') }) presetShapes.set('halfFrame', (w, h, adjustments) => { // OOXML spec defaults: adj1=33333, adj2=33333 const adj1Raw = adjustments?.get('adj1') ?? 33333 const adj2Raw = adjustments?.get('adj2') ?? 33333 const minWH = Math.min(w, h) const a2 = Math.max(0, Math.min(adj2Raw, (100000 * w) / Math.max(minWH, 1))) const x1 = (minWH * a2) / 100000 const g1 = (h * x1) / Math.max(w, 1) const g2 = h - g1 const a1 = Math.max(0, Math.min(adj1Raw, (100000 * g2) / Math.max(minWH, 1))) const y1 = (minWH * a1) / 100000 const x2 = w - (y1 * w) / Math.max(h, 1) const y2 = h - (x1 * h) / Math.max(w, 1) return [`M0,0`, `L${w},0`, `L${x2},${y1}`, `L${x1},${y1}`, `L${x1},${y2}`, `L0,${h}`, 'Z'].join( ' ' ) }) presetShapes.set('donut', (w, h, adjustments) => { // OOXML: adj=25000, dr = ss * a / 100000, inner radii = wd2-dr, hd2-dr const ss = Math.min(w, h) const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 25000), 0), 50000) const dr = (ss * a) / 100000 const rx = w / 2 const ry = h / 2 const iwd2 = Math.max(0, rx - dr) const ihd2 = Math.max(0, ry - dr) return [ // Outer circle (CW) `M0,${ry}`, `A${rx},${ry} 0 1,1 ${w},${ry}`, `A${rx},${ry} 0 1,1 0,${ry}`, 'Z', // Inner circle (CCW for evenodd hole) `M${dr},${ry}`, `A${iwd2},${ihd2} 0 1,0 ${w - dr},${ry}`, `A${iwd2},${ihd2} 0 1,0 ${dr},${ry}`, 'Z', ].join(' ') }) presetShapes.set('noSmoking', (w, h, adjustments) => { // OOXML: adj=18750. Ring thickness = ss*a/100000. Diagonal band via inner ellipse arcs + evenodd. const ss = Math.min(w, h) const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 18750), 0), 50000) const dr = (ss * a) / 100000 const rx = w / 2 const ry = h / 2 const hc = w / 2 const vc = h / 2 const iwd2 = rx - dr const ihd2 = ry - dr // Compute diagonal angle and band intersection with inner ellipse const ang = Math.atan2(h, w) // at2(w, h) in OOXML: at2 x y = atan2(y, x) // Inner ellipse radius at diagonal angle const ct = ihd2 * Math.cos(ang) const st = iwd2 * Math.sin(ang) const m = Math.sqrt(ct * ct + st * st) || 1 const n = (iwd2 * ihd2) / m const drd2 = dr / 2 const dang = Math.atan2(drd2, n) const dang2 = dang * 2 // Sweep for inner arcs: -(180° - dang2) expressed as OOXML 60000ths then converted const swAngRad = -(Math.PI - dang2) const stAng1 = ang - dang const stAng2 = stAng1 - Math.PI // Compute points on inner ellipse for the two diagonal band arcs const innerPt = (angle: number) => { const ct2 = ihd2 * Math.cos(angle) const st2 = iwd2 * Math.sin(angle) const m2 = Math.sqrt(ct2 * ct2 + st2 * st2) || 1 const n2 = (iwd2 * ihd2) / m2 return { x: hc + n2 * Math.cos(angle), y: vc + n2 * Math.sin(angle) } } const p1 = innerPt(stAng1) const p2 = innerPt(stAng2) // End points of arcs const endAng1 = stAng1 + swAngRad const endAng2 = stAng2 + swAngRad const e1 = innerPt(endAng1) const e2 = innerPt(endAng2) const largeArc = Math.abs(swAngRad) > Math.PI ? 1 : 0 const sweep = swAngRad > 0 ? 1 : 0 return [ // Outer circle (CW) `M0,${vc}`, `A${rx},${ry} 0 1,1 ${w},${vc}`, `A${rx},${ry} 0 1,1 0,${vc}`, 'Z', // First diagonal band arc (inner ellipse) `M${p1.x},${p1.y}`, `A${iwd2},${ihd2} 0 ${largeArc},${sweep} ${e1.x},${e1.y}`, 'Z', // Second diagonal band arc (opposite quadrant) `M${p2.x},${p2.y}`, `A${iwd2},${ihd2} 0 ${largeArc},${sweep} ${e2.x},${e2.y}`, 'Z', ].join(' ') }) presetShapes.set('blockArc', (w, h, adjustments) => { const adj1Raw = adjustments?.get('adj1') ?? 10800000 // start angle const adj2Raw = adjustments?.get('adj2') ?? 0 // sweep/end angle const adj3Raw = adjustments?.get('adj3') ?? 25000 // thickness ratio const startDeg = Math.min(Math.max(adj1Raw / 60000, 0), 360) const innerStartDeg = Math.min(Math.max(adj2Raw / 60000, 0), 360) const sweepDeg = (innerStartDeg - startDeg + 360) % 360 || 360 const endDeg = startDeg + sweepDeg const innerEndDeg = innerStartDeg - sweepDeg const wd2 = w / 2 const hd2 = h / 2 const dr = (Math.min(w, h) * Math.max(0, Math.min(adj3Raw, 50000))) / 100000 const iwd2 = Math.max(1, wd2 - dr) const ihd2 = Math.max(1, hd2 - dr) const p = (cx: number, cy: number, rx: number, ry: number, deg: number) => { const r = (deg * Math.PI) / 180 return { x: cx + rx * Math.cos(r), y: cy + ry * Math.sin(r) } } const oStart = p(wd2, hd2, wd2, hd2, startDeg) const oEnd = p(wd2, hd2, wd2, hd2, endDeg) const iStart = p(wd2, hd2, iwd2, ihd2, innerStartDeg) const iEnd = p(wd2, hd2, iwd2, ihd2, innerEndDeg) const largeArc = sweepDeg > 180 ? 1 : 0 return [ `M${oStart.x},${oStart.y}`, `A${wd2},${hd2} 0 ${largeArc},1 ${oEnd.x},${oEnd.y}`, `L${iStart.x},${iStart.y}`, `A${iwd2},${ihd2} 0 ${largeArc},0 ${iEnd.x},${iEnd.y}`, 'Z', ].join(' ') }) // ===== Gear Shapes ===== presetShapes.set('gear6', (w, h, adjustments) => { const a1 = adjustments?.get('adj1') ?? 15000 const a2 = adjustments?.get('adj2') ?? 3526 return gearShape(w, h, 6, a1, a2) }) presetShapes.set('gear9', (w, h, adjustments) => { const a1 = adjustments?.get('adj1') ?? 10000 const a2 = adjustments?.get('adj2') ?? 1763 return gearShape(w, h, 9, a1, a2) }) function gearShape(w: number, h: number, teeth: number, adj1Raw: number, adj2Raw: number): string { // Gear shape: teeth protrude from inner ellipse by th, narrowed by lFD at tips. // Uses per-tooth edge-perpendicular computation for B/C tip direction. const cx = w / 2 const cy = h / 2 const ss = Math.min(w, h) const maxAdj2 = teeth === 6 ? 5358 : 2679 const a1v = Math.min(Math.max(adj1Raw, 0), 20000) const a2v = Math.min(Math.max(adj2Raw, 0), maxAdj2) const th = (ss * a1v) / 100000 // tooth height const lFD = (ss * a2v) / 100000 // tooth flat distance offset const rw = w / 2 - th // inner ellipse width radius const rh = h / 2 - th // inner ellipse height radius if (rw <= 0 || rh <= 0) return `M0,0 L${w},0 L${w},${h} L0,${h} Z` // OOXML: ha = at2(maxr, l3) where maxr=min(rw,rh), l3=th/2+lFD/2 const l3 = th / 2 + lFD / 2 const maxr = Math.min(rw, rh) const ha = Math.atan2(l3, maxr) // half-angle of each tooth on the inner ellipse const centerDegs = teeth === 6 ? [330, 30, 90, 150, 210, 270] : [310, 350, 30, 70, 110, 150, 190, 230, 270] const parts: string[] = [] for (let i = 0; i < centerDegs.length; i++) { const baseAngle = (centerDegs[i] * Math.PI) / 180 const aStart = baseAngle - ha // tooth base start angle (A point) const aEnd = baseAngle + ha // tooth base end angle (D point) // A and D: inner ellipse points at tooth base edges const ax = cx + rw * Math.cos(aStart) const ay = cy + rh * Math.sin(aStart) const dx = cx + rw * Math.cos(aEnd) const dy = cy + rh * Math.sin(aEnd) // Per-tooth edge-perpendicular tip computation: // Edge direction A→D const edgeX = dx - ax const edgeY = dy - ay const edgeLen = Math.sqrt(edgeX * edgeX + edgeY * edgeY) // Unit normal perpendicular to edge, pointing outward // For clockwise winding (our standard), outward normal is (-edgeY, edgeX) / len // Verify with radial dot product and flip if needed let nx = -edgeY / edgeLen let ny = edgeX / edgeLen const radX = Math.cos(baseAngle) const radY = Math.sin(baseAngle) if (nx * radX + ny * radY < 0) { nx = -nx ny = -ny } // Narrowing: slide A and D inward along edge by lFD const ex = edgeLen > 0 ? edgeX / edgeLen : 0 const ey = edgeLen > 0 ? edgeY / edgeLen : 0 const axN = ax + ex * lFD // A narrowed (moved toward D) const ayN = ay + ey * lFD const dxN = dx - ex * lFD // D narrowed (moved toward A) const dyN = dy - ey * lFD // B and C: tip points = narrowed base + th * outward normal const bx = axN + nx * th const by = ayN + ny * th const _cx = dxN + nx * th const _cy = dyN + ny * th if (i === 0) { // Start at the valley before first tooth const prevEnd = (centerDegs[centerDegs.length - 1] * Math.PI) / 180 + ha const prevIx = cx + rw * Math.cos(prevEnd) const prevIy = cy + rh * Math.sin(prevEnd) parts.push(`M${prevIx},${prevIy}`) parts.push(`A${rw},${rh} 0 0,1 ${ax},${ay}`) } // Tooth: A→B→C→D parts.push(`L${bx},${by}`) parts.push(`L${_cx},${_cy}`) parts.push(`L${dx},${dy}`) // Arc along inner ring to next tooth if (i < centerDegs.length - 1) { const nextStart = (centerDegs[i + 1] * Math.PI) / 180 - ha const nx2 = cx + rw * Math.cos(nextStart) const ny2 = cy + rh * Math.sin(nextStart) parts.push(`A${rw},${rh} 0 0,1 ${nx2},${ny2}`) } } parts.push('Z') return parts.join(' ') } // ===== Misc Shapes ===== presetShapes.set('mathPlus', (w, h, adjustments) => { // OOXML: adj1=23520 (max 73490). dx1 = w*73490/200000, dx2 = ss*a/200000 const ss = Math.min(w, h) const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj', 23520), 0), 73490) const dx1 = (w * 73490) / 200000 const dy1 = (h * 73490) / 200000 const dx2 = (ss * a1) / 200000 const hc = w / 2 const vc = h / 2 const x1 = hc - dx1 const x2 = hc - dx2 const x3 = hc + dx2 const x4 = hc + dx1 const y1 = vc - dy1 const y2 = vc - dx2 const y3 = vc + dx2 const y4 = vc + dy1 return [ `M${x1},${y2}`, `L${x2},${y2}`, `L${x2},${y1}`, `L${x3},${y1}`, `L${x3},${y2}`, `L${x4},${y2}`, `L${x4},${y3}`, `L${x3},${y3}`, `L${x3},${y4}`, `L${x2},${y4}`, `L${x2},${y3}`, `L${x1},${y3}`, 'Z', ].join(' ') }) presetShapes.set('mathMinus', (w, h, adjustments) => { // OOXML: adj1=23520 (max 100000). dy1 = h*a1/200000, dx1 = w*73490/200000 const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 23520), 0), 100000) const dy1 = (h * a1) / 200000 const dx1 = (w * 73490) / 200000 const hc = w / 2 const vc = h / 2 const x1 = hc - dx1 const x2 = hc + dx1 const y1 = vc - dy1 const y2 = vc + dy1 return `M${x1},${y1} L${x2},${y1} L${x2},${y2} L${x1},${y2} Z` }) presetShapes.set('mathMultiply', (w, h, adjustments) => { // OOXML: adj1=23520 (max 51965). X shape with diagonal arms. // Key: a = at2 w h → atan2(w, h), coordinates are absolute from top-left. const ss = Math.min(w, h) const hc = w / 2 const vc = h / 2 const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 23520), 0), 51965) const th = (ss * a1) / 100000 const a = Math.atan2(h, w) const sa = Math.sin(a) const ca = Math.cos(a) const ta = sa / ca // tan(a) const dl = Math.sqrt(w * w + h * h) const rw = (dl * 51965) / 100000 const lM = dl - rw // xM, yM: half-distance along the diagonal from the outer tip to the outer tip const xM = (ca * lM) / 2 const yM = (sa * lM) / 2 // Perpendicular offset for arm thickness const dxAM = (sa * th) / 2 const dyAM = (ca * th) / 2 // xA, yA = upper-left outer tip (left side of arm), coordinates from (0,0) const xA = xM - dxAM const yA = yM + dyAM const xB = xM + dxAM const yB = yM - dyAM // yC = center notch: where the inner edge of one arm meets the inner edge of the other const xBC = hc - xB const yBC = xBC * ta const yC = yBC + yB // Mirror points for upper-right quadrant const xD = w - xB const xE = w - xA // xF: where the arm inner edge meets vc (center y) const yFE = vc - yA const xFE = yFE / ta const xF = xE - xFE const xL = xA + xFE // Bottom half mirrors const yG = h - yA const yH = h - yB const yI = h - yC return [ `M${xA},${yA}`, `L${xB},${yB}`, `L${hc},${yC}`, `L${xD},${yB}`, `L${xE},${yA}`, `L${xF},${vc}`, `L${xE},${yG}`, `L${xD},${yH}`, `L${hc},${yI}`, `L${xB},${yH}`, `L${xA},${yG}`, `L${xL},${vc}`, 'Z', ].join(' ') }) presetShapes.set('mathDivide', (w, h, adjustments) => { const adj1 = adjustments?.get('adj1') ?? 23520 const adj2 = adjustments?.get('adj2') ?? 5880 const adj3 = adjustments?.get('adj3') ?? 11760 const a1 = Math.min(Math.max(adj1, 1000), 36745) const maxAdj3 = Math.min((73490 - a1) / 4, (36745 * w) / Math.max(h, 1)) const a3 = Math.min(Math.max(adj3, 1000), maxAdj3) const maxAdj2 = 73490 - 4 * a3 - a1 const a2 = Math.min(Math.max(adj2, 0), maxAdj2) const hc = w / 2 const vc = h / 2 const dy1 = (h * a1) / 200000 const yg = (h * a2) / 100000 const rad = (h * a3) / 100000 const dx1 = (w * 73490) / 200000 const y3 = vc - dy1 const y4 = vc + dy1 const y2 = y3 - (yg + rad) const y1 = y2 - rad const y5 = h - y1 const x1 = hc - dx1 const x3 = hc + dx1 return [ // Top dot `M${hc + rad},${y1 + rad} A${rad},${rad} 0 1,1 ${hc - rad},${y1 + rad} A${rad},${rad} 0 1,1 ${hc + rad},${y1 + rad} Z`, // Bottom dot `M${hc + rad},${y5 - rad} A${rad},${rad} 0 1,1 ${hc - rad},${y5 - rad} A${rad},${rad} 0 1,1 ${hc + rad},${y5 - rad} Z`, // Bar `M${x1},${y3} L${x3},${y3} L${x3},${y4} L${x1},${y4} Z`, ].join(' ') }) presetShapes.set('mathEqual', (w, h, adjustments) => { // OOXML: adj1=23520 (bar thickness, max 36745), adj2=11760 (gap, max 100000-2*a1) const adj1Raw = adjustments?.get('adj1') ?? 23520 const adj2Raw = adjustments?.get('adj2') ?? 11760 const a1 = Math.min(Math.max(adj1Raw, 0), 36745) const mAdj2 = 100000 - a1 * 2 const a2 = Math.min(Math.max(adj2Raw, 0), Math.max(mAdj2, 0)) const dy1 = (h * a1) / 100000 const dy2 = (h * a2) / 200000 const dx1 = (w * 73490) / 200000 const hc = w / 2 const vc = h / 2 const y2 = vc - dy2 // center of top bar const y3 = vc + dy2 // center of bottom bar const y1 = y2 - dy1 // top of top bar const y4 = y3 + dy1 // bottom of bottom bar const x1 = hc - dx1 const x2 = hc + dx1 return [ `M${x1},${y1} L${x2},${y1} L${x2},${y2} L${x1},${y2} Z`, `M${x1},${y3} L${x2},${y3} L${x2},${y4} L${x1},${y4} Z`, ].join(' ') }) presetShapes.set('mathNotEqual', (w, h, adjustments) => { // Follow OOXML mathNotEqual geometry (single closed contour), which keeps // bar thickness/slash width and intersections aligned with PowerPoint. const adj1Raw = adjustments?.get('adj1') ?? 23520 const adj2Raw = adjustments?.get('adj2') const adj3Raw = adjustments?.get('adj3') ?? 11760 const hc = w / 2 const vc = h / 2 const hd2 = h / 2 const a1 = Math.min(Math.max(adj1Raw, 0), 50000) const crAng = (() => { if (adj2Raw === undefined) return (110 * Math.PI) / 180 const rad = ((adj2Raw / 60000) * Math.PI) / 180 const min = (70 * Math.PI) / 180 const max = (110 * Math.PI) / 180 return Math.min(Math.max(rad, min), max) })() const maxAdj3 = 100000 - a1 * 2 const a3 = Math.min(Math.max(adj3Raw, 0), maxAdj3) const dy1 = (h * a1) / 100000 const dy2 = (h * a3) / 200000 const dx1 = (w * 73490) / 200000 const x1 = hc - dx1 const x8 = hc + dx1 const y2 = vc - dy2 const y3 = vc + dy2 const y1 = y2 - dy1 const y4 = y3 + dy1 const cadj2 = crAng - Math.PI / 2 const xadj2 = hd2 * Math.tan(cadj2) const len = Math.hypot(xadj2, hd2) || 1 const bhw = (len * dy1) / hd2 const bhw2 = bhw / 2 const x7 = hc + xadj2 - bhw2 const x6 = x7 - (xadj2 * y1) / hd2 const x5 = x7 - (xadj2 * y2) / hd2 const x4 = x7 - (xadj2 * y3) / hd2 const x3 = x7 - (xadj2 * y4) / hd2 const rx7 = x7 + bhw const rx6 = x6 + bhw const rx5 = x5 + bhw const rx4 = x4 + bhw const rx3 = x3 + bhw const dx7 = (dy1 * hd2) / len const rx = cadj2 > 0 ? x7 + dx7 : rx7 const lx = cadj2 > 0 ? x7 : rx7 - dx7 const dy3 = (dy1 * xadj2) / len const ry = cadj2 > 0 ? dy3 : 0 const ly = cadj2 > 0 ? 0 : -dy3 const dlx = w - rx const drx = w - lx const dly = h - ry const dry = h - ly return [ `M${x1},${y1}`, `L${x6},${y1}`, `L${lx},${ly}`, `L${rx},${ry}`, `L${rx6},${y1}`, `L${x8},${y1}`, `L${x8},${y2}`, `L${rx5},${y2}`, `L${rx4},${y3}`, `L${x8},${y3}`, `L${x8},${y4}`, `L${rx3},${y4}`, `L${drx},${dry}`, `L${dlx},${dly}`, `L${x3},${y4}`, `L${x1},${y4}`, `L${x1},${y3}`, `L${x4},${y3}`, `L${x5},${y2}`, `L${x1},${y2}`, 'Z', ].join(' ') }) presetShapes.set('round1Rect', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 16667) const r = Math.min(w, h) * a return [`M0,0`, `L${w - r},0`, `A${r},${r} 0 0,1 ${w},${r}`, `L${w},${h}`, `L0,${h}`, 'Z'].join( ' ' ) }) presetShapes.set('round2SameRect', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 16667) const a2 = adj(adjustments, 'adj2', 0) const r1 = Math.min(w, h) * a1 const r2 = Math.min(w, h) * a2 return [ `M${r1},0`, `L${w - r1},0`, `A${r1},${r1} 0 0,1 ${w},${r1}`, `L${w},${h - r2}`, `A${r2},${r2} 0 0,1 ${w - r2},${h}`, `L${r2},${h}`, `A${r2},${r2} 0 0,1 0,${h - r2}`, `L0,${r1}`, `A${r1},${r1} 0 0,1 ${r1},0`, 'Z', ].join(' ') }) presetShapes.set('round2DiagRect', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 16667) const a2 = adj(adjustments, 'adj2', 0) const r1 = Math.min(w, h) * a1 const r2 = Math.min(w, h) * a2 return [ `M${r1},0`, `L${w},0`, `L${w},${h - r2}`, `A${r2},${r2} 0 0,1 ${w - r2},${h}`, `L0,${h}`, `L0,${r1}`, `A${r1},${r1} 0 0,1 ${r1},0`, 'Z', ].join(' ') }) presetShapes.set('snip1Rect', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 16667) const d = Math.min(w, h) * a return `M0,0 L${w - d},0 L${w},${d} L${w},${h} L0,${h} Z` }) presetShapes.set('snip2SameRect', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 16667) const a2 = adj(adjustments, 'adj2', 0) const d1 = Math.min(w, h) * a1 const d2 = Math.min(w, h) * a2 return `M${d1},0 L${w - d1},0 L${w},${d1} L${w},${h - d2} L${w - d2},${h} L${d2},${h} L0,${h - d2} L0,${d1} Z` }) presetShapes.set('snip2DiagRect', (w, h, adjustments) => { // OOXML spec: diagonal snipped rectangle. adj1=top-left/bottom-right, adj2=top-right/bottom-left const ss = Math.min(w, h) const a1 = Math.min(Math.max(adjustments?.get('adj1') ?? 0, 0), 50000) const a2 = Math.min(Math.max(adjustments?.get('adj2') ?? 16667, 0), 50000) const lx1 = (ss * a1) / 100000 const lx2 = w - lx1 const ly1 = h - lx1 const rx1 = (ss * a2) / 100000 const rx2 = w - rx1 const ry1 = h - rx1 return `M${lx1},0 L${rx2},0 L${w},${rx1} L${w},${ly1} L${lx2},${h} L${rx1},${h} L0,${ry1} L0,${lx1} Z` }) presetShapes.set('snipRoundRect', (w, h, adjustments) => { const a1 = adj(adjustments, 'adj1', 16667) const a2 = adj(adjustments, 'adj2', 16667) const r = Math.min(w, h) * a1 const d = Math.min(w, h) * a2 return [ `M${r},0`, `L${w - d},0`, `L${w},${d}`, `L${w},${h}`, `L0,${h}`, `L0,${r}`, `A${r},${r} 0 0,1 ${r},0`, 'Z', ].join(' ') }) presetShapes.set('bevel', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 12500) const t = Math.min(w, h) * a return [ // Outer `M0,0 L${w},0 L${w},${h} L0,${h} Z`, // Inner `M${t},${t} L${t},${h - t} L${w - t},${h - t} L${w - t},${t} Z`, // Connecting triangles (top) `M0,0 L${w},0 L${w - t},${t} L${t},${t} Z`, // Right `M${w},0 L${w},${h} L${w - t},${h - t} L${w - t},${t} Z`, // Bottom `M${w},${h} L0,${h} L${t},${h - t} L${w - t},${h - t} Z`, // Left `M0,${h} L0,0 L${t},${t} L${t},${h - t} Z`, ].join(' ') }) presetShapes.set('foldedCorner', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 16667) const fold = Math.min(w, h) * a * 0.7 return [ `M0,0 L${w},0 L${w},${h} L0,${h} Z`, // Fold triangle `M${w - fold},${h} L${w},${h} L${w},${h - fold}`, ].join(' ') }) // smileyFace is implemented as multiPathPreset (see multiPathPresets below) presetShapes.set('sun', (w, h, adjustments) => { // OOXML spec: adj default=25000, pinned 12500..46875 const adjRaw = adjustments?.get('adj') ?? 25000 const a = Math.min(Math.max(adjRaw, 12500), 46875) const g0 = 50000 - a // OOXML guide formulas const g1 = (g0 * 30274) / 32768 const g2 = (g0 * 12540) / 32768 const _g3 = g1 + 50000 const _g4 = g2 + 50000 const g5 = 50000 - g1 const g6 = 50000 - g2 const g7 = (g0 * 23170) / 32768 const g8 = 50000 + g7 const g9 = 50000 - g7 const g10 = (g5 * 3) / 4 const g11 = (g6 * 3) / 4 const g12 = g10 + 3662 const g13 = g11 + 3662 const g14 = g11 + 12500 const g15 = 100000 - g10 const g16 = 100000 - g12 const g17 = 100000 - g13 const g18 = 100000 - g14 // Pixel coordinates const hc = w / 2 const vc = h / 2 const ox1 = (w * 18436) / 21600 const oy1 = (h * 3163) / 21600 const ox2 = (w * 3163) / 21600 const oy2 = (h * 18436) / 21600 const s = (pct: number, dim: number) => (dim * pct) / 100000 const _x8 = s(g8, w) const _x9 = s(g9, w) const x10 = s(g10, w) const x12 = s(g12, w) const x13 = s(g13, w) const x14 = s(g14, w) const x15 = s(g15, w) const x16 = s(g16, w) const x17 = s(g17, w) const x18 = s(g18, w) const wR = s(g0, w) const hR = s(g0, h) const _y8 = s(g8, h) const _y9 = s(g9, h) const y10 = s(g10, h) const y12 = s(g12, h) const y13 = s(g13, h) const y14 = s(g14, h) const y15 = s(g15, h) const y16 = s(g16, h) const y17 = s(g17, h) const y18 = s(g18, h) const x19 = s(a, w) return [ // Ray 0: right `M${w},${vc} L${x15},${y18} L${x15},${y14} Z`, // Ray 1: top-right `M${ox1},${oy1} L${x16},${y13} L${x17},${y12} Z`, // Ray 2: top `M${hc},0 L${x18},${y10} L${x14},${y10} Z`, // Ray 3: top-left `M${ox2},${oy1} L${x13},${y12} L${x12},${y13} Z`, // Ray 4: left `M0,${vc} L${x10},${y14} L${x10},${y18} Z`, // Ray 5: bottom-left `M${ox2},${oy2} L${x12},${y17} L${x13},${y16} Z`, // Ray 6: bottom `M${hc},${h} L${x14},${y15} L${x18},${y15} Z`, // Ray 7: bottom-right `M${ox1},${oy2} L${x17},${y16} L${x16},${y17} Z`, // Center ellipse (arcTo from x19,vc with wR,hR, startAngle=180°, sweep=360°) `M${x19},${vc}`, `A${wR},${hR} 0 1,1 ${x19 + 2 * wR},${vc}`, `A${wR},${hR} 0 1,1 ${x19},${vc}`, 'Z', ].join(' ') }) presetShapes.set('moon', (w, h, adjustments) => { if (w <= 0 || h <= 0) return `M0,0 L${w},0 L${w},${h} L0,${h} Z` // OOXML moon: outer semicircle (rx=w, ry=h/2) + inner semicircle (rx=g18w, ry=dy1). // Both arcs share endpoints (w,0) and (w,h). Inner ellipse centered at (g0w+g18w, h/2). const ss = Math.min(w, h) const hd2 = h / 2 const a = Math.min(Math.max(adjustments?.get('adj') ?? 50000, 0), 87500) const g0 = (ss * a) / 100000 const g1 = ss - g0 if (g1 <= 0) return `M0,0 L${w},0 L${w},${h} L0,${h} Z` const g0w = (g0 * w) / ss const g5 = (2 * ss * ss - g0 * g0) / g1 const g6w = ((g5 - g0) * w) / ss const g8 = g5 / 2 - g0 const dy1 = (g8 * hd2) / ss const g18w = (g6w - g0w) / 2 return [ `M${w},${h}`, `A${w},${hd2} 0 0,1 ${w},0`, // outer: (w,h) → left semicircle → (w,0) `A${g18w},${dy1} 0 0,0 ${w},${h}`, // inner: (w,0) → concave arc → (w,h) 'Z', ].join(' ') }) presetShapes.set('lightningBolt', (w, h) => { // Calibrated against OOXML preset rendering (PowerPoint PDF export): // the old simplified 7-point bolt was too wide and lacked the inner notches. // This normalized 11-point contour follows the default lightningBolt geometry. return [ `M${w * 0.3895},${h * 0.0}`, `L${w * 0.0},${h * 0.1821}`, `L${w * 0.3425},${h * 0.3845}`, `L${w * 0.2265},${h * 0.4452}`, `L${w * 0.5497},${h * 0.6391}`, `L${w * 0.453},${h * 0.683}`, `L${w * 0.9972},${h * 0.9983}`, `L${w * 0.6796},${h * 0.5919}`, `L${w * 0.7624},${h * 0.5514}`, `L${w * 0.5138},${h * 0.3153}`, `L${w * 0.5939},${h * 0.2816}`, 'Z', ].join(' ') }) presetShapes.set('bracketPair', (w, h, adjustments) => { // OOXML: adj=16667 (max 50000), radius = ss * a / 100000 const ss = Math.min(w, h) const a = Math.min(Math.max(adjRaw(adjustments, 'adj', 16667), 0), 50000) const r = (ss * a) / 100000 const x2 = w - r const y2 = h - r return [ // Left bracket: bottom-left arc → vertical → top-left arc `M${r},${h}`, `A${r},${r} 0 0,1 0,${y2}`, `L0,${r}`, `A${r},${r} 0 0,1 ${r},0`, // Right bracket: top-right arc → vertical → bottom-right arc `M${x2},0`, `A${r},${r} 0 0,1 ${w},${r}`, `L${w},${y2}`, `A${r},${r} 0 0,1 ${x2},${h}`, ].join(' ') }) presetShapes.set('bracePair', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 8333) const r = Math.min(w, h) * a const cy = h / 2 return [ // Left brace `M${r * 2},0`, `A${r},${r} 0 0,0 ${r},${r}`, `L${r},${cy - r}`, `A${r},${r} 0 0,1 0,${cy}`, `A${r},${r} 0 0,1 ${r},${cy + r}`, `L${r},${h - r}`, `A${r},${r} 0 0,0 ${r * 2},${h}`, // Right brace `M${w - r * 2},0`, `A${r},${r} 0 0,1 ${w - r},${r}`, `L${w - r},${cy - r}`, `A${r},${r} 0 0,0 ${w},${cy}`, `A${r},${r} 0 0,0 ${w - r},${cy + r}`, `L${w - r},${h - r}`, `A${r},${r} 0 0,1 ${w - r * 2},${h}`, ].join(' ') }) presetShapes.set('leftBracket', (w, h, adjustments) => { const ss = Math.min(w, h) const maxAdj = ss > 0 ? (50000 * h) / ss : 0 const a = Math.max(0, Math.min(adjustments?.get('adj') ?? 8333, maxAdj)) const y1 = (ss * a) / 100000 const toDeg = (ooxmlAng: number) => ooxmlAng / 60000 const arcFrom = ( x0: number, y0: number, rx: number, ry: number, stAng: number, swAng: number ) => { const st = (toDeg(stAng) * Math.PI) / 180 const sw = (toDeg(swAng) * Math.PI) / 180 const cx = x0 - rx * Math.cos(st) const cy = y0 - ry * Math.sin(st) const x1 = cx + rx * Math.cos(st + sw) const y1p = cy + ry * Math.sin(st + sw) const large = Math.abs(toDeg(swAng)) > 180 ? 1 : 0 const sweep = swAng >= 0 ? 1 : 0 return { cmd: `A${rx},${ry} 0 ${large},${sweep} ${x1},${y1p}`, x: x1, y: y1p } } const a1 = arcFrom(w, h, w, y1, 5400000, 5400000) // cd4, cd4 const a2 = arcFrom(0, y1, w, y1, 10800000, 5400000) // cd2, cd4 return [`M${w},${h}`, a1.cmd, `L0,${y1}`, a2.cmd].join(' ') }) presetShapes.set('rightBracket', (w, h, adjustments) => { const ss = Math.min(w, h) const maxAdj = ss > 0 ? (50000 * h) / ss : 0 const a = Math.max(0, Math.min(adjustments?.get('adj') ?? 8333, maxAdj)) const y1 = (ss * a) / 100000 const y2 = h - y1 const toDeg = (ooxmlAng: number) => ooxmlAng / 60000 const arcFrom = ( x0: number, y0: number, rx: number, ry: number, stAng: number, swAng: number ) => { const st = (toDeg(stAng) * Math.PI) / 180 const sw = (toDeg(swAng) * Math.PI) / 180 const cx = x0 - rx * Math.cos(st) const cy = y0 - ry * Math.sin(st) const x1 = cx + rx * Math.cos(st + sw) const y1p = cy + ry * Math.sin(st + sw) const large = Math.abs(toDeg(swAng)) > 180 ? 1 : 0 const sweep = swAng >= 0 ? 1 : 0 return { cmd: `A${rx},${ry} 0 ${large},${sweep} ${x1},${y1p}`, x: x1, y: y1p } } const a1 = arcFrom(0, 0, w, y1, 16200000, 5400000) // 3cd4, cd4 const a2 = arcFrom(w, y2, w, y1, 0, 5400000) // 0, cd4 return [`M0,0`, a1.cmd, `L${w},${y2}`, a2.cmd].join(' ') }) presetShapes.set('leftBrace', (w, h, adjustments) => { const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 50000, 100000)) const q1 = 100000 - a2 const q2 = Math.min(q1, a2) const q3 = q2 / 2 const maxAdj1 = ss > 0 ? (q3 * h) / ss : 0 const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 8333, maxAdj1)) const y1 = (ss * a1) / 100000 const y3 = (h * a2) / 100000 const y4 = y3 + y1 const wd2 = w / 2 const hc = w / 2 const toDeg = (ooxmlAng: number) => ooxmlAng / 60000 const arcFrom = ( x0: number, y0: number, rx: number, ry: number, stAng: number, swAng: number ) => { const st = (toDeg(stAng) * Math.PI) / 180 const sw = (toDeg(swAng) * Math.PI) / 180 const cx = x0 - rx * Math.cos(st) const cy = y0 - ry * Math.sin(st) const x1 = cx + rx * Math.cos(st + sw) const y1p = cy + ry * Math.sin(st + sw) const large = Math.abs(toDeg(swAng)) > 180 ? 1 : 0 const sweep = swAng >= 0 ? 1 : 0 return { cmd: `A${rx},${ry} 0 ${large},${sweep} ${x1},${y1p}`, x: x1, y: y1p } } let x = w let y = h const aTop = arcFrom(x, y, wd2, y1, 5400000, 5400000) // cd4, cd4 x = aTop.x y = aTop.y const aMid1 = arcFrom(hc, y4, wd2, y1, 0, -5400000) const aMid2 = arcFrom(aMid1.x, aMid1.y, wd2, y1, 5400000, -5400000) const aBot = arcFrom(hc, y1, wd2, y1, 10800000, 5400000) // cd2, cd4 return [ `M${w},${h}`, aTop.cmd, `L${hc},${y4}`, aMid1.cmd, aMid2.cmd, `L${hc},${y1}`, aBot.cmd, ].join(' ') }) presetShapes.set('rightBrace', (w, h, adjustments) => { const ss = Math.min(w, h) const a2 = Math.max(0, Math.min(adjustments?.get('adj2') ?? 50000, 100000)) const q1 = 100000 - a2 const q2 = Math.min(q1, a2) const q3 = q2 / 2 const maxAdj1 = ss > 0 ? (q3 * h) / ss : 0 const a1 = Math.max(0, Math.min(adjustments?.get('adj1') ?? 8333, maxAdj1)) const y1 = (ss * a1) / 100000 const y3 = (h * a2) / 100000 const y2 = y3 - y1 const y4 = h - y1 const wd2 = w / 2 const hc = w / 2 const toDeg = (ooxmlAng: number) => ooxmlAng / 60000 const arcFrom = ( x0: number, y0: number, rx: number, ry: number, stAng: number, swAng: number ) => { const st = (toDeg(stAng) * Math.PI) / 180 const sw = (toDeg(swAng) * Math.PI) / 180 const cx = x0 - rx * Math.cos(st) const cy = y0 - ry * Math.sin(st) const x1 = cx + rx * Math.cos(st + sw) const y1p = cy + ry * Math.sin(st + sw) const large = Math.abs(toDeg(swAng)) > 180 ? 1 : 0 const sweep = swAng >= 0 ? 1 : 0 return { cmd: `A${rx},${ry} 0 ${large},${sweep} ${x1},${y1p}`, x: x1, y: y1p } } const aTop = arcFrom(0, 0, wd2, y1, 16200000, 5400000) // 3cd4, cd4 const aMid1 = arcFrom(hc, y2, wd2, y1, 10800000, -5400000) // cd2,-cd4 const aMid2 = arcFrom(aMid1.x, aMid1.y, wd2, y1, 16200000, -5400000) //3cd4,-cd4 const aBot = arcFrom(hc, y4, wd2, y1, 0, 5400000) //0,cd4 return [`M0,0`, aTop.cmd, `L${hc},${y2}`, aMid1.cmd, aMid2.cmd, `L${hc},${y4}`, aBot.cmd].join( ' ' ) }) // ===== Action Buttons ===== // Action buttons are multi-path shapes: background rect + icon with darken fill + icon outline + rect outline. // OOXML spec uses ss*3/8 as the icon half-size (dx2), with the icon centred at (hc, vc). // Shapes with multiPathPresets entries below get proper 3D treatment. Remaining shapes // fall back to the legacy actionButtonIcons overlay (single flat icon path). presetShapes.set('actionButtonBlank', (w, h) => `M0,0 L${w},0 L${w},${h} L0,${h} Z`) // Fallback rectangle for action buttons without multiPathPresets entry yet // actionButtonSound fallback removed — uses multiPathPresets entry below // Multi-path action button presets are registered after the multiPathPresets Map // declaration (see below in the multiPathPresets section). // --------------------------------------------------------------------------- // Action button icon paths (rendered as a second with contrasting fill) // --------------------------------------------------------------------------- const actionButtonIcons = new Map string>() // actionButtonHome icon removed — uses multiPathPresets entry below actionButtonIcons.set('actionButtonForwardNext', (w, h) => { // Right-pointing triangle (▶) const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.3 return `M${cx - s * 0.5},${cy - s} L${cx + s},${cy} L${cx - s * 0.5},${cy + s} Z` }) actionButtonIcons.set('actionButtonBackPrevious', (w, h) => { // Left-pointing triangle (◀) const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.3 return `M${cx + s * 0.5},${cy - s} L${cx - s},${cy} L${cx + s * 0.5},${cy + s} Z` }) actionButtonIcons.set('actionButtonReturn', (w, h) => { // Curved return arrow (↩) — shaft goes right at bottom, curves UP at right end, // returns left at top with arrowhead pointing left (standard PowerPoint icon). const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.28 const thick = s * 0.22 // shaft thickness const bottomY = cy + s * 0.4 const topY = cy - s * 0.4 const leftX = cx - s * 0.6 const rightX = cx + s * 0.6 const r = (bottomY - topY) / 2 // semicircle radius return [ // Outer edge: bottom-left → right → arc up → left to arrowhead junction `M${leftX},${bottomY}`, `L${rightX},${bottomY}`, `A${r},${r} 0 0,1 ${rightX},${topY}`, `L${leftX + s * 0.15},${topY}`, // Inner edge: top → right → arc down → bottom-left `L${leftX + s * 0.15},${topY + thick}`, `L${rightX - thick * 0.3},${topY + thick}`, `A${r - thick},${r - thick} 0 0,0 ${rightX - thick * 0.3},${bottomY - thick}`, `L${leftX},${bottomY - thick}`, `Z`, // Arrowhead pointing left at top-left `M${leftX - s * 0.3},${topY + thick / 2}`, `L${leftX + s * 0.15},${topY - s * 0.2}`, `L${leftX + s * 0.15},${topY + thick + s * 0.2}`, `Z`, ].join(' ') }) actionButtonIcons.set('actionButtonBeginning', (w, h) => { // Skip-to-beginning (|◀) const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.28 return [ // Left bar `M${cx - s},${cy - s} L${cx - s + s * 0.2},${cy - s} L${cx - s + s * 0.2},${cy + s} L${cx - s},${cy + s} Z`, // Left-pointing triangle `M${cx + s},${cy - s} L${cx - s + s * 0.35},${cy} L${cx + s},${cy + s} Z`, ].join(' ') }) actionButtonIcons.set('actionButtonEnd', (w, h) => { // Skip-to-end (▶|) const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.28 return [ // Right bar `M${cx + s - s * 0.2},${cy - s} L${cx + s},${cy - s} L${cx + s},${cy + s} L${cx + s - s * 0.2},${cy + s} Z`, // Right-pointing triangle `M${cx - s},${cy - s} L${cx + s - s * 0.35},${cy} L${cx - s},${cy + s} Z`, ].join(' ') }) // actionButtonHelp icon removed — uses multiPathPresets entry below actionButtonIcons.set('actionButtonInformation', (w, h) => { // Info icon (i) const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.28 return [ // Dot `M${cx - s * 0.1},${cy - s * 0.65} L${cx + s * 0.1},${cy - s * 0.65} L${cx + s * 0.1},${cy - s * 0.4} L${cx - s * 0.1},${cy - s * 0.4} Z`, // Stem `M${cx - s * 0.12},${cy - s * 0.2} L${cx + s * 0.12},${cy - s * 0.2} L${cx + s * 0.12},${cy + s * 0.65} L${cx - s * 0.12},${cy + s * 0.65} Z`, ].join(' ') }) actionButtonIcons.set('actionButtonDocument', (w, h) => { // Document with folded corner const cx = w / 2 const cy = h / 2 const s = Math.min(w, h) * 0.28 const dx = s * 0.7 const dy = s const fold = s * 0.3 return [ `M${cx - dx},${cy - dy}`, `L${cx + dx - fold},${cy - dy} L${cx + dx},${cy - dy + fold}`, `L${cx + dx},${cy + dy} L${cx - dx},${cy + dy} Z`, `M${cx + dx - fold},${cy - dy} L${cx + dx - fold},${cy - dy + fold} L${cx + dx},${cy - dy + fold}`, ].join(' ') }) // actionButtonSound icon removed — uses multiPathPresets entry below // actionButtonMovie icon is now rendered via multiPathPresets (see below). /** * Get the SVG path for the icon overlay of an action button. * Returns undefined if the shape is not an action button or is actionButtonBlank. */ export function getActionButtonIconPath( shapeType: string, w: number, h: number ): string | undefined { const key = shapeType.toLowerCase() const generator = actionButtonIcons.get(key) ?? actionButtonIcons.get(shapeType) return generator?.(w, h) } // ===== Aliases and common alternative names ===== // Some shapes are known by multiple names in different OOXML versions // flowChartOfflineStorage: registered as multiPathPreset (see below) // ribbon is implemented as multiPathPreset (see multiPathPresets below) presetShapes.set('wave', (w, h, adjustments) => { // OOXML: adj1=12500 (max 20000), adj2=0 (phase shift, range -10000..10000) const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 12500), 0), 20000) const a2 = Math.min(Math.max(adjRaw(adjustments, 'adj2', 0), -10000), 10000) const y1 = (h * a1) / 100000 const dy2 = (y1 * 10) / 3 const y2 = y1 - dy2 // control above crest const y3 = y1 + dy2 // control below crest const y4 = h - y1 // bottom wave y const y5 = y4 - dy2 const y6 = y4 + dy2 // Phase shift const of2 = (w * a2) / 50000 const dx2 = of2 < 0 ? 0 : of2 const dx5 = of2 < 0 ? of2 : 0 const x2 = -dx2 const x5 = w - dx5 const dx3 = (x5 - x2) / 3 const x3 = x2 + dx3 const x4 = (x3 + x5) / 2 const x6 = dx5 const x10 = w + dx2 const x7 = x6 + (x10 - x6) / 3 const x8 = (x7 + x10) / 2 return [ `M${x2},${y1}`, `C${x3},${y2} ${x4},${y3} ${x5},${y1}`, `L${x10},${y4}`, `C${x8},${y6} ${x7},${y5} ${x6},${y4}`, 'Z', ].join(' ') }) presetShapes.set('doubleWave', (w, h, adjustments) => { // OOXML: adj1=6250 (max 12500), adj2=0 (phase shift) const a1 = Math.min(Math.max(adjRaw(adjustments, 'adj1', 6250), 0), 12500) const a2 = Math.min(Math.max(adjRaw(adjustments, 'adj2', 0), -10000), 10000) const y1 = (h * a1) / 100000 const dy2 = (y1 * 10) / 3 const y2 = y1 - dy2 const y3 = y1 + dy2 const y4 = h - y1 const y5 = y4 - dy2 const y6 = y4 + dy2 const of2 = (w * a2) / 50000 const dx2 = of2 < 0 ? 0 : of2 const dx8 = of2 < 0 ? of2 : 0 const x2 = -dx2 const x8 = w - dx8 const dx3 = (x8 - x2) / 6 const x3 = x2 + dx3 const dx4 = (x8 - x2) / 3 const x4 = x2 + dx4 const x5 = (x2 + x8) / 2 const x6 = x5 + dx3 const x7 = (x6 + x8) / 2 const x9 = dx8 const x15 = w + dx2 const dx3b = (x15 - x9) / 6 const x10 = x9 + dx3b const x11 = x9 + (x15 - x9) / 3 const x12 = (x9 + x15) / 2 const x13 = x12 + dx3b const x14 = (x13 + x15) / 2 return [ `M${x2},${y1}`, `C${x3},${y2} ${x4},${y3} ${x5},${y1}`, `C${x6},${y2} ${x7},${y3} ${x8},${y1}`, `L${x15},${y4}`, `C${x14},${y6} ${x13},${y5} ${x12},${y4}`, `C${x11},${y6} ${x10},${y5} ${x9},${y4}`, 'Z', ].join(' ') }) // verticalScroll and horizontalScroll are implemented as multi-path presets // (see multiPathPresets below) for accurate OOXML rendering with darkenLess shadows. presetShapes.set('irregularSeal1', (w, h) => { // OOXML spec: exact coordinates on 21600x21600 grid const sx = (x: number) => (w * x) / 21600 const sy = (y: number) => (h * y) / 21600 return [ `M${sx(10800)},${sy(5800)}`, `L${sx(14522)},0`, `L${sx(14155)},${sy(5325)}`, `L${sx(18380)},${sy(4457)}`, `L${sx(16702)},${sy(7315)}`, `L${sx(21097)},${sy(8137)}`, `L${sx(17607)},${sy(10475)}`, `L${sx(21600)},${sy(13290)}`, `L${sx(16837)},${sy(12942)}`, `L${sx(18145)},${sy(18095)}`, `L${sx(14020)},${sy(14457)}`, `L${sx(13247)},${sy(19737)}`, `L${sx(10532)},${sy(14935)}`, `L${sx(8485)},${sy(21600)}`, `L${sx(7715)},${sy(15627)}`, `L${sx(4762)},${sy(17617)}`, `L${sx(5667)},${sy(13937)}`, `L${sx(135)},${sy(14587)}`, `L${sx(3722)},${sy(11775)}`, `L0,${sy(8615)}`, `L${sx(4627)},${sy(7617)}`, `L${sx(370)},${sy(2295)}`, `L${sx(7312)},${sy(6320)}`, `L${sx(8352)},${sy(2295)}`, 'Z', ].join(' ') }) presetShapes.set('irregularSeal2', (w, h) => { // Office-like irregularSeal2 coordinates (21600 design grid). return [ `M${(w * 11462) / 21600},${(h * 4342) / 21600}`, `L${(w * 14790) / 21600},0`, `L${(w * 14525) / 21600},${(h * 5777) / 21600}`, `L${(w * 18007) / 21600},${(h * 3172) / 21600}`, `L${(w * 16380) / 21600},${(h * 6532) / 21600}`, `L${w},${(h * 6645) / 21600}`, `L${(w * 16985) / 21600},${(h * 9402) / 21600}`, `L${(w * 18270) / 21600},${(h * 11290) / 21600}`, `L${(w * 16380) / 21600},${(h * 12310) / 21600}`, `L${(w * 18877) / 21600},${(h * 15632) / 21600}`, `L${(w * 14640) / 21600},${(h * 14350) / 21600}`, `L${(w * 14942) / 21600},${(h * 17370) / 21600}`, `L${(w * 12180) / 21600},${(h * 15935) / 21600}`, `L${(w * 11612) / 21600},${(h * 18842) / 21600}`, `L${(w * 9872) / 21600},${(h * 17370) / 21600}`, `L${(w * 8700) / 21600},${(h * 19712) / 21600}`, `L${(w * 7527) / 21600},${(h * 18125) / 21600}`, `L${(w * 4917) / 21600},${h}`, `L${(w * 4805) / 21600},${(h * 18240) / 21600}`, `L${(w * 1285) / 21600},${(h * 17825) / 21600}`, `L${(w * 3330) / 21600},${(h * 15370) / 21600}`, `L0,${(h * 12877) / 21600}`, `L${(w * 3935) / 21600},${(h * 11592) / 21600}`, `L${(w * 1172) / 21600},${(h * 8270) / 21600}`, `L${(w * 5372) / 21600},${(h * 7817) / 21600}`, `L${(w * 4502) / 21600},${(h * 3625) / 21600}`, `L${(w * 8550) / 21600},${(h * 6382) / 21600}`, `L${(w * 9722) / 21600},${(h * 1887) / 21600}`, 'Z', ].join(' ') }) presetShapes.set('teardrop', (w, h) => { const rx = w / 2 const ry = h / 2 return [`M${w},${ry}`, `A${rx},${ry} 0 1,1 ${rx},0`, `L${w},0`, `L${w},${ry}`, 'Z'].join(' ') }) presetShapes.set('pie', (w, h, adjustments) => { // OOXML pie: adj1 = start angle, adj2 = end angle (60000ths of a degree). Sweep clockwise from start to end. // OOXML angles are "visual" (geometric) — must convert to parametric for ellipses (rx≠ry). const adj1Raw = adjustments?.get('adj1') ?? 0 const adj2Raw = adjustments?.get('adj2') ?? 16200000 // 270° end default const startDeg = (adj1Raw / 60000) % 360 const endDeg = (adj2Raw / 60000) % 360 let sweepDeg = (((endDeg - startDeg) % 360) + 360) % 360 if (sweepDeg === 0 && startDeg !== endDeg) sweepDeg = 360 const rx = w / 2 const ry = h / 2 const toRad = (d: number) => (d * Math.PI) / 180 const visualToParam = (deg: number) => Math.atan2(Math.sin(toRad(deg)) / ry, Math.cos(toRad(deg)) / rx) const startParam = visualToParam(startDeg) const endParam = visualToParam(endDeg) const x1 = rx + rx * Math.cos(startParam) const y1 = ry + ry * Math.sin(startParam) const x2 = rx + rx * Math.cos(endParam) const y2 = ry + ry * Math.sin(endParam) const largeArc = sweepDeg > 180 ? 1 : 0 return [`M${rx},${ry}`, `L${x1},${y1}`, `A${rx},${ry} 0 ${largeArc},1 ${x2},${y2}`, 'Z'].join(' ') }) presetShapes.set('pieWedge', (w, h) => { // OOXML: Quarter-ellipse pie wedge. Center at (w, h), radii = (w, h). // Arc from 180° sweeping 90° CW: starts at (0, h), ends at (w, 0). // The arc bulges toward the upper-left. return [`M0,${h}`, `A${w},${h} 0 0,1 ${w},0`, `L${w},${h}`, 'Z'].join(' ') }) presetShapes.set('arc', (w, h, adjustments) => { // OOXML arc: adj1/adj2 are angles in 60000ths of a degree // OOXML angles are "visual" (geometric) — must convert to parametric for ellipses (rx≠ry). const adj1Raw = adjustments?.get('adj1') ?? 16200000 // default 270° const adj2Raw = adjustments?.get('adj2') ?? 0 // default 0° const startDeg = adj1Raw / 60000 const endDeg = adj2Raw / 60000 const rx = w / 2 const ry = h / 2 const toRad = (d: number) => (d * Math.PI) / 180 const visualToParam = (deg: number) => Math.atan2(Math.sin(toRad(deg)) / ry, Math.cos(toRad(deg)) / rx) const startParam = visualToParam(startDeg) const endParam = visualToParam(endDeg) const x1 = rx + rx * Math.cos(startParam) const y1 = ry + ry * Math.sin(startParam) const x2 = rx + rx * Math.cos(endParam) const y2 = ry + ry * Math.sin(endParam) let sweepDeg = (((endDeg - startDeg) % 360) + 360) % 360 if (sweepDeg === 0 && startDeg !== endDeg) sweepDeg = 360 const largeArc = sweepDeg > 180 ? 1 : 0 return `M${x1},${y1} A${rx},${ry} 0 ${largeArc},1 ${x2},${y2}` }) presetShapes.set('chord', (w, h, adjustments) => { // OOXML chord: arc + chord line. Spec uses ellipse (arcTo wR="wd2" hR="hd2") per presetShapeDefinitions. // OOXML angles are "visual" (geometric) angles — the angle of the ray from center to the point. // For ellipses (rx≠ry), convert to parametric angle: t = atan2(sin(θ)/ry, cos(θ)/rx) const adj1Raw = adjustments?.get('adj1') ?? 2700000 // default 45° const adj2Raw = adjustments?.get('adj2') ?? 16200000 // default 270° const startDeg = adj1Raw / 60000 const endDeg = adj2Raw / 60000 const cx = w / 2 const cy = h / 2 const rx = w / 2 const ry = h / 2 const toRad = (d: number) => (d * Math.PI) / 180 // Convert OOXML visual angles to parametric angles on the ellipse const visualToParam = (deg: number) => Math.atan2(Math.sin(toRad(deg)) / ry, Math.cos(toRad(deg)) / rx) const startParam = visualToParam(startDeg) const endParam = visualToParam(endDeg) const x1 = cx + rx * Math.cos(startParam) const y1 = cy + ry * Math.sin(startParam) const x2 = cx + rx * Math.cos(endParam) const y2 = cy + ry * Math.sin(endParam) // Use OOXML visual sweep to determine large-arc-flag let sweepDeg = (((endDeg - startDeg) % 360) + 360) % 360 if (sweepDeg === 0 && startDeg !== endDeg) sweepDeg = 360 // When adj1 == adj2, the chord covers the full ellipse (360° sweep) if (sweepDeg === 0) { return `M${cx - rx},${cy} A${rx},${ry} 0 1,1 ${cx + rx},${cy} A${rx},${ry} 0 1,1 ${cx - rx},${cy} Z` } const largeArc = sweepDeg > 180 ? 1 : 0 return `M${x1},${y1} A${rx},${ry} 0 ${largeArc},1 ${x2},${y2} Z` }) presetShapes.set('funnel', (w, h) => { // OOXML funnel: top rim ellipse arc + tapered sides + bottom spout arc + inset top ellipse. // From presetShapeDefinitions.xml (ECMA-376). const ss = Math.min(w, h) const wd2 = w / 2 const hd4 = h / 4 const hc = w / 2 const b = h const d = ss / 20 // inset margin const rw2 = wd2 - d // inset top-ellipse x-radius const rh2 = hd4 - d // inset top-ellipse y-radius // Angle (in radians) where funnel sides are tangent to top ellipse. // OOXML: t1 = cos(wd2, 480000), t2 = sin(hd4, 480000) → da = atan2(t1, t2) // 480000 = 8° in 60000ths of a degree const ang8 = (8 * Math.PI) / 180 const t1 = wd2 * Math.cos(ang8) const t2 = hd4 * Math.sin(ang8) const da = Math.atan2(t2, t1) // radians // Angles for the top rim arc (OOXML convention: sweep from stAng1 by swAng1) const stAng1 = Math.PI - da // cd2 - da const swAng1 = Math.PI + 2 * da // cd2 + 2*da // Sweep for the bottom spout arc const swAng3 = Math.PI - 2 * da // cd2 - 2*da // Bottom spout ellipse radii: 1/4 of top ellipse const rw3 = wd2 / 4 const rh3 = hd4 / 4 // Start point on top ellipse at stAng1 (visual angle → ellipse point) // OOXML uses: n = (wR*hR) / mod(cos(hR,ang), sin(wR,ang), 0), then x = hc + cos(n,ang), y = hd4 + sin(n,ang) // This is equivalent to the parametric ellipse point at the "visual" angle. const ct1 = hd4 * Math.cos(stAng1) const st1 = wd2 * Math.sin(stAng1) const m1 = Math.sqrt(ct1 * ct1 + st1 * st1) const n1 = (wd2 * hd4) / m1 const dx1 = n1 * Math.cos(stAng1) const dy1 = n1 * Math.sin(stAng1) const x1 = hc + dx1 const y1 = hd4 + dy1 // End point of top arc (at stAng1 + swAng1 = pi + da) const endAng1 = stAng1 + swAng1 const ct1e = hd4 * Math.cos(endAng1) const st1e = wd2 * Math.sin(endAng1) const m1e = Math.sqrt(ct1e * ct1e + st1e * st1e) const n1e = (wd2 * hd4) / m1e const dx1e = n1e * Math.cos(endAng1) const dy1e = n1e * Math.sin(endAng1) const x1e = hc + dx1e const y1e = hd4 + dy1e // Point on spout ellipse at angle da const vc3 = b - rh3 // vertical center of spout ellipse const ct3 = rh3 * Math.cos(da) const st3 = rw3 * Math.sin(da) const m3 = Math.sqrt(ct3 * ct3 + st3 * st3) const n3 = (rw3 * rh3) / m3 const dx3 = n3 * Math.cos(da) const dy3 = n3 * Math.sin(da) const x3 = hc + dx3 const y2 = vc3 + dy3 // End point of spout arc (at da + swAng3) const endAng3 = da + swAng3 const ct3e = rh3 * Math.cos(endAng3) const st3e = rw3 * Math.sin(endAng3) const m3e = Math.sqrt(ct3e * ct3e + st3e * st3e) const n3e = (rw3 * rh3) / m3e const dx3e = n3e * Math.cos(endAng3) const dy3e = n3e * Math.sin(endAng3) const x3e = hc + dx3e const y2e = vc3 + dy3e // Determine arc flags const swDeg1 = (swAng1 * 180) / Math.PI const largeArc1 = Math.abs(swDeg1) > 180 ? 1 : 0 const sweep1 = swAng1 > 0 ? 1 : 0 const swDeg3 = (swAng3 * 180) / Math.PI const largeArc3 = Math.abs(swDeg3) > 180 ? 1 : 0 const sweep3 = swAng3 > 0 ? 1 : 0 // Sub-path 1: Funnel body (top arc → line to spout → spout arc → close) const body = [ `M${x1},${y1}`, `A${wd2},${hd4} 0 ${largeArc1},${sweep1} ${x1e},${y1e}`, `L${x3},${y2}`, `A${rw3},${rh3} 0 ${largeArc3},${sweep3} ${x3e},${y2e}`, 'Z', ].join(' ') // Sub-path 2: Inset top ellipse (full ellipse, counter-clockwise for even-odd hole) const x2 = wd2 - rw2 // leftmost point of inset ellipse const x2r = wd2 + rw2 // rightmost point const inset = [ `M${x2},${hd4}`, `A${rw2},${rh2} 0 1,0 ${x2r},${hd4}`, `A${rw2},${rh2} 0 1,0 ${x2},${hd4}`, 'Z', ].join(' ') return `${body} ${inset}` }) // ===== Fallback ===== /** * Get the SVG path for a preset shape, falling back to a simple rectangle * if the shape type is not implemented. */ // --------------------------------------------------------------------------- // Preset shape overlays — additional paths for 3D-like shapes (lighter top face, etc.) // --------------------------------------------------------------------------- interface PresetOverlay { /** SVG path d-attribute for the overlay */ path: string /** Fill modifier: 'lighten' brightens the base fill */ fillModifier: 'lighten' } export type PresetOverlayGenerator = ( w: number, h: number, adjustments?: Map ) => PresetOverlay[] const presetOverlays: Map = new Map() presetOverlays.set('can', (w, h) => { const ry = h * 0.1 const rx = w / 2 return [ { path: [`M0,${ry}`, `A${rx},${ry} 0 0,1 ${w},${ry}`, `A${rx},${ry} 0 0,1 0,${ry}`, 'Z'].join( ' ' ), fillModifier: 'lighten', }, ] }) /** * Get overlay paths for a preset shape (3D top faces, etc.). * Returns empty array if the shape has no overlays. */ export function getPresetOverlays( shapeType: string, w: number, h: number, adjustments?: Map ): PresetOverlay[] { const key = shapeType.toLowerCase() const gen = presetOverlays.get(key) ?? presetOverlays.get(shapeType) return gen ? gen(w, h, adjustments) : [] } // --------------------------------------------------------------------------- // Multi-path preset shapes — complex shapes with multiple SVG paths // Each path has its own fill modifier and stroke behavior, matching OOXML spec. // --------------------------------------------------------------------------- /** A single sub-path within a multi-path preset shape. */ export interface PresetSubPath { /** SVG path d-attribute string */ d: string /** * Fill behavior: * - 'norm': use the shape's normal fill * - 'darken': darken the base fill (multiply with ~60% gray) * - 'darkenLess': slightly darken (multiply with ~80% gray) * - 'lighten': lighten the base fill * - 'lightenLess': slightly lighten * - 'none': no fill (stroke-only detail lines) */ fill: 'norm' | 'darken' | 'darkenLess' | 'lighten' | 'lightenLess' | 'none' /** Whether this path should have a stroke (default true) */ stroke: boolean /** Optional stroke width multiplier for detail lines that should render lighter than the outline. */ strokeWidthScale?: number /** Restrict visibility of this detail path to a stroke band around the main outline path. */ maskToMainOutline?: boolean /** Optional scale for the outline-band mask stroke width. */ maskStrokeScale?: number /** Restrict visibility of this detail path to the band between the main outline and an inset-scaled outline. */ maskToMainOutlineBandScale?: number } type MultiPathPresetGenerator = ( w: number, h: number, adjustments?: Map ) => PresetSubPath[] const multiPathPresets: Map = new Map() // ===== Action Button multi-path presets (OOXML spec-accurate) ===== // Common helper: OOXML action button guide values function _abGuides(w: number, h: number) { const ss = Math.min(w, h) const hc = w / 2 const vc = h / 2 const dx2 = (ss * 3) / 8 // icon half-extent return { ss, hc, vc, dx2, g9: vc - dx2, g10: vc + dx2, g11: hc - dx2, g12: hc + dx2, g13: (ss * 3) / 4, } } const _rect = (w: number, h: number) => `M0,0 L${w},0 L${w},${h} L0,${h} Z` // actionButtonForwardNext (VBA 0130): right-pointing triangle ▶ multiPathPresets.set('actionButtonForwardNext', (w, h) => { const { g9, g10, g11, g12, vc } = _abGuides(w, h) const tri = `M${g12},${vc} L${g11},${g9} L${g11},${g10} Z` return [ { d: `${_rect(w, h)} ${tri}`, fill: 'norm', stroke: false }, { d: tri, fill: 'darken', stroke: false }, { d: tri, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) multiPathPresets.set('actionButtonForward', (w, h) => { const forwardNext = multiPathPresets.get('actionButtonForwardNext') return forwardNext ? forwardNext(w, h) : [] }) // actionButtonBackPrevious (VBA 0129): left-pointing triangle ◀ multiPathPresets.set('actionButtonBackPrevious', (w, h) => { const { g9, g10, g11, g12, vc } = _abGuides(w, h) const tri = `M${g11},${vc} L${g12},${g9} L${g12},${g10} Z` return [ { d: `${_rect(w, h)} ${tri}`, fill: 'norm', stroke: false }, { d: tri, fill: 'darken', stroke: false }, { d: tri, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonBeginning (VBA 0131): |◀ skip-to-start multiPathPresets.set('actionButtonBeginning', (w, h) => { const { g9, g10, g11, g12, g13, vc } = _abGuides(w, h) const g14 = g13 / 8 const g15 = g13 / 4 const g16 = g11 + g14 const g17 = g11 + g15 const tri = `M${g17},${vc} L${g12},${g9} L${g12},${g10} Z` const bar = `M${g16},${g9} L${g11},${g9} L${g11},${g10} L${g16},${g10} Z` const icon = `${tri} ${bar}` return [ { d: `${_rect(w, h)} ${icon}`, fill: 'norm', stroke: false }, { d: icon, fill: 'darken', stroke: false }, { d: icon, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonEnd (VBA 0132): ▶| skip-to-end multiPathPresets.set('actionButtonEnd', (w, h) => { const { g9, g10, g11, g12, g13, vc } = _abGuides(w, h) const g14 = (g13 * 3) / 4 const g15 = (g13 * 7) / 8 const g16 = g11 + g14 const g17 = g11 + g15 const tri = `M${g16},${vc} L${g11},${g9} L${g11},${g10} Z` const bar = `M${g17},${g9} L${g12},${g9} L${g12},${g10} L${g17},${g10} Z` const icon = `${tri} ${bar}` return [ { d: `${_rect(w, h)} ${icon}`, fill: 'norm', stroke: false }, { d: icon, fill: 'darken', stroke: false }, { d: icon, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonReturn (VBA 0133): curved return arrow ↩ // OOXML spec: 4 paths – bg+icon cutout (norm), icon fill (darken), icon outline (stroke), rect outline (stroke) // Fill paths use inner arcs curving inward; outline path traces the full shape with reversed arc winding. multiPathPresets.set('actionButtonReturn', (w, h) => { const { g9, g10, g11, g12, g13, hc, vc: _vcR } = _abGuides(w, h) const g14 = (g13 * 7) / 8 const g15 = (g13 * 3) / 4 const g16 = (g13 * 5) / 8 const g17 = (g13 * 3) / 8 // outer arc radius const g18 = g13 / 4 const g27 = g13 / 8 // inner arc radius const g19 = g9 + g15 const g20 = g9 + g16 const g21 = g9 + g18 const g22 = g11 + g14 const g23 = g11 + g15 const g24 = g11 + g16 const g25 = g11 + g17 const g26 = g11 + g18 // Fill icon path (paths 0 & 1 in OOXML spec — identical geometry) // Arc 1: from (g24, g20), wR=g27 hR=g27 stAng=0° swAng=90° // center = (g24-g27, g20), endpoint = (g24-g27, g20+g27) = (g24-g27, g19) // Arc 2: from (g25, g19), wR=g27 hR=g27 stAng=90° swAng=90° // center = (g25, g19-g27), endpoint = (g25-g27, g19-g27) = (g26, g20) // Arc 3: from (g11, g20), wR=g17 hR=g17 stAng=180° swAng=-90° // center = (g11+g17, g20) = (g25, g20), endpoint = (g25, g20+g17) = (g25, g10) // Arc 4: from (hc, g10), wR=g17 hR=g17 stAng=90° swAng=-90° // center = (hc, g10-g17), endpoint = (hc+g17, g10-g17) const fillIcon = [ `M${g12},${g21}`, `L${g23},${g9}`, `L${hc},${g21}`, `L${g24},${g21}`, `L${g24},${g20}`, `A${g27},${g27} 0 0,1 ${g24 - g27},${g19}`, // arc 1: inner bottom-right corner `L${g25},${g19}`, // across inner bottom `A${g27},${g27} 0 0,1 ${g26},${g20}`, // arc 2: inner bottom-left corner `L${g26},${g21}`, `L${g11},${g21}`, `L${g11},${g20}`, `A${g17},${g17} 0 0,0 ${g25},${g10}`, // arc 3: outer bottom-left curve `L${hc},${g10}`, // across outer bottom `A${g17},${g17} 0 0,0 ${hc + g17},${g10 - g17}`, // arc 4: outer bottom-right curve `L${g22},${g21}`, `Z`, ].join(' ') // Outline path (path 2 in OOXML spec — traces shape with different arc winding) // Starts from right outer edge, traces clockwise: outer right → outer bottom → outer left → inner left → inner bottom → inner right → arrow // Arc A: from (g22, g20), wR=g17 hR=g17 stAng=0° swAng=90° // center = (g22-g17, g20) = (g22-g17, g20), endpoint = (g22-g17, g20+g17) // g22-g17 = g11+g14-g17 = g11 + g13*7/8 - g13*3/8 = g11 + g13/2 = g25 + g13/8 = hc? No. // Actually: g22 = g11+g14, g14 = g13*7/8, g17 = g13*3/8 // g22 - g17 = g11 + g13*7/8 - g13*3/8 = g11 + g13*4/8 = g11 + g13/2 = hc (since hc = g11 + dx2 = g11 + g13/2) // Hmm wait, dx2 = ss*3/8 and g13 = ss*3/4. So g13/2 = ss*3/8 = dx2. So hc = g11 + dx2 = g11 + g13/2. Yes! // endpoint = (hc, g20+g17) = (hc, g10)? g20+g17 = (g9+g16)+g17 = g9+g13*5/8+g13*3/8 = g9+g13 = g9+ss*3/4 // g10 = vc+dx2. g9+g13 = (vc-dx2) + 2*dx2 = vc+dx2 = g10. Yes! endpoint = (hc, g10) ✓ but wait... // Actually stAng=0° means start angle is 0°. center = (g22 - g17*cos(0), g20 - g17*sin(0)) = (g22-g17, g20). // endAng = 0+90 = 90°. endX = center.x + g17*cos(90°) = g22-g17. endY = center.y + g17*sin(90°) = g20+g17. // So endpoint = (g22-g17, g20+g17). Let's verify: g22-g17 = g11+g14-g17 = g11+g13*(7/8-3/8) = g11+g13/2 = g25+g13/8 // Hmm, g25 = g11+g17 = g11+g13*3/8. g11+g13/2 = g11+g13*4/8. That's not g25, it's g25 + g13/8. // Actually let me just compute: g11+g13/2. g13/2 is not one of the named guides. // OK, the spec says after this arc: lnTo (g25, g10). So endpoint.x must be something, then line to g25. // endpoint.x = g22-g17 = g11+g14-g17 = g11+g13*7/8-g13*3/8 = g11+g13*4/8 = g11+g13/2. // Then lnTo (g25, g10) where g25 = g11+g13*3/8. // endpointY = g20+g17 = g10. So endpoint = (g11+g13/2, g10). // Line from there to (g25, g10) is horizontal. Makes sense. // Arc B: from (g25, g10), wR=g17 hR=g17 stAng=90° swAng=90° // center = (g25, g10-g17), endAng=180° // endX = g25+g17*cos(180°) = g25-g17 = g11+g17-g17 = g11 // endY = (g10-g17)+g17*sin(180°) = g10-g17 = g20 // endpoint = (g11, g20). Then lnTo (g11, g21). // Arc C: from (g26, g20), wR=g27 hR=g27 stAng=180° swAng=-90° // center = (g26+g27, g20) = (g26+g27, g20). g26+g27 = g11+g18+g13/8 = g11+g13/4+g13/8 = g11+g13*3/8 = g25 // endAng = 180-90 = 90°. endX = g25+g27*cos(90°) = g25. endY = g20+g27*sin(90°) = g20+g27 = g19. // endpoint = (g25, g19). Hmm, but spec says lnTo(hc, g19) after this arc. // Wait: lnTo before spec says ``. So endpoint is (g25, g19), then line to (hc, g19). // Hmm actually spec says: ``. // Wait no: `L(hc, g19)` in the spec. // Arc D: from (hc, g19), wR=g27 hR=g27 stAng=90° swAng=-90° // center = (hc, g19-g27), endAng = 0°. // endX = hc+g27*cos(0°) = hc+g27. g19-g27 = g20. endY = g20+g27*sin(0°) = g20. // endpoint = (hc+g27, g20). Hmm, but g24 = g11+g16 = g11+g13*5/8. // hc+g27 = g11+g13/2+g13/8 = g11+g13*5/8 = g24. So endpoint = (g24, g20). // Then lnTo (g24, g21). Then lnTo (hc, g21). Then lnTo (g23, g9). Close. const outline = [ `M${g12},${g21}`, `L${g22},${g21}`, `L${g22},${g20}`, `A${g17},${g17} 0 0,1 ${g11 + g13 / 2},${g10}`, // arc A: outer bottom-right (0°→90°) `L${g25},${g10}`, // across outer bottom `A${g17},${g17} 0 0,1 ${g11},${g20}`, // arc B: outer bottom-left (90°→180°) `L${g11},${g21}`, `L${g26},${g21}`, `L${g26},${g20}`, `A${g27},${g27} 0 0,0 ${g25},${g19}`, // arc C: inner bottom-left (180°→90°, CCW) `L${hc},${g19}`, // across inner bottom `A${g27},${g27} 0 0,0 ${g24},${g20}`, // arc D: inner bottom-right (90°→0°, CCW) `L${g24},${g21}`, `L${hc},${g21}`, `L${g23},${g9}`, `Z`, ].join(' ') return [ { d: `${_rect(w, h)} ${fillIcon}`, fill: 'norm', stroke: false }, { d: fillIcon, fill: 'darken', stroke: false }, { d: outline, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonSound (VBA 0135): speaker icon with 3 sound wave lines // OOXML spec: 4 paths – bg+speaker cutout (norm), speaker fill (darken), speaker outline+waves (stroke), rect outline (stroke) multiPathPresets.set('actionButtonSound', (w, h) => { const { g9, g10, g11, g12, g13, hc: _hcS, vc } = _abGuides(w, h) // Guide calculations from OOXML presetShapeDefinitions.xml const g14 = g13 / 8 const g15 = (g13 * 5) / 16 const g16 = (g13 * 5) / 8 const g17 = (g13 * 11) / 16 const g18 = (g13 * 3) / 4 const g19 = (g13 * 7) / 8 // Absolute positions const g20 = g9 + g14 const g21 = g9 + g15 const g22 = g9 + g17 const g23 = g9 + g19 const g24 = g11 + g15 const g25 = g11 + g16 const g26 = g11 + g18 // Speaker shape (pentagon-like) const speaker = `M${g11},${g21} L${g11},${g22} L${g24},${g22} L${g25},${g10} L${g25},${g9} L${g24},${g21} Z` // Outline path: speaker outline (different winding) + 3 sound wave lines const speakerOutline = `M${g11},${g21} L${g24},${g21} L${g25},${g9} L${g25},${g10} L${g24},${g22} L${g11},${g22} Z` const waveLine1 = `M${g26},${g21} L${g12},${g20}` // top-right diagonal const waveLine2 = `M${g26},${vc} L${g12},${vc}` // middle horizontal const waveLine3 = `M${g26},${g22} L${g12},${g23}` // bottom-right diagonal const outlineWithWaves = `${speakerOutline} ${waveLine1} ${waveLine2} ${waveLine3}` return [ { d: `${_rect(w, h)} ${speaker}`, fill: 'norm', stroke: false }, { d: speaker, fill: 'darken', stroke: false }, { d: outlineWithWaves, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonInformation (VBA 0128): circle with "i" inside multiPathPresets.set('actionButtonInformation', (w, h) => { const { g9, g10, g11, g13, hc, vc: _vcI, dx2 } = _abGuides(w, h) const g14 = g13 / 32 const g17v = (g13 * 5) / 16 const g18v = (g13 * 3) / 8 const g19v = (g13 * 13) / 32 const g20v = (g13 * 19) / 32 const g22v = (g13 * 11) / 16 const g23v = (g13 * 13) / 16 const g24v = (g13 * 7) / 8 const g38 = (g13 * 3) / 32 const y25 = g9 + g14 const y28 = g9 + g17v const y29 = g9 + g18v const y30 = g9 + g23v const y31 = g9 + g24v const x32 = g11 + g17v const x34 = g11 + g19v const x35 = g11 + g20v const x37 = g11 + g22v const circle = `M${hc},${g9} A${dx2},${dx2} 0 1,1 ${hc},${g10} A${dx2},${dx2} 0 1,1 ${hc},${g9} Z` const dot = `M${hc},${y25} A${g38},${g38} 0 1,1 ${hc},${y25 + g38 * 2} A${g38},${g38} 0 1,1 ${hc},${y25} Z` const iBody = `M${x32},${y28} L${x37},${y28} L${x37},${y29} L${x35},${y29} L${x35},${y30} L${x37},${y30} L${x37},${y31} L${x32},${y31} L${x32},${y30} L${x34},${y30} L${x34},${y29} L${x32},${y29} Z` const iconInner = `${dot} ${iBody}` return [ { d: `${_rect(w, h)} ${circle}`, fill: 'norm', stroke: false }, { d: `${circle} ${iconInner}`, fill: 'darken', stroke: false }, { d: iconInner, fill: 'lighten', stroke: false }, { d: `${circle} ${iconInner}`, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonHome (VBA 0126): house icon with chimney and door // OOXML spec: 5 paths – bg+house cutout (norm), walls+chimney (darkenLess), roof+door (darken), // icon outline (stroke), rect outline (stroke) multiPathPresets.set('actionButtonHome', (w, h) => { const { g9, g10, g11, g12, g13, hc, vc } = _abGuides(w, h) // Guide calculations from OOXML presetShapeDefinitions.xml const g14 = g13 / 16 const g15 = g13 / 8 const g16 = (g13 * 3) / 16 const g17 = (g13 * 5) / 16 const g18 = (g13 * 7) / 16 const g19 = (g13 * 9) / 16 const g20 = (g13 * 11) / 16 const g21 = (g13 * 3) / 4 const g22 = (g13 * 13) / 16 const g23 = (g13 * 7) / 8 // Absolute positions const g24 = g9 + g14 const g25 = g9 + g16 const g26 = g9 + g17 const g27 = g9 + g21 const g28 = g11 + g15 const g29 = g11 + g18 const g30 = g11 + g19 const g31 = g11 + g20 const g32 = g11 + g22 const g33 = g11 + g23 // Path 0: background rect + full house outline cutout (norm, no stroke) // House outline: roof triangle → right side → chimney → left side → base const houseOutline = `M${hc},${g9} ` + `L${g11},${vc} L${g28},${vc} L${g28},${g10} L${g33},${g10} L${g33},${vc} L${g12},${vc} ` + `L${g32},${g26} L${g32},${g24} L${g31},${g24} L${g31},${g25} Z` // Path 1: walls + chimney (darkenLess, no stroke) // Sub-path 1: chimney bar const chimney = `M${g32},${g26} L${g32},${g24} L${g31},${g24} L${g31},${g25} Z` // Sub-path 2: house body (walls) with door cutout const walls = `M${g28},${vc} L${g28},${g10} L${g29},${g10} L${g29},${g27} L${g30},${g27} L${g30},${g10} L${g33},${g10} L${g33},${vc} Z` // Path 2: roof triangle + door (darken, no stroke) const roof = `M${hc},${g9} L${g11},${vc} L${g12},${vc} Z` const door = `M${g29},${g27} L${g30},${g27} L${g30},${g10} L${g29},${g10} Z` // Path 3: icon outline with all detail lines (none fill, stroke) const iconOutline = `M${hc},${g9} ` + `L${g31},${g25} L${g31},${g24} L${g32},${g24} L${g32},${g26} L${g12},${vc} ` + `L${g33},${vc} L${g33},${g10} L${g28},${g10} L${g28},${vc} L${g11},${vc} Z ` + // Chimney diagonal line `M${g31},${g25} L${g32},${g26} ` + // Horizontal eave line `M${g33},${vc} L${g28},${vc} ` + // Door outline `M${g29},${g10} L${g29},${g27} L${g30},${g27} L${g30},${g10}` return [ { d: `${_rect(w, h)} ${houseOutline}`, fill: 'norm', stroke: false }, { d: `${chimney} ${walls}`, fill: 'darkenLess', stroke: false }, { d: `${roof} ${door}`, fill: 'darken', stroke: false }, { d: iconOutline, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonHelp (VBA 0127): question mark "?" inside rectangle // OOXML spec: 4 paths – bg+icon cutout (norm), icon fill (darken), icon outline (stroke), rect outline (stroke) multiPathPresets.set('actionButtonHelp', (w, h) => { const { g9, g11, g13, hc, vc: _vcH } = _abGuides(w, h) // Guide calculations from OOXML presetShapeDefinitions.xml const g14 = g13 / 7 const g15 = (g13 * 3) / 14 const g16 = (g13 * 2) / 7 const g19 = (g13 * 3) / 7 const g20 = (g13 * 4) / 7 const g21 = (g13 * 17) / 28 const g23 = (g13 * 21) / 28 const g24 = (g13 * 11) / 14 const g41 = g13 / 14 const g42 = (g13 * 3) / 28 // Absolute positions const g27 = g9 + g16 const g29 = g9 + g21 const g30 = g9 + g23 const g31 = g9 + g24 const g33 = g11 + g15 const g36 = g11 + g19 const g37 = g11 + g20 // Helper: OOXML arcTo → SVG arc segment // Computes endpoint from center (derived from current point + start angle) and returns SVG A command const arcSeg = ( curX: number, curY: number, wR: number, hR: number, stDeg: number, swDeg: number ) => { const stRad = (stDeg * Math.PI) / 180 const endRad = ((stDeg + swDeg) * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hR * Math.sin(stRad) const endX = cx + wR * Math.cos(endRad) const endY = cy + hR * Math.sin(endRad) const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 return { endX, endY, svg: `A${wR},${hR} 0 ${largeArc},${sweep} ${endX},${endY}` } } // Build question mark path following OOXML arcTo sequence exactly // Start at (g33, g27) let cx = g33 let cy = g27 // Arc 1: wR=g16 hR=g16 stAng=180° swAng=180° (top semicircle, clockwise) const a1 = arcSeg(cx, cy, g16, g16, 180, 180) cx = a1.endX cy = a1.endY // Arc 2: wR=g14 hR=g15 stAng=0° swAng=90° (curve down right) const a2 = arcSeg(cx, cy, g14, g15, 0, 90) cx = a2.endX cy = a2.endY // Arc 3: wR=g41 hR=g42 stAng=270° swAng=-90° (small reverse curve) const a3 = arcSeg(cx, cy, g41, g42, 270, -90) // After arc 3, lines to stem // lnTo (g37, g30), (g36, g30), (g36, g29) // then more arcs back up // Arc 4: wR=g14 hR=g15 stAng=180° swAng=90° (inner curve going up) const a4 = arcSeg(g36, g29, g14, g15, 180, 90) // Arc 5: wR=g41 hR=g42 stAng=90° swAng=-90° (small inner reverse curve) const a5 = arcSeg(a4.endX, a4.endY, g41, g42, 90, -90) // Arc 6: wR=g14 hR=g14 stAng=0° swAng=-180° (inner top semicircle, counter-clockwise) const a6 = arcSeg(a5.endX, a5.endY, g14, g14, 0, -180) // Bottom dot circle at (hc, g31) with radius g42 const dot = `M${hc},${g31} A${g42},${g42} 0 1,1 ${hc},${g31 + g42 * 2} A${g42},${g42} 0 1,1 ${hc},${g31} Z` // Question mark path (outer shape with arcs + stem + inner cutout arcs) const qMark = `M${g33},${g27} ` + `${a1.svg} ` + `${a2.svg} ` + `${a3.svg} ` + `L${g37},${g30} L${g36},${g30} L${g36},${g29} ` + `${a4.svg} ` + `${a5.svg} ` + `${a6.svg} Z` const icon = `${qMark} ${dot}` return [ { d: `${_rect(w, h)} ${icon}`, fill: 'norm', stroke: false }, // Background with icon cutout { d: icon, fill: 'darken', stroke: false }, // Darkened icon fill { d: icon, fill: 'none', stroke: true }, // Icon outline { d: _rect(w, h), fill: 'none', stroke: true }, // Rect outline ] }) // actionButtonDocument (VBA 0134): document with folded corner multiPathPresets.set('actionButtonDocument', (w, h) => { const ss = Math.min(w, h) const hc = w / 2 const vc = h / 2 const dx2 = (ss * 3) / 8 const dx1 = (ss * 9) / 32 const g9 = vc - dx2 const g10 = vc + dx2 const g11 = hc - dx1 const g12 = hc + dx1 const g13 = (ss * 3) / 16 const g14 = g12 - g13 const g15 = g9 + g13 const doc = `M${g11},${g9} L${g14},${g9} L${g12},${g15} L${g12},${g10} L${g11},${g10} Z` const fold = `M${g14},${g9} L${g14},${g15} L${g12},${g15} Z` const outline = `${doc} M${g12},${g15} L${g14},${g15} L${g14},${g9}` return [ { d: `${_rect(w, h)} ${doc}`, fill: 'norm', stroke: false }, { d: doc, fill: 'darkenLess', stroke: false }, { d: fold, fill: 'darken', stroke: false }, { d: outline, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // actionButtonMovie (VBA 0136): film strip / camera icon multiPathPresets.set('actionButtonMovie', (w, h) => { const { g9, g11, g12, g13 } = _abGuides(w, h) // Guide values from OOXML presetShapeDefinitions.xml (fractions of g13 = ss*3/4) const g14 = (g13 * 1455) / 21600 const g15 = (g13 * 1905) / 21600 const g16 = (g13 * 2325) / 21600 const g17 = (g13 * 16155) / 21600 const g18 = (g13 * 17010) / 21600 const g19 = (g13 * 19335) / 21600 const g20 = (g13 * 19725) / 21600 const g21 = (g13 * 20595) / 21600 const g22 = (g13 * 5280) / 21600 const g23 = (g13 * 5730) / 21600 const g24 = (g13 * 6630) / 21600 const g25 = (g13 * 7492) / 21600 const g26 = (g13 * 9067) / 21600 const g27 = (g13 * 9555) / 21600 const g28 = (g13 * 13342) / 21600 const g29 = (g13 * 14580) / 21600 const g30 = (g13 * 15592) / 21600 // Composite guides: x = g11 + gN, y = g9 + gN const x31 = g11 + g14 const x32 = g11 + g15 const x33 = g11 + g16 const x34 = g11 + g17 const x35 = g11 + g18 const x36 = g11 + g19 const x37 = g11 + g20 const x38 = g11 + g21 const y39 = g9 + g22 const y40 = g9 + g23 const y41 = g9 + g24 const y42 = g9 + g25 const y43 = g9 + g26 const y44 = g9 + g27 const y45 = g9 + g28 const y46 = g9 + g29 const y47 = g9 + g30 const icon = [ `M${g11},${y39}`, `L${g11},${y44}`, `L${x31},${y44}`, `L${x32},${y43}`, `L${x33},${y43}`, `L${x33},${y47}`, `L${x35},${y47}`, `L${x35},${y45}`, `L${x36},${y45}`, `L${x38},${y46}`, `L${g12},${y46}`, `L${g12},${y41}`, `L${x38},${y41}`, `L${x37},${y42}`, `L${x35},${y42}`, `L${x35},${y41}`, `L${x34},${y40}`, `L${x32},${y40}`, `L${x31},${y39}`, `Z`, ].join(' ') return [ { d: `${_rect(w, h)} ${icon}`, fill: 'norm', stroke: false }, { d: icon, fill: 'darken', stroke: false }, { d: icon, fill: 'none', stroke: true }, { d: _rect(w, h), fill: 'none', stroke: true }, ] }) // flowChartOfflineStorage (VBA 0139): inverted triangle with horizontal base line multiPathPresets.set('flowChartOfflineStorage', (w, h) => { const tri = `M0,0 L${w},0 L${w / 2},${h} Z` const lineY = (h * 4) / 5 const line = `M${(w * 2) / 5},${lineY} L${(w * 3) / 5},${lineY}` return [ { d: tri, fill: 'norm', stroke: false }, { d: line, fill: 'none', stroke: true }, { d: tri, fill: 'none', stroke: true }, ] }) multiPathPresets.set('cube', (w, h, adjustments) => { const a = Math.min(Math.max(adj(adjustments, 'adj', 25000), 0), 0.45) const depth = Math.min(w, h) * a const front = [ `M0,${depth}`, `L${w - depth},${depth}`, `L${w - depth},${h}`, `L0,${h}`, 'Z', ].join(' ') const top = [`M0,${depth}`, `L${depth},0`, `L${w},0`, `L${w - depth},${depth}`, 'Z'].join(' ') const right = [ `M${w - depth},${depth}`, `L${w},0`, `L${w},${h - depth}`, `L${w - depth},${h}`, 'Z', ].join(' ') return [ { d: front, fill: 'norm', stroke: true }, { d: top, fill: 'lightenLess', stroke: true }, { d: right, fill: 'darkenLess', stroke: true }, ] }) multiPathPresets.set('bevel', (w, h, adjustments) => { // OOXML bevel: picture-frame shape with 4 beveled faces + center rect. // adj = bevel thickness (default 12500 = 12.5% of min(w,h)) const a = Math.min(Math.max(adj(adjustments, 'adj', 12500), 0), 0.45) const t = Math.min(w, h) * a const inner = `M${t},${t} L${w - t},${t} L${w - t},${h - t} L${t},${h - t} Z` const top = `M0,0 L${w},0 L${w - t},${t} L${t},${t} Z` const bottom = `M0,${h} L${t},${h - t} L${w - t},${h - t} L${w},${h} Z` const left = `M0,0 L${t},${t} L${t},${h - t} L0,${h} Z` const right = `M${w},0 L${w},${h} L${w - t},${h - t} L${w - t},${t} Z` return [ { d: inner, fill: 'norm', stroke: true }, { d: top, fill: 'lightenLess', stroke: true }, { d: right, fill: 'darkenLess', stroke: true }, { d: bottom, fill: 'darken', stroke: true }, { d: left, fill: 'lighten', stroke: true }, ] }) multiPathPresets.set('leftRightRibbon', (w, h, adjustments) => { // OOXML leftRightRibbon: 3-path shape (body + center fold shadow + stroke outline). // adj1=50000 (band height), adj2=50000 (notch width), adj3=16667 (wave amplitude). const ss = Math.min(w, h) const wd2 = w / 2 const wd32 = w / 32 const hc = w / 2 const vc = h / 2 const a3 = Math.min(Math.max((adjustments?.get('adj3') ?? 16667) / 100000, 0), 0.33333) const maxAdj1 = 1 - a3 const a1 = Math.min(Math.max((adjustments?.get('adj1') ?? 50000) / 100000, 0), maxAdj1) const w1 = wd2 - wd32 const maxAdj2 = w1 / ss const a2 = Math.min(Math.max((adjustments?.get('adj2') ?? 50000) / 100000, 0), maxAdj2) const x1 = ss * a2 const x4 = w - x1 const dy1 = (h * a1) / 2 const dy2 = (-h * a3) / 2 const ly1 = vc + dy2 - dy1 const ry4 = vc + dy1 - dy2 const ly2 = ly1 + dy1 const ry3 = h - ly2 const ly4 = ly2 * 2 const ry1 = h - ly4 const ly3 = ly4 - ly1 const ry2 = h - ly3 const hR = (a3 * ss) / 4 const x2 = hc - wd32 const x3 = hc + wd32 const y1 = ly1 + hR const y2 = ry2 - hR // Helper: compute OOXML arcTo → SVG arc segment const arcTo = ( curX: number, curY: number, wR: number, hRad: number, stDeg: number, swDeg: number ) => { const stRad = (stDeg * Math.PI) / 180 const endRad = ((stDeg + swDeg) * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hRad * Math.sin(stRad) const endX = cx + wR * Math.cos(endRad) const endY = cy + hRad * Math.sin(endRad) const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 return { endX, endY, svg: `A${wR},${hRad} 0 ${largeArc},${sweep} ${endX},${endY}` } } // Path 1: Main body (fill, no stroke) const cx1 = hc const cy1 = ly1 // after lnTo (hc, ly1) const arc1a = arcTo(cx1, cy1, wd32, hR, 270, 180) const arc1b = arcTo(arc1a.endX, arc1a.endY, wd32, hR, 270, -180) const cx1c = hc const cy1c = ry4 // after lnTo (hc, ry4) const arc1c = arcTo(cx1c, cy1c, wd32, hR, 90, 90) const body = [ `M0,${ly2}`, `L${x1},0`, `L${x1},${ly1}`, `L${hc},${ly1}`, arc1a.svg, arc1b.svg, `L${x4},${ry2}`, `L${x4},${ry1}`, `L${w},${ry3}`, `L${x4},${h}`, `L${x4},${ry4}`, `L${hc},${ry4}`, arc1c.svg, `L${x2},${ly3}`, `L${x1},${ly3}`, `L${x1},${ly4}`, 'Z', ].join(' ') // Path 2: Center fold shadow (darkenLess, no stroke) const arc2a = arcTo(x3, y1, wd32, hR, 0, 90) const arc2b = arcTo(arc2a.endX, arc2a.endY, wd32, hR, 270, -180) const shadow = [`M${x3},${y1}`, arc2a.svg, arc2b.svg, `L${x3},${ry2}`, 'Z'].join(' ') // Path 3: Stroke outline (no fill) — same as body + interior fold lines const outline = [body, `M${x3},${y1} L${x3},${ry2}`, `M${x2},${y2} L${x2},${ly3}`].join(' ') return [ { d: body, fill: 'norm', stroke: false }, { d: shadow, fill: 'darkenLess', stroke: false }, { d: outline, fill: 'none', stroke: true }, ] }) multiPathPresets.set('ellipseRibbon', (w, h, adjustments) => { // OOXML ellipseRibbon: ribbon with parabolic curved bottom edge // 3 paths: body (fill=norm), darkenLess shadow folds, outline (fill=none) const adj1 = adjustments?.get('adj1') ?? 25000 const adj2 = adjustments?.get('adj2') ?? 50000 const adj3 = adjustments?.get('adj3') ?? 12500 const a1 = Math.max(0, Math.min(adj1, 100000)) const a2 = Math.max(25000, Math.min(adj2, 75000)) const q10 = 100000 - a1 const q11 = q10 / 2 const q12 = a1 - q11 const minAdj3 = Math.max(0, q12) const a3 = Math.max(minAdj3, Math.min(adj3, a1)) const dx2 = (w * a2) / 200000 const x2 = w / 2 - dx2 const x3 = x2 + w / 8 const x4 = w - x3 const x5 = w - x2 const x6 = w - w / 8 const dy1 = (h * a3) / 100000 const f1 = w > 0 ? (4 * dy1) / w : 0 // Parabola: p(x) = f1 * x * (1 - x/w) const parab = (x: number) => f1 * (x - (x * x) / w) const y1 = parab(x3) const cx1 = x3 / 2 const cy1 = f1 * cx1 // Bezier control (approximation) const cx2 = w - cx1 // q1 redefined: total fold height const q1 = (h * a1) / 100000 const dy3 = q1 - dy1 const q5 = parab(x2) const y3 = q5 + dy3 const q6 = dy1 + dy3 - y3 const q7 = q6 + dy1 const cy3 = q7 + dy3 const rh = h - q1 const q8 = (dy1 * 14) / 16 const y2 = (q8 + rh) / 2 const y5 = q5 + rh const y6 = y3 + rh const cx4 = x2 / 2 const cy4 = f1 * cx4 + rh const cx5 = w - cx4 const cy6 = cy3 + rh const y7 = y1 + dy3 const cy7 = q1 + q1 - y7 const hc = w / 2 const wd8 = w / 8 // Path 1: body fill (stroke=false) const body = [ `M0,0`, `Q${cx1},${cy1} ${x3},${y1}`, `L${x2},${y3}`, `Q${hc},${cy3} ${x5},${y3}`, `L${x4},${y1}`, `Q${cx2},${cy1} ${w},0`, `L${x6},${y2}`, `L${w},${rh}`, `Q${cx5},${cy4} ${x5},${y5}`, `L${x5},${y6}`, `Q${hc},${cy6} ${x2},${y6}`, `L${x2},${y5}`, `Q${cx4},${cy4} 0,${rh}`, `L${wd8},${y2}`, `Z`, ].join(' ') // Path 2: darkenLess shadow folds (stroke=false) const shadow = [ `M${x3},${y7}`, `L${x3},${y1}`, `L${x2},${y3}`, `Q${hc},${cy3} ${x5},${y3}`, `L${x4},${y1}`, `L${x4},${y7}`, `Q${hc},${cy7} ${x3},${y7}`, `Z`, ].join(' ') // Path 3: outline (fill=none) const outline = [ `M0,0`, `Q${cx1},${cy1} ${x3},${y1}`, `L${x2},${y3}`, `Q${hc},${cy3} ${x5},${y3}`, `L${x4},${y1}`, `Q${cx2},${cy1} ${w},0`, `L${x6},${y2}`, `L${w},${rh}`, `Q${cx5},${cy4} ${x5},${y5}`, `L${x5},${y6}`, `Q${hc},${cy6} ${x2},${y6}`, `L${x2},${y5}`, `Q${cx4},${cy4} 0,${rh}`, `L${wd8},${y2}`, `Z`, `M${x2},${y5} L${x2},${y3}`, `M${x5},${y3} L${x5},${y5}`, `M${x3},${y1} L${x3},${y7}`, `M${x4},${y7} L${x4},${y1}`, ].join(' ') return [ { d: body, fill: 'norm', stroke: false }, { d: shadow, fill: 'darkenLess', stroke: false }, { d: outline, fill: 'none', stroke: true }, ] }) multiPathPresets.set('ellipseRibbon2', (w, h, adjustments) => { // OOXML ellipseRibbon2: inverted ribbon with parabolic curved top edge // 3 paths: body (fill=norm), darkenLess shadow folds, outline (fill=none) // All y-values computed as b - value (measured from bottom) const adj1 = adjustments?.get('adj1') ?? 25000 const adj2 = adjustments?.get('adj2') ?? 50000 const adj3 = adjustments?.get('adj3') ?? 12500 const a1 = Math.max(0, Math.min(adj1, 100000)) const a2 = Math.max(25000, Math.min(adj2, 75000)) const q10 = 100000 - a1 const q11 = q10 / 2 const q12 = a1 - q11 const minAdj3 = Math.max(0, q12) const a3 = Math.max(minAdj3, Math.min(adj3, a1)) const b = h const dx2 = (w * a2) / 200000 const x2 = w / 2 - dx2 const x3 = x2 + w / 8 const x4 = w - x3 const x5 = w - x2 const x6 = w - w / 8 const dy1 = (h * a3) / 100000 const f1 = w > 0 ? (4 * dy1) / w : 0 // u1 = parabola at x3 const u1 = f1 * (x3 - (x3 * x3) / w) const y1 = b - u1 const cx1 = x3 / 2 const cu1 = f1 * cx1 const cy1 = b - cu1 const cx2 = w - cx1 // q1 redefined: total fold height const q1 = (h * a1) / 100000 const dy3 = q1 - dy1 const q5 = f1 * (x2 - (x2 * x2) / w) const u3 = q5 + dy3 const y3 = b - u3 const q6 = dy1 + dy3 - u3 const q7 = q6 + dy1 const cu3 = q7 + dy3 const cy3 = b - cu3 const rh = b - q1 const q8 = (dy1 * 14) / 16 const u2 = (q8 + rh) / 2 const y2 = b - u2 const u5 = q5 + rh const y5 = b - u5 const u6 = u3 + rh const y6 = b - u6 const cx4 = x2 / 2 const cu4 = f1 * cx4 + rh const cy4 = b - cu4 const cx5 = w - cx4 const cu6 = cu3 + rh const cy6 = b - cu6 const u7 = u1 + dy3 const y7 = b - u7 const cu7 = q1 + q1 - u7 const cy7 = b - cu7 const hc = w / 2 const wd8 = w / 8 // Path 1: body fill (stroke=false) const body = [ `M0,${b}`, `Q${cx1},${cy1} ${x3},${y1}`, `L${x2},${y3}`, `Q${hc},${cy3} ${x5},${y3}`, `L${x4},${y1}`, `Q${cx2},${cy1} ${w},${b}`, `L${x6},${y2}`, `L${w},${q1}`, `Q${cx5},${cy4} ${x5},${y5}`, `L${x5},${y6}`, `Q${hc},${cy6} ${x2},${y6}`, `L${x2},${y5}`, `Q${cx4},${cy4} 0,${q1}`, `L${wd8},${y2}`, `Z`, ].join(' ') // Path 2: darkenLess shadow folds (stroke=false) const shadow = [ `M${x3},${y7}`, `L${x3},${y1}`, `L${x2},${y3}`, `Q${hc},${cy3} ${x5},${y3}`, `L${x4},${y1}`, `L${x4},${y7}`, `Q${hc},${cy7} ${x3},${y7}`, `Z`, ].join(' ') // Path 3: outline (fill=none) const outline = [ `M0,${b}`, `L${wd8},${y2}`, `L0,${q1}`, `Q${cx4},${cy4} ${x2},${y5}`, `L${x2},${y6}`, `Q${hc},${cy6} ${x5},${y6}`, `L${x5},${y5}`, `Q${cx5},${cy4} ${w},${q1}`, `L${x6},${y2}`, `L${w},${b}`, `Q${cx2},${cy1} ${x4},${y1}`, `L${x5},${y3}`, `Q${hc},${cy3} ${x2},${y3}`, `L${x3},${y1}`, `Q${cx1},${cy1} 0,${b}`, `Z`, `M${x2},${y3} L${x2},${y5}`, `M${x5},${y5} L${x5},${y3}`, `M${x3},${y7} L${x3},${y1}`, `M${x4},${y1} L${x4},${y7}`, ].join(' ') return [ { d: body, fill: 'norm', stroke: false }, { d: shadow, fill: 'darkenLess', stroke: false }, { d: outline, fill: 'none', stroke: true }, ] }) multiPathPresets.set('smileyFace', (w, h, adjustments) => { // OOXML smileyFace: 4 paths — face(norm), eyes(darkenLess), smile(none), outline(none+stroke) const wd2 = w / 2 const hd2 = h / 2 const hc = w / 2 const vc = h / 2 // Adjustment: smile amplitude (default 4653, range -4653..4653) const rawAdj = adjustments?.get('adj') ?? 4653 const a = Math.max(-4653, Math.min(rawAdj, 4653)) // Eye positions (OOXML exact) const x2 = (w * 6215) / 21600 const x3 = (w * 13135) / 21600 const y1 = (h * 7570) / 21600 const wR = (w * 1125) / 21600 const hR = (h * 1125) / 21600 // Smile curve positions (OOXML exact) const x1 = (w * 4969) / 21699 const x4 = (w * 16640) / 21600 const y3 = (h * 16515) / 21600 const dy2 = (h * a) / 100000 const y2 = y3 - dy2 const y4 = y3 + dy2 const dy3 = (h * a) / 50000 const y5 = y4 + dy3 // Path 1: face ellipse (fill=norm, stroke=false) — two half-arcs for full circle const face = `M${w},${vc} A${wd2},${hd2} 0 1,1 0,${vc} A${wd2},${hd2} 0 1,1 ${w},${vc} Z` // Path 2: eyes (fill=darkenLess) — two small ellipses at OOXML positions (two half-arcs each) const leftEye = `M${(x2 + wR).toFixed(2)},${y1.toFixed(2)} A${wR.toFixed(2)},${hR.toFixed(2)} 0 1,1 ${(x2 - wR).toFixed(2)},${y1.toFixed(2)} A${wR.toFixed(2)},${hR.toFixed(2)} 0 1,1 ${(x2 + wR).toFixed(2)},${y1.toFixed(2)} Z` const rightEye = `M${(x3 + wR).toFixed(2)},${y1.toFixed(2)} A${wR.toFixed(2)},${hR.toFixed(2)} 0 1,1 ${(x3 - wR).toFixed(2)},${y1.toFixed(2)} A${wR.toFixed(2)},${hR.toFixed(2)} 0 1,1 ${(x3 + wR).toFixed(2)},${y1.toFixed(2)} Z` // Path 3: smile (fill=none) — quadratic Bezier (OOXML quadBezTo) const smile = `M${x1.toFixed(2)},${y2.toFixed(2)} Q${hc.toFixed(2)},${y5.toFixed(2)} ${x4.toFixed(2)},${y2.toFixed(2)}` // Path 4: face outline (fill=none, stroke=true) — same as path 1 const outline = `M${w},${vc} A${wd2},${hd2} 0 1,1 0,${vc} A${wd2},${hd2} 0 1,1 ${w},${vc} Z` return [ { d: face, fill: 'norm', stroke: false }, { d: `${leftEye} ${rightEye}`, fill: 'darkenLess', stroke: false }, { d: smile, fill: 'none', stroke: true }, { d: outline, fill: 'none', stroke: true }, ] }) multiPathPresets.set('foldedCorner', (w, h, adjustments) => { const a = adj(adjustments, 'adj', 16667) const fold = Math.min(w, h) * a * 0.7 const body = `M0,0 L${w},0 L${w},${h - fold} L${w - fold},${h} L0,${h} Z` const foldFace = `M${w - fold},${h} L${w - fold},${h - fold} L${w},${h - fold} Z` const crease = `M${w - fold},${h} L${w - fold},${h - fold}` return [ { d: body, fill: 'norm', stroke: true }, { d: foldFace, fill: 'darkenLess', stroke: false }, { d: crease, fill: 'none', stroke: true }, ] }) multiPathPresets.set('can', (w, h, adjustments) => { // OOXML: 3 paths — body (norm), top face (lighten), outline (stroke-only) const ss = Math.min(w, h) const maxAdj = (50000 * h) / ss const a = Math.min(Math.max(adjustments?.get('adj') ?? 25000, 0), maxAdj) const y1 = (ss * a) / 200000 const y3 = h - y1 const wd2 = w / 2 const arcSeg = ( curX: number, curY: number, wR: number, hR: number, stDeg: number, swDeg: number ) => { const stRad = (stDeg * Math.PI) / 180 const endRad = ((stDeg + swDeg) * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hR * Math.sin(stRad) const endX = cx + wR * Math.cos(endRad) const endY = cy + hR * Math.sin(endRad) const largeArc = Math.abs(swDeg) > 180 ? 1 : 0 const sweep = swDeg > 0 ? 1 : 0 return { endX, endY, svg: `A${wR},${hR} 0 ${largeArc},${sweep} ${endX},${endY}` } } // Path 1: Body (stroke:false, fill:norm) const a1 = arcSeg(0, y1, wd2, y1, 180, -180) const a2 = arcSeg(w, y3, wd2, y1, 0, 180) const body = `M0,${y1} ${a1.svg} L${w},${y3} ${a2.svg} Z` // Path 2: Top face (stroke:false, fill:lighten) const a3 = arcSeg(0, y1, wd2, y1, 180, 180) const a4 = arcSeg(a3.endX, a3.endY, wd2, y1, 0, 180) const topFace = `M0,${y1} ${a3.svg} ${a4.svg} Z` // Path 3: Outline (fill:none, stroke:true) const a5 = arcSeg(w, y1, wd2, y1, 0, 180) const a6 = arcSeg(a5.endX, a5.endY, wd2, y1, 180, 180) const a7 = arcSeg(w, y3, wd2, y1, 0, 180) const outline = `M${w},${y1} ${a5.svg} ${a6.svg} L${w},${y3} ${a7.svg} L0,${y1}` return [ { d: body, fill: 'norm', stroke: false }, { d: topFace, fill: 'lighten', stroke: false }, { d: outline, fill: 'none', stroke: true }, ] }) multiPathPresets.set('curvedrightarrow', (w, h, adjustments) => buildCurvedArrowMultiPath('curvedRightArrow', w, h, adjustments) ) multiPathPresets.set('curvedleftarrow', (w, h, adjustments) => buildCurvedArrowMultiPath('curvedLeftArrow', w, h, adjustments) ) multiPathPresets.set('curveduparrow', (w, h, adjustments) => buildCurvedVerticalArrowMultiPath('curvedUpArrow', w, h, adjustments) ) multiPathPresets.set('curveddownarrow', (w, h, adjustments) => buildCurvedVerticalArrowMultiPath('curvedDownArrow', w, h, adjustments) ) multiPathPresets.set('bordercallout1', (w, h, adjustments) => { // OOXML: filled+stroked rectangle body + separate leader line (stroke-only). const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 112500)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -38333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},${y1} L${x2},${y2}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('accentcallout1', (w, h, adjustments) => { // OOXML: filled rect + accent bar at x1 + 1-segment callout line const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 112500)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -38333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('accentcallout2', (w, h, adjustments) => { // OOXML: filled rect + accent bar at x1 + 2-segment callout line const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 112500)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -46667)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('accentcallout3', (w, h, adjustments) => { // OOXML: filled rect + accent bar at x1 + 3-segment callout line const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 100000)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -16667)) / 100000 const y4 = (h * (adjustments?.get('adj7') ?? 112963)) / 100000 const x4 = (w * (adjustments?.get('adj8') ?? -8333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4}`, fill: 'none', stroke: true }, ] }) // --- callout1/2/3: filled rect (no stroke) + callout line segments --- multiPathPresets.set('callout1', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 112500)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -38333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},${y1} L${x2},${y2}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('callout2', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 112500)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -46667)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('callout3', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 100000)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -16667)) / 100000 const y4 = (h * (adjustments?.get('adj7') ?? 112963)) / 100000 const x4 = (w * (adjustments?.get('adj8') ?? -8333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4}`, fill: 'none', stroke: true }, ] }) // --- borderCallout2/3: filled+stroked rect + callout line segments --- multiPathPresets.set('bordercallout2', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 112500)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -46667)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('bordercallout3', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 100000)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -16667)) / 100000 const y4 = (h * (adjustments?.get('adj7') ?? 112963)) / 100000 const x4 = (w * (adjustments?.get('adj8') ?? -8333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4}`, fill: 'none', stroke: true }, ] }) // --- accentBorderCallout1/2/3: filled+stroked rect + accent bar + callout line --- multiPathPresets.set('accentbordercallout1', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 112500)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -38333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('accentbordercallout2', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 112500)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -46667)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('accentbordercallout3', (w, h, adjustments) => { const y1 = (h * (adjustments?.get('adj1') ?? 18750)) / 100000 const x1 = (w * (adjustments?.get('adj2') ?? -8333)) / 100000 const y2 = (h * (adjustments?.get('adj3') ?? 18750)) / 100000 const x2 = (w * (adjustments?.get('adj4') ?? -16667)) / 100000 const y3 = (h * (adjustments?.get('adj5') ?? 100000)) / 100000 const x3 = (w * (adjustments?.get('adj6') ?? -16667)) / 100000 const y4 = (h * (adjustments?.get('adj7') ?? 112963)) / 100000 const x4 = (w * (adjustments?.get('adj8') ?? -8333)) / 100000 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: true }, { d: `M${x1},0 L${x1},${h}`, fill: 'none', stroke: true }, { d: `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4}`, fill: 'none', stroke: true }, ] }) // Chart placeholders: frame + guide lines. // PowerPoint uses these as pre-chart placeholders (chartX / chartPlus / chartStar). multiPathPresets.set('chartx', (w, h) => { return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M0,0 L${w},${h} M${w},0 L0,${h}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('chartplus', (w, h) => { const cx = w / 2 const cy = h / 2 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M${cx},0 L${cx},${h} M0,${cy} L${w},${cy}`, fill: 'none', stroke: true }, ] }) multiPathPresets.set('chartstar', (w, h) => { // OOXML: 3 guide paths — 2 diagonals + 1 vertical (no horizontal center line) const cx = w / 2 return [ { d: `M0,0 L${w},0 L${w},${h} L0,${h} Z`, fill: 'norm', stroke: false }, { d: `M0,0 L${w},${h} M${w},0 L0,${h} M${cx},0 L${cx},${h}`, fill: 'none', stroke: true, }, ] }) /** * Helper: compute OOXML arcTo endpoint and SVG arc command from current position. * OOXML arcTo: center = curPos - radius*dir(stAng), endpoint = center + radius*dir(stAng+swAng) * Returns { svgArc, endX, endY } */ function ooArcTo( curX: number, curY: number, wR: number, hR: number, stAngDeg: number, swAngDeg: number ): { svg: string; x: number; y: number } { const stRad = (stAngDeg * Math.PI) / 180 const cx = curX - wR * Math.cos(stRad) const cy = curY - hR * Math.sin(stRad) const endRad = ((stAngDeg + swAngDeg) * Math.PI) / 180 const ex = cx + wR * Math.cos(endRad) const ey = cy + hR * Math.sin(endRad) const absSweep = Math.abs(swAngDeg) const largeArc = absSweep > 180 ? 1 : 0 const sweepFlag = swAngDeg >= 0 ? 1 : 0 return { svg: `A${wR},${hR} 0 ${largeArc},${sweepFlag} ${ex},${ey}`, x: ex, y: ey } } // --- ribbon (OOXML spec: 3 paths with arcTo, adj1=16667, adj2=50000) --- // Ribbon with tails at top, front panel at bottom. Three paths: body, darkenLess folds, outline. multiPathPresets.set('ribbon', (w, h, adjustments) => { const adj1Raw = adjustments?.get('adj1') ?? 16667 const adj2Raw = adjustments?.get('adj2') ?? 50000 const a1 = Math.min(Math.max(adj1Raw, 0), 33333) const a2 = Math.min(Math.max(adj2Raw, 25000), 75000) const hc = w / 2 const wd8 = w / 8 const wd32 = w / 32 const x10 = w - wd8 const dx2 = (w * a2) / 200000 const x2 = hc - dx2 const x9 = hc + dx2 const x3 = x2 + wd32 const x8 = x9 - wd32 const x5 = x2 + wd8 const x6 = x9 - wd8 const x4 = x5 - wd32 const x7 = x6 + wd32 const y1 = (h * a1) / 200000 const y2 = (h * a1) / 100000 const y4 = h - y2 const y3 = y4 / 2 const hR = (h * a1) / 400000 const y5 = h - hR const y6 = y2 - hR let cx: number let cy: number let arc // Path 1: body fill (stroke=false) const p1: string[] = [] cx = 0 cy = 0 p1.push(`M${0},${0}`) p1.push(`L${x4},${0}`) cx = x4 cy = 0 arc = ooArcTo(cx, cy, wd32, hR, 270, 180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x3},${y1}`) cx = x3 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, -180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x8},${y2}`) cx = x8 cy = y2 arc = ooArcTo(cx, cy, wd32, hR, 90, -180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x7},${y1}`) cx = x7 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, 180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${w},${0}`) p1.push(`L${x10},${y3}`) p1.push(`L${w},${y4}`) p1.push(`L${x9},${y4}`) p1.push(`L${x9},${y5}`) cx = x9 cy = y5 arc = ooArcTo(cx, cy, wd32, hR, 0, 90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x3},${h}`) cx = x3 cy = h arc = ooArcTo(cx, cy, wd32, hR, 90, 90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x2},${y4}`) p1.push(`L${0},${y4}`) p1.push(`L${wd8},${y3}`) p1.push('Z') // Path 2: darkenLess folds (stroke=false) const p2: string[] = [] // Left fold cx = x5 cy = hR p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, wd32, hR, 0, 90) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x3},${y1}`) cx = x3 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, -180) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x5},${y2}`) p2.push('Z') // Right fold cx = x6 cy = hR p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, wd32, hR, 180, -90) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x8},${y1}`) cx = x8 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, 180) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x6},${y2}`) p2.push('Z') // Path 3: outline (fill=none, includes fold lines) const p3: string[] = [] cx = 0 cy = 0 p3.push(`M${0},${0}`) p3.push(`L${x4},${0}`) cx = x4 cy = 0 arc = ooArcTo(cx, cy, wd32, hR, 270, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x3},${y1}`) cx = x3 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, -180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x8},${y2}`) cx = x8 cy = y2 arc = ooArcTo(cx, cy, wd32, hR, 90, -180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x7},${y1}`) cx = x7 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${w},${0}`) p3.push(`L${x10},${y3}`) p3.push(`L${w},${y4}`) p3.push(`L${x9},${y4}`) p3.push(`L${x9},${y5}`) cx = x9 cy = y5 arc = ooArcTo(cx, cy, wd32, hR, 0, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x3},${h}`) cx = x3 cy = h arc = ooArcTo(cx, cy, wd32, hR, 90, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x2},${y4}`) p3.push(`L${0},${y4}`) p3.push(`L${wd8},${y3}`) p3.push('Z') // Fold lines p3.push(`M${x5},${hR} L${x5},${y2}`) p3.push(`M${x6},${y2} L${x6},${hR}`) p3.push(`M${x2},${y4} L${x2},${y6}`) p3.push(`M${x9},${y6} L${x9},${y4}`) return [ { d: p1.join(' '), fill: 'norm', stroke: false }, { d: p2.join(' '), fill: 'darkenLess', stroke: false }, { d: p3.join(' '), fill: 'none', stroke: true }, ] }) // --- ribbon2 (OOXML spec: 3 paths, inverted ribbon with tails at bottom) --- multiPathPresets.set('ribbon2', (w, h, adjustments) => { const adj1Raw = adjustments?.get('adj1') ?? 16667 const adj2Raw = adjustments?.get('adj2') ?? 50000 const a1 = Math.min(Math.max(adj1Raw, 0), 33333) const a2 = Math.min(Math.max(adj2Raw, 25000), 75000) const hc = w / 2 const wd8 = w / 8 const wd32 = w / 32 const x10 = w - wd8 const dx2 = (w * a2) / 200000 const x2 = hc - dx2 const x9 = hc + dx2 const x3 = x2 + wd32 const x8 = x9 - wd32 const x5 = x2 + wd8 const x6 = x9 - wd8 const x4 = x5 - wd32 const x7 = x6 + wd32 const dy1 = (h * a1) / 200000 const y1 = h - dy1 const dy2 = (h * a1) / 100000 const y2 = h - dy2 const y4 = dy2 const y3 = (y4 + h) / 2 const hR = (h * a1) / 400000 const y6 = h - hR const y7 = y1 - hR let cx: number let cy: number let arc // Path 1: body fill (stroke=false) const p1: string[] = [] p1.push(`M${0},${h}`) p1.push(`L${x4},${h}`) cx = x4 cy = h arc = ooArcTo(cx, cy, wd32, hR, 90, -180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x3},${y1}`) cx = x3 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, 180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x8},${y2}`) cx = x8 cy = y2 arc = ooArcTo(cx, cy, wd32, hR, 270, 180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x7},${y1}`) cx = x7 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, -180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${w},${h}`) p1.push(`L${x10},${y3}`) p1.push(`L${w},${y4}`) p1.push(`L${x9},${y4}`) p1.push(`L${x9},${hR}`) cx = x9 cy = hR arc = ooArcTo(cx, cy, wd32, hR, 0, -90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x3},${0}`) cx = x3 cy = 0 arc = ooArcTo(cx, cy, wd32, hR, 270, -90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x2},${y4}`) p1.push(`L${0},${y4}`) p1.push(`L${wd8},${y3}`) p1.push('Z') // Path 2: darkenLess folds (stroke=false) const p2: string[] = [] // Left fold cx = x5 cy = y6 p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, wd32, hR, 0, -90) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x3},${y1}`) cx = x3 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, 180) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x5},${y2}`) p2.push('Z') // Right fold cx = x6 cy = y6 p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, wd32, hR, 180, 90) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x8},${y1}`) cx = x8 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, -180) p2.push(arc.svg) cx = arc.x cy = arc.y p2.push(`L${x6},${y2}`) p2.push('Z') // Path 3: outline (fill=none) const p3: string[] = [] p3.push(`M${0},${h}`) p3.push(`L${wd8},${y3}`) p3.push(`L${0},${y4}`) p3.push(`L${x2},${y4}`) p3.push(`L${x2},${hR}`) cx = x2 cy = hR arc = ooArcTo(cx, cy, wd32, hR, 180, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x8},${0}`) cx = x8 cy = 0 arc = ooArcTo(cx, cy, wd32, hR, 270, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x9},${y4}`) p3.push(`L${w},${y4}`) p3.push(`L${x10},${y3}`) p3.push(`L${w},${h}`) p3.push(`L${x7},${h}`) cx = x7 cy = h arc = ooArcTo(cx, cy, wd32, hR, 90, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x8},${y1}`) cx = x8 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 90, -180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x3},${y2}`) cx = x3 cy = y2 arc = ooArcTo(cx, cy, wd32, hR, 270, -180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x4},${y1}`) cx = x4 cy = y1 arc = ooArcTo(cx, cy, wd32, hR, 270, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push('Z') // Fold lines p3.push(`M${x5},${y2} L${x5},${y6}`) p3.push(`M${x6},${y6} L${x6},${y2}`) p3.push(`M${x2},${y7} L${x2},${y4}`) p3.push(`M${x9},${y4} L${x9},${y7}`) return [ { d: p1.join(' '), fill: 'norm', stroke: false }, { d: p2.join(' '), fill: 'darkenLess', stroke: false }, { d: p3.join(' '), fill: 'none', stroke: true }, ] }) // --- horizontalScroll (OOXML spec: 3 paths with arcTo) --- multiPathPresets.set('horizontalscroll', (w, h, adjustments) => { const adjVal = adjustments?.get('adj') ?? 12500 const a = Math.min(Math.max(adjVal, 0), 25000) const ss = Math.min(w, h) const ch = (ss * a) / 100000 const ch2 = ch / 2 const ch4 = ch / 4 const y3 = ch + ch2 const y4 = ch + ch const y6 = h - ch const y7 = h - ch2 const y5 = y6 - ch2 const x3 = w - ch const x4 = w - ch2 // Path 1: main fill (stroke=false) const p1: string[] = [] let cx: number let cy: number // moveTo (r, ch2) = (w, ch2) cx = w cy = ch2 p1.push(`M${cx},${cy}`) // arcTo wR=ch2 hR=ch2 stAng=0 swAng=cd4(90°) let arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p1.push(arc.svg) cx = arc.x cy = arc.y // lnTo (x4, ch2) — but after the arc we should be at (x4, 0)… wait // Actually: arcTo from (w, ch2) with stAng=0 swAng=90° → center=(w-ch2, ch2), end=(w-ch2, 0)=x4,0 // Then lnTo (x4, ch2)... hmm, this goes from top-right curl area // Let me re-read: lnTo pt x="x4" y="ch2"... that doesn't match. Wait, the lnTo goes DOWN. // After arc: we're at (x4, 0). lnTo (x4, ch2): p1.push(`L${x4},${ch2}`) // arcTo wR=ch4 hR=ch4 stAng=0 swAng=cd2(180°) arc = ooArcTo(x4, ch2, ch4, ch4, 0, 180) p1.push(arc.svg) cx = arc.x cy = arc.y // lnTo (x3, ch) p1.push(`L${x3},${ch}`) // lnTo (ch2, ch) p1.push(`L${ch2},${ch}`) // arcTo wR=ch2 hR=ch2 stAng=3cd4(270°) swAng=-5400000(-90°) cx = ch2 cy = ch arc = ooArcTo(cx, cy, ch2, ch2, 270, -90) p1.push(arc.svg) cx = arc.x cy = arc.y // lnTo (0, y7) p1.push(`L${0},${y7}`) // arcTo wR=ch2 hR=ch2 stAng=cd2(180°) swAng=-10800000(-180°) cx = 0 cy = y7 arc = ooArcTo(cx, cy, ch2, ch2, 180, -180) p1.push(arc.svg) cx = arc.x cy = arc.y // lnTo (ch, y6) p1.push(`L${ch},${y6}`) // lnTo (x4, y6) p1.push(`L${x4},${y6}`) // arcTo wR=ch2 hR=ch2 stAng=cd4(90°) swAng=-5400000(-90°) cx = x4 cy = y6 arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p1.push(arc.svg) p1.push('Z') // Sub-path 2 in Path 1: left bottom curl circle cx = ch2 cy = y4 p1.push(`M${cx},${cy}`) // arcTo wR=ch2 hR=ch2 stAng=cd4(90°) swAng=-5400000(-90°) arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p1.push(arc.svg) cx = arc.x cy = arc.y // arcTo wR=ch4 hR=ch4 stAng=0 swAng=-10800000(-180°) arc = ooArcTo(cx, cy, ch4, ch4, 0, -180) p1.push(arc.svg) p1.push('Z') // Path 2: darkenLess fill (stroke=false) — shadow areas const p2: string[] = [] // Sub-path 1: same as path1 sub-path2 (left bottom curl) cx = ch2 cy = y4 p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p2.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch4, ch4, 0, -180) p2.push(arc.svg) p2.push('Z') // Sub-path 2: right top curl cx = x4 cy = ch p2.push(`M${cx},${cy}`) // arcTo wR=ch2 hR=ch2 stAng=cd4(90°) swAng=-16200000(-270°) arc = ooArcTo(cx, cy, ch2, ch2, 90, -270) p2.push(arc.svg) cx = arc.x cy = arc.y // arcTo wR=ch4 hR=ch4 stAng=cd2(180°) swAng=-10800000(-180°) arc = ooArcTo(cx, cy, ch4, ch4, 180, -180) p2.push(arc.svg) p2.push('Z') // Path 3: stroke-only detail lines (fill=none) const p3: string[] = [] // Sub-path 1: left side detail cx = 0 cy = y3 p3.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, ch2, ch2, 180, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x3},${ch}`) p3.push(`L${x3},${ch2}`) cx = x3 cy = ch2 arc = ooArcTo(cx, cy, ch2, ch2, 180, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${w},${y5}`) cx = w cy = y5 arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${ch},${y6}`) p3.push(`L${ch},${y7}`) cx = ch cy = y7 arc = ooArcTo(cx, cy, ch2, ch2, 0, 180) p3.push(arc.svg) p3.push('Z') // Sub-path 2: top-right connector p3.push(`M${x3},${ch}`) p3.push(`L${x4},${ch}`) cx = x4 cy = ch arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p3.push(arc.svg) // Sub-path 3: right curl inner detail p3.push(`M${x4},${ch}`) p3.push(`L${x4},${ch2}`) cx = x4 cy = ch2 arc = ooArcTo(cx, cy, ch4, ch4, 0, 180) p3.push(arc.svg) // Sub-path 4: left curl inner detail p3.push(`M${ch2},${y4}`) p3.push(`L${ch2},${y3}`) cx = ch2 cy = y3 arc = ooArcTo(cx, cy, ch4, ch4, 180, 180) p3.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch2, ch2, 0, 180) p3.push(arc.svg) // Sub-path 5: vertical divider p3.push(`M${ch},${y3}`) p3.push(`L${ch},${y6}`) return [ { d: p1.join(' '), fill: 'norm', stroke: false }, { d: p2.join(' '), fill: 'darkenLess', stroke: false }, { d: p3.join(' '), fill: 'none', stroke: true }, ] }) // --- verticalScroll (OOXML spec: 3 paths with arcTo) --- multiPathPresets.set('verticalscroll', (w, h, adjustments) => { const adjVal = adjustments?.get('adj') ?? 12500 const a = Math.min(Math.max(adjVal, 0), 25000) const ss = Math.min(w, h) const ch = (ss * a) / 100000 const ch2 = ch / 2 const ch4 = ch / 4 const x3 = ch + ch2 const x4 = ch + ch const x6 = w - ch const x7 = w - ch2 const _x5 = x6 - ch2 const y3 = h - ch const y4 = h - ch2 // Path 1: main fill (stroke=false) const p1: string[] = [] let cx: number let cy: number cx = ch2 cy = h p1.push(`M${cx},${cy}`) let arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${ch2},${y4}`) cx = ch2 cy = y4 arc = ooArcTo(cx, cy, ch4, ch4, 90, -180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${ch},${y3}`) p1.push(`L${ch},${ch2}`) cx = ch cy = ch2 arc = ooArcTo(cx, cy, ch2, ch2, 180, 90) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x7},${0}`) cx = x7 cy = 0 arc = ooArcTo(cx, cy, ch2, ch2, 270, 180) p1.push(arc.svg) cx = arc.x cy = arc.y p1.push(`L${x6},${ch}`) p1.push(`L${x6},${y4}`) cx = x6 cy = y4 arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p1.push(arc.svg) p1.push('Z') // Sub-path 2: top-right curl circle cx = x4 cy = ch2 p1.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p1.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch4, ch4, 90, 180) p1.push(arc.svg) p1.push('Z') // Path 2: darkenLess fill (stroke=false) const p2: string[] = [] cx = x4 cy = ch2 p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p2.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch4, ch4, 90, 180) p2.push(arc.svg) p2.push('Z') cx = ch cy = y4 p2.push(`M${cx},${cy}`) arc = ooArcTo(cx, cy, ch2, ch2, 0, 270) p2.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch4, ch4, 270, 180) p2.push(arc.svg) p2.push('Z') // Path 3: stroke-only detail lines (fill=none) const p3: string[] = [] cx = ch cy = y3 p3.push(`M${cx},${cy}`) p3.push(`L${ch},${ch2}`) cx = ch cy = ch2 arc = ooArcTo(cx, cy, ch2, ch2, 180, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x7},${0}`) cx = x7 cy = 0 arc = ooArcTo(cx, cy, ch2, ch2, 270, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x6},${ch}`) p3.push(`L${x6},${y4}`) cx = x6 cy = y4 arc = ooArcTo(cx, cy, ch2, ch2, 0, 90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${ch2},${h}`) cx = ch2 cy = h arc = ooArcTo(cx, cy, ch2, ch2, 90, 180) p3.push(arc.svg) p3.push('Z') // top curl p3.push(`M${x3},${0}`) cx = x3 cy = 0 arc = ooArcTo(cx, cy, ch2, ch2, 270, 180) p3.push(arc.svg) cx = arc.x cy = arc.y arc = ooArcTo(cx, cy, ch4, ch4, 90, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${x4},${ch2}`) // horizontal divider p3.push(`M${x6},${ch}`) p3.push(`L${x3},${ch}`) // bottom-left curl detail p3.push(`M${ch2},${y3}`) cx = ch2 cy = y3 arc = ooArcTo(cx, cy, ch4, ch4, 270, 180) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${ch},${y4}`) // bottom curl p3.push(`M${ch2},${h}`) cx = ch2 cy = h arc = ooArcTo(cx, cy, ch2, ch2, 90, -90) p3.push(arc.svg) cx = arc.x cy = arc.y p3.push(`L${ch},${y3}`) return [ { d: p1.join(' '), fill: 'norm', stroke: false }, { d: p2.join(' '), fill: 'darkenLess', stroke: false }, { d: p3.join(' '), fill: 'none', stroke: true }, ] }) /** * Get multi-path preset sub-paths for a shape type. * Returns null if the shape is not a multi-path preset (use getPresetShapePath instead). */ export function getMultiPathPreset( shapeType: string, w: number, h: number, adjustments?: Map ): PresetSubPath[] | null { const key = shapeType.toLowerCase() const gen = multiPathPresets.get(key) ?? multiPathPresets.get(shapeType) return gen ? gen(w, h, adjustments) : null } export function getPresetShapePath( shapeType: string, w: number, h: number, adjustments?: Map ): string { // means text-only shape without geometry. if (shapeType === 'textNoShape' || shapeType.toLowerCase() === 'textnoshape') return '' // OOXML preset names are often camelCase; normalize to lowercase for lookup const key = shapeType.toLowerCase() const generator = presetShapes.get(key) ?? presetShapes.get(shapeType) if (generator) { return generator(w, h, adjustments) } // Fallback: simple rectangle logger.warn('Unknown preset shape, falling back to rectangle', { shapeType }) return `M0,0 L${w},0 L${w},${h} L0,${h} Z` }