Coverage Report

Created: 2025-07-07 10:01

/src/libreoffice/oox/source/drawingml/scene3dhelper.cxx
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 *
9
 */
10
11
#include <drawingml/scene3dhelper.hxx>
12
13
#include <basegfx/matrix/b3dhommatrix.hxx>
14
#include <basegfx/numeric/ftools.hxx>
15
#include <basegfx/vector/b3dvector.hxx>
16
#include <oox/drawingml/drawingmltypes.hxx>
17
#include <oox/helper/propertymap.hxx>
18
#include <oox/token/properties.hxx>
19
#include <oox/token/tokens.hxx>
20
21
#include <com/sun/star/drawing/Direction3D.hpp>
22
#include <com/sun/star/drawing/EnhancedCustomShapeParameter.hpp>
23
#include <com/sun/star/drawing/EnhancedCustomShapeMetalType.hpp>
24
#include <com/sun/star/drawing/EnhancedCustomShapeParameterPair.hpp>
25
#include <com/sun/star/drawing/EnhancedCustomShapeParameterType.hpp>
26
#include <com/sun/star/drawing/Position3D.hpp>
27
#include <com/sun/star/drawing/ProjectionMode.hpp>
28
#include <com/sun/star/drawing/ShadeMode.hpp>
29
30
#include <cmath>
31
32
namespace oox
33
{
34
/** This struct is used to hold values from the OOXML camera preset types.*/
35
namespace
36
{
37
struct PrstCameraValues
38
{
39
    std::u16string_view msCameraPrstName; // identifies the value set
40
41
    bool mbIsParallel;
42
43
    // values as shown in the UI of MS Office, converted to 1/60000 deg
44
    double mfRotateAngleX; // unit 1/60000 degree
45
    double mfRotateAngleY; // unit 1/60000 degree
46
    double mfRotateAngleZ; // unit 1/60000 degree
47
48
    // Position of origin relative to the bounding box of the transformed 2D shape.
49
    // LibreOffice can handle values outside the ODF range.
50
    double mfOriginX; // ODF range [-0.5 (left).. 0.5 (right)], fraction of width
51
    double mfOriginY; // ODF range [-0.5 (top) 0.5 (bottom)], fraction of height
52
53
    // mandatory for PARALLEL, ignored for PERSPECTIVE
54
    double mfSkewAmount; // range 0 to 100, percent of depth used as slant length
55
    double mfSkewAngle; // unit degree
56
57
    // mandatory for PERSPECTIVE, ignored for PARALLEL
58
    // API type ::com::sun::star::drawing::Position3D; unit 1/100 mm
59
    double mfViewPointX; // shift from Origin
60
    double mfViewPointY; // shift from Origin
61
    double mfViewPointZ; // absolute z-coordinate
62
63
    // The OOXML camera attribute "zoom" is not contained, because it is not set in preset camera
64
    // types and LO cannot render it in custom shape extrusion scene.
65
};
66
} // end anonymous namespace
67
68
// The values were found experimental using MS Office. A spreadsheet with remarks is attached
69
// to tdf#70039.
70
constexpr sal_uInt16 nCameraPresetCount(62); // Fixed, specified in OOXML standard.
71
constexpr PrstCameraValues aPrstCameraValuesArray[nCameraPresetCount] = {
72
    { u"isometricBottomDown", true, 2124000, 18882000, 17988000, 0, 0, 0, 0, 0, 0, 0 },
73
    { u"isometricBottomUp", true, 2124000, 2718000, 3612000, 0, 0, 0, 0, 0, 0, 0 },
74
    { u"isometricLeftDown", true, 2100000, 2700000, 0, 0, 0, 0, 0, 0, 0, 0 },
75
    { u"isometricLeftUp", true, 19500000, 2700000, 0, 0, 0, 0, 0, 0, 0, 0 },
76
    { u"isometricOffAxis1Left", true, 1080000, 3840000, 0, 0, 0, 0, 0, 0, 0, 0 },
77
    { u"isometricOffAxis1Right", true, 1080000, 20040000, 0, 0, 0, 0, 0, 0, 0, 0 },
78
    { u"isometricOffAxis1Top", true, 18078000, 18390000, 3456000, 0, 0, 0, 0, 0, 0, 0 },
79
    { u"isometricOffAxis2Left", true, 1080000, 1560000, 0, 0, 0, 0, 0, 0, 0, 0 },
80
    { u"isometricOffAxis2Right", true, 1080000, 17760000, 0, 0, 0, 0, 0, 0, 0, 0 },
81
    { u"isometricOffAxis2Top", true, 18078000, 3210000, 18144000, 0, 0, 0, 0, 0, 0, 0 },
82
    { u"isometricOffAxis3Bottom", true, 3522000, 18390000, 18144000, 0, 0, 0, 0, 0, 0, 0 },
83
    { u"isometricOffAxis3Left", true, 20520000, 3840000, 0, 0, 0, 0, 0, 0, 0, 0 },
84
    { u"isometricOffAxis3Right", true, 20520000, 20040000, 0, 0, 0, 0, 0, 0, 0, 0 },
85
    { u"isometricOffAxis4Bottom", true, 3522000, 3210000, 3456000, 0, 0, 0, 0, 0, 0, 0 },
86
    { u"isometricOffAxis4Left", true, 20520000, 1560000, 0, 0, 0, 0, 0, 0, 0, 0 },
87
    { u"isometricOffAxis4Right", true, 20520000, 17760000, 0, 0, 0, 0, 0, 0, 0, 0 },
88
    { u"isometricRightDown", true, 19500000, 18900000, 0, 0, 0, 0, 0, 0, 0, 0 },
89
    { u"isometricRightUp", true, 2100000, 18900000, 0, 0, 0, 0, 0, 0, 0, 0 },
90
    { u"isometricTopDown", true, 19476000, 2718000, 17988000, 0, 0, 0, 0, 0, 0, 0 },
91
    { u"isometricTopUp", true, 19476000, 18882000, 3612000, 0, 0, 0, 0, 0, 0, 0 },
92
    { u"legacyObliqueBottom", true, 0, 0, 0, 0, 0.5, 50, 90, 0, 0, 0 },
93
    { u"legacyObliqueBottomLeft", true, 0, 0, 0, -0.5, 0.5, 50, 45, 0, 0, 0 },
94
    { u"legacyObliqueBottomRight", true, 0, 0, 0, 0.5, 0.5, 50, 135, 0, 0, 0 },
95
    { u"legacyObliqueFront", true, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
96
    { u"legacyObliqueLeft", true, 0, 0, 0, -0.5, 0, 50, -360, 0, 0, 0 },
97
    { u"legacyObliqueRight", true, 0, 0, 0, 0.5, 0, 50, 180, 0, 0, 0 },
98
    { u"legacyObliqueTop", true, 0, 0, 0, 0, -0.5, 50, -90, 0, 0, 0 },
99
    { u"legacyObliqueTopLeft", true, 0, 0, 0, -0.5, -0.5, 50, -45, 0, 0, 0 },
100
    { u"legacyObliqueTopRight", true, 0, 0, 0, 0.5, -0.5, 50, -135, 0, 0, 0 },
101
    { u"legacyPerspectiveBottom", false, 0, 0, 0, 0, 0.5, 50, 90, 0, 3472, 25000 },
102
    { u"legacyPerspectiveBottomLeft", false, 0, 0, 0, -0.5, 0.5, 50, 45, -3472, 3472, 25000 },
103
    { u"legacyPerspectiveBottomRight", false, 0, 0, 0, 0.5, 0.5, 50, 135, 3472, 3472, 25000 },
104
    { u"legacyPerspectiveFront", false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25000 },
105
    { u"legacyPerspectiveLeft", false, 0, 0, 0, -0.5, 0, 50, -360, -3472, 0, 25000 },
106
    { u"legacyPerspectiveRight", false, 0, 0, 0, 0.5, 0, 50, 180, 3472, 0, 25000 },
107
    { u"legacyPerspectiveTop", false, 0, 0, 0, 0, -0.5, 50, -90, 0, -3472, 25000 },
108
    { u"legacyPerspectiveTopLeft", false, 0, 0, 0, -0.5, -0.5, 50, -45, -3472, -3472, 25000 },
109
    { u"legacyPerspectiveTopRight", false, 0, 0, 0, 0.5, -0.5, 50, -135, 3472, -3472, 25000 },
110
    { u"obliqueBottom", true, 0, 0, 0, 0, 0.5, 30, 90, 0, 0, 0 },
111
    { u"obliqueBottomLeft", true, 0, 0, 0, -0.5, 0.5, 30, 45, 0, 0, 0 },
112
    { u"obliqueBottomRight", true, 0, 0, 0, 0.5, 0.5, 30, 135, 0, 0, 0 },
113
    { u"obliqueLeft", true, 0, 0, 0, -0.5, 0, 30, -360, 0, 0, 0 },
114
    { u"obliqueRight", true, 0, 0, 0, 0.5, 0, 30, 180, 0, 0, 0 },
115
    { u"obliqueTop", true, 0, 0, 0, 0, -0.5, 30, -90, 0, 0, 0 },
116
    { u"obliqueTopLeft", true, 0, 0, 0, -0.5, -0.5, 30, -45, 0, 0, 0 },
117
    { u"obliqueTopRight", true, 0, 0, 0, 0.5, -0.5, 30, -135, 0, 0, 0 },
118
    { u"orthographicFront", true, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
119
    { u"perspectiveAbove", false, 20400000, 0, 0, 0, 0, 0, 0, 0, 0, 38451 },
120
    { u"perspectiveAboveLeftFacing", false, 2358000, 858000, 20466000, 0, 0, 0, 0, 0, 0, 38451 },
121
    { u"perspectiveAboveRightFacing", false, 2358000, 20742000, 1134000, 0, 0, 0, 0, 0, 0, 38451 },
122
    { u"perspectiveBelow", false, 1200000, 0, 0, 0, 0, 0, 0, 0, 0, 38451 },
123
    { u"perspectiveContrastingLeftFacing", false, 624000, 2634000, 21384000, 0, 0, 0, 0, 0, 0,
124
      38451 },
125
    { u"perspectiveContrastingRightFacing", false, 624000, 18966000, 216000, 0, 0, 0, 0, 0, 0,
126
      38451 },
127
    { u"perspectiveFront", false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38451 },
128
    { u"perspectiveHeroicExtremeLeftFacing", false, 486000, 2070000, 21426000, 0, 0, 0, 0, 0, 0,
129
      18981 },
130
    { u"perspectiveHeroicExtremeRightFacing", false, 486000, 19530000, 174000, 0, 0, 0, 0, 0, 0,
131
      18981 },
132
    { u"perspectiveHeroicLeftFacing", false, 20940000, 858000, 156000, 0, 0, 0, 0, 0, 0, 38451 },
133
    { u"perspectiveHeroicRightFacing", false, 20940000, 20742000, 21444000, 0, 0, 0, 0, 0, 0,
134
      38451 },
135
    { u"perspectiveLeft", false, 0, 1200000, 0, 0, 0, 0, 0, 0, 0, 38451 },
136
    { u"perspectiveRelaxed", false, 18576000, 0, 0, 0, 0, 0, 0, 0, 0, 38451 },
137
    { u"perspectiveRelaxedModerately", false, 19488000, 0, 0, 0, 0, 0, 0, 0, 0, 38451 },
138
    { u"perspectiveRight", false, 0, 20400000, 0, 0, 0, 0, 0, 0, 0, 38451 }
139
};
140
141
namespace
142
{
143
/** Searches for the item in aPrstCameraValuesArray with given sPresetName.
144
    @param [in] sPresetName name as specified in OOXML standard
145
    @return returns the index if item exists, otherwise -1*/
146
sal_Int16 getPrstCameraIndex(std::u16string_view sPresetName)
147
26
{
148
26
    sal_Int16 nIt(0);
149
728
    while (nIt < nCameraPresetCount && aPrstCameraValuesArray[nIt].msCameraPrstName != sPresetName)
150
702
        ++nIt;
151
26
    if (nIt >= nCameraPresetCount)
152
0
    {
153
0
        nIt = -1; // Error is handled by caller
154
0
    }
155
26
    return nIt;
156
26
}
157
} // end anonymous namespace
158
159
void Scene3DHelper::getAPIAnglesFromOOXAngle(const sal_Int32 nLat, const sal_Int32 nLon,
160
                                             const sal_Int32 nRev, double& fX, double& fY,
161
                                             double& fZ)
162
16
{
163
    // MS Office applies the rotations in the order first around y-axis by nLon, then around x-axis
164
    // by nLat and last around z-axis by nRev. The extrusion mode in ODF and also the API
165
    // first rotate around the z-axis, then around the y-axis and last around the x-axis. In ODF, the
166
    // rotation around the z-axis is integrated into the shape transformation and the others are
167
    // specified in the enhanced geometry of the shape.
168
    // The orientation of the resulting angles equals the orientation in API, but the angles are in
169
    // radians.
170
171
    // First we build the total rotation matrix from the OOX angles. y-axis points down.
172
16
    basegfx::B3DHomMatrix aXMat;
173
16
    const double fLatRad = basegfx::deg2rad<60000>(nLat);
174
16
    aXMat.set(1, 1, cos(fLatRad));
175
16
    aXMat.set(2, 2, cos(fLatRad));
176
16
    aXMat.set(1, 2, sin(fLatRad));
177
16
    aXMat.set(2, 1, -sin(fLatRad));
178
179
16
    basegfx::B3DHomMatrix aYMat;
180
16
    const double fLonRad = basegfx::deg2rad<60000>(nLon);
181
16
    aYMat.set(0, 0, cos(fLonRad));
182
16
    aYMat.set(2, 2, cos(fLonRad));
183
16
    aYMat.set(0, 2, -sin(fLonRad));
184
16
    aYMat.set(2, 0, sin(fLonRad));
185
186
16
    basegfx::B3DHomMatrix aZMat;
187
16
    const double fRevRad = basegfx::deg2rad<60000>(nRev);
188
16
    aZMat.set(0, 0, cos(fRevRad));
189
16
    aZMat.set(1, 1, cos(fRevRad));
190
16
    aZMat.set(0, 1, sin(fRevRad));
191
16
    aZMat.set(1, 0, -sin(fRevRad));
192
16
    basegfx::B3DHomMatrix aTotalMat = aZMat * aXMat * aYMat;
193
194
    // Now we decompose it so that rotation around z-axis is the first rotation. We know it is a
195
    // orthonormal matrix, so some steps seen in B3DHomMatrix::decompose() are not needed.
196
    // The solution fY2 = pi - fY results in the same projection, thus we do not consider it.
197
16
    fY = std::asin(-aTotalMat.get(0, 2));
198
199
16
    if (basegfx::fTools::equalZero(cos(fY)))
200
0
    {
201
        // This case has zeros at positions (0,0), (0,1), (1,2) and (2,2) in aTotalMat.
202
        // This special case means, that the shape is rotated around the y-axis so, that the user
203
        // looks on the extruded faces. Front face and back face are orthogonal to the xy-plane. The
204
        // rotation around the x-axis cannot be distinguished from an initial rotation of the shape
205
        // outside 3D. Thus there exist no unique solution.
206
0
        fX = 0.0;
207
0
        fZ = std::atan2(aTotalMat.get(2, 1), aTotalMat.get(1, 1));
208
0
    }
209
16
    else
210
16
    {
211
16
        fX = std::atan2(-aTotalMat.get(1, 2) / cos(fY), aTotalMat.get(2, 2) / cos(fY));
212
16
        fZ = std::atan2(aTotalMat.get(0, 1) / cos(fY), aTotalMat.get(0, 0) / cos(fY));
213
16
    }
214
16
}
215
216
void Scene3DHelper::getAPIAnglesFrom3DProperties(
217
    const oox::drawingml::Shape3DPropertiesPtr p3DProperties, const sal_Int32& rnMSOShapeRotation,
218
    double& fX, double& fY, double& fZ)
219
16
{
220
16
    if (!p3DProperties)
221
0
        return;
222
223
    // on x-axis, unit 1/60000 deg
224
16
    sal_Int32 nLatitude = (*p3DProperties).maCameraRotation.mnLatitude.value_or(0);
225
    // on y-axis, unit 1/60000 deg
226
16
    sal_Int32 nLongitude = (*p3DProperties).maCameraRotation.mnLongitude.value_or(0);
227
    // on z-axis, unit 1/60000 deg
228
16
    sal_Int32 nRevolution = (*p3DProperties).maCameraRotation.mnRevolution.value_or(0);
229
230
    // Some projection types need special treatment:
231
16
    if (29 <= mnPrstCameraIndex && mnPrstCameraIndex <= 37)
232
0
    {
233
        // legacyPerspective. MS Office does not use z-rotation but writes it to file. We need to
234
        // ignore it. The preset cameras have no rotation.
235
0
        nRevolution = 0;
236
0
    }
237
16
    else if (47 <= mnPrstCameraIndex)
238
0
    {
239
0
        assert(mnPrstCameraIndex <= 61
240
0
               && "by definition we don't set anything >= nCameraPresetCount (62)");
241
        // perspective. MS Office has a strange rendering behavior: If the shape rotation is not zero
242
        // and the angle for rotation on x-axis (=latitude) is >90deg and <=270deg, then MSO renders
243
        // the shape with an addition 180deg rotation on the z-axis. This happens only with user
244
        // entered angles.
245
0
        if (rnMSOShapeRotation != 0 && nLatitude > 5400000 && nLatitude <= 16200000)
246
0
            nRevolution += 10800000;
247
0
    }
248
249
    // In case attributes lat, lon and rev of the <rot> child element of the <scene3d> element in
250
    // OOXML markup are given, they overwrite the values from the preset camera type. Otherwise the
251
    // values from the preset camera are used. OOXML requires that all three attributes must exist at
252
    // the same time. Thus it is enough to test one of them.
253
16
    if (!(*p3DProperties).maCameraRotation.mnRevolution.has_value())
254
0
    {
255
        // The angles are given in 1/60000 deg in aPrstCameraValuesArray.
256
0
        nLatitude = aPrstCameraValuesArray[mnPrstCameraIndex].mfRotateAngleX;
257
0
        nLongitude = aPrstCameraValuesArray[mnPrstCameraIndex].mfRotateAngleY;
258
0
        nRevolution = aPrstCameraValuesArray[mnPrstCameraIndex].mfRotateAngleZ;
259
0
    }
260
261
    // MS Office applies the shape rotation after the rotations from camera in case of non-legacy
262
    // cameras, and before for legacy cameras. ODF specifies to first rotate the shape. Thus we need
263
    // to add shape rotation to nRevolution in case of non-legacy cameras. The shape rotation has
264
    // opposite orientation than camera z-rotation.
265
16
    bool bIsLegacyCamera = 20 <= mnPrstCameraIndex && mnPrstCameraIndex <= 37;
266
16
    if (!bIsLegacyCamera)
267
13
        nRevolution -= rnMSOShapeRotation;
268
269
    // Now calculate the angles for LO rotation order and orientation.
270
16
    Scene3DHelper::getAPIAnglesFromOOXAngle(nLatitude, nLongitude, nRevolution, fX, fY, fZ);
271
272
16
    if (bIsLegacyCamera)
273
3
        fZ -= basegfx::deg2rad<60000>(rnMSOShapeRotation);
274
16
}
275
276
void Scene3DHelper::addRotateAngleToMap(oox::PropertyMap& rPropertyMap, const double fX,
277
                                        const double fY)
278
3
{
279
3
    css::drawing::EnhancedCustomShapeParameterPair aAnglePair;
280
3
    aAnglePair.First.Value <<= basegfx::rad2deg(fX);
281
3
    aAnglePair.First.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
282
3
    aAnglePair.Second.Value <<= basegfx::rad2deg(fY);
283
3
    aAnglePair.Second.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
284
3
    rPropertyMap.setAnyProperty(oox::PROP_RotateAngle, css::uno::Any(aAnglePair));
285
3
}
286
287
void Scene3DHelper::addExtrusionDepthToMap(const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
288
                                           oox::PropertyMap& rPropertyMap)
289
3
{
290
    // Amount of extrusion and its position relative to the original shape face. This moves the
291
    // shape inside the scene.
292
    // The GetExtrusionDepth() method in EnhancedCustomShape3d.cxx expects type double for both.
293
3
    sal_Int32 nDepthAmount = (*p3DProperties).mnExtrusionH.value_or(0); // unit EMU
294
3
    double fDepthAmount = o3tl::convert(nDepthAmount, o3tl::Length::emu, o3tl::Length::mm100);
295
3
    sal_Int32 nZPosition = (*p3DProperties).mnShapeZ.value_or(0); // unit EMU
296
3
    double fZPosition = o3tl::convert(nZPosition, o3tl::Length::emu, o3tl::Length::mm100);
297
3
    double fDepthRelPos = 0.0;
298
3
    if (nDepthAmount == 0 && nZPosition != 0)
299
0
    {
300
        // We cannot express the position relative to the extrusion depth.
301
        // Use an artificial, small depth of 1Hmm
302
0
        fDepthRelPos = fZPosition;
303
0
        fDepthAmount = 1.0; // unit Hmm
304
0
    }
305
3
    else if (nDepthAmount != 0)
306
3
        fDepthRelPos = fZPosition / fDepthAmount;
307
308
3
    css::drawing::EnhancedCustomShapeParameterPair aPair;
309
3
    css::drawing::EnhancedCustomShapeParameter& rDepthAmount = aPair.First;
310
3
    rDepthAmount.Value <<= fDepthAmount;
311
3
    rDepthAmount.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
312
3
    css::drawing::EnhancedCustomShapeParameter& rDepthFraction = aPair.Second;
313
3
    rDepthFraction.Value <<= fDepthRelPos;
314
3
    rDepthFraction.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
315
3
    rPropertyMap.setProperty(oox::PROP_Depth, aPair);
316
3
}
317
318
void Scene3DHelper::addProjectionGeometryToMap(
319
    const oox::drawingml::Shape3DPropertiesPtr p3DProperties, oox::PropertyMap& rPropertyMap,
320
    const bool bIsParallel, const sal_Int32 rnMSOShapeRotation)
321
3
{
322
    // origin is needed for parallel and perspective as well
323
3
    css::drawing::EnhancedCustomShapeParameterPair aOrigin;
324
3
    aOrigin.First.Value <<= aPrstCameraValuesArray[mnPrstCameraIndex].mfOriginX;
325
3
    aOrigin.First.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
326
3
    aOrigin.Second.Value <<= aPrstCameraValuesArray[mnPrstCameraIndex].mfOriginY;
327
3
    aOrigin.Second.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
328
3
    rPropertyMap.setProperty(oox::PROP_Origin, aOrigin);
329
330
3
    if (bIsParallel)
331
3
    {
332
        // PARALLEL needs API property Skew.
333
        // orthographicFront and isometric projections do not use skew. We write it nevertheless
334
        // to prevent LO defaults. Zeros are contained in aPrstCameraValuesArray for these cases.
335
3
        double fSkewAngle = aPrstCameraValuesArray[mnPrstCameraIndex].mfSkewAngle; // unit degree
336
3
        double fSkewAmount = aPrstCameraValuesArray[mnPrstCameraIndex].mfSkewAmount;
337
        // oblique projections (index [38..45]) need special treatment. MS Office rotates around the
338
        // z-axis after the projection was created. Thus the rotation affects the skew direction. ODF
339
        // rotates the shape before creating the projection. Thus we need to incorporate the shape
340
        // rotation into the skew angle.
341
3
        if (38 <= mnPrstCameraIndex && mnPrstCameraIndex <= 45)
342
0
        {
343
0
            fSkewAngle -= rnMSOShapeRotation / 60000.0;
344
0
        }
345
3
        css::drawing::EnhancedCustomShapeParameterPair aSkew;
346
3
        aSkew.First.Value <<= fSkewAmount;
347
3
        aSkew.First.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
348
3
        aSkew.Second.Value <<= fSkewAngle;
349
3
        aSkew.Second.Type = css::drawing::EnhancedCustomShapeParameterType::NORMAL;
350
3
        rPropertyMap.setProperty(oox::PROP_Skew, aSkew);
351
3
    }
352
0
    else
353
0
    {
354
        // PERSPECTIVE needs API property ViewPoint.
355
0
        css::drawing::Position3D aViewPoint;
356
357
        // x- and y-coordinate depend on preset camera type.
358
0
        aViewPoint.PositionX = aPrstCameraValuesArray[mnPrstCameraIndex].mfViewPointX;
359
0
        aViewPoint.PositionY = aPrstCameraValuesArray[mnPrstCameraIndex].mfViewPointY;
360
361
        // The z-coordinate is determined bei a field of view angle in OOXML and by a
362
        // distance in LibreOffice. MS Office users can change its value.
363
0
        if ((*p3DProperties).mfFieldOfVision.has_value())
364
0
        {
365
0
            double fFov = (*p3DProperties).mfFieldOfVision.value();
366
0
            fFov = std::clamp(fFov, 0.5, 179.5);
367
            // 15976 = 25000 * tan(32.5°) as in legacy. Better ideas to calculate the distance are
368
            // welcome.
369
0
            aViewPoint.PositionZ = 15976.0 / tan(basegfx::deg2rad(fFov / 2.0));
370
0
        }
371
0
        else
372
0
            aViewPoint.PositionZ = aPrstCameraValuesArray[mnPrstCameraIndex].mfViewPointZ;
373
374
0
        rPropertyMap.setProperty(oox::PROP_ViewPoint, aViewPoint);
375
0
    }
376
    // ToDo: It is possible in OOXML to set a 3D-scene on a group. It is not clear yet how that can
377
    // be mimicked in LO. In case of perspective projection, it produces a horizontal or vertical
378
    // shift of the viewpoint in relation to the shapes of the group, for example.
379
3
}
380
381
bool Scene3DHelper::setExtrusionProperties(const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
382
                                           const sal_Int32& rnMSOShapeRotation,
383
                                           oox::PropertyMap& rPropertyMap, double& rRotZ,
384
                                           oox::drawingml::Color& rExtrusionColor,
385
                                           const bool bBlockExtrusion)
386
82.4k
{
387
    // We convert rnMSOShapeRotation, so that Shape::createAndInsert() can use rRotZ the same way in
388
    // all cases.
389
82.4k
    rRotZ = basegfx::deg2rad<60000>(-rnMSOShapeRotation);
390
391
82.4k
    if (!p3DProperties || !(*p3DProperties).mnPreset.has_value())
392
82.4k
        return false;
393
394
26
    const sal_Int32 nCameraPrstID((*p3DProperties).mnPreset.value());
395
26
    sal_Int16 nPrstCameraIndex
396
26
        = getPrstCameraIndex(oox::drawingml::Generic3DProperties::getCameraPrstName(nCameraPrstID));
397
26
    if (nPrstCameraIndex < 0 or nPrstCameraIndex >= nCameraPresetCount)
398
0
        return false; // error in document. OOXML specifies a fixed set of preset camera types.
399
26
    mnPrstCameraIndex = nPrstCameraIndex;
400
401
    // Extrusion color is not handled as extrusion property but as shape property. Thus deliver it
402
    // in any case, so that Shape::createAndInsert() knows about it.
403
26
    rExtrusionColor = (*p3DProperties).maExtrusionColor;
404
405
    // Even if we do not extrude an image, we still want to get the z-Rotation.
406
26
    if (bBlockExtrusion)
407
10
    {
408
10
        rRotZ = basegfx::deg2rad<60000>((*p3DProperties).maCameraRotation.mnRevolution.value_or(0)
409
10
                                        - rnMSOShapeRotation);
410
10
        return false;
411
10
    }
412
413
    // We use extrusion, if there is a rotation around x-axis or y-axis,
414
    // or if there is no such rotation but we have a perspective projection with true depth,
415
    // or we have a parallel projection other than a 'front' type.
416
    // In other cases the rendering as normal shape is better than any current extrusion.
417
16
    double fX = 0.0;
418
16
    double fY = 0.0;
419
16
    Scene3DHelper::getAPIAnglesFrom3DProperties(p3DProperties, rnMSOShapeRotation, fX, fY, rRotZ);
420
16
    sal_Int32 nDepthAmount = (*p3DProperties).mnExtrusionH.value_or(0);
421
16
    bool bIsParallel = aPrstCameraValuesArray[mnPrstCameraIndex].mbIsParallel;
422
16
    bool bIsParallelFrontType
423
16
        = (nCameraPrstID == XML_legacyObliqueFront) || (nCameraPrstID == XML_orthographicFront);
424
16
    bool bCreateExtrusion = (!basegfx::fTools::equalZero(fX) || !basegfx::fTools::equalZero(fY))
425
16
                            || (!bIsParallel && nDepthAmount > 0)
426
16
                            || (bIsParallel && !bIsParallelFrontType);
427
428
16
    if (!bCreateExtrusion)
429
13
        return false;
430
431
    // Create the extrusion properties in rPropertyMap so that they can be directly used.
432
    // Turn extrusion on
433
3
    rPropertyMap.setProperty(oox::PROP_Extrusion, true);
434
435
    // Dummy value. Will be changed from evaluating the material properties.
436
3
    rPropertyMap.setProperty(oox::PROP_Diffusion, 100.0);
437
438
    // Camera properties
439
3
    css::drawing::ProjectionMode eProjectionMode = bIsParallel
440
3
                                                       ? css::drawing::ProjectionMode_PARALLEL
441
3
                                                       : css::drawing::ProjectionMode_PERSPECTIVE;
442
3
    rPropertyMap.setProperty(oox::PROP_ProjectionMode, eProjectionMode);
443
444
3
    Scene3DHelper::addRotateAngleToMap(rPropertyMap, fX, fY);
445
446
3
    Scene3DHelper::addProjectionGeometryToMap(p3DProperties, rPropertyMap, bIsParallel,
447
3
                                              rnMSOShapeRotation);
448
449
    // Shape properties
450
3
    Scene3DHelper::addExtrusionDepthToMap(p3DProperties, rPropertyMap);
451
452
    // The 'automatic' extrusion color is different in MS Office. Thus we enable it in any case.
453
    // CreateAndInsert method will set a suitable 'automatic' color, if rExtrusionColor is not used.
454
3
    rPropertyMap.setProperty(oox::PROP_Color, true);
455
    // ToDo: Some materials might need ShadeMode_Smooth or ShadeMode_PHONG.
456
3
    rPropertyMap.setProperty(oox::PROP_ShadeMode, css::drawing::ShadeMode_FLAT);
457
458
3
    return true;
459
16
}
460
461
namespace
462
{
463
/* This struct is used to hold light properties for a light in a preset light rig.*/
464
struct MSOLight
465
{
466
    // Values are as specified in [MS-OI29500], see commit message.
467
    // The color is specified as RGBA, but alpha value is always 1.0 and ignored anyway, so it is
468
    // dropped here. The RGB values are in decimal, but might exceed the usual [0;1] range.
469
    double fMSOColorR;
470
    double fMSOColorG;
471
    double fMSOColorB;
472
    // MSO uses 4 decimals precision, some light directions are not normalized.
473
    double fMSOLightDirectionX;
474
    double fMSOLightDirectionY;
475
    double fMSOLightDirectionZ;
476
    double fScale;
477
    double fOffset;
478
    bool bSpecular;
479
    bool bDiffuse;
480
};
481
482
/* This struct is used to hold properties of a light rig*/
483
struct PrstLightRigValues
484
{
485
    // values are as specified in [MS-OI29500], see commit message.
486
    std::u16string_view sLightRigName; // identifies the light rig, mandatory in OOXML
487
    // The ambient color is specified as RGBA, but alpha value is always 1.0 and R = B = G. Thus we
488
    // store here only one value.
489
    std::optional<double> fAmbient;
490
    // Each rig has at least one light and maximal four lights
491
    MSOLight aLight1;
492
    std::optional<MSOLight> aLight2;
493
    std::optional<MSOLight> aLight3;
494
    std::optional<MSOLight> aLight4;
495
    // Light rig rotation is not contained in the presets.
496
};
497
} // end anonymous namespace
498
499
// The values are taken from [MS-OI29500]. For details see the spreadsheet attached to
500
// tdf#70039 and the commit message.
501
constexpr sal_uInt16 nLightRigPresetCount(27); // Fix value, specified in OOXML standard.
502
constexpr PrstLightRigValues aPrstLightRigValuesArray[nLightRigPresetCount] = {
503
    { u"balanced",
504
      { 0.13 },
505
      { 1.05, 1.05, 1.05, 0.5263, -0.4092, -0.7453, 1, 0, true, true },
506
      { { 1, 1, 1, -0.9386, 0.3426, -0.041, 1, 0, true, true } },
507
      { { 0.5, 0.5, 0.5, 0.0934, 0.763, 0.6396, 1, 0, true, true } },
508
      {} },
509
    { u"brightRoom",
510
      { 1.5 },
511
      { 1, 1, 1, 0, -1, 0, 1, 0, false, true },
512
      { { 1, 1, 1, 0.8227, -0.1882, -0.5364, 1, 0, true, false } },
513
      { { -0.5, -0.5, -0.5, 0, 0, -1, 1, 0, false, true } },
514
      { { 0.5, 0.5, 0.5, 0, 1, 0, 1, 0, false, true } } },
515
    { u"chilly",
516
      { 0.11 },
517
      { 0.31, 0.32, 0.32, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
518
      { { 0.45, 0.45, 0.45, -0.3539, -0.1505, -0.9231, 1, 0, false, true } },
519
      { { 1.03, 1.02, 1.15, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
520
      { { 0.41, 0.45, 0.48, -0.5781, 0.7976, 0.1722, 1, 0, true, true } } },
521
    { u"contrasting",
522
      { 1 },
523
      { 1, 1, 1, 0, -1, 0, 1, 0, true, false },
524
      { { 1, 1, 1, 0, 1, 0, 1, 0, true, false } },
525
      {},
526
      {} },
527
    { u"flat",
528
      { 1 },
529
      { 0.821, 0.821, 0.821, -0.9546, -0.1619, -0.2502, 1, 0, true, false },
530
      { { 2.072, 2.54, 2.91, 0.0009, 0.8605, 0.5095, 1, 0, true, false } },
531
      { { 3.843, 3.843, 3.843, 0.6574, -0.7316, -0.1806, 1, 0, true, false } },
532
      {} },
533
    { u"flood",
534
      { 0.13 },
535
      { 1.1, 1.1, 1.1, 0.5685, -0.7651, -0.3022, 1, 0, true, true },
536
      { { 1.1, 1.1, 1.1, -0.2366, -0.9595, -0.1531, 1, 0, true, true } },
537
      { { 0.55, 0.55, 0.55, -0.8982, 0.1386, -0.4171, 1, 0, true, true } },
538
      {} },
539
    { u"freezing",
540
      {},
541
      { 0.53, 0.567, 0.661, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
542
      { { 0.37, 0.461, 0.461, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
543
      { { 0.649, 0.638, 0.904, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
544
      { { 0.971, 1.19, 1.363, -0.1825, 0.968, 0.1722, 1, 0, true, true } } },
545
    { u"glow",
546
      { 1 },
547
      { 1, 1, 1, 0, -1, 0, 1, 0, true, true },
548
      { { 0.7, 0.7, 0.7, 0, 1, 0, 1, 0, true, true } },
549
      {},
550
      {} },
551
    { u"harsh",
552
      { 0.28 },
553
      { 0.88, 0.88, 0.88, 0.6689, -0.6755, -0.3104, 1, 0, true, true },
554
      { { 0.88, 0.88, 0.88, -0.592, -0.7371, -0.326, 1, 0, true, true } },
555
      {},
556
      {} },
557
    { u"legacyFlat1",
558
      { 0.305 },
559
      { 0.58, 0.58, 0.58, 0, 0, -0.2, 1, 0, true, true },
560
      { { 0.58, 0.58, 0.58, 0, 0, -0.2, 0.5, 0, false, true } },
561
      {},
562
      {} },
563
    { u"legacyFlat2",
564
      { 0.305 },
565
      { 0.58, 0.58, 0.58, -1, -1, -0.2, 1, 0, true, true },
566
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
567
      {},
568
      {} },
569
    { u"legacyFlat3",
570
      { 0.305 },
571
      { 0.58, 0.58, 0.58, 0, -1, -0.2, 1, 0, true, true },
572
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
573
      {},
574
      {} },
575
    { u"legacyFlat4",
576
      { 0.305 },
577
      { 0.58, 0.58, 0.58, 1, -1, -0.2, 1, 0, true, true },
578
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
579
      {},
580
      {} },
581
    { u"legacyHarsh1",
582
      { 0.061 },
583
      { 0.793, 0.793, 0.793, 0, 0, -0.2, 1, 0, true, true },
584
      { { 0.214, 0.214, 0.214, 0, 0, -0.2, 1, 0, false, true } },
585
      {},
586
      {} },
587
    { u"legacyHarsh2",
588
      { 0.061 },
589
      { 0.793, 0.793, 0.793, -1, -1, -0.2, 1, 0, true, true },
590
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
591
      {},
592
      {} },
593
    { u"legacyHarsh3",
594
      { 0.061 },
595
      { 0.793, 0.793, 0.793, 0, -1, -0.2, 1, 0, true, true },
596
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
597
      {},
598
      {} },
599
    { u"legacyHarsh4",
600
      { 0.061 },
601
      { 0.793, 0.793, 0.793, 1, -1, -0.2, 1, 0, true, true },
602
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
603
      {},
604
      {} },
605
    { u"legacyNormal1",
606
      { 0.153 },
607
      { 0.671, 0.671, 0.671, 0, 0, -0.2, 1, 0, true, true },
608
      { { 0.366, 0.366, 0.366, 0, 0, -0.2, 0.5, 0, false, true } },
609
      {},
610
      {} },
611
    { u"legacyNormal2",
612
      { 0.153 },
613
      { 0.671, 0.671, 0.671, -1, -1, -0.2, 1, 0, true, true },
614
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
615
      {},
616
      {} },
617
    { u"legacyNormal3",
618
      { 0.153 },
619
      { 0.671, 0.671, 0.671, 0, -1, -0.2, 1, 0, true, true },
620
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
621
      {},
622
      {} },
623
    { u"legacyNormal4",
624
      { 0.153 },
625
      { 0.671, 0.671, 0.671, 1, -1, -0.2, 1, 0, true, true },
626
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
627
      {},
628
      {} },
629
    { u"morning",
630
      {},
631
      { 0.669, 0.648, 0.596, 0.6574, -0.7316, -0.1806, 0.5, 0.5, true, true },
632
      { { 0.459, 0.454, 0.385, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
633
      { { 0.9, 0.86, 0.83, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
634
      { { 0.911, 0.846, 0.728, -0.1825, 0.968, 0.1722, 1, 0, true, true } } },
635
    { u"soft", { 0.3 }, { 0.8, 0.8, 0.8, -0.6897, 0.2484, -0.6802, 1, 0, true, true }, {}, {}, {} },
636
    { u"sunrise",
637
      {},
638
      { 0.667, 0.63, 0.527, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
639
      { { 0.459, 0.459, 0.371, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
640
      { { 0.826, 0.712, 0.638, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
641
      { { 1.511, 1.319, 0.994, -0.1825, 0.968, 0.1722, 1, 0, false, true } } },
642
    { u"sunset",
643
      {},
644
      { 0.672, 0.169, 0.169, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
645
      { { 0.459, 0.448, 0.327, 0.0922, -0.3551, -0.9303, 1, 0, false, true } },
646
      { { 0.775, 0.612, 0.502, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
647
      { { 0.761, 0.69, 0.397, -0.424, 0.8891, 0.1722, 1, 0, false, true } } },
648
    { u"threePt",
649
      {},
650
      { 1.141, 1.141, 1.141, -0.6515, -0.2693, -0.7093, 1, 0, true, true },
651
      { { 0.5, 0.5, 0.5, 0.8482, 0.2469, -0.4686, 1, 0, true, true } },
652
      { { 1, 1, 1, 0.5634, -0.2812, 0.7769, 1, 0, true, true } },
653
      {} },
654
    { u"twoPt",
655
      { 0.25 },
656
      { 0.84, 0.84, 0.84, 0.5266, -0.4089, -0.7454, 0, 0, true, true },
657
      { { 0.3, 0.3, 0.3, -0.8983, 0.2365, -0.3704, 1, 0, true, true } },
658
      {},
659
      {} }
660
};
661
662
namespace
663
{
664
/** Searches for the item in aPrstLightRigValuesArray with given sPresetName.
665
    @param [in] sPresetName name as specified in OOXML standard
666
    @return returns the index if item exists, otherwise -1.*/
667
sal_Int16 lcl_getPrstLightRigIndex(std::u16string_view sPresetName)
668
3
{
669
3
    sal_Int16 nIt(0);
670
30
    while (nIt < nLightRigPresetCount && aPrstLightRigValuesArray[nIt].sLightRigName != sPresetName)
671
27
        ++nIt;
672
3
    if (nIt >= nLightRigPresetCount)
673
0
    {
674
0
        nIt = -1; // Error is handled by caller
675
0
    }
676
3
    return nIt;
677
3
}
678
679
/** Extracts the light directions from the preset lightRig.
680
    @param [in] rLightRig from which the lights are extracted
681
    @param [out] rLightDirVec contains the preset lights but each as B3DVector*/
682
void lcl_getLightDirectionsFromRig(const PrstLightRigValues& rLightRig,
683
                                   std::vector<basegfx::B3DVector>& rLightDirVec)
684
3
{
685
6
    auto addLightDir = [&](const MSOLight& aMSOLight) {
686
6
        basegfx::B3DVector aLightDir(aMSOLight.fMSOLightDirectionX, aMSOLight.fMSOLightDirectionY,
687
6
                                     aMSOLight.fMSOLightDirectionZ);
688
6
        rLightDirVec.push_back(std::move(aLightDir));
689
6
    };
690
    // aLight1 always exists, the others are optional
691
3
    addLightDir(rLightRig.aLight1);
692
3
    if (rLightRig.aLight2.has_value())
693
3
        addLightDir(rLightRig.aLight2.value());
694
3
    if (rLightRig.aLight3.has_value())
695
0
        addLightDir(rLightRig.aLight3.value());
696
3
    if (rLightRig.aLight4.has_value())
697
0
        addLightDir(rLightRig.aLight4.value());
698
3
}
699
700
/** Converts the directions from MSO specification to coordinates in the shape coordinate system.
701
    @details The extruded shape uses a left-hand Cartesian coordinate system with x-axis right, y-axis
702
    down and z-axis towards observer. When L(Lx,Ly,Lz) is the specified light direction, then
703
    V(-Ly, -Lx, Lz) is the direction in the shape coordinate system.
704
    @param [in,out] rLightDirVec contains for each individual light its direction.*/
705
void lcl_AdaptAndNormalizeLightDirections(std::vector<basegfx::B3DVector>& rLightDirVec)
706
3
{
707
3
    basegfx::B3DHomMatrix aTransform; // unit matrix
708
3
    aTransform.set(0, 0, 0.0);
709
3
    aTransform.set(0, 1, -1.0);
710
3
    aTransform.set(1, 0, -1.0);
711
3
    aTransform.set(1, 1, 0.0);
712
3
    for (auto& rDirection : rLightDirVec)
713
6
    {
714
6
        rDirection *= aTransform;
715
6
        rDirection.normalize();
716
6
    }
717
3
}
718
719
/** Gets the rotation angles fX and fY from the extrusion property RotateAngle in the map.
720
    Does nothing if property does not exist.
721
    @param [in] rPropertyMap should contain valid value in RotateAngle property
722
    @param [out] fX, fY rotation angle in unit rad with orientation as in API.*/
723
void lcl_getXYAnglesFromMap(oox::PropertyMap& rPropertyMap, double& rfX, double& rfY)
724
0
{
725
0
    if (!rPropertyMap.hasProperty(oox::PROP_RotateAngle))
726
0
        return;
727
0
    css::drawing::EnhancedCustomShapeParameterPair aAnglePair;
728
0
    css::uno::Any aAny = rPropertyMap.getProperty(oox::PROP_RotateAngle);
729
0
    if (aAny >>= aAnglePair)
730
0
    {
731
0
        rfX = basegfx::deg2rad(aAnglePair.First.Value.get<double>());
732
0
        rfY = basegfx::deg2rad(aAnglePair.Second.Value.get<double>());
733
0
    }
734
0
}
735
736
/** Applies the rotations given in fX, fY, fZ to the light directions.
737
    @details The rotations are applied in the order fZ, fY, fX. All angles have unit rad. The
738
        orientation of the angles fX and fY is the same as in the extrusion property RotateAngle in
739
        API. The orientation of angle fZ is the same as in shape property RotateAngle in API.
740
    @param [in, out] rLightDirVec contains the to be transformed light directions
741
    @param [in] fX angle for rotation around x-axis
742
    @param [in] fY angle for rotation around y-axis
743
    @param {in] fZ angle for rotation around z-axis*/
744
void lcl_ApplyShapeRotationToLights(std::vector<basegfx::B3DVector>& rLightDirVec, const double& fX,
745
                                    const double& fY, const double& fZ)
746
0
{
747
0
    basegfx::B3DHomMatrix aTransform; // unit matrix
748
    // rotate has the order first x, then y, last z. We need order z, y, x.
749
0
    aTransform.rotate(0.0, 0.0, -fZ);
750
0
    aTransform.rotate(0.0, -fY, 0.0);
751
0
    aTransform.rotate(fX, 0.0, 0.0);
752
0
    for (auto it = rLightDirVec.begin(); it != rLightDirVec.end(); ++it)
753
0
        (*it) *= aTransform;
754
0
}
755
756
/** Applies the light rig rotation to the directions of the individual lights
757
    @details A light rig has a mandatory attribute 'dir' for rotating the rig in 45deg steps. It might
758
        have an element 'rot', that describes a rotation by spherical coordinates 'lat', 'lon' and
759
        'rev'. The element has precedence over the attribute.
760
    @param [in] p3DProperties contains info about light rig.
761
    @param {in, out] rLightDirVec contains for each individual light its direction in shape coordinate
762
        system with x-axis right, y-axis down, z-axis toward observer.*/
763
void lcl_IncorporateRigRotationIntoLightDirections(
764
    const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
765
    std::vector<basegfx::B3DVector>& rLightDirVec)
766
3
{
767
3
    basegfx::B3DHomMatrix aTransform; // unit matrix
768
    // if a 'rot' element exists, then all of 'lat', 'lon' and 'rev' needs to exist.
769
3
    if ((*p3DProperties).maLightRigRotation.mnLatitude.has_value())
770
0
    {
771
0
        double fLat
772
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnLatitude.value_or(0));
773
0
        double fLon
774
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnLongitude.value_or(0));
775
0
        double fRev
776
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnRevolution.value_or(0));
777
0
        aTransform.rotate(0.0, 0.0, fRev);
778
0
        aTransform.rotate(fLat, fLon, 0.0);
779
0
    }
780
3
    else
781
3
    {
782
3
        sal_Int32 nDir = 0;
783
3
        switch ((*p3DProperties).mnLightRigDirection.value_or(XML_t))
784
3
        {
785
3
            case XML_t:
786
3
                nDir = 0;
787
3
                break;
788
0
            case XML_tr:
789
0
                nDir = 45;
790
0
                break;
791
0
            case XML_r:
792
0
                nDir = 90;
793
0
                break;
794
0
            case XML_br:
795
0
                nDir = 135;
796
0
                break;
797
0
            case XML_b:
798
0
                nDir = 180;
799
0
                break; // or -180
800
0
            case XML_bl:
801
0
                nDir = -135;
802
0
                break;
803
0
            case XML_l:
804
0
                nDir = -90;
805
0
                break;
806
0
            case XML_tl:
807
0
                nDir = -45;
808
0
                break;
809
0
            default:
810
0
                nDir = 0;
811
3
        }
812
        // Rotation is always only around z-axis
813
3
        aTransform.rotate(0.0, 0.0, basegfx::deg2rad(nDir));
814
3
    }
815
3
    for (auto& rDirection : rLightDirVec)
816
6
        rDirection *= aTransform;
817
3
}
818
819
/** The lights in OOXML are basically incompatible with our lights. We try to tweak some rigs to
820
    reduce obvious problems.
821
    @param [in, out] rLightDirVec light directions with already incorporated rotations
822
    @param [in, out] rLightRig the to be tweaked rig
823
*/
824
void lcl_tweakLightRig(std::vector<basegfx::B3DVector>& rLightDirVec, PrstLightRigValues& rLightRig)
825
3
{
826
3
    if (rLightRig.sLightRigName == u"brightRoom")
827
0
    {
828
        // The fourth light has more significant direction.
829
0
        if (rLightDirVec.size() >= 4 && rLightRig.aLight2.has_value()
830
0
            && rLightRig.aLight4.has_value())
831
0
        {
832
0
            std::swap(rLightDirVec[1], rLightDirVec[3]);
833
            // swap fourth and second in light rig too, swap their other properties too.
834
0
            MSOLight aTemp = rLightRig.aLight4.value();
835
0
            rLightRig.aLight4 = rLightRig.aLight2.value();
836
0
            rLightRig.aLight2 = aTemp;
837
            // and make it brighter, 1.0 instead of 0.5
838
0
            rLightRig.aLight2.value().fMSOColorR = 1.0;
839
0
            rLightRig.aLight2.value().fMSOColorG = 1.0;
840
0
            rLightRig.aLight2.value().fMSOColorB = 1.0;
841
0
        }
842
        // The object is far too bright.
843
0
        rLightRig.fAmbient = 0.6; // instead 1.5
844
0
    }
845
3
    else if (rLightRig.sLightRigName == u"chilly" || rLightRig.sLightRigName == u"flood")
846
0
    {
847
        // They are too dark.
848
0
        rLightRig.fAmbient = 0.35; // instead 0.11 resp. 0.13
849
0
    }
850
3
    else if (rLightRig.sLightRigName == u"freezing" || rLightRig.sLightRigName == u"morning"
851
3
             || rLightRig.sLightRigName == u"sunrise")
852
0
    {
853
        // These rigs have no ambient color but four lights. The objects are too dark with only
854
        // two lights.
855
0
        rLightRig.fAmbient = 0.4;
856
0
    }
857
3
    else if (rLightRig.sLightRigName == u"sunset")
858
0
    {
859
        // The fourth light is more significant.
860
0
        if (rLightDirVec.size() >= 4 && rLightRig.aLight4.has_value())
861
0
        {
862
0
            MSOLight aTemp = rLightRig.aLight2.value();
863
0
            rLightRig.aLight2 = rLightRig.aLight4.value();
864
0
            rLightRig.aLight4 = aTemp;
865
0
            std::swap(rLightDirVec[1], rLightDirVec[3]);
866
0
        }
867
0
    }
868
3
    else if (rLightRig.sLightRigName == u"soft")
869
0
    {
870
        // This is the only modern light rig with Scale=0.5 and Offset=0.5. It would be harsh=false
871
        // and specular=true at the same time. We switch specular off as that is used to set harsh on.
872
0
        rLightRig.aLight1.bSpecular = false;
873
0
    }
874
3
}
875
876
} // end anonymous namespace
877
878
void Scene3DHelper::setLightingProperties(const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
879
                                          const double& rfRotZ, oox::PropertyMap& rPropertyMap)
880
3
{
881
3
    if (!p3DProperties || !(*p3DProperties).mnLightRigType.has_value())
882
0
        return;
883
884
    // get index of light rig in aPrstLightRigValuesArray
885
3
    const sal_Int32 nLightRigPrstID((*p3DProperties).mnLightRigType.value()); // token
886
3
    sal_Int16 nPrstLightRigIndex = lcl_getPrstLightRigIndex(
887
3
        oox::drawingml::Generic3DProperties::getLightRigName(nLightRigPrstID));
888
3
    if (nPrstLightRigIndex < 0 or nPrstLightRigIndex >= nLightRigPresetCount)
889
0
        return; // error in document. OOXML specifies a fixed set of preset light rig types.
890
891
    // The light rig is copied because it might be tweaked later.
892
3
    PrstLightRigValues aLightRig = aPrstLightRigValuesArray[nPrstLightRigIndex];
893
894
3
    std::vector<basegfx::B3DVector> aLightDirVec;
895
3
    aLightDirVec.reserve(4);
896
3
    lcl_getLightDirectionsFromRig(aLightRig, aLightDirVec);
897
3
    lcl_AdaptAndNormalizeLightDirections(aLightDirVec);
898
899
3
    lcl_IncorporateRigRotationIntoLightDirections(p3DProperties, aLightDirVec);
900
901
    // Parts (1) to (6) are workarounds for the problem that our current model as well as API and
902
    // ODF are not able to describe or use the capabilities of extruded custom shapes of MS Office.
903
    // If the implementation is improved one day, the parts will need to be adapted.
904
905
    // (1) Moving the camera around does not change shape or light directions for modern cameras in
906
    // MS Office. For legacy cameras MS Office behaves same as LibreOffice: Not the camera is moved
907
    // but the shape is rotated. For modern cameras we need to rotate the light rig the same way as
908
    // the shape to get a similar illumination as in MS Office.
909
3
    if (mnPrstCameraIndex < 20 || 37 < mnPrstCameraIndex)
910
0
    {
911
0
        double fX = 0.0; // unit rad, orientation as in API
912
0
        double fY = 0.0; // unit rad, orientation as in API
913
0
        lcl_getXYAnglesFromMap(rPropertyMap, fX, fY);
914
0
        lcl_ApplyShapeRotationToLights(aLightDirVec, fX, fY, rfRotZ);
915
0
    }
916
917
    // (2) We try to tweak some light rigs a little bit, e.g. make sure the first light is specular
918
    // or add some ambient light instead of not possible third or forth light.
919
3
    lcl_tweakLightRig(aLightDirVec, aLightRig);
920
921
3
    rPropertyMap.setProperty(oox::PROP_Brightness, aLightRig.fAmbient.value_or(0) * 100);
922
923
    // (3) A 3D-scene of an extruded custom shape has currently no colored light, but only a
924
    // level. We get the level from Red.
925
3
    rPropertyMap.setProperty(oox::PROP_FirstLightLevel, aLightRig.aLight1.fMSOColorR * 100);
926
927
    // (4) 'Specular' and 'Diffuse' in the MSO specification belong to modern 3D geometry. That is not
928
    // available in our legacy one. Here we treat 'Specular' as property 'Harsh' and ignore 'Diffuse'.
929
3
    rPropertyMap.setProperty(oox::PROP_FirstLightHarsh, aLightRig.aLight1.bSpecular);
930
931
    // (5) In fact we have stored position in FirstLightDirection and SecondLightDirection,
932
    // not direction, thus the minus sign.
933
3
    css::drawing::Direction3D aLightPos;
934
3
    aLightPos.DirectionX = -aLightDirVec[0].getX();
935
3
    aLightPos.DirectionY = -aLightDirVec[0].getY();
936
3
    aLightPos.DirectionZ = -aLightDirVec[0].getZ();
937
3
    rPropertyMap.setProperty(oox::PROP_FirstLightDirection, aLightPos);
938
939
    // (6) For extruded custom shapes only two lights are possible although our rendering engine has
940
    // eight lights. We will loose lights.
941
3
    if (aLightDirVec.size() > 1)
942
3
    {
943
3
        rPropertyMap.setProperty(oox::PROP_SecondLightLevel,
944
3
                                 aLightRig.aLight2.value().fMSOColorR * 100);
945
3
        rPropertyMap.setProperty(oox::PROP_SecondLightHarsh, aLightRig.aLight2.value().bSpecular);
946
3
        aLightPos.DirectionX = -aLightDirVec[1].getX();
947
3
        aLightPos.DirectionY = -aLightDirVec[1].getY();
948
3
        aLightPos.DirectionZ = -aLightDirVec[1].getZ();
949
3
        rPropertyMap.setProperty(oox::PROP_SecondLightDirection, aLightPos);
950
3
    }
951
0
    else
952
0
        rPropertyMap.setProperty(oox::PROP_SecondLightLevel, 0.0); // prevent defaults.
953
3
}
954
955
namespace
956
/** This struct is used to hold material values for extruded custom shapes. Because we cannot yet
957
    render all material properties MS Office uses, the values are adapted to our current abilities.*/
958
{
959
struct MaterialValues
960
{
961
    std::u16string_view msMaterialPrstName; // identifies the material type
962
    // Corresponds to MS Office 'Diffuse Color' and 'Ambient Color'.
963
    double fDiffusion;
964
    double fSpecularity; // Corresponds to MS Office 'Specular Color'.
965
    // Corresponds to our 'Shininess' as 2^(Shininess/10) = nSpecularPower.
966
    sal_uInt8 nSpecularPower;
967
    bool bMetal; // Corresponds to MS Office 'Metal'
968
    // constants com::sun::star::drawing::EnhancedCustomShapeMetalType
969
    // MetalMSCompatible belongs to 'legacyMetal' material type.
970
    std::optional<sal_Int16> oMetalType; // MetalODF = 0, MetalMSCompatible = 1
971
    // MS Office properties 'Emissive Color', 'Diffuse Fresnel', 'Alpha Fresnel' and 'Blinn Highlight'
972
    // are not contained.
973
};
974
} // end anonymous namespace
975
976
// OOXML standard has a fixed amount of 15 material types. The type 'legacyWireframe' is special and
977
// thus is handled separately. A spreadsheet with further remarks is attached to tdf#70039.
978
constexpr sal_uInt16 nPrstMaterialCount(14);
979
constexpr MaterialValues aPrstMaterialArray[nPrstMaterialCount]
980
    = { { u"clear", 100, 60, 20, false, {} },
981
        { u"dkEdge", 70, 100, 35, false, {} },
982
        { u"flat", 100, 80, 50, false, {} },
983
        { u"legacyMatte", 100, 0, 0, false, {} },
984
        { u"legacyMetal", 66.69921875, 122.0703125, 32, true, { 1 } },
985
        { u"legacyPlastic", 100, 122.0703125, 32, false, {} },
986
        { u"matte", 100, 0, 0, false, {} },
987
        { u"metal", 100, 100, 12, true, { 0 } },
988
        { u"plastic", 100, 60, 12, true, { 0 } },
989
        { u"powder", 100, 30, 10, false, {} },
990
        { u"softEdge", 100, 100, 35, false, {} },
991
        { u"softmetal", 100, 100, 8, true, { 0 } },
992
        { u"translucentPowder", 100, 30, 10, true, { 0 } },
993
        { u"warmMatte", 100, 30, 8, false, {} } };
994
995
void Scene3DHelper::setMaterialProperties(const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
996
                                          oox::PropertyMap& rPropertyMap)
997
3
{
998
3
    if (!p3DProperties)
999
0
        return;
1000
1001
    // PowerPoint does not write aus prstMaterial="warmMatte", but handles it as default.
1002
3
    const sal_Int32 nMaterialID = (*p3DProperties).mnMaterial.value_or(XML_warmMatte); // token
1003
1004
    // special handling for 'legacyWireframe'
1005
3
    if (nMaterialID == XML_legacyWireframe)
1006
0
    {
1007
        // This is handled via shade mode of the scene.
1008
0
        rPropertyMap.setProperty(oox::PROP_ShadeMode, css::drawing::ShadeMode_DRAFT);
1009
        // Notice, the color of the strokes will be different from MS Office, because LO uses the
1010
        // shape line color even if the line style is 'none', whereas MS Office uses contour color or
1011
        // Black.
1012
0
        return;
1013
0
    }
1014
1015
3
    sal_Int16 nIdx(0); // Index into aPrstMaterialArray
1016
12
    while (nIdx < nPrstMaterialCount
1017
12
           && aPrstMaterialArray[nIdx].msMaterialPrstName
1018
12
                  != oox::drawingml::Generic3DProperties::getPresetMaterialTypeString(nMaterialID))
1019
9
        ++nIdx;
1020
3
    if (nIdx >= nPrstMaterialCount)
1021
0
        return; // error in document
1022
1023
    // extrusion-diffuse, extrusion-specularity-loext
1024
3
    rPropertyMap.setProperty(oox::PROP_Diffusion, aPrstMaterialArray[nIdx].fDiffusion);
1025
3
    rPropertyMap.setProperty(oox::PROP_Specularity, aPrstMaterialArray[nIdx].fSpecularity);
1026
1027
    // extrusion-shininess
1028
3
    double fShininess = 0.0;
1029
    // Conversion 2^(fShininess/10) = nSpecularPower
1030
3
    if (aPrstMaterialArray[nIdx].nSpecularPower > 0)
1031
0
        fShininess = 10.0 * std::log2(aPrstMaterialArray[nIdx].nSpecularPower);
1032
3
    rPropertyMap.setProperty(oox::PROP_Shininess, fShininess);
1033
1034
    // extrusion-metal, extrusion-metal-type
1035
3
    rPropertyMap.setProperty(oox::PROP_Metal, aPrstMaterialArray[nIdx].bMetal);
1036
3
    if (aPrstMaterialArray[nIdx].bMetal)
1037
0
    {
1038
0
        sal_Int16 eMetalType = aPrstMaterialArray[nIdx].oMetalType.value_or(0) == 1
1039
0
                                   ? css::drawing::EnhancedCustomShapeMetalType::MetalMSCompatible
1040
0
                                   : css::drawing::EnhancedCustomShapeMetalType::MetalODF;
1041
0
        rPropertyMap.setProperty(oox::PROP_MetalType, eMetalType);
1042
0
    }
1043
3
}
1044
1045
} // end namespace oox
1046
1047
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */