chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Parse OOXML custom geometry (a:custGeom) into SVG path strings.
|
||||
*/
|
||||
|
||||
import type { SafeXmlNode } from '../parser/xml-parser'
|
||||
|
||||
function inferPathExtent(pathNode: SafeXmlNode): { w: number; h: number } {
|
||||
let maxX = 0
|
||||
let maxY = 0
|
||||
|
||||
for (const cmd of pathNode.allChildren()) {
|
||||
if (cmd.localName === 'moveTo' || cmd.localName === 'lnTo') {
|
||||
const pt = cmd.child('pt')
|
||||
maxX = Math.max(maxX, pt.numAttr('x') ?? 0)
|
||||
maxY = Math.max(maxY, pt.numAttr('y') ?? 0)
|
||||
continue
|
||||
}
|
||||
if (cmd.localName === 'cubicBezTo' || cmd.localName === 'quadBezTo') {
|
||||
for (const pt of cmd.children('pt')) {
|
||||
maxX = Math.max(maxX, pt.numAttr('x') ?? 0)
|
||||
maxY = Math.max(maxY, pt.numAttr('y') ?? 0)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (cmd.localName === 'arcTo') {
|
||||
maxX = Math.max(maxX, cmd.numAttr('wR') ?? 0)
|
||||
maxY = Math.max(maxY, cmd.numAttr('hR') ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
w: Math.max(1, maxX),
|
||||
h: Math.max(1, maxY),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a custom geometry element to an SVG path d-attribute string.
|
||||
*
|
||||
* @param custGeom - SafeXmlNode wrapping the `a:custGeom` element
|
||||
* @param width - Target width in pixels
|
||||
* @param height - Target height in pixels
|
||||
* @returns SVG path d-attribute string
|
||||
*/
|
||||
export function renderCustomGeometry(
|
||||
custGeom: SafeXmlNode,
|
||||
width: number,
|
||||
height: number,
|
||||
sourceExtent?: { w: number; h: number }
|
||||
): string {
|
||||
const pathLst = custGeom.child('pathLst')
|
||||
if (!pathLst.exists()) return ''
|
||||
|
||||
const paths = pathLst.children('path')
|
||||
const segments: string[] = []
|
||||
|
||||
for (const pathNode of paths) {
|
||||
const fallbackExtent = inferPathExtent(pathNode)
|
||||
const pathW = pathNode.numAttr('w') ?? sourceExtent?.w ?? fallbackExtent.w
|
||||
const pathH = pathNode.numAttr('h') ?? sourceExtent?.h ?? fallbackExtent.h
|
||||
|
||||
const scaleX = pathW > 0 ? width / pathW : 1
|
||||
const scaleY = pathH > 0 ? height / pathH : 1
|
||||
|
||||
// Track current position for arcTo calculations
|
||||
let curX = 0
|
||||
let curY = 0
|
||||
|
||||
const commands = pathNode.allChildren()
|
||||
for (const cmd of commands) {
|
||||
switch (cmd.localName) {
|
||||
case 'moveTo': {
|
||||
const pt = cmd.child('pt')
|
||||
const x = (pt.numAttr('x') ?? 0) * scaleX
|
||||
const y = (pt.numAttr('y') ?? 0) * scaleY
|
||||
segments.push(`M${x},${y}`)
|
||||
curX = x
|
||||
curY = y
|
||||
break
|
||||
}
|
||||
|
||||
case 'lnTo': {
|
||||
const pt = cmd.child('pt')
|
||||
const x = (pt.numAttr('x') ?? 0) * scaleX
|
||||
const y = (pt.numAttr('y') ?? 0) * scaleY
|
||||
segments.push(`L${x},${y}`)
|
||||
curX = x
|
||||
curY = y
|
||||
break
|
||||
}
|
||||
|
||||
case 'cubicBezTo': {
|
||||
const pts = cmd.children('pt')
|
||||
if (pts.length >= 3) {
|
||||
const x1 = (pts[0].numAttr('x') ?? 0) * scaleX
|
||||
const y1 = (pts[0].numAttr('y') ?? 0) * scaleY
|
||||
const x2 = (pts[1].numAttr('x') ?? 0) * scaleX
|
||||
const y2 = (pts[1].numAttr('y') ?? 0) * scaleY
|
||||
const x3 = (pts[2].numAttr('x') ?? 0) * scaleX
|
||||
const y3 = (pts[2].numAttr('y') ?? 0) * scaleY
|
||||
segments.push(`C${x1},${y1} ${x2},${y2} ${x3},${y3}`)
|
||||
curX = x3
|
||||
curY = y3
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'quadBezTo': {
|
||||
const pts = cmd.children('pt')
|
||||
if (pts.length >= 2) {
|
||||
const x1 = (pts[0].numAttr('x') ?? 0) * scaleX
|
||||
const y1 = (pts[0].numAttr('y') ?? 0) * scaleY
|
||||
const x2 = (pts[1].numAttr('x') ?? 0) * scaleX
|
||||
const y2 = (pts[1].numAttr('y') ?? 0) * scaleY
|
||||
segments.push(`Q${x1},${y1} ${x2},${y2}`)
|
||||
curX = x2
|
||||
curY = y2
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'arcTo': {
|
||||
const wRRaw = cmd.numAttr('wR') ?? 0
|
||||
const hRRaw = cmd.numAttr('hR') ?? 0
|
||||
const wR = wRRaw * scaleX
|
||||
const hR = hRRaw * scaleY
|
||||
const stAngRaw = cmd.numAttr('stAng') ?? 0
|
||||
const swAngRaw = cmd.numAttr('swAng') ?? 0
|
||||
|
||||
// OOXML angles are in 60000ths of a degree
|
||||
const stAng = stAngRaw / 60000
|
||||
const swAng = swAngRaw / 60000
|
||||
|
||||
if (wR === 0 || hR === 0 || swAng === 0) {
|
||||
// Degenerate arc, skip
|
||||
break
|
||||
}
|
||||
|
||||
// OOXML arcTo angles are visual (geometric ray) angles in path coordinate space.
|
||||
// Convert to parametric using UNSCALED radii before computing positions.
|
||||
const stVisRad = (stAng * Math.PI) / 180
|
||||
const stAngRad = Math.atan2(wRRaw * Math.sin(stVisRad), hRRaw * Math.cos(stVisRad))
|
||||
|
||||
const endVisRad = ((stAng + swAng) * Math.PI) / 180
|
||||
const endAngRad = Math.atan2(wRRaw * Math.sin(endVisRad), hRRaw * Math.cos(endVisRad))
|
||||
|
||||
// Compute center and endpoint in unscaled path space, then scale
|
||||
const curXU = curX / scaleX
|
||||
const curYU = curY / scaleY
|
||||
const cx = curXU - wRRaw * Math.cos(stAngRad)
|
||||
const cy = curYU - hRRaw * Math.sin(stAngRad)
|
||||
const endX = (cx + wRRaw * Math.cos(endAngRad)) * scaleX
|
||||
const endY = (cy + hRRaw * Math.sin(endAngRad)) * scaleY
|
||||
|
||||
// SVG arc flags
|
||||
const largeArc = Math.abs(swAng) > 180 ? 1 : 0
|
||||
const sweep = swAng > 0 ? 1 : 0
|
||||
|
||||
segments.push(`A${wR},${hR} 0 ${largeArc},${sweep} ${endX},${endY}`)
|
||||
curX = endX
|
||||
curY = endY
|
||||
break
|
||||
}
|
||||
|
||||
case 'close': {
|
||||
segments.push('Z')
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown command, skip
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segments.join(' ')
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Convert OOXML arc specification to SVG path arc command.
|
||||
* Based on PPTXjs shapeArc() implementation.
|
||||
*
|
||||
* @param cx - Center X coordinate
|
||||
* @param cy - Center Y coordinate
|
||||
* @param rx - Horizontal radius
|
||||
* @param ry - Vertical radius
|
||||
* @param startAngle - Start angle in degrees
|
||||
* @param endAngle - End angle in degrees
|
||||
* @param isClose - Whether to close the path with Z
|
||||
* @returns SVG path string for the arc
|
||||
*/
|
||||
export function shapeArc(
|
||||
cx: number,
|
||||
cy: number,
|
||||
rx: number,
|
||||
ry: number,
|
||||
startAngle: number,
|
||||
endAngle: number,
|
||||
isClose: boolean
|
||||
): string {
|
||||
const startRad = (startAngle * Math.PI) / 180
|
||||
const endRad = (endAngle * Math.PI) / 180
|
||||
|
||||
const x1 = cx + rx * Math.cos(startRad)
|
||||
const y1 = cy + ry * Math.sin(startRad)
|
||||
const x2 = cx + rx * Math.cos(endRad)
|
||||
const y2 = cy + ry * Math.sin(endRad)
|
||||
|
||||
// OOXML convention: always sweep clockwise from startAngle to endAngle.
|
||||
// Compute the clockwise sweep in degrees, handling angle wrapping.
|
||||
let sweepDeg = (((endAngle - startAngle) % 360) + 360) % 360
|
||||
if (sweepDeg === 0 && startAngle !== endAngle) sweepDeg = 360
|
||||
|
||||
const largeArc = sweepDeg > 180 ? 1 : 0
|
||||
const sweep = 1 // always clockwise
|
||||
|
||||
let d = `M${x1},${y1} A${rx},${ry} 0 ${largeArc},${sweep} ${x2},${y2}`
|
||||
if (isClose) {
|
||||
d += ' Z'
|
||||
}
|
||||
return d
|
||||
}
|
||||
Reference in New Issue
Block a user