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