pptx-angular-viewer 1.1.19 → 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1922,9 +1922,9 @@ function getComputedImageStyle(element) {
1922
1922
  };
1923
1923
  }
1924
1924
 
1925
- const NONE_PRESETS = new Set(['textNoShape', 'textPlain']);
1925
+ const NONE_PRESETS$1 = new Set(['textNoShape', 'textPlain']);
1926
1926
  /** Presets that render best with SVG textPath along curved/circular paths. */
1927
- const PATH_PRESETS = new Set([
1927
+ const PATH_PRESETS$1 = new Set([
1928
1928
  'textArchUp',
1929
1929
  'textArchDown',
1930
1930
  'textCircle',
@@ -1948,7 +1948,7 @@ const PATH_PRESETS = new Set([
1948
1948
  'textStop',
1949
1949
  ]);
1950
1950
  /** Presets that stretch text non-uniformly per line (inflate/deflate/can). */
1951
- const ENVELOPE_PRESETS = new Set([
1951
+ const ENVELOPE_PRESETS$1 = new Set([
1952
1952
  'textInflate',
1953
1953
  'textDeflate',
1954
1954
  'textInflateBottom',
@@ -1961,7 +1961,7 @@ const ENVELOPE_PRESETS = new Set([
1961
1961
  'textCanDown',
1962
1962
  ]);
1963
1963
  /** Presets approximated by basic 2D transforms (slant, fade, cascade). */
1964
- const SIMPLE_PRESETS = new Set([
1964
+ const SIMPLE_PRESETS$1 = new Set([
1965
1965
  'textSlantUp',
1966
1966
  'textSlantDown',
1967
1967
  'textFadeRight',
@@ -1978,29 +1978,29 @@ const SIMPLE_PRESETS = new Set([
1978
1978
  * without an explicit allowlist check.
1979
1979
  */
1980
1980
  function classifyTextWarp(preset) {
1981
- if (!preset || NONE_PRESETS.has(preset)) {
1981
+ if (!preset || NONE_PRESETS$1.has(preset)) {
1982
1982
  return 'none';
1983
1983
  }
1984
- if (PATH_PRESETS.has(preset)) {
1984
+ if (PATH_PRESETS$1.has(preset)) {
1985
1985
  return 'path';
1986
1986
  }
1987
- if (ENVELOPE_PRESETS.has(preset)) {
1987
+ if (ENVELOPE_PRESETS$1.has(preset)) {
1988
1988
  return 'envelope';
1989
1989
  }
1990
- if (SIMPLE_PRESETS.has(preset)) {
1990
+ if (SIMPLE_PRESETS$1.has(preset)) {
1991
1991
  return 'simple';
1992
1992
  }
1993
1993
  return 'none';
1994
1994
  }
1995
1995
  /** All classified warp presets (excludes `none`). */
1996
- const ALL_CLASSIFIED_PRESETS = new Set([
1997
- ...NONE_PRESETS,
1998
- ...PATH_PRESETS,
1999
- ...ENVELOPE_PRESETS,
2000
- ...SIMPLE_PRESETS,
1996
+ const ALL_CLASSIFIED_PRESETS$1 = new Set([
1997
+ ...NONE_PRESETS$1,
1998
+ ...PATH_PRESETS$1,
1999
+ ...ENVELOPE_PRESETS$1,
2000
+ ...SIMPLE_PRESETS$1,
2001
2001
  ]);
2002
2002
  /** Presets that require SVG textPath rendering (all others fall back to flat). */
2003
- const SVG_WARP_PRESETS = new Set([
2003
+ const SVG_WARP_PRESETS$1 = new Set([
2004
2004
  // Priority 1
2005
2005
  'textArchUp',
2006
2006
  'textArchDown',
@@ -2048,7 +2048,7 @@ const SVG_WARP_PRESETS = new Set([
2048
2048
  const clamp4 = (n) => Math.max(0, Math.min(n, 4));
2049
2049
  // ── Priority 1 ──────────────────────────────────────────────────────────
2050
2050
  /** Concentric upward arcs. t=0 is the tallest arch. */
2051
- function archUpPath(w, h, t, adj) {
2051
+ function archUpPath$1(w, h, t, adj) {
2052
2052
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
2053
2053
  const maxArch = (0.85 * adjNorm) / 0.5;
2054
2054
  const archH = h * Math.max(0, maxArch - t * 0.7);
@@ -2058,7 +2058,7 @@ function archUpPath(w, h, t, adj) {
2058
2058
  return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
2059
2059
  }
2060
2060
  /** Concentric downward arcs. t=1 is the deepest. */
2061
- function archDownPath(w, h, t, adj) {
2061
+ function archDownPath$1(w, h, t, adj) {
2062
2062
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
2063
2063
  const baseDepth = (0.15 * adjNorm) / 0.5;
2064
2064
  const archH = h * (baseDepth + t * 0.7);
@@ -2068,7 +2068,7 @@ function archDownPath(w, h, t, adj) {
2068
2068
  return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
2069
2069
  }
2070
2070
  /** Full ellipse — concentric ellipses shrink towards centre. */
2071
- function circlePath(w, h, t, adj) {
2071
+ function circlePath$1(w, h, t, adj) {
2072
2072
  const cx = w / 2;
2073
2073
  const cy = h / 2;
2074
2074
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
@@ -2081,7 +2081,7 @@ function circlePath(w, h, t, adj) {
2081
2081
  `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
2082
2082
  }
2083
2083
  /** Single sine-wave via cubic Bézier. */
2084
- function wave1Path(w, h, t, adj, adj2) {
2084
+ function wave1Path$1(w, h, t, adj, adj2) {
2085
2085
  const yMid = h * (0.25 + t * 0.5);
2086
2086
  const adjFactor = adj !== undefined ? adj / 12500 : 1;
2087
2087
  const amp = h * 0.2 * clamp4(adjFactor);
@@ -2091,28 +2091,28 @@ function wave1Path(w, h, t, adj, adj2) {
2091
2091
  return `M 0,${yMid} C ${cp1x},${yMid - amp} ${cp2x},${yMid + amp} ${w},${yMid}`;
2092
2092
  }
2093
2093
  /** Inflate — top lines bow up, bottom lines bow down. */
2094
- function inflatePath(w, h, t, adj) {
2094
+ function inflatePath$1(w, h, t, adj) {
2095
2095
  const yBase = h * (0.15 + t * 0.7);
2096
2096
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2097
2097
  const bulge = h * 0.3 * (1 - 2 * t) * clamp4(adjFactor);
2098
2098
  return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
2099
2099
  }
2100
2100
  /** Deflate — opposite of inflate. */
2101
- function deflatePath(w, h, t, adj) {
2101
+ function deflatePath$1(w, h, t, adj) {
2102
2102
  const yBase = h * (0.15 + t * 0.7);
2103
2103
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2104
2104
  const pinch = h * 0.3 * (2 * t - 1) * clamp4(adjFactor);
2105
2105
  return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
2106
2106
  }
2107
2107
  /** Gentle upward curve. */
2108
- function curveUpPath(w, h, t, adj) {
2108
+ function curveUpPath$1(w, h, t, adj) {
2109
2109
  const yBase = h * (0.35 + t * 0.55);
2110
2110
  const adjFactor = adj !== undefined ? adj / 45977 : 1;
2111
2111
  const curve = h * 0.4 * (1 - t * 0.3) * clamp4(adjFactor);
2112
2112
  return `M 0,${yBase} Q ${w / 2},${yBase - curve} ${w},${yBase}`;
2113
2113
  }
2114
2114
  /** Gentle downward curve. */
2115
- function curveDownPath(w, h, t, adj) {
2115
+ function curveDownPath$1(w, h, t, adj) {
2116
2116
  const yBase = h * (0.1 + t * 0.55);
2117
2117
  const adjFactor = adj !== undefined ? adj / 45977 : 1;
2118
2118
  const curve = h * 0.4 * (1 - (1 - t) * 0.3) * clamp4(adjFactor);
@@ -2120,7 +2120,7 @@ function curveDownPath(w, h, t, adj) {
2120
2120
  }
2121
2121
  // ── Priority 2 ──────────────────────────────────────────────────────────
2122
2122
  /** Inverted single wave (phase-shifted wave1). */
2123
- function wave2Path(w, h, t, adj, adj2) {
2123
+ function wave2Path$1(w, h, t, adj, adj2) {
2124
2124
  const yMid = h * (0.25 + t * 0.5);
2125
2125
  const adjFactor = adj !== undefined ? adj / 12500 : 1;
2126
2126
  const amp = h * 0.2 * clamp4(adjFactor);
@@ -2130,7 +2130,7 @@ function wave2Path(w, h, t, adj, adj2) {
2130
2130
  return `M 0,${yMid} C ${cp1x},${yMid + amp} ${cp2x},${yMid - amp} ${w},${yMid}`;
2131
2131
  }
2132
2132
  /** Double wave — two full cycles. */
2133
- function wave4Path(w, h, t, adj, adj2) {
2133
+ function wave4Path$1(w, h, t, adj, adj2) {
2134
2134
  const yMid = h * (0.25 + t * 0.5);
2135
2135
  const adjFactor = adj !== undefined ? adj / 12500 : 1;
2136
2136
  const amp = h * 0.15 * clamp4(adjFactor);
@@ -2141,7 +2141,7 @@ function wave4Path(w, h, t, adj, adj2) {
2141
2141
  `C ${w / 2 + q + hShift},${yMid - amp} ${w - q + hShift},${yMid + amp} ${w},${yMid}`);
2142
2142
  }
2143
2143
  /** Double wave with alternating rhythm. */
2144
- function doubleWave1Path(w, h, t, adj, adj2) {
2144
+ function doubleWave1Path$1(w, h, t, adj, adj2) {
2145
2145
  const yMid = h * (0.25 + t * 0.5);
2146
2146
  const adjFactor = adj !== undefined ? adj / 6250 : 1;
2147
2147
  const amp = h * 0.18 * clamp4(adjFactor);
@@ -2152,7 +2152,7 @@ function doubleWave1Path(w, h, t, adj, adj2) {
2152
2152
  `C ${w / 2 + q + hShift},${yMid + amp} ${w - q + hShift},${yMid - amp} ${w},${yMid}`);
2153
2153
  }
2154
2154
  /** Cylindrical text — upward. */
2155
- function canUpPath(w, h, t, adj) {
2155
+ function canUpPath$1(w, h, t, adj) {
2156
2156
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2157
2157
  const curvature = clamp4(adjFactor);
2158
2158
  const archH = h * (0.35 - t * 0.25) * curvature;
@@ -2162,7 +2162,7 @@ function canUpPath(w, h, t, adj) {
2162
2162
  return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
2163
2163
  }
2164
2164
  /** Cylindrical text — downward. */
2165
- function canDownPath(w, h, t, adj) {
2165
+ function canDownPath$1(w, h, t, adj) {
2166
2166
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2167
2167
  const curvature = clamp4(adjFactor);
2168
2168
  const archH = h * (0.1 + t * 0.25) * curvature;
@@ -2172,14 +2172,14 @@ function canDownPath(w, h, t, adj) {
2172
2172
  return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
2173
2173
  }
2174
2174
  /** Button — convex top / concave bottom. */
2175
- function buttonPath(w, h, t, adj) {
2175
+ function buttonPath$1(w, h, t, adj) {
2176
2176
  const yBase = h * (0.1 + t * 0.8);
2177
2177
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2178
2178
  const bulge = h * 0.15 * (1 - 2 * t) * clamp4(adjFactor);
2179
2179
  return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
2180
2180
  }
2181
2181
  /** Ring inside — concentric ellipses scaled inward. */
2182
- function ringInsidePath(w, h, t, adj) {
2182
+ function ringInsidePath$1(w, h, t, adj) {
2183
2183
  const cx = w / 2;
2184
2184
  const cy = h / 2;
2185
2185
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
@@ -2192,7 +2192,7 @@ function ringInsidePath(w, h, t, adj) {
2192
2192
  `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
2193
2193
  }
2194
2194
  /** Ring outside — concentric ellipses scaled outward. */
2195
- function ringOutsidePath(w, h, t, adj) {
2195
+ function ringOutsidePath$1(w, h, t, adj) {
2196
2196
  const cx = w / 2;
2197
2197
  const cy = h / 2;
2198
2198
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
@@ -2205,7 +2205,7 @@ function ringOutsidePath(w, h, t, adj) {
2205
2205
  `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
2206
2206
  }
2207
2207
  /** Cascading up — lines tilt from lower-left to upper-right. */
2208
- function cascadeUpPath(w, h, t, adj) {
2208
+ function cascadeUpPath$1(w, h, t, adj) {
2209
2209
  const adjFactor = adj !== undefined ? adj / 44444 : 1;
2210
2210
  const tilt = 0.2 * clamp4(adjFactor);
2211
2211
  const yMid = h * (0.2 + t * 0.6);
@@ -2214,7 +2214,7 @@ function cascadeUpPath(w, h, t, adj) {
2214
2214
  return `M 0,${yStart} L ${w},${yEnd}`;
2215
2215
  }
2216
2216
  /** Cascading down — lines tilt from upper-left to lower-right. */
2217
- function cascadeDownPath(w, h, t, adj) {
2217
+ function cascadeDownPath$1(w, h, t, adj) {
2218
2218
  const adjFactor = adj !== undefined ? adj / 44444 : 1;
2219
2219
  const tilt = 0.2 * clamp4(adjFactor);
2220
2220
  const yMid = h * (0.2 + t * 0.6);
@@ -2224,7 +2224,7 @@ function cascadeDownPath(w, h, t, adj) {
2224
2224
  }
2225
2225
  // ── Priority 3 ──────────────────────────────────────────────────────────
2226
2226
  /** Triangle / trapezoid — top line narrow, bottom line full width. */
2227
- function trianglePath(w, h, t, adj) {
2227
+ function trianglePath$1(w, h, t, adj) {
2228
2228
  const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
2229
2229
  const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
2230
2230
  const lineW = narrowW + t * (w - narrowW);
@@ -2233,7 +2233,7 @@ function trianglePath(w, h, t, adj) {
2233
2233
  return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
2234
2234
  }
2235
2235
  /** Inverted triangle — top line full width, bottom line narrow. */
2236
- function triangleInvertedPath(w, h, t, adj) {
2236
+ function triangleInvertedPath$1(w, h, t, adj) {
2237
2237
  const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
2238
2238
  const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
2239
2239
  const lineW = w - t * (w - narrowW);
@@ -2242,7 +2242,7 @@ function triangleInvertedPath(w, h, t, adj) {
2242
2242
  return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
2243
2243
  }
2244
2244
  /** Stop / octagon — lines narrow at top and bottom, widest in centre. */
2245
- function stopPath(w, h, t, adj) {
2245
+ function stopPath$1(w, h, t, adj) {
2246
2246
  const adjFactor = adj !== undefined ? adj / 25000 : 1;
2247
2247
  const insetScale = clamp4(adjFactor);
2248
2248
  const inset = w * 0.15 * (1 - (1 - 2 * Math.abs(t - 0.5)) ** 2) * insetScale;
@@ -2250,42 +2250,42 @@ function stopPath(w, h, t, adj) {
2250
2250
  return `M ${inset},${yBase} L ${w - inset},${yBase}`;
2251
2251
  }
2252
2252
  /** Chevron — V-shape pointing down. */
2253
- function chevronPath(w, h, t, adj) {
2253
+ function chevronPath$1(w, h, t, adj) {
2254
2254
  const yBase = h * (0.15 + t * 0.7);
2255
2255
  const adjFactor = adj !== undefined ? adj / 25000 : 1;
2256
2256
  const dip = h * 0.2 * (1 - t) * clamp4(adjFactor);
2257
2257
  return `M 0,${yBase} L ${w / 2},${yBase + dip} L ${w},${yBase}`;
2258
2258
  }
2259
2259
  /** Inverted chevron — V-shape pointing up. */
2260
- function chevronInvertedPath(w, h, t, adj) {
2260
+ function chevronInvertedPath$1(w, h, t, adj) {
2261
2261
  const yBase = h * (0.15 + t * 0.7);
2262
2262
  const adjFactor = adj !== undefined ? adj / 25000 : 1;
2263
2263
  const rise = h * 0.2 * t * clamp4(adjFactor);
2264
2264
  return `M 0,${yBase} L ${w / 2},${yBase - rise} L ${w},${yBase}`;
2265
2265
  }
2266
2266
  /** Inflate bottom only. */
2267
- function inflateBottomPath(w, h, t, adj) {
2267
+ function inflateBottomPath$1(w, h, t, adj) {
2268
2268
  const yBase = h * (0.15 + t * 0.7);
2269
2269
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2270
2270
  const bulge = t > 0.4 ? h * 0.25 * ((t - 0.4) / 0.6) * clamp4(adjFactor) : 0;
2271
2271
  return `M 0,${yBase} Q ${w / 2},${yBase + bulge} ${w},${yBase}`;
2272
2272
  }
2273
2273
  /** Inflate top only. */
2274
- function inflateTopPath(w, h, t, adj) {
2274
+ function inflateTopPath$1(w, h, t, adj) {
2275
2275
  const yBase = h * (0.15 + t * 0.7);
2276
2276
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2277
2277
  const bulge = t < 0.6 ? h * 0.25 * ((0.6 - t) / 0.6) * clamp4(adjFactor) : 0;
2278
2278
  return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
2279
2279
  }
2280
2280
  /** Deflate bottom only. */
2281
- function deflateBottomPath(w, h, t, adj) {
2281
+ function deflateBottomPath$1(w, h, t, adj) {
2282
2282
  const yBase = h * (0.15 + t * 0.7);
2283
2283
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2284
2284
  const pinch = t > 0.4 ? h * 0.2 * ((t - 0.4) / 0.6) * clamp4(adjFactor) : 0;
2285
2285
  return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
2286
2286
  }
2287
2287
  /** Deflate top only. */
2288
- function deflateTopPath(w, h, t, adj) {
2288
+ function deflateTopPath$1(w, h, t, adj) {
2289
2289
  const yBase = h * (0.15 + t * 0.7);
2290
2290
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2291
2291
  const pinch = t < 0.6 ? h * 0.2 * ((0.6 - t) / 0.6) * clamp4(adjFactor) : 0;
@@ -2293,7 +2293,7 @@ function deflateTopPath(w, h, t, adj) {
2293
2293
  }
2294
2294
  // ── Priority 4 ──────────────────────────────────────────────────────────
2295
2295
  /** Slant up — baseline rises from left to right. */
2296
- function slantUpPath(w, h, t, adj) {
2296
+ function slantUpPath$1(w, h, t, adj) {
2297
2297
  const adjFactor = adj !== undefined ? adj / 55000 : 1;
2298
2298
  const slant = 0.25 * clamp4(adjFactor);
2299
2299
  const yMid = h * (0.175 + t * 0.55);
@@ -2302,7 +2302,7 @@ function slantUpPath(w, h, t, adj) {
2302
2302
  return `M 0,${yStart} L ${w},${yEnd}`;
2303
2303
  }
2304
2304
  /** Slant down — baseline falls from left to right. */
2305
- function slantDownPath(w, h, t, adj) {
2305
+ function slantDownPath$1(w, h, t, adj) {
2306
2306
  const adjFactor = adj !== undefined ? adj / 55000 : 1;
2307
2307
  const slant = 0.25 * clamp4(adjFactor);
2308
2308
  const yMid = h * (0.175 + t * 0.55);
@@ -2311,7 +2311,7 @@ function slantDownPath(w, h, t, adj) {
2311
2311
  return `M 0,${yStart} L ${w},${yEnd}`;
2312
2312
  }
2313
2313
  /** Fade right — text narrows towards the right (trapezoid). */
2314
- function fadeRightPath(w, h, t, adj) {
2314
+ function fadeRightPath$1(w, h, t, adj) {
2315
2315
  const adjFactor = adj !== undefined ? adj / 50000 : 1;
2316
2316
  const squeezeScale = clamp4(adjFactor);
2317
2317
  const yLeft = h * (0.1 + t * 0.8);
@@ -2320,7 +2320,7 @@ function fadeRightPath(w, h, t, adj) {
2320
2320
  return `M 0,${yLeft} L ${w},${yRight}`;
2321
2321
  }
2322
2322
  /** Fade left — text narrows towards the left (trapezoid). */
2323
- function fadeLeftPath(w, h, t, adj) {
2323
+ function fadeLeftPath$1(w, h, t, adj) {
2324
2324
  const adjFactor = adj !== undefined ? adj / 50000 : 1;
2325
2325
  const squeezeScale = clamp4(adjFactor);
2326
2326
  const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
@@ -2329,7 +2329,7 @@ function fadeLeftPath(w, h, t, adj) {
2329
2329
  return `M 0,${yLeft} L ${w},${yRight}`;
2330
2330
  }
2331
2331
  /** Fade up — text narrows towards the top. */
2332
- function fadeUpPath(w, h, t, adj) {
2332
+ function fadeUpPath$1(w, h, t, adj) {
2333
2333
  const adjFactor = adj !== undefined ? adj / 50000 : 1;
2334
2334
  const taperScale = clamp4(adjFactor);
2335
2335
  const narrowFraction = 1 - 0.7 * taperScale;
@@ -2340,7 +2340,7 @@ function fadeUpPath(w, h, t, adj) {
2340
2340
  return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
2341
2341
  }
2342
2342
  /** Fade down — text narrows towards the bottom. */
2343
- function fadeDownPath(w, h, t, adj) {
2343
+ function fadeDownPath$1(w, h, t, adj) {
2344
2344
  const adjFactor = adj !== undefined ? adj / 50000 : 1;
2345
2345
  const taperScale = clamp4(adjFactor);
2346
2346
  const narrowFraction = 1 - 0.7 * taperScale;
@@ -2351,7 +2351,7 @@ function fadeDownPath(w, h, t, adj) {
2351
2351
  return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
2352
2352
  }
2353
2353
  /** Arch up pour — hollowed arch upward. */
2354
- function archUpPourPath(w, h, t, adj) {
2354
+ function archUpPourPath$1(w, h, t, adj) {
2355
2355
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
2356
2356
  const maxArch = (0.7 * adjNorm) / 0.5;
2357
2357
  const archH = h * Math.max(0, maxArch - t * 0.5);
@@ -2361,7 +2361,7 @@ function archUpPourPath(w, h, t, adj) {
2361
2361
  return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
2362
2362
  }
2363
2363
  /** Arch down pour — hollowed arch downward. */
2364
- function archDownPourPath(w, h, t, adj) {
2364
+ function archDownPourPath$1(w, h, t, adj) {
2365
2365
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
2366
2366
  const baseDepth = (0.2 * adjNorm) / 0.5;
2367
2367
  const archH = h * (baseDepth + t * 0.5);
@@ -2371,7 +2371,7 @@ function archDownPourPath(w, h, t, adj) {
2371
2371
  return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
2372
2372
  }
2373
2373
  /** Circle pour — concentric ellipses with an inner gap. */
2374
- function circlePourPath(w, h, t, adj) {
2374
+ function circlePourPath$1(w, h, t, adj) {
2375
2375
  const cx = w / 2;
2376
2376
  const cy = h / 2;
2377
2377
  const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
@@ -2384,14 +2384,14 @@ function circlePourPath(w, h, t, adj) {
2384
2384
  `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
2385
2385
  }
2386
2386
  /** Button pour — convex top / concave bottom with larger margins. */
2387
- function buttonPourPath(w, h, t, adj) {
2387
+ function buttonPourPath$1(w, h, t, adj) {
2388
2388
  const yBase = h * (0.15 + t * 0.7);
2389
2389
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2390
2390
  const bulge = h * 0.12 * (1 - 2 * t) * clamp4(adjFactor);
2391
2391
  return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
2392
2392
  }
2393
2393
  /** Deflate-inflate — pinched in centre top/bottom, expanded at edges. */
2394
- function deflateInflatePath(w, h, t, adj) {
2394
+ function deflateInflatePath$1(w, h, t, adj) {
2395
2395
  const yBase = h * (0.15 + t * 0.7);
2396
2396
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2397
2397
  const factor = Math.sin(t * Math.PI);
@@ -2399,7 +2399,7 @@ function deflateInflatePath(w, h, t, adj) {
2399
2399
  return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
2400
2400
  }
2401
2401
  /** Deflate-inflate-deflate — triple oscillation. */
2402
- function deflateInflateDeflatePath(w, h, t, adj) {
2402
+ function deflateInflateDeflatePath$1(w, h, t, adj) {
2403
2403
  const yBase = h * (0.15 + t * 0.7);
2404
2404
  const adjFactor = adj !== undefined ? adj / 18750 : 1;
2405
2405
  const ampScale = clamp4(adjFactor);
@@ -2412,54 +2412,54 @@ function deflateInflateDeflatePath(w, h, t, adj) {
2412
2412
  `Q ${q2},${yBase + bulge} ${w},${yBase}`);
2413
2413
  }
2414
2414
  // ── Generator look-up table ─────────────────────────────────────────────
2415
- const WARP_PATH_GENERATORS = {
2416
- textArchUp: archUpPath,
2417
- textArchDown: archDownPath,
2418
- textCircle: circlePath,
2419
- textWave1: wave1Path,
2420
- textInflate: inflatePath,
2421
- textDeflate: deflatePath,
2422
- textCurveUp: curveUpPath,
2423
- textCurveDown: curveDownPath,
2424
- textWave2: wave2Path,
2425
- textWave4: wave4Path,
2426
- textDoubleWave1: doubleWave1Path,
2427
- textCanUp: canUpPath,
2428
- textCanDown: canDownPath,
2429
- textButton: buttonPath,
2430
- textRingInside: ringInsidePath,
2431
- textRingOutside: ringOutsidePath,
2432
- textCascadeUp: cascadeUpPath,
2433
- textCascadeDown: cascadeDownPath,
2434
- textTriangle: trianglePath,
2435
- textTriangleInverted: triangleInvertedPath,
2436
- textStop: stopPath,
2437
- textChevron: chevronPath,
2438
- textChevronInverted: chevronInvertedPath,
2439
- textInflateBottom: inflateBottomPath,
2440
- textInflateTop: inflateTopPath,
2441
- textDeflateBottom: deflateBottomPath,
2442
- textDeflateTop: deflateTopPath,
2443
- textSlantUp: slantUpPath,
2444
- textSlantDown: slantDownPath,
2445
- textFadeRight: fadeRightPath,
2446
- textFadeLeft: fadeLeftPath,
2447
- textFadeUp: fadeUpPath,
2448
- textFadeDown: fadeDownPath,
2449
- textArchUpPour: archUpPourPath,
2450
- textArchDownPour: archDownPourPath,
2451
- textCirclePour: circlePourPath,
2452
- textButtonPour: buttonPourPath,
2453
- textDeflateInflate: deflateInflatePath,
2454
- textDeflateInflateDeflate: deflateInflateDeflatePath,
2415
+ const WARP_PATH_GENERATORS$1 = {
2416
+ textArchUp: archUpPath$1,
2417
+ textArchDown: archDownPath$1,
2418
+ textCircle: circlePath$1,
2419
+ textWave1: wave1Path$1,
2420
+ textInflate: inflatePath$1,
2421
+ textDeflate: deflatePath$1,
2422
+ textCurveUp: curveUpPath$1,
2423
+ textCurveDown: curveDownPath$1,
2424
+ textWave2: wave2Path$1,
2425
+ textWave4: wave4Path$1,
2426
+ textDoubleWave1: doubleWave1Path$1,
2427
+ textCanUp: canUpPath$1,
2428
+ textCanDown: canDownPath$1,
2429
+ textButton: buttonPath$1,
2430
+ textRingInside: ringInsidePath$1,
2431
+ textRingOutside: ringOutsidePath$1,
2432
+ textCascadeUp: cascadeUpPath$1,
2433
+ textCascadeDown: cascadeDownPath$1,
2434
+ textTriangle: trianglePath$1,
2435
+ textTriangleInverted: triangleInvertedPath$1,
2436
+ textStop: stopPath$1,
2437
+ textChevron: chevronPath$1,
2438
+ textChevronInverted: chevronInvertedPath$1,
2439
+ textInflateBottom: inflateBottomPath$1,
2440
+ textInflateTop: inflateTopPath$1,
2441
+ textDeflateBottom: deflateBottomPath$1,
2442
+ textDeflateTop: deflateTopPath$1,
2443
+ textSlantUp: slantUpPath$1,
2444
+ textSlantDown: slantDownPath$1,
2445
+ textFadeRight: fadeRightPath$1,
2446
+ textFadeLeft: fadeLeftPath$1,
2447
+ textFadeUp: fadeUpPath$1,
2448
+ textFadeDown: fadeDownPath$1,
2449
+ textArchUpPour: archUpPourPath$1,
2450
+ textArchDownPour: archDownPourPath$1,
2451
+ textCirclePour: circlePourPath$1,
2452
+ textButtonPour: buttonPourPath$1,
2453
+ textDeflateInflate: deflateInflatePath$1,
2454
+ textDeflateInflateDeflate: deflateInflateDeflatePath$1,
2455
2455
  };
2456
2456
  // ── Public API ──────────────────────────────────────────────────────────
2457
2457
  /** Returns `true` when the preset should use SVG `<textPath>` rendering. */
2458
- function shouldUseSvgWarp(preset) {
2458
+ function shouldUseSvgWarp$1(preset) {
2459
2459
  if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
2460
2460
  return false;
2461
2461
  }
2462
- return SVG_WARP_PRESETS.has(preset);
2462
+ return SVG_WARP_PRESETS$1.has(preset);
2463
2463
  }
2464
2464
  /**
2465
2465
  * Predicate: does this element carry a WordArt/text-warp preset that the SVG
@@ -2470,7 +2470,7 @@ function hasTextWarp(element) {
2470
2470
  if (!hasTextProperties(element)) {
2471
2471
  return false;
2472
2472
  }
2473
- return shouldUseSvgWarp(element.textStyle?.textWarpPreset);
2473
+ return shouldUseSvgWarp$1(element.textStyle?.textWarpPreset);
2474
2474
  }
2475
2475
  /**
2476
2476
  * Build the SVG path `d` attribute for a warp preset at a given line position.
@@ -2485,7 +2485,7 @@ function hasTextWarp(element) {
2485
2485
  */
2486
2486
  function buildWarpPath(preset, width, height, lineIndex, lineCount, adj, adj2) {
2487
2487
  const t = lineCount <= 1 ? 0.5 : lineIndex / (lineCount - 1);
2488
- const generator = WARP_PATH_GENERATORS[preset];
2488
+ const generator = WARP_PATH_GENERATORS$1[preset];
2489
2489
  if (generator) {
2490
2490
  return generator(width, height, t, adj, adj2);
2491
2491
  }
@@ -4023,7 +4023,7 @@ function resolveCategoryLabels(chartData) {
4023
4023
  * `mode === 'bar'` centres on category slots; `'line'` anchors at points.
4024
4024
  * Mirrors the React `xToPixel`.
4025
4025
  */
4026
- function xToPixel(xVal, catCount, layout, mode) {
4026
+ function xToPixel$1(xVal, catCount, layout, mode) {
4027
4027
  if (mode === 'bar') {
4028
4028
  const slotWidth = layout.plotWidth / Math.max(catCount, 1);
4029
4029
  return layout.plotLeft + slotWidth * xVal + slotWidth / 2;
@@ -4032,7 +4032,7 @@ function xToPixel(xVal, catCount, layout, mode) {
4032
4032
  return layout.plotLeft + (xVal / maxIdx) * layout.plotWidth;
4033
4033
  }
4034
4034
  /** Ordinary least-squares linear regression of `yVals` on `xVals`. */
4035
- function computeLinearRegression(xVals, yVals) {
4035
+ function computeLinearRegression$1(xVals, yVals) {
4036
4036
  const n = xVals.length;
4037
4037
  if (n < 2) {
4038
4038
  return { slope: 0, intercept: 0, rSquared: 0 };
@@ -4060,7 +4060,7 @@ function computeLinearRegression(xVals, yVals) {
4060
4060
  return { slope, intercept, rSquared };
4061
4061
  }
4062
4062
  /** Fit polynomial coefficients (ascending order) via Gaussian elimination. */
4063
- function fitPolynomial(xVals, yVals, order) {
4063
+ function fitPolynomial$1(xVals, yVals, order) {
4064
4064
  const n = xVals.length;
4065
4065
  const m = order + 1;
4066
4066
  const matrix = Array.from({ length: m }, () => Array(m + 1).fill(0));
@@ -4106,7 +4106,7 @@ function fitPolynomial(xVals, yVals, order) {
4106
4106
  return matrix.map((row) => row[m]);
4107
4107
  }
4108
4108
  /** R² of an arbitrary fit function against the data. */
4109
- function computeRSquared(xVals, yVals, evalFn) {
4109
+ function computeRSquared$1(xVals, yVals, evalFn) {
4110
4110
  const n = xVals.length;
4111
4111
  const meanY = yVals.reduce((s, y) => s + y, 0) / n;
4112
4112
  let ssRes = 0;
@@ -4122,7 +4122,7 @@ function computeRSquared(xVals, yVals, evalFn) {
4122
4122
  * series' values. Returns an empty point list when the type is unsupported or
4123
4123
  * the data is too sparse to fit. Mirrors the React `computeTrendlinePoints`.
4124
4124
  */
4125
- function computeTrendlinePoints(trendline, values, catCount, layout, range, mode) {
4125
+ function computeTrendlinePoints$1(trendline, values, catCount, layout, range, mode) {
4126
4126
  const n = values.length;
4127
4127
  if (n < 2) {
4128
4128
  return { points: [], equation: '', rSquared: 0 };
@@ -4139,7 +4139,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4139
4139
  let rSquared = 0;
4140
4140
  switch (trendline.trendlineType) {
4141
4141
  case 'linear': {
4142
- const reg = computeLinearRegression(xVals, yVals);
4142
+ const reg = computeLinearRegression$1(xVals, yVals);
4143
4143
  const intercept = trendline.intercept;
4144
4144
  const slope = intercept !== undefined
4145
4145
  ? yVals.reduce((s, y, i) => s + (y - intercept) * xVals[i], 0) /
@@ -4157,7 +4157,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4157
4157
  if (logY.length < 2) {
4158
4158
  return { points: [], equation: '', rSquared: 0 };
4159
4159
  }
4160
- const reg = computeLinearRegression(filteredX, logY);
4160
+ const reg = computeLinearRegression$1(filteredX, logY);
4161
4161
  const a = Math.exp(reg.intercept);
4162
4162
  const b = reg.slope;
4163
4163
  evalFn = (x) => a * Math.exp(b * x);
@@ -4171,7 +4171,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4171
4171
  if (lnX.length < 2) {
4172
4172
  return { points: [], equation: '', rSquared: 0 };
4173
4173
  }
4174
- const reg = computeLinearRegression(lnX, filteredY);
4174
+ const reg = computeLinearRegression$1(lnX, filteredY);
4175
4175
  evalFn = (x) => (x > 0 ? reg.slope * Math.log(x) + reg.intercept : 0);
4176
4176
  equation = `y = ${reg.slope.toFixed(2)}ln(x) + ${reg.intercept.toFixed(2)}`;
4177
4177
  rSquared = reg.rSquared;
@@ -4183,7 +4183,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4183
4183
  if (logX.length < 2) {
4184
4184
  return { points: [], equation: '', rSquared: 0 };
4185
4185
  }
4186
- const reg = computeLinearRegression(logX, logY);
4186
+ const reg = computeLinearRegression$1(logX, logY);
4187
4187
  const a = Math.exp(reg.intercept);
4188
4188
  evalFn = (x) => (x > 0 ? a * x ** reg.slope : 0);
4189
4189
  equation = `y = ${a.toFixed(2)}x^${reg.slope.toFixed(2)}`;
@@ -4192,10 +4192,10 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4192
4192
  }
4193
4193
  case 'polynomial': {
4194
4194
  const order = Math.min(trendline.order ?? 2, 6);
4195
- const coeffs = fitPolynomial(xVals, yVals, order);
4195
+ const coeffs = fitPolynomial$1(xVals, yVals, order);
4196
4196
  evalFn = (x) => coeffs.reduce((s, c, i) => s + c * x ** i, 0);
4197
4197
  equation = coeffs.map((c, i) => `${c.toFixed(2)}x^${i}`).join(' + ');
4198
- rSquared = computeRSquared(xVals, yVals, evalFn);
4198
+ rSquared = computeRSquared$1(xVals, yVals, evalFn);
4199
4199
  break;
4200
4200
  }
4201
4201
  case 'movingAvg': {
@@ -4207,7 +4207,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4207
4207
  sum += yVals[j];
4208
4208
  }
4209
4209
  const avgVal = sum / period;
4210
- const px = xToPixel(i, catCount, layout, mode);
4210
+ const px = xToPixel$1(i, catCount, layout, mode);
4211
4211
  const py = valueToYLocal(avgVal, range, layout.plotTop, layout.plotBottom);
4212
4212
  maPoints.push({ x: px, y: py });
4213
4213
  }
@@ -4227,7 +4227,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
4227
4227
  if (!Number.isFinite(yVal)) {
4228
4228
  continue;
4229
4229
  }
4230
- const px = xToPixel(xVal, catCount, layout, mode);
4230
+ const px = xToPixel$1(xVal, catCount, layout, mode);
4231
4231
  const py = valueToYLocal(yVal, range, layout.plotTop, layout.plotBottom);
4232
4232
  points.push({ x: px, y: py });
4233
4233
  }
@@ -4257,7 +4257,7 @@ function computeChartTrendlines(chartData, layout, range, mode, styleId, colorPa
4257
4257
  return;
4258
4258
  }
4259
4259
  series.trendlines.forEach((tl) => {
4260
- const { points, equation, rSquared } = computeTrendlinePoints(tl, series.values, catCount, layout, range, mode);
4260
+ const { points, equation, rSquared } = computeTrendlinePoints$1(tl, series.values, catCount, layout, range, mode);
4261
4261
  if (points.length < 2) {
4262
4262
  return;
4263
4263
  }
@@ -16015,7 +16015,7 @@ function stringFromEvent$2(event) {
16015
16015
  const DEFAULT_FILL = '#ffffff';
16016
16016
  const DEFAULT_STROKE = '#000000';
16017
16017
  const DEFAULT_TEXT_COLOR = '#000000';
16018
- const DEFAULT_FONT_SIZE = 12;
16018
+ const DEFAULT_FONT_SIZE$1 = 12;
16019
16019
  // ── Readers ──────────────────────────────────────────────────────────────────
16020
16020
  /**
16021
16021
  * Returns the fill colour of the element's shapeStyle, or a white default.
@@ -16053,9 +16053,9 @@ function textColorOf(el) {
16053
16053
  */
16054
16054
  function fontSizeOf(el) {
16055
16055
  if (hasTextProperties(el)) {
16056
- return el.textStyle?.fontSize ?? DEFAULT_FONT_SIZE;
16056
+ return el.textStyle?.fontSize ?? DEFAULT_FONT_SIZE$1;
16057
16057
  }
16058
- return DEFAULT_FONT_SIZE;
16058
+ return DEFAULT_FONT_SIZE$1;
16059
16059
  }
16060
16060
  /**
16061
16061
  * Returns whether the element's text is bold (false when absent).
@@ -19610,6 +19610,730 @@ function buildStockViewModel(element, chartData, categoryLabels) {
19610
19610
  };
19611
19611
  }
19612
19612
 
19613
+ /**
19614
+ * chart-overlays.ts — chart overlay depth for Angular pptx-angular-viewer.
19615
+ *
19616
+ * Pure functions that produce additional `SvgPrimitive[]` / `SvgText[]` for an
19617
+ * existing cartesian chart. No Angular dependencies; all inputs are typed
19618
+ * against `pptx-viewer-core` and the `SvgPrimitive` union already defined in
19619
+ * `chart-renderer-helpers.ts`.
19620
+ *
19621
+ * Ported / adapted from:
19622
+ * packages/react/src/viewer/utils/chart-trendlines.tsx (regression engine)
19623
+ * packages/react/src/viewer/utils/chart-overlay-lines.tsx (error bars)
19624
+ * packages/react/src/viewer/utils/chart-chrome.tsx (axis titles)
19625
+ * packages/react/src/viewer/utils/chart-data-table.tsx (data table)
19626
+ * packages/shared/src/render/chart-trendlines.ts (shared port)
19627
+ *
19628
+ * @module chart-overlays
19629
+ */
19630
+ // ─────────────────────────────────────────────────────────────────────────────
19631
+ // Internal: coordinate helpers
19632
+ // ─────────────────────────────────────────────────────────────────────────────
19633
+ /**
19634
+ * Map a (possibly fractional / extrapolated) category index to an X pixel.
19635
+ * `mode === 'bar'` centres on category slots; `'line'` anchors at data points.
19636
+ * Mirrors `xToPixel` from chart-overlay-utils.ts (React) and the shared port.
19637
+ */
19638
+ function xToPixel(xVal, catCount, layout, mode) {
19639
+ if (mode === 'bar') {
19640
+ const slotWidth = layout.plotWidth / Math.max(catCount, 1);
19641
+ return layout.plotLeft + slotWidth * xVal + slotWidth / 2;
19642
+ }
19643
+ const maxIdx = Math.max(catCount - 1, 1);
19644
+ return layout.plotLeft + (xVal / maxIdx) * layout.plotWidth;
19645
+ }
19646
+ /**
19647
+ * Ordinary least-squares linear regression of `yVals` on `xVals`.
19648
+ * Returns slope=0, intercept=mean(y), rSquared=0 when fewer than 2 points or
19649
+ * when the denominator is effectively zero (vertical / constant-x data).
19650
+ *
19651
+ * Mirrors `computeLinearRegression` in chart-trendlines.tsx (React) and
19652
+ * chart-trendlines.ts (shared).
19653
+ */
19654
+ function computeLinearRegression(xVals, yVals) {
19655
+ const n = xVals.length;
19656
+ if (n < 2) {
19657
+ return { slope: 0, intercept: 0, rSquared: 0 };
19658
+ }
19659
+ let sumX = 0;
19660
+ let sumY = 0;
19661
+ let sumXY = 0;
19662
+ let sumXX = 0;
19663
+ for (let i = 0; i < n; i++) {
19664
+ sumX += xVals[i];
19665
+ sumY += yVals[i];
19666
+ sumXY += xVals[i] * yVals[i];
19667
+ sumXX += xVals[i] * xVals[i];
19668
+ }
19669
+ const denom = n * sumXX - sumX * sumX;
19670
+ if (Math.abs(denom) < 1e-12) {
19671
+ return { slope: 0, intercept: sumY / n, rSquared: 0 };
19672
+ }
19673
+ const slope = (n * sumXY - sumX * sumY) / denom;
19674
+ const intercept = (sumY - slope * sumX) / n;
19675
+ const ssRes = yVals.reduce((s, y, i) => s + (y - (slope * xVals[i] + intercept)) ** 2, 0);
19676
+ const meanY = sumY / n;
19677
+ const ssTot = yVals.reduce((s, y) => s + (y - meanY) ** 2, 0);
19678
+ const rSquared = ssTot > 0 ? 1 - ssRes / ssTot : 0;
19679
+ return { slope, intercept, rSquared };
19680
+ }
19681
+ /**
19682
+ * Fit polynomial coefficients (ascending order: [a0, a1, …, a_order]) via
19683
+ * Gaussian elimination on the normal equations.
19684
+ * Mirrors `fitPolynomial` in chart-trendlines.tsx (React).
19685
+ */
19686
+ function fitPolynomial(xVals, yVals, order) {
19687
+ const n = xVals.length;
19688
+ const m = order + 1;
19689
+ const matrix = Array.from({ length: m }, () => Array(m + 1).fill(0));
19690
+ for (let i = 0; i < m; i++) {
19691
+ for (let j = 0; j < m; j++) {
19692
+ let sum = 0;
19693
+ for (let k = 0; k < n; k++) {
19694
+ sum += xVals[k] ** (i + j);
19695
+ }
19696
+ matrix[i][j] = sum;
19697
+ }
19698
+ let sum = 0;
19699
+ for (let k = 0; k < n; k++) {
19700
+ sum += yVals[k] * xVals[k] ** i;
19701
+ }
19702
+ matrix[i][m] = sum;
19703
+ }
19704
+ for (let i = 0; i < m; i++) {
19705
+ let maxRow = i;
19706
+ for (let k = i + 1; k < m; k++) {
19707
+ if (Math.abs(matrix[k][i]) > Math.abs(matrix[maxRow][i])) {
19708
+ maxRow = k;
19709
+ }
19710
+ }
19711
+ [matrix[i], matrix[maxRow]] = [matrix[maxRow], matrix[i]];
19712
+ const pivot = matrix[i][i];
19713
+ if (Math.abs(pivot) < 1e-12) {
19714
+ continue;
19715
+ }
19716
+ for (let j = i; j <= m; j++) {
19717
+ matrix[i][j] /= pivot;
19718
+ }
19719
+ for (let k = 0; k < m; k++) {
19720
+ if (k === i) {
19721
+ continue;
19722
+ }
19723
+ const factor = matrix[k][i];
19724
+ for (let j = i; j <= m; j++) {
19725
+ matrix[k][j] -= factor * matrix[i][j];
19726
+ }
19727
+ }
19728
+ }
19729
+ return matrix.map((row) => row[m]);
19730
+ }
19731
+ /**
19732
+ * Coefficient of determination (R²) of an arbitrary fit function against data.
19733
+ * Mirrors `computeRSquared` in chart-trendlines.tsx (React).
19734
+ */
19735
+ function computeRSquared(xVals, yVals, evalFn) {
19736
+ const n = xVals.length;
19737
+ if (n === 0) {
19738
+ return 0;
19739
+ }
19740
+ const meanY = yVals.reduce((s, y) => s + y, 0) / n;
19741
+ let ssRes = 0;
19742
+ let ssTot = 0;
19743
+ for (let i = 0; i < n; i++) {
19744
+ ssRes += (yVals[i] - evalFn(xVals[i])) ** 2;
19745
+ ssTot += (yVals[i] - meanY) ** 2;
19746
+ }
19747
+ return ssTot > 0 ? 1 - ssRes / ssTot : 0;
19748
+ }
19749
+ /**
19750
+ * Compute the polyline points (and equation / R²) for one trendline over a
19751
+ * series' values. Returns empty points when the type is unsupported or data is
19752
+ * too sparse. Mirrors `computeTrendlinePoints` in chart-trendlines.tsx (React).
19753
+ */
19754
+ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode) {
19755
+ const n = values.length;
19756
+ if (n < 2) {
19757
+ return { points: [], equation: '', rSquared: 0 };
19758
+ }
19759
+ const xVals = values.map((_v, i) => i);
19760
+ const yVals = values;
19761
+ const forward = trendline.forward ?? 0;
19762
+ const backward = trendline.backward ?? 0;
19763
+ const startX = -backward;
19764
+ const endX = n - 1 + forward;
19765
+ const steps = Math.max(Math.ceil((endX - startX) * 4), 20);
19766
+ let evalFn;
19767
+ let equation = '';
19768
+ let rSquared = 0;
19769
+ switch (trendline.trendlineType) {
19770
+ case 'linear': {
19771
+ const reg = computeLinearRegression(xVals, yVals);
19772
+ const fixedIntercept = trendline.intercept;
19773
+ const slope = fixedIntercept !== undefined
19774
+ ? yVals.reduce((s, y, i) => s + (y - fixedIntercept) * xVals[i], 0) /
19775
+ xVals.reduce((s, x) => s + x * x, 0)
19776
+ : reg.slope;
19777
+ const b = fixedIntercept ?? reg.intercept;
19778
+ evalFn = (x) => slope * x + b;
19779
+ equation = `y = ${slope.toFixed(2)}x + ${b.toFixed(2)}`;
19780
+ rSquared = reg.rSquared;
19781
+ break;
19782
+ }
19783
+ case 'exponential': {
19784
+ const posY = yVals.filter((y) => y > 0).map((y) => Math.log(y));
19785
+ const posX = xVals.filter((_x, i) => yVals[i] > 0);
19786
+ if (posY.length < 2) {
19787
+ return { points: [], equation: '', rSquared: 0 };
19788
+ }
19789
+ const reg = computeLinearRegression(posX, posY);
19790
+ const a = Math.exp(reg.intercept);
19791
+ const b = reg.slope;
19792
+ evalFn = (x) => a * Math.exp(b * x);
19793
+ equation = `y = ${a.toFixed(2)}e^(${b.toFixed(2)}x)`;
19794
+ rSquared = reg.rSquared;
19795
+ break;
19796
+ }
19797
+ case 'logarithmic': {
19798
+ const posLnX = xVals.filter((x) => x > 0).map((x) => Math.log(x));
19799
+ const filteredY = yVals.filter((_y, i) => xVals[i] > 0);
19800
+ if (posLnX.length < 2) {
19801
+ return { points: [], equation: '', rSquared: 0 };
19802
+ }
19803
+ const reg = computeLinearRegression(posLnX, filteredY);
19804
+ evalFn = (x) => (x > 0 ? reg.slope * Math.log(x) + reg.intercept : 0);
19805
+ equation = `y = ${reg.slope.toFixed(2)}ln(x) + ${reg.intercept.toFixed(2)}`;
19806
+ rSquared = reg.rSquared;
19807
+ break;
19808
+ }
19809
+ case 'power': {
19810
+ const logXArr = xVals.filter((x, i) => x > 0 && yVals[i] > 0).map((x) => Math.log(x));
19811
+ const logYArr = yVals.filter((y, i) => y > 0 && xVals[i] > 0).map((y) => Math.log(y));
19812
+ if (logXArr.length < 2) {
19813
+ return { points: [], equation: '', rSquared: 0 };
19814
+ }
19815
+ const reg = computeLinearRegression(logXArr, logYArr);
19816
+ const a = Math.exp(reg.intercept);
19817
+ evalFn = (x) => (x > 0 ? a * x ** reg.slope : 0);
19818
+ equation = `y = ${a.toFixed(2)}x^${reg.slope.toFixed(2)}`;
19819
+ rSquared = reg.rSquared;
19820
+ break;
19821
+ }
19822
+ case 'polynomial': {
19823
+ const order = Math.min(trendline.order ?? 2, 6);
19824
+ const coeffs = fitPolynomial(xVals, yVals, order);
19825
+ evalFn = (x) => coeffs.reduce((s, c, i) => s + c * x ** i, 0);
19826
+ equation = coeffs.map((c, i) => `${c.toFixed(2)}x^${i}`).join(' + ');
19827
+ rSquared = computeRSquared(xVals, yVals, evalFn);
19828
+ break;
19829
+ }
19830
+ case 'movingAvg': {
19831
+ const period = trendline.period ?? 2;
19832
+ const maPoints = [];
19833
+ for (let i = period - 1; i < n; i++) {
19834
+ let sum = 0;
19835
+ for (let j = i - period + 1; j <= i; j++) {
19836
+ sum += yVals[j];
19837
+ }
19838
+ const avgVal = sum / period;
19839
+ const px = xToPixel(i, catCount, layout, mode);
19840
+ const py = valueToY(avgVal, range, layout.plotTop, layout.plotBottom);
19841
+ maPoints.push({ x: px, y: py });
19842
+ }
19843
+ return {
19844
+ points: maPoints,
19845
+ equation: `${period}-period moving average`,
19846
+ rSquared: 0,
19847
+ };
19848
+ }
19849
+ default:
19850
+ return { points: [], equation: '', rSquared: 0 };
19851
+ }
19852
+ const points = [];
19853
+ for (let step = 0; step <= steps; step++) {
19854
+ const xVal = startX + ((endX - startX) * step) / steps;
19855
+ const yVal = evalFn(xVal);
19856
+ if (!Number.isFinite(yVal)) {
19857
+ continue;
19858
+ }
19859
+ const px = xToPixel(xVal, catCount, layout, mode);
19860
+ const py = valueToY(yVal, range, layout.plotTop, layout.plotBottom);
19861
+ points.push({ x: px, y: py });
19862
+ }
19863
+ return { points, equation, rSquared };
19864
+ }
19865
+ // ─────────────────────────────────────────────────────────────────────────────
19866
+ // Public: trendline primitives
19867
+ // ─────────────────────────────────────────────────────────────────────────────
19868
+ /**
19869
+ * Build `SvgPrimitive[]` for all trendlines declared by every series in
19870
+ * `chartData`. Returns an empty array when no series declares a trendline.
19871
+ *
19872
+ * Each trendline produces:
19873
+ * - one `SvgPath` (dashed polyline in the series / trendline colour), and
19874
+ * - optionally one `SvgText` with the equation / R² label at the last point.
19875
+ *
19876
+ * @param chartData Full parsed chart data.
19877
+ * @param catCount Number of categories (x-slots), e.g. `chartData.categories.length || 1`.
19878
+ * @param layout Plot-area bounding box from `computePlotLayout`.
19879
+ * @param range Value-axis range from `computeValueRange` / `computeStackedValueRange`.
19880
+ * @param mode `'bar'` for bar/column, `'line'` for line/area/scatter.
19881
+ * @param colorPalette Optional resolved palette (same as passed to `seriesColor`).
19882
+ */
19883
+ function computeTrendlinePrimitives(chartData, catCount, layout, range, mode = 'line', colorPalette) {
19884
+ const out = [];
19885
+ chartData.series.forEach((series, si) => {
19886
+ if (!series.trendlines || series.trendlines.length === 0) {
19887
+ return;
19888
+ }
19889
+ series.trendlines.forEach((tl) => {
19890
+ const { points, equation, rSquared } = computeTrendlinePoints(tl, series.values, catCount, layout, range, mode);
19891
+ if (points.length < 2) {
19892
+ return;
19893
+ }
19894
+ const pathD = points
19895
+ .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`)
19896
+ .join(' ');
19897
+ const strokeColor = tl.color ?? seriesColor(series, si, colorPalette);
19898
+ const pathPrimitive = {
19899
+ kind: 'path',
19900
+ d: pathD,
19901
+ fill: 'none',
19902
+ stroke: strokeColor,
19903
+ strokeWidth: 1.5,
19904
+ };
19905
+ out.push(pathPrimitive);
19906
+ const labelParts = [];
19907
+ if (tl.displayEq && equation) {
19908
+ labelParts.push(equation);
19909
+ }
19910
+ if (tl.displayRSq) {
19911
+ labelParts.push(`R² = ${rSquared.toFixed(4)}`);
19912
+ }
19913
+ if (labelParts.length > 0) {
19914
+ const last = points[points.length - 1];
19915
+ const labelText = {
19916
+ kind: 'text',
19917
+ x: last.x,
19918
+ y: last.y - 6,
19919
+ text: labelParts.join(' '),
19920
+ fontSize: 7,
19921
+ fill: strokeColor,
19922
+ textAnchor: 'end',
19923
+ };
19924
+ out.push(labelText);
19925
+ }
19926
+ });
19927
+ });
19928
+ return out;
19929
+ }
19930
+ // ─────────────────────────────────────────────────────────────────────────────
19931
+ // Internal: error-value computation
19932
+ // ─────────────────────────────────────────────────────────────────────────────
19933
+ /**
19934
+ * Resolve the half-width of an error bar for a single data point.
19935
+ * Mirrors `computeErrorValue` in chart-overlay-lines.tsx (React).
19936
+ */
19937
+ function computeErrorValue(errBars, values, pointIndex, direction) {
19938
+ switch (errBars.valType) {
19939
+ case 'fixedVal':
19940
+ return errBars.val ?? 0;
19941
+ case 'percentage':
19942
+ return Math.abs(values[pointIndex]) * ((errBars.val ?? 0) / 100);
19943
+ case 'stdDev': {
19944
+ const n = values.length;
19945
+ if (n === 0) {
19946
+ return 0;
19947
+ }
19948
+ const mean = values.reduce((s, v) => s + v, 0) / n;
19949
+ const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
19950
+ return Math.sqrt(variance) * (errBars.val ?? 1);
19951
+ }
19952
+ case 'stdErr': {
19953
+ const n2 = values.length;
19954
+ if (n2 === 0) {
19955
+ return 0;
19956
+ }
19957
+ const mean2 = values.reduce((s, v) => s + v, 0) / n2;
19958
+ const variance2 = values.reduce((s, v) => s + (v - mean2) ** 2, 0) / n2;
19959
+ return Math.sqrt(variance2 / n2);
19960
+ }
19961
+ case 'cust':
19962
+ return direction === 'plus'
19963
+ ? (errBars.customPlus?.[pointIndex] ?? 0)
19964
+ : (errBars.customMinus?.[pointIndex] ?? 0);
19965
+ default:
19966
+ return 0;
19967
+ }
19968
+ }
19969
+ // ─────────────────────────────────────────────────────────────────────────────
19970
+ // Public: error-bar primitives
19971
+ // ─────────────────────────────────────────────────────────────────────────────
19972
+ /** Stroke colour for error-bar whiskers. */
19973
+ const ERROR_BAR_COLOR = '#334155';
19974
+ /**
19975
+ * Build `SvgPrimitive[]` for all Y-direction error bars in `chartData`.
19976
+ * Produces stem + cap `SvgLine` pairs for each data point.
19977
+ * X-direction error bars are intentionally skipped (not supported in PPTX
19978
+ * bar/line charts displayed here).
19979
+ *
19980
+ * Mirrors `renderErrorBars` in chart-overlay-lines.tsx (React).
19981
+ *
19982
+ * @param chartData Full parsed chart data.
19983
+ * @param catCount Number of categories.
19984
+ * @param layout Plot-area bounding box.
19985
+ * @param range Value-axis range.
19986
+ * @param mode `'bar'` or `'line'` — controls x-pixel mapping.
19987
+ */
19988
+ function computeErrorBarPrimitives(chartData, catCount, layout, range, mode = 'line') {
19989
+ const out = [];
19990
+ const capW = 4;
19991
+ chartData.series.forEach((series) => {
19992
+ if (!series.errBars || series.errBars.length === 0) {
19993
+ return;
19994
+ }
19995
+ series.errBars.forEach((eb) => {
19996
+ if (eb.direction !== 'y') {
19997
+ return;
19998
+ }
19999
+ series.values.forEach((val, vi) => {
20000
+ const cx = xToPixel(vi, catCount, layout, mode);
20001
+ const baseY = valueToY(val, range, layout.plotTop, layout.plotBottom);
20002
+ if (eb.barType === 'plus' || eb.barType === 'both') {
20003
+ const plusErr = computeErrorValue(eb, series.values, vi, 'plus');
20004
+ const topY = valueToY(val + plusErr, range, layout.plotTop, layout.plotBottom);
20005
+ const stem = {
20006
+ kind: 'line',
20007
+ x1: cx,
20008
+ y1: baseY,
20009
+ x2: cx,
20010
+ y2: topY,
20011
+ stroke: ERROR_BAR_COLOR,
20012
+ strokeWidth: 1,
20013
+ };
20014
+ const cap = {
20015
+ kind: 'line',
20016
+ x1: cx - capW,
20017
+ y1: topY,
20018
+ x2: cx + capW,
20019
+ y2: topY,
20020
+ stroke: ERROR_BAR_COLOR,
20021
+ strokeWidth: 1,
20022
+ };
20023
+ out.push(stem, cap);
20024
+ }
20025
+ if (eb.barType === 'minus' || eb.barType === 'both') {
20026
+ const minusErr = computeErrorValue(eb, series.values, vi, 'minus');
20027
+ const botY = valueToY(val - minusErr, range, layout.plotTop, layout.plotBottom);
20028
+ const stem = {
20029
+ kind: 'line',
20030
+ x1: cx,
20031
+ y1: baseY,
20032
+ x2: cx,
20033
+ y2: botY,
20034
+ stroke: ERROR_BAR_COLOR,
20035
+ strokeWidth: 1,
20036
+ };
20037
+ const cap = {
20038
+ kind: 'line',
20039
+ x1: cx - capW,
20040
+ y1: botY,
20041
+ x2: cx + capW,
20042
+ y2: botY,
20043
+ stroke: ERROR_BAR_COLOR,
20044
+ strokeWidth: 1,
20045
+ };
20046
+ out.push(stem, cap);
20047
+ }
20048
+ });
20049
+ });
20050
+ });
20051
+ return out;
20052
+ }
20053
+ // ─────────────────────────────────────────────────────────────────────────────
20054
+ // Public: axis title primitives
20055
+ // ─────────────────────────────────────────────────────────────────────────────
20056
+ /** Fill colour for axis title text. */
20057
+ const AXIS_TITLE_COLOR = '#475569';
20058
+ /**
20059
+ * Build `SvgText[]` for the X and Y axis titles.
20060
+ *
20061
+ * Axis titles are read from `chartData.axes`:
20062
+ * - the primary category axis (`catAx`, `axPos === 'b'`) drives the X title
20063
+ * - the primary value axis (`valAx`, `axPos === 'l'` or first `valAx`) drives the Y title
20064
+ *
20065
+ * **Rotation note**: `SvgText` has no `transform` or `rotate` field. The Y
20066
+ * axis title is therefore placed to the left of the plot without rotation and
20067
+ * noted inline. If the orchestrator adds a `transform?: string` field to
20068
+ * `SvgText` (or a new `SvgTransform` wrapper primitive), the Y title can be
20069
+ * rendered rotated -90° by passing
20070
+ * `transform: \`rotate(-90, ${x}, ${y})\`` — the template expression is
20071
+ * straightforward once the field exists.
20072
+ *
20073
+ * @param chartData Full parsed chart data.
20074
+ * @param layout Plot-area bounding box.
20075
+ */
20076
+ function computeAxisTitlePrimitives(chartData, layout) {
20077
+ const out = [];
20078
+ const axes = chartData.axes;
20079
+ if (!axes || axes.length === 0) {
20080
+ return out;
20081
+ }
20082
+ // X axis title (category axis at bottom).
20083
+ const catAxis = axes.find((a) => a.axisType === 'catAx' && a.axPos !== 'r' && a.titleText);
20084
+ if (catAxis?.titleText) {
20085
+ const xTitle = {
20086
+ kind: 'text',
20087
+ x: layout.plotLeft + layout.plotWidth / 2,
20088
+ y: layout.plotBottom + 22,
20089
+ text: catAxis.titleText,
20090
+ fontSize: 9,
20091
+ fill: AXIS_TITLE_COLOR,
20092
+ textAnchor: 'middle',
20093
+ fontWeight: 'bold',
20094
+ };
20095
+ out.push(xTitle);
20096
+ }
20097
+ // Y axis title (value axis at left), rotated -90° about its own anchor and
20098
+ // centred vertically on the plot area.
20099
+ const valAxis = axes.find((a) => a.axisType === 'valAx' && a.axPos !== 'r' && a.titleText) ??
20100
+ axes.find((a) => a.axisType === 'valAx' && a.titleText);
20101
+ if (valAxis?.titleText) {
20102
+ const yx = 12;
20103
+ const yy = layout.plotTop + layout.plotHeight / 2;
20104
+ const yTitle = {
20105
+ kind: 'text',
20106
+ x: yx,
20107
+ y: yy,
20108
+ text: valAxis.titleText,
20109
+ fontSize: 9,
20110
+ fill: AXIS_TITLE_COLOR,
20111
+ textAnchor: 'middle',
20112
+ fontWeight: 'bold',
20113
+ transform: `rotate(-90, ${yx}, ${yy})`,
20114
+ };
20115
+ out.push(yTitle);
20116
+ }
20117
+ return out;
20118
+ }
20119
+ // ─────────────────────────────────────────────────────────────────────────────
20120
+ // Internal: data-table cell formatting
20121
+ // ─────────────────────────────────────────────────────────────────────────────
20122
+ /**
20123
+ * Format a single data value as a short human-readable string.
20124
+ * Mirrors `formatValue` in chart-data-table.tsx (React) — identical to
20125
+ * `formatAxisValue` but kept local for clarity.
20126
+ */
20127
+ function formatDataValue(val) {
20128
+ return formatAxisValue(val);
20129
+ }
20130
+ // ─────────────────────────────────────────────────────────────────────────────
20131
+ // Public: data table primitives
20132
+ // ─────────────────────────────────────────────────────────────────────────────
20133
+ /**
20134
+ * Layout constants for the SVG data table rendered below the plot area.
20135
+ * Kept as named constants so tests can assert against them without magic numbers.
20136
+ */
20137
+ const DATA_TABLE_ROW_H = 14;
20138
+ const DATA_TABLE_HEADER_H = 14;
20139
+ const DATA_TABLE_KEY_W = 60;
20140
+ const DATA_TABLE_PADDING = 4;
20141
+ /**
20142
+ * Build `SvgPrimitive[]` for a simple data table rendered below the plot area.
20143
+ *
20144
+ * The table is rendered as SVG `rect` (borders) + `text` (labels) primitives.
20145
+ * Columns = categories; rows = series (with an optional series-key column on
20146
+ * the left when `dataTable.showKeys !== false`).
20147
+ *
20148
+ * Border flags from `PptxChartDataTable` are respected:
20149
+ * - `showHorzBorder` — horizontal rules between rows
20150
+ * - `showVertBorder` — vertical rules between columns
20151
+ * - `showOutline` — outer border rectangle
20152
+ * - `showKeys` — series name/colour key column
20153
+ *
20154
+ * Mirrors `renderChartDataTable` in chart-data-table.tsx (React), translated
20155
+ * to pure SVG primitives so the Angular template can render them natively.
20156
+ *
20157
+ * @param chartData Full parsed chart data (`dataTable` must be present).
20158
+ * @param layout Plot-area bounding box — the table is placed at `plotBottom + 4`.
20159
+ * @param colorPalette Optional resolved colour palette (same as chart).
20160
+ */
20161
+ function computeDataTablePrimitives(chartData, layout, colorPalette) {
20162
+ const table = chartData.dataTable;
20163
+ if (!table) {
20164
+ return [];
20165
+ }
20166
+ const categories = chartData.categories;
20167
+ const series = chartData.series;
20168
+ if (categories.length === 0 && series.length === 0) {
20169
+ return [];
20170
+ }
20171
+ const out = [];
20172
+ const showH = table.showHorzBorder !== false;
20173
+ const showV = table.showVertBorder !== false;
20174
+ const showO = table.showOutline !== false;
20175
+ const showK = table.showKeys !== false;
20176
+ const borderColor = '#cbd5e1';
20177
+ const textColor = '#334155';
20178
+ const headerWeight = 'bold';
20179
+ const catCount = categories.length;
20180
+ const seriesCount = series.length;
20181
+ // Column metrics
20182
+ const keyColW = showK ? DATA_TABLE_KEY_W : 0;
20183
+ const totalW = layout.plotWidth;
20184
+ const cellW = catCount > 0 ? (totalW - keyColW) / catCount : totalW - keyColW;
20185
+ // Table top edge (just below the plot bottom)
20186
+ const tableTop = layout.plotBottom + DATA_TABLE_PADDING;
20187
+ // Total table height: 1 header row + N series rows
20188
+ const tableH = DATA_TABLE_HEADER_H + seriesCount * DATA_TABLE_ROW_H;
20189
+ // Outer border — rendered as four SvgLine segments because SvgRect has no
20190
+ // `stroke` field (only `fill`).
20191
+ if (showO) {
20192
+ const mkBorderLine = (x1, y1, x2, y2) => ({
20193
+ kind: 'line',
20194
+ x1,
20195
+ y1,
20196
+ x2,
20197
+ y2,
20198
+ stroke: borderColor,
20199
+ strokeWidth: 1,
20200
+ });
20201
+ out.push(mkBorderLine(layout.plotLeft, tableTop, layout.plotLeft + totalW, tableTop));
20202
+ out.push(mkBorderLine(layout.plotLeft + totalW, tableTop, layout.plotLeft + totalW, tableTop + tableH));
20203
+ out.push(mkBorderLine(layout.plotLeft + totalW, tableTop + tableH, layout.plotLeft, tableTop + tableH));
20204
+ out.push(mkBorderLine(layout.plotLeft, tableTop + tableH, layout.plotLeft, tableTop));
20205
+ }
20206
+ // Helper: x-position of column ci (0-based category columns, after key col)
20207
+ function colX(ci) {
20208
+ return layout.plotLeft + keyColW + ci * cellW;
20209
+ }
20210
+ // Helper: y-position of row ri (0 = header)
20211
+ function rowY(ri) {
20212
+ return tableTop + (ri === 0 ? 0 : DATA_TABLE_HEADER_H + (ri - 1) * DATA_TABLE_ROW_H);
20213
+ }
20214
+ // Header row: category labels
20215
+ categories.forEach((cat, ci) => {
20216
+ const x = colX(ci) + cellW / 2;
20217
+ const y = rowY(0) + DATA_TABLE_HEADER_H / 2 + 3;
20218
+ const label = {
20219
+ kind: 'text',
20220
+ x,
20221
+ y,
20222
+ text: cat,
20223
+ fontSize: 8,
20224
+ fill: textColor,
20225
+ textAnchor: 'middle',
20226
+ fontWeight: headerWeight,
20227
+ };
20228
+ out.push(label);
20229
+ // Vertical border after this column header (not after the last)
20230
+ if (showV && ci < catCount - 1) {
20231
+ const vx = colX(ci) + cellW;
20232
+ const vLine = {
20233
+ kind: 'line',
20234
+ x1: vx,
20235
+ y1: tableTop,
20236
+ x2: vx,
20237
+ y2: tableTop + tableH,
20238
+ stroke: borderColor,
20239
+ strokeWidth: 1,
20240
+ };
20241
+ out.push(vLine);
20242
+ }
20243
+ });
20244
+ // Horizontal border under header
20245
+ if (showH) {
20246
+ const hy = tableTop + DATA_TABLE_HEADER_H;
20247
+ const hLine = {
20248
+ kind: 'line',
20249
+ x1: layout.plotLeft,
20250
+ y1: hy,
20251
+ x2: layout.plotLeft + totalW,
20252
+ y2: hy,
20253
+ stroke: borderColor,
20254
+ strokeWidth: 1,
20255
+ };
20256
+ out.push(hLine);
20257
+ }
20258
+ // Vertical border between key column and first data column
20259
+ if (showK && showV) {
20260
+ const kvx = layout.plotLeft + keyColW;
20261
+ const kvLine = {
20262
+ kind: 'line',
20263
+ x1: kvx,
20264
+ y1: tableTop,
20265
+ x2: kvx,
20266
+ y2: tableTop + tableH,
20267
+ stroke: borderColor,
20268
+ strokeWidth: 1,
20269
+ };
20270
+ out.push(kvLine);
20271
+ }
20272
+ // Data rows
20273
+ series.forEach((s, si) => {
20274
+ const rowIndex = si + 1; // row 0 is the header
20275
+ const ry = rowY(rowIndex);
20276
+ const cellCy = ry + DATA_TABLE_ROW_H / 2 + 3;
20277
+ // Series key cell (colour swatch + name)
20278
+ if (showK) {
20279
+ const swatchX = layout.plotLeft + DATA_TABLE_PADDING;
20280
+ const swatchY = ry + DATA_TABLE_ROW_H / 2 - 3;
20281
+ const swatchColor = seriesColor(s, si, colorPalette);
20282
+ // Colour swatch as a small filled rect
20283
+ out.push({
20284
+ kind: 'rect',
20285
+ x: swatchX,
20286
+ y: swatchY,
20287
+ w: 7,
20288
+ h: 7,
20289
+ fill: swatchColor,
20290
+ rx: 1,
20291
+ });
20292
+ // Series name text
20293
+ const nameX = swatchX + 9;
20294
+ const nameLabel = {
20295
+ kind: 'text',
20296
+ x: nameX,
20297
+ y: cellCy,
20298
+ text: s.name,
20299
+ fontSize: 8,
20300
+ fill: textColor,
20301
+ textAnchor: 'start',
20302
+ };
20303
+ out.push(nameLabel);
20304
+ }
20305
+ // Data cells
20306
+ categories.forEach((_cat, ci) => {
20307
+ const val = s.values[ci];
20308
+ const cellLabel = {
20309
+ kind: 'text',
20310
+ x: colX(ci) + cellW / 2,
20311
+ y: cellCy,
20312
+ text: val !== undefined ? formatDataValue(val) : '',
20313
+ fontSize: 8,
20314
+ fill: textColor,
20315
+ textAnchor: 'middle',
20316
+ };
20317
+ out.push(cellLabel);
20318
+ });
20319
+ // Horizontal border below this row (not after the last)
20320
+ if (showH && si < seriesCount - 1) {
20321
+ const hy2 = ry + DATA_TABLE_ROW_H;
20322
+ const hRowLine = {
20323
+ kind: 'line',
20324
+ x1: layout.plotLeft,
20325
+ y1: hy2,
20326
+ x2: layout.plotLeft + totalW,
20327
+ y2: hy2,
20328
+ stroke: borderColor,
20329
+ strokeWidth: 1,
20330
+ };
20331
+ out.push(hRowLine);
20332
+ }
20333
+ });
20334
+ return out;
20335
+ }
20336
+
19613
20337
  /**
19614
20338
  * View-model builders for surface and treemap chart kinds.
19615
20339
  *
@@ -21409,6 +22133,9 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
21409
22133
  }
21410
22134
  }
21411
22135
  const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
22136
+ // Overlays (depth): regression trendlines, error bars, axis titles, data
22137
+ // table — appended on top of the base cartesian primitives.
22138
+ primitives.push(...computeTrendlinePrimitives(chartData, catCount, layout, range, catAxisStyle, chartData.colorPalette), ...computeErrorBarPrimitives(chartData, catCount, layout, range, catAxisStyle), ...computeAxisTitlePrimitives(chartData, layout), ...computeDataTablePrimitives(chartData, layout, chartData.colorPalette));
21412
22139
  return {
21413
22140
  svgWidth: layout.svgWidth,
21414
22141
  svgHeight: layout.svgHeight,
@@ -21751,6 +22478,21 @@ class ChartRendererComponent {
21751
22478
  [attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
21752
22479
  />
21753
22480
  }
22481
+ @case ('text') {
22482
+ <text
22483
+ [attr.x]="asText(prim).x"
22484
+ [attr.y]="asText(prim).y"
22485
+ [attr.text-anchor]="asText(prim).textAnchor"
22486
+ [attr.font-size]="asText(prim).fontSize"
22487
+ [attr.fill]="asText(prim).fill"
22488
+ [attr.font-weight]="asText(prim).fontWeight ?? 'normal'"
22489
+ [attr.dominant-baseline]="asText(prim).dominantBaseline ?? 'auto'"
22490
+ [attr.opacity]="asText(prim).opacity ?? 1"
22491
+ [attr.transform]="asText(prim).transform ?? null"
22492
+ >
22493
+ {{ asText(prim).text }}
22494
+ </text>
22495
+ }
21754
22496
  }
21755
22497
  }
21756
22498
 
@@ -21940,6 +22682,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
21940
22682
  [attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
21941
22683
  />
21942
22684
  }
22685
+ @case ('text') {
22686
+ <text
22687
+ [attr.x]="asText(prim).x"
22688
+ [attr.y]="asText(prim).y"
22689
+ [attr.text-anchor]="asText(prim).textAnchor"
22690
+ [attr.font-size]="asText(prim).fontSize"
22691
+ [attr.fill]="asText(prim).fill"
22692
+ [attr.font-weight]="asText(prim).fontWeight ?? 'normal'"
22693
+ [attr.dominant-baseline]="asText(prim).dominantBaseline ?? 'auto'"
22694
+ [attr.opacity]="asText(prim).opacity ?? 1"
22695
+ [attr.transform]="asText(prim).transform ?? null"
22696
+ >
22697
+ {{ asText(prim).text }}
22698
+ </text>
22699
+ }
21943
22700
  }
21944
22701
  }
21945
22702
 
@@ -27653,6 +28410,855 @@ function bulletIndentPx(level) {
27653
28410
  return lvl * INDENT_PX_PER_LEVEL;
27654
28411
  }
27655
28412
 
28413
+ /**
28414
+ * Presets that require SVG `<textPath>` rendering along a curved/circular path.
28415
+ * All other warp presets (envelope: inflate/deflate/can; simple: slant/fade/
28416
+ * cascade) are rendered with CSS transforms instead — see `text-warp.ts`'s
28417
+ * `getWarpCategory`, which this set must stay in sync with (the `'path'`
28418
+ * category). Listing a CSS preset here would wrongly route it to `<textPath>`.
28419
+ */
28420
+ const SVG_WARP_PRESETS = new Set([
28421
+ 'textArchUp',
28422
+ 'textArchDown',
28423
+ 'textCircle',
28424
+ 'textWave1',
28425
+ 'textWave2',
28426
+ 'textWave4',
28427
+ 'textDoubleWave1',
28428
+ 'textCurveUp',
28429
+ 'textCurveDown',
28430
+ 'textArchUpPour',
28431
+ 'textArchDownPour',
28432
+ 'textCirclePour',
28433
+ 'textButton',
28434
+ 'textButtonPour',
28435
+ 'textRingInside',
28436
+ 'textRingOutside',
28437
+ 'textTriangle',
28438
+ 'textTriangleInverted',
28439
+ 'textChevron',
28440
+ 'textChevronInverted',
28441
+ 'textStop',
28442
+ ]);
28443
+ // ── Cascade helpers (inlined from warp-path-cascade) ──────────────────
28444
+ /** Cascading up — lines tilt from lower-left to upper-right.
28445
+ * adj (default 44444) controls the cascade tilt amount. */
28446
+ function cascadeUpPath(w, h, t, adj) {
28447
+ const adjFactor = adj !== undefined ? adj / 44444 : 1;
28448
+ const tilt = 0.2 * Math.max(0, Math.min(adjFactor, 4));
28449
+ const yMid = h * (0.2 + t * 0.6);
28450
+ const yStart = yMid + (h * tilt) / 2;
28451
+ const yEnd = yMid - (h * tilt) / 2;
28452
+ return `M 0,${yStart} L ${w},${yEnd}`;
28453
+ }
28454
+ /** Cascading down — lines tilt from upper-left to lower-right.
28455
+ * adj (default 44444) controls the cascade tilt amount. */
28456
+ function cascadeDownPath(w, h, t, adj) {
28457
+ const adjFactor = adj !== undefined ? adj / 44444 : 1;
28458
+ const tilt = 0.2 * Math.max(0, Math.min(adjFactor, 4));
28459
+ const yMid = h * (0.2 + t * 0.6);
28460
+ const yStart = yMid - (h * tilt) / 2;
28461
+ const yEnd = yMid + (h * tilt) / 2;
28462
+ return `M 0,${yStart} L ${w},${yEnd}`;
28463
+ }
28464
+ // ── Priority 1 path generators ─────────────────────────────────────────
28465
+ /** Concentric upward arcs from (0, h) → (w, h). t=0 is the tallest arch.
28466
+ * adj (default 10800000) controls the arch height — higher values = taller arch. */
28467
+ function archUpPath(w, h, t, adj) {
28468
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28469
+ const maxArch = (0.85 * adjNorm) / 0.5;
28470
+ const archH = h * Math.max(0, maxArch - t * 0.7);
28471
+ if (archH < 1) {
28472
+ return `M 0,${h} L ${w},${h}`;
28473
+ }
28474
+ return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
28475
+ }
28476
+ /** Concentric downward arcs from (0, 0) → (w, 0). t=1 is the deepest.
28477
+ * adj (default 10800000) controls the arch depth. */
28478
+ function archDownPath(w, h, t, adj) {
28479
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28480
+ const baseDepth = (0.15 * adjNorm) / 0.5;
28481
+ const archH = h * (baseDepth + t * 0.7);
28482
+ if (archH < 1) {
28483
+ return `M 0,0 L ${w},0`;
28484
+ }
28485
+ return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
28486
+ }
28487
+ /** Full ellipse — concentric ellipses shrink towards centre.
28488
+ * adj (default 10800000) controls the arc span angle in 60000ths of a degree. */
28489
+ function circlePath(w, h, t, adj) {
28490
+ const cx = w / 2;
28491
+ const cy = h / 2;
28492
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28493
+ const baseScale = 0.45 + adjNorm * 1.1;
28494
+ const scale = Math.min(1, baseScale) - t * 0.55;
28495
+ const rx = Math.max(1, (w / 2) * scale);
28496
+ const ry = Math.max(1, (h / 2) * scale);
28497
+ return (`M ${cx},${cy - ry} ` +
28498
+ `A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
28499
+ `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
28500
+ }
28501
+ /** Single sine-wave from left to right using a cubic Bézier.
28502
+ * adj (default 12500) controls wave amplitude.
28503
+ * adj2 (default 0) controls horizontal phase shift. */
28504
+ function wave1Path(w, h, t, adj, adj2) {
28505
+ const yMid = h * (0.25 + t * 0.5);
28506
+ const adjFactor = adj !== undefined ? adj / 12500 : 1;
28507
+ const amp = h * 0.2 * Math.max(0, Math.min(adjFactor, 4));
28508
+ const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.3 : 0;
28509
+ const cp1x = w / 3 + hShift;
28510
+ const cp2x = (2 * w) / 3 + hShift;
28511
+ return `M 0,${yMid} C ${cp1x},${yMid - amp} ${cp2x},${yMid + amp} ${w},${yMid}`;
28512
+ }
28513
+ /** Inflate — top lines bow upward, bottom lines bow downward.
28514
+ * adj (default 18750) controls bulge amount. */
28515
+ function inflatePath(w, h, t, adj) {
28516
+ const yBase = h * (0.15 + t * 0.7);
28517
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28518
+ const bulge = h * 0.3 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
28519
+ return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
28520
+ }
28521
+ /** Deflate — opposite of inflate.
28522
+ * adj (default 18750) controls pinch amount. */
28523
+ function deflatePath(w, h, t, adj) {
28524
+ const yBase = h * (0.15 + t * 0.7);
28525
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28526
+ const pinch = h * 0.3 * (2 * t - 1) * Math.max(0, Math.min(adjFactor, 4));
28527
+ return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
28528
+ }
28529
+ /** Gentle upward curve.
28530
+ * adj (default 45977) controls curve height. */
28531
+ function curveUpPath(w, h, t, adj) {
28532
+ const yBase = h * (0.35 + t * 0.55);
28533
+ const adjFactor = adj !== undefined ? adj / 45977 : 1;
28534
+ const curve = h * 0.4 * (1 - t * 0.3) * Math.max(0, Math.min(adjFactor, 4));
28535
+ return `M 0,${yBase} Q ${w / 2},${yBase - curve} ${w},${yBase}`;
28536
+ }
28537
+ /** Gentle downward curve.
28538
+ * adj (default 45977) controls curve depth. */
28539
+ function curveDownPath(w, h, t, adj) {
28540
+ const yBase = h * (0.1 + t * 0.55);
28541
+ const adjFactor = adj !== undefined ? adj / 45977 : 1;
28542
+ const curve = h * 0.4 * (1 - (1 - t) * 0.3) * Math.max(0, Math.min(adjFactor, 4));
28543
+ return `M 0,${yBase} Q ${w / 2},${yBase + curve} ${w},${yBase}`;
28544
+ }
28545
+ // ── Priority 2 path generators ─────────────────────────────────────────
28546
+ /** Inverted single wave (phase-shifted wave1).
28547
+ * adj (default 12500) controls wave amplitude.
28548
+ * adj2 (default 0) controls horizontal phase shift. */
28549
+ function wave2Path(w, h, t, adj, adj2) {
28550
+ const yMid = h * (0.25 + t * 0.5);
28551
+ const adjFactor = adj !== undefined ? adj / 12500 : 1;
28552
+ const amp = h * 0.2 * Math.max(0, Math.min(adjFactor, 4));
28553
+ const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.3 : 0;
28554
+ const cp1x = w / 3 + hShift;
28555
+ const cp2x = (2 * w) / 3 + hShift;
28556
+ return `M 0,${yMid} C ${cp1x},${yMid + amp} ${cp2x},${yMid - amp} ${w},${yMid}`;
28557
+ }
28558
+ /** Double wave — two full wave cycles across the width.
28559
+ * adj (default 12500) controls wave amplitude.
28560
+ * adj2 (default 0) controls horizontal phase shift. */
28561
+ function wave4Path(w, h, t, adj, adj2) {
28562
+ const yMid = h * (0.25 + t * 0.5);
28563
+ const adjFactor = adj !== undefined ? adj / 12500 : 1;
28564
+ const amp = h * 0.15 * Math.max(0, Math.min(adjFactor, 4));
28565
+ const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.15 : 0;
28566
+ const q = w / 4;
28567
+ return (`M 0,${yMid} ` +
28568
+ `C ${q + hShift},${yMid - amp} ${2 * q + hShift},${yMid + amp} ${w / 2},${yMid} ` +
28569
+ `C ${w / 2 + q + hShift},${yMid - amp} ${w - q + hShift},${yMid + amp} ${w},${yMid}`);
28570
+ }
28571
+ /** Double wave with alternating rhythm.
28572
+ * adj (default 6250) controls wave amplitude.
28573
+ * adj2 (default 0) controls horizontal phase shift. */
28574
+ function doubleWave1Path(w, h, t, adj, adj2) {
28575
+ const yMid = h * (0.25 + t * 0.5);
28576
+ const adjFactor = adj !== undefined ? adj / 6250 : 1;
28577
+ const amp = h * 0.18 * Math.max(0, Math.min(adjFactor, 4));
28578
+ const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.15 : 0;
28579
+ const q = w / 4;
28580
+ return (`M 0,${yMid} ` +
28581
+ `C ${q + hShift},${yMid - amp} ${2 * q + hShift},${yMid + amp} ${w / 2},${yMid} ` +
28582
+ `C ${w / 2 + q + hShift},${yMid + amp} ${w - q + hShift},${yMid - amp} ${w},${yMid}`);
28583
+ }
28584
+ /** Cylindrical text — upward.
28585
+ * adj (default 18750) controls the cylinder curvature. */
28586
+ function canUpPath(w, h, t, adj) {
28587
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28588
+ const curvature = Math.max(0, Math.min(adjFactor, 4));
28589
+ const archH = h * (0.35 - t * 0.25) * curvature;
28590
+ if (archH < 1) {
28591
+ return `M 0,${h} L ${w},${h}`;
28592
+ }
28593
+ return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
28594
+ }
28595
+ /** Cylindrical text — downward.
28596
+ * adj (default 18750) controls the cylinder curvature. */
28597
+ function canDownPath(w, h, t, adj) {
28598
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28599
+ const curvature = Math.max(0, Math.min(adjFactor, 4));
28600
+ const archH = h * (0.1 + t * 0.25) * curvature;
28601
+ if (archH < 1) {
28602
+ return `M 0,0 L ${w},0`;
28603
+ }
28604
+ return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
28605
+ }
28606
+ /** Button shape — convex top / concave bottom.
28607
+ * adj (default 18750) controls the curve amount. */
28608
+ function buttonPath(w, h, t, adj) {
28609
+ const yBase = h * (0.1 + t * 0.8);
28610
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28611
+ const bulge = h * 0.15 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
28612
+ return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
28613
+ }
28614
+ /** Ring inside — concentric ellipses scaled inward.
28615
+ * adj (default 18750) controls ring thickness. */
28616
+ function ringInsidePath(w, h, t, adj) {
28617
+ const cx = w / 2;
28618
+ const cy = h / 2;
28619
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28620
+ const thickness = 0.35 * Math.max(0, Math.min(adjFactor, 4));
28621
+ const scale = 0.7 - t * thickness;
28622
+ const rx = Math.max(1, (w / 2) * scale);
28623
+ const ry = Math.max(1, (h / 2) * scale);
28624
+ return (`M ${cx},${cy - ry} ` +
28625
+ `A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
28626
+ `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
28627
+ }
28628
+ /** Ring outside — concentric ellipses scaled outward.
28629
+ * adj (default 18750) controls ring thickness. */
28630
+ function ringOutsidePath(w, h, t, adj) {
28631
+ const cx = w / 2;
28632
+ const cy = h / 2;
28633
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28634
+ const thickness = 0.35 * Math.max(0, Math.min(adjFactor, 4));
28635
+ const scale = 1 - t * thickness;
28636
+ const rx = Math.max(1, (w / 2) * scale);
28637
+ const ry = Math.max(1, (h / 2) * scale);
28638
+ return (`M ${cx},${cy - ry} ` +
28639
+ `A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
28640
+ `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
28641
+ }
28642
+ // ── Priority 3 path generators ─────────────────────────────────────────
28643
+ /** Triangle / trapezoid — top line narrow, bottom line full width.
28644
+ * adj (default 50000) controls the narrowness at the top. */
28645
+ function trianglePath(w, h, t, adj) {
28646
+ const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
28647
+ const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
28648
+ const lineW = narrowW + t * (w - narrowW);
28649
+ const xStart = (w - lineW) / 2;
28650
+ const yBase = h * (0.1 + t * 0.8);
28651
+ return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
28652
+ }
28653
+ /** Inverted triangle — top line full width, bottom line narrow.
28654
+ * adj (default 50000) controls the narrowness at the bottom. */
28655
+ function triangleInvertedPath(w, h, t, adj) {
28656
+ const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
28657
+ const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
28658
+ const lineW = w - t * (w - narrowW);
28659
+ const xStart = (w - lineW) / 2;
28660
+ const yBase = h * (0.1 + t * 0.8);
28661
+ return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
28662
+ }
28663
+ /** Stop / octagon — lines narrow at top and bottom, widest in centre.
28664
+ * adj (default 25000) controls the amount of corner inset. */
28665
+ function stopPath(w, h, t, adj) {
28666
+ const adjFactor = adj !== undefined ? adj / 25000 : 1;
28667
+ const insetScale = Math.max(0, Math.min(adjFactor, 4));
28668
+ const inset = w * 0.15 * (1 - (1 - 2 * Math.abs(t - 0.5)) ** 2) * insetScale;
28669
+ const yBase = h * (0.1 + t * 0.8);
28670
+ return `M ${inset},${yBase} L ${w - inset},${yBase}`;
28671
+ }
28672
+ /** Chevron — V-shape pointing down.
28673
+ * adj (default 25000) controls chevron point height. */
28674
+ function chevronPath(w, h, t, adj) {
28675
+ const yBase = h * (0.15 + t * 0.7);
28676
+ const adjFactor = adj !== undefined ? adj / 25000 : 1;
28677
+ const dip = h * 0.2 * (1 - t) * Math.max(0, Math.min(adjFactor, 4));
28678
+ return `M 0,${yBase} L ${w / 2},${yBase + dip} L ${w},${yBase}`;
28679
+ }
28680
+ /** Inverted chevron — V-shape pointing up.
28681
+ * adj (default 25000) controls chevron point height. */
28682
+ function chevronInvertedPath(w, h, t, adj) {
28683
+ const yBase = h * (0.15 + t * 0.7);
28684
+ const adjFactor = adj !== undefined ? adj / 25000 : 1;
28685
+ const rise = h * 0.2 * t * Math.max(0, Math.min(adjFactor, 4));
28686
+ return `M 0,${yBase} L ${w / 2},${yBase - rise} L ${w},${yBase}`;
28687
+ }
28688
+ /** Inflate bottom only.
28689
+ * adj (default 18750) controls bulge amount. */
28690
+ function inflateBottomPath(w, h, t, adj) {
28691
+ const yBase = h * (0.15 + t * 0.7);
28692
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28693
+ const bulge = t > 0.4 ? h * 0.25 * ((t - 0.4) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
28694
+ return `M 0,${yBase} Q ${w / 2},${yBase + bulge} ${w},${yBase}`;
28695
+ }
28696
+ /** Inflate top only.
28697
+ * adj (default 18750) controls bulge amount. */
28698
+ function inflateTopPath(w, h, t, adj) {
28699
+ const yBase = h * (0.15 + t * 0.7);
28700
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28701
+ const bulge = t < 0.6 ? h * 0.25 * ((0.6 - t) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
28702
+ return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
28703
+ }
28704
+ /** Deflate bottom only.
28705
+ * adj (default 18750) controls pinch amount. */
28706
+ function deflateBottomPath(w, h, t, adj) {
28707
+ const yBase = h * (0.15 + t * 0.7);
28708
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28709
+ const pinch = t > 0.4 ? h * 0.2 * ((t - 0.4) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
28710
+ return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
28711
+ }
28712
+ /** Deflate top only.
28713
+ * adj (default 18750) controls pinch amount. */
28714
+ function deflateTopPath(w, h, t, adj) {
28715
+ const yBase = h * (0.15 + t * 0.7);
28716
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28717
+ const pinch = t < 0.6 ? h * 0.2 * ((0.6 - t) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
28718
+ return `M 0,${yBase} Q ${w / 2},${yBase + pinch} ${w},${yBase}`;
28719
+ }
28720
+ // ── Priority 4 path generators ─────────────────────────────────────────
28721
+ /** Slant up — baseline rises from left to right.
28722
+ * adj (default 55000) controls the slant angle. */
28723
+ function slantUpPath(w, h, t, adj) {
28724
+ const adjFactor = adj !== undefined ? adj / 55000 : 1;
28725
+ const slant = 0.25 * Math.max(0, Math.min(adjFactor, 4));
28726
+ const yMid = h * (0.175 + t * 0.55);
28727
+ const yStart = yMid + (h * slant) / 2;
28728
+ const yEnd = yMid - (h * slant) / 2;
28729
+ return `M 0,${yStart} L ${w},${yEnd}`;
28730
+ }
28731
+ /** Slant down — baseline falls from left to right.
28732
+ * adj (default 55000) controls the slant angle. */
28733
+ function slantDownPath(w, h, t, adj) {
28734
+ const adjFactor = adj !== undefined ? adj / 55000 : 1;
28735
+ const slant = 0.25 * Math.max(0, Math.min(adjFactor, 4));
28736
+ const yMid = h * (0.175 + t * 0.55);
28737
+ const yStart = yMid - (h * slant) / 2;
28738
+ const yEnd = yMid + (h * slant) / 2;
28739
+ return `M 0,${yStart} L ${w},${yEnd}`;
28740
+ }
28741
+ /** Fade right — text narrows towards the right (trapezoid).
28742
+ * adj (default 50000) controls the fade/squeeze amount. */
28743
+ function fadeRightPath(w, h, t, adj) {
28744
+ const adjFactor = adj !== undefined ? adj / 50000 : 1;
28745
+ const squeezeScale = Math.max(0, Math.min(adjFactor, 4));
28746
+ const yLeft = h * (0.1 + t * 0.8);
28747
+ const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
28748
+ const yRight = h * (0.5 + squeeze * 0.4);
28749
+ return `M 0,${yLeft} L ${w},${yRight}`;
28750
+ }
28751
+ /** Fade left — text narrows towards the left (trapezoid).
28752
+ * adj (default 50000) controls the fade/squeeze amount. */
28753
+ function fadeLeftPath(w, h, t, adj) {
28754
+ const adjFactor = adj !== undefined ? adj / 50000 : 1;
28755
+ const squeezeScale = Math.max(0, Math.min(adjFactor, 4));
28756
+ const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
28757
+ const yLeft = h * (0.5 + squeeze * 0.4);
28758
+ const yRight = h * (0.1 + t * 0.8);
28759
+ return `M 0,${yLeft} L ${w},${yRight}`;
28760
+ }
28761
+ /** Fade up — text narrows towards the top.
28762
+ * adj (default 50000) controls the fade/taper amount. */
28763
+ function fadeUpPath(w, h, t, adj) {
28764
+ const adjFactor = adj !== undefined ? adj / 50000 : 1;
28765
+ const taperScale = Math.max(0, Math.min(adjFactor, 4));
28766
+ const narrowFraction = 1 - 0.7 * taperScale;
28767
+ const narrowW = w * Math.max(0, narrowFraction);
28768
+ const lineW = narrowW + t * (w - narrowW);
28769
+ const xStart = (w - lineW) / 2;
28770
+ const yBase = h * (0.1 + t * 0.8);
28771
+ return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
28772
+ }
28773
+ /** Fade down — text narrows towards the bottom.
28774
+ * adj (default 50000) controls the fade/taper amount. */
28775
+ function fadeDownPath(w, h, t, adj) {
28776
+ const adjFactor = adj !== undefined ? adj / 50000 : 1;
28777
+ const taperScale = Math.max(0, Math.min(adjFactor, 4));
28778
+ const narrowFraction = 1 - 0.7 * taperScale;
28779
+ const narrowW = w * Math.max(0, narrowFraction);
28780
+ const lineW = w - t * (w - narrowW);
28781
+ const xStart = (w - lineW) / 2;
28782
+ const yBase = h * (0.1 + t * 0.8);
28783
+ return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
28784
+ }
28785
+ /** Arch up pour — hollowed arch upward (like archUp but with inner hole).
28786
+ * adj (default 10800000) controls the arch height in 60000ths of a degree. */
28787
+ function archUpPourPath(w, h, t, adj) {
28788
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28789
+ const maxArch = (0.7 * adjNorm) / 0.5;
28790
+ const archH = h * Math.max(0, maxArch - t * 0.5);
28791
+ if (archH < 1) {
28792
+ return `M 0,${h} L ${w},${h}`;
28793
+ }
28794
+ return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
28795
+ }
28796
+ /** Arch down pour — hollowed arch downward.
28797
+ * adj (default 10800000) controls the arch depth in 60000ths of a degree. */
28798
+ function archDownPourPath(w, h, t, adj) {
28799
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28800
+ const baseDepth = (0.2 * adjNorm) / 0.5;
28801
+ const archH = h * (baseDepth + t * 0.5);
28802
+ if (archH < 1) {
28803
+ return `M 0,0 L ${w},0`;
28804
+ }
28805
+ return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
28806
+ }
28807
+ /** Circle pour — concentric ellipses (like circle but with an inner gap).
28808
+ * adj (default 10800000) controls the arc span angle in 60000ths of a degree. */
28809
+ function circlePourPath(w, h, t, adj) {
28810
+ const cx = w / 2;
28811
+ const cy = h / 2;
28812
+ const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
28813
+ const baseScale = 0.35 + Number(adjNorm);
28814
+ const scale = Math.min(1, baseScale) - t * 0.45;
28815
+ const rx = Math.max(1, (w / 2) * scale);
28816
+ const ry = Math.max(1, (h / 2) * scale);
28817
+ return (`M ${cx},${cy - ry} ` +
28818
+ `A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
28819
+ `A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
28820
+ }
28821
+ /** Button pour — convex top / concave bottom with larger margins.
28822
+ * adj (default 18750) controls the curve amount. */
28823
+ function buttonPourPath(w, h, t, adj) {
28824
+ const yBase = h * (0.15 + t * 0.7);
28825
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28826
+ const bulge = h * 0.12 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
28827
+ return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
28828
+ }
28829
+ /** Deflate-inflate — pinched in centre top/bottom, expanded at edges.
28830
+ * adj (default 18750) controls the oscillation amplitude. */
28831
+ function deflateInflatePath(w, h, t, adj) {
28832
+ const yBase = h * (0.15 + t * 0.7);
28833
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28834
+ const factor = Math.sin(t * Math.PI);
28835
+ const bulge = h * 0.2 * (factor - 0.5) * Math.max(0, Math.min(adjFactor, 4));
28836
+ return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
28837
+ }
28838
+ /** Deflate-inflate-deflate — triple oscillation.
28839
+ * adj (default 18750) controls the oscillation amplitude. */
28840
+ function deflateInflateDeflatePath(w, h, t, adj) {
28841
+ const yBase = h * (0.15 + t * 0.7);
28842
+ const adjFactor = adj !== undefined ? adj / 18750 : 1;
28843
+ const ampScale = Math.max(0, Math.min(adjFactor, 4));
28844
+ const factor = Math.sin(t * Math.PI * 2);
28845
+ const bulge = h * 0.15 * factor * ampScale;
28846
+ const q1 = w / 3;
28847
+ const q2 = (2 * w) / 3;
28848
+ return (`M 0,${yBase} ` +
28849
+ `Q ${q1},${yBase - bulge} ${w / 2},${yBase} ` +
28850
+ `Q ${q2},${yBase + bulge} ${w},${yBase}`);
28851
+ }
28852
+ // ── Generator look-up table ────────────────────────────────────────────
28853
+ const WARP_PATH_GENERATORS = {
28854
+ textArchUp: archUpPath,
28855
+ textArchDown: archDownPath,
28856
+ textCircle: circlePath,
28857
+ textWave1: wave1Path,
28858
+ textInflate: inflatePath,
28859
+ textDeflate: deflatePath,
28860
+ textCurveUp: curveUpPath,
28861
+ textCurveDown: curveDownPath,
28862
+ textWave2: wave2Path,
28863
+ textWave4: wave4Path,
28864
+ textDoubleWave1: doubleWave1Path,
28865
+ textCanUp: canUpPath,
28866
+ textCanDown: canDownPath,
28867
+ textButton: buttonPath,
28868
+ textRingInside: ringInsidePath,
28869
+ textRingOutside: ringOutsidePath,
28870
+ textCascadeUp: cascadeUpPath,
28871
+ textCascadeDown: cascadeDownPath,
28872
+ textTriangle: trianglePath,
28873
+ textTriangleInverted: triangleInvertedPath,
28874
+ textStop: stopPath,
28875
+ textChevron: chevronPath,
28876
+ textChevronInverted: chevronInvertedPath,
28877
+ textInflateBottom: inflateBottomPath,
28878
+ textInflateTop: inflateTopPath,
28879
+ textDeflateBottom: deflateBottomPath,
28880
+ textDeflateTop: deflateTopPath,
28881
+ textSlantUp: slantUpPath,
28882
+ textSlantDown: slantDownPath,
28883
+ textFadeRight: fadeRightPath,
28884
+ textFadeLeft: fadeLeftPath,
28885
+ textFadeUp: fadeUpPath,
28886
+ textFadeDown: fadeDownPath,
28887
+ textArchUpPour: archUpPourPath,
28888
+ textArchDownPour: archDownPourPath,
28889
+ textCirclePour: circlePourPath,
28890
+ textButtonPour: buttonPourPath,
28891
+ textDeflateInflate: deflateInflatePath,
28892
+ textDeflateInflateDeflate: deflateInflateDeflatePath,
28893
+ };
28894
+ // ── Public API ─────────────────────────────────────────────────────────
28895
+ /** Returns `true` when the preset should use SVG `<textPath>` rendering. */
28896
+ function shouldUseSvgWarp(preset) {
28897
+ if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
28898
+ return false;
28899
+ }
28900
+ return SVG_WARP_PRESETS.has(preset);
28901
+ }
28902
+ /** Generate an SVG path `d` attribute for a warp preset at a given line position.
28903
+ * Optional adj/adj2 are raw OOXML adjustment values (1/60000th units). */
28904
+ function getWarpPath(preset, width, height, lineIndex, lineCount, adj, adj2) {
28905
+ const t = lineCount <= 1 ? 0.5 : lineIndex / (lineCount - 1);
28906
+ const generator = WARP_PATH_GENERATORS[preset];
28907
+ if (generator) {
28908
+ return generator(width, height, t, adj, adj2);
28909
+ }
28910
+ const y = height * (0.2 + t * 0.6);
28911
+ return `M 0,${y} L ${width},${y}`;
28912
+ }
28913
+
28914
+ const NONE_PRESETS = new Set(['textNoShape', 'textPlain']);
28915
+ /** Presets that render best with SVG textPath along curved/circular paths. */
28916
+ const PATH_PRESETS = new Set([
28917
+ 'textArchUp',
28918
+ 'textArchDown',
28919
+ 'textCircle',
28920
+ 'textWave1',
28921
+ 'textWave2',
28922
+ 'textWave4',
28923
+ 'textDoubleWave1',
28924
+ 'textCurveUp',
28925
+ 'textCurveDown',
28926
+ 'textArchUpPour',
28927
+ 'textArchDownPour',
28928
+ 'textCirclePour',
28929
+ 'textButton',
28930
+ 'textButtonPour',
28931
+ 'textRingInside',
28932
+ 'textRingOutside',
28933
+ 'textTriangle',
28934
+ 'textTriangleInverted',
28935
+ 'textChevron',
28936
+ 'textChevronInverted',
28937
+ 'textStop',
28938
+ ]);
28939
+ /**
28940
+ * Envelope presets stretch text non-uniformly (wider/narrower per line).
28941
+ * CSS transforms approximate the visual effect per text block.
28942
+ */
28943
+ const ENVELOPE_PRESETS = new Set([
28944
+ 'textInflate',
28945
+ 'textDeflate',
28946
+ 'textInflateBottom',
28947
+ 'textInflateTop',
28948
+ 'textDeflateBottom',
28949
+ 'textDeflateTop',
28950
+ 'textDeflateInflate',
28951
+ 'textDeflateInflateDeflate',
28952
+ 'textCanUp',
28953
+ 'textCanDown',
28954
+ ]);
28955
+ /** Simple presets that work with basic 2D CSS transforms (skew, perspective). */
28956
+ const SIMPLE_PRESETS = new Set([
28957
+ 'textSlantUp',
28958
+ 'textSlantDown',
28959
+ 'textFadeRight',
28960
+ 'textFadeLeft',
28961
+ 'textFadeUp',
28962
+ 'textFadeDown',
28963
+ 'textCascadeUp',
28964
+ 'textCascadeDown',
28965
+ ]);
28966
+ /** All known classified presets (excludes `none`-family). */
28967
+ const ALL_CLASSIFIED_PRESETS = new Set([
28968
+ ...NONE_PRESETS,
28969
+ ...PATH_PRESETS,
28970
+ ...ENVELOPE_PRESETS,
28971
+ ...SIMPLE_PRESETS,
28972
+ ]);
28973
+ /**
28974
+ * Classify a warp preset into a rendering strategy category.
28975
+ *
28976
+ * Returns `'none'` for unknown or empty presets so callers can safely
28977
+ * skip rendering without an explicit allowlist check.
28978
+ */
28979
+ function getWarpCategory(preset) {
28980
+ if (!preset || NONE_PRESETS.has(preset)) {
28981
+ return 'none';
28982
+ }
28983
+ if (PATH_PRESETS.has(preset)) {
28984
+ return 'path';
28985
+ }
28986
+ if (ENVELOPE_PRESETS.has(preset)) {
28987
+ return 'envelope';
28988
+ }
28989
+ if (SIMPLE_PRESETS.has(preset)) {
28990
+ return 'simple';
28991
+ }
28992
+ return 'none';
28993
+ }
28994
+ /**
28995
+ * Split an element's `textSegments` into paragraphs.
28996
+ * Paragraph-break segments are used as delimiters and excluded from output.
28997
+ * Falls back to a single synthetic paragraph when only `element.text` is set.
28998
+ */
28999
+ function groupIntoParagraphs(el) {
29000
+ const segs = el.textSegments;
29001
+ if (!segs || segs.length === 0) {
29002
+ if (el.text) {
29003
+ return [{ segments: [{ text: el.text, style: {} }] }];
29004
+ }
29005
+ return [];
29006
+ }
29007
+ const paragraphs = [];
29008
+ let current = [];
29009
+ for (const seg of segs) {
29010
+ if (seg.isParagraphBreak) {
29011
+ if (current.length > 0) {
29012
+ paragraphs.push({ segments: current });
29013
+ }
29014
+ current = [];
29015
+ }
29016
+ else {
29017
+ current.push(seg);
29018
+ }
29019
+ }
29020
+ if (current.length > 0) {
29021
+ paragraphs.push({ segments: current });
29022
+ }
29023
+ return paragraphs;
29024
+ }
29025
+ // ── CSS transform generators ───────────────────────────────────────────
29026
+ // (mirrors text-warp-classifier.ts getEnvelopeCssTransform / getSimpleCssTransform)
29027
+ /** Default OOXML adjustment values for envelope presets (raw 1/60000th units). */
29028
+ const ENVELOPE_DEFAULTS = {
29029
+ textInflate: 18750,
29030
+ textDeflate: 18750,
29031
+ textInflateBottom: 18750,
29032
+ textInflateTop: 18750,
29033
+ textDeflateBottom: 18750,
29034
+ textDeflateTop: 18750,
29035
+ textDeflateInflate: 18750,
29036
+ textDeflateInflateDeflate: 18750,
29037
+ textCanUp: 18750,
29038
+ textCanDown: 18750,
29039
+ };
29040
+ /** Resolve CSS transform + origin for envelope-family presets. */
29041
+ function envelopeCss(preset, adj1) {
29042
+ const defaultAdj = ENVELOPE_DEFAULTS[preset] ?? 18750;
29043
+ const a1 = adj1 ?? defaultAdj;
29044
+ const intensity = Math.max(0, Math.min(a1 / 18750, 4));
29045
+ switch (preset) {
29046
+ case 'textInflate':
29047
+ return {
29048
+ transform: `scaleY(${1 + 0.15 * intensity}) scaleX(${1 + 0.05 * intensity})`,
29049
+ transformOrigin: 'center center',
29050
+ };
29051
+ case 'textInflateBottom':
29052
+ return {
29053
+ transform: `perspective(${600 - 100 * intensity}px) rotateX(${-8 * intensity}deg)`,
29054
+ transformOrigin: 'center bottom',
29055
+ };
29056
+ case 'textInflateTop':
29057
+ return {
29058
+ transform: `perspective(${600 - 100 * intensity}px) rotateX(${8 * intensity}deg)`,
29059
+ transformOrigin: 'center top',
29060
+ };
29061
+ case 'textDeflate':
29062
+ return {
29063
+ transform: `scaleY(${1 - 0.12 * intensity}) scaleX(${1 - 0.05 * intensity})`,
29064
+ transformOrigin: 'center center',
29065
+ };
29066
+ case 'textDeflateBottom':
29067
+ return {
29068
+ transform: `perspective(${600 - 100 * intensity}px) rotateX(${6 * intensity}deg)`,
29069
+ transformOrigin: 'center bottom',
29070
+ };
29071
+ case 'textDeflateTop':
29072
+ return {
29073
+ transform: `perspective(${600 - 100 * intensity}px) rotateX(${-6 * intensity}deg)`,
29074
+ transformOrigin: 'center top',
29075
+ };
29076
+ case 'textDeflateInflate':
29077
+ return {
29078
+ transform: `scaleY(${1 - 0.08 * intensity}) scaleX(${1 + 0.04 * intensity})`,
29079
+ transformOrigin: 'center center',
29080
+ };
29081
+ case 'textDeflateInflateDeflate':
29082
+ return {
29083
+ transform: `scaleY(${1 - 0.15 * intensity}) scaleX(${1 + 0.06 * intensity})`,
29084
+ transformOrigin: 'center center',
29085
+ };
29086
+ case 'textCanUp':
29087
+ return {
29088
+ transform: `perspective(${500 - 80 * intensity}px) rotateX(${-6 * intensity}deg)`,
29089
+ transformOrigin: 'center center',
29090
+ };
29091
+ case 'textCanDown':
29092
+ return {
29093
+ transform: `perspective(${500 - 80 * intensity}px) rotateX(${6 * intensity}deg)`,
29094
+ transformOrigin: 'center center',
29095
+ };
29096
+ default:
29097
+ return undefined;
29098
+ }
29099
+ }
29100
+ /** Default OOXML adjustment values for simple presets (raw 1/60000th units). */
29101
+ const SIMPLE_DEFAULTS = {
29102
+ textSlantUp: 55000,
29103
+ textSlantDown: 55000,
29104
+ textFadeRight: 50000,
29105
+ textFadeLeft: 50000,
29106
+ textFadeUp: 50000,
29107
+ textFadeDown: 50000,
29108
+ textCascadeUp: 44444,
29109
+ textCascadeDown: 44444,
29110
+ };
29111
+ /** Resolve CSS transform + origin for simple-family presets. */
29112
+ function simpleCss(preset, adj1) {
29113
+ const defaultAdj = SIMPLE_DEFAULTS[preset] ?? 50000;
29114
+ const a1 = adj1 ?? defaultAdj;
29115
+ switch (preset) {
29116
+ case 'textSlantUp': {
29117
+ const skew = -4 * (a1 / 55000);
29118
+ return {
29119
+ transform: `perspective(500px) rotateY(${8 * (a1 / 55000)}deg) skewY(${skew}deg)`,
29120
+ transformOrigin: 'left center',
29121
+ };
29122
+ }
29123
+ case 'textSlantDown': {
29124
+ const skew = 4 * (a1 / 55000);
29125
+ return {
29126
+ transform: `perspective(500px) rotateY(${-8 * (a1 / 55000)}deg) skewY(${skew}deg)`,
29127
+ transformOrigin: 'right center',
29128
+ };
29129
+ }
29130
+ case 'textFadeRight': {
29131
+ const angle = 10 * (a1 / 50000);
29132
+ return {
29133
+ transform: `perspective(400px) rotateY(${-angle}deg)`,
29134
+ transformOrigin: 'left center',
29135
+ };
29136
+ }
29137
+ case 'textFadeLeft': {
29138
+ const angle = 10 * (a1 / 50000);
29139
+ return {
29140
+ transform: `perspective(400px) rotateY(${angle}deg)`,
29141
+ transformOrigin: 'right center',
29142
+ };
29143
+ }
29144
+ case 'textFadeUp': {
29145
+ const angle = 10 * (a1 / 50000);
29146
+ return {
29147
+ transform: `perspective(400px) rotateX(${-angle}deg)`,
29148
+ transformOrigin: 'center bottom',
29149
+ };
29150
+ }
29151
+ case 'textFadeDown': {
29152
+ const angle = 10 * (a1 / 50000);
29153
+ return {
29154
+ transform: `perspective(400px) rotateX(${angle}deg)`,
29155
+ transformOrigin: 'center top',
29156
+ };
29157
+ }
29158
+ case 'textCascadeUp': {
29159
+ const skew = -8 * (a1 / 44444);
29160
+ return {
29161
+ transform: `skewY(${skew}deg)`,
29162
+ transformOrigin: 'left top',
29163
+ };
29164
+ }
29165
+ case 'textCascadeDown': {
29166
+ const skew = 8 * (a1 / 44444);
29167
+ return {
29168
+ transform: `skewY(${skew}deg)`,
29169
+ transformOrigin: 'left top',
29170
+ };
29171
+ }
29172
+ default:
29173
+ return undefined;
29174
+ }
29175
+ }
29176
+ // ── SVG alignment helpers ──────────────────────────────────────────────
29177
+ /** Map paragraph alignment to SVG textPath properties. */
29178
+ function resolveAlignment(align) {
29179
+ switch (align) {
29180
+ case 'center':
29181
+ return { startOffset: '50%', textAnchor: 'middle' };
29182
+ case 'right':
29183
+ return { startOffset: '100%', textAnchor: 'end' };
29184
+ default:
29185
+ return { startOffset: '0%', textAnchor: 'start' };
29186
+ }
29187
+ }
29188
+ // ── Public API ─────────────────────────────────────────────────────────
29189
+ const DEFAULT_FONT_SIZE = 18;
29190
+ const DEFAULT_FONT_FAMILY = 'Calibri, sans-serif';
29191
+ const DEFAULT_COLOR = '#000000';
29192
+ /**
29193
+ * Resolve a `PptxElement`'s text warp preset into a `TextWarpDef` descriptor,
29194
+ * or `undefined` when the element carries no warp (or the preset is `textNoShape` /
29195
+ * `textPlain` / unknown).
29196
+ *
29197
+ * @param element Any `PptxElement`. Elements without text properties always
29198
+ * return `undefined`.
29199
+ * @returns A `TextWarpDef` with `strategy: 'path'` for SVG textPath warps, or
29200
+ * `strategy: 'css'` for CSS-transform approximations.
29201
+ */
29202
+ function getTextWarp(element) {
29203
+ if (!hasTextProperties(element)) {
29204
+ return undefined;
29205
+ }
29206
+ const ts = element.textStyle;
29207
+ const preset = ts?.textWarpPreset;
29208
+ if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
29209
+ return undefined;
29210
+ }
29211
+ const adj1 = ts?.textWarpAdj;
29212
+ const adj2 = ts?.textWarpAdj2;
29213
+ // ── Strategy: SVG path ──────────────────────────────────────────────
29214
+ if (shouldUseSvgWarp(preset)) {
29215
+ const paragraphs = groupIntoParagraphs(element);
29216
+ if (paragraphs.length === 0) {
29217
+ return undefined;
29218
+ }
29219
+ const lineCount = paragraphs.length;
29220
+ const width = element.width;
29221
+ const height = element.height;
29222
+ const pathIdPrefix = `ng-warp-${element.id}`;
29223
+ const { startOffset, textAnchor } = resolveAlignment(ts?.align);
29224
+ const pathLines = paragraphs.map((para, i) => ({
29225
+ pathId: `${pathIdPrefix}-${i}`,
29226
+ d: getWarpPath(preset, width, height, i, lineCount, adj1, adj2),
29227
+ segments: para.segments,
29228
+ }));
29229
+ return {
29230
+ strategy: 'path',
29231
+ preset,
29232
+ pathLines,
29233
+ width,
29234
+ height,
29235
+ textAnchor,
29236
+ startOffset,
29237
+ baseFontSize: (ts?.fontSize ?? DEFAULT_FONT_SIZE),
29238
+ baseFontFamily: ts?.fontFamily ?? DEFAULT_FONT_FAMILY,
29239
+ baseColor: ts?.color ?? DEFAULT_COLOR,
29240
+ };
29241
+ }
29242
+ // ── Strategy: CSS transform ─────────────────────────────────────────
29243
+ const category = getWarpCategory(preset);
29244
+ let cssDef;
29245
+ if (category === 'envelope') {
29246
+ cssDef = envelopeCss(preset, adj1);
29247
+ }
29248
+ else if (category === 'simple') {
29249
+ cssDef = simpleCss(preset, adj1);
29250
+ }
29251
+ if (!cssDef) {
29252
+ return undefined;
29253
+ }
29254
+ return {
29255
+ strategy: 'css',
29256
+ preset,
29257
+ cssTransform: cssDef.transform,
29258
+ cssTransformOrigin: cssDef.transformOrigin,
29259
+ };
29260
+ }
29261
+
27656
29262
  /**
27657
29263
  * Build the complete zoom view-model for a given element.
27658
29264
  * Returns sensible defaults for non-zoom elements (all derived strings are
@@ -27856,6 +29462,25 @@ class ElementRendererComponent {
27856
29462
  ...(ngDevMode ? [{ debugName: "textStyle" }] : /* istanbul ignore next */ []));
27857
29463
  imageSrc = computed(() => getImageSrc(this.element(), this.mediaDataUrls()), /* @ts-ignore */
27858
29464
  ...(ngDevMode ? [{ debugName: "imageSrc" }] : /* istanbul ignore next */ []));
29465
+ /** Text-warp (WordArt) descriptor for the element, if any. */
29466
+ textWarp = computed(() => getTextWarp(this.element()), /* @ts-ignore */
29467
+ ...(ngDevMode ? [{ debugName: "textWarp" }] : /* istanbul ignore next */ []));
29468
+ /** Only the SVG-textPath warp variant (for the `<svg>` overlay branch). */
29469
+ pathWarp = computed(() => {
29470
+ const w = this.textWarp();
29471
+ return w?.strategy === 'path' ? w : undefined;
29472
+ }, /* @ts-ignore */
29473
+ ...(ngDevMode ? [{ debugName: "pathWarp" }] : /* istanbul ignore next */ []));
29474
+ /** Text block style, folding in a CSS-transform warp when present. */
29475
+ warpedTextStyle = computed(() => {
29476
+ const base = this.textStyle();
29477
+ const w = this.textWarp();
29478
+ if (w?.strategy === 'css') {
29479
+ return { ...base, transform: w.cssTransform, 'transform-origin': w.cssTransformOrigin };
29480
+ }
29481
+ return base;
29482
+ }, /* @ts-ignore */
29483
+ ...(ngDevMode ? [{ debugName: "warpedTextStyle" }] : /* istanbul ignore next */ []));
27859
29484
  children = computed(() => {
27860
29485
  const el = this.element();
27861
29486
  return el.type === 'group' ? (el.children ?? []) : [];
@@ -28077,8 +29702,45 @@ class ElementRendererComponent {
28077
29702
  [ngStyle]="shapeContainerStyle()"
28078
29703
  [attr.data-element-id]="element().id"
28079
29704
  >
28080
- @if (hasText()) {
28081
- <div class="pptx-ng-text" [ngStyle]="textStyle()">
29705
+ @if (pathWarp(); as warp) {
29706
+ <svg
29707
+ [attr.width]="warp.width"
29708
+ [attr.height]="warp.height"
29709
+ [attr.viewBox]="'0 0 ' + warp.width + ' ' + warp.height"
29710
+ style="position: absolute; inset: 0; overflow: visible; pointer-events: none"
29711
+ aria-hidden="true"
29712
+ >
29713
+ <defs>
29714
+ @for (line of warp.pathLines; track line.pathId) {
29715
+ <path [attr.id]="line.pathId" [attr.d]="line.d" fill="none" />
29716
+ }
29717
+ </defs>
29718
+ @for (line of warp.pathLines; track line.pathId) {
29719
+ <text
29720
+ [attr.font-size]="warp.baseFontSize"
29721
+ [attr.font-family]="warp.baseFontFamily"
29722
+ [attr.fill]="warp.baseColor"
29723
+ >
29724
+ <textPath
29725
+ [attr.href]="'#' + line.pathId"
29726
+ [attr.startOffset]="warp.startOffset"
29727
+ [attr.text-anchor]="warp.textAnchor"
29728
+ >
29729
+ @for (seg of line.segments; track $index) {
29730
+ <tspan
29731
+ [attr.fill]="seg.style?.color ?? warp.baseColor"
29732
+ [attr.font-weight]="seg.style?.bold ? 700 : 400"
29733
+ [attr.font-style]="seg.style?.italic ? 'italic' : 'normal'"
29734
+ >
29735
+ {{ seg.text }}
29736
+ </tspan>
29737
+ }
29738
+ </textPath>
29739
+ </text>
29740
+ }
29741
+ </svg>
29742
+ } @else if (hasText()) {
29743
+ <div class="pptx-ng-text" [ngStyle]="warpedTextStyle()">
28082
29744
  @for (para of paragraphs(); track $index) {
28083
29745
  <p class="pptx-ng-para" [style.padding-left.px]="para.indentPx">
28084
29746
  @if (para.bulletMarker) {
@@ -28294,8 +29956,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
28294
29956
  [ngStyle]="shapeContainerStyle()"
28295
29957
  [attr.data-element-id]="element().id"
28296
29958
  >
28297
- @if (hasText()) {
28298
- <div class="pptx-ng-text" [ngStyle]="textStyle()">
29959
+ @if (pathWarp(); as warp) {
29960
+ <svg
29961
+ [attr.width]="warp.width"
29962
+ [attr.height]="warp.height"
29963
+ [attr.viewBox]="'0 0 ' + warp.width + ' ' + warp.height"
29964
+ style="position: absolute; inset: 0; overflow: visible; pointer-events: none"
29965
+ aria-hidden="true"
29966
+ >
29967
+ <defs>
29968
+ @for (line of warp.pathLines; track line.pathId) {
29969
+ <path [attr.id]="line.pathId" [attr.d]="line.d" fill="none" />
29970
+ }
29971
+ </defs>
29972
+ @for (line of warp.pathLines; track line.pathId) {
29973
+ <text
29974
+ [attr.font-size]="warp.baseFontSize"
29975
+ [attr.font-family]="warp.baseFontFamily"
29976
+ [attr.fill]="warp.baseColor"
29977
+ >
29978
+ <textPath
29979
+ [attr.href]="'#' + line.pathId"
29980
+ [attr.startOffset]="warp.startOffset"
29981
+ [attr.text-anchor]="warp.textAnchor"
29982
+ >
29983
+ @for (seg of line.segments; track $index) {
29984
+ <tspan
29985
+ [attr.fill]="seg.style?.color ?? warp.baseColor"
29986
+ [attr.font-weight]="seg.style?.bold ? 700 : 400"
29987
+ [attr.font-style]="seg.style?.italic ? 'italic' : 'normal'"
29988
+ >
29989
+ {{ seg.text }}
29990
+ </tspan>
29991
+ }
29992
+ </textPath>
29993
+ </text>
29994
+ }
29995
+ </svg>
29996
+ } @else if (hasText()) {
29997
+ <div class="pptx-ng-text" [ngStyle]="warpedTextStyle()">
28299
29998
  @for (para of paragraphs(); track $index) {
28300
29999
  <p class="pptx-ng-para" [style.padding-left.px]="para.indentPx">
28301
30000
  @if (para.bulletMarker) {
@@ -29777,6 +31476,779 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
29777
31476
  type: Injectable
29778
31477
  }], ctorParameters: () => [] });
29779
31478
 
31479
+ /**
31480
+ * presentation-annotations-helpers.ts — Pure geometry helpers for presentation
31481
+ * ink annotations (pen, highlighter, eraser, laser).
31482
+ *
31483
+ * Ported from React:
31484
+ * packages/react/src/viewer/components/PresentationAnnotationOverlay.tsx
31485
+ * packages/react/src/viewer/hooks/usePresentationAnnotations.ts
31486
+ *
31487
+ * No Angular dependencies — all functions are pure so they can be unit-tested
31488
+ * without TestBed.
31489
+ */
31490
+ // ---------------------------------------------------------------------------
31491
+ // Constants
31492
+ // ---------------------------------------------------------------------------
31493
+ const PEN_WIDTH = 2.5;
31494
+ const HIGHLIGHTER_WIDTH = 14;
31495
+ const HIGHLIGHTER_OPACITY = 0.4;
31496
+ const ERASER_RADIUS = 16;
31497
+ // ---------------------------------------------------------------------------
31498
+ // Stroke id generation
31499
+ // ---------------------------------------------------------------------------
31500
+ let strokeIdCounter = 0;
31501
+ /**
31502
+ * Generate a monotonically-increasing stroke id.
31503
+ * Module-level counter is fine: ids only need to be unique within a session.
31504
+ */
31505
+ function nextStrokeId() {
31506
+ strokeIdCounter += 1;
31507
+ return `stroke-${strokeIdCounter}`;
31508
+ }
31509
+ /**
31510
+ * Reset the stroke-id counter. Exposed for tests only — never call from
31511
+ * production code.
31512
+ */
31513
+ function resetStrokeIdCounter() {
31514
+ strokeIdCounter = 0;
31515
+ }
31516
+ // ---------------------------------------------------------------------------
31517
+ // SVG path generation
31518
+ // ---------------------------------------------------------------------------
31519
+ /**
31520
+ * Convert a sequence of points to an SVG path `d` attribute string using
31521
+ * M + L commands (no bezier smoothing — matches the React original).
31522
+ *
31523
+ * Returns an empty string for an empty point array.
31524
+ *
31525
+ * @example
31526
+ * buildPathD([{x:0,y:0},{x:10,y:5}]) // "M 0 0 L 10 5"
31527
+ */
31528
+ function buildPathD(points) {
31529
+ if (points.length === 0) {
31530
+ return '';
31531
+ }
31532
+ const first = points[0];
31533
+ let d = `M ${first.x} ${first.y}`;
31534
+ for (let i = 1; i < points.length; i++) {
31535
+ const pt = points[i];
31536
+ d += ` L ${pt.x} ${pt.y}`;
31537
+ }
31538
+ return d;
31539
+ }
31540
+ // ---------------------------------------------------------------------------
31541
+ // Point smoothing
31542
+ // ---------------------------------------------------------------------------
31543
+ /**
31544
+ * Apply a simple sliding-window average to a series of points, reducing
31545
+ * jitter without imposing bezier curve complexity.
31546
+ *
31547
+ * A window size of 1 is a no-op (identity). The first and last points are
31548
+ * always preserved as-is.
31549
+ *
31550
+ * @param points Raw pointer-event coordinates.
31551
+ * @param window Number of neighbours on each side to average (default 2).
31552
+ */
31553
+ function smoothPoints(points, window = 2) {
31554
+ const n = points.length;
31555
+ if (n <= 2 || window <= 0) {
31556
+ return points;
31557
+ }
31558
+ const out = [];
31559
+ for (let i = 0; i < n; i++) {
31560
+ if (i === 0 || i === n - 1) {
31561
+ out.push(points[i]);
31562
+ continue;
31563
+ }
31564
+ const lo = Math.max(0, i - window);
31565
+ const hi = Math.min(n - 1, i + window);
31566
+ let sx = 0;
31567
+ let sy = 0;
31568
+ const count = hi - lo + 1;
31569
+ for (let j = lo; j <= hi; j++) {
31570
+ sx += points[j].x;
31571
+ sy += points[j].y;
31572
+ }
31573
+ out.push({ x: sx / count, y: sy / count });
31574
+ }
31575
+ return out;
31576
+ }
31577
+ // ---------------------------------------------------------------------------
31578
+ // Eraser hit-testing
31579
+ // ---------------------------------------------------------------------------
31580
+ /**
31581
+ * Return `true` when any point on `stroke` lies within `radius` pixels of the
31582
+ * eraser centre `(ex, ey)` in slide space.
31583
+ *
31584
+ * Uses squared-distance comparison to avoid Math.sqrt.
31585
+ */
31586
+ function strokeHitsEraser(stroke, ex, ey, radius) {
31587
+ const r2 = radius * radius;
31588
+ for (const pt of stroke.points) {
31589
+ const dx = pt.x - ex;
31590
+ const dy = pt.y - ey;
31591
+ if (dx * dx + dy * dy < r2) {
31592
+ return true;
31593
+ }
31594
+ }
31595
+ return false;
31596
+ }
31597
+ /**
31598
+ * Filter `strokes`, removing any that hit the eraser at `(ex, ey)`.
31599
+ * Returns a new array; does not mutate the input.
31600
+ */
31601
+ function eraseAtPoint(strokes, ex, ey, radius = ERASER_RADIUS) {
31602
+ return strokes.filter((s) => !strokeHitsEraser(s, ex, ey, radius));
31603
+ }
31604
+ // ---------------------------------------------------------------------------
31605
+ // Laser dot fade
31606
+ // ---------------------------------------------------------------------------
31607
+ /**
31608
+ * Compute the CSS `opacity` value for a fading laser dot given a fade ratio
31609
+ * in [0, 1] where 0 = fully visible and 1 = fully invisible.
31610
+ *
31611
+ * Uses a simple quadratic ease-out so the dot lingers at full intensity then
31612
+ * fades quickly near the end.
31613
+ *
31614
+ * @param ratio 0 (fresh) → 1 (expired)
31615
+ */
31616
+ function laserDotOpacity(ratio) {
31617
+ const clamped = Math.max(0, Math.min(1, ratio));
31618
+ return (1 - clamped) * (1 - clamped);
31619
+ }
31620
+ // ---------------------------------------------------------------------------
31621
+ // Cursor helper
31622
+ // ---------------------------------------------------------------------------
31623
+ /**
31624
+ * Return the CSS `cursor` value that matches `tool`.
31625
+ */
31626
+ function cursorForTool(tool) {
31627
+ switch (tool) {
31628
+ case 'laser':
31629
+ return 'none';
31630
+ case 'pen':
31631
+ return 'crosshair';
31632
+ case 'highlighter':
31633
+ return 'crosshair';
31634
+ case 'eraser':
31635
+ return 'crosshair';
31636
+ default:
31637
+ return 'default';
31638
+ }
31639
+ }
31640
+
31641
+ /**
31642
+ * presentation-annotations.service.ts — Signal-based service that manages ink
31643
+ * annotation state during presentation mode.
31644
+ *
31645
+ * Ported from React:
31646
+ * packages/react/src/viewer/hooks/usePresentationAnnotations.ts
31647
+ * packages/react/src/viewer/hooks/usePresentationAnnotations.types.ts
31648
+ *
31649
+ * Provides: pen / highlighter / eraser / laser-pointer tools, per-slide stroke
31650
+ * storage keyed by slide index, begin / extend / end stroke lifecycle, undo
31651
+ * (single-step: clear current slide), clear-all, and a transient laser position.
31652
+ *
31653
+ * Provide at the component level so lifetime tracks the host overlay:
31654
+ * `@Component({ providers: [PresentationAnnotationsService] })`.
31655
+ */
31656
+ class PresentationAnnotationsService {
31657
+ destroyRef = inject(DestroyRef);
31658
+ // ------------------------------------------------------------------
31659
+ // Private signals
31660
+ // ------------------------------------------------------------------
31661
+ _tool = signal('none', /* @ts-ignore */
31662
+ ...(ngDevMode ? [{ debugName: "_tool" }] : /* istanbul ignore next */ []));
31663
+ _penColor = signal('#ff0000', /* @ts-ignore */
31664
+ ...(ngDevMode ? [{ debugName: "_penColor" }] : /* istanbul ignore next */ []));
31665
+ _highlighterColor = signal('#ffff00', /* @ts-ignore */
31666
+ ...(ngDevMode ? [{ debugName: "_highlighterColor" }] : /* istanbul ignore next */ []));
31667
+ _currentStroke = signal(null, /* @ts-ignore */
31668
+ ...(ngDevMode ? [{ debugName: "_currentStroke" }] : /* istanbul ignore next */ []));
31669
+ _laserPosition = signal(null, /* @ts-ignore */
31670
+ ...(ngDevMode ? [{ debugName: "_laserPosition" }] : /* istanbul ignore next */ []));
31671
+ _toolbarVisible = signal(false, /* @ts-ignore */
31672
+ ...(ngDevMode ? [{ debugName: "_toolbarVisible" }] : /* istanbul ignore next */ []));
31673
+ /**
31674
+ * Strokes visible on the current slide. Cleared / populated whenever
31675
+ * `setActiveSlide()` is called.
31676
+ */
31677
+ _annotationStrokes = signal([], /* @ts-ignore */
31678
+ ...(ngDevMode ? [{ debugName: "_annotationStrokes" }] : /* istanbul ignore next */ []));
31679
+ /**
31680
+ * Per-slide annotation storage. Strokes for the current slide are also kept
31681
+ * in `_annotationStrokes` for rendering convenience; they are synchronised
31682
+ * back into this map on `setActiveSlide()` or `endStroke()`.
31683
+ */
31684
+ _slideAnnotations = new Map();
31685
+ /** Tracks whether a pointer-down initiated an erase gesture. */
31686
+ _isErasing = false;
31687
+ /** Toolbar auto-hide timer handle. */
31688
+ _toolbarTimer = null;
31689
+ /** Index of the slide currently being annotated. */
31690
+ _activeSlideIndex = 0;
31691
+ // ------------------------------------------------------------------
31692
+ // Public read-only signal surface
31693
+ // ------------------------------------------------------------------
31694
+ /** The currently-armed presentation tool. */
31695
+ tool = computed(() => this._tool(), /* @ts-ignore */
31696
+ ...(ngDevMode ? [{ debugName: "tool" }] : /* istanbul ignore next */ []));
31697
+ /** Stroke colour used by the pen tool. */
31698
+ penColor = computed(() => this._penColor(), /* @ts-ignore */
31699
+ ...(ngDevMode ? [{ debugName: "penColor" }] : /* istanbul ignore next */ []));
31700
+ /** Stroke colour used by the highlighter tool. */
31701
+ highlighterColor = computed(() => this._highlighterColor(), /* @ts-ignore */
31702
+ ...(ngDevMode ? [{ debugName: "highlighterColor" }] : /* istanbul ignore next */ []));
31703
+ /** Committed strokes on the active slide (does not include the live stroke). */
31704
+ annotationStrokes = computed(() => this._annotationStrokes(), /* @ts-ignore */
31705
+ ...(ngDevMode ? [{ debugName: "annotationStrokes" }] : /* istanbul ignore next */ []));
31706
+ /** The stroke currently being drawn, or null when not drawing. */
31707
+ currentStroke = computed(() => this._currentStroke(), /* @ts-ignore */
31708
+ ...(ngDevMode ? [{ debugName: "currentStroke" }] : /* istanbul ignore next */ []));
31709
+ /** Laser pointer position in slide-space pixels, or null when not visible. */
31710
+ laserPosition = computed(() => this._laserPosition(), /* @ts-ignore */
31711
+ ...(ngDevMode ? [{ debugName: "laserPosition" }] : /* istanbul ignore next */ []));
31712
+ /** Whether the annotation toolbar should be visible. */
31713
+ toolbarVisible = computed(() => this._toolbarVisible(), /* @ts-ignore */
31714
+ ...(ngDevMode ? [{ debugName: "toolbarVisible" }] : /* istanbul ignore next */ []));
31715
+ /**
31716
+ * `true` when any annotations exist on any slide (used for the
31717
+ * "keep / discard" dialog when exiting presentation mode).
31718
+ */
31719
+ hasAnyAnnotations = computed(() => this._annotationStrokes().length > 0 || this._slideAnnotations.size > 0, /* @ts-ignore */
31720
+ ...(ngDevMode ? [{ debugName: "hasAnyAnnotations" }] : /* istanbul ignore next */ []));
31721
+ // ------------------------------------------------------------------
31722
+ // Lifecycle
31723
+ // ------------------------------------------------------------------
31724
+ constructor() {
31725
+ this.destroyRef.onDestroy(() => {
31726
+ this._clearToolbarTimer();
31727
+ });
31728
+ }
31729
+ // ------------------------------------------------------------------
31730
+ // Tool management
31731
+ // ------------------------------------------------------------------
31732
+ /**
31733
+ * Arm `tool`. If it is already armed, toggle back to `'none'` (matches
31734
+ * the React behaviour of `setPresentationTool`).
31735
+ */
31736
+ setTool(tool) {
31737
+ this._tool.update((current) => (current === tool ? 'none' : tool));
31738
+ // Clear transient state when switching away from drawing tools.
31739
+ if (this._tool() === 'none' || this._tool() === 'laser') {
31740
+ this._currentStroke.set(null);
31741
+ this._isErasing = false;
31742
+ }
31743
+ if (this._tool() !== 'laser') {
31744
+ this._laserPosition.set(null);
31745
+ }
31746
+ }
31747
+ /** Set the pen colour directly (without toggling the tool). */
31748
+ setPenColor(color) {
31749
+ this._penColor.set(color);
31750
+ }
31751
+ /** Set the highlighter colour directly. */
31752
+ setHighlighterColor(color) {
31753
+ this._highlighterColor.set(color);
31754
+ }
31755
+ /** Show / hide the toolbar programmatically. */
31756
+ setToolbarVisible(visible) {
31757
+ this._toolbarVisible.set(visible);
31758
+ }
31759
+ // ------------------------------------------------------------------
31760
+ // Active slide management
31761
+ // ------------------------------------------------------------------
31762
+ /**
31763
+ * Notify the service that the visible slide has changed. Saves the
31764
+ * in-progress strokes for the previous slide and loads the strokes
31765
+ * for `newIndex`.
31766
+ */
31767
+ setActiveSlide(newIndex) {
31768
+ if (newIndex === this._activeSlideIndex) {
31769
+ return;
31770
+ }
31771
+ this._flushCurrentSlide();
31772
+ this._activeSlideIndex = newIndex;
31773
+ const existing = this._slideAnnotations.get(newIndex) ?? [];
31774
+ this._annotationStrokes.set([...existing]);
31775
+ this._currentStroke.set(null);
31776
+ this._isErasing = false;
31777
+ }
31778
+ // ------------------------------------------------------------------
31779
+ // Drawing
31780
+ // ------------------------------------------------------------------
31781
+ /**
31782
+ * Begin a stroke at `(x, y)` in slide-space coordinates.
31783
+ * Called on pointer-down when the pen or highlighter is armed.
31784
+ */
31785
+ beginStroke(x, y) {
31786
+ const tool = this._tool();
31787
+ if (tool !== 'pen' && tool !== 'highlighter') {
31788
+ return;
31789
+ }
31790
+ const isPen = tool === 'pen';
31791
+ this._currentStroke.set({
31792
+ id: nextStrokeId(),
31793
+ points: [{ x, y }],
31794
+ color: isPen ? this._penColor() : this._highlighterColor(),
31795
+ width: isPen ? PEN_WIDTH : HIGHLIGHTER_WIDTH,
31796
+ opacity: isPen ? 1 : HIGHLIGHTER_OPACITY,
31797
+ });
31798
+ }
31799
+ /**
31800
+ * Extend the active stroke by appending `(x, y)`.
31801
+ * Called on pointer-move while drawing.
31802
+ */
31803
+ extendStroke(x, y) {
31804
+ this._currentStroke.update((prev) => {
31805
+ if (!prev) {
31806
+ return null;
31807
+ }
31808
+ return { ...prev, points: [...prev.points, { x, y }] };
31809
+ });
31810
+ }
31811
+ /**
31812
+ * Commit the active stroke to the slide's annotation list.
31813
+ * Called on pointer-up or pointer-leave while drawing.
31814
+ * Strokes with fewer than 2 points are discarded.
31815
+ */
31816
+ endStroke() {
31817
+ const stroke = this._currentStroke();
31818
+ this._currentStroke.set(null);
31819
+ if (!stroke || stroke.points.length < 2) {
31820
+ return;
31821
+ }
31822
+ this._annotationStrokes.update((prev) => {
31823
+ // Guard against duplicate ids (shouldn't happen, but matches React safety).
31824
+ if (prev.some((s) => s.id === stroke.id)) {
31825
+ return prev;
31826
+ }
31827
+ const updated = [...prev, stroke];
31828
+ this._slideAnnotations.set(this._activeSlideIndex, updated);
31829
+ return updated;
31830
+ });
31831
+ }
31832
+ // ------------------------------------------------------------------
31833
+ // Eraser
31834
+ // ------------------------------------------------------------------
31835
+ /**
31836
+ * Begin an eraser gesture at `(x, y)`.
31837
+ * Must be called on pointer-down when the eraser is armed.
31838
+ */
31839
+ beginErase(x, y) {
31840
+ if (this._tool() !== 'eraser') {
31841
+ return;
31842
+ }
31843
+ this._isErasing = true;
31844
+ this._applyErase(x, y);
31845
+ }
31846
+ /**
31847
+ * Continue erasing at `(x, y)` during a pointer-move.
31848
+ */
31849
+ continueErase(x, y) {
31850
+ if (!this._isErasing || this._tool() !== 'eraser') {
31851
+ return;
31852
+ }
31853
+ this._applyErase(x, y);
31854
+ }
31855
+ /**
31856
+ * End an eraser gesture. Called on pointer-up or pointer-leave.
31857
+ */
31858
+ endErase() {
31859
+ this._isErasing = false;
31860
+ }
31861
+ // ------------------------------------------------------------------
31862
+ // Laser
31863
+ // ------------------------------------------------------------------
31864
+ /**
31865
+ * Update the laser dot position (slide-space coords).
31866
+ * Only takes effect when the laser tool is armed.
31867
+ */
31868
+ moveLaser(x, y) {
31869
+ if (this._tool() !== 'laser') {
31870
+ return;
31871
+ }
31872
+ this._laserPosition.set({ x, y });
31873
+ }
31874
+ /**
31875
+ * Hide the laser dot (called on pointer-leave).
31876
+ */
31877
+ hideLaser() {
31878
+ this._laserPosition.set(null);
31879
+ }
31880
+ // ------------------------------------------------------------------
31881
+ // Clear / undo
31882
+ // ------------------------------------------------------------------
31883
+ /**
31884
+ * Clear all annotations on the active slide.
31885
+ */
31886
+ clearAnnotations() {
31887
+ this._annotationStrokes.set([]);
31888
+ this._currentStroke.set(null);
31889
+ this._isErasing = false;
31890
+ this._slideAnnotations.delete(this._activeSlideIndex);
31891
+ }
31892
+ /**
31893
+ * Clear all annotations across every slide.
31894
+ */
31895
+ clearAllAnnotations() {
31896
+ this._annotationStrokes.set([]);
31897
+ this._currentStroke.set(null);
31898
+ this._isErasing = false;
31899
+ this._slideAnnotations.clear();
31900
+ }
31901
+ // ------------------------------------------------------------------
31902
+ // Snapshot
31903
+ // ------------------------------------------------------------------
31904
+ /**
31905
+ * Return a snapshot of all annotations across every slide.
31906
+ * The current slide's in-progress committed strokes are folded in.
31907
+ *
31908
+ * Used by the "keep as ink elements" dialog handler.
31909
+ */
31910
+ getAllSlideAnnotations() {
31911
+ this._flushCurrentSlide();
31912
+ return new Map(this._slideAnnotations);
31913
+ }
31914
+ // ------------------------------------------------------------------
31915
+ // Toolbar auto-hide
31916
+ // ------------------------------------------------------------------
31917
+ /**
31918
+ * Show the toolbar and schedule it to auto-hide after `delayMs` (default 3 s).
31919
+ * Resets the timer if called repeatedly (debounce on mouse-move).
31920
+ */
31921
+ showToolbarTemporarily(delayMs = 3000) {
31922
+ this._toolbarVisible.set(true);
31923
+ this._clearToolbarTimer();
31924
+ this._toolbarTimer = setTimeout(() => {
31925
+ this._toolbarVisible.set(false);
31926
+ this._toolbarTimer = null;
31927
+ }, delayMs);
31928
+ }
31929
+ // ------------------------------------------------------------------
31930
+ // Reset on exit
31931
+ // ------------------------------------------------------------------
31932
+ /**
31933
+ * Reset transient presentation state (tool, current stroke, laser, toolbar
31934
+ * timer). Does NOT clear stored strokes — those persist for the
31935
+ * keep/discard dialog.
31936
+ */
31937
+ resetForExit() {
31938
+ this._tool.set('none');
31939
+ this._currentStroke.set(null);
31940
+ this._laserPosition.set(null);
31941
+ this._isErasing = false;
31942
+ this._toolbarVisible.set(false);
31943
+ this._clearToolbarTimer();
31944
+ }
31945
+ // ------------------------------------------------------------------
31946
+ // Private helpers
31947
+ // ------------------------------------------------------------------
31948
+ _applyErase(x, y) {
31949
+ this._annotationStrokes.update((prev) => {
31950
+ const next = eraseAtPoint(prev, x, y, ERASER_RADIUS);
31951
+ if (next.length > 0) {
31952
+ this._slideAnnotations.set(this._activeSlideIndex, next);
31953
+ }
31954
+ else {
31955
+ this._slideAnnotations.delete(this._activeSlideIndex);
31956
+ }
31957
+ return next;
31958
+ });
31959
+ }
31960
+ _flushCurrentSlide() {
31961
+ const strokes = this._annotationStrokes();
31962
+ if (strokes.length > 0) {
31963
+ this._slideAnnotations.set(this._activeSlideIndex, [...strokes]);
31964
+ }
31965
+ else {
31966
+ this._slideAnnotations.delete(this._activeSlideIndex);
31967
+ }
31968
+ }
31969
+ _clearToolbarTimer() {
31970
+ if (this._toolbarTimer !== null) {
31971
+ clearTimeout(this._toolbarTimer);
31972
+ this._toolbarTimer = null;
31973
+ }
31974
+ }
31975
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
31976
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService });
31977
+ }
31978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService, decorators: [{
31979
+ type: Injectable
31980
+ }], ctorParameters: () => [] });
31981
+
31982
+ /**
31983
+ * presentation-annotation-overlay.component.ts — SVG overlay that captures
31984
+ * pointer events and renders ink strokes + laser dot during presentation mode.
31985
+ *
31986
+ * Ported from React:
31987
+ * packages/react/src/viewer/components/PresentationAnnotationOverlay.tsx
31988
+ *
31989
+ * Selector: `pptx-presentation-annotation-overlay`
31990
+ *
31991
+ * This component is purely presentational: it calls out to
31992
+ * {@link PresentationAnnotationsService} for all state mutations, and
31993
+ * accepts `canvasSize` and `zoom` to correctly map pointer events from
31994
+ * screen space into slide space.
31995
+ *
31996
+ * When the tool is `'none'` the component renders nothing and sets
31997
+ * `pointer-events: none` on its host so click-to-advance still works.
31998
+ */
31999
+ class PresentationAnnotationOverlayComponent {
32000
+ // ------------------------------------------------------------------
32001
+ // Inputs
32002
+ // ------------------------------------------------------------------
32003
+ /** Logical canvas dimensions (the slide's authored size in pixels). */
32004
+ canvasSize = input.required(/* @ts-ignore */
32005
+ ...(ngDevMode ? [{ debugName: "canvasSize" }] : /* istanbul ignore next */ []));
32006
+ /**
32007
+ * The zoom factor currently applied to the slide stage.
32008
+ * Pointer-event coordinates are divided by this value to obtain
32009
+ * slide-space coordinates.
32010
+ */
32011
+ zoom = input(1, /* @ts-ignore */
32012
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
32013
+ // ------------------------------------------------------------------
32014
+ // Injected state
32015
+ // ------------------------------------------------------------------
32016
+ service = inject(PresentationAnnotationsService);
32017
+ // ------------------------------------------------------------------
32018
+ // Template references
32019
+ // ------------------------------------------------------------------
32020
+ svgRef = viewChild('svg', /* @ts-ignore */
32021
+ ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
32022
+ // ------------------------------------------------------------------
32023
+ // Eraser bookkeeping
32024
+ // ------------------------------------------------------------------
32025
+ /** True while a pointer-down is active in eraser mode. */
32026
+ _isErasing = false;
32027
+ // ------------------------------------------------------------------
32028
+ // Derived signals
32029
+ // ------------------------------------------------------------------
32030
+ /** True when any tool other than 'none' is armed. */
32031
+ isArmed = computed(() => this.service.tool() !== 'none', /* @ts-ignore */
32032
+ ...(ngDevMode ? [{ debugName: "isArmed" }] : /* istanbul ignore next */ []));
32033
+ /** SVG viewBox string that covers the full canvas. */
32034
+ viewBox = computed(() => {
32035
+ const { width, height } = this.canvasSize();
32036
+ return `0 0 ${width} ${height}`;
32037
+ }, /* @ts-ignore */
32038
+ ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
32039
+ /** CSS cursor for the outer wrapper div. */
32040
+ wrapperStyle = computed(() => ({
32041
+ cursor: cursorForTool(this.service.tool()),
32042
+ 'pointer-events': this.isArmed() ? 'auto' : 'none',
32043
+ 'z-index': '60',
32044
+ }), /* @ts-ignore */
32045
+ ...(ngDevMode ? [{ debugName: "wrapperStyle" }] : /* istanbul ignore next */ []));
32046
+ /** Transform the SVG to match the slide's zoom level. */
32047
+ svgStyle = computed(() => ({
32048
+ transform: `scale(${this.zoom()})`,
32049
+ }), /* @ts-ignore */
32050
+ ...(ngDevMode ? [{ debugName: "svgStyle" }] : /* istanbul ignore next */ []));
32051
+ /** All strokes to render: committed + the live in-progress stroke. */
32052
+ allStrokes = computed(() => {
32053
+ const committed = this.service.annotationStrokes();
32054
+ const live = this.service.currentStroke();
32055
+ return live ? [...committed, live] : committed;
32056
+ }, /* @ts-ignore */
32057
+ ...(ngDevMode ? [{ debugName: "allStrokes" }] : /* istanbul ignore next */ []));
32058
+ // ------------------------------------------------------------------
32059
+ // Template helpers
32060
+ // ------------------------------------------------------------------
32061
+ strokePath(points) {
32062
+ if (points.length === 0) {
32063
+ return '';
32064
+ }
32065
+ const first = points[0];
32066
+ let d = `M ${first.x} ${first.y}`;
32067
+ for (let i = 1; i < points.length; i++) {
32068
+ const pt = points[i];
32069
+ d += ` L ${pt.x} ${pt.y}`;
32070
+ }
32071
+ return d;
32072
+ }
32073
+ laserDotStyle(x, y) {
32074
+ const z = this.zoom();
32075
+ const dotSize = 24;
32076
+ return {
32077
+ width: `${dotSize}px`,
32078
+ height: `${dotSize}px`,
32079
+ left: `${x * z - dotSize / 2}px`,
32080
+ top: `${y * z - dotSize / 2}px`,
32081
+ 'z-index': '70',
32082
+ };
32083
+ }
32084
+ // ------------------------------------------------------------------
32085
+ // Pointer event dispatch
32086
+ // ------------------------------------------------------------------
32087
+ onPointerDown(event) {
32088
+ const tool = this.service.tool();
32089
+ if (tool === 'none') {
32090
+ return;
32091
+ }
32092
+ event.preventDefault();
32093
+ event.stopPropagation();
32094
+ const coords = this._toSlideCoords(event.clientX, event.clientY);
32095
+ if (!coords) {
32096
+ return;
32097
+ }
32098
+ if (tool === 'eraser') {
32099
+ this._isErasing = true;
32100
+ this.service.beginErase(coords.x, coords.y);
32101
+ return;
32102
+ }
32103
+ if (tool === 'pen' || tool === 'highlighter') {
32104
+ this.service.beginStroke(coords.x, coords.y);
32105
+ }
32106
+ }
32107
+ onPointerMove(event) {
32108
+ const tool = this.service.tool();
32109
+ if (tool === 'none') {
32110
+ return;
32111
+ }
32112
+ const coords = this._toSlideCoords(event.clientX, event.clientY);
32113
+ if (!coords) {
32114
+ return;
32115
+ }
32116
+ if (tool === 'laser') {
32117
+ this.service.moveLaser(coords.x, coords.y);
32118
+ return;
32119
+ }
32120
+ if (tool === 'eraser' && this._isErasing) {
32121
+ this.service.continueErase(coords.x, coords.y);
32122
+ return;
32123
+ }
32124
+ if (tool === 'pen' || tool === 'highlighter') {
32125
+ this.service.extendStroke(coords.x, coords.y);
32126
+ }
32127
+ }
32128
+ onPointerUp(event) {
32129
+ const tool = this.service.tool();
32130
+ if (tool === 'none') {
32131
+ return;
32132
+ }
32133
+ event.preventDefault();
32134
+ if (tool === 'eraser') {
32135
+ this._isErasing = false;
32136
+ this.service.endErase();
32137
+ return;
32138
+ }
32139
+ this.service.endStroke();
32140
+ }
32141
+ onPointerLeave(_event) {
32142
+ const tool = this.service.tool();
32143
+ if (tool === 'laser') {
32144
+ this.service.hideLaser();
32145
+ }
32146
+ if (tool === 'eraser') {
32147
+ this._isErasing = false;
32148
+ this.service.endErase();
32149
+ }
32150
+ this.service.endStroke();
32151
+ }
32152
+ // ------------------------------------------------------------------
32153
+ // Coordinate mapping
32154
+ // ------------------------------------------------------------------
32155
+ /**
32156
+ * Map a client-space pointer position to slide-space coordinates by
32157
+ * subtracting the SVG element's bounding rect and dividing by the zoom.
32158
+ * Returns `null` when the SVG ref is not yet available.
32159
+ */
32160
+ _toSlideCoords(clientX, clientY) {
32161
+ const svgEl = this.svgRef()?.nativeElement;
32162
+ if (!svgEl) {
32163
+ return null;
32164
+ }
32165
+ const rect = svgEl.getBoundingClientRect();
32166
+ const z = this.zoom() || 1;
32167
+ return {
32168
+ x: (clientX - rect.left) / z,
32169
+ y: (clientY - rect.top) / z,
32170
+ };
32171
+ }
32172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32173
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationAnnotationOverlayComponent, isStandalone: true, selector: "pptx-presentation-annotation-overlay", inputs: { canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svg"], descendants: true, isSignal: true }], ngImport: i0, template: `
32174
+ @if (isArmed()) {
32175
+ <div
32176
+ class="pptx-ng-anno-wrapper"
32177
+ [ngStyle]="wrapperStyle()"
32178
+ (pointerdown)="onPointerDown($event)"
32179
+ (pointermove)="onPointerMove($event)"
32180
+ (pointerup)="onPointerUp($event)"
32181
+ (pointerleave)="onPointerLeave($event)"
32182
+ >
32183
+ <svg
32184
+ #svg
32185
+ class="pptx-ng-anno-svg"
32186
+ [attr.width]="canvasSize().width"
32187
+ [attr.height]="canvasSize().height"
32188
+ [attr.viewBox]="viewBox()"
32189
+ [ngStyle]="svgStyle()"
32190
+ >
32191
+ @for (s of allStrokes(); track s.id) {
32192
+ <path
32193
+ [attr.d]="strokePath(s.points)"
32194
+ fill="none"
32195
+ [attr.stroke]="s.color"
32196
+ [attr.stroke-width]="s.width"
32197
+ stroke-linecap="round"
32198
+ stroke-linejoin="round"
32199
+ [attr.opacity]="s.opacity"
32200
+ />
32201
+ }
32202
+ </svg>
32203
+
32204
+ @if (service.tool() === 'laser' && service.laserPosition(); as lp) {
32205
+ <div class="pptx-ng-laser-dot" [ngStyle]="laserDotStyle(lp.x, lp.y)"></div>
32206
+ }
32207
+ </div>
32208
+ }
32209
+ `, isInline: true, styles: [":host{display:block;position:absolute;inset:0;pointer-events:none}.pptx-ng-anno-wrapper{position:absolute;inset:0}.pptx-ng-anno-svg{position:absolute;top:0;left:0;transform-origin:top left;overflow:visible}.pptx-ng-laser-dot{position:absolute;border-radius:50%;pointer-events:none;background:#ff0000d9;box-shadow:0 0 12px 6px #ff000080,0 0 24px 12px #ff000040;filter:drop-shadow(0 0 8px rgba(255,0,0,.7))}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
32210
+ }
32211
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationOverlayComponent, decorators: [{
32212
+ type: Component,
32213
+ args: [{ selector: 'pptx-presentation-annotation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle], template: `
32214
+ @if (isArmed()) {
32215
+ <div
32216
+ class="pptx-ng-anno-wrapper"
32217
+ [ngStyle]="wrapperStyle()"
32218
+ (pointerdown)="onPointerDown($event)"
32219
+ (pointermove)="onPointerMove($event)"
32220
+ (pointerup)="onPointerUp($event)"
32221
+ (pointerleave)="onPointerLeave($event)"
32222
+ >
32223
+ <svg
32224
+ #svg
32225
+ class="pptx-ng-anno-svg"
32226
+ [attr.width]="canvasSize().width"
32227
+ [attr.height]="canvasSize().height"
32228
+ [attr.viewBox]="viewBox()"
32229
+ [ngStyle]="svgStyle()"
32230
+ >
32231
+ @for (s of allStrokes(); track s.id) {
32232
+ <path
32233
+ [attr.d]="strokePath(s.points)"
32234
+ fill="none"
32235
+ [attr.stroke]="s.color"
32236
+ [attr.stroke-width]="s.width"
32237
+ stroke-linecap="round"
32238
+ stroke-linejoin="round"
32239
+ [attr.opacity]="s.opacity"
32240
+ />
32241
+ }
32242
+ </svg>
32243
+
32244
+ @if (service.tool() === 'laser' && service.laserPosition(); as lp) {
32245
+ <div class="pptx-ng-laser-dot" [ngStyle]="laserDotStyle(lp.x, lp.y)"></div>
32246
+ }
32247
+ </div>
32248
+ }
32249
+ `, styles: [":host{display:block;position:absolute;inset:0;pointer-events:none}.pptx-ng-anno-wrapper{position:absolute;inset:0}.pptx-ng-anno-svg{position:absolute;top:0;left:0;transform-origin:top left;overflow:visible}.pptx-ng-laser-dot{position:absolute;border-radius:50%;pointer-events:none;background:#ff0000d9;box-shadow:0 0 12px 6px #ff000080,0 0 24px 12px #ff000040;filter:drop-shadow(0 0 8px rgba(255,0,0,.7))}\n"] }]
32250
+ }], propDecorators: { canvasSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasSize", required: true }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svg', { isSignal: true }] }] } });
32251
+
29780
32252
  /**
29781
32253
  * Clamp `index` to the valid range [0, count - 1].
29782
32254
  * Returns 0 when `count` is 0 to avoid -1 states.
@@ -29842,6 +32314,235 @@ function fitZoom(canvasW, canvasH, vw, vh) {
29842
32314
  return Math.min(vw / canvasW, vh / canvasH);
29843
32315
  }
29844
32316
 
32317
+ /**
32318
+ * presentation-subtitle-helpers.ts — Pure helpers for the subtitle/caption bar.
32319
+ *
32320
+ * Isolates all text-segment logic so it can be unit-tested without DOM or
32321
+ * Angular dependencies.
32322
+ *
32323
+ * Ported from React:
32324
+ * packages/react/src/viewer/components/PresentationSubtitleBar.tsx
32325
+ */
32326
+ // ---------------------------------------------------------------------------
32327
+ // Caption text merging
32328
+ // ---------------------------------------------------------------------------
32329
+ /**
32330
+ * Merge the results from a `SpeechRecognitionEventLite` (starting at
32331
+ * `resultIndex`) into a single caption string.
32332
+ *
32333
+ * Final results form the stable prefix; interim results form the unstable
32334
+ * suffix. Both are joined and the combined string is trimmed.
32335
+ *
32336
+ * @param event The recognition event from `onresult`.
32337
+ * @param resultIndex `event.resultIndex` — the first new result index.
32338
+ * @param results `event.results` — the full results list.
32339
+ * @returns The merged caption string, or `''` if nothing recognised.
32340
+ */
32341
+ function mergeCaptionResults(resultIndex, results) {
32342
+ let finalText = '';
32343
+ let interimText = '';
32344
+ for (let i = resultIndex; i < results.length; i += 1) {
32345
+ const result = results[i];
32346
+ const fragment = result?.[0]?.transcript ?? '';
32347
+ if (result?.isFinal) {
32348
+ finalText += fragment;
32349
+ }
32350
+ else {
32351
+ interimText += fragment;
32352
+ }
32353
+ }
32354
+ const merged = `${finalText} ${interimText}`.trim();
32355
+ return merged;
32356
+ }
32357
+ // ---------------------------------------------------------------------------
32358
+ // Safe speech recognition factory
32359
+ // ---------------------------------------------------------------------------
32360
+ /**
32361
+ * Attempt to obtain the `SpeechRecognition` constructor from `globalThis`
32362
+ * (browser `window`). Returns `null` when unavailable (SSR / unsupported
32363
+ * browsers / Firefox without the flag enabled).
32364
+ *
32365
+ * Checks both the standard and the webkit-prefixed name.
32366
+ */
32367
+ function getSpeechRecognitionCtor() {
32368
+ // Use `globalThis` rather than `window` so this runs in non-DOM environments.
32369
+ const g = globalThis;
32370
+ const ctor = (g['SpeechRecognition'] ?? g['webkitSpeechRecognition']);
32371
+ return ctor ?? null;
32372
+ }
32373
+ /**
32374
+ * Compute the text to display in the caption bar.
32375
+ *
32376
+ * - When the API is unsupported, returns `fallbackNotSupported`.
32377
+ * - When supported but no text has been captured yet, returns `fallbackListening`.
32378
+ * - Otherwise returns the captured text.
32379
+ */
32380
+ function captionDisplayText(supportState, captionText, fallbackNotSupported, fallbackListening) {
32381
+ if (supportState === 'unsupported') {
32382
+ return fallbackNotSupported;
32383
+ }
32384
+ return captionText.length > 0 ? captionText : fallbackListening;
32385
+ }
32386
+
32387
+ /**
32388
+ * presentation-subtitle-bar.component.ts — Live caption/subtitle bar for
32389
+ * presentation mode, driven by the Web Speech API when available.
32390
+ *
32391
+ * Ported from React:
32392
+ * packages/react/src/viewer/components/PresentationSubtitleBar.tsx
32393
+ *
32394
+ * Selector: `pptx-presentation-subtitle-bar`
32395
+ *
32396
+ * Inputs:
32397
+ * - `visible` (required) — show/hide the bar and start/stop recognition.
32398
+ *
32399
+ * When `visible` is false the component renders nothing and stops the
32400
+ * recognition session. When the Web Speech API is unavailable the bar still
32401
+ * renders but shows a "not supported" message.
32402
+ *
32403
+ * All speech-API access is isolated behind `_createRecognition()` so the rest
32404
+ * of the component (and tests) can stay pure. Caption text accumulation logic
32405
+ * lives in {@link mergeCaptionResults} (pure helper, unit-tested separately).
32406
+ */
32407
+ class PresentationSubtitleBarComponent {
32408
+ // ------------------------------------------------------------------
32409
+ // Inputs
32410
+ // ------------------------------------------------------------------
32411
+ /** Show the subtitle bar and start speech recognition when true. */
32412
+ visible = input.required(/* @ts-ignore */
32413
+ ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
32414
+ // ------------------------------------------------------------------
32415
+ // Internal signals
32416
+ // ------------------------------------------------------------------
32417
+ _captionText = signal('', /* @ts-ignore */
32418
+ ...(ngDevMode ? [{ debugName: "_captionText" }] : /* istanbul ignore next */ []));
32419
+ _supportState = signal('unknown', /* @ts-ignore */
32420
+ ...(ngDevMode ? [{ debugName: "_supportState" }] : /* istanbul ignore next */ []));
32421
+ /** The text string rendered in the caption bar. */
32422
+ displayText = signal('Listening…', /* @ts-ignore */
32423
+ ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
32424
+ // ------------------------------------------------------------------
32425
+ // Recognition lifecycle bookkeeping
32426
+ // ------------------------------------------------------------------
32427
+ /**
32428
+ * Whether the recognition session should remain running.
32429
+ * Toggled on `visible` changes; checked in `onend` to decide whether
32430
+ * to restart.
32431
+ */
32432
+ _shouldRun = false;
32433
+ /** Active recognition instance, or null when stopped. */
32434
+ _recognition = null;
32435
+ _destroyRef = inject(DestroyRef);
32436
+ constructor() {
32437
+ this._destroyRef.onDestroy(() => {
32438
+ this._stopRecognition();
32439
+ });
32440
+ }
32441
+ // ------------------------------------------------------------------
32442
+ // Lifecycle
32443
+ // ------------------------------------------------------------------
32444
+ ngOnChanges(changes) {
32445
+ if (!('visible' in changes)) {
32446
+ return;
32447
+ }
32448
+ if (this.visible()) {
32449
+ this._startRecognition();
32450
+ }
32451
+ else {
32452
+ this._stopRecognition();
32453
+ }
32454
+ }
32455
+ // ------------------------------------------------------------------
32456
+ // Speech recognition management
32457
+ // ------------------------------------------------------------------
32458
+ _startRecognition() {
32459
+ this._shouldRun = true;
32460
+ this._captionText.set('');
32461
+ this._updateDisplayText();
32462
+ const Ctor = this._getSpeechCtor();
32463
+ if (!Ctor) {
32464
+ this._supportState.set('unsupported');
32465
+ this._updateDisplayText();
32466
+ return;
32467
+ }
32468
+ this._supportState.set('supported');
32469
+ const recognition = new Ctor();
32470
+ recognition.continuous = true;
32471
+ recognition.interimResults = true;
32472
+ recognition.lang = typeof navigator !== 'undefined' ? navigator.language || 'en-US' : 'en-US';
32473
+ recognition.onresult = (event) => {
32474
+ const merged = mergeCaptionResults(event.resultIndex, event.results);
32475
+ if (merged.length > 0) {
32476
+ this._captionText.set(merged);
32477
+ this._updateDisplayText();
32478
+ }
32479
+ };
32480
+ recognition.onerror = () => {
32481
+ // Keep the bar alive; `onend` will attempt restart if still visible.
32482
+ };
32483
+ recognition.onend = () => {
32484
+ if (!this._shouldRun) {
32485
+ return;
32486
+ }
32487
+ try {
32488
+ recognition.start();
32489
+ }
32490
+ catch {
32491
+ // Browser may throttle rapid restarts; the next visibility toggle will retry.
32492
+ }
32493
+ };
32494
+ this._recognition = recognition;
32495
+ try {
32496
+ recognition.start();
32497
+ }
32498
+ catch {
32499
+ this._supportState.set('unsupported');
32500
+ this._updateDisplayText();
32501
+ }
32502
+ }
32503
+ _stopRecognition() {
32504
+ this._shouldRun = false;
32505
+ if (this._recognition) {
32506
+ this._recognition.stop();
32507
+ this._recognition = null;
32508
+ }
32509
+ this._captionText.set('');
32510
+ this._updateDisplayText();
32511
+ }
32512
+ /**
32513
+ * Thin wrapper around {@link getSpeechRecognitionCtor} so tests can spy on
32514
+ * or override this method without patching `globalThis`.
32515
+ */
32516
+ _getSpeechCtor() {
32517
+ return getSpeechRecognitionCtor();
32518
+ }
32519
+ // ------------------------------------------------------------------
32520
+ // Display text derivation
32521
+ // ------------------------------------------------------------------
32522
+ _updateDisplayText() {
32523
+ const text = captionDisplayText(this._supportState(), this._captionText(), 'Subtitles not supported in this browser.', 'Listening…');
32524
+ this.displayText.set(text);
32525
+ }
32526
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationSubtitleBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32527
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationSubtitleBarComponent, isStandalone: true, selector: "pptx-presentation-subtitle-bar", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: true, transformFunction: null } }, usesOnChanges: true, ngImport: i0, template: `
32528
+ @if (visible()) {
32529
+ <div class="pptx-ng-subtitle-inner">
32530
+ <span class="pptx-ng-subtitle-text">{{ displayText() }}</span>
32531
+ </div>
32532
+ }
32533
+ `, isInline: true, styles: [":host{display:block;position:absolute;bottom:3.5rem;left:50%;transform:translate(-50%);z-index:70;max-width:80%;min-width:300px;pointer-events:none}.pptx-ng-subtitle-inner{padding:.75rem 1.5rem;border-radius:.5rem;background:#000000bf;border:1px solid rgba(255,255,255,.1);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.pptx-ng-subtitle-text{display:block;text-align:center;font-size:.9375rem;line-height:1.5;color:#ffffffb3;font-style:italic;font-family:system-ui,sans-serif;white-space:pre-wrap;word-break:break-word}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
32534
+ }
32535
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationSubtitleBarComponent, decorators: [{
32536
+ type: Component,
32537
+ args: [{ selector: 'pptx-presentation-subtitle-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
32538
+ @if (visible()) {
32539
+ <div class="pptx-ng-subtitle-inner">
32540
+ <span class="pptx-ng-subtitle-text">{{ displayText() }}</span>
32541
+ </div>
32542
+ }
32543
+ `, styles: [":host{display:block;position:absolute;bottom:3.5rem;left:50%;transform:translate(-50%);z-index:70;max-width:80%;min-width:300px;pointer-events:none}.pptx-ng-subtitle-inner{padding:.75rem 1.5rem;border-radius:.5rem;background:#000000bf;border:1px solid rgba(255,255,255,.1);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.pptx-ng-subtitle-text{display:block;text-align:center;font-size:.9375rem;line-height:1.5;color:#ffffffb3;font-style:italic;font-family:system-ui,sans-serif;white-space:pre-wrap;word-break:break-word}\n"] }]
32544
+ }], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: true }] }] } });
32545
+
29845
32546
  // ---------------------------------------------------------------------------
29846
32547
  // Direction resolvers
29847
32548
  // ---------------------------------------------------------------------------
@@ -30446,6 +33147,30 @@ function getSlideTransitionAnimations(type, durationMs, direction, orient, spoke
30446
33147
  const randomType = RANDOM_ELIGIBLE_TYPES[Math.floor(Math.random() * RANDOM_ELIGIBLE_TYPES.length)];
30447
33148
  return getSlideTransitionAnimations(randomType, durationMs, direction, orient, spokes);
30448
33149
  }
33150
+ // ── Exotic / 3-D transitions — approximated with the closest 2-D CSS
33151
+ // behaviour (mirrors React's CSS fallback for the dynamic-content family
33152
+ // that has no faithful pure-CSS analogue). Delegates to an existing
33153
+ // well-tested case rather than emitting a plain fade.
33154
+ case 'pan':
33155
+ case 'gallery':
33156
+ case 'conveyor':
33157
+ return getSlideTransitionAnimations('push', durationMs, direction, orient, spokes);
33158
+ case 'reveal':
33159
+ return getSlideTransitionAnimations('wipe', durationMs, direction, orient, spokes);
33160
+ case 'doors':
33161
+ return getSlideTransitionAnimations('split', durationMs, direction, orient, spokes);
33162
+ case 'switch':
33163
+ case 'flythrough':
33164
+ case 'ferris':
33165
+ case 'prism':
33166
+ return getSlideTransitionAnimations('zoom', durationMs, direction, orient, spokes);
33167
+ case 'ripple':
33168
+ case 'honeycomb':
33169
+ case 'glitter':
33170
+ case 'shred':
33171
+ return getSlideTransitionAnimations('dissolve', durationMs, direction, orient, spokes);
33172
+ case 'flash':
33173
+ return getSlideTransitionAnimations('fade', durationMs, direction, orient, spokes);
30449
33174
  // Fallback (unknown / not-yet-mapped type)
30450
33175
  default:
30451
33176
  return {
@@ -30732,6 +33457,11 @@ class PresentationOverlayComponent {
30732
33457
  ...(ngDevMode ? [{ debugName: "activeTransition" }] : /* istanbul ignore next */ []));
30733
33458
  /** Click-stepped element-animation playback for the current slide. */
30734
33459
  playback = inject(AnimationPlaybackService);
33460
+ /** Ink-annotation state (pen/highlighter/eraser/laser) for the show. */
33461
+ annotations = inject(PresentationAnnotationsService);
33462
+ /** Whether the live-caption bar is shown. */
33463
+ subtitlesVisible = signal(false, /* @ts-ignore */
33464
+ ...(ngDevMode ? [{ debugName: "subtitlesVisible" }] : /* istanbul ignore next */ []));
30735
33465
  /** The slide stage root — animation styles are applied to its elements. */
30736
33466
  stageRef = viewChild('stage', /* @ts-ignore */
30737
33467
  ...(ngDevMode ? [{ debugName: "stageRef" }] : /* istanbul ignore next */ []));
@@ -30973,8 +33703,20 @@ class PresentationOverlayComponent {
30973
33703
  if (event.button !== 0) {
30974
33704
  return;
30975
33705
  }
33706
+ // A drawing tool owns pointer gestures — don't hijack them to advance.
33707
+ if (this.annotations.tool() !== 'none') {
33708
+ return;
33709
+ }
30976
33710
  this.navigate('next');
30977
33711
  }
33712
+ /** Toggle an annotation tool (clicking the active one disarms it). */
33713
+ selectTool(tool) {
33714
+ this.annotations.setTool(tool);
33715
+ }
33716
+ /** Toggle the live-caption (subtitle) bar. */
33717
+ toggleSubtitles() {
33718
+ this.subtitlesVisible.update((v) => !v);
33719
+ }
30978
33720
  /** Close button click — stop propagation so it does not also advance. */
30979
33721
  onClose(event) {
30980
33722
  event.stopPropagation();
@@ -31098,6 +33840,7 @@ class PresentationOverlayComponent {
31098
33840
  this.activeTransition.set(null);
31099
33841
  }
31100
33842
  this.currentIndex.set(next);
33843
+ this.annotations.setActiveSlide(next);
31101
33844
  this.indexChange.emit(next);
31102
33845
  }
31103
33846
  }
@@ -31105,7 +33848,7 @@ class PresentationOverlayComponent {
31105
33848
  this.closed.emit();
31106
33849
  }
31107
33850
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
31108
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationOverlayComponent, isStandalone: true, selector: "pptx-presentation-overlay", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, startIndex: { classPropertyName: "startIndex", publicName: "startIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { indexChange: "indexChange", closed: "closed" }, host: { listeners: { "window:resize": "onWindowResize()", "document:keydown": "onKeyDown($event)" } }, providers: [AnimationPlaybackService], viewQueries: [{ propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }], ngImport: i0, template: `
33851
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationOverlayComponent, isStandalone: true, selector: "pptx-presentation-overlay", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, startIndex: { classPropertyName: "startIndex", publicName: "startIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { indexChange: "indexChange", closed: "closed" }, host: { listeners: { "window:resize": "onWindowResize()", "document:keydown": "onKeyDown($event)" } }, providers: [AnimationPlaybackService, PresentationAnnotationsService], viewQueries: [{ propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }], ngImport: i0, template: `
31109
33852
  <div
31110
33853
  class="pptx-ng-presentation-root"
31111
33854
  (touchstart)="onTouchStart($event)"
@@ -31134,6 +33877,59 @@ class PresentationOverlayComponent {
31134
33877
  (complete)="activeTransition.set(null)"
31135
33878
  />
31136
33879
  }
33880
+
33881
+ <!-- Ink annotation overlay (pen/highlighter/eraser/laser). -->
33882
+ <pptx-presentation-annotation-overlay [canvasSize]="canvasSize()" [zoom]="zoom()" />
33883
+ </div>
33884
+
33885
+ <!-- Live-caption (subtitle) bar. -->
33886
+ <pptx-presentation-subtitle-bar [visible]="subtitlesVisible()" />
33887
+
33888
+ <!-- Annotation tool toolbar (bottom-centre). -->
33889
+ <div class="pptx-ng-presentation-tools" role="toolbar" aria-label="Annotation tools">
33890
+ <button
33891
+ type="button"
33892
+ [class.is-active]="annotations.tool() === 'pen'"
33893
+ (click)="selectTool('pen')"
33894
+ aria-label="Pen"
33895
+ >
33896
+
33897
+ </button>
33898
+ <button
33899
+ type="button"
33900
+ [class.is-active]="annotations.tool() === 'highlighter'"
33901
+ (click)="selectTool('highlighter')"
33902
+ aria-label="Highlighter"
33903
+ >
33904
+
33905
+ </button>
33906
+ <button
33907
+ type="button"
33908
+ [class.is-active]="annotations.tool() === 'eraser'"
33909
+ (click)="selectTool('eraser')"
33910
+ aria-label="Eraser"
33911
+ >
33912
+
33913
+ </button>
33914
+ <button
33915
+ type="button"
33916
+ [class.is-active]="annotations.tool() === 'laser'"
33917
+ (click)="selectTool('laser')"
33918
+ aria-label="Laser pointer"
33919
+ >
33920
+
33921
+ </button>
33922
+ <button type="button" (click)="annotations.clearAnnotations()" aria-label="Clear ink">
33923
+ 🗑
33924
+ </button>
33925
+ <button
33926
+ type="button"
33927
+ [class.is-active]="subtitlesVisible()"
33928
+ (click)="toggleSubtitles()"
33929
+ aria-label="Live captions"
33930
+ >
33931
+ CC
33932
+ </button>
31137
33933
  </div>
31138
33934
 
31139
33935
  <!-- Always-visible close button (top-right, safe-area aware). -->
@@ -31175,11 +33971,17 @@ class PresentationOverlayComponent {
31175
33971
  {{ counterLabel() }}
31176
33972
  </span>
31177
33973
  </div>
31178
- `, isInline: true, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "selectedIds", "editingId"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "rotateUpdate", "marqueeSelect"] }, { kind: "component", type: PresentationTransitionOverlayComponent, selector: "pptx-presentation-transition-overlay", inputs: ["outgoingSlide", "canvasSize", "transition", "templateElements", "mediaDataUrls", "durationMs"], outputs: ["complete"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33974
+ `, isInline: true, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}.pptx-ng-presentation-tools{position:absolute;bottom:max(1rem,env(safe-area-inset-bottom));left:50%;transform:translate(-50%);display:flex;gap:.25rem;padding:.25rem;border-radius:.5rem;background:#0000008c;z-index:80}.pptx-ng-presentation-tools button{width:2rem;height:2rem;border:none;border-radius:.35rem;background:transparent;color:#fff;font-size:1rem;cursor:pointer}.pptx-ng-presentation-tools button:hover{background:#ffffff26}.pptx-ng-presentation-tools button.is-active{background:#ffffff4d}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "selectedIds", "editingId"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "rotateUpdate", "marqueeSelect"] }, { kind: "component", type: PresentationTransitionOverlayComponent, selector: "pptx-presentation-transition-overlay", inputs: ["outgoingSlide", "canvasSize", "transition", "templateElements", "mediaDataUrls", "durationMs"], outputs: ["complete"] }, { kind: "component", type: PresentationAnnotationOverlayComponent, selector: "pptx-presentation-annotation-overlay", inputs: ["canvasSize", "zoom"] }, { kind: "component", type: PresentationSubtitleBarComponent, selector: "pptx-presentation-subtitle-bar", inputs: ["visible"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
31179
33975
  }
31180
33976
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationOverlayComponent, decorators: [{
31181
33977
  type: Component,
31182
- args: [{ selector: 'pptx-presentation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle, SlideCanvasComponent, PresentationTransitionOverlayComponent], providers: [AnimationPlaybackService], template: `
33978
+ args: [{ selector: 'pptx-presentation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
33979
+ NgStyle,
33980
+ SlideCanvasComponent,
33981
+ PresentationTransitionOverlayComponent,
33982
+ PresentationAnnotationOverlayComponent,
33983
+ PresentationSubtitleBarComponent,
33984
+ ], providers: [AnimationPlaybackService, PresentationAnnotationsService], template: `
31183
33985
  <div
31184
33986
  class="pptx-ng-presentation-root"
31185
33987
  (touchstart)="onTouchStart($event)"
@@ -31208,6 +34010,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
31208
34010
  (complete)="activeTransition.set(null)"
31209
34011
  />
31210
34012
  }
34013
+
34014
+ <!-- Ink annotation overlay (pen/highlighter/eraser/laser). -->
34015
+ <pptx-presentation-annotation-overlay [canvasSize]="canvasSize()" [zoom]="zoom()" />
34016
+ </div>
34017
+
34018
+ <!-- Live-caption (subtitle) bar. -->
34019
+ <pptx-presentation-subtitle-bar [visible]="subtitlesVisible()" />
34020
+
34021
+ <!-- Annotation tool toolbar (bottom-centre). -->
34022
+ <div class="pptx-ng-presentation-tools" role="toolbar" aria-label="Annotation tools">
34023
+ <button
34024
+ type="button"
34025
+ [class.is-active]="annotations.tool() === 'pen'"
34026
+ (click)="selectTool('pen')"
34027
+ aria-label="Pen"
34028
+ >
34029
+
34030
+ </button>
34031
+ <button
34032
+ type="button"
34033
+ [class.is-active]="annotations.tool() === 'highlighter'"
34034
+ (click)="selectTool('highlighter')"
34035
+ aria-label="Highlighter"
34036
+ >
34037
+
34038
+ </button>
34039
+ <button
34040
+ type="button"
34041
+ [class.is-active]="annotations.tool() === 'eraser'"
34042
+ (click)="selectTool('eraser')"
34043
+ aria-label="Eraser"
34044
+ >
34045
+
34046
+ </button>
34047
+ <button
34048
+ type="button"
34049
+ [class.is-active]="annotations.tool() === 'laser'"
34050
+ (click)="selectTool('laser')"
34051
+ aria-label="Laser pointer"
34052
+ >
34053
+
34054
+ </button>
34055
+ <button type="button" (click)="annotations.clearAnnotations()" aria-label="Clear ink">
34056
+ 🗑
34057
+ </button>
34058
+ <button
34059
+ type="button"
34060
+ [class.is-active]="subtitlesVisible()"
34061
+ (click)="toggleSubtitles()"
34062
+ aria-label="Live captions"
34063
+ >
34064
+ CC
34065
+ </button>
31211
34066
  </div>
31212
34067
 
31213
34068
  <!-- Always-visible close button (top-right, safe-area aware). -->
@@ -31249,7 +34104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
31249
34104
  {{ counterLabel() }}
31250
34105
  </span>
31251
34106
  </div>
31252
- `, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}\n"] }]
34107
+ `, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}.pptx-ng-presentation-tools{position:absolute;bottom:max(1rem,env(safe-area-inset-bottom));left:50%;transform:translate(-50%);display:flex;gap:.25rem;padding:.25rem;border-radius:.5rem;background:#0000008c;z-index:80}.pptx-ng-presentation-tools button{width:2rem;height:2rem;border:none;border-radius:.35rem;background:transparent;color:#fff;font-size:1rem;cursor:pointer}.pptx-ng-presentation-tools button:hover{background:#ffffff26}.pptx-ng-presentation-tools button.is-active{background:#ffffff4d}\n"] }]
31253
34108
  }], ctorParameters: () => [], propDecorators: { slides: [{ type: i0.Input, args: [{ isSignal: true, alias: "slides", required: true }] }], canvasSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasSize", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], startIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "startIndex", required: false }] }], indexChange: [{ type: i0.Output, args: ["indexChange"] }], closed: [{ type: i0.Output, args: ["closed"] }], stageRef: [{ type: i0.ViewChild, args: ['stage', { isSignal: true }] }], onWindowResize: [{
31254
34109
  type: HostListener,
31255
34110
  args: ['window:resize']
@@ -36443,5 +39298,5 @@ function cn(...values) {
36443
39298
  * Generated bundle index. Do not edit.
36444
39299
  */
36445
39300
 
36446
- export { AccessibilityPanelComponent, AccessibilityService, AnimationAuthorPanelComponent, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartDataEditorComponent, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, ConnectorTextOverlayComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, EffectsPanelComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, FindReplaceBarComponent, GradientPickerComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, IsMobileService, LoadContentService, MobileBottomBarComponent, MobileMenuSheetComponent, MobileSheetComponent, MobileSlidesSheetComponent, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationOverlayComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableDataEditorComponent, TableRendererComponent, TextAdvancedPanelComponent, VIEWER_THEME, WEBM_MIME_CANDIDATES, ZoomRendererComponent, addCommentToList, advanceStep, applyFindReplacements, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildDuotoneFilter, buildDuotoneFilterId, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampGifDimensions, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computeIsMobile, computeIsTablet, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, encodeGif, estimatePageCount, findInSlides, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, fpsToFrameIntervalMs, generateBroadcastRoomId, generateCommentId, getContainerStyle, getDuotoneFilterDef, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, msToFrameDelayCs, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, pickSupportedMimeType, planGifFrames, planVideoSegments, presenceToCursors, provideViewerTheme, recordWebm, removeCommentFromList, renderToCanvas, replaceInSlides, replaceMatch, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, routeOrthogonalConnector, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, segmentFrameCount, sendBackward, sendToBack, setElementPosition, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, waypointsToPathD, worstStatus };
39301
+ export { AccessibilityPanelComponent, AccessibilityService, AnimationAuthorPanelComponent, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartDataEditorComponent, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, ConnectorTextOverlayComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, EffectsPanelComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, FindReplaceBarComponent, GradientPickerComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, IsMobileService, LoadContentService, MobileBottomBarComponent, MobileMenuSheetComponent, MobileSheetComponent, MobileSlidesSheetComponent, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationAnnotationOverlayComponent, PresentationAnnotationsService, PresentationOverlayComponent, PresentationSubtitleBarComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, SVG_WARP_PRESETS, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableDataEditorComponent, TableRendererComponent, TextAdvancedPanelComponent, VIEWER_THEME, WEBM_MIME_CANDIDATES, ZoomRendererComponent, addCommentToList, advanceStep, applyFindReplacements, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildDuotoneFilter, buildDuotoneFilterId, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampGifDimensions, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computeIsMobile, computeIsTablet, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, encodeGif, estimatePageCount, findInSlides, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, fpsToFrameIntervalMs, generateBroadcastRoomId, generateCommentId, getContainerStyle, getDuotoneFilterDef, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, getTextWarp, getWarpCategory, getWarpPath, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, msToFrameDelayCs, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, pickSupportedMimeType, planGifFrames, planVideoSegments, presenceToCursors, provideViewerTheme, recordWebm, removeCommentFromList, renderToCanvas, replaceInSlides, replaceMatch, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, routeOrthogonalConnector, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, segmentFrameCount, sendBackward, sendToBack, setElementPosition, shouldUseSvgWarp, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, waypointsToPathD, worstStatus };
36447
39302
  //# sourceMappingURL=pptx-angular-viewer.mjs.map