项目文件夹

文件
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

6592 行
205 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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, number>) => string
/** Helper: get adjustment value or default, converting from 100000ths to fraction. */
function adj(
adjustments: Map<string, number> | 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<string, number> | 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<string, PresetShapeGenerator> = 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<string, number>
): 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<string, number>
): 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<string, number>,
_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 <path> with contrasting fill)
// ---------------------------------------------------------------------------
const actionButtonIcons = new Map<string, (w: number, h: number) => 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<string, number>
) => PresetOverlay[]
const presetOverlays: Map<string, PresetOverlayGenerator> = 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<string, number>
): 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<string, number>
) => PresetSubPath[]
const multiPathPresets: Map<string, MultiPathPresetGenerator> = 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 `<lnTo><pt x="hc" y="g19"/></lnTo>`. So endpoint is (g25, g19), then line to (hc, g19).
// Hmm actually spec says: `<lnTo><pt x="hc" y="g19" /></lnTo>`.
// 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<string, number>
): 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, number>
): string {
// <a:prstGeom prst="textNoShape"> 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`
}