pptx-angular-viewer 1.1.11 → 1.1.13

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.
@@ -3737,6 +3737,977 @@ const ANIMATION_KEYFRAMES_CSS = `
3737
3737
  }
3738
3738
  `.trim();
3739
3739
 
3740
+ /**
3741
+ * CSS-based 3D approximation for PPTX shapes (framework-agnostic).
3742
+ *
3743
+ * Self-contained module that translates OOXML scene3d/shape3d properties
3744
+ * (camera/perspective, extrusion depth, contour, bevel, material, light rig)
3745
+ * into the CSS pieces a renderer can apply. It mirrors the React layer
3746
+ * (`packages/react/src/viewer/utils/shape-3d-styles.ts` +
3747
+ * `shape-visual-3d.ts`) closely enough that every binding produces the same
3748
+ * visual output, while staying framework-agnostic (plain TS, unit-testable).
3749
+ *
3750
+ * The aggregate {@link getComputed3dStyle} returns plain string/number CSS
3751
+ * pieces — no framework `CSSProperties` type — so React, Vue, and Angular can
3752
+ * each merge them into their own style object. The extrusion box-shadow is
3753
+ * returned as a SEPARATE `extrusionBoxShadow` value (rather than folded into
3754
+ * `boxShadow`) so the caller can comma-join it with any pre-existing effect
3755
+ * shadow instead of clobbering it.
3756
+ *
3757
+ * @module render/visual-3d
3758
+ */
3759
+ /**
3760
+ * EMU per CSS pixel. PowerPoint stores 3D dimensions in English Metric Units;
3761
+ * the React layer uses the same constant (9525). Defined locally to keep this
3762
+ * module self-contained.
3763
+ */
3764
+ const EMU_PER_PX = 9525;
3765
+ /**
3766
+ * Maximum stacked shadow layers for extrusion (performance guard). Matches the
3767
+ * React engine — each layer is a single box-shadow, so 40 is still performant.
3768
+ */
3769
+ const MAX_EXTRUSION_LAYERS = 40;
3770
+ const MATERIAL_MAP = {
3771
+ matte: {
3772
+ filter: 'brightness(0.95) saturate(0.9)',
3773
+ backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 40%, rgba(0,0,0,0.03) 100%)',
3774
+ },
3775
+ warmMatte: {
3776
+ filter: 'brightness(1.0) saturate(0.85) sepia(0.08)',
3777
+ backgroundImage: 'linear-gradient(180deg, rgba(255,240,220,0.06) 0%, transparent 50%, rgba(0,0,0,0.03) 100%)',
3778
+ },
3779
+ plastic: {
3780
+ filter: 'brightness(1.05) contrast(1.05)',
3781
+ boxShadow: 'inset -2px -2px 6px rgba(255,255,255,0.35), inset 1px 1px 3px rgba(255,255,255,0.15)',
3782
+ backgroundImage: 'radial-gradient(ellipse 40% 30% at 25% 20%, rgba(255,255,255,0.18) 0%, transparent 70%)',
3783
+ },
3784
+ metal: {
3785
+ filter: 'brightness(1.1) contrast(1.15) saturate(1.2)',
3786
+ boxShadow: 'inset -3px -3px 8px rgba(255,255,255,0.45), inset 2px 2px 4px rgba(255,255,255,0.2), inset 0 0 2px rgba(0,0,0,0.15)',
3787
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 20%, transparent 45%, rgba(0,0,0,0.06) 75%, rgba(255,255,255,0.1) 100%)',
3788
+ },
3789
+ dkEdge: {
3790
+ filter: 'brightness(0.85) contrast(1.2)',
3791
+ boxShadow: 'inset 0 0 8px rgba(0,0,0,0.2), inset 0 0 2px rgba(0,0,0,0.1)',
3792
+ backgroundImage: 'radial-gradient(ellipse at center, rgba(255,255,255,0.06) 0%, transparent 50%, rgba(0,0,0,0.1) 100%)',
3793
+ },
3794
+ softEdge: {
3795
+ filter: 'brightness(1.05) contrast(0.9)',
3796
+ backgroundImage: 'radial-gradient(ellipse at center, rgba(255,255,255,0.06) 0%, transparent 60%)',
3797
+ },
3798
+ flat: {},
3799
+ softmetal: {
3800
+ filter: 'brightness(1.05) contrast(1.08) saturate(1.1)',
3801
+ boxShadow: 'inset -2px -2px 6px rgba(255,255,255,0.3), inset 1px 1px 3px rgba(255,255,255,0.12)',
3802
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.04) 25%, transparent 50%, rgba(0,0,0,0.04) 85%, rgba(255,255,255,0.06) 100%)',
3803
+ },
3804
+ clear: {
3805
+ opacity: 0.7,
3806
+ filter: 'brightness(1.15)',
3807
+ boxShadow: 'inset -1px -1px 4px rgba(255,255,255,0.3), inset 1px 1px 2px rgba(255,255,255,0.2)',
3808
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 40%, rgba(255,255,255,0.08) 100%)',
3809
+ },
3810
+ powder: {
3811
+ filter: 'brightness(1.1) contrast(0.85) saturate(0.8)',
3812
+ backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 50%)',
3813
+ },
3814
+ translucentPowder: {
3815
+ opacity: 0.75,
3816
+ filter: 'brightness(1.1) contrast(0.85)',
3817
+ backgroundImage: 'radial-gradient(ellipse at 30% 30%, rgba(255,255,255,0.1) 0%, transparent 60%)',
3818
+ },
3819
+ legacyMatte: {
3820
+ filter: 'brightness(0.92) saturate(0.85)',
3821
+ backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.03) 0%, transparent 50%, rgba(0,0,0,0.04) 100%)',
3822
+ },
3823
+ legacyPlastic: {
3824
+ filter: 'brightness(1.02) contrast(1.03)',
3825
+ boxShadow: 'inset -2px -2px 5px rgba(255,255,255,0.3)',
3826
+ backgroundImage: 'radial-gradient(ellipse 35% 25% at 25% 20%, rgba(255,255,255,0.15) 0%, transparent 70%)',
3827
+ },
3828
+ legacyMetal: {
3829
+ filter: 'brightness(1.05) contrast(1.1) saturate(1.1)',
3830
+ boxShadow: 'inset -2px -2px 6px rgba(255,255,255,0.35), inset 1px 1px 3px rgba(255,255,255,0.15)',
3831
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.06) 25%, transparent 50%, rgba(0,0,0,0.05) 80%)',
3832
+ },
3833
+ legacyWireframe: {
3834
+ filter: 'brightness(1) contrast(1.4) saturate(0.6)',
3835
+ boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.4)',
3836
+ },
3837
+ };
3838
+ /** CSS overrides for a material preset; empty object for unknown/undefined. */
3839
+ function getMaterialCssOverrides(material) {
3840
+ if (!material) {
3841
+ return {};
3842
+ }
3843
+ return MATERIAL_MAP[material] ?? {};
3844
+ }
3845
+ const CAMERA_PRESET_MAP = {
3846
+ orthographicFront: { rotateX: 0, rotateY: 0, rotateZ: 0 },
3847
+ perspectiveFront: { perspective: '1000px', rotateX: 0, rotateY: 0, rotateZ: 0 },
3848
+ perspectiveAbove: { perspective: '1000px', rotateX: -20, rotateY: 0, rotateZ: 0 },
3849
+ perspectiveBelow: { perspective: '1000px', rotateX: 20, rotateY: 0, rotateZ: 0 },
3850
+ perspectiveLeft: { perspective: '1000px', rotateX: 0, rotateY: 20, rotateZ: 0 },
3851
+ perspectiveRight: { perspective: '1000px', rotateX: 0, rotateY: -20, rotateZ: 0 },
3852
+ perspectiveAboveLeftFacing: { perspective: '1000px', rotateX: -20, rotateY: 25, rotateZ: 0 },
3853
+ perspectiveAboveRightFacing: { perspective: '1000px', rotateX: -20, rotateY: -25, rotateZ: 0 },
3854
+ perspectiveContrastingLeftFacing: { perspective: '800px', rotateX: -15, rotateY: 30, rotateZ: 0 },
3855
+ perspectiveContrastingRightFacing: {
3856
+ perspective: '800px',
3857
+ rotateX: -15,
3858
+ rotateY: -30,
3859
+ rotateZ: 0,
3860
+ },
3861
+ perspectiveHeroicLeftFacing: { perspective: '600px', rotateX: -10, rotateY: 35, rotateZ: 0 },
3862
+ perspectiveHeroicRightFacing: { perspective: '600px', rotateX: -10, rotateY: -35, rotateZ: 0 },
3863
+ perspectiveHeroicExtremeLeftFacing: {
3864
+ perspective: '500px',
3865
+ rotateX: -8,
3866
+ rotateY: 45,
3867
+ rotateZ: 0,
3868
+ },
3869
+ perspectiveHeroicExtremeRightFacing: {
3870
+ perspective: '500px',
3871
+ rotateX: -8,
3872
+ rotateY: -45,
3873
+ rotateZ: 0,
3874
+ },
3875
+ perspectiveRelaxed: { perspective: '1200px', rotateX: -10, rotateY: 0, rotateZ: 0 },
3876
+ perspectiveRelaxedModerately: { perspective: '1400px', rotateX: -5, rotateY: 0, rotateZ: 0 },
3877
+ isometricLeftDown: { perspective: '1200px', rotateX: -35, rotateY: 45, rotateZ: 0 },
3878
+ isometricRightUp: { perspective: '1200px', rotateX: -35, rotateY: -45, rotateZ: 0 },
3879
+ isometricTopUp: { perspective: '1200px', rotateX: -55, rotateY: 0, rotateZ: 45 },
3880
+ isometricTopDown: { perspective: '1200px', rotateX: -55, rotateY: 0, rotateZ: -45 },
3881
+ isometricBottomUp: { perspective: '1200px', rotateX: 55, rotateY: 0, rotateZ: 45 },
3882
+ isometricBottomDown: { perspective: '1200px', rotateX: 55, rotateY: 0, rotateZ: -45 },
3883
+ isometricOffAxis1Left: { perspective: '1200px', rotateX: -30, rotateY: 30, rotateZ: 0 },
3884
+ isometricOffAxis1Right: { perspective: '1200px', rotateX: -30, rotateY: -30, rotateZ: 0 },
3885
+ isometricOffAxis1Top: { perspective: '1200px', rotateX: -45, rotateY: 0, rotateZ: 30 },
3886
+ isometricOffAxis2Left: { perspective: '1200px', rotateX: -30, rotateY: 20, rotateZ: 0 },
3887
+ isometricOffAxis2Right: { perspective: '1200px', rotateX: -30, rotateY: -20, rotateZ: 0 },
3888
+ isometricOffAxis2Top: { perspective: '1200px', rotateX: -45, rotateY: 0, rotateZ: -30 },
3889
+ isometricOffAxis3Left: { perspective: '1200px', rotateX: -25, rotateY: 35, rotateZ: 0 },
3890
+ isometricOffAxis3Right: { perspective: '1200px', rotateX: -25, rotateY: -35, rotateZ: 0 },
3891
+ isometricOffAxis3Bottom: { perspective: '1200px', rotateX: 45, rotateY: 0, rotateZ: 30 },
3892
+ isometricOffAxis4Left: { perspective: '1200px', rotateX: -25, rotateY: 25, rotateZ: 0 },
3893
+ isometricOffAxis4Right: { perspective: '1200px', rotateX: -25, rotateY: -25, rotateZ: 0 },
3894
+ isometricOffAxis4Bottom: { perspective: '1200px', rotateX: 45, rotateY: 0, rotateZ: -30 },
3895
+ obliqueTopLeft: { perspective: '900px', rotateX: -20, rotateY: 20, rotateZ: 0 },
3896
+ obliqueTop: { perspective: '900px', rotateX: -25, rotateY: 0, rotateZ: 0 },
3897
+ obliqueTopRight: { perspective: '900px', rotateX: -20, rotateY: -20, rotateZ: 0 },
3898
+ obliqueLeft: { perspective: '900px', rotateX: 0, rotateY: 25, rotateZ: 0 },
3899
+ obliqueRight: { perspective: '900px', rotateX: 0, rotateY: -25, rotateZ: 0 },
3900
+ obliqueBottomLeft: { perspective: '900px', rotateX: 20, rotateY: 20, rotateZ: 0 },
3901
+ obliqueBottom: { perspective: '900px', rotateX: 25, rotateY: 0, rotateZ: 0 },
3902
+ obliqueBottomRight: { perspective: '900px', rotateX: 20, rotateY: -20, rotateZ: 0 },
3903
+ };
3904
+ /**
3905
+ * Resolve a camera preset name + explicit rotation overrides into final CSS
3906
+ * perspective and rotation (degrees). Explicit `cameraRot*` (1/60000 deg)
3907
+ * override preset defaults; the X axis is negated to match CSS conventions.
3908
+ */
3909
+ function getCameraTransform(scene3d) {
3910
+ if (!scene3d) {
3911
+ return { rotateX: 0, rotateY: 0, rotateZ: 0 };
3912
+ }
3913
+ const preset = scene3d.cameraPreset ? CAMERA_PRESET_MAP[scene3d.cameraPreset] : undefined;
3914
+ let perspective = preset?.perspective;
3915
+ let rotateX = preset?.rotateX ?? 0;
3916
+ let rotateY = preset?.rotateY ?? 0;
3917
+ let rotateZ = preset?.rotateZ ?? 0;
3918
+ if (scene3d.cameraRotX) {
3919
+ rotateX = -(scene3d.cameraRotX / 60000);
3920
+ }
3921
+ if (scene3d.cameraRotY) {
3922
+ rotateY = scene3d.cameraRotY / 60000;
3923
+ }
3924
+ if (scene3d.cameraRotZ) {
3925
+ rotateZ = scene3d.cameraRotZ / 60000;
3926
+ }
3927
+ if (!perspective && (rotateX !== 0 || rotateY !== 0 || rotateZ !== 0)) {
3928
+ perspective = '800px';
3929
+ }
3930
+ return { perspective, rotateX, rotateY, rotateZ };
3931
+ }
3932
+ const LIGHT_RIG_MAP = {
3933
+ threePt: {
3934
+ backgroundImage: [
3935
+ 'linear-gradient(135deg, rgba(255,255,255,0.14) 0%, transparent 35%)',
3936
+ 'linear-gradient(315deg, rgba(255,255,255,0.05) 0%, transparent 25%)',
3937
+ 'linear-gradient(0deg, rgba(0,0,0,0.06) 0%, transparent 20%)',
3938
+ ].join(', '),
3939
+ },
3940
+ balanced: {
3941
+ backgroundImage: [
3942
+ 'linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 40%)',
3943
+ 'linear-gradient(0deg, rgba(255,255,255,0.03) 0%, transparent 30%)',
3944
+ 'linear-gradient(90deg, rgba(255,255,255,0.02) 0%, transparent 20%)',
3945
+ ].join(', '),
3946
+ },
3947
+ harsh: {
3948
+ backgroundImage: [
3949
+ 'linear-gradient(135deg, rgba(255,255,255,0.22) 0%, transparent 28%)',
3950
+ 'linear-gradient(315deg, rgba(0,0,0,0.12) 0%, transparent 40%)',
3951
+ ].join(', '),
3952
+ filter: 'contrast(1.08)',
3953
+ },
3954
+ flat: {},
3955
+ flood: {
3956
+ backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.03) 50%, transparent 100%)',
3957
+ filter: 'brightness(1.08)',
3958
+ },
3959
+ contrasting: {
3960
+ backgroundImage: [
3961
+ 'linear-gradient(120deg, rgba(255,255,255,0.2) 0%, transparent 30%)',
3962
+ 'linear-gradient(300deg, rgba(0,0,0,0.1) 0%, transparent 35%)',
3963
+ ].join(', '),
3964
+ filter: 'contrast(1.1)',
3965
+ },
3966
+ morning: {
3967
+ backgroundImage: [
3968
+ 'linear-gradient(90deg, rgba(255,240,200,0.16) 0%, transparent 45%)',
3969
+ 'linear-gradient(270deg, rgba(0,0,0,0.04) 0%, transparent 30%)',
3970
+ ].join(', '),
3971
+ },
3972
+ sunrise: {
3973
+ backgroundImage: [
3974
+ 'linear-gradient(45deg, rgba(255,220,180,0.16) 0%, transparent 40%)',
3975
+ 'radial-gradient(ellipse at 20% 80%, rgba(255,200,140,0.08) 0%, transparent 50%)',
3976
+ ].join(', '),
3977
+ },
3978
+ sunset: {
3979
+ backgroundImage: [
3980
+ 'linear-gradient(270deg, rgba(255,180,100,0.14) 0%, transparent 45%)',
3981
+ 'radial-gradient(ellipse at 85% 50%, rgba(255,160,60,0.06) 0%, transparent 40%)',
3982
+ ].join(', '),
3983
+ },
3984
+ chilly: {
3985
+ backgroundImage: [
3986
+ 'linear-gradient(180deg, rgba(180,200,255,0.1) 0%, transparent 50%)',
3987
+ 'radial-gradient(ellipse at center, rgba(200,220,255,0.04) 0%, transparent 60%)',
3988
+ ].join(', '),
3989
+ },
3990
+ freezing: {
3991
+ backgroundImage: [
3992
+ 'linear-gradient(180deg, rgba(160,190,255,0.16) 0%, transparent 40%)',
3993
+ 'linear-gradient(0deg, rgba(140,170,255,0.06) 0%, transparent 25%)',
3994
+ ].join(', '),
3995
+ filter: 'saturate(0.9)',
3996
+ },
3997
+ glow: {
3998
+ backgroundImage: 'radial-gradient(ellipse at center, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.04) 40%, transparent 70%)',
3999
+ },
4000
+ brightRoom: {
4001
+ backgroundImage: [
4002
+ 'linear-gradient(180deg, rgba(255,255,255,0.1) 0%, transparent 50%)',
4003
+ 'radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.06) 0%, transparent 60%)',
4004
+ ].join(', '),
4005
+ filter: 'brightness(1.05)',
4006
+ },
4007
+ soft: {
4008
+ backgroundImage: [
4009
+ 'linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 40%)',
4010
+ 'radial-gradient(ellipse at center, rgba(255,255,255,0.03) 0%, transparent 60%)',
4011
+ ].join(', '),
4012
+ filter: 'contrast(0.95)',
4013
+ },
4014
+ twoPt: {
4015
+ backgroundImage: [
4016
+ 'linear-gradient(90deg, rgba(255,255,255,0.12) 0%, transparent 35%)',
4017
+ 'linear-gradient(270deg, rgba(255,255,255,0.07) 0%, transparent 30%)',
4018
+ ].join(', '),
4019
+ },
4020
+ legacyFlat1: {},
4021
+ legacyFlat2: {},
4022
+ legacyFlat3: {},
4023
+ legacyFlat4: {},
4024
+ legacyNormal1: {
4025
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%)',
4026
+ },
4027
+ legacyNormal2: {
4028
+ backgroundImage: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, transparent 50%)',
4029
+ },
4030
+ legacyNormal3: {
4031
+ backgroundImage: 'linear-gradient(120deg, rgba(255,255,255,0.1) 0%, transparent 50%)',
4032
+ },
4033
+ legacyNormal4: {
4034
+ backgroundImage: 'linear-gradient(150deg, rgba(255,255,255,0.1) 0%, transparent 50%)',
4035
+ },
4036
+ legacyHarsh1: {
4037
+ backgroundImage: [
4038
+ 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 28%)',
4039
+ 'linear-gradient(315deg, rgba(0,0,0,0.1) 0%, transparent 35%)',
4040
+ ].join(', '),
4041
+ filter: 'contrast(1.1)',
4042
+ },
4043
+ legacyHarsh2: {
4044
+ backgroundImage: [
4045
+ 'linear-gradient(135deg, rgba(255,255,255,0.16) 0%, transparent 28%)',
4046
+ 'linear-gradient(315deg, rgba(0,0,0,0.08) 0%, transparent 35%)',
4047
+ ].join(', '),
4048
+ filter: 'contrast(1.08)',
4049
+ },
4050
+ legacyHarsh3: {
4051
+ backgroundImage: [
4052
+ 'linear-gradient(120deg, rgba(255,255,255,0.2) 0%, transparent 28%)',
4053
+ 'linear-gradient(300deg, rgba(0,0,0,0.1) 0%, transparent 35%)',
4054
+ ].join(', '),
4055
+ filter: 'contrast(1.1)',
4056
+ },
4057
+ legacyHarsh4: {
4058
+ backgroundImage: [
4059
+ 'linear-gradient(150deg, rgba(255,255,255,0.2) 0%, transparent 28%)',
4060
+ 'linear-gradient(330deg, rgba(0,0,0,0.1) 0%, transparent 35%)',
4061
+ ].join(', '),
4062
+ filter: 'contrast(1.1)',
4063
+ },
4064
+ };
4065
+ /** Map a light-rig direction token to a CSS gradient angle (degrees). */
4066
+ function getLightDirectionAngle(direction) {
4067
+ switch (direction) {
4068
+ case 't':
4069
+ return 180;
4070
+ case 'b':
4071
+ return 0;
4072
+ case 'l':
4073
+ return 90;
4074
+ case 'r':
4075
+ return 270;
4076
+ case 'tl':
4077
+ return 135;
4078
+ case 'tr':
4079
+ return 225;
4080
+ case 'bl':
4081
+ return 45;
4082
+ case 'br':
4083
+ return 315;
4084
+ default:
4085
+ return 135;
4086
+ }
4087
+ }
4088
+ /** Shift every `linear-gradient(Ndeg` angle in a background-image by a delta. */
4089
+ function rotateGradientAngles(backgroundImage, angleDelta) {
4090
+ if (angleDelta === 0) {
4091
+ return backgroundImage;
4092
+ }
4093
+ return backgroundImage.replace(/linear-gradient\((?<deg>\d+)deg/gu, (_match, degStr) => {
4094
+ const newAngle = (parseInt(degStr, 10) + angleDelta + 360) % 360;
4095
+ return `linear-gradient(${newAngle}deg`;
4096
+ });
4097
+ }
4098
+ /** Resolve light rig CSS overrides for a given rig type + direction. */
4099
+ function getLightRigCss(lightRigType, lightRigDirection) {
4100
+ if (!lightRigType) {
4101
+ return {};
4102
+ }
4103
+ const config = LIGHT_RIG_MAP[lightRigType];
4104
+ if (!config) {
4105
+ return {};
4106
+ }
4107
+ if (config.backgroundImage && lightRigDirection) {
4108
+ const targetAngle = getLightDirectionAngle(lightRigDirection);
4109
+ const delta = targetAngle - 135;
4110
+ if (delta !== 0) {
4111
+ return {
4112
+ ...config,
4113
+ backgroundImage: rotateGradientAngles(config.backgroundImage, delta),
4114
+ };
4115
+ }
4116
+ }
4117
+ return config;
4118
+ }
4119
+ // ── Bevel preset mapping ─────────────────────────────────────────────────
4120
+ function getBevelShadow(bevelType, bW, bH, isBottom) {
4121
+ const hlDir = isBottom ? -1 : 1;
4122
+ const shDir = isBottom ? 1 : -1;
4123
+ const hlOpacity = isBottom ? 0.2 : 0.3;
4124
+ const shOpacity = isBottom ? 0.3 : 0.2;
4125
+ const maxDim = Math.max(bW, bH);
4126
+ switch (bevelType) {
4127
+ case 'circle':
4128
+ return [
4129
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim + 2}px rgba(255,255,255,${hlOpacity + 0.12})`,
4130
+ `inset ${hlDir * Math.round(bW * 0.5)}px ${hlDir * Math.round(bH * 0.5)}px ${maxDim + 4}px rgba(255,255,255,${hlOpacity * 0.4})`,
4131
+ `inset ${shDir * bW}px ${shDir * bH}px ${maxDim + 2}px rgba(0,0,0,${shOpacity + 0.06})`,
4132
+ `inset ${shDir * Math.round(bW * 0.5)}px ${shDir * Math.round(bH * 0.5)}px ${maxDim + 4}px rgba(0,0,0,${shOpacity * 0.3})`,
4133
+ ].join(', ');
4134
+ case 'relaxedInset':
4135
+ return [
4136
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim + 5}px rgba(255,255,255,${hlOpacity - 0.04})`,
4137
+ `inset ${shDir * bW}px ${shDir * bH}px ${maxDim + 5}px rgba(0,0,0,${shOpacity - 0.04})`,
4138
+ `inset 0 0 ${maxDim + 8}px rgba(0,0,0,${shOpacity * 0.15})`,
4139
+ ].join(', ');
4140
+ case 'hardEdge':
4141
+ return [
4142
+ `inset ${hlDir * bW}px ${hlDir * bH}px 0 rgba(255,255,255,${hlOpacity + 0.18})`,
4143
+ `inset ${shDir * bW}px ${shDir * bH}px 0 rgba(0,0,0,${shOpacity + 0.18})`,
4144
+ `inset ${hlDir * Math.round(bW * 0.4)}px ${hlDir * Math.round(bH * 0.4)}px 0 rgba(255,255,255,${hlOpacity * 0.3})`,
4145
+ ].join(', ');
4146
+ case 'cross':
4147
+ return [
4148
+ `inset ${hlDir * bW}px 0 ${bW}px rgba(255,255,255,${hlOpacity})`,
4149
+ `inset 0 ${hlDir * bH}px ${bH}px rgba(255,255,255,${hlOpacity})`,
4150
+ `inset ${shDir * bW}px 0 ${bW}px rgba(0,0,0,${shOpacity})`,
4151
+ `inset 0 ${shDir * bH}px ${bH}px rgba(0,0,0,${shOpacity})`,
4152
+ `inset 0 0 ${Math.round(maxDim * 0.5)}px rgba(0,0,0,${shOpacity * 0.2})`,
4153
+ ].join(', ');
4154
+ case 'coolSlant':
4155
+ return [
4156
+ `inset ${hlDir * bW}px ${hlDir * Math.round(bH * 0.4)}px ${maxDim}px rgba(255,255,255,${hlOpacity + 0.12})`,
4157
+ `inset ${hlDir * Math.round(bW * 0.6)}px 0 ${Math.round(maxDim * 0.6)}px rgba(255,255,255,${hlOpacity * 0.4})`,
4158
+ `inset ${shDir * Math.round(bW * 0.4)}px ${shDir * bH}px ${maxDim}px rgba(0,0,0,${shOpacity + 0.1})`,
4159
+ ].join(', ');
4160
+ case 'angle':
4161
+ return [
4162
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${Math.round(maxDim * 0.4)}px rgba(255,255,255,${hlOpacity + 0.16})`,
4163
+ `inset ${hlDir * Math.round(bW * 0.5)}px ${hlDir * Math.round(bH * 0.5)}px 0 rgba(255,255,255,${hlOpacity * 0.5})`,
4164
+ `inset ${shDir * bW}px ${shDir * bH}px ${Math.round(maxDim * 0.4)}px rgba(0,0,0,${shOpacity + 0.12})`,
4165
+ ].join(', ');
4166
+ case 'softRound':
4167
+ return [
4168
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim + 7}px rgba(255,255,255,${hlOpacity + 0.02})`,
4169
+ `inset ${hlDir * Math.round(bW * 0.3)}px ${hlDir * Math.round(bH * 0.3)}px ${maxDim + 10}px rgba(255,255,255,${hlOpacity * 0.3})`,
4170
+ `inset ${shDir * bW}px ${shDir * bH}px ${maxDim + 7}px rgba(0,0,0,${shOpacity - 0.04})`,
4171
+ ].join(', ');
4172
+ case 'convex':
4173
+ return [
4174
+ `inset 0 0 ${maxDim + 4}px rgba(255,255,255,${hlOpacity + 0.06})`,
4175
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim}px rgba(255,255,255,${hlOpacity + 0.02})`,
4176
+ `inset ${shDir * bW}px ${shDir * bH}px ${maxDim}px rgba(0,0,0,${shOpacity})`,
4177
+ `inset ${shDir * Math.round(bW * 1.5)}px ${shDir * Math.round(bH * 1.5)}px ${maxDim + 2}px rgba(0,0,0,${shOpacity * 0.3})`,
4178
+ ].join(', ');
4179
+ case 'slope':
4180
+ return [
4181
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim + 4}px rgba(255,255,255,${hlOpacity + 0.06})`,
4182
+ `inset ${hlDir * Math.round(bW * 0.5)}px ${hlDir * Math.round(bH * 0.5)}px ${maxDim + 6}px rgba(255,255,255,${hlOpacity * 0.35})`,
4183
+ `inset ${shDir * Math.round(bW * 0.7)}px ${shDir * Math.round(bH * 0.7)}px ${maxDim}px rgba(0,0,0,${shOpacity})`,
4184
+ ].join(', ');
4185
+ case 'divot':
4186
+ return [
4187
+ `inset ${shDir * Math.round(bW * 0.5)}px ${shDir * Math.round(bH * 0.5)}px ${Math.round(maxDim * 0.5)}px rgba(255,255,255,${hlOpacity + 0.06})`,
4188
+ `inset ${hlDir * Math.round(bW * 0.5)}px ${hlDir * Math.round(bH * 0.5)}px ${Math.round(maxDim * 0.5)}px rgba(0,0,0,${shOpacity + 0.12})`,
4189
+ `inset 0 0 ${Math.round(maxDim * 0.3)}px rgba(0,0,0,${shOpacity * 0.3})`,
4190
+ ].join(', ');
4191
+ case 'riblet':
4192
+ return [
4193
+ `inset 0 ${hlDir * bH}px ${Math.round(bH * 0.4)}px rgba(255,255,255,${hlOpacity + 0.02})`,
4194
+ `inset 0 ${shDir * bH}px ${Math.round(bH * 0.4)}px rgba(0,0,0,${shOpacity})`,
4195
+ `inset 0 ${hlDir * Math.round(bH * 2)}px ${bH}px rgba(255,255,255,${hlOpacity * 0.45})`,
4196
+ `inset 0 ${shDir * Math.round(bH * 2)}px ${bH}px rgba(0,0,0,${shOpacity * 0.25})`,
4197
+ ].join(', ');
4198
+ case 'artDeco':
4199
+ return [
4200
+ `inset ${hlDir * bW}px ${hlDir * bH}px 0 rgba(255,255,255,${hlOpacity + 0.12})`,
4201
+ `inset ${hlDir * Math.round(bW * 2)}px ${hlDir * Math.round(bH * 2)}px 0 rgba(255,255,255,${hlOpacity * 0.45})`,
4202
+ `inset ${hlDir * Math.round(bW * 3)}px ${hlDir * Math.round(bH * 3)}px 0 rgba(255,255,255,${hlOpacity * 0.2})`,
4203
+ `inset ${shDir * bW}px ${shDir * bH}px 0 rgba(0,0,0,${shOpacity + 0.12})`,
4204
+ `inset ${shDir * Math.round(bW * 2)}px ${shDir * Math.round(bH * 2)}px 0 rgba(0,0,0,${shOpacity * 0.4})`,
4205
+ ].join(', ');
4206
+ default:
4207
+ return [
4208
+ `inset ${hlDir * bW}px ${hlDir * bH}px ${maxDim}px rgba(255,255,255,${hlOpacity})`,
4209
+ `inset ${shDir * bW}px ${shDir * bH}px ${maxDim}px rgba(0,0,0,${shOpacity})`,
4210
+ ].join(', ');
4211
+ }
4212
+ }
4213
+ // ── Extrusion shadow generation ──────────────────────────────────────────
4214
+ /** Darken a hex colour by a factor (0 = black, 1 = unchanged). */
4215
+ function darkenColor(hex, factor) {
4216
+ const clean = hex.replace('#', '');
4217
+ const r = Math.round(parseInt(clean.slice(0, 2), 16) * factor);
4218
+ const g = Math.round(parseInt(clean.slice(2, 4), 16) * factor);
4219
+ const b = Math.round(parseInt(clean.slice(4, 6), 16) * factor);
4220
+ return `rgb(${r},${g},${b})`;
4221
+ }
4222
+ /** Compute (dx, dy) extrusion offset direction from camera rotation. */
4223
+ function getExtrusionDirection(rotateX, rotateY) {
4224
+ let dx = 1;
4225
+ let dy = 1;
4226
+ if (rotateY > 5) {
4227
+ dx = -1;
4228
+ }
4229
+ else if (rotateY < -5) {
4230
+ dx = 1;
4231
+ }
4232
+ if (rotateX < -5) {
4233
+ dy = 1;
4234
+ }
4235
+ else if (rotateX > 5) {
4236
+ dy = -1;
4237
+ }
4238
+ return { dx, dy };
4239
+ }
4240
+ /**
4241
+ * Camera/perspective → CSS transform. Maps OOXML camera presets and explicit
4242
+ * rotation angles to `perspective(...)` + `rotateX/Y/Z(...)`. When extrusion is
4243
+ * present a `translateZ` is appended so the front face sits above the stacked
4244
+ * box-shadow depth (mirrors React's `apply3dEffects`).
4245
+ *
4246
+ * Returns `undefined` when there is nothing 3D to apply.
4247
+ */
4248
+ function get3dTransformCss(scene3d, shape3d) {
4249
+ if (!scene3d && !shape3d) {
4250
+ return undefined;
4251
+ }
4252
+ const { perspective, rotateX, rotateY, rotateZ } = getCameraTransform(scene3d);
4253
+ const hasRotation = rotateX !== 0 || rotateY !== 0 || rotateZ !== 0;
4254
+ const hasExtrusion = Boolean(shape3d?.extrusionHeight && shape3d.extrusionHeight > 0);
4255
+ const transforms = [];
4256
+ if (rotateX !== 0) {
4257
+ transforms.push(`rotateX(${rotateX}deg)`);
4258
+ }
4259
+ if (rotateY !== 0) {
4260
+ transforms.push(`rotateY(${rotateY}deg)`);
4261
+ }
4262
+ if (rotateZ !== 0) {
4263
+ transforms.push(`rotateZ(${rotateZ}deg)`);
4264
+ }
4265
+ if (hasExtrusion && shape3d) {
4266
+ const depthPx = Math.max(1, Math.round((shape3d.extrusionHeight ?? 0) / EMU_PER_PX));
4267
+ const halfDepth = Math.min(depthPx, 80) / 2;
4268
+ transforms.push(`translateZ(${halfDepth}px)`);
4269
+ }
4270
+ const has3D = hasRotation || Boolean(perspective) || Boolean(shape3d);
4271
+ if (!has3D && transforms.length === 0) {
4272
+ return undefined;
4273
+ }
4274
+ const result = {};
4275
+ if (perspective) {
4276
+ result.perspective = perspective;
4277
+ }
4278
+ if (transforms.length > 0) {
4279
+ result.transform = transforms.join(' ');
4280
+ }
4281
+ if (has3D) {
4282
+ result.transformStyle = 'preserve-3d';
4283
+ }
4284
+ return result;
4285
+ }
4286
+ /**
4287
+ * Extrusion depth → layered `box-shadow`. Stacks up to {@link MAX_EXTRUSION_LAYERS}
4288
+ * offset shadows (radiating per camera angle) with a final soft shadow for
4289
+ * depth perception. Returns `undefined` when there is no extrusion.
4290
+ */
4291
+ function getExtrusionBoxShadow(shape3d, cameraRotX = 0, cameraRotY = 0) {
4292
+ if (!shape3d?.extrusionHeight || shape3d.extrusionHeight <= 0) {
4293
+ return undefined;
4294
+ }
4295
+ const rawDepthPx = Math.round(shape3d.extrusionHeight / EMU_PER_PX);
4296
+ if (rawDepthPx <= 0) {
4297
+ return undefined;
4298
+ }
4299
+ const layerCount = Math.min(rawDepthPx, MAX_EXTRUSION_LAYERS);
4300
+ const step = rawDepthPx / layerCount;
4301
+ const extColor = shape3d.extrusionColor || '#888888';
4302
+ const { dx, dy } = getExtrusionDirection(cameraRotX, cameraRotY);
4303
+ const depthShadows = [];
4304
+ for (let i = 1; i <= layerCount; i++) {
4305
+ const offset = Math.round(i * step);
4306
+ const darkenFactor = 1 - (i / layerCount) * 0.25;
4307
+ const layerColor = i > layerCount * 0.7 ? darkenColor(extColor, darkenFactor) : extColor;
4308
+ const spread = step > 1.5 ? Math.ceil(step / 2) : 0;
4309
+ depthShadows.push(`${dx * offset}px ${dy * offset}px ${spread}px ${layerColor}`);
4310
+ }
4311
+ const finalOffset = rawDepthPx + 1;
4312
+ depthShadows.push(`${dx * finalOffset}px ${dy * finalOffset}px ${Math.max(2, Math.round(rawDepthPx / 3))}px rgba(0,0,0,0.2)`);
4313
+ return depthShadows.join(', ');
4314
+ }
4315
+ /** Contour (outline ring) → box-shadow. Returns `undefined` when no contour. */
4316
+ function getContourBoxShadow(shape3d) {
4317
+ if (!shape3d?.contourWidth || shape3d.contourWidth <= 0) {
4318
+ return undefined;
4319
+ }
4320
+ const widthPx = Math.max(1, Math.round(shape3d.contourWidth / EMU_PER_PX));
4321
+ const color = shape3d.contourColor || '#000000';
4322
+ return `0 0 0 ${widthPx}px ${color}`;
4323
+ }
4324
+ /**
4325
+ * Bevel preset → inset `box-shadow` (top + bottom bevels combined), plus an
4326
+ * optional background gradient for presets that benefit from one
4327
+ * (convex/divot/softRound). Returns `undefined` when no bevel is present.
4328
+ */
4329
+ function getBevelStyle(shape3d) {
4330
+ if (!shape3d) {
4331
+ return undefined;
4332
+ }
4333
+ const parts = [];
4334
+ if (shape3d.bevelTopType && shape3d.bevelTopType !== 'none') {
4335
+ const bW = shape3d.bevelTopWidth
4336
+ ? Math.max(1, Math.round(shape3d.bevelTopWidth / EMU_PER_PX))
4337
+ : 3;
4338
+ const bH = shape3d.bevelTopHeight
4339
+ ? Math.max(1, Math.round(shape3d.bevelTopHeight / EMU_PER_PX))
4340
+ : 3;
4341
+ parts.push(getBevelShadow(shape3d.bevelTopType, bW, bH, false));
4342
+ }
4343
+ if (shape3d.bevelBottomType && shape3d.bevelBottomType !== 'none') {
4344
+ const bW = shape3d.bevelBottomWidth
4345
+ ? Math.max(1, Math.round(shape3d.bevelBottomWidth / EMU_PER_PX))
4346
+ : 3;
4347
+ const bH = shape3d.bevelBottomHeight
4348
+ ? Math.max(1, Math.round(shape3d.bevelBottomHeight / EMU_PER_PX))
4349
+ : 3;
4350
+ parts.push(getBevelShadow(shape3d.bevelBottomType, bW, bH, true));
4351
+ }
4352
+ if (parts.length === 0) {
4353
+ return undefined;
4354
+ }
4355
+ let background;
4356
+ switch (shape3d.bevelTopType) {
4357
+ case 'convex':
4358
+ background =
4359
+ 'radial-gradient(ellipse at 40% 35%, rgba(255,255,255,0.08) 0%, transparent 60%)';
4360
+ break;
4361
+ case 'divot':
4362
+ background = 'radial-gradient(ellipse at 50% 50%, rgba(0,0,0,0.04) 0%, transparent 50%)';
4363
+ break;
4364
+ case 'softRound':
4365
+ background =
4366
+ 'radial-gradient(ellipse at 35% 30%, rgba(255,255,255,0.06) 0%, transparent 55%)';
4367
+ break;
4368
+ }
4369
+ return { boxShadow: parts.join(', '), background };
4370
+ }
4371
+ /** Material preset → CSS `filter`. Returns `undefined` when none/flat. */
4372
+ function getMaterialFilter(shape3d) {
4373
+ if (!shape3d?.presetMaterial) {
4374
+ return undefined;
4375
+ }
4376
+ return getMaterialCssOverrides(shape3d.presetMaterial).filter;
4377
+ }
4378
+ /**
4379
+ * Compute the complete set of 3D CSS for an element's shape style. Reads
4380
+ * `scene3d`/`shape3d` off the element's `shapeStyle`. Returns `undefined`
4381
+ * when the element carries no 3D data so callers can skip merging entirely.
4382
+ */
4383
+ function getComputed3dStyle(el) {
4384
+ if (!hasShapeProperties(el)) {
4385
+ return undefined;
4386
+ }
4387
+ const ss = el.shapeStyle;
4388
+ const scene3d = ss?.scene3d;
4389
+ const shape3d = ss?.shape3d;
4390
+ if (!scene3d && !shape3d) {
4391
+ return undefined;
4392
+ }
4393
+ const result = {};
4394
+ // ── Camera / perspective / rotation ──
4395
+ const { perspective, rotateX, rotateY, rotateZ } = getCameraTransform(scene3d);
4396
+ const transformCss = get3dTransformCss(scene3d, shape3d);
4397
+ if (transformCss?.perspective) {
4398
+ result.perspective = transformCss.perspective;
4399
+ }
4400
+ if (transformCss?.transform) {
4401
+ result.transform = transformCss.transform;
4402
+ }
4403
+ const hasRotation = rotateX !== 0 || rotateY !== 0 || rotateZ !== 0;
4404
+ if (hasRotation || perspective || shape3d) {
4405
+ result.willChange = 'transform';
4406
+ result.transformStyle = 'preserve-3d';
4407
+ }
4408
+ // ── Extrusion (kept SEPARATE for shadow combination) ──
4409
+ const extrusion = getExtrusionBoxShadow(shape3d, rotateX, rotateY);
4410
+ if (extrusion) {
4411
+ result.extrusionBoxShadow = extrusion;
4412
+ }
4413
+ // ── Contour + bevel + backdrop shadows (folded into boxShadow) ──
4414
+ const shadowParts = [];
4415
+ const contour = getContourBoxShadow(shape3d);
4416
+ if (contour) {
4417
+ shadowParts.push(contour);
4418
+ }
4419
+ const bevel = getBevelStyle(shape3d);
4420
+ if (bevel) {
4421
+ shadowParts.push(bevel.boxShadow);
4422
+ if (bevel.background) {
4423
+ result.background = bevel.background;
4424
+ }
4425
+ }
4426
+ if (scene3d?.hasBackdrop) {
4427
+ shadowParts.push('0px 8px 24px -4px rgba(0,0,0,0.25)');
4428
+ }
4429
+ // ── Material preset ──
4430
+ const filterParts = [];
4431
+ const bgParts = [];
4432
+ if (shape3d?.presetMaterial) {
4433
+ const mat = getMaterialCssOverrides(shape3d.presetMaterial);
4434
+ if (mat.filter) {
4435
+ filterParts.push(mat.filter);
4436
+ }
4437
+ if (mat.opacity !== undefined) {
4438
+ result.opacity = mat.opacity;
4439
+ }
4440
+ if (mat.boxShadow) {
4441
+ shadowParts.push(mat.boxShadow);
4442
+ }
4443
+ if (mat.backgroundImage) {
4444
+ bgParts.push(mat.backgroundImage);
4445
+ }
4446
+ }
4447
+ // ── Light rig ──
4448
+ const lightRig = getLightRigCss(scene3d?.lightRigType, scene3d?.lightRigDirection);
4449
+ if (lightRig.filter) {
4450
+ filterParts.push(lightRig.filter);
4451
+ }
4452
+ if (lightRig.backgroundImage) {
4453
+ bgParts.push(lightRig.backgroundImage);
4454
+ }
4455
+ if (shadowParts.length > 0) {
4456
+ result.boxShadow = shadowParts.join(', ');
4457
+ }
4458
+ if (filterParts.length > 0) {
4459
+ result.filter = filterParts.join(' ');
4460
+ }
4461
+ if (bgParts.length > 0) {
4462
+ result.backgroundImage = bgParts.join(', ');
4463
+ }
4464
+ return result;
4465
+ }
4466
+
4467
+ /** Map an OOXML `a:prstDash/@val` value to a CSS `border-style` keyword. */
4468
+ function ooxmlDashToCssBorderStyle$1(dashVal) {
4469
+ if (!dashVal) {
4470
+ return 'solid';
4471
+ }
4472
+ switch (dashVal) {
4473
+ case 'dot':
4474
+ case 'sysDot':
4475
+ return 'dotted';
4476
+ case 'dash':
4477
+ case 'sysDash':
4478
+ case 'lgDash':
4479
+ case 'dashDot':
4480
+ case 'lgDashDot':
4481
+ case 'sysDashDot':
4482
+ case 'lgDashDotDot':
4483
+ case 'sysDashDotDot':
4484
+ return 'dashed';
4485
+ default:
4486
+ return 'solid';
4487
+ }
4488
+ }
4489
+ /**
4490
+ * Convert a structured {@link PptxTableCellStyle} to CSS properties.
4491
+ *
4492
+ * Mirrors the React `cellStyleToCss`, minus the SVG pattern-fill branch
4493
+ * (approximated by its background colour here).
4494
+ */
4495
+ function cellStyleToCss(style) {
4496
+ if (!style) {
4497
+ return {};
4498
+ }
4499
+ const css = {};
4500
+ if (style.fontSize) {
4501
+ css.fontSize = `${style.fontSize}px`;
4502
+ }
4503
+ if (style.bold) {
4504
+ css.fontWeight = 'bold';
4505
+ }
4506
+ if (style.italic) {
4507
+ css.fontStyle = 'italic';
4508
+ }
4509
+ if (style.underline) {
4510
+ css.textDecorationLine = 'underline';
4511
+ }
4512
+ if (style.color) {
4513
+ css.color = style.color;
4514
+ }
4515
+ // Cell background fill — gradient takes precedence, then pattern (approximated
4516
+ // by its background colour), then a solid colour.
4517
+ if (style.gradientFillCss) {
4518
+ css.background = style.gradientFillCss;
4519
+ }
4520
+ else if (style.fillMode === 'pattern') {
4521
+ const patternBg = style.patternFillBackground ?? style.backgroundColor;
4522
+ if (patternBg) {
4523
+ css.backgroundColor = patternBg;
4524
+ }
4525
+ }
4526
+ else if (style.backgroundColor) {
4527
+ css.backgroundColor = style.backgroundColor;
4528
+ }
4529
+ if (style.align) {
4530
+ css.textAlign = style.align;
4531
+ }
4532
+ if (style.vAlign) {
4533
+ css.verticalAlign = style.vAlign;
4534
+ }
4535
+ // Vertical text direction — map all variants to CSS writing-mode + orientation.
4536
+ if (style.textDirection) {
4537
+ switch (style.textDirection) {
4538
+ case 'vert':
4539
+ case 'eaVert':
4540
+ case 'wordArtVert':
4541
+ case 'wordArtVertRtl':
4542
+ css.writingMode = 'vertical-rl';
4543
+ break;
4544
+ case 'vert270':
4545
+ case 'mongolianVert':
4546
+ css.writingMode = 'vertical-lr';
4547
+ break;
4548
+ }
4549
+ if (style.textDirection === 'wordArtVert') {
4550
+ css.textOrientation = 'upright';
4551
+ }
4552
+ else if (css.writingMode) {
4553
+ css.textOrientation = 'mixed';
4554
+ }
4555
+ if (style.textDirection === 'wordArtVertRtl') {
4556
+ css.direction = 'rtl';
4557
+ }
4558
+ }
4559
+ // Per-edge borders (width, color, dash style).
4560
+ const borderEdges = [
4561
+ {
4562
+ prefix: 'borderTop',
4563
+ width: style.borderTopWidth,
4564
+ color: style.borderTopColor,
4565
+ dash: style.borderTopDash,
4566
+ },
4567
+ {
4568
+ prefix: 'borderBottom',
4569
+ width: style.borderBottomWidth,
4570
+ color: style.borderBottomColor,
4571
+ dash: style.borderBottomDash,
4572
+ },
4573
+ {
4574
+ prefix: 'borderLeft',
4575
+ width: style.borderLeftWidth,
4576
+ color: style.borderLeftColor,
4577
+ dash: style.borderLeftDash,
4578
+ },
4579
+ {
4580
+ prefix: 'borderRight',
4581
+ width: style.borderRightWidth,
4582
+ color: style.borderRightColor,
4583
+ dash: style.borderRightDash,
4584
+ },
4585
+ ];
4586
+ for (const edge of borderEdges) {
4587
+ if (edge.width || edge.color) {
4588
+ const w = edge.width ?? 1;
4589
+ const c = edge.color ?? style.borderColor ?? '#000000';
4590
+ const s = ooxmlDashToCssBorderStyle$1(edge.dash);
4591
+ css[edge.prefix] = `${w}px ${s} ${c}`;
4592
+ }
4593
+ }
4594
+ // Cell margins → padding.
4595
+ if (style.marginLeft) {
4596
+ css.paddingLeft = `${style.marginLeft}px`;
4597
+ }
4598
+ if (style.marginRight) {
4599
+ css.paddingRight = `${style.marginRight}px`;
4600
+ }
4601
+ if (style.marginTop) {
4602
+ css.paddingTop = `${style.marginTop}px`;
4603
+ }
4604
+ if (style.marginBottom) {
4605
+ css.paddingBottom = `${style.marginBottom}px`;
4606
+ }
4607
+ // Text effects (shadow / glow) via CSS text-shadow.
4608
+ const textShadowParts = [];
4609
+ if (style.textShadowColor) {
4610
+ const offX = style.textShadowOffsetX ?? 1;
4611
+ const offY = style.textShadowOffsetY ?? 1;
4612
+ const blur = style.textShadowBlur ?? 0;
4613
+ textShadowParts.push(`${offX}px ${offY}px ${blur}px ${style.textShadowColor}`);
4614
+ }
4615
+ if (style.textGlowColor) {
4616
+ const radius = style.textGlowRadius ?? 2;
4617
+ textShadowParts.push(`0px 0px ${radius}px ${style.textGlowColor}`);
4618
+ }
4619
+ if (textShadowParts.length > 0) {
4620
+ css.textShadow = textShadowParts.join(', ');
4621
+ }
4622
+ return css;
4623
+ }
4624
+ /** Extract diagonal-border info from a cell style, or `null` when none. */
4625
+ function getDiagonalBorders(style) {
4626
+ if (!style) {
4627
+ return null;
4628
+ }
4629
+ const hasDown = Boolean(style.borderDiagDownColor && style.borderDiagDownWidth);
4630
+ const hasUp = Boolean(style.borderDiagUpColor && style.borderDiagUpWidth);
4631
+ if (!hasDown && !hasUp) {
4632
+ return null;
4633
+ }
4634
+ return {
4635
+ diagDownColor: style.borderDiagDownColor,
4636
+ diagDownWidth: style.borderDiagDownWidth,
4637
+ diagUpColor: style.borderDiagUpColor,
4638
+ diagUpWidth: style.borderDiagUpWidth,
4639
+ };
4640
+ }
4641
+ /**
4642
+ * Banding / header / total-row / first-last-column emphasis for a cell.
4643
+ *
4644
+ * A port of the React `getTableCellBandStyle` that operates purely on the
4645
+ * structured {@link PptxTableData} banding flags. Without the parsed table
4646
+ * style map + theme colour scheme (not threaded into the viewer yet) it
4647
+ * uses the same hardcoded fallback colours the React renderer falls back to.
4648
+ *
4649
+ * Returns `undefined` when no banding applies, so callers can treat the
4650
+ * result as a lower-priority style layer beneath explicit cell styles.
4651
+ */
4652
+ function getTableCellBandStyle(tableData, rowIndex, cellIndex, rowCount, columnCount) {
4653
+ if (!tableData) {
4654
+ return undefined;
4655
+ }
4656
+ const style = {};
4657
+ let applied = false;
4658
+ // ── Banded rows (skip the header row when present). ──
4659
+ const bandStartRow = tableData.firstRowHeader ? 1 : 0;
4660
+ const bandEndRow = tableData.lastRow ? rowCount - 1 : rowCount;
4661
+ if (tableData.bandedRows && rowIndex >= bandStartRow && rowIndex < bandEndRow) {
4662
+ const bandIndex = rowIndex - bandStartRow;
4663
+ const rowCycle = Math.max(tableData.bandRowCycle ?? 1, 1);
4664
+ const bandGroup = Math.floor(bandIndex / rowCycle) % 2;
4665
+ if (bandGroup === 0) {
4666
+ style.backgroundColor = 'rgba(217, 226, 243, 0.5)';
4667
+ applied = true;
4668
+ }
4669
+ }
4670
+ // ── Banded columns. ──
4671
+ if (tableData.bandedColumns) {
4672
+ const isFirstCol = tableData.firstCol;
4673
+ const isLastCol = tableData.lastCol;
4674
+ const colBandIndex = isFirstCol && cellIndex > 0 ? cellIndex - 1 : cellIndex;
4675
+ const skipCol = (isFirstCol && cellIndex === 0) || (isLastCol && cellIndex === columnCount - 1);
4676
+ if (!skipCol) {
4677
+ const colCycle = Math.max(tableData.bandColCycle ?? 1, 1);
4678
+ const colBandGroup = Math.floor(colBandIndex / colCycle) % 2;
4679
+ if (colBandGroup === 0 && (!style.backgroundColor || !tableData.bandedRows)) {
4680
+ style.backgroundColor = 'rgba(217, 226, 243, 0.35)';
4681
+ applied = true;
4682
+ }
4683
+ }
4684
+ }
4685
+ // ── Header row (first row). ──
4686
+ if (tableData.firstRowHeader && rowIndex === 0) {
4687
+ style.fontWeight = 700;
4688
+ style.backgroundColor = 'rgba(68, 114, 196, 0.85)';
4689
+ style.color = '#ffffff';
4690
+ applied = true;
4691
+ }
4692
+ // ── Total / last row emphasis. ──
4693
+ if (tableData.lastRow && rowIndex === rowCount - 1) {
4694
+ style.fontWeight = 700;
4695
+ style.borderTop = '2px solid rgba(68, 114, 196, 0.7)';
4696
+ applied = true;
4697
+ }
4698
+ // ── First column emphasis. ──
4699
+ if (tableData.firstCol && cellIndex === 0) {
4700
+ style.fontWeight = 700;
4701
+ applied = true;
4702
+ }
4703
+ // ── Last column emphasis. ──
4704
+ if (tableData.lastCol && cellIndex === columnCount - 1) {
4705
+ style.fontWeight = 700;
4706
+ applied = true;
4707
+ }
4708
+ return applied ? style : undefined;
4709
+ }
4710
+
3740
4711
  /**
3741
4712
  * Pure geometry helpers for the align / distribute editor operations.
3742
4713
  *
@@ -4020,6 +4991,8 @@ function snapAngle(angleDeg, step = 15, tolerance = step / 2) {
4020
4991
  * - text: `text-warp` (WordArt paths), `omml-to-mathml` (equations).
4021
4992
  * - charts: `chart-helpers` (layout/palette/axis math).
4022
4993
  * - animation: `animation-css` (preset → CSS keyframes).
4994
+ * - 3d: `visual-3d` (scene3d/shape3d → CSS transform/shadow pieces).
4995
+ * - tables: `table-style` (cell style + banding → CSS).
4023
4996
  * - editing: `element-align` (align/distribute), `element-interaction`
4024
4997
  * (drag/resize/rotate math).
4025
4998
  */