( ln: SafeXmlNode, ctx: RenderContext, lnRef?: SafeXmlNode )
| 513 | * when `<a:ln>` has no explicit solidFill (common for connectors) |
| 514 | */ |
| 515 | export function resolveLineStyle( |
| 516 | ln: SafeXmlNode, |
| 517 | ctx: RenderContext, |
| 518 | lnRef?: SafeXmlNode |
| 519 | ): { width: number; color: string; dash: string; dashKind: string } { |
| 520 | // Width: a:ln@w is in EMU, convert to px |
| 521 | const widthEmu = ln.numAttr('w') ?? 0 |
| 522 | let width = emuToPx(widthEmu) |
| 523 | |
| 524 | // Color from solidFill child |
| 525 | let color = 'transparent' |
| 526 | const solidFill = ln.child('solidFill') |
| 527 | if (solidFill.exists()) { |
| 528 | const phClr = solidFill.child('schemeClr') |
| 529 | const usesPlaceholder = phClr.exists() && (phClr.attr('val') ?? '').toLowerCase() === 'phclr' |
| 530 | if (usesPlaceholder && lnRef && lnRef.exists()) { |
| 531 | // Theme line styles often use schemeClr=phClr and expect the concrete color from lnRef. |
| 532 | const base = resolveColor(lnRef, ctx) |
| 533 | const baseHex = base.color.startsWith('#') ? base.color.slice(1) : base.color |
| 534 | const adjusted = applyColorModifiers(baseHex, collectModifiers(phClr)) |
| 535 | color = toCssColor(adjusted.color, adjusted.alpha * base.alpha) |
| 536 | } else { |
| 537 | const resolved = resolveColor(solidFill, ctx) |
| 538 | color = toCssColor(resolved.color, resolved.alpha) |
| 539 | } |
| 540 | } else if (lnRef?.exists() && (lnRef.numAttr('idx') ?? 0) > 0) { |
| 541 | const idx = lnRef.numAttr('idx') ?? 0 |
| 542 | // Look up theme line style for width, color, and dash |
| 543 | if (idx > 0 && ctx.theme.lineStyles && ctx.theme.lineStyles.length >= idx) { |
| 544 | const themeLn = ctx.theme.lineStyles[idx - 1] |
| 545 | // Get width from theme line if not set on the explicit ln node |
| 546 | if (width === 0) { |
| 547 | const themeW = themeLn.numAttr('w') ?? 0 |
| 548 | width = emuToPx(themeW) |
| 549 | } |
| 550 | // Get color: prefer lnRef's own color child, fall back to theme line's solidFill |
| 551 | const resolved = resolveColor(lnRef, ctx) |
| 552 | color = toCssColor(resolved.color, resolved.alpha) |
| 553 | } else { |
| 554 | // Fallback: use lnRef color directly, approximate width from idx |
| 555 | const resolved = resolveColor(lnRef, ctx) |
| 556 | color = toCssColor(resolved.color, resolved.alpha) |
| 557 | if (width === 0 && idx > 0) { |
| 558 | width = idx * 0.75 // approximate: idx 1 = ~0.75px, idx 2 = ~1.5px |
| 559 | } |
| 560 | } |
| 561 | } |
| 562 | |
| 563 | // Width fallback should still use lnRef/theme even when explicit solidFill is present on <a:ln>. |
| 564 | if (width === 0 && lnRef && lnRef.exists()) { |
| 565 | const idx = lnRef.numAttr('idx') ?? 0 |
| 566 | if (idx > 0 && ctx.theme.lineStyles && ctx.theme.lineStyles.length >= idx) { |
| 567 | const themeLn = ctx.theme.lineStyles[idx - 1] |
| 568 | const themeW = themeLn.numAttr('w') ?? 0 |
| 569 | width = emuToPx(themeW) |
| 570 | } else if (idx > 0) { |
| 571 | width = idx * 0.75 |
| 572 | } |
no test coverage detected