Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickqmlgenerator.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6#include "utils_p.h"
7
8#include <private/qsgcurveprocessor_p.h>
9#include <private/qquickshape_p.h>
10#include <private/qquadpath_p.h>
11#include <private/qquickitem_p.h>
12#include <private/qquickimagebase_p_p.h>
13#include <private/qquicktext_p.h>
14#include <private/qquicktranslate_p.h>
15#include <private/qquickimage_p.h>
16
17#include <QtCore/qloggingcategory.h>
18#include <QtCore/qdir.h>
19#include <QtCore/qstandardpaths.h>
20
22
23using namespace Qt::StringLiterals;
24
25static QString sanitizeString(const QString &input)
26{
27 QString s = input;
28 s.replace(QLatin1Char('\\'), QLatin1String("\\\\"));
29 s.replace(QLatin1Char('"'), QLatin1String("\\\""));
30 return s;
31}
32
33
34QQuickAnimatedProperty::PropertyAnimation QQuickAnimatedProperty::PropertyAnimation::simplified() const
35{
36 QQuickAnimatedProperty::PropertyAnimation res = *this;
37 int consecutiveEquals = 0;
38 int prevTimePoint = -1;
39 QVariant prevValue;
40 for (const auto &[timePoint, value] : frames.asKeyValueRange()) {
41 if (value != prevValue) {
42 consecutiveEquals = 1;
43 prevValue = value;
44 } else if (consecutiveEquals < 2) {
45 consecutiveEquals++;
46 } else {
47 // Third consecutive equal value found, remove the redundant middle one
48 res.frames.remove(prevTimePoint);
49 res.easingPerFrame.remove(prevTimePoint);
50 }
51 prevTimePoint = timePoint;
52 }
53
54 return res;
55}
56
57QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName)
58 : QQuickGenerator(fileName, flags)
59 , outputFileName(outFileName)
60{
61 m_result.open(QIODevice::ReadWrite);
62 m_oldIndentLevels.push(0);
63
64 if (outFileName.isEmpty()) {
65 setRetainFilePaths(true);
66 setAssetFileDirectory(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
67 setAssetFilePrefix(QStringLiteral("_qt_vectorimage_"));
68 setUrlPrefix(QStringLiteral("file:"));
69 }
70}
71
72QQuickQmlGenerator::~QQuickQmlGenerator()
73{
74}
75
76bool QQuickQmlGenerator::save()
77{
78 if (Q_UNLIKELY(errorState()))
79 return false;
80
81 bool res = true;
82 if (!outputFileName.isEmpty()) {
83 QFileInfo fileInfo(outputFileName);
84 QDir dir(fileInfo.absolutePath());
85 if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
86 qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath();
87 res = false;
88 } else {
89 QFile outFile(outputFileName);
90 if (outFile.open(QIODevice::WriteOnly)) {
91 outFile.write(m_result.data());
92 outFile.close();
93 } else {
94 qCWarning(lcQuickVectorImage) << "Failed to write to file" << outFile.fileName();
95 res = false;
96 }
97 }
98 }
99
100 if (lcQuickVectorImage().isDebugEnabled())
101 qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(300);
102
103 return res;
104}
105
106void QQuickQmlGenerator::setShapeTypeName(const QString &name)
107{
108 m_shapeTypeName = name.toLatin1();
109}
110
111QString QQuickQmlGenerator::shapeTypeName() const
112{
113 return QString::fromLatin1(m_shapeTypeName);
114}
115
116void QQuickQmlGenerator::setCommentString(const QString commentString)
117{
118 m_commentString = commentString;
119}
120
121QString QQuickQmlGenerator::commentString() const
122{
123 return m_commentString;
124}
125
126QString QQuickQmlGenerator::generateNodeBase(const NodeInfo &info, const QString &idSuffix)
127{
128 static qint64 maxNodes = qEnvironmentVariableIntegerValue("QT_QUICKVECTORIMAGE_MAX_NODES").value_or(10000);
129 if (Q_UNLIKELY(!checkSanityLimit(++m_nodeCounter, maxNodes, "nodes"_L1)))
130 return {};
131
132 if (!info.nodeId.isEmpty())
133 stream() << "objectName: \"" << info.nodeId << "\"";
134
135 if (!info.id.isEmpty())
136 stream() << "id: " << info.id << idSuffix;
137
138 if (!info.bounds.isNull() || !info.boundsReferenceId.isEmpty()) {
139 stream() << "property var originalBounds: ";
140 if (!info.bounds.isNull()) {
141 stream(SameLine) << "Qt.rect(" << info.bounds.x() << ", " << info.bounds.y() << ", "
142 << info.bounds.width() << ", " << info.bounds.height() << ")";
143 } else {
144 stream(SameLine) << info.boundsReferenceId << ".originalBounds";
145 }
146 stream() << "width: originalBounds.width";
147 stream() << "height: originalBounds.height";
148 }
149
150 stream() << "transformOrigin: Item.TopLeft";
151
152 if (info.filterId.isEmpty() && info.maskId.isEmpty()) {
153 if (!info.isDefaultOpacity)
154 stream() << "opacity: " << info.opacity.defaultValue().toReal();
155 generateItemAnimations(info.id, info);
156 }
157
158 return info.id;
159}
160
161void QQuickQmlGenerator::generateNodeEnd(const NodeInfo &info)
162{
163 if (Q_UNLIKELY(errorState()))
164 return;
165 m_indentLevel--;
166 stream() << "}";
167 generateShaderUse(info);
168}
169
170void QQuickQmlGenerator::generateItemAnimations(const QString &idString, const NodeInfo &info)
171{
172 const bool hasTransform = info.transform.isAnimated()
173 || !info.maskId.isEmpty()
174 || !info.filterId.isEmpty()
175 || !info.isDefaultTransform
176 || !info.transformReferenceId.isEmpty()
177 || info.motionPath.isAnimated();
178
179 if (hasTransform) {
180 stream() << "transform: TransformGroup {";
181 m_indentLevel++;
182
183 bool hasNonConstantTransform = false;
184 int earliestOverrideGroup = -1;
185
186 if (!idString.isEmpty()) {
187 stream() << "id: " << idString << "_transform_base_group";
188
189 if (!info.maskId.isEmpty() || !info.filterId.isEmpty())
190 stream() << "Translate { x: " << idString << ".sourceX; y: " << idString << ".sourceY }";
191
192 if (info.transform.isAnimated()) {
193 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
194 stream() << "TransformGroup {";
195 m_indentLevel++;
196
197 if (!idString.isEmpty())
198 stream() << "id: " << idString << "_transform_group_" << groupIndex;
199
200 int animationStart = info.transform.animationGroup(groupIndex);
201 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
202 ? info.transform.animationGroup(groupIndex + 1)
203 : info.transform.animationCount();
204
205 const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart);
206 const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations;
207 if (replace && earliestOverrideGroup < 0)
208 earliestOverrideGroup = groupIndex;
209
210 for (int i = nextAnimationStart - 1; i >= animationStart; --i) {
211 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
212 if (animation.frames.isEmpty())
213 continue;
214
215 const QVariantList &parameters = animation.frames.first().value<QVariantList>();
216 switch (animation.subtype) {
217 case QTransform::TxTranslate:
218 if (animation.isConstant()) {
219 const QPointF translation = parameters.value(0).value<QPointF>();
220 if (!translation.isNull())
221 stream() << "Translate { x: " << translation.x() << "; y: " << translation.y() << " }";
222 } else {
223 hasNonConstantTransform = true;
224 stream() << "Translate { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
225 }
226 break;
227 case QTransform::TxScale:
228 if (animation.isConstant()) {
229 const QPointF scale = parameters.value(0).value<QPointF>();
230 if (scale != QPointF(1, 1))
231 stream() << "Scale { xScale: " << scale.x() << "; yScale: " << scale.y() << " }";
232 } else {
233 hasNonConstantTransform = true;
234 stream() << "Scale { id: " << idString << "_transform_" << groupIndex << "_" << i << "}";
235 }
236 break;
237 case QTransform::TxRotate:
238 if (animation.isConstant()) {
239 const QPointF center = parameters.value(0).value<QPointF>();
240 const qreal angle = parameters.value(1).toReal();
241 if (!qFuzzyIsNull(angle))
242 stream() << "Rotation { angle: " << angle << "; origin.x: " << center.x() << "; origin.y: " << center.y() << " }"; //### center relative to what?
243 } else {
244 hasNonConstantTransform = true;
245 stream() << "Rotation { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
246 }
247 break;
248 case QTransform::TxShear:
249 if (animation.isConstant()) {
250 const QPointF skew = parameters.value(0).value<QPointF>();
251 if (!skew.isNull())
252 stream() << "Shear { xAngle: " << skew.x() << "; yAngle: " << skew.y() << " }";
253 } else {
254 hasNonConstantTransform = true;
255 stream() << "Shear { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
256 }
257 break;
258 default:
259 Q_UNREACHABLE();
260 }
261 }
262
263 m_indentLevel--;
264 stream() << "}";
265 }
266 }
267
268 if (info.motionPath.isAnimated()) {
269 QVariantList defaultProps = info.motionPath.defaultValue().value<QVariantList>();
270 const bool adaptAngle = defaultProps.value(1).toBool();
271 const qreal baseRotation = defaultProps.value(2).toReal();
272 QString interpolatorId = idString + QStringLiteral("_motion_interpolator");
273 if (adaptAngle || !qFuzzyIsNull(baseRotation)) {
274 stream() << "Rotation {";
275 m_indentLevel++;
276
277 if (adaptAngle) {
278 stream() << "angle: " << interpolatorId << ".angle";
279 if (!qFuzzyIsNull(baseRotation))
280 stream(SameLine) << " + " << baseRotation;
281 } else {
282 stream() << "angle: " << baseRotation;
283 }
284
285 m_indentLevel--;
286 stream() << "}";
287 }
288
289 stream() << "Translate {";
290 m_indentLevel++;
291
292 stream() << "x: " << interpolatorId << ".x";
293 stream() << "y: " << interpolatorId << ".y";
294
295 m_indentLevel--;
296 stream() << "}";
297 }
298 }
299
300 if (!info.isDefaultTransform) {
301 QTransform xf = info.transform.defaultValue().value<QTransform>();
302 if (xf.type() <= QTransform::TxTranslate) {
303 stream() << "Translate { x: " << xf.dx() << "; y: " << xf.dy() << "}";
304 } else {
305 stream() << "Matrix4x4 { matrix: ";
306 generateTransform(xf);
307 stream(SameLine) << "}";
308 }
309 }
310
311 if (!info.transformReferenceId.isEmpty())
312 stream() << "Matrix4x4 { matrix: " << info.transformReferenceId << ".transformMatrix }";
313
314 m_indentLevel--;
315 stream() << "}";
316
317 if (hasNonConstantTransform) {
318 generateAnimateTransform(idString, info);
319 } else if (info.transform.isAnimated() && earliestOverrideGroup >= 0) {
320 // We have animations, but they are all constant? Then we still need to respect the
321 // override flag of the animations
322 stream() << "Component.onCompleted: {";
323 m_indentLevel++;
324
325 stream() << idString << "_transform_base_group.activateOverride("
326 << idString << "_transform_group_" << earliestOverrideGroup << ")";
327
328 m_indentLevel--;
329 stream() << "}";
330 }
331 }
332
333 generateAnimateMotionPath(idString, info.motionPath);
334
335 generatePropertyAnimation(info.opacity, idString, QStringLiteral("opacity"));
336}
337
338void QQuickQmlGenerator::generateShaderUse(const NodeInfo &info)
339{
340 const bool hasMask = !info.maskId.isEmpty();
341 const bool hasFilters = !info.filterId.isEmpty();
342 if (!hasMask && !hasFilters)
343 return;
344
345 const QString effectId = hasFilters
346 ? info.filterId + QStringLiteral("_") + info.id + QStringLiteral("_effect")
347 : QString{};
348
349 QString animatedItemId;
350 if (hasFilters) {
351 stream() << "ShaderEffectSource {";
352 m_indentLevel++;
353
354 const QString seId = info.id + QStringLiteral("_se");
355 stream() << "id: " << seId;
356
357 stream() << "ItemSpy {";
358 m_indentLevel++;
359 stream() << "id: " << info.id << "_itemspy";
360 stream() << "anchors.fill: parent";
361 m_indentLevel--;
362 stream() << "}";
363
364 stream() << "hideSource: true";
365 stream() << "wrapMode: " << info.filterId << "_filterParameters.wrapMode";
366 stream() << "sourceItem: " << info.id;
367 stream() << "sourceRect: " << info.filterId
368 << "_filterParameters.adaptToFilterRect("
369 << info.id << ".originalBounds.x, "
370 << info.id << ".originalBounds.y, "
371 << info.id << ".originalBounds.width, "
372 << info.id << ".originalBounds.height)";
373 stream() << "textureSize: " << info.id << "_itemspy.requiredTextureSize";
374 stream() << "width: sourceRect.width";
375 stream() << "height: sourceRect.height";
376 stream() << "visible: false";
377
378 m_indentLevel--;
379 stream() << "}";
380
381 stream() << "Loader {";
382 m_indentLevel++;
383
384 animatedItemId = effectId;
385 stream() << "id: " << effectId;
386
387 stream() << "property var filterSourceItem: " << seId;
388 stream() << "sourceComponent: " << info.filterId << "_container";
389 stream() << "property real sourceX: " << info.id << ".originalBounds.x";
390 stream() << "property real sourceY: " << info.id << ".originalBounds.y";
391 stream() << "width: " << info.id << ".originalBounds.width";
392 stream() << "height: " << info.id << ".originalBounds.height";
393
394 if (hasMask) {
395 m_indentLevel--;
396 stream() << "}";
397 }
398 }
399
400 if (hasMask) {
401 // Shader effect source for the mask itself
402 stream() << "ShaderEffectSource {";
403 m_indentLevel++;
404
405 const QString maskId = info.maskId + QStringLiteral("_") + info.id + QStringLiteral("_mask");
406 stream() << "id: " << maskId;
407 stream() << "sourceItem: " << info.maskId;
408 stream() << "visible: false";
409 stream() << "hideSource: true";
410
411 if (m_contentRelativeMasks.contains(info.maskId)) {
412 stream() << "Binding { target: " << info.maskId
413 << "; property: \"contentX\"; value: " << info.id << ".originalBounds.x }";
414 stream() << "Binding { target: " << info.maskId
415 << "; property: \"contentY\"; value: " << info.id << ".originalBounds.y }";
416 stream() << "Binding { target: " << info.maskId
417 << "; property: \"contentWidth\"; value: " << info.id
418 << ".originalBounds.width }";
419 stream() << "Binding { target: " << info.maskId
420 << "; property: \"contentHeight\"; value: " << info.id
421 << ".originalBounds.height }";
422 }
423
424 stream() << "ItemSpy {";
425 m_indentLevel++;
426 stream() << "id: " << maskId << "_itemspy";
427 stream() << "anchors.fill: parent";
428 m_indentLevel--;
429 stream() << "}";
430 stream() << "textureSize: " << maskId << "_itemspy.requiredTextureSize";
431
432 stream() << "sourceRect: " << info.maskId << ".maskRect("
433 << info.id << ".originalBounds.x,"
434 << info.id << ".originalBounds.y,"
435 << info.id << ".originalBounds.width,"
436 << info.id << ".originalBounds.height)";
437
438 stream() << "width: sourceRect.width";
439 stream() << "height: sourceRect.height";
440
441 m_indentLevel--;
442 stream() << "}";
443
444 // Shader effect source of the masked item
445 stream() << "ShaderEffectSource {";
446 m_indentLevel++;
447
448 const QString seId = info.id + QStringLiteral("_masked_se");
449 stream() << "id: " << seId;
450
451 stream() << "ItemSpy {";
452 m_indentLevel++;
453 stream() << "id: " << info.id << "_masked_se_itemspy";
454 stream() << "anchors.fill: parent";
455 m_indentLevel--;
456 stream() << "}";
457
458 stream() << "hideSource: true";
459 if (hasFilters)
460 stream() << "sourceItem: " << effectId;
461 else
462 stream() << "sourceItem: " << info.id;
463 stream() << "textureSize: " << info.id << "_masked_se_itemspy.requiredTextureSize";
464 if (!hasFilters) {
465 stream() << "sourceRect: " << info.maskId << ".maskRect("
466 << info.id << ".originalBounds.x,"
467 << info.id << ".originalBounds.y,"
468 << info.id << ".originalBounds.width,"
469 << info.id << ".originalBounds.height)";
470 } else {
471 stream() << "sourceRect: " << info.maskId << ".maskRect(0, 0,"
472 << info.id << ".originalBounds.width,"
473 << info.id << ".originalBounds.height)";
474 }
475 stream() << "width: sourceRect.width";
476 stream() << "height: sourceRect.height";
477 stream() << "smooth: false";
478 stream() << "visible: false";
479
480 m_indentLevel--;
481 stream() << "}";
482
483 stream() << "ShaderEffect {";
484 m_indentLevel++;
485
486 const QString maskShaderId = maskId + QStringLiteral("_se");
487 animatedItemId = maskShaderId;
488
489 stream() << "id:" << maskShaderId;
490
491 stream() << "property real sourceX: " << maskId << ".sourceRect.x";
492 stream() << "property real sourceY: " << maskId << ".sourceRect.y";
493 stream() << "width: " << maskId << ".sourceRect.width";
494 stream() << "height: " << maskId << ".sourceRect.height";
495
496 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/genericmask.frag.qsb\"";
497 stream() << "property var source: " << seId;
498 stream() << "property var maskSource: " << maskId;
499 stream() << "property bool isAlpha: " << (info.isMaskAlpha ? "true" : "false");
500 stream() << "property bool isInverted: " << (info.isMaskInverted ? "true" : "false");
501 }
502
503 if (!info.isDefaultOpacity)
504 stream() << "opacity: " << info.opacity.defaultValue().toReal();
505
506 generateItemAnimations(animatedItemId, info);
507
508 m_indentLevel--;
509 stream() << "}";
510}
511
512bool QQuickQmlGenerator::generateDefsNode(const StructureNodeInfo &info)
513{
514 if (Q_UNLIKELY(errorState()))
515 return false;
516
517 if (info.stage == StructureNodeStage::Start) {
518 m_oldIndentLevels.push(m_indentLevel);
519
520 stream() << "Component {";
521 m_indentLevel++;
522
523 stream() << "id: " << info.id << "_container";
524
525 stream() << "Item {";
526 m_indentLevel++;
527
528 generateTimelineFields(info);
529 if (!info.transformReferenceChildId.isEmpty()) {
530 stream() << "property alias transformMatrix: "
531 << info.transformReferenceChildId << ".transformMatrix";
532 }
533
534 generateNodeBase(info, QStringLiteral("_defs"));
535 } else {
536 generateNodeEnd(info);
537
538 m_indentLevel--;
539 stream() << "}"; // Component
540
541 stream() << m_defsSuffix;
542 m_defsSuffix.clear();
543
544 m_indentLevel = m_oldIndentLevels.pop();
545 }
546
547 return true;
548}
549
550void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info)
551{
552 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
553 return;
554
555 const QFileInfo outputFileInfo(outputFileName);
556 const QDir outputDir(outputFileInfo.absolutePath());
557
558 QString filePath;
559
560 if (!m_retainFilePaths || info.externalFileReference.isEmpty()) {
561 filePath = m_assetFileDirectory;
562 if (filePath.isEmpty())
563 filePath = outputDir.absolutePath();
564
565 if (!filePath.isEmpty() && !filePath.endsWith(u'/'))
566 filePath += u'/';
567
568 QDir fileDir(filePath);
569 if (!fileDir.exists()) {
570 if (!fileDir.mkpath(QStringLiteral(".")))
571 qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath;
572 }
573
574 filePath += QStringLiteral("%1%2.png").arg(m_assetFilePrefix.isEmpty()
575 ? QStringLiteral("svg_asset_")
576 : m_assetFilePrefix)
577 .arg(info.image.cacheKey());
578
579 if (!info.image.save(filePath))
580 qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath;
581 qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath;
582 } else {
583 filePath = info.externalFileReference;
584 }
585
586 const QFileInfo assetFileInfo(filePath);
587
588 stream() << "Image {";
589
590 m_indentLevel++;
591 generateNodeBase(info);
592 stream() << "x: " << info.rect.x();
593 stream() << "y: " << info.rect.y();
594 stream() << "width: " << info.rect.width();
595 stream() << "height: " << info.rect.height();
596 stream() << "source: \"" << m_urlPrefix << outputDir.relativeFilePath(assetFileInfo.absoluteFilePath()) <<"\"";
597 generateNodeEnd(info);
598}
599
600void QQuickQmlGenerator::generateMarkers(const PathNodeInfo &info)
601{
602 const QPainterPath path = info.path.defaultValue().value<QPainterPath>();
603 for (int i = 0; i < path.elementCount(); ++i) {
604 const QPainterPath::Element element = path.elementAt(i);
605 QString markerId;
606 qreal angle = 0;
607
608 // Copied from Qt SVG
609 auto getMeanAngle = [](QPointF p0, QPointF p1, QPointF p2) -> qreal {
610 QPointF t1 = p1 - p0;
611 QPointF t2 = p2 - p1;
612 qreal hyp1 = hypot(t1.x(), t1.y());
613 if (hyp1 > 0)
614 t1 /= hyp1;
615 else
616 return 0.;
617 qreal hyp2 = hypot(t2.x(), t2.y());
618 if (hyp2 > 0)
619 t2 /= hyp2;
620 else
621 return 0.;
622 QPointF tangent = t1 + t2;
623 return -atan2(tangent.y(), tangent.x()) / M_PI * 180.;
624 };
625
626 if (i == 0) {
627 markerId = info.markerStartId;
628 angle = path.angleAtPercent(0.0);
629 } else if (i == path.elementCount() - 1) {
630 markerId = info.markerEndId;
631 angle = path.angleAtPercent(1.0);
632 } else if (path.elementAt(i + 1).type != QPainterPath::CurveToDataElement) {
633 markerId = info.markerMidId;
634
635 const QPainterPath::Element prevElement = path.elementAt(i - 1);
636 const QPainterPath::Element nextElement = path.elementAt(i + 1);
637
638 QPointF p1(prevElement.x, prevElement.y);
639 QPointF p2(element.x, element.y);
640 QPointF p3(nextElement.x, nextElement.y);
641
642 angle = getMeanAngle(p1, p2, p3);
643 }
644
645 if (!markerId.isEmpty()) {
646 stream() << "Loader {";
647 m_indentLevel++;
648
649 //stream() << "clip: true";
650 stream() << "sourceComponent: " << markerId << "_container";
651 stream() << "property real strokeWidth: " << info.strokeStyle.width.defaultValue().toReal();
652 stream() << "transform: [";
653 m_indentLevel++;
654 if (i == 0) {
655 stream() << "Scale { "
656 << "xScale: " << markerId << "_markerParameters.startReversed ? -1 : 1; "
657 << "yScale: " << markerId << "_markerParameters.startReversed ? -1 : 1 },";
658 }
659 stream() << "Rotation { angle: " << markerId << "_markerParameters.autoAngle(" << -angle << ") },";
660 stream() << "Translate { x: " << element.x << "; y: " << element.y << "}";
661
662 m_indentLevel--;
663 stream() << "]";
664
665 m_indentLevel--;
666 stream() << "}";
667 }
668 }
669}
670
671void QQuickQmlGenerator::generatePath(const PathNodeInfo &info, const QRectF &overrideBoundingRect)
672{
673 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
674 return;
675
676 if (m_inShapeItemLevel > 0) {
677 if (!info.isDefaultTransform)
678 qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)";
679 optimizePaths(info, overrideBoundingRect);
680 } else {
681 m_inShapeItemLevel++;
682 stream() << shapeName() << " {";
683
684 m_indentLevel++;
685 generateNodeBase(info);
686
687 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
688 stream() << "preferredRendererType: Shape.CurveRenderer";
689 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::AsyncShapes))
690 stream() << "asynchronous: true";
691 optimizePaths(info, overrideBoundingRect);
692 //qCDebug(lcQuickVectorGraphics) << *node->qpath();
693
694 if (!info.markerStartId.isEmpty()
695 || !info.markerMidId.isEmpty()
696 || !info.markerEndId.isEmpty()) {
697 generateMarkers(info);
698 }
699
700 generateNodeEnd(info);
701 m_inShapeItemLevel--;
702 }
703}
704
705void QQuickQmlGenerator::generateGradient(const QGradient *grad,
706 const QString &propertyName,
707 const QRectF &coordinateConversion)
708{
709 const QSizeF &scale = coordinateConversion.size();
710 const QPointF &translation = coordinateConversion.topLeft();
711
712 if (grad->type() == QGradient::LinearGradient) {
713 auto *linGrad = static_cast<const QLinearGradient *>(grad);
714 stream() << propertyName << ": LinearGradient {";
715 m_indentLevel++;
716
717 QRectF gradRect(linGrad->start(), linGrad->finalStop());
718
719 stream() << "x1: " << (gradRect.left() * scale.width()) + translation.x();
720 stream() << "y1: " << (gradRect.top() * scale.height()) + translation.y();
721 stream() << "x2: " << (gradRect.right() * scale.width()) + translation.x();
722 stream() << "y2: " << (gradRect.bottom() * scale.height()) + translation.y();
723 for (auto &stop : linGrad->stops())
724 stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7)
725 << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }";
726 } else if (grad->type() == QGradient::RadialGradient) {
727 auto *radGrad = static_cast<const QRadialGradient*>(grad);
728 stream() << propertyName << ": RadialGradient {";
729 m_indentLevel++;
730
731 stream() << "centerX: " << (radGrad->center().x() * scale.width()) + translation.x();
732 stream() << "centerY: " << (radGrad->center().y() * scale.height()) + translation.y();
733 stream() << "centerRadius: " << (radGrad->radius() * scale.width()); // ### ?
734 stream() << "focalX:" << (radGrad->focalPoint().x() * scale.width()) + translation.x();
735 stream() << "focalY:" << (radGrad->focalPoint().y() * scale.height()) + translation.y();
736 for (auto &stop : radGrad->stops())
737 stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7)
738 << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }";
739 }
740
741 stream() << "spread: ShapeGradient.";
742 switch (grad->spread()) {
743 case QGradient::PadSpread:
744 stream(SameLine) << "PadSpread";
745 break;
746 case QGradient::ReflectSpread:
747 stream(SameLine) << "ReflectSpread";
748 break;
749 case QGradient::RepeatSpread:
750 stream(SameLine) << "RepeatSpread";
751 break;
752 }
753
754 m_indentLevel--;
755 stream() << "}";
756}
757
758void QQuickQmlGenerator::generateAnimationBindings()
759{
760 QString prefix;
761 if (Q_UNLIKELY(!isRuntimeGenerator()))
762 prefix = QStringLiteral(".animations");
763
764 stream() << "loops: " << m_topLevelIdString << prefix << ".loops";
765 stream() << "paused: " << m_topLevelIdString << prefix << ".paused";
766 stream() << "running: true";
767
768 // We need to reset the animation when the loop count changes
769 stream() << "onLoopsChanged: { if (running) { restart() } }";
770}
771
772void QQuickQmlGenerator::generateEasing(const QQuickAnimatedProperty::PropertyAnimation &animation,
773 int time, int streamFlags)
774{
775 if (animation.easingPerFrame.contains(time)) {
776 QBezier bezier = animation.easingPerFrame.value(time);
777 QPointF c1 = bezier.pt2();
778 QPointF c2 = bezier.pt3();
779
780 bool isLinear = (c1 == c1.transposed() && c2 == c2.transposed());
781 if (!isLinear) {
782 int nextIdx = m_easings.size();
783 QString &id = m_easings[{c1.x(), c1.y(), c2.x(), c2.y()}];
784 if (id.isNull())
785 id = QString(QLatin1String("easing_%1")).arg(nextIdx, 2, 10, QLatin1Char('0'));
786 if (streamFlags & SameLine)
787 stream(streamFlags) << "; ";
788 stream(streamFlags) << "easing: " << m_topLevelIdString << "." << id;
789 }
790 }
791}
792
793static int processAnimationTime(int time)
794{
795 static qreal multiplier = qreal(qEnvironmentVariable("QT_QUICKVECTORIMAGE_TIME_DILATION", QStringLiteral("1.0"))
796 .toDouble());
797 return std::round(multiplier * time);
798}
799
800void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty &property,
801 const QString &targetName,
802 const QString &propertyName,
803 AnimationType animationType)
804{
805 if (!property.isAnimated())
806 return;
807
808 if (usingTimelineAnimation())
809 return generatePropertyTimeline(property, targetName, propertyName, animationType);
810
811 QString mainAnimationId = targetName
812 + QStringLiteral("_")
813 + propertyName
814 + QStringLiteral("_animation");
815 mainAnimationId.replace(QLatin1Char('.'), QLatin1Char('_'));
816
817 QString prefix;
818 if (Q_UNLIKELY(!isRuntimeGenerator()))
819 prefix = QStringLiteral(".animations");
820
821 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
822
823 stream() << "ParallelAnimation {";
824 m_indentLevel++;
825
826 stream() << "id: " << mainAnimationId;
827
828 generateAnimationBindings();
829
830 for (int i = 0; i < property.animationCount(); ++i) {
831 const QQuickAnimatedProperty::PropertyAnimation &animation = property.animation(i);
832
833 stream() << "SequentialAnimation {";
834 m_indentLevel++;
835
836 const int startOffset = processAnimationTime(animation.startOffset);
837 if (startOffset > 0)
838 stream() << "PauseAnimation { duration: " << startOffset << " }";
839
840 stream() << "SequentialAnimation {";
841 m_indentLevel++;
842
843 const int repeatCount = animation.repeatCount;
844 if (repeatCount < 0)
845 stream() << "loops: Animation.Infinite";
846 else
847 stream() << "loops: " << repeatCount;
848
849 int previousTime = 0;
850 QVariant previousValue;
851 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
852 const int time = it.key();
853 const int frameTime = processAnimationTime(time - previousTime);
854 const QVariant &value = it.value();
855
856 if (previousValue.isValid() && previousValue == value) {
857 if (frameTime > 0)
858 stream() << "PauseAnimation { duration: " << frameTime << " }";
859 } else if (animationType == AnimationType::Auto && value.typeId() == QMetaType::Bool) {
860 // We special case bools, with PauseAnimation and then a setter at the end
861 if (frameTime > 0)
862 stream() << "PauseAnimation { duration: " << frameTime << " }";
863 stream() << "ScriptAction {";
864 m_indentLevel++;
865
866 stream() << "script:" << targetName << "." << propertyName << " = " << value.toString();
867
868 m_indentLevel--;
869 stream() << "}";
870 } else {
871 generateAnimatedPropertySetter(targetName,
872 propertyName,
873 value,
874 animation,
875 frameTime,
876 time,
877 animationType);
878 }
879
880 previousTime = time;
881 previousValue = value;
882 }
883
884 if (!(animation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd)) {
885 stream() << "ScriptAction {";
886 m_indentLevel++;
887 stream() << "script: ";
888
889 switch (animationType) {
890 case AnimationType::Auto:
891 stream(SameLine) << targetName << "." << propertyName << " = ";
892 break;
893 case AnimationType::ColorOpacity:
894 stream(SameLine) << targetName << "." << propertyName << ".a = ";
895 break;
896 };
897
898 QVariant value = property.defaultValue();
899 if (value.typeId() == QMetaType::QColor)
900 stream(SameLine) << "\"" << value.toString() << "\"";
901 else
902 stream(SameLine) << value.toReal();
903
904 m_indentLevel--;
905 stream() << "}";
906 }
907
908 m_indentLevel--;
909 stream() << "}";
910
911 m_indentLevel--;
912 stream() << "}";
913 }
914
915 m_indentLevel--;
916 stream() << "}";
917}
918
919void QQuickQmlGenerator::generateTimelinePropertySetter(
920 const QString &targetName,
921 const QString &propertyName,
922 const QQuickAnimatedProperty::PropertyAnimation &animation,
923 std::function<QVariant(const QVariant &)> const& extractValue,
924 int valueIndex)
925{
926 if (animation.repeatCount != 1 || animation.startOffset
927 || animation.flags != QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd) {
928 qCWarning(lcQuickVectorImage) << "Animation feature not implemented in timeline mode, for"
929 << targetName << propertyName;
930 }
931
932 stream() << "KeyframeGroup {";
933 m_indentLevel++;
934 stream() << "target: " << targetName;
935 stream() << "property: \"" << propertyName << "\"";
936
937 for (const auto &[frame, rawValue] : animation.frames.asKeyValueRange()) {
938 QVariant value;
939 if (rawValue.typeId() == QMetaType::QVariantList)
940 value = extractValue(rawValue.toList().value(valueIndex));
941 else
942 value = extractValue(rawValue);
943
944 stream() << "Keyframe { frame: " << frame << "; value: ";
945 if (value.typeId() == QMetaType::QVector3D) {
946 const QVector3D &v = value.value<QVector3D>();
947 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
948 } else if (value.typeId() == QMetaType::QColor) {
949 stream(SameLine) << "\"" << value.toString() << "\"";
950 } else {
951 stream(SameLine) << value.toReal();
952 }
953 generateEasing(animation, frame, SameLine);
954 stream(SameLine) << " }";
955 }
956
957 m_indentLevel--;
958 stream() << "}";
959}
960
961void QQuickQmlGenerator::generatePropertyTimeline(const QQuickAnimatedProperty &property,
962 const QString &targetName,
963 const QString &propertyName,
964 AnimationType animationType)
965{
966 if (animationType == QQuickQmlGenerator::AnimationType::ColorOpacity) {
967 qCWarning(lcQuickVectorImage) << "ColorOpacity animation not available in timeline mode";
968 return;
969 }
970
971 if (property.animationGroupCount() > 1 || property.animationCount() > 1) {
972 qCWarning(lcQuickVectorImage) << "Property feature not implemented in timeline mode, for"
973 << targetName << propertyName;
974 }
975
976 stream() << "Timeline {";
977 m_indentLevel++;
978 stream() << "currentFrame: " << property.timelineReferenceId() << ".frameCounter";
979 stream() << "enabled: true";
980
981 auto extractor = [](const QVariant &value) { return value; };
982 generateTimelinePropertySetter(targetName, propertyName, property.animation(0), extractor);
983
984 m_indentLevel--;
985 stream() << "}";
986}
987
988void QQuickQmlGenerator::generateTransform(const QTransform &xf)
989{
990 if (xf.isAffine()) {
991 stream(SameLine) << "PlanarTransform.fromAffineMatrix("
992 << xf.m11() << ", " << xf.m12() << ", "
993 << xf.m21() << ", " << xf.m22() << ", "
994 << xf.dx() << ", " << xf.dy() << ")";
995 } else {
996 QMatrix4x4 m(xf);
997 stream(SameLine) << "Qt.matrix4x4(";
998 m_indentLevel += 3;
999 const auto *data = m.data();
1000 for (int i = 0; i < 4; i++) {
1001 stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12];
1002 if (i < 3)
1003 stream(SameLine) << ", ";
1004 }
1005 stream(SameLine) << ")";
1006 m_indentLevel -= 3;
1007 }
1008}
1009
1010void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect)
1011{
1012 Q_UNUSED(pathSelector)
1013 Q_ASSERT(painterPath || quadPath);
1014
1015 if (Q_UNLIKELY(errorState()))
1016 return;
1017
1018 const bool invalidGradientBounds = info.strokeGrad.coordinateMode() == QGradient::ObjectMode
1019 && (qFuzzyIsNull(boundingRect.width()) ||
1020 qFuzzyIsNull(boundingRect.height()));
1021
1022 const QColor strokeColor = info.strokeStyle.color.defaultValue().value<QColor>();
1023 const bool noPen = (strokeColor == QColorConstants::Transparent || !strokeColor.isValid())
1024 && !info.strokeStyle.color.isAnimated()
1025 && !info.strokeStyle.opacity.isAnimated()
1026 && (info.strokeGrad.type() == QGradient::NoGradient
1027 || invalidGradientBounds);
1028 if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen)
1029 return;
1030
1031 const QColor fillColor = info.fillColor.defaultValue().value<QColor>();
1032 const bool noFill = info.grad.type() == QGradient::NoGradient
1033 && fillColor == QColorConstants::Transparent
1034 && !info.fillColor.isAnimated()
1035 && !info.fillOpacity.isAnimated();
1036 if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill)
1037 return;
1038
1039 if (noPen && noFill)
1040 return;
1041 auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule());
1042 stream() << "ShapePath {";
1043 m_indentLevel++;
1044
1045 QString shapePathId = info.id;
1046 if (pathSelector & QQuickVectorImageGenerator::FillPath)
1047 shapePathId += QStringLiteral("_fill");
1048 if (pathSelector & QQuickVectorImageGenerator::StrokePath)
1049 shapePathId += QStringLiteral("_stroke");
1050
1051 stream() << "id: " << shapePathId;
1052
1053 if (!info.nodeId.isEmpty()) {
1054 switch (pathSelector) {
1055 case QQuickVectorImageGenerator::FillPath:
1056 stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\"";
1057 break;
1058 case QQuickVectorImageGenerator::StrokePath:
1059 stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\"";
1060 break;
1061 case QQuickVectorImageGenerator::FillAndStroke:
1062 stream() << "objectName: \"svg_path:" << info.nodeId << "\"";
1063 break;
1064 }
1065 }
1066
1067 if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) {
1068 stream() << "strokeColor: \"transparent\"";
1069 } else {
1070 if (info.strokeGrad.type() != QGradient::NoGradient && !invalidGradientBounds) {
1071 QRectF coordinateSys = info.strokeGrad.coordinateMode() == QGradient::ObjectMode
1072 ? boundingRect
1073 : QRectF(0.0, 0.0, 1.0, 1.0);
1074 generateGradient(&info.strokeGrad, QStringLiteral("strokeGradient"), coordinateSys);
1075 } else if (info.strokeStyle.opacity.isAnimated()) {
1076 stream() << "property color strokeBase: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1077 stream() << "property real strokeOpacity: " << info.strokeStyle.opacity.defaultValue().toReal();
1078 stream() << "strokeColor: Qt.rgba(strokeBase.r, strokeBase.g, strokeBase.b, strokeOpacity)";
1079 } else {
1080 stream() << "strokeColor: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1081 }
1082 stream() << "strokeWidth: " << info.strokeStyle.width.defaultValue().toReal();
1083 stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(info.strokeStyle.lineCapStyle);
1084 stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(info.strokeStyle.lineJoinStyle);
1085 stream() << "miterLimit: " << info.strokeStyle.miterLimit;
1086 if (info.strokeStyle.cosmetic)
1087 stream() << "cosmeticStroke: true";
1088 if (info.strokeStyle.dashArray.length() != 0) {
1089 stream() << "strokeStyle: " << "ShapePath.DashLine";
1090 stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(info.strokeStyle.dashArray);
1091 stream() << "dashOffset: " << info.strokeStyle.dashOffset.defaultValue().toReal();
1092 }
1093 }
1094
1095 QTransform fillTransform = info.fillTransform;
1096 if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) {
1097 stream() << "fillColor: \"transparent\"";
1098 } else if (info.grad.type() != QGradient::NoGradient) {
1099 generateGradient(&info.grad, QStringLiteral("fillGradient"));
1100
1101 // Scaling done via fillTransform to get correct order of operations
1102 if (info.grad.coordinateMode() == QGradient::ObjectMode) {
1103 QTransform objectToUserSpace;
1104 objectToUserSpace.translate(boundingRect.x(), boundingRect.y());
1105 objectToUserSpace.scale(boundingRect.width(), boundingRect.height());
1106 fillTransform *= objectToUserSpace;
1107 }
1108 } else {
1109 if (info.fillOpacity.isAnimated()) {
1110 stream() << "property color fillBase: \"" << fillColor.name(QColor::HexArgb) << "\"";
1111 stream() << "property real fillOpacity:" << info.fillOpacity.defaultValue().toReal();
1112 stream() << "fillColor: Qt.rgba(fillBase.r, fillBase.g, fillBase.b, fillOpacity)";
1113 } else {
1114 stream() << "fillColor: \"" << fillColor.name(QColor::HexArgb) << "\"";
1115 }
1116 }
1117
1118 if (!info.patternId.isEmpty()) {
1119 stream() << "fillItem: ShaderEffectSource {";
1120 m_indentLevel++;
1121
1122 stream() << "parent: " << info.id;
1123 stream() << "sourceItem: " << info.patternId;
1124 stream() << "hideSource: true";
1125 stream() << "visible: false";
1126 stream() << "width: " << info.patternId << ".width";
1127 stream() << "height: " << info.patternId << ".height";
1128 stream() << "wrapMode: ShaderEffectSource.Repeat";
1129 stream() << "textureSize: Qt.size(width * __qt_toplevel_scale_itemspy.requiredTextureSize.width, "
1130 << "height * __qt_toplevel_scale_itemspy.requiredTextureSize.height)";;
1131 stream() << "sourceRect: " << info.patternId << ".sourceRect("
1132 << info.id << ".width, "
1133 << info.id << ".height)";
1134
1135 m_indentLevel--;
1136 stream() << "}";
1137
1138 // Fill transform has to include the inverse of the scene scale, since the texture size
1139 // is scaled by this amount
1140 stream() << "function calculateFillTransform(xScale, yScale) {";
1141 m_indentLevel++;
1142
1143 stream() << "var m = ";
1144 generateTransform(fillTransform);
1145
1146 stream() << "m.translate(" << info.patternId << ".sourceOffset("
1147 << info.id << ".width, "
1148 << info.id << ".height))";
1149
1150 stream() << "m.scale(1.0 / xScale, 1.0 / yScale, 1.0)";
1151 stream() << "return m";
1152
1153 m_indentLevel--;
1154 stream() << "}";
1155
1156 stream() << "fillTransform: calculateFillTransform(__qt_toplevel_scale_itemspy.requiredTextureSize.width, "
1157 << "__qt_toplevel_scale_itemspy.requiredTextureSize.height)";
1158
1159 } else if (!fillTransform.isIdentity()) {
1160 const QTransform &xf = fillTransform;
1161 stream() << "fillTransform: ";
1162 if (info.fillTransform.type() == QTransform::TxTranslate)
1163 stream(SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")";
1164 else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy())
1165 stream(SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")";
1166 else
1167 generateTransform(xf);
1168 }
1169
1170 if (info.trim.enabled) {
1171 stream() << "trim.start: " << info.trim.start.defaultValue().toReal();
1172 stream() << "trim.end: " << info.trim.end.defaultValue().toReal();
1173 stream() << "trim.offset: " << info.trim.offset.defaultValue().toReal();
1174
1175 }
1176
1177 if (fillRule == QQuickShapePath::WindingFill)
1178 stream() << "fillRule: ShapePath.WindingFill";
1179 else
1180 stream() << "fillRule: ShapePath.OddEvenFill";
1181
1182 QString hintStr;
1183 if (quadPath)
1184 hintStr = QQuickVectorImageGenerator::Utils::pathHintString(*quadPath);
1185 if (!hintStr.isEmpty())
1186 stream() << hintStr;
1187
1188 QQuickAnimatedProperty pathFactor(QVariant::fromValue(0));
1189 pathFactor.setTimelineReferenceId(info.path.timelineReferenceId());
1190 QString pathId = shapePathId + "_ip"_L1;
1191 if (!info.path.isAnimated() || (info.path.animation(0).startOffset == 0 && info.path.animation(0).isConstant())) {
1192 QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(*painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(*quadPath);
1193 stream() << "PathSvg { path: \"" << svgPathString << "\" }";
1194 } else {
1195 stream() << "PathInterpolated {";
1196 m_indentLevel++;
1197 stream() << "id: " << pathId;
1198 stream() << "svgPaths: [";
1199 m_indentLevel++;
1200 QQuickAnimatedProperty::PropertyAnimation pathFactorAnim = info.path.animation(0);
1201 auto &frames = pathFactorAnim.frames;
1202 int pathIdx = -1;
1203 QString lastSvg;
1204 for (auto it = frames.begin(); it != frames.end(); ++it) {
1205 QString svg = QQuickVectorImageGenerator::Utils::toSvgString(it->value<QPainterPath>());
1206 if (svg != lastSvg) {
1207 if (pathIdx >= 0)
1208 stream(SameLine) << ",";
1209 stream() << "\"" << svg << "\"";
1210 ++pathIdx;
1211 lastSvg = svg;
1212 }
1213 *it = QVariant::fromValue(pathIdx);
1214 }
1215 pathFactor.addAnimation(pathFactorAnim);
1216 m_indentLevel--;
1217 stream() << "]";
1218 m_indentLevel--;
1219 stream() << "}";
1220 }
1221
1222 m_indentLevel--;
1223 stream() << "}";
1224
1225 if (pathFactor.isAnimated())
1226 generatePropertyAnimation(pathFactor, pathId, "factor"_L1);
1227
1228 if (info.trim.enabled) {
1229 generatePropertyAnimation(info.trim.start, shapePathId + QStringLiteral(".trim"), QStringLiteral("start"));
1230 generatePropertyAnimation(info.trim.end, shapePathId + QStringLiteral(".trim"), QStringLiteral("end"));
1231 generatePropertyAnimation(info.trim.offset, shapePathId + QStringLiteral(".trim"), QStringLiteral("offset"));
1232 }
1233
1234 if (info.strokeStyle.opacity.isAnimated()) {
1235 generatePropertyAnimation(info.strokeStyle.color, shapePathId, QStringLiteral("strokeBase"));
1236 generatePropertyAnimation(info.strokeStyle.opacity, shapePathId, QStringLiteral("strokeOpacity"));
1237 } else {
1238 generatePropertyAnimation(info.strokeStyle.color, shapePathId, QStringLiteral("strokeColor"));
1239 }
1240 if (info.strokeStyle.width.isAnimated())
1241 generatePropertyAnimation(info.strokeStyle.width, shapePathId, QStringLiteral("strokeWidth"));
1242 if (info.strokeStyle.dashOffset.isAnimated())
1243 generatePropertyAnimation(info.strokeStyle.dashOffset, shapePathId, QStringLiteral("dashOffset"));
1244
1245 if (info.fillOpacity.isAnimated()) {
1246 generatePropertyAnimation(info.fillColor, shapePathId, QStringLiteral("fillBase"));
1247 generatePropertyAnimation(info.fillOpacity, shapePathId, QStringLiteral("fillOpacity"));
1248 } else {
1249 generatePropertyAnimation(info.fillColor, shapePathId, QStringLiteral("fillColor"));
1250 }
1251}
1252
1253void QQuickQmlGenerator::generateNode(const NodeInfo &info)
1254{
1255 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1256 return;
1257
1258 stream() << "// Missing Implementation for SVG Node: " << info.typeName;
1259 stream() << "// Adding an empty Item and skipping";
1260 stream() << "Item {";
1261 m_indentLevel++;
1262 generateNodeBase(info);
1263 generateNodeEnd(info);
1264}
1265
1266void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
1267{
1268 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1269 return;
1270
1271 stream() << "Item {";
1272 m_indentLevel++;
1273 generateNodeBase(info);
1274
1275 if (!info.isTextArea)
1276 stream() << "Item { id: textAlignItem_" << m_textNodeCounter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}";
1277
1278 stream() << "Text {";
1279
1280 m_indentLevel++;
1281
1282 const QString textItemId = QStringLiteral("_qt_textItem_%1").arg(m_textNodeCounter);
1283 stream() << "id: " << textItemId;
1284
1285 generatePropertyAnimation(info.fillColor, textItemId, QStringLiteral("color"));
1286 generatePropertyAnimation(info.fillOpacity, textItemId, QStringLiteral("color"), AnimationType::ColorOpacity);
1287 generatePropertyAnimation(info.strokeColor, textItemId, QStringLiteral("styleColor"));
1288 generatePropertyAnimation(info.strokeOpacity, textItemId, QStringLiteral("styleColor"), AnimationType::ColorOpacity);
1289
1290 if (info.isTextArea) {
1291 stream() << "x: " << info.position.x();
1292 stream() << "y: " << info.position.y();
1293 if (info.size.width() > 0)
1294 stream() << "width: " << info.size.width();
1295 if (info.size.height() > 0)
1296 stream() << "height: " << info.size.height();
1297 stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard
1298 stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level
1299 } else {
1300 QString hAlign = QStringLiteral("left");
1301 stream() << "anchors.baseline: textAlignItem_" << m_textNodeCounter << ".top";
1302 switch (info.alignment) {
1303 case Qt::AlignHCenter:
1304 hAlign = QStringLiteral("horizontalCenter");
1305 break;
1306 case Qt::AlignRight:
1307 hAlign = QStringLiteral("right");
1308 break;
1309 default:
1310 qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment;
1311 Q_FALLTHROUGH();
1312 case Qt::AlignLeft:
1313 break;
1314 }
1315 stream() << "anchors." << hAlign << ": textAlignItem_" << m_textNodeCounter << ".left";
1316 }
1317 m_textNodeCounter++;
1318
1319 stream() << "color: \"" << info.fillColor.defaultValue().value<QColor>().name(QColor::HexArgb) << "\"";
1320 stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText");
1321
1322 stream() << "text: \"" << sanitizeString(info.text) << "\"";
1323 stream() << "font.family: \"" << sanitizeString(info.font.family()) << "\"";
1324 if (info.font.pixelSize() > 0)
1325 stream() << "font.pixelSize:" << info.font.pixelSize();
1326 else if (info.font.pointSize() > 0)
1327 stream() << "font.pixelSize:" << info.font.pointSizeF();
1328 if (info.font.underline())
1329 stream() << "font.underline: true";
1330 if (info.font.weight() != QFont::Normal)
1331 stream() << "font.weight: " << int(info.font.weight());
1332 if (info.font.italic())
1333 stream() << "font.italic: true";
1334 switch (info.font.hintingPreference()) {
1335 case QFont::PreferFullHinting:
1336 stream() << "font.hintingPreference: Font.PreferFullHinting";
1337 break;
1338 case QFont::PreferVerticalHinting:
1339 stream() << "font.hintingPreference: Font.PreferVerticalHinting";
1340 break;
1341 case QFont::PreferNoHinting:
1342 stream() << "font.hintingPreference: Font.PreferNoHinting";
1343 break;
1344 case QFont::PreferDefaultHinting:
1345 stream() << "font.hintingPreference: Font.PreferDefaultHinting";
1346 break;
1347 };
1348
1349 const QColor strokeColor = info.strokeColor.defaultValue().value<QColor>();
1350 if (strokeColor != QColorConstants::Transparent || info.strokeColor.isAnimated()) {
1351 stream() << "styleColor: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1352 stream() << "style: Text.Outline";
1353 }
1354
1355 m_indentLevel--;
1356 stream() << "}";
1357
1358 generateNodeEnd(info);
1359}
1360
1361void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info)
1362{
1363 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1364 return;
1365
1366 if (info.stage == StructureNodeStage::Start) {
1367 stream() << "Item {";
1368 m_indentLevel++;
1369 generateNodeBase(info);
1370 } else {
1371 generateNodeEnd(info);
1372 }
1373}
1374
1375void QQuickQmlGenerator::generatePathContainer(const StructureNodeInfo &info)
1376{
1377 Q_UNUSED(info);
1378 stream() << shapeName() <<" {";
1379 m_indentLevel++;
1380 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
1381 stream() << "preferredRendererType: Shape.CurveRenderer";
1382 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::AsyncShapes))
1383 stream() << "asynchronous: true";
1384 m_indentLevel--;
1385
1386 m_inShapeItemLevel++;
1387}
1388
1389void QQuickQmlGenerator::generateAnimateMotionPath(const QString &targetName,
1390 const QQuickAnimatedProperty &property)
1391{
1392 if (!property.isAnimated())
1393 return;
1394
1395 QPainterPath path = property.defaultValue().value<QVariantList>().value(0).value<QPainterPath>();
1396 const QString mainAnimationId = targetName + QStringLiteral("_motion_interpolator");
1397 stream() << "PathInterpolator {";
1398 m_indentLevel++;
1399 stream() << "id: " << mainAnimationId;
1400 const QString svgPathString = QQuickVectorImageGenerator::Utils::toSvgString(path);
1401 stream() << "path: Path { PathSvg { path: \"" << svgPathString << "\" } }";
1402 m_indentLevel--;
1403 stream() << "}";
1404
1405 generatePropertyAnimation(property, mainAnimationId, QStringLiteral("progress"));
1406}
1407
1408void QQuickQmlGenerator::generateAnimatedPropertySetter(const QString &targetName,
1409 const QString &propertyName,
1410 const QVariant &value,
1411 const QQuickAnimatedProperty::PropertyAnimation &animation,
1412 int frameTime,
1413 int time,
1414 AnimationType animationType)
1415{
1416 if (frameTime > 0) {
1417 switch (animationType) {
1418 case AnimationType::Auto:
1419 if (value.typeId() == QMetaType::QColor)
1420 stream() << "ColorAnimation {";
1421 else
1422 stream() << "PropertyAnimation {";
1423 break;
1424 case AnimationType::ColorOpacity:
1425 stream() << "ColorOpacityAnimation {";
1426 break;
1427 };
1428 m_indentLevel++;
1429
1430 stream() << "duration: " << frameTime;
1431 stream() << "target: " << targetName;
1432 stream() << "property: \"" << propertyName << "\"";
1433 stream() << "to: ";
1434 if (value.typeId() == QMetaType::QVector3D) {
1435 const QVector3D &v = value.value<QVector3D>();
1436 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
1437 } else if (value.typeId() == QMetaType::QColor) {
1438 stream(SameLine) << "\"" << value.toString() << "\"";
1439 } else {
1440 stream(SameLine) << value.toReal();
1441 }
1442 generateEasing(animation, time);
1443 m_indentLevel--;
1444 stream() << "}";
1445 } else {
1446 stream() << "ScriptAction {";
1447 m_indentLevel++;
1448 stream() << "script:" << targetName << "." << propertyName;
1449 if (animationType == AnimationType::ColorOpacity)
1450 stream(SameLine) << ".a";
1451
1452 stream(SameLine) << " = ";
1453 if (value.typeId() == QMetaType::QVector3D) {
1454 const QVector3D &v = value.value<QVector3D>();
1455 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
1456 } else if (value.typeId() == QMetaType::QColor) {
1457 stream(SameLine) << "\"" << value.toString() << "\"";
1458 } else {
1459 stream(SameLine) << value.toReal();
1460 }
1461 m_indentLevel--;
1462 stream() << "}";
1463 }
1464}
1465
1466void QQuickQmlGenerator::generateAnimateTransform(const QString &targetName, const NodeInfo &info)
1467{
1468 if (!info.transform.isAnimated())
1469 return;
1470
1471 if (usingTimelineAnimation())
1472 return generateTransformTimeline(targetName, info);
1473
1474 const QString mainAnimationId = targetName
1475 + QStringLiteral("_transform_animation");
1476
1477 QString prefix;
1478 if (Q_UNLIKELY(!isRuntimeGenerator()))
1479 prefix = QStringLiteral(".animations");
1480 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
1481
1482 stream() << "ParallelAnimation {";
1483 m_indentLevel++;
1484
1485 stream() << "id:" << mainAnimationId;
1486
1487 generateAnimationBindings();
1488 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
1489 int animationStart = info.transform.animationGroup(groupIndex);
1490 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
1491 ? info.transform.animationGroup(groupIndex + 1)
1492 : info.transform.animationCount();
1493
1494 // The first animation in the group holds the shared properties for the whole group
1495 const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart);
1496 const bool freeze = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
1497 const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations;
1498
1499 stream() << "SequentialAnimation {";
1500 m_indentLevel++;
1501
1502 const int startOffset = processAnimationTime(firstAnimation.startOffset);
1503 if (startOffset > 0)
1504 stream() << "PauseAnimation { duration: " << startOffset << " }";
1505
1506 const int repeatCount = firstAnimation.repeatCount;
1507 if (repeatCount < 0)
1508 stream() << "loops: Animation.Infinite";
1509 else
1510 stream() << "loops: " << repeatCount;
1511
1512 if (replace) {
1513 stream() << "ScriptAction {";
1514 m_indentLevel++;
1515
1516 stream() << "script: " << targetName << "_transform_base_group"
1517 << ".activateOverride(" << targetName << "_transform_group_" << groupIndex << ")";
1518
1519 m_indentLevel--;
1520 stream() << "}";
1521 }
1522
1523 stream() << "ParallelAnimation {";
1524 m_indentLevel++;
1525
1526 for (int i = animationStart; i < nextAnimationStart; ++i) {
1527 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
1528 if (animation.isConstant())
1529 continue;
1530 bool hasRotationCenter = false;
1531 if (animation.subtype == QTransform::TxRotate) {
1532 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1533 const QPointF center = it->value<QVariantList>().value(0).value<QPointF>();
1534 if (!center.isNull()) {
1535 hasRotationCenter = true;
1536 break;
1537 }
1538 }
1539 }
1540
1541 stream() << "SequentialAnimation {";
1542 m_indentLevel++;
1543
1544 int previousTime = 0;
1545 QVariantList previousParameters;
1546 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1547 const int time = it.key();
1548 const int frameTime = processAnimationTime(time - previousTime);
1549 const QVariantList &parameters = it.value().value<QVariantList>();
1550 if (parameters.isEmpty())
1551 continue;
1552
1553 if (parameters == previousParameters) {
1554 if (frameTime > 0)
1555 stream() << "PauseAnimation { duration: " << frameTime << " }";
1556 } else {
1557 stream() << "ParallelAnimation {";
1558 m_indentLevel++;
1559
1560 const QString propertyTargetName = targetName
1561 + QStringLiteral("_transform_")
1562 + QString::number(groupIndex)
1563 + QStringLiteral("_")
1564 + QString::number(i);
1565
1566 switch (animation.subtype) {
1567 case QTransform::TxTranslate:
1568 {
1569 const QPointF translation = parameters.first().value<QPointF>();
1570
1571 generateAnimatedPropertySetter(propertyTargetName,
1572 QStringLiteral("x"),
1573 translation.x(),
1574 animation,
1575 frameTime,
1576 time);
1577 generateAnimatedPropertySetter(propertyTargetName,
1578 QStringLiteral("y"),
1579 translation.y(),
1580 animation,
1581 frameTime,
1582 time);
1583 break;
1584 }
1585 case QTransform::TxScale:
1586 {
1587 const QPointF scale = parameters.first().value<QPointF>();
1588 generateAnimatedPropertySetter(propertyTargetName,
1589 QStringLiteral("xScale"),
1590 scale.x(),
1591 animation,
1592 frameTime,
1593 time);
1594 generateAnimatedPropertySetter(propertyTargetName,
1595 QStringLiteral("yScale"),
1596 scale.y(),
1597 animation,
1598 frameTime,
1599 time);
1600 break;
1601 }
1602 case QTransform::TxRotate:
1603 {
1604 Q_ASSERT(parameters.size() == 2);
1605 const qreal angle = parameters.value(1).toReal();
1606 if (hasRotationCenter) {
1607 const QPointF center = parameters.value(0).value<QPointF>();
1608 generateAnimatedPropertySetter(propertyTargetName,
1609 QStringLiteral("origin"),
1610 QVector3D(center.x(), center.y(), 0.0),
1611 animation,
1612 frameTime,
1613 time);
1614 }
1615 generateAnimatedPropertySetter(propertyTargetName,
1616 QStringLiteral("angle"),
1617 angle,
1618 animation,
1619 frameTime,
1620 time);
1621 break;
1622 }
1623 case QTransform::TxShear:
1624 {
1625 const QPointF skew = parameters.first().value<QPointF>();
1626
1627 generateAnimatedPropertySetter(propertyTargetName,
1628 QStringLiteral("xAngle"),
1629 skew.x(),
1630 animation,
1631 frameTime,
1632 time);
1633
1634 generateAnimatedPropertySetter(propertyTargetName,
1635 QStringLiteral("yAngle"),
1636 skew.y(),
1637 animation,
1638 frameTime,
1639 time);
1640 break;
1641 }
1642 default:
1643 Q_UNREACHABLE();
1644 }
1645
1646 m_indentLevel--;
1647 stream() << "}"; // Parallel key frame animation
1648 }
1649
1650 previousTime = time;
1651 previousParameters = parameters;
1652 }
1653
1654 m_indentLevel--;
1655 stream() << "}"; // Parallel key frame animation
1656 }
1657
1658 m_indentLevel--;
1659 stream() << "}"; // Parallel key frame animation
1660
1661 // If the animation ever finishes, then we add an action on the end that handles itsr
1662 // freeze state
1663 if (firstAnimation.repeatCount >= 0) {
1664 stream() << "ScriptAction {";
1665 m_indentLevel++;
1666
1667 stream() << "script: {";
1668 m_indentLevel++;
1669
1670 if (!freeze) {
1671 stream() << targetName << "_transform_base_group.deactivate("
1672 << targetName << "_transform_group_" << groupIndex << ")";
1673 } else if (!replace) {
1674 stream() << targetName << "_transform_base_group.deactivateOverride("
1675 << targetName << "_transform_group_" << groupIndex << ")";
1676 }
1677
1678 m_indentLevel--;
1679 stream() << "}";
1680
1681 m_indentLevel--;
1682 stream() << "}";
1683 }
1684
1685 m_indentLevel--;
1686 stream() << "}";
1687 }
1688
1689 m_indentLevel--;
1690 stream() << "}";
1691}
1692
1693void QQuickQmlGenerator::generateTransformTimeline(const QString &targetName, const NodeInfo &info)
1694{
1695 stream() << "Timeline {";
1696 m_indentLevel++;
1697 stream() << "currentFrame: " << info.transform.timelineReferenceId() << ".frameCounter";
1698 stream() << "enabled: true";
1699
1700 const int groupIndex = 0;
1701 for (int i = 0; i < info.transform.animationCount(); ++i) {
1702 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
1703 if (animation.isConstant())
1704 continue;
1705 if (info.transform.animationGroupCount() > 1
1706 || animation.repeatCount != 1 || animation.startOffset
1707 || animation.flags != QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd) {
1708 qCWarning(lcQuickVectorImage) << "Feature not implemented in timeline xf animation mode, for"
1709 << targetName << "subtype" << animation.subtype;
1710 }
1711
1712 bool hasRotationCenter = false;
1713 if (animation.subtype == QTransform::TxRotate) {
1714 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1715 const QPointF center = it->value<QVariantList>().value(0).value<QPointF>();
1716 if (!center.isNull()) {
1717 hasRotationCenter = true;
1718 break;
1719 }
1720 }
1721 }
1722
1723 auto pointFxExtractor = [](const QVariant &value) { return value.toPointF().x(); };
1724 auto pointFyExtractor = [](const QVariant &value) { return value.toPointF().y(); };
1725 auto realExtractor = [](const QVariant &value) { return value.toReal(); };
1726 auto pointFtoVector3dExtractor = [](const QVariant &v) { return QVector3D(v.toPointF()); };
1727
1728 const QString propertyTargetName = targetName
1729 + QStringLiteral("_transform_")
1730 + QString::number(groupIndex)
1731 + QStringLiteral("_")
1732 + QString::number(i);
1733
1734 switch (animation.subtype) {
1735 case QTransform::TxTranslate:
1736 generateTimelinePropertySetter(propertyTargetName, "x"_L1, animation, pointFxExtractor);
1737 generateTimelinePropertySetter(propertyTargetName, "y"_L1, animation, pointFyExtractor);
1738 break;
1739 case QTransform::TxScale:
1740 generateTimelinePropertySetter(propertyTargetName, "xScale"_L1, animation, pointFxExtractor);
1741 generateTimelinePropertySetter(propertyTargetName, "yScale"_L1, animation, pointFyExtractor);
1742 break;
1743 case QTransform::TxRotate:
1744 if (hasRotationCenter)
1745 generateTimelinePropertySetter(propertyTargetName, "origin"_L1, animation, pointFtoVector3dExtractor);
1746 generateTimelinePropertySetter(propertyTargetName, "angle"_L1, animation, realExtractor, 1);
1747 break;
1748 case QTransform::TxShear:
1749 generateTimelinePropertySetter(propertyTargetName, "xAngle"_L1, animation, pointFxExtractor);
1750 generateTimelinePropertySetter(propertyTargetName, "yAngle"_L1, animation, pointFyExtractor);
1751 break;
1752 }
1753 }
1754
1755 m_indentLevel--;
1756 stream() << "}";
1757}
1758
1759bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info)
1760{
1761 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1762 return false;
1763
1764 const bool isPathContainer = !info.forceSeparatePaths && info.isPathContainer;
1765 if (info.stage == StructureNodeStage::Start) {
1766 if (!info.clipBox.isEmpty()) {
1767 stream() << "Item { // Clip";
1768
1769 m_indentLevel++;
1770 stream() << "width: " << info.clipBox.width();
1771 stream() << "height: " << info.clipBox.height();
1772 stream() << "clip: true";
1773 }
1774
1775 if (isPathContainer) {
1776 generatePathContainer(info);
1777 } else if (!info.customItemType.isEmpty()) {
1778 stream() << info.customItemType << " {";
1779 } else {
1780 stream() << "Item { // Structure node";
1781 }
1782 m_indentLevel++;
1783
1784 generateTimelineFields(info);
1785
1786 if (!info.viewBox.isEmpty()) {
1787 stream() << "transform: [";
1788 m_indentLevel++;
1789 bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y());
1790 if (translate)
1791 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
1792 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
1793 m_indentLevel--;
1794 stream() << "]";
1795 }
1796
1797 generateNodeBase(info);
1798 } else {
1799 generateNodeEnd(info);
1800 if (isPathContainer)
1801 m_inShapeItemLevel--;
1802
1803 if (!info.clipBox.isEmpty()) {
1804 m_indentLevel--;
1805 stream() << "}";
1806 }
1807 }
1808
1809 return true;
1810}
1811
1812bool QQuickQmlGenerator::generateMaskNode(const MaskNodeInfo &info)
1813{
1814 if (Q_UNLIKELY(errorState()))
1815 return false;
1816
1817 // Generate an invisible item subtree which can be used in ShaderEffectSource
1818 if (info.stage == StructureNodeStage::Start) {
1819 if (info.isMaskContentRelativeCoordinates) {
1820 m_contentRelativeMasks.insert(info.id);
1821 stream() << "Item {";
1822 m_indentLevel++;
1823 stream() << "transform: Matrix4x4 {";
1824 m_indentLevel++;
1825 stream() << "matrix: Qt.matrix4x4(";
1826 m_indentLevel++;
1827 stream() << info.id << ".contentWidth, 0, 0, " << info.id << ".contentX,";
1828 stream() << "0, " << info.id << ".contentHeight, 0, " << info.id << ".contentY,";
1829 stream() << "0, 0, 1, 0,";
1830 stream() << "0, 0, 0, 1)";
1831 m_indentLevel--;
1832 m_indentLevel--;
1833 stream() << "}";
1834 }
1835 return true;
1836 }
1837
1838 if (info.stage == StructureNodeStage::End) {
1839 if (info.isMaskContentRelativeCoordinates) {
1840 m_indentLevel--;
1841 stream() << "}";
1842 }
1843
1844 // Generate code to add after defs block
1845 startDefsSuffixBlock();
1846 stream() << "Loader {";
1847 m_indentLevel++;
1848
1849 stream() << "id: " << info.id; // This is in a different scope, so we can reuse the ID
1850 stream() << "visible: false";
1851 stream() << "sourceComponent: " << info.id << "_container";
1852 stream() << "width: item !== null ? item.originalBounds.width : 0";
1853 stream() << "height: item !== null ? item.originalBounds.height : 0";
1854
1855 if (info.boundsReferenceId.isEmpty()) {
1856 stream() << "property real maskX: " << info.maskRect.left();
1857 stream() << "property real maskY: " << info.maskRect.top();
1858 stream() << "property real maskWidth: " << info.maskRect.width();
1859 stream() << "property real maskHeight: " << info.maskRect.height();
1860 }
1861
1862 if (info.isMaskContentRelativeCoordinates) {
1863 stream() << "property real contentX: 0";
1864 stream() << "property real contentY: 0";
1865 stream() << "property real contentWidth: 1";
1866 stream() << "property real contentHeight: 1";
1867 }
1868
1869 stream() << "function maskRect(otherX, otherY, otherWidth, otherHeight) {";
1870 m_indentLevel++;
1871
1872 stream() << "return ";
1873 if (!info.boundsReferenceId.isEmpty()) {
1874 stream(SameLine) << info.boundsReferenceId << ".originalBounds";
1875 } else if (info.isMaskRectRelativeCoordinates) {
1876 stream(SameLine)
1877 << "Qt.rect("
1878 << info.id << ".maskX * otherWidth + otherX,"
1879 << info.id << ".maskY * otherHeight + otherY,"
1880 << info.id << ".maskWidth * otherWidth,"
1881 << info.id << ".maskHeight * otherHeight)";
1882 } else {
1883 stream(SameLine)
1884 << "Qt.rect("
1885 << info.id << ".maskX, "
1886 << info.id << ".maskY, "
1887 << info.id << ".maskWidth, "
1888 << info.id << ".maskHeight)";
1889 }
1890
1891 m_indentLevel--;
1892 stream() << "}";
1893
1894 m_indentLevel--;
1895 stream() << "}";
1896
1897 endDefsSuffixBlock();
1898 }
1899
1900 return true;
1901}
1902
1903void QQuickQmlGenerator::generateFilterNode(const FilterNodeInfo &info)
1904{
1905 if (Q_UNLIKELY(errorState()))
1906 return;
1907
1908 stream() << "Item {";
1909 m_indentLevel++;
1910
1911 generateNodeBase(info);
1912
1913 stream() << "property real originalWidth: filterSourceItem.sourceItem.originalBounds.width";
1914 stream() << "property real originalHeight: filterSourceItem.sourceItem.originalBounds.height";
1915 stream() << "property rect filterRect: " << info.id << "_filterParameters"
1916 << ".adaptToFilterRect(0, 0, originalWidth, originalHeight)";
1917
1918 for (qsizetype i = 0; i < info.steps.size();)
1919 i = generateFilterStep(info, i);
1920
1921 // Generate code to be added after defs block
1922 startDefsSuffixBlock();
1923 stream() << "QtObject {";
1924 m_indentLevel++;
1925
1926 stream() << "id: " << info.id << "_filterParameters";
1927 stream() << "property int wrapMode: ";
1928 if (info.wrapMode == QSGTexture::Repeat)
1929 stream(SameLine) << "ShaderEffectSource.Repeat";
1930 else
1931 stream(SameLine) << "ShaderEffectSource.ClampToEdge";
1932
1933 stream() << "property rect filterRect: Qt.rect("
1934 << info.filterRect.x() << ", "
1935 << info.filterRect.y() << ", "
1936 << info.filterRect.width() << ", "
1937 << info.filterRect.height() << ")";
1938
1939 stream() << "function adaptToFilterRect(sx, sy, sw, sh) {";
1940 m_indentLevel++;
1941
1942 if (info.csFilterRect == FilterNodeInfo::CoordinateSystem::Absolute) {
1943 stream() << "return Qt.rect(filterRect.x, filterRect.y, filterRect.width, filterRect.height)";
1944 } else {
1945 stream() << "return Qt.rect(sx + sw * filterRect.x, sy + sh * filterRect.y, sw * filterRect.width, sh * filterRect.height)";
1946 }
1947
1948 m_indentLevel--;
1949 stream() << "}";
1950
1951 m_indentLevel--;
1952 stream() << "}";
1953 endDefsSuffixBlock();
1954
1955 generateNodeEnd(info);
1956}
1957
1958qsizetype QQuickQmlGenerator::generateFilterStep(const FilterNodeInfo &info,
1959 qsizetype stepIndex)
1960{
1961 const FilterNodeInfo::FilterStep &step = info.steps.at(stepIndex);
1962 const QString primitiveId = info.id + QStringLiteral("_primitive") + QString::number(stepIndex);
1963
1964 stepIndex++;
1965
1966 QString inputId = step.input1 != FilterNodeInfo::FilterInput::SourceColor
1967 ? step.namedInput1
1968 : QStringLiteral("filterSourceItem");
1969
1970 bool isComposite = false;
1971 switch (step.filterType) {
1972 case FilterNodeInfo::Type::Merge:
1973 {
1974 const int maxNodeCount = 8;
1975
1976 // Find all nodes for this merge
1977 QList<std::pair<FilterNodeInfo::FilterInput, QString> > inputs;
1978 for (; stepIndex < info.steps.size(); ++stepIndex) {
1979 const FilterNodeInfo::FilterStep &nodeStep = info.steps.at(stepIndex);
1980 if (nodeStep.filterType != FilterNodeInfo::Type::MergeNode)
1981 break;
1982
1983 inputs.emplace_back(nodeStep.input1, nodeStep.namedInput1);
1984 }
1985
1986 if (inputs.size() > maxNodeCount) {
1987 qCWarning(lcQuickVectorImage) << "Maximum of" << maxNodeCount
1988 << "nodes exceeded in merge effect.";
1989 }
1990
1991 if (inputs.isEmpty()) {
1992 qCWarning(lcQuickVectorImage) << "Merge effect requires at least one node.";
1993 break;
1994 }
1995
1996 stream() << "ShaderEffect {";
1997 m_indentLevel++;
1998
1999 stream() << "id: " << primitiveId;
2000 stream() << "visible: false";
2001
2002 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/femerge.frag.qsb\"";
2003 stream() << "width: source1.width";
2004 stream() << "height: source1.height";
2005 stream() << "property int sourceCount: " << std::min(qsizetype(8), inputs.size());
2006
2007 for (int i = 0; i < maxNodeCount; ++i) {
2008 auto input = i < inputs.size()
2009 ? inputs.at(i)
2010 : std::pair(FilterNodeInfo::FilterInput::None, u"null"_s);
2011
2012 QString inputId = input.first != FilterNodeInfo::FilterInput::SourceColor
2013 ? input.second
2014 : QStringLiteral("filterSourceItem");
2015
2016 stream() << "property var source" << (i + 1) << ": " << inputId;
2017 }
2018
2019 m_indentLevel--;
2020 stream() << "}";
2021
2022 break;
2023 }
2024 case FilterNodeInfo::Type::CompositeOver:
2025 case FilterNodeInfo::Type::CompositeOut:
2026 case FilterNodeInfo::Type::CompositeIn:
2027 case FilterNodeInfo::Type::CompositeXor:
2028 case FilterNodeInfo::Type::CompositeAtop:
2029 case FilterNodeInfo::Type::CompositeArithmetic:
2030 case FilterNodeInfo::Type::CompositeLighter:
2031 isComposite = true;
2032 Q_FALLTHROUGH();
2033
2034 case FilterNodeInfo::Type::BlendNormal:
2035 case FilterNodeInfo::Type::BlendMultiply:
2036 case FilterNodeInfo::Type::BlendScreen:
2037 case FilterNodeInfo::Type::BlendDarken:
2038 case FilterNodeInfo::Type::BlendLighten:
2039 {
2040 stream() << "ShaderEffect {";
2041 m_indentLevel++;
2042
2043 QString input2Id = step.input2 != FilterNodeInfo::FilterInput::SourceColor
2044 ? step.namedInput2
2045 : QStringLiteral("filterSourceItem");
2046
2047 stream() << "id: " << primitiveId;
2048 stream() << "visible: false";
2049
2050 QString shader;
2051 switch (step.filterType) {
2052 case FilterNodeInfo::Type::CompositeOver:
2053 shader = QStringLiteral("fecompositeover");
2054 break;
2055 case FilterNodeInfo::Type::CompositeOut:
2056 shader = QStringLiteral("fecompositeout");
2057 break;
2058 case FilterNodeInfo::Type::CompositeIn:
2059 shader = QStringLiteral("fecompositein");
2060 break;
2061 case FilterNodeInfo::Type::CompositeXor:
2062 shader = QStringLiteral("fecompositexor");
2063 break;
2064 case FilterNodeInfo::Type::CompositeAtop:
2065 shader = QStringLiteral("fecompositeatop");
2066 break;
2067 case FilterNodeInfo::Type::CompositeArithmetic:
2068 shader = QStringLiteral("fecompositearithmetic");
2069 break;
2070 case FilterNodeInfo::Type::CompositeLighter:
2071 shader = QStringLiteral("fecompositelighter");
2072 break;
2073 case FilterNodeInfo::Type::BlendNormal:
2074 shader = QStringLiteral("feblendnormal");
2075 break;
2076 case FilterNodeInfo::Type::BlendMultiply:
2077 shader = QStringLiteral("feblendmultiply");
2078 break;
2079 case FilterNodeInfo::Type::BlendScreen:
2080 shader = QStringLiteral("feblendscreen");
2081 break;
2082 case FilterNodeInfo::Type::BlendDarken:
2083 shader = QStringLiteral("feblenddarken");
2084 break;
2085 case FilterNodeInfo::Type::BlendLighten:
2086 shader = QStringLiteral("feblendlighten");
2087 break;
2088 default:
2089 Q_UNREACHABLE();
2090 }
2091
2092 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/"
2093 << shader << ".frag.qsb\"";
2094 stream() << "property var source: " << inputId;
2095 stream() << "property var source2: " << input2Id;
2096 stream() << "width: source.width";
2097 stream() << "height: source.height";
2098
2099 if (isComposite) {
2100 QVector4D k = step.filterParameter.value<QVector4D>();
2101 stream() << "property var k: Qt.vector4d("
2102 << k.x() << ", "
2103 << k.y() << ", "
2104 << k.z() << ", "
2105 << k.w() << ")";
2106 }
2107
2108 m_indentLevel--;
2109 stream() << "}";
2110
2111 break;
2112
2113 }
2114 case FilterNodeInfo::Type::Flood:
2115 {
2116 stream() << "Rectangle {";
2117 m_indentLevel++;
2118
2119 stream() << "id: " << primitiveId;
2120 stream() << "visible: false";
2121
2122 stream() << "width: " << inputId << ".width";
2123 stream() << "height: " << inputId << ".height";
2124
2125 QColor floodColor = step.filterParameter.value<QColor>();
2126 stream() << "color: \"" << floodColor.name(QColor::HexArgb) << "\"";
2127
2128 m_indentLevel--;
2129 stream() << "}";
2130
2131 break;
2132 }
2133 case FilterNodeInfo::Type::ColorMatrix:
2134 {
2135 stream() << "ShaderEffect {";
2136 m_indentLevel++;
2137
2138 stream() << "id: " << primitiveId;
2139 stream() << "visible: false";
2140
2141 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag.qsb\"";
2142 stream() << "property var source: " << inputId;
2143 stream() << "width: source.width";
2144 stream() << "height: source.height";
2145
2146 QGenericMatrix<5, 5, qreal> matrix = step.filterParameter.value<QGenericMatrix<5, 5, qreal> >();
2147 for (int row = 0; row < 4; ++row) { // Last row is ignored
2148
2149 // Qt SVG stores rows as columns, so we flip the coordinates
2150 for (int col = 0; col < 5; ++col)
2151 stream() << "property real m_" << row << "_" << col << ": " << matrix(col, row);
2152 }
2153
2154 m_indentLevel--;
2155 stream() << "}";
2156
2157 break;
2158 }
2159
2160 case FilterNodeInfo::Type::Offset:
2161 {
2162 stream() << "ShaderEffectSource {";
2163 m_indentLevel++;
2164
2165 stream() << "id: " << primitiveId;
2166 stream() << "visible: false";
2167 stream() << "sourceItem: " << inputId;
2168 stream() << "width: sourceItem.width + offset.x";
2169 stream() << "height: sourceItem.height + offset.y";
2170
2171 QVector2D offset = step.filterParameter.value<QVector2D>();
2172 stream() << "property vector2d offset: Qt.vector2d(";
2173 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute)
2174 stream(SameLine) << offset.x() << " / width, " << offset.y() << " / height)";
2175 else
2176 stream(SameLine) << offset.x() << ", " << offset.y() << ")";
2177
2178 stream() << "sourceRect: Qt.rect(-offset.x, -offset.y, width, height)";
2179
2180 stream() << "ItemSpy {";
2181 m_indentLevel++;
2182 stream() << "id: " << primitiveId << "_offset_itemspy";
2183 stream() << "anchors.fill: parent";
2184
2185 m_indentLevel--;
2186 stream() << "}";
2187 stream() << "textureSize: " << primitiveId << "_offset_itemspy.requiredTextureSize";
2188
2189
2190 m_indentLevel--;
2191 stream() << "}";
2192
2193 break;
2194 }
2195
2196 case FilterNodeInfo::Type::GaussianBlur:
2197 {
2198 // Approximate blur effect with fast blur
2199 stream() << "MultiEffect {";
2200 m_indentLevel++;
2201
2202 stream() << "id: " << primitiveId;
2203 stream() << "visible: false";
2204
2205 stream() << "source: " << inputId;
2206 stream() << "blurEnabled: true";
2207 stream() << "width: source.width";
2208 stream() << "height: source.height";
2209
2210 const qreal maxDeviation(12.0); // Decided experimentally
2211 const qreal deviation = step.filterParameter.toReal();
2212 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative)
2213 stream() << "blur: Math.min(1.0, " << deviation << " * filterSourceItem.width / " << maxDeviation << ")";
2214 else
2215 stream() << "blur: " << std::min(qreal(1.0), deviation / maxDeviation);
2216 stream() << "blurMax: 64";
2217
2218 m_indentLevel--;
2219 stream() << "}";
2220
2221 break;
2222 }
2223 default:
2224 qCWarning(lcQuickVectorImage) << "Unhandled filter type: " << int(step.filterType);
2225 // Dummy item to avoid empty component
2226 stream() << "Item { id: " << primitiveId << " }";
2227 break;
2228 }
2229
2230 // Sample correct part of primitive
2231 stream() << "ShaderEffectSource {";
2232 m_indentLevel++;
2233
2234 stream() << "id: " << step.outputName;
2235 if (stepIndex < info.steps.size())
2236 stream() << "visible: false";
2237
2238 qreal x1, x2, y1, y2;
2239 step.filterPrimitiveRect.getCoords(&x1, &y1, &x2, &y2);
2240 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute) {
2241 stream() << "property real fpx1: " << x1;
2242 stream() << "property real fpy1: " << y1;
2243 stream() << "property real fpx2: " << x2;
2244 stream() << "property real fpy2: " << y2;
2245 } else if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative) {
2246 // If they are relative, they are actually in the coordinate system
2247 // of the original bounds of the filtered item. This means we first have to convert
2248 // them to the filter's coordinate system first.
2249 stream() << "property real fpx1: " << x1 << " * filterSourceItem.sourceItem.originalBounds.width";
2250 stream() << "property real fpy1: " << y1 << " * filterSourceItem.sourceItem.originalBounds.height";
2251 stream() << "property real fpx2: " << x2 << " * filterSourceItem.sourceItem.originalBounds.width";
2252 stream() << "property real fpy2: " << y2 << " * filterSourceItem.sourceItem.originalBounds.height";
2253 } else { // Just match filter rect
2254 stream() << "property real fpx1: parent.filterRect.x";
2255 stream() << "property real fpy1: parent.filterRect.y";
2256 stream() << "property real fpx2: parent.filterRect.x + parent.filterRect.width";
2257 stream() << "property real fpy2: parent.filterRect.y + parent.filterRect.height";
2258 }
2259
2260 stream() << "sourceItem: " << primitiveId;
2261 stream() << "sourceRect: Qt.rect(fpx1 - parent.filterRect.x, fpy1 - parent.filterRect.y, width, height)";
2262
2263 stream() << "x: fpx1";
2264 stream() << "y: fpy1";
2265 stream() << "width: " << "fpx2 - fpx1";
2266 stream() << "height: " << "fpy2 - fpy1";
2267
2268 stream() << "ItemSpy {";
2269 m_indentLevel++;
2270 stream() << "id: " << primitiveId << "_itemspy";
2271 stream() << "anchors.fill: parent";
2272
2273 m_indentLevel--;
2274 stream() << "}";
2275 stream() << "textureSize: " << primitiveId << "_itemspy.requiredTextureSize";
2276
2277 m_indentLevel--;
2278 stream() << "}";
2279
2280 return stepIndex;
2281}
2282
2283void QQuickQmlGenerator::generateTimelineFields(const StructureNodeInfo &info)
2284{
2285 if (usingTimelineAnimation() && info.timelineInfo) {
2286 QString frameCounterRef = info.timelineInfo->frameCounterReference
2287 + QStringLiteral(".frameCounter");
2288
2289 if (info.timelineInfo->generateVisibility) {
2290 stream() << "visible: " << frameCounterRef << " >= " << info.timelineInfo->startFrame
2291 << " && " << frameCounterRef << " < " << info.timelineInfo->endFrame;
2292 }
2293
2294 if (info.timelineInfo->generateFrameCounter) {
2295 stream() << "property real frameCounter: ";
2296 if (info.timelineInfo->frameCounterMapper.isAnimated()) {
2297 // Animated frame counter remapping; overrides offset / multiplier
2298 stream(SameLine) << "0";
2299 generatePropertyTimeline(info.timelineInfo->frameCounterMapper, info.id, "frameCounter"_L1);
2300 } else {
2301 const auto offset = info.timelineInfo->frameCounterOffset;
2302 const auto multiplier = info.timelineInfo->frameCounterMultiplier;
2303 const bool needsParens = (offset && multiplier);
2304 if (needsParens)
2305 stream(SameLine) << "(";
2306 stream(SameLine) << frameCounterRef;
2307 if (offset)
2308 stream(SameLine) << (offset > 0 ? " + " : " - ") << qAbs(offset);
2309 if (needsParens)
2310 stream(SameLine) << ")";
2311 if (multiplier)
2312 stream(SameLine) << " * " << multiplier;
2313 }
2314 }
2315 }
2316}
2317
2318bool QQuickQmlGenerator::generatePatternNode(const PatternNodeInfo &info)
2319{
2320 if (info.stage == StructureNodeStage::Start) {
2321 return true;
2322 } else {
2323 startDefsSuffixBlock();
2324 stream() << "Loader {";
2325 m_indentLevel++;
2326
2327 stream() << "id: " << info.id; // This is in a different scope, so we can reuse the ID
2328 stream() << "sourceComponent: " << info.id << "_container";
2329 stream() << "width: item !== null ? item.originalBounds.width : 0";
2330 stream() << "height: item !== null ? item.originalBounds.height : 0";
2331 stream() << "visible: false";
2332 stream() << "function sourceRect(targetWidth, targetHeight) {";
2333 m_indentLevel++;
2334
2335 stream() << "return Qt.rect(0, 0, ";
2336 if (!info.isPatternRectRelativeCoordinates) {
2337 stream(SameLine) << info.patternRect.width() << ", "
2338 << info.patternRect.height();
2339 } else {
2340 stream(SameLine) << info.patternRect.width() << " * targetWidth, "
2341 << info.patternRect.height() << " * targetHeight";
2342 }
2343 stream(SameLine) << ")";
2344 m_indentLevel--;
2345 stream() << "}";
2346
2347 stream() << "function sourceOffset(targetWidth, targetHeight) {";
2348 m_indentLevel++;
2349
2350 stream() << "return Qt.vector3d(";
2351 if (!info.isPatternRectRelativeCoordinates) {
2352 stream(SameLine) << info.patternRect.x() << ", "
2353 << info.patternRect.y() << ", ";
2354 } else {
2355 stream(SameLine) << info.patternRect.x() << " * targetWidth, "
2356 << info.patternRect.y() << " * targetHeight, ";
2357 }
2358 stream(SameLine) << "0.0)";
2359 m_indentLevel--;
2360 stream() << "}";
2361
2362
2363 m_indentLevel--;
2364 stream() << "}";
2365
2366 endDefsSuffixBlock();
2367
2368 return true;
2369 }
2370}
2371
2372void QQuickQmlGenerator::generateDefsInstantiationNode(const StructureNodeInfo &info)
2373{
2374 if (Q_UNLIKELY(errorState()))
2375 return;
2376
2377 if (info.stage == StructureNodeStage::Start) {
2378 stream() << "Loader {";
2379 m_indentLevel++;
2380
2381 stream() << "sourceComponent: " << info.defsId << "_container";
2382 generateNodeBase(info);
2383 generateTimelineFields(info);
2384 } else {
2385 m_indentLevel--;
2386 stream() << "}";
2387 }
2388}
2389
2390bool QQuickQmlGenerator::generateMarkerNode(const MarkerNodeInfo &info)
2391{
2392 if (info.stage == StructureNodeStage::Start) {
2393 startDefsSuffixBlock();
2394 stream() << "QtObject {";
2395 m_indentLevel++;
2396
2397 stream() << "id: " << info.id << "_markerParameters";
2398
2399 stream() << "property bool startReversed: ";
2400 if (info.orientation == MarkerNodeInfo::Orientation::AutoStartReverse)
2401 stream(SameLine) << "true";
2402 else
2403 stream(SameLine) << "false";
2404
2405 stream() << "function autoAngle(adaptedAngle) {";
2406 m_indentLevel++;
2407 if (info.orientation == MarkerNodeInfo::Orientation::Value)
2408 stream() << "return " << info.angle;
2409 else
2410 stream() << "return adaptedAngle";
2411 m_indentLevel--;
2412 stream() << "}";
2413
2414 m_indentLevel--;
2415 stream() << "}";
2416 endDefsSuffixBlock();
2417
2418 if (!info.clipBox.isEmpty()) {
2419 stream() << "Item {";
2420 m_indentLevel++;
2421
2422 stream() << "x: " << info.clipBox.x();
2423 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2424 stream(SameLine) << " * strokeWidth";
2425 stream() << "y: " << info.clipBox.y();
2426 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2427 stream(SameLine) << " * strokeWidth";
2428 stream() << "width: " << info.clipBox.width();
2429 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2430 stream(SameLine) << " * strokeWidth";
2431 stream() << "height: " << info.clipBox.height();
2432 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2433 stream(SameLine) << " * strokeWidth";
2434 stream() << "clip: true";
2435 }
2436
2437 stream() << "Item {";
2438 m_indentLevel++;
2439
2440 if (!info.clipBox.isEmpty()) {
2441 stream() << "x: " << -info.clipBox.x();
2442 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2443 stream(SameLine) << " * strokeWidth";
2444 stream() << "y: " << -info.clipBox.y();
2445 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2446 stream(SameLine) << " * strokeWidth";
2447 }
2448
2449 stream() << "id: " << info.id;
2450
2451 stream() << "property real markerWidth: " << info.markerSize.width();
2452 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2453 stream(SameLine) << " * strokeWidth";
2454
2455 stream() << "property real markerHeight: " << info.markerSize.height();
2456 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2457 stream(SameLine) << " * strokeWidth";
2458
2459 stream() << "function calculateMarkerScale(w, h) {";
2460 m_indentLevel++;
2461
2462 stream() << "var scaleX = 1.0";
2463 stream() << "var scaleY = 1.0";
2464 stream() << "var offsetX = 0.0";
2465 stream() << "var offsetY = 0.0";
2466 if (info.viewBox.width() > 0)
2467 stream() << "if (w > 0) scaleX = w / " << info.viewBox.width();
2468 if (info.viewBox.height() > 0)
2469 stream() << "if (h > 0) scaleY = h / " << info.viewBox.height();
2470
2471 if (info.preserveAspectRatio & MarkerNodeInfo::xyMask) {
2472 stream() << "if (scaleX != scaleY) {";
2473 m_indentLevel++;
2474
2475 if (info.preserveAspectRatio & MarkerNodeInfo::meet)
2476 stream() << "scaleX = scaleY = Math.min(scaleX, scaleY)";
2477 else
2478 stream() << "scaleX = scaleY = Math.max(scaleX, scaleY)";
2479
2480 QString overflowX = QStringLiteral("scaleX * %1 - w").arg(info.viewBox.width());
2481 QString overflowY = QStringLiteral("scaleY * %1 - h").arg(info.viewBox.height());
2482
2483 const quint8 xRatio = info.preserveAspectRatio & MarkerNodeInfo::xMask;
2484 if (xRatio == MarkerNodeInfo::xMid)
2485 stream() << "offsetX -= " << overflowX << " / 2";
2486 else if (xRatio == MarkerNodeInfo::xMax)
2487 stream() << "offsetX -= " << overflowX;
2488
2489 const quint8 yRatio = info.preserveAspectRatio & MarkerNodeInfo::yMask;
2490 if (yRatio == MarkerNodeInfo::yMid)
2491 stream() << "offsetY -= " << overflowY << " / 2";
2492 else if (yRatio == MarkerNodeInfo::yMax)
2493 stream() << "offsetY -= " << overflowY;
2494
2495 m_indentLevel--;
2496 stream() << "}";
2497 }
2498
2499 stream() << "return Qt.vector4d("
2500 << "offsetX - " << info.anchorPoint.x() << " * scaleX, "
2501 << "offsetY - " << info.anchorPoint.y() << " * scaleY, "
2502 << "scaleX, "
2503 << "scaleY)";
2504
2505 m_indentLevel--;
2506 stream() << "}";
2507
2508 stream() << "property vector4d markerScale: calculateMarkerScale(markerWidth, markerHeight)";
2509
2510 stream() << "transform: [";
2511 m_indentLevel++;
2512
2513 stream() << "Scale { xScale: " << info.id << ".markerScale.z; yScale: " << info.id << ".markerScale.w },";
2514 stream() << "Translate { x: " << info.id << ".markerScale.x; y: " << info.id << ".markerScale.y }";
2515
2516 m_indentLevel--;
2517 stream() << "]";
2518
2519 } else {
2520 generateNodeEnd(info);
2521
2522 if (!info.clipBox.isEmpty()) {
2523 m_indentLevel--;
2524 stream() << "}";
2525 }
2526 }
2527
2528 return true;
2529}
2530
2531bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
2532{
2533 if (Q_UNLIKELY(errorState()))
2534 return false;
2535
2536 const QStringList comments = m_commentString.split(u'\n');
2537
2538 if (!isNodeVisible(info)) {
2539 m_indentLevel = 0;
2540
2541 if (comments.isEmpty()) {
2542 stream() << "// Generated from SVG";
2543 } else {
2544 for (const auto &comment : comments)
2545 stream() << "// " << comment;
2546 }
2547
2548 stream() << "import QtQuick";
2549 stream() << "import QtQuick.Shapes" << Qt::endl;
2550 stream() << "Item {";
2551 m_indentLevel++;
2552
2553 double w = info.size.width();
2554 double h = info.size.height();
2555 if (w > 0)
2556 stream() << "implicitWidth: " << w;
2557 if (h > 0)
2558 stream() << "implicitHeight: " << h;
2559
2560 m_indentLevel--;
2561 stream() << "}";
2562
2563 return false;
2564 }
2565
2566 if (info.stage == StructureNodeStage::Start) {
2567 m_indentLevel = 0;
2568
2569 if (comments.isEmpty())
2570 stream() << "// Generated from SVG";
2571 else
2572 for (const auto &comment : comments)
2573 stream() << "// " << comment;
2574
2575 stream() << "import QtQuick";
2576 stream() << "import QtQuick.VectorImage";
2577 stream() << "import QtQuick.VectorImage.Helpers";
2578 stream() << "import QtQuick.Shapes";
2579 stream() << "import QtQuick.Effects";
2580 if (usingTimelineAnimation())
2581 stream() << "import QtQuick.Timeline";
2582
2583 for (const auto &import : std::as_const(m_extraImports))
2584 stream() << "import " << import;
2585
2586 stream() << Qt::endl << "Item {";
2587 m_indentLevel++;
2588
2589 double w = info.size.width();
2590 double h = info.size.height();
2591 if (w > 0)
2592 stream() << "implicitWidth: " << w;
2593 if (h > 0)
2594 stream() << "implicitHeight: " << h;
2595
2596 if (Q_UNLIKELY(!isRuntimeGenerator())) {
2597 stream() << "component AnimationsInfo : QtObject";
2598 stream() << "{";
2599 m_indentLevel++;
2600 }
2601
2602 stream() << "property bool paused: false";
2603 stream() << "property int loops: 1";
2604 stream() << "signal restart()";
2605
2606 if (Q_UNLIKELY(!isRuntimeGenerator())) {
2607 m_indentLevel--;
2608 stream() << "}";
2609 stream() << "property AnimationsInfo animations : AnimationsInfo {}";
2610 }
2611
2612 stream() << "Item {";
2613 m_indentLevel++;
2614 stream() << "width: 1";
2615 stream() << "height: 1";
2616
2617 stream() << "ItemSpy { id: __qt_toplevel_scale_itemspy; anchors.fill: parent }";
2618
2619 m_indentLevel--;
2620 stream() << "}";
2621
2622 if (!info.viewBox.isEmpty()) {
2623 stream() << "transform: [";
2624 m_indentLevel++;
2625 bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y());
2626 if (translate)
2627 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
2628 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
2629 m_indentLevel--;
2630 stream() << "]";
2631 }
2632
2633 if (!info.forceSeparatePaths && info.isPathContainer) {
2634 m_topLevelIdString = QStringLiteral("__qt_toplevel");
2635 stream() << "id: " << m_topLevelIdString;
2636
2637 generatePathContainer(info);
2638 m_indentLevel++;
2639
2640 generateNodeBase(info);
2641 } else {
2642 m_topLevelIdString = generateNodeBase(info);
2643 if (m_topLevelIdString.isEmpty())
2644 qCWarning(lcQuickVectorImage) << "No ID specified for top level item";
2645 }
2646
2647 if (usingTimelineAnimation() && info.timelineInfo) {
2648 stream() << "property real startFrame: " << info.timelineInfo->startFrame;
2649 stream() << "property real endFrame: " << info.timelineInfo->endFrame;
2650 stream() << "property real frameRate: " << info.timelineInfo->frameRate;
2651 stream() << "property real frameCounter: " << info.timelineInfo->startFrame;
2652 if (!info.timelineInfo->markers.isEmpty()) {
2653 QStringList markerEntries;
2654 markerEntries.reserve(info.timelineInfo->markers.size());
2655 for (const LottieMarkerInfo &marker : info.timelineInfo->markers) {
2656 markerEntries << u'"' + sanitizeString(marker.name) + u"\": ["
2657 + QString::number(marker.frame) + u", "
2658 + QString::number(marker.duration) + u']';
2659 }
2660 stream() << "property var markers: ({" << markerEntries.join(", "_L1) << "})";
2661 }
2662 stream() << "NumberAnimation on frameCounter {";
2663 m_indentLevel++;
2664 stream() << "objectName: \"_qt_frameCounterAnimation\"";
2665 stream() << "from: " << m_topLevelIdString << ".startFrame";
2666 stream() << "to: " << m_topLevelIdString << ".endFrame";
2667 stream() << "duration: " << processAnimationTime(1000) << " * Math.abs(to - from) / "
2668 << "Math.max(" << m_topLevelIdString << ".frameRate, 1)";
2669 generateAnimationBindings();
2670 m_indentLevel--;
2671 stream() << "}";
2672 stream() << "visible: frameCounter >= " << info.timelineInfo->startFrame
2673 << " && frameCounter < " << info.timelineInfo->endFrame;
2674 }
2675 } else {
2676 if (m_inShapeItemLevel > 0) {
2677 m_inShapeItemLevel--;
2678 m_indentLevel--;
2679 stream() << "}";
2680 }
2681
2682 for (const auto [coords, id] : m_easings.asKeyValueRange()) {
2683 stream() << "readonly property easingCurve " << id << ": ({ type: Easing.BezierSpline, bezierCurve: [ ";
2684 for (auto coord : coords)
2685 stream(SameLine) << coord << ", ";
2686 stream(SameLine) << "1, 1 ] })";
2687 }
2688
2689 generateNodeEnd(info);
2690 stream().flush();
2691 }
2692
2693 return true;
2694}
2695
2696void QQuickQmlGenerator::startDefsSuffixBlock()
2697{
2698 int tmp = m_oldIndentLevels.top();
2699 m_oldIndentLevels.push(m_indentLevel);
2700 m_indentLevel = tmp;
2701 m_stream.setString(&m_defsSuffix);
2702}
2703
2704void QQuickQmlGenerator::endDefsSuffixBlock()
2705{
2706 m_indentLevel = m_oldIndentLevels.pop();
2707 m_stream.setDevice(&m_result);
2708}
2709
2710QStringView QQuickQmlGenerator::indent()
2711{
2712 int indentWidth = m_indentLevel * 4;
2713 if (indentWidth > m_indentString.size())
2714 m_indentString.fill(QLatin1Char(' '), indentWidth * 2);
2715 return QStringView(m_indentString).first(indentWidth);
2716}
2717
2718QTextStream &QQuickQmlGenerator::stream(int flags)
2719{
2720 if (m_stream.device() == nullptr && m_stream.string() == nullptr)
2721 m_stream.setDevice(&m_result);
2722 else if (!(flags & StreamFlags::SameLine))
2723 m_stream << Qt::endl << indent();
2724
2725 static qint64 maxBufferSize = qEnvironmentVariableIntegerValue("QT_QUICKVECTORIMAGE_MAX_BUFFER").value_or(64 << 20); // 64MB
2726 if (m_stream.device()) {
2727 if (Q_UNLIKELY(!checkSanityLimit(m_stream.device()->size(), maxBufferSize, "buffer size"_L1)))
2728 m_stream.device()->reset();
2729 } else {
2730 if (Q_UNLIKELY(!checkSanityLimit(m_stream.string()->size(), maxBufferSize, "buffer string size"_L1)))
2731 m_stream.string()->clear();
2732 }
2733
2734 return m_stream;
2735}
2736
2737const char *QQuickQmlGenerator::shapeName() const
2738{
2739 return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData();
2740}
2741
2742QT_END_NAMESPACE
Combined button and popup list for selecting options.
static QString sanitizeString(const QString &input)
static int processAnimationTime(int time)