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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,267 @@
/**
* Shape node parser — handles auto-shapes, text boxes, and connectors.
*/
import { angleToDeg, emuToPx } from '../../parser/units'
import type { SafeXmlNode } from '../../parser/xml-parser'
import { type BaseNodeData, parseBaseProps } from './base-node'
interface TextRun {
text: string
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
properties?: SafeXmlNode
}
interface TextParagraph {
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
properties?: SafeXmlNode
runs: TextRun[]
level: number
/** @internal End-of-paragraph run properties (a:endParaRPr). Defines font size for trailing paragraph mark. */
endParaRPr?: SafeXmlNode
}
export interface TextBody {
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
bodyProperties?: SafeXmlNode
/** @internal Fallback bodyPr from layout/master placeholder (used when shape's own bodyPr is missing attrs). */
layoutBodyProperties?: SafeXmlNode
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
listStyle?: SafeXmlNode
paragraphs: TextParagraph[]
}
export interface LineEndInfo {
type: string // 'triangle', 'arrow', 'stealth', 'diamond', 'oval', 'none'
w?: string // 'sm', 'med', 'lg'
len?: string // 'sm', 'med', 'lg'
}
/** Text box bounds in shape-local coordinates (used by diagram shapes with txXfrm). */
interface TextBoxBounds {
x: number
y: number
w: number
h: number
rotation?: number
}
export interface ShapeNodeData extends BaseNodeData {
nodeType: 'shape'
presetGeometry?: string
adjustments: Map<string, number>
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
customGeometry?: SafeXmlNode
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
fill?: SafeXmlNode
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
line?: SafeXmlNode
headEnd?: LineEndInfo
tailEnd?: LineEndInfo
textBody?: TextBody
/** When set (e.g. diagram txXfrm), text is laid out in this rect instead of full shape. */
textBoxBounds?: TextBoxBounds
}
/**
* Parse a single text paragraph (`a:p`).
*/
function parseParagraph(pNode: SafeXmlNode): TextParagraph {
const pPr = pNode.child('pPr')
const level = pPr.numAttr('lvl') ?? 0
// Re-scan in document order to get correct interleaving of r, br, fld
const orderedRuns: TextRun[] = []
for (const child of pNode.allChildren()) {
const ln = child.localName
if (ln === 'r') {
const rPr = child.child('rPr')
const tNode = child.child('t')
orderedRuns.push({
text: tNode.text(),
properties: rPr.exists() ? rPr : undefined,
})
} else if (ln === 'br') {
const rPr = child.child('rPr')
orderedRuns.push({
text: '\n',
properties: rPr.exists() ? rPr : undefined,
})
} else if (ln === 'fld') {
const rPr = child.child('rPr')
const tNode = child.child('t')
orderedRuns.push({
text: tNode.text(),
properties: rPr.exists() ? rPr : undefined,
})
}
}
const endParaRPrNode = pNode.child('endParaRPr')
return {
properties: pPr.exists() ? pPr : undefined,
runs: orderedRuns,
level,
endParaRPr: endParaRPrNode.exists() ? endParaRPrNode : undefined,
}
}
/**
* Parse a text body (`p:txBody` or `a:txBody`).
*/
export function parseTextBody(txBody: SafeXmlNode): TextBody | undefined {
if (!txBody.exists()) return undefined
const bodyPr = txBody.child('bodyPr')
const lstStyle = txBody.child('lstStyle')
const paragraphs: TextParagraph[] = []
for (const pNode of txBody.children('p')) {
paragraphs.push(parseParagraph(pNode))
}
return {
bodyProperties: bodyPr.exists() ? bodyPr : undefined,
listStyle: lstStyle.exists() ? lstStyle : undefined,
paragraphs,
}
}
/** Fill type local names in priority order. */
const FILL_TYPES = ['solidFill', 'gradFill', 'blipFill', 'pattFill', 'grpFill', 'noFill'] as const
/**
* Find the first fill element in a shape properties node.
*/
function findFill(spPr: SafeXmlNode): SafeXmlNode | undefined {
for (const fillType of FILL_TYPES) {
const fill = spPr.child(fillType)
if (fill.exists()) return fill
}
return undefined
}
/**
* Parse adjustment values from `a:avLst > a:gd` elements.
* Each guide has a `name` attribute and a `fmla` attribute like "val 50000".
*/
function parseAdjustments(avLst: SafeXmlNode): Map<string, number> {
const adjustments = new Map<string, number>()
for (const gd of avLst.children('gd')) {
const name = gd.attr('name')
const fmla = gd.attr('fmla') ?? ''
if (!name) continue
// fmla is typically "val NNNNN" — extract the numeric part
const match = fmla.match(/val\s+(-?\d+)/)
if (match) {
adjustments.set(name, Number(match[1]))
} else {
// Try direct numeric value
const num = Number(fmla)
if (!Number.isNaN(num)) {
adjustments.set(name, num)
}
}
}
return adjustments
}
/**
* Parse a shape XML node (`p:sp` or `p:cxnSp`) into ShapeNodeData.
*/
export function parseShapeNode(spNode: SafeXmlNode): ShapeNodeData {
const base = parseBaseProps(spNode)
const spPr = spNode.child('spPr')
// --- Preset geometry ---
const prstGeom = spPr.child('prstGeom')
const presetGeometry = prstGeom.attr('prst')
const avLst = prstGeom.child('avLst')
const adjustments = parseAdjustments(avLst)
// --- Custom geometry ---
const custGeom = spPr.child('custGeom')
const customGeometry = custGeom.exists() ? custGeom : undefined
// --- Fill ---
const fill = findFill(spPr)
// --- Line ---
const ln = spPr.child('ln')
const line = ln.exists() ? ln : undefined
// --- Line end markers (arrowheads) ---
let headEnd: LineEndInfo | undefined
let tailEnd: LineEndInfo | undefined
if (ln.exists()) {
const headEndNode = ln.child('headEnd')
if (headEndNode.exists()) {
const t = headEndNode.attr('type')
if (t && t !== 'none') {
headEnd = { type: t, w: headEndNode.attr('w'), len: headEndNode.attr('len') }
}
}
const tailEndNode = ln.child('tailEnd')
if (tailEndNode.exists()) {
const t = tailEndNode.attr('type')
if (t && t !== 'none') {
tailEnd = { type: t, w: tailEndNode.attr('w'), len: tailEndNode.attr('len') }
}
}
}
// --- Text body ---
const txBody = spNode.child('txBody')
const textBody = parseTextBody(txBody)
// --- Text transform (diagram shapes: dsp:txXfrm gives text box position/size in same space as xfrm)
let textBoxBounds: TextBoxBounds | undefined
const txXfrm = spNode.child('txXfrm')
if (txXfrm.exists()) {
const txOff = txXfrm.child('off')
const txExt = txXfrm.child('ext')
const xfrm = spPr.child('xfrm')
const off = xfrm.child('off')
const ext = xfrm.child('ext')
const shapeX = off.numAttr('x') ?? 0
const shapeY = off.numAttr('y') ?? 0
const shapeW = ext.numAttr('cx') ?? 0
const shapeH = ext.numAttr('cy') ?? 0
const txX = txOff.numAttr('x') ?? 0
const txY = txOff.numAttr('y') ?? 0
const txW = txExt.numAttr('cx') ?? 0
const txH = txExt.numAttr('cy') ?? 0
if (shapeW > 0 && shapeH > 0) {
const txRotDeg = angleToDeg(txXfrm.numAttr('rot') ?? 0)
const localX = txX - shapeX
const localY = txY - shapeY
// For 180deg txXfrm, mirror text box placement inside shape-local coordinates.
// (Common in SmartArt where shape xfrm also rotates by 180deg but text should remain upright.)
const isHalfTurn = Math.abs(Math.round(txRotDeg)) % 360 === 180
const boxX = isHalfTurn ? shapeW - (localX + txW) : localX
const boxY = isHalfTurn ? shapeH - (localY + txH) : localY
textBoxBounds = {
x: emuToPx(boxX),
y: emuToPx(boxY),
w: emuToPx(txW),
h: emuToPx(txH),
rotation: txRotDeg,
}
}
}
return {
...base,
nodeType: 'shape',
presetGeometry,
adjustments,
customGeometry,
fill,
line,
headEnd,
tailEnd,
textBody,
textBoxBounds,
}
}