pptx-angular-viewer 3.14.0 → 3.16.0

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.
@@ -7685,6 +7685,122 @@ const NOMINAL_ENVELOPE_BAND = {
7685
7685
  bottom: BOTTOM_MARGIN,
7686
7686
  };
7687
7687
 
7688
+ /**
7689
+ * Canvas-based text measurement for the glyph envelope layout
7690
+ * (`text-warp-envelope-layout.ts`): per-character advance widths and the
7691
+ * line's real (ink-measured) ascent, both backed by a single lazily-created
7692
+ * `CanvasRenderingContext2D` shared across calls.
7693
+ */
7694
+ let measureCtx;
7695
+ function getMeasureCtx$1() {
7696
+ if (measureCtx !== undefined) {
7697
+ return measureCtx;
7698
+ }
7699
+ if (typeof document === 'undefined') {
7700
+ measureCtx = null;
7701
+ return null;
7702
+ }
7703
+ measureCtx = document.createElement('canvas').getContext('2d');
7704
+ return measureCtx;
7705
+ }
7706
+ function toCanvasFont$1(font) {
7707
+ const size = font.fontSizePx && font.fontSizePx > 0 ? font.fontSizePx : DEFAULT_TEXT_FONT_SIZE;
7708
+ const family = font.fontFamily || DEFAULT_FONT_FAMILY;
7709
+ return `${font.italic ? 'italic ' : ''}${font.bold ? 'bold ' : ''}${size}px ${family}`;
7710
+ }
7711
+ /**
7712
+ * Per-character advance widths for `text` set in `font`, measured as prefix
7713
+ * differences (never a lone character: see `text-metric-tracking.ts`'s
7714
+ * `advancesOf` for why - shaped scripts and ligatures need the context).
7715
+ *
7716
+ * Falls back to a flat `0.55em`-per-character estimate when there is no DOM
7717
+ * to measure with (SSR, or a test environment without a 2D canvas context);
7718
+ * the estimate only affects horizontal glyph spacing, never the envelope
7719
+ * curve itself, so it stays visually reasonable even when approximate.
7720
+ */
7721
+ function measureGlyphAdvances(text, font) {
7722
+ const chars = [...text];
7723
+ const ctx = getMeasureCtx$1();
7724
+ if (!ctx) {
7725
+ const size = font.fontSizePx && font.fontSizePx > 0 ? font.fontSizePx : DEFAULT_TEXT_FONT_SIZE;
7726
+ return chars.map(() => size * 0.55);
7727
+ }
7728
+ ctx.font = toCanvasFont$1(font);
7729
+ const advances = [];
7730
+ let previous = 0;
7731
+ let prefix = '';
7732
+ for (const char of chars) {
7733
+ prefix += char;
7734
+ const width = ctx.measureText(prefix).width;
7735
+ advances.push(Math.max(0, width - previous));
7736
+ previous = width;
7737
+ }
7738
+ return advances;
7739
+ }
7740
+ /**
7741
+ * The real (ink-measured) ascent of `segments`' text at their own font
7742
+ * sizes, as the tallest `actualBoundingBoxAscent` across every segment on
7743
+ * the line (not a per-character average - one tall glyph anywhere on the
7744
+ * line sets the reference the whole line warps against, matching how a
7745
+ * single baseline/cap-height pair governs a real text run).
7746
+ *
7747
+ * `buildGlyphEnvelope` used to map every glyph's nominal band from a FIXED
7748
+ * `NOMINAL_ENVELOPE_BAND` fraction of the box height (0.15..0.85), assuming
7749
+ * a glyph's own cap height fills that whole span. COM-measured (2026-09-11,
7750
+ * `text-warp-glyph-outline.ts`'s doc comment): for an 8-shape WordArt
7751
+ * fixture (Arimo Bold 44pt captions in 100pt-tall boxes, the `textCanUp` /
7752
+ * `textCanDown` / `textInflate` / `textDeflate` presets at both default and
7753
+ * extreme `adj`), real cap height reaches only about `t = 0.57` of that
7754
+ * nominal span, not `t = 0`, so every glyph's mapped top undershot the
7755
+ * curve's own top edge by the same amount - an outline-vs-COM interior-
7756
+ * column ink-scan comparison measured ~30-40% of box height mean error (max
7757
+ * 58-80%) on BOTH the outline path and the affine fallback alike (both use
7758
+ * this same nominal band, so both shared the bug identically: the residual
7759
+ * lived here, not in the outline point-mapping math). Anchoring `nomTop` to
7760
+ * the line's REAL measured ascent instead - clamped to never exceed the
7761
+ * historical fixed band, so a line whose font genuinely fills (or exceeds)
7762
+ * the nominal span keeps the old, already-validated behaviour unchanged -
7763
+ * dropped the `textInflate`/`textDeflate` interior mean error to ~2.6-2.9%
7764
+ * (max ~9-10%), in the range `text-warp-glyph-slicing.ts`'s doc comment
7765
+ * already documents as the residual once this band mismatch is not also
7766
+ * present. The `textCanUp`/`textCanDown` cases still show an elevated
7767
+ * residual (their interior mean measured ~6-20% even after this fix) that
7768
+ * further investigation traced to a SEPARATE, larger issue: real PowerPoint
7769
+ * spaces envelope-warped glyphs to fill the box's own width edge-to-edge
7770
+ * (measured ink spanning ~99.9% of box width) rather than centering the
7771
+ * text at its natural (unstretched) advance width the way `startX`/
7772
+ * `measureGlyphAdvances` do today, with `textCanUp`/`textCanDown` additionally
7773
+ * showing non-uniform (cylinder-projection-like) horizontal spacing this fix
7774
+ * does not address - both are horizontal-layout gaps, out of scope for this
7775
+ * (purely vertical) band fix and left as an open, separately-scoped issue.
7776
+ *
7777
+ * Returns `undefined` with no DOM (SSR, or a test environment without a 2D
7778
+ * canvas context), so a caller falls back to the previous fixed-fraction
7779
+ * band unchanged, exactly like {@link measureGlyphAdvances}'s own fallback.
7780
+ */
7781
+ function measureLineAscent(segments) {
7782
+ const ctx = getMeasureCtx$1();
7783
+ if (!ctx) {
7784
+ return undefined;
7785
+ }
7786
+ let maxAscent = 0;
7787
+ for (const segment of segments) {
7788
+ if (!segment.text) {
7789
+ continue;
7790
+ }
7791
+ ctx.font = toCanvasFont$1(segment.font);
7792
+ const ascent = ctx.measureText(segment.text).actualBoundingBoxAscent;
7793
+ if (Number.isFinite(ascent) && ascent > maxAscent) {
7794
+ maxAscent = ascent;
7795
+ }
7796
+ }
7797
+ return maxAscent > 0 ? maxAscent : undefined;
7798
+ }
7799
+ /** Test hook: forget the cached measurement context. */
7800
+ function resetGlyphEnvelopeMeasureCache() {
7801
+ measureCtx = undefined;
7802
+ }
7803
+
7688
7804
  /**
7689
7805
  * The per-glyph affine transform for the WordArt two-curve envelope (see
7690
7806
  * `text-warp-envelope-layout.ts`), split out to keep that file under the
@@ -7698,12 +7814,99 @@ function sliceBand(top, bottom, index, count) {
7698
7814
  bottom: top + ((index + 1) / count) * span,
7699
7815
  };
7700
7816
  }
7701
- /** The envelope band (absolute height units, already line-sliced) at one horizontal position. */
7702
- function edgeBandAt(preset, u, adj, adj2, height, lineIndex, lineCount) {
7817
+ /** A preset's deformed top/bottom band (absolute height units) at horizontal position `u`. */
7818
+ function deformedBandAt(preset, u, adj, adj2, height) {
7703
7819
  const curve = envelopeCurveAt(preset, u, adj, adj2);
7704
- const bandTop = (curve?.top ?? NOMINAL_ENVELOPE_BAND.top) * height;
7705
- const bandBottom = (curve?.bottom ?? NOMINAL_ENVELOPE_BAND.bottom) * height;
7706
- return sliceBand(bandTop, bandBottom, lineIndex, lineCount);
7820
+ return {
7821
+ top: (curve?.top ?? NOMINAL_ENVELOPE_BAND.top) * height,
7822
+ bottom: (curve?.bottom ?? NOMINAL_ENVELOPE_BAND.bottom) * height,
7823
+ };
7824
+ }
7825
+ /**
7826
+ * Horizontal position used to compute the FIXED boundary shared by two
7827
+ * adjacent paragraph rows (see {@link edgeBandAt}'s doc comment): the box's
7828
+ * own centre, matching where `NOMINAL_ENVELOPE_BAND` and every other
7829
+ * u-independent reference in this module already anchor.
7830
+ */
7831
+ const ROW_BOUNDARY_REFERENCE_U = 0.5;
7832
+ /**
7833
+ * The envelope band (absolute height units, already line-sliced) at one
7834
+ * horizontal position.
7835
+ *
7836
+ * For a single-line element (`lineCount <= 1`) this is exactly the deformed
7837
+ * band at `u` - unchanged from before per-paragraph slicing existed.
7838
+ *
7839
+ * For a multi-paragraph element, naively slicing the band AFTER deforming it
7840
+ * AT `u` (dividing `[bandTop(u), bandBottom(u)]` into `lineCount` equal
7841
+ * fractions) lets the boundary between row `i` and row `i+1` drift with `u`.
7842
+ * Since each row's OWN glyphs are laid out independently (see
7843
+ * `text-warp-envelope-layout.ts`'s `buildGlyphEnvelope`, called once per
7844
+ * paragraph) and can span very different horizontal ranges - a short
7845
+ * paragraph stretched hard to fill the box samples very different `u` than a
7846
+ * longer one in the SAME box - two rows can end up comparing their own
7847
+ * boundary at two DIFFERENT `u` values where the curve's amplitude differs
7848
+ * enough that row `i`'s computed bottom sits BELOW row `i+1`'s computed top:
7849
+ * an inverted, overlapping pair, even though each row's own boundary is
7850
+ * "correct" in isolation. COM review 2026-09-11 found this pre-existing (not
7851
+ * introduced by the box-fill horizontal-placement fix, though a paragraph's
7852
+ * `stretch` factor can widen how differently two rows sample the curve and
7853
+ * so widen the effect): an 8-shape fixture's two-paragraph `textInflate`
7854
+ * block (`"Top"` over `"Bottom"`) measured its `"Top"` row's own bottom edge
7855
+ * at `y=138.1` while its `"Bottom"` row's top edge measured `y=79.4` - rows
7856
+ * swapped order.
7857
+ *
7858
+ * The fix: the boundary BETWEEN two rows must be the SAME value regardless
7859
+ * of which row (or which glyph's own `u`) is asking, so it is computed from
7860
+ * the band deformed at a FIXED reference position
7861
+ * ({@link ROW_BOUNDARY_REFERENCE_U}, the box's own horizontal centre) rather
7862
+ * than each row's own actual `u`. Only a row's OUTER edge - the one facing
7863
+ * the box's own top (row 0's top) or bottom (the last row's bottom), never
7864
+ * shared with a neighbour - still bends with the curve at the glyph's real
7865
+ * `u`, preserving genuine per-glyph height variation there (the property
7866
+ * `text-warp-envelope-layout.test.ts`'s "places line 0 of 2 strictly above
7867
+ * line 1 of 2" and the scaleY-variation tests already pin). An interior row
7868
+ * (`lineCount > 2`, both edges shared with neighbours) gets a fixed band on
7869
+ * both sides; no fixture in this repo yet exercises three or more WordArt
7870
+ * paragraph rows, so this is the untested-but-consistent extension of the
7871
+ * same rule, not a separately-measured case.
7872
+ */
7873
+ function edgeBandAt(preset, u, adj, adj2, height, lineIndex, lineCount) {
7874
+ const actual = deformedBandAt(preset, u, adj, adj2, height);
7875
+ if (lineCount <= 1) {
7876
+ return sliceBand(actual.top, actual.bottom, 0, 1);
7877
+ }
7878
+ const reference = deformedBandAt(preset, ROW_BOUNDARY_REFERENCE_U, adj, adj2, height);
7879
+ const fixedSlice = sliceBand(reference.top, reference.bottom, lineIndex, lineCount);
7880
+ const isFirstRow = lineIndex <= 0;
7881
+ const isLastRow = lineIndex >= lineCount - 1;
7882
+ // An outer edge bending by its FULL deviation from the flat (undeformed)
7883
+ // band gives a row roughly `lineCount` TIMES the vertical scale its own
7884
+ // (`1/lineCount`-narrowed) nominal source band (`buildGlyphEnvelope`'s
7885
+ // `sliceBand(..., safeLineIndex, safeLineCount)` of
7886
+ // `NOMINAL_ENVELOPE_BAND`) was sized for, because the row's target band
7887
+ // still deforms by the WHOLE box's curve amplitude, not its own narrower
7888
+ // share of it (COM review 2026-09-11: a two-row `textInflate` fixture
7889
+ // measured a `d` vertical-scale term of ~3.27 for its first row at a
7890
+ // bulging u, stretching a 3-glyph "Top" caption's descender far enough to
7891
+ // invade the second row's territory even after the shared-boundary fix
7892
+ // alone; even scaled by `1/lineCount` here, a deep enough descender/
7893
+ // ascender can still extrapolate past the shared boundary - a
7894
+ // COM-unverified residual left open below). Damping the OUTER edge's OWN
7895
+ // deviation from the flat band by `1/lineCount` keeps a row's bend
7896
+ // proportional to its own narrowed share of the box, matching how its
7897
+ // nominal band was narrowed the same way - a multi-row block still
7898
+ // visibly bends (the deviation is not zeroed, just scaled), without a
7899
+ // `lineCount`-fold excess.
7900
+ const flatTop = NOMINAL_ENVELOPE_BAND.top * height;
7901
+ const flatBottom = NOMINAL_ENVELOPE_BAND.bottom * height;
7902
+ const dampedOuterTop = flatTop + (actual.top - flatTop) / lineCount;
7903
+ const dampedOuterBottom = flatBottom + (actual.bottom - flatBottom) / lineCount;
7904
+ // A first/last row's own OUTER edge bends (damped, see above); only the
7905
+ // shared INNER boundary ever comes from `fixedSlice`.
7906
+ return {
7907
+ top: isFirstRow ? dampedOuterTop : fixedSlice.top,
7908
+ bottom: isLastRow ? dampedOuterBottom : fixedSlice.bottom,
7909
+ };
7707
7910
  }
7708
7911
  /**
7709
7912
  * Affine `matrix(1 b 0 d 0 f)` mapping a glyph's nominal (undeformed) band
@@ -7793,7 +7996,81 @@ function glyphEnvelopeMatrix(x0, x1, edge0, edge1, nomTop, nomBottom) {
7793
7996
  * file, e.g. a signature font on the reader's OS with no webfont match),
7794
7997
  * `text-warp-envelope-layout.ts` falls back to the existing per-glyph affine
7795
7998
  * / piecewise-affine-slice transform, unchanged.
7796
- */
7999
+ *
8000
+ * COM-verified 2026-09-11 (an 8-shape Arimo Bold fixture, `textCanUp`/
8001
+ * `textCanDown`/`textInflate`/`textDeflate` at default and extreme `adj`): an
8002
+ * outline-vs-PowerPoint ink-scan comparison found a large interior-column
8003
+ * mismatch (~30-40% of box height, max 58-80%) that traced NOT to this
8004
+ * module's point-mapping (verified correct: the affine fallback, driven by
8005
+ * the identical inputs, showed the same error to within measurement noise),
8006
+ * but to `text-warp-envelope-layout.ts`'s `nomTop`/`nomBottom` - the
8007
+ * "undeformed" reference band both this module and the affine path map a
8008
+ * glyph's points FROM - being a fixed fraction of box height regardless of
8009
+ * the actual text's real (font-metric) size. See
8010
+ * `measureLineAscent`'s doc comment there for the fix and the re-measured
8011
+ * numbers. A separate, larger, NOT-yet-fixed gap the same investigation
8012
+ * found: real PowerPoint spaces envelope-warped glyphs to fill the box's own
8013
+ * width edge-to-edge (`textCanUp`/`textCanDown` additionally non-uniformly,
8014
+ * cylinder-projection-like) rather than centring the text at its natural
8015
+ * advance width the way `measureGlyphAdvances`/`startX` do today - out of
8016
+ * scope for that fix, left as an open, separately-scoped issue.
8017
+ *
8018
+ * Open question, not root-caused: the same investigation's COM fixture had
8019
+ * to be rendered from a deck that embeds no font at all (`warp-outline-
8020
+ * noembed-clean.pptx`, Arimo installed as a Windows user font instead) -
8021
+ * PowerPoint refused to open an earlier variant of the SAME fixture that
8022
+ * embedded Arimo Bold as a `ppt/fonts/{guid}.fntdata` part (obfuscated per
8023
+ * ECMA-376 14.2.1, wired via `p:embeddedFontLst`/`embedTrueTypeFonts="1"`)
8024
+ * with error `0x808D1001`. Left as an open note for whoever next touches
8025
+ * embedded-font packaging or generates a COM fixture that needs one.
8026
+ */
8027
+ /**
8028
+ * Horizontally scale a glyph's outline commands around `originX` (its own
8029
+ * left edge, matching where {@link buildWarpedGlyphOutlinePathD}'s caller
8030
+ * positioned it): `newX = originX + (x - originX) * scale`. `scale === 1` is
8031
+ * a no-op that returns `commands` unchanged (no new array allocated).
8032
+ *
8033
+ * Used by `text-warp-envelope-layout.ts` to widen a glyph's actual outline
8034
+ * for the `inflate`/`deflate` envelope family, which PowerPoint stretches as
8035
+ * a literal 2D distortion (both glyph spacing AND glyph shape widen together
8036
+ * to fill the box). The `can` family does NOT get this: COM-measured
8037
+ * 2026-09-11 (an 8-shape Arimo Bold fixture), a `can` glyph's own ink width
8038
+ * matches its NATURAL (unstretched) width closely (interior span
8039
+ * ~15.6%-15.8% of box width measured vs. ~15.6% predicted unstretched,
8040
+ * vs. ~16.6% predicted if the glyph itself were widened too) - `can`'s
8041
+ * cylindrical metaphor spreads glyphs apart (wider gaps) without literally
8042
+ * stretching each glyph's own shape, unlike `inflate`/`deflate`'s rubber-
8043
+ * sheet distortion. See `text-warp-envelope-layout.ts`'s `buildGlyphEnvelope`
8044
+ * for where the two families' `shapeScale` diverge.
8045
+ */
8046
+ function scaleOutlineCommandsX(commands, originX, scale) {
8047
+ if (scale === 1) {
8048
+ return commands;
8049
+ }
8050
+ const sx = (x) => originX + (x - originX) * scale;
8051
+ return commands.map((cmd) => {
8052
+ switch (cmd.type) {
8053
+ case 'M':
8054
+ case 'L':
8055
+ return { type: cmd.type, x: sx(cmd.x), y: cmd.y };
8056
+ case 'Q':
8057
+ return { type: 'Q', x1: sx(cmd.x1), y1: cmd.y1, x: sx(cmd.x), y: cmd.y };
8058
+ case 'C':
8059
+ return {
8060
+ type: 'C',
8061
+ x1: sx(cmd.x1),
8062
+ y1: cmd.y1,
8063
+ x2: sx(cmd.x2),
8064
+ y2: cmd.y2,
8065
+ x: sx(cmd.x),
8066
+ y: cmd.y,
8067
+ };
8068
+ case 'Z':
8069
+ default:
8070
+ return cmd;
8071
+ }
8072
+ });
8073
+ }
7797
8074
  /**
7798
8075
  * Map `y` (a point on the glyph's nominal, undeformed `[nomTop, nomBottom]`
7799
8076
  * band) into the envelope curve's own `[edgeTop, edgeBottom]` band at this
@@ -7988,10 +8265,12 @@ function buildGlyphSlices(preset, x0, x1, u0, u1, adj, adj2, height, lineIndex,
7988
8265
  const sliceX1 = x0 + ((x1 - x0) * (i + 1)) / n;
7989
8266
  const sliceU0 = u0 + ((u1 - u0) * i) / n;
7990
8267
  const sliceU1 = u0 + ((u1 - u0) * (i + 1)) / n;
8268
+ const e0 = edgeAt(sliceU0);
8269
+ const e1 = edgeAt(sliceU1);
7991
8270
  slices.push({
7992
8271
  clipX0: sliceX0 - (i === 0 ? 0 : SEAM_OVERLAP_PX),
7993
8272
  clipX1: sliceX1 + (i === n - 1 ? 0 : SEAM_OVERLAP_PX),
7994
- transform: glyphEnvelopeMatrix(sliceX0, sliceX1, edgeAt(sliceU0), edgeAt(sliceU1), nomTop, nomBottom),
8273
+ transform: glyphEnvelopeMatrix(sliceX0, sliceX1, e0, e1, nomTop, nomBottom),
7995
8274
  });
7996
8275
  }
7997
8276
  return slices;
@@ -8011,52 +8290,6 @@ function buildGlyphSlices(preset, x0, x1, u0, u1, adj, adj2, height, lineIndex,
8011
8290
  * React/Vue/Angular/Svelte/Vanilla, matching the framework-neutral
8012
8291
  * `WarpPathGenerator` shape the `'path'` family already uses.
8013
8292
  */
8014
- let measureCtx;
8015
- function getMeasureCtx$1() {
8016
- if (measureCtx !== undefined) {
8017
- return measureCtx;
8018
- }
8019
- if (typeof document === 'undefined') {
8020
- measureCtx = null;
8021
- return null;
8022
- }
8023
- measureCtx = document.createElement('canvas').getContext('2d');
8024
- return measureCtx;
8025
- }
8026
- function toCanvasFont$1(font) {
8027
- const size = font.fontSizePx && font.fontSizePx > 0 ? font.fontSizePx : DEFAULT_TEXT_FONT_SIZE;
8028
- const family = font.fontFamily || DEFAULT_FONT_FAMILY;
8029
- return `${font.italic ? 'italic ' : ''}${font.bold ? 'bold ' : ''}${size}px ${family}`;
8030
- }
8031
- /**
8032
- * Per-character advance widths for `text` set in `font`, measured as prefix
8033
- * differences (never a lone character: see `text-metric-tracking.ts`'s
8034
- * `advancesOf` for why - shaped scripts and ligatures need the context).
8035
- *
8036
- * Falls back to a flat `0.55em`-per-character estimate when there is no DOM
8037
- * to measure with (SSR, or a test environment without a 2D canvas context);
8038
- * the estimate only affects horizontal glyph spacing, never the envelope
8039
- * curve itself, so it stays visually reasonable even when approximate.
8040
- */
8041
- function measureGlyphAdvances(text, font) {
8042
- const chars = [...text];
8043
- const ctx = getMeasureCtx$1();
8044
- if (!ctx) {
8045
- const size = font.fontSizePx && font.fontSizePx > 0 ? font.fontSizePx : DEFAULT_TEXT_FONT_SIZE;
8046
- return chars.map(() => size * 0.55);
8047
- }
8048
- ctx.font = toCanvasFont$1(font);
8049
- const advances = [];
8050
- let previous = 0;
8051
- let prefix = '';
8052
- for (const char of chars) {
8053
- prefix += char;
8054
- const width = ctx.measureText(prefix).width;
8055
- advances.push(Math.max(0, width - previous));
8056
- previous = width;
8057
- }
8058
- return advances;
8059
- }
8060
8293
  function startX(align, width, lineWidth) {
8061
8294
  if (align === 'right') {
8062
8295
  return width - lineWidth;
@@ -8094,16 +8327,57 @@ function buildGlyphEnvelope(preset, segments, width, height, align, adj, adj2, l
8094
8327
  const safeLineIndex = Math.min(Math.max(0, Math.floor(lineIndex)), safeLineCount - 1);
8095
8328
  const perSegmentAdvances = segments.map((seg) => measureGlyphAdvances(seg.text, seg.font));
8096
8329
  const lineWidth = perSegmentAdvances.reduce((sum, advances) => sum + advances.reduce((s, w) => s + w, 0), 0);
8097
- const { top: nomTop, bottom: nomBottom } = sliceBand(height * NOMINAL_ENVELOPE_BAND.top, height * NOMINAL_ENVELOPE_BAND.bottom, safeLineIndex, safeLineCount);
8330
+ const { top: fixedBandTop, bottom: nomBottom } = sliceBand(height * NOMINAL_ENVELOPE_BAND.top, height * NOMINAL_ENVELOPE_BAND.bottom, safeLineIndex, safeLineCount);
8331
+ // Prefer the line's own real ink ascent over the fixed-fraction band (see
8332
+ // `measureLineAscent`'s doc comment for why): never LOWER than the fixed
8333
+ // band's top, so a line whose font already fills (or exceeds) the nominal
8334
+ // span keeps today's behaviour unchanged.
8335
+ const realAscent = measureLineAscent(segments);
8336
+ const nomTop = realAscent !== undefined ? Math.max(fixedBandTop, nomBottom - realAscent) : fixedBandTop;
8337
+ // PowerPoint spaces envelope-warped glyphs edge to edge across the box's
8338
+ // own width, rather than centring the line at its natural (unstretched)
8339
+ // advance width the way `startX`/`measureGlyphAdvances` did before this
8340
+ // fix (COM-measured 2026-09-11, an 8-shape Arimo Bold fixture: measured
8341
+ // ink spans ~99.9% of box width for BOTH the `can` and `inflate`/
8342
+ // `deflate` families). `stretch` is the uniform factor (box width /
8343
+ // natural line width) that reproduces the measured glyph PITCH closely
8344
+ // (interior boundary positions within ~1-3% of box width of COM ground
8345
+ // truth) for every glyph-envelope preset tested; every glyph's advance is
8346
+ // scaled by it, so the line always spans exactly `[0, width]`.
8347
+ //
8348
+ // Whether the glyph's own SHAPE also widens by `stretch` differs by
8349
+ // family though: `shapeScale` is `stretch` for `inflate`/`deflate` (and
8350
+ // the rest of the non-`can` envelope family) - COM-measured, their
8351
+ // per-glyph ink WIDTH scales with the stretch factor, matching a literal
8352
+ // rubber-sheet distortion where letters get visibly fatter. It is `1` for
8353
+ // `textCanUp`/`textCanDown` - their per-glyph ink width stays at its
8354
+ // NATURAL (unstretched) value; only the gaps between glyphs widen,
8355
+ // matching the "wrap around a cylinder" metaphor (letters keep their own
8356
+ // proportions, spaced further apart) rather than 2D stretching. Only
8357
+ // `shapeScale` reaches the OUTLINE render path (`scaleOutlineCommandsX`
8358
+ // below): the affine-fallback `transform`/`slices` path has no
8359
+ // horizontal-scale term by design (see `glyphEnvelopeMatrix`'s `a=1, c=0,
8360
+ // e=0` doc note), so it always fits the glyph's own NATURAL (unscaled)
8361
+ // width regardless of family - an accepted simplification for the
8362
+ // secondary (no-outline-available) path.
8363
+ const isCanFamily = preset === 'textCanUp' || preset === 'textCanDown';
8364
+ const stretch = width > 0 && lineWidth > 0 ? width / lineWidth : 1;
8365
+ const shapeScale = isCanFamily ? 1 : stretch;
8098
8366
  const placements = [];
8099
- let x = startX(align, width, lineWidth);
8367
+ let x = lineWidth > 0 ? 0 : startX(align, width, lineWidth);
8100
8368
  segments.forEach((segment, segIdx) => {
8101
8369
  const chars = [...segment.text];
8102
8370
  const advances = perSegmentAdvances[segIdx];
8103
8371
  chars.forEach((char, i) => {
8104
- const glyphWidth = advances[i] ?? 0;
8372
+ const naturalGlyphWidth = advances[i] ?? 0;
8373
+ const pitch = naturalGlyphWidth * stretch;
8105
8374
  const x0 = x;
8106
- const x1 = x + glyphWidth;
8375
+ // The affine-fit extent always uses the NATURAL (unscaled) width:
8376
+ // the affine/slice path can only ever render a glyph at its own
8377
+ // natural on-screen width (no horizontal-scale term available), so
8378
+ // fitting the curve across a wider span than what actually renders
8379
+ // would reintroduce the very mismatch this fix closes.
8380
+ const x1 = x0 + naturalGlyphWidth;
8107
8381
  const u0 = width > 0 ? x0 / width : 0.5;
8108
8382
  const u1 = width > 0 ? x1 / width : 0.5;
8109
8383
  const edge0 = edgeBandAt(preset, u0, adj, adj2, height, safeLineIndex, safeLineCount);
@@ -8111,7 +8385,10 @@ function buildGlyphEnvelope(preset, segments, width, height, align, adj, adj2, l
8111
8385
  // Outline warping takes priority when the caller can supply the
8112
8386
  // glyph's real outline: it is exact, so the affine fit (and its
8113
8387
  // piecewise-slice fallback) is only worth computing when it can't.
8114
- const outlineCommands = getGlyphOutline?.(char, segment.font, x, nomBottom);
8388
+ const rawOutline = getGlyphOutline?.(char, segment.font, x0, nomBottom);
8389
+ const outlineCommands = rawOutline
8390
+ ? scaleOutlineCommandsX(rawOutline, x0, shapeScale)
8391
+ : undefined;
8115
8392
  const outlinePath = outlineCommands
8116
8393
  ? buildWarpedGlyphOutlinePathD(outlineCommands, preset, width, height, nomTop, nomBottom, adj, adj2, safeLineIndex, safeLineCount)
8117
8394
  : undefined;
@@ -8121,7 +8398,7 @@ function buildGlyphEnvelope(preset, segments, width, height, align, adj, adj2, l
8121
8398
  placements.push({
8122
8399
  char,
8123
8400
  segmentIndex: segment.segmentIndex,
8124
- x,
8401
+ x: x0,
8125
8402
  y: nomBottom,
8126
8403
  transform: glyphEnvelopeMatrix(x0, x1, edge0, edge1, nomTop, nomBottom),
8127
8404
  slices: sliceCount > 1
@@ -8129,15 +8406,11 @@ function buildGlyphEnvelope(preset, segments, width, height, align, adj, adj2, l
8129
8406
  : undefined,
8130
8407
  outlinePath,
8131
8408
  });
8132
- x += glyphWidth;
8409
+ x += pitch;
8133
8410
  });
8134
8411
  });
8135
8412
  return placements;
8136
8413
  }
8137
- /** Test hook: forget the cached measurement context. */
8138
- function resetGlyphEnvelopeMeasureCache() {
8139
- measureCtx = undefined;
8140
- }
8141
8414
 
8142
8415
  /**
8143
8416
  * embedded-fonts.ts: Pure (no DOM-injection) helpers for the embedded-font
@@ -38133,6 +38406,147 @@ function isBevelProfileInverted(bevelType) {
38133
38406
  return bevelType === 'softRound';
38134
38407
  }
38135
38408
 
38409
+ /**
38410
+ * `a:bevelT/@prst` profile -> SVG height-map shape.
38411
+ *
38412
+ * Split out of `visual-3d-bevel-lighting-tables.ts` to keep both files under
38413
+ * the repo's ~300 LOC guideline; see that module's doc comment for the other
38414
+ * two axes (light rig elevation, material response) and the highlight
38415
+ * DIRECTION, which is resolved separately in `visual-3d-bevel-light.ts`.
38416
+ *
38417
+ * @module render/visual-3d-bevel-lighting-profile
38418
+ */
38419
+ /**
38420
+ * `a:bevelT/@prst` (and `bevelB`, which shares the same profile vocabulary)
38421
+ * -> height-map shape. ECMA-376 20.1.10.9 describes each profile's silhouette
38422
+ * (a "circular", "flat sloped", "crossed", "art-deco stepped" etc.
38423
+ * cross-section); these factors were ORIGINALLY grouped into 3
38424
+ * physically-motivated buckets by that description (curved / faceted /
38425
+ * steep-narrow) rather than 12 independent hand-tuned entries, reasoned from
38426
+ * ECMA-376 alone.
38427
+ *
38428
+ * COM-MEASURED 2026-09 (real PowerPoint `Slide.Export`, mid-grey `matte`
38429
+ * square, `threePt` rig / `dir="t"`, `orthographicFront` camera, both a 6pt
38430
+ * and a 24pt `a:bevelT`, a line of 40 brightness samples from the top edge
38431
+ * inward for all 12 profiles): fitting these SAME 3 factors (grid search,
38432
+ * minimum RMSE against the measured curve, both depths jointly) against real
38433
+ * cross-section data overturned the bucket story for 3 profiles.
38434
+ * `circle`/`convex`/`softRound`/`divot` (curved) and `angle`/`cross`/
38435
+ * `coolSlant`/`riblet`/`artDeco` (faceted) fit closely (RMSE 1-7 brightness
38436
+ * units) with factors in the same rough range as the original reasoning, so
38437
+ * their bucket membership held up. `relaxedInset`, `slope` and `hardEdge`
38438
+ * did NOT: all three measured a genuine BRIGHT-BUMP-THEN-DARK-TROUGH double
38439
+ * transition partway through the ramp (e.g. `hardEdge` at 24pt: baseline 133
38440
+ * -> peaks ~139 -> drops to 67 -> recovers), which this filter's single
38441
+ * monotonic blur(+erode) height map (one bell-shaped slope lobe) cannot
38442
+ * reproduce - the fit pushes `surfaceScaleFactor` to the largest tested
38443
+ * value trying to reach the trough depth, landing the LARGEST relief factor
38444
+ * of any profile, the opposite of the pre-2026-09 "slope/hardEdge are
38445
+ * low-relief" assumption (`slope`/`hardEdge` were previously reasoned as
38446
+ * "steep/narrow" with REDUCED relief; `relaxedInset` was previously grouped
38447
+ * as "curved" with full relief and no erode at all). Their factors below are
38448
+ * therefore the closest achievable fit within this 3-parameter chain, not a
38449
+ * claim of a clean match (RMSE 12-19, versus 1-7 for the other 9); a proper
38450
+ * fix needs a genuinely non-monotonic (two-lobe) height-map primitive chain,
38451
+ * out of scope for this pass - see `docs/guide/limitations.md`. The
38452
+ * direction-independence these three still show (`measuredUniform`) is
38453
+ * unaffected: it is a separate, already-COM-confirmed finding (see
38454
+ * `visual-3d-bevel-light.ts`'s module doc comment) about which CARDINAL EDGE
38455
+ * lights up, not about the cross-section ramp shape this campaign measures.
38456
+ * Scripts (scratch, not committed, same convention as `com-acceptance.mjs`):
38457
+ * `scripts/make-bevel-profile-fixture.mjs` (fixture, all 12
38458
+ * profiles x 2 depths), `scripts/measure-bevel-profile-com.ps1`
38459
+ * (COM export + 40-point sampler), `scripts/fit-bevel-profile-com.mjs`
38460
+ * (grid-search fit; a closed-form Gaussian-CDF reimplementation of the
38461
+ * primitive chain, not a headless-browser render - Playwright's Chromium
38462
+ * launch hangs indefinitely via a plain script in this environment, though
38463
+ * `bunx playwright test` itself works fine, used to independently verify the
38464
+ * metal/circle routing conclusion in `visual-3d-bevel-lighting-routing.ts`).
38465
+ * The raw 10-point-per-profile table (24pt depth) is pinned in
38466
+ * `visual-3d-bevel-lighting-tables.test.ts`; the full 40-point x 2-depth
38467
+ * table is in the task report.
38468
+ */
38469
+ const BEVEL_PROFILE_HEIGHT_MAP = {
38470
+ circle: { blurFactor: 0.35, surfaceScaleFactor: 0.65, measuredUniform: false },
38471
+ convex: {
38472
+ blurFactor: 0.18,
38473
+ morphologyFactor: 0.4,
38474
+ surfaceScaleFactor: 0.2,
38475
+ measuredUniform: false,
38476
+ },
38477
+ softRound: {
38478
+ blurFactor: 0.25,
38479
+ morphologyFactor: 0.4,
38480
+ surfaceScaleFactor: 0.65,
38481
+ measuredUniform: false,
38482
+ },
38483
+ relaxedInset: {
38484
+ blurFactor: 0.35,
38485
+ morphologyFactor: 0.5,
38486
+ surfaceScaleFactor: 1.5,
38487
+ measuredUniform: false,
38488
+ },
38489
+ divot: { blurFactor: 0.18, surfaceScaleFactor: 0.5, measuredUniform: false },
38490
+ angle: {
38491
+ blurFactor: 0.35,
38492
+ morphologyFactor: 0.4,
38493
+ surfaceScaleFactor: 0.35,
38494
+ measuredUniform: false,
38495
+ },
38496
+ cross: {
38497
+ blurFactor: 0.12,
38498
+ morphologyFactor: 0.18,
38499
+ surfaceScaleFactor: 0.2,
38500
+ measuredUniform: false,
38501
+ },
38502
+ coolSlant: {
38503
+ blurFactor: 0.25,
38504
+ morphologyFactor: 0.06,
38505
+ surfaceScaleFactor: 0.5,
38506
+ measuredUniform: false,
38507
+ },
38508
+ riblet: {
38509
+ blurFactor: 0.25,
38510
+ surfaceScaleFactor: 0.5,
38511
+ measuredUniform: false,
38512
+ },
38513
+ artDeco: {
38514
+ blurFactor: 0.18,
38515
+ morphologyFactor: 0.32,
38516
+ surfaceScaleFactor: 0.35,
38517
+ measuredUniform: false,
38518
+ },
38519
+ // `relaxedInset`/`slope`/`hardEdge` (see this table's doc comment): COM
38520
+ // measured a genuine BRIGHT-BUMP-THEN-DARK-TROUGH double transition for
38521
+ // all three, which a single monotonic blur(+erode) ramp cannot reproduce
38522
+ // (its height field has one bell-shaped slope lobe, so the diffuse/
38523
+ // specular response can only rise-then-settle, never rise-then-undershoot-
38524
+ // then-settle). These factors are the closest achievable fit within the
38525
+ // existing 3-parameter primitive chain (the grid search pushed
38526
+ // `surfaceScaleFactor` to its upper bound trying to reach the measured
38527
+ // trough depth), not a claim of a clean match; see the doc comment.
38528
+ slope: {
38529
+ blurFactor: 0.55,
38530
+ morphologyFactor: 0.5,
38531
+ surfaceScaleFactor: 1.5,
38532
+ measuredUniform: true,
38533
+ },
38534
+ hardEdge: {
38535
+ blurFactor: 0.55,
38536
+ morphologyFactor: 0.5,
38537
+ surfaceScaleFactor: 1.5,
38538
+ measuredUniform: true,
38539
+ },
38540
+ };
38541
+ const DEFAULT_HEIGHT_MAP = {
38542
+ blurFactor: 0.4,
38543
+ surfaceScaleFactor: 0.8,
38544
+ measuredUniform: false,
38545
+ };
38546
+ function getBevelProfileHeightMap(bevelType) {
38547
+ return BEVEL_PROFILE_HEIGHT_MAP[bevelType] ?? DEFAULT_HEIGHT_MAP;
38548
+ }
38549
+
38136
38550
  /**
38137
38551
  * Material -> `feDiffuseLighting`/`feSpecularLighting` response table.
38138
38552
  *
@@ -38165,12 +38579,26 @@ const white = '#ffffff';
38165
38579
  * .ts`'s module doc table) found this module's `metal` numbers give a mixed
38166
38580
  * result: `matte` improved clearly over the old box-shadow approach (56.3 ->
38167
38581
  * 34.8 mean error) while `metal` did not (61.2 -> 59.7, and `metal`/`circle`
38168
- * specifically got WORSE, 54.9 -> 80.1) - the constants below are therefore
38169
- * flagged as UNVALIDATED for `metal` specifically, not just "less precisely
38170
- * calibrated" than `matte`. The other materials are positioned between/
38171
- * around the `matte`/`metal` anchors by category (glossy plastics near
38172
- * `metal` but softer, matte/powder variants near `matte`), not independently
38173
- * COM-measured at all.
38582
+ * specifically got WORSE, 54.9 -> 80.1) - the constants below were therefore
38583
+ * flagged UNVALIDATED for `metal`, and `metal`/`circle` was ROUTED to the
38584
+ * legacy box-shadow model (`visual-3d-bevel-lighting-routing.ts`). The other
38585
+ * materials are positioned between/around the `matte`/`metal` anchors by
38586
+ * category (glossy plastics near `metal` but softer, matte/powder variants
38587
+ * near `matte`), not independently COM-measured at all.
38588
+ *
38589
+ * RESOLVED for `metal`/`circle` (2026-09, the bevel-profile cross-section
38590
+ * campaign, `visual-3d-bevel-lighting-tables.ts`'s `BEVEL_PROFILE_HEIGHT_MAP`
38591
+ * doc comment): re-fitting `circle`'s profile-table entry against real COM
38592
+ * cross-section data (unrelated to material tuning) changed
38593
+ * `surfaceScaleFactor` from 1 to 0.65, and with these SAME `metal` constants
38594
+ * (unchanged from the paragraph above) that alone brought `metal`/`circle`'s
38595
+ * mean error to 39.5 against the SAME 54.9 baseline - confirmed against
38596
+ * fresh COM ground truth AND an actual headless-Chromium rasterisation of
38597
+ * the real filter chain (not just the closed-form model this module's own
38598
+ * campaigns otherwise used), so `metal`/`circle` no longer routes; see
38599
+ * `visual-3d-bevel-lighting-routing.ts`'s doc comment for the numbers and a
38600
+ * separate, NOT-landed attempt at fixing the flat-interior specular
38601
+ * saturation defect below.
38174
38602
  */
38175
38603
  const MATERIAL_LIGHTING = {
38176
38604
  matte: {
@@ -38247,10 +38675,14 @@ const MATERIAL_LIGHTING = {
38247
38675
  // (`angle`: baseline ~70, now ~19-27; `hardEdge`: baseline ~69.5, now 44;
38248
38676
  // `softRound`: baseline ~50, now 6.5-19). `circle` alone could NOT be
38249
38677
  // brought below baseline with any tested combination of these four
38250
- // parameters (tried down to `surfaceScaleMultiplier` 0.15 and up to 2.5;
38251
- // see `getBevelLightingFilterMarkup`'s `LEGACY_BEVEL_ROUTING` doc comment
38252
- // for why) and is routed to the legacy `box-shadow` model instead of
38253
- // shipping a regression.
38678
+ // MATERIAL parameters (tried down to `surfaceScaleMultiplier` 0.15 and up
38679
+ // to 2.5) against the then-current (ECMA-376-reasoned) profile table, and
38680
+ // was routed to the legacy `box-shadow` model instead of shipping a
38681
+ // regression. SUPERSEDED 2026-09: re-fitting the PROFILE table's own
38682
+ // `circle` entry against real COM data (a change orthogonal to these
38683
+ // material constants) resolved it without touching the numbers below; see
38684
+ // this file's module doc comment and `visual-3d-bevel-lighting-routing
38685
+ // .ts`.
38254
38686
  //
38255
38687
  // A SEPARATE, more severe issue was found (2026-09, while attempting an
38256
38688
  // ambient/wrap-term fix for the `circle` routing above) that this
@@ -38347,9 +38779,12 @@ function getMaterialLighting(material) {
38347
38779
  * Split out of `visual-3d-bevel-lighting.ts` to keep that module under the
38348
38780
  * repo's ~300 LOC guideline. Three independent axes feed the filter this
38349
38781
  * module's data drives: bevel PROFILE shape (`a:bevelT/@prst`, ECMA-376
38350
- * 20.1.10.9 `ST_BevelPresetType`), light rig ELEVATION/specular character
38351
- * (`a:lightRig/@rig`, ECMA-376 20.1.10.36 `ST_LightRigType`), and MATERIAL
38352
- * response (`a:sp3d/@prstMaterial`, ECMA-376 20.1.10.50 `ST_PresetMaterialType`).
38782
+ * 20.1.10.9 `ST_BevelPresetType`, re-exported here but defined in
38783
+ * `visual-3d-bevel-lighting-profile.ts`, itself split out for the same LOC
38784
+ * reason), light rig ELEVATION/specular character (`a:lightRig/@rig`,
38785
+ * ECMA-376 20.1.10.36 `ST_LightRigType`, defined below), and MATERIAL
38786
+ * response (`a:sp3d/@prstMaterial`, ECMA-376 20.1.10.50 `ST_PresetMaterialType`,
38787
+ * re-exported from `visual-3d-bevel-lighting-material.ts`).
38353
38788
  * The highlight/shadow DIRECTION (azimuth) itself is mostly resolved
38354
38789
  * elsewhere: `visual-3d-bevel-light`'s already COM-measured cardinal-snap
38355
38790
  * vector supplies the base azimuth from `a:lightRig/@dir`, and this module's
@@ -38374,80 +38809,6 @@ function getMaterialLighting(material) {
38374
38809
  *
38375
38810
  * @module render/visual-3d-bevel-lighting-tables
38376
38811
  */
38377
- /**
38378
- * `a:bevelT/@prst` (and `bevelB`, which shares the same profile vocabulary)
38379
- * -> height-map shape. ECMA-376 20.1.10.9 describes each profile's silhouette
38380
- * (a "circular", "flat sloped", "crossed", "art-deco stepped" etc. cross-
38381
- * section); this table groups the 12 values by that description into 3
38382
- * physically-motivated buckets rather than 12 independent hand-tuned entries:
38383
- *
38384
- * - **Curved** (`circle`, `convex`, `softRound`, `relaxedInset`, `divot`):
38385
- * a smooth, rounded cross-section -> wide Gaussian-only ramp, full relief.
38386
- * - **Faceted** (`angle`, `cross`, `coolSlant`, `riblet`, `artDeco`): a flat
38387
- * angled facet with a visible crease -> a medium blur PLUS a light erode so
38388
- * the ramp gets a crisper inner edge (the crease), full relief.
38389
- * - **Steep/narrow** (`slope`, `hardEdge`): COM-measured to show no clean
38390
- * directional signal (see {@link BevelProfileHeightMap.measuredUniform});
38391
- * a narrow, heavily-eroded ramp with reduced relief reproduces that
38392
- * physically instead of guessing a highlight side.
38393
- */
38394
- const BEVEL_PROFILE_HEIGHT_MAP = {
38395
- circle: { blurFactor: 0.55, surfaceScaleFactor: 1, measuredUniform: false },
38396
- convex: { blurFactor: 0.6, surfaceScaleFactor: 1.05, measuredUniform: false },
38397
- softRound: { blurFactor: 0.5, surfaceScaleFactor: 0.9, measuredUniform: false },
38398
- relaxedInset: { blurFactor: 0.45, surfaceScaleFactor: 0.85, measuredUniform: false },
38399
- divot: { blurFactor: 0.4, surfaceScaleFactor: 0.8, measuredUniform: false },
38400
- angle: {
38401
- blurFactor: 0.32,
38402
- morphologyFactor: 0.12,
38403
- surfaceScaleFactor: 1,
38404
- measuredUniform: false,
38405
- },
38406
- cross: {
38407
- blurFactor: 0.3,
38408
- morphologyFactor: 0.15,
38409
- surfaceScaleFactor: 0.95,
38410
- measuredUniform: false,
38411
- },
38412
- coolSlant: {
38413
- blurFactor: 0.28,
38414
- morphologyFactor: 0.14,
38415
- surfaceScaleFactor: 0.95,
38416
- measuredUniform: false,
38417
- },
38418
- riblet: {
38419
- blurFactor: 0.26,
38420
- morphologyFactor: 0.18,
38421
- surfaceScaleFactor: 0.9,
38422
- measuredUniform: false,
38423
- },
38424
- artDeco: {
38425
- blurFactor: 0.24,
38426
- morphologyFactor: 0.2,
38427
- surfaceScaleFactor: 1,
38428
- measuredUniform: false,
38429
- },
38430
- slope: {
38431
- blurFactor: 0.12,
38432
- morphologyFactor: 0.35,
38433
- surfaceScaleFactor: 0.4,
38434
- measuredUniform: true,
38435
- },
38436
- hardEdge: {
38437
- blurFactor: 0.1,
38438
- morphologyFactor: 0.4,
38439
- surfaceScaleFactor: 0.35,
38440
- measuredUniform: true,
38441
- },
38442
- };
38443
- const DEFAULT_HEIGHT_MAP = {
38444
- blurFactor: 0.4,
38445
- surfaceScaleFactor: 0.8,
38446
- measuredUniform: false,
38447
- };
38448
- function getBevelProfileHeightMap(bevelType) {
38449
- return BEVEL_PROFILE_HEIGHT_MAP[bevelType] ?? DEFAULT_HEIGHT_MAP;
38450
- }
38451
38812
  /**
38452
38813
  * `a:lightRig/@rig` -> elevation/sharpness/direction. COM-CALIBRATED
38453
38814
  * (2026-09, real PowerPoint `Slide.Export`, mid-grey #808080 1.4in square,
@@ -38638,7 +38999,7 @@ function resolveLayer(index, bevelType, widthEmu, heightEmu, isBottom, scene, ma
38638
38999
 
38639
39000
  /**
38640
39001
  * Legacy `box-shadow` routing for a `material`/profile combination the SVG
38641
- * lighting filter cannot yet beat.
39002
+ * lighting filter cannot beat.
38642
39003
  *
38643
39004
  * Split out of `visual-3d-bevel-lighting.ts` to keep that file under the
38644
39005
  * repo's ~300 LOC guideline.
@@ -38646,26 +39007,45 @@ function resolveLayer(index, bevelType, widthEmu, heightEmu, isBottom, scene, ma
38646
39007
  * @module render/visual-3d-bevel-lighting-routing
38647
39008
  */
38648
39009
  /**
38649
- * `material|profile` pairs that measured WORSE than the legacy `box-shadow`
38650
- * approach even after calibration (see `visual-3d-bevel-lighting-material
38651
- * .ts`'s module doc comment for the numbers) and therefore route to the
38652
- * legacy model instead of shipping a regression. Currently just
38653
- * `metal|circle`: a grid search over `diffuseConstant`/`specularConstant`/
38654
- * `specularExponent`/`surfaceScaleMultiplier` (2026-09, the same real
38655
- * render-vs-COM pipeline used throughout this module) could not find ANY
38656
- * combination bringing `metal`/`circle`'s mean error at or below the
38657
- * box-shadow baseline in any of the 4 `a:lightRig/@dir` values tested:
38658
- * `feDiffuseLighting`'s `N.L<=0` clamp-to-black on the shadow side is
38659
- * structural to this primitive chain (independent of `diffuseConstant`'s
38660
- * magnitude, which only scales the LIT side), and reducing `surfaceScale`
38661
- * enough to lift the clamped shadow side toward COM's measured ~178/255
38662
- * pulls the highlight side down away from its own accurate ~221/255 reading
38663
- * faster than it helps - the two targets cannot both be reached with these
38664
- * four parameters for this specific profile/material pair. A real fix needs
38665
- * an ambient/floor term this primitive chain does not have; out of scope for
38666
- * this pass.
38667
- */
38668
- const LEGACY_BEVEL_ROUTING = new Set(['metal|circle']);
39010
+ * `material|profile` pairs that measure WORSE than the legacy `box-shadow`
39011
+ * approach and therefore route to the legacy model instead of shipping a
39012
+ * regression. Empty as of the 2026-09 bevel-profile-cross-section campaign
39013
+ * (`visual-3d-bevel-lighting-tables.ts`'s `BEVEL_PROFILE_HEIGHT_MAP` doc
39014
+ * comment): `metal|circle` was the one routed pair (a grid search over
39015
+ * `diffuseConstant`/`specularConstant`/`specularExponent`/
39016
+ * `surfaceScaleMultiplier` against the OLD, ECMA-376-reasoned profile table
39017
+ * could not bring it at or below baseline in any direction - see this file's
39018
+ * git history for that campaign's numbers). Re-fitting `circle`'s
39019
+ * `BEVEL_PROFILE_HEIGHT_MAP` entry against real COM cross-section data
39020
+ * (`surfaceScaleFactor` 1 -> 0.65) changed the balance enough that the
39021
+ * UNCHANGED material constants now beat the box-shadow baseline in every
39022
+ * `a:lightRig/@dir`, confirmed two ways: fresh COM ground truth (mid-grey
39023
+ * `circle`/metal square, 24pt bevel, `threePt` rig, all 4 directions,
39024
+ * 0.15in-from-edge highlight+shadow sampling, script
39025
+ * `scripts/make-bevel-material-fixture.mjs` +
39026
+ * `measure-bevel-material-com.ps1`) and an ACTUAL headless-Chromium
39027
+ * rasterisation of the real filter primitive chain (a one-off Playwright
39028
+ * spec, not committed) sampled the same points: mean absolute error 39.5
39029
+ * (baseline was 54.9). The same real-browser check also re-confirmed
39030
+ * `angle`/`hardEdge`/`softRound` still beat their baselines (41.5/62.0/27.5
39031
+ * against baselines ~70/~69.5/~50) with the new profile table, so nothing
39032
+ * newly regressed.
39033
+ *
39034
+ * A candidate fix for the SEPARATE flat-interior specular-saturation defect
39035
+ * (masking `feSpecularLighting`'s contribution to the actual curved bevel
39036
+ * band via a `feComponentTransfer`/`feColorMatrix` triangle-of-height mask,
39037
+ * `1 - |2*height-1|`, zero at the flat cap) was measured against the same
39038
+ * real-browser pipeline for all 4 profiles and made EVERY ONE of them worse,
39039
+ * often drastically (`circle` 39.5 -> 103.5, `angle` 41.5 -> 104.0,
39040
+ * `hardEdge` 62.0 -> 94.0, `softRound` 27.5 -> 89.0): the mask's `feFuncA
39041
+ * type="table" tableValues="0 1 0"` zeroes specular well before COM's real
39042
+ * highlight has decayed at the 0.15in sample offset (the mask, tuned only to
39043
+ * the height VALUE crossing 0.5, does not track where the actual specular
39044
+ * lobe sits for a high `specularExponent`), so this attempt was measured and
39045
+ * NOT landed. See `docs/guide/limitations.md` for the still-open
39046
+ * flat-interior saturation defect this was meant to fix.
39047
+ */
39048
+ const LEGACY_BEVEL_ROUTING = new Set();
38669
39049
  /**
38670
39050
  * Whether a `material`/`profile` combination is routed to the legacy
38671
39051
  * `box-shadow` bevel model instead of the SVG lighting filter.
@@ -38737,12 +39117,12 @@ function isRoutedToLegacyBevelShadow(material, profile) {
38737
39117
  * `metal`/`circle` below its baseline in any direction (the two targets pull
38738
39118
  * in opposite directions as `surfaceScale` changes - see
38739
39119
  * `visual-3d-bevel-lighting-routing.ts`'s `isRoutedToLegacyBevelShadow` doc
38740
- * comment), so `metal`/`circle` ROUTES to the legacy `box-shadow` model. The
38741
- * "before" numbers are themselves large because this campaign scores
38742
- * absolute brightness match, not just highlight/shadow SIGN agreement (which
38743
- * is all `getBevelShadow`'s box-shadow output was previously verified
38744
- * against). All scripts used are scratch tooling (not committed, not wired
38745
- * into CI, same as `com-acceptance.mjs`); full tables are in the task report.
39120
+ * comment), so `metal`/`circle` ROUTED to the legacy `box-shadow` model at
39121
+ * the time (SUPERSEDED 2026-09 below; it no longer routes). The "before"
39122
+ * numbers are large because this campaign scores absolute brightness match,
39123
+ * not just highlight/shadow SIGN agreement (all `getBevelShadow`'s
39124
+ * box-shadow output was previously verified against). Scripts: scratch
39125
+ * tooling, same convention as `com-acceptance.mjs`.
38746
39126
  *
38747
39127
  * ## Re-run against the CURRENT `threePt` elevationDeg (2026-09, post-lightRig-recalibration)
38748
39128
  *
@@ -38800,6 +39180,16 @@ function isRoutedToLegacyBevelShadow(material, profile) {
38800
39180
  * material against this same COM ground truth. Neither was completed in
38801
39181
  * this pass; see `docs/guide/limitations.md`.
38802
39182
  *
39183
+ * ## 2026-09 bevel-profile cross-section + specular-masking follow-up
39184
+ *
39185
+ * Kept in the files they most directly touch (LOC budget): the 12
39186
+ * `a:bevelT/@prst` height-map SHAPES were fit against real COM cross-section
39187
+ * curves for the first time (`visual-3d-bevel-lighting-tables.ts`'s doc +
39188
+ * pinned table in its `.test.ts`), changing `circle`'s `surfaceScaleFactor`
39189
+ * enough that `metal`/`circle` now beats baseline unrouted; a follow-up
39190
+ * specular-band-masking attempt at the saturation defect above was measured
39191
+ * and made things WORSE (`visual-3d-bevel-lighting-routing.ts`'s doc).
39192
+ *
38803
39193
  * @module render/visual-3d-bevel-lighting
38804
39194
  */
38805
39195
  /**
@@ -39236,19 +39626,13 @@ function applyHomography(h, p) {
39236
39626
  *
39237
39627
  * 1. The shape's flat picture plane is a unit square in its own local XY
39238
39628
  * plane (`z=0`), corners at `(-0.5,-0.5) .. (0.5,0.5)`.
39239
- * 2. Each corner is projected by {@link projectCorner}: a PRIMARY per-axis
39240
- * orthographic cosine foreshortening (`lon` shrinks width, `lat` shrinks
39241
- * height), COM-validated for a single-axis rotation (see below), plus a
39242
- * SECONDARY genuine pinhole perspective skew that activates only for a
39243
- * combined (both axes nonzero) pose - see that function's own doc comment
39244
- * for why a naive single pinhole projection is the WRONG primary model
39245
- * here, unlike the preset table's own two-axis families.
39246
- * 3. The pinhole secondary term's focal length is `f = 1/tan(fov/2)` (the
39247
- * same FOV <-> perspective-distance relationship `visual-3d-camera-fov`
39248
- * already uses), so `lat=lon=rev=0` reproduces an EXACT identity
39249
- * homography - the same trivial case `orthographicFront` is COM-measured
39250
- * to produce - by construction (the secondary term is architecturally
39251
- * zero whenever either axis is zero, so this holds regardless of `fov`).
39629
+ * 2. Each corner is projected by {@link projectCorner}: an ORTHOGRAPHIC
39630
+ * (parallel, no perspective divide) rotation-composition transform - see
39631
+ * that function's own doc comment for the exact formula and its
39632
+ * derivation.
39633
+ * 3. `lat=lon=rev=0` reproduces an EXACT identity homography - the same
39634
+ * trivial case `orthographicFront` is COM-measured to produce - by
39635
+ * construction (`cos(0)=1`, every other term vanishes).
39252
39636
  * 4. `rev` (roll about the view axis) commutes with the projection: rolling
39253
39637
  * the camera about its own aim axis is exactly a 2D rotation of the
39254
39638
  * already-projected image, applied here as a post-projection step rather
@@ -39259,88 +39643,96 @@ function applyHomography(h, p) {
39259
39643
  * `visual-3d-camera-homography`'s existing `homographyToMatrix3d`
39260
39644
  * embedding unchanged.
39261
39645
  *
39262
- * ## COM validation (2026-09, real PowerPoint `Slide.Export`, 144px/in)
39646
+ * ## COM validation, round 2: the 27-point lat x lon x rev grid (2026-09)
39647
+ *
39648
+ * The first campaign (single COM measurement per case, see history) found
39649
+ * the primary per-axis cosine scale exact for any single-axis `a:rot`, but a
39650
+ * damped pinhole-perspective "secondary term" (weighted by
39651
+ * `sin(lat)*sin(lon)`, FOV-dependent) under-predicted a genuinely combined
39652
+ * pose (`lat=35.26deg lon=45deg rev=45deg`) by ~25-29% relative corner error,
39653
+ * repeatably across two independent measurements. That secondary term is
39654
+ * REPLACED here, not patched: a fresh 27-point grid (`lat in {0, 25,
39655
+ * 35.26deg}` x `lon in {0, 25, 45deg}` x `rev in {0, 25, 45deg}`, including
39656
+ * both prior points exactly) was rendered via real PowerPoint COM
39657
+ * (`Slide.Export`, 144px/in, flat 2in `prst="orthographicFront"` + `a:rot`
39658
+ * squares) and each cell's 4 corners extracted by convex-hull fit (the
39659
+ * boundary/hull/quad-simplification method `visual-3d-camera-homography.ts`
39660
+ * already validated, NOT the fragile "4 extreme pixels" shortcut the first
39661
+ * campaign used, which silently mis-ordered corners for any near-45deg `rev`
39662
+ * by matching against UNDISTORTED reference positions - a large rotation's
39663
+ * true nearest axis-aligned corner is not its physical origin; fixed by
39664
+ * matching against a cosine-scale-plus-rev PRIOR position instead).
39665
+ *
39666
+ * Fitting the 27 measured cells against every hypothesis in the task brief
39667
+ * (Euler order lon-then-lat vs lat-then-lon vs the old damped-pinhole model;
39668
+ * orthographic vs a true perspective divide at the override's own FOV;
39669
+ * rotation about the shape centre - confirmed by near-zero centroid shift
39670
+ * uncorrelated with a cell's distance from the canvas centre, ruling out a
39671
+ * slide-centre pivot) found an EXACT closed form: keep `x` as the
39672
+ * already-validated pure cosine scale (COM-confirmed independent of `lat`:
39673
+ * the same `lon=45deg` cells produced identical `x` at `lat=25deg` and
39674
+ * `lat=35.26deg`), and add a rotation-composition cross term to `y` ONLY,
39675
+ * with a NEGATIVE sign relative to the naive `Ry(lon).Rx(lat)` composition
39676
+ * this module's first attempt used:
39677
+ *
39678
+ * ```
39679
+ * x = X * cos(lon)
39680
+ * y = Y * cos(lat) - X * sin(lat) * sin(lon)
39681
+ * ```
39263
39682
  *
39264
- * Three explicit `a:camera/a:rot` cases (a required `prst="orthographicFront"`
39265
- * plus an overriding `a:rot`, since real PowerPoint rejects a schema-invalid
39266
- * `a:camera` with no `@prst` at all - `CT_Camera`'s `prst` attribute turned
39267
- * out to be REQUIRED, contrary to what this codebase's own writer, which
39268
- * merges onto an already-`@prst`-bearing parsed node, implied was optional),
39269
- * a flat 2in square, corners extracted as the 4 extreme (min/max x/y) grey
39270
- * pixels - reliable here since none of the 3 cases roll far enough to turn
39271
- * the square into a diamond whose extremes are edge midpoints, the situation
39272
- * `visual-3d-camera-homography.ts`'s own campaign had to use a full
39273
- * convex-hull fit for:
39683
+ * Across all 27 grid cells (script: `gen-fixture.mjs` -> `measure.ps1` ->
39684
+ * `solve-corners.mjs` -> `fit-model.mjs`, scratch/one-off, not committed):
39685
+ * average max-corner error 0.61%, median well under 1%, worst 3 cells (all
39686
+ * `rev=25deg`, an "ugly" non-axis-aligned roll angle that maximises
39687
+ * antialiasing-boundary noise at a 288px-side element, not a systematic
39688
+ * lat/lon pattern) at 2.10% / 1.96% / 1.70% - see the raw per-cell table
39689
+ * below. This lands the combined case in the SAME ~1% band as the
39690
+ * single-axis cases, closing the ~25-29% gap the first campaign left open,
39691
+ * with NO fov/zoom dependency at all: the model is purely orthographic, so
39692
+ * `ParametricCameraParams.fovRad` is now unused by {@link projectCorner}
39693
+ * (kept in the type for API stability; `@fov`/`@zoom` were not
39694
+ * independently varied by this campaign, only held at their
39695
+ * `orthographicFront` default, so this does not claim they have no effect
39696
+ * under some other combination this grid did not cover).
39697
+ *
39698
+ * Raw per-cell max-corner error (fraction of the square's own side, sorted
39699
+ * worst-first; `lat=35.26` is the isometric angle `atan(1/sqrt(2))`, reusing
39700
+ * the first campaign's own combined-case angle set):
39274
39701
  *
39275
39702
  * ```
39276
- * case corner error (px, avg of 4, on a 288px-side element)
39277
- * lat=0 lon=0 rev=0 (sanity: == identity) 1.2
39278
- * lat=0 lon=25deg rev=0 (single-axis yaw) 0.75
39279
- * lat=35.26deg lon=45deg rev=45deg (combined + roll) 82 (29% relative)
39703
+ * lat25_lon0_rev25 2.098% lat25_lon0_rev0 0.390%
39704
+ * lat0_lon45_rev25 1.959% lat25_lon0_rev45 0.362%
39705
+ * lat35.26_lon45_rev25 1.696% lat35.26_lon0_rev45 0.353%
39706
+ * lat0_lon25_rev25 0.776% lat0_lon25_rev45 0.349%
39707
+ * lat25_lon25_rev25 0.756% lat35.26_lon0_rev25 0.347%
39708
+ * lat25_lon25_rev45 0.735% lat35.26_lon45_rev45 0.347%
39709
+ * lat0_lon0_rev45 0.669% lat25_lon45_rev0 0.342%
39710
+ * lat35.26_lon25_rev45 0.654% lat25_lon25_rev0 0.323%
39711
+ * lat35.26_lon25_rev25 0.585% lat35.26_lon45_rev0 0.312%
39712
+ * lat0_lon0_rev0 0.491% lat35.26_lon25_rev0 0.304%
39713
+ * lat0_lon45_rev0 0.450% lat25_lon45_rev25 0.292%
39714
+ * lat25_lon45_rev45 0.420% lat0_lon0_rev25 0.259%
39715
+ * lat0_lon45_rev45 0.404%
39716
+ * lat35.26_lon0_rev0 0.402% (avg 0.610%, max 2.098%)
39280
39717
  * ```
39281
39718
  *
39282
- * The identity and single-axis cases are sub-pixel accurate - well within the
39283
- * preset homography table's own ~0.7%-relative-error tolerance. The combined
39284
- * case is NOT: at this extreme (all three angles large and simultaneous) the
39285
- * primary cosine term plus the damped secondary skew above under-predicts the
39286
- * real distortion by roughly 29%, i.e. this module does NOT claim COM parity
39287
- * for a genuinely combined multi-axis override, only documents the measured
39288
- * gap. This is the same class of difficulty `visual-3d-camera.ts`'s own doc
39289
- * comment records for the PRESET two-axis families ("A centred `perspective`
39290
- * alone cannot fully reproduce the two-axis presets' off-axis camera... a
39291
- * genuine off-axis vanishing point"): PowerPoint's real camera formula for a
39292
- * combined pose is not fully reverse-engineered here either. What IS
39293
- * COM-established, and was previously entirely unverified (the old code used
39294
- * a `rotateX`/`rotateY` + centred CSS `perspective()` approximation for
39295
- * EVERY override, single-axis included): a pure single-axis `a:rot` is a
39296
- * symmetric per-axis scale with NO keystone and NO centre shift, which the
39297
- * old model could not represent either (it always keystones via
39298
- * `perspective()`). `lon`'s sign was independently isolated and COM-checked
39299
- * (a positive `lon` measured a symmetric width shrink, matching this
39300
- * module).
39301
- *
39302
- * `lat`'s sign is NOT independently observable from a single-axis case:
39303
- * `cos` is an even function, so this module's primary term produces the
39304
- * IDENTICAL homography for `lat=+25deg` and `lat=-25deg` in isolation (no
39305
- * `lon`) - proven analytically, and confirmed by a real `lat=25deg only`
39306
- * COM measurement (2026-09, same 2in-square/144px-in methodology) matching
39307
- * this module's prediction to within 1px on every one of the 4 measured
39308
- * corners (predicted top/bottom edge at y=56.7/317.7 vs measured 56/317,
39309
- * width unchanged both sides). Sign only becomes observable jointly with
39310
- * `lon` (the secondary term), which the combined case below already
39311
- * exercises; a single-axis case genuinely cannot add information here.
39312
- *
39313
- * `rev`'s sign WAS independently isolated: a real `rev=45deg only` COM
39314
- * measurement (lat=lon=0) produced a diamond-oriented square whose 4 extreme
39315
- * points matched this module's predicted corner-to-extreme mapping (which
39316
- * original corner becomes the new top/right/bottom/left vertex) for a
39317
- * POSITIVE `rev`, each within about 10 degrees of angle from the shape's own
39318
- * centre (a small, consistent systematic offset in the SAME rotational
39319
- * sense across all 4 points, not a sign flip) - this module's `rev` sign
39320
- * convention is therefore COM-confirmed, not merely architecturally
39321
- * plausible.
39322
- *
39323
- * A second, independent combined-case measurement (a fresh fixture, same
39324
- * lat=35.26/lon=45/rev=45 angles) reproduced the same ~25-29% relative
39325
- * corner error as the original campaign above (70.8px average this time, vs
39326
- * 82px originally, both on a 288px element) - confirming the combined-case
39327
- * residual is a real, repeatable limitation of this module's secondary term,
39328
- * not measurement noise from a single run. The fixture/export/pixel-sampling
39329
- * scripts used for all of this measurement were scratch, one-off tooling
39330
- * (not committed - see the task report for the methodology if reproducing).
39719
+ * `lon`'s sign was independently isolated and COM-checked in the first
39720
+ * campaign (a positive `lon` measured a symmetric width shrink, matching
39721
+ * this module) and is unaffected by the cross-term replacement (`x` is
39722
+ * unchanged). `lat`'s sign is not independently observable from a
39723
+ * single-axis case (`cos` is even) but IS observable jointly with `lon` via
39724
+ * the cross term; the 27-point grid's fit (rather than an isolated
39725
+ * combined-case check) is itself the confirmation this module's `lat` sign
39726
+ * convention is correct across the whole grid, not just one pose. `rev`'s
39727
+ * sign was independently isolated in the first campaign (a real `rev=45deg
39728
+ * only` measurement matched this module's predicted corner-to-extreme
39729
+ * mapping for a positive `rev`) and is reused unchanged here: it is still
39730
+ * applied as a simple post-projection 2D roll, and the fit above already
39731
+ * exercises every `rev` level jointly with every `lat`/`lon` combination
39732
+ * without needing a different composition order.
39331
39733
  *
39332
39734
  * @module render/visual-3d-camera-parametric
39333
39735
  */
39334
- function rotateX(v, angle) {
39335
- const c = Math.cos(angle);
39336
- const s = Math.sin(angle);
39337
- return { x: v.x, y: v.y * c - v.z * s, z: v.y * s + v.z * c };
39338
- }
39339
- function rotateY(v, angle) {
39340
- const c = Math.cos(angle);
39341
- const s = Math.sin(angle);
39342
- return { x: v.x * c + v.z * s, y: v.y, z: -v.x * s + v.z * c };
39343
- }
39344
39736
  function rotate2d(p, angle) {
39345
39737
  if (angle === 0) {
39346
39738
  return p;
@@ -39353,52 +39745,22 @@ function rotate2d(p, angle) {
39353
39745
  * Project one local unit-square corner `(x, y)` (already centred, y-up)
39354
39746
  * through the camera.
39355
39747
  *
39356
- * The PRIMARY term is an orthographic per-axis cosine foreshortening
39357
- * (`x *= cos(lon)`, `y *= cos(lat)`), not a full pinhole perspective divide:
39358
- * COM measurement (see the module doc comment) found a pure single-axis
39359
- * `a:rot` produces a symmetric scale with NO keystone and NO centre shift at
39360
- * all - matching this term to within ~1% - whereas a naive pinhole
39361
- * projection (translate the camera sideways, re-aim, divide by depth)
39362
- * predicts both a shift and a slant that COM does not show. This mirrors
39363
- * `visual-3d-camera-homography.ts`'s own finding #2 for the equivalent
39364
- * single-axis PRESET family (`perspectiveLeft`/`Right`/`Above`/`Below`):
39365
- * "a pure anisotropic scale + small offset", not a keystone.
39366
- *
39367
- * A SECONDARY genuine perspective skew (a real off-axis vanishing point, the
39368
- * pinhole formula's deviation from the cosine term) is blended in only when
39369
- * BOTH `lat` and `lon` are nonzero at once (weighted by `sin(lat)*sin(lon)`,
39370
- * which is exactly 0 for any single-axis rotation, so that COM-validated
39371
- * case is reproduced UNCHANGED). This mirrors the preset table's own
39372
- * two-axis families (`*Facing`/`Contrasting*`/`Heroic*`) genuinely needing a
39373
- * skew a pure scale cannot represent. `fov` modulates this secondary term's
39374
- * strength (a wider FOV -> a nearer, more exaggerated camera -> more
39375
- * foreshortening), the only place `@fov`/`@zoom` affect this model: no COM
39376
- * data varies FOV independently for an override, so treat this coupling as
39377
- * physically-motivated but NOT independently calibrated, unlike the
39378
- * COM-validated primary term.
39748
+ * `x` is a pure per-axis cosine foreshortening (`x = X*cos(lon)`), COM-
39749
+ * confirmed independent of `lat` (see the module doc comment): the 27-point
39750
+ * grid's `lon=45deg` cells produced the identical `x` at both `lat=25deg`
39751
+ * and `lat=35.26deg`. `y` gets the SAME cosine scale on its own axis
39752
+ * (`Y*cos(lat)`) plus a rotation-composition cross term, `-X*sin(lat)*
39753
+ * sin(lon)`, that is exactly 0 whenever EITHER axis is 0 (so both the
39754
+ * identity and every single-axis case reproduce their already-COM-validated
39755
+ * result unchanged) and otherwise fits the 27-point grid to within ~1% on
39756
+ * average (see the module doc comment for the full per-cell table). This is
39757
+ * a purely ORTHOGRAPHIC transform (no perspective divide, no `fov`
39758
+ * dependency): a genuine pinhole projection was one of the hypotheses tested
39759
+ * against the grid and fit measurably worse than this cross term.
39379
39760
  */
39380
39761
  function projectCorner(localX, localY, params) {
39381
- const scaleX = Math.cos(params.lonRad);
39382
- const scaleY = Math.cos(params.latRad);
39383
- let x = localX * scaleX;
39384
- let y = localY * scaleY;
39385
- const twoAxisWeight = Math.sin(params.latRad) * Math.sin(params.lonRad);
39386
- if (twoAxisWeight !== 0) {
39387
- const f = 1 / Math.tan(params.fovRad / 2);
39388
- const local = { x: localX, y: localY, z: 0 };
39389
- // R^T * P, where R = Ry(lon) . Rx(lat): apply Ry(-lon) then Rx(-lat).
39390
- const viewNoTranslate = rotateX(rotateY(local, -params.lonRad), -params.latRad);
39391
- const viewZ = viewNoTranslate.z - f;
39392
- // Guard a degenerate camera-through-the-plane case (should not occur
39393
- // for any realistic lat/lon): skip the secondary term rather than
39394
- // divide by ~0.
39395
- if (Math.abs(viewZ) > 1e-6) {
39396
- const pinholeX = (f * viewNoTranslate.x) / -viewZ;
39397
- const pinholeY = (f * viewNoTranslate.y) / -viewZ;
39398
- x += (pinholeX - localX * scaleX) * Math.abs(twoAxisWeight);
39399
- y += (pinholeY - localY * scaleY) * Math.abs(twoAxisWeight);
39400
- }
39401
- }
39762
+ const x = localX * Math.cos(params.lonRad);
39763
+ const y = localY * Math.cos(params.latRad) - localX * Math.sin(params.latRad) * Math.sin(params.lonRad);
39402
39764
  return rotate2d({ x, y }, params.revRad);
39403
39765
  }
39404
39766
  /**
@@ -43527,6 +43889,7 @@ function deleteTableRow(tableData, rowIdx) {
43527
43889
  return {
43528
43890
  ...cc,
43529
43891
  text: cell.text || cc.text,
43892
+ textRuns: cell.text ? cell.textRuns : cc.textRuns,
43530
43893
  style: cc.style || cell.style,
43531
43894
  rowSpan: newRs > 1 ? newRs : undefined,
43532
43895
  vMerge: undefined,
@@ -43658,6 +44021,7 @@ function deleteTableColumn(tableData, colIdx) {
43658
44021
  adjustedCells[nextColIdx] = {
43659
44022
  ...nextCell,
43660
44023
  text: cell.text || nextCell.text,
44024
+ textRuns: cell.text ? cell.textRuns : nextCell.textRuns,
43661
44025
  style: nextCell.style || cell.style,
43662
44026
  gridSpan: gs - 1 > 1 ? gs - 1 : undefined,
43663
44027
  hMerge: undefined,
@@ -43734,7 +44098,7 @@ function buildTableDataGrid(element) {
43734
44098
  * structural edit therefore rebuilds `rawXml` alongside it, exactly as the
43735
44099
  * on-canvas cell editor does.
43736
44100
  */
43737
- function withTableData$1(element, transform) {
44101
+ function withTableData$1(element, transform, edit) {
43738
44102
  const tableData = element.tableData;
43739
44103
  if (!tableData) {
43740
44104
  return element;
@@ -43745,7 +44109,7 @@ function withTableData$1(element, transform) {
43745
44109
  }
43746
44110
  const updated = { ...element, tableData: next };
43747
44111
  if (element.rawXml) {
43748
- const rawXml = rebuildTableStructureInRawXml(element, next);
44112
+ const rawXml = rebuildTableStructureInRawXml(element, next, edit);
43749
44113
  if (rawXml) {
43750
44114
  updated.rawXml = rawXml;
43751
44115
  }
@@ -43780,7 +44144,11 @@ function setTableElementCellText(element, rowIndex, colIndex, text) {
43780
44144
  * @returns A new element with the row inserted.
43781
44145
  */
43782
44146
  function insertTableElementRow(element, rowIdx, position) {
43783
- return withTableData$1(element, (data) => insertTableRow(data, rowIdx, position));
44147
+ return withTableData$1(element, (data) => insertTableRow(data, rowIdx, position), {
44148
+ axis: 'row',
44149
+ action: 'insert',
44150
+ index: position === 'above' ? rowIdx : rowIdx + 1,
44151
+ });
43784
44152
  }
43785
44153
  /**
43786
44154
  * Remove the row at `rowIdx`, preserving merge spans. No-op on the last row.
@@ -43790,7 +44158,11 @@ function insertTableElementRow(element, rowIdx, position) {
43790
44158
  * @returns A new element with the row removed.
43791
44159
  */
43792
44160
  function removeTableElementRow(element, rowIdx) {
43793
- return withTableData$1(element, (data) => deleteTableRow(data, rowIdx));
44161
+ return withTableData$1(element, (data) => deleteTableRow(data, rowIdx), {
44162
+ axis: 'row',
44163
+ action: 'delete',
44164
+ index: rowIdx,
44165
+ });
43794
44166
  }
43795
44167
  /**
43796
44168
  * Insert a blank column left or right of `colIdx`, preserving merge spans.
@@ -43801,7 +44173,11 @@ function removeTableElementRow(element, rowIdx) {
43801
44173
  * @returns A new element with the column inserted.
43802
44174
  */
43803
44175
  function insertTableElementColumn(element, colIdx, position) {
43804
- return withTableData$1(element, (data) => insertTableColumn(data, colIdx, position));
44176
+ return withTableData$1(element, (data) => insertTableColumn(data, colIdx, position), {
44177
+ axis: 'column',
44178
+ action: 'insert',
44179
+ index: position === 'left' ? colIdx : colIdx + 1,
44180
+ });
43805
44181
  }
43806
44182
  /**
43807
44183
  * Remove the column at `colIdx`, preserving merge spans. No-op on the last one.
@@ -43811,7 +44187,11 @@ function insertTableElementColumn(element, colIdx, position) {
43811
44187
  * @returns A new element with the column removed.
43812
44188
  */
43813
44189
  function removeTableElementColumn(element, colIdx) {
43814
- return withTableData$1(element, (data) => deleteTableColumn(data, colIdx));
44190
+ return withTableData$1(element, (data) => deleteTableColumn(data, colIdx), {
44191
+ axis: 'column',
44192
+ action: 'delete',
44193
+ index: colIdx,
44194
+ });
43815
44195
  }
43816
44196
  /**
43817
44197
  * Append a blank row after the last one.
@@ -45558,8 +45938,17 @@ function computeShadeToTitleFillToRect(title, slideWidthPx, slideHeightPx) {
45558
45938
  b: clampUnit(1 - (title.y + title.height) / slideHeightPx),
45559
45939
  };
45560
45940
  }
45561
- /** Matches one `<colour> <position>%` gradient stop token (see {@link parseGradientCssStops}). */
45562
- const GRADIENT_STOP_TOKEN = /(rgba?\([^)]*\)|#[0-9a-fA-F]{3,8})\s+(-?[\d.]+)%/gu;
45941
+ /**
45942
+ * Matches one `<colour> <position>%` gradient stop token (see
45943
+ * {@link parseGradientCssStops}). The `rgba?(...)` alternative excludes `(`
45944
+ * from its content class (`[^()]*` rather than `[^)]*`): otherwise, on a
45945
+ * string with many repeated unclosed `rgb(` prefixes, each occurrence lets
45946
+ * the content group re-swallow every later `rgb(` before failing to find a
45947
+ * closing `)`, which is `js/polynomial-redos` (quadratic in the number of
45948
+ * repeats). Real `rgba?()` content never contains `(`, so this does not
45949
+ * change what valid input matches.
45950
+ */
45951
+ const GRADIENT_STOP_TOKEN = /(rgba?\([^()]*\)|#[0-9a-fA-F]{3,8})\s+(-?[\d.]+)%/gu;
45563
45952
  function toHexChannel(value) {
45564
45953
  return Math.min(255, Math.max(0, Math.round(value)))
45565
45954
  .toString(16)
@@ -96574,8 +96963,20 @@ async function fetchTextIfCrossOriginSafe(url) {
96574
96963
  return null;
96575
96964
  }
96576
96965
  }
96577
- /** Every distinct `url(...)` reference inside a `@font-face` CSS block's `src` list. */
96578
- const FONT_FACE_URL_PATTERN = /url\(\s*["']?([^"')]+)["']?\s*\)/gu;
96966
+ /**
96967
+ * Every distinct `url(...)` reference inside a `@font-face` CSS block's `src`
96968
+ * list, as a double-quoted, single-quoted, or bare token. The three forms are
96969
+ * separate alternatives (rather than one `["']?...["']?` wrapped around a
96970
+ * single content group) so the surrounding `\s*` never shares characters with
96971
+ * the content group: a naive `\s*["']?([^"')]+)["']?\s*` lets whitespace be
96972
+ * split between the leading `\s*` and the content group in exponentially many
96973
+ * equivalent ways, which is polynomial-time (`js/polynomial-redos`) on an
96974
+ * unclosed `url(` followed by many tabs/spaces (this stylesheet text can come
96975
+ * from a fetched cross-origin `@font-face` CSS file, see
96976
+ * `fetchTextIfCrossOriginSafe`). Each alternative here has a fixed,
96977
+ * non-overlapping character class, so there is only one way to match.
96978
+ */
96979
+ const FONT_FACE_URL_PATTERN = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^"')\s]+))\s*\)/gu;
96579
96980
  /**
96580
96981
  * Replace every `url(https://...)` reference in `css` with a `data:` URI of
96581
96982
  * the fetched resource, dropping (leaving as-is) any reference that fails to
@@ -96587,8 +96988,8 @@ const FONT_FACE_URL_PATTERN = /url\(\s*["']?([^"')]+)["']?\s*\)/gu;
96587
96988
  async function inlineFontFaceUrls(css, fetchDataUrl) {
96588
96989
  const urls = new Set();
96589
96990
  for (const match of css.matchAll(FONT_FACE_URL_PATTERN)) {
96590
- const raw = match[1];
96591
- if (raw.startsWith('http:') || raw.startsWith('https:')) {
96991
+ const raw = match[1] ?? match[2] ?? match[3];
96992
+ if (raw && (raw.startsWith('http:') || raw.startsWith('https:'))) {
96592
96993
  urls.add(raw);
96593
96994
  }
96594
96995
  }
@@ -96690,10 +97091,25 @@ async function fetchAsDataUrl(url) {
96690
97091
  return null;
96691
97092
  }
96692
97093
  }
97094
+ /**
97095
+ * Matches one `url(...)` reference, as a double-quoted, single-quoted, or bare
97096
+ * token. The three forms are matched as separate alternatives (rather than one
97097
+ * `["']?...["']?` wrapped around a single content group) so the surrounding
97098
+ * `\s*` never shares characters with the content group: a naive
97099
+ * `\s*["']?([^"')]+)["']?\s*` lets whitespace be split between the leading
97100
+ * `\s*` and the content group in exponentially many equivalent ways, which is
97101
+ * polynomial-time (`js/polynomial-redos`) on an unclosed `url(` followed by
97102
+ * many tabs/spaces. Each alternative here has a fixed, non-overlapping
97103
+ * character class, so there is only one way to match.
97104
+ */
97105
+ const CSS_URL_PATTERN = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^"')\s]+))\s*\)/u;
96693
97106
  /** Extract a `url(...)` reference from a CSS value; returns `null` when none is present. */
96694
97107
  function extractCssUrl(value) {
96695
- const match = /url\(\s*["']?([^"')]+)["']?\s*\)/u.exec(value);
96696
- return match ? match[1] : null;
97108
+ const match = CSS_URL_PATTERN.exec(value);
97109
+ if (!match) {
97110
+ return null;
97111
+ }
97112
+ return match[1] ?? match[2] ?? match[3] ?? null;
96697
97113
  }
96698
97114
  function needsEmbedding(url) {
96699
97115
  return url.startsWith('blob:') || url.startsWith('http:') || url.startsWith('https:');
@@ -100638,6 +101054,21 @@ function groupTilesByRow(tiles) {
100638
101054
  const rowCount = tiles.reduce((max, t) => Math.max(max, t.row), 0) + 1;
100639
101055
  const rows = Array.from({ length: rowCount }, () => []);
100640
101056
  for (const tile of tiles) {
101057
+ // `row`/`col` are declared as `number`, but this is a public export: a
101058
+ // caller could pass a `RasterizedTile[]` built from untrusted data whose
101059
+ // `row`/`col` are, at runtime, a string like `__proto__`. Indexing an
101060
+ // array with that string resolves through `Array.prototype` (itself
101061
+ // inherited from `Object.prototype`), so `rows[tile.row][tile.col] = tile`
101062
+ // would assign onto `Array.prototype` rather than `rows`, polluting every
101063
+ // array in the process. Requiring a genuine non-negative integer index
101064
+ // closes that off without changing behaviour for any real tile, which is
101065
+ // always produced by `computeExportTilePlan`'s integer loop counters.
101066
+ if (!Number.isInteger(tile.row) ||
101067
+ !Number.isInteger(tile.col) ||
101068
+ tile.row < 0 ||
101069
+ tile.col < 0) {
101070
+ continue;
101071
+ }
100641
101072
  rows[tile.row][tile.col] = tile;
100642
101073
  }
100643
101074
  return rows;
@@ -103016,7 +103447,7 @@ function createLocalStorageBackend(namespace) {
103016
103447
  /** Try IndexedDB first; fall back to localStorage on any failure. */
103017
103448
  async function resolveBackend(dbName, namespace) {
103018
103449
  try {
103019
- const { openChatDb, createIdbBackend } = await import('./pptx-angular-viewer-chat-history-idb-C08A1rjA.mjs');
103450
+ const { openChatDb, createIdbBackend } = await import('./pptx-angular-viewer-chat-history-idb-D_H4MXBh.mjs');
103020
103451
  const db = await openChatDb(dbName);
103021
103452
  return createIdbBackend(db);
103022
103453
  }
@@ -109650,10 +110081,14 @@ class EditorContextMenuComponent {
109650
110081
  return;
109651
110082
  }
109652
110083
  const updated = op(ctx.element, ctx.sel);
109653
- if (updated.tableData) {
109654
- this.editor.updateElement(this.slideIndex(), ctx.element.id, {
110084
+ if (updated !== ctx.element && updated.tableData) {
110085
+ const patch = {
109655
110086
  tableData: updated.tableData,
109656
- });
110087
+ };
110088
+ if (updated.rawXml !== ctx.element.rawXml) {
110089
+ patch.rawXml = updated.rawXml;
110090
+ }
110091
+ this.editor.updateElement(this.slideIndex(), ctx.element.id, patch);
109657
110092
  }
109658
110093
  }
109659
110094
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: EditorContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -142944,7 +143379,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
142944
143379
  }], propDecorators: { canEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "canEdit", required: false }] }], slideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "slideIndex", required: false }] }], selectedElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedElement", required: false }] }] } });
142945
143380
 
142946
143381
  // Generated by scripts/inline-shared.mjs from package.json. Do not edit.
142947
- const PPTX_ANGULAR_VIEWER_VERSION = "3.13.0";
143382
+ const PPTX_ANGULAR_VIEWER_VERSION = "3.15.0";
142948
143383
 
142949
143384
  /**
142950
143385
  * account-page.component.ts: File > Account content.
@@ -181406,4 +181841,4 @@ function cn(...values) {
181406
181841
  */
181407
181842
 
181408
181843
  export { CollaborationService as $, AFTER_ANIMATION_VALUES as A, AnimationAuthorPanelComponent as B, AnimationPanelComponent as C, AnimationPlaybackService as D, AutosaveRecoveryDialogComponent as E, AutosaveService as F, BroadcastDialogComponent as G, CHART_EDITOR_STYLES as H, CURSOR_PALETTE as I, CanvasFitService as J, ChartAxisOptionsComponent as K, ChartAxisStyleOptionsComponent as L, ChartComboTypeOptionsComponent as M, ChartDataEditorComponent as N, ChartDataLabelOptionsComponent as O, ChartDatapointMarkerOptionsComponent as P, ChartDatapointOptionsComponent as Q, ChartDisplayOptionsComponent as R, ChartElementViewComponent as S, ChartErrorBarOptionsComponent as T, ChartMarkerOptionsComponent as U, ChartPartSelectionService as V, ChartPrimitivesComponent as W, ChartRendererComponent as X, ChartTrendlineOptionsComponent as Y, ChartTypeSelectorComponent as Z, CollaborationCursorsComponent as _, ALIGN_OPTIONS as a, InsertSmartArtDialogComponent as a$, ColorChangedImageComponent as a0, CommentMarkersOverlayComponent as a1, CommentsPanelComponent as a2, CommentsService as a3, ComparePanelComponent as a4, ConnectorRendererComponent as a5, ConnectorTextOverlayComponent as a6, CustomShowsComponent as a7, DEFAULT_BOUNDS as a8, DEFAULT_BROADCAST_SERVER_URL as a9, EditorToolbarComponent as aA, EffectsPanelComponent as aB, ElementRendererComponent as aC, EmbeddedFontsService as aD, EncryptedFileDialogComponent as aE, EquationEditorDialogComponent as aF, EquationRendererComponent as aG, EquationTemplateGalleryComponent as aH, ExportProgressModalComponent as aI, ExportService as aJ, FieldContextService as aK, FindBarComponent as aL, FindReplaceBarComponent as aM, FollowModeBarComponent as aN, FontEmbeddingListComponent as aO, FontEmbeddingPanelComponent as aP, GALLERY_THEME_PRESETS as aQ, GOOGLE_WEBFONTS_LINK_ID as aR, GRIDLINE_COLOR$1 as aS, GoogleWebfontsService as aT, GradientPickerComponent as aU, HANDOUT_OPTIONS as aV, HeaderFooterDialogComponent as aW, HyperlinkDialogComponent as aX, ImagePropertiesPanelComponent as aY, InkDrawingService as aZ, InkRendererComponent as a_, DEFAULT_CANVAS_HEIGHT as aa, DEFAULT_CANVAS_WIDTH as ab, DEFAULT_COLOR_SCHEME as ac, DEFAULT_FILL_COLOR$1 as ad, DEFAULT_LAYOUT as ae, DEFAULT_PALETTE$1 as af, DEFAULT_PATTERN_FILL_PRESET as ag, DEFAULT_PRINT_SETTINGS as ah, DEFAULT_SLIDE_BACKGROUND as ai, DEFAULT_STROKE_COLOR as aj, DEFAULT_STYLE as ak, DEFAULT_TABLE_ROW_HEIGHT as al, DEFAULT_TEXT_COLOR$2 as am, DEFAULT_VIEWER_PROFILE as an, DIRECTIONAL_PRESETS as ao, DIRECTION_OPTIONS as ap, DocumentPropertiesCardComponent as aq, EFFECT_SOUND_CATALOGUE as ar, EMBEDDED_FONTS_STYLE_ID as as, EMPHASIS_PRESETS as at, ENTRANCE_PRESETS as au, TEMPLATES as av, EXIT_PRESETS as aw, EditorContextMenuComponent as ax, EditorHistory as ay, EditorStateService as az, ANIMATION_PRESET_CATEGORIES as b, RibbonArrangeSectionComponent as b$, InspectorPaneHeaderComponent as b0, InspectorPanelComponent as b1, IsMobileService as b2, KeepAnnotationsDialogComponent as b3, LOCALE_CATALOG as b4, LONG_PRESS_DURATION_MS as b5, LONG_PRESS_MOVE_TOLERANCE_PX as b6, LoadContentService as b7, LocalPresencePublisher as b8, MAX_ZOOM_SCALE as b9, PX_PER_INCH as bA, PasswordProtectionDialogComponent as bB, PasswordStrengthMeterComponent as bC, PowerPointViewerComponent as bD, PresentToolbarAutoHide as bE, PresentationAnnotationOverlayComponent as bF, PresentationAnnotationsService as bG, PresentationOverlayComponent as bH, PresentationPropertiesPanelComponent as bI, PresentationSettingsCardComponent as bJ, PresentationSubtitleBarComponent as bK, PresentationToolbarComponent as bL, PresentationTransitionOverlayComponent as bM, PresenterViewComponent as bN, PresenterWindowService as bO, PrintDialogComponent as bP, PrintService as bQ, PrintSettingsPanelComponent as bR, PropertiesDialogComponent as bS, REPEAT_MODE_OPTIONS as bT, RESIZE_HANDLES as bU, RULER_FONT_SIZE as bV, RULER_THICKNESS as bW, ReadingViewOverlayComponent as bX, RemoteSelectionOverlayComponent as bY, RibbonAnimationGalleryComponent as bZ, RibbonAnimationsSectionComponent as b_, MIN_ZOOM_SCALE as ba, MOTION_PATH_COLUMNS as bb, MediaPreviewComponent as bc, MediaPropertiesPanelComponent as bd, MediaRendererComponent as be, MediaTrimTimelineComponent as bf, MobileBottomBarComponent as bg, MobileMenuSheetComponent as bh, MobilePresenterViewComponent as bi, MobileSheetComponent as bj, MobileSlidesSheetComponent as bk, MobileToolbarComponent as bl, ModalDialogComponent as bm, Model3DRendererComponent as bn, NotesHandoutCardComponent as bo, NotesPanelComponent as bp, NotesToolbarComponent as bq, OleRendererComponent as br, OutlineViewOverlayComponent as bs, POWER_POINT_VIEWER_PROVIDERS as bt, PPTX_OPEN_ACCEPT as bu, PRESENTATION_OPEN_EXTENSIONS as bv, PRESENTER_CHANNEL_NAME as bw, PRESENTER_MSG_ORIGIN as bx, PRESENTER_TIMER_SEGMENT_MS as by, PX_PER_CM as bz, AUDIENCE_HASH as c, TABLE_STRUCTURE_TOGGLES as c$, RibbonColorPopoverComponent as c0, RibbonComponent as c1, RibbonDesignSectionComponent as c2, RibbonDrawSectionComponent as c3, RibbonDrawingGroupComponent as c4, RibbonEditingSectionComponent as c5, RibbonFileSectionComponent as c6, RibbonFontControlsComponent as c7, RibbonHomeSectionComponent as c8, RibbonHyperlinkButtonComponent as c9, SettingsAppearanceTabComponent as cA, SettingsDialogComponent as cB, SettingsLanguageTabComponent as cC, ShareDialogComponent as cD, ShortcutPanelComponent as cE, ShowOptionsFieldsetComponent as cF, ShowSlidesFieldsetComponent as cG, SignatureStrippedDialogComponent as cH, SignaturesPanelComponent as cI, SignaturesService as cJ, SlideBackgroundCardComponent as cK, SlideCanvasComponent as cL, SlideDefaultInspectorComponent as cM, SlideDiffChangesComponent as cN, SlideDiffRowComponent as cO, SlideDiffThumbnailsComponent as cP, SlideSizeCardComponent as cQ, SlideSorterOverlayComponent as cR, SlideThemeOverridePanelComponent as cS, SlideTransitionCardComponent as cT, SlidesPanelComponent as cU, SmartArt3DRendererComponent as cV, SmartArt3DService as cW, SmartArtPreviewComponent as cX, SmartArtPropertiesComponent as cY, SmartArtRendererComponent as cZ, StatusBarComponent as c_, RibbonInsertFieldsComponent as ca, RibbonInsertSectionComponent as cb, RibbonMotionPathGalleryComponent as cc, RibbonParagraphControlsComponent as cd, RibbonPrimaryRowComponent as ce, RibbonReviewSectionComponent as cf, RibbonShapeExtrasComponent as cg, RibbonSlideshowSectionComponent as ch, RibbonTransitionsSectionComponent as ci, RibbonViewSectionComponent as cj, RulerGuidesService as ck, SEQUENCE_OPTIONS as cl, SEVERITY_GROUPS as cm, SEVERITY_LABELS as cn, SHORTCUT_REFERENCE_ITEMS as co, SLIDE_TRANSITION_KEYFRAMES as cp, DEFAULT_PALETTE as cq, PALETTES$1 as cr, SMART_ART_COLOR_SCHEMES as cs, SMART_ART_STYLE_OPTIONS as ct, SUB_ITEM_LABEL as cu, SVG_WARP_PRESETS as cv, SWIPE_MAX_VERTICAL_PX as cw, SWIPE_THRESHOLD_PX as cx, SelectionPaneComponent as cy, SetUpSlideShowDialogComponent as cz, AUDIENCE_NONCE_KEY as d, alignPatch as d$, TEXT_3D_BOTTOM_BEVEL_KEYS as d0, TEXT_3D_TOP_BEVEL_KEYS as d1, TEXT_DIRECTION_OPTIONS$1 as d2, THEME_CATALOG as d3, TIMING_CURVE_OPTIONS as d4, TRIGGER_OPTIONS as d5, TYPE_LABELS as d6, TableCellAdvancedFillComponent as d7, TableCellFormattingComponent as d8, TableDataEditorComponent as d9, ViewerExportService as dA, ViewerExtraDialogsComponent as dB, ViewerFileIOService as dC, ViewerFindReplaceService as dD, ViewerFormatPainterService as dE, ViewerInspectorPanelService as dF, ViewerKeyboardService as dG, ViewerMobileSheetService as dH, ViewerPresentationModeService as dI, ViewerThemeGalleryService as dJ, ViewerTouchGesturesService as dK, ViewerZoomService as dL, WEBM_MIME_CANDIDATES as dM, WriteBackScheduler as dN, ZERO_LINE_COLOR as dO, ZoomNavigationService as dP, ZoomRendererComponent as dQ, ZoomTargetService as dR, addCategory as dS, addCommentToList as dT, addGradientStopPatch as dU, addItem as dV, addSeries as dW, addSubItem as dX, advanceStep as dY, affordanceElements as dZ, aiToggleVisible as d_, TablePropertiesComponent as da, TableRendererComponent as db, TableResizeOverlayComponent as dc, TableSelectionService as dd, TagsCardComponent as de, Text3DBevelSectionComponent as df, Text3DPanelComponent as dg, TextAdvancedPanelComponent as dh, ThemeEditorFieldsComponent as di, ThemeGalleryComponent as dj, ThemeSelectorCardComponent as dk, TitleBarComponent as dl, TitleBarSearchComponent as dm, TransitionDirectionPickerComponent as dn, TransitionPreviewComponent as dp, VALIGN_OPTIONS as dq, VIEWER_THEME as dr, VersionHistoryPanelComponent as ds, ViewerCanvasEditingService as dt, ViewerCollabCursorService as du, ViewerCollaborationSessionService as dv, ViewerCompareService as dw, ViewerCustomShowsService as dx, ViewerDialogsService as dy, ViewerDocumentPropertiesService as dz, AVATAR_COLOR_SWATCHES as e, buildStockViewModel as e$, animationFor as e0, animationPresetLabelKey as e1, annotationMapToInkInserts as e2, applyAcceptedDiff as e3, applyAnimationPreset as e4, applyFindReplacements as e5, applyFormatToElement as e6, applyMove as e7, applyResize as e8, asMediaElement as e9, buildEmbeddedFontStyles as eA, buildEquationElement as eB, buildEquationSegment as eC, buildFallbackViewModel as eD, buildFontFaceRule as eE, buildGradientFillCss as eF, buildGridlinesAndLabels as eG, buildHyperlinkPatch as eH, buildInkContainerStyle as eI, buildInkStrokes as eJ, buildLegend as eK, buildLiveInkStrokeView as eL, buildMarkTooltip as eM, buildModel3DContainerStyle as eN, buildModel3DViewModel as eO, buildOleActionModel as eP, buildOleInfoRows as eQ, buildPatternFillCss as eR, buildPieViewModel as eS, buildPrintHtmlDocument as eT, buildPropertiesPatch as eU, buildRadarViewModel as eV, buildRegionMapViewModel as eW, buildSaveSlides as eX, buildShareUrl as eY, buildSmartArtInsertElement as eZ, buildSmartArtNodes as e_, assignUserColor as ea, attachShowVisibilityPause as eb, attachTouchGestures as ec, axisTickValues as ed, beginNodeEdit as ee, bevelSizePatch as ef, boolFromEvent as eg, bringForward as eh, bringToFront as ei, buildBarActions as ej, buildBroadcastConfig as ek, buildBroadcastViewerUrl as el, buildCategoryLabels as em, buildCellParagraphs as en, buildChartViewModel as eo, buildChatLogExport as ep, buildChatLogMarkdown as eq, buildChromeStyle as er, buildClearHyperlinkPatch as es, buildClickGroups as et, buildColStyles as eu, buildCollaborationConfig as ev, buildComboViewModel as ew, buildCssGradientFromShapeStyle as ex, buildDuotoneFilter as ey, buildDuotoneFilterId as ez, AXIS_LABEL_COLOR as f, computePlotLayout as f$, buildSurfaceViewModel as f0, buildTableViewModel as f1, buildTreemapViewModel as f2, buildTrimFragment as f3, buildWaterfallViewModel as f4, buildZeroLine as f5, buildZoomContainerStyle as f6, buildZoomViewModel as f7, bulletIndentPx as f8, canAddTopLevelNode as f9, collectAccessibilityIssues as fA, collectElementText as fB, collectSlideText as fC, collectStoredChats as fD, collectUsedFontFamilies as fE, columnWidthStyle as fF, commitNodeText as fG, computeAlign as fH, computeAxisTitlePrimitives as fI, computeBarRects as fJ, computeBubbleRadius as fK, computeCornerHandle as fL, computeDistribute as fM, computeDrawingViewBox as fN, computeErrorBarPrimitives as fO, computeFocusTargets as fP, computeGridSpacingPx as fQ, computeHandleBoxes as fR, computeHandoutLayout as fS, computeIsMobile as fT, computeIsTablet as fU, computeLinePoints as fV, computeLinearRegression as fW, computePageCount as fX, computePieLayout as fY, computePieSlicePath as fZ, computePieSlices as f_, canEditSmartArtNodes as fa, canGroupSelection as fb, canRemoveTopLevelNode as fc, canSetStrokeWidth as fd, canStartBroadcast as fe, canStartShare as ff, canUngroupSelection as fg, canUseClipboard as fh, captionDisplayText as fi, cellRunStyle as fj, cellStyleToStyleMap as fk, cellTdStyle as fl, changeCountLabel as fm, changeIcon as fn, characterSpacingPatch as fo, chartPreserveAspectRatio as fp, checkFontAvailable as fq, clampCursorPosition as fr, clampGifDimensions as fs, clampIndex as ft, clampNotesFontSize as fu, clampScale as fv, clampStep as fw, clearAllLocalViewerData as fx, clearAudienceContent as fy, cn as fz, AccessibilityPanelComponent as g, focusTargetChips as g$, computeRSquared as g0, computeRadarPoints as g1, computeResizeHandleBoxes as g2, computeRotateHandleBox as g3, computeScatterDots as g4, computeScatterXDomain as g5, computeSelectionBoxes as g6, computeSingleSelected as g7, computeSlideIndices as g8, computeSnap as g9, disableGlowPatch as gA, disableInnerShadowPatch as gB, disableOuterShadowPatch as gC, disableReflectionPatch as gD, disableSoftEdgePatch as gE, duplicateElementById as gF, durationOf as gG, effectsStateOf as gH, enableGlowPatch as gI, enableInnerShadowPatch as gJ, enableOuterShadowPatch as gK, enableReflectionPatch as gL, enableSoftEdgePatch as gM, encodeGif as gN, endShowMediaCleanup as gO, estimatePageCount as gP, exitPresentationFullscreen as gQ, exportAiChatLogs as gR, extractPathPoints as gS, eyedropperAvailable as gT, fillColorOf$1 as gU, findInSlides as gV, findOwningSlideIndex as gW, findSlideIndexByElementId as gX, firstVisibleIndex as gY, fitPolynomial as gZ, fitZoom as g_, computeStackedBarRects as ga, computeStackedValueRange as gb, computeTrendlinePrimitives as gc, computeValueRange as gd, convertOmmlToMathMl as ge, copyFormatFromElement as gf, countAccessibilityIssues as gg, countAnnotationStrokes as gh, createAngularAiBridge as gi, createCustomShow as gj, createSwipeDismissDrag as gk, createWebrtcBundle as gl, createWebsocketBundle as gm, cssObjectToStyleMap as gn, currentColorScheme as go, currentLayout as gp, currentStyle as gq, defaultCssVars as gr, defaultRadius as gs, defaultThemeColors as gt, deleteElementsByIds as gu, deleteVersion as gv, demoteNode as gw, deriveModel3DBlobUrl as gx, derivePresenceList as gy, describeSmartArtBounds as gz, AccessibilityService as h, insertTableElementRow as h$, fontMimeForFormat as h0, fontSizeOf as h1, forgetSessionDeck as h2, formatAxisValue as h3, formatBytes as h4, formatCursorLabel as h5, formatElapsed as h6, formatFileSize as h7, formatPropertyDate as h8, formatTime as h9, getShapeFillStrokeStyle as hA, getSlideBackgroundStyle as hB, getSlideTransitionAnimations as hC, getSmartArtNodeBounds as hD, getSpeechRecognitionCtor as hE, getTextBlockStyle as hF, getTextWarp as hG, getTouchDistance as hH, getWarpCategory as hI, getWarpPath as hJ, gradientStateFromStyle as hK, gradientStateOf as hL, gradientStatePatch as hM, gradientStopColorCommitPatch as hN, gridColumns as hO, groupIssuesBySeverity as hP, hasAnimation as hQ, hasCopyableFormat as hR, hasExistingLink as hS, hasExitedFullscreen as hT, hasGradientFill as hU, hasPressureVariation as hV, hasVisibleSlideAfter as hW, headerLabel as hX, imageDimensions as hY, inkViewBox as hZ, insertTableElementColumn as h_, fpsToFrameIntervalMs as ha, generateBroadcastRoomId as hb, generateCommentId as hc, generateCustomShowId as hd, generatePressureCircles as he, generateTicks as hf, getClrChangeParams as hg, getContainerStyle as hh, getDuotoneFilterDef as hi, getEffectSoundAsset as hj, getEffectSoundState as hk, getImageSrc as hl, getLocalStorageUsageSummary as hm, getOleAriaLabel as hn, getOleBadgeLabel as ho, getOleDisplayName as hp, getOleDownloadFileName as hq, getOleTypeColor as hr, getOleTypeLabel as hs, getPasswordStrength as ht, getPatternSvg as hu, getPlaceholderStyle as hv, getVersions as hw, getResolvedShapeClipPath as hx, getResolvedShapeClipPathFor as hy, getSessionTabId as hz, AccessibilityTextPanelComponent as i, normalizeSlidesPerPage as i$, interpolateWidth as i0, isAudienceTab as i1, isBold as i2, isBrowserOpenableMime as i3, isChildNode as i4, isElementInteractive as i5, isInjectableUrl as i6, isItalic as i7, isLegacyBinaryPresentation as i8, isPpactionUrl as i9, mergeDown as iA, mergeRight as iB, mergeSelection as iC, mergeTablesDirective as iD, moveElementBy as iE, moveNodeDown as iF, moveNodeUp as iG, msToFrameDelayCs as iH, narrowToCircle as iI, narrowToPolygon as iJ, narrowToRect as iK, newChartElement as iL, newEquationElement as iM, newPresetShapeElement as iN, newShapeElement as iO, newSmartArtElement as iP, newTableElement as iQ, newTextElement as iR, nextVisibleIndex as iS, nodeBold as iT, nodeEditBox as iU, nodeFillColor as iV, nodeFontColor as iW, nodeIdFromKey as iX, nodeItalic as iY, nodeStyle as iZ, normalizeFontFormat as i_, isPresenterMessage as ia, isSigned as ib, isSupportedPresentationFile as ic, isTextElement as id, isTwoTableFocus as ie, isUnderline as ig, isUrlSafe as ih, isValidRoomId as ii, isViewportBackgroundPressTarget as ij, isZoomActivationKey as ik, issueTrackKey as il, issueTypeLabel as im, keyToLabel as io, lastVisibleIndex as ip, latexToMathml as iq, layoutConnectorPaints as ir, layoutNodeLabels as is, linePointsToSvgString as it, lineSpacingPatch as iu, loadAudienceContent as iv, loadSessionDeck as iw, mediaFallbackFor as ix, mediaSurfaceFor as iy, mergeCaptionResults as iz, AccountPageComponent as j, resolveParagraphBullet as j$, normalizeValue as j0, numFromEvent as j1, ommlToMathml as j2, ooxmlDashToCssBorderStyle as j3, openNativeEyeDropper as j4, overallStatus as j5, paletteColor as j6, parseAudienceNonce as j7, parseNodeTextarea as j8, partitionSlides as j9, readAsDataUrl as jA, recordWebm as jB, registerCrossSlideAudio as jC, rememberSessionDeck as jD, removeAnimation as jE, removeCategory as jF, removeTableElementColumn as jG, removeCommentFromList as jH, removeElementAnimation as jI, removeGradientStopPatch as jJ, removeNode as jK, removeTableElementRow as jL, removeSeries as jM, renderToCanvas as jN, reorderAnimationDown as jO, reorderAnimationUp as jP, replaceInSlides as jQ, replaceMatch as jR, requestPresentationFullscreen as jS, resizeElement as jT, resolveCaptionTracks as jU, resolveChartKind as jV, resolveFontVariant as jW, resolveHyperlinkHref as jX, resolveInteractiveElementId as jY, resolveMediaSrc as jZ, resolveOleType as j_, patchChartData as ja, patchChartStyle as jb, patchTableData as jc, patchTextStyle as jd, patternPresetOptions as je, pendingElementStyles as jf, pickColorByClickFallback as jg, pickFile as jh, pickSupportedMimeType as ji, planGifFrames as jj, planVideoSegments as jk, pointFromPointerEvent as jl, pointsToSvgPathD as jm, presenceToCursors as jn, presentationBaseName as jo, presentationStageStyle as jp, presenterTimerProgress as jq, presetByLayout as jr, presetsForCategory as js, pressuresToWidths as jt, prevVisibleIndex as ju, projectDrawingShapes as jv, promoteNode as jw, provideViewerTheme as jx, radarAngle as jy, radarRingPoints as jz, ActionSettingsPanelComponent as k, setSequence as k$, resolvePresenterNotes as k0, resolveProfileInitial as k1, resolveRegionCode as k2, resolveRibbonCanGroup as k3, resolveSlideAutoAdvanceMs as k4, resolvePalette as k5, resolveThemeCatalogEntry as k6, resolveTransitionDuration as k7, restoreSessionDeck as k8, revealedElementStyles as k9, setAnimationEmphasis as kA, setAnimationEntrance as kB, setAnimationExit as kC, setAxis as kD, setAxisLogScale as kE, setAxisTitleStyle as kF, setCategoryLabel as kG, setCellText as kH, setColorScheme as kI, setDataLabels as kJ, setDataPointExplosion as kK, setDataPointFill as kL, setDataPointLabel as kM, setDataPointMarker as kN, setDelay as kO, setDirection as kP, setDuration as kQ, setEffectSound as kR, setEffectStockSound as kS, setElementPosition as kT, setGridlineStyle as kU, setLayout as kV, setLegend as kW, setNodeStyle as kX, setNodeText as kY, setRepeatCount as kZ, setRepeatMode as k_, routeOrthogonalConnector as ka, rowStyle as kb, rulerDragToGuidePosition as kc, rulerHighlight as kd, rulerStripTicks as ke, sampleColorFromSlide as kf, sanitizeColor as kg, sanitizeSlideIndex as kh, sanitizeUserName as ki, saveViewerProfile as kj, savedPresentationFileName as kk, scanAvailableFonts as kl, searchSlides as km, seedBroadcastFields as kn, seedHyperlinkDraft as ko, seedPropertiesDraft as kp, seedShareFields as kq, segmentFrameCount as kr, selectValue$3 as ks, sendBackward as kt, sendToBack as ku, sequentialColorScale as kv, serializeWriteBack as kw, seriesColor as kx, setAfterAnimation as ky, setAfterAnimationColor as kz, AdvancedChartEditorComponent as l, updateReflectionPatch as l$, setSeriesChartType as l0, setSeriesColor as l1, setSeriesErrorBars as l2, setSeriesMarker as l3, setSeriesName as l4, setSeriesTrendline as l5, setSeriesValue as l6, setStyle as l7, setTimingCurve as l8, setTitle as l9, strokeWidthOf as lA, styleShadowFilter as lB, surfaceColor as lC, textAdvancedPatch as lD, textAdvancedStateFromStyle as lE, textAdvancedStateOf as lF, textColorOf as lG, textDirectionPatch as lH, textFontSizePatch as lI, textStyleOf as lJ, textStylePatch as lK, themeStyle as lL, themeToCssVars as lM, thumbnailHeight as lN, thumbnailZoom as lO, toggleCommentResolvedInList as lP, toggleNodeBold as lQ, toggleNodeItalic as lR, toggleSheet as lS, topLevelNodeCount as lT, transformSelectedTextCase as lU, translationsEn as lV, updateElementById as lW, updateGlowPatch as lX, updateGradientStopPatch as lY, updateInnerShadowPatch as lZ, updateOuterShadowPatch as l_, setTrigger as la, setTriggerShapeId as lb, shapeStylePatch$1 as lc, sheetAfterNavigate as ld, shouldBlockClickAdvance as le, shouldUseSvgWarp as lf, showDirectionPicker as lg, showsTemplateAffordance as lh, signatureCountLabel as li, signatureKey as lj, signatureTimestamp as lk, signerName as ll, statusLabel as lm, slideNumberOf as ln, slidesWithReappliedLayout as lo, smartArtNodes as lp, paletteColour as lq, snapToGridStep as lr, splitCursorCell as ls, splitMergedCell as lt, statusKind as lu, statusLabel$1 as lv, storeAudienceContent as lw, stringFromEvent$5 as lx, strokeColorOf as ly, strokeToInkElement as lz, AiChangeOverlayComponent as m, vAlignPatch as m0, validatePassword as m1, validatePrintSettings as m2, validateRoomId as m3, valueToY as m4, vermilionDarkColors as m5, vermilionDarkTheme as m6, vermilionLightColors as m7, vermilionLightTheme as m8, vermilionRadius as m9, waypointsToPathD as ma, withManualLayouts as mb, worstStatus as mc, zoomTargetSlideIndex as md, AiChatPanelComponent as n, AiChatService as o, AiComposerComponent as p, AiFocusBarComponent as q, AiFocusHighlightOverlayComponent as r, AiHistoryMenuComponent as s, toChatSummary as t, AiHistoryService as u, AiMessageListComponent as v, AiPanelStore as w, AiProposalCardComponent as x, AiSettingsSectionComponent as y, AiToolCallCardComponent as z };
181409
- //# sourceMappingURL=pptx-angular-viewer-pptx-angular-viewer-CrlToiB1.mjs.map
181844
+ //# sourceMappingURL=pptx-angular-viewer-pptx-angular-viewer-wH2ZRE8b.mjs.map