Files
simstudioai--sim/apps/sim/lib/pptx-renderer/shapes/presets.ts
T
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 lines
205 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`
}