Coverage Report

Created: 2026-06-30 11:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/oox/source/drawingml/scene3dhelper.cxx
Line
Count
Source
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
17
{
148
17
    sal_Int16 nIt(0);
149
437
    while (nIt < nCameraPresetCount && aPrstCameraValuesArray[nIt].msCameraPrstName != sPresetName)
150
420
        ++nIt;
151
17
    if (nIt >= nCameraPresetCount)
152
0
    {
153
0
        nIt = -1; // Error is handled by caller
154
0
    }
155
17
    return nIt;
156
17
}
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
10
{
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
10
    basegfx::B3DHomMatrix aXMat;
173
10
    const double fLatRad = basegfx::deg2rad<60000>(nLat);
174
10
    aXMat.set(1, 1, cos(fLatRad));
175
10
    aXMat.set(2, 2, cos(fLatRad));
176
10
    aXMat.set(1, 2, sin(fLatRad));
177
10
    aXMat.set(2, 1, -sin(fLatRad));
178
179
10
    basegfx::B3DHomMatrix aYMat;
180
10
    const double fLonRad = basegfx::deg2rad<60000>(nLon);
181
10
    aYMat.set(0, 0, cos(fLonRad));
182
10
    aYMat.set(2, 2, cos(fLonRad));
183
10
    aYMat.set(0, 2, -sin(fLonRad));
184
10
    aYMat.set(2, 0, sin(fLonRad));
185
186
10
    basegfx::B3DHomMatrix aZMat;
187
10
    const double fRevRad = basegfx::deg2rad<60000>(nRev);
188
10
    aZMat.set(0, 0, cos(fRevRad));
189
10
    aZMat.set(1, 1, cos(fRevRad));
190
10
    aZMat.set(0, 1, sin(fRevRad));
191
10
    aZMat.set(1, 0, -sin(fRevRad));
192
10
    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
10
    fY = std::asin(-aTotalMat.get(0, 2));
198
199
10
    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
10
    else
210
10
    {
211
10
        fX = std::atan2(-aTotalMat.get(1, 2) / cos(fY), aTotalMat.get(2, 2) / cos(fY));
212
10
        fZ = std::atan2(aTotalMat.get(0, 1) / cos(fY), aTotalMat.get(0, 0) / cos(fY));
213
10
    }
214
10
}
215
216
void Scene3DHelper::getAPIAnglesFrom3DProperties(
217
    const oox::drawingml::Shape3DPropertiesPtr p3DProperties, const sal_Int32& rnMSOShapeRotation,
218
    double& fX, double& fY, double& fZ)
219
10
{
220
10
    if (!p3DProperties)
221
0
        return;
222
223
    // on x-axis, unit 1/60000 deg
224
10
    sal_Int32 nLatitude = (*p3DProperties).maCameraRotation.mnLatitude.value_or(0);
225
    // on y-axis, unit 1/60000 deg
226
10
    sal_Int32 nLongitude = (*p3DProperties).maCameraRotation.mnLongitude.value_or(0);
227
    // on z-axis, unit 1/60000 deg
228
10
    sal_Int32 nRevolution = (*p3DProperties).maCameraRotation.mnRevolution.value_or(0);
229
230
    // Some projection types need special treatment:
231
10
    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
10
    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
10
    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
10
    bool bIsLegacyCamera = 20 <= mnPrstCameraIndex && mnPrstCameraIndex <= 37;
266
10
    if (!bIsLegacyCamera)
267
7
        nRevolution -= rnMSOShapeRotation;
268
269
    // Now calculate the angles for LO rotation order and orientation.
270
10
    Scene3DHelper::getAPIAnglesFromOOXAngle(nLatitude, nLongitude, nRevolution, fX, fY, fZ);
271
272
10
    if (bIsLegacyCamera)
273
3
        fZ -= basegfx::deg2rad<60000>(rnMSOShapeRotation);
274
10
}
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(
382
    const oox::drawingml::Shape3DPropertiesPtr& p3DProperties, const sal_Int32& rnMSOShapeRotation,
383
    oox::PropertyMap& rPropertyMap, double& rRotZ, oox::drawingml::Color& rExtrusionColor,
384
    const bool bBlockExtrusion)
385
66.8k
{
386
    // We convert rnMSOShapeRotation, so that Shape::createAndInsert() can use rRotZ the same way in
387
    // all cases.
388
66.8k
    rRotZ = basegfx::deg2rad<60000>(-rnMSOShapeRotation);
389
390
66.8k
    if (!p3DProperties || !(*p3DProperties).mnPreset.has_value())
391
66.7k
        return false;
392
393
17
    const sal_Int32 nCameraPrstID((*p3DProperties).mnPreset.value());
394
17
    sal_Int16 nPrstCameraIndex
395
17
        = getPrstCameraIndex(oox::drawingml::Generic3DProperties::getCameraPrstName(nCameraPrstID));
396
17
    if (nPrstCameraIndex < 0 or nPrstCameraIndex >= nCameraPresetCount)
397
0
        return false; // error in document. OOXML specifies a fixed set of preset camera types.
398
17
    mnPrstCameraIndex = nPrstCameraIndex;
399
400
    // Extrusion color is not handled as extrusion property but as shape property. Thus deliver it
401
    // in any case, so that Shape::createAndInsert() knows about it.
402
17
    rExtrusionColor = (*p3DProperties).maExtrusionColor;
403
404
    // Even if we do not extrude an image, we still want to get the z-Rotation.
405
17
    if (bBlockExtrusion)
406
7
    {
407
7
        rRotZ = basegfx::deg2rad<60000>((*p3DProperties).maCameraRotation.mnRevolution.value_or(0)
408
7
                                        - rnMSOShapeRotation);
409
7
        return false;
410
7
    }
411
412
    // We use extrusion, if there is a rotation around x-axis or y-axis,
413
    // or if there is no such rotation but we have a perspective projection with true depth,
414
    // or we have a parallel projection other than a 'front' type.
415
    // In other cases the rendering as normal shape is better than any current extrusion.
416
10
    double fX = 0.0;
417
10
    double fY = 0.0;
418
10
    Scene3DHelper::getAPIAnglesFrom3DProperties(p3DProperties, rnMSOShapeRotation, fX, fY, rRotZ);
419
10
    sal_Int32 nDepthAmount = (*p3DProperties).mnExtrusionH.value_or(0);
420
10
    bool bIsParallel = aPrstCameraValuesArray[mnPrstCameraIndex].mbIsParallel;
421
10
    bool bIsParallelFrontType
422
10
        = (nCameraPrstID == XML_legacyObliqueFront) || (nCameraPrstID == XML_orthographicFront);
423
10
    bool bCreateExtrusion = (!basegfx::fTools::equalZero(fX) || !basegfx::fTools::equalZero(fY))
424
7
                            || (!bIsParallel && nDepthAmount > 0)
425
7
                            || (bIsParallel && !bIsParallelFrontType);
426
427
10
    if (!bCreateExtrusion)
428
7
        return false;
429
430
    // Create the extrusion properties in rPropertyMap so that they can be directly used.
431
    // Turn extrusion on
432
3
    rPropertyMap.setProperty(oox::PROP_Extrusion, true);
433
434
    // Dummy value. Will be changed from evaluating the material properties.
435
3
    rPropertyMap.setProperty(oox::PROP_Diffusion, 100.0);
436
437
    // Camera properties
438
3
    css::drawing::ProjectionMode eProjectionMode = bIsParallel
439
3
                                                       ? css::drawing::ProjectionMode_PARALLEL
440
3
                                                       : css::drawing::ProjectionMode_PERSPECTIVE;
441
3
    rPropertyMap.setProperty(oox::PROP_ProjectionMode, eProjectionMode);
442
443
3
    Scene3DHelper::addRotateAngleToMap(rPropertyMap, fX, fY);
444
445
3
    Scene3DHelper::addProjectionGeometryToMap(p3DProperties, rPropertyMap, bIsParallel,
446
3
                                              rnMSOShapeRotation);
447
448
    // Shape properties
449
3
    Scene3DHelper::addExtrusionDepthToMap(p3DProperties, rPropertyMap);
450
451
    // The 'automatic' extrusion color is different in MS Office. Thus we enable it in any case.
452
    // CreateAndInsert method will set a suitable 'automatic' color, if rExtrusionColor is not used.
453
3
    rPropertyMap.setProperty(oox::PROP_Color, true);
454
    // ToDo: Some materials might need ShadeMode_Smooth or ShadeMode_PHONG.
455
3
    rPropertyMap.setProperty(oox::PROP_ShadeMode, css::drawing::ShadeMode_FLAT);
456
457
3
    return true;
458
10
}
459
460
namespace
461
{
462
/* This struct is used to hold light properties for a light in a preset light rig.*/
463
struct MSOLight
464
{
465
    // Values are as specified in [MS-OI29500], see commit message.
466
    // The color is specified as RGBA, but alpha value is always 1.0 and ignored anyway, so it is
467
    // dropped here. The RGB values are in decimal, but might exceed the usual [0;1] range.
468
    double fMSOColorR;
469
    double fMSOColorG;
470
    double fMSOColorB;
471
    // MSO uses 4 decimals precision, some light directions are not normalized.
472
    double fMSOLightDirectionX;
473
    double fMSOLightDirectionY;
474
    double fMSOLightDirectionZ;
475
    double fScale;
476
    double fOffset;
477
    bool bSpecular;
478
    bool bDiffuse;
479
};
480
481
/* This struct is used to hold properties of a light rig*/
482
struct PrstLightRigValues
483
{
484
    // values are as specified in [MS-OI29500], see commit message.
485
    std::u16string_view sLightRigName; // identifies the light rig, mandatory in OOXML
486
    // The ambient color is specified as RGBA, but alpha value is always 1.0 and R = B = G. Thus we
487
    // store here only one value.
488
    std::optional<double> fAmbient;
489
    // Each rig has at least one light and maximal four lights
490
    MSOLight aLight1;
491
    std::optional<MSOLight> aLight2;
492
    std::optional<MSOLight> aLight3;
493
    std::optional<MSOLight> aLight4;
494
    // Light rig rotation is not contained in the presets.
495
};
496
} // end anonymous namespace
497
498
// The values are taken from [MS-OI29500]. For details see the spreadsheet attached to
499
// tdf#70039 and the commit message.
500
constexpr sal_uInt16 nLightRigPresetCount(27); // Fix value, specified in OOXML standard.
501
constexpr PrstLightRigValues aPrstLightRigValuesArray[nLightRigPresetCount] = {
502
    { u"balanced",
503
      { 0.13 },
504
      { 1.05, 1.05, 1.05, 0.5263, -0.4092, -0.7453, 1, 0, true, true },
505
      { { 1, 1, 1, -0.9386, 0.3426, -0.041, 1, 0, true, true } },
506
      { { 0.5, 0.5, 0.5, 0.0934, 0.763, 0.6396, 1, 0, true, true } },
507
      {} },
508
    { u"brightRoom",
509
      { 1.5 },
510
      { 1, 1, 1, 0, -1, 0, 1, 0, false, true },
511
      { { 1, 1, 1, 0.8227, -0.1882, -0.5364, 1, 0, true, false } },
512
      { { -0.5, -0.5, -0.5, 0, 0, -1, 1, 0, false, true } },
513
      { { 0.5, 0.5, 0.5, 0, 1, 0, 1, 0, false, true } } },
514
    { u"chilly",
515
      { 0.11 },
516
      { 0.31, 0.32, 0.32, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
517
      { { 0.45, 0.45, 0.45, -0.3539, -0.1505, -0.9231, 1, 0, false, true } },
518
      { { 1.03, 1.02, 1.15, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
519
      { { 0.41, 0.45, 0.48, -0.5781, 0.7976, 0.1722, 1, 0, true, true } } },
520
    { u"contrasting",
521
      { 1 },
522
      { 1, 1, 1, 0, -1, 0, 1, 0, true, false },
523
      { { 1, 1, 1, 0, 1, 0, 1, 0, true, false } },
524
      {},
525
      {} },
526
    { u"flat",
527
      { 1 },
528
      { 0.821, 0.821, 0.821, -0.9546, -0.1619, -0.2502, 1, 0, true, false },
529
      { { 2.072, 2.54, 2.91, 0.0009, 0.8605, 0.5095, 1, 0, true, false } },
530
      { { 3.843, 3.843, 3.843, 0.6574, -0.7316, -0.1806, 1, 0, true, false } },
531
      {} },
532
    { u"flood",
533
      { 0.13 },
534
      { 1.1, 1.1, 1.1, 0.5685, -0.7651, -0.3022, 1, 0, true, true },
535
      { { 1.1, 1.1, 1.1, -0.2366, -0.9595, -0.1531, 1, 0, true, true } },
536
      { { 0.55, 0.55, 0.55, -0.8982, 0.1386, -0.4171, 1, 0, true, true } },
537
      {} },
538
    { u"freezing",
539
      {},
540
      { 0.53, 0.567, 0.661, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
541
      { { 0.37, 0.461, 0.461, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
542
      { { 0.649, 0.638, 0.904, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
543
      { { 0.971, 1.19, 1.363, -0.1825, 0.968, 0.1722, 1, 0, true, true } } },
544
    { u"glow",
545
      { 1 },
546
      { 1, 1, 1, 0, -1, 0, 1, 0, true, true },
547
      { { 0.7, 0.7, 0.7, 0, 1, 0, 1, 0, true, true } },
548
      {},
549
      {} },
550
    { u"harsh",
551
      { 0.28 },
552
      { 0.88, 0.88, 0.88, 0.6689, -0.6755, -0.3104, 1, 0, true, true },
553
      { { 0.88, 0.88, 0.88, -0.592, -0.7371, -0.326, 1, 0, true, true } },
554
      {},
555
      {} },
556
    { u"legacyFlat1",
557
      { 0.305 },
558
      { 0.58, 0.58, 0.58, 0, 0, -0.2, 1, 0, true, true },
559
      { { 0.58, 0.58, 0.58, 0, 0, -0.2, 0.5, 0, false, true } },
560
      {},
561
      {} },
562
    { u"legacyFlat2",
563
      { 0.305 },
564
      { 0.58, 0.58, 0.58, -1, -1, -0.2, 1, 0, true, true },
565
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
566
      {},
567
      {} },
568
    { u"legacyFlat3",
569
      { 0.305 },
570
      { 0.58, 0.58, 0.58, 0, -1, -0.2, 1, 0, true, true },
571
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
572
      {},
573
      {} },
574
    { u"legacyFlat4",
575
      { 0.305 },
576
      { 0.58, 0.58, 0.58, 1, -1, -0.2, 1, 0, true, true },
577
      { { 0.58, 0.58, 0.58, 0, 1, -0.2, 0.5, 0, false, true } },
578
      {},
579
      {} },
580
    { u"legacyHarsh1",
581
      { 0.061 },
582
      { 0.793, 0.793, 0.793, 0, 0, -0.2, 1, 0, true, true },
583
      { { 0.214, 0.214, 0.214, 0, 0, -0.2, 1, 0, false, true } },
584
      {},
585
      {} },
586
    { u"legacyHarsh2",
587
      { 0.061 },
588
      { 0.793, 0.793, 0.793, -1, -1, -0.2, 1, 0, true, true },
589
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
590
      {},
591
      {} },
592
    { u"legacyHarsh3",
593
      { 0.061 },
594
      { 0.793, 0.793, 0.793, 0, -1, -0.2, 1, 0, true, true },
595
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
596
      {},
597
      {} },
598
    { u"legacyHarsh4",
599
      { 0.061 },
600
      { 0.793, 0.793, 0.793, 1, -1, -0.2, 1, 0, true, true },
601
      { { 0.214, 0.214, 0.214, 0, 1, -0.2, 1, 0, false, true } },
602
      {},
603
      {} },
604
    { u"legacyNormal1",
605
      { 0.153 },
606
      { 0.671, 0.671, 0.671, 0, 0, -0.2, 1, 0, true, true },
607
      { { 0.366, 0.366, 0.366, 0, 0, -0.2, 0.5, 0, false, true } },
608
      {},
609
      {} },
610
    { u"legacyNormal2",
611
      { 0.153 },
612
      { 0.671, 0.671, 0.671, -1, -1, -0.2, 1, 0, true, true },
613
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
614
      {},
615
      {} },
616
    { u"legacyNormal3",
617
      { 0.153 },
618
      { 0.671, 0.671, 0.671, 0, -1, -0.2, 1, 0, true, true },
619
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
620
      {},
621
      {} },
622
    { u"legacyNormal4",
623
      { 0.153 },
624
      { 0.671, 0.671, 0.671, 1, -1, -0.2, 1, 0, true, true },
625
      { { 0.366, 0.366, 0.366, 0, 1, -0.2, 0.5, 0, false, true } },
626
      {},
627
      {} },
628
    { u"morning",
629
      {},
630
      { 0.669, 0.648, 0.596, 0.6574, -0.7316, -0.1806, 0.5, 0.5, true, true },
631
      { { 0.459, 0.454, 0.385, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
632
      { { 0.9, 0.86, 0.83, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
633
      { { 0.911, 0.846, 0.728, -0.1825, 0.968, 0.1722, 1, 0, true, true } } },
634
    { u"soft", { 0.3 }, { 0.8, 0.8, 0.8, -0.6897, 0.2484, -0.6802, 1, 0, true, true }, {}, {}, {} },
635
    { u"sunrise",
636
      {},
637
      { 0.667, 0.63, 0.527, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
638
      { { 0.459, 0.459, 0.371, -0.2781, -0.4509, -0.8482, 1, 0, false, true } },
639
      { { 0.826, 0.712, 0.638, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
640
      { { 1.511, 1.319, 0.994, -0.1825, 0.968, 0.1722, 1, 0, false, true } } },
641
    { u"sunset",
642
      {},
643
      { 0.672, 0.169, 0.169, 0.6574, -0.7316, -0.1806, 1, 0, true, true },
644
      { { 0.459, 0.448, 0.327, 0.0922, -0.3551, -0.9303, 1, 0, false, true } },
645
      { { 0.775, 0.612, 0.502, 0.672, -0.6185, -0.4073, 1, 0, false, true } },
646
      { { 0.761, 0.69, 0.397, -0.424, 0.8891, 0.1722, 1, 0, false, true } } },
647
    { u"threePt",
648
      {},
649
      { 1.141, 1.141, 1.141, -0.6515, -0.2693, -0.7093, 1, 0, true, true },
650
      { { 0.5, 0.5, 0.5, 0.8482, 0.2469, -0.4686, 1, 0, true, true } },
651
      { { 1, 1, 1, 0.5634, -0.2812, 0.7769, 1, 0, true, true } },
652
      {} },
653
    { u"twoPt",
654
      { 0.25 },
655
      { 0.84, 0.84, 0.84, 0.5266, -0.4089, -0.7454, 0, 0, true, true },
656
      { { 0.3, 0.3, 0.3, -0.8983, 0.2365, -0.3704, 1, 0, true, true } },
657
      {},
658
      {} }
659
};
660
661
namespace
662
{
663
/** Searches for the item in aPrstLightRigValuesArray with given sPresetName.
664
    @param [in] sPresetName name as specified in OOXML standard
665
    @return returns the index if item exists, otherwise -1.*/
666
sal_Int16 lcl_getPrstLightRigIndex(std::u16string_view sPresetName)
667
3
{
668
3
    sal_Int16 nIt(0);
669
30
    while (nIt < nLightRigPresetCount && aPrstLightRigValuesArray[nIt].sLightRigName != sPresetName)
670
27
        ++nIt;
671
3
    if (nIt >= nLightRigPresetCount)
672
0
    {
673
0
        nIt = -1; // Error is handled by caller
674
0
    }
675
3
    return nIt;
676
3
}
677
678
/** Extracts the light directions from the preset lightRig.
679
    @param [in] rLightRig from which the lights are extracted
680
    @param [out] rLightDirVec contains the preset lights but each as B3DVector*/
681
void lcl_getLightDirectionsFromRig(const PrstLightRigValues& rLightRig,
682
                                   std::vector<basegfx::B3DVector>& rLightDirVec)
683
3
{
684
6
    auto addLightDir = [&](const MSOLight& aMSOLight) {
685
6
        basegfx::B3DVector aLightDir(aMSOLight.fMSOLightDirectionX, aMSOLight.fMSOLightDirectionY,
686
6
                                     aMSOLight.fMSOLightDirectionZ);
687
6
        rLightDirVec.push_back(std::move(aLightDir));
688
6
    };
689
    // aLight1 always exists, the others are optional
690
3
    addLightDir(rLightRig.aLight1);
691
3
    if (rLightRig.aLight2.has_value())
692
3
        addLightDir(rLightRig.aLight2.value());
693
3
    if (rLightRig.aLight3.has_value())
694
0
        addLightDir(rLightRig.aLight3.value());
695
3
    if (rLightRig.aLight4.has_value())
696
0
        addLightDir(rLightRig.aLight4.value());
697
3
}
698
699
/** Converts the directions from MSO specification to coordinates in the shape coordinate system.
700
    @details The extruded shape uses a left-hand Cartesian coordinate system with x-axis right, y-axis
701
    down and z-axis towards observer. When L(Lx,Ly,Lz) is the specified light direction, then
702
    V(-Ly, -Lx, Lz) is the direction in the shape coordinate system.
703
    @param [in,out] rLightDirVec contains for each individual light its direction.*/
704
void lcl_AdaptAndNormalizeLightDirections(std::vector<basegfx::B3DVector>& rLightDirVec)
705
3
{
706
3
    basegfx::B3DHomMatrix aTransform; // unit matrix
707
3
    aTransform.set(0, 0, 0.0);
708
3
    aTransform.set(0, 1, -1.0);
709
3
    aTransform.set(1, 0, -1.0);
710
3
    aTransform.set(1, 1, 0.0);
711
3
    for (auto& rDirection : rLightDirVec)
712
6
    {
713
6
        rDirection *= aTransform;
714
6
        rDirection.normalize();
715
6
    }
716
3
}
717
718
/** Gets the rotation angles fX and fY from the extrusion property RotateAngle in the map.
719
    Does nothing if property does not exist.
720
    @param [in] rPropertyMap should contain valid value in RotateAngle property
721
    @param [out] fX, fY rotation angle in unit rad with orientation as in API.*/
722
void lcl_getXYAnglesFromMap(const oox::PropertyMap& rPropertyMap, double& rfX, double& rfY)
723
0
{
724
0
    if (!rPropertyMap.hasProperty(oox::PROP_RotateAngle))
725
0
        return;
726
0
    css::drawing::EnhancedCustomShapeParameterPair aAnglePair;
727
0
    css::uno::Any aAny = rPropertyMap.getProperty(oox::PROP_RotateAngle);
728
0
    if (aAny >>= aAnglePair)
729
0
    {
730
0
        rfX = basegfx::deg2rad(aAnglePair.First.Value.get<double>());
731
0
        rfY = basegfx::deg2rad(aAnglePair.Second.Value.get<double>());
732
0
    }
733
0
}
734
735
/** Applies the rotations given in fX, fY, fZ to the light directions.
736
    @details The rotations are applied in the order fZ, fY, fX. All angles have unit rad. The
737
        orientation of the angles fX and fY is the same as in the extrusion property RotateAngle in
738
        API. The orientation of angle fZ is the same as in shape property RotateAngle in API.
739
    @param [in, out] rLightDirVec contains the to be transformed light directions
740
    @param [in] fX angle for rotation around x-axis
741
    @param [in] fY angle for rotation around y-axis
742
    @param {in] fZ angle for rotation around z-axis*/
743
void lcl_ApplyShapeRotationToLights(std::vector<basegfx::B3DVector>& rLightDirVec, const double& fX,
744
                                    const double& fY, const double& fZ)
745
0
{
746
0
    basegfx::B3DHomMatrix aTransform; // unit matrix
747
    // rotate has the order first x, then y, last z. We need order z, y, x.
748
0
    aTransform.rotate(0.0, 0.0, -fZ);
749
0
    aTransform.rotate(0.0, -fY, 0.0);
750
0
    aTransform.rotate(fX, 0.0, 0.0);
751
0
    for (auto& rDir : rLightDirVec)
752
0
        rDir *= aTransform;
753
0
}
754
755
/** Applies the light rig rotation to the directions of the individual lights
756
    @details A light rig has a mandatory attribute 'dir' for rotating the rig in 45deg steps. It might
757
        have an element 'rot', that describes a rotation by spherical coordinates 'lat', 'lon' and
758
        'rev'. The element has precedence over the attribute.
759
    @param [in] p3DProperties contains info about light rig.
760
    @param {in, out] rLightDirVec contains for each individual light its direction in shape coordinate
761
        system with x-axis right, y-axis down, z-axis toward observer.*/
762
void lcl_IncorporateRigRotationIntoLightDirections(
763
    const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
764
    std::vector<basegfx::B3DVector>& rLightDirVec)
765
3
{
766
3
    basegfx::B3DHomMatrix aTransform; // unit matrix
767
    // if a 'rot' element exists, then all of 'lat', 'lon' and 'rev' needs to exist.
768
3
    if ((*p3DProperties).maLightRigRotation.mnLatitude.has_value())
769
0
    {
770
0
        double fLat
771
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnLatitude.value_or(0));
772
0
        double fLon
773
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnLongitude.value_or(0));
774
0
        double fRev
775
0
            = basegfx::deg2rad<60000>((*p3DProperties).maLightRigRotation.mnRevolution.value_or(0));
776
0
        aTransform.rotate(0.0, 0.0, fRev);
777
0
        aTransform.rotate(fLat, fLon, 0.0);
778
0
    }
779
3
    else
780
3
    {
781
3
        sal_Int32 nDir = 0;
782
3
        switch ((*p3DProperties).mnLightRigDirection.value_or(XML_t))
783
3
        {
784
3
            case XML_t:
785
3
                nDir = 0;
786
3
                break;
787
0
            case XML_tr:
788
0
                nDir = 45;
789
0
                break;
790
0
            case XML_r:
791
0
                nDir = 90;
792
0
                break;
793
0
            case XML_br:
794
0
                nDir = 135;
795
0
                break;
796
0
            case XML_b:
797
0
                nDir = 180;
798
0
                break; // or -180
799
0
            case XML_bl:
800
0
                nDir = -135;
801
0
                break;
802
0
            case XML_l:
803
0
                nDir = -90;
804
0
                break;
805
0
            case XML_tl:
806
0
                nDir = -45;
807
0
                break;
808
0
            default:
809
0
                nDir = 0;
810
3
        }
811
        // Rotation is always only around z-axis
812
3
        aTransform.rotate(0.0, 0.0, basegfx::deg2rad(nDir));
813
3
    }
814
3
    for (auto& rDirection : rLightDirVec)
815
6
        rDirection *= aTransform;
816
3
}
817
818
/** The lights in OOXML are basically incompatible with our lights. We try to tweak some rigs to
819
    reduce obvious problems.
820
    @param [in, out] rLightDirVec light directions with already incorporated rotations
821
    @param [in, out] rLightRig the to be tweaked rig
822
*/
823
void lcl_tweakLightRig(std::vector<basegfx::B3DVector>& rLightDirVec, PrstLightRigValues& rLightRig)
824
3
{
825
3
    if (rLightRig.sLightRigName == u"brightRoom")
826
0
    {
827
        // The fourth light has more significant direction.
828
0
        if (rLightDirVec.size() >= 4 && rLightRig.aLight2.has_value()
829
0
            && rLightRig.aLight4.has_value())
830
0
        {
831
0
            std::swap(rLightDirVec[1], rLightDirVec[3]);
832
            // swap fourth and second in light rig too, swap their other properties too.
833
0
            MSOLight aTemp = rLightRig.aLight4.value();
834
0
            rLightRig.aLight4 = rLightRig.aLight2.value();
835
0
            rLightRig.aLight2 = aTemp;
836
            // and make it brighter, 1.0 instead of 0.5
837
0
            rLightRig.aLight2.value().fMSOColorR = 1.0;
838
0
            rLightRig.aLight2.value().fMSOColorG = 1.0;
839
0
            rLightRig.aLight2.value().fMSOColorB = 1.0;
840
0
        }
841
        // The object is far too bright.
842
0
        rLightRig.fAmbient = 0.6; // instead 1.5
843
0
    }
844
3
    else if (rLightRig.sLightRigName == u"chilly" || rLightRig.sLightRigName == u"flood")
845
0
    {
846
        // They are too dark.
847
0
        rLightRig.fAmbient = 0.35; // instead 0.11 resp. 0.13
848
0
    }
849
3
    else if (rLightRig.sLightRigName == u"freezing" || rLightRig.sLightRigName == u"morning"
850
3
             || rLightRig.sLightRigName == u"sunrise")
851
0
    {
852
        // These rigs have no ambient color but four lights. The objects are too dark with only
853
        // two lights.
854
0
        rLightRig.fAmbient = 0.4;
855
0
    }
856
3
    else if (rLightRig.sLightRigName == u"sunset")
857
0
    {
858
        // The fourth light is more significant.
859
0
        if (rLightDirVec.size() >= 4 && rLightRig.aLight4.has_value())
860
0
        {
861
0
            MSOLight aTemp = rLightRig.aLight2.value();
862
0
            rLightRig.aLight2 = rLightRig.aLight4.value();
863
0
            rLightRig.aLight4 = aTemp;
864
0
            std::swap(rLightDirVec[1], rLightDirVec[3]);
865
0
        }
866
0
    }
867
3
    else if (rLightRig.sLightRigName == u"soft")
868
0
    {
869
        // This is the only modern light rig with Scale=0.5 and Offset=0.5. It would be harsh=false
870
        // and specular=true at the same time. We switch specular off as that is used to set harsh on.
871
0
        rLightRig.aLight1.bSpecular = false;
872
0
    }
873
3
}
874
875
} // end anonymous namespace
876
877
void Scene3DHelper::setLightingProperties(const oox::drawingml::Shape3DPropertiesPtr& p3DProperties,
878
                                          const double& rfRotZ, oox::PropertyMap& rPropertyMap)
879
3
{
880
3
    if (!p3DProperties || !(*p3DProperties).mnLightRigType.has_value())
881
0
        return;
882
883
    // get index of light rig in aPrstLightRigValuesArray
884
3
    const sal_Int32 nLightRigPrstID((*p3DProperties).mnLightRigType.value()); // token
885
3
    sal_Int16 nPrstLightRigIndex = lcl_getPrstLightRigIndex(
886
3
        oox::drawingml::Generic3DProperties::getLightRigName(nLightRigPrstID));
887
3
    if (nPrstLightRigIndex < 0 or nPrstLightRigIndex >= nLightRigPresetCount)
888
0
        return; // error in document. OOXML specifies a fixed set of preset light rig types.
889
890
    // The light rig is copied because it might be tweaked later.
891
3
    PrstLightRigValues aLightRig = aPrstLightRigValuesArray[nPrstLightRigIndex];
892
893
3
    std::vector<basegfx::B3DVector> aLightDirVec;
894
3
    aLightDirVec.reserve(4);
895
3
    lcl_getLightDirectionsFromRig(aLightRig, aLightDirVec);
896
3
    lcl_AdaptAndNormalizeLightDirections(aLightDirVec);
897
898
3
    lcl_IncorporateRigRotationIntoLightDirections(p3DProperties, aLightDirVec);
899
900
    // Parts (1) to (6) are workarounds for the problem that our current model as well as API and
901
    // ODF are not able to describe or use the capabilities of extruded custom shapes of MS Office.
902
    // If the implementation is improved one day, the parts will need to be adapted.
903
904
    // (1) Moving the camera around does not change shape or light directions for modern cameras in
905
    // MS Office. For legacy cameras MS Office behaves same as LibreOffice: Not the camera is moved
906
    // but the shape is rotated. For modern cameras we need to rotate the light rig the same way as
907
    // the shape to get a similar illumination as in MS Office.
908
3
    if (mnPrstCameraIndex < 20 || 37 < mnPrstCameraIndex)
909
0
    {
910
0
        double fX = 0.0; // unit rad, orientation as in API
911
0
        double fY = 0.0; // unit rad, orientation as in API
912
0
        lcl_getXYAnglesFromMap(rPropertyMap, fX, fY);
913
0
        lcl_ApplyShapeRotationToLights(aLightDirVec, fX, fY, rfRotZ);
914
0
    }
915
916
    // (2) We try to tweak some light rigs a little bit, e.g. make sure the first light is specular
917
    // or add some ambient light instead of not possible third or forth light.
918
3
    lcl_tweakLightRig(aLightDirVec, aLightRig);
919
920
3
    rPropertyMap.setProperty(oox::PROP_Brightness, aLightRig.fAmbient.value_or(0) * 100);
921
922
    // (3) A 3D-scene of an extruded custom shape has currently no colored light, but only a
923
    // level. We get the level from Red.
924
3
    rPropertyMap.setProperty(oox::PROP_FirstLightLevel, aLightRig.aLight1.fMSOColorR * 100);
925
926
    // (4) 'Specular' and 'Diffuse' in the MSO specification belong to modern 3D geometry. That is not
927
    // available in our legacy one. Here we treat 'Specular' as property 'Harsh' and ignore 'Diffuse'.
928
3
    rPropertyMap.setProperty(oox::PROP_FirstLightHarsh, aLightRig.aLight1.bSpecular);
929
930
    // (5) In fact we have stored position in FirstLightDirection and SecondLightDirection,
931
    // not direction, thus the minus sign.
932
3
    css::drawing::Direction3D aLightPos;
933
3
    aLightPos.DirectionX = -aLightDirVec[0].getX();
934
3
    aLightPos.DirectionY = -aLightDirVec[0].getY();
935
3
    aLightPos.DirectionZ = -aLightDirVec[0].getZ();
936
3
    rPropertyMap.setProperty(oox::PROP_FirstLightDirection, aLightPos);
937
938
    // (6) For extruded custom shapes only two lights are possible although our rendering engine has
939
    // eight lights. We will loose lights.
940
3
    if (aLightDirVec.size() > 1)
941
3
    {
942
3
        rPropertyMap.setProperty(oox::PROP_SecondLightLevel,
943
3
                                 aLightRig.aLight2.value().fMSOColorR * 100);
944
3
        rPropertyMap.setProperty(oox::PROP_SecondLightHarsh, aLightRig.aLight2.value().bSpecular);
945
3
        aLightPos.DirectionX = -aLightDirVec[1].getX();
946
3
        aLightPos.DirectionY = -aLightDirVec[1].getY();
947
3
        aLightPos.DirectionZ = -aLightDirVec[1].getZ();
948
3
        rPropertyMap.setProperty(oox::PROP_SecondLightDirection, aLightPos);
949
3
    }
950
0
    else
951
0
        rPropertyMap.setProperty(oox::PROP_SecondLightLevel, 0.0); // prevent defaults.
952
3
}
953
954
namespace
955
/** This struct is used to hold material values for extruded custom shapes. Because we cannot yet
956
    render all material properties MS Office uses, the values are adapted to our current abilities.*/
957
{
958
struct MaterialValues
959
{
960
    std::u16string_view msMaterialPrstName; // identifies the material type
961
    // Corresponds to MS Office 'Diffuse Color' and 'Ambient Color'.
962
    double fDiffusion;
963
    double fSpecularity; // Corresponds to MS Office 'Specular Color'.
964
    // Corresponds to our 'Shininess' as 2^(Shininess/10) = nSpecularPower.
965
    sal_uInt8 nSpecularPower;
966
    bool bMetal; // Corresponds to MS Office 'Metal'
967
    // constants com::sun::star::drawing::EnhancedCustomShapeMetalType
968
    // MetalMSCompatible belongs to 'legacyMetal' material type.
969
    std::optional<sal_Int16> oMetalType; // MetalODF = 0, MetalMSCompatible = 1
970
    // MS Office properties 'Emissive Color', 'Diffuse Fresnel', 'Alpha Fresnel' and 'Blinn Highlight'
971
    // are not contained.
972
};
973
} // end anonymous namespace
974
975
// OOXML standard has a fixed amount of 15 material types. The type 'legacyWireframe' is special and
976
// thus is handled separately. A spreadsheet with further remarks is attached to tdf#70039.
977
constexpr sal_uInt16 nPrstMaterialCount(14);
978
constexpr MaterialValues aPrstMaterialArray[nPrstMaterialCount]
979
    = { { u"clear", 100, 60, 20, false, {} },
980
        { u"dkEdge", 70, 100, 35, false, {} },
981
        { u"flat", 100, 80, 50, false, {} },
982
        { u"legacyMatte", 100, 0, 0, false, {} },
983
        { u"legacyMetal", 66.69921875, 122.0703125, 32, true, { 1 } },
984
        { u"legacyPlastic", 100, 122.0703125, 32, false, {} },
985
        { u"matte", 100, 0, 0, false, {} },
986
        { u"metal", 100, 100, 12, true, { 0 } },
987
        { u"plastic", 100, 60, 12, true, { 0 } },
988
        { u"powder", 100, 30, 10, false, {} },
989
        { u"softEdge", 100, 100, 35, false, {} },
990
        { u"softmetal", 100, 100, 8, true, { 0 } },
991
        { u"translucentPowder", 100, 30, 10, true, { 0 } },
992
        { u"warmMatte", 100, 30, 8, false, {} } };
993
994
void Scene3DHelper::setMaterialProperties(const oox::drawingml::Shape3DPropertiesPtr p3DProperties,
995
                                          oox::PropertyMap& rPropertyMap)
996
3
{
997
3
    if (!p3DProperties)
998
0
        return;
999
1000
    // PowerPoint does not write aus prstMaterial="warmMatte", but handles it as default.
1001
3
    const sal_Int32 nMaterialID = (*p3DProperties).mnMaterial.value_or(XML_warmMatte); // token
1002
1003
    // special handling for 'legacyWireframe'
1004
3
    if (nMaterialID == XML_legacyWireframe)
1005
0
    {
1006
        // This is handled via shade mode of the scene.
1007
0
        rPropertyMap.setProperty(oox::PROP_ShadeMode, css::drawing::ShadeMode_DRAFT);
1008
        // Notice, the color of the strokes will be different from MS Office, because LO uses the
1009
        // shape line color even if the line style is 'none', whereas MS Office uses contour color or
1010
        // Black.
1011
0
        return;
1012
0
    }
1013
1014
3
    sal_Int16 nIdx(0); // Index into aPrstMaterialArray
1015
12
    while (nIdx < nPrstMaterialCount
1016
12
           && aPrstMaterialArray[nIdx].msMaterialPrstName
1017
12
                  != oox::drawingml::Generic3DProperties::getPresetMaterialTypeString(nMaterialID))
1018
9
        ++nIdx;
1019
3
    if (nIdx >= nPrstMaterialCount)
1020
0
        return; // error in document
1021
1022
    // extrusion-diffuse, extrusion-specularity-loext
1023
3
    rPropertyMap.setProperty(oox::PROP_Diffusion, aPrstMaterialArray[nIdx].fDiffusion);
1024
3
    rPropertyMap.setProperty(oox::PROP_Specularity, aPrstMaterialArray[nIdx].fSpecularity);
1025
1026
    // extrusion-shininess
1027
3
    double fShininess = 0.0;
1028
    // Conversion 2^(fShininess/10) = nSpecularPower
1029
3
    if (aPrstMaterialArray[nIdx].nSpecularPower > 0)
1030
0
        fShininess = 10.0 * std::log2(aPrstMaterialArray[nIdx].nSpecularPower);
1031
3
    rPropertyMap.setProperty(oox::PROP_Shininess, fShininess);
1032
1033
    // extrusion-metal, extrusion-metal-type
1034
3
    rPropertyMap.setProperty(oox::PROP_Metal, aPrstMaterialArray[nIdx].bMetal);
1035
3
    if (aPrstMaterialArray[nIdx].bMetal)
1036
0
    {
1037
0
        sal_Int16 eMetalType = aPrstMaterialArray[nIdx].oMetalType.value_or(0) == 1
1038
0
                                   ? css::drawing::EnhancedCustomShapeMetalType::MetalMSCompatible
1039
0
                                   : css::drawing::EnhancedCustomShapeMetalType::MetalODF;
1040
0
        rPropertyMap.setProperty(oox::PROP_MetalType, eMetalType);
1041
0
    }
1042
3
}
1043
1044
} // end namespace oox
1045
1046
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */